@polygonjs/polygonjs
Version:
node-based WebGL 3D engine https://polygonjs.com
459 lines (420 loc) • 13.6 kB
text/typescript
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;
}
}
}