Files
XCEngine/docs/api/_guides/Rendering/Camera-Request-Planning-And-Clear-Rules.md

172 lines
8.3 KiB
Markdown
Raw Normal View History

2026-04-08 16:07:03 +08:00
# Camera Request Planning And Clear Rules
## 这条链路现在是谁在负责什么
当前 XCEngine 的相机渲染不是“场景直接丢给主管线”这么简单,而是拆成了三层:
1. `SceneRenderRequestPlanner` 负责决定“这次到底要渲染哪些相机”。
2. `SceneRenderRequestUtils` 负责决定“这些相机怎样排序、怎样推导 clear、怎样落成 request”。
3. `SceneRenderer` 负责把 request 组织好,再交给 `CameraRenderer` 真正执行。
这种拆法和商业级引擎里常见的“规划层 / 执行层”是同一路思路。原因很现实:
- 场景侧关心的是相机选择、排序、stack 规则、viewport 解析。
- 执行侧关心的是 scene extraction、pipeline、object-id、post pass、overlay pass。
- 如果把这两类职责揉在一个类里Editor 叠加 pass、离屏目标、手工 request 提交都会很快失控。
当前源码正是按这个边界实现的:
- `SceneRenderer::BuildRenderRequests(...)`
- 直接委托给 `SceneRenderRequestPlanner::BuildRequests(...)`
- `SceneRenderer::Render(const std::vector<CameraRenderRequest>&)`
- 只做 request 校验、稳定排序和逐条转发
- `CameraRenderer::Render(...)`
- 才真正做 `RenderSceneExtractor`、主管线、object-id、post / overlay sequence
## override camera 的真实语义
`overrideCamera` 不是“无条件强制渲染这台相机”,而是“如果这台相机当前可用,就只渲染它;否则退回场景相机收集”。
可用性的判定来自 `SceneRenderRequestUtils::IsUsableCamera()`,当前必须同时满足:
- 相机指针非空
- 组件启用
- 挂载的 `GameObject` 非空
- `GameObject::IsActiveInHierarchy()` 为真
这很重要,因为它解释了为什么 `overrideCamera` 被禁用或所在对象失活时,系统不会报错,而是自动回退到 `scene.FindObjectsOfType<CameraComponent>()`
`tests/Rendering/unit/test_scene_render_request_planner.cpp` 已经明确覆盖了这两个分支:
- `UsesOverrideCameraExclusivelyWhenItIsUsable`
- `FallsBackToSceneCamerasWhenOverrideCameraIsNotUsable`
## 相机排序为什么是 Base 在前、Overlay 在后
当前排序规则非常直接,而且是源码写死的:
- 场景相机排序:`stackType -> depth`
- request 排序:`cameraStackOrder -> cameraDepth`
这里 `CameraStackType::Base = 0``CameraStackType::Overlay = 1`,因此自然得到:
- 所有 base camera 先画
- 所有 overlay camera 后画
- 同一 stack 内再按 `depth` 升序
排序函数都使用 `std::stable_sort()`。这意味着如果排序键完全相同,系统会保留原始相对顺序。
这条“稳定性”不是小细节,而是编辑器和复杂场景里很重要的可预期性来源:
- 从场景收集来的相机tie 时保留场景遍历顺序
- 手工提交的 request 数组tie 时保留调用方提交顺序
测试已经分别验证了这两种稳定性:
- `SceneRenderRequestPlanner_Test.CollectsOnlyUsableSceneCamerasInStableRenderOrder`
- `SceneRenderer_Test.PreservesSceneTraversalOrderForEqualPriorityCameras`
- `SceneRenderer_Test.PreservesManualSubmissionOrderForEqualPriorityRequests`
这和很多商业引擎的设计取向一致。即使当前还没有完整 camera stacking 依赖图,也先保证“线性规则简单、稳定、可预测”。
## Auto clear 为什么不是所有相机都清 Color
`CameraClearMode::Auto` 的真实语义,不是“总是清屏”,而是“按当前 request 在整次提交里的位置推导一个最保守、最稳定的默认值”。
当前实现的规则是:
- 显式 `ColorAndDepth`:返回 `RenderClearFlags::All`
- 显式 `DepthOnly`:返回 `RenderClearFlags::Depth`
- 显式 `None`:返回 `RenderClearFlags::None`
- `Auto`
- 对 base camera如果此前还没有成功渲染过 base camera`All`,否则 `Depth`
- 对 overlay camera如果这是整次提交里的第一个成功 request`All`,否则 `Depth`
这有两个很现实的工程收益。
第一,正常的 base camera 链不会重复清颜色。后续 base camera 只清 depth就能保留前面已经写好的颜色目标。
第二,纯 overlay 场景也不会黑屏或叠脏数据。如果场景里根本没有 base camera第一个 overlay 仍然会退回到 `All`,保证有一个可预测的起始帧。
对应测试锚点是:
- `SceneRenderRequestUtils_Test.ResolvesClearFlagsForExplicitAndAutoModes`
- `SceneRenderer_Test.RendersBaseCamerasBeforeOverlayCamerasAndResolvesAutoClearPerStackType`
- `SceneRenderer_Test.FallsBackToColorClearForFirstOverlayCameraWhenNoBaseCameraExists`
## viewport 和 render area 是怎样组合出来的
这套系统当前把 `RenderSurface` 当成“输出模板”,而不是最终固定矩形。
`BuildCameraRenderRequest()` 会先拷贝调用方传入的 `surface`,再把其中的 `renderArea` 改成当前相机对应的子区域。这个子区域来自:
- 相机的归一化 `viewportRect`
-`RenderSurface` 当前已经存在的 `renderArea`
换句话说,它支持“在已有 render area 上再切相机子视口”,而不是只能对整张 surface 做 0..1 映射。
当前实现细节是:
- 左上边界用 `floor`
- 右下边界用 `ceil`
这样做的好处是边界更稳定,不容易因为浮点截断把应该覆盖到的最后一列 / 最后一行像素吃掉。
如果最终算出来的 render area 宽或高为 `0`,这条 request 会被直接丢弃。
关键点在于:被丢弃的 base camera 不会推进 `renderedBaseCameraCount`。因此后面的相机 clear 推导仍然基于“真正成功进入请求队列的相机”,不会因为一个零尺寸视口把整条 clear 链搞错。
这个行为在下面两条测试里都有明确验证:
- `SceneRenderRequestPlanner_Test.BuildsRequestsAndDropsZeroSizedViewportsWithoutBreakingClearFlow`
- `SceneRenderRequestUtils_Test.BuildsRequestMetadataAndRejectsZeroSizedRenderAreas`
## 为什么 `SceneRenderer` 还要对手工 request 再排一次序
看起来调用方既然已经自己构造了 `std::vector<CameraRenderRequest>`,似乎可以按原顺序直接执行。
当前实现没有这么做,而是仍然会:
1. 检查每条 request 的 `IsValid()`
2. 复制一份数组
3.`SceneRenderRequestUtils::SortCameraRenderRequests(...)` 做稳定排序
4. 再逐条交给 `CameraRenderer`
这样做不是多余,而是为了把“相机优先级规则”固定在一个地方。
它带来的好处是:
- 从场景自动生成的 request 和手工注入的 request 遵循同一排序语义
- Editor、工具链、离屏渲染代码不必自己复制一套 stack/depth 规则
- 即使外部提交顺序混乱,只要 request 元数据是正确的,最终执行顺序仍然一致
测试 `SceneRenderer_Test.SortsManualCameraRequestsByDepthBeforeRendering``SceneRenderer_Test.PreservesManualSubmissionOrderForEqualPriorityRequests` 就是在验证这条契约。
## 当前这套模型的边界
这条链路已经合理,但还不是完整商业引擎的最终形态。当前边界很明确:
- 没有更复杂的 camera dependency graph
- 没有跨帧的 request 缓存或 render graph
- `SceneRenderRequestPlanner` 不会自动补 object-id、post passes、overlay passes
- `SceneRenderer` 不负责 scene extraction 和具体 pipeline 细节
- `CameraRenderer` 只执行单个 request不负责多相机策略
这其实是一个健康的中间阶段。它先把最容易失控的职责边界分干净,再在每一层之上继续增强。
如果以后要往更商业化的方向演进,最自然的扩展点通常会是:
- 更完整的 camera stacking 依赖
- 更显式的 renderer asset / render pipeline asset 配置
- 更系统的 request graph / frame graph
- 更成熟的 Scene View 和 runtime camera 共用编排模型
## 相关 API
- [Rendering](../../XCEngine/Rendering/Rendering.md)
- [SceneRenderer](../../XCEngine/Rendering/Execution/SceneRenderer/SceneRenderer.md)
- [SceneRenderRequestPlanner](../../XCEngine/Rendering/Planning/SceneRenderRequestPlanner/SceneRenderRequestPlanner.md)
- [SceneRenderRequestUtils](../../XCEngine/Rendering/Planning/SceneRenderRequestUtils/SceneRenderRequestUtils.md)
- [CameraRenderRequest](../../XCEngine/Rendering/Planning/CameraRenderRequest/CameraRenderRequest.md)
- [CameraRenderer](../../XCEngine/Rendering/Execution/CameraRenderer/CameraRenderer.md)