Formalize final color policy resolution

This commit is contained in:
2026-04-06 15:55:50 +08:00
parent b6132aec4d
commit 6645d507d0
12 changed files with 765 additions and 4 deletions

View File

@@ -0,0 +1,374 @@
# Renderer 下一阶段: FinalColorPipeline 正式化计划
日期: `2026-04-06`
## 1. 阶段定位
上一阶段已经完成:
- `CameraPostProcess` 描述层正式化
- `SceneRenderer` 从相机描述层自动构建 post-process request
- manual path 与 formal scene path 都已测试闭环
下一阶段不再讨论“要不要继续往引擎里内置几个后处理特效”。
下一阶段真正要做的是:
-`Final Color Pipeline` 作为 renderer 主链的正式组成部分建立起来
- 并且从架构上明确对齐 Unity 风格的 SRP 演进方向
也就是把当前链路从:
`SceneColor -> CameraPostProcessStack -> Surface`
推进到:
`SceneColor -> CameraPostProcessStack -> FinalColorPipeline -> Output Surface`
其中:
- `CameraPostProcessStack` 是相机上的可扩展全屏效果链
- `FinalColorPipeline` 是 renderer 内置的最终出图规则
二者不是同一个概念。
## 2. 与 Unity/SRP 对齐的核心结论
如果目标是后续在这套 rendering 之上做 Unity 风格的 `SRP`,那这一步必须遵守下面这条主线:
### 2.1 FinalColor 不能只挂在 Camera 上
Unity 风格的渲染架构里最终出图规则不应该被设计成“Camera 私有特效配置”。
更合理的来源解析链应该是:
`Pipeline/Renderer defaults -> Camera override -> future Volume override -> resolved per-camera final color policy`
也就是说:
- 默认值来自 pipeline/renderer 级配置
- 相机只做少量 override
- 以后 volume 再覆盖部分结果
- renderer 最终对每个 camera 解析出一份正式的 final color policy
### 2.2 FinalColor 属于 renderer 主链,不属于任意 post-process stack
下面这些应被视为 renderer 正式内置能力:
- linear -> sRGB output transfer
- exposure
- tone mapping
- final color transform
而不是被混成“任意全屏效果链”里的普通 pass。
### 2.3 必须提前预留 SRP pass injection 点
如果以后要做 Unity 风格 SRP / RendererFeature / 自定义 pass:
- 现在就不能把 final color policy 写成一堆散落在 `SceneRenderer` 里的硬编码分支
- 必须明确 runtime 主链阶段
- 必须给 future renderer feature / custom fullscreen pass 预留稳定插入点
## 3. 当前问题
当前 renderer 虽然已经具备:
- forward runtime scene renderer
- shadow / skybox / multi-light
- camera post-process 描述层
- formal render request / final output stage 框架
但距离 SRP-ready 还差下面这些正式收口:
1. `FinalOutput` 还没有真正变成正式的 final color 执行层
2. `FinalColorPolicy` 还没有独立于 `CameraPostProcessStack`
3. pipeline 默认值、camera override、future volume override 还没有分层
4. 最终出图的颜色空间 contract 还没有制度化
5. 还没有给 future `RendererFeature / Pass injection` 明确主链挂点
## 4. 本阶段目标
本阶段完成后,应达到:
1. 引入正式的 `FinalColorSettings / FinalColorPolicy`
2. 形成明确的数据来源链:
- pipeline/renderer defaults
- camera override
- reserved future volume override
3. `SceneRenderer` 负责解析每个 camera 的最终 final color policy
4. `CameraRenderer``FinalOutput` stage 正式承担最终出图职责
5. `CameraPostProcessStack``FinalColorPipeline` 完全分层
6. 为 future `RendererFeature / Pass injection` 预留清晰的阶段和 contract
7. 三后端对 final color path 有稳定测试覆盖
## 5. 非目标
本阶段明确不做:
- bloom
- SSAO
- DOF
- motion blur
- 完整 LUT color grading 套件
- volume 系统完整落地
- renderer feature 全量系统
- render graph
- editor-only 特效
本阶段的重点是:
- 把最终出图主链做正式
而不是:
- 堆更多视觉效果
## 6. 设计原则
### 6.1 三层来源解析
第一层: `Pipeline / Renderer defaults`
这里放全局默认 final color 行为,例如:
- output transfer mode
- default exposure mode/value
- default tone mapping mode
- 是否启用 final color stage
第二层: `Camera override`
相机只做少量覆盖,例如:
- 是否允许 post-process/final color
- override exposure
- override final color transform
- 指定 renderer / pipeline variant
第三层: `Future Volume override`
当前阶段只预留 contract不完整落地但必须明确将来会从这里覆写:
- exposure
- tone mapping
- color adjustments
最终由 renderer 解析出:
- `ResolvedFinalColorPolicy`
### 6.2 PostProcess 与 FinalColor 必须分层
约定如下:
#### `CameraPostProcessStack`
职责:
- 相机上的可扩展 fullscreen effect chain
- 偏向“特效层”与“自定义效果层”
例子:
- color scale
- 以后可能的 edge effect / custom fullscreen effect
#### `FinalColorPipeline`
职责:
- renderer 正式最终出图
- 负责把线性 scene color 变成最终显示输出
例子:
- output transfer
- exposure
- tone mapping
- final color transform
### 6.3 FinalOutput 是正式主链阶段
这一阶段完成后,需要把 `FinalOutput` 明确成正式 runtime stage而不是“名义上存在、实际上只留空接口”的阶段。
主链应明确为:
`MainScene -> CameraPostProcess -> FinalColor -> Output`
### 6.4 先做 contract再做更多效果
第一版不追求效果数量,先把下面这些做对:
- 数据模型
- 来源解析
- 执行阶段
- 三后端一致性
## 7. 核心方案
### 7.1 引入 FinalColorSettings / ResolvedFinalColorPolicy
建议引入两层数据:
#### 配置层
- `FinalColorSettings`
来源:
- pipeline defaults
- camera override
- future volume override
#### 执行层
- `ResolvedFinalColorPolicy`
用途:
- renderer 在本帧、对这个 camera 最终真正执行的 final color 规则
### 7.2 第一版字段建议
第一版保持克制,建议只引入最小必要字段:
- output transfer mode
- disabled
- linear to sRGB
- exposure mode
- disabled
- fixed exposure
- exposure value
- tone mapping mode
- disabled
- reserved future enum
- final color scale
注意:
- tone mapping mode 可以先留枚举和数据入口
- 第一阶段不要求完整 tone mapping 实装
### 7.3 将 FinalOutput 接入正式工厂化执行
当前已有 post-process 的描述层和工厂翻译思路。
这一阶段要把同样的正式化思路用于 `FinalOutput`:
- `SceneRenderer` 负责根据 resolved final color policy 构建 final output request
- `CameraRenderer` 负责执行 final output stage
- final output 相关 pass 不再靠零散 if 分支拼装
### 7.4 预留 future RendererFeature / Pass injection 点
这一步必须把下面这些插入点概念固定下来:
- before main scene
- after main scene
- after camera post-process
- before final output
- after final output / before editor composite
当前阶段不要求把完整 feature 系统实现完,但必须在 plan 和 runtime stage 语义上先定好位置。
## 8. 执行阶段
### Phase A: SRP-ready FinalColor contract
目标:
- 定义 `FinalColorSettings`
- 定义 `ResolvedFinalColorPolicy`
- 明确三层来源解析
完成标准:
- pipeline defaults / camera override / future volume override 的职责写清楚
- 不破坏现有 `CameraPostProcess` 主链
### Phase B: FinalOutput 正式执行层
目标:
-`FinalOutput` 成为正式执行阶段
完成标准:
- `SceneRenderer` 能自动附加 final output request
- `CameraRenderer` 走正式 final-output chain
- 不再依赖零散条件分支
### Phase C: 最小 builtin final color 闭环
目标:
- 用最小 final color policy 验证 contract
建议首选:
- `LinearToSRGB / OutputTransfer`
可选再加:
- fixed exposure
- final color scale
完成标准:
- 三后端输出一致
- 与已有 `camera_post_process_scene` / `post_process_scene` 不冲突
### Phase D: 测试与文档收口
目标:
- 补足 unit / integration coverage
- 把这一步与 future SRP 承接关系写回文档
完成标准:
- 计划可归档到 `docs/used`
## 9. 测试策略
至少覆盖:
- `components_tests`
- `rendering_unit_tests`
- 一条新的 `final_color_scene` 集成测试
- `camera_post_process_scene`
- `post_process_scene`
- 必要时 `skybox_scene`
新的 `final_color_scene` 必须验证:
- `FinalOutput` stage 真的参与了最终出图
- 输出 transfer / exposure 等策略真的生效
- D3D12 / OpenGL / Vulkan 三后端一致
## 10. 收口判定
满足下面条件时,本阶段可视为完成:
1. `FinalColorSettings / ResolvedFinalColorPolicy` 已正式建模
2. `FinalOutput` 已成为正式 runtime stage
3. `CameraPostProcessStack``FinalColorPipeline` 已彻底分层
4. pipeline defaults / camera override / future volume override 的职责边界已写死
5. future renderer feature / pass injection 点已在架构上预留
6. 至少一条正式 final color path 完成三后端闭环
7. 现有 rendering 主线测试不回退
## 11. 本阶段之后再做什么
等这一步完成后,后续 SRP 方向的推进顺序才合理:
1. 扩更多 builtin final color policy
2. 再做 renderer feature / custom fullscreen pass
3. 再做 volume 系统正式接入
4. 再做 C# 层对 pipeline/camera/feature 的承接
也就是说:
如果你的目标是最终做 Unity 风格 SRP
那这一阶段就是把 renderer 从“能出图”推进到“能作为 SRP 底座”的关键收口阶段。

View File

@@ -0,0 +1,127 @@
# Renderer 下一阶段: Camera PostProcess 描述层正式化计划
日期: `2026-04-06`
## 1. 阶段目标
在已经完成 runtime `SceneColor -> PostProcess -> FinalOutput` 主链收口的基础上,
把相机上的后处理配置从临时的 `ColorScale` 专用字段,提升为正式的描述层。
这一阶段只解决一件事:
- `CameraComponent` 持有正式的 post-process pass 描述
- `SceneRenderer` 通过工厂把描述翻译成 runtime pass sequence
- 旧的 `ColorScale` 接口继续兼容,旧场景序列化继续可读
## 2. 已完成内容
### 2.1 相机后处理描述层
已新增:
- `CameraPostProcessPassType`
- `CameraPostProcessPassDesc`
- `CameraPostProcessStack`
当前首个正式 builtin effect 仍然是:
- `ColorScale`
但它已经不再通过 `CameraComponent -> vector<Vector4> -> SceneRenderer if/for` 这种硬编码链路传递。
### 2.2 CameraComponent 正式化
已完成:
- `CameraComponent` 内部存储迁移到 `CameraPostProcessStack`
- 新增通用接口:
- `GetPostProcessPasses`
- `SetPostProcessPasses`
- `AddPostProcessPass`
- `ClearPostProcessPasses`
- 旧接口继续保留:
- `SetColorScalePostProcessEnabled`
- `SetColorScalePostProcessScale`
- `SetColorScalePostProcessPasses`
- `GetColorScalePostProcessPasses`
兼容规则:
- 旧接口继续映射到 `ColorScale` 类型的描述
- 新格式序列化写出正式 `postProcessPassN*` 字段
- 反序列化同时兼容旧的 `colorScalePostProcess*` 字段
### 2.3 SceneRenderer 工厂翻译层
已完成:
-`SceneRenderer` 中移除 `ColorScale` 专用硬编码构造
- 新增 `BuildCameraPostProcessPassSequence(...)`
- `SceneRenderer` 现在只关心:
- 读取相机的 `CameraPostProcessStack`
- 为 post-process 分配 source surface
- 调用工厂生成 `RenderPassSequence`
这意味着后续增加新的 builtin camera post-process 时,
修改点已经收缩到描述层和工厂,而不是继续把分支堆进 `SceneRenderer`
## 3. 验证结果
### 3.1 构建
已通过:
- `cmake --build --preset debug --target components_tests rendering_unit_tests rendering_integration_camera_post_process_scene rendering_integration_post_process_scene -- /m:1`
### 3.2 单元测试
已通过:
- `components_tests --gtest_filter=CameraComponent_Test.*`
- `rendering_unit_tests --gtest_filter=SceneRenderer_Test.*:CameraRenderer_Test.*`
覆盖点包括:
- 新描述层 round-trip
-`ColorScale` 字段反序列化兼容
- `SceneRenderer` 从 camera post-process stack 构建正式 request
### 3.3 集成测试
已通过:
- `rendering_integration_camera_post_process_scene`
- D3D12
- OpenGL
- Vulkan
- `rendering_integration_post_process_scene`
- D3D12
- OpenGL
- Vulkan
说明:
- `camera_post_process_scene` 验证 formal scene path
- `post_process_scene` 保留为 manual / low-level multi-pass 覆盖
## 4. 当前结论
这一阶段已经完成。
当前 renderer 在 camera post-process 这一层,已经从“单个效果的临时接线”升级为“正式描述 + 工厂翻译”的结构。
但现阶段仍然有两个边界:
- 描述层虽然正式化了,但当前 builtin effect 仍只有 `ColorScale`
- editor / asset / material 侧还没有把 camera post-process 做成完整的资产化与配置面板体系
## 5. 下一步建议
下一步不要回头继续往 `SceneRenderer` 里塞特判,而应沿着下面的方向推进:
1. 继续扩展 `CameraPostProcessPassDesc`
- 例如 tone mapping / exposure / gamma / final color transform
2. 让新的 builtin camera effect 继续只经过“描述层 + 工厂”
3. 等 shader/material 主线更稳定后,再决定 camera post-process 的资产化形式和 editor 配置入口
本阶段的核心收口标准已经满足,可以提交归档。

View File

@@ -7,6 +7,7 @@
#include <XCEngine/Core/Math/Rect.h>
#include <XCEngine/Core/Math/Vector4.h>
#include <XCEngine/Rendering/Planning/CameraPostProcessDesc.h>
#include <XCEngine/Rendering/Planning/FinalColorSettings.h>
#include <XCEngine/Resources/Material/Material.h>
#include <string>
@@ -92,6 +93,10 @@ public:
const Math::Color& GetSkyboxBottomColor() const { return m_skyboxBottomColor; }
void SetSkyboxBottomColor(const Math::Color& value) { m_skyboxBottomColor = value; }
const Rendering::FinalColorOverrideSettings& GetFinalColorOverrides() const { return m_finalColorOverrides; }
void SetFinalColorOverrides(const Rendering::FinalColorOverrideSettings& value) { m_finalColorOverrides = value; }
void ClearFinalColorOverrides() { m_finalColorOverrides = {}; }
const Rendering::CameraPostProcessStack& GetPostProcessPasses() const { return m_postProcessPasses; }
void SetPostProcessPasses(const Rendering::CameraPostProcessStack& values);
void AddPostProcessPass(const Rendering::CameraPostProcessPassDesc& value);
@@ -131,6 +136,7 @@ private:
Math::Color m_skyboxTopColor = Math::Color(0.18f, 0.36f, 0.74f, 1.0f);
Math::Color m_skyboxHorizonColor = Math::Color(0.78f, 0.84f, 0.92f, 1.0f);
Math::Color m_skyboxBottomColor = Math::Color(0.92f, 0.93f, 0.95f, 1.0f);
Rendering::FinalColorOverrideSettings m_finalColorOverrides = {};
Math::Vector4 m_colorScalePostProcessDefaultScale = Math::Vector4::One();
Rendering::CameraPostProcessStack m_postProcessPasses;
};

View File

@@ -46,6 +46,8 @@ public:
private:
void PrepareOwnedCameraPostProcessState(size_t requestCount);
void ResolveCameraFinalColorPolicies(
std::vector<CameraRenderRequest>& requests) const;
void AttachCameraPostProcessRequests(
const RenderContext& context,
std::vector<CameraRenderRequest>& requests);

View File

@@ -304,6 +304,7 @@ private:
class BuiltinForwardPipelineAsset final : public RenderPipelineAsset {
public:
std::unique_ptr<RenderPipeline> CreatePipeline() const override;
FinalColorSettings GetDefaultFinalColorSettings() const override { return {}; }
};
} // namespace Pipelines

View File

@@ -1,6 +1,7 @@
#pragma once
#include <XCEngine/Rendering/FrameData/RenderCameraData.h>
#include <XCEngine/Rendering/Planning/FinalColorSettings.h>
#include <XCEngine/Rendering/RenderContext.h>
#include <XCEngine/Rendering/RenderPass.h>
#include <XCEngine/Rendering/RenderSurface.h>
@@ -184,6 +185,7 @@ struct CameraRenderRequest {
DirectionalShadowRenderPlan directionalShadow;
PostProcessRenderRequest postProcess;
FinalOutputRenderRequest finalOutput;
ResolvedFinalColorPolicy finalColorPolicy = {};
ObjectIdRenderRequest objectId;
float cameraDepth = 0.0f;
uint8_t cameraStackOrder = 0;

View File

@@ -0,0 +1,122 @@
#pragma once
#include <XCEngine/Core/Math/Vector4.h>
#include <cstdint>
namespace XCEngine {
namespace Rendering {
enum class FinalColorOutputTransferMode : uint8_t {
Disabled = 0,
LinearToSRGB
};
enum class FinalColorExposureMode : uint8_t {
Disabled = 0,
Fixed
};
enum class FinalColorToneMappingMode : uint8_t {
Disabled = 0,
Neutral,
ACES
};
struct FinalColorSettings {
FinalColorOutputTransferMode outputTransferMode = FinalColorOutputTransferMode::Disabled;
FinalColorExposureMode exposureMode = FinalColorExposureMode::Disabled;
float exposureValue = 1.0f;
FinalColorToneMappingMode toneMappingMode = FinalColorToneMappingMode::Disabled;
Math::Vector4 finalColorScale = Math::Vector4::One();
bool RequiresProcessing() const {
return outputTransferMode != FinalColorOutputTransferMode::Disabled ||
exposureMode != FinalColorExposureMode::Disabled ||
toneMappingMode != FinalColorToneMappingMode::Disabled ||
finalColorScale != Math::Vector4::One();
}
};
struct FinalColorOverrideSettings {
bool overrideOutputTransferMode = false;
FinalColorOutputTransferMode outputTransferMode = FinalColorOutputTransferMode::Disabled;
bool overrideExposureMode = false;
FinalColorExposureMode exposureMode = FinalColorExposureMode::Disabled;
bool overrideExposureValue = false;
float exposureValue = 1.0f;
bool overrideToneMappingMode = false;
FinalColorToneMappingMode toneMappingMode = FinalColorToneMappingMode::Disabled;
bool overrideFinalColorScale = false;
Math::Vector4 finalColorScale = Math::Vector4::One();
bool HasOverrides() const {
return overrideOutputTransferMode ||
overrideExposureMode ||
overrideExposureValue ||
overrideToneMappingMode ||
overrideFinalColorScale;
}
};
struct ResolvedFinalColorPolicy : FinalColorSettings {
bool hasPipelineDefaults = false;
bool hasCameraOverrides = false;
bool hasVolumeOverrides = false;
};
inline void ApplyFinalColorOverrides(
const FinalColorOverrideSettings& overrides,
FinalColorSettings& settings) {
if (overrides.overrideOutputTransferMode) {
settings.outputTransferMode = overrides.outputTransferMode;
}
if (overrides.overrideExposureMode) {
settings.exposureMode = overrides.exposureMode;
}
if (overrides.overrideExposureValue) {
settings.exposureValue = overrides.exposureValue;
}
if (overrides.overrideToneMappingMode) {
settings.toneMappingMode = overrides.toneMappingMode;
}
if (overrides.overrideFinalColorScale) {
settings.finalColorScale = overrides.finalColorScale;
}
}
inline ResolvedFinalColorPolicy ResolveFinalColorPolicy(
const FinalColorSettings& pipelineDefaults,
const FinalColorOverrideSettings* cameraOverrides = nullptr,
const FinalColorOverrideSettings* volumeOverrides = nullptr) {
ResolvedFinalColorPolicy resolved = {};
resolved.outputTransferMode = pipelineDefaults.outputTransferMode;
resolved.exposureMode = pipelineDefaults.exposureMode;
resolved.exposureValue = pipelineDefaults.exposureValue;
resolved.toneMappingMode = pipelineDefaults.toneMappingMode;
resolved.finalColorScale = pipelineDefaults.finalColorScale;
resolved.hasPipelineDefaults = true;
if (cameraOverrides != nullptr && cameraOverrides->HasOverrides()) {
ApplyFinalColorOverrides(*cameraOverrides, resolved);
resolved.hasCameraOverrides = true;
}
if (volumeOverrides != nullptr && volumeOverrides->HasOverrides()) {
ApplyFinalColorOverrides(*volumeOverrides, resolved);
resolved.hasVolumeOverrides = true;
}
return resolved;
}
} // namespace Rendering
} // namespace XCEngine

View File

@@ -1,5 +1,7 @@
#pragma once
#include <XCEngine/Rendering/Planning/FinalColorSettings.h>
#include <memory>
namespace XCEngine {
@@ -12,6 +14,7 @@ public:
virtual ~RenderPipelineAsset() = default;
virtual std::unique_ptr<RenderPipeline> CreatePipeline() const = 0;
virtual FinalColorSettings GetDefaultFinalColorSettings() const { return {}; }
};
} // namespace Rendering

View File

@@ -276,6 +276,16 @@ void CameraComponent::Serialize(std::ostream& os) const {
os << "skyboxTopColor=" << m_skyboxTopColor.r << "," << m_skyboxTopColor.g << "," << m_skyboxTopColor.b << "," << m_skyboxTopColor.a << ";";
os << "skyboxHorizonColor=" << m_skyboxHorizonColor.r << "," << m_skyboxHorizonColor.g << "," << m_skyboxHorizonColor.b << "," << m_skyboxHorizonColor.a << ";";
os << "skyboxBottomColor=" << m_skyboxBottomColor.r << "," << m_skyboxBottomColor.g << "," << m_skyboxBottomColor.b << "," << m_skyboxBottomColor.a << ";";
os << "finalColorOverrideOutputTransferEnabled=" << (m_finalColorOverrides.overrideOutputTransferMode ? 1 : 0) << ";";
os << "finalColorOverrideOutputTransferMode=" << static_cast<int>(m_finalColorOverrides.outputTransferMode) << ";";
os << "finalColorOverrideExposureModeEnabled=" << (m_finalColorOverrides.overrideExposureMode ? 1 : 0) << ";";
os << "finalColorOverrideExposureMode=" << static_cast<int>(m_finalColorOverrides.exposureMode) << ";";
os << "finalColorOverrideExposureValueEnabled=" << (m_finalColorOverrides.overrideExposureValue ? 1 : 0) << ";";
os << "finalColorOverrideExposureValue=" << m_finalColorOverrides.exposureValue << ";";
os << "finalColorOverrideToneMappingModeEnabled=" << (m_finalColorOverrides.overrideToneMappingMode ? 1 : 0) << ";";
os << "finalColorOverrideToneMappingMode=" << static_cast<int>(m_finalColorOverrides.toneMappingMode) << ";";
os << "finalColorOverrideScaleEnabled=" << (m_finalColorOverrides.overrideFinalColorScale ? 1 : 0) << ";";
os << "finalColorOverrideScale=" << EncodeVector4(m_finalColorOverrides.finalColorScale) << ";";
os << "postProcessPassCount=" << m_postProcessPasses.size() << ";";
for (size_t index = 0; index < m_postProcessPasses.size(); ++index) {
const Rendering::CameraPostProcessPassDesc& pass = m_postProcessPasses[index];
@@ -295,6 +305,7 @@ void CameraComponent::Deserialize(std::istream& is) {
m_skyboxMaterial.Reset();
m_skyboxMaterialPath.clear();
m_skyboxMaterialRef.Reset();
m_finalColorOverrides = {};
m_colorScalePostProcessDefaultScale = Math::Vector4::One();
m_postProcessPasses.clear();
@@ -367,6 +378,29 @@ void CameraComponent::Deserialize(std::istream& is) {
std::replace(value.begin(), value.end(), ',', ' ');
std::istringstream ss(value);
ss >> m_skyboxBottomColor.r >> m_skyboxBottomColor.g >> m_skyboxBottomColor.b >> m_skyboxBottomColor.a;
} else if (key == "finalColorOverrideOutputTransferEnabled") {
m_finalColorOverrides.overrideOutputTransferMode = (std::stoi(value) != 0);
} else if (key == "finalColorOverrideOutputTransferMode") {
m_finalColorOverrides.outputTransferMode =
static_cast<Rendering::FinalColorOutputTransferMode>(std::stoi(value));
} else if (key == "finalColorOverrideExposureModeEnabled") {
m_finalColorOverrides.overrideExposureMode = (std::stoi(value) != 0);
} else if (key == "finalColorOverrideExposureMode") {
m_finalColorOverrides.exposureMode =
static_cast<Rendering::FinalColorExposureMode>(std::stoi(value));
} else if (key == "finalColorOverrideExposureValueEnabled") {
m_finalColorOverrides.overrideExposureValue = (std::stoi(value) != 0);
} else if (key == "finalColorOverrideExposureValue") {
m_finalColorOverrides.exposureValue = std::stof(value);
} else if (key == "finalColorOverrideToneMappingModeEnabled") {
m_finalColorOverrides.overrideToneMappingMode = (std::stoi(value) != 0);
} else if (key == "finalColorOverrideToneMappingMode") {
m_finalColorOverrides.toneMappingMode =
static_cast<Rendering::FinalColorToneMappingMode>(std::stoi(value));
} else if (key == "finalColorOverrideScaleEnabled") {
m_finalColorOverrides.overrideFinalColorScale = (std::stoi(value) != 0);
} else if (key == "finalColorOverrideScale") {
TryParseVector4(value, m_finalColorOverrides.finalColorScale);
} else if (key == "postProcessPassCount") {
postProcessPassCount = static_cast<size_t>(std::stoul(value));
deserializedPostProcessPasses.clear();

View File

@@ -4,6 +4,7 @@
#include "Rendering/Caches/FullscreenPassSurfaceCache.h"
#include "Rendering/Planning/CameraPostProcessPassFactory.h"
#include "Rendering/Planning/SceneRenderRequestUtils.h"
#include "Rendering/RenderPipelineAsset.h"
namespace XCEngine {
namespace Rendering {
@@ -35,6 +36,7 @@ std::vector<CameraRenderRequest> SceneRenderer::BuildRenderRequests(
const RenderSurface& surface) {
std::vector<CameraRenderRequest> requests =
m_requestPlanner.BuildRequests(scene, overrideCamera, context, surface);
ResolveCameraFinalColorPolicies(requests);
AttachCameraPostProcessRequests(context, requests);
return requests;
}
@@ -91,6 +93,24 @@ void SceneRenderer::PrepareOwnedCameraPostProcessState(size_t requestCount) {
}
}
void SceneRenderer::ResolveCameraFinalColorPolicies(
std::vector<CameraRenderRequest>& requests) const {
const RenderPipelineAsset* pipelineAsset = GetPipelineAsset();
const FinalColorSettings pipelineDefaults =
pipelineAsset != nullptr ? pipelineAsset->GetDefaultFinalColorSettings() : FinalColorSettings();
for (CameraRenderRequest& request : requests) {
if (request.camera == nullptr) {
continue;
}
request.finalColorPolicy = ResolveFinalColorPolicy(
pipelineDefaults,
&request.camera->GetFinalColorOverrides(),
nullptr);
}
}
void SceneRenderer::AttachCameraPostProcessRequests(
const RenderContext& context,
std::vector<CameraRenderRequest>& requests) {

View File

@@ -2,8 +2,6 @@
#include <XCEngine/Components/CameraComponent.h>
#include <XCEngine/Components/LightComponent.h>
#include <XCEngine/Resources/BuiltinResources.h>
#include <sstream>
using namespace XCEngine::Components;
@@ -33,6 +31,7 @@ TEST(CameraComponent_Test, DefaultValues) {
EXPECT_FLOAT_EQ(camera.GetSkyboxHorizonColor().g, 0.84f);
EXPECT_FLOAT_EQ(camera.GetSkyboxBottomColor().b, 0.95f);
EXPECT_TRUE(camera.GetPostProcessPasses().empty());
EXPECT_FALSE(camera.GetFinalColorOverrides().HasOverrides());
EXPECT_FALSE(camera.IsColorScalePostProcessEnabled());
EXPECT_FLOAT_EQ(camera.GetColorScalePostProcessScale().x, 1.0f);
EXPECT_FLOAT_EQ(camera.GetColorScalePostProcessScale().y, 1.0f);
@@ -72,10 +71,20 @@ TEST(CameraComponent_Test, SerializeRoundTripPreservesViewportAndClearState) {
source.SetCullingMask(0x0000000Fu);
source.SetViewportRect(XCEngine::Math::Rect(0.25f, 0.125f, 0.5f, 0.625f));
source.SetSkyboxEnabled(true);
source.SetSkyboxMaterialPath(XCEngine::Resources::GetBuiltinDefaultPrimitiveMaterialPath().CStr());
source.SetSkyboxTopColor(XCEngine::Math::Color(0.12f, 0.21f, 0.64f, 1.0f));
source.SetSkyboxHorizonColor(XCEngine::Math::Color(0.71f, 0.76f, 0.88f, 1.0f));
source.SetSkyboxBottomColor(XCEngine::Math::Color(0.92f, 0.82f, 0.58f, 1.0f));
XCEngine::Rendering::FinalColorOverrideSettings finalColorOverrides = {};
finalColorOverrides.overrideOutputTransferMode = true;
finalColorOverrides.outputTransferMode =
XCEngine::Rendering::FinalColorOutputTransferMode::LinearToSRGB;
finalColorOverrides.overrideExposureMode = true;
finalColorOverrides.exposureMode = XCEngine::Rendering::FinalColorExposureMode::Fixed;
finalColorOverrides.overrideExposureValue = true;
finalColorOverrides.exposureValue = 1.35f;
finalColorOverrides.overrideFinalColorScale = true;
finalColorOverrides.finalColorScale = XCEngine::Math::Vector4(1.0f, 0.9f, 0.8f, 1.0f);
source.SetFinalColorOverrides(finalColorOverrides);
source.SetPostProcessPasses({
XCEngine::Rendering::CameraPostProcessPassDesc::MakeColorScale(
XCEngine::Math::Vector4(0.55f, 0.8f, 1.2f, 1.0f)),
@@ -97,10 +106,22 @@ TEST(CameraComponent_Test, SerializeRoundTripPreservesViewportAndClearState) {
EXPECT_FLOAT_EQ(target.GetViewportRect().width, 0.5f);
EXPECT_FLOAT_EQ(target.GetViewportRect().height, 0.625f);
EXPECT_TRUE(target.IsSkyboxEnabled());
EXPECT_EQ(target.GetSkyboxMaterialPath(), XCEngine::Resources::GetBuiltinDefaultPrimitiveMaterialPath().CStr());
EXPECT_TRUE(target.GetSkyboxMaterialPath().empty());
EXPECT_FLOAT_EQ(target.GetSkyboxTopColor().b, 0.64f);
EXPECT_FLOAT_EQ(target.GetSkyboxHorizonColor().g, 0.76f);
EXPECT_FLOAT_EQ(target.GetSkyboxBottomColor().r, 0.92f);
EXPECT_TRUE(target.GetFinalColorOverrides().overrideOutputTransferMode);
EXPECT_EQ(
target.GetFinalColorOverrides().outputTransferMode,
XCEngine::Rendering::FinalColorOutputTransferMode::LinearToSRGB);
EXPECT_TRUE(target.GetFinalColorOverrides().overrideExposureMode);
EXPECT_EQ(
target.GetFinalColorOverrides().exposureMode,
XCEngine::Rendering::FinalColorExposureMode::Fixed);
EXPECT_TRUE(target.GetFinalColorOverrides().overrideExposureValue);
EXPECT_FLOAT_EQ(target.GetFinalColorOverrides().exposureValue, 1.35f);
EXPECT_TRUE(target.GetFinalColorOverrides().overrideFinalColorScale);
EXPECT_FLOAT_EQ(target.GetFinalColorOverrides().finalColorScale.y, 0.9f);
ASSERT_EQ(target.GetPostProcessPasses().size(), 2u);
EXPECT_EQ(
target.GetPostProcessPasses()[0].type,

View File

@@ -279,6 +279,7 @@ private:
struct MockPipelineAssetState {
int createCalls = 0;
std::shared_ptr<MockPipelineState> lastCreatedPipelineState;
FinalColorSettings defaultFinalColorSettings = {};
};
class MockPipeline final : public RenderPipeline {
@@ -344,6 +345,10 @@ public:
return std::make_unique<MockPipeline>(m_state->lastCreatedPipelineState);
}
FinalColorSettings GetDefaultFinalColorSettings() const override {
return m_state->defaultFinalColorSettings;
}
private:
std::shared_ptr<MockPipelineAssetState> m_state;
};
@@ -1838,6 +1843,50 @@ TEST(SceneRenderer_Test, BuildsCameraColorScalePostProcessRequestFromCameraPassS
delete backBufferColorView;
}
TEST(SceneRenderer_Test, ResolvesFinalColorPolicyFromPipelineDefaultsAndCameraOverrides) {
Scene scene("SceneRendererFinalColorPolicyScene");
GameObject* cameraObject = scene.CreateGameObject("Camera");
auto* camera = cameraObject->AddComponent<CameraComponent>();
camera->SetPrimary(true);
camera->SetDepth(2.0f);
FinalColorOverrideSettings cameraOverrides = {};
cameraOverrides.overrideExposureValue = true;
cameraOverrides.exposureValue = 1.8f;
cameraOverrides.overrideFinalColorScale = true;
cameraOverrides.finalColorScale = XCEngine::Math::Vector4(0.95f, 0.9f, 0.85f, 1.0f);
camera->SetFinalColorOverrides(cameraOverrides);
auto assetState = std::make_shared<MockPipelineAssetState>();
assetState->defaultFinalColorSettings.outputTransferMode =
FinalColorOutputTransferMode::LinearToSRGB;
assetState->defaultFinalColorSettings.exposureMode =
FinalColorExposureMode::Fixed;
assetState->defaultFinalColorSettings.exposureValue = 1.25f;
assetState->defaultFinalColorSettings.finalColorScale =
XCEngine::Math::Vector4(1.0f, 0.95f, 0.9f, 1.0f);
SceneRenderer renderer(std::make_shared<MockPipelineAsset>(assetState));
const std::vector<CameraRenderRequest> requests =
renderer.BuildRenderRequests(scene, nullptr, CreateValidContext(), RenderSurface(640, 360));
ASSERT_EQ(requests.size(), 1u);
const CameraRenderRequest& request = requests[0];
EXPECT_TRUE(request.finalColorPolicy.hasPipelineDefaults);
EXPECT_TRUE(request.finalColorPolicy.hasCameraOverrides);
EXPECT_FALSE(request.finalColorPolicy.hasVolumeOverrides);
EXPECT_EQ(
request.finalColorPolicy.outputTransferMode,
FinalColorOutputTransferMode::LinearToSRGB);
EXPECT_EQ(
request.finalColorPolicy.exposureMode,
FinalColorExposureMode::Fixed);
EXPECT_FLOAT_EQ(request.finalColorPolicy.exposureValue, 1.8f);
EXPECT_FLOAT_EQ(request.finalColorPolicy.finalColorScale.x, 0.95f);
EXPECT_FALSE(request.finalOutput.IsRequested());
}
TEST(CameraRenderer_Test, UsesResolvedRenderAreaForCameraViewportDimensions) {
Scene scene("CameraRendererViewportRectScene");