如何在Android上渲染VR場景——GvrView

前言

前幾章我們簡單介紹了一下如何通過Google提供的SDK來展示全景圖和VR視頻受楼。這章節(jié)我們來介紹如何手動渲染VR場景,主要涉及兩個重要的類:GvrActivity和GvrView。

GvrActivity

先依賴庫

implementation 'com.google.vr:sdk-base:1.180.0'

GvrActivity提供了VR相關(guān)的一些細節(jié)膳凝,通過代碼可以看到,它持有一個GvrView對象恭陡,但是需要我們手動set才可以蹬音,GvrActivity處理了一些相關(guān)事件以及管理GvrView的生命周期等等。

所以我們需要先創(chuàng)建一個繼承GvrActivity休玩,然后通過setGvrView(GvrView gvrView)函數(shù)將GvrView對象賦給它著淆,這樣有些事件,比如GvrView的生命周期管理等就不需要我們再操心了拴疤。

GvrView

GvrView才是用來渲染的永部,我們在這個View上渲染VR場景或組件。它有兩個接口呐矾,如下:

public interface StereoRenderer {
    @UsedByNative
    void onNewFrame(HeadTransform headTransform);

    @UsedByNative
    void onDrawEye(Eye eye);

    @UsedByNative
    void onFinishFrame(Viewport viewport);

    void onSurfaceChanged(int width, int height);

    void onSurfaceCreated(EGLConfig config);

    void onRendererShutdown();
}

public interface Renderer {
    @UsedByNative
    void onDrawFrame(HeadTransform headTransform, Eye leftEye, Eye rightEye);

    @UsedByNative
    void onFinishFrame(Viewport viewport);

    void onSurfaceChanged(int width, int height);

    void onSurfaceCreated(EGLConfig config);

    void onRendererShutdown();
}

這里注意幾個重要的函數(shù):

  • onSurfaceCreated:這里我們可以進行一些初始化操作苔埋。比如初始化材料,創(chuàng)建Buffer蜒犯,加載著色器等等
  • onNewFrame:StereoRenderer獨有的组橄,在繪制一幀畫面前做一些準備工作。
  • onDrawEye/onDrawFrame:在這里我們進行繪制工作罚随。

下面我們用一個簡單的demo來看看GvrActivity和GvrView如何使用玉工。

簡單demo

源碼如下:

import android.os.Bundle
import com.google.vr.sdk.base.*
import com.huichongzi.vrardemo.databinding.ActivityGvrDemoBinding
import javax.microedition.khronos.egl.EGLConfig
import android.opengl.GLES30

class GvrDemoActivity : GvrActivity(), GvrView.StereoRenderer {

    private var _binding : ActivityGvrDemoBinding? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        _binding = ActivityGvrDemoBinding.inflate(layoutInflater)
        setContentView(_binding?.root)

        //初始化
        gvrView = _binding?.gvrView
        gvrView.setEGLConfigChooser(8,8,8,8,16,8)
        gvrView.setRenderer(this)
        gvrView.setTransitionViewEnabled(true)
        gvrView.enableCardboardTriggerEmulation()
    }

    override fun onNewFrame(headTransform: HeadTransform?) {

    }

    override fun onDrawEye(eye: Eye?) {
        GLES30.glEnable(GLES30.GL_DEPTH_TEST) //啟用深度測試,自動隱藏被遮住的材料
        //清除緩沖區(qū)淘菩,即將緩沖區(qū)設(shè)置為glClearColor設(shè)定的顏色
        GLES30.glClear(GLES30.GL_COLOR_BUFFER_BIT or GLES30.GL_DEPTH_BUFFER_BIT)
    }

    override fun onFinishFrame(viewport: Viewport?) {
    }

    override fun onSurfaceChanged(width: Int, height: Int) {

    }

    override fun onSurfaceCreated(config: EGLConfig?) {
        //設(shè)置清除顏色遵班,即背景色
        GLES30.glClearColor(0f,0f,1f,1f)
    }

    override fun onRendererShutdown() {
    }

    override fun onBackPressed() {
        finish()
    }
}

布局很簡單,只有一個GvrView瞄勾。

我們這個Activity實現(xiàn)了GvrView.StereoRenderer接口费奸。在onCreate中將GvrView實例set一下,并且通過setRenderer函數(shù)將GvrView.StereoRenderer關(guān)聯(lián)GvrView进陡。然后我們在GvrView.StereoRenderer的對應(yīng)函數(shù)中實現(xiàn)渲染即可。

在這個demo中微服,我們只是繪制了一個藍色背景色趾疚。在onSurfaceCreated中設(shè)置清除顏色(背景色),然后在onDrawEye中清除緩沖區(qū)(用剛才設(shè)置的顏色)以蕴,這樣就繪制了藍色的背景色糙麦,運行結(jié)果如下:

image.png

這樣我們就可以看到背景色了,下一步我們就可以繪制一些物體丛肮。

OpenGL繪制三角形

我們先來復(fù)習一下如何用OpenGL來繪制圖形赡磅。OpenGL繪制的基本單元有點、線和三角形宝与,其他所有的圖形實際上都是由三角形組成的焚廊。所以我們來看看如何用OpenGL繪制三角形冶匹。

由于OpenGL相關(guān)知識過于龐大,這里就不詳細解釋太多咆瘟,大家自行查閱OpenGL資料嚼隘。

著色器

首先我們需要創(chuàng)建頂點著色器(Vrtex Shader)和片段著色器(Fragment Shader)。著色器是用來實現(xiàn)圖像渲染的袒餐,用來替代固定渲染管線的可編輯程序飞蛹,在整個繪制過程中發(fā)揮著重要的作用,如圖:

image.png

其中頂點著色器主要就是處理頂點數(shù)據(jù)的灸眼,比如位置卧檐、顏色;片段著色器就是處理每個片段的顏色和屬性焰宣。著色器相關(guān)知識一兩句說不清楚泄隔,大家自行查閱資料吧。

其實著色器就是一段程序代碼宛徊,所以我們可以直接用字符串佛嬉。但是在Android Studio中提供了著色器類型文件,即glsl文件闸天。如果想創(chuàng)建這樣的文件暖呕,首先需要為Android Studio安裝一個GLSL Support插件

image.png

然后我們在創(chuàng)建新文件的菜單中就會發(fā)現(xiàn)多出一個GLSL Shader類型

image.png

我們在raw目錄下創(chuàng)建一個頂點著色器vertex_simple_shade.glsl

attribute vec4 vPosition;
void main() {
    gl_Position = vPosition;
    gl_PointSize = 10.0;
}

這里簡單的將輸入的頂點坐標vPosition拷貝給gl_Position,并設(shè)置頂點直徑為10(繪制點的時候會用到)苞氮。

再創(chuàng)建一個片段著色器fragment_simple_shade.glsl

precision mediump float;
void main() {
    gl_FragColor = vec4(1.0,1.0,1.0,1.0);
}

只是設(shè)定顏色為白色而已湾揽。關(guān)于著色器語法大家同樣查閱資料吧。

加載著色器

首先創(chuàng)建編譯著色器笼吟,的到著色器id库物,代碼如下:

fun compileShader(type: Int, code : String) : Int{
    //創(chuàng)建一個著色器
    val id = GLES20.glCreateShader(type)
    if(id != 0){
        //載入著色器源碼
        GLES20.glShaderSource(id, code)
        //編譯著色器
        GLES20.glCompileShader(id)
        //檢查著色器是否編譯成功
        val status = IntArray(1)
        GLES20.glGetShaderiv(id, GLES20.GL_COMPILE_STATUS, status, 0)
        if(status[0] == 0){
            //創(chuàng)建失敗
            GLES20.glDeleteShader(id)
            return 0
        }
        return id
    }
    else {
        return 0
    }
}

這里

  • code就是將上面我們創(chuàng)建的glsl文件讀取為字符串即可,所以我們前面說著色器程序直接使用字符串也可以贷帮。

  • type就是著色器類型戚揭,頂點著色器GLES20.GL_VERTEX_SHADER和片段著色器GLES20.GL_FRAGMENT_SHADER

再得到頂點和片段著色器的id后,下一步將它們鏈接到程序中撵枢,代碼如下:

fun linkProgram(vertexShaderId : Int, fragmentShaderId : Int) : Int{
    //創(chuàng)建一個GLES程序
    val id = GLES20.glCreateProgram()
    if(id != 0){
        //將頂點和片元著色器加入程序
        GLES20.glAttachShader(id, vertexShaderId)
        GLES20.glAttachShader(id, fragmentShaderId)

        //鏈接著色器程序
        GLES20.glLinkProgram(id)

        //檢查是否鏈接成功
        val status = IntArray(1)
        GLES20.glGetProgramiv(id, GLES20.GL_LINK_STATUS, status, 0)
        if(status[0] == 0){
            GLES20.glDeleteProgram(id)
            return 0
        }
        return id
    }
    else{
        return 0
    }
}

這樣我們就得到了一個programId特纤,最后通過GLES20.glUseProgram(programId)將其使用起來即可落萎,后面我們會看到在哪一步來處理這些豌鸡。

繪制

準備好著色器后磺平,我們就可以著手繪制三角形了,在頁面中添加一個GLSurfaceView沃但,然后我們準備三個頂點坐標磁滚,如下

val triangleCoords = floatArrayOf(0.5f, 0.5f, 0.0f, // top
    -0.5f, -0.5f, 0.0f, // bottom left
    0.5f, -0.5f, 0.0f // bottom right
)

OpenGL的坐標系是屏幕中心是(0,0),上下左右的最大值都是1宵晚,所以在手機上x軸和y軸上相同數(shù)值對應(yīng)的實際長度并不一樣垂攘,所以上面數(shù)值上雖然看著是等腰三角形维雇,但是實際上并不是。

然后為GLSurfaceView設(shè)置Radnerer搜贤,并繪制三角形谆沃,代碼如下:

setRenderer(object : GLSurfaceView.Renderer{
    override fun onSurfaceCreated(gl: GL10?, config: EGLConfig?) {
        //設(shè)置背景顏色
        GLES20.glClearColor(0f,0f,1f,1f)

        //初始化頂點坐標緩沖
        vertexBuffer = ByteBuffer.allocateDirect(triangleCoords.size * 4).order(
            ByteOrder.nativeOrder()).asFloatBuffer()
        vertexBuffer.put(triangleCoords)
        vertexBuffer.position(0)

        activity?.apply {
            //加載著色器
            val vertexShaderId = ShaderUtils.compileVertexShader(R.raw.vertex_simple_shade, this)
            val fragmentShaderId = ShaderUtils.compileFragmentShader(R.raw.fragment_simple_shade, this)
            program = ShaderUtils.linkProgram(vertexShaderId, fragmentShaderId)
            GLES20.glUseProgram(program)
        }
    }

    override fun onSurfaceChanged(gl: GL10?, width: Int, height: Int) {
        //設(shè)置視圖窗口
        GLES20.glViewport(0,0,width,height)
    }

    override fun onDrawFrame(gl: GL10?) {
        //繪制背景
        GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT or GLES20.GL_DEPTH_BUFFER_BIT)

        val attr = GLES20.glGetAttribLocation(program, "vPosition")
        //準備坐標數(shù)據(jù)
        GLES20.glVertexAttribPointer(attr, 3, GLES20.GL_FLOAT, false, 0, vertexBuffer)
        //啟用頂點位置句柄,注意這里是屬性0仪芒,對應(yīng)著頂點著色器中的layout (location = 0)
        GLES20.glEnableVertexAttribArray(attr)

        //繪制三角形
        GLES20.glDrawArrays(GLES20.GL_TRIANGLES, 0, 3)

        //禁用頂點位置句柄
        GLES20.glDisableVertexAttribArray(attr)
    }

})

在onSurfaceCreated初始化頂點數(shù)據(jù)緩沖唁影,然后加載著色器。

在onDrawFrame中繪制三角形掂名,注意這里先通過GLES20.glGetAttribLocation(program, "vPosition")獲取我們在頂點著色器中定義的vPosition屬性据沈,然后將頂點坐標傳入,最后通過GLES20.glDrawArrays(GLES20.GL_TRIANGLES, 0, 3)來繪制三角形饺蔑。

有關(guān)OpenGL各個函數(shù)這里同樣不詳細介紹了锌介,這里只重點說說glDrawArrays這個函數(shù)。它有三個參數(shù):

  • mode:繪制模式猾警。這里有很多中孔祸,比如我們用的GLES20.GL_TRIANGLES就是三角形;還有點GLES20.GL_POINTS发皿,繪制出來就是三個點崔慧;還有閉環(huán)的線GLES20.GL_LINE_LOOP,繪制出來就是三角形的三條邊穴墅;還有非閉環(huán)的線GLES20.GL_LINE_STRIP等等
  • first:從哪個點開始
  • count:繪制點的數(shù)量

繪制結(jié)果就不上圖了惶室,就是一個三角形。

這樣我們回顧了如何繪制三角形玄货,接下來看看如果在GvrView中繪制一個三角形皇钞。

繪制三角形

在GvrView中繪制三角形與在上面類似,同樣需要兩個著色器松捉,我們先復(fù)用上面兩個看看是什么效果夹界。

然后在onSurfaceCreated中加載著色器,在onDrawEye中繪制三角形即可惩坑,代碼如下:

    override fun onSurfaceCreated(config: EGLConfig?) {
        GLES20.glClearColor(0f,0f,1f,1f)

        //初始化頂點坐標緩沖
        vertexBuffer = ByteBuffer.allocateDirect(triangleCoords.size * 4).order(ByteOrder.nativeOrder()).asFloatBuffer()
        vertexBuffer.put(triangleCoords)
        vertexBuffer.position(0)

        //加載著色器
        val vertexShaderId = ShaderUtils.compileVertexShader(R.raw.vertex_simple_shade, this)
        val fragmentShaderId = ShaderUtils.compileFragmentShader(R.raw.fragment_simple_shade, this)
        objProgram = ShaderUtils.linkProgram(vertexShaderId, fragmentShaderId)

        objectPositionParam = GLES20.glGetAttribLocation(objProgram, "vPosition")
    }

    override fun onDrawEye(eye: Eye) {

        GLES20.glEnable(GLES20.GL_DEPTH_TEST) //啟用深度測試掉盅,自動隱藏被遮住的材料
        GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT or GLES20.GL_DEPTH_BUFFER_BIT)

        GLES20.glUseProgram(objProgram)

        //啟用頂點位置句柄
        GLES20.glEnableVertexAttribArray(objectPositionParam)
        //準備坐標數(shù)據(jù)
        GLES20.glVertexAttribPointer(objectPositionParam, 3, GLES20.GL_FLOAT, false, 0, vertexBuffer)

        //繪制物體
        GLES20.glDrawArrays(GLES20.GL_TRIANGLES, 0, 3)

        //禁用頂點位置句柄
        GLES20.glDisableVertexAttribArray(objectPositionParam)
    }

可以看到在左右分別繪制了一個三角形,所以說在GvrView中左右其實是兩個區(qū)域以舒,而圖形會在兩個區(qū)域都進行繪制。同時我們也看到上面繪制的圖形是無法變動的慢哈,沒有隨著設(shè)備的移動而變化蔓钟,也就是說只是單純的繪制在屏幕上,并沒有繪制在VR空間中卵贱。下一步我們來看看如果讓它動起來滥沫。

動起來

如果圖形不動侣集,那么就失去了VR的意義,而如何才能讓圖形動起來兰绣?這就用到了我們之前提到的著色器世分,我們知道在頂點著色器中可以對頂點坐標進行轉(zhuǎn)換,正是通過它我們可以實現(xiàn)圖形的各種移動或變形缀辩。

著色器

先來重新創(chuàng)建一個頂點著色器gvr_vertex_shade.glsl

uniform mat4 u_MVP; //外部輸入臭埋,4x4矩陣
attribute vec4 vPosition;
void main() {
    gl_Position = u_MVP * vPosition;
}

這里除了定義了一個輸入vPosition,還定義了一個Uniform類型的輸入u_MVP臀玄。這里先簡單說一下Uniform和Attribute瓢阴,在著色器中有三種類型變量

  • Uniform:是全局的,在頂點和片段著色器中都可以訪問健无,一般用來表示轉(zhuǎn)換矩陣荣恐、顏色、材質(zhì)等
  • Attribute:只在頂點著色器中使用累贤,一般用來表示頂點的一些數(shù)據(jù)叠穆,如坐標、頂點顏色等等臼膏;
  • Varying:除它們倆還有一個Varying硼被,它是用來在頂點著色器和片段著色器間傳遞數(shù)據(jù)的,一般在頂點著色器中修改它的值讶请,在片段著色器中使用祷嘶。

所以在我們的代碼里u_MVP就是轉(zhuǎn)換矩陣,通過它與vPosition相乘來得到新的坐標夺溢,這樣我們通過修改u_MVP就可以實現(xiàn)渲染位置的變動论巍。

片段著色器不變,保持原樣即可风响。

初始化

那么接下來就是u_MVP如何得到嘉汰?我們來修改一下GvrDemoActivity的代碼。

首先設(shè)置相機的位置状勤,我們在onNewFrame中來做這部分操作

override fun onNewFrame(headTransform: HeadTransform?) {
    //設(shè)置相機位置
    Matrix.setLookAtM(camera, 0, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, -1.0f, 0.0f, 1.0f, 0.0f)
}

然后在onSurfaceCreated中做一些初始化的操作鞋怀,如下:

override fun onSurfaceCreated(config: EGLConfig?) {
    GLES20.glClearColor(0f,0f,1f,1f)

    //初始化頂點坐標緩沖
    vertexBuffer = ByteBuffer.allocateDirect(triangleCoords.size * 4).order(ByteOrder.nativeOrder()).asFloatBuffer()
    vertexBuffer.put(triangleCoords)
    vertexBuffer.position(0)

    //加載著色器
    val vertexShaderId = ShaderUtils.compileVertexShader(R.raw.gvr_vertex_shade, this)
    val fragmentShaderId = ShaderUtils.compileFragmentShader(R.raw.fragment_simple_shade, this)
    objProgram = ShaderUtils.linkProgram(vertexShaderId, fragmentShaderId)

    //獲取u_MVP這個輸入量
    objectModelViewProjectionParam = GLES20.glGetUniformLocation(objProgram, "u_MVP")
    objectPositionParam = GLES20.glGetAttribLocation(objProgram, "vPosition")

    //初始化modelTarget
    Matrix.setIdentityM(modelTarget, 0)
    Matrix.translateM(modelTarget, 0,0.0f, 0.0f, -3.0f)
}

在這里主要做以下操作:

  • 初始化緩沖
  • 加載著色器
  • 獲取著色器屬性:這里可以看到通過glGetUniformLocationglGetAttribLocation分別獲取兩種不同類型的屬性。
  • 初始化物體的世界坐標轉(zhuǎn)換矩陣modelTarget

這里來說一下modelTarget持搜,要理解這部分以及下面計算繪制的部分密似,需要你先了解OpenGL中各種坐標系的知識,因為這部分知識也很龐大葫盼,我這里簡單說一下残腌,大家想詳細了解可以自行查詢相關(guān)資料。

坐標系

在OpenGL中存在幾種坐標系:

  • 局部坐標系:物體的局部空間,比如上面我們定義的頂點坐標triangleCoords抛猫,它是物體的本地坐標

  • 世界坐標系:物體在三維空間中的坐標蟆盹。我們需要將物體的各個頂點坐標轉(zhuǎn)換到三維空間的坐標,上面的modelTarget就是這個轉(zhuǎn)換矩陣闺金,將頂點坐標與它相乘就會得到世界坐標系中的位置

  • 視圖坐標系:以觀察點為中心的坐標系逾滥。我們知道觀察的位置不同看到的景象也是不同的,所以需要將世界坐標轉(zhuǎn)換成視圖坐標败匹,這個后面會處理

  • 投影坐標系:以上都是針對三維的頂點坐標進行轉(zhuǎn)換寨昙,但是最終呈現(xiàn)在屏幕上還是一個二維平面,所以需要一個投影過程哎壳,所以需要將視圖坐標轉(zhuǎn)換成投影坐標

  • 屏幕坐標系:最后將投影坐標轉(zhuǎn)成屏幕坐標并顯示出來毅待,這部分我們不需要自己處理。

所以可以看到物體的頂點通過以上4個坐標系(局部坐標系归榕、世界坐標系尸红、視圖坐標系和投影坐標系)和三個變換矩陣,得到了最終坐標才進行繪制刹泄,這部分處理如圖:


image.png

計算繪制

簡單復(fù)習了OpenGL的坐標系知識后外里,我來看看最后一步。

最后在onDrawEye中計算回繪制物體特石,代碼如下:

override fun onDrawEye(eye: Eye) {

    GLES20.glEnable(GLES20.GL_DEPTH_TEST)
    GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT or GLES20.GL_DEPTH_BUFFER_BIT)

    //將眼睛的位置變化應(yīng)用到相機
    Matrix.multiplyMM(view, 0, eye.eyeView, 0, camera, 0)

    val perspective = eye.getPerspective(Z_NEAR, Z_FAR)

    Matrix.multiplyMM(modelView, 0, view, 0, modelTarget, 0)
    Matrix.multiplyMM(modelViewProjection, 0, perspective, 0, modelView, 0)

    //將modelViewProjection輸入頂點著色器(u_MVP)
    GLES20.glUseProgram(objProgram)
    GLES20.glUniformMatrix4fv(objectModelViewProjectionParam, 1, false, modelViewProjection, 0)

    //啟用頂點位置句柄
    GLES20.glEnableVertexAttribArray(objectPositionParam)
    //準備坐標數(shù)據(jù)
    GLES20.glVertexAttribPointer(objectPositionParam, 3, GLES20.GL_FLOAT, false, 0, vertexBuffer)

    //繪制物體
    GLES20.glDrawArrays(GLES20.GL_TRIANGLES, 0, 3)

    //禁用頂點位置句柄
    GLES20.glDisableVertexAttribArray(objectPositionParam)
}

注意這個函數(shù)有一個參數(shù)eye盅蝗,這里面包含著當前視線的一些信息,比如當我們移動手機的時候姆蘸,我們的視線是變化的墩莫,onDrawEye中的eye中的屬性也是實時變化的。這樣我們通過這種變化來調(diào)整繪制實現(xiàn)場景的移動逞敷。

首先通過相機和眼睛的位置狂秦,得到一個世界坐標系到眼坐標系(視圖坐標系)的轉(zhuǎn)換矩陣view;然后將物體的世界坐標轉(zhuǎn)換成眼坐標modelView推捐;在通過投影矩陣轉(zhuǎn)換成投影坐標modelViewProjection裂问;最后將這個最終的轉(zhuǎn)換矩陣傳入著色器的u_MVP,這樣在著色器中頂點坐標通過這一系列轉(zhuǎn)換就成功的計算成最終坐標牛柒,這樣就可以進行繪制的堪簿。

計算完成后進行繪制即可。

可以看到皮壁,我們在VR世界中成功繪制了一個三角形椭更,隨著手機(視線)的移動景象也有了變化。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末蛾魄,一起剝皮案震驚了整個濱河市甜孤,隨后出現(xiàn)的幾起案子协饲,更是在濱河造成了極大的恐慌畏腕,老刑警劉巖缴川,帶你破解...
    沈念sama閱讀 218,682評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異描馅,居然都是意外死亡把夸,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,277評論 3 395
  • 文/潘曉璐 我一進店門铭污,熙熙樓的掌柜王于貴愁眉苦臉地迎上來恋日,“玉大人,你說我怎么就攤上這事嘹狞∑裆牛” “怎么了?”我有些...
    開封第一講書人閱讀 165,083評論 0 355
  • 文/不壞的土叔 我叫張陵磅网,是天一觀的道長谈截。 經(jīng)常有香客問我,道長涧偷,這世上最難降的妖魔是什么簸喂? 我笑而不...
    開封第一講書人閱讀 58,763評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮燎潮,結(jié)果婚禮上喻鳄,老公的妹妹穿的比我還像新娘。我一直安慰自己确封,他們只是感情好除呵,可當我...
    茶點故事閱讀 67,785評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著爪喘,像睡著了一般颜曾。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上腥放,一...
    開封第一講書人閱讀 51,624評論 1 305
  • 那天泛啸,我揣著相機與錄音,去河邊找鬼秃症。 笑死候址,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的种柑。 我是一名探鬼主播岗仑,決...
    沈念sama閱讀 40,358評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼聚请!你這毒婦竟也來了荠雕?” 一聲冷哼從身側(cè)響起稳其,我...
    開封第一講書人閱讀 39,261評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎炸卑,沒想到半個月后既鞠,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,722評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡盖文,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,900評論 3 336
  • 正文 我和宋清朗相戀三年嘱蛋,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片五续。...
    茶點故事閱讀 40,030評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡洒敏,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出疙驾,到底是詐尸還是另有隱情凶伙,我是刑警寧澤,帶...
    沈念sama閱讀 35,737評論 5 346
  • 正文 年R本政府宣布它碎,位于F島的核電站函荣,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏链韭。R本人自食惡果不足惜偏竟,卻給世界環(huán)境...
    茶點故事閱讀 41,360評論 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望敞峭。 院中可真熱鬧踊谋,春花似錦、人聲如沸旋讹。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,941評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽沉迹。三九已至睦疫,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間鞭呕,已是汗流浹背蛤育。 一陣腳步聲響...
    開封第一講書人閱讀 33,057評論 1 270
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留葫松,地道東北人瓦糕。 一個月前我還...
    沈念sama閱讀 48,237評論 3 371
  • 正文 我出身青樓,卻偏偏與公主長得像腋么,于是被迫代替她去往敵國和親咕娄。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 44,976評論 2 355

推薦閱讀更多精彩內(nèi)容