[WIP Prototype] Add HDR support to tonemappers
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 build artifact here.
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:
- Enable 2D HDR:
rendering/viewport/hdr_2d - 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):
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
whiteis less than 1.0 and HDR has been enabled. The code currently checks to see ifmax_value != 1.0instead of checking if HDR is enabled, but this is incorrect becausemax_valuecan equal1.0when HDR is enabled andreference_luminance == max_luminance.
Makes me think we should take a little time to ponder about supporting HDR color pickers. Such as suggests this proposal: https://github.com/godotengine/godot-proposals/issues/1031 Edit: there's a pending implementation: https://github.com/godotengine/godot/pull/103583
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
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 |
|---|---|---|
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.
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:
SDR.png: HDR is disabled in WindowsHDR.jxr: HDR enabled before opening the Godot editorHDR 2.jxr: Same asHDR.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.
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.
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.
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:
SDR.png: HDR is disabled in WindowsHDR Disabled.png: HDR is enabled in Windows, but Project Settings HDR remains disabledHDR 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.
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.
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.
I've created an SDR-only PR that implements a number of the elements from this prototype PR: #106940
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!
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/
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.
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.