Three color problem

Hi there,

I’ve just noticed that there is some sort of inconsistency on how Three handles colors.

Please check this example:

Both spheres have the same color assigned: rgb(15,15,255)
One is given a string with the color, the other a Three JS Color object.

Yet somehow the hues rendered in the end are noticeably different.
I can say for sure that the purple hue is wrong(the one using the new THREE.Color() object).

If this a problem with Three JS handling color or am I missing something?

Any clarification would be greatly appreciated.

it works better if you write color.getHex() or color.style

The problem is a need to set the color using .setColorAt which only takes a THREE.Color object, so I can’t pass it a string using color.getHex().

Any ideas why Three behaves likes this?

Just to clarify - that looks like react-three-fiber issue specifically? :thinking: Setting a Three.Color using either rgb / hex / string name does give the same, correct result in all three cases.

(/cc @drcmda )

2 Likes

This looks like a gamma/linear issue — see Color management in three.js. Perhaps react-three-fiber is doing some color management here that THREE.Color doesn’t provide out of the box?

4 Likes

Yeah it’s exactly that. R3f handles color conversion to linear space automatically if you pass in a string or hex (e.g. "orange", "rgb(0,10,0)", 0xff00ff) but if you create a Color manually you have to handle this yourself. Do this to make the colors match:

const color = new THREE.Color("rgb(15,15,255)").convertSRGBToLinear();

You can read about the rationale here:

6 Likes

like the others said, r3f handles color management automatically. every color, texture, etc is converted according to Color management in three.js

this is one of the few places where react deviates from vanilla three defaults, but you can completely disable it:

<Canvas linear>

but i would recommend not doing that. just like don’s article above states, incorrect gamma is the #1 culprit for that old, plasticky cgi look with blown out highs and lows, eye popping colors. most applications (photoshop, blender, etc) use srgb.

4 Likes

Thanks for the reply guys, much appreciated.

Pardon my not knowing but shouldn’t the conversion to linear space be handled by Three JS by default? And maybe override it if you want to?

Seems to me that that is the expected behavior. I think r3f handles it the right way, props for that.

If and only if you are using a “linear workflow” (recommended for PBR) then three.js should — in an ideal world — automatically convert material colors from sRGB to linear space for you. However a “linear workflow” is more of a collection of settings and practices than any one setting, these didn’t all exist in the project when three.js began, and so it’s not an easy API change to make retroactively. It’s been discussed some (see this thread) but I don’t know when or if there will just be a single setting to enable color management like this, it is difficult to do it right with backward compatibility.

2 Likes

All we need to do is add a colorManagement flag to the renderer. Have it false by default for backwards compatibility, true will apply all the recommended settings. I’ve followed all the discussion for the past couple of years and I really fail to see why this is seen as such a hard thing to do. React-three-fiber implemented it, for example.

I would love to be wrong, but I’m still not sure how to do this. After adding the flag to the renderer, would we do sRGB → linear conversion for every Color instance on every frame? I like the idea of treating any hex code as sRGB while treating .r/.g/.b components as linear in the Color class, converted automatically when setHex(...) is called, but the Color class can’t really depend on WebGLRenderer flags, so that’s messy for backwards compatibility. :confused:

After some discussion during the last release cycle it looks like changing the default “outputEncoding” to “sRGBEncoding” is being looked at – see here for the PR.

@looeee

All we need to do is add a colorManagement flag to the renderer. Have it false by default for backwards compatibility, true will apply all the recommended settings. I’ve followed all the discussion for the past couple of years and I really fail to see why this is seen as such a hard thing to do. React-three-fiber implemented it, for example.

In my opinion changing the outputEncoding to be sRGBEncoding is the right way to do it and would have a pretty easy migration step of setting outputEncoding back to LinearEncoding for those who aren’t ready for or don’t like the change. I’m not sure what else needs to updated. It’s been slow and maybe not as noticeable but there have been some nice ergonomic changes regarding color management with render targets and PMREM but there’s still a little more to go.

@donmccurdy

After adding the flag to the renderer, would we do sRGB → linear conversion for every Color instance on every frame?

Perhaps I haven’t thought it through enough but is it not the case that all material diffuse colors and vertex colors are assumed to be Linear as it is? I agree it might be nice to allow people to specify the color space for material colors and vertex colors so it’s easy to use CSS colors but I’d think that could be an independent effort from changing the default color output.

This problem is not insurmountable - and it certainly doesn’t deserve the years of bikeshedding that has happened. After all, R3F solved it in a couple of days and basically no one ever complained about how they did it - this is the first post I’ve even seen where someone asked for clarification on how it worked. #344 was opened on 18th April 2020, closed on 20th April 2020. Case closed, problem solved.

If we were too announce that in the next release of three.js there will be a colorManagement flag, we have one month to implement it, then we could solve it. All these questions about whether setHex should be converted (probably yes), whether r/g/b should be considered linear (also probably yes) - the answer doesn’t matter, as long as we actually pick an answer.

I think managing user created Color instances is not a good idea, possibly with the exception of Color.setHex or setStyle.

The way R3F does it is pretty smart. If you create a Color instance manually then you’re responsible for managing it yourself, but if you do new MeshStandardMaterial({color: 0xff00ff}) it’ll be converted automatically to linear space for you.

Well, maybe the renderer is not the right place for it. Maybe THREE.ColorManagement, similar to THREE.Cache?

Yes, but that’s part of the problem. You have to understand color spaces to know that, and most people just put in a CSS color and expect to work, then end up confused when their colors are washed out. Beginners should just be able to set colorManagement = true and not worry about it anymore. Experts who understand color spaces will probably also just use colorManagement = true but if they need to they can set it to false and have fine-grained control.

Won’t that break backwards compatibility? I thought the whole reason this is such a sticking point is that we don’t want to do that?

A THREE.ColorManagement object to enable/disable sounds like a nice way to go here.

…is it not the case that all material diffuse colors and vertex colors are assumed to be Linear as it is?

Color management would, presumably, change this for material colors. I’d assume that vertex colors are already linear.

After all, R3F solved it in a couple of days and basically no one ever complained about how they did it…

Somehow I don’t think we will be so lucky, switching to a linear workflow after 10 years of gamma workflow… :sweat_smile: :crossed_fingers:

The way R3F does it is pretty smart. If you create a Color instance manually then you’re responsible for managing it yourself…

Hm, that’s the only way they could have implemented it, given the available API surface, I think? What about a THREE.Color used for a uniform on a ShaderMaterial?

There’s some precedent for considering hexadecimal and CSS colors to be sRGB while assuming RGB components and arrays are linear — have a look at the documentation for Blender’s color picker: Color Picker — Blender Manual, same thing there. There’s no perfect answer here but I think it’s pretty good, and provides a nice symmetry because setting color.fromArray([.5, .5, .5]) and colorAttribute.setXYZ(index, .5, .5, .5) will give you the same result, both are linear. It’s rarer to use that syntax in CSS.

1 Like

Might as well take this conversation to GitHub… :sweat_smile: Add THREE.ColorManagement by donmccurdy · Pull Request #22346 · mrdoob/three.js · GitHub

1 Like

maybe this helps in some way, essentially this is what it does:

it converts colors, textures, also on shader materials and uniforms. this works for all props because they are immutable, i don’t change the user-provided data, only the internal representation of it:

<meshBasicMaterial color="hotpink" />

i do not allow it to convert externals because mutation is dangerous, externals are the users concern (i imagine that is threes dilemma):

const color = new THREE.Color("hotpink")
...
<meshBasicMaterial color={color} />

in the reconciler i know the object lifecycle and when props change on any and all objects, so i can convert colors on the spot no matter if hex, css or rgb. this happens only when props change, it’s not frame by frame.

const gl = new THREE.WebGLRenderer(...)
// Set color management
if (!linear) {
  if (!flat) gl.toneMapping = THREE.ACESFilmicToneMapping
  gl.outputEncoding = THREE.sRGBEncoding
}
function applyProps(instance, newProps, oldProps) {
    ...
    // Special treatment for objects with support for set/copy, and layers
    if (targetProp && targetProp.set) {
      ...
      // Otherwise just set ...
      else targetProp.set(value)
      // Auto-convert sRGB colors
      // https://github.com/react-spring/react-three-fiber/issues/344
      if (!rootState.linear && targetProp instanceof THREE.Color) {
        targetProp.convertSRGBToLinear()
      }
    }
    // Else, just overwrite the value
  } else {
    currentInstance[key] = value
    // Auto-convert sRGB textures
    // https://github.com/react-spring/react-three-fiber/issues/344
    if (!rootState.linear && currentInstance[key] instanceof THREE.Texture)
      currentInstance[key].encoding = THREE.sRGBEncoding
    }

texture handling is probably still incorrect, it should differ between LDR and HDR i believe, and it does unfortunately mutate which is a wart, but no complaints until now.

In Threes case i imagine the problem is communicating the users intent for color management to the object instances. How would a THREE.Color know that gl.colorManagement is true when the object has no connection to it (which is good, otherwise we’d have the same complexity as Bablyon).

What happens if i have 2 canvases, one color managed, the other not, and i’m sharing a THREE.Color between the two. If the canvases starts mutating my data, that seems dangerous, probably results in ping-ponging.

It’s cleanly solved by something that can manage the object lifecycle, but this isn’t threes domain - so maybe it’s fine as it is and abstractions have to take care of it. Or, three picks srgb as a breaking change outright. Just out of curiosity, is there ever a need for a non srgb-colorspace? With semantic versioning breaking changes are nothing to be afraid of, they are the norm, but as long as three runs on meaningless minors the extent in which it can advance is limited. Wouldn’t this be a chance to collect some heavy breaking changes that you always wanted, then finally give it a 1.0.0, and then patches, minors, majors?

Yeah, it’s another reason why this shouldn’t be on the renderer. But a Color knowing about a THREE.ColorManagement flag shouldn’t be too much of an issue.

How would you solve this without auto color management?

I remember from a while back that the duck/horse/parrot/flamingo have vertex colors in sRGB. Did we ever get to the bottom of that?

OK, fingers crossed it goes somewhere this time :crossed_fingers: :sweat_smile:

you don’t. you use two colors. but at least this is explicit. mutation is a slippery slope when it starts to implicitely mess with the ground data. for instance if the renderer that’s first calls convertSrgbToLinear(). if the color data remains untouched and it’s only getting interpreted in three according to the gl config (colorManagement=true/false), that would be perfectly fine.