UNPKG

mapillary-js

Version:

WebGL JavaScript library for displaying street level imagery from mapillary.com

348 lines (252 loc) 9.9 kB
import * as THREE from "three"; import { Camera, Spatial, Transform, ViewportCoords, Geo, } from "../Geo"; import { RenderMode, ISize, } from "../Render"; import { IRotation, IFrame, State, } from "../State"; import { ICurrentState } from "../state/interfaces/interfaces"; export class RenderCamera { private _spatial: Spatial; private _viewportCoords: ViewportCoords; private _alpha: number; private _renderMode: RenderMode; private _zoom: number; private _frameId: number; private _camera: Camera; private _perspective: THREE.PerspectiveCamera; private _rotation: IRotation; private _changed: boolean; private _changedForFrame: number; private _currentNodeId: string; private _previousNodeId: string; private _currentPano: boolean; private _previousPano: boolean; private _state: State; private _currentProjectedPoints: number[][]; private _previousProjectedPoints: number[][]; private _currentFov: number; private _previousFov: number; private _initialFov: number; constructor(elementWidth: number, elementHeight: number, renderMode: RenderMode) { this._spatial = new Spatial(); this._viewportCoords = new ViewportCoords(); this._initialFov = 50; this._alpha = -1; this._renderMode = renderMode; this._zoom = 0; this._frameId = -1; this._changed = false; this._changedForFrame = -1; this._currentNodeId = null; this._previousNodeId = null; this._currentPano = false; this._previousPano = false; this._state = null; this._currentProjectedPoints = []; this._previousProjectedPoints = []; this._currentFov = this._initialFov; this._previousFov = this._initialFov; this._camera = new Camera(); this._perspective = new THREE.PerspectiveCamera( this._initialFov, this._computeAspect(elementWidth, elementHeight), 0.16, 10000); this._perspective.matrixAutoUpdate = false; this._rotation = { phi: 0, theta: 0 }; } public get alpha(): number { return this._alpha; } public get camera(): Camera { return this._camera; } public get changed(): boolean { return this._frameId === this._changedForFrame; } public get frameId(): number { return this._frameId; } public get perspective(): THREE.PerspectiveCamera { return this._perspective; } public get renderMode(): RenderMode { return this._renderMode; } public get rotation(): IRotation { return this._rotation; } public get zoom(): number { return this._zoom; } public getTilt(): number { return 90 - this._spatial.radToDeg(this._rotation.theta); } public fovToZoom(fov: number): number { fov = Math.min(90, Math.max(0, fov)); const currentFov: number = this._computeCurrentFov(0); const actualFov: number = this._alpha === 1 ? currentFov : this._interpolateFov(currentFov, this._computePreviousFov(0), this._alpha); const y0: number = Math.tan(actualFov / 2 * Math.PI / 180); const y1: number = Math.tan(fov / 2 * Math.PI / 180); const zoom: number = Math.log(y0 / y1) / Math.log(2); return zoom; } public setFrame(frame: IFrame): void { const state: ICurrentState = frame.state; if (state.state !== this._state) { this._state = state.state; this._changed = true; } const currentNodeId: string = state.currentNode.key; const previousNodeId: string = !!state.previousNode ? state.previousNode.key : null; if (currentNodeId !== this._currentNodeId) { this._currentNodeId = currentNodeId; this._currentPano = !!state.currentTransform.gpano; this._currentProjectedPoints = this._computeProjectedPoints(state.currentTransform); this._changed = true; } if (previousNodeId !== this._previousNodeId) { this._previousNodeId = previousNodeId; this._previousPano = !!state.previousTransform.gpano; this._previousProjectedPoints = this._computeProjectedPoints(state.previousTransform); this._changed = true; } const zoom: number = state.zoom; if (zoom !== this._zoom) { this._zoom = zoom; this._changed = true; } if (this._changed) { this._currentFov = this._computeCurrentFov(this.zoom); this._previousFov = this._computePreviousFov(this._zoom); } const alpha: number = state.alpha; if (this._changed || alpha !== this._alpha) { this._alpha = alpha; this._perspective.fov = this._state === State.Earth ? 60 : this._interpolateFov( this._currentFov, this._previousFov, this._alpha); this._changed = true; } const camera: Camera = state.camera; if (this._camera.diff(camera) > 1e-9) { this._camera.copy(camera); this._rotation = this._computeRotation(camera); this._perspective.up.copy(camera.up); this._perspective.position.copy(camera.position); // Workaround for shaking camera this._perspective.matrixAutoUpdate = true; this._perspective.lookAt(camera.lookat); this._perspective.matrixAutoUpdate = false; this._perspective.updateMatrix(); this._perspective.updateMatrixWorld(false); this._changed = true; } if (this._changed) { this._perspective.updateProjectionMatrix(); } this._setFrameId(frame.id); } public setRenderMode(renderMode: RenderMode): void { this._renderMode = renderMode; this._perspective.fov = this._computeFov(); this._perspective.updateProjectionMatrix(); this._changed = true; } public setSize(size: ISize): void { this._perspective.aspect = this._computeAspect(size.width, size.height); this._perspective.fov = this._computeFov(); this._perspective.updateProjectionMatrix(); this._changed = true; } private _computeAspect(elementWidth: number, elementHeight: number): number { return elementWidth === 0 ? 0 : elementWidth / elementHeight; } private _computeCurrentFov(zoom: number): number { if (this._perspective.aspect === 0) { return 0; } if (!this._currentNodeId) { return this._initialFov; } return this._currentPano ? this._yToFov(1, zoom) : this._computeVerticalFov(this._currentProjectedPoints, this._renderMode, zoom, this.perspective.aspect); } private _computeFov(): number { this._currentFov = this._computeCurrentFov(this._zoom); this._previousFov = this._computePreviousFov(this._zoom); return this._interpolateFov(this._currentFov, this._previousFov, this._alpha); } private _computePreviousFov(zoom: number): number { if (this._perspective.aspect === 0) { return 0; } if (!this._currentNodeId) { return this._initialFov; } return !this._previousNodeId ? this._currentFov : this._previousPano ? this._yToFov(1, zoom) : this._computeVerticalFov(this._previousProjectedPoints, this._renderMode, zoom, this.perspective.aspect); } private _computeProjectedPoints(transform: Transform): number[][] { const vertices: number[][] = [[0.5, 0], [1, 0]]; const directions: number[][] = [[0.5, 0], [0, 0.5]]; const pointsPerLine: number = 100; return Geo.computeProjectedPoints(transform, vertices, directions, pointsPerLine, this._viewportCoords); } private _computeRequiredVerticalFov(projectedPoint: number[], zoom: number, aspect: number): number { const maxY: number = Math.max(projectedPoint[0] / aspect, projectedPoint[1]); return this._yToFov(maxY, zoom); } private _computeRotation(camera: Camera): IRotation { let direction: THREE.Vector3 = camera.lookat.clone().sub(camera.position); let up: THREE.Vector3 = camera.up.clone(); let phi: number = this._spatial.azimuthal(direction.toArray(), up.toArray()); let theta: number = Math.PI / 2 - this._spatial.angleToPlane(direction.toArray(), [0, 0, 1]); return { phi: phi, theta: theta }; } private _computeVerticalFov(projectedPoints: number[][], renderMode: RenderMode, zoom: number, aspect: number): number { const fovs: number[] = projectedPoints .map( (projectedPoint: number[]): number => { return this._computeRequiredVerticalFov(projectedPoint, zoom, aspect); }); const fov: number = renderMode === RenderMode.Fill ? Math.min(...fovs) * 0.995 : Math.max(...fovs); return fov; } private _yToFov(y: number, zoom: number): number { return 2 * Math.atan(y / Math.pow(2, zoom)) * 180 / Math.PI; } private _interpolateFov(v1: number, v2: number, alpha: number): number { return alpha * v1 + (1 - alpha) * v2; } private _setFrameId(frameId: number): void { this._frameId = frameId; if (this._changed) { this._changed = false; this._changedForFrame = frameId; } } } export default RenderCamera;