使用OpenGL ES来显示图像

Android framework提供了很多的标准工具来创建有吸引力的,功能强大的图形用户界面.但是如果你想要拥有更多的控制权来控制你的app在屏幕上画的东西或是画三维图像,你就需要使用一个不同的工具,这就是OpenGL ES. Android framework中的OpenGL ES API带有一堆用于显示高端动画的工具,只要你能想象的到的效果,它就有工具来让你实现,并且使用这些工具还可以利用大多数Android设备的GPU来加速.

下面的代码使用的是OpenGL ES 2.0的API,这个版本的API也是推荐使用的,更多的信息可参考OpenGL Developer Guide.

注意: 不要将OpenGL ES 1.x API 和 2.0 API的方法弄混,这两个API是不能相互替换的.

1. 建立一个OpenGL ES环境

要在你的应用中使用OpenGL ES来画图,你要先有一个view container.创建view container的最直接的一个方法是实现GLSurfaceViewGLSurfaceView.Renderer这两个类.其中:

  • GLSurfaceView是一个view container,用来存放绘制的图像.
  • GLSurfaceView.Renderer用来控制view中绘制的内容.

注: GLSurfaceView只是一个存放OpenGL ES绘制的图像的view container,适用于全屏或近乎全屏的view. 对于只占据整个布局一部分的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绘制的图像创建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 创建一个GLSurfaceView对象

GLSurfaceView是一个特殊的View,可以用来绘制OpenGL ES图像. 实际图像的绘制是由GLSurfaceView中设置的GLSurfaceView.Renderer来控制的,因此GLSurfaceView中要做的事情不多,所以代码会很少,但是不要去直接去用匿名类来实现它,因为在后续的事件处理上还有需要.如下示例:

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,这样可以减少渲染次数,也就可以减少电量的使用以及更少的使用系统的GPU和CPU资源.

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

1.4 创建一个Renderer类

Renderer这个类中有个三个方法会被系统回调:

如下绘制黑色背景示例:

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

定义多边形是实现各种复杂的图形的基础,下面将介绍相关基础知识.

2.1 定义三角形

OpenGL ES 允许你使用三维空间坐标来定义绘制的对象,在你绘制三角形之前,你需要先定义坐标. 最典型的做法是定义一个float类型的数组来存放三角形各顶点坐标,为了效率最大化,你可以使用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],如下图(来自官网).

coordinates.png

注意:上述图形坐标是按逆时针来排序的,

2.2 定义正方形

正方形的定义方式有很多种,有一种常用的方法就是画两个三角形,如下图(来自官网):

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

在前面定义完形状之后,现在开始将它们画出来.

3.1 初始化shape

在绘制之前,需要先将各shape初始化并加载,如果这些shape的坐标不会在执行的时候变化,那么可以在onSurfaceCreated()中进行初始化和加载工作,这样会更省内存和提高处理效率.

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来绘制图像需要你提供一些细节给它,如下:

  • 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的,所以你应该要尽量避免多次执行这些工作.

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);
    }
}

现在就可以来绘制shape了.使用OpenGL ES需要一些参数来告诉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);
}

现在就可以调用draw()方法来绘制了,在onDrawFrame()中调用:

public void onDrawFrame(GL10 unused) {
    ...

    mTriangle.draw();
}

效果如下:

triangle.png

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

  • 效果不够叼,吓不到别人.
  • 上面三角形有点扭曲,屏幕旋转后会变形,因为上面的三角形的顶点不是按GLSurfaceView的frame的比例来定点的,这个可以在下一个知识点中通过projection和camera view来解决.
  • 这个是静态的,不够叼.

4. 应用Projection和Camera Views

在OpenGL ES环境中,projection和camera views允许你将图像以现实中人眼所看到的物理物体的形式显示出来,这种物理视角的模拟是通过对绘制对象的坐标进行相应的数学转换来实现:

  • Projection - 这个是根据绘制对象在GLSurfaceView中的宽和高的坐标来转换的.如果没有这个计算,图像会变形.这个转换计算通常只是在OpenGL View的比例刚被建立改变(在onSurfaceChanged()中回调).
  • Camera View - 这个是根据绘制对象的虚拟camera position来转换的.有一点很重要的是OpenGL ES没有定义一个真实的camera对象,而是提供工具方法通过转换绘制对象来模拟一个camera. 一个camera view的转换可能只会在GLSurfaceView刚建立的时候计算一次,也有可能会根据用户的行为或app的行为而动态改变.

4.1 定义一个Projection

projection转换的数据是在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转换到绘制对象上时会导致empty display,通常你在projection转换时也要应用camera view转换来让屏幕有东西显示.

4.2 定义一个Camera View

通过Matrix.setLookAtM()方法来计算camera view转换,然后与之前计算的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转换

为了让combine后的projection和camera view的转换矩阵在预览的时候显示,需要先在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还提供了其他的功能,你可以使用它们来在三维空间中移动和转换绘制对象.

5.1 旋转shape

要旋转一个图像,只需要创建一个变换矩阵(a rotation matrix)然后将其与projection和camera view的转换矩阵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

注: 若图像没有旋转,你可能是设将GLSurfaceview的模式设置为GLSurfaceView.RENDERMODE_WHEN_DIRTY,将其注释掉即可.

5.2 允许持续渲染

如前面所说,若要运行绘制对象持续渲染,如下将渲染模式设置为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监听

为了响应用户的touch事件,你必须要在你的GLSurfaceView实现类中实现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;
}

要注意的是在计算完角度后,要调用 requestRender ()来告诉渲染器去渲染,这是本例中最有效的方法,因为这可以减少渲染器的无用渲染次数,当然该这也要将渲染模式设置为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 暴露旋转角度

由于渲染器是在其他的线程上运行的,你需要将角度变量声明为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 应用旋转

将之前生成角度的方法注释掉,取而代之的是touch事件产生的角度:

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

总结

本篇只是通过一个例子简单介绍了Android中OpenGL ES的非常基本的使用, 只是熟悉一下流程,来发现该功能的强大,背后的原理还是需要去其他的地方学习.

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
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,425评论 4 361
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,058评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,186评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,848评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,249评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,554评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,830评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,536评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,239评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,505评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,004评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,346评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,999评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,060评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,821评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,574评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,480评论 2 267

推荐阅读更多精彩内容