Catlike Coding

Custom SRP 7.1.0

Splitting Shadow Code

Changed code, same shadows.

This tutorial is made with Unity 6000.5.8f1 and follows Custom SRP 7.0.0.

Separate Other Shadows

This time we're going to refactor our old shadow rendering code a bit, preparing it for future improvements. Currently the code for all shadows is packed together in Shadows. We're going to isolate the code for directional shadows and for other shadows, because those shadow types are independent. We'll also make some minor convention changes, but otherwise leave the code as is. So functionally everything remains the same, we only restructure our code.

We start by introducing an OtherShadows class that will contain the code for rendering point lights and spotlights. To make communicating what resource handles it uses easier we'll also give it a public inner Handles readonly struct type that contains the handles for the texture atlas and the data buffer. Give it a convenient Use method that can be called by passes that use the shadow resources for read access, so they don't need to know the details.

public partial class OtherShadows
{
	public readonly struct Handles
	{
		public readonly TextureHandle atlas;
		public readonly BufferHandle buffer;

		public Handles(TextureHandle atlas, BufferHandle buffer)
		{
			this.atlas = atlas;
			this.buffer = buffer;
		}

		public void Use(IBaseRenderGraphBuilder builder)
		{
			builder.UseTexture(atlas);
			builder.UseBuffer(buffer);
		}
	}
}

We make OtherShadows a partial class because we move OtherShadowData into it as well. We placed that class in its own file because it matches the data type that we use in the shader and put all those struct types in same folder. We might change that in the future, but for now we keep it where it is.

partial class OtherShadows
{
	[StructLayout(LayoutKind.Sequential)]
	struct OtherShadowData { … }
}

Copy all constants and field from Shadows to OtherShadows that are related to other shadows. Because they're now explicitly in the scope for other shadows we can simplify their names by removing other and shadow from them.

We also need to know the shadow settings, but only for other shadows. So we keep track of ShadowSettings.Other instead of ShadowSettings, but also need a field for the filter size as that's determined by the generic shadow settings.

We now use Handles to keep track of the atlas and data buffer handles.

As the RenderInfo array is fully private to OtherShadows let's also give it its own private definition of RenderInfo, which just bundles a few fields.

We also add fields for the split and tile size used for the atlas.

using Unity.Collections;
using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.RenderGraphModule;

public partial class OtherShadows
{
	public readonly struct Handles { … }
	
	const int maxLightCount = 16;
	const int maxTilesPerLight = 6;

	static readonly int
		atlasId = Shader.PropertyToID("_OtherShadowAtlas"),
		dataId = Shader.PropertyToID("_OtherShadowData");
	
	static readonly OtherShadowData[] data = new OtherShadowData[maxLightCount];

	struct ShadowedLight
	{
		public int visibleLightIndex;
		public float slopeScaleBias;
		public float normalBias;
		public bool isPoint;
	}
	
	readonly ShadowedLight[] lights = new ShadowedLight[maxLightCount];

	ShadowSettings.Other settings;

	float filterSize;

	int lightCount;

	Handles handles;

	struct RenderInfo
	{
		public RendererListHandle handle;
		public Matrix4x4 view, projection;
	}
	
	readonly RenderInfo[] renderInfo =
		new RenderInfo[maxLightCount * Shadows.maxTilesPerLight];

	int split, tileSize;
}

The Shadows class will still drive the shadow rendering process, thus it needs to know if there are any shadow maps to render. We'll make this possible by adding a public HasLights property, which checks if there are any registered lights.

	public bool HasLights => lightCount > 0;

Add a public Setup method to apply the shadow settings and sets the light count to zero.

	public void Setup(ShadowSettings settings)
	{
		this.settings = settings.other;
		filterSize = settings.OtherFilterSize;
		lightCount = 0;
	}

Next we copy ReserveOtherShadows from Shadows, rename it to ReserveShadows, and make it public. Because we're not keeping track of the culling results via a field it has to become a parameter. We also have to communicate whether the shadow mask is used. As we're already returning the light's shadow data we do this via a ref field. Besides that only some renaming is needed.

	public Vector4 ReserveShadows(
		Light light,
		int visibleLightIndex,
		CullingResults cullingResults,
		ref bool useShadowMask)
	{
		if (light.shadows == LightShadows.None || light.shadowStrength <= 0f)
		{
			return new Vector4(0f, 0f, 0f, -1f);
		}

		float maskChannel = -1f;
		LightBakingOutput lightBaking = light.bakingOutput;
		if (
			lightBaking.lightmapBakeType == LightmapBakeType.Mixed &&
			lightBaking.mixedLightingMode == MixedLightingMode.Shadowmask)
		{
			useShadowMask = true;
			maskChannel = lightBaking.occlusionMaskChannel;
		}

		bool isPoint = light.type == LightType.Point;
		int newLightCount = lightCount + (isPoint ? 6 : 1);
		if (
			newLightCount > maxLightCount ||
			!cullingResults.GetShadowCasterBounds(visibleLightIndex, out _))
		{
			return new Vector4(-light.shadowStrength, 0f, 0f, maskChannel);
		}

		lights[lightCount] = new ShadowedLight
		{
			visibleLightIndex = visibleLightIndex,
			slopeScaleBias = light.shadowBias,
			normalBias = light.shadowNormalBias,
			isPoint = isPoint
		};

		var data = new Vector4(
			light.shadowStrength, lightCount,
			isPoint ? 1f : 0f, maskChannel);
		lightCount = newLightCount;
		return data;
	}

We introduce a new GetHandles method that isolates the code for other shadows from Shadows.GetResources and uses the new Handles struct.

	public Handles GetHandles(
		RenderGraph renderGraph, IUnsafeRenderGraphBuilder builder)
	{
		TextureHandle atlas;
		if (lightCount > 0)
		{
			int atlasSize = (int)settings.atlasSize;
			atlas = renderGraph.CreateTexture(new TextureDesc(
				atlasSize, atlasSize)
			{
				depthBufferBits = DepthBits.Depth32,
				isShadowMap = true,
				name = "Other Shadow Atlas"
			});
			builder.UseTexture(atlas, AccessFlags.WriteAll);
		}
		else
		{
			atlas = renderGraph.defaultResources.defaultShadowTexture;
		}

		handles = new Handles(
			atlas,
			renderGraph.CreateBuffer(new BufferDesc(
				maxLightCount, OtherShadowData.stride)
			{
				name = "Other Shadow Data"
			})
		);
		builder.UseBuffer(resources.buffer, AccessFlags.WriteAll);
		return handles;
	}

Do the same for BuildRendererLists. Besides the culling results the required native arrays must also become parameters and be passed through to the specialized methods for point shadows and spot shadows.

	public void BuildRendererLists(
		RenderGraph renderGraph,
		IUnsafeRenderGraphBuilder builder,
		CullingResults cullingResults,
		NativeArray<ShadowSplitData> shadowSplitDataPerLight,
		NativeArray<LightShadowCasterCullingInfo> cullingInfoPerLight)
	{
		int atlasSize = (int)settings.atlasSize;
		int tiles = lightCount;
		split = tiles <= 1 ? 1 : tiles <= 4 ? 2 : 4;
		tileSize = atlasSize / split;

		for (int i = 0; i < lightCount;)
		{
			if (lights[i].isPoint)
			{
				BuildPointShadowsRendererList(
					i, renderGraph, builder, cullingResults,
					shadowSplitDataPerLight, cullingInfoPerLight);
				i += 6;
			}
			else
			{
				BuildSpotShadowsRendererList(
					i, renderGraph, builder, cullingResults,
					shadowSplitDataPerLight, cullingInfoPerLight);
				i += 1;
			}
		}
	}

The specialized methods rely on some shared functionality for all shadow types. Let's keep that in Shadows for now and make it public. This involves the maxTilesPerLight constant, the ConvertToAtlasMatrix method, and the SetTileViewport methods. The methods can become static, if we add the command buffer as a parameter to SetTileViewport.

	public const int maxTilesPerLight = 6;

	…

	public static Matrix4x4 ConvertToAtlasMatrix(…) { … }

	public static Vector2 SetTileViewport(
		UnsafeCommandBuffer buffer, int index, int split, float tileSize) { … }

Now copy over and adapt BuilSportShadowsRendererList.

	void BuildSpotShadowsRendererList(
		int index,
		RenderGraph renderGraph,
		IUnsafeRenderGraphBuilder builder,
		CullingResults cullingResults,
		NativeArray<ShadowSplitData> shadowSplitDataPerLight,
		NativeArray<LightShadowCasterCullingInfo> cullingInfoPerLight)
	{
		ShadowedLight light = lights[index];
		var shadowSettings = new ShadowDrawingSettings(
			cullingResults, light.visibleLightIndex)
		{
			useRenderingLayerMaskTest = true
		};
		ref RenderInfo info = ref renderInfo[index * Shadows.maxTilesPerLight];
		cullingResults.ComputeSpotShadowMatricesAndCullingPrimitives(
			light.visibleLightIndex, out info.view, out info.projection,
			out ShadowSplitData splitData);

		int splitOffset = light.visibleLightIndex * Shadows.maxTilesPerLight;
		shadowSplitDataPerLight[splitOffset] = splitData;
		info.handle = renderGraph.CreateShadowRendererList(ref shadowSettings);
		builder.UseRendererList(info.handle);
		cullingInfoPerLight[light.visibleLightIndex] =
			new LightShadowCasterCullingInfo
			{
				projectionType = BatchCullingProjectionType.Perspective,
				splitRange = new RangeInt(splitOffset, 1)
			};
	}

And also BuildPointShadowsRendererList.

	void BuildPointShadowsRendererList(
		int index, RenderGraph renderGraph,
		IUnsafeRenderGraphBuilder builder,
		CullingResults cullingResults,
		NativeArray<ShadowSplitData> shadowSplitDataPerLight,
		NativeArray<LightShadowCasterCullingInfo> cullingInfoPerLight)
	{
		ShadowedLight light = lights[index];
		var shadowSettings = new ShadowDrawingSettings(
			cullingResults, light.visibleLightIndex)
		{
			useRenderingLayerMaskTest = true
		};
		float texelSize = 2f / tileSize;
		float filterTexelSize = texelSize * filterSize;
		float bias = light.normalBias * filterTexelSize * 1.4142136f;
		float fovBias =
			Mathf.Atan(1f + bias + filterTexelSize) * Mathf.Rad2Deg * 2f - 90f;
		int splitOffset = light.visibleLightIndex * Shadows.maxTilesPerLight;
		for (int i = 0; i < 6; i++)
		{
			ref RenderInfo info =
				ref renderInfo[index * Shadows.maxTilesPerLight + i];
			cullingResults.ComputePointShadowMatricesAndCullingPrimitives(
				light.visibleLightIndex, (CubemapFace)i, fovBias,
				out info.view, out info.projection,
				out ShadowSplitData splitData);
			shadowSplitDataPerLight[splitOffset + i] = splitData;
			info.handle = renderGraph.CreateShadowRendererList(
				ref shadowSettings);
			builder.UseRendererList(info.handle);
		}

		cullingInfoPerLight[light.visibleLightIndex] =
			new LightShadowCasterCullingInfo
			{
				projectionType = BatchCullingProjectionType.Perspective,
				splitRange = new RangeInt(splitOffset, 6)
			};
	}

The last code to insert is for rendering the shadows. Copy RenderOtherShadows, make it public, and give it a command buffer parameter. We'll no longer set the combined atlas sizes here and leave that to Shadows. We only render shadows if there is work to do, and if so pass the buffer and tile border size to the specialized render methods. We now always clear the global depth bias here and set the texture and buffer shader properties.

	public void RenderOtherShadows(UnsafeCommandBuffer buffer) {
		//int atlasSize = (int)settings.atlasSize;
		//atlasSizes.z = atlasSize;
		//atlasSizes.w = 1f / atlasSize;
		
		buffer.BeginSample("Other Shadows");
		if (lightCount > 0)
		{
			buffer.SetRenderTarget(
				handles.atlas,
				RenderBufferLoadAction.DontCare, RenderBufferStoreAction.Store);
			buffer.ClearRenderTarget(true, false, Color.clear);
			float border = 0.5f / (float)settings.atlasSize;
			for (int i = 0; i < lightCount;)
			{
				if (lights[i].isPoint)
				{
					RenderPointShadows(i, buffer, border);
					i += 6;
				}
				else
				{
					RenderSpotShadows(i, buffer, border);
					i += 1;
				}
			}
		}

		buffer.SetGlobalDepthBias(0f, 0f);
		buffer.SetGlobalTexture(atlasId, handles.atlas);
		buffer.SetGlobalBuffer(dataId, handles.buffer);
		buffer.SetBufferData(handles.buffer, data, 0, 0, lightCount);
		buffer.EndSample("Other Shadows");
	}

Copy and adapt RenderSpotShadows.

	void RenderSpotShadows(
		int index, UnsafeCommandBuffer buffer, float border)
	{
		ShadowedLight light = lights[index];
		RenderInfo info = renderInfo[index * Shadows.maxTilesPerLight];
		float texelSize = 2f / (tileSize * info.projection.m00);
		//float filterSize = texelSize * settings.OtherFilterSize;
		float bias = light.normalBias * filterSize * texelSize * 1.4142136f;
		Vector2 offset = Shadows.SetTileViewport(
			buffer, index, split, tileSize);
		float tileScale = 1f / split;
		data[index] = new OtherShadowData(
			offset, tileScale, bias, border,
			Shadows.ConvertToAtlasMatrix(
				info.projection * info.view, offset, tileScale));
		buffer.SetViewProjectionMatrices(info.view, info.projection);
		buffer.SetGlobalDepthBias(0f, light.slopeScaleBias);
		buffer.DrawRendererList(info.handle);
	}

And RenderPointShadows as well.

	void RenderPointShadows(
		int index, UnsafeCommandBuffer buffer, float border)
	{
		ShadowedLight light = lights[index];
		float texelSize = 2f / tileSize;
		//float filterSize = texelSize * settings.OtherFilterSize;
		float bias = light.normalBias * filterSize * texelSize * 1.4142136f;
		float tileScale = 1f / split;
		buffer.SetGlobalDepthBias(0f, light.slopeScaleBias);
		for (int i = 0; i < 6; i++)
		{
			RenderInfo info = renderInfo[index * Shadows.maxTilesPerLight + i];
			info.view.m11 = -info.view.m11;
			info.view.m12 = -info.view.m12;
			info.view.m13 = -info.view.m13;
			int tileIndex = index + i;
			Vector2 offset = Shadows.SetTileViewport(
				buffer, tileIndex, split, tileSize);
			data[tileIndex] = new OtherShadowData(
				offset, tileScale, bias, border,
				Shadows.ConvertToAtlasMatrix(
					info.projection * info.view, offset, tileScale));
			buffer.SetViewProjectionMatrices(info.view, info.projection);
			buffer.DrawRendererList(info.handle);
		}
	}

Separate Directional Shadows

We're going to do the same for directional shadows, introducing a DirectionalShadows class. Make DirectionalShadowCascade an inner class of it.

partial class DirectionalShadows
{
	[StructLayout(LayoutKind.Sequential)]
	struct DirectionalShadowCascade { … }
}

Then create the actual class, following the same structure that we used for the other shadows, up to and including a Setup method.

public partial class DirectionalShadows
{
	public readonly struct Handles
	{
		public readonly TextureHandle atlas;
		public readonly BufferHandle cascadeBuffer, matrixBuffer;

		public Handles(
			TextureHandle atlas,
			BufferHandle cascadeBuffer,
			BufferHandle matrixBuffer)
		{
			this.atlas = atlas;
			this.cascadeBuffer = cascadeBuffer;
			this.matrixBuffer = matrixBuffer;
		}

		public void Use(IBaseRenderGraphBuilder builder)
		{
			builder.UseTexture(atlas);
			builder.UseBuffer(cascadeBuffer);
			builder.UseBuffer(matrixBuffer);
		}
	}

	const int maxLightCount = 4;
	const int maxCascades = 4;

	static readonly int
		atlasId =
			Shader.PropertyToID("_DirectionalShadowAtlas"),
		cascadesId =
			Shader.PropertyToID("_DirectionalShadowCascades"),
		matricesId =
			Shader.PropertyToID("_DirectionalShadowMatrices"),
		cascadeCountId = Shader.PropertyToID("_CascadeCount"),
		shadowPancakingId = Shader.PropertyToID("_ShadowPancaking");

	static readonly GlobalKeyword softCascadeBlendKeyword =
		GlobalKeyword.Create("_SOFT_CASCADE_BLEND");
	
	static readonly DirectionalShadowCascade[] cascades =
		new DirectionalShadowCascade[maxCascades];

	static readonly Matrix4x4[] matrices =
		new Matrix4x4[maxLightCount * maxCascades];

	struct ShadowedLight
	{
		public int visibleLightIndex;
		public float slopeScaleBias;
		public float nearPlaneOffset;
	}

	readonly ShadowedLight[] lights = new ShadowedLight[maxLightCount];
	
	ShadowSettings.Directional settings;

	float filterSize;

	int lightCount;

	Handles handles;

	struct RenderInfo
	{
		public RendererListHandle handle;
		public Matrix4x4 view, projection;
	}
	
	readonly RenderInfo[] renderInfo =
		new RenderInfo[maxLightCount * maxCascades];

	int split, tileSize;

	public bool HasLights => lightCount > 0;
	
	public void Setup(ShadowSettings settings)
	{
		this.settings = settings.directional;
		filterSize = settings.DirectionalFilterSize;
		lightCount = 0;
	}
}

Copy over and adapt ReserveDirectionalShadows next.

	public Vector4 ReserveDirectionalShadows(
		Light light,
		int visibleLightIndex,
		CullingResults cullingResults,
		ref bool useShadowMask)
	{
		if (
			lightCount < maxLightCount &&
			light.shadows != LightShadows.None && light.shadowStrength > 0f)
		{
			float maskChannel = -1;
			LightBakingOutput lightBaking = light.bakingOutput;
			if (
				lightBaking.lightmapBakeType == LightmapBakeType.Mixed &&
				lightBaking.mixedLightingMode == MixedLightingMode.Shadowmask)
			{
				useShadowMask = true;
				maskChannel = lightBaking.occlusionMaskChannel;
			}

			if (!cullingResults.GetShadowCasterBounds(visibleLightIndex, out _))
			{
				return new Vector4(-light.shadowStrength, 0f, 0f, maskChannel);
			}

			lights[lightCount] = new ShadowedLight
			{
				visibleLightIndex = visibleLightIndex,
				slopeScaleBias = light.shadowBias,
				nearPlaneOffset = light.shadowNearPlane
			};
			return new Vector4(
				light.shadowStrength,
				settings.cascadeCount * lightCount++,
				light.shadowNormalBias, maskChannel);
		}
		return new Vector4(0f, 0f, 0f, -1f);
	}

Give it its own GetHandles method.

	public Handles GetHandles(
		RenderGraph renderGraph,
		IUnsafeRenderGraphBuilder builder)
	{
		TextureHandle atlas;
		if (lightCount > 0)
		{
			int atlasSize = (int)settings.atlasSize;
			atlas = renderGraph.CreateTexture(new TextureDesc(
				atlasSize, atlasSize)
			{
				depthBufferBits = DepthBits.Depth32,
				isShadowMap = true,
				name = "Directional Shadow Atlas"
			});
			builder.UseTexture(atlas, AccessFlags.WriteAll);
		}
		else
		{
			atlas = renderGraph.defaultResources.defaultShadowTexture;
		}

		handles = new Handles(
			atlas,
			renderGraph.CreateBuffer(new BufferDesc(
				maxCascades, DirectionalShadowCascade.stride)
				{
					name = "Shadow Cascades"
				}),
			renderGraph.CreateBuffer(new BufferDesc(
				maxLightCount * maxCascades, 4 * 16)
			{
				name = "Directional Shadow Matrices"
			})
		);
		builder.UseBuffer(resources.cascadeBuffer, AccessFlags.WriteAll);
		builder.UseBuffer(resources.matrixBuffer, AccessFlags.WriteAll);
		return handles;
	}

Next up is the public BuildRendererLists method for directional lights.

	public void BuildRendererLists(
		RenderGraph renderGraph,
		IUnsafeRenderGraphBuilder builder,
		CullingResults cullingResults,
		NativeArray<ShadowSplitData> shadowSplitDataPerLight,
		NativeArray<LightShadowCasterCullingInfo> cullingInfoPerLight)
	{
		int atlasSize = (int)settings.atlasSize;
		int tiles = lightCount * settings.cascadeCount;
		split = tiles <= 1 ? 1 : tiles <= 4 ? 2 : 4;
		tileSize = atlasSize / split;

		for (int i = 0; i < lightCount; i++)
		{
			BuildRendererLists(
				i, renderGraph, builder, cullingResults,
				shadowSplitDataPerLight, cullingInfoPerLight);
		}
	}

Followed by the one that builds the lists for a single light.

	void BuildRendererLists(
		int index,
		RenderGraph renderGraph,
		IUnsafeRenderGraphBuilder builder,
		CullingResults cullingResults,
		NativeArray<ShadowSplitData> shadowSplitDataPerLight,
		NativeArray<LightShadowCasterCullingInfo> cullingInfoPerLight)
	{
		ShadowedLight light = lights[index];
		var shadowSettings = new ShadowDrawingSettings(
			cullingResults, light.visibleLightIndex)
		{
			useRenderingLayerMaskTest = true
		};

		int cascadeCount = settings.cascadeCount;
		Vector3 ratios = settings.CascadeRatios;
		float cullingFactor = Mathf.Max(0f, 0.8f - settings.cascadeFade);
		int splitOffset = light.visibleLightIndex * Shadows.maxTilesPerLight;
		for (int i = 0; i < cascadeCount; i++)
		{
			ref RenderInfo info = ref renderInfo[index * maxCascades + i];
			cullingResults.ComputeDirectionalShadowMatricesAndCullingPrimitives(
				light.visibleLightIndex, i, cascadeCount, ratios,
				tileSize, light.nearPlaneOffset, out info.view,
				out info.projection, out ShadowSplitData splitData);
			splitData.shadowCascadeBlendCullingFactor = cullingFactor;
			shadowSplitDataPerLight[splitOffset + i] = splitData;
			if (index == 0)
			{
				cascades[i] = new DirectionalShadowCascade(
					splitData.cullingSphere, tileSize, filterSize);
			}
			info.handle = renderGraph.CreateShadowRendererList(
				ref shadowSettings);
			builder.UseRendererList(info.handle);
		}

		cullingInfoPerLight[light.visibleLightIndex] =
			new LightShadowCasterCullingInfo
			{
				projectionType = BatchCullingProjectionType.Orthographic,
				splitRange = new RangeInt(splitOffset, cascadeCount)
			};
	}

Last up is the public method for rendering.

	public void RenderDirectionalShadows(UnsafeCommandBuffer buffer)
	{
		//int atlasSize = (int)settings.directional.atlasSize;
		//atlasSizes.x = atlasSize;
		//atlasSizes.y = 1f / atlasSize;
		buffer.BeginSample("Directional Shadows");
		if (lightCount > 0)
		{
			buffer.SetRenderTarget(
				handles.atlas,
				RenderBufferLoadAction.DontCare, RenderBufferStoreAction.Store);
			buffer.ClearRenderTarget(true, false, Color.clear);
			buffer.SetGlobalFloat(shadowPancakingId, 1f);

			for (int i = 0; i < lightCount; i++)
			{
				RenderDirectionalShadows(i, buffer);
			}
			buffer.SetGlobalFloat(shadowPancakingId, 0f);
		}

		buffer.SetGlobalDepthBias(0f, 0f);
		buffer.SetGlobalBuffer(cascadesId, handles.cascadeBuffer);
		buffer.SetGlobalBuffer(matricesId, handles.matrixBuffer);
		buffer.SetGlobalTexture(atlasId, handles.atlas);
		buffer.SetGlobalInt(cascadeCountId,
			lightCount > 0 ? settings.cascadeCount : 0);
		buffer.SetBufferData(
			handles.cascadeBuffer, cascades, 0, 0, settings.cascadeCount);
		buffer.SetBufferData(
			handles.matrixBuffer, matrices,
			0, 0, lightCount * settings.cascadeCount);
		buffer.SetKeyword(
			softCascadeBlendKeyword, settings.softCascadeBlend);
		buffer.EndSample("Directional Shadows");
	}

With the private one that renders shadows for a single light.

	void RenderDirectionalShadows(int index, UnsafeCommandBuffer buffer)
	{
		//int cascadeCount = settings.directional.cascadeCount;
		int tileOffset = index * settings.cascadeCount;
		float tileScale = 1f / split;
		buffer.SetGlobalDepthBias(0f, lights[index].slopeScaleBias);
		for (int i = 0; i < settings.cascadeCount; i++)
		{
			RenderInfo info = renderInfo[index * maxCascades + i];
			int tileIndex = tileOffset + i;
			matrices[tileIndex] = Shadows.ConvertToAtlasMatrix(
				info.projection * info.view,
				Shadows.SetTileViewport(buffer,
					tileIndex, split, tileSize),
				tileScale);
			buffer.SetViewProjectionMatrices(info.view, info.projection);
			buffer.DrawRendererList(info.handle);
		}
	}

Shadows Cleanup

Now we can drastically simplify Shadows. First, remove the separate ShadowResources ref struct and replace it with an inner Handles ref struct that wraps the specialized handles. Then remove everything that is no longer needed here and replace it with forwarding work to the specialized classes. Render still sets the generic shadow shader properties and it now also calculates the atlas sizes.

using Unity.Collections;
using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.RenderGraphModule;

public partial class Shadows
{
	public readonly ref struct Handles
	{
		public readonly DirectionalShadows.Handles directional;
		public readonly OtherShadows.Handles other;
	
		public ShadowHandles(
			DirectionalShadows.Handles directional,
			OtherShadows.Handles other)
		{
			this.directional = directional;
			this.other = other;
		}
	
		public void Use(IBaseRenderGraphBuilder builder)
		{
			directional.Use(builder);
			other.Use(builder);
		}
	}

	//…
	public const int maxTilesPerLight = 6;

	static readonly GlobalKeyword[] filterQualityKeywords = {
		GlobalKeyword.Create("_SHADOW_FILTER_MEDIUM"),
		GlobalKeyword.Create("_SHADOW_FILTER_HIGH"),
	};
	
	//…
	
	static readonly GlobalKeyword[] shadowMaskKeywords = {
		GlobalKeyword.Create("_SHADOW_MASK_ALWAYS"),
		GlobalKeyword.Create("_SHADOW_MASK_DISTANCE"),
	};

	static readonly int
		//…
		shadowAtlastSizeId = Shader.PropertyToID("_ShadowAtlasSize"),
		shadowDistanceFadeId = Shader.PropertyToID("_ShadowDistanceFade");
		//
	
	//…
	
	CullingResults cullingResults;

	ShadowSettings settings;

	bool useShadowMask;
	
	//…
	
	NativeArray<LightShadowCasterCullingInfo> cullingInfoPerLight;

	NativeArray<ShadowSplitData> shadowSplitDataPerLight;
	
	//…
	
	readonly DirectionalShadows directionalShadows = new();
	readonly OtherShadows otherShadows = new();

	public void Setup(CullingResults cullingResults, ShadowSettings settings)
	{
		this.cullingResults = cullingResults;
		this.settings = settings;
		//shadowedDirLightCount = shadowedOtherLightCount = 0;
		directionalShadows.Setup(settings);
		otherShadows.Setup(settings);
		useShadowMask = false;
		cullingInfoPerLight = new NativeArray<LightShadowCasterCullingInfo>(
			cullingResults.visibleLights.Length, Allocator.Temp);
		shadowSplitDataPerLight = new NativeArray<ShadowSplitData>(
			cullingInfoPerLight.Length * maxTilesPerLight,
			Allocator.Temp, NativeArrayOptions.UninitializedMemory);
	}

	public Vector4 ReserveDirectionalShadows(
		Light light, int visibleLightIndex) =>
		directionalShadows.ReserveDirectionalShadows(
			light, visibleLightIndex, cullingResults, ref useShadowMask);

	public Vector4 ReserveOtherShadows(Light light, int visibleLightIndex) =>
		otherShadows.ReserveShadows(
			light, visibleLightIndex, cullingResults, ref useShadowMask);
	
	//public ShadowResources GetResources(…) { … }
	
	public Handles GetHandles(
		RenderGraph renderGraph, IUnsafeRenderGraphBuilder builder) => new(
		directionalShadows.GetHandles(renderGraph, builder),
		otherShadows.GetHandles(renderGraph, builder));

	public void BuildRendererLists(
		RenderGraph renderGraph,
		IUnsafeRenderGraphBuilder builder,
		ScriptableRenderContext context)
	{
		bool culling = false;
		if (directionalShadows.HasLights)
		{
			directionalShadows.BuildRendererLists(
				renderGraph, builder, cullingResults,
				shadowSplitDataPerLight, cullingInfoPerLight);
			culling = true;
		}
		if (otherShadows.HasLights)
		{
			otherShadows.BuildRendererLists(
				renderGraph, builder, cullingResults,
				shadowSplitDataPerLight, cullingInfoPerLight);
			culling = true;
		}
		if (culling)
		{
			context.CullShadowCasters(
				cullingResults,
				new ShadowCastersCullingInfos
				{
					perLightInfos = cullingInfoPerLight,
					splitBuffer = shadowSplitDataPerLight
				});
		}
	}
	
	//…
	
	public void Render(UnsafeCommandBuffer buffer)
	{
		directionalShadows.RenderDirectionalShadows(buffer);
		otherShadows.RenderOtherShadows(buffer);
		SetKeywords(buffer,
			filterQualityKeywords, (int)settings.filterQuality - 1);
		//…
		SetKeywords(buffer, shadowMaskKeywords, useShadowMask ?
			QualitySettings.shadowmaskMode == ShadowmaskMode.Shadowmask ?
			0 : 1 : -1);
		float f = 1f - settings.directional.cascadeFade;
		buffer.SetGlobalVector(shadowDistanceFadeId, new Vector4(
			1f / settings.maxDistance, 1f / settings.distanceFade,
			1f / (1f - f * f)));
		//buffer.SetGlobalInt(…);
		int directionalAtlasSize = (int)settings.directional.atlasSize;
		int otherAtlasSize = (int)settings.other.atlasSize;
		buffer.SetGlobalVector(shadowAtlastSizeId, new Vector4(
			directionalAtlasSize, 1f / directionalAtlasSize,
			otherAtlasSize, 1f / otherAtlasSize));
	}
	
	//…
	
	public static Matrix4x4 ConvertToAtlasMatrix(
		Matrix4x4 m, Vector2 offset, float scale) { … }

	public static Vector2 SetTileViewport(
		UnsafeCommandBuffer buffer, int index, int split, float tileSize) { … }

	void SetKeywords(…) { … }
}

Final Changes

We have to change LightResources so it works with Shadows.Handles instead of the old resources. Besides that we'll leave the light resources unchanged.

public readonly ref struct LightResources
{
	public readonly BufferHandle
		directionalLightDataBuffer, otherLightDataBuffer, tilesBuffer;
	
	public readonly Shadows.Handles shadowHandles;

	public LightResources(
		BufferHandle directionalLightDataBuffer,
		BufferHandle otherLightDataBuffer,
		BufferHandle tilesBuffer,
		Shadows.Handles shadowHandles)
	{
		this.directionalLightDataBuffer = directionalLightDataBuffer;
		this.otherLightDataBuffer = otherLightDataBuffer;
		this.tilesBuffer = tilesBuffer;
		this.shadowHandles = shadowHandles;
	}
}

In LightingPass.Record we now have to build the renderer lists for the shadows explicitly and get the handles for them separately.

		pass.shadows.BuildRendererLists(renderGraph, builder, context);
		return new LightResources(
			pass.directionalLightDataBuffer,
			pass.otherLightDataBuffer,
			pass.tilesBuffer,
			pass.shadows.GetHandles(renderGraph, builder)); //, context));

Finally, we can simplify GeometryPass.Record, simply indicating that we use the shadow handles via Use instead of having to deal with the actual handles.

		builder.UseBuffer(lightData.directionalLightDataBuffer);
		builder.UseBuffer(lightData.otherLightDataBuffer);
		builder.UseBuffer(lightData.tilesBuffer);
		//builder.UseTexture(lightData.shadowResources.directionalAtlas);
		//builder.UseTexture(lightData.shadowResources.otherAtlas);
		//builder.UseBuffer(
			//lightData.shadowResources.directionalShadowCascadesBuffer);
		//builder.UseBuffer(
			//lightData.shadowResources.directionalShadowMatricesBuffer);
		//builder.UseBuffer(lightData.shadowResources.otherShadowDataBuffer);
		lightData.shadowHandles.Use(builder);

That wraps up a rather dry refactor process. With the specialized code isolated we paved the way for dedicated shadow passes, which we'll add in the future.

license repository PDF