本文繼續(xù)對(duì)《UnityShader入門精要》——馮樂(lè)樂(lè) 第十八章 基于物理的渲染 進(jìn)行學(xué)習(xí)
在https://github.com/candycat1992/Unity_Shaders_Book中岸更,馮樂(lè)樂(lè)給出了2019年改版后的第18章期丰,本文基于此版本進(jìn)行學(xué)習(xí)蹲盘。
一、實(shí)踐
我們已經(jīng)介紹了足夠多的理論內(nèi)容了溯祸,現(xiàn)在是時(shí)候動(dòng)手自己實(shí)現(xiàn)一個(gè)基于物理渲染的 Shader了!在本節(jié)中,我們將在 Unity Shader 中實(shí)現(xiàn) 18.1.7 節(jié)?到的 BRDF 模型绽快。讀者可以發(fā)現(xiàn),把 PBS應(yīng)用到自己的材質(zhì)中并不是一件非常困難的事情紧阔。
我們回顧使用了精確光源簡(jiǎn)化后的渲染方程:
其中坊罢,Le(v)是自發(fā)光部分,f(lic, v)是最為關(guān)鍵的 BRDF 模型部分擅耽。BRDF 的高光反射項(xiàng)則可以用下面的通用形式來(lái)表示:
在本例中活孩,我們會(huì)使用 Disney BRDF 中的漫反射項(xiàng)、Schlick 菲涅耳近似等式乖仇、基于 GGX 模型的法線分布函數(shù)和 Smith-Joint 陰影-遮掩函數(shù)作為 BRDF 光照模型的實(shí)現(xiàn)憾儒。在學(xué)習(xí)完本節(jié)后,我們會(huì)得到類似圖 18.7 中的效果:
① https://www.assetstore.unity3d.com/en/#!/content/58870
1.
(1)在 Unity 中新建一個(gè)場(chǎng)景乃沙。在本書(shū)資源中起趾,該場(chǎng)景名為 Scene_18_2。我們使用本書(shū)資源中的天空盒材質(zhì) EveningSkyboxHDR警儒,在 Window -> Lighting -> Skybox 中代替場(chǎng)景默認(rèn)的天空盒训裆。
(2)新建兩個(gè)材質(zhì)。在本書(shū)資源中蜀铲,這兩個(gè)材質(zhì)分別名為 CustomPBSCubeMat 和CustomPBSSphereMat边琉。
(3)新建一個(gè) Unity Shader。在本書(shū)資源中蝙茶,該 Unity Shader 名為 Chapter18-CustomPBR艺骂。把新的 Unity Shader 賦給第 2 步中創(chuàng)建的材質(zhì)。
(4)在場(chǎng)景中放置一個(gè)球體和立方體隆夯,并把第 2 步中的兩個(gè)材質(zhì)分別賦給兩個(gè)物體钳恕。
(5)保存場(chǎng)景别伏。
2.
打開(kāi)新建的 Chapter18-CustomPBR,刪除所有已有代碼忧额,并進(jìn)行如下修改厘肮。
(1)首先,我們需要為這個(gè) Unity Shader 起一個(gè)名字:Shader "Unity Shaders Book v2/Chapter 18/Custom PBR"
(2)然后睦番,我們需要在 Properties 語(yǔ)義塊中聲明 PBR 中需要的所有材質(zhì)屬性:
Properties {
_Color ("Color", Color) = (1, 1, 1, 1)
_MainTex ("Albedo", 2D) = "white" {}
_Glossiness ("Smoothness", Range(0.0, 1.0)) = 0.5
_SpecColor ("Specular", Color) = (0.2, 0.2, 0.2)
_SpecGlossMap ("Specular (RGB) Smoothness (A)", 2D) = "white" {}
_BumpScale ("Bump Scale", Float) = 1.0
_BumpMap ("Normal Map", 2D) = "bump" {}
_EmissionColor ("Color", Color) = (0, 0, 0)
_EmissionMap ("Emission", 2D) = "white" {}
}
其中类茂, _MainTex 和 _Color 用于控制漫反射項(xiàng)中的材質(zhì)紋 理和顏色, _SpecColor 和_SpecGlossMap 的 RGB 通道值用于控制材質(zhì)的高光反射顏色托嚣。_SpecGlossMap 的 A 通道值和_Glossiness 用于共同控制材質(zhì)的粗糙度巩检。_BumpMap 則是材質(zhì)的法線紋理,它的凹凸程度可以依靠_BumpScale 屬性來(lái)控制示启。最后兢哭,_EmissionColor 和_EmissionMap 用于控制材質(zhì)的自發(fā)光顏色。
3)定義 Forwad Base Pass:
SubShader {
Tags { "RenderType"="Opaque" }
LOD 300
Pass {
Tags { "LightMode" = "ForwardBase" }
CGPROGRAM
#pragma target 3.0
#pragma multi_compile_fwdbase
#pragma multi_compile_fog
注意夫嗓,在上面的代碼中我們通過(guò)使用#pragma target 3.0 來(lái)指明使用 Shader Target 3.0迟螺,這是因?yàn)榛谖锢礓秩旧婕傲溯^多的公式,因此需要較多的數(shù)學(xué)指令來(lái)進(jìn)行計(jì)算舍咖,這可能會(huì)超過(guò) Shader Target 2.0 對(duì)指令數(shù)目的規(guī)定矩父,因此我們選擇使用更高的 Shader Target 3.0。
4)接下來(lái)排霉,我們來(lái)定義頂點(diǎn)著色器:
struct v2f {
float4 pos : SV_POSITION;
float2 uv : TEXCOORD0;
float4 TtoW0 : TEXCOORD1;
float4 TtoW1 : TEXCOORD2;
float4 TtoW2 : TEXCOORD3;
SHADOW_COORDS(4) // Defined in AutoLight.cginc
UNITY_FOG_COORDS(5) // Defined in UnityCG.cginc
};
v2f vert(a2v v) {
v2f o;
UNITY_INITIALIZE_OUTPUT(v2f, o); // Defined in HLSLSupport.cginc
o.pos = UnityObjectToClipPos(v.vertex); // Defined in UnityCG.cginc
o.uv = TRANSFORM_TEX(v.texcoord, _MainTex); // Defined in UnityCG.cginc
float3 worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
fixed3 worldNormal = UnityObjectToWorldNormal(v.normal);
fixed3 worldTangent = UnityObjectToWorldDir(v.tangent.xyz);
fixed3 worldBinormal = cross(worldNormal, worldTangent) * v.tangent.w;
o.TtoW0 = float4(worldTangent.x, worldBinormal.x, worldNormal.x, worldPos.x);
o.TtoW1 = float4(worldTangent.y, worldBinormal.y, worldNormal.y, worldPos.y);
o.TtoW2 = float4(worldTangent.z, worldBinormal.z, worldNormal.z, worldPos.z);
//We need this for shadow receving
TRANSFER_SHADOW(o); // Defined in AutoLight.cginc
//We need this for fog rendering
UNITY_TRANSFER_FOG(o, o.pos); // Defined in UnityCG.cginc
return o;
}
頂點(diǎn)著色器中的計(jì)算比較簡(jiǎn)單窍株,我們?cè)谥暗恼鹿?jié)中也進(jìn)行過(guò)類似的計(jì)算。為了在片元著色器中把采樣得到的切線空間下的法線方向轉(zhuǎn)換到世界空間下攻柠,我們把變換矩陣的相關(guān)數(shù)據(jù)存儲(chǔ)在了 o.TtoW0夹姥、o.TtoW1 和 o.TtoW2 中。除此之外辙诞,我們還使用內(nèi)置宏 SHADOW_COORDS、UNITY_FOG_COORDS轻抱、TRANSFER_SHADOW 和 UNITY_TRANSFER_FOG 等聲明和計(jì)算了陰影和霧效所需要的一些紋理坐標(biāo)參數(shù)飞涂。
5)片元著色器是我們的重點(diǎn),我們來(lái)一步步看它是怎樣實(shí)現(xiàn)的:
half4 frag(v2f i) : SV_Target {
///// Prepare all the inputs
half4 specGloss = tex2D(_SpecGlossMap, i.uv);
specGloss.a *= _Glossiness;
half3 specColor = specGloss.rgb * _SpecColor.rgb;
half roughness = 1 - specGloss.a;
half oneMinusReflectivity = 1 - max(max(specColor.r, specColor.g), specColor.b);
half3 diffColor = _Color.rgb * tex2D(_MainTex, i.uv).rgb * oneMinusReflectivity;
half3 normalTangent = UnpackNormal(tex2D(_BumpMap, i.uv));
normalTangent.xy *= _BumpScale;
normalTangent.z = sqrt(1.0 - saturate(dot(normalTangent.xy, normalTangent.xy)));
half3 normalWorld = normalize(half3(dot(i.TtoW0.xyz, normalTangent),
dot(i.TtoW1.xyz, normalTangent), dot(i.TtoW2.xyz, normalTangent)));
float3 worldPos = float3(i.TtoW0.w, i.TtoW1.w, i.TtoW2.w);
half3 lightDir = normalize(UnityWorldSpaceLightDir(worldPos)); // Defined in
UnityCG.cginc
half3 viewDir = normalize(UnityWorldSpaceViewDir(worldPos)); // Defined in
UnityCG.cginc
half3 reflDir = reflect(-viewDir, normalWorld);
UNITY_LIGHT_ATTENUATION(atten, i, worldPos); // Defined in AutoLight.cginc
我們首先需要為后續(xù)計(jì)算準(zhǔn)備好所有的輸入數(shù)據(jù)祈搜,這些輸入大多來(lái)源于材質(zhì)面板中的各個(gè)屬性较店,例如漫反射顏色 diffColor 和高光反射顏色 specColor、粗糙度 roughness容燕、世界空間下的法線方向梁呈、光源方向、觀察方向蘸秘、反射方向等官卡。我們還使用內(nèi)置宏 UNITY_LIGHT_ATTENUATION 計(jì)算了陰影和光照衰減值 atten蝗茁。除此之外,我們還計(jì)算了一個(gè)變量 oneMinusReflectivity寻咒,這個(gè)變量并不是我們之前提到的 BRDF 中需要的變量哮翘,它主要是為了計(jì)算掠射角的反射顏色,從而得到效果更好的菲涅耳反射效果毛秘。
接下來(lái)饭寺,我們開(kāi)始計(jì)算最重要的 BRDF 光照模型。在此之前叫挟,我們先準(zhǔn)備好各個(gè)角度的余弦值艰匙,即之前公式中的各個(gè)點(diǎn)乘項(xiàng),如(n ? v)抹恳、(n ? l)等:
///// Compute BRDF terms
half3 halfDir = normalize(lightDir + viewDir);
half nv = saturate(dot(normalWorld, viewDir));
half nl = saturate(dot(normalWorld, lightDir));
half nh = saturate(dot(normalWorld, halfDir));
half lv = saturate(dot(lightDir, viewDir));
half lh = saturate(dot(lightDir, halfDir));
通過(guò)使用 Cg 的 saturate 函數(shù)员凝,我們把這些點(diǎn)乘值的范圍截取到了[0, 1]之間,來(lái)避免背光面的光照适秩。然后绊序,我們來(lái)計(jì)算 BRDF 中的漫反射項(xiàng):
// Diffuse term
half3 diffuseTerm = CustomDisneyDiffuseTerm(nv, nl, lh, roughness, diffColor);
6)對(duì)于漫反射項(xiàng),我們選擇使用 Disney BRDF 中的漫反射項(xiàng)實(shí)現(xiàn)秽荞,CustomDisneyDiffuseTerm函數(shù)的實(shí)現(xiàn)(依照 Disney BRDF 中的漫反射項(xiàng)公式)如下:
inline half3 CustomDisneyDiffuseTerm(half NdotV, half NdotL, half LdotH, half
roughness, half3 baseColor) {
half fd90 = 0.5 + 2 * LdotH * LdotH * roughness;
// Two schlick fresnel term
half lightScatter = (1 + (fd90 - 1) * pow(1 - NdotL, 5));
half viewScatter = (1 + (fd90 - 1) * pow(1 - NdotV, 5));
return baseColor * UNITY_INV_PI * lightScatter * viewScatter;
}
這個(gè)函數(shù)非常簡(jiǎn)單骤公,我們就是按照之前提到的公式實(shí)現(xiàn)相應(yīng)代碼而已。UNITY_INV_PI 是在UnityCG.cginc 文件中定義的宏變量,即圓周率π的倒數(shù)。在上面的實(shí)現(xiàn)中麦牺,我們還使用了 Cg 關(guān)鍵詞 inline①來(lái)修飾函數(shù)聲明疙筹,inline 的作用是用于告訴編譯器應(yīng)該盡可能使用內(nèi)聯(lián)調(diào)用的方式來(lái)調(diào)用該函數(shù),減少函數(shù)調(diào)用的開(kāi)銷茅糜。
① http://http.developer.nvidia.com/Cg/Cg_language.html
7)下面,我們來(lái)實(shí)現(xiàn)高光反射項(xiàng):
// Specular term
half V = CustomSmithJointGGXVisibilityTerm(nl, nv, roughness);
half D = CustomGGXTerm(nh, roughness * roughness);
half3 F = CustomFresnelTerm(specColor, lh);
half3 specularTerm = F * V * D;
首先是可見(jiàn)性項(xiàng) V,它計(jì)算的是陰影-遮掩函數(shù)除以高光反射項(xiàng)的分母部分后的結(jié)果垒棋。CustomSmithJointGGXVisibilityTerm 函數(shù)的實(shí)現(xiàn)(依照 Eric Heitz[12]提出的按 Height-Correlated Masking and Shadowing 方式組合的 Smith-Joint 陰影-遮掩函數(shù))如下:
inline half CustomSmithJointGGXVisibilityTerm(half NdotL, half NdotV, half roughness)
{
// Original formulation:
// lambda_v = (-1 + sqrt(a2 * (1 - NdotL2) / NdotL2 + 1)) * 0.5f;
// lambda_l = (-1 + sqrt(a2 * (1 - NdotV2) / NdotV2 + 1)) * 0.5f;
// G = 1 / (1 + lambda_v + lambda_l);
// Approximation of the above formulation (simplify the sqrt, not mathematically
correct but close enough)
half a2 = roughness * roughness;
half lambdaV = NdotL * (NdotV * (1 - a2) + a2);
half lambdaL = NdotV * (NdotL * (1 - a2) + a2);
return 0.5f / (lambdaV + lambdaL + 1e-5f);
}
接下來(lái)是法線分布項(xiàng) D,CustomGGXTerm 函數(shù)的實(shí)現(xiàn)(依照基于 GGX 模型的法線分布函數(shù))如下:
inline half CustomGGXTerm(half NdotH, half roughness) {
half a2 = roughness * roughness;
half d = (NdotH * a2 - NdotH) * NdotH + 1.0f;
return UNITY_INV_PI * a2 / (d * d + 1e-7f);
}
最后是菲涅耳反射項(xiàng) F痪宰,CustomFresnelTerm 函數(shù)(依照 Schlick 菲涅耳近似等式[7])的實(shí)現(xiàn)如下:
inline half3 CustomFresnelTerm(half3 c, half cosA) {
half t = pow(1 - cosA, 5);
return c + (1 - c) * t;
}
最后的高光反射項(xiàng)就是把 V叼架、D 和 F 相乘后的結(jié)果。
8)接下來(lái)衣撬,我們還需要計(jì)算自發(fā)光項(xiàng):
// Emission term
half3 emisstionTerm = tex2D(_EmissionMap, i.uv).rgb * _EmissionColor.rgb;
自發(fā)光項(xiàng)非常簡(jiǎn)單乖订,我們只需要從自發(fā)光紋理中進(jìn)行采樣再乘以自發(fā)光顏色即可。
9)為了得到更加真實(shí)的光照具练,我們還需要計(jì)算基于圖像的光照部分(IBL):
// IBL
half perceptualRoughness = roughness * (1.7 - 0.7 * roughness);
half mip = perceptualRoughness * 6;
half4 envMap = UNITY_SAMPLE_TEXCUBE_LOD(unity_SpecCube0, reflDir, mip); // Defined in
HLSLSupport.cginc
half grazingTerm = saturate((1 - roughness) + (1 - oneMinusReflectivity));
half surfaceReduction = 1.0 / (roughness * roughness + 1.0);
half3 indirectSpecular = surfaceReduction * envMap.rgb * CustomFresnelLerp(specColor,
grazingTerm, nv);
IBL 部分的主要思想是使用材質(zhì)粗糙度對(duì)環(huán)境貼圖進(jìn)行 LOD(Level Of Detail)采樣乍构,這是因?yàn)榇植诙仍酱蟮牟馁|(zhì),反射的環(huán)境光照應(yīng)該越模糊扛点,而這可以通過(guò)對(duì)環(huán)境貼圖不同級(jí)數(shù)的多級(jí)漸遠(yuǎn)紋理(mipmaps)進(jìn)行采樣來(lái)模擬得到哥遮。級(jí)數(shù)越高岂丘,在多級(jí)漸遠(yuǎn)紋理中對(duì)應(yīng)的紋理就越小,圖像也就越模糊昔善。
為了計(jì)算需要采樣的多級(jí)漸遠(yuǎn)紋理的級(jí)數(shù)元潘,我們將材質(zhì)粗糙度乘以某個(gè)常數(shù)(在上述實(shí)現(xiàn)中該常數(shù)為 6),這個(gè)常數(shù)表明了整個(gè)粗糙度范圍內(nèi)多級(jí)漸遠(yuǎn)紋理的總級(jí)數(shù)君仆。需要注意的是翩概,這種由粗糙度計(jì)算級(jí)數(shù)的方法并不是唯一的,讀者可以在 UnityImageBasedLighting.cginc 文件的perceptualRoughnessToMipmapLevel 函數(shù)中找到相關(guān)實(shí)現(xiàn)返咱。
然后钥庇,我們使用該級(jí)數(shù)和反射方向來(lái)對(duì)環(huán)境貼圖進(jìn)行采樣。其中咖摹,unity_SpecCube0 包含了該物體周圍當(dāng)前活躍的反射探針(Reflection Probe)中所包含的環(huán)境貼圖评姨。盡管我們沒(méi)有在場(chǎng)景中手動(dòng)放置任何反射探針,但 Unity 會(huì)根據(jù)Window -> Lighting -> Skybox 中的設(shè)置萤晴,在場(chǎng)景中生成一個(gè)默認(rèn)的反射探針吐句。由于在本節(jié)的準(zhǔn)備工作中我們?cè)?Window -> Lighting -> Skybox 中設(shè)置了自定義的天空盒,因此此時(shí) unity_SpecCube0中包含的就是這個(gè)自定義天空盒的環(huán)境貼圖店读。
如果我們?cè)趫?chǎng)景中放置了其他反射探針嗦枢,Unity 則會(huì)根據(jù)相關(guān)設(shè)置和物體所在的位置自動(dòng)把距離該物體最近的一個(gè)或幾個(gè)反射探針數(shù)據(jù)傳遞給 Shader。盡管在之前的內(nèi)容中屯断,我們是使用 samplerCUBE 來(lái)聲明一個(gè)立方體貼圖并使用 texCUBE 來(lái)采樣它文虏,但是 Unity 內(nèi)置反射探針的立方體貼圖則是以一種特殊的方式聲明的,這主要是為了在某些平臺(tái)下可以節(jié)省 sampler slots殖演。讀者可以在 UnityShaderVariables.cginc 文件中找到 unity_SpecCube0的聲明氧秘,Unity 主要是通過(guò) HLSLSupport.cginc 文件中定義的內(nèi)置宏 UNITY_DECLARE_TEXCUBE來(lái)實(shí)現(xiàn)的。 由于這樣的特殊性趴久, 在采樣 unity_SpecCube0 時(shí)我們也應(yīng)該使用內(nèi)置宏如UNITY_SAMPLE_TEXCUBE(在 HLSLSupport.cginc 文件中被定義)來(lái)采樣丸相。由于在這里我們還需要對(duì)指定級(jí)數(shù)的多級(jí)漸遠(yuǎn)紋理采樣,因此我們使用內(nèi)置宏 UNITY_SAMPLE_TEXCUBE_LOD(在 HLSLSupport.cginc 文件中被定義)來(lái)實(shí)現(xiàn)彼棍。
至此已添,我們得到了采樣后的環(huán)境光照顏色 envMap。然后滥酥,為了給 IBL 添加更加真實(shí)的菲涅耳反射,我們對(duì)高光反射顏色 specColor 和掠射顏色grazingTerm 進(jìn)行菲涅耳插值畦幢。掠射顏色 grazingTerm 是由材質(zhì)粗糙度和之前計(jì)算得到的oneMinusReflectivity 共同決定的坎吻。使用掠射角度進(jìn)行菲涅耳插值的好處是,我們可以在掠射角得到更加真實(shí)的菲涅耳反射效果宇葱,同時(shí)還考慮了材質(zhì)粗糙度的影響瘦真。除此之外刊头,我們還使用了由粗糙度計(jì)算得到的 surfaceReduction 參數(shù)進(jìn)一步對(duì) IBL 的進(jìn)行修正。CustomFresnelLerp 的函數(shù)實(shí)現(xiàn)如下:
inline half3 CustomFresnelLerp(half3 c0, half3 c1, half cosA) {
half t = pow(1 - cosA, 5);
return lerp (c0, c1, t);
}
它的實(shí)現(xiàn)和之前實(shí)現(xiàn)的 CustomFresnelTerm 函數(shù)很類似诸尽,不同的是這里使用參數(shù) t 來(lái)混合兩個(gè)顏色原杂。盡管 grazingTerm 被聲明為單一維數(shù)的 half 變量,在傳遞給 CustomFresnelLerp 時(shí)它會(huì)自動(dòng)被轉(zhuǎn)換成 half3 類型的變量您机,這在 Cg 中被稱為是"Smearing" Of Scalars To Vectors①穿肄。
① http://http.developer.nvidia.com/Cg/Cg_language.html
10)最后,我們只需要按照渲染方程把所有項(xiàng)加起來(lái)即可:
// Combine all togather
half3 col = emisstionTerm + UNITY_PI * (diffuseTerm + specularTerm) * _LightColor0.rgb
* nl * atten + indirectSpecular;
UNITY_APPLY_FOG(i.fogCoord, c.rgb); // Defined in UnityCG.cginc
return half4(col, 1);
在返回最后的像素顏色前际看,我們還添加了霧效的影響咸产。至此我們完成了 Forward Base Pass 中的所有實(shí)現(xiàn)。
11)由于場(chǎng)景中可能存在多個(gè)光源仲闽,我們還需要實(shí)現(xiàn) Forward Add Pass脑溢。Forward Add Pass 的實(shí)現(xiàn)與 Forward Base Pass 基本一致,其中不同的是赖欣,F(xiàn)orward Add Pass 不需要計(jì)算霧效屑彻、自發(fā)光和 IBL 的部分,因?yàn)檫@些只需要在 Forward Base Pass 計(jì)算一遍即可顶吮。其他實(shí)現(xiàn)在此不再贅述社牲。至此,我們就完成了一個(gè)較為完整的基于物理渲染的 Shader云矫。
保存后返回場(chǎng)景膳沽,再調(diào)整相關(guān)參數(shù)即可得到類似圖 18.7 中的效果。圖 18.7 中物體所用的材質(zhì)面板如圖 18.8 所示让禀,它們分別對(duì)應(yīng)了一個(gè)金屬類型的材質(zhì)和一個(gè)塑料類型的材質(zhì)挑社。關(guān)于如何使用PBR 設(shè)置各種材質(zhì)參數(shù),讀者可以參見(jiàn) 18.3.2 節(jié)中的內(nèi)容巡揍。
需要注意的是痛阻,我們還需要保證 Player Settings → Other Settings → Rendering → Color Space 中的選項(xiàng)是 Linear,即線性空間腮敌,只有這樣才能保證我們的計(jì)算是在線性空間下進(jìn)行的阱当,且輸出的為線性顏色。與線性空間相關(guān)的是伽馬校正糜工,這部分內(nèi)容讀者可以參見(jiàn)本章的 18.4.2 節(jié)弊添。
在上面的內(nèi)容中,我們依靠自定義的函數(shù)實(shí)現(xiàn)了一個(gè)基于 GGX BRDF 模型的 Shader捌木。實(shí)際上油坝,Unity 已經(jīng)幫我們實(shí)現(xiàn)了很多 BRDF 模型中的函數(shù),并為我們提供了現(xiàn)成的基于物理著色的Shader,也就是 Standard Shader澈圈。