diff --git a/docs/plan/Unity式SceneView_Gizmo系统完整审查与正式化重构方案.md b/docs/plan/Unity式SceneView_Gizmo系统完整审查与正式化重构方案.md new file mode 100644 index 00000000..dd1a2e08 --- /dev/null +++ b/docs/plan/Unity式SceneView_Gizmo系统完整审查与正式化重构方案.md @@ -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 A:Runtime Render Capability(属于 `engine`) + +负责“能画什么”: +- scene geometry 渲染 +- object id 渲染 +- render pass 扩展点 +- 通用 full-screen / line / sprite / mesh / debug primitive 渲染能力 +- 后端抽象、资源绑定、shader 编译与执行 + +### Layer B:SceneView Gizmo Orchestration(属于 `editor`) + +负责“为什么画、何时画、画哪些”: +- SceneView grid +- selection outline +- camera / light / helper gizmo +- transform gizmo +- Scene icon +- gizmo subset 调度 +- 交互 handle 组织与命中规则 + +### Layer C:HUD 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”稳定得多。 + diff --git a/editor/src/Viewport/SceneViewportRenderPlan.h b/editor/src/Viewport/SceneViewportRenderPlan.h new file mode 100644 index 00000000..e7e4ecc3 --- /dev/null +++ b/editor/src/Viewport/SceneViewportRenderPlan.h @@ -0,0 +1,96 @@ +#pragma once + +#include "Passes/SceneViewportEditorOverlayPass.h" +#include "SceneViewportEditorOverlayData.h" +#include "ViewportHostRenderFlowUtils.h" + +#include +#include + +#include +#include +#include +#include + +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(const SceneViewportOverlayFrameData&)>; + +struct SceneViewportRenderPlanBuildResult { + SceneViewportRenderPlan plan = {}; + const char* warningStatusText = nullptr; +}; + +inline SceneViewportRenderPlanBuildResult BuildSceneViewportRenderPlan( + const ViewportRenderTargets& targets, + const SceneViewportOverlayData& overlay, + const std::vector& 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 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 diff --git a/editor/src/Viewport/ViewportHostService.h b/editor/src/Viewport/ViewportHostService.h index 7590c075..91e5242a 100644 --- a/editor/src/Viewport/ViewportHostService.h +++ b/editor/src/Viewport/ViewportHostService.h @@ -9,6 +9,7 @@ #include "SceneViewportEditorOverlayData.h" #include "SceneViewportOverlayHandleBuilder.h" #include "SceneViewportOverlayBuilder.h" +#include "SceneViewportRenderPlan.h" #include "ViewportHostRenderFlowUtils.h" #include "ViewportHostRenderTargets.h" #include "ViewportObjectIdPicker.h" @@ -417,9 +418,7 @@ private: struct SceneViewportRenderState { SceneViewportOverlayData overlay = {}; - Rendering::BuiltinPostProcessRequest builtinPostProcess = {}; - SceneViewportOverlayFrameData editorOverlayFrameData = {}; - std::vector selectedObjectIds; + SceneViewportRenderPlan renderPlan = {}; }; ViewportEntry& GetEntry(EditorViewportKind kind) { @@ -641,17 +640,25 @@ private: return; } - outState.selectedObjectIds = context.GetSelectionManager().GetSelectedEntities(); - outState.editorOverlayFrameData = GetSceneViewEditorOverlayFrameData(context); - const SceneViewportBuiltinPostProcessBuildResult builtinPostProcess = - BuildSceneViewportBuiltinPostProcess( + const std::vector selectedObjectIds = context.GetSelectionManager().GetSelectedEntities(); + const SceneViewportOverlayFrameData& editorOverlayFrameData = + GetSceneViewEditorOverlayFrameData(context); + SceneViewportRenderPlanBuildResult renderPlan = + BuildSceneViewportRenderPlan( + entry.renderTargets, outState.overlay, - outState.selectedObjectIds, - entry.renderTargets.objectIdShaderView != nullptr, + selectedObjectIds, + editorOverlayFrameData, + BuildSceneViewTransientTransformGizmoOverlayFrameData(), + [this](const SceneViewportOverlayFrameData& frameData) { + return CreateSceneViewportEditorOverlayPass( + m_sceneViewportEditorOverlayRenderer, + frameData); + }, kDebugSceneSelectionMask); - outState.builtinPostProcess = builtinPostProcess.request; - if (builtinPostProcess.warningStatusText != nullptr) { - SetViewportStatusIfEmpty(entry.statusText, builtinPostProcess.warningStatusText); + outState.renderPlan = std::move(renderPlan.plan); + if (renderPlan.warningStatusText != nullptr) { + SetViewportStatusIfEmpty(entry.statusText, renderPlan.warningStatusText); } } @@ -695,25 +702,7 @@ private: return false; } - ApplySceneViewportRenderRequestSetup( - 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); + ApplySceneViewportRenderPlan(entry.renderTargets, sceneState.renderPlan, requests[0]); if (!m_sceneRenderer->Render(requests)) { ApplyViewportRenderFailure( diff --git a/tests/editor/test_viewport_render_flow_utils.cpp b/tests/editor/test_viewport_render_flow_utils.cpp index b3f28d18..46aa4547 100644 --- a/tests/editor/test_viewport_render_flow_utils.cpp +++ b/tests/editor/test_viewport_render_flow_utils.cpp @@ -1,5 +1,7 @@ #include +#include "Viewport/SceneViewportEditorOverlayData.h" +#include "Viewport/SceneViewportRenderPlan.h" #include "Viewport/ViewportHostRenderFlowUtils.h" #include @@ -8,16 +10,21 @@ namespace { using XCEngine::Editor::ApplySceneViewportRenderRequestSetup; +using XCEngine::Editor::ApplySceneViewportRenderPlan; using XCEngine::Editor::ApplyViewportFailureStatus; using XCEngine::Editor::BuildGameViewportRenderFailurePolicy; using XCEngine::Editor::BuildSceneViewportBuiltinPostProcess; +using XCEngine::Editor::BuildSceneViewportRenderPlan; using XCEngine::Editor::BuildSceneViewportRenderFailurePolicy; using XCEngine::Editor::BuildViewportRenderTargetUnavailablePolicy; using XCEngine::Editor::GameViewportRenderFailure; using XCEngine::Editor::MarkGameViewportRenderSuccess; using XCEngine::Editor::MarkSceneViewportRenderSuccess; +using XCEngine::Editor::SceneViewportOverlayFrameData; +using XCEngine::Editor::SceneViewportOverlayLinePrimitive; using XCEngine::Editor::SceneViewportRenderFailure; using XCEngine::Editor::SceneViewportOverlayData; +using XCEngine::Editor::SceneViewportRenderPlan; using XCEngine::Editor::ViewportRenderTargets; using XCEngine::RHI::Format; using XCEngine::RHI::RHIResourceView; @@ -92,6 +99,20 @@ SceneViewportOverlayData CreateValidOverlay() { 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) { const auto targetUnavailable = BuildViewportRenderTargetUnavailablePolicy(); EXPECT_STREQ(targetUnavailable.statusText, "Viewport render target is unavailable"); @@ -293,6 +314,80 @@ TEST(ViewportRenderFlowUtilsTest, ApplySceneRenderRequestSetupPreservesBuiltinGr 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(); + }, + 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()); + plan.overlayPasses.AddPass(std::make_unique()); + 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) { DummyResourceView depthView(ResourceViewType::DepthStencil, Format::D24_UNorm_S8_UInt); DummyResourceView objectIdView(ResourceViewType::RenderTarget);