-
-
Notifications
You must be signed in to change notification settings - Fork 101
Description
Describe the project you are working on
Godot engine
Describe the problem or limitation you are having in your project
Environment colour adjustments are incorrect or sub-optimal in a number of rendering configurations.
Describe the feature / enhancement and how it helps to overcome the problem or limitation
Considerations
Considering game developers using Godot:
- May change from a default engine setting late in the development process or after release, only when a limitation is met that requires the setting to be changed.
- Cannot predict with certainty which buffer format is best suited for their project at the beginning of development.
- Have HDR 2D disabled in their existing, released game and may wish to add HDR output support as a feature update.
- May wish to port their game to a platform that has technical limitations compared to their launch platform, such as from mobile to web or desktop to mobile.
- Require built-in features to maximize performance by minimizing colour transformations.
- May update the version of Godot to address critical or noncritical bugs late in the development process or after release.
- Expect the engine to apply appropriate colour maths when performing colour adjustments.
- May require 8-bit buffers for one export and improved quality with 16-bit buffers for another export.
Proposal overview
This proposal depends on #13181, which lays the groundwork for minimal colour transformations. (It enables BCS adjustments to efficiently be applied to linear values for all rendering configurations.)
I propose:
- The brightness, contrast, and saturation (BCS)
Environmentadjustments should be applied to linear-encoded values in the all rendering methods to minimize required colour transformations. - Adjustments visual quality should match or exceed existing glow effects.
- Editor BCS ranges should be changed.
- Improvements to the contrast and saturation function when applying them to linear-encoded values.
1. Apply BCS to linear-encoded values
After #13181 has been implemented, no additional colour transformations will be required to apply colour BCS adjustments on linear-encoded values for any rendering method, regardless of the HDR 2D setting; this is the only highest performance approach to BCS colour adjustments for some rendering configurations.
Impact on Brightness
The existing "brightness" colour adjustment simply multiplies the colour value by the brightness value. This image adjustment must be applied to relative luminance to ensures that the hue and saturation of colours is not affected by the adjustment. This means the multiplication must be performed on linear encoded values:
| Original scene | Godot 4.5 HDR 2D disabled | This proposal (clipped) |
|---|---|---|
![]() |
![]() |
![]() |
In practice, there is sometimes no clipping applied before the brightness adjustment, so an image might actually look like this. This is the same as the existing HDR 2D behaviour when using the Linear tonemapper and no glow effects:
| This proposal (not clipped) |
|---|
![]() |
Brightness scaling
While applying the brightness adjustment to linear values is the only correct approach, it means that animation of the brightness adjustment is not perceptually uniform as it would be when using the nonlinear sRGB transfer function. I propose applying this perceptually uniform transform function to the brightness value just before passing it to the post.glsl or tonemap.glsl shader. This allows the user to easily perform a perceptually uniform change of brightness while being correct in terms of colour science. The perceptually uniform change is especially valuable for easily animating a "fade to black" effect and matches the existing behaviour in Godot when HDR 2D is disabled or when using the Compatibility rendering method.
In this video I demonstrate the two approaches to the brightness adjustment: brightness fades in and out with the sRGB transfer function and then without (cycling back and forth between the two). Note that without the sRGB transfer function, the image fully darkens very suddenly, making for a more abrupt transition:
brightess-scale.mp4
2. Match or exceed existing visual quality
To mitigate challenges brought up by consideration 6, the visual quality of the new adjustments should match or exceed the existing implementation.
3. Changes to BCS editor ranges
When applying BCS to linear-encoded values, the following editor ranges should be used instead of the current [0.01, 8.0] range:
ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "adjustment_brightness", PROPERTY_HINT_RANGE, "0.0,2.0,0.01,or_greater"), "set_adjustment_brightness", "get_adjustment_brightness");
ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "adjustment_contrast", PROPERTY_HINT_RANGE, "0.75,1.25,0.01,or_greater,or_less"), "set_adjustment_contrast", "get_adjustment_contrast");
ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "adjustment_saturation", PROPERTY_HINT_RANGE, "0.0,2.0,0.01,or_greater,or_less"), "set_adjustment_saturation", "get_adjustment_saturation");By reducing the range of the contrast adjustment significantly, it allows for much better control of this very sensitive adjustment. The user can still set these above or below the minimums, allowing for full artistic control.
4. Contrast and saturation improvements
The current contrast and saturation adjustments have been configured to work well on nonlinear sRGB-encoded values and should be adjusted to work well with linear-encoded values.
Contrast
Encoding
There are three common colour encodings that are normally used when applying this sort of very basic contrast function: linear, nonlinear sRGB, and log encoding.
From my experience, RGB values encoded with the nonlinear sRGB transfer function work best for the very simple contrast function. This is the behaviour of Godot 4.5 and earlier with HDR 2D disabled. Unfortunately, there would be a performance cost to convert to and from nonlinear sRGB encoding when HDR 2D is enabled because there would otherwise be no nonlinear sRGB conversion taking place. For this reason, I believe the visual advantages of using nonlinear sRGB encoding for the contrast adjustment are not worthwhile (see consideration 5).
Log encoding provides good promise and is seriously worth considering. It may be implemented something like this:
color.rgb = max(color.rgb, 1e-10); // log2 requires positive values.
color.rgb = log2(color.rgb);
color.rgb = mix(vec3(-2.473931188332412f), color.rgb, params.bcs.y); // 0.18 middle grey in log2
color.rgb = exp2(color.rgb);Unfortunately, without additional colour grading and adjustment tools, this approach does not allow for rich/dark blacks and highlights clip badly when a user attempts to make dark values become black. With the very simple colour adjustment tooling that Godot provides, I don't think log encoding is a good choice for contrast adjustment.
For this reason, I propose that this simple contrast adjustment is simply applied to linear-encoded RGB values, which provides reasonably good results given its extremely high performance characteristics. This satisfies consideration 5.
Contrast pivot
The pivot of the contrast function is an important consideration. Currently in Godot, the contrast pivot is 0.214041140482232 when HDR 2D is disabled and 0.5 when HDR 2D is enabled.
I propose that the contrast adjustment pivots on linear-encoded values at 0.18, the industry standard "middle grey". Other alternative pivots could be CIE L* of 50% (0.1841865) or the previous Godot behaviour of the sRGB transfer function at 50% (0.214041140482232).
I did some comparisons with Adobe Photoshop 26.1 to see what pivot points it uses for its contrast adjustments. First, it is clear that both Godot 4.5 with HDR 2D disabled and Photoshop use the same pivot point when applying contrast to nonlinear sRGB encoded values:
| Photoshop 26.1 contrast | Godot 4.5 HDR 2D disabled |
|---|---|
![]() |
![]() |
I also found that the Photoshop camera raw filter contrast is applied to an entirely different colour space, but its pivot seems to be somewhere around 0.18. Here is a comparison with the proposed pivot point and Godot 4.5's behaviour with HDR 2D enabled (0.5 pivot):
| Photoshop 26.1 contrast (camera raw filter) | This proposal (0.18 pivot) | Godot 4.5 HDR 2D enabled (0.5 pivot) |
|---|---|---|
![]() |
![]() |
![]() |
Saturation
I propose that the saturation adjustment uses luminance weights rather than uniform weights for desaturation:
const vec3 rec709_luminance_weights = vec3(0.2126, 0.7152, 0.0722);
color.rgb = mix(vec3(dot(rec709_luminance_weights, color.rgb)), color.rgb, params.bcs.z);This approach produces noticeably higher quality results when desaturating images (note that rich/dark blue and red colours become brighter when using even weights instead of the Rec. 709 luminance weights):
| Original | Even weights (Godot 4.5 HDR 2D enabled) | This proposal (Rec. 709 luminance weights) |
|---|---|---|
![]() |
![]() |
![]() |
When increasing saturation, there are arguments for using even weights between channels or to use arbitrary (subjective) weights instead of luminance weights. I did some quick tests to see what Photoshop's camera raw filter does and my guess is that it uses luminance weights with additional processing to colours that would be clipped by the target dynamic range. Here is a comparison of my attempt to match Photoshop with the two approaches to a saturation adjustment:
| Even weights (Godot 4.5 HDR 2D enabled) | Attempt to match Photoshop to even weights |
|---|---|
![]() |
![]() |
| This proposal (Rec. 709 luminance weights) | Attempt to match Photoshop to luminance weights |
|---|---|
![]() |
![]() |
I was concerned that blue colours would not behave nicely when increasing saturation, but to my surprise I feel that the results in this bistro scene are actually better when using luminance weights because blues and reds don't become darker when increasing saturation.
While the behaviour of increasing saturation is fundamentally subjective, I believe there is relevance to matching Photoshop's modern camera raw filter behaviour. Additionally, desaturation is scientifically correct only when using the colour primary's luminance weights.
Before or after tonemapping
There is a substantial argument that adjustments should be applied before tonemapping. To maintain compatibility with existing projects that may wish to use the new adjustments but do not want radically different interaction with their tonemapping function, I propose that the existing behaviour of applying adjustments after tonemapping be kept in Godot. This means that applying adjustments before tonemapping must be introduced as an additional feature. I believe this additional feature is outside of the scope of this proposal.
Describe how your proposal will work, with code, pseudo-code, mock-ups, and/or diagrams
(See above)
If this enhancement will not be used often, can it be worked around with a few lines of script?
No
Is there a reason why this should be core and not an add-on in the asset library?
This proposal is regarding the math and logic inside of tonemap.glsl and post.glsl.














