Custom SRP 7.2
Separate Shadow Passes
This Unity tutorial is made with Unity 6000.5.8f1 and follows Custom SRP 7.1.0.
Isolating Shadows
Last time we split the shadow code into dedicated classes for directional and for other shadows. But both are still managed by the overall class for shadows, which in turn is still part of the lighting pass. This time we're going to pull the shadow code out of the lighting pass and introduce three shadow passes: one for direction shadows, one for other shadows, and one for the overall shadow stuff. We will do this incrementally, making sure that everything keeps working during the entire process. Specifically, at the end of each section shadows should be fully functional.
Pulling Shadows out of Lighting Pass
The first step of untangling shadows from LightingPass is to no longer create the Shadows object instance inside it. We will instead provide an instance to the pass when it is recorded. So get rid of the object creation.
//readonly Shadows shadows = new();Shadows shadows;
We will now pass a shadows instance that has already been set up to Setup, so it only needs to keep track of it. Thus the shadows settings are also no longer needed here.
void Setup( CullingResults cullingResults, Vector2Int attachmentSize, ForwardPlusSettings forwardPlusSettings,//ShadowSettings shadowSettings,Shadows shadows, int renderingLayerMask) { this.cullingResults = cullingResults;//shadows.Setup(cullingResults, shadowSettings);this.shadows = shadows; … }
Record just passes the shadows object to Setup.
public static LightResources Record( RenderGraph renderGraph, CullingResults cullingResults, Vector2Int attachmentSize, ForwardPlusSettings forwardPlusSettings,//ShadowSettings shadowSettings,Shadows shadows, int renderingLayerMask, ScriptableRenderContext context) { using IUnsafeRenderGraphBuilder builder = renderGraph.AddUnsafePass( sampler.name, out LightingPass pass, sampler); pass.Setup(cullingResults, attachmentSize, forwardPlusSettings, shadows, renderingLayerMask); … }
From now on CameraRenderer will create and hold on to the shadows object, just like it does for the post FX stack.
readonly Shadows shadows = new(); readonly PostFXStack postFXStack = new();
It has to set up the shadows in Render before recording the LightingPass.
shadows.Setup(cullingResults, shadowSettings); LightResources lightResources = LightingPass.Record( renderGraph, cullingResults, bufferSize, settings.forwardPlus, shadows, cameraSettings.maskLights ? cameraSettings.renderingLayerMask : -1, context);
Lighting Pass Handles
LightingPass should have as little as possible to do with shadows. It shouldn't need to know about the shadow handles at all. Currently it gets the shadow handles to bundle the LightResources, but ideally it should only return its own handles via Record. To make that possible we'll first give the pass its own Handles struct type to bundle its handles.
public readonly struct Handles
{
public readonly BufferHandle
directionalBuffer, otherBuffer, tilesBuffer;
public Handles(
BufferHandle directionalBuffer,
BufferHandle otherBuffer,
BufferHandle tilesBuffer)
{
this.directionalBuffer = directionalBuffer;
this.otherBuffer = otherBuffer;
this.tilesBuffer = tilesBuffer;
}
public void Use(IBaseRenderGraphBuilder builder)
{
builder.UseBuffer(directionalBuffer);
builder.UseBuffer(otherBuffer);
builder.UseBuffer(tilesBuffer);
}
}
Replace the separate fields with a single Handles field.
//BufferHandle//directionalLightDataBuffer, otherLightDataBuffer, tilesBuffer;Handles handles;
Update field references in Render so they work with the new handles field (not shown).
Change Record so it sets the handles and passes it directly to LightResources.
var handles = pass.handles = new Handles(
renderGraph.CreateBuffer(new BufferDesc(
maxDirectionalLightCount, DirectionalLightData.stride)
{
name = "Directional Light Data"
}),
//builder.UseBuffer(…);
renderGraph.CreateBuffer(new BufferDesc(
maxOtherLightCount, OtherLightData.stride)
{
name = "Other Light Data"
}),
//builder.UseBuffer(…);
renderGraph.CreateBuffer(new BufferDesc(
pass.TileCount * pass.maxTileDataSize, 4
)
{
name = "Forward+ Tiles"
})
);
builder.UseBuffer(handles.directionalBuffer, AccessFlags.WriteAll);
builder.UseBuffer(handles.otherBuffer, AccessFlags.WriteAll);
builder.UseBuffer(handles.tilesBuffer, AccessFlags.WriteAll);
builder.SetRenderFunc(
static (pass, context) => pass.Render(context));
builder.AllowPassCulling(false);
pass.shadows.BuildRendererLists(renderGraph, builder, context);
return new LightResources(
handles,
pass.shadows.GetHandles(renderGraph, builder));
Adapt LightResources so it works with LightingPass.Handles. Also give it a convenient Use method, like for the shadow handles.
//public readonly BufferHandle//directionalLightDataBuffer, otherLightDataBuffer, tilesBuffer;public readonly LightingPass.Handles lightHandles; public readonly Shadows.Handles shadowHandles; public LightResources( LightingPass.Handles lightHandles, Shadows.Handles shadowHandles) { this.lightHandles = lightHandles; this.shadowHandles = shadowHandles; } public void Use(IBaseRenderGraphBuilder builder) { lightHandles.Use(builder); shadowHandles.Use(builder); }
Now we can simplify GeometryPass.Record.
//builder.UseBuffer(lightData.directionalLightDataBuffer);//builder.UseBuffer(lightData.otherLightDataBuffer);//builder.UseBuffer(lightData.tilesBuffer);//lightData.shadowHandles.Use(builder);lightData.Use(builder);
And we have to update DebugPass.Record to use the new handles layout.
builder.UseBuffer(lightData.lightHandles.tilesBuffer);
Shadows Pass
It is time to introduce a ShadowsPass class that forwards all work to the Shadows object. It starts as a simple unsafe pass that cannot be culled. It renders shadows in its Render method. It builds the renderer lists and returns the shadow handles in its Record method.
using UnityEngine.Rendering;
using UnityEngine.Rendering.RenderGraphModule;
public class ShadowsPass
{
static readonly ProfilingSampler sampler = new("Shadows");
Shadows shadows;
void Render(UnsafeGraphContext context) {
shadows.Render(context.cmd);
}
public static Shadows.Handles Record(
RenderGraph renderGraph,
Shadows shadows,
ScriptableRenderContext context)
{
using IUnsafeRenderGraphBuilder builder = renderGraph.AddUnsafePass(
sampler.name, out ShadowsPass pass, sampler);
pass.shadows = shadows;
builder.AllowPassCulling(false);
builder.SetRenderFunc(
static (pass, context) => pass.Render(context));
shadows.BuildRendererLists(renderGraph, builder, context);
return shadows.GetHandles(renderGraph, builder);
}
}
From now on LightingPass will no longer keep track of shadows, so remove the field for it. It still needs a reference to it in SetupLights to reserve shadows, so pass it to that method via a parameter instead.
//Shadows shadows;… void Setup(…) { this.cullingResults = cullingResults;//this.shadows = shadows;… SetupLights(renderingLayerMask, shadows); } … void SetupLights(int renderingLayerMask, Shadows shadows) { … }
It must no longer build the shadow rendering lists in Record and only returns its own handles.
public static Handles Record(…)
{
…
//pass.shadows.BuildRendererLists(renderGraph, builder, context);
return handles;
}
CameraRenderer.Render now has to create the LightResources, using the handles its get from recording LightingPass and the new ShadowsPass.
shadows.Setup(cullingResults, shadowSettings);//LightResources lightResources =var lightResources = new LightResources( LightingPass.Record( renderGraph, cullingResults, bufferSize, settings.forwardPlus, shadows, cameraSettings.maskLights ? cameraSettings.renderingLayerMask : -1, context), ShadowsPass.Record(renderGraph, shadows, context));
Migrating Code to Shadows Pass
We finally have a dedicated pass for shadows, but it just forwards everything to the Shadows object. To be able to create dedicated passes for directional and for other shadows we have to pull code into the pass, which we can then segregate.
Ideally Shadows is just a convenient bundle for DirectionalShadows and OtherShadows and we go directly to those for the actual work. To make that possible they have to become public.
public readonly DirectionalShadows directionalShadows = new(); public readonly OtherShadows otherShadows = new();
Building the Renderer Lists
We start by copying BuildRendererLists from Shadows to ShadowsPass and update the references to the directional and other shadows. The new method is private, because it's only used by the pass itself. We only need an additional parameter for the culling results. We can create the required temporary native arrays in the method itself.
//publicvoid BuildRendererLists( RenderGraph renderGraph, CullingResults cullingResults, IUnsafeRenderGraphBuilder builder, ScriptableRenderContext context) { var cullingInfoPerLight = new NativeArray<LightShadowCasterCullingInfo>( cullingResults.visibleLights.Length, Allocator.Temp); var shadowSplitDataPerLight = new NativeArray<ShadowSplitData>( cullingInfoPerLight.Length * Shadows.maxTilesPerLight, Allocator.Temp, NativeArrayOptions.UninitializedMemory); bool culling = false; if (shadows.directionalShadows.HasLights) { shadows.directionalShadows.BuildRendererLists( renderGraph, builder, cullingResults, shadowSplitDataPerLight, cullingInfoPerLight); culling = true; } if (shadows.otherShadows.HasLights) { shadows.otherShadows.BuildRendererLists( renderGraph, builder, cullingResults, shadowSplitDataPerLight, cullingInfoPerLight); culling = true; } if (culling) { context.CullShadowCasters( cullingResults, new ShadowCastersCullingInfos { perLightInfos = cullingInfoPerLight, splitBuffer = shadowSplitDataPerLight }); } }
Now Record will invoke BuildRendererLists on the pass instead of on Shadows, for which it also needs the culling results.
public static Shadows.Handles Record(
RenderGraph renderGraph,
CullingResults cullingResults,
Shadows shadows,
ScriptableRenderContext context)
{
…
pass.BuildRendererLists(renderGraph, cullingResults, builder, context);
return shadows.GetHandles(renderGraph, builder);
}
Provide the culling results in CameraRenderer.Render.
ShadowsPass.Record( renderGraph, cullingResults, shadows, context));
Remove the BuildRendererLists method and accompanying fields from Shadows. It stored the native arrays in fields because in the past it did all the work itself instead of forwarding to the specialized classes.
//using Unity.Collections;…//NativeArray<LightShadowCasterCullingInfo> cullingInfoPerLight;//NativeArray<ShadowSplitData> shadowSplitDataPerLight;… public void Setup(CullingResults cullingResults, ShadowSettings settings) { …//cullingInfoPerLight = …//shadowSplitDataPerLight = …} …//public void BuildRendererLists(…) { … }
Passing Culling Infos
We can simplify passing around the native arrays used for building the renderer lists by immediately bundling them in ShadowsPass.BuildRendererLists and passing along the ShadowCastersCullingInfos struct value.
int visibleLightCount = cullingResults.visibleLights.Length;
var cullingInfos = new ShadowCastersCullingInfos
{
perLightInfos = new NativeArray<LightShadowCasterCullingInfo>(
visibleLightCount, Allocator.Temp),
splitBuffer = new NativeArray<ShadowSplitData>(
visibleLightCount * Shadows.maxTilesPerLight,
Allocator.Temp, NativeArrayOptions.UninitializedMemory)
};
bool culling = false;
if (shadows.directionalShadows.HasLights)
{
shadows.directionalShadows.BuildRendererLists(
renderGraph, builder, cullingResults, cullingInfos);
culling = true;
}
if (shadows.otherShadows.HasLights)
{
shadows.otherShadows.BuildRendererLists(
renderGraph, builder, cullingResults, cullingInfos);
culling = true;
}
if (culling)
{
context.CullShadowCasters(cullingResults, cullingInfos);
}
Adapt the code in DirectionalShadows to work with that, simplifying it.
//using Unity.Collections;… public void BuildRendererLists( RenderGraph renderGraph, IUnsafeRenderGraphBuilder builder, CullingResults cullingResults,//NativeArray<ShadowSplitData> shadowSplitDataPerLight,//NativeArray<LightShadowCasterCullingInfo> cullingInfoPerLightShadowCastersCullingInfos cullingInfos) { … for (int i = 0; i < lightCount; i++) { BuildRendererLists( i, renderGraph, builder, cullingResults, cullingInfos); } } void BuildRendererLists( int index, RenderGraph renderGraph, IUnsafeRenderGraphBuilder builder, CullingResults cullingResults,//NativeArray<ShadowSplitData> shadowSplitDataPerLight,//NativeArray<LightShadowCasterCullingInfo> cullingInfoPerLightShadowCastersCullingInfos cullingInfos) { … for (int i = 0; i < cascadeCount; i++) { … cullingInfos.splitBuffer[splitOffset + i] = splitData; … } cullingInfos.perLightInfos[light.visibleLightIndex] = … }
Do the same for OtherShadows (not shown).
Shadow Mask Usage
We have awkward code that uses a ref parameter to make Shadows track whether the shadow mask is used. We get rid of the ref parameter in DirectionalShadows by replacing it with a public get and private set UsesShadowMask property that keeps track of its own usage.
public bool UsesShadowMask { get; private set; }
public void Setup(ShadowSettings settings)
{
…
UsesShadowMask = false;
}
public Vector4 ReserveDirectionalShadows(
Light light,
int visibleLightIndex,
CullingResults cullingResults)
//ref bool useShadowMask)
{
…
UsesShadowMask = true;
…
}
Make this change to OtherShadows as well (not shown).
Eliminate the useShadowMask field from Shadows.
//bool useShadowMask;… public void Setup(CullingResults cullingResults, ShadowSettings settings) { …//useShadowMask = false;} 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);
We instead check whether the specialized classes use the shadow mask in Render.
SetKeywords(buffer, shadowMaskKeywords, directionalShadows.UsesShadowMask || otherShadows.UsesShadowMask ? QualitySettings.shadowmaskMode == ShadowmaskMode.Shadowmask ? 0 : 1 : -1);
Direct Shadow Reservations
Now that Shadows no longer keeps track of the shadow mask usage the ReserveDirectionalShadows and ReserveOtherShadows methods only forward invocations verbatim. So let's get rid of them.
//public Vector4 ReserveDirectionalShadows(…) => …//public Vector4 ReserveOtherShadows(…) => …
While we're at it, let's shorten the name of DirectionalShadows.ReserveDirectionalShadows to just ReserveShadows.
public Vector4 ReserveShadows(…) { … }
In LightingPass replace the invocation of shadows.ReserveDirectionalShadows with shadows.directionalShadows.ReserveShadows, adding the culling results as an extra argument, then do the same for other shadows (not shown).
Now Shadows no longer needs the culling results.
//CullingResults cullingResults;…//public void Setup(CullingResults cullingResults, ShadowSettings settings)public void Setup(ShadowSettings settings) {//this.cullingResults = cullingResults;… }
And we no longer need to provide it in CameraRenderer.Render when setting up the shadows.
//shadows.Setup(cullingResults, shadowSettings);shadows.Setup(shadowSettings);
Shadow Pass Handles
The last thing that we move over from Shadows to ShadowsPass are the handles. Copy the struct type and directly create and return it in Record.
public readonly ref struct Handles { … }
…
public static Handles Record(…)
{
…
return new Handles(
shadows.directionalShadows.GetHandles(renderGraph, builder),
shadows.otherShadows.GetHandles(renderGraph, builder));
}
Remove the struct type and the GetHandles method from Shadows. By the way, it stopped needing to be a partial class a while ago already.
//using UnityEngine.Rendering.RenderGraphModule;//public partial class Shadowspublic class Shadows {//public readonly ref struct Handles { … }…//public Handles GetHandles(…) => …… }
Update the shadow handles type in LightResources to match the new type.
public readonly ShadowsPass.Handles shadowHandles;
public LightResources(
LightingPass.Handles lightHandles,
ShadowsPass.Handles shadowHandles) { … }
Directional and Other Passes
We have finally reached the point where we can create the dedicated shadow passes. ShadowsPass will take care of recording these passes while it records itself, to hide the complexity of shadows from the camera renderer.
Pulling Apart Shadow Code
To pull apart the code for directional shadows and for other shadows we have to rearrange the code in ShadowPass a bit. Two more steps are required. First, we pull the rendering code for directional and other shadows directly in Render.
void Render(UnsafeGraphContext context)
{
shadows.directionalShadows.RenderDirectionalShadows(context.cmd);
shadows.otherShadows.RenderOtherShadows(context.cmd);
shadows.Render(context.cmd);
}
And then remove it from Shadows.Render.
//directionalShadows.RenderDirectionalShadows(buffer);//otherShadows.RenderOtherShadows(buffer);
Second, we pull the code from BuildRendererLists into Record. This is needed because each pass requires its own builder for that, but only a single builder can be used at the same time. For now we start with creating the culling infos struct value, then build the shadows pass, then use its builder to build the renderer lists.
//void BuildRendererLists(…) { … }public static Handles Record( RenderGraph renderGraph, CullingResults cullingResults, Shadows shadows, ScriptableRenderContext context) { int visibleLightCount = cullingResults.visibleLights.Length; var cullingInfos = new ShadowCastersCullingInfos { … } using IUnsafeRenderGraphBuilder builder = renderGraph.AddUnsafePass( sampler.name, out ShadowsPass pass, sampler); pass.shadows = shadows; builder.AllowPassCulling(false); builder.SetRenderFunc<ShadowsPass>( static (pass, context) => pass.Render(context));//shadows.BuildRendererLists(renderGraph, builder, context);bool culling = false; if (shadows.directionalShadows.HasLights) { … } if (shadows.otherShadows.HasLights) { … } if (culling) { … } return new Handles( shadows.directionalShadows.GetHandles(renderGraph, builder), shadows.otherShadows.GetHandles(renderGraph, builder)); }
Directional Shadows Pass
It is time to create a dedicated DirectionalShadowsPass class. We keep it simple for now, just an unsafe pass that forwards rendering, building the renderer lists, and getting the handles to the DirectionalShadows object.
using UnityEngine.Rendering;
using UnityEngine.Rendering.RenderGraphModule;
public class DirectionalShadowsPass
{
static readonly ProfilingSampler sampler = new("Directional Shadows");
DirectionalShadows shadows;
void Render(UnsafeGraphContext context) =>
shadows.RenderDirectionalShadows(context.cmd);
public static DirectionalShadows.Handles Record(
RenderGraph renderGraph,
CullingResults cullingResults,
ShadowCastersCullingInfos cullingInfos,
Shadows shadows)
{
using IUnsafeRenderGraphBuilder builder = renderGraph.AddUnsafePass(
sampler.name, out DirectionalShadowsPass pass, sampler);
pass.shadows = shadows.directionalShadows;
builder.SetRenderFunc<DirectionalShadowsPass>(
static (pass, context) => pass.Render(context));
if (pass.shadows.HasLights)
{
pass.shadows.BuildRendererLists(
renderGraph, builder, cullingResults, cullingInfos);
}
return pass.shadows.GetHandles(renderGraph, builder);
}
}
Then record DirectionalShadowsPass in ShadowsPass.Record before building its own pass. Skip building the renderer lists for directional shadows after that, because that's already been done. The directional handles are also already known.
int visibleLightCount = cullingResults.visibleLights.Length;
var cullingInfos = new ShadowCastersCullingInfos { … }
DirectionalShadows.Handles directionalHandles =
DirectionalShadowsPass.Record(
renderGraph, cullingResults, cullingInfos, shadows);
using IUnsafeRenderGraphBuilder builder = renderGraph.AddUnsafePass(
sampler.name, out ShadowsPass pass, sampler);
pass.shadows = shadows;
builder.AllowPassCulling(false);
builder.SetRenderFunc<ShadowsPass>(
static (pass, context) => pass.Render(context));
bool culling = shadows.directionalShadows.HasLights;
//if (shadows.directionalShadows.HasLights) { … }
if (shadows.otherShadows.HasLights) { … }
if (culling) { … }
return new Handles(
directionalHandles,
shadows.otherShadows.GetHandles(renderGraph, builder));
}
Also remove directional shadows rendering from Render.
void Render(UnsafeGraphContext context)
{
//shadows.directionalShadows.RenderDirectionalShadows(context.cmd);
shadows.otherShadows.RenderOtherShadows(context.cmd);
shadows.Render(context.cmd);
}
Other Shadows Pass
Create OtherShadowsPass just like DirectionalShadowsPass, but for OtherShadows instead.
using UnityEngine.Rendering;
using UnityEngine.Rendering.RenderGraphModule;
public class OtherShadowsPass
{
static readonly ProfilingSampler sampler = new("Other Shadows");
OtherShadows shadows;
void Render(UnsafeGraphContext context) =>
shadows.RenderOtherShadows(context.cmd);
public static OtherShadows.Handles Record(
RenderGraph renderGraph,
CullingResults cullingResults,
ShadowCastersCullingInfos cullingInfos,
Shadows shadows)
{
using IUnsafeRenderGraphBuilder builder = renderGraph.AddUnsafePass(
sampler.name, out OtherShadowsPass pass, sampler);
pass.shadows = shadows.otherShadows;
builder.SetRenderFunc<OtherShadowsPass>(
static (pass, context) => pass.Render(context));
if (pass.shadows.HasLights)
{
pass.shadows.BuildRendererLists(
renderGraph, builder, cullingResults, cullingInfos);
}
return pass.shadows.GetHandles(renderGraph, builder);
}
}
Now we can simplify ShadowsPass.Record further, also recording OtherShadowsPass before building its own pass. We can now start culling the shadow casters before building its own shadows pass.
var handles = new Handles(
DirectionalShadowsPass.Record(
renderGraph, cullingResults, cullingInfos, shadows),
OtherShadowsPass.Record(
renderGraph, cullingResults, cullingInfos, shadows));
if (shadows.directionalShadows.HasLights ||
shadows.otherShadows.HasLights)
{
context.CullShadowCasters(cullingResults, cullingInfos);
}
using IUnsafeRenderGraphBuilder builder = renderGraph.AddUnsafePass(
sampler.name, out ShadowsPass pass, sampler);
pass.shadows = shadows;
builder.AllowPassCulling(false);
builder.SetRenderFunc<ShadowsPass>(
static (pass, context) => pass.Render(context));
//bool culling = shadows.directionalShadows.HasLights;
//if (shadows.otherShadows.HasLights) { … }
//if (culling) { … }
return handles;
Finally, also stop rendering other shadows in ShadowsPass.Record.
void Render(UnsafeGraphContext context)
{
//shadows.otherShadows.RenderOtherShadows(context.cmd);
shadows.Render(context.cmd);
}
The shadow work is now split into three passes, separated from the pass for lighting. We'll improve these passes further in the future.