diff --git a/+ b/+ new file mode 100644 index 0000000..8e33f19 --- /dev/null +++ b/+ @@ -0,0 +1,294 @@ +use crate::xr_input::{QuatConv, Vec3Conv}; +use crate::{LEFT_XR_TEXTURE_HANDLE, RIGHT_XR_TEXTURE_HANDLE}; +use bevy::core_pipeline::tonemapping::{DebandDither, Tonemapping}; +use bevy::ecs::system::lifetimeless::Read; +use bevy::math::Vec3A; +use bevy::prelude::*; +use bevy::render::camera::{CameraProjection, CameraRenderGraph, RenderTarget}; +use bevy::render::extract_component::ExtractComponent; +use bevy::render::primitives::Frustum; +use bevy::render::view::{ColorGrading, VisibleEntities}; +use openxr::Fovf; + +#[derive(Bundle)] +pub struct XrCamerasBundle { + pub left: XrCameraBundle, + pub right: XrCameraBundle, +} +impl XrCamerasBundle { + pub fn new() -> Self { + Self::default() + } +} +impl Default for XrCamerasBundle { + fn default() -> Self { + Self { + left: XrCameraBundle::new(Eye::Left), + right: XrCameraBundle::new(Eye::Right), + } + } +} + +#[derive(Bundle)] +pub struct XrCameraBundle { + pub camera: Camera, + pub camera_render_graph: CameraRenderGraph, + pub xr_projection: XRProjection, + 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(Component)] +pub(super) struct TransformExtract; + + +impl ExtractComponent for TransformExtract { + type Query = Read; + + type Filter = (); + + type Out = Transform; + + fn extract_component(item: bevy::ecs::query::QueryItem<'_, Self::Query>) -> Option { + info!("extracting Transform"); + Some(*item) + } +} + +impl ExtractComponent for XrCameraType { + type Query = Read; + + type Filter = (); + + type Out = Self; + + fn extract_component(item: bevy::ecs::query::QueryItem<'_, Self::Query>) -> Option { + info!("extracting Cam Type"); + Some(*item) + } +} + +#[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, ExtractComponent)] +#[reflect(Component, Default)] +pub struct XRProjection { + pub near: f32, + pub far: f32, + #[reflect(ignore)] + pub fov: Fovf, +} + +impl Default for XRProjection { + fn default() -> Self { + Self { + near: 0.1, + far: 1000., + fov: Default::default(), + } + } +} + +impl XRProjection { + pub fn new(near: f32, far: f32, fov: Fovf) -> Self { + XRProjection { near, far, fov } + } +} + +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 + ] + } +} + +pub fn xr_camera_head_sync( + views: ResMut, + mut query: Query<(&mut Transform, &XrCameraType, &mut XRProjection)>, +) { + //TODO calculate HMD position + for (mut transform, camera_type, mut xr_projection) in query.iter_mut() { + let view_idx = match camera_type { + XrCameraType::Xr(eye) => *eye as usize, + // I don't belive we need a flatscrenn cam, that's just a cam without this component + XrCameraType::Flatscreen => continue, + }; + let view = match views.get(view_idx) { + Some(views) => views, + None => continue, + }; + xr_projection.fov = view.fov; + transform.rotation = view.pose.orientation.to_quat(); + transform.translation = view.pose.position.to_vec3(); + } +} diff --git a/src/xr_init/mod.rs b/src/xr_init/mod.rs new file mode 100644 index 0000000..d7db1c9 --- /dev/null +++ b/src/xr_init/mod.rs @@ -0,0 +1,197 @@ +pub mod schedules; +pub use schedules::*; + +use bevy::{ + prelude::*, + render::{ + camera::{ManualTextureView, ManualTextureViews}, + extract_resource::{ExtractResource, ExtractResourcePlugin}, + renderer::{RenderAdapter, RenderDevice, RenderInstance}, + }, + window::{PrimaryWindow, RawHandleWrapper}, +}; + +use crate::{ + graphics, + resources::{ + OXrSessionSetupInfo, XrFormat, XrInstance, XrResolution, XrSession, XrSessionRunning, + XrSwapchain, + }, + LEFT_XR_TEXTURE_HANDLE, RIGHT_XR_TEXTURE_HANDLE, +}; + +#[derive(Resource, Event, Clone, Copy, PartialEq, Eq, Reflect, Debug, ExtractResource)] +pub enum XrStatus { + NoInstance, + Enabled, + Enabling, + Disabled, + Disabling, +} + +#[derive( + Resource, Clone, Copy, PartialEq, Eq, Reflect, Debug, ExtractResource, Default, Deref, DerefMut, +)] +pub struct XrShouldRender(bool); + +pub struct XrEarlyInitPlugin; + +pub struct XrInitPlugin; + +pub fn xr_only() -> impl FnMut(Option>) -> bool { + resource_exists_and_equals(XrStatus::Enabled) +} +pub fn xr_render_only() -> impl FnMut(Option>) -> bool { + resource_exists_and_equals(XrShouldRender(true)) +} + +impl Plugin for XrEarlyInitPlugin { + fn build(&self, app: &mut App) { + app.add_event::() + .add_event::() + .add_event::() + .add_event::(); + } +} + +impl Plugin for XrInitPlugin { + fn build(&self, app: &mut App) { + add_schedules(app); + app.add_plugins(ExtractResourcePlugin::::default()); + app.add_plugins(ExtractResourcePlugin::::default()); + app.init_resource::(); + app.add_systems(PreUpdate, setup_xr.run_if(on_event::())) + .add_systems(PreUpdate, cleanup_xr.run_if(on_event::())); + app.add_systems( + PostUpdate, + start_xr_session.run_if(on_event::()), + ); + app.add_systems( + PostUpdate, + stop_xr_session.run_if(on_event::()), + ); + app.add_systems(XrSetup, setup_manual_texture_views); + } +} + +fn setup_manual_texture_views( + mut manual_texture_views: ResMut, + swapchain: Res, + xr_resolution: Res, + xr_format: Res, +) { + info!("Creating Texture views"); + let (left, right) = swapchain.get_render_views(); + let left = ManualTextureView { + texture_view: left.into(), + size: **xr_resolution, + format: **xr_format, + }; + let right = ManualTextureView { + texture_view: right.into(), + size: **xr_resolution, + format: **xr_format, + }; + manual_texture_views.insert(LEFT_XR_TEXTURE_HANDLE, left); + manual_texture_views.insert(RIGHT_XR_TEXTURE_HANDLE, right); +} + +pub fn setup_xr(world: &mut World) { + world.run_schedule(XrPreSetup); + world.run_schedule(XrSetup); + world.run_schedule(XrPrePostSetup); + world.run_schedule(XrPostSetup); + *world.resource_mut::() = XrStatus::Enabled; +} +fn cleanup_xr(world: &mut World) { + world.run_schedule(XrPreCleanup); + world.run_schedule(XrCleanup); + world.run_schedule(XrPostCleanup); + *world.resource_mut::() = XrStatus::Disabled; +} + +#[derive(Event, Clone, Copy, Default)] +pub struct StartXrSession; + +#[derive(Event, Clone, Copy, Default)] +pub struct EndXrSession; + +#[derive(Event, Clone, Copy, Default)] +struct SetupXrData; +#[derive(Event, Clone, Copy, Default)] +pub(crate) struct CleanupXrData; + +#[allow(clippy::too_many_arguments)] +fn start_xr_session( + mut commands: Commands, + mut setup_xr: EventWriter, + mut status: ResMut, + instance: Res, + primary_window: Query<&RawHandleWrapper, With>, + setup_info: NonSend, + render_device: Res, + render_adapter: Res, + render_instance: Res, +) { + info!("start Session"); + match *status { + XrStatus::Disabled => {} + XrStatus::NoInstance => { + warn!("Trying to start OpenXR Session without instance, ignoring"); + return; + } + XrStatus::Enabled | XrStatus::Enabling => { + warn!("Trying to start OpenXR Session while one already exists, ignoring"); + return; + } + XrStatus::Disabling => { + warn!("Trying to start OpenXR Session while one is stopping, ignoring"); + return; + } + } + let ( + xr_session, + xr_resolution, + xr_format, + xr_session_running, + xr_frame_waiter, + xr_swapchain, + xr_input, + xr_views, + xr_frame_state, + ) = match graphics::start_xr_session( + primary_window.get_single().cloned().ok(), + &setup_info, + &instance, + &render_device, + &render_adapter, + &render_instance, + ) { + Ok(data) => data, + Err(err) => { + error!("Unable to start OpenXR Session: {}", err); + return; + } + }; + commands.insert_resource(xr_session); + commands.insert_resource(xr_resolution); + commands.insert_resource(xr_format); + commands.insert_resource(xr_session_running); + commands.insert_resource(xr_frame_waiter); + commands.insert_resource(xr_swapchain); + commands.insert_resource(xr_input); + commands.insert_resource(xr_views); + commands.insert_resource(xr_frame_state); + *status = XrStatus::Enabling; + setup_xr.send_default(); +} + +fn stop_xr_session(session: ResMut, mut status: ResMut) { + match session.request_exit() { + Ok(_) => {} + Err(err) => { + error!("Error while trying to request session exit: {}", err) + } + } + *status = XrStatus::Enabling; +} diff --git a/src/xr_init/schedules.rs b/src/xr_init/schedules.rs new file mode 100644 index 0000000..f08fde6 --- /dev/null +++ b/src/xr_init/schedules.rs @@ -0,0 +1,51 @@ +use bevy::{ecs::schedule::{ScheduleLabel, Schedule, ExecutorKind}, app::App}; + +#[derive(Debug, ScheduleLabel, Clone, Copy, Hash, PartialEq, Eq)] +pub struct XrPreSetup; + +#[derive(Debug, ScheduleLabel, Clone, Copy, Hash, PartialEq, Eq)] +pub struct XrSetup; + +#[derive(Debug, ScheduleLabel, Clone, Copy, Hash, PartialEq, Eq)] +pub struct XrPrePostSetup; + +#[derive(Debug, ScheduleLabel, Clone, Copy, Hash, PartialEq, Eq)] +pub struct XrPostSetup; + +#[derive(Debug, ScheduleLabel, Clone, Copy, Hash, PartialEq, Eq)] +pub struct XrPreCleanup; + +#[derive(Debug, ScheduleLabel, Clone, Copy, Hash, PartialEq, Eq)] +pub struct XrCleanup; + +#[derive(Debug, ScheduleLabel, Clone, Copy, Hash, PartialEq, Eq)] +pub struct XrPostCleanup; + +#[derive(Debug, ScheduleLabel, Clone, Copy, Hash, PartialEq, Eq)] +pub struct XrPreRenderUpdate; + +#[derive(Debug, ScheduleLabel, Clone, Copy, Hash, PartialEq, Eq)] +pub struct XrRenderUpdate; + +#[derive(Debug, ScheduleLabel, Clone, Copy, Hash, PartialEq, Eq)] +pub struct XrPostRenderUpdate; + +pub(super) fn add_schedules(app: &mut App) { + let schedules = [ + Schedule::new(XrPreSetup), + Schedule::new(XrSetup), + Schedule::new(XrPrePostSetup), + Schedule::new(XrPostSetup), + Schedule::new(XrPreRenderUpdate), + Schedule::new(XrRenderUpdate), + Schedule::new(XrPostRenderUpdate), + Schedule::new(XrPreCleanup), + Schedule::new(XrCleanup), + Schedule::new(XrPostCleanup), + ]; + for mut schedule in schedules { + schedule.set_executor_kind(ExecutorKind::SingleThreaded); + schedule.set_apply_final_deferred(true); + app.add_schedule(schedule); + } +}