Unity自定义SRP(三):平行光

https://catlikecoding.com/unity/tutorials/custom-srp/directional-lights/

光照

Lit Shader

这里新建一个可接受光照的shader。新建LitPass.hlsl文件,内容和UnlitPass.hlsl一致,修改函数名:

#ifndef CUSTOM_LIT_PASS_INCLUDED
#define CUSTOM_LIT_PASS_INCLUDED

...

Varyings LitPassVertex(Attributes input)
{
}

float4 LitPassFragment(Varyings input) : SV_TARGET
{
}
#endif

Lit.shader:

Shader "Custom RP/Lit" {
    
    Properties {
        _BaseMap("Texture", 2D) = "white" {}
        _BaseColor("Color", Color) = (0.5, 0.5, 0.5, 1.0)
        ...
    }
    
    SubShader {
        Pass {
            HLSLPROGRAM
            ...
            #pragma vertex LitPassVertex
            #pragma fragment LitPassFragment
            #include "LitPass.hlsl"
            ENDHLSL
        }
}

我们将使用自定义的光照模式,加上标签:

        Pass {
            Tags {
                "LightMode" = "CustomLit"
            }

CameraRenderer中加入该pass:

    static ShaderTagId
        unlitShaderTagId = new ShaderTagId("SRPDefaultUnlit"),
        litShaderTagId = new ShaderTagId("CustomLit");

接着添加到DrawVisibleGeometry中:

        var drawingSettings = new DrawingSettings(unlitShaderTagId, sortingSettings)
        {
            enableDynamicBatching = useDynamicBatching,
            enableInstancing = useGPUInstancing
        };
        drawingSettings.SetShaderPassName(1, litShaderTagId);

法线

为使用光照模型,我们需要法线。在结构体中声明:

struct Attributes
{
    float3 positionOS : POSITION;
    float3 normalOS : NORMAL;
    float2 baseUV : TEXCOORD0;
    UNITY_VERTEX_INPUT_INSTANCE_ID
};

struct Varyings
{
    float4 positionCS : SV_POSITION;
    float3 positionWS : VAR_POSITION;
    float3 normalWS : VAR_NORMAL;
    float2 baseUV : VAR_BASE_UV;
    UNITY_VERTEX_INPUT_INSTANCE_ID
};

在顶点着色器中我们使用TransformObjectToWorldNormal来完成法线从模型空间到世界空间的变换:

    float3 positionWS = TransformObjectToWorld(input.positionOS);
    output.positionCS = TransformWorldToHClip(positionWS);
    output.normalWS = TransformObjectToWorldNormal(input.normalOS);

我们可以在片元着色器中用颜色显示法线:

    base.rgb = input.normalWS;
    return base;

注意,小于0的值会取为0。

法线插值

尽管法线在顶点着色器中是单位长度的,但经过沿三角形插值后其长度会变化,因此在片元着色器中要进行标准化:

    base.rgb = normalize(input.normalWS);

表面属性

shader中的光照是模拟光线遇到表面后的反应的,因此我们需要跟踪表面的属性。我们新建一个Surface.hlsl文件,置于ShaderLibrary文件夹下:

#ifndef CUSTOM_SURFACE_INCLUDED
#define CUSTOM_SURFACE_INCLUDED

struct Surface
{
    float3 normal;
    float3 color;
    float alpha;
};

#endif

LitPass中包含:

#include "../ShaderLibrary/Common.hlsl"
#include "../ShaderLibrary/Surface.hlsl"

在片元着色器中定义一个surface变量,并填充:

    Surface surface;
    surface.normal = normalize(input.normalWS);
    surface.color = base.rgb;
    surface.alpha = base.a;

    return float4(surface.color, surface.alpha);

计算光照

新建一个Lighting.hlsl文件,存放光照计算函数:

#ifndef CUSTOM_LIGHTING_INCLUDED
#define CUSTOM_LIGHTING_INCLUDED

float3 GetLighting(Surface surface)
{
    return surface.normal.y * surface.color;
}

#endif

LitPass中包含:

#include "../ShaderLibrary/Common.hlsl"
#include "../ShaderLibrary/Surface.hlsl"
#include "../ShaderLibrary/Lighting.hlsl"

在片元着色器中可以使用GetLighting获取光照:

    float3 color = GetLighting(surface);
    return float4(color, surface.alpha);

灯光

这里我们只模拟拥有方向的平行光。

灯光结构体

新建Light.hlsl文件,定义一个平行光结构体,同时定义一个GetDirectionalLight函数来返回一个配置好的平行光:

#ifndef CUSTOM_LIGHT_INCLUDED
#define CUSTOM_LIGHT_INCLUDED

struct Light
{
    float3 color;
    float3 direction;
    float attenuation;
};

Light GetDirectionalLight(int index, Surface surfaceWS, ShadowData shadowData)
{
    Light light;
    light.color = 1.0;
    light.direction = float3(0.0, 1.0, 0.0);
    return light;
}

#endif

LitPass中包含:

#include "../ShaderLibrary/Common.hlsl"
#include "../ShaderLibrary/Surface.hlsl"
#include "../ShaderLibrary/Light.hlsl"
#include "../ShaderLibrary/Lighting.hlsl"

光照函数

在文件中新增一个IncomingLight函数,用于计算光照程度,这里使用兰伯特模型:

float3 IncomingLight(Surface surface, Light light)
{
    return saturate(dot(surface.normal, light.direction)* light.color;
}

同时添加一个重载函数GetLighting,用于计算每个灯光的光照:

float3 GetLighting(Surface surface, Light light)
{
    return IncomingLight(surface, light) * surface.color;
}

修改另一个GetLighting,计算总的光照:

float3 GetLighting(Surface surface)
{
    return GetLighting(surface, light);
}

将灯光数据送往GPU

为了在shader中可以得到灯光数据,我们定义几个uniform变量,放于_CustomLight缓冲中:

CBUFFER_START(_CustomLight)
    float3 _DirectionalLightColors;
    float3 _DirectionalLightDirection;
CBUFFER_END

GetDirectionalLight中使用:

Light GetDirectionalLight()
{
    Light light;
    light.color = _DirectionalLightColors;
    light.direction = _DirectionalLightDirections;
    return light;
}

现在我们将数据送往GPU。创建一个新的类Lighting,类似于CameraRenderer

public class Lighting
{

    const string bufferName = "Lighting";

    CommandBuffer buffer = new CommandBuffer
    {
        name = bufferName
    };

    public void Setup(ScriptableRenderContext context)
    {
        buffer.BeginSample(bufferName);
        SetupDirectionalLight();
        buffer.EndSample(bufferName);
        context.ExecuteCommandBuffer(buffer);
        buffer.Clear();
    }

    void SetupDirectionalLight()
    {
    }
}

同时跟踪两个shader属性:

    static int
        dirLightColorsId = Shader.PropertyToID("_DirectionalLightColor"),
        dirLightDirectionsId = Shader.PropertyToID("_DirectionalLightDirection");

我们可以使用RenderSetting.sun来获取场景中的主光源:

    void SetupDirectionalLight(int index, ref VisibleLight visibleLight)
    {
        Light light = RenderSetting.sun;
        buffer.SetGlobalVector(dirLightColorId, light.color.linear * light.intensity);
        buffer.SetGlobalVector(dirLightDirectionalId, -light.transform.forward);
    }

CameraRenderer中设置光照:

    Lighting lighting = new Lighting();

    public void Render(ScriptableRenderContext context, Camera camera, bool useDynamicBatching, bool useGPUInstancing)
    {
        Setup();
        lighting.Setup(context);
        DrawVisibleGeometry(useDynamicBatching, useGPUInstancing);
        DrawUnsupportedShaders();
        DrawGizmos();
        lighting.Cleanup();
        Submit();
    }

可见光

在进行剔除时,Unity也会判断那些灯光会影响摄像机的可见范围。为此,Lighting需要获取剔除结果,在Setup中添加一个参数,同时使用SetupLights进行替换:

    public void Setup(ScriptableRenderContext context, CullingResults cullingResults)
    {
        this.cullingResults = cullingResults;
        buffer.BeginSample(bufferName);
        SetupLights();
        ...
    }

    void SetupLights()
    {
    }

CameraRenderer.Render中添加参数:

        lighting.Setup(context, cullingResults);

使用NativeArray设置可见光:

using Unity.Collections;

public class Lighting
{
    ...
    void SetupLights()
    {
        NativeArray<VisibleLight> visibleLights = cullingResults.visibleLights;
    }
    ...
}

多个平行光

使用可见光数据的话,可以支持多个平行光源,但我们需要将这些数据送往GPU。首先定义平行光源的最大数量,接着定义相应的颜色和方向数组,记得声明相应的材质属性ID:

    const int maxDirLightCount = 4;

    static int
        dirLightCountId = Shader.PropertyToID("_DirectionalLightCount"),
        dirLightColorsId = Shader.PropertyToID("_DirectionalLightColors"),
        dirLightDirectionsId = Shader.PropertyToID("_DirectionalLightDirections");

    static Vector4[]
        dirLightColors = new Vector4[maxDirLightCount],
        dirLightDirections = new Vector4[maxDirLightCount];

SetupDirectionalLight函数添加一个索引参数,可以根据索引获得相应光源的属性:

    void SetupDirectionalLight(int index, ref VisibleLight visibleLight)
    {
        dirLightColors[index] = visibleLight.finalColor;
        dirLightDirections[index] = -visibleLight.localToWorldMatrix.GetColumn(2);
    }

Unity默认不使用线性空间的灯光强度:

    public CustomRenderPipeline(bool useDynamicBatching, bool useGPUInstancing, bool useSRPBatcher)
    {
        this.useDynamicBatching = useDynamicBatching;
        this.useGPUInstancing = useGPUInstancing;
        GraphicsSettings.useScriptableRenderPipelineBatching = useSRPBatcher;
        GraphicsSettings.lightsUseLinearIntensity = true;
    }

接着我们遍历可见光,并设置灯光属性:

    void SetupLights()
    {
        NativeArray<VisibleLight> visibleLights = cullingResults.visibleLights;
        for (int i = 0; i < visibleLights.Length; i++)
        {
            VisibleLight visibleLight = visibleLights[i];
            SetupDirectionalLight(i, visibleLight);
        }

        buffer.SetGlobalInt(dirLightCountId, dirLightCount);
        buffer.SetGlobalVectorArray(dirLightColorsId, dirLightColors);
        buffer.SetGlobalVectorArray(dirLightDirectionsId, dirLightDirections);
    }

注意,我们目前只支持至多4个平行光:

    void SetupLights()
    {
        NativeArray<VisibleLight> visibleLights = cullingResults.visibleLights;
        int dirLightCount = 0;
        for (int i = 0; i < visibleLights.Length; i++)
        {
            VisibleLight visibleLight = visibleLights[i];
            if (visibleLight.lightType == LightType.Directional)
            {
                SetupDirectionalLight(dirLightCount++, ref visibleLight);
                if (dirLightCount >= maxDirLightCount)
                {
                    break;
                }
            }
        }

        buffer.SetGlobalInt(dirLightCountId, dirLightCount);
        buffer.SetGlobalVectorArray(dirLightColorsId, dirLightColors);
        buffer.SetGlobalVectorArray(dirLightDirectionsId, dirLightDirections);
    }

Shader循环

修改Light.hlsl中的_CustomLight缓冲:

#define MAX_DIRECTIONAL_LIGHT_COUNT 4

CBUFFER_START(_CustomLight)
    int _DirectionalLightCount;
    float4 _DirectionalLightColors[MAX_DIRECTIONAL_LIGHT_COUNT];
    float4 _DirectionalLightDirections[MAX_DIRECTIONAL_LIGHT_COUNT];
CBUFFER_END

同时添加一个获取灯光数量的方法:

int GetDirectionalLightCount()
{
    return _DirectionalLightCount;
}

Light GetDirectionalLight(int index)
{
    Light light;
    light.color = _DirectionalLightColors[index].rgb;
    light.direction = _DirectionalLightDirections[index].xyz;
    return light;
}

接着修改针对一个表面的GetLighting函数:

float3 GetLighting(Surface surface)
{
    float3 color = 0.0;
    for (int i = 0; i < GetDirectionalLightCount(); i++)
    {
        Light light = GetDirectionalLight(i);
        color += GetLighting(surface, light);
    }
    return color;
}

BRDF

BRDF不过多介绍

表面属性

这里使用金属流程,我们在Surface中添加金属度和光滑度的属性,在shader中也添加对应的属性:

        _Metallic ("Metallic", Range(0, 1)) = 0
        _Smoothness ("Smoothness", Range(0, 1)) = 0.5
struct Surface
{
    float3 normal;
    float3 color;
    float alpha;
    float metallic;
    float smoothness;
};
UNITY_INSTANCING_BUFFER_START(UnityPerMaterial)
    UNITY_DEFINE_INSTANCED_PROP(float4, _BaseMap_ST)
    UNITY_DEFINE_INSTANCED_PROP(float4, _BaseColor)
    UNITY_DEFINE_INSTANCED_PROP(float, _Cutoff)
    UNITY_DEFINE_INSTANCED_PROP(float, _Metallic)
    UNITY_DEFINE_INSTANCED_PROP(float, _Smoothness)
UNITY_INSTANCING_BUFFER_END(UnityPerMaterial)

在片元着色器中填充相应属性:

    surface.metallic = UNITY_ACCESS_INSTANCED_PROP(UnityPerMaterial, _Metallic);
    surface.smoothness = UNITY_ACCESS_INSTANCED_PROP(UnityPerMaterial, _Smoothness);

BRDF属性

BRDF是漫反射和高光反射的结合,除了将表面颜色分为这两块外,我们还需要知道表面的粗糙度。新建BRDF.hlsl,加入结构体:

#ifndef CUSTOM_BRDF_INCLUDED
#define CUSTOM_BRDF_INCLUDED

struct BRDF
{
    float3 diffuse;
    float3 specular;
    float roughness;
};

#endif

添加GetBRDF方法,返回某一表面的BRDF数据:

BRDF GetBRDF(inout Surface surface)
{
    BRDF brdf;
    brdf.diffuse = surface.color;
    brdf.specular = 0.0;
    brdf.roughness = 1.0;
    return brdf;
}

LitPass中包含相应文件:

#include "../ShaderLibrary/Common.hlsl"
#include "../ShaderLibrary/Surface.hlsl"
#include "../ShaderLibrary/Light.hlsl"
#include "../ShaderLibrary/BRDF.hlsl"
#include "../ShaderLibrary/Lighting.hlsl"

在所有的GetLighting函数中加入brdf参数,使用其diffuse属性替换:

float3 GetLighting(Surface surface, BRDF brdf, Light light)
{
    return IncomingLight(surface, light) * brdf.diffuse;
}

float3 GetLighting(Surface surface, BRDF brdf)
{
    float3 color = 0.0;
    for (int i = 0; i < GetDirectionalLightCount(); i++)
    {
        Light light = GetDirectionalLight(i, surface);
        color += GetLighting(surface, brdf, light);
    }
    return color;
}

在片元着色器末尾调用:

    BRDF brdf = GetBRDF(surface);
    float3 color = GetLighting(surface, brdf);

反射率

这里我们假设金属材质进行完全高光反射,反射率等价为表面的金属度,以此计算漫反射:

    float oneMinusReflectivity = surface.metallic;
    brdf.diffuse = surface.color * oneMinusReflectivity;

不过实际中,非金属也会有光的反射,即高光,它们的反射率的平均值大约为0.04。我们添加一个函数来将1-反射率的范围调整到0-0.96(即URP采用的方法):

#define MIN_REFLECTIVITY 0.04

float OneMinusReflectivity(float metallic)
{
    float range = 1.0 - MIN_REFLECTIVITY;
    return range - metallic * range;
}

使用该函数计算漫反射:

    float oneMinusReflectivity = OneMinusReflectivity(surface.metallic);
    brdf.diffuse = surface.color * oneMinusReflectivity;

高光

根据能量守恒公式,反射的出射光的能量不会大部入射光的能量,也就是说直接相减就能得到高光颜色:

    brdf.diffuse = surface.color * oneMinusReflectivity;
    brdf.specular = surface.color - brdf.diffuse;

不过这样就忽略了一点,金属材质会影响高光颜色,而非金属不会,非金属的高光应该是白色的,我们可以将表面颜色与最小的反射率根据表面的金属度插值得到:

    brdf.specular = lerp(MIN_REFLECTIVITY, surface.color, surface.metallic);

粗糙度

Core RP Library有一个方法PerceptualSmoothnessToPerceptualRoughness,可以根据表面的光滑程度获得粗糙度:

real PerceptualSmoothnessToPerceptualRoughness(real perceptualSmoothness)
{
    return (1.0 - perceptualSmoothness);
}

perceptual即感知到的。

真正的粗糙度可以使用PerceptualRoughnessToRoughness得到:

real RoughnessToPerceptualRoughness(real roughness)
{
    return sqrt(roughness);
}

这些方法在CommonMaterial.hlsl文件中:

#include "Packages/com.unity.render-pipelines.core/ShaderLibrary/Common.hlsl"
#include "Packages/com.unity.render-pipelines.core/ShaderLibrary/CommonMaterial.hlsl"
#include "UnityInput.hlsl"

使用这些函数获得粗糙度:

    float perceptualRoughness = PerceptualSmoothnessToPerceptualRoughness(surface.smoothness);
    brdf.roughness = PerceptualRoughnessToRoughness(perceptualRoughness);

观察方向

我们需要摄像机的位置,在UnityInput中定义:

float3 _WorldSpaceCameraPos;

同时相关光照计算我们需要顶点的世界坐标:

struct Varyings
{
    float4 positionCS : SV_POSITION;
    float3 positionWS : VAR_POSITION;
    ...
};

Varyings LitPassVertex(Attributes input)
{
    ...
    output.positionWS = TransformObjectToWorld(input.positionOS);
    output.positionCS = TransformWorldToHClip(output.positionWS);
    ...
}

我们可以将观察方向作为表面属性的一部分:

struct Surface
{
    float3 normal;
    float3 viewDirection;
    float3 color;
    float alpha;
    float metallic;
    float smoothness;
};

接着在片元着色器中计算:

    surface.viewDirection = normalize(_WorldSpaceCameraPos - input.positionWS);

高光强度

这里的高光强度计算使用简化CookTorrance BRDF的一种变体,也是URP使用的。因为需要平方计算,我们在Common.hlsl添加相应的方法:

float Square(float v)
{
    return v * v;
}

上述BRDF的计算公式为:\frac {r^2} {d^2max(0.1, (L \cdot H)^2)n},其中d = (N \cdot H)^2(r^2 - 1) + 1.0001 ,r是粗糙度,N是法线,L是入射光方向,H是中间矢量,n = 4r + 2

float SpecularStrength(Surface surface, BRDF brdf, Light light)
{
    float3 h = SafeNormalize(light.direction + surface.viewDirection);
    float nh2 = Square(saturate(dot(surface.normal, h)));
    float lh2 = Square(saturate(dot(light.direction, h)));
    float r2 = Square(brdf.roughness);
    float d2 = Square(nh2 * (r2 - 1) + 1.0001);
    float normalization = brdf.roughness * 4.0 + 2.0;
    return r2 / (d2 * max(0.1, lh2) * normalization);
}

接着添加一个计算最后颜色的函数:

float3 DirectBRDF(Surface surface, BRDF brdf, Light light)
{
    return SpecularStrength(surface, brdf, light) * brdf.specular + brdf.diffuse;
}

在针对单个光源的GetLighting中应用:

float3 GetLighting(Surface surface, BRDF brdf, Light light)
{
    return IncomingLight(surface, light) * DirectBRDF(surface, brdf, light);
}

透明

如果应用带有透明度的纹理进行透明度混合的话,会发现反射光会变暗:



这并不合理,漫发射的确会变暗,但高光反射就说不通了(比如玻璃)。

预乘Alpha

我们只想让漫发射衰减,而高光反射不变。为了能够不应用衰减,我们将混合模式的Source改为1。不过这样的话,漫反射也不会衰减了,因此我们在GetBRDF中提前乘上alpha值:

    brdf.diffuse = surface.color * oneMinusReflectivity;
    brdf.diffuse *= surface.alpha;

预乘选项

我们想同时支持预乘和一般情况,因此加上一个参数:

BRDF GetBRDF(inout Surface surface, bool applyAlphaToDiffuse = false)
{
    ...
    brdf.diffuse = surface.color * oneMinusReflectivity;
    if (applyAlphaToDiffuse)
    {
        brdf.diffuse *= surface.alpha;
    }
    ...
}

在片元着色器中,我们可以使用_PREMULTIPLY_ALPHA关键字来决定是否使用预乘:

#if defined(_PREMULTIPLY_ALPHA)
    BRDF brdf = GetBRDF(surface, true);
#else
    BRDF brdf = GetBRDF(surface);
#endif
    float3 color = GetLighting(surface, brdf);
    return float4(color, surface.alpha);

同时添加相应的着色器变体和属性配置:

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

推荐阅读更多精彩内容