From cea1d58a156f552fce936c886037fb58e9515ba1 Mon Sep 17 00:00:00 2001 From: awtterpip Date: Tue, 13 Feb 2024 16:06:30 -0600 Subject: [PATCH] more webxr support --- Cargo.toml | 7 +- src/main.rs => examples/3d_scene.rs | 8 +- src/actions.rs | 142 +++++++++++ src/actions/oculus_touch.rs | 238 ++++++++++++++++++ src/lib.rs | 370 +--------------------------- src/types.rs | 8 + src/webxr.rs | 175 +++++++++++++ xr_api/Cargo.toml | 2 +- xr_api/src/api.rs | 5 +- xr_api/src/api_traits.rs | 6 +- xr_api/src/backend/oxr.rs | 8 +- xr_api/src/backend/webxr.rs | 207 ---------------- xr_api/src/types.rs | 2 + 13 files changed, 588 insertions(+), 590 deletions(-) rename src/main.rs => examples/3d_scene.rs (86%) create mode 100644 src/actions.rs create mode 100644 src/actions/oculus_touch.rs create mode 100644 src/types.rs create mode 100644 src/webxr.rs diff --git a/Cargo.toml b/Cargo.toml index a7f274b..a9c6f9b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,12 +12,12 @@ linked = ["openxr/linked"] vulkan = ["dep:ash"] [dependencies] -xr_api = { path = "./xr_api" } async-std = "1.12.0" bevy = "0.12.1" paste = "1.0.14" wgpu = "0.17.1" wgpu-hal = "0.17.1" +winit = "0.28.7" [target.'cfg(target_family = "unix")'.dependencies] openxr = { version = "0.17.1", features = ["mint"] } @@ -29,8 +29,9 @@ openxr = { version = "0.17.1", features = ["mint", "static"] } ash = { version = "0.37.3", optional = true } [target.'cfg(target_family = "wasm")'.dependencies] -wasm-bindgen = "0.2.87" -web-sys = { version = "0.3.61", features = [ +js-sys = "0.3" +wasm-bindgen = "0.2.91" +web-sys = { version = "0.3.68", features = [ # STANDARD 'console', 'Document', diff --git a/src/main.rs b/examples/3d_scene.rs similarity index 86% rename from src/main.rs rename to examples/3d_scene.rs index ccbc79f..dbf1602 100644 --- a/src/main.rs +++ b/examples/3d_scene.rs @@ -1,13 +1,11 @@ //! A simple 3D scene with light shining over a cube sitting on a plane. -use bevy::{ - core_pipeline::clear_color::ClearColorConfig, prelude::*, render::camera::RenderTarget, -}; -use bevy_oxr::{DefaultXrPlugins, LEFT_XR_TEXTURE_HANDLE}; +use bevy::prelude::*; +use bevy_oxr::webxr::XrInitPlugin; fn main() { App::new() - .add_plugins(DefaultXrPlugins) + .add_plugins((DefaultPlugins, XrInitPlugin)) .add_systems(Startup, setup) .run(); } diff --git a/src/actions.rs b/src/actions.rs new file mode 100644 index 0000000..27bacde --- /dev/null +++ b/src/actions.rs @@ -0,0 +1,142 @@ +pub mod oculus_touch; + +mod private { + use bevy::math::Vec2; + + use crate::types::{Haptic, Pose}; + + pub trait Sealed {} + + impl Sealed for bool {} + impl Sealed for f32 {} + impl Sealed for Vec2 {} + impl Sealed for Pose {} + impl Sealed for Haptic {} +} + +use std::borrow::Cow; +use std::marker::PhantomData; + +pub trait ActionType: private::Sealed {} + +impl ActionType for T {} + +pub trait ActionPathTrait { + type PathType: ActionType; + fn path(&self) -> Cow<'_, str>; + fn name(&self) -> Cow<'_, str>; +} + +pub struct ActionPath { + path: &'static str, + name: &'static str, + _marker: PhantomData, +} + +impl ActionPathTrait for ActionPath { + type PathType = T; + + fn path(&self) -> Cow<'_, str> { + self.path.into() + } + + fn name(&self) -> Cow<'_, str> { + self.name.into() + } +} + +macro_rules! actions { + // create path struct + ( + $($subpath:literal),* + $id:ident { + path: $path:literal; + } + ) => {}; + + // handle action path attrs + ( + $($subpath:literal),* + $id:ident { + path: $path:literal; + name: $name:literal; + path_type: $path_type:ty; + } + ) => { + paste::paste! { + pub const [<$id:snake:upper>]: crate::actions::ActionPath<$path_type> = crate::actions::ActionPath { + path: concat!($($subpath,)* $path), + name: $name, + _marker: std::marker::PhantomData, + }; + } + }; + + // handle action path attrs + ( + $($subpath:literal),* + $id:ident { + path: $path:literal; + name: $name:literal; + path_type: $path_type:ty; + $($children:tt)* + } + ) => { + crate::path::actions! { + $($subpath),* + $id { + path: $path; + name: $name; + path_type: $path_type; + } + } + + crate::path::actions! { + $($subpath),* + $id { + path: $path; + $($children)* + } + } + }; + + // handle children + ( + $($subpath:literal),* + $id:ident { + path: $path:literal; + $($children:tt)* + } + ) => { + pub mod $id { + crate::actions::actions! { + $($subpath,)* $path + $($children)* + } + } + }; + + // handle siblings + ( + $($subpath:literal),* + $id:ident { + path: $path:literal; + $($attrs:tt)* + } + $($siblings:tt)* + ) => { + crate::actions::actions! { + $($subpath),* + $id { + path: $path; + $($attrs)* + } + } + crate::actions::actions! { + $($subpath),* + $($siblings)* + } + }; +} + +pub(crate) use actions; diff --git a/src/actions/oculus_touch.rs b/src/actions/oculus_touch.rs new file mode 100644 index 0000000..895c2ef --- /dev/null +++ b/src/actions/oculus_touch.rs @@ -0,0 +1,238 @@ +super::actions! { + "/user" + hand { + path: "/hand"; + left { + path: "/left"; + input { + path: "/input"; + x { + path: "/x"; + click { + path: "/click"; + name: "x_click"; + path_type: bool; + } + touch { + path: "/touch"; + name: "x_touch"; + path_type: bool; + } + } + y { + path: "/y"; + click { + path: "/click"; + name: "y_click"; + path_type: bool; + } + touch { + path: "/touch"; + name: "y_touch"; + path_type: bool; + } + } + menu { + path: "/menu"; + click { + path: "/click"; + name: "menu_click"; + path_type: bool; + } + } + squeeze { + path: "/squeeze"; + value { + path: "/value"; + name: "left_grip_val"; + path_type: f32; + } + } + trigger { + path: "/trigger"; + value { + path: "/value"; + name: "left_trigger_val"; + path_type: f32; + } + touch { + path: "/touch"; + name: "left_trigger_touch"; + path_type: bool; + } + } + thumbstick { + path: "/thumbstick"; + x { + path: "/x"; + name: "left_thumbstick_x"; + path_type: f32; + } + y { + path: "/y"; + name: "left_thumbstick_y"; + path_type: f32; + } + click { + path: "/click"; + name: "left_thumbstick_click"; + path_type: bool; + } + touch { + path: "/touch"; + name: "left_thumbstick_touch"; + path_type: bool; + } + } + thumbrest { + path: "/thumbrest"; + touch { + path: "/touch"; + name: "left_thumbrest_touch"; + path_type: bool; + } + } + grip { + path: "/grip"; + pose { + path: "/pose"; + name: "left_grip_pose"; + path_type: crate::types::Pose; + } + } + aim { + path: "/aim"; + pose { + path: "/pose"; + name: "left_aim_pose"; + path_type: crate::types::Pose; + } + } + } + output { + path: "/output"; + haptic { + path: "/haptic"; + name: "left_controller_haptic"; + path_type: crate::types::Haptic; + } + } + } + right { + path: "/right"; + input { + path: "/input"; + a { + path: "/a"; + click { + path: "/click"; + name: "a_click"; + path_type: bool; + } + touch { + path: "/touch"; + name: "a_touch"; + path_type: bool; + } + } + b { + path: "/b"; + click { + path: "/click"; + name: "b_click"; + path_type: bool; + } + touch { + path: "/touch"; + name: "b_touch"; + path_type: bool; + } + } + system { + path: "/system"; + click { + path: "/click"; + name: "system_click"; + path_type: bool; + } + } + squeeze { + path: "/squeeze"; + value { + path: "/value"; + name: "right_grip_val"; + path_type: f32; + } + } + trigger { + path: "/trigger"; + value { + path: "/value"; + name: "right_trigger_val"; + path_type: f32; + } + touch { + path: "/touch"; + name: "right_trigger_touch"; + path_type: bool; + } + } + thumbstick { + path: "/thumbstick"; + x { + path: "/x"; + name: "right_thumbstick_x"; + path_type: f32; + } + y { + path: "/y"; + name: "right_thumbstick_y"; + path_type: f32; + } + click { + path: "/click"; + name: "right_thumbstick_click"; + path_type: bool; + } + touch { + path: "/touch"; + name: "right_thumbstick_touch"; + path_type: bool; + } + } + thumbrest { + path: "/thumbrest"; + touch { + path: "/touch"; + name: "right_thumbrest_touch"; + path_type: bool; + } + } + grip { + path: "/grip"; + pose { + path: "/pose"; + name: "right_grip_pose"; + path_type: crate::types::Pose; + } + } + aim { + path: "/aim"; + pose { + path: "/pose"; + name: "right_aim_pose"; + path_type: crate::types::Pose; + } + } + } + output { + path: "/output"; + haptic { + path: "/haptic"; + name: "right_controller_haptic"; + path_type: crate::types::Haptic; + } + } + } + } +} diff --git a/src/lib.rs b/src/lib.rs index dac2401..b00ee2a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,366 +1,4 @@ -use std::sync::Arc; - -use bevy::{ - app::PluginGroupBuilder, - core_pipeline::tonemapping::{DebandDither, Tonemapping}, - math::Vec3A, - prelude::*, - render::{ - camera::{ - CameraProjection, CameraProjectionPlugin, CameraRenderGraph, ManualTextureView, - ManualTextureViewHandle, ManualTextureViews, RenderTarget, - }, - pipelined_rendering::PipelinedRenderingPlugin, - primitives::Frustum, - renderer::{render_system, RenderAdapter, RenderAdapterInfo, RenderInstance, RenderQueue}, - view::{ColorGrading, VisibleEntities}, - Render, RenderApp, RenderPlugin, - }, - window::PresentMode, -}; -use xr_api::prelude::*; - -pub const LEFT_XR_TEXTURE_HANDLE: ManualTextureViewHandle = ManualTextureViewHandle(1208214591); -pub const RIGHT_XR_TEXTURE_HANDLE: ManualTextureViewHandle = ManualTextureViewHandle(3383858418); - -pub struct XrPlugin; - -impl Plugin for XrPlugin { - fn build(&self, app: &mut App) { - let instance = Entry::new() - .create_instance(ExtensionSet { vulkan: true }) - .unwrap(); - let session = instance - .create_session(SessionCreateInfo { - texture_format: wgpu::TextureFormat::Rgba8UnormSrgb, - }) - .unwrap(); - - let (device, queue, adapter_info, adapter, instance) = - session.get_render_resources().unwrap(); - - let input = session.create_input(Bindings::OculusTouch).unwrap(); - - let left_primary_button = input - .create_action(input::hand_left::PrimaryButton::CLICK) - .unwrap(); - - let left_hand_pose = input.create_action(input::hand_left::Grip::POSE).unwrap(); - - app.insert_non_send_resource(left_primary_button); - app.insert_non_send_resource(left_hand_pose); - app.insert_non_send_resource(session.clone()); - app.add_plugins(( - RenderPlugin { - render_creation: bevy::render::settings::RenderCreation::Manual( - device.into(), - RenderQueue(Arc::new(queue)), - RenderAdapterInfo(adapter_info), - RenderAdapter(Arc::new(adapter)), - RenderInstance(Arc::new(instance)), - ), - }, - CameraProjectionPlugin::::default(), - )); - - app.add_systems(PreUpdate, begin_frame); - app.add_systems(Last, locate_views); - app.add_systems(Startup, setup); - let render_app = app.sub_app_mut(RenderApp); - render_app.insert_non_send_resource(session); - render_app.add_systems(Render, end_frame.after(render_system)); - } -} - -#[derive(Bundle)] -pub struct XrCameraBundle { - pub camera: Camera, - pub camera_render_graph: CameraRenderGraph, - pub xr_projection: PerspectiveProjection, - pub visible_entities: VisibleEntities, - pub frustum: Frustum, - pub transform: Transform, - pub global_transform: GlobalTransform, - pub camera_3d: Camera3d, - pub tonemapping: Tonemapping, - pub dither: DebandDither, - pub color_grading: ColorGrading, - pub xr_camera_type: XrCameraType, -} -#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, Ord, PartialOrd, Component)] -pub enum XrCameraType { - Xr(Eye), - Flatscreen, -} - -#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, Ord, PartialOrd)] -pub enum Eye { - Left = 0, - Right = 1, -} - -impl XrCameraBundle { - pub fn new(eye: Eye) -> Self { - Self { - camera: Camera { - order: -1, - target: RenderTarget::TextureView(match eye { - Eye::Left => LEFT_XR_TEXTURE_HANDLE, - Eye::Right => RIGHT_XR_TEXTURE_HANDLE, - }), - viewport: None, - ..default() - }, - camera_render_graph: CameraRenderGraph::new(bevy::core_pipeline::core_3d::graph::NAME), - xr_projection: Default::default(), - visible_entities: Default::default(), - frustum: Default::default(), - transform: Default::default(), - global_transform: Default::default(), - camera_3d: Default::default(), - tonemapping: Default::default(), - dither: DebandDither::Enabled, - color_grading: Default::default(), - xr_camera_type: XrCameraType::Xr(eye), - } - } -} - -#[derive(Debug, Clone, Component, Reflect)] -#[reflect(Component, Default)] -pub struct XRProjection { - pub near: f32, - pub far: f32, - #[reflect(ignore)] - pub fov: Fov, -} - -impl Default for XRProjection { - fn default() -> Self { - Self { - near: 0.1, - far: 1000., - fov: Default::default(), - } - } -} - -impl CameraProjection for XRProjection { - // ============================================================================= - // math code adapted from - // https://github.com/KhronosGroup/OpenXR-SDK-Source/blob/master/src/common/xr_linear.h - // Copyright (c) 2017 The Khronos Group Inc. - // Copyright (c) 2016 Oculus VR, LLC. - // SPDX-License-Identifier: Apache-2.0 - // ============================================================================= - fn get_projection_matrix(&self) -> Mat4 { - // symmetric perspective for debugging - // let x_fov = (self.fov.angle_left.abs() + self.fov.angle_right.abs()); - // let y_fov = (self.fov.angle_up.abs() + self.fov.angle_down.abs()); - // return Mat4::perspective_infinite_reverse_rh(y_fov, x_fov / y_fov, self.near); - - let fov = self.fov; - let is_vulkan_api = false; // FIXME wgpu probably abstracts this - let near_z = self.near; - let far_z = -1.; // use infinite proj - // let far_z = self.far; - - let tan_angle_left = fov.angle_left.tan(); - let tan_angle_right = fov.angle_right.tan(); - - let tan_angle_down = fov.angle_down.tan(); - let tan_angle_up = fov.angle_up.tan(); - - let tan_angle_width = tan_angle_right - tan_angle_left; - - // Set to tanAngleDown - tanAngleUp for a clip space with positive Y - // down (Vulkan). Set to tanAngleUp - tanAngleDown for a clip space with - // positive Y up (OpenGL / D3D / Metal). - // const float tanAngleHeight = - // graphicsApi == GRAPHICS_VULKAN ? (tanAngleDown - tanAngleUp) : (tanAngleUp - tanAngleDown); - let tan_angle_height = if is_vulkan_api { - tan_angle_down - tan_angle_up - } else { - tan_angle_up - tan_angle_down - }; - - // Set to nearZ for a [-1,1] Z clip space (OpenGL / OpenGL ES). - // Set to zero for a [0,1] Z clip space (Vulkan / D3D / Metal). - // const float offsetZ = - // (graphicsApi == GRAPHICS_OPENGL || graphicsApi == GRAPHICS_OPENGL_ES) ? nearZ : 0; - // FIXME handle enum of graphics apis - let offset_z = 0.; - - let mut cols: [f32; 16] = [0.0; 16]; - - if far_z <= near_z { - // place the far plane at infinity - cols[0] = 2. / tan_angle_width; - cols[4] = 0.; - cols[8] = (tan_angle_right + tan_angle_left) / tan_angle_width; - cols[12] = 0.; - - cols[1] = 0.; - cols[5] = 2. / tan_angle_height; - cols[9] = (tan_angle_up + tan_angle_down) / tan_angle_height; - cols[13] = 0.; - - cols[2] = 0.; - cols[6] = 0.; - cols[10] = -1.; - cols[14] = -(near_z + offset_z); - - cols[3] = 0.; - cols[7] = 0.; - cols[11] = -1.; - cols[15] = 0.; - - // bevy uses the _reverse_ infinite projection - // https://dev.theomader.com/depth-precision/ - let z_reversal = Mat4::from_cols_array_2d(&[ - [1f32, 0., 0., 0.], - [0., 1., 0., 0.], - [0., 0., -1., 0.], - [0., 0., 1., 1.], - ]); - - return z_reversal * Mat4::from_cols_array(&cols); - } else { - // normal projection - cols[0] = 2. / tan_angle_width; - cols[4] = 0.; - cols[8] = (tan_angle_right + tan_angle_left) / tan_angle_width; - cols[12] = 0.; - - cols[1] = 0.; - cols[5] = 2. / tan_angle_height; - cols[9] = (tan_angle_up + tan_angle_down) / tan_angle_height; - cols[13] = 0.; - - cols[2] = 0.; - cols[6] = 0.; - cols[10] = -(far_z + offset_z) / (far_z - near_z); - cols[14] = -(far_z * (near_z + offset_z)) / (far_z - near_z); - - cols[3] = 0.; - cols[7] = 0.; - cols[11] = -1.; - cols[15] = 0.; - } - - Mat4::from_cols_array(&cols) - } - - fn update(&mut self, _width: f32, _height: f32) {} - - fn far(&self) -> f32 { - self.far - } - - fn get_frustum_corners(&self, z_near: f32, z_far: f32) -> [Vec3A; 8] { - let tan_angle_left = self.fov.angle_left.tan(); - let tan_angle_right = self.fov.angle_right.tan(); - - let tan_angle_bottom = self.fov.angle_down.tan(); - let tan_angle_top = self.fov.angle_up.tan(); - - // NOTE: These vertices are in the specific order required by [`calculate_cascade`]. - [ - Vec3A::new(tan_angle_right, tan_angle_bottom, 1.0) * z_near, // bottom right - Vec3A::new(tan_angle_right, tan_angle_top, 1.0) * z_near, // top right - Vec3A::new(tan_angle_left, tan_angle_top, 1.0) * z_near, // top left - Vec3A::new(tan_angle_left, tan_angle_bottom, 1.0) * z_near, // bottom left - Vec3A::new(tan_angle_right, tan_angle_bottom, 1.0) * z_far, // bottom right - Vec3A::new(tan_angle_right, tan_angle_top, 1.0) * z_far, // top right - Vec3A::new(tan_angle_left, tan_angle_top, 1.0) * z_far, // top left - Vec3A::new(tan_angle_left, tan_angle_bottom, 1.0) * z_far, // bottom left - ] - } -} - -#[derive(Resource)] -struct Cameras(Entity, Entity); - -fn setup(mut commands: Commands) { - let left = commands.spawn(XrCameraBundle::new(Eye::Left)).id(); - let right = commands.spawn(XrCameraBundle::new(Eye::Right)).id(); - commands.insert_resource(Cameras(left, right)); -} - -pub fn begin_frame(session: NonSend, action: NonSend>) { - session.begin_frame().unwrap(); -} - -fn locate_views( - session: NonSend, - mut manual_texture_views: ResMut, - cameras: Res, - mut transforms: Query<(&mut Transform)>, -) { - let (left_view, right_view) = session.locate_views().unwrap(); - - let left = ManualTextureView { - texture_view: left_view.texture_view().unwrap().into(), - size: left_view.resolution(), - format: left_view.format(), - }; - let right = ManualTextureView { - texture_view: right_view.texture_view().unwrap().into(), - size: right_view.resolution(), - format: right_view.format(), - }; - - if let Ok(mut transform) = transforms.get_mut(cameras.0) { - let Pose { - translation, - rotation, - } = left_view.pose(); - - transform.translation = translation; - transform.rotation = rotation; - } - - if let Ok(mut transform) = transforms.get_mut(cameras.1) { - let Pose { - translation, - rotation, - } = right_view.pose(); - - transform.translation = translation; - transform.rotation = rotation; - } - - manual_texture_views.insert(RIGHT_XR_TEXTURE_HANDLE, right); - manual_texture_views.insert(LEFT_XR_TEXTURE_HANDLE, left); -} - -pub fn end_frame(session: NonSend) { - session.end_frame().unwrap(); -} - -pub struct DefaultXrPlugins; - -impl PluginGroup for DefaultXrPlugins { - fn build(self) -> PluginGroupBuilder { - DefaultPlugins - .build() - .disable::() - .disable::() - .add_before::(XrPlugin) - .set(WindowPlugin { - #[cfg(not(target_os = "android"))] - primary_window: Some(Window { - transparent: true, - present_mode: PresentMode::AutoNoVsync, - ..default() - }), - #[cfg(target_os = "android")] - primary_window: None, - #[cfg(target_os = "android")] - exit_condition: bevy::window::ExitCondition::DontExit, - #[cfg(target_os = "android")] - close_when_requested: true, - ..default() - }) - } -} +pub mod actions; +pub mod types; +#[cfg(target_family = "wasm")] +pub mod webxr; diff --git a/src/types.rs b/src/types.rs new file mode 100644 index 0000000..18cd965 --- /dev/null +++ b/src/types.rs @@ -0,0 +1,8 @@ +use bevy::math::{Quat, Vec3}; + +pub struct Pose { + pub translation: Vec3, + pub rotation: Quat, +} + +pub struct Haptic; diff --git a/src/webxr.rs b/src/webxr.rs new file mode 100644 index 0000000..72f4136 --- /dev/null +++ b/src/webxr.rs @@ -0,0 +1,175 @@ +use std::cell::RefCell; +use std::rc::Rc; +use std::sync::Mutex; +use std::time::Duration; + +use bevy::app::{App, Plugin, PluginsState}; +use bevy::ecs::entity::Entity; +use bevy::ecs::query::With; +use bevy::ecs::world::World; +use bevy::log::{error, info}; +use bevy::render::RenderApp; +use bevy::window::PrimaryWindow; +use bevy::winit::WinitWindows; +use js_sys::Object; +use wasm_bindgen::closure::Closure; +use wasm_bindgen::{JsCast, JsValue}; +use wasm_bindgen_futures::JsFuture; +use web_sys::{ + HtmlCanvasElement, WebGl2RenderingContext, XrFrame, XrReferenceSpace, XrReferenceSpaceType, + XrRenderStateInit, XrSession, XrSessionMode, XrWebGlLayer, +}; +use winit::platform::web::WindowExtWebSys; + +#[derive(Clone)] +struct FutureXrSession(Rc>>>); + +pub struct XrInitPlugin; + +impl Plugin for XrInitPlugin { + fn build(&self, app: &mut App) { + let canvas = get_canvas(&mut app.world).unwrap(); + let future_session = FutureXrSession(Default::default()); + app.set_runner(webxr_runner); + app.insert_non_send_resource(future_session.clone()); + bevy::tasks::IoTaskPool::get().spawn_local(async move { + let result = init_webxr( + canvas, + XrSessionMode::ImmersiveVr, + XrReferenceSpaceType::Local, + ) + .await; + *future_session.0.lock().unwrap() = Some(result); + }); + } + + fn ready(&self, app: &App) -> bool { + app.world + .get_non_send_resource::() + .and_then(|fxr| fxr.0.try_lock().map(|locked| locked.is_some()).ok()) + .unwrap_or(true) + } + + fn finish(&self, app: &mut App) { + info!("finishing"); + + if let Some(Ok((session, reference_space))) = app + .world + .remove_non_send_resource::() + .and_then(|fxr| fxr.0.lock().unwrap().take()) + { + app.insert_non_send_resource(session.clone()) + .insert_non_send_resource(reference_space.clone()); + app.sub_app_mut(RenderApp) + .insert_non_send_resource(session) + .insert_non_send_resource(reference_space); + } + } +} + +fn webxr_runner(mut app: App) { + fn set_timeout(f: &Closure, dur: Duration) { + web_sys::window() + .unwrap() + .set_timeout_with_callback_and_timeout_and_arguments_0( + f.as_ref().unchecked_ref(), + dur.as_millis() as i32, + ) + .expect("Should register `setTimeout`."); + } + let run_xr_inner = Rc::new(RefCell::new(None)); + let run_xr: Rc>>> = run_xr_inner.clone(); + *run_xr.borrow_mut() = Some(Closure::new(move || { + let app = &mut app; + if app.plugins_state() == PluginsState::Ready { + app.finish(); + app.cleanup(); + run_xr_app(std::mem::take(app)); + } else { + set_timeout( + run_xr_inner.borrow().as_ref().unwrap(), + Duration::from_millis(1), + ); + } + })); + set_timeout(run_xr.borrow().as_ref().unwrap(), Duration::from_millis(1)); +} + +fn run_xr_app(mut app: App) { + let session = app.world.non_send_resource::().clone(); + let inner_closure: Rc>>> = + Rc::new(RefCell::new(None)); + let closure = inner_closure.clone(); + *closure.borrow_mut() = Some(Closure::new(move |_time, frame: XrFrame| { + let session = frame.session(); + app.insert_non_send_resource(frame); + info!("update"); + app.update(); + session.request_animation_frame( + inner_closure + .borrow() + .as_ref() + .unwrap() + .as_ref() + .unchecked_ref(), + ); + })); + session.request_animation_frame(closure.borrow().as_ref().unwrap().as_ref().unchecked_ref()); +} + +fn get_canvas(world: &mut World) -> Option { + let window_entity = world + .query_filtered::>() + .get_single(world) + .ok()?; + let windows = world.get_non_send_resource::()?; + Some(windows.get_window(window_entity)?.canvas()) +} + +async fn init_webxr( + canvas: HtmlCanvasElement, + mode: XrSessionMode, + reference_type: XrReferenceSpaceType, +) -> Result<(XrSession, XrReferenceSpace), JsValue> { + let xr = web_sys::window().unwrap().navigator().xr(); + + let supports_session = JsFuture::from(xr.is_session_supported(mode)).await?; + if supports_session == false { + error!("XR session {:?} not supported", mode); + return Err(JsValue::from_str(&format!( + "XR session {:?} not supported", + mode + ))); + } + + info!("creating session"); + let session: XrSession = JsFuture::from(xr.request_session(mode)).await?.into(); + + info!("creating gl"); + let gl: WebGl2RenderingContext = { + let gl_attribs = Object::new(); + js_sys::Reflect::set( + &gl_attribs, + &JsValue::from_str("xrCompatible"), + &JsValue::TRUE, + )?; + canvas + .get_context_with_context_options("webgl2", &gl_attribs)? + .ok_or(JsValue::from_str( + "Unable to create WebGL rendering context", + ))? + .dyn_into()? + }; + + let xr_gl_layer = XrWebGlLayer::new_with_web_gl2_rendering_context(&session, &gl)?; + let mut render_state_init = XrRenderStateInit::new(); + render_state_init.base_layer(Some(&xr_gl_layer)); + session.update_render_state_with_state(&render_state_init); + info!("creating ref space"); + let reference_space = JsFuture::from(session.request_reference_space(reference_type)) + .await? + .into(); + + info!("finished"); + Ok((session, reference_space)) +} diff --git a/xr_api/Cargo.toml b/xr_api/Cargo.toml index c803779..09bdf8c 100644 --- a/xr_api/Cargo.toml +++ b/xr_api/Cargo.toml @@ -8,7 +8,6 @@ default = ["linked"] linked = ["openxr/linked"] [dependencies] -ash = "0.37.3" futures = "0.3.29" glam = "0.24.1" hashbrown = "0.14" @@ -20,6 +19,7 @@ wgpu-hal = "0.17.1" [target.'cfg(not(target_family = "wasm"))'.dependencies] openxr = "0.17.1" +ash = "0.37.3" [target.'cfg(target_family = "wasm")'.dependencies] wasm-bindgen = "0.2.87" diff --git a/xr_api/src/api.rs b/xr_api/src/api.rs index 51f63f8..4733b8f 100644 --- a/xr_api/src/api.rs +++ b/xr_api/src/api.rs @@ -14,10 +14,7 @@ pub struct Entry(Rc); impl Entry { /// Constructs a new Xr entry pub fn new() -> Self { - #[cfg(target_family = "wasm")] - return crate::backend::webxr::WebXrEntry::new().into(); - #[cfg(not(target_family = "wasm"))] - return crate::backend::oxr::OXrEntry::new().into(); + todo!() } } diff --git a/xr_api/src/api_traits.rs b/xr_api/src/api_traits.rs index 05c8d9d..ec29aee 100644 --- a/xr_api/src/api_traits.rs +++ b/xr_api/src/api_traits.rs @@ -31,12 +31,14 @@ pub trait SessionTrait { fn headset_location(&self) -> Result; /// Request input modules with the specified bindings. fn create_input(&self, bindings: Bindings) -> Result; - /// Blocks until a rendering frame is available, then returns the views for the left and right eyes. + /// Wait until a frame is ready to render to. + fn wait_frame(&self) -> Result; + /// Begin rendering work for the frame. fn begin_frame(&self) -> Result<()>; /// Locate the views of each eye. fn locate_views(&self) -> Result<(View, View)>; /// Submits rendering work for this frame. - fn end_frame(&self) -> Result<()>; + fn end_frame(&self, data: FrameData) -> Result<()>; /// Gets the resolution of a single eye. fn resolution(&self) -> UVec2; /// Gets the texture format for the session. diff --git a/xr_api/src/backend/oxr.rs b/xr_api/src/backend/oxr.rs index 3a96652..c793047 100644 --- a/xr_api/src/backend/oxr.rs +++ b/xr_api/src/backend/oxr.rs @@ -145,7 +145,7 @@ impl SessionTrait for OXrSession { .into()) } - fn begin_frame(&self) -> Result<()> { + fn wait_frame(&self) -> Result { { let mut bindings = self.bindings.lock().unwrap(); if !bindings.sessions_attached { @@ -239,6 +239,10 @@ impl SessionTrait for OXrSession { } }; } + Ok(FrameData) + } + + fn begin_frame(&self) -> Result<()> { { let _span = info_span!("xr_begin_frame").entered(); self.swapchain.begin().unwrap() @@ -292,7 +296,7 @@ impl SessionTrait for OXrSession { } } - fn end_frame(&self) -> Result<()> { + fn end_frame(&self, data: FrameData) -> Result<()> { { let _span = info_span!("xr_release_image").entered(); self.swapchain.release_image().unwrap(); diff --git a/xr_api/src/backend/webxr.rs b/xr_api/src/backend/webxr.rs index b5ea43e..8b13789 100644 --- a/xr_api/src/backend/webxr.rs +++ b/xr_api/src/backend/webxr.rs @@ -1,208 +1 @@ -use std::sync::{ - mpsc::{channel, Sender}, - Mutex, -}; -use crate::prelude::*; - -use wasm_bindgen::{closure::Closure, JsCast}; -use wasm_bindgen_futures::js_sys; -use web_sys::{XrFrame, XrInputSource}; - -mod utils; - -use utils::*; - -#[derive(Clone)] -pub struct WebXrEntry(web_sys::XrSystem); - -impl WebXrEntry { - pub fn new() -> Self { - Self( - web_sys::window() - .expect("No window available in current environment") - .navigator() - .xr(), - ) - } -} - -impl EntryTrait for WebXrEntry { - fn available_extensions(&self) -> Result { - Ok(ExtensionSet::default()) - } - - fn create_instance(&self, exts: ExtensionSet) -> Result { - Ok(WebXrInstance { - entry: self.clone(), - exts, - } - .into()) - } -} - -#[derive(Clone)] -pub struct WebXrInstance { - entry: WebXrEntry, - exts: ExtensionSet, -} - -impl InstanceTrait for WebXrInstance { - fn entry(&self) -> Entry { - self.entry.clone().into() - } - - fn enabled_extensions(&self) -> ExtensionSet { - self.exts - } - - fn create_session(&self, _info: SessionCreateInfo) -> Result { - Ok(WebXrSession { - instance: self.clone().into(), - session: self - .entry - .0 - .request_session(web_sys::XrSessionMode::ImmersiveVr) - .resolve() - .map_err(|_| XrError::Placeholder)?, - end_frame_sender: Mutex::default(), - } - .into()) - } -} - -pub struct WebXrSession { - instance: Instance, - session: web_sys::XrSession, - end_frame_sender: Mutex>>, -} - -impl SessionTrait for WebXrSession { - fn instance(&self) -> &Instance { - &self.instance - } - - fn create_input(&self, bindings: Bindings) -> Result { - Ok(WebXrInput { - devices: self.session.input_sources(), - bindings, - } - .into()) - } - - fn begin_frame(&self) -> Result<(View, View)> { - let mut end_frame_sender = self.end_frame_sender.lock().unwrap(); - if end_frame_sender.is_some() { - Err(XrError::Placeholder)? - } - let (tx, rx) = channel::<()>(); - let (tx_end, rx_end) = channel::<()>(); - *end_frame_sender = Some(tx_end); - let on_frame: Closure = - Closure::new(move |_time: f64, _frame: XrFrame| { - tx.send(()).ok(); - rx_end.recv().ok(); - }); - - self.session - .request_animation_frame(on_frame.as_ref().unchecked_ref()); - - rx.recv().ok(); - todo!() - } - - fn end_frame(&self) -> Result<()> { - let mut end_frame_sender = self.end_frame_sender.lock().unwrap(); - match std::mem::take(&mut *end_frame_sender) { - Some(sender) => sender.send(()).ok(), - None => Err(XrError::Placeholder)?, - }; - Ok(()) - } - - fn get_render_resources( - &self, - ) -> Option<( - wgpu::Device, - wgpu::Queue, - wgpu::AdapterInfo, - wgpu::Adapter, - wgpu::Instance, - )> { - todo!() - } -} - -pub struct WebXrInput { - devices: web_sys::XrInputSourceArray, - bindings: Bindings, -} - -impl From for Handedness { - fn from(value: web_sys::XrHandedness) -> Self { - match value { - web_sys::XrHandedness::None => Handedness::None, - web_sys::XrHandedness::Left => Handedness::Left, - web_sys::XrHandedness::Right => Handedness::Right, - _ => todo!(), - } - } -} - -impl WebXrInput { - fn get_controller(&self, handedness: Handedness) -> Option { - js_sys::try_iter(&self.devices).ok()??.find_map(|dev| { - if let Ok(dev) = dev { - let dev: XrInputSource = dev.into(); - if Into::::into(dev.handedness()) == handedness { - Some(dev) - } else { - None - } - } else { - None - } - }) - } -} - -impl InputTrait for WebXrInput { - fn get_haptics(&self, path: ActionPath) -> Result> { - // let haptics = self - // .get_controller(path.handedness) - // .ok_or(XrError::Placeholder)? - // .gamepad() - // .ok_or(XrError::Placeholder)? - // .haptic_actuators() - // .iter() - // .next() - // .ok_or(XrError::Placeholder)? - // .into(); - // Ok(WebXrHaptics(haptics, path).into()) - todo!() - } - - fn get_pose(&self, _path: ActionPath) -> Result> { - todo!() - } - - fn get_float(&self, _path: ActionPath) -> Result> { - todo!() - } - - fn get_bool(&self, _path: ActionPath) -> Result> { - todo!() - } -} - -pub struct OXrActionInput(openxr::Action); - -pub struct WebXrHaptics(web_sys::GamepadHapticActuator, ActionPath); - -impl ActionTrait for WebXrHaptics { - fn id(&self) -> ActionPath { - self.1 - } -} - -impl HapticTrait for WebXrHaptics {} diff --git a/xr_api/src/types.rs b/xr_api/src/types.rs index 0b95e89..ea6600a 100644 --- a/xr_api/src/types.rs +++ b/xr_api/src/types.rs @@ -10,6 +10,8 @@ pub struct ExtensionSet { pub vulkan: bool, } +pub struct FrameData; + pub struct SessionCreateInfo { /// preferred texture format pub texture_format: wgpu::TextureFormat,