跳转至

在 Linear Color Space 中渲染

为了计算更准确,现在都在 Linear 色彩空间 做渲染。自己写引擎时,要相应地做一些处理。

RTV

建议所有中间的临时 RTV 都不带 _SRGB 后缀,最后的 Back Buffer RTV 使用 DXGI_FORMAT_*_SRGB。这样中间所有内容都在 Linear 空间,只在 Present 前进行一次 Gamma 校正。

Texture

像 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::GetColorSpace() == 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

我们平时说的颜色值、Editor 里配置的颜色都是 sRGB 空间的。从外部向 Shader 传入颜色时(例如 Material Constant Buffer),需要将颜色从 sRGB 空间转到 Linear 空间。

ImGui

ImGui 目前所有操作都是在 sRGB 空间进行的,不支持 Linear Color Space。1 考虑到透明混合的问题,不能直接把 ImGui 传入 Shader 的颜色转到 Linear 空间,否则在不同 Color Space 下 ImGui 看上去不一致,尤其是它的 Color Picker。

注意到 ImGui 只有一张 Texture,在不使用 Colorful Glyphs/Emojis 时,这张 Texture 的 RGB 全是 1,这意味着把它当成 Linear 颜色变换到 sRGB 或者反过来结果都一样。我引擎的 Texture 在被 Shader 采样后得到的都是 Linear 颜色,所以不妨把 ImGui 的 Texture 当成 Linear Texture。

修改 ImGui 的 Pixel Shader,把 Texture 的采样结果变换到 sRGB 空间,使得 ImGui 依旧在 sRGB 空间渲染。

struct PS_INPUT
{
  float4 pos : SV_POSITION;
  float4 col : COLOR0;
  float2 uv  : TEXCOORD0;
};
SamplerState sampler0 : register(s0);
Texture2D texture0 : register(t0);

float LinearToSRGB1(float x)
{
    return (x < 0.0031308) ? (12.92 * x) : (1.055 * pow(x, 1.0 / 2.4) - 0.055);
}
float3 LinearToSRGB3(float3 x)
{
    return float3(LinearToSRGB1(x.r), LinearToSRGB1(x.g), LinearToSRGB1(x.b));
}
float4 main(PS_INPUT input) : SV_Target
{
  float4 tex_col = texture0.Sample(sampler0, input.uv);
  float4 out_col = input.col * float4(LinearToSRGB3(tex_col.rgb), tex_col.a);
  return out_col;
}

等 ImGui 渲染完成后,将 sRGB 颜色转到 Linear 空间,同时 Blit 到 Back Buffer(sRGB RTV)。

void GameEditor::DrawImGuiRenderGraph(GfxDevice* device, int32_t renderTargetId)
{
    auto builder = m_ImGuiRenderGraph->AddPass("DrawImGui");

    GfxRenderTextureDesc desc = device->GetBackBuffer()->GetDesc();
    desc.Format = m_ImGuiRtvFormat;

    builder.CreateTransientTexture(renderTargetId, desc);
    builder.SetRenderTargets(renderTargetId);
    builder.ClearRenderTargets(ClearFlags::Color);

    builder.SetRenderFunc([=](RenderGraphContext& context)
    {
        ImGui::Render();
        ImGui_ImplDX12_RenderDrawData(ImGui::GetDrawData(), context.GetD3D12GraphicsCommandList());
    });
}

void GameEditor::BlitImGuiToBackBuffer(GfxDevice* device, int32_t srcTextureId, int32_t backBufferId)
{
    auto builder = m_ImGuiRenderGraph->AddPass("BlitImGuiToBackBuffer");

    builder.ImportTexture(backBufferId, device->GetBackBuffer());
    builder.SetRenderTargets(backBufferId);

    TextureHandle srcTexture = builder.ReadTexture(srcTextureId, ReadFlags::PixelShader);

    builder.SetRenderFunc([=](RenderGraphContext& context)
    {
        m_BlitImGuiMaterial->SetTexture("_SrcTex", srcTexture.Get());
        context.DrawMesh(GetFullScreenTriangleMesh(), m_BlitImGuiMaterial.get());
    });
}
float4 c = _SrcTex.Sample(sampler_SrcTex, input.uv);

#ifdef MARCH_COLORSPACE_GAMMA
    return c;
#else
    return SRGBToLinear(c);
#endif

评论