// math.gl // SPDX-License-Identifier: MIT and Apache-2.0 // Copyright (c) vis.gl contributors // This file is derived from the Cesium math library under Apache 2 license // See LICENSE.md and https://github.com/AnalyticalGraphicsInc/cesium/blob/master/LICENSE.md // Note: This class is still an experimental export, mainly used by other test cases // - It has not been fully adapted to math.gl conventions // - Documentation has not been ported import {NumericArray} from '@math.gl/types'; import {assert, Matrix4, Vector2} from '@math.gl/core'; import {PerspectiveOffCenterFrustum} from './perspective-off-center-frustum'; import {CullingVolume} from './culling-volume'; const defined = (val: unknown) => val !== null && typeof val !== 'undefined'; type PerspectiveFrustumOptions = { /** The angle of the field of view (FOV), in radians. */ fov?: number; /** The aspect ratio of the frustum's width to it's height. */ aspectRatio?: number; /** The distance of the near plane. */ near?: number; /** The distance of the far plane. */ far?: number; /** The offset in the x direction. */ xOffset?: number; /** The offset in the y direction. */ yOffset?: number; }; /** * The viewing frustum is defined by 6 planes. * Each plane is represented by a {@link Vector4} 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 PerspectiveFrustum * * @example * var frustum = new PerspectiveFrustum({ * fov : Math.PI_OVER_THREE, * aspectRatio : canvas.clientWidth / canvas.clientHeight * near : 1.0, * far : 1000.0 * }); * * @see PerspectiveOffCenterFrustum */ export class PerspectiveFrustum { private _offCenterFrustum = new PerspectiveOffCenterFrustum(); /** * The angle of the field of view (FOV), in radians. This angle will be used * as the horizontal FOV if the width is greater than the height, otherwise * it will be the vertical FOV. */ fov?: number; private _fov: number; private _fovy: number; private _sseDenominator: number; /** * The aspect ratio of the frustum's width to it's height. */ aspectRatio?: number; private _aspectRatio: number; /** * The distance of the near plane. * @default 1.0 */ near: number; private _near: number; /** * The distance of the far plane. * @default 500000000.0 */ far: number; private _far: number; /** * Offsets the frustum in the x direction. * @default 0.0 */ xOffset: number; private _xOffset: number; /** * Offsets the frustum in the y direction. * @default 0.0 */ yOffset: number; private _yOffset: number; constructor(options: PerspectiveFrustumOptions = {}) { const {fov, aspectRatio, near = 1.0, far = 500000000.0, xOffset = 0.0, yOffset = 0.0} = options; this.fov = fov; this.aspectRatio = aspectRatio; this.near = near; this.far = far; this.xOffset = xOffset; this.yOffset = yOffset; } /** * Returns a duplicate of a PerspectiveFrustum instance. */ clone(): PerspectiveFrustum { return new PerspectiveFrustum({ aspectRatio: this.aspectRatio, fov: this.fov, near: this.near, far: this.far }); } /** * Compares the provided PerspectiveFrustum componentwise and returns * true if they are equal, false otherwise. */ equals(other: PerspectiveFrustum): boolean { if (!defined(other) || !(other instanceof PerspectiveFrustum)) { return false; } this._update(); other._update(); return ( this.fov === other.fov && this.aspectRatio === other.aspectRatio && this.near === other.near && this.far === other.far && this._offCenterFrustum.equals(other._offCenterFrustum) ); } /** * Gets the perspective projection matrix computed from the view this. */ get projectionMatrix(): Matrix4 { this._update(); return this._offCenterFrustum.projectionMatrix; } /** * The perspective projection matrix computed from the view frustum with an infinite far plane. */ get infiniteProjectionMatrix(): Matrix4 { this._update(); return this._offCenterFrustum.infiniteProjectionMatrix; } /** * Gets the angle of the vertical field of view, in radians. */ get fovy(): number { this._update(); return this._fovy; } /** * @private */ get sseDenominator(): number { this._update(); return this._sseDenominator; } /** * Creates a culling volume for this this.ion. * @returns {CullingVolume} A culling volume at the given position and orientation. * * @example * // Check if a bounding volume intersects the this. * var cullingVolume = this.computeCullingVolume(cameraPosition, cameraDirection, cameraUp); * var intersect = cullingVolume.computeVisibility(boundingVolume); */ computeCullingVolume( /** A Vector3 defines the eye position. */ position: Readonly, /** A Vector3 defines the view direction. */ direction: Readonly, /** A Vector3 defines the up direction. */ up: Readonly ): CullingVolume { this._update(); return this._offCenterFrustum.computeCullingVolume(position, direction, up); } /** * Returns the pixel's width and height in meters. * @returns {Vector2} The modified result parameter or a new instance of {@link Vector2} with the pixel's width and height in the x and y properties, respectively. * * @exception {DeveloperError} drawingBufferWidth must be greater than zero. * @exception {DeveloperError} drawingBufferHeight must be greater than zero. * * @example * // Example 1 * // Get the width and height of a pixel. * var pixelSize = camera.this.getPixelDimensions(scene.drawingBufferWidth, scene.drawingBufferHeight, 1.0, new Vector2()); * * @example * // Example 2 * // Get the width and height of a pixel if the near plane was set to 'distance'. * // For example, get the size of a pixel of an image on a billboard. * var position = camera.position; * var direction = camera.direction; * var toCenter = Vector3.subtract(primitive.boundingVolume.center, position, new Vector3()); // vector from camera to a primitive * var toCenterProj = Vector3.multiplyByScalar(direction, Vector3.dot(direction, toCenter), new Vector3()); // project vector onto camera direction vector * var distance = Vector3.magnitude(toCenterProj); * var pixelSize = camera.this.getPixelDimensions(scene.drawingBufferWidth, scene.drawingBufferHeight, distance, new Vector2()); */ getPixelDimensions( /** The width of the drawing buffer. */ drawingBufferWidth: number, /** The height of the drawing buffer. */ drawingBufferHeight: number, /** The distance to the near plane in meters. */ distance: number, /** The object onto which to store the result. */ result?: Vector2 ): Vector2 { this._update(); return this._offCenterFrustum.getPixelDimensions( drawingBufferWidth, drawingBufferHeight, distance, result || new Vector2() ); } // eslint-disable-next-line complexity, max-statements private _update(): void { assert( Number.isFinite(this.fov) && Number.isFinite(this.aspectRatio) && Number.isFinite(this.near) && Number.isFinite(this.far) ); // 'fov, aspectRatio, near, or far parameters are not set.' const f = this._offCenterFrustum; if ( this.fov !== this._fov || this.aspectRatio !== this._aspectRatio || this.near !== this._near || this.far !== this._far || this.xOffset !== this._xOffset || this.yOffset !== this._yOffset ) { assert(this.fov >= 0 && this.fov < Math.PI); // throw new DeveloperError('fov must be in the range [0, PI).'); assert(this.aspectRatio > 0); // throw new DeveloperError('aspectRatio must be positive.'); assert(this.near >= 0 && this.near < this.far); // throw new DeveloperError('near must be greater than zero and less than far.'); this._aspectRatio = this.aspectRatio; this._fov = this.fov; this._fovy = this.aspectRatio <= 1 ? this.fov : Math.atan(Math.tan(this.fov * 0.5) / this.aspectRatio) * 2.0; this._near = this.near; this._far = this.far; this._sseDenominator = 2.0 * Math.tan(0.5 * this._fovy); this._xOffset = this.xOffset; this._yOffset = this.yOffset; f.top = this.near * Math.tan(0.5 * this._fovy); f.bottom = -f.top; f.right = this.aspectRatio * f.top; f.left = -f.right; f.near = this.near; f.far = this.far; f.right += this.xOffset; f.left += this.xOffset; f.top += this.yOffset; f.bottom += this.yOffset; } } }