Introduction of Render Graph in the Universal Render Pipeline (URP)

I see :thinking:

If that’s the case, could we get this docs page updated accordingly? It’s the top result when searching “rendergraph createtexture” and says to use UniversalRenderer.CreateRenderGraphTexture—it doesn’t even mention that renderGraph.CreateTexture exists as an alternative.

The post you linked to gives an example for getting a depth texture & descriptor which worked perfectly, but is there an equivalent for a color texture?

TextureDesc desc = resourceData.cameraColor.GetDescriptor(renderGraph);
TextureHandle maskTex = renderGraph.CreateTexture(in desc);

The above gives me the following error:

InvalidOperationException: Trying to use a texture (_InternalGradingLut) that was already released or not yet created. Make sure you declare it for reading in your pass or you don't read it before it's been written to at least once.

Which I guess makes sense since cameraColor at this point references _InternalGradingLut, and I haven’t told render graph that I’m using that in my pass.

Is there something similar to resourceData.cameraDepthTexture but for a color texture whose descriptor I could steal for my pass? I just want a descriptor that describes a color texture with the same dimensions as the screen.

Creating a TextureDesc from scratch seems rather daunting since I have no idea what most of its fields do, so for now I’ve done this:

textureDescriptor = new RenderTextureDescriptor(Screen.width, Screen.height, RenderTextureFormat.RGFloat, 0);
TextureDesc desc = new(textureDescriptor) { name = WaterFogMaskTextureName };
TextureHandle maskTex = renderGraph.CreateTexture(in desc);

This appears to work, but is it an appropriate way to approach this? It seems a little silly to have to create a RenderTextureDescriptor just to create a TextureDesc from it, and the TextureDesc constructor which takes a RenderTextureDescriptor says it does “a best effort conversion” which sounds a bit sketchy :sweat_smile:

Good point, it will be updated soon.

Strange, resourceData.cameraColor should work if you are using the intermediate textures and not rendering directly to the backbuffer.
Otherwise, use the backbuffer (see my previous tips and tricks).
The texture resources.

Indeed, don’t do this :slight_smile:

1 Like

So this should work?

TextureDesc desc = resourceData.cameraColor.GetDescriptor(renderGraph);
TextureHandle maskTex = renderGraph.CreateTexture(in desc);
builder.SetRenderAttachment(maskTex, 0);

Because it gives me the error mentioned in my last post. Am I missing something?

In your tips and tricks post you say to use

renderGraph.GetRenderTargetInfo(resourceData.backBufferColor);

with the backbuffer, but this returns a RenderTargetInfo. Is there a way I can convert that to a TextureDesc? Or how do I turn this into a TextureHandle?

Hey @tomweiland, here is one way to use the backbuffer RenderTargetInfo to obtain a TextureDesc, and then use it to create a TextureHandle and attach it to your pass. Handling the backbuffer is a bit more complicated than internal RenderGraph resources because it comes from the native engine with limited information, making it hard to generate a full TextureDesc that can be reused.

                            RenderTargetInfo backBufferDesc = renderGraph.GetRenderTargetInfo(resourceData.backBufferColor);
                            TextureHandle testTex = renderGraph.CreateTexture(new TextureDesc(backBufferDesc.width, backBufferDesc.height, false, true)
                            {
                                name = "Test Texture",
                                slices = backBufferDesc.volumeDepth,
                                format = backBufferDesc.format,
                                msaaSamples = (MSAASamples)backBufferDesc.msaaSamples,
                                clearBuffer = false
                            });
                            builder.SetRenderAttachment(testTex, 0);
1 Like

I’m having trouble adapting this render list feature to one that renders the selected layers for use as a global shader texture rather than blitting to the screen. When looking at the Frame Debugger I can see the meshes rendering properly but when checking the texture with a quad or full screen pass it’s completely empty / white. Is SetGlobalTextureAfterPass the correct way to handle this? The code I’m working on is here:

using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.RenderGraphModule;
using UnityEngine.Rendering.Universal;

public class RenderLayersToTextureFeature : ScriptableRendererFeature
{
    public RenderPassEvent injectionPoint = RenderPassEvent.AfterRenderingOpaques;
    public LayerMask layerMask;
    public bool writeToDepth;
    public string shaderID = "_GlobalTextureID";
    RenderLayersToTexturePass pass;

    public override void Create()
    {
        pass = new(this)
        {
            renderPassEvent = injectionPoint
        };
    }

    public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData)
    {
        renderer.EnqueuePass(pass);
    }

    class RenderLayersToTexturePass : ScriptableRenderPass
    {
        private readonly RenderLayersToTextureFeature feature;
        private readonly List<ShaderTagId> shaderTagIDs = new ();
        private readonly int shaderID;

        public RenderLayersToTexturePass(RenderLayersToTextureFeature feature)
        {
            this.feature = feature;
            shaderID = Shader.PropertyToID(feature.shaderID);
        }

        // Sample utility method that showcases how to create a renderer list via the RenderGraph API
        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(renderQueueRange, feature.layerMask);

            ShaderTagId[] forwardOnlyShaderTagIds = new ShaderTagId[]
            {
                new("UniversalForwardOnly"),
                new("UniversalForward"),
                new("SRPDefaultUnlit"), // Legacy shaders (do not have a gbuffer pass) are considered forward-only for backward compatibility
                new("LightweightForward") // Legacy shaders (do not have a gbuffer pass) are considered forward-only for backward compatibility
            };

            shaderTagIDs.Clear();

            foreach (ShaderTagId sid in forwardOnlyShaderTagIds)
                shaderTagIDs.Add(sid);

            DrawingSettings drawSettings = RenderingUtils.CreateDrawingSettings(shaderTagIDs, universalRenderingData, cameraData, lightData, sortFlags);

            var param = new RendererListParams(universalRenderingData.cullResults, drawSettings, filterSettings);
            passData.rendererListHandle = renderGraph.CreateRendererList(param);
        }

        // This class stores the data needed by the pass, passed as parameter to the delegate function that executes the pass
        private class PassData
        {
            public RendererListHandle rendererListHandle;
        }

        // This static method is used to execute the pass and passed as the RenderFunc delegate to the RenderGraph render pass
        static void ExecutePass(PassData data, RasterGraphContext context)
        {
            context.cmd.ClearRenderTarget(RTClearFlags.Color, Color.black, 1, 0);
            context.cmd.DrawRendererList(data.rendererListHandle);
        }

        // 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)
        {
            // Add a raster render pass to the render graph, specifying the name and the data type that will be passed to the ExecutePass function
            using (var builder = renderGraph.AddRasterRenderPass<PassData>(feature.name, 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);

                // We declare the RendererList we just created as an input dependency to this pass, via UseRendererList()
                builder.UseRendererList(passData.rendererListHandle);

                var desc = resourceData.activeColorTexture.GetDescriptor(renderGraph);
                var destinationTexture = renderGraph.CreateTexture(desc);
                
                builder.SetRenderAttachment(destinationTexture, 0, AccessFlags.Write);

                if (feature.writeToDepth)
                    builder.SetRenderAttachmentDepth(resourceData.activeDepthTexture, AccessFlags.Write);

                builder.SetGlobalTextureAfterPass(destinationTexture, shaderID);

                // Assign the ExecutePass function to the render pass delegate, which will be called by the render graph when executing the pass
                builder.SetRenderFunc((PassData data, RasterGraphContext context) => ExecutePass(data, context));
                builder.SetRenderFunc<PassData>(ExecutePass);
            }
        }
    }
}

Unfortunately I’m getting very strange results with the code you provided.

In the pass that renders my mask (and creates the texture) the render target is correctly set to _WaterFogMaskTexture_1049x590_B10G11R11_UFloatPack32_Tex2DArray (and the Render Graph Viewer confirms the texture has the correct size, though it displays R16G16B16A16_SFloat as the format), but by the time the pass which consumes/uses this mask texture is executed, it has somehow been reduced to a 16x16 gray texture with none of the information in it which it definitely did contain earlier in the frame:
image

Any idea what could be causing this? It’s as if the texture is being reinitialized with different parameters later in the frame…

As janky/scuffed as it feels to create a RenderTextureDescriptor just to convert it to a TextureDesc, the backbuffer approach doesn’t feel a whole lot better/more proper :thinking:

Is there a way to ‘add a renderer’ to a rendererList or rerun the rendererList creation after the graph is already created, mid-execution before the draw command executes?

This was something that was incredibly easy with the previous setup, but seems impossible with renderGraph from everything I’ve tried, and also seems like a common use case? Here is my use case:

I have a rendergraph pass setup like this:

MyCustomPrePass: This renders the objects in a special way to a render texture
MyCustomReadPass: This reads the render texture, and modifies rendering layers on renderers before our main pass
MyCustomMainPass: Looks for specific rendering layers and reacts different to them during our main pass.

The issue I’m running into is a one frame delay in my main pass picking up a renderer layer change, a delay that seems systemic to rendergraph.

Unless I’m completely misunderstanding something, there is no way to change a rendererList to a newly generated one before finally doing the draw command, add a renderer, or anything of the sort. A passes rendererList is determined at graph compile time, and that’s that?

Or is there some way to ‘refresh’ a rendererList? I believe the issue I’m encountering is that since the rendererList is at compile time, the object newly placed on a renderer Layer won’t be properly placed in the cull results until the next compile.

Again, this technique worked with the non-rendergraph approach. And I can think of many ‘game’ use cases for having a pass that ‘informs’ the rendererList of a later pass. But alas…here we are unless I am just completely misunderstanding things, which I would love.

1 Like

For awareness, I have created a separate post about a change in the AfterRendering injection point:

3 Likes

Hooray for consistency.

What was the exact condition for the difference? Just if post processing was turned on or not?

These are some massive changes.

I though Unity 6 was in LTS mode, not in make nothing work mode in every update.

Seems this will require to release a new asset version for Unity 6.2 specifically

Now will be a separate asset for each Unity update as it seems

Also these changes are not clear at all how to be handled, change the render order might affect other things, also change the Y-flipping might affect other modes etc etc and is not clear what to change where and at which stage of our effects.

This can be massive amount of work to handle and fix, if everything is fixable after such huge changes.

Is it not possible to absorb these changes in your side of the rendering pipeline and keep our code intact for example ?

1 Like

How on earth are they going to absord “it was inconsistent when the injection point fired”???

1 Like

Thanks for your feedback!

To keep the Render Graph thread focused, we’ve created a new discussion for the AfterRendering changes here. I also updated my original post.

The AfterRendering change fixes historic inconsistencies across settings. We will also provide an upgrade guide soon. Feel free to continue the discussion in the new thread - we’re happy to help if needed!

any thoughts on my question above?

Having to make some decisions about what to support in our render pipeline. I want to use RenderGraph, but its proving inflexible in not allowing a user to have passes that inform the rendererList of a future pass

Anyone have any idea how or where CreateSharedTexture should be done? It must be done outside of the rendergraph execution, but you need to access RenderGraph in order to do it so its feeling like a catch 22 without another base method to override that gives access to rendergraph.

or if anyone wants to point out the(probably numerous) flaws in my attempt to convert something to rendergraph, the old execute works as compatibility mode. Completely unsure of how to create persistent rendertexture data for use over multiple frames. BlazeBin - iftbyrloavkl

If a texture needs to persistent across cameras or frames, you can create and manage an RTHandle yourself, and import it in RenderGraph every frame using RG.ImportTexture(). This samples uses that pattern.

You can find more samples and learning materials here.

1 Like

thanks, running into another issue of not knowing how to blit using a custom material/shader:

            {
                passData.runtime            = runtimeTexture;
                passData.edgeBleedMaterial = fixedEdgesMaterial;
                passData.mask               = maskTexture;
                passData.bleed              = bleedTexture;
                passData.width              = _paintFeature.Width;
                passData.height             = _paintFeature.Height;
                
                builder.UseTexture(passData.runtime);
                builder.UseTexture(passData.mask);
                builder.SetRenderAttachment(passData.bleed, 0);
                
                builder.SetRenderFunc((PassData data, RasterGraphContext context) =>
                {
                    data.edgeBleedMaterial.SetTexture("_BaseMap", data.runtime);
                    data.edgeBleedMaterial.SetTexture("_MaskIslands", data.mask);
                    data.edgeBleedMaterial.SetVector("_BaseMap_TexelSize", 
                        new Vector4(1f / data.width, 1f / data.height, 0, 0));
                    
                    //Blitter.BlitTexture(context.cmd, data.runtime, new Vector4(1, 1, 0, 0), data.edgeBleedMaterial, 0);
                    //context.cmd.DrawProcedural(Matrix4x4.identity, data.edgeBleedMaterial, 0, MeshTopology.Triangles, 3, 1);
                    Graphics.Blit(data.runtime, data.bleed, data.edgeBleedMaterial);
                });
            }

Graphics.Blit does get me what I want, but I assume I should really be using the rendergraph context here? Neither of the commented out parts appear to modify the bleed texture at all

Or use Shader.setglobaltexture and Shader.getglobaltexture works

looks like I should stick with compatibility mode and legacy passes then if I need to edit rendererlists after a pass but before another? @AljoshaD @oliverschnabel

You should indeed use Blitter.BlitTexture or context.cmd.DrawProcedural (NOT Graphics.Blit).

You can read this tips and tricks about using local textures on your material. If your material is only used in that pass then it should be fine.

At a glance, the code looks correct so you’ll need to debug using the frame debugger and the Render Graph Viewer to figure out the issue.

1 Like

so you are rendering to a texture, and reading back the result, and based on that result you are changing the rendering layers on a Mesh Renderer?

The read back to CPU would stall the rendering, giving bad performance? So it’s typical in many systems to have a frame delay to avoid this stall.

If you just need to add Mesh Renderers, not remove them, you can inject a pass using the RenderObject feature.

You can use a separate camera to render to the RenderTexture. Or a render request.