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

8.3 KiB
Raw Blame History

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 = 0CameraStackType::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 cameraAll,否则 Depth
    • 对 overlay camera如果这是整次提交里的第一个成功 requestAll,否则 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.SortsManualCameraRequestsByDepthBeforeRenderingSceneRenderer_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