UNPKG

playcanvas

Version:

Open-source WebGL/WebGPU 3D engine for the web

108 lines (106 loc) 4.18 kB
/** * Helper that caches derived fisheye projection values from a normalized slider value, camera FOV, * and projection matrix. Each consumer (renderer, culling, future skydome) creates its own instance * and calls {@link FisheyeProjection#update} when it needs current values. The instance only * mutates its own cached fields, with no external side effects. * * Uses the generalized fisheye model g(θ) = k·tan(θ/k), where k controls the projection * characteristic: k=1 is rectilinear perspective, lower k increases barrel distortion. * * @ignore */ class FisheyeProjection { /** * Recomputes all derived fisheye values. Short-circuits if inputs haven't changed. * * @param {number} t - Normalized fisheye slider value in [0, 1]. 0 = rectilinear, 1 = max distortion. * @param {number} fov - Camera vertical FOV in degrees. * @param {import('../../core/math/mat4.js').Mat4} projMatrix - The camera's projection matrix. */ update(t, fov, projMatrix) { // Fisheye is only meaningful for perspective cameras (projMatrix[15] === 0). // Force it off for orthographic projections. if (projMatrix.data[15] === 1) { t = 0; } const p00 = projMatrix.data[0]; const p11 = projMatrix.data[5]; if (t === this._lastT && fov === this._lastFov && p00 === this._lastP00 && p11 === this._lastP11) { return; } this._lastT = t; this._lastFov = fov; this._lastP00 = p00; this._lastP11 = p11; if (t <= 0) { this.enabled = false; this.k = 1.0; this.invK = 1.0; this.cornerScale = 1.0; this.maxTheta = Math.PI; return; } this.enabled = true; // Map t to internal k via log-space interpolation. // kMin is derived from FOV to stay clear of the singularity at θ = k·π/2. const kMin = fov / 180 + 0.15; const kStart = Math.max(1.0, fov / 180 + 0.05); const k = kStart * Math.pow(kMin / kStart, t); this.k = k; this.invK = 1.0 / k; this.cornerScale = 1.0 + (Math.SQRT2 - 1.0) * t; // Projection-dependent values derived from the camera's projection matrix. // Compute X and Y scales independently to avoid the 0/0 singularity at 180° FOV // and to correctly handle the non-linear fisheye mapping for non-square aspect ratios. const maxTheta = Math.min(k * Math.PI / 2, 3.13); const cs = this.cornerScale; const halfFovX = Math.atan2(1.0, p00); const effHalfFovX = Math.min(halfFovX, maxTheta - 0.01); this.projMat00 = cs / (k * Math.tan(effHalfFovX / k)); const halfFovY = Math.atan2(1.0, p11); const effHalfFovY = Math.min(halfFovY, maxTheta - 0.01); this.projMat11 = cs / (k * Math.tan(effHalfFovY / k)); this.maxTheta = maxTheta; } constructor(){ /** * Whether fisheye is active (t > 0). * * @type {boolean} */ this.enabled = false; /** * The fisheye k parameter controlling projection curvature. * * @type {number} */ this.k = 1.0; /** * Precomputed 1/k to avoid per-splat division in shaders. * * @type {number} */ this.invK = 1.0; /** * Scale factor blending from edge-fit (1.0) to corner-fit (sqrt(2)) based on t. * * @type {number} */ this.cornerScale = 1.0; /** * Fisheye-adjusted horizontal projection scale for NDC conversion. * * @type {number} */ this.projMat00 = 1.0; /** * Fisheye-adjusted vertical projection scale for NDC conversion. * * @type {number} */ this.projMat11 = 1.0; /** * Maximum viewing angle before singularity, used for cone culling. * * @type {number} */ this.maxTheta = Math.PI; // Cached inputs for short-circuit check /** @private */ this._lastT = -1; /** @private */ this._lastFov = -1; /** @private */ this._lastP00 = 0; /** @private */ this._lastP11 = 0; } } export { FisheyeProjection };