glsl

概述:

以c为基础:

          OpenGL着色语言是基于ANSI C编程语言的语法,使用这种语言编写的程序与C程序非常相似。这是有意要设计成这样,就是要使这种语言对于最可能使用它的那些人(使用c、c++开发应用程序的那些人)非常容易使用。

            使用OpenGL着色语言编写的程序基本结构与使用C编写的程序结构基本相同,着色器的入口函数是void main(),OpenGL中的常量、标识符、运算符、表达式和语句与C是基本相同的。用于循环的控制流、if-then-else以及函数调用与C几乎完全相同。

c扩展:

            由于作为一种用来编写图形算法的语言,OpenGL着色语言具有其专用性,所以该语言中添加了许多语言特性,主要表现在以下方面:

            支持用于浮点数、整数和布尔值的失量类型。对于浮点值,这些矢量类型被称为vec2,vec3,vec4等

            像基本类型一样,浮点类型矩阵同样收到支持。如mat2指2*2矩阵,mat3指3*3矩阵

            被添加的还有一组名为取样器(sampler)的基本类型,它可以创建着色器用来访问纹理内存的机制。取样器是一种特殊的不透明变量类型,它可以用来访问特定的纹理贴图。sampler1D类型的变量可以用来访问1D纹理贴图, sampler2D类型的变量可以用来访问一个2D纹理贴图,以此类推,通过这种机制还可以支持阴影和立方贴图纹理。

            限定符的添加是为了管理着色器的输入和输出。attribute、uniform和varying

            用opengl着色语言编写的着色器可以使用以保留“gl_”开始的内置变量

            Opengl还提供了各种内置函数,目的是使代码编写变得简单,并针对某些操作利用可能的硬件加速,比如:三角运算,指数运算等等  

来自c++的扩展:

            opengl着色语言还包括了C++中的一些重要语言特性。尤其是,它支持函数重载,

            构造函数的概念同样来源于C++。OpenGL着色语言中只使用构造函数就完成了初始化程序。使用构造函数可以用多种方式来初始化变量。

            另一个从C++借用的特性就是可以在需要变量时再次声明它们,而不必在一个基本块开始时就声明它们。像在C++中一样,该语言也支持基本类型bool。

            像在C++一样,函数必须在使用之前进行声明。通过提供函数的定义或者使用原型,就可以实现这一点。

不受支持的C特性:

            glsl语言不存在数据类型的自动提升。如:使用float f=0;就会产生一个错误,尔使用float f = 0.0就不会产生错误。

            glsl语言没有包含对指针、字符串或者字符或者基于这些类型的任何操作的支持。它基本上是一种处理数字数据的言语,而不是处理字符或者字符串的语言。    

                glsl不支持联合、枚举类型、结构中的位字段以及按位运算。最后一点glsl不是基于文件的,所以看不到任何#include指令或者对文件名的其它引用

GLSL 中文手册

基本类型:

类型说明

void空类型,即不返回任何值

bool布尔类型 true,false

int带符号的整数 signed integer

float带符号的浮点数 floating scalar

vec2, vec3, vec4n维浮点数向量 n-component floating point vector

bvec2, bvec3, bvec4n维布尔向量 Boolean vector

ivec2, ivec3, ivec4n维整数向量 signed integer vector

mat2, mat3, mat42x2, 3x3, 4x4 浮点数矩阵 float matrix

sampler2D2D纹理 a 2D texture

samplerCube盒纹理 cube mapped texture

基本结构和数组:

类型说明

结构struct type-name{} 类似c语言中的 结构体

数组float foo[3] glsl只支持1维数组,数组可以是结构体的成员

向量的分量访问:

glsl中的向量(vec2,vec3,vec4)往往有特殊的含义,比如可能代表了一个空间坐标(x,y,z,w),或者代表了一个颜色(r,g,b,a),再或者代表一个纹理坐标(s,t,p,q) 所以glsl提供了一些更人性化的分量访问方式.

vector.xyzw其中xyzw 可以任意组合

vector.rgba其中rgba 可以任意组合

vector.stpq其中rgba 可以任意组合

运算符:

优先级(越小越高)运算符说明结合性

1()聚组:a*(b+c)N/A

2[] () . ++ --数组下标__[],方法参数__fun(arg1,arg2,arg3),属性访问__a.b__,自增/减后缀__a++ a--__L - R

3++ -- + - !自增/减前缀__++a --a__,正负号(一般正号不写)a ,-a,取反__!false__R - L

4* /乘除数学运算L - R

5+ -加减数学运算L - R

7< > <= >=关系运算符L - R

8== !=相等性运算符L - R

12&&逻辑与L - R

13^^逻辑排他或(用处基本等于!=)L - R

14||逻辑或L - R

15? :三目运算符L - R

16= += -= *= /=赋值与复合赋值L - R

17,顺序分配运算L - R

ps 左值与右值:

基础类型间的运算:

glsl中,没有隐式类型转换,原则上glsl要求任何表达式左右两侧(l-value),(r-value)的类型必须一致 也就是说以下表达式都是错误的:

下面来分别说说可能遇到的情况:

1.float与int:

float与float , int与int之间是可以直接运算的,但float与int不行.它们需要进行一次显示转换.即要么把float转成int:int(1.0),要么把int转成float:float(1),以下表达式都是正确的:

2.float与vec(向量)mat(矩阵):

vec,mat这些类型其实是由float复合而成的,当它们与float运算时,其实就是在每一个分量上分别与float进行运算,这就是所谓的逐分量运算.glsl里 大部分涉及vec,mat的运算都是逐分量运算,但也并不全是. 下文中就会讲到特例.

逐分量运算是线性的,这就是说 vec 与 float 的运算结果是还是 vec.

int 与 vec,mat之间是不可运算的, 因为vec和mat中的每一个分量都是 float 类型的. 无法与int进行逐分量计算.

下面枚举了几种 float 与 vec,mat 运算的情况

3.vec(向量)与vec(向量):

两向量间的运算首先要保证操作数的阶数都相同.否则不能计算.例如: vec3*vec2 vec4+vec3 等等都是不行的.

它们的计算方式是两操作数在同位置上的分量分别进行运算,其本质还是逐分量进行的,这和上面所说的float类型的 逐分量运算可能有一点点差异,相同的是 vec 与 vec 运算结果还是 vec, 且阶数不变.

3.vec(向量)与mat(矩阵):

要保证操作数的阶数相同,且vec与mat间只存在乘法运算.

它们的计算方式和线性代数中的矩阵乘法相同,不是逐分量运算.

向量与矩阵的乘法规则如下:

4.mat(矩阵)与mat(矩阵):

要保证操作数的阶数相同.

在mat与mat的运算中, 除了乘法是线性代数中的矩阵乘法外.其余的运算任为逐分量运算.简单说就是只有乘法是特殊的,其余都和vec与vec运算类似.

矩阵乘法规则如下:

变量限定符:

修饰符说明

none(默认的可省略)本地变量,可读可写,函数的输入参数既是这种类型

const声明变量或函数的参数为只读类型

attribute只能存在于vertex shader中,一般用于保存顶点或法线数据,它可以在数据缓冲区中读取数据

uniform在运行时shader无法改变uniform变量, 一般用来放置程序传递给shader的变换矩阵,材质,光照参数等等.

varying主要负责在vertex 和 fragment 之间传递变量

const:

和C语言类似,被const限定符修饰的变量初始化后不可变,除了局部变量,函数参数也可以使用const修饰符.但要注意的是结构变量可以用const修饰, 但结构中的字段不行.

const变量必须在声明时就初始化const vec3 v3 = vec3(0.,0.,0.)

局部变量只能使用const限定符.

函数参数只能使用const限定符.

attribute:

attribute变量是全局且只读的,它只能在vertex shader中使用,只能与浮点数,向量或矩阵变量组合, 一般attribute变量用来放置程序传递来的模型顶点,法线,颜色,纹理等数据它可以访问数据缓冲区 (还记得__gl.vertexAttribPointer__这个函数吧)

uniform:

uniform变量是全局且只读的,在整个shader执行完毕前其值不会改变,他可以和任意基本类型变量组合, 一般我们使用uniform变量来放置外部程序传递来的环境数据(如点光源位置,模型的变换矩阵等等) 这些数据在运行中显然是不需要被改变的.

varying:

varying类型变量是 vertex shader 与 fragment shader 之间的信使,一般我们在 vertex shader 中修改它然后在fragment shader使用它,但不能在 fragment shader中修改它.

要注意全局变量限制符只能为 const、attribute、uniform和varying中的一个.不可复合.

函数参数限定符:

函数的参数默认是以拷贝的形式传递的,也就是值传递,任何传递给函数参数的变量,其值都会被复制一份,然后再交给函数内部进行处理. 我们可以为参数添加限定符来达到传递引用的目的,glsl中提供的参数限定符如下:

限定符说明

< none: default >默认使用 in 限定符

in复制到函数中在函数中可读写

out返回时从函数中复制出来

inout复制到函数中并在返回时复制出来

in是函数参数的默认限定符,最终真正传入函数形参的其实是实参的一份拷贝.在函数中,修改in修饰的形参不会影响到实参变量本身.

out它的作用是向函数外部传递新值,out模式下传递进来的参数是write-only的(可写不可读).就像是一个"坑位",坑位中的值需要函数给他赋予. 在函数中,修改out修饰的形参会影响到实参本身.

inoutinout下,形参可以被理解为是一个带值的"坑位",及可读也可写,在函数中,修改inout修饰的形参会影响到实参本身.

glsl的函数:

glsl允许在程序的最外部声明函数.函数不能嵌套,不能递归调用,且必须声明返回值类型(无返回值时声明为void) 在其他方面glsl函数与c函数非常类似.

构造函数:

glsl中变量可以在声明的时候初始化,float pSize = 10.0也可以先声明然后等需要的时候在进行赋值.

聚合类型对象如(向量,矩阵,数组,结构) 需要使用其构造函数来进行初始化.vec4 color = vec4(0.0, 1.0, 0.0, 1.0);

类型转换:

glsl可以使用构造函数进行显式类型转换,各值如下:

精度限定:

glsl在进行光栅化着色的时候,会产生大量的浮点数运算,这些运算可能是当前设备所不能承受的,所以glsl提供了3种浮点数精度,我们可以根据不同的设备来使用合适的精度.

在变量前面加上highpmediumplowp即可完成对该变量的精度声明.

我们一般在片元着色器(fragment shader)最开始的地方加上precision mediump float;便设定了默认的精度.这样所有没有显式表明精度的变量 都会按照设定好的默认精度来处理.

如何确定精度:

变量的精度首先是由精度限定符决定的,如果没有精度限定符,则要寻找其右侧表达式中,已经确定精度的变量,一旦找到,那么整个表达式都将在该精度下运行.如果找到多个, 则选择精度较高的那种,如果一个都找不到,则使用默认或更大的精度类型.

invariant关键字:

由于shader在编译时会进行一些内部优化,可能会导致同样的运算在不同shader里结果不一定精确相等.这会引起一些问题,尤其是vertx shader向fragmeng shader传值的时候. 所以我们需要使用invariant关键字来显式要求计算结果必须精确一致. 当然我们也可使用#pragma STDGL invariant(all)来命令所有输出变量必须精确一致, 但这样会限制编译器优化程度,降低性能.

限定符的顺序:

当需要用到多个限定符的时候要遵循以下顺序:

1.在一般变量中: invariant > storage > precision

2.在参数中: storage > parameter > precision

我们来举例说明:

预编译指令:

以 # 开头的是预编译指令,常用的有:

比如#version 100他的意思是规定当前shader使用 GLSL ES 1.00标准进行编译,如果使用这条预编译指令,则他必须出现在程序的最开始位置.

内置的宏:

__LINE__: 当前源码中的行号.

__VERSION__: 一个整数,指示当前的glsl版本 比如 100 ps: 100 = v1.00

GL_ES: 如果当前是在 OPGL ES 环境中运行则 GL_ES 被设置成1,一般用来检查当前环境是不是 OPENGL ES.

GL_FRAGMENT_PRECISION_HIGH: 如果当前系统glsl的片元着色器支持高浮点精度,则设置为1.一般用于检查着色器精度.

实例:

1.如何通过判断系统环境,来选择合适的精度:

2.自定义宏:

内置的特殊变量

glsl程序使用一些特殊的内置变量与硬件进行沟通.他们大致分成两种 一种是input类型,他负责向硬件(渲染管线)发送数据. 另一种是output类型,负责向程序回传数据,以便编程时需要.

在 vertex Shader 中:

output 类型的内置变量:

变量说明单位

highp vec4gl_Position;gl_Position 放置顶点坐标信息vec4

mediump floatgl_PointSize;gl_PointSize 需要绘制点的大小,(只在gl.POINTS模式下有效)float

在 fragment Shader 中:

input 类型的内置变量:

变量说明单位

mediump vec4gl_FragCoord;片元在framebuffer画面的相对位置vec4

boolgl_FrontFacing;标志当前图元是不是正面图元的一部分bool

mediump vec2gl_PointCoord;经过插值计算后的纹理坐标,点的范围是0.0到1.0vec2

output 类型的内置变量:

变量说明单位

mediump vec4gl_FragColor;设置当前片点的颜色vec4 RGBA color

mediump vec4gl_FragData[n]设置当前片点的颜色,使用glDrawBuffers数据数组vec4 RGBA color

内置的常量

glsl提供了一些内置的常量,用来说明当前系统的一些特性. 有时我们需要针对这些特性,对shader程序进行优化,让程序兼容度更好.

在 vertex Shader 中:

1.const mediump intgl_MaxVertexAttribs>=8

gl_MaxVertexAttribs 表示在vertex shader(顶点着色器)中可用的最大attributes数.这个值的大小取决于 OpenGL ES 在某设备上的具体实现, 不过最低不能小于 8 个.

2.const mediump intgl_MaxVertexUniformVectors>= 128

gl_MaxVertexUniformVectors 表示在vertex shader(顶点着色器)中可用的最大uniform vectors数. 这个值的大小取决于 OpenGL ES 在某设备上的具体实现, 不过最低不能小于 128 个.

3.const mediump intgl_MaxVaryingVectors>= 8

gl_MaxVaryingVectors 表示在vertex shader(顶点着色器)中可用的最大varying vectors数. 这个值的大小取决于 OpenGL ES 在某设备上的具体实现, 不过最低不能小于 8 个.

4.const mediump intgl_MaxVertexTextureImageUnits>= 0

gl_MaxVaryingVectors 表示在vertex shader(顶点着色器)中可用的最大纹理单元数(贴图). 这个值的大小取决于 OpenGL ES 在某设备上的具体实现, 甚至可以一个都没有(无法获取顶点纹理)

5.const mediump intgl_MaxCombinedTextureImageUnits>= 8

gl_MaxVaryingVectors 表示在 vertex Shader和fragment Shader总共最多支持多少个纹理单元. 这个值的大小取决于 OpenGL ES 在某设备上的具体实现, 不过最低不能小于 8 个.

在 fragment Shader 中:

1.const mediump intgl_MaxTextureImageUnits>= 8

gl_MaxVaryingVectors 表示在 fragment Shader(片元着色器)中能访问的最大纹理单元数,这个值的大小取决于 OpenGL ES 在某设备上的具体实现, 不过最低不能小于 8 个.

2.const mediump intgl_MaxFragmentUniformVectors>= 16

gl_MaxFragmentUniformVectors 表示在 fragment Shader(片元着色器)中可用的最大uniform vectors数,这个值的大小取决于 OpenGL ES 在某设备上的具体实现, 不过最低不能小于 16 个.

3.const mediump intgl_MaxDrawBuffers= 1

gl_MaxDrawBuffers 表示可用的drawBuffers数,在OpenGL ES 2.0中这个值为1, 在将来的版本可能会有所变化.

glsl中还有一种内置的uniform状态变量,gl_DepthRange它用来表明全局深度范围.

结构如下:

除了 gl_DepthRange 外的所有uniform状态常量都已在glsl 1.30 中废弃.

流控制

glsl的流控制和c语言非常相似,这里不必再做过多说明,唯一不同的是片段着色器中有一种特殊的控制流discard. 使用discard会退出片段着色器,不执行后面的片段着色操作。片段也不会写入帧缓冲区。

内置函数库

glsl提供了非常丰富的函数库,供我们使用,这些功能都是非常有用且会经常用到的. 这些函数按功能区分大改可以分成7类:

通用函数:

下文中的 类型 T可以是 float, vec2, vec3, vec4,且可以逐分量操作.

方法说明

T abs(T x)返回x的绝对值

T sign(T x)比较x与0的值,大于,等于,小于 分别返回 1.0 ,0.0,-1.0

T floor(T x)返回<=x的最大整数

T ceil(T x)返回>=等于x的最小整数

T fract(T x)获取x的小数部分

T mod(T x, T y)

T mod(T x, float y)

取x,y的余数

T min(T x, T y)

T min(T x, float y)

取x,y的最小值

T max(T x, T y)

T max(T x, float y)

取x,y的最大值

T clamp(T x, T minVal, T maxVal)

T clamp(T x, float minVal,float maxVal)

min(max(x, minVal), maxVal),返回值被限定在 minVal,maxVal之间

T mix(T x, T y, T a)

T mix(T x, T y, float a)

取x,y的线性混合,x*(1-a)+y*a

T step(T edge, T x)

T step(float edge, T x)

如果 x

T smoothstep(T edge0, T edge1, T x)

T smoothstep(float edge0,float edge1, T x)

如果xedge1返回1.0, 否则返回Hermite插值

角度&三角函数:

下文中的 类型 T可以是 float, vec2, vec3, vec4,且可以逐分量操作.

方法说明

T radians(T degrees)角度转弧度

T degrees(T radians)弧度转角度

T sin(T angle)正弦函数,角度是弧度

T cos(T angle)余弦函数,角度是弧度

T tan(T angle)正切函数,角度是弧度

T asin(T x)反正弦函数,返回值是弧度

T acos(T x)反余弦函数,返回值是弧度

T atan(T y, T x)

T atan(T y_over_x)

反正切函数,返回值是弧度

指数函数:

下文中的 类型 T可以是 float, vec2, vec3, vec4,且可以逐分量操作.

方法说明

T pow(T x, T y)返回x的y次幂 xy

T exp(T x)返回x的自然指数幂 ex  如exp(1) = 2.718282

T log(T x)返回x的自然对数 ln   如log(2.718282) = 1.0

T exp2(T x)返回2的x次幂 2x

T log2(T x)返回2为底的对数 log2 如log2(4) =2

T sqrt(T x)开根号 √x 如sqrt(4) = 2

T inversesqrt(T x)先开根号,在取倒数,就是 1/√x

几何函数:

下文中的 类型 T可以是 float, vec2, vec3, vec4,且可以逐分量操作.

方法说明

float length(T x)返回矢量x的长度

float distance(T p0, T p1)返回p0 p1两点的距离

float dot(T x, T y)返回x y的点积

vec3 cross(vec3 x, vec3 y)返回x y的叉积

T normalize(T x)对x进行归一化,保持向量方向不变但长度变为1

T faceforward(T N, T I, T Nref)根据 矢量 N 与Nref 调整法向量

T reflect(T I, T N)返回 I - 2 * dot(N,I) * N, 结果是入射矢量 I 关于法向量N的 镜面反射矢量

T refract(T I, T N, float eta)返回入射矢量I关于法向量N的折射矢量,折射率为eta

矩阵函数:

mat可以为任意类型矩阵.

方法说明

mat matrixCompMult(mat x, mat y)将矩阵 x 和 y的元素逐分量相乘

向量函数:

下文中的 类型 T可以是 vec2, vec3, vec4, 且可以逐分量操作.

bvec指的是由bool类型组成的一个向量:

方法说明

bvec lessThan(T x, T y)逐分量比较x < y,将结果写入bvec对应位置

bvec lessThanEqual(T x, T y)逐分量比较 x <= y,将结果写入bvec对应位置

bvec greaterThan(T x, T y)逐分量比较 x > y,将结果写入bvec对应位置

bvec greaterThanEqual(T x, T y)逐分量比较 x >= y,将结果写入bvec对应位置

bvec equal(T x, T y)

bvec equal(bvec x, bvec y)

逐分量比较 x == y,将结果写入bvec对应位置

bvec notEqual(T x, T y)

bvec notEqual(bvec x, bvec y)

逐分量比较 x!= y,将结果写入bvec对应位置

bool any(bvec x)如果x的任意一个分量是true,则结果为true

bool all(bvec x)如果x的所有分量是true,则结果为true

bvec not(bvec x)bool矢量的逐分量取反

纹理查询函数:

图像纹理有两种 一种是平面2d纹理,另一种是盒纹理,针对不同的纹理类型有不同访问方法.

纹理查询的最终目的是从sampler中提取指定坐标的颜色信息. 函数中带有Cube字样的是指 需要传入盒状纹理. 带有Proj字样的是指带投影的版本.

以下函数只在vertex shader中可用:

以下函数只在fragment shader中可用:

在 vertex shader 与 fragment shader 中都可用:

官方的shader范例:

下面的shader如果你可以一眼看懂,说明你已经对glsl语言基本掌握了.

Vertex Shader:

Fragment Shader:

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念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

推荐阅读更多精彩内容