学实时渲染不了解全局光照,就像看四大名著不看红楼梦……
这次笔记没什么美术含量,算是一个代码解析,如果有错误的地方欢迎勘误。
UnityURPshader光照部分学习分析(中)-全局光照和阴影雾效 GI部分 在理解这些之前希望大伙先去看看CatlikeCoding的SRP部分的Baked Light这一章,非常有助于理解URP的全局光照。
EnvironmentSpecular 大部分的GI代码在Lighting.hlsl里,但是我们发现BRDF.hlsl里也有几个关于环境光的函数,
代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 // Computes the specular term for EnvironmentBRDF 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; } // Environment BRDF without diffuse for clear coat half3 EnvironmentBRDFClearCoat(BRDFData brdfData, half clearCoatMask, half3 indirectSpecular, half fresnelTerm) { float surfaceReduction = 1.0 / (brdfData.roughness2 + 1.0); return indirectSpecular * EnvironmentBRDFSpecular(brdfData, fresnelTerm) * clearCoatMask; }
代码看着还是不如公式方便,我们把公式写下来。
首先,在BRDFData中specular是直接输入的数值,grazingTerm是saturate(smoothness+reflectivity),而reflectivity也是直接输入的物理量。
大写字母都是brdfData中的参数或者函数参数,S是specular,G是grazingTerm,D是diffuse,而F是输入的菲涅尔项,之后的公式都会以这种方式表示。
而这个函数调用的位置是在GlobalIllumination.hlsl文件里,接下来开始看看这个文件。
GlobalIllumination.hlsl 先不看这个文件本身,而是去看它引用的三个文件:EntityLighting.hlsl、ImageBasedLighting.hlsl、ReatimeLights.hlsl,光看名字就知道是非常有用(难啃)的文件。
EntityLighting.hlsl 看过或者敲过CatlikeCoding教程的全局光照部分的朋友应该比较熟悉这个文件,里面放的是采样烘焙贴图Lightmap以及球谐函数采样光照探针的函数方法,有很多变体,包括HDR、DoubleLDR、环境吸收的采样,但是如果看过catlike的话这些都比较熟悉了,这里就不深入讨论下去。
ImageBasedLighting.hlsl 正如其名,这个文件是用来处理IBL的,urp有一套自己的IBL方法,文件里有一些转换粗糙度的函数,其他函数大部分可以找到引用的原文,我自己也没完全看完,就不记录了,怕误导到别人。
ReatimeLights.hlsl 这个文件里装的都是一些大家比较熟悉的函数,诸如GetMainLight()、GetAdditionalLight()、各种attenuation之类的,主要是提供一些访问实时光源的接口。
回到Globalillumination.hlsl 这个文件的大部分函数所使用的函数都在前面三个文件里了,没有的也都是一些有印象的词,比如unity_specCube0这样的全局变量。
代码 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 half3 SubtractDirectMainLightFromLightmap(Light mainLight, half3 normalWS, half3 bakedGI) { // Let's try to make realtime shadows work on a surface, which already contains // baked lighting and shadowing from the main sun light. // Summary: // 1) Calculate possible value in the shadow by subtracting estimated light contribution from the places occluded by realtime shadow: // a) preserves other baked lights and light bounces // b) eliminates shadows on the geometry facing away from the light // 2) Clamp against user defined ShadowColor. // 3) Pick original lightmap value, if it is the darkest one. // 1) Gives good estimate of illumination as if light would've been shadowed during the bake. // We only subtract the main direction light. This is accounted in the contribution term below. half shadowStrength = GetMainLightShadowStrength(); half contributionTerm = saturate(dot(mainLight.direction, normalWS)); half3 lambert = mainLight.color * contributionTerm; half3 estimatedLightContributionMaskedByInverseOfShadow = lambert * (1.0 - mainLight.shadowAttenuation); half3 subtractedLightmap = bakedGI - estimatedLightContributionMaskedByInverseOfShadow; // 2) Allows user to define overall ambient of the scene and control situation when realtime shadow becomes too dark. half3 realtimeShadow = max(subtractedLightmap, _SubtractiveShadowColor.xyz); realtimeShadow = lerp(bakedGI, realtimeShadow, shadowStrength); // 3) Pick darkest color return min(bakedGI, realtimeShadow); } ... half3 GlobalIllumination(BRDFData brdfData, half3 bakedGI, half occlusion, half3 normalWS, half3 viewDirectionWS) { const BRDFData noClearCoat = (BRDFData)0; return GlobalIllumination(brdfData, noClearCoat, 0.0, bakedGI, occlusion, normalWS, viewDirectionWS); } void MixRealtimeAndBakedGI(inout Light light, half3 normalWS, inout half3 bakedGI) { #if defined(LIGHTMAP_ON) && defined(_MIXED_LIGHTING_SUBTRACTIVE) bakedGI = SubtractDirectMainLightFromLightmap(light, normalWS, bakedGI); #endif } // Backwards compatibility void MixRealtimeAndBakedGI(inout Light light, half3 normalWS, inout half3 bakedGI, half4 shadowMask) { MixRealtimeAndBakedGI(light, normalWS, bakedGI); } void MixRealtimeAndBakedGI(inout Light light, half3 normalWS, inout half3 bakedGI, AmbientOcclusionFactor aoFactor) { if (IsLightingFeatureEnabled(DEBUGLIGHTINGFEATUREFLAGS_AMBIENT_OCCLUSION)) { bakedGI *= aoFactor.indirectAmbientOcclusion; } MixRealtimeAndBakedGI(light, normalWS, bakedGI); }
MixRealtimeAndBakeGI()这段函数做了什么,在SubtractDirectMainLightFromLightmap()的注释里其实说清楚了,简单来讲它是用来处理全局光照下的实时阴影的,作用是从lightmap中减掉直接光的影响。
代码 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 half3 GlobalIllumination(BRDFData brdfData, BRDFData brdfDataClearCoat, float clearCoatMask, half3 bakedGI, half occlusion, float3 positionWS, half3 normalWS, half3 viewDirectionWS) { half3 reflectVector = reflect(-viewDirectionWS, normalWS); half NoV = saturate(dot(normalWS, viewDirectionWS)); half fresnelTerm = Pow4(1.0 - NoV); half3 indirectDiffuse = bakedGI; half3 indirectSpecular = GlossyEnvironmentReflection(reflectVector, positionWS, brdfData.perceptualRoughness, 1.0h); half3 color = EnvironmentBRDF(brdfData, indirectDiffuse, indirectSpecular, fresnelTerm); if (IsOnlyAOLightingFeatureEnabled()) { color = half3(1,1,1); // "Base white" for AO debug lighting mode } #if defined(_CLEARCOAT) || defined(_CLEARCOATMAP) half3 coatIndirectSpecular = GlossyEnvironmentReflection(reflectVector, positionWS, brdfDataClearCoat.perceptualRoughness, 1.0h); // TODO: "grazing term" causes problems on full roughness half3 coatColor = EnvironmentBRDFClearCoat(brdfDataClearCoat, clearCoatMask, coatIndirectSpecular, fresnelTerm); // Blend with base layer using khronos glTF recommended way using NoV // Smooth surface & "ambiguous" lighting // NOTE: fresnelTerm (above) is pow4 instead of pow5, but should be ok as blend weight. half coatFresnel = kDielectricSpec.x + kDielectricSpec.a * fresnelTerm; return (color * (1.0 - coatFresnel * clearCoatMask) + coatColor) * occlusion; #else return color * occlusion; #endif }
而GlobalIllumination()这个函数里做的才是我们一般情况下做的环境反射,看到这里我比较疑惑,因为这个函数里并不包含上面的SampleSH()系列的函数,而是只去采样了环境贴图和原来BRDF.hlsl里面的EnvironmentBRDF(),我的想法是或许在Unity的全局光照思路里,环境反射、球谐光照和lightmap都是分开的?回头去看一看litshader或许能够解答这个问题。
回到UniversalFragmentPBR 代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 // 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);
直接看到这一段,它创建了LightingData,调用了我们上文提到的MixRealtimeAndBakedGI()和GlobalIllumination(),而球谐的调用在InitializeInputData()里。
代码 1 2 3 4 5 6 7 #if defined(DYNAMICLIGHTMAP_ON) inputData.bakedGI = SAMPLE_GI(input.staticLightmapUV, input.dynamicLightmapUV, input.vertexSH, inputData.normalWS); #else inputData.bakedGI = SAMPLE_GI(input.staticLightmapUV, input.vertexSH, inputData.normalWS); #endif
这里的SAMPLE_GI正是GlobalIllumination.hlsl中的一个宏,根据不同的宏设置来选择采样的全局光照,当DYNAMICLIGHTMAP_ON和LIGHTMAP_ON都被关闭的时候会去采样球谐光照。
代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 // We either sample GI from baked lightmap or from probes. // If lightmap: sampleData.xy = lightmapUV // If probe: sampleData.xyz = L2 SH terms #if defined(LIGHTMAP_ON) && defined(DYNAMICLIGHTMAP_ON) #define SAMPLE_GI(staticLmName, dynamicLmName, shName, normalWSName) SampleLightmap(staticLmName, dynamicLmName, normalWSName) #elif defined(DYNAMICLIGHTMAP_ON) #define SAMPLE_GI(staticLmName, dynamicLmName, shName, normalWSName) SampleLightmap(0, dynamicLmName, normalWSName) #elif defined(LIGHTMAP_ON) #define SAMPLE_GI(staticLmName, shName, normalWSName) SampleLightmap(staticLmName, 0, normalWSName) #else #define SAMPLE_GI(staticLmName, shName, normalWSName) SampleSHPixel(shName, normalWSName) #endif
阶段总结-GI 到这里总算是把全局光照的所有接口顺序捋清楚了。简单来讲,unity的光照分为三个部分:直接光照、间接光照、环境反射,直接光照在上回中捋过了,间接光照是通过GlobalIllumination.hlsl中的宏去敲定的,底层在EntityLighting.hlsl中,要么是采样LightMap要么是采样光照探针(不理解这里的可以去看CatlikeCoding全局光照那章),而环境反射是在同一个文件里面直接进行的。
在LitForwardPass中直接光照和间接光照的参数是在InitializeStandardLitSurfaceData()和InitializeInputData()的时候就已经初始化好的,后者直接把间接光照算到InputData的bakedGI里了(个人理解上间接光照的计算并不属于BRDF的范畴所以没有在之后放到BRDFData里一起去算),然后直接把所有数据放到UniversalFragmentPBR里得到颜色,这个过程在某个步骤里计算了shadow和fog。
阴影和雾 上回说到在某个步骤里URP计算了shadow和fog对颜色造成的影响,那么是在哪里呢?按照上面的思路顺藤摸瓜就能发现,很显然也是在InitializeInputData()这个函数里进行的,就在计算GI项的上面。
代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 #if defined(REQUIRES_VERTEX_SHADOW_COORD_INTERPOLATOR) inputData.shadowCoord = input.shadowCoord; #elif defined(MAIN_LIGHT_CALCULATE_SHADOWS) inputData.shadowCoord = TransformWorldToShadowCoord(inputData.positionWS); #else inputData.shadowCoord = float4(0, 0, 0, 0); #endif #ifdef _ADDITIONAL_LIGHTS_VERTEX inputData.fogCoord = InitializeInputDataFog(float4(input.positionWS, 1.0), input.fogFactorAndVertexLight.x); inputData.vertexLighting = input.fogFactorAndVertexLight.yzw; #else inputData.fogCoord = InitializeInputDataFog(float4(input.positionWS, 1.0), input.fogFactor); #endif
在这个函数里unity处理出了inputData中shadowCoord的值和fogCoord的值,那么这些值是在哪里进行运算的呢?
在RealtimeLights.hlsl里,实际上这个哈函数在我们平时自己写shader的时候也会用到:
代码 1 2 3 4 5 6 7 8 Light GetMainLight(float4 shadowCoord) { Light light = GetMainLight(); light.shadowAttenuation = MainLightRealtimeShadow(shadowCoord); return light; }
而其中的MainLightRealtimeShadow(shadowCoord)函数在Shadow.hlsl文件中,这个函数就是Sample了一下光源的Shadowmap,因为阴影的具体原理不是这次学习的重点,就略过了。
而雾效则是在LitForwardPass.hlsl里依靠一个叫MixFog()的函数进行实现的(实际上Lighting.hlsl里的CalculateFinalColor函数也调用了这个函数,但是在着色器里是分开在两块里的,估计是为了自由度吧)
而MixFog()这个函数在ShaderVariablesFunctions.hlsl里,InitializeInputDataFog()也在里面,用来计算fogcoord。
代码 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 float ComputeFogIntensity(float fogFactor) { float fogIntensity = 0.0; #if defined(FOG_LINEAR) || defined(FOG_EXP) || defined(FOG_EXP2) #if defined(FOG_EXP) // factor = exp(-density*z) // fogFactor = density*z compute at vertex fogIntensity = saturate(exp2(-fogFactor)); #elif defined(FOG_EXP2) // factor = exp(-(density*z)^2) // fogFactor = density*z compute at vertex fogIntensity = saturate(exp2(-fogFactor * fogFactor)); #elif defined(FOG_LINEAR) fogIntensity = fogFactor; #endif #endif return fogIntensity; } half3 MixFogColor(half3 fragColor, half3 fogColor, half fogFactor) { #if defined(FOG_LINEAR) || defined(FOG_EXP) || defined(FOG_EXP2) half fogIntensity = ComputeFogIntensity(fogFactor); fragColor = lerp(fogColor, fragColor, fogIntensity); #endif return fragColor; } float3 MixFogColor(float3 fragColor, float3 fogColor, float fogFactor) { #if defined(FOG_LINEAR) || defined(FOG_EXP) || defined(FOG_EXP2) if (IsFogEnabled()) { float fogIntensity = ComputeFogIntensity(fogFactor); fragColor = lerp(fogColor, fragColor, fogIntensity); } #endif return fragColor; } half3 MixFog(half3 fragColor, half fogFactor) { return MixFogColor(fragColor, unity_FogColor.rgb, fogFactor); } float3 MixFog(float3 fragColor, float fogFactor) { return MixFogColor(fragColor, unity_FogColor.rgb, fogFactor); }
函数非常简单,就是根据输入的雾的density和color插值算一下,然后再和片元原本的颜色插值算一下,就得到了输出,那么雾的density又是在哪得到的?同样在ShaderVariablesFunctions.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 real ComputeFogFactorZ0ToFar(float z) { #if defined(FOG_LINEAR) // factor = (end-z)/(end-start) = z * (-1/(end-start)) + (end/(end-start)) float fogFactor = saturate(z * unity_FogParams.z + unity_FogParams.w); return real(fogFactor); #elif defined(FOG_EXP) || defined(FOG_EXP2) // factor = exp(-(density*z)^2) // -density * z computed at vertex return real(unity_FogParams.x * z); #else return real(0.0); #endif } real ComputeFogFactor(float zPositionCS) { float clipZ_0Far = UNITY_Z_0_FAR_FROM_CLIPSPACE(zPositionCS); return ComputeFogFactorZ0ToFar(clipZ_0Far); } ... // Force enable fog fragment shader evaluation #define _FOG_FRAGMENT 1 real InitializeInputDataFog(float4 positionWS, real vertFogFactor) { real fogFactor = 0.0; #if defined(_FOG_FRAGMENT) #if (defined(FOG_LINEAR) || defined(FOG_EXP) || defined(FOG_EXP2)) // Compiler eliminates unused math --> matrix.column_z * vec float viewZ = -(mul(UNITY_MATRIX_V, positionWS).z); // View Z is 0 at camera pos, remap 0 to near plane. float nearToFarZ = max(viewZ - _ProjectionParams.y, 0); fogFactor = ComputeFogFactorZ0ToFar(nearToFarZ); #endif #else fogFactor = vertFogFactor; #endif return fogFactor; }
而这个函数是在片元着色器里的InitializeInputData()函数中调用的,走到底层就可以发现fogFactor是通过一个全局变量unity_FogParams来算得的。
PS:这我真不知道怎么理解这种结构了,unity这么写自己不会晕吗……
小结 那么到现在为止,阴影和雾效的结构基本也清楚了,它们大多是在片元着色器中的InitializeInputData()函数中进行初始化到InputData中,然后在Lighiting.hlsl中的函数中进行计算,阴影的部分被并入了光源类型,而雾效则在片元着色器里进行处理,一切好像都连起来了。
看了unity写的这么一大块代码,自然而然地就浮现出一个疑问:我看这些干啥?我能如何去使用这些代码来提高自己的效率。
那么我们就需要知道unity到底把光照这件事情分为了多少个部分,让我们从头开始再理清一遍到目前为止的整个过程。
整个过程还是很清晰的,每个步骤都单独属于一个模块,我们要写代码的话可以根据这个模块直接进行拆解,实际上在写风格化shader的时候,我们往往不需要改变阴影采样(最多可能会改变一下阴影的外观?)、GI、雾效这些值,大部分需要改的都在LightingPhysicallyBased里,所以我们只需要把UniversalFragmentPBR改成我们需要的函数,其他的地方完全可以直接照搬。
根据需求可以灵活地去调节这个配置,利用unity为我们提供的代码让自己的shader事半功倍。
中篇后话 那么接下来还有一个clearCoat,其实一路看下来clearCoat是个什么东西已经非常明显了,它是urp为我们提供的一个双层材质效果,在原有的材质基础上附加了一层光滑的清漆材质,靠mask来控制,其中的原理在源码里也能看到,由于我目前还没有做双层材质的需求,所以暂时跳过这个部分。
不过要写一个完整的shader,光有光照也还是不行的,从urp的Lit.shader中就可以看出,我还需要shadowcast、depthnormals等一系列的pass,这些也是写一个自己的高效率管线所绕不开的坎。
因此,在后篇中,我会去研究其它Pass所使用的代码结构和原理。