We are in the process of upgrading our project to the Unity 6 beta, and I’m trying to rewrite the Outline Renderer feature that we’ve been using to work with Render Graph.
This is the repo:
Robinseibold/Unity-URP-Outlines: A custom renderer feature for screen space outlines (github.com)
I had some initial luck getting the passes set up using the latest Render Graph samples. I took the BlitRendererFeature.cs example and started plugging in the two shaders required, a full screen shader graph shader that outputs screen space normals, and a full screen shader graph that processes the screen space normals camera data to produce the final outline effect. So far so good:
I’m struggling with implementing Layer Masks to isolate the effect to only selected objects. The original uses ScriptableRenderContext.DrawRenderers:
using (new ProfilingScope(cmd, new ProfilingSampler("ScreenSpaceNormalsTextureCreation")))
{
context.ExecuteCommandBuffer(cmd);
cmd.Clear();
DrawingSettings drawSettings = CreateDrawingSettings(shaderTagIdList, ref renderingData, renderingData.cameraData.defaultOpaqueSortFlags);
drawSettings.overrideMaterial = normalsMaterial;
DrawingSettings occluderSettings = drawSettings;
context.DrawRenderers(renderingData.cullResults, ref drawSettings, ref m_filteringSettings);
}
The result of the old method viewed in the Frame Debugger shows the selected objects drawn with the screen space normal shader applied, over black. This temp texture is what the outline shader processes, which is finally composited back on top of the original color buffer.
I started using the DrawRendererList from the example:
rgContext.cmd.ClearRenderTarget(RTClearFlags.Color, Color.black, 1, 0);
rgContext.cmd.DrawRendererList(data.rendererListHandle);
Now I am able to get the filtered objects rendered to the temp texture, but I can’t figure out how to use the DrawRendererList in combination with the Screen Space normals applied before the Outline pass runs. The closest I could get was this:
Where the Outline shader is correctly applied to the masked object, but does not have the screenspace normal shader applied. It seems that I can’t call DrawRendererList and use Blitter.BlitTexture within the Execute pass method to apply the material transformation to the filtered objects.
This is the part of my modified sample code where I’m using the renderer list handle:
static void ExecutePass(PassData data, RasterGraphContext rgContext)
{
// We can use blit with or without a material both using the static scaleBias to avoid reallocations.
if (data.material == null)
{
Blitter.BlitTexture(rgContext.cmd, data.source, scaleBias, 0, false);
}
else
{
if (data.rendererListHandle.IsValid())
{
rgContext.cmd.ClearRenderTarget(RTClearFlags.Color, Color.black, 1, 0);
rgContext.cmd.DrawRendererList(data.rendererListHandle);
}
Blitter.BlitTexture(rgContext.cmd, data.source, scaleBias, data.material, 0);
}
}
For more context, this is my modified RecordFullScreenPass method that runs for the Screen Space Normals/Outline shader. I’m using a global texture reference to update the Outline Shader Graph shader Input:
public void RecordFullScreenPass(RenderGraph renderGraph, string passName, Material material, ContextContainer context, LayerMask layerMask)
{
// Checks if the data is previously initialized and if the material is valid.
if (!texture.IsValid() || material == null)
{
Debug.LogWarning("Invalid input texture handle, will skip fullscreen pass.");
return;
}
// Starts the recording of the render graph pass given the name of the pass
// and outputting the data used to pass data to the execution of the render function.
using (var builder = renderGraph.AddRasterRenderPass<PassData>(passName, out var passData))
{
passData.layerMask = layerMask;
// Switching the active texture handles to avoid blit. If we want the most recent
// texture we can simply look at the variable texture
m_IsFront = !m_IsFront;
// Setting data to be used when executing the render function.
passData.material = material;
passData.source = texture;
// Swap the active texture.
passData.destination = m_IsFront ? m_TextureHandleOutlineFront : m_TextureHandleNormalsBack;
// Sets input attachment to BlitData's old active texture.
builder.UseTexture(passData.source);
// Sets output attachment 0 to BitData's new active texture.
builder.SetRenderAttachment(passData.destination, 0);
// Update the texture after switching.
texture = passData.destination;
if (!m_IsFront)
{
Debug.Log($"InitRenderLists: {passName}");
InitRendererLists(context, ref passData, renderGraph);
builder.UseRendererList(passData.rendererListHandle);
};
// Sets the render function.
builder.SetRenderFunc((PassData passData, RasterGraphContext rgContext) => ExecutePass(passData, rgContext));
builder.SetGlobalTextureAfterPass(m_TextureHandleNormalsBack, Shader.PropertyToID("_SceneViewSpaceNormals"));
}
}
I can’t tell if I’m even close or if my approach is just wrong. I think this just needs to be set up differently. Maybe the DrawRendererList needs to happen before this Execute pass?
Appreciate if anyone can steer me in the right direction. Thank you!

