UNPKG

@cesium/engine

Version:

CesiumJS is a JavaScript library for creating 3D globes and 2D maps in a web browser without a plugin.

1,743 lines (1,538 loc) 123 kB
import BoundingSphere from "../Core/BoundingSphere.js"; import Cartesian2 from "../Core/Cartesian2.js"; import Cartesian3 from "../Core/Cartesian3.js"; import Cartesian4 from "../Core/Cartesian4.js"; import Cartographic from "../Core/Cartographic.js"; import Frozen from "../Core/Frozen.js"; import defined from "../Core/defined.js"; import DeveloperError from "../Core/DeveloperError.js"; import EasingFunction from "../Core/EasingFunction.js"; import Ellipsoid from "../Core/Ellipsoid.js"; import EllipsoidGeodesic from "../Core/EllipsoidGeodesic.js"; import Event from "../Core/Event.js"; import getTimestamp from "../Core/getTimestamp.js"; import HeadingPitchRange from "../Core/HeadingPitchRange.js"; import HeadingPitchRoll from "../Core/HeadingPitchRoll.js"; import Intersect from "../Core/Intersect.js"; import IntersectionTests from "../Core/IntersectionTests.js"; import CesiumMath from "../Core/Math.js"; import Matrix3 from "../Core/Matrix3.js"; import Matrix4 from "../Core/Matrix4.js"; import OrthographicFrustum from "../Core/OrthographicFrustum.js"; import OrthographicOffCenterFrustum from "../Core/OrthographicOffCenterFrustum.js"; import PerspectiveFrustum from "../Core/PerspectiveFrustum.js"; import Quaternion from "../Core/Quaternion.js"; import Ray from "../Core/Ray.js"; import Rectangle from "../Core/Rectangle.js"; import Transforms from "../Core/Transforms.js"; import CameraFlightPath from "./CameraFlightPath.js"; import MapMode2D from "./MapMode2D.js"; import SceneMode from "./SceneMode.js"; /** * @typedef {object} DirectionUp * * An orientation given by a pair of unit vectors * * @property {Cartesian3} direction The unit "direction" vector * @property {Cartesian3} up The unit "up" vector **/ /** * @typedef {object} HeadingPitchRollValues * * An orientation given by numeric heading, pitch, and roll * * @property {number} [heading=0.0] The heading in radians * @property {number} [pitch=-CesiumMath.PI_OVER_TWO] The pitch in radians * @property {number} [roll=0.0] The roll in radians **/ /** * The camera is defined by a position, orientation, and view frustum. * <br /><br /> * The orientation forms an orthonormal basis with a view, up and right = view x up unit vectors. * <br /><br /> * The viewing frustum is defined by 6 planes. * Each plane is represented by a {@link Cartesian4} object, where the x, y, and z components * define the unit vector normal to the plane, and the w component is the distance of the * plane from the origin/camera position. * * @alias Camera * * @constructor * * @param {Scene} scene The scene. * * @demo {@link https://sandcastle.cesium.com/index.html?src=Camera.html|Cesium Sandcastle Camera Demo} * @demo {@link https://sandcastle.cesium.com/index.html?src=Camera%20Tutorial.html|Cesium Sandcastle Camera Tutorial Example} * @demo {@link https://cesium.com/learn/cesiumjs-learn/cesiumjs-camera|Camera Tutorial} * * @example * // Create a camera looking down the negative z-axis, positioned at the origin, * // with a field of view of 60 degrees, and 1:1 aspect ratio. * const camera = new Cesium.Camera(scene); * camera.position = new Cesium.Cartesian3(); * camera.direction = Cesium.Cartesian3.negate(Cesium.Cartesian3.UNIT_Z, new Cesium.Cartesian3()); * camera.up = Cesium.Cartesian3.clone(Cesium.Cartesian3.UNIT_Y); * camera.frustum.fov = Cesium.Math.PI_OVER_THREE; * camera.frustum.near = 1.0; * camera.frustum.far = 2.0; */ function Camera(scene) { //>>includeStart('debug', pragmas.debug); if (!defined(scene)) { throw new DeveloperError("scene is required."); } //>>includeEnd('debug'); this._scene = scene; this._transform = Matrix4.clone(Matrix4.IDENTITY); this._invTransform = Matrix4.clone(Matrix4.IDENTITY); this._actualTransform = Matrix4.clone(Matrix4.IDENTITY); this._actualInvTransform = Matrix4.clone(Matrix4.IDENTITY); this._transformChanged = false; /** * The position of the camera. * * @type {Cartesian3} */ this.position = new Cartesian3(); this._position = new Cartesian3(); this._positionWC = new Cartesian3(); this._positionCartographic = new Cartographic(); this._oldPositionWC = undefined; /** * The position delta magnitude. * * @private */ this.positionWCDeltaMagnitude = 0.0; /** * The position delta magnitude last frame. * * @private */ this.positionWCDeltaMagnitudeLastFrame = 0.0; /** * How long in seconds since the camera has stopped moving * * @private */ this.timeSinceMoved = 0.0; this._lastMovedTimestamp = 0.0; /** * The view direction of the camera. * * @type {Cartesian3} */ this.direction = new Cartesian3(); this._direction = new Cartesian3(); this._directionWC = new Cartesian3(); /** * The up direction of the camera. * * @type {Cartesian3} */ this.up = new Cartesian3(); this._up = new Cartesian3(); this._upWC = new Cartesian3(); /** * The right direction of the camera. * * @type {Cartesian3} */ this.right = new Cartesian3(); this._right = new Cartesian3(); this._rightWC = new Cartesian3(); /** * The region of space in view. * * @type {PerspectiveFrustum|PerspectiveOffCenterFrustum|OrthographicFrustum} * @default PerspectiveFrustum() * * @see PerspectiveFrustum * @see PerspectiveOffCenterFrustum * @see OrthographicFrustum */ this.frustum = new PerspectiveFrustum(); this.frustum.aspectRatio = scene.drawingBufferWidth / scene.drawingBufferHeight; this.frustum.fov = CesiumMath.toRadians(60.0); /** * The default amount to move the camera when an argument is not * provided to the move methods. * @type {number} * @default 100000.0; */ this.defaultMoveAmount = 100000.0; /** * The default amount to rotate the camera when an argument is not * provided to the look methods. * @type {number} * @default Math.PI / 60.0 */ this.defaultLookAmount = Math.PI / 60.0; /** * The default amount to rotate the camera when an argument is not * provided to the rotate methods. * @type {number} * @default Math.PI / 3600.0 */ this.defaultRotateAmount = Math.PI / 3600.0; /** * The default amount to move the camera when an argument is not * provided to the zoom methods. * @type {number} * @default 100000.0; */ this.defaultZoomAmount = 100000.0; /** * If set, the camera will not be able to rotate past this axis in either direction. * @type {Cartesian3 | undefined} * @default undefined */ this.constrainedAxis = undefined; /** * The factor multiplied by the the map size used to determine where to clamp the camera position * when zooming out from the surface. The default is 1.5. Only valid for 2D and the map is rotatable. * @type {number} * @default 1.5 */ this.maximumZoomFactor = 1.5; this._moveStart = new Event(); this._moveEnd = new Event(); this._changed = new Event(); this._changedPosition = undefined; this._changedDirection = undefined; this._changedFrustum = undefined; this._changedHeading = undefined; this._changedRoll = undefined; /** * The amount the camera has to change before the <code>changed</code> event is raised. The value is a percentage in the [0, 1] range. * @type {number} * @default 0.5 */ this.percentageChanged = 0.5; this._viewMatrix = new Matrix4(); this._invViewMatrix = new Matrix4(); updateViewMatrix(this); this._mode = SceneMode.SCENE3D; this._modeChanged = true; const projection = scene.mapProjection; this._projection = projection; this._maxCoord = projection.project( new Cartographic(Math.PI, CesiumMath.PI_OVER_TWO), ); this._max2Dfrustum = undefined; // set default view rectangleCameraPosition3D( this, Camera.DEFAULT_VIEW_RECTANGLE, this.position, true, ); let mag = Cartesian3.magnitude(this.position); mag += mag * Camera.DEFAULT_VIEW_FACTOR; Cartesian3.normalize(this.position, this.position); Cartesian3.multiplyByScalar(this.position, mag, this.position); } /** * @private */ Camera.TRANSFORM_2D = new Matrix4( 0.0, 0.0, 1.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, ); /** * @private */ Camera.TRANSFORM_2D_INVERSE = Matrix4.inverseTransformation( Camera.TRANSFORM_2D, new Matrix4(), ); /** * The default rectangle the camera will view on creation. * @type Rectangle */ Camera.DEFAULT_VIEW_RECTANGLE = Rectangle.fromDegrees( -95.0, -20.0, -70.0, 90.0, ); /** * A scalar to multiply to the camera position and add it back after setting the camera to view the rectangle. * A value of zero means the camera will view the entire {@link Camera#DEFAULT_VIEW_RECTANGLE}, a value greater than zero * will move it further away from the extent, and a value less than zero will move it close to the extent. * @type {number} */ Camera.DEFAULT_VIEW_FACTOR = 0.5; /** * The default heading/pitch/range that is used when the camera flies to a location that contains a bounding sphere. * @type HeadingPitchRange */ Camera.DEFAULT_OFFSET = new HeadingPitchRange( 0.0, -CesiumMath.PI_OVER_FOUR, 0.0, ); function updateViewMatrix(camera) { Matrix4.computeView( camera._position, camera._direction, camera._up, camera._right, camera._viewMatrix, ); Matrix4.multiply( camera._viewMatrix, camera._actualInvTransform, camera._viewMatrix, ); Matrix4.inverseTransformation(camera._viewMatrix, camera._invViewMatrix); } function updateCameraDeltas(camera) { if (!defined(camera._oldPositionWC)) { camera._oldPositionWC = Cartesian3.clone( camera.positionWC, camera._oldPositionWC, ); } else { camera.positionWCDeltaMagnitudeLastFrame = camera.positionWCDeltaMagnitude; const delta = Cartesian3.subtract( camera.positionWC, camera._oldPositionWC, camera._oldPositionWC, ); camera.positionWCDeltaMagnitude = Cartesian3.magnitude(delta); camera._oldPositionWC = Cartesian3.clone( camera.positionWC, camera._oldPositionWC, ); // Update move timers if (camera.positionWCDeltaMagnitude > 0.0) { camera.timeSinceMoved = 0.0; camera._lastMovedTimestamp = getTimestamp(); } else { camera.timeSinceMoved = Math.max(getTimestamp() - camera._lastMovedTimestamp, 0.0) / 1000.0; } } } /** * Checks if there's a camera flight with preload for this camera. * * @returns {boolean} Whether or not this camera has a current flight with a valid preloadFlightCamera in scene. * * @private * */ Camera.prototype.canPreloadFlight = function () { return defined(this._currentFlight) && this._mode !== SceneMode.SCENE2D; }; Camera.prototype._updateCameraChanged = function () { const camera = this; updateCameraDeltas(camera); if (camera._changed.numberOfListeners === 0) { return; } const percentageChanged = camera.percentageChanged; // check heading const currentHeading = camera.heading; if (!defined(camera._changedHeading)) { camera._changedHeading = currentHeading; } let headingDelta = Math.abs(camera._changedHeading - currentHeading) % CesiumMath.TWO_PI; headingDelta = headingDelta > CesiumMath.PI ? CesiumMath.TWO_PI - headingDelta : headingDelta; // Since delta is computed as the shortest distance between two angles // the percentage is relative to the half circle. const headingChangedPercentage = headingDelta / Math.PI; if (headingChangedPercentage > percentageChanged) { camera._changedHeading = currentHeading; } // check roll const currentRoll = camera.roll; if (!defined(camera._changedRoll)) { camera._changedRoll = currentRoll; } let rollDelta = Math.abs(camera._changedRoll - currentRoll) % CesiumMath.TWO_PI; rollDelta = rollDelta > CesiumMath.PI ? CesiumMath.TWO_PI - rollDelta : rollDelta; // Since delta is computed as the shortest distance between two angles // the percentage is relative to the half circle. const rollChangedPercentage = rollDelta / Math.PI; if (rollChangedPercentage > percentageChanged) { camera._changedRoll = currentRoll; } if ( rollChangedPercentage > percentageChanged || headingChangedPercentage > percentageChanged ) { camera._changed.raiseEvent( Math.max(rollChangedPercentage, headingChangedPercentage), ); } if (camera._mode === SceneMode.SCENE2D) { if (!defined(camera._changedFrustum)) { camera._changedPosition = Cartesian3.clone( camera.position, camera._changedPosition, ); camera._changedFrustum = camera.frustum.clone(); return; } const position = camera.position; const lastPosition = camera._changedPosition; const frustum = camera.frustum; const lastFrustum = camera._changedFrustum; const x0 = position.x + frustum.left; const x1 = position.x + frustum.right; const x2 = lastPosition.x + lastFrustum.left; const x3 = lastPosition.x + lastFrustum.right; const y0 = position.y + frustum.bottom; const y1 = position.y + frustum.top; const y2 = lastPosition.y + lastFrustum.bottom; const y3 = lastPosition.y + lastFrustum.top; const leftX = Math.max(x0, x2); const rightX = Math.min(x1, x3); const bottomY = Math.max(y0, y2); const topY = Math.min(y1, y3); let areaPercentage; if (leftX >= rightX || bottomY >= y1) { areaPercentage = 1.0; } else { let areaRef = lastFrustum; if (x0 < x2 && x1 > x3 && y0 < y2 && y1 > y3) { areaRef = frustum; } areaPercentage = 1.0 - ((rightX - leftX) * (topY - bottomY)) / ((areaRef.right - areaRef.left) * (areaRef.top - areaRef.bottom)); } if (areaPercentage > percentageChanged) { camera._changed.raiseEvent(areaPercentage); camera._changedPosition = Cartesian3.clone( camera.position, camera._changedPosition, ); camera._changedFrustum = camera.frustum.clone(camera._changedFrustum); } return; } if (!defined(camera._changedDirection)) { camera._changedPosition = Cartesian3.clone( camera.positionWC, camera._changedPosition, ); camera._changedDirection = Cartesian3.clone( camera.directionWC, camera._changedDirection, ); return; } const dirAngle = CesiumMath.acosClamped( Cartesian3.dot(camera.directionWC, camera._changedDirection), ); let dirPercentage; if (defined(camera.frustum.fovy)) { dirPercentage = dirAngle / (camera.frustum.fovy * 0.5); } else { dirPercentage = dirAngle; } const distance = Cartesian3.distance( camera.positionWC, camera._changedPosition, ); const heightPercentage = distance / camera.positionCartographic.height; if ( dirPercentage > percentageChanged || heightPercentage > percentageChanged ) { camera._changed.raiseEvent(Math.max(dirPercentage, heightPercentage)); camera._changedPosition = Cartesian3.clone( camera.positionWC, camera._changedPosition, ); camera._changedDirection = Cartesian3.clone( camera.directionWC, camera._changedDirection, ); } }; function convertTransformForColumbusView(camera) { Transforms.basisTo2D( camera._projection, camera._transform, camera._actualTransform, ); } const scratchCartographic = new Cartographic(); const scratchCartesian3Projection = new Cartesian3(); const scratchCartesian3 = new Cartesian3(); const scratchCartesian4Origin = new Cartesian4(); const scratchCartesian4NewOrigin = new Cartesian4(); const scratchCartesian4NewXAxis = new Cartesian4(); const scratchCartesian4NewYAxis = new Cartesian4(); const scratchCartesian4NewZAxis = new Cartesian4(); function convertTransformFor2D(camera) { const projection = camera._projection; const ellipsoid = projection.ellipsoid; const origin = Matrix4.getColumn( camera._transform, 3, scratchCartesian4Origin, ); const cartographic = ellipsoid.cartesianToCartographic( origin, scratchCartographic, ); const projectedPosition = projection.project( cartographic, scratchCartesian3Projection, ); const newOrigin = scratchCartesian4NewOrigin; newOrigin.x = projectedPosition.z; newOrigin.y = projectedPosition.x; newOrigin.z = projectedPosition.y; newOrigin.w = 1.0; const newZAxis = Cartesian4.clone( Cartesian4.UNIT_X, scratchCartesian4NewZAxis, ); const xAxis = Cartesian4.add( Matrix4.getColumn(camera._transform, 0, scratchCartesian3), origin, scratchCartesian3, ); ellipsoid.cartesianToCartographic(xAxis, cartographic); projection.project(cartographic, projectedPosition); const newXAxis = scratchCartesian4NewXAxis; newXAxis.x = projectedPosition.z; newXAxis.y = projectedPosition.x; newXAxis.z = projectedPosition.y; newXAxis.w = 0.0; Cartesian3.subtract(newXAxis, newOrigin, newXAxis); newXAxis.x = 0.0; const newYAxis = scratchCartesian4NewYAxis; if (Cartesian3.magnitudeSquared(newXAxis) > CesiumMath.EPSILON10) { Cartesian3.cross(newZAxis, newXAxis, newYAxis); } else { const yAxis = Cartesian4.add( Matrix4.getColumn(camera._transform, 1, scratchCartesian3), origin, scratchCartesian3, ); ellipsoid.cartesianToCartographic(yAxis, cartographic); projection.project(cartographic, projectedPosition); newYAxis.x = projectedPosition.z; newYAxis.y = projectedPosition.x; newYAxis.z = projectedPosition.y; newYAxis.w = 0.0; Cartesian3.subtract(newYAxis, newOrigin, newYAxis); newYAxis.x = 0.0; if (Cartesian3.magnitudeSquared(newYAxis) < CesiumMath.EPSILON10) { Cartesian4.clone(Cartesian4.UNIT_Y, newXAxis); Cartesian4.clone(Cartesian4.UNIT_Z, newYAxis); } } Cartesian3.cross(newYAxis, newZAxis, newXAxis); Cartesian3.normalize(newXAxis, newXAxis); Cartesian3.cross(newZAxis, newXAxis, newYAxis); Cartesian3.normalize(newYAxis, newYAxis); Matrix4.setColumn( camera._actualTransform, 0, newXAxis, camera._actualTransform, ); Matrix4.setColumn( camera._actualTransform, 1, newYAxis, camera._actualTransform, ); Matrix4.setColumn( camera._actualTransform, 2, newZAxis, camera._actualTransform, ); Matrix4.setColumn( camera._actualTransform, 3, newOrigin, camera._actualTransform, ); } const scratchCartesian = new Cartesian3(); function updateMembers(camera) { const mode = camera._mode; let heightChanged = false; let height = 0.0; if (mode === SceneMode.SCENE2D) { height = camera.frustum.right - camera.frustum.left; heightChanged = height !== camera._positionCartographic.height; } let position = camera._position; const positionChanged = !Cartesian3.equals(position, camera.position) || heightChanged; if (positionChanged) { position = Cartesian3.clone(camera.position, camera._position); } let direction = camera._direction; const directionChanged = !Cartesian3.equals(direction, camera.direction); if (directionChanged) { Cartesian3.normalize(camera.direction, camera.direction); direction = Cartesian3.clone(camera.direction, camera._direction); } let up = camera._up; const upChanged = !Cartesian3.equals(up, camera.up); if (upChanged) { Cartesian3.normalize(camera.up, camera.up); up = Cartesian3.clone(camera.up, camera._up); } let right = camera._right; const rightChanged = !Cartesian3.equals(right, camera.right); if (rightChanged) { Cartesian3.normalize(camera.right, camera.right); right = Cartesian3.clone(camera.right, camera._right); } const transformChanged = camera._transformChanged || camera._modeChanged; camera._transformChanged = false; if (transformChanged) { Matrix4.inverseTransformation(camera._transform, camera._invTransform); if ( camera._mode === SceneMode.COLUMBUS_VIEW || camera._mode === SceneMode.SCENE2D ) { if (Matrix4.equals(Matrix4.IDENTITY, camera._transform)) { Matrix4.clone(Camera.TRANSFORM_2D, camera._actualTransform); } else if (camera._mode === SceneMode.COLUMBUS_VIEW) { convertTransformForColumbusView(camera); } else { convertTransformFor2D(camera); } } else { Matrix4.clone(camera._transform, camera._actualTransform); } Matrix4.inverseTransformation( camera._actualTransform, camera._actualInvTransform, ); camera._modeChanged = false; } const transform = camera._actualTransform; if (positionChanged || transformChanged) { camera._positionWC = Matrix4.multiplyByPoint( transform, position, camera._positionWC, ); // Compute the Cartographic position of the camera. if (mode === SceneMode.SCENE3D || mode === SceneMode.MORPHING) { camera._positionCartographic = camera._projection.ellipsoid.cartesianToCartographic( camera._positionWC, camera._positionCartographic, ); } else { // The camera position is expressed in the 2D coordinate system where the Y axis is to the East, // the Z axis is to the North, and the X axis is out of the map. Express them instead in the ENU axes where // X is to the East, Y is to the North, and Z is out of the local horizontal plane. const positionENU = scratchCartesian; positionENU.x = camera._positionWC.y; positionENU.y = camera._positionWC.z; positionENU.z = camera._positionWC.x; // In 2D, the camera height is always 12.7 million meters. // The apparent height is equal to half the frustum width. if (mode === SceneMode.SCENE2D) { positionENU.z = height; } camera._projection.unproject(positionENU, camera._positionCartographic); } } if (directionChanged || upChanged || rightChanged) { const det = Cartesian3.dot( direction, Cartesian3.cross(up, right, scratchCartesian), ); if (Math.abs(1.0 - det) > CesiumMath.EPSILON2) { //orthonormalize axes const invUpMag = 1.0 / Cartesian3.magnitudeSquared(up); const scalar = Cartesian3.dot(up, direction) * invUpMag; const w0 = Cartesian3.multiplyByScalar( direction, scalar, scratchCartesian, ); up = Cartesian3.normalize( Cartesian3.subtract(up, w0, camera._up), camera._up, ); Cartesian3.clone(up, camera.up); right = Cartesian3.cross(direction, up, camera._right); Cartesian3.clone(right, camera.right); } } if (directionChanged || transformChanged) { camera._directionWC = Matrix4.multiplyByPointAsVector( transform, direction, camera._directionWC, ); Cartesian3.normalize(camera._directionWC, camera._directionWC); } if (upChanged || transformChanged) { camera._upWC = Matrix4.multiplyByPointAsVector(transform, up, camera._upWC); Cartesian3.normalize(camera._upWC, camera._upWC); } if (rightChanged || transformChanged) { camera._rightWC = Matrix4.multiplyByPointAsVector( transform, right, camera._rightWC, ); Cartesian3.normalize(camera._rightWC, camera._rightWC); } if ( positionChanged || directionChanged || upChanged || rightChanged || transformChanged ) { updateViewMatrix(camera); } } function getHeading(direction, up) { let heading; if ( !CesiumMath.equalsEpsilon(Math.abs(direction.z), 1.0, CesiumMath.EPSILON3) ) { heading = Math.atan2(direction.y, direction.x) - CesiumMath.PI_OVER_TWO; } else { heading = Math.atan2(up.y, up.x) - CesiumMath.PI_OVER_TWO; } return CesiumMath.TWO_PI - CesiumMath.zeroToTwoPi(heading); } function getPitch(direction) { return CesiumMath.PI_OVER_TWO - CesiumMath.acosClamped(direction.z); } function getRoll(direction, up, right) { let roll = 0.0; if ( !CesiumMath.equalsEpsilon(Math.abs(direction.z), 1.0, CesiumMath.EPSILON3) ) { roll = Math.atan2(-right.z, up.z); roll = CesiumMath.zeroToTwoPi(roll + CesiumMath.TWO_PI); } return roll; } const scratchHPRMatrix1 = new Matrix4(); const scratchHPRMatrix2 = new Matrix4(); Object.defineProperties(Camera.prototype, { /** * Gets the camera's reference frame. The inverse of this transformation is appended to the view matrix. * @memberof Camera.prototype * * @type {Matrix4} * @readonly * * @default {@link Matrix4.IDENTITY} */ transform: { get: function () { return this._transform; }, }, /** * Gets the inverse camera transform. * @memberof Camera.prototype * * @type {Matrix4} * @readonly * * @default {@link Matrix4.IDENTITY} */ inverseTransform: { get: function () { updateMembers(this); return this._invTransform; }, }, /** * Gets the view matrix. * @memberof Camera.prototype * * @type {Matrix4} * @readonly * * @see Camera#inverseViewMatrix */ viewMatrix: { get: function () { updateMembers(this); return this._viewMatrix; }, }, /** * Gets the inverse view matrix. * @memberof Camera.prototype * * @type {Matrix4} * @readonly * * @see Camera#viewMatrix */ inverseViewMatrix: { get: function () { updateMembers(this); return this._invViewMatrix; }, }, /** * Gets the {@link Cartographic} position of the camera, with longitude and latitude * expressed in radians and height in meters. In 2D and Columbus View, it is possible * for the returned longitude and latitude to be outside the range of valid longitudes * and latitudes when the camera is outside the map. * @memberof Camera.prototype * * @type {Cartographic} * @readonly */ positionCartographic: { get: function () { updateMembers(this); return this._positionCartographic; }, }, /** * Gets the position of the camera in world coordinates. * @memberof Camera.prototype * * @type {Cartesian3} * @readonly */ positionWC: { get: function () { updateMembers(this); return this._positionWC; }, }, /** * Gets the view direction of the camera in world coordinates. * @memberof Camera.prototype * * @type {Cartesian3} * @readonly */ directionWC: { get: function () { updateMembers(this); return this._directionWC; }, }, /** * Gets the up direction of the camera in world coordinates. * @memberof Camera.prototype * * @type {Cartesian3} * @readonly */ upWC: { get: function () { updateMembers(this); return this._upWC; }, }, /** * Gets the right direction of the camera in world coordinates. * @memberof Camera.prototype * * @type {Cartesian3} * @readonly */ rightWC: { get: function () { updateMembers(this); return this._rightWC; }, }, /** * Gets the camera heading in radians. * @memberof Camera.prototype * * @type {number} * @readonly */ heading: { get: function () { if (this._mode !== SceneMode.MORPHING) { const ellipsoid = this._projection.ellipsoid; const oldTransform = Matrix4.clone(this._transform, scratchHPRMatrix1); const transform = Transforms.eastNorthUpToFixedFrame( this.positionWC, ellipsoid, scratchHPRMatrix2, ); this._setTransform(transform); const heading = getHeading(this.direction, this.up); this._setTransform(oldTransform); return heading; } return undefined; }, }, /** * Gets the camera pitch in radians. * @memberof Camera.prototype * * @type {number} * @readonly */ pitch: { get: function () { if (this._mode !== SceneMode.MORPHING) { const ellipsoid = this._projection.ellipsoid; const oldTransform = Matrix4.clone(this._transform, scratchHPRMatrix1); const transform = Transforms.eastNorthUpToFixedFrame( this.positionWC, ellipsoid, scratchHPRMatrix2, ); this._setTransform(transform); const pitch = getPitch(this.direction); this._setTransform(oldTransform); return pitch; } return undefined; }, }, /** * Gets the camera roll in radians. * @memberof Camera.prototype * * @type {number} * @readonly */ roll: { get: function () { if (this._mode !== SceneMode.MORPHING) { const ellipsoid = this._projection.ellipsoid; const oldTransform = Matrix4.clone(this._transform, scratchHPRMatrix1); const transform = Transforms.eastNorthUpToFixedFrame( this.positionWC, ellipsoid, scratchHPRMatrix2, ); this._setTransform(transform); const roll = getRoll(this.direction, this.up, this.right); this._setTransform(oldTransform); return roll; } return undefined; }, }, /** * Gets the event that will be raised at when the camera starts to move. * @memberof Camera.prototype * @type {Event} * @readonly */ moveStart: { get: function () { return this._moveStart; }, }, /** * Gets the event that will be raised when the camera has stopped moving. * @memberof Camera.prototype * @type {Event} * @readonly */ moveEnd: { get: function () { return this._moveEnd; }, }, /** * Gets the event that will be raised when the camera has changed by <code>percentageChanged</code>. * @memberof Camera.prototype * @type {Event} * @readonly */ changed: { get: function () { return this._changed; }, }, }); /** * @private */ Camera.prototype.update = function (mode) { //>>includeStart('debug', pragmas.debug); if (!defined(mode)) { throw new DeveloperError("mode is required."); } if ( mode === SceneMode.SCENE2D && !(this.frustum instanceof OrthographicOffCenterFrustum) ) { throw new DeveloperError( "An OrthographicOffCenterFrustum is required in 2D.", ); } if ( (mode === SceneMode.SCENE3D || mode === SceneMode.COLUMBUS_VIEW) && !(this.frustum instanceof PerspectiveFrustum) && !(this.frustum instanceof OrthographicFrustum) ) { throw new DeveloperError( "A PerspectiveFrustum or OrthographicFrustum is required in 3D and Columbus view", ); } //>>includeEnd('debug'); let updateFrustum = false; if (mode !== this._mode) { this._mode = mode; this._modeChanged = mode !== SceneMode.MORPHING; updateFrustum = this._mode === SceneMode.SCENE2D; } if (updateFrustum) { const frustum = (this._max2Dfrustum = this.frustum.clone()); //>>includeStart('debug', pragmas.debug); if (!(frustum instanceof OrthographicOffCenterFrustum)) { throw new DeveloperError( "The camera frustum is expected to be orthographic for 2D camera control.", ); } //>>includeEnd('debug'); const maxZoomOut = 2.0; const ratio = frustum.top / frustum.right; frustum.right = this._maxCoord.x * maxZoomOut; frustum.left = -frustum.right; frustum.top = ratio * frustum.right; frustum.bottom = -frustum.top; } if (this._mode === SceneMode.SCENE2D) { clampMove2D(this, this.position); } }; const setTransformPosition = new Cartesian3(); const setTransformUp = new Cartesian3(); const setTransformDirection = new Cartesian3(); Camera.prototype._setTransform = function (transform) { const position = Cartesian3.clone(this.positionWC, setTransformPosition); const up = Cartesian3.clone(this.upWC, setTransformUp); const direction = Cartesian3.clone(this.directionWC, setTransformDirection); Matrix4.clone(transform, this._transform); this._transformChanged = true; updateMembers(this); const inverse = this._actualInvTransform; Matrix4.multiplyByPoint(inverse, position, this.position); Matrix4.multiplyByPointAsVector(inverse, direction, this.direction); Matrix4.multiplyByPointAsVector(inverse, up, this.up); Cartesian3.cross(this.direction, this.up, this.right); updateMembers(this); }; const scratchAdjustOrthographicFrustumMousePosition = new Cartesian2(); const scratchPickRay = new Ray(); const scratchRayIntersection = new Cartesian3(); const scratchDepthIntersection = new Cartesian3(); function calculateOrthographicFrustumWidth(camera) { // Camera is fixed to an object, so keep frustum width constant. if (!Matrix4.equals(Matrix4.IDENTITY, camera.transform)) { return Cartesian3.magnitude(camera.position); } const scene = camera._scene; const globe = scene.globe; const mousePosition = scratchAdjustOrthographicFrustumMousePosition; mousePosition.x = scene.drawingBufferWidth / scene.pixelRatio / 2.0; mousePosition.y = scene.drawingBufferHeight / scene.pixelRatio / 2.0; let rayIntersection; if (defined(globe)) { const ray = camera.getPickRay(mousePosition, scratchPickRay); rayIntersection = globe.pickWorldCoordinates( ray, scene, true, scratchRayIntersection, ); } let depthIntersection; if (scene.pickPositionSupported) { depthIntersection = scene.pickPositionWorldCoordinates( mousePosition, scratchDepthIntersection, ); } let distance; if (defined(rayIntersection) || defined(depthIntersection)) { const depthDistance = defined(depthIntersection) ? Cartesian3.distance(depthIntersection, camera.positionWC) : Number.POSITIVE_INFINITY; const rayDistance = defined(rayIntersection) ? Cartesian3.distance(rayIntersection, camera.positionWC) : Number.POSITIVE_INFINITY; distance = Math.min(depthDistance, rayDistance); } else { distance = Math.max(camera.positionCartographic.height, 0.0); } return distance; } Camera.prototype._adjustOrthographicFrustum = function (zooming) { if (!(this.frustum instanceof OrthographicFrustum)) { return; } if (!zooming && this._positionCartographic.height < 150000.0) { return; } this.frustum.width = calculateOrthographicFrustumWidth(this); }; const scratchSetViewCartesian = new Cartesian3(); const scratchSetViewTransform1 = new Matrix4(); const scratchSetViewTransform2 = new Matrix4(); const scratchSetViewQuaternion = new Quaternion(); const scratchSetViewMatrix3 = new Matrix3(); const scratchSetViewCartographic = new Cartographic(); function setView3D(camera, position, hpr) { //>>includeStart('debug', pragmas.debug); if (isNaN(position.x) || isNaN(position.y) || isNaN(position.z)) { throw new DeveloperError("position has a NaN component"); } //>>includeEnd('debug'); const currentTransform = Matrix4.clone( camera.transform, scratchSetViewTransform1, ); const localTransform = Transforms.eastNorthUpToFixedFrame( position, camera._projection.ellipsoid, scratchSetViewTransform2, ); camera._setTransform(localTransform); Cartesian3.clone(Cartesian3.ZERO, camera.position); hpr.heading = hpr.heading - CesiumMath.PI_OVER_TWO; const rotQuat = Quaternion.fromHeadingPitchRoll( hpr, scratchSetViewQuaternion, ); const rotMat = Matrix3.fromQuaternion(rotQuat, scratchSetViewMatrix3); Matrix3.getColumn(rotMat, 0, camera.direction); Matrix3.getColumn(rotMat, 2, camera.up); Cartesian3.cross(camera.direction, camera.up, camera.right); camera._setTransform(currentTransform); camera._adjustOrthographicFrustum(true); } function setViewCV(camera, position, hpr, convert) { const currentTransform = Matrix4.clone( camera.transform, scratchSetViewTransform1, ); camera._setTransform(Matrix4.IDENTITY); if (!Cartesian3.equals(position, camera.positionWC)) { if (convert) { const projection = camera._projection; const cartographic = projection.ellipsoid.cartesianToCartographic( position, scratchSetViewCartographic, ); position = projection.project(cartographic, scratchSetViewCartesian); } Cartesian3.clone(position, camera.position); } hpr.heading = hpr.heading - CesiumMath.PI_OVER_TWO; const rotQuat = Quaternion.fromHeadingPitchRoll( hpr, scratchSetViewQuaternion, ); const rotMat = Matrix3.fromQuaternion(rotQuat, scratchSetViewMatrix3); Matrix3.getColumn(rotMat, 0, camera.direction); Matrix3.getColumn(rotMat, 2, camera.up); Cartesian3.cross(camera.direction, camera.up, camera.right); camera._setTransform(currentTransform); camera._adjustOrthographicFrustum(true); } function setView2D(camera, position, hpr, convert) { const currentTransform = Matrix4.clone( camera.transform, scratchSetViewTransform1, ); camera._setTransform(Matrix4.IDENTITY); if (!Cartesian3.equals(position, camera.positionWC)) { if (convert) { const projection = camera._projection; const cartographic = projection.ellipsoid.cartesianToCartographic( position, scratchSetViewCartographic, ); position = projection.project(cartographic, scratchSetViewCartesian); } Cartesian2.clone(position, camera.position); const newLeft = -position.z * 0.5; const newRight = -newLeft; const frustum = camera.frustum; if (newRight > newLeft) { const ratio = frustum.top / frustum.right; frustum.right = newRight; frustum.left = newLeft; frustum.top = frustum.right * ratio; frustum.bottom = -frustum.top; } } if (camera._scene.mapMode2D === MapMode2D.ROTATE) { hpr.heading = hpr.heading - CesiumMath.PI_OVER_TWO; hpr.pitch = -CesiumMath.PI_OVER_TWO; hpr.roll = 0.0; const rotQuat = Quaternion.fromHeadingPitchRoll( hpr, scratchSetViewQuaternion, ); const rotMat = Matrix3.fromQuaternion(rotQuat, scratchSetViewMatrix3); Matrix3.getColumn(rotMat, 2, camera.up); Cartesian3.cross(camera.direction, camera.up, camera.right); } camera._setTransform(currentTransform); } const scratchToHPRDirection = new Cartesian3(); const scratchToHPRUp = new Cartesian3(); const scratchToHPRRight = new Cartesian3(); function directionUpToHeadingPitchRoll(camera, position, orientation, result) { const direction = Cartesian3.clone( orientation.direction, scratchToHPRDirection, ); const up = Cartesian3.clone(orientation.up, scratchToHPRUp); if (camera._scene.mode === SceneMode.SCENE3D) { const ellipsoid = camera._projection.ellipsoid; const transform = Transforms.eastNorthUpToFixedFrame( position, ellipsoid, scratchHPRMatrix1, ); const invTransform = Matrix4.inverseTransformation( transform, scratchHPRMatrix2, ); Matrix4.multiplyByPointAsVector(invTransform, direction, direction); Matrix4.multiplyByPointAsVector(invTransform, up, up); } const right = Cartesian3.cross(direction, up, scratchToHPRRight); result.heading = getHeading(direction, up); result.pitch = getPitch(direction); result.roll = getRoll(direction, up, right); return result; } const scratchSetViewOptions = { destination: undefined, orientation: { direction: undefined, up: undefined, heading: undefined, pitch: undefined, roll: undefined, }, convert: undefined, endTransform: undefined, }; const scratchHpr = new HeadingPitchRoll(); /** * Sets the camera position, orientation and transform. * * @param {object} options Object with the following properties: * @param {Cartesian3|Rectangle} [options.destination] The final position of the camera in world coordinates or a rectangle that would be visible from a top-down view. * @param {HeadingPitchRollValues|DirectionUp} [options.orientation] An object that contains either direction and up properties or heading, pitch and roll properties. By default, the direction will point * towards the center of the frame in 3D and in the negative z direction in Columbus view. The up direction will point towards local north in 3D and in the positive * y direction in Columbus view. Orientation is not used in 2D when in infinite scrolling mode. * @param {Matrix4} [options.endTransform] Transform matrix representing the reference frame of the camera. * @param {boolean} [options.convert] Whether to convert the destination from world coordinates to scene coordinates (only relevant when not using 3D). Defaults to <code>true</code>. * * @example * // 1. Set position with a top-down view * viewer.camera.setView({ * destination : Cesium.Cartesian3.fromDegrees(-117.16, 32.71, 15000.0) * }); * * // 2 Set view with heading, pitch and roll * viewer.camera.setView({ * destination : cartesianPosition, * orientation: { * heading : Cesium.Math.toRadians(90.0), // east, default value is 0.0 (north) * pitch : Cesium.Math.toRadians(-90), // default value (looking down) * roll : 0.0 // default value * } * }); * * // 3. Change heading, pitch and roll with the camera position remaining the same. * viewer.camera.setView({ * orientation: { * heading : Cesium.Math.toRadians(90.0), // east, default value is 0.0 (north) * pitch : Cesium.Math.toRadians(-90), // default value (looking down) * roll : 0.0 // default value * } * }); * * * // 4. View rectangle with a top-down view * viewer.camera.setView({ * destination : Cesium.Rectangle.fromDegrees(west, south, east, north) * }); * * // 5. Set position with an orientation using unit vectors. * viewer.camera.setView({ * destination : Cesium.Cartesian3.fromDegrees(-122.19, 46.25, 5000.0), * orientation : { * direction : new Cesium.Cartesian3(-0.04231243104240401, -0.20123236049443421, -0.97862924300734), * up : new Cesium.Cartesian3(-0.47934589305293746, -0.8553216253114552, 0.1966022179118339) * } * }); */ Camera.prototype.setView = function (options) { options = options ?? Frozen.EMPTY_OBJECT; let orientation = options.orientation ?? Frozen.EMPTY_OBJECT; const mode = this._mode; if (mode === SceneMode.MORPHING) { return; } if (defined(options.endTransform)) { this._setTransform(options.endTransform); } let convert = options.convert ?? true; let destination = options.destination ?? Cartesian3.clone(this.positionWC, scratchSetViewCartesian); if (defined(destination) && defined(destination.west)) { destination = this.getRectangleCameraCoordinates( destination, scratchSetViewCartesian, ); //>>includeStart('debug', pragmas.debug); // destination.z may be null in 2D, but .x and .y should be numeric if (isNaN(destination.x) || isNaN(destination.y)) { throw new DeveloperError(`destination has a NaN component`); } //>>includeEnd('debug'); convert = false; } if (defined(orientation.direction)) { orientation = directionUpToHeadingPitchRoll( this, destination, orientation, scratchSetViewOptions.orientation, ); } scratchHpr.heading = orientation.heading ?? 0.0; scratchHpr.pitch = orientation.pitch ?? -CesiumMath.PI_OVER_TWO; scratchHpr.roll = orientation.roll ?? 0.0; if (mode === SceneMode.SCENE3D) { setView3D(this, destination, scratchHpr); } else if (mode === SceneMode.SCENE2D) { setView2D(this, destination, scratchHpr, convert); } else { setViewCV(this, destination, scratchHpr, convert); } }; const pitchScratch = new Cartesian3(); /** * Fly the camera to the home view. Use {@link Camera#.DEFAULT_VIEW_RECTANGLE} to set * the default view for the 3D scene. The home view for 2D and columbus view shows the * entire map. * * @param {number} [duration] The duration of the flight in seconds. If omitted, Cesium attempts to calculate an ideal duration based on the distance to be traveled by the flight. See {@link Camera#flyTo} */ Camera.prototype.flyHome = function (duration) { const mode = this._mode; if (mode === SceneMode.MORPHING) { this._scene.completeMorph(); } if (mode === SceneMode.SCENE2D) { this.flyTo({ destination: Camera.DEFAULT_VIEW_RECTANGLE, duration: duration, endTransform: Matrix4.IDENTITY, }); } else if (mode === SceneMode.SCENE3D) { const destination = this.getRectangleCameraCoordinates( Camera.DEFAULT_VIEW_RECTANGLE, ); let mag = Cartesian3.magnitude(destination); mag += mag * Camera.DEFAULT_VIEW_FACTOR; Cartesian3.normalize(destination, destination); Cartesian3.multiplyByScalar(destination, mag, destination); this.flyTo({ destination: destination, duration: duration, endTransform: Matrix4.IDENTITY, }); } else if (mode === SceneMode.COLUMBUS_VIEW) { const maxRadii = this._projection.ellipsoid.maximumRadius; let position = new Cartesian3(0.0, -1.0, 1.0); position = Cartesian3.multiplyByScalar( Cartesian3.normalize(position, position), 5.0 * maxRadii, position, ); this.flyTo({ destination: position, duration: duration, orientation: { heading: 0.0, pitch: -Math.acos(Cartesian3.normalize(position, pitchScratch).z), roll: 0.0, }, endTransform: Matrix4.IDENTITY, convert: false, }); } }; /** * Transform a vector or point from world coordinates to the camera's reference frame. * * @param {Cartesian4} cartesian The vector or point to transform. * @param {Cartesian4} [result] The object onto which to store the result. * @returns {Cartesian4} The transformed vector or point. */ Camera.prototype.worldToCameraCoordinates = function (cartesian, result) { //>>includeStart('debug', pragmas.debug); if (!defined(cartesian)) { throw new DeveloperError("cartesian is required."); } //>>includeEnd('debug'); if (!defined(result)) { result = new Cartesian4(); } updateMembers(this); return Matrix4.multiplyByVector(this._actualInvTransform, cartesian, result); }; /** * Transform a point from world coordinates to the camera's reference frame. * * @param {Cartesian3} cartesian The point to transform. * @param {Cartesian3} [result] The object onto which to store the result. * @returns {Cartesian3} The transformed point. */ Camera.prototype.worldToCameraCoordinatesPoint = function (cartesian, result) { //>>includeStart('debug', pragmas.debug); if (!defined(cartesian)) { throw new DeveloperError("cartesian is required."); } //>>includeEnd('debug'); if (!defined(result)) { result = new Cartesian3(); } updateMembers(this); return Matrix4.multiplyByPoint(this._actualInvTransform, cartesian, result); }; /** * Transform a vector from world coordinates to the camera's reference frame. * * @param {Cartesian3} cartesian The vector to transform. * @param {Cartesian3} [result] The object onto which to store the result. * @returns {Cartesian3} The transformed vector. */ Camera.prototype.worldToCameraCoordinatesVector = function (cartesian, result) { //>>includeStart('debug', pragmas.debug); if (!defined(cartesian)) { throw new DeveloperError("cartesian is required."); } //>>includeEnd('debug'); if (!defined(result)) { result = new Cartesian3(); } updateMembers(this); return Matrix4.multiplyByPointAsVector( this._actualInvTransform, cartesian, result, ); }; /** * Transform a vector or point from the camera's reference frame to world coordinates. * * @param {Cartesian4} cartesian The vector or point to transform. * @param {Cartesian4} [result] The object onto which to store the result. * @returns {Cartesian4} The transformed vector or point. */ Camera.prototype.cameraToWorldCoordinates = function (cartesian, result) { //>>includeStart('debug', pragmas.debug); if (!defined(cartesian)) { throw new DeveloperError("cartesian is required."); } //>>includeEnd('debug'); if (!defined(result)) { result = new Cartesian4(); } updateMembers(this); return Matrix4.multiplyByVector(this._actualTransform, cartesian, result); }; /** * Transform a point from the camera's reference frame to world coordinates. * * @param {Cartesian3} cartesian The point to transform. * @param {Cartesian3} [result] The object onto which to store the result. * @returns {Cartesian3} The transformed point. */ Camera.prototype.cameraToWorldCoordinatesPoint = function (cartesian, result) { //>>includeStart('debug', pragmas.debug); if (!defined(cartesian)) { throw new DeveloperError("cartesian is required."); } //>>includeEnd('debug'); if (!defined(result)) { result = new Cartesian3(); } updateMembers(this); return Matrix4.multiplyByPoint(this._actualTransform, cartesian, result); }; /** * Transform a vector from the camera's reference frame to world coordinates. * * @param {Cartesian3} cartesian The vector to transform. * @param {Cartesian3} [result] The object onto which to store the result. * @returns {Cartesian3} The transformed vector. */ Camera.prototype.cameraToWorldCoordinatesVector = function (cartesian, result) { //>>includeStart('debug', pragmas.debug); if (!defined(cartesian)) { throw new DeveloperError("cartesian is required."); } //>>includeEnd('debug'); if (!defined(result)) { result = new Cartesian3(); } updateMembers(this); return Matrix4.multiplyByPointAsVector( this._actualTransform, cartesian, result, ); }; function clampMove2D(camera, position) { const rotatable2D = camera._scene.mapMode2D === MapMode2D.ROTATE; const maxProjectedX = camera._maxCoord.x; const maxProjectedY = camera._maxCoord.y; let minX; let maxX; if (rotatable2D) { maxX = maxProjectedX; minX = -maxX; } else {