How to Explore Unity 5's Shader System Code - III - Reading the Standard Shader (the PBS version)

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.