跳转至

颜色准确性

颜色基础 中提到过,编码图像时会用 OETF 把线性颜色变成非线性颜色。游戏引擎通常使用 sRGB 图像,以 Unity 为例

  • 启用 Gamma color space 时,会直接采样图像,用非线性颜色做渲染。这样得到的结果在物理上是不准确的,但出于一些原因,偶尔还是会这么做。
  • 启用 Linear color space 时,采样图像会自动去 Gamma 校正,输出颜色会自动做 Gamma 校正。这样渲染时,颜色都是在 sRGB 线性空间里的,计算结果是准确的。

Unity 的文档 Differences between linear and gamma color space 给出了两种渲染结果的对比,差异主要体现在光照和透明混合上,而且 Gamma color space 的渲染看上去会暗一点。

为了在 DirectX 12 中得到更准确的渲染结果(Linear color space 渲染),需要一些额外处理。

DXGI_FORMAT

DXGI_FORMAT 中带有 _SRGB 后缀的格式表示其保存的是 sRGB 非线性颜色。在 Shader 中采样这些格式的纹理时,会自动去 Gamma 校正,转为线性颜色。在 Shader 中向这些格式的 RTV 绘制时,会自动对 SV_Target 进行 Gamma 校正(使用准确的分段公式,不是 2.2 那个近似公式),转为非线性颜色。

只有整数格式(UNORM)才有 _SRGB 后缀。事实上只有整数格式才需要用非线性的方式保存颜色。

编码 原理 是否需要 Gamma
UNORM 用 n 位无符号整数表示小数 0.0 - 1.0,整数 \(x\) 表示小数 \(x/(2^n-1)\) 在将 0.0 - 1.0 的颜色值编码为整数前,应该先进行 Gamma 校正,把暗色的范围变大,使得编码后有更多整数表示暗色,更多地保留暗色的变化
FLOAT IEEE-754 或类似表示方法 没必要,可能还会起反作用 1

如果要往整数格式的 Texture 绘制颜色,应该创建 DXGI_FORMAT_*_SRGB 格式的 RTV。

Swap Chain

如果 Back Buffer 是 DXGI_FORMAT_*_UNORM 整数格式(不允许带有 _SRGB 后缀),必须写入经过 Gamma 校正的 sRGB 非线性颜色。可以为它创建 DXGI_FORMAT_*_SRGB 类型的 RTV,自动执行 Gamma 校正。

只有 Back Buffer 可以这样创建 RTV,其他资源只有创建为 DXGI_FORMAT_*_TYPELESS,才能创建与资源不同格式的 RTV。

如果 Back Buffer 是 DXGI_FORMAT_R16G16B16A16_FLOAT 这类浮点数格式,则必须写入线性颜色。

Texture

游戏用到的贴图种类很多,除了颜色贴图,还有 法线贴图 等非颜色贴图。对于颜色贴图,通常应该创建为 DXGI_FORMAT_*_SRGB 格式,采样后自动去 Gamma 校正。对于非颜色贴图,编码的方式一般是自定义的,应该采样后自己解码,不使用 DXGI_FORMAT_*_SRGB 格式。

可以像 Unity 一样提供一个 sRGB 选项,然后使用 DirectXTex 提供的 CREATETEX_FLAGS 进行 sRGB 配置。

// https://github.com/microsoft/DirectXTex/wiki/CreateTexture
// The CREATETEX_SRGB flag provides an option for working around gamma issues with content
// that is in the sRGB or similar color space but is not encoded explicitly as an SRGB format.
// This will force the resource format be one of the of DXGI_FORMAT_*_SRGB formats if it exist.
// Note that no pixel data conversion takes place.
// The CREATETEX_IGNORE_SRGB flag does the opposite;
// it will force the resource format to not have the _*_SRGB version.
CREATETEX_FLAGS createFlags;

if constexpr (GfxSettings::ColorSpace == GfxColorSpace::Linear)
{
    createFlags = m_IsSRGB ? CREATETEX_FORCE_SRGB : CREATETEX_IGNORE_SRGB;
}
else
{
    // shader 中采样时不进行任何转换
    createFlags = CREATETEX_IGNORE_SRGB;
}

GFX_HR(CreateTextureEx(device, m_MetaData, D3D12_RESOURCE_FLAG_NONE, createFlags, &m_Resource));

Color

我们在 Shader 外面用的颜色都是经过 Gamma 校正的非线性 sRGB 颜色。比如 Unity Editor 里配置的颜色、Unity C# 代码中的颜色、平时生活中说的颜色值。我的引擎编辑器 UI 用的是 Dear ImGui,所以 C++ 里的颜色也是非线性的。

向 Shader 传入颜色时(例如 Material Constant Buffer),需要手动对颜色值做去 Gamma 校正。如果是在 Unity 中用 Material.SetColor 这类方法设置颜色的话,Unity 会帮你做去 Gamma 校正。2

参考