From 8a31099c68b2fa8c6576fe17ce122eae1a180cf4 Mon Sep 17 00:00:00 2001 From: paulisere Date: Sat, 8 Nov 2025 17:08:10 +0000 Subject: [PATCH] WebXr rendering working (#1) * WebXr rendering working * Code tidying * Code tidying --- demos/webvr/WebVrView.js | 43 +++++--- demos/webvr/index.js | 217 ++++++++++++++++++++------------------- 2 files changed, 140 insertions(+), 120 deletions(-) diff --git a/demos/webvr/WebVrView.js b/demos/webvr/WebVrView.js index 06d785704..8b9e95edf 100644 --- a/demos/webvr/WebVrView.js +++ b/demos/webvr/WebVrView.js @@ -19,16 +19,16 @@ var eventEmitter = Marzipano.dependencies.eventEmitter; var mat4 = Marzipano.dependencies.glMatrix.mat4; var vec4 = Marzipano.dependencies.glMatrix.vec4; -// A minimal View implementation for use with WebVR. +// A minimal View implementation for use with WebXR. // -// Note that RectilinearView cannot be used because the WebVR API exposes a view +// Note that RectilinearView cannot be used because the WebXR API exposes a view // matrix instead of view parameters (yaw, pitch and roll). // // Most of the code has been copied verbatim from RectilinearView, but some // methods are missing (e.g. screenToCoordinates and coordinatesToScreen). // If we ever graduate this class to the core library, we'll need to figure out // the best way to share code between the two. -function WebVrView() { +function WebXrView() { this._width = 0; this._height = 0; @@ -46,13 +46,13 @@ function WebVrView() { this._tmpVec = vec4.create(); }; -eventEmitter(WebVrView); +eventEmitter(WebXrView); -WebVrView.prototype.destroy = function() { +WebXrView.prototype.destroy = function() { clearOwnProperties(this); }; -WebVrView.prototype.size = function(size) { +WebXrView.prototype.size = function(size) { size = size || {}; size.width = this._width; size.height = this._height; @@ -61,20 +61,20 @@ WebVrView.prototype.size = function(size) { return size; }; -WebVrView.prototype.setSize = function(size) { +WebXrView.prototype.setSize = function(size) { this._width = size.width; this._height = size.height; }; -WebVrView.prototype.projection = function() { +WebXrView.prototype.projection = function() { return this._proj; }; -WebVrView.prototype.inverseProjection = function() { +WebXrView.prototype.inverseProjection = function() { return this._invProj; }; -WebVrView.prototype.setProjection = function(proj) { +WebXrView.prototype.setProjection = function(proj) { var p = this._proj; var invp = this._invProj; var f = this._frustum; @@ -93,13 +93,30 @@ WebVrView.prototype.setProjection = function(proj) { this.emit('change'); }; -WebVrView.prototype.selectLevel = function(levelList) { +// Set projection matrix from a WebXR XRView +WebXrView.prototype.setProjectionFromXRView = function(xrView) { + var pose = mat4.create(); + mat4.copy(pose, xrView.transform.matrix); + // Clear out translation + pose[12] = 0; + pose[13] = 0; + pose[14] = 0; + mat4.invert(pose, pose); + + var proj = mat4.create(); + mat4.copy(proj, xrView.projectionMatrix); + mat4.multiply(proj, proj, pose); + + this.setProjection(proj); +}; + +WebXrView.prototype.selectLevel = function(levelList) { // TODO: Figure out how to determine the most appropriate resolution. // For now, always default to the highest resolution level. return levelList[levelList.length-1]; }; -WebVrView.prototype.intersects = function(rectangle) { +WebXrView.prototype.intersects = function(rectangle) { // Check whether the rectangle is on the outer side of any of the frustum // planes. This is a sufficient condition, though not necessary, for the // rectangle to be completely outside the frustum. @@ -123,4 +140,4 @@ WebVrView.prototype.intersects = function(rectangle) { }; // Pretend to be a RectilinearView so that an appropriate renderer can be found. -WebVrView.type = WebVrView.prototype.type = 'rectilinear'; +WebXrView.type = WebXrView.prototype.type = 'rectilinear'; diff --git a/demos/webvr/index.js b/demos/webvr/index.js index 7c54ec274..c1b02d268 100644 --- a/demos/webvr/index.js +++ b/demos/webvr/index.js @@ -15,119 +15,122 @@ */ 'use strict'; -var mat4 = Marzipano.dependencies.glMatrix.mat4; -var quat = Marzipano.dependencies.glMatrix.quat; +async function main() { + + var viewerElement = document.querySelector("#pano"); + var enterVrElement = document.querySelector("#enter-vr"); + var noVrElement = document.querySelector("#no-vr"); + + // Create stage and register renderers. + var stage = new Marzipano.WebGlStage(); + Marzipano.registerDefaultRenderers(stage); + + // Insert stage into the DOM. + viewerElement.appendChild(stage.domElement()); + + // Create geometry. + var geometry = new Marzipano.CubeGeometry([ + { tileSize: 256, size: 256, fallbackOnly: true }, + { tileSize: 512, size: 512 }, + { tileSize: 512, size: 1024 }, + { tileSize: 512, size: 2048 }, + { tileSize: 512, size: 4096 } + ]); + + // Create view. + var limiter = Marzipano.RectilinearView.limit.traditional(4096, 110*Math.PI/180); + var viewLeft = new WebXrView(); + var viewRight = new WebXrView(); + + // Create layers. + var layerLeft = createLayer(stage, viewLeft, geometry, 'left', + { relativeWidth: 0.5, relativeX: 0 }); + var layerRight = createLayer(stage, viewRight, geometry, 'right', + { relativeWidth: 0.5, relativeX: 0.5 }); + + // Add layers into stage. + stage.addLayer(layerLeft); + stage.addLayer(layerRight); + + // WebXR session and rendering logic + let xrRefSpace = null; + + let supported = await navigator.xr.isSessionSupported('immersive-vr'); + enterVrElement.style.display = supported ? 'block' : 'none'; + noVrElement.style.display = supported ? 'none' : 'block'; + + // Enter WebxR mode when the button is clicked. + enterVrElement.addEventListener('click', function() { + if (!navigator.xr) return; + navigator.xr.requestSession('immersive-vr', { requiredFeatures: ['local-floor'] }).then(onSessionStarted); + }); -var degToRad = Marzipano.util.degToRad; + function onSessionStarted(session) { + let xrSession = session; + + // Set up XRWebGLLayer with Marzipano's WebGL context + const gl = stage.webGlContext(); + gl.makeXRCompatible().then(() => { + xrSession.updateRenderState({ baseLayer: new XRWebGLLayer(xrSession, gl) }); + xrSession.requestReferenceSpace('local-floor').then(function(refSpace) { + xrRefSpace = refSpace; + xrSession.requestAnimationFrame(onXRFrame); + }); + }); + } -var viewerElement = document.querySelector("#pano"); -var enterVrElement = document.querySelector("#enter-vr"); -var noVrElement = document.querySelector("#no-vr"); + function onXRFrame(time, frame) { + let session = frame.session; + let pose = frame.getViewerPose(xrRefSpace); + if (!pose) { + session.requestAnimationFrame(onXRFrame); + return; + } + + // Ensure we're rendering to the layer's backbuffer. + let layer = session.renderState.baseLayer; + const gl = stage.webGlContext(); + gl.bindFramebuffer(gl.FRAMEBUFFER, layer.framebuffer); + + // For stereo, use the first two views (usually left/right) + if (pose.views.length >= 2) { + viewLeft.setProjectionFromXRView(pose.views[0]); + viewRight.setProjectionFromXRView(pose.views[1]); + + let layer = session.renderState.baseLayer; + let viewportLeft = layer.getViewport(pose.views[0]); + let viewportRight = layer.getViewport(pose.views[1]); + + // Width of stage is the width for the left and right eyes + stage.setSize({ + width: viewportLeft.width + viewportRight.width, + height: viewportLeft.height + }); + + } else if (pose.views.length === 1) { + viewLeft.setProjectionFromXRView(pose.views[0]); + viewRight.setProjectionFromXRView(pose.views[0]); + } + + stage.render(); + + session.requestAnimationFrame(onXRFrame); + } -// Install the WebVR polyfill, which makes the demo functional on "fake" WebVR -// displays such as Google Cardboard. -var polyfill = new WebVRPolyfill(); + function createLayer(stage, view, geometry, eye, rect) { + var urlPrefix = "//www.marzipano.net/media/music-room"; + var source = new Marzipano.ImageUrlSource.fromString( + urlPrefix + "/" + eye + "/{z}/{f}/{y}/{x}.jpg", + { cubeMapPreviewUrl: urlPrefix + "/" + eye + "/preview.jpg" }); -// Create stage and register renderers. -var stage = new Marzipano.WebGlStage(); -Marzipano.registerDefaultRenderers(stage); + var textureStore = new Marzipano.TextureStore(source, stage); + var layer = new Marzipano.Layer(source, geometry, view, textureStore, + { effects: { rect: rect }}); -// Insert stage into the DOM. -viewerElement.appendChild(stage.domElement()); + layer.pinFirstLevel(); -// Update the stage size whenever the window is resized. -function updateSize() { - stage.setSize({ - width: viewerElement.clientWidth, - height: viewerElement.clientHeight - }); -} -updateSize(); -window.addEventListener('resize', updateSize); - -// Create geometry. -var geometry = new Marzipano.CubeGeometry([ - { tileSize: 256, size: 256, fallbackOnly: true }, - { tileSize: 512, size: 512 }, - { tileSize: 512, size: 1024 }, - { tileSize: 512, size: 2048 }, - { tileSize: 512, size: 4096 } -]); - -// Create view. -var limiter = Marzipano.RectilinearView.limit.traditional(4096, 110*Math.PI/180); -var viewLeft = new WebVrView(); -var viewRight = new WebVrView(); - -// Create layers. -var layerLeft = createLayer(stage, viewLeft, geometry, 'left', - { relativeWidth: 0.5, relativeX: 0 }); -var layerRight = createLayer(stage, viewRight, geometry, 'right', - { relativeWidth: 0.5, relativeX: 0.5 }); - -// Add layers into stage. -stage.addLayer(layerLeft); -stage.addLayer(layerRight); - -// Check for an available VR device and initialize accordingly. -var vrDisplay = null; -navigator.getVRDisplays().then(function(vrDisplays) { - if (vrDisplays.length > 0) { - vrDisplay = vrDisplays[0]; - vrDisplay.requestAnimationFrame(render); - } - enterVrElement.style.display = vrDisplay ? 'block' : 'none'; - noVrElement.style.display = vrDisplay ? 'none' : 'block'; -}); - -// Enter WebVR mode when the button is clicked. -enterVrElement.addEventListener('click', function() { - vrDisplay.requestPresent([{source: stage.domElement()}]); -}); - -var proj = mat4.create(); -var pose = mat4.create(); - -function render() { - var frameData = new VRFrameData; - vrDisplay.getFrameData(frameData); - - // Update the view. - // The panorama demo at https://github.com/toji/webvr.info/tree/master/samples - // recommends computing the view matrix from `frameData.pose.orientation` - // instead of using `frameData.{left,right}ViewMatrix. - if (frameData.pose.orientation) { - mat4.fromQuat(pose, frameData.pose.orientation); - mat4.invert(pose, pose); - - mat4.copy(proj, frameData.leftProjectionMatrix); - mat4.multiply(proj, proj, pose); - viewLeft.setProjection(proj); - - mat4.copy(proj, frameData.rightProjectionMatrix); - mat4.multiply(proj, proj, pose); - viewRight.setProjection(proj); + return layer; } - - // Render and submit to WebVR display. - stage.render(); - vrDisplay.submitFrame(); - - // Render again on the next frame. - vrDisplay.requestAnimationFrame(render); } -function createLayer(stage, view, geometry, eye, rect) { - var urlPrefix = "//www.marzipano.net/media/music-room"; - var source = new Marzipano.ImageUrlSource.fromString( - urlPrefix + "/" + eye + "/{z}/{f}/{y}/{x}.jpg", - { cubeMapPreviewUrl: urlPrefix + "/" + eye + "/preview.jpg" }); - - var textureStore = new Marzipano.TextureStore(source, stage); - var layer = new Marzipano.Layer(source, geometry, view, textureStore, - { effects: { rect: rect }}); - - layer.pinFirstLevel(); - - return layer; -} +main(); \ No newline at end of file