1.1 前言
在SRP中C++提供了最底層的渲染接口,URP和HDRP根據底層渲染接口構建出各自的渲染管線。如下圖所示,整個幀渲染的每個Pass都是在C#中完成,只需要打開URP的源碼就可以輕松進行調試,這在Built-in管線中是不可能做到的。管線開源還有個好處就是我們可以進一步優化性能,URP為了兼容性默認會經過4次RT拷貝,但其實完全可以節約掉這部分性能,只需要改改源碼就可以實現。
Unity目前攝像機動態分辨率支持的不佳,只有在iOS和Android的Vulkan環境下才支持,由於目前大部分設備還只是OpenGL ES,所以無法做到3D降分辨率,UI不降分辨率。有了URP我們就可以通過修改源碼的方式實現這種需求。如下圖所示, 我們將3D攝像機的分辨率改成了0.5,這樣渲染的壓力大大減小。

如下圖所示,后面UI是高清分辨率直接寫到FrameBuffer中,從而實現性能與效果的雙重兼容性。

本篇文章會提供一些經典案例,比如URP優化RT拷貝次數、UI一部分背景+3D模糊效果、干掉FinalBlit優化性能,3D部分與UI部分不同分辨率提高性能與效果。這些功能都離不開源碼的定制以及修改,所以對URP源碼我們一定要了然於胸。
1.2 渲染管線與渲染技術
渲染管線和渲染技術可以說是兩個完全不同的概念,渲染技術和平台引擎是無關的,學術界的大佬將渲染公式研究出來,才慢慢應用在工業界。
比如Phong光照模型,Bui Tuong Phong(裴祥風)1975年出版的博士論文 《Illumination for computer generated pictures(計算機生成圖片光照)》中的計算光的反射向量有一定開銷,Jim Blinn(吉姆·布林)就基於Phong光照模型的基礎上於1977年提出使用光的向量+視向量算出中向量(計算中向量的效率高於計算反射向量),中向量與法向量做點乘計算高光強度,這就是我們現在手繪貼圖最常用的Blinn–Phong光照模型。
現在手游中,PBR光照模型幾乎已經成為標配,早在上世紀80年代由康奈爾大學就開始研究物理正確的渲染理論基礎。迪士尼(Disney) 在總結前人經驗的基礎上,於2012年首先提出實時渲染的PBR方案。論文中提到,原本他們可以實現多個物理光照模型,讓藝術家們選擇和組合它們,但是無法避免參數過多的情況。所以他們將實時基於物理的渲染整合成一個模型。該光照模型由Epic Games首先應用於自家Unreal Engine的實時渲染中,上一章我們提到Unity也根據這個模型提出了一套精簡版擬合方案。
如果大家對PBR的理論基礎感興趣,推薦閱讀出版於2016年的這本曠世巨作《Physically Based Rendering(基於物理的渲染)》,作者是三位巨佬:包括Matt Pharr(NVIDIA的傑出研究科學家)、Wenzel Jakob(EPFL計算機與通信科學學院的助理教授)和Greg Humphreys(FanDuel的工程總監,之前曾在Google的Chrome圖形團隊和NVIDIA的OptiX GPU光線追蹤引擎工作)。
這本書提供了在線免費閱讀,推薦大家一定要看《Physically Based Rendering(基於物理的渲染)》。
通過Phong光照模型和PBR光照模型我們可以看出,渲染技術與渲染引擎是無關的,時至今日渲染技術還有很多,實時圖形渲染背后的都是學術界的大佬,並不受限於Unity引擎或者Unreal Engine引擎。再回到工業界游戲引擎會根據學術界的公式適當做一些拓展或者優化,並且引入一些通用渲染效果加入引擎中。目前比較火的崗位包括圖形程序員和技術美術。我認為頂級的圖形程序員並不僅僅是渲染公式的搬運工,一定要搞懂背后的原理,只有完全搞懂公式的原理才能有目的性地修改渲染公式,實現項目風格化的圖形渲染。(在這條路上我願與君共勉。)
1.2.1 普通光照計算
內置管線中將光照都封裝在Surface Shaders中,不利於學習與修改,現在URP將Blinn-Phong和PBR的光照都封裝在Lighting.hlsl中,直接引用即可。如果某些地方想修改也可以自己寫一個Shader文件,引用一個自定義的Lighting.hlsl:
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl"
計算光照必須要得到每盞光的光方向、光顏色、光衰減、視線方向和着色點法線方向。
1. Blinn-Phong
在URP中直接使用SimpleLit.shader就可以直接實現Blinn-Phong光照,在頂點着色器中先得到視線方向:
Varyings LitPassVertexSimple(Attributes input) { Varyings output = (Varyings)0; //...略 output.viewDir = GetWorldSpaceViewDir(vertexInput.positionWS); //...略 return output; }
在片元着色器中計算光照顏色:
half4 LitPassFragmentSimple(Varyings input) : SV_Target { //...略 InputData inputData; InitializeInputData(input, normalTS, inputData); //...略 half4 color = UniversalFragmentBlinnPhong(inputData, diffuse, specular, smoothness, emission, alpha); return color; }
最終結果:
half4 UniversalFragmentBlinnPhong(InputData inputData, half3 diffuse, half4 specularGloss, half smoothness, half3 emission, half alpha) { //主光 Light mainLight = GetMainLight(inputData.shadowCoord, inputData.positionWS, shadowMask); half3 attenuatedLightColor = mainLight.color * (mainLight.distanceAttenuation * mainLight.shadowAttenuation); half3 diffuseColor = inputData.bakedGI + LightingLambert(attenuatedLightColor, mainLight.direction, inputData.normalWS); half3 specularColor = LightingSpecular(attenuatedLightColor, mainLight.direction, inputData.normalWS, inputData.viewDirectionWS, specularGloss, smoothness); //點光 #ifdef _ADDITIONAL_LIGHTS uint pixelLightCount = GetAdditionalLightsCount(); for (uint lightIndex = 0u; lightIndex < pixelLightCount; ++lightIndex) { Light light = GetAdditionalLight(lightIndex, inputData.positionWS, shadowMask); #if defined(_SCREEN_SPACE_OCCLUSION) light.color *= aoFactor.directAmbientOcclusion; #endif half3 attenuatedLightColor = light.color * (light.distanceAttenuation * light.shadowAttenuation); diffuseColor += LightingLambert(attenuatedLightColor, light.direction, inputData.normalWS); specularColor += LightingSpecular(attenuatedLightColor, light.direction, inputData.normalWS, inputData.viewDirectionWS, specularGloss, smoothness); } #endif //最終結果 環境光 + 漫反射 + 高光 half3 finalColor = diffuseColor * diffuse + emission + specularColor; return half4(finalColor, alpha); }
1.2.2 PBR光照計算
接着就是處理主光陰影的CBUFFER_START(MainLightShadows),URP的C#代碼將陰影級聯與陰影偏移等參數傳遞到GPU中,Shader根據這些參數就可以采樣正確的陰影信息了。
Shadows.hlsl #ifndef SHADER_API_GLES3 CBUFFER_START(MainLightShadows) #endif // Last cascade is initialized with a no-op matrix. It always transforms // shadow coord to half3(0, 0, NEAR_PLANE). We use this trick to avoid // branching since ComputeCascadeIndex can return cascade index = MAX_SHADOW_CASCADES float4x4 _MainLightWorldToShadow[MAX_SHADOW_CASCADES + 1]; float4 _CascadeShadowSplitSpheres0; float4 _CascadeShadowSplitSpheres1; float4 _CascadeShadowSplitSpheres2; float4 _CascadeShadowSplitSpheres3; float4 _CascadeShadowSplitSphereRadii; half4 _MainLightShadowOffset0; half4 _MainLightShadowOffset1; half4 _MainLightShadowOffset2; half4 _MainLightShadowOffset3; half4 _MainLightShadowParams; // (x: shadowStrength, y: 1.0 if soft shadows, 0.0 otherwise, z: oneOverFadeDist, w: minusStartFade) float4 _MainLightShadowmapSize; // (xy: 1/width and 1/height, zw: width and height) #ifndef SHADER_API_GLES3 CBUFFER_END #endif
URP的C#部分代碼是如何傳遞以上信息的,請大家仔細閱讀MainLightShadowCasterPass.cs。
非主光陰影參數CBUFFER_START(AdditionalLightShadows)同樣是由C#傳遞進來的。
Shadows.hlsl #ifndef SHADER_API_GLES3 CBUFFER_START(AdditionalLightShadows) #endif float4x4 _AdditionalLightsWorldToShadow[MAX_VISIBLE_LIGHTS]; half4 _AdditionalShadowParams[MAX_VISIBLE_LIGHTS]; half4 _AdditionalShadowOffset0; half4 _AdditionalShadowOffset1; half4 _AdditionalShadowOffset2; half4 _AdditionalShadowOffset3; float4 _AdditionalShadowmapSize; // (xy: 1/width and 1/height, zw: width and height) #ifndef SHADER_API_GLES3 CBUFFER_END #endif #endif
URP的C#部分代碼是如何傳遞以上信息的,請大家仔細閱讀AdditionalLightsShadowCasterPass.cs。
有了陰影的參數還不夠,還需要將1盞主光和8盞非主光的顏色、矩陣、LightProbe和燈光的衰減從URP的C#代碼傳入GPU中,這樣Shader就能着色燈光和陰影了。如以下代碼所示,每個物體都需要引用Input.hlsl並且C#中傳遞主光的信息。
Input.hlsl float4 _MainLightPosition; half4 _MainLightColor; half4 _MainLightOcclusionProbes;
接着再傳遞點光的信息,它們保存在CBUFFER_START(AdditionalLights)中:
Input.hlsl #ifndef SHADER_API_GLES3 CBUFFER_START(AdditionalLights) #endif float4 _AdditionalLightsPosition[MAX_VISIBLE_LIGHTS]; half4 _AdditionalLightsColor[MAX_VISIBLE_LIGHTS]; half4 _AdditionalLightsAttenuation[MAX_VISIBLE_LIGHTS]; half4 _AdditionalLightsSpotDir[MAX_VISIBLE_LIGHTS]; half4 _AdditionalLightsOcclusionProbes[MAX_VISIBLE_LIGHTS]; #ifndef SHADER_API_GLES3 CBUFFER_END #endif #endif
如下圖所示,C#代碼中請大家仔細閱讀URP中ForwardLights.cs類文件,可以找到實際傳遞的地方。
光的數據已經全部傳遞到Shader中了,接着就可以計算光照了。請大家仔細閱讀Lighting.hlsl文件,這里包含了物體表面主光和點光源的顏色計算。
Light mainLight = GetMainLight(inputData.shadowCoord, inputData.positionWS, shadowMask); //..部分略,mainLight計算物體表面的主光顏色 color += LightingPhysicallyBased(brdfData, brdfDataClearCoat, mainLight, inputData.normalWS, inputData.viewDirectionWS, surfaceData.clearCoatMask, specularHighlightsOff); #ifdef _ADDITIONAL_LIGHTS uint pixelLightCount = GetAdditionalLightsCount(); for (uint lightIndex = 0u; lightIndex < pixelLightCount; ++lightIndex) { Light light = GetAdditionalLight(lightIndex, inputData.positionWS, shadowMask); #if defined(_SCREEN_SPACE_OCCLUSION) light.color *= aoFactor.directAmbientOcclusion; #endif //light計算物體表明點光的顏色,最多8盞顏色進行相加 color += LightingPhysicallyBased(brdfData, brdfDataClearCoat, light, inputData.normalWS, inputData.viewDirectionWS, surfaceData.clearCoatMask, specularHighlightsOff); } #endif
最終物體表面顏色由BRDF乘以輻射率得出:
half3 LightingPhysicallyBased(BRDFData brdfData, BRDFData brdfDataClearCoat, half3 lightColor, half3 lightDirectionWS, half lightAttenuation, half3 normalWS, half3 viewDirectionWS, half clearCoatMask, bool specularHighlightsOff) { //..部分代略 half NdotL = saturate(dot(normalWS, lightDirectionWS)); //請注意這里,光源顏色*光源衰減*(法線方向點乘光的方向) half3 radiance = lightColor * (lightAttenuation * NdotL); half3 brdf = brdfData.diffuse; brdf += brdfData.specular * DirectBRDFSpecular(brdfData, normalWS, lightDirectionWS, viewDirectionWS); //最終將計算的輻射率*BRDF return brdf * radiance; }
1.2.3 BRDF優化
Unity的工程師Renaldas Zioma在2015年發表了一篇在移動端優化PBR的論文《See "Optimizing PBR for Mobile" from Siggraph 2015 moving mobile graphics course》(強烈建議大家看看),對BRDF高光感興趣的同學可以繼續看這段代碼。
half DirectBRDFSpecular(BRDFData brdfData, half3 normalWS, half3 lightDirectionWS, half3 viewDirectionWS) { float3 halfDir = SafeNormalize(float3(lightDirectionWS) + float3(viewDirectionWS)); float NoH = saturate(dot(normalWS, halfDir)); half LoH = saturate(dot(lightDirectionWS, halfDir)); // GGX Distribution multiplied by combined approximation of Visibility and Fresnel // BRDFspec = (D * V * F) / 4.0 // D = roughness^2 / ( NoH^2 * (roughness^2 - 1) + 1 )^2 // V * F = 1.0 / ( LoH^2 * (roughness + 0.5) ) // See "Optimizing PBR for Mobile" from Siggraph 2015 moving mobile graphics course // https://community.arm.com/events/1155 // Final BRDFspec = roughness^2 / ( NoH^2 * (roughness^2 - 1) + 1 )^2 * (LoH^2 * (roughness + 0.5) * 4.0) // We further optimize a few light invariant terms // brdfData.normalizationTerm = (roughness + 0.5) * 4.0 rewritten as roughness * 4.0 + 2.0 to a fit a MAD. float d = NoH * NoH * brdfData.roughness2MinusOne + 1.00001f; half LoH2 = LoH * LoH; half specularTerm = brdfData.roughness2 / ((d * d) * max(0.1h, LoH2) * brdfData.normalizationTerm); // On platforms where half actually means something, the denominator has a risk of overflow // clamp below was added specifically to "fix" that, but dx compiler (we convert bytecode to metal/gles) // sees that specularTerm have only non-negative terms, so it skips max(0,..) in clamp (leaving only min(100,...)) #if defined (SHADER_API_MOBILE) || defined (SHADER_API_SWITCH) specularTerm = specularTerm - HALF_MIN; specularTerm = clamp(specularTerm, 0.0, 100.0); // Prevent FP16 overflow on mobiles #endif return specularTerm; }
代碼中的注釋可以清晰地看到Unity是如何進行優化的,首先看看完整的BRDF公式。
D:微表面分部函數(注意這里的D就是GGX);
G:遮擋可見性函數(注意這里的G並不是GGX);
F:菲涅爾函數;
首先Unity先將G項進行擬合稱為V項遮擋可見性函數,如下圖所示,擬合結果 V=G/(4(N⋅V)(N⋅L)):
接着對V*F進行擬合:
最終BRDF = V * F * D (前面說到D就是GGX):
如下圖所示,按照上面的公式大家可以自己乘一下看看最終是不是正確。
大家可以按照這個公式再對比一下前面函數DirectBRDFSpecular計算的是否正確。