diff --git a/examples/al-vr.html b/examples/al-vr.html new file mode 100644 index 000000000..3e333e2de --- /dev/null +++ b/examples/al-vr.html @@ -0,0 +1,113 @@ + + + + + + + +
+ + + + + diff --git a/src/core/al-core/src/object/vertex_array_object.rs b/src/core/al-core/src/object/vertex_array_object.rs index e34f85950..6e52432a6 100644 --- a/src/core/al-core/src/object/vertex_array_object.rs +++ b/src/core/al-core/src/object/vertex_array_object.rs @@ -9,8 +9,8 @@ pub mod vao { use crate::object::element_array_buffer::ElementArrayBuffer; use crate::webgl_ctx::WebGlContext; - use std::collections::HashMap; use crate::Abort; + use std::collections::HashMap; pub struct VertexArrayObject { array_buffer: HashMap<&'static str, ArrayBuffer>, @@ -88,7 +88,10 @@ pub mod vao { }*/ pub fn num_elements(&self) -> usize { - self.element_array_buffer.as_ref().unwrap_abort().num_elements() + self.element_array_buffer + .as_ref() + .unwrap_abort() + .num_elements() } pub fn num_instances(&self) -> i32 { @@ -155,6 +158,7 @@ pub mod vao { pub fn unbind(&self) { self.vao.gl.bind_vertex_array(None); + self._shader.unbind(&self.vao.gl); } } @@ -170,8 +174,9 @@ pub mod vao { } impl<'a, 'b> ShaderVertexArrayObjectBoundRef<'a, 'b> { - pub fn draw_arrays(&self, mode: u32, byte_offset: i32, size: i32) { + pub fn draw_arrays(&self, mode: u32, byte_offset: i32, size: i32) -> &Self { self.vao.gl.draw_arrays(mode, byte_offset, size); + self } pub fn draw_elements_with_i32( @@ -180,11 +185,12 @@ pub mod vao { num_elements: Option, type_: u32, byte_offset: i32, - ) { + ) -> &Self { let num_elements = num_elements.unwrap_or(self.vao.num_elements() as i32); self.vao .gl .draw_elements_with_i32(mode, num_elements, type_, byte_offset); + self } pub fn draw_elements_instanced_with_i32( @@ -192,7 +198,7 @@ pub mod vao { mode: u32, offset_element_idx: i32, num_instances: i32, - ) { + ) -> &Self { self.vao.gl.draw_elements_instanced_with_i32( mode, self.vao.num_elements() as i32, @@ -200,10 +206,12 @@ pub mod vao { offset_element_idx, num_instances, ); + self } pub fn unbind(&self) { self.vao.gl.bind_vertex_array(None); + self._shader.unbind(&self.vao.gl); } } @@ -444,7 +452,10 @@ pub mod vao { }*/ pub fn num_elements(&self) -> usize { - self.element_array_buffer.as_ref().unwrap_abort().num_elements() + self.element_array_buffer + .as_ref() + .unwrap_abort() + .num_elements() } pub fn num_instances(&self) -> i32 { @@ -511,7 +522,8 @@ pub mod vao { } pub fn unbind(&self) { - //self.vao.gl.bind_vertex_array(None); + self.vao.gl.bind_vertex_array(None); + self._shader.unbind(&self.vao.gl); } } @@ -528,13 +540,15 @@ pub mod vao { } use crate::object::array_buffer::VertexBufferObject; impl<'a, 'b> ShaderVertexArrayObjectBoundRef<'a, 'b> { - pub fn draw_arrays(&self, mode: u32, byte_offset: i32, size: i32) { + pub fn draw_arrays(&self, mode: u32, byte_offset: i32, size: i32) -> &Self { for (attr, buf) in self.vao.array_buffer.iter() { buf.bind(); buf.set_vertex_attrib_pointer_by_name::(self.shader, attr); } self.vao.gl.draw_arrays(mode, byte_offset, size); + + self } pub fn draw_elements_with_i32( @@ -543,7 +557,7 @@ pub mod vao { num_elements: Option, type_: u32, byte_offset: i32, - ) { + ) -> &Self { for (attr, buf) in self.vao.array_buffer.iter() { buf.bind(); buf.set_vertex_attrib_pointer_by_name::(self.shader, attr); @@ -555,6 +569,7 @@ pub mod vao { self.vao .gl .draw_elements_with_i32(mode, num_elements, type_, byte_offset); + self } pub fn draw_elements_instanced_with_i32( @@ -562,7 +577,7 @@ pub mod vao { mode: u32, offset_element_idx: i32, num_instances: i32, - ) { + ) -> &Self { for (attr, buf) in self.vao.array_buffer.iter() { buf.bind(); buf.set_vertex_attrib_pointer_by_name::(self.shader, attr); @@ -587,10 +602,12 @@ pub mod vao { offset_element_idx, num_instances, ); + self } pub fn unbind(&self) { - //self.vao.gl.bind_vertex_array(None); + self.vao.gl.bind_vertex_array(None); + self.shader.unbind(&self.vao.gl); } } @@ -716,6 +733,9 @@ pub mod vao { pub fn unbind(&self) { //self.vao.gl.bind_vertex_array(None); + + self.vao.gl.bind_vertex_array(None); + self.shader.unbind(&self.vao.gl); } } diff --git a/src/core/src/app.rs b/src/core/src/app.rs index 5dba46155..4a457bbf1 100644 --- a/src/core/src/app.rs +++ b/src/core/src/app.rs @@ -875,7 +875,7 @@ impl App { &self.colormaps, &self.projection, )?; - + /* // Draw the catalog //let fbo_view = &self.fbo_view; //catalogs.draw(&gl, shaders, camera, colormaps, fbo_view)?; @@ -903,7 +903,7 @@ impl App { self.line_renderer.draw(&self.camera)?; //let dpi = self.camera.get_dpi(); //ui.draw(&gl, dpi)?; - + */ // Reset the flags about the user action self.camera.reset(); diff --git a/src/core/src/renderable/hips/mod.rs b/src/core/src/renderable/hips/mod.rs index d8fb7e6b3..bc3a857a0 100644 --- a/src/core/src/renderable/hips/mod.rs +++ b/src/core/src/renderable/hips/mod.rs @@ -1047,7 +1047,8 @@ impl HiPS { Some(self.num_idx as i32), WebGl2RenderingContext::UNSIGNED_SHORT, 0, - ); + ) + .unbind(); } Ok(()) diff --git a/src/core/src/renderable/hips/raytracing.rs b/src/core/src/renderable/hips/raytracing.rs index 396648923..d52e1c8ec 100644 --- a/src/core/src/renderable/hips/raytracing.rs +++ b/src/core/src/renderable/hips/raytracing.rs @@ -225,7 +225,8 @@ impl RayTracer { None, WebGl2RenderingContext::UNSIGNED_SHORT, 0, - ); + ) + .unbind(); #[cfg(feature = "webgl2")] shader .attach_uniform("position_tex", &self.position_tex) @@ -236,6 +237,7 @@ impl RayTracer { WebGl2RenderingContext::UNSIGNED_SHORT, 0, ) + .unbind(); } pub fn is_rendering(&self, camera: &CameraViewPort) -> bool { diff --git a/src/core/src/renderable/image/mod.rs b/src/core/src/renderable/image/mod.rs index 8376da226..0e02bea0c 100644 --- a/src/core/src/renderable/image/mod.rs +++ b/src/core/src/renderable/image/mod.rs @@ -612,7 +612,8 @@ impl Image { Some(num_indices), WebGl2RenderingContext::UNSIGNED_SHORT, ((off_indices as usize) * std::mem::size_of::()) as i32, - ); + ) + .unbind(); off_indices += self.num_indices[idx]; } diff --git a/src/core/src/renderable/mod.rs b/src/core/src/renderable/mod.rs index a345a6511..0c1a2ab6d 100644 --- a/src/core/src/renderable/mod.rs +++ b/src/core/src/renderable/mod.rs @@ -259,7 +259,8 @@ impl Layers { None, WebGl2RenderingContext::UNSIGNED_SHORT, 0, - ); + ) + .unbind(); } // The first layer must be paint independently of its alpha channel diff --git a/src/js/Aladin.js b/src/js/Aladin.js index b97af9720..04ee8b69d 100644 --- a/src/js/Aladin.js +++ b/src/js/Aladin.js @@ -50,6 +50,7 @@ import { ContextMenu } from "./gui/ContextMenu.js"; import { ALEvent } from "./events/ALEvent.js"; import { Color } from './Color.js'; import { ImageFITS } from "./ImageFITS.js"; +import { VRButton } from "./VRButton.js"; import { DefaultActionsForContextMenu } from "./DefaultActionsForContextMenu.js"; import A from "./A.js"; @@ -458,7 +459,8 @@ export let Aladin = (function () { //this.discoverytree = new DiscoveryTree(this); //} - this.view.redraw(); + // [ ] That might pose problems + //this.view.redraw(); // go to full screen ? if (options.fullScreen) { @@ -471,6 +473,11 @@ export let Aladin = (function () { this.contextMenu = new ContextMenu(this); this.contextMenu.attachTo(this.view.catalogCanvas, DefaultActionsForContextMenu.getDefaultActions(this)); } + + // initialize the VR button + if (options.vr) { + this.aladinDiv.appendChild(VRButton.createButton(this.view)); + } }; /**** CONSTANTS ****/ @@ -671,6 +678,11 @@ export let Aladin = (function () { }); }; + // @API + Aladin.prototype.setRenderer = function(renderer) { + this.options.vr.renderer = renderer; + } + Aladin.prototype.setFrame = function (frameName) { if (!frameName) { return; diff --git a/src/js/VRButton.js b/src/js/VRButton.js new file mode 100644 index 000000000..968962969 --- /dev/null +++ b/src/js/VRButton.js @@ -0,0 +1,252 @@ +/** + * This is an adaptation of the original VRButton. + * Original at: + * https://github.com/mrdoob/three.js/blob/dev/examples/jsm/webxr/VRButton.js + */ + +/** + * VRButton class that handles the creation of a VR session + * + * @class VRButton + */ +class VRButton { + /** + * Constructs a VRButton + * + * @static + * @param {View} view - The aladin view + * @return {HTMLButtonElement|HTMLAnchorElement} The VR mode button or an + * error message + */ + static createButton(view) { + const button = document.createElement('button'); + + /** + * Function for handling the process of entering VR mode. + */ + function showEnterVR(/* device*/) { + let currentSession = null; + + /** + * Callback function to handle when the XR session is started + * + * @param {XRSession} session - The XR session that has been started + */ + async function onSessionStarted(session) { + session.addEventListener('end', onSessionEnded); + + let gl = view.imageCanvas.getContext('webgl2'); + await gl.makeXRCompatible(); + + session.updateRenderState({ + baseLayer: new XRWebGLLayer(session, gl) + }); + + await view.options.vr.renderer.xr.setSession(session); + button.textContent = 'EXIT VR'; + + // view.options.vr.renderer.setAnimationLoop(view.redrawVR.bind(view)); + + session.requestReferenceSpace('local-floor').then((refSpace) => { + const xrRefSpace = refSpace; + session.requestAnimationFrame((t, frame) => {view.redrawVR(t, frame, xrRefSpace)}); + }); + + currentSession = session; + } + + /** + * Function to render the whole scene + */ + // NOTE A supprimer + function onXRAnimationFrame(t, xrFrame) { + currentSession.requestAnimationFrame(onXRAnimationFrame); + view.redrawVR(); + } + + /** + * Callback function to handle when the XR session ends + */ + function onSessionEnded(/* event*/) { + currentSession.removeEventListener('end', onSessionEnded); + + button.textContent = 'ENTER VR'; + + currentSession = null; + } + + // + + button.style.display = ''; + + button.style.cursor = 'pointer'; + button.style.left = 'calc(50% - 50px)'; + button.style.width = '100px'; + + button.textContent = 'ENTER VR'; + + button.onmouseenter = function() { + button.style.opacity = '1.0'; + }; + + button.onmouseleave = function() { + button.style.opacity = '0.5'; + }; + + button.onclick = function() { + if (currentSession === null) { + // WebXR's requestReferenceSpace only works if the corresponding + // feature was requested at session creation time. For simplicity, + // just ask for the interesting ones as optional features, but be + // aware that the requestReferenceSpace call will fail if it turns + // out to be unavailable. + // ('local' is always available for immersive sessions and doesn't + // need to be requested separately.) + + const sessionInit = {optionalFeatures: ['local-floor']}; + navigator.xr.requestSession( + 'immersive-vr', sessionInit).then(onSessionStarted); + } else { + currentSession.end(); + } + }; + } + + /** + * Function for disabling the VR mode button + * + * @param {HTMLButtonElement} button - The VR mode button element to + * be disabled + */ + function disableButton() { + button.style.display = ''; + + button.style.cursor = 'auto'; + button.style.left = 'calc(50% - 75px)'; + button.style.width = '150px'; + + button.onmouseenter = null; + button.onmouseleave = null; + + button.onclick = null; + } + + /** + * Function for handling the case where WebXR is not supported + * + * @description This function disables the VR mode button and displays a + * message indicating that VR is not supported + * + * @param {HTMLButtonElement} button - The VR mode button element to be + * disabled and updated with a message + */ + function showWebXRNotFound() { + disableButton(); + + button.textContent = 'VR NOT SUPPORTED'; + } + + /** + * Function for handling the case where VR is not allowed due to an + * exception + * + * @description This function disables the VR mode button, logs an + * exception to the console, and displays a message indicating that VR + * is not allowed + * + * @param {any} exception - The exception object or error that indicates + * why VR is not allowed + * @param {HTMLButtonElement} button - The VR mode button element to be + * disabled and updated with a message + */ + function showVRNotAllowed(exception) { + disableButton(); + + console.warn('Exception when trying to call xr.isSessionSupported', + exception); + + button.textContent = 'VR NOT ALLOWED'; + } + + /** + * Function for styling an HTML element with specific CSS properties + * + * @param {HTMLElement} element - The HTML element to be styled + */ + function stylizeElement(element) { + element.style.position = 'absolute'; + element.style.bottom = '20px'; + element.style.padding = '12px 6px'; + element.style.border = '1px solid #fff'; + element.style.borderRadius = '4px'; + element.style.background = 'rgba(0,0,0,0.1)'; + element.style.color = '#fff'; + element.style.font = 'normal 13px sans-serif'; + element.style.textAlign = 'center'; + element.style.opacity = '0.5'; + element.style.outline = 'none'; + element.style.zIndex = '999'; + } + + if ('xr' in navigator) { + button.id = 'VRButton'; + button.style.display = 'none'; + + stylizeElement(button); + + navigator.xr.isSessionSupported('immersive-vr').then(function(supported) { + supported ? showEnterVR() : showWebXRNotFound(); + + if (supported && VRButton.xrSessionIsGranted) { + button.click(); + } + }).catch(showVRNotAllowed); + + return button; + } else { + const message = document.createElement('a'); + + if (window.isSecureContext === false) { + message.href = document.location.href.replace(/^http:/, 'https:'); + message.innerHTML = 'WEBXR NEEDS HTTPS'; + } else { + message.href = 'https://immersiveweb.dev/'; + message.innerHTML = 'WEBXR NOT AVAILABLE'; + } + + message.style.left = 'calc(50% - 90px)'; + message.style.width = '180px'; + message.style.textDecoration = 'none'; + + stylizeElement(message); + + return message; + } + } + + /** + * Registers a listener for the "sessiongranted" event to track the XR + * session being granted. + * + * @description This method checks if the WebXR API is available and + * registers a listener for the "sessiongranted" event to track when an + * XR session is granted. It sets the `VRButton.xrSessionIsGranted` + * property to `true` when the event is triggered. + */ + static registerSessionGrantedListener() { + if ('xr' in navigator) { + // WebXRViewer (based on Firefox) has a bug where addEventListener + // throws a silent exception and aborts execution entirely. + if (/WebXRViewer\//i.test(navigator.userAgent)) return; + + navigator.xr.addEventListener('sessiongranted', () => { + VRButton.xrSessionIsGranted = true; + }); + } + } +} + +VRButton.xrSessionIsGranted = false; +VRButton.registerSessionGrantedListener(); + +export {VRButton}; diff --git a/src/js/View.js b/src/js/View.js index be7d0b4a5..8d1f14544 100644 --- a/src/js/View.js +++ b/src/js/View.js @@ -370,7 +370,7 @@ export let View = (function () { } this.computeNorder(); - this.redraw(); + //this.redraw(); }; var pixelateCanvasContext = function (ctx, pixelateFlag) { @@ -1059,6 +1059,41 @@ export let View = (function () { View.FPS_INTERVAL = 1000 / 140; + + View.prototype.redrawVR = function (t, frame, xrRefSpace) { + const session = frame.session; + session.requestAnimationFrame((t, frame) => {this.redrawVR(t, frame, xrRefSpace)}); + + let pose = frame.getViewerPose(xrRefSpace); + + if (!pose) return; + + // Elapsed time since last loop + const now = Date.now(); + const elapsedTime = now - this.then; + + // If enough time has elapsed, draw the next frame + //if (elapsedTime >= View.FPS_INTERVAL) { + // Get ready for next frame by setting then=now, but also adjust for your + // specified fpsInterval not being a multiple of RAF's interval (16.7ms) + + // Drawing code + try { + this.moving = this.wasm.update(elapsedTime); + } catch (e) { + console.warn(e) + } + + ////// 2. Draw catalogues//////// + const isViewRendering = this.wasm.isRendering(); + if (isViewRendering || this.needRedraw) { + this.drawAllOverlays(); + } + this.needRedraw = false; + + this.options.vr.animation(); + } + /** * redraw the whole view */