import { Camera } from './Camera.js'; import * as MathUtils from '../math/MathUtils.js'; class PerspectiveCamera extends Camera { constructor(fov = 50, aspect = 1, near = 0.1, far = 2000) { super(); this.type = 'PerspectiveCamera'; this.fov = fov; this.zoom = 1; this.near = near; this.far = far; this.focus = 10; this.aspect = aspect; this.view = null; this.filmGauge = 35; // width of the film (default in millimeters) this.filmOffset = 0; // horizontal film offset (same unit as gauge) this.updateProjectionMatrix(); } copy(source, recursive) { super.copy(source, recursive); this.fov = source.fov; this.zoom = source.zoom; this.near = source.near; this.far = source.far; this.focus = source.focus; this.aspect = source.aspect; this.view = source.view === null ? null : Object.assign({}, source.view); this.filmGauge = source.filmGauge; this.filmOffset = source.filmOffset; return this; } /** * Sets the FOV by focal length in respect to the current .filmGauge. * * The default film gauge is 35, so that the focal length can be specified for * a 35mm (full frame) camera. * * Values for focal length and film gauge must have the same unit. */ setFocalLength(focalLength) { /** see {@link http://www.bobatkins.com/photography/technical/field_of_view.html} */ const vExtentSlope = (0.5 * this.getFilmHeight()) / focalLength; this.fov = MathUtils.RAD2DEG * 2 * Math.atan(vExtentSlope); this.updateProjectionMatrix(); } /** * Calculates the focal length from the current .fov and .filmGauge. */ getFocalLength() { const vExtentSlope = Math.tan(MathUtils.DEG2RAD * 0.5 * this.fov); return (0.5 * this.getFilmHeight()) / vExtentSlope; } getEffectiveFOV() { return MathUtils.RAD2DEG * 2 * Math.atan(Math.tan(MathUtils.DEG2RAD * 0.5 * this.fov) / this.zoom); } getFilmWidth() { // film not completely covered in portrait format (aspect < 1) return this.filmGauge * Math.min(this.aspect, 1); } getFilmHeight() { // film not completely covered in landscape format (aspect > 1) return this.filmGauge / Math.max(this.aspect, 1); } /** * Sets an offset in a larger frustum. This is useful for multi-window or * multi-monitor/multi-machine setups. * * For example, if you have 3x2 monitors and each monitor is 1920x1080 and * the monitors are in grid like this * * +---+---+---+ * | A | B | C | * +---+---+---+ * | D | E | F | * +---+---+---+ * * then for each monitor you would call it like this * * const w = 1920; * const h = 1080; * const fullWidth = w * 3; * const fullHeight = h * 2; * * --A-- * camera.setViewOffset( fullWidth, fullHeight, w * 0, h * 0, w, h ); * --B-- * camera.setViewOffset( fullWidth, fullHeight, w * 1, h * 0, w, h ); * --C-- * camera.setViewOffset( fullWidth, fullHeight, w * 2, h * 0, w, h ); * --D-- * camera.setViewOffset( fullWidth, fullHeight, w * 0, h * 1, w, h ); * --E-- * camera.setViewOffset( fullWidth, fullHeight, w * 1, h * 1, w, h ); * --F-- * camera.setViewOffset( fullWidth, fullHeight, w * 2, h * 1, w, h ); * * Note there is no reason monitors have to be the same size or in a grid. */ setViewOffset(fullWidth, fullHeight, x, y, width, height) { this.aspect = fullWidth / fullHeight; if (this.view === null) { this.view = { enabled: true, fullWidth: 1, fullHeight: 1, offsetX: 0, offsetY: 0, width: 1, height: 1, }; } this.view.enabled = true; this.view.fullWidth = fullWidth; this.view.fullHeight = fullHeight; this.view.offsetX = x; this.view.offsetY = y; this.view.width = width; this.view.height = height; this.updateProjectionMatrix(); } clearViewOffset() { if (this.view !== null) { this.view.enabled = false; } this.updateProjectionMatrix(); } updateProjectionMatrix() { const near = this.near; let top = (near * Math.tan(MathUtils.DEG2RAD * 0.5 * this.fov)) / this.zoom; let height = 2 * top; let width = this.aspect * height; let left = -0.5 * width; const view = this.view; if (this.view !== null && this.view.enabled) { const fullWidth = view.fullWidth, fullHeight = view.fullHeight; left += (view.offsetX * width) / fullWidth; top -= (view.offsetY * height) / fullHeight; width *= view.width / fullWidth; height *= view.height / fullHeight; } const skew = this.filmOffset; if (skew !== 0) left += (near * skew) / this.getFilmWidth(); this.projectionMatrix.makePerspective(left, left + width, top, top - height, near, this.far); this.projectionMatrixInverse.copy(this.projectionMatrix).invert(); } toJSON(meta) { const data = super.toJSON(meta); data.object.fov = this.fov; data.object.zoom = this.zoom; data.object.near = this.near; data.object.far = this.far; data.object.focus = this.focus; data.object.aspect = this.aspect; if (this.view !== null) data.object.view = Object.assign({}, this.view); data.object.filmGauge = this.filmGauge; data.object.filmOffset = this.filmOffset; return data; } } PerspectiveCamera.prototype.isPerspectiveCamera = true; export { PerspectiveCamera };