一、差异
threejs中默认的控制器存在的问题
- 极易穿墙,不容观察mesh的细节
- 缩放过多时,再次缩放幅度非常小,导致看起来【失灵】了,用户没有感知
作为比较,xeokit交互的优势明显,可以非常近距离观察到mesh面的细节,同时无论怎么缩放,都不会出现距离过小的问题
二、原因分析
我们来看下three.js中的轨道控制器缩放相关核心代码实现
function update( ) {
//省略其它代码
if ( scope.object.isPerspectiveCamera ) {
// move the camera down the pointer ray
// this method avoids floating point error
const prevRadius = offset.length(); // 当前相机与目标点的距离
newRadius = clampDistance( prevRadius * scale ); //scale是一个定值,默认是缩小0.95/放大1.05
const radiusDelta = prevRadius - newRadius; // 移动的距离
scope.object.position.addScaledVector( dollyDirection, radiusDelta ); //相机位置
scope.object.updateMatrixWorld(); //更新矩阵
zoomChanged = !! radiusDelta;
}
//省略其它代码
}
从代码中可以看到缩放的核心逻辑
- 首先计算相机与目标点的距离
- 然后把距离缩放一个固定的倍数
- 把相机移动新距离和旧距离的差值
- 更新相机矩阵,应用新的相机位置,实现缩放
这里就可以看到问题的核心在于缩放的效果完全取决于相机与目标点的距离,并且相机永远不能越过目标点
所以当放大时,就会出现非常容易越过网格面,不能精细观察;并且当放大到距离目标点非常近距离时,再次放大,相机移动距离无限接近0,导致看起来没有动的效果。
三、解决方案
我们看xeokit的缩放这么丝滑,是如何做到的呢?
虽然xeokit也是对webgl的封装,但是一些方法会存在比较大的差异,甚至一些数学库的方法实现也会不同,请务必查看其对应实现,确保进行相同的计算
直接看代码实现逻辑
xeokit-sdk/src/viewer/scene/CameraControl/lib/controllers/PanController.js at master · xeokit/xeokit
dollyToCanvasPos(optionalTargetWorldPos, targetCanvasPos, dollyDelta) {
let dolliedThroughSurface = false;
const camera = this._scene.camera;
if (optionalTargetWorldPos) {
//
const eyeToWorldPosVec = math.subVec3(optionalTargetWorldPos, camera.eye, tempVec3a);
const eyeWorldPosDist = math.lenVec3(eyeToWorldPosVec);
dolliedThroughSurface = (eyeWorldPosDist < dollyDelta);
}
if (camera.projection === "perspective") {
// 鼠标点转换到世界空间中
const unprojectedWorldPos = this._unproject(targetCanvasPos, tempVec4a);
//计算目标画布位置对应的世界坐标
const offset = math.subVec3(unprojectedWorldPos, camera.eye, tempVec4c);
// 计算应该位移的向量
const moveVec = math.mulVec3Scalar(math.normalizeVec3(offset), -dollyDelta, []);
//相机和目标点一起移动
camera.eye = [camera.eye[0] - moveVec[0], camera.eye[1] - moveVec[1], camera.eye[2] - moveVec[2]];
camera.look = [camera.look[0] - moveVec[0], camera.look[1] - moveVec[1], camera.look[2] - moveVec[2]];
if (optionalTargetWorldPos) {
// Subtle UX tweak - if we have a target World position, then set camera eye->look distance to
// the same distance as from eye->target. This just gives us a better position for look,
// if we subsequently orbit eye about look, so that we don't orbit a position that's
// suddenly a lot closer than the point we pivoted about on the surface of the last object
// that we click-drag-pivoted on.
const eyeTargetVec = math.subVec3(optionalTargetWorldPos, camera.eye, tempVec3a);
const lenEyeTargetVec = math.lenVec3(eyeTargetVec);
// 保持世界空间的点到相机距离
const eyeLookVec = math.mulVec3Scalar(math.normalizeVec3(math.subVec3(camera.look, camera.eye, tempVec3b)), lenEyeTargetVec);
camera.look = [camera.eye[0] + eyeLookVec[0], camera.eye[1] + eyeLookVec[1], camera.eye[2] + eyeLookVec[2]];
}
} else if (camera.projection === "ortho") {
//...
}
return dolliedThroughSurface;
}
它的核心逻辑在于dollyToCanvas这个函数, 它接受三个参数:
optionalTargetWorldPos: 可选的目标世界坐标位置targetCanvasPos: 目标在画布上的2D位置dollyDelta: 推拉的距离值
完整的执行逻辑:
-
首先检查是否已经推过了目标表面
计算相机位置到目标世界位置的向量,如果距离小于dollyDelta,说明已经推过了表面,则会返回true标识下一次需要更新目标世界坐标位置
实际上还会在其它情况下去更新这个目标世界坐标,比如进行了旋转、鼠标移动了较大的篇幅
-
计算相机移动的距离
-
计算目标画布位置对应的世界坐标
-
计算移动向量
-
更新相机位置和目标点look位置 (camera.eye 相当于threejs中的camera.position,look相当于target)
-
调整优化
-
如果有拾取到目标世界坐标,还要额外调整look位置
-
使相机位置到目标世界坐标的距离等于相机位置到目标点的距离
这个调整防止在后续进行轨道旋转(orbit)操作时出现突然的视角跳跃,是一个交互上的优化
-
dollyDelta的计算逻辑
// 计算optionalTargetWorldPos 到相机的距离
if (optionalTargetWorldPos) {
const dist = Math.abs(math.lenVec3(math.subVec3(optionalTargetWorldPos, scene.camera.eye, tempVec3)));
// 距离除以一个常数控制比例
dollyDistFactor = dist / configs.dollyProximityThreshold;
}
// updates.dollyDelta 实际上只是一个方向值
const dollyDelta = (updates.dollyDelta * dollyDistFactor);
整体的计算逻辑实际上和threejs中有些类似,但是额外做了一些优化
-
optionalTargetWorldPos并不会每一次更新都拾取,而是在进行了其它操作如旋转,鼠标位移、越过mesh表面时才会去重新拾取,避免了频繁拾取的计算
-
optionalTargetWorldPos是模型上面的点,所以距离远的时候,缩放的幅度大,越靠近表面,幅度越小,这样就实现了近距离的观察效果,不会大幅移动穿过mesh
-
限定了最小值,越过表面重新拾取计算,这样即可解决了放大到一定的程度后,相机移动距离无限接近0导致操作无效的假象
把整体逻辑理解清楚,可以按着思路优化threejs中的轨道控制器