Shader Graph – Shadows for URP Unlit Graph

This was initially written as an answer to someone on the Unity Discord. Have tried updating it since v11 (2021.1) changed some things. I probably won’t be updating further as I’ve moved to a new site.

For up-to-date information, I’ve shared some custom lighting functions/subgraphs on github! It includes a Main Light Shadows one which will work in Unlit Graph, with any shadow cascades setting!


Warning : The post below may be outdated

 


A quick introduction to keywords

material.EnableKeyword allows us to enable keywords for a particular material. You’d use it in a script with the material reference. For example something like :

public class Example : MonoBehaviour {

    public Material material;

    void Start(){
        material.EnableKeyword("SOME_KEYWORD");
        // etc
    }

}

Probably can also add [ExecuteAlways] before the class so it works in edit mode too. There’s also Shader.EnableKeyword which is a static method and does it globally. There’s also DisableKeyword for disabling keywords that have been previously enabled.

The shader is responsible for how those keywords are used. Usually, we use multi_compile and shader_feature in code to define a keyword which allows us to toggle effects on and off in a shader. (This can be done in shadergraph too, using the Keywords in the blackboard).

Once we have one of those set up, e.g. SOME_KEYWORD, we can use it by doing something like :

#ifdef SOME_KEYWORD  // short for "#if defined(SOME_KEYWORD)"
// do this code
#else
// do nothing, or do some other code instead
#endif

It’s the same in shader graph, but it’s a Boolean Keyword node with an On and Off state.

This is useful as we can make sure particular code is, or isn’t, included in the shader, without manually creating two separate shaders. This is typically used for calculations that are complex.

There are some problems, as there is a maximum keyword count and for every boolean keyword defined it doubles the amount of shader variants, which affects build time – but not going into that now. You can read more here : https://docs.unity3d.com/Manual/SL-MultipleProgramVariants.html

Main Light Shadows

Specifically when it comes to main light shadows in URP, you are supposed to define the following keywords :

// For 2021.1+ / v11
#pragma multi_compile _ _MAIN_LIGHT_SHADOWS _MAIN_LIGHT_SHADOWS_CASCADE _MAIN_LIGHT_SHADOWS_SCREEN
#pragma multi_compile _ _SHADOWS_SOFT

// Prior to v11
#pragma multi_compile _ _MAIN_LIGHT_SHADOWS
#pragma multi_compile _ _MAIN_LIGHT_SHADOWS_CASCADE
#pragma multi_compile _ _SHADOWS_SOFT

For the PBR/Lit Graph in URP they are already defined for us. However, they aren’t in an Unlit Graph.

We can define them using Boolean Keywords in the Blackboard (and possibly the Enum Keyword for v11+, but untested. I tend to still use the boolean keywords to define them – though this would result in more shader variants than what is actually required)

One problem however, is that defining _MAIN_LIGHT_SHADOWS causes an error. This is because the code expects the shadowCoord to be passed from the vertex to fragment stages through the Varyings struct – and it doesn’t exist in the generated code for the Unlit Graph. We can’t add it in unless we were to edit the generated code, and that’s not ideal.

That keyword is used in URP’s ShaderLibrary (Shadows.hlsl) to define MAIN_LIGHT_CALCULATE_SHADOWS, which is then used to ensure those shadow calculations take place. In the past, I’ve mentioned it’s possible to define this keyword instead and the graph works – but only in editor as without _MAIN_LIGHT_SHADOWS being defined, the shader strips all shadow-based variants from the build! 😦

Old image sorry, use _MAIN_LIGHT_SHADOWS in the first reference instead!!

So I mentioned that defining _MAIN_LIGHT_SHADOWS produces an error, but it actually doesn’t error (in editor at least) if the Shadow Cascades setting on the URP Asset is 2 or higher. It only errors if set to none / 1 cascade. But I’ve still had troubles with building the project.

Luckily there is a fix if you want to be able to use any cascade setting. I’ve discovered that you can prevent the error occurring with none/1 cascades by using the following in your custom lighting HLSL file (outside the function) :

#undef REQUIRES_VERTEX_SHADOW_COORD_INTERPOLATOR

We can also surround it in a few SHADERPASS checks to be slightly more optimised if we use the custom lighting functions in the PBR/Lit graph. Though this would still be defined in other passes, which probably isn’t ideal, but is hard to work around.

/*
- This undef (un-define) is required to prevent the "invalid subscript 'shadowCoord'" error,
  which occurs when _MAIN_LIGHT_SHADOWS is used with 1/No Shadow Cascades with the Unlit Graph.
- It's technically not required for the PBR/Lit graph, so I'm using the SHADERPASS_FORWARD to ignore it for the pass.
*/
#ifndef SHADERGRAPH_PREVIEW
	#if VERSION_GREATER_EQUAL(9, 0)
		#include "Packages/com.unity.render-pipelines.universal/Editor/ShaderGraph/Includes/ShaderPass.hlsl"
		#if (SHADERPASS != SHADERPASS_FORWARD)
			#undef REQUIRES_VERTEX_SHADOW_COORD_INTERPOLATOR
		#endif
	#else
		#ifndef SHADERPASS_FORWARD
			#undef REQUIRES_VERTEX_SHADOW_COORD_INTERPOLATOR
		#endif
	#endif
#endif
// Also see https://github.com/Cyanilux/URP_ShaderGraphCustomLighting

After this, you’d define the custom function. For example :

void MainLight_float (float3 WorldPos, out float3 Direction, out float3 Color,
	out float DistanceAtten, out float ShadowAtten){

#ifdef SHADERGRAPH_PREVIEW
	Direction = normalize(float3(1,1,-0.4));
	Color = float4(1,1,1,1);
	DistanceAtten = 1;
	ShadowAtten = 1;
#else
	float4 shadowCoord = TransformWorldToShadowCoord(WorldPos);
	Light mainLight = GetMainLight(shadowCoord);

	Direction = mainLight.direction;
	Color = mainLight.color;
	DistanceAtten = mainLight.distanceAttenuation;
	ShadowAtten = mainLight.shadowAttenuation;
#endif

}

Which you’d then use in graph like so :

For an example of how you’d use this further, see Unity’s article on custom lighting here.

Make sure you also set the keywords to Multi Compile & Global so that Unity/URP can set them automatically.

With changes to Shader Graph / URP, these methods of custom lighting could potentially break in the future though… so be aware that you may need to fix things later if that does happen. Check out the github linked at the top of this post for (hopefully) more up-to-date Custom Lighting functions.

Alternative

An alternative, is bypassing all keywords entirely. For shadows & shadow cascades, it should be editing the custom function to this.

Though be aware this is untested in build. I’m pretty sure I’ve seen some strange shadowing artifacts with this too, when no shadows should be cast on the object.

void MainLight_float (float3 WorldPos, out float3 Direction, out float3 Color,
	out float DistanceAtten, out float ShadowAtten){

#ifdef SHADERGRAPH_PREVIEW
	Direction = normalize(float3(1,1,-0.4));
	Color = float4(1,1,1,1);
	DistanceAtten = 1;
	ShadowAtten = 1;
#else
	Light mainLight = GetMainLight();
	Direction = mainLight.direction;
	Color = mainLight.color;
	DistanceAtten = mainLight.distanceAttenuation;

	float4 shadowCoord = TransformWorldToShadowCoord(WorldPos);
	// or if you want shadow cascades :
	// half cascadeIndex = ComputeCascadeIndex(WorldPos);
	// float4 shadowCoord = mul(_MainLightWorldToShadow[cascadeIndex], float4(WorldPos, 1.0));

	ShadowSamplingData shadowSamplingData = GetMainLightShadowSamplingData();
	float shadowStrength = GetMainLightShadowStrength();
	ShadowAtten = SampleShadowmap(shadowCoord, TEXTURE2D_ARGS(_MainLightShadowmapTexture, sampler_MainLightShadowmapTexture), shadowSamplingData, shadowStrength, false);
#endif
}

This won’t support soft shadows though. For that could look at the SampleShadowmapFiltered method. But this is a bit awkward to have these hardcoded, and less flexible if we want to change them later, so I’d recommend sticking to the keyword methods above instead.