Setting multiple render targets without a depth buffer?

The only overloads of CommandBuffer.SetRenderTarget that allow for MRT (multiple targets) also require a depth buffer to be specified. For example, SetRenderTarget(RenderTargetIdentifier[ ] colors, RenderTargetIdentifier depth).

What is the proper value to pass for depth when there is no depth buffer?

I’m working on the builtin render pipeline, on 2021.3.1.

I’ve tried the obvious things, like (RenderTargetIdentifier)RenderTargetIdentifier.AllDepthSlices, but without success. In fact, this appears to lead to buggy behaviour. As confirmed by RenderDoc, this sometimes does what I’m wanting, and sometimes errors with Invalid output merger - Depth target is different size or MS count to render target(s). The behaviour appears 50/50 random between frames.

Note that that’s an error showing up in RenderDoc. Unity itself doesn’t display any error, it simply fails to switch render targets, and clears the previously bound one instead.

My comlete test repro, there’s nothing else in this project interfering here.

var cb = new CommandBuffer();
var textureId = Shader.PropertyToID("RenderTextureName");
cb.GetTemporaryRT(textureId, new RenderTextureDescriptor(
    width: 1024,
    height: 1024,
    colorFormat: RenderTextureFormat.ARGB32,
    depthBufferBits: 0,
    mipCount: 0
 ));

// This works fine
//cb.SetRenderTarget(textureId);

// This breaks
//cb.SetRenderTarget(textureId, (RenderTargetIdentifier)RenderTargetIdentifier.AllDepthSlices);

// As does this, which is what I'm actually trying to do
cb.SetRenderTarget(
    new RenderTargetIdentifier[] {
        textureId,
        /*and others*/
    },
    (RenderTargetIdentifier)RenderTargetIdentifier.AllDepthSlices
);

cb.ClearRenderTarget(false, true, Color.blue);
Graphics.ExecuteCommandBuffer(cb);

I’m hoping this is a me problem, and not another case of finding an engine bug within an hour of trying to do some interesting rendering work.

Try BuiltinRenderTextureType.None and BuiltinRenderTextureType.CurrentActive.

Don’t remember exactly what happened when I tried it, but my guess

  • none probably didn’t work
  • current active does work but could either bind nothing, the current target which happens to be the right size or the current target which gives a warning if it’s the wrong size

If you use CurrentActive, you have to make sure to disable depth testing and writing (ZWrite Off, ZTest Always).

BuiltinRenderTextureType.None is one I had already tried.

BuiltinRenderTextureType.CurrentActive is a new idea I believe, and it sort of works! A shader with ZWrite Off, ZTest Always appears to work fine, but a simple ClearRenderTarget(false, true, color) is still broken. I can work around that obviously, but it still feels like I’m doing this The Wrong Way.

Given the strange behaviour, I’m a little concerned that this solution is going to have platform compatibility or stability problems, or something like that. It seems very strange that there isn’t simply a depth free overload. If I were able to use the single target version, all of these things would work seamlessly, so clearly the engine is doing something different internally in that case.

Thanks for the pointer @georgerh , I think this has gotten me far enough to progress for now, but I would still like to hear whether there’s a more The Right Way way of doing this. Perhaps a Unity staff member feels like charming in? :slight_smile:

Maybe you could do two SetRenderTarget calls, one to unbind the depth target and then one to bind the MRT targets with CurrentActive.

But yeah, would be great to hear from Unity.

It’s also worth nothing that it’s not documented how SetRenderTarget(RenderTexture rt) behaves exactly.

Render textures may contain a color AND/OR depth buffer. Does SetRenderTarget bind whatever the render texture contains and unbind what it does not contain? Or does it leave the other target alone?

var colorOnly = new RenderTexture(width, height, 0, RenderTextureFormat.ARGB32);
var depthOnly = new RenderTexture(width, height, 16, RenderTextureFormat.Depth);
var colorAndDepth = new RenderTexture(width, height, 16, RenderTextureFormat.ARGB32);

SetRenderTarget(colorOnly) // does this unbind the current depth texture?
SetRenderTarget(depthOnly) // does this unbind the current color texture?
SetRenderTarget(colorAndDepth) // does this bind both color and depth?

I think you can just pick any of your existing render textures and bind the RenderTexture.depthBuffer. Preferably one where RenderTexture.depth == 0;

I’m actually finding that CurrentActive or binding any other existing buffer throws an exception if the sizes don’t match. I’ve been able to just about shift things around to work because I got lucky that I’ve got some other RTs of the right size, but this isn’t a robust or proper solution. CurrentActive is particularly unstable, because it relies on state that may be coming from other parts of the application.

I’d appreciate hearing from Unity about what the proper way to do this is!

Another workaround would probably be to just give the RenderTexture depth and not use it, but that’s no fun.

I’ve decided to change this thread to being marked as a feature request for now, as pending some revelation of The Right Way, I think Unity could really use an extra overload on CommandBuffer.SetRenderTarget that takes an array without a separate depth buffer.

And somewhat off topic, but overloads that take Span would be nice too, just to avoid having to awkwardly keep track of a reusable array to avoid GC.

So it turns out I’m a giant dumb dumb head.

You can just pass the any of the RenderTargetIdentifiers for the RenderTexture you’re targeting as depth. If they have 0 bits of depth, that’s fine.

I swear that was the first thing I tried… But it seems to work now. Maybe there’s some other bug that I hit the first time, as I’ve changed the renderer about quite a bit since then.

I meet the same issue of setting MRT without a depth buffer, it’s not allowed in Unity (tested with 2021.3).
It’s pretty often that in a full-screen post-process writing to MRT, that case requiring a depth buffer is very inconvenient.

I have a similar issue.
I’m using an mrt for gbuffer, and then a second mrt in the lighting pass as I need separate output for diffuse/specular
I need to preserve the depth buffer generated by the first mrt render target, and I don’t need the lighting pass to write to it.
If I set buffer.SetRenderTarget(frameBufferID, BuiltinRenderTextureType.None); on the lighting stage then it breaks the UI in scene view.


So my only solution is to create a temporary empty render target (at the same width/height), which is a waste of memory.

Have been running into this issue a lot. I’ve been using BuiltinRenderTextureType.None for ages, but would always have issues in random edge cases. Using a renderTexture with no depth component fixes most of them, thanks for the tip!

It’s really dumb that Unity has no “DepthTexture.None” or similar, and the whole API design of mixing color and depth targets in the same RenderTexture is really weird and creates a lot of headaches too. They should really just remove it.

It would’ve been great to get some Unity acknowledgement of this. Maybe @richardkettlewell or somebody would like to comment on the proper way to handle this?

Still no sane replies from Unity, nor this bug is fixed in 6000.2. What even is the rationale behind this quirk? None of the known APIs support MRT but do not support MRT without depth target…

If other solutions don’t work, like using BuiltinRenderTextureType.None, I would try to set one of the RenderTextures used for the MRT as the depth, even if it doesn’t have a depth buffer.
That works for me, inspecting the result with RenderDoc it shows the depth buffer set to “no resource”.

// Allocate this once somewhere once instead of continually.
static readonly RenderTargetIdentifier[] colorBuffers2 = new RenderTargetIdentifier[2];

// rt0 and rt1 are RenderTextures with no depth created elsewhere
colorBuffers2[0] = rt0;
colorBuffers2[1] = rt1;
cmd.SetRenderTarget(targets, rt0); // Or rt1, doesn't matter

Found this, saying that it’s by design. But as I just said, I managed to make it work so this may not be relevant: