8.3 KiB
Camera Request Planning And Clear Rules
这条链路现在是谁在负责什么
当前 XCEngine 的相机渲染不是“场景直接丢给主管线”这么简单,而是拆成了三层:
SceneRenderRequestPlanner负责决定“这次到底要渲染哪些相机”。SceneRenderRequestUtils负责决定“这些相机怎样排序、怎样推导 clear、怎样落成 request”。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 已经明确覆盖了这两个分支:
UsesOverrideCameraExclusivelyWhenItIsUsableFallsBackToSceneCamerasWhenOverrideCameraIsNotUsable
相机排序为什么是 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.CollectsOnlyUsableSceneCamerasInStableRenderOrderSceneRenderer_Test.PreservesSceneTraversalOrderForEqualPriorityCamerasSceneRenderer_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,则
这有两个很现实的工程收益。
第一,正常的 base camera 链不会重复清颜色。后续 base camera 只清 depth,就能保留前面已经写好的颜色目标。
第二,纯 overlay 场景也不会黑屏或叠脏数据。如果场景里根本没有 base camera,第一个 overlay 仍然会退回到 All,保证有一个可预测的起始帧。
对应测试锚点是:
SceneRenderRequestUtils_Test.ResolvesClearFlagsForExplicitAndAutoModesSceneRenderer_Test.RendersBaseCamerasBeforeOverlayCamerasAndResolvesAutoClearPerStackTypeSceneRenderer_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.BuildsRequestsAndDropsZeroSizedViewportsWithoutBreakingClearFlowSceneRenderRequestUtils_Test.BuildsRequestMetadataAndRejectsZeroSizedRenderAreas
为什么 SceneRenderer 还要对手工 request 再排一次序
看起来调用方既然已经自己构造了 std::vector<CameraRenderRequest>,似乎可以按原顺序直接执行。
当前实现没有这么做,而是仍然会:
- 检查每条 request 的
IsValid() - 复制一份数组
- 用
SceneRenderRequestUtils::SortCameraRenderRequests(...)做稳定排序 - 再逐条交给
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 passesSceneRenderer不负责 scene extraction 和具体 pipeline 细节CameraRenderer只执行单个 request,不负责多相机策略
这其实是一个健康的中间阶段。它先把最容易失控的职责边界分干净,再在每一层之上继续增强。
如果以后要往更商业化的方向演进,最自然的扩展点通常会是:
- 更完整的 camera stacking 依赖
- 更显式的 renderer asset / render pipeline asset 配置
- 更系统的 request graph / frame graph
- 更成熟的 Scene View 和 runtime camera 共用编排模型