From 6645d507d0aa1d863a39477346fa8805267d8c95 Mon Sep 17 00:00:00 2001 From: ssdfasd <2156608475@qq.com> Date: Mon, 6 Apr 2026 15:55:50 +0800 Subject: [PATCH] Formalize final color policy resolution --- ...段_FinalColorPipeline正式化计划_2026-04-06.md | 374 ++++++++++++++++++ ...Process描述层正式化计划_完成归档_2026-04-06.md | 127 ++++++ .../XCEngine/Components/CameraComponent.h | 6 + .../Rendering/Execution/SceneRenderer.h | 2 + .../Pipelines/BuiltinForwardPipeline.h | 1 + .../Rendering/Planning/CameraRenderRequest.h | 2 + .../Rendering/Planning/FinalColorSettings.h | 122 ++++++ .../XCEngine/Rendering/RenderPipelineAsset.h | 3 + engine/src/Components/CameraComponent.cpp | 34 ++ .../src/Rendering/Execution/SceneRenderer.cpp | 20 + .../test_camera_light_component.cpp | 29 +- .../unit/test_camera_scene_renderer.cpp | 49 +++ 12 files changed, 765 insertions(+), 4 deletions(-) create mode 100644 docs/plan/Renderer下一阶段_FinalColorPipeline正式化计划_2026-04-06.md create mode 100644 docs/used/Renderer下一阶段_CameraPostProcess描述层正式化计划_完成归档_2026-04-06.md create mode 100644 engine/include/XCEngine/Rendering/Planning/FinalColorSettings.h diff --git a/docs/plan/Renderer下一阶段_FinalColorPipeline正式化计划_2026-04-06.md b/docs/plan/Renderer下一阶段_FinalColorPipeline正式化计划_2026-04-06.md new file mode 100644 index 00000000..ee14c64a --- /dev/null +++ b/docs/plan/Renderer下一阶段_FinalColorPipeline正式化计划_2026-04-06.md @@ -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 底座”的关键收口阶段。 diff --git a/docs/used/Renderer下一阶段_CameraPostProcess描述层正式化计划_完成归档_2026-04-06.md b/docs/used/Renderer下一阶段_CameraPostProcess描述层正式化计划_完成归档_2026-04-06.md new file mode 100644 index 00000000..49b7b467 --- /dev/null +++ b/docs/used/Renderer下一阶段_CameraPostProcess描述层正式化计划_完成归档_2026-04-06.md @@ -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 -> 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 配置入口 + +本阶段的核心收口标准已经满足,可以提交归档。 diff --git a/engine/include/XCEngine/Components/CameraComponent.h b/engine/include/XCEngine/Components/CameraComponent.h index e57a70ed..e4e31920 100644 --- a/engine/include/XCEngine/Components/CameraComponent.h +++ b/engine/include/XCEngine/Components/CameraComponent.h @@ -7,6 +7,7 @@ #include #include #include +#include #include #include @@ -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; }; diff --git a/engine/include/XCEngine/Rendering/Execution/SceneRenderer.h b/engine/include/XCEngine/Rendering/Execution/SceneRenderer.h index b732a768..706fd109 100644 --- a/engine/include/XCEngine/Rendering/Execution/SceneRenderer.h +++ b/engine/include/XCEngine/Rendering/Execution/SceneRenderer.h @@ -46,6 +46,8 @@ public: private: void PrepareOwnedCameraPostProcessState(size_t requestCount); + void ResolveCameraFinalColorPolicies( + std::vector& requests) const; void AttachCameraPostProcessRequests( const RenderContext& context, std::vector& requests); diff --git a/engine/include/XCEngine/Rendering/Pipelines/BuiltinForwardPipeline.h b/engine/include/XCEngine/Rendering/Pipelines/BuiltinForwardPipeline.h index ce8bcf47..0b8e6e17 100644 --- a/engine/include/XCEngine/Rendering/Pipelines/BuiltinForwardPipeline.h +++ b/engine/include/XCEngine/Rendering/Pipelines/BuiltinForwardPipeline.h @@ -304,6 +304,7 @@ private: class BuiltinForwardPipelineAsset final : public RenderPipelineAsset { public: std::unique_ptr CreatePipeline() const override; + FinalColorSettings GetDefaultFinalColorSettings() const override { return {}; } }; } // namespace Pipelines diff --git a/engine/include/XCEngine/Rendering/Planning/CameraRenderRequest.h b/engine/include/XCEngine/Rendering/Planning/CameraRenderRequest.h index b5ee0a37..3f105bf4 100644 --- a/engine/include/XCEngine/Rendering/Planning/CameraRenderRequest.h +++ b/engine/include/XCEngine/Rendering/Planning/CameraRenderRequest.h @@ -1,6 +1,7 @@ #pragma once #include +#include #include #include #include @@ -184,6 +185,7 @@ struct CameraRenderRequest { DirectionalShadowRenderPlan directionalShadow; PostProcessRenderRequest postProcess; FinalOutputRenderRequest finalOutput; + ResolvedFinalColorPolicy finalColorPolicy = {}; ObjectIdRenderRequest objectId; float cameraDepth = 0.0f; uint8_t cameraStackOrder = 0; diff --git a/engine/include/XCEngine/Rendering/Planning/FinalColorSettings.h b/engine/include/XCEngine/Rendering/Planning/FinalColorSettings.h new file mode 100644 index 00000000..c9e3b82f --- /dev/null +++ b/engine/include/XCEngine/Rendering/Planning/FinalColorSettings.h @@ -0,0 +1,122 @@ +#pragma once + +#include + +#include + +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 diff --git a/engine/include/XCEngine/Rendering/RenderPipelineAsset.h b/engine/include/XCEngine/Rendering/RenderPipelineAsset.h index 6e86a41d..10cc3f4d 100644 --- a/engine/include/XCEngine/Rendering/RenderPipelineAsset.h +++ b/engine/include/XCEngine/Rendering/RenderPipelineAsset.h @@ -1,5 +1,7 @@ #pragma once +#include + #include namespace XCEngine { @@ -12,6 +14,7 @@ public: virtual ~RenderPipelineAsset() = default; virtual std::unique_ptr CreatePipeline() const = 0; + virtual FinalColorSettings GetDefaultFinalColorSettings() const { return {}; } }; } // namespace Rendering diff --git a/engine/src/Components/CameraComponent.cpp b/engine/src/Components/CameraComponent.cpp index 2112694a..aaf4488c 100644 --- a/engine/src/Components/CameraComponent.cpp +++ b/engine/src/Components/CameraComponent.cpp @@ -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(m_finalColorOverrides.outputTransferMode) << ";"; + os << "finalColorOverrideExposureModeEnabled=" << (m_finalColorOverrides.overrideExposureMode ? 1 : 0) << ";"; + os << "finalColorOverrideExposureMode=" << static_cast(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(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(std::stoi(value)); + } else if (key == "finalColorOverrideExposureModeEnabled") { + m_finalColorOverrides.overrideExposureMode = (std::stoi(value) != 0); + } else if (key == "finalColorOverrideExposureMode") { + m_finalColorOverrides.exposureMode = + static_cast(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(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(std::stoul(value)); deserializedPostProcessPasses.clear(); diff --git a/engine/src/Rendering/Execution/SceneRenderer.cpp b/engine/src/Rendering/Execution/SceneRenderer.cpp index 885803e8..f8f12010 100644 --- a/engine/src/Rendering/Execution/SceneRenderer.cpp +++ b/engine/src/Rendering/Execution/SceneRenderer.cpp @@ -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 SceneRenderer::BuildRenderRequests( const RenderSurface& surface) { std::vector 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& 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& requests) { diff --git a/tests/Components/test_camera_light_component.cpp b/tests/Components/test_camera_light_component.cpp index f7a6fc3b..bd0784dd 100644 --- a/tests/Components/test_camera_light_component.cpp +++ b/tests/Components/test_camera_light_component.cpp @@ -2,8 +2,6 @@ #include #include -#include - #include 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, diff --git a/tests/Rendering/unit/test_camera_scene_renderer.cpp b/tests/Rendering/unit/test_camera_scene_renderer.cpp index 9c53d978..29fdf9b1 100644 --- a/tests/Rendering/unit/test_camera_scene_renderer.cpp +++ b/tests/Rendering/unit/test_camera_scene_renderer.cpp @@ -279,6 +279,7 @@ private: struct MockPipelineAssetState { int createCalls = 0; std::shared_ptr lastCreatedPipelineState; + FinalColorSettings defaultFinalColorSettings = {}; }; class MockPipeline final : public RenderPipeline { @@ -344,6 +345,10 @@ public: return std::make_unique(m_state->lastCreatedPipelineState); } + FinalColorSettings GetDefaultFinalColorSettings() const override { + return m_state->defaultFinalColorSettings; + } + private: std::shared_ptr 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(); + 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(); + 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(assetState)); + const std::vector 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");