神说要有光,于是便有了光。EmiteInna说要有光,他的shader粉了。
这次笔记没什么美术含量,算是一个代码解析,如果有错误的地方欢迎勘误。
UnityURPshader光照部分学习分析(上)-基本结构 BRDF方程 首先,在这里放置一个cook-torrance的BRDF方程,对照着这个来分析代码。
公式图来自其他大佬的整理。
Lighting.hlsl 首先看Lighting.hlsl里面,前两个是我们再熟悉不过的兰伯特漫反射和BlinnPhong高光算法。
代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 half3 LightingLambert(half3 lightColor, half3 lightDir, half3 normal) { half NdotL = saturate(dot(normal, lightDir)); return lightColor * NdotL; } half3 LightingSpecular(half3 lightColor, half3 lightDir, half3 normal, half3 viewDir, half4 specular, half smoothness) { float3 halfVec = SafeNormalize(float3(lightDir) + float3(viewDir)); half NdotH = half(saturate(dot(normal, halfVec))); half modifier = pow(NdotH, smoothness); half3 specularReflection = specular.rgb * modifier; return lightColor * specularReflection; }
往后出现了LightingPhysicallyBased函数,这个函数大概就是Unity自己的PBR函数之一,他的参数包含两个BRDFData结构体——brdfData,brdfDataClearCoat,和一些输入,包括lightColor,lightDirectionWS,lightAttenuation,normalWS,viewDirectionWS,clearCoatMask,specularHighlightsOff,大体都是比较熟悉的变量。
为了搞明白所有的参数,先进入BRDF.hlsl里去看看。
BRDF.hlsl 代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 struct BRDFData { half3 albedo; half3 diffuse; half3 specular; half reflectivity; half perceptualRoughness; half roughness; half roughness2; half grazingTerm; // We save some light invariant BRDF terms so we don't have to recompute // them in the light loop. Take a look at DirectBRDF function for detailed explaination. half normalizationTerm; // roughness * 4.0 + 2.0 half roughness2MinusOne; // roughness^2 - 1.0 };
声明了一些基础变量和额外变量,额外变量在代码中URP也写了注释,但是有些还是难以理解(因为不出现在cook-torrance的方程里,比如grazingTerm掠射角项、perceptualRoughness感知粗糙度(字面翻译..))即使不理解也没关系,等着看后面的代码。
同时,BRDF.hlsl文件定义了非电解质的反射率0(也就是F0)为(0.04,0.04,0.04,1-0.04)。以及一些能够为BRDF数据做好初始化的其它转换函数,在这里它又用到了BSDF.hlsl、CommonMaterial.hlsl、Deprecated.hlsl、SurfaceData.hlsl四个hlsl文件,从后往前去看。
SurfaceData.hlsl 这个文件就是一个非常简单的声明结构体的文件,它包含了一些我们经常会用到的数据。
代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 struct SurfaceData { half3 albedo; half3 specular; half metallic; half smoothness; half3 normalTS; half3 emission; half occlusion; half alpha; half clearCoatMask; half clearCoatSmoothness; };
基础反射率albedo、高光值specular、金属度metallic、平滑度smoothness、切线空间法线normalTS、emission自发光、occlusion环境吸收、alpha透明度,两个clearCoat之后再看。
回到BRDF.hlsl-不那么熟悉的BRDF方程 熟悉catlikecoding的大佬们可能会发现surfaceData到BRDFData的这个过程和catlikecoding教程里的SRP系列是同一个写法(毕竟教程里也说了是模仿URP写的)。
但实际上我们发现BRDF.hlsl好像并没有强制用到SurfaceData,而只是把它用作初始化BRDF的一个选项。
InitializeBRDFDataDirect()函数直接把输出的BRDFData按顺序填进去,而实际上用于初始化的代码是IntializeBRDFData函数,它用_SPECULAR_STEP宏把BRDFdata的初始化分为了金属流和高光流两种,在初始化上有一些区别(主要在于处理reflectivity、brdfDiffuse、brdfSpecular)
暂且跳过关于clearcoat的部分,往下看到EnvironmentBRDF函数。
代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 half3 EnvironmentBRDFSpecular(BRDFData brdfData, half fresnelTerm) { float surfaceReduction = 1.0 / (brdfData.roughness2 + 1.0); return half3(surfaceReduction * lerp(brdfData.specular, brdfData.grazingTerm, fresnelTerm)); } half3 EnvironmentBRDF(BRDFData brdfData, half3 indirectDiffuse, half3 indirectSpecular, half fresnelTerm) { half3 c = indirectDiffuse * brdfData.diffuse; c += indirectSpecular * EnvironmentBRDFSpecular(brdfData, fresnelTerm); return c; }
然后是DirectBRDFSpecular
代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 // Computes the scalar specular term for Minimalist CookTorrance BRDF // NOTE: needs to be multiplied with reflectance f0, i.e. specular color to complete half DirectBRDFSpecular(BRDFData brdfData, half3 normalWS, half3 lightDirectionWS, half3 viewDirectionWS) { float3 lightDirectionWSFloat3 = float3(lightDirectionWS); float3 halfDir = SafeNormalize(lightDirectionWSFloat3 + float3(viewDirectionWS)); float NoH = saturate(dot(float3(normalWS), halfDir)); half LoH = half(saturate(dot(lightDirectionWSFloat3, 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; }
直接光的brdf就好了很多,unity在注释里为我们细说了每一行公式的意义。
根据注释,Unity的高光反射项和文章顶部的公式并不是相同的。
这是我们数值的cook-torrance方程的高光反射项,而unity中的高光反射项是这样的
这个公式的来源是Siggraph 2015中的”Optimzing PBR For Moblie”
在unity中这个函数的表达做了一些运算上的优化,我们可以发现它并没有4(roughness+0.5)这个项,而是使用了brdfData.normalizationTerm这个项,而在我们的InitializeBRDFDataDirect()函数中可以窥见这个变量,它的值确实就是roughness*4+2。
在DirectBDRF(没错,确实是BDRF,算是彩蛋吗?)函数里unity选择用brdfData的diffuse项和高光项乘过一个金属和albedo的插值相加来获得brdf值输出,而这个diffuse项呢?我们也在IntializeBRDFDataDirect()函数里去看看,可以发现它在金属流下的公式是half3 brdfDiffuse = albedo * oneMinusReflectivity;
代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 // Initialize BRDFData for material, managing both specular and metallic setup using shader keyword _SPECULAR_SETUP. inline void InitializeBRDFData(half3 albedo, half metallic, half3 specular, half smoothness, inout half alpha, out BRDFData outBRDFData) { #ifdef _SPECULAR_SETUP half reflectivity = ReflectivitySpecular(specular); half oneMinusReflectivity = half(1.0) - reflectivity; half3 brdfDiffuse = albedo * (half3(1.0, 1.0, 1.0) - specular); half3 brdfSpecular = specular; #else half oneMinusReflectivity = OneMinusReflectivityMetallic(metallic); half reflectivity = half(1.0) - oneMinusReflectivity; half3 brdfDiffuse = albedo * oneMinusReflectivity; half3 brdfSpecular = lerp(kDieletricSpec.rgb, albedo, metallic); #endif InitializeBRDFDataDirect(albedo, brdfDiffuse, brdfSpecular, reflectivity, oneMinusReflectivity, smoothness, alpha, outBRDFData); }
我们知道kd项是公式是(1-F0)(1-Metallic),而这个oneMinusReflectivity是根据另一个函数算出。
代码 1 2 3 4 5 6 7 8 9 10 11 12 half OneMinusReflectivityMetallic(half metallic) { // We'll need oneMinusReflectivity, so // 1-reflectivity = 1-lerp(dielectricSpec, 1, metallic) = lerp(1-dielectricSpec, 0, metallic) // store (1-dielectricSpec) in kDielectricSpec.a, then // 1-reflectivity = lerp(alpha, 0, metallic) = alpha + metallic*(0 - alpha) = // = alpha - metallic * alpha half oneMinusDielectricSpec = kDielectricSpec.a; return oneMinusDielectricSpec - metallic * oneMinusDielectricSpec; }
kDielectriSpec.a就是(1-F0)这个项,很容易得知oneMinusReflectivity这个项就是kd项,那么问题来了,式子里的漫反射项应该是还要除以π的,但是unity里却没有除以π,这似乎是一个经常被群友提及的话题,个人认为这是unity觉得没必要除以这个π也能获得不错的效果,unity说不用那就不用吧……
看完这些好像对于BRDF.hlsl有个大概的理解了,但如果要追求更深的理解,还需要去继续往下看剩下的几个core里的头文件干了什么。
BSDF.hlsl 这位更是重量级,BSDF.hlsl可以说是一个关于反射方程的参考库,unity在每个算法中都为我们注释了论文来源,可以用来参考和学习。
文件里包含各种各样的Fresnel、GGX、Irediscence、Fabric材质和flowmap位移切线的kajiya头发渲染。
CommonMaterial.hlsl 这个文件里包含了一些基础材质算法和函数的代码,同样标注了论文来源,在BRDF.hlsl只用了一个转换smoothness为roughness的算法,它的代码是这样的。
代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 real RoughnessToPerceptualSmoothness(real roughness) { return 1.0 - sqrt(roughness); } real PerceptualSmoothnessToRoughness(real perceptualSmoothness) { return (1.0 - perceptualSmoothness) * (1.0 - perceptualSmoothness); } real PerceptualSmoothnessToPerceptualRoughness(real perceptualSmoothness) { return (1.0 - perceptualSmoothness); }
回到Lighting.hlsl 解决了BRDFData的问题(并没),接下来继续回头看LightingPhysicallyBased干了什么。从刚刚对BRDF的分析可以得知,BRDFData这个结构体里存储的都是微表面本身的属性(不包含法线方向),而LightingPhysicallyBased函数里传入的其它数据则是法线方向、光照方向、视角方向、光照衰减、光照颜色等和表面本身无关的属性,分离了数据使整个系统更加成熟。
代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 half NdotL = saturate(dot(normalWS, lightDirectionWS)); half3 radiance = lightColor * (lightAttenuation * NdotL); half3 brdf = brdfData.diffuse; #ifndef _SPECULARHIGHLIGHTS_OFF [branch] if (!specularHighlightsOff) { brdf += brdfData.specular * DirectBRDFSpecular(brdfData, normalWS, lightDirectionWS, viewDirectionWS); #if defined(_CLEARCOAT) || defined(_CLEARCOATMAP) // Clear coat evaluates the specular a second timw and has some common terms with the base specular. // We rely on the compiler to merge these and compute them only once. half brdfCoat = kDielectricSpec.r * DirectBRDFSpecular(brdfDataClearCoat, normalWS, lightDirectionWS, viewDirectionWS); // Mix clear coat and base layer using khronos glTF recommended formula // https://github.com/KhronosGroup/glTF/blob/master/extensions/2.0/Khronos/KHR_materials_clearcoat/README.md // Use NoV for direct too instead of LoH as an optimization (NoV is light invariant). half NoV = saturate(dot(normalWS, viewDirectionWS)); // Use slightly simpler fresnelTerm (Pow4 vs Pow5) as a small optimization. // It is matching fresnel used in the GI/Env, so should produce a consistent clear coat blend (env vs. direct) half coatFresnel = kDielectricSpec.x + kDielectricSpec.a * Pow4(1.0 - NoV); brdf = brdf * (1.0 - clearCoatMask * coatFresnel) + brdfCoat * clearCoatMask; #endif // _CLEARCOAT } #endif // _SPECULARHIGHLIGHTS_OFF
这里出现了一个新的变量radiance,它的组成是光照乘NoL再乘一个衰减,除去这个衰减以外我们可以发现它对应的就是cook-torrance方程里的光照乘cosθ这一项,所以它乘上brdf确实就是光照的输出,而这里的高光运算运用的就是BRDF.hlsl中的DirectBRDFSpecular()函数。
发现就在不远处还有一个F项,define在了_CLEARCOAT里,并且用clearcoatmask值来控制,或者说对brdf用clearCoatMask做了一个brdfCoat到brdf*coatFresnel的插值,而brdfCoat如代码所示,是使用另一个BRDFData来进行DirectBRDFSpecular()函数运算得到的高光值再乘上0.04,我认为这是urp提供的一个金属和非金属混合制品的方便的计算方式(即带有coat宏的物体是作为金属来计算的)。
正体-UniversalFragmentPBR 文件中间是一堆对于光照的处理,之后进入了一个醒目的Fragment Functions板块,其中第一个函数UniversalFragmentPBR就比较值得一看。
代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 half4 UniversalFragmentPBR(InputData inputData, SurfaceData surfaceData) { #if defined(_SPECULARHIGHLIGHTS_OFF) bool specularHighlightsOff = true; #else bool specularHighlightsOff = false; #endif BRDFData brdfData; // NOTE: can modify "surfaceData"... InitializeBRDFData(surfaceData, brdfData); #if defined(DEBUG_DISPLAY) half4 debugColor; if (CanDebugOverrideOutputColor(inputData, surfaceData, brdfData, debugColor)) { return debugColor; } #endif // Clear-coat calculation... BRDFData brdfDataClearCoat = CreateClearCoatBRDFData(surfaceData, brdfData); half4 shadowMask = CalculateShadowMask(inputData); AmbientOcclusionFactor aoFactor = CreateAmbientOcclusionFactor(inputData, surfaceData); uint meshRenderingLayers = GetMeshRenderingLightLayer(); Light mainLight = GetMainLight(inputData, shadowMask, aoFactor); // NOTE: We don't apply AO to the GI here because it's done in the lighting calculation below... MixRealtimeAndBakedGI(mainLight, inputData.normalWS, inputData.bakedGI); LightingData lightingData = CreateLightingData(inputData, surfaceData); lightingData.giColor = GlobalIllumination(brdfData, brdfDataClearCoat, surfaceData.clearCoatMask, inputData.bakedGI, aoFactor.indirectAmbientOcclusion, inputData.positionWS, inputData.normalWS, inputData.viewDirectionWS); if (IsMatchingLightLayer(mainLight.layerMask, meshRenderingLayers)) { lightingData.mainLightColor = LightingPhysicallyBased(brdfData, brdfDataClearCoat, mainLight, inputData.normalWS, inputData.viewDirectionWS, surfaceData.clearCoatMask, specularHighlightsOff); } #if defined(_ADDITIONAL_LIGHTS) uint pixelLightCount = GetAdditionalLightsCount(); #if USE_CLUSTERED_LIGHTING for (uint lightIndex = 0; lightIndex < min(_AdditionalLightsDirectionalCount, MAX_VISIBLE_LIGHTS); lightIndex++) { Light light = GetAdditionalLight(lightIndex, inputData, shadowMask, aoFactor); if (IsMatchingLightLayer(light.layerMask, meshRenderingLayers)) { lightingData.additionalLightsColor += LightingPhysicallyBased(brdfData, brdfDataClearCoat, light, inputData.normalWS, inputData.viewDirectionWS, surfaceData.clearCoatMask, specularHighlightsOff); } } #endif LIGHT_LOOP_BEGIN(pixelLightCount) Light light = GetAdditionalLight(lightIndex, inputData, shadowMask, aoFactor); if (IsMatchingLightLayer(light.layerMask, meshRenderingLayers)) { lightingData.additionalLightsColor += LightingPhysicallyBased(brdfData, brdfDataClearCoat, light, inputData.normalWS, inputData.viewDirectionWS, surfaceData.clearCoatMask, specularHighlightsOff); } LIGHT_LOOP_END #endif #if defined(_ADDITIONAL_LIGHTS_VERTEX) lightingData.vertexLightingColor += inputData.vertexLighting * brdfData.diffuse; #endif return CalculateFinalColor(lightingData, surfaceData.alpha); }
这个函数做了很多事情,或者说可能是全部事情,它根据输入的surfaceData创建了brdfData,然后又根据这个brdfData来创建了clearCoat,从此我们可以推出pbr使用的材质大抵上用这种方式计算了额外的F项完成了其它的效果,然后它计算了shadowMask、renderingLayer和GI,根据inputData和surfaceData创建了lightingData,然后遍历所有的光源进行光照计算(基于之前的LightingPhysicallyBased),输出最后的颜色。
这个函数又带来了新的疑问:1.inputData是什么,很容易猜到,这个结构体大概包含了除平面本身信息以外的其它信息,那么为什么靠它可以算出lightingData呢?2.shadowmask怎么计算,又为什么要在这里进行计算?3.GI怎么计算?4.urp的内置shader如何去调用这个函数完成渲染流程?
我急了我急了,赶紧把剩下看完,哦,是关于phong、blinnphong和unlit的,跳过吧。
先解决第一个问题,InputData是什么?这个结构体在DBuffer.hlsl所引用的Input.hlsl结构体中。
代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 struct InputData { float3 positionWS; float4 positionCS; half3 normalWS; half3 viewDirectionWS; float4 shadowCoord; half fogCoord; half3 vertexLighting; half3 bakedGI; float2 normalizedScreenSpaceUV; half4 shadowMask; half3x3 tangentToWorld; #if defined(DEBUG_DISPLAY) half2 dynamicLightmapUV; half2 staticLightmapUV; float3 vertexSH; half3 brdfDiffuse; half3 brdfSpecular; float2 uv; uint mipCount; // texelSize : // x = 1 / width // y = 1 / height // z = width // w = height float4 texelSize; // mipInfo : // x = quality settings minStreamingMipLevel // y = original mip count for texture // z = desired on screen mip level // w = loaded mip level float4 mipInfo; #endif };
这个结构体里包含了基础的positionWS、positionCS、normalWS、viewDirection,还包含了shadowCoord、fogCoord、bakedGI等等各种表面光照的函数,同时一些全局变量也是在这个文件里声明的,算是一个万用的匹配工具。
告一段落 shadowmap和gi的事情之后再去补充,先看到第二个问题:urp的shader怎么使用这个函数。
Lit.shader 研究过Lit.shader的人都知道,除开properties里的一大段东西和后面的一堆诸如shadowcaster和depthnormals之类的pass,这个shader的光照都是在”ForwardLit”这个pass里进行的。他的函数LitPassVertex和LitPassFragment在LitInput.hlsl和LitForwardPass.hlsl里,我们来看看这些文件。
这个两个文件其实是一个文件,也就是我们常写的shader结构,在LitInput里声明了SRP Batcher和GPU Instancing的变量,并区分Metallic和Specular工作流,然后声明了一些采样用的函数,可以看作整个LitInput.hlsl文件是为LitForwardPass.hlsl作准备,并且做了一些比较普遍的工作。
代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 // NOTE: Do not ifdef the properties here as SRP batcher can not handle different layouts. CBUFFER_START(UnityPerMaterial) float4 _BaseMap_ST; float4 _DetailAlbedoMap_ST; half4 _BaseColor; half4 _SpecColor; half4 _EmissionColor; half _Cutoff; half _Smoothness; half _Metallic; half _BumpScale; half _Parallax; half _OcclusionStrength; half _ClearCoatMask; half _ClearCoatSmoothness; half _DetailAlbedoMapScale; half _DetailNormalMapScale; half _Surface; CBUFFER_END // NOTE: Do not ifdef the properties for dots instancing, but ifdef the actual usage. // Otherwise you might break CPU-side as property constant-buffer offsets change per variant. // NOTE: Dots instancing is orthogonal to the constant buffer above. #ifdef UNITY_DOTS_INSTANCING_ENABLED UNITY_DOTS_INSTANCING_START(MaterialPropertyMetadata) UNITY_DOTS_INSTANCED_PROP(float4, _BaseColor) UNITY_DOTS_INSTANCED_PROP(float4, _SpecColor) UNITY_DOTS_INSTANCED_PROP(float4, _EmissionColor) UNITY_DOTS_INSTANCED_PROP(float , _Cutoff) UNITY_DOTS_INSTANCED_PROP(float , _Smoothness) UNITY_DOTS_INSTANCED_PROP(float , _Metallic) UNITY_DOTS_INSTANCED_PROP(float , _BumpScale) UNITY_DOTS_INSTANCED_PROP(float , _Parallax) UNITY_DOTS_INSTANCED_PROP(float , _OcclusionStrength) UNITY_DOTS_INSTANCED_PROP(float , _ClearCoatMask) UNITY_DOTS_INSTANCED_PROP(float , _ClearCoatSmoothness) UNITY_DOTS_INSTANCED_PROP(float , _DetailAlbedoMapScale) UNITY_DOTS_INSTANCED_PROP(float , _DetailNormalMapScale) UNITY_DOTS_INSTANCED_PROP(float , _Surface) UNITY_DOTS_INSTANCING_END(MaterialPropertyMetadata) #define _BaseColor UNITY_ACCESS_DOTS_INSTANCED_PROP_FROM_MACRO(float4 , Metadata_BaseColor) #define _SpecColor UNITY_ACCESS_DOTS_INSTANCED_PROP_FROM_MACRO(float4 , Metadata_SpecColor) #define _EmissionColor UNITY_ACCESS_DOTS_INSTANCED_PROP_FROM_MACRO(float4 , Metadata_EmissionColor) #define _Cutoff UNITY_ACCESS_DOTS_INSTANCED_PROP_FROM_MACRO(float , Metadata_Cutoff) #define _Smoothness UNITY_ACCESS_DOTS_INSTANCED_PROP_FROM_MACRO(float , Metadata_Smoothness) #define _Metallic UNITY_ACCESS_DOTS_INSTANCED_PROP_FROM_MACRO(float , Metadata_Metallic) #define _BumpScale UNITY_ACCESS_DOTS_INSTANCED_PROP_FROM_MACRO(float , Metadata_BumpScale) #define _Parallax UNITY_ACCESS_DOTS_INSTANCED_PROP_FROM_MACRO(float , Metadata_Parallax) #define _OcclusionStrength UNITY_ACCESS_DOTS_INSTANCED_PROP_FROM_MACRO(float , Metadata_OcclusionStrength) #define _ClearCoatMask UNITY_ACCESS_DOTS_INSTANCED_PROP_FROM_MACRO(float , Metadata_ClearCoatMask) #define _ClearCoatSmoothness UNITY_ACCESS_DOTS_INSTANCED_PROP_FROM_MACRO(float , Metadata_ClearCoatSmoothness) #define _DetailAlbedoMapScale UNITY_ACCESS_DOTS_INSTANCED_PROP_FROM_MACRO(float , Metadata_DetailAlbedoMapScale) #define _DetailNormalMapScale UNITY_ACCESS_DOTS_INSTANCED_PROP_FROM_MACRO(float , Metadata_DetailNormalMapScale) #define _Surface UNITY_ACCESS_DOTS_INSTANCED_PROP_FROM_MACRO(float , Metadata_Surface) #endif TEXTURE2D(_ParallaxMap); SAMPLER(sampler_ParallaxMap); TEXTURE2D(_OcclusionMap); SAMPLER(sampler_OcclusionMap); TEXTURE2D(_DetailMask); SAMPLER(sampler_DetailMask); TEXTURE2D(_DetailAlbedoMap); SAMPLER(sampler_DetailAlbedoMap); TEXTURE2D(_DetailNormalMap); SAMPLER(sampler_DetailNormalMap); TEXTURE2D(_MetallicGlossMap); SAMPLER(sampler_MetallicGlossMap); TEXTURE2D(_SpecGlossMap); SAMPLER(sampler_SpecGlossMap); TEXTURE2D(_ClearCoatMap); SAMPLER(sampler_ClearCoatMap); half4 SampleMetallicSpecGloss(float2 uv, half albedoAlpha) ... half SampleOcclusion(float2 uv) ... half2 SampleClearCoat(float2 uv) ... void ApplyPerPixelDisplacement(half3 viewDirTS, inout float2 uv) ... half3 ScaleDetailAlbedo(half3 detailAlbedo, half scale) ... half3 ApplyDetailAlbedo(float2 detailUv, half3 albedo, half detailMask) ... half3 ApplyDetailNormal(float2 detailUv, half3 normalTS, half detailMask) ... inline void InitializeStandardLitSurfaceData(float2 uv, out SurfaceData outSurfaceData) ...
而LitPassForward里声明了shader里用到的Attributes和Varyings结构体,用来读入模型的顶点信息、处理几何阶段问题。
代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 struct Attributes { float4 positionOS : POSITION; float3 normalOS : NORMAL; float4 tangentOS : TANGENT; float2 texcoord : TEXCOORD0; float2 staticLightmapUV : TEXCOORD1; float2 dynamicLightmapUV : TEXCOORD2; UNITY_VERTEX_INPUT_INSTANCE_ID }; struct Varyings { float2 uv : TEXCOORD0; #if defined(REQUIRES_WORLD_SPACE_POS_INTERPOLATOR) float3 positionWS : TEXCOORD1; #endif float3 normalWS : TEXCOORD2; #if defined(REQUIRES_WORLD_SPACE_TANGENT_INTERPOLATOR) half4 tangentWS : TEXCOORD3; // xyz: tangent, w: sign #endif float3 viewDirWS : TEXCOORD4; #ifdef _ADDITIONAL_LIGHTS_VERTEX half4 fogFactorAndVertexLight : TEXCOORD5; // x: fogFactor, yzw: vertex light #else half fogFactor : TEXCOORD5; #endif #if defined(REQUIRES_VERTEX_SHADOW_COORD_INTERPOLATOR) float4 shadowCoord : TEXCOORD6; #endif #if defined(REQUIRES_TANGENT_SPACE_VIEW_DIR_INTERPOLATOR) half3 viewDirTS : TEXCOORD7; #endif DECLARE_LIGHTMAP_OR_SH(staticLightmapUV, vertexSH, 8); #ifdef DYNAMICLIGHTMAP_ON float2 dynamicLightmapUV : TEXCOORD9; // Dynamic lightmap UVs #endif float4 positionCS : SV_POSITION; UNITY_VERTEX_INPUT_INSTANCE_ID UNITY_VERTEX_OUTPUT_STEREO };
在Varings里,代码中不断用define来判定是否需要声明变量来使得性能达到最优。
后面的InitializeInputData()函数通过Varings、法线贴图的采样值和一些全局变量直接输出了InputData。这个InputData在之前已经有介绍,有了InputData几乎就可以直接在Lighting.hlsl函数里计算光照结果了。
顶点着色器的函数使用了Unity的GetVertexXXXInputs()函数,可以方便地输出一个属性在各个空间的值而不需要多行的矩阵乘法,是一个节省代码量的写法,不过我个人还是不太喜欢用。顶点着色器里还有一些其他的变量处理,在全局光照的时候再分析吧。
代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 // Used in Standard (Physically Based) shader Varyings LitPassVertex(Attributes input) { Varyings output = (Varyings)0; UNITY_SETUP_INSTANCE_ID(input); UNITY_TRANSFER_INSTANCE_ID(input, output); UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(output); VertexPositionInputs vertexInput = GetVertexPositionInputs(input.positionOS.xyz); // normalWS and tangentWS already normalize. // this is required to avoid skewing the direction during interpolation // also required for per-vertex lighting and SH evaluation VertexNormalInputs normalInput = GetVertexNormalInputs(input.normalOS, input.tangentOS); half3 vertexLight = VertexLighting(vertexInput.positionWS, normalInput.normalWS); half fogFactor = 0; #if !defined(_FOG_FRAGMENT) fogFactor = ComputeFogFactor(vertexInput.positionCS.z); #endif output.uv = TRANSFORM_TEX(input.texcoord, _BaseMap); // already normalized from normal transform to WS. output.normalWS = normalInput.normalWS; #if defined(REQUIRES_WORLD_SPACE_TANGENT_INTERPOLATOR) || defined(REQUIRES_TANGENT_SPACE_VIEW_DIR_INTERPOLATOR) real sign = input.tangentOS.w * GetOddNegativeScale(); half4 tangentWS = half4(normalInput.tangentWS.xyz, sign); #endif #if defined(REQUIRES_WORLD_SPACE_TANGENT_INTERPOLATOR) output.tangentWS = tangentWS; #endif #if defined(REQUIRES_TANGENT_SPACE_VIEW_DIR_INTERPOLATOR) half3 viewDirWS = GetWorldSpaceNormalizeViewDir(vertexInput.positionWS); half3 viewDirTS = GetViewDirectionTangentSpace(tangentWS, output.normalWS, viewDirWS); output.viewDirTS = viewDirTS; #endif OUTPUT_LIGHTMAP_UV(input.staticLightmapUV, unity_LightmapST, output.staticLightmapUV); #ifdef DYNAMICLIGHTMAP_ON output.dynamicLightmapUV = input.dynamicLightmapUV.xy * unity_DynamicLightmapST.xy + unity_DynamicLightmapST.zw; #endif OUTPUT_SH(output.normalWS.xyz, output.vertexSH); #ifdef _ADDITIONAL_LIGHTS_VERTEX output.fogFactorAndVertexLight = half4(fogFactor, vertexLight); #else output.fogFactor = fogFactor; #endif #if defined(REQUIRES_WORLD_SPACE_POS_INTERPOLATOR) output.positionWS = vertexInput.positionWS; #endif #if defined(REQUIRES_VERTEX_SHADOW_COORD_INTERPOLATOR) output.shadowCoord = GetShadowCoord(vertexInput); #endif output.positionCS = vertexInput.positionCS; return output; }
片元着色器就显得简单一点,首先提前跑一下位移贴图的变换,然后直接初始化InputData,接下来就用Lighting.hlsl中的UniversalFragmentPBR()函数输出颜色。
代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 // Used in Standard (Physically Based) shader half4 LitPassFragment(Varyings input) : SV_Target { UNITY_SETUP_INSTANCE_ID(input); UNITY_SETUP_STEREO_EYE_INDEX_POST_VERTEX(input); #if defined(_PARALLAXMAP) #if defined(REQUIRES_TANGENT_SPACE_VIEW_DIR_INTERPOLATOR) half3 viewDirTS = input.viewDirTS; #else half3 viewDirWS = GetWorldSpaceNormalizeViewDir(input.positionWS); half3 viewDirTS = GetViewDirectionTangentSpace(input.tangentWS, input.normalWS, viewDirWS); #endif ApplyPerPixelDisplacement(viewDirTS, input.uv); #endif SurfaceData surfaceData; InitializeStandardLitSurfaceData(input.uv, surfaceData); InputData inputData; InitializeInputData(input, surfaceData.normalTS, inputData); SETUP_DEBUG_TEXTURE_DATA(inputData, input.uv, _BaseMap); #ifdef _DBUFFER ApplyDecalToSurfaceData(input.positionCS, surfaceData, inputData); #endif half4 color = UniversalFragmentPBR(inputData, surfaceData); color.rgb = MixFog(color.rgb, inputData.fogCoord); color.a = OutputAlpha(color.a, _Surface); return color; }
总结 明明全局光照、阴影mask以及分开的clearCoat还没有分析,怎么就总结了。
因为今天已经结束了,该下班了,明天继续。
前半部分其实已经对Unity默认shader光照结构有一定的认识了。总结一下就是urp采用了现在很多人都喜欢使用的一种方式(确实是很容易管理的一种方式)——将.shader和其使用的着色器函数分开在不同的文件里,前者只进行properties的声明以及pass的声明和调用,而后者在urp中又分为两块——properties中input变量的处理一块,attributes、varings以及顶点着色器和片元着色器函数一块,这样的方式并没有看见很多人在使用,但个人觉得非常不错,因为在不同的shader间迭代的时候我们往往会碰见这样的情况:因为shader需要实现的功能需求不同导致它需要不同的变量参数,但是着色器的算法却大同小异,将两者分离开来不仅有助于提高效率,也能减少写代码中犯错的概率。
这是urp中shader的结构,再整理一下它的文件结构:Lighting.hlsl作为一个光照计算的总入口,它的底层指向了BRDF.hlsl等库,而BRDF.hlsl作为实现PBR光照方程的库,本身包含了DirectBRDFSpecular()和EnvironmentBRDF()两个重要函数,它的底层是BSDF.hlsl、CommonMaterial.hlsl等库,前者包含了很多光照的方程,而后者包含了一些常用的PBR材质的属性的变换函数。Input.hlsl用于传递片元的各种各样的参数,而光源的参数则在Lighting.hlsl中的UniversalFragmentPBR()中遍历直接光、采样间接光获取,并放入同文件的LightingPhysicallyBased()中运算,这个函数又调用了BRDF.hlsl中的的DirectBRDFSpecular()和EnvironmentBRDF()中进行运算。
unity他把这个方程拆成了多个部分,我们可以看出D项和Vis项是在BRDF.hlsl中计算的,而L*NoL和F项的部分又是在Lighting.hlsl中计算的。
按照CatlikeCoding的说法,urp中使用的BRDF方程是一个修改过的轻量级的方程,如果自己写过BRDF方程代码的朋友们也会发现urp中的默认材质和按照常见的BRDF方程写出来的直接光照部分并没有太大的区别,但是间接光照部分会有不小的区别(毕竟Lighting.hlsl中考虑到的东西比较全面)
那么次回再探讨全局光照、阴影、雾效部分。