One final-image owner
Tone mapping compresses scene-linear HDR values into a displayable range. Output conversion maps the working color representation to the output color space. Applying either transformation twice can darken midtones, flatten highlights, shift saturation, or make a LUT look much stronger than authored.
A TSL pipeline needs one explicit ownership pattern:
- Let the
RenderPipelineapply its automatic output transform and do not place another explicitrenderOutput()in the graph. - Let the graph own presentation with
renderOutput()and setrenderPipeline.outputColorTransform = false.
When exposure, tone mapping, and a tone-mapped-linear LUT are explicit, the expected order is:
scene-linear HDR
-> exposure
-> toneMapping()
-> tone-mapped-linear LUT
-> renderOutput(..., NoToneMapping, renderer.outputColorSpace)
After changing outputNode or output ownership, invalidate the graph:
renderPipeline.outputColorTransform = false;
renderPipeline.outputNode = finalNode;
renderPipeline.needsUpdate = true;
Keep renderer.toneMappingExposure neutral when dynamic exposure is already applied explicitly. Otherwise exposure also gains two owners even if the tone-map operator itself appears only once.
Source checks and evidence status
The image-pipeline source validator includes a negative control named double-output-transform. It deliberately combines explicit renderOutput() ownership with automatic output conversion, and the validator must reject it. The exposure validator independently rejects outputTransformOwner === "renderOutput" when outputColorTransform is not false.
The published image-pipeline evidence report and exposure and grading report are accepted and match their current source hashes. They demonstrate the output-ownership and diagnostic contracts in those captured graphs. They do not prove that an unrelated application has the same cause; inspect that application's graph, exposure state, LUT domain, and final output before assigning the failure.
Diagnose before changing the graph
Compare the no-post, pre-tone-map, post-tone-map, LUT, and final outputs. A similar appearance can come from:
- color textures with wrong color-space metadata;
- a display-encoded LUT sampled as tone-mapped-linear data;
- a scene-linear LUT used without its required shaper;
- non-neutral renderer exposure in addition to explicit EV adaptation;
- a stale graph after changing
outputNodewithout settingneedsUpdate; - an output image or canvas that is interpreted under the wrong transfer function.
This answer is verified against Three.js 0.185.1. Recheck the ownership APIs when upgrading. Compare RenderPipeline and EffectComposer, verify the native backend, or use the striped-row answer when the artifact is spatial corruption rather than color compression.
Question provenance
This question comes from a verified local failure control that reproduces duplicate output conversion and requires validation to reject it. It is not presented as a customer question. First observed 2026-07-11; last observed and answer reviewed 2026-07-16.
Relevant skills
Relevant demos and evidence
Sources and correction path
- threejs.org/docs/TSL.html
- github.com/linegel/threejs-complete-set-of-skill/blob/main/skills/threejs-image-pipeline/SKILL.md
- github.com/linegel/threejs-complete-set-of-skill/blob/main/skills/threejs-exposure-color-grading/SKILL.md
- github.com/linegel/threejs-complete-set-of-skill/blob/main/threejs-image-pipeline/examples/webgpu-image-pipeline/validateImagePipelineConfig.js
- github.com/linegel/threejs-complete-set-of-skill/blob/main/threejs-exposure-color-grading/examples/webgpu-exposure-color-pipeline/validate-exposure.js
- Report a correction