What's this about gammaFactor?

webgl-renderer
gamma

#1

Hi,

I want to be sure my animation is showing the right (gamma-corrected) colors on screen. I’ve seen Threejs’s wegGL-renderer has a gammaFactor-property to set the gamma-value a boolean switch gammaOutput to activate it. All fine, but there are a vew questionmarks that I have right now, hopefully somebody here can help me with this:

  1. Why is the default value in threejs for the gammaFactor 2 instead of the usual 2.2 for the sRGB colorSPace?

  2. I would expect gammacorrection to be enabled per default on the renderer, 'cause we normally want to view our render on sRGB right? Am I missing something here? Is this left off to optimize performance?

  3. In the typescript definition of gammaFactor ( three-core.d.ts) it has a @deprecated without a string attached to it. Looking on the online API there’s no comment on deprecation and there’s no warning in the console either. Is gammaFactor deprecated? And than what to use instead? or is it future safe to use?

  4. Setting gamma on 2.2 (to have gamma-correction of 0.45) and turning gammaOutput to true looks like the right brightness for a gamma corrected graphic, but there are some pretty not-so-smooth transitions between gradients (using vertexcolors) that I wouldn’t expect. It looks like the gamma correction isn’t quite right, s this gamma correction really correct?

Thanks in advance!


#2

(1) and (2), the three.js defaults were chosen a long time ago and haven’t been changed, yet, to avoid breaking colors for users. But you’re right, for correct rendering you should always1 be setting:

renderer.gammaOutput = true;
renderer.gammaFactor = 2.2;

In addition, you’ll want to set texture.encoding = THREE.sRGBEncoding for any textures containing color information… Many loaders2 will not do that automatically, and so you’d need to traverse the model and modify the textures.

(3) I’m not sure why gammaFactor is marked as deprecated, or what that’s based on… there are plans to replace it with a proper colorspace feature (see https://github.com/mrdoob/three.js/issues/11337) but that hasn’t happened yet, so you should certainly use gammaFactor in the meantime.

(4) I’m not sure why this would be, could you post an example and/or demo? That may be a bug worth filing on the repository, as well.


1 An exception would be if you’re doing post-processing in linear colorspace, in which case you’d convert to sRGB at the end of that.
2 GLTFLoader is the only loader that configures texture colorspace automatically, at the moment. I recently wrote up a lot of this in the GLTFLoader documentation, as we’ve been working fairly hard to make PBR rendering consistent across different engines.


#3

Thanks for your reply @donmccurdy . I made you a chart to show you the differences between gammOutput turned on and off. [see pdf in the attachment]

It’s a pretty basic scene. There are no lights, no post-processing and no textures. I only use vertexcolors in a MeshBasicMaterial on the vertices you see with a label. No lights that can influence the colors. No shadows, no transparancies, no reflections.

Thanks for posting the link to another talk about this on github. I just read it and also read some other threads/issues on github about it. It looks like it’s going the right direction, can’t wait for it to be in there. ONce you know about color spaces and gamma correction it is important to take it seriously. I don’t really understand why threejs should still be ‘backwards compatible’ with that strange 2.0 gamma value that we’d normally never use and is rather confusing and making people set the wrong value without even knowing it, while there are other things changing in threejs updates that are far more breaking code, but hey; we can set it manually and that’s fine with me :slight_smile:

To be able to chose a color space on the renderer would be a great solution though!. The gamma factor is always bound to a color space, so just chosing a color space should be the way to go. Although we’d use sRGB always still for years to come I guess, 'cause most browsers only work with sRGB at the moment (or have other color spaces disabled in their settings). But very nice still if we could just set it to sRGB (or adobeRGB for a few) and perhaps make that use the sRGB features on webGL (I’m not that into the core of webGL, but I read there are some build in features to convert gamma and stuff).

The thing I can’t find is if it’s posible to set the sRGB space on the output. I read about renderer.encoding, but that didn’t make it to the build yet I believe and only works on textures right now?

I also applied an own gamma transfer function, but because I’m working with vertexcolors only on the primaries and the secundary colors, black and white, they all have ones (1) or zeros (0) in their r, g and b-channels, zo a power function doesn’t do anything when converting the colors to gamma space than. I always use threejs and I’m not very familiar with the WebGL core, but applying the gamma-correction just to the primaries that create the gradients doesn’t work and isn’t the right way to do it obviously. There should be a transfer function for each pixel, zo perhaps I end up creating a postprocessing shader to do so, but I’d rather not.

Anyway, here’s the chart. I added comments to it:

gamma-comparison-threejs.pdf (328.7 KB)


#4

Just to clarify:

property effect
gammaOutput whether to convert renderer output from linear to target colorspace (e.g. sRGB)
gammaFactor determines conversion factor for renderer output, and for texture input if texture.encoding = THREE.sRGBEncoding

Note that none of these affect vertex color input… The renderer always expects vertex color inputs to be linear. Are yours sRGB? That might explain things if you’re seeing double gamma correction here.


#5

Thanks for your response again @donmccurdy. Just to be clear here too: gamma is not a color space, but is an important part of a color space definition.
Where Color Models like RGB or CMYK tell systems about relative locations of colors, Color Spaces tell systems about the absolute locations of the primaries red, green and blue and the white point (D65 for sRGB) in a threedimensional reference space. So how red is the reddest red, how green the greenest green and how blue the bluest blue, and what do we call white.

Next to this there is a correction needed for old crt screens where the canons couldn’t convert input voltages linear to output luminance. Which still applies to flatscreens today. That’s gamma correction. Because gamma correction is affecting all color outputs, gamma correction is color-space specific, so included in a color space spec.For sRGB we count gamma around 2.2, so gamma correction for sRGB is (around) 1 / 2.2.

Yes, I understand threeJs works this way on the moment.

So let’s say we want to work in linear space, which is great to have the right lighting and stuff. If we input images made in sRGB color space (and if there’s no color profile embedded, it should be interpreted sRGB anyway), so the image has already gamma-correction applied. We tell threejs this by setting the texture.encoding to THREE.sRGBEncoding so it interprets its primary colors, whitepoint and gamma-correction as sRGB. With that the gamma-correction from the file is now known te be 1 / 2.2 is around 0.45. To make the image linear to fit in our linear environment, the gamma correction can be corrected to 1 by 1 / 0.45 again and we have the image in linear space. Great. The image is now at the same level as all materials in our scene and that’s what we want.

So we have a linear space and everything in it is linear, also all materials and vertex colors.
But now we want to output our linear render via an sRGB browser on an sRGB screen. And the browser and screen expect a gamma correction of 0.45 in order to be correct on a gamma of 2.2.

This means that ALL PIXELS in the output render need to go through a gamma transfer function to make the full render gamma-corrected.

Threejs should do that by setting these:

renderer.gammaFactor = 2.2
renderer.gammaOutput = true

Vertex colors we color with regular color values. These color-values are relative color values and so are not absolute color values. Unless we tell a system in which color space these values are in there’s no way we could know how red red is, how green green and how blue blue.

For vertex colors I use just the regular hex color values. These are all extremes: red, green, blue and the secondary mixing colors from that. These values are relative. And that’s exactly what we want, because we work in linear space. Only on the final stage, when all PIXELS of the render gets rendered they should be converted to the right color space, in this case sRGB. And only then their relative color values (like 0xFF0000 for the reddest red) get their absolute so real color definition. Only than we know how the reddest red looks like, because only than there is a definiton attached to all relative colors: a color space.

So to conclude: what a normal workflow would be if we want to work in linear space:

  • We import linear images (like openEXR) --> no conversion to linear needed

  • We import gamma corrected images (like jpg, png) --> conversion to linear space

  • We use colors, lights, materials and effects --> everything in linear space

  • We render --> convert linear space to a given color space and apply the gamma correction that comes with that color space --> so for sRGB the gamma transfer function on all PIXELS should be 1 / 2.2 gamma = 0.45.

What I did was exactly that: I create everything in linear (and relative) space and only on the end I gamma correct it. So there shouldn’t be double gamma correction.


#6

Thanks — Appreciate the clarification on color space and gamma. Ideally a future version of the threejs API will let users just set colorspace and not worry about gamma, but for now we have to discuss both, and I’m likely to get more of the terms wrong. :sweat_smile:

I think we’re on the same page with everything else you said, except this —

Relative to what, though? Assume we’ve set gammaFactor+gammaOutput as we discussed. If I assign a vertex color as 0x4285f4 and then render a div in CSS with background: #4285f4, I presumably want the results to match, but in fact they’ll be very different… that vertex color is assumed to not need conversion to linear space, but when rendered each pixel will have the gamma transfer function applied.

But if you’re confident you’ve created everything in linear space, maybe this is not the issue.


#7

HI @donmccurdy no problem you got terms wrong; it’s a very confusing subject and there seems to be a lot of wrong information on internet about it that makes it even more confusing.

It’s too much to explain everything here, but in short: a color model, like RGB or CMY, is a description of how colors are made. For RGB we use primaries red, green and blue to do that. So in a color model on internet we say that #000 is black, #FFF is white, #F00 is red and so on. These are numeric values for all possibilities inside that specific color MODEL. #F00 is the redest red in the model, #0F0 the greenest green and #00F the bluest blue. But that’s all we know.
So a color 0x4285f4 doesn’t tell how that color really looks like.

We still don’t know how white looks like, how red the redest red is, the greenest green and the bluest blue, and with that all values in between and mixed. A lot of developer think that #FFF is absolutely white… but you have all kinds of whites. There is no real white. The same goes for red, green and blue. So a color Model is relative, because we don’t know the absolute values for them to describe them.
Without that definition (the color space), these color hex-values look different on different screens. Eventhough the digital values are exactly the same, the device doesn’t know how to translate them into real colors.

That’s why we need color spaces. They describe how to translate these values into real colors. For this we need to define the exact definition of all three primaries in a CIE reference space and also define what we call white. Therefore we call a color space absolute. And if we display the same color values on a different device that supports the color space the image is in, the colors should (idealy) be the same.
With a color space applied now we do know how 0x4285f4 really looks like, because it is defined.

For internet this color space is from the beginning sRGB. But there is a transition going on for browsers being able to support more (wider) color spaces with much more color definition in it, so we can have ‘purer’ primary colors.

I did a dive in on the matter of color on internet according to use/no-use of color spaces to find out if css-color, svg-color and webgl-colors have a color space attached. And it seems they do.

I found, among others, this great article from webkit: https://webkit.org/blog/6682/improving-color-on-the-web/

Just like images also css-colors, svg-colors and webgl-colors are in sRGB color space, so absolute. That also means that these values are already gamma corrected. Or at least they should be, 'cause they are in sRGB space and sRGB means that they have a transfer function (gamma correction) applied.
So that could explain why the vertex colors I use are right if I leave gammaOutput on false and wrong if I apply a transfer function to gamma correct it. Although the resulting colors look too dark to me when I compare it to images I find on the matter of color on wikipedia, which I take seriously because these people write on the matter themselves, so I’m not fully convinced.

So to go back to threejs… I don’t know about the core of WebGL like you guys do, so I don’t know if there is some setting in webGL to go around the sRGB color space and work linear, but what I find doing a deep search and reading some articles is that webGL is always sRGB. But I’m not sure.

So it looks like webGL and canvas are always in the sRGB color space. If that’s true it means that there’s no way we can work linear in webGL. And if that’s the case than making textures linear at import doesn’t make any sense and putting the renderer to enable gammaCorrection is always wrong, because everything is already is sRGB color space and so already gamma-corrected. So if webGL always works in sRGB the whole renderer.gammaOutput and renderer.gammaFactor thing arn’t working as supposed to do. And a conversion at importing images should only be needed if we import images which are not in sRGB color space. To make them sRGB. But again, maybe there is a setting in webGL to make webGL work in linear space. But I couldn’t find anything about that so far.

And than we have CSS-colors and SVG-colors. CSS-colors and SVG-colors seems to be two different modules. CSS-colors are in sRGB space and so are SVG-colors.
But they are working on CSS Colors level 4 and that has some real interesting additions to apply other colors spaces!!
Read this: https://www.w3.org/TR/css-color-4/#sRGB

There will be a setting to set a full html-document in a different color space. And there will be even a css way of importing custom color profiles! So that means we could work in all existing color spaces!
Next to this we can define a color to be in a special color space. And there will be even media queries to check for color space support on that browser/device, also within the element so we can load different pictures for different color spaces. Also SVG has (already) some support for different colorspaces, but version 2 seems to take years to finally be implemented.

Anyway, this will be a huge improvement on internet and it’s the next big step to go to visual quality after the web changed to high-definition/retina/hi-resolution screens.

So to conclude: I’m not sure if the vertexcolors are right, because they look much brighter on examples online, but it seems like webGL is doing everything in sRGB already and so gamma-correction at the renderer should be never needed as far as I can find information about it at least in webGL 1. And if that’s true it means that working linear isn’t possible, just because and are always in sRGB color space.

Food for thought…


#8

Great article! :+1:

Just like images also css-colors, svg-colors and webgl-colors are in sRGB color space, so absolute.

… I don’t know if there is some setting in webGL to go around the sRGB color space and work linear, but what I find doing a deep search and reading some articles is that webGL is always sRGB. But I’m not sure.

WebGL 1.0 does not have low-level color space settings like more modern graphics APIs, but in practice, libraries like three.js apply gamma correction in shader code and work in linear anyway. There are limitations to that approach (e.g. with precision and compressed textures, I believe?) but nonetheless linear rendering is possible. Setting gammaOutput=true enables per-pixel output gamma correction.

So to conclude: I’m not sure if the vertexcolors are right, because they look much brighter on examples online,

I think I need a more concrete problem to help you with this… can you share an example model, and how it was created? When you say “they look much brighter online”, what are you comparing to? While both three.js and e.g. Unity (1) support FBX and (2) support linear rendering, it’s entirely possible that an FBX model loaded in both will look different, for various reasons.


#9

@donmccurdy I got the strong feeling you didn’t read or understood my post. According to info I find online everything inside canvas and webGL is in sRGB, so that’s not linear but should apply a gamma transfer function ‘automatically’, because sRGB has a gamma of around 2.2 by the spec. So if that’s true, and it seems like it is, than we should never turn outputGamma nor inputGamma to true in threejs, because we would always have double gamma.

You also ask me where I compare vertext colors to, but I already wrote you in my post I compare them to the colors I found on someone who made a 3D graphic about the sRGB color space on a wikipedia page. So that person should know the right values to use I’d say.


#10

According to info I find online everything inside canvas and webGL is in sRGB

Could you post your sources? The actual WebGL spec doesn’t have anything to say about color spaces.

https://www.khronos.org/registry/webgl/specs/latest/1.0/


#11

Yes, I am reading your posts entirely, and understanding well enough. But I need to reproduce the result shown in your PDF, and so far I don’t have enough information from you to do that. If linear rendering is not working in correctly in three.js we should file a bug and get people better-qualified than me to look at it, but this graphic does not demonstrate that there is any issue by itself.

According to info I find online everything inside canvas and webGL is in sRGB, … So if that’s true, and it seems like it is, than we should never turn outputGamma nor inputGamma to true in threejs, because we would always have double gamma.

This isn’t correct — could you provide links so I can see what you’re reading? From Realtime Rendering, in the Display Encoding chapter:

In most cases you can tell the GPU to do [color correction] for you… Most APIs controlling GPUs can be set to automatically apply the proper sRGB conversions when values are read from textures or written to the color buffer. … If you do need to apply sRGB manually, there is a standard conversion equation …

To the best of my understanding, WebGL is one of the APIs that is missing some of these features to automatically apply sRGB conversions, but that simply means we have to apply the standard conversions in shader code instead. Three.js does so, and implements linear rendering.


#12

I already did post it in a previous post here. It’s by webkit about webGL. It’s written in 2016, but it obviously talks about css, svg, webGL 1 and canvas and looks like it’s still pretty present today, at least on the webGL 1 part. This was the link I posted: Webkit: Improving color on the web

The article states:

While CSS handles most of the presentation of an HTML document, there is still one important area which is outside its scope: the canvas element. Both 2D and WebGL canvases assume they operate within the sRGB color space. This means that even on wide-gamut displays, you won’t be able to create a canvas that exercises the full range of color.

I’m not into the core of webGL and I can’t find much info about using color spaces in webGL neither like you. But I’d say webkit is a pretty decent and reliable source. What I do know is how color spaces work, because I put a lot of time last weeks/months deep diving into the matter. And know that the sRGB color space is especially created for internet by Microsoft and HP those days. I also found that both CSS colors as well as SVG colors are already in the sRGB color space. And using sRGB also influences the gamma transfer function which so using sRGB should also mean that all colors used in the browser would be gamma corrected already. So all your stylesheet-colors should be already in sRGB and thus will be gamma-corrected at the end.

So my conclusion so far is that everything in the browser is working with sRGB and that colors will be gamma corrected and browsers expect our images to be in sRGB and already gamma corrected.

The question is now how about webGL. According to the article both 2D canvas as well as webGL (at least v1) are assuming everything to be in sRGB. So that fits with how the rest of the browser handles images. So logical conclusion to me would be that all colors in webGL (at least v1) are working exactly like colors in css and svg, so in sRGB color space. So that they will be gamma corrected by the system automatically. Always. Probably at the stage of rendering writing frames to the canvas.

So than again the conclusion would be that everything is already taken care of on the output. So colors will be in the right sRGB color space with gamma-correction applied, and images are assumed to be in sRGB already and thus gamma corrected already. And according to that article that would also mean that every internal working inside webGL would be in sRGB too and so threejs don’t need to worry about gamma-correction. Not on the input, nor on the output. But again, there’s not much info about how color spaces are implemented exactly on internet.

Yesterday I did another search and found some info of people trying to work linear with Unity on webGL 1. The answer they get was that Unity wan’t able to work linearly with webGL. But I found another source on a Unity Blog that’s writing about that linear support is there now in webGL 2. So this seem to confirm my believe that webGL 1 always works in sRGB and isn’t able to work in linear space. And it would also explain why we can’t find much info on the subject of using color spaces in webGL 1.

So I still have the idea that threejs’s input and output gamma settings are unnececary and they seem to cause behaviour we don’t want. Like applying another gamma correction, although the system probably already does that automatically on itself, probably at renderstage at the end. Just like the rest of the browser does. And that makes sense to me logically. Otherwise webGL would work completely different than the rest of the browser and we should at least know about a setting in webGL to set it to linear or some color space right? So I believe the article of webGL already says it and is still relevant on webGL 1 today.

I don’t believe this is true. It looks like Unity wasn’t able to work linearly at first because they relied on webGL1 and they are able to work linear only from webGL 2. Next to this the webkit article says browsers are assuming everything is in sRGB space. It could be indeed that webGL 1 is chosing to convert images automatically to linear space and works linear. But than 1) the gammaInput-property of threejs would be usesless, because it ALWAYS assumes images are in sRGB. And 2) than the gammaOutput-property of threejs would be not needed either, because webGL HAS TO convert everything to sRGB at the end, because everything else on the page works in sRGB and you can’t have multiple color spaces on the same page. Everything should be rendered (at least anno 2018) to the same color space at the end and that’s sRGB in 99.9% of all browsers, because sRGB has the widest support, is meant for internet in the first place and almost all consumer-monitors can’t handle other color spaces.

Were did you find this? Is it about webGL 1?
I can’t really tell what they say exactly, but it’s true that we don’t need to have sRGB conversion functions if what I think is true. Because all colors will go through some lookup-table to apply gamma-correction (like css colors and svg colors, maybe they even share the same codebase internally in the browser?) and the webkit-article says that images are assumed to be in sRGB (so gamma-corrected) already, so there’s no need for us to manually apply any conversion to or from sRGB. They only time you could want to convert is when you want to use textures which don’t have the sRGB color space and gamma correction applied, so when images come from another color space, have another gamma transfer function applied, or are linear. So to me this makes sense.


#13

This means that when the browser reads pixel data from a 2D or WebGL canvas, it assumes the data written there is in sRGB. It does not mean that the browser forces shaders to put pixel data there correctly — it cannot. For that browser assumption to be met correctly, you should be using gammaOutput=true as we discuss above.

ThreeJS, BabylonJS, and PlayCanvas all use linear rendering — there’s nothing fundamentally impossible about doing so in WebGL. The line I quoted above was from the fourth edition of Realtime Rendering, but it doesn’t talk about WebGL specifically there. For Unity, I believe their linear rendering depended on WebGL 2 for a particular compressed texture API, which isn’t the core issue here.

Unfortunately, I don’t have time to persuade you in detail that ThreeJS, BabylonJS, and PlayCanvas have not made some huge mistake. If you’ve created an example or a model you can share, and see results suggesting that linear rendering is not working as intended, I am glad to help you debug.