OpenGL ES 3.0(二)GLSL與著色器

1医窿、概述

在上一篇文章OpenGL ES 3.0(一)綜述 中提到,著色器(Shader)是運(yùn)行在GPU上的小程序。這些小程序?yàn)閳D形渲染管線的某個特定部分而運(yùn)行。從本質(zhì)上來說还绘,著色器只是一種把輸入轉(zhuǎn)化為輸出的程序。著色器也是一種非常獨(dú)立的程序栖袋,因?yàn)樗鼈冎g不能相互通信拍顷,它們之間唯一的溝通只有通過輸入和輸出。這篇文章主要討論用一種更加廣泛的形式詳細(xì)解釋著色器塘幅,特別是OpenGL著色器語言(GLSL)昔案。

2、GLSL

著色器是使用一種叫GLSL的類C語言寫成的晌块。GLSL是為圖形計(jì)算量身定制的,它包含一些針對向量和矩陣操作的有用特性帅霜。著色器的開頭總是要聲明版本匆背,接著是輸入和輸出變量、uniform和main函數(shù)身冀。每個著色器的入口點(diǎn)都是main函數(shù)钝尸,在這個函數(shù)中我們處理所有的輸入變量括享,并將結(jié)果輸出到輸出變量中。后面會進(jìn)行講解珍促。一個典型的著色器有下面的結(jié)構(gòu):


#version version_number

in type in_variable_name;

in type in_variable_name;

out type out_variable_name;

uniform type uniform_name;

int main()

{

  // 處理輸入并進(jìn)行一些圖形操作

  ...

  // 輸出處理過的結(jié)果到輸出變量

  out_variable_name = weird_stuff_we_processed;

}

當(dāng)討論到頂點(diǎn)著色器的時候铃辖,每個輸入變量也叫頂點(diǎn)屬性(Vertex Attribute)。能聲明的頂點(diǎn)屬性是有上限的猪叙,它一般由硬件來決定娇斩。OpenGL ES確保至少有16個包含4分量的頂點(diǎn)屬性可用,但是有些硬件或許允許更多的頂點(diǎn)屬性穴翩,可以查詢GL_MAX_VERTEX_ATTRIBS來獲取具體的上限:


var maxVertexAttribute = IntBuffer.allocate(1)

GLES30.glGetIntegerv(GL_MAX_VERTEX_ATTRIBS, maxVertexAttribute)

Log.d(TAG, "maxVertexAttribute:" + maxVertexAttribute.get(0))

3犬第、數(shù)據(jù)類型

和其他編程語言一樣,GLSL有數(shù)據(jù)類型可以來指定變量的種類芒帕。GLSL中包含C等其它語言大部分的默認(rèn)基礎(chǔ)數(shù)據(jù)類型:int歉嗓、float、double背蟆、uint和bool鉴分。GLSL也有兩種容器類型,分別是向量(Vector)和矩陣(Matrix)带膀,其中矩陣會單獨(dú)出一篇文章討論志珍。

GLSL中的向量是一個可以包含有1、2本砰、3或者4個分量的容器碴裙,分量的類型可以是前面默認(rèn)基礎(chǔ)類型的任意一個。它們可以是下面的形式(n代表分量的數(shù)量)点额。

類型 含義
vecn 包含n個float分量的默認(rèn)向量
bvecn 包含n個bool分量的向量
ivecn 包含n個int分量的向量
uvecn 包含n個unsigned int分量的向量
dvecn 包含n個double分量的向量

大多數(shù)時候使用vecn舔株,因?yàn)閒loat足夠滿足大多數(shù)要求了。一個向量的分量可以通過vec.x這種方式獲取还棱,這里x是指這個向量的第一個分量载慈。你可以分別使用.x、.y珍手、.z和.w來獲取它們的第1办铡、2、3琳要、4個分量寡具。GLSL也允許你對顏色使用rgba,或是對紋理坐標(biāo)使用stpq訪問相同的分量稚补。

向量這一數(shù)據(jù)類型也允許一些有趣而靈活的分量選擇方式童叠,叫做重組(Swizzling)。重組允許這樣的語法:


vec2 someVec;

vec4 differentVec = someVec.xyxx;

vec3 anotherVec = differentVec.zyw;

vec4 otherVec = someVec.xxxx + anotherVec.yxzy;

可以使用上面4個字母任意組合來創(chuàng)建一個和原來向量一樣長的(同類型)新向量课幕,只要原來向量有那些分量即可厦坛;但是不允許在一個vec2向量中去獲取.z元素五垮。也可以把一個向量作為一個參數(shù)傳給不同的向量構(gòu)造函數(shù),以減少需求參數(shù)的數(shù)量:


vec2 vect = vec2(0.5, 0.7);

vec4 result = vec4(vect, 0.0, 0.0);

vec4 otherResult = vec4(result.xyz, 1.0);

4杜秸、輸入與輸出

雖然著色器是各自獨(dú)立的小程序放仗,但是它們都是一個整體的一部分,出于這樣的原因撬碟,會希望每個著色器都有輸入和輸出诞挨,這樣才能進(jìn)行數(shù)據(jù)交流和傳遞。GLSL定義了in和out關(guān)鍵字專門來實(shí)現(xiàn)這個目的小作。每個著色器使用這兩個關(guān)鍵字設(shè)定輸入和輸出亭姥,只要一個輸出變量與下一個著色器階段的輸入匹配,它就會傳遞下去顾稀。但在頂點(diǎn)和片段著色器中會有點(diǎn)不同达罗。

頂點(diǎn)著色器應(yīng)該接收的是一種特殊形式的輸入,否則就會效率低下静秆。頂點(diǎn)著色器的輸入特殊在粮揉,它從頂點(diǎn)數(shù)據(jù)中直接接收輸入。為了定義頂點(diǎn)數(shù)據(jù)該如何管理抚笔,使用location這一元數(shù)據(jù)指定輸入變量扶认,這樣才可以在CPU上配置頂點(diǎn)屬性。在前面的文章已經(jīng)看過這個了殊橙,layout (location = 0)辐宾。頂點(diǎn)著色器需要為它的輸入提供一個額外的layout標(biāo)識,這樣才能把它鏈接到頂點(diǎn)數(shù)據(jù)膨蛮。也可以忽略layout (location = 0)標(biāo)識符叠纹,通過在OpenGL ES代碼中使用glGetAttribLocation()查詢屬性位置值(Location),但是在著色器中設(shè)置它們敞葛,會更容易理解而且節(jié)省工作量誉察。

另一個例外是片段著色器,它需要一個vec4顏色輸出變量惹谐,因?yàn)槠沃餍枰梢粋€最終輸出的顏色持偏。如果在片段著色器沒有定義輸出顏色,OpenGL會把物體渲染為黑色(或白色)氨肌。所以鸿秆,如果打算從一個著色器向另一個著色器發(fā)送數(shù)據(jù),必須在發(fā)送方著色器中聲明一個輸出怎囚,在接收方著色器中聲明一個類似的輸入卿叽。當(dāng)類型和名字都一樣的時候,OpenGL ES就會把兩個變量鏈接到一起,它們之間就能發(fā)送數(shù)據(jù)了(這是在鏈接程序?qū)ο髸r完成的)附帽。為了展示這是如何工作的,稍微改動一下前面一篇文章里的那個著色器井誉,讓頂點(diǎn)著色器為片段著色器決定顏色蕉扮。


// 頂點(diǎn)著色器

private val vertexShaderCode =

        "#version 300 es \n" +

        "out vec4 ourColor;" +

        " layout (location = 0) in vec3 aPos;" +

        "void main() {" +

                " gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0);" +

                " ourColor = vec4(0.5, 0.2, 0.1, 1.0);" +

        "}"

// 片段著色器

private val fragmentShaderCode = 

            "#version 300 es \n " +

                    "#ifdef GL_ES\n"+

                    "precision highp float;\n"+

                    "#endif\n"+

                    "out vec4 FragColor; " +

                    "in vec4 ourColor; " +

                  // "uniform vec4 outColor; " +

                    "void main() {" +

                    "  FragColor = ourColor ;" +

                    "}"

可以看到在頂點(diǎn)著色器中聲明了一個ourColor變量作為vec4輸出,并在片段著色器中聲明了一個類似的ourColor颗圣。由于它們名字相同且類型相同喳钟,片段著色器中的ourColor就和頂點(diǎn)著色器中的ourColor鏈接了。就可以通過這樣的方式將頂點(diǎn)著色器中的數(shù)據(jù)傳遞至片段著色器在岂。下面的圖片展示了輸出結(jié)果:

著色器間傳遞數(shù)據(jù)

5奔则、Uniform

Uniform是一種從CPU中的應(yīng)用向GPU中的著色器發(fā)送數(shù)據(jù)的方式,但uniform和頂點(diǎn)屬性有些不同蔽午。首先易茬,uniform是全局的(Global)。全局意味著uniform變量必須在每個著色器程序?qū)ο笾卸际仟?dú)一無二的及老,而且它可以被著色器程序的任意著色器在任意階段訪問抽莱。第二,無論把uniform值設(shè)置成什么骄恶,uniform會一直保存它們的數(shù)據(jù)食铐,直到它們被重置或更新。

可以在一個著色器中添加uniform關(guān)鍵字至類型和變量名前來聲明一個GLSL的uniform僧鲁。從此處開始就可以在著色器中使用新聲明的uniform了虐呻。通過uniform設(shè)置三角形的顏色:


private val fragmentShaderCode =

            "#version 300 es \n " +

                    "#ifdef GL_ES\n"+

                    "precision mediump float;\n"+

                    "#endif\n"+

                    "out vec4 FragColor; " +

                    //"in vec4 ourColor; " +

                    "uniform vec4 outColor; " +

                    "void main() {" +

                    "  FragColor = ourColor ;" +

                    "}"

在片段著色器中聲明了一個uniform vec4的ourColor,并把片段著色器的輸出顏色設(shè)置為uniform值的內(nèi)容寞秃。因?yàn)閡niform是全局變量斟叼,可以在任何著色器中定義它們,而無需通過頂點(diǎn)著色器作為中介蜕该。頂點(diǎn)著色器中不需要這個uniform犁柜,所以不用在那里定義它。如果聲明了一個uniform卻在GLSL代碼中沒用過堂淡,編譯器會靜默移除這個變量馋缅,導(dǎo)致最后編譯出的版本中并不會包含它。

這個uniform現(xiàn)在還是空的绢淀,還沒有給它添加任何數(shù)據(jù)萤悴。接下來首先需要找到著色器中uniform屬性的索引/位置值。當(dāng)?shù)玫絬niform的索引/位置值后皆的,就可以更新它的值了覆履。這次不去給像素傳遞單獨(dú)一個顏色,而是讓它隨著時間改變顏色:


// Triangle.kt

fun draw() {

        ...

        val timeValue = System.currentTimeMillis()

        val greenValue = Math.sin((timeValue / 300 % 50).toDouble()) / 2 + 0.5

        GLES30.glUseProgram(mProgram)

        val vertexColorLocation = GLES30.glGetUniformLocation(mProgram, "ourColor")

        ...

    }

// MyGLSurfaceView.kt

init {

        ...

        //renderMode = GLSurfaceView.RENDERMODE_WHEN_DIRTY

    }

首先通過System.currentTimeMillis()當(dāng)前時間。然后使用sin函數(shù)讓顏色在0.0到1.0之間改變硝全,最后將結(jié)果儲存到greenValue里栖雾。接著,用glGetUniformLocation()查詢uniform ourColor的位置值伟众。為查詢函數(shù)提供著色器程序和uniform的名字,這時候獲得的是查詢的屬性的位置值析藕。如果glGetUniformLocation返回-1就代表沒有找到這個位置值。最后凳厢,可以通過glUniform4f()設(shè)置uniform值账胧。注意,查詢uniform地址不要求之前使用過著色器程序先紫,但是更新一個uniform之前必須先使用程序,即調(diào)用glUseProgram()治泥,因?yàn)樗窃诋?dāng)前激活的著色器程序中設(shè)置uniform的。

因?yàn)镺penGL ES在其核心是一個C庫遮精,所以它不支持類型重載居夹,在函數(shù)參數(shù)不同的時候就要為其定義新的函數(shù);glUniform是一個典型例子本冲。這個函數(shù)有一個特定的后綴吮播,標(biāo)識設(shè)定的uniform的類型⊙劭。可能的后綴有:

后綴 含義
f 函數(shù)需要一個float作為它的值
i 函數(shù)需要一個int作為它的值
ui 函數(shù)需要一個unsigned int作為它的值
3f 函數(shù)需要3個float作為它的值
fv 函數(shù)需要一個float向量/數(shù)組作為它的值

每當(dāng)打算配置一個OpenGL ES的選項(xiàng)時就可以簡單地根據(jù)這些規(guī)則選擇適合的數(shù)據(jù)類型的重載函數(shù)意狠。在例子里,希望分別設(shè)定uniform的4個float值疮胖,所以通過glUniform4f傳遞數(shù)據(jù)环戈。

這邊要注意下需要將MyGLSurfaceView.kt里面的renderMode = GLSurfaceView.RENDERMODE_WHEN_DIRTY 這個模式注釋掉,不然不會自動刷新數(shù)據(jù)澎灸。最終效果如下:

變換效果

6院塞、更多屬性

在前面的文章中,提到了如何填充VBO性昭、配置頂點(diǎn)屬性指針以及如何把它們都儲存到一個VAO里拦止。這次,同樣打算把顏色數(shù)據(jù)加進(jìn)頂點(diǎn)數(shù)據(jù)中糜颠。將把顏色數(shù)據(jù)添加為3個float值至vertices數(shù)組汹族。將把三角形的三個角分別指定為紅色、綠色和藍(lán)色:


internal var vertices = floatArrayOf(// 按逆時針順序

                // 位置              // 顏色

                0.5f, -0.5f, 0.0f,  1.0f, 0.0f, 0.0f,  // 右下

                -0.5f, -0.5f, 0.0f,  0.0f, 1.0f, 0.0f,  // 左下

                0.0f,  0.5f, 0.0f,  0.0f, 0.0f, 1.0f    // 頂部

        )

由于現(xiàn)在有更多的數(shù)據(jù)要發(fā)送到頂點(diǎn)著色器其兴,有必要去調(diào)整一下頂點(diǎn)著色器顶瞒,使它能夠接收顏色值作為一個頂點(diǎn)屬性輸入。需要注意的是用layout標(biāo)識符來把a(bǔ)Color屬性的位置值設(shè)置為1:


private val vertexShaderCode =

            "#version 300 es \n" +

                    " layout (location = 0) in vec3 aPos;" +

                    "layout (location = 1) in vec3 aColor;" +

                    "out vec3 ourColor;" +

                    "void main() {" +

                    " gl_Position = vec4(aPos, 1.0);" +

                    " ourColor = aColor;" +

                    "}"

由于不再使用uniform來傳遞片段的顏色了元旬,現(xiàn)在使用ourColor輸出變量榴徐,必須再修改一下片段著色器:


private val fragmentShaderCode =

            "#version 300 es \n " +

                    "#ifdef GL_ES\n" +

                    "precision highp float;\n" +

                    "#endif\n" +

                    "out vec4 FragColor; " +

                    "in vec3 ourColor; " +

                    "void main() {" +

                    "  FragColor = vec4(ourColor, 1.0) ;" +

                    "}"

因?yàn)樘砑恿肆硪粋€頂點(diǎn)屬性守问,并且更新了VBO的內(nèi)存,就必須重新配置頂點(diǎn)屬性指針坑资。更新后的VBO內(nèi)存中的數(shù)據(jù)現(xiàn)在看起來像這樣:

VBO中數(shù)據(jù)存儲格式

此時就需要使用glVertexAttribPointer()更新頂點(diǎn)格式,并且啟用索引為1的頂點(diǎn)數(shù)組:


init {

        GLES30.glVertexAttribPointer(0,3, GLES30.GL_FLOAT, false, vertexStride, 0)

        GLES30.glVertexAttribPointer(1, 3, GLES30.GL_FLOAT, false, vertexStride, 3*4)

}

fun draw() {

        GLES30.glEnableVertexAttribArray(0);

        GLES30.glEnableVertexAttribArray(1);

        GLES30.glUseProgram(mProgram)

        GLES30.glBindVertexArray(VAOids.get(0))

        GLES30.glDrawElements(GLES30.GL_TRIANGLES, 3, GLES30.GL_UNSIGNED_INT, 0);

        GLES30.glDisableVertexAttribArray(0)

        GLES30.glDisableVertexAttribArray(1);

    }

companion object {

        internal val COORDS_PER_VERTEX = 6

        internal val vertexStride = COORDS_PER_VERTEX * 4

        internal var indices = intArrayOf(// 按逆時針順序

                0, 1, 2

        )

        internal var vertices = floatArrayOf(// 按逆時針順序

                // 位置              // 顏色

                0.5f, -0.5f, 0.0f,  1.0f, 0.0f, 0.0f,  // 右下

                -0.5f, -0.5f, 0.0f,  0.0f, 1.0f, 0.0f,  // 左下

                0.0f,  0.5f, 0.0f,  0.0f, 0.0f, 1.0f    // 頂部

        )

    }

glVertexAttribPointer()的前幾個參數(shù)比較明了耗帕。這次配置屬性位置值為1的頂點(diǎn)屬性。每個顏色值占3個float袱贮,并且數(shù)據(jù)就是標(biāo)準(zhǔn)化的不需要再次標(biāo)準(zhǔn)化兴垦。

由于現(xiàn)在有了兩個頂點(diǎn)屬性,需要重新計(jì)算步長值字柠。為獲得數(shù)據(jù)隊(duì)列中下一個屬性值(比如位置向量的下個x分量)必須向右移動6個float,其中3個是位置值狡赐,另外3個是顏色值窑业。所以步長值為6乘以float的字節(jié)數(shù)(=24字節(jié))。

同樣枕屉,這次必須指定一個偏移量常柄。對于每個頂點(diǎn)來說,位置頂點(diǎn)屬性在前搀擂,所以它的偏移量是0西潘。顏色屬性緊隨位置數(shù)據(jù)之后,所以偏移量就是3 * float的字節(jié)數(shù)哨颂,用字節(jié)來計(jì)算就是12字節(jié)喷市。

運(yùn)行程序結(jié)果如下:

賦值頂點(diǎn)顏色效果

雖然只提供了3個顏色,但出現(xiàn)的效果確實(shí)一個大調(diào)色板威恼。這是在片段著色器中進(jìn)行的所謂片段插值(Fragment Interpolation)的結(jié)果品姓。當(dāng)渲染一個三角形時,光柵化(Rasterization)階段通常會造成比原指定頂點(diǎn)更多的片段箫措。光柵會根據(jù)每個片段在三角形形狀上所處相對位置決定這些片段的位置腹备。

基于這些位置,它會插值(Interpolate)所有片段著色器的輸入變量斤蔓。比如說植酥,有一個線段,上面的端點(diǎn)是綠色的弦牡,下面的端點(diǎn)是藍(lán)色的友驮。如果一個片段著色器在線段的70%的位置運(yùn)行,它的顏色輸入屬性就會是一個綠色和藍(lán)色的線性結(jié)合驾锰;更精確地說就是30%藍(lán) + 70%綠喊儡。

這個三角形也是如此,有3個頂點(diǎn)稻据,和相應(yīng)的3個顏色艾猜,片段著色器為這三個點(diǎn)圍起來的這些像素進(jìn)行插值顏色买喧。

Tips:在最前面的OpenGL ES 2.0 顯示圖形(上)這篇文章中對于GLSL中的輸入輸出并并不是用關(guān)鍵字in 和 out來表示的而是用vary、atrribute匆赃。其實(shí)這兩者對應(yīng)與一個意思淤毛。會出現(xiàn)不一樣 這是由于GLSL的版本不同,在OpenGL ES 3.0版本中已經(jīng)將vary算柳、atrribute廢棄而使用in和out低淡。由于OpenGL ES 2.0 的GLSL還是使用老版本,這也是我在這個系列中使用3.0為例子而不是2.0的其中一個原因瞬项。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末蔗蹋,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子囱淋,更是在濱河造成了極大的恐慌猪杭,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,968評論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件妥衣,死亡現(xiàn)場離奇詭異皂吮,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)税手,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,601評論 2 382
  • 文/潘曉璐 我一進(jìn)店門蜂筹,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人芦倒,你說我怎么就攤上這事艺挪。” “怎么了兵扬?”我有些...
    開封第一講書人閱讀 153,220評論 0 344
  • 文/不壞的土叔 我叫張陵闺属,是天一觀的道長。 經(jīng)常有香客問我周霉,道長掂器,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,416評論 1 279
  • 正文 為了忘掉前任俱箱,我火速辦了婚禮国瓮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘狞谱。我一直安慰自己乃摹,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,425評論 5 374
  • 文/花漫 我一把揭開白布跟衅。 她就那樣靜靜地躺著孵睬,像睡著了一般。 火紅的嫁衣襯著肌膚如雪伶跷。 梳的紋絲不亂的頭發(fā)上掰读,一...
    開封第一講書人閱讀 49,144評論 1 285
  • 那天秘狞,我揣著相機(jī)與錄音,去河邊找鬼蹈集。 笑死烁试,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的拢肆。 我是一名探鬼主播减响,決...
    沈念sama閱讀 38,432評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼郭怪!你這毒婦竟也來了支示?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,088評論 0 261
  • 序言:老撾萬榮一對情侶失蹤鄙才,失蹤者是張志新(化名)和其女友劉穎颂鸿,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體咒循,經(jīng)...
    沈念sama閱讀 43,586評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,028評論 2 325
  • 正文 我和宋清朗相戀三年绞愚,在試婚紗的時候發(fā)現(xiàn)自己被綠了叙甸。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,137評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡位衩,死狀恐怖裆蒸,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情僚祷,我是刑警寧澤,帶...
    沈念sama閱讀 33,783評論 4 324
  • 正文 年R本政府宣布贮缕,位于F島的核電站,受9級特大地震影響感昼,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜定嗓,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,343評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望宵溅。 院中可真熱鬧,春花似錦恃逻、人聲如沸雏搂。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,333評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽畔派。三九已至铅碍,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間线椰,已是汗流浹背胞谈。 一陣腳步聲響...
    開封第一講書人閱讀 31,559評論 1 262
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留憨愉,地道東北人烦绳。 一個月前我還...
    沈念sama閱讀 45,595評論 2 355
  • 正文 我出身青樓,卻偏偏與公主長得像配紫,于是被迫代替她去往敵國和親径密。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,901評論 2 345

推薦閱讀更多精彩內(nèi)容