Shader Graph Custom Lighting (Shadows for URP Unlit Graph)

Hey! Wrote this up as an answer to someone on the Unity Discord. I had plans to extend this, but probably wont get around to it as I’ve moved to a new site. But it still possibly provides a bit of information about getting shadows to work in an Unlit Graph.

Since this still shows on google searches, I’ve updated the page (numerous times) so information below may be a bit messy. Images are also outdated, focus on reading the text instead.

..

I’ve also 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! It used to error with 1/None due to things explained below but I found a fix (also explained below).

..

..


Keywords

So, material.EnableKeyword allows you 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");
        material.EnableKeyword("_MAIN_LIGHT_SHADOWS");
        // 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.

Just enabling a keyword isn’t enough to magically make it work though. 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

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

It’s useful as we can ensure some particular code is, or isn’t included in the shader, without manually creating two separate shaders. Especially useful if those calculations are complex and some materials might need them while others won’t, like shadow calculations.

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 :

#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, however defining _MAIN_LIGHT_SHADOWS causes an error as the code later 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. We can’t add it in unless we were to edit the generated code, and that’s not ideal.

That keyword isn’t actually used in URP’s ShaderLibrary Shadows.hlsl, except to also 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 that keyword instead and the graph works. This is only partly true however, as it’ll work perfectly in-editor, but in builds there’s a problem.

In builds, without _MAIN_LIGHT_SHADOWS being defined, the shader strips variants including the cascades & soft shadows. The MAIN_LIGHT_CALCULATE_SHADOWS one will still work, but only for no cascades and hard shadows only. So it’s pointless to define those other ones.

Old image, either only define MAIN_LIGHT_CALCULATE_SHADOWS, or instead define the other 3 only.

That is unless we instead define _MAIN_LIGHT_SHADOWS. While I mentioned it produces an error, it actually doesn’t error if the Shadow Cascades on the URP asset is 2 or higher. It only errors if set to none/1 cascade.

So if you are using cascades, we can define _MAIN_LIGHT_SHADOWS instead, along with _MAIN_LIGHT_SHADOWS_CASCADE and _SHADOWS_SOFT and everything works as long as you don’t change the cascades to none/1.

UPDATE : I’ve also 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 :

/*
- 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.
  (But it would probably still remove the interpolator for other passes in the PBR/Lit graph and use a per-pixel version)
*/
#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

The custom function here btw is :

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

}

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.

If you’re fine with no cascades & soft shadows and go for the MAIN_LIGHT_CALCULATE_SHADOWS only keyword, it won’t be set automatically so in order to enable it, we can either switch to the debug inspector and enter it manually (as shown in this tweet thread), or we can use material.EnableKeyword from a C# script as mentioned at the start.

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.

Alternative

An alternative, is bypassing all keywords entirely. For shadows & shadow cascades, it should be editing the custom function to this (though this is untested in build) :

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
}

For soft shadows might want to look at the SampleShadowmapFiltered method for that. But this is a bit awkward to have these hardcoded, and less flexible if we want to change them later.