简体中文
简体中文
编辑器导航控件的设计与实现

编辑器导航控件的设计与实现

technical
Jujie Xu
June 21, 20239 min read

控制相机是三维场景中基础而普遍的需求,如展示类项目中控制相机围绕物品全方位观察,游戏类项目中操控相机以第一人称视角移动。常见的相机控件有 orbitControl ,绕场景中某个锚点控制相机移动;freeControl,相机以自身为锚点进行旋转移动。 在编辑器中,除以上两种通过键盘鼠标配合控制相机的控件外,可视化的相机控件 - 导航控件必不可少。作为视图导航的一种固定方式,它能 (1) 直观的展示场景中相机的方向,(2) 方便的切换到固定视角,如顶视图、侧视图等三视图,并且(3) 仅需单键点击就能操作相机,极大降低了普通用户的使用门槛。下文中我们将针对导航控件的这三种主要功能进行拆解。

gif

多相机

实现的第一步,需要考虑如何将导航控件显示在画面中。根据构想,导航控件和主画面属于同一场景,相机信息上同步,操作上互不干扰。类似于游戏中略缩地图、装备总览。一般以多相机的思路进行实现。即在场景中添加另一相机,将该导航控件与这一相机的 cullingmask (裁剪遮罩,用来选择性地渲染场景中的渲染组件) 放置在同一layer。

同步相机方向

构想中,导航控件功能是能同步场景相机的方向,也能对相机进行操控。

第一步,如何同步场景相机的方向?很自然的想到是将相机的 rotationQuaternion 赋予导航控件。但如此实现后,会发现和预期完全不同。让我们重新思考,导航控件的三个轴到底展示的是什么?想象场景在原点有一个代表 XYZ 方向的轴,移动场景相机可以观察到该坐标轴的相对变化,这才是约定俗成的导航控件要展示的内容,而非场景相机自身方向。

img

因此,场景相机和导航控件的worldMatrix应该是互逆的。场景相机因为相机特性,自身具有 viewMatrix,从定义上与场景相机实体的 worldMatrix 互逆,即与控件的 worldMatrix 相同。

Mgizmo=Mview=McameraEntity1M_{gizmo} = M_{view} = M_{cameraEntity}^{-1}

而导航控件的位置是固定的,没有位置变化。用矩阵的语言表示,就是在齐次坐标中表示位置变化的最后一列的 tx,ty,tzt_x,t_y,t_z 可以置为零。

example.ts
tempMat.copyFrom(sceneCamera.viewMatrix);
const { elements: ele } = tempMat;
// ignore translate
ele[12] = ele[13] = ele[14] = 0;
gizmoEntity.transform.worldMatrix = tempMat;

切换三视图

导航控件另一主要功能是控制场景相机切换到对应的三视图视角。值得注意的是,三视图从定义上而言,不是仅扭转相机自身的方向向上向侧观看,而是也要绕观察物体移动到相应的方位上。想象一个以观察对象为中心的球体,我们需要的是控制场景相机在这个球上移动到指定位置,并且扭转朝向。

navigatorDiagram3.png

那么如何获得特定三视图对应的场景相机worldMatrix呢?一般而言,matrixlookAt方法,获得的lookAtMatrix即为相机的viewMatrix:

Matrix.lookAt(eye, target, upVec, viewMat);

那么其中各个分量targeteyeupVec如何获得?

  • target 即看向的对象。在场景中,有什么因素会改变场景相机的 target ?除主动更改外,这里就要考虑在该场景相机上是否有 orbitControl 组件了。orbitControl有三种操作 orbitzoompan,其中orbitzoom虽然改变了相机位置,但都围绕着同一中心进行,而pan则会在移动相机位置的同时改变lookAttarget。因此导航控件需要分同一相机上有无orbitControl组件两种情况进行讨论:如果有orbitControl就调用其target;如果没有则需要用户指定target
  • eye 即场景相机期望的位置。确定了特定三视图的方向后,通过 targetforwardVectorradius 组合计算得到。
  • upVector 一般为(0,1,0),顶视图和底实图建议使用(0,0,1)或(0,0,-1)
// target
// 场景相机有 orbitControl 控件
const target = orbitControl.target 
// 场景相机无 orbitControl 控件, 指定位置
const target = new Vector3(...);
 
// eye
Vector3.subtract(sceneCameraEntity.transform.worldPosition, target, tempVect);
const radius = tempVect.length();
// 根据需要三视图获得方向,以右视图为例
const directVec = new Vector3(0,radius,0)
Vector3.add(directVec, target, eye);
 
// up direction
const upVec = new Vector3(0,1,0)
 
// 获得场景相机实体worldMatrix
Matrix.lookAt(eye, target, upVec, tempMat);
 
tempMat.invert()
sceneCameraEntity.transform.worldMatrix = tempMat;

拖拽操控相机

我们是通过在屏幕上鼠标的拖拽来改变场景相机的方位,鼠标的移动是二维的,而相机则是在三维空间中产生了变化,如何通过二维的交互来操控三维场景呢? 正如上文想象相机是在一个球面上移动,一个思路是拖拽球体表面,从而改变相机方位。这个思路直观,但有明显的缺点,因为是在屏幕二维空间操作,每次拖拽球体很难转过180度到达背面;并且涉及球体计算复杂。

navigatorDiagram2.png

我们采用的思路是,把拖拽分解为横轴与纵轴两个方向,屏幕空间 x 方向上的差值对应为绕着纵轴转过的角度,屏幕空间 y 方向上的差值对应为绕着横轴轴转过的角度。在世界空间中,纵轴即为 y 轴方向,横轴为与场景相机平行的方向。这样的对应即能达到拖拽相机的效果,并且可以通过增加系数控制转速。

// 分解为屏幕空间中x、y方向上每帧移动差值
let x = deltaPointer.x 
let y = deltaPointer.y 
 
// 计算得出横轴
horizontalAxis.copyFrom(sceneCameraEntity.transform.worldForward);
Vector3.cross(horizontalAxis, upVec, horizontalAxis);
 
// 获得对应旋转角度
Quaternion.rotationAxisAngle(horizontalAxis, y, tempQuat);
Quaternion.rotationYawPitchRoll(x, 0, 0, tempQuat2);
Quaternion.multiply(tempQuat, tempQuat2, tempQuat);

小结

在业务场景中,该导航组件可视化程度高、无使用门槛,可以作为用户操作轴来控制场景。在实现上,可以为其他需要在主画面上显示、信息同步但操作独立的组件提供思路借鉴。在计算上,涵盖了相机最常使用的lookAt MatrixviewMatrix,以及相机实体的特殊性质,从而帮助引擎使用者更好的理解相机。

源码地址: https://github.com/galacean/engine-toolkit
导航示例: https://galacean.antgroup.com/#/examples/latest/gizmo
多相机示例: https://galacean.antgroup.com/#/examples/latest/multi-camera

Galacean Logo
Make fantastic web apps with the prospective
technologies and tools.
Copyright © 2024 Galacean
All rights reserved.