|
|
@@ -0,0 +1,1075 @@
|
|
|
+import { ArrayCamera } from '../../cameras/ArrayCamera.js';
|
|
|
+import { EventDispatcher } from '../../core/EventDispatcher.js';
|
|
|
+import { RenderTarget } from '../../core/RenderTarget.js';
|
|
|
+import { PerspectiveCamera } from '../../cameras/PerspectiveCamera.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 { RGBAFormat, UnsignedByteType } from '../../constants.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.
|
|
|
+ */
|
|
|
+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 current render target of the renderer.
|
|
|
+ *
|
|
|
+ * @private
|
|
|
+ * @type {RenderTarget?}
|
|
|
+ * @default null
|
|
|
+ */
|
|
|
+ this._currentRenderTarget = null;
|
|
|
+
|
|
|
+ /**
|
|
|
+ * The XR render target that represents the rendering destination
|
|
|
+ * during an active XR session.
|
|
|
+ *
|
|
|
+ * @private
|
|
|
+ * @type {RenderTarget?}
|
|
|
+ * @default null
|
|
|
+ */
|
|
|
+ this._xrRenderTarget = 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 applicatio. 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 {String}
|
|
|
+ * @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 frame.
|
|
|
+ *
|
|
|
+ * @private
|
|
|
+ * @type {XRFrame?}
|
|
|
+ * @default null
|
|
|
+ */
|
|
|
+ this._xrFrame = null;
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 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 layer is defined.
|
|
|
+ */
|
|
|
+ getFoveation() {
|
|
|
+
|
|
|
+ if ( 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._glBaseLayer !== null && this._glBaseLayer.fixedFoveation !== undefined ) {
|
|
|
+
|
|
|
+ this._glBaseLayer.fixedFoveation = foveation;
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Returns the frammebuffer scale factor.
|
|
|
+ *
|
|
|
+ * @return {Number} The frammebuffer scale factor.
|
|
|
+ */
|
|
|
+ getFramebufferScaleFactor() {
|
|
|
+
|
|
|
+ return this._framebufferScaleFactor;
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Sets the frammebuffer scale factor.
|
|
|
+ *
|
|
|
+ * This method can not be used during a XR session.
|
|
|
+ *
|
|
|
+ * @param {Number} factor - The frammebuffer 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 {String} The reference space type.
|
|
|
+ */
|
|
|
+ getReferenceSpaceType() {
|
|
|
+
|
|
|
+ return this._referenceSpaceType;
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Sets the reference space type.
|
|
|
+ *
|
|
|
+ * This method can not be used during a XR session.
|
|
|
+ *
|
|
|
+ * @param {String} 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'} The environment blend mode.
|
|
|
+ */
|
|
|
+ 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;
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 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 gl = renderer.getContext();
|
|
|
+
|
|
|
+ this._session = session;
|
|
|
+
|
|
|
+ if ( session !== null ) {
|
|
|
+
|
|
|
+ if ( renderer.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.' );
|
|
|
+
|
|
|
+ this._currentRenderTarget = renderer.getRenderTarget();
|
|
|
+
|
|
|
+ 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 renderer.makeXRCompatible();
|
|
|
+
|
|
|
+ this._currentPixelRatio = renderer.getPixelRatio();
|
|
|
+ renderer.getSize( this._currentSize );
|
|
|
+
|
|
|
+ this._currentAnimationContext = renderer._animation.getContext();
|
|
|
+ this._currentAnimationLoop = renderer._animation.getAnimationLoop();
|
|
|
+ renderer._animation.stop();
|
|
|
+
|
|
|
+ const attributes = gl.getContextAttributes();
|
|
|
+
|
|
|
+ const layerInit = {
|
|
|
+ antialias: attributes.antialias,
|
|
|
+ alpha: true,
|
|
|
+ depth: attributes.depth,
|
|
|
+ stencil: attributes.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 RenderTarget(
|
|
|
+ glBaseLayer.framebufferWidth,
|
|
|
+ glBaseLayer.framebufferHeight,
|
|
|
+ {
|
|
|
+ format: RGBAFormat,
|
|
|
+ type: UnsignedByteType,
|
|
|
+ colorSpace: renderer.outputColorSpace,
|
|
|
+ stencilBuffer: attributes.stencil
|
|
|
+ }
|
|
|
+ );
|
|
|
+
|
|
|
+ this._xrRenderTarget.isXRRenderTarget = true; // TODO Remove this when possible, see #23278
|
|
|
+
|
|
|
+ this.setFoveation( this.getFoveation() );
|
|
|
+
|
|
|
+ this._referenceSpace = await session.requestReferenceSpace( this.getReferenceSpaceType() );
|
|
|
+
|
|
|
+ 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 "normal"
|
|
|
+ * 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 transfomration 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.setRenderTarget( this._currentRenderTarget );
|
|
|
+
|
|
|
+ this._session = null;
|
|
|
+ this._xrRenderTarget = null;
|
|
|
+
|
|
|
+ //
|
|
|
+
|
|
|
+ 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 );
|
|
|
+
|
|
|
+ renderer.setXRTarget( null );
|
|
|
+
|
|
|
+ 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 );
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+}
|
|
|
+
|
|
|
+function onAnimationFrame( time, frame ) {
|
|
|
+
|
|
|
+ if ( frame === undefined ) return;
|
|
|
+
|
|
|
+ const cameraXR = this._cameraXR;
|
|
|
+ const renderer = this._renderer;
|
|
|
+
|
|
|
+ const glBaseLayer = this._glBaseLayer;
|
|
|
+
|
|
|
+ const referenceSpace = this.getReferenceSpace();
|
|
|
+ const pose = frame.getViewerPose( referenceSpace );
|
|
|
+
|
|
|
+ this._xrFrame = frame;
|
|
|
+
|
|
|
+ if ( pose !== null ) {
|
|
|
+
|
|
|
+ const views = pose.views;
|
|
|
+
|
|
|
+ renderer.setXRTarget( glBaseLayer.framebuffer );
|
|
|
+ renderer.setRenderTarget( this._xrRenderTarget );
|
|
|
+
|
|
|
+ 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 ];
|
|
|
+
|
|
|
+ const 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 );
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ //
|
|
|
+
|
|
|
+ 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;
|