Search Unity

  1. Welcome to the Unity Forums! Please take the time to read our Code of Conduct to familiarize yourself with the forum rules and how to post constructively.
  2. Dismiss Notice

ScriptableRenderPasses cannot consistently access the depth buffer

Discussion in 'Universal Render Pipeline' started by LevelExThew, Apr 4, 2022.

  1. LevelExThew

    LevelExThew

    Joined:
    Sep 29, 2017
    Posts:
    9
    A fairly common need when writing custom render passes in URP is rendering to one or more rendertargets while ztesting against (and possibly zwriting) the actual camera depth buffer.

    i.e. inside ScriptableRenderPass.OnCameraSetup:
    Code (CSharp):
    1. cmd.GetTemporaryRT(someRTHandle.id, descriptor);
    2. cmd.GetTemporaryRT(anotherRTHandle.id, descriptor);
    3.  
    4. RenderTargetIdentifier[] mrt =
    5. {
    6.     someRTHandle.Identifier(),
    7.     anotherRTHandle.Identifier(),
    8. };
    9.  
    10. RenderTargetIdentifier depthTarget = ?????
    11. ConfigureTarget(mrt, depthTarget);
    What to pass to ConfigureTarget's second argument?
    There is no documentation, but there appear to be three possibilities:
    - ScriptableRenderer.cameraColorTarget
    - ScriptableRenderer.cameraDepthTarget
    - no argument, which defaults to BuiltinRenderTextureType.CameraTarget

    THE PROBLEM:
    There is basically no way to know which of these variables ACTUALLY contains the camera's depth buffer. One would expect the variable named cameraDepthTarget to contain the camera's depth target, but in practice this is wildly variable. The depth buffer's location changes between cameraColorTarget and cameraDepthTarget, depending on AT LEAST:
    - The editor version
    - CameraData.cameraType (scene, game, preview, vr, etc)
    - Whether MSAA is enabled
    - Whether Depth Texture is enabled
    - Whether a DoF posprocess is active
    - Whether the SSAO rendererfeature is active
    - Whether camera stacking is enabled
    - The Depth Priming mode (in 2021.2+)

    Here is a chart where I attempted to map out the behavior for a specific unity version + platform. There are still cases I didn't cover (notably around DoF), but even after *144* tests there is no discernible pattern:
    2022-04-04 15_52_40.png

    And this is only for *one* platform.

    I attempted a similar test in 2021.2, but gave up after realizing:
    - The results were completely different from 2021.1
    - I would need at least 3x as many test cases to account for Depth Priming modes

    CONCLUSION:
    Doing any non-trivial custom rendering in URP is untenable if any combination of 8+ different settings at both scene and project level can break the API.

    Can someone from Unity please provide a reliable approach to obtaining a reference to the camera depth target inside a ScriptableRenderPass?

    Here is the code that calls ConfigureTarget:
    Code (CSharp):
    1. // Declared in a higher scope
    2. public static class DebugRenderTargets
    3. {
    4.     public static RenderTargetHandle RT_0 = new RenderTargetHandle() {id = Shader.PropertyToID("_RT_0")};
    5.     public static RenderTargetHandle RT_1 = new RenderTargetHandle() {id = Shader.PropertyToID("_RT_1")};
    6.     public static RenderTargetHandle RT_2 = new RenderTargetHandle() {id = Shader.PropertyToID("_RT_2")};
    7. }
    8.  
    9.  
    10. // Inside a ScriptableRenderPass:
    11. public override void OnCameraSetup(CommandBuffer cmd, ref RenderingData renderingData)
    12. {
    13.     RenderTextureDescriptor descriptor = renderingData.cameraData.cameraTargetDescriptor;
    14.     descriptor.colorFormat = RenderTextureFormat.ARGBFloat;
    15.     descriptor.depthBufferBits = 0;
    16.  
    17.     cmd.GetTemporaryRT(DebugRenderTargets.RT_0.id, descriptor);
    18.     cmd.GetTemporaryRT(DebugRenderTargets.RT_1.id, descriptor);
    19.     cmd.GetTemporaryRT(DebugRenderTargets.RT_2.id, descriptor);
    20.  
    21.     RenderTargetIdentifier[] mrt =
    22.     {
    23.         DebugRenderTargets.RT_0.Identifier(),
    24.         DebugRenderTargets.RT_1.Identifier(),
    25.         DebugRenderTargets.RT_2.Identifier(),
    26.     };
    27.  
    28.     // _depthSource is just an enum to control which case I'm testing.
    29.     // Its value is passed in from the RendererFeature.
    30.     switch (_depthSource)
    31.     {
    32.         case DepthSource.RendererCameraColorTarget:
    33.             ConfigureTarget(mrt, renderingData.cameraData.renderer.cameraColorTarget);
    34.             break;
    35.  
    36.         case DepthSource.RendererCameraDepthTarget:
    37.             ConfigureTarget(mrt, renderingData.cameraData.renderer.cameraDepthTarget);
    38.             break;
    39.  
    40.         case DepthSource.BuiltinRenderTextureTypeCameraTarget:
    41.             ConfigureTarget(mrt);
    42.             break;
    43.  
    44.         default:
    45.             throw new ArgumentOutOfRangeException();
    46.     }
    47.  
    48.     ConfigureClear(ClearFlag.Color, Color.clear);
    49. }
    This pass runs in AfterRenderingOpaques, and renders a cylinder with a UV-debug shader into the color MRTs.
    Those MRTs are then blitted back onto the camera target in a second pass.

    A successful test looks like this:
    2022-04-05 15_59_15.png

    The custom-pass cylinder correctly ztests against the existing depth buffer, occluding the far faces of the opaque torus and being occluded by the near faces.
    It also correctly WRITES depth, as shown by the transparent torus, which renders after the cylinder but is still correctly occluded by it.

    A failed test looks like this:
    2022-04-05 16_00_10.png

    The zbuffer was not correctly bound when the cylinder was rendered, so it never fails a depth test, and also does not successfully write depth. The opaque torus is drawn first, then the custom pass draws on top of it, then the transparent torus draws on top of that.
     
    Last edited: Apr 5, 2022
    ElliotB, Prodigga and scottjdaley like this.
  2. daneobyrd

    daneobyrd

    Joined:
    Mar 29, 2018
    Posts:
    101
    When you create your RenderTargetIdentifiers are you setting their depthBufferBits to greater than zero?

    You can handle a lot of the settings for a RenderTargetIdentifier by using RenderTextureDescriptor in cmd.GetTemporaryRT(). I assume your issue is that your depthBufferBits are likely still 0. Some of the settings you are confused by are properties of RenderTextureDescriptor.

    TLDR; You don't need to configure the depth target to get correct depth culling for objects drawn to your color buffer(s). You just need to set the render target’s depthBufferBits.
    Your depth buffer won't display anything as long as your depth buffer bits are set to 0.

    Overview of some structs used in SRP functions

    RenderTargetIdentifier has a lot of constructors, allowing it to be created using:
    RenderTargetHandles are a wrapper class that obfuscate things and only lead to more errors and confusion.¹
    • RenderTargetHandle.id = int;
    • RenderTargetHandle.Identifier() = RenderTargetIdentifier;

    You should be able to access the active camera's depth buffer with:
    ² If the current ScriptablePass' depthTarget has been altered, you can pass in the RendererFeature's ScriptableRenderer to use that renderer's cameraDepthTarget in your render pass.

    If you want to provide a custom RenderTargetIdentifier as the depth target for this render pass, it must have the correct RenderTextureFormat set (.Depth).

    I’ve seen some people create create a RenderTextureDescriptor variable to use across multiple functions and ensure the same settings are being used. This can be useful if you are creating different render targets with many different settings.

    If you want greater control over what is drawn to your active render target, I recommend looking into the DrawRenderers function and it's associated structs.

    Here are some other ideas and methods that an ensure that you are drawing to the depth buffer during your scriptable render pass:

    Calling ScriptableRenderPass.ConfigureInput() with the ScriptableRenderPassInput.Depth argument ensures that the depth texture is available to the Render Pass but that shouldn't be necessary.

    You can set cameraData.requiresDepthTexture = true; for the ScriptableRenderer used in the Execute function of your ScriptableRenderPass.

    cameraData.cameraType can be used to only show the results of your pass in the scene or game view which can be useful depending on your needs.

    One way to check if the camera has a depth texture is with Camera.DepthTextureMode. There are others but I forget.

    It has been a few months since I was last writing scriptable render passes so I may have forgotten a few things.
     
    Last edited: Feb 24, 2023
  3. LevelExThew

    LevelExThew

    Joined:
    Sep 29, 2017
    Posts:
    9
    Thanks for the detailed response, but this is not correct.

    For clarity, here's the actual OnCameraSetup function I used for my testing:
    Code (CSharp):
    1. // Declared in a higher scope
    2. public static class DebugRenderTargets
    3. {
    4.     public static RenderTargetHandle RT_0 = new RenderTargetHandle() {id = Shader.PropertyToID("_RT_0")};
    5.     public static RenderTargetHandle RT_1 = new RenderTargetHandle() {id = Shader.PropertyToID("_RT_1")};
    6.     public static RenderTargetHandle RT_2 = new RenderTargetHandle() {id = Shader.PropertyToID("_RT_2")};
    7. }
    8.  
    9.  
    10. // Inside a ScriptableRenderPass:
    11. public override void OnCameraSetup(CommandBuffer cmd, ref RenderingData renderingData)
    12. {
    13.     RenderTextureDescriptor descriptor = renderingData.cameraData.cameraTargetDescriptor;
    14.     descriptor.colorFormat = RenderTextureFormat.ARGBFloat;
    15.     descriptor.depthBufferBits = 0;
    16.  
    17.     cmd.GetTemporaryRT(DebugRenderTargets.RT_0.id, descriptor);
    18.     cmd.GetTemporaryRT(DebugRenderTargets.RT_1.id, descriptor);
    19.     cmd.GetTemporaryRT(DebugRenderTargets.RT_2.id, descriptor);
    20.  
    21.     RenderTargetIdentifier[] mrt =
    22.     {
    23.         DebugRenderTargets.RT_0.Identifier(),
    24.         DebugRenderTargets.RT_1.Identifier(),
    25.         DebugRenderTargets.RT_2.Identifier(),
    26.     };
    27.  
    28.     // _depthSource is just an enum to control which case I'm testing.
    29.     // Its value is passed in from the RendererFeature.
    30.     switch (_depthSource)
    31.     {
    32.         case DepthSource.RendererCameraColorTarget:
    33.             ConfigureTarget(mrt, renderingData.cameraData.renderer.cameraColorTarget);
    34.             break;
    35.  
    36.         case DepthSource.RendererCameraDepthTarget:
    37.             ConfigureTarget(mrt, renderingData.cameraData.renderer.cameraDepthTarget);
    38.             break;
    39.  
    40.         case DepthSource.BuiltinRenderTextureTypeCameraTarget:
    41.             ConfigureTarget(mrt);
    42.             break;
    43.  
    44.         default:
    45.             throw new ArgumentOutOfRangeException();
    46.     }
    47.  
    48.     ConfigureClear(ClearFlag.Color, Color.clear);
    49. }
    I don't like RenderTargetHandles either but I do understand how they work.

    As you can see, I'm using cameraTargetDescriptor to set up the temporary RTs, and then intentionally setting them to use 0 depth bits. Since I will be manually specifying a depth target, I do not want the color RTs to have their own depth. They DO need to inherit the msaaSamples value from the camera descriptor, however, or you get errors in the Output Merger (which unity can't detect, frustratingly, but at least RenderDoc shows them)

    This combination of settings allows me to pass either cameraColorTarget or cameraDepthTarget as the second argument to ConfigureTarget(), without output merger errors. However, which of those two actually HAS the real depth attachment is unpredictable.

    In other words, you think if I set the RT's depthBufferBits correctly, I should be able to use the single-arg version of ConfigureTarget? If I do not specify a depth target, ConfigureTarget defaults to using BuiltinRenderTextureType.CameraTarget for depth.

    You can see from my test results that this does not work.

    I didn't detail this in the original post, but BuiltinRenderTextureType.CameraTarget fails for multiple reasons. It turns out that whatever RT BuiltinRenderTextureType.CameraTarget points to does NOT use the same depth+msaa bits as cameraTargetDescriptor, so if your color RTs are derived from cameraTargetDescriptor you get Output Merger errors. If you manually futz with the depthBufferBits and msaaSamples values, you can sometimes make it work for specific combinations of pipeline settings, but EVEN THEN, often BuiltinRenderTextureType.CameraTarget just doesn't have any depth data, so you don't get errors but the rendering is still broken.

    BuiltinRenderTextureType.Depth and .DepthNormals are references to the depth TEXTURE, not the depth buffer. They are used when you need to sample the depth texture in a fragment shader, not for doing ztesting during rasterization. Different use case.

    ScriptableRenderer.cameraDepthTarget is the thing that I am testing. This entire thread is about the fact that it doesn't work reliably

    None of this has anything to do with the problem.
    DrawRenderers does not control which buffers are bound as targets.
    ConfigureInput is concerned with generating the depth or depthnormals textures for sampling, not the actual depth buffer.
     
    ElliotB likes this.
  4. daneobyrd

    daneobyrd

    Joined:
    Mar 29, 2018
    Posts:
    101
    If you are using RenderTargetHandle.Identifier() make sure you have initialized the handle ( RenderTargetHandle.Init() ).

    I'm going to look into a past project and see how exactly I got proper depth culling by only setting the depth buffer bits (and not configuring a depth target).

    Edit: I knew there was a method I had forgotten - ConfigureCameraTarget
     
    Last edited: Apr 5, 2022
  5. LevelExThew

    LevelExThew

    Joined:
    Sep 29, 2017
    Posts:
    9
    The RenderTargetHandles are initialized correctly.

    ConfigureCameraTarget is part of ScriptableRenderer, not ScriptableRenderPass.
    Even if it *could* be safely used here, it lacks an overload for MRT.
     
  6. ali_mohebali

    ali_mohebali

    Unity Technologies

    Joined:
    Apr 8, 2020
    Posts:
    119
    Hi @LevelExThew, thanks for the feedback. Admittedly, this is quite confusing to our users, and the lack of comprehensive guidelines doesn't help. We intend to improve the workflow for this. The team is already working on URP Render Graph among other things to make the customizability and dealing with custom passes more intuitive. We also have plans for improving the guidelines and documentation for it.

    Accessing camera targets through ScriptableRenderer.cameraColorTarget, and Scriptable.cameraDepthTarget should be the right way to go. But I have tried to recreate your issue based on your explanation, and I can see something funky going on when MRT is used. I am discussing this with the URP engineers. But to make sure we have the same repo of your case, can I ask you to submit a bug report with a repo case, please.

    And on a side note, we definitely appreciate feedback and understand the frustrations. We are always happy to fix issues and bugs reported. However, inviting users to cyberbully Unity and devs on the forums will not fix anything. But reporting bugs, especially with repo cases, will.

    upload_2022-4-12_14-40-53.png
     
    Last edited: Apr 12, 2022
  7. LevelExThew

    LevelExThew

    Joined:
    Sep 29, 2017
    Posts:
    9
    Hi, thanks a lot for checking this out. I'll put a bug report together when I get a chance. I wasn't sure whether this is a bug or just intended-but-undocumented behavior, so figured making a thread about it first made more sense.

    In the meantime, is there any chance we could get some guidance on how to work around this issue in the current unity versions? The behavior seems to be consistent, just also inscrutable without a very deep understanding of URP's internals.

    That tweet was 100% a joke. I had thought the phrasing made it clearly silly ("very very politely" being absurd, and spelling out "Unity Technologies", as in the billion-dollar corporation rather than any actual person or team). It received 4 Likes and did not lead any of my friends to come here and cyberbully anyone.

    But, since you have taken it seriously, it seems I failed at that. I apologize.
    I know the URP team is probably insanely stressed out right now, and I certainly don't want to make things worse for any of you, even if by accident.

    I have removed the tweet to ensure no one does try to act on it.
     
    Shaderic likes this.
  8. ali_mohebali

    ali_mohebali

    Unity Technologies

    Joined:
    Apr 8, 2020
    Posts:
    119
    Thank you, if you ping me with the bug report number I would appreciate it, I can then follow up.

    No worries, absolutely. Forums are perfect for it.

    Ideally, if this is a bug I would like us to fix and backport it to make it convenient for our users. Having said that, the engineers understand the complexity of this better, so give us a bit of time to investigate and get back on what our approach will be (either fix/backport or provide guidelines to users)
     
  9. ManueleB

    ManueleB

    Unity Technologies

    Joined:
    Jul 6, 2020
    Posts:
    98
    Hey @LevelExThew, as @ali_mohebali already mentioned, there is a lot of work going on in improving the pipeline attachments management and workflow as we agree that it was a problematic area in terms of user customization workflow. In 22.x the specific issue you are facing should be already fixed, but I am waiting for your bug report so it can be tested using your specific project.

    In general a good way to figure out the way color and depth attachments are setup is to look at this code in ScriptableRenderer.cs:

    Code (CSharp):
    1. static void SetRenderTarget(CommandBuffer cmd,
    2.             RenderTargetIdentifier colorAttachment,
    3.             RenderBufferLoadAction colorLoadAction,
    4.             RenderBufferStoreAction colorStoreAction,
    5.             RenderTargetIdentifier depthAttachment,
    6.             RenderBufferLoadAction depthLoadAction,
    7.             RenderBufferStoreAction depthStoreAction,
    8.             ClearFlag clearFlags,
    9.             Color clearColor)
    10.         {
    11.             // XRTODO: Revisit the logic. Why treat CameraTarget depth specially?
    12.             if (depthAttachment == BuiltinRenderTextureType.CameraTarget)
    13.             {
    14.                 CoreUtils.SetRenderTarget(cmd, colorAttachment, colorLoadAction, colorStoreAction, depthLoadAction, depthStoreAction, clearFlags, clearColor);
    15.             }
    16.             else
    17.             {
    18.                 CoreUtils.SetRenderTarget(cmd, colorAttachment, colorLoadAction, colorStoreAction,
    19.                     depthAttachment, depthLoadAction, depthStoreAction, clearFlags, clearColor);
    20.             }
    21.         }
    so, if the pass is targeting the backbuffer depth (BuiltinRenderTextureType.CameraTarget is the backbuffer), URP calls the CoreUtils.SetRenderTarget overload that takes only the color attachment as parameter. What this does under the hood is to set as depth attachment the same RenderTexture that is used as the color attachment, which is equivalent of what you achieved by configuring renderingData.cameraData.renderer.cameraColorTarget as the depth target.

    When not writing to the backbuffer, it means that an offscreen depth texture has been created (you can do a search for "createDepthTexture" in UniversalRenderer.cs to see in which scenarios this happens), and CoreUtils.SetRenderTarget will take both color and depth attachments as parameters. This second scenario means that using renderingData.cameraData.renderer.cameraDepthTarget in your sample code will work now.

    So in short, a workaround you can use for now to configure the correct depth target, is to check whether the current camera depth is the backbuffer one or not and setup your depth in the same way as the previous code snippet does:


    Code (CSharp):
    1. if (renderingData.cameraData.renderer.cameraDepthTarget == BuiltinRenderTextureType.CameraTarget)
    2.                 ConfigureTarget(mrt, renderingData.cameraData.renderer.cameraColorTarget);
    3.             else
    4.                 ConfigureTarget(mrt, renderingData.cameraData.renderer.cameraDepthTarget);
     
  10. zhoucy001

    zhoucy001

    Joined:
    Jul 15, 2019
    Posts:
    9
    More accurately, when ENABLE native render pass, BuiltinRenderTextureType.Depth refers to back buffer depth BUFFER; while DISABLE native render pass, BuiltinRenderTextureType.Depth refers to depth TEXTURE (according to Unity source code).
    I have problem using BuiltinRenderTextureType.Depth when disable native render pass that Unity Console report warning " built-in render texture type 3 not found while executing ".
    I'm wondering if BuiltinRenderTextureType.Depth is still supported whendisable native render pass.

    PS: BuiltinRenderTextureType.Depth works in Build-in Render Pipeline, while not in SRP (disable native render pass).
     
  11. Tudor

    Tudor

    Joined:
    Sep 27, 2012
    Posts:
    150
    Quesiton about where the Stencil Buffer fits in this context.

    To me it seems that when you do a scriptable render pass and use a blit with `renderingData.cameraData.renderer.cameraColorTarget;` the material/shader you blit with will not receive the stencil buffer set during the geometry passes.

    According to the frame debugger the stencil buffer isn't cleared, and I can successfully access it in the last stages of the transparency queue.

    Am I misunderstanding how to use `cameraColorTarget` or how to specifically also pass in the stencil in a scriptable render pass?

    I know that I'm supposed to use a `descriptor.depthBufferBits = 24` on a new temporaryRT, if I want it to pass the stencil to the render texture. But my issue is not getting the stencil in the first place..?