Hi kogitrash!
This is one of the more complex and interesting parts of the Texture Tools Exporter, so this answer is going to be a bit long - apologies in advance!
The short answer is that the Texture Tools Exporter doesn’t currently implement linear/sRGB input/output override flags by design, because
- It turns out that most of the file and DXGI formats we work with have specified color spaces. It would be nice if the exporter could detect and perform correct color management instead of having the user specify linear/sRGB for input and output, so the exporter tries to respect these color spaces wherever possible.
- A flag to specify linear/sRGB output doesn’t map exactly to what the
DXGI_FORMAT
enumeration does in the DDS file format. For instance, DXGI_FORMAT_R8G8B8A8_UNORM
says “this contains 8-bit-per-channel data”, while DXGI_FORMAT_R8G8B8A8_UNORM_SRGB
says “this contains 8-bit-per-channel sRGB data, which will be returned as linear by the GPU when read”. This is different from “this contains sRGB data” or “this contains linear data”, it’s easy to make mistakes when switching between these two. (There’s more to say about this as well with the DX10 extension!)
- We want to avoid cases where a user stores color data in linear format in an 8-bit file, because this results in significant quantization error in darker colors. Alan Wolfe writes about this here: Don’t Convert sRGB U8 to Linear U8! « The blog at the bottom of the sea
- Input files can have color spaces other than linear and sRGB. For instance, DCI-P3 is surprisingly common! Adobe Photoshop also includes a large variety of color spaces, and more can be installed.
The result of these design problems were that the exporter currently respects and writes color profiles wherever possible; if a file doesn’t have a color profile, it tries to match how other image viewers read that file type. From the engine point of view, an engine can decide to remap DXGI formats without _SRGB
to DXGI formats with _SRGB
if it wants to read and write sRGB data as linear. Finally, if it turns out in the future that the exporter handled colors incorrectly and cannot be fixed, it implements a backup mechanism by which engines can identify if a file was created by the Texture Tools Exporter, by checking if dwReserved1[9]
in the DDS_HEADER
structure is equal to the FourCC code for 'NVT3'
, and then adjusting how they import the file appropriately.
The goal of this is that even though the internal implementation is complex, engines should be able to read the exported files more easily, consistently, and flexibly.
Please let us know if you find any bugs in this where a file isn’t displaying as expected, or if you have a use case where the input and output color spaces need to be explicitly set by the user!
Here’s the longer description of how the Texture Tools Exporter handles colors when it reads or writes a file (determining what the Texture Tools Exporter does by experiment may be faster than reading through this):
For sRGB, we use Rec. 709, and our linear color space uses the Rec. 709 primaries with a linearized transfer function.
When reading a file, the standalone and Photoshop plugin versions of the exporter do different things.
If color management was enabled in Photoshop, the plugin reads and converts from the given ICC profile (up to v4). Otherwise, it tries to match how Photoshop displays images without color management - 32-bit images are linear, while 8-bit and 16-bit images are sRGB. (Photoshop also has different color management for RGB and grayscale images.)
The standalone exporter reads and converts from the file’s ICC profile (up to v4) if stored. Otherwise, its behavior depends on the file format (most file formats store 8-bit sRGB, while others store linear; I’m ignoring premultiplied alpha in this message). We try to match how other image readers display them.
When writing a file, we always use sRGB or linear; the idea is that this reduces complexity for engines that have to read files the exporter writes. Color management for non-DDS files works like the above, but in reverse; we specify the color space if we can, and otherwise attempt to write the file so that other image readers can display it correctly.
For DDS files containing color data (normal maps are discussed below):
The exporter can read and write files with and without the DX10 DDS header extension; DX10 is nicer from a color space point of view, but many older engines will crash if given files with it, so we write without the header extension by default. DX10 headers use DXGI formats. We always write to a _UNORM
, _SF16
, or _FLOAT
format (i.e. we never write to a _UNORM_SRGB
format, but it’s totally fine for engines to remap _UNORM
formats to _UNORM_SRGB
formats by incrementing the DXGI enum if they wish). The DDS pixel formats we write other than 8-bit BGR correspond to the DXGI formats listed above. (8-bit BGR is the only format as of this writing that never uses the DX10 extension, since there is no corresponding DXGI format.)
When writing DDS files, we try to match how DirectXTex handles color spaces. The general rule of thumb is that half- and single-precision floating-point formats use linear, while other formats use sRGB. More specifically, (LDR) ASTC, BC1-5, BC7, A8, R8, BGR8, BGRA8, and BGRX use sRGB, while BC6, R16, RG16, RGBA16, R32, RG32, and RGBA32 use linear.
However, normal maps don’t really have a color space. When saving a file that we know is a normal map (i.e. the user took a color image and converted it to a normal map inside the exporter), we ignore color spaces, and treat the DDS file as containing only numerical data. (That is, a value of 0.5 in a normal map gets saved as the quantized version of 0.5 regardless of the format, while an sRGB value of 127/255 would not be saved as 127/255 in a format that uses linear colors, e.g. BC6.)
In most cases, this doesn’t matter - normal maps are usually saved to sRGB formats, so they look normal in an image editor; this is only noticeable when saving a normal map to a linear format such as BC6 or RG32. (The idea is that engines can read data directly from normal maps without needing to perform sRGB → linear conversion implicitly or explicitly).
Hope this helps! As mentioned above, please let us know if you find any bugs in this where a file isn’t displaying as expected, or if you have a use case where the input and output color spaces need to be explicitly set by the user.