1
0

7 Commits

Author SHA1 Message Date
Verdi
40e3711925 Resolve image paths relative to script URL for CDN support 2026-02-19 12:15:36 -06:00
Michael Verdi
c41ad12b32 Update index.html
Updated to use jsdelivr URLs
2026-02-19 11:23:08 -06:00
Michael Verdi
dbe6e5b1d9 Update README.md
Added jsdelivr.net CDN links
2026-02-19 11:21:45 -06:00
Michael Verdi
626b7f451b Create .gitattributes 2026-02-19 11:13:56 -06:00
Michael Verdi
a56e36eaf6 Merge pull request #4 from Verdi/fix/quest-stereo-v2
Fix stereo eye detection with multiple fallback methods
2026-01-26 23:39:48 -06:00
Verdi
bebcb3d355 Fix stereo eye detection with multiple fallback methods
- Replace onBeforeRender stereo detection to use cascading fallbacks:
  1. Direct xrCamera.cameras[0]/[1] comparison
  2. View matrix matrixWorldInverse.elements[12] with 0.001 threshold
  3. Projection matrix elements[8] as last resort
- Add video texture sync in renderXR before render call

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-26 23:27:45 -06:00
Michael Verdi
f4fb9cf6bb Merge pull request #3 from Verdi/revert-1-fix/quest-stereo-rendering
Revert "Fix stereo rendering glitches on Meta Quest browsers"
2026-01-26 23:14:47 -06:00
4 changed files with 41 additions and 16 deletions

4
.gitattributes vendored Normal file
View File

@@ -0,0 +1,4 @@
index.html export-ignore
README.md export-ignore
sbs-video.mp4 export-ignore
poster.jpg export-ignore

View File

@@ -4,10 +4,9 @@ A web-based video player for 180 degree, 3D video.
Got an immersive video you want people to see with the Apple Vision Pro or Meta Quest headsets? Now you can put it on your website just like any other video! People will see the immersive 3D video if they have a capable headset or they'll get a 2D version on other devices.
## How to use it
1. Drop the `vr180player` directory in the root level of your website.
2. Link to the player CSS file `<link rel="stylesheet" href="vr180player/vr180-player.css">`.
3. Add the player script `<script type="module" src="vr180player/vr180-player.js"></script>` before the closing body tag.
4. And use this HTML snippet to embed your video:
1. Link to the player CSS file `<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/Verdi/VR180-Web-Player@v1.0.1/vr180player/vr180-player.css">`.
2. Add the player script `<script type="module" src="https://cdn.jsdelivr.net/gh/Verdi/VR180-Web-Player@v1.0.1/vr180player/vr180-player.js"></script>` before the closing body tag.
3. And use this HTML snippet to embed your video:
```
<div id="vr-container">
<video id="vr180" poster="poster.jpg" title="Demo Video" crossOrigin="anonymous" playsinline>

View File

@@ -4,7 +4,7 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<title>VR180 Web Player</title>
<link rel="stylesheet" href="vr180player/vr180-player.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/Verdi/VR180-Web-Player@v1.0.1/vr180player/vr180-player.css">
<style>
body {
font-family: system-ui, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
@@ -28,6 +28,6 @@
<!-- UI elements will be dynamically inserted here by JavaScript -->
</div>
</main>
<script type="module" src="vr180player/vr180-player.js"></script>
<script type="module" src="https://cdn.jsdelivr.net/gh/Verdi/VR180-Web-Player@v1.0.1/vr180player/vr180-player.js"></script>
</body>
</html>

View File

@@ -1,4 +1,5 @@
import * as THREE from 'https://unpkg.com/three/build/three.module.js';
const _playerBase = new URL('.', import.meta.url).href;
let scene, camera, renderer, video, videoTexture, sphereMaterial;
let vr180Mesh;
@@ -111,7 +112,7 @@ function createPlayButton() {
playButton.setAttribute('aria-label', 'Play video');
const playImg = document.createElement('img');
playImg.src = 'vr180player/images/play.png';
playImg.src = _playerBase + 'images/play.png';
playImg.alt = 'Play';
playButton.appendChild(playImg);
@@ -429,24 +430,41 @@ function init() {
}
const xrCamera = renderer.xr.getCamera();
let isLeftEye = true; // Default to left eye
if (xrCamera && xrCamera.cameras && xrCamera.cameras.length >= 2) {
// Method 1: Direct camera reference comparison
if (activeCamera === xrCamera.cameras[0]) {
material.map.offset.x = 0;
isLeftEye = true;
} else if (activeCamera === xrCamera.cameras[1]) {
material.map.offset.x = 0.5;
isLeftEye = false;
} else {
material.map.offset.x = 0;
}
material.map.repeat.x = 0.5;
// Method 2: View matrix position (matrixWorldInverse.elements[12] is the X translation)
const viewMatrixX = activeCamera.matrixWorldInverse.elements[12];
const leftCamX = xrCamera.cameras[0].matrixWorldInverse.elements[12];
const rightCamX = xrCamera.cameras[1].matrixWorldInverse.elements[12];
const diffToLeft = Math.abs(viewMatrixX - leftCamX);
const diffToRight = Math.abs(viewMatrixX - rightCamX);
if (diffToLeft < 0.001 || diffToLeft < diffToRight) {
isLeftEye = true;
} else if (diffToRight < 0.001) {
isLeftEye = false;
} else {
// Method 3: Projection matrix asymmetry (elements[8] indicates eye offset)
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;
isLeftEye = projMatrixEl8 <= 0;
}
}
} else {
// Fallback when xrCamera.cameras is not available
const projMatrixEl8 = activeCamera.projectionMatrix.elements[8];
isLeftEye = projMatrixEl8 <= 0;
}
material.map.offset.x = isLeftEye ? 0 : 0.5;
material.map.repeat.x = 0.5;
};
// Initialize 2D camera
@@ -1731,6 +1749,10 @@ function renderXR(timestamp, frame) {
}
}
try {
// Sync video texture before render to ensure frame consistency
if (videoTexture && video && !video.paused && !video.ended) {
videoTexture.needsUpdate = true;
}
handleControllerInteractions();
renderer.render(scene, camera);
} catch (error) {