MSAA on a RenderTarget breaks GTAO on WebGPU

webgpu, three-js, post-processing, debugging

If you are building an offscreen HDR pipeline in three.js on WebGPU and you turn on MSAA, GTAO stops working. Not subtly — the pass fails to compile and you get this:

Invalid ShaderModule "fragment_GTAO"

There is nothing wrong with your AO settings. The problem is two lines away, in how you created the render target.

What actually happens

Asking a RenderTarget for anti-aliasing:

const target = new THREE.RenderTarget(width, height, {
  samples: 4,
})

samples > 0 makes the target multisampled — and that includes its depth attachment. GTAO reconstructs position from depth, so it binds that attachment as a sampled texture. A multisampled depth texture is not the same binding type as a regular one, the shader module it generates is invalid, and the pass fails at compile time rather than at draw time. Hence a compile error with no obvious connection to the thing you changed.

Set samples: 0 and GTAO compiles again. Which is correct, and also means you now have no anti-aliasing.

What to do instead

For an offscreen HDR chain, resolve anti-aliasing at the end, after tonemapping, rather than asking the render target for it:

  1. FXAA as the last pass in the chain. It works on the finished LDR image and never touches depth, so nothing upstream cares.
  2. A Karis-weighted first bloom downsample. Not anti-aliasing exactly, but it solves the problem MSAA was hiding: single bright pixels on HDR silhouettes that flicker as white dashes. Weighting the first downsample by luminance kills those fireflies at the source.

One ordering constraint worth stating, because it is easy to get backwards: FXAA needs finished sRGB input. That means the bloom chain owns tonemapping and encoding (renderer.toneMapping = NoToneMapping), and the FXAA pass decodes, does its work, and lets the canvas re-encode — a net identity round trip. Exposure lives on a chain uniform, not on the renderer.

Reading the error

The wider lesson is about the error itself. Invalid ShaderModule "fragment_GTAO" names the pass that failed, not the thing that broke it, and WebGPU validation errors in three.js often arrive this way — as a cascade whose loudest member is the least informative.

When a pipeline or shader-module error appears after a change that seems unrelated, check what that change did to your attachments before you touch the shader. The primary error is usually logged earlier, and it is usually much more specific than the one that stopped your frame.