UNPKG

mapillary-js

Version:

A WebGL interactive street imagery library

470 lines (385 loc) 15.9 kB
import * as THREE from "three"; import { combineLatest as observableCombineLatest, merge as observableMerge, Observable, Subject, Subscription, } from "rxjs"; import { distinctUntilChanged, filter, first, map, mergeMap, publishReplay, refCount, scan, share, skip, startWith, } from "rxjs/operators"; import { RenderPass } from "./RenderPass"; import { RenderCamera } from "./RenderCamera"; import { RenderService } from "./RenderService"; import { GLFrameRenderer } from "./interfaces/GLFrameRenderer"; import { GLRenderFunction } from "./interfaces/GLRenderFunction"; import { GLRenderHash } from "./interfaces/IGLRenderHash"; import { ViewportSize } from "./interfaces/ViewportSize"; import { SubscriptionHolder } from "../util/SubscriptionHolder"; interface GLRendererStatus { needsRender: boolean; renderer: THREE.WebGLRenderer; } interface GLRenderCamera { frameId: number; needsRender: boolean; perspective: THREE.PerspectiveCamera; } interface GLRenderHashes { [name: string]: GLFrameRenderer; } interface ForceRenderer { needsRender: boolean; } interface GLRendererOperation { (renderer: GLRendererStatus): GLRendererStatus; } interface GLRenderCameraOperation { (camera: GLRenderCamera): GLRenderCamera; } interface GLRenderHashesOperation extends Function { (hashes: GLRenderHashes): GLRenderHashes; } interface ForceRendererOperation { (forcer: ForceRenderer): ForceRenderer; } interface GLRendererCombination { camera: GLRenderCamera; eraser: ForceRenderer; trigger: ForceRenderer; renderer: GLRendererStatus; renders: GLFrameRenderer[]; } export class GLRenderer { private _renderService: RenderService; private _renderFrame$: Subject<RenderCamera> = new Subject<RenderCamera>(); private _renderCameraOperation$: Subject<GLRenderCameraOperation> = new Subject<GLRenderCameraOperation>(); private _renderCamera$: Observable<GLRenderCamera>; private _render$: Subject<GLRenderHash> = new Subject<GLRenderHash>(); private _clear$: Subject<string> = new Subject<string>(); private _renderOperation$: Subject<GLRenderHashesOperation> = new Subject<GLRenderHashesOperation>(); private _renderCollection$: Observable<GLRenderHashes>; private _rendererOperation$: Subject<GLRendererOperation> = new Subject<GLRendererOperation>(); private _renderer$: Observable<GLRendererStatus>; private _eraserOperation$: Subject<ForceRendererOperation> = new Subject<ForceRendererOperation>(); private _eraser$: Observable<ForceRenderer>; private _triggerOperation$: Subject<ForceRendererOperation> = new Subject<ForceRendererOperation>(); private _webGLRenderer$: Observable<THREE.WebGLRenderer>; private _renderFrameSubscription: Subscription; private _subscriptions: SubscriptionHolder = new SubscriptionHolder(); private _opaqueRender$: Subject<void> = new Subject<void>(); constructor( canvas: HTMLCanvasElement, canvasContainer: HTMLElement, renderService: RenderService) { this._renderService = renderService; const subs = this._subscriptions; this._renderer$ = this._rendererOperation$.pipe( scan( (renderer: GLRendererStatus, operation: GLRendererOperation): GLRendererStatus => { return operation(renderer); }, { needsRender: false, renderer: null }), filter( (renderer: GLRendererStatus): boolean => { return !!renderer.renderer; })); this._renderCollection$ = this._renderOperation$.pipe( scan( (hashes: GLRenderHashes, operation: GLRenderHashesOperation): GLRenderHashes => { return operation(hashes); }, {}), share()); this._renderCamera$ = this._renderCameraOperation$.pipe( scan( (rc: GLRenderCamera, operation: GLRenderCameraOperation): GLRenderCamera => { return operation(rc); }, { frameId: -1, needsRender: false, perspective: null })); this._eraser$ = this._eraserOperation$.pipe( startWith( (eraser: ForceRenderer): ForceRenderer => { return eraser; }), scan( (eraser: ForceRenderer, operation: ForceRendererOperation): ForceRenderer => { return operation(eraser); }, { needsRender: false })); const trigger$ = this._triggerOperation$.pipe( startWith( (trigger: ForceRenderer): ForceRenderer => { return trigger; }), scan( (trigger: ForceRenderer, operation: ForceRendererOperation): ForceRenderer => { return operation(trigger); }, { needsRender: false })); const clearColor = new THREE.Color(0x0F0F0F); const renderSubscription = observableCombineLatest( this._renderer$, this._renderCollection$, this._renderCamera$, this._eraser$, trigger$).pipe( map( ([renderer, hashes, rc, eraser, trigger]: [GLRendererStatus, GLRenderHashes, GLRenderCamera, ForceRenderer, ForceRenderer]): GLRendererCombination => { const renders: GLFrameRenderer[] = Object.keys(hashes) .map((key: string): GLFrameRenderer => { return hashes[key]; }); return { camera: rc, eraser: eraser, trigger: trigger, renderer: renderer, renders: renders }; }), filter( (co: GLRendererCombination): boolean => { let needsRender: boolean = co.renderer.needsRender || co.camera.needsRender || co.eraser.needsRender || co.trigger.needsRender; const frameId: number = co.camera.frameId; for (const render of co.renders) { if (render.frameId !== frameId) { return false; } needsRender = needsRender || render.needsRender; } return needsRender; }), distinctUntilChanged( (n1: number, n2: number): boolean => { return n1 === n2; }, (co: GLRendererCombination): number => { return co.eraser.needsRender || co.trigger.needsRender ? -co.camera.frameId : co.camera.frameId; })) .subscribe( (co: GLRendererCombination): void => { co.renderer.needsRender = false; co.camera.needsRender = false; co.eraser.needsRender = false; co.trigger.needsRender = false; const perspectiveCamera = co.camera.perspective; const backgroundRenders: GLRenderFunction[] = []; const opaqueRenders: GLRenderFunction[] = []; for (const render of co.renders) { if (render.pass === RenderPass.Background) { backgroundRenders.push(render.render); } else if (render.pass === RenderPass.Opaque) { opaqueRenders.push(render.render); } } const renderer = co.renderer.renderer; renderer.resetState(); renderer.setClearColor(clearColor, 1.0); renderer.clear(); for (const renderBackground of backgroundRenders) { renderBackground(perspectiveCamera, renderer); } renderer.clearDepth(); for (const renderOpaque of opaqueRenders) { renderOpaque(perspectiveCamera, renderer); } renderer.resetState(); this._opaqueRender$.next(); }); subs.push(renderSubscription); subs.push(this._renderFrame$.pipe( map( (rc: RenderCamera): GLRenderCameraOperation => { return (irc: GLRenderCamera): GLRenderCamera => { irc.frameId = rc.frameId; irc.perspective = rc.perspective; if (rc.changed === true) { irc.needsRender = true; } return irc; }; })) .subscribe(this._renderCameraOperation$)); this._renderFrameSubscribe(); const renderHash$ = this._render$.pipe( map( (hash: GLRenderHash) => { return (hashes: GLRenderHashes): GLRenderHashes => { hashes[hash.name] = hash.renderer; return hashes; }; })); const clearHash$ = this._clear$.pipe( map( (name: string) => { return (hashes: GLRenderHashes): GLRenderHashes => { delete hashes[name]; return hashes; }; })); subs.push(observableMerge(renderHash$, clearHash$) .subscribe(this._renderOperation$)); this._webGLRenderer$ = this._render$.pipe( first(), map( (): THREE.WebGLRenderer => { canvasContainer.appendChild(canvas); const element = renderService.element; const webGLRenderer = new THREE.WebGLRenderer({ canvas: canvas }); webGLRenderer.setPixelRatio(window.devicePixelRatio); webGLRenderer.setSize(element.offsetWidth, element.offsetHeight); webGLRenderer.autoClear = false; return webGLRenderer; }), publishReplay(1), refCount()); subs.push(this._webGLRenderer$ .subscribe(() => { /*noop*/ })); const createRenderer$ = this._webGLRenderer$.pipe( first(), map( (webGLRenderer: THREE.WebGLRenderer): GLRendererOperation => { return (renderer: GLRendererStatus): GLRendererStatus => { renderer.needsRender = true; renderer.renderer = webGLRenderer; return renderer; }; })); const resizeRenderer$ = this._renderService.size$.pipe( map( (size: ViewportSize): GLRendererOperation => { return (renderer: GLRendererStatus): GLRendererStatus => { if (renderer.renderer == null) { return renderer; } renderer.renderer.setSize(size.width, size.height); renderer.needsRender = true; return renderer; }; })); const clearRenderer$ = this._clear$.pipe( map( () => { return (renderer: GLRendererStatus): GLRendererStatus => { if (renderer.renderer == null) { return renderer; } renderer.needsRender = true; return renderer; }; })); subs.push(observableMerge( createRenderer$, resizeRenderer$, clearRenderer$) .subscribe(this._rendererOperation$)); const renderCollectionEmpty$ = this._renderCollection$.pipe( filter( (hashes: GLRenderHashes): boolean => { return Object.keys(hashes).length === 0; }), share()); subs.push(renderCollectionEmpty$ .subscribe( (): void => { if (this._renderFrameSubscription == null) { return; } this._renderFrameSubscription.unsubscribe(); this._renderFrameSubscription = null; this._renderFrameSubscribe(); })); subs.push(renderCollectionEmpty$.pipe( map( (): ForceRendererOperation => { return (eraser: ForceRenderer): ForceRenderer => { eraser.needsRender = true; return eraser; }; })) .subscribe(this._eraserOperation$)); } public get render$(): Subject<GLRenderHash> { return this._render$; } public get opaqueRender$(): Observable<void> { return this._opaqueRender$; } public get webGLRenderer$(): Observable<THREE.WebGLRenderer> { return this._webGLRenderer$; } public clear(name: string): void { this._clear$.next(name); } public remove(): void { this._rendererOperation$.next( (renderer: GLRendererStatus): GLRendererStatus => { if (renderer.renderer != null) { const extension = renderer.renderer .getContext() .getExtension('WEBGL_lose_context'); if (!!extension) { extension.loseContext(); } renderer.renderer = null; } return renderer; }); if (this._renderFrameSubscription != null) { this._renderFrameSubscription.unsubscribe(); } this._subscriptions.unsubscribe(); } public triggerRerender(): void { this._renderService.renderCameraFrame$ .pipe( skip(1), first()) .subscribe( (): void => { this._triggerOperation$.next( (trigger: ForceRenderer): ForceRenderer => { trigger.needsRender = true; return trigger; }); }); } private _renderFrameSubscribe(): void { this._render$.pipe( first(), map( (): GLRenderCameraOperation => { return (irc: GLRenderCamera): GLRenderCamera => { irc.needsRender = true; return irc; }; })) .subscribe( (operation: GLRenderCameraOperation): void => { this._renderCameraOperation$.next(operation); }); this._renderFrameSubscription = this._render$.pipe( first(), mergeMap( (): Observable<RenderCamera> => { return this._renderService.renderCameraFrame$; })) .subscribe(this._renderFrame$); } }