Android OpenGLES3.0 入門教程(一)——Hello Word

簡(jiǎn)述

最近一段時(shí)間由于項(xiàng)目上用到了濾鏡功能,所以一直都在學(xué)習(xí)opengles的相關(guān)知識(shí)宾巍。opengles是opengl的一個(gè)子集倒堕,是opengl針對(duì)移動(dòng)端的版本闽巩。Android手機(jī)上現(xiàn)在最多的版本應(yīng)該是opengles3.0+了框冀,不過(guò)通過(guò)前段時(shí)間的學(xué)習(xí)流椒,發(fā)現(xiàn)網(wǎng)上的教程大多數(shù)還是2.0的版本,3.0的資料不僅少明也,而且大多數(shù)只是opengles3.0的基礎(chǔ)功能宣虾。出于這個(gè)原因,嘗試寫幾篇基于OpenGLES3.0的系列博客温数。最終目標(biāo)是從零到可以自己編寫實(shí)現(xiàn)一個(gè)視頻濾鏡應(yīng)用绣硝。前半部分介紹基本的OpenGLES3.0 的基礎(chǔ)知識(shí),后半部分介紹基于OpenGL的視頻濾鏡制作撑刺。本系列博客側(cè)重用代碼說(shuō)話鹉胖,設(shè)計(jì)到的原理部分可能寫的比較粗淺,畢竟水平有限??
在這推薦兩本相關(guān)書籍:

《OPENGL ES 3.0編程指南》

《OpenGLES應(yīng)用開發(fā)實(shí)踐指南Android卷》

第一本以概念講解為主,第二本以實(shí)際應(yīng)用為主甫菠,不過(guò)第二本使用的2.0版本败许。

Hello Word

大家都知道每一門編程語(yǔ)言都有一個(gè)Hello Word,說(shuō)白了就是在控制臺(tái)輸出一個(gè)字符串淑蔚。那么OpenGL(下面OpenGL 代指OpenGLES3.0)的Hello Word應(yīng)該是什么呢。其實(shí)在OpenGL的世界里邊是沒有控制臺(tái)的愕撰,可以輸出可觀察信息的只有繪制表面也刹衫,并且OpenGL不能輸出字符,只能輸出三種基本圖元:點(diǎn)搞挣、線带迟、三角形,OpenGL繪制出來(lái)的所有東西都是由這三種基本元素組成囱桨,和Android的canvas api有些類似只不過(guò)canvas支持的圖元更多一些仓犬。對(duì)應(yīng)編程語(yǔ)言的Hello Word,咱們就繪制一個(gè)最簡(jiǎn)單的點(diǎn)舍肠。
想在Android系統(tǒng)里邊使用OpenGL首先需要一個(gè)繪制表面搀继,也就是Android里邊的一個(gè)View來(lái)承載OpenGL繪制出來(lái)的界面。Android里邊有一種專門用來(lái)處理OpenGL的View ——GLSurfaceView〈溆铮現(xiàn)在咱們就用GLSurfaceView+OpenGL來(lái)繪制一個(gè)點(diǎn)叽躯。
簡(jiǎn)單介紹一下,GLSurfaceView繼承于SurfaceView肌括,不同的是GLSurfaceView會(huì)幫你初始化一個(gè)OpenGLES的環(huán)境点骑,所以GLSurfaceView能辦到的事SurfaceView其實(shí)也能辦到的,只不過(guò)需要自己額外的初始化一個(gè)OpenGl的環(huán)境谍夭。

OK黑滴,直接上代碼

class MainActivity : AppCompatActivity() {
    lateinit var rootLayout: RelativeLayout
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        rootLayout = findViewById(R.id.root)
        val activityManager =getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
        val configurationInfo = activityManager.deviceConfigurationInfo
        val supportsEs3 = configurationInfo.reqGlEsVersion >= 0x30000
        val layoutParams: RelativeLayout.LayoutParams = RelativeLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)
        val glSurfaceView = GLSurfaceView(this)
        if (supportsEs3) {
            glSurfaceView.setEGLContextClientVersion(3)
        }
        rootLayout.addView(glSurfaceView, layoutParams)
        glSurfaceView.setRenderer(PointRenderer(this))
    }
}

class PointRenderer(var context: Context) : GLSurfaceView.Renderer {
    var pointProgram = -1
    var vertexBuffer: FloatBuffer
    var avPosition = -1
    private val POSITION_VERTEX = floatArrayOf(
        0.0f, 0.0f, 0.0f
    )

    init {
        vertexBuffer = ByteBuffer.allocateDirect(POSITION_VERTEX.size * 4)
            .order(ByteOrder.nativeOrder())
            .asFloatBuffer()
            .put(POSITION_VERTEX)
        vertexBuffer.position(0)
    }

    override fun onDrawFrame(gl: GL10?) {
        GLES30.glClear(GLES30.GL_COLOR_BUFFER_BIT)
        GLES30.glUseProgram(pointProgram)
        GLES30.glEnableVertexAttribArray(avPosition)
        GLES30.glVertexAttribPointer(avPosition, 3, GLES30.GL_FLOAT, false, 0, vertexBuffer)
        GLES30.glDrawArrays(GLES30.GL_POINTS, 0, 1)
        GLES30.glDisableVertexAttribArray(avPosition)
    }

    override fun onSurfaceChanged(gl: GL10?, width: Int, height: Int) {
        GLES30.glViewport(0, 0, width, height)
    }

    override fun onSurfaceCreated(gl: GL10?, config: EGLConfig?) {
        pointProgram = ShaderUtil.loadProgramFromAssets(
            "vertex_point.glsl",
            "frag_point.glsl",
            context.resources
        )
        avPosition = GLES30.glGetAttribLocation(pointProgram, "av_Position")
    }
}

object ShaderUtil {
    fun loadShader(
        shaderType: Int,
        source: String?
    ): Int {
        var shader = GLES30.glCreateShader(shaderType)
        if (shader != 0) {
            GLES30.glShaderSource(shader, source)
            GLES30.glCompileShader(shader)
            checkGLError("glCompileShader")
            val compiled = IntArray(1)
            GLES30.glGetShaderiv(shader, GLES30.GL_COMPILE_STATUS, compiled, 0)
            if (compiled[0] == 0) {
                Log.e("ES30_ERROR", "Could not compile shader $shaderType:")
                Log.e("ES30_ERROR", "ERROR: " + GLES30.glGetShaderInfoLog(shader))
                GLES30.glDeleteShader(shader)
                shader = 0
            }
        } else {
            Log.e(
                "ES30_ERROR", "Could not Create shader " + shaderType + ":" +
                        "Error:" + shader
            )
        }
        return shader
    }

    fun loadProgramFromAssets(
        VShaderName: String?,
        FShaderName: String?,
        resources: Resources
    ): Int {
        val vertexText = loadFromAssetsFile(VShaderName, resources)
        val fragmentText = loadFromAssetsFile(FShaderName, resources)
        return createProgram(vertexText, fragmentText)
    }


    fun checkGLError(op: String) {
        var error: Int
        while (GLES30.glGetError().also { error = it } != GLES30.GL_NO_ERROR) {
            Log.e("ES30_ERROR", "$op: glError $error")
            throw RuntimeException("$op: glError $error")
        }
    }

    fun createProgram(
        vertexSource: String?,
        fragmentSource: String?
    ): Int {
        val vertexShader = loadShader(GLES30.GL_VERTEX_SHADER, vertexSource)
        if (vertexShader == 0) {
            return 0
        }
        val fragShader = loadShader(GLES30.GL_FRAGMENT_SHADER, fragmentSource)
        if (fragShader == 0) {
            return 0
        }
        var program = GLES30.glCreateProgram()
        if (program != 0) {
            GLES30.glAttachShader(program, vertexShader)
            checkGLError("glAttachShader")
            GLES30.glAttachShader(program, fragShader)
            checkGLError("glAttachShader")
            GLES30.glLinkProgram(program)
            val linkStatus = IntArray(1)
            GLES30.glGetProgramiv(program, GLES30.GL_LINK_STATUS, linkStatus, 0)
            if (linkStatus[0] != GLES30.GL_TRUE) {
                Log.e("ES30_ERROR", "ERROR: " + GLES30.glGetProgramInfoLog(program))
                GLES30.glDeleteProgram(program)
                program = 0
            }
        } else {
            Log.e("ES30_ERROR", "glCreateProgram Failed: $program")
        }
        return program
    }

    fun loadFromAssetsFile(
        fileName: String?,
        resources: Resources
    ): String? {
        var result: String? = null
        try {
            val inputStream = resources.assets.open(fileName!!)
            var ch = 0
            val baos = ByteArrayOutputStream()
            while (inputStream.read().also { ch = it } != -1) {
                baos.write(ch)
            }
            val buffer = baos.toByteArray()
            baos.close()
            inputStream.close()
            result = String(buffer)
            result = result.replace("\\r\\n".toRegex(), "\n")
        } catch (e: Exception) {
            e.printStackTrace()
        }
        return result
    }
}

然后是著色器的代碼,頂點(diǎn)著色器:

#version 300 es
layout (location = 0) in vec4 av_Position;
void main() {
    gl_Position  = av_Position;
    gl_PointSize = 10.0;
}

片段著色器

#version 300 es
precision mediump float;
out vec4 fragColor;
void main() {
    fragColor =vec4(1.0,0.0,0.0,1.0);
}

這兩個(gè)著色器分別對(duì)應(yīng)著Render里邊加載的 "vertex_point.glsl","frag_point.glsl",這兩個(gè)資源文件紧索。好了現(xiàn)在就可以連上手機(jī)RUN一下了袁辈。效果圖如下:


one_point.jpg

這就是咱們的第一個(gè)opengles3.0(后續(xù)簡(jiǎn)稱gl)demo,內(nèi)容則是在gl3的坐標(biāo)體系的(0齐板,0吵瞻,0)處繪制一個(gè)POINT,顏色為紅色甘磨。對(duì)于Android開發(fā)的同學(xué)來(lái)說(shuō)這里邊存在一個(gè)和平時(shí)開發(fā)習(xí)慣不太一樣的地方橡羞,首先gl的坐標(biāo)默認(rèn)都是三維坐標(biāo),也就是xyz坐標(biāo)济舆,并且坐標(biāo)原點(diǎn)也不太一樣卿泽,Android的坐標(biāo)原點(diǎn)是坐上角,但是gl的坐標(biāo)體系的坐標(biāo)原點(diǎn)是屏幕中點(diǎn),并且gl系統(tǒng)中不止一套坐標(biāo)系签夭。后面的章節(jié)會(huì)給大家詳細(xì)的講解gl的坐標(biāo)系統(tǒng)齐邦。

hello world 跑完之后,咱們重新看代碼第租,梳理一遍gl的使用流程:

  1. MainActivity中開啟了opengles3.0的功能措拇,并且生成了一個(gè)glSurfaceView。
  2. 給glSurfaceView設(shè)置一個(gè)實(shí)現(xiàn)了GLSurfaceView.Renderer接口的Render對(duì)象慎宾。
  3. 在Render的onSurfaceCreated方法也就是gl環(huán)境創(chuàng)建完成時(shí)的回調(diào)中編譯gl的著色器程序丐吓。
  4. 在onSurfaceChanged回調(diào)中設(shè)置gl繪制區(qū)域在Android屏幕上的區(qū)域大小。
  5. 在onDrawFrame中繪制每一幀需要閑置的圖像趟据。

下邊著重介紹一下gl著色器的編譯和繪制過(guò)程券犁。

編譯著色器

那么著色器是個(gè)什么東西呢,大部分資料都會(huì)告訴你“著色器是用來(lái)實(shí)現(xiàn)圖像渲染的用來(lái)替代固定渲染管線的可編輯程序”汹碱。


wenhao.jpeg

WTF! 我第一次讀的時(shí)候斷句我都斷不清楚粘衬。本質(zhì)上著色器是個(gè)“可編輯程序”,作用是“用來(lái)實(shí)現(xiàn)圖像渲染”并且“用來(lái)替代固定渲染管線”咳促。好了斷句斷清楚了稚新,但是并沒有影響我看不懂這堆鬼玩意。有同感的同學(xué)請(qǐng)扣個(gè)1跪腹。

毫不夸張枷莉,以上就是我第一次接觸著色器時(shí)候內(nèi)心想法。經(jīng)過(guò)長(zhǎng)時(shí)間接觸著色器尺迂,現(xiàn)在大致上有了一些自己的理解笤妙,其實(shí)看不懂上面的話很正常,因?yàn)槟歉揪筒皇敲嫦駻ndroid工程師的解釋噪裕,更像是給一些有過(guò)計(jì)算機(jī)圖形學(xué)經(jīng)驗(yàn)的人看的蹲盘。經(jīng)過(guò)我自己的摸索,從一個(gè)Android工程師的角度來(lái)看這個(gè)著色器更像是兩個(gè)回調(diào)函數(shù)膳音,頂點(diǎn)著色器是gl在確認(rèn)繪制圖像的邊緣頂點(diǎn)位置時(shí)的一個(gè)回調(diào)函數(shù)召衔,片段著色器則可以看成gl再確認(rèn)每個(gè)像素顏色時(shí)的一個(gè)回調(diào)函數(shù)(這么說(shuō)并不準(zhǔn)確,因?yàn)間l內(nèi)部會(huì)做優(yōu)化祭陷,并不會(huì)保證每個(gè)像素都會(huì)有回調(diào)苍凛,更準(zhǔn)確的說(shuō)法是確認(rèn)每個(gè)“片段”顏色時(shí)候的回調(diào)函數(shù),所以叫片段著色器)兵志。

就像上邊demo代碼醇蝴,咱們一共繪制了一個(gè)點(diǎn),gl知道咱們只有一個(gè)頂點(diǎn)想罕,要確認(rèn)這個(gè)頂點(diǎn)位置的時(shí)候就會(huì)執(zhí)行頂點(diǎn)著色器的代碼:

#version 300 es
layout (location = 0) in vec4 av_Position;
void main() {
    gl_Position  = av_Position;
    gl_PointSize = 10.0;
}

gl_Position是頂點(diǎn)著色器的內(nèi)置變量悠栓,av_Position是從Android環(huán)境中傳下來(lái)的參數(shù),這個(gè)頂點(diǎn)著色器其實(shí)就是把Android傳下來(lái)的位置坐標(biāo)參數(shù),賦值給了gl_Position變量惭适,從而gl知道了咱們要繪制的點(diǎn)的位置笙瑟。

確認(rèn)位置后gl還需要知道點(diǎn)的顏色,這個(gè)時(shí)候就會(huì)執(zhí)行片段著色器

#version 300 es
precision mediump float;
out vec4 fragColor;
void main() {
    fragColor =vec4(1.0,0.0,0.0,1.0);
}

因?yàn)樵蹅兝L制的是一個(gè)點(diǎn)癞志,可以看成片段著色器只調(diào)用了一次往枷。fragColor則是描述該片段的輸出顏色(與定點(diǎn)著色器不同,fragColor并不是內(nèi)置變量凄杯,而是咱們自己聲明的)师溅。片段著色器中并沒有Android環(huán)境傳下來(lái)的參數(shù),而是直接將一個(gè)固定顏色紅色賦值給了fragColor變量盾舌,所以咱們會(huì)繪制出來(lái)一個(gè)紅色的點(diǎn)。

以上就是著色器的大致作用蘸鲸,后續(xù)章節(jié)還會(huì)更深入的去講著色器相關(guān)知識(shí)的妖谴,現(xiàn)在只需要對(duì)著色器有一個(gè)大致的了解就行了。
編譯著色器的流程也不復(fù)雜酌摇,主要流程就是

  1. glCreateShader gl生成一個(gè)空的著色器程序膝舅,需要指定類型(頂點(diǎn)著色器,片段著色器)并返回著色器索引
  2. glShaderSource gl載入著色器源碼窑多,需要輸入1中生成的著色器索引仍稀,以及著色器源碼
  3. glCompileShader gl編譯著色器,需要輸入1中生成的著色器索引
  4. glGetShaderiv gl檢查編譯結(jié)果(不會(huì)影響編譯結(jié)果埂息,一般用來(lái)輔助查錯(cuò))
  5. 重復(fù)上述1—4步驟技潘,生成另外一個(gè)著色器
  6. 現(xiàn)在已經(jīng)有了兩個(gè)著色器程序,需要做的就是連接兩個(gè)著色器了
  7. glCreateProgram 創(chuàng)建gl程序(也就是兩個(gè)著色器鏈接成功之后的完整的gl程序)千康,會(huì)返回程序索引
  8. glAttachShader 給7創(chuàng)建的程序添加著色器程序享幽,需要調(diào)用兩次 分別添加頂點(diǎn)著色器和片段著色器
  9. glLinkProgram 鏈接兩個(gè)著色器
  10. glGetProgramiv gl檢查鏈接結(jié)果,類似第4步

以上就是編譯著色器的整個(gè)流程拾弃,編譯完成之后我們會(huì)持有一個(gè)gl程序(Program)的索引值桩。

OPENGLES3.0的繪制

gl的繪制過(guò)程是在Render的onDrawFrame回調(diào)里邊處理的,沒回調(diào)一次代表著屏幕要刷新一幀畫面豪椿。這個(gè)直接上代碼

override fun onDrawFrame(gl: GL10?) {
    //用預(yù)制的值來(lái)清空緩沖區(qū)
    GLES30.glClear(GLES30.GL_COLOR_BUFFER_BIT)
    //啟用pointProgram gl程序
    GLES30.glUseProgram(pointProgram)
    //啟用頂點(diǎn)屬性 avPosition
    GLES30.glEnableVertexAttribArray(avPosition)
    //用頂點(diǎn)數(shù)組給avPosition 
    GLES30.glVertexAttribPointer(avPosition , 3, GLES30.GL_FLOAT, false, 0, vertexBuffer)
    //繪制圖元
    GLES30.glDrawArrays(GLES30.GL_POINTS, 0, 1)
    //關(guān)閉頂點(diǎn)屬性 avPosition
    GLES30.glDisableVertexAttribArray(avPosition)
}

最核心的流程其實(shí)就是給gl傳遞參數(shù)奔坟,然后進(jìn)行繪制。

一個(gè)gl環(huán)境中是可以有多個(gè)gl程序的搭盾,然后使用glUseProgram 來(lái)確定當(dāng)前使用的gl程序咳秉,當(dāng)gl環(huán)境中只會(huì)使用一個(gè)gl程序時(shí),可以在onSurfaceCreated中調(diào)用一次就可以了鸯隅。

詳細(xì)說(shuō)一下傳遞參數(shù)這一塊滴某。

#version 300 es
layout (location = 0) in vec4 av_Position;
void main() {
    gl_Position  = av_Position;
    gl_PointSize = 10.0;
}

Android環(huán)境中的int值avPosition就是頂點(diǎn)著色器av_Position變量在Android中的索引值。

 GLES30.glVertexAttribPointer(avPosition , 3, GLES30.GL_FLOAT, false, 0, vertexBuffer)

這行代碼就是給av_Position賦值的代碼。但是現(xiàn)在咱們只有一個(gè)頂點(diǎn)霎奢,也就是頂點(diǎn)著色器只會(huì)調(diào)用一次户誓。很明顯av_Position的值只有一個(gè)。假如咱們要繪制兩個(gè)頂點(diǎn)(0幕侠,0帝美,0)(0,1晤硕,0)這個(gè)時(shí)候頂點(diǎn)著色器就會(huì)調(diào)用兩次悼潭。這個(gè)時(shí)候你可能會(huì)考慮一下Android在給gl的頂點(diǎn)著色器傳遞坐標(biāo)參數(shù)的時(shí)候怎么在兩次調(diào)用著色器的時(shí)候把兩個(gè)坐標(biāo)分別傳入呢?glVertexAttribPointer api中并不會(huì)設(shè)置你的值是傳給第幾次調(diào)用的頂點(diǎn)著色器的舞箍。連著調(diào)用兩次嗎舰褪?并不是,如果是這樣那我有100個(gè)頂點(diǎn)豈不是要調(diào)用100 次api疏橄,那就太麻煩了(其實(shí)OPENGL完整版的確是有類似的api的OPENGLES作為閹割版舍去了這些低效的api)占拍。這個(gè)時(shí)候就需要了解gl中頂點(diǎn)數(shù)組的概念了,正是使用了頂點(diǎn)數(shù)組捎迫,才可以讓我們一次性的在多次頂點(diǎn)著色器的調(diào)用中晃酒,準(zhǔn)確的把不同的頂點(diǎn)參數(shù)值傳遞到每次調(diào)用的頂點(diǎn)著色器(其實(shí)頂點(diǎn)著色器就那一個(gè),只不過(guò)每次調(diào)用都會(huì)關(guān)聯(lián)一個(gè)新的頂點(diǎn)窄绒,所以更準(zhǔn)確的說(shuō)頂點(diǎn)數(shù)組的作用就是把多個(gè)頂點(diǎn)的參數(shù)正確的傳給對(duì)應(yīng)的多個(gè)頂點(diǎn)??????好繞)贝次。

可能看了上邊的入門介紹,還是有點(diǎn)云里霧里彰导,很正常蛔翅。我第一次也是嘛玩意都沒看懂。主要是因?yàn)楝F(xiàn)在很多教程都是上來(lái)就給你講功能位谋,你完全不知道這個(gè)功能是是解決什么問(wèn)題的搁宾。就像你只知道glVertexAttribPointer 是賦值api,但是不知道為什么這么設(shè)計(jì)這個(gè)api倔幼。所以在這個(gè)系列的文章中我會(huì)盡量先描述問(wèn)題盖腿,再由問(wèn)題引申出來(lái)gl的相應(yīng)方案,感覺這樣理解起來(lái)會(huì)輕松很多的损同。

好了這就是第一篇的所有內(nèi)容翩腐,又看不明白的地方不用怕,后續(xù)的文章會(huì)把坑慢慢填回來(lái)的膏燃。后續(xù)會(huì)介紹著色器的基本語(yǔ)法茂卦,以及應(yīng)用層給著色器傳值的各種方式。并且會(huì)嘗試?yán)L制更多的圖形组哩。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末等龙,一起剝皮案震驚了整個(gè)濱河市处渣,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌蛛砰,老刑警劉巖罐栈,帶你破解...
    沈念sama閱讀 219,427評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異泥畅,居然都是意外死亡荠诬,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,551評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門位仁,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)柑贞,“玉大人,你說(shuō)我怎么就攤上這事聂抢【唬” “怎么了?”我有些...
    開封第一講書人閱讀 165,747評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵琳疏,是天一觀的道長(zhǎng)有决。 經(jīng)常有香客問(wèn)我,道長(zhǎng)轿亮,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,939評(píng)論 1 295
  • 正文 為了忘掉前任胸墙,我火速辦了婚禮我注,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘迟隅。我一直安慰自己但骨,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,955評(píng)論 6 392
  • 文/花漫 我一把揭開白布智袭。 她就那樣靜靜地躺著奔缠,像睡著了一般。 火紅的嫁衣襯著肌膚如雪吼野。 梳的紋絲不亂的頭發(fā)上校哎,一...
    開封第一講書人閱讀 51,737評(píng)論 1 305
  • 那天,我揣著相機(jī)與錄音瞳步,去河邊找鬼闷哆。 笑死,一個(gè)胖子當(dāng)著我的面吹牛单起,可吹牛的內(nèi)容都是我干的抱怔。 我是一名探鬼主播,決...
    沈念sama閱讀 40,448評(píng)論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼嘀倒,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼屈留!你這毒婦竟也來(lái)了局冰?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,352評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤灌危,失蹤者是張志新(化名)和其女友劉穎康二,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體乍狐,經(jīng)...
    沈念sama閱讀 45,834評(píng)論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡赠摇,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,992評(píng)論 3 338
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了浅蚪。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片藕帜。...
    茶點(diǎn)故事閱讀 40,133評(píng)論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖惜傲,靈堂內(nèi)的尸體忽然破棺而出洽故,到底是詐尸還是另有隱情,我是刑警寧澤盗誊,帶...
    沈念sama閱讀 35,815評(píng)論 5 346
  • 正文 年R本政府宣布时甚,位于F島的核電站,受9級(jí)特大地震影響哈踱,放射性物質(zhì)發(fā)生泄漏荒适。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,477評(píng)論 3 331
  • 文/蒙蒙 一开镣、第九天 我趴在偏房一處隱蔽的房頂上張望刀诬。 院中可真熱鬧,春花似錦邪财、人聲如沸陕壹。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,022評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)糠馆。三九已至,卻和暖如春怎憋,著一層夾襖步出監(jiān)牢的瞬間又碌,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,147評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工绊袋, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留赠橙,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,398評(píng)論 3 373
  • 正文 我出身青樓愤炸,卻偏偏與公主長(zhǎng)得像期揪,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子规个,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,077評(píng)論 2 355