OpenGl-ES2.0 For Android 读书笔记(三)

一、开始

在上篇文章中,我们已经让桌面的颜色有了变化,也让桌面适配了屏幕的比例,让手机在旋转的时候桌面也不会产生变形。但现在看到的桌面就像是在纸上画的一样,一点都没有立体感。所以现在我们要学习的就是3D的去展示我们的桌面,这个只是第一点,为了让我们的桌面更好看一些,我们准备了一张图片,我们也会学习如何让OpenGL去渲染我们要展示的图片,总的来说我们要学习两点:
1.3D展示桌面
2.OpenGL渲染图片
最终我们做出来的效果应该是这个样子的:

效果图.png

二、3D展示桌面

为了用3D效果展示桌面,我们先要知道一个gl_Position是如何转换成屏幕上显示的点的坐标的:

坐标转换流程图.png

大致流程就像上图所示,书中讲的很详细,我这里简单讲下,大家感兴趣的可以自己去看书或者查资料,我们制造出3D效果主要实在Perspective Division(透视除法)这一步,一个点的坐标应该是(x , y , z , w)在Clip Space(裁剪空间)这个里面,这个时候x,y,z的取值范围是在-w到w之间的,然后会经过Perspective Division转换,这个时候的坐标就应该是(x/w , y/w , z/w)。
然后我们现在就用代码去实现了,这个时候我们一个点的坐标就应该有4个参数了,所以先修改如下代码:

private static final int POSITION_COMPONENT_COUNT = 4;

然后我们需要修改我们的数据

private final float[] mData = new float[]{
            // Order of coordinates: X, Y, Z, W, R, G, B

            // Triangle Fan
               0f,    0f, 0f, 1.5f,   1f,   1f,   1f,
            -0.5f, -0.8f, 0f,   1f, 0.7f, 0.7f, 0.7f,
             0.5f, -0.8f, 0f,   1f, 0.7f, 0.7f, 0.7f,
             0.5f,  0.8f, 0f,   2f, 0.7f, 0.7f, 0.7f,
            -0.5f,  0.8f, 0f,   2f, 0.7f, 0.7f, 0.7f,
            -0.5f, -0.8f, 0f,   1f, 0.7f, 0.7f, 0.7f,

            //线
            -0.5f, 0f, 0f, 1.5f, 1f, 0f, 0f,
             0.5f, 0f, 0f, 1.5f, 1f, 0f, 0f,

            //点
            0f, -0.4f, 0f, 1.25f, 0f, 0f, 1f,
            0f,  0.4f, 0f, 1.75f, 1f, 0f, 0f
    };

运行下看看,是不是有点3D的感觉了!但是这样我们要硬编码w参数,这样明显是不好的,当然我们的前人也不会这么傻,当然有别的方法去处理,我们可以做移动,缩放,旋转等操作,让我们把之前的代码还原,重新开始吧。
这个时候我们又要回到矩阵的操作了,这次我们要同时处理屏幕适配,和视野的处理,书中讲了很多原理,大致就是讲透视投影是怎么一回事,有兴趣的可以自己去看看,我们就是要用这个东西去做出我们的3D效果。

投影矩阵.png

这个是书中给出的投影矩阵的说明,这个投影矩阵可以同时适配屏幕和调整视野角度具体的参数说明都有说明,a,f,n可能需要看书中对透视投影的讲解才知道,大家可以google下,个人觉得不理解的话,写完代码之后也大概知道是什么了,所以就不讲解了,接下来我们定义一个这样的矩阵。
书中是这样告诉我们的

Android’s Matrix class contains two methods for this, frustumM() and perspectiveM(). Unfortunately, frustumM() has a bug that affects some types of projections,3 and perspectiveM() was only introduced in Android Ice Cream Sandwich and is not available on earlier versions of Android.

意思就是说,骚年,自己写吧!
所以我们就自己来写吧!我们在util包下新建一个类叫MatrixHelper.java类,并声明如下方法:

public static void perspectiveM(float[] m, float yFovInDegrees, float aspect, float n, float f) {
        final float angleInRadians = (float) (yFovInDegrees * Math.PI / 180.0);
        final float a = (float) (1.0 / Math.tan(angleInRadians / 2.0));

        m[0] = a / aspect;
        m[1] = 0f;
        m[2] = 0f;
        m[3] = 0f;

        m[4] = 0f;
        m[5] = a;
        m[6] = 0f;
        m[7] = 0f;

        m[8] = 0f;
        m[9] = 0f;
        m[10] = -((f + n) / (f - n));
        m[11] = -1f;

        m[12] = 0f;
        m[13] = 0f;
        m[14] = -((2f * f * n) / (f - n));
        m[15] = 0f;
    }

这样我们就可以生成一个上面的矩阵了,现在让我们删掉onSurfacedChanged()方法中除了glViewport()之外的东西,然后调用上面的方法:

MatrixHelper.perspectiveM(mProjectionMatrix , 45 , (float)width / (float)height , 1 , 10);

运行一遍之后,你会发现,WTF,怎么成黑的了,小伙子,不要着急,这是正常的,因为我们n,f的值分别为1,10,就是说我们只能看到z轴上-1到-10之间的东西,这个时候我们可以用移动矩阵在z轴上移动一下,就能看到了。
我们先定义一个Model矩阵:

private float[] mModelMatrix = new float[16];

然后我们就要算出一个Model矩阵了,在onSurfacedChanged()中添加如下代码:

setIdentityM(mModelMatrix, 0);
translateM(mModelMatrix, 0, 0f, 0f, -2f);

第一行代码是设置成单位矩阵,第二行代码是生成我们要的移动矩阵。这个时候我们就遇到一个问题,难道我们又要给gl_Point设置一个转换矩阵了么?答案是不需要,我们可以只使用一个矩阵就可以解决了,只不过这个时候需要把两个矩阵乘一下,但是顺序要特别注意,不能颠倒:
vertexclip = ProjectionMatrix * ModelMatrix * vertexmodel
onSurfacedChanged()方法中再添加如下方法:

final float[] temp = new float[16];
multiplyMM(temp, 0, mProjectionMatrix, 0, mModelMatrix, 0);
System.arraycopy(temp, 0, mProjectionMatrix, 0, temp.length);

我们用一个临时的变量去储存两个矩阵相乘的结果,然后再把临时变量里面的内容copy到mProjectionMatrix变量传递给OpenGL,这样我们就可以同时看到两个矩阵作用的效果了,运行下,看看吧,是不是能看到了。
然后我们可以再加上一个旋转的效果就能看到之前做出来的3D效果了,旋转需要选择绕着X,Y,Z那个轴旋转,根据不同的轴旋转矩阵也不一样,书中有说明,这里就不讲解了,我们在代码中只需要调用一个方法就可以了,在translateM(mModelMatrix, 0, 0f, 0f, -2f);添加如下代码

rotateM(mModelMatrix, 0 , -60 , 1 , 0 , 0);

同时把移动的z轴的值改成-2.5,这样可以让桌面离我们远一点。
我们调用方法,让桌面绕着x轴旋转了-60度,这里可能涉及到坐标系的问题,书中有讲解,可以自行了解。再运行一遍,看看效果是不是就出来了。

三、用Texture渲染图片

现在我们就可以开始想办法让我们的桌面更好看一些了。首先我们要知道Texture是个什么东西

Textures in OpenGL can be used to represent images, pictures, and even fractal data that are generated by a mathematical algorithm.

这是原文的解释,就是用来展示图片的,甚至可以展示数学算法制作出来的数据。所以我们就是用这个东西去渲染我们要渲染的图片的。
首先我们要做的事情就是把图片加载到Texture上,我们在util包下建一个类TextureHelper.java,并实现如下代码:

public static int loadTexture(Context context , int resourceId){

        final int[] textureObjectId = new int[1];
        glGenTextures(1 , textureObjectId , 0);

        if (textureObjectId[0] == 0){
            Logger.debug(TAG , "create texture fail.");
            return 0;
        }

        return textureObjectId[0];
    }

我们声明了一个loadTexture()方法来加载texture,我们先创建了一个Texture的对象,接下来我们要把图片加载到texture上,我们继续添加如下代码:

final BitmapFactory.Options options = new BitmapFactory.Options();
options.inScaled = false;
final Bitmap bitmap = BitmapFactory.decodeResource(
                context.getResources(), resourceId, options);
if (bitmap == null) {
    Logger.debug(TAG, "Resource ID " + resourceId + " could not be decoded.");
    glDeleteTextures(1, textureObjectId, 0);
    return 0;
}

glBindTexture(GL_TEXTURE_2D , textureObjectId[0]);

glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);

texImage2D(GL_TEXTURE_2D, 0, bitmap, 0);
bitmap.recycle();

glGenerateMipmap(GL_TEXTURE_2D);

glBindTexture(GL_TEXTURE_2D , 0);

首先我们把图片资源转换成了bitmap,然后我们让OpenGL绑定了我们之前创建的Texture对象,并设置了两个参数GL_TEXTURE_MIN_FILTERGL_TEXTURE_MAG_FILTER,这两个参数在书中有详细解释,大家可以自己去了解,大概就是设置图片的清晰度,同时如果考虑到效率的话有多种选择。然后我们就可以把图片加载到我们创建的Texture对象上了,最后就是bitmap的回收,解除Texture的绑定了。
加载图片资源到Texture的问题解决了,接下来我们需要知道OpenGL如何去绘制Texture了,这个跟之前只画颜色类似,我们需要新建一组.glsl文件,我们先创建一个texture_vertex_shader.glsl文件,实现如下代码:

uniform mat4 u_Matrix;

attribute vec4 a_Position;
attribute vec2 a_TextureCoordinates;

varying vec2 v_TextureCoordinates;

void main() {

    gl_Position = u_Matrix * a_Position;
    v_TextureCoordinates = a_TextureCoordinates;

}

大家注意到了,我们声明了一个有两个参数的向量的变量a_TextureCoordinates,这个Texture是有一个范围为(0,0)到(1,1)的坐标空间的,它的坐标系有两种ST、UV,具体解释书中是有讲解的,在这里不做过多讲解了。
接下来我们创建texture_fragment_shader.glsl文件,实现如下代码:

precision mediump float;

uniform sampler2D u_TextureUnit;
varying vec2 v_TextureCoordinates;

void main() {
     gl_FragColor = texture2D(u_TextureUnit, v_TextureCoordinates);
}

大家可以这样理解,u_TextureUnit就相当于我们要绘制的图片的每个点的颜色,然后v_TextureCoordinates就是告诉我们这些颜色要画在什么位置的坐标,最后通过texture2D()方法获得我们在当前点要绘制的颜色。
书中讲到这里是重构了一遍代码,其实如果看过前面两篇文章的大概应该是知道如何去渲染了图片了,为了后面一篇文章做准备,我还是把书中的重构过程讲一遍吧。
首先我们把绘制的对象的架构改成下图的样子:

对象架构.png

我们创建一个data包,然后创建VertexArray.java类实现如下代码:

public class VertexArray {

    private final FloatBuffer mFloatBuffer;

    public VertexArray(float[] data){
        mFloatBuffer = ByteBuffer
                .allocateDirect(Constants.BYTE_PRE_FLOAT * data.length)
                .order(ByteOrder.nativeOrder())
                .asFloatBuffer();
        mFloatBuffer.put(data);
    }

    public void setVertexAttribPointer(int dataOffset, int attributeLocation, int componentCount, int stride){
        mFloatBuffer.position(dataOffset);

        glVertexAttribPointer(attributeLocation, componentCount, GL_FLOAT,
                false, stride, mFloatBuffer);
        glEnableVertexAttribArray(attributeLocation);

        mFloatBuffer.position(0);
    }

}

在我们创建Mallet和Table之前,我们要先创建Shader类,因为我们想在Mallet和Table中实现绘制功能,所以我们先把Shader实现这部分代码抽离出来,整体架构如下:

Shader架构.png

我们先在ShaderHelper.java中添加如下代码:

public static int buildProgram(String vertexShaderCode , String fragmentShaderCode){

        int vertexShaderObjectId = compileVertexShader(vertexShaderCode);
        int fragmentShaderObjectId = compileFragmentShader(fragmentShaderCode);

        int program = linkProgram(vertexShaderObjectId , fragmentShaderObjectId);

        vaildProgram(program);

        return program;
    }

然后新建一个包programs并创建类ShaderProgram.java实现如下代码:

public class ShaderProgram {

    // Uniform constants
    protected static final String U_MATRIX = "u_Matrix";
    protected static final String U_TEXTURE_UNIT = "u_TextureUnit";
    // Attribute constants
    protected static final String A_POSITION = "a_Position";
    protected static final String A_COLOR = "a_Color";
    protected static final String A_TEXTURE_COORDINATES = "a_TextureCoordinates";
    // Shader program
    protected final int mProgram;
    protected ShaderProgram(Context context, int vertexShaderResourceId,
                            int fragmentShaderResourceId) {
        // Compile the shaders and link the program.
        mProgram = ShaderHelper.buildProgram(
                TextResouceReader.readTextFileFromResource(context, vertexShaderResourceId),
                TextResouceReader.readTextFileFromResource(context, fragmentShaderResourceId));
    }

    public void useProgram() {
        // Set the current OpenGL shader program to this program.
        glUseProgram(mProgram);
    }

}

在该包下继续创建TextureShaderProgram.java类实现如下代码:

public class TextureShaderProgram extends ShaderProgram{

    // Uniform locations
    private final int mUMatrixLocation;
    private final int mUTextureUnitLocation;
    // Attribute locations
    private final int mAPositionLocation;
    private final int mATextureCoordinatesLocation;

    public TextureShaderProgram(Context context) {
        super(context, R.raw.texture_vertex_shader, R.raw.texture_fragment_shader);

        mUMatrixLocation = glGetUniformLocation(mProgram , U_MATRIX);
        mUTextureUnitLocation = glGetUniformLocation(mProgram , U_TEXTURE_UNIT);

        mAPositionLocation = glGetAttribLocation(mProgram , A_POSITION);
        mATextureCoordinatesLocation = glGetAttribLocation(mProgram , A_TEXTURE_COORDINATES);
    }

    public void setUniforms(float[] matrix, int textureId) {
        //把矩阵传递给Shader程序
        glUniformMatrix4fv(mUMatrixLocation, 1, false, matrix, 0);
        // Set the active texture unit to texture unit 0.
        glActiveTexture(GL_TEXTURE0);
        // Bind the texture to this unit.
        glBindTexture(GL_TEXTURE_2D, textureId);
        //告诉Texture Uniform Sampler用这个位置的Texture去渲染,从0开始
        glUniform1i(mUTextureUnitLocation, 0);
    }

    public int getPositionAttributeLocation() {
        return mAPositionLocation;
    }
    public int getTextureCoordinatesAttributeLocation() {
        return mATextureCoordinatesLocation;
    }


}

主要注意setUniforms()这块的代码,这块代码就是用Texture去渲染的方法了。我们再在该包下面创建ColorShaderProgram.java实现如下代码:

public class ColorShaderProgram extends ShaderProgram{

    // Uniform locations
    private final int mUMatrixLocation;
    // Attribute locations
    private final int mAPositionLocation;
    private final int mAColorLocation;

    public ColorShaderProgram(Context context) {
        super(context, R.raw.simple_vertex_shader, R.raw.simple_fragment_shader);

        mUMatrixLocation = glGetUniformLocation(mProgram , U_MATRIX);

        mAColorLocation = glGetAttribLocation(mProgram , A_COLOR);
        mAPositionLocation = glGetAttribLocation(mProgram , A_POSITION);
    }


    public void setUniforms(float[] matrix) {
        // 把矩阵传递给渲染程序
       glUniformMatrix4fv(mUMatrixLocation, 1, false, matrix, 0);
    }

    public int getPositionAttributeLocation() {
        return mAPositionLocation;
    }
    public int getColorAttributeLocation() {
        return mAColorLocation;
    }
}

这部分代码就不用多做解释了,现在我们就可以去创建我们的Table、Mallet了,
先创建Table.java,实现如下代码:

public class Table {

    private final VertexArray mVertexData;

    private static final int POSITION_COMPONENT_COUNT = 2;
    private static final int TEXTURE_COORDINATES_COMPONENT_COUNT = 2;
    private static final int STRIDE = (POSITION_COMPONENT_COUNT
            + TEXTURE_COORDINATES_COMPONENT_COUNT) * Constants.BYTE_PRE_FLOAT;

    private static final float[] VERTEX_DATA = new float[]{
            // Order of coordinates: X, Y, S, T

            // Triangle Fan
            0f,    0f, 0.5f, 0.5f,
            -0.5f, -0.8f,   0f, 0.9f,
            0.5f, -0.8f,   1f, 0.9f,
            0.5f,  0.8f,   1f, 0.1f,
            -0.5f,  0.8f,   0f, 0.1f,
            -0.5f, -0.8f,   0f, 0.9f
    };

    public Table(){
        mVertexData = new VertexArray(VERTEX_DATA);
    }

    public void bindData(TextureShaderProgram program){
        mVertexData.setVertexAttribPointer(0 ,
                program.getPositionAttributeLocation() ,
                POSITION_COMPONENT_COUNT , STRIDE);
        mVertexData.setVertexAttribPointer(POSITION_COMPONENT_COUNT ,
                program.getTextureCoordinatesAttributeLocation() ,
                TEXTURE_COORDINATES_COMPONENT_COUNT , STRIDE);
    }

    public void draw(){
        glDrawArrays(GL_TRIANGLE_FAN, 0, 6);
    }

}

再创建Mallet.java,实现如下代码:

public class Mallet {

    private static final int POSITION_COMPONENT_COUNT = 2;
    private static final int COLOR_COMPONENT_COUNT = 3;
    private static final int STRIDE = (POSITION_COMPONENT_COUNT + COLOR_COMPONENT_COUNT)
            * Constants.BYTE_PRE_FLOAT;

    private static final float[] VERTEXT_DATA = new float[]{
            // Order of coordinates: X, Y, R, G, B
            0f, -0.4f, 0f, 0f, 1f,
            0f, 0.4f, 1f, 0f, 0f
    };

    private VertexArray mVertexData;

    public Mallet(){
        mVertexData = new VertexArray(VERTEXT_DATA);
    }

    public void bindData(ColorShaderProgram program){
        mVertexData.setVertexAttribPointer(0 ,
                program.getPositionAttributeLocation() ,
                POSITION_COMPONENT_COUNT , STRIDE);
        mVertexData.setVertexAttribPointer(
                POSITION_COMPONENT_COUNT ,
                program.getColorAttributeLocation(),
                COLOR_COMPONENT_COUNT , STRIDE);
    }

    public void draw(){
        glDrawArrays(GL_POINTS, 0, 2);
    }

}

准备工作终于做好了,我们可以开始绘制了,因为在Renderer中基本要删掉所有代码,所以我们直接新建一个好了叫做TextrueRenderer.java,并实现如下代码:

public class TextureRenderer implements GLSurfaceView.Renderer{

    private final Context mContext;

    private float[] mProjectionMatrix = new float[16];
    private float[] mModelMatrix = new float[16];

    private Table mTable;
    private Mallet mMallet;

    private TextureShaderProgram mTextureShaderProgram;
    private ColorShaderProgram mColorShaderProgram;

    private int mTexture;

    public TextureRenderer(Context context){
        mContext = context;
    }

    @Override
    public void onSurfaceCreated(GL10 gl10, EGLConfig eglConfig) {
        glClearColor(0.0f, 0.0f, 0.0f, 0.0f);

        mTable = new Table();
        mMallet = new Mallet();

        mTextureShaderProgram = new TextureShaderProgram(mContext);
        mColorShaderProgram = new ColorShaderProgram(mContext);

        mTexture = TextureHelper.loadTexture(mContext , R.drawable.air_hockey_surface);
    }

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

        MatrixHelper.perspectiveM(mProjectionMatrix , 45 , (float)width / (float)height , 1f , 10f);

        setIdentityM(mModelMatrix, 0);
        translateM(mModelMatrix, 0, 0f, 0f, -2.5f);
        rotateM(mModelMatrix, 0 , -60 , 1 , 0 , 0);

        final float[] temp = new float[16];
        multiplyMM(temp, 0, mProjectionMatrix, 0, mModelMatrix, 0);
        System.arraycopy(temp, 0, mProjectionMatrix, 0, temp.length);
    }

    @Override
    public void onDrawFrame(GL10 gl10) {
        glClear(GL_COLOR_BUFFER_BIT);

        mTextureShaderProgram.useProgram();
        mTextureShaderProgram.setUniforms(mProjectionMatrix , mTexture);
        mTable.bindData(mTextureShaderProgram);
        mTable.draw();

        mColorShaderProgram.useProgram();
        mColorShaderProgram.setUniforms(mProjectionMatrix);
        mMallet.bindData(mColorShaderProgram);
        mMallet.draw();

    }
}

然后现在运行一遍,你就能看到我们效果图的效果了。

项目代码在这里:https://github.com/KevinKmoo/AirHockey3DWithTexture

能力有限,自己读书的学习所得,有错误请指导,轻虐!
转载请注明出处。----by kmoo

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

推荐阅读更多精彩内容