@openhps/core
Version:
Open Hybrid Positioning System - Core component
1,182 lines (1,087 loc) • 38.3 kB
JavaScript
import { ArrayCamera } from '../../cameras/ArrayCamera.js';
import { EventDispatcher } from '../../core/EventDispatcher.js';
import { PerspectiveCamera } from '../../cameras/PerspectiveCamera.js';
import { Quaternion } from '../../math/Quaternion.js';
import { RAD2DEG } from '../../math/MathUtils.js';
import { Vector2 } from '../../math/Vector2.js';
import { Vector3 } from '../../math/Vector3.js';
import { Vector4 } from '../../math/Vector4.js';
import { WebXRController } from '../webxr/WebXRController.js';
import { AddEquation, BackSide, CustomBlending, DepthFormat, DepthStencilFormat, FrontSide, RGBAFormat, UnsignedByteType, UnsignedInt248Type, UnsignedIntType, ZeroFactor } from '../../constants.js';
import { DepthTexture } from '../../textures/DepthTexture.js';
import { XRRenderTarget } from './XRRenderTarget.js';
import { CylinderGeometry } from '../../geometries/CylinderGeometry.js';
import { PlaneGeometry } from '../../geometries/PlaneGeometry.js';
import { MeshBasicMaterial } from '../../materials/MeshBasicMaterial.js';
import { Mesh } from '../../objects/Mesh.js';
const _cameraLPos = /*@__PURE__*/new Vector3();
const _cameraRPos = /*@__PURE__*/new Vector3();
/**
* The XR manager is built on top of the WebXR Device API to
* manage XR sessions with `WebGPURenderer`.
*
* XR is currently only supported with a WebGL 2 backend.
*
* @augments EventDispatcher
*/
class XRManager extends EventDispatcher {
/**
* Constructs a new XR manager.
*
* @param {Renderer} renderer - The renderer.
*/
constructor(renderer) {
super();
/**
* This flag globally enables XR rendering.
*
* @type {boolean}
* @default false
*/
this.enabled = false;
/**
* Whether the XR device is currently presenting or not.
*
* @type {boolean}
* @default false
* @readonly
*/
this.isPresenting = false;
/**
* Whether the XR camera should automatically be updated or not.
*
* @type {boolean}
* @default true
*/
this.cameraAutoUpdate = true;
/**
* The renderer.
*
* @private
* @type {Renderer}
*/
this._renderer = renderer;
// camera
/**
* Represents the camera for the left eye.
*
* @private
* @type {PerspectiveCamera}
*/
this._cameraL = new PerspectiveCamera();
this._cameraL.viewport = new Vector4();
/**
* Represents the camera for the right eye.
*
* @private
* @type {PerspectiveCamera}
*/
this._cameraR = new PerspectiveCamera();
this._cameraR.viewport = new Vector4();
/**
* A list of cameras used for rendering the XR views.
*
* @private
* @type {Array<Camera>}
*/
this._cameras = [this._cameraL, this._cameraR];
/**
* The main XR camera.
*
* @private
* @type {ArrayCamera}
*/
this._cameraXR = new ArrayCamera();
/**
* The current near value of the XR camera.
*
* @private
* @type {?number}
* @default null
*/
this._currentDepthNear = null;
/**
* The current far value of the XR camera.
*
* @private
* @type {?number}
* @default null
*/
this._currentDepthFar = null;
/**
* A list of WebXR controllers requested by the application.
*
* @private
* @type {Array<WebXRController>}
*/
this._controllers = [];
/**
* A list of XR input source. Each input source belongs to
* an instance of WebXRController.
*
* @private
* @type {Array<XRInputSource?>}
*/
this._controllerInputSources = [];
/**
* The XR render target that represents the rendering destination
* during an active XR session.
*
* @private
* @type {?RenderTarget}
* @default null
*/
this._xrRenderTarget = null;
/**
* An array holding all the non-projection layers
*
* @private
* @type {Array<Object>}
* @default []
*/
this._layers = [];
/**
* Whether the device has support for all layer types.
*
* @type {boolean}
* @default false
*/
this._supportsLayers = false;
/**
* Helper function to create native WebXR Layer.
*
* @private
* @type {Function}
*/
this._createXRLayer = createXRLayer.bind(this);
/**
* The current WebGL context.
*
* @private
* @type {?WebGL2RenderingContext}
* @default null
*/
this._gl = null;
/**
* The current animation context.
*
* @private
* @type {?Window}
* @default null
*/
this._currentAnimationContext = null;
/**
* The current animation loop.
*
* @private
* @type {?Function}
* @default null
*/
this._currentAnimationLoop = null;
/**
* The current pixel ratio.
*
* @private
* @type {?number}
* @default null
*/
this._currentPixelRatio = null;
/**
* The current size of the renderer's canvas
* in logical pixel unit.
*
* @private
* @type {Vector2}
*/
this._currentSize = new Vector2();
/**
* The default event listener for handling events inside a XR session.
*
* @private
* @type {Function}
*/
this._onSessionEvent = onSessionEvent.bind(this);
/**
* The event listener for handling the end of a XR session.
*
* @private
* @type {Function}
*/
this._onSessionEnd = onSessionEnd.bind(this);
/**
* The event listener for handling the `inputsourceschange` event.
*
* @private
* @type {Function}
*/
this._onInputSourcesChange = onInputSourcesChange.bind(this);
/**
* The animation loop which is used as a replacement for the default
* animation loop of the application. It is only used when a XR session
* is active.
*
* @private
* @type {Function}
*/
this._onAnimationFrame = onAnimationFrame.bind(this);
/**
* The current XR reference space.
*
* @private
* @type {?XRReferenceSpace}
* @default null
*/
this._referenceSpace = null;
/**
* The current XR reference space type.
*
* @private
* @type {XRReferenceSpaceType}
* @default 'local-floor'
*/
this._referenceSpaceType = 'local-floor';
/**
* A custom reference space defined by the application.
*
* @private
* @type {?XRReferenceSpace}
* @default null
*/
this._customReferenceSpace = null;
/**
* The framebuffer scale factor.
*
* @private
* @type {number}
* @default 1
*/
this._framebufferScaleFactor = 1;
/**
* The foveation factor.
*
* @private
* @type {number}
* @default 1
*/
this._foveation = 1.0;
/**
* A reference to the current XR session.
*
* @private
* @type {?XRSession}
* @default null
*/
this._session = null;
/**
* A reference to the current XR base layer.
*
* @private
* @type {?XRWebGLLayer}
* @default null
*/
this._glBaseLayer = null;
/**
* A reference to the current XR binding.
*
* @private
* @type {?XRWebGLBinding}
* @default null
*/
this._glBinding = null;
/**
* A reference to the current XR projection layer.
*
* @private
* @type {?XRProjectionLayer}
* @default null
*/
this._glProjLayer = null;
/**
* A reference to the current XR frame.
*
* @private
* @type {?XRFrame}
* @default null
*/
this._xrFrame = null;
/**
* Whether to use the WebXR Layers API or not.
*
* @private
* @type {boolean}
* @readonly
*/
this._useLayers = typeof XRWebGLBinding !== 'undefined' && 'createProjectionLayer' in XRWebGLBinding.prototype; // eslint-disable-line compat/compat
}
/**
* Returns an instance of `THREE.Group` that represents the transformation
* of a XR controller in target ray space. The requested controller is defined
* by the given index.
*
* @param {number} index - The index of the XR controller.
* @return {Group} A group that represents the controller's transformation.
*/
getController(index) {
const controller = this._getController(index);
return controller.getTargetRaySpace();
}
/**
* Returns an instance of `THREE.Group` that represents the transformation
* of a XR controller in grip space. The requested controller is defined
* by the given index.
*
* @param {number} index - The index of the XR controller.
* @return {Group} A group that represents the controller's transformation.
*/
getControllerGrip(index) {
const controller = this._getController(index);
return controller.getGripSpace();
}
/**
* Returns an instance of `THREE.Group` that represents the transformation
* of a XR controller in hand space. The requested controller is defined
* by the given index.
*
* @param {number} index - The index of the XR controller.
* @return {Group} A group that represents the controller's transformation.
*/
getHand(index) {
const controller = this._getController(index);
return controller.getHandSpace();
}
/**
* Returns the foveation value.
*
* @return {number|undefined} The foveation value. Returns `undefined` if no base or projection layer is defined.
*/
getFoveation() {
if (this._glProjLayer === null && this._glBaseLayer === null) {
return undefined;
}
return this._foveation;
}
/**
* Sets the foveation value.
*
* @param {number} foveation - A number in the range `[0,1]` where `0` means no foveation (full resolution)
* and `1` means maximum foveation (the edges render at lower resolution).
*/
setFoveation(foveation) {
this._foveation = foveation;
if (this._glProjLayer !== null) {
this._glProjLayer.fixedFoveation = foveation;
}
if (this._glBaseLayer !== null && this._glBaseLayer.fixedFoveation !== undefined) {
this._glBaseLayer.fixedFoveation = foveation;
}
}
/**
* Returns the framebuffer scale factor.
*
* @return {number} The framebuffer scale factor.
*/
getFramebufferScaleFactor() {
return this._framebufferScaleFactor;
}
/**
* Sets the framebuffer scale factor.
*
* This method can not be used during a XR session.
*
* @param {number} factor - The framebuffer scale factor.
*/
setFramebufferScaleFactor(factor) {
this._framebufferScaleFactor = factor;
if (this.isPresenting === true) {
console.warn('THREE.XRManager: Cannot change framebuffer scale while presenting.');
}
}
/**
* Returns the reference space type.
*
* @return {XRReferenceSpaceType} The reference space type.
*/
getReferenceSpaceType() {
return this._referenceSpaceType;
}
/**
* Sets the reference space type.
*
* This method can not be used during a XR session.
*
* @param {XRReferenceSpaceType} type - The reference space type.
*/
setReferenceSpaceType(type) {
this._referenceSpaceType = type;
if (this.isPresenting === true) {
console.warn('THREE.XRManager: Cannot change reference space type while presenting.');
}
}
/**
* Returns the XR reference space.
*
* @return {XRReferenceSpace} The XR reference space.
*/
getReferenceSpace() {
return this._customReferenceSpace || this._referenceSpace;
}
/**
* Sets a custom XR reference space.
*
* @param {XRReferenceSpace} space - The XR reference space.
*/
setReferenceSpace(space) {
this._customReferenceSpace = space;
}
/**
* Returns the XR camera.
*
* @return {ArrayCamera} The XR camera.
*/
getCamera() {
return this._cameraXR;
}
/**
* Returns the environment blend mode from the current XR session.
*
* @return {'opaque'|'additive'|'alpha-blend'|undefined} The environment blend mode. Returns `undefined` when used outside of a XR session.
*/
getEnvironmentBlendMode() {
if (this._session !== null) {
return this._session.environmentBlendMode;
}
}
/**
* Returns the current XR frame.
*
* @return {?XRFrame} The XR frame. Returns `null` when used outside a XR session.
*/
getFrame() {
return this._xrFrame;
}
createQuadLayer(width, height, translation, quaternion, pixelwidth, pixelheight, rendercall, attributes = []) {
const geometry = new PlaneGeometry(width, height);
const renderTarget = new XRRenderTarget(pixelwidth, pixelheight, {
format: RGBAFormat,
type: UnsignedByteType,
depthTexture: new DepthTexture(pixelwidth, pixelheight, attributes.stencil ? UnsignedInt248Type : UnsignedIntType, undefined, undefined, undefined, undefined, undefined, undefined, attributes.stencil ? DepthStencilFormat : DepthFormat),
stencilBuffer: attributes.stencil,
resolveDepthBuffer: false,
resolveStencilBuffer: false
});
const material = new MeshBasicMaterial({
color: 0xffffff,
side: FrontSide
});
material.map = renderTarget.texture;
material.map.offset.y = 1;
material.map.repeat.y = -1;
const plane = new Mesh(geometry, material);
plane.position.copy(translation);
plane.quaternion.copy(quaternion);
const layer = {
type: 'quad',
width: width,
height: height,
translation: translation,
quaternion: quaternion,
pixelwidth: pixelwidth,
pixelheight: pixelheight,
plane: plane,
material: material,
rendercall: rendercall,
renderTarget: renderTarget
};
this._layers.push(layer);
if (this._session !== null) {
layer.plane.material = new MeshBasicMaterial({
color: 0xffffff,
side: FrontSide
});
layer.plane.material.blending = CustomBlending;
layer.plane.material.blendEquation = AddEquation;
layer.plane.material.blendSrc = ZeroFactor;
layer.plane.material.blendDst = ZeroFactor;
layer.xrlayer = this._createXRLayer(layer);
const xrlayers = this._session.renderState.layers;
xrlayers.unshift(layer.xrlayer);
this._session.updateRenderState({
layers: xrlayers
});
} else {
renderTarget.isXRRenderTarget = false;
}
return plane;
}
createCylinderLayer(radius, centralAngle, aspectratio, translation, quaternion, pixelwidth, pixelheight, rendercall, attributes = []) {
const geometry = new CylinderGeometry(radius, radius, radius * centralAngle / aspectratio, 64, 64, true, Math.PI - centralAngle / 2, centralAngle);
const renderTarget = new XRRenderTarget(pixelwidth, pixelheight, {
format: RGBAFormat,
type: UnsignedByteType,
depthTexture: new DepthTexture(pixelwidth, pixelheight, attributes.stencil ? UnsignedInt248Type : UnsignedIntType, undefined, undefined, undefined, undefined, undefined, undefined, attributes.stencil ? DepthStencilFormat : DepthFormat),
stencilBuffer: attributes.stencil,
resolveDepthBuffer: false,
resolveStencilBuffer: false
});
const material = new MeshBasicMaterial({
color: 0xffffff,
side: BackSide
});
material.map = renderTarget.texture;
material.map.offset.y = 1;
material.map.repeat.y = -1;
const plane = new Mesh(geometry, material);
plane.position.copy(translation);
plane.quaternion.copy(quaternion);
const layer = {
type: 'cylinder',
radius: radius,
centralAngle: centralAngle,
aspectratio: aspectratio,
translation: translation,
quaternion: quaternion,
pixelwidth: pixelwidth,
pixelheight: pixelheight,
plane: plane,
material: material,
rendercall: rendercall,
renderTarget: renderTarget
};
this._layers.push(layer);
if (this._session !== null) {
layer.plane.material = new MeshBasicMaterial({
color: 0xffffff,
side: BackSide
});
layer.plane.material.blending = CustomBlending;
layer.plane.material.blendEquation = AddEquation;
layer.plane.material.blendSrc = ZeroFactor;
layer.plane.material.blendDst = ZeroFactor;
layer.xrlayer = this._createXRLayer(layer);
const xrlayers = this._session.renderState.layers;
xrlayers.unshift(layer.xrlayer);
this._session.updateRenderState({
layers: xrlayers
});
} else {
renderTarget.isXRRenderTarget = false;
}
return plane;
}
renderLayers() {
const translationObject = new Vector3();
const quaternionObject = new Quaternion();
const wasPresenting = this.isPresenting;
this.isPresenting = false;
for (const layer of this._layers) {
layer.renderTarget.isXRRenderTarget = this._session !== null;
layer.renderTarget.hasExternalTextures = layer.renderTarget.isXRRenderTarget;
layer.renderTarget.autoAllocateDepthBuffer = !layer.renderTarget.isXRRenderTarget;
if (layer.renderTarget.isXRRenderTarget && this._supportsLayers) {
layer.xrlayer.transform = new XRRigidTransform(layer.plane.getWorldPosition(translationObject), layer.plane.getWorldQuaternion(quaternionObject));
const glSubImage = this._glBinding.getSubImage(layer.xrlayer, this._xrFrame);
this._renderer.backend.setXRRenderTargetTextures(layer.renderTarget, glSubImage.colorTexture, glSubImage.depthStencilTexture);
}
this._renderer.setRenderTarget(layer.renderTarget);
layer.rendercall();
}
this.isPresenting = wasPresenting;
this._renderer.setRenderTarget(null);
}
/**
* Returns the current XR session.
*
* @return {?XRSession} The XR session. Returns `null` when used outside a XR session.
*/
getSession() {
return this._session;
}
/**
* After a XR session has been requested usually with one of the `*Button` modules, it
* is injected into the renderer with this method. This method triggers the start of
* the actual XR rendering.
*
* @async
* @param {XRSession} session - The XR session to set.
* @return {Promise} A Promise that resolves when the session has been set.
*/
async setSession(session) {
const renderer = this._renderer;
const backend = renderer.backend;
this._gl = renderer.getContext();
const gl = this._gl;
const attributes = gl.getContextAttributes();
this._session = session;
if (session !== null) {
if (backend.isWebGPUBackend === true) throw new Error('THREE.XRManager: XR is currently not supported with a WebGPU backend. Use WebGL by passing "{ forceWebGL: true }" to the constructor of the renderer.');
session.addEventListener('select', this._onSessionEvent);
session.addEventListener('selectstart', this._onSessionEvent);
session.addEventListener('selectend', this._onSessionEvent);
session.addEventListener('squeeze', this._onSessionEvent);
session.addEventListener('squeezestart', this._onSessionEvent);
session.addEventListener('squeezeend', this._onSessionEvent);
session.addEventListener('end', this._onSessionEnd);
session.addEventListener('inputsourceschange', this._onInputSourcesChange);
await backend.makeXRCompatible();
this._currentPixelRatio = renderer.getPixelRatio();
renderer.getSize(this._currentSize);
this._currentAnimationContext = renderer._animation.getContext();
this._currentAnimationLoop = renderer._animation.getAnimationLoop();
renderer._animation.stop();
//
if (this._useLayers === true) {
// default path using XRWebGLBinding/XRProjectionLayer
let depthFormat = null;
let depthType = null;
let glDepthFormat = null;
if (renderer.depth) {
glDepthFormat = renderer.stencil ? gl.DEPTH24_STENCIL8 : gl.DEPTH_COMPONENT24;
depthFormat = renderer.stencil ? DepthStencilFormat : DepthFormat;
depthType = renderer.stencil ? UnsignedInt248Type : UnsignedIntType;
}
const projectionlayerInit = {
colorFormat: gl.RGBA8,
depthFormat: glDepthFormat,
scaleFactor: this._framebufferScaleFactor
};
const glBinding = new XRWebGLBinding(session, gl);
const glProjLayer = glBinding.createProjectionLayer(projectionlayerInit);
const layersArray = [glProjLayer];
this._glBinding = glBinding;
this._glProjLayer = glProjLayer;
renderer.setPixelRatio(1);
renderer.setSize(glProjLayer.textureWidth, glProjLayer.textureHeight, false);
this._xrRenderTarget = new XRRenderTarget(glProjLayer.textureWidth, glProjLayer.textureHeight, {
format: RGBAFormat,
type: UnsignedByteType,
colorSpace: renderer.outputColorSpace,
depthTexture: new DepthTexture(glProjLayer.textureWidth, glProjLayer.textureHeight, depthType, undefined, undefined, undefined, undefined, undefined, undefined, depthFormat),
stencilBuffer: renderer.stencil,
samples: attributes.antialias ? 4 : 0,
resolveDepthBuffer: glProjLayer.ignoreDepthValues === false,
resolveStencilBuffer: glProjLayer.ignoreDepthValues === false
});
this._xrRenderTarget.hasExternalTextures = true;
this._referenceSpace = await session.requestReferenceSpace(this.getReferenceSpaceType());
this._supportsLayers = session.enabledFeatures.includes('layers');
if (this._supportsLayers) {
// switch layers to native
for (const layer of this._layers) {
// change material so it "punches" out a hole to show the XR Layer.
layer.plane.material = new MeshBasicMaterial({
color: 0xffffff,
side: layer.type === 'cylinder' ? BackSide : FrontSide
});
layer.plane.material.blending = CustomBlending;
layer.plane.material.blendEquation = AddEquation;
layer.plane.material.blendSrc = ZeroFactor;
layer.plane.material.blendDst = ZeroFactor;
layer.xrlayer = this._createXRLayer(layer);
layersArray.unshift(layer.xrlayer);
}
}
session.updateRenderState({
layers: layersArray
});
} else {
// fallback to XRWebGLLayer
const layerInit = {
antialias: renderer.samples > 0,
alpha: true,
depth: renderer.depth,
stencil: renderer.stencil,
framebufferScaleFactor: this.getFramebufferScaleFactor()
};
const glBaseLayer = new XRWebGLLayer(session, gl, layerInit);
this._glBaseLayer = glBaseLayer;
session.updateRenderState({
baseLayer: glBaseLayer
});
renderer.setPixelRatio(1);
renderer.setSize(glBaseLayer.framebufferWidth, glBaseLayer.framebufferHeight, false);
this._xrRenderTarget = new XRRenderTarget(glBaseLayer.framebufferWidth, glBaseLayer.framebufferHeight, {
format: RGBAFormat,
type: UnsignedByteType,
colorSpace: renderer.outputColorSpace,
stencilBuffer: renderer.stencil,
resolveDepthBuffer: glBaseLayer.ignoreDepthValues === false,
resolveStencilBuffer: glBaseLayer.ignoreDepthValues === false
});
}
//
this.setFoveation(this.getFoveation());
renderer._animation.setAnimationLoop(this._onAnimationFrame);
renderer._animation.setContext(session);
renderer._animation.start();
this.isPresenting = true;
this.dispatchEvent({
type: 'sessionstart'
});
}
}
/**
* This method is called by the renderer per frame and updates the XR camera
* and it sub cameras based on the given camera. The given camera is the "user"
* camera created on application level and used for non-XR rendering.
*
* @param {PerspectiveCamera} camera - The camera.
*/
updateCamera(camera) {
const session = this._session;
if (session === null) return;
const depthNear = camera.near;
const depthFar = camera.far;
const cameraXR = this._cameraXR;
const cameraL = this._cameraL;
const cameraR = this._cameraR;
cameraXR.near = cameraR.near = cameraL.near = depthNear;
cameraXR.far = cameraR.far = cameraL.far = depthFar;
if (this._currentDepthNear !== cameraXR.near || this._currentDepthFar !== cameraXR.far) {
// Note that the new renderState won't apply until the next frame. See #18320
session.updateRenderState({
depthNear: cameraXR.near,
depthFar: cameraXR.far
});
this._currentDepthNear = cameraXR.near;
this._currentDepthFar = cameraXR.far;
}
cameraL.layers.mask = camera.layers.mask | 0b010;
cameraR.layers.mask = camera.layers.mask | 0b100;
cameraXR.layers.mask = cameraL.layers.mask | cameraR.layers.mask;
const parent = camera.parent;
const cameras = cameraXR.cameras;
updateCamera(cameraXR, parent);
for (let i = 0; i < cameras.length; i++) {
updateCamera(cameras[i], parent);
}
// update projection matrix for proper view frustum culling
if (cameras.length === 2) {
setProjectionFromUnion(cameraXR, cameraL, cameraR);
} else {
// assume single camera setup (AR)
cameraXR.projectionMatrix.copy(cameraL.projectionMatrix);
}
// update user camera and its children
updateUserCamera(camera, cameraXR, parent);
}
/**
* Returns a WebXR controller for the given controller index.
*
* @private
* @param {number} index - The controller index.
* @return {WebXRController} The XR controller.
*/
_getController(index) {
let controller = this._controllers[index];
if (controller === undefined) {
controller = new WebXRController();
this._controllers[index] = controller;
}
return controller;
}
}
/**
* Assumes 2 cameras that are parallel and share an X-axis, and that
* the cameras' projection and world matrices have already been set.
* And that near and far planes are identical for both cameras.
* Visualization of this technique: https://computergraphics.stackexchange.com/a/4765
*
* @param {ArrayCamera} camera - The camera to update.
* @param {PerspectiveCamera} cameraL - The left camera.
* @param {PerspectiveCamera} cameraR - The right camera.
*/
function setProjectionFromUnion(camera, cameraL, cameraR) {
_cameraLPos.setFromMatrixPosition(cameraL.matrixWorld);
_cameraRPos.setFromMatrixPosition(cameraR.matrixWorld);
const ipd = _cameraLPos.distanceTo(_cameraRPos);
const projL = cameraL.projectionMatrix.elements;
const projR = cameraR.projectionMatrix.elements;
// VR systems will have identical far and near planes, and
// most likely identical top and bottom frustum extents.
// Use the left camera for these values.
const near = projL[14] / (projL[10] - 1);
const far = projL[14] / (projL[10] + 1);
const topFov = (projL[9] + 1) / projL[5];
const bottomFov = (projL[9] - 1) / projL[5];
const leftFov = (projL[8] - 1) / projL[0];
const rightFov = (projR[8] + 1) / projR[0];
const left = near * leftFov;
const right = near * rightFov;
// Calculate the new camera's position offset from the
// left camera. xOffset should be roughly half `ipd`.
const zOffset = ipd / (-leftFov + rightFov);
const xOffset = zOffset * -leftFov;
// TODO: Better way to apply this offset?
cameraL.matrixWorld.decompose(camera.position, camera.quaternion, camera.scale);
camera.translateX(xOffset);
camera.translateZ(zOffset);
camera.matrixWorld.compose(camera.position, camera.quaternion, camera.scale);
camera.matrixWorldInverse.copy(camera.matrixWorld).invert();
// Check if the projection uses an infinite far plane.
if (projL[10] === -1.0) {
// Use the projection matrix from the left eye.
// The camera offset is sufficient to include the view volumes
// of both eyes (assuming symmetric projections).
camera.projectionMatrix.copy(cameraL.projectionMatrix);
camera.projectionMatrixInverse.copy(cameraL.projectionMatrixInverse);
} else {
// Find the union of the frustum values of the cameras and scale
// the values so that the near plane's position does not change in world space,
// although must now be relative to the new union camera.
const near2 = near + zOffset;
const far2 = far + zOffset;
const left2 = left - xOffset;
const right2 = right + (ipd - xOffset);
const top2 = topFov * far / far2 * near2;
const bottom2 = bottomFov * far / far2 * near2;
camera.projectionMatrix.makePerspective(left2, right2, top2, bottom2, near2, far2);
camera.projectionMatrixInverse.copy(camera.projectionMatrix).invert();
}
}
/**
* Updates the world matrices for the given camera based on the parent 3D object.
*
* @inner
* @param {Camera} camera - The camera to update.
* @param {Object3D} parent - The parent 3D object.
*/
function updateCamera(camera, parent) {
if (parent === null) {
camera.matrixWorld.copy(camera.matrix);
} else {
camera.matrixWorld.multiplyMatrices(parent.matrixWorld, camera.matrix);
}
camera.matrixWorldInverse.copy(camera.matrixWorld).invert();
}
/**
* Updates the given camera with the transformation of the XR camera and parent object.
*
* @inner
* @param {Camera} camera - The camera to update.
* @param {ArrayCamera} cameraXR - The XR camera.
* @param {Object3D} parent - The parent 3D object.
*/
function updateUserCamera(camera, cameraXR, parent) {
if (parent === null) {
camera.matrix.copy(cameraXR.matrixWorld);
} else {
camera.matrix.copy(parent.matrixWorld);
camera.matrix.invert();
camera.matrix.multiply(cameraXR.matrixWorld);
}
camera.matrix.decompose(camera.position, camera.quaternion, camera.scale);
camera.updateMatrixWorld(true);
camera.projectionMatrix.copy(cameraXR.projectionMatrix);
camera.projectionMatrixInverse.copy(cameraXR.projectionMatrixInverse);
if (camera.isPerspectiveCamera) {
camera.fov = RAD2DEG * 2 * Math.atan(1 / camera.projectionMatrix.elements[5]);
camera.zoom = 1;
}
}
function onSessionEvent(event) {
const controllerIndex = this._controllerInputSources.indexOf(event.inputSource);
if (controllerIndex === -1) {
return;
}
const controller = this._controllers[controllerIndex];
if (controller !== undefined) {
const referenceSpace = this.getReferenceSpace();
controller.update(event.inputSource, event.frame, referenceSpace);
controller.dispatchEvent({
type: event.type,
data: event.inputSource
});
}
}
function onSessionEnd() {
const session = this._session;
const renderer = this._renderer;
session.removeEventListener('select', this._onSessionEvent);
session.removeEventListener('selectstart', this._onSessionEvent);
session.removeEventListener('selectend', this._onSessionEvent);
session.removeEventListener('squeeze', this._onSessionEvent);
session.removeEventListener('squeezestart', this._onSessionEvent);
session.removeEventListener('squeezeend', this._onSessionEvent);
session.removeEventListener('end', this._onSessionEnd);
session.removeEventListener('inputsourceschange', this._onInputSourcesChange);
for (let i = 0; i < this._controllers.length; i++) {
const inputSource = this._controllerInputSources[i];
if (inputSource === null) continue;
this._controllerInputSources[i] = null;
this._controllers[i].disconnect(inputSource);
}
this._currentDepthNear = null;
this._currentDepthFar = null;
// restore framebuffer/rendering state
renderer.backend.setXRTarget(null);
renderer.setOutputRenderTarget(null);
renderer.setRenderTarget(null);
this._session = null;
this._xrRenderTarget = null;
// switch layers back to emulated
if (this._supportsLayers === true) {
for (const layer of this._layers) {
// Recreate layer render target to reset state
layer.renderTarget = new XRRenderTarget(layer.pixelwidth, layer.pixelheight, {
format: RGBAFormat,
type: UnsignedByteType,
depthTexture: new DepthTexture(layer.pixelwidth, layer.pixelheight, layer.stencilBuffer ? UnsignedInt248Type : UnsignedIntType, undefined, undefined, undefined, undefined, undefined, undefined, layer.stencilBuffer ? DepthStencilFormat : DepthFormat),
stencilBuffer: layer.stencilBuffer,
resolveDepthBuffer: false,
resolveStencilBuffer: false
});
layer.renderTarget.isXRRenderTarget = false;
layer.plane.material = layer.material;
layer.material.map = layer.renderTarget.texture;
delete layer.xrlayer;
}
}
//
this.isPresenting = false;
renderer._animation.stop();
renderer._animation.setAnimationLoop(this._currentAnimationLoop);
renderer._animation.setContext(this._currentAnimationContext);
renderer._animation.start();
renderer.setPixelRatio(this._currentPixelRatio);
renderer.setSize(this._currentSize.width, this._currentSize.height, false);
this.dispatchEvent({
type: 'sessionend'
});
}
function onInputSourcesChange(event) {
const controllers = this._controllers;
const controllerInputSources = this._controllerInputSources;
// Notify disconnected
for (let i = 0; i < event.removed.length; i++) {
const inputSource = event.removed[i];
const index = controllerInputSources.indexOf(inputSource);
if (index >= 0) {
controllerInputSources[index] = null;
controllers[index].disconnect(inputSource);
}
}
// Notify connected
for (let i = 0; i < event.added.length; i++) {
const inputSource = event.added[i];
let controllerIndex = controllerInputSources.indexOf(inputSource);
if (controllerIndex === -1) {
// Assign input source a controller that currently has no input source
for (let i = 0; i < controllers.length; i++) {
if (i >= controllerInputSources.length) {
controllerInputSources.push(inputSource);
controllerIndex = i;
break;
} else if (controllerInputSources[i] === null) {
controllerInputSources[i] = inputSource;
controllerIndex = i;
break;
}
}
// If all controllers do currently receive input we ignore new ones
if (controllerIndex === -1) break;
}
const controller = controllers[controllerIndex];
if (controller) {
controller.connect(inputSource);
}
}
}
// Creation method for native WebXR layers
function createXRLayer(layer) {
if (layer.type === 'quad') {
return this._glBinding.createQuadLayer({
transform: new XRRigidTransform(layer.translation, layer.quaternion),
depthFormat: this._gl.DEPTH_COMPONENT,
width: layer.width / 2,
height: layer.height / 2,
space: this._referenceSpace,
viewPixelWidth: layer.pixelwidth,
viewPixelHeight: layer.pixelheight
});
} else {
return this._glBinding.createCylinderLayer({
transform: new XRRigidTransform(layer.translation, layer.quaternion),
depthFormat: this._gl.DEPTH_COMPONENT,
radius: layer.radius,
centralAngle: layer.centralAngle,
aspectRatio: layer.aspectRatio,
space: this._referenceSpace,
viewPixelWidth: layer.pixelwidth,
viewPixelHeight: layer.pixelheight
});
}
}
// Animation Loop
function onAnimationFrame(time, frame) {
if (frame === undefined) return;
const cameraXR = this._cameraXR;
const renderer = this._renderer;
const backend = renderer.backend;
const glBaseLayer = this._glBaseLayer;
const referenceSpace = this.getReferenceSpace();
const pose = frame.getViewerPose(referenceSpace);
this._xrFrame = frame;
if (pose !== null) {
const views = pose.views;
if (this._glBaseLayer !== null) {
backend.setXRTarget(glBaseLayer.framebuffer);
}
let cameraXRNeedsUpdate = false;
// check if it's necessary to rebuild cameraXR's camera list
if (views.length !== cameraXR.cameras.length) {
cameraXR.cameras.length = 0;
cameraXRNeedsUpdate = true;
}
for (let i = 0; i < views.length; i++) {
const view = views[i];
let viewport;
if (this._useLayers === true) {
const glSubImage = this._glBinding.getViewSubImage(this._glProjLayer, view);
viewport = glSubImage.viewport;
// For side-by-side projection, we only produce a single texture for both eyes.
if (i === 0) {
backend.setXRRenderTargetTextures(this._xrRenderTarget, glSubImage.colorTexture, this._glProjLayer.ignoreDepthValues ? undefined : glSubImage.depthStencilTexture);
}
} else {
viewport = glBaseLayer.getViewport(view);
}
let camera = this._cameras[i];
if (camera === undefined) {
camera = new PerspectiveCamera();
camera.layers.enable(i);
camera.viewport = new Vector4();
this._cameras[i] = camera;
}
camera.matrix.fromArray(view.transform.matrix);
camera.matrix.decompose(camera.position, camera.quaternion, camera.scale);
camera.projectionMatrix.fromArray(view.projectionMatrix);
camera.projectionMatrixInverse.copy(camera.projectionMatrix).invert();
camera.viewport.set(viewport.x, viewport.y, viewport.width, viewport.height);
if (i === 0) {
cameraXR.matrix.copy(camera.matrix);
cameraXR.matrix.decompose(cameraXR.position, cameraXR.quaternion, cameraXR.scale);
}
if (cameraXRNeedsUpdate === true) {
cameraXR.cameras.push(camera);
}
}
renderer.setOutputRenderTarget(this._xrRenderTarget);
}
//
for (let i = 0; i < this._controllers.length; i++) {
const inputSource = this._controllerInputSources[i];
const controller = this._controllers[i];
if (inputSource !== null && controller !== undefined) {
controller.update(inputSource, frame, referenceSpace);
}
}
if (this._currentAnimationLoop) this._currentAnimationLoop(time, frame);
if (frame.detectedPlanes) {
this.dispatchEvent({
type: 'planesdetected',
data: frame
});
}
this._xrFrame = null;
}
export default XRManager;