11 KiB
SceneViewPanel
命名空间: XCEngine::Editor
类型: class
源文件: editor/src/panels/SceneViewPanel.h
描述: 编辑器 Scene View 面板,负责场景视口 UI、工具栏与工具叠层、对象选择、相机导航,以及 transform gizmo 的交互编排与 overlay 接线。
概述
SceneViewPanel 是当前 Editor 里最像商业级场景视图壳层的面板之一。按 SceneViewPanel.cpp 的真实实现,它把以下链路编排到同一帧里:
- 顶部
Pivot / Center与Global / Local切换。 - 左侧
ViewMove / Move / Rotate / Scale / Transform工具条。 - Scene 视口纹理申请与交互表面建立。
- scene icon、orientation gizmo、transform gizmo 与空白区域 picking 的命中仲裁。
- look / pan / fly / focus-selection 输入回传。
- transform gizmo 的绘制、命中、拖拽和撤销事务。
SceneViewPanel 自己不持有 render target,也不直接做 object-id 读回或 SceneRenderer 调用;这些都通过 IViewportHostService 完成。它的职责更准确地说是交互编排层。
生命周期与公开入口
- Constructor
创建标题固定为
"Scene"的面板实例,并保留 Scene View 运行期私有状态。 - Render 执行完整的 Scene View 工具栏、命中仲裁、导航输入、gizmo 拖拽与 overlay 提交流程。
当前帧执行链路
Render() 当前不是简单的 ImGui 绘制,而是按下面的顺序组织 Scene View:
- 绘制顶部 Scene toolbar。
- 调用 ViewportPanelContent 请求
Scene视口纹理并建立交互表面。 - 绘制左侧工具条,并处理
Q/W/E/R快捷键与工具切换。 - 若视口可交互,先读取宿主服务的
SceneViewportOverlayData,刷新 move / rotate / scale gizmo 上下文。 - 通过
BuildSceneViewportTransformGizmoHandleBuildInputs(...)和BuildSceneViewportTransformGizmoOverlayState(...)组装当前帧 gizmo state,并先调用SetSceneViewTransformGizmoOverlayState(...)写回宿主服务。 - 调用
GetSceneViewEditorOverlayFrameData(...)取回一份已经合成好 scene icon、相机/灯光辅助几何和 gizmo handle 的 frame data。 - 对这份 frame data 执行 SceneViewportOverlayHitTester 命中测试,并再和 orientation gizmo 命中做统一仲裁。
- 处理点击、拖拽和导航,然后把
SceneViewportInput回传给宿主服务。 - 视口图像存在时,再次刷新 gizmo,使其对齐本帧更新后的相机与输入状态,并再次调用
SetSceneViewTransformGizmoOverlayState(...),让后续 render path 消费最新 gizmo state。 - 最后绘制前端 HUD / orientation overlay。
这里有个实现细节很重要:gizmo 当前会在一帧里刷新两次。
- 第一次用于交互命中。
- 第二次用于最终 Scene View render path。
这样做的目的,是让导航输入和 gizmo 命中使用同一套几何语义,同时保证最终画出来的 overlay 和本帧相机状态保持一致。
顶部工具栏语义
当前顶部 Scene toolbar 只有两个切换,但它们都会直接改变 gizmo 的数学语义。
Pivot / Center
m_pivotMode 当前有两种值:
PivotCenter
它会改变 BuildSceneViewportSelectionGizmoState(...) 的 pivot 计算:
Pivot- 使用 primary selected object 的 transform 世界位置。
Center- 对所有选中对象求中心点平均值。
这里的“中心点”当前按源码有两级回退:
- 若对象有有效
MeshFilterComponent + Mesh,使用mesh->GetBounds().center经过对象 transform 变换后的世界位置。 - 否则回退到对象 transform 世界位置。
因此 Center 更接近视觉中心,而 Pivot 更接近主对象原点。
Global / Local
m_transformSpaceMode 当前有两种值:
GlobalLocal
它直接影响 gizmo context 中的 axisOrientation:
Global- 使用单位旋转,gizmo 轴始终对齐世界轴。
Local- 使用 primary selected object 的稳定世界旋转。
这里的稳定世界旋转不是直接取单一字段,而是沿父链把 local rotation 连乘得到,再归一化,因此它反映的是当前层级下的真实世界朝向。
primary object 规则
gizmo 使用的 primary object 规则是:
- 优先取
SelectionManager::GetSelectedEntity()。 - 再读取
GetSelectedEntities()构造selectedObjects。 - 若 primary entity 为空,但多选列表非空,则回退到
selectedObjects.back()。 - 若只有 primary,没有多选列表,则把 primary 补回
selectedObjects。
这意味着多选时仍然会有一个主对象,它决定:
Pivot模式的 pivot 原点。Local模式的轴向旋转来源。
命中仲裁与点击顺序
当前 Scene View 不是“点到什么就直接处理什么”,而是先把不同来源的命中结果统一抽象成 SceneViewportInteractionCandidate,再比较优先级、距离和深度。
命中前置条件
只有在以下条件同时满足时,面板才会解析视口交互:
- 视口纹理存在且尺寸有效。
- 鼠标位于 viewport 内容区。
- 当前不是
ViewMove工具。 - 当前不在 look / pan 拖拽中。
- 左侧工具条没有悬停。
- 没有 gizmo 正在活动拖拽。
当前优先级
按当前源码中的 priority 常量,交互优先级从高到低是:
- scale uniform:
333 - scale axis cap:
332 - scale axis line:
331 - move axis:
322 - move plane:
321 - rotate axis:
311 - orientation gizmo:
200 - scene icon:
100 - 空白区域 object-id picking: 兜底路径
这带来几个当前行为:
- transform gizmo 总会压过 orientation gizmo 和 scene icon。
- orientation gizmo 会压过 scene icon。
- 只有没有 overlay handle 命中时,才会退回到普通 object-id picking。
点击结果
左键点击当前有四种分支:
- 命中 transform gizmo
- 调用对应 gizmo 的
TryBeginDrag(...)
- 调用对应 gizmo 的
- 命中 orientation gizmo
AlignSceneViewToOrientationAxis(...)
- 命中 scene icon
- 直接
SetSelectedEntity(entityId)
- 直接
- 都没命中
- 调用
PickSceneViewEntity(...)走 object-id picking,没选中则清空选择
- 调用
scene icon 这一支不依赖 object-id 纹理,而是直接消费 overlay frame data 里的 handle record,因此图标点击优先走 overlay 选择,不和场景几何 picking 混在一起。
gizmo 更新、拖拽与撤销
SceneViewPanel 自己不实现 move / rotate / scale 数学,但负责决定哪一套 gizmo 现在拥有输入权。
当前面板会做这些事情:
- 根据 selection、pivot mode、transform space 和鼠标位置构造 gizmo context。
- 当某一套 gizmo 处于活动状态时,把其他 gizmo 的
mousePosition置成(-1, -1),防止 hover 状态串扰。 - 工具切换、视口失效、selection 变更到其他对象时,主动
CancelDrag(...)。 - 鼠标按住左键时持续
UpdateDrag(...),释放时EndDrag(...)。
撤销事务本身仍由 gizmo 类发起,但面板负责保证事务只被当前活动 gizmo 使用。
相机导航与 Focus Selection
导航模型当前和成熟 Scene View 很接近:
- 右键按住:look
- 右键 look 期间:
WASDQE飞行移动 Shift:快速移动- 中键拖拽:pan
ViewMove工具下左键拖拽:pan- 滚轮
- 普通悬停时:缩放
- 右键 look 时:调整飞行速度
F:发送focusSelectionRequested
真正的 focus 行为由 ViewportHostService 执行:
- 有 primary selection 时聚焦该对象的 transform position。
- 否则聚焦所有根对象位置的平均值。
- 场景为空时聚焦原点。
当前 SceneViewportCameraController 虽然支持 orbit 输入,但 SceneViewPanel 仍把 orbiting 固定为 false,所以完整 orbit 手势还没有在面板层接线。
overlay 接线模型
SceneViewPanel 当前和宿主服务之间的 overlay 接线可以概括成两步:
命中前
- 面板先构造当前帧
SceneViewportTransformGizmoOverlayState - 再调用
SetSceneViewTransformGizmoOverlayState(...) - 然后通过
GetSceneViewEditorOverlayFrameData(...)拿到合成 frame data 做 hit-test
渲染前
- 导航和拖拽处理结束后,面板再次刷新 gizmo state
- 再次调用
SetSceneViewTransformGizmoOverlayState(...) - 后续
ViewportHostService::RenderRequestedViewports(...)会因为 state 重新变脏而重建 overlay frame data,并把它交给 SceneViewportEditorOverlayPass
这意味着当前 transform gizmo overlay 是“面板生成、宿主缓存并渲染、逐帧失效”的状态输入,而不是面板自己持有的一份 GPU pass 数据。
与之对应,DrawSceneViewportHudOverlay(...) 现在只补一层 ImGui HUD / orientation gizmo,不再承担 transform gizmo 主绘制。
设计说明
SceneViewPanel 当前的结构体现出一个比较成熟的编辑器设计取向:
- 面板负责交互编排和 UI 状态。
- 宿主服务负责视口资源、渲染与 picking。
- gizmo 类负责具体变换数学。
- overlay 构建与渲染进一步拆成数据层、命中层、GPU pass 层和 ImGui 层。
这种分层的好处是 Scene View 能持续长大,而不会迅速坍缩成一个几千行、什么都管的大面板类。
生命周期与线程语义
- 该面板依赖有效
IEditorContext和IViewportHostService。 - 所有交互与绘制都应视为 Editor 主线程 / ImGui 帧内行为。
m_moveGizmo、m_rotateGizmo、m_scaleGizmo、导航拖拽标记、工具模式、pivot mode 和 transform space mode 都属于面板实例私有状态。
当前限制
- 选择主路径仍依赖有效 object-id 帧;只有 scene icon 点击走 overlay 直选。
- orbit 手势能力仍未从控制器完整接到面板。
Transform模式下的 scale 只暴露uniformOnly行为。- scale gizmo 当前仍偏单对象语义,多对象缩放没有和 move / rotate 一样展开。