OpenGL 基础变化

  • 顶点变换管线


    image.png
  • GLMatrixStack 矩阵堆栈

//类型
GLMatrixStack::GLMatrixStack(int iStackDepth = 64); 
//在堆栈顶部载⼊⼀个单元矩阵
void GLMatrixStack::LoadIdentity(void); 
//在堆栈顶部载⼊任何矩阵
//参数:4*4矩阵
void GLMatrixStack::LoadMatrix(const M3DMatrix44f m); 
//矩阵乘以矩阵堆栈顶部矩阵,相乘结果存储到堆栈的顶部
void GLMatrixStack::MultMatrix(const M3DMatrix44f); 
//获取矩阵堆栈顶部的值 GetMatrix 函数
//为了适应GLShaderMananger的使⽤,或者获取顶部矩阵的副本
const M3DMatrix44f & GLMatrixStack::GetMatrix(void); 
void GLMatrixStack::GetMatrix(M3DMatrix44f mMatrix);
//将当前矩阵压⼊堆栈
void GLMatrixStack::PushMatrix(void);
//将M3DMatrix44f 矩阵对象压⼊当前矩阵堆栈
void PushMatrix(const M3DMatrix44f mMatrix);
//将GLFame 对象压⼊矩阵对象
void PushMatrix(GLFame &frame); 
//出栈(出栈指的是移除顶部的矩阵对象)
void GLMatrixStack::PopMatrix(void);
// 仿射变换,旋转平移缩放
//Rotate 函数angle参数是传递的度数
void MatrixStack::Rotate(GLfloat angle,GLfloat x,GLfloat y,GLfloat z);
void MatrixStack::Translate(GLfloat x,GLfloat y,GLfloat z);
void MatrixStack::Scale(GLfloat x,GLfloat y,GLfloat z);

GLFrame -参考帧

  • 其中存储了1个世界坐标点和2个世界坐标下的方向向量,也就是9个glFloat值,分别用来表示:当前位置点,向前方向向量,向上方向向量
  • GLFrame可以表示世界坐标系中任意物体的位置与方向。无论是相机还是模型,都可以使用GLFrame来表示。对任意一个使用GLFrame来表示的物体而言,涉及到的坐标系有两个:永远不变的世界坐标系,针对于自身的物体坐标系(即绘图坐标系)
  • 一般来说,针对于物体自身的物体坐标系有如下特点:X轴永远平行于视口的水平方向,+X的方向根据右手定则由+Y与+Z得出;Y轴永远平行于视口的竖直方向,竖直向上为+Y;Z轴永远平行于视口的垂直纸面向里方向,正前方为+Z。也就是说,在世界坐标系中,物体坐标系的Y轴看上去就是GLFrame的向上方向向量,Z轴看上去就是GLFrame的向前方向向量,而X轴由Y轴方向向量与Z轴方向向量根据右手定则可得出
class GLFrame  { 
 protected: 
 M3DVector3f vOrigin; // Where am I? 
 M3DVector3f vForward; // Where am I going? 
 M3DVector3f vUp; // Which way is up? 

public:
//默认构造函数(世界坐标系,位置在(0,0,0)点,up为(0,1,0)朝+Y,forward为(0,0,-1)朝向-Z)
  GLFrame(void)
//设置/获取世界坐标系下模型/相机的位置
inline void SetOrigin(const M3DVector3f vPoint)
inline void SetOrigin(float x, float y, float z)
inline void GetOrigin(M3DVector3f vPoint)
//获取世界坐标系下模型/相机位置的X/Y/Z分量
inline float GetOriginX(void)
inline float GetOriginY(void)
inline float GetOriginZ(void)
 
//设置/获取世界坐标系下模型/相机向前的方向向量
inline void SetForwardVector(const M3DVector3f vDirection)
inline void SetForwardVector(float x, float y, float z)
inline void GetForwardVector(M3DVector3f vVector)
 
//设置/获取世界坐标系下模型/相机向上的方向向量
inline void SetUpVector(const M3DVector3f vDirection)
inline void SetUpVector(float x, float y, float z)
inline void GetUpVector(M3DVector3f vVector)
 
//获取世界坐标系下模型/相机X/Y/Z轴方向向量
inline void GetZAxis(M3DVector3f vVector) {GetForwardVector(vVector); }
inline void GetYAxis(M3DVector3f vVector) { GetUpVector(vVector); }
inline void GetXAxis(M3DVector3f vVector) {m3dCrossProduct(vVector, vUp, vForward); }
 
// 以世界坐标系下(x,y,z)偏移量移动模型/相机
inline void TranslateWorld(float x, float y, float z)
// 以物体坐标系下(x,y,z)偏移量移动模型/相机
inline void TranslateLocal(float x, float y, float z)
// 沿物体坐标系下Z轴以指定偏移fDelta量移动模型/相机
inline void MoveForward(float fDelta)
// 沿物体坐标系下Y轴以指定偏移fDelta移动物体/相机
inline void MoveUp(float fDelta)
// 沿物体坐标系下X轴以指定偏移fDelta移动物体/相机
inline void MoveRight(float fDelta)
 
// 获取一个用于描述模型属性的4×4的矩阵
void GetMatrix(M3DMatrix44f matrix, bool bRotationOnly = false)
// 获取一个用于描述相机属性的4×4的矩阵
inline void GetCameraOrientation(M3DMatrix44f m)
 
// 应用所有的相机变换。该函数仅用于相机
inline void ApplyCameraTransform(bool bRotOnly = false)
// 应用所有的物体变换。该函数仅用于除相机外的物体
void ApplyActorTransform(bool bRotationOnly = false)
// 令物体/相机以自身位置为中心,绕X/Y/Z轴旋转。其角度以PI为单位
void RotateLocalX(float fAngle)
void RotateLocalY(float fAngle)
void RotateLocalZ(float fAngle)
 
// Reset axes to make sure they are orthonormal. This should be called on occasion
// if the matrix is long-lived and frequently transformed.
// 规范化
void Normalize(void)
// 模型/相机绕世界坐标系下的指定轴(x,y,z)旋转fAngle度
void RotateWorld(float fAngle, float x, float y, float z)
 // 模型/相机绕当前物体坐标系下的指定轴(x,y,z)旋转fAngle度
void RotateLocal(float fAngle, float x, float y, float z)
 
// 将点/向量vLocal从当前物体坐标系转换为世界坐标系
void LocalToWorld(const M3DVector3f vLocal, M3DVector3f vWorld)
 
// 将点/向量vLocal从世界坐标系转换为当前物体坐标系
void WorldToLocal(const M3DVector3f vWorld, M3DVector3f vLocal)
 
// 通过当前frame矩阵将vPointSrc点变换为vPointDst点
void TransformPoint(M3DVector3f vPointSrc, M3DVector3f vPointDst)
 //通过当前frame矩阵将vVectorSrc向量旋转为vVectorDst向量
void RotateVector(M3DVector3f vVectorSrc, M3DVector3f vVectorDst)
}
  • GLFrustum - 视景体
// 设置正投影
void SetOrthographic(GLfloat xMin, GLfloat xMax, GLfloat yMin, GLfloat yMax, GLfloat zMin, GLfloat zMax)
// 设置透视投影
void SetPerspective(float fFov, float fAspect, float fNear, float fFar)
//  获取透视矩阵
const M3DMatrix44f& GetProjectionMatrix(void) { return projMatrix; }
  • GLBatch - 批次容器
    GLTools库中,包含了一个容器类GLBatch,可以作为7种图元批次容器使用,它知道使用GLShaderManager支持的任意存储着色器时,如何对图元进行渲染。
// 参数1:图元
// 参数2:顶点个数
// 参数3:一组或两组纹理图标
void GLBatch::Begain(GLeunm primitive,GLuint nVerts,GLuint nTexttureUnints = 0);
// 复制顶点
void CopyVertexData3f(M3DVector3f *vVerts);
// 复制表面法线
void GLBatch::CopyNormalDataf(GLfloat *vNorms);
// 复制颜色
void GLBatch::CopyColorData4f(GLfloat *vColors);
// 复制纹理坐标
void GLBatch::CopyTexCoordData2f(GLFloat *vTextCoords,GLuint u
iTextureLayer);
// 重置
void Reset(void);
// 设置顶点
void Vertex3f(GLfloat x, GLfloat y, GLfloat z);
 void Vertex3fv(M3DVector3f vVertex);
// 设置法线        
void Normal3f(GLfloat x, GLfloat y, GLfloat z);
void Normal3fv(M3DVector3f vNormal);
// 设置颜色    
void Color4f(GLfloat r, GLfloat g, GLfloat b, GLfloat a);
void Color4fv(M3DVector4f vColor);
// 设置纹理        
void MultiTexCoord2f(GLuint texture, GLclampf s, GLclampf t);
void MultiTexCoord2fv(GLuint texture, M3DVector2f vTexCoord);
// 画
virtual void Draw(void);
// 绘制结束
void GLBatch::End(void);
  • GLTriangleBatch - 三角形批次类
// Use these three functions to add triangles
void BeginMesh(GLuint nMaxVerts);
void AddTriangle(M3DVector3f verts[3], M3DVector3f vNorms[3], M3DVector2f vTexCoords[3]);
void End(void);
virtual void Draw(void);
  • GLGeometryTransform - 几何变换管道
// 设置模型视图矩阵堆栈
inline void SetModelViewMatrixStack(GLMatrixStack& mModelView) { _mModelView = &mModelView; }
// 设置投影视图矩阵堆栈
inline void SetProjectionMatrixStack(GLMatrixStack& mProjection) { _mProjection = &mProjection; }
// 设置模型视图投影矩阵堆栈
inline void SetMatrixStacks(GLMatrixStack& mModelView, GLMatrixStack& mProjection)  {
    _mModelView = &mModelView;
    _mProjection = &mProjection;
}
// 获取模型视图投影矩阵
const M3DMatrix44f& GetModelViewProjectionMatrix(void)
// 获取模型视图矩阵
inline const M3DMatrix44f& GetModelViewMatrix(void) { return _mModelView->GetMatrix(); }
// 获取投影视图矩阵
inline const M3DMatrix44f& GetProjectionMatrix(void) { return _mProjection->GetMatrix(); }

绘制流程

  • main函数
int main(int argc, char* argv[])
{
    // 设置工作路径
    gltSetWorkingDirectory(argv[0]);
    // 这个函数用来初始化 GLUT 库
    glutInit(&argc, argv);
    //申请一个双缓存区,颜色缓存区、深度缓存区、模板缓存区
    glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGBA | GLUT_DEPTH | GLUT_STENCIL);
    //设置window 的尺寸
    glutInitWindowSize(800, 600);
    //创建window的名称
    glutCreateWindow("GL_POINTS");
    //注册回调函数(改变尺寸)
    glutReshapeFunc(ChangeSize);
    //点击空格时,调用的函数
    glutKeyboardFunc(KeyPressFunc);
    //特殊键位函数(上下左右)
    glutSpecialFunc(SpecialKeys);
    //显示函数
    glutDisplayFunc(RenderScene);
    
    //判断一下是否能初始化glew库,确保项目能正常使用OpenGL 框架
    GLenum err = glewInit();
    
    if (GLEW_OK != err) {
        fprintf(stderr, "GLEW Error: %s\n", glewGetErrorString(err));
        return 1;
    }
    
    // 此函数在呈现上下文中进行任何必要的初始化。.
    // 这是第一次做任何与opengl相关的任务。
    SetupRC();
    
    //runloop运行循环
    glutMainLoop();
    
    return 0;
}
  • 上下文初始化等工作
void SetupRC()
{
    // Black background
    glClearColor(0.7f, 0.7f, 0.7f, 1.0f );
    
    //初始化固定着色管理器
    shaderManager.InitializeStockShaders();
    
    //开启深度测试
    glEnable(GL_DEPTH_TEST);
    
    //通过GLGeometryTransform管理矩阵堆栈
    //使用transformPipeline 管道管理模型视图矩阵堆栈 和 投影矩阵堆栈
    transformPipeline.SetMatrixStacks(modelViewMatrix, projectionMatrix);
    
    //将观察者坐标位置Z移动往屏幕里移动15个单位位置
    //表示离屏幕之间的距离 负数,是往屏幕后面移动;正数,往屏幕前面移动
    cameraFrame.MoveForward(-15.0f);
    
    //利用三角形批次类构造图形对象
    // 球
    /*
      gltMakeSphere(GLTriangleBatch& sphereBatch, GLfloat fRadius, GLint iSlices, GLint iStacks);
     参数1:sphereBatch,三角形批次类对象
     参数2:fRadius,球体半径
     参数3:iSlices,从球体底部堆叠到顶部的三角形带的数量;其实球体是一圈一圈三角形带组成
     参数4:iStacks,围绕球体一圈排列的三角形对数
     
     建议:一个对称性较好的球体的片段数量是堆叠数量的2倍,就是iStacks = 2 * iSlices;
     绘制球体都是围绕Z轴,这样+z就是球体的顶点,-z就是球体的底部。
     */
    gltMakeSphere(sphereBatch, 3.0, 10, 20);
    
    // 环面
    /*
     gltMakeTorus(GLTriangleBatch& torusBatch, GLfloat majorRadius, GLfloat minorRadius, GLint numMajor, GLint numMinor);
     参数1:torusBatch,三角形批次类对象
     参数2:majorRadius,甜甜圈中心到外边缘的半径
     参数3:minorRadius,甜甜圈中心到内边缘的半径
     参数4:numMajor,沿着主半径的三角形数量
     参数5:numMinor,沿着内部较小半径的三角形数量
     */
    gltMakeTorus(torusBatch, 3.0f, 0.75f, 15, 15);
    
    // 圆柱
    /*
     void gltMakeCylinder(GLTriangleBatch& cylinderBatch, GLfloat baseRadius, GLfloat topRadius, GLfloat fLength, GLint numSlices, GLint numStacks);
     参数1:cylinderBatch,三角形批次类对象
     参数2:baseRadius,底部半径
     参数3:topRadius,头部半径
     参数4:fLength,圆形长度
     参数5:numSlices,围绕Z轴的三角形对的数量
     参数6:numStacks,圆柱底部堆叠到顶部圆环的三角形数量
     */
    gltMakeCylinder(cylinderBatch, 2.0f, 2.0f, 3.0f, 15, 2);
    
    //锥
    /*
     void gltMakeCylinder(GLTriangleBatch& cylinderBatch, GLfloat baseRadius, GLfloat topRadius, GLfloat fLength, GLint numSlices, GLint numStacks);
     参数1:cylinderBatch,三角形批次类对象
     参数2:baseRadius,底部半径
     参数3:topRadius,头部半径
     参数4:fLength,圆形长度
     参数5:numSlices,围绕Z轴的三角形对的数量
     参数6:numStacks,圆柱底部堆叠到顶部圆环的三角形数量
     */
    //圆柱体,从0开始向Z轴正方向延伸。
    //圆锥体,是一端的半径为0,另一端半径可指定。
    gltMakeCylinder(coneBatch, 2.0f, 0.0f, 3.0f, 13, 2);
    
    // 磁盘
    /*
    void gltMakeDisk(GLTriangleBatch& diskBatch, GLfloat innerRadius, GLfloat outerRadius, GLint nSlices, GLint nStacks);
     参数1:diskBatch,三角形批次类对象
     参数2:innerRadius,内圆半径
     参数3:outerRadius,外圆半径
     参数4:nSlices,圆盘围绕Z轴的三角形对的数量
     参数5:nStacks,圆盘外网到内围的三角形数量
     */
    gltMakeDisk(diskBatch, 1.5f, 3.0f, 13, 3);
}

  • 屏幕大小发生改变
void ChangeSize(int w, int h)
{
    // 修改视口大小
    glViewport(0, 0, w, h);
    
    //设置透视投影
    viewFrustum.SetPerspective(35.0f, float(w) / float(h), 1.0f, 500.0f);
    
    //projectionMatrix 矩阵堆栈 加载透视投影矩阵
    projectionMatrix.LoadMatrix(viewFrustum.GetProjectionMatrix());
    
    //modelViewMatrix 矩阵堆栈 加载单元矩阵
    modelViewMatrix.LoadIdentity();
}
  • 绘制场景
//召唤场景
void RenderScene(void)
{
    //用当前清除颜色清除窗口背景
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);
    
    //模型视图矩阵栈堆,压栈
    modelViewMatrix.PushMatrix();
    //获取摄像头矩阵
    M3DMatrix44f mCamera;
    //从camereaFrame中获取矩阵到mCamera
    cameraFrame.GetCameraMatrix(mCamera);
    //模型视图堆栈的 矩阵与mCamera矩阵 相乘之后,存储到modelViewMatrix矩阵堆栈中
    modelViewMatrix.MultMatrix(mCamera);
    
    //创建矩阵mObjectFrame
    M3DMatrix44f mObjectFrame;
    //从ObjectFrame 获取矩阵到mOjectFrame中
    objectFrame.GetMatrix(mObjectFrame);
    //将modelViewMatrix 的堆栈中的矩阵 与 mOjbectFrame 矩阵相乘,存储到modelViewMatrix矩阵堆栈中
    modelViewMatrix.MultMatrix(mObjectFrame);
    
    //判断你目前是绘制第几个图形
    switch(nStep) {
        case 0:
            DrawWireFramedBatch(&sphereBatch);
            break;
        case 1:
            DrawWireFramedBatch(&torusBatch);
            break;
        case 2:
            DrawWireFramedBatch(&cylinderBatch);
            break;
        case 3:
            DrawWireFramedBatch(&coneBatch);
            break;
        case 4:
            DrawWireFramedBatch(&diskBatch);
            break;
    }
    
    modelViewMatrix.PopMatrix();
    
    // Flush drawing commands
    glutSwapBuffers();
}

void DrawWireFramedBatch(GLTriangleBatch* pBatch)
{
    //平面着色器,绘制三角形
    shaderManager.UseStockShader(GLT_SHADER_FLAT, transformPipeline.GetModelViewProjectionMatrix(), vGreen);
   
    //传过来的参数,对应不同的图形Batch
    pBatch->Draw();
    
    // 画出黑色轮廓
    glPolygonOffset(-1.0f, -1.0f);
    
    //开启线段抗锯齿
    glEnable(GL_LINE_SMOOTH);
    
    //开启混合功能
    glEnable(GL_BLEND);
    
    //颜色混合,设置混合因子
    //表示源颜色乘以自身的alpha 值,目标颜色乘以1.0减去源颜色的alpha值,这样一来,源颜色的alpha值越大,则产生的新颜色中源颜色所占比例就越大,而目标颜色所占比例则减 小。这种情况下,我们可以简单的将源颜色的alpha值理解为“不透明度”。这也是混合时最常用的方式。
    glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
    
    //通过程序点大小模式来设置点的大小
    glEnable(GL_POLYGON_OFFSET_LINE);
    
    //多边形模型(背面、线) 将多边形背面设为线框模式
    glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);
    
    //线条宽度
    glLineWidth(2.5f);
    
    //平面着色器绘制线条
    shaderManager.UseStockShader(GLT_SHADER_FLAT, transformPipeline.GetModelViewProjectionMatrix(), vBlack);

    pBatch->Draw();
    
    // 恢复多边形模式和深度测试
    glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);
    glDisable(GL_POLYGON_OFFSET_LINE);
    glLineWidth(1.0f);
    glDisable(GL_BLEND);
    glDisable(GL_LINE_SMOOTH);
}
  • 按键控制
//上下左右,移动图形
void SpecialKeys(int key, int x, int y)
{
    if(key == GLUT_KEY_UP)
        //移动世界坐标系,而不是去移动物体。
        //将世界坐标系在X方向移动-5.0
        objectFrame.RotateWorld(m3dDegToRad(-5.0f), 1.0f, 0.0f, 0.0f);
    
    if(key == GLUT_KEY_DOWN)
        objectFrame.RotateWorld(m3dDegToRad(5.0f), 1.0f, 0.0f, 0.0f);
    
    if(key == GLUT_KEY_LEFT)
        objectFrame.RotateWorld(m3dDegToRad(-5.0f), 0.0f, 1.0f, 0.0f);
    
    if(key == GLUT_KEY_RIGHT)
        objectFrame.RotateWorld(m3dDegToRad(5.0f), 0.0f, 1.0f, 0.0f);
    
    glutPostRedisplay();
}

//点击空格,切换渲染图形
void KeyPressFunc(unsigned char key, int x, int y)
{
    if(key == 32)
    {
        nStep++;
        
        if(nStep > 4)
            nStep = 0;
    }
    
    switch(nStep)
    {
        case 0:
            glutSetWindowTitle("Sphere");
            break;
        case 1:
            glutSetWindowTitle("Torus");
            break;
        case 2:
            glutSetWindowTitle("Cylinder");
            break;
        case 3:
            glutSetWindowTitle("Cone");
            break;
        case 4:
            glutSetWindowTitle("Disk");
            break;
    }
    
    glutPostRedisplay();
}

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

推荐阅读更多精彩内容