Live concept-proxy screenshot · not canonical evidence

Image Pipeline

Build a minimal, workload-selected WebGPU/TSL final-image pipeline for advanced Three.js scenes. Use for RenderPipeline ownership; conditional pass()/mrt() depth, normal, albedo, emissive, velocity, and history signals; GTAONode, BloomNode, TRAANode, exposure, tone mapping, 3D LUT grading, outputColorTransform, diagnostics, and measured quality/performance tradeoffs.

$threejs-image-pipeline 7 primary implementations 5 flagships 1 secondary surface native evidence pending Latest skill update commit 9077075 ↗ SKILL.md on GitHub ↗ raw (for agents) ↗

Primary implementation surface

These routes are generated from canonical source. Their exact status remains separate from implementation availability.

Related skill preview · Live concept-proxy screenshot · not canonical evidence for Creature HabitatRelated skill preview · Live concept-proxy screenshot · not canonical evidence IntegrationEvidence pending

Creature Habitat

1 fixed scenarios, 5 mechanism routes, and 3 locked tiers.

Open published implementation →
Related skill preview · Live concept-proxy screenshot · not canonical evidence for Final Image FlightRelated skill preview · Live concept-proxy screenshot · not canonical evidence IntegrationEvidence pending

Final Image Flight

1 fixed scenarios, 6 mechanism routes, and 3 locked tiers.

Open published implementation →
Related skill preview · Live concept-proxy screenshot · not canonical evidence for Procedural DistrictRelated skill preview · Live concept-proxy screenshot · not canonical evidence IntegrationEvidence pending

Procedural District

1 fixed scenarios, 6 mechanism routes, and 3 locked tiers.

Open published implementation →
Related skill preview · Live concept-proxy screenshot · not canonical evidence for Relativistic Space ShotRelated skill preview · Live concept-proxy screenshot · not canonical evidence IntegrationEvidence pending

Relativistic Space Shot

1 fixed scenarios, 6 mechanism routes, and 3 locked tiers.

Open published implementation →
Related skill preview · Live concept-proxy screenshot · not canonical evidence for Weathered WorldRelated skill preview · Live concept-proxy screenshot · not canonical evidence IntegrationEvidence pending

Weathered World

1 fixed scenarios, 6 mechanism routes, and 3 locked tiers.

Open published implementation →
Related skill preview · Live concept-proxy screenshot · not canonical evidence for WebGPU Image PipelineRelated skill preview · Live concept-proxy screenshot · not canonical evidence Canonical labEvidence pending

WebGPU Image Pipeline

1 fixed scenarios, 6 mechanism routes, and 3 locked tiers.

Open published implementation →
Related skill preview · Live concept-proxy screenshot · not canonical evidence for WebGPU Temporal HistoryRelated skill preview · Live concept-proxy screenshot · not canonical evidence Mechanism benchEvidence pending

WebGPU Temporal History

1 fixed scenarios, 1 mechanism routes, and 1 locked tiers.

Open published implementation →

The approach, mathematically

The final image is a graph with single ownership of every shared signal. One scene pass publishes MRT outputs — depth, normal, albedo, emissive, velocity — and every effect consumes the same copies:

$$\text{pass}(scene) \Rightarrow \{D, \mathbf N, A, E, \mathbf v\} \;\to\; \text{GTAO} \to \text{bloom} \to \text{exposure} \to \text{tonemap} \to \text{LUT} \to \text{output}$$

Velocity is the derivative of the reprojection chain — current clip position against previous-frame clip position through both model and camera history:

$$\mathbf v = \Pi\big(V_t M_t\, \mathbf p\big) - \Pi\big(V_{t-1} M_{t-1}\, \mathbf p\big)$$

Ordering is semantic, not aesthetic: AO modulates indirect light (pre-tonemap), bloom reads scene-linear HDR (pre-tonemap), grading reads display-referred color (post-tonemap). The validator's job is to enumerate the live pass graph and assert each signal has exactly one producer.

Preview and evidence ledger

Every image identifies what it proves. Page screenshots demonstrate the published presentation only; generated inputs demonstrate asset channels only; canonical acceptance still requires render-target readback and a schema-v2 bundle.

Canonical runtime evidence pending1 published image

The full skill

The complete SKILL.md as loaded by agents — verbatim, rendered.

Image Pipeline

Use this coordinator when several image-space systems share a scene pass, depth, color attachments, history, ordering, or final output. For one isolated effect, load its atomic skill.

Route only what the graph needs:

  • $threejs-ambient-contact-shading for GTAONode and indirect-light use;
  • $threejs-bloom for BloomNode and emissive policy;
  • $threejs-exposure-color-grading for metering, EV adaptation, tone mapping, LUT domains, and output conversion;
  • $threejs-dynamic-surface-effects for feature-local screen history;
  • $threejs-visual-validation for fixed-view, timing, and leak evidence.

Read references/production-image-pipeline.md before implementation.

Numeric Evidence Rule

Tag every number:

  • [Derived]: formula, dimensions, format, or verified API consequence;
  • [Gated]: enabled only after a named capability/correctness gate;
  • [Measured]: captured on a named target and complete graph;
  • [Authored]: a deliberate quality/look/controller starting point.

This applies to attachment counts, formats, byte costs, resolution scales, workgroup sizes, history lengths, timing thresholds, DPR limits, hysteresis, and frame budgets.

Required Architecture

Lead with one WebGPURenderer, one RenderPipeline, and one primary pass(scene, camera) [Authored: baseline architecture]. Add only the MRT outputs whose measured alternative is worse. "One pass plus every possible gbuffer" is not a performance rule, especially on a tile GPU.

Depth is the pass depth texture; it is not a color output in mrt().

Default order:

primary scene pass: HDR output + depth + selected MRT outputs
  -> effect-local lighting histories, such as temporal AO
  -> lighting-aware AO / atmosphere and layers admitted to temporal history
  -> temporal reconstruction of stable scene radiance, when complete
  -> transparent/refractive layers excluded from that history
  -> exposure meter tap from resolved pre-bloom HDR by default
  -> bloom / glare and other scene-linear optical effects
  -> adapted exposure
  -> tone map
  -> grade in its declared domain
  -> one output conversion
  -> display-domain antialiasing / dither / UI

Temporal resolve precedes exposure by default so history remains in stable scene radiance. Bloom follows temporal resolve by default to avoid reprojecting broad depthless glare. Deviations require a named history/exposure contract.

Use built-in nodes first. A custom pass must add a measured capability, not duplicate a private built-in target.

Native WebGPU Gate And r185 Surface

// Numeric/API literals: [Gated: installed three@0.185.1 source]
import {
  HalfFloatType,
  RenderPipeline,
  WebGPURenderer
} from 'three/webgpu';

const renderer = new WebGPURenderer( {
  antialias: false,
  outputBufferType: HalfFloatType,
  trackTimestamp: true
} );

await renderer.init();
if ( renderer.backend.isWebGPUBackend !== true ) {
  throw new Error( 'Native WebGPU is required for this image pipeline.' );
}

const canTimeGpu = renderer.hasFeature( 'timestamp-query' );
const renderPipeline = new RenderPipeline( renderer );

Verified r185 APIs used by this skill:

  • RenderPipeline.render(), outputColorTransform, needsUpdate;
  • pass(), mrt(), PassNode.setResolutionScale(), setMRT(), getTextureNode(), getViewZNode(), getLinearDepthNode(), compileAsync(renderer);
  • ao(depth, normalOrNull, camera) with the public GTAONode.resolutionScale property;
  • bloom(input) with BloomNode.setResolutionScale();
  • traa(beauty, depth, velocity, camera) and TRAANode.setViewOffset();
  • renderOutput() and TSL toneMapping().

PostProcessing is the deprecated historical name for RenderPipeline; do not teach it. Do not teach a WebGPU-unavailable route here. If the user explicitly asks how to apply fallback when WebGPU is unavailable, route to $threejs-compatibility-fallbacks.

Build Order

  • Declare physical canvas pixels, target frame time, target browser/GPU, primary visual contract, and no-post baseline.
  • Write the signal table: producer, consumers, coordinate/color domain, physical format, resolution, first write, last read, history, disable path, and bytes.
  • Create the primary pass(scene, camera) and request depth.
  • Compare each candidate MRT output against reconstruction or a narrow rerender on the target device. Add only winners.
  • Compose lighting, atmosphere, and only the layers with valid temporal signals before temporal color history.
  • Add temporal reconstruction only after velocity, jitter, rejection, and reset contracts pass diagnostics.
  • Composite transparent/refractive layers excluded from history after the temporal resolve and before the photographed meter tap.
  • Tap exposure before bloom by default; apply exposure after temporal history.
  • Assign tone-map, LUT-domain, and output-conversion ownership once.
  • Compute peak live resident bytes and bandwidth separately. Do not claim aliasing for built-in private targets.
  • Add adaptive DPR only after fixed-DPR graph timings exist.

Use examples/webgpu-image-pipeline/ as a public-API baseline and validateImagePipelineConfig.js as a graph gate before browser validation.

Conditional MRT Rule

For candidate attachment a, compare:

costMRT(a) = fragment export + attachment store + later reads        [Measured]
costAlt(a) = reconstruction or narrow rerender + its memory traffic  [Measured]

Keep a only when costMRT(a) < costAlt(a) and peak resident memory stays inside the declared budget. On tile GPUs, an attachment sampled by a later pass must normally leave tile memory; an MRT can save scene traversal yet still lose to bandwidth.

Practical candidates:

  • normal: omit and pass null to ao() when depth reconstruction is cheaper; retain when several consumers need stable geometric normals;
  • emissive: retain only for selective bloom; otherwise bloom from HDR color;
  • velocity: retain only while temporal consumers are enabled;
  • albedo/base color: r185 exports diffuseColor, not albedo; use a named MRT mapping such as albedo: diffuseColor.rgb only when a real composite consumes it;
  • material/object IDs: add only with a concrete rejection or classification consumer.

r185 PassNode creates additional named attachments by cloning the output texture. Do not budget compact RG16F velocity or RGBA8 normals unless the physical texture format is explicitly configured, compiled, and verified; the default HDR pass can otherwise allocate all named attachments at its HDR output format.

Temporal Contract

Three.js r185 VelocityNode writes:

velocityNDC = currentNDC.xy - previousNDC.xy            [Derived: r185 source]

Its built-in TRAANode converts that to texture UV with a Y flip:

// Constants are [Derived: r185 TRAANode source].
const offsetUV = velocityTexel.xy.mul( vec2( 0.5, - 0.5 ) );
const previousUV = currentUV.sub( offsetUV );

Do not use currentUV - velocity * 0.5 for both axes. The sign mismatch ghosts vertical motion.

Before enabling temporal output, declare:

  • velocity producer for rigid, instanced, skinned, and procedurally deformed geometry actually present;
  • jitter owner and whether velocity includes jitter;
  • depth convention and rejection domain;
  • history color domain and exposure used to create it;
  • reset events: resize/DPR, cut, projection change, scene load, discontinuous deformation, format change, and invalid velocity;
  • current/history/rejection/velocity diagnostics.

traa() requires a texture input. Passing a composite node causes r185 to materialize an additional full-resolution RTTNode (color target and fullscreen draw, with default depth unless explicitly disabled). Account, own, and dispose that target. Stock TRAA also requires scene color/depth/velocity/input extents to match the drawing buffer; a scaled scene pass is not a silent TAAU path.

TRAANode requires MSAA disabled [Gated: r185 source]. r185 exposes no public general reset() method; resize is handled internally, while cuts and other discontinuities need a validated wrapper policy or node rebuild/disposal.

Physics presentation and reactive radiance

When the route manifest declares a physics-to-render boundary, it first publishes a view-independent immutable PhysicsPresentationCandidate, then one CameraViewPublication per target/view, then ViewPreparationPublication for visibility/shadow/cache/reset results, and finally seals PhysicsPresentationSnapshot. Bind the scene pass to that exact publication chain and bind the matching LightingTransportSnapshot through a provider-wide PresentedStatePair (entityId: typed-absence) referenced from the candidate by the Snapshot, from the physics domain and interaction contract. Validate the exact central Candidate, camera publication, preparation publication, Snapshot, PhysicsSignalDescriptor, and lighting-channel schemas rather than defining a post-local subset. The target/view Snapshot contains references, not copied pairs, transforms, or reset records. Resolve stable binding pairs through presentedStatePairRefs, previous/current render instants and complete transforms/matrices through cameraPublicationId, and reactive/ reset records through viewPreparationId. A post node may not sample a different provider bracket or silently mix radiance and irradiance. Match the lighting pair's context, provider/signal IDs, descriptor/state/resource generations, PresentationStateHandle, each state's requested presentation instant, mapped source instant and clock-map revision/error, plus the bundle sampleInstant; validate channel actualPhysicsTime, filter/age, maximum staleness, validity, and error.

Canonical lighting-provider channels remain SI-valued. A normalized RGB working buffer is a separately named render-local signal derived through a versioned SI-to-render conversion with reference scale, provenance, and error; it is not a normalized LightingTransportSnapshot channel. A nonphysical image route keeps the router physics fields not used and declares its render-local color basis without instantiating this ABI.

Velocity is projected from the pair's independent previousPresented and currentPresented state handles/global bindings, using the corresponding previous/current RenderSimilarityTransform and unjittered matrices from CameraViewPublication. Each provider's fixed-step states only bracket each presented sample through PresentationSampleProvenance; they are not the previous/current rendered poses. Bindings cover rigid, skinned, instanced, and procedural deformation and explicitly invalidate spawn/despawn/teleport/reparent/LOD changes. A rebase must disappear after origin compensation, not become a full-screen motion vector.

Velocity projects unjittered previous/current view and projection matrices; the temporal owner applies jitter separately. Stock r185 VelocityNode is gated to one presentation history and one velocity-bearing render per object per presented frame, with built-in previous-state coverage sufficient for the rendered rigid/skinned/instanced/deformation path. It is not target/view keyed. Multiple views/targets, multiple velocity passes, or arbitrary snapshot-bound previous deformation require a custom velocity path consuming the central presented pairs and leased resources.

The camera owner returns CameraViewPublication; view-preparation owners return ViewPreparationPublication; no phase mutates an earlier record. Feedback that cannot precede the seal is explicitly one-frame deferred; the current render continues to sample the prior committed version named by its snapshot.

The route serializes typed view-scoped reactivePublications and resetDependencies in ViewPreparationPublication. At minimum, dependency edges cover solver reset/quality migration, uncompensated origin or projection change, shadow-content commit, and discontinuous foam, emissive, or optical state. Use a conservative full reset when no reliable affected-pixel mask exists; otherwise a custom/patched temporal node may consume a versioned reactive mask. Stock r185 TRAANode has no reactive-mask input or public general reset, so its executable choices are evidenced rebuild, bypass/reseed wrapper, or conservative full reset—not a diagnostic mask alone.

Each publication uses the exact central view-scoped record: source/version/ epoch, kind (shadow-content, foam, emissive, optical, topology, deformation, disocclusion, or event), full-frame or leased mask affected-region form, validity/error, and planned history action. A mask ID without descriptor, alignment, error, resource generation, and retirement lease is invalid. Do not reset exposure for every local radiance edit. Reset or analytically convert exposure state only when the radiance basis/calibration, working primaries, quantity convention, or authored exposure key changes.

Execute the dependency DAG before the first consumer of the new epoch:

presentation candidate
  -> CameraViewPublication with transforms/matrices/jitter/depth
  -> ViewPreparationPublication with visibility/shadows/reactive/reset records
  -> seal snapshot references
  -> depth + velocity + scene-linear radiance
  -> AO/surface/volumetric/color history rejection or reseed
  -> resolved meter source
  -> exposure adaptation
  -> bloom, tone map, grade, output

Every planned edge names its writer, consumers, and action. The immutable snapshot does not claim completion; append actual rebuild/reseed/submission results to a separate FrameExecutionRecord. GPU descriptors pin resource generation, layout, entity map, slot/range, and a central PresentationResourceLease until all consumers submit and its reuseProhibitedUntil completion join is satisfied; retirement is recorded in FrameExecutionRecord.leaseDispositionById. Logical state version, submission epoch, GPU queue availability, and host visibility are distinct; computeAsync() is not a fence.

A required camera/shadow/cache or sealing failure appends a FrameExecutionRecord with overallStatus: aborted (or partial-failure when other targets survive), omits that target's snapshot from snapshotIds, stores typed absence in its targetExecutions.snapshotId, cancels or defers actions, retires only failed-target-exclusive preparation leases, and retains Candidate/shared leases until all surviving snapshot consumers join through leaseDispositionById. Device loss appends overallStatus: device-lost and affected target statuses device-lost, advances deviceLossGeneration, cancels dependent actions, and invalidates resources and leases from the lost generation without inventing a completion token. The immutable Candidate/Snapshot remain audit records; their lost-generation resource references are no longer bindable. Rebuild histories and timing proof under the new backend/resource generation.

Even with compensated custom velocity, stock r185 TRAANode cannot preserve history across a render-origin translation or tangent-basis rebase because its previous-depth reconstruction has no previous-render-to-current-render bridge. Rebuild/reseed it on every such rebase. Only a custom/patched temporal node that consumes both complete global-to-render transforms may preserve history after an invariance proof.

Output Ownership

  • source color textures tagged with an sRGB transfer use SRGBColorSpace;
  • linear HDR sources and working render targets remain in the registered working space;
  • normals, masks, velocity, depth, LUT transform data, and histories that are not colors use NoColorSpace/data semantics;
  • choose the HDR working format from required range, precision, filtering, blending, target support, and measured bandwidth. HalfFloatType is a common candidate, not a universal requirement [Gated];
  • one tone-map owner, one working-to-output conversion owner;
  • a scene-linear LUT needs a shaper for unbounded HDR;
  • a tone-mapped-linear LUT sits after toneMapping() and before renderOutput(..., NoToneMapping, outputColorSpace);
  • a display-encoded LUT sits after renderOutput() and receives no second conversion;
  • explicit renderOutput() requires RenderPipeline.outputColorTransform = false.

Lifetime And Adaptive Resolution

r185 has no public transient render-graph alias allocator. Built-in BloomNode, GTAONode, and TRAANode own persistent private render targets. Count all of those allocations. Custom target reuse is legal only for non-overlapping lifetimes with identical dimensions, format, sample count, and usage; history and diagnostic pins never alias.

Adaptive DPR uses sustained GPU pressure, asymmetric hysteresis, and a cooldown. It must distinguish scalable pixel work from fixed work, quantize changes to an authored step, and reset/reseed every affected history. The complete controller and formulas live in the reference.

Composable Budget Contract

Never add a coordinator's full-post cost to the absolute costs of effects it already contains. Use:

fullGraph = measured end-to-end GPU frame                            [Measured]
marginal(effect | graph) = graphWithEffect - identicalGraphWithout  [Measured]

For planning only:

estimatedGraph = measuredBase + sum(measuredMarginals) + interactionReserve

interactionReserve is [Authored] until the assembled full graph is [Measured]; then the full-graph measurement wins. Every result states canvas physical pixels, DPR, enabled graph, warmup, statistic, browser, and GPU.

r185 render/compute timestamp pools sum instrumented pass durations. They do not automatically include copies, barriers, submission gaps, or presentation; do not label their sum end-to-end fullGraph without an independent scope gate.

For parent physical extent W * H, scale s, texel bytes b, sample count m, array layers l, and retained allocation slots k, the uncompressed payload lower bound is:

Wp = floor(W * s)                                         [Derived: r185 PassNode]
Hp = floor(H * s)                                         [Derived: r185 PassNode]
bytes >= Wp * Hp * b * m * l * k                          [Derived]

Use the dimensions actually reported by the target; backend alignment, compression, resolves, allocator granularity, and private targets are outside that payload equation. Count resident bytes and peak live bytes separately; sum bandwidth only for actual reads/writes.

Rules

  • the primary scene pass is the baseline; extra scene renders need a measured reason;
  • depth is shared from the pass, not duplicated as an MRT color attachment;
  • MRT is conditional, not a completeness checklist;
  • apply AO only to the indirect-light term it models;
  • use depth/normal-aware reconstruction for reduced effects;
  • keep temporal history in a stable pre-exposure domain by default;
  • exclude UI/debug from exposure and HDR bloom unless intentionally authored;
  • call scenePass.compileAsync(renderer) only after MRT and texture-node requests are complete;
  • set renderPipeline.needsUpdate = true after output-graph changes;
  • expose disable paths, effect-only views, physical formats, peak bytes, and timings in production diagnostics;
  • dispose removed node effects and their targets.

Routing Boundary

Use this skill for shared buffers, ordering, history, lifetimes, adaptive DPR, budgets, or final output. Use an atomic skill for one isolated effect and return here only when it joins the shared graph.

Secondary provider surfaces

Preserved concept proxies and generated-asset previews. They are excluded from primary completion counts and link to the canonical lab through the schema-v2 registry.