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
*/