1
0

18 Commits
2D ... v1.0.1

Author SHA1 Message Date
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
Michael Verdi
38200c82f2 Revert "Fix stereo rendering glitches on Meta Quest browsers" 2026-01-26 23:12:43 -06:00
Michael Verdi
715e762fc9 Merge pull request #2 from Verdi/fix/quest-stereo-rendering
update video
2026-01-26 22:51:52 -06:00
Verdi
928fafa290 update video 2026-01-26 22:41:56 -06:00
Michael Verdi
065e8310e3 Merge pull request #1 from Verdi/fix/quest-stereo-rendering
Fix stereo rendering glitches on Meta Quest browsers
2026-01-26 18:23:51 -06:00
Verdi
4183ae2530 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 <noreply@anthropic.com>
2026-01-26 18:20:13 -06:00
Michael Verdi
d869f75e1e Update vr180-player.js
Updated threejs CDN link with one that updates to latest version.
2026-01-26 11:07:07 -06:00
Michael Verdi
b559ea6ebf Merge remote changes with canvas positioning fix 2025-08-01 15:23:12 -05:00
Michael Verdi
85baf3cd79 Fix 2D canvas positioning bug
- Changed canvas positioning from absolute to relative in start2DMode()
- Resolves issue where content below video was hidden underneath canvas
- Fixes canvas disappearing during page resize and fullscreen operations
- Canvas now properly flows within document layout
2025-08-01 15:22:26 -05:00
Michael Verdi
0a8cb8196c Update README.md 2025-08-01 14:45:12 -05:00
Michael Verdi
5087c3cbb2 Merge remote changes with local updates 2025-08-01 14:40:20 -05:00
Michael Verdi
0ef4ca56a5 Update HTML, CSS, and video file
- Updated index.html with latest changes
- Updated vr180player/vr180-player.css with styling improvements
- Updated sbs-video.mp4 with new video content
2025-08-01 14:38:42 -05:00
Michael Verdi
ffb29bc4ec Update README.md 2025-08-01 12:43:41 -05:00
Michael Verdi
090ad5f315 Merge 2D branch: Fix canvas resizing bug in 2D mode 2025-08-01 12:37:36 -05:00
Michael Verdi
957f1af8b0 Update README.md 2025-08-01 10:31:52 -05:00
6 changed files with 59 additions and 40 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

@@ -1,43 +1,38 @@
# VR180 Web Player # VR180 Web Player
A web-based video player for 180 degree, 3D video. 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? You could build an app and deal with app stores. You could jump through some hoops to put it on YouTube but it will be limited to Meta headsets. Or you can use this web player and put it on your website. 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 ## How to use it
Add the player script `<script type="module" src="vr180-player.js"></script>` before the closing body tag and use this HTML snippet: 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:
``` ```
<div id="vr-container"> <div id="vr-container">
<video id="vr180" poster="poster.jpg" title="Demo Video" crossOrigin="anonymous" playsinline> <video id="vr180" poster="poster.jpg" title="Demo Video" crossOrigin="anonymous" playsinline>
<source src="sbs-video.mp4" type="video/mp4"> <source src="sbs-video.mp4" type="video/mp4">
</video> </video>
</div> </div>
<button id="playBtn">Play</button>
``` ```
This creates a button on your page. When VR is available, the button will be active and clicking it will begin the immersive experience.
<img src="https://github.com/user-attachments/assets/05db6208-6d42-48fa-a0da-55de41f35e6d" width=50%> ## How it works
When the webpage loads, the video file is embeded normally, with a play button positioned over the poster frame. When the user clicks play, the player script checks if `navigator.xr` exists. If it does, a VR experience is initiated. If not, it builds a rectilinear, 2D view of your video and plays it that way.
*Example Button* **VR on Apple Vision Pro**
![vr](https://github.com/user-attachments/assets/c1097a4f-8712-4e6b-a233-a52d49cb261e)
Once the video is playing, you can bring up video controls. When the video is over, you'll automatically exit the experience. **2D in Safari on Mac**
![vr180-web-player](https://github.com/user-attachments/assets/ac86dba9-add9-462e-9590-26abc5f20912) You can drag the 2D video around to see things outside the frame.
<img width="1000" height="793" alt="2d" src="https://github.com/user-attachments/assets/094d30b7-7175-44ba-a700-d333196f8bb3" />
## Video Format ## Video Format
**The player only supports 2:1, side-by-side video using either H.264 or HEVC in an mp4 file.** It does not support over-under, MV-HEVC, or .aivu. **The player only supports 2:1, side-by-side video using either H.264 or HEVC in an mp4 file.** It does not support over-under, MV-HEVC, APMP, or .aivu.
## Features ## Support
Tapping anywhere will bring up the controls. Without interaction they will go away in 10 seconds. Tapping outside of the controls will close them right away. I'm not a developer and I might not be able to help you if you run into problems, want to customize this, or add new features (not that I won't try). I'm releasing this with the [unlicense](https://unlicense.org/) so you're free to do anything at all with it. That said, if you have ideas or want to contribute code, I'd love to [hear](mailto:hello@michaelverdi.com) from you.
- Play/Pause
- Rewind 15 seconds
- Skip 15 seconds
- Mute/Unmute
- Seek
- Exit VR
## Future
I'm not a developer. I used AI to help create this and give me the ability to post immersive videos on my website. I'm unlikely to be able to help you if you run into problems, want to customize this, or add new features. I'm releasing this with the [unlicense](https://unlicense.org/) so you're free to do anything at all with it. That said, if you have ideas or want to contribute code, I'd love to [hear](mailto:hello@michaelverdi.com) from you.
## Demo ## Demo
**Test it out in a headset!** **Test it out!**
Open [https://verdi.github.io/VR180-Web-Player/](https://verdi.github.io/VR180-Web-Player/) in a browser on your headset and then click the Enter VR button. Open [https://verdi.github.io/VR180-Web-Player/](https://verdi.github.io/VR180-Web-Player/) in a browser on your headset (or another device). Or check out this short film I made -> [Blandscape](https://michaelverdi.com/blandscape)

View File

@@ -15,11 +15,6 @@
max-width: 750px; max-width: 750px;
margin: auto; margin: auto;
} }
video {
width: 100%;
height: auto;
aspect-ratio: 16/9;
}
</style> </style>
</head> </head>
<body> <body>

Binary file not shown.

View File

@@ -4,6 +4,12 @@
width: 100%; width: 100%;
} }
video {
width: 100%;
height: auto;
aspect-ratio: 16/9;
}
#playBtn { #playBtn {
position: absolute; position: absolute;
top: 50%; top: 50%;

View File

@@ -1,4 +1,4 @@
import * as THREE from 'https://cdn.jsdelivr.net/npm/three@0.176.0/build/three.module.js'; import * as THREE from 'https://unpkg.com/three/build/three.module.js';
let scene, camera, renderer, video, videoTexture, sphereMaterial; let scene, camera, renderer, video, videoTexture, sphereMaterial;
let vr180Mesh; let vr180Mesh;
@@ -429,24 +429,41 @@ function init() {
} }
const xrCamera = renderer.xr.getCamera(); const xrCamera = renderer.xr.getCamera();
let isLeftEye = true; // Default to left eye
if (xrCamera && xrCamera.cameras && xrCamera.cameras.length >= 2) { if (xrCamera && xrCamera.cameras && xrCamera.cameras.length >= 2) {
// Method 1: Direct camera reference comparison
if (activeCamera === xrCamera.cameras[0]) { if (activeCamera === xrCamera.cameras[0]) {
material.map.offset.x = 0; isLeftEye = true;
} else if (activeCamera === xrCamera.cameras[1]) { } else if (activeCamera === xrCamera.cameras[1]) {
material.map.offset.x = 0.5; isLeftEye = false;
} else { } else {
material.map.offset.x = 0; // 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];
isLeftEye = projMatrixEl8 <= 0;
}
} }
material.map.repeat.x = 0.5;
} else { } else {
// Fallback when xrCamera.cameras is not available
const projMatrixEl8 = activeCamera.projectionMatrix.elements[8]; const projMatrixEl8 = activeCamera.projectionMatrix.elements[8];
if (projMatrixEl8 < -0.0001) { isLeftEye = projMatrixEl8 <= 0;
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;
}
} }
material.map.offset.x = isLeftEye ? 0 : 0.5;
material.map.repeat.x = 0.5;
}; };
// Initialize 2D camera // Initialize 2D camera
@@ -1436,9 +1453,7 @@ function start2DMode() {
// Position the canvas to match the video element // Position the canvas to match the video element
const canvas = renderer.domElement; const canvas = renderer.domElement;
canvas.style.position = 'absolute'; canvas.style.position = 'relative';
canvas.style.top = '0';
canvas.style.left = '0';
canvas.style.width = '100%'; canvas.style.width = '100%';
canvas.style.height = 'auto'; canvas.style.height = 'auto';
canvas.style.aspectRatio = '16/9'; canvas.style.aspectRatio = '16/9';
@@ -1733,6 +1748,10 @@ function renderXR(timestamp, frame) {
} }
} }
try { try {
// Sync video texture before render to ensure frame consistency
if (videoTexture && video && !video.paused && !video.ended) {
videoTexture.needsUpdate = true;
}
handleControllerInteractions(); handleControllerInteractions();
renderer.render(scene, camera); renderer.render(scene, camera);
} catch (error) { } catch (error) {