threejs-image-pipeline · git:20260716.2ce13bc · 2026-07-16 · sha256 6447ea4072d19f3b

threejs-image-pipeline git:20260716.2ce13bcA

Immutable. This exact content is served forever at /api/v1/blob/6447ea4072d19f3b.

---
name: threejs-image-pipeline
description: Coordinate Three.js WebGPU/TSL final-image graphs. Use when effects share scene-pass signals or output ownership, when choosing MRT versus reconstruction or narrow passes, when admitting temporal history, or when whole-graph budgets and lifetimes decide the design.
---

# Image Pipeline

Build one causal image graph: one HDR scene pass with depth, selected shared
signals, and one final output conversion. Add a scene traversal or attachment
only when its measured alternative is worse.

Use the atomic owner for each admitted effect:

- `$threejs-ambient-contact-shading` for GTAO and indirect-light composition;
- `$threejs-bloom` for glare source selection and `BloomNode` controls;
- `$threejs-exposure-color-grading` for metering, adaptation, tone mapping, and
  LUT domains;
- `$threejs-dynamic-surface-effects` for feature-local screen history;
- `$threejs-visual-validation` for capture, timing, and lifecycle evidence.

## 1. Fix the baseline

Declare physical canvas pixels, target browser/GPU, frame budget, primary
visual contract, and a readable no-post view. Initialize one
`WebGPURenderer`, confirm `renderer.backend.isWebGPUBackend`, create one
`RenderPipeline`, and make one `pass(scene, camera)` own scene-linear HDR plus
its depth texture. Set `trackTimestamp` before `renderer.init()` when GPU timing
is requested.

This step is complete when the baseline renders without optional post, the HDR
and depth producers are named, and exactly one component owns presentation.

## 2. Inventory signals

For every candidate signal—HDR color, depth, normal, emissive, velocity,
diffuse/base color, IDs, histories, exposure, and UI—record:

```text
writer | readers | mathematical/color domain | physical format and extent
first write -> last read | history/reset owner | disable path
```

Treat depth as the pass depth texture rather than an MRT color output. Request
only signals with a real reader.

This step is complete when every graph edge has one writer, all consumers agree
on domain and extent, and every optional signal has a working disable path.

## 3. Admit attachments

Compare each candidate attachment with reconstruction or a narrow rerender on
the target graph:

```text
costMRT(a) = export + store/resolve + all later reads
costAlt(a) = reconstruction or narrow rerender + its traffic
```

Keep the attachment when paired evidence shows `costMRT(a) < costAlt(a)` and
the peak resident budget still passes. Inspect compiled physical formats:
r185 named `PassNode` attachments clone the pass output by default, so compact
normal or velocity storage exists only after explicit configuration and
verification.

This step is complete when every retained attachment has a named reader,
verified physical format, measured winning alternative, and accounted peak
bytes.

When implementing `pass()`, MRT, compact formats, or depth branches, read
[Graph construction and signal formats](references/production-image-pipeline.md#graph-construction-and-signal-formats).

## 4. Admit temporal history

Enable temporal output only after all rendered motion has valid previous and
current presentation state:

- rigid transforms, instances, bones, and procedural deformation;
- stable particle/slot identity where particles enter history;
- unjittered previous/current camera transforms and one jitter owner;
- depth, velocity, neighborhood, and out-of-bounds rejection;
- resets for resize/DPR, cut, projection or origin change, scene load,
  spawn/despawn, teleport, reparent, LOD/topology change, and discontinuous
  deformation;
- current, history, rejected-history, velocity, jitter, and reset diagnostics.

r185 velocity is `currentNDC - previousNDC`; TRAA converts it to UV with a
negative Y scale. Stock `TRAANode` requires matching color/depth/velocity/input
extents and MSAA off. A composite temporal input materializes another texture
and fullscreen draw. Stock TRAA has no public general reset or reactive-mask
input, so cuts and discontinuities require an evidenced rebuild or
bypass/reseed policy.

This step is complete when horizontal and vertical motion reproject correctly,
every rendered representation has a velocity policy, each discontinuity fires
an executable reset, and every temporal allocation has an owner.

When temporal reconstruction is present, read
[Temporal admission and resets](references/production-image-pipeline.md#temporal-admission-and-resets)
before creating velocity or history nodes.

## 5. Compose once

Use this default order:

```text
HDR scene pass + depth + admitted MRT
  -> effect-local lighting histories
  -> lighting/AO/atmosphere with valid temporal inputs
  -> temporal scene-radiance resolve, when admitted
  -> excluded transparent or refractive layers
  -> meter tap from resolved pre-bloom HDR
  -> bloom and other scene-linear optical effects
  -> adapted exposure
  -> tone map
  -> LUT in its declared domain
  -> one output conversion
  -> display-domain AA, dither, diagnostics, and UI
```

Keep history in stable pre-exposure scene radiance by default. Add bloom RGB
while preserving scene alpha. If `renderOutput()` owns presentation, set
`renderPipeline.outputColorTransform = false`; after any output-node change,
set `renderPipeline.needsUpdate = true`.

This step is complete when meter, exposure, tone map, LUT, alpha, and output
conversion each have one owner and every transparent/refractive layer has an
explicit position.

When choosing a LUT/output ending or handling transparent alpha, read
[Color, alpha, and legal endings](references/production-image-pipeline.md#color-alpha-and-legal-endings).

## 6. Own toggles, size, and lifetime

Count persistent private targets owned by built-in `BloomNode`, `GTAONode`,
`TRAANode`, and `PassNode`. Rebuild the pass to reclaim an attachment previously
requested with `getTextureNode()`; a logical MRT toggle does not reclaim it.
On resize or DPR change, update every explicit extent and reseed all affected
histories. Dispose removed nodes, passes, targets, materials, and storage after
their final GPU use.

Add adaptive DPR only after the fixed-DPR graph has sustained timings. Use
asymmetric dwell and cooldown, distinguish fixed from pixel-scaled work, and
remeasure every quality tier after a size change.

This step is complete when repeated enable/disable, resize, tier-switch, and
dispose cycles stabilize resource counts and every graph mutation marks the
pipeline dirty.

When estimating traffic, private target residency, marginal cost, or adaptive
DPR, read
[Memory, timing, and adaptive resolution](references/production-image-pipeline.md#memory-timing-and-adaptive-resolution).

## 7. Prove the graph

Capture the no-post baseline, each admitted signal, temporal rejection/reset
views, meter source, bloom source, pre-tone and final output, and physical
target inventory. Measure the complete warmed graph and paired marginal
variants at identical scene state. Exercise negative controls: disable each
effect, force each reset class, resize, and destroy/recreate the graph.

When a capture, timing scope, reset, output-isolation, or lifecycle control fails, read [Diagnostics and failure signatures](references/production-image-pipeline.md#diagnostics-and-failure-signatures).

The pipeline is complete when all shipping tiers preserve the visual contract,
the full graph and resident targets meet declared budgets, output isolation
shows one tone map and one conversion, temporal diagnostics pass in both axes,
and lifecycle counts plateau after disposal.