UNPKG

@openhps/core

Version:

Open Hybrid Positioning System - Core component

1,188 lines (1,092 loc) 39.2 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.default = void 0; var _ArrayCamera = require("../../cameras/ArrayCamera.js"); var _EventDispatcher = require("../../core/EventDispatcher.js"); var _PerspectiveCamera = require("../../cameras/PerspectiveCamera.js"); var _Quaternion = require("../../math/Quaternion.js"); var _MathUtils = require("../../math/MathUtils.js"); var _Vector = require("../../math/Vector2.js"); var _Vector2 = require("../../math/Vector3.js"); var _Vector3 = require("../../math/Vector4.js"); var _WebXRController = require("../webxr/WebXRController.js"); var _constants = require("../../constants.js"); var _DepthTexture = require("../../textures/DepthTexture.js"); var _XRRenderTarget = require("./XRRenderTarget.js"); var _CylinderGeometry = require("../../geometries/CylinderGeometry.js"); var _PlaneGeometry = require("../../geometries/PlaneGeometry.js"); var _MeshBasicMaterial = require("../../materials/MeshBasicMaterial.js"); var _Mesh = require("../../objects/Mesh.js"); const _cameraLPos = /*@__PURE__*/new _Vector2.Vector3(); const _cameraRPos = /*@__PURE__*/new _Vector2.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.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.PerspectiveCamera(); this._cameraL.viewport = new _Vector3.Vector4(); /** * Represents the camera for the right eye. * * @private * @type {PerspectiveCamera} */ this._cameraR = new _PerspectiveCamera.PerspectiveCamera(); this._cameraR.viewport = new _Vector3.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.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 _Vector.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.PlaneGeometry(width, height); const renderTarget = new _XRRenderTarget.XRRenderTarget(pixelwidth, pixelheight, { format: _constants.RGBAFormat, type: _constants.UnsignedByteType, depthTexture: new _DepthTexture.DepthTexture(pixelwidth, pixelheight, attributes.stencil ? _constants.UnsignedInt248Type : _constants.UnsignedIntType, undefined, undefined, undefined, undefined, undefined, undefined, attributes.stencil ? _constants.DepthStencilFormat : _constants.DepthFormat), stencilBuffer: attributes.stencil, resolveDepthBuffer: false, resolveStencilBuffer: false }); const material = new _MeshBasicMaterial.MeshBasicMaterial({ color: 0xffffff, side: _constants.FrontSide }); material.map = renderTarget.texture; material.map.offset.y = 1; material.map.repeat.y = -1; const plane = new _Mesh.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.MeshBasicMaterial({ color: 0xffffff, side: _constants.FrontSide }); layer.plane.material.blending = _constants.CustomBlending; layer.plane.material.blendEquation = _constants.AddEquation; layer.plane.material.blendSrc = _constants.ZeroFactor; layer.plane.material.blendDst = _constants.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.CylinderGeometry(radius, radius, radius * centralAngle / aspectratio, 64, 64, true, Math.PI - centralAngle / 2, centralAngle); const renderTarget = new _XRRenderTarget.XRRenderTarget(pixelwidth, pixelheight, { format: _constants.RGBAFormat, type: _constants.UnsignedByteType, depthTexture: new _DepthTexture.DepthTexture(pixelwidth, pixelheight, attributes.stencil ? _constants.UnsignedInt248Type : _constants.UnsignedIntType, undefined, undefined, undefined, undefined, undefined, undefined, attributes.stencil ? _constants.DepthStencilFormat : _constants.DepthFormat), stencilBuffer: attributes.stencil, resolveDepthBuffer: false, resolveStencilBuffer: false }); const material = new _MeshBasicMaterial.MeshBasicMaterial({ color: 0xffffff, side: _constants.BackSide }); material.map = renderTarget.texture; material.map.offset.y = 1; material.map.repeat.y = -1; const plane = new _Mesh.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.MeshBasicMaterial({ color: 0xffffff, side: _constants.BackSide }); layer.plane.material.blending = _constants.CustomBlending; layer.plane.material.blendEquation = _constants.AddEquation; layer.plane.material.blendSrc = _constants.ZeroFactor; layer.plane.material.blendDst = _constants.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 _Vector2.Vector3(); const quaternionObject = new _Quaternion.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 ? _constants.DepthStencilFormat : _constants.DepthFormat; depthType = renderer.stencil ? _constants.UnsignedInt248Type : _constants.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.XRRenderTarget(glProjLayer.textureWidth, glProjLayer.textureHeight, { format: _constants.RGBAFormat, type: _constants.UnsignedByteType, colorSpace: renderer.outputColorSpace, depthTexture: new _DepthTexture.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.MeshBasicMaterial({ color: 0xffffff, side: layer.type === 'cylinder' ? _constants.BackSide : _constants.FrontSide }); layer.plane.material.blending = _constants.CustomBlending; layer.plane.material.blendEquation = _constants.AddEquation; layer.plane.material.blendSrc = _constants.ZeroFactor; layer.plane.material.blendDst = _constants.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.XRRenderTarget(glBaseLayer.framebufferWidth, glBaseLayer.framebufferHeight, { format: _constants.RGBAFormat, type: _constants.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.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 = _MathUtils.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.XRRenderTarget(layer.pixelwidth, layer.pixelheight, { format: _constants.RGBAFormat, type: _constants.UnsignedByteType, depthTexture: new _DepthTexture.DepthTexture(layer.pixelwidth, layer.pixelheight, layer.stencilBuffer ? _constants.UnsignedInt248Type : _constants.UnsignedIntType, undefined, undefined, undefined, undefined, undefined, undefined, layer.stencilBuffer ? _constants.DepthStencilFormat : _constants.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.PerspectiveCamera(); camera.layers.enable(i); camera.viewport = new _Vector3.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; } var _default = exports.default = XRManager;