7.4 KiB
SceneView Interaction And Gizmo Model
为什么要单独讲这件事
SceneViewPanel 现在已经不是一个“把 ImGui 按钮和 gizmo 画上去”的薄面板。
按当前实现,它至少同时在协调四件事:
- Scene toolbar 状态
- 选择集到 gizmo context 的映射
- overlay 命中测试
- Scene View 最终 GPU overlay 渲染
如果只看 SceneViewPanel 类页,很容易知道“它能做什么”,但不容易看清“为什么这样分层”。这篇 guide 就是把这套模型拆开讲清楚。
第一层:工具栏状态不是装饰,而是变换语义
当前 Scene toolbar 只有两组切换:
Pivot / CenterGlobal / 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
这是 SceneViewportTransformGizmoCoordinator 每帧通过 RefreshAndSubmitSceneViewportTransformGizmoFrame(...) / SubmitSceneViewportTransformGizmoOverlaySubmission(...) 间接写回宿主服务的数据。它保存的是:
- 当前是否显示 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 命中
然后统一比较:
prioritydistanceSqdepth
这比“谁后画谁赢”稳定得多,也更适合后续继续扩展新的 gizmo handle。
当前优先级顺序
按当前代码里的 priority 常量,大致顺序是从高到低:
Scale uniform333Scale axis cap332Scale axis line331Move axis322Move plane321Rotate axis311Orientation gizmo200Scene icon100
这组数字说明了当前设计取向:
- 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。当前实现改成了另一种方式:
- 面板先提交当前帧 gizmo state
- 宿主服务按需重建合成 frame data
- 命中测试消费这份 frame data
- 若导航或拖拽导致 gizmo state 变化,面板会再次提交 state
- 后续
RenderRequestedViewports(...)再按最新 state 重建 frame data,并交给SceneViewportEditorOverlayPass
这套设计的含义是:
- 面板只负责“告诉宿主这帧 gizmo 长什么样”
- 宿主负责把它和 world overlay 合成成统一 frame data
- 命中测试和最终 GPU overlay pass 围绕同一份语义展开
这样做减少了双通道协议漂移,也避免 SceneViewPanel 自己管理 overlay 缓存和 GPU pass 资源。
如果你要继续扩展 Scene View,应该从哪层下手
想加一种新的 transform handle
优先顺序通常是:
- 在 gizmo draw data 里定义新 handle
- 在
SceneViewportOverlayHandleBuilder里为它写 handle record - 给它分配合理 priority
- 在
SceneViewPanel的候选映射里接上点击与拖拽语义
想加一种新的 scene icon
优先顺序通常是:
- 先接入 world overlay provider;如果属于默认 Scene View 语义,再注册进默认 provider registry
- 再决定它是否参与 hit-test
- 最后再决定点击后是选中、聚焦还是别的动作
想改 pivot / center 规则
不要直接去 gizmo 类里改。
更正确的入口是:
BuildSceneViewportSelectionGizmoState(...)
因为那一层才是“选择集语义 -> gizmo context 语义”的统一变换点。
当前限制
- orbit 手势在控制器层有能力,但面板层还没有完整接线
Center模式当前依赖 mesh bounds,可视中心不是严格的包围盒聚合系统Transform组合工具当前仍偏 move / rotate 主导,scale 只暴露uniformOnly语义