使用OpenGL ES來顯示圖像

Android framework提供了很多的標準工具來創(chuàng)建有吸引力的,功能強大的圖形用戶界面.但是如果你想要擁有更多的控制權來控制你的app在屏幕上畫的東西或是畫三維圖像,你就需要使用一個不同的工具,這就是OpenGL ES. Android framework中的OpenGL ES API帶有一堆用于顯示高端動畫的工具,只要你能想象的到的效果,它就有工具來讓你實現(xiàn),并且使用這些工具還可以利用大多數(shù)Android設備的GPU來加速.

下面的代碼使用的是OpenGL ES 2.0的API,這個版本的API也是推薦使用的,更多的信息可參考OpenGL Developer Guide.

注意: 不要將OpenGL ES 1.x API 和 2.0 API的方法弄混,這兩個API是不能相互替換的.

1. 建立一個OpenGL ES環(huán)境

要在你的應用中使用OpenGL ES來畫圖,你要先有一個view container.創(chuàng)建view container的最直接的一個方法是實現(xiàn)GLSurfaceViewGLSurfaceView.Renderer這兩個類.其中:

  • GLSurfaceView是一個view container,用來存放繪制的圖像.
  • GLSurfaceView.Renderer用來控制view中繪制的內(nèi)容.

注: GLSurfaceView只是一個存放OpenGL ES繪制的圖像的view container,適用于全屏或近乎全屏的view. 對于只占據(jù)整個布局一部分的view來說,可以使用TextureView.你還可以直接自定義一個view繼承SurfaceView,這樣你就能控制更多同時也需要寫更多的代碼.

1.1 在Manifest中聲明OpenGL ES的使用

OpenGL ES 2.0 API的聲明如下,注意每個版本android:glEsVersion的值都不同:

<uses-feature android:glEsVersion="0x00020000" android:required="true" />

如果你的應用使用了Texture compression,你也要在manife中通過<supports-gl-texture>聲明你的應用所支持的格式,然后用戶在Google Play上下載安裝時就會過濾不支持的應用.

<supports-gl-texture android:name="GL_OES_compressed_ETC1_RGB8_texture" />
<supports-gl-texture android:name="GL_OES_compressed_paletted_texture" />

1.2 給OpenGL ES繪制的圖像創(chuàng)建Activity

使用OpenGL ES的應用的activity和其他的應用一樣,主要的不同在于content view中放的是什么.其他app放的是Button,TextView等等,而用OpenGL ES的應用的Activity不僅可以放其他標準的View,還可以放GLSurfaceView.如下示例:

public class OpenGLES20Activity extends Activity {

    private GLSurfaceView mGLView;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        // Create a GLSurfaceView instance and set it
        // as the ContentView for this Activity.
        mGLView = new MyGLSurfaceView(this);
        setContentView(mGLView);
    }
}

注意: OpenGL ES 2.0需要Android 2.2(API 8)及以上版本才支持.

1.3 創(chuàng)建一個GLSurfaceView對象

GLSurfaceView是一個特殊的View,可以用來繪制OpenGL ES圖像. 實際圖像的繪制是由GLSurfaceView中設置的GLSurfaceView.Renderer來控制的,因此GLSurfaceView中要做的事情不多,所以代碼會很少,但是不要去直接去用匿名類來實現(xiàn)它,因為在后續(xù)的事件處理上還有需要.如下示例:

class MyGLSurfaceView extends GLSurfaceView {

    private final MyGLRenderer mRenderer;

    public MyGLSurfaceView(Context context){
        super(context);

        // Create an OpenGL ES 2.0 context
        setEGLContextClientVersion(2);

        mRenderer = new MyGLRenderer();

        // Set the Renderer for drawing on the GLSurfaceView
        setRenderer(mRenderer);
    }
}

有一個可選的設置是setRenderMode(),將模式設置為RENDERMODE_WHEN_DIRTY,這樣可以減少渲染次數(shù),也就可以減少電量的使用以及更少的使用系統(tǒng)的GPU和CPU資源.

// Render the view only when there is a change in the drawing data
setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY);

1.4 創(chuàng)建一個Renderer類

Renderer這個類中有個三個方法會被系統(tǒng)回調(diào):

如下繪制黑色背景示例:

public class MyGLRenderer implements GLSurfaceView.Renderer {

    public void onSurfaceCreated(GL10 unused, EGLConfig config) {
        // Set the background frame color
        GLES20.glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
    }

    public void onDrawFrame(GL10 unused) {
        // Redraw background color
        GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);
    }

    public void onSurfaceChanged(GL10 unused, int width, int height) {
        GLES20.glViewport(0, 0, width, height);
    }
}

上述代碼就是一個完整OpenGL ES的使用,運行起來會是一個黑色的背景.

2. 定義Shape

定義多邊形是實現(xiàn)各種復雜的圖形的基礎,下面將介紹相關基礎知識.

2.1 定義三角形

OpenGL ES 允許你使用三維空間坐標來定義繪制的對象,在你繪制三角形之前,你需要先定義坐標. 最典型的做法是定義一個float類型的數(shù)組來存放三角形各頂點坐標,為了效率最大化,你可以使用ByteBuffer,這會被傳到OpenGL ES的pipeline來處理.如下示例:

public class Triangle {

    private FloatBuffer vertexBuffer;

    // number of coordinates per vertex in this array
    static final int COORDS_PER_VERTEX = 3;
    static float triangleCoords[] = {   // in counterclockwise order:
             0.0f,  0.622008459f, 0.0f, // top
            -0.5f, -0.311004243f, 0.0f, // bottom left
             0.5f, -0.311004243f, 0.0f  // bottom right
    };

    // Set color with red, green, blue and alpha (opacity) values
    float color[] = { 0.63671875f, 0.76953125f, 0.22265625f, 1.0f };

    public Triangle() {
        // initialize vertex byte buffer for shape coordinates
        ByteBuffer bb = ByteBuffer.allocateDirect(
                // (number of coordinate values * 4 bytes per float)
                triangleCoords.length * 4);
        // use the device hardware's native byte order
        bb.order(ByteOrder.nativeOrder());

        // create a floating point buffer from the ByteBuffer
        vertexBuffer = bb.asFloatBuffer();
        // add the coordinates to the FloatBuffer
        vertexBuffer.put(triangleCoords);
        // set the buffer to read the first coordinate
        vertexBuffer.position(0);
    }
}

默認情況下,OpenGL ES設定GLSurfaceView的frame的中心為坐標的原點[0,0,0](X,Y,Z),frame的右上角的坐標為[1,1,0],左下角的坐標為[-1,-1,0],如下圖(來自官網(wǎng)).

coordinates.png

注意:上述圖形坐標是按逆時針來排序的,

2.2 定義正方形

正方形的定義方式有很多種,有一種常用的方法就是畫兩個三角形,如下圖(來自官網(wǎng)):

ccw-square.png

為了避免兩次定義被兩個三角形公用的兩個點的坐標,使用一個drawing list來告訴OpenGL ES繪制順序,代碼如下:

public class Square {

    private FloatBuffer vertexBuffer;
    private ShortBuffer drawListBuffer;

    // number of coordinates per vertex in this array
    static final int COORDS_PER_VERTEX = 3;
    static float squareCoords[] = {
            -0.5f,  0.5f, 0.0f,   // top left
            -0.5f, -0.5f, 0.0f,   // bottom left
             0.5f, -0.5f, 0.0f,   // bottom right
             0.5f,  0.5f, 0.0f }; // top right

    private short drawOrder[] = { 0, 1, 2, 0, 2, 3 }; // order to draw vertices

    public Square() {
        // initialize vertex byte buffer for shape coordinates
        ByteBuffer bb = ByteBuffer.allocateDirect(
        // (# of coordinate values * 4 bytes per float)
                squareCoords.length * 4);
        bb.order(ByteOrder.nativeOrder());
        vertexBuffer = bb.asFloatBuffer();
        vertexBuffer.put(squareCoords);
        vertexBuffer.position(0);

        // initialize byte buffer for the draw list
        ByteBuffer dlb = ByteBuffer.allocateDirect(
        // (# of coordinate values * 2 bytes per short)
                drawOrder.length * 2);
        dlb.order(ByteOrder.nativeOrder());
        drawListBuffer = dlb.asShortBuffer();
        drawListBuffer.put(drawOrder);
        drawListBuffer.position(0);
    }
}

3. 繪制Shape

在前面定義完形狀之后,現(xiàn)在開始將它們畫出來.

3.1 初始化shape

在繪制之前,需要先將各shape初始化并加載,如果這些shape的坐標不會在執(zhí)行的時候變化,那么可以在onSurfaceCreated()中進行初始化和加載工作,這樣會更省內(nèi)存和提高處理效率.

public class MyGLRenderer implements GLSurfaceView.Renderer {

    ...
    private Triangle mTriangle;
    private Square   mSquare;

    public void onSurfaceCreated(GL10 unused, EGLConfig config) {
        ...

        // initialize a triangle
        mTriangle = new Triangle();
        // initialize a square
        mSquare = new Square();
    }
    ...
}

3.2 繪制一個Shape

使用OpenGL ES 2.0來繪制圖像需要你提供一些細節(jié)給它,如下:

  • Vertex Shader - 渲染shape頂點的OpenGl ES代碼.
  • Fragment Shader - 渲染shape的face的顏色和texture的OpenGl ES代碼.
  • Program - 包含繪制一個或多個shape的shader的OpenGL ES對象.

以上三個,你至少需要一個vertex shader來繪制shape和一個fragment shader來繪制顏色和texture,這些shader必須要被編譯然后再添加到一個OpenGL ES program中,然后這個progrem會被用來繪制shape.如下示例:

public class Triangle {

    private final String vertexShaderCode =
        "attribute vec4 vPosition;" +
        "void main() {" +
        "  gl_Position = vPosition;" +
        "}";

    private final String fragmentShaderCode =
        "precision mediump float;" +
        "uniform vec4 vColor;" +
        "void main() {" +
        "  gl_FragColor = vColor;" +
        "}";

    ...
}

上面的Shader包含有OpenGl Shading Language(GLSL)代碼,這些代碼必須在使用之前編譯,如下面的編譯方法:

public static int loadShader(int type, String shaderCode){

    // create a vertex shader type (GLES20.GL_VERTEX_SHADER)
    // or a fragment shader type (GLES20.GL_FRAGMENT_SHADER)
    int shader = GLES20.glCreateShader(type);

    // add the source code to the shader and compile it
    GLES20.glShaderSource(shader, shaderCode);
    GLES20.glCompileShader(shader);

    return shader;
}

注: 編譯OpenGL shader和把它們綁定到program上是非常消耗CPU的,所以你應該要盡量避免多次執(zhí)行這些工作.

public class Triangle() {
    ...

    private final int mProgram;

    public Triangle() {
        ...

        int vertexShader = MyGLRenderer.loadShader(GLES20.GL_VERTEX_SHADER,
                                        vertexShaderCode);
        int fragmentShader = MyGLRenderer.loadShader(GLES20.GL_FRAGMENT_SHADER,
                                        fragmentShaderCode);

        // create empty OpenGL ES Program
        mProgram = GLES20.glCreateProgram();

        // add the vertex shader to program
        GLES20.glAttachShader(mProgram, vertexShader);

        // add the fragment shader to program
        GLES20.glAttachShader(mProgram, fragmentShader);

        // creates OpenGL ES program executables
        GLES20.glLinkProgram(mProgram);
    }
}

現(xiàn)在就可以來繪制shape了.使用OpenGL ES需要一些參數(shù)來告訴redering pipeline你要繪制什么并且要怎么繪制,由于每個shape的drawing option都不一樣,因此將每個shape的繪制邏輯放到自己的類里面是一個比較好的方法.如下代碼,將繪制邏輯方法draw()方法中:

private int mPositionHandle;
private int mColorHandle;

private final int vertexCount = triangleCoords.length / COORDS_PER_VERTEX;
private final int vertexStride = COORDS_PER_VERTEX * 4; // 4 bytes per vertex

public void draw() {
    // Add program to OpenGL ES environment
    GLES20.glUseProgram(mProgram);

    // get handle to vertex shader's vPosition member
    mPositionHandle = GLES20.glGetAttribLocation(mProgram, "vPosition");

    // Enable a handle to the triangle vertices
    GLES20.glEnableVertexAttribArray(mPositionHandle);

    // Prepare the triangle coordinate data
    GLES20.glVertexAttribPointer(mPositionHandle, COORDS_PER_VERTEX,
                                 GLES20.GL_FLOAT, false,
                                 vertexStride, vertexBuffer);

    // get handle to fragment shader's vColor member
    mColorHandle = GLES20.glGetUniformLocation(mProgram, "vColor");

    // Set color for drawing the triangle
    GLES20.glUniform4fv(mColorHandle, 1, color, 0);

    // Draw the triangle
    GLES20.glDrawArrays(GLES20.GL_TRIANGLES, 0, vertexCount);

    // Disable vertex array
    GLES20.glDisableVertexAttribArray(mPositionHandle);
}

現(xiàn)在就可以調(diào)用draw()方法來繪制了,在onDrawFrame()中調(diào)用:

public void onDrawFrame(GL10 unused) {
    ...

    mTriangle.draw();
}

效果如下:

triangle.png

上面的示例有些不足的地方:

  • 效果不夠叼,嚇不到別人.
  • 上面三角形有點扭曲,屏幕旋轉(zhuǎn)后會變形,因為上面的三角形的頂點不是按GLSurfaceView的frame的比例來定點的,這個可以在下一個知識點中通過projection和camera view來解決.
  • 這個是靜態(tài)的,不夠叼.

4. 應用Projection和Camera Views

在OpenGL ES環(huán)境中,projection和camera views允許你將圖像以現(xiàn)實中人眼所看到的物理物體的形式顯示出來,這種物理視角的模擬是通過對繪制對象的坐標進行相應的數(shù)學轉(zhuǎn)換來實現(xiàn):

  • Projection - 這個是根據(jù)繪制對象在GLSurfaceView中的寬和高的坐標來轉(zhuǎn)換的.如果沒有這個計算,圖像會變形.這個轉(zhuǎn)換計算通常只是在OpenGL View的比例剛被建立改變(在onSurfaceChanged()中回調(diào)).
  • Camera View - 這個是根據(jù)繪制對象的虛擬camera position來轉(zhuǎn)換的.有一點很重要的是OpenGL ES沒有定義一個真實的camera對象,而是提供工具方法通過轉(zhuǎn)換繪制對象來模擬一個camera. 一個camera view的轉(zhuǎn)換可能只會在GLSurfaceView剛建立的時候計算一次,也有可能會根據(jù)用戶的行為或app的行為而動態(tài)改變.

4.1 定義一個Projection

projection轉(zhuǎn)換的數(shù)據(jù)是在GLSurfaceView.RendereronSurfaceChanged()方法中計算出來的,如下:

// mMVPMatrix is an abbreviation for "Model View Projection Matrix"
private final float[] mMVPMatrix = new float[16];
private final float[] mProjectionMatrix = new float[16];
private final float[] mViewMatrix = new float[16];

@Override
public void onSurfaceChanged(GL10 unused, int width, int height) {
    GLES20.glViewport(0, 0, width, height);

    float ratio = (float) width / height;

    // this projection matrix is applied to object coordinates
    // in the onDrawFrame() method
    Matrix.frustumM(mProjectionMatrix, 0, -ratio, ratio, -1, 1, 3, 7);
}

注:應用projection轉(zhuǎn)換到繪制對象上時會導致empty display,通常你在projection轉(zhuǎn)換時也要應用camera view轉(zhuǎn)換來讓屏幕有東西顯示.

4.2 定義一個Camera View

通過Matrix.setLookAtM()方法來計算camera view轉(zhuǎn)換,然后與之前計算的projection的矩陣進行combine,如下:

@Override
public void onDrawFrame(GL10 unused) {
    ...
    // Set the camera position (View matrix)
    Matrix.setLookAtM(mViewMatrix, 0, 0, 0, -3, 0f, 0f, 0f, 0f, 1.0f, 0.0f);

    // Calculate the projection and view transformation
    Matrix.multiplyMM(mMVPMatrix, 0, mProjectionMatrix, 0, mViewMatrix, 0);

    // Draw shape
    mTriangle.draw(mMVPMatrix);
}

4.3 應用Projection和Camera轉(zhuǎn)換

為了讓combine后的projection和camera view的轉(zhuǎn)換矩陣在預覽的時候顯示,需要先在vertex shader中添加一個matrix變量:

public class Triangle {

    private final String vertexShaderCode =
        // This matrix member variable provides a hook to manipulate
        // the coordinates of the objects that use this vertex shader
        "uniform mat4 uMVPMatrix;" +
        "attribute vec4 vPosition;" +
        "void main() {" +
        // the matrix must be included as a modifier of gl_Position
        // Note that the uMVPMatrix factor *must be first* in order
        // for the matrix multiplication product to be correct.
        "  gl_Position = uMVPMatrix * vPosition;" +
        "}";

    // Use to access and set the view transformation
    private int mMVPMatrixHandle;

    ...
}

然后修改繪制對象的draw()方法:

public void draw(float[] mvpMatrix) { // pass in the calculated transformation matrix
    ...

    // get handle to shape's transformation matrix
    mMVPMatrixHandle = GLES20.glGetUniformLocation(mProgram, "uMVPMatrix");

    // Pass the projection and view transformation to the shader
    GLES20.glUniformMatrix4fv(mMVPMatrixHandle, 1, false, mvpMatrix, 0);

    // Draw the triangle
    GLES20.glDrawArrays(GLES20.GL_TRIANGLES, 0, vertexCount);

    // Disable vertex array
    GLES20.glDisableVertexAttribArray(mPositionHandle);
}

跑起來,效果如下:

projection_camera.png

5. 添加動作

前面主要一個基本的OpenGL的繪制圖像功能,實際上你還可以將其與CanvasDrawable配合一起使用.OpenGL ES還提供了其他的功能,你可以使用它們來在三維空間中移動和轉(zhuǎn)換繪制對象.

5.1 旋轉(zhuǎn)shape

要旋轉(zhuǎn)一個圖像,只需要創(chuàng)建一個變換矩陣(a rotation matrix)然后將其與projection和camera view的轉(zhuǎn)換矩陣combine:

private float[] mRotationMatrix = new float[16];
public void onDrawFrame(GL10 gl) {
    float[] scratch = new float[16];

    ...

    // Create a rotation transformation for the triangle
    long time = SystemClock.uptimeMillis() % 4000L;
    float angle = 0.090f * ((int) time);
    Matrix.setRotateM(mRotationMatrix, 0, angle, 0, 0, -1.0f);

    // Combine the rotation matrix with the projection and camera view
    // Note that the mMVPMatrix factor *must be first* in order
    // for the matrix multiplication product to be correct.
    Matrix.multiplyMM(scratch, 0, mMVPMatrix, 0, mRotationMatrix, 0);

    // Draw triangle
    mTriangle.draw(scratch);
}

效果如下:

rotate.gif

注: 若圖像沒有旋轉(zhuǎn),你可能是設將GLSurfaceview的模式設置為GLSurfaceView.RENDERMODE_WHEN_DIRTY,將其注釋掉即可.

5.2 允許持續(xù)渲染

如前面所說,若要運行繪制對象持續(xù)渲染,如下將渲染模式設置為RENDERMODE_CONTINUOUSLY(也是默認的),或者直接將之前設置為其他的方法注釋掉:

public MyGLSurfaceView(Context context) {
    ...
    // Render the view only when there is a change in the drawing data.
    // To allow the triangle to rotate automatically, this line is commented out:
    //setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY);
}

6. 響應用戶操作

6.1 設置Touch監(jiān)聽

為了響應用戶的touch事件,你必須要在你的GLSurfaceView實現(xiàn)類中實現(xiàn)onTouchEvent()方法,如下:

private final float TOUCH_SCALE_FACTOR = 180.0f / 320;
private float mPreviousX;
private float mPreviousY;

@Override
public boolean onTouchEvent(MotionEvent e) {
    // MotionEvent reports input details from the touch screen
    // and other input controls. In this case, you are only
    // interested in events where the touch position changed.

    float x = e.getX();
    float y = e.getY();

    switch (e.getAction()) {
        case MotionEvent.ACTION_MOVE:

            float dx = x - mPreviousX;
            float dy = y - mPreviousY;

            // reverse direction of rotation above the mid-line
            if (y > getHeight() / 2) {
              dx = dx * -1 ;
            }

            // reverse direction of rotation to left of the mid-line
            if (x < getWidth() / 2) {
              dy = dy * -1 ;
            }

            mRenderer.setAngle(
                    mRenderer.getAngle() +
                    ((dx + dy) * TOUCH_SCALE_FACTOR));
            requestRender();
    }

    mPreviousX = x;
    mPreviousY = y;
    return true;
}

要注意的是在計算完角度后,要調(diào)用 requestRender ()來告訴渲染器去渲染,這是本例中最有效的方法,因為這可以減少渲染器的無用渲染次數(shù),當然該這也要將渲染模式設置為GLSurfaceView.RENDERMODE_WHEN_DIRTY才有效:

public MyGLSurfaceView(Context context) {
    ...
    // Render the view only when there is a change in the drawing data
    setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY);
}

6.2 暴露旋轉(zhuǎn)角度

由于渲染器是在其他的線程上運行的,你需要將角度變量聲明為volatile:

public class MyGLRenderer implements GLSurfaceView.Renderer {
    ...

    public volatile float mAngle;

    public float getAngle() {
        return mAngle;
    }

    public void setAngle(float angle) {
        mAngle = angle;
    }
}

6.3 應用旋轉(zhuǎn)

將之前生成角度的方法注釋掉,取而代之的是touch事件產(chǎn)生的角度:

public void onDrawFrame(GL10 gl) {
    ...
    float[] scratch = new float[16];

    // Create a rotation for the triangle
    // long time = SystemClock.uptimeMillis() % 4000L;
    // float angle = 0.090f * ((int) time);
    Matrix.setRotateM(mRotationMatrix, 0, mAngle, 0, 0, -1.0f);

    // Combine the rotation matrix with the projection and camera view
    // Note that the mMVPMatrix factor *must be first* in order
    // for the matrix multiplication product to be correct.
    Matrix.multiplyMM(scratch, 0, mMVPMatrix, 0, mRotationMatrix, 0);

    // Draw triangle
    mTriangle.draw(scratch);
}

最終效果如下:

touch_rotate.gif

總結(jié)

本篇只是通過一個例子簡單介紹了Android中OpenGL ES的非常基本的使用, 只是熟悉一下流程,來發(fā)現(xiàn)該功能的強大,背后的原理還是需要去其他的地方學習.

Reference

  1. Displaying Graphics with OpenGL ES
  2. Building an OpenGL ES Environment
  3. Defining Shapes
  4. Drawing Shapes
  5. Applying Projection and Camera Views
  6. Adding Motion
  7. Responding to Touch Events
最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末畔规,一起剝皮案震驚了整個濱河市亿笤,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌蜀漆,老刑警劉巖涩馆,帶你破解...
    沈念sama閱讀 206,602評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異左驾,居然都是意外死亡,警方通過查閱死者的電腦和手機极谊,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,442評論 2 382
  • 文/潘曉璐 我一進店門诡右,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人轻猖,你說我怎么就攤上這事帆吻。” “怎么了咙边?”我有些...
    開封第一講書人閱讀 152,878評論 0 344
  • 文/不壞的土叔 我叫張陵猜煮,是天一觀的道長。 經(jīng)常有香客問我败许,道長王带,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,306評論 1 279
  • 正文 為了忘掉前任市殷,我火速辦了婚禮愕撰,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘醋寝。我一直安慰自己搞挣,他們只是感情好,可當我...
    茶點故事閱讀 64,330評論 5 373
  • 文/花漫 我一把揭開白布音羞。 她就那樣靜靜地躺著囱桨,像睡著了一般。 火紅的嫁衣襯著肌膚如雪嗅绰。 梳的紋絲不亂的頭發(fā)上舍肠,一...
    開封第一講書人閱讀 49,071評論 1 285
  • 那天,我揣著相機與錄音办陷,去河邊找鬼貌夕。 笑死,一個胖子當著我的面吹牛民镜,可吹牛的內(nèi)容都是我干的啡专。 我是一名探鬼主播,決...
    沈念sama閱讀 38,382評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼制圈,長吁一口氣:“原來是場噩夢啊……” “哼们童!你這毒婦竟也來了畔况?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,006評論 0 259
  • 序言:老撾萬榮一對情侶失蹤慧库,失蹤者是張志新(化名)和其女友劉穎跷跪,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體齐板,經(jīng)...
    沈念sama閱讀 43,512評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡吵瞻,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,965評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了甘磨。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片橡羞。...
    茶點故事閱讀 38,094評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖济舆,靈堂內(nèi)的尸體忽然破棺而出卿泽,到底是詐尸還是另有隱情,我是刑警寧澤滋觉,帶...
    沈念sama閱讀 33,732評論 4 323
  • 正文 年R本政府宣布签夭,位于F島的核電站,受9級特大地震影響椎侠,放射性物質(zhì)發(fā)生泄漏第租。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,283評論 3 307
  • 文/蒙蒙 一肺蔚、第九天 我趴在偏房一處隱蔽的房頂上張望煌妈。 院中可真熱鬧,春花似錦宣羊、人聲如沸璧诵。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,286評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽之宿。三九已至,卻和暖如春苛坚,著一層夾襖步出監(jiān)牢的瞬間比被,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,512評論 1 262
  • 我被黑心中介騙來泰國打工泼舱, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留等缀,地道東北人。 一個月前我還...
    沈念sama閱讀 45,536評論 2 354
  • 正文 我出身青樓娇昙,卻偏偏與公主長得像尺迂,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 42,828評論 2 345

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