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

Following up from part I, you should have downloaded the zip file with the Shader code for the version of Unity you're using.

I'm going to use Unity 5.4 (still in beta, has an improved cube map convolution implementation), and I suggest you to use at least Unity 5.3, because they changed the lighting model (BRDF) from a version of Phong to GGX back then, which in my opinion should be a marked improvement, and also makes for a more interesting example.

Chasing the pragmas

So, back to our shader code.
Let's start from the simpler Standard shader, DefaultResourcesExtra\Standard.shader

If you open the file you'll see it is a Surface Shader, it's got a Properties section, and various shader passes. A few are dedicated to shadow drawing, and meta information gathering. It also has different passes for the deferred and the forward renderer.

Let's analyse the base pass for the forward renderer (the forward renderer uses two passes, a base one for the first light, and an add one for all the others):

// ------------------------------------------------------------------
		//  Base forward pass (directional light, emission, lightmaps, ...)
		Pass
		{
			Name "FORWARD" 
			Tags { "LightMode" = "ForwardBase" }

			Blend [_SrcBlend] [_DstBlend]
			ZWrite [_ZWrite]

			CGPROGRAM
			#pragma target 3.0

			// -------------------------------------

			#pragma shader_feature _NORMALMAP
			#pragma shader_feature _ _ALPHATEST_ON _ALPHABLEND_ON _ALPHAPREMULTIPLY_ON
			#pragma shader_feature _EMISSION
			#pragma shader_feature _METALLICGLOSSMAP
			#pragma shader_feature ___ _DETAIL_MULX2
			#pragma shader_feature _ _SMOOTHNESS_TEXTURE_ALBEDO_CHANNEL_A
			#pragma shader_feature _ _SPECULARHIGHLIGHTS_OFF
			#pragma shader_feature _ _GLOSSYREFLECTIONS_OFF
			#pragma shader_feature _PARALLAXMAP

			#pragma multi_compile_fwdbase
			#pragma multi_compile_fog

			#pragma vertex vertBase
			#pragma fragment fragBase
			#include "UnityStandardCoreForward.cginc"

			ENDCG
		}

As you can see it consist mainly of a lot of pragmas and defines.

In ubershader style, the pragmas are activating sections of code which actually reside in various include files.

So to piece together what is actually happening in this pass we must open CGIncludes\UnityStandardCoreForward.cginc and look up each sections, pragma by pragma.

That would take a much longer post than this to complete, so for now we'll focus on finding the base functions, mainly where the lighting calculations are.

CGIncludes\UnityStandardCoreForward.cginc is also just wiring up stuff which resides in other cgincludes.
In this case it's setting up what vertex and fragment functions to use, depending on the define UNITY_STANDARD_SIMPLE.

Let's look at that simpler one, CGIncludes\UnityStandardCoreForwardSimple.cginc.
It's "simple" because it doesn't support _PARALLAXMAP, DIRLIGHTMAP_COMBINED, DIRLIGHTMAP_SEPARATE, so it should be easier for us to read as well.

Fundamental functions and structs

Finally in this file we find some actual functions and structs, the fundamental ones are:

  • struct VertexOutputBaseSimple, which is the data structure that carries the data we need from the vertex shader to the fragment shader.

  • vertForwardBaseSimple which is the function that is executed on every vertex, filling in the VertexOutputBaseSimple struct

  • fragForwardBaseSimpleInternal, which is the function that takes the vertex output struct and calculates the first light in the forward renderer

Fragment function

It returns a vector of four half precision floats ( a colour & alpha), and it takes a VertexOutputBaseSimple struct:

half4 fragForwardBaseSimpleInternal (VertexOutputBaseSimple i)
{
	FragmentCommonData s = FragmentSetupSimple(i);
	UnityLight mainLight = MainLightSimple(i, s);	
	half atten = SHADOW_ATTENUATION(i);	
	half occlusion = Occlusion(i.tex.xy);
    half rl = dot(REFLECTVEC_FOR_SPECULAR(i, s), LightDirForSpecular(i, mainLight));
	UnityGI gi = FragmentGI (s, occlusion, i.ambientOrLightmapUV, atten, mainLight);
	half3 attenuatedLightColor = gi.light.color * mainLight.ndotl;
	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);
	c += Emission(i.tex.xy);

	UNITY_APPLY_FOG(i.fogCoord, c);
	
	return OutputForward (half4(c, 1), s.alpha);
}

Looking at the code, it gathers the information needed for, and calculates, the direct and indirect contributions, plus fog, attenuation, emission and occlusion.

The description is very high level, as we need to look at each of these function one by one and see what each does exactly, to figure out precisely which code calculates what.

More function chasing

The actual functions that calculate the lights are not in this file, some are in CGIncludes/UnityStandardCore.cginc:

  • MainLight (actually uses to MainLightSimple, which is in UnityStandardCoreForward.cginc)
UnityLight MainLightSimple(VertexOutputBaseSimple i, FragmentCommonData s)
{
	UnityLight mainLight = MainLight(s.normalWorld);
	#if defined(LIGHTMAP_OFF) && defined(_NORMALMAP)
		mainLight.ndotl = LambertTerm(s.tangentSpaceNormal, i.tangentSpaceLightDir);
	#endif
	return mainLight;
}

we can see a LambertTerm being calculated there, but only if lightmaps are off and normal map is on.

CGIncludes/UnityStandardBRDF.cginc:

  • BRDF3DirectSimple (which uses BRDF3Direct)
half3 BRDF3_Direct(half3 diffColor, half3 specColor, half rlPow4, half oneMinusRoughness)
{
	half LUT_RANGE = 16.0; // must match range in NHxRoughness() function in GeneratedTextures.cpp
	// Lookup texture to save instructions
	half specular = tex2D(unity_NHxRoughness, half2(rlPow4, 1-oneMinusRoughness)).UNITY_ATTEN_CHANNEL * LUT_RANGE;
#if defined(_SPECULARHIGHLIGHTS_OFF)
	specular = 0.0;
#endif

it looks like it's calculating the specular contribution, by using a lookup table.

  • LambertTerm, which leads to DotClamped
inline half DotClamped (half3 a, half3 b)
{
	#if (SHADER_TARGET < 30 || defined(SHADER_API_PS3))
		return saturate(dot(a, b));
	#else
		return max(0.0h, dot(a, b));
	#endif
}

We know from MainLightSimple that the arguments being passed there are N and L.

So the fragment function, first sets up the fragment, then calculates the ndotl for the main light, the attenuation, the occlusion, the gi, the light colour. Then it calculates every contribution of the final light and add them up together, direct, indirect, global illumination and then applies the fog.

As you could see shader is pretty light on lighting calculations, just using a lambert, and look up tables. It doesn't look like Physically Based shading is being used much, it's probably the cheapest version of the standard shader.

In part III we'll do the same for the normal Standard Shader version, and we'll likely see some more advanced BRDFs being used.