Search Unity

  1. Megacity Metro Demo now available. Download now.
    Dismiss Notice
  2. Unity support for visionOS is now available. Learn more in our blog post.
    Dismiss Notice

Question How to both write to and read from a RenderTexture in one draw call, using command buffers?

Discussion in 'General Graphics' started by Sinterklaas, May 12, 2021.

  1. Sinterklaas

    Sinterklaas

    Joined:
    Jun 6, 2018
    Posts:
    93
    Hi, I'm trying to build a custom "3D sprite" renderer in Unity, using command buffers. Currently, I am attempting to introduce a feature similar to the grab texture, where every draw call renders into a render texture, which other draw calls should be able to sample from. I aim to use this to create effects like water or glass distorting objects behind them. All objects in my scene are renderered in a back-to-front order to make this possible. My current code for drawing is basically this:

    Code (CSharp):
    1. commandBuffer.SetRenderTarget(renderTarget);
    2. commandBuffer.ClearRenderTarget(true, true, Color.clear, 0);
    3. foreach (QuadRenderer r in quadRenderers)
    4. {
    5.     commandBuffer.SetGlobalTexture("_GrabTex", renderTarget);
    6.     // Also set a bunch of other properties that aren't relevant here
    7.     commandBuffer.DrawMesh(r.mesh, r.modelMatrix, r.material, 0,
    8.         r.material.FindPass("ForwardBase"));
    9. }
    10. commandBuffer.Blit(renderTarget, BuiltinRenderTextureType.CameraTarget);
    My fragment shader looks like this:

    Code (CSharp):
    1. float4 Frag(Interpolators i) : SV_TARGET
    2. {
    3.     float4 result = 0;
    4.  
    5.     // Omitted: sample albedo, normal & specGloss textures, calculate lighting, etc.
    6.  
    7.     // Debug: sample grab texture when enabled, replace pre-existing result color.
    8.     #if defined(USE_GRAB)
    9.     float4 grabPixel = tex2D(_GrabTex, i.uv);
    10.     result = grabPixel;
    11.     #endif
    12.  
    13.     return result;
    14. }
    These uv values are also used to sample the albedo, normal and specGloss textures, and don't use screen-space uv coordinates yet (for debug reasons), so I'd expect any quad with USE_GRAB enabled to display the full render texture (the uvs go from (0, 0) in the bottom-left of the quad to (1, 1) in the top-right). While the final render texture does get blitted to the screen correctly, any draw call with my custom shader that has USE_GRAB enabled is rendered as fully black. I have no idea why. Other textures like albedo show up normally.

    My render texture is created with this code:
    Code (CSharp):
    1. Camera mainCamera = Camera.main;
    2. renderTarget = new RenderTexture(mainCamera.pixelWidth, mainCamera.pixelHeight, 0);
    Does anyone have a clue what I'm missing here? Is it even possible to sample from and render to the same render texture within a single draw call?
     
    Last edited: May 12, 2021
  2. Sinterklaas

    Sinterklaas

    Joined:
    Jun 6, 2018
    Posts:
    93
    Welp, I think I found out what the problem was. It seems you indeed cannot make a shader sample from a render texture that is also the current render target.

    Just in case anyone else stumbles upon this topic with a similar problem, to make this work, I did the following:
    Code (CSharp):
    1. commandBuffer.SetRenderTarget(renderTarget);
    2. commandBuffer.ClearRenderTarget(true, true, Color.clear, 0);
    3. foreach (QuadRenderer r in quadRenderers)
    4. {
    5.     if (renderer.material.IsKeywordEnabled("USE_GRAB"))
    6.     {
    7.         commandBuffer.CopyTexture(renderTarget, renderTargetBuffer);
    8.         commandBuffer.SetGlobalTexture("_GrabTex", renderTargetBuffer);
    9.     }
    10.     commandBuffer.DrawMesh(r.mesh, r.modelMatrix, r.material, 0,
    11.         r.material.FindPass("ForwardBase"));
    12. }
    13. commandBuffer.Blit(renderTarget, BuiltinRenderTextureType.CameraTarget);
    So first, you check if the material actually uses the custom grab texture (in my case, check if the appropriate shader keyword is enabled), and if so, you issue a command to copy the render target to another "buffer" render texture (which should have the same size and format), and then you sample from that buffer texture instead of sampling from the main render texture. The rest of the code remains unchanged. Problem solved!

    Of course, this does mean that you need to make a copy of the render target for every object that uses the grab texture, which is a pretty big performance problem if you use it lots. If anyone knows a more efficient way to solve this problem, I'd like to hear it, so I'm leaving the threat on "help wanted" for now.
     
    Genebris likes this.