I have implemented the Z pre-pass technique into my engine, leading to a 2x performance improvement in a test scene.

However, the technique darkens the objects (but not the skybox), introduces z-fighting, and causes weird artifacts at the border of objects:

enter image description here

Here is the same picture without z pre-pass (performance divided by 2, but no visual bug): enter image description here

Here is the depth framebuffer setup:

  GLCall(glGenFramebuffers(1, &cameraDepthMapFBO));

  GLCall(glGenTextures(1, &cameraDepthMapTexture));
  GLCall(glBindTexture(GL_TEXTURE_2D, cameraDepthMapTexture));

  // If I put GL_FLOAT instead of GL_UNSIGNED_INT here, it doesn't work.


  GLCall(glBindFramebuffer(GL_FRAMEBUFFER, cameraDepthMapFBO));
  GLCall(glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_TEXTURE_2D, cameraDepthMapTexture, 0));

  GLCall(glBindFramebuffer(GL_FRAMEBUFFER, 0));

I then do a z pre-pass where I render the scene on the cameraDepthMapFBO framebuffer:

  GLCall(glViewport(0, 0, SCR_WIDTH, SCR_HEIGHT));
  GLCall(glBindFramebuffer(GL_FRAMEBUFFER, cameraDepthMapFBO));

  // ... all draw commands

  GLCall(glBindFramebuffer(GL_FRAMEBUFFER, 0));

Finally, in the main rendering pass, I copy the depth data from the pre-pass into the main depth buffer, and I render the scene.

  // Clear screen, define viewport
  GLCall(glClearColor(0.01f, 0.01f, 0.01f, 1.0f));
  GLCall(glViewport(0, 0, SCR_WIDTH, SCR_HEIGHT));

  // Copy depth framebuffer onto main depth buffer
  GLCall(glBindFramebuffer(GL_READ_FRAMEBUFFER, cameraDepthMapFBO));
  GLCall(glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0));

  // Set depth settings

  // ... all draw commands

  // Restore depth settings


  1. I have tested to copy the cameraDepthMapFBO via a shader instead of simply using glBlitFramebuffer. It leads to the same result.
  2. The depth map itself seems correct, because I already use it for SSAO.
  3. In the framebuffer setup phase, I have noted that it stops working if I put "GL_FLOAT" instead of "GL_UNSIGNED_INT" as glTexImage2D parameter.

Any idea of what could be wrong?

  • \$\begingroup\$ Are you using multi-sample anti-aliasing (MSAA), or is it possible your graphics driver is applying this AA process automatically? \$\endgroup\$
    – DMGregory
    Commented Mar 25, 2023 at 18:20
  • \$\begingroup\$ Good shot! I've disabled MSAA and uploaded the result. Now there is just some artifacts on surfaces, that look like depth fighting. \$\endgroup\$
    – HenriV
    Commented Mar 25, 2023 at 18:38
  • \$\begingroup\$ My bet there was that the depth reservation from the first pass was counting as a partially overlapping black polygon, so MSAA blended it with equal weight with the actual colour sample, darkening everything. The clue was the fringes on the blades of grass. A polygon edge won't render partial transparency like that alone, without some AA shader/pass going on. It looks like you can fix this to work with MSAA by changing the multisample state when drawing your depth pre-pass, but I'm not fluent enough in OpenGL to know the syntax to do so - I'd invite answers showing how! \$\endgroup\$
    – DMGregory
    Commented Mar 25, 2023 at 18:48
  • \$\begingroup\$ I will probably be able to apply MSAA on the depth buffer, yes! Now I just have to deal with this depth fighting. \$\endgroup\$
    – HenriV
    Commented Mar 25, 2023 at 18:54
  • \$\begingroup\$ Searching for "depth pre-pass z-fighting" is turning up some discussion of using the invariant qualifier on your vertex position, to ensure the floating point math is not optimized differently between the depth-only and real-colour shader passes. A small change in the ordering of operations can change which way a bit rounds, which is enough to throw off an LEQUAL test. But again I'd need an OpenGL expert to weigh in as I haven't experimented with this myself. I hope those leads are useful to you or other readers who can take this further. \$\endgroup\$
    – DMGregory
    Commented Mar 25, 2023 at 18:54

1 Answer 1


There were two problems with my code, thanks @DMGregory for having pointed them out.

1 - The depth buffer also need to be multisampled if the main rendering pass has MSAA. The solution I have found is to render the depth pre-pass and the main rendering pass on an off-screen FBO containing multisampled color and depth texture buffers. Then I copy this color buffer to the default color buffer. It works fine.

2 - Z-fighting is caused by precision errors between the early-z pass and the final pass. These precision errors can be due to slightly different code in the shaders. Even if their result should be the same, minor differences, even in operations order or small other factors, can be sufficient to bring slightly different results after compilation. This problem could be approached by using the glsl "invariant" keyword, which states that the code should be compiled as it is, without optimizations. I haven't been successful with this solution though. This problem can also be tackled by adding an offset to the depths rendered in the early-z pass, which resolves it with a negligible performance hit.

Early depth pass vertex shader:

vec3 FragPos = vec3(model * vec4(aPos, 1.0));
gl_Position = projection * view * vec4(FragPos, 1.0);
gl_Position.z += 0.001; // Offset

The only downside of this offset is that fragments between the real depth and the depth + offset will also have the possibility to be rendered, but this is negligible considering the small offset size.

If anyone has a better answer, I would be glad to accept it.


You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .