Unity3D Shader 入門

轉載注明出處:點擊打開鏈接

Shader(著色器)是一段能夠針對3D對象進行操作、并被GPU所執行的程序。Shader并不是一個統一的標準,不同的圖形接口的Shader并不相同。OpenGL的著色語言是GLSL, NVidia開發了Cg,而微軟的Direct3D使用高級著色器語言(HLSL)。而Unity的Shader 是將傳統的圖形接口的Shader(由 Cg / HLSL編寫)嵌入到獨有的描述性結構中而形成的一種代碼生成框架,最終會自動生成各硬件平臺自己的Shader,從而實現跨平臺。

Shader程序結構

屬性定義(Property Definition):定義Shader的輸入,這些輸入可以在材質編輯的時候指定

子著色器(SubShader):一個Shader可以有多個子著色器。這些子著色器互不相干且只有一個會在最終的平臺運行。編寫多個的目的是解決兼容性問題。Unity會自己選擇兼容終端平臺的Shader運行。

回滾(Fallback):如果子著色器在終端平臺上都無法運行,那么使用Fallback指定的備用Shader,俗稱備胎。

Pass:一個Pass就是一次繪制。對于表面著色器,只能有一個Pass,所以不存在Pass節。頂點片段著色器可以有多個Pass。多次Pass可以實現很多特殊效果,例如當人物被環境遮擋時還可以看到人物輪廓就可以用多Pass來實現。

Cg代碼:每個Pass中都可以包含自定義的Cg代碼,從CGPROGRAM開始到ENDCG結束。

Shader 輸入

Shader的輸入有兩個來源,一是通過屬性定義,一是通過Shader.SetGlobalXXX方法全局設置。
屬性定義變量:屬性定義中的變量是Shader參數的主要設置方式。 它是隨材質變化的,每個使用該Shader的材質都可以在Inspector或者腳本中設置這些參數。這些參數除了在Shader的Properties段中定義外,還需要在Cg中聲明方可使用。例如上面表面著色器的例子中我們定義了_MainTex這個類型為2D的屬性,還需要在Cg中聲明 sampler2D _MainTex。

全局變量:Shader有一組SetGlobalXXX方法,可以對Shader的在Cg中定義而沒有在屬性中定義的uniform變量進行設置。這個設置是全局的,所有定義了該uniform的Shader都會受到影響。例如我們希望場景隨著時間變化而改變顏色,就可以給場景所使用到的Shader設置統一的全局顏色變量,然后在腳本中通過設置該顏色來改變場景的顏色。在角色釋放技能時場景變黑也可以使用這個方法。

Unity shader 中允許定義的屬性類型有:

關鍵字
類型
對應Cg類型

Float
浮點數
float
_MyFloat (“My float”, Float) = 0.5

Range
浮點數 (在指定范圍內)
float
_MyRange (“My Range”, Range(0.01, 0.5)) = 0.1

Color
浮點四元組
float4
_MyColor (“Some Color”, Color) = (1,1,1,1)

Vector
浮點四元組
float4
_MyVector(“Some Vector”,Vector) = (1,1,1,1)

2D
2的階數大小的貼圖
sampler2D
_MyTexture (“Texture”, 2D) = “white” {}

Rect
非2的階數大小的貼圖
sampler2D
_MyRect(“My Rect”, Rect) = “white” {}

CUBE
CubeMap
samplerCUBE
_MyCubemap (“Cubemap”, CUBE) = “” {}

注:CubeMap 是6張有聯系的2D貼圖的組合主要用來做反射效果(比如天空盒和動態反射)
SubShader
SubShader中除了Pass,有兩個標簽值得關注:LOD和Tags

LOD

LOD是 Level of Detail的簡寫,確切地說是Shader Level of Detail的簡寫,因為Unity中還有一個模型的LOD概念,這是兩個不同的東西。我們這里只介紹Shader中LOD
Shader LOD 就是讓我們設置一個數值,這個數值決定了我們能用什么樣的Shader。可以通過Shader.maximumLOD或者Shader.globalMaximumLOD 設定允許的最大LOD,當設定的LOD小于SubShader所指定的LOD時,這個SubShader將不可用。通過LOD,我們就可以為某個材質寫一組SubShader,指定不同的LOD,LOD越大則渲染效果越好,當然對硬件的要求也可能越高,然后根據不同的終端硬件配置來設置 globalMaximumLOD來達到兼顧性能的最佳顯示效果。
Unity內建Shader定義了一組LOD的數值,我們在實現自己的Shader的時候可以將其作為參考來設定自己的LOD數值
VertexLit及其系列 = 100
Decal, Reflective VertexLit = 150
Diffuse = 200
Diffuse Detail, Reflective Bumped Unlit, Reflective Bumped VertexLit = 250
Bumped, Specular = 300
Bumped Specular = 400
Parallax = 500
Parallax Specular = 600

Tag

SubShader可以被若干的標簽(tags)所修飾,而硬件將通過判定這些標簽來決定什么時候調用該著色器。 比較常見的標簽有:
Queue 這個標簽很重要,它定義了一個整數,決定了Shader的渲染的次序,數字越小就越早被渲染,早渲染就意味著可能被后面渲染的東西覆蓋掉看不見。 預定義的Queue有:

名字

描述

Background
1000
最早被調用的渲染,用來渲染天空盒或者背景

Geometry
2000
這是默認值,用來渲染非透明物體(普通情況下,場景中的絕大多數物體應該是非透明的)

AlphaTest
2450
用來渲染經過Alpha Test的像素,單獨為AlphaTest設定一個Queue是出于對效率的考慮

Transparent
3000
以從后往前的順序渲染透明物體

Overlay
4000
用來渲染疊加的效果,是渲染的最后階段(比如鏡頭光暈等特效)

RenderType “Opaque”或”Transparent”是兩個常用的RenderType。如果輸出中都是非透明物體,那寫在Opaque里;如果想渲染透明或者半透明的像素,那應該寫在Transparent中。這個Tag主要用ShaderReplacement,一般情況下這Tag好像也沒什么作用。

CommonState
SubShader中可以定義一組Render State,基本上就是一些渲染的開關選項,他們對該SubShader的所有的Pass都有效,所以稱Common。這些Render State也可以在每個Pass中分別定義,將在Pass中詳細介紹。
Pass
Render State
Render State主要就是控制渲染過程的一些開關選項,例如是否開啟alpha blending ,是否開啟depth testing。 常用的Render State有:
Cull 用法:Cull Back | Front | Off 多邊形表面剔除開關。Back表示背面剔除,Front表示正面剔除,Off表示關閉表面剔除即雙面渲染。有時候如裙擺,飄帶之類很薄的東西在建模時會做成一個面片,這就需要設置Cull Off來雙面渲染,否則背面會是黑色。

ZWrite 用法:ZWrite On | Off 控制當前對象的像素是否寫入深度緩沖區(depth buffer),默認是開啟的。一般來說繪制不透明物體的話ZWrite開啟,繪制透明或半透明物體則ZWrite關閉。 深度緩沖區:當圖形處理卡渲染物體的時候,每一個所生成的像素的深度(即 z 坐標)就保存在一個緩沖區中。這個緩沖區叫作 z 緩沖區或者深度緩沖區,這個緩沖區通常組織成一個保存每個屏幕像素深度的 x-y 二維數組。如果場景中的另外一個物體也在同一個像素生成渲染結果,那么圖形處理卡就會比較二者的深度,并且保留距離觀察者較近的物體。然后這個所保留的物體點深度保存到深度緩沖區中。最后,圖形卡就可以根據深度緩沖區正確地生成通常的深度感知效果:較近的物體遮擋較遠的物體。 理解了深度緩沖區也就理解了為什么繪制透明或半透明物體需要關閉ZWrite, 如果不關閉,透明物體的depth也會被寫入深度緩沖區,從而會剔除掉它后面的物體,后面的物體就不會被渲染,看不見后面的物體還能叫透明嗎?因此我們使用Alpha blending的時候需要設置ZWrite Off。

ZTest 用法:ZTest (Less | Greater | LEqual | GEqual | Equal | NotEqual | Always) 控制如何進行深度測試,也就是上面說的圖形處理卡比較二者的深度的比較方法。默認是LEqual。 值得一提的是使用Aplha blending的時候ZWrite需要關閉但是ZTest是要開啟的,因為如果透明物體前面還有不透明物體,透明物體還是應該被遮擋剔除的。

Blend 混合。控制了每個Shader的輸出如何和屏幕上已有的顏色混合。 用法: Blend Off: 關閉混合 Blend SrcFactor DstFactor:最終顏色 = Shader產生的顏色 × SrcFactor + 屏幕上原來的顏色 × DstFactor Blend SrcFactor DstFactor, SrcFactorA DstFactor:和上面一樣,只是Alpha通道使用后面兩個參數計算 常用的Blend模式有: Blend SrcAlpha OneMinusSrcAlpha // Alpha blending Blend One One // Additive Blend OneMinusDstColor One // Soft Additive Blend DstColor Zero // Multiplicative Blend DstColor SrcColor // 2x Multiplicative
Unity5開始下列固定功能的Shader命令被標記為過時了,這些命令的功能現在建議在Shader(Cg)中通過代碼來實現,這里列出是為了方便閱讀以前寫的Shader:

Lighting On | Off
Material { Material Block }
SeparateSpecular On | Off
Color Color-value
ColorMaterial AmbientAndDiffuse | Emission
Fog { Fog Block }
AlphaTest (Less | Greater | LEqual | GEqual | Equal | NotEqual | Always) CutoffValue
SetTexture textureProperty { combine options }

Surface Shader

Surface Shader 隱藏了很多光照處理的細節,它的設計初衷是為了讓用戶僅僅使用一些指令(#pragma)就可以完成很多事情,并且封裝了很多常用的光照模型和函數。相比底層的Vertex And Fragment Shader,Suface Shader的限制比較多,它只能有一次Pass。如果做一些常規的功能又需要光照,可以用Surface Shader寫,比較快速便捷。如果要寫比較高級的Shader還是建議使用Vertex Shader 和 Fragment Shader。 Surface Shader主要有兩部分組成,一個是#pragma后面的指令,一個是surf函數。 pragma的語法是 #pragma surface surfaceFunction lightModel [optionalparams]

  • surfaceFunction 通常就是名為surf的函數, 函數名可以自己取 surf函數原型是:void surf (Input IN, inout SurfaceOutput o)
  • lightModel是Unity內置的光照模型,可以是Lambert,Blinn-Phong等。 - optionalparams: 包含很多指令
    surf函數主要有一個Input結構的輸入和SurfaceOutput結構的輸出。
    Input
    Input 結構需要在Shader中定義。它可以包含如下字段, 如果你定義了這些字段就可以在surf函數中使用它們(好神奇的黑科技)
    多個貼圖的uv坐標,名字必須符合格式uv+貼圖名。例如 float2 uv_MainTex

float3 viewDir - 視圖方向( view direction)值。為了計算視差效果(Parallax effects),邊緣光照(rim lighting)等,需要包含視圖方向( view direction)值。
float4 with COLOR semantic - 每個頂點(per-vertex)顏色的插值。
float4 screenPos - 屏幕空間中的位置。 為了反射效果,需要包含屏幕空間中的位置信息。比如在Dark Unity中所使用的 WetStreet著色器。
float3 worldPos - 世界空間中的位置。
float3 worldRefl - 世界空間中的反射向量。如果表面著色器(surface shader)不寫入法線(o.Normal)參數,將包含這個參數。 請參考這個例子:Reflect-Diffuse 著色器。
float3 worldNormal - 世界空間中的法線向量(normal vector)。如果表面著色器(surface shader)不寫入法線(o.Normal)參數,將包含這個參數。
float3 worldRefl; INTERNAL_DATA - 世界空間中的反射向量。如果表面著色器(surface shader)不寫入法線(o.Normal)參數,將包含這個參數。
float3 worldNormal; INTERNAL_DATA -世界空間中的法線向量(normal vector)。如果表面著色器(surface shader)不寫入法線(o.Normal)參數,將包含這個參數。

SurfaceOutput

SurfaceOutput 描述了表面的特性(光照的顏色反射率、法線、散射、鏡面等),這個結構是固定的,不需要在Shader中再定義。
struct SurfaceOutput { half3 Albedo; //反射率,一般就是在光照之前的原始顏色 half3 Normal; //法線 half3 Emission; //自發光,用于增強物體自身的亮度,使之看起來好像可以自己發光 half Specular; //鏡面 half Gloss; //光澤 half Alpha; //透明 };

Unity5 由于引入了基于物理的光照模型,所以新增加了兩個Output
struct SurfaceOutputStandard { fixed3 Albedo; // base (diffuse or specular) color fixed3 Normal; // tangent space normal, if written half3 Emission; half Metallic; // 0=non-metal, 1=metal half Smoothness; // 0=rough, 1=smooth half Occlusion; // occlusion (default 1) fixed Alpha; // alpha for transparencies }; struct SurfaceOutputStandardSpecular { fixed3 Albedo; // diffuse color fixed3 Specular; // specular color fixed3 Normal; // tangent space normal, if written half3 Emission; half Smoothness; // 0=rough, 1=smooth half Occlusion; // occlusion (default 1) fixed Alpha; // alpha for transparencies };

Unity提供了一些基本的SurfaceShader的例子,有助于我們理解輸入輸出是如何被使用的。 Unity提供的SurfaceShader的例子
Vertex Shader
如果不想使用Surface Shader而直接編寫opengl和Direct3D中常見的頂點著色器和片段著色器,可以通過Cg代碼段嵌入到Pass中:
Pass { // ... the usual pass state setup ... CGPROGRAM // compilation directives for this snippet, e.g.: #pragma vertex vert #pragma fragment frag // the Cg/HLSL code itself ENDCG // ... the rest of pass setup ... }

其中vert就是頂點著色器函數,frag就是片段著色器函數。一般來說,可以在頂點著色器中進行的計算就不應該放到片段著色器中去算,因為頂點著色器是逐頂點計算的而片段著色器是逐像素計算的,一個模型頂點總比表明像素少很多吧。
編寫頂點和片段著色器一般需要包含Unity預定義的一個幫助文件UnityCG.cginc,里面預定義了一些常用的結構和方法。Windows版Unity這個文件位于({unity install path}/Data/CGIncludes/UnityCG.cginc
。 Mac版位于/Applications/Unity/Unity.app/Contents/CGIncludes/UnityCG.cginc

在代碼中我們只需要添加 #include "UnityCG.cginc"
就可以使用里面的結構和方法。
Input
頂點著色器的原型是 v2f vert (appdata v) appdata 是輸入,可以自己定義也可以使用Unity預定義的。Unity在UnityCG.cginc預定義了三種常用的輸入結構:appdata_base,appdata_tan,appdata_full。
struct appdata_base { float4 vertex : POSITION; float3 normal : NORMAL; float4 texcoord : TEXCOORD0; }; struct appdata_tan { float4 vertex : POSITION; float4 tangent : TANGENT; float3 normal : NORMAL; float4 texcoord : TEXCOORD0; }; struct appdata_full { float4 vertex : POSITION; float4 tangent : TANGENT; float3 normal : NORMAL; float4 texcoord : TEXCOORD0; float4 texcoord1 : TEXCOORD1; float4 texcoord2 : TEXCOORD2; float4 texcoord3 : TEXCOORD3; #if defined(SHADER_API_XBOX360) half4 texcoord4 : TEXCOORD4; half4 texcoord5 : TEXCOORD5; #endif fixed4 color : COLOR; };

我們注意到這些結構的字段和表面著色器中的字段不同,后面多了一個冒號和一個標簽。這是該字段的語義,用于告訴GPU這個字段的數據應該去哪里讀寫。GPU畢竟是為了圖形計算而特別設計的東西,很多東西都是固定的,我們只要記得有這么幾個名字可以用行了。
類型
名字
標簽
備注

float4
vertex
POSITION
頂點在模型坐標系下的位置

float3
normal
NORMAL
頂點的法向量

float4
tangent
TANGENT
頂點的切向量

float4
color
COLOR
頂點色

float4
texcoord
TEXCOORD0
頂點的第一個uv坐標

float4
texcoord1
TEXCOORD1
頂點的第二個uv坐標,最多可以到5

Output
頂點著色器的輸出是也是一個可以自己定義的結構,但是結構內容也是比較固定的,一般包含了頂點投影后的位置,uv,頂點色等,也可以加一些后面片段著色器需要用到但是需要在頂點著色器中計算的值。這個輸出就是后面片段著色器的輸入。
struct v2f { float4 pos : SV_POSITION; half2 uv : TEXCOORD0; };

可以使用的字段有:
類型
標簽
描述

float4
SV_POSITION
頂點在投影空間下的位置,注意和輸入的模型坐標系下的位置不同,這個字段必必須設置,這個坐標轉換是頂點著色器的重要工作

float3
NORMAL
頂點在視圖坐標系下的法向量

float4
TEXCOORD0
第一張貼圖的uv坐標

float4
TEXCOORD1
第二張貼圖的uv坐標

float4
TANGENT
切向量,主要用來修正法線貼圖Normal Maps

fixed4
COLOR
第一個定點色

fixed4
COLOR1
第二個定點色

Any
Any
其他自定義的字段

坐標變換

頂點著色器有一項重要的工作就是進行坐標變換。頂點著色器的輸入中的坐標是模型坐標系(ObjectSpace)下的坐標,而最終繪制到屏幕上的是投影坐標。 在我們Shader里面只需要一句話就可以完成坐標的轉換,這也是最簡單的頂點著色器:
v2f vert(appdata v) { v2f o; o.pos = mul(UNITY_MATRIX_MVP, v.vertex); return o; }

用UNITY_MATRIX_MVP矩陣乘以頂點在模型坐標系下的坐標就得到投影坐標。 UNITY_MATRIX_MVP是Unity內建的模型->視->投影矩陣, Unity內建矩陣如下:
UNITY_MATRIX_MVP:當前模型->視圖->投影矩陣。(注:模型矩陣為 本地->世界)
UNITY_MATRIX_MV:當前模型->視圖矩陣
UNITY_MATRIX_V:當前視圖矩陣
UNITY_MATRIX_P:當前投影矩陣
UNITY_MATRIX_VP:當前視圖->投影矩陣
UNITY_MATRIX_T_MV:轉置模型->視圖矩陣
UNITY_MATRIX_IT_MV:逆轉置模型->視矩陣, 用于將法線從ObjectSpace旋轉到WorldSpace。為什么法線變化不能和位置變換一樣用UNITY_MATRIX_MV呢?一是因為法線是3維的向量而- UNITY_MATRIX_MV是一個4x4矩陣,二是因為法線是向量,我們只希望對它旋轉,但是在進行空間變換的時候,如果發生非等比縮放,方向會發生偏移。
UNITY_MATRIX_TEXTURE0 to UNITY_MATRIX_TEXTURE3:紋理變換矩陣

下面簡單介紹一下里面提到的幾個坐標系: 模型坐標系:也叫物體坐標系,3D建模的時候每個模型都是在自己的坐標系下建立的,如果一個人物模型腳底是(0,0,0) 點的話它的身上其它點的坐標都是相對腳底這個原點的。 世界坐標系:我們場景是一個世界,有自己的原點,模型放置到場景中后模型上的每個頂點就有了一個新的世界坐標。這個坐標可以通過模型矩陣×模型上頂點的模型坐標得到。 視圖坐標系:又叫觀察坐標系,是以觀察者(相機)為原點的坐標系。場景中的物體只有被相機觀察到才會繪制到屏幕上,相機可以設置視口大小和裁剪平面來控制可視范圍,這些都是相對相機來說的,所以需要把世界坐標轉換到視圖坐標系來方便處理。 投影坐標系:場景是3D的,但是最終繪制到屏幕上是2D,投影坐標系完成這個降維的工作,投影變換后3D的坐標就變成2D的坐標了。投影有平行投影和透視投影兩種,可以在Unity的相機上設置。 屏幕坐標系 : 最終繪制到屏幕上的坐標。屏幕的左下角為原點。
除了內建矩陣,Unity還內建了一些輔助函數也可以在頂點著色器里面使用:
float3 WorldSpaceViewDir (float4 v):根據給定的局部空間頂點位置到相機返回世界空間的方向(非規范化的)
float3 ObjSpaceViewDir (float4 v):根據給定的局部空間頂點位置到相機返回局部空間的方向(非規范化的)
float2 ParallaxOffset (half h, half height, half3 viewDir):為視差法線貼圖計算UV偏移
fixed Luminance (fixed3 c):將顏色轉換為亮度(灰度)
fixed3 DecodeLightmap (fixed4 color):從Unity光照貼圖解碼顏色(基于平臺為RGBM 或dLDR)
float4 EncodeFloatRGBA (float v):為儲存低精度的渲染目標,編碼[0..1)范圍的浮點數到RGBA顏色。
float DecodeFloatRGBA (float4 enc):解碼RGBA顏色到float。
float2 EncodeViewNormalStereo (float3 n):編碼視圖空間法線到在0到1范圍的兩個數。
float3 DecodeViewNormalStereo (float4 enc4):從enc4.xy解碼視圖空間法線

Fragment Shader
// TODO

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,488評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,034評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,327評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,554評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,337評論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,883評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,975評論 3 439
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,114評論 0 286
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,625評論 1 332
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,555評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,737評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,244評論 5 355
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 43,973評論 3 345
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,362評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,615評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,343評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,699評論 2 370

推薦閱讀更多精彩內容