reversedDepthBuffer and VR

I’ve been working on a personal game project for over a year now and I’m having an issue with the WebGLRenderer and reversedDepthBuffer in VR. I found a fix for most of the issues I had but 2 remain. I know I can use logarithmicDepthBuffer but there is a performance cost and reversedDepth gives me a better result.

First, I would like to share what I had to do to make this work. When switching to VR (button) with RDB (reversedDepthBuffer) everything is not ordered properly. It’s in reverse. After some digging and help from Google Gemini :grinning_face: , I corrected this issue by doing the following:

  1. Update Material Depth Function
    Traverse the scene and set material.depthFunc = THREE.GreaterEqualDepth;
  2. Ensure Proper Clearing
    renderer.state.buffers.depth.setClear( 0 );

I feel this fix should be part of Three somehow. Maybe it’s a work in progress or I missed something. Let me know.

The remaining issues are:

  1. Shaders
    I could dig deeper for those, but would be gratefull for some help on what to adjust.
  2. VR Controllers
    For some reason they don’t seem to be catched by the scene.traverse when setting the depthFunc. So they don’t display properly.

I believe it’s easy to reproduce for VR by using the Immerssive Web Immulator Chrome extension.

If anyone can help me with these two issues I would appreciate greatly.

Nice find on the fix you already have. What you’re seeing makes sense once you remember that reversed depth flips a couple of core assumptions that three.js and WebXR helpers still rely on.

About your two remaining issues:

Shaders
Any custom or built-in shader that touches depth is going to break unless it’s made aware of the reversed range. With reversedDepthBuffer, depth goes from 1 at the near plane to 0 at the far plane, so anything using gl_FragDepth, manual depth comparisons, or unpacking depth textures needs to be flipped.

If you’re using ShaderMaterial or modifying chunks, look at:

  • anything writing to gl_FragDepth

  • usage of perspectiveDepthToViewZ / viewZToPerspectiveDepth

  • depth comparisons like < that should become >

In practice, the easiest way is to mirror what three.js does internally when renderer.capabilities.reverseDepthBuffer is true. You can hook onBeforeCompile and conditionally swap the logic there instead of rewriting everything from scratch.

Also watch out for postprocessing passes that sample depth textures, those usually assume standard depth too.

VR Controllers
Those aren’t part of your main scene graph at the time you’re traversing it. WebXR injects controller models through renderer.xr.getController() and getControllerGrip(), and they get their own objects and materials after the session starts.

That’s why your scene.traverse doesn’t catch them.

Fix is to explicitly handle them after XR setup, something like:

const controller1 = renderer.xr.getController(0);
const controller2 = renderer.xr.getController(1);

[controller1, controller2].forEach(ctrl => {
  ctrl.traverse(obj => {
    if (obj.material) {
      obj.material.depthFunc = THREE.GreaterEqualDepth;
    }
  });
});

If you’re using controller models (XRControllerModelFactory), you’ll want to traverse the grip objects as well.

One more gotcha: controllers can be created or updated after session start, so make sure this runs after sessionstart or whenever you attach the models.

And yeah, your observation is fair, reversed depth + XR isn’t fully “plug and play” in three.js yet. The engine handles the projection side, but materials, helpers, and examples don’t consistently adapt, so you end up patching depthFunc and shaders manually like you did.

1 Like

Thank you for your reply Umbawa.

For the shaders, I will probaly go with the onBeforeCompile method like you suggested.

For the VR controllers issue, the ‘gotcha’ seems a bit more gotcha. I tried to traverse (controller, grip, hand) when I get the controller connected event, which is the latest I can do this. But even then, no child with material to be found. Inspecting with GChrome DevTools eventually shows the populated children with meshes ! So, I added a setTimeout from the connected event to do a traverse of the controllers 10 seconds later and it worked. I don’t really like this ‘patchy’ solution though, but getting somewhere.

If you have other suggestions, I’m all ears.

I’ll mark your reply as solution because it did give me all I needed to continue.

Thank you again. Very appreciated.

1 Like