精選文章

SmallBurger Asset Home

  SmallBurger

2025年5月28日 星期三

ScriptableRenderPass to RenderGraph: Smooth Transition in Unity 6 URP


With the official release of Unity 6 LTS, RenderGraph is no longer an experimental toy—it has become a significant evolution in Unity’s rendering pipeline. For developers using the Universal Render Pipeline (URP), how to seamlessly transition from the traditional ScriptableRenderPass to RenderGraph has become a hot topic.

I created the URPRenderGraphBridgeExample project with the hope of helping everyone maintain compatibility with different versions of Unity’s rendering systems on a single codebase, making it easier to learn and use RenderGraph.

Next, I’d like to share some design ideas and insights, which I hope will help those of you who are exploring RenderGraph!

Let me start with a key takeaway: “Always bind your resources precisely.”
When working with RenderGraph, this should be like a warning label stuck on every developer’s monitor. The core philosophy of RenderGraph is explicit resource declaration—you must clearly state what you’re using and how you’re using it. No ambiguity, no wishful thinking.

Now, let’s go through the overall workflow:

1. Retrieve relevant bound resources from ContextContainer (frameData):
In the Unity RenderGraph workflow, you typically obtain the necessary context for the current frame (such as camera data, command buffer, resource handles, etc.) from RenderGraphContext or a similar container.

2. Create a RenderGraphBuilder interface and corresponding PassData:
You call RenderGraph.AddRenderPass<T>("PassName", out passData) to create a pass, which returns a RenderGraphBuilder. You use this builder to set up resource reads/writes, dependencies, and more.

3. Bind the required resources precisely:
With the builder’s ReadTextureWriteTextureReadBuffer, etc., you explicitly declare which resources the current pass needs. RenderGraph will automatically track dependencies.

4. Use the RenderGraphBuilder interface to call SetRenderFunc:
You must use a lambda function here; you cannot use a global function, or it will cause garbage collection (GC) issues.

5. The entry point: RecordRenderGraph:
As the name suggests, this is where you collect your rendering pass requirements. The order and process of rendering are managed by RenderGraph. For example, instead of worrying about how many times you call ConfigureRenderTarget, you just need to specify what you want to bind (remember: bind precisely!).

Let’s take a look at the implementation of a typical render object pass, CustomRenderObjectPass:

First, retrieve the relevant resources:

UniversalResourceData resourceData = frameData.Get<UniversalResourceData>();
UniversalRenderingData renderingData = frameData.Get<UniversalRenderingData>();
UniversalCameraData cameraData = frameData.Get<UniversalCameraData>();
UniversalLightData lightData = frameData.Get<UniversalLightData>();

Next, set up the Builder interface:
Use the builder to clearly define how each resource will be accessed.
SetRenderAttachment and SetRenderAttachmentDepth are especially important here—unlike UseTexture, RenderGraph will batch render state changes for these attachments. For other UseTexture calls, if the resource is not written to, just specify read.

P.S.: If you’re using ForwardPlus, you must set builder.UseAllGlobalTextures(true). I personally disagree with this approach—shouldn’t we specify resource usage precisely? Using everything means no optimization. I think Unity should provide a handle for ZBins-related buffers so we can bind resources precisely. See the code comments for more details.

Then, RenderStateBlock:
Since RendererListParams requires a RenderStateBlock, you must provide one even if you don’t use it—just set it to RenderStateMask.Nothing if you rely entirely on material definitions.

Finally, SetRenderFunc:
This is where the actual rendering call happens. In my tests, you must use a lambda function to avoid GC. If you don’t like this style, consider calling a static function within the lambda to maintain a reusable structure.

As for the example of a full-screen rendering pass (DistortionPass), there’s nothing particularly special to mention, except that the render target written to is only used by the current pass. So here, use builder.UseTexture(activeColorTH, AccessFlags.Write); and call Blitter.BlitCameraTexture to handle the full-screen pass.

Finally, regarding compatibility between 2022.3.x and Unity 6:
I use partial classes to separate the logic, unlike Unity URP where everything is written in a single RenderPass. Here’s a summary:

  • Shared parts: Elements like RenderEventShaderTagIDFilteringSettings, and ProfileSampler are kept in the original RenderPass.
  • Original RenderPass: Override Execute and use the command buffer as before.
  • RenderGraphPass: The file name is suffixed with _RenderGraph, and you override RecordRenderGraph.

Why use partial classes instead of inheritance? Mainly because the event sources are different, so inheritance doesn’t fit well. If anyone has better suggestions for splitting the logic, feel free to comment and discuss.

Related GitHub project:
URPRenderGraphBridgeExample

沒有留言:

張貼留言