rendering code and api crate
This commit is contained in:
@@ -12,6 +12,8 @@ linked = ["openxr/linked"]
|
|||||||
vulkan = ["dep:ash"]
|
vulkan = ["dep:ash"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
bevy_openxr.path = "./crates/bevy_openxr"
|
||||||
|
bevy_xr.path = "./crates/bevy_xr"
|
||||||
anyhow = "1.0.79"
|
anyhow = "1.0.79"
|
||||||
async-std = "1.12.0"
|
async-std = "1.12.0"
|
||||||
bevy = "0.13.0"
|
bevy = "0.13.0"
|
||||||
@@ -88,3 +90,9 @@ web-sys = { version = "0.3.67", features = [
|
|||||||
'XrSystem',
|
'XrSystem',
|
||||||
] }
|
] }
|
||||||
wasm-bindgen-futures = "0.4"
|
wasm-bindgen-futures = "0.4"
|
||||||
|
|
||||||
|
[workspace]
|
||||||
|
members = ["crates/*"]
|
||||||
|
|
||||||
|
[workspace.dependencies]
|
||||||
|
bevy = "0.13.0"
|
||||||
|
|||||||
25
crates/bevy_openxr/Cargo.toml
Normal file
25
crates/bevy_openxr/Cargo.toml
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
[package]
|
||||||
|
name = "bevy_openxr"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[features]
|
||||||
|
default = ["vulkan"]
|
||||||
|
vulkan = ["dep:ash"]
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
thiserror = "1.0.57"
|
||||||
|
wgpu = "0.19.3"
|
||||||
|
wgpu-hal = "0.19.3"
|
||||||
|
|
||||||
|
bevy_xr.path = "../bevy_xr"
|
||||||
|
bevy.workspace = true
|
||||||
|
|
||||||
|
|
||||||
|
ash = { version = "0.37.3", optional = true }
|
||||||
|
|
||||||
|
[target.'cfg(target_family = "unix")'.dependencies]
|
||||||
|
openxr = { version = "0.18.0", features = ["mint"] }
|
||||||
|
|
||||||
|
[target.'cfg(target_family = "windows")'.dependencies]
|
||||||
|
openxr = { version = "0.18.0", features = ["mint", "static"] }
|
||||||
55
crates/bevy_openxr/examples/3d_scene.rs
Normal file
55
crates/bevy_openxr/examples/3d_scene.rs
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
//! A simple 3D scene with light shining over a cube sitting on a plane.
|
||||||
|
|
||||||
|
use bevy::{
|
||||||
|
prelude::*,
|
||||||
|
render::camera::{ManualTextureViewHandle, RenderTarget},
|
||||||
|
};
|
||||||
|
use bevy_openxr::{add_xr_plugins, render::XR_TEXTURE_INDEX};
|
||||||
|
use bevy_xr::camera::XrCameraBundle;
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
App::new()
|
||||||
|
.add_plugins(add_xr_plugins(DefaultPlugins))
|
||||||
|
.add_systems(Startup, setup)
|
||||||
|
.run();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// set up a simple 3D scene
|
||||||
|
fn setup(
|
||||||
|
mut commands: Commands,
|
||||||
|
mut meshes: ResMut<Assets<Mesh>>,
|
||||||
|
mut materials: ResMut<Assets<StandardMaterial>>,
|
||||||
|
) {
|
||||||
|
// circular base
|
||||||
|
commands.spawn(PbrBundle {
|
||||||
|
mesh: meshes.add(Circle::new(4.0)),
|
||||||
|
material: materials.add(Color::WHITE),
|
||||||
|
transform: Transform::from_rotation(Quat::from_rotation_x(-std::f32::consts::FRAC_PI_2)),
|
||||||
|
..default()
|
||||||
|
});
|
||||||
|
// cube
|
||||||
|
commands.spawn(PbrBundle {
|
||||||
|
mesh: meshes.add(Cuboid::new(1.0, 1.0, 1.0)),
|
||||||
|
material: materials.add(Color::rgb_u8(124, 144, 255)),
|
||||||
|
transform: Transform::from_xyz(0.0, 0.5, 0.0),
|
||||||
|
..default()
|
||||||
|
});
|
||||||
|
// light
|
||||||
|
commands.spawn(PointLightBundle {
|
||||||
|
point_light: PointLight {
|
||||||
|
shadows_enabled: true,
|
||||||
|
..default()
|
||||||
|
},
|
||||||
|
transform: Transform::from_xyz(4.0, 8.0, 4.0),
|
||||||
|
..default()
|
||||||
|
});
|
||||||
|
// // camera
|
||||||
|
// commands.spawn(XrCameraBundle {
|
||||||
|
// transform: Transform::from_xyz(-2.5, 4.5, 9.0).looking_at(Vec3::ZERO, Vec3::Y),
|
||||||
|
// camera: Camera {
|
||||||
|
// target: RenderTarget::TextureView(ManualTextureViewHandle(XR_TEXTURE_INDEX + 1)),
|
||||||
|
// ..default()
|
||||||
|
// },
|
||||||
|
// ..default()
|
||||||
|
// });
|
||||||
|
}
|
||||||
0
crates/bevy_openxr/src/camera.rs
Normal file
0
crates/bevy_openxr/src/camera.rs
Normal file
66
crates/bevy_openxr/src/error.rs
Normal file
66
crates/bevy_openxr/src/error.rs
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
use crate::graphics::GraphicsBackend;
|
||||||
|
use std::borrow::Cow;
|
||||||
|
use std::fmt;
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
#[derive(Error, Debug)]
|
||||||
|
pub enum XrError {
|
||||||
|
#[error("OpenXR error: {0}")]
|
||||||
|
OpenXrError(#[from] openxr::sys::Result),
|
||||||
|
#[error("OpenXR loading error: {0}")]
|
||||||
|
OpenXrLoadingError(#[from] openxr::LoadError),
|
||||||
|
#[error("WGPU instance error: {0}")]
|
||||||
|
WgpuInstanceError(#[from] wgpu_hal::InstanceError),
|
||||||
|
#[error("WGPU device error: {0}")]
|
||||||
|
WgpuDeviceError(#[from] wgpu_hal::DeviceError),
|
||||||
|
#[error("WGPU request device error: {0}")]
|
||||||
|
WgpuRequestDeviceError(#[from] wgpu::RequestDeviceError),
|
||||||
|
#[error("Unsupported texture format: {0:?}")]
|
||||||
|
UnsupportedTextureFormat(wgpu::TextureFormat),
|
||||||
|
#[error("Vulkan error: {0}")]
|
||||||
|
VulkanError(#[from] ash::vk::Result),
|
||||||
|
#[error("Vulkan loading error: {0}")]
|
||||||
|
VulkanLoadingError(#[from] ash::LoadingError),
|
||||||
|
#[error("Graphics backend '{0:?}' is not available")]
|
||||||
|
UnavailableBackend(GraphicsBackend),
|
||||||
|
#[error("No compatible backend available")]
|
||||||
|
NoAvailableBackend,
|
||||||
|
#[error("No compatible view configuration available")]
|
||||||
|
NoAvailableViewConfiguration,
|
||||||
|
#[error("No compatible blend mode available")]
|
||||||
|
NoAvailableBlendMode,
|
||||||
|
#[error("No compatible format available")]
|
||||||
|
NoAvailableFormat,
|
||||||
|
#[error("OpenXR runtime does not support these extensions: {0}")]
|
||||||
|
UnavailableExtensions(UnavailableExts),
|
||||||
|
#[error("Could not meet graphics requirements for platform. See console for details")]
|
||||||
|
FailedGraphicsRequirements,
|
||||||
|
#[error(
|
||||||
|
"Tried to use item {item} with backend {backend}. Expected backend {expected_backend}"
|
||||||
|
)]
|
||||||
|
GraphicsBackendMismatch {
|
||||||
|
item: &'static str,
|
||||||
|
backend: &'static str,
|
||||||
|
expected_backend: &'static str,
|
||||||
|
},
|
||||||
|
#[error("Failed to create CString: {0}")]
|
||||||
|
NulError(#[from] std::ffi::NulError),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Vec<Cow<'static, str>>> for XrError {
|
||||||
|
fn from(value: Vec<Cow<'static, str>>) -> Self {
|
||||||
|
Self::UnavailableExtensions(UnavailableExts(value))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct UnavailableExts(Vec<Cow<'static, str>>);
|
||||||
|
|
||||||
|
impl fmt::Display for UnavailableExts {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
for s in &self.0 {
|
||||||
|
write!(f, "\t{s}")?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -62,7 +62,7 @@ macro_rules! unavailable_exts {
|
|||||||
) => {
|
) => {
|
||||||
impl $exts {
|
impl $exts {
|
||||||
/// Returns any extensions needed by `required_exts` that aren't available in `self`
|
/// Returns any extensions needed by `required_exts` that aren't available in `self`
|
||||||
pub(crate) fn unavailable_exts(&self, required_exts: &Self) -> Vec<std::borrow::Cow<'static, str>> {
|
pub fn unavailable_exts(&self, required_exts: &Self) -> Vec<std::borrow::Cow<'static, str>> {
|
||||||
let mut exts = vec![];
|
let mut exts = vec![];
|
||||||
$(
|
$(
|
||||||
$(
|
$(
|
||||||
@@ -167,6 +167,7 @@ macro_rules! impl_ext {
|
|||||||
$macro! {
|
$macro! {
|
||||||
XrExtensions;
|
XrExtensions;
|
||||||
almalence_digital_lens_control,
|
almalence_digital_lens_control,
|
||||||
|
bd_controller_interaction,
|
||||||
epic_view_configuration_fov,
|
epic_view_configuration_fov,
|
||||||
ext_performance_settings,
|
ext_performance_settings,
|
||||||
ext_thermal_query,
|
ext_thermal_query,
|
||||||
@@ -183,13 +184,18 @@ macro_rules! impl_ext {
|
|||||||
ext_hp_mixed_reality_controller,
|
ext_hp_mixed_reality_controller,
|
||||||
ext_palm_pose,
|
ext_palm_pose,
|
||||||
ext_uuid,
|
ext_uuid,
|
||||||
extx_overlay,
|
ext_hand_interaction,
|
||||||
|
ext_active_action_set_priority,
|
||||||
|
ext_local_floor,
|
||||||
|
ext_hand_tracking_data_source,
|
||||||
|
ext_plane_detection,
|
||||||
fb_composition_layer_image_layout,
|
fb_composition_layer_image_layout,
|
||||||
fb_composition_layer_alpha_blend,
|
fb_composition_layer_alpha_blend,
|
||||||
#[cfg(target_os = "android")]
|
#[cfg(target_os = "android")]
|
||||||
fb_android_surface_swapchain_create,
|
fb_android_surface_swapchain_create,
|
||||||
fb_swapchain_update_state,
|
fb_swapchain_update_state,
|
||||||
fb_composition_layer_secure_content,
|
fb_composition_layer_secure_content,
|
||||||
|
fb_body_tracking,
|
||||||
fb_display_refresh_rate,
|
fb_display_refresh_rate,
|
||||||
fb_color_space,
|
fb_color_space,
|
||||||
fb_hand_tracking_mesh,
|
fb_hand_tracking_mesh,
|
||||||
@@ -209,17 +215,29 @@ macro_rules! impl_ext {
|
|||||||
fb_swapchain_update_state_android_surface,
|
fb_swapchain_update_state_android_surface,
|
||||||
fb_swapchain_update_state_opengl_es,
|
fb_swapchain_update_state_opengl_es,
|
||||||
fb_swapchain_update_state_vulkan,
|
fb_swapchain_update_state_vulkan,
|
||||||
|
fb_touch_controller_pro,
|
||||||
|
fb_spatial_entity_sharing,
|
||||||
fb_space_warp,
|
fb_space_warp,
|
||||||
|
fb_haptic_amplitude_envelope,
|
||||||
fb_scene,
|
fb_scene,
|
||||||
|
fb_scene_capture,
|
||||||
fb_spatial_entity_container,
|
fb_spatial_entity_container,
|
||||||
|
fb_face_tracking,
|
||||||
|
fb_eye_tracking_social,
|
||||||
fb_passthrough_keyboard_hands,
|
fb_passthrough_keyboard_hands,
|
||||||
fb_composition_layer_settings,
|
fb_composition_layer_settings,
|
||||||
|
fb_touch_controller_proximity,
|
||||||
|
fb_haptic_pcm,
|
||||||
|
fb_composition_layer_depth_test,
|
||||||
|
fb_spatial_entity_storage_batch,
|
||||||
|
fb_spatial_entity_user,
|
||||||
htc_vive_cosmos_controller_interaction,
|
htc_vive_cosmos_controller_interaction,
|
||||||
htc_facial_tracking,
|
htc_facial_tracking,
|
||||||
htc_vive_focus3_controller_interaction,
|
htc_vive_focus3_controller_interaction,
|
||||||
htc_hand_interaction,
|
htc_hand_interaction,
|
||||||
htc_vive_wrist_tracker_interaction,
|
htc_vive_wrist_tracker_interaction,
|
||||||
htcx_vive_tracker_interaction,
|
htc_passthrough,
|
||||||
|
htc_foveation,
|
||||||
huawei_controller_interaction,
|
huawei_controller_interaction,
|
||||||
#[cfg(target_os = "android")]
|
#[cfg(target_os = "android")]
|
||||||
khr_android_thread_settings,
|
khr_android_thread_settings,
|
||||||
@@ -251,12 +269,21 @@ macro_rules! impl_ext {
|
|||||||
khr_composition_layer_equirect2,
|
khr_composition_layer_equirect2,
|
||||||
khr_binding_modification,
|
khr_binding_modification,
|
||||||
khr_swapchain_usage_input_attachment_bit,
|
khr_swapchain_usage_input_attachment_bit,
|
||||||
|
meta_foveation_eye_tracked,
|
||||||
|
meta_local_dimming,
|
||||||
|
meta_passthrough_preferences,
|
||||||
|
meta_virtual_keyboard,
|
||||||
meta_vulkan_swapchain_create_info,
|
meta_vulkan_swapchain_create_info,
|
||||||
meta_performance_metrics,
|
meta_performance_metrics,
|
||||||
|
meta_headset_id,
|
||||||
|
meta_passthrough_color_lut,
|
||||||
ml_ml2_controller_interaction,
|
ml_ml2_controller_interaction,
|
||||||
|
ml_frame_end_info,
|
||||||
|
ml_global_dimmer,
|
||||||
|
ml_compat,
|
||||||
|
ml_user_calibration,
|
||||||
mnd_headless,
|
mnd_headless,
|
||||||
mnd_swapchain_usage_input_attachment_bit,
|
mnd_swapchain_usage_input_attachment_bit,
|
||||||
mndx_egl_enable,
|
|
||||||
msft_unbounded_reference_space,
|
msft_unbounded_reference_space,
|
||||||
msft_spatial_anchor,
|
msft_spatial_anchor,
|
||||||
msft_spatial_graph_bridge,
|
msft_spatial_graph_bridge,
|
||||||
@@ -274,6 +301,9 @@ macro_rules! impl_ext {
|
|||||||
#[cfg(target_os = "android")]
|
#[cfg(target_os = "android")]
|
||||||
oculus_android_session_state_enable,
|
oculus_android_session_state_enable,
|
||||||
oculus_audio_device_guid,
|
oculus_audio_device_guid,
|
||||||
|
oculus_external_camera,
|
||||||
|
oppo_controller_interaction,
|
||||||
|
qcom_tracking_optimization_settings,
|
||||||
ultraleap_hand_tracking_forearm,
|
ultraleap_hand_tracking_forearm,
|
||||||
valve_analog_threshold,
|
valve_analog_threshold,
|
||||||
varjo_quad_views,
|
varjo_quad_views,
|
||||||
@@ -282,6 +312,7 @@ macro_rules! impl_ext {
|
|||||||
varjo_environment_depth_estimation,
|
varjo_environment_depth_estimation,
|
||||||
varjo_marker_tracking,
|
varjo_marker_tracking,
|
||||||
varjo_view_offset,
|
varjo_view_offset,
|
||||||
|
yvr_controller_interaction,
|
||||||
}
|
}
|
||||||
)*
|
)*
|
||||||
|
|
||||||
@@ -1,11 +1,10 @@
|
|||||||
|
#[cfg(feature = "vulkan")]
|
||||||
pub mod vulkan;
|
pub mod vulkan;
|
||||||
|
|
||||||
use std::any::TypeId;
|
use std::any::TypeId;
|
||||||
|
|
||||||
use bevy::math::UVec2;
|
use crate::extensions::XrExtensions;
|
||||||
|
use crate::types::*;
|
||||||
use crate::openxr::types::{AppInfo, Result, XrError};
|
|
||||||
use crate::types::BlendMode;
|
|
||||||
|
|
||||||
pub unsafe trait GraphicsExt: openxr::Graphics {
|
pub unsafe trait GraphicsExt: openxr::Graphics {
|
||||||
/// Wrap the graphics specific type into the [GraphicsWrap] enum
|
/// Wrap the graphics specific type into the [GraphicsWrap] enum
|
||||||
@@ -30,8 +29,42 @@ pub unsafe trait GraphicsExt: openxr::Graphics {
|
|||||||
fn required_exts() -> XrExtensions;
|
fn required_exts() -> XrExtensions;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub trait GraphicsType {
|
||||||
|
type Inner<G: GraphicsExt>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl GraphicsType for () {
|
||||||
|
type Inner<G: GraphicsExt> = ();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type GraphicsBackend = GraphicsWrap<()>;
|
||||||
|
|
||||||
|
impl GraphicsBackend {
|
||||||
|
const ALL: &'static [Self] = &[Self::Vulkan(())];
|
||||||
|
|
||||||
|
pub fn available_backends(exts: &XrExtensions) -> Vec<Self> {
|
||||||
|
Self::ALL
|
||||||
|
.iter()
|
||||||
|
.copied()
|
||||||
|
.filter(|backend| backend.is_available(exts))
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_available(&self, exts: &XrExtensions) -> bool {
|
||||||
|
self.required_exts().is_available(exts)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn required_exts(&self) -> XrExtensions {
|
||||||
|
graphics_match!(
|
||||||
|
self;
|
||||||
|
_ => Api::required_exts()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
pub enum GraphicsWrap<T: GraphicsType> {
|
pub enum GraphicsWrap<T: GraphicsType> {
|
||||||
|
#[cfg(feature = "vulkan")]
|
||||||
Vulkan(T::Inner<openxr::Vulkan>),
|
Vulkan(T::Inner<openxr::Vulkan>),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,21 +95,14 @@ impl<T: GraphicsType> GraphicsWrap<T> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait GraphicsType {
|
|
||||||
type Inner<G: GraphicsExt>;
|
|
||||||
}
|
|
||||||
|
|
||||||
impl GraphicsType for () {
|
|
||||||
type Inner<G: GraphicsExt> = ();
|
|
||||||
}
|
|
||||||
|
|
||||||
macro_rules! graphics_match {
|
macro_rules! graphics_match {
|
||||||
(
|
(
|
||||||
$field:expr;
|
$field:expr;
|
||||||
$var:pat => $expr:expr $(=> $($return:tt)*)?
|
$var:pat => $expr:expr $(=> $($return:tt)*)?
|
||||||
) => {
|
) => {
|
||||||
match $field {
|
match $field {
|
||||||
$crate::openxr::graphics::GraphicsWrap::Vulkan($var) => {
|
#[cfg(feature = "vulkan")]
|
||||||
|
$crate::graphics::GraphicsWrap::Vulkan($var) => {
|
||||||
#[allow(unused)]
|
#[allow(unused)]
|
||||||
type Api = openxr::Vulkan;
|
type Api = openxr::Vulkan;
|
||||||
graphics_match!(@arm_impl Vulkan; $expr $(=> $($return)*)?)
|
graphics_match!(@arm_impl Vulkan; $expr $(=> $($return)*)?)
|
||||||
@@ -101,32 +127,5 @@ macro_rules! graphics_match {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
use bevy::math::UVec2;
|
||||||
pub(crate) use graphics_match;
|
pub(crate) use graphics_match;
|
||||||
|
|
||||||
use super::{WgpuGraphics, XrExtensions};
|
|
||||||
|
|
||||||
impl From<openxr::EnvironmentBlendMode> for BlendMode {
|
|
||||||
fn from(value: openxr::EnvironmentBlendMode) -> Self {
|
|
||||||
use openxr::EnvironmentBlendMode;
|
|
||||||
if value == EnvironmentBlendMode::OPAQUE {
|
|
||||||
BlendMode::Opaque
|
|
||||||
} else if value == EnvironmentBlendMode::ADDITIVE {
|
|
||||||
BlendMode::Additive
|
|
||||||
} else if value == EnvironmentBlendMode::ALPHA_BLEND {
|
|
||||||
BlendMode::AlphaBlend
|
|
||||||
} else {
|
|
||||||
unreachable!()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<BlendMode> for openxr::EnvironmentBlendMode {
|
|
||||||
fn from(value: BlendMode) -> Self {
|
|
||||||
use openxr::EnvironmentBlendMode;
|
|
||||||
match value {
|
|
||||||
BlendMode::Opaque => EnvironmentBlendMode::OPAQUE,
|
|
||||||
BlendMode::Additive => EnvironmentBlendMode::ADDITIVE,
|
|
||||||
BlendMode::AlphaBlend => EnvironmentBlendMode::ALPHA_BLEND,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -7,10 +7,11 @@ use openxr::Version;
|
|||||||
use wgpu_hal::api::Vulkan;
|
use wgpu_hal::api::Vulkan;
|
||||||
use wgpu_hal::Api;
|
use wgpu_hal::Api;
|
||||||
|
|
||||||
use crate::openxr::types::Result;
|
use crate::error::XrError;
|
||||||
use crate::openxr::{extensions::XrExtensions, WgpuGraphics};
|
use crate::extensions::XrExtensions;
|
||||||
|
use crate::types::*;
|
||||||
|
|
||||||
use super::{AppInfo, GraphicsExt, XrError};
|
use super::GraphicsExt;
|
||||||
|
|
||||||
#[cfg(not(target_os = "android"))]
|
#[cfg(not(target_os = "android"))]
|
||||||
const VK_TARGET_VERSION: Version = Version::new(1, 2, 0);
|
const VK_TARGET_VERSION: Version = Version::new(1, 2, 0);
|
||||||
455
crates/bevy_openxr/src/init.rs
Normal file
455
crates/bevy_openxr/src/init.rs
Normal file
@@ -0,0 +1,455 @@
|
|||||||
|
use bevy::app::{App, First, Plugin, PreUpdate};
|
||||||
|
use bevy::ecs::event::EventWriter;
|
||||||
|
use bevy::ecs::schedule::common_conditions::{not, on_event};
|
||||||
|
use bevy::ecs::schedule::IntoSystemConfigs;
|
||||||
|
use bevy::ecs::system::{Commands, Res, ResMut, Resource};
|
||||||
|
use bevy::ecs::world::World;
|
||||||
|
use bevy::log::{error, info};
|
||||||
|
use bevy::math::{uvec2, UVec2};
|
||||||
|
use bevy::render::camera::ManualTextureViews;
|
||||||
|
use bevy::render::extract_resource::ExtractResourcePlugin;
|
||||||
|
use bevy::render::renderer::{
|
||||||
|
RenderAdapter, RenderAdapterInfo, RenderDevice, RenderInstance, RenderQueue,
|
||||||
|
};
|
||||||
|
use bevy::render::settings::RenderCreation;
|
||||||
|
use bevy::render::{ExtractSchedule, MainWorld, RenderApp, RenderPlugin};
|
||||||
|
use bevy_xr::session::{
|
||||||
|
BeginXrSession, CreateXrSession, XrInstanceCreated, XrInstanceDestroyed, XrSessionState,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::error::XrError;
|
||||||
|
use crate::graphics::GraphicsBackend;
|
||||||
|
use crate::resources::*;
|
||||||
|
use crate::types::*;
|
||||||
|
|
||||||
|
pub struct XrInitPlugin {
|
||||||
|
/// Information about the app this is being used to build.
|
||||||
|
pub app_info: AppInfo,
|
||||||
|
/// Extensions wanted for this session.
|
||||||
|
// TODO!() This should be changed to take a simpler list of features wanted that this crate supports. i.e. hand tracking
|
||||||
|
pub exts: XrExtensions,
|
||||||
|
/// List of blend modes the openxr session can use. If [None], pick the first available blend mode.
|
||||||
|
pub blend_modes: Option<Vec<EnvironmentBlendMode>>,
|
||||||
|
/// List of backends the openxr session can use. If [None], pick the first available backend.
|
||||||
|
pub backends: Option<Vec<GraphicsBackend>>,
|
||||||
|
/// List of formats the openxr session can use. If [None], pick the first available format
|
||||||
|
pub formats: Option<Vec<wgpu::TextureFormat>>,
|
||||||
|
/// List of resolutions that the openxr swapchain can use. If [None] pick the first available resolution.
|
||||||
|
pub resolutions: Option<Vec<UVec2>>,
|
||||||
|
/// Passed into the render plugin when added to the app.
|
||||||
|
pub synchronous_pipeline_compilation: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn instance_created(status: Option<Res<XrStatus>>) -> bool {
|
||||||
|
status.is_some_and(|status| status.instance_created)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn session_created(status: Option<Res<XrStatus>>) -> bool {
|
||||||
|
status.is_some_and(|status| status.session_created)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn session_running(status: Option<Res<XrStatus>>) -> bool {
|
||||||
|
status.is_some_and(|status| status.session_running)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn session_ready(status: Option<Res<XrStatus>>) -> bool {
|
||||||
|
status.is_some_and(|status| status.session_ready)
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Plugin for XrInitPlugin {
|
||||||
|
fn build(&self, app: &mut App) {
|
||||||
|
if let Err(e) = init_xr(&self, app) {
|
||||||
|
error!("Failed to initialize openxr instance: {e}.");
|
||||||
|
app.add_plugins(RenderPlugin::default())
|
||||||
|
.insert_resource(XrStatus::UNINITIALIZED);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn xr_entry() -> Result<XrEntry> {
|
||||||
|
#[cfg(windows)]
|
||||||
|
let entry = openxr::Entry::linked();
|
||||||
|
#[cfg(not(windows))]
|
||||||
|
let entry = unsafe { openxr::Entry::load()? };
|
||||||
|
Ok(XrEntry(entry))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn init_xr(config: &XrInitPlugin, app: &mut App) -> Result<()> {
|
||||||
|
let entry = xr_entry()?;
|
||||||
|
|
||||||
|
let available_exts = entry.enumerate_extensions()?;
|
||||||
|
|
||||||
|
// check available extensions and send a warning for any wanted extensions that aren't available.
|
||||||
|
for ext in available_exts.unavailable_exts(&config.exts) {
|
||||||
|
error!(
|
||||||
|
"Extension \"{ext}\" not available in the current OpenXR runtime. Disabling extension."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let available_backends = GraphicsBackend::available_backends(&available_exts);
|
||||||
|
|
||||||
|
// Backend selection
|
||||||
|
let backend = if let Some(wanted_backends) = &config.backends {
|
||||||
|
let mut backend = None;
|
||||||
|
for wanted_backend in wanted_backends {
|
||||||
|
if available_backends.contains(wanted_backend) {
|
||||||
|
backend = Some(*wanted_backend);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
backend
|
||||||
|
} else {
|
||||||
|
available_backends.first().copied()
|
||||||
|
}
|
||||||
|
.ok_or(XrError::NoAvailableBackend)?;
|
||||||
|
|
||||||
|
let exts = config.exts.clone() & available_exts;
|
||||||
|
|
||||||
|
let instance = entry.create_instance(config.app_info.clone(), exts, backend)?;
|
||||||
|
let instance_props = instance.properties()?;
|
||||||
|
|
||||||
|
info!(
|
||||||
|
"Loaded OpenXR runtime: {} {}",
|
||||||
|
instance_props.runtime_name, instance_props.runtime_version
|
||||||
|
);
|
||||||
|
|
||||||
|
let system_id = instance.system(openxr::FormFactor::HEAD_MOUNTED_DISPLAY)?;
|
||||||
|
let system_props = instance.system_properties(system_id)?;
|
||||||
|
|
||||||
|
info!(
|
||||||
|
"Using system: {}",
|
||||||
|
if system_props.system_name.is_empty() {
|
||||||
|
"<unnamed>"
|
||||||
|
} else {
|
||||||
|
&system_props.system_name
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
let (WgpuGraphics(device, queue, adapter_info, adapter, wgpu_instance), create_info) =
|
||||||
|
instance.init_graphics(system_id)?;
|
||||||
|
|
||||||
|
app.world.send_event(XrInstanceCreated);
|
||||||
|
app.add_plugins((
|
||||||
|
RenderPlugin {
|
||||||
|
render_creation: RenderCreation::manual(
|
||||||
|
device.into(),
|
||||||
|
RenderQueue(queue.into()),
|
||||||
|
RenderAdapterInfo(adapter_info),
|
||||||
|
RenderAdapter(adapter.into()),
|
||||||
|
RenderInstance(wgpu_instance.into()),
|
||||||
|
),
|
||||||
|
synchronous_pipeline_compilation: config.synchronous_pipeline_compilation,
|
||||||
|
},
|
||||||
|
ExtractResourcePlugin::<XrTime>::default(),
|
||||||
|
ExtractResourcePlugin::<XrStatus>::default(),
|
||||||
|
))
|
||||||
|
.insert_resource(instance.clone())
|
||||||
|
.insert_resource(SystemId(system_id))
|
||||||
|
.insert_resource(XrStatus {
|
||||||
|
instance_created: true,
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
.insert_non_send_resource(XrSessionInitConfig {
|
||||||
|
blend_modes: config.blend_modes.clone(),
|
||||||
|
formats: config.formats.clone(),
|
||||||
|
resolutions: config.resolutions.clone(),
|
||||||
|
create_info,
|
||||||
|
})
|
||||||
|
.add_systems(
|
||||||
|
First,
|
||||||
|
(
|
||||||
|
poll_events.run_if(instance_created),
|
||||||
|
create_xr_session
|
||||||
|
.run_if(not(session_created))
|
||||||
|
.run_if(on_event::<CreateXrSession>()),
|
||||||
|
begin_xr_session
|
||||||
|
.run_if(session_ready)
|
||||||
|
.run_if(on_event::<BeginXrSession>()),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.sub_app_mut(RenderApp)
|
||||||
|
.insert_resource(instance)
|
||||||
|
.insert_resource(SystemId(system_id))
|
||||||
|
.add_systems(
|
||||||
|
ExtractSchedule,
|
||||||
|
transfer_xr_resources.run_if(not(session_running)),
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// This is used to store information from startup that is needed to create the session after the instance has been created.
|
||||||
|
struct XrSessionInitConfig {
|
||||||
|
/// List of blend modes the openxr session can use. If [None], pick the first available blend mode.
|
||||||
|
blend_modes: Option<Vec<EnvironmentBlendMode>>,
|
||||||
|
/// List of formats the openxr session can use. If [None], pick the first available format
|
||||||
|
formats: Option<Vec<wgpu::TextureFormat>>,
|
||||||
|
/// List of resolutions that the openxr swapchain can use. If [None] pick the first available resolution.
|
||||||
|
resolutions: Option<Vec<UVec2>>,
|
||||||
|
/// Graphics info used to create a session.
|
||||||
|
create_info: SessionCreateInfo,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn create_xr_session(world: &mut World) {
|
||||||
|
let Some(create_info) = world.remove_non_send_resource() else {
|
||||||
|
error!(
|
||||||
|
"Failed to retrive SessionCreateInfo. This is likely due to improper initialization."
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let Some(instance) = world.get_resource().cloned() else {
|
||||||
|
error!("Failed to retrieve XrInstance. This is likely due to improper initialization.");
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let Some(system_id) = world.get_resource::<SystemId>().cloned() else {
|
||||||
|
error!("Failed to retrieve SystemId. THis is likely due to improper initialization");
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Err(e) = init_xr_session(world, instance, *system_id, create_info) {
|
||||||
|
error!("Failed to initialize XrSession: {e}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn init_xr_session(
|
||||||
|
world: &mut World,
|
||||||
|
instance: XrInstance,
|
||||||
|
system_id: openxr::SystemId,
|
||||||
|
config: XrSessionInitConfig,
|
||||||
|
) -> Result<()> {
|
||||||
|
let (session, frame_waiter, frame_stream) =
|
||||||
|
unsafe { instance.create_session(system_id, config.create_info)? };
|
||||||
|
|
||||||
|
// TODO!() support other view configurations
|
||||||
|
let available_view_configurations = instance.enumerate_view_configurations(system_id)?;
|
||||||
|
if !available_view_configurations.contains(&openxr::ViewConfigurationType::PRIMARY_STEREO) {
|
||||||
|
return Err(XrError::NoAvailableViewConfiguration);
|
||||||
|
}
|
||||||
|
|
||||||
|
let view_configuration_type = openxr::ViewConfigurationType::PRIMARY_STEREO;
|
||||||
|
|
||||||
|
let view_configuration_views =
|
||||||
|
instance.enumerate_view_configuration_views(system_id, view_configuration_type)?;
|
||||||
|
|
||||||
|
let (resolution, _view) = if let Some(resolutions) = &config.resolutions {
|
||||||
|
let mut preferred = None;
|
||||||
|
for resolution in resolutions {
|
||||||
|
for view_config in view_configuration_views.iter() {
|
||||||
|
if view_config.recommended_image_rect_height == resolution.y
|
||||||
|
&& view_config.recommended_image_rect_width == resolution.x
|
||||||
|
{
|
||||||
|
preferred = Some((*resolution, *view_config));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if preferred.is_none() {
|
||||||
|
for resolution in resolutions {
|
||||||
|
for view_config in view_configuration_views.iter() {
|
||||||
|
if view_config.max_image_rect_height >= resolution.y
|
||||||
|
&& view_config.max_image_rect_width >= resolution.x
|
||||||
|
{
|
||||||
|
preferred = Some((*resolution, *view_config));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
preferred
|
||||||
|
} else {
|
||||||
|
if let Some(config) = view_configuration_views.first() {
|
||||||
|
Some((
|
||||||
|
uvec2(
|
||||||
|
config.recommended_image_rect_width,
|
||||||
|
config.recommended_image_rect_height,
|
||||||
|
),
|
||||||
|
*config,
|
||||||
|
))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.ok_or(XrError::NoAvailableViewConfiguration)?;
|
||||||
|
|
||||||
|
let available_formats = session.enumerate_swapchain_formats()?;
|
||||||
|
|
||||||
|
let format = if let Some(formats) = &config.formats {
|
||||||
|
let mut format = None;
|
||||||
|
for wanted_format in formats {
|
||||||
|
if available_formats.contains(wanted_format) {
|
||||||
|
format = Some(*wanted_format);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
format
|
||||||
|
} else {
|
||||||
|
available_formats.first().copied()
|
||||||
|
}
|
||||||
|
.ok_or(XrError::NoAvailableFormat)?;
|
||||||
|
|
||||||
|
let mut swapchain = session.create_swapchain(SwapchainCreateInfo {
|
||||||
|
create_flags: SwapchainCreateFlags::EMPTY,
|
||||||
|
usage_flags: SwapchainUsageFlags::COLOR_ATTACHMENT | SwapchainUsageFlags::SAMPLED,
|
||||||
|
format,
|
||||||
|
// TODO() add support for multisampling
|
||||||
|
sample_count: 1,
|
||||||
|
width: resolution.x,
|
||||||
|
height: resolution.y,
|
||||||
|
face_count: 1,
|
||||||
|
array_size: 2,
|
||||||
|
mip_count: 1,
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let images = swapchain.enumerate_images(
|
||||||
|
world.resource::<RenderDevice>().wgpu_device(),
|
||||||
|
format,
|
||||||
|
resolution,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let available_blend_modes =
|
||||||
|
instance.enumerate_environment_blend_modes(system_id, view_configuration_type)?;
|
||||||
|
|
||||||
|
// blend mode selection
|
||||||
|
let blend_mode = if let Some(wanted_blend_modes) = &config.blend_modes {
|
||||||
|
let mut blend_mode = None;
|
||||||
|
for wanted_blend_mode in wanted_blend_modes {
|
||||||
|
if available_blend_modes.contains(wanted_blend_mode) {
|
||||||
|
blend_mode = Some(*wanted_blend_mode);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
blend_mode
|
||||||
|
} else {
|
||||||
|
available_blend_modes.first().copied()
|
||||||
|
}
|
||||||
|
.ok_or(XrError::NoAvailableBackend)?;
|
||||||
|
|
||||||
|
let stage = XrStage(
|
||||||
|
session
|
||||||
|
.create_reference_space(openxr::ReferenceSpaceType::STAGE, openxr::Posef::IDENTITY)?
|
||||||
|
.into(),
|
||||||
|
);
|
||||||
|
|
||||||
|
let graphics_info = XrGraphicsInfo {
|
||||||
|
blend_mode,
|
||||||
|
resolution,
|
||||||
|
format,
|
||||||
|
};
|
||||||
|
|
||||||
|
world.resource_mut::<XrStatus>().session_created = true;
|
||||||
|
world.insert_resource(session.clone());
|
||||||
|
world.insert_resource(frame_waiter);
|
||||||
|
world.insert_resource(images.clone());
|
||||||
|
world.insert_resource(graphics_info.clone());
|
||||||
|
world.insert_resource(stage.clone());
|
||||||
|
world.insert_resource(XrRenderResources {
|
||||||
|
session,
|
||||||
|
frame_stream,
|
||||||
|
swapchain,
|
||||||
|
images,
|
||||||
|
graphics_info,
|
||||||
|
stage,
|
||||||
|
});
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn begin_xr_session(
|
||||||
|
session: Res<XrSession>,
|
||||||
|
mut session_state: EventWriter<XrSessionState>,
|
||||||
|
mut status: ResMut<XrStatus>,
|
||||||
|
) {
|
||||||
|
session
|
||||||
|
.begin(openxr::ViewConfigurationType::PRIMARY_STEREO)
|
||||||
|
.expect("Failed to begin session");
|
||||||
|
status.session_running = true;
|
||||||
|
session_state.send(XrSessionState::Running);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// This is used solely to transport resources from the main world to the render world.
|
||||||
|
#[derive(Resource)]
|
||||||
|
struct XrRenderResources {
|
||||||
|
session: XrSession,
|
||||||
|
frame_stream: XrFrameStream,
|
||||||
|
swapchain: XrSwapchain,
|
||||||
|
images: XrSwapchainImages,
|
||||||
|
graphics_info: XrGraphicsInfo,
|
||||||
|
stage: XrStage,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn transfer_xr_resources(mut commands: Commands, mut world: ResMut<MainWorld>) {
|
||||||
|
let Some(XrRenderResources {
|
||||||
|
session,
|
||||||
|
frame_stream,
|
||||||
|
swapchain,
|
||||||
|
images,
|
||||||
|
graphics_info,
|
||||||
|
stage,
|
||||||
|
}) = world.remove_resource()
|
||||||
|
else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
commands.insert_resource(session);
|
||||||
|
commands.insert_resource(frame_stream);
|
||||||
|
commands.insert_resource(swapchain);
|
||||||
|
commands.insert_resource(images);
|
||||||
|
commands.insert_resource(graphics_info);
|
||||||
|
commands.insert_resource(stage);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn poll_events(
|
||||||
|
instance: Res<XrInstance>,
|
||||||
|
session: Option<Res<XrSession>>,
|
||||||
|
mut session_state: EventWriter<XrSessionState>,
|
||||||
|
mut instance_destroyed: EventWriter<XrInstanceDestroyed>,
|
||||||
|
mut status: ResMut<XrStatus>,
|
||||||
|
) {
|
||||||
|
let mut buffer = Default::default();
|
||||||
|
while let Some(event) = instance
|
||||||
|
.poll_event(&mut buffer)
|
||||||
|
.expect("Failed to poll event")
|
||||||
|
{
|
||||||
|
use openxr::Event::*;
|
||||||
|
match event {
|
||||||
|
SessionStateChanged(e) => {
|
||||||
|
if let Some(ref session) = session {
|
||||||
|
info!("entered XR state {:?}", e.state());
|
||||||
|
use openxr::SessionState;
|
||||||
|
|
||||||
|
match e.state() {
|
||||||
|
SessionState::IDLE => {
|
||||||
|
status.session_ready = false;
|
||||||
|
session_state.send(XrSessionState::Idle);
|
||||||
|
}
|
||||||
|
SessionState::READY => {
|
||||||
|
status.session_ready = true;
|
||||||
|
session_state.send(XrSessionState::Ready);
|
||||||
|
}
|
||||||
|
SessionState::STOPPING => {
|
||||||
|
status.session_running = false;
|
||||||
|
status.session_ready = false;
|
||||||
|
session.end().expect("Failed to end session");
|
||||||
|
session_state.send(XrSessionState::Stopping);
|
||||||
|
}
|
||||||
|
// TODO: figure out how to destroy the session
|
||||||
|
SessionState::EXITING | SessionState::LOSS_PENDING => {
|
||||||
|
status.session_running = false;
|
||||||
|
status.session_created = false;
|
||||||
|
status.session_ready = false;
|
||||||
|
session.end().expect("Failed to end session");
|
||||||
|
session_state.send(XrSessionState::Destroyed);
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
InstanceLossPending(_) => {
|
||||||
|
*status = XrStatus::UNINITIALIZED;
|
||||||
|
instance_destroyed.send_default();
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
169
crates/bevy_openxr/src/layer_builder.rs
Normal file
169
crates/bevy_openxr/src/layer_builder.rs
Normal file
@@ -0,0 +1,169 @@
|
|||||||
|
use std::mem;
|
||||||
|
|
||||||
|
use openxr::{sys, CompositionLayerFlags, Fovf, Posef, Rect2Di, Space};
|
||||||
|
|
||||||
|
use crate::graphics::graphics_match;
|
||||||
|
use crate::resources::XrSwapchain;
|
||||||
|
|
||||||
|
#[derive(Copy, Clone)]
|
||||||
|
pub struct SwapchainSubImage<'a> {
|
||||||
|
inner: sys::SwapchainSubImage,
|
||||||
|
swapchain: Option<&'a XrSwapchain>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> SwapchainSubImage<'a> {
|
||||||
|
#[inline]
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
inner: sys::SwapchainSubImage {
|
||||||
|
..unsafe { mem::zeroed() }
|
||||||
|
},
|
||||||
|
swapchain: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[inline]
|
||||||
|
pub fn into_raw(self) -> sys::SwapchainSubImage {
|
||||||
|
self.inner
|
||||||
|
}
|
||||||
|
#[inline]
|
||||||
|
pub fn as_raw(&self) -> &sys::SwapchainSubImage {
|
||||||
|
&self.inner
|
||||||
|
}
|
||||||
|
#[inline]
|
||||||
|
pub fn swapchain(mut self, value: &'a XrSwapchain) -> Self {
|
||||||
|
graphics_match!(
|
||||||
|
&value.0;
|
||||||
|
swap => self.inner.swapchain = swap.as_raw()
|
||||||
|
);
|
||||||
|
self.swapchain = Some(value);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
#[inline]
|
||||||
|
pub fn image_rect(mut self, value: Rect2Di) -> Self {
|
||||||
|
self.inner.image_rect = value;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
#[inline]
|
||||||
|
pub fn image_array_index(mut self, value: u32) -> Self {
|
||||||
|
self.inner.image_array_index = value;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Default for SwapchainSubImage<'a> {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Copy, Clone)]
|
||||||
|
pub struct CompositionLayerProjectionView<'a> {
|
||||||
|
inner: sys::CompositionLayerProjectionView,
|
||||||
|
swapchain: Option<&'a XrSwapchain>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> CompositionLayerProjectionView<'a> {
|
||||||
|
#[inline]
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
inner: sys::CompositionLayerProjectionView {
|
||||||
|
ty: sys::StructureType::COMPOSITION_LAYER_PROJECTION_VIEW,
|
||||||
|
..unsafe { mem::zeroed() }
|
||||||
|
},
|
||||||
|
swapchain: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[inline]
|
||||||
|
pub fn into_raw(self) -> sys::CompositionLayerProjectionView {
|
||||||
|
self.inner
|
||||||
|
}
|
||||||
|
#[inline]
|
||||||
|
pub fn as_raw(&self) -> &sys::CompositionLayerProjectionView {
|
||||||
|
&self.inner
|
||||||
|
}
|
||||||
|
#[inline]
|
||||||
|
pub fn pose(mut self, value: Posef) -> Self {
|
||||||
|
self.inner.pose = value;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
#[inline]
|
||||||
|
pub fn fov(mut self, value: Fovf) -> Self {
|
||||||
|
self.inner.fov = value;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
#[inline]
|
||||||
|
pub fn sub_image(mut self, value: SwapchainSubImage<'a>) -> Self {
|
||||||
|
self.inner.sub_image = value.inner;
|
||||||
|
self.swapchain = value.swapchain;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl<'a> Default for CompositionLayerProjectionView<'a> {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub unsafe trait CompositionLayer<'a> {
|
||||||
|
fn swapchain(&self) -> Option<&'a XrSwapchain>;
|
||||||
|
fn header(&self) -> &'a sys::CompositionLayerBaseHeader;
|
||||||
|
}
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct CompositionLayerProjection<'a> {
|
||||||
|
inner: sys::CompositionLayerProjection,
|
||||||
|
swapchain: Option<&'a XrSwapchain>,
|
||||||
|
views: Vec<sys::CompositionLayerProjectionView>,
|
||||||
|
}
|
||||||
|
impl<'a> CompositionLayerProjection<'a> {
|
||||||
|
#[inline]
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
inner: sys::CompositionLayerProjection {
|
||||||
|
ty: sys::StructureType::COMPOSITION_LAYER_PROJECTION,
|
||||||
|
..unsafe { mem::zeroed() }
|
||||||
|
},
|
||||||
|
swapchain: None,
|
||||||
|
views: Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[inline]
|
||||||
|
pub fn into_raw(self) -> sys::CompositionLayerProjection {
|
||||||
|
self.inner
|
||||||
|
}
|
||||||
|
#[inline]
|
||||||
|
pub fn as_raw(&self) -> &sys::CompositionLayerProjection {
|
||||||
|
&self.inner
|
||||||
|
}
|
||||||
|
#[inline]
|
||||||
|
pub fn layer_flags(mut self, value: CompositionLayerFlags) -> Self {
|
||||||
|
self.inner.layer_flags = value;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
#[inline]
|
||||||
|
pub fn space(mut self, value: &'a Space) -> Self {
|
||||||
|
self.inner.space = value.as_raw();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
#[inline]
|
||||||
|
pub fn views(mut self, value: &'a [CompositionLayerProjectionView<'a>]) -> Self {
|
||||||
|
for view in value {
|
||||||
|
self.views.push(view.inner.clone());
|
||||||
|
}
|
||||||
|
self.inner.views = self.views.as_slice().as_ptr() as *const _ as _;
|
||||||
|
self.inner.view_count = value.len() as u32;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
unsafe impl<'a> CompositionLayer<'a> for CompositionLayerProjection<'a> {
|
||||||
|
fn swapchain(&self) -> Option<&'a XrSwapchain> {
|
||||||
|
self.swapchain
|
||||||
|
}
|
||||||
|
|
||||||
|
fn header(&self) -> &'a sys::CompositionLayerBaseHeader {
|
||||||
|
unsafe { std::mem::transmute(&self.inner) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl<'a> Default for CompositionLayerProjection<'a> {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
36
crates/bevy_openxr/src/lib.rs
Normal file
36
crates/bevy_openxr/src/lib.rs
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
use bevy::{
|
||||||
|
app::{PluginGroup, PluginGroupBuilder},
|
||||||
|
render::RenderPlugin,
|
||||||
|
utils::default,
|
||||||
|
};
|
||||||
|
use bevy_xr::camera::XrCameraPlugin;
|
||||||
|
use init::XrInitPlugin;
|
||||||
|
use render::XrRenderPlugin;
|
||||||
|
|
||||||
|
pub mod camera;
|
||||||
|
pub mod error;
|
||||||
|
pub mod extensions;
|
||||||
|
pub mod graphics;
|
||||||
|
pub mod init;
|
||||||
|
pub mod layer_builder;
|
||||||
|
pub mod render;
|
||||||
|
pub mod resources;
|
||||||
|
pub mod types;
|
||||||
|
|
||||||
|
pub fn add_xr_plugins<G: PluginGroup>(plugins: G) -> PluginGroupBuilder {
|
||||||
|
plugins
|
||||||
|
.build()
|
||||||
|
.disable::<RenderPlugin>()
|
||||||
|
.add_before::<RenderPlugin, _>(bevy_xr::session::XrSessionPlugin)
|
||||||
|
.add_before::<RenderPlugin, _>(XrInitPlugin {
|
||||||
|
app_info: default(),
|
||||||
|
exts: default(),
|
||||||
|
blend_modes: default(),
|
||||||
|
backends: default(),
|
||||||
|
formats: Some(vec![wgpu::TextureFormat::Rgba8UnormSrgb]),
|
||||||
|
resolutions: default(),
|
||||||
|
synchronous_pipeline_compilation: default(),
|
||||||
|
})
|
||||||
|
.add(XrRenderPlugin)
|
||||||
|
.add(XrCameraPlugin)
|
||||||
|
}
|
||||||
332
crates/bevy_openxr/src/render.rs
Normal file
332
crates/bevy_openxr/src/render.rs
Normal file
@@ -0,0 +1,332 @@
|
|||||||
|
use bevy::{
|
||||||
|
prelude::*,
|
||||||
|
render::{
|
||||||
|
camera::{ManualTextureView, ManualTextureViewHandle, ManualTextureViews, RenderTarget},
|
||||||
|
renderer::render_system,
|
||||||
|
Render, RenderApp, RenderSet,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
use bevy_xr::camera::{XrCameraBundle, XrProjection, XrView};
|
||||||
|
use openxr::CompositionLayerFlags;
|
||||||
|
|
||||||
|
use crate::layer_builder::*;
|
||||||
|
use crate::resources::*;
|
||||||
|
|
||||||
|
use crate::init::session_running;
|
||||||
|
|
||||||
|
pub struct XrRenderPlugin;
|
||||||
|
|
||||||
|
impl Plugin for XrRenderPlugin {
|
||||||
|
fn build(&self, app: &mut App) {
|
||||||
|
app.add_systems(
|
||||||
|
PreUpdate,
|
||||||
|
(
|
||||||
|
init_views.run_if(resource_added::<XrGraphicsInfo>),
|
||||||
|
wait_frame.run_if(session_running),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
// .add_systems(Startup, init_views);
|
||||||
|
app.sub_app_mut(RenderApp).add_systems(
|
||||||
|
Render,
|
||||||
|
(
|
||||||
|
(begin_frame, insert_texture_views)
|
||||||
|
.chain()
|
||||||
|
.in_set(RenderSet::PrepareAssets)
|
||||||
|
.before(render_system),
|
||||||
|
end_frame.in_set(RenderSet::Cleanup),
|
||||||
|
)
|
||||||
|
.run_if(session_running),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const XR_TEXTURE_INDEX: u32 = 3383858418;
|
||||||
|
|
||||||
|
/// This is needed to properly initialize the texture views so that bevy will set them to the correct resolution despite them being updated in the render world.
|
||||||
|
pub fn init_views(
|
||||||
|
graphics_info: Res<XrGraphicsInfo>,
|
||||||
|
mut manual_texture_views: ResMut<ManualTextureViews>,
|
||||||
|
swapchain_images: Res<XrSwapchainImages>,
|
||||||
|
mut commands: Commands,
|
||||||
|
) {
|
||||||
|
let temp_tex = swapchain_images.first().unwrap();
|
||||||
|
// this for loop is to easily add support for quad or mono views in the future.
|
||||||
|
let mut views = vec![];
|
||||||
|
for index in 0..2 {
|
||||||
|
info!("{}", graphics_info.resolution);
|
||||||
|
let view_handle =
|
||||||
|
add_texture_view(&mut manual_texture_views, temp_tex, &graphics_info, index);
|
||||||
|
|
||||||
|
let entity = commands
|
||||||
|
.spawn(XrCameraBundle {
|
||||||
|
camera: Camera {
|
||||||
|
target: RenderTarget::TextureView(view_handle),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
.id();
|
||||||
|
views.push(entity);
|
||||||
|
}
|
||||||
|
commands.insert_resource(XrViews(views));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn wait_frame(mut frame_waiter: ResMut<XrFrameWaiter>, mut commands: Commands) {
|
||||||
|
let state = frame_waiter.wait().expect("Failed to wait frame");
|
||||||
|
commands.insert_resource(XrTime(openxr::Time::from_nanos(
|
||||||
|
state.predicted_display_time.as_nanos() + state.predicted_display_period.as_nanos(),
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update_views(
|
||||||
|
mut views: Query<(&mut Transform, &mut XrProjection), With<XrView>>,
|
||||||
|
view_entities: Res<XrViews>,
|
||||||
|
session: Res<XrSession>,
|
||||||
|
stage: Res<XrStage>,
|
||||||
|
time: Res<XrTime>,
|
||||||
|
) {
|
||||||
|
let (_flags, xr_views) = session
|
||||||
|
.locate_views(
|
||||||
|
openxr::ViewConfigurationType::PRIMARY_STEREO,
|
||||||
|
**time,
|
||||||
|
&stage,
|
||||||
|
)
|
||||||
|
.expect("Failed to locate views");
|
||||||
|
|
||||||
|
for (i, view) in xr_views.iter().enumerate() {
|
||||||
|
if let Some((mut transform, mut projection)) = view_entities
|
||||||
|
.0
|
||||||
|
.get(i)
|
||||||
|
.and_then(|entity| views.get_mut(*entity).ok())
|
||||||
|
{
|
||||||
|
let projection_matrix = calculate_projection(projection.near, view.fov);
|
||||||
|
projection.projection_matrix = projection_matrix;
|
||||||
|
|
||||||
|
let openxr::Quaternionf { x, y, z, w } = view.pose.orientation;
|
||||||
|
let rotation = Quat::from_xyzw(x, y, z, w);
|
||||||
|
transform.rotation = rotation;
|
||||||
|
let openxr::Vector3f { x, y, z } = view.pose.position;
|
||||||
|
let translation = Vec3::new(x, y, z);
|
||||||
|
transform.translation = translation;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn calculate_projection(near_z: f32, fov: openxr::Fovf) -> 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 is_vulkan_api = false; // FIXME wgpu probably abstracts this
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn begin_frame(mut frame_stream: ResMut<XrFrameStream>) {
|
||||||
|
frame_stream.begin().expect("Failed to begin frame");
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn insert_texture_views(
|
||||||
|
swapchain_images: Res<XrSwapchainImages>,
|
||||||
|
mut swapchain: ResMut<XrSwapchain>,
|
||||||
|
mut manual_texture_views: ResMut<ManualTextureViews>,
|
||||||
|
graphics_info: Res<XrGraphicsInfo>,
|
||||||
|
) {
|
||||||
|
let index = swapchain.acquire_image().expect("Failed to acquire image");
|
||||||
|
swapchain
|
||||||
|
.wait_image(openxr::Duration::INFINITE)
|
||||||
|
.expect("Failed to wait image");
|
||||||
|
let image = &swapchain_images[index as usize];
|
||||||
|
|
||||||
|
for i in 0..2 {
|
||||||
|
add_texture_view(&mut manual_texture_views, image, &graphics_info, i);
|
||||||
|
}
|
||||||
|
// let left = image.create_view(&wgpu::TextureViewDescriptor {
|
||||||
|
// dimension: Some(wgpu::TextureViewDimension::D2),
|
||||||
|
// array_layer_count: Some(1),
|
||||||
|
// ..Default::default()
|
||||||
|
// });
|
||||||
|
// let right = image.create_view(&wgpu::TextureViewDescriptor {
|
||||||
|
// dimension: Some(wgpu::TextureViewDimension::D2),
|
||||||
|
// array_layer_count: Some(1),
|
||||||
|
// base_array_layer: 1,
|
||||||
|
// ..Default::default()
|
||||||
|
// });
|
||||||
|
// let resolution = graphics_info.resolution;
|
||||||
|
// let format = graphics_info.format;
|
||||||
|
// let left = ManualTextureView {
|
||||||
|
// texture_view: left.into(),
|
||||||
|
// size: resolution,
|
||||||
|
// format: format,
|
||||||
|
// };
|
||||||
|
// let right = ManualTextureView {
|
||||||
|
// texture_view: right.into(),
|
||||||
|
// size: resolution,
|
||||||
|
// format: format,
|
||||||
|
// };
|
||||||
|
// manual_texture_views.insert(LEFT_XR_TEXTURE_HANDLE, left);
|
||||||
|
// manual_texture_views.insert(RIGHT_XR_TEXTURE_HANDLE, right);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_texture_view(
|
||||||
|
manual_texture_views: &mut ManualTextureViews,
|
||||||
|
texture: &wgpu::Texture,
|
||||||
|
info: &XrGraphicsInfo,
|
||||||
|
index: u32,
|
||||||
|
) -> ManualTextureViewHandle {
|
||||||
|
let view = texture.create_view(&wgpu::TextureViewDescriptor {
|
||||||
|
dimension: Some(wgpu::TextureViewDimension::D2),
|
||||||
|
array_layer_count: Some(1),
|
||||||
|
base_array_layer: index,
|
||||||
|
..default()
|
||||||
|
});
|
||||||
|
let view = ManualTextureView {
|
||||||
|
texture_view: view.into(),
|
||||||
|
size: info.resolution,
|
||||||
|
format: info.format,
|
||||||
|
};
|
||||||
|
let handle = ManualTextureViewHandle(XR_TEXTURE_INDEX + index);
|
||||||
|
manual_texture_views.insert(handle, view);
|
||||||
|
handle
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn end_frame(
|
||||||
|
mut frame_stream: ResMut<XrFrameStream>,
|
||||||
|
session: Res<XrSession>,
|
||||||
|
mut swapchain: ResMut<XrSwapchain>,
|
||||||
|
stage: Res<XrStage>,
|
||||||
|
display_time: Res<XrTime>,
|
||||||
|
graphics_info: Res<XrGraphicsInfo>,
|
||||||
|
) {
|
||||||
|
swapchain.release_image().unwrap();
|
||||||
|
let (_flags, views) = session
|
||||||
|
.locate_views(
|
||||||
|
openxr::ViewConfigurationType::PRIMARY_STEREO,
|
||||||
|
**display_time,
|
||||||
|
&stage,
|
||||||
|
)
|
||||||
|
.expect("Failed to locate views");
|
||||||
|
|
||||||
|
let rect = openxr::Rect2Di {
|
||||||
|
offset: openxr::Offset2Di { x: 0, y: 0 },
|
||||||
|
extent: openxr::Extent2Di {
|
||||||
|
width: graphics_info.resolution.x as _,
|
||||||
|
height: graphics_info.resolution.y as _,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
frame_stream
|
||||||
|
.end(
|
||||||
|
**display_time,
|
||||||
|
graphics_info.blend_mode,
|
||||||
|
&[&CompositionLayerProjection::new()
|
||||||
|
.layer_flags(CompositionLayerFlags::BLEND_TEXTURE_SOURCE_ALPHA)
|
||||||
|
.space(&stage)
|
||||||
|
.views(&[
|
||||||
|
CompositionLayerProjectionView::new()
|
||||||
|
.pose(views[0].pose)
|
||||||
|
.fov(views[0].fov)
|
||||||
|
.sub_image(
|
||||||
|
SwapchainSubImage::new()
|
||||||
|
.swapchain(&swapchain)
|
||||||
|
.image_array_index(0)
|
||||||
|
.image_rect(rect),
|
||||||
|
),
|
||||||
|
CompositionLayerProjectionView::new()
|
||||||
|
.pose(views[1].pose)
|
||||||
|
.fov(views[1].fov)
|
||||||
|
.sub_image(
|
||||||
|
SwapchainSubImage::new()
|
||||||
|
.swapchain(&swapchain)
|
||||||
|
.image_array_index(1)
|
||||||
|
.image_rect(rect),
|
||||||
|
),
|
||||||
|
])],
|
||||||
|
)
|
||||||
|
.expect("Failed to end stream");
|
||||||
|
}
|
||||||
@@ -1,13 +1,13 @@
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use crate::error::XrError;
|
||||||
|
use crate::graphics::*;
|
||||||
|
use crate::layer_builder::CompositionLayer;
|
||||||
|
use crate::types::*;
|
||||||
use bevy::prelude::*;
|
use bevy::prelude::*;
|
||||||
|
|
||||||
use bevy::render::extract_resource::ExtractResource;
|
use bevy::render::extract_resource::ExtractResource;
|
||||||
use openxr::AnyGraphics;
|
use openxr::AnyGraphics;
|
||||||
|
|
||||||
use super::graphics::{graphics_match, GraphicsExt, GraphicsType, GraphicsWrap};
|
|
||||||
use super::types::*;
|
|
||||||
|
|
||||||
#[derive(Deref, Clone)]
|
#[derive(Deref, Clone)]
|
||||||
pub struct XrEntry(pub openxr::Entry);
|
pub struct XrEntry(pub openxr::Entry);
|
||||||
|
|
||||||
@@ -51,16 +51,6 @@ impl XrEntry {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Copy, Clone, Deref, Default, Eq, PartialEq, Ord, PartialOrd, Hash, Resource)]
|
|
||||||
pub struct SystemId(pub openxr::SystemId);
|
|
||||||
|
|
||||||
#[derive(Debug, Copy, Clone, Eq, PartialEq, Resource)]
|
|
||||||
pub struct XrGraphicsInfo {
|
|
||||||
pub blend_mode: EnvironmentBlendMode,
|
|
||||||
pub swapchain_resolution: UVec2,
|
|
||||||
pub swapchain_format: wgpu::TextureFormat,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Resource, Deref, Clone)]
|
#[derive(Resource, Deref, Clone)]
|
||||||
pub struct XrInstance(
|
pub struct XrInstance(
|
||||||
#[deref] pub openxr::Instance,
|
#[deref] pub openxr::Instance,
|
||||||
@@ -229,7 +219,7 @@ impl XrSwapchain {
|
|||||||
device: &wgpu::Device,
|
device: &wgpu::Device,
|
||||||
format: wgpu::TextureFormat,
|
format: wgpu::TextureFormat,
|
||||||
resolution: UVec2,
|
resolution: UVec2,
|
||||||
) -> Result<SwapchainImages> {
|
) -> Result<XrSwapchainImages> {
|
||||||
graphics_match!(
|
graphics_match!(
|
||||||
&mut self.0;
|
&mut self.0;
|
||||||
swap => {
|
swap => {
|
||||||
@@ -239,7 +229,7 @@ impl XrSwapchain {
|
|||||||
images.push(Api::to_wgpu_img(image, device, format, resolution)?);
|
images.push(Api::to_wgpu_img(image, device, format, resolution)?);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(SwapchainImages(images.into()))
|
Ok(XrSwapchainImages(images.into()))
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -249,14 +239,43 @@ impl XrSwapchain {
|
|||||||
pub struct XrStage(pub Arc<openxr::Space>);
|
pub struct XrStage(pub Arc<openxr::Space>);
|
||||||
|
|
||||||
#[derive(Debug, Deref, Resource, Clone)]
|
#[derive(Debug, Deref, Resource, Clone)]
|
||||||
pub struct SwapchainImages(pub Arc<Vec<wgpu::Texture>>);
|
pub struct XrSwapchainImages(pub Arc<Vec<wgpu::Texture>>);
|
||||||
|
|
||||||
#[derive(Copy, Clone, Eq, PartialEq, Deref, DerefMut, Resource, ExtractResource)]
|
#[derive(Copy, Clone, Eq, PartialEq, Deref, DerefMut, Resource, ExtractResource)]
|
||||||
pub struct XrTime(pub openxr::Time);
|
pub struct XrTime(pub openxr::Time);
|
||||||
|
|
||||||
#[derive(Clone, Copy, Eq, PartialEq, Default, Resource, ExtractResource)]
|
#[derive(Copy, Clone, Eq, PartialEq, Resource)]
|
||||||
pub enum XrStatus {
|
pub struct XrSwapchainInfo {
|
||||||
Enabled,
|
pub format: wgpu::TextureFormat,
|
||||||
#[default]
|
pub resolution: UVec2,
|
||||||
Disabled,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Copy, Clone, Deref, Default, Eq, PartialEq, Ord, PartialOrd, Hash, Resource)]
|
||||||
|
pub struct SystemId(pub openxr::SystemId);
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Eq, PartialEq, Default, Resource, ExtractResource)]
|
||||||
|
pub struct XrStatus {
|
||||||
|
pub instance_created: bool,
|
||||||
|
pub session_created: bool,
|
||||||
|
pub session_ready: bool,
|
||||||
|
pub session_running: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl XrStatus {
|
||||||
|
pub const UNINITIALIZED: Self = Self {
|
||||||
|
instance_created: false,
|
||||||
|
session_created: false,
|
||||||
|
session_ready: false,
|
||||||
|
session_running: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Resource)]
|
||||||
|
pub struct XrGraphicsInfo {
|
||||||
|
pub blend_mode: EnvironmentBlendMode,
|
||||||
|
pub resolution: UVec2,
|
||||||
|
pub format: wgpu::TextureFormat,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Resource)]
|
||||||
|
pub struct XrViews(pub Vec<Entity>);
|
||||||
77
crates/bevy_openxr/src/types.rs
Normal file
77
crates/bevy_openxr/src/types.rs
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
use std::borrow::Cow;
|
||||||
|
|
||||||
|
use crate::error::XrError;
|
||||||
|
pub use crate::extensions::XrExtensions;
|
||||||
|
use crate::graphics::GraphicsExt;
|
||||||
|
|
||||||
|
pub use openxr::{EnvironmentBlendMode, SwapchainCreateFlags, SwapchainUsageFlags};
|
||||||
|
|
||||||
|
pub type Result<T> = std::result::Result<T, XrError>;
|
||||||
|
|
||||||
|
pub struct WgpuGraphics(
|
||||||
|
pub wgpu::Device,
|
||||||
|
pub wgpu::Queue,
|
||||||
|
pub wgpu::AdapterInfo,
|
||||||
|
pub wgpu::Adapter,
|
||||||
|
pub wgpu::Instance,
|
||||||
|
);
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, Default, PartialEq)]
|
||||||
|
pub struct Version(pub u8, pub u8, pub u16);
|
||||||
|
|
||||||
|
impl Version {
|
||||||
|
pub const BEVY: Self = Self(0, 12, 1);
|
||||||
|
|
||||||
|
pub const fn to_u32(self) -> u32 {
|
||||||
|
let major = (self.0 as u32) << 24;
|
||||||
|
let minor = (self.1 as u32) << 16;
|
||||||
|
self.2 as u32 | major | minor
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
|
pub struct AppInfo {
|
||||||
|
pub name: Cow<'static, str>,
|
||||||
|
pub version: Version,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for AppInfo {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
name: "Bevy".into(),
|
||||||
|
version: Version::BEVY,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Copy, Clone)]
|
||||||
|
pub struct SwapchainCreateInfo {
|
||||||
|
pub create_flags: SwapchainCreateFlags,
|
||||||
|
pub usage_flags: SwapchainUsageFlags,
|
||||||
|
pub format: wgpu::TextureFormat,
|
||||||
|
pub sample_count: u32,
|
||||||
|
pub width: u32,
|
||||||
|
pub height: u32,
|
||||||
|
pub face_count: u32,
|
||||||
|
pub array_size: u32,
|
||||||
|
pub mip_count: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<G: GraphicsExt> TryFrom<SwapchainCreateInfo> for openxr::SwapchainCreateInfo<G> {
|
||||||
|
type Error = XrError;
|
||||||
|
|
||||||
|
fn try_from(value: SwapchainCreateInfo) -> Result<Self> {
|
||||||
|
Ok(openxr::SwapchainCreateInfo {
|
||||||
|
create_flags: value.create_flags,
|
||||||
|
usage_flags: value.usage_flags,
|
||||||
|
format: G::from_wgpu_format(value.format)
|
||||||
|
.ok_or(XrError::UnsupportedTextureFormat(value.format))?,
|
||||||
|
sample_count: value.sample_count,
|
||||||
|
width: value.width,
|
||||||
|
height: value.height,
|
||||||
|
face_count: value.face_count,
|
||||||
|
array_size: value.array_size,
|
||||||
|
mip_count: value.mip_count,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
9
crates/bevy_xr/Cargo.toml
Normal file
9
crates/bevy_xr/Cargo.toml
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
[package]
|
||||||
|
name = "bevy_xr"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
bevy.workspace = true
|
||||||
102
crates/bevy_xr/src/camera.rs
Normal file
102
crates/bevy_xr/src/camera.rs
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
use bevy::app::{App, Plugin};
|
||||||
|
use bevy::core_pipeline::core_3d::graph::Core3d;
|
||||||
|
use bevy::core_pipeline::core_3d::Camera3d;
|
||||||
|
use bevy::core_pipeline::tonemapping::{DebandDither, Tonemapping};
|
||||||
|
use bevy::ecs::bundle::Bundle;
|
||||||
|
use bevy::ecs::component::Component;
|
||||||
|
use bevy::ecs::reflect::ReflectComponent;
|
||||||
|
use bevy::math::{Mat4, Vec3A};
|
||||||
|
use bevy::reflect::Reflect;
|
||||||
|
use bevy::render::camera::{
|
||||||
|
Camera, CameraMainTextureUsages, CameraProjection, CameraProjectionPlugin, CameraRenderGraph,
|
||||||
|
Exposure,
|
||||||
|
};
|
||||||
|
use bevy::render::primitives::Frustum;
|
||||||
|
use bevy::render::view::{ColorGrading, VisibleEntities};
|
||||||
|
use bevy::transform::components::{GlobalTransform, Transform};
|
||||||
|
|
||||||
|
pub struct XrCameraPlugin;
|
||||||
|
|
||||||
|
impl Plugin for XrCameraPlugin {
|
||||||
|
fn build(&self, app: &mut App) {
|
||||||
|
app.add_plugins(CameraProjectionPlugin::<XrProjection>::default());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Component, Reflect, Debug)]
|
||||||
|
#[reflect(Component)]
|
||||||
|
pub struct XrProjection {
|
||||||
|
pub projection_matrix: Mat4,
|
||||||
|
pub near: f32,
|
||||||
|
pub far: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for XrProjection {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
near: 0.1,
|
||||||
|
far: 1000.,
|
||||||
|
projection_matrix: Mat4::IDENTITY,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Marker component for an XR view. It is the backends responsibility to update this.
|
||||||
|
#[derive(Clone, Copy, Component, Debug, Default)]
|
||||||
|
pub struct XrView;
|
||||||
|
|
||||||
|
impl CameraProjection for XrProjection {
|
||||||
|
fn get_projection_matrix(&self) -> Mat4 {
|
||||||
|
self.projection_matrix
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update(&mut self, _width: f32, _height: f32) {}
|
||||||
|
|
||||||
|
fn far(&self) -> f32 {
|
||||||
|
self.far
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO calculate this properly
|
||||||
|
fn get_frustum_corners(&self, _z_near: f32, _z_far: f32) -> [Vec3A; 8] {
|
||||||
|
Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Bundle)]
|
||||||
|
pub struct XrCameraBundle {
|
||||||
|
pub camera: Camera,
|
||||||
|
pub camera_render_graph: CameraRenderGraph,
|
||||||
|
pub 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 exposure: Exposure,
|
||||||
|
pub main_texture_usages: CameraMainTextureUsages,
|
||||||
|
pub view: XrView,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for XrCameraBundle {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
camera_render_graph: CameraRenderGraph::new(Core3d),
|
||||||
|
camera: Default::default(),
|
||||||
|
projection: Default::default(),
|
||||||
|
visible_entities: Default::default(),
|
||||||
|
frustum: Default::default(),
|
||||||
|
transform: Default::default(),
|
||||||
|
global_transform: Default::default(),
|
||||||
|
camera_3d: Default::default(),
|
||||||
|
tonemapping: Default::default(),
|
||||||
|
color_grading: Default::default(),
|
||||||
|
exposure: Default::default(),
|
||||||
|
main_texture_usages: Default::default(),
|
||||||
|
dither: DebandDither::Enabled,
|
||||||
|
view: XrView,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
2
crates/bevy_xr/src/lib.rs
Normal file
2
crates/bevy_xr/src/lib.rs
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
pub mod camera;
|
||||||
|
pub mod session;
|
||||||
89
crates/bevy_xr/src/session.rs
Normal file
89
crates/bevy_xr/src/session.rs
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
use bevy::app::{App, Plugin, PreUpdate};
|
||||||
|
use bevy::ecs::event::{Event, EventReader, EventWriter};
|
||||||
|
use bevy::ecs::system::Local;
|
||||||
|
|
||||||
|
pub struct XrSessionPlugin;
|
||||||
|
|
||||||
|
impl Plugin for XrSessionPlugin {
|
||||||
|
fn build(&self, app: &mut App) {
|
||||||
|
app.add_event::<CreateXrSession>()
|
||||||
|
.add_event::<BeginXrSession>()
|
||||||
|
.add_event::<EndXrSession>()
|
||||||
|
.add_event::<XrSessionState>()
|
||||||
|
.add_event::<XrInstanceCreated>()
|
||||||
|
.add_event::<XrInstanceDestroyed>()
|
||||||
|
.add_systems(PreUpdate, handle_xr_events);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn handle_xr_events(
|
||||||
|
mut instance_created: EventReader<XrInstanceCreated>,
|
||||||
|
mut session_state: EventReader<XrSessionState>,
|
||||||
|
mut instance_destroyed: EventReader<XrInstanceDestroyed>,
|
||||||
|
mut create_session: EventWriter<CreateXrSession>,
|
||||||
|
mut begin_session: EventWriter<BeginXrSession>,
|
||||||
|
mut has_instance: Local<bool>,
|
||||||
|
mut local_session_state: Local<Option<XrSessionState>>,
|
||||||
|
) {
|
||||||
|
// Don't do anything if no events recieved
|
||||||
|
if instance_created.is_empty() && instance_destroyed.is_empty() && session_state.is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if !instance_created.is_empty() {
|
||||||
|
*has_instance = true;
|
||||||
|
instance_created.clear();
|
||||||
|
}
|
||||||
|
if !instance_destroyed.is_empty() {
|
||||||
|
*has_instance = false;
|
||||||
|
instance_destroyed.clear();
|
||||||
|
}
|
||||||
|
for state in session_state.read() {
|
||||||
|
*local_session_state = Some(*state);
|
||||||
|
}
|
||||||
|
if *has_instance {
|
||||||
|
if local_session_state.is_none() {
|
||||||
|
create_session.send_default();
|
||||||
|
} else if matches!(*local_session_state, Some(XrSessionState::Ready)) {
|
||||||
|
begin_session.send_default();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Event sent to backends to create an XR session
|
||||||
|
#[derive(Event, Clone, Copy, Default)]
|
||||||
|
pub struct CreateXrSession;
|
||||||
|
|
||||||
|
/// Event sent to backends to begin an XR session
|
||||||
|
#[derive(Event, Clone, Copy, Default)]
|
||||||
|
pub struct BeginXrSession;
|
||||||
|
|
||||||
|
/// Event sent to backends to end an XR session.
|
||||||
|
#[derive(Event, Clone, Copy, Default)]
|
||||||
|
pub struct EndXrSession;
|
||||||
|
|
||||||
|
// /// Event sent to backends to destroy an XR session.
|
||||||
|
// #[derive(Event, Clone, Copy, Default)]
|
||||||
|
// pub struct DestroyXrSession;
|
||||||
|
|
||||||
|
/// Event sent from backends to inform the frontend of the session state.
|
||||||
|
#[derive(Event, Clone, Copy)]
|
||||||
|
pub enum XrSessionState {
|
||||||
|
/// The session is in an idle state. Either just created or stopped
|
||||||
|
Idle,
|
||||||
|
/// The session is ready. You may send a [`BeginXrSession`] event.
|
||||||
|
Ready,
|
||||||
|
/// The session is running.
|
||||||
|
Running,
|
||||||
|
/// The session is being stopped
|
||||||
|
Stopping,
|
||||||
|
/// The session is destroyed
|
||||||
|
Destroyed,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Event sent from backends to inform the frontend that the instance was created.
|
||||||
|
#[derive(Event, Clone, Copy, Default)]
|
||||||
|
pub struct XrInstanceCreated;
|
||||||
|
|
||||||
|
/// Event sent from backends to inform the frontend that the instance was destroyed.
|
||||||
|
#[derive(Event, Clone, Copy, Default)]
|
||||||
|
pub struct XrInstanceDestroyed;
|
||||||
@@ -1,51 +1,53 @@
|
|||||||
//! A simple 3D scene with light shining over a cube sitting on a plane.
|
// //! A simple 3D scene with light shining over a cube sitting on a plane.
|
||||||
|
|
||||||
use bevy::{prelude::*, render::camera::RenderTarget};
|
// use bevy::{prelude::*, render::camera::RenderTarget};
|
||||||
use bevy_oxr::openxr::{render::LEFT_XR_TEXTURE_HANDLE, DefaultXrPlugins};
|
// use bevy_oxr::openxr::{render::LEFT_XR_TEXTURE_HANDLE, DefaultXrPlugins};
|
||||||
|
|
||||||
fn main() {
|
// fn main() {
|
||||||
App::new()
|
// App::new()
|
||||||
.add_plugins(DefaultXrPlugins)
|
// .add_plugins(DefaultXrPlugins)
|
||||||
.add_systems(Startup, setup)
|
// .add_systems(Startup, setup)
|
||||||
.run();
|
// .run();
|
||||||
}
|
// }
|
||||||
|
|
||||||
/// set up a simple 3D scene
|
// /// set up a simple 3D scene
|
||||||
fn setup(
|
// fn setup(
|
||||||
mut commands: Commands,
|
// mut commands: Commands,
|
||||||
mut meshes: ResMut<Assets<Mesh>>,
|
// mut meshes: ResMut<Assets<Mesh>>,
|
||||||
mut materials: ResMut<Assets<StandardMaterial>>,
|
// mut materials: ResMut<Assets<StandardMaterial>>,
|
||||||
) {
|
// ) {
|
||||||
// circular base
|
// // circular base
|
||||||
commands.spawn(PbrBundle {
|
// commands.spawn(PbrBundle {
|
||||||
mesh: meshes.add(Circle::new(4.0)),
|
// mesh: meshes.add(Circle::new(4.0)),
|
||||||
material: materials.add(Color::WHITE),
|
// material: materials.add(Color::WHITE),
|
||||||
transform: Transform::from_rotation(Quat::from_rotation_x(-std::f32::consts::FRAC_PI_2)),
|
// transform: Transform::from_rotation(Quat::from_rotation_x(-std::f32::consts::FRAC_PI_2)),
|
||||||
..default()
|
// ..default()
|
||||||
});
|
// });
|
||||||
// cube
|
// // cube
|
||||||
commands.spawn(PbrBundle {
|
// commands.spawn(PbrBundle {
|
||||||
mesh: meshes.add(Cuboid::new(1.0, 1.0, 1.0)),
|
// mesh: meshes.add(Cuboid::new(1.0, 1.0, 1.0)),
|
||||||
material: materials.add(Color::rgb_u8(124, 144, 255)),
|
// material: materials.add(Color::rgb_u8(124, 144, 255)),
|
||||||
transform: Transform::from_xyz(0.0, 0.5, 0.0),
|
// transform: Transform::from_xyz(0.0, 0.5, 0.0),
|
||||||
..default()
|
// ..default()
|
||||||
});
|
// });
|
||||||
// light
|
// // light
|
||||||
commands.spawn(PointLightBundle {
|
// commands.spawn(PointLightBundle {
|
||||||
point_light: PointLight {
|
// point_light: PointLight {
|
||||||
shadows_enabled: true,
|
// shadows_enabled: true,
|
||||||
..default()
|
// ..default()
|
||||||
},
|
// },
|
||||||
transform: Transform::from_xyz(4.0, 8.0, 4.0),
|
// transform: Transform::from_xyz(4.0, 8.0, 4.0),
|
||||||
..default()
|
// ..default()
|
||||||
});
|
// });
|
||||||
// camera
|
// // camera
|
||||||
commands.spawn(Camera3dBundle {
|
// commands.spawn(Camera3dBundle {
|
||||||
transform: Transform::from_xyz(-2.5, 4.5, 9.0).looking_at(Vec3::ZERO, Vec3::Y),
|
// transform: Transform::from_xyz(-2.5, 4.5, 9.0).looking_at(Vec3::ZERO, Vec3::Y),
|
||||||
camera: Camera {
|
// camera: Camera {
|
||||||
target: RenderTarget::TextureView(LEFT_XR_TEXTURE_HANDLE),
|
// target: RenderTarget::TextureView(LEFT_XR_TEXTURE_HANDLE),
|
||||||
..default()
|
// ..default()
|
||||||
},
|
// },
|
||||||
..default()
|
// ..default()
|
||||||
});
|
// });
|
||||||
}
|
// }
|
||||||
|
|
||||||
|
fn main() {}
|
||||||
|
|||||||
@@ -1,130 +0,0 @@
|
|||||||
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<T: private::Sealed> ActionType for T {}
|
|
||||||
|
|
||||||
pub trait ActionPathTrait {
|
|
||||||
type PathType: ActionType;
|
|
||||||
fn path(&self) -> Cow<'_, str>;
|
|
||||||
fn name(&self) -> Cow<'_, str>;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct ActionPath<T: ActionType> {
|
|
||||||
pub path: &'static str,
|
|
||||||
pub name: &'static str,
|
|
||||||
_marker: PhantomData<T>,
|
|
||||||
}
|
|
||||||
|
|
||||||
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::action_paths::ActionPath<$path_type> = crate::action_paths::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::action_paths::actions! {
|
|
||||||
$($subpath),*
|
|
||||||
$id {
|
|
||||||
path: $path;
|
|
||||||
name: $name;
|
|
||||||
path_type: $path_type;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
crate::action_paths::actions! {
|
|
||||||
$($subpath),*
|
|
||||||
$id {
|
|
||||||
path: $path;
|
|
||||||
$($children)*
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// handle children
|
|
||||||
(
|
|
||||||
$($subpath:literal),*
|
|
||||||
$id:ident {
|
|
||||||
path: $path:literal;
|
|
||||||
$($children:tt)*
|
|
||||||
}
|
|
||||||
) => {
|
|
||||||
pub mod $id {
|
|
||||||
crate::action_paths::actions! {
|
|
||||||
$($subpath,)* $path
|
|
||||||
$($children)*
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// handle siblings
|
|
||||||
(
|
|
||||||
$($subpath:literal),*
|
|
||||||
$id:ident {
|
|
||||||
path: $path:literal;
|
|
||||||
$($attrs:tt)*
|
|
||||||
}
|
|
||||||
$($siblings:tt)*
|
|
||||||
) => {
|
|
||||||
crate::action_paths::actions! {
|
|
||||||
$($subpath),*
|
|
||||||
$id {
|
|
||||||
path: $path;
|
|
||||||
$($attrs)*
|
|
||||||
}
|
|
||||||
}
|
|
||||||
crate::action_paths::actions! {
|
|
||||||
$($subpath),*
|
|
||||||
$($siblings)*
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) use actions;
|
|
||||||
@@ -1,238 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
use std::{borrow::Cow, marker::PhantomData};
|
|
||||||
|
|
||||||
use bevy::prelude::*;
|
|
||||||
|
|
||||||
pub use crate::action_paths::*;
|
|
||||||
|
|
||||||
#[derive(Event)]
|
|
||||||
pub struct XrCreateActionSet {
|
|
||||||
pub handle: Handle<XrActionSet>,
|
|
||||||
pub name: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct XrAction<'a, T: ActionType> {
|
|
||||||
pub name: Cow<'a, str>,
|
|
||||||
pub pretty_name: Cow<'a, str>,
|
|
||||||
pub action_set: Handle<XrActionSet>,
|
|
||||||
_marker: PhantomData<T>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(TypePath, Asset)]
|
|
||||||
pub struct XrActionSet {
|
|
||||||
pub name: String,
|
|
||||||
}
|
|
||||||
10
src/lib.rs
10
src/lib.rs
@@ -1,8 +1,2 @@
|
|||||||
mod action_paths;
|
pub use bevy_openxr;
|
||||||
pub mod actions;
|
pub use bevy_xr;
|
||||||
#[cfg(not(target_family = "wasm"))]
|
|
||||||
pub mod openxr;
|
|
||||||
pub mod render;
|
|
||||||
pub mod types;
|
|
||||||
#[cfg(target_family = "wasm")]
|
|
||||||
pub mod webxr;
|
|
||||||
|
|||||||
334
src/openxr.rs
334
src/openxr.rs
@@ -1,334 +0,0 @@
|
|||||||
mod extensions;
|
|
||||||
pub mod graphics;
|
|
||||||
pub mod render;
|
|
||||||
mod resources;
|
|
||||||
pub mod types;
|
|
||||||
|
|
||||||
use bevy::ecs::schedule::common_conditions::resource_equals;
|
|
||||||
use bevy::ecs::system::{Res, ResMut};
|
|
||||||
use bevy::math::{uvec2, UVec2};
|
|
||||||
use bevy::render::extract_resource::ExtractResourcePlugin;
|
|
||||||
use bevy::render::renderer::{RenderAdapter, RenderAdapterInfo, RenderInstance, RenderQueue};
|
|
||||||
use bevy::render::settings::RenderCreation;
|
|
||||||
use bevy::render::{RenderApp, RenderPlugin};
|
|
||||||
use bevy::utils::default;
|
|
||||||
use bevy::DefaultPlugins;
|
|
||||||
pub use resources::*;
|
|
||||||
pub use types::*;
|
|
||||||
|
|
||||||
use bevy::app::{App, First, Plugin, PluginGroup};
|
|
||||||
use bevy::log::{error, info, warn};
|
|
||||||
|
|
||||||
pub fn xr_entry() -> Result<XrEntry> {
|
|
||||||
#[cfg(windows)]
|
|
||||||
let entry = openxr::Entry::linked();
|
|
||||||
#[cfg(not(windows))]
|
|
||||||
let entry = unsafe { openxr::Entry::load()? };
|
|
||||||
Ok(XrEntry(entry))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct XrInitPlugin {
|
|
||||||
/// Information about the app this is being used to build.
|
|
||||||
pub app_info: AppInfo,
|
|
||||||
/// Extensions wanted for this session.
|
|
||||||
// TODO!() This should be changed to take a simpler list of features wanted that this crate supports. i.e. hand tracking
|
|
||||||
pub exts: XrExtensions,
|
|
||||||
/// List of blend modes the openxr session can use. If [None], pick the first available blend mode.
|
|
||||||
pub blend_modes: Option<Vec<EnvironmentBlendMode>>,
|
|
||||||
/// List of backends the openxr session can use. If [None], pick the first available backend.
|
|
||||||
pub backends: Option<Vec<GraphicsBackend>>,
|
|
||||||
/// List of formats the openxr session can use. If [None], pick the first available format
|
|
||||||
pub formats: Option<Vec<wgpu::TextureFormat>>,
|
|
||||||
/// List of resolutions that the openxr swapchain can use. If [None] pick the first available resolution.
|
|
||||||
pub resolutions: Option<Vec<UVec2>>,
|
|
||||||
/// Passed into the render plugin when added to the app.
|
|
||||||
pub synchronous_pipeline_compilation: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Plugin for XrInitPlugin {
|
|
||||||
fn build(&self, app: &mut App) {
|
|
||||||
if let Err(e) = init_xr(self, app) {
|
|
||||||
panic!("Encountered an error while trying to initialize XR: {e}");
|
|
||||||
}
|
|
||||||
app.add_systems(First, poll_events);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn init_xr(config: &XrInitPlugin, app: &mut App) -> Result<()> {
|
|
||||||
let entry = xr_entry()?;
|
|
||||||
|
|
||||||
let available_exts = entry.enumerate_extensions()?;
|
|
||||||
|
|
||||||
// check available extensions and send a warning for any wanted extensions that aren't available.
|
|
||||||
for ext in available_exts.unavailable_exts(&config.exts) {
|
|
||||||
error!(
|
|
||||||
"Extension \"{ext}\" not available in the current openxr runtime. Disabling extension."
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
let available_backends = GraphicsBackend::available_backends(&available_exts);
|
|
||||||
|
|
||||||
// Backend selection
|
|
||||||
let backend = if let Some(wanted_backends) = &config.backends {
|
|
||||||
let mut backend = None;
|
|
||||||
for wanted_backend in wanted_backends {
|
|
||||||
if available_backends.contains(wanted_backend) {
|
|
||||||
backend = Some(*wanted_backend);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
backend
|
|
||||||
} else {
|
|
||||||
available_backends.first().copied()
|
|
||||||
}
|
|
||||||
.ok_or(XrError::NoAvailableBackend)?;
|
|
||||||
|
|
||||||
let exts = config.exts.clone() & available_exts;
|
|
||||||
|
|
||||||
let instance = entry.create_instance(config.app_info.clone(), exts, backend)?;
|
|
||||||
let instance_props = instance.properties()?;
|
|
||||||
|
|
||||||
info!(
|
|
||||||
"Loaded OpenXR runtime: {} {}",
|
|
||||||
instance_props.runtime_name, instance_props.runtime_version
|
|
||||||
);
|
|
||||||
|
|
||||||
let system_id = instance.system(openxr::FormFactor::HEAD_MOUNTED_DISPLAY)?;
|
|
||||||
let system_props = instance.system_properties(system_id)?;
|
|
||||||
|
|
||||||
info!(
|
|
||||||
"Using system: {}",
|
|
||||||
if system_props.system_name.is_empty() {
|
|
||||||
"<unnamed>"
|
|
||||||
} else {
|
|
||||||
&system_props.system_name
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
// TODO!() support other view configurations
|
|
||||||
let available_view_configurations = instance.enumerate_view_configurations(system_id)?;
|
|
||||||
if !available_view_configurations.contains(&openxr::ViewConfigurationType::PRIMARY_STEREO) {
|
|
||||||
return Err(XrError::NoAvailableViewConfiguration);
|
|
||||||
}
|
|
||||||
|
|
||||||
let view_configuration_type = openxr::ViewConfigurationType::PRIMARY_STEREO;
|
|
||||||
|
|
||||||
let available_blend_modes =
|
|
||||||
instance.enumerate_environment_blend_modes(system_id, view_configuration_type)?;
|
|
||||||
|
|
||||||
// blend mode selection
|
|
||||||
let blend_mode = if let Some(wanted_blend_modes) = &config.blend_modes {
|
|
||||||
let mut blend_mode = None;
|
|
||||||
for wanted_blend_mode in wanted_blend_modes {
|
|
||||||
if available_blend_modes.contains(wanted_blend_mode) {
|
|
||||||
blend_mode = Some(*wanted_blend_mode);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
blend_mode
|
|
||||||
} else {
|
|
||||||
available_blend_modes.first().copied()
|
|
||||||
}
|
|
||||||
.ok_or(XrError::NoAvailableBackend)?;
|
|
||||||
|
|
||||||
let view_configuration_views =
|
|
||||||
instance.enumerate_view_configuration_views(system_id, view_configuration_type)?;
|
|
||||||
|
|
||||||
let (resolution, _view) = if let Some(resolutions) = &config.resolutions {
|
|
||||||
let mut preferred = None;
|
|
||||||
for resolution in resolutions {
|
|
||||||
for view_config in view_configuration_views.iter() {
|
|
||||||
if view_config.recommended_image_rect_height == resolution.y
|
|
||||||
&& view_config.recommended_image_rect_width == resolution.x
|
|
||||||
{
|
|
||||||
preferred = Some((*resolution, *view_config));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if preferred.is_none() {
|
|
||||||
for resolution in resolutions {
|
|
||||||
for view_config in view_configuration_views.iter() {
|
|
||||||
if view_config.max_image_rect_height >= resolution.y
|
|
||||||
&& view_config.max_image_rect_width >= resolution.x
|
|
||||||
{
|
|
||||||
preferred = Some((*resolution, *view_config));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
preferred
|
|
||||||
} else {
|
|
||||||
if let Some(config) = view_configuration_views.first() {
|
|
||||||
Some((
|
|
||||||
uvec2(
|
|
||||||
config.recommended_image_rect_width,
|
|
||||||
config.recommended_image_rect_height,
|
|
||||||
),
|
|
||||||
*config,
|
|
||||||
))
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.ok_or(XrError::NoAvailableViewConfiguration)?;
|
|
||||||
|
|
||||||
let (WgpuGraphics(device, queue, adapter_info, adapter, wgpu_instance), create_info) =
|
|
||||||
instance.init_graphics(system_id)?;
|
|
||||||
|
|
||||||
let (session, frame_waiter, frame_stream) =
|
|
||||||
unsafe { instance.create_session(system_id, create_info)? };
|
|
||||||
|
|
||||||
let available_formats = session.enumerate_swapchain_formats()?;
|
|
||||||
|
|
||||||
let format = if let Some(formats) = &config.formats {
|
|
||||||
let mut format = None;
|
|
||||||
for wanted_format in formats {
|
|
||||||
if available_formats.contains(wanted_format) {
|
|
||||||
format = Some(*wanted_format);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
format
|
|
||||||
} else {
|
|
||||||
available_formats.first().copied()
|
|
||||||
}
|
|
||||||
.ok_or(XrError::NoAvailableFormat)?;
|
|
||||||
|
|
||||||
let mut swapchain = session.create_swapchain(SwapchainCreateInfo {
|
|
||||||
create_flags: SwapchainCreateFlags::EMPTY,
|
|
||||||
usage_flags: SwapchainUsageFlags::COLOR_ATTACHMENT | SwapchainUsageFlags::SAMPLED,
|
|
||||||
format,
|
|
||||||
// TODO() add support for multisampling
|
|
||||||
sample_count: 1,
|
|
||||||
width: resolution.x,
|
|
||||||
height: resolution.y,
|
|
||||||
face_count: 1,
|
|
||||||
array_size: 2,
|
|
||||||
mip_count: 1,
|
|
||||||
})?;
|
|
||||||
|
|
||||||
let images = swapchain.enumerate_images(&device, format, resolution)?;
|
|
||||||
|
|
||||||
let stage = XrStage(
|
|
||||||
session
|
|
||||||
.create_reference_space(openxr::ReferenceSpaceType::STAGE, openxr::Posef::IDENTITY)?
|
|
||||||
.into(),
|
|
||||||
);
|
|
||||||
|
|
||||||
app.add_plugins((
|
|
||||||
RenderPlugin {
|
|
||||||
render_creation: RenderCreation::manual(
|
|
||||||
device.into(),
|
|
||||||
RenderQueue(queue.into()),
|
|
||||||
RenderAdapterInfo(adapter_info),
|
|
||||||
RenderAdapter(adapter.into()),
|
|
||||||
RenderInstance(wgpu_instance.into()),
|
|
||||||
),
|
|
||||||
synchronous_pipeline_compilation: config.synchronous_pipeline_compilation,
|
|
||||||
},
|
|
||||||
ExtractResourcePlugin::<XrTime>::default(),
|
|
||||||
ExtractResourcePlugin::<XrStatus>::default(),
|
|
||||||
));
|
|
||||||
let graphics_info = XrGraphicsInfo {
|
|
||||||
blend_mode,
|
|
||||||
swapchain_resolution: resolution,
|
|
||||||
swapchain_format: format,
|
|
||||||
};
|
|
||||||
|
|
||||||
app.insert_resource(instance.clone())
|
|
||||||
.insert_resource(session.clone())
|
|
||||||
.insert_resource(frame_waiter)
|
|
||||||
.insert_resource(images.clone())
|
|
||||||
.insert_resource(graphics_info)
|
|
||||||
.insert_resource(stage.clone())
|
|
||||||
.init_resource::<XrStatus>();
|
|
||||||
app.sub_app_mut(RenderApp)
|
|
||||||
.insert_resource(instance)
|
|
||||||
.insert_resource(session)
|
|
||||||
.insert_resource(frame_stream)
|
|
||||||
.insert_resource(swapchain)
|
|
||||||
.insert_resource(images)
|
|
||||||
.insert_resource(graphics_info)
|
|
||||||
.insert_resource(stage)
|
|
||||||
.init_resource::<XrStatus>();
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn session_running() -> impl FnMut(Res<XrStatus>) -> bool {
|
|
||||||
resource_equals(XrStatus::Enabled)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn poll_events(
|
|
||||||
instance: Res<XrInstance>,
|
|
||||||
session: Res<XrSession>,
|
|
||||||
mut xr_status: ResMut<XrStatus>,
|
|
||||||
) {
|
|
||||||
while let Some(event) = instance.poll_event(&mut Default::default()).unwrap() {
|
|
||||||
use openxr::Event::*;
|
|
||||||
match event {
|
|
||||||
SessionStateChanged(e) => {
|
|
||||||
// Session state change is where we can begin and end sessions, as well as
|
|
||||||
// find quit messages!
|
|
||||||
info!("entered XR state {:?}", e.state());
|
|
||||||
match e.state() {
|
|
||||||
openxr::SessionState::READY => {
|
|
||||||
info!("Calling Session begin :3");
|
|
||||||
session
|
|
||||||
.begin(openxr::ViewConfigurationType::PRIMARY_STEREO)
|
|
||||||
.unwrap();
|
|
||||||
*xr_status = XrStatus::Enabled;
|
|
||||||
}
|
|
||||||
openxr::SessionState::STOPPING => {
|
|
||||||
session.end().unwrap();
|
|
||||||
*xr_status = XrStatus::Disabled;
|
|
||||||
}
|
|
||||||
// openxr::SessionState::EXITING => {
|
|
||||||
// if *exit_type == ExitAppOnSessionExit::Always
|
|
||||||
// || *exit_type == ExitAppOnSessionExit::OnlyOnExit
|
|
||||||
// {
|
|
||||||
// app_exit.send_default();
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// openxr::SessionState::LOSS_PENDING => {
|
|
||||||
// if *exit_type == ExitAppOnSessionExit::Always {
|
|
||||||
// app_exit.send_default();
|
|
||||||
// }
|
|
||||||
// if *exit_type == ExitAppOnSessionExit::OnlyOnExit {
|
|
||||||
// start_session.send_default();
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// InstanceLossPending(_) => {
|
|
||||||
// app_exit.send_default();
|
|
||||||
// }
|
|
||||||
EventsLost(e) => {
|
|
||||||
warn!("lost {} XR events", e.lost_event_count());
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct DefaultXrPlugins;
|
|
||||||
|
|
||||||
impl PluginGroup for DefaultXrPlugins {
|
|
||||||
fn build(self) -> bevy::app::PluginGroupBuilder {
|
|
||||||
DefaultPlugins
|
|
||||||
.build()
|
|
||||||
.disable::<RenderPlugin>()
|
|
||||||
.add_before::<RenderPlugin, _>(XrInitPlugin {
|
|
||||||
app_info: default(),
|
|
||||||
exts: default(),
|
|
||||||
blend_modes: default(),
|
|
||||||
backends: default(),
|
|
||||||
formats: Some(vec![wgpu::TextureFormat::Rgba8UnormSrgb]),
|
|
||||||
resolutions: default(),
|
|
||||||
synchronous_pipeline_compilation: default(),
|
|
||||||
})
|
|
||||||
.add(render::XrRenderPlugin)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,176 +0,0 @@
|
|||||||
use bevy::{
|
|
||||||
prelude::*,
|
|
||||||
render::{
|
|
||||||
camera::{ManualTextureView, ManualTextureViewHandle, ManualTextureViews},
|
|
||||||
Render, RenderApp, RenderSet,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
use openxr::CompositionLayerFlags;
|
|
||||||
|
|
||||||
use crate::openxr::resources::*;
|
|
||||||
use crate::openxr::types::*;
|
|
||||||
use crate::openxr::XrTime;
|
|
||||||
|
|
||||||
use super::{poll_events, session_running};
|
|
||||||
|
|
||||||
pub struct XrRenderPlugin;
|
|
||||||
|
|
||||||
impl Plugin for XrRenderPlugin {
|
|
||||||
fn build(&self, app: &mut App) {
|
|
||||||
app.add_systems(
|
|
||||||
First,
|
|
||||||
wait_frame.after(poll_events).run_if(session_running()),
|
|
||||||
)
|
|
||||||
.add_systems(Startup, init_texture_views);
|
|
||||||
app.sub_app_mut(RenderApp).add_systems(
|
|
||||||
Render,
|
|
||||||
(
|
|
||||||
(begin_frame, insert_texture_views)
|
|
||||||
.chain()
|
|
||||||
.in_set(RenderSet::PrepareAssets),
|
|
||||||
end_frame.in_set(RenderSet::Cleanup),
|
|
||||||
)
|
|
||||||
.run_if(session_running()),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub const LEFT_XR_TEXTURE_HANDLE: ManualTextureViewHandle = ManualTextureViewHandle(1208214591);
|
|
||||||
pub const RIGHT_XR_TEXTURE_HANDLE: ManualTextureViewHandle = ManualTextureViewHandle(3383858418);
|
|
||||||
|
|
||||||
fn init_texture_views(
|
|
||||||
graphics_info: Res<XrGraphicsInfo>,
|
|
||||||
mut manual_texture_views: ResMut<ManualTextureViews>,
|
|
||||||
swapchain_images: Res<SwapchainImages>,
|
|
||||||
) {
|
|
||||||
let temp_tex = swapchain_images.first().unwrap();
|
|
||||||
let left = temp_tex.create_view(&wgpu::TextureViewDescriptor {
|
|
||||||
dimension: Some(wgpu::TextureViewDimension::D2),
|
|
||||||
array_layer_count: Some(1),
|
|
||||||
..Default::default()
|
|
||||||
});
|
|
||||||
let right = temp_tex.create_view(&wgpu::TextureViewDescriptor {
|
|
||||||
dimension: Some(wgpu::TextureViewDimension::D2),
|
|
||||||
array_layer_count: Some(1),
|
|
||||||
base_array_layer: 1,
|
|
||||||
..Default::default()
|
|
||||||
});
|
|
||||||
let resolution = graphics_info.swapchain_resolution;
|
|
||||||
let format = graphics_info.swapchain_format;
|
|
||||||
let left = ManualTextureView {
|
|
||||||
texture_view: left.into(),
|
|
||||||
size: resolution,
|
|
||||||
format: format,
|
|
||||||
};
|
|
||||||
let right = ManualTextureView {
|
|
||||||
texture_view: right.into(),
|
|
||||||
size: resolution,
|
|
||||||
format: format,
|
|
||||||
};
|
|
||||||
manual_texture_views.insert(LEFT_XR_TEXTURE_HANDLE, left);
|
|
||||||
manual_texture_views.insert(RIGHT_XR_TEXTURE_HANDLE, right);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn wait_frame(mut frame_waiter: ResMut<XrFrameWaiter>, mut commands: Commands) {
|
|
||||||
let state = frame_waiter.wait().expect("Failed to wait frame");
|
|
||||||
commands.insert_resource(XrTime(openxr::Time::from_nanos(
|
|
||||||
state.predicted_display_time.as_nanos() + state.predicted_display_period.as_nanos(),
|
|
||||||
)));
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn begin_frame(mut frame_stream: ResMut<XrFrameStream>) {
|
|
||||||
frame_stream.begin().expect("Failed to begin frame");
|
|
||||||
}
|
|
||||||
|
|
||||||
fn insert_texture_views(
|
|
||||||
swapchain_images: Res<SwapchainImages>,
|
|
||||||
mut swapchain: ResMut<XrSwapchain>,
|
|
||||||
mut manual_texture_views: ResMut<ManualTextureViews>,
|
|
||||||
graphics_info: Res<XrGraphicsInfo>,
|
|
||||||
) {
|
|
||||||
let index = swapchain.acquire_image().expect("Failed to acquire image");
|
|
||||||
swapchain
|
|
||||||
.wait_image(openxr::Duration::INFINITE)
|
|
||||||
.expect("Failed to wait image");
|
|
||||||
let image = &swapchain_images[index as usize];
|
|
||||||
let left = image.create_view(&wgpu::TextureViewDescriptor {
|
|
||||||
dimension: Some(wgpu::TextureViewDimension::D2),
|
|
||||||
array_layer_count: Some(1),
|
|
||||||
..Default::default()
|
|
||||||
});
|
|
||||||
let right = image.create_view(&wgpu::TextureViewDescriptor {
|
|
||||||
dimension: Some(wgpu::TextureViewDimension::D2),
|
|
||||||
array_layer_count: Some(1),
|
|
||||||
base_array_layer: 1,
|
|
||||||
..Default::default()
|
|
||||||
});
|
|
||||||
let resolution = graphics_info.swapchain_resolution;
|
|
||||||
let format = graphics_info.swapchain_format;
|
|
||||||
let left = ManualTextureView {
|
|
||||||
texture_view: left.into(),
|
|
||||||
size: resolution,
|
|
||||||
format: format,
|
|
||||||
};
|
|
||||||
let right = ManualTextureView {
|
|
||||||
texture_view: right.into(),
|
|
||||||
size: resolution,
|
|
||||||
format: format,
|
|
||||||
};
|
|
||||||
manual_texture_views.insert(LEFT_XR_TEXTURE_HANDLE, left);
|
|
||||||
manual_texture_views.insert(RIGHT_XR_TEXTURE_HANDLE, right);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn end_frame(
|
|
||||||
mut frame_stream: ResMut<XrFrameStream>,
|
|
||||||
session: Res<XrSession>,
|
|
||||||
mut swapchain: ResMut<XrSwapchain>,
|
|
||||||
stage: Res<XrStage>,
|
|
||||||
display_time: Res<XrTime>,
|
|
||||||
graphics_info: Res<XrGraphicsInfo>,
|
|
||||||
) {
|
|
||||||
swapchain.release_image().unwrap();
|
|
||||||
let (_flags, views) = session
|
|
||||||
.locate_views(
|
|
||||||
openxr::ViewConfigurationType::PRIMARY_STEREO,
|
|
||||||
**display_time,
|
|
||||||
&stage,
|
|
||||||
)
|
|
||||||
.expect("Failed to locate views");
|
|
||||||
|
|
||||||
let rect = openxr::Rect2Di {
|
|
||||||
offset: openxr::Offset2Di { x: 0, y: 0 },
|
|
||||||
extent: openxr::Extent2Di {
|
|
||||||
width: graphics_info.swapchain_resolution.x as _,
|
|
||||||
height: graphics_info.swapchain_resolution.y as _,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
frame_stream
|
|
||||||
.end(
|
|
||||||
**display_time,
|
|
||||||
graphics_info.blend_mode,
|
|
||||||
&[&CompositionLayerProjection::new()
|
|
||||||
.layer_flags(CompositionLayerFlags::BLEND_TEXTURE_SOURCE_ALPHA)
|
|
||||||
.space(&stage)
|
|
||||||
.views(&[
|
|
||||||
CompositionLayerProjectionView::new()
|
|
||||||
.pose(views[0].pose)
|
|
||||||
.fov(views[0].fov)
|
|
||||||
.sub_image(
|
|
||||||
SwapchainSubImage::new()
|
|
||||||
.swapchain(&swapchain)
|
|
||||||
.image_array_index(0)
|
|
||||||
.image_rect(rect),
|
|
||||||
),
|
|
||||||
CompositionLayerProjectionView::new()
|
|
||||||
.pose(views[0].pose)
|
|
||||||
.fov(views[0].fov)
|
|
||||||
.sub_image(
|
|
||||||
SwapchainSubImage::new()
|
|
||||||
.swapchain(&swapchain)
|
|
||||||
.image_array_index(0)
|
|
||||||
.image_rect(rect),
|
|
||||||
),
|
|
||||||
])],
|
|
||||||
)
|
|
||||||
.expect("Failed to end stream");
|
|
||||||
}
|
|
||||||
@@ -1,348 +0,0 @@
|
|||||||
use std::borrow::Cow;
|
|
||||||
|
|
||||||
use super::graphics::{graphics_match, GraphicsExt, GraphicsWrap};
|
|
||||||
|
|
||||||
pub use super::extensions::XrExtensions;
|
|
||||||
pub use openxr::{
|
|
||||||
EnvironmentBlendMode, Extent2Di, FormFactor, Graphics, Offset2Di, Rect2Di,
|
|
||||||
SwapchainCreateFlags, SwapchainUsageFlags,
|
|
||||||
};
|
|
||||||
pub type Result<T> = std::result::Result<T, XrError>;
|
|
||||||
|
|
||||||
pub struct WgpuGraphics(
|
|
||||||
pub wgpu::Device,
|
|
||||||
pub wgpu::Queue,
|
|
||||||
pub wgpu::AdapterInfo,
|
|
||||||
pub wgpu::Adapter,
|
|
||||||
pub wgpu::Instance,
|
|
||||||
);
|
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, Default, PartialEq)]
|
|
||||||
pub struct Version(pub u8, pub u8, pub u16);
|
|
||||||
|
|
||||||
impl Version {
|
|
||||||
pub const BEVY: Self = Self(0, 12, 1);
|
|
||||||
|
|
||||||
pub const fn to_u32(self) -> u32 {
|
|
||||||
let major = (self.0 as u32) << 24;
|
|
||||||
let minor = (self.1 as u32) << 16;
|
|
||||||
self.2 as u32 | major | minor
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq)]
|
|
||||||
pub struct AppInfo {
|
|
||||||
pub name: Cow<'static, str>,
|
|
||||||
pub version: Version,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for AppInfo {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self {
|
|
||||||
name: "Bevy".into(),
|
|
||||||
version: Version::BEVY,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub type GraphicsBackend = GraphicsWrap<()>;
|
|
||||||
|
|
||||||
impl GraphicsBackend {
|
|
||||||
const ALL: &'static [Self] = &[Self::Vulkan(())];
|
|
||||||
|
|
||||||
pub fn available_backends(exts: &XrExtensions) -> Vec<Self> {
|
|
||||||
Self::ALL
|
|
||||||
.iter()
|
|
||||||
.copied()
|
|
||||||
.filter(|backend| backend.is_available(exts))
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn is_available(&self, exts: &XrExtensions) -> bool {
|
|
||||||
self.required_exts().is_available(exts)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn required_exts(&self) -> XrExtensions {
|
|
||||||
graphics_match!(
|
|
||||||
self;
|
|
||||||
_ => Api::required_exts()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
mod error {
|
|
||||||
use super::GraphicsBackend;
|
|
||||||
use std::borrow::Cow;
|
|
||||||
use std::fmt;
|
|
||||||
use thiserror::Error;
|
|
||||||
|
|
||||||
#[derive(Error, Debug)]
|
|
||||||
pub enum XrError {
|
|
||||||
#[error("OpenXR error: {0}")]
|
|
||||||
OpenXrError(#[from] openxr::sys::Result),
|
|
||||||
#[error("OpenXR loading error: {0}")]
|
|
||||||
OpenXrLoadingError(#[from] openxr::LoadError),
|
|
||||||
#[error("WGPU instance error: {0}")]
|
|
||||||
WgpuInstanceError(#[from] wgpu_hal::InstanceError),
|
|
||||||
#[error("WGPU device error: {0}")]
|
|
||||||
WgpuDeviceError(#[from] wgpu_hal::DeviceError),
|
|
||||||
#[error("WGPU request device error: {0}")]
|
|
||||||
WgpuRequestDeviceError(#[from] wgpu::RequestDeviceError),
|
|
||||||
#[error("Unsupported texture format: {0:?}")]
|
|
||||||
UnsupportedTextureFormat(wgpu::TextureFormat),
|
|
||||||
#[error("Vulkan error: {0}")]
|
|
||||||
VulkanError(#[from] ash::vk::Result),
|
|
||||||
#[error("Vulkan loading error: {0}")]
|
|
||||||
VulkanLoadingError(#[from] ash::LoadingError),
|
|
||||||
#[error("Graphics backend '{0:?}' is not available")]
|
|
||||||
UnavailableBackend(GraphicsBackend),
|
|
||||||
#[error("No compatible backend available")]
|
|
||||||
NoAvailableBackend,
|
|
||||||
#[error("No compatible view configuration available")]
|
|
||||||
NoAvailableViewConfiguration,
|
|
||||||
#[error("No compatible blend mode available")]
|
|
||||||
NoAvailableBlendMode,
|
|
||||||
#[error("No compatible format available")]
|
|
||||||
NoAvailableFormat,
|
|
||||||
#[error("OpenXR runtime does not support these extensions: {0}")]
|
|
||||||
UnavailableExtensions(UnavailableExts),
|
|
||||||
#[error("Could not meet graphics requirements for platform. See console for details")]
|
|
||||||
FailedGraphicsRequirements,
|
|
||||||
#[error(
|
|
||||||
"Tried to use item {item} with backend {backend}. Expected backend {expected_backend}"
|
|
||||||
)]
|
|
||||||
GraphicsBackendMismatch {
|
|
||||||
item: &'static str,
|
|
||||||
backend: &'static str,
|
|
||||||
expected_backend: &'static str,
|
|
||||||
},
|
|
||||||
#[error("Failed to create CString: {0}")]
|
|
||||||
NulError(#[from] std::ffi::NulError),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<Vec<Cow<'static, str>>> for XrError {
|
|
||||||
fn from(value: Vec<Cow<'static, str>>) -> Self {
|
|
||||||
Self::UnavailableExtensions(UnavailableExts(value))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct UnavailableExts(Vec<Cow<'static, str>>);
|
|
||||||
|
|
||||||
impl fmt::Display for UnavailableExts {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
||||||
for s in &self.0 {
|
|
||||||
write!(f, "\t{s}")?;
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub use error::XrError;
|
|
||||||
|
|
||||||
#[derive(Debug, Copy, Clone)]
|
|
||||||
pub struct SwapchainCreateInfo {
|
|
||||||
pub create_flags: SwapchainCreateFlags,
|
|
||||||
pub usage_flags: SwapchainUsageFlags,
|
|
||||||
pub format: wgpu::TextureFormat,
|
|
||||||
pub sample_count: u32,
|
|
||||||
pub width: u32,
|
|
||||||
pub height: u32,
|
|
||||||
pub face_count: u32,
|
|
||||||
pub array_size: u32,
|
|
||||||
pub mip_count: u32,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<G: GraphicsExt> TryFrom<SwapchainCreateInfo> for openxr::SwapchainCreateInfo<G> {
|
|
||||||
type Error = XrError;
|
|
||||||
|
|
||||||
fn try_from(value: SwapchainCreateInfo) -> Result<Self> {
|
|
||||||
Ok(openxr::SwapchainCreateInfo {
|
|
||||||
create_flags: value.create_flags,
|
|
||||||
usage_flags: value.usage_flags,
|
|
||||||
format: G::from_wgpu_format(value.format)
|
|
||||||
.ok_or(XrError::UnsupportedTextureFormat(value.format))?,
|
|
||||||
sample_count: value.sample_count,
|
|
||||||
width: value.width,
|
|
||||||
height: value.height,
|
|
||||||
face_count: value.face_count,
|
|
||||||
array_size: value.array_size,
|
|
||||||
mip_count: value.mip_count,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub use builder::*;
|
|
||||||
|
|
||||||
/// Copied with modification from the openxr crate to allow for a safe, graphics agnostic api to work with Bevy.
|
|
||||||
mod builder {
|
|
||||||
use std::mem;
|
|
||||||
|
|
||||||
use openxr::{sys, CompositionLayerFlags, Fovf, Posef, Rect2Di, Space};
|
|
||||||
|
|
||||||
use crate::openxr::{graphics::graphics_match, XrSwapchain};
|
|
||||||
|
|
||||||
#[derive(Copy, Clone)]
|
|
||||||
pub struct SwapchainSubImage<'a> {
|
|
||||||
inner: sys::SwapchainSubImage,
|
|
||||||
swapchain: Option<&'a XrSwapchain>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a> SwapchainSubImage<'a> {
|
|
||||||
#[inline]
|
|
||||||
pub fn new() -> Self {
|
|
||||||
Self {
|
|
||||||
inner: sys::SwapchainSubImage {
|
|
||||||
..unsafe { mem::zeroed() }
|
|
||||||
},
|
|
||||||
swapchain: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#[inline]
|
|
||||||
pub fn into_raw(self) -> sys::SwapchainSubImage {
|
|
||||||
self.inner
|
|
||||||
}
|
|
||||||
#[inline]
|
|
||||||
pub fn as_raw(&self) -> &sys::SwapchainSubImage {
|
|
||||||
&self.inner
|
|
||||||
}
|
|
||||||
#[inline]
|
|
||||||
pub fn swapchain(mut self, value: &'a XrSwapchain) -> Self {
|
|
||||||
graphics_match!(
|
|
||||||
&value.0;
|
|
||||||
swap => self.inner.swapchain = swap.as_raw()
|
|
||||||
);
|
|
||||||
self.swapchain = Some(value);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
#[inline]
|
|
||||||
pub fn image_rect(mut self, value: Rect2Di) -> Self {
|
|
||||||
self.inner.image_rect = value;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
#[inline]
|
|
||||||
pub fn image_array_index(mut self, value: u32) -> Self {
|
|
||||||
self.inner.image_array_index = value;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a> Default for SwapchainSubImage<'a> {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self::new()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Copy, Clone)]
|
|
||||||
pub struct CompositionLayerProjectionView<'a> {
|
|
||||||
inner: sys::CompositionLayerProjectionView,
|
|
||||||
swapchain: Option<&'a XrSwapchain>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a> CompositionLayerProjectionView<'a> {
|
|
||||||
#[inline]
|
|
||||||
pub fn new() -> Self {
|
|
||||||
Self {
|
|
||||||
inner: sys::CompositionLayerProjectionView {
|
|
||||||
ty: sys::StructureType::COMPOSITION_LAYER_PROJECTION_VIEW,
|
|
||||||
..unsafe { mem::zeroed() }
|
|
||||||
},
|
|
||||||
swapchain: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#[inline]
|
|
||||||
pub fn into_raw(self) -> sys::CompositionLayerProjectionView {
|
|
||||||
self.inner
|
|
||||||
}
|
|
||||||
#[inline]
|
|
||||||
pub fn as_raw(&self) -> &sys::CompositionLayerProjectionView {
|
|
||||||
&self.inner
|
|
||||||
}
|
|
||||||
#[inline]
|
|
||||||
pub fn pose(mut self, value: Posef) -> Self {
|
|
||||||
self.inner.pose = value;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
#[inline]
|
|
||||||
pub fn fov(mut self, value: Fovf) -> Self {
|
|
||||||
self.inner.fov = value;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
#[inline]
|
|
||||||
pub fn sub_image(mut self, value: SwapchainSubImage<'a>) -> Self {
|
|
||||||
self.inner.sub_image = value.inner;
|
|
||||||
self.swapchain = value.swapchain;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl<'a> Default for CompositionLayerProjectionView<'a> {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self::new()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pub unsafe trait CompositionLayer<'a> {
|
|
||||||
fn swapchain(&self) -> Option<&'a XrSwapchain>;
|
|
||||||
fn header(&self) -> &'a sys::CompositionLayerBaseHeader;
|
|
||||||
}
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub struct CompositionLayerProjection<'a> {
|
|
||||||
inner: sys::CompositionLayerProjection,
|
|
||||||
swapchain: Option<&'a XrSwapchain>,
|
|
||||||
views: Vec<sys::CompositionLayerProjectionView>,
|
|
||||||
}
|
|
||||||
impl<'a> CompositionLayerProjection<'a> {
|
|
||||||
#[inline]
|
|
||||||
pub fn new() -> Self {
|
|
||||||
Self {
|
|
||||||
inner: sys::CompositionLayerProjection {
|
|
||||||
ty: sys::StructureType::COMPOSITION_LAYER_PROJECTION,
|
|
||||||
..unsafe { mem::zeroed() }
|
|
||||||
},
|
|
||||||
swapchain: None,
|
|
||||||
views: Vec::new(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#[inline]
|
|
||||||
pub fn into_raw(self) -> sys::CompositionLayerProjection {
|
|
||||||
self.inner
|
|
||||||
}
|
|
||||||
#[inline]
|
|
||||||
pub fn as_raw(&self) -> &sys::CompositionLayerProjection {
|
|
||||||
&self.inner
|
|
||||||
}
|
|
||||||
#[inline]
|
|
||||||
pub fn layer_flags(mut self, value: CompositionLayerFlags) -> Self {
|
|
||||||
self.inner.layer_flags = value;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
#[inline]
|
|
||||||
pub fn space(mut self, value: &'a Space) -> Self {
|
|
||||||
self.inner.space = value.as_raw();
|
|
||||||
self
|
|
||||||
}
|
|
||||||
#[inline]
|
|
||||||
pub fn views(mut self, value: &'a [CompositionLayerProjectionView<'a>]) -> Self {
|
|
||||||
for view in value {
|
|
||||||
self.views.push(view.inner.clone());
|
|
||||||
}
|
|
||||||
self.inner.views = self.views.as_slice().as_ptr() as *const _ as _;
|
|
||||||
self.inner.view_count = value.len() as u32;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
unsafe impl<'a> CompositionLayer<'a> for CompositionLayerProjection<'a> {
|
|
||||||
fn swapchain(&self) -> Option<&'a XrSwapchain> {
|
|
||||||
self.swapchain
|
|
||||||
}
|
|
||||||
|
|
||||||
fn header(&self) -> &'a sys::CompositionLayerBaseHeader {
|
|
||||||
unsafe { std::mem::transmute(&self.inner) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl<'a> Default for CompositionLayerProjection<'a> {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self::new()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
use bevy::ecs::system::Resource;
|
|
||||||
use bevy::math::Mat4;
|
|
||||||
use bevy::prelude::{Deref, DerefMut};
|
|
||||||
use bevy::render::camera::{RenderTarget, Viewport};
|
|
||||||
|
|
||||||
use crate::types::Pose;
|
|
||||||
|
|
||||||
pub const XR_TEXTURE_VIEW_INDEX: u32 = 1208214591;
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct XrView {
|
|
||||||
pub projection_matrix: Mat4,
|
|
||||||
pub pose: Pose,
|
|
||||||
pub render_target: RenderTarget,
|
|
||||||
pub view_port: Option<Viewport>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Deref, DerefMut, Default, Debug, Clone, Resource)]
|
|
||||||
pub struct XrViews(#[deref] pub Vec<XrView>);
|
|
||||||
24
src/types.rs
24
src/types.rs
@@ -1,24 +0,0 @@
|
|||||||
use bevy::ecs::component::Component;
|
|
||||||
use bevy::math::{Quat, Vec3};
|
|
||||||
use bevy::render::camera::ManualTextureViewHandle;
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct Pose {
|
|
||||||
pub translation: Vec3,
|
|
||||||
pub rotation: Quat,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Component)]
|
|
||||||
pub struct XrView {
|
|
||||||
pub view_handle: ManualTextureViewHandle,
|
|
||||||
pub view_index: usize,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct Haptic;
|
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug)]
|
|
||||||
pub enum BlendMode {
|
|
||||||
Opaque,
|
|
||||||
Additive,
|
|
||||||
AlphaBlend,
|
|
||||||
}
|
|
||||||
178
src/webxr.rs
178
src/webxr.rs
@@ -1,178 +0,0 @@
|
|||||||
pub mod render;
|
|
||||||
mod resources;
|
|
||||||
|
|
||||||
pub use resources::*;
|
|
||||||
|
|
||||||
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, XrReferenceSpaceType, XrRenderStateInit,
|
|
||||||
XrSessionMode,
|
|
||||||
};
|
|
||||||
use winit::platform::web::WindowExtWebSys;
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
struct FutureXrSession(Rc<Mutex<Option<Result<(XrSession, XrReferenceSpace), JsValue>>>>);
|
|
||||||
|
|
||||||
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::Inline, XrReferenceSpaceType::Viewer).await;
|
|
||||||
*future_session.0.lock().unwrap() = Some(result);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
fn ready(&self, app: &App) -> bool {
|
|
||||||
app.world
|
|
||||||
.get_non_send_resource::<FutureXrSession>()
|
|
||||||
.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(result) = app
|
|
||||||
.world
|
|
||||||
.remove_non_send_resource::<FutureXrSession>()
|
|
||||||
.and_then(|fxr| fxr.0.lock().unwrap().take())
|
|
||||||
{
|
|
||||||
let (session, reference_space) = result.unwrap();
|
|
||||||
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<dyn FnMut()>, 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<RefCell<Option<Closure<dyn FnMut()>>>> = 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::<XrSession>().clone();
|
|
||||||
let inner_closure: Rc<RefCell<Option<Closure<dyn FnMut(f64, web_sys::XrFrame)>>>> =
|
|
||||||
Rc::new(RefCell::new(None));
|
|
||||||
let closure = inner_closure.clone();
|
|
||||||
*closure.borrow_mut() = Some(Closure::new(move |_time, frame: web_sys::XrFrame| {
|
|
||||||
let session = frame.session();
|
|
||||||
app.insert_non_send_resource(XrFrame(frame.clone()));
|
|
||||||
app.sub_app_mut(RenderApp)
|
|
||||||
.insert_non_send_resource(XrFrame(frame));
|
|
||||||
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<HtmlCanvasElement> {
|
|
||||||
let window_entity = world
|
|
||||||
.query_filtered::<Entity, With<PrimaryWindow>>()
|
|
||||||
.get_single(world)
|
|
||||||
.ok()?;
|
|
||||||
let windows = world.get_non_send_resource::<WinitWindows>()?;
|
|
||||||
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: web_sys::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 = web_sys::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((XrSession(session), XrReferenceSpace(reference_space)))
|
|
||||||
}
|
|
||||||
@@ -1,154 +0,0 @@
|
|||||||
use crate::render::{XrView, XrViews};
|
|
||||||
use crate::types::Pose;
|
|
||||||
|
|
||||||
use super::resources::*;
|
|
||||||
|
|
||||||
use bevy::app::{App, Plugin, PreUpdate};
|
|
||||||
use bevy::ecs::schedule::IntoSystemConfigs;
|
|
||||||
use bevy::ecs::system::{NonSend, Res, ResMut};
|
|
||||||
use bevy::ecs::world::World;
|
|
||||||
use bevy::math::{quat, uvec2, vec3, Mat4};
|
|
||||||
use bevy::render::camera::{
|
|
||||||
ManualTextureView, ManualTextureViewHandle, ManualTextureViews, RenderTarget, Viewport,
|
|
||||||
};
|
|
||||||
use bevy::render::renderer::RenderDevice;
|
|
||||||
use bevy::utils::default;
|
|
||||||
|
|
||||||
pub const XR_TEXTURE_VIEW_HANDLE: ManualTextureViewHandle =
|
|
||||||
ManualTextureViewHandle(crate::render::XR_TEXTURE_VIEW_INDEX);
|
|
||||||
|
|
||||||
pub struct XrRenderingPlugin;
|
|
||||||
|
|
||||||
impl Plugin for XrRenderingPlugin {
|
|
||||||
fn build(&self, app: &mut App) {
|
|
||||||
app.init_resource::<XrViews>();
|
|
||||||
app.add_systems(
|
|
||||||
PreUpdate,
|
|
||||||
(insert_gl_layer, update_manual_texture_views, insert_views).chain(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn insert_gl_layer(world: &mut World) {
|
|
||||||
let gl_layer = world
|
|
||||||
.non_send_resource::<XrFrame>()
|
|
||||||
.session()
|
|
||||||
.render_state()
|
|
||||||
.base_layer()
|
|
||||||
.unwrap();
|
|
||||||
world.insert_non_send_resource(XrWebGlLayer(gl_layer));
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn update_manual_texture_views(
|
|
||||||
gl_layer: NonSend<XrWebGlLayer>,
|
|
||||||
render_device: Res<RenderDevice>,
|
|
||||||
mut manual_tex_view: ResMut<ManualTextureViews>,
|
|
||||||
) {
|
|
||||||
let dest_texture = create_framebuffer_texture(render_device.wgpu_device(), &gl_layer);
|
|
||||||
let view = dest_texture.create_view(&default());
|
|
||||||
|
|
||||||
manual_tex_view.insert(
|
|
||||||
XR_TEXTURE_VIEW_HANDLE,
|
|
||||||
ManualTextureView::with_default_format(
|
|
||||||
view.into(),
|
|
||||||
uvec2(gl_layer.framebuffer_width(), gl_layer.framebuffer_height()),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn insert_views(
|
|
||||||
gl_layer: NonSend<XrWebGlLayer>,
|
|
||||||
reference_space: NonSend<XrReferenceSpace>,
|
|
||||||
frame: NonSend<XrFrame>,
|
|
||||||
mut xr_views: ResMut<XrViews>,
|
|
||||||
) {
|
|
||||||
let Some(viewer_pose) = frame.get_viewer_pose(&reference_space) else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
let views = viewer_pose
|
|
||||||
.views()
|
|
||||||
.into_iter()
|
|
||||||
.map(Into::<web_sys::XrView>::into)
|
|
||||||
.map(|view| {
|
|
||||||
let transform = view.transform();
|
|
||||||
let position = transform.position();
|
|
||||||
let orientation = transform.orientation();
|
|
||||||
let viewport = gl_layer
|
|
||||||
.get_viewport(&view)
|
|
||||||
.map(|viewport| Viewport {
|
|
||||||
physical_position: uvec2(viewport.x() as u32, viewport.y() as u32),
|
|
||||||
physical_size: uvec2(viewport.width() as u32, viewport.height() as u32),
|
|
||||||
..Default::default()
|
|
||||||
})
|
|
||||||
.unwrap();
|
|
||||||
XrView {
|
|
||||||
projection_matrix: Mat4::from_cols_array(
|
|
||||||
&view.projection_matrix().try_into().unwrap(),
|
|
||||||
),
|
|
||||||
pose: Pose {
|
|
||||||
translation: vec3(
|
|
||||||
position.x() as f32,
|
|
||||||
position.y() as f32,
|
|
||||||
position.z() as f32,
|
|
||||||
),
|
|
||||||
rotation: quat(
|
|
||||||
orientation.x() as f32,
|
|
||||||
orientation.y() as f32,
|
|
||||||
orientation.z() as f32,
|
|
||||||
orientation.w() as f32,
|
|
||||||
),
|
|
||||||
},
|
|
||||||
render_target: RenderTarget::TextureView(XR_TEXTURE_VIEW_HANDLE),
|
|
||||||
view_port: Some(viewport),
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
xr_views.0 = views;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn create_framebuffer_texture(device: &wgpu::Device, gl_layer: &XrWebGlLayer) -> wgpu::Texture {
|
|
||||||
unsafe {
|
|
||||||
device.create_texture_from_hal::<wgpu_hal::gles::Api>(
|
|
||||||
wgpu_hal::gles::Texture {
|
|
||||||
inner: wgpu_hal::gles::TextureInner::ExternalFramebuffer {
|
|
||||||
// inner: framebuffer,
|
|
||||||
inner: gl_layer.framebuffer_unwrapped(),
|
|
||||||
// inner: framebuffer.as_ref().unwrap().clone(),
|
|
||||||
},
|
|
||||||
mip_level_count: 1,
|
|
||||||
array_layer_count: 1,
|
|
||||||
format: wgpu::TextureFormat::Rgba8Unorm, //TODO check this is ok, different from bevy default
|
|
||||||
format_desc: wgpu_hal::gles::TextureFormatDesc {
|
|
||||||
internal: glow::RGBA,
|
|
||||||
external: glow::RGBA,
|
|
||||||
data_type: glow::UNSIGNED_BYTE,
|
|
||||||
},
|
|
||||||
copy_size: wgpu_hal::CopyExtent {
|
|
||||||
width: gl_layer.framebuffer_width(),
|
|
||||||
height: gl_layer.framebuffer_height(),
|
|
||||||
depth: 1,
|
|
||||||
},
|
|
||||||
drop_guard: None,
|
|
||||||
is_cubemap: false,
|
|
||||||
},
|
|
||||||
&wgpu::TextureDescriptor {
|
|
||||||
label: Some("framebuffer (color)"),
|
|
||||||
size: wgpu::Extent3d {
|
|
||||||
width: gl_layer.framebuffer_width(),
|
|
||||||
height: gl_layer.framebuffer_height(),
|
|
||||||
depth_or_array_layers: 1,
|
|
||||||
},
|
|
||||||
mip_level_count: 1,
|
|
||||||
sample_count: 1,
|
|
||||||
dimension: wgpu::TextureDimension::D2,
|
|
||||||
format: wgpu::TextureFormat::Rgba8UnormSrgb,
|
|
||||||
view_formats: &[wgpu::TextureFormat::Rgba8UnormSrgb],
|
|
||||||
usage: wgpu::TextureUsages::RENDER_ATTACHMENT
|
|
||||||
| wgpu::TextureUsages::TEXTURE_BINDING,
|
|
||||||
// | wgpu::TextureUsages::COPY_SRC,
|
|
||||||
// | wgpu::TextureUsages::COPY_DST,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
use bevy::prelude::{Deref, DerefMut};
|
|
||||||
use web_sys::WebGlFramebuffer;
|
|
||||||
|
|
||||||
#[derive(Deref, DerefMut, Clone)]
|
|
||||||
pub struct XrSession(#[deref] pub(crate) web_sys::XrSession);
|
|
||||||
|
|
||||||
#[derive(Deref, DerefMut, Clone)]
|
|
||||||
pub struct XrReferenceSpace(#[deref] pub(crate) web_sys::XrReferenceSpace);
|
|
||||||
|
|
||||||
#[derive(Deref, DerefMut, Clone)]
|
|
||||||
pub struct XrFrame(#[deref] pub(crate) web_sys::XrFrame);
|
|
||||||
|
|
||||||
#[derive(Deref, DerefMut, Clone)]
|
|
||||||
pub struct XrWebGlLayer(#[deref] pub(crate) web_sys::XrWebGlLayer);
|
|
||||||
|
|
||||||
impl XrWebGlLayer {
|
|
||||||
pub(crate) fn framebuffer_unwrapped(&self) -> WebGlFramebuffer {
|
|
||||||
js_sys::Reflect::get(&self, &"framebuffer".into())
|
|
||||||
.unwrap()
|
|
||||||
.into()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub struct XrView(pub(crate) web_sys::XrView);
|
|
||||||
Reference in New Issue
Block a user