UNPKG

@babylonjs/core

Version:

Getting started? Play directly with the Babylon.js API using our [playground](https://playground.babylonjs.com/). It also contains a lot of samples to learn how to use it.

246 lines 13.8 kB
import { CameraMovement } from "./cameraMovement.js"; import { Epsilon } from "../Maths/math.constants.js"; import { Matrix, TmpVectors, Vector3 } from "../Maths/math.vector.js"; import { Plane } from "../Maths/math.plane.js"; import { Ray } from "../Culling/ray.js"; import { Vector3Distance } from "../Maths/math.vector.functions.js"; import { Clamp } from "../Maths/math.scalar.functions.js"; /** * Geospatial-specific camera movement system that extends the base movement with * raycasting and altitude-aware zoom constraints. * * This class encapsulates geospatial camera movement logic: * - Dragging in a way which keeps cursor anchored to globe * - Latitude-based pan speed dampening * - Zoom speed scaling based on distance to center * - Raycasting to determine zoom constraints based on terrain/globe * - Altitude-based zoom clamping * - Zoom direction calculation (towards cursor vs along look vector) */ export class GeospatialCameraMovement extends CameraMovement { constructor(scene, limits, cameraPosition, _cameraCenter, _cameraLookAt, pickPredicate, behavior) { super(scene, cameraPosition, behavior); this.limits = limits; this._cameraCenter = _cameraCenter; this._cameraLookAt = _cameraLookAt; this.zoomToCursor = true; this._hitPointRadius = undefined; this._dragPlane = new Plane(0, 0, 0, 0); this._dragPlaneNormal = Vector3.Zero(); this._dragPlaneOriginPointEcef = Vector3.Zero(); this._dragPlaneHitPointLocal = Vector3.Zero(); this._previousDragPlaneHitPointLocal = Vector3.Zero(); /** * Function to calculate the up vector from a given point. * Can be overridden to support non-spherical planets or custom up vector logic. * Defaults to using the geocentric normal. * @param point The point from which to calculate the up vector (e.g., camera position) * @param result The vector to store the calculated up vector * @returns The calculated up vector */ this.calculateUpVectorFromPointToRef = (point, result) => { return point.normalizeToRef(result); }; this.pickPredicate = pickPredicate; this._tempPickingRay = new Ray(this._cameraPosition, this._cameraLookAt); this.panInertia = 0; this.rotationInertia = 0; this.rotationXSpeed = Math.PI / 500; // Move 1/500th of a half circle per pixel this.rotationYSpeed = Math.PI / 500; // Move 1/500th of a half circle per pixel this.zoomSpeed = 2; // Base zoom speed; actual speed is scaled based on altitude } startDrag(pointerX, pointerY) { const pickResult = this._scene.pick(pointerX, pointerY, this.pickPredicate); if (pickResult.pickedPoint && pickResult.ray) { // Store radius from earth center to pickedPoint, used when calculating drag plane this._hitPointRadius = pickResult.pickedPoint.length(); this._recalculateDragPlaneHitPoint(this._hitPointRadius, pickResult.ray, TmpVectors.Matrix[0]); this._previousDragPlaneHitPointLocal.copyFrom(this._dragPlaneHitPointLocal); } else { this._hitPointRadius = undefined; // can't drag without a hit on the globe } } stopDrag() { this._hitPointRadius = undefined; } /** * The previous drag plane hit point in local space is stored to compute the movement delta. * As the drag movement occurs, we will continuously recalculate this point. The delta between the previous and current hit points is the delta we will apply to the camera's localtranslation * @param hitPointRadius The distance between the world origin (center of globe) and the initial drag hit point * @param ray The ray from the camera to the new cursor location * @param localToEcefResult The matrix to convert from local to ECEF space */ _recalculateDragPlaneHitPoint(hitPointRadius, ray, localToEcefResult) { // Use the camera's geocentric normal to find the dragPlaneOriginPoint which lives at hitPointRadius along the camera's geocentric normal this.calculateUpVectorFromPointToRef(this._cameraPosition, this._dragPlaneNormal); this._dragPlaneNormal.scaleToRef(hitPointRadius, this._dragPlaneOriginPointEcef); // The dragPlaneOffsetVector will later be recalculated when drag occurs, and the delta between the offset vectors will be applied to localTranslation ComputeLocalBasisToRefs(this._dragPlaneOriginPointEcef, TmpVectors.Vector3[0], TmpVectors.Vector3[1], TmpVectors.Vector3[2], this._scene.useRightHandedSystem, this.calculateUpVectorFromPointToRef); const localToEcef = Matrix.FromXYZAxesToRef(TmpVectors.Vector3[0], TmpVectors.Vector3[1], TmpVectors.Vector3[2], localToEcefResult); localToEcef.setTranslationFromFloats(this._dragPlaneOriginPointEcef.x, this._dragPlaneOriginPointEcef.y, this._dragPlaneOriginPointEcef.z); const ecefToLocal = localToEcef.invertToRef(TmpVectors.Matrix[1]); // Now create a plane at that point, perpendicular to the camera's geocentric normal Plane.FromPositionAndNormalToRef(this._dragPlaneOriginPointEcef, this._dragPlaneNormal, this._dragPlane); // Lastly, find the _dragPlaneHitPoint where the ray intersects the _dragPlane. if (IntersectRayWithPlaneToRef(ray, this._dragPlane, this._dragPlaneHitPointLocal)) { // If hit, convert the drag plane hit point into the local space. Vector3.TransformCoordinatesToRef(this._dragPlaneHitPointLocal, ecefToLocal, this._dragPlaneHitPointLocal); } } handleDrag(pointerX, pointerY) { if (this._hitPointRadius) { const pickResult = this._scene.pick(pointerX, pointerY, this.pickPredicate); if (pickResult.ray) { const localToEcef = TmpVectors.Matrix[0]; this._recalculateDragPlaneHitPoint(this._hitPointRadius, pickResult.ray, localToEcef); const delta = this._dragPlaneHitPointLocal.subtractToRef(this._previousDragPlaneHitPointLocal, TmpVectors.Vector3[6]); // When the camera is pitched nearly parallel to the drag plane, ray-plane intersection // can produce enormous deltas. Clamp the delta to avoid massive jumps. const maxDragDelta = this._hitPointRadius * 0.1; // Max 10% of hit radius per frame const deltaLength = delta.length(); if (deltaLength > maxDragDelta) { delta.scaleInPlace(maxDragDelta / deltaLength); } this._previousDragPlaneHitPointLocal.copyFrom(this._dragPlaneHitPointLocal); Vector3.TransformNormalToRef(delta, localToEcef, delta); this._dragPlaneOriginPointEcef.addInPlace(delta); this.panAccumulatedPixels.subtractInPlace(delta); } } } /** @override */ computeCurrentFrameDeltas() { const cameraCenter = this._cameraCenter; // Slows down panning near the poles if (this.panAccumulatedPixels.lengthSquared() > Epsilon) { const centerRadius = cameraCenter.length(); // distance from planet origin to camera center const currentRadius = this._cameraPosition.length(); // Dampen the pan speed based on latitude (slower near poles) const upAtCenter = TmpVectors.Vector3[7]; this.calculateUpVectorFromPointToRef(cameraCenter, upAtCenter); // Latitude is derived from the Z component of the up vector (ECEF convention: Z = polar axis) const sineOfSphericalLat = upAtCenter.z; const cosOfSphericalLat = Math.sqrt(1 - Math.min(1, sineOfSphericalLat * sineOfSphericalLat)); const latitudeDampening = Math.sqrt(Math.abs(cosOfSphericalLat)); // sqrt here reduces effect near equator // Reduce the dampening effect near surface (so that at ground level, pan speed is not affected by latitude) const height = Math.max(currentRadius - centerRadius, Epsilon); const latitudeDampeningScale = Math.max(1, centerRadius / height); this._panSpeedMultiplier = Clamp(latitudeDampeningScale * latitudeDampening, 0, 1); } else { this._panSpeedMultiplier = 1; } // If a pan drag or rotate is occurring, stop zooming. let zoomTargetDistance; if (this.isDragging || this.rotationAccumulatedPixels.lengthSquared() > Epsilon) { this._zoomSpeedMultiplier = 0; this._zoomVelocity = 0; } else { zoomTargetDistance = this.computedPerFrameZoomPickPoint ? Vector3Distance(this._cameraPosition, this.computedPerFrameZoomPickPoint) : undefined; // Scales zoom movement speed based on camera distance to zoom target. this._zoomSpeedMultiplier = (zoomTargetDistance ?? Vector3Distance(this._cameraPosition, cameraCenter)) * 0.01; } super.computeCurrentFrameDeltas(); } get isDragging() { return this._hitPointRadius !== undefined; } handleZoom(zoomDelta, toCursor) { if (zoomDelta !== 0) { this.zoomAccumulatedPixels += zoomDelta; const pickResult = this._scene.pick(this._scene.pointerX, this._scene.pointerY, this.pickPredicate); if (toCursor && pickResult.hit && pickResult.pickedPoint && pickResult.ray && this.zoomToCursor) { this.computedPerFrameZoomPickPoint = pickResult.pickedPoint; } else { // If no hit under cursor or explicitly told not to zoom to cursor, zoom along lookVector instead const lookPickResult = this.pickAlongVector(this._cameraLookAt); this.computedPerFrameZoomPickPoint = lookPickResult?.pickedPoint ?? undefined; } } } pickAlongVector(vector) { this._tempPickingRay.origin.copyFrom(this._cameraPosition); this._tempPickingRay.direction.copyFrom(vector); return this._scene.pickWithRay(this._tempPickingRay, this.pickPredicate); } } /** @internal */ export function ClampCenterFromPolesInPlace(center) { const sineOfSphericalLatitudeLimit = 0.998749218; // ~90 degrees const centerMagnitude = center.length(); // distance from planet origin if (centerMagnitude > Epsilon) { const sineSphericalLat = centerMagnitude === 0 ? 0 : center.z / centerMagnitude; if (Math.abs(sineSphericalLat) > sineOfSphericalLatitudeLimit) { // Clamp the spherical latitude (and derive longitude) const sineOfClampedSphericalLat = Clamp(sineSphericalLat, -sineOfSphericalLatitudeLimit, sineOfSphericalLatitudeLimit); const cosineOfClampedSphericalLat = Math.sqrt(1 - sineOfClampedSphericalLat * sineOfClampedSphericalLat); const longitude = Math.atan2(center.y, center.x); // Spherical to Cartesian const newX = centerMagnitude * Math.cos(longitude) * cosineOfClampedSphericalLat; const newY = centerMagnitude * Math.sin(longitude) * cosineOfClampedSphericalLat; const newZ = centerMagnitude * sineOfClampedSphericalLat; center.set(newX, newY, newZ); } } return center; } function IntersectRayWithPlaneToRef(ray, plane, ref) { // Distance along the ray to the plane; null if no hit const dist = ray.intersectsPlane(plane); if (dist !== null && dist >= 0) { ray.origin.addToRef(ray.direction.scaleToRef(dist, TmpVectors.Vector3[0]), ref); return true; } return false; } /** * Helper to build east/north/up basis vectors at a world position. * Cross product order is swapped based on handedness so that the east vector * encodes the coordinate-system convention, removing the need for a separate yawScale. * @param worldPos - The position on the globe * @param refEast - Receives the east direction * @param refNorth - Receives the north direction * @param refUp - Receives the up (outward) direction * @param useRightHandedSystem - Whether the scene uses a right-handed coordinate system (default: false) * @param calculateUpVectorFromPointToRef - Optional function to calculate the up vector from a point. If supplied, this function will be used instead of assuming a spherical geocentric normal, allowing support for non-spherical planets or custom up vector logic. * @internal */ export function ComputeLocalBasisToRefs(worldPos, refEast, refNorth, refUp, useRightHandedSystem = false, calculateUpVectorFromPointToRef) { if (calculateUpVectorFromPointToRef) { calculateUpVectorFromPointToRef(worldPos, refUp); } else { // up = normalized position (geocentric normal) refUp.copyFrom(worldPos).normalize(); } // east – cross product order determines handedness const worldNorth = Vector3.LeftHandedForwardReadOnly; // (0,0,1) if (useRightHandedSystem) { Vector3.CrossToRef(worldNorth, refUp, refEast); } else { Vector3.CrossToRef(refUp, worldNorth, refEast); } // at poles, cross with worldRight instead if (refEast.lengthSquared() < Epsilon) { if (useRightHandedSystem) { Vector3.CrossToRef(Vector3.Right(), refUp, refEast); } else { Vector3.CrossToRef(refUp, Vector3.Right(), refEast); } } refEast.normalize(); // north – completes the basis (cross order also swapped for handedness) if (useRightHandedSystem) { Vector3.CrossToRef(refUp, refEast, refNorth); } else { Vector3.CrossToRef(refEast, refUp, refNorth); } refNorth.normalize(); } //# sourceMappingURL=geospatialCameraMovement.js.map