Texture is not automatically converted to match the working color space.

If the project’s working color space is changed from sRGB to another color space — for example, AP1 — the textures used by artists in the project will not automatically be converted to the working color space (AP1) for rendering. However, the post-process pipeline will assume that the lighting result is in AP1 and apply ACES tonemapping accordingly, which causes an oversaturation problem.

Normally, artists import sRGB textures by checking the sRGB flag in the texture conversion settings, but the source color settings are not modified. The only way to force a texture to convert to the working color space is to explicitly set the encoding override and color space, in addition to enabling Generate Mipmaps, as noted in Eric’s comment (SHA-1: 973792234be62b6e6dd8b25c6c41f22dc9c6e71a).

Intuitively, one would expect non-linear textures to be automatically transformed to the working color space, but that is not the case here. When the AP1 color space is explicitly set, the texture remains compressed in raw sRGB, with the GPU hardware applying gamma decoding to sRGB space. This behaviour appears to be a bug, as it is not consistent with the assumption that the lighting result is in AP1 color space when the input texture source is still in the linear sRGB gamut.

My expectation is that when the sRGB flag is checked in the texture settings, the texture should automatically be converted to match the working color space. (If the working color space is AP1, then the sRGB to AP1 conversion should be executed.)

SHA-1: 973792234be62b6e6dd8b25c6c41f22dc9c6e71a

* ColorManagement: Utilize OpenColorIO for advanced color space transformations on textures during mip generation.

Thank you for reaching out. The behavior you describe is expected given the current defaults, but we do not plan to extend the sRGB checkbox to drive this conversion. Our reasoning and recommendation follow.

sRGB Checkbox

The sRGB setting governs only the transfer function (encoding) — the linear ↔ sRGB gamma curve. It does not, and has never, affected the color primaries (gamut). These are independent axes:

- Transfer function / encoding — linear vs sRGB gamma; controlled by sRGB and the Encoding Override.

- Primaries / color space — Rec.709 vs AP1 vs Rec.2020; the subject of your request (an sRGB/Rec.709 → working-color-space matrix multiply).

Coupling primaries conversion to the encoding flag would break a legitimate case: authoring genuine wide-gamut content packed into 8-bit channels, where sRGB is ticked purely for precision (better bit distribution in the darks) with no primaries conversion intended. The same 8-bit precision consideration exists elsewhere in the pipeline (e.g. GBufferC), so the checkbox is not a clean lever for gamut handling in any case. For these reasons we consider the two settings should remain separate.

Recommended approach

As you are aware, the conversion is already supported opt-in per texture, under Texture → Advanced → Source Color Settings → Color Space. Setting it explicitly applies the source→ working-color-space conversion at serialization time, and re-serializes automatically if the project working color space changes. With Color Space = None (default), the engine assumes the source is already in the working color space, which is the cause of the oversaturation you observe.

At project scale, this can be applied either manually, via Asset Actions > Bulk Edit via Property Matrix, or python scripting. This is the correct and fully-supported path, and it is independent of the sRGB checkbox.

Future considerations

We recognize per-texture tagging is a chore at scale, and that this convenience would become close to essential if wide working color spaces were in common use. Directions we have discussed but not prioritized:

- Automatic source-color-space inference from naming conventions or texture metadata, removing the need for manual tagging.

- Material-level opt-out (TBD).

For now, we recommend the per-texture Source Color Space setting, applied either manually, via Asset Actions > Bulk Edit via Property Matrix, or python scripting.

Hi Eric,

Thank you for the detailed explanation. It clarifies the distinction between the transfer function and the primaries conversion, and confirms why the sRGB checkbox is not the appropriate mechanism for this conversion.

I have also reviewed the matter further in relation to texture compression and precision, and would like to raise a few additional questions:

When using AP1 with linear texture compression, or alternatively FP16 storage, avoids the precision considerations. Does Unreal Engine currently offer, or have plans to support, any approach for incorporating neural texture compression techniques, such as NVIDIA’s Neural Texture Compression (NTC)? I would appreciate any insight into whether this is being considered as part of the engine’s texture pipeline roadmap.

As an alternative, would it be advisable to store textures in sRGB-compressed format and convert them to AP1 at the point of sampling, on every texture use?

Thank you again for the thorough clarification. I look forward to your response.

Best,

It’s worth separating three things that are easy to conflate, since a couple of the phrasings here mix them together (“AP1 with linear texture compression,” “sRGB-compressed format”). They are independent axes:

Color space (color gamut): The set of colors a texture’s values can represent, defined by the primaries (the specific RGB chromaticities) and white point. sRGB, AP1/ACEScg, and Rec.2020 are different gamuts; changing gamut requires a primaries conversion (a matrix multiply), independent of how the data is stored.

Color encoding: How the color values are represented numerically: the transfer function (e.g. the sRGB gamma curve vs. linear) plus the numeric format and precision (8-bit integer, FP16, etc.). The transfer function is chosen to match non-linear human vision, concentrating limited integer precision where the eye is most sensitive, but it does not change the gamut.

Compression: How the encoded texel data is reduced in size for storage and bandwidth, e.g. block compression (BC1–BC7, ASTC). Unlike gamut and encoding, which act on each color value in isolation, compression works across neighboring pixels (or the entire data stream), exploiting correlations between texels rather than treating any single color independently.

One note on “sRGB” itself, since it’s the root of the confusion: it names both a transfer curve (encoding) and a color space (the Rec.709 primaries), and these are separate settings on the texture. As a result, you can have an sRGB-encoded texture with a wide AP1 color gamut.

As far as neural texture compression goes, I know it’s being evaluated but I couldn’t speak to the current status.

Best,

Eric

Hi Eric,

Thank you for clarifying the concepts around color management.

The objective of our project is to use the working color space (AP1) for light calculations. From what I checked in the code, the engine does not automatically convert textures to the working color space, and instead calculates lighting in sRGB linear space. At the tonemapping stage, the engine assumes the calculation result is already based on the working color space and performs tonemapping accordingly. This means that if I set the working color space to something other than sRGB, it could mislead UE into skipping the proper matrix conversion.

In our case, the source textures from artists are in sRGB, so what’s needed is to convert them to the AP1 gamut before lighting.

Currently, the engine architecture saves textures with an sRGB gamma curve using block compression format. So essentially, there are two possible fixes:

Convert textures to the AP1 gamut before they are saved to file, or

Convert them at the point of use.

This leads to my earlier question: as an alternative, would it be advisable to store textures in sRGB-compressed format and convert them to the AP1 gamut at the point of sampling, on every texture use?

What I want to confirm here is: when sampling a texture from shader code (like sRGBColorToWorkingColorSpace in material expression), is it possible to automatically convert it to the working color space based on texture flag used for AP1 space light calculation?

That is my current understanding — please let me know if there are any misunderstandings.

Best regards,

Eason

You are correct in that the engine assumes it is in working color space, and automatically does the necessary conversions on output. It is also true that this automatic assumption does not apply to texture assets on input. So the user is responsible for managing their texture color space. However, regarding these two options:

  • Convert textures to the AP1 gamut before they are saved to file, or
  • Convert them at the point of use.

There is a third option, which is the current recommended worfklow. Have the engine automatically convert them to your working color space (on import) inside Unreal using the Texture --> Advanced --> Source Color Settings --> Color Space property. Doing so, the texture is now tagged as being in sRGB color space (aka color gamut) explicitely, and will therefore automatically be converted to working color space.

As such if your Engine - Rendering - Working Color Space Project setting is ACES AP1 / ACEScg, the sRGB/Rec709 texture you have now “defined” using the above property will automatically be converted to AP1 before they are sampled. This is done on the asset/DDC.

This also avoids shader instructions if you were to do it at sampling time, and avoids upstream pipeline changes. You can then continue using the hardware (sRGB) encoding and/or block compression, as needed.

Otherwise, we don’t have further convenience in materials at the moment, since we assume this color space transformation is done on the texture asset.

Best,

Eric

Hi Eric,

[There is a third option, which is the current recommended worfklow. Have the engine automatically convert them to your working color space (on import) inside Unreal using the Texture --> Advanced --> Source Color Settings --> Color Space property. Doing so, the texture is now tagged as being in sRGB color space (aka color gamut) explicitely, and will therefore automatically be converted to working color space.]

I would like to ask whether the method I referenced corresponds to the behavior introduced in the following commit:

SHA-1: 973792234be62b6e6dd8b25c6c41f22dc9c6e71a

* ColorManagement: Utilize OpenColorIO for advanced color space transformations on textures during mip generation.

As I understand it, this change is tied to mipmap generation and is executed when a texture is imported. It converts the texture to the working color space format, after which the result is saved either as a engine asset or as a cooked asset derived from the DDC system.

You mentioned that the engine can then continue to use hardware sRGB encoding and/or block compression as needed. I may be misunderstanding the process, so I would like to clarify: does this mean the native asset itself is converted to the working color space, or does the texture remain sRGB and block-compressed, with the conversion to working color space occurring only at the point of use?

If the latter is the case, could you clarify how a texture using descriptor DXGI_FORMAT_BC7_UNORM_SRGB would be loaded via a shader resource view and converted to the working color space, given that no API is provided by the driver for this purpose?

Best,

Eason

Hi Eason,

The referenced commit did not change the behavior or add a sampling-time conversion. It replaced the backend used to perform the same build-time transformations into the working color space.

Let’s take an example. Consider an sRGB/Rec.709 source pixel with the encoded value (1.000, 0.735, 0.000), approximately RGB8 (255, 188, 0):

1. The asset’s editor-only source data retains this original value.

2. During texture building, the sRGB transfer function is decoded, producing linear Rec.709 (1.000, 0.500, 0.000).

3. Assuming the project’s working color space is ACES AP1, LinearizeToWorkingColorSpace() transforms this into linear AP1 (0.782859, 0.528371, 0.075400).

4. If the SRGB checkbox is enabled, these AP1 channel values are sRGB-encoded to approximately (0.8977, 0.7537, 0.3043) before BC7 compression.

This encoded AP1 representation is stored as derived platform data in DDC, subsequently included in the cooked payload, and uploaded to the GPU.

At runtime, DXGI_FORMAT_BC7_UNORM_SRGB performs BC7 decompression and sRGB transfer decoding only. The shader therefore receives approximately (0.782859, 0.528371, 0.075400) in linear AP1, subject to compression error. DXGI does not perform or need to know about the Rec.709-to-AP1 gamut conversion because that already occurred during texture building.

Therefore, the editor-only source data remains unchanged, while the DDC, cooked payload, and GPU texture contain working-gamut values. This is the role of a working color space: unify the internal definition of colors to a common shared definition. The _SRGB suffix describes the (hardware) storage transfer encoding, not the color primaries. As such, no per-sample gamut conversion is required, per example above.

Hope this helps!

Best,

Eric

Hi Eric,

Thank you for the thorough explanation — that makes sense and resolves my question.

I appreciate you taking the time to walk through the example in detail.

This return to convert textures to the AP1 gamut before they are saved to file.

I think of this approach what I worried about is precision. Since the AP1 gamut is wider than sRGB/Rec.709, the transformed values occupy a narrower portion of the 0-1 range prior to encoding. Given that BC7 compression already introduces quantization error, I’m wondering whether this reduced range causes a meaningful loss of precision after compression and decompression — and whether that loss offsets the benefits of working in AP1 in the first place.

Best,

Eason

Hi Eric,

Referring to my previous question

[I have also reviewed the matter further in relation to texture compression and precision, and would like to raise a few additional questions:

When using AP1 with linear texture compression, or alternatively FP16 storage, avoids the precision considerations. Does Unreal Engine currently offer, or have plans to support, any approach for incorporating neural texture compression techniques, such as NVIDIA’s Neural Texture Compression (NTC)? I would appreciate any insight into whether this is being considered as part of the engine’s texture pipeline roadmap.]

I didn’t mention BC7-compressed AP1 here, as I wasn’t confident that it would provide sufficient quality. At the time, I was considering compression from linear FP16 or others instead (for example NTC)."

I discussed the compression precision issue with TA three weeks ago. I believe it is now an appropriate time to revisit this approach and conduct some tests. If you have already run any tests about using AP1 with BC7 and have results available, please share them.

Thank you very much for the help.

Best,

Eason

There are other potential sources of quantization such as the 8-bit base color GBuffer, or the default r.LUT.Size=32 on the output side. I do not have tests available to share but I encourage you to assess. In my experience, noticeable issues only start to arise with gamuts as large as AP0 (which should only be used for archival and master interchange, not working color space).

Best,

Eric