UNPKG

@here/harp-mapview

Version:

Functionality needed to render a map.

367 lines 17.8 kB
"use strict"; /* * Copyright (C) 2021 HERE Europe B.V. * Licensed under Apache 2.0, see full license in LICENSE * SPDX-License-Identifier: Apache-2.0 */ Object.defineProperty(exports, "__esModule", { value: true }); exports.CameraUtils = void 0; const harp_utils_1 = require("@here/harp-utils"); const THREE = require("three"); const FovCalculation_1 = require("./FovCalculation"); // In centered projections the principal point is at NDC origin, splitting vertical and horizontal // fovs in two equal halves. function isCenteredProjection(principalPoint) { return principalPoint.x === 0 && principalPoint.y === 0; } /** * Computes the fov on the positive side of NDC x or y dimension (i.e. either right or top fov). * @param focalLength - Focal length in pixels. It must be larger than 0. * @param ppOffset - Principal point NDC offset either in y or x dimension. * @param viewportSide - Viewport height or width in pixels, must be same dimension as ppOffset. * @returns side fov in radians. */ function computePosSideFov(focalLength, ppOffset, viewportSide) { // see diagram in computeFocalLengthFromFov(). harp_utils_1.assert(focalLength > 0, "Focal length must be larger than 0"); return Math.atan(((1 - ppOffset) * viewportSide * 0.5) / focalLength); } /** * Computes the vertical or horizontal fov. * @param focalLength - Focal length in pixels. It must be larger than 0. * @param ppOffset - Principal point NDC offset in y (vertical fov) or x dimension (horizontal fov). * @param viewportSide - Viewport height or width in pixels, must be same dimension as ppOffset. * @returns vertical or horizontal fov in radians. */ function computeFov(focalLength, ppOffset, viewportSide) { harp_utils_1.assert(focalLength > 0, "Focal length must be larger than 0"); // For uncentered fov, compute the two fov sides separately. The fov on the negative NDC // side is computed in the same way as that for the positive side but flipping the offset sign. return ppOffset === 0 ? 2 * Math.atan((0.5 * viewportSide) / focalLength) : computePosSideFov(focalLength, ppOffset, viewportSide) + computePosSideFov(focalLength, -ppOffset, viewportSide); } function getFovs(camera) { return camera.userData.fovs; } /** * Saves camera vertical fov and focal length. For off-center projections, saves side fovs as well. */ function setCameraParams(camera, ppalPoint, focalLength, viewportHeight, verticalFov) { const viewportWidth = viewportHeight * camera.aspect; let hFov = computeFov(focalLength, ppalPoint.x, viewportWidth); if (hFov < FovCalculation_1.MIN_FOV_RAD || hFov > FovCalculation_1.MAX_FOV_RAD) { // Invalid horizontal fov, clamp and compute again focal length and vertical fov. hFov = THREE.MathUtils.clamp(hFov, FovCalculation_1.MIN_FOV_RAD, FovCalculation_1.MAX_FOV_RAD); const focalLength = computeFocalLengthFromFov(hFov, viewportWidth, ppalPoint.x); verticalFov = computeFov(focalLength, ppalPoint.y, viewportHeight); } camera.fov = THREE.MathUtils.radToDeg(verticalFov); if (isCenteredProjection(ppalPoint)) { delete camera.userData.fovs; } else { const width = viewportHeight * camera.aspect; camera.userData.fovs = { top: computePosSideFov(focalLength, ppalPoint.y, viewportHeight), right: computePosSideFov(focalLength, ppalPoint.x, width), horizontal: hFov }; } camera.userData.focalLength = focalLength; } /** * Computes a camera's focal length from vertical fov and viewport height or horizontal fov and * viewport width. * @beta * * @param fov - Vertical or horizontal field of view in radians. * @param viewportSide - Viewport height if fov is vertical, otherwise viewport width. * @param ppOffset - Principal point offset in y direction if fov is vertical, * otherwise in x direction. * @returns focal length in pixels. */ function computeFocalLengthFromFov(fov, viewportSide, ppOffset) { // C <- Projection center // /|-_ // / | -_ pfov = fov along positive NDC side (tfov or rfov) // / | -_ nfov = fov along negative NDC side (bfov or lfov) // / | -_ // / | -_ // /pfov | nfov -_ // / | -_ // / | -_ // a / |focal length(f) -_ b // / | -_ // / | Principal point -_ // / | / (pp) -_ // A/____________P________________________-_B Viewport // <------------><------------------------> // (1-ppOff)*s/2 (1+ppOff)*s/2 // <--------------------------------------> // s = viewportSide (height or width) // // Diagram of fov splitting (potentially asymmetric) along a viewport side (height or width). // For viewport height, fov is split into top (tfov) and bottom (bfov) fovs. For width, it's // split into right fov (rfov) and left fov (lfov). // Case 1. Symmetric fov split. Principal point is centered (centered projection): const halfSide = viewportSide / 2; const ppCentered = ppOffset === 0; if (ppCentered) { return halfSide / Math.tan(fov / 2); } // Case 2. Asymmetric fov split. Off-center perspective projection: const eps = 1e-6; const ppOffsetSq = ppOffset ** 2; if (Math.abs(fov - Math.PI / 2) < eps) { // Case 2a. Special case for (close to) right angle fov, tangent approaches infinity: // 3 right triangles: ACB, APC, BPC. Use pythagorean theorem on each to get 3 equations: // a^2 = f^2 + (1-ppOff)*s/2 // b^2 = f^2 + (1+ppOff)*s/2 // h^2 = a^2 + b^2 // Substitute a^2 and b^2 in third equation and solve for f to get: // f = (s/2) * sqrt(1-ppOff^2) return halfSide * Math.sqrt(1 - ppOffsetSq); } // Case 2b. General asymmetric fov case: // (1) tan(pfov) = (1-ppOff)*s / (2*f) // (2) tan(nfov) = (1+ppOff)*s / (2*f) // Use formula for the tan of the sum of two angles: // (3) tan(fov) = tan(pfov+nfov) = (tan(pfov) + tan(nfov)) / (1 - (tan(pfov) * tan(nfov))) // Substitute (1) and (2) in (3) and solve for f to get a quadratic equation: // 4*(tan(fov))^2 - 4*s*f - tan(fov)(1-ppOff^2)*s^2 = 0 , solving for f: // f = (s/2) * (1 +/- sqrt(1 + tan(fov)(1-ppOff^2)^2)) / tan(fov) // ppOff (principal point offset) is in [-1,1], so there's two real solutions (radicant is >=1) // and we choose the positive solution on each case: // a) tan(fov) > 0, fov in (0,pi/2) -> f = (s/2) * (1 + sqrt(1 + tan(fov)^2(1-ppOff^2))) / tan(fov) // b) tan(fov) < 0, fov in (pi/2,pi) -> f = (s/2) * (1 - sqrt(1 + tan(fov)^2(1-ppOff^2))) / tan(fov) const tanFov = Math.tan(fov); const sign = Math.sign(tanFov); const sqrt = Math.sqrt(1 + tanFov ** 2 * (1 - ppOffsetSq)); const f = (halfSide * (1 + sign * sqrt)) / tanFov; harp_utils_1.assert(f >= 0, "Focal length must be larger than 0"); return f; } var CameraUtils; (function (CameraUtils) { /** * Returns the camera's focal length. * @beta * * @param camera - The camera. * @returns The focal length in pixels or `undefined` if not set. */ function getFocalLength(camera) { var _a; return (_a = camera.userData) === null || _a === void 0 ? void 0 : _a.focalLength; } CameraUtils.getFocalLength = getFocalLength; /** * Sets a camera's focal length. * @remarks The camera's vertical fov will be updated to achieve the given viewport height. * @beta * * @param camera * @param focalLength - Focal length in pixels. It must be larger than 0. * @param viewportHeight - Viewport height in pixels, used to compute vertical fov. * @returns The new camera's focal length in pixels. */ function setFocalLength(camera, focalLength, viewportHeight) { const ppalPoint = getPrincipalPoint(camera); const vFov = computeFov(focalLength, ppalPoint.y, viewportHeight); if (vFov < FovCalculation_1.MIN_FOV_RAD || vFov > FovCalculation_1.MAX_FOV_RAD) { // Invalid vertical fov, clamp and compute again focal length. setVerticalFov(camera, vFov, viewportHeight); } else { setCameraParams(camera, ppalPoint, focalLength, viewportHeight, vFov); } // focal length might change in setCameraParams due to horizontal fov restrictions. return getFocalLength(camera); } CameraUtils.setFocalLength = setFocalLength; /** * Returns the camera's vertical field of view. * @param camera - The camera. * @returns The vertical fov in radians. */ function getVerticalFov(camera) { return THREE.MathUtils.degToRad(camera.fov); } CameraUtils.getVerticalFov = getVerticalFov; /** * Sets a camera's vertical fov. * @remarks The camera's focal length will be updated to achieve the given viewport height. * @beta * * @param camera * @param verticalFov - Vertical field of view in radians. It'll be clamped to * [{@link MIN_FOV_RAD}, {@link MAX_FOV_RAD}]. * @param viewportHeight - Viewport height in pixels, used to compute focal length. * @returns The new camera's vertical fov in radians. */ function setVerticalFov(camera, verticalFov, viewportHeight) { verticalFov = THREE.MathUtils.clamp(verticalFov, FovCalculation_1.MIN_FOV_RAD, FovCalculation_1.MAX_FOV_RAD); const ppalPoint = getPrincipalPoint(camera); const focalLength = computeFocalLengthFromFov(verticalFov, viewportHeight, ppalPoint.y); setCameraParams(camera, ppalPoint, focalLength, viewportHeight, verticalFov); // vertical fov might change in setCameraParams due to horizontal fov restrictions. return getVerticalFov(camera); } CameraUtils.setVerticalFov = setVerticalFov; /** * Calculates object's screen size based on the focal length and it's camera distance. * @beta * * @param focalLength - Focal length in pixels (see {@link setVerticalFov}) * @param distance - Object distance in world space. * @param worldSize - Object size in world space. * @return object size in screen space. */ function convertWorldToScreenSize(focalLength, distance, worldSize) { return (focalLength * worldSize) / distance; } CameraUtils.convertWorldToScreenSize = convertWorldToScreenSize; /** * Calculates object's world size based on the focal length and it's camera distance. * @beta * * @param focalLength - Focal length in pixels (see {@link setVerticalFov}) * @param distance - Object distance in world space. * @param screenSize - Object size in screen space. * @return object size in world space. */ function convertScreenToWorldSize(focalLength, distance, screenSize) { return (distance * screenSize) / focalLength; } CameraUtils.convertScreenToWorldSize = convertScreenToWorldSize; /** * Returns the camera's principal point (intersection of principal ray and image plane) * in NDC coordinates. * @beta * @see https://en.wikipedia.org/wiki/Pinhole_camera_model * @remarks This point coincides with the principal vanishing point. By default it's located at * the image center (NDC coords [0,0]), and the resulting projection is centered or symmetric. * But it may be offset (@see THREE.PerspectiveCamera.setViewOffset) for some use cases such as * multiview setups (e.g. stereoscopic rendering), resulting in an asymmetric perspective * projection. * @param camera - The camera. * @param result - Optional vector where the principal point coordinates will be copied. * @returns A vector containing the principal point NDC coordinates. */ function getPrincipalPoint(camera, result = new THREE.Vector2()) { result.x = -camera.projectionMatrix.elements[8]; result.y = -camera.projectionMatrix.elements[9]; return result; } CameraUtils.getPrincipalPoint = getPrincipalPoint; /** * Sets the camera's principal point (intersection of principal ray and image plane) * in NDC coordinates. * @beta * @see {@link getPrincipalPoint} * @param camera - The camera. * @param ndcCoords - The principal point's NDC coordinates, each coordinate can have values in * the open interval (-1,1). */ function setPrincipalPoint(camera, ndcCoords) { // We only need to set to proper elements in the projection matrix: // camera.projectionMatrix.elements[8] = -ndcCoords.x // camera.projectionMatrix.elements[9] = -ndcCoords.y // However, this can't be done directly, otherwise it'd be overwritten on the next call to // camera.updateProjectionMatrix(). The only way to set the principal point is through a // THREE.js camera method for multi-view setup, see: // https://threejs.org/docs/#api/en/cameras/PerspectiveCamera.setViewOffset const height = 1; const width = camera.aspect; // Principal point splits fov in two angles that must be strictly less than 90 degrees // (each one belongs to a right triangle). Setting the principal point at the edges (-1 or // 1) would make it impossible to achieve an fov >= 90. Thus, clamp the principal point // coordinates to values slightly smaller than 1. const maxNdcCoord = 1 - 1e-6; camera.setViewOffset(width, height, (-THREE.MathUtils.clamp(ndcCoords.x, -maxNdcCoord, maxNdcCoord) * width) / 2, (THREE.MathUtils.clamp(ndcCoords.y, -maxNdcCoord, maxNdcCoord) * height) / 2, width, height); } CameraUtils.setPrincipalPoint = setPrincipalPoint; /** * Returns the camera's horizontal field of view. * @param camera - The camera. * @returns The horizontal fov in radians. */ function getHorizontalFov(camera) { var _a, _b; // If horizontal fov is not stored in camera, assume centered projection and compute // it from the vertical fov. return ((_b = (_a = getFovs(camera)) === null || _a === void 0 ? void 0 : _a.horizontal) !== null && _b !== void 0 ? _b : 2 * Math.atan(Math.tan(THREE.MathUtils.degToRad(camera.fov) / 2) * camera.aspect)); } CameraUtils.getHorizontalFov = getHorizontalFov; /** * Returns top fov angle for a given perspective camera. * @beta * @remarks In symmetric projections, the principal point coincides with the image center, and * the vertical and horizontal FOVs are each split at that point in two equal halves. * However, in asymmetric projections the principal point is not at the image center, and thus * each fov is split unevenly in two parts: * * Symmetric projection Asymmetric projection * ------------------------- -------------------------- * | ^ | | ^ | * | | | | |tFov | * | |tFov | | lFov v rFov | * | | | |<----->x<-------------->| * | lFov v rFov | | ppal ^ point | * |<--------->x<--------->| | | o | * | ppal point=img center | | | img center | * | ^ | | | | * | |bFov | | |bFov | * | | | | | | * | v | | v | * ------------------------- -------------------------- * * @param camera - The camera. * @returns The top fov angle in radians. */ function getTopFov(camera) { var _a, _b; return (_b = (_a = getFovs(camera)) === null || _a === void 0 ? void 0 : _a.top) !== null && _b !== void 0 ? _b : THREE.MathUtils.degToRad(camera.fov / 2); } CameraUtils.getTopFov = getTopFov; /** * Returns bottom fov angle for a given perspective camera. * @see {@link CameraUtils.getTopFov} * @beta * @param camera - The camera. * @returns The bottom fov angle in radians. */ function getBottomFov(camera) { return THREE.MathUtils.degToRad(camera.fov) - getTopFov(camera); } CameraUtils.getBottomFov = getBottomFov; /** * Returns right fov angle for a given perspective camera. * @see {@link CameraUtils.getTopFov} * @beta * @param camera - The camera. * @returns The right fov angle in radians. */ function getRightFov(camera) { var _a, _b; return (_b = (_a = getFovs(camera)) === null || _a === void 0 ? void 0 : _a.right) !== null && _b !== void 0 ? _b : getHorizontalFov(camera) / 2; } CameraUtils.getRightFov = getRightFov; /** * Returns left fov angle for a given perspective camera. * @see {@link CameraUtils.getTopFov} * @beta * @param camera - The camera. * @returns The left fov angle in radians. */ function getLeftFov(camera) { var _a; return ((_a = getFovs(camera)) === null || _a === void 0 ? void 0 : _a.right) !== undefined ? getHorizontalFov(camera) - getRightFov(camera) : getHorizontalFov(camera) / 2; } CameraUtils.getLeftFov = getLeftFov; })(CameraUtils = exports.CameraUtils || (exports.CameraUtils = {})); //# sourceMappingURL=CameraUtils.js.map