Matching Blender with ThreeJS colors

In our application we can render the same scene in realtime using ThreeJS and offline using Cycles (i.e. the renderer of Blender).

I set up a simple scene, where a color target is illuminated from a distant light source.
I have lately migrating our whole codebase from ThreeJS v137 to the most recent v160.

In this process, I have noticed, that the color intensities seem to have changed. Before, we had basically a 1:1 alignment of the colors in both renderes. Now, the colors rendered in ThreeJS v160 seem to come out too bright.

I setup a scene with a color target being illuminated by a distant point light source. You can see the difference in the versions in the following image.

Unfortunately it is not easy to go in one step version increments to identify which version has introduced this, as so much has changed between v137 and I believe v138 (due to the new node system) that we decided to migrate directly to v160 (I can say as of now that in v157 the situation is identical).

I was hoping, if somebody with deeper insights in the changes related shading / colors of the ThreeJS versions could point me into a direction what settings / modifications could be causing the higher brightness in the newer versions.

Note, that we also already used renderer.physicallyCorrectLights = true in the old version of ThreeJS.
In both versions the renderer.outputColorSpace = THREE.LinearSRGBColorSpace and we setup the correct color space information for all used textures.

Meanwhile, I was indeed able to track it down to a specific version:

154 colors match
155 three js colors too bright

None of the code was changed in our app between the version switches…

renderer.useLegacyLights = false for both versions

Mainly r155 contained changes to lighting, enabling .useLegacyLights = false by default. If you were already disabling it in r154 then I’m not sure why it wouldn’t match before/after though.

Here is the color management guide to changes in r152 as well, for good measure, but if you’ve narrowed it to r155 then that’s probably not the cause.

A few additional questions / comments —

  1. Are you using any tone mapping?
  2. How are you matching point lighting across three.js / Blender? I’ve never managed to do this without manually adjusting either intensity or exposure, although I know it should be possible.
  3. Are you using any tone mapping in three.js, or a view transform in Blender? (“Filmic” is the default in Blender 3.x, “AgX” in Blender 4.x)
  4. Blender’s configuration is comparable to renderer.outputColorSpace = THREE.SRGBColorSpace, not LinearSRGBColorSpace … are you sure this is right? It sounds very hard to match colors and lighting without that.

Thanks for the feedback!

You are right, it indeed seems to be related to tone mapping and the changes that were performed in this PR: WebGLRenderer: Use inline tone mapping only when rendering to screen. by Mugen87 · Pull Request #26371 · mrdoob/three.js · GitHub

In Three we are rendering to a render target

renderer.outputColorSpace = THREE.LinearSRGBColorSpace
renderer.toneMapping = THREE.LinearToneMapping
renderer.toneMappingExposure = 0.8

const renderTarget = new THREE.WebGLRenderTarget(size.width * pixelRatio, size.height * pixelRatio, {
            minFilter: THREE.LinearFilter,
            magFilter: THREE.LinearFilter,
            format: THREE.RGBAFormat,
            type: THREE.HalfFloatType,
        }) = "EffectComposer.rt1"
const composer = new EffectComposer(renderer, renderTarget)

In Cycles we use the same exposure scaling.

This switch to 155 seem to have deactivated the toneMapping entirely, that’s why the scene gets too bright.
However, when adding an outline pass to the composer (as indicated in the PR), the scaling becomes active again but the scene suddenly becomes too dark.
I can tweak the scaling to 0.91 to make it match the colors. But this seems somehow strange, as before the scaling was consistent with the scaling done in Cycles.

Regarding your questions. We render to linear images in Cycles and in ThreeJS and before version 155 the Cycles used 0.8 exposure in the camera, ThreeJS 0.8 linear tone mapping (so it was consistent). Cycles and Three both use a quadratic decay of light intensity, so we did not had to perform any sort of matching of light sources. Now, it somehow seems to have gotten inconsistent.

The decay is the same (physically-based) but the units of light strength are different. Though perhaps rendering to linear images and skipping Blender’s view transform helps here.

However, when adding an outline pass to the composer (as indicated in the PR), the scaling becomes active again but the scene suddenly becomes too dark.

You mean OutputPass, correct? Are there any other effects in the stack? If you had other effects then presumably they’ll need to come after OutputPass to match what you did before. But that isn’t usually what you want, most effects should come before tone-mapping, see Different color output when rendering to WebGLRenderTarget - #7 by donmccurdy for more on that.

Thanks! Your remarks were really helpful!

Turned out, there was some sRGB correction in a shader I was not aware of already going on before, and then afterwards the tonemapping of the OutputPass happened on the already sRGB corrected images. If the factor of 0.8 is used before the sRGB correction then we get again correct alignment of the colors.

This means, I also have to correct something I said before:
renderer.outputColorSpace = THREE.LinearSRGBColorSpace but one of the shaders was already doing sRGB corrections, so the output image was in fact sRGB.
Thus, we compare the images with sRGB outputs of Cycles. We disabled any sort of tone-mapping. This works really good, we get very good alignment.

Cycles vs. Three


1 Like

The results looks excellent, glad that worked out!