從Built-in到URP

從Built-in到URP

HLSL語法

變量

  • bool – true or false.
  • float – 32位浮點數(shù)维哈。通常用于世界空間位置润文,紋理坐標或涉及復雜函數(shù)(例如三角函數(shù)或冪/冪)的標量計算薇缅。
  • half – 16位浮點數(shù)坊萝。通常用于短向量,方向嗡善,對象空間位置渡蜻,顏色术吝。
  • double – 64位浮點數(shù)。不能用作輸入/輸出
  • fixed – 僅在內(nèi)置著色器中使用茸苇,在URP中不支持顿苇,請改用half
  • real – 僅用于URP嗎税弃?我認為這只是half的默認值(假設平臺上支持它們),除非著色器指定“ #define PREFER_HALF 0”凑队,否則它將使用浮點精度则果。
  • int – 32位有符號整數(shù)
  • uint – 32位無符號整數(shù)(GLES2除外,不支持此整數(shù)漩氨,而是將其定義為int)西壮。

向量

  • float4 –包含4個浮點的向量
  • half3
  • int2
  • ……

矩陣

  • float4x4 – 4行,4列
  • int4x3 – 4行叫惊,3列
  • half2x1 – 2行款青,1列
  • float1x4 – 1行,4列
float3x3 matrix = {0,1,2,
                   3,4,5,
                   6,7,8};
float3 row0 = matrix[0]; // (0, 1, 2)
float3 row1 = matrix[1]; // (3, 4, 5)
float3 row2 = matrix[2]; // (6, 7, 8)
float row1column2 = matrix[1][2]; // 5
// 注意我們也可以這樣做
float row1column2 = matrix[1].z;

矩陣通常用于不同坐標空間之間的轉換霍狰。為此抡草,我們需要進行矩陣乘法饰及,可以使用mul函數(shù)來完成(而不是*運算符,該運算符不適用于矩陣和向量類型)

數(shù)組

可以在著色器中指定數(shù)組康震,盡管Shaderlab屬性或材質(zhì)檢查器不支持它們燎含,并且必須從C#腳本中進行設置。必須在著色器中指定數(shù)組的大小腿短,并且數(shù)組大小應保持恒定以防止出現(xiàn)問題屏箍。如果我們不知道數(shù)組的大小,則需要設置最大值并以0s傳入數(shù)組填充橘忱。我們可以指定另一個float來作為需要遍歷數(shù)組的長度赴魁,例如此處的示例。

float _Array[10]; // Float array
float4 _Array[10]; // Vector array
float4x4 _Array[10]; // Matrix array

設置浮點數(shù)組時钝诚,請使用material.SetFloatArrayShader.SetGlobalFloatArray颖御。還有SetVectorArraySetMatrixArray及其全局版本。

其他種類

HLSL還包括其他類型敲长,例如“紋理”和“采樣器”郎嫁,可以使用URP中的以下宏進行定義:

TEXTURE2D(textureName);
SAMPLER(sampler_textureName);

還有緩沖區(qū),盡管我從未真正使用過它們祈噪,所以對它們的用法并不熟悉泽铛。它們是使用material.SetBufferShader.SetGlobalBuffer從C#設置的。

#ifdef SHADER_API_D3D11
StructuredBuffer<float3> buffer;
#endif
// I think this is only supported in Direct3D 11?
// and also require #pragma target 4.5 or higher?
// see https://docs.unity3d.com/Manual/SL-ShaderCompileTargets.html

你可能還希望研究HLSL的其他部分辑鲤,例如流控制 (if盔腔,for,while等)月褥,但是如果我們熟悉語法弛随,則其語法基本上與C#相同。我們還可以在此處找到HLSL支持的所有運算符的列表宁赤。

函數(shù)

HLSL中的函數(shù)聲明與C#非常相似舀透。這是一個例子:

float3 example(float3 a, float3 b){
    return a * b;
}

其中float3是返回類型,示例是函數(shù)名稱决左,括號內(nèi)是傳遞給函數(shù)的參數(shù)愕够。在沒有返回類型的情況下,將使用void佛猛。您還可以在參數(shù)類型之前使用“ out”來指定輸出參數(shù)惑芭,如果希望它成為可編輯并回傳的輸入,則可以使用“ inout”來指定輸出參數(shù)继找。

宏在編譯著色器之前進行處理遂跟,并且在使用宏時將替換為帶有替換參數(shù)的定義。例如

#define EXAMPLE(x, y) ((x) * (y))
float f = EXAMPLE(3, 5);
float3 a = float3(1,1,1);
float3 f2 = EXAMPLE(a, float3(0,1,0));
 
// becomes :
float f = ((3) * (5));
float a = float(1,1,1);
float3 f2 = ((a) * (float3(0,1,0)));
// then the shader is compiled.
 
// Note that the macro has () around x and y.
// This is because we could do :
float b = EXAMPLE(1+2, 3+4);
// becomes :
float b = ((1+2) * (3+4)); // 3 * 7, so 21
// If those () wasn't included, it would instead be :
float b = (1+2*3+4)
// which equals 11 due to * taking precedence over +

他們還可以做一些功能無法做到的事情。例如 :

#define TRANSFORM_TEX(tex,name) (tex.xy * name##_ST.xy + name##_ST.zw)
 
// Usage :
OUT.uv = TRANSFORM_TEX(IN.uv, _MainTex)
 
// becomes :
OUT.uv = (IN.uv.xy * _MainTex_ST.xy + _MainTex_ST.zw);

“##”運算符是一種特殊情況幻锁,其中宏可能很有用凯亮。它使我們可以將名稱和_ST部分連接起來,從而為此用法輸入生成_MainTex_ST越败。如果省略##部分触幼,它將僅生成“name_ST”,從而導致錯誤究飞,因為尚未定義置谦。(當然,仍然需要定義_MainTex_ST亿傅,但這是預期的行為媒峡,因為在紋理名稱后附加_ST是Unity處理紋理的平鋪和偏移值的方式)。

Tags

URP LIGHTMODE TAGS :

  • UniversalForward – 用于前向渲染
  • ShadowCaster – 用于投射陰影
  • DepthOnly – 似乎在為場景視圖渲染深度紋理時使用葵擎,而不是在運行中使用嗎谅阿?不過,某些渲染器功能可能會使用它酬滤。
  • Meta – 僅在光照貼圖烘焙期間使用
  • Universal2D – 在啟用 2D 渲染器時使用签餐,而不是前向渲染器。
  • UniversalGBuffer – 與延遲渲染有關盯串。我認為這是測試功能氯檐。
Tags { "LightMode" = "UniversalForward" }

可以在子著色器中定義多個Pass塊,但是每個都應該用一個特定的LightMode標記(見下面)体捏。URP使用了單通道前向渲染器冠摄,所以只有第一個“通用前向”通道(GPU支持的)將用于渲染對象——你不能同時渲染多個對象。雖然我們可以讓其他傳遞沒有標記几缭,但要注意它們將中斷SRP批處理程序的批處理河泳。相反,我們建議使用單獨的著色器/材質(zhì)年栓,無論是在單獨的MeshRenderers上拆挥,還是使用Forward Renderer上的Render Objects特性,用一個overrideMaterial在一個特定的圖層上重新渲染對象某抓。

屬性

在Shaderlab示例中竿刁,我們有一個HLSLINCLUDE,它會自動將代碼包含在Subshader內(nèi)部的每個Pass中搪缨。

我們可以使用UnityPerMaterial CBUFFER來確保著色器兼容SRP批處理。這個CBUFFER需要包括所有公開的屬性(與Shaderlab屬性塊中的相同)鸵熟。但它不能包括其他未公開的變量副编,紋理也不需要被包括。

HLSLINCLUDE
    #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
 
    CBUFFER_START(UnityPerMaterial)
    float4 _BaseMap_ST;
    float4 _BaseMap_TexelSize;
    float4 _BaseColor;
    //float4 _ExampleDir;
    //float _ExampleFloat;
    CBUFFER_END
ENDHLSL

需要注意的是_BaseMap_ST_BaseMap_TexelSize是兩個東西流强,前者是紋理的縮放與偏移痹届,而后者代表紋理的大小呻待。

結構體

在定義頂點或片段著色器功能之前,我們需要定義一些用于將數(shù)據(jù)傳入和傳出的結構队腐。在內(nèi)置函數(shù)中蚕捉,它們通常被命名為“appdata”和“v2f”(頂點到片段的縮寫),而URP著色器則傾向于使用“ Attributes”和“ Varyings ”柴淘。這些只是名稱迫淹,可能不太重要。

struct Attributes {
    float4 positionOS   : POSITION;
    float2 uv           : TEXCOORD0;
    float4 color        : COLOR;
};

該屬性結構將輸入到頂點著色器为严。它允許我們使用大寫字母中被稱為語義的部分從網(wǎng)格中獲取每個頂點的數(shù)據(jù)敛熬。其中包括:頂點位置(POSITION),頂點顏色(COLOR)和UV(又稱為紋理坐標)第股。網(wǎng)格具有8個不同的UV通道应民,可以通過TEXCOORD0TEXCOORD7進行訪問。

我們還可以通過NORMAL訪問頂點法線夕吻,并通過TANGENT訪問切線诲锹。

在這些結構之后,您通常還會看到已定義了紋理采樣器(雖然紋理位于著色器屬性中涉馅,但尚未在hlsl中定義归园。其他屬性包括在CBUFFER中)。在URP中控漠,我們使用以下內(nèi)容:

TEXTURE2D(_BaseMap);
SAMPLER(sampler_BaseMap);

頂點著色器

我們的頂點著色器需要做的主要事情是將網(wǎng)格從對象空間位置轉換為剪輯空間位置蔓倍。為了在目標屏幕位置正確渲染片元/像素。

在內(nèi)置著色器中盐捷,您可以使用UnityObjectToClipPos函數(shù)執(zhí)行此操作偶翅,但是URP已將其重命名為TransformObjectToHClip(可以在函數(shù)庫SpaceTransforms.hlsl中找到)。也就是說碉渡,還有另一種方法來處理URP中的轉換聚谁,如下所示。

Varyings vert(Attributes IN) {
    Varyings OUT;
 
    VertexPositionInputs positionInputs = GetVertexPositionInputs(IN.positionOS.xyz);
    OUT.positionCS = positionInputs.positionCS;
    // Or this :
    //OUT.positionCS = TransformObjectToHClip(IN.positionOS.xyz);
 
    OUT.uv = TRANSFORM_TEX(IN.uv, _BaseMap);
    OUT.color = IN.color;
    return OUT;
}
  1. 我們從Attributes中輸入對象空間的位置滞诺,并獲得一個VertexPositionInputs結構形导,其中包含:

    • positionWS,在世界空間中的位置
    • positionVS习霹,視圖空間中的位置
    • positionCS朵耕,裁剪空間中的位置
    • positionNDC,標準化設備坐標中的位置
  2. 頂點著色器還負責將數(shù)據(jù)傳遞到片段淋叶。對于頂點顏色阎曹,這只是一個簡單的OUT.color = IN.color;

  3. 如果我們希望能夠?qū)y理進行采樣,則還需要傳遞模型的UV(紋理坐標)处嫌。雖然我們可以做OUT.uv = IN.uv;(假設兩者均為float2)栅贴,通常會使用TRANSFORM_TEX宏,該宏采用uv和texture屬性名稱熏迹,并應用材質(zhì)檢查器的偏移和平鋪進行矯正(存儲在“ _BaseMap” +“ _ ST”中檐薯,S用于比例尺和T))。此宏位于內(nèi)置和URP中(在core / ShaderLibrary / Macros.hlsl內(nèi)部注暗,應自動包含在Core.hlsl中)坛缕。

    實際上,這只是IN.uv.xy * _BaseMap_ST.xy + _BaseMap_ST.z的簡寫友存,因此您也可以這樣寫(將_BaseMap換成預期的紋理屬性祷膳。(texture)_STfloat4變量還必須添加到UnityPerMaterial CBUFFER(已在屬性部分中討論過)。

VertexNormalInputs normalInputs = GetVertexNormalInputs(IN.normalOS, IN.tangentOS);

GetVertexNormalInputs可用于將對象空間的法線和切線轉換為世界空間屡立。它包含:

  • normalWS直晨,在世界空間中的法線向量
  • tangentWS,在世界空間中的切線向量
  • bitangentWS膨俐,在世界空間中的副切線向量

還有一個僅將法線作為輸入的版本勇皇,將tangentWS保留為(1,0,0),bitangentWS保留為(0,1,0)焚刺,或者您也可以改用TransformObjectToWorldNormal(IN.normalOS)敛摘。

片元著色器

half4 frag(Varyings IN) : SV_Target {
    half4 baseMap = SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, IN.uv);
 
    return baseMap * _BaseColor * IN.color;
}

這將生成一個著色器,該著色器基于_BaseMap紋理輸出一個Half4顏色乳愉,該著色器還由_BaseColor和頂點顏色(IN.color)進行著色兄淫。

SV_Target部分是與half4輸出一起使用的語義,它告訴著色器它是顏色輸出蔓姚。

還有一個SV_Depth輸出捕虽,它是一個浮點數(shù),用于覆蓋每個像素的Z緩沖區(qū)值坡脐。(可以將它們放入一個結構中以同時輸出SV_TargetSV_Depth)泄私。在大多數(shù)情況下,不需要覆蓋它备闲,對于許多GPU晌端,它都會關閉某些基于深度緩沖區(qū)的優(yōu)化,因此除非您知道自己在做什么和需要做什么恬砂,否則不要覆蓋它咧纠。

我們的片段著色器使用URP ShaderLibrary提供的SAMPLE_TEXTURE2D宏對_BaseMap紋理進行采樣,該宏將紋理泻骤,采樣器和UV作為輸入惧盹。

我們可能還想做的是乳幸,如果像素的alpha值低于某個閾值,則將其丟棄钧椰,以使整個網(wǎng)格都不可見。

例如符欠,對于四邊形上的草/葉紋理嫡霞。既可以在不透明著色器中也可以在透明著色器中完成此操作,通常將其稱為Alpha裁剪希柿。如果您熟悉shadergraph诊沪,可以使用主節(jié)點上的“Alpha Clip Threshold”輸入來處理它。

解決此問題的常用方法是提供_Cutoff屬性以控制閾值曾撤,然后執(zhí)行以下操作端姚。(此屬性必須添加到我們的Shaderlab屬性以及UnityPerMaterial CBUFFER中以實現(xiàn)SRP Batcher兼容性)。

if (_BaseMap.a < _Cutoff){
    discard;
}
// OR
clip(_BaseMap.a - _Cutoff);
// inside the fragment function, before returning

環(huán)境光在URP下用_GlossyEnvironmentColor獲取挤悉,但得到的效果可能與Builit-in下的結果相差較大渐裸,這時候可以考慮用球諧函數(shù)獲取

//URP使用的環(huán)境光
half3 ambient = _GlossyEnvironmentColor
//使用球諧函數(shù)獲取
half3 ambient = SampleSH(worldNormal);

//--Builit-in
half3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;

關鍵字和著色器變體

著色器變體

在著色器中,我們可以指定更多的#pragma指令装悲,其中一些指令包括multi_compileshader_feature昏鹃。這些可用于指定用于將“著色器”代碼的某些部分“打開”或“關閉”的關鍵字。著色器實際上被編譯為多個版本的著色器诀诊,稱為著色器變體洞渤。

MULTI_COMPILE

#pragma multi_compile _A _B _C (...etc)

在此示例中,我們將生成著色器的三個變體属瓣,其中_A载迄,_B和_C是關鍵字。

在著色器代碼中抡蛙,我們可以使用以下內(nèi)容:

#ifdef _A
// 如果A啟用护昧,編譯此代碼
#endif
 
#ifndef _B
// 當B被禁用時編譯此代碼,也就是只在A和C中溜畅。
// 注意#ifndef中額外的“n”表示“如果沒有定義”
#else
// 如果B啟用捏卓,編譯此代碼
#endif
 
#if defined(_A) || defined(_C)
// 用A或c (aka與上面的相同,假設沒有其他關鍵字)編譯此代碼
// 如果需要多個條件慈格,則必須使用長形式的"#if defined()"
// 其中|| = or怠晴, && = and
// 注意,因為關鍵字是在一個multi_compile語句中定義的
// 實際上不可能同時啟用兩者浴捆,所以&&在這里沒有意義蒜田。
#endif
 
// 還有#elif,用于else if語句选泻。

SHADER_FEATURE

#pragma shader_feature _A _B

這與multi_compile完全相同冲粤,但是未使用的變體將不包括在最終版本中美莫。因此,在運行時啟用/禁用這些關鍵字是不好的梯捕,因為它所需的著色器可能未包含在構建中厢呵!如果需要在運行時處理關鍵字,請改用multi_compile傀顾。

這些指令還有“頂點”和“片元”版本襟铭,可用于僅針對頂點或片段程序編譯著色器變體,從而減少了變體的總數(shù)短曾。例如 :

#pragma multi_compile_vertex _ _A
#pragma multi_compile_fragment _ _B
// also shader_feature_vertex and shader_feature_fragment

在此示例中寒砖,_A關鍵字僅用于頂點程序,_B僅用于片元嫉拐。不能同時啟用_A和_B的變體哩都。Unity告訴我們,這會產(chǎn)生2個著色器變體婉徘,盡管當您查看實際的編譯代碼時漠嵌,它更像是一個禁用兩個著色器的著色器變體和兩個“half”的變體。

著色器變體的增長

每增加一個multi_compile和shader_feature判哥,它就會為啟用/禁用關鍵字的每種可能組合生成越來越多的著色器變體献雅。以以下為例:

#pragma multi_compile _A _B _C
#pragma multi_compile _D _E
#pragma shader_feature _F _G

在這里,第一行將生成3個著色器變體塌计。但是第二行需要為已啟用_D或_E的那些變體生成2個著色器變體挺身。

因此,A&D锌仅,A&E章钾,B&D,B&E热芹,C&D和C&E〖現(xiàn)在有6個變體。

第三行伊脓,是這6個中的每一個的另外2個變體府寒,因此我們現(xiàn)在總共有12個著色器變體。由于該行是shader_feature报腔,因此某些變體可能不會包含在構建中株搔。

每個添加了2個關鍵字的multi_compile都會使產(chǎn)生的變體數(shù)量加倍,因此包含10個變體的著色器將產(chǎn)生1024個著色器變體纯蛾!它需要編譯最終構建中需要包含的每個著色器變體纤房,因此將增加構建時間以及構建大小。

如何查看著色器的變體個數(shù)

如果要查看一個著色器產(chǎn)生多少個著色器變體翻诉,請單擊該著色器炮姨,然后在檢查器中有一個“Compile and Show Code”按鈕捌刮,旁邊是一個小的下拉箭頭,其中列出了所包含的變體數(shù)舒岸。如果單擊“skip unused shader_features”绅作,則可以切換以查看變體的總數(shù)。

關鍵字

每個項目最多還有256個關鍵字蛾派,因此最好遵循其他著色器的命名約定棚蓄。

您還會注意到,對于許多multi_compileshader_features而言碍脏,第一個關鍵字通常僅保留為“ _”。實際上稍算,這實際上不會產(chǎn)生關鍵字典尾,因此會為256個最大值的其他關鍵字留出更多空間。

#pragma multi_compile _ _KEYWORD
 
#pragma shader_feature _KEYWORD
// 僅是shader_features的簡寫
#pragma shader_feature _ _KEYWORD
 
// 如果您需要知道該關鍵字是否已禁用
// 然后我們可以這樣做:
#ifndef _KEYWORD
// 或#if糊探!defined(_KEYWORD)
// 或#ifdef _KEYWORD #else
// code
#endif

我們還可以通過使用multi_compile和shader_feature的本地版本來避免耗盡最大的關鍵字數(shù)钾埂。這些生成的關鍵字對于該著色器來說是本地的,但是每個著色器最多也有64個本地關鍵字科平。

#pragma multi_compile_local _ _KEYWORD
#pragma shader_feature_local _KEYWORD
 
// 還有l(wèi)ocal_fragment/vertex !
#pragma multi_compile_local_fragment _ _KEYWORD
#pragma shader_feature_local_vertex _KEYWORD

光照

Universal RP不支持表面著色器褥紫,但是ShaderLibrary確實提供了幫助我們處理大量光照計算的功能。這些包含在Lighting.hlsl

Lighting.hlsl中瞪慧,有一個GetMainLight函數(shù)髓考,如果您熟悉著色器圖中的自定義照明,您可能已經(jīng)知道弃酌。為了使用此功能氨菇,我們首先在HLSLPROGRAM的頂部引用Lighting.hlsl文件,我還將添加一些multi_compile指令妓湘,這些指令提供了接收陰影所需的關鍵字查蓉。

#pragma multi_compile _ _MAIN_LIGHT_SHADOWS
#pragma multi_compile _ _MAIN_LIGHT_SHADOWS_CASCADE
#pragma multi_compile _ _SHADOWS_SOFT
 
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl"

接下來,我們將需要頂點法線來處理陰影/光照榜贴,因此我們將它們添加到AttributesVaryings結構中豌研,并更新頂點著色器。在這里唬党,我僅顯示基于上一節(jié)中制作的Unlit著色器添加的代碼鹃共。

struct Attributes {
    ...
    float4 normalOS     : NORMAL;
};
 
struct Varyings {
    ...
    float3 normalWS     : NORMAL;
    float3 positionWS   : TEXCOORD2;
};
...
Varyings vert(Attributes IN) {
    Varyings OUT;
    VertexPositionInputs positionInputs = GetVertexPositionInputs(IN.positionOS.xyz);
    ...
    OUT.positionWS = positionInputs.positionWS;
 
    VertexNormalInputs normalInputs = GetVertexNormalInputs(IN.normalOS.xyz);
    OUT.normalWS = normalInputs.normalWS;
 
    return OUT;
}

在片元著色器中,我們現(xiàn)在可以采用世界空間法線初嘹,并使用世界空間位置來計算陰影坐標及汉。

half4 frag(Varyings IN) : SV_Target {
    half4 baseMap = SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, IN.uv);
    half4 color = baseMap * _BaseColor * IN.color;
 
    float4 shadowCoord = TransformWorldToShadowCoord(IN.positionWS.xyz);
    Light light = GetMainLight(shadowCoord);
 
    half3 diffuse = LightingLambert(light.color, light.direction, IN.normalWS);
 
    return half4(color.rgb * diffuse * light.shadowAttenuation, color.a);
}

雖然我們的著色器將從其他著色器接收陰影,但是請注意屯烦,它沒有ShadowCaster傳遞坷随,因此不會將陰影投射到自身或其他對象上房铭。請參見ShadowCaster部分。

如果我們需要陰影温眉,但對象上沒有漫反射陰影缸匪,則也可以刪除漫反射陰影計算,而只需使用light.shadowAttenuation类溢。

如果要進一步擴展以包括環(huán)境/烘焙GI和其他光源凌蔬,請以Lighting.hlsl中的UniversalFragmentBlinnPhong方法為例,或者讓它為您處理照明闯冷。它使用InputData結構砂心,下一部分討論的PBR示例也將使用該結構。

PBR光照

基于物理的渲染(PBR)是Unity的“Standard”著色器使用的著色/照明模型蛇耀,以及UPR的“ Lit”著色器和ShaderGraph中的PBR主節(jié)點辩诞。

如前一節(jié)所述,內(nèi)置管道中的陰影/照明通常由Surface Shaders處理纺涤,其中“Standard”選用是PBR模型译暂。它們使用了一個曲面函數(shù),該函數(shù)輸出了反照率撩炊,法線外永,發(fā)射,平滑度拧咳,遮擋伯顶,Alpha和Metallic(如果使用“ StandardSpecular”工作流程,則為Specular)呛踊。Unity將采用這些并在幕后生成一個頂點和片段著色器甲捏,為您處理某些計算鸽扁,例如PBR陰影/照明和陰影苟呐。

Universal RP不支持表面著色器低斋,但是ShaderLibrary確實提供了幫助我們處理大量光照計算的功能。這些包含在Lighting.hlsl中愉择。在本節(jié)中劫乱,我們將重點介紹UniversalFragmentPBR

half4 UniversalFragmentPBR(InputData inputData, half3 albedo, half metallic, half3 specular, half smoothness, half occlusion,  half3 emission, half alpha)
 
// 在v10.xx中添加了帶有SurfaceData結構的版本
// 對于之前的版本,需要改用以上版本锥涕。
//(但是您仍然可以使用SurfaceData結構來組織/保存數(shù)據(jù))
half4 UniversalFragmentPBR(InputData inputData, SurfaceData surfaceData)
 
// 還有:
half4 UniversalFragmentBlinnPhong(InputData inputData, half3 diffuse, half4 specularGloss, half smoothness, half3 emission, half alpha)
//復制Unity v4之前的“舊”表面著色器衷戈,
//并由URP的“ SimpleLit”著色器使用
//使用Lambert(漫反射)和BlinnPhong(鏡面反射)照明模型

首先,我們應該添加PBR照明模型使用的一些屬性层坠。我省去了金屬/高光貼圖和遮擋貼圖殖妇,主要是因為它們沒有很好的功能來為您處理采樣(除非您從LitInput.hlsl中復制它們,這是URP提供的Lit shader的一部分) 破花,而不是實際的ShaderLibrary)谦趣,并且此部分已經(jīng)相當長且足夠復雜疲吸。實際上我?guī)缀鯚o法解釋,因為它主要是知道在哪里使用哪個函數(shù)前鹅。您以后總是可以使用LitInput作為示例來添加它們摘悴。

Properties {
    _BaseMap ("Base Texture", 2D) = "white" {}
    _BaseColor ("Example Colour", Color) = (0, 0.66, 0.73, 1)
    _Smoothness ("Smoothness", Float) = 0.5
 
    [Toggle(_ALPHATEST_ON)] _EnableAlphaTest("Enable Alpha Cutoff", Float) = 0.0
    _Cutoff ("Alpha Cutoff", Float) = 0.5
 
    [Toggle(_NORMALMAP)] _EnableBumpMap("Enable Normal/Bump Map", Float) = 0.0
    _BumpMap ("Normal/Bump Texture", 2D) = "bump" {}
    _BumpScale ("Bump Scale", Float) = 1
 
    [Toggle(_EMISSION)] _EnableEmission("Enable Emission", Float) = 0.0
    _EmissionMap ("Emission Texture", 2D) = "white" {}
    _EmissionColor ("Emission Colour", Color) = (0, 0, 0, 0)
    }
...
// And need to adjust the CBUFFER to include these too
CBUFFER_START(UnityPerMaterial)
    float4 _BaseMap_ST; // Texture tiling & offset inspector values
    float4 _BaseColor;
    float _BumpScale;
    float4 _EmissionColor;
    float _Smoothness;
    float _Cutoff;
CBUFFER_END

我們還需要對Unlit著色器代碼進行大量更改,包括添加一些multi_compileshader_features以及對AttributesVaryings結構進行調(diào)整舰绘,因為我們需要來自網(wǎng)格的法線和切線數(shù)據(jù)并將其發(fā)送到片元中以便使用它們用于照明計算蹂喻。

“屬性”塊中的這些TOGGLE特性使我們能夠從材質(zhì)檢查器啟用/禁用shader_feature關鍵字。(或者捂寿,我們可以為著色器編寫自定義編輯器/檢查器GUI或使用調(diào)試檢查器)口四。

如果要支持烘焙的光照貼圖,我們還需要在TEXCOORD1通道中傳遞的光照貼圖UV秦陋。

我還使用了來自ShaderLibrary的SurfaceInput.hlsl來幫助完成某些事情窃祝,它可以幫助SurfaceData結構保存PBR所需的數(shù)據(jù)以及一些用于采樣的反射率,法線和發(fā)射貼圖的函數(shù)(請注意踱侣,該結構似乎已經(jīng)移動了到URP v10中的SurfaceData.hlsl,但SurfaceInput.hlsl會自動包含它)

// Material Keywords
#pragma shader_feature _NORMALMAP
#pragma shader_feature _ALPHATEST_ON
#pragma shader_feature _ALPHAPREMULTIPLY_ON
#pragma shader_feature _EMISSION
//#pragma shader_feature _METALLICSPECGLOSSMAP
//#pragma shader_feature _SMOOTHNESS_TEXTURE_ALBEDO_CHANNEL_A
//#pragma shader_feature _OCCLUSIONMAP
 
//#pragma shader_feature _SPECULARHIGHLIGHTS_OFF
//#pragma shader_feature _ENVIRONMENTREFLECTIONS_OFF
//#pragma shader_feature _SPECULAR_SETUP
#pragma shader_feature _RECEIVE_SHADOWS_OFF
 
// URP Keywords
#pragma multi_compile _ _MAIN_LIGHT_SHADOWS
#pragma multi_compile _ _MAIN_LIGHT_SHADOWS_CASCADE
#pragma multi_compile _ _ADDITIONAL_LIGHTS_VERTEX _ADDITIONAL_LIGHTS
#pragma multi_compile _ _ADDITIONAL_LIGHT_SHADOWS
#pragma multi_compile _ _SHADOWS_SOFT
#pragma multi_compile _ _MIXED_LIGHTING_SUBTRACTIVE
 
// Unity defined keywords
#pragma multi_compile _ DIRLIGHTMAP_COMBINED
#pragma multi_compile _ LIGHTMAP_ON
#pragma multi_compile_fog
 
// Some added includes, required to use the Lighting functions
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl"
// And this one for the SurfaceData struct and albedo/normal/emission sampling functions.
// Note : It also defines the _BaseMap, _BumpMap and _EmissionMap textures for us, so we should use these as Shaderlab Properties too.
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/SurfaceInput.hlsl"
 
struct Attributes {
    float4 positionOS   : POSITION;
    float3 normalOS     : NORMAL;
    float4 tangentOS    : TANGENT;
    float4 color        : COLOR;
    float2 uv           : TEXCOORD0;
    float2 lightmapUV   : TEXCOORD1;
};
 
struct Varyings {
    float4 positionCS               : SV_POSITION;
    float4 color                    : COLOR;
    float2 uv                       : TEXCOORD0;
    DECLARE_LIGHTMAP_OR_SH(lightmapUV, vertexSH, 1);
    // Note this macro is using TEXCOORD1
#ifdef REQUIRES_WORLD_SPACE_POS_INTERPOLATOR
    float3 positionWS               : TEXCOORD2;
#endif
    float3 normalWS                 : TEXCOORD3;
#ifdef _NORMALMAP
    float4 tangentWS                : TEXCOORD4;
#endif
    float3 viewDirWS                : TEXCOORD5;
    half4 fogFactorAndVertexLight   : TEXCOORD6;
    // x: fogFactor, yzw: vertex light
#ifdef REQUIRES_VERTEX_SHADOW_COORD_INTERPOLATOR
    float4 shadowCoord              : TEXCOORD7;
#endif
};
 
//TEXTURE2D(_BaseMap);
//SAMPLER(sampler_BaseMap);
// Removed, since SurfaceInput.hlsl now defines the _BaseMap for us

我們的“變量”現(xiàn)在還包含正在使用的光照貼圖UV大磺,法線和切線抡句,但是我們還添加了“視圖方向”,這對于照明計算杠愧,霧待榔,頂點照明支持和接收陰影的陰影坐標是必不可少的。

現(xiàn)在我們需要更新頂點著色器以處理所有這些更改流济,這主要是僅知道要使用的功能:

#if SHADER_LIBRARY_VERSION_MAJOR < 9
    // This function was added in URP v9.x.x versions
    // If we want to support URP versions before, we need to handle it instead.
    // Computes the world space view direction (pointing towards the viewer).
    float3 GetWorldSpaceViewDir(float3 positionWS) {
        if (unity_OrthoParams.w == 0) {
            // Perspective
            return _WorldSpaceCameraPos - positionWS;
        } else {
            // Orthographic
            float4x4 viewMat = GetWorldToViewMatrix();
            return viewMat[2].xyz;
        }
    }
#endif
 
Varyings vert(Attributes IN) {
    Varyings OUT;
 
    // Vertex Position
    VertexPositionInputs positionInputs = GetVertexPositionInputs(IN.positionOS.xyz);
    OUT.positionCS = positionInputs.positionCS;
#ifdef REQUIRES_WORLD_SPACE_POS_INTERPOLATOR
    OUT.positionWS = positionInputs.positionWS;
#endif
    // UVs & Vertex Colour
    OUT.uv = TRANSFORM_TEX(IN.uv, _BaseMap);
    OUT.color = IN.color;
 
    // View Direction
    OUT.viewDirWS = GetWorldSpaceViewDir(positionInputs.positionWS);
 
    // Normals & Tangents
    VertexNormalInputs normalInputs = GetVertexNormalInputs(IN.normalOS, IN.tangentOS);
    OUT.normalWS =  normalInputs.normalWS;
#ifdef _NORMALMAP
    real sign = IN.tangentOS.w * GetOddNegativeScale();
    OUT.tangentWS = half4(normalInputs.tangentWS.xyz, sign);
#endif
 
    // Vertex Lighting & Fog
    half3 vertexLight = VertexLighting(positionInputs.positionWS, normalInputs.normalWS);
    half fogFactor = ComputeFogFactor(positionInputs.positionCS.z);
    OUT.fogFactorAndVertexLight = half4(fogFactor, vertexLight);
 
    // Baked Lighting & SH (used for Ambient if there is no baked)
    OUTPUT_LIGHTMAP_UV(IN.lightmapUV, unity_LightmapST, OUT.lightmapUV);
    OUTPUT_SH(OUT.normalWS.xyz, OUT.vertexSH);
 
    // Shadow Coord
#ifdef REQUIRES_VERTEX_SHADOW_COORD_INTERPOLATOR
    OUT.shadowCoord = GetShadowCoord(positionInputs);
#endif
    return OUT;
}

現(xiàn)在锐锣,我們還可以更新該片元著色器以實際使用UniversalFragmentPBR函數(shù)。由于它需要InputData結構輸入绳瘟,因此我們需要創(chuàng)建和設置它雕憔。代替在片元著色器中執(zhí)行此操作,我們將創(chuàng)建另一個函數(shù)來幫助組織事物糖声。

類似地斤彼,要處理所有反照率,金屬蘸泻,鏡面琉苇,平滑度,遮擋悦施,發(fā)射和Alpha輸入并扇,我們將使用SurfaceData結構(由我們之前包含的SurfaceInput.hlsl提供),并創(chuàng)建另一個函數(shù)來處理它抡诞。

InputData InitializeInputData(Varyings IN, half3 normalTS){
    InputData inputData = (InputData)0;
 
#if defined(REQUIRES_WORLD_SPACE_POS_INTERPOLATOR)
    inputData.positionWS = IN.positionWS;
#endif
                 
    half3 viewDirWS = SafeNormalize(IN.viewDirWS);
#ifdef _NORMALMAP
    float sgn = IN.tangentWS.w; // should be either +1 or -1
    float3 bitangent = sgn * cross(IN.normalWS.xyz, IN.tangentWS.xyz);
    inputData.normalWS = TransformTangentToWorld(normalTS, half3x3(IN.tangentWS.xyz, bitangent.xyz, IN.normalWS.xyz));
#else
    inputData.normalWS = IN.normalWS;
#endif
 
    inputData.normalWS = NormalizeNormalPerPixel(inputData.normalWS);
    inputData.viewDirectionWS = viewDirWS;
 
#if defined(REQUIRES_VERTEX_SHADOW_COORD_INTERPOLATOR)
    inputData.shadowCoord = IN.shadowCoord;
#elif defined(MAIN_LIGHT_CALCULATE_SHADOWS)
    inputData.shadowCoord = TransformWorldToShadowCoord(inputData.positionWS);
#else
    inputData.shadowCoord = float4(0, 0, 0, 0);
#endif
 
    inputData.fogCoord = IN.fogFactorAndVertexLight.x;
    inputData.vertexLighting = IN.fogFactorAndVertexLight.yzw;
    inputData.bakedGI = SAMPLE_GI(IN.lightmapUV, IN.vertexSH, inputData.normalWS);
    return inputData;
}
 
SurfaceData InitializeSurfaceData(Varyings IN){
    SurfaceData surfaceData = (SurfaceData)0;
    // Note, we can just use SurfaceData surfaceData; here and not set it.
    // However we then need to ensure all values in the struct are set before returning.
    // By casting 0 to SurfaceData, we automatically set all the contents to 0.
         
    half4 albedoAlpha = SampleAlbedoAlpha(IN.uv, TEXTURE2D_ARGS(_BaseMap, sampler_BaseMap));
    surfaceData.alpha = Alpha(albedoAlpha.a, _BaseColor, _Cutoff);
    surfaceData.albedo = albedoAlpha.rgb * _BaseColor.rgb * IN.color.rgb;
 
    // Not supporting the metallic/specular map or occlusion map
    // for an example of that see : https://github.com/Unity-Technologies/Graphics/blob/master/com.unity.render-pipelines.universal/Shaders/LitInput.hlsl
 
    surfaceData.smoothness = _Smoothness;
    surfaceData.normalTS = SampleNormal(IN.uv, TEXTURE2D_ARGS(_BumpMap, sampler_BumpMap), _BumpScale);
    surfaceData.emission = SampleEmission(IN.uv, _EmissionColor.rgb, TEXTURE2D_ARGS(_EmissionMap, sampler_EmissionMap));
    surfaceData.occlusion = 1;
    return surfaceData;
}
 
half4 frag(Varyings IN) : SV_Target {
    SurfaceData surfaceData = InitializeSurfaceData(IN);
    InputData inputData = InitializeInputData(IN, surfaceData.normalTS);
                 
    // In URP v10+ versions we could use this :
    // half4 color = UniversalFragmentPBR(inputData, surfaceData);
 
    // But for other versions, we need to use this instead.
    // We could also avoid using the SurfaceData struct completely, but it helps to organise things.
    half4 color = UniversalFragmentPBR(inputData, surfaceData.albedo, surfaceData.metallic, 
      surfaceData.specular, surfaceData.smoothness, surfaceData.occlusion, 
      surfaceData.emission, surfaceData.alpha);
                 
    color.rgb = MixFog(color.rgb, inputData.fogCoord);
 
    // color.a = OutputAlpha(color.a);
    // Not sure if this is important really. It's implemented as :
    // saturate(outputAlpha + _DrawObjectPassData.a);
    // Where _DrawObjectPassData.a is 1 for opaque objects and 0 for alpha blended.
    // But it was added in URP v8, and versions before just didn't have it.
    // And I'm writing thing for v7.3.1 currently
    // We could still saturate the alpha to ensure it doesn't go outside the 0-1 range though :
    color.a = saturate(color.a);
 
    return color;
}

當前穷蛹,雖然我們的著色器可以接收陰影土陪,但它不包含ShadowCaster傳遞,因此不會投射任何陰影俩莽。這將在下一部分中處理旺坠。

ShadowCaster & DepthOnly Passes

SHADOWCASTER

如果我們希望著色器投射陰影,則需要通過標簽“ LightMode” =“ ShadowCaster”的傳遞扮超∪∪校可以在“Unlit”和“Lit”著色器上進行此操作,但要注意出刷,盡管它們會投射陰影璧疗,但如果您不在UniversalForwardPass中處理陰影,它們將不會接收陰影馁龟。

除了使用UsePass時(在Shaderlab部分中已討論過)崩侠。盡管我們可以使用其他著色器中的陰影投射器,例如UsePass“Universal Render Pipeline/Lit/ShadowCaster”坷檩,但由于該著色器中使用的CBUFFER可能不同却音,因此SRP Batcher兼容性可能會丟失。

相反矢炼,您應該自己定義這些Pass系瓢,有一個取巧的解決方法,我們可以執(zhí)行以下操作:

Pass {
    Name "ShadowCaster"
    Tags { "LightMode"="ShadowCaster" }
 
    ZWrite On
    ZTest LEqual
 
    HLSLPROGRAM
    // Required to compile gles 2.0 with standard srp library
    #pragma prefer_hlslcc gles
    #pragma exclude_renderers d3d11_9x gles
    //#pragma target 4.5
 
    // Material Keywords
    #pragma shader_feature _ALPHATEST_ON
    #pragma shader_feature _SMOOTHNESS_TEXTURE_ALBEDO_CHANNEL_A
 
    // GPU Instancing
    #pragma multi_compile_instancing
    #pragma multi_compile _ DOTS_INSTANCING_ON
             
    #pragma vertex ShadowPassVertex
    #pragma fragment ShadowPassFragment
     
    #include "Packages/com.unity.render-pipelines.core/ShaderLibrary/CommonMaterial.hlsl"
    #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/SurfaceInput.hlsl"
    #include "Packages/com.unity.render-pipelines.universal/Shaders/ShadowCasterPass.hlsl"
 
    ENDHLSL
}

我們使用了ShadowCasterPass.hlsl中的函數(shù)句灌,意味著定義該Pass較為容易夷陋,但是它需要使用_BaseMap_BaseColor_Cutoff屬性胰锌,我們也需要將它們添加到UnityPerMaterial CBUFFER中骗绕。陰影投射器中的fragment函數(shù)僅在需要陰影的位置返回0,并丟棄不應有陰影的像素(請注意资昧,僅在啟用了_ALPHATEST_ON關鍵字的情況下才會發(fā)生裁剪)

如果我們的常規(guī)著色器通道也進行頂點位移酬土,則也需要將其添加到ShadowCaster通道中,以便正確投射位移的陰影格带。為了解決這個問題诺凡,我們要么將ShadowCasterPass的內(nèi)容復制到我們的過程中,要么只是定義一個新的頂點函數(shù)并交換#pragma頂點ShadowPassVertex践惑。例如 :

#pragma vertex vert
 
...
 
// function copied from ShadowCasterPass and edited slightly.
Varyings vert(Attributes input) {
    Varyings output;
    UNITY_SETUP_INSTANCE_ID(input);
 
    // Example Displacement
    input.positionOS += float4(0, _SinTime.y, 0, 0);
 
    output.uv = TRANSFORM_TEX(input.texcoord, _BaseMap);
    output.positionCS = GetShadowPositionHClip(input);
    return output;
}

DEPTHONLY

著色器還應包含標記為“ LightMode” =“ DepthOnly”的過程腹泌。此過程與ShadowCaster非常相似,但沒有陰影偏差偏移尔觉。我不完全確定URP中使用DepthOnly傳遞的用途凉袱。場景視圖似乎在渲染深度紋理時使用了它(由ShaderGraph中的“Scene Depth”節(jié)點使用),而“游戲視圖”深度紋理在沒有此傳遞的情況下似乎可以正常工作。但是专甩,可能還有其他一些東西钟鸵,例如自定義渲染功能(用于前向渲染器)依賴于DepthOnly傳遞。

我們可以以類似的方式處理DepthOnly傳遞涤躲,但有一些細微差異:

Pass {
    Name "DepthOnly"
    Tags { "LightMode"="DepthOnly" }
 
    ZWrite On
    ColorMask 0
 
    HLSLPROGRAM
    // Required to compile gles 2.0 with standard srp library
    #pragma prefer_hlslcc gles
    #pragma exclude_renderers d3d11_9x gles
    //#pragma target 4.5
 
    // Material Keywords
    #pragma shader_feature _ALPHATEST_ON
    #pragma shader_feature _SMOOTHNESS_TEXTURE_ALBEDO_CHANNEL_A
 
    // GPU Instancing
    #pragma multi_compile_instancing
    #pragma multi_compile _ DOTS_INSTANCING_ON
             
    #pragma vertex DepthOnlyVertex
    #pragma fragment DepthOnlyFragment
             
    #include "Packages/com.unity.render-pipelines.core/ShaderLibrary/CommonMaterial.hlsl"
    #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/SurfaceInput.hlsl"
    #include "Packages/com.unity.render-pipelines.universal/Shaders/DepthOnlyPass.hlsl"
 
    // Again, using this means we also need _BaseMap, _BaseColor and _Cutoff shader properties
    // Also including them in cbuffer, except _BaseMap as it's a texture.
 
    ENDHLSL
}

這次使用Unity的URP著色器提供的DepthOnlyPass 棺耍。同樣,如果需要頂點位移种樱,我們應該將DepthOnlyVertex函數(shù)復制到我們的代碼中蒙袍,將其重命名為vert,然后像上面的ShadowCaster示例中一樣添加位移代碼嫩挤。

最后編輯于
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末害幅,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子岂昭,更是在濱河造成了極大的恐慌以现,老刑警劉巖,帶你破解...
    沈念sama閱讀 207,113評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件约啊,死亡現(xiàn)場離奇詭異邑遏,居然都是意外死亡,警方通過查閱死者的電腦和手機恰矩,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,644評論 2 381
  • 文/潘曉璐 我一進店門无宿,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人枢里,你說我怎么就攤上這事□逦纾” “怎么了栏豺?”我有些...
    開封第一講書人閱讀 153,340評論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長豆胸。 經(jīng)常有香客問我奥洼,道長,這世上最難降的妖魔是什么晚胡? 我笑而不...
    開封第一講書人閱讀 55,449評論 1 279
  • 正文 為了忘掉前任灵奖,我火速辦了婚禮,結果婚禮上估盘,老公的妹妹穿的比我還像新娘瓷患。我一直安慰自己,他們只是感情好遣妥,可當我...
    茶點故事閱讀 64,445評論 5 374
  • 文/花漫 我一把揭開白布擅编。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪爱态。 梳的紋絲不亂的頭發(fā)上谭贪,一...
    開封第一講書人閱讀 49,166評論 1 284
  • 那天,我揣著相機與錄音锦担,去河邊找鬼俭识。 笑死,一個胖子當著我的面吹牛洞渔,可吹牛的內(nèi)容都是我干的套媚。 我是一名探鬼主播,決...
    沈念sama閱讀 38,442評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼痘煤,長吁一口氣:“原來是場噩夢啊……” “哼凑阶!你這毒婦竟也來了?” 一聲冷哼從身側響起衷快,我...
    開封第一講書人閱讀 37,105評論 0 261
  • 序言:老撾萬榮一對情侶失蹤宙橱,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后蘸拔,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體师郑,經(jīng)...
    沈念sama閱讀 43,601評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,066評論 2 325
  • 正文 我和宋清朗相戀三年调窍,在試婚紗的時候發(fā)現(xiàn)自己被綠了宝冕。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,161評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡邓萨,死狀恐怖地梨,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情缔恳,我是刑警寧澤宝剖,帶...
    沈念sama閱讀 33,792評論 4 323
  • 正文 年R本政府宣布,位于F島的核電站歉甚,受9級特大地震影響万细,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜纸泄,卻給世界環(huán)境...
    茶點故事閱讀 39,351評論 3 307
  • 文/蒙蒙 一赖钞、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧聘裁,春花似錦雪营、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,352評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春征唬,著一層夾襖步出監(jiān)牢的瞬間捌显,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,584評論 1 261
  • 我被黑心中介騙來泰國打工总寒, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留扶歪,地道東北人。 一個月前我還...
    沈念sama閱讀 45,618評論 2 355
  • 正文 我出身青樓摄闸,卻偏偏與公主長得像善镰,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子年枕,可洞房花燭夜當晚...
    茶點故事閱讀 42,916評論 2 344

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