On WebGPU, with Babylon’s default depth setup, nearly coplanar surfaces z-fight noticeably more than the same scene on WebGL2. Setting engine.useReverseDepthBuffer = true makes it go away completely. We hit this in a game where tree canopies are thousands of overlapping, camera-facing leaf cards: on WebGPU the cards visibly traded places frame to frame as the camera moved.
Repro (Playground, WebGPU engine). A grid of green cards, each with a slightly smaller red card a tiny distance behind it. The red card is drawn second, so it wins every depth tie, and red should never show. Rows are distances from the camera (25, 100, 400, 1600). Columns are the gap as a fraction of that distance (1e-3 down to 1e-5).
const createScene = () => {
// engine.useReverseDepthBuffer = true; // <- uncomment: all the red disappears on WebGPU
const scene = new BABYLON.Scene(engine);
scene.clearColor = new BABYLON.Color4(0, 0, 0, 1);
const camera = new BABYLON.FreeCamera("cam", BABYLON.Vector3.Zero(), scene);
camera.minZ = 0.1; camera.maxZ = 10000; // a typical open-world range
camera.setTarget(new BABYLON.Vector3(0, 0, 1));
const flat = (name, color) => {
const m = new BABYLON.StandardMaterial(name, scene);
m.disableLighting = true; m.emissiveColor = color; return m;
};
const green = flat("green", new BABYLON.Color3(0, 1, 0));
const red = flat("red", new BABYLON.Color3(1, 0, 0));
const distances = [25, 100, 400, 1600];
const gaps = [1e-3, 3e-4, 1e-4, 3e-5, 1e-5];
distances.forEach((d, row) => gaps.forEach((g, col) => {
const x = (col - 2) * 0.16 * d, y = (1.5 - row) * 0.16 * d;
const card = (mat, z, s) => {
const p = BABYLON.MeshBuilder.CreatePlane(mat.name, { size: 0.12 * d * s }, scene);
p.position.set(x, y, z); p.rotation.y = 0.5; p.material = mat; // tilted, so depth varies across it
};
card(green, d, 1);
card(red, d * (1 + g), 0.95); // drawn second: it wins every depth tie
}));
return scene;
};
Measured: the share of card pixels where the red card wrongly shows, averaged over the 20 cells. Babylon 9.31.0 and 9.12.0 gave identical numbers. Chrome, Intel Iris Xe (Dawn), 1200×700, no MSAA.
| Setup | Red showing (mean of 20 cells) |
|---|---|
| WebGL2 | 25.9 % |
WebGPU, default (depth24plus-stencil8, standard Z) |
39.1 % |
WebGPU, stencil: false (depth32float, standard Z) |
39.1 % (no change) |
WebGPU, useReverseDepthBuffer = true |
0.0 % |
WebGPU, reversed Z + depth32float |
0.0 % |
(Screenshot attached: WebGL2, WebGPU default, and WebGPU reversed, left to right.)
On the real scene, a slow camera orbit around a leafy tree, counting pixels that flip between consecutive frames, the result is the same: WebGL2 0.075‰, WebGPU default 0.157‰, WebGPU reversed Z 0.029‰.
A float depth buffer alone changes nothing; reversed Z is what fixes it. Our reading is that with WebGPU’s [0, 1] clip-space depth, the float32 depth the vertex stage writes near 1.0 has about one bit less to work with than WebGL’s [-1, 1] range, which the viewport transform then halves. Reversed Z puts the float’s dense range at the far plane instead. I know it’s been said that WebGPU’s depth precision should be no different from WebGL’s ( What are the limits of WebGPU? ), so I wanted to share numbers showing a difference in practice.
Suggestion: since reversed Z already works fully on WebGPU (as @Evgeni_Popov noted in Reverse Depth Buffer question ), could WebGPU default to useReverseDepthBuffer = true, ideally with a float depth buffer? Failing that, a note on the WebGPU breaking-changes page would help: “if you see more z-fighting than on WebGL, set engine.useReverseDepthBuffer = true”. It took us a while to pin down.
Thanks for the great engine!
