@polygonjs/polygonjs
Version:
node-based WebGL 3D engine https://polygonjs.com
130 lines (122 loc) • 4.9 kB
text/typescript
import {Camera, Matrix4, WebGLRenderer} from 'three';
import type {PolyScene} from '../../engine/scene/PolyScene';
import {isBooleanTrue} from '../Type';
import {CoreWebXRControllerOptions, DEFAULT_WEBXR_REFERENCE_SPACE_TYPE} from './Common';
import {CoreWebXRControllerContainer} from './CoreWebXRControllerContainer';
const tempMatrix = new Matrix4();
const webXRButtonsContainerClass = 'polygonjs-webxr-buttons-container';
export type OnWebXRSessionStartedCallback = (session: XRSession) => Promise<void>;
export abstract class BaseCoreWebXRController {
protected controllerContainers: CoreWebXRControllerContainer[] = [];
constructor(
protected scene: PolyScene,
protected renderer: WebGLRenderer,
protected camera: Camera,
private canvas: HTMLCanvasElement,
protected options: CoreWebXRControllerOptions
) {
renderer.xr.enabled = true;
if (isBooleanTrue(options.overrideReferenceSpaceType) && options.referenceSpaceType) {
renderer.xr.setReferenceSpaceType(options.referenceSpaceType);
} else {
renderer.xr.setReferenceSpaceType(DEFAULT_WEBXR_REFERENCE_SPACE_TYPE);
}
}
getController(controllerIndex: number) {
return this.controllerContainers[controllerIndex] || this._createController(controllerIndex);
}
mount() {
// sessionstart and sessionend events
// are only added right before the session request
// and not when mounting the button.
// Otherwise, when mounting both AR and VR buttons,
// both AR and VR controllers would react to a sessionstart event.
// By adding the events only when doing the session request,
// we ensure that only one controller has those events added
this._mountButton();
}
unmount() {
this._removedStartEndEvents();
this._unmountButton();
}
requestSession(sessionInit: XRSessionInit, callback: OnWebXRSessionStartedCallback) {
this._addStartEndEvents();
}
private _addStartEndEvents() {
this.renderer.xr.addEventListener('sessionstart', this._onSessionStartBound);
this.renderer.xr.addEventListener('sessionend', this._onSessionEndBound);
}
private _removedStartEndEvents() {
this.renderer.xr.removeEventListener('sessionstart', this._onSessionStartBound);
this.renderer.xr.removeEventListener('sessionend', this._onSessionEndBound);
}
private _createController(controllerIndex: number): CoreWebXRControllerContainer {
const controllerContainer = new CoreWebXRControllerContainer(this.scene, this.renderer, controllerIndex);
controllerContainer.initialize(this.camera);
this.controllerContainers.push(controllerContainer);
this._addControllerEvents(controllerContainer, controllerIndex);
return controllerContainer;
}
protected _addControllerEvents(controllerContainer: CoreWebXRControllerContainer, controllerIndex: number): void {}
process(frame?: XRFrame) {
for (const controllerContainer of this.controllerContainers) {
tempMatrix.identity().extractRotation(controllerContainer.controller.matrixWorld);
controllerContainer.ray.origin.setFromMatrixPosition(controllerContainer.controller.matrixWorld);
controllerContainer.ray.direction.set(0, 0, -1).applyMatrix4(tempMatrix);
}
}
private _onSessionStartBound = this._onSessionStart.bind(this);
private _onSessionEndBound = this._onSessionEnd.bind(this);
protected _onSessionStart() {
for (const controllerContainer of this.controllerContainers) {
controllerContainer.initialize(this.camera);
}
this.scene.play();
}
protected _onSessionEnd() {
this._removedStartEndEvents();
for (const controllerContainer of this.controllerContainers) {
controllerContainer.initialize(null);
}
this.scene.pause();
}
/**
*
* BUTTONS
*
*/
private _buttonByCanvasId: Map<string, HTMLElement> = new Map();
abstract createButton(): HTMLElement;
private _mountButton() {
if (!this.renderer.xr.enabled) {
console.warn('renderer.xr is not enabled, not mounting webXR button');
return;
}
const parent = this.canvas.parentElement;
if (parent) {
let buttonsContainer: HTMLElement | null = parent.querySelector(`.${webXRButtonsContainerClass}`);
if (!buttonsContainer) {
buttonsContainer = document.createElement('div');
buttonsContainer.classList.add(webXRButtonsContainerClass);
parent.prepend(buttonsContainer);
buttonsContainer.style.position = 'absolute';
buttonsContainer.style.bottom = '20px';
buttonsContainer.style.textAlign = 'center';
buttonsContainer.style.width = '100%';
}
const button = this.createButton();
this.attachButton(buttonsContainer, button);
this._buttonByCanvasId.set(this.canvas.id, button);
} else {
console.warn('canvas has no parent');
}
}
abstract attachButton(parentElement: HTMLElement, buttonElement: HTMLElement): void;
private _unmountButton() {
const button = this._buttonByCanvasId.get(this.canvas.id);
if (!button) {
return;
}
button.parentElement?.removeChild(button);
}
}