Skip to content
Draft
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions src/renderers/webxr/WebXRManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,17 @@ 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).
* 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
*/
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
Expand Down Expand Up @@ -480,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 {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you don't need to do a try/catch here. This method can't throw


glProjLayer.forceMonoPresentation = true;

} catch ( e ) {}

}

session.updateRenderState( { layers: [ glProjLayer ] } );

renderer.setPixelRatio( 1 );
Expand Down Expand Up @@ -982,6 +1005,15 @@ class WebXRManager extends EventDispatcher {

camera.matrix.fromArray( view.transform.matrix );
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 ) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you still do this when the attribute exists

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unfortunately, I only have a Quest 3 for testing. The feature shows as available, but it’s not fully working yet. It will work once the feature is actually supported. In the meantime, it only falls back when not available.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How is it not working? Can you send me a file where it's broken?

Copy link
Copy Markdown
Author

@AmrKhamis1 AmrKhamis1 Mar 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don’t have a reproducible example beyond the Quest 3. The attribute appears in the API but stays at Stereo because it’s not fully functional yet. I can share a small test snippet if needed.take a look at last edit 568b724.


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 );
Expand Down