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)
|
2026-04-10 17:14:27 +08:00
|
|
|
|
- [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)
|