Handling Gamma Correction for Three.JS pmndrs postprocessing
Effects
I recently received a bug report on a library I built - three-good-godrays
- which implements screen-space raymarched godrays for Three.JS as a pass for the pmndrs postprocessing
library. One of the problems pointed out was that colors seemed washed out/desaturated when my pass was used, even when the pass wasn’t rendering any godrays.
Here’s how things look by default without the effect (and are supposed to look with it on):
But here’s how they looked with the godrays pass enabled:
The bug report is right - there’s definitely something wrong.
The Cause⌗
The reason this is happening is due to differences in color space handling. According to the docs on the pmndrs postprocessing
library, the internal buffers used by postprocessing passes are expected to store data in sRGB format (relevant docs). To handle this, popular effects such as n8ao
perform linear -> sRGB conversion by default: https://github.com/N8python/n8ao?tab=readme-ov-file#usage
To implement this, n8ao
calls LinearTosRGB()
on the pixel data output if gammaCorrect
is enabled (which is the default).
The Fix⌗
To fix three-good-godrays
, I simply added the exact same solution that n8ao
used. I added the same gammaCorrection
flag to the pass params, set it enabled by default, and added the same call to LinearTosRGB()
to the shader.
However, after adding this change, I noticed that my demos were experiencing some severe artifacts:
After digging into this, I determined that the artifacts went away if I disabled the SMAAEffect
and its EffectPass
that I had added to the postprocessing pipeline after the GodraysPass
.
It turns out that output encoding is performed by default by EffectPass
in some circumstances. This was causing my colors to get double-converted to sRGB which produced those artifacts.
To work around that, I … just set gammaCorrection: false
in the godrays pass params. This had the result of making things look the same way they did before for my demos. Another method of achieving this is to set outputEncoding = false
on the SMAAEffect
’s EffectPass
which disables the double-encoding.
One thing to note is that I have also set the frameBufferType
to HalfFloatType
on the framebuffers used by the postprocessing
EffectComposer
. The reason I do this is to avoid color banding that results from the loss of precision by the default 8-bit color format.
new EffectComposer(renderer, { frameBufferType: THREE.HalfFloatType });
Other Notes⌗
The color space/encoding handling in the Three.JS ecosystem is very complex and confusing to me at the moment. Three.JS recently made some internal changes to Color Management which might be interfering/conflicting with pmndrs postprocessing
in some ways.
If you’re still running into color issues in your Three.JS projects, some other things you can try are:
- Toggling
THREE.ColorManagement.enabled = true;
. I set this totrue
for my demos, and I believe this is the “right way” to do things in modern Three.JS. - Changing the
outputColorSpace
of yourWebGLRenderer
. This is set toTHREE.SRGBColorSpace
by default, and I think it’s suggested to not change that.
I’m honestly not really sure about the details of these things myself. For my projects, I generally end up trying various combinations of these settings until things look right :p
In any case, I hope this helps give you some info on possible solutions if you’re running into these problems yourself.