OpenGL 学习 08 - 球体世界

学习书籍: OpenGL 超级宝典(中文第五版) 密码:fu4w

书籍源码:OpenGL 超级宝典第五版源代码 密码:oyb4

环境搭建:OpenGL 学习 01 - Mac 搭建 OpenGL 环境

PS:因为以后 Demo 源码代码量会越来越多,不太可能全部复制在这里了,我就解释下 Demo 的核心源码,全部源码请前往我的 github/OpenGLDemo 下载,源码里会有详细注释。

基本概念

三角形批次

GLTriangleBatch 就是专门为了绘制三角形的批次类,它将以更加高效的方式(索引顶点数组)进行组织,并实际上将多边形存储在图形卡(使用定点缓冲区对象)上,这里只需要了解它的用法即可。

角色帧

在场景中移动的物体通常被称为角色,为了方便我们计算角色在 3D 场景中进行移动、旋转等操作的坐标变换,每个角色都有自己的参考帧,即本地对象坐标系,更简洁的表示角色在空间中的坐标和方向。

为了在 3D 场景中表示任何对象的位置和方向,GLTools 为我们实现了该参考帧的数据结构 GLFrame角色帧),包含了空间中的一个位置、一个指向前方的向量和一个指向上方的向量。

class GLFrame {
    M3DVector3f vOrigin;    // Where am I?
    M3DVector3f vForward;   // Where am I going?
    M3DVector3f vUp;        // Which way is up?
    ...
}

可能会有疑问说,怎么少了一个指向左边或右边的向量?因为这个指向左边或右边的向量可通过 vForwardvUp 向量叉乘得到。

有了角色帧的概念后,我们就知道照相机实际上就是一个角色帧,可以想象一下,一个人抬着照相机进行拍摄,它拍摄到的东西就是我们在窗口上看到的东西。

变换光线

对于几何图形变换来说,典型情况下我们会设置变换矩阵,将它们传递到着色器,然后让硬件完成所有顶点的变换,但对于光源来说,会有一些不同,因为光源变换是独立于几何变换以外。

光源位置也需要转化到视觉坐标系,但传递到着色器的矩阵是几何图形,而不是光线。想象下,光源就像是一个灯光师,它总是跟随着照相机的一起移动的。

得到光源矩阵:

// 先获取到照相机矩阵
M3DMatrix44f mCamera;
cameraFrame.GetCameraMatrix(mCamera);

// 根据光源位置,得到光源矩阵
M3DVector4f vLightPos = { 0.0f, 10.0f, 5.0f, 1.0f };
M3DVector4f vLightEyePos;
m3dTransformVector4(vLightEyePos, vLightPos, mCamera);

使用点光源着色器,传入光源矩阵:

shaderManager.UseStockShader(
    GLT_SHADER_POINT_LIGHT_DIFF,       
    transformPipeline.GetModelViewMatrix(),          
    transformPipeline.GetProjectionMatrix(), 
    vLightEyePos, 
    vColor
);

源码解析

更多对象(球、花托、圆柱、圆锥、圆盘)

09-Objects 核心源码如下,全部源码下载:09-Objects

// 初始化球体的三角形批次,后面参数依次是,球半径,片段数,堆叠数
gltMakeSphere(sphereBatch, 3.0, 10, 20);
// 初始化花托的三角形批次,后面参数依次是,外半径,内半径,片段数,堆叠数
gltMakeTorus(torusBatch, 3.0f, 0.75f, 150, 15);
花托
// 初始化圆柱的三角形批次,后面参数依次是,底部半径,顶部半径,高度,片段数,堆叠数
gltMakeCylinder(cylinderBatch, 2.0f, 2.0f, 3.0f, 13, 2);
圆柱
// 初始化圆锥的三角形批次,后面参数依次是,底部半径,顶部半径,高度,片段数,堆叠数
gltMakeCylinder(coneBatch, 2.0f, 0.0f, 3.0f, 13, 2);
圆锥
// 初始化圆盘的三角形批次,后面参数依次是,内半径,外半径,片段数,堆叠数
gltMakeDisk(diskBatch, 1.5f, 3.0f, 13, 3);
圆盘

球体世界 Lv1(绘制地板、旋转的花托)

13-SphereWorld-Lv1 核心源码如下,全部源码下载:13-SphereWorld-Lv1

// 程序初始化
void SetupRC() {
    // 设置窗口背景颜色为黑色
    glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
    // 初始化着色器
    shaderManager.InitializeStockShaders();
    // 开启深度测试
    glEnable(GL_DEPTH_TEST);
    // 设置多边形模式为前后面线段模式
    glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);
    // 得到花托批次数据
    gltMakeTorus(torusBatch, 0.4f, 0.15f, 30, 30);
    // 得到方格地板批次数据
    floorBatch.Begin(GL_LINES, 324);
    for(GLfloat x = -20.0; x <= 20.0f; x+= 0.5) {
        floorBatch.Vertex3f(x, -0.55f, 20.0f);
        floorBatch.Vertex3f(x, -0.55f, -20.0f);
        floorBatch.Vertex3f(20.0f, -0.55f, x);
        floorBatch.Vertex3f(-20.0f, -0.55f, x);
    }
    floorBatch.End();
}

// 窗口渲染回调
void RenderScene(void) {
    // 获取2次渲染之间的时间间隔
    static CStopWatch    rotTimer;
    float yRot = rotTimer.GetElapsedSeconds() * 60.0f;
    // 清空缓冲区
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
    // 压栈,保持原始矩阵
    modelViewMatrix.PushMatrix();
    // 绘制地板
    shaderManager.UseStockShader(GLT_SHADER_FLAT, transformPipeline.GetModelViewProjectionMatrix(), vFloorColor);
    floorBatch.Draw();
   
    // 视图矩阵进行平移、旋转后进行绘制花托
    modelViewMatrix.Translate(0.0f, 0.0f, -2.5f);
    modelViewMatrix.Rotate(yRot, 0.0f, 1.0f, 0.0f);
    shaderManager.UseStockShader(GLT_SHADER_FLAT, transformPipeline.GetModelViewProjectionMatrix(), vTorusColor);
    torusBatch.Draw();
    
    // 出栈,恢复原始矩阵
    modelViewMatrix.PopMatrix();
    // 因为是双缓冲区模式,后台缓冲区替换到前台缓存区进行显示
    glutSwapBuffers();
    // 自动触发渲染,达到动画效果
    glutPostRedisplay();
}

球体世界 Lv2(加入角色移动和蓝色小球旋转)

14-SphereWorld-Lv2 核心源码如下,全部源码下载:14-SphereWorld-Lv2

// 程序初始化
void SetupRC() {
    // 设置窗口背景颜色为黑色
    glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
    // 初始化着色器
    shaderManager.InitializeStockShaders();
    // 开启深度测试
    glEnable(GL_DEPTH_TEST);
    // 设置多边形模式为前后面线段模式
    glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);
    // 得到花托批次数据
    gltMakeTorus(torusBatch, 0.4f, 0.15f, 30, 30);
    // 得到旋转小球批次数据【Lv2 增加的代码】
    gltMakeSphere(sphereBatch, 0.1f, 26, 13);
    
    // 得到方格地板批次数据
    floorBatch.Begin(GL_LINES, 324);
    for(GLfloat x = -20.0; x <= 20.0f; x+= 0.5) {
        floorBatch.Vertex3f(x, -0.55f, 20.0f);
        floorBatch.Vertex3f(x, -0.55f, -20.0f);
        floorBatch.Vertex3f(20.0f, -0.55f, x);
        floorBatch.Vertex3f(-20.0f, -0.55f, x);
    }
    floorBatch.End();
}

// 窗口渲染回调
void RenderScene(void) {
    // 获取2次渲染之间的时间间隔
    static CStopWatch    rotTimer;
    float yRot = rotTimer.GetElapsedSeconds() * 60.0f;
    // 清空缓冲区
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
    
    // 原来是没法移动所以压入原始矩阵,但现在角色可以移动通过角色帧获取照相机矩阵,并压入栈中【Lv2 修改的代码块】
    M3DMatrix44f mCamera;
    cameraFrame.GetCameraMatrix(mCamera);
    modelViewMatrix.PushMatrix(mCamera);
    
    // 绘制地板
    shaderManager.UseStockShader(GLT_SHADER_FLAT, transformPipeline.GetModelViewProjectionMatrix(), vFloorColor);
    floorBatch.Draw();
    
    // 视图矩阵进行平移
    modelViewMatrix.Translate(0.0f, 0.0f, -2.5f);
    
    // 保持旋转前的视图矩阵,因为如果不在旋转前保存,会导致旋转小球也会带上花托的旋转【Lv2 增加的代码】
    modelViewMatrix.PushMatrix();
    
    // 继续进行旋转并进行绘制花托
    modelViewMatrix.Rotate(yRot, 0.0f, 1.0f, 0.0f);
    shaderManager.UseStockShader(GLT_SHADER_FLAT, transformPipeline.GetModelViewProjectionMatrix(), vTorusColor);
    torusBatch.Draw();
    
    // 恢复到旋转前的视图矩阵【Lv2 增加的代码】
    modelViewMatrix.PopMatrix();
    
    // 绘制旋转小球【Lv2 增加的代码】
    modelViewMatrix.Rotate(yRot * -2.0f, 0.0f, 1.0f, 0.0f);
    modelViewMatrix.Translate(0.8f, 0.0f, 0.0f);
    shaderManager.UseStockShader(GLT_SHADER_FLAT, transformPipeline.GetModelViewProjectionMatrix(), vSphereColor);
    sphereBatch.Draw();
    
    // 出栈,恢复成原始矩阵
    modelViewMatrix.PopMatrix();
    // 因为是双缓冲区模式,后台缓冲区替换到前台缓存区进行显示
    glutSwapBuffers();
    // 自动触发渲染,达到动画效果
    glutPostRedisplay();
}
// 特殊按键点击回调【Lv2 增加的代码】
void SpecialKeys(int key, int x, int y) {
    float linear = 0.1f;
    float angular = float(m3dDegToRad(5.0f));

    // 控制角色向前后移动
    if(key == GLUT_KEY_UP)
        cameraFrame.MoveForward(linear);

    if(key == GLUT_KEY_DOWN)
        cameraFrame.MoveForward(-linear);

    // 控制角色向左右旋转
    if(key == GLUT_KEY_LEFT)
        cameraFrame.RotateWorld(angular, 0.0f, 1.0f, 0.0f);

    if(key == GLUT_KEY_RIGHT)
        cameraFrame.RotateWorld(-angular, 0.0f, 1.0f, 0.0f);
}

球体世界 Lv3(加入随机小球分布场景)

15-SphereWorld-Lv3 核心源码如下,全部源码下载:15-SphereWorld-Lv3

// 程序初始化
void SetupRC() {
    // 设置窗口背景颜色为黑色
    glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
    // 初始化着色器
    shaderManager.InitializeStockShaders();
    // 开启深度测试
    glEnable(GL_DEPTH_TEST);
    // 设置多边形模式为前后面线段模式
    glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);
    // 得到花托批次数据
    gltMakeTorus(torusBatch, 0.4f, 0.15f, 30, 30);
    // 得到旋转小球批次数据【Lv2 增加的代码】
    gltMakeSphere(sphereBatch, 0.1f, 26, 13);
    // 得到方格地板批次数据
    floorBatch.Begin(GL_LINES, 324);
    for(GLfloat x = -20.0; x <= 20.0f; x+= 0.5) {
        floorBatch.Vertex3f(x, -0.55f, 20.0f);
        floorBatch.Vertex3f(x, -0.55f, -20.0f);
        floorBatch.Vertex3f(20.0f, -0.55f, x);
        floorBatch.Vertex3f(-20.0f, -0.55f, x);
    }
    floorBatch.End();
    
    // 随机小球群位置数据生成【Lv3 增加的代码】
    for(int i = 0; i < NUM_SPHERES; i++) {
        GLfloat x = ((GLfloat)((rand() % 400) - 200) * 0.1f);
        GLfloat z = ((GLfloat)((rand() % 400) - 200) * 0.1f);
        spheres[i].SetOrigin(x, 0.0f, z);
    }
}
// 窗口渲染回调
void RenderScene(void) {
    // 获取2次渲染之间的时间间隔
    static CStopWatch    rotTimer;
    float yRot = rotTimer.GetElapsedSeconds() * 60.0f;
    // 清空缓冲区
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
    // 原来是没法移动所以压入原始矩阵,但现在角色可以移动通过角色帧获取照相机矩阵,并压入栈中【Lv2 修改的代码块】
    M3DMatrix44f mCamera;
    cameraFrame.GetCameraMatrix(mCamera);
    modelViewMatrix.PushMatrix(mCamera);
    // 绘制地板
    shaderManager.UseStockShader(GLT_SHADER_FLAT, transformPipeline.GetModelViewProjectionMatrix(), vFloorColor);
    floorBatch.Draw();
    
    // 绘制随机小球群,这里的做法是瞬间切换当前角色位置并开始绘制小球,然后切换回原来角色位置【Lv3 增加的代码】
    for(int i = 0; i < NUM_SPHERES; i++) {
        // 保存当前矩阵状态,为了能切换回原来的角色位置
        modelViewMatrix.PushMatrix();
        // 调整当前角色位置为随机生成小球的位置
        modelViewMatrix.MultMatrix(spheres[I]);
        // 绘制蓝色小球
        shaderManager.UseStockShader(GLT_SHADER_FLAT, transformPipeline.GetModelViewProjectionMatrix(), vSphereColor);
        sphereBatch.Draw();
        // 还原矩阵状态,切换为原来角色位置
        modelViewMatrix.PopMatrix();
    }
    
    // 视图矩阵进行平移
    modelViewMatrix.Translate(0.0f, 0.0f, -2.5f);
    // 保持旋转前的视图矩阵,因为如果不在旋转前保存,会导致旋转小球也会带上花托的旋转【Lv2 增加的代码】
    modelViewMatrix.PushMatrix();
    // 继续进行旋转并进行绘制花托
    modelViewMatrix.Rotate(yRot, 0.0f, 1.0f, 0.0f);
    shaderManager.UseStockShader(GLT_SHADER_FLAT, transformPipeline.GetModelViewProjectionMatrix(), vTorusColor);
    torusBatch.Draw();
    // 恢复到旋转前的视图矩阵【Lv2 增加的代码】
    modelViewMatrix.PopMatrix();
    // 绘制旋转小球【Lv2 增加的代码】
    modelViewMatrix.Rotate(yRot * -2.0f, 0.0f, 1.0f, 0.0f);
    modelViewMatrix.Translate(0.8f, 0.0f, 0.0f);
    shaderManager.UseStockShader(GLT_SHADER_FLAT, transformPipeline.GetModelViewProjectionMatrix(), vSphereColor);
    sphereBatch.Draw();
    // 出栈,恢复成原始矩阵
    modelViewMatrix.PopMatrix();
    // 因为是双缓冲区模式,后台缓冲区替换到前台缓存区进行显示
    glutSwapBuffers();
    // 自动触发渲染,达到动画效果
    glutPostRedisplay();
}

球体世界 Lv4(加入点光源着色)

16-SphereWorld-Lv4 核心源码如下,全部源码下载:16-SphereWorld-Lv4

// 程序初始化
void SetupRC() {
    // 设置窗口背景颜色为黑色
    glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
    // 初始化着色器
    shaderManager.InitializeStockShaders();
    // 开启深度测试
    glEnable(GL_DEPTH_TEST);
    
    // 设置多边形模式为前后面线段模式【Lv4 删除的代码】
//    glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);
    
    // 得到花托批次数据
    gltMakeTorus(torusBatch, 0.4f, 0.15f, 30, 30);
    // 得到旋转小球批次数据【Lv2 增加的代码】
    gltMakeSphere(sphereBatch, 0.1f, 26, 13);
    // 得到方格地板批次数据
    floorBatch.Begin(GL_LINES, 324);
    for(GLfloat x = -20.0; x <= 20.0f; x+= 0.5) {
        floorBatch.Vertex3f(x, -0.55f, 20.0f);
        floorBatch.Vertex3f(x, -0.55f, -20.0f);
        floorBatch.Vertex3f(20.0f, -0.55f, x);
        floorBatch.Vertex3f(-20.0f, -0.55f, x);
    }
    floorBatch.End();
    // 随机小球群位置数据生成【Lv3 增加的代码】
    for(int i = 0; i < NUM_SPHERES; i++) {
        GLfloat x = ((GLfloat)((rand() % 400) - 200) * 0.1f);
        GLfloat z = ((GLfloat)((rand() % 400) - 200) * 0.1f);
        spheres[i].SetOrigin(x, 0.0f, z);
    }
}
// 窗口渲染回调
void RenderScene(void) {
    // 获取2次渲染之间的时间间隔
    static CStopWatch    rotTimer;
    float yRot = rotTimer.GetElapsedSeconds() * 60.0f;
    // 清空缓冲区
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
    
    // 原来是没法移动所以压入原始矩阵,但现在角色可以移动通过角色帧获取照相机矩阵,并压入栈中【Lv2 修改的代码块】
    M3DMatrix44f mCamera;
    cameraFrame.GetCameraMatrix(mCamera);
    modelViewMatrix.PushMatrix(mCamera);
    
    // 得到点光源位置 【Lv4 增加的代码】
    M3DVector4f vLightPos = { 0.0f, 10.0f, 5.0f, 1.0f };
    M3DVector4f vLightEyePos;
    m3dTransformVector4(vLightEyePos, vLightPos, mCamera);
    
    // 绘制地板
    shaderManager.UseStockShader(GLT_SHADER_FLAT, transformPipeline.GetModelViewProjectionMatrix(), vFloorColor);
    floorBatch.Draw();
    // 绘制随机小球群,这里的做法是瞬间切换当前角色位置并开始绘制小球,然后切换回原来角色位置【Lv3 增加的代码】
    for(int i = 0; i < NUM_SPHERES; i++) {
        // 保存当前矩阵状态,为了能切换回原来的角色位置
        modelViewMatrix.PushMatrix();
        // 调整当前角色位置为随机生成小球的位置
        modelViewMatrix.MultMatrix(spheres[I]);
        // 绘制蓝色小球
        // 设置着色器为点光源着色器【Lv4 调整的代码】
        shaderManager.UseStockShader(GLT_SHADER_POINT_LIGHT_DIFF, 
                                     transformPipeline.GetModelViewMatrix(),
                                     transformPipeline.GetProjectionMatrix(), 
                                     vLightEyePos, 
                                     vSphereColor);
        sphereBatch.Draw();
        // 还原矩阵状态,切换为原来角色位置
        modelViewMatrix.PopMatrix();
    }
    // 视图矩阵进行平移
    modelViewMatrix.Translate(0.0f, 0.0f, -2.5f);
    // 保持旋转前的视图矩阵,因为如果不在旋转前保存,会导致旋转小球也会带上花托的旋转【Lv2 增加的代码】
    modelViewMatrix.PushMatrix();
    // 继续进行旋转并进行绘制花托
    modelViewMatrix.Rotate(yRot, 0.0f, 1.0f, 0.0f);

    // 设置着色器为点光源着色器【Lv4 调整的代码】
    shaderManager.UseStockShader(GLT_SHADER_POINT_LIGHT_DIFF, 
                                 transformPipeline.GetModelViewMatrix(),
                                 transformPipeline.GetProjectionMatrix(), 
                                 vLightEyePos, 
                                 vTorusColor);

    torusBatch.Draw();
    // 恢复到旋转前的视图矩阵【Lv2 增加的代码】
    modelViewMatrix.PopMatrix();
    // 绘制旋转小球【Lv2 增加的代码】
    modelViewMatrix.Rotate(yRot * -2.0f, 0.0f, 1.0f, 0.0f);
    modelViewMatrix.Translate(0.8f, 0.0f, 0.0f);

    // 设置着色器为点光源着色器【Lv4 调整的代码】
    shaderManager.UseStockShader(GLT_SHADER_POINT_LIGHT_DIFF, 
                                 transformPipeline.GetModelViewMatrix(),
                                 transformPipeline.GetProjectionMatrix(), 
                                 vLightEyePos, 
                                 vSphereColor);

    sphereBatch.Draw();
    // 出栈,恢复成原始矩阵
    modelViewMatrix.PopMatrix();
    // 因为是双缓冲区模式,后台缓冲区替换到前台缓存区进行显示
    glutSwapBuffers();
    // 自动触发渲染,达到动画效果
    glutPostRedisplay();
}

上面的 Demo 源码全部都放在我的 github/OpenGLDemo 上,大家可以去下载和调试。

有什么问题可以在下方评论区提出,写得不好可以提出你的意见,我会合理采纳的,O(∩_∩)O哈哈~,求关注求赞

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

推荐阅读更多精彩内容