Refactor scene viewport render planning

This commit is contained in:
2026-04-03 14:17:50 +08:00
parent c04dbb07e0
commit 608e0bc9d8
4 changed files with 1004 additions and 31 deletions

View File

@@ -0,0 +1,793 @@
# Unity式 SceneView Gizmo 系统完整审查与正式化重构方案
日期:`2026-04-03`
相关旧文档:
- `docs/plan/SceneViewport_Overlay_Gizmo_Rework_Plan.md`
- `docs/plan/SceneViewport_Overlay_Gizmo_Rework_Checkpoint_2026-04-02.md`
- `docs/plan/Unity SRP API参考文档.md`
本文档以当前代码真实状态为准,覆盖:
- `editor` 中 SceneView gizmo / grid / outline / overlay 的现状审查
- `engine``editor` 的职责边界重划
- 一套更接近 Unity SceneView / Gizmos / Handles 思路的正式化重构方案
- 可分阶段落地、可测试、可验收的迁移计划
## 0. 执行结论
当前 SceneView 可视化链路已经完成了第一轮“从 ImGui 临时绘制向正式渲染 pass 迁移”的关键基础设施建设,但还没有真正收口。
当前最重要的结论有四条:
1. `engine` 里现有的通用渲染扩展点是对的,应该保留。
- `RenderPass`
- `RenderPassSequence`
- `CameraRenderRequest::preScenePasses`
- `CameraRenderRequest::postScenePasses`
- `CameraRenderRequest::overlayPasses`
2. `editor` 里现有的 `SceneViewportEditorOverlayData``SceneViewportEditorOverlayPass` 方向是对的,应该继续扩展,而不是回退到 ImGui world draw。
3. 目前真正的架构问题,不是“某个 gizmo 画得还不够像 Unity”而是 **SceneView 的 editor 语义仍然泄漏进了 engine 的 builtin postprocess**
- `infinite grid`
- `selection outline`
- `outline debug mask`
- 与之绑定的 shader / resource 注册
4. 最终正确边界应当是:
- `engine/runtime` 负责“能画什么、怎样插 pass、怎样跑后端”
- `editor/SceneView` 负责“为什么要画、何时画、画哪些 gizmo、哪些对象参与、交互规则是什么”
换句话说,**Unity-like 的正确做法不是把所有 gizmo 都塞进 runtime engine而是 runtime 提供绘制能力editor 拥有 SceneView gizmo 的语义和调度权。**
---
## 1. 当前实现完整审查
## 1.1 已经正式化、应保留的部分
### A. CameraRenderer 的可扩展 pass 时序已经成立
当前 `CameraRenderer` 的执行顺序已经具备正规的扩展缝:
```text
preScenePasses
-> scene geometry
-> object id
-> postScenePasses
-> builtin postprocess
-> overlayPasses
```
这条缝本身是正确的,说明现在的 renderer 已经不是一个只能硬编码单条流水线的结构,后续 SceneView 的正式化工作不需要推翻它。
### B. Editor world overlay 已经有 canonical frame data
`SceneViewportEditorOverlayData.h` 已经提供了一份比较正确的中间表示:
- `worldLines`
- `worldSprites`
- `screenTriangles`
- `handleRecords`
这意味着当前系统已经不必再依赖“某个 gizmo 组件自己画、自己 hit test、自己决定屏幕图元”的散乱做法而是可以朝一份单帧 canonical overlay 数据前进。
### C. Editor overlay 已经具备独立 GPU pass
`SceneViewportEditorOverlayPass` 已经能够在 renderer pass 中绘制:
- world line
- sprite billboard
- screen-space triangle
这一步非常关键。它说明 camera/light icon、camera frustum、light shape、transform gizmo 等内容已经不需要再走 ImGui world draw 这条临时路径。
### D. Scene object picking 已经走 object-id GPU 路线
当前对象选中本身已经不是纯 CPU 射线猜测,而是建立在 object id buffer 之上的正式读回流程。这个方向是对的,后续无需推翻。
结论:
**当前系统最有价值的资产不是若干单独功能而是已经出现了一套可继续扩张的“SceneView render extension seam + canonical overlay data + editor overlay pass”。**
---
## 1.2 已经进入正式化,但还没有彻底收口的部分
### A. Camera / Light icon、Camera frustum、Directional Light gizmo
这部分现在已经通过 `SceneViewportOverlayBuilder -> SceneViewportEditorOverlayPass` 进入 GPU overlay 链路,已经不再是纯 ImGui hack。
这是正确方向,但当前还存在几个明显问题:
- `SceneViewportOverlayBuilder` 仍然是一个偏“堆逻辑”的单体 builder
- gizmo provider 还没有独立注册体系
- 灯光 gizmo 的覆盖范围还不完整
- `PointLight` / `SpotLight` 还没有形成完整正式方案
### B. Transform gizmo 的绘制链路已经正规化,但来源还偏临时
当前 transform gizmo 的 solver、状态机、屏幕几何、handle records 已经大量进入 canonical overlay 路线,这是好的。
但当前仍然有两个收口问题:
- `SceneViewPanel` 仍然参与了 transform gizmo 输入组装与 transient overlay 提交
- `ViewportHostService` 仍然通过 `SetSceneViewTransientTransformGizmoOverlayData(...)` 注入一份“临时 overlay”
这说明 transform gizmo 还没有真正变成一套由 SceneView gizmo system 自己统一调度的 provider而是“正规链路 + 临时桥接层”并存。
### C. Orientation gizmo 仍然走 ImGui / HUD 路线
这本身不算错误,因为 orientation gizmo 本质上就是 HUD而不是 world overlay。
真正的问题在于:
- 当前 HUD overlay 与 world overlay 的系统边界还没有被正式命名
- `SceneViewportOverlayRenderer.cpp` 还承担着一个“历史遗留收容层”的角色
结论:
**不是所有 gizmo 都必须迁入 GPU world pass。SceneView 右上角 orientation gizmo 属于 HUD可以继续保留在 editor UI 层,但应该被正式归类为 HUD overlay而不是继续以“杂项 overlay”存在。**
---
## 1.3 当前最核心的架构问题SceneView 语义泄漏进了 engine
### A. grid / outline 不应该继续作为 engine 的 builtin scene semantics 存在
当前 `CameraRenderRequest::BuiltinPostProcessRequest` 直接携带:
- `InfiniteGridPassData`
- `selectedObjectIds`
- `ObjectIdOutlineStyle`
这在工程上能跑,但从架构边界看是不对的。原因很简单:
- SceneView infinite grid 是编辑器语义,不是 runtime camera 的通用语义
- Scene selection outline 是编辑器语义,不是 runtime scene 的通用语义
- `debugSelectionMask` 更是纯 editor 调试语义
如果这些字段挂在 engine 的公共 camera request 上,等价于让 runtime camera API 默认理解 SceneView 的编辑器概念。
这与 Unity 风格不一致。
### B. 具体 editor 效果被注册成 engine builtin shader / builtin resource
当前 engine builtin resource 中已经出现:
- `builtin://shaders/object-id-outline`
- `builtin://shaders/infinite-grid`
这意味着:
- runtime 资源表里已经掺入 editor SceneView 专用效果
- 后续 player/package 很容易继续被这些 editor-only 资源污染
- engine API 会逐步被 SceneView 需求反向牵着走
### C. SceneView 的 post scene policy 目前仍然依赖 engine 内建的固定流程
现在 `SceneView` 的 grid / outline 虽然是从 editor 发起,但执行上还是通过 engine builtin postprocess builder 被硬编码规划:
- `SceneInfiniteGrid`
- `SceneSelectionOutline`
- `SceneSelectionMaskDebug`
这相当于 SceneView 的一部分渲染语义并不真正属于 editor而是“借用 engine builtin 实现”。
短期可用,长期会越来越难维护。
结论:
**当前系统最大的结构性缺陷,不是 gizmo 样式,而是 editor 语义与 runtime 语义没有彻底分层。**
---
## 1.4 仍然偏临时的实现点
### A. icon 资源加载仍然是 ad hoc 文件路径解析
`SceneViewportEditorOverlayPass` 当前直接解析:
- `editor/resources/Icons/camera_gizmo.png`
- `editor/resources/Icons/main_light_gizmo.png`
问题在于:
- 没有 editor 内建资源注册表
- 没有生命周期与缓存抽象
- 没有 icon atlas / versioning / fallback 策略
- 这条链仍然像“功能先跑起来”的工程写法
### B. gizmo provider 没有正式注册点
当前 camera/light gizmo 是 `SceneViewportOverlayBuilder` 直接扫描 scene 后构建。
这在功能上可行,但有明显扩展隐患:
- 新增 gizmo 类型时会继续堆进单个 builder
- provider 的职责无法单测
- 组件与 gizmo 的映射关系不清晰
- 后续 collider / reflection probe / audio source / particle / custom component gizmo 很难优雅接入
### C. 视觉图元与交互图元虽然已经接近统一,但规范还不完整
现在 `handleRecords` 已经是一个很好的方向,但还缺少更明确的约束:
- 哪些图元负责可见渲染
- 哪些记录负责 hit test
- 同一 handle 是否允许多份可视 primitive
- 优先级、遮挡、深度模式、拾取扩张半径等是否有统一规则
当前这些规则大多“隐含在代码里”,还没有真正上升为系统规范。
---
## 1.5 测试体系现状
当前测试并不差,但仍然没有覆盖到最关键的“正式化边界”。
已有较好覆盖:
- grid 数学与相机投影相关测试
- render flow utils 测试
- object id picker 测试
- move / rotate / scale gizmo solver 测试
当前缺口:
- `SceneViewportOverlayBuilder` 的 contract 测试不足
- `SceneViewportEditorOverlayPass` 的资源契约与 primitive 分发测试不足
- camera / light gizmo provider 行为测试不足
- SceneView pass 顺序与装配测试不足
- “editor-only 资源不污染 runtime” 的构建层测试缺失
结论:
**当前最大测试缺口不是数学公式而是架构边界、provider 契约、pass 组合关系。**
---
## 2. Unity 风格的目标架构
## 2.1 总体原则
参考 Unity 的思路,应当把 SceneView 可视化拆成三层:
### Layer ARuntime Render Capability属于 `engine`
负责“能画什么”:
- scene geometry 渲染
- object id 渲染
- render pass 扩展点
- 通用 full-screen / line / sprite / mesh / debug primitive 渲染能力
- 后端抽象、资源绑定、shader 编译与执行
### Layer BSceneView Gizmo Orchestration属于 `editor`
负责“为什么画、何时画、画哪些”:
- SceneView grid
- selection outline
- camera / light / helper gizmo
- transform gizmo
- Scene icon
- gizmo subset 调度
- 交互 handle 组织与命中规则
### Layer CHUD Overlay属于 `editor UI`
负责固定在面板屏幕空间的内容:
- orientation gizmo
- toolbar
- 状态提示
- 调试开关
- 2D 操作提示与 hover 标签
这三层之间的关系应该是:
```text
engine 提供绘制能力
editor SceneView 决定 world gizmo / editor pass 计划
editor UI 决定 HUD 展示
```
---
## 2.2 Unity-like 的关键不是“都放 engine”而是“runtime 与 editor 的语义边界清楚”
Unity 的本质做法并不是让 player runtime 永远背着 SceneView gizmo 语义。
更接近本项目的正确映射应当是:
- `engine` 对应 Unity 中底层 render context / renderer capability
- `editor` 的 SceneView 系统对应 Unity 中 SceneView 对 gizmos / wire overlay / handles 的调度
- `HUD` 对应 SceneView 窗口自己的 UI overlay
因此本项目的最终方案应当遵循:
1. `engine` 暴露“generic render extension seam”
2. `editor` 组装 SceneView 专属 render plan
3. SceneView 的 gizmo/grid/outline 作为 editor-only feature 实现
4. runtime/player 构建不再默认携带 SceneView 语义资产
---
## 2.3 正式目标流水线
推荐的 SceneView 渲染时序如下:
```text
PreScenePasses
-> Scene Geometry
-> ObjectId Pass
-> SceneView Editor PostScene Passes
- Grid
- Selection Outline
- Future: bounds / selection mask visualize / editor-only debug fullscreen pass
-> SceneView World Overlay Pass
- Scene icons
- Camera frustum
- Light gizmos
- Transform gizmos
- Future: collider / audio / volume / probe gizmos
-> SceneView HUD Overlay
- Orientation gizmo
- Toolbar
- Labels / hints / debug text
```
这里面最关键的一点是:
- `grid``selection outline` 仍然属于 world-space / scene-view editor pass
- 但它们不再属于 engine 的 builtin camera semantics
- 它们应该变成 editor 自己构建的 post-scene pass chain
---
## 2.4 Gizmo 系统内部应再拆三类数据
最终系统不应只有“画什么”,还要清楚地区分三种数据:
### A. Visual Primitive Data
纯视觉数据:
- line
- billboard sprite
- mesh / wire mesh
- screen triangle
### B. Handle Interaction Data
纯交互数据:
- handle id
- entity id
- pick shape
- priority
- depth / sort rule
- hit thickness / pick expansion
### C. Gizmo Provider Output
语义层输出:
- Camera gizmo provider 输出 camera icon + frustum + camera specific handles
- Light gizmo provider 输出 light icon + light shape
- Transform gizmo provider 输出 axis / plane / ring / center handles
这样做的好处是:
- 视觉可以演进
- 交互可以演进
- provider 可以独立测试
- 不会再把 SceneViewPanel 变成绘制器 + 命中器 + 业务调度器的混合体
---
## 3. 最终职责边界
## 3.1 `engine` 应保留的内容
以下内容应明确保留在 `engine`
- `RenderPass` / `RenderPassSequence`
- `CameraRenderer` 的 pass 执行骨架
- scene geometry 渲染
- object id pass
- render target / state transition / descriptor / backend shader compilation
- 如果未来可复用,则保留“无 editor 语义”的底层 helper
- full-screen quad/blit helper
- line primitive renderer helper
- sprite billboard renderer helper
- debug mesh renderer helper
核心要求:
**这些能力必须是 generic 的,不能带 SceneView / Gizmo / Editor 的具体业务语义。**
---
## 3.2 `editor` 应拥有的内容
以下内容应明确归 `editor`
- SceneView grid
- selection outline 及其颜色、宽度、debug mask 策略
- camera / light scene icon
- camera frustum
- directional / point / spot light gizmo
- transform gizmo
- orientation gizmo
- 组件到 gizmo provider 的注册关系
- editor-only gizmo shader / icon / material / texture 资源
换句话说:
**凡是只有 SceneView 才关心的“显示语义”,最终都必须回收到 `editor`。**
---
## 3.3 可以下沉到 `engine` 的只有“无语义 helper”不是 SceneView 语义本身
例如:
- 如果 full-screen outline 的底层执行流程是通用的,可以提炼成 helper
- 如果 world line / billboard / wire mesh 的 GPU 提交代码是通用的,可以提炼成 helper
但是不能继续暴露成这样的 engine 公共语义:
- `SceneInfiniteGrid`
- `SceneSelectionOutline`
- `debugSelectionMask`
原则非常简单:
**实现可以共享,语义不能泄漏。**
---
## 4. 正式重构方案
## 4.1 Phase 1先把 SceneView pass 规划权完全收回 editor
### 目标
`editor` 自己决定 SceneView 需要哪些 pass而不是继续把 grid / outline 塞进 `engine::BuiltinPostProcessRequest`
### 方案
新增 editor 侧的 SceneView render planning 层,例如:
- `SceneViewportRenderPlan`
- `SceneViewportPassChainBuilder`
由它负责:
- 生成 SceneView 的 `postScenePasses`
- 生成 SceneView 的 `overlayPasses`
- 规划 object id 是否需要
- 规划 grid / outline / gizmo 的具体顺序
### 结果
`CameraRenderRequest` 仍然保留:
- `preScenePasses`
- `postScenePasses`
- `overlayPasses`
`BuiltinPostProcessRequest` 不再继续承载 SceneView 专属语义。
### 验收标准
- `SceneView` 渲染路径不再依赖 `BuiltinPostProcessRequest` 中的 grid / outline 字段
- SceneView pass 顺序完全由 editor 侧 builder 决定
- engine 只负责执行 pass不再理解 SceneView 概念
---
## 4.2 Phase 2把 Grid 与 Selection Outline 从 engine builtin 中迁出
### 目标
完成这次重构中最关键的一刀:**切断 SceneView editor 语义对 engine builtin postprocess 的依赖。**
### 方案
`editor/src/Viewport/Passes` 新建 editor-owned passes
- `SceneViewportGridPass`
- `SceneViewportSelectionOutlinePass`
如果当前实现代码中有通用部分,可以这样拆:
- `engine`
- 保留无语义的 GPU helper
- `editor`
- 维护 `SceneViewportGridPassData`
- 维护 `SceneViewportSelectionOutlineSettings`
- 维护 SceneView 下的调用时机与参数策略
### 需要同步迁移的内容
- `InfiniteGridPassData` 从 engine 公共 camera request 中移除
- `ObjectIdOutlineStyle` 不再作为 engine camera request 的 editor 语义字段暴露
- `builtin://shaders/infinite-grid`
- `builtin://shaders/object-id-outline`
上述资源应迁移为 editor-owned builtin 资源,或至少从注册语义上变为 editor 专属。
### 验收标准
- runtime build 不再默认依赖 SceneView grid / outline shader
- `CameraRenderRequest` 不再携带 SceneView grid / outline 语义
- SceneView grid / outline 仍能正常工作,并维持现有视觉效果
---
## 4.3 Phase 3建立正式的 Gizmo Provider Registry
### 目标
把当前“一个大 builder 扫全场”的模式升级为“SceneView 系统调度多个 provider”。
### 建议接口
```text
ISceneViewportGizmoProvider
Gather(const SceneViewportGizmoBuildContext&, SceneViewportOverlayFrameData&)
```
### 建议首批 provider
- `SceneCameraGizmoProvider`
- `SceneLightGizmoProvider`
- `SceneTransformHandleProvider`
- `SceneSelectionHelperProvider`
后续可继续扩展:
- collider gizmo provider
- audio source gizmo provider
- reflection probe gizmo provider
- custom editor component gizmo provider
### 为什么必须这么做
因为 Unity-like 的 SceneView 不是“一个巨大 builder 拼所有形状”而是“editor 根据对象类型和选择状态,调度多种 gizmo provider / handle provider / wire provider”。
### 验收标准
- `SceneViewportOverlayBuilder` 从单体 builder 退化为 orchestration layer
- 新增 gizmo 类型时,不需要继续往一个 cpp 文件里堆逻辑
- provider 可以独立单测
---
## 4.4 Phase 4把 World Overlay 与 HUD Overlay 正式分家
### 目标
让系统命名和责任与实际表现一致。
### 最终划分
#### World Overlay
使用 renderer pass
- scene icons
- frustum
- light shapes
- transform gizmos
- future helper volumes
#### HUD Overlay
使用 editor UI / ImGui
- orientation gizmo
- toolbar
- screen label
- 操作提示
### 注意
这一步不是要求 orientation gizmo 也上 GPU。
正确目标是:
- world-space 的内容不再走 ImGui
- HUD 的内容明确归 UI
### 验收标准
- `SceneViewportOverlayRenderer.cpp` 不再承担历史兼容杂项职责
- 所有 world-anchored overlay 都从统一 world overlay frame 进入
- HUD overlay 有独立职责命名
---
## 4.5 Phase 5建立 editor gizmo 资源体系
### 目标
消除当前 ad hoc 图标与 shader 资源加载方式。
### 方案
新增 editor 资源注册层,例如:
- `EditorBuiltinResourceRegistry`
- `EditorGizmoResourceRegistry`
负责:
- icon texture 注册
- gizmo shader 注册
- gizmo material / pipeline preset 注册
- future atlas / hot reload / fallback
### 具体要求
- 不再在 pass 内部直接写文件路径解析作为长期方案
- camera / light / future icons 统一纳入 editor builtin 资源
- gizmo pass 通过资源 key 查询,而不是硬编码本地 png 路径
### 验收标准
- gizmo 资源加载路径统一
- 资源生命周期与缓存不再散落在单个 pass 里
- future 新增 icon / shader 不需要继续复制粘贴路径解析代码
---
## 4.6 Phase 6测试体系重构
### 目标
让测试覆盖系统边界,而不只是覆盖若干数学细节。
### 建议补齐的测试类别
#### A. Provider contract tests
例如:
- camera provider 在 camera enabled 时输出 icon + frustum
- disabled camera 不输出 gizmo
- directional / point / spot light 分别输出正确图元组合
#### B. Overlay builder orchestration tests
例如:
- 多 provider 输出能正确合并到单帧 overlay data
- selected / unselected subset 行为正确
- provider 顺序不影响 hit test 规则
#### C. Pass assembly tests
例如:
- SceneView render plan 能生成正确的 `postScenePasses`
- SceneView world overlay pass 顺序正确
- object id / outline / gizmo 的依赖关系正确
#### D. Editor resource contract tests
例如:
- icon key 能解析到合法资源
- 缺失资源时能给出可诊断 fallback
#### E. Packaging / build boundary tests
例如:
- player runtime target 不依赖 editor gizmo shader / icon 资源
- editor target 才链接或注册 SceneView gizmo 资源
### 验收标准
- 新增 gizmo 类型时,至少有 provider contract test
- 修改 SceneView pass 顺序时,至少有 render plan test 可以兜底
- editor/runtime 边界错误能通过构建或测试尽早暴露
---
## 5. 关于 Picking 与 Handle 命中的最终建议
## 5.1 Scene object picking
当前对象选择已经基于 object-id buffer这条路线应保留。
原因:
- 这是正规的 GPU picking
- 它与场景真实渲染结果一致性更好
- 更适合 editor 里对 mesh / model / future submesh 的对象级选中
## 5.2 Gizmo handle picking
当前 gizmo handle 的命中是基于 canonical `handleRecords` 的 CPU hit test。
这不是临时方案,反而是合理的正式方案之一。
原因:
- gizmo handle 本身就是高度人工设计的 screen-space / mixed-space 交互图元
- CPU 命中更容易做 priority、命中扩张、前后景优先级、hover 宽容度
- 没必要为了“全 GPU 化”把 handles 也做成 object-id pass
最终建议:
- Scene object picking继续 GPU object-id
- Gizmo handle picking继续 CPU hit test但必须建立在统一 canonical handle 数据之上
这与 Unity 的使用体验和工程组织都更接近。
---
## 6. 对当前代码的具体重构建议
## 6.1 立即保留、不要动错的部分
- 保留 `CameraRenderer` 的 pass 时序骨架
- 保留 `overlayPasses`
- 保留 `SceneViewportEditorOverlayData`
- 保留 `SceneViewportEditorOverlayPass`
- 保留 object-id picking
这些是已经验证正确的基础设施,不应推翻重做。
## 6.2 应优先迁出的部分
- `BuiltinPostProcessRequest` 中的 SceneView 语义字段
- `BuiltinInfiniteGridPass`
- `BuiltinObjectIdOutlinePass`
- SceneView 专属 shader 的 engine builtin 注册
## 6.3 应逐步拆分的部分
- `SceneViewportOverlayBuilder`
- `ViewportHostService` 中 SceneView overlay / transient gizmo 的编排逻辑
- `SceneViewPanel` 中仍然残留的 gizmo orchestration 代码
---
## 7. 推荐落地顺序
为避免大爆炸式重写,建议严格按下面顺序推进:
1. 先做 Phase 1
- editor 拿回 SceneView pass 规划权
- 不要先拆 gizmo provider
2. 再做 Phase 2
- grid / outline 脱离 engine builtin
- 先保证边界正确
3. 再做 Phase 3
- provider registry
- camera / light / transform 分 provider
4. 再做 Phase 4
- 正式命名 HUD 与 world overlay 的分层
5. 然后做 Phase 5
- 资源体系
6. 最后做 Phase 6
- 测试与 packaging 收口
核心原则:
**不要先推翻正在工作的 overlay pass应当先纠正语义归属再做 provider 化和资源化。**
---
## 8. 非目标
本轮重构不应顺手扩展成以下主题:
- render graph 改造
- gameplay runtime debug draw 系统
- 把所有 ImGui HUD 都迁到 GPU
- 把 gizmo handle picking 也改成 GPU object-id
- 重写整个 editor UI 框架
这些都不是本轮的主目标。
本轮的唯一主目标是:
**把 SceneView gizmo / grid / outline 的系统边界、调度方式、资源归属、测试契约一次性做正规。**
---
## 9. 完成态判定标准
当以下条件全部满足时,可以认为本轮正式化重构收口:
1. `engine` 不再公开 SceneView grid / outline 的具体 camera 语义
2. `editor` 自己构建 SceneView post-scene passes 与 world overlay passes
3. camera / light / transform gizmo 由正式 provider 输出
4. orientation gizmo 被正式归类为 HUD overlay
5. gizmo 图标和 shader 拥有 editor 侧资源注册体系
6. player runtime 构建不再依赖 SceneView gizmo 资源
7. 测试覆盖 provider、render plan、resource boundary 三类关键契约
达到这一步后,当前 SceneView gizmo 系统才算真正进入“可持续扩展”的阶段。
---
## 10. 本文档对应的直接行动建议
下一阶段的实施建议如下:
1. 先开一个“SceneView pass ownership 回收”阶段
- 目标只做 Phase 1 + Phase 2
- 不在同一阶段内继续大规模重写 gizmo 样式
2. 该阶段完成后再开“Gizmo Provider Registry”阶段
- 先把 camera / light provider 正式拆出
- transform gizmo provider 最后并入
3. 再开“Editor Gizmo Resources + Tests 收口”阶段
- 把图标、shader、测试边界一次性清干净
这会比“看到哪改哪、边做边塞进现有 builder”稳定得多。

View File

@@ -0,0 +1,96 @@
#pragma once
#include "Passes/SceneViewportEditorOverlayPass.h"
#include "SceneViewportEditorOverlayData.h"
#include "ViewportHostRenderFlowUtils.h"
#include <XCEngine/Core/Math/Color.h>
#include <XCEngine/Rendering/RenderPass.h>
#include <functional>
#include <memory>
#include <utility>
#include <vector>
namespace XCEngine {
namespace Editor {
struct SceneViewportRenderPlan {
Rendering::BuiltinPostProcessRequest builtinPostProcess = {};
Rendering::RenderPassSequence postScenePasses = {};
Rendering::RenderPassSequence overlayPasses = {};
bool hasClearColorOverride = true;
Math::Color clearColorOverride = Math::Color(0.27f, 0.27f, 0.27f, 1.0f);
bool HasPostScenePasses() const {
return postScenePasses.GetPassCount() > 0;
}
bool HasOverlayPasses() const {
return overlayPasses.GetPassCount() > 0;
}
};
using SceneViewportOverlayPassFactory =
std::function<std::unique_ptr<Rendering::RenderPass>(const SceneViewportOverlayFrameData&)>;
struct SceneViewportRenderPlanBuildResult {
SceneViewportRenderPlan plan = {};
const char* warningStatusText = nullptr;
};
inline SceneViewportRenderPlanBuildResult BuildSceneViewportRenderPlan(
const ViewportRenderTargets& targets,
const SceneViewportOverlayData& overlay,
const std::vector<uint64_t>& selectedObjectIds,
const SceneViewportOverlayFrameData& editorOverlayFrameData,
const SceneViewportOverlayFrameData& transientOverlayFrameData,
const SceneViewportOverlayPassFactory& overlayPassFactory,
bool debugSelectionMask = false) {
SceneViewportRenderPlanBuildResult result = {};
if (!overlay.valid) {
return result;
}
const SceneViewportBuiltinPostProcessBuildResult builtinPostProcess =
BuildSceneViewportBuiltinPostProcess(
overlay,
selectedObjectIds,
targets.objectIdShaderView != nullptr,
debugSelectionMask);
result.plan.builtinPostProcess = builtinPostProcess.request;
result.warningStatusText = builtinPostProcess.warningStatusText;
SceneViewportOverlayFrameData renderOverlayFrameData = editorOverlayFrameData;
AppendSceneViewportOverlayFrameData(renderOverlayFrameData, transientOverlayFrameData);
if (renderOverlayFrameData.HasOverlayPrimitives() &&
overlayPassFactory != nullptr) {
std::unique_ptr<Rendering::RenderPass> overlayPass = overlayPassFactory(renderOverlayFrameData);
if (overlayPass != nullptr) {
result.plan.overlayPasses.AddPass(std::move(overlayPass));
}
}
return result;
}
inline void ApplySceneViewportRenderPlan(
const ViewportRenderTargets& targets,
SceneViewportRenderPlan& plan,
Rendering::CameraRenderRequest& request) {
ApplySceneViewportRenderRequestSetup(
targets,
&plan.builtinPostProcess,
&plan.postScenePasses,
request);
if (plan.HasOverlayPasses()) {
request.overlayPasses = &plan.overlayPasses;
}
request.hasClearColorOverride = plan.hasClearColorOverride;
request.clearColorOverride = plan.clearColorOverride;
}
} // namespace Editor
} // namespace XCEngine

View File

@@ -9,6 +9,7 @@
#include "SceneViewportEditorOverlayData.h" #include "SceneViewportEditorOverlayData.h"
#include "SceneViewportOverlayHandleBuilder.h" #include "SceneViewportOverlayHandleBuilder.h"
#include "SceneViewportOverlayBuilder.h" #include "SceneViewportOverlayBuilder.h"
#include "SceneViewportRenderPlan.h"
#include "ViewportHostRenderFlowUtils.h" #include "ViewportHostRenderFlowUtils.h"
#include "ViewportHostRenderTargets.h" #include "ViewportHostRenderTargets.h"
#include "ViewportObjectIdPicker.h" #include "ViewportObjectIdPicker.h"
@@ -417,9 +418,7 @@ private:
struct SceneViewportRenderState { struct SceneViewportRenderState {
SceneViewportOverlayData overlay = {}; SceneViewportOverlayData overlay = {};
Rendering::BuiltinPostProcessRequest builtinPostProcess = {}; SceneViewportRenderPlan renderPlan = {};
SceneViewportOverlayFrameData editorOverlayFrameData = {};
std::vector<uint64_t> selectedObjectIds;
}; };
ViewportEntry& GetEntry(EditorViewportKind kind) { ViewportEntry& GetEntry(EditorViewportKind kind) {
@@ -641,17 +640,25 @@ private:
return; return;
} }
outState.selectedObjectIds = context.GetSelectionManager().GetSelectedEntities(); const std::vector<uint64_t> selectedObjectIds = context.GetSelectionManager().GetSelectedEntities();
outState.editorOverlayFrameData = GetSceneViewEditorOverlayFrameData(context); const SceneViewportOverlayFrameData& editorOverlayFrameData =
const SceneViewportBuiltinPostProcessBuildResult builtinPostProcess = GetSceneViewEditorOverlayFrameData(context);
BuildSceneViewportBuiltinPostProcess( SceneViewportRenderPlanBuildResult renderPlan =
BuildSceneViewportRenderPlan(
entry.renderTargets,
outState.overlay, outState.overlay,
outState.selectedObjectIds, selectedObjectIds,
entry.renderTargets.objectIdShaderView != nullptr, editorOverlayFrameData,
BuildSceneViewTransientTransformGizmoOverlayFrameData(),
[this](const SceneViewportOverlayFrameData& frameData) {
return CreateSceneViewportEditorOverlayPass(
m_sceneViewportEditorOverlayRenderer,
frameData);
},
kDebugSceneSelectionMask); kDebugSceneSelectionMask);
outState.builtinPostProcess = builtinPostProcess.request; outState.renderPlan = std::move(renderPlan.plan);
if (builtinPostProcess.warningStatusText != nullptr) { if (renderPlan.warningStatusText != nullptr) {
SetViewportStatusIfEmpty(entry.statusText, builtinPostProcess.warningStatusText); SetViewportStatusIfEmpty(entry.statusText, renderPlan.warningStatusText);
} }
} }
@@ -695,25 +702,7 @@ private:
return false; return false;
} }
ApplySceneViewportRenderRequestSetup( ApplySceneViewportRenderPlan(entry.renderTargets, sceneState.renderPlan, requests[0]);
entry.renderTargets,
&sceneState.builtinPostProcess,
nullptr,
requests[0]);
SceneViewportOverlayFrameData renderOverlayFrameData = sceneState.editorOverlayFrameData;
AppendSceneViewportOverlayFrameData(
renderOverlayFrameData,
BuildSceneViewTransientTransformGizmoOverlayFrameData());
Rendering::RenderPassSequence overlayPassSequence = {};
if (renderOverlayFrameData.HasOverlayPrimitives()) {
overlayPassSequence.AddPass(
CreateSceneViewportEditorOverlayPass(
m_sceneViewportEditorOverlayRenderer,
renderOverlayFrameData));
requests[0].overlayPasses = &overlayPassSequence;
}
requests[0].hasClearColorOverride = true;
requests[0].clearColorOverride = Math::Color(0.27f, 0.27f, 0.27f, 1.0f);
if (!m_sceneRenderer->Render(requests)) { if (!m_sceneRenderer->Render(requests)) {
ApplyViewportRenderFailure( ApplyViewportRenderFailure(

View File

@@ -1,5 +1,7 @@
#include <gtest/gtest.h> #include <gtest/gtest.h>
#include "Viewport/SceneViewportEditorOverlayData.h"
#include "Viewport/SceneViewportRenderPlan.h"
#include "Viewport/ViewportHostRenderFlowUtils.h" #include "Viewport/ViewportHostRenderFlowUtils.h"
#include <memory> #include <memory>
@@ -8,16 +10,21 @@
namespace { namespace {
using XCEngine::Editor::ApplySceneViewportRenderRequestSetup; using XCEngine::Editor::ApplySceneViewportRenderRequestSetup;
using XCEngine::Editor::ApplySceneViewportRenderPlan;
using XCEngine::Editor::ApplyViewportFailureStatus; using XCEngine::Editor::ApplyViewportFailureStatus;
using XCEngine::Editor::BuildGameViewportRenderFailurePolicy; using XCEngine::Editor::BuildGameViewportRenderFailurePolicy;
using XCEngine::Editor::BuildSceneViewportBuiltinPostProcess; using XCEngine::Editor::BuildSceneViewportBuiltinPostProcess;
using XCEngine::Editor::BuildSceneViewportRenderPlan;
using XCEngine::Editor::BuildSceneViewportRenderFailurePolicy; using XCEngine::Editor::BuildSceneViewportRenderFailurePolicy;
using XCEngine::Editor::BuildViewportRenderTargetUnavailablePolicy; using XCEngine::Editor::BuildViewportRenderTargetUnavailablePolicy;
using XCEngine::Editor::GameViewportRenderFailure; using XCEngine::Editor::GameViewportRenderFailure;
using XCEngine::Editor::MarkGameViewportRenderSuccess; using XCEngine::Editor::MarkGameViewportRenderSuccess;
using XCEngine::Editor::MarkSceneViewportRenderSuccess; using XCEngine::Editor::MarkSceneViewportRenderSuccess;
using XCEngine::Editor::SceneViewportOverlayFrameData;
using XCEngine::Editor::SceneViewportOverlayLinePrimitive;
using XCEngine::Editor::SceneViewportRenderFailure; using XCEngine::Editor::SceneViewportRenderFailure;
using XCEngine::Editor::SceneViewportOverlayData; using XCEngine::Editor::SceneViewportOverlayData;
using XCEngine::Editor::SceneViewportRenderPlan;
using XCEngine::Editor::ViewportRenderTargets; using XCEngine::Editor::ViewportRenderTargets;
using XCEngine::RHI::Format; using XCEngine::RHI::Format;
using XCEngine::RHI::RHIResourceView; using XCEngine::RHI::RHIResourceView;
@@ -92,6 +99,20 @@ SceneViewportOverlayData CreateValidOverlay() {
return overlay; return overlay;
} }
SceneViewportOverlayFrameData CreateOverlayFrameDataWithLine(
const SceneViewportOverlayData& overlay,
const XCEngine::Math::Vector3& start,
const XCEngine::Math::Vector3& end) {
SceneViewportOverlayFrameData frameData = {};
frameData.overlay = overlay;
SceneViewportOverlayLinePrimitive& line = frameData.worldLines.emplace_back();
line.startWorld = start;
line.endWorld = end;
line.color = XCEngine::Math::Color::White();
return frameData;
}
TEST(ViewportRenderFlowUtilsTest, BuildFailurePoliciesExposeExpectedStatusAndClearBehavior) { TEST(ViewportRenderFlowUtilsTest, BuildFailurePoliciesExposeExpectedStatusAndClearBehavior) {
const auto targetUnavailable = BuildViewportRenderTargetUnavailablePolicy(); const auto targetUnavailable = BuildViewportRenderTargetUnavailablePolicy();
EXPECT_STREQ(targetUnavailable.statusText, "Viewport render target is unavailable"); EXPECT_STREQ(targetUnavailable.statusText, "Viewport render target is unavailable");
@@ -293,6 +314,80 @@ TEST(ViewportRenderFlowUtilsTest, ApplySceneRenderRequestSetupPreservesBuiltinGr
EXPECT_TRUE(request.objectId.IsRequested()); EXPECT_TRUE(request.objectId.IsRequested());
} }
TEST(ViewportRenderFlowUtilsTest, BuildSceneViewportRenderPlanCollectsBuiltinAndOverlayPasses) {
DummyResourceView objectIdShaderView(ResourceViewType::ShaderResource);
ViewportRenderTargets targets = {};
targets.objectIdShaderView = &objectIdShaderView;
const SceneViewportOverlayData overlay = CreateValidOverlay();
const SceneViewportOverlayFrameData editorOverlayFrameData =
CreateOverlayFrameDataWithLine(
overlay,
XCEngine::Math::Vector3::Zero(),
XCEngine::Math::Vector3::Right());
const SceneViewportOverlayFrameData transientOverlayFrameData =
CreateOverlayFrameDataWithLine(
overlay,
XCEngine::Math::Vector3::Zero(),
XCEngine::Math::Vector3::Up());
size_t factoryCallCount = 0u;
size_t combinedWorldLineCount = 0u;
const auto result = BuildSceneViewportRenderPlan(
targets,
overlay,
{ 7u, 11u },
editorOverlayFrameData,
transientOverlayFrameData,
[&factoryCallCount, &combinedWorldLineCount](const SceneViewportOverlayFrameData& frameData) {
++factoryCallCount;
combinedWorldLineCount = frameData.worldLines.size();
return std::make_unique<NoopRenderPass>();
},
false);
EXPECT_TRUE(result.plan.builtinPostProcess.IsRequested());
EXPECT_EQ(result.plan.overlayPasses.GetPassCount(), 1u);
EXPECT_EQ(factoryCallCount, 1u);
EXPECT_EQ(combinedWorldLineCount, 2u);
EXPECT_EQ(result.warningStatusText, nullptr);
}
TEST(ViewportRenderFlowUtilsTest, ApplySceneViewportRenderPlanAttachesPlannedPassesAndClearState) {
DummyResourceView depthView(ResourceViewType::DepthStencil, Format::D24_UNorm_S8_UInt);
DummyResourceView objectIdView(ResourceViewType::RenderTarget);
DummyResourceView objectIdShaderView(ResourceViewType::ShaderResource);
ViewportRenderTargets targets = {};
targets.width = 800;
targets.height = 600;
targets.depthView = &depthView;
targets.objectIdView = &objectIdView;
targets.objectIdShaderView = &objectIdShaderView;
SceneViewportRenderPlan plan = {};
plan.builtinPostProcess.gridPassData.valid = true;
plan.postScenePasses.AddPass(std::make_unique<NoopRenderPass>());
plan.overlayPasses.AddPass(std::make_unique<NoopRenderPass>());
plan.clearColorOverride = XCEngine::Math::Color(0.1f, 0.2f, 0.3f, 1.0f);
XCEngine::Rendering::CameraRenderRequest request = {};
request.surface = RenderSurface(800, 600);
request.surface.SetRenderArea(XCEngine::Math::RectInt(10, 20, 300, 200));
ApplySceneViewportRenderPlan(targets, plan, request);
EXPECT_EQ(request.postScenePasses, &plan.postScenePasses);
EXPECT_EQ(request.overlayPasses, &plan.overlayPasses);
EXPECT_TRUE(request.objectId.IsRequested());
EXPECT_TRUE(request.builtinPostProcess.IsRequested());
EXPECT_TRUE(request.hasClearColorOverride);
EXPECT_FLOAT_EQ(request.clearColorOverride.r, 0.1f);
EXPECT_FLOAT_EQ(request.clearColorOverride.g, 0.2f);
EXPECT_FLOAT_EQ(request.clearColorOverride.b, 0.3f);
}
TEST(ViewportRenderFlowUtilsTest, MarkSceneRenderSuccessMovesTargetsToShaderResourceState) { TEST(ViewportRenderFlowUtilsTest, MarkSceneRenderSuccessMovesTargetsToShaderResourceState) {
DummyResourceView depthView(ResourceViewType::DepthStencil, Format::D24_UNorm_S8_UInt); DummyResourceView depthView(ResourceViewType::DepthStencil, Format::D24_UNorm_S8_UInt);
DummyResourceView objectIdView(ResourceViewType::RenderTarget); DummyResourceView objectIdView(ResourceViewType::RenderTarget);