Why Does Render Queue Affect Materials With "ZTest Always" and "ZWrite On"?

Note: I originally posted this thread in the “General Graphics” forums, but I decided it would be more appropriate here as well.

Question:
Why do materials with “ZTest Always” and “ZWrite On” render behind materials with greater render queue values?

Example:
I have 2 cubes in a scene. Both use the same shader and the only difference between them is that one cube’s shader has “ZTest Always”. The black cube has “ZTest LEqual” and the blue cube has “ZTest Always”

The blue cube always renders in front of the black cube as expected when they are both in the same render queue (2000), but when I change the blue cube to render queue 1000, the black cube renders in front.

Same Render Queues (Note: the blue cube is positionally behind the black cube, but renders ahead as expected):

Different Render Queues (Note: the blue cube is positionally behind the black cube, but still renders behind the black cube even though it is “ZTest Always”):

Shouldn’t “ZTest Always” with “ZWrite On” cause all other depth tests against blue cube to fail regardless of render queue?

Context:
I am trying to create a Vignette for a VR game. I want the Vignette to be rendered first and in front of everything else to prevent overdraw and increase performance. To have it render first, I put it on the background render queue (1000). To have it always render ahead of everything else, I set “ZTest Always”. But the combination of these too techniques doesn’t seem to work as illustrated above.

More Context:
I am using the URP on Unity 2020.3.25f1 and working with the Quest 2.

I think the problem you’re having is understanding what exactly ZTest does and how it relates to the rest of the rendering. But lets go over the main three components you’re talking about.

The material’s queue helps determine the render order. The render order is explicitly the order the GPU renders objects in. Higher queues are guaranteed to render after lower queues, and materials with the same queue are sorted based on things like the sorting order (front to back for < 3000 queue “opaques”, back to front for > 3000 queue “transparencies”), if a shader has multiple passes or not, etc.
ZWrite controls if a shader writes to the depth buffer, aka the z buffer. This records the depth of an object at each pixel it is visible.
ZTest controls how a shader tests against the existing depth buffer to determine what pixels it should render to. If using the default LEqual (aka “less or equal”), the GPU checks to see if the current pixel is at least as close to the camera as the value in the depth buffer before rendering, and skips rendering that pixel if it’s further away.

Without the use of ZWrite On and an otherwise empty depth buffer, the sorting is entirely dependant on the sorting order. This is how things like transparent objects, sprites and UI are rendered, so the sorting has to be strictly controlled by the user so the “background” objects render first and the “foreground” objects render last. Sprites primarily make use of the sorting index property on the sprite renderer, though also make use of the distance from the camera like 3D objects, and UI elements are 100% controlled by their order in the hierarchy.

ZWrite On along with ZTest LEqual exist to ensure that the per pixel sorting of opaque surfaces result in exactly the same final image regardless of the sorting order. This isn’t to say the sorting doesn’t matter, as it can still have an impact on performance, but which pixels of each object appear in final image does not depend on the order.

When you use ZWrite On with ZTest Always, the order matters again. This is because when the object using this is rendered, it is ignoring the depth buffer. ZTest Always is saying “always render”. However this doesn’t mean it’ll always be visible in the final image, because there’s no guarantee something that renders after it won’t be closer to the camera, or also have ZTest Always, in which case that object will be visible.

And that’s what you’re seeing in your examples. With the same render queue, the opaque objects are being sorted front to back, meaning the closer black cube is rendering first, and then the further blue cube is rendering afterwards and it’s rendering “on top”. With the blue cube using a queue of 1000 it renders first to an empty render target, and then the black cube renders afterwards, and because it’s closer than the blue cube the black cube renders “on top”.

3 Likes

I had a similar issue that happened to be from rendering to a render texture that did not have depth turned on.

Remember to turn on the depth buffer in whatever you’re rendering to when using transparency.

Hopefully that helps someone.