Hi,
I’m implementing a custom super cheap precomputed sky, in a ShaderMaterial.
I want to use a linear color managed setup, so that I can render HDR values to the renderer, including a sun disk, which should play nicely with bloom.
However, I’m not sure what the current best practice is.
To get tonemapping to work, I had to append to my fragment shader:
#include <tonemapping_fragment>
#include <colorspace_fragment>
to get it to work, which seams incorrect, because I expect tonemapping to happen in post, not in fragment.
Any tips?
Some work-in-progress images:
In Three.js, tonemapping is typically handled in the final pass rather than baked directly into each material’s fragment shader. By default, built-in materials (MeshStandardMaterial, etc.) use internal shader chunks that call tonemapping_fragment
and colorspace_fragment
at the end of the shader. But if you’re writing a custom ShaderMaterial
, you have two main options:
- Let Three.js do the tonemapping in the final pass
- Write your shader so that it outputs linear (HDR) color values.
- In your Three.js setup, enable the renderer’s tone mapping, for example:
renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure = 1.0; // or whatever looks good
renderer.outputEncoding = THREE.sRGBEncoding;
- Mark your material as
toneMapped = true
so Three.js knows to apply tonemapping to it:
const mySkyMaterial = new THREE.ShaderMaterial(...);
mySkyMaterial.toneMapped = true;
- Now, your custom sky shader can remain “pure,” simply outputting linear color. Three.js will run a final pass that does tonemapping (and gamma correction, if set) on all toneMapped materials.
- Embed tonemapping in your custom shader
- This is what you’re doing by including
#include <tonemapping_fragment>
and #include <colorspace_fragment>
at the end of your fragment code.
- It works, but it effectively replicates the final pass tonemapping inside your material. If you’re not also doing a separate postprocessing pass, it can be fine—but it feels “wrong” if you expect to handle tonemapping later in a post chain.
Which approach is “best”?
- Most developers prefer the first approach: output linear HDR color from your sky, let Three.js handle the global tonemapping pass. This is how standard materials do it internally.
- If you remove the tonemapping chunks from your shader and just produce linear color, then enable
toneMapped = true
, you’ll get the same end result. It also makes it easier if you add a postprocessing pipeline (e.g. EffectComposer
) later, because you’re not double-tonemapping.
- The second approach is only recommended if you explicitly want to handle tonemapping per-material for some reason.
HDR and Bloom
If you want a bright sun disk that triggers bloom, be sure your sky shader can output values above 1.0
(in linear space). Then, as long as your bloom pass or your tonemapping pass is set up correctly, you’ll see that highlight bloom properly.
So in summary:
- Write your sky shader in linear space, no embedded tonemapping.
- Set
toneMapped = true
on the material, and let the renderer’s tonemapping do the rest.