# 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&)` - 只做 request 校验、稳定排序和逐条转发 - `CameraRenderer::Render(...)` - 才真正做 `RenderSceneExtractor`、主管线、object-id、post / overlay sequence ## override camera 的真实语义 `overrideCamera` 不是“无条件强制渲染这台相机”,而是“如果这台相机当前可用,就只渲染它;否则退回场景相机收集”。 可用性的判定来自 `SceneRenderRequestUtils::IsUsableCamera()`,当前必须同时满足: - 相机指针非空 - 组件启用 - 挂载的 `GameObject` 非空 - `GameObject::IsActiveInHierarchy()` 为真 这很重要,因为它解释了为什么 `overrideCamera` 被禁用或所在对象失活时,系统不会报错,而是自动回退到 `scene.FindObjectsOfType()`。 `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`,似乎可以按原顺序直接执行。 当前实现没有这么做,而是仍然会: 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)