Skip to content

Conversation

allenwp
Copy link
Contributor

@allenwp allenwp commented May 21, 2025

This prototype demonstrates how tonemappers could behave when HDR output is enabled. Please provide feedback in the comments! You can use Windows Game bar to take HDR screenshots OBS Studio has good support for HDR video recording as well (let me know if you want me to give you OBS configuration details).

This branch builds on top of #94496. Please share any feedback relating to HDR (not tonemappers) on that PR instead! Many of the changes from this PR should be merged in a separate PR, as these changes include adding white parameter support to the AgX tonemapper, which is required independently of HDR output support.

This draft PR implements godotengine/godot-proposals#12317, including the addition of two new tonemaping user parameters: black and contrast, both of which I feel are required for flexible and stable HDR output behaviour. Please review this proposal for rationale.

Importantly, Flimic and ACES tonemappers cannot support HDR output. This is because the white parameter of these tonemappers was designed exclusively for SDR and this style of white parameter cannot work with HDR and variable Extended Dynamic Range (EDR). This PR demonstrates how they should behave in HDR mode: exactly the same as they behave in SDR mode, limited to [0.0, 1.0] range.

Because Filmic cannot support HDR mode, I have introduced a new tonemapper that is HDR-compatible to replace it: the Adjustable tonemapper. Unlike AgX, this tonemapper does not desaturate colours to white as they become very bright and applies the tone curve directly in the linear sRGB working colour space, just like Filmic does. It's configuration parameters match AgX and it can be configured to appear similar to Filmic in SDR.

Check out videos of how this new "Adjustable" tonemapping curve works on my blog post.

On a related note, the new configuration parameters of AgX (specifically contrast) can be used to make it appear similar to ACES, but with correct HDR output support.

Usage

Download windows-editor Artifact from the Checks page of my branch.

The basics of tonemapping in Godot is covered in the docs. In this PR I've updated the in-engine docs to have some more detail.

HDR output is covered in detail in the original PR #94496, but here's a quick summary:

  1. Enable 2D HDR: rendering/viewport/hdr_2d
  2. Enable HDR output: display/window/hdr/enabled

To control HDR brightness settings:

Option 1: Simply change the SDR content brightness in Windows (you might need to move or resize the Godot window to force it to refresh):
image

Option 2: Disable display/window/hdr/use_screen_luminance and manually set the Reference Luminance (this is equivalent to SDR content brightness) and Max Luminance.

Note: Reducing the max luminance with Option 2 may be necessary to circumvent the built-in tonemapping of your display.

Performance

Performance of the AgX tonemapper is better than the current implementation in master. I wasn't able to get as good of performance as my best attempt using Timothy Lottes' curve in #102435, but the stability and predictability of the curve in this PR across all of variable EDR is a reasonable trade off that gives a significantly better user experience when targeting both SDR and HDR.

A number of additional parameters are now passed from the CPU to the GPU, increasing the number of bytes passed into the tonemapper. This has a minor impact on performance (barely measurable on my NVIDIA 980 Ti for a 4K window). Unfortunately, this affects all tonemappers... which leads me to my next point:

Adding another tonemapper to Godot, as it is currently implemented, causes a notable performance degradation to all tonemappers. This happened in Godot 4.4 when AgX was added to Godot, and it will happen again when the new Adjustable tonemapper is added. Work needs to be done to address this at a structural level.

The black parameter makes ALL tonemapping notably slower. I haven't looked into how this can be optimized yet; this PR simply demonstrates how it should look when it is correctly implemented.

Limitations

  • Windows only
  • Currently only works with Mobile and Forward+ rendering methods.
  • Optimization work is not complete: It's possible these changes cause a minor performance regression and performance of the new tonemappers is not representative of their final optimized form.

Known Issues

  • My intent is for Reinhard to behave differently than SDR when white is less than 1.0 and HDR has been enabled. The code currently checks to see if max_value != 1.0 instead of checking if HDR is enabled, but this is incorrect because max_value can equal 1.0 when HDR is enabled and reference_luminance == max_luminance.

@adamscott
Copy link
Member

adamscott commented May 21, 2025

Makes me think we should take a little time to ponder about supporting HDR color pickers. Such as suggests this proposal: godotengine/godot-proposals#1031
Edit: there's a pending implementation: #103583

@Jamsers
Copy link

Jamsers commented May 23, 2025

I tried this PR out with Crater-Province-Level, and took some HDR screenshots, then took some SDR screenshots for comparison. I can't speak to the quality quite yet because I just borrowed the living room LG C3 to take these shots, and that TV isn't calibrated for accuracy or neutrality - it had auto tonemapping and over saturated colors and all that. What I will say is - it works - and it looked ****ing amazing on the big TV. 😆

I'm linking a public Google Drive folder that contains the screenshots because the folder is too big to upload to Github. (148 MB. HDR screenshots are massive! And these are only 1080p!) I recommend downloading the folder locally to your computer, and opening the files with the built in Windows image viewer. Of course, make sure HDR is enabled in the settings.

The HDR screenshots are in JPEG XR format (.jxr) and the SDR screenshots are in PNG format.

https://drive.google.com/drive/folders/1roqSMTxdNaFINImqaxk_ReQAqtpUrz0n?usp=drive_link

@allenwp
Copy link
Contributor Author

allenwp commented May 23, 2025

EDIT: I've updated this branch to use a CIE-correct black tonemapping parameter. The performance is trash, but it demonstrates how a black tonemapping parameter should work. My old approach was high performance, but not suitable for a general purpose game engine where some users might want to increase the black parameter to hide objects in shadows until a light is shone on it, etc.

I don't like how this black tonemapping parameter works. It might be computationally cheap (only one vec3 arithmetic operation per pixel), but it introduces massive hue shift with dark saturated colours for all tonemappers.

Here's a comparison of the Reinhard tonemapper with a white of 1.0 (meaning it's equivalent to the Linear tonemapper):

No black CIE-correct 2% black This PR's hacky 2% black
Reinhard-No-Black Reinhard-CIE-Correct-Black Reinhard-Hack-Black

Notice how the dark values incorrectly converge to red, green, and blue. This is a similar effect to poor quantization of dark values, which is generally not something that should be simulated in HDR when much better quantization is possible. Additionally, while there is no objectively correct solution to hue shift when compressing bright values into a lower dynamic range, there is a correct solution in the world of CIE colour science for reducing the low end of dynamic range without getting a bad hue shift.

Although the effect from the hacky approach in this PR might be reasonable for some cases, I feel that it is not suitable for including in a general purpose game engine when the result is so different from the well understood correct approach.

@iuymatiao
Copy link

Hi @allenwp,

I've gone ahead and tried out your PR to see how HDR is working on my Windows 11 PC. I don't have a personal 3D project going on, so I'm just using the opening scene of the TPS demo for testing.

I've taken some screenshots on an ASUS PG27AQDM OLED monitor, using the "Console HDR Mode", using the Nvidia App to take the screenshots, and setting the SDR content brightness in the Windows settings to 14. I haven't yet added a tonemap to the Camera3D node. Here is the link.

To explain the three screenshots:

  1. SDR.png : HDR is disabled in Windows
  2. HDR.jxr : HDR enabled before opening the Godot editor
  3. HDR 2.jxr : Same as HDR.jxr, but enabled HDR 2D and Display/Window/HDR before running the demo

When viewing through the Windows Photos app via the OLED monitor, only HDR.jxr seems to be taking advantage of the monitor's max luminance (around 900 nits). As for HDR 2.jxr, it's almost identical in dynamic range to SDR.png, to the point that it may as well be a SDR image. I don't know if this is working as intended, but I thought this was worth mentioning here.

As was pointed out in the other PR thread, I can't get these screenshots to perfectly align in colors and brightness when viewing in SDR. Leaving aside HDR 2.jxr, which might be an unrelated issue, the differences between SDR and HDR are difficult to get my head around.

My assumption is that the tonemap timeline of SDR.png looks something like this:

Internal Luminance (Godot) -> Internal Tonemap to SDR (Godot) -> Direct Conversion to PNG (Nvidia App)

Whereas HDR.jxr in SDR looks like this:

Internal Luminance (Godot) -> Internal Tonemap to Monitor Max Luminance (Godot) -> Direct Conversion to JXR (Nvidia App) -> Tonemap to SDR (Windows Photos app)

If so, I wouldn't rule out the possibility that the Windows Photos app might be altering the colors that's totally outside our control. To be honest, I'm not sure if it's feasible for the "double tonemap" that's occurring with HDR.jxr (first within Godot, and again within Windows Photos) to come to the exact same colors as just performing the tonemap exactly once as in the case with SDR.png. Maybe there's some inevitable "color wear and tear" that occurs when performing a tonemap multiple times from internal 3D render to shareable image file.

@iuymatiao
Copy link

Looks like I made a big oopsie and took those screenshots while RTX HDR was enabled globally. So the screenshots are not accurate for a baseline HDR setup. I’ll rename the folder above to fix this error.

In my spare time, I’ll redo the screenshots with RTX HDR disabled. Since this PR is focused on the tonemappers, I’ll go ahead and test Linear and AgX specifically.

@allenwp
Copy link
Contributor Author

allenwp commented May 26, 2025

Looks like I made a big oopsie and took those screenshots while RTX HDR was enabled globally. So the screenshots are not accurate for a baseline HDR setup. I’ll rename the folder above to fix this error.

In my spare time, I’ll redo the screenshots with RTX HDR disabled. Since this PR is focused on the tonemappers, I’ll go ahead and test Linear and AgX specifically.

Cool, thanks @iuymatiao! I honestly haven't tried many approaches for taking screenshots and videos yet, so I can't even comment on whether the process is beneficial.

In the end, it is most important that you get consistent behaviour between SDR and HDR modes with the only difference being that the brightest parts of the scene appear brighter in HDR mode. This naturally brings a higher contrast and vibrancy to the image in HDR mode as a side-effect. Importantly, you should notice that scenes that have no bright values (underexposed scenes) should be identical between HDR and SDR. This is important to aid developers in being able to easily and quickly author content that is consistent between the two output modes, with HDR simply taking advantage of the higher brightness that is available.

@iuymatiao
Copy link

OK. Here is Round 2. This time, RTX HDR has remained disabled globally. The link to the new screenshots can be found here.

Folders are split between the Linear and AgX tonemappers. Describing each file:

  1. SDR.png : HDR is disabled in Windows
  2. HDR Disabled.png : HDR is enabled in Windows, but Project Settings HDR remains disabled
  3. HDR Enabled.jxr : HDR is enabled in Windows, and enabled HDR 2D and Display/Window/HDR

Note that I'm using the 'Use Screen Luminance' setting in Project Settings, and SDR Content Brightness in Windows 11 is set to 14.

Unfortunately, for some reason, the peak brightness seems to be unchanged over SDR. For what it's worth, the Nvidia App did recognize that HDR was enabled in the project, hence why HDR Enabled is a JXR file and not PNG like the other two. It's possible that I missed a brightness setting somewhere, or perhaps the TPS Demo is just ill-equipped for testing HDR in its out-of-the-box state. If someone has a sample project that is ready-made to test HDR brightness, I'd be happy to give it a test.

With that said, I did find some interesting observations. When viewed on my HDR monitor, with the Linear tonemapper, SDR.png and HDR Enabled.jxr look very similar overall. The image differences become stark when viewed in SDR, but when HDR is enabled in Windows, they're nearly indistinguishable. For the AgX screenshots, SDR.png and HDR Enabled.jxr don't look quite identical, with HDR Enabled.jxr being slightly less saturated. However, SDR.png and HDR Disabled.png look nearly identical when viewed in HDR.

I'm not sure what to make of these findings. Right now, I can't get HDR to explicitly appear unless I use Nvidia's RTX HDR feature (and keep Project Settings HDR disabled). Not sure how @Jamsers got it working, but it might be worth double-checking if RTX HDR was globally enabled there as well. If there's a silver lining, the fact that RTX HDR stopped functioning as soon as Project Settings HDR was enabled probably means that RTX HDR recognized a native HDR implementation and "stepped aside". If so, that means it's just a matter of getting peak luminance to work properly.

I'll probably need to find another Godot project to test to make sure I get a fuller picture.

@Jamsers
Copy link

Jamsers commented May 27, 2025

Hi @iuymatiao, for me, I had to use Godot's DX12 driver to get HDR working, HDR didn't seem to work when using Godot's default Vulkan driver. This can be changed in rendering/rendering_device/driver in project settings by setting it to "d3d12".

However, my own builds of this branch didn't have DX12 working, because IIRC you need to do some Windows SDK specific stuff to have DX12 working with Godot. So I used the build from the CI artifacts from allenwp's branch. (the latest one as of this comment can be found here, just scroll down and download the windows-editor artifact)

I think HDR isn't working on Vulkan on Windows yet because for Vulkan on Windows, I think you need a DXGI swapchain for HDR to work, and there isn't any progress on that front.

Apart from changing the tonemapper of my project from ACES to AgX, enabling Windows HDR, and enabling Godot HDR, I kept everything else at default.

@Jamsers
Copy link

Jamsers commented May 27, 2025

Another thing to note may be that differences between HDR and SDR for my project (Crater-Province-Level) might be a lot more significant compared to other Godot projects because I use physical light units in that project and stick to as close to reference values for all light brightness. That means that, at noon for example, you could be getting as much as 100,000 lux outdoors, while only getting 1,000 lux indoors. Such an extreme differential in luminosity lends itself well to HDR, as you can imagine.

@allenwp
Copy link
Contributor Author

allenwp commented May 30, 2025

I've created an SDR-only PR that implements a number of the elements from this prototype PR: #106940

@allenwp
Copy link
Contributor Author

allenwp commented May 30, 2025

OK. Here is Round 2. This time, RTX HDR has remained disabled globally. The link to the new screenshots can be found here.

Edit: I wrote this wrong initially, I meant to say: For linear, the SDR, HDR disabled, and HDR enabled screenshots should look the basically same regarding all values that are less than 1.0 when viewed on Windows with its HDR mode enabled, so long as the Windows SDR Content Brightness matches the way it was set when the screenshots were taken.

...But dark colours are more vibrant in the HDR version than in the SDR version, so maybe taking screenshots isn't working quite right. Or it's something in else the environment effects. I suspect I'll need to look more into screenshot capture to see if there's a stable way to capture output.

I also plan to make a little test scene for HDR output. I'll do this sometime in the next couple of weeks, likely.

Thanks for checking this out!

@allenwp
Copy link
Contributor Author

allenwp commented Jun 12, 2025

For those interested, I've made a blog post that has videos of how the new Adjustable tonemapper works:

https://allenwp.com/blog/2025/05/29/allenwp-tonemapping-curve/

@iuymatiao
Copy link

Just wanted to provide a quick update that I was able to get an HDR output when using @Jamsers's Crater Province project. So the existing Godot demo projects really aren't built to push max luminance at the moment.

@allenwp allenwp force-pushed the rendering/hdr-output branch from c7f8582 to 275a57c Compare July 16, 2025 15:29
Co-authored-by: Alvin Wong <[email protected]>
Co-authored-by: Allen Pestaluky <[email protected]>
allenwp added 2 commits August 5, 2025 11:23
Fixed negative numbers created by black parameter.

Updated HDR tonemappers to include udpates from godotengine#106940

This is a CIE correct way of doing a "black" tonemapper parameter.

Attempt at optimization of the curve function

Changed minimum white parameter for AgX to be 1.0, just like the Adjustable tonemapper.

Added docs for new tonemapping parameters.

Fixed unused variable error

Spattered my domain over the code to direct LLM AI users to the blog post I'm working on

Removed brightness tonemap parameter: this is artistic control is better expressed with exposure and contrast.

Added an "Adjustable" tonemapper

Added user controls for AgX HDR and black parameter for all tonemappers

HDR AgX (first pass)

Re-do tonemappers for HDR support (AgX HDR, Reinhard HDR, and a new curve...)
@allenwp allenwp force-pushed the rendering/hdr-output branch from 275a57c to 9103ac7 Compare August 5, 2025 20:13
@stuartcarnie
Copy link
Contributor

For those interested, I've made a blog post that has videos of how the new Adjustable tonemapper works:

Very cool – tried the HDR video on my MacBook Pro display, and it looks awesome.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants