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

172 lines
8.2 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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/SceneRenderer/SceneRenderer.md)
- [SceneRenderRequestPlanner](../../XCEngine/Rendering/SceneRenderRequestPlanner/SceneRenderRequestPlanner.md)
- [SceneRenderRequestUtils](../../XCEngine/Rendering/SceneRenderRequestUtils/SceneRenderRequestUtils.md)
- [CameraRenderRequest](../../XCEngine/Rendering/CameraRenderRequest/CameraRenderRequest.md)
- [CameraRenderer](../../XCEngine/Rendering/CameraRenderer/CameraRenderer.md)