// 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 { Vector3, Matrix4, assert } from '@math.gl/core'; import { CullingVolume } from "./culling-volume.js"; import { Plane } from "./plane.js"; const scratchPlaneUpVector = new Vector3(); const scratchPlaneRightVector = new Vector3(); const scratchPlaneNearCenter = new Vector3(); const scratchPlaneFarCenter = new Vector3(); const scratchPlaneNormal = new Vector3(); export class PerspectiveOffCenterFrustum { /** * 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 PerspectiveOffCenterFrustum * * @example * const frustum = new PerspectiveOffCenterFrustum({ * left : -1.0, * right : 1.0, * top : 1.0, * bottom : -1.0, * near : 1.0, * far : 100.0 * }); * * @see PerspectiveFrustum */ constructor(options = {}) { this._cullingVolume = new CullingVolume([ new Plane(), new Plane(), new Plane(), new Plane(), new Plane(), new Plane() ]); this._perspectiveMatrix = new Matrix4(); this._infinitePerspective = new Matrix4(); const { near = 1.0, far = 500000000.0 } = options; this.left = options.left; this._left = undefined; this.right = options.right; this._right = undefined; this.top = options.top; this._top = undefined; this.bottom = options.bottom; this._bottom = undefined; this.near = near; this._near = near; this.far = far; this._far = far; } /** * Returns a duplicate of a PerspectiveOffCenterFrustum instance. * @returns {PerspectiveOffCenterFrustum} A new PerspectiveFrustum instance. * */ clone() { return new PerspectiveOffCenterFrustum({ right: this.right, left: this.left, top: this.top, bottom: this.bottom, near: this.near, far: this.far }); } /** * Compares the provided PerspectiveOffCenterFrustum componentwise and returns * true if they are equal, false otherwise. * * @returns {Boolean} true if they are equal, false otherwise. */ equals(other) { return (other && other instanceof PerspectiveOffCenterFrustum && this.right === other.right && this.left === other.left && this.top === other.top && this.bottom === other.bottom && this.near === other.near && this.far === other.far); } /** * Gets the perspective projection matrix computed from the view frustum. * @memberof PerspectiveOffCenterFrustum.prototype * @type {Matrix4} * * @see PerspectiveOffCenterFrustum#infiniteProjectionMatrix */ get projectionMatrix() { this._update(); return this._perspectiveMatrix; } /** * Gets the perspective projection matrix computed from the view frustum with an infinite far plane. * @memberof PerspectiveOffCenterFrustum.prototype * @type {Matrix4} * * @see PerspectiveOffCenterFrustum#projectionMatrix */ get infiniteProjectionMatrix() { this._update(); return this._infinitePerspective; } /** * Creates a culling volume for this frustum. * @returns {CullingVolume} A culling volume at the given position and orientation. * * @example * // Check if a bounding volume intersects the frustum. * const cullingVolume = frustum.computeCullingVolume(cameraPosition, cameraDirection, cameraUp); * const intersect = cullingVolume.computeVisibility(boundingVolume); */ // eslint-disable-next-line complexity, max-statements computeCullingVolume( /** A Vector3 defines the eye position. */ position, /** A Vector3 defines the view direction. */ direction, /** A Vector3 defines the up direction. */ up) { assert(position, 'position is required.'); assert(direction, 'direction is required.'); assert(up, 'up is required.'); const planes = this._cullingVolume.planes; up = scratchPlaneUpVector.copy(up).normalize(); const right = scratchPlaneRightVector.copy(direction).cross(up).normalize(); const nearCenter = scratchPlaneNearCenter .copy(direction) .multiplyByScalar(this.near) .add(position); const farCenter = scratchPlaneFarCenter .copy(direction) .multiplyByScalar(this.far) .add(position); let normal = scratchPlaneNormal; // Left plane computation normal.copy(right).multiplyByScalar(this.left).add(nearCenter).subtract(position).cross(up); planes[0].fromPointNormal(position, normal); // Right plane computation normal .copy(right) .multiplyByScalar(this.right) .add(nearCenter) .subtract(position) .cross(up) .negate(); planes[1].fromPointNormal(position, normal); // Bottom plane computation normal .copy(up) .multiplyByScalar(this.bottom) .add(nearCenter) .subtract(position) .cross(right) .negate(); planes[2].fromPointNormal(position, normal); // Top plane computation normal.copy(up).multiplyByScalar(this.top).add(nearCenter).subtract(position).cross(right); planes[3].fromPointNormal(position, normal); normal = new Vector3().copy(direction); // Near plane computation planes[4].fromPointNormal(nearCenter, normal); // Far plane computation normal.negate(); planes[5].fromPointNormal(farCenter, normal); return this._cullingVolume; } /** * 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. * const pixelSize = camera.frustum.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. * const position = camera.position; * const direction = camera.direction; * const toCenter = Vector3.subtract(primitive.boundingVolume.center, position, new Vector3()); // vector from camera to a primitive * const toCenterProj = Vector3.multiplyByScalar(direction, Vector3.dot(direction, toCenter), new Vector3()); // project vector onto camera direction vector * const distance = Vector3.magnitude(toCenterProj); * const pixelSize = camera.frustum.getPixelDimensions(scene.drawingBufferWidth, scene.drawingBufferHeight, distance, new Vector2()); */ getPixelDimensions( /** The width of the drawing buffer. */ drawingBufferWidth, /** The height of the drawing buffer. */ drawingBufferHeight, /** The distance to the near plane in meters. */ distance, /** The object onto which to store the result. */ result) { this._update(); assert(Number.isFinite(drawingBufferWidth) && Number.isFinite(drawingBufferHeight)); // 'Both drawingBufferWidth and drawingBufferHeight are required.' assert(drawingBufferWidth > 0); // 'drawingBufferWidth must be greater than zero.' assert(drawingBufferHeight > 0); // 'drawingBufferHeight must be greater than zero.' assert(distance > 0); // 'distance is required.'); assert(result); // 'A result object is required.'); const inverseNear = 1.0 / this.near; let tanTheta = this.top * inverseNear; const pixelHeight = (2.0 * distance * tanTheta) / drawingBufferHeight; tanTheta = this.right * inverseNear; const pixelWidth = (2.0 * distance * tanTheta) / drawingBufferWidth; result.x = pixelWidth; result.y = pixelHeight; return result; } // eslint-disable-next-line complexity, max-statements _update() { assert(Number.isFinite(this.right) && Number.isFinite(this.left) && Number.isFinite(this.top) && Number.isFinite(this.bottom) && Number.isFinite(this.near) && Number.isFinite(this.far)); // throw new DeveloperError('right, left, top, bottom, near, or far parameters are not set.'); const { top, bottom, right, left, near, far } = this; if (top !== this._top || bottom !== this._bottom || left !== this._left || right !== this._right || near !== this._near || far !== this._far) { assert(this.near > 0 && this.near < this.far, 'near must be greater than zero and less than far.'); this._left = left; this._right = right; this._top = top; this._bottom = bottom; this._near = near; this._far = far; this._perspectiveMatrix = new Matrix4().frustum({ left, right, bottom, top, near, far }); this._infinitePerspective = new Matrix4().frustum({ left, right, bottom, top, near, far: Infinity }); } } } //# sourceMappingURL=perspective-off-center-frustum.js.map