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.

377 lines 19.4 kB
import { GeospatialCameraInputsManager } from "./geospatialCameraInputsManager.js"; import { Vector3, Matrix, TmpVectors } from "../Maths/math.vector.js"; import { Epsilon } from "../Maths/math.constants.js"; import { Camera } from "./camera.js"; import { GeospatialLimits } from "./Limits/geospatialLimits.js"; import { ClampCenterFromPolesInPlace, ComputeLocalBasisToRefs, GeospatialCameraMovement } from "./geospatialCameraMovement.js"; import { Vector3CopyToRef, Vector3Distance } from "../Maths/math.vector.functions.js"; import { Clamp, NormalizeRadians } from "../Maths/math.scalar.functions.js"; import { InterpolatingBehavior } from "../Behaviors/Cameras/interpolatingBehavior.js"; /** * @experimental * This camera's movements are limited to a camera orbiting a globe, and as the API evolves it will introduce conversions between cartesian coordinates and true lat/long/alt * * Please note this is marked as experimental and the API (including the constructor!) will change until we remove that flag */ export class GeospatialCamera extends Camera { constructor(name, scene, options, pickPredicate) { super(name, new Vector3(), scene); // Temp vars this._tempPosition = new Vector3(); this._tempCenter = new Vector3(); this._viewMatrix = new Matrix(); this._lookAtVector = new Vector3(); this._flyToTargets = new Map(); this._center = new Vector3(); this._yaw = 0; this._pitch = 0; this._radius = 0; this._tempVect = new Vector3(); this._tempEast = new Vector3(); this._tempNorth = new Vector3(); this._tempUp = new Vector3(); this._limits = new GeospatialLimits(options.planetRadius); this._resetToDefault(this._limits); this._flyingBehavior = new InterpolatingBehavior(); this.addBehavior(this._flyingBehavior); this.movement = new GeospatialCameraMovement(scene, this._limits, this.position, this.center, this._lookAtVector, pickPredicate, this._flyingBehavior); this.pickPredicate = pickPredicate; this.inputs = new GeospatialCameraInputsManager(this); this.inputs.addMouse().addMouseWheel().addKeyboard(); } /** The point on the globe that we are anchoring around. If no alternate rotation point is supplied, this will represent the center of screen*/ get center() { return this._center; } /** * Sets the camera position to orbit around a new center point * @param center The world position (ECEF) to orbit around */ set center(center) { this._center.copyFromFloats(center.x, center.y, center.z); this._setOrientation(this._yaw, this._pitch, this._radius, this._center); } /** * Gets the camera's yaw (rotation around the geocentric normal) in radians */ get yaw() { return this._yaw; } /** * Sets the camera's yaw (rotation around the geocentric normal). Will wrap value to [-π, π) * @param yaw The desired yaw angle in radians (0 = north, π/2 = east) */ set yaw(yaw) { yaw !== this._yaw && this._setOrientation(yaw, this.pitch, this.radius, this.center); } /** * Gets the camera's pitch (angle from looking straight at globe) * Pitch is measured from looking straight down at planet center: * - zero pitch = looking straight at planet center (down) * - positive pitch = tilting up away from planet * - π/2 pitch = looking at horizon (perpendicular to geocentric normal) */ get pitch() { return this._pitch; } /** * Sets the camera's pitch (angle from looking straight at globe). Will wrap value to [-π, π) * @param pitch The desired pitch angle in radians (0 = looking at planet center, π/2 = looking at horizon) */ set pitch(pitch) { pitch !== this._pitch && this._setOrientation(this.yaw, pitch, this.radius, this.center); } get radius() { return this._radius; } /** * Sets the camera's distance from the current center point * @param radius The desired radius */ set radius(radius) { radius !== this._radius && this._setOrientation(this.yaw, this.pitch, radius, this.center); } _checkLimits() { const limits = this.limits; this._yaw = Clamp(this._yaw, limits.yawMin, limits.yawMax); this._pitch = Clamp(this._pitch, limits.pitchMin, limits.pitchMax); this._radius = Clamp(this._radius, limits.radiusMin, limits.radiusMax); this._center = ClampCenterFromPolesInPlace(this._center); } _setOrientation(yaw, pitch, radius, center) { // Wrap yaw and pitch to [-π, π) this._yaw = NormalizeRadians(yaw); this._pitch = NormalizeRadians(pitch); this._radius = radius; Vector3CopyToRef(center, this._center); // Clamp to limits this._checkLimits(); // Refresh local basis at center (treat these as read-only for the whole call) ComputeLocalBasisToRefs(this._center, this._tempEast, this._tempNorth, this._tempUp); // Trig const yawScale = this._scene.useRightHandedSystem ? 1 : -1; const cosYaw = Math.cos(this._yaw * yawScale); const sinYaw = Math.sin(this._yaw * yawScale); const sinPitch = Math.sin(this._pitch); // horizontal weight const cosPitch = Math.cos(this._pitch); // vertical weight (toward center) // Temps const horiz = TmpVectors.Vector3[0]; const t1 = TmpVectors.Vector3[1]; const t2 = TmpVectors.Vector3[2]; const right = TmpVectors.Vector3[3]; // horizontalDirection = North*cosYaw + East*sinYaw (avoids mutating _temp basis vectors) horiz.copyFrom(this._tempNorth).scaleInPlace(cosYaw).addInPlace(t1.copyFrom(this._tempEast).scaleInPlace(sinYaw)); // look = horiz*sinPitch - Up*cosPitch this._lookAtVector.copyFrom(horiz).scaleInPlace(sinPitch).addInPlace(t2.copyFrom(this._tempUp).scaleInPlace(-cosPitch)).normalize(); // keep it unit // Build an orthonormal up aligned with geocentric Up // right = normalize(cross(upRef, look)) Vector3.CrossToRef(this._tempUp, this._lookAtVector, right); // up = normalize(cross(look, right)) Vector3.CrossToRef(this._lookAtVector, right, this.upVector); // Position = center - look * radius (preserve unit look) this._tempVect.copyFrom(this._lookAtVector).scaleInPlace(-this._radius); this._tempPosition.copyFrom(this._center).addInPlace(this._tempVect); this._position.copyFrom(this._tempPosition); this._isViewMatrixDirty = true; } /** The point around which the camera will geocentrically rotate. Uses center (pt we are anchored to) if no alternateRotationPt is defined */ get _geocentricRotationPt() { return this.movement.alternateRotationPt ?? this.center; } /** * If camera is actively in flight, will update the target properties and use up the remaining duration from original flyTo call * * To start a new flyTo curve entirely, call into flyToAsync again (it will stop the inflight animation) * @param targetYaw * @param targetPitch * @param targetRadius * @param targetCenter */ updateFlyToDestination(targetYaw, targetPitch, targetRadius, targetCenter) { this._flyToTargets.clear(); this._flyToTargets.set("yaw", targetYaw != undefined ? NormalizeRadians(targetYaw) : undefined); this._flyToTargets.set("pitch", targetPitch != undefined ? NormalizeRadians(targetPitch) : undefined); this._flyToTargets.set("radius", targetRadius); this._flyToTargets.set("center", targetCenter); this._flyingBehavior.updateProperties(this._flyToTargets); } /** * Animate camera towards passed in property values. If undefined, will use current value * @param targetYaw * @param targetPitch * @param targetRadius * @param targetCenter * @param flightDurationMs * @param easingFunction * @param centerHopScale If supplied, will define the parabolic hop height scale for center animation to create a "bounce" effect * @returns Promise that will return when the animation is complete (or interuppted by pointer input) */ async flyToAsync(targetYaw, targetPitch, targetRadius, targetCenter, flightDurationMs = 1000, easingFunction, centerHopScale) { this._flyToTargets.clear(); this._flyToTargets.set("yaw", targetYaw !== undefined ? NormalizeRadians(targetYaw) : undefined); this._flyToTargets.set("pitch", targetPitch !== undefined ? NormalizeRadians(targetPitch) : undefined); this._flyToTargets.set("radius", targetRadius); this._flyToTargets.set("center", targetCenter); let overrideAnimationFunction; if (targetCenter !== undefined && !targetCenter.equals(this.center)) { // Animate center directly with custom interpolation const start = this.center.clone(); const end = targetCenter.clone(); overrideAnimationFunction = (key, animation) => { if (key === "center") { // Override the Vector3 interpolation to use SLERP + hop animation.vector3InterpolateFunction = (startValue, endValue, gradient) => { // gradient is the eased value (0 to 1) after easing function is applied // Slerp between start and end const newCenter = Vector3.SlerpToRef(start, end, gradient, this._tempCenter); // Apply parabolic hop if requested if (centerHopScale && centerHopScale > 0) { // Parabolic formula: peaks at t=0.5, returns to 0 at gradient=0 and gradient=1 // if hopPeakT = .5 the denominator would be hopPeakT * hopPeakT - hopPeakT, which = -.25 const hopPeakOffset = centerHopScale * Vector3Distance(start, end); const hopOffset = hopPeakOffset * Clamp((gradient * gradient - gradient) / -0.25); // Scale the center outward (away from origin) newCenter.scaleInPlace(1 + hopOffset / newCenter.length()); } return newCenter; }; } }; } return await this._flyingBehavior.animatePropertiesAsync(this._flyToTargets, flightDurationMs, easingFunction, overrideAnimationFunction); } /** * Helper function to move camera towards a given point by radiusScale% of radius (by default 50%) * @param destination point to move towards * @param radiusScale value between 0 and 1, % of radius to move * @param durationMs duration of flight, default 1s * @param easingFn optional easing function for flight interpolation of properties * @param overshootRadiusScale optional scale to apply to the current radius to achieve a 'hop' animation */ async flyToPointAsync(destination, radiusScale = 0.5, durationMs = 1000, easingFn, overshootRadiusScale) { // Zoom to radiusScale% of radius towards the given destination point const zoomDistance = this.radius * radiusScale; const newRadius = this._getCenterAndRadiusFromZoomToPoint(destination, zoomDistance, this._tempCenter); await this.flyToAsync(undefined, undefined, newRadius, this._tempCenter, durationMs, easingFn, overshootRadiusScale); } get limits() { return this._limits; } _resetToDefault(limits) { // Camera configuration vars const maxCameraRadius = limits.altitudeMax !== undefined ? limits.planetRadius + limits.altitudeMax : undefined; const restingAltitude = maxCameraRadius ?? limits.planetRadius * 4; this.position.copyFromFloats(restingAltitude, 0, 0); this._center.copyFromFloats(limits.planetRadius, 0, 0); this._radius = Vector3.Distance(this.position, this.center); // Temp vars this._tempPosition = new Vector3(); // View matrix calculation vars this._viewMatrix = Matrix.Identity(); this._center.subtractToRef(this._position, this._lookAtVector).normalize(); // Lookat vector of the camera this.upVector = Vector3.Up(); // Up vector of the camera (does work for -X look at) this._isViewMatrixDirty = true; this._setOrientation(this._yaw, this._pitch, this._radius, this._center); } /** @internal */ _getViewMatrix() { if (!this._isViewMatrixDirty) { return this._viewMatrix; } this._isViewMatrixDirty = false; // Ensure vectors are normalized this.upVector.normalize(); this._lookAtVector.normalize(); // Calculate view matrix with camera position and center if (this.getScene().useRightHandedSystem) { Matrix.LookAtRHToRef(this.position, this._center, this.upVector, this._viewMatrix); } else { Matrix.LookAtLHToRef(this.position, this._center, this.upVector, this._viewMatrix); } return this._viewMatrix; } /** @internal */ _isSynchronizedViewMatrix() { if (!super._isSynchronizedViewMatrix() || this._isViewMatrixDirty) { return false; } return true; } _applyGeocentricTranslation() { // Store pending position (without any corrections applied) this.center.addToRef(this.movement.panDeltaCurrentFrame, this._tempPosition); if (!this.movement.isInterpolating) { // Calculate the position correction to keep camera at the same radius when applying translation this._tempPosition.normalize().scaleInPlace(this.center.length()); } // Set center which will call _setOrientation this.center = this._tempPosition; } /** * This rotation keeps the camera oriented towards the globe as it orbits around it. This is different from cameraCentricRotation which is when the camera rotates around its own axis */ _applyGeocentricRotation() { const rotationDeltaCurrentFrame = this.movement.rotationDeltaCurrentFrame; if (rotationDeltaCurrentFrame.x !== 0 || rotationDeltaCurrentFrame.y !== 0) { const pitch = rotationDeltaCurrentFrame.x !== 0 ? Clamp(this._pitch + rotationDeltaCurrentFrame.x, 0, 0.5 * Math.PI - Epsilon) : this._pitch; const yaw = rotationDeltaCurrentFrame.y !== 0 ? this._yaw + rotationDeltaCurrentFrame.y : this._yaw; // TODO: If _geocentricRotationPt is not the center, this will need to be adjusted. this._setOrientation(yaw, pitch, this._radius, this._geocentricRotationPt); } } _getCenterAndRadiusFromZoomToPoint(targetPoint, distance, newCenter) { // Clamp new radius to limits const requestedRadius = this._radius - distance; const newRadius = Clamp(requestedRadius, this.limits.radiusMin, this.limits.radiusMax); const actualDistance = this._radius - newRadius; const actualRatio = actualDistance / this._radius; // Direction from current center to target point const directionToTarget = TmpVectors.Vector3[0]; targetPoint.subtractToRef(this._center, directionToTarget); // Move center toward target by the ratio amount const centerOffset = TmpVectors.Vector3[1]; directionToTarget.scaleToRef(actualRatio, centerOffset); // Calculate new center this._center.addToRef(centerOffset, newCenter); // Preserve center altitude (distance from planet origin) const currentCenterRadius = this._center.length(); const newCenterRadius = newCenter.length(); if (newCenterRadius > Epsilon) { newCenter.scaleInPlace(currentCenterRadius / newCenterRadius); } return newRadius; } /** * Apply zoom by moving the camera toward/away from a target point. */ _applyZoom() { const zoomDelta = this.movement.zoomDeltaCurrentFrame; const pickedPoint = this.movement.computedPerFrameZoomPickPoint; if (pickedPoint) { // Zoom toward the picked point under cursor this._zoomToPoint(pickedPoint, zoomDelta); } else { // Zoom along lookAt vector (fallback when no surface under cursor) this._zoomAlongLookAt(zoomDelta); } } _zoomToPoint(targetPoint, distance) { const newRadius = this._getCenterAndRadiusFromZoomToPoint(targetPoint, distance, this._tempCenter); // Apply the new orientation this._setOrientation(this._yaw, this._pitch, newRadius, this._tempCenter); } _zoomAlongLookAt(distance) { // Clamp radius to limits const requestedRadius = this._radius - distance; const newRadius = Clamp(requestedRadius, this.limits.radiusMin, this.limits.radiusMax); // Simply change radius without moving center this._setOrientation(this._yaw, this._pitch, newRadius, this._center); } _checkInputs() { this.inputs.checkInputs(); // Let movement class handle all per-frame logic this.movement.computeCurrentFrameDeltas(); let recalculateCenter = false; if (this.movement.panDeltaCurrentFrame.lengthSquared() > 0) { this._applyGeocentricTranslation(); recalculateCenter = true; } if (this.movement.rotationDeltaCurrentFrame.lengthSquared() > 0) { this._applyGeocentricRotation(); } if (Math.abs(this.movement.zoomDeltaCurrentFrame) > Epsilon) { this._applyZoom(); recalculateCenter = true; } // After a movement impacting center or radius, recalculate the center point to ensure it's still on the surface. recalculateCenter && this._recalculateCenter(); super._checkInputs(); } _recalculateCenter() { // Wait until dragging is complete to avoid wasted raycasting if (!this.movement.isDragging) { const newCenter = this.movement.pickAlongVector(this._lookAtVector); if (newCenter?.pickedPoint) { // Direction from new center to origin const centerToOrigin = TmpVectors.Vector3[4]; centerToOrigin.copyFrom(newCenter.pickedPoint).negateInPlace().normalize(); // Check if this direction aligns with camera's lookAt vector const dotProduct = Vector3.Dot(this._lookAtVector, centerToOrigin); // Only update if the center is looking toward the origin (dot product > 0) to avoid a center on the opposite side of globe if (dotProduct > 0) { const newRadius = Vector3.Distance(this.position, newCenter.pickedPoint); this._setOrientation(this._yaw, this._pitch, newRadius, newCenter.pickedPoint); } } } } attachControl(noPreventDefault) { this.inputs.attachElement(noPreventDefault); } detachControl() { this.inputs.detachElement(); } } //# sourceMappingURL=geospatialCamera.js.map