Files
XCEngine/docs/api/_guides/Editor/SceneView-Interaction-And-Gizmo-Model.md

231 lines
7.3 KiB
Markdown
Raw Normal View History

2026-04-04 00:41:13 +08:00
# SceneView Interaction And Gizmo Model
## 为什么要单独讲这件事
`SceneViewPanel` 现在已经不是一个“把 ImGui 按钮和 gizmo 画上去”的薄面板。
按当前实现,它至少同时在协调四件事:
- Scene toolbar 状态
- 选择集到 gizmo context 的映射
- overlay 命中测试
- Scene View 最终 GPU overlay 渲染
如果只看 [SceneViewPanel](../../XCEngine/Editor/panels/SceneViewPanel/SceneViewPanel.md) 类页,很容易知道“它能做什么”,但不容易看清“为什么这样分层”。这篇 guide 就是把这套模型拆开讲清楚。
## 第一层:工具栏状态不是装饰,而是变换语义
当前 Scene toolbar 只有两组切换:
- `Pivot / Center`
- `Global / Local`
但它们都直接影响 gizmo 的数学上下文。
### `Pivot / Center`
`Pivot` 模式下:
- 以 primary selected object 的 transform 世界位置作为 pivot。
`Center` 模式下:
- 先为每个选中对象求一个中心点
- 再把这些中心点取平均
这里的“中心点”采用的是商业编辑器里很常见的一种折中策略:
- 若对象有有效 mesh则用 mesh bounds center 变换到世界空间
- 若没有 mesh则退回 transform 位置
### `Global / Local`
`Global` 模式下:
- gizmo 轴向直接对齐世界坐标轴
`Local` 模式下:
- gizmo 轴向来自 primary selected object 的稳定世界旋转
当前实现不是简单读一个缓存好的 world rotation 字段,而是沿父链把 local rotation 连乘得到最终朝向,因此 Scene View 表达的是层级真实结果,而不是某个孤立节点的局部姿态。
## 第二层:选择集先被整理成统一的 gizmo state
`SceneViewPanel` 不会直接把 `SelectionManager` 的原始结果塞给 move / rotate / scale gizmo。
它会先构造一个 `SceneViewportSelectionGizmoState`,里面至少整理出:
- primary object
- selected objects
- pivot world position
- primary world rotation
这一步的价值在于:
- 多选与单选后面的代码能走统一上下文
- pivot / center 的差异被提前收口
- local / global 的差异也被提前收口
这就是商业工具里常见的“先做 selection state normalization再做 gizmo evaluation”思路。
## 第三层Scene View 不是只有一份静态 overlay
当前最容易误解的地方,是把所有 overlay 都当成一回事。
按现在的实现Scene View 至少涉及三块不同性质的数据:
### 1. 相机 overlay 数据
这是 `GetSceneViewOverlayData()` 返回的前端数学上下文,包含:
- camera position / forward / right / up
- FOV
- clip plane
- orbit distance
它给 gizmo 投影、HUD 和 orientation gizmo 使用,但它还不是最终 overlay frame data。
### 2. 当前帧 gizmo state
这是 `SceneViewPanel` 每帧通过 `SetSceneViewTransformGizmoOverlayState(...)` 写回宿主服务的数据。它保存的是:
- 当前是否显示 move / rotate / scale gizmo
- 各自的 draw data
- 对应 entity id
这份数据会在 `BeginFrame()` 时清空,所以它是逐帧输入,不是长期缓存。
### 3. 合成后的 overlay frame data
这是 `ViewportHostService::GetSceneViewEditorOverlayFrameData(...)` 返回的缓存结果。它会把:
- world overlay provider 输出
- scene icon
- 相机视锥
- 灯光辅助线
- 当前帧 gizmo state 对应的 screen triangle 和 handle record
合成到同一份 `SceneViewportOverlayFrameData` 里。
## 第四层:命中测试不是直接点按钮,而是候选比较
当前 Scene View 的 hover / click 解析是候选比较模型,不是“第一个命中就赢”。
面板会先收集不同来源的候选:
- overlay handle 命中
- orientation gizmo 命中
然后统一比较:
- `priority`
- `distanceSq`
- `depth`
这比“谁后画谁赢”稳定得多,也更适合后续继续扩展新的 gizmo handle。
## 当前优先级顺序
按当前代码里的 priority 常量,大致顺序是从高到低:
1. `Scale uniform` `333`
2. `Scale axis cap` `332`
3. `Scale axis line` `331`
4. `Move axis` `322`
5. `Move plane` `321`
6. `Rotate axis` `311`
7. `Orientation gizmo` `200`
8. `Scene icon` `100`
这组数字说明了当前设计取向:
- transform 编辑动作优先
- 视角切换次之
- scene icon 选择再次之
## 第五层scene icon 选择为什么先于 object-id picking
当前 Scene View 选择路径并不是只有一条。
### overlay 路径
如果点中的是 scene icon
- 直接用 overlay hit 结果拿到 `entityId`
- 立刻设置选中对象
### object-id 路径
如果没有任何 overlay 命中:
- 才回退到 `PickSceneViewEntity(...)`
- 走 object-id readback
这样做的好处很直接:
- 点击相机图标、灯光图标时不会穿透到背后的模型
- 编辑器专属交互优先于场景几何 picking
## 第六层:为什么 hit-test 和最终渲染现在共用一份 frame data
旧模型里很容易把“交互 overlay”和“最终渲染 overlay”拆成两套 API。当前实现改成了另一种方式
1. 面板先提交当前帧 gizmo state
2. 宿主服务按需重建合成 frame data
3. 命中测试消费这份 frame data
4. 若导航或拖拽导致 gizmo state 变化,面板会再次提交 state
5. 后续 `RenderRequestedViewports(...)` 再按最新 state 重建 frame data并交给 `SceneViewportEditorOverlayPass`
这套设计的含义是:
- 面板只负责“告诉宿主这帧 gizmo 长什么样”
- 宿主负责把它和 world overlay 合成成统一 frame data
- 命中测试和最终 GPU overlay pass 围绕同一份语义展开
这样做减少了双通道协议漂移,也避免 `SceneViewPanel` 自己管理 overlay 缓存和 GPU pass 资源。
## 如果你要继续扩展 Scene View应该从哪层下手
### 想加一种新的 transform handle
优先顺序通常是:
1. 在 gizmo draw data 里定义新 handle
2.`SceneViewportOverlayHandleBuilder` 里为它写 handle record
3. 给它分配合理 priority
4.`SceneViewPanel` 的候选映射里接上点击与拖拽语义
### 想加一种新的 scene icon
优先顺序通常是:
1. 先接入 world overlay provider如果属于默认 Scene View 语义,再注册进默认 provider registry
2. 再决定它是否参与 hit-test
3. 最后再决定点击后是选中、聚焦还是别的动作
### 想改 pivot / center 规则
不要直接去 gizmo 类里改。
更正确的入口是:
- `BuildSceneViewportSelectionGizmoState(...)`
因为那一层才是“选择集语义 -> gizmo context 语义”的统一变换点。
## 当前限制
- orbit 手势在控制器层有能力,但面板层还没有完整接线
- `Center` 模式当前依赖 mesh bounds可视中心不是严格的包围盒聚合系统
- `Transform` 组合工具当前仍偏 move / rotate 主导scale 只暴露 `uniformOnly` 语义
## 推荐连读
1. [SceneViewPanel](../../XCEngine/Editor/panels/SceneViewPanel/SceneViewPanel.md)
2. [IViewportHostService](../../XCEngine/Editor/Viewport/IViewportHostService/IViewportHostService.md)
3. [ViewportHostService](../../XCEngine/Editor/Viewport/ViewportHostService/ViewportHostService.md)
4. [SceneViewportMoveGizmo](../../XCEngine/Editor/Viewport/SceneViewportMoveGizmo/SceneViewportMoveGizmo.md)
5. [SceneViewportRotateGizmo](../../XCEngine/Editor/Viewport/SceneViewportRotateGizmo/SceneViewportRotateGizmo.md)
6. [SceneViewportScaleGizmo](../../XCEngine/Editor/Viewport/SceneViewportScaleGizmo/SceneViewportScaleGizmo.md)