From 83f322a7e0493e1114f3fd4b2a8bb5849b3f423a Mon Sep 17 00:00:00 2001 From: Amr Khamis <101368138+AmrKhamis1@users.noreply.github.com> Date: Sat, 14 Mar 2026 23:02:09 +0200 Subject: [PATCH 1/4] Add forceMonoscopic option for WebXR --- src/renderers/webxr/WebXRManager.js | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/renderers/webxr/WebXRManager.js b/src/renderers/webxr/WebXRManager.js index 03ed5ab7e7426b..0d84de98c446d8 100644 --- a/src/renderers/webxr/WebXRManager.js +++ b/src/renderers/webxr/WebXRManager.js @@ -111,6 +111,15 @@ class WebXRManager extends EventDispatcher { */ this.isPresenting = false; + /** + * When `true`, both eyes use the left eye's view position. Useful for content + * that must be viewed from a single viewpoint (e.g. 360° panoramas, Matterport-style). + * + * @type {boolean} + * @default false + */ + this.forceMonoscopic = false; + /** * Returns a group representing the `target ray` space of the XR controller. * Use this space for visualizing 3D objects that support the user in pointing @@ -982,6 +991,13 @@ class WebXRManager extends EventDispatcher { camera.matrix.fromArray( view.transform.matrix ); camera.matrix.decompose( camera.position, camera.quaternion, camera.scale ); + if ( scope.forceMonoscopic && i === 1 && cameras[ 0 ] !== undefined ) { + + camera.position.copy( cameras[ 0 ].position ); + camera.matrix.compose( 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 ); From bee0a65a99accae731fbf4e92bb6ec785c6a7235 Mon Sep 17 00:00:00 2001 From: Amr Khamis <101368138+AmrKhamis1@users.noreply.github.com> Date: Mon, 16 Mar 2026 01:47:10 +0200 Subject: [PATCH 2/4] handling for native support --- src/renderers/webxr/WebXRManager.js | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/src/renderers/webxr/WebXRManager.js b/src/renderers/webxr/WebXRManager.js index 0d84de98c446d8..409f05c79bb611 100644 --- a/src/renderers/webxr/WebXRManager.js +++ b/src/renderers/webxr/WebXRManager.js @@ -114,6 +114,8 @@ class WebXRManager extends EventDispatcher { /** * When `true`, both eyes use the left eye's view position. Useful for content * that must be viewed from a single viewpoint (e.g. 360° panoramas, Matterport-style). + * Uses native `forceMonoPresentation` when available, otherwise a position-override fallback. + * Maintainers: please review the implementation when the WebXR Layers API evolves. * * @type {boolean} * @default false @@ -489,6 +491,18 @@ class WebXRManager extends EventDispatcher { glProjLayer = glBinding.createProjectionLayer( projectionlayerInit ); + // Monoscopic: fallback to native XR API if available. + // This will be ignored if the device/browser already supports native mono presentation + if ( scope.forceMonoscopic && 'forceMonoPresentation' in glProjLayer ) { + + try { + + glProjLayer.forceMonoPresentation = true; + + } catch ( e ) {} + + } + session.updateRenderState( { layers: [ glProjLayer ] } ); renderer.setPixelRatio( 1 ); @@ -991,9 +1005,11 @@ class WebXRManager extends EventDispatcher { camera.matrix.fromArray( view.transform.matrix ); camera.matrix.decompose( camera.position, camera.quaternion, camera.scale ); - if ( scope.forceMonoscopic && i === 1 && cameras[ 0 ] !== undefined ) { - camera.position.copy( cameras[ 0 ].position ); + // Monoscopic fallback: override right eye position when native API not available + if ( scope.forceMonoscopic && i === 1 && cameras[0] !== undefined ) { + + camera.position.copy( cameras[0].position ); camera.matrix.compose( camera.position, camera.quaternion, camera.scale ); } From 32a4d771d44d2050c6fa18bd82d3dd6ad8741869 Mon Sep 17 00:00:00 2001 From: Amr Khamis <101368138+AmrKhamis1@users.noreply.github.com> Date: Mon, 16 Mar 2026 01:52:26 +0200 Subject: [PATCH 3/4] handling for native support --- src/renderers/webxr/WebXRManager.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/renderers/webxr/WebXRManager.js b/src/renderers/webxr/WebXRManager.js index 409f05c79bb611..10fd6550bc00ef 100644 --- a/src/renderers/webxr/WebXRManager.js +++ b/src/renderers/webxr/WebXRManager.js @@ -500,7 +500,7 @@ class WebXRManager extends EventDispatcher { glProjLayer.forceMonoPresentation = true; } catch ( e ) {} - + } session.updateRenderState( { layers: [ glProjLayer ] } ); @@ -1007,9 +1007,9 @@ class WebXRManager extends EventDispatcher { camera.matrix.decompose( camera.position, camera.quaternion, camera.scale ); // Monoscopic fallback: override right eye position when native API not available - if ( scope.forceMonoscopic && i === 1 && cameras[0] !== undefined ) { + if ( scope.forceMonoscopic && i === 1 && cameras[ 0 ] !== undefined ) { - camera.position.copy( cameras[0].position ); + camera.position.copy( cameras[ 0 ].position ); camera.matrix.compose( camera.position, camera.quaternion, camera.scale ); } From 568b724dc19bdcac2574d1ff309594fe0462e9f9 Mon Sep 17 00:00:00 2001 From: Amr Khamis <101368138+AmrKhamis1@users.noreply.github.com> Date: Mon, 16 Mar 2026 09:17:33 +0200 Subject: [PATCH 4/4] only allow Native Monoscopic if availbe --- src/renderers/webxr/WebXRManager.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/renderers/webxr/WebXRManager.js b/src/renderers/webxr/WebXRManager.js index 10fd6550bc00ef..58747d79ac2972 100644 --- a/src/renderers/webxr/WebXRManager.js +++ b/src/renderers/webxr/WebXRManager.js @@ -51,6 +51,7 @@ class WebXRManager extends EventDispatcher { let glBinding = null; let glProjLayer = null; let glBaseLayer = null; + let supportsMonoscopic = false; let xrFrame = null; const supportsGlBinding = typeof XRWebGLBinding !== 'undefined'; @@ -438,6 +439,8 @@ class WebXRManager extends EventDispatcher { if ( ! supportsLayers ) { + supportsMonoscopic = false; + const layerInit = { antialias: attributes.antialias, alpha: true, @@ -491,15 +494,12 @@ class WebXRManager extends EventDispatcher { glProjLayer = glBinding.createProjectionLayer( projectionlayerInit ); + supportsMonoscopic = 'forceMonoPresentation' in glProjLayer; // Monoscopic: fallback to native XR API if available. // This will be ignored if the device/browser already supports native mono presentation - if ( scope.forceMonoscopic && 'forceMonoPresentation' in glProjLayer ) { - - try { - - glProjLayer.forceMonoPresentation = true; + if ( scope.forceMonoscopic && supportsMonoscopic ) { - } catch ( e ) {} + glProjLayer.forceMonoPresentation = true; } @@ -1007,7 +1007,7 @@ class WebXRManager extends EventDispatcher { camera.matrix.decompose( camera.position, camera.quaternion, camera.scale ); // Monoscopic fallback: override right eye position when native API not available - if ( scope.forceMonoscopic && i === 1 && cameras[ 0 ] !== undefined ) { + if ( scope.forceMonoscopic && ! supportsMonoscopic && i === 1 && cameras[ 0 ] !== undefined ) { camera.position.copy( cameras[ 0 ].position ); camera.matrix.compose( camera.position, camera.quaternion, camera.scale );