# MSAA on a RenderTarget breaks GTAO on WebGPU — Tobi Moccagatta

> Why three.js GTAO fails with `Invalid ShaderModule "fragment_GTAO"` the moment a RenderTarget asks for samples, and what to do instead.

- Site: https://tobis.vision — Tobi Moccagatta, Creative Developer at basement.studio
- Canonical: https://tobis.vision/notes/msaa-breaks-gtao-on-webgpu
- Contact: contact@tobis.vision

_Published 2026-08-31 · 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:

```js
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.

---

## Other pages

- [Creative Developer](https://tobis.vision) — hey, I'm tobi. creative developer at basement.studio, working on shaders and 3D on the web. peek into my vision. Markdown: https://tobis.vision/index.md
- [Experiments](https://tobis.vision/experiments) — A growing list of experiments across shaders, sound, scenes, and interaction. Markdown: https://tobis.vision/experiments.md
- [Works](https://tobis.vision/works) — Shaders and real-time 3D for Coinbase, Modal, Baseten, E2B and Rox at basement.studio, plus Shader Lab and Annex. Markdown: https://tobis.vision/works.md
- [About](https://tobis.vision/about) — Creative developer at basement.studio working on shaders, WebGL and WebGPU. Self-taught, in Buenos Aires, open to freelance. Markdown: https://tobis.vision/about.md
- [Contact](https://tobis.vision/contact) — How to reach Tobi Moccagatta: one email address, what to put in it, what he takes on, and current availability from Buenos Aires (UTC-3). Markdown: https://tobis.vision/contact.md
- [Privacy](https://tobis.vision/privacy) — What this site collects and what it does not: cookieless analytics, no forms, no trackers, and an in-memory rate-limit counter that expires with its window. Markdown: https://tobis.vision/privacy.md
- [Developer resources](https://tobis.vision/developers) — Machine-readable tobis.vision: llms.txt, the OpenAPI spec, the read-only content API, and the markdown representation of every page. Markdown: https://tobis.vision/developers.md
- [Notes](https://tobis.vision/notes) — Write-ups from building shaders and real-time 3D on the web: WebGPU, TSL and three.js, mostly the parts that went wrong. Markdown: https://tobis.vision/notes.md
- [Grass (2026)](https://tobis.vision/experiments/grass) — A walkable Val d'Orcia field: 790,000 instanced grass cards on a procedurally baked atlas, backlit through a translucency map, bent by a travelling wind field and parted by a displacement trail you leave behind you. Markdown: https://tobis.vision/experiments/grass.md
- [Critters](https://tobis.vision/experiments/critters) — Soft vinyl creatures raymarched from signed distance fields, morphing between forms as one continuous surface and reacting to your cursor. Markdown: https://tobis.vision/experiments/critters.md
- [LPV](https://tobis.vision/experiments/lpv) — Real-time global illumination via light propagation volumes, hand-written in TSL on WebGPU compute. Markdown: https://tobis.vision/experiments/lpv.md
- [BIP](https://tobis.vision/experiments/bip) — A holoprojector experiment with BIP, the robot influencer. Markdown: https://tobis.vision/experiments/bip.md
- [Fluid](https://tobis.vision/experiments/fluid) — A WebGPU fluid simulation ported from Pavel Dobryakov's classic to three.js TSL. Markdown: https://tobis.vision/experiments/fluid.md
- [Alpha-to-coverage, and the black edges it doesn't fix](https://tobis.vision/notes/alpha-to-coverage-foliage-atlases) — 790,000 grass cards you can walk inside cannot be depth-sorted. Alpha-to-coverage solves the ordering, a contrast curve solves mip alpha collapse, and neither touches the dark outlines. Markdown: https://tobis.vision/notes/alpha-to-coverage-foliage-atlases.md
- [Real-time GI that just brightened the walls](https://tobis.vision/notes/real-time-gi-that-just-brightened-the-walls) — A light propagation volume can solve live and still read as a brightness slider: the missing sun-patch bounce, the dynamic range propagation destroys, and the read-side falloff that gives it back. Markdown: https://tobis.vision/notes/real-time-gi-that-just-brightened-the-walls.md
- [An MCP server that lets an agent write WebGPU shaders](https://tobis.vision/notes/mcp-server-that-writes-webgpu-shaders) — You cannot render WebGPU in a Node process, so the server owns nothing and relays into a live editor tab. The compile feedback loop is what makes an agent able to write shaders at all. Markdown: https://tobis.vision/notes/mcp-server-that-writes-webgpu-shaders.md
- [Making three.js bloom 9x faster with the Call of Duty blur](https://tobis.vision/notes/three-js-bloom-jimenez-dual-filter) — three.js renders a fixed 5-mip chain of separable Gaussians every frame and ignores your radius. The Advanced Warfare downsample/upsample took bloom from 8.2ms to 0.9ms. Markdown: https://tobis.vision/notes/three-js-bloom-jimenez-dual-filter.md

## Machine-readable

- [llms.txt](https://tobis.vision/llms.txt)
- [sitemap.xml](https://tobis.vision/sitemap.xml)
- [robots.txt](https://tobis.vision/robots.txt)
- [openapi.json](https://tobis.vision/openapi.json)
- [developer resources](https://tobis.vision/developers)
