Cesium 相机控制器(1)-wheel 实现原理简析
已经做大量简化, 不是代码最终的样子.
js
Viewer
┖ CesiumWidget
┖ ScreenSpaceCameraController(_screenSpaceCameraController)
┣ CameraEventAggregator(_aggregator) // 相机事件代理
┃ ┖ ScreenSpaceEventHandler(_eventHandler)
┖ TweenCollection
1、注册事件监听器
注册事件 wheel
和 mousemove
js
function registerListener() {
element.addEventListener(domType, handleWheel);
}
// 事件处理器, 提供设置监听者的能力。 分配活的。
class ScreenSpaceEventHandler {
constructor(scene) {
registerListener('"wheel"', handleWheel);
registerListener('"pointermove"', handlePointerMove);
}
}
2、事件监听器的实现
js
function handleWheel (delta) {
const action = screenSpaceEventHandler.getInputAction(
ScreenSpaceEventType.WHEEL,
);
action(delta);
}
function handleMouseMove(screenSpaceEventHandler, event) {
const modifier = getModifier(event);
const position = getPosition();
const previousPosition = screenSpaceEventHandler._primaryPreviousPosition;
const action = screenSpaceEventHandler.getInputAction(
ScreenSpaceEventType.MOUSE_MOVE,
modifier
);
if (defined(action)) {
Cartesian2.clone(previousPosition, mouseMoveEvent.startPosition);
Cartesian2.clone(position, mouseMoveEvent.endPosition);
action(mouseMoveEvent);
}
Cartesian2.clone(position, previousPosition);
}
3、事件监听的执行
js
// 事件代理器, 设置真正的监听者。真正干活的。
class CameraEventAggregator {
constructor() {
listenToWheel(this, undefined);
listenMouseMove(this, undefined);
}
}
const listenToWheel = function (aggregator) {
const key = getKey(CameraEventType.WHEEL);
const update = aggregator._update;
update[key] = true;
let movement = aggregator._movement[key];
let lastMovement = aggregator._lastMovement[key];
movement.startPosition = new Cartesian2();
movement.endPosition = new Cartesian2();
Cartesian2.clone(Cartesian2.ZERO, movement.startPosition);
aggregator._eventHandler.setInputAction(
function (delta) {
const arcLength = 7.5 * CesiumMath.toRadians(delta);
movement.endPosition.x = 0.0;
movement.endPosition.y = arcLength;
Cartesian2.clone(movement.endPosition, lastMovement.endPosition);
lastMovement.valid = true;
update[key] = false;
},
ScreenSpaceEventType.WHEEL,
);
}
const listenMouseMove = function (aggregator) {
const update = aggregator._update;
const movement = aggregator._movement;
const lastMovement = aggregator._lastMovement;
const isDown = aggregator._isDown;
aggregator._eventHandler.setInputAction(
function (mouseMovement) {
Cartesian2.clone(
mouseMovement.endPosition,
aggregator._currentMousePosition
);
},
ScreenSpaceEventType.MOUSE_MOVE,
);
}
可视化查看位置:
js
const e = document.querySelector(".end");
const pos = viewer.scene._screenSpaceCameraController._aggregator._currentMousePosition
setInterval(() => {
e.style.top = pos.y + "px";
e.style.left = pos.x + "px";
}, 10);
4、控制器的更新
js
`Scene` 的 `render`
┖ Scene.prototype.initializeFrame()
┖ this._screenSpaceCameraController.update();
┖ update3D(this);
┖ reactToInput()
┖ zoom3D()
5、zoom3D 实现细节
js
function zoom3D(
controller: ScreenSpaceCameraController,
startPosition: Cartesian2,
movement) {
// 屏幕中心位置
let windowPosition;
windowPosition.x = canvas.clientWidth / 2;
windowPosition.y = canvas.clientHeight / 2;
// 得到射线
const ray = camera.getPickRayPerspective(windowPosition, zoomCVWindowRay);
// 相机的当前高度
let distance = ellipsoid.cartesianToCartographic(
camera.position,
zoom3DCartographic
).height;
const unitPosition = Cartesian3.normalize(
camera.position,
zoom3DUnitPosition
);
handleZoom(
controller,
startPosition,
movement,
controller._zoomFactor,
distance,
Cartesian3.dot(unitPosition, camera.direction)
);
}
6、getPickRayPerspective 实现细节
js
function getPickRayPerspective(camera, windowPosition, result) {
const canvas = camera._scene.canvas;
const width = canvas.clientWidth;
const height = canvas.clientHeight;
const tanPhi = Math.tan(camera.frustum.fovy * 0.5);
// tanTheta 是相机视角张角的正切值,表示近平面的宽度与近平面距离的比值
const tanTheta = camera.frustum.aspectRatio * tanPhi;
const near = camera.frustum.near;
// 屏幕空间 转 NDC 空间, [-1, 1]
const x = (2.0 / width) * windowPosition.x - 1.0;
const y = (2.0 / height) * (height - windowPosition.y) - 1.0;
const position = camera.positionWC;
Cartesian3.clone(position, result.origin);
// near * tanTheta 表示近平面的宽度,将其乘以 x 就可以得到近平面上某个点的水平距离。
const nearCenter = Cartesian3.multiplyByScalar(
camera.directionWC,
near,
pickPerspCenter
);
Cartesian3.add(position, nearCenter, nearCenter);
const xDir = Cartesian3.multiplyByScalar(
camera.rightWC,
x * near * tanTheta,
pickPerspXDir
);
const yDir = Cartesian3.multiplyByScalar(
camera.upWC,
y * near * tanPhi,
pickPerspYDir
);
const direction = Cartesian3.add(nearCenter, xDir, result.direction);
Cartesian3.add(direction, yDir, direction);
Cartesian3.subtract(direction, position, direction);
Cartesian3.normalize(direction, direction);
return result;
}
7、pickPosition 实现细节
js
const pickGlobeScratchRay = new Ray();
const scratchDepthIntersection = new Cartesian3();
const scratchRayIntersection = new Cartesian3();
function pickPosition(controller, mousePosition, result) {
const scene = controller._scene;
const globe = controller._globe;
const camera = scene.camera;
let depthIntersection;
if (scene.pickPositionSupported) {
depthIntersection = scene.pickPositionWorldCoordinates(
mousePosition,
scratchDepthIntersection
);
}
if (!defined(globe)) {
return Cartesian3.clone(depthIntersection, result);
}
const cullBackFaces = !controller._cameraUnderground;
const ray = camera.getPickRay(mousePosition, pickGlobeScratchRay);
const rayIntersection = globe.pickWorldCoordinates(
ray,
scene,
cullBackFaces,
scratchRayIntersection
);
const pickDistance = defined(depthIntersection)
? Cartesian3.distance(depthIntersection, camera.positionWC)
: Number.POSITIVE_INFINITY;
const rayDistance = defined(rayIntersection)
? Cartesian3.distance(rayIntersection, camera.positionWC)
: Number.POSITIVE_INFINITY;
if (pickDistance < rayDistance) {
return Cartesian3.clone(depthIntersection, result);
}
return Cartesian3.clone(rayIntersection, result);
}
8、handleZoom 实现细节
js
viewer.scene.screenSpaceCameraController._zoomWorldPosition
js
function handleZoom(distanceMeasure) {
let percentage = 1.0;
// startPosition 是固定的 (0,0)
// endPosition.y 是滚轮距离
const diff = movement.endPosition.y - movement.startPosition.y;
const minHeight = 0;
const maxHeight = Infinity;
const minDistance = distanceMeasure - minHeight;
let zoomRate = zoomFactor * minDistance;
let rangeWindowRatio = diff / object._scene.canvas.clientHeight;
let distance = zoomRate * rangeWindowRatio;
pickedPosition = pickPosition(
object,
startPosition,
scratchPickCartesian
);
if (defined(pickedPosition)) {
object._useZoomWorldPosition = true;
object._zoomWorldPosition = Cartesian3.clone(
pickedPosition,
object._zoomWorldPosition
);
} else {
object._useZoomWorldPosition = false;
}
const positionNormal = Cartesian3.normalize(
centerPosition,
scratchPositionNormal
);
const pickedNormal = Cartesian3.normalize(
controller._zoomWorldPosition,
scratchPickNormal
);
const dotProduct = Cartesian3.dot(pickedNormal, positionNormal);
if (dotProduct > 0.0 && dotProduct < 1.0) {
const angle = CesiumMath.acosClamped(dotProduct);
const axis = Cartesian3.cross(
pickedNormal,
positionNormal,
scratchZoomAxis
);
const denom =
Math.abs(angle) > CesiumMath.toRadians(20.0)
? camera.positionCartographic.height * 0.75
: camera.positionCartographic.height - distance;
const scalar = distance / denom;
camera.rotate(axis, angle * scalar);
}
9、相机的旋转
js
/**
* Rotates the camera around <code>axis</code> by <code>angle</code>. The distance
* of the camera's position to the center of the camera's reference frame remains the same.
*
* @param {Cartesian3} axis The axis to rotate around given in world coordinates.
* @param {number} [angle] The angle, in radians, to rotate by. Defaults to <code>defaultRotateAmount</code>.
*
* @see Camera#rotateUp
* @see Camera#rotateDown
* @see Camera#rotateLeft
* @see Camera#rotateRight
*/
Camera.prototype.rotate = function (axis, angle) {
const turnAngle = defaultValue(angle, this.defaultRotateAmount);
const quaternion = Quaternion.fromAxisAngle(
axis,
-turnAngle,
rotateScratchQuaternion
);
const rotation = Matrix3.fromQuaternion(quaternion, rotateScratchMatrix);
Matrix3.multiplyByVector(rotation, this.position, this.position);
Matrix3.multiplyByVector(rotation, this.direction, this.direction);
Matrix3.multiplyByVector(rotation, this.up, this.up);
Cartesian3.cross(this.direction, this.up, this.right);
Cartesian3.cross(this.right, this.direction, this.up);
};