Unreal 5 – Colour Management

Colour Management for Unreal Engine 5. In this post, we discuss configuring a project to render in a wider gamut.

Feature Image

Colour Management in Unreal

Let's talk about colour management and unreal engine 5. By default, Unreal Engine renders in the sRGB colour space, and it assumes all textures are imported in that colour space. This isn't necessarily a bad assumption: at present, the vast majority of computer monitors still use the sRGB colour space. Some monitors, however, make use of wider gamuts. Modern HDR-compliant monitors in particular are in this category. On that front, even when internally rendering in sRGB, Unreal is still capable of outputting a good HDR experience to these panels. But what if we want to go further? If we want to push visual fidelity a bit more?

There are a variety of reasons to want to render in a wider colour space, even if the target is ultimately an sRGB panel. One of the primary reasons is to maintain as much colour information as possible throughout the rendering pipeline and to render as close to a spectral renderer as possible. As an aside, spectral renders are largely the gold-standard of realistic renderers. The sRGB colour space only encapsulates a very small portion, comparatively, of the human visible colours. Wider gamuts, such as BT2020 and ACEScg, encapsulate far more, with ACEScg encapsulating the majority of human visible colour.

Side Note: ACEScg

As a small side note, the ACES colour space in general encapsulates the entire human visible spectrum. I'm particularly talking about the AP0 gamut. This gamut encapsulates all human visible light spectra. We don't make heavy use of AP0 in realtime 3D due to its blue primary being negative and there being a large distance between all three primaries, all of which as also imaginary. The AP1 gamut used in ACEScg, on the other hand, does not have imaginary primaries. This lends itself far better to 3D and is where ACEScg gets its "cg" denotation.

A second good reason to want to render in a wider colour space is the proliferation of HDR-compatible or -compliant panels. Over the last eight or so years, HDR has gone from somewhat-of-a-gimmick to a noticeable immersion-improving experience. In no small part, this is due to advances in monitor technologies, such as OLED and MiniLED. Alongside HDR comes a requirement for wide gamut panels. As mentioned above, while using the sRGB colour gamut will still give a good HDR presentation, it doesn't give the best HDR presentation. HDR truly shines when rendering in wider gamut, pun intended.

Luckily for us, Unreal Engine supports some very solid options for colour management. Chief among them is the aforementioned ACEScg colour space. BT2020 is another good option here with both options providing us a wide gamut. There's a few reasons to choose either-or here, though. ACEScg is the wider gamut, providing us with more colour information throughout our render; however, BT2020 uses the same D65 illuminant white point as both sRGB and D65-P3. I won't get into the complexities of the ACEScg illuminant white point as I frankly don't fully grok them, but for our purposes in Unreal, it is D60. Given that a lot of monitors are calibrated somewhat for D65, the slightly lower white point of D60 for ACEScg does mean it can blue shift slightly on whites.

Regardless of which option you choose, there are a couple of settings in Unreal that need changed when making use of wide gamut internal rendering. By default, a lot of the sRGB settings are carried over to the wider gamut. It should be noted that these settings are predominantly SDR only, HDR doesn't present the same issues in 5.4. Before we cover those though, let's talk about how to actually change between the settings and update all of our texture information.

Location in Project Settings

To start, you can find the setting in Project Settings, Rendering, Working Colour Space. When you change the setting, you will need to restart the editor; otherwise, all shader constants in shaders like the Sky Atmosphere will be incorrect. As a note, shader constants in the material graph will still be incorrect even after the restart. Unreal Engine 5.4 adds in a node to correct from sRGB to the working colour space, but this didn't exist prior to 5.4.

UE Five Four sRGB Correction Node

Unreal engine, by default, assumes the textures are imported in the working colour space. In truth, this is actually an oversimplification, but in practise, is what happens. We need to tell Unreal what colour space our colour textures were authored in. Unless a setting has been changed in your DCC, this is probably sRGB/Rec.709. Modern texturing tools will enable the option to export in wider gamuts, especially ACEScg.

Un-simplified Texture Import

If you're interested in the less simplified version of the import for textures, I'll discuss it here. By default, Unreal does not apply a colour space to an imported texture, and there's a good reason for this: not every texture carries colour information.

Obviously, our diffuse texture carries colour information, but our metallic and roughness textures don't. They just carry data. Data has no colour space, so no transform can be done. For this reason, Unreal assumes that there is no colour spaces associated with the texture and simply brings in the values as it reads them directly from the imported texture. With a diffuse, base colour, or albedo texture, we do have colour data, and we should configure that.

The reason we didn't need to do so previously is that when our rendering space was sRGB, all we had to do is remove the sRGB gamma, the colour space was already our working space. However, we are not rendering in sRGB anymore. So now if we bring in our texture verbatim, with or without sRGB gamma correction, we now have the red, green and blue channels corresponding to the red, green and blue primary of our current, working colour space. This is by proxy of assuming there was no colour space.

If you plan on working with non-sRGB colour spaces, you may wish to stop using the sRGB boolean. This is because Unreal uses a different texture for sRGB flagged textures: "Color" vs "Linear Color" (or "Virtual Color" vs "Virtual Linear Color" when Virtual Texturing is enabled).

You can linearise the textures fairly easily in the texture panel, so it's not too big a deal. Firstly, unselect sRGB, then expand the "Source Color Settings" dropdown, select sRGB as the encoding and sRGB/Rec.709 as the colour space. For most textures, this should work well.

Settings for sRGB source textures
Settings for sRGB source textures

There's a bit more complexity here though. If you grab the mannequin textures and perform the above steps, everything will look correct. If, however, you grab a Megascan texture and do the same, it will be overly dark. The reason is Megascan textures are actually already linear: the use the OpenEXR format. The sRGB flag checks if the file type should be de-gammatised, and OpenEXR should not, so no transform is done. In fact, toggling the sRGB flag does nothing to the texture. The source encoding field isn't so "smart", it will apply whatever transform you ask, correct or not. This is actually useful since it allows image data in any gamma in any container to be displayed correctly.

I'm not 100% sure that Megascans are in the sRGB colourspace, but they oversaturate in ACEScg and BT.2020, so I'm inclined to believe they are. Additionally, sRGB would make sense as it would prevent the textures undersaturating on sRGB working space projects, which is probably most of them.

Settings for Megascans textures
Settings for Megascans textures

Now that all your colour textures have been converted and the engine has been restarted, the bulk of the scene will now render as it looked in sRGB, but we're rendering in a wider gamut. We can now far better support wide gamut displays and HDR, and we support bringing in wide-gamut textures. It is possible in this mode to have minor changes in colour, and this isn't always incorrect with a wider gamut. We are more faithful to ground truth for our lighting, though all these changes should be minor. If they're not, something else has gone wrong. It may be worth double checking textures, and at least once in my case, deleting the DDC cache in Appdata.

So let's stocktake: we're now rendering in a wider gamut, our textures have been converted, and if we're using a more modern version of the engine, our shader constants have also been converted. Well done! We're taking advantage of the wide gamut rendering.

If your output is SDR, you might notice a few issues. These stem from the post processing settings I alluded to earlier. If your panel is sRGB, there is a limit to what we can do here, though. Fundamentally, we can't display more colours than we have to play with. If the panel is a wide-gamut panel, you may need to look into playing with the current panel output with r.HDR.Display.ColorGamut and r.HDR.Display.OutputDevice. Despite the name, these apply to SDR too. In fact, recent UE version seem to be ignoring these settings when the editor is actually running in HDR.

Also, if your monitor is in HDR and is wide-gamut, you may wish to disable the expand gamut option. The expand gamut option attempts to fake a wider gamut for SDR. However, if we have a wider gamut and are using HDR, there is no need to fake it. All it's doing is incorrectly mapping our colours.

There is only one last note I will leave you with: the colour picker, at least in 5.4, is still a little broken. The hex "sRGB" value is interpreted in the working colour space you're currently in, which unfortunately now isn't sRGB. The name is a little unfortunate.

Lastly, if you chose to render in the ACEScg colour space and you're noticing a blue issue in HDR, there is an option to enable blue correction for this colour space. I've not really noticed this issue in practice, and I like the overall presentation ACEScg gives.