// 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, Vector2, Matrix4, assert, NumericArray} from '@math.gl/core';
import {CullingVolume} from './culling-volume';
import {Plane} from './plane';
const scratchPlaneUpVector = new Vector3();
const scratchPlaneRightVector = new Vector3();
const scratchPlaneNearCenter = new Vector3();
const scratchPlaneFarCenter = new Vector3();
const scratchPlaneNormal = new Vector3();
type PerspectiveOffCenterFrustumOptions = {
left?: number;
right?: number;
top?: number;
bottom?: number;
near?: number;
far?: number;
};
export class PerspectiveOffCenterFrustum {
/**
* Defines the left clipping plane.
* @type {Number}
* @default undefined
*/
left?: number;
private _left?: number;
/**
* Defines the right clipping plane.
* @type {Number}
* @default undefined
*/
right?: number;
private _right?: number;
/**
* Defines the top clipping plane.
* @type {Number}
* @default undefined
*/
top?: number;
private _top?: number;
/**
* Defines the bottom clipping plane.
* @type {Number}
* @default undefined
*/
bottom?: number;
private _bottom?: number;
/**
* The distance of the near plane.
* @type {Number}
* @default 1.0
*/
near: number;
private _near: number;
/**
* The distance of the far plane.
* @type {Number}
* @default 500000000.0
*/
far: number;
private _far: number;
private _cullingVolume = new CullingVolume([
new Plane(),
new Plane(),
new Plane(),
new Plane(),
new Plane(),
new Plane()
]);
private _perspectiveMatrix = new Matrix4();
private _infinitePerspective = new Matrix4();
/**
* 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: PerspectiveOffCenterFrustumOptions = {}) {
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(): PerspectiveOffCenterFrustum {
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: PerspectiveOffCenterFrustum): boolean {
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(): Matrix4 {
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(): Matrix4 {
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: Readonly,
/** A Vector3 defines the view direction. */
direction: Readonly,
/** A Vector3 defines the up direction. */
up: Readonly
): CullingVolume {
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: 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();
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
private _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
});
}
}
}