Documentation
    Preparing search index...

    Interface OutputTransformEffect

    §60a's output transform: the linear-light source encoded as sRGB on the way to a presentable surface (R-15, 2026-08-08).

    graph.addPass("world", { root: scene, views, target: sceneColor });
    graph.addPass(
    "present",
    { kind: "effect", source: sceneColor.colorTexture, effect: OUTPUT_TRANSFORM_EFFECT },
    { inputs: ["world"] },
    );

    Because §60a says so, in one sentence: "the output transform — tone mapping (§68) followed by sRGB encoding — is the final render-graph pass (§63)". The alternative — an encode appended to the unlit, lit, standard, sprite and particle fragment stages — would encode five times into one framebuffer, make every blend between two draws happen in the wrong space, and multiply the backend's shader variants by two. As a pass it runs exactly once, after everything has been composited in linear light, which is the property that makes the transform correct rather than merely present.

    §60a describes the transform as the final pass, i.e. always present. Here it is a pass an application adds, and a scene that does not add it renders exactly the frame it rendered before this existed — byte-identical GL, byte-identical pixels. That is deliberate (R-15, 2026-08-08): every scene, example and pixel golden in this repository was authored against an untagged pipeline whose output happened to be its working space, and an on-by-default encode would relight all of them at once. Making it default is an owner decision, and the honest sequence is: ship the transform, let a scene opt in, move the goldens deliberately rather than as a side effect.

    §60a's transform is tone mapping then encoding, and this effect performs the encoding only. The reason is unchanged from R-6 and is a resource, not a preference: an operator compresses an HDR range onto 0…1, every surface in this tier is rgba8 (R-4's one-member RenderTargetFormat), and there is no range to compress. When float targets land, tone mapping arrives as a field on this effect — the two halves belong to one pass — rather than as a sixth member of the union.

    The source of this pass is a linear-light rgba8 surface, so the frame is quantized to 256 linear steps before it is encoded. sRGB spends more of its code space on darks than linear does, so re-encoding an 8-bit linear buffer can band in shadow gradients in a way a directly-encoded framebuffer would not. The fix is a wider intermediate (rgba16f), which is the same staged RenderTargetFormat widening tone mapping waits on — not a change to this pass. Named here because it is the one visible difference between this tier and a complete §60a pipeline.

    Per colour channel, @fourjs/math's linearToSrgb: the IEC 61966-2-1 piecewise curve, odd-extended below zero. Alpha is not encoded — it is a coverage fraction, not a light quantity — which is the same rule srgbToLinearRGBA follows on the way in. Nothing is clamped by the effect; the rgba8 destination saturates on write, as it does for a grade.

    interface OutputTransformEffect {
        kind: "output-transform";
    }
    Index
    kind: "output-transform"

    Required, and the only value — the discriminant.