From 4183ae253092387d512f769ef1920ab413c07148 Mon Sep 17 00:00:00 2001 From: Verdi Date: Tue, 27 Jan 2026 00:16:54 +0000 Subject: [PATCH] Fix stereo rendering glitches on Meta Quest browsers Replace camera reference comparison with view matrix eye detection for more reliable left/right eye identification. Recent Quest Browser updates (Chromium 138/140, Horizon OS v83) changed WebXR multiview behavior, causing the previous xrCamera.cameras[0]/[1] comparison to fail intermittently on the left eye. The new approach uses activeCamera.matrixWorldInverse.elements[12] (the X translation in the view matrix) which reliably indicates: - Negative value = left eye - Positive value = right eye This method is consistent across both Quest Browser and Safari/VisionOS. Also adds explicit video texture synchronization in the render loop to prevent timing-related glitches. Co-Authored-By: Claude Opus 4.5 --- vr180player/vr180-player.js | 37 +++++++++++++++++++------------------ 1 file changed, 19 insertions(+), 18 deletions(-) diff --git a/vr180player/vr180-player.js b/vr180player/vr180-player.js index cdb098a..1cbe520 100644 --- a/vr180player/vr180-player.js +++ b/vr180player/vr180-player.js @@ -411,7 +411,7 @@ function init() { vr180Mesh.onBeforeRender = function (renderer, scene, activeCamera, geometry, material, group) { if (!material.map) return; const isPresentingXR = renderer.xr.isPresenting; - + // Handle 2D mode - show only left eye view if (is2DMode && !isPresentingXR) { material.map.offset.x = 0; @@ -420,7 +420,7 @@ function init() { material.map.repeat.y = 1; return; } - + // Default to full texture for non-VR, non-2D mode material.map.offset.x = 0; material.map.repeat.x = 1; material.map.offset.y = 0; material.map.repeat.y = 1; @@ -428,25 +428,20 @@ function init() { return; } - const xrCamera = renderer.xr.getCamera(); + // Use view matrix eye offset for reliable stereo detection + // This works consistently across Quest Browser updates and Safari/VisionOS + // Left eye has negative X offset, right eye has positive X offset + const viewMatrix = activeCamera.matrixWorldInverse; + const eyeOffsetX = viewMatrix.elements[12]; - if (xrCamera && xrCamera.cameras && xrCamera.cameras.length >= 2) { - if (activeCamera === xrCamera.cameras[0]) { - material.map.offset.x = 0; - } else if (activeCamera === xrCamera.cameras[1]) { - material.map.offset.x = 0.5; - } else { - material.map.offset.x = 0; - } - material.map.repeat.x = 0.5; + if (eyeOffsetX < 0) { + // Left eye - show left half of SBS video + material.map.offset.x = 0; } else { - const projMatrixEl8 = activeCamera.projectionMatrix.elements[8]; - if (projMatrixEl8 < -0.0001) { - material.map.offset.x = 0; material.map.repeat.x = 0.5; - } else if (projMatrixEl8 > 0.0001) { - material.map.offset.x = 0.5; material.map.repeat.x = 0.5; - } + // Right eye - show right half of SBS video + material.map.offset.x = 0.5; } + material.map.repeat.x = 0.5; }; // Initialize 2D camera @@ -1731,6 +1726,12 @@ function renderXR(timestamp, frame) { } } try { + // Ensure video texture is synchronized with render loop + // This prevents glitches from texture update timing issues on Quest browsers + if (videoTexture && video && !video.paused && !video.ended) { + videoTexture.needsUpdate = true; + } + handleControllerInteractions(); renderer.render(scene, camera); } catch (error) {