231 lines
7.3 KiB
Markdown
231 lines
7.3 KiB
Markdown
# 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)
|