Live concept-proxy screenshot · not canonical evidence

Bloom

Design workload-selected bloom in Three.js r185 WebGPU/TSL, choosing physical HDR scene bloom versus selective MRT contribution, with exact BloomNode pyramid costs, transparent blending rules, PSF limits, and mobile/tile-GPU gates.

$threejs-bloom 1 primary implementation 2 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.

The approach, mathematically

Bloom is a camera response to bright HDR signal: energy above a threshold scatters into a wide kernel. The signal chain is the contract — bloom samples scene-linear HDR before tone mapping:

$$L_{bloom} = K_{blur} * \max(L_{scene} - T, 0), \qquad L_{final} = \operatorname{tonemap}(L_{scene} + s\,L_{bloom})$$

The blur is a mip pyramid: progressive downsample + upsample accumulation approximates a large Gaussian at a fraction of the cost, with radius scaling per mip level. Selective bloom routes emissive contribution through an MRT channel so only authored emitters glow:

$$L_{bloom} = K * (E_{mrt}), \qquad E = \text{emissiveNode output, scene-relative units}$$

The skill's core rule: an object's brightness hierarchy is authored in scene units (sun ≫ plasma ≫ LED); bloom reveals that hierarchy, it must never be the object.

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.

Bloom

Bloom approximates the broad tail of an imaging point-spread function (PSF). The physically motivated input is scene-linear HDR radiance. A bright-pass and selective emissive membership are real-time/art-direction approximations, not optical laws. The base scene must remain readable with bloom disabled.

Use $threejs-choose-skills for renderer/budget preflight, $threejs-exposure-color-grading for exposure and output ownership, and $threejs-image-pipeline when the scene pass/MRT is shared.

Numeric provenance

  • [Derived] follows from installed r185 source or a displayed equation.
  • [Gated] is a branch threshold validated on the target scene/device.
  • [Measured] is target-device evidence.
  • [Authored] is a starting value or planning ceiling.

Release numbers and list ordering are identifiers, not tuning claims.

Choose the bloom signal before building MRT

Visual contract Input Decision threshold
Optical glare: bright direct lighting, reflections, transmission, sky/sun, and emission should respond to one exposure-relative luminance rule bloom(sceneColor) Default. It needs no bloom-membership MRT and includes final transparent/reflected radiance.
Art-directed selective glow: named surfaces must bloom differently from equally bright radiance bloom(emissiveContribution) from the existing scene-pass MRT Use only when no single bright-pass threshold can meet the contributor false-positive/false-negative contract [Gated] and the measured MRT delta fits.
Selective boost plus optical highlights bloom(sceneColor + authoredBoost) Use only when both mechanisms are required and the extra attachment is charged once [Gated].
Base form disappears without bloom neither Repair geometry/material/lighting first. Bloom is deferred.

Selective MRT is not automatically higher quality. It omits bright reflected and transmitted radiance unless those signals are authored into the contribution target. Full-scene bloom is usually both more physical and cheaper on bandwidth-limited devices.

Radiometric And Reactive Contract

When the route declares a physics-to-render boundary, consume its immutable PhysicsPresentationCandidate -> CameraViewPublication -> ViewPreparationPublication -> PhysicsPresentationSnapshot chain and bind LightingTransportSnapshot through a provider-wide PresentedStatePair (entityId: typed-absence) in the Candidate whose binding ID is referenced by the Snapshot, from the physics domain and interaction contract. Validate the exact central presentation and lighting channel descriptors; do not redeclare a bloom-local lighting record. Bloom input is scene-linear radiance in its declared render-local basis. Match the pair's context/provider/ signal IDs, descriptor/state/resource generations, PresentationStateHandle, each state's requested presentation instant, mapped source instant, clock-map revision/error, and the lighting bundle sampleInstant; validate channel actualPhysicsTime, filter/age, maximum staleness, validity, and error. Irradiance transport values must pass through the lighting/BRDF contract before bloom. Selective emission cannot use an unrelated arbitrary intensity scale.

Basis, quantity, SI unit, bundle sampleInstant, channel actualPhysicsTime, state/resource version, validity, and error are checked per canonical channel. Canonical lighting-provider channels remain SI-valued. A normalized RGB bloom input is a separately named render-local signal produced by a versioned SI-to-render conversion with reference scale, provenance, and error; it is not a normalized canonical lighting channel. One route-wide label cannot make incidentRadiance, surfaceIrradiance, directSolarIrradiance, skyIrradiance, transmittance, or material emission dimensionally compatible; applied attenuation uses attenuationFactorIds. A nonphysical route leaves the router physics fields not used and declares only its render-local color contract.

Declare threshold domain explicitly:

  • scene-referred threshold: fixed in the bloom input's radiance units;
  • exposed-linear threshold: convert each frame by the adapted exposure;
  • display-referred threshold: invert the declared output/tone-map path or reject the policy when no stable inverse exists.

Do not tune one numeric threshold across incompatible lighting bases. A change to basis/calibration, working primaries, quantity convention, or exposure-key policy emits the relevant ViewPreparationPublication.reactivePublications entry and recomputes/reseeds threshold state before bloom. Shadow commits and discontinuous foam, emissive, or optical changes produce a versioned radiance-reactive mask for any upstream temporal history. BloomNode itself has no history to reset, but it must consume the source after required rejection/reseed and publish the same epoch in diagnostics. ViewPreparationPublication.resetDependencies is an immutable plan; append the performed threshold conversion, upstream history action, graph rebuild, and submission to FrameExecutionRecord. Device loss appends a FrameExecutionRecord with overallStatus: device-lost, affected target execution statuses device-lost, cancelled dependent actions, and lost-generation entries in leaseDispositionById; it invalidates bloom resources and timing evidence without mutating the sealed snapshot. Rebuild under the new backend/resource generation.

r185 source facts

Verified against installed three@0.185.1 [Measured]:

Fact Consequence
bloom(node, strength, radius, threshold) and BloomNode.setResolutionScale() exist. Use the built-in baseline; do not invent stale pass wrappers.
The default internal linear scale is 0.5 [Derived]. It is a source default, not a device-quality decision.
High-pass weight is smoothstep(threshold, threshold + smoothWidth, luminance(input.rgb)) [Derived]. Thresholding occurs in the input's scene-linear domain. smoothWidth is a soft knee, not blur radius.
The node uses 5 mip levels with separable kernel radii [6, 10, 14, 18, 22] [Derived]. It is a multi-Gaussian approximation, not an arbitrary physical PSF.
radius mixes fixed cross-mip weights; it does not change kernel support [Derived]. Call it a mip-spread control, not a physical radius or sigma.
Internal bright/blur targets are RGBA16F [Derived]. BloomNode itself cannot become an R11/RG compact path through a quality flag.
Blur stages write alpha 1 and the composite carries nonzero alpha [Derived]. Add bloom RGB while preserving scene alpha when canvas/compositor alpha matters.
Only MRT output named output uses material blending by default; other outputs default to no blending [Derived]. Transparent emissive contributions overwrite unless the MRT output receives an explicit blend mode.
r185 MRTNode.merge() assigns merged modes to blendings, not the operative blendModes map [Derived]. A material-level mrtNode can drop the scene emissive blend mode; do not use that combination in the canonical transparent path.
PassNode.compileAsync() compiles the scene pass, not BloomNode's fullscreen materials [Derived]. Warm and time the complete RenderPipeline; scene compilation alone is insufficient.

Initialize and hard-gate WebGPU:

await renderer.init();

if ( renderer.backend.isWebGPUBackend !== true ) {
  throw new Error( 'threejs-bloom requires WebGPU.' );
}

There is no non-WebGPU implementation in this skill.

Canonical graphs

Optical/full-scene path:

one HDR scene pass -> BloomNode(scene color) -> HDR add -> one output transform

Selective path:

one HDR scene pass with output + emissive MRT
  -> BloomNode(emissive contribution)
  -> add to scene color
  -> one output transform

Both paths use one scene traversal. The selective path adds an HDR attachment; it does not add a selection scene render.

Treat the HDR add as vec4(scene.rgb + bloom.rgb, scene.a), not an unchecked vec4 sum. An opaque swapchain can hide alpha corruption that later appears in DOM/video compositing.

Read references/hdr-bloom-system.md for verified code, transparent MRT blending, exact pass/fetch/memory equations, adaptation rules, and validation.

Transparent contribution contract

For selective bloom, inspect the emissive texture before tuning bloom. r185 MRT outputs other than output do not inherit material blending by default. Configure the emissive MRT output with BlendMode(MaterialBlending). Author transparent contribution through the regular emissiveNode, already weighted for the material's premultiplied/straight-alpha policy. Avoid material-level mrtNode overrides in r185 because the stock MRT merge loses the configured non-output blend map.

Use additive contribution for additive optical energy; use alpha compositing only when the visible surface uses the same ordered model. Charge transparent cost by measured covered fragments/overdraw, not object count. A screen-filling layer can cost more than thousands of tiny contributors.

If visible emission and bloom contribution must diverge per transparent material, use a separately measured contribution pass or a source-verified custom MRT fix. This forfeits the stock one-scene-pass guarantee and must be budgeted explicitly.

Quality gates

Tier Authored start Gate
Full scale 0.5, strength 0.35-0.75, mip-spread 0.25-0.45, soft knee 0.05-0.12 [Authored] HDR signal, PSF footprint, and full marginal time pass at maximum target DPR.
Balanced scale 0.33-0.5, narrower contribution/threshold range [Authored] Fixed-view halo error is acceptable and measured bloom delta fits.
Constrained WebGPU scale 0.25-0.33, reduced transparent screen coverage, optional bloom disable [Authored] Tile bandwidth, thermal run, and minimum-mip dimension pass; otherwise disable bloom.

Threshold has no portable numeric default. Derive it from the stabilized exposure and a false-color pre-tone luminance view. For selective input, threshold still controls energy within membership; it is not the membership mechanism.

The deepest r185 level is valid only when floor(scale * min(width, height)) >= 16 [Derived/Gated]. Below that, the fixed 5-level chain reaches a zero-sized target. Reduce mip count in a custom node or disable bloom for that surface size; do not rely on undefined tiny- target behavior.

Composable marginal budget

deltaBloomFull = time(scene + full-scene bloom) - time(scene)       [Measured]
deltaMRT = time(scene with contribution MRT) - time(scene)          [Measured]
deltaBloomSelective = time(scene + contribution MRT + bloom) - time(scene)
                                                                    [Measured]
route valid iff charged delta <= declaredMarginalBloomBudget        [Gated]

If another effect already owns the same emissive attachment, charge its measured attachment delta once in the unique-work ledger. Use the complete paired deltaBloomSelective for acceptance; do not add independently sampled MRT and BloomNode percentiles or assume their interaction is zero.

Let A = scale^2 * width * height bloom pixels [Derived]. The fixed r185 node performs 12 fullscreen draws [Derived]: one high-pass, two blur directions across 5 levels, and one composite. Its allocated internal target storage approaches 29.3125 * A bytes before integer-size floors [Derived] and its shader work is about 42.3047 * A texture samples plus 4.6641 * A pixel writes [Derived].

At 1920x1080, scale 0.5, internal targets occupy 14.49 MiB; a selective RGBA16F emissive attachment adds 15.82 MiB, for 30.31 MiB incremental live image storage before depth, output, MSAA, alignment, and tile scratch [Derived].

Do not seed the search with adapter-class millisecond tables. They obscure DPR, HDR sparsity, MRT stores, browser state, and thermal behavior. Declare one product marginal budget [Gated], measure the complete charged delta with matched warmed A/B traces [Measured], and reject every tier that misses it.

For a pixel-bound miss, estimate nextScale = currentScale * sqrt(budgetMs / measuredMs) [Derived], clamp to the tier and minimum-mip gates, then remeasure. The square-root estimate is invalid when MRT stores, fixed pass overhead, or transparent overdraw dominate.

Tile/mobile rules

  • Prefer full-scene input when it meets the visual contract; it removes the full-resolution contribution attachment.
  • Measure MRT with the target MSAA count. An RGBA16F contribution attachment is 8 * width * height resolved bytes [Derived] and may add multisample tile storage before resolve.
  • Reduce bloom scale before changing the scene resolution. Bloom cost is approximately quadratic in linear scale while the pixel-bound assumption holds [Derived].
  • Use screen-coverage/overdraw heatmaps for transparent contributors. Sorting, destination reads, and two attachment writes can dominate a tile renderer.
  • Run a sustained thermal trace; a short desktop capture does not validate mobile equilibrium.

Visual wrongness signatures

Signature Cause / decision
Gray, clipped highlight cores HDR was clamped or output-converted before high-pass.
Uniform milky veil Threshold/knee admits broad midtones, or strength compensates for a bad source signal.
Bright reflections stay razor-sharp while emitters glow Selective MRT omitted optical radiance; use scene-color or hybrid input.
Transparent contributors disappear or pop by draw order Emissive MRT blending/alpha is wrong.
Halo changes with DPR or resize Fixed mip/kernel footprint changed in output-pixel terms; validate endpoints or use a custom PSF.
Blocky staircase around small highlights Bloom scale is too low for the feature; raise scale or reject bloom for that tier.
Tiny hot pixels create enormous halos Unbounded HDR fireflies; repair sampling/exposure or robustly cap the source before bloom.
Scene form vanishes when bloom is disabled Bloom is carrying silhouette/lighting; repair the base scene.
Bloom-off timing is unchanged Bloom remains reachable in the active graph; replace output node and set needsUpdate.
Transparent canvas gains an opaque rectangle or wrong edge alpha Bloom alpha was added to scene alpha; preserve base alpha explicitly.

Ownership boundary

This skill owns source-signal selection, BloomNode controls, selective MRT contribution, transparent blending, PSF diagnostics, and bloom marginal cost. Exposure/color grading owns metering, adaptation, tone mapping, and display conversion. Image-pipeline owns shared scene passes and global ordering.

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.