UNPKG

@polygonjs/polygonjs

Version:

node-based WebGL 3D engine https://polygonjs.com

459 lines (420 loc) 13.6 kB
import {Camera, WebGLRenderer, Scene} from 'three'; import {TypedViewer, TypedViewerOptions, BaseViewerMountOptions} from './_Base'; import {AvailableRenderConfig, CoreCameraRendererController} from '../../core/camera/CoreCameraRendererController'; import {CoreCameraPostProcessController} from '../../core/camera/CoreCameraPostProcessController'; import {CoreCameraCSSRendererController, CSSRendererConfig} from '../../core/camera/CoreCameraCSSRendererController'; import {CoreCameraViewerCodeController, ViewerCodeConfig} from '../../core/camera/CoreCameraViewerCodeController'; import {CoreCameraControlsController} from '../../core/camera/CoreCameraControlsController'; import {CoreCameraRenderSceneController} from '../../core/camera/CoreCameraRenderSceneController'; import type {EffectComposer} from 'postprocessing'; import {AbstractRenderer} from './Common'; import {CoreCameraWebXRController, CoreCameraWebXRControllerConfig} from '../../core/camera/webXR/CoreCameraWebXR'; import {MarkerTrackingControllerConfig} from '../../core/webXR/markerTracking/Common'; import {CoreCameraMarkerTrackingController} from '../../core/camera/webXR/CoreCameraMarkerTracking'; import {CoreCameraViewerFPSController, ViewerFPSConfig, isDeltaValid} from '../../core/camera/CoreCameraFPS'; const CSS_CLASS = 'CoreThreejsViewer'; declare global { interface HTMLCanvasElement { onwebglcontextlost: () => void; onwebglcontextrestored: () => void; } } // export interface ThreejsViewerProperties { // autoRender?: boolean; // } export interface ThreejsViewerOptions<C extends Camera> extends TypedViewerOptions<C> { // properties?: ThreejsViewerProperties; // renderer?: AbstractRenderer; } // type RenderFuncWithDelta = (delta: number) => void; // type RenderFunc = () => void; export interface ThreejsViewerSetupData<C extends Camera> { renderer: AbstractRenderer; renderScene: Scene; camera: C; } interface SetAutoRenderOptions { cancelAnimate?: boolean; } /** * * * threejs viewers are created by the [PerspectiveCamera](/docs/nodes/obj/perspectivecamera) and [OrthographicCamera](/docs/nodes/obj/orthographiccamera) object nodes. They inherit from [TypedViewer](/docs/api/TypedViewer). * */ export class ThreejsViewer<C extends Camera> extends TypedViewer<C> { protected _requestAnimationFrameId: number | undefined; private _webXRConfig: CoreCameraWebXRControllerConfig | undefined; private _markerTrackingConfig: MarkerTrackingControllerConfig | undefined; // protected _renderer: AbstractRenderer | undefined; private _rendererConfig: AvailableRenderConfig | undefined; // protected _renderFunc: RenderFuncWithDelta | undefined; // protected _renderCSSFunc: RenderFunc | undefined; private _cssRendererConfig: CSSRendererConfig | undefined; private _codeConfig: ViewerCodeConfig | undefined; private _FPSConfig: ViewerFPSConfig | undefined; private _effectComposer: EffectComposer | undefined; protected _errorMessage: string | undefined; private _resizeObserver: ResizeObserver | undefined; static override _canvasIdPrefix() { return 'ThreejsViewer'; } constructor(options: ThreejsViewerOptions<C>) { super(options); this._setupFunctions(options); // coreMountDebugElement(); // this._container.style.height = '100%'; // this should be app specific } rendererConfig() { return this._rendererConfig; } protected _setupFunctions(options: ThreejsViewerOptions<C>): ThreejsViewerSetupData<C> | void { const camera = this.camera(); const scene = this.scene(); const canvas = this.canvas(); const threejsScene = scene.threejsScene(); this._errorMessage = undefined; // WebGLRenderer this._renderer = options.renderer; if (!this._renderer) { this._rendererConfig = CoreCameraRendererController.rendererConfig({ camera, scene, canvas, }); if (this._rendererConfig) { this._renderer = this._rendererConfig.renderer; } } const renderer = this._renderer; if (!renderer) { console.error('no renderer'); } if (renderer) { // scene const rendererScene = CoreCameraRenderSceneController.renderScene({camera, scene}); const renderScene = rendererScene || threejsScene; // post this._effectComposer = CoreCameraPostProcessController.createComposer({ camera, scene, renderScene, renderer, viewer: this, }); const effectComposer = this._effectComposer; // webXR if (renderer instanceof WebGLRenderer) { this._webXRConfig = CoreCameraWebXRController.process({ camera, scene, renderer, canvas: this.canvas(), }); this._markerTrackingConfig = CoreCameraMarkerTrackingController.process({ canvas, camera, scene, onError: (errorMessage) => { this._errorMessage = errorMessage; }, }); } // ViewerCode this._codeConfig = CoreCameraViewerCodeController.viewerCodeConfig({camera}); // CSSRender this._cssRendererConfig = CoreCameraCSSRendererController.cssRendererConfig({scene, camera, canvas}); const cssRenderer = this._cssRendererConfig?.cssRenderer; this._renderCSSFunc = cssRenderer ? () => cssRenderer.render(renderScene, camera) : undefined; // controls this._controlsNode = CoreCameraControlsController.controlsNode({camera, scene}); // FPS this._FPSConfig = CoreCameraViewerFPSController.viewerFPSConfig({camera}); // renderFunc if (effectComposer) { this._renderFunc = (delta) => effectComposer.render(delta); } else { this._renderFunc = () => renderer.render(renderScene, camera); } return {renderer, renderScene, camera}; } } /** * mounts the viewer onto an element * * */ override mount(element: HTMLElement, options?: BaseViewerMountOptions) { super.mount(element, options); const canvas = this.canvas(); const _appendCanvasWithViewerCodeConfig = (config: ViewerCodeConfig) => { if (!this._domElement) { return; } return config.createViewerElement({domElement: this._domElement, canvas, CSSClass: CSS_CLASS}); }; const _appendCanvasWithoutViewerCodeConfig = () => { this._domElement?.appendChild(canvas); this._domElement?.classList.add(CSS_CLASS); return this._domElement; }; const viewerContainerElement = this._codeConfig ? _appendCanvasWithViewerCodeConfig(this._codeConfig) : _appendCanvasWithoutViewerCodeConfig(); // mount CSSRenderer const cssRendererNode = this._cssRendererConfig?.cssRendererNode; if (cssRendererNode) { cssRendererNode.mountRenderer(canvas); } // mount webXR this._webXRConfig?.mountFunction(); this._markerTrackingConfig?.mountFunction(); this._build(); this._setEvents(); const canvasParent = canvas.parentElement; if (canvasParent) { this._createResizeObserver(canvasParent); } this.onResize(); // display error if any if (this._errorMessage) { const errorElement = document.createElement('div'); errorElement.style.position = 'absolute'; errorElement.style.top = '0px'; errorElement.style.width = '100%'; errorElement.style.color = 'red'; errorElement.style.backgroundColor = 'white'; errorElement.style.padding = '20px'; // errorElement.style.margin = '20px'; errorElement.style.textAlign = 'center'; errorElement.style.opacity = '90%'; errorElement.innerText = this._errorMessage; viewerContainerElement?.append(errorElement); } // if (Poly.logo.displayed()) { // new ViewerLogoController(this); // } } public _build() { this._initDisplay(); this.activate(); } /** * disposes the viewer * * */ override dispose() { const canvas = this.canvas(); // dispose cssRenderer const cssRendererNode = this._cssRendererConfig?.cssRendererNode; if (cssRendererNode) { cssRendererNode.unmountRenderer(canvas); } this._cssRendererConfig = undefined; // dispose webXR this._webXRConfig?.unmountFunction(); this._markerTrackingConfig?.unmountFunction(); // dispose effectComposer this._effectComposer = undefined; this.setAutoRender(false); this._cancelAnimate(); // this.controlsController().dispose(); this._unlistenToWindowResize(); this._disposeResizeObserver(); // if I dispose the renderer here, // this prevents env maps from displaying // when the viewer is switched // TODO: consider disposing the renderer only if it is not a default one, // as this may satisfy most cases //this._renderer?.dispose(); super.dispose(); } private _setEvents() { this.eventsController().init(); this.webglController().init(); this._unlistenToWindowResize(); this._listenToWindowResize(); } private _initDisplay() { if (!this._canvas) { console.warn('no canvas found for viewer'); return; } if (!this._renderer) { return; } const pixelRatio = this._renderer.getPixelRatio(); this.camerasController().computeSizeAndAspect(pixelRatio); this.audioController().update(); this._startAnimate(); } /** * setAutoRender to false will stop the rendering. This can be useful if you know that nothing has changed in the scene, or if the renderer is currently not visible. * * */ override setAutoRender(state = true, options?: SetAutoRenderOptions) { super.setAutoRender(state); // if this._requestAnimationFrameId is already defined, // calling this a second time would start another requestAnimationFrame // and we would therefore render at twice the rate if (this._doRender && this._requestAnimationFrameId == null) { this._startAnimate(); } if (!this._doRender) { if (options && options.cancelAnimate == true) { this._cancelAnimate(); } } } isXR(): boolean { if (!this._renderer) { return false; } return this._renderer instanceof WebGLRenderer && this._renderer.xr.enabled; } private _startAnimate() { if (this.isXR()) { const renderer = this._renderer as WebGLRenderer; if (!renderer) { return; } const webXRController = this.scene().webXR; const xrCallback: XRFrameRequestCallback = (timestamp, frame) => { webXRController.activeXRController()?.process(frame); this._animateWebXR(); }; renderer.setAnimationLoop(xrCallback); } else { this._animateWeb(); } } private _cancelAnimate() { if (this.isXR()) { (this._renderer as WebGLRenderer)?.setAnimationLoop(null); } else { this._cancelAnimateCommon(); } } protected _animateWebBound: () => void = this._animateWeb.bind(this); protected _animateWeb() { this._requestAnimationFrameId = requestAnimationFrame(this._animateWebBound); this.__animateCommon__(); } private _animateWebXR() { this.__animateCommon__(); } private _accumulatedDelta = 0; protected __animateCommon__() { const delta = this._scene.timeController.updateClockDelta(); if (this._FPSConfig) { this._accumulatedDelta += delta; if (!isDeltaValid(this._accumulatedDelta, this._FPSConfig)) { return; } this._accumulatedDelta = 0; } this._tickAndRender(delta); } private _cancelAnimateCommon() { this._doRender = false; if (this._requestAnimationFrameId != null) { cancelAnimationFrame(this._requestAnimationFrameId); this._requestAnimationFrameId = undefined; } if (this._canvas) { // this._cameraNode.renderController().deleteRenderer(this._canvas); } } protected override _tick(delta: number) { super._tick(delta); this._markerTrackingConfig?.renderFunction(); } /** * returns the current renderer * * */ renderer() { return this._renderer; // if (this._canvas) { // // return this._cameraNode.renderController().renderer(this._canvas); // } } effectComposer() { return this._effectComposer; } preCompile() { if (!this._renderer) { return; } // if (this._canvas) { this._renderer.compile(this._scene.threejsScene(), this._camera); // } } override markAsReady() { this.preCompile(); this.setAutoRender(true); } // // // resize // // private _onResizeBound = this.onResize.bind(this); onResize() { const renderUpdateRequired = this.updateSize(); if (!renderUpdateRequired) { return; } this._updateRendererSize(); if (this._renderFunc) { this._renderFunc(this.scene().timeController.delta()); } this._runOnResizeCallbacks(); } override updateSize(): boolean { const renderer = this._renderer; if (!renderer) { return false; } const pixelRatio = renderer.getPixelRatio(); this.camerasController().computeSizeAndAspect(pixelRatio); const size = this.camerasController().size; if (this._size.equals(size)) { return false; } this._size.copy(size); this._scene.viewersRegister.markViewerAsSizeUpdated(this); return true; } private _updateRendererSize() { const canvas = this._canvas; if (!canvas) { return; } CoreCameraRendererController.setRendererSize(canvas, this._size); this._cssRendererConfig?.cssRenderer.setSize(this._size.x, this._size.y); this._effectComposer?.setSize(this._size.x, this._size.y); this.camerasController().updateCameraAspect(); } private _listenToWindowResize() { // window.addEventListener('resize', this._onResizeBound, false); } private _unlistenToWindowResize() { // window.removeEventListener('resize', this._onResizeBound, false); } private _createResizeObserver(canvasElementParent: HTMLElement) { this._disposeResizeObserver(); this._resizeObserver = new ResizeObserver(this._onResizeBound); this._resizeObserver.observe(canvasElementParent, {box: 'border-box'}); // this.onResize(); } private _disposeResizeObserver() { if (this._resizeObserver) { this._resizeObserver.disconnect(); this._resizeObserver = undefined; } } }