@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
JavaScript
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