Following up from part II, this time we're going to build on the knowledge we already have of how the shader system works, and focus on the lighting function of the standard shader.
So, starting back at Standard.shader
, but this time in UnityStandardCoreForward.shader
we'll take the other branch, the 'not simple' branch.
That leads us to UnityStandardCore.shader
and we're interested in the fragForwardBaseInternal
function.
half4 fragForwardBaseInternal (VertexOutputForwardBase i)
{
FRAGMENT_SETUP(s)
#if UNITY_OPTIMIZE_TEXCUBELOD
s.reflUVW = i.reflUVW;
#endif
UnityLight mainLight = MainLight (s.normalWorld);
half atten = SHADOW_ATTENUATION(i);
half occlusion = Occlusion(i.tex.xy);
UnityGI gi = FragmentGI (s, occlusion, i.ambientOrLightmapUV, atten, mainLight);
half4 c = UNITY_BRDF_PBS (s.diffColor, s.specColor, s.oneMinusReflectivity, s.oneMinusRoughness, s.normalWorld, -s.eyeVec, gi.light, gi.indirect);
c.rgb += UNITY_BRDF_GI (s.diffColor, s.specColor, s.oneMinusReflectivity, s.oneMinusRoughness, s.normalWorld, -s.eyeVec, occlusion, gi);
c.rgb += Emission(i.tex.xy);
UNITY_APPLY_FOG(i.fogCoord, c.rgb);
return OutputForward (c, s.alpha);
}
Simple version, for reference:
half3 c = BRDF3_Indirect(s.diffColor, s.specColor, gi.indirect, PerVertexGrazingTerm(i, s), PerVertexFresnelTerm(i));
c += BRDF3DirectSimple(s.diffColor, s.specColor, s.oneMinusRoughness, rl) * attenuatedLightColor;
c += UNITY_BRDF_GI (s.diffColor, s.specColor, s.oneMinusReflectivity, s.oneMinusRoughness, s.normalWorld, -s.eyeVec, occlusion, gi);
Comparing with last article's version we can see that the final colour is produced by adding the result of calls to UNITY_BRDF_PBS
, UNITY_BRDF_GI
and Emission
.
Emission is the same as the Simple version. UNITY_BRDF_PBS
and UNITY_BRDF_GI
are aliases of functions which are defined in the included files. Looking up in the includes:
#include "UnityCG.cginc"
#include "UnityShaderVariables.cginc"
#include "UnityInstancing.cginc"
#include "UnityStandardConfig.cginc"
#include "UnityStandardInput.cginc"
#include "UnityPBSLighting.cginc"
#include "UnityStandardUtils.cginc"
#include "UnityStandardBRDF.cginc"
#include "AutoLight.cginc"
the most likely ones are UnityStandardBRDF
and UnityPBSLighting
so looking at those first.
And they are in UnityPBSLighting.cginc
, depending on the Shader target it will choose a different function.
Let's pick BRDF1_Unity_PBS
, which resides in UnityStandardBRDF.cginc
, it looks like it's the more expensive and realistic BRDF available, with BRDF3_Unity_PBS
being the cheapest.
As you can see, it's a big function, so I'll skip over some details related to optimisation, assume we're using linear, and comment it in chunks, starting with this very useful comment:
// Main Physically Based BRDF
// Derived from Disney work and based on Torrance-Sparrow micro-facet model
//
// BRDF = kD / pi + kS * (D * V * F) / 4
// I = BRDF * NdotL
//
// * NDF (depending on UNITY_BRDF_GGX):
// a) Normalized BlinnPhong
// b) GGX
// * Smith for Visiblity term
// * Schlick approximation for Fresnel
this gives us the formula used, and the references/influences. There is a choice of NDF (normal distribution function), but I'll only cover GGX, which is IMO way better (if more expensive).
You may already be familiar with the formula, but for the benefit of those who aren't I'll at least connect those two letter variables to proper definitions:
- kD: diffuse reflectance
- pi: the π constant (no surprises here I guess)
- kS: specular reflectance
- D: Distribution of normals
- V: Geometric Visibility factor
- F: Fresnel reflectance
(see "Minimalist Cook-Torrance (ShaderX7 style)")
Without further ado, the custom lighting function:
half4 BRDF1_Unity_PBS (half3 diffColor, half3 specColor, half oneMinusReflectivity, half oneMinusRoughness,
half3 normal, half3 viewDir,
UnityLight light, UnityIndirect gi)
{
half roughness = 1-oneMinusRoughness;
inverts the smoothness to be roughness instead.
half3 halfDir = Unity_SafeNormalize (light.dir + viewDir);
the half vector.
Handling correctly NdotV (see comments in the file):
half nl = DotClamped(normal, light.dir);
half nh = BlinnTerm (normal, halfDir);
half nv = DotClamped(normal, viewDir);
half lv = DotClamped (light.dir, viewDir);
half lh = DotClamped (light.dir, halfDir);
Calculating V and D:
half V = SmithJointGGXVisibilityTerm (nl, nv, roughness);
half D = GGXTerm (nh, roughness);
Calculating the Diffuse term according to a version of the Disney BRDF & putting the specular factors together:
half disneyDiffuse = (1 + (Fd90-1) * nlPow5) * (1 + (Fd90-1) * nvPow5);
half specularTerm = (V * D) * (UNITY_PI/4); // Torrance-Sparrow model, Fresnel is applied later (for optimization reasons)
//HACK (see file for more comments)
specularTerm = max(0, specularTerm * nl);
half diffuseTerm = disneyDiffuse * nl;
// surfaceReduction = Int D(NdotH) * NdotH * Id(NdotL>0) dH = 1/(realRoughness^2+1)
half realRoughness = roughness*roughness; // need to square perceptual roughness
half surfaceReduction = 1.0 / (realRoughness*realRoughness + 1.0); // fade \in [0.5;1]
half grazingTerm = saturate(oneMinusRoughness + (1-oneMinusReflectivity));
Putting it all together, including the Global Illumination contribution:
half3 color = diffColor * (gi.diffuse + light.color * diffuseTerm)
+ specularTerm * light.color * FresnelTerm (specColor, lh)
+ surfaceReduction * gi.specular * FresnelLerp (specColor, grazingTerm, nv);
return half4(color, 1);
}
And that's all for the lighting function.
In part IV we're going to chase down the contribution of the Global Illumination in the final result.
Comments? Give me a shout at @shadercat.
To get the latest post updates subscribe to the ShaderCat newsletter.
You can support my writing on ShaderCat's Patreon.