Skip to content

Add forceMonoscopic option for WebXR#33180

Draft
AmrKhamis1 wants to merge 4 commits intomrdoob:devfrom
AmrKhamis1:force-monoscopic
Draft

Add forceMonoscopic option for WebXR#33180
AmrKhamis1 wants to merge 4 commits intomrdoob:devfrom
AmrKhamis1:force-monoscopic

Conversation

@AmrKhamis1
Copy link
Copy Markdown

Related issue: #33177

Description

Adds optional forceMonoscopic flag to WebXRManager for single-viewpoint content. When enabled, both eyes use the left eye's view position, fixing projection misalignment on the right eye for content authored from a single viewpoint.

  • renderer.xr.forceMonoscopic = true enables monoscopic mode
  • Default remains stereo (backward compatible)

@github-actions
Copy link
Copy Markdown

github-actions bot commented Mar 14, 2026

📦 Bundle size

Full ESM build, minified and gzipped.

Before After Diff
WebGL 359.97
85.54
360.22
85.61
+243 B
+76 B
WebGPU 630.72
175.04
630.72
175.04
+0 B
+0 B
WebGPU Nodes 628.84
174.75
628.84
174.75
+0 B
+0 B

🌳 Bundle size after tree-shaking

Minimal build including a renderer, camera, empty scene, and dependencies.

Before After Diff
WebGL 492.17
120.12
492.41
120.19
+243 B
+69 B
WebGPU 703.46
189.94
703.46
189.94
+0 B
+0 B
WebGPU Nodes 652.69
177.33
652.69
177.33
+0 B
+0 B

@Mugen87
Copy link
Copy Markdown
Collaborator

Mugen87 commented Mar 15, 2026

@cabanier Does this change look good to you?

@cabanier
Copy link
Copy Markdown
Contributor

cabanier commented Mar 15, 2026

@Mugen87 WebXR Layers has an attribute to do this more efficient at the browser level: https://immersive-web.github.io/layers/#dom-xrcompositionlayer-forcemonopresentation.
@AmrKhamis1 , can you use that feature when it's available? Generally, WebXR implementors don't like it if you override the view/projection matrices that we give you.

@AmrKhamis1
Copy link
Copy Markdown
Author

@cabanier Agree and it shouldn't be a problem, but for now since the Layers API is not yet widely implemented, this proposal aims to provide a small opt-in workaround for cases where monoscopic presentation is required (such as panoramic or single-viewpoint scenes).
When forceMonoPresentation becomes available in browser implementations then we can switch to the native approach.

@cabanier
Copy link
Copy Markdown
Contributor

Quest is the vast majority of the WebXR market so it's widely implemented :-)
I think it should be possible to implement this by using the attribute if you detect that it is supported and switching to your implementation if it's not.
From the spec:

it is strongly recommended that applications use this matrix without modification or decomposition. Failure to use the provided projection matrices when rendering may cause the presented frame to be distorted or badly aligned, resulting in varying degrees of user discomfort.

At least for Quest, we don't like if you don't use the matrices that we give you and there might be some shaking in the right eye with your approach. I would object if it's turned on for our platform and you should likely test it on other headsets.

@AmrKhamis1
Copy link
Copy Markdown
Author

@cabanier Agree, also for context, the current workaround was tested on a Quest 3 device as in images #33177 and is already used in a production application without noticeable issues so far "all the job done on Quest". so detecting support for forceMonoPresentation and use it, otherwise use my current implementation sounds like the right approach.I'll update the implementation.

// 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

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.

@cabanier
Copy link
Copy Markdown
Contributor

I think I misunderstood this feature. It seems you are trying to display non-stereo content.
Re-using the left eye matrix for the right one is not the correct way to go.
Instead, you should create a mono quad layer. This will be properly projected and time warped. If your content is static or lower than the HMD's framerate, you could only update that layer when needed.

@Mugen87 , I don't believe this is the right approach for monoscopic content.

@cabanier
Copy link
Copy Markdown
Contributor

Alternatively, you can display your content in a three.js quad. It won't be as good as a native layer, but it will work everywhere.

@AmrKhamis1
Copy link
Copy Markdown
Author

Most of the environments for the case I'm trying to solve is related to 3D geometry projection content and 3D Reconstruction needs precise position for the camera, so both eyes needs to see the full scene non-static from same position and same content (Monoscopic), quad layer wouldn't help.

@cabanier
Copy link
Copy Markdown
Contributor

A full screen quad that is viewer locked should work for you. It might even be faster because you would draw the geometry only once.

@AmrKhamis1
Copy link
Copy Markdown
Author

it's solid point and that is why matterport and realsee.ai and any similar uses this approch on their VR while did not maintain the same implementation of webGL into the XR session... here is an example https://my.matterport.com/show/?m=fCdL9GEmVag (they did use similar forceMono technique i suggest ) noticing the deference between web and vr, that cause they had to abandoned the real time rendering. my approach was keep using XRProjectionLayer that is why i said quad layer wouldn't help i think, do you think we should proceed different approach?

@cabanier
Copy link
Copy Markdown
Contributor

For Matterport, they should really be using an equirectlayer. It's possible to use those with three.js.
We also have a polyfill if the code runs in other headsets.
If you don't want to use layers, you could create the equirect yourself. In that case you only need to draw the scene once and then rotate it.

Both approaches will produce a correct perspective for both eyes.

@AmrKhamis1
Copy link
Copy Markdown
Author

@cabanier Thanks for the suggestions and discussion. I currently have several projects that rely on this behavior, so I'll move this PR to draft for now and further investigate the layers-based approaches.

@AmrKhamis1 AmrKhamis1 marked this pull request as draft March 16, 2026 19:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants