Writing Shader Code for the Universal RP

ShadowCaster & DepthOnly Passes

If we want the shader to cast shadows, it needs a pass with the tag “LightMode”=”ShadowCaster”. This can be done on both Unlit and Lit shaders, but be aware that while they will cast shadows – they won’t receive shadows unless you handle that in the UniversalForward pass.

Shaders should also include a pass tagged with “LightMode”=”DepthOnly”. This pass is very similar to the ShadowCaster, but without the shadow bias offsets. I’m not completely sure what the DepthOnly pass is used for in the URP. The Scene View seems to use it when rendering the depth texture (used by the Scene Depth node in shadergraph), while the Game View depth texture seems to work fine without this pass. There may be other things like custom render features (for the forward renderer) that rely on the DepthOnly pass though.

Like with all the passes in the shader, they have to share the same UnityPerMaterial CBUFFER for the shader to be compatible with the SRP Batcher. In previous sections of this post, we put the buffer inside the HLSLINCLUDE so that it is automatically used for every pass in the shader.

Except when using UsePass, which was discussed back in the Shaderlab section. Although we could use the shadow caster from other shaders, e.g. UsePass “Universal Render Pipeline/Lit/ShadowCaster”, the SRP Batcher compatibility might be lost as the CBUFFER used in that shader is probably different.

Instead you should define these passes yourself, but as a slight workaround we can do the following :

Pass {
	Name "ShadowCaster"
	Tags { "LightMode"="ShadowCaster" }

	ZWrite On
	ZTest LEqual

	HLSLPROGRAM
	// Required to compile gles 2.0 with standard srp library
	#pragma prefer_hlslcc gles
	#pragma exclude_renderers d3d11_9x gles
	//#pragma target 4.5

	// Material Keywords
	#pragma shader_feature _ALPHATEST_ON
	#pragma shader_feature _SMOOTHNESS_TEXTURE_ALBEDO_CHANNEL_A

	// GPU Instancing
	#pragma multi_compile_instancing
	#pragma multi_compile _ DOTS_INSTANCING_ON
            
	#pragma vertex ShadowPassVertex
	#pragma fragment ShadowPassFragment
	
	#include "Packages/com.unity.render-pipelines.core/ShaderLibrary/CommonMaterial.hlsl"
	#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/SurfaceInput.hlsl"
	#include "Packages/com.unity.render-pipelines.universal/Shaders/ShadowCasterPass.hlsl"

	ENDHLSL
}

Using the ShadowCasterPass that Unity uses for their URP shaders means it’s a bit easier to define the pass, however it requires using the _BaseMap, _BaseColor and _Cutoff properties, and we need to add them to our UnityPerMaterial CBUFFER too.

The fragment function in the shadow caster just returns 0 where a shadow needs to be, and discards the pixel where there shouldn’t be any shadows (note, that clipping only happens if the _ALPHATEST_ON keyword is enabled), you can take a look at the code by clicking the link above.

If our regular shader pass also does vertex displacement, this needs to be added to the ShadowCaster pass too, so that it correctly casts the displaced shadows. In order to handle this we would either have to copy the contents of the ShadowCasterPass into our pass, or just define a new vertex function and swap the #pragma vertex ShadowPassVertex out. For example :

#pragma vertex vert

...

// function copied from ShadowCasterPass and edited slightly.
Varyings vert(Attributes input) {
	Varyings output;
	UNITY_SETUP_INSTANCE_ID(input);

	// Example Displacement
	input.positionOS += float4(0, _SinTime.y, 0, 0);

	output.uv = TRANSFORM_TEX(input.texcoord, _BaseMap);
	output.positionCS = GetShadowPositionHClip(input);
	return output;
}
DepthOnly

We can handle the DepthOnly pass in a similar way, with a few minor differences :

Pass {
	Name "DepthOnly"
	Tags { "LightMode"="DepthOnly" }

	ZWrite On
	ColorMask 0

	HLSLPROGRAM
	// Required to compile gles 2.0 with standard srp library
	#pragma prefer_hlslcc gles
	#pragma exclude_renderers d3d11_9x gles
	//#pragma target 4.5

	// Material Keywords
	#pragma shader_feature _ALPHATEST_ON
	#pragma shader_feature _SMOOTHNESS_TEXTURE_ALBEDO_CHANNEL_A

 	// GPU Instancing
	#pragma multi_compile_instancing
	#pragma multi_compile _ DOTS_INSTANCING_ON
            
	#pragma vertex DepthOnlyVertex
	#pragma fragment DepthOnlyFragment
			
	#include "Packages/com.unity.render-pipelines.core/ShaderLibrary/CommonMaterial.hlsl"
	#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/SurfaceInput.hlsl"
	#include "Packages/com.unity.render-pipelines.universal/Shaders/DepthOnlyPass.hlsl"

	// Again, using this means we also need _BaseMap, _BaseColor and _Cutoff shader properties
	// Also including them in cbuffer, except _BaseMap as it's a texture.

	ENDHLSL
}

This time using the DepthOnlyPass provided by Unity’s URP shaders. Again, if you need vertex displacement we should copy the DepthOnlyVertex function into our code, rename it to vert, and add the displacement code like in the ShadowCaster example above.


Hey! This is basically the end of the guide, (the next page goes over a summary of differences between the built-in and URP if you are interested).

Thank you for reading, I have no idea if it’s written well but I hope this helps!! 🙂

If you have any suggestions for new sections or improvements to other sections, or spot any mistakes or typos, feel free to contact me. @Cyanilux on twitter. I also have a discord server.