Understanding Unity’s RenderGraph Samples
Hi. This is an article based on the new upcoming series of Render graph video tutorials, the links can be found in the resources section at the bottom. In this article we will look at Unity’s Render graph samples in detail.
The video series is now live.
<< WATCH THE PLAYLIST >>
The RenderGraph API first appeared with Unity 6.0. It is a way of extending URP with custom rendering features. To help developers understand RenderGraph Unity provides several samples. To install these, open package manager from a URP project. In the left sidebar select ‘Unity Registry’. In the search bar type ‘universal’. Select ‘universal render pipeline’. Then in the right panel select ‘samples’. Finally, choose ‘import’ for the ‘URP RenderGraph Samples’. Alternatively you can use the GitHub Unity Project linked to this video.
Despite the name, RenderGraph isn’t a node-based visual editor like ShaderGraph. Instead it’s a programming interface. Its name originated from how it stores the data about the render pipeline. This new programming API works by implementing custom rendering passes and adding them to the Render Graph.
If you want an introduction to Render Graph, don’t miss the Understanding the Render Graph viewer article and the resources at the bottom of this article.
Why did Unity create RenderGraph in the first place?
Before RenderGraph, ScriptableRenderPasses were used to implement the rendering logic using various methods. While this approach gave programmers a lot of freedom, it completely broke down at scale. Larger projects became fragile and hard to debug. There was no organized way to store references to the GPU resources, which resulted in a project that was difficult to maintain. RenderGraph not only solves that but because of the way it handles resources and passes it improves performance and memory management and thanks to the new analysis window RenderGraph Viewer, it is much easier to debug.
Understanding RenderGraph
The Unity RenderGraph samples provide a great way to learn to get the best from the new approach to custom rendering. For those new to the Universal Render Pipeline (URP), it requires two settings assets to control settings and custom rendering. We recommend this video, for those new to the pipeline. Custom rendering is handled by adding a Renderer Feature to the list of Renderer Features used by the Universal Renderer Data asset.
To create a new Renderer Feature right-click in a folder in the Project window and choose Create > Scripting > URP Renderer Feature Script. Give your script a name and you will have a boiler plate renderer feature ready for you to add your custom code.
Once you have a Renderer Feature you can add it to the Universal Renderer Data. In the image above I’ve added the Copy Render Feature from the Samples. It is found in the Samples > Universal Render Pipeline > 17.x > URP RenderGraph Samples folder. The Samples don’t include scene files, they just include the Renderer Features and a few materials and shaders. The GitHub repo previously mentioned is used throughout the video to illustrate the effect of each sample that does include scene files. Each scene uses a dedicated pair of URP settings that have the Renderer Feature assigned. There is a repository with the samples shown in the video. You can take your pick of which approach you prefer as you follow along. The rest of this video will explain each Sample in turn.
Blit
In the repo the Scenes > Blit folder includes a Scene file also called Blit. Alternatively add the Copy Render Feature we saw a moment ago to the Universal Renderer Data.
This Sample appears to do nothing, the Game View is unchanged if we toggle the activation of the Renderer Feature. But if you open Window > Analysis > RenderGraph Viewer. This useful window shows the various passes horizontally, left to right and the resources used top to bottom.
When Copy Render Feature is inactive the pass following Draw Opaque Objects is Draw Transparent Objects. When Copy Render Feature is active, there are two additional passes between Draw Opaque Objects and Draw Transparent Objects.
The first extra pass is Copy Active Color Texture to Temp Texture. Scanning down the column for this we see that _CameraTargetAttachment is green with an F in a grey circle. This indicates read access using FramebufferFetch, more about this later when we look at the Framebuffer Fetch sample. Further down we see CameraColor-CopyRenderPass. This is red indicating write access. The current active color texture is _CameraTargetAttachment and this is copied to CameraColor-CopyRenderPass using a Blit operation, a fast block copy. The next pass Copy Temp Texture to Active Color Texture simply copies CameraColor-CopyRenderPass back to _CameraTargetAttachment as we can see by the colors of the blocks, green for read and red for write.
Now you’re familiar with what this Renderer Feature does, we’ll take a look at the code. Open CopyRenderFeature.cs from the Samples > Universal Render Pipeline > 17.x > URP RenderGraph Samples > Blit folder.
using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.RenderGraphModule;
using UnityEngine.Rendering.RenderGraphModule.Util;
using UnityEngine.Rendering.Universal;
// This example copies the active color texture to a new texture. This example is for API demonstrative purposes,
// so the new texture is not used anywhere else in the frame, you can use the frame debugger to verify its contents.
public class CopyRenderFeature : ScriptableRendererFeature
{
class CopyRenderPass : ScriptableRenderPass
{
public CopyRenderPass()
{
// The pass will read the current color texture. That needs to be an intermediate texture. It's not supported to use the BackBuffer as input texture.
// By setting this property, URP will automatically create an intermediate texture.
requiresIntermediateTexture = true;
}
// This is where the renderGraph handle can be accessed.
// Each ScriptableRenderPass can use the RenderGraph handle to add multiple render passes to the render graph
public override void RecordRenderGraph(RenderGraph renderGraph, ContextContainer frameData)
{
//...
}
}
CopyRenderPass m_CopyRenderPass;
/// <inheritdoc/>
public override void Create()
{
m_CopyRenderPass = new CopyRenderPass();
// Configures where the render pass should be injected.
m_CopyRenderPass.renderPassEvent = RenderPassEvent.AfterRenderingOpaques;
}
// Here you can inject one or multiple render passes in the renderer.
// This method is called when setting up the renderer once per-camera.
public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData)
{
renderer.EnqueuePass(m_CopyRenderPass);
}
}
A Renderer Feature extends the ScriptableRendererFeature class. The usual form is to include a class that extends ScriptableRenderPass in the same file. Here this is given the name CopyRenderPass. This pass has a constructor without any parameters. The constructor sets the requiresIntermediateTexture flag. Indicating it will be using the active color texture and so URP will need to automatically create an intermediate texture to avoid using the BackBuffer as an input texture.
A pass that extends the ScriptableRenderPass should define a custom RecordRenderGraph method. This is where the custom rendering is constructed.
public override void RecordRenderGraph(RenderGraph renderGraph, ContextContainer frameData)
{
// UniversalResourceData contains all the texture handles used by the renderer, including the active color and depth textures
// The active color and depth textures are the main color and depth buffers that the camera renders into
UniversalResourceData resourceData = frameData.Get<UniversalResourceData>();
// The destination texture is created here,
// the texture is created with the same dimensions as the active color texture
var source = resourceData.activeColorTexture;
var destinationDesc = renderGraph.GetTextureDesc(source);
destinationDesc.name = $"CameraColor-{passName}";
destinationDesc.clearBuffer = false;
TextureHandle destination = renderGraph.CreateTexture(destinationDesc);
…
Here we use the resourceData passed as a parameter to the method to access the activeColorTexture. Then we get the format and size of this texture. Customise the name and use this descriptor to create a TextureHandle. The beauty of Render Graph is it handles and optimizes memory bandwidth and usage. A TextureHandle does not initially mean memory for the texture has been allocated. A TextureHandle contains a RTHandle that will initially be unassigned. Developers sometimes look at the code in the RecordRenderGraph method and mistakenly assume that this is where the memory allocation takes place and think it doesn’t make sense to create textures in a method that is called on each frame update. Instead what is happening is you are describing your intentions and then RenderGraph can decide when to allocate memory and resources and where passes can be merged.
If we look at RenderGraph Viewer, notice that the five passes from Draw Opaque Objects to Setup Post FX passes are all merged. The continuous blue line or white blocks when mouse over indicate this. AddCopyPass can be safely merged.
…
if (RenderGraphUtils.CanAddCopyPassMSAA())
{
// This simple pass copies the active color texture to a new texture.
renderGraph.AddCopyPass(resourceData.activeColorTexture, destination, passName: "Copy Active Color Texture to Temp Texture");
// Need to copy back - otherwise the pass gets culled since the result of the previous copy is not read. This is just for demonstration purposes.
renderGraph.AddCopyPass(destination, resourceData.activeColorTexture, passName: "Copy Temp Texture to Active Color Texture");
}
else
{
Debug.Log("Can't add the copy pass due to MSAA");
}
}
We check whether the current platform allows for copying with Multi Sampled Anti Aliasing. If so we add two copy passes. From active color texture to temporary texture and back again.
This sample shows the most basic kind of RenderGraph pass — allocating a transient texture, reading from a source, and writing to a target.
Blit with material
The URP RenderGraph Samples > Blit with Material folder contains BlitAndSwapColorRendererFeature.cs. This takes a different approach to the previous Blit sample. Opening the Blit with Material Scene in the Scenes > Blit with Material folder. There is a script attached to the Main Camera for each scene in the RenderSamples repo that assigns a new URP settings asset and renderer. The script AutoLoadPipelineAsset is found in the Scripts folder and is useful when you need to switch settings as you load a different scene.
The first thing we notice with this Scene is the screen has a green tint. The green tint comes from the shader selected, BlitWithMaterial.shader, which is found in the same folder as the Renderer Feature. Again as a first step let’s look at the RenderGraph Viewer. Make sure you have Pass Filter Unsafe Pass enabled, because AddBlitPass, used in this script, is an unsafe pass.
Take a look at the BlitAndSwapColorPass. Resource _CameraTargetAttachment is green indicating read access and CameraColor-BlitAndSwapColorPass resource is red indicating write access. But no F in a gray circle so reading is not using FramebufferFetch, and notice no merging is taking place. Unsafe passes cannot be merged.
Now let’s look at the code.
public override void RecordRenderGraph(RenderGraph renderGraph, ContextContainer frameData)
{
var resourceData = frameData.Get<UniversalResourceData>();
…
var source = resourceData.activeColorTexture;
var destinationDesc = source.GetDescriptor(renderGraph);
destinationDesc.name = $"CameraColor-{m_PassName}";
destinationDesc.clearBuffer = false;
TextureHandle destination = renderGraph.CreateTexture(destinationDesc);
RenderGraphUtils.BlitMaterialParameters para = new(source, destination, m_BlitMaterial, 0);
renderGraph.AddBlitPass(para, passName: m_PassName);
resourceData.cameraColor = destination;
}
Take a look at the RecordRenderGraph method. Again we create a TextureHandle using the active color texture descriptor to ensure it is the same pixel size and format. Rather than using AddCopyPass this time we create a BlitMaterialParameters object. This takes a source texture, a destination texture, a material and a material pass index. We then use the renderGraph method AddBlitPass which takes the BlitMaterialParameters object as parameter one and a pass name as parameter two. Now when blitting, the material is used when copying. Finally, to avoid a second pass to copy the temporary texture back to the active color texture. Instead we change the cameraColor which is the active color texture to the destination texture. This ensures that the temporary texture is not disposed of and instead acts as the current active color texture.
Framebuffer Fetch
The idea of Framebuffer Fetch is you sample from the buffer stored in the GPU without copying it to main memory. This not only saves memory but it is a great deal faster. As we saw in the Blit sample, AddCopyPass uses Framebuffer Fetch under the hood, but this doesn’t allow for any manipulation of the source when copying to the destination. The Blit with Material sample that uses AddBlitPass does not use Framebuffer Fetch as we saw in the RenderGraph Viewer. The Framebuffer Fetch sample shows how we can manipulate the data when copying from source to destination while also using the GPU buffer. Take a look at FrameBufferFetchRenderFeature.cs from the URP RenderGraph Samples > FramebufferFetch folder.
public override void RecordRenderGraph(RenderGraph renderGraph, ContextContainer frameData)
{
…
var source = resourceData.activeColorTexture;
var destinationDesc = renderGraph.GetTextureDesc(source);
destinationDesc.name = "FBFetchDestTexture";
destinationDesc.clearBuffer = false;
if (destinationDesc.msaaSamples == MSAASamples.None || RenderGraphUtils.CanAddCopyPassMSAA())
{
TextureHandle fbFetchDestination = renderGraph.CreateTexture(destinationDesc);
FBFetchPass(renderGraph, frameData, source, fbFetchDestination, destinationDesc.msaaSamples != MSAASamples.None);
// Copy back the FBF output to the camera color to easily see the result in the game view
// This copy pass also uses FBF under the hood. All the passes should be merged this way and the destination attachment should be memoryless (no load/store of memory).
renderGraph.AddCopyPass(fbFetchDestination, source, passName: "Copy Back FF Destination (also using FBF)");
}
else
{
Debug.Log("Can't add the FBF pass and the copy pass due to MSAA");
}
}
Let’s start with the RecordRenderGraph method. We get a source descriptor from the current activeColorTexture. Then create a TextureHandle from this using renderGraph.CreateTexture. Then we add a call to a custom method FBFetchPass. It receives 5 parameters: the renderGraph, the frameData, the source, the TextureHandle just created, fbFetchDestination and a boolean that is true if msaaSamples is enabled. Finally we copy the result back to the source using a copy pass.
private void FBFetchPass(RenderGraph renderGraph, ContextContainer frameData, TextureHandle source, TextureHandle destination, bool useMSAA)
{
string passName = "FrameBufferFetchPass";
// This simple pass copies the target of the previous pass … using (var builder = renderGraph.AddRasterRenderPass<PassData>(passName, out var passData))
{
// Fill the pass data
passData.material = m_FBFetchMaterial;
passData.useMSAA = useMSAA;
// We declare the src as input attachment. This is required for framebuffer fetch.
builder.SetInputAttachment(source, 0, AccessFlags.Read);
// Setup as a render target via UseTextureFragment, which is the equivalent of using the old cmd.SetRenderTarget
builder.SetRenderAttachment(destination, 0);
// We disable culling for this pass for the demonstrative purpose of this sample, as normally this pass would be culled,
// since the destination texture is not used anywhere else
builder.AllowPassCulling(false);
// Assign the ExecutePass function to the render pass delegate, which will be called by the render graph when executing the pass
builder.SetRenderFunc(static (PassData data, RasterGraphContext context) => ExecuteFBFetchPass(data, context));
}
}
Now we look at the FBFetchPass function. We add a raster render pass. The source is set as an input attachment. This is required for frame buffer fetch. We set the destination texture as the render attachment. The work will be done by the ExecuteFBFetchPass set as the render function.
static void ExecuteFBFetchPass(PassData data, RasterGraphContext context)
{
context.cmd.DrawProcedural(Matrix4x4.identity, data.material, data.useMSAA? 1 : 0, MeshTopology.Triangles, 3, 1, null);
]}
This uses DrawProcedural to draw the texture with a custom material. The material used has to contain rather specific code. Take a look at FrameBufferFetch.shader from the same folder as the renderer feature. There are two passes, one if msaaSamples is disabled and the second if it is enabled. The DrawProcedural call will use only one. They both have the same basic form.
// Declares the framebuffer input as a texture 2d containing half.
FRAMEBUFFER_INPUT_HALF(0);
// Out frag function takes as input a struct that contains the …
float4 Frag(Varyings input) : SV_Target0
{
// this is needed so we account XR platform differences in how they handle texture arrays
UNITY_SETUP_STEREO_EYE_INDEX_POST_VERTEX(input);
// read the current pixel from the framebuffer
//float2 uv = input.texcoord.xy;
// read previous subpasses directly from the framebuffer.
half4 color = LOAD_FRAMEBUFFER_INPUT(0, input.positionCS.xy);
// Modify the sampled color
return half4(0,0,1,1) * color;
}
First a call to the FRAMEBUFFER_INPUT_HALF macro. Then in the fragment shader we source the pixel from the framebuffer using LOAD_FRAMEBUFFER_INPUT. The pixel is then set to a blue tint by multiplying it by the half4 value 0,0,1,1. Red 0, green 0, blue 1 and alpha 1.
Looking at this scene with Render Graph Viewer we can see that the Frame Buffer Fetch Pass and Copy Back FF Destination both use FrameBufferFetch when reading the buffer as the F indicates and also that the 5 passes from Draw Opaque Objects to Setup Post FX passes are all merged. It is well worth using this approach to maximize performance and minimize memory use.
Blit with FrameData
Blit with FrameData shows an example of how to use the same texture in different passes. A possible use case would be if you wanted to do a before and after effect, with two textures side by side. To do this you could capture the before texture in an initial custom pass and store the texture in a FrameData instance. This is passed as an instance of the ContextContainer class, a type of Dictionary, to the RecordRenderGraph method. Subsequent passes do the post processing, and then with a further custom pass you could fetch the before texture back from the frameData and have the pass draw the current Camera Color and the beforeTexture side by side.
Take a look at file BlitRendererFeature.cs from the folder URP RenderGraph Samples > Blit with FrameData.
public class BlitData : ContextItem
{
// Render graph texture handles.
TextureHandle m_TextureHandleFront;
TextureHandle m_TextureHandleBack;
// Scale bias is used to control how the blit operation is done.
static Vector4 scaleBias = new Vector4(1f, 1f, 0f, 0f);
// Bool to manage which texture is the most recent.
bool m_IsFront = true;
// The texture which contains the color buffer from the most recent blit operation.
public TextureHandle texture;
// Function used to initialize BlitData.
// Should be called before starting to use the class for each frame.
public void Init(RenderGraph renderGraph, TextureDesc targetDescriptor, string textureName = null)
{ ... }
// We will need to reset the texture handle after each frame
// to avoid leaking invalid texture handles
// since the texture handles only lives for one frame.
public override void Reset()
{ ... }
// For this function we don't take a material as argument
//to show that we should remember to reset values
// we don't use to avoid leaking values from last frame.
public void RecordBlitColor(RenderGraph renderGraph, ContextContainer frameData)
{ ... }
// Records a render graph render pass which blits the BlitData's
// active texture back to the camera's color attachment.
public void RecordBlitBackToColor(RenderGraph renderGraph, ContextContainer frameData)
{ ... }
// This function blits the whole screen for a given material.
public void RecordFullScreenPass(RenderGraph renderGraph, string passName, Material material)
{ ... }
}
The key class is BlitData which will be added to the FrameData parameter of the RecordRenderGraph method for each pass. It extends ContextItem. The class creates two TextureHandles, m_TextureHandleFront and m_TextureHandleBack. BlitData uses the property texture to select between these two textures. The class also includes three Record methods, one for each pass.
This file contains three passes:
- BlitStartRenderPass - used to initialise the BlitData class.
- BlitRenderPass - used to blit using an array of materials
- BlitEndRenderPass - used to restore from the texture stored in the first pass.
class BlitStartRenderPass : ScriptableRenderPass
{
public override void RecordRenderGraph(RenderGraph renderGraph, ContextContainer frameData)
{
// Creating the data BlitData inside frameData.
var blitTextureData = frameData.Create<BlitData>();
// Copies the camera's color attachment to a texture inside BlitData.
blitTextureData.RecordBlitColor(renderGraph, frameData);
}
}
class BlitRenderPass : ScriptableRenderPass
{
…
public override void RecordRenderGraph(RenderGraph renderGraph, ContextContainer frameData)
{
// Retrieves the BlitData from the current frame.
var blitTextureData = frameData.Get<BlitData>();
…
}
}
class BlitEndRenderPass : ScriptableRenderPass
{
public override void RecordRenderGraph(RenderGraph renderGraph, ContextContainer frameData)
{
// Retrieves the BlitData from the current frame and blit it back again to the camera's color attachment.
var blitTextureData = frameData.Get<BlitData>();
blitTextureData.RecordBlitBackToColor(renderGraph, frameData);
}
}
Notice that for BlitRenderPass and BlitEndRenderPass the RecordRenderGraph method accesses the BlitData instance created in the BlitStartRenderPass from the frameData parameter using the Get method. Then each pass uses a custom Record method found in the BlitData class. The key take away here is passing data between passes using a custom class that is created using the Create method of the frameData instance.
Texture Reference with FrameData
Similar to the previous sample, Texture Reference with FrameData uses a custom class extended from ContextItem to hold a texture reference to be used by future passes.
public class TexRefData : ContextItem
{
// The texture reference variable.
public TextureHandle texture = TextureHandle.nullHandle;
// Reset function required by ContextItem. It should reset all variables not carried
// over to next frame.
public override void Reset()
{
// We should always reset texture handles since they are only valid for the current frame.
texture = TextureHandle.nullHandle;
}
}
This is useful to avoid additional blit operations copying back and forth to the camera’s color attachment. Instead of copying it back after the blit operation we can instead update the reference to the blit destination and use that for future passes. The frameData instance is the preferred way to share resources between passes. Previously, it was common to use global textures for this. However, it’s better to avoid using global textures where you can. Global textures impact performance.
Output Texture
In this sample a named output texture is created that can be attached by name to a material. Take a look at OutputTextureRendererFeature.cs from the URP Render Graph Samples folder.
public class OutputTextureRendererFeature : ScriptableRendererFeature
{
...
// Pass which outputs a texture from rendering to inspect a texture
class OutputTexturePass : ScriptableRenderPass
{
// The texture name you wish to bind the texture handle to for a given material.
string m_TextureName;
// The texture type you want to retrieve from URP.
TextureType m_TextureType;
// The material used for blitting to the color output.
Material m_Material;
// Function to set up the ConfigureInput() and transfer the renderer feature settings to the render pass.
public void Setup(string textureName, TextureType textureType, Material material)
{ ... }
// Records a render graph render pass which blits the BlitData's active texture back to the camera's color attachment.
public override void RecordRenderGraph(RenderGraph renderGraph, ContextContainer frameData)
{ ... }
...
}
This file contains the classes OutputTextureRendererFeature and OutputTexturePass. The Renderer Feature contains four properties that can be set in the Inspector.
[SerializeField]
RenderPassEvent m_PassEvent = RenderPassEvent.AfterRenderingTransparents;
[SerializeField]
string m_TextureName = "_InputTexture";
[SerializeField]
TextureType m_TextureType;
[SerializeField]
Material m_Material;
- m_PassEvent - is the stage in the pipeline to run the pass
- m_TextureName - the name attached to the texture
- m_TextureType - the texture input can be Color, Depth, Normal or Motion
- m_Material - the material used for the Blit pass.
The Create method of the Renderer Feature uses the m_PassEvent property to set the renderPassEvent for the m_ScriptablePass. To pass the properties from the Renderer Feature to the Render Pass this sample uses a Setup method. Let’s take a look.
public void Setup(string textureName, TextureType textureType, Material material)
{
// Setup code to trigger each corresponding texture is ready for use when the pass is run.
if (textureType == TextureType.OpaqueColor)
ConfigureInput(ScriptableRenderPassInput.Color);
else if (textureType == TextureType.Depth)
ConfigureInput(ScriptableRenderPassInput.Depth);
else if (textureType == TextureType.Normal)
ConfigureInput(ScriptableRenderPassInput.Normal);
else if (textureType == TextureType.MotionVector)
ConfigureInput(ScriptableRenderPassInput.Motion);
// Set up the texture name, type and material used when blitting.
// In this example we will use a material using a custom name for the input texture name when blitting.
// This texture name has to match the material texture input you are using.
m_TextureName = String.IsNullOrEmpty(textureName) ? "_BlitTexture" : textureName;
// Texture type selects which input we would like to retrieve from the camera.
m_TextureType = textureType;
// The material is used to blit the texture to the cameras color attachment.
m_Material = material;
}
The textureType is set using the ConfigureInput method of a ScriptableRenderPass. The texture name is set with a default value of _BlitTexture and the material to use when Blitting is set.
public override void RecordRenderGraph(RenderGraph renderGraph, ContextContainer frameData)
{
// Fetch UniversalResourceData from frameData to retrieve the URP's texture handles.
var resourceData = frameData.Get<UniversalResourceData>();
// Sets the texture handle input using the helper function to fetch the correct handle from resourceData.
var source = GetTextureHandleFromType(resourceData, m_TextureType);
if (!source.IsValid())
{
Debug.Log("Input texture is not created. Likely the pass event is before the creation of the resource. Skipping OutputTexturePass.");
return;
}
RenderGraphUtils.BlitMaterialParameters para = new(source, resourceData.activeColorTexture, m_Material, 0);
para.sourceTexturePropertyID = Shader.PropertyToID(m_TextureName);
renderGraph.AddBlitPass(para, passName: "Blit Selected Resource");
}
Then in the RecordRenderGraph method we get the source based on the m_TextureType property. The source might be invalid if the pass event is set to before the source is created, so we check and return if this is the case. Finally we configure a BlitMaterialsParameters instance. The source is set to source, destination to activeColorTexture, material to the m_Material property and the pass to zero. Then we assign the sourceTexturePropertyID using the texture name. This means we can access this texture using the name in a Shader Graph.
Take a look at the Shader Graph BlitTargetTexture.
You will see it uses a single property, _InputTexture, a Texture 2D. The simple graph uses this as the texture input to a Sample Texture 2D node. To use this shader you need to set the Texture Name property of the Renderer Feature to _InputTexture and the material to BlitTargetTexture. If you’re using the GitHub repo with complete scenes, notice this uses the shader graph OutputTexture which uses the texture name _MyTexture. If you try changing the texture name you may need to close and reopen the scene for the updates to be used correctly.
Compute
Render Graph does not just deal with raster operations. A developer can use Compute shaders to handle computations that benefit from the massive parallelism available via the GPU. ComputeRendererFeature.cs is a simple example to help you understand the code used. You’ll find it in the URP RenderGraph Samples > Compute folder. If you’re using the GitHub repo open Scenes > Compute > Compute. Take a look at Render Graph Viewer, make sure the Pass Filter includes Compute Passes.
Notice the Compute Pass uses the InputBuffer, green indicating read access and uses the OutputBuffer, red indicating write access, from the resource list. The work done by the ComputeShader is shown in the Console. If you’re new to ComputeShaders there are three Unity videos available that explain the topic, links in the resources below.
Let’s take a look at the ComputeRendererFeature.cs code.
public class ComputeRendererFeature : ScriptableRendererFeature
{
class ComputePass : ScriptableRenderPass
{
// Compute shader.
ComputeShader m_ComputeShader;
// Compute buffers.
BufferHandle m_InputBuffer;
BufferHandle m_OutputBuffer;
// Input data for the compute shader.
private List<int> inputData = new List<int>();
// Constructor is used to initialize the input data.
public ComputePass()
{
for (int i = 0; i < 20; i++)
{
inputData.Add(i);
}
}
// Setup function to transfer the compute shader from the renderer feature to
// the render pass.
public void Setup(ComputeShader cs)
{ ... }
// PassData is used to pass data when recording to the execution of the pass.
class PassData
{ ... }
// ReadbackPassData is used to read data asynchronously from the specified bufferHandle.
class ReadbackPassData
{ ... }
// Records a render graph render pass which blits the BlitData's active texture back to the camera's color attachment.
public override void RecordRenderGraph(RenderGraph renderGraph, ContextContainer frameData)
{ ... }
static void ExecutePass(PassData data, ComputeGraphContext cgContext)
{ ... }
}
...
}
Notice the ComputePass class includes a ComputeShader, two BufferHandles and a List. In the constructor the List is initialized by storing the first 20 integers, 0 to 19. The class also includes the definition of two more custom classes: PassData, that is used when passing data to the execution function and ReadbackPass, that is used by the pass that reads the result of the ComputeShader.
public override void RecordRenderGraph(RenderGraph renderGraph, ContextContainer frameData)
{
// Create buffers
var bufferDesc = new BufferDesc()
{
name = "InputBuffer",
count = 20,
stride = sizeof(int),
target = GraphicsBuffer.Target.Structured
};
m_InputBuffer = renderGraph.CreateBuffer(bufferDesc);
bufferDesc.name = "OutputBuffer";
m_OutputBuffer = renderGraph.CreateBuffer(bufferDesc);
using (var builder = renderGraph.AddComputePass("ComputePass", out PassData passData))
{
builder.AllowPassCulling(false);
// Set the pass data so the data can be transferred from the recording to the execution.
passData.cs = m_ComputeShader;
passData.input = m_InputBuffer;
passData.output = m_OutputBuffer;
passData.bufferData = inputData;
// UseBuffer is used to set up render graph dependencies together with read and write flags.
builder.UseBuffer(passData.input, AccessFlags.Read);
builder.UseBuffer(passData.output, AccessFlags.Write);
// The execution function is also called SetRenderFunc for compute passes.
builder.SetRenderFunc(static (PassData data, ComputeGraphContext cgContext) => ExecutePass(data, cgContext));
}
// Because our BufferHandles are managed by the render graph, we don't have access to the data when the
// RenderGraph is done executing. We need to add a pass to read from the output buffer if we want to
// use the output data from the compute shader.
using (var builder = renderGraph.AddUnsafePass<ReadbackPassData>("ReadbackPass", out var passData))
{
builder.AllowPassCulling(false);
// Which buffer to read from
passData.bufferHandle = m_OutputBuffer;
builder.UseBuffer(passData.bufferHandle, AccessFlags.Read);
builder.SetRenderFunc(static (ReadbackPassData data, UnsafeGraphContext ctx) =>
{
ctx.cmd.RequestAsyncReadback(data.bufferHandle, (AsyncGPUReadbackRequest request) =>
{
var result = request.GetData<int>();
Debug.Log(string.Join(",", result));
});
});
}
}
The RecordRenderGraph method creates two buffers, m_InputBuffer and m_OutputBuffer. Then we use the AddComputePass method. It populates the passData out parameter, sets the access parameters for the buffers and sets the render function. As discussed the sample includes a second pass to read the result of the ComputeShader. This uses the AddUnsafePass method. Here after assigning the buffer to read the set render function simply asynchronously gets data from the buffer and dumps it to the console using Debug.Log.
All the ComputerShader does is double the input value. So with the input data 0,1,2 … 19 we see 0,2,4 … 38 in the console. The important take away being how to use a RendererFeature to run a ComputeShader.
Renderer List
In this sample we clear the active color texture and then draw geometry (filtered by a layer mask) via a RendererListHandle. The purpose of the sample is to demonstrate combining draw operations into a pass. Take a look at RendererListRenderFeature.cs from the URP RenderGraph Samples > RendererList folder.
public class RendererListRenderFeature : ScriptableRendererFeature
{
class RendererListPass : ScriptableRenderPass
{ ... }
RendererListPass m_ScriptablePass;
public LayerMask m_LayerMask;
/// <inheritdoc/>
public override void Create()
{ ... }
// Here you can inject one or multiple render passes in the renderer.
// This method is called when setting up the renderer once per-camera.
public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData)
{ ... }
}
The file contains a custom pass class and a renderer feature. The renderer feature uses a custom constructor to pass the layer mask to the RendererListPass.
public override void Create()
{
m_ScriptablePass = new RendererListPass(m_LayerMask);
// Configures where the render pass should be injected.
m_ScriptablePass.renderPassEvent = RenderPassEvent.AfterRenderingOpaques;
}
The Renderer List referred to by this sample is the list of objects to render after layer mask filtering, not the Renderer List we see for the active URP asset used to define the settings for the render..
The key to this sample is the InitRendererLists method in the RendererListPass.
private void InitRendererLists(ContextContainer frameData, ref PassData passData, RenderGraph renderGraph)
{
// Access the relevant frame data from the Universal Render Pipeline
UniversalRenderingData universalRenderingData = frameData.Get<UniversalRenderingData>();
UniversalCameraData cameraData = frameData.Get<UniversalCameraData>();
UniversalLightData lightData = frameData.Get<UniversalLightData>();
var sortFlags = cameraData.defaultOpaqueSortFlags;
RenderQueueRange renderQueueRange = RenderQueueRange.opaque;
FilteringSettings filterSettings = new FilteringSettings(renderQueueRange, m_LayerMask);
ShaderTagId[] forwardOnlyShaderTagIds = new ShaderTagId[]
{
new ShaderTagId("UniversalForwardOnly"),
new ShaderTagId("UniversalForward"),
new ShaderTagId("SRPDefaultUnlit"), // Legacy shaders (do not have a gbuffer pass) are considered forward-only for backward compatibility
new ShaderTagId("LightweightForward") // Legacy shaders (do not have a gbuffer pass) are considered forward-only for backward compatibility
};
m_ShaderTagIdList.Clear();
foreach (ShaderTagId sid in forwardOnlyShaderTagIds)
m_ShaderTagIdList.Add(sid);
DrawingSettings drawSettings = RenderingUtils.CreateDrawingSettings(m_ShaderTagIdList, universalRenderingData, cameraData, lightData, sortFlags);
var param = new RendererListParams(universalRenderingData.cullResults, drawSettings, filterSettings);
passData.rendererListHandle = renderGraph.CreateRendererList(param);
}
Here we access the model assets via UniversalRenderingData, cameras via UniversalCamera Data and lights via UniversaLightData. We set sortFlags variable to defaultOpaqueSortFlags. We use RenderQueueRange.opaque and the layer mask property to create a filterSettings variable. We create a forward only shader tag id list. This is needed to build the render list. Now we have the properties needed to create an instance of the DrawingSettings class. Which gives the necessary properties to generate a renderer list handle.
Let’s look at the RecordRenderGraph method.
public override void RecordRenderGraph(RenderGraph renderGraph, ContextContainer frameData)
{
string passName = "RenderList Render Pass";
using (var builder = renderGraph.AddRasterRenderPass<PassData>(passName, out var passData))
{
// UniversalResourceData contains all the texture handles used by the renderer, including the active color and depth textures
// The active color and depth textures are the main color and depth buffers that the camera renders into
UniversalResourceData resourceData = frameData.Get<UniversalResourceData>();
// Fill up the passData with the data needed by the pass
InitRendererLists(frameData, ref passData, renderGraph);
// Make sure the renderer list is valid
if (!passData.rendererListHandle.IsValid())
return;
// We declare the RendererList we just created as an input dependency to this pass, via UseRendererList()
builder.UseRendererList(passData.rendererListHandle);
// Setup as a render target via UseTextureFragment and UseTextureFragmentDepth, which are the equivalent of using the old cmd.SetRenderTarget(color,depth)
builder.SetRenderAttachment(resourceData.activeColorTexture, 0);
builder.SetRenderAttachmentDepth(resourceData.activeDepthTexture, AccessFlags.Write);
// Assign the ExecutePass function to the render pass delegate, which will be called by the render graph when executing the pass
builder.SetRenderFunc(static (PassData data, RasterGraphContext context) => ExecutePass(data, context));
}
}
Here we get the frameData and pass it to the InitRendererLists method. We check if this call gave a valid rendererListHandle and return if it didn’t. We tell the builder to use a renderer list. Set the render target and depth buffer and finally set the render function. It will use the ExecutePass method which is super simple, it clears the render target to green and then calls DrawRendererList using the rendererListHandle as a parameter.
In the repo for this video we created Desk and Chair layers. The desk and the things on the desk are on the desk layer. If we switch to this layer we see just the desk on a green background. Useful for highlighting something in a scene.
Culling
The Culling sample is very similar to the RendererList sample; the key difference is demonstrating how to access the culling results from the camera, rather than taking them directly from the universalRenderingData.cullResults contained in the frameData. This would be useful if you wanted to manually customize the cullingResults.
Take a look at CullRenderPassRendererFeature.cs from the URP RenderGraph Samples > Culling folder.
private void InitRendererLists(CullingResults cullResults, ContextContainer frameData, ref PassData passData, RenderGraph renderGraph)
{ ... }
Notice the InitRendererLists method includes a cullResults parameter missing in the RendererList version. InitRendererLists is called from the RecordRenderGraph method.
public override void RecordRenderGraph(RenderGraph renderGraph, ContextContainer frameData)
{
…
using (var builder = renderGraph.AddRasterRenderPass<PassData>(passName, out var passData, profilingSampler))
{
…
// CullContextData contains the culling APIs.
var cullContextData = frameData.Get<CullContextData>();
// Retrieve the culling parameters for the camera used.
cameraData.camera.TryGetCullingParameters(false, out var cullingParameters);
// Perform culling using the CullContextData API.
var cullingResults = cullContextData.Cull(ref cullingParameters);
// Fill up the passData with the data needed by the pass.
InitRendererLists(cullingResults, frameData, ref passData, renderGraph);
…
}
…
}
To create the cullResults we first get the cullContextData from the frameData. Then we call the camera method TryGetCullingParameters. Once we have the cullingParameters and the cullContextData we can get the cullingResults by passing the cullingParameters to the Cull method of the cullContextData instance.
Unsafe Pass
This sample shows how to set up a blit when source and target have different dimensions. This means a Raster Render Pass cannot be used and an Unsafe pass is used instead. An Unsafe pass cannot be merged and so should be used only when essential. In some cases using an Unsafe Pass does make sense. In this sample, for example, we know that the set of adjacent passes are not mergeable because of the size differences, so we can optimize the Render Graph compile time, on top of simplifying a multiple passes setup.
public override void RecordRenderGraph(RenderGraph renderGraph, ContextContainer frameData)
{
...
// Create a new temporary texture to keep the blit result.
descriptor.name = "UnsafeTexture";
var destination = renderGraph.CreateTexture(descriptor);
descriptor.width /= 2;
descriptor.height /= 2;
descriptor.name = "UnsafeTexture2";
var destinationHalf = renderGraph.CreateTexture(descriptor);
descriptor.width /= 2;
descriptor.height /= 2;
descriptor.name = "UnsafeTexture3";
var destinationQuarter = renderGraph.CreateTexture(descriptor);
passData.destination = destination;
passData.destinationHalf = destinationHalf;
passData.destinationQuarter = destinationQuarter;
...
// Assign the ExecutePass function to the render pass delegate, which will be called by the render graph when executing the pass
builder.SetRenderFunc(static (PassData data, UnsafeGraphContext context) => ExecutePass(data, context));
}
}
If we review the RecordRenderGraph method you can see three textures are created, the first matches the size of the activeColorTexture, the second halves the size and the third halves it again.
static void ExecutePass(PassData data, UnsafeGraphContext context)
{
...
context.cmd.SetRenderTarget(data.destinationHalf);
Blitter.BlitTexture(unsafeCmd, data.destination, new Vector4(1, 1, 0, 0), 0, false);
...
}
The render function uses BlitTexture which handles the differences in size between the source and the destination of the blit.
private class PassData
{
internal TextureHandle source;
internal TextureHandle destination;
internal TextureHandle destinationHalf;
internal TextureHandle destinationQuarter;
}
We can see the UnsafePass includes a custom PassData class which has 4 texture handles. The RecordRenderGraph method adds an Unsafe pass with PassData.
...
passData.source = resourceData.activeColorTexture;
...
descriptor.name = "UnsafeTexture";
var destination = renderGraph.CreateTexture(descriptor);
descriptor.width /= 2;
descriptor.height /= 2;
descriptor.name = "UnsafeTexture2";
var destinationHalf = renderGraph.CreateTexture(descriptor);
descriptor.width /= 2;
descriptor.height /= 2;
descriptor.name = "UnsafeTexture3";
var destinationQuarter = renderGraph.CreateTexture(descriptor);
...
// UnsafePasses don't setup the outputs using UseTextureFragment/UseTextureFragmentDepth, you should specify your writes with UseTexture instead
builder.UseTexture(passData.destination, AccessFlags.WriteAll);
builder.UseTexture(passData.destinationHalf, AccessFlags.WriteAll);
builder.UseTexture(passData.destinationQuarter, AccessFlags.WriteAll);
...
builder.SetRenderFunc(static (PassData data, UnsafeGraphContext context) => ExecutePass(data, context));
We set the passData as the current active color texture, create 3 textures each half the size of the previous. We set access for each texture.
static void ExecutePass(PassData data, UnsafeGraphContext context)
{
...
context.cmd.SetRenderTarget(data.destination);
Blitter.BlitTexture(unsafeCmd, data.source, new Vector4(1, 1, 0, 0), 0, false);
// downscale x2
context.cmd.SetRenderTarget(data.destinationHalf);
Blitter.BlitTexture(unsafeCmd, data.destination, new Vector4(1, 1, 0, 0), 0, false);
context.cmd.SetRenderTarget(data.destinationQuarter);
Blitter.BlitTexture(unsafeCmd, data.destinationHalf, new Vector4(1, 1, 0, 0), 0, false);
// upscale x2
context.cmd.SetRenderTarget(data.destinationHalf);
Blitter.BlitTexture(unsafeCmd, data.destinationQuarter, new Vector4(1, 1, 0, 0), 0, false);
context.cmd.SetRenderTarget(data.destination);
Blitter.BlitTexture(unsafeCmd, data.destinationHalf, new Vector4(1, 1, 0, 0), 0, false);
}
In the render function we get a command buffer and set up 5 blits.
We can see the result of the blits using Frame Debugger.
MRT
This sample demonstrates using Multiple Render Targets with Render Graph in URP. This would be useful if a pass needs to output more than a single RGBA texture (e.g. output various buffers in one pass).
public class MrtRendererFeature : ScriptableRendererFeature
{
// This pass is using MRT and will output to 3 different Render Targets.
class MrtPass : ScriptableRenderPass
{ ... }
[Tooltip("The material used when making the MRT pass.")]
public Material mrtMaterial;
[Tooltip("Name to apply the camera's color attachment to for the given material.")]
public string textureName = "_ColorTexture";
[Tooltip("Render Textures to output the result to. Is has to have the size of 3.")]
public RenderTexture[] renderTextures = new RenderTexture[3];
...
}
The MRTRendererFeature class includes an array of 3 RenderTextures.
class MrtPass : ScriptableRenderPass
{
...
// RTHandle outputs for the MRT destinations.
RTHandle[] m_RTs = new RTHandle[3];
RenderTargetInfo[] m_RTInfos = new RenderTargetInfo[3];
...
}
The MRTPass includes properties m_RTs, an array of 3 RTHandles and an array of 3 RenderTargetInfos m_RTInfos.
public void Setup(string texName, Material material, RenderTexture[] renderTextures)
{
m_Material = material;
m_texName = String.IsNullOrEmpty(texName) ? "_ColorTexture" : texName;
// Create RTHandles from the RenderTextures if they have changed.
for (int i = 0; i < 3; i++)
{
if (m_RTs[i] == null || m_RTs[i].rt != renderTextures[i])
{
m_RTs[i]?.Release();
m_RTs[i] = RTHandles.Alloc(renderTextures[i], $"ChannelTexture[{i}]");
m_RTInfos[i] = new RenderTargetInfo()
{
format = renderTextures[i].graphicsFormat,
height = renderTextures[i].height,
width = renderTextures[i].width,
bindMS = renderTextures[i].bindTextureMS,
msaaSamples = 1,
volumeDepth = renderTextures[i].volumeDepth,
};
}
}
}
The Setup method includes a reference to the MRTRendererFeature RenderTexture properties as parameter three. In the method we check if each of the MRTPass properties m_RTs are empty or do not match the MRTRendererFeature RenderTexture for their index. If this is the case we optionally Release an existing reference and allocate the handle. We also copy the descriptor details.
public override void RecordRenderGraph(RenderGraph renderGraph, ContextContainer frameData)
{
var handles = new TextureHandle[3];
// Imports the texture handles in RenderGraph.
for (int i = 0; i < 3; i++)
{
handles[i] = renderGraph.ImportTexture(m_RTs[i], m_RTInfos[i]);
}
// 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>("MRT Pass", out var passData))
{
// Fetch the universal resource data to extract the camera's color attachment.
var resourceData = frameData.Get<UniversalResourceData>();
// Fill in the pass data using by the render function.
// Use the camera's color attachment as input.
passData.color = resourceData.activeColorTexture;
// Input Texture name for the material.
passData.texName = m_texName;
// Material used in the pass.
passData.material = m_Material;
// Sets input attachment.
builder.UseTexture(passData.color);
// Sets color attachments.
for (int i = 0; i < 3; i++)
{
builder.SetRenderAttachment(handles[i], i);
}
// Sets the render function.
builder.SetRenderFunc(static (PassData data, RasterGraphContext rgContext) => ExecutePass(data, rgContext));
}
}
In the RecordRenderGraph method we create an array of TextureHandles and assign these using the m_RTs and m_RTInfos created in the Setup method. We set the render attachments from these handles.
static void ExecutePass(PassData data, RasterGraphContext rgContext)
{
// Sets the input color texture to the name used in the MRTPass
data.material.SetTexture(data.texName, data.color);
// Draw the fullscreen triangle with the MRT shader.
rgContext.cmd.DrawProcedural(Matrix4x4.identity, data.material, 0, MeshTopology.Triangles, 3);
}
In the render function we use DrawProcedural to render the fullscreen triangle with the material property. You could use this approach to output to color, normals and depth buffers in a single pass useful for deferred rendering or creating custom lighting buffers.
Gbuffer Visualization
This sample demonstrates accessing G-buffer components inside a pass when they are not global. So you can inspect or use G-buffer data locally in a pass. This sample applies to a deferred render pipeline where lighting is delayed until after surfaces are drawn, storing surface data in a G-Buffer. This makes handling many lights efficient but has trade-offs with memory and transparency. Visualizing can be useful for debugging. Take a look at the file GbufferVisualizationRendererFeature.cs from the URP RenderGraph Samples folder.
private static readonly int[] s_GBufferShaderPropertyIDs = new int[]
{
// Contains Albedo Texture
Shader.PropertyToID("_GBuffer0"),
// Contains Specular Metallic Texture
Shader.PropertyToID("_GBuffer1"),
// Contains Normals and Smoothness, referenced as _CameraNormalsTexture in other shaders
Shader.PropertyToID("_GBuffer2"),
// Contains Lighting texture
Shader.PropertyToID("_GBuffer3"),
// Contains Depth texture, referenced as _CameraDepthTexture in other shaders (optional)
Shader.PropertyToID("_GBuffer4"),
// Contains Rendering Layers Texture, referenced as _CameraRenderingLayersTexture in other shaders (optional)
Shader.PropertyToID("_GBuffer5"),
// Contains ShadowMask texture (optional)
Shader.PropertyToID("_GBuffer6")
};
First we store an array of integer values which are the property ids of each G-buffer.
public override void RecordRenderGraph(RenderGraph renderGraph, ContextContainer frameData)
{
...
// Get the gBuffer texture handles stored in the resourceData
UniversalResourceData resourceData = frameData.Get<UniversalResourceData>();
TextureHandle[] gBuffer = resourceData.gBuffer;
using (var builder = renderGraph.AddRasterRenderPass<PassData>(m_PassName, out var passData))
{
...
for (int i = 0; i < resourceData.gBuffer.Length; i++)
{
if (i == GbufferLightingIndex)
{
// We already specify we are writing to it above (SetRenderAttachment)
continue;
}
builder.UseTexture(resourceData.gBuffer[i]);
}
...
}
}
In the RecordRenderGraph method we set UseTexture for each G-buffer.
If you take a look at the Render Graph Viewer you will see the Visualize GBuffer Components pass has GBuffer0 through to GBuffer4 with read access. GBuffer3 is skipped so doesn’t appear in the resources for the path.
static void ExecutePass(PassData data, RasterGraphContext context)
{
// Here, we read all the gBuffer components as an example even though the shader only needs one.
// We still need to set it explicitly since it is not accessible globally (so the
// shader won't have access to it by default).
for (int i = 0; i < data.gBuffer.Length; i++)
{
data.material.SetTexture(s_GBufferShaderPropertyIDs[i], data.gBuffer[i]);
}
// Draw the gBuffer component requested by the shader over the geometry
context.cmd.DrawProcedural(Matrix4x4.identity, data.material, 0, MeshTopology.Triangles, 3, 1);
}
In the render function for each G-buffer we set the texture then use DrawProcedural to draw the buffer. This uses a custom material. Let’s take a look at the shader.
Varyings GBufferVisPassVertex(Attributes input)
{
Varyings output;
UNITY_SETUP_INSTANCE_ID(input);
UNITY_TRANSFER_INSTANCE_ID(input, output);
UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(output);
float4 pos = GetFullScreenTriangleVertexPosition(input.vertexID);
float2 uv = GetFullScreenTriangleTexCoord(input.vertexID);
output.positionCS = pos;
output.texcoord = uv;
return output;
}
void GBufferVisPassFragment(Varyings input, out half4 outColor : SV_Target0)
{
UNITY_SETUP_INSTANCE_ID(input);
UNITY_SETUP_STEREO_EYE_INDEX_POST_VERTEX(input);
float2 uv = input.texcoord;
#ifndef UNITY_UV_STARTS_AT_TOP
uv.y = 1.0 - uv.y;
#endif
// Change the sampled GBuffer here
outColor = SAMPLE_TEXTURE2D_X_LOD(_GBuffer2, sampler_PointClamp, uv, 0);
}
You’ll find GBuffer_Visualization_Shader_Sample.shader in the same folder as the Renderer Feature. It is set to use Gbuffer2, the normals buffer. It is a super simple shader, the vertex shader gets the transformed position and uv for the vertex and the fragment shader uses the interpolated uv values for the fragments position in the triangle being rendered to sample the buffer.
The key take-away of this sample is how to request access to G-buffer textures when they’re not set as global shader properties.
Global Gbuffers
As an alternative to the previous sample Global GBuffers declares G-buffer textures as global resources in the render graph, so that subsequent passes can access them although this sample itself doesn’t make any use of them.
Take a look at GlobalGbuffersRendererFeature.cs from the URP RenderGraph Samples folder. As in the previous sample in the GlobalGBuffersRenderPass we create an array of ids of the G-buffers.
public override void RecordRenderGraph(RenderGraph renderGraph, ContextContainer frameData)
{
...
UniversalResourceData resourceData = frameData.Get<UniversalResourceData>();
TextureHandle[] gBuffer = resourceData.gBuffer;
using (var builder = renderGraph.AddRasterRenderPass<PassData>(m_PassName, out var passData))
{
builder.AllowPassCulling(false);
// Set the gBuffers to be global after the pass
SetGlobalGBufferTextures(builder, gBuffer);
builder.SetRenderFunc(static (PassData data, RasterGraphContext context) => { /* nothing to be rendered */ });
}
}
In the RecordRenderGraph method we get the gBuffer array from the UniversalResourceData found in the frameData passed to the method. The key is the call to SetGlobalGBufferTextures.
private void SetGlobalGBufferTextures( IRasterRenderGraphBuilder builder,
TextureHandle[] gBuffer)
{
for (int i = 0; i < gBuffer.Length; i++)
{
if (i != GbufferLightingIndex && gBuffer[i].IsValid())
builder.SetGlobalTextureAfterPass(gBuffer[i], s_GBufferShaderPropertyIDs[i]);
}
if (gBuffer[GBufferNormalSmoothnessIndex].IsValid())
{
// After this pass, shaders that use the _CameraNormalsTexture
// will get the gBuffer's NormalsSmoothnessTexture component.
builder.SetGlobalTextureAfterPass(gBuffer[GBufferNormalSmoothnessIndex],
Shader.PropertyToID("_CameraNormalsTexture"));
}
if (GBufferRenderingLayersIndex < gBuffer.Length && gBuffer[GBufferRenderingLayersIndex].IsValid())
{
// After this pass, shaders that use the _CameraRenderingLayersTexture
// will get the gBuffer's RenderingLayersTexture component.
builder.SetGlobalTextureAfterPass(gBuffer[GBufferRenderingLayersIndex],
Shader.PropertyToID("_CameraRenderingLayersTexture"));
}
}
In this method we iterate over the gBuffer array and if a buffer is valid and the index is not 3, the GbufferLightingIndex value. The IRasterRenderGraphBuilder parameter, builder, has a method SetGlobalTextureAfterPass. This takes a buffer and an id and sets the G-buffer as global for subsequent passes. Some global textures are accessed using specific shader IDs that are internal to URP. To use the gBuffer in these places, we need to set the ID to point to the corresponding gBuffer component. This applies to GBufferNormalSmoothnessIndex GBufferRenderingLayersIndex.
If we look at the Render Graph Viewer you’ll see that GBuffer0 to GBuffer4, excluding the lighting index 3, are all now global. Indicated by the globe icon.
The key take-away for this sample is how to promote textures from local scope to global scope in Render Graph.
Conclusion
The Render Graph API has a fairly steep learning curve. The Render Graph samples help those new to Render Graph by giving best practice approaches to many of the problems facing developers who need to customize the rendered frame. We hope this article has provided a useful walk through of these samples.
Resources
If you want to learn more about Render Graph don’t miss the recent resources that we published:
- Discussions Article: Understanding the Render Graph Viewer
- Stages of rendering a frame in URP infographic
- Migrating to Render Graph video tutorial
- Render graph samples - part 1
- Render graph samples - part 2
- Introduction to Render Graph in Unity
- URP Cookbook: Compute shaders - Part 1: Particle fun
- URP Cookbook: Compute shaders - Part 2: Flocking
- URP Cookbook: Compute shaders - Part 3: Vertex animation
- Understanding URP and essentials
- E-book: Introduction to URP for advanced Unity creators (Unity 6 ed.)
- E-book: Create popular shaders and visual effects with the Universal Render Pipeline (Unity 6 ed.)

















