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

前言

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

GvrActivity

先依賴庫

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

GvrActivity提供了VR相關的一些細節(jié),通過代碼可以看到,它持有一個GvrView對象,但是需要我們手動set才可以,GvrActivity處理了一些相關事件以及管理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ū)設置為glClearColor設定的顏色
        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?) {
        //設置清除顏色,即背景色
        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關聯(lián)GvrView。然后我們在GvrView.StereoRenderer的對應函數(shù)中實現(xiàn)渲染即可。

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

image.png

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

OpenGL繪制三角形

我們先來復習一下如何用OpenGL來繪制圖形。OpenGL繪制的基本單元有點、線和三角形,其他所有的圖形實際上都是由三角形組成的。所以我們來看看如何用OpenGL繪制三角形。

由于OpenGL相關知識過于龐大,這里就不詳細解釋太多,大家自行查閱OpenGL資料。

著色器

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

image.png

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

其實著色器就是一段程序代碼,所以我們可以直接用字符串。但是在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,并設置頂點直徑為10(繪制點的時候會用到)。

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

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

只是設定顏色為白色而已。關于著色器語法大家同樣查閱資料吧。

加載著色器

首先創(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ù)值對應的實際長度并不一樣,所以上面數(shù)值上雖然看著是等腰三角形,但是實際上并不是。

然后為GLSurfaceView設置Radnerer,并繪制三角形,代碼如下:

setRenderer(object : GLSurfaceView.Renderer{
    override fun onSurfaceCreated(gl: GL10?, config: EGLConfig?) {
        //設置背景顏色
        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) {
        //設置視圖窗口
        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,對應著頂點著色器中的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)來繪制三角形。

有關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ù)量

繪制結果就不上圖了,就是一個三角形。

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

繪制三角形

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

然后在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ū)域都進行繪制。同時我們也看到上面繪制的圖形是無法變動的,沒有隨著設備的移動而變化,也就是說只是單純的繪制在屏幕上,并沒有繪制在VR空間中。下一步我們來看看如果讓它動起來。

動起來

如果圖形不動,那么就失去了VR的意義,而如何才能讓圖形動起來?這就用到了我們之前提到的著色器,我們知道在頂點著色器中可以對頂點坐標進行轉換,正是通過它我們可以實現(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:是全局的,在頂點和片段著色器中都可以訪問,一般用來表示轉換矩陣、顏色、材質(zhì)等
  • Attribute:只在頂點著色器中使用,一般用來表示頂點的一些數(shù)據(jù),如坐標、頂點顏色等等;
  • Varying:除它們倆還有一個Varying,它是用來在頂點著色器和片段著色器間傳遞數(shù)據(jù)的,一般在頂點著色器中修改它的值,在片段著色器中使用。

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

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

初始化

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

首先設置相機的位置,我們在onNewFrame中來做這部分操作

override fun onNewFrame(headTransform: HeadTransform?) {
    //設置相機位置
    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分別獲取兩種不同類型的屬性。
  • 初始化物體的世界坐標轉換矩陣modelTarget

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

坐標系

在OpenGL中存在幾種坐標系:

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

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

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

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

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

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


image.png

計算繪制

簡單復習了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)

    //將眼睛的位置變化應用到相機
    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)場景的移動。

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

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

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

?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,837評論 6 531
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,196評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,688評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,654評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,456評論 6 406
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 54,955評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,044評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,195評論 0 287
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,725評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 40,608評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,802評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,318評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,048評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,422評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,673評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,424評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,762評論 2 372

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