OpenGL基础纹理

一、纹理综述

物理世界中,视域内的颜色会发生快速的变化。你可以看到很多物体表面都会呈现出丰富的颜色,并且在狭小的面积上产生多彩的变化。要捕捉细节如此丰富的色彩变化是非常辛苦和缜密的工作(你需要有效地辨别每个线性色彩变化区域中的每个三角形)。如果能找到一张图片,然后把它“粘”到物体表面上,就像贴墙纸一样,那就简单多了。这就是 纹理映射(texture mapping)
纹理贴图(简称纹理) 是通过拍摄或者绘制的一张图片,OpenGL支持一维、二维、三维、立方体映射纹理。以及缓存纹理,同时还支持数组纹理。

二、认识函数

像素包装
图像存储空间 = 图像的高度 * 图像宽度 * 每个像素的字节数

1、改变或者恢复像素的存储方式

我们可以使用下列函数改变或者恢复像素的存储方式:
void glPixelStorei(GLenum pname,GLint param);
void glPixelStoref(GLenum pname,GLfloat param);
这俩函数用途是一样的,只不过函数名一个是 i 结尾,一个是 f 结尾,区别只是第二个参数的类型,i 的是 GLintf 的是 GLfloat
举例来说,如果我们想要改成紧密包装像素数据,就这样调用:

    glPixelStorei(GL_UNPACK_ALIGNMENT, 1);

参数1:GL_UNPACK_ALIGNMENT,指定 OpenGL 如何从数据缓存
区中解包图像数据
参数2:针对 GL_UNPACK_ALIGNMENT 设置的值
类似的,我们可以使用 GL_PACK_ALIGNMENT 来告诉 OpenGL 如何将像素缓冲区中读取并放置到一个用户指定的内存缓冲区的数据进行包装。

2、将颜色缓存区的内容作为像素图直接读取

我们无法直接将一个像素图绘制到颜色缓冲区中,但可以使用下面的函数将将颜色缓存区的内容作为像素图直接读取:
void glReadPixels(GLint x,GLint y,GLSizei width,GLSizei height, GLenum format, GLenum type,const void * pixels);

参数说明

  • 参数1:x,矩形左下角的窗⼝坐标
  • 参数2:y,矩形左下角的窗⼝坐标
  • 参数3:width,矩形的宽,以像素为单位
  • 参数4:height,矩形的高,以像素为单位
  • 参数5:formatOpenGL 的像素格式,参考 表1-1
  • 参数6:type,解释参数 pixels 指向的数据,告诉OpenGL 使⽤缓存区中的什么数据类型来存储颜⾊分量量,像素数据的数据类型,参考表1-2
  • 参数7:pixels,指向图形数据的指针

void glReadBuffer(GLenum mode);—> 指定读取的缓存
void glWriteBuffer(GLenum mode);—> 指定写⼊的缓存

表1-1 OpenGL 像素格式

常量 描述
GL_RGB 描述红、绿、蓝顺序排列的颜色
GL_RGBA 按照红、绿、蓝、Alpha顺序排列的颜色
GL_BGR 按照蓝、绿、红顺序排列颜色
GL_BGRA 按照蓝、绿、红、Alpha顺序排列颜色
GL_RED 每个像素只包含了一个红色分量
GL_GREEN 每个像素只包含了一个绿色分量
GL_BLUE 每个像素只包含了一个蓝色分量
GL_RG 每个像素依次包含了一个红色和绿色的分量
GL_RED_INTEGER 每个像素包含了一个整数形式的红色分量
GL_GREEN_INTEGER 每个像素包含了一个整数形式的绿色分量
GL_BLUE_INTEGER 每个像素包含了一个整数形式的蓝色分量
GL_RG_INTEGER 每个像素依次包含了一个整数形式的红色、绿色分量
GL_RGB_INTEGER 每个像素包含了一个整数形式的红色、蓝色、绿色分量
GL_RGBA_INTEGER 每个像素包含了一个整数形式的红色、蓝色、绿色、Alpha分量
GL_BGR_INTEGER 每个像素包含了一个整数形式的蓝色、绿色、红色分量
GL_BGRA_INTEGER 每个像素包含了一个整数形式的蓝色、绿色、红色、Aplha分量
GL_STENCIL_INDEX 每个像素只包含一个模板值
GL_DEPTH_COMPONENT 每个像素只包含一个深度值
GL_DEPTH_STENCIL 每个像素包含一个深度值和一个模板值

最后3个格式 GL_STENCIL_INDEXGL_DEPTH_COMPONENT 和 ** GL_DEPTH_STENCIL** 用于对模板缓冲区和深度缓冲区直接进行读写。
参数 type解释参数 *pixels 指向的数据,它告诉 OpenGL 使用缓冲区的什么数据类型来存储颜色分量。如下表1-2。
表1-2 像素数据的数据类型

常量 描述
GL_UNSIGNED_BYTE 每种颜色分量都是一个 8符号整数
GL_BYTE 8符号整数
GL_UNSIGNED_SHORT 16符号整数
GL_SHORT 16符号整数
GL_UNSIGNED_INT 32符号整数
GL_INT 32符号整数
GL_FLOAT 精度浮点数
GL_HALF_FLOAT 精度浮点数
GL_UNSIGNED_BYTE_3_2_2 包装的 RGB
GL_UNSIGNED_BYTE_2_3_3_REV 包装的 RGB
GL_UNSIGNED_SHORT_5_6_5 包装的 RGB
GL_UNSIGNED_SHORT_5_6_5_REV 包装的 RGB
GL_UNSIGNED_SHORT_4_4_4_4 包装的 RGBA
GL_UNSIGNED_SHORT_4_4_4_4_REV 包装的 RGBA
GL_UNSIGNED_SHORT_5_5_5_1 包装的 RGBA
GL_UNSIGNED_SHORT_5_5_5_1_REV 包装的 RGBA
GL_UNSIGNED_INT_8_8_8_8 包装的 RGBA
GL_UNSIGNED_INT_8_8_8_8_REV 包装的 RGBA
GL_UNSIGNED_INT_10_10_10_2 包装的 RGBA
GL_UNSIGNED_INT_2_10_10_10_REV 包装的 RGBA
GL_UNSIGNED_INT_24_8 包装的 RGBA
GL_UNSIGNED_INT_10F_11F_11F_REV 包装的 RGBA
GL_FLOAT_32_UNSIGNED_INT_24_8_REV 包装的 RGBA

3、载入纹理

在几何图形中应用文理贴图时,第一个必要步骤就是将纹理载入内存。一旦被载入,这些纹理就会成为当前纹理状态的一部分。有 3OpenGL 函数最经常用来从存储器缓冲区中载入(比如从一个磁盘文件中读取)纹理数据。

void glTexImage1D (GLenum target, GLint level, GLint internalformat, GLsizei width, GLint border, GLenum format, GLenum type, const GLvoid *pixels);

void glTexImage2D (GLenum target, GLint level, GLint internalformat, GLsizei width, GLsizei height, GLint border, GLenum format, GLenum type, const GLvoid *pixels);

void glTexImage3D (GLenum target, GLint level, GLint internalformat, GLsizei width, GLsizei height, GLsizei depth, GLint border, GLenum format, GLenum type, const GLvoid *pixels);

这三个函数实际上是由同一个函数 glTexImage 派生出来的。

参数说明

  • target:纹理维度GL_TEXTURE_1DGL_TEXTURE_2DGL_TEXTURE_3D
  • Level:指定所加载的mip贴图层次。⼀一般我们都把这个参数设置为0。
  • internalformat:每个纹理理单元中存储多少颜⾊色成分。(从读取像素图时获得)
  • width、height、depth 参数:指加载纹理理的宽度、⾼高度、深度。==注意!==这些值必须是 2的整数次⽅方。(这是因为OpenGL 旧版本上的遗留留下的⼀一个要求。当然现在已经可以⽀支持不不是 2的整数次⽅方。但是开发者们还是习惯使⽤用以2的整数次⽅方去设置这些参数。)
  • border 参数:允许为纹理理贴图指定⼀一个边界宽度。
  • format 参数:像素数据的数据类型(GL_UNSIGNED_BYTE,每个颜色分量都是一个8位无符号整数)
  • type 参数:
  • data 参数:指向纹理图像数据的指针

4、使用颜色缓冲区

一维和二维纹理也可以从颜色缓冲区加载数据。我们可以从颜色缓冲区读取一幅图像,并通过下面这两个函数将它作为一个新的纹理使用。

void glCopyTexImage1D(GLenum target,GLint level,GLenum
  internalformt,GLint x,GLint y,GLsizei width,GLint border);
void glCopyTexImage2D(GLenum target,GLint level,GLenum
  internalformt,GLint x,GLint y,GLsizei width,GLsizei
  height,GLint border);

这俩函数的操作类似于 glTexImage,但在这里 xy 在颜色缓冲区中指定了开始读取纹理数据的位置。源缓冲区是通过 glReadBuffer 函数设置的。请注意,并不存在 glCopyTexImage3D,因为我们无法从 2D 颜色缓冲区获取体积数据。

5、更新纹理

替换一个纹理图像常常要比直接使用 glTexImage 重新加载一个新纹理快得多。用于完成这个任务的函数就是 glTexSubImage,它同样有3 个变型。

void glTexSubImage1D(GLenum target,GLint level,GLint xOffset,GLsizei width,GLenum
  format,GLenum type,const GLvoid *data);

void glTexSubImage2D(GLenum target,GLint level,GLint xOffset,GLint yOffset,GLsizei
  width,GLsizei height,GLenum format,GLenum type,const GLvoid *data);

void glTexSubImage3D(GLenum target,GLint level,GLint xOffset,GLint yOffset,GLint
  zOffset,GLsizei width,GLsizei height,GLsizei depth,Glenum type,const GLvoid * data);

绝大部分参数都与 glTexImage 函数的参数准确地对应。xOffset、yOffset 和 zOffset 参数指定了在原来的纹理贴图中开始替换纹理数据的偏移量。width、height 和 depth 参数指定了“插入”到原来那个纹理中的新纹理的宽度、高度和深度。

6、插入替换纹理

下面这组函数允许我们从颜色缓冲区读取纹理,并插入或替换原来纹理的一部分。下面这组函数都是 glCopyTexSubImage 函数的变型。

void glTexSubImage1D(GLenum target,GLint level,GLint xOffset,GLsizei width,GLenum
  format,GLenum type,const GLvoid *data);

void glTexSubImage2D(GLenum target,GLint level,GLint xOffset,GLint yOffset,GLsizei
  width,GLsizei height,GLenum format,GLenum type,const GLvoid *data);

void glTexSubImage3D(GLenum target,GLint level,GLint xOffset,GLint yOffset,GLint
  zOffset,GLsizei width,GLsizei height,GLsizei depth,Glenum type,const GLvoid * data);

注意:
这里并没有 glCopyTexImage 函数。这是因为颜色缓冲区是 2D 的,不存在一种对应的方法来将一副 2D 彩色图像作为一个 3D 纹理的来源。但是,我们可以使用 glCopyTexSubImage3D 函数,在一个三维纹理中使用颜色缓冲区的数据来设置它的一个纹理单元平面。

7、纹理对象

1)分配纹理对象
void glGenTextures (GLsizei n, GLuint *textures);

在这个函数中,我们可以指定纹理对象的 数量n 和一个指针 *textures,这个指针指向一个无符号整型数组(由纹理对象标识符填充)。

2)绑定纹理状态

我们可以把他们看成是不同的可用纹理状态的句柄。为了“绑定”其中一种纹理状态,可以调用下面这个函数。此后,所有的纹理加载和纹理参数设置只影响当前绑定的纹理对象。

void glBindTexture (GLenum target, GLuint texture);
  • 参数target: GL_TEXTURE_1D、GL_TEXTURE_2D、GL_TEXTURE_3D
  • 参数texture:需要绑定的纹理对象
3)读纹理位,读取像素
// Load a .TGA file
GLbyte *gltReadTGABits(const char *szFileName, GLint *iWidth, GLint *iHeight, GLint *iComponents, GLenum *eFormat, GLbyte *pData = NULL);
  • 参数szFileName: 纹理文件名称
  • 参数iWidth:文件宽度地址
  • 参数iHeight:文件高度地址
  • 参数iComponents:文件组件地址
  • 参数eFormat:文件格式地址
  • 返回值:指向图像数据的指针
4)删除绑定的纹理对象
void glDeleteTextures (GLsizei n, const GLuint *textures);
5)测试纹理对象是否有效
GLboolean glIsTexture(GLuint texture)

如果这是一个以前已经分配的纹理对象名,则返回 GL_TRUE,否则返回 GL_FALSE

8、设置纹理参数

很多参数的应用都会影响渲染的规则和纹理贴图的行为。这些纹理参数都是通过 glTexParameter 函数的变量进行设置的。

void glTexParameterf (GLenum target, GLenum pname, GLfloat param);
void glTexParameterfv (GLenum target, GLenum pname, const GLfloat *params);
void glTexParameteri (GLenum target, GLenum pname, GLint param);
void glTexParameteriv (GLenum target, GLenum pname, const GLint *params);
  • 参数1:target,指定这些参数将要应⽤在哪个纹理模式上,⽐如 GL_TEXTURE_1D、GL_TEXTURE_2D、GL_TEXTURE_3D
  • 参数2:pname,指定了需要设置哪个纹理参数
  • 参数3:param 或 params,用于设置特定的纹理参数的值

9、设置过滤方式

根据一个拉伸或收缩的纹理贴图计算颜色片段的过程称为纹理过滤(Texture Filitering)。使用 OpenGL 的纹理参数函数,可以同时设置放大和缩小过滤器。这两种过滤器的参数名分别是 GL_TEXTURE_MAG_FILTERGL_TEXTURE_MIN_FILTER。我们可以为它们从两种基本的纹理过滤器 GL_NEARESTGL_LINEAR 中进行选择,它们分别对应于邻近过滤和线性过滤。

1)邻近过滤(GL_NEAREST):

邻近过滤是把最邻近的纹理单元应用到纹理坐标中。如图1,左上角那个纹理像素的中心距离纹理坐标最近,所以它会被选择为样本颜色:

最邻近过滤.png

2)线性过滤(GL_LINEAR):

如图2,线性过滤会把这个纹理坐标周围的纹理单元的加权平均值应用到这个纹理坐标上(线性插值),一个纹理像素的中心距离纹理坐标越近,那么这个纹理像素的颜色对最终的样本颜色的贡献越大。下图中你可以看到返回的颜色是邻近像素的混合色:

线性过滤.png

两种纹理过滤方式的视觉效果,当在一个很大的物体上应用一张低分辨率的纹理时(纹理被放大了,每个纹理像素都能看到):

过滤方式区别.png

我们可以使用下面这个函数进行过滤:

void glTexParameteri (GLenum target, GLenum pname, GLint param);

四种组合方式的过滤:

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

10、设置环绕方式

正常情况下,我们在 0.01.0 的范围内指定纹理坐标,使它与纹理贴图中的纹理单元形成映射关系。如果纹理坐标落在这个范围之外,OpenGL 则根据当前纹理环绕模式(Wrapping Mode)处理这个问题。

环绕方式(Wrapping) 描述
GL_REPEAT 对纹理的默认行为。重复纹理图像。
GL_MIRRORED_REPEAT 和GL_REPEAT一样,但每次重复图片是镜像放置的。
GL_CLAMP_TO_EDGE 纹理坐标会被约束在0到1之间,超出的部分会重复纹理坐标的边缘,产生一种边缘被拉伸的效果。
GL_CLAMP_TO_BORDER 超出的坐标为用户指定的边缘颜色。

当纹理坐标超出默认范围时,每个选项有不同的效果。如下图:

环绕方式对比.png

设置纹理参数:

glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_WRAR_S,GL_CLAMP_TO_EDGE);
  • 参数1:纹理维度
    GL_TEXTURE_1D、GL_TEXTURE_2D、GL_TEXTURE_3D
  • 参数2:为S/T坐标设置模式
    GL_TEXTURE_WRAP_S、GL_TEXTURE_T、GL_TEXTURE_R,针对s,t,r坐标
  • 参数3:wrapMode,环绕模式
    GL_REPEAT、GL_CLAMP、GL_CLAMP_TO_EDGE、GL_CLAMP_TO_BORDER
    (1) GL_REPEAT: OpenGL 在纹理坐标超过1.0的⽅向上对纹理理进⾏重复;
    (2) GL_CLAMP:所需的纹理单元取自纹理边界或TEXTURE_BORDER_COLOR.
    (3) GL_CLAMP_TO_EDGE:环绕模式强制对范围之外的纹理坐标沿着合法的纹理单元的最后一⾏或者最后一
    列来进行采样。
    (4) GL_CLAMP_TO_BORDER:在纹理坐标在0.0到1.0范围之外的只使⽤边界纹理单元。边界纹理单元是作为围绕基本图像的额外的行和列,并与基本纹理图像⼀起加载的。

下面是综合前面的函数的一个例子:

// 将TGA文件加载为2D纹理。
bool LoadTGATexture(const char *szFileName, GLenum minFilter, GLenum magFilter, GLenum wrapMode)
{
    GLbyte *pBits;
    int nWidth, nHeight, nComponents;
    GLenum eFormat;
    
    //1、读纹理位,读取像素
    //参数1:纹理文件名称
    //参数2:文件宽度地址
    //参数3:文件高度地址
    //参数4:文件组件地址
    //参数5:文件格式地址
    //返回值:pBits,指向图像数据的指针
    
    pBits = gltReadTGABits(szFileName, &nWidth, &nHeight, &nComponents, &eFormat);
    if(pBits == NULL)
        return false;
    
    //2、设置纹理参数
    //参数1:纹理维度
    //参数2:为S/T坐标设置模式
    //参数3:wrapMode,环绕模式
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, wrapMode);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, wrapMode);
    
    //参数1:纹理维度
    //参数2:线性过滤
    //参数3:wrapMode,环绕模式
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, minFilter);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, magFilter);
    
    //3、精密包装像素数据
    //参数1:GL_UNPACK_ALIGNMENT,指定OpenGL如何从数据缓存区中解包图像数据
    //参数2:针对GL_UNPACK_ALIGNMENT 设置的值
    glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
    
    //载入纹理
    //参数1:纹理维度
    //参数2:mip贴图层次
    //参数3:纹理单元存储的颜色成分(从读取像素图是获得)
    //参数4:加载纹理宽
    //参数5:加载纹理高
    //参数6:加载纹理的深度
    //参数7:像素数据的数据类型(GL_UNSIGNED_BYTE,每个颜色分量都是一个8位无符号整数)
    //参数8:指向纹理图像数据的指针
    
    glTexImage2D(GL_TEXTURE_2D, 0, nComponents, nWidth, nHeight, 0,
                 eFormat, GL_UNSIGNED_BYTE, pBits);
    
    //使用完毕释放pBits
    free(pBits);
    
    
    //只有minFilter 等于以下四种模式,才可以生成Mip贴图
    //GL_NEAREST_MIPMAP_NEAREST具有非常好的性能,并且闪烁现象非常弱
    //GL_LINEAR_MIPMAP_NEAREST常常用于对游戏进行加速,它使用了高质量的线性过滤器
    //GL_LINEAR_MIPMAP_LINEAR 和GL_NEAREST_MIPMAP_LINEAR 过滤器在Mip层之间执行了一些额外的插值,以消除他们之间的过滤痕迹。
    //GL_LINEAR_MIPMAP_LINEAR 三线性Mip贴图。纹理过滤的黄金准则,具有最高的精度。
    if(minFilter == GL_LINEAR_MIPMAP_LINEAR ||
       minFilter == GL_LINEAR_MIPMAP_NEAREST ||
       minFilter == GL_NEAREST_MIPMAP_LINEAR ||
       minFilter == GL_NEAREST_MIPMAP_NEAREST)
        
        //加载Mip,纹理生成所有的Mip层
        //参数:GL_TEXTURE_1D、GL_TEXTURE_2D、GL_TEXTURE_3D
        glGenerateMipmap(GL_TEXTURE_2D);
    
    return true;
}

以上的总结参考了并部分摘抄了以下文章,非常感谢以下作者的分享:
1、OpenGl的学习网站 LearnOpenGL-CN 其中的“纹理”一节
2、《OpenGL超级宝典 第5版》
3、《OpenGL编程指南(原书第9版)》

转载请备注原文出处,不得用于商业传播——凡几多

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

推荐阅读更多精彩内容