Merge pull request #115 from awtterpip/webxr-refactor

Webxr refactor
This commit is contained in:
Schmarni
2024-07-05 02:17:42 +02:00
committed by GitHub
100 changed files with 8372 additions and 11619 deletions

3
.cargo/config.toml Normal file
View File

@@ -0,0 +1,3 @@
[target.wasm32-unknown-unknown]
runner = "wasm-server-runner"
rustflags = ["--cfg","web_sys_unstable_apis",]

1
.envrc.example Normal file
View File

@@ -0,0 +1 @@
use flake

View File

@@ -1,34 +0,0 @@
name: Build
on:
push:
branches:
- "main"
paths-ignore:
- "/docs"
- "README.md"
pull_request:
paths-ignore:
- "/docs"
- "README.md"
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: "Checkout"
uses: actions/checkout@v3
- name: "Cache"
uses: Swatinem/rust-cache@v2
- name: "External dependencies"
run: sudo apt-get update -y && sudo apt-get install -y libasound2-dev portaudio19-dev build-essential libpulse-dev libdbus-1-dev libudev-dev libopenxr-loader1 libopenxr-dev
- name: "Checks"
run: |
cargo fmt --check --all
#cargo clippy --no-deps --tests -- -D warnings
#cargo rustdoc -- -D warnings
- name: "Build"
run: |
cargo build
cargo build --examples
- name: "Test"
run: |
cargo test --verbose

3
.gitignore vendored
View File

@@ -1,3 +1,6 @@
**/target
**/runtime_libs/arm64-v8a/*
.vscode
\.DS_Store
.envrc
.direnv

3189
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,71 +1,18 @@
[workspace]
resolver = "2"
members = [
"examples/android",
"examples/demo",
]
[workspace.package]
edition = "2021"
license = "MIT OR Apache-2.0"
repository = "https://github.com/awtterpip/bevy_oxr"
[workspace.dependencies]
eyre = "0.6.2"
bevy = "0.14.0"
openxr = "0.18"
color-eyre = "0.6.2"
[package]
name = "bevy_oxr"
version = "0.2.0"
name = "bevy_mod_xr_backends"
version = "0.1.0"
edition = "2021"
description = "Community crate for OpenXR in Bevy"
edition.workspace = true
license.workspace = true
repository.workspace = true
[features]
default = ["vulkan", "d3d12"]
force-link = ["openxr/linked"]
vulkan = ["wgpu-core/vulkan"]
d3d12 = ["wgpu-core/dx12", "dep:winapi", "dep:d3d12"]
repository = "https://github.com/awtterpip/bevy_oxr"
license = "MIT/Apache-2.0"
[dependencies]
ash = "0.37.3"
bevy_mod_openxr.path = "./crates/bevy_openxr"
bevy_mod_xr.path = "./crates/bevy_xr"
bevy.workspace = true
eyre.workspace = true
futures-lite = "2.0.1"
mint = "0.5.9"
wgpu = "0.20"
wgpu-core = "0.21"
wgpu-hal = "0.21"
[target.'cfg(windows)'.dependencies]
openxr = { workspace = true, features = [ "linked", "static", "mint" ] }
winapi = { version = "0.3.9", optional = true }
d3d12 = { version = "0.20", features = ["libloading"], optional = true }
[workspace]
members = ["crates/*", "crates/bevy_openxr/examples/android"]
[target.'cfg(all(target_family = "unix", not(target_arch = "wasm32")) )'.dependencies]
openxr = { workspace = true, features = [ "mint" ] }
[target.'cfg(all(not(target_family = "unix"), not(target_arch = "wasm32")))'.dependencies]
openxr = { workspace = true, features = [ "mint", "static" ] }
[target.'cfg(target_os = "android")'.dependencies]
ndk-context = "0.1"
jni = "0.20"
[dev-dependencies]
color-eyre.workspace = true
[[example]]
name = "xr"
path = "examples/xr.rs"
[profile.release]
debug = true
[package.metadata.docs.rs]
rustdoc-args = ["-Zunstable-options", "--cfg", "docsrs"]
all-features = true
cargo-args = ["-Zunstable-options", "-Zrustdoc-scrape-examples"]
[workspace.dependencies]
bevy = "0.13.0"

View File

@@ -1,14 +1,8 @@
# Bevy OpenXR
# Bevy Mod XR
A crate for adding openxr support to Bevy ( planned to be upstreamed ).
A crate for adding openxr (and in the future webxr) support to Bevy.
To see it in action run the example in `examples` with `cargo run --example xr`
## This crate will not recive any feature or performance updates
the current implementation will not be updated!
we will release a version for bevy 0.14 once that releases but no further version will be supported!
there is a new rewrite that will be upstreamed into bevy in the future.
you can use that from this git repo if you want, but be warned it has a completly different public api.
To see it in action run the example in `crates/bevy_openxr/examples` with `cargo run -p bevy_openxr --example 3d_scene`
## Discord
@@ -22,7 +16,7 @@ https://discord.gg/sqMw7UJhNc
- Make sure, if you're on Linux, that you have the `openxr` package installed on your system.
- I'm getting poor performance.
- Like other bevy projects, make sure you're building in release (example: `cargo run --example xr --release`)
- Like other bevy projects, make sure you're building in release (example: `cargo run -p bevy_openxr --example 3d_scene --release`)
## License
@@ -39,3 +33,4 @@ at your option. This means you can select the license you prefer!
Unless you explicitly state otherwise, any contribution intentionally submitted
for inclusion in the work by you, as defined in the Apache-2.0 license, shall
be dual licensed as above, without any additional terms or conditions.

View File

@@ -0,0 +1,40 @@
[package]
name = "bevy_mod_openxr"
version = "0.1.0"
edition = "2021"
[features]
default = ["vulkan", "d3d12", "passthrough"]
vulkan = ["dep:ash"]
d3d12 = ["wgpu/dx12", "wgpu-hal/dx12", "dep:winapi", "dep:d3d12"]
passthrough = []
[dev-dependencies]
bevy_xr_utils.path = "../bevy_xr_utils"
[target.'cfg(target_os = "android")'.dependencies]
ndk-context = "0.1"
jni = "0.20"
# bevy can't be placed behind target or proc macros won't work properly
[dependencies]
bevy.workspace = true
# all other dependencies are placed under this since on wasm, this crate is completely empty
[target.'cfg(not(target_family = "wasm"))'.dependencies]
openxr = "0.18.0"
thiserror = "1.0.57"
wgpu = "0.19.3"
wgpu-hal = "0.19.3"
bevy_mod_xr.path = "../bevy_xr"
ash = { version = "0.37.3", optional = true }
[target.'cfg(target_family = "unix")'.dependencies]
openxr = { version = "0.18.0", features = ["mint"] }
wgpu = { version = "0.19.3", features = ["vulkan-portability"] }
[target.'cfg(target_family = "windows")'.dependencies]
openxr = { version = "0.18.0", features = ["mint", "static"] }
winapi = { version = "0.3.9", optional = true }
d3d12 = { version = "0.19", features = ["libloading"], optional = true }

View File

@@ -0,0 +1,47 @@
//! A simple 3D scene with light shining over a cube sitting on a plane.
use bevy::prelude::*;
use bevy_mod_openxr::add_xr_plugins;
fn main() {
App::new()
.add_plugins(add_xr_plugins(DefaultPlugins))
.add_plugins(bevy_xr_utils::hand_gizmos::HandGizmosPlugin)
.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()
});
commands.spawn(Camera3dBundle {
transform: Transform::from_xyz(-2.5, 4.5, 9.0).looking_at(Vec3::ZERO, Vec3::Y),
..default()
});
}

View File

@@ -0,0 +1,160 @@
// a simple example showing basic actions using the xr utils actions
use bevy::{math::vec3, prelude::*};
use bevy_mod_openxr::{add_xr_plugins, helper_traits::ToQuat, resources::OxrViews};
use bevy_mod_xr::session::XrTrackingRoot;
use bevy_xr_utils::xr_utils_actions::{
ActiveSet, XRUtilsAction, XRUtilsActionSet, XRUtilsActionState, XRUtilsActionSystemSet,
XRUtilsActionsPlugin, XRUtilsBinding,
};
fn main() {
App::new()
.add_plugins(add_xr_plugins(DefaultPlugins))
.add_plugins(bevy_xr_utils::hand_gizmos::HandGizmosPlugin)
.add_systems(Startup, setup_scene)
.add_systems(
Startup,
create_action_entities.before(XRUtilsActionSystemSet::CreateEvents),
)
.add_plugins(XRUtilsActionsPlugin)
.add_systems(Update, read_action_with_marker_component)
.add_systems(Update, handle_flight_input)
// Realtime lighting is expensive, use ambient light instead
.insert_resource(AmbientLight {
color: Default::default(),
brightness: 500.0,
})
.run();
}
/// set up a simple 3D scene
fn setup_scene(
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()
});
commands.spawn(Camera3dBundle {
transform: Transform::from_xyz(-2.5, 4.5, 9.0).looking_at(Vec3::ZERO, Vec3::Y),
..default()
});
}
#[derive(Component)]
struct FlightActionMarker;
fn create_action_entities(mut commands: Commands) {
//create a set
let set = commands
.spawn((
XRUtilsActionSet {
name: "flight".into(),
pretty_name: "pretty flight set".into(),
priority: u32::MIN,
},
ActiveSet, //marker to indicate we want this synced
))
.id();
//create an action
let action = commands
.spawn((
XRUtilsAction {
action_name: "flight_input".into(),
localized_name: "flight_input_localized".into(),
action_type: bevy_mod_xr::actions::ActionType::Vector,
},
FlightActionMarker, //lets try a marker component
))
.id();
//create a binding
let binding_index = commands
.spawn(XRUtilsBinding {
profile: "/interaction_profiles/valve/index_controller".into(),
binding: "/user/hand/right/input/thumbstick".into(),
})
.id();
let binding_touch = commands
.spawn(XRUtilsBinding {
profile: "/interaction_profiles/oculus/touch_controller".into(),
binding: "/user/hand/right/input/thumbstick".into(),
})
.id();
//add action to set, this isnt the best
//TODO look into a better system
commands.entity(action).add_child(binding_index);
commands.entity(action).add_child(binding_touch);
commands.entity(set).add_child(action);
}
fn read_action_with_marker_component(
mut action_query: Query<&XRUtilsActionState, With<FlightActionMarker>>,
) {
//now for the actual checking
for state in action_query.iter_mut() {
info!("action state is: {:?}", state);
}
}
//lets add some flycam stuff
fn handle_flight_input(
action_query: Query<&XRUtilsActionState, With<FlightActionMarker>>,
mut oxr_root: Query<&mut Transform, With<XrTrackingRoot>>,
time: Res<Time>,
//use the views for hmd orientation
views: ResMut<OxrViews>,
) {
//now for the actual checking
for state in action_query.iter() {
// info!("action state is: {:?}", state);
match state {
XRUtilsActionState::Bool(_) => (),
XRUtilsActionState::Float(_) => (),
XRUtilsActionState::Vector(vector_state) => {
//assuming we are mapped to a vector lets fly
let input_vector = vec3(
vector_state.current_state[0],
0.0,
-vector_state.current_state[1],
);
//hard code speed for now
let speed = 5.0;
let root = oxr_root.get_single_mut();
match root {
Ok(mut root_position) => {
//lets assume HMD based direction for now
let view = views.first();
match view {
Some(v) => {
let reference_quat = v.pose.orientation.to_quat();
let locomotion_vector = reference_quat.mul_vec3(input_vector);
root_position.translation +=
locomotion_vector * speed * time.delta_seconds();
}
None => return,
}
}
Err(_) => {
info!("handle_flight_input: error getting root position for flight actions")
}
}
}
}
}
}

View File

@@ -0,0 +1 @@
/runtime_libs

View File

@@ -1,26 +1,21 @@
[package]
name = "bevy_openxr_android"
version = "0.1.0"
description = "Example for building an Android OpenXR app with Bevy"
edition.workspace = true
license.workspace = true
publish = false
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
bevy_mod_openxr.path = "../.."
bevy.workspace = true
bevy_xr_utils.path = "../../../bevy_xr_utils"
[lib]
name = "bevy_openxr_android"
crate-type = ["rlib", "cdylib"]
[dependencies]
bevy.workspace = true
bevy_oxr.path = "../.."
openxr = { workspace = true, features = ["mint"] }
# [profile.release]
# lto = "fat"
# codegen-units = 1
# panic = "abort"
# This metadata is used by `cargo-apk` - `xbuild` uses the `manifest.yaml` instead.
[package.metadata.android]
package = "org.bevyengine.example_openxr_android"
@@ -47,11 +42,31 @@ launch_mode = "singleTask"
orientation = "landscape"
resizeable_activity = false
[[package.metadata.android.uses_feature]]
name = "android.hardware.vr.headtracking"
required = true
[[package.metadata.android.uses_feature]]
name = "oculus.software.handtracking"
required = true
[[package.metadata.android.uses_feature]]
name = "com.oculus.feature.PASSTHROUGH"
required = true
[[package.metadata.android.uses_feature]]
name = "com.oculus.experimental.enabled"
required = true
[[package.metadata.android.uses_permission]]
name = "com.oculus.permission.HAND_TRACKING"
[[package.metadata.android.application.activity.intent_filter]]
actions = ["android.intent.action.MAIN"]
categories = [
"com.oculus.intent.category.VR",
"android.intent.category.LAUNCHER",
"org.khronos.openxr.intent.category.IMMERSIVE_HMD",
]
# !! IMPORTANT !!

View File

@@ -36,4 +36,4 @@ android:
- "android.intent.category.LAUNCHER"
- "org.khronos.openxr.intent.category.IMMERSIVE_HMD"
sdk:
target_sdk_version: 32
target_sdk_version: 32

View File

@@ -0,0 +1,58 @@
//! A simple 3D scene with light shining over a cube sitting on a plane.
use bevy::prelude::*;
use bevy_mod_openxr::{add_xr_plugins, init::OxrInitPlugin, types::OxrExtensions};
#[bevy_main]
fn main() {
App::new()
.add_plugins(add_xr_plugins(DefaultPlugins).set(OxrInitPlugin {
app_info: default(),
exts: {
let mut exts = OxrExtensions::default();
exts.enable_fb_passthrough();
exts.enable_hand_tracking();
exts
},
blend_modes: default(),
backends: default(),
formats: default(),
resolutions: default(),
synchronous_pipeline_compilation: default(),
}))
.add_plugins(bevy_xr_utils::hand_gizmos::HandGizmosPlugin)
.insert_resource(Msaa::Off)
.add_systems(Startup, setup)
.insert_resource(AmbientLight {
color: Default::default(),
brightness: 500.0,
})
.insert_resource(ClearColor(Color::NONE))
.run();
}
/// set up a simple 3D scene
fn setup(
mut commands: Commands,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<StandardMaterial>>,
) {
let mut white: StandardMaterial = Color::WHITE.into();
white.unlit = true;
// circular base
commands.spawn(PbrBundle {
mesh: meshes.add(Circle::new(4.0)),
material: materials.add(white),
transform: Transform::from_rotation(Quat::from_rotation_x(-std::f32::consts::FRAC_PI_2)),
..default()
});
let mut cube_mat: StandardMaterial = Color::rgb_u8(124, 144, 255).into();
cube_mat.unlit = true;
// cube
commands.spawn(PbrBundle {
mesh: meshes.add(Cuboid::new(1.0, 1.0, 1.0)),
material: materials.add(cube_mat),
transform: Transform::from_xyz(0.0, 0.5, 0.0),
..default()
});
}

View File

@@ -0,0 +1,108 @@
//! A simple 3D scene with light shining over a cube sitting on a plane.
use bevy::prelude::*;
use bevy_mod_openxr::{add_xr_plugins, init::OxrInitPlugin, types::OxrExtensions};
use bevy_mod_xr::session::XrState;
// use openxr::EnvironmentBlendMode;
// use wgpu::TextureFormat;
fn main() {
App::new()
.add_plugins(add_xr_plugins(DefaultPlugins).build().set(OxrInitPlugin {
exts: {
let mut exts = OxrExtensions::default();
exts.enable_hand_tracking();
exts.other.push("XR_EXTX_overlay\0".into());
exts
},
// blend_modes: Some({
// let mut v = Vec::new();
// v.push(EnvironmentBlendMode::ALPHA_BLEND);
// v.push(EnvironmentBlendMode::ADDITIVE);
// v.push(EnvironmentBlendMode::OPAQUE);
// v
// }),
// formats: Some({
// let mut v = Vec::new();
// // v.push(TextureFormat::Rgba8Uint);
// v.push(TextureFormat::Rgba8Unorm);
// v.push(TextureFormat::Rgba8UnormSrgb);
// v
// }),
..OxrInitPlugin::default()
}))
.insert_resource(ClearColor(Color::NONE))
.add_plugins(bevy_xr_utils::hand_gizmos::HandGizmosPlugin)
.add_systems(Startup, setup)
.add_systems(Update, handle_input)
.run();
}
fn handle_input(
keys: Res<ButtonInput<KeyCode>>,
mut end: EventWriter<bevy_mod_xr::session::XrEndSessionEvent>,
mut destroy: EventWriter<bevy_mod_xr::session::XrDestroySessionEvent>,
mut begin: EventWriter<bevy_mod_xr::session::XrBeginSessionEvent>,
mut create: EventWriter<bevy_mod_xr::session::XrCreateSessionEvent>,
mut request_exit: EventWriter<bevy_mod_xr::session::XrRequestExitEvent>,
state: Res<XrState>,
) {
if keys.just_pressed(KeyCode::KeyE) {
info!("sending end");
end.send_default();
}
if keys.just_pressed(KeyCode::KeyC) {
info!("sending create");
create.send_default();
}
if keys.just_pressed(KeyCode::KeyD) {
info!("sending destroy");
destroy.send_default();
}
if keys.just_pressed(KeyCode::KeyB) {
info!("sending begin");
begin.send_default();
}
if keys.just_pressed(KeyCode::KeyR) {
info!("sending request exit");
request_exit.send_default();
}
if keys.just_pressed(KeyCode::KeyI) {
info!("current state: {:?}", *state);
}
}
/// 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, 2.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()
});
commands.spawn(Camera3dBundle {
transform: Transform::from_xyz(-2.5, 4.5, 9.0).looking_at(Vec3::ZERO, Vec3::Y),
..default()
});
}

View File

@@ -0,0 +1,160 @@
use std::ops::Deref;
use bevy::prelude::*;
use bevy_mod_openxr::{
action_binding::{OxrSendActionBindings, OxrSuggestActionBinding},
action_set_attaching::OxrAttachActionSet,
action_set_syncing::{OxrActionSetSyncSet, OxrSyncActionSet},
add_xr_plugins,
init::create_xr_session,
resources::OxrInstance,
session::OxrSession,
spaces::OxrSpaceExt,
};
use bevy_mod_xr::{
session::{session_available, XrCreateSession, XrTrackingRoot},
spaces::XrSpace,
types::XrPose,
};
use openxr::Posef;
fn main() {
let mut app = App::new();
app.add_plugins(add_xr_plugins(DefaultPlugins));
app.add_systems(XrCreateSession, spawn_hands.after(create_xr_session));
app.add_systems(XrCreateSession, attach_set.after(create_xr_session));
app.add_systems(PreUpdate, sync_actions.before(OxrActionSetSyncSet));
app.add_systems(OxrSendActionBindings, suggest_action_bindings);
app.add_systems(Startup, create_actions.run_if(session_available));
app.add_systems(Startup, setup);
app.run();
}
fn attach_set(actions: Res<ControllerActions>, mut attach: EventWriter<OxrAttachActionSet>) {
attach.send(OxrAttachActionSet(actions.set.clone()));
}
#[derive(Resource)]
struct ControllerActions {
set: openxr::ActionSet,
left: openxr::Action<Posef>,
right: openxr::Action<Posef>,
}
fn sync_actions(actions: Res<ControllerActions>, mut sync: EventWriter<OxrSyncActionSet>) {
sync.send(OxrSyncActionSet(actions.set.clone()));
}
/// 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()
});
commands.spawn(Camera3dBundle {
transform: Transform::from_xyz(-2.5, 4.5, 9.0).looking_at(Vec3::ZERO, Vec3::Y),
..default()
});
}
fn suggest_action_bindings(
actions: Res<ControllerActions>,
mut bindings: EventWriter<OxrSuggestActionBinding>,
) {
bindings.send(OxrSuggestActionBinding {
action: actions.left.as_raw(),
interaction_profile: "/interaction_profiles/oculus/touch_controller".into(),
bindings: vec!["/user/hand/left/input/grip/pose".into()],
});
bindings.send(OxrSuggestActionBinding {
action: actions.right.as_raw(),
interaction_profile: "/interaction_profiles/oculus/touch_controller".into(),
bindings: vec!["/user/hand/right/input/grip/pose".into()],
});
}
fn create_actions(instance: Res<OxrInstance>, mut cmds: Commands) {
let set = instance.create_action_set("hands", "Hands", 0).unwrap();
let left = set
.create_action("left_pose", "Left Hand Grip Pose", &[])
.unwrap();
let right = set
.create_action("right_pose", "Right Hand Grip Pose", &[])
.unwrap();
cmds.insert_resource(ControllerActions { set, left, right })
}
fn spawn_hands(
actions: Res<ControllerActions>,
mut cmds: Commands,
root: Query<Entity, With<XrTrackingRoot>>,
session: Res<OxrSession>,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<StandardMaterial>>,
) {
// This is a demonstation of how to integrate with the openxr crate, the right space is the
// recommended way
let left_space = XrSpace::from_openxr_space(
actions
.left
.create_space(
session.deref().deref().clone(),
openxr::Path::NULL,
Posef::IDENTITY,
)
.unwrap(),
);
let right_space = session
.create_action_space(&actions.right, openxr::Path::NULL, XrPose::IDENTITY)
.unwrap();
let left = cmds
.spawn((
PbrBundle {
mesh: meshes.add(Cuboid::new(0.1, 0.1, 0.05)),
material: materials.add(Color::rgb_u8(124, 144, 255)),
transform: Transform::from_xyz(0.0, 0.5, 0.0),
..default()
},
left_space,
Controller,
))
.id();
let right = cmds
.spawn((
PbrBundle {
mesh: meshes.add(Cuboid::new(0.1, 0.1, 0.05)),
material: materials.add(Color::rgb_u8(124, 144, 255)),
transform: Transform::from_xyz(0.0, 0.5, 0.0),
..default()
},
right_space,
Controller,
))
.id();
cmds.entity(root.single()).push_children(&[left, right]);
}
#[derive(Component)]
struct Controller;

View File

@@ -0,0 +1,75 @@
//! A simple 3D scene with light shining over a cube sitting on a plane.
use bevy::prelude::*;
use bevy_mod_openxr::add_xr_plugins;
use bevy_mod_xr::session::{XrSessionPlugin, XrState};
fn main() {
App::new()
.add_plugins(add_xr_plugins(DefaultPlugins).set(XrSessionPlugin { auto_handle: false }))
.add_plugins(bevy_xr_utils::hand_gizmos::HandGizmosPlugin)
.add_systems(Startup, setup)
.add_systems(Update, handle_input)
.insert_resource(AmbientLight::default())
.run();
}
fn handle_input(
keys: Res<ButtonInput<KeyCode>>,
mut end: EventWriter<bevy_mod_xr::session::XrEndSessionEvent>,
mut destroy: EventWriter<bevy_mod_xr::session::XrDestroySessionEvent>,
mut begin: EventWriter<bevy_mod_xr::session::XrBeginSessionEvent>,
mut create: EventWriter<bevy_mod_xr::session::XrCreateSessionEvent>,
mut request_exit: EventWriter<bevy_mod_xr::session::XrRequestExitEvent>,
state: Res<XrState>,
) {
if keys.just_pressed(KeyCode::KeyE) {
info!("sending end");
end.send_default();
}
if keys.just_pressed(KeyCode::KeyC) {
info!("sending create");
create.send_default();
}
if keys.just_pressed(KeyCode::KeyD) {
info!("sending destroy");
destroy.send_default();
}
if keys.just_pressed(KeyCode::KeyB) {
info!("sending begin");
begin.send_default();
}
if keys.just_pressed(KeyCode::KeyR) {
info!("sending request exit");
request_exit.send_default();
}
if keys.just_pressed(KeyCode::KeyI) {
info!("current state: {:?}", *state);
}
}
/// 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()
});
commands.spawn(Camera3dBundle {
transform: Transform::from_xyz(-2.5, 4.5, 9.0).looking_at(Vec3::ZERO, Vec3::Y),
..default()
});
}

View File

@@ -0,0 +1,201 @@
//! A simple example of how to use the transform utils to set the players position and orientation
use bevy::prelude::*;
use bevy_mod_openxr::add_xr_plugins;
use bevy_xr_utils::transform_utils::{self, SnapToPosition, SnapToRotation};
use bevy_xr_utils::xr_utils_actions::{
ActiveSet, XRUtilsAction, XRUtilsActionSet, XRUtilsActionState, XRUtilsActionSystemSet,
XRUtilsActionsPlugin, XRUtilsBinding,
};
fn main() {
App::new()
.add_plugins(add_xr_plugins(DefaultPlugins))
.add_plugins(bevy_xr_utils::hand_gizmos::HandGizmosPlugin)
.add_plugins(transform_utils::TransformUtilitiesPlugin)
.add_systems(Startup, setup)
.add_plugins(XRUtilsActionsPlugin)
.add_systems(
Startup,
create_action_entities.before(XRUtilsActionSystemSet::CreateEvents),
)
.add_systems(
Update,
send_look_at_red_cube_event.after(XRUtilsActionSystemSet::SyncActionStates),
)
.add_systems(
Update,
send_recenter.after(XRUtilsActionSystemSet::SyncActionStates),
)
.insert_resource(AmbientLight {
color: Default::default(),
brightness: 500.0,
})
.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()
});
// red cube
commands.spawn(PbrBundle {
mesh: meshes.add(Cuboid::new(1.0, 1.0, 1.0)),
material: materials.add(Color::rgb_u8(252, 44, 3)),
transform: Transform::from_xyz(4.0, 0.5, 0.0).with_scale(Vec3::splat(0.5)),
..default()
});
// blue cube
commands.spawn(PbrBundle {
mesh: meshes.add(Cuboid::new(1.0, 1.0, 1.0)),
material: materials.add(Color::rgb_u8(3, 28, 252)),
transform: Transform::from_xyz(-4.0, 0.5, 0.0).with_scale(Vec3::splat(0.5)),
..default()
});
// green cube
commands.spawn(PbrBundle {
mesh: meshes.add(Cuboid::new(1.0, 1.0, 1.0)),
material: materials.add(Color::rgb_u8(3, 252, 32)),
transform: Transform::from_xyz(0.0, 0.5, 4.0).with_scale(Vec3::splat(0.5)),
..default()
});
// white cube
commands.spawn(PbrBundle {
mesh: meshes.add(Cuboid::new(1.0, 1.0, 1.0)),
material: materials.add(Color::rgb_u8(250, 250, 250)),
transform: Transform::from_xyz(0.0, 0.5, -4.0).with_scale(Vec3::splat(0.5)),
..default()
});
// black cube
commands.spawn(PbrBundle {
mesh: meshes.add(Cuboid::new(1.0, 1.0, 1.0)),
material: materials.add(Color::rgb_u8(0, 0, 0)),
transform: Transform::from_xyz(0.0, 0.1, 0.0).with_scale(Vec3::splat(0.2)),
..default()
});
commands.spawn(Camera3dBundle {
transform: Transform::from_xyz(-2.5, 4.5, 9.0).looking_at(Vec3::ZERO, Vec3::Y),
..default()
});
}
#[derive(Component)]
pub struct FaceRedAction;
fn create_action_entities(mut commands: Commands) {
//create a set
let set = commands
.spawn((
XRUtilsActionSet {
name: "locomotion".into(),
pretty_name: "locomotion set".into(),
priority: u32::MIN,
},
ActiveSet, //marker to indicate we want this synced
))
.id();
//create an action
let action = commands
.spawn((
XRUtilsAction {
action_name: "face_red".into(),
localized_name: "face_red_localized".into(),
action_type: bevy_mod_xr::actions::ActionType::Bool,
},
FaceRedAction, //lets try a marker component
))
.id();
//create a binding
let binding = commands
.spawn(XRUtilsBinding {
profile: "/interaction_profiles/valve/index_controller".into(),
binding: "/user/hand/left/input/a/click".into(),
})
.id();
//add action to set, this isnt the best
//TODO look into a better system
commands.entity(action).add_child(binding);
commands.entity(set).add_child(action);
let action = commands
.spawn((
XRUtilsAction {
action_name: "center".into(),
localized_name: "center_localized".into(),
action_type: bevy_mod_xr::actions::ActionType::Bool,
},
Center, //lets try a marker component
))
.id();
//create a binding
let binding = commands
.spawn(XRUtilsBinding {
profile: "/interaction_profiles/valve/index_controller".into(),
binding: "/user/hand/right/input/a/click".into(),
})
.id();
//add action to set, this isnt the best
//TODO look into a better system
commands.entity(action).add_child(binding);
commands.entity(set).add_child(action);
}
fn send_look_at_red_cube_event(
mut action_query: Query<&XRUtilsActionState, With<FaceRedAction>>,
mut event_writer: EventWriter<SnapToRotation>,
) {
//now for the actual checking
for state in action_query.iter_mut() {
match state {
XRUtilsActionState::Bool(state) => {
let send = state.current_state && state.changed_since_last_sync;
if send {
info!("send facing");
let quat = Transform::default()
.looking_at(Transform::from_xyz(4.0, 0.0, 0.0).translation, Vec3::Y); //this is a transform facing the red cube from the center of the scene, you should use the HMD posision but I was lazy.
event_writer.send(SnapToRotation(quat.rotation));
}
}
XRUtilsActionState::Float(_) => (),
XRUtilsActionState::Vector(_) => (),
}
}
}
#[derive(Component)]
pub struct Center;
fn send_recenter(
mut action_query: Query<&XRUtilsActionState, With<Center>>,
mut event_writer: EventWriter<SnapToPosition>,
) {
//now for the actual checking
for state in action_query.iter_mut() {
match state {
XRUtilsActionState::Bool(state) => {
let send = state.current_state && state.changed_since_last_sync;
if send {
let center = Transform::default().translation;
event_writer.send(SnapToPosition(center));
}
}
XRUtilsActionState::Float(_) => (),
XRUtilsActionState::Vector(_) => (),
}
}
}

View File

@@ -0,0 +1,4 @@
#[cfg(not(target_family = "wasm"))]
mod openxr;
#[cfg(not(target_family = "wasm"))]
pub use openxr::*;

View File

@@ -0,0 +1,102 @@
use std::borrow::Cow;
use std::ptr;
use bevy::ecs::schedule::ScheduleLabel;
use bevy::ecs::system::RunSystemOnce;
use bevy::prelude::*;
use bevy::utils::HashMap;
use bevy_mod_xr::session::XrSessionCreatedEvent;
use openxr::sys::ActionSuggestedBinding;
use crate::resources::OxrInstance;
impl Plugin for OxrActionBindingPlugin {
fn build(&self, app: &mut App) {
app.add_schedule(Schedule::new(OxrSendActionBindings));
app.add_event::<OxrSuggestActionBinding>();
app.add_systems(
Update,
run_action_binding_sugestion.run_if(on_event::<XrSessionCreatedEvent>()),
);
}
}
// This could for now be handled better with a SystemSet, but in the future we might want to add an
// Event to allow requesting binding suggestion for new actions
pub(crate) fn run_action_binding_sugestion(world: &mut World) {
world.run_schedule(OxrSendActionBindings);
world.run_system_once(bind_actions);
}
fn bind_actions(instance: Res<OxrInstance>, mut actions: EventReader<OxrSuggestActionBinding>) {
let mut bindings: HashMap<&str, Vec<ActionSuggestedBinding>> = HashMap::new();
for e in actions.read() {
bindings.entry(&e.interaction_profile).or_default().extend(
e.bindings
.clone()
.into_iter()
.filter_map(|b| match instance.string_to_path(&b) {
Ok(p) => Some(p),
Err(err) => {
error!(
"Unable to convert path: \"{}\"; error: {}",
b,
err.to_string()
);
None
}
})
.map(|p| ActionSuggestedBinding {
action: e.action,
binding: p,
})
.collect::<Vec<_>>(),
);
}
use openxr::sys;
for (profile, bindings) in bindings.iter() {
let interaction_profile = match instance.string_to_path(profile) {
Ok(v) => v,
Err(err) => {
error!(
"Unable to convert interaction profile path: \"{}\"; error: \"{}\" Skipping all suggestions for this interaction profile",
profile,
err.to_string()
);
continue;
}
};
// Using the raw way since we want all actions through one event and we can't use the
// Bindings from the openxr crate since they can't be created from raw actions
let info = sys::InteractionProfileSuggestedBinding {
ty: sys::InteractionProfileSuggestedBinding::TYPE,
next: ptr::null(),
interaction_profile,
count_suggested_bindings: bindings.len() as u32,
suggested_bindings: bindings.as_ptr() as *const _ as _,
};
match unsafe {
(instance.fp().suggest_interaction_profile_bindings)(instance.as_raw(), &info)
} {
openxr::sys::Result::ERROR_ACTIONSETS_ALREADY_ATTACHED => error!(
"Binding Suggested for an Action whith an ActionSet that was already attached!"
),
openxr::sys::Result::ERROR_PATH_INVALID => error!("Invalid Path Suggested!"),
openxr::sys::Result::ERROR_PATH_UNSUPPORTED => error!("Suggested Path Unsupported!"),
_ => {}
}
}
}
#[derive(Event, Clone)]
/// Only Send this for Actions that were not attached yet!
pub struct OxrSuggestActionBinding {
pub action: openxr::sys::Action,
pub interaction_profile: Cow<'static, str>,
pub bindings: Vec<Cow<'static, str>>,
}
pub struct OxrActionBindingPlugin;
// Maybe use a SystemSet in an XrStartup Schedule?
#[derive(ScheduleLabel, Hash, Debug, Clone, Copy, PartialEq, Eq)]
pub struct OxrSendActionBindings;

View File

@@ -0,0 +1,44 @@
use crate::{action_binding::run_action_binding_sugestion, session::OxrSession};
use bevy::prelude::*;
use bevy_mod_xr::session::XrSessionCreatedEvent;
impl Plugin for OxrActionAttachingPlugin {
fn build(&self, app: &mut App) {
app.add_event::<OxrAttachActionSet>();
app.add_systems(
PostUpdate,
attach_sets
.run_if(on_event::<XrSessionCreatedEvent>())
.after(run_action_binding_sugestion),
);
}
}
fn attach_sets(session: Res<OxrSession>, mut events: EventReader<OxrAttachActionSet>) {
let sets = events.read().map(|v| &v.0).collect::<Vec<_>>();
if sets.is_empty() {
return;
}
info!("attaching {} sessions", sets.len());
match session.attach_action_sets(&sets) {
Ok(_) => {
info!("attached sessions!")
}
Err(openxr::sys::Result::ERROR_ACTIONSETS_ALREADY_ATTACHED) => {
error!("Action Sets Already attached!");
}
Err(openxr::sys::Result::ERROR_HANDLE_INVALID) => error!("Invalid ActionSet Handle!"),
Err(e) => error!(
"Unhandled Error while attaching action sets: {}",
e.to_string()
),
};
}
#[derive(Event, Clone)]
/// Send this event for every ActionSet you want to attach to the [`OxrSession`] once the Session Status changed to Ready. all requests will
/// be applied in [`PostUpdate`]
pub struct OxrAttachActionSet(pub openxr::ActionSet);
pub struct OxrActionAttachingPlugin;

View File

@@ -0,0 +1,38 @@
use crate::session::OxrSession;
use bevy::prelude::*;
use bevy_mod_xr::session::session_running;
#[derive(SystemSet, Debug, Hash, PartialEq, Eq, Clone, Copy)]
pub struct OxrActionSetSyncSet;
impl Plugin for OxrActionSyncingPlugin {
fn build(&self, app: &mut App) {
app.add_event::<OxrSyncActionSet>();
app.add_systems(
PreUpdate,
sync_sets.in_set(OxrActionSetSyncSet).run_if(session_running), // .in_set(OxrPreUpdateSet::SyncActions),
);
}
}
fn sync_sets(session: Res<OxrSession>, mut events: EventReader<OxrSyncActionSet>) {
let sets = events
.read()
.map(|v| &v.0)
.map(openxr::ActiveActionSet::new)
.collect::<Vec<_>>();
if sets.is_empty() {
return;
}
if let Err(err) = session.sync_actions(&sets) {
warn!("error while syncing actionsets: {}", err.to_string());
}
}
#[derive(Event, Clone)]
/// Send this event for every ActionSet you want to attach to the [`OxrSession`] once the Session Status changed to Ready. all requests will
pub struct OxrSyncActionSet(pub openxr::ActionSet);
pub struct OxrActionSyncingPlugin;

View File

@@ -0,0 +1,117 @@
use std::borrow::Cow;
use std::fmt;
use super::graphics::GraphicsBackend;
use thiserror::Error;
#[derive(Error, Debug)]
pub enum OxrError {
#[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("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),
#[error("Graphics init error: {0}")]
InitError(InitError),
}
pub use init_error::InitError;
/// This module is needed because thiserror does not allow conditional compilation within enums for some reason,
/// so graphics api specific errors are implemented here.
mod init_error {
use super::OxrError;
use std::fmt;
#[derive(Debug)]
pub enum InitError {
#[cfg(feature = "vulkan")]
VulkanError(ash::vk::Result),
#[cfg(feature = "vulkan")]
VulkanLoadingError(ash::LoadingError),
#[cfg(all(feature = "d3d12", windows))]
FailedToFindD3D12Adapter,
}
impl fmt::Display for InitError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
#[cfg(feature = "vulkan")]
InitError::VulkanError(error) => write!(f, "Vulkan error: {}", error),
#[cfg(feature = "vulkan")]
InitError::VulkanLoadingError(error) => {
write!(f, "Vulkan loading error: {}", error)
}
#[cfg(all(feature = "d3d12", windows))]
InitError::FailedToFindD3D12Adapter => write!(
f,
"Failed to find D3D12 adapter matching LUID provided by the OpenXR runtime"
),
}
}
}
#[cfg(feature = "vulkan")]
impl From<ash::vk::Result> for OxrError {
fn from(value: ash::vk::Result) -> Self {
Self::InitError(InitError::VulkanError(value))
}
}
#[cfg(feature = "vulkan")]
impl From<ash::LoadingError> for OxrError {
fn from(value: ash::LoadingError) -> Self {
Self::InitError(InitError::VulkanLoadingError(value))
}
}
}
impl From<Vec<Cow<'static, str>>> for OxrError {
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(())
}
}

View File

@@ -0,0 +1,325 @@
use bevy::{ecs::system::Resource, prelude::{Deref, DerefMut}};
use openxr::ExtensionSet;
#[derive(Clone, Debug, Eq, PartialEq, Deref, DerefMut, Resource)]
pub struct OxrEnabledExtensions(pub OxrExtensions);
#[derive(Clone, Debug, Eq, PartialEq, Deref, DerefMut)]
pub struct OxrExtensions(ExtensionSet);
impl OxrExtensions {
pub fn raw_mut(&mut self) -> &mut ExtensionSet {
&mut self.0
}
pub fn raw(&self) -> &ExtensionSet {
&self.0
}
pub fn enable_fb_passthrough(&mut self) -> &mut Self {
self.0.fb_passthrough = true;
self
}
pub fn disable_fb_passthrough(&mut self) -> &mut Self {
self.0.fb_passthrough = false;
self
}
pub fn enable_hand_tracking(&mut self) -> &mut Self {
self.0.ext_hand_tracking = true;
self
}
pub fn disable_hand_tracking(&mut self) -> &mut Self {
self.0.ext_hand_tracking = false;
self
}
/// returns true if all of the extensions enabled are also available in `available_exts`
pub fn is_available(&self, available_exts: &OxrExtensions) -> bool {
self.clone() & available_exts.clone() == *self
}
}
impl From<ExtensionSet> for OxrExtensions {
fn from(value: ExtensionSet) -> Self {
Self(value)
}
}
impl From<OxrExtensions> for ExtensionSet {
fn from(val: OxrExtensions) -> Self {
val.0
}
}
impl Default for OxrExtensions {
fn default() -> Self {
let exts = ExtensionSet::default();
//exts.ext_hand_tracking = true;
Self(exts)
}
}
macro_rules! unavailable_exts {
(
$exts:ty;
$(
$(
#[$meta:meta]
)*
$ident:ident
),*
$(,)?
) => {
impl $exts {
/// Returns any extensions needed by `required_exts` that aren't available in `self`
pub fn unavailable_exts(&self, required_exts: &Self) -> Vec<std::borrow::Cow<'static, str>> {
let mut exts = vec![];
$(
$(
#[$meta]
)*
if required_exts.0.$ident && !self.0.$ident {
exts.push(std::borrow::Cow::Borrowed(stringify!($ident)))
}
)*
for ext in required_exts.0.other.iter() {
if !self.0.other.contains(ext) {
exts.push(std::borrow::Cow::Owned(ext.clone()))
}
}
exts
}
}
};
}
macro_rules! bitor {
(
$exts:ty;
$(
$(
#[$meta:meta]
)*
$ident:ident
),*
$(,)?
) => {
impl std::ops::BitOr for $exts {
type Output = Self;
fn bitor(self, rhs: Self) -> Self::Output {
let mut out = ExtensionSet::default();
$(
$(
#[$meta]
)*
{
out.$ident = self.0.$ident || rhs.0.$ident;
}
)*
out.other = self.0.other;
for ext in rhs.0.other {
if !out.other.contains(&ext) {
out.other.push(ext);
}
}
Self(out)
}
}
};
}
macro_rules! bitand {
(
$exts:ty;
$(
$(
#[$meta:meta]
)*
$ident:ident
),*
$(,)?
) => {
impl std::ops::BitAnd for $exts {
type Output = Self;
fn bitand(self, rhs: Self) -> Self::Output {
let mut out = ExtensionSet::default();
$(
$(
#[$meta]
)*
{
out.$ident = self.0.$ident && rhs.0.$ident;
}
)*
for ext in self.0.other {
if rhs.0.other.contains(&ext) {
out.other.push(ext);
}
}
Self(out)
}
}
};
}
macro_rules! impl_ext {
(
$(
$macro:ident
),*
) => {
$(
$macro! {
OxrExtensions;
almalence_digital_lens_control,
bd_controller_interaction,
epic_view_configuration_fov,
ext_performance_settings,
ext_thermal_query,
ext_debug_utils,
ext_eye_gaze_interaction,
ext_view_configuration_depth_range,
ext_conformance_automation,
ext_hand_tracking,
#[cfg(windows)]
ext_win32_appcontainer_compatible,
ext_dpad_binding,
ext_hand_joints_motion_range,
ext_samsung_odyssey_controller,
ext_hp_mixed_reality_controller,
ext_palm_pose,
ext_uuid,
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_alpha_blend,
#[cfg(target_os = "android")]
fb_android_surface_swapchain_create,
fb_swapchain_update_state,
fb_composition_layer_secure_content,
fb_body_tracking,
fb_display_refresh_rate,
fb_color_space,
fb_hand_tracking_mesh,
fb_hand_tracking_aim,
fb_hand_tracking_capsules,
fb_spatial_entity,
fb_foveation,
fb_foveation_configuration,
fb_keyboard_tracking,
fb_triangle_mesh,
fb_passthrough,
fb_render_model,
fb_spatial_entity_query,
fb_spatial_entity_storage,
fb_foveation_vulkan,
#[cfg(target_os = "android")]
fb_swapchain_update_state_android_surface,
fb_swapchain_update_state_opengl_es,
fb_swapchain_update_state_vulkan,
fb_touch_controller_pro,
fb_spatial_entity_sharing,
fb_space_warp,
fb_haptic_amplitude_envelope,
fb_scene,
fb_scene_capture,
fb_spatial_entity_container,
fb_face_tracking,
fb_eye_tracking_social,
fb_passthrough_keyboard_hands,
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_facial_tracking,
htc_vive_focus3_controller_interaction,
htc_hand_interaction,
htc_vive_wrist_tracker_interaction,
htc_passthrough,
htc_foveation,
huawei_controller_interaction,
#[cfg(target_os = "android")]
khr_android_thread_settings,
#[cfg(target_os = "android")]
khr_android_surface_swapchain,
khr_composition_layer_cube,
#[cfg(target_os = "android")]
khr_android_create_instance,
khr_composition_layer_depth,
khr_vulkan_swapchain_format_list,
khr_composition_layer_cylinder,
khr_composition_layer_equirect,
khr_opengl_enable,
khr_opengl_es_enable,
khr_vulkan_enable,
#[cfg(windows)]
khr_d3d11_enable,
#[cfg(windows)]
khr_d3d12_enable,
khr_visibility_mask,
khr_composition_layer_color_scale_bias,
#[cfg(windows)]
khr_win32_convert_performance_counter_time,
khr_convert_timespec_time,
khr_loader_init,
#[cfg(target_os = "android")]
khr_loader_init_android,
khr_vulkan_enable2,
khr_composition_layer_equirect2,
khr_binding_modification,
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_performance_metrics,
meta_headset_id,
meta_passthrough_color_lut,
ml_ml2_controller_interaction,
ml_frame_end_info,
ml_global_dimmer,
ml_compat,
ml_user_calibration,
mnd_headless,
mnd_swapchain_usage_input_attachment_bit,
msft_unbounded_reference_space,
msft_spatial_anchor,
msft_spatial_graph_bridge,
msft_hand_interaction,
msft_hand_tracking_mesh,
msft_secondary_view_configuration,
msft_first_person_observer,
msft_controller_model,
#[cfg(windows)]
msft_perception_anchor_interop,
#[cfg(windows)]
msft_holographic_window_attachment,
msft_composition_layer_reprojection,
msft_spatial_anchor_persistence,
#[cfg(target_os = "android")]
oculus_android_session_state_enable,
oculus_audio_device_guid,
oculus_external_camera,
oppo_controller_interaction,
qcom_tracking_optimization_settings,
ultraleap_hand_tracking_forearm,
valve_analog_threshold,
varjo_quad_views,
varjo_foveated_rendering,
varjo_composition_layer_depth_test,
varjo_environment_depth_estimation,
varjo_marker_tracking,
varjo_view_offset,
yvr_controller_interaction,
}
)*
};
}
impl_ext!(bitor, bitand, unavailable_exts);

View File

@@ -0,0 +1,196 @@
use bevy::prelude::*;
use bevy_mod_xr::hands::{LeftHand, RightHand, XrHandBoneEntities, HAND_JOINT_COUNT};
use bevy_mod_xr::session::{XrCreateSession, XrDestroySession, XrTrackingRoot};
use bevy_mod_xr::spaces::{XrPrimaryReferenceSpace, XrReferenceSpace};
use bevy_mod_xr::{
hands::{HandBone, HandBoneRadius},
session::session_running,
};
use openxr::SpaceLocationFlags;
use crate::init::create_xr_session;
use crate::resources::OxrFrameState;
use crate::resources::Pipelined;
use crate::session::OxrSession;
pub struct HandTrackingPlugin {
default_hands: bool,
}
impl Default for HandTrackingPlugin {
fn default() -> Self {
Self {
default_hands: true,
}
}
}
impl Plugin for HandTrackingPlugin {
fn build(&self, app: &mut App) {
app.add_systems(PreUpdate, locate_hands.run_if(session_running));
if self.default_hands {
app.add_systems(XrDestroySession, clean_up_default_hands)
.add_systems(
XrCreateSession,
(spawn_default_hands, apply_deferred)
.chain()
.after(create_xr_session),
);
}
}
}
pub fn spawn_hand_bones<T: Bundle + Clone>(
cmds: &mut Commands,
bundle: T,
) -> [Entity; HAND_JOINT_COUNT] {
let mut bones: [Entity; HAND_JOINT_COUNT] = [Entity::PLACEHOLDER; HAND_JOINT_COUNT];
// screw you clippy, i don't see a better way to init this array
#[allow(clippy::needless_range_loop)]
for bone in HandBone::get_all_bones().into_iter() {
bones[bone as usize] = cmds
.spawn((SpatialBundle::default(), bone, HandBoneRadius(0.0)))
.insert(bundle.clone())
.id();
}
bones
}
fn spawn_default_hands(
mut cmds: Commands,
session: Res<OxrSession>,
root: Query<Entity, With<XrTrackingRoot>>,
) {
debug!("spawning default hands");
let Ok(root) = root.get_single() else {
error!("unable to get tracking root, skipping hand creation");
return;
};
let tracker_left = match session.create_hand_tracker(openxr::HandEXT::LEFT) {
Ok(t) => t,
Err(openxr::sys::Result::ERROR_EXTENSION_NOT_PRESENT) => {
warn!("Handtracking Extension not loaded, Unable to create Handtracker!");
return;
}
Err(err) => {
warn!("Error while creating Handtracker: {}", err.to_string());
return;
}
};
let tracker_right = match session.create_hand_tracker(openxr::HandEXT::RIGHT) {
Ok(t) => t,
Err(openxr::sys::Result::ERROR_EXTENSION_NOT_PRESENT) => {
warn!("Handtracking Extension not loaded, Unable to create Handtracker!");
return;
}
Err(err) => {
warn!("Error while creating Handtracker: {}", err.to_string());
return;
}
};
let left_bones = spawn_hand_bones(&mut cmds, (DefaultHandBone, LeftHand));
let right_bones = spawn_hand_bones(&mut cmds, (DefaultHandBone, RightHand));
cmds.entity(root).push_children(&left_bones);
cmds.entity(root).push_children(&right_bones);
cmds.spawn((
DefaultHandTracker,
OxrHandTracker(tracker_left),
XrHandBoneEntities(left_bones),
LeftHand,
));
cmds.spawn((
DefaultHandTracker,
OxrHandTracker(tracker_right),
XrHandBoneEntities(right_bones),
RightHand,
));
}
#[derive(Component, Clone, Copy)]
struct DefaultHandTracker;
#[derive(Component, Clone, Copy)]
struct DefaultHandBone;
#[allow(clippy::type_complexity)]
fn clean_up_default_hands(
mut cmds: Commands,
query: Query<Entity, Or<(With<DefaultHandTracker>, With<DefaultHandBone>)>>,
) {
for e in &query {
debug!("removing default hand entity");
cmds.entity(e).despawn_recursive();
}
}
#[derive(Deref, DerefMut, Component)]
pub struct OxrHandTracker(pub openxr::HandTracker);
fn locate_hands(
default_ref_space: Res<XrPrimaryReferenceSpace>,
frame_state: Res<OxrFrameState>,
tracker_query: Query<(
&OxrHandTracker,
Option<&XrReferenceSpace>,
&XrHandBoneEntities,
)>,
session: Res<OxrSession>,
mut bone_query: Query<(&HandBone, &mut HandBoneRadius, &mut Transform)>,
pipelined: Option<Res<Pipelined>>,
) {
for (tracker, ref_space, hand_entities) in &tracker_query {
let ref_space = ref_space.map(|v| &v.0).unwrap_or(&default_ref_space.0);
// relate_hand_joints also provides velocities
let joints = match session.locate_hand_joints(
tracker,
ref_space,
if pipelined.is_some() {
openxr::Time::from_nanos(
frame_state.predicted_display_time.as_nanos()
+ frame_state.predicted_display_period.as_nanos(),
)
} else {
frame_state.predicted_display_time
},
) {
Ok(Some(v)) => v,
Ok(None) => continue,
Err(openxr::sys::Result::ERROR_EXTENSION_NOT_PRESENT) => {
error!("HandTracking Extension not loaded");
continue;
}
Err(err) => {
warn!("Error while locating hand joints: {}", err.to_string());
continue;
}
};
let bone_entities = match bone_query.get_many_mut(hand_entities.0) {
Ok(v) => v,
Err(err) => {
warn!("unable to get entities, {}", err);
continue;
}
};
for (bone, mut bone_radius, mut transform) in bone_entities {
let joint = joints[*bone as usize];
**bone_radius = joint.radius;
if joint
.location_flags
.contains(SpaceLocationFlags::POSITION_VALID)
{
transform.translation.x = joint.pose.position.x;
transform.translation.y = joint.pose.position.y;
transform.translation.z = joint.pose.position.z;
}
if joint
.location_flags
.contains(SpaceLocationFlags::ORIENTATION_VALID)
{
transform.rotation.x = joint.pose.orientation.x;
transform.rotation.y = joint.pose.orientation.y;
transform.rotation.z = joint.pose.orientation.z;
transform.rotation.w = joint.pose.orientation.w;
}
}
}
}

View File

@@ -0,0 +1,4 @@
pub mod handtracking;
#[cfg(feature = "passthrough")]
pub mod passthrough;
pub mod overlay;

View File

@@ -0,0 +1,96 @@
use std::{mem, ptr};
use bevy::prelude::*;
use bevy_mod_xr::session::session_available;
use openxr::sys;
use crate::{
next_chain::{OxrNextChainStructBase, OxrNextChainStructProvider},
openxr::exts::OxrEnabledExtensions,
session::{OxrSessionCreateNextChain, OxrSessionCreateNextProvider},
};
pub struct OxrOverlayPlugin;
impl Plugin for OxrOverlayPlugin {
fn build(&self, app: &mut bevy::prelude::App) {
app.add_event::<OxrOverlaySessionEvent>();
app.init_resource::<OxrOverlaySettings>();
app.add_systems(First, add_overlay_info_to_chain.run_if(session_available));
}
}
#[derive(Resource)]
pub struct OxrOverlaySettings {
pub session_layer_placement: u32,
pub flags: openxr::OverlaySessionCreateFlagsEXTX,
}
impl Default for OxrOverlaySettings {
fn default() -> Self {
OxrOverlaySettings {
session_layer_placement: 0,
flags: openxr::OverlaySessionCreateFlagsEXTX::EMPTY,
}
}
}
fn add_overlay_info_to_chain(
mut chain: NonSendMut<OxrSessionCreateNextChain>,
exts: Res<OxrEnabledExtensions>,
settings: Res<OxrOverlaySettings>,
) {
if exts.other.contains(&"XR_EXTX_overlay\0".to_string()) {
chain.push(OxrSessionCreateInfoOverlay::new(
settings.flags,
settings.session_layer_placement,
));
}
}
#[derive(Event, Clone, Copy, Debug)]
pub enum OxrOverlaySessionEvent {
MainSessionVisibilityChanged {
visible: bool,
flags: openxr::OverlayMainSessionFlagsEXTX,
},
}
pub struct OxrSessionCreateInfoOverlay {
inner: sys::SessionCreateInfoOverlayEXTX,
}
impl OxrSessionCreateInfoOverlay {
pub const fn new(
flags: openxr::OverlaySessionCreateFlagsEXTX,
session_layers_placement: u32,
) -> Self {
Self {
inner: sys::SessionCreateInfoOverlayEXTX {
ty: sys::SessionCreateInfoOverlayEXTX::TYPE,
next: ptr::null(),
create_flags: flags,
session_layers_placement,
},
}
}
}
impl Default for OxrSessionCreateInfoOverlay {
fn default() -> Self {
Self::new(openxr::OverlaySessionCreateFlagsEXTX::EMPTY, 0)
}
}
impl OxrNextChainStructProvider for OxrSessionCreateInfoOverlay {
fn header(&self) -> &OxrNextChainStructBase {
unsafe { mem::transmute(&self.inner) }
}
fn set_next(&mut self, next: &OxrNextChainStructBase) {
self.inner.next = next as *const _ as *const _;
}
fn clear_next(&mut self) {
self.inner.next = ptr::null();
}
}
impl OxrSessionCreateNextProvider for OxrSessionCreateInfoOverlay {}

View File

@@ -0,0 +1,119 @@
use bevy::prelude::*;
use bevy::render::Render;
use bevy::render::RenderApp;
use bevy::render::RenderSet;
use openxr::sys::SystemPassthroughProperties2FB;
use openxr::PassthroughCapabilityFlagsFB;
use crate::layer_builder::PassthroughLayer;
use crate::resources::*;
use crate::session::OxrSession;
use crate::types::*;
pub struct OxrPassthroughPlugin;
impl Plugin for OxrPassthroughPlugin {
fn build(&self, app: &mut App) {
let resources = app
.world
.get_resource::<OxrInstance>()
.and_then(|instance| {
app.world
.get_resource::<OxrSystemId>()
.map(|system_id| (instance, system_id))
});
if resources.is_some_and(|(instance, system)| {
supports_passthrough(instance, *system).is_ok_and(|s| s)
}) {
app.sub_app_mut(RenderApp).add_systems(
Render,
insert_passthrough
.in_set(RenderSet::PrepareAssets)
.run_if(resource_added::<OxrSession>),
);
} else {
error!("Passthrough is not supported with this runtime")
}
}
}
pub fn insert_passthrough(world: &mut World) {
let session = world.resource::<OxrSession>();
let (passthrough, passthrough_layer) = create_passthrough(
session,
openxr::PassthroughFlagsFB::IS_RUNNING_AT_CREATION,
openxr::PassthroughLayerPurposeFB::RECONSTRUCTION,
)
.unwrap();
world
.resource_mut::<OxrRenderLayers>()
.insert(0, Box::new(PassthroughLayer));
world.insert_resource(passthrough);
world.insert_resource(passthrough_layer);
}
pub fn resume_passthrough(
passthrough: Res<OxrPassthrough>,
passthrough_layer: Res<OxrPassthroughLayer>,
) {
passthrough.start().unwrap();
passthrough_layer.resume().unwrap();
}
pub fn pause_passthrough(
passthrough: Res<OxrPassthrough>,
passthrough_layer: Res<OxrPassthroughLayer>,
) {
passthrough_layer.pause().unwrap();
passthrough.pause().unwrap();
}
pub fn create_passthrough(
session: &OxrSession,
flags: openxr::PassthroughFlagsFB,
purpose: openxr::PassthroughLayerPurposeFB,
) -> Result<(OxrPassthrough, OxrPassthroughLayer)> {
let passthrough = session.create_passthrough(flags)?;
let passthrough_layer = session.create_passthrough_layer(&passthrough, purpose)?;
Ok((passthrough, passthrough_layer))
}
#[inline]
pub fn supports_passthrough(instance: &OxrInstance, system: OxrSystemId) -> Result<bool> {
if instance.exts().fb_passthrough.is_none() {
return Ok(false);
}
unsafe {
let mut hand = openxr::sys::SystemPassthroughProperties2FB {
ty: SystemPassthroughProperties2FB::TYPE,
next: std::ptr::null(),
capabilities: PassthroughCapabilityFlagsFB::PASSTHROUGH_CAPABILITY,
};
let mut p = openxr::sys::SystemProperties::out(&mut hand as *mut _ as _);
cvt((instance.fp().get_system_properties)(
instance.as_raw(),
system.0,
p.as_mut_ptr(),
))?;
bevy::log::info!(
"From supports_passthrough: Passthrough capabilities: {:?}",
hand.capabilities
);
Ok(
(hand.capabilities & PassthroughCapabilityFlagsFB::PASSTHROUGH_CAPABILITY)
== PassthroughCapabilityFlagsFB::PASSTHROUGH_CAPABILITY,
)
}
}
fn cvt(x: openxr::sys::Result) -> openxr::Result<openxr::sys::Result> {
if x.into_raw() >= 0 {
Ok(x)
} else {
Err(x)
}
}

View File

@@ -0,0 +1,202 @@
#[cfg(all(feature = "d3d12", windows))]
mod d3d12;
#[cfg(feature = "vulkan")]
pub mod vulkan;
use std::any::TypeId;
use bevy::math::UVec2;
use openxr::{FrameStream, FrameWaiter, Session};
use crate::{session::OxrSessionCreateNextChain, types::{AppInfo, OxrExtensions, Result, WgpuGraphics}};
/// This is an extension trait to the [`Graphics`](openxr::Graphics) trait and is how the graphics API should be interacted with.
pub unsafe trait GraphicsExt: openxr::Graphics {
/// Wrap the graphics specific type into the [GraphicsWrap] enum
fn wrap<T: GraphicsType>(item: T::Inner<Self>) -> GraphicsWrap<T>;
/// Returns all of the required openxr extensions to use this graphics API.
fn required_exts() -> OxrExtensions;
/// Convert from wgpu format to the graphics format
fn from_wgpu_format(format: wgpu::TextureFormat) -> Option<Self::Format>;
/// Convert from the graphics format to wgpu format
fn into_wgpu_format(format: Self::Format) -> Option<wgpu::TextureFormat>;
/// Convert an API specific swapchain image to a [`Texture`](wgpu::Texture).
///
/// # Safety
///
/// The `image` argument must be a valid handle.
unsafe fn to_wgpu_img(
image: Self::SwapchainImage,
device: &wgpu::Device,
format: wgpu::TextureFormat,
resolution: UVec2,
) -> Result<wgpu::Texture>;
/// Initialize graphics for this backend and return a [`WgpuGraphics`] for bevy and an API specific [Self::SessionCreateInfo] for openxr
fn init_graphics(
app_info: &AppInfo,
instance: &openxr::Instance,
system_id: openxr::SystemId,
) -> Result<(WgpuGraphics, Self::SessionCreateInfo)>;
unsafe fn create_session(
instance: &openxr::Instance,
system_id: openxr::SystemId,
info: &Self::SessionCreateInfo,
session_create_info_chain: &mut OxrSessionCreateNextChain,
) -> openxr::Result<(Session<Self>, FrameWaiter, FrameStream<Self>)>;
}
/// A type that can be used in [`GraphicsWrap`].
///
/// # Example
///
/// ```
/// pub struct XrSession(GraphicsWrap<XrSession>);
///
/// impl GraphicsType for XrSession {
/// type Inner<G: GraphicsExt> = openxr::Session<G>;
/// }
///
/// ```
///
/// In this example, `GraphicsWrap<XrSession>` is now an enum with variants for each graphics API. The enum can be matched to get the graphics specific inner type.
pub trait GraphicsType {
type Inner<G: GraphicsExt>;
}
impl GraphicsType for () {
type Inner<G: GraphicsExt> = ();
}
/// This is a special variant of [GraphicsWrap] using the unit struct as the inner type. This is to simply represent a graphics backend without storing data.
pub type GraphicsBackend = GraphicsWrap<()>;
impl GraphicsBackend {
const ALL: &'static [Self] = &[
#[cfg(feature = "vulkan")]
Self::Vulkan(()),
#[cfg(all(feature = "d3d12", windows))]
Self::D3D12(()),
];
pub fn available_backends(exts: &OxrExtensions) -> Vec<Self> {
Self::ALL
.iter()
.copied()
.filter(|backend| backend.is_available(exts))
.collect()
}
pub fn is_available(&self, exts: &OxrExtensions) -> bool {
self.required_exts().is_available(exts)
}
pub fn required_exts(&self) -> OxrExtensions {
graphics_match!(
self;
_ => Api::required_exts()
)
}
}
/// This struct is for creating agnostic objects for OpenXR graphics API specific structs.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum GraphicsWrap<T: GraphicsType> {
#[cfg(feature = "vulkan")]
Vulkan(T::Inner<openxr::Vulkan>),
#[cfg(all(feature = "d3d12", windows))]
D3D12(T::Inner<openxr::D3D12>),
}
impl<T: GraphicsType> GraphicsWrap<T> {
/// Returns the name of the graphics api this struct is using.
pub fn graphics_name(&self) -> &'static str {
graphics_match!(
self;
_ => std::any::type_name::<Api>()
)
}
fn graphics_type(&self) -> TypeId {
graphics_match!(
self;
_ => TypeId::of::<Api>()
)
}
/// Checks if this struct is using the wanted graphics api.
pub fn using_graphics<G: GraphicsExt + 'static>(&self) -> bool {
self.graphics_type() == TypeId::of::<G>()
}
/// Checks if the two values are both using the same graphics backend
pub fn using_graphics_of_val<V: GraphicsType>(&self, other: &GraphicsWrap<V>) -> bool {
self.graphics_type() == other.graphics_type()
}
}
/// This macro can be used to quickly run the same code for every variant of [GraphicsWrap].
/// The first argument should be an expression that returns either a reference or owned value of [GraphicsWrap].
/// The second argument should be a match arm with the pattern for the inner type.
///
/// # Example
///
/// ```
/// pub struct OxrFrameStream(GraphicsWrap<OxrFrameStream>);
///
/// impl GraphicsType for OxrFrameStream {
/// // Here is the inner type
/// type Inner<G: GraphicsExt> = openxr::FrameStream<G>;
/// }
///
/// fn begin(frame_stream: &mut OxrFrameStream) {
/// graphics_match! {
/// // get the inner 'GraphicsWrap' struct
/// &mut frame_stream.0;
/// // now we can handle every match arm with a single arm
/// // important: the first pattern here represents the inner type of `GraphicsWrap`
/// // it is already destructured for you.
/// stream => stream.begin();
/// }
/// }
/// ```
///
/// Additionally, if you want the type that implements `GraphicsExt` in the scope of the match body, you can access that type through the type alias `Api`.
macro_rules! graphics_match {
(
$field:expr;
$var:pat => $expr:expr $(=> $($return:tt)*)?
) => {
match $field {
#[cfg(feature = "vulkan")]
$crate::graphics::GraphicsWrap::Vulkan($var) => {
#[allow(unused)]
type Api = openxr::Vulkan;
graphics_match!(@arm_impl Vulkan; $expr $(=> $($return)*)?)
},
#[cfg(all(feature = "d3d12", windows))]
$crate::graphics::GraphicsWrap::D3D12($var) => {
#[allow(unused)]
type Api = openxr::D3D12;
graphics_match!(@arm_impl D3D12; $expr $(=> $($return)*)?)
},
}
};
(
@arm_impl
$variant:ident;
$expr:expr => $wrap_ty:ty
) => {
$crate::graphics::GraphicsWrap::<$wrap_ty>::$variant($expr)
};
(
@arm_impl
$variant:ident;
$expr:expr
) => {
$expr
};
}
pub(crate) use graphics_match;

View File

@@ -0,0 +1,393 @@
use bevy::log::error;
use openxr::sys;
use wgpu_hal::{Adapter, Instance};
use winapi::shared::dxgiformat::DXGI_FORMAT;
use winapi::um::d3d12 as winapi_d3d12;
use super::{GraphicsExt, GraphicsType, GraphicsWrap};
use crate::error::OxrError;
use crate::session::OxrSessionCreateNextChain;
use crate::types::{AppInfo, OxrExtensions, Result, WgpuGraphics};
unsafe impl GraphicsExt for openxr::D3D12 {
fn wrap<T: GraphicsType>(item: T::Inner<Self>) -> GraphicsWrap<T> {
GraphicsWrap::D3D12(item)
}
fn required_exts() -> OxrExtensions {
let mut extensions = openxr::ExtensionSet::default();
extensions.khr_d3d12_enable = true;
extensions.into()
}
fn from_wgpu_format(format: wgpu::TextureFormat) -> Option<Self::Format> {
wgpu_to_d3d12(format)
}
fn into_wgpu_format(format: Self::Format) -> Option<wgpu::TextureFormat> {
d3d12_to_wgpu(format)
}
unsafe fn to_wgpu_img(
image: Self::SwapchainImage,
device: &wgpu::Device,
format: wgpu::TextureFormat,
resolution: bevy::prelude::UVec2,
) -> Result<wgpu::Texture> {
let wgpu_hal_texture = <wgpu_hal::dx12::Api as wgpu_hal::Api>::Device::texture_from_raw(
d3d12::ComPtr::from_raw(image as *mut _),
format,
wgpu::TextureDimension::D2,
wgpu::Extent3d {
width: resolution.x,
height: resolution.y,
depth_or_array_layers: 2,
},
1,
1,
);
let texture = device.create_texture_from_hal::<wgpu_hal::dx12::Api>(
wgpu_hal_texture,
&wgpu::TextureDescriptor {
label: Some("VR Swapchain"),
size: wgpu::Extent3d {
width: resolution.x,
height: resolution.y,
depth_or_array_layers: 2,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: format,
usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_DST,
view_formats: &[],
},
);
Ok(texture)
}
fn init_graphics(
app_info: &AppInfo,
instance: &openxr::Instance,
system_id: openxr::SystemId,
) -> Result<(WgpuGraphics, Self::SessionCreateInfo)> {
let reqs = instance.graphics_requirements::<openxr::D3D12>(system_id)?;
let instance_descriptor = &wgpu_hal::InstanceDescriptor {
name: &app_info.name,
dx12_shader_compiler: wgpu::util::dx12_shader_compiler_from_env().unwrap_or(
wgpu::Dx12Compiler::Dxc {
dxil_path: None,
dxc_path: None,
},
),
flags: wgpu::InstanceFlags::from_build_config().with_env(),
gles_minor_version: Default::default(),
};
let wgpu_raw_instance: wgpu_hal::dx12::Instance =
unsafe { wgpu_hal::dx12::Instance::init(instance_descriptor)? };
let wgpu_adapters: Vec<wgpu_hal::ExposedAdapter<wgpu_hal::dx12::Api>> =
unsafe { wgpu_raw_instance.enumerate_adapters() };
let wgpu_exposed_adapter = wgpu_adapters
.into_iter()
.find(|a| {
let mut desc = unsafe { std::mem::zeroed() };
unsafe { a.adapter.raw_adapter().GetDesc1(&mut desc) };
desc.AdapterLuid.HighPart == reqs.adapter_luid.HighPart
&& desc.AdapterLuid.LowPart == reqs.adapter_luid.LowPart
})
.ok_or(OxrError::InitError(
crate::error::InitError::FailedToFindD3D12Adapter,
))?;
let wgpu_instance =
unsafe { wgpu::Instance::from_hal::<wgpu_hal::api::Dx12>(wgpu_raw_instance) };
let wgpu_features = wgpu::Features::TEXTURE_ADAPTER_SPECIFIC_FORMAT_FEATURES
| wgpu::Features::MULTIVIEW
| wgpu::Features::MULTI_DRAW_INDIRECT_COUNT
| wgpu::Features::MULTI_DRAW_INDIRECT;
let wgpu_limits = wgpu_exposed_adapter.capabilities.limits.clone();
let wgpu_open_device = unsafe {
wgpu_exposed_adapter
.adapter
.open(wgpu_features, &wgpu_limits)?
};
let device_supported_feature_level: d3d12::FeatureLevel =
get_device_feature_level(wgpu_open_device.device.raw_device());
if (device_supported_feature_level as u32) < (reqs.min_feature_level as u32) {
error!(
"OpenXR runtime requires D3D12 feature level >= {}",
reqs.min_feature_level
);
return Err(OxrError::FailedGraphicsRequirements);
}
let wgpu_adapter = unsafe { wgpu_instance.create_adapter_from_hal(wgpu_exposed_adapter) };
let raw_device = wgpu_open_device.device.raw_device().as_mut_ptr();
let raw_queue = wgpu_open_device.device.raw_queue().as_mut_ptr();
let (wgpu_device, wgpu_queue) = unsafe {
wgpu_adapter.create_device_from_hal(
wgpu_open_device,
&wgpu::DeviceDescriptor {
label: Some("bevy_oxr device"),
required_features: wgpu_features,
required_limits: wgpu_limits,
},
None,
)?
};
Ok((
WgpuGraphics(
wgpu_device,
wgpu_queue,
wgpu_adapter.get_info(),
wgpu_adapter,
wgpu_instance,
),
Self::SessionCreateInfo {
device: raw_device.cast(),
queue: raw_queue.cast(),
},
))
}
unsafe fn create_session(
instance: &openxr::Instance,
system_id: openxr::SystemId,
info: &Self::SessionCreateInfo,
session_create_info_chain: &mut OxrSessionCreateNextChain,
) -> openxr::Result<(
openxr::Session<Self>,
openxr::FrameWaiter,
openxr::FrameStream<Self>,
)> {
let binding = sys::GraphicsBindingD3D12KHR {
ty: sys::GraphicsBindingD3D12KHR::TYPE,
next: session_create_info_chain.chain_pointer(),
device: info.device,
queue: info.queue,
};
let info = sys::SessionCreateInfo {
ty: sys::SessionCreateInfo::TYPE,
next: &binding as *const _ as *const _,
create_flags: Default::default(),
system_id: system_id,
};
let mut out = sys::Session::NULL;
cvt((instance.fp().create_session)(
instance.as_raw(),
&info,
&mut out,
))?;
Ok(openxr::Session::from_raw(
instance.clone(),
out,
Box::new(()),
))
}
}
fn cvt(x: sys::Result) -> openxr::Result<sys::Result> {
if x.into_raw() >= 0 {
Ok(x)
} else {
Err(x)
}
}
// Extracted from https://github.com/gfx-rs/wgpu/blob/1161a22f4fbb4fc204eb06f2ac4243f83e0e980d/wgpu-hal/src/dx12/adapter.rs#L73-L94
// license: MIT OR Apache-2.0
fn get_device_feature_level(
device: &d3d12::ComPtr<winapi_d3d12::ID3D12Device>,
) -> d3d12::FeatureLevel {
// Detect the highest supported feature level.
let d3d_feature_level = [
d3d12::FeatureLevel::L12_1,
d3d12::FeatureLevel::L12_0,
d3d12::FeatureLevel::L11_1,
d3d12::FeatureLevel::L11_0,
];
type FeatureLevelsInfo = winapi_d3d12::D3D12_FEATURE_DATA_FEATURE_LEVELS;
let mut device_levels: FeatureLevelsInfo = unsafe { std::mem::zeroed() };
device_levels.NumFeatureLevels = d3d_feature_level.len() as u32;
device_levels.pFeatureLevelsRequested = d3d_feature_level.as_ptr().cast();
unsafe {
device.CheckFeatureSupport(
winapi_d3d12::D3D12_FEATURE_FEATURE_LEVELS,
(&mut device_levels as *mut FeatureLevelsInfo).cast(),
std::mem::size_of::<FeatureLevelsInfo>() as _,
)
};
// This cast should never fail because we only requested feature levels that are already in the enum.
let max_feature_level = d3d12::FeatureLevel::try_from(device_levels.MaxSupportedFeatureLevel)
.expect("Unexpected feature level");
max_feature_level
}
fn d3d12_to_wgpu(format: DXGI_FORMAT) -> Option<wgpu::TextureFormat> {
use wgpu::TextureFormat as Tf;
use winapi::shared::dxgiformat::*;
Some(match format {
DXGI_FORMAT_R8_UNORM => Tf::R8Unorm,
DXGI_FORMAT_R8_SNORM => Tf::R8Snorm,
DXGI_FORMAT_R8_UINT => Tf::R8Uint,
DXGI_FORMAT_R8_SINT => Tf::R8Sint,
DXGI_FORMAT_R16_UINT => Tf::R16Uint,
DXGI_FORMAT_R16_SINT => Tf::R16Sint,
DXGI_FORMAT_R16_UNORM => Tf::R16Unorm,
DXGI_FORMAT_R16_SNORM => Tf::R16Snorm,
DXGI_FORMAT_R16_FLOAT => Tf::R16Float,
DXGI_FORMAT_R8G8_UNORM => Tf::Rg8Unorm,
DXGI_FORMAT_R8G8_SNORM => Tf::Rg8Snorm,
DXGI_FORMAT_R8G8_UINT => Tf::Rg8Uint,
DXGI_FORMAT_R8G8_SINT => Tf::Rg8Sint,
DXGI_FORMAT_R16G16_UNORM => Tf::Rg16Unorm,
DXGI_FORMAT_R16G16_SNORM => Tf::Rg16Snorm,
DXGI_FORMAT_R32_UINT => Tf::R32Uint,
DXGI_FORMAT_R32_SINT => Tf::R32Sint,
DXGI_FORMAT_R32_FLOAT => Tf::R32Float,
DXGI_FORMAT_R16G16_UINT => Tf::Rg16Uint,
DXGI_FORMAT_R16G16_SINT => Tf::Rg16Sint,
DXGI_FORMAT_R16G16_FLOAT => Tf::Rg16Float,
DXGI_FORMAT_R8G8B8A8_UNORM => Tf::Rgba8Unorm,
DXGI_FORMAT_R8G8B8A8_UNORM_SRGB => Tf::Rgba8UnormSrgb,
DXGI_FORMAT_B8G8R8A8_UNORM_SRGB => Tf::Bgra8UnormSrgb,
DXGI_FORMAT_R8G8B8A8_SNORM => Tf::Rgba8Snorm,
DXGI_FORMAT_B8G8R8A8_UNORM => Tf::Bgra8Unorm,
DXGI_FORMAT_R8G8B8A8_UINT => Tf::Rgba8Uint,
DXGI_FORMAT_R8G8B8A8_SINT => Tf::Rgba8Sint,
DXGI_FORMAT_R9G9B9E5_SHAREDEXP => Tf::Rgb9e5Ufloat,
DXGI_FORMAT_R10G10B10A2_UINT => Tf::Rgb10a2Uint,
DXGI_FORMAT_R10G10B10A2_UNORM => Tf::Rgb10a2Unorm,
DXGI_FORMAT_R11G11B10_FLOAT => Tf::Rg11b10Float,
DXGI_FORMAT_R32G32_UINT => Tf::Rg32Uint,
DXGI_FORMAT_R32G32_SINT => Tf::Rg32Sint,
DXGI_FORMAT_R32G32_FLOAT => Tf::Rg32Float,
DXGI_FORMAT_R16G16B16A16_UINT => Tf::Rgba16Uint,
DXGI_FORMAT_R16G16B16A16_SINT => Tf::Rgba16Sint,
DXGI_FORMAT_R16G16B16A16_UNORM => Tf::Rgba16Unorm,
DXGI_FORMAT_R16G16B16A16_SNORM => Tf::Rgba16Snorm,
DXGI_FORMAT_R16G16B16A16_FLOAT => Tf::Rgba16Float,
DXGI_FORMAT_R32G32B32A32_UINT => Tf::Rgba32Uint,
DXGI_FORMAT_R32G32B32A32_SINT => Tf::Rgba32Sint,
DXGI_FORMAT_R32G32B32A32_FLOAT => Tf::Rgba32Float,
DXGI_FORMAT_D24_UNORM_S8_UINT => Tf::Stencil8,
DXGI_FORMAT_D16_UNORM => Tf::Depth16Unorm,
DXGI_FORMAT_D32_FLOAT => Tf::Depth32Float,
DXGI_FORMAT_D32_FLOAT_S8X24_UINT => Tf::Depth32FloatStencil8,
DXGI_FORMAT_NV12 => Tf::NV12,
DXGI_FORMAT_BC1_UNORM => Tf::Bc1RgbaUnorm,
DXGI_FORMAT_BC1_UNORM_SRGB => Tf::Bc1RgbaUnormSrgb,
DXGI_FORMAT_BC2_UNORM => Tf::Bc2RgbaUnorm,
DXGI_FORMAT_BC2_UNORM_SRGB => Tf::Bc2RgbaUnormSrgb,
DXGI_FORMAT_BC3_UNORM => Tf::Bc3RgbaUnorm,
DXGI_FORMAT_BC3_UNORM_SRGB => Tf::Bc3RgbaUnormSrgb,
DXGI_FORMAT_BC4_UNORM => Tf::Bc4RUnorm,
DXGI_FORMAT_BC4_SNORM => Tf::Bc4RSnorm,
DXGI_FORMAT_BC5_UNORM => Tf::Bc5RgUnorm,
DXGI_FORMAT_BC5_SNORM => Tf::Bc5RgSnorm,
DXGI_FORMAT_BC6H_UF16 => Tf::Bc6hRgbUfloat,
DXGI_FORMAT_BC6H_SF16 => Tf::Bc6hRgbFloat,
DXGI_FORMAT_BC7_UNORM => Tf::Bc7RgbaUnorm,
DXGI_FORMAT_BC7_UNORM_SRGB => Tf::Bc7RgbaUnormSrgb,
_ => return None,
})
}
fn wgpu_to_d3d12(format: wgpu::TextureFormat) -> Option<DXGI_FORMAT> {
// Copied wholesale from:
// https://github.com/gfx-rs/wgpu/blob/v0.19/wgpu-hal/src/auxil/dxgi/conv.rs#L12-L94
// license: MIT OR Apache-2.0
use wgpu::TextureFormat as Tf;
use winapi::shared::dxgiformat::*;
Some(match format {
Tf::R8Unorm => DXGI_FORMAT_R8_UNORM,
Tf::R8Snorm => DXGI_FORMAT_R8_SNORM,
Tf::R8Uint => DXGI_FORMAT_R8_UINT,
Tf::R8Sint => DXGI_FORMAT_R8_SINT,
Tf::R16Uint => DXGI_FORMAT_R16_UINT,
Tf::R16Sint => DXGI_FORMAT_R16_SINT,
Tf::R16Unorm => DXGI_FORMAT_R16_UNORM,
Tf::R16Snorm => DXGI_FORMAT_R16_SNORM,
Tf::R16Float => DXGI_FORMAT_R16_FLOAT,
Tf::Rg8Unorm => DXGI_FORMAT_R8G8_UNORM,
Tf::Rg8Snorm => DXGI_FORMAT_R8G8_SNORM,
Tf::Rg8Uint => DXGI_FORMAT_R8G8_UINT,
Tf::Rg8Sint => DXGI_FORMAT_R8G8_SINT,
Tf::Rg16Unorm => DXGI_FORMAT_R16G16_UNORM,
Tf::Rg16Snorm => DXGI_FORMAT_R16G16_SNORM,
Tf::R32Uint => DXGI_FORMAT_R32_UINT,
Tf::R32Sint => DXGI_FORMAT_R32_SINT,
Tf::R32Float => DXGI_FORMAT_R32_FLOAT,
Tf::Rg16Uint => DXGI_FORMAT_R16G16_UINT,
Tf::Rg16Sint => DXGI_FORMAT_R16G16_SINT,
Tf::Rg16Float => DXGI_FORMAT_R16G16_FLOAT,
Tf::Rgba8Unorm => DXGI_FORMAT_R8G8B8A8_UNORM,
Tf::Rgba8UnormSrgb => DXGI_FORMAT_R8G8B8A8_UNORM_SRGB,
Tf::Bgra8UnormSrgb => DXGI_FORMAT_B8G8R8A8_UNORM_SRGB,
Tf::Rgba8Snorm => DXGI_FORMAT_R8G8B8A8_SNORM,
Tf::Bgra8Unorm => DXGI_FORMAT_B8G8R8A8_UNORM,
Tf::Rgba8Uint => DXGI_FORMAT_R8G8B8A8_UINT,
Tf::Rgba8Sint => DXGI_FORMAT_R8G8B8A8_SINT,
Tf::Rgb9e5Ufloat => DXGI_FORMAT_R9G9B9E5_SHAREDEXP,
Tf::Rgb10a2Uint => DXGI_FORMAT_R10G10B10A2_UINT,
Tf::Rgb10a2Unorm => DXGI_FORMAT_R10G10B10A2_UNORM,
Tf::Rg11b10Float => DXGI_FORMAT_R11G11B10_FLOAT,
Tf::Rg32Uint => DXGI_FORMAT_R32G32_UINT,
Tf::Rg32Sint => DXGI_FORMAT_R32G32_SINT,
Tf::Rg32Float => DXGI_FORMAT_R32G32_FLOAT,
Tf::Rgba16Uint => DXGI_FORMAT_R16G16B16A16_UINT,
Tf::Rgba16Sint => DXGI_FORMAT_R16G16B16A16_SINT,
Tf::Rgba16Unorm => DXGI_FORMAT_R16G16B16A16_UNORM,
Tf::Rgba16Snorm => DXGI_FORMAT_R16G16B16A16_SNORM,
Tf::Rgba16Float => DXGI_FORMAT_R16G16B16A16_FLOAT,
Tf::Rgba32Uint => DXGI_FORMAT_R32G32B32A32_UINT,
Tf::Rgba32Sint => DXGI_FORMAT_R32G32B32A32_SINT,
Tf::Rgba32Float => DXGI_FORMAT_R32G32B32A32_FLOAT,
Tf::Stencil8 => DXGI_FORMAT_D24_UNORM_S8_UINT,
Tf::Depth16Unorm => DXGI_FORMAT_D16_UNORM,
Tf::Depth24Plus => DXGI_FORMAT_D24_UNORM_S8_UINT,
Tf::Depth24PlusStencil8 => DXGI_FORMAT_D24_UNORM_S8_UINT,
Tf::Depth32Float => DXGI_FORMAT_D32_FLOAT,
Tf::Depth32FloatStencil8 => DXGI_FORMAT_D32_FLOAT_S8X24_UINT,
Tf::NV12 => DXGI_FORMAT_NV12,
Tf::Bc1RgbaUnorm => DXGI_FORMAT_BC1_UNORM,
Tf::Bc1RgbaUnormSrgb => DXGI_FORMAT_BC1_UNORM_SRGB,
Tf::Bc2RgbaUnorm => DXGI_FORMAT_BC2_UNORM,
Tf::Bc2RgbaUnormSrgb => DXGI_FORMAT_BC2_UNORM_SRGB,
Tf::Bc3RgbaUnorm => DXGI_FORMAT_BC3_UNORM,
Tf::Bc3RgbaUnormSrgb => DXGI_FORMAT_BC3_UNORM_SRGB,
Tf::Bc4RUnorm => DXGI_FORMAT_BC4_UNORM,
Tf::Bc4RSnorm => DXGI_FORMAT_BC4_SNORM,
Tf::Bc5RgUnorm => DXGI_FORMAT_BC5_UNORM,
Tf::Bc5RgSnorm => DXGI_FORMAT_BC5_SNORM,
Tf::Bc6hRgbUfloat => DXGI_FORMAT_BC6H_UF16,
Tf::Bc6hRgbFloat => DXGI_FORMAT_BC6H_SF16,
Tf::Bc7RgbaUnorm => DXGI_FORMAT_BC7_UNORM,
Tf::Bc7RgbaUnormSrgb => DXGI_FORMAT_BC7_UNORM_SRGB,
Tf::Etc2Rgb8Unorm
| Tf::Etc2Rgb8UnormSrgb
| Tf::Etc2Rgb8A1Unorm
| Tf::Etc2Rgb8A1UnormSrgb
| Tf::Etc2Rgba8Unorm
| Tf::Etc2Rgba8UnormSrgb
| Tf::EacR11Unorm
| Tf::EacR11Snorm
| Tf::EacRg11Unorm
| Tf::EacRg11Snorm
| Tf::Astc {
block: _,
channel: _,
} => return None,
})
}

View File

@@ -0,0 +1,727 @@
use std::ffi::{c_void, CString};
use ash::vk::Handle;
use bevy::log::error;
use bevy::math::UVec2;
use openxr::{sys, Version};
use wgpu_hal::api::Vulkan;
use wgpu_hal::Api;
use super::{GraphicsExt, GraphicsType, GraphicsWrap};
use crate::error::OxrError;
use crate::session::OxrSessionCreateNextChain;
use crate::types::{AppInfo, OxrExtensions, Result, WgpuGraphics};
#[cfg(not(target_os = "android"))]
const VK_TARGET_VERSION: Version = Version::new(1, 2, 0);
#[cfg(target_os = "android")]
const VK_TARGET_VERSION: Version = Version::new(1, 1, 0);
const VK_TARGET_VERSION_ASH: u32 = ash::vk::make_api_version(
0,
VK_TARGET_VERSION.major() as u32,
VK_TARGET_VERSION.minor() as u32,
VK_TARGET_VERSION.patch() as u32,
);
unsafe impl GraphicsExt for openxr::Vulkan {
fn wrap<T: GraphicsType>(item: T::Inner<Self>) -> GraphicsWrap<T> {
GraphicsWrap::Vulkan(item)
}
fn required_exts() -> OxrExtensions {
let mut extensions = openxr::ExtensionSet::default();
extensions.khr_vulkan_enable2 = true;
extensions.into()
}
fn from_wgpu_format(format: wgpu::TextureFormat) -> Option<Self::Format> {
wgpu_to_vulkan(format).map(|f| f.as_raw() as _)
}
fn into_wgpu_format(format: Self::Format) -> Option<wgpu::TextureFormat> {
vulkan_to_wgpu(ash::vk::Format::from_raw(format as _))
}
unsafe fn to_wgpu_img(
color_image: Self::SwapchainImage,
device: &wgpu::Device,
format: wgpu::TextureFormat,
resolution: UVec2,
) -> Result<wgpu::Texture> {
let color_image = ash::vk::Image::from_raw(color_image);
let wgpu_hal_texture = unsafe {
<wgpu_hal::vulkan::Api as wgpu_hal::Api>::Device::texture_from_raw(
color_image,
&wgpu_hal::TextureDescriptor {
label: Some("VR Swapchain"),
size: wgpu::Extent3d {
width: resolution.x,
height: resolution.y,
depth_or_array_layers: 2,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: format,
usage: wgpu_hal::TextureUses::COLOR_TARGET | wgpu_hal::TextureUses::COPY_DST,
memory_flags: wgpu_hal::MemoryFlags::empty(),
view_formats: vec![],
},
None,
)
};
let texture = unsafe {
device.create_texture_from_hal::<wgpu_hal::vulkan::Api>(
wgpu_hal_texture,
&wgpu::TextureDescriptor {
label: Some("VR Swapchain"),
size: wgpu::Extent3d {
width: resolution.x,
height: resolution.y,
depth_or_array_layers: 2,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: format,
usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_DST,
view_formats: &[],
},
)
};
Ok(texture)
}
fn init_graphics(
app_info: &AppInfo,
instance: &openxr::Instance,
system_id: openxr::SystemId,
) -> Result<(WgpuGraphics, Self::SessionCreateInfo)> {
let reqs = instance.graphics_requirements::<openxr::Vulkan>(system_id)?;
if VK_TARGET_VERSION < reqs.min_api_version_supported
|| VK_TARGET_VERSION.major() > reqs.max_api_version_supported.major()
{
error!(
"OpenXR runtime requires Vulkan version > {}, < {}.0.0",
reqs.min_api_version_supported,
reqs.max_api_version_supported.major() + 1
);
return Err(OxrError::FailedGraphicsRequirements);
};
let vk_entry = unsafe { ash::Entry::load() }?;
let flags = wgpu::InstanceFlags::empty();
let extensions =
<Vulkan as Api>::Instance::desired_extensions(&vk_entry, VK_TARGET_VERSION_ASH, flags)?;
let device_extensions = vec![
ash::extensions::khr::Swapchain::name(),
ash::extensions::khr::DrawIndirectCount::name(),
#[cfg(target_os = "android")]
ash::extensions::khr::TimelineSemaphore::name(),
ash::vk::KhrImagelessFramebufferFn::name(),
ash::vk::KhrImageFormatListFn::name(),
];
let vk_instance = unsafe {
let extensions_cchar: Vec<_> = extensions.iter().map(|s| s.as_ptr()).collect();
let app_name = CString::new(app_info.name.clone().into_owned())?;
let vk_app_info = ash::vk::ApplicationInfo::builder()
.application_name(&app_name)
.application_version(1)
.engine_name(&app_name)
.engine_version(1)
.api_version(VK_TARGET_VERSION_ASH);
let vk_instance = instance
.create_vulkan_instance(
system_id,
std::mem::transmute(vk_entry.static_fn().get_instance_proc_addr),
&ash::vk::InstanceCreateInfo::builder()
.application_info(&vk_app_info)
.enabled_extension_names(&extensions_cchar) as *const _
as *const _,
)?
.map_err(ash::vk::Result::from_raw)?;
ash::Instance::load(
vk_entry.static_fn(),
ash::vk::Instance::from_raw(vk_instance as _),
)
};
let vk_instance_ptr = vk_instance.handle().as_raw() as *const c_void;
let vk_physical_device = ash::vk::PhysicalDevice::from_raw(unsafe {
instance.vulkan_graphics_device(system_id, vk_instance.handle().as_raw() as _)? as _
});
let vk_physical_device_ptr = vk_physical_device.as_raw() as *const c_void;
let vk_device_properties =
unsafe { vk_instance.get_physical_device_properties(vk_physical_device) };
if vk_device_properties.api_version < VK_TARGET_VERSION_ASH {
unsafe { vk_instance.destroy_instance(None) }
error!(
"Vulkan physical device doesn't support version {}.{}.{}",
VK_TARGET_VERSION.major(),
VK_TARGET_VERSION.minor(),
VK_TARGET_VERSION.patch()
);
return Err(OxrError::FailedGraphicsRequirements);
}
let wgpu_vk_instance = unsafe {
<Vulkan as Api>::Instance::from_raw(
vk_entry.clone(),
vk_instance.clone(),
VK_TARGET_VERSION_ASH,
0,
None,
extensions,
flags,
false,
Some(Box::new(())),
)?
};
let wgpu_features = wgpu::Features::TEXTURE_ADAPTER_SPECIFIC_FORMAT_FEATURES
| wgpu::Features::MULTIVIEW
| wgpu::Features::MULTI_DRAW_INDIRECT_COUNT
| wgpu::Features::MULTI_DRAW_INDIRECT;
let Some(wgpu_exposed_adapter) = wgpu_vk_instance.expose_adapter(vk_physical_device) else {
error!("WGPU failed to provide an adapter");
return Err(OxrError::FailedGraphicsRequirements);
};
let enabled_extensions = wgpu_exposed_adapter
.adapter
.required_device_extensions(wgpu_features);
let (wgpu_open_device, vk_device_ptr, queue_family_index) = {
let extensions_cchar: Vec<_> = device_extensions.iter().map(|s| s.as_ptr()).collect();
let mut enabled_phd_features = wgpu_exposed_adapter
.adapter
.physical_device_features(&enabled_extensions, wgpu_features);
let family_index = 0;
let family_info = ash::vk::DeviceQueueCreateInfo::builder()
.queue_family_index(family_index)
.queue_priorities(&[1.0])
.build();
let family_infos = [family_info];
let info = enabled_phd_features
.add_to_device_create_builder(
ash::vk::DeviceCreateInfo::builder()
.queue_create_infos(&family_infos)
.push_next(&mut ash::vk::PhysicalDeviceMultiviewFeatures {
multiview: ash::vk::TRUE,
..Default::default()
}),
)
.enabled_extension_names(&extensions_cchar)
.build();
let vk_device = unsafe {
let vk_device = instance
.create_vulkan_device(
system_id,
std::mem::transmute(vk_entry.static_fn().get_instance_proc_addr),
vk_physical_device.as_raw() as _,
&info as *const _ as *const _,
)?
.map_err(ash::vk::Result::from_raw)?;
ash::Device::load(
vk_instance.fp_v1_0(),
ash::vk::Device::from_raw(vk_device as _),
)
};
let vk_device_ptr = vk_device.handle().as_raw() as *const c_void;
let wgpu_open_device = unsafe {
wgpu_exposed_adapter.adapter.device_from_raw(
vk_device,
true,
&enabled_extensions,
wgpu_features,
family_info.queue_family_index,
0,
)
}?;
(
wgpu_open_device,
vk_device_ptr,
family_info.queue_family_index,
)
};
let wgpu_instance =
unsafe { wgpu::Instance::from_hal::<wgpu_hal::api::Vulkan>(wgpu_vk_instance) };
let wgpu_adapter = unsafe { wgpu_instance.create_adapter_from_hal(wgpu_exposed_adapter) };
let (wgpu_device, wgpu_queue) = unsafe {
wgpu_adapter.create_device_from_hal(
wgpu_open_device,
&wgpu::DeviceDescriptor {
label: None,
required_features: wgpu_features,
required_limits: wgpu::Limits {
max_bind_groups: 8,
max_storage_buffer_binding_size: wgpu_adapter
.limits()
.max_storage_buffer_binding_size,
max_push_constant_size: 4,
..Default::default()
},
},
None,
)
}?;
Ok((
WgpuGraphics(
wgpu_device,
wgpu_queue,
wgpu_adapter.get_info(),
wgpu_adapter,
wgpu_instance,
),
openxr::vulkan::SessionCreateInfo {
instance: vk_instance_ptr,
physical_device: vk_physical_device_ptr,
device: vk_device_ptr,
queue_family_index,
queue_index: 0,
},
))
}
unsafe fn create_session(
instance: &openxr::Instance,
system_id: openxr::SystemId,
info: &Self::SessionCreateInfo,
session_create_info_chain: &mut OxrSessionCreateNextChain,
) -> openxr::Result<(
openxr::Session<Self>,
openxr::FrameWaiter,
openxr::FrameStream<Self>,
)> {
let next_ptr = session_create_info_chain.chain_pointer();
let binding = sys::GraphicsBindingVulkanKHR {
ty: sys::GraphicsBindingVulkanKHR::TYPE,
next: next_ptr,
instance: info.instance,
physical_device: info.physical_device,
device: info.device,
queue_family_index: info.queue_family_index,
queue_index: info.queue_index,
};
let info = sys::SessionCreateInfo {
ty: sys::SessionCreateInfo::TYPE,
next: &binding as *const _ as *const _,
create_flags: Default::default(),
system_id: system_id,
};
let mut out = sys::Session::NULL;
cvt((instance.fp().create_session)(
instance.as_raw(),
&info,
&mut out,
))?;
Ok(openxr::Session::from_raw(
instance.clone(),
out,
Box::new(()),
))
}
}
fn cvt(x: sys::Result) -> openxr::Result<sys::Result> {
if x.into_raw() >= 0 {
Ok(x)
} else {
Err(x)
}
}
fn vulkan_to_wgpu(format: ash::vk::Format) -> Option<wgpu::TextureFormat> {
use ash::vk::Format as F;
use wgpu::TextureFormat as Tf;
use wgpu::{AstcBlock, AstcChannel};
Some(match format {
F::R8_UNORM => Tf::R8Unorm,
F::R8_SNORM => Tf::R8Snorm,
F::R8_UINT => Tf::R8Uint,
F::R8_SINT => Tf::R8Sint,
F::R16_UINT => Tf::R16Uint,
F::R16_SINT => Tf::R16Sint,
F::R16_UNORM => Tf::R16Unorm,
F::R16_SNORM => Tf::R16Snorm,
F::R16_SFLOAT => Tf::R16Float,
F::R8G8_UNORM => Tf::Rg8Unorm,
F::R8G8_SNORM => Tf::Rg8Snorm,
F::R8G8_UINT => Tf::Rg8Uint,
F::R8G8_SINT => Tf::Rg8Sint,
F::R16G16_UNORM => Tf::Rg16Unorm,
F::R16G16_SNORM => Tf::Rg16Snorm,
F::R32_UINT => Tf::R32Uint,
F::R32_SINT => Tf::R32Sint,
F::R32_SFLOAT => Tf::R32Float,
F::R16G16_UINT => Tf::Rg16Uint,
F::R16G16_SINT => Tf::Rg16Sint,
F::R16G16_SFLOAT => Tf::Rg16Float,
F::R8G8B8A8_UNORM => Tf::Rgba8Unorm,
F::R8G8B8A8_SRGB => Tf::Rgba8UnormSrgb,
F::B8G8R8A8_SRGB => Tf::Bgra8UnormSrgb,
F::R8G8B8A8_SNORM => Tf::Rgba8Snorm,
F::B8G8R8A8_UNORM => Tf::Bgra8Unorm,
F::R8G8B8A8_UINT => Tf::Rgba8Uint,
F::R8G8B8A8_SINT => Tf::Rgba8Sint,
F::A2B10G10R10_UINT_PACK32 => Tf::Rgb10a2Uint,
F::A2B10G10R10_UNORM_PACK32 => Tf::Rgb10a2Unorm,
F::B10G11R11_UFLOAT_PACK32 => Tf::Rg11b10Float,
F::R32G32_UINT => Tf::Rg32Uint,
F::R32G32_SINT => Tf::Rg32Sint,
F::R32G32_SFLOAT => Tf::Rg32Float,
F::R16G16B16A16_UINT => Tf::Rgba16Uint,
F::R16G16B16A16_SINT => Tf::Rgba16Sint,
F::R16G16B16A16_UNORM => Tf::Rgba16Unorm,
F::R16G16B16A16_SNORM => Tf::Rgba16Snorm,
F::R16G16B16A16_SFLOAT => Tf::Rgba16Float,
F::R32G32B32A32_UINT => Tf::Rgba32Uint,
F::R32G32B32A32_SINT => Tf::Rgba32Sint,
F::R32G32B32A32_SFLOAT => Tf::Rgba32Float,
F::D32_SFLOAT => Tf::Depth32Float,
F::D32_SFLOAT_S8_UINT => Tf::Depth32FloatStencil8,
F::D16_UNORM => Tf::Depth16Unorm,
F::G8_B8R8_2PLANE_420_UNORM => Tf::NV12,
F::E5B9G9R9_UFLOAT_PACK32 => Tf::Rgb9e5Ufloat,
F::BC1_RGBA_UNORM_BLOCK => Tf::Bc1RgbaUnorm,
F::BC1_RGBA_SRGB_BLOCK => Tf::Bc1RgbaUnormSrgb,
F::BC2_UNORM_BLOCK => Tf::Bc2RgbaUnorm,
F::BC2_SRGB_BLOCK => Tf::Bc2RgbaUnormSrgb,
F::BC3_UNORM_BLOCK => Tf::Bc3RgbaUnorm,
F::BC3_SRGB_BLOCK => Tf::Bc3RgbaUnormSrgb,
F::BC4_UNORM_BLOCK => Tf::Bc4RUnorm,
F::BC4_SNORM_BLOCK => Tf::Bc4RSnorm,
F::BC5_UNORM_BLOCK => Tf::Bc5RgUnorm,
F::BC5_SNORM_BLOCK => Tf::Bc5RgSnorm,
F::BC6H_UFLOAT_BLOCK => Tf::Bc6hRgbUfloat,
F::BC6H_SFLOAT_BLOCK => Tf::Bc6hRgbFloat,
F::BC7_UNORM_BLOCK => Tf::Bc7RgbaUnorm,
F::BC7_SRGB_BLOCK => Tf::Bc7RgbaUnormSrgb,
F::ETC2_R8G8B8_UNORM_BLOCK => Tf::Etc2Rgb8Unorm,
F::ETC2_R8G8B8_SRGB_BLOCK => Tf::Etc2Rgb8UnormSrgb,
F::ETC2_R8G8B8A1_UNORM_BLOCK => Tf::Etc2Rgb8A1Unorm,
F::ETC2_R8G8B8A1_SRGB_BLOCK => Tf::Etc2Rgb8A1UnormSrgb,
F::ETC2_R8G8B8A8_UNORM_BLOCK => Tf::Etc2Rgba8Unorm,
F::ETC2_R8G8B8A8_SRGB_BLOCK => Tf::Etc2Rgba8UnormSrgb,
F::EAC_R11_UNORM_BLOCK => Tf::EacR11Unorm,
F::EAC_R11_SNORM_BLOCK => Tf::EacR11Snorm,
F::EAC_R11G11_UNORM_BLOCK => Tf::EacRg11Unorm,
F::EAC_R11G11_SNORM_BLOCK => Tf::EacRg11Snorm,
F::ASTC_4X4_UNORM_BLOCK => Tf::Astc {
block: AstcBlock::B4x4,
channel: AstcChannel::Unorm,
},
F::ASTC_5X4_UNORM_BLOCK => Tf::Astc {
block: AstcBlock::B5x4,
channel: AstcChannel::Unorm,
},
F::ASTC_5X5_UNORM_BLOCK => Tf::Astc {
block: AstcBlock::B5x5,
channel: AstcChannel::Unorm,
},
F::ASTC_6X5_UNORM_BLOCK => Tf::Astc {
block: AstcBlock::B6x5,
channel: AstcChannel::Unorm,
},
F::ASTC_6X6_UNORM_BLOCK => Tf::Astc {
block: AstcBlock::B6x6,
channel: AstcChannel::Unorm,
},
F::ASTC_8X5_UNORM_BLOCK => Tf::Astc {
block: AstcBlock::B8x5,
channel: AstcChannel::Unorm,
},
F::ASTC_8X6_UNORM_BLOCK => Tf::Astc {
block: AstcBlock::B8x6,
channel: AstcChannel::Unorm,
},
F::ASTC_8X8_UNORM_BLOCK => Tf::Astc {
block: AstcBlock::B8x8,
channel: AstcChannel::Unorm,
},
F::ASTC_10X5_UNORM_BLOCK => Tf::Astc {
block: AstcBlock::B10x5,
channel: AstcChannel::Unorm,
},
F::ASTC_10X6_UNORM_BLOCK => Tf::Astc {
block: AstcBlock::B10x6,
channel: AstcChannel::Unorm,
},
F::ASTC_10X8_UNORM_BLOCK => Tf::Astc {
block: AstcBlock::B10x8,
channel: AstcChannel::Unorm,
},
F::ASTC_10X10_UNORM_BLOCK => Tf::Astc {
block: AstcBlock::B10x10,
channel: AstcChannel::Unorm,
},
F::ASTC_12X10_UNORM_BLOCK => Tf::Astc {
block: AstcBlock::B12x10,
channel: AstcChannel::Unorm,
},
F::ASTC_12X12_UNORM_BLOCK => Tf::Astc {
block: AstcBlock::B12x12,
channel: AstcChannel::Unorm,
},
F::ASTC_4X4_SRGB_BLOCK => Tf::Astc {
block: AstcBlock::B4x4,
channel: AstcChannel::UnormSrgb,
},
F::ASTC_5X4_SRGB_BLOCK => Tf::Astc {
block: AstcBlock::B5x4,
channel: AstcChannel::UnormSrgb,
},
F::ASTC_5X5_SRGB_BLOCK => Tf::Astc {
block: AstcBlock::B5x5,
channel: AstcChannel::UnormSrgb,
},
F::ASTC_6X5_SRGB_BLOCK => Tf::Astc {
block: AstcBlock::B6x5,
channel: AstcChannel::UnormSrgb,
},
F::ASTC_6X6_SRGB_BLOCK => Tf::Astc {
block: AstcBlock::B6x6,
channel: AstcChannel::UnormSrgb,
},
F::ASTC_8X5_SRGB_BLOCK => Tf::Astc {
block: AstcBlock::B8x5,
channel: AstcChannel::UnormSrgb,
},
F::ASTC_8X6_SRGB_BLOCK => Tf::Astc {
block: AstcBlock::B8x6,
channel: AstcChannel::UnormSrgb,
},
F::ASTC_8X8_SRGB_BLOCK => Tf::Astc {
block: AstcBlock::B8x8,
channel: AstcChannel::UnormSrgb,
},
F::ASTC_10X5_SRGB_BLOCK => Tf::Astc {
block: AstcBlock::B10x5,
channel: AstcChannel::UnormSrgb,
},
F::ASTC_10X6_SRGB_BLOCK => Tf::Astc {
block: AstcBlock::B10x6,
channel: AstcChannel::UnormSrgb,
},
F::ASTC_10X8_SRGB_BLOCK => Tf::Astc {
block: AstcBlock::B10x8,
channel: AstcChannel::UnormSrgb,
},
F::ASTC_10X10_SRGB_BLOCK => Tf::Astc {
block: AstcBlock::B10x10,
channel: AstcChannel::UnormSrgb,
},
F::ASTC_12X10_SRGB_BLOCK => Tf::Astc {
block: AstcBlock::B12x10,
channel: AstcChannel::UnormSrgb,
},
F::ASTC_12X12_SRGB_BLOCK => Tf::Astc {
block: AstcBlock::B12x12,
channel: AstcChannel::UnormSrgb,
},
F::ASTC_4X4_SFLOAT_BLOCK_EXT => Tf::Astc {
block: AstcBlock::B4x4,
channel: AstcChannel::Hdr,
},
F::ASTC_5X4_SFLOAT_BLOCK_EXT => Tf::Astc {
block: AstcBlock::B5x4,
channel: AstcChannel::Hdr,
},
F::ASTC_5X5_SFLOAT_BLOCK_EXT => Tf::Astc {
block: AstcBlock::B5x5,
channel: AstcChannel::Hdr,
},
F::ASTC_6X5_SFLOAT_BLOCK_EXT => Tf::Astc {
block: AstcBlock::B6x5,
channel: AstcChannel::Hdr,
},
F::ASTC_6X6_SFLOAT_BLOCK_EXT => Tf::Astc {
block: AstcBlock::B6x6,
channel: AstcChannel::Hdr,
},
F::ASTC_8X5_SFLOAT_BLOCK_EXT => Tf::Astc {
block: AstcBlock::B8x5,
channel: AstcChannel::Hdr,
},
F::ASTC_8X6_SFLOAT_BLOCK_EXT => Tf::Astc {
block: AstcBlock::B8x6,
channel: AstcChannel::Hdr,
},
F::ASTC_8X8_SFLOAT_BLOCK_EXT => Tf::Astc {
block: AstcBlock::B8x8,
channel: AstcChannel::Hdr,
},
F::ASTC_10X5_SFLOAT_BLOCK_EXT => Tf::Astc {
block: AstcBlock::B10x5,
channel: AstcChannel::Hdr,
},
F::ASTC_10X6_SFLOAT_BLOCK_EXT => Tf::Astc {
block: AstcBlock::B10x6,
channel: AstcChannel::Hdr,
},
F::ASTC_10X8_SFLOAT_BLOCK_EXT => Tf::Astc {
block: AstcBlock::B10x8,
channel: AstcChannel::Hdr,
},
F::ASTC_10X10_SFLOAT_BLOCK_EXT => Tf::Astc {
block: AstcBlock::B10x10,
channel: AstcChannel::Hdr,
},
F::ASTC_12X10_SFLOAT_BLOCK_EXT => Tf::Astc {
block: AstcBlock::B12x10,
channel: AstcChannel::Hdr,
},
F::ASTC_12X12_SFLOAT_BLOCK_EXT => Tf::Astc {
block: AstcBlock::B12x12,
channel: AstcChannel::Hdr,
},
_ => return None,
})
}
fn wgpu_to_vulkan(format: wgpu::TextureFormat) -> Option<ash::vk::Format> {
// Copied with minor modification from:
// https://github.com/gfx-rs/wgpu/blob/a7defb723f856d946d6d220e9897d20dbb7b8f61/wgpu-hal/src/vulkan/conv.rs#L5-L151
// license: MIT OR Apache-2.0
use ash::vk::Format as F;
use wgpu::TextureFormat as Tf;
use wgpu::{AstcBlock, AstcChannel};
Some(match format {
Tf::R8Unorm => F::R8_UNORM,
Tf::R8Snorm => F::R8_SNORM,
Tf::R8Uint => F::R8_UINT,
Tf::R8Sint => F::R8_SINT,
Tf::R16Uint => F::R16_UINT,
Tf::R16Sint => F::R16_SINT,
Tf::R16Unorm => F::R16_UNORM,
Tf::R16Snorm => F::R16_SNORM,
Tf::R16Float => F::R16_SFLOAT,
Tf::Rg8Unorm => F::R8G8_UNORM,
Tf::Rg8Snorm => F::R8G8_SNORM,
Tf::Rg8Uint => F::R8G8_UINT,
Tf::Rg8Sint => F::R8G8_SINT,
Tf::Rg16Unorm => F::R16G16_UNORM,
Tf::Rg16Snorm => F::R16G16_SNORM,
Tf::R32Uint => F::R32_UINT,
Tf::R32Sint => F::R32_SINT,
Tf::R32Float => F::R32_SFLOAT,
Tf::Rg16Uint => F::R16G16_UINT,
Tf::Rg16Sint => F::R16G16_SINT,
Tf::Rg16Float => F::R16G16_SFLOAT,
Tf::Rgba8Unorm => F::R8G8B8A8_UNORM,
Tf::Rgba8UnormSrgb => F::R8G8B8A8_SRGB,
Tf::Bgra8UnormSrgb => F::B8G8R8A8_SRGB,
Tf::Rgba8Snorm => F::R8G8B8A8_SNORM,
Tf::Bgra8Unorm => F::B8G8R8A8_UNORM,
Tf::Rgba8Uint => F::R8G8B8A8_UINT,
Tf::Rgba8Sint => F::R8G8B8A8_SINT,
Tf::Rgb10a2Uint => F::A2B10G10R10_UINT_PACK32,
Tf::Rgb10a2Unorm => F::A2B10G10R10_UNORM_PACK32,
Tf::Rg11b10Float => F::B10G11R11_UFLOAT_PACK32,
Tf::Rg32Uint => F::R32G32_UINT,
Tf::Rg32Sint => F::R32G32_SINT,
Tf::Rg32Float => F::R32G32_SFLOAT,
Tf::Rgba16Uint => F::R16G16B16A16_UINT,
Tf::Rgba16Sint => F::R16G16B16A16_SINT,
Tf::Rgba16Unorm => F::R16G16B16A16_UNORM,
Tf::Rgba16Snorm => F::R16G16B16A16_SNORM,
Tf::Rgba16Float => F::R16G16B16A16_SFLOAT,
Tf::Rgba32Uint => F::R32G32B32A32_UINT,
Tf::Rgba32Sint => F::R32G32B32A32_SINT,
Tf::Rgba32Float => F::R32G32B32A32_SFLOAT,
Tf::Depth32Float => F::D32_SFLOAT,
Tf::Depth32FloatStencil8 => F::D32_SFLOAT_S8_UINT,
Tf::Depth24Plus | Tf::Depth24PlusStencil8 | Tf::Stencil8 => return None, // Dependent on device properties
Tf::Depth16Unorm => F::D16_UNORM,
Tf::NV12 => F::G8_B8R8_2PLANE_420_UNORM,
Tf::Rgb9e5Ufloat => F::E5B9G9R9_UFLOAT_PACK32,
Tf::Bc1RgbaUnorm => F::BC1_RGBA_UNORM_BLOCK,
Tf::Bc1RgbaUnormSrgb => F::BC1_RGBA_SRGB_BLOCK,
Tf::Bc2RgbaUnorm => F::BC2_UNORM_BLOCK,
Tf::Bc2RgbaUnormSrgb => F::BC2_SRGB_BLOCK,
Tf::Bc3RgbaUnorm => F::BC3_UNORM_BLOCK,
Tf::Bc3RgbaUnormSrgb => F::BC3_SRGB_BLOCK,
Tf::Bc4RUnorm => F::BC4_UNORM_BLOCK,
Tf::Bc4RSnorm => F::BC4_SNORM_BLOCK,
Tf::Bc5RgUnorm => F::BC5_UNORM_BLOCK,
Tf::Bc5RgSnorm => F::BC5_SNORM_BLOCK,
Tf::Bc6hRgbUfloat => F::BC6H_UFLOAT_BLOCK,
Tf::Bc6hRgbFloat => F::BC6H_SFLOAT_BLOCK,
Tf::Bc7RgbaUnorm => F::BC7_UNORM_BLOCK,
Tf::Bc7RgbaUnormSrgb => F::BC7_SRGB_BLOCK,
Tf::Etc2Rgb8Unorm => F::ETC2_R8G8B8_UNORM_BLOCK,
Tf::Etc2Rgb8UnormSrgb => F::ETC2_R8G8B8_SRGB_BLOCK,
Tf::Etc2Rgb8A1Unorm => F::ETC2_R8G8B8A1_UNORM_BLOCK,
Tf::Etc2Rgb8A1UnormSrgb => F::ETC2_R8G8B8A1_SRGB_BLOCK,
Tf::Etc2Rgba8Unorm => F::ETC2_R8G8B8A8_UNORM_BLOCK,
Tf::Etc2Rgba8UnormSrgb => F::ETC2_R8G8B8A8_SRGB_BLOCK,
Tf::EacR11Unorm => F::EAC_R11_UNORM_BLOCK,
Tf::EacR11Snorm => F::EAC_R11_SNORM_BLOCK,
Tf::EacRg11Unorm => F::EAC_R11G11_UNORM_BLOCK,
Tf::EacRg11Snorm => F::EAC_R11G11_SNORM_BLOCK,
Tf::Astc { block, channel } => match channel {
AstcChannel::Unorm => match block {
AstcBlock::B4x4 => F::ASTC_4X4_UNORM_BLOCK,
AstcBlock::B5x4 => F::ASTC_5X4_UNORM_BLOCK,
AstcBlock::B5x5 => F::ASTC_5X5_UNORM_BLOCK,
AstcBlock::B6x5 => F::ASTC_6X5_UNORM_BLOCK,
AstcBlock::B6x6 => F::ASTC_6X6_UNORM_BLOCK,
AstcBlock::B8x5 => F::ASTC_8X5_UNORM_BLOCK,
AstcBlock::B8x6 => F::ASTC_8X6_UNORM_BLOCK,
AstcBlock::B8x8 => F::ASTC_8X8_UNORM_BLOCK,
AstcBlock::B10x5 => F::ASTC_10X5_UNORM_BLOCK,
AstcBlock::B10x6 => F::ASTC_10X6_UNORM_BLOCK,
AstcBlock::B10x8 => F::ASTC_10X8_UNORM_BLOCK,
AstcBlock::B10x10 => F::ASTC_10X10_UNORM_BLOCK,
AstcBlock::B12x10 => F::ASTC_12X10_UNORM_BLOCK,
AstcBlock::B12x12 => F::ASTC_12X12_UNORM_BLOCK,
},
AstcChannel::UnormSrgb => match block {
AstcBlock::B4x4 => F::ASTC_4X4_SRGB_BLOCK,
AstcBlock::B5x4 => F::ASTC_5X4_SRGB_BLOCK,
AstcBlock::B5x5 => F::ASTC_5X5_SRGB_BLOCK,
AstcBlock::B6x5 => F::ASTC_6X5_SRGB_BLOCK,
AstcBlock::B6x6 => F::ASTC_6X6_SRGB_BLOCK,
AstcBlock::B8x5 => F::ASTC_8X5_SRGB_BLOCK,
AstcBlock::B8x6 => F::ASTC_8X6_SRGB_BLOCK,
AstcBlock::B8x8 => F::ASTC_8X8_SRGB_BLOCK,
AstcBlock::B10x5 => F::ASTC_10X5_SRGB_BLOCK,
AstcBlock::B10x6 => F::ASTC_10X6_SRGB_BLOCK,
AstcBlock::B10x8 => F::ASTC_10X8_SRGB_BLOCK,
AstcBlock::B10x10 => F::ASTC_10X10_SRGB_BLOCK,
AstcBlock::B12x10 => F::ASTC_12X10_SRGB_BLOCK,
AstcBlock::B12x12 => F::ASTC_12X12_SRGB_BLOCK,
},
AstcChannel::Hdr => match block {
AstcBlock::B4x4 => F::ASTC_4X4_SFLOAT_BLOCK_EXT,
AstcBlock::B5x4 => F::ASTC_5X4_SFLOAT_BLOCK_EXT,
AstcBlock::B5x5 => F::ASTC_5X5_SFLOAT_BLOCK_EXT,
AstcBlock::B6x5 => F::ASTC_6X5_SFLOAT_BLOCK_EXT,
AstcBlock::B6x6 => F::ASTC_6X6_SFLOAT_BLOCK_EXT,
AstcBlock::B8x5 => F::ASTC_8X5_SFLOAT_BLOCK_EXT,
AstcBlock::B8x6 => F::ASTC_8X6_SFLOAT_BLOCK_EXT,
AstcBlock::B8x8 => F::ASTC_8X8_SFLOAT_BLOCK_EXT,
AstcBlock::B10x5 => F::ASTC_10X5_SFLOAT_BLOCK_EXT,
AstcBlock::B10x6 => F::ASTC_10X6_SFLOAT_BLOCK_EXT,
AstcBlock::B10x8 => F::ASTC_10X8_SFLOAT_BLOCK_EXT,
AstcBlock::B10x10 => F::ASTC_10X10_SFLOAT_BLOCK_EXT,
AstcBlock::B12x10 => F::ASTC_12X10_SFLOAT_BLOCK_EXT,
AstcBlock::B12x12 => F::ASTC_12X12_SFLOAT_BLOCK_EXT,
},
},
})
}

View File

@@ -0,0 +1,117 @@
use bevy::prelude::*;
use bevy_mod_xr::types::XrPose;
pub trait ToPosef {
fn to_posef(&self) -> openxr::Posef;
}
pub trait ToTransform {
fn to_transform(&self) -> Transform;
}
pub trait ToXrPose {
fn to_xr_pose(&self) -> XrPose;
}
pub trait ToQuaternionf {
fn to_quaternionf(&self) -> openxr::Quaternionf;
}
pub trait ToQuat {
fn to_quat(&self) -> Quat;
}
pub trait ToVector3f {
fn to_vector3f(&self) -> openxr::Vector3f;
}
pub trait ToVec3 {
fn to_vec3(&self) -> Vec3;
}
pub trait ToVector2f {
fn to_vector2f(&self) -> openxr::Vector2f;
}
pub trait ToVec2 {
fn to_vec2(&self) -> Vec2;
}
impl ToPosef for Transform {
fn to_posef(&self) -> openxr::Posef {
openxr::Posef {
orientation: self.rotation.to_quaternionf(),
position: self.translation.to_vector3f(),
}
}
}
impl ToTransform for openxr::Posef {
fn to_transform(&self) -> Transform {
Transform::from_translation(self.position.to_vec3())
.with_rotation(self.orientation.to_quat())
}
}
impl ToXrPose for openxr::Posef {
fn to_xr_pose(&self) -> XrPose {
XrPose {
translation: self.position.to_vec3(),
rotation: self.orientation.to_quat(),
}
}
}
impl ToPosef for XrPose {
fn to_posef(&self) -> openxr::Posef {
openxr::Posef {
orientation: self.rotation.to_quaternionf(),
position: self.translation.to_vector3f(),
}
}
}
impl ToQuaternionf for Quat {
fn to_quaternionf(&self) -> openxr::Quaternionf {
openxr::Quaternionf {
x: self.x,
y: self.y,
z: self.z,
w: self.w,
}
}
}
impl ToQuat for openxr::Quaternionf {
fn to_quat(&self) -> Quat {
let mut quat = Quat::from_xyzw(self.x, self.y, self.z, self.w);
if quat.length() == 0.0 {
quat = Quat::IDENTITY;
}
if !quat.is_normalized() {
quat = quat.normalize();
}
quat
}
}
impl ToVector3f for Vec3 {
fn to_vector3f(&self) -> openxr::Vector3f {
openxr::Vector3f {
x: self.x,
y: self.y,
z: self.z,
}
}
}
impl ToVec3 for openxr::Vector3f {
fn to_vec3(&self) -> Vec3 {
Vec3 {
x: self.x,
y: self.y,
z: self.z,
}
}
}
impl ToVector2f for Vec2 {
fn to_vector2f(&self) -> openxr::Vector2f {
openxr::Vector2f {
x: self.x,
y: self.y,
}
}
}
impl ToVec2 for openxr::Vector2f {
fn to_vec2(&self) -> Vec2 {
Vec2 {
x: self.x,
y: self.y,
}
}
}

View File

@@ -0,0 +1,526 @@
use bevy::prelude::*;
use bevy::render::extract_resource::ExtractResourcePlugin;
use bevy::render::renderer::RenderAdapter;
use bevy::render::renderer::RenderAdapterInfo;
use bevy::render::renderer::RenderDevice;
use bevy::render::renderer::RenderInstance;
use bevy::render::renderer::RenderQueue;
use bevy::render::settings::RenderCreation;
use bevy::render::MainWorld;
use bevy::render::RenderApp;
use bevy::render::RenderPlugin;
use bevy::winit::UpdateMode;
use bevy::winit::WinitSettings;
use bevy_mod_xr::session::*;
use crate::error::OxrError;
use crate::features::overlay::OxrOverlaySessionEvent;
use crate::graphics::*;
use crate::resources::*;
use crate::session::OxrSession;
use crate::session::OxrSessionCreateNextChain;
use crate::types::*;
use super::exts::OxrEnabledExtensions;
pub fn session_started(started: Option<Res<OxrSessionStarted>>) -> bool {
started.is_some_and(|started| started.0)
}
pub fn should_run_frame_loop(
started: Option<Res<OxrSessionStarted>>,
state: Option<Res<XrState>>,
) -> bool {
started.is_some_and(|started| started.0)
&& state.is_some_and(|state| *state != XrState::Stopping)
}
pub fn should_render(frame_state: Option<Res<OxrFrameState>>) -> bool {
frame_state.is_some_and(|frame_state| frame_state.should_render)
}
pub struct OxrInitPlugin {
/// 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: OxrExtensions,
/// 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 Default for OxrInitPlugin {
fn default() -> Self {
Self {
app_info: default(),
exts: {
let mut exts = OxrExtensions::default();
exts.enable_fb_passthrough();
exts.enable_hand_tracking();
exts
},
blend_modes: default(),
backends: default(),
formats: Some(vec![wgpu::TextureFormat::Rgba8UnormSrgb]),
resolutions: default(),
synchronous_pipeline_compilation: default(),
}
}
}
impl Plugin for OxrInitPlugin {
fn build(&self, app: &mut App) {
match self.init_xr() {
Ok((
instance,
system_id,
WgpuGraphics(device, queue, adapter_info, adapter, wgpu_instance),
session_create_info,
enabled_exts,
)) => {
app.insert_resource(enabled_exts)
.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: self.synchronous_pipeline_compilation,
},
ExtractResourcePlugin::<OxrSessionStarted>::default(),
))
.add_systems(
XrFirst,
poll_events
.before(XrHandleEvents)
.run_if(not(state_equals(XrState::Unavailable))),
)
.add_systems(XrCreateSession, create_xr_session)
.add_systems(XrDestroySession, destroy_xr_session)
.add_systems(XrBeginSession, begin_xr_session)
.add_systems(XrEndSession, end_xr_session)
.add_systems(XrRequestExit, request_exit_xr_session)
.insert_resource(instance.clone())
.insert_resource(system_id)
.insert_resource(XrState::Available)
.insert_resource(WinitSettings {
focused_mode: UpdateMode::Continuous,
unfocused_mode: UpdateMode::Continuous,
})
.insert_resource(OxrSessionStarted(false))
.insert_non_send_resource(session_create_info)
.init_non_send_resource::<OxrSessionCreateNextChain>();
app.world
.spawn((TransformBundle::default(), XrTrackingRoot));
app.world
.resource_mut::<Events<XrStateChanged>>()
.send(XrStateChanged(XrState::Available));
let render_app = app.sub_app_mut(RenderApp);
render_app
.add_systems(ExtractSchedule, transfer_xr_resources)
.insert_resource(instance)
.insert_resource(system_id)
.insert_resource(XrState::Available)
.insert_resource(OxrSessionStarted(false));
}
Err(e) => {
error!("Failed to initialize openxr: {e}");
app.add_plugins(RenderPlugin::default())
.insert_resource(XrState::Unavailable);
}
};
}
fn finish(&self, app: &mut App) {
app.sub_app_mut(RenderApp)
.add_systems(XrDestroySession, destroy_xr_session);
}
}
impl OxrInitPlugin {
fn init_xr(
&self,
) -> Result<(
OxrInstance,
OxrSystemId,
WgpuGraphics,
SessionConfigInfo,
OxrEnabledExtensions,
)> {
#[cfg(windows)]
let entry = OxrEntry(openxr::Entry::linked());
#[cfg(not(windows))]
let entry = OxrEntry(unsafe { openxr::Entry::load()? });
#[cfg(target_os = "android")]
entry.initialize_android_loader()?;
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(&self.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) = &self.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(OxrError::NoAvailableBackend)?;
let exts = self.exts.clone() & available_exts;
let instance = entry.create_instance(
self.app_info.clone(),
exts.clone(),
// &["XR_APILAYER_LUNARG_api_dump"],
&[],
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 (graphics, graphics_info) = instance.init_graphics(system_id)?;
let session_create_info = SessionConfigInfo {
blend_modes: self.blend_modes.clone(),
formats: self.formats.clone(),
resolutions: self.resolutions.clone(),
graphics_info,
};
Ok((
instance,
OxrSystemId(system_id),
graphics,
session_create_info,
OxrEnabledExtensions(exts),
))
}
}
/// Polls any OpenXR events and handles them accordingly
pub fn poll_events(
instance: Res<OxrInstance>,
mut status: ResMut<XrState>,
mut changed_event: EventWriter<XrStateChanged>,
mut overlay_writer: Option<ResMut<Events<OxrOverlaySessionEvent>>>,
) {
let _span = info_span!("xr_poll_events");
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(state) => {
use openxr::SessionState;
let state = state.state();
info!("entered XR state {:?}", state);
let new_status = match state {
SessionState::IDLE => XrState::Idle,
SessionState::READY => XrState::Ready,
SessionState::SYNCHRONIZED | SessionState::VISIBLE | SessionState::FOCUSED => {
XrState::Running
}
SessionState::STOPPING => XrState::Stopping,
SessionState::EXITING => XrState::Exiting {
should_restart: false,
},
SessionState::LOSS_PENDING => XrState::Exiting {
should_restart: true,
},
_ => unreachable!(),
};
changed_event.send(XrStateChanged(new_status));
*status = new_status;
}
InstanceLossPending(_) => {}
EventsLost(e) => warn!("lost {} XR events", e.lost_event_count()),
MainSessionVisibilityChangedEXTX(d) => {
if let Some(writer) = overlay_writer.as_mut() {
writer.send(OxrOverlaySessionEvent::MainSessionVisibilityChanged {
visible: d.visible(),
flags: d.flags(),
});
} else {
warn!("Overlay Event Recieved without the OverlayPlugin being added!");
}
}
_ => {}
}
}
}
fn init_xr_session(
device: &wgpu::Device,
instance: &OxrInstance,
system_id: openxr::SystemId,
chain: &mut OxrSessionCreateNextChain,
SessionConfigInfo {
blend_modes,
formats,
resolutions,
graphics_info,
}: SessionConfigInfo,
) -> Result<(
OxrSession,
OxrFrameWaiter,
OxrFrameStream,
OxrSwapchain,
OxrSwapchainImages,
OxrGraphicsInfo,
)> {
let (session, frame_waiter, frame_stream) =
unsafe { instance.create_session(system_id, graphics_info, chain)? };
// 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(OxrError::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) = &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 {
view_configuration_views.first().map(|config| {
(
UVec2::new(
config.recommended_image_rect_width,
config.recommended_image_rect_height,
),
*config,
)
})
}
.ok_or(OxrError::NoAvailableViewConfiguration)?;
let available_formats = session.enumerate_swapchain_formats()?;
let format = if let Some(formats) = &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(OxrError::NoAvailableFormat)?;
let 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 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) = &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(OxrError::NoAvailableBackend)?;
let graphics_info = OxrGraphicsInfo {
blend_mode,
resolution,
format,
};
Ok((
session,
frame_waiter,
frame_stream,
swapchain,
images,
graphics_info,
))
}
pub fn create_xr_session(world: &mut World) {
let mut chain = world
.remove_non_send_resource::<OxrSessionCreateNextChain>()
.unwrap();
let device = world.resource::<RenderDevice>();
let instance = world.resource::<OxrInstance>();
let create_info = world.non_send_resource::<SessionConfigInfo>();
let system_id = world.resource::<OxrSystemId>();
match init_xr_session(
device.wgpu_device(),
&instance,
**system_id,
&mut chain,
create_info.clone(),
) {
Ok((session, frame_waiter, frame_stream, swapchain, images, graphics_info)) => {
world.insert_resource(session.clone());
world.insert_resource(frame_waiter);
world.insert_resource(images.clone());
world.insert_resource(graphics_info.clone());
world.insert_resource(OxrRenderResources {
session,
frame_stream,
swapchain,
images,
graphics_info,
});
}
Err(e) => error!("Failed to initialize XrSession: {e}"),
}
world.insert_non_send_resource(chain);
}
pub fn destroy_xr_session(world: &mut World) {
world.remove_resource::<OxrSession>();
world.remove_resource::<OxrFrameWaiter>();
world.remove_resource::<OxrFrameStream>();
world.remove_resource::<OxrSwapchain>();
world.remove_resource::<OxrSwapchainImages>();
world.remove_resource::<OxrGraphicsInfo>();
world.insert_resource(XrState::Available);
}
pub fn begin_xr_session(session: Res<OxrSession>, mut session_started: ResMut<OxrSessionStarted>) {
let _span = info_span!("xr_begin_session");
session
.begin(openxr::ViewConfigurationType::PRIMARY_STEREO)
.expect("Failed to begin session");
session_started.0 = true;
}
pub fn end_xr_session(session: Res<OxrSession>, mut session_started: ResMut<OxrSessionStarted>) {
let _span = info_span!("xr_end_session");
session.end().expect("Failed to end session");
session_started.0 = false;
}
pub fn request_exit_xr_session(session: Res<OxrSession>) {
session.request_exit().expect("Failed to request exit");
}
/// This is used solely to transport resources from the main world to the render world.
#[derive(Resource)]
struct OxrRenderResources {
session: OxrSession,
frame_stream: OxrFrameStream,
swapchain: OxrSwapchain,
images: OxrSwapchainImages,
graphics_info: OxrGraphicsInfo,
}
/// This system transfers important render resources from the main world to the render world when a session is created.
pub fn transfer_xr_resources(mut commands: Commands, mut world: ResMut<MainWorld>) {
let Some(OxrRenderResources {
session,
frame_stream,
swapchain,
images,
graphics_info,
}) = 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);
}

View File

@@ -0,0 +1,267 @@
use std::mem;
use bevy::ecs::world::World;
use bevy_mod_xr::spaces::{XrPrimaryReferenceSpace, XrSpace};
use openxr::{sys, CompositionLayerFlags, Fovf, Posef, Rect2Di};
use crate::graphics::graphics_match;
use crate::resources::*;
use crate::spaces::OxrSpaceExt as _;
pub trait LayerProvider {
fn get<'a>(&'a self, world: &'a World) -> Option<Box<dyn CompositionLayer + '_>>;
}
pub struct ProjectionLayer;
pub struct PassthroughLayer;
impl LayerProvider for ProjectionLayer {
fn get<'a>(&self, world: &'a World) -> Option<Box<dyn CompositionLayer<'a> + 'a>> {
let stage = world.get_resource::<XrPrimaryReferenceSpace>()?;
let openxr_views = world.get_resource::<OxrViews>()?;
let swapchain = world.get_resource::<OxrSwapchain>()?;
let graphics_info = world.get_resource::<OxrGraphicsInfo>()?;
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 _,
},
};
if openxr_views.len() < 2 {
return None;
}
Some(Box::new(
CompositionLayerProjection::new()
.layer_flags(CompositionLayerFlags::BLEND_TEXTURE_SOURCE_ALPHA)
.space(&stage)
.views(&[
CompositionLayerProjectionView::new()
.pose(openxr_views.0[0].pose)
.fov(openxr_views.0[0].fov)
.sub_image(
SwapchainSubImage::new()
.swapchain(&swapchain)
.image_array_index(0)
.image_rect(rect),
),
CompositionLayerProjectionView::new()
.pose(openxr_views.0[1].pose)
.fov(openxr_views.0[1].fov)
.sub_image(
SwapchainSubImage::new()
.swapchain(&swapchain)
.image_array_index(1)
.image_rect(rect),
),
]),
))
}
}
impl LayerProvider for PassthroughLayer {
fn get<'a>(&'a self, world: &'a World) -> Option<Box<dyn CompositionLayer + '_>> {
Some(Box::new(
CompositionLayerPassthrough::new()
.layer_handle(world.get_resource::<OxrPassthroughLayer>()?)
.layer_flags(CompositionLayerFlags::BLEND_TEXTURE_SOURCE_ALPHA),
))
}
}
#[derive(Copy, Clone)]
pub struct SwapchainSubImage<'a> {
inner: sys::SwapchainSubImage,
swapchain: Option<&'a OxrSwapchain>,
}
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 OxrSwapchain) -> 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 OxrSwapchain>,
}
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 OxrSwapchain>;
fn header(&self) -> &sys::CompositionLayerBaseHeader;
}
#[derive(Clone)]
pub struct CompositionLayerProjection<'a> {
inner: sys::CompositionLayerProjection,
swapchain: Option<&'a OxrSwapchain>,
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: &XrSpace) -> Self {
self.inner.space = value.as_raw_openxr_space();
self
}
#[inline]
pub fn views(mut self, value: &[CompositionLayerProjectionView<'a>]) -> Self {
self.views = value.iter().map(|view| view.inner).collect();
self.inner.views = self.views.as_slice().as_ptr() as *const _ as _;
self.inner.view_count = self.views.len() as u32;
self
}
}
unsafe impl<'a> CompositionLayer<'a> for CompositionLayerProjection<'a> {
fn swapchain(&self) -> Option<&'a OxrSwapchain> {
self.swapchain
}
fn header(&self) -> &sys::CompositionLayerBaseHeader {
unsafe { mem::transmute(&self.inner) }
}
}
impl<'a> Default for CompositionLayerProjection<'a> {
fn default() -> Self {
Self::new()
}
}
pub struct CompositionLayerPassthrough {
inner: sys::CompositionLayerPassthroughFB,
}
impl CompositionLayerPassthrough {
#[inline]
pub fn new() -> Self {
Self {
inner: openxr::sys::CompositionLayerPassthroughFB {
ty: openxr::sys::CompositionLayerPassthroughFB::TYPE,
..unsafe { mem::zeroed() }
},
}
}
#[inline]
pub fn layer_handle(mut self, layer_handle: &OxrPassthroughLayer) -> Self {
self.inner.layer_handle = *layer_handle.inner();
self
}
#[inline]
pub fn layer_flags(mut self, value: CompositionLayerFlags) -> Self {
self.inner.flags = value;
self
}
}
unsafe impl<'a> CompositionLayer<'a> for CompositionLayerPassthrough {
fn swapchain(&self) -> Option<&'a OxrSwapchain> {
None
}
fn header(&self) -> &sys::CompositionLayerBaseHeader {
unsafe { mem::transmute(&self.inner) }
}
}

View File

@@ -0,0 +1,70 @@
// use actions::XrActionPlugin;
use bevy::{
app::{PluginGroup, PluginGroupBuilder},
render::RenderPlugin,
utils::default,
window::{PresentMode, Window, WindowPlugin},
};
use bevy_mod_xr::camera::XrCameraPlugin;
use bevy_mod_xr::session::XrSessionPlugin;
use init::OxrInitPlugin;
use render::OxrRenderPlugin;
use self::{
features::{handtracking::HandTrackingPlugin, passthrough::OxrPassthroughPlugin},
reference_space::OxrReferenceSpacePlugin,
};
pub mod action_binding;
pub mod action_set_attaching;
pub mod action_set_syncing;
pub mod error;
mod exts;
pub mod features;
pub mod graphics;
pub mod helper_traits;
pub mod init;
pub mod layer_builder;
pub mod next_chain;
pub mod reference_space;
pub mod render;
pub mod resources;
pub mod session;
pub mod spaces;
pub mod types;
pub fn add_xr_plugins<G: PluginGroup>(plugins: G) -> PluginGroupBuilder {
plugins
.build()
.disable::<RenderPlugin>()
// .disable::<PipelinedRenderingPlugin>()
.add_before::<RenderPlugin, _>(XrSessionPlugin { auto_handle: true })
.add_before::<RenderPlugin, _>(OxrInitPlugin::default())
.add(OxrReferenceSpacePlugin::default())
.add(OxrRenderPlugin)
.add(OxrPassthroughPlugin)
.add(HandTrackingPlugin::default())
.add(XrCameraPlugin)
.add(action_set_attaching::OxrActionAttachingPlugin)
.add(action_binding::OxrActionBindingPlugin)
.add(action_set_syncing::OxrActionSyncingPlugin)
.add(features::overlay::OxrOverlayPlugin)
.add(spaces::OxrSpatialPlugin)
.add(spaces::OxrSpacePatchingPlugin)
// .add(XrActionPlugin)
// we should probably handle the exiting ourselfs so that we can correctly end the
// session and instance
.set(WindowPlugin {
primary_window: Some(Window {
transparent: true,
present_mode: PresentMode::AutoNoVsync,
// title: self.app_info.name.clone(),
..default()
}),
// #[cfg(target_os = "android")]
// exit_condition: bevy::window::ExitCondition::DontExit,
#[cfg(target_os = "android")]
close_when_requested: true,
..default()
})
}

View File

@@ -0,0 +1,44 @@
use openxr::sys;
use std::{ffi::c_void, ptr};
/// An abstraction for the next pointer fields for openxr calls
#[derive(Default)]
pub struct OxrNextChain {
structs: Vec<Box<dyn OxrNextChainStructProvider>>,
}
impl OxrNextChain {
pub fn push<T: OxrNextChainStructProvider>(&mut self, info_struct: T) {
if let Some(last) = self.structs.last_mut() {
let mut info = Box::new(info_struct);
info.as_mut().clear_next();
last.as_mut().set_next(info.as_ref().header());
self.structs.push(info);
} else {
let mut info_struct = Box::new(info_struct);
info_struct.as_mut().clear_next();
self.structs.push(info_struct);
}
}
pub fn chain(&self) -> Option<&OxrNextChainStructBase> {
self.structs.first().map(|v| v.as_ref().header())
}
pub fn chain_pointer(&self) -> *const c_void {
self.chain()
.map(|v| v as *const _ as *const c_void)
.unwrap_or(ptr::null())
}
}
pub trait OxrNextChainStructProvider: 'static {
fn header(&self) -> &OxrNextChainStructBase;
fn set_next(&mut self, next: &OxrNextChainStructBase);
fn clear_next(&mut self);
}
#[repr(C)]
#[derive(Copy, Clone, Debug)]
pub struct OxrNextChainStructBase {
pub ty: sys::StructureType,
pub next: *const OxrNextChainStructBase,
}

View File

@@ -0,0 +1,74 @@
use bevy::{
prelude::*,
render::{extract_resource::ExtractResourcePlugin, RenderApp},
};
use bevy_mod_xr::{
session::{XrCreateSession, XrDestroySession},
spaces::{XrPrimaryReferenceSpace, XrReferenceSpace},
};
use crate::{init::create_xr_session, session::OxrSession};
pub struct OxrReferenceSpacePlugin {
pub default_primary_ref_space: openxr::ReferenceSpaceType,
}
impl Default for OxrReferenceSpacePlugin {
fn default() -> Self {
Self {
default_primary_ref_space: openxr::ReferenceSpaceType::STAGE,
}
}
}
/// Resource specifying what the type should be for [`OxrPrimaryReferenceSpace`]. Set through [`OxrReferenceSpacePlugin`].
#[derive(Resource)]
struct OxrDefaultPrimaryReferenceSpaceType(openxr::ReferenceSpaceType);
/// The Default Reference space used for locating things
// #[derive(Resource, Deref, ExtrctResource, Clone)]
// pub struct OxrPrimaryReferenceSpace(pub Arc<openxr::Space>);
/// The Reference space used for locating spaces on this entity
#[derive(Component)]
pub struct OxrReferenceSpace(pub openxr::Space);
impl Plugin for OxrReferenceSpacePlugin {
fn build(&self, app: &mut App) {
app.add_plugins(ExtractResourcePlugin::<XrPrimaryReferenceSpace>::default())
.insert_resource(OxrDefaultPrimaryReferenceSpaceType(
self.default_primary_ref_space,
))
.add_systems(
XrCreateSession,
set_primary_ref_space.after(create_xr_session),
)
.add_systems(XrDestroySession, cleanup);
let render_app = app.sub_app_mut(RenderApp);
render_app.add_systems(XrDestroySession, cleanup);
}
}
fn cleanup(query: Query<Entity, With<XrReferenceSpace>>, mut cmds: Commands) {
cmds.remove_resource::<XrPrimaryReferenceSpace>();
for e in &query {
cmds.entity(e).remove::<XrReferenceSpace>();
}
}
fn set_primary_ref_space(
session: Res<OxrSession>,
space_type: Res<OxrDefaultPrimaryReferenceSpaceType>,
mut cmds: Commands,
) {
match session.create_reference_space(space_type.0, Transform::IDENTITY) {
Ok(space) => {
cmds.insert_resource(XrPrimaryReferenceSpace(space));
}
Err(openxr::sys::Result::ERROR_EXTENSION_NOT_PRESENT) => {
error!("Required Extension for Reference Space not loaded");
}
Err(err) => error!("Error while creating reference space: {}", err.to_string()),
};
}

View File

@@ -0,0 +1,468 @@
use bevy::{
ecs::query::QuerySingleError,
prelude::*,
render::{
camera::{ManualTextureView, ManualTextureViewHandle, ManualTextureViews, RenderTarget},
extract_resource::ExtractResourcePlugin,
pipelined_rendering::PipelinedRenderingPlugin,
view::ExtractedView,
Render, RenderApp,
},
transform::TransformSystem,
};
use bevy_mod_xr::{
camera::{XrCamera, XrCameraBundle, XrProjection},
session::{
XrDestroySession, XrFirst, XrHandleEvents, XrRenderSet, XrRootTransform, XrTrackingRoot,
},
spaces::XrPrimaryReferenceSpace,
};
use openxr::ViewStateFlags;
use crate::{init::should_run_frame_loop, resources::*};
use crate::{layer_builder::ProjectionLayer, session::OxrSession};
#[derive(Debug, Hash, PartialEq, Eq, Clone, Copy, SystemSet)]
pub struct OxrRenderBegin;
#[derive(Debug, Hash, PartialEq, Eq, Clone, Copy, SystemSet)]
pub struct OxrRenderEnd;
pub struct OxrRenderPlugin;
impl Plugin for OxrRenderPlugin {
fn build(&self, app: &mut App) {
if app.is_plugin_added::<PipelinedRenderingPlugin>() {
app.init_resource::<Pipelined>();
// if let Some(sub_app) = app.remove_sub_app(RenderExtractApp) {
// app.insert_sub_app(RenderExtractApp, SubApp::new(sub_app.app, update_rendering));
// }
}
app.add_plugins((
ExtractResourcePlugin::<OxrFrameState>::default(),
ExtractResourcePlugin::<OxrGraphicsInfo>::default(),
ExtractResourcePlugin::<OxrSwapchainImages>::default(),
ExtractResourcePlugin::<OxrViews>::default(),
))
.add_systems(XrDestroySession, clean_views)
.add_systems(
XrFirst,
(
wait_frame.run_if(should_run_frame_loop),
update_cameras.run_if(should_run_frame_loop),
init_views.run_if(resource_added::<OxrSession>),
)
.chain()
.after(XrHandleEvents),
)
.add_systems(
PostUpdate,
(locate_views, update_views)
.before(TransformSystem::TransformPropagate)
.chain()
// .run_if(should_render)
.run_if(should_run_frame_loop),
)
.init_resource::<OxrViews>();
let render_app = app.sub_app_mut(RenderApp);
render_app
.add_systems(XrDestroySession, clean_views)
.add_systems(
Render,
(
begin_frame,
insert_texture_views,
locate_views,
update_views_render_world,
wait_image,
)
.chain()
.in_set(XrRenderSet::PreRender)
.run_if(should_run_frame_loop),
)
.add_systems(
Render,
(release_image, end_frame)
.chain()
.run_if(should_run_frame_loop)
.in_set(XrRenderSet::PostRender),
)
.insert_resource(OxrRenderLayers(vec![Box::new(ProjectionLayer)]));
}
}
// fn update_rendering(app_world: &mut World, _sub_app: &mut App) {
// app_world.resource_scope(|world, main_thread_executor: Mut<MainThreadExecutor>| {
// world.resource_scope(|world, mut render_channels: Mut<RenderAppChannels>| {
// // we use a scope here to run any main thread tasks that the render world still needs to run
// // while we wait for the render world to be received.
// let mut render_app = ComputeTaskPool::get()
// .scope_with_executor(true, Some(&*main_thread_executor.0), |s| {
// s.spawn(async { render_channels.recv().await });
// })
// .pop()
// .unwrap();
// if matches!(world.resource::<XrState>(), XrState::Stopping) {
// world.run_schedule(XrEndSession);
// }
// if matches!(world.resource::<XrState>(), XrState::Exiting { .. }) {
// world.run_schedule(XrDestroySession);
// render_app.app.world.run_schedule(XrDestroySession);
// }
// render_app.extract(world);
// render_channels.send_blocking(render_app);
// });
// });
// }
pub const XR_TEXTURE_INDEX: u32 = 3383858418;
pub fn clean_views(
mut manual_texture_views: ResMut<ManualTextureViews>,
mut commands: Commands,
cam_query: Query<(Entity, &XrCamera)>,
) {
for (e, cam) in &cam_query {
manual_texture_views.remove(&ManualTextureViewHandle(XR_TEXTURE_INDEX + cam.0));
commands.entity(e).despawn_recursive();
}
}
pub fn init_views(
graphics_info: Res<OxrGraphicsInfo>,
mut manual_texture_views: ResMut<ManualTextureViews>,
swapchain_images: Res<OxrSwapchainImages>,
root: Query<Entity, With<XrTrackingRoot>>,
mut commands: Commands,
) {
let _span = info_span!("xr_init_views");
let temp_tex = swapchain_images.first().unwrap();
// this for loop is to easily add support for quad or mono views in the future.
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 cam = commands
.spawn((XrCameraBundle {
camera: Camera {
target: RenderTarget::TextureView(view_handle),
..Default::default()
},
view: XrCamera(index),
..Default::default()
},))
.id();
match root.get_single() {
Ok(root) => {
commands.entity(root).add_child(cam);
}
Err(QuerySingleError::NoEntities(_)) => {
warn!("No XrTrackingRoot!");
}
Err(QuerySingleError::MultipleEntities(_)) => {
warn!("Multiple XrTrackingRoots! this is not allowed");
}
}
}
}
pub fn wait_frame(mut frame_waiter: ResMut<OxrFrameWaiter>, mut commands: Commands) {
let _span = info_span!("xr_wait_frame");
let state = frame_waiter.wait().expect("Failed to wait frame");
commands.insert_resource(OxrFrameState(state));
}
pub fn update_cameras(
frame_state: Res<OxrFrameState>,
mut cameras: Query<&mut Camera, With<XrCamera>>,
) {
if frame_state.is_changed() {
for mut camera in &mut cameras {
camera.is_active = frame_state.should_render
}
}
}
pub fn locate_views(
session: Res<OxrSession>,
ref_space: Res<XrPrimaryReferenceSpace>,
frame_state: Res<OxrFrameState>,
mut openxr_views: ResMut<OxrViews>,
pipelined: Option<Res<Pipelined>>,
) {
let time = if pipelined.is_some() {
openxr::Time::from_nanos(
frame_state.predicted_display_time.as_nanos()
+ frame_state.predicted_display_period.as_nanos(),
)
} else {
frame_state.predicted_display_time
};
let (flags, xr_views) = session
.locate_views(
openxr::ViewConfigurationType::PRIMARY_STEREO,
time,
&ref_space,
)
.expect("Failed to locate views");
match (
flags & ViewStateFlags::ORIENTATION_VALID == ViewStateFlags::ORIENTATION_VALID,
flags & ViewStateFlags::POSITION_VALID == ViewStateFlags::POSITION_VALID,
) {
(true, true) => *openxr_views = OxrViews(xr_views),
(true, false) => {
for (i, view) in openxr_views.iter_mut().enumerate() {
let Some(xr_view) = xr_views.get(i) else {
break;
};
view.pose.orientation = xr_view.pose.orientation;
}
}
(false, true) => {
for (i, view) in openxr_views.iter_mut().enumerate() {
let Some(xr_view) = xr_views.get(i) else {
break;
};
view.pose.position = xr_view.pose.position;
}
}
(false, false) => {}
}
}
pub fn update_views(
mut query: Query<(&mut Transform, &mut XrProjection, &XrCamera)>,
views: ResMut<OxrViews>,
) {
for (mut transform, mut projection, camera) in query.iter_mut() {
let Some(view) = views.get(camera.0 as usize) else {
continue;
};
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;
}
}
pub fn update_views_render_world(
views: Res<OxrViews>,
root: Res<XrRootTransform>,
mut query: Query<(&mut ExtractedView, &XrCamera)>,
) {
for (mut extracted_view, camera) in query.iter_mut() {
let Some(view) = views.get(camera.0 as usize) else {
continue;
};
let mut transform = Transform::IDENTITY;
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;
extracted_view.transform = root.0.mul_transform(transform);
}
}
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)
}
/// # Safety
/// Images inserted into texture views here should not be written to until [`wait_image`] is ran
pub fn insert_texture_views(
swapchain_images: Res<OxrSwapchainImages>,
mut swapchain: ResMut<OxrSwapchain>,
mut manual_texture_views: ResMut<ManualTextureViews>,
graphics_info: Res<OxrGraphicsInfo>,
) {
let _span = info_span!("xr_insert_texture_views");
let index = swapchain.acquire_image().expect("Failed to acquire image");
let image = &swapchain_images[index as usize];
for i in 0..2 {
add_texture_view(&mut manual_texture_views, image, &graphics_info, i);
}
}
pub fn wait_image(mut swapchain: ResMut<OxrSwapchain>) {
swapchain
.wait_image(openxr::Duration::INFINITE)
.expect("Failed to wait image");
}
pub fn add_texture_view(
manual_texture_views: &mut ManualTextureViews,
texture: &wgpu::Texture,
info: &OxrGraphicsInfo,
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 begin_frame(mut frame_stream: ResMut<OxrFrameStream>) {
let _span = info_span!("xr_begin_frame");
frame_stream.begin().expect("Failed to begin frame");
}
pub fn release_image(mut swapchain: ResMut<OxrSwapchain>) {
let _span = info_span!("xr_release_image");
#[cfg(target_os = "android")]
{
let ctx = ndk_context::android_context();
let vm = unsafe { jni::JavaVM::from_raw(ctx.vm().cast()) }.unwrap();
let env = vm.attach_current_thread_as_daemon();
}
swapchain.release_image().unwrap();
}
pub fn end_frame(world: &mut World) {
let _span = info_span!("xr_end_frame");
#[cfg(target_os = "android")]
{
let ctx = ndk_context::android_context();
let vm = unsafe { jni::JavaVM::from_raw(ctx.vm().cast()) }.unwrap();
let env = vm.attach_current_thread_as_daemon();
}
world.resource_scope::<OxrFrameStream, ()>(|world, mut frame_stream| {
let mut layers = vec![];
let frame_state = world.resource::<OxrFrameState>();
if frame_state.should_render {
for layer in world.resource::<OxrRenderLayers>().iter() {
if let Some(layer) = layer.get(world) {
layers.push(layer);
}
}
}
let layers: Vec<_> = layers.iter().map(Box::as_ref).collect();
if let Err(e) = frame_stream.end(
frame_state.predicted_display_time,
world.resource::<OxrGraphicsInfo>().blend_mode,
&layers,
) {
error!("Failed to end frame stream: {e}");
}
});
}

View File

@@ -0,0 +1,360 @@
use bevy::prelude::*;
use bevy::render::extract_resource::ExtractResource;
use crate::error::OxrError;
use crate::graphics::*;
use crate::layer_builder::{CompositionLayer, LayerProvider};
use crate::session::{OxrSession, OxrSessionCreateNextChain};
use crate::types::*;
/// Wrapper around an [`Entry`](openxr::Entry) with some methods overridden to use bevy types.
///
/// See [`openxr::Entry`] for other available methods.
#[derive(Deref, Clone)]
pub struct OxrEntry(pub openxr::Entry);
impl OxrEntry {
/// Enumerate available extensions for this OpenXR runtime.
pub fn enumerate_extensions(&self) -> Result<OxrExtensions> {
Ok(self.0.enumerate_extensions().map(Into::into)?)
}
/// Creates an [`OxrInstance`].
///
/// Calls [`create_instance`](openxr::Entry::create_instance) internally.
pub fn create_instance(
&self,
app_info: AppInfo,
exts: OxrExtensions,
layers: &[&str],
backend: GraphicsBackend,
) -> Result<OxrInstance> {
let available_exts = self.enumerate_extensions()?;
if !backend.is_available(&available_exts) {
return Err(OxrError::UnavailableBackend(backend));
}
let required_exts = exts | backend.required_exts();
let instance = self.0.create_instance(
&openxr::ApplicationInfo {
application_name: &app_info.name,
application_version: app_info.version.to_u32(),
engine_name: "Bevy",
engine_version: Version::BEVY.to_u32(),
},
&required_exts.into(),
layers,
)?;
Ok(OxrInstance(instance, backend, app_info))
}
/// Returns a list of all of the backends the OpenXR runtime supports.
pub fn available_backends(&self) -> Result<Vec<GraphicsBackend>> {
Ok(GraphicsBackend::available_backends(
&self.enumerate_extensions()?,
))
}
}
/// Wrapper around [`openxr::Instance`] with additional data for safety and some methods overriden to use bevy types.
///
/// See [`openxr::Instance`] for other available methods.
#[derive(Resource, Deref, Clone)]
pub struct OxrInstance(
#[deref] pub(crate) openxr::Instance,
/// [`GraphicsBackend`] is stored here to let us know what graphics API the current instance wants to target.
pub(crate) GraphicsBackend,
pub(crate) AppInfo,
);
impl OxrInstance {
/// Creates an [`OxrInstance`] from an [`openxr::Instance`] if needed.
/// In the majority of cases, you should use [`create_instance`](OxrEntry::create_instance) instead.
///
/// # Safety
///
/// The OpenXR instance passed in *must* have support for the backend specified.
pub unsafe fn from_inner(
instance: openxr::Instance,
backend: GraphicsBackend,
info: AppInfo,
) -> Self {
Self(instance, backend, info)
}
/// Consumes self and returns the inner [`openxr::Instance`]
pub fn into_inner(self) -> openxr::Instance {
self.0
}
/// Returns the current backend being used by this instance.
pub fn backend(&self) -> GraphicsBackend {
self.1
}
/// Returns the [`AppInfo`] being used by this instance.
pub fn app_info(&self) -> &AppInfo {
&self.2
}
/// Initialize graphics. This is used to create [WgpuGraphics] for the bevy app and to get the [SessionCreateInfo] needed to make an XR session.
pub fn init_graphics(
&self,
system_id: openxr::SystemId,
) -> Result<(WgpuGraphics, SessionCreateInfo)> {
graphics_match!(
self.1;
_ => {
let (graphics, session_info) = Api::init_graphics(&self.2, &self, system_id)?;
Ok((graphics, SessionCreateInfo(Api::wrap(session_info))))
}
)
}
/// Creates an [OxrSession]
///
/// Calls [`create_session`](openxr::Instance::create_session) internally.
///
/// # Safety
///
/// `info` must contain valid handles for the graphics api
pub unsafe fn create_session(
&self,
system_id: openxr::SystemId,
info: SessionCreateInfo,
chain: &mut OxrSessionCreateNextChain,
) -> Result<(OxrSession, OxrFrameWaiter, OxrFrameStream)> {
if !info.0.using_graphics_of_val(&self.1) {
return Err(OxrError::GraphicsBackendMismatch {
item: std::any::type_name::<SessionCreateInfo>(),
backend: info.0.graphics_name(),
expected_backend: self.1.graphics_name(),
});
}
graphics_match!(
info.0;
info => {
let (session, frame_waiter, frame_stream) = Api::create_session(self,system_id, &info,chain)?;
Ok((session.into(), OxrFrameWaiter(frame_waiter), OxrFrameStream(Api::wrap(frame_stream))))
}
)
}
}
/// Graphics agnostic wrapper around [openxr::FrameStream]
#[derive(Resource)]
pub struct OxrFrameStream(pub GraphicsWrap<Self>);
impl GraphicsType for OxrFrameStream {
type Inner<G: GraphicsExt> = openxr::FrameStream<G>;
}
impl OxrFrameStream {
/// Creates a new [`OxrFrameStream`] from an [`openxr::FrameStream`].
/// In the majority of cases, you should use [`create_session`](OxrInstance::create_session) instead.
pub fn from_inner<G: GraphicsExt>(frame_stream: openxr::FrameStream<G>) -> Self {
Self(G::wrap(frame_stream))
}
/// Indicate that graphics device work is beginning.
///
/// Calls [`begin`](openxr::FrameStream::begin) internally.
pub fn begin(&mut self) -> openxr::Result<()> {
graphics_match!(
&mut self.0;
stream => stream.begin()
)
}
/// Indicate that all graphics work for the frame has been submitted
///
/// `layers` is an array of references to any type of composition layer that implements [`CompositionLayer`],
/// e.g. [`CompositionLayerProjection`](crate::layer_builder::CompositionLayerProjection)
pub fn end(
&mut self,
display_time: openxr::Time,
environment_blend_mode: openxr::EnvironmentBlendMode,
layers: &[&dyn CompositionLayer],
) -> Result<()> {
graphics_match!(
&mut self.0;
stream => {
let mut new_layers = vec![];
for (i, layer) in layers.into_iter().enumerate() {
if let Some(swapchain) = layer.swapchain() {
if !swapchain.0.using_graphics::<Api>() {
error!(
"Composition layer {i} is using graphics api '{}', expected graphics api '{}'. Excluding layer from frame submission.",
swapchain.0.graphics_name(),
std::any::type_name::<Api>(),
);
continue;
}
}
new_layers.push(unsafe { std::mem::transmute(layer.header()) });
}
Ok(stream.end(display_time, environment_blend_mode, new_layers.as_slice())?)
}
)
}
}
/// Handle for waiting to render a frame.
///
/// See [`FrameWaiter`](openxr::FrameWaiter) for available methods.
#[derive(Resource, Deref, DerefMut)]
pub struct OxrFrameWaiter(pub openxr::FrameWaiter);
/// Graphics agnostic wrapper around [openxr::Swapchain]
#[derive(Resource)]
pub struct OxrSwapchain(pub GraphicsWrap<Self>);
impl GraphicsType for OxrSwapchain {
type Inner<G: GraphicsExt> = openxr::Swapchain<G>;
}
impl OxrSwapchain {
/// Creates a new [`OxrSwapchain`] from an [`openxr::Swapchain`].
/// In the majority of cases, you should use [`create_swapchain`](OxrSession::create_swapchain) instead.
pub fn from_inner<G: GraphicsExt>(swapchain: openxr::Swapchain<G>) -> Self {
Self(G::wrap(swapchain))
}
/// Determine the index of the next image to render to in the swapchain image array.
///
/// Calls [`acquire_image`](openxr::Swapchain::acquire_image) internally.
pub fn acquire_image(&mut self) -> Result<u32> {
graphics_match!(
&mut self.0;
swap => Ok(swap.acquire_image()?)
)
}
/// Wait for the compositor to finish reading from the oldest unwaited acquired image.
///
/// Calls [`wait_image`](openxr::Swapchain::wait_image) internally.
pub fn wait_image(&mut self, timeout: openxr::Duration) -> Result<()> {
graphics_match!(
&mut self.0;
swap => Ok(swap.wait_image(timeout)?)
)
}
/// Release the oldest acquired image.
///
/// Calls [`release_image`](openxr::Swapchain::release_image) internally.
pub fn release_image(&mut self) -> Result<()> {
graphics_match!(
&mut self.0;
swap => Ok(swap.release_image()?)
)
}
/// Enumerates swapchain images and converts them to wgpu [`Texture`](wgpu::Texture)s.
///
/// Calls [`enumerate_images`](openxr::Swapchain::enumerate_images) internally.
pub fn enumerate_images(
&self,
device: &wgpu::Device,
format: wgpu::TextureFormat,
resolution: UVec2,
) -> Result<OxrSwapchainImages> {
graphics_match!(
&self.0;
swap => {
let mut images = vec![];
for image in swap.enumerate_images()? {
unsafe {
images.push(Api::to_wgpu_img(image, device, format, resolution)?);
}
}
Ok(OxrSwapchainImages(images.leak()))
}
)
}
}
/// Stores the generated swapchain images.
#[derive(Debug, Deref, Resource, Clone, Copy, ExtractResource)]
pub struct OxrSwapchainImages(pub &'static [wgpu::Texture]);
/// Thread safe wrapper around [openxr::Space] representing the stage.
// #[derive(Deref, Clone, Resource)]
// pub struct OxrStage(pub Arc<openxr::Space>);
/// Stores the latest generated [OxrViews]
#[derive(Clone, Resource, ExtractResource, Deref, DerefMut, Default)]
pub struct OxrViews(pub Vec<openxr::View>);
/// Wrapper around [openxr::SystemId] to allow it to be stored as a resource.
#[derive(Debug, Copy, Clone, Deref, Default, Eq, PartialEq, Ord, PartialOrd, Hash, Resource)]
pub struct OxrSystemId(pub openxr::SystemId);
/// Wrapper around [`openxr::Passthrough`].
///
/// Used to [`start`](openxr::Passthrough::start) or [`pause`](openxr::Passthrough::pause) passthrough on the physical device.
///
/// See [`openxr::Passthrough`] for available methods.
#[derive(Resource, Deref, DerefMut)]
pub struct OxrPassthrough(
#[deref] pub openxr::Passthrough,
/// The flags are stored here so that they don't need to be passed in again when creating an [`OxrPassthroughLayer`].
pub openxr::PassthroughFlagsFB,
);
impl OxrPassthrough {
/// This function can create an [`OxrPassthrough`] from raw openxr types if needed.
/// In the majority of cases, you should use [`create_passthrough`](OxrSession::create_passthrough) instead.
pub fn from_inner(passthrough: openxr::Passthrough, flags: openxr::PassthroughFlagsFB) -> Self {
Self(passthrough, flags)
}
}
/// Wrapper around [`openxr::Passthrough`].
///
/// Used to create a [`CompositionLayerPassthrough`](crate::layer_builder::CompositionLayerPassthrough), and to [`pause`](openxr::PassthroughLayer::pause) or [`resume`](openxr::PassthroughLayer::resume) rendering of the passthrough layer.
///
/// See [`openxr::PassthroughLayer`] for available methods.
#[derive(Resource, Deref, DerefMut)]
pub struct OxrPassthroughLayer(pub openxr::PassthroughLayer);
#[derive(Resource, Deref, DerefMut, Default)]
pub struct OxrRenderLayers(pub Vec<Box<dyn LayerProvider + Send + Sync>>);
/// Resource storing graphics info for the currently running session.
#[derive(Clone, Copy, Resource, ExtractResource)]
pub struct OxrGraphicsInfo {
pub blend_mode: EnvironmentBlendMode,
pub resolution: UVec2,
pub format: wgpu::TextureFormat,
}
#[derive(Clone)]
/// This is used to store information from startup that is needed to create the session after the instance has been created.
pub struct SessionConfigInfo {
/// 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 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>>,
/// Graphics info used to create a session.
pub graphics_info: SessionCreateInfo,
}
#[derive(ExtractResource, Resource, Clone, Default)]
pub struct OxrSessionStarted(pub bool);
/// The frame state returned from [FrameWaiter::wait_frame](openxr::FrameWaiter::wait)
#[derive(Clone, Deref, DerefMut, Resource, ExtractResource)]
pub struct OxrFrameState(pub openxr::FrameState);
/// Instructs systems to add display period
#[derive(Clone, Copy, Default, Resource)]
pub struct Pipelined;

View File

@@ -0,0 +1,117 @@
use std::ffi::c_void;
use crate::next_chain::{OxrNextChain, OxrNextChainStructBase, OxrNextChainStructProvider};
use crate::resources::{OxrPassthrough, OxrPassthroughLayer, OxrSwapchain};
use crate::types::{Result, SwapchainCreateInfo};
use bevy::prelude::*;
use openxr::AnyGraphics;
use crate::graphics::{graphics_match, GraphicsExt, GraphicsType, GraphicsWrap};
/// Graphics agnostic wrapper around [openxr::Session].
///
/// See [`openxr::Session`] for other available methods.
#[derive(Resource, Deref, Clone)]
pub struct OxrSession(
/// A session handle with [`AnyGraphics`].
/// Having this here allows the majority of [`Session`](openxr::Session)'s methods to work without having to rewrite them.
#[deref]
pub(crate) openxr::Session<AnyGraphics>,
/// A [`GraphicsWrap`] with [`openxr::Session<G>`] as the inner type.
/// This is so that we can still operate on functions that don't take [`AnyGraphics`] as the generic.
pub(crate) GraphicsWrap<Self>,
);
impl GraphicsType for OxrSession {
type Inner<G: GraphicsExt> = openxr::Session<G>;
}
impl<G: GraphicsExt> From<openxr::Session<G>> for OxrSession {
fn from(session: openxr::Session<G>) -> Self {
Self::from_inner(session)
}
}
impl OxrSession {
/// Creates a new [`OxrSession`] from an [`openxr::Session`].
/// In the majority of cases, you should use [`create_session`](OxrInstance::create_session) instead.
pub fn from_inner<G: GraphicsExt>(session: openxr::Session<G>) -> Self {
Self(session.clone().into_any_graphics(), G::wrap(session))
}
/// Returns [`GraphicsWrap`] with [`openxr::Session<G>`] as the inner type.
///
/// This can be useful if you need access to the original [`openxr::Session`] with the graphics API still specified.
pub fn typed_session(&self) -> &GraphicsWrap<Self> {
&self.1
}
/// Enumerates all available swapchain formats and converts them to wgpu's [`TextureFormat`](wgpu::TextureFormat).
///
/// Calls [`enumerate_swapchain_formats`](openxr::Session::enumerate_swapchain_formats) internally.
pub fn enumerate_swapchain_formats(&self) -> Result<Vec<wgpu::TextureFormat>> {
graphics_match!(
&self.1;
session => Ok(session.enumerate_swapchain_formats()?.into_iter().filter_map(Api::into_wgpu_format).collect())
)
}
/// Creates an [OxrSwapchain].
///
/// Calls [`create_swapchain`](openxr::Session::create_swapchain) internally.
pub fn create_swapchain(&self, info: SwapchainCreateInfo) -> Result<OxrSwapchain> {
Ok(OxrSwapchain(graphics_match!(
&self.1;
session => session.create_swapchain(&info.try_into()?)? => OxrSwapchain
)))
}
/// Creates a passthrough.
///
/// Requires [`XR_FB_passthrough`](https://www.khronos.org/registry/OpenXR/specs/1.0/html/xrspec.html#XR_FB_passthrough).
///
/// Calls [`create_passthrough`](openxr::Session::create_passthrough) internally.
pub fn create_passthrough(&self, flags: openxr::PassthroughFlagsFB) -> Result<OxrPassthrough> {
Ok(OxrPassthrough(
graphics_match! {
&self.1;
session => session.create_passthrough(flags)?
},
flags,
))
}
/// Creates a passthrough layer that can be used to make a [`CompositionLayerPassthrough`](crate::layer_builder::CompositionLayerPassthrough) for frame submission.
///
/// Requires [`XR_FB_passthrough`](https://www.khronos.org/registry/OpenXR/specs/1.0/html/xrspec.html#XR_FB_passthrough).
///
/// Calls [`create_passthrough_layer`](openxr::Session::create_passthrough_layer) internally.
pub fn create_passthrough_layer(
&self,
passthrough: &OxrPassthrough,
purpose: openxr::PassthroughLayerPurposeFB,
) -> Result<OxrPassthroughLayer> {
Ok(OxrPassthroughLayer(graphics_match! {
&self.1;
session => session.create_passthrough_layer(&passthrough.0, passthrough.1, purpose)?
}))
}
}
pub trait OxrSessionCreateNextProvider: OxrNextChainStructProvider {}
/// NonSend Resource
#[derive(Default)]
pub struct OxrSessionCreateNextChain(OxrNextChain);
impl OxrSessionCreateNextChain {
pub fn push<T: OxrSessionCreateNextProvider>(&mut self, info_struct: T) {
self.0.push(info_struct)
}
pub fn chain(&self) -> Option<&OxrNextChainStructBase> {
self.0.chain()
}
pub fn chain_pointer(&self) -> *const c_void {
self.0.chain_pointer()
}
}

View File

@@ -0,0 +1,582 @@
use std::{mem::MaybeUninit, ptr, sync::Mutex};
use bevy::{prelude::*, utils::hashbrown::HashSet};
use bevy_mod_xr::{
session::{session_available, session_running, XrFirst, XrHandleEvents},
spaces::{XrDestroySpace, XrPrimaryReferenceSpace, XrReferenceSpace, XrSpace, XrSpatialOffset},
types::XrPose,
};
use openxr::{
sys, HandJointLocation, HandJointLocations, HandJointVelocities, HandJointVelocity,
ReferenceSpaceType, SpaceLocationFlags, HAND_JOINT_COUNT,
};
use crate::{
helper_traits::{ToPosef, ToQuat, ToVec3},
resources::{OxrFrameState, OxrInstance, Pipelined},
session::OxrSession,
};
#[derive(SystemSet, Hash, Debug, Clone, Copy, PartialEq, Eq)]
pub struct OxrSpaceSyncSet;
/// VERY IMPORTANT!! only disable when you know what you are doing
pub struct OxrSpacePatchingPlugin;
impl Plugin for OxrSpacePatchingPlugin {
fn build(&self, app: &mut App) {
app.add_systems(Startup, patch_destroy_space.run_if(session_available));
}
}
pub struct OxrSpatialPlugin;
impl Plugin for OxrSpatialPlugin {
fn build(&self, app: &mut App) {
app.add_event::<XrDestroySpace>()
.add_systems(XrFirst, destroy_space_event.before(XrHandleEvents))
.add_systems(
PreUpdate,
update_space_transforms
.in_set(OxrSpaceSyncSet)
.run_if(session_running),
);
}
}
fn destroy_space_event(instance: Res<OxrInstance>, mut events: EventReader<XrDestroySpace>) {
for space in events.read() {
match instance.destroy_space(space.0) {
Ok(_) => (),
Err(err) => warn!("error while destroying space: {}", err),
}
}
}
pub static OXR_DO_NOT_CALL_DESTOY_SPACE_FOR_SPACES: Mutex<Option<HashSet<u64>>> = Mutex::new(None);
pub static OXR_ORIGINAL_DESTOY_SPACE: Mutex<Option<openxr::sys::pfn::DestroySpace>> =
Mutex::new(None);
fn patch_destroy_space(instance: ResMut<OxrInstance>) {
OXR_DO_NOT_CALL_DESTOY_SPACE_FOR_SPACES
.lock()
.unwrap()
.replace(HashSet::new());
let raw_instance_ptr = instance.fp() as *const _ as *mut openxr::raw::Instance;
unsafe {
OXR_ORIGINAL_DESTOY_SPACE
.lock()
.unwrap()
.replace((*raw_instance_ptr).destroy_space);
(*raw_instance_ptr).destroy_space = patched_destroy_space;
}
}
unsafe extern "system" fn patched_destroy_space(space: openxr::sys::Space) -> openxr::sys::Result {
if !OXR_DO_NOT_CALL_DESTOY_SPACE_FOR_SPACES
.lock()
.unwrap()
.as_ref()
.unwrap()
.contains(&space.into_raw())
{
OXR_ORIGINAL_DESTOY_SPACE
.lock()
.unwrap()
.expect("has to be initialized")(space)
} else {
info!("Inject Worked, not destroying space");
openxr::sys::Result::SUCCESS
}
}
fn update_space_transforms(
session: Res<OxrSession>,
default_ref_space: Res<XrPrimaryReferenceSpace>,
pipelined: Option<Res<Pipelined>>,
frame_state: Res<OxrFrameState>,
mut query: Query<(
&mut Transform,
&XrSpace,
Option<&XrSpatialOffset>,
Option<&XrReferenceSpace>,
)>,
) {
for (mut transform, space, offset, ref_space) in &mut query {
let offset = offset.copied().unwrap_or_default();
let ref_space = ref_space.unwrap_or(&default_ref_space);
if let Ok(space_location) = session.locate_space(
space,
ref_space,
if pipelined.is_some() {
openxr::Time::from_nanos(
frame_state.predicted_display_time.as_nanos()
+ frame_state.predicted_display_period.as_nanos(),
)
} else {
frame_state.predicted_display_time
},
) {
if space_location
.location_flags
.contains(SpaceLocationFlags::POSITION_VALID)
{
transform.translation = offset
.to_transform()
.transform_point(space_location.pose.position.to_vec3())
}
if space_location
.location_flags
.contains(SpaceLocationFlags::ORIENTATION_VALID)
{
transform.rotation = offset.rotation * space_location.pose.orientation.to_quat();
}
}
}
}
impl OxrSession {
pub fn create_action_space<T: openxr::ActionTy>(
&self,
action: &openxr::Action<T>,
subaction_path: openxr::Path,
pose_in_space: XrPose,
) -> openxr::Result<XrSpace> {
let info = sys::ActionSpaceCreateInfo {
ty: sys::ActionSpaceCreateInfo::TYPE,
next: ptr::null(),
action: action.as_raw(),
subaction_path,
pose_in_action_space: pose_in_space.to_posef(),
};
let mut out = sys::Space::NULL;
unsafe {
cvt((self.instance().fp().create_action_space)(
self.as_raw(),
&info,
&mut out,
))?;
Ok(XrSpace::from_raw(out.into_raw()))
}
}
pub fn create_reference_space(
&self,
ref_space_type: ReferenceSpaceType,
pose_in_ref_space: Transform,
) -> openxr::Result<XrReferenceSpace> {
let info = sys::ReferenceSpaceCreateInfo {
ty: sys::ReferenceSpaceCreateInfo::TYPE,
next: ptr::null(),
reference_space_type: ref_space_type,
pose_in_reference_space: pose_in_ref_space.to_posef(),
};
let mut out = sys::Space::NULL;
unsafe {
cvt((self.instance().fp().create_reference_space)(
self.as_raw(),
&info,
&mut out,
))?;
Ok(XrReferenceSpace(XrSpace::from_raw(out.into_raw())))
}
}
}
fn locate_space(
instance: &openxr::Instance,
space: &XrSpace,
base: &XrSpace,
time: openxr::Time,
) -> openxr::Result<openxr::SpaceLocation> {
unsafe {
let mut x = sys::SpaceLocation::out(ptr::null_mut());
cvt((instance.fp().locate_space)(
space.as_raw_openxr_space(),
base.as_raw_openxr_space(),
time,
x.as_mut_ptr(),
))?;
Ok(create_space_location(&x))
}
}
fn locate_space_with_velocity(
instance: &openxr::Instance,
space: &XrSpace,
base: &XrSpace,
time: openxr::Time,
) -> openxr::Result<(openxr::SpaceLocation, openxr::SpaceVelocity)> {
unsafe {
let mut velocity = sys::SpaceVelocity::out(ptr::null_mut());
let mut location = sys::SpaceLocation::out(&mut velocity as *mut _ as _);
cvt((instance.fp().locate_space)(
space.as_raw_openxr_space(),
base.as_raw_openxr_space(),
time,
location.as_mut_ptr(),
))?;
Ok((
create_space_location(&location),
create_space_velocity(&velocity),
))
}
}
pub fn locate_hand_joints(
instance: &openxr::Instance,
tracker: &openxr::HandTracker,
base: &XrSpace,
time: openxr::Time,
) -> openxr::Result<Option<HandJointLocations>> {
unsafe {
let locate_info = sys::HandJointsLocateInfoEXT {
ty: sys::HandJointsLocateInfoEXT::TYPE,
next: ptr::null(),
base_space: base.as_raw_openxr_space(),
time,
};
let mut locations =
MaybeUninit::<[openxr::HandJointLocation; openxr::HAND_JOINT_COUNT]>::uninit();
let mut location_info = sys::HandJointLocationsEXT {
ty: sys::HandJointLocationsEXT::TYPE,
next: ptr::null_mut(),
is_active: false.into(),
joint_count: openxr::HAND_JOINT_COUNT as u32,
joint_locations: locations.as_mut_ptr() as _,
};
cvt((instance
.exts()
.ext_hand_tracking
.as_ref()
.expect("Somehow created HandTracker without XR_EXT_hand_tracking being enabled")
.locate_hand_joints)(
tracker.as_raw(),
&locate_info,
&mut location_info,
))?;
Ok(if location_info.is_active.into() {
Some(locations.assume_init())
} else {
None
})
}
}
pub fn locate_hand_joints_with_velocities(
instance: &openxr::Instance,
tracker: &openxr::HandTracker,
base: &XrSpace,
time: openxr::Time,
) -> openxr::Result<Option<(HandJointLocations, HandJointVelocities)>> {
unsafe {
let locate_info = sys::HandJointsLocateInfoEXT {
ty: sys::HandJointsLocateInfoEXT::TYPE,
next: ptr::null(),
base_space: base.as_raw_openxr_space(),
time,
};
let mut velocities = MaybeUninit::<[HandJointVelocity; HAND_JOINT_COUNT]>::uninit();
let mut velocity_info = sys::HandJointVelocitiesEXT {
ty: sys::HandJointVelocitiesEXT::TYPE,
next: ptr::null_mut(),
joint_count: HAND_JOINT_COUNT as u32,
joint_velocities: velocities.as_mut_ptr() as _,
};
let mut locations = MaybeUninit::<[HandJointLocation; HAND_JOINT_COUNT]>::uninit();
let mut location_info = sys::HandJointLocationsEXT {
ty: sys::HandJointLocationsEXT::TYPE,
next: &mut velocity_info as *mut _ as _,
is_active: false.into(),
joint_count: HAND_JOINT_COUNT as u32,
joint_locations: locations.as_mut_ptr() as _,
};
cvt((instance
.exts()
.ext_hand_tracking
.as_ref()
.expect("Somehow created HandTracker without XR_EXT_hand_tracking being enabled")
.locate_hand_joints)(
tracker.as_raw(),
&locate_info,
&mut location_info,
))?;
Ok(if location_info.is_active.into() {
Some((locations.assume_init(), velocities.assume_init()))
} else {
None
})
}
}
pub fn destroy_space(
instance: &openxr::Instance,
space: sys::Space,
) -> openxr::Result<sys::Result> {
OXR_DO_NOT_CALL_DESTOY_SPACE_FOR_SPACES
.lock()
.unwrap()
.as_mut()
.unwrap()
.remove(&space.into_raw());
let result = unsafe { (instance.fp().destroy_space)(space) };
cvt(result)
}
impl OxrSession {
pub fn allow_auto_destruct_of_openxr_space(&self, space: &openxr::Space) {
OXR_DO_NOT_CALL_DESTOY_SPACE_FOR_SPACES
.lock()
.unwrap()
.as_mut()
.unwrap()
.remove(&space.as_raw().into_raw());
}
pub fn destroy_space(&self, space: XrSpace) -> openxr::Result<sys::Result> {
destroy_space(self.instance(), space.as_raw_openxr_space())
}
pub fn destroy_openxr_space(&self, space: openxr::Space) -> openxr::Result<sys::Result> {
destroy_space(self.instance(), space.as_raw())
}
pub fn locate_views(
&self,
view_configuration_type: openxr::ViewConfigurationType,
display_time: openxr::Time,
ref_space: &XrReferenceSpace,
) -> openxr::Result<(openxr::ViewStateFlags, Vec<openxr::View>)> {
let info = sys::ViewLocateInfo {
ty: sys::ViewLocateInfo::TYPE,
next: ptr::null(),
view_configuration_type,
display_time,
space: ref_space.as_raw_openxr_space(),
};
let (flags, raw) = unsafe {
let mut out = sys::ViewState::out(ptr::null_mut());
let raw = get_arr_init(sys::View::out(ptr::null_mut()), |cap, count, buf| {
(self.instance().fp().locate_views)(
self.as_raw(),
&info,
out.as_mut_ptr(),
cap,
count,
buf as _,
)
})?;
(out.assume_init().view_state_flags, raw)
};
Ok((
flags,
raw.into_iter()
.map(|x| unsafe { create_view(flags, &x) })
.collect(),
))
}
pub fn locate_space(
&self,
space: &XrSpace,
base: &XrSpace,
time: openxr::Time,
) -> openxr::Result<openxr::SpaceLocation> {
locate_space(self.instance(), space, base, time)
}
pub fn locate_space_with_velocity(
&self,
space: &XrSpace,
base: &XrSpace,
time: openxr::Time,
) -> openxr::Result<(openxr::SpaceLocation, openxr::SpaceVelocity)> {
locate_space_with_velocity(self.instance(), space, base, time)
}
pub fn locate_hand_joints(
&self,
tracker: &openxr::HandTracker,
base: &XrSpace,
time: openxr::Time,
) -> openxr::Result<Option<openxr::HandJointLocations>> {
locate_hand_joints(self.instance(), tracker, base, time)
}
pub fn locate_hand_joints_with_velocities(
&self,
tracker: &openxr::HandTracker,
base: &XrSpace,
time: openxr::Time,
) -> openxr::Result<Option<(HandJointLocations, HandJointVelocities)>> {
locate_hand_joints_with_velocities(self.instance(), tracker, base, time)
}
}
impl OxrInstance {
pub fn allow_auto_destruct_of_openxr_space(&self, space: &openxr::Space) {
OXR_DO_NOT_CALL_DESTOY_SPACE_FOR_SPACES
.lock()
.unwrap()
.as_mut()
.unwrap()
.remove(&space.as_raw().into_raw());
}
pub fn destroy_space(&self, space: XrSpace) -> openxr::Result<sys::Result> {
destroy_space(self, space.as_raw_openxr_space())
}
pub fn destroy_openxr_space(&self, space: openxr::Space) -> openxr::Result<sys::Result> {
destroy_space(self, space.as_raw())
}
pub fn locate_space(
&self,
space: &XrSpace,
base: &XrSpace,
time: openxr::Time,
) -> openxr::Result<openxr::SpaceLocation> {
locate_space(self, space, base, time)
}
pub fn locate_space_with_velocity(
&self,
space: &XrSpace,
base: &XrSpace,
time: openxr::Time,
) -> openxr::Result<(openxr::SpaceLocation, openxr::SpaceVelocity)> {
locate_space_with_velocity(self, space, base, time)
}
pub fn locate_hand_joints(
&self,
tracker: &openxr::HandTracker,
base: &XrSpace,
time: openxr::Time,
) -> openxr::Result<Option<openxr::HandJointLocations>> {
locate_hand_joints(self, tracker, base, time)
}
pub fn locate_hand_joints_with_velocities(
&self,
tracker: &openxr::HandTracker,
base: &XrSpace,
time: openxr::Time,
) -> openxr::Result<Option<(HandJointLocations, HandJointVelocities)>> {
locate_hand_joints_with_velocities(self, tracker, base, time)
}
}
/// # Safety
/// This is an Extension trait. DO NOT IMPLEMENT IT!
pub unsafe trait OxrSpaceExt {
/// get an openxr::sys::Space as a reference to the XrSpace
/// does not remove the space from the space managment system!
fn as_raw_openxr_space(&self) -> sys::Space;
/// Adds the openxr::sys::Space into the the space managment system
fn from_raw_openxr_space(space: sys::Space) -> Self;
/// Adds the openxr::Space into the the space manegment system
fn from_openxr_space(space: openxr::Space) -> Self;
/// get an openxr::Space as a reference to the XrSpace
/// does not remove the space from the space managment system!
/// # Safety
/// Session has to be the session from which the space is from
unsafe fn as_openxr_space<T>(&self, session: &openxr::Session<T>) -> openxr::Space;
/// get an openxr::Space as an onwned version of the XrSpace
/// removes the space from the space managment system!
/// # Safety
/// Session has to be the session from which the space is from
unsafe fn into_openxr_space<T>(self, session: &openxr::Session<T>) -> openxr::Space;
}
unsafe impl OxrSpaceExt for XrSpace {
fn as_raw_openxr_space(&self) -> sys::Space {
sys::Space::from_raw(self.as_raw())
}
fn from_raw_openxr_space(space: sys::Space) -> Self {
let raw = space.into_raw();
OXR_DO_NOT_CALL_DESTOY_SPACE_FOR_SPACES
.lock()
.unwrap()
.as_mut()
.unwrap()
.insert(raw);
unsafe { XrSpace::from_raw(raw) }
}
fn from_openxr_space(space: openxr::Space) -> Self {
Self::from_raw_openxr_space(space.as_raw())
}
unsafe fn as_openxr_space<T>(&self, session: &openxr::Session<T>) -> openxr::Space {
unsafe { openxr::Space::reference_from_raw(session.clone(), self.as_raw_openxr_space()) }
}
unsafe fn into_openxr_space<T>(self, session: &openxr::Session<T>) -> openxr::Space {
OXR_DO_NOT_CALL_DESTOY_SPACE_FOR_SPACES
.lock()
.unwrap()
.as_mut()
.unwrap()
.remove(&self.as_raw());
unsafe { openxr::Space::reference_from_raw(session.clone(), self.as_raw_openxr_space()) }
}
}
fn cvt(x: sys::Result) -> openxr::Result<sys::Result> {
if x.into_raw() >= 0 {
Ok(x)
} else {
Err(x)
}
}
unsafe fn create_view(flags: openxr::ViewStateFlags, raw: &MaybeUninit<sys::View>) -> openxr::View {
// Applications *must* not read invalid parts of a poses, i.e. they may be uninitialized
let ptr = raw.as_ptr();
openxr::View {
pose: openxr::Posef {
orientation: flags
.contains(sys::ViewStateFlags::ORIENTATION_VALID)
.then(|| *ptr::addr_of!((*ptr).pose.orientation))
.unwrap_or_default(),
position: flags
.contains(sys::ViewStateFlags::POSITION_VALID)
.then(|| *ptr::addr_of!((*ptr).pose.position))
.unwrap_or_default(),
},
fov: *ptr::addr_of!((*ptr).fov),
}
}
unsafe fn create_space_location(raw: &MaybeUninit<sys::SpaceLocation>) -> openxr::SpaceLocation {
// Applications *must* not read invalid parts of a pose, i.e. they may be uninitialized
let ptr = raw.as_ptr();
let flags = *ptr::addr_of!((*ptr).location_flags);
openxr::SpaceLocation {
location_flags: flags,
pose: openxr::Posef {
orientation: flags
.contains(sys::SpaceLocationFlags::ORIENTATION_VALID)
.then(|| *ptr::addr_of!((*ptr).pose.orientation))
.unwrap_or_default(),
position: flags
.contains(sys::SpaceLocationFlags::POSITION_VALID)
.then(|| *ptr::addr_of!((*ptr).pose.position))
.unwrap_or_default(),
},
}
}
unsafe fn create_space_velocity(raw: &MaybeUninit<sys::SpaceVelocity>) -> openxr::SpaceVelocity {
// Applications *must* not read invalid velocities, i.e. they may be uninitialized
let ptr = raw.as_ptr();
let flags = *ptr::addr_of!((*ptr).velocity_flags);
openxr::SpaceVelocity {
velocity_flags: flags,
linear_velocity: flags
.contains(sys::SpaceVelocityFlags::LINEAR_VALID)
.then(|| *ptr::addr_of!((*ptr).linear_velocity))
.unwrap_or_default(),
angular_velocity: flags
.contains(sys::SpaceVelocityFlags::ANGULAR_VALID)
.then(|| *ptr::addr_of!((*ptr).angular_velocity))
.unwrap_or_default(),
}
}
fn get_arr_init<T: Copy>(
init: T,
mut getter: impl FnMut(u32, &mut u32, *mut T) -> sys::Result,
) -> openxr::Result<Vec<T>> {
let mut output = 0;
cvt(getter(0, &mut output, std::ptr::null_mut()))?;
let mut buffer = vec![init; output as usize];
loop {
match cvt(getter(output, &mut output, buffer.as_mut_ptr() as _)) {
Ok(_) => {
buffer.truncate(output as usize);
return Ok(buffer);
}
Err(sys::Result::ERROR_SIZE_INSUFFICIENT) => {
buffer.resize(output as usize, init);
}
Err(e) => {
return Err(e);
}
}
}
}

View File

@@ -0,0 +1,98 @@
use std::borrow::Cow;
use crate::error::OxrError;
use crate::graphics::{GraphicsExt, GraphicsType, GraphicsWrap};
pub use crate::openxr::exts::OxrExtensions;
pub use openxr::{EnvironmentBlendMode, SwapchainCreateFlags, SwapchainUsageFlags};
pub type Result<T> = std::result::Result<T, OxrError>;
/// A container for all required graphics objects needed for a bevy app.
pub struct WgpuGraphics(
pub wgpu::Device,
pub wgpu::Queue,
pub wgpu::AdapterInfo,
pub wgpu::Adapter,
pub wgpu::Instance,
);
/// A version number that can be stored inside of a u32
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub struct Version(pub u8, pub u8, pub u16);
impl Version {
/// Bevy's version number
pub const BEVY: Self = Self(0, 13, 0);
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
}
}
/// Info needed about an app for OpenXR
#[derive(Clone, Debug, PartialEq)]
pub struct AppInfo {
pub name: Cow<'static, str>,
pub version: Version,
}
impl AppInfo {
/// The default app info for a generic bevy app
pub const BEVY: Self = Self {
name: Cow::Borrowed("Bevy"),
version: Version::BEVY,
};
}
impl Default for AppInfo {
fn default() -> Self {
Self::BEVY
}
}
/// Info needed to create a swapchain.
/// This is an API agnostic version of [openxr::SwapchainCreateInfo] used for some of this library's functions
#[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 = OxrError;
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(OxrError::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,
})
}
}
/// Info needed to create a session. Mostly contains graphics info.
/// This is an API agnostic version of [openxr::Graphics::SessionCreateInfo] used for some of this library's functions
#[derive(Clone)]
pub struct SessionCreateInfo(pub GraphicsWrap<Self>);
impl GraphicsType for SessionCreateInfo {
type Inner<G: GraphicsExt> = G::SessionCreateInfo;
}

View File

@@ -0,0 +1,16 @@
[package]
name = "bevy_mod_webxr"
version = "0.1.0"
edition = "2021"
# bevy can't be placed behind target or proc macros won't work properly
[dependencies]
bevy.workspace = true
# all dependencies are placed under this since on anything but wasm, this crate is completely empty
[target.'cfg(target_family = "wasm")'.dependencies]
thiserror = "1.0.57"
wgpu = "0.19.3"
wgpu-hal = "0.19.3"
bevy_mod_xr.path = "../bevy_xr"

View File

@@ -0,0 +1,2 @@
# Bevy WebXR
currently not yet in a working state

View File

@@ -0,0 +1,5 @@
#[cfg(target_family = "wasm")]
mod webxr;
#[cfg(target_family = "wasm")]
pub use webxr::*;

View File

@@ -0,0 +1 @@

View File

@@ -0,0 +1,9 @@
[package]
name = "bevy_mod_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

View File

@@ -0,0 +1,107 @@
use std::{any::TypeId, marker::PhantomData};
use bevy::app::{App, Plugin};
use bevy::ecs::system::Resource;
use bevy::math::Vec2;
pub struct ActionPlugin<A: Action>(PhantomData<A>);
impl<A: Action> Default for ActionPlugin<A> {
fn default() -> Self {
Self(Default::default())
}
}
impl<A: Action> Plugin for ActionPlugin<A> {
fn build(&self, app: &mut App) {
app.init_resource::<ActionList>()
.init_resource::<ActionState<A>>();
app.world.resource_mut::<ActionList>().0.push(A::info());
}
}
#[derive(Debug, PartialEq, Eq, Hash, Clone, Copy)]
pub enum ActionType {
Bool,
Float,
Vector,
}
pub trait ActionTy: Send + Sync + Default + Clone + Copy {
const TYPE: ActionType;
}
impl ActionTy for bool {
const TYPE: ActionType = ActionType::Bool;
}
impl ActionTy for f32 {
const TYPE: ActionType = ActionType::Float;
}
impl ActionTy for Vec2 {
const TYPE: ActionType = ActionType::Float;
}
pub trait Action: Send + Sync + 'static {
type ActionType: ActionTy;
fn info() -> ActionInfo;
}
pub struct ActionInfo {
pub pretty_name: &'static str,
pub name: &'static str,
pub action_type: ActionType,
pub type_id: TypeId,
}
#[derive(Resource, Default)]
pub struct ActionList(pub Vec<ActionInfo>);
#[derive(Resource)]
pub struct ActionState<A: Action> {
previous_state: A::ActionType,
current_state: A::ActionType,
}
impl<A: Action> Default for ActionState<A> {
fn default() -> Self {
Self {
previous_state: Default::default(),
current_state: Default::default(),
}
}
}
impl<A: Action> ActionState<A> {
pub fn current_state(&self) -> A::ActionType {
self.current_state
}
pub fn previous_state(&self) -> A::ActionType {
self.previous_state
}
pub fn set(&mut self, state: A::ActionType) {
self.previous_state = std::mem::replace(&mut self.current_state, state);
}
}
impl<A: Action<ActionType = bool>> ActionState<A> {
pub fn pressed(&self) -> bool {
self.current_state
}
pub fn just_pressed(&self) -> bool {
self.previous_state == false && self.current_state == true
}
pub fn just_released(&self) -> bool {
self.previous_state == true && self.current_state == false
}
pub fn press(&mut self) {
self.current_state = true
}
}

View File

@@ -0,0 +1,134 @@
use bevy::app::{App, Plugin, PostUpdate};
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::ecs::schedule::IntoSystemConfigs;
use bevy::math::{Mat4, Vec3A};
use bevy::reflect::std_traits::ReflectDefault;
use bevy::reflect::Reflect;
use bevy::render::camera::{
Camera, CameraMainTextureUsages, CameraProjection, CameraProjectionPlugin, CameraRenderGraph,
Exposure,
};
use bevy::render::extract_component::{ExtractComponent, ExtractComponentPlugin};
use bevy::render::primitives::Frustum;
use bevy::render::view::{update_frusta, ColorGrading, VisibilitySystems, VisibleEntities};
use bevy::transform::components::{GlobalTransform, Transform};
use bevy::transform::TransformSystem;
pub struct XrCameraPlugin;
impl Plugin for XrCameraPlugin {
fn build(&self, app: &mut App) {
app.add_plugins(CameraProjectionPlugin::<XrProjection>::default());
app.add_systems(
PostUpdate,
update_frusta::<XrProjection>
.after(TransformSystem::TransformPropagate)
.before(VisibilitySystems::UpdatePerspectiveFrusta),
);
app.add_plugins((
ExtractComponentPlugin::<XrProjection>::default(),
ExtractComponentPlugin::<XrCamera>::default(),
));
}
}
#[derive(Debug, Clone, Component, Reflect, ExtractComponent)]
#[reflect(Component, Default)]
pub struct XrProjection {
pub projection_matrix: Mat4,
pub near: f32,
}
impl Default for XrProjection {
fn default() -> Self {
Self {
near: 0.1,
projection_matrix: Mat4::IDENTITY,
}
}
}
/// Marker component for an XR view. It is the backends responsibility to update this.
#[derive(Clone, Copy, Component, ExtractComponent, Debug, Default)]
pub struct XrCamera(pub u32);
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 {
let far = self.projection_matrix.to_cols_array()[14]
/ (self.projection_matrix.to_cols_array()[10] + 1.0);
far
}
// TODO calculate this properly
fn get_frustum_corners(&self, _z_near: f32, _z_far: f32) -> [Vec3A; 8] {
let ndc_corners = [
Vec3A::new(1.0, -1.0, 1.0), // Bottom-right far
Vec3A::new(1.0, 1.0, 1.0), // Top-right far
Vec3A::new(-1.0, 1.0, 1.0), // Top-left far
Vec3A::new(-1.0, -1.0, 1.0), // Bottom-left far
Vec3A::new(1.0, -1.0, -1.0), // Bottom-right near
Vec3A::new(1.0, 1.0, -1.0), // Top-right near
Vec3A::new(-1.0, 1.0, -1.0), // Top-left near
Vec3A::new(-1.0, -1.0, -1.0), // Bottom-left near
];
let mut view_space_corners = [Vec3A::ZERO; 8];
let inverse_matrix = self.projection_matrix.inverse();
for (i, corner) in ndc_corners.into_iter().enumerate() {
view_space_corners[i] = inverse_matrix.transform_point3a(corner);
}
view_space_corners
}
}
#[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: XrCamera,
}
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: XrCamera(0),
}
}
}

143
crates/bevy_xr/src/hands.rs Normal file
View File

@@ -0,0 +1,143 @@
use bevy::{
ecs::{component::Component, entity::Entity},
math::bool,
prelude::{Deref, DerefMut},
};
pub const HAND_JOINT_COUNT: usize = 26;
#[derive(Clone, Copy, Component, Debug)]
pub struct LeftHand;
#[derive(Clone, Copy, Component, Debug)]
pub struct RightHand;
#[derive(Deref, DerefMut, Component, Clone, Copy)]
pub struct XrHandBoneEntities(pub [Entity; HAND_JOINT_COUNT]);
#[repr(transparent)]
#[derive(Clone, Copy, Component, Debug, DerefMut, Deref)]
pub struct HandBoneRadius(pub f32);
#[repr(u8)]
#[derive(Clone, Copy, Component, Debug)]
pub enum HandBone {
Palm = 0,
Wrist = 1,
ThumbMetacarpal = 2,
ThumbProximal = 3,
ThumbDistal = 4,
ThumbTip = 5,
IndexMetacarpal = 6,
IndexProximal = 7,
IndexIntermediate = 8,
IndexDistal = 9,
IndexTip = 10,
MiddleMetacarpal = 11,
MiddleProximal = 12,
MiddleIntermediate = 13,
MiddleDistal = 14,
MiddleTip = 15,
RingMetacarpal = 16,
RingProximal = 17,
RingIntermediate = 18,
RingDistal = 19,
RingTip = 20,
LittleMetacarpal = 21,
LittleProximal = 22,
LittleIntermediate = 23,
LittleDistal = 24,
LittleTip = 25,
}
impl HandBone {
pub const fn is_metacarpal(&self) -> bool {
matches!(
self,
HandBone::ThumbMetacarpal
| HandBone::IndexMetacarpal
| HandBone::MiddleMetacarpal
| HandBone::RingMetacarpal
| HandBone::LittleMetacarpal
)
}
pub const fn is_thumb(&self) -> bool {
matches!(
self,
HandBone::ThumbMetacarpal
| HandBone::ThumbProximal
| HandBone::ThumbDistal
| HandBone::ThumbTip
)
}
pub const fn is_index(&self) -> bool {
matches!(
self,
HandBone::IndexMetacarpal
| HandBone::IndexProximal
| HandBone::IndexIntermediate
| HandBone::IndexDistal
| HandBone::IndexTip
)
}
pub const fn is_middle(&self) -> bool {
matches!(
self,
HandBone::MiddleMetacarpal
| HandBone::MiddleProximal
| HandBone::MiddleIntermediate
| HandBone::MiddleDistal
| HandBone::MiddleTip
)
}
pub const fn is_ring(&self) -> bool {
matches!(
self,
HandBone::RingMetacarpal
| HandBone::RingProximal
| HandBone::RingIntermediate
| HandBone::RingDistal
| HandBone::RingTip
)
}
pub const fn is_little(&self) -> bool {
matches!(
self,
HandBone::LittleMetacarpal
| HandBone::LittleProximal
| HandBone::LittleIntermediate
| HandBone::LittleDistal
| HandBone::LittleTip
)
}
pub const fn get_all_bones() -> [HandBone; 26] {
[
HandBone::Palm,
HandBone::Wrist,
HandBone::ThumbMetacarpal,
HandBone::ThumbProximal,
HandBone::ThumbDistal,
HandBone::ThumbTip,
HandBone::IndexMetacarpal,
HandBone::IndexProximal,
HandBone::IndexIntermediate,
HandBone::IndexDistal,
HandBone::IndexTip,
HandBone::MiddleMetacarpal,
HandBone::MiddleProximal,
HandBone::MiddleIntermediate,
HandBone::MiddleDistal,
HandBone::MiddleTip,
HandBone::RingMetacarpal,
HandBone::RingProximal,
HandBone::RingIntermediate,
HandBone::RingDistal,
HandBone::RingTip,
HandBone::LittleMetacarpal,
HandBone::LittleProximal,
HandBone::LittleIntermediate,
HandBone::LittleDistal,
HandBone::LittleTip,
]
}
}

View File

@@ -0,0 +1,6 @@
pub mod actions;
pub mod camera;
pub mod hands;
pub mod session;
pub mod types;
pub mod spaces;

View File

@@ -0,0 +1,338 @@
use bevy::app::{AppExit, MainScheduleOrder};
use bevy::ecs::schedule::ScheduleLabel;
use bevy::prelude::*;
use bevy::render::extract_resource::{ExtractResource, ExtractResourcePlugin};
use bevy::render::{Render, RenderApp, RenderSet};
/// Event sent to instruct backends to create an XR session. Only works when the [`XrState`] is [`Available`](XrState::Available).
#[derive(Event, Clone, Copy, Default)]
pub struct XrCreateSessionEvent;
/// A schedule thats ran whenever an [`XrCreateSessionEvent`] is recieved while the [`XrState`] is [`Available`](XrState::Available)
#[derive(Clone, Copy, Default, PartialEq, Eq, Debug, Hash, ScheduleLabel)]
pub struct XrCreateSession;
/// Event sent when [`XrCreateSession`] is ran
#[derive(Event, Clone, Copy, Default)]
pub struct XrSessionCreatedEvent;
/// Event sent to instruct backends to destroy an XR session. Only works when the [`XrState`] is [`Exiting`](XrState::Exiting).
/// If you would like to request that a running session be destroyed, send the [`XrRequestExitEvent`] instead.
#[derive(Event, Clone, Copy, Default)]
pub struct XrDestroySessionEvent;
/// Resource flag thats inserted into the world and extracted to the render world to inform any session resources in the render world to drop.
#[derive(Resource, ExtractResource, Clone, Copy, Default)]
pub struct XrDestroySessionRender;
/// Schedule thats ran whenever an [`XrDestroySessionEvent`] is recieved while the [`XrState`] is [`Exiting`](XrState::Exiting).
#[derive(Clone, Copy, Default, PartialEq, Eq, Debug, Hash, ScheduleLabel)]
pub struct XrDestroySession;
/// Event sent to instruct backends to begin an XR session. Only works when the [`XrState`] is [`Ready`](XrState::Ready).
#[derive(Event, Clone, Copy, Default)]
pub struct XrBeginSessionEvent;
/// Schedule thats ran whenever an [`XrBeginSessionEvent`] is recieved while the [`XrState`] is [`Ready`](XrState::Ready).
#[derive(Clone, Copy, Default, PartialEq, Eq, Debug, Hash, ScheduleLabel)]
pub struct XrBeginSession;
/// Event sent to backends to end an XR session. Only works when the [`XrState`] is [`Stopping`](XrState::Stopping).
#[derive(Event, Clone, Copy, Default)]
pub struct XrEndSessionEvent;
/// Schedule thats rna whenever an [`XrEndSessionEvent`] is recieved while the [`XrState`] is [`Stopping`](XrState::Stopping).
#[derive(Clone, Copy, Default, PartialEq, Eq, Debug, Hash, ScheduleLabel)]
pub struct XrEndSession;
/// Event sent to backends to request the [`XrState`] proceed to [`Exiting`](XrState::Exiting) and for the session to be exited. Can be called at any time a session exists.
#[derive(Event, Clone, Copy, Default)]
pub struct XrRequestExitEvent;
#[derive(Clone, Copy, Default, PartialEq, Eq, Debug, Hash, ScheduleLabel)]
pub struct XrRequestExit;
/// Schedule ran before [`First`] to handle XR events.
#[derive(Clone, Copy, Default, PartialEq, Eq, Debug, Hash, ScheduleLabel)]
pub struct XrFirst;
/// System set for systems related to handling XR session events and updating the [`XrState`]
#[derive(Debug, Hash, PartialEq, Eq, Clone, Copy, SystemSet)]
pub struct XrHandleEvents;
/// System sets ran in the render world for XR.
#[derive(Debug, Hash, PartialEq, Eq, Clone, Copy, SystemSet)]
pub enum XrRenderSet {
/// Ran before [`XrRenderSet::PreRender`] but after [`RenderSet::ExtractCommands`].
HandleEvents,
/// For any XR systems needing to be ran before rendering begins.
/// Ran after [`XrRenderSet::HandleEvents`] but before every render set except [`RenderSet::ExtractCommands`].
PreRender,
/// For any XR systems needing to be ran after [`RenderSet::Render`] but before [`RenderSet::Cleanup`].
PostRender,
}
/// The root transform's global position for late latching in the render world.
#[derive(ExtractResource, Resource, Clone, Copy, Default)]
pub struct XrRootTransform(pub GlobalTransform);
/// Component used to specify the entity we should use as the tracking root.
#[derive(Component)]
pub struct XrTrackingRoot;
pub struct XrSessionPlugin {
pub auto_handle: bool,
}
impl Plugin for XrSessionPlugin {
fn build(&self, app: &mut App) {
let mut xr_first = Schedule::new(XrFirst);
xr_first.set_executor_kind(bevy::ecs::schedule::ExecutorKind::Simple);
app.add_event::<XrCreateSessionEvent>()
.add_event::<XrDestroySessionEvent>()
.add_event::<XrBeginSessionEvent>()
.add_event::<XrEndSessionEvent>()
.add_event::<XrRequestExitEvent>()
.add_event::<XrStateChanged>()
.add_event::<XrSessionCreatedEvent>()
.init_schedule(XrCreateSession)
.init_schedule(XrDestroySession)
.init_schedule(XrBeginSession)
.init_schedule(XrEndSession)
.init_schedule(XrRequestExit)
.add_schedule(xr_first)
.add_systems(
XrFirst,
(
exits_session_on_app_exit
.run_if(on_event::<AppExit>())
.run_if(session_created),
reset_per_frame_resources,
run_xr_create_session
.run_if(state_equals(XrState::Available))
.run_if(on_event::<XrCreateSessionEvent>()),
run_xr_destroy_session
.run_if(state_matches!(XrState::Exiting { .. }))
.run_if(on_event::<XrDestroySessionEvent>()),
run_xr_begin_session
.run_if(state_equals(XrState::Ready))
.run_if(on_event::<XrBeginSessionEvent>()),
run_xr_end_session
.run_if(state_equals(XrState::Stopping))
.run_if(on_event::<XrEndSessionEvent>()),
run_xr_request_exit
.run_if(session_created)
.run_if(on_event::<XrRequestExitEvent>()),
)
.chain()
.in_set(XrHandleEvents),
);
app.world
.resource_mut::<MainScheduleOrder>()
.labels
.insert(0, XrFirst.intern());
if self.auto_handle {
app.add_systems(PreUpdate, auto_handle_session);
}
}
fn finish(&self, app: &mut App) {
if app.get_sub_app(RenderApp).is_err() {
return;
}
app.add_plugins((
ExtractResourcePlugin::<XrState>::default(),
ExtractResourcePlugin::<XrDestroySessionRender>::default(),
ExtractResourcePlugin::<XrRootTransform>::default(),
))
.init_resource::<XrRootTransform>()
.add_systems(
PostUpdate,
update_root_transform.after(TransformSystem::TransformPropagate),
)
.add_systems(
XrFirst,
exits_session_on_app_exit
.before(XrHandleEvents)
.run_if(on_event::<AppExit>().and_then(session_running)),
);
let render_app = app.sub_app_mut(RenderApp);
render_app
.init_schedule(XrDestroySession)
.init_resource::<XrRootTransform>()
.configure_sets(
Render,
(XrRenderSet::HandleEvents, XrRenderSet::PreRender).chain(),
)
.configure_sets(
Render,
XrRenderSet::HandleEvents.after(RenderSet::ExtractCommands),
)
.configure_sets(
Render,
XrRenderSet::PreRender
.before(RenderSet::ManageViews)
.before(RenderSet::PrepareAssets),
)
.configure_sets(
Render,
XrRenderSet::PostRender
.after(RenderSet::Render)
.before(RenderSet::Cleanup),
)
.add_systems(
Render,
(
run_xr_destroy_session
.run_if(resource_exists::<XrDestroySessionRender>)
.in_set(XrRenderSet::HandleEvents),
reset_per_frame_resources.in_set(RenderSet::Cleanup),
),
);
}
}
fn exits_session_on_app_exit(mut request_exit: EventWriter<XrRequestExitEvent>) {
request_exit.send_default();
}
/// Event sent by backends whenever [`XrState`] is changed.
#[derive(Event, Clone, Copy, Deref)]
pub struct XrStateChanged(pub XrState);
/// A resource in the main world and render world representing the current session state.
#[derive(Clone, Copy, Debug, ExtractResource, Resource, PartialEq, Eq)]
#[repr(u8)]
pub enum XrState {
/// An XR session is not available here
Unavailable,
/// An XR session is available and ready to be created with an [`XrCreateSessionEvent`].
Available,
/// An XR session is created but not ready to begin. Backends are not required to use this state.
Idle,
/// An XR session has been created and is ready to start rendering with an [`XrBeginSessionEvent`].
Ready,
/// The XR session is running and can be stopped with an [`XrEndSessionEvent`].
Running,
/// The runtime has requested that the session should be ended with an [`XrEndSessionEvent`].
Stopping,
/// The XR session should be destroyed with an [`XrDestroySessionEvent`].
Exiting {
/// Whether we should automatically restart the session
should_restart: bool,
},
}
pub fn run_xr_create_session(world: &mut World) {
world.run_schedule(XrCreateSession);
world.send_event(XrSessionCreatedEvent);
}
pub fn run_xr_destroy_session(world: &mut World) {
world.run_schedule(XrDestroySession);
world.insert_resource(XrDestroySessionRender);
}
pub fn run_xr_begin_session(world: &mut World) {
world.run_schedule(XrBeginSession);
}
pub fn run_xr_end_session(world: &mut World) {
world.run_schedule(XrEndSession);
}
pub fn run_xr_request_exit(world: &mut World) {
world.run_schedule(XrRequestExit);
}
pub fn reset_per_frame_resources(world: &mut World) {
world.remove_resource::<XrDestroySessionRender>();
}
pub fn auto_handle_session(
mut state_changed: EventReader<XrStateChanged>,
mut create_session: EventWriter<XrCreateSessionEvent>,
mut begin_session: EventWriter<XrBeginSessionEvent>,
mut end_session: EventWriter<XrEndSessionEvent>,
mut destroy_session: EventWriter<XrDestroySessionEvent>,
) {
for XrStateChanged(state) in state_changed.read() {
match state {
XrState::Available => {
create_session.send_default();
}
XrState::Ready => {
begin_session.send_default();
}
XrState::Stopping => {
end_session.send_default();
}
XrState::Exiting { .. } => {
destroy_session.send_default();
}
_ => (),
}
}
}
pub fn update_root_transform(
mut root_transform: ResMut<XrRootTransform>,
root: Query<&GlobalTransform, With<XrTrackingRoot>>,
) {
let Ok(transform) = root.get_single() else {
return;
};
root_transform.0 = *transform;
}
/// A [`Condition`](bevy::ecs::schedule::Condition) that allows the system to run when the xr status changed to a specific [`XrStatus`].
pub fn status_changed_to(
status: XrState,
) -> impl FnMut(EventReader<XrStateChanged>) -> bool + Clone {
move |mut reader: EventReader<XrStateChanged>| {
reader.read().any(|new_status| new_status.0 == status)
}
}
/// A [`Condition`](bevy::ecs::schedule::Condition) system that says if the XR session is available. Returns true as long as [`XrState`] exists and isn't [`Unavailable`](XrStatus::Unavailable).
pub fn session_available(status: Option<Res<XrState>>) -> bool {
status.is_some_and(|s| *s != XrState::Unavailable)
}
pub fn session_created(status: Option<Res<XrState>>) -> bool {
!matches!(
status.as_deref(),
Some(XrState::Unavailable | XrState::Available) | None
)
}
/// A [`Condition`](bevy::ecs::schedule::Condition) system that says if the XR session is ready or running
pub fn session_ready_or_running(status: Option<Res<XrState>>) -> bool {
matches!(status.as_deref(), Some(XrState::Ready | XrState::Running))
}
/// A [`Condition`](bevy::ecs::schedule::Condition) system that says if the XR session is running
pub fn session_running(status: Option<Res<XrState>>) -> bool {
matches!(status.as_deref(), Some(XrState::Running))
}
/// A function that returns a [`Condition`](bevy::ecs::schedule::Condition) system that says if the [`XrState`] is in a specific state
pub fn state_equals(status: XrState) -> impl FnMut(Option<Res<XrState>>) -> bool {
move |state: Option<Res<XrState>>| state.is_some_and(|s| *s == status)
}
#[macro_export]
macro_rules! state_matches {
($match:pat) => {
(|state: Option<Res<XrState>>| core::matches!(state.as_deref(), Some($match)))
};
}
use bevy::transform::TransformSystem;
pub use state_matches;

View File

@@ -0,0 +1,44 @@
use bevy::{
prelude::*,
render::{extract_component::ExtractComponent, extract_resource::ExtractResource},
};
use crate::types::XrPose;
/// Any Spaces will be invalid after the owning session exits
#[repr(transparent)]
#[derive(Clone, Copy, Hash, PartialEq, Eq, Reflect, Debug, Component, ExtractComponent)]
pub struct XrSpace(u64);
// Does repr(transparent) even make sense here?
#[repr(transparent)]
#[derive(
Clone, Copy, PartialEq, Reflect, Debug, Component, ExtractComponent, Default, Deref, DerefMut,
)]
pub struct XrSpatialOffset(pub XrPose);
#[derive(Event, Clone, Copy, Deref, DerefMut)]
pub struct XrDestroySpace(pub XrSpace);
#[repr(transparent)]
#[derive(
Clone, Copy, Hash, PartialEq, Eq, Reflect, Debug, Component, Deref, DerefMut, ExtractComponent,
)]
pub struct XrReferenceSpace(pub XrSpace);
#[repr(transparent)]
#[derive(
Clone, Copy, Hash, PartialEq, Eq, Reflect, Debug, Resource, Deref, DerefMut, ExtractResource,
)]
pub struct XrPrimaryReferenceSpace(pub XrReferenceSpace);
impl XrSpace {
/// # Safety
/// only call with known valid handles
pub unsafe fn from_raw(handle: u64) -> Self {
Self(handle)
}
pub fn as_raw(&self) -> u64 {
self.0
}
}

View File

@@ -0,0 +1,26 @@
use bevy::{
math::{Quat, Vec3},
reflect::Reflect,
transform::components::Transform,
};
#[derive(Clone, Copy, PartialEq, Reflect, Debug)]
pub struct XrPose {
pub translation: Vec3,
pub rotation: Quat,
}
impl Default for XrPose {
fn default() -> Self {
Self::IDENTITY
}
}
impl XrPose {
pub const IDENTITY: XrPose = XrPose {
translation: Vec3::ZERO,
rotation: Quat::IDENTITY,
};
pub const fn to_transform(self) -> Transform {
Transform::from_translation(self.translation).with_rotation(self.rotation)
}
}

View File

@@ -0,0 +1,14 @@
[package]
name = "bevy_xr_utils"
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
bevy_mod_xr.path = "../bevy_xr"
bevy_mod_openxr.path = "../bevy_openxr"
[target.'cfg(not(target_family = "wasm"))'.dependencies]
openxr = "0.18.0"

View File

@@ -0,0 +1,34 @@
use bevy::{prelude::*, transform::TransformSystem};
use bevy_mod_xr::hands::{HandBone, HandBoneRadius};
pub struct HandGizmosPlugin;
impl Plugin for HandGizmosPlugin {
fn build(&self, app: &mut App) {
app.add_systems(
PostUpdate,
draw_hand_gizmos.after(TransformSystem::TransformPropagate),
);
}
}
fn draw_hand_gizmos(
mut gizmos: Gizmos,
query: Query<(&GlobalTransform, &HandBone, &HandBoneRadius)>,
) {
for (transform, bone, radius) in &query {
let pose = transform.compute_transform();
gizmos.sphere(pose.translation, pose.rotation, **radius, gizmo_color(bone));
}
}
fn gizmo_color(bone: &HandBone) -> Color {
match bone {
HandBone::Palm => Color::WHITE,
HandBone::Wrist => Color::GRAY,
b if b.is_thumb() => Color::RED,
b if b.is_index() => Color::ORANGE,
b if b.is_middle() => Color::YELLOW,
b if b.is_ring() => Color::GREEN,
b if b.is_little() => Color::BLUE,
// should be impossible to hit
_ => Color::rgb(1.0, 0.0, 1.0),
}
}

View File

@@ -0,0 +1,6 @@
// use bevy::prelude::*;
pub mod hand_gizmos;
#[cfg(not(target_family = "wasm"))]
pub mod xr_utils_actions;
#[cfg(not(target_family = "wasm"))]
pub mod transform_utils;

View File

@@ -0,0 +1,73 @@
use bevy::prelude::*;
use bevy_mod_openxr::{
helper_traits::{ToQuat, ToVec3},
resources::OxrViews,
};
use bevy_mod_xr::session::XrTrackingRoot;
pub struct TransformUtilitiesPlugin;
impl Plugin for TransformUtilitiesPlugin {
fn build(&self, app: &mut App) {
app.add_event::<SnapToRotation>();
app.add_event::<SnapToPosition>();
app.add_systems(PostUpdate, handle_transform_events);
}
}
//events
#[derive(Event, Debug)]
pub struct SnapToRotation(pub Quat);
#[derive(Event, Debug)]
pub struct SnapToPosition(pub Vec3);
pub fn handle_transform_events(
mut root_query: Query<&mut Transform, With<XrTrackingRoot>>,
views: ResMut<OxrViews>,
mut position_reader: EventReader<SnapToPosition>,
mut rotation_reader: EventReader<SnapToRotation>,
) {
let result = root_query.get_single_mut();
match result {
Ok(mut root_transform) => {
let view = views.first();
match view {
Some(view) => {
//we want the view translation with a height of zero for a few calculations
let mut view_translation = view.pose.position.to_vec3();
view_translation.y = 0.0;
//position
for position in position_reader.read() {
root_transform.translation =
position.0 - root_transform.rotation.mul_vec3(view_translation);
}
//rotation
let root_local = root_transform.translation.clone();
let hmd_global =
root_transform.rotation.mul_vec3(view_translation) + root_local;
let view_rot = view.pose.orientation.to_quat();
let root_rot = root_transform.rotation;
let view_global_rotation = root_rot.mul_quat(view_rot).normalize();
let (global_view_yaw, _pitch, _roll) =
view_global_rotation.to_euler(bevy::math::EulerRot::YXZ);
let up = Vec3::Y;
for rotation in rotation_reader.read() {
let (target_yaw, _pitch, _roll) =
rotation.0.normalize().to_euler(bevy::math::EulerRot::YXZ);
let diff_yaw = target_yaw - global_view_yaw;
//build a rotation quat?
let rotation_quat = Quat::from_axis_angle(up, diff_yaw);
//apply rotation this works
root_transform.rotate_around(hmd_global, rotation_quat);
}
}
None => debug!("error getting first view"),
}
}
Err(_) => debug!("error getting root transform"),
}
}

View File

@@ -0,0 +1,426 @@
//! This plugin and module are here to ease the creation of actions withing openxr
//! The general idea is any plugin can create entities in startup before XRUtilsActionSystemSet::CreateEvents
//! this plugin will then create the neccessary actions sets, actions, and bindings and get them ready for use.
//!
//! example creating actions
//!
//! //create a set
//! let set = commands
//! .spawn((
//! XRUtilsActionSet {
//! name: "flight".into(),
//! pretty_name: "pretty flight set".into(),
//! priority: u32::MIN,
//! },
//! ActiveSet, //marker to indicate we want this synced
//! ))
//! .id();
//! //create an action
//! let action = commands
//! .spawn((
//! XRUtilsAction {
//! action_name: "flight_input".into(),
//! localized_name: "flight_input_localized".into(),
//! action_type: bevy_mod_xr::actions::ActionType::Vector,
//! },
//! FlightActionMarker, //lets try a marker component
//! ))
//! .id();
//!
//! //create a binding
//! let binding = commands
//! .spawn(XRUtilsBinding {
//! profile: "/interaction_profiles/valve/index_controller".into(),
//! binding: "/user/hand/right/input/thumbstick".into(),
//! })
//! .id();
//!
//! //add action to set, this isnt the best
//! //TODO look into a better system
//! commands.entity(action).add_child(binding);
//! commands.entity(set).add_child(action);
//!
//! then you can read the action states after XRUtilsActionSystemSet::SyncActionStates
//! for example
//!
//! fn read_action_with_marker_component(
//! mut action_query: Query<&XRUtilsActionState, With<FlightActionMarker>>,
//! ) {
//! //now for the actual checking
//! for state in action_query.iter_mut() {
//! info!("action state is: {:?}", state);
//! }
//! }
//!
//!
use bevy::prelude::*;
use bevy_mod_openxr::{
action_binding::OxrSuggestActionBinding, action_set_attaching::OxrAttachActionSet,
action_set_syncing::OxrActionSetSyncSet, action_set_syncing::OxrSyncActionSet,
resources::OxrInstance, session::OxrSession,
};
use bevy_mod_xr::session::{session_available, session_running};
use openxr::{Path, Vector2f};
use std::borrow::Cow;
pub struct XRUtilsActionsPlugin;
impl Plugin for XRUtilsActionsPlugin {
fn build(&self, app: &mut App) {
app.configure_sets(
Startup,
XRUtilsActionSystemSet::CreateEvents.run_if(session_available),
);
app.configure_sets(
PreUpdate,
XRUtilsActionSystemSet::SyncActionStates.run_if(session_running),
);
app.add_systems(
Startup,
create_openxr_events
.in_set(XRUtilsActionSystemSet::CreateEvents)
.run_if(session_available),
);
app.add_systems(Update, sync_active_action_sets.run_if(session_running));
app.add_systems(
PreUpdate,
sync_and_update_action_states_f32
.run_if(session_running)
.in_set(XRUtilsActionSystemSet::SyncActionStates)
.after(OxrActionSetSyncSet),
);
app.add_systems(
PreUpdate,
sync_and_update_action_states_bool
.run_if(session_running)
.in_set(XRUtilsActionSystemSet::SyncActionStates)
.after(OxrActionSetSyncSet),
);
app.add_systems(
PreUpdate,
sync_and_update_action_states_vector
.run_if(session_running)
.in_set(XRUtilsActionSystemSet::SyncActionStates)
.after(OxrActionSetSyncSet),
);
}
}
fn create_openxr_events(
action_sets_query: Query<(&XRUtilsActionSet, &Children, Entity)>,
actions_query: Query<(&XRUtilsAction, &Children)>,
bindings_query: Query<&XRUtilsBinding>,
instance: ResMut<OxrInstance>,
mut binding_writer: EventWriter<OxrSuggestActionBinding>,
mut attach_writer: EventWriter<OxrAttachActionSet>,
mut commands: Commands,
) {
//lets create some sets!
for (set, children, id) in action_sets_query.iter() {
//create action set
let action_set: openxr::ActionSet = instance
.create_action_set(&set.name, &set.pretty_name, set.priority)
.unwrap();
//now that we have the action set we need to put it back onto the entity for later
let oxr_action_set = XRUtilsActionSetReference(action_set.clone());
commands.entity(id).insert(oxr_action_set);
//since the actions are made from the sets lets go
for &child in children.iter() {
//first get the action entity and stuff
let (create_action, bindings) = actions_query.get(child).unwrap();
//lets create dat action
match create_action.action_type {
bevy_mod_xr::actions::ActionType::Bool => {
let action: openxr::Action<bool> = action_set
.create_action::<bool>(
&create_action.action_name,
&create_action.localized_name,
&[],
)
.unwrap();
//please put this in a function so I dont go crazy
//insert a reference for later
commands.entity(child).insert((
ActionBooleference {
action: action.clone(),
},
XRUtilsActionState::Bool(ActionStateBool {
current_state: false,
changed_since_last_sync: false,
last_change_time: i64::MIN,
is_active: false,
}),
));
//since we need actions for bindings lets go!!
for &bind in bindings.iter() {
//interaction profile
//get the binding entity and stuff
let create_binding = bindings_query.get(bind).unwrap();
let profile = create_binding.profile.clone();
//bindings
let binding = vec![create_binding.binding.clone()];
let sugestion = OxrSuggestActionBinding {
action: action.as_raw(),
interaction_profile: profile,
bindings: binding,
};
//finally send the suggestion
binding_writer.send(sugestion);
}
}
bevy_mod_xr::actions::ActionType::Float => {
let action: openxr::Action<f32> = action_set
.create_action::<f32>(
&create_action.action_name,
&create_action.localized_name,
&[],
)
.unwrap();
//please put this in a function so I dont go crazy
//insert a reference for later
commands.entity(child).insert((
Actionf32Reference {
action: action.clone(),
},
XRUtilsActionState::Float(ActionStateFloat {
current_state: 0.0,
changed_since_last_sync: false,
last_change_time: i64::MIN,
is_active: false,
}),
));
//since we need actions for bindings lets go!!
for &bind in bindings.iter() {
//interaction profile
//get the binding entity and stuff
let create_binding = bindings_query.get(bind).unwrap();
let profile = create_binding.profile.clone();
//bindings
let binding = vec![create_binding.binding.clone()];
let sugestion = OxrSuggestActionBinding {
action: action.as_raw(),
interaction_profile: profile,
bindings: binding,
};
//finally send the suggestion
binding_writer.send(sugestion);
}
}
bevy_mod_xr::actions::ActionType::Vector => {
let action: openxr::Action<Vector2f> = action_set
.create_action::<Vector2f>(
&create_action.action_name,
&create_action.localized_name,
&[],
)
.unwrap();
//please put this in a function so I dont go crazy
//insert a reference for later
commands.entity(child).insert((
ActionVector2fReference {
action: action.clone(),
},
XRUtilsActionState::Vector(ActionStateVector {
current_state: [0.0, 0.0],
changed_since_last_sync: false,
last_change_time: i64::MIN,
is_active: false,
}),
));
//since we need actions for bindings lets go!!
for &bind in bindings.iter() {
//interaction profile
//get the binding entity and stuff
let create_binding = bindings_query.get(bind).unwrap();
let profile = create_binding.profile.clone();
//bindings
let binding = vec![create_binding.binding.clone()];
let sugestion = OxrSuggestActionBinding {
action: action.as_raw(),
interaction_profile: profile,
bindings: binding,
};
//finally send the suggestion
binding_writer.send(sugestion);
}
}
};
}
attach_writer.send(OxrAttachActionSet(action_set));
}
}
fn sync_active_action_sets(
mut sync_set: EventWriter<OxrSyncActionSet>,
active_action_set_query: Query<&XRUtilsActionSetReference, With<ActiveSet>>,
) {
for set in &active_action_set_query {
sync_set.send(OxrSyncActionSet(set.0.clone()));
}
}
fn sync_and_update_action_states_f32(
session: Res<OxrSession>,
mut f32_query: Query<(&Actionf32Reference, &mut XRUtilsActionState)>,
) {
//now we do the action state for f32
for (reference, mut silly_state) in f32_query.iter_mut() {
let state = reference.action.state(&session, Path::NULL);
match state {
Ok(s) => {
let new_state = XRUtilsActionState::Float(ActionStateFloat {
current_state: s.current_state,
changed_since_last_sync: s.changed_since_last_sync,
last_change_time: s.last_change_time.as_nanos(),
is_active: s.is_active,
});
*silly_state = new_state;
}
Err(_) => {
info!("error getting action state");
}
}
}
}
fn sync_and_update_action_states_bool(
session: Res<OxrSession>,
mut f32_query: Query<(&ActionBooleference, &mut XRUtilsActionState)>,
) {
//now we do the action state for f32
for (reference, mut silly_state) in f32_query.iter_mut() {
let state = reference.action.state(&session, Path::NULL);
match state {
Ok(s) => {
let new_state = XRUtilsActionState::Bool(ActionStateBool {
current_state: s.current_state,
changed_since_last_sync: s.changed_since_last_sync,
last_change_time: s.last_change_time.as_nanos(),
is_active: s.is_active,
});
*silly_state = new_state;
}
Err(_) => {
info!("error getting action state");
}
}
}
}
fn sync_and_update_action_states_vector(
session: Res<OxrSession>,
mut vector_query: Query<(&ActionVector2fReference, &mut XRUtilsActionState)>,
) {
//now we do the action state for f32
for (reference, mut silly_state) in vector_query.iter_mut() {
let state = reference.action.state(&session, Path::NULL);
match state {
Ok(s) => {
let new_state = XRUtilsActionState::Vector(ActionStateVector {
current_state: [s.current_state.x, s.current_state.y],
changed_since_last_sync: s.changed_since_last_sync,
last_change_time: s.last_change_time.as_nanos(),
is_active: s.is_active,
});
*silly_state = new_state;
}
Err(_) => {
info!("error getting action state");
}
}
}
}
#[derive(Debug, Hash, PartialEq, Eq, Clone, Copy, SystemSet)]
pub enum XRUtilsActionSystemSet {
/// Runs in Startup
CreateEvents,
/// Runs in PreUpdate
SyncActionStates,
}
#[derive(Component)]
pub struct XRUtilsActionSet {
pub name: Cow<'static, str>,
pub pretty_name: Cow<'static, str>,
pub priority: u32,
}
#[derive(Component, Clone)]
pub struct XRUtilsActionSetReference(pub openxr::ActionSet);
//I want to use this to indicate when an action set is attached
// #[derive(Component)]
// struct AttachedActionSet;
//this is used to determine if this set should be synced
#[derive(Component)]
pub struct ActiveSet;
#[derive(Component)]
pub struct XRUtilsAction {
pub action_name: Cow<'static, str>,
pub localized_name: Cow<'static, str>,
pub action_type: bevy_mod_xr::actions::ActionType,
}
#[derive(Component)]
pub struct XRUtilsBinding {
pub profile: Cow<'static, str>,
pub binding: Cow<'static, str>,
}
//Prototype action states
//TODO refactor this
#[derive(Component, Debug)]
pub enum XRUtilsActionState {
Bool(ActionStateBool),
Float(ActionStateFloat),
Vector(ActionStateVector),
}
#[derive(Debug)]
pub struct ActionStateBool {
pub current_state: bool,
pub changed_since_last_sync: bool,
pub last_change_time: i64,
pub is_active: bool,
}
#[derive(Debug)]
pub struct ActionStateFloat {
pub current_state: f32,
pub changed_since_last_sync: bool,
pub last_change_time: i64,
pub is_active: bool,
}
#[derive(Debug)]
pub struct ActionStateVector {
pub current_state: [f32; 2],
pub changed_since_last_sync: bool,
pub last_change_time: i64,
pub is_active: bool,
}
//prototype action references
//TODO refactor along with action states
#[derive(Component)]
struct Actionf32Reference {
action: openxr::Action<f32>,
}
#[derive(Component)]
struct ActionBooleference {
action: openxr::Action<bool>,
}
#[derive(Component)]
struct ActionVector2fReference {
action: openxr::Action<Vector2f>,
}

View File

@@ -1,3 +0,0 @@
/target
/Cargo.lock
/runtime_libs

View File

@@ -1,67 +0,0 @@
# Bevy OpenXR Android example
## Setup
Get libopenxr_loader.so from the Oculus OpenXR Mobile SDK and add it to `examples/android/runtime_libs/arm64-v8a`
https://developer.oculus.com/downloads/package/oculus-openxr-mobile-sdk/
`examples/android/runtime_libs/arm64-v8a/libopenxr_loader.so`
Also, install either `cargo-apk` (marked as deprecated):
```sh
cargo install cargo-apk
```
or, install `xbuild` as it supersedes `cargo-apk`. Note that the `--git` is
very important here.
```sh
cargo install --git https://github.com/rust-mobile/xbuild
```
## Run
Running on Meta Quest can be done with https://github.com/rust-mobile/cargo-apk.
```sh
cargo apk run --release
```
But cargo-apk is deprecated in favour of xbuild https://github.com/rust-mobile/xbuild.
```sh
# List devices and copy device string "adb:***"
x devices
# Run on this device
x run --release --device adb:***
```
There is [manifest.yaml](./manifest.yaml) example required by xbuild.
Interface for this manifest can be found as AndroidConfig struct in https://github.com/rust-mobile/xbuild/blob/master/xbuild/src/config.rs
## Notes
### Relase mode
More optimisations enabled in Cargo.toml for the release mode.
This gives more performance but longer build time.
```toml
[profile.release]
lto = "fat"
codegen-units = 1
panic = "abort"
```
### Cargo apk
If you see error like `Error: String `` is not a PID`, try to install cargo apk with a fix in branch.
```sh
cargo install --git https://github.com/rust-mobile/cargo-apk --branch=adb-logcat-uid
```
### Temporary JNIEnv log
This message is logged every frame. It's not yet fixed.
```sh
I JniUtils-inl: Creating temporary JNIEnv. This is a heavy operation and should be infrequent. To optimize, use JNI AttachCurrentThread on calling threa
```
### Android keystore
Release mode requires keystore. See Cargo.toml `package.metadata.android.signing.release`.
When creating your own apps, make sure to generate your own keystore, rather than using our example one!
You can use `keytool` like so:
```sh
keytool -genkey -v -keystore my-release-key.keystore -keyalg RSA -keysize 2048 -validity 10000
```
For more information on key signing and why it's so important, check out this article:
https://developer.android.com/studio/publish/app-signing

View File

@@ -1,130 +0,0 @@
use bevy::color::palettes;
use bevy::diagnostic::{FrameTimeDiagnosticsPlugin, LogDiagnosticsPlugin};
use bevy::prelude::*;
use bevy::transform::components::Transform;
use bevy_oxr::graphics::extensions::XrExtensions;
use bevy_oxr::graphics::XrAppInfo;
use bevy_oxr::passthrough::{PausePassthrough, ResumePassthrough, XrPassthroughState};
use bevy_oxr::xr_init::xr_only;
use bevy_oxr::xr_input::hands::common::HandInputDebugRenderer;
use bevy_oxr::xr_input::hands::HandBone;
use bevy_oxr::xr_input::prototype_locomotion::{proto_locomotion, PrototypeLocomotionConfig};
use bevy_oxr::xr_input::trackers::{
OpenXRController, OpenXRLeftController, OpenXRRightController, OpenXRTracker,
};
use bevy_oxr::DefaultXrPlugins;
#[bevy_main]
fn main() {
let mut xr_extensions = XrExtensions::default();
xr_extensions.enable_fb_passthrough();
xr_extensions.enable_hand_tracking();
App::new()
.add_plugins(DefaultXrPlugins {
reqeusted_extensions: xr_extensions,
app_info: XrAppInfo {
name: "Bevy OXR Android Example".into(),
},
..Default::default()
})
// .add_plugins(OpenXrDebugRenderer)
.add_plugins(LogDiagnosticsPlugin::default())
.add_plugins(FrameTimeDiagnosticsPlugin)
.add_plugins(HandInputDebugRenderer)
.add_plugins(bevy_oxr::passthrough::EnablePassthroughStartup)
.add_systems(Startup, setup)
.add_systems(
Update,
(proto_locomotion, toggle_passthrough).run_if(xr_only()),
)
.add_systems(Update, debug_hand_render.run_if(xr_only()))
.add_systems(Startup, spawn_controllers_example)
.insert_resource(PrototypeLocomotionConfig::default())
.run();
}
fn debug_hand_render(query: Query<&GlobalTransform, With<HandBone>>, mut gizmos: Gizmos) {
for transform in &query {
gizmos.sphere(
transform.translation(),
Quat::IDENTITY,
0.01,
palettes::css::RED,
);
}
}
/// set up a simple 3D scene
fn setup(
mut commands: Commands,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<StandardMaterial>>,
) {
// plane
commands.spawn(PbrBundle {
mesh: meshes.add(Plane3d::new(Vec3::Y, Vec2::splat(2.5))),
material: materials.add(StandardMaterial::from(Color::srgb(0.3, 0.5, 0.3))),
..default()
});
// cube
commands.spawn(PbrBundle {
mesh: meshes.add(Cuboid::from_size(Vec3::splat(0.1)).mesh()),
material: materials.add(StandardMaterial::from(Color::srgb(0.8, 0.7, 0.6))),
transform: Transform::from_xyz(0.0, 0.5, 0.0),
..default()
});
// cube
commands.spawn(PbrBundle {
mesh: meshes.add(Mesh::from(Cuboid::from_size(Vec3::splat(0.1)))),
material: materials.add(StandardMaterial::from(Color::srgb(0.8, 0.0, 0.0))),
transform: Transform::from_xyz(0.0, 0.5, 1.0),
..default()
});
// light
commands.spawn(PointLightBundle {
point_light: PointLight {
intensity: 1500.0,
shadows_enabled: true,
..default()
},
transform: Transform::from_xyz(4.0, 8.0, 4.0),
..default()
});
}
fn spawn_controllers_example(mut commands: Commands) {
//left hand
commands.spawn((
OpenXRLeftController,
OpenXRController,
OpenXRTracker,
SpatialBundle::default(),
));
//right hand
commands.spawn((
OpenXRRightController,
OpenXRController,
OpenXRTracker,
SpatialBundle::default(),
));
}
// TODO: make this a vr button
fn toggle_passthrough(
keys: Res<ButtonInput<KeyCode>>,
passthrough_state: Res<XrPassthroughState>,
mut resume: EventWriter<ResumePassthrough>,
mut pause: EventWriter<PausePassthrough>,
) {
if keys.just_pressed(KeyCode::Space) {
match *passthrough_state {
XrPassthroughState::Unsupported => {}
XrPassthroughState::Running => {
pause.send_default();
}
XrPassthroughState::Paused => {
resume.send_default();
}
}
}
}

View File

@@ -1,15 +0,0 @@
[package]
name = "demo"
version = "0.1.0"
description = "Demo using bevy_oxr"
edition.workspace = true
publish = false
[lib]
crate-type = ["rlib", "cdylib"]
[dependencies]
bevy.workspace = true
bevy_oxr.path = "../../"
bevy_rapier3d = "0.25"
color-eyre.workspace = true

View File

@@ -1,11 +0,0 @@
## setup
install xbuild ```cargo install --git https://github.com/rust-mobile/xbuild```
run ```x doctor``` and install all required depencencies
download the [openxr loader](https://developer.oculus.com/downloads/package/oculus-openxr-mobile-sdk/) and put it in the runtime_libs/arm64-v8a folder
## how to run
run ```x devices```
and get the device name that looks something like this ```adb:1WD523S``` (probably a bit longer)
then ```run x run --release```

View File

@@ -1,39 +0,0 @@
android:
runtime_libs:
- "runtime_libs"
manifest:
package: "org.bevyengine.demo_openxr_android"
uses_feature:
- name: "android.hardware.vr.headtracking"
required: true
- name: "oculus.software.handtracking"
required: true
- name: "com.oculus.feature.PASSTHROUGH"
required: true
- name: "com.oculus.experimental.enabled"
required: true
uses_permission:
- name: "com.oculus.permission.HAND_TRACKING"
application:
label: "Bevy Openxr Android"
theme: "@android:style/Theme.DeviceDefault.NoActionBar.Fullscreen"
meta_data:
- name: "com.oculus.intent.category.VR"
value: "vr_only"
- name: "com.samsung.android.vr.application.mode"
value: "vr_only"
- name: "com.oculus.supportedDevices"
value: "quest|quest2|quest3|questpro"
activities:
- config_changes: "density|keyboard|keyboardHidden|navigation|orientation|screenLayout|screenSize|uiMode|screenLayout"
launch_mode: "singleTask"
orientation: "landscape"
intent_filters:
- actions:
- "android.intent.action.MAIN"
categories:
- "com.oculus.intent.category.VR"
- "android.intent.category.LAUNCHER"
- "org.khronos.openxr.intent.category.IMMERSIVE_HMD"
sdk:
target_sdk_version: 32

View File

@@ -1,771 +0,0 @@
mod setup;
use std::time::Duration;
use bevy::{
diagnostic::{FrameTimeDiagnosticsPlugin, LogDiagnosticsPlugin},
ecs::schedule::ScheduleLabel,
log::info,
math::primitives::{Capsule3d, Cuboid},
prelude::{
bevy_main, default, App, Assets, Color, Commands, Component, Entity, Event, EventReader,
EventWriter, FixedUpdate, GlobalTransform, IntoSystemConfigs, IntoSystemSetConfigs, Mesh,
PbrBundle, PostUpdate, Query, Res, ResMut, Resource, Schedule, SpatialBundle,
StandardMaterial, Startup, Transform, Update, Vec3, With, Without, World,
},
render::mesh::Meshable,
time::{Fixed, Time, Timer, TimerMode},
transform::TransformSystem,
};
use bevy_oxr::{
graphics::{extensions::XrExtensions, XrAppInfo},
input::XrInput,
resources::{XrFrameState, XrSession},
xr_init::xr_only,
xr_input::{
actions::XrActionSets,
debug_gizmos::OpenXrDebugRenderer,
hands::common::{HandInputDebugRenderer, HandResource, HandsResource},
hands::HandBone,
interactions::{
draw_interaction_gizmos, draw_socket_gizmos, interactions, socket_interactions,
update_interactable_states, InteractionEvent, Touched, XRDirectInteractor,
XRInteractable, XRInteractableState, XRInteractorState, XRSelection,
},
oculus_touch::OculusController,
prototype_locomotion::{proto_locomotion, PrototypeLocomotionConfig},
trackers::{OpenXRController, OpenXRLeftController, OpenXRRightController, OpenXRTracker},
Hand,
},
DefaultXrPlugins,
};
use crate::setup::setup_scene;
use bevy_rapier3d::prelude::*;
#[bevy_main]
pub fn main() {
color_eyre::install().unwrap();
info!("Running bevy_openxr demo");
let mut app = App::new();
let mut xr_extensions = XrExtensions::default();
xr_extensions.enable_fb_passthrough();
app
//lets get the usual diagnostic stuff added
.add_plugins(LogDiagnosticsPlugin::default())
.add_plugins(FrameTimeDiagnosticsPlugin)
//lets get the xr defaults added
.add_plugins(DefaultXrPlugins {
reqeusted_extensions: xr_extensions,
app_info: XrAppInfo {
name: "Bevy OXR Demo".into(),
},
..Default::default()
})
//lets add the debug renderer for the controllers
.add_plugins(OpenXrDebugRenderer)
//rapier goes here
.add_plugins(RapierPhysicsPlugin::<NoUserData>::default().with_default_system_setup(false))
// .add_plugins(RapierDebugRenderPlugin::default())
//lets setup the starting scene
.add_systems(Startup, setup_scene)
.add_systems(Startup, spawn_controllers_example) //you need to spawn controllers or it crashes TODO:: Fix this
//add locomotion
.add_systems(Update, proto_locomotion.run_if(xr_only()))
.insert_resource(PrototypeLocomotionConfig::default())
//lets add the interaction systems
.add_event::<InteractionEvent>()
.add_systems(Update, prototype_interaction_input.run_if(xr_only()))
.add_systems(Update, interactions.before(update_interactable_states))
.add_systems(Update, update_interactable_states)
.add_systems(
Update,
socket_interactions.before(update_interactable_states),
)
//add the grabbable system
.add_systems(Update, update_grabbables.after(update_interactable_states))
//draw the interaction gizmos
.add_systems(
Update,
draw_interaction_gizmos
.run_if(xr_only())
.after(update_interactable_states),
)
.add_systems(Update, draw_socket_gizmos.after(update_interactable_states))
//add our cube spawning system
.add_event::<SpawnCubeRequest>()
.insert_resource(SpawnCubeTimer(Timer::from_seconds(
0.25,
bevy::time::TimerMode::Once,
)))
.add_systems(Update, request_cube_spawn.run_if(xr_only()))
.add_systems(Update, cube_spawner.after(request_cube_spawn))
//test capsule
.add_systems(Startup, spawn_capsule)
//physics hands
// .add_plugins(OpenXrHandInput)
.add_plugins(HandInputDebugRenderer)
.add_systems(Startup, spawn_physics_hands)
.add_systems(
FixedUpdate,
update_physics_hands.before(PhysicsSet::SyncBackend),
)
.add_event::<GhostHandEvent>()
.add_systems(Update, handle_ghost_hand_events.after(update_grabbables))
.insert_resource(GhostTimers {
left: Timer::from_seconds(0.25, TimerMode::Once),
right: Timer::from_seconds(0.25, TimerMode::Once),
})
.add_systems(Update, watch_ghost_timers.before(handle_ghost_hand_events));
//configure rapier sets
let mut physics_schedule = Schedule::new(PhysicsSchedule);
physics_schedule.configure_sets(
(
PhysicsSet::SyncBackend,
PhysicsSet::StepSimulation,
PhysicsSet::Writeback,
)
.chain()
.before(TransformSystem::TransformPropagate),
);
app.configure_sets(
PostUpdate,
(
PhysicsSet::SyncBackend,
PhysicsSet::StepSimulation,
PhysicsSet::Writeback,
)
.chain()
.before(TransformSystem::TransformPropagate),
);
//add rapier systems
physics_schedule.add_systems((
RapierPhysicsPlugin::<NoUserData>::get_systems(PhysicsSet::SyncBackend)
.in_set(PhysicsSet::SyncBackend),
RapierPhysicsPlugin::<NoUserData>::get_systems(PhysicsSet::StepSimulation)
.in_set(PhysicsSet::StepSimulation),
RapierPhysicsPlugin::<NoUserData>::get_systems(PhysicsSet::Writeback)
.in_set(PhysicsSet::Writeback),
));
app.add_schedule(physics_schedule) // configure our fixed timestep schedule to run at the rate we want
.insert_resource(Time::<Fixed>::from_duration(Duration::from_secs_f32(
FIXED_TIMESTEP,
)))
.add_systems(FixedUpdate, run_physics_schedule)
.add_systems(Startup, configure_physics);
app.run();
}
//fixed timesteps?
const FIXED_TIMESTEP: f32 = 1. / 90.;
// A label for our new Schedule!
#[derive(ScheduleLabel, Debug, Hash, PartialEq, Eq, Clone)]
struct PhysicsSchedule;
fn run_physics_schedule(world: &mut World) {
world.run_schedule(PhysicsSchedule);
}
fn configure_physics(mut rapier_config: ResMut<RapierConfiguration>) {
rapier_config.timestep_mode = TimestepMode::Fixed {
dt: FIXED_TIMESTEP,
substeps: 1,
}
}
fn spawn_controllers_example(mut commands: Commands) {
//left hand
commands.spawn((
OpenXRLeftController,
OpenXRController,
OpenXRTracker,
SpatialBundle::default(),
XRDirectInteractor,
XRInteractorState::default(),
XRSelection::default(),
Hand::Left,
));
//right hand
commands.spawn((
OpenXRRightController,
OpenXRController,
OpenXRTracker,
SpatialBundle::default(),
XRDirectInteractor,
XRInteractorState::default(),
XRSelection::default(),
Hand::Right,
));
}
fn spawn_capsule(
mut commands: Commands,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<StandardMaterial>>,
) {
commands.spawn((
PbrBundle {
mesh: meshes.add(Capsule3d::new(0.033, 0.115).mesh()),
material: materials.add(StandardMaterial::from(Color::rgb(0.8, 0.7, 0.6))),
transform: Transform::from_xyz(0.0, 2.0, 0.0),
..default()
},
// Collider::capsule_y(0.0575, 0.034),
Collider::capsule(
Vec3 {
x: 0.0,
y: -0.0575,
z: 0.0,
},
Vec3 {
x: 0.0,
y: 0.0575,
z: 0.0,
},
0.034,
),
RigidBody::Dynamic,
));
}
#[derive(Component, PartialEq, Debug, Clone, Copy)]
pub enum PhysicsHandBone {
Palm,
Wrist,
ThumbMetacarpal,
ThumbProximal,
ThumbDistal,
ThumbTip,
IndexMetacarpal,
IndexProximal,
IndexIntermediate,
IndexDistal,
IndexTip,
MiddleMetacarpal,
MiddleProximal,
MiddleIntermediate,
MiddleDistal,
MiddleTip,
RingMetacarpal,
RingProximal,
RingIntermediate,
RingDistal,
RingTip,
LittleMetacarpal,
LittleProximal,
LittleIntermediate,
LittleDistal,
LittleTip,
}
#[derive(Component, PartialEq)]
pub enum BoneInitState {
True,
False,
}
fn spawn_physics_hands(mut commands: Commands) {
//here we go
let hands = [Hand::Left, Hand::Right];
let bones = [
PhysicsHandBone::Palm,
PhysicsHandBone::Wrist,
PhysicsHandBone::ThumbMetacarpal,
PhysicsHandBone::ThumbProximal,
PhysicsHandBone::ThumbDistal,
PhysicsHandBone::ThumbTip,
PhysicsHandBone::IndexMetacarpal,
PhysicsHandBone::IndexProximal,
PhysicsHandBone::IndexIntermediate,
PhysicsHandBone::IndexDistal,
PhysicsHandBone::IndexTip,
PhysicsHandBone::MiddleMetacarpal,
PhysicsHandBone::MiddleProximal,
PhysicsHandBone::MiddleIntermediate,
PhysicsHandBone::MiddleDistal,
PhysicsHandBone::MiddleTip,
PhysicsHandBone::RingMetacarpal,
PhysicsHandBone::RingProximal,
PhysicsHandBone::RingIntermediate,
PhysicsHandBone::RingDistal,
PhysicsHandBone::RingTip,
PhysicsHandBone::LittleMetacarpal,
PhysicsHandBone::LittleProximal,
PhysicsHandBone::LittleIntermediate,
PhysicsHandBone::LittleDistal,
PhysicsHandBone::LittleTip,
];
let radius = 0.010;
let left_hand_membership_group = Group::GROUP_1;
let right_hand_membership_group = Group::GROUP_2;
let floor_membership = Group::GROUP_3;
for hand in hands.iter() {
let hand_membership = match hand {
Hand::Left => left_hand_membership_group,
Hand::Right => right_hand_membership_group,
};
let mut hand_filter: Group = Group::ALL;
hand_filter.remove(hand_membership);
hand_filter.remove(floor_membership);
for bone in bones.iter() {
//spawn the thing
commands.spawn((
SpatialBundle::default(),
Collider::capsule(
Vec3 {
x: 0.0,
y: -0.0575,
z: 0.0,
},
Vec3 {
x: 0.0,
y: 0.0575,
z: 0.0,
},
radius,
),
RigidBody::Dynamic,
Velocity::default(),
CollisionGroups::new(hand_membership, Group::from_bits(0b0001).unwrap()),
// SolverGroups::new(self_group, interaction_group),
bone.clone(),
BoneInitState::False,
hand.clone(),
));
}
}
}
pub enum MatchingType {
PositionMatching,
VelocityMatching,
}
fn update_physics_hands(
hands_res: Option<Res<HandsResource>>,
mut bone_query: Query<(
&mut Transform,
&mut Collider,
&PhysicsHandBone,
&mut BoneInitState,
&Hand,
&mut Velocity,
)>,
hand_query: Query<(&Transform, &HandBone, &Hand), Without<PhysicsHandBone>>,
time: Res<Time>,
) {
let matching = MatchingType::VelocityMatching;
//sanity check do we even have hands?
match hands_res {
Some(res) => {
//config stuff
let radius = 0.010;
for mut bone in bone_query.iter_mut() {
let hand_res = match bone.4 {
Hand::Left => res.left,
Hand::Right => res.right,
};
//lets just do the Right ThumbMetacarpal for now
let result = get_start_and_end_entities(hand_res, bone.2);
if let Some((start_entity, end_entity)) = result {
//now we need their transforms
let start_components = hand_query.get(start_entity);
let end_components = hand_query.get(end_entity);
let direction = end_components.unwrap().0.translation
- start_components.unwrap().0.translation;
if direction.length() < 0.001 {
//i hate this but we need to skip init if the length is zero
return;
}
match *bone.3 {
BoneInitState::True => {
match matching {
MatchingType::PositionMatching => {
//if we are init then we just move em?
*bone.0 = start_components
.unwrap()
.0
.clone()
.looking_at(end_components.unwrap().0.translation, Vec3::Y);
}
MatchingType::VelocityMatching => {
//calculate position difference
let diff = (start_components.unwrap().0.translation
- bone.0.translation)
/ time.delta_seconds();
bone.5.linvel = diff;
//calculate angular velocity?
// gizmos.ray(bone.0.translation, bone.0.forward(), Color::WHITE);
let desired_forward = start_components
.unwrap()
.0
.clone()
.looking_at(end_components.unwrap().0.translation, Vec3::Y)
.rotation;
// gizmos.ray(
// bone.0.translation,
// desired_forward.mul_vec3(-Vec3::Z),
// Color::GREEN,
// );
let cross =
bone.0.forward().cross(desired_forward.mul_vec3(-Vec3::Z));
// gizmos.ray(
// bone.0.translation,
// cross,
// Color::RED,
// );
bone.5.angvel = cross / time.delta_seconds();
}
}
}
BoneInitState::False => {
//build a new collider?
*bone.1 = Collider::capsule(
Vec3::splat(0.0),
Vec3 {
x: 0.0,
y: 0.0,
z: -direction.length(),
},
radius,
);
*bone.3 = BoneInitState::True;
}
}
}
}
}
None => info!("hand states resource not initialized yet"),
}
}
fn get_start_and_end_entities(
hand_res: HandResource,
bone: &PhysicsHandBone,
) -> Option<(Entity, Entity)> {
match bone {
PhysicsHandBone::Palm => return None,
PhysicsHandBone::Wrist => return None,
PhysicsHandBone::ThumbMetacarpal => {
return Some((hand_res.thumb.metacarpal, hand_res.thumb.proximal))
}
PhysicsHandBone::ThumbProximal => {
return Some((hand_res.thumb.proximal, hand_res.thumb.distal))
}
PhysicsHandBone::ThumbDistal => return Some((hand_res.thumb.distal, hand_res.thumb.tip)),
PhysicsHandBone::ThumbTip => return None,
PhysicsHandBone::IndexMetacarpal => {
return Some((hand_res.index.metacarpal, hand_res.index.proximal))
}
PhysicsHandBone::IndexProximal => {
return Some((hand_res.index.proximal, hand_res.index.intermediate))
}
PhysicsHandBone::IndexIntermediate => {
return Some((hand_res.index.intermediate, hand_res.index.distal))
}
PhysicsHandBone::IndexDistal => return Some((hand_res.index.distal, hand_res.index.tip)),
PhysicsHandBone::IndexTip => return None,
PhysicsHandBone::MiddleMetacarpal => {
return Some((hand_res.middle.metacarpal, hand_res.middle.proximal))
}
PhysicsHandBone::MiddleProximal => {
return Some((hand_res.middle.proximal, hand_res.middle.intermediate))
}
PhysicsHandBone::MiddleIntermediate => {
return Some((hand_res.middle.intermediate, hand_res.middle.distal))
}
PhysicsHandBone::MiddleDistal => {
return Some((hand_res.middle.distal, hand_res.middle.tip))
}
PhysicsHandBone::MiddleTip => return None,
PhysicsHandBone::RingMetacarpal => {
return Some((hand_res.ring.metacarpal, hand_res.ring.proximal))
}
PhysicsHandBone::RingProximal => {
return Some((hand_res.ring.proximal, hand_res.ring.intermediate))
}
PhysicsHandBone::RingIntermediate => {
return Some((hand_res.ring.intermediate, hand_res.ring.distal))
}
PhysicsHandBone::RingDistal => return Some((hand_res.ring.distal, hand_res.ring.tip)),
PhysicsHandBone::RingTip => return None,
PhysicsHandBone::LittleMetacarpal => {
return Some((hand_res.little.metacarpal, hand_res.little.proximal))
}
PhysicsHandBone::LittleProximal => {
return Some((hand_res.little.proximal, hand_res.little.intermediate))
}
PhysicsHandBone::LittleIntermediate => {
return Some((hand_res.little.intermediate, hand_res.little.distal))
}
PhysicsHandBone::LittleDistal => {
return Some((hand_res.little.distal, hand_res.little.tip))
}
PhysicsHandBone::LittleTip => return None,
};
}
#[derive(Event, Default)]
pub struct SpawnCubeRequest;
#[derive(Resource)]
pub struct SpawnCubeTimer(Timer);
fn request_cube_spawn(
oculus_controller: Res<OculusController>,
frame_state: Res<XrFrameState>,
xr_input: Res<XrInput>,
session: Res<XrSession>,
mut writer: EventWriter<SpawnCubeRequest>,
time: Res<Time>,
mut timer: ResMut<SpawnCubeTimer>,
action_sets: Res<XrActionSets>,
) {
timer.0.tick(time.delta());
if timer.0.finished() {
//get controller
let controller = oculus_controller.get_ref(&session, &frame_state, &xr_input, &action_sets);
//get controller triggers
let left_main_button = controller.a_button();
if left_main_button {
writer.send(SpawnCubeRequest::default());
timer.0.reset();
}
let right_main_button = controller.x_button();
if right_main_button {
writer.send(SpawnCubeRequest::default());
timer.0.reset();
}
}
}
fn cube_spawner(
mut commands: Commands,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<StandardMaterial>>,
mut events: EventReader<SpawnCubeRequest>,
) {
for _request in events.read() {
// cube
commands.spawn((
PbrBundle {
mesh: meshes.add(Cuboid::from_size(Vec3::splat(0.1)).mesh()),
material: materials.add(StandardMaterial::from(Color::rgb(0.8, 0.7, 0.6))),
transform: Transform::from_xyz(0.0, 1.0, 0.0),
..default()
},
RigidBody::Dynamic,
Collider::cuboid(0.05, 0.05, 0.05),
ColliderDebugColor(Color::hsl(220.0, 1.0, 0.3)),
XRInteractable,
XRInteractableState::default(),
Grabbable,
Touched(false),
));
}
}
//TODO: find a real place for this
fn prototype_interaction_input(
oculus_controller: Res<OculusController>,
frame_state: Res<XrFrameState>,
xr_input: Res<XrInput>,
session: Res<XrSession>,
mut right_interactor_query: Query<
&mut XRInteractorState,
(
With<XRDirectInteractor>,
With<OpenXRRightController>,
Without<OpenXRLeftController>,
),
>,
mut left_interactor_query: Query<
&mut XRInteractorState,
(
With<XRDirectInteractor>,
With<OpenXRLeftController>,
Without<OpenXRRightController>,
),
>,
action_sets: Res<XrActionSets>,
) {
//get controller
let controller = oculus_controller.get_ref(&session, &frame_state, &xr_input, &action_sets);
//get controller triggers
let left_trigger = controller.trigger(Hand::Left);
let right_trigger = controller.trigger(Hand::Right);
//get the interactors and do state stuff
let mut left_state = left_interactor_query.single_mut();
if left_trigger > 0.8 {
*left_state = XRInteractorState::Selecting;
} else {
*left_state = XRInteractorState::Idle;
}
let mut right_state = right_interactor_query.single_mut();
if right_trigger > 0.8 {
*right_state = XRInteractorState::Selecting;
} else {
*right_state = XRInteractorState::Idle;
}
}
//this event is for transitioning the physics hand in an out of existent so we can drop things better
#[derive(Event)]
pub struct GhostHandEvent {
pub hand: Hand,
pub desired_state: bool, //true for no interactions, false for normal interactions
}
#[derive(Resource)]
pub struct GhostTimers {
pub left: Timer,
pub right: Timer,
}
pub fn handle_ghost_hand_events(
mut events: EventReader<GhostHandEvent>,
mut bones: Query<(&Hand, &mut CollisionGroups), With<PhysicsHandBone>>,
) {
for event in events.read() {
// info!(
// "Ghost hand Event: {:?}, {:?}",
// event.hand, event.desired_state
// );
//do work
for mut bone in bones.iter_mut() {
match *bone.0 == event.hand {
true => match event.desired_state {
true => bone.1.filters = Group::NONE,
false => bone.1.filters = Group::from_bits(0b0001).unwrap(),
},
false => (),
}
}
}
}
pub fn watch_ghost_timers(
mut timers: ResMut<GhostTimers>,
mut writer: EventWriter<GhostHandEvent>,
time: Res<Time>,
) {
//tick both timers
timers.left.tick(time.delta());
timers.right.tick(time.delta());
//if they finish send events to make the hands physical again
if timers.left.just_finished() {
writer.send(GhostHandEvent {
hand: Hand::Left,
desired_state: false,
});
}
if timers.right.just_finished() {
writer.send(GhostHandEvent {
hand: Hand::Right,
desired_state: false,
});
}
}
#[derive(Component)]
pub struct Grabbable;
pub fn update_grabbables(
mut events: EventReader<InteractionEvent>,
mut grabbable_query: Query<
(Entity, &mut Transform, Option<&mut RigidBody>),
(Without<XRDirectInteractor>, With<Grabbable>),
>,
mut interactor_query: Query<
(
&GlobalTransform,
&XRInteractorState,
&mut XRSelection,
&Hand,
),
Without<Grabbable>,
>,
mut writer: EventWriter<GhostHandEvent>,
mut timers: ResMut<GhostTimers>,
) {
//so basically the idea is to try all the events?
for event in events.read() {
// info!("some event");
match grabbable_query.get_mut(event.interactable) {
Ok(mut grabbable_transform) => {
// info!("we got a grabbable");
//now we need the location of our interactor
match interactor_query.get_mut(event.interactor) {
Ok(mut interactor_transform) => {
match *interactor_transform.2 {
XRSelection::Empty => {
match interactor_transform.1 {
XRInteractorState::Idle => match grabbable_transform.2 {
Some(mut thing) => {
*thing = RigidBody::Dynamic;
*interactor_transform.2 = XRSelection::Empty;
}
None => (),
},
XRInteractorState::Selecting => {
// info!("its a direct interactor?");
match grabbable_transform.2 {
Some(mut thing) => {
*thing = RigidBody::KinematicPositionBased;
*interactor_transform.2 =
XRSelection::Full(grabbable_transform.0);
//raise enter ghost hand event
writer.send(GhostHandEvent {
hand: *interactor_transform.3,
desired_state: true,
});
}
None => (),
}
*grabbable_transform.1 =
interactor_transform.0.compute_transform();
}
}
}
XRSelection::Full(ent) => {
info!("nah bro we holding something");
match grabbable_transform.0 == ent {
true => {
*grabbable_transform.1 =
interactor_transform.0.compute_transform();
}
false => {}
}
match interactor_transform.1 {
XRInteractorState::Idle => {
*interactor_transform.2 = XRSelection::Empty;
//reset timers to make hands physical again
match *interactor_transform.3 {
Hand::Left => timers.left.reset(),
Hand::Right => timers.right.reset(),
}
}
XRInteractorState::Selecting => {}
}
}
}
}
Err(_) => {
// info!("not a direct interactor")
}
}
}
Err(_) => {
// info!("not a grabbable?")
}
}
}
}

View File

@@ -1,3 +0,0 @@
fn main() {
demo::main();
}

View File

@@ -1,81 +0,0 @@
use bevy::{
math::primitives::{Cuboid, Plane3d},
prelude::{
Assets, Camera3dBundle, Color, Commands, Mesh, PbrBundle, ResMut, StandardMaterial,
Transform, Vec3,
},
render::mesh::Meshable,
utils::default,
};
use bevy_oxr::xr_input::interactions::{Touched, XRInteractable, XRInteractableState};
use bevy_rapier3d::{
prelude::{Collider, CollisionGroups, Group, RigidBody},
render::ColliderDebugColor,
};
use crate::Grabbable;
/// set up a simple 3D scene
pub fn setup_scene(
mut commands: Commands,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<StandardMaterial>>,
) {
/*
* workbench plane
*/
let ground_size = 2.5;
let ground_height = 0.825;
let ground_thickness = 0.05;
// plane
commands.spawn((
PbrBundle {
mesh: meshes.add(Plane3d::new(Vec3::Y).mesh()),
material: materials.add(StandardMaterial::from(Color::rgb(0.3, 0.5, 0.3))),
transform: Transform::from_xyz(0.0, ground_height, 0.0),
..default()
},
RigidBody::Fixed,
Collider::cuboid(ground_size, ground_thickness, ground_size),
CollisionGroups::new(Group::GROUP_3, Group::ALL),
));
// cube
commands.spawn((
PbrBundle {
mesh: meshes.add(Cuboid::from_size(Vec3::splat(0.1))),
material: materials.add(StandardMaterial::from(Color::rgb(0.8, 0.7, 0.6))),
transform: Transform::from_xyz(0.0, 1.0, 0.0),
..default()
},
RigidBody::Dynamic,
Collider::cuboid(0.05, 0.05, 0.05),
ColliderDebugColor(Color::hsl(220.0, 1.0, 0.3)),
XRInteractable,
XRInteractableState::default(),
Grabbable,
Touched(false),
));
// light
// commands.spawn(PointLightBundle {
// point_light: PointLight {
// intensity: 1500.0,
// shadows_enabled: true,
// ..default()
// },
// transform: Transform::from_xyz(4.0, 8.0, 4.0),
// ..default()
// });
// camera
commands.spawn((Camera3dBundle {
transform: Transform::from_xyz(5.25, 5.25, 5.0).looking_at(
Vec3 {
x: -0.548,
y: -0.161,
z: -0.137,
},
Vec3::Y,
),
..default()
},));
}

View File

@@ -1,187 +0,0 @@
use bevy::diagnostic::LogDiagnosticsPlugin;
use bevy::prelude::*;
use bevy::render::render_asset::RenderAssetUsages;
use bevy::transform::components::Transform;
use bevy_oxr::graphics::XrAppInfo;
use bevy_oxr::prelude::XrSystems;
use bevy_oxr::resources::XrViews;
use bevy_oxr::xr_input::hands::common::HandInputDebugRenderer;
use bevy_oxr::xr_input::interactions::{
InteractionEvent, XRDirectInteractor, XRInteractorState, XRRayInteractor, XRSocketInteractor,
};
use bevy_oxr::xr_input::prototype_locomotion::{proto_locomotion, PrototypeLocomotionConfig};
use bevy_oxr::xr_input::trackers::{
AimPose, OpenXRController, OpenXRLeftController, OpenXRRightController, OpenXRTracker,
OpenXRTrackingRoot,
};
use bevy_oxr::xr_input::Vec3Conv;
use bevy_oxr::DefaultXrPlugins;
use wgpu::{Extent3d, TextureDimension, TextureFormat};
fn main() {
color_eyre::install().unwrap();
App::new()
.add_plugins(DefaultXrPlugins {
app_info: XrAppInfo {
name: "Bevy OXR Globe Example".into(),
},
..default()
})
.add_plugins(LogDiagnosticsPlugin::default())
.add_systems(Startup, setup)
.add_systems(Update, (proto_locomotion, pull_to_ground).chain().xr_only())
.insert_resource(PrototypeLocomotionConfig::default())
.add_systems(Startup, spawn_controllers_example)
.add_plugins(HandInputDebugRenderer)
.add_event::<InteractionEvent>()
.run();
}
#[derive(Component)]
struct Globe {
radius: f32,
}
/// Creates a colorful test pattern
fn uv_debug_texture() -> Image {
const TEXTURE_SIZE: usize = 8;
let mut palette: [u8; 32] = [
255, 102, 159, 255, 255, 159, 102, 255, 236, 255, 102, 255, 121, 255, 102, 255, 102, 255,
198, 255, 102, 198, 255, 255, 121, 102, 255, 255, 236, 102, 255, 255,
];
let mut texture_data = [0; TEXTURE_SIZE * TEXTURE_SIZE * 4];
for y in 0..TEXTURE_SIZE {
let offset = TEXTURE_SIZE * y * 4;
texture_data[offset..(offset + TEXTURE_SIZE * 4)].copy_from_slice(&palette);
palette.rotate_right(4);
}
Image::new_fill(
Extent3d {
width: TEXTURE_SIZE as u32,
height: TEXTURE_SIZE as u32,
depth_or_array_layers: 1,
},
TextureDimension::D2,
&texture_data,
TextureFormat::Rgba8UnormSrgb,
RenderAssetUsages::RENDER_WORLD,
)
}
/// set up a simple 3D scene
fn setup(
mut commands: Commands,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<StandardMaterial>>,
mut images: ResMut<Assets<Image>>,
) {
// plane
let radius = 5.0;
commands.spawn((
PbrBundle {
mesh: meshes.add(Sphere::new(radius)),
material: materials.add(StandardMaterial {
base_color_texture: Some(images.add(uv_debug_texture())),
..default()
}),
transform: Transform::from_xyz(0.0, -radius, 0.0),
..default()
},
Globe { radius },
));
// cube
commands.spawn(PbrBundle {
mesh: meshes.add(Cuboid::from_size(Vec3::splat(0.1)).mesh()),
material: materials.add(StandardMaterial::from(Color::srgb(0.8, 0.7, 0.6))),
transform: Transform::from_xyz(0.0, 0.5, 0.0),
..default()
});
// socket
commands.spawn((
SpatialBundle {
transform: Transform::from_xyz(0.0, 0.5, 1.0),
..default()
},
XRInteractorState::Selecting,
XRSocketInteractor,
));
// light
commands.spawn(PointLightBundle {
point_light: PointLight {
intensity: 1500.0,
shadows_enabled: true,
..default()
},
transform: Transform::from_xyz(4.0, 8.0, 4.0),
..default()
});
// camera
commands.spawn((Camera3dBundle {
transform: Transform::from_xyz(0.25, 1.25, 0.0).looking_at(
Vec3 {
x: -0.548,
y: -0.161,
z: -0.137,
},
Vec3::Y,
),
..default()
},));
}
fn spawn_controllers_example(mut commands: Commands) {
//left hand
commands.spawn((
OpenXRLeftController,
OpenXRController,
OpenXRTracker,
SpatialBundle::default(),
XRRayInteractor,
AimPose(Transform::default()),
XRInteractorState::default(),
));
//right hand
commands.spawn((
OpenXRRightController,
OpenXRController,
OpenXRTracker,
SpatialBundle::default(),
XRDirectInteractor,
XRInteractorState::default(),
));
}
fn pull_to_ground(
time: Res<Time>,
mut tracking_root_query: Query<&mut Transform, (With<OpenXRTrackingRoot>, Without<Globe>)>,
globe: Query<(&Transform, &Globe), Without<OpenXRTrackingRoot>>,
views: ResMut<XrViews>,
) {
let mut root = tracking_root_query.single_mut();
let (globe_pos, globe) = globe.single();
// Get player position (position of playground + position within playground)
let Some(view) = views.first() else { return };
let mut hmd_translation = view.pose.position.to_vec3();
hmd_translation.y = 0.0;
let local = root.translation;
let offset = root.rotation.mul_vec3(hmd_translation);
let global = offset + local;
let adjustment_rate = (time.delta_seconds() * 10.0).min(1.0);
// Lower player onto sphere
let up = (global - globe_pos.translation).normalize();
let diff = up * globe.radius + globe_pos.translation - offset - root.translation;
root.translation += diff * adjustment_rate;
// Rotate player to be upright on sphere
let angle_diff = Quat::from_rotation_arc(*root.up(), up);
let point = root.translation + offset;
root.rotate_around(point, Quat::IDENTITY.slerp(angle_diff, adjustment_rate));
}

View File

@@ -1,252 +0,0 @@
use bevy::diagnostic::{FrameTimeDiagnosticsPlugin, LogDiagnosticsPlugin};
use bevy::prelude::*;
use bevy::transform::components::Transform;
use bevy_oxr::graphics::XrAppInfo;
use bevy_oxr::input::XrInput;
use bevy_oxr::resources::{XrFrameState, XrSession};
use bevy_oxr::xr_init::{xr_only, EndXrSession, StartXrSession, XrSetup};
use bevy_oxr::xr_input::actions::XrActionSets;
use bevy_oxr::xr_input::hands::common::HandInputDebugRenderer;
use bevy_oxr::xr_input::interactions::{
draw_interaction_gizmos, draw_socket_gizmos, interactions, socket_interactions,
update_interactable_states, InteractionEvent, Touched, XRDirectInteractor, XRInteractable,
XRInteractableState, XRInteractorState, XRRayInteractor, XRSocketInteractor,
};
use bevy_oxr::xr_input::oculus_touch::OculusController;
use bevy_oxr::xr_input::prototype_locomotion::{proto_locomotion, PrototypeLocomotionConfig};
use bevy_oxr::xr_input::trackers::{
AimPose, OpenXRController, OpenXRLeftController, OpenXRRightController, OpenXRTracker,
};
use bevy_oxr::xr_input::Hand;
use bevy_oxr::DefaultXrPlugins;
fn main() {
color_eyre::install().unwrap();
info!("Running `openxr-6dof` skill");
App::new()
.add_plugins(DefaultXrPlugins {
app_info: XrAppInfo {
name: "Bevy OXR Example".into(),
},
..default()
})
// .add_plugins(OpenXrDebugRenderer) //new debug renderer adds gizmos to
.add_plugins(LogDiagnosticsPlugin::default())
.add_plugins(FrameTimeDiagnosticsPlugin)
.add_systems(Startup, setup)
.add_systems(Update, proto_locomotion.run_if(xr_only()))
.insert_resource(PrototypeLocomotionConfig::default())
.add_systems(XrSetup, spawn_controllers_example)
.add_plugins(HandInputDebugRenderer)
.add_systems(
Update,
draw_interaction_gizmos
.after(update_interactable_states)
.run_if(xr_only()),
)
.add_systems(
Update,
draw_socket_gizmos
.after(update_interactable_states)
.run_if(xr_only()),
)
.add_systems(
Update,
interactions
.before(update_interactable_states)
.run_if(xr_only()),
)
.add_systems(
Update,
socket_interactions.before(update_interactable_states),
)
.add_systems(Update, prototype_interaction_input.run_if(xr_only()))
.add_systems(Update, update_interactable_states)
.add_systems(Update, update_grabbables.after(update_interactable_states))
.add_systems(Update, start_stop_session)
.add_event::<InteractionEvent>()
.run();
}
fn start_stop_session(
keyboard: Res<ButtonInput<KeyCode>>,
mut start: EventWriter<StartXrSession>,
mut stop: EventWriter<EndXrSession>,
) {
if keyboard.just_pressed(KeyCode::KeyS) {
start.send_default();
}
if keyboard.just_pressed(KeyCode::KeyE) {
stop.send_default();
}
}
/// set up a simple 3D scene
fn setup(
mut commands: Commands,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<StandardMaterial>>,
) {
// plane
commands.spawn(PbrBundle {
mesh: meshes.add(Plane3d::new(Vec3::Y, Vec2::splat(2.5)).mesh()),
material: materials.add(StandardMaterial::from(Color::srgb(0.3, 0.5, 0.3))),
..default()
});
// cube
commands.spawn(PbrBundle {
mesh: meshes.add(Cuboid::from_size(Vec3::splat(0.1)).mesh()),
material: materials.add(StandardMaterial::from(Color::srgb(0.8, 0.7, 0.6))),
transform: Transform::from_xyz(0.0, 0.5, 0.0),
..default()
});
// socket
commands.spawn((
SpatialBundle {
transform: Transform::from_xyz(0.0, 0.5, 1.0),
..default()
},
XRInteractorState::Selecting,
XRSocketInteractor,
));
// light
commands.spawn(PointLightBundle {
point_light: PointLight {
intensity: 1500.0,
shadows_enabled: true,
..default()
},
transform: Transform::from_xyz(4.0, 8.0, 4.0),
..default()
});
// camera
commands.spawn((Camera3dBundle {
transform: Transform::from_xyz(0.25, 1.25, 0.0).looking_at(
Vec3 {
x: -0.548,
y: -0.161,
z: -0.137,
},
Vec3::Y,
),
..default()
},));
//simple interactable
commands.spawn((
SpatialBundle {
transform: Transform::from_xyz(0.0, 1.0, 0.0),
..default()
},
XRInteractable,
XRInteractableState::default(),
Grabbable,
Touched(false),
));
}
fn spawn_controllers_example(mut commands: Commands) {
//left hand
commands.spawn((
OpenXRLeftController,
OpenXRController,
OpenXRTracker,
SpatialBundle::default(),
XRRayInteractor,
AimPose(Transform::default()),
XRInteractorState::default(),
));
//right hand
commands.spawn((
OpenXRRightController,
OpenXRController,
OpenXRTracker,
SpatialBundle::default(),
XRDirectInteractor,
XRInteractorState::default(),
));
}
#[allow(clippy::type_complexity)]
fn prototype_interaction_input(
oculus_controller: Res<OculusController>,
frame_state: Res<XrFrameState>,
xr_input: Res<XrInput>,
session: Res<XrSession>,
mut right_interactor_query: Query<
&mut XRInteractorState,
(
With<XRDirectInteractor>,
With<OpenXRRightController>,
Without<OpenXRLeftController>,
),
>,
mut left_interactor_query: Query<
&mut XRInteractorState,
(
With<XRRayInteractor>,
With<OpenXRLeftController>,
Without<OpenXRRightController>,
),
>,
action_sets: Res<XrActionSets>,
) {
//get controller
let controller = oculus_controller.get_ref(&session, &frame_state, &xr_input, &action_sets);
//get controller triggers
let left_trigger = controller.trigger(Hand::Left);
let right_trigger = controller.trigger(Hand::Right);
//get the interactors and do state stuff
let mut left_state = left_interactor_query.single_mut();
if left_trigger > 0.8 {
*left_state = XRInteractorState::Selecting;
} else {
*left_state = XRInteractorState::Idle;
}
let mut right_state = right_interactor_query.single_mut();
if right_trigger > 0.8 {
*right_state = XRInteractorState::Selecting;
} else {
*right_state = XRInteractorState::Idle;
}
}
#[derive(Component)]
pub struct Grabbable;
pub fn update_grabbables(
mut events: EventReader<InteractionEvent>,
mut grabbable_query: Query<&mut Transform, (With<Grabbable>, Without<XRDirectInteractor>)>,
interactor_query: Query<(&GlobalTransform, &XRInteractorState), Without<Grabbable>>,
) {
//so basically the idea is to try all the events?
for event in events.read() {
// info!("some event");
match grabbable_query.get_mut(event.interactable) {
Ok(mut grabbable_transform) => {
// info!("we got a grabbable");
//now we need the location of our interactor
match interactor_query.get(event.interactor) {
Ok(interactor_transform) => {
match interactor_transform.1 {
XRInteractorState::Idle => (),
XRInteractorState::Selecting => {
// info!("its a direct interactor?");
*grabbable_transform = interactor_transform.0.compute_transform();
}
}
}
Err(_) => {
// info!("not a direct interactor")
}
}
}
Err(_) => {
// info!("not a grabbable?")
}
}
}
}

100
flake.lock generated Normal file
View File

@@ -0,0 +1,100 @@
{
"nodes": {
"fenix": {
"inputs": {
"nixpkgs": [
"nixpkgs"
],
"rust-analyzer-src": "rust-analyzer-src"
},
"locked": {
"lastModified": 1714631076,
"narHash": "sha256-at4+1R9gx3CGvX0ZJo9GwDZyt3RzOft7qDCTsYHjI4M=",
"owner": "nix-community",
"repo": "fenix",
"rev": "22a9eb3f20dd340d084cee4426f386a90b1351ca",
"type": "github"
},
"original": {
"owner": "nix-community",
"repo": "fenix",
"type": "github"
}
},
"flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1710146030,
"narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1714656196,
"narHash": "sha256-kjQkA98lMcsom6Gbhw8SYzmwrSo+2nruiTcTZp5jK7o=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "94035b482d181af0a0f8f77823a790b256b7c3cc",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixpkgs-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"fenix": "fenix",
"flake-utils": "flake-utils",
"nixpkgs": "nixpkgs"
}
},
"rust-analyzer-src": {
"flake": false,
"locked": {
"lastModified": 1714572655,
"narHash": "sha256-xjD8vmit0Nz1qaSSSpeXOK3saSvAZtOGHS2SHZE75Ek=",
"owner": "rust-lang",
"repo": "rust-analyzer",
"rev": "cfce2bb46da62950a8b70ddb0b2a12332da1b1e1",
"type": "github"
},
"original": {
"owner": "rust-lang",
"ref": "nightly",
"repo": "rust-analyzer",
"type": "github"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
}
},
"root": "root",
"version": 7
}

115
flake.nix Normal file
View File

@@ -0,0 +1,115 @@
{
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
fenix = {
url = "github:nix-community/fenix";
inputs.nixpkgs.follows = "nixpkgs";
};
flake-utils.url = "github:numtide/flake-utils";
};
outputs = {
self,
nixpkgs,
fenix,
flake-utils,
}:
flake-utils.lib.eachDefaultSystem (
system: let
# setup pkgs
pkgs = import nixpkgs {
inherit system;
overlays = [fenix.overlays.default];
config = {
android_sdk.accept_license = true;
allowUnfree = true;
};
};
# the entire rust toolchain with required targets
rustToolchain = with fenix.packages.${system};
combine [
(stable.withComponents [
"rustc"
"cargo"
"rustfmt"
"clippy"
"rust-src"
])
targets.wasm32-unknown-unknown.stable.rust-std
targets.aarch64-linux-android.stable.rust-std
targets.x86_64-pc-windows-gnu.stable.rust-std
];
androidComposition = pkgs.androidenv.composeAndroidPackages {
abiVersions = ["arm64-v8a"];
includeNDK = true;
platformVersions = ["32"];
};
in {
devShells.default = pkgs.mkShell rec {
# build dependencies
nativeBuildInputs = with pkgs; [
# the entire rust toolchain
rustToolchain
# tool for cross compiling
cargo-apk
# xbuild
pkg-config
# Common cargo tools we often use
cargo-deny
cargo-expand
cargo-binutils
# cmake for openxr
cmake
];
# runtime dependencies
buildInputs =
[
pkgs.zstd
pkgs.libxml2
]
++ pkgs.lib.optionals pkgs.stdenv.isLinux (with pkgs; [
# bevy dependencies
udev
alsa-lib
# vulkan
vulkan-loader
vulkan-headers
vulkan-tools
vulkan-validation-layers
# x11
xorg.libX11
xorg.libXcursor
xorg.libXi
xorg.libXrandr
# wayland
libxkbcommon
wayland
# xr
openxr-loader
libGL
])
++ pkgs.lib.optionals pkgs.stdenv.isDarwin [
pkgs.darwin.apple_sdk.frameworks.Cocoa
# # This is missing on mac m1 nix, for some reason.
# # see https://stackoverflow.com/a/69732679
pkgs.libiconv
];
# android vars
ANDROID_SDK_ROOT = "${androidComposition.androidsdk}/libexec/android-sdk";
ANDROID_NDK_ROOT = "${ANDROID_SDK_ROOT}/ndk-bundle";
LD_LIBRARY_PATH = pkgs.lib.makeLibraryPath buildInputs;
# this is most likely not needed. for some reason shadows flicker without it.
AMD_VULKAN_ICD = "RADV";
};
# This only formats the nix files.
formatter = pkgs.nixpkgs-fmt;
}
);
}

View File

@@ -1 +0,0 @@
# Use defaults

View File

@@ -1,476 +0,0 @@
use std::sync::atomic::AtomicBool;
use std::sync::{Arc, Mutex};
// use anyhow::Context;
use bevy::math::uvec2;
use bevy::prelude::*;
use bevy::render::renderer::{
RenderAdapter, RenderAdapterInfo, RenderDevice, RenderQueue, WgpuWrapper,
};
use bevy::window::RawHandleWrapper;
use eyre::{Context, ContextCompat};
use openxr as xr;
use wgpu::Instance;
use wgpu_hal::{api::Dx12, Api};
use wgpu_hal::{Adapter as HalAdapter, Instance as HalInstance};
use winapi::shared::dxgiformat::{self, DXGI_FORMAT};
use winapi::um::{d3d12 as winapi_d3d12, d3dcommon};
use xr::EnvironmentBlendMode;
use crate::graphics::extensions::XrExtensions;
use crate::input::XrInput;
use crate::resources::{
OXrSessionSetupInfo, Swapchain, SwapchainInner, XrEnvironmentBlendMode, XrFormat, XrFrameState,
XrFrameWaiter, XrInstance, XrResolution, XrSession, XrSessionRunning, XrSwapchain, XrViews,
};
#[cfg(all(feature = "d3d12", windows))]
use crate::resources::D3D12OXrSessionSetupInfo;
#[cfg(feature = "vulkan")]
use crate::resources::VulkanOXrSessionSetupInfo;
use super::{XrAppInfo, XrPreferdBlendMode};
use crate::VIEW_TYPE;
pub fn initialize_xr_instance(
window: Option<RawHandleWrapper>,
xr_entry: xr::Entry,
reqeusted_extensions: XrExtensions,
available_extensions: XrExtensions,
prefered_blend_mode: XrPreferdBlendMode,
app_info: XrAppInfo,
) -> eyre::Result<(
XrInstance,
OXrSessionSetupInfo,
XrEnvironmentBlendMode,
RenderDevice,
RenderQueue,
RenderAdapterInfo,
RenderAdapter,
Instance,
)> {
#[cfg(target_os = "android")]
xr_entry.initialize_android_loader()?;
assert!(available_extensions.raw().khr_d3d12_enable);
//info!("available xr exts: {:#?}", available_extensions);
let mut enabled_extensions: xr::ExtensionSet =
(available_extensions & reqeusted_extensions).into();
enabled_extensions.khr_d3d12_enable = true;
let available_layers = xr_entry.enumerate_layers()?;
//info!("available xr layers: {:#?}", available_layers);
let xr_instance = xr_entry.create_instance(
&xr::ApplicationInfo {
application_name: &app_info.name,
engine_name: "Bevy",
..Default::default()
},
&enabled_extensions,
&[],
)?;
info!("created OpenXR instance");
let instance_props = xr_instance.properties()?;
let xr_system_id = xr_instance.system(xr::FormFactor::HEAD_MOUNTED_DISPLAY)?;
info!("created OpenXR system");
let system_props = xr_instance.system_properties(xr_system_id).unwrap();
info!(
"loaded OpenXR runtime: {} {} {}",
instance_props.runtime_name,
instance_props.runtime_version,
if system_props.system_name.is_empty() {
"<unnamed>"
} else {
&system_props.system_name
}
);
let blend_modes = xr_instance.enumerate_environment_blend_modes(xr_system_id, VIEW_TYPE)?;
let blend_mode: EnvironmentBlendMode = match prefered_blend_mode {
XrPreferdBlendMode::Opaque if blend_modes.contains(&EnvironmentBlendMode::OPAQUE) => {
bevy::log::info!("Using Opaque");
EnvironmentBlendMode::OPAQUE
}
XrPreferdBlendMode::Additive if blend_modes.contains(&EnvironmentBlendMode::ADDITIVE) => {
bevy::log::info!("Using Additive");
EnvironmentBlendMode::ADDITIVE
}
XrPreferdBlendMode::AlphaBlend
if blend_modes.contains(&EnvironmentBlendMode::ALPHA_BLEND) =>
{
bevy::log::info!("Using AlphaBlend");
EnvironmentBlendMode::ALPHA_BLEND
}
_ => {
bevy::log::info!("Using Opaque");
EnvironmentBlendMode::OPAQUE
}
};
let reqs = xr_instance.graphics_requirements::<xr::D3D12>(xr_system_id)?;
let instance_descriptor = &wgpu_hal::InstanceDescriptor {
name: &app_info.name,
dx12_shader_compiler: wgpu::util::dx12_shader_compiler_from_env().unwrap_or(
wgpu::Dx12Compiler::Dxc {
dxil_path: None,
dxc_path: None,
},
),
flags: wgpu::InstanceFlags::from_build_config().with_env(),
gles_minor_version: Default::default(),
};
let wgpu_raw_instance: wgpu_hal::dx12::Instance =
unsafe { wgpu_hal::dx12::Instance::init(instance_descriptor)? };
let wgpu_adapters: Vec<wgpu_hal::ExposedAdapter<wgpu_hal::dx12::Api>> =
unsafe { wgpu_raw_instance.enumerate_adapters() };
let wgpu_exposed_adapter = wgpu_adapters
.into_iter()
.find(|a| {
let mut desc = unsafe { std::mem::zeroed() };
unsafe { a.adapter.raw_adapter().GetDesc1(&mut desc) };
desc.AdapterLuid.HighPart == reqs.adapter_luid.HighPart
&& desc.AdapterLuid.LowPart == reqs.adapter_luid.LowPart
})
.context("failed to find DXGI adapter matching LUID provided by runtime")?;
let wgpu_instance =
unsafe { wgpu::Instance::from_hal::<wgpu_hal::api::Dx12>(wgpu_raw_instance) };
let wgpu_features = wgpu::Features::TEXTURE_ADAPTER_SPECIFIC_FORMAT_FEATURES
| wgpu::Features::MULTIVIEW
| wgpu::Features::MULTI_DRAW_INDIRECT_COUNT
| wgpu::Features::MULTI_DRAW_INDIRECT;
let wgpu_limits = wgpu::Limits {
max_bind_groups: 8,
max_storage_buffer_binding_size: wgpu_exposed_adapter
.capabilities
.limits
.max_storage_buffer_binding_size,
max_push_constant_size: 4,
..Default::default()
};
let wgpu_open_device = unsafe {
wgpu_exposed_adapter
.adapter
.open(wgpu_features, &wgpu_limits)?
};
let device_supported_feature_level: d3d12::FeatureLevel =
get_device_feature_level(wgpu_open_device.device.raw_device());
if (device_supported_feature_level as u32) < (reqs.min_feature_level as u32) {
panic!(
"OpenXR runtime requires D3D12 feature level >= {}",
reqs.min_feature_level
);
}
let (session, frame_wait, frame_stream) = unsafe {
xr_instance.create_session::<xr::D3D12>(
xr_system_id,
&xr::d3d::SessionCreateInfoD3D12 {
device: wgpu_open_device.device.raw_device().as_mut_ptr().cast(),
queue: wgpu_open_device.device.raw_queue().as_mut_ptr().cast(),
},
)
}?;
let wgpu_adapter = unsafe { wgpu_instance.create_adapter_from_hal(wgpu_exposed_adapter) };
let raw_device = wgpu_open_device.device.raw_device().as_mut_ptr();
let raw_queue = wgpu_open_device.device.raw_queue().as_mut_ptr();
let (wgpu_device, wgpu_queue) = unsafe {
wgpu_adapter.create_device_from_hal(
wgpu_open_device,
&wgpu::DeviceDescriptor {
label: Some("bevy_oxr device"),
required_features: wgpu_features,
required_limits: wgpu_limits,
},
None,
)?
};
Ok((
xr_instance.into(),
OXrSessionSetupInfo::D3D12(D3D12OXrSessionSetupInfo {
raw_device,
raw_queue,
xr_system_id,
}),
blend_mode.into(),
wgpu_device.into(),
RenderQueue(Arc::new(WgpuWrapper::new(wgpu_queue))),
RenderAdapterInfo(WgpuWrapper::new(wgpu_adapter.get_info())),
RenderAdapter(Arc::new(WgpuWrapper::new(wgpu_adapter))),
wgpu_instance.into(),
))
}
pub fn start_xr_session(
window: Option<RawHandleWrapper>,
ptrs: &OXrSessionSetupInfo,
xr_instance: &XrInstance,
render_device: &RenderDevice,
render_adapter: &RenderAdapter,
wgpu_instance: &Instance,
) -> eyre::Result<(
XrSession,
XrResolution,
XrFormat,
XrSessionRunning,
XrFrameWaiter,
XrSwapchain,
XrInput,
XrViews,
XrFrameState,
)> {
let wgpu_device = render_device.wgpu_device();
let wgpu_adapter = &render_adapter.0;
#[allow(unreachable_patterns)]
let setup_info = match ptrs {
OXrSessionSetupInfo::D3D12(v) => v,
_ => eyre::bail!("Wrong Graphics Api"),
};
let (session, frame_wait, frame_stream) = unsafe {
xr_instance.create_session::<xr::D3D12>(
setup_info.xr_system_id,
&xr::d3d::SessionCreateInfoD3D12 {
device: setup_info.raw_device.cast(),
queue: setup_info.raw_queue.cast(),
},
)
}?;
let views =
xr_instance.enumerate_view_configuration_views(setup_info.xr_system_id, VIEW_TYPE)?;
let surface = window.map(|wrapper| unsafe {
// SAFETY: Plugins should be set up on the main thread.
let handle = wrapper.get_handle();
wgpu_instance
.create_surface(handle)
.expect("Failed to create wgpu surface")
});
let swapchain_format = surface
.as_ref()
.map(|surface| surface.get_capabilities(wgpu_adapter).formats[0])
.unwrap_or(wgpu::TextureFormat::Rgba8UnormSrgb);
// TODO: Log swapchain format
let resolution = uvec2(
views[0].recommended_image_rect_width,
views[0].recommended_image_rect_height,
);
let handle = session
.create_swapchain(&xr::SwapchainCreateInfo {
create_flags: xr::SwapchainCreateFlags::EMPTY,
usage_flags: xr::SwapchainUsageFlags::COLOR_ATTACHMENT
| xr::SwapchainUsageFlags::SAMPLED,
format: wgpu_to_d3d12(swapchain_format).expect("Unsupported texture format"),
// The Vulkan graphics pipeline we create is not set up for multisampling,
// so we hardcode this to 1. If we used a proper multisampling setup, we
// could set this to `views[0].recommended_swapchain_sample_count`.
sample_count: 1,
width: resolution.x,
height: resolution.y,
face_count: 1,
array_size: 2,
mip_count: 1,
})
.unwrap();
let images = handle.enumerate_images().unwrap();
let buffers = images
.into_iter()
.map(|color_image| {
info!("image map swapchain");
let wgpu_hal_texture = unsafe {
<Dx12 as Api>::Device::texture_from_raw(
d3d12::ComPtr::from_raw(color_image as *mut _),
swapchain_format,
wgpu::TextureDimension::D2,
wgpu::Extent3d {
width: resolution.x,
height: resolution.y,
depth_or_array_layers: 2,
},
1,
1,
)
};
let texture = unsafe {
wgpu_device.create_texture_from_hal::<Dx12>(
wgpu_hal_texture,
&wgpu::TextureDescriptor {
label: Some("bevy_openxr swapchain"),
size: wgpu::Extent3d {
width: resolution.x,
height: resolution.y,
depth_or_array_layers: 2,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: swapchain_format,
usage: wgpu::TextureUsages::RENDER_ATTACHMENT
| wgpu::TextureUsages::COPY_DST,
view_formats: &[],
},
)
};
texture
})
.collect();
Ok((
XrSession::D3D12(session.clone()),
resolution.into(),
swapchain_format.into(),
// TODO: this shouldn't be in here
AtomicBool::new(false).into(),
frame_wait.into(),
Swapchain::D3D12(SwapchainInner {
stream: Mutex::new(frame_stream),
handle: Mutex::new(handle),
buffers,
image_index: Mutex::new(0),
})
.into(),
XrInput::new(xr_instance, &session.into_any_graphics())?,
Vec::default().into(),
// TODO: Feels wrong to return a FrameState here, we probably should just wait for the next frame
xr::FrameState {
predicted_display_time: xr::Time::from_nanos(1),
predicted_display_period: xr::Duration::from_nanos(1),
should_render: true,
}
.into(),
))
}
// Extracted from https://github.com/gfx-rs/wgpu/blob/1161a22f4fbb4fc204eb06f2ac4243f83e0e980d/wgpu-hal/src/dx12/adapter.rs#L73-L94
// license: MIT OR Apache-2.0
fn get_device_feature_level(
device: &d3d12::ComPtr<winapi_d3d12::ID3D12Device>,
) -> d3d12::FeatureLevel {
// Detect the highest supported feature level.
let d3d_feature_level = [
d3d12::FeatureLevel::L12_1,
d3d12::FeatureLevel::L12_0,
d3d12::FeatureLevel::L11_1,
d3d12::FeatureLevel::L11_0,
];
type FeatureLevelsInfo = winapi_d3d12::D3D12_FEATURE_DATA_FEATURE_LEVELS;
let mut device_levels: FeatureLevelsInfo = unsafe { std::mem::zeroed() };
device_levels.NumFeatureLevels = d3d_feature_level.len() as u32;
device_levels.pFeatureLevelsRequested = d3d_feature_level.as_ptr().cast();
unsafe {
device.CheckFeatureSupport(
winapi_d3d12::D3D12_FEATURE_FEATURE_LEVELS,
(&mut device_levels as *mut FeatureLevelsInfo).cast(),
std::mem::size_of::<FeatureLevelsInfo>() as _,
)
};
// This cast should never fail because we only requested feature levels that are already in the enum.
let max_feature_level = d3d12::FeatureLevel::try_from(device_levels.MaxSupportedFeatureLevel)
.expect("Unexpected feature level");
max_feature_level
}
fn wgpu_to_d3d12(format: wgpu::TextureFormat) -> Option<DXGI_FORMAT> {
// Copied wholesale from:
// https://github.com/gfx-rs/wgpu/blob/v0.19/wgpu-hal/src/auxil/dxgi/conv.rs#L12-L94
// license: MIT OR Apache-2.0
use wgpu::TextureFormat as Tf;
use winapi::shared::dxgiformat::*;
Some(match format {
Tf::R8Unorm => DXGI_FORMAT_R8_UNORM,
Tf::R8Snorm => DXGI_FORMAT_R8_SNORM,
Tf::R8Uint => DXGI_FORMAT_R8_UINT,
Tf::R8Sint => DXGI_FORMAT_R8_SINT,
Tf::R16Uint => DXGI_FORMAT_R16_UINT,
Tf::R16Sint => DXGI_FORMAT_R16_SINT,
Tf::R16Unorm => DXGI_FORMAT_R16_UNORM,
Tf::R16Snorm => DXGI_FORMAT_R16_SNORM,
Tf::R16Float => DXGI_FORMAT_R16_FLOAT,
Tf::Rg8Unorm => DXGI_FORMAT_R8G8_UNORM,
Tf::Rg8Snorm => DXGI_FORMAT_R8G8_SNORM,
Tf::Rg8Uint => DXGI_FORMAT_R8G8_UINT,
Tf::Rg8Sint => DXGI_FORMAT_R8G8_SINT,
Tf::Rg16Unorm => DXGI_FORMAT_R16G16_UNORM,
Tf::Rg16Snorm => DXGI_FORMAT_R16G16_SNORM,
Tf::R32Uint => DXGI_FORMAT_R32_UINT,
Tf::R32Sint => DXGI_FORMAT_R32_SINT,
Tf::R32Float => DXGI_FORMAT_R32_FLOAT,
Tf::Rg16Uint => DXGI_FORMAT_R16G16_UINT,
Tf::Rg16Sint => DXGI_FORMAT_R16G16_SINT,
Tf::Rg16Float => DXGI_FORMAT_R16G16_FLOAT,
Tf::Rgba8Unorm => DXGI_FORMAT_R8G8B8A8_UNORM,
Tf::Rgba8UnormSrgb => DXGI_FORMAT_R8G8B8A8_UNORM_SRGB,
Tf::Bgra8UnormSrgb => DXGI_FORMAT_B8G8R8A8_UNORM_SRGB,
Tf::Rgba8Snorm => DXGI_FORMAT_R8G8B8A8_SNORM,
Tf::Bgra8Unorm => DXGI_FORMAT_B8G8R8A8_UNORM,
Tf::Rgba8Uint => DXGI_FORMAT_R8G8B8A8_UINT,
Tf::Rgba8Sint => DXGI_FORMAT_R8G8B8A8_SINT,
Tf::Rgb9e5Ufloat => DXGI_FORMAT_R9G9B9E5_SHAREDEXP,
Tf::Rgb10a2Uint => DXGI_FORMAT_R10G10B10A2_UINT,
Tf::Rgb10a2Unorm => DXGI_FORMAT_R10G10B10A2_UNORM,
Tf::Rg11b10Float => DXGI_FORMAT_R11G11B10_FLOAT,
Tf::Rg32Uint => DXGI_FORMAT_R32G32_UINT,
Tf::Rg32Sint => DXGI_FORMAT_R32G32_SINT,
Tf::Rg32Float => DXGI_FORMAT_R32G32_FLOAT,
Tf::Rgba16Uint => DXGI_FORMAT_R16G16B16A16_UINT,
Tf::Rgba16Sint => DXGI_FORMAT_R16G16B16A16_SINT,
Tf::Rgba16Unorm => DXGI_FORMAT_R16G16B16A16_UNORM,
Tf::Rgba16Snorm => DXGI_FORMAT_R16G16B16A16_SNORM,
Tf::Rgba16Float => DXGI_FORMAT_R16G16B16A16_FLOAT,
Tf::Rgba32Uint => DXGI_FORMAT_R32G32B32A32_UINT,
Tf::Rgba32Sint => DXGI_FORMAT_R32G32B32A32_SINT,
Tf::Rgba32Float => DXGI_FORMAT_R32G32B32A32_FLOAT,
Tf::Stencil8 => DXGI_FORMAT_D24_UNORM_S8_UINT,
Tf::Depth16Unorm => DXGI_FORMAT_D16_UNORM,
Tf::Depth24Plus => DXGI_FORMAT_D24_UNORM_S8_UINT,
Tf::Depth24PlusStencil8 => DXGI_FORMAT_D24_UNORM_S8_UINT,
Tf::Depth32Float => DXGI_FORMAT_D32_FLOAT,
Tf::Depth32FloatStencil8 => DXGI_FORMAT_D32_FLOAT_S8X24_UINT,
Tf::NV12 => DXGI_FORMAT_NV12,
Tf::Bc1RgbaUnorm => DXGI_FORMAT_BC1_UNORM,
Tf::Bc1RgbaUnormSrgb => DXGI_FORMAT_BC1_UNORM_SRGB,
Tf::Bc2RgbaUnorm => DXGI_FORMAT_BC2_UNORM,
Tf::Bc2RgbaUnormSrgb => DXGI_FORMAT_BC2_UNORM_SRGB,
Tf::Bc3RgbaUnorm => DXGI_FORMAT_BC3_UNORM,
Tf::Bc3RgbaUnormSrgb => DXGI_FORMAT_BC3_UNORM_SRGB,
Tf::Bc4RUnorm => DXGI_FORMAT_BC4_UNORM,
Tf::Bc4RSnorm => DXGI_FORMAT_BC4_SNORM,
Tf::Bc5RgUnorm => DXGI_FORMAT_BC5_UNORM,
Tf::Bc5RgSnorm => DXGI_FORMAT_BC5_SNORM,
Tf::Bc6hRgbUfloat => DXGI_FORMAT_BC6H_UF16,
Tf::Bc6hRgbFloat => DXGI_FORMAT_BC6H_SF16,
Tf::Bc7RgbaUnorm => DXGI_FORMAT_BC7_UNORM,
Tf::Bc7RgbaUnormSrgb => DXGI_FORMAT_BC7_UNORM_SRGB,
Tf::Etc2Rgb8Unorm
| Tf::Etc2Rgb8UnormSrgb
| Tf::Etc2Rgb8A1Unorm
| Tf::Etc2Rgb8A1UnormSrgb
| Tf::Etc2Rgba8Unorm
| Tf::Etc2Rgba8UnormSrgb
| Tf::EacR11Unorm
| Tf::EacR11Snorm
| Tf::EacRg11Unorm
| Tf::EacRg11Snorm
| Tf::Astc {
block: _,
channel: _,
} => return None,
})
}

View File

@@ -1,252 +0,0 @@
use openxr::ExtensionSet;
use std::ops;
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct XrExtensions(ExtensionSet);
impl XrExtensions {
pub fn raw_mut(&mut self) -> &mut ExtensionSet {
&mut self.0
}
pub fn raw(&self) -> &ExtensionSet {
&self.0
}
pub fn enable_fb_passthrough(&mut self) -> &mut Self {
self.0.fb_passthrough = true;
self
}
pub fn disable_fb_passthrough(&mut self) -> &mut Self {
self.0.fb_passthrough = false;
self
}
pub fn enable_hand_tracking(&mut self) -> &mut Self {
self.0.ext_hand_tracking = true;
self
}
pub fn disable_hand_tracking(&mut self) -> &mut Self {
self.0.ext_hand_tracking = false;
self
}
pub fn enable_local_floor(&mut self) -> &mut Self {
self.0.ext_local_floor = true;
self
}
pub fn disable_local_floor(&mut self) -> &mut Self {
self.0.ext_local_floor = false;
self
}
}
impl From<ExtensionSet> for XrExtensions {
fn from(value: ExtensionSet) -> Self {
Self(value)
}
}
impl From<XrExtensions> for ExtensionSet {
fn from(val: XrExtensions) -> Self {
val.0
}
}
impl Default for XrExtensions {
fn default() -> Self {
let mut exts = ExtensionSet::default();
exts.ext_hand_tracking = true;
exts.ext_local_floor = true;
Self(exts)
}
}
impl ops::BitAnd for XrExtensions {
type Output = Self;
fn bitand(self, rhs: Self) -> Self::Output {
let mut out = ExtensionSet::default();
out.ext_local_floor = self.0.ext_local_floor && rhs.0.ext_local_floor;
out.almalence_digital_lens_control =
self.0.almalence_digital_lens_control && rhs.0.almalence_digital_lens_control;
out.epic_view_configuration_fov =
self.0.epic_view_configuration_fov && rhs.0.epic_view_configuration_fov;
out.ext_performance_settings =
self.0.ext_performance_settings && rhs.0.ext_performance_settings;
out.ext_thermal_query = self.0.ext_thermal_query && rhs.0.ext_thermal_query;
out.ext_debug_utils = self.0.ext_debug_utils && rhs.0.ext_debug_utils;
out.ext_eye_gaze_interaction =
self.0.ext_eye_gaze_interaction && rhs.0.ext_eye_gaze_interaction;
out.ext_view_configuration_depth_range =
self.0.ext_view_configuration_depth_range && rhs.0.ext_view_configuration_depth_range;
out.ext_conformance_automation =
self.0.ext_conformance_automation && rhs.0.ext_conformance_automation;
out.ext_hand_tracking = self.0.ext_hand_tracking && rhs.0.ext_hand_tracking;
out.ext_dpad_binding = self.0.ext_dpad_binding && rhs.0.ext_dpad_binding;
out.ext_hand_joints_motion_range =
self.0.ext_hand_joints_motion_range && rhs.0.ext_hand_joints_motion_range;
out.ext_samsung_odyssey_controller =
self.0.ext_samsung_odyssey_controller && rhs.0.ext_samsung_odyssey_controller;
out.ext_hp_mixed_reality_controller =
self.0.ext_hp_mixed_reality_controller && rhs.0.ext_hp_mixed_reality_controller;
out.ext_palm_pose = self.0.ext_palm_pose && rhs.0.ext_palm_pose;
out.ext_uuid = self.0.ext_uuid && rhs.0.ext_uuid;
// out.extx_overlay = self.0.extx_overlay && rhs.0.extx_overlay;
out.fb_composition_layer_image_layout =
self.0.fb_composition_layer_image_layout && rhs.0.fb_composition_layer_image_layout;
out.fb_composition_layer_alpha_blend =
self.0.fb_composition_layer_alpha_blend && rhs.0.fb_composition_layer_alpha_blend;
out.fb_swapchain_update_state =
self.0.fb_swapchain_update_state && rhs.0.fb_swapchain_update_state;
out.fb_composition_layer_secure_content =
self.0.fb_composition_layer_secure_content && rhs.0.fb_composition_layer_secure_content;
out.fb_display_refresh_rate =
self.0.fb_display_refresh_rate && rhs.0.fb_display_refresh_rate;
out.fb_color_space = self.0.fb_color_space && rhs.0.fb_color_space;
out.fb_hand_tracking_mesh = self.0.fb_hand_tracking_mesh && rhs.0.fb_hand_tracking_mesh;
out.fb_hand_tracking_aim = self.0.fb_hand_tracking_aim && rhs.0.fb_hand_tracking_aim;
out.fb_hand_tracking_capsules =
self.0.fb_hand_tracking_capsules && rhs.0.fb_hand_tracking_capsules;
out.fb_spatial_entity = self.0.fb_spatial_entity && rhs.0.fb_spatial_entity;
out.fb_foveation = self.0.fb_foveation && rhs.0.fb_foveation;
out.fb_foveation_configuration =
self.0.fb_foveation_configuration && rhs.0.fb_foveation_configuration;
out.fb_keyboard_tracking = self.0.fb_keyboard_tracking && rhs.0.fb_keyboard_tracking;
out.fb_triangle_mesh = self.0.fb_triangle_mesh && rhs.0.fb_triangle_mesh;
out.fb_passthrough = self.0.fb_passthrough && rhs.0.fb_passthrough;
out.fb_render_model = self.0.fb_render_model && rhs.0.fb_render_model;
out.fb_spatial_entity_query =
self.0.fb_spatial_entity_query && rhs.0.fb_spatial_entity_query;
out.fb_spatial_entity_storage =
self.0.fb_spatial_entity_storage && rhs.0.fb_spatial_entity_storage;
out.fb_foveation_vulkan = self.0.fb_foveation_vulkan && rhs.0.fb_foveation_vulkan;
out.fb_swapchain_update_state_opengl_es =
self.0.fb_swapchain_update_state_opengl_es && rhs.0.fb_swapchain_update_state_opengl_es;
out.fb_swapchain_update_state_vulkan =
self.0.fb_swapchain_update_state_vulkan && rhs.0.fb_swapchain_update_state_vulkan;
out.fb_space_warp = self.0.fb_space_warp && rhs.0.fb_space_warp;
out.fb_scene = self.0.fb_scene && rhs.0.fb_scene;
out.fb_spatial_entity_container =
self.0.fb_spatial_entity_container && rhs.0.fb_spatial_entity_container;
out.fb_passthrough_keyboard_hands =
self.0.fb_passthrough_keyboard_hands && rhs.0.fb_passthrough_keyboard_hands;
out.fb_composition_layer_settings =
self.0.fb_composition_layer_settings && rhs.0.fb_composition_layer_settings;
out.htc_vive_cosmos_controller_interaction = self.0.htc_vive_cosmos_controller_interaction
&& rhs.0.htc_vive_cosmos_controller_interaction;
out.htc_facial_tracking = self.0.htc_facial_tracking && rhs.0.htc_facial_tracking;
out.htc_vive_focus3_controller_interaction = self.0.htc_vive_focus3_controller_interaction
&& rhs.0.htc_vive_focus3_controller_interaction;
out.htc_hand_interaction = self.0.htc_hand_interaction && rhs.0.htc_hand_interaction;
out.htc_vive_wrist_tracker_interaction =
self.0.htc_vive_wrist_tracker_interaction && rhs.0.htc_vive_wrist_tracker_interaction;
// out.htcx_vive_tracker_interaction =
// self.0.htcx_vive_tracker_interaction && rhs.0.htcx_vive_tracker_interaction;
out.huawei_controller_interaction =
self.0.huawei_controller_interaction && rhs.0.huawei_controller_interaction;
out.khr_composition_layer_cube =
self.0.khr_composition_layer_cube && rhs.0.khr_composition_layer_cube;
out.khr_composition_layer_depth =
self.0.khr_composition_layer_depth && rhs.0.khr_composition_layer_depth;
out.khr_vulkan_swapchain_format_list =
self.0.khr_vulkan_swapchain_format_list && rhs.0.khr_vulkan_swapchain_format_list;
out.khr_composition_layer_cylinder =
self.0.khr_composition_layer_cylinder && rhs.0.khr_composition_layer_cylinder;
out.khr_composition_layer_equirect =
self.0.khr_composition_layer_equirect && rhs.0.khr_composition_layer_equirect;
out.khr_opengl_enable = self.0.khr_opengl_enable && rhs.0.khr_opengl_enable;
out.khr_opengl_es_enable = self.0.khr_opengl_es_enable && rhs.0.khr_opengl_es_enable;
out.khr_vulkan_enable = self.0.khr_vulkan_enable && rhs.0.khr_vulkan_enable;
out.khr_visibility_mask = self.0.khr_visibility_mask && rhs.0.khr_visibility_mask;
out.khr_composition_layer_color_scale_bias = self.0.khr_composition_layer_color_scale_bias
&& rhs.0.khr_composition_layer_color_scale_bias;
out.khr_convert_timespec_time =
self.0.khr_convert_timespec_time && rhs.0.khr_convert_timespec_time;
out.khr_loader_init = self.0.khr_loader_init && rhs.0.khr_loader_init;
out.khr_vulkan_enable2 = self.0.khr_vulkan_enable2 && rhs.0.khr_vulkan_enable2;
out.khr_composition_layer_equirect2 =
self.0.khr_composition_layer_equirect2 && rhs.0.khr_composition_layer_equirect2;
out.khr_binding_modification =
self.0.khr_binding_modification && rhs.0.khr_binding_modification;
out.khr_swapchain_usage_input_attachment_bit =
self.0.khr_swapchain_usage_input_attachment_bit
&& rhs.0.khr_swapchain_usage_input_attachment_bit;
out.meta_vulkan_swapchain_create_info =
self.0.meta_vulkan_swapchain_create_info && rhs.0.meta_vulkan_swapchain_create_info;
out.meta_performance_metrics =
self.0.meta_performance_metrics && rhs.0.meta_performance_metrics;
out.ml_ml2_controller_interaction =
self.0.ml_ml2_controller_interaction && rhs.0.ml_ml2_controller_interaction;
out.mnd_headless = self.0.mnd_headless && rhs.0.mnd_headless;
out.mnd_swapchain_usage_input_attachment_bit =
self.0.mnd_swapchain_usage_input_attachment_bit
&& rhs.0.mnd_swapchain_usage_input_attachment_bit;
// out.mndx_egl_enable = self.0.mndx_egl_enable && rhs.0.mndx_egl_enable;
out.msft_unbounded_reference_space =
self.0.msft_unbounded_reference_space && rhs.0.msft_unbounded_reference_space;
out.msft_spatial_anchor = self.0.msft_spatial_anchor && rhs.0.msft_spatial_anchor;
out.msft_spatial_graph_bridge =
self.0.msft_spatial_graph_bridge && rhs.0.msft_spatial_graph_bridge;
out.msft_hand_interaction = self.0.msft_hand_interaction && rhs.0.msft_hand_interaction;
out.msft_hand_tracking_mesh =
self.0.msft_hand_tracking_mesh && rhs.0.msft_hand_tracking_mesh;
out.msft_secondary_view_configuration =
self.0.msft_secondary_view_configuration && rhs.0.msft_secondary_view_configuration;
out.msft_first_person_observer =
self.0.msft_first_person_observer && rhs.0.msft_first_person_observer;
out.msft_controller_model = self.0.msft_controller_model && rhs.0.msft_controller_model;
out.msft_composition_layer_reprojection =
self.0.msft_composition_layer_reprojection && rhs.0.msft_composition_layer_reprojection;
out.msft_spatial_anchor_persistence =
self.0.msft_spatial_anchor_persistence && rhs.0.msft_spatial_anchor_persistence;
out.oculus_audio_device_guid =
self.0.oculus_audio_device_guid && rhs.0.oculus_audio_device_guid;
out.ultraleap_hand_tracking_forearm =
self.0.ultraleap_hand_tracking_forearm && rhs.0.ultraleap_hand_tracking_forearm;
out.valve_analog_threshold = self.0.valve_analog_threshold && rhs.0.valve_analog_threshold;
out.varjo_quad_views = self.0.varjo_quad_views && rhs.0.varjo_quad_views;
out.varjo_foveated_rendering =
self.0.varjo_foveated_rendering && rhs.0.varjo_foveated_rendering;
out.varjo_composition_layer_depth_test =
self.0.varjo_composition_layer_depth_test && rhs.0.varjo_composition_layer_depth_test;
out.varjo_environment_depth_estimation =
self.0.varjo_environment_depth_estimation && rhs.0.varjo_environment_depth_estimation;
out.varjo_marker_tracking = self.0.varjo_marker_tracking && rhs.0.varjo_marker_tracking;
out.varjo_view_offset = self.0.varjo_view_offset && rhs.0.varjo_view_offset;
and_android_only_exts(&self, &rhs, &mut out);
and_windows_only_exts(&self, &rhs, &mut out);
for ext in self.0.other {
if rhs.0.other.contains(&ext) {
out.other.push(ext);
}
}
Self(out)
}
}
#[cfg(not(target_os = "android"))]
fn and_android_only_exts(lhs: &XrExtensions, rhs: &XrExtensions, out: &mut ExtensionSet) {}
#[cfg(not(windows))]
fn and_windows_only_exts(lhs: &XrExtensions, rhs: &XrExtensions, out: &mut ExtensionSet) {}
#[cfg(target_os = "android")]
fn and_android_only_exts(lhs: &XrExtensions, rhs: &XrExtensions, out: &mut ExtensionSet) {
out.oculus_android_session_state_enable =
lhs.0.oculus_android_session_state_enable && rhs.0.oculus_android_session_state_enable;
out.khr_loader_init_android = lhs.0.khr_loader_init_android && rhs.0.khr_loader_init_android;
out.fb_android_surface_swapchain_create =
lhs.0.fb_android_surface_swapchain_create && rhs.0.fb_android_surface_swapchain_create;
out.fb_swapchain_update_state_android_surface = lhs.0.fb_swapchain_update_state_android_surface
&& rhs.0.fb_swapchain_update_state_android_surface;
out.khr_android_thread_settings =
lhs.0.khr_android_thread_settings && rhs.0.khr_android_thread_settings;
out.khr_android_surface_swapchain =
lhs.0.khr_android_surface_swapchain && rhs.0.khr_android_surface_swapchain;
out.khr_android_create_instance =
lhs.0.khr_android_create_instance && rhs.0.khr_android_create_instance;
}
#[cfg(windows)]
fn and_windows_only_exts(lhs: &XrExtensions, rhs: &XrExtensions, out: &mut ExtensionSet) {
out.ext_win32_appcontainer_compatible =
lhs.0.ext_win32_appcontainer_compatible && rhs.0.ext_win32_appcontainer_compatible;
out.khr_d3d11_enable = lhs.0.khr_d3d11_enable && rhs.0.khr_d3d11_enable;
out.khr_d3d12_enable = lhs.0.khr_d3d12_enable && rhs.0.khr_d3d12_enable;
out.khr_win32_convert_performance_counter_time =
lhs.0.khr_win32_convert_performance_counter_time
&& rhs.0.khr_win32_convert_performance_counter_time;
out.msft_perception_anchor_interop =
lhs.0.msft_perception_anchor_interop && rhs.0.msft_perception_anchor_interop;
out.msft_holographic_window_attachment =
lhs.0.msft_holographic_window_attachment && rhs.0.msft_holographic_window_attachment;
}

View File

@@ -1,243 +0,0 @@
pub mod extensions;
#[cfg(all(feature = "d3d12", windows))]
mod d3d12;
#[cfg(feature = "vulkan")]
mod vulkan;
use std::sync::Arc;
use bevy::ecs::query::With;
use bevy::ecs::system::{Query, SystemState};
use bevy::ecs::world::World;
use bevy::render::renderer::{
RenderAdapter, RenderAdapterInfo, RenderDevice, RenderInstance, RenderQueue, WgpuWrapper,
};
use bevy::window::{PrimaryWindow, RawHandleWrapper};
use wgpu::Instance;
use crate::input::XrInput;
use crate::resources::{
XrEnvironmentBlendMode, XrFormat, XrFrameState, XrFrameWaiter, XrInstance, XrResolution,
XrSession, XrSessionRunning, XrSwapchain, XrViews,
};
use crate::OXrSessionSetupInfo;
use crate::Backend;
use openxr as xr;
use self::extensions::XrExtensions;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum XrPreferdBlendMode {
Opaque,
Additive,
AlphaBlend,
}
impl Default for XrPreferdBlendMode {
fn default() -> Self {
Self::Opaque
}
}
#[derive(Clone, Debug)]
pub struct XrAppInfo {
pub name: String,
}
impl Default for XrAppInfo {
fn default() -> Self {
Self {
name: "Ambient".into(),
}
}
}
pub fn start_xr_session(
window: Option<RawHandleWrapper>,
session_setup_data: &OXrSessionSetupInfo,
xr_instance: &XrInstance,
render_device: &RenderDevice,
render_adapter: &RenderAdapter,
wgpu_instance: &Instance,
) -> eyre::Result<(
XrSession,
XrResolution,
XrFormat,
XrSessionRunning,
XrFrameWaiter,
XrSwapchain,
XrInput,
XrViews,
XrFrameState,
)> {
match session_setup_data {
#[cfg(feature = "vulkan")]
OXrSessionSetupInfo::Vulkan(_) => vulkan::start_xr_session(
window,
session_setup_data,
xr_instance,
render_device,
render_adapter,
wgpu_instance,
),
#[cfg(all(feature = "d3d12", windows))]
OXrSessionSetupInfo::D3D12(_) => d3d12::start_xr_session(
window,
session_setup_data,
xr_instance,
render_device,
render_adapter,
wgpu_instance,
),
}
}
pub fn initialize_xr_instance(
backend_preference: &[Backend],
window: Option<RawHandleWrapper>,
reqeusted_extensions: XrExtensions,
prefered_blend_mode: XrPreferdBlendMode,
app_info: XrAppInfo,
) -> eyre::Result<(
XrInstance,
OXrSessionSetupInfo,
XrEnvironmentBlendMode,
RenderDevice,
RenderQueue,
RenderAdapterInfo,
RenderAdapter,
Instance,
)> {
if backend_preference.is_empty() {
eyre::bail!("Cannot initialize with no backend selected");
}
let xr_entry = xr_entry()?;
#[cfg(target_os = "android")]
xr_entry.initialize_android_loader()?;
let available_extensions: XrExtensions = xr_entry.enumerate_extensions()?.into();
for backend in backend_preference {
match backend {
#[cfg(feature = "vulkan")]
Backend::Vulkan => {
if !available_extensions.raw().khr_vulkan_enable2 {
continue;
}
return vulkan::initialize_xr_instance(
window,
xr_entry,
reqeusted_extensions,
available_extensions,
prefered_blend_mode,
app_info,
);
}
#[cfg(all(feature = "d3d12", windows))]
Backend::D3D12 => {
if !available_extensions.raw().khr_d3d12_enable {
continue;
}
return d3d12::initialize_xr_instance(
window,
xr_entry,
reqeusted_extensions,
available_extensions,
prefered_blend_mode,
app_info,
);
}
}
}
eyre::bail!(
"No selected backend was supported by the runtime. Selected: {:?}",
backend_preference
);
}
pub fn try_full_init(
world: &mut World,
backend_preference: &[Backend],
reqeusted_extensions: XrExtensions,
prefered_blend_mode: XrPreferdBlendMode,
app_info: XrAppInfo,
) -> eyre::Result<(
RenderDevice,
RenderQueue,
RenderAdapterInfo,
RenderAdapter,
RenderInstance,
)> {
let mut system_state: SystemState<Query<&RawHandleWrapper, With<PrimaryWindow>>> =
SystemState::new(world);
let primary_window = system_state.get(world).get_single().ok().cloned();
let (
xr_instance,
setup_info,
blend_mode,
render_device,
render_queue,
render_adapter_info,
render_adapter,
wgpu_instance,
) = initialize_xr_instance(
backend_preference,
primary_window.clone(),
reqeusted_extensions,
prefered_blend_mode,
app_info,
)?;
world.insert_resource(xr_instance);
world.insert_non_send_resource(setup_info);
// TODO: move BlendMode the session init?
world.insert_resource(blend_mode);
let setup_info = world
.get_non_send_resource::<OXrSessionSetupInfo>()
.unwrap();
let xr_instance = world.get_resource::<XrInstance>().unwrap();
let (
xr_session,
xr_resolution,
xr_format,
xr_session_running,
xr_frame_waiter,
xr_swapchain,
xr_input,
xr_views,
xr_frame_state,
) = start_xr_session(
primary_window,
setup_info,
xr_instance,
&render_device,
&render_adapter,
&wgpu_instance,
)?;
world.insert_resource(xr_session);
world.insert_resource(xr_resolution);
world.insert_resource(xr_format);
world.insert_resource(xr_session_running);
world.insert_resource(xr_frame_waiter);
world.insert_resource(xr_swapchain);
world.insert_resource(xr_input);
world.insert_resource(xr_views);
world.insert_resource(xr_frame_state);
Ok((
render_device,
render_queue,
render_adapter_info,
render_adapter,
RenderInstance(Arc::new(WgpuWrapper::new(wgpu_instance))),
))
}
pub fn xr_entry() -> eyre::Result<xr::Entry> {
#[cfg(windows)]
let entry = Ok(xr::Entry::linked());
#[cfg(not(windows))]
let entry = unsafe { xr::Entry::load().map_err(|e| eyre::eyre!(e)) };
entry
}

View File

@@ -1,606 +0,0 @@
use std::ffi::{c_void, CString};
use std::sync::atomic::AtomicBool;
use std::sync::{Arc, Mutex};
// use anyhow::Context;
use ash::vk::{self, Handle};
use bevy::math::uvec2;
use bevy::prelude::*;
use bevy::render::renderer::{
RenderAdapter, RenderAdapterInfo, RenderDevice, RenderQueue, WgpuWrapper,
};
use bevy::window::RawHandleWrapper;
use eyre::{Context, ContextCompat};
use openxr as xr;
use wgpu::Instance;
use wgpu_hal::{api::Vulkan as V, Api};
use xr::EnvironmentBlendMode;
use crate::graphics::extensions::XrExtensions;
use crate::input::XrInput;
use crate::resources::{
OXrSessionSetupInfo, Swapchain, SwapchainInner, VulkanOXrSessionSetupInfo,
XrEnvironmentBlendMode, XrFormat, XrFrameState, XrFrameWaiter, XrInstance, XrResolution,
XrSession, XrSessionRunning, XrSwapchain, XrViews,
};
use crate::VIEW_TYPE;
use super::{XrAppInfo, XrPreferdBlendMode};
pub fn initialize_xr_instance(
window: Option<RawHandleWrapper>,
xr_entry: xr::Entry,
reqeusted_extensions: XrExtensions,
available_extensions: XrExtensions,
prefered_blend_mode: XrPreferdBlendMode,
app_info: XrAppInfo,
) -> eyre::Result<(
XrInstance,
OXrSessionSetupInfo,
XrEnvironmentBlendMode,
RenderDevice,
RenderQueue,
RenderAdapterInfo,
RenderAdapter,
Instance,
)> {
#[cfg(target_os = "android")]
xr_entry.initialize_android_loader()?;
assert!(available_extensions.raw().khr_vulkan_enable2);
// info!("available OpenXR extensions: {:#?}", available_extensions);
let mut enabled_extensions: xr::ExtensionSet =
(available_extensions & reqeusted_extensions).into();
enabled_extensions.khr_vulkan_enable2 = true;
#[cfg(target_os = "android")]
{
enabled_extensions.khr_android_create_instance = true;
}
let available_layers = xr_entry.enumerate_layers()?;
// info!("available OpenXR layers: {:#?}", available_layers);
let xr_instance = xr_entry.create_instance(
&xr::ApplicationInfo {
application_name: &app_info.name,
engine_name: "Bevy",
..Default::default()
},
&enabled_extensions,
&[],
)?;
info!("created OpenXR instance");
let instance_props = xr_instance.properties()?;
let xr_system_id = xr_instance.system(xr::FormFactor::HEAD_MOUNTED_DISPLAY)?;
info!("created OpenXR system");
let system_props = xr_instance.system_properties(xr_system_id).unwrap();
info!(
"loaded OpenXR runtime: {} {} {}",
instance_props.runtime_name,
instance_props.runtime_version,
if system_props.system_name.is_empty() {
"<unnamed>"
} else {
&system_props.system_name
}
);
let blend_modes = xr_instance.enumerate_environment_blend_modes(xr_system_id, VIEW_TYPE)?;
let blend_mode: EnvironmentBlendMode = match prefered_blend_mode {
XrPreferdBlendMode::Opaque if blend_modes.contains(&EnvironmentBlendMode::OPAQUE) => {
bevy::log::info!("Using Opaque");
EnvironmentBlendMode::OPAQUE
}
XrPreferdBlendMode::Additive if blend_modes.contains(&EnvironmentBlendMode::ADDITIVE) => {
bevy::log::info!("Using Additive");
EnvironmentBlendMode::ADDITIVE
}
XrPreferdBlendMode::AlphaBlend
if blend_modes.contains(&EnvironmentBlendMode::ALPHA_BLEND) =>
{
bevy::log::info!("Using AlphaBlend");
EnvironmentBlendMode::ALPHA_BLEND
}
_ => {
bevy::log::info!("Using Opaque");
EnvironmentBlendMode::OPAQUE
}
};
#[cfg(not(target_os = "android"))]
let vk_target_version = vk::make_api_version(0, 1, 2, 0);
#[cfg(not(target_os = "android"))]
let vk_target_version_xr = xr::Version::new(1, 2, 0);
#[cfg(target_os = "android")]
let vk_target_version = vk::make_api_version(0, 1, 1, 0);
#[cfg(target_os = "android")]
let vk_target_version_xr = xr::Version::new(1, 1, 0);
let reqs = xr_instance.graphics_requirements::<xr::Vulkan>(xr_system_id)?;
if vk_target_version_xr < reqs.min_api_version_supported
|| vk_target_version_xr.major() > reqs.max_api_version_supported.major()
{
panic!(
"OpenXR runtime requires Vulkan version >= {}, < {}.0.0",
reqs.min_api_version_supported,
reqs.max_api_version_supported.major() + 1
);
}
let vk_entry = unsafe { ash::Entry::load() }?;
let flags = wgpu::InstanceFlags::from_build_config();
let extensions = <V as Api>::Instance::desired_extensions(&vk_entry, vk_target_version, flags)?;
let device_extensions = vec![
ash::extensions::khr::Swapchain::name(),
ash::extensions::khr::DrawIndirectCount::name(),
#[cfg(target_os = "android")]
ash::extensions::khr::TimelineSemaphore::name(),
];
info!(
"creating Vulkan instance with these extensions: {:#?}",
extensions
);
let vk_instance = unsafe {
let extensions_cchar: Vec<_> = extensions.iter().map(|s| s.as_ptr()).collect();
let app_name = CString::new(app_info.name)?;
let vk_app_info = vk::ApplicationInfo::builder()
.application_name(&app_name)
.application_version(1)
.engine_name(&app_name)
.engine_version(1)
.api_version(vk_target_version);
let vk_instance = xr_instance
.create_vulkan_instance(
xr_system_id,
std::mem::transmute(vk_entry.static_fn().get_instance_proc_addr),
&vk::InstanceCreateInfo::builder()
.application_info(&vk_app_info)
.enabled_extension_names(&extensions_cchar) as *const _
as *const _,
)
.context("OpenXR error creating Vulkan instance")
.unwrap()
.map_err(vk::Result::from_raw)
.context("Vulkan error creating Vulkan instance")
.unwrap();
ash::Instance::load(
vk_entry.static_fn(),
vk::Instance::from_raw(vk_instance as _),
)
};
info!("created Vulkan instance");
let vk_instance_ptr = vk_instance.handle().as_raw() as *const c_void;
let vk_physical_device = vk::PhysicalDevice::from_raw(unsafe {
xr_instance.vulkan_graphics_device(xr_system_id, vk_instance.handle().as_raw() as _)? as _
});
let vk_physical_device_ptr = vk_physical_device.as_raw() as *const c_void;
let vk_device_properties =
unsafe { vk_instance.get_physical_device_properties(vk_physical_device) };
if vk_device_properties.api_version < vk_target_version {
unsafe { vk_instance.destroy_instance(None) }
panic!("Vulkan physical device doesn't support version 1.1");
}
let wgpu_vk_instance = unsafe {
<V as Api>::Instance::from_raw(
vk_entry.clone(),
vk_instance.clone(),
vk_target_version,
0,
None,
extensions,
flags,
false,
Some(Box::new(())),
)?
};
let wgpu_features = wgpu::Features::TEXTURE_ADAPTER_SPECIFIC_FORMAT_FEATURES
| wgpu::Features::MULTIVIEW
| wgpu::Features::MULTI_DRAW_INDIRECT_COUNT
| wgpu::Features::MULTI_DRAW_INDIRECT;
let wgpu_exposed_adapter = wgpu_vk_instance
.expose_adapter(vk_physical_device)
.context("failed to expose adapter")?;
let enabled_extensions = wgpu_exposed_adapter
.adapter
.required_device_extensions(wgpu_features);
let (wgpu_open_device, vk_device_ptr, queue_family_index) = {
let extensions_cchar: Vec<_> = device_extensions.iter().map(|s| s.as_ptr()).collect();
let mut enabled_phd_features = wgpu_exposed_adapter
.adapter
.physical_device_features(&enabled_extensions, wgpu_features);
let family_index = 0;
let family_info = vk::DeviceQueueCreateInfo::builder()
.queue_family_index(family_index)
.queue_priorities(&[1.0])
.build();
let family_infos = [family_info];
let info = enabled_phd_features
.add_to_device_create_builder(
vk::DeviceCreateInfo::builder()
.queue_create_infos(&family_infos)
.push_next(&mut vk::PhysicalDeviceMultiviewFeatures {
multiview: vk::TRUE,
..Default::default()
}),
)
.enabled_extension_names(&extensions_cchar)
.build();
let vk_device = unsafe {
let vk_device = xr_instance
.create_vulkan_device(
xr_system_id,
std::mem::transmute(vk_entry.static_fn().get_instance_proc_addr),
vk_physical_device.as_raw() as _,
&info as *const _ as *const _,
)
.context("OpenXR error creating Vulkan device")?
.map_err(vk::Result::from_raw)
.context("Vulkan error creating Vulkan device")?;
ash::Device::load(vk_instance.fp_v1_0(), vk::Device::from_raw(vk_device as _))
};
let vk_device_ptr = vk_device.handle().as_raw() as *const c_void;
let wgpu_open_device = unsafe {
wgpu_exposed_adapter.adapter.device_from_raw(
vk_device,
true,
&enabled_extensions,
wgpu_features,
family_info.queue_family_index,
0,
)
}?;
(
wgpu_open_device,
vk_device_ptr,
family_info.queue_family_index,
)
};
let wgpu_instance =
unsafe { wgpu::Instance::from_hal::<wgpu_hal::api::Vulkan>(wgpu_vk_instance) };
let wgpu_adapter = unsafe { wgpu_instance.create_adapter_from_hal(wgpu_exposed_adapter) };
let (wgpu_device, wgpu_queue) = unsafe {
wgpu_adapter.create_device_from_hal(
wgpu_open_device,
&wgpu::DeviceDescriptor {
label: None,
required_features: wgpu_features,
required_limits: wgpu::Limits {
max_bind_groups: 8,
max_storage_buffer_binding_size: wgpu_adapter
.limits()
.max_storage_buffer_binding_size,
max_push_constant_size: 4,
..Default::default()
},
},
None,
)
}?;
Ok((
xr_instance.into(),
OXrSessionSetupInfo::Vulkan(VulkanOXrSessionSetupInfo {
device_ptr: vk_device_ptr,
physical_device_ptr: vk_physical_device_ptr,
vk_instance_ptr,
queue_family_index,
xr_system_id,
}),
blend_mode.into(),
wgpu_device.into(),
RenderQueue(Arc::new(WgpuWrapper::new(wgpu_queue))),
RenderAdapterInfo(WgpuWrapper::new(wgpu_adapter.get_info())),
RenderAdapter(Arc::new(WgpuWrapper::new(wgpu_adapter))),
wgpu_instance.into(),
))
}
pub fn start_xr_session(
window: Option<RawHandleWrapper>,
ptrs: &OXrSessionSetupInfo,
xr_instance: &XrInstance,
render_device: &RenderDevice,
render_adapter: &RenderAdapter,
wgpu_instance: &Instance,
) -> eyre::Result<(
XrSession,
XrResolution,
XrFormat,
XrSessionRunning,
XrFrameWaiter,
XrSwapchain,
XrInput,
XrViews,
XrFrameState,
)> {
let wgpu_device = render_device.wgpu_device();
let wgpu_adapter = &render_adapter.0;
#[allow(unreachable_patterns)]
let setup_info = match ptrs {
OXrSessionSetupInfo::Vulkan(v) => v,
_ => eyre::bail!("Wrong Graphics Api"),
};
let (session, frame_wait, frame_stream) = unsafe {
xr_instance.create_session::<xr::Vulkan>(
xr_instance.system(xr::FormFactor::HEAD_MOUNTED_DISPLAY)?,
&xr::vulkan::SessionCreateInfo {
instance: setup_info.vk_instance_ptr,
physical_device: setup_info.physical_device_ptr,
device: setup_info.device_ptr,
queue_family_index: setup_info.queue_family_index,
queue_index: 0,
},
)
}?;
let views =
xr_instance.enumerate_view_configuration_views(setup_info.xr_system_id, VIEW_TYPE)?;
let surface = window.map(|wrapper| unsafe {
// SAFETY: Plugins should be set up on the main thread.
let handle = wrapper.get_handle();
wgpu_instance
.create_surface(handle)
.expect("Failed to create wgpu surface")
});
let swapchain_format = surface
.as_ref()
.map(|surface| surface.get_capabilities(wgpu_adapter).formats[0])
.unwrap_or(wgpu::TextureFormat::Rgba8UnormSrgb);
// TODO: Log swapchain format
let resolution = uvec2(
views[0].recommended_image_rect_width,
views[0].recommended_image_rect_height,
);
let handle = session
.create_swapchain(&xr::SwapchainCreateInfo {
create_flags: xr::SwapchainCreateFlags::EMPTY,
usage_flags: xr::SwapchainUsageFlags::COLOR_ATTACHMENT
| xr::SwapchainUsageFlags::SAMPLED,
format: wgpu_to_vulkan(swapchain_format).as_raw() as _,
// The Vulkan graphics pipeline we create is not set up for multisampling,
// so we hardcode this to 1. If we used a proper multisampling setup, we
// could set this to `views[0].recommended_swapchain_sample_count`.
sample_count: 1,
width: resolution.x,
height: resolution.y,
face_count: 1,
array_size: 2,
mip_count: 1,
})
.unwrap();
let images = handle.enumerate_images().unwrap();
let buffers = images
.into_iter()
.map(|color_image| {
info!("image map swapchain");
let color_image = vk::Image::from_raw(color_image);
let wgpu_hal_texture = unsafe {
<V as Api>::Device::texture_from_raw(
color_image,
&wgpu_hal::TextureDescriptor {
label: Some("bevy_openxr swapchain"), // unused internally
size: wgpu::Extent3d {
width: resolution.x,
height: resolution.y,
depth_or_array_layers: 2,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: swapchain_format,
usage: wgpu_hal::TextureUses::COLOR_TARGET
| wgpu_hal::TextureUses::COPY_DST,
memory_flags: wgpu_hal::MemoryFlags::empty(),
view_formats: vec![],
},
None,
)
};
let texture = unsafe {
wgpu_device.create_texture_from_hal::<V>(
wgpu_hal_texture,
&wgpu::TextureDescriptor {
label: Some("bevy_openxr swapchain"),
size: wgpu::Extent3d {
width: resolution.x,
height: resolution.y,
depth_or_array_layers: 2,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: swapchain_format,
usage: wgpu::TextureUsages::RENDER_ATTACHMENT
| wgpu::TextureUsages::COPY_DST,
view_formats: &[],
},
)
};
texture
})
.collect();
Ok((
XrSession::Vulkan(session.clone()),
resolution.into(),
swapchain_format.into(),
// TODO: this shouldn't be in here
AtomicBool::new(false).into(),
frame_wait.into(),
Swapchain::Vulkan(SwapchainInner {
stream: Mutex::new(frame_stream),
handle: Mutex::new(handle),
buffers,
image_index: Mutex::new(0),
})
.into(),
XrInput::new(xr_instance, &session.into_any_graphics())?,
Vec::default().into(),
// TODO: Feels wrong to return a FrameState here, we probably should just wait for the next frame
xr::FrameState {
predicted_display_time: xr::Time::from_nanos(1),
predicted_display_period: xr::Duration::from_nanos(1),
should_render: true,
}
.into(),
))
}
fn wgpu_to_vulkan(format: wgpu::TextureFormat) -> vk::Format {
// Copied with minor modification from:
// https://github.com/gfx-rs/wgpu/blob/v0.19/wgpu-hal/src/vulkan/conv.rs#L5C1-L153
// license: MIT OR Apache-2.0
use ash::vk::Format as F;
use wgpu::TextureFormat as Tf;
use wgpu::{AstcBlock, AstcChannel};
match format {
Tf::R8Unorm => F::R8_UNORM,
Tf::R8Snorm => F::R8_SNORM,
Tf::R8Uint => F::R8_UINT,
Tf::R8Sint => F::R8_SINT,
Tf::R16Uint => F::R16_UINT,
Tf::R16Sint => F::R16_SINT,
Tf::R16Unorm => F::R16_UNORM,
Tf::R16Snorm => F::R16_SNORM,
Tf::R16Float => F::R16_SFLOAT,
Tf::Rg8Unorm => F::R8G8_UNORM,
Tf::Rg8Snorm => F::R8G8_SNORM,
Tf::Rg8Uint => F::R8G8_UINT,
Tf::Rg8Sint => F::R8G8_SINT,
Tf::Rg16Unorm => F::R16G16_UNORM,
Tf::Rg16Snorm => F::R16G16_SNORM,
Tf::R32Uint => F::R32_UINT,
Tf::R32Sint => F::R32_SINT,
Tf::R32Float => F::R32_SFLOAT,
Tf::Rg16Uint => F::R16G16_UINT,
Tf::Rg16Sint => F::R16G16_SINT,
Tf::Rg16Float => F::R16G16_SFLOAT,
Tf::Rgba8Unorm => F::R8G8B8A8_UNORM,
Tf::Rgba8UnormSrgb => F::R8G8B8A8_SRGB,
Tf::Bgra8UnormSrgb => F::B8G8R8A8_SRGB,
Tf::Rgba8Snorm => F::R8G8B8A8_SNORM,
Tf::Bgra8Unorm => F::B8G8R8A8_UNORM,
Tf::Rgba8Uint => F::R8G8B8A8_UINT,
Tf::Rgba8Sint => F::R8G8B8A8_SINT,
Tf::Rgb10a2Uint => F::A2B10G10R10_UINT_PACK32,
Tf::Rgb10a2Unorm => F::A2B10G10R10_UNORM_PACK32,
Tf::Rg11b10Float => F::B10G11R11_UFLOAT_PACK32,
Tf::Rg32Uint => F::R32G32_UINT,
Tf::Rg32Sint => F::R32G32_SINT,
Tf::Rg32Float => F::R32G32_SFLOAT,
Tf::Rgba16Uint => F::R16G16B16A16_UINT,
Tf::Rgba16Sint => F::R16G16B16A16_SINT,
Tf::Rgba16Unorm => F::R16G16B16A16_UNORM,
Tf::Rgba16Snorm => F::R16G16B16A16_SNORM,
Tf::Rgba16Float => F::R16G16B16A16_SFLOAT,
Tf::Rgba32Uint => F::R32G32B32A32_UINT,
Tf::Rgba32Sint => F::R32G32B32A32_SINT,
Tf::Rgba32Float => F::R32G32B32A32_SFLOAT,
Tf::Depth32Float => F::D32_SFLOAT,
Tf::Depth32FloatStencil8 => F::D32_SFLOAT_S8_UINT,
Tf::Depth24Plus | Tf::Depth24PlusStencil8 | Tf::Stencil8 => {
panic!("Cannot convert format that is dependent on device properties")
}
Tf::Depth16Unorm => F::D16_UNORM,
Tf::NV12 => F::G8_B8R8_2PLANE_420_UNORM,
Tf::Rgb9e5Ufloat => F::E5B9G9R9_UFLOAT_PACK32,
Tf::Bc1RgbaUnorm => F::BC1_RGBA_UNORM_BLOCK,
Tf::Bc1RgbaUnormSrgb => F::BC1_RGBA_SRGB_BLOCK,
Tf::Bc2RgbaUnorm => F::BC2_UNORM_BLOCK,
Tf::Bc2RgbaUnormSrgb => F::BC2_SRGB_BLOCK,
Tf::Bc3RgbaUnorm => F::BC3_UNORM_BLOCK,
Tf::Bc3RgbaUnormSrgb => F::BC3_SRGB_BLOCK,
Tf::Bc4RUnorm => F::BC4_UNORM_BLOCK,
Tf::Bc4RSnorm => F::BC4_SNORM_BLOCK,
Tf::Bc5RgUnorm => F::BC5_UNORM_BLOCK,
Tf::Bc5RgSnorm => F::BC5_SNORM_BLOCK,
Tf::Bc6hRgbUfloat => F::BC6H_UFLOAT_BLOCK,
Tf::Bc6hRgbFloat => F::BC6H_SFLOAT_BLOCK,
Tf::Bc7RgbaUnorm => F::BC7_UNORM_BLOCK,
Tf::Bc7RgbaUnormSrgb => F::BC7_SRGB_BLOCK,
Tf::Etc2Rgb8Unorm => F::ETC2_R8G8B8_UNORM_BLOCK,
Tf::Etc2Rgb8UnormSrgb => F::ETC2_R8G8B8_SRGB_BLOCK,
Tf::Etc2Rgb8A1Unorm => F::ETC2_R8G8B8A1_UNORM_BLOCK,
Tf::Etc2Rgb8A1UnormSrgb => F::ETC2_R8G8B8A1_SRGB_BLOCK,
Tf::Etc2Rgba8Unorm => F::ETC2_R8G8B8A8_UNORM_BLOCK,
Tf::Etc2Rgba8UnormSrgb => F::ETC2_R8G8B8A8_SRGB_BLOCK,
Tf::EacR11Unorm => F::EAC_R11_UNORM_BLOCK,
Tf::EacR11Snorm => F::EAC_R11_SNORM_BLOCK,
Tf::EacRg11Unorm => F::EAC_R11G11_UNORM_BLOCK,
Tf::EacRg11Snorm => F::EAC_R11G11_SNORM_BLOCK,
Tf::Astc { block, channel } => match channel {
AstcChannel::Unorm => match block {
AstcBlock::B4x4 => F::ASTC_4X4_UNORM_BLOCK,
AstcBlock::B5x4 => F::ASTC_5X4_UNORM_BLOCK,
AstcBlock::B5x5 => F::ASTC_5X5_UNORM_BLOCK,
AstcBlock::B6x5 => F::ASTC_6X5_UNORM_BLOCK,
AstcBlock::B6x6 => F::ASTC_6X6_UNORM_BLOCK,
AstcBlock::B8x5 => F::ASTC_8X5_UNORM_BLOCK,
AstcBlock::B8x6 => F::ASTC_8X6_UNORM_BLOCK,
AstcBlock::B8x8 => F::ASTC_8X8_UNORM_BLOCK,
AstcBlock::B10x5 => F::ASTC_10X5_UNORM_BLOCK,
AstcBlock::B10x6 => F::ASTC_10X6_UNORM_BLOCK,
AstcBlock::B10x8 => F::ASTC_10X8_UNORM_BLOCK,
AstcBlock::B10x10 => F::ASTC_10X10_UNORM_BLOCK,
AstcBlock::B12x10 => F::ASTC_12X10_UNORM_BLOCK,
AstcBlock::B12x12 => F::ASTC_12X12_UNORM_BLOCK,
},
AstcChannel::UnormSrgb => match block {
AstcBlock::B4x4 => F::ASTC_4X4_SRGB_BLOCK,
AstcBlock::B5x4 => F::ASTC_5X4_SRGB_BLOCK,
AstcBlock::B5x5 => F::ASTC_5X5_SRGB_BLOCK,
AstcBlock::B6x5 => F::ASTC_6X5_SRGB_BLOCK,
AstcBlock::B6x6 => F::ASTC_6X6_SRGB_BLOCK,
AstcBlock::B8x5 => F::ASTC_8X5_SRGB_BLOCK,
AstcBlock::B8x6 => F::ASTC_8X6_SRGB_BLOCK,
AstcBlock::B8x8 => F::ASTC_8X8_SRGB_BLOCK,
AstcBlock::B10x5 => F::ASTC_10X5_SRGB_BLOCK,
AstcBlock::B10x6 => F::ASTC_10X6_SRGB_BLOCK,
AstcBlock::B10x8 => F::ASTC_10X8_SRGB_BLOCK,
AstcBlock::B10x10 => F::ASTC_10X10_SRGB_BLOCK,
AstcBlock::B12x10 => F::ASTC_12X10_SRGB_BLOCK,
AstcBlock::B12x12 => F::ASTC_12X12_SRGB_BLOCK,
},
AstcChannel::Hdr => match block {
AstcBlock::B4x4 => F::ASTC_4X4_SFLOAT_BLOCK_EXT,
AstcBlock::B5x4 => F::ASTC_5X4_SFLOAT_BLOCK_EXT,
AstcBlock::B5x5 => F::ASTC_5X5_SFLOAT_BLOCK_EXT,
AstcBlock::B6x5 => F::ASTC_6X5_SFLOAT_BLOCK_EXT,
AstcBlock::B6x6 => F::ASTC_6X6_SFLOAT_BLOCK_EXT,
AstcBlock::B8x5 => F::ASTC_8X5_SFLOAT_BLOCK_EXT,
AstcBlock::B8x6 => F::ASTC_8X6_SFLOAT_BLOCK_EXT,
AstcBlock::B8x8 => F::ASTC_8X8_SFLOAT_BLOCK_EXT,
AstcBlock::B10x5 => F::ASTC_10X5_SFLOAT_BLOCK_EXT,
AstcBlock::B10x6 => F::ASTC_10X6_SFLOAT_BLOCK_EXT,
AstcBlock::B10x8 => F::ASTC_10X8_SFLOAT_BLOCK_EXT,
AstcBlock::B10x10 => F::ASTC_10X10_SFLOAT_BLOCK_EXT,
AstcBlock::B12x10 => F::ASTC_12X10_SFLOAT_BLOCK_EXT,
AstcBlock::B12x12 => F::ASTC_12X12_SFLOAT_BLOCK_EXT,
},
},
}
}

View File

@@ -1,86 +0,0 @@
use std::sync::Arc;
use bevy::{prelude::*, render::extract_resource::ExtractResource};
use openxr as xr;
use xr::{FrameState, FrameWaiter, ViewConfigurationType};
#[derive(Clone, Resource, ExtractResource)]
pub struct XrInput {
//pub action_set: xr::ActionSet,
//pub hand_pose: xr::Action<xr::Posef>,
//pub right_space: Arc<xr::Space>,
//pub left_space: Arc<xr::Space>,
pub stage: Arc<xr::Space>,
pub head: Arc<xr::Space>,
}
impl XrInput {
pub fn new(
instance: &xr::Instance,
session: &xr::Session<xr::AnyGraphics>,
// frame_state: &FrameState,
) -> xr::Result<Self> {
// let right_hand_subaction_path = instance.string_to_path("/user/hand/right").unwrap();
// let right_hand_grip_pose_path = instance
// .string_to_path("/user/hand/right/input/grip/pose")
// .unwrap();
// let hand_pose = action_set.create_action::<xr::Posef>(
// "hand_pose",
// "Hand Pose",
// &[left_hand_subaction_path, right_hand_subaction_path],
// )?;
// /* let left_action =
// action_set.create_action::<xr::Posef>("left_hand", "Left Hand Controller", &[])?;*/
// instance.suggest_interaction_profile_bindings(
// instance.string_to_path("/interaction_profiles/khr/simple_controller")?,
// &[
// xr::Binding::new(&hand_pose, right_hand_grip_pose_path),
// xr::Binding::new(&hand_pose, left_hand_grip_pose_path),
// ],
// )?;
//
// let right_space = hand_pose.create_space(
// session.clone(),
// right_hand_subaction_path,
// xr::Posef::IDENTITY,
// )?;
// let left_space = hand_pose.create_space(
// session.clone(),
// left_hand_subaction_path,
// xr::Posef::IDENTITY,
// )?;
let stage = match instance.exts().ext_local_floor {
None => session
.create_reference_space(xr::ReferenceSpaceType::STAGE, xr::Posef::IDENTITY)?,
Some(_) => session.create_reference_space(
xr::ReferenceSpaceType::LOCAL_FLOOR_EXT,
xr::Posef::IDENTITY,
)?,
};
let head =
session.create_reference_space(xr::ReferenceSpaceType::VIEW, xr::Posef::IDENTITY)?;
// let y = stage
// .locate(&head, frame_state.predicted_display_time).unwrap()
// .pose
// .position
// .y;
// let local = session.create_reference_space(
// xr::ReferenceSpaceType::LOCAL,
// xr::Posef {
// position: xr::Vector3f { x: 0.0, y, z: 0.0 },
// orientation: xr::Quaternionf::IDENTITY,
// },
// ).unwrap();
//session.attach_action_sets(&[&action_set])?;
//session.attach_action_sets(&[])?;
Ok(Self {
//action_set,
//hand_pose,
// right_space: Arc::new(right_space),
// left_space: Arc::new(left_space),
stage: Arc::new(stage),
head: Arc::new(head),
})
}
}

View File

@@ -1,541 +1,2 @@
pub mod graphics;
pub mod input;
pub mod passthrough;
pub mod prelude;
pub mod resource_macros;
pub mod resources;
pub mod xr_init;
pub mod xr_input;
use std::sync::atomic::AtomicBool;
use std::sync::Arc;
use crate::xr_init::{StartXrSession, XrInitPlugin};
use crate::xr_input::oculus_touch::ActionSets;
use crate::xr_input::trackers::verify_quat;
use bevy::app::{AppExit, PluginGroupBuilder};
use bevy::core::TaskPoolThreadAssignmentPolicy;
use bevy::ecs::system::SystemState;
use bevy::prelude::*;
use bevy::render::camera::{ManualTextureView, ManualTextureViewHandle, ManualTextureViews};
use bevy::render::pipelined_rendering::PipelinedRenderingPlugin;
use bevy::render::renderer::{render_system, RenderInstance, WgpuWrapper};
use bevy::render::settings::RenderCreation;
use bevy::render::{Render, RenderApp, RenderPlugin, RenderSet};
use bevy::window::{PresentMode, PrimaryWindow, RawHandleWrapper};
use graphics::extensions::XrExtensions;
use graphics::{XrAppInfo, XrPreferdBlendMode};
use input::XrInput;
use openxr as xr;
use passthrough::{PassthroughPlugin, XrPassthroughLayer, XrPassthroughState};
use resources::*;
use xr_init::{
xr_after_wait_only, xr_only, xr_render_only, CleanupRenderWorld, CleanupXrData,
ExitAppOnSessionExit, SetupXrData, StartSessionOnStartup, XrCleanup, XrEarlyInitPlugin,
XrHasWaited, XrPostCleanup, XrShouldRender, XrStatus,
};
use xr_input::actions::XrActionsPlugin;
use xr_input::hands::emulated::HandEmulationPlugin;
use xr_input::hands::hand_tracking::HandTrackingPlugin;
use xr_input::hands::HandPlugin;
use xr_input::xr_camera::XrCameraPlugin;
use xr_input::XrInputPlugin;
const VIEW_TYPE: xr::ViewConfigurationType = xr::ViewConfigurationType::PRIMARY_STEREO;
pub const LEFT_XR_TEXTURE_HANDLE: ManualTextureViewHandle = ManualTextureViewHandle(1208214591);
pub const RIGHT_XR_TEXTURE_HANDLE: ManualTextureViewHandle = ManualTextureViewHandle(3383858418);
/// Adds OpenXR support to an App
pub struct OpenXrPlugin {
pub backend_preference: Vec<Backend>,
pub reqeusted_extensions: XrExtensions,
pub prefered_blend_mode: XrPreferdBlendMode,
pub app_info: XrAppInfo,
pub synchronous_pipeline_compilation: bool,
}
impl Plugin for OpenXrPlugin {
fn build(&self, app: &mut App) {
app.insert_resource(XrSessionRunning::new(AtomicBool::new(false)));
app.insert_resource(ExitAppOnSessionExit::default());
#[cfg(not(target_arch = "wasm32"))]
match graphics::initialize_xr_instance(
&self.backend_preference,
SystemState::<Query<&RawHandleWrapper, With<PrimaryWindow>>>::new(app.world_mut())
.get(&app.world())
.get_single()
.ok()
.cloned(),
self.reqeusted_extensions.clone(),
self.prefered_blend_mode,
self.app_info.clone(),
) {
Ok((
xr_instance,
oxr_session_setup_info,
blend_mode,
device,
queue,
adapter_info,
render_adapter,
instance,
)) => {
debug!("Configured wgpu adapter Limits: {:#?}", device.limits());
debug!("Configured wgpu adapter Features: {:#?}", device.features());
warn!("Starting with OpenXR Instance");
app.insert_resource(xr_instance.clone());
app.insert_resource(blend_mode);
app.insert_resource(ActionSets(vec![]));
app.insert_resource(xr_instance);
app.insert_resource(blend_mode);
app.insert_non_send_resource(oxr_session_setup_info);
let render_instance = RenderInstance(Arc::new(WgpuWrapper::new(instance)));
app.insert_resource(render_instance.clone());
app.add_plugins(RenderPlugin {
render_creation: RenderCreation::Manual(
device,
queue,
adapter_info,
render_adapter,
render_instance,
),
// Expose this? if yes we also have to set this in the non xr case
synchronous_pipeline_compilation: self.synchronous_pipeline_compilation,
});
app.insert_resource(XrStatus::Disabled);
// app.world.send_event(StartXrSession);
}
Err(err) => {
warn!("OpenXR Instance Failed to initialize: {}", err);
app.add_plugins(RenderPlugin {
synchronous_pipeline_compilation: self.synchronous_pipeline_compilation,
..Default::default()
});
app.insert_resource(XrStatus::NoInstance);
}
}
#[cfg(target_arch = "wasm32")]
{
app.add_plugins(RenderPlugin::default());
app.insert_resource(XrStatus::Disabled);
}
app.add_systems(XrPostCleanup, clean_resources);
app.add_systems(XrPostCleanup, || info!("Main World Post Cleanup!"));
app.add_systems(
PreUpdate,
xr_poll_events.run_if(|status: Res<XrStatus>| *status != XrStatus::NoInstance),
);
app.add_systems(
PreUpdate,
(
xr_reset_per_frame_resources,
xr_wait_frame.run_if(xr_only()),
locate_views.run_if(xr_only()),
apply_deferred,
)
.chain()
.after(xr_poll_events),
);
let render_app = app.sub_app_mut(RenderApp);
render_app.add_systems(
Render,
xr_pre_frame
.run_if(xr_only())
.run_if(xr_after_wait_only())
.run_if(xr_render_only())
.before(render_system)
.after(RenderSet::ExtractCommands),
);
render_app.add_systems(
Render,
xr_end_frame
.run_if(xr_only())
.run_if(xr_after_wait_only())
.run_if(xr_render_only())
.in_set(RenderSet::Cleanup),
);
render_app.add_systems(
Render,
xr_skip_frame
.run_if(xr_only())
.run_if(xr_after_wait_only())
.run_if(not(xr_render_only()))
.in_set(RenderSet::Cleanup),
);
render_app.add_systems(
Render,
clean_resources_render
.run_if(resource_exists::<CleanupRenderWorld>)
.after(RenderSet::ExtractCommands),
);
}
}
#[cfg(all(not(feature = "vulkan"), not(all(feature = "d3d12", windows))))]
compile_error!("At least one platform-compatible backend feature must be enabled.");
#[derive(Debug)]
pub enum Backend {
#[cfg(feature = "vulkan")]
Vulkan,
#[cfg(all(feature = "d3d12", windows))]
D3D12,
}
fn clean_resources_render(cmds: &mut World) {
// let session = cmds.remove_resource::<XrSession>().unwrap();
cmds.remove_resource::<XrSession>();
cmds.remove_resource::<XrResolution>();
cmds.remove_resource::<XrFormat>();
// cmds.remove_resource::<XrSessionRunning>();
cmds.remove_resource::<XrFrameWaiter>();
cmds.remove_resource::<XrSwapchain>();
cmds.remove_resource::<XrInput>();
cmds.remove_resource::<XrViews>();
cmds.remove_resource::<XrFrameState>();
cmds.remove_resource::<CleanupRenderWorld>();
// unsafe {
// (session.instance().fp().destroy_session)(session.as_raw());
// }
warn!("Cleanup Resources Render");
}
fn clean_resources(cmds: &mut World) {
cmds.remove_resource::<XrSession>();
cmds.remove_resource::<XrResolution>();
cmds.remove_resource::<XrFormat>();
// cmds.remove_resource::<XrSessionRunning>();
cmds.remove_resource::<XrFrameWaiter>();
cmds.remove_resource::<XrSwapchain>();
cmds.remove_resource::<XrInput>();
cmds.remove_resource::<XrViews>();
cmds.remove_resource::<XrFrameState>();
// cmds.remove_resource::<CleanupRenderWorld>();
// unsafe {
// (session.instance().fp().destroy_session)(session.as_raw());
// }
warn!("Cleanup Resources");
}
fn xr_skip_frame(
xr_swapchain: Res<XrSwapchain>,
xr_frame_state: Res<XrFrameState>,
environment_blend_mode: Res<XrEnvironmentBlendMode>,
) {
let swapchain: &Swapchain = &xr_swapchain;
match swapchain {
#[cfg(feature = "vulkan")]
Swapchain::Vulkan(swap) => &swap
.stream
.lock()
.unwrap()
.end(
xr_frame_state.predicted_display_time,
**environment_blend_mode,
&[],
)
.unwrap(),
#[cfg(all(feature = "d3d12", windows))]
Swapchain::D3D12(swap) => &swap
.stream
.lock()
.unwrap()
.end(
xr_frame_state.predicted_display_time,
**environment_blend_mode,
&[],
)
.unwrap(),
};
}
pub struct DefaultXrPlugins {
pub backend_preference: Vec<Backend>,
pub reqeusted_extensions: XrExtensions,
pub prefered_blend_mode: XrPreferdBlendMode,
pub app_info: XrAppInfo,
pub synchronous_pipeline_compilation: bool,
}
impl Default for DefaultXrPlugins {
fn default() -> Self {
Self {
backend_preference: vec![
#[cfg(feature = "vulkan")]
Backend::Vulkan,
#[cfg(all(feature = "d3d12", windows))]
Backend::D3D12,
],
reqeusted_extensions: default(),
prefered_blend_mode: default(),
app_info: default(),
synchronous_pipeline_compilation: false,
}
}
}
impl PluginGroup for DefaultXrPlugins {
fn build(self) -> PluginGroupBuilder {
DefaultPlugins
.build()
.set(TaskPoolPlugin {
task_pool_options: TaskPoolOptions {
compute: TaskPoolThreadAssignmentPolicy {
min_threads: 2,
max_threads: std::usize::MAX, // unlimited max threads
percent: 1.0, // this value is irrelevant in this case
},
// keep the defaults for everything else
..default()
},
})
.disable::<RenderPlugin>()
.disable::<PipelinedRenderingPlugin>()
.add_before::<RenderPlugin, _>(OpenXrPlugin {
backend_preference: self.backend_preference,
prefered_blend_mode: self.prefered_blend_mode,
reqeusted_extensions: self.reqeusted_extensions,
app_info: self.app_info.clone(),
synchronous_pipeline_compilation: self.synchronous_pipeline_compilation,
})
.add_after::<OpenXrPlugin, _>(XrInitPlugin)
.add(XrInputPlugin)
.add(XrActionsPlugin)
.add(XrCameraPlugin)
.add_before::<OpenXrPlugin, _>(XrEarlyInitPlugin)
.add(HandPlugin)
.add(HandTrackingPlugin)
.add(HandEmulationPlugin)
.add(PassthroughPlugin)
.add(XrResourcePlugin)
.add(StartSessionOnStartup)
.set(WindowPlugin {
#[cfg(not(target_os = "android"))]
primary_window: Some(Window {
transparent: true,
present_mode: PresentMode::AutoNoVsync,
title: self.app_info.name.clone(),
..default()
}),
#[cfg(target_os = "android")]
primary_window: Some(Window {
present_mode: PresentMode::AutoNoVsync,
..default()
}),
#[cfg(target_os = "android")]
exit_condition: bevy::window::ExitCondition::DontExit,
#[cfg(target_os = "android")]
close_when_requested: true,
..default()
})
}
}
fn xr_reset_per_frame_resources(
mut should: ResMut<XrShouldRender>,
mut waited: ResMut<XrHasWaited>,
) {
**should = false;
**waited = false;
}
fn xr_poll_events(
instance: Option<Res<XrInstance>>,
session: Option<Res<XrSession>>,
session_running: Res<XrSessionRunning>,
exit_type: Res<ExitAppOnSessionExit>,
mut app_exit: EventWriter<AppExit>,
mut start_session: EventWriter<StartXrSession>,
mut setup_xr: EventWriter<SetupXrData>,
mut cleanup_xr: EventWriter<CleanupXrData>,
) {
if let (Some(instance), Some(session)) = (instance, session) {
let _span = info_span!("xr_poll_events");
while let Some(event) = instance.poll_event(&mut Default::default()).unwrap() {
use xr::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() {
xr::SessionState::READY => {
info!("Calling Session begin :3");
session.begin(VIEW_TYPE).unwrap();
setup_xr.send_default();
session_running.store(true, std::sync::atomic::Ordering::Relaxed);
}
xr::SessionState::STOPPING => {
session.end().unwrap();
session_running.store(false, std::sync::atomic::Ordering::Relaxed);
cleanup_xr.send_default();
}
xr::SessionState::EXITING => {
if *exit_type == ExitAppOnSessionExit::Always
|| *exit_type == ExitAppOnSessionExit::OnlyOnExit
{
app_exit.send_default();
}
}
xr::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 fn xr_wait_frame(
world: &mut World,
// mut frame_state: ResMut<XrFrameState>,
// mut frame_waiter: ResMut<XrFrameWaiter>,
// mut should_render: ResMut<XrShouldRender>,
// mut waited: ResMut<XrHasWaited>,
) {
let mut frame_waiter = world.get_resource_mut::<XrFrameWaiter>().unwrap();
{
let _span = info_span!("xr_wait_frame").entered();
*world.get_resource_mut::<XrFrameState>().unwrap() = match frame_waiter.wait() {
Ok(a) => a.into(),
Err(e) => {
warn!("error: {}", e);
return;
}
};
let should_render = world.get_resource::<XrFrameState>().unwrap().should_render;
**world.get_resource_mut::<XrShouldRender>().unwrap() = should_render;
**world.get_resource_mut::<XrHasWaited>().unwrap() = true;
}
world
.get_resource::<XrSwapchain>()
.unwrap()
.begin()
.unwrap();
}
pub fn xr_pre_frame(
resolution: Res<XrResolution>,
format: Res<XrFormat>,
swapchain: Res<XrSwapchain>,
mut manual_texture_views: ResMut<ManualTextureViews>,
) {
{
let _span = info_span!("xr_acquire_image").entered();
swapchain.acquire_image().unwrap()
}
{
let _span = info_span!("xr_wait_image").entered();
swapchain.wait_image().unwrap();
}
{
let _span = info_span!("xr_update_manual_texture_views").entered();
let (left, right) = swapchain.get_render_views();
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);
}
}
#[allow(clippy::too_many_arguments)]
pub fn xr_end_frame(
xr_frame_state: Res<XrFrameState>,
views: Res<XrViews>,
input: Res<XrInput>,
swapchain: Res<XrSwapchain>,
resolution: Res<XrResolution>,
environment_blend_mode: Res<XrEnvironmentBlendMode>,
passthrough_layer: Option<Res<XrPassthroughLayer>>,
passthrough_state: Option<Res<XrPassthroughState>>,
) {
#[cfg(target_os = "android")]
{
let ctx = ndk_context::android_context();
let vm = unsafe { jni::JavaVM::from_raw(ctx.vm().cast()) }.unwrap();
let env = vm.attach_current_thread_as_daemon();
}
{
let _span = info_span!("xr_release_image").entered();
swapchain.release_image().unwrap();
}
{
let _span = info_span!("xr_end_frame").entered();
let pass_layer = match passthrough_state.as_deref() {
Some(XrPassthroughState::Running) => passthrough_layer.as_deref(),
_ => None,
};
let result = swapchain.end(
xr_frame_state.predicted_display_time,
&views,
&input.stage,
**resolution,
**environment_blend_mode,
pass_layer,
);
match result {
Ok(_) => {}
Err(e) => warn!("error: {}", e),
}
}
}
pub fn locate_views(
mut views: ResMut<XrViews>,
input: Res<XrInput>,
session: Res<XrSession>,
xr_frame_state: Res<XrFrameState>,
) {
let _span = info_span!("xr_locate_views").entered();
**views = match session.locate_views(
VIEW_TYPE,
xr_frame_state.predicted_display_time,
&input.stage,
) {
Ok(this) => this
.1
.into_iter()
.map(|mut view| {
use crate::prelude::*;
let quat = view.pose.orientation.to_quat();
let fixed_quat = verify_quat(quat);
let oxr_quat = xr::Quaternionf {
x: fixed_quat.x,
y: fixed_quat.y,
z: fixed_quat.z,
w: fixed_quat.w,
};
view.pose.orientation = oxr_quat;
view
})
.collect(),
Err(err) => {
warn!("error: {}", err);
return;
}
}
}
pub use bevy_mod_openxr;
pub use bevy_mod_xr;

View File

@@ -1,229 +0,0 @@
use bevy::color::palettes;
use bevy::render::extract_resource::ExtractResource;
use bevy::{prelude::*, render::extract_resource::ExtractResourcePlugin};
use std::{marker::PhantomData, mem, ptr};
use crate::resources::XrSession;
use crate::{
resources::XrInstance,
xr_arc_resource_wrapper,
xr_init::{XrCleanup, XrSetup},
};
use openxr as xr;
use xr::{
sys::{Space, SystemPassthroughProperties2FB},
CompositionLayerBase, CompositionLayerFlags, FormFactor, Graphics,
PassthroughCapabilityFlagsFB,
};
#[derive(
Clone, Copy, Default, Debug, Resource, PartialEq, PartialOrd, Ord, Eq, Reflect, ExtractResource,
)]
pub enum XrPassthroughState {
#[default]
Unsupported,
Running,
Paused,
}
xr_arc_resource_wrapper!(XrPassthrough, xr::Passthrough);
xr_arc_resource_wrapper!(XrPassthroughLayer, xr::PassthroughLayer);
pub struct PassthroughPlugin;
impl Plugin for PassthroughPlugin {
fn build(&self, app: &mut App) {
app.add_event::<ResumePassthrough>();
app.add_event::<PausePassthrough>();
app.add_plugins(ExtractResourcePlugin::<XrPassthroughLayer>::default());
app.add_plugins(ExtractResourcePlugin::<XrPassthroughState>::default());
app.register_type::<XrPassthroughState>();
app.add_systems(Startup, check_passthrough_support);
app.add_systems(
XrSetup,
setup_passthrough
.run_if(|state: Res<XrPassthroughState>| *state != XrPassthroughState::Unsupported),
);
app.add_systems(XrCleanup, cleanup_passthrough);
app.add_systems(
Update,
resume_passthrough.run_if(
resource_exists_and_equals(XrPassthroughState::Paused)
.and_then(on_event::<ResumePassthrough>()),
),
);
app.add_systems(
Update,
pause_passthrough.run_if(
resource_exists_and_equals(XrPassthroughState::Running)
.and_then(on_event::<PausePassthrough>()),
),
);
}
}
fn check_passthrough_support(mut cmds: Commands, instance: Option<Res<XrInstance>>) {
match instance {
None => cmds.insert_resource(XrPassthroughState::Unsupported),
Some(instance) => {
let supported = instance.exts().fb_passthrough.is_some()
&& supports_passthrough(
&instance,
instance.system(FormFactor::HEAD_MOUNTED_DISPLAY).unwrap(),
)
.is_ok_and(|v| v);
match supported {
false => cmds.insert_resource(XrPassthroughState::Unsupported),
true => cmds.insert_resource(XrPassthroughState::Paused),
}
}
}
}
fn resume_passthrough(
layer: Res<XrPassthroughLayer>,
mut state: ResMut<XrPassthroughState>,
mut clear_color: ResMut<ClearColor>,
) {
if let Err(e) = layer.resume() {
warn!("Unable to resume Passthrough: {}", e);
return;
}
**clear_color = Srgba::NONE.into();
*state = XrPassthroughState::Running;
}
fn pause_passthrough(
layer: Res<XrPassthroughLayer>,
mut state: ResMut<XrPassthroughState>,
mut clear_color: ResMut<ClearColor>,
) {
if let Err(e) = layer.pause() {
warn!("Unable to resume Passthrough: {}", e);
return;
}
clear_color.set_alpha(1.0);
*state = XrPassthroughState::Paused;
}
fn cleanup_passthrough(mut cmds: Commands) {
cmds.remove_resource::<XrPassthrough>();
cmds.remove_resource::<XrPassthroughLayer>();
}
fn setup_passthrough(mut cmds: Commands, session: Res<XrSession>) {
match create_passthrough(&session) {
Ok((passthrough, layer)) => {
cmds.insert_resource(XrPassthrough::from(passthrough));
cmds.insert_resource(XrPassthroughLayer::from(layer));
}
Err(e) => {
warn!("Unable to create passthrough: {}", e);
}
}
}
#[derive(Clone, Copy, Debug, Default, Reflect, Event)]
pub struct ResumePassthrough;
#[derive(Clone, Copy, Debug, Default, Reflect, Event)]
pub struct PausePassthrough;
fn cvt(x: xr::sys::Result) -> xr::Result<xr::sys::Result> {
if x.into_raw() >= 0 {
Ok(x)
} else {
Err(x)
}
}
#[derive(Copy, Clone)]
#[repr(transparent)]
pub(crate) struct CompositionLayerPassthrough<'a, G: xr::Graphics> {
inner: xr::sys::CompositionLayerPassthroughFB,
_marker: PhantomData<&'a G>,
}
impl<'a, G: Graphics> std::ops::Deref for CompositionLayerPassthrough<'a, G> {
type Target = CompositionLayerBase<'a, G>;
#[inline]
fn deref(&self) -> &Self::Target {
unsafe { mem::transmute(&self.inner) }
}
}
impl<'a, G: xr::Graphics> CompositionLayerPassthrough<'a, G> {
pub(crate) fn from_xr_passthrough_layer(layer: &XrPassthroughLayer) -> Self {
Self {
inner: xr::sys::CompositionLayerPassthroughFB {
ty: xr::sys::CompositionLayerPassthroughFB::TYPE,
next: ptr::null(),
flags: CompositionLayerFlags::BLEND_TEXTURE_SOURCE_ALPHA,
space: Space::NULL,
layer_handle: *layer.inner(),
},
_marker: PhantomData,
}
}
}
#[inline]
pub fn supports_passthrough(instance: &XrInstance, system: xr::SystemId) -> xr::Result<bool> {
unsafe {
let mut hand = xr::sys::SystemPassthroughProperties2FB {
ty: SystemPassthroughProperties2FB::TYPE,
next: ptr::null(),
capabilities: PassthroughCapabilityFlagsFB::PASSTHROUGH_CAPABILITY,
};
let mut p = xr::sys::SystemProperties::out(&mut hand as *mut _ as _);
cvt((instance.fp().get_system_properties)(
instance.as_raw(),
system,
p.as_mut_ptr(),
))?;
bevy::log::info!(
"From supports_passthrough: Passthrough capabilities: {:?}",
hand.capabilities
);
Ok(
(hand.capabilities & PassthroughCapabilityFlagsFB::PASSTHROUGH_CAPABILITY)
== PassthroughCapabilityFlagsFB::PASSTHROUGH_CAPABILITY,
)
}
}
#[inline]
pub fn create_passthrough(
xr_session: &XrSession,
) -> xr::Result<(xr::Passthrough, xr::PassthroughLayer)> {
let flags = xr::PassthroughFlagsFB::IS_RUNNING_AT_CREATION;
let purpose = xr::PassthroughLayerPurposeFB::RECONSTRUCTION;
let passthrough = match xr_session {
#[cfg(feature = "vulkan")]
XrSession::Vulkan(session) => {
session.create_passthrough(xr::PassthroughFlagsFB::IS_RUNNING_AT_CREATION)
}
#[cfg(all(feature = "d3d12", windows))]
XrSession::D3D12(session) => {
session.create_passthrough(xr::PassthroughFlagsFB::IS_RUNNING_AT_CREATION)
}
}?;
let passthrough_layer = match xr_session {
#[cfg(feature = "vulkan")]
XrSession::Vulkan(session) => {
session.create_passthrough_layer(&passthrough, flags, purpose)
}
#[cfg(all(feature = "d3d12", windows))]
XrSession::D3D12(session) => session.create_passthrough_layer(&passthrough, flags, purpose),
}?;
Ok((passthrough, passthrough_layer))
}
/// Enable Passthrough on xr startup
/// just sends the [`ResumePassthrough`] event in [`XrSetup`]
pub struct EnablePassthroughStartup;
impl Plugin for EnablePassthroughStartup {
fn build(&self, app: &mut App) {
app.add_systems(XrSetup, |mut e: EventWriter<ResumePassthrough>| {
e.send_default();
});
}
}

View File

@@ -1,15 +0,0 @@
use bevy::ecs::schedule::{IntoSystemConfigs, SystemConfigs};
pub use crate::xr_init::schedules::XrSetup;
use crate::xr_init::xr_only;
pub use crate::xr_input::{QuatConv, Vec2Conv, Vec3Conv};
pub trait XrSystems<Marker> {
fn xr_only(self) -> SystemConfigs;
}
impl<T: IntoSystemConfigs<M>, M> XrSystems<M> for T {
fn xr_only(self) -> SystemConfigs {
self.into_configs().run_if(xr_only())
}
}

View File

@@ -1,133 +0,0 @@
#[macro_export]
macro_rules! xr_resource_wrapper {
($wrapper_type:ident, $xr_type:ty) => {
#[derive(
Clone,
bevy::prelude::Resource,
bevy::prelude::Deref,
bevy::prelude::DerefMut,
bevy::render::extract_resource::ExtractResource,
)]
pub struct $wrapper_type($xr_type);
impl $wrapper_type {
pub fn new(value: $xr_type) -> Self {
Self(value)
}
}
// impl std::ops::Deref for $wrapper_type {
// type Target = $xr_type;
//
// fn deref(&self) -> &Self::Target {
// &self.0
// }
// }
impl From<$xr_type> for $wrapper_type {
fn from(value: $xr_type) -> Self {
Self::new(value)
}
}
};
}
#[macro_export]
macro_rules! xr_resource_wrapper_copy {
($wrapper_type:ident, $xr_type:ty) => {
#[derive(
Clone,
Copy,
bevy::prelude::Resource,
bevy::prelude::Deref,
bevy::prelude::DerefMut,
bevy::render::extract_resource::ExtractResource,
)]
pub struct $wrapper_type($xr_type);
impl $wrapper_type {
pub fn new(value: $xr_type) -> Self {
Self(value)
}
}
// impl std::ops::Deref for $wrapper_type {
// type Target = $xr_type;
//
// fn deref(&self) -> &Self::Target {
// &self.0
// }
// }
impl From<$xr_type> for $wrapper_type {
fn from(value: $xr_type) -> Self {
Self::new(value)
}
}
};
}
#[macro_export]
macro_rules! xr_arc_resource_wrapper {
($wrapper_type:ident, $xr_type:ty) => {
#[derive(
Clone,
bevy::prelude::Resource,
bevy::prelude::Deref,
bevy::prelude::DerefMut,
bevy::render::extract_resource::ExtractResource,
)]
pub struct $wrapper_type(std::sync::Arc<$xr_type>);
impl $wrapper_type {
pub fn new(value: $xr_type) -> Self {
Self(std::sync::Arc::new(value))
}
}
// impl std::ops::Deref for $wrapper_type {
// type Target = $xr_type;
//
// fn deref(&self) -> &Self::Target {
// self.0.as_ref()
// }
// }
impl From<$xr_type> for $wrapper_type {
fn from(value: $xr_type) -> Self {
Self::new(value)
}
}
};
}
#[macro_export]
macro_rules! xr_no_clone_resource_wrapper {
($wrapper_type:ident, $xr_type:ty) => {
#[derive(bevy::prelude::Resource, bevy::prelude::Deref, bevy::prelude::DerefMut)]
pub struct $wrapper_type($xr_type);
impl $wrapper_type {
pub fn new(value: $xr_type) -> Self {
Self(value)
}
}
// impl std::ops::Deref for $wrapper_type {
// type Target = $xr_type;
//
// fn deref(&self) -> &Self::Target {
// &self.0
// }
// }
impl From<$xr_type> for $wrapper_type {
fn from(value: $xr_type) -> Self {
Self::new(value)
}
}
};
}
pub use xr_arc_resource_wrapper;
pub use xr_no_clone_resource_wrapper;
pub use xr_resource_wrapper;

View File

@@ -1,319 +0,0 @@
use std::ffi::c_void;
use std::sync::atomic::AtomicBool;
use std::sync::Mutex;
use crate::input::XrInput;
use crate::passthrough::{CompositionLayerPassthrough, XrPassthroughLayer};
use crate::resource_macros::*;
use crate::xr::sys::CompositionLayerPassthroughFB;
use crate::xr::{CompositionLayerBase, CompositionLayerFlags};
use crate::{resource_macros::*, xr_resource_wrapper_copy};
use bevy::prelude::*;
use bevy::prelude::*;
use bevy::render::extract_component::ExtractComponent;
use bevy::render::extract_resource::{ExtractResource, ExtractResourcePlugin};
use core::ptr;
use openxr as xr;
#[cfg(all(feature = "d3d12", windows))]
use winapi::um::d3d12::{ID3D12CommandQueue, ID3D12Device};
xr_resource_wrapper!(XrInstance, xr::Instance);
xr_resource_wrapper_copy!(XrEnvironmentBlendMode, xr::EnvironmentBlendMode);
xr_resource_wrapper_copy!(XrResolution, UVec2);
xr_resource_wrapper_copy!(XrFormat, wgpu::TextureFormat);
xr_resource_wrapper_copy!(XrFrameState, xr::FrameState);
xr_resource_wrapper!(XrViews, Vec<xr::View>);
xr_arc_resource_wrapper!(XrSessionRunning, AtomicBool);
xr_arc_resource_wrapper!(XrSwapchain, Swapchain);
xr_no_clone_resource_wrapper!(XrFrameWaiter, xr::FrameWaiter);
#[derive(Clone, Resource, ExtractResource)]
pub enum XrSession {
#[cfg(feature = "vulkan")]
Vulkan(xr::Session<xr::Vulkan>),
#[cfg(all(feature = "d3d12", windows))]
D3D12(xr::Session<xr::D3D12>),
}
impl std::ops::Deref for XrSession {
type Target = xr::Session<xr::AnyGraphics>;
fn deref(&self) -> &Self::Target {
// SAFTEY: should be fine i think -Schmarni
unsafe {
match self {
#[cfg(feature = "vulkan")]
XrSession::Vulkan(sess) => std::mem::transmute(sess),
#[cfg(all(feature = "d3d12", windows))]
XrSession::D3D12(sess) => std::mem::transmute(sess),
}
}
}
}
#[cfg(feature = "vulkan")]
pub struct VulkanOXrSessionSetupInfo {
pub(crate) device_ptr: *const c_void,
pub(crate) physical_device_ptr: *const c_void,
pub(crate) vk_instance_ptr: *const c_void,
pub(crate) queue_family_index: u32,
pub(crate) xr_system_id: xr::SystemId,
}
#[cfg(all(feature = "d3d12", windows))]
pub struct D3D12OXrSessionSetupInfo {
pub(crate) raw_device: *mut ID3D12Device,
pub(crate) raw_queue: *mut ID3D12CommandQueue,
pub(crate) xr_system_id: xr::SystemId,
}
pub enum OXrSessionSetupInfo {
#[cfg(feature = "vulkan")]
Vulkan(VulkanOXrSessionSetupInfo),
#[cfg(all(feature = "d3d12", windows))]
D3D12(D3D12OXrSessionSetupInfo),
}
pub struct XrResourcePlugin;
impl Plugin for XrResourcePlugin {
fn build(&self, app: &mut App) {
app.add_plugins(ExtractResourcePlugin::<XrResolution>::default());
app.add_plugins(ExtractResourcePlugin::<XrFormat>::default());
app.add_plugins(ExtractResourcePlugin::<XrSwapchain>::default());
app.add_plugins(ExtractResourcePlugin::<XrFrameState>::default());
app.add_plugins(ExtractResourcePlugin::<XrViews>::default());
app.add_plugins(ExtractResourcePlugin::<XrInput>::default());
app.add_plugins(ExtractResourcePlugin::<XrEnvironmentBlendMode>::default());
// app.add_plugins(ExtractResourcePlugin::<XrSessionRunning>::default());
app.add_plugins(ExtractResourcePlugin::<XrSession>::default());
}
}
pub enum Swapchain {
#[cfg(feature = "vulkan")]
Vulkan(SwapchainInner<xr::Vulkan>),
#[cfg(all(feature = "d3d12", windows))]
D3D12(SwapchainInner<xr::D3D12>),
}
impl Swapchain {
pub(crate) fn begin(&self) -> xr::Result<()> {
match self {
#[cfg(feature = "vulkan")]
Swapchain::Vulkan(swapchain) => swapchain.begin(),
#[cfg(all(feature = "d3d12", windows))]
Swapchain::D3D12(swapchain) => swapchain.begin(),
}
}
pub(crate) fn get_render_views(&self) -> (wgpu::TextureView, wgpu::TextureView) {
match self {
#[cfg(feature = "vulkan")]
Swapchain::Vulkan(swapchain) => swapchain.get_render_views(),
#[cfg(all(feature = "d3d12", windows))]
Swapchain::D3D12(swapchain) => swapchain.get_render_views(),
}
}
pub(crate) fn acquire_image(&self) -> xr::Result<()> {
match self {
#[cfg(feature = "vulkan")]
Swapchain::Vulkan(swapchain) => swapchain.acquire_image(),
#[cfg(all(feature = "d3d12", windows))]
Swapchain::D3D12(swapchain) => swapchain.acquire_image(),
}
}
pub(crate) fn wait_image(&self) -> xr::Result<()> {
match self {
#[cfg(feature = "vulkan")]
Swapchain::Vulkan(swapchain) => swapchain.wait_image(),
#[cfg(all(feature = "d3d12", windows))]
Swapchain::D3D12(swapchain) => swapchain.wait_image(),
}
}
pub(crate) fn release_image(&self) -> xr::Result<()> {
match self {
#[cfg(feature = "vulkan")]
Swapchain::Vulkan(swapchain) => swapchain.release_image(),
#[cfg(all(feature = "d3d12", windows))]
Swapchain::D3D12(swapchain) => swapchain.release_image(),
}
}
pub(crate) fn end(
&self,
predicted_display_time: xr::Time,
views: &[openxr::View],
stage: &xr::Space,
resolution: UVec2,
environment_blend_mode: xr::EnvironmentBlendMode,
passthrough_layer: Option<&XrPassthroughLayer>,
) -> xr::Result<()> {
match self {
#[cfg(feature = "vulkan")]
Swapchain::Vulkan(swapchain) => swapchain.end(
predicted_display_time,
views,
stage,
resolution,
environment_blend_mode,
passthrough_layer,
),
#[cfg(all(feature = "d3d12", windows))]
Swapchain::D3D12(swapchain) => swapchain.end(
predicted_display_time,
views,
stage,
resolution,
environment_blend_mode,
passthrough_layer,
),
}
}
}
pub struct SwapchainInner<G: xr::Graphics> {
pub(crate) stream: Mutex<xr::FrameStream<G>>,
pub(crate) handle: Mutex<xr::Swapchain<G>>,
pub(crate) buffers: Vec<wgpu::Texture>,
pub(crate) image_index: Mutex<usize>,
}
impl<G: xr::Graphics> Drop for SwapchainInner<G> {
fn drop(&mut self) {
for _ in 0..self.buffers.len() {
let v = self.buffers.remove(0);
Box::leak(Box::new(v));
}
}
}
impl<G: xr::Graphics> SwapchainInner<G> {
fn begin(&self) -> xr::Result<()> {
self.stream.lock().unwrap().begin()
}
fn get_render_views(&self) -> (wgpu::TextureView, wgpu::TextureView) {
let texture = &self.buffers[*self.image_index.lock().unwrap()];
(
texture.create_view(&wgpu::TextureViewDescriptor {
dimension: Some(wgpu::TextureViewDimension::D2),
array_layer_count: Some(1),
..Default::default()
}),
texture.create_view(&wgpu::TextureViewDescriptor {
dimension: Some(wgpu::TextureViewDimension::D2),
array_layer_count: Some(1),
base_array_layer: 1,
..Default::default()
}),
)
}
fn acquire_image(&self) -> xr::Result<()> {
let image_index = self.handle.lock().unwrap().acquire_image()?;
*self.image_index.lock().unwrap() = image_index as _;
Ok(())
}
fn wait_image(&self) -> xr::Result<()> {
self.handle
.lock()
.unwrap()
.wait_image(xr::Duration::INFINITE)
}
fn release_image(&self) -> xr::Result<()> {
self.handle.lock().unwrap().release_image()
}
fn end(
&self,
predicted_display_time: xr::Time,
views: &[openxr::View],
stage: &xr::Space,
resolution: UVec2,
environment_blend_mode: xr::EnvironmentBlendMode,
passthrough_layer: Option<&XrPassthroughLayer>,
) -> xr::Result<()> {
let rect = xr::Rect2Di {
offset: xr::Offset2Di { x: 0, y: 0 },
extent: xr::Extent2Di {
width: resolution.x as _,
height: resolution.y as _,
},
};
let swapchain = self.handle.lock().unwrap();
if views.is_empty() {
warn!("views are len of 0");
return Ok(());
}
match passthrough_layer {
Some(pass) => {
//bevy::log::info!("Rendering with pass through");
self.stream.lock().unwrap().end(
predicted_display_time,
environment_blend_mode,
&[
&CompositionLayerPassthrough::from_xr_passthrough_layer(pass),
&xr::CompositionLayerProjection::new()
.layer_flags(CompositionLayerFlags::BLEND_TEXTURE_SOURCE_ALPHA)
.space(stage)
.views(&[
xr::CompositionLayerProjectionView::new()
.pose(views[0].pose)
.fov(views[0].fov)
.sub_image(
xr::SwapchainSubImage::new()
.swapchain(&swapchain)
.image_array_index(0)
.image_rect(rect),
),
xr::CompositionLayerProjectionView::new()
.pose(views[1].pose)
.fov(views[1].fov)
.sub_image(
xr::SwapchainSubImage::new()
.swapchain(&swapchain)
.image_array_index(1)
.image_rect(rect),
),
]),
],
)
}
None => {
// bevy::log::info!("Rendering without pass through");
self.stream.lock().unwrap().end(
predicted_display_time,
environment_blend_mode,
&[&xr::CompositionLayerProjection::new().space(stage).views(&[
xr::CompositionLayerProjectionView::new()
.pose(views[0].pose)
.fov(views[0].fov)
.sub_image(
xr::SwapchainSubImage::new()
.swapchain(&swapchain)
.image_array_index(0)
.image_rect(rect),
),
xr::CompositionLayerProjectionView::new()
.pose(views[1].pose)
.fov(views[1].fov)
.sub_image(
xr::SwapchainSubImage::new()
.swapchain(&swapchain)
.image_array_index(1)
.image_rect(rect),
),
])],
)
}
}
}
}

View File

@@ -1,263 +0,0 @@
pub mod schedules;
pub use schedules::*;
use bevy::{
prelude::*,
render::{
camera::{ManualTextureView, ManualTextureViews},
extract_resource::{ExtractResource, ExtractResourcePlugin},
renderer::{RenderAdapter, RenderDevice, RenderInstance},
Render, RenderApp, RenderSet,
},
window::{PrimaryWindow, RawHandleWrapper},
};
use crate::{
clean_resources, graphics,
resources::{OXrSessionSetupInfo, XrFormat, XrInstance, XrResolution, XrSession, XrSwapchain},
LEFT_XR_TEXTURE_HANDLE, RIGHT_XR_TEXTURE_HANDLE,
};
#[derive(Resource, Event, Clone, Copy, PartialEq, Eq, Reflect, Debug, ExtractResource)]
pub enum XrStatus {
NoInstance,
Enabled,
Enabling,
Disabled,
Disabling,
}
#[derive(
Resource, Clone, Copy, PartialEq, Eq, Reflect, Debug, ExtractResource, Default, Deref, DerefMut,
)]
pub struct XrShouldRender(bool);
#[derive(
Resource, Clone, Copy, PartialEq, Eq, Reflect, Debug, ExtractResource, Default, Deref, DerefMut,
)]
pub struct XrHasWaited(bool);
pub struct XrEarlyInitPlugin;
pub struct XrInitPlugin;
pub fn xr_only() -> impl FnMut(Res<XrStatus>) -> bool {
resource_equals(XrStatus::Enabled)
}
pub fn xr_render_only() -> impl FnMut(Res<XrShouldRender>) -> bool {
resource_equals(XrShouldRender(true))
}
pub fn xr_after_wait_only() -> impl FnMut(Res<XrHasWaited>) -> bool {
resource_equals(XrHasWaited(true))
}
#[derive(Resource, Clone, Copy, ExtractResource)]
pub struct CleanupRenderWorld;
impl Plugin for XrEarlyInitPlugin {
fn build(&self, app: &mut App) {
add_schedules(app);
app.add_event::<SetupXrData>()
.add_event::<CleanupXrData>()
.add_event::<StartXrSession>()
.add_event::<EndXrSession>();
}
}
impl Plugin for XrInitPlugin {
fn build(&self, app: &mut App) {
app.add_plugins(ExtractResourcePlugin::<XrStatus>::default());
app.add_plugins(ExtractResourcePlugin::<XrShouldRender>::default());
app.add_plugins(ExtractResourcePlugin::<XrHasWaited>::default());
app.add_plugins(ExtractResourcePlugin::<CleanupRenderWorld>::default());
app.init_resource::<XrShouldRender>();
app.init_resource::<XrHasWaited>();
app.add_systems(PreUpdate, setup_xr.run_if(on_event::<SetupXrData>()))
.add_systems(PreUpdate, cleanup_xr.run_if(on_event::<CleanupXrData>()));
app.add_systems(
PostUpdate,
start_xr_session.run_if(on_event::<StartXrSession>()),
);
app.add_systems(
PostUpdate,
stop_xr_session.run_if(on_event::<EndXrSession>()),
);
app.add_systems(XrSetup, setup_manual_texture_views);
app.add_systems(XrCleanup, set_cleanup_res);
app.add_systems(PreUpdate, remove_cleanup_res.before(cleanup_xr));
let render_app = app.sub_app_mut(RenderApp);
render_app.add_systems(
Render,
remove_cleanup_res
.in_set(RenderSet::Cleanup)
.after(clean_resources),
);
}
}
#[derive(Resource, Clone, Copy, PartialEq, Eq, Default)]
pub enum ExitAppOnSessionExit {
#[default]
/// Restart XrSession when session is lost
OnlyOnExit,
/// Always exit the app
Always,
/// Keep app open when XrSession wants to exit or is lost
Never,
}
pub struct StartSessionOnStartup;
impl Plugin for StartSessionOnStartup {
fn build(&self, app: &mut App) {
app.add_systems(Startup, |mut event: EventWriter<StartXrSession>| {
event.send_default();
});
}
}
fn set_cleanup_res(mut commands: Commands) {
info!("Set Cleanup Res");
commands.insert_resource(CleanupRenderWorld);
}
fn remove_cleanup_res(mut commands: Commands) {
commands.remove_resource::<CleanupRenderWorld>();
}
fn setup_manual_texture_views(
mut manual_texture_views: ResMut<ManualTextureViews>,
swapchain: Res<XrSwapchain>,
xr_resolution: Res<XrResolution>,
xr_format: Res<XrFormat>,
) {
info!("Creating Texture views");
let (left, right) = swapchain.get_render_views();
let left = ManualTextureView {
texture_view: left.into(),
size: **xr_resolution,
format: **xr_format,
};
let right = ManualTextureView {
texture_view: right.into(),
size: **xr_resolution,
format: **xr_format,
};
manual_texture_views.insert(LEFT_XR_TEXTURE_HANDLE, left);
manual_texture_views.insert(RIGHT_XR_TEXTURE_HANDLE, right);
}
pub fn setup_xr(world: &mut World) {
info!("Pre XrPreSetup");
world.run_schedule(XrPreSetup);
info!("Post XrPreSetup");
world.run_schedule(XrSetup);
world.run_schedule(XrPrePostSetup);
world.run_schedule(XrPostSetup);
*world.resource_mut::<XrStatus>() = XrStatus::Enabled;
}
fn cleanup_xr(world: &mut World) {
world.run_schedule(XrPreCleanup);
world.run_schedule(XrCleanup);
world.run_schedule(XrPostCleanup);
*world.resource_mut::<XrStatus>() = XrStatus::Disabled;
}
#[derive(Event, Clone, Copy, Default)]
pub struct StartXrSession;
#[derive(Event, Clone, Copy, Default)]
pub struct EndXrSession;
#[derive(Event, Clone, Copy, Default)]
pub(crate) struct SetupXrData;
#[derive(Event, Clone, Copy, Default)]
pub(crate) struct CleanupXrData;
#[allow(clippy::too_many_arguments)]
fn start_xr_session(
mut commands: Commands,
mut status: ResMut<XrStatus>,
instance: Option<Res<XrInstance>>,
primary_window: Query<&RawHandleWrapper, With<PrimaryWindow>>,
setup_info: Option<NonSend<OXrSessionSetupInfo>>,
render_device: Option<Res<RenderDevice>>,
render_adapter: Option<Res<RenderAdapter>>,
render_instance: Option<Res<RenderInstance>>,
) {
info!("start Session");
match *status {
XrStatus::Disabled => {}
XrStatus::NoInstance => {
warn!("Trying to start OpenXR Session without instance, ignoring");
return;
}
XrStatus::Enabled | XrStatus::Enabling => {
warn!("Trying to start OpenXR Session while one already exists, ignoring");
return;
}
XrStatus::Disabling => {
warn!("Trying to start OpenXR Session while one is stopping, ignoring");
return;
}
}
let (
Some(instance),
Some(setup_info),
Some(render_device),
Some(render_adapter),
Some(render_instance),
) = (
instance,
setup_info,
render_device,
render_adapter,
render_instance,
)
else {
error!("Missing resources after passing status check");
return;
};
let (
xr_session,
xr_resolution,
xr_format,
xr_session_running,
xr_frame_waiter,
xr_swapchain,
xr_input,
xr_views,
xr_frame_state,
) = match graphics::start_xr_session(
primary_window.get_single().cloned().ok(),
&setup_info,
&instance,
&render_device,
&render_adapter,
&render_instance,
) {
Ok(data) => data,
Err(err) => {
error!("Unable to start OpenXR Session: {}", err);
return;
}
};
commands.insert_resource(xr_session);
commands.insert_resource(xr_resolution);
commands.insert_resource(xr_format);
commands.insert_resource(xr_session_running);
commands.insert_resource(xr_frame_waiter);
commands.insert_resource(xr_swapchain);
commands.insert_resource(xr_input);
commands.insert_resource(xr_views);
commands.insert_resource(xr_frame_state);
*status = XrStatus::Enabling;
}
fn stop_xr_session(session: ResMut<XrSession>, mut status: ResMut<XrStatus>) {
match session.request_exit() {
Ok(_) => {}
Err(err) => {
error!("Error while trying to request session exit: {}", err)
}
}
*status = XrStatus::Disabling;
}

View File

@@ -1,54 +0,0 @@
use bevy::{
app::App,
ecs::schedule::{ExecutorKind, Schedule, ScheduleLabel},
};
#[derive(Debug, ScheduleLabel, Clone, Copy, Hash, PartialEq, Eq)]
pub struct XrPreSetup;
#[derive(Debug, ScheduleLabel, Clone, Copy, Hash, PartialEq, Eq)]
pub struct XrSetup;
#[derive(Debug, ScheduleLabel, Clone, Copy, Hash, PartialEq, Eq)]
pub struct XrPrePostSetup;
#[derive(Debug, ScheduleLabel, Clone, Copy, Hash, PartialEq, Eq)]
pub struct XrPostSetup;
#[derive(Debug, ScheduleLabel, Clone, Copy, Hash, PartialEq, Eq)]
pub struct XrPreCleanup;
#[derive(Debug, ScheduleLabel, Clone, Copy, Hash, PartialEq, Eq)]
pub struct XrCleanup;
#[derive(Debug, ScheduleLabel, Clone, Copy, Hash, PartialEq, Eq)]
pub struct XrPostCleanup;
#[derive(Debug, ScheduleLabel, Clone, Copy, Hash, PartialEq, Eq)]
pub struct XrPreRenderUpdate;
#[derive(Debug, ScheduleLabel, Clone, Copy, Hash, PartialEq, Eq)]
pub struct XrRenderUpdate;
#[derive(Debug, ScheduleLabel, Clone, Copy, Hash, PartialEq, Eq)]
pub struct XrPostRenderUpdate;
pub(super) fn add_schedules(app: &mut App) {
let schedules = [
Schedule::new(XrPreSetup),
Schedule::new(XrSetup),
Schedule::new(XrPrePostSetup),
Schedule::new(XrPostSetup),
Schedule::new(XrPreRenderUpdate),
Schedule::new(XrRenderUpdate),
Schedule::new(XrPostRenderUpdate),
Schedule::new(XrPreCleanup),
Schedule::new(XrCleanup),
Schedule::new(XrPostCleanup),
];
for mut schedule in schedules {
schedule.set_executor_kind(ExecutorKind::SingleThreaded);
schedule.set_apply_final_deferred(true);
app.add_schedule(schedule);
}
}

View File

@@ -1,409 +0,0 @@
use std::error::Error;
use bevy::{prelude::*, utils::HashMap};
use openxr as xr;
use xr::{Action, Binding, Haptic, Posef, Vector2f};
use crate::{
resources::{XrInstance, XrSession},
xr_init::{xr_only, XrCleanup, XrPrePostSetup, XrPreSetup},
};
use super::oculus_touch::ActionSets;
pub use xr::sys::NULL_PATH;
pub struct XrActionsPlugin;
impl Plugin for XrActionsPlugin {
fn build(&self, app: &mut App) {
app.add_systems(PreUpdate, sync_actions.run_if(xr_only()));
app.add_systems(
XrPreSetup,
(insert_setup_action_sets, apply_deferred).chain(),
);
app.add_systems(XrPrePostSetup, setup_oxr_actions);
app.add_systems(XrCleanup, clean_actions);
}
}
fn insert_setup_action_sets(mut cmds: Commands) {
info!("WHAT?!");
cmds.insert_resource(SetupActionSets {
sets: HashMap::new(),
});
}
fn clean_actions(mut cmds: Commands) {
cmds.remove_resource::<ActionSets>();
cmds.remove_resource::<XrActionSets>();
}
#[inline(always)]
fn create_action<T: xr::ActionTy>(
action: &SetupAction,
action_name: &'static str,
oxr_action_set: &xr::ActionSet,
hands: &[xr::Path],
) -> xr::Action<T> {
match action.handednes {
ActionHandednes::Single => oxr_action_set
.create_action(action_name, &action.pretty_name, &[])
.unwrap_or_else(|_| panic!("Unable to create action: {}", action_name)),
ActionHandednes::Double => oxr_action_set
.create_action(action_name, &action.pretty_name, hands)
.unwrap_or_else(|_| panic!("Unable to create action: {}", action_name)),
}
}
pub fn setup_oxr_actions(world: &mut World) {
let actions = world.remove_resource::<SetupActionSets>().unwrap();
let instance = world.get_resource::<XrInstance>().unwrap();
let session = world.get_resource::<XrSession>().unwrap();
let left_path = instance.string_to_path("/user/hand/left").unwrap();
let right_path = instance.string_to_path("/user/hand/right").unwrap();
let hands = [left_path, right_path];
let mut action_sets = XrActionSets { sets: default() };
// let mut action_bindings: HashMap<&'static str, Vec<xr::Path>> = HashMap::new();
let mut action_bindings: HashMap<
(&'static str, &'static str),
HashMap<&'static str, Vec<xr::Path>>,
> = HashMap::new();
for (set_name, set) in actions.sets.into_iter() {
let mut actions: HashMap<&'static str, TypedAction> = default();
let oxr_action_set = instance
.create_action_set(set_name, &set.pretty_name, set.priority)
.expect("Unable to create action set");
for (action_name, action) in set.actions.into_iter() {
use self::create_action as ca;
let typed_action = match action.action_type {
ActionType::Vec2 => {
TypedAction::Vec2(ca(&action, action_name, &oxr_action_set, &hands))
}
ActionType::F32 => {
TypedAction::F32(ca(&action, action_name, &oxr_action_set, &hands))
}
ActionType::Bool => {
TypedAction::Bool(ca(&action, action_name, &oxr_action_set, &hands))
}
ActionType::PoseF => {
TypedAction::PoseF(ca(&action, action_name, &oxr_action_set, &hands))
}
ActionType::Haptic => {
TypedAction::Haptic(ca(&action, action_name, &oxr_action_set, &hands))
}
};
actions.insert(action_name, typed_action);
for (device_path, bindings) in action.bindings.into_iter() {
for b in bindings {
// info!("binding {} to {}", action_name, b);
action_bindings
.entry((set_name, action_name))
.or_default()
.entry(device_path)
.or_default()
.push(instance.string_to_path(b).unwrap());
}
}
}
// oxr_action_sets.push(oxr_action_set);
action_sets.sets.insert(
set_name,
ActionSet {
oxr_action_set,
actions,
enabled: true,
},
);
}
let mut b_indings: HashMap<&'static str, Vec<Binding>> = HashMap::new();
for (dev, mut bindings) in action_sets
.sets
.iter()
.flat_map(|(set_name, set)| {
set.actions
.iter()
.map(move |(action_name, a)| (set_name, action_name, a))
})
.zip([&action_bindings].into_iter().cycle())
.flat_map(move |((set_name, action_name, action), bindings)| {
bindings
.get(&(set_name as &'static str, action_name as &'static str))
.unwrap()
.iter()
.map(move |(dev, bindings)| (action, dev, bindings))
})
.map(|(action, dev, bindings)| {
(
dev,
bindings
.iter()
.map(move |binding| match &action {
TypedAction::Vec2(a) => Binding::new(a, *binding),
TypedAction::F32(a) => Binding::new(a, *binding),
TypedAction::Bool(a) => Binding::new(a, *binding),
TypedAction::PoseF(a) => Binding::new(a, *binding),
TypedAction::Haptic(a) => Binding::new(a, *binding),
})
.collect::<Vec<_>>(),
)
})
{
b_indings.entry(dev).or_default().append(&mut bindings);
}
for (dev, bindings) in b_indings.into_iter() {
instance
.suggest_interaction_profile_bindings(instance.string_to_path(dev).unwrap(), &bindings)
.expect("Unable to suggest interaction bindings!");
}
session
.attach_action_sets(
&action_sets
.sets
.values()
.map(|set| &set.oxr_action_set)
.collect::<Vec<_>>(),
)
.expect("Unable to attach action sets!");
world.insert_resource(action_sets);
}
pub enum ActionHandednes {
Single,
Double,
}
#[derive(Clone, Copy)]
pub enum ActionType {
F32,
Bool,
PoseF,
Haptic,
Vec2,
}
pub enum TypedAction {
F32(Action<f32>),
Bool(Action<bool>),
PoseF(Action<Posef>),
Haptic(Action<Haptic>),
Vec2(Action<Vector2f>),
}
pub struct SetupAction {
pretty_name: String,
action_type: ActionType,
handednes: ActionHandednes,
bindings: HashMap<&'static str, Vec<&'static str>>,
}
pub struct SetupActionSet {
pretty_name: String,
priority: u32,
actions: HashMap<&'static str, SetupAction>,
}
impl SetupActionSet {
pub fn new_action(
&mut self,
name: &'static str,
pretty_name: String,
action_type: ActionType,
handednes: ActionHandednes,
) {
self.actions.insert(
name,
SetupAction {
pretty_name,
action_type,
handednes,
bindings: default(),
},
);
}
pub fn suggest_binding(&mut self, device_path: &'static str, bindings: &[XrBinding]) {
for binding in bindings {
self.actions
.get_mut(binding.action)
.ok_or(eyre::eyre!("Missing Action: {}", binding.action))
.unwrap()
.bindings
.entry(device_path)
.or_default()
.push(binding.path);
}
}
}
pub struct XrBinding {
action: &'static str,
path: &'static str,
}
impl XrBinding {
pub fn new(action_name: &'static str, binding_path: &'static str) -> XrBinding {
XrBinding {
action: action_name,
path: binding_path,
}
}
}
#[derive(Resource)]
pub struct SetupActionSets {
sets: HashMap<&'static str, SetupActionSet>,
}
impl SetupActionSets {
pub fn add_action_set(
&mut self,
name: &'static str,
pretty_name: String,
priority: u32,
) -> &mut SetupActionSet {
self.sets.insert(
name,
SetupActionSet {
pretty_name,
priority,
actions: HashMap::new(),
},
);
self.sets.get_mut(name).unwrap()
}
}
pub struct ActionSet {
// add functionality to enable/disable action sets
enabled: bool,
actions: HashMap<&'static str, TypedAction>,
oxr_action_set: xr::ActionSet,
}
#[derive(Resource)]
pub struct XrActionSets {
sets: HashMap<&'static str, ActionSet>,
}
use std::fmt::Display as FmtDisplay;
impl FmtDisplay for ActionError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let err = match self {
ActionError::NoActionSet => "Action Set Not Found!",
ActionError::NoAction => "Action Not Found!",
ActionError::WrongActionType => "Wrong Action Type!",
};
write!(f, "{}", err)
}
}
impl Error for ActionError {}
#[derive(Debug)]
pub enum ActionError {
NoActionSet,
NoAction,
WrongActionType,
}
impl XrActionSets {
pub fn get_action_vec2(
&self,
action_set: &'static str,
action_name: &'static str,
) -> Result<&Action<Vector2f>, ActionError> {
let action = self
.sets
.get(action_set)
.ok_or(ActionError::NoActionSet)?
.actions
.get(action_name)
.ok_or(ActionError::NoAction)?;
match action {
TypedAction::Vec2(a) => Ok(a),
_ => Err(ActionError::WrongActionType),
}
}
pub fn get_action_f32(
&self,
action_set: &'static str,
action_name: &'static str,
) -> Result<&Action<f32>, ActionError> {
let action = self
.sets
.get(action_set)
.ok_or(ActionError::NoActionSet)?
.actions
.get(action_name)
.ok_or(ActionError::NoAction)?;
match action {
TypedAction::F32(a) => Ok(a),
_ => Err(ActionError::WrongActionType),
}
}
pub fn get_action_bool(
&self,
action_set: &'static str,
action_name: &'static str,
) -> Result<&Action<bool>, ActionError> {
let action = self
.sets
.get(action_set)
.ok_or(ActionError::NoActionSet)?
.actions
.get(action_name)
.ok_or(ActionError::NoAction)?;
match action {
TypedAction::Bool(a) => Ok(a),
_ => Err(ActionError::WrongActionType),
}
}
pub fn get_action_posef(
&self,
action_set: &'static str,
action_name: &'static str,
) -> Result<&Action<Posef>, ActionError> {
let action = self
.sets
.get(action_set)
.ok_or(ActionError::NoActionSet)?
.actions
.get(action_name)
.ok_or(ActionError::NoAction)?;
match action {
TypedAction::PoseF(a) => Ok(a),
_ => Err(ActionError::WrongActionType),
}
}
pub fn get_action_haptic(
&self,
action_set: &'static str,
action_name: &'static str,
) -> Result<&Action<Haptic>, ActionError> {
let action = self
.sets
.get(action_set)
.ok_or(ActionError::NoActionSet)?
.actions
.get(action_name)
.ok_or(ActionError::NoAction)?;
match action {
TypedAction::Haptic(a) => Ok(a),
_ => Err(ActionError::WrongActionType),
}
}
}
pub fn sync_actions(action_sets: Res<XrActionSets>, session: Res<XrSession>) {
let active_sets = action_sets
.sets
.values()
.filter_map(|set| {
if set.enabled {
Some(xr::ActiveActionSet::new(&set.oxr_action_set))
} else {
None
}
})
.collect::<Vec<_>>();
if let Err(err) = session.sync_actions(&active_sets) {
warn!("OpenXR action sync error: {}", err);
}
}

View File

@@ -1,10 +0,0 @@
use openxr::{Action, ActionTy};
pub struct Touchable<T: ActionTy> {
pub inner: Action<T>,
pub touch: Action<bool>,
}
pub struct Handed<T> {
pub left: T,
pub right: T,
}

View File

@@ -1,396 +0,0 @@
use bevy::color::palettes;
use bevy::ecs::schedule::IntoSystemConfigs;
use bevy::log::{debug, info};
use bevy::math::Dir3;
use bevy::prelude::{
Gizmos, GlobalTransform, Plugin, Quat, Query, Res, Transform, Update, Vec2, Vec3, With, Without,
};
use crate::xr_init::xr_only;
use crate::{
input::XrInput,
resources::{XrFrameState, XrSession},
};
use crate::xr_input::{
oculus_touch::{OculusController, OculusControllerRef},
Hand,
};
use super::{
actions::XrActionSets,
trackers::{OpenXRLeftController, OpenXRRightController, OpenXRTrackingRoot},
};
/// add debug renderer for controllers
#[derive(Default)]
pub struct OpenXrDebugRenderer;
impl Plugin for OpenXrDebugRenderer {
fn build(&self, app: &mut bevy::prelude::App) {
app.add_systems(Update, draw_gizmos.run_if(xr_only()));
}
}
#[allow(clippy::too_many_arguments, clippy::complexity)]
pub fn draw_gizmos(
mut gizmos: Gizmos,
oculus_controller: Res<OculusController>,
frame_state: Res<XrFrameState>,
xr_input: Res<XrInput>,
session: Res<XrSession>,
tracking_root_query: Query<
&mut Transform,
(
With<OpenXRTrackingRoot>,
Without<OpenXRLeftController>,
Without<OpenXRRightController>,
),
>,
left_controller_query: Query<
&GlobalTransform,
(
With<OpenXRLeftController>,
Without<OpenXRRightController>,
Without<OpenXRTrackingRoot>,
),
>,
right_controller_query: Query<
&GlobalTransform,
(
With<OpenXRRightController>,
Without<OpenXRLeftController>,
Without<OpenXRTrackingRoot>,
),
>,
action_sets: Res<XrActionSets>,
) {
// if let Some(hand_tracking) = hand_tracking {
// let handtracking_ref = hand_tracking.get_ref(&xr_input, &frame_state);
// if let Some(joints) = handtracking_ref.get_poses(Hand::Left) {
// for joint in joints.inner() {
// let trans = Transform::from_rotation(joint.orientation);
// gizmos.circle(
// joint.position,
// trans.forward(),
// joint.radius,
// palettes::css::ORANGE_RED,
// );
// }
// }
// if let Some(joints) = handtracking_ref.get_poses(Hand::Right) {
// for joint in joints.inner() {
// let trans = Transform::from_rotation(joint.orientation);
// gizmos.circle(
// joint.position,
// trans.forward(),
// joint.radius,
// palettes::css::LIME_GREEN,
// );
// }
// return;
// }
// }
//lock frame
// let frame_state = *frame_state.lock().unwrap();
//get controller
let controller = oculus_controller.get_ref(&session, &frame_state, &xr_input, &action_sets);
let root = tracking_root_query.get_single();
match root {
Ok(position) => {
gizmos.circle(
position.translation
+ Vec3 {
x: 0.0,
y: 0.01,
z: 0.0,
},
Dir3::Y,
0.2,
palettes::css::RED,
);
}
Err(_) => info!("too many tracking roots"),
}
//draw the hands
//left
let left_transform = left_controller_query.get_single();
match left_transform {
Ok(left_entity) => {
draw_hand_gizmo(&mut gizmos, &controller, Hand::Left, left_entity);
}
Err(_) => debug!("no left controller entity for debug gizmos"),
}
//right
let right_transform = right_controller_query.get_single();
match right_transform {
Ok(right_entity) => {
draw_hand_gizmo(&mut gizmos, &controller, Hand::Right, right_entity);
}
Err(_) => debug!("no right controller entity for debug gizmos"),
}
}
fn draw_hand_gizmo(
gizmos: &mut Gizmos,
controller: &OculusControllerRef<'_>,
hand: Hand,
hand_transform: &GlobalTransform,
) {
match hand {
Hand::Left => {
let left_color = palettes::css::YELLOW_GREEN;
let off_color = palettes::css::BLUE;
let touch_color = palettes::css::GREEN;
let pressed_color = palettes::css::RED;
let grip_quat_offset = Quat::from_rotation_x(-1.4);
let face_quat_offset = Quat::from_rotation_x(1.05);
let trans = hand_transform.compute_transform();
let controller_vec3 = trans.translation;
let controller_quat = trans.rotation;
let face_quat = controller_quat.mul_quat(face_quat_offset);
let face_quat_normal = face_quat.mul_vec3(Vec3::Z);
//draw grip
gizmos.rect(
controller_vec3,
controller_quat * grip_quat_offset,
Vec2::new(0.05, 0.1),
left_color,
);
let face_translation_offset = Quat::from_rotation_x(-1.7); //direction to move the face from the controller tracking point
let face_translation_vec3 = controller_vec3
+ controller_quat
.mul_quat(face_translation_offset)
.mul_vec3(Vec3::Y * 0.075); //distance to move face by
//draw face
gizmos.circle(
face_translation_vec3,
Dir3::new_unchecked(face_quat_normal),
0.04,
palettes::css::YELLOW_GREEN,
);
//button b
let mut b_color = off_color;
if controller.y_button_touched() {
b_color = touch_color;
}
if controller.y_button() {
b_color = pressed_color;
}
let b_offset_quat = face_quat;
let b_translation_vec3 =
face_translation_vec3 + b_offset_quat.mul_vec3(Vec3::new(0.025, -0.01, 0.0));
gizmos.circle(
b_translation_vec3,
Dir3::new_unchecked(face_quat_normal),
0.0075,
b_color,
);
//button a
let mut a_color = off_color;
if controller.x_button_touched() {
a_color = touch_color;
}
if controller.x_button() {
a_color = pressed_color;
}
let a_offset_quat = face_quat;
let a_translation_vec3 =
face_translation_vec3 + a_offset_quat.mul_vec3(Vec3::new(0.025, 0.01, 0.0));
gizmos.circle(
a_translation_vec3,
Dir3::new_unchecked(face_quat_normal),
0.0075,
a_color,
);
//joystick
let joystick_offset_quat = face_quat;
let joystick_base_vec =
face_translation_vec3 + joystick_offset_quat.mul_vec3(Vec3::new(-0.02, 0.0, 0.0));
let mut joystick_color = off_color;
if controller.thumbstick_touch(Hand::Left) {
joystick_color = touch_color;
}
//base
gizmos.circle(
joystick_base_vec,
Dir3::new_unchecked(face_quat_normal),
0.014,
joystick_color,
);
let stick = controller.thumbstick(Hand::Left);
let input = Vec3::new(stick.x, -stick.y, 0.0);
let joystick_top_vec = face_translation_vec3
+ joystick_offset_quat.mul_vec3(Vec3::new(-0.02, 0.0, -0.01))
+ joystick_offset_quat.mul_vec3(input * 0.01);
//top
gizmos.circle(
joystick_top_vec,
Dir3::new_unchecked(face_quat_normal),
0.005,
joystick_color,
);
//trigger
let trigger_state = controller.trigger(Hand::Left);
let trigger_rotation = Quat::from_rotation_x(-0.75 * trigger_state);
let mut trigger_color = off_color;
if controller.trigger_touched(Hand::Left) {
trigger_color = touch_color;
}
let trigger_transform = Transform {
translation: face_translation_vec3
+ face_quat
.mul_quat(trigger_rotation)
.mul_vec3(Vec3::new(0.0, 0.0, 0.02)),
rotation: face_quat.mul_quat(trigger_rotation),
scale: Vec3 {
x: 0.01,
y: 0.02,
z: 0.03,
},
};
gizmos.cuboid(trigger_transform, trigger_color);
}
Hand::Right => {
//get right controller
let right_color = palettes::css::YELLOW_GREEN;
let off_color = palettes::css::BLUE;
let touch_color = palettes::css::GREEN;
let pressed_color = palettes::css::RED;
let grip_quat_offset = Quat::from_rotation_x(-1.4);
let face_quat_offset = Quat::from_rotation_x(1.05);
let trans = hand_transform.compute_transform();
let controller_vec3 = trans.translation;
let controller_quat = trans.rotation;
let face_quat = controller_quat.mul_quat(face_quat_offset);
let face_quat_normal = face_quat.mul_vec3(Vec3::Z);
let _squeeze = controller.squeeze(Hand::Right);
//info!("{:?}", squeeze);
//grip
gizmos.rect(
controller_vec3,
controller_quat * grip_quat_offset,
Vec2::new(0.05, 0.1),
right_color,
);
let face_translation_offset = Quat::from_rotation_x(-1.7); //direction to move the face from the controller tracking point
let face_translation_vec3 = controller_vec3
+ controller_quat
.mul_quat(face_translation_offset)
.mul_vec3(Vec3::Y * 0.075); //distance to move face by
//draw face
gizmos.circle(
face_translation_vec3,
Dir3::new_unchecked(face_quat_normal),
0.04,
palettes::css::YELLOW_GREEN,
);
//button b
let mut b_color = off_color;
if controller.b_button_touched() {
b_color = touch_color;
}
if controller.b_button() {
b_color = pressed_color;
}
let b_offset_quat = face_quat;
let b_translation_vec3 =
face_translation_vec3 + b_offset_quat.mul_vec3(Vec3::new(-0.025, -0.01, 0.0));
gizmos.circle(
b_translation_vec3,
Dir3::new_unchecked(face_quat_normal),
0.0075,
b_color,
);
//button a
let mut a_color = off_color;
if controller.a_button_touched() {
a_color = touch_color;
}
if controller.a_button() {
a_color = pressed_color;
}
let a_offset_quat = face_quat;
let a_translation_vec3 =
face_translation_vec3 + a_offset_quat.mul_vec3(Vec3::new(-0.025, 0.01, 0.0));
gizmos.circle(
a_translation_vec3,
Dir3::new_unchecked(face_quat_normal),
0.0075,
a_color,
);
//joystick time
let joystick_offset_quat = face_quat;
let joystick_base_vec =
face_translation_vec3 + joystick_offset_quat.mul_vec3(Vec3::new(0.02, 0.0, 0.0));
let mut joystick_color = off_color;
if controller.thumbstick_touch(Hand::Right) {
joystick_color = touch_color;
}
//base
gizmos.circle(
joystick_base_vec,
Dir3::new_unchecked(face_quat_normal),
0.014,
joystick_color,
);
let stick = controller.thumbstick(Hand::Right);
let input = Vec3::new(stick.x, -stick.y, 0.0);
let joystick_top_vec = face_translation_vec3
+ joystick_offset_quat.mul_vec3(Vec3::new(0.02, 0.0, -0.01))
+ joystick_offset_quat.mul_vec3(input * 0.01);
//top
gizmos.circle(
joystick_top_vec,
Dir3::new_unchecked(face_quat_normal),
0.005,
joystick_color,
);
//trigger
let trigger_state = controller.trigger(Hand::Right);
let trigger_rotation = Quat::from_rotation_x(-0.75 * trigger_state);
let mut trigger_color = off_color;
if controller.trigger_touched(Hand::Right) {
trigger_color = touch_color;
}
let trigger_transform = Transform {
translation: face_translation_vec3
+ face_quat
.mul_quat(trigger_rotation)
.mul_vec3(Vec3::new(0.0, 0.0, 0.02)),
rotation: face_quat.mul_quat(trigger_rotation),
scale: Vec3 {
x: 0.01,
y: 0.02,
z: 0.03,
},
};
gizmos.cuboid(trigger_transform, trigger_color);
}
}
}

View File

@@ -1,519 +0,0 @@
use bevy::prelude::{Quat, Transform, Vec3};
use openxr::{Posef, Quaternionf, Vector3f};
use super::Hand;
pub fn get_simulated_open_hand_transforms(hand: Hand) -> [Transform; 26] {
let test_hand_bones: [Vec3; 26] = [
Vec3 {
x: 0.0,
y: 0.0,
z: 0.0,
}, //palm
Vec3 {
x: 0.0,
y: 0.0,
z: -0.04,
}, //wrist
Vec3 {
x: -0.02,
y: 0.00,
z: 0.015,
}, //thumb
Vec3 {
x: 0.0,
y: 0.0,
z: 0.03,
},
Vec3 {
x: 0.0,
y: 0.0,
z: 0.024,
},
Vec3 {
x: 0.0,
y: 0.0,
z: 0.024,
},
Vec3 {
x: -0.01,
y: -0.015,
z: 0.0155,
}, //index
Vec3 {
x: 0.0,
y: 0.0,
z: 0.064,
},
Vec3 {
x: 0.0,
y: 0.0,
z: 0.037,
},
Vec3 {
x: 0.0,
y: 0.0,
z: 0.02,
},
Vec3 {
x: 0.0,
y: 0.0,
z: 0.01,
},
Vec3 {
x: 0.0,
y: -0.02,
z: 0.016,
}, //middle
Vec3 {
x: 0.0,
y: 0.0,
z: 0.064,
},
Vec3 {
x: 0.0,
y: 0.0,
z: 0.037,
},
Vec3 {
x: 0.0,
y: 0.0,
z: 0.02,
},
Vec3 {
x: 0.0,
y: 0.0,
z: 0.01,
},
Vec3 {
x: 0.01,
y: -0.015,
z: 0.015,
}, //ring
Vec3 {
x: 0.0,
y: 0.0,
z: 0.064,
},
Vec3 {
x: 0.0,
y: 0.0,
z: 0.037,
},
Vec3 {
x: 0.0,
y: 0.0,
z: 0.02,
},
Vec3 {
x: 0.0,
y: 0.0,
z: 0.01,
},
Vec3 {
x: 0.02,
y: -0.01,
z: 0.015,
}, //little
Vec3 {
x: 0.0,
y: 0.0,
z: 0.064,
},
Vec3 {
x: 0.0,
y: 0.0,
z: 0.037,
},
Vec3 {
x: 0.0,
y: 0.0,
z: 0.02,
},
Vec3 {
x: 0.0,
y: 0.0,
z: 0.01,
},
];
bones_to_transforms(test_hand_bones, hand)
}
fn bones_to_transforms(hand_bones: [Vec3; 26], hand: Hand) -> [Transform; 26] {
match hand {
Hand::Left => {
let mut result_array: [Transform; 26] = [Transform::default(); 26];
for (place, data) in result_array.iter_mut().zip(hand_bones.iter()) {
*place = Transform {
translation: Vec3 {
x: -data.x,
y: -data.y,
z: -data.z,
},
rotation: Quat::IDENTITY,
scale: Vec3::splat(1.0),
}
}
return result_array;
}
Hand::Right => {
let mut result_array: [Transform; 26] = [Transform::default(); 26];
for (place, data) in result_array.iter_mut().zip(hand_bones.iter()) {
*place = Transform {
translation: Vec3 {
x: data.x,
y: -data.y,
z: -data.z,
},
rotation: Quat::IDENTITY,
scale: Vec3::splat(1.0),
}
}
return result_array;
}
}
}
pub fn get_test_hand_pose_array() -> [Posef; 26] {
let test_hand_pose: [Posef; 26] = [
Posef {
position: Vector3f {
x: 0.0,
y: 0.0,
z: 0.0,
},
orientation: Quaternionf {
x: -0.267,
y: 0.849,
z: 0.204,
w: 0.407,
},
}, //palm
Posef {
position: Vector3f {
x: 0.02,
y: -0.040,
z: -0.015,
},
orientation: Quaternionf {
x: -0.267,
y: 0.849,
z: 0.204,
w: 0.407,
},
},
Posef {
position: Vector3f {
x: 0.019,
y: -0.037,
z: 0.011,
},
orientation: Quaternionf {
x: -0.744,
y: -0.530,
z: 0.156,
w: -0.376,
},
},
Posef {
position: Vector3f {
x: 0.015,
y: -0.014,
z: 0.047,
},
orientation: Quaternionf {
x: -0.786,
y: -0.550,
z: 0.126,
w: -0.254,
},
},
Posef {
position: Vector3f {
x: 0.004,
y: 0.003,
z: 0.068,
},
orientation: Quaternionf {
x: -0.729,
y: -0.564,
z: 0.027,
w: -0.387,
},
},
Posef {
position: Vector3f {
x: -0.009,
y: 0.011,
z: 0.072,
},
orientation: Quaternionf {
x: -0.585,
y: -0.548,
z: -0.140,
w: -0.582,
},
},
Posef {
position: Vector3f {
x: 0.027,
y: -0.021,
z: 0.001,
},
orientation: Quaternionf {
x: -0.277,
y: -0.826,
z: 0.317,
w: -0.376,
},
},
Posef {
position: Vector3f {
x: -0.002,
y: 0.026,
z: 0.034,
},
orientation: Quaternionf {
x: -0.277,
y: -0.826,
z: 0.317,
w: -0.376,
},
},
Posef {
position: Vector3f {
x: -0.023,
y: 0.049,
z: 0.055,
},
orientation: Quaternionf {
x: -0.244,
y: -0.843,
z: 0.256,
w: -0.404,
},
},
Posef {
position: Vector3f {
x: -0.037,
y: 0.059,
z: 0.067,
},
orientation: Quaternionf {
x: -0.200,
y: -0.866,
z: 0.165,
w: -0.428,
},
},
Posef {
position: Vector3f {
x: -0.045,
y: 0.063,
z: 0.073,
},
orientation: Quaternionf {
x: -0.172,
y: -0.874,
z: 0.110,
w: -0.440,
},
},
Posef {
position: Vector3f {
x: 0.021,
y: -0.017,
z: -0.007,
},
orientation: Quaternionf {
x: -0.185,
y: -0.817,
z: 0.370,
w: -0.401,
},
},
Posef {
position: Vector3f {
x: -0.011,
y: 0.029,
z: 0.018,
},
orientation: Quaternionf {
x: -0.185,
y: -0.817,
z: 0.370,
w: -0.401,
},
},
Posef {
position: Vector3f {
x: -0.034,
y: 0.06,
z: 0.033,
},
orientation: Quaternionf {
x: -0.175,
y: -0.809,
z: 0.371,
w: -0.420,
},
},
Posef {
position: Vector3f {
x: -0.051,
y: 0.072,
z: 0.045,
},
orientation: Quaternionf {
x: -0.109,
y: -0.856,
z: 0.245,
w: -0.443,
},
},
Posef {
position: Vector3f {
x: -0.06,
y: 0.077,
z: 0.051,
},
orientation: Quaternionf {
x: -0.075,
y: -0.871,
z: 0.180,
w: -0.450,
},
},
Posef {
position: Vector3f {
x: 0.013,
y: -0.017,
z: -0.015,
},
orientation: Quaternionf {
x: -0.132,
y: -0.786,
z: 0.408,
w: -0.445,
},
},
Posef {
position: Vector3f {
x: -0.02,
y: 0.025,
z: 0.0,
},
orientation: Quaternionf {
x: -0.132,
y: -0.786,
z: 0.408,
w: -0.445,
},
},
Posef {
position: Vector3f {
x: -0.042,
y: 0.055,
z: 0.007,
},
orientation: Quaternionf {
x: -0.131,
y: -0.762,
z: 0.432,
w: -0.464,
},
},
Posef {
position: Vector3f {
x: -0.06,
y: 0.069,
z: 0.015,
},
orientation: Quaternionf {
x: -0.071,
y: -0.810,
z: 0.332,
w: -0.477,
},
},
Posef {
position: Vector3f {
x: -0.069,
y: 0.075,
z: 0.02,
},
orientation: Quaternionf {
x: -0.029,
y: -0.836,
z: 0.260,
w: -0.482,
},
},
Posef {
position: Vector3f {
x: 0.004,
y: -0.022,
z: -0.022,
},
orientation: Quaternionf {
x: -0.060,
y: -0.749,
z: 0.481,
w: -0.452,
},
},
Posef {
position: Vector3f {
x: -0.028,
y: 0.018,
z: -0.015,
},
orientation: Quaternionf {
x: -0.060,
y: -0.749,
z: 0.481,
w: -0.452,
},
},
Posef {
position: Vector3f {
x: -0.046,
y: 0.042,
z: -0.017,
},
orientation: Quaternionf {
x: -0.061,
y: -0.684,
z: 0.534,
w: -0.493,
},
},
Posef {
position: Vector3f {
x: -0.059,
y: 0.053,
z: -0.015,
},
orientation: Quaternionf {
x: 0.002,
y: -0.745,
z: 0.444,
w: -0.498,
},
},
Posef {
position: Vector3f {
x: -0.068,
y: 0.059,
z: -0.013,
},
orientation: Quaternionf {
x: 0.045,
y: -0.780,
z: 0.378,
w: -0.496,
},
},
];
return test_hand_pose;
}

View File

@@ -1,258 +0,0 @@
use bevy::{
color::{palettes, Srgba},
core::Name,
prelude::{
default, Color, Commands, Component, Deref, DerefMut, Entity, Gizmos, Plugin, PostUpdate,
Query, Resource, SpatialBundle, Startup, Transform,
},
transform::components::GlobalTransform,
};
use crate::xr_input::{trackers::OpenXRTracker, Hand};
use super::{BoneTrackingStatus, HandBone};
/// add debug renderer for controllers
// #[derive(Default)]
// pub struct OpenXrHandInput;
//
// impl Plugin for OpenXrHandInput {
// fn build(&self, app: &mut bevy::prelude::App) {
// app.add_systems(Startup, spawn_hand_entities);
// }
// }
/// add debug renderer for controllers
#[derive(Default)]
pub struct HandInputDebugRenderer;
impl Plugin for HandInputDebugRenderer {
fn build(&self, app: &mut bevy::prelude::App) {
app.add_systems(PostUpdate, draw_hand_entities);
}
}
#[derive(Resource, Default, Clone, Copy)]
pub struct HandsResource {
pub left: HandResource,
pub right: HandResource,
}
#[derive(Clone, Copy)]
pub struct HandResource {
pub palm: Entity,
pub wrist: Entity,
pub thumb: ThumbResource,
pub index: IndexResource,
pub middle: MiddleResource,
pub ring: RingResource,
pub little: LittleResource,
}
impl Default for HandResource {
fn default() -> Self {
Self {
palm: Entity::PLACEHOLDER,
wrist: Entity::PLACEHOLDER,
thumb: Default::default(),
index: Default::default(),
middle: Default::default(),
ring: Default::default(),
little: Default::default(),
}
}
}
#[derive(Clone, Copy)]
pub struct ThumbResource {
pub metacarpal: Entity,
pub proximal: Entity,
pub distal: Entity,
pub tip: Entity,
}
impl Default for ThumbResource {
fn default() -> Self {
Self {
metacarpal: Entity::PLACEHOLDER,
proximal: Entity::PLACEHOLDER,
distal: Entity::PLACEHOLDER,
tip: Entity::PLACEHOLDER,
}
}
}
#[derive(Clone, Copy)]
pub struct IndexResource {
pub metacarpal: Entity,
pub proximal: Entity,
pub intermediate: Entity,
pub distal: Entity,
pub tip: Entity,
}
impl Default for IndexResource {
fn default() -> Self {
Self {
metacarpal: Entity::PLACEHOLDER,
proximal: Entity::PLACEHOLDER,
intermediate: Entity::PLACEHOLDER,
distal: Entity::PLACEHOLDER,
tip: Entity::PLACEHOLDER,
}
}
}
#[derive(Clone, Copy)]
pub struct MiddleResource {
pub metacarpal: Entity,
pub proximal: Entity,
pub intermediate: Entity,
pub distal: Entity,
pub tip: Entity,
}
impl Default for MiddleResource {
fn default() -> Self {
Self {
metacarpal: Entity::PLACEHOLDER,
proximal: Entity::PLACEHOLDER,
intermediate: Entity::PLACEHOLDER,
distal: Entity::PLACEHOLDER,
tip: Entity::PLACEHOLDER,
}
}
}
#[derive(Clone, Copy)]
pub struct RingResource {
pub metacarpal: Entity,
pub proximal: Entity,
pub intermediate: Entity,
pub distal: Entity,
pub tip: Entity,
}
impl Default for RingResource {
fn default() -> Self {
Self {
metacarpal: Entity::PLACEHOLDER,
proximal: Entity::PLACEHOLDER,
intermediate: Entity::PLACEHOLDER,
distal: Entity::PLACEHOLDER,
tip: Entity::PLACEHOLDER,
}
}
}
#[derive(Clone, Copy)]
pub struct LittleResource {
pub metacarpal: Entity,
pub proximal: Entity,
pub intermediate: Entity,
pub distal: Entity,
pub tip: Entity,
}
impl Default for LittleResource {
fn default() -> Self {
Self {
metacarpal: Entity::PLACEHOLDER,
proximal: Entity::PLACEHOLDER,
intermediate: Entity::PLACEHOLDER,
distal: Entity::PLACEHOLDER,
tip: Entity::PLACEHOLDER,
}
}
}
pub fn spawn_hand_entities(mut commands: Commands) {
let hands = [Hand::Left, Hand::Right];
let bones = HandBone::get_all_bones();
//hand resource
let mut hand_resource = HandsResource { ..default() };
for hand in hands.iter() {
for bone in bones.iter() {
let boneid = commands
.spawn((
Name::new(format!("{:?} {:?}", hand, bone)),
SpatialBundle::default(),
*bone,
OpenXRTracker,
*hand,
BoneTrackingStatus::Tracked,
HandBoneRadius(0.1),
))
.id();
let hand_res = match hand {
Hand::Left => &mut hand_resource.left,
Hand::Right => &mut hand_resource.right,
};
match bone {
HandBone::Palm => hand_res.palm = boneid,
HandBone::Wrist => hand_res.wrist = boneid,
HandBone::ThumbMetacarpal => hand_res.thumb.metacarpal = boneid,
HandBone::ThumbProximal => hand_res.thumb.proximal = boneid,
HandBone::ThumbDistal => hand_res.thumb.distal = boneid,
HandBone::ThumbTip => hand_res.thumb.tip = boneid,
HandBone::IndexMetacarpal => hand_res.index.metacarpal = boneid,
HandBone::IndexProximal => hand_res.index.proximal = boneid,
HandBone::IndexIntermediate => hand_res.index.intermediate = boneid,
HandBone::IndexDistal => hand_res.index.distal = boneid,
HandBone::IndexTip => hand_res.index.tip = boneid,
HandBone::MiddleMetacarpal => hand_res.middle.metacarpal = boneid,
HandBone::MiddleProximal => hand_res.middle.proximal = boneid,
HandBone::MiddleIntermediate => hand_res.middle.intermediate = boneid,
HandBone::MiddleDistal => hand_res.middle.distal = boneid,
HandBone::MiddleTip => hand_res.middle.tip = boneid,
HandBone::RingMetacarpal => hand_res.ring.metacarpal = boneid,
HandBone::RingProximal => hand_res.ring.proximal = boneid,
HandBone::RingIntermediate => hand_res.ring.intermediate = boneid,
HandBone::RingDistal => hand_res.ring.distal = boneid,
HandBone::RingTip => hand_res.ring.tip = boneid,
HandBone::LittleMetacarpal => hand_res.little.metacarpal = boneid,
HandBone::LittleProximal => hand_res.little.proximal = boneid,
HandBone::LittleIntermediate => hand_res.little.intermediate = boneid,
HandBone::LittleDistal => hand_res.little.distal = boneid,
HandBone::LittleTip => hand_res.little.tip = boneid,
}
}
}
commands.insert_resource(hand_resource);
}
#[derive(Debug, Component, DerefMut, Deref)]
pub struct HandBoneRadius(pub f32);
pub fn draw_hand_entities(
mut gizmos: Gizmos,
query: Query<(&GlobalTransform, &HandBone, &HandBoneRadius)>,
) {
for (transform, hand_bone, hand_bone_radius) in query.iter() {
let (_, color) = get_bone_gizmo_style(hand_bone);
let (_, rotation, translation) = transform.to_scale_rotation_translation();
gizmos.sphere(translation, rotation, hand_bone_radius.0, color);
}
}
pub(crate) fn get_bone_gizmo_style(hand_bone: &HandBone) -> (f32, Srgba) {
match hand_bone {
HandBone::Palm => (0.01, palettes::css::WHITE),
HandBone::Wrist => (0.01, palettes::css::GRAY),
HandBone::ThumbMetacarpal => (0.01, palettes::css::RED),
HandBone::ThumbProximal => (0.008, palettes::css::RED),
HandBone::ThumbDistal => (0.006, palettes::css::RED),
HandBone::ThumbTip => (0.004, palettes::css::RED),
HandBone::IndexMetacarpal => (0.01, palettes::css::ORANGE),
HandBone::IndexProximal => (0.008, palettes::css::ORANGE),
HandBone::IndexIntermediate => (0.006, palettes::css::ORANGE),
HandBone::IndexDistal => (0.004, palettes::css::ORANGE),
HandBone::IndexTip => (0.002, palettes::css::ORANGE),
HandBone::MiddleMetacarpal => (0.01, palettes::css::YELLOW),
HandBone::MiddleProximal => (0.008, palettes::css::YELLOW),
HandBone::MiddleIntermediate => (0.006, palettes::css::YELLOW),
HandBone::MiddleDistal => (0.004, palettes::css::YELLOW),
HandBone::MiddleTip => (0.002, palettes::css::YELLOW),
HandBone::RingMetacarpal => (0.01, palettes::css::GREEN),
HandBone::RingProximal => (0.008, palettes::css::GREEN),
HandBone::RingIntermediate => (0.006, palettes::css::GREEN),
HandBone::RingDistal => (0.004, palettes::css::GREEN),
HandBone::RingTip => (0.002, palettes::css::GREEN),
HandBone::LittleMetacarpal => (0.01, palettes::css::BLUE),
HandBone::LittleProximal => (0.008, palettes::css::BLUE),
HandBone::LittleIntermediate => (0.006, palettes::css::BLUE),
HandBone::LittleDistal => (0.004, palettes::css::BLUE),
HandBone::LittleTip => (0.002, palettes::css::BLUE),
}
}

View File

@@ -1,546 +0,0 @@
use std::f32::consts::PI;
use bevy::prelude::*;
use openxr::{ActionTy, HandJoint};
use super::common::{get_bone_gizmo_style, HandBoneRadius};
use crate::{
resources::{XrInstance, XrSession},
xr_init::{xr_only, XrSetup},
xr_input::{
actions::{
ActionHandednes, ActionType, SetupActionSet, SetupActionSets, XrActionSets, XrBinding,
},
hand_poses::get_simulated_open_hand_transforms,
trackers::{OpenXRLeftController, OpenXRRightController, OpenXRTrackingRoot},
Hand,
},
};
use super::{BoneTrackingStatus, HandBone};
pub enum TouchValue<T: ActionTy> {
None,
Touched(T),
}
pub struct HandEmulationPlugin;
impl Plugin for HandEmulationPlugin {
fn build(&self, app: &mut App) {
app.add_systems(Update, update_hand_skeleton_from_emulated.run_if(xr_only()));
app.add_systems(XrSetup, setup_hand_emulation_action_set);
}
}
const HAND_ACTION_SET: &str = "hand_pose_approx";
fn setup_hand_emulation_action_set(mut action_sets: ResMut<SetupActionSets>) {
let action_set =
action_sets.add_action_set(HAND_ACTION_SET, "Hand Pose Approximaiton".into(), 0);
action_set.new_action(
"thumb_touch",
"Thumb Touched".into(),
ActionType::Bool,
ActionHandednes::Double,
);
action_set.new_action(
"thumb_x",
"Thumb X".into(),
ActionType::F32,
ActionHandednes::Double,
);
action_set.new_action(
"thumb_y",
"Thumb Y".into(),
ActionType::F32,
ActionHandednes::Double,
);
action_set.new_action(
"index_touch",
"Index Finger Touched".into(),
ActionType::Bool,
ActionHandednes::Double,
);
action_set.new_action(
"index_value",
"Index Finger Pull".into(),
ActionType::F32,
ActionHandednes::Double,
);
action_set.new_action(
"middle_value",
"Middle Finger Pull".into(),
ActionType::F32,
ActionHandednes::Double,
);
action_set.new_action(
"ring_value",
"Ring Finger Pull".into(),
ActionType::F32,
ActionHandednes::Double,
);
action_set.new_action(
"little_value",
"Little Finger Pull".into(),
ActionType::F32,
ActionHandednes::Double,
);
suggest_oculus_touch_profile(action_set);
}
fn suggest_oculus_touch_profile(action_set: &mut SetupActionSet) {
action_set.suggest_binding(
"/interaction_profiles/oculus/touch_controller",
&[
XrBinding::new("thumb_x", "/user/hand/left/input/thumbstick/x"),
XrBinding::new("thumb_x", "/user/hand/right/input/thumbstick/x"),
XrBinding::new("thumb_y", "/user/hand/left/input/thumbstick/y"),
XrBinding::new("thumb_y", "/user/hand/right/input/thumbstick/y"),
XrBinding::new("thumb_touch", "/user/hand/left/input/thumbstick/touch"),
XrBinding::new("thumb_touch", "/user/hand/right/input/thumbstick/touch"),
XrBinding::new("thumb_touch", "/user/hand/left/input/x/touch"),
XrBinding::new("thumb_touch", "/user/hand/left/input/y/touch"),
XrBinding::new("thumb_touch", "/user/hand/right/input/a/touch"),
XrBinding::new("thumb_touch", "/user/hand/right/input/b/touch"),
XrBinding::new("thumb_touch", "/user/hand/left/input/thumbrest/touch"),
XrBinding::new("thumb_touch", "/user/hand/right/input/thumbrest/touch"),
XrBinding::new("index_touch", "/user/hand/left/input/trigger/touch"),
XrBinding::new("index_value", "/user/hand/left/input/trigger/value"),
XrBinding::new("index_touch", "/user/hand/right/input/trigger/touch"),
XrBinding::new("index_value", "/user/hand/right/input/trigger/value"),
XrBinding::new("middle_value", "/user/hand/left/input/squeeze/value"),
XrBinding::new("middle_value", "/user/hand/right/input/squeeze/value"),
XrBinding::new("ring_value", "/user/hand/left/input/squeeze/value"),
XrBinding::new("ring_value", "/user/hand/right/input/squeeze/value"),
XrBinding::new("little_value", "/user/hand/left/input/squeeze/value"),
XrBinding::new("little_value", "/user/hand/right/input/squeeze/value"),
],
);
}
#[allow(clippy::type_complexity)]
pub(crate) fn update_hand_skeleton_from_emulated(
session: Res<XrSession>,
instance: Res<XrInstance>,
action_sets: Res<XrActionSets>,
left_controller_transform: Query<&Transform, With<OpenXRLeftController>>,
right_controller_transform: Query<&Transform, With<OpenXRRightController>>,
mut bones: Query<
(
&mut Transform,
&HandBone,
&Hand,
&BoneTrackingStatus,
&mut HandBoneRadius,
),
(
Without<OpenXRLeftController>,
Without<OpenXRRightController>,
Without<OpenXRTrackingRoot>,
),
>,
) {
//get the transforms outside the loop
let left = left_controller_transform.get_single();
let right = right_controller_transform.get_single();
let mut data: [[Transform; 26]; 2] = [[Transform::default(); 26]; 2];
for (subaction_path, hand) in [
(
instance.string_to_path("/user/hand/left").unwrap(),
Hand::Left,
),
(
instance.string_to_path("/user/hand/right").unwrap(),
Hand::Right,
),
] {
let thumb_curl = match action_sets
.get_action_bool(HAND_ACTION_SET, "thumb_touch")
.unwrap()
.state(&session, subaction_path)
.unwrap()
.current_state
{
true => 1.0,
false => 0.0,
};
let index_curl = action_sets
.get_action_f32(HAND_ACTION_SET, "index_value")
.unwrap()
.state(&session, subaction_path)
.unwrap()
.current_state;
let middle_curl = action_sets
.get_action_f32(HAND_ACTION_SET, "middle_value")
.unwrap()
.state(&session, subaction_path)
.unwrap()
.current_state;
let ring_curl = action_sets
.get_action_f32(HAND_ACTION_SET, "ring_value")
.unwrap()
.state(&session, subaction_path)
.unwrap()
.current_state;
let little_curl = action_sets
.get_action_f32(HAND_ACTION_SET, "little_value")
.unwrap()
.state(&session, subaction_path)
.unwrap()
.current_state;
match hand {
Hand::Left => match left {
Ok(hand_transform) => {
data[0] = update_hand_bones_emulated(
hand_transform,
hand,
thumb_curl,
index_curl,
middle_curl,
ring_curl,
little_curl,
);
}
Err(_) => debug!("no left controller transform for hand bone emulation"),
},
Hand::Right => match right {
Ok(hand_transform) => {
data[1] = update_hand_bones_emulated(
hand_transform,
hand,
thumb_curl,
index_curl,
middle_curl,
ring_curl,
little_curl,
);
}
Err(_) => debug!("no right controller transform for hand bone emulation"),
},
}
}
for (mut t, bone, hand, status, mut radius) in bones.iter_mut() {
match status {
BoneTrackingStatus::Emulated => {}
BoneTrackingStatus::Tracked => continue,
}
radius.0 = get_bone_gizmo_style(bone).0;
*t = data[match hand {
Hand::Left => 0,
Hand::Right => 1,
}][bone.get_index_from_bone()];
// *t = t.with_scale(trt.scale);
// *t = t.with_rotation(trt.rotation * t.rotation);
// *t = t.with_translation(trt.transform_point(t.translation));
}
}
pub fn update_hand_bones_emulated(
controller_transform: &Transform,
hand: Hand,
thumb_curl: f32,
index_curl: f32,
middle_curl: f32,
ring_curl: f32,
little_curl: f32,
) -> [Transform; 26] {
let left_hand_rot = Quat::from_rotation_y(PI);
let hand_translation: Vec3 = controller_transform.translation;
let controller_quat: Quat = match hand {
Hand::Left => controller_transform.rotation.mul_quat(left_hand_rot),
Hand::Right => controller_transform.rotation,
};
let splay_direction = match hand {
Hand::Left => -1.0,
Hand::Right => 1.0,
};
//lets make a structure to hold our calculated transforms for now
let mut calc_transforms = [Transform::default(); 26];
//get palm quat
let y = Quat::from_rotation_y(-90.0 * PI / 180.0);
let x = Quat::from_rotation_x(-90.0 * PI / 180.0);
let palm_quat = controller_quat.mul_quat(y).mul_quat(x);
//get simulated bones
let hand_transform_array: [Transform; 26] = get_simulated_open_hand_transforms(hand);
//palm
let palm = hand_transform_array[HandJoint::PALM];
calc_transforms[HandJoint::PALM] = Transform {
translation: hand_translation + palm.translation,
..default()
};
//wrist
let wrist = hand_transform_array[HandJoint::WRIST];
calc_transforms[HandJoint::WRIST] = Transform {
translation: hand_translation + palm.translation + palm_quat.mul_vec3(wrist.translation),
..default()
};
//thumb
let thumb_joints = [
HandJoint::THUMB_METACARPAL,
HandJoint::THUMB_PROXIMAL,
HandJoint::THUMB_DISTAL,
HandJoint::THUMB_TIP,
];
let mut prior_start: Option<Vec3> = None;
let mut prior_quat: Option<Quat> = None;
let mut prior_vector: Option<Vec3> = None;
let splay = Quat::from_rotation_y(splay_direction * 30.0 * PI / 180.0);
let huh = Quat::from_rotation_x(-35.0 * PI / 180.0);
let splay_quat = palm_quat.mul_quat(huh).mul_quat(splay);
for bone in thumb_joints.iter() {
match prior_start {
Some(start) => {
let curl_angle: f32 = get_bone_curl_angle(*bone, thumb_curl);
let tp_lrot = Quat::from_rotation_y(splay_direction * curl_angle * PI / 180.0);
let tp_quat = prior_quat.unwrap().mul_quat(tp_lrot);
let thumb_prox = hand_transform_array[*bone];
let tp_start = start + prior_vector.unwrap();
let tp_vector = tp_quat.mul_vec3(thumb_prox.translation);
prior_start = Some(tp_start);
prior_quat = Some(tp_quat);
prior_vector = Some(tp_vector);
//store it
calc_transforms[*bone] = Transform {
translation: tp_start + tp_vector,
..default()
};
}
None => {
let thumb_meta = hand_transform_array[*bone];
let tm_start = hand_translation
+ palm_quat.mul_vec3(palm.translation)
+ palm_quat.mul_vec3(wrist.translation);
let tm_vector = palm_quat.mul_vec3(thumb_meta.translation);
prior_start = Some(tm_start);
prior_quat = Some(splay_quat);
prior_vector = Some(tm_vector);
//store it
calc_transforms[*bone] = Transform {
translation: tm_start + tm_vector,
..default()
};
}
}
}
//index
let thumb_joints = [
HandJoint::INDEX_METACARPAL,
HandJoint::INDEX_PROXIMAL,
HandJoint::INDEX_INTERMEDIATE,
HandJoint::INDEX_DISTAL,
HandJoint::INDEX_TIP,
];
let mut prior_start: Option<Vec3> = None;
let mut prior_quat: Option<Quat> = None;
let mut prior_vector: Option<Vec3> = None;
let splay = Quat::from_rotation_y(splay_direction * 10.0 * PI / 180.0);
let splay_quat = palm_quat.mul_quat(splay);
for bone in thumb_joints.iter() {
match prior_start {
Some(start) => {
let curl_angle: f32 = get_bone_curl_angle(*bone, index_curl);
let tp_lrot = Quat::from_rotation_x(curl_angle * PI / 180.0);
let tp_quat = prior_quat.unwrap().mul_quat(tp_lrot);
let thumb_prox = hand_transform_array[*bone];
let tp_start = start + prior_vector.unwrap();
let tp_vector = tp_quat.mul_vec3(thumb_prox.translation);
prior_start = Some(tp_start);
prior_quat = Some(tp_quat);
prior_vector = Some(tp_vector);
//store it
calc_transforms[*bone] = Transform {
translation: tp_start + tp_vector,
..default()
};
}
None => {
let thumb_meta = hand_transform_array[*bone];
let tm_start = hand_translation
+ palm_quat.mul_vec3(palm.translation)
+ palm_quat.mul_vec3(wrist.translation);
let tm_vector = palm_quat.mul_vec3(thumb_meta.translation);
prior_start = Some(tm_start);
prior_quat = Some(splay_quat);
prior_vector = Some(tm_vector);
//store it
calc_transforms[*bone] = Transform {
translation: tm_start + tm_vector,
..default()
};
}
}
}
//middle
let thumb_joints = [
HandJoint::MIDDLE_METACARPAL,
HandJoint::MIDDLE_PROXIMAL,
HandJoint::MIDDLE_INTERMEDIATE,
HandJoint::MIDDLE_DISTAL,
HandJoint::MIDDLE_TIP,
];
let mut prior_start: Option<Vec3> = None;
let mut prior_quat: Option<Quat> = None;
let mut prior_vector: Option<Vec3> = None;
let splay = Quat::from_rotation_y(splay_direction * 0.0 * PI / 180.0);
let splay_quat = palm_quat.mul_quat(splay);
for bone in thumb_joints.iter() {
match prior_start {
Some(start) => {
let curl_angle: f32 = get_bone_curl_angle(*bone, middle_curl);
let tp_lrot = Quat::from_rotation_x(curl_angle * PI / 180.0);
let tp_quat = prior_quat.unwrap().mul_quat(tp_lrot);
let thumb_prox = hand_transform_array[*bone];
let tp_start = start + prior_vector.unwrap();
let tp_vector = tp_quat.mul_vec3(thumb_prox.translation);
prior_start = Some(tp_start);
prior_quat = Some(tp_quat);
prior_vector = Some(tp_vector);
//store it
calc_transforms[*bone] = Transform {
translation: tp_start + tp_vector,
..default()
};
}
None => {
let thumb_meta = hand_transform_array[*bone];
let tm_start = hand_translation
+ palm_quat.mul_vec3(palm.translation)
+ palm_quat.mul_vec3(wrist.translation);
let tm_vector = palm_quat.mul_vec3(thumb_meta.translation);
prior_start = Some(tm_start);
prior_quat = Some(splay_quat);
prior_vector = Some(tm_vector);
//store it
calc_transforms[*bone] = Transform {
translation: tm_start + tm_vector,
..default()
};
}
}
}
//ring
let thumb_joints = [
HandJoint::RING_METACARPAL,
HandJoint::RING_PROXIMAL,
HandJoint::RING_INTERMEDIATE,
HandJoint::RING_DISTAL,
HandJoint::RING_TIP,
];
let mut prior_start: Option<Vec3> = None;
let mut prior_quat: Option<Quat> = None;
let mut prior_vector: Option<Vec3> = None;
let splay = Quat::from_rotation_y(splay_direction * -10.0 * PI / 180.0);
let splay_quat = palm_quat.mul_quat(splay);
for bone in thumb_joints.iter() {
match prior_start {
Some(start) => {
let curl_angle: f32 = get_bone_curl_angle(*bone, ring_curl);
let tp_lrot = Quat::from_rotation_x(curl_angle * PI / 180.0);
let tp_quat = prior_quat.unwrap().mul_quat(tp_lrot);
let thumb_prox = hand_transform_array[*bone];
let tp_start = start + prior_vector.unwrap();
let tp_vector = tp_quat.mul_vec3(thumb_prox.translation);
prior_start = Some(tp_start);
prior_quat = Some(tp_quat);
prior_vector = Some(tp_vector);
//store it
calc_transforms[*bone] = Transform {
translation: tp_start + tp_vector,
..default()
};
}
None => {
let thumb_meta = hand_transform_array[*bone];
let tm_start = hand_translation
+ palm_quat.mul_vec3(palm.translation)
+ palm_quat.mul_vec3(wrist.translation);
let tm_vector = palm_quat.mul_vec3(thumb_meta.translation);
prior_start = Some(tm_start);
prior_quat = Some(splay_quat);
prior_vector = Some(tm_vector);
//store it
calc_transforms[*bone] = Transform {
translation: tm_start + tm_vector,
..default()
};
}
}
}
//little
let thumb_joints = [
HandJoint::LITTLE_METACARPAL,
HandJoint::LITTLE_PROXIMAL,
HandJoint::LITTLE_INTERMEDIATE,
HandJoint::LITTLE_DISTAL,
HandJoint::LITTLE_TIP,
];
let mut prior_start: Option<Vec3> = None;
let mut prior_quat: Option<Quat> = None;
let mut prior_vector: Option<Vec3> = None;
let splay = Quat::from_rotation_y(splay_direction * -20.0 * PI / 180.0);
let splay_quat = palm_quat.mul_quat(splay);
for bone in thumb_joints.iter() {
match prior_start {
Some(start) => {
let curl_angle: f32 = get_bone_curl_angle(*bone, little_curl);
let tp_lrot = Quat::from_rotation_x(curl_angle * PI / 180.0);
let tp_quat = prior_quat.unwrap().mul_quat(tp_lrot);
let thumb_prox = hand_transform_array[*bone];
let tp_start = start + prior_vector.unwrap();
let tp_vector = tp_quat.mul_vec3(thumb_prox.translation);
prior_start = Some(tp_start);
prior_quat = Some(tp_quat);
prior_vector = Some(tp_vector);
//store it
calc_transforms[*bone] = Transform {
translation: tp_start + tp_vector,
..default()
};
}
None => {
let thumb_meta = hand_transform_array[*bone];
let tm_start = hand_translation
+ palm_quat.mul_vec3(palm.translation)
+ palm_quat.mul_vec3(wrist.translation);
let tm_vector = palm_quat.mul_vec3(thumb_meta.translation);
prior_start = Some(tm_start);
prior_quat = Some(splay_quat);
prior_vector = Some(tm_vector);
//store it
calc_transforms[*bone] = Transform {
translation: tm_start + tm_vector,
..default()
};
}
}
}
calc_transforms
}
fn get_bone_curl_angle(bone: HandJoint, curl: f32) -> f32 {
let mul: f32 = match bone {
HandJoint::INDEX_PROXIMAL => 0.0,
HandJoint::MIDDLE_PROXIMAL => 0.0,
HandJoint::RING_PROXIMAL => 0.0,
HandJoint::LITTLE_PROXIMAL => 0.0,
HandJoint::THUMB_PROXIMAL => 0.0,
HandJoint::THUMB_TIP => 0.1,
HandJoint::THUMB_DISTAL => 0.1,
HandJoint::THUMB_METACARPAL => 0.1,
_ => 1.0,
};
let curl_angle = -((mul * curl * 80.0) + 5.0);
#[allow(clippy::needless_return)]
return curl_angle;
}

View File

@@ -1,212 +0,0 @@
use bevy::prelude::*;
use openxr::{HandTracker, Result, SpaceLocationFlags};
use super::common::HandBoneRadius;
use crate::{
input::XrInput,
resources::{XrFrameState, XrSession},
xr_init::xr_only,
xr_input::{hands::HandBone, Hand, QuatConv, Vec3Conv},
};
use super::BoneTrackingStatus;
#[derive(Resource, PartialEq)]
pub enum DisableHandTracking {
OnlyLeft,
OnlyRight,
Both,
}
pub struct HandTrackingPlugin;
#[derive(Resource)]
pub struct HandTrackingData {
left_hand: HandTracker,
right_hand: HandTracker,
}
impl HandTrackingData {
pub fn new(session: &XrSession) -> Result<HandTrackingData> {
let left = session.create_hand_tracker(openxr::HandEXT::LEFT)?;
let right = session.create_hand_tracker(openxr::HandEXT::RIGHT)?;
Ok(HandTrackingData {
left_hand: left,
right_hand: right,
})
}
pub fn get_ref<'a>(
&'a self,
input: &'a XrInput,
frame_state: &'a XrFrameState,
) -> HandTrackingRef<'a> {
HandTrackingRef {
tracking: self,
input,
frame_state,
}
}
}
pub struct HandTrackingRef<'a> {
tracking: &'a HandTrackingData,
input: &'a XrInput,
frame_state: &'a XrFrameState,
}
#[derive(Debug)]
pub struct HandJoint {
pub position: Vec3,
pub position_valid: bool,
pub position_tracked: bool,
pub orientation: Quat,
pub orientation_valid: bool,
pub orientation_tracked: bool,
pub radius: f32,
}
#[derive(Debug)]
pub struct HandJoints {
inner: [HandJoint; 26],
}
impl HandJoints {
pub fn inner(&self) -> &[HandJoint; 26] {
&self.inner
}
}
impl HandJoints {
pub fn get_joint(&self, bone: HandBone) -> &HandJoint {
&self.inner[bone.get_index_from_bone()]
}
}
impl<'a> HandTrackingRef<'a> {
pub fn get_poses(&self, side: Hand) -> Option<HandJoints> {
self.input
.stage
.locate_hand_joints(
match side {
Hand::Left => &self.tracking.left_hand,
Hand::Right => &self.tracking.right_hand,
},
self.frame_state.predicted_display_time,
)
.unwrap()
.map(|joints| {
joints
.into_iter()
.map(|joint| HandJoint {
position: joint.pose.position.to_vec3(),
orientation: joint.pose.orientation.to_quat(),
position_valid: joint
.location_flags
.contains(SpaceLocationFlags::POSITION_VALID),
position_tracked: joint
.location_flags
.contains(SpaceLocationFlags::POSITION_TRACKED),
orientation_valid: joint
.location_flags
.contains(SpaceLocationFlags::ORIENTATION_VALID),
orientation_tracked: joint
.location_flags
.contains(SpaceLocationFlags::ORIENTATION_TRACKED),
radius: joint.radius,
})
.collect::<Vec<HandJoint>>()
.try_into()
.unwrap()
})
.map(|joints| HandJoints { inner: joints })
}
}
impl Plugin for HandTrackingPlugin {
fn build(&self, app: &mut App) {
app.add_systems(
PreUpdate,
(
update_hand_bones
.run_if(|dh: Option<Res<DisableHandTracking>>| {
!dh.is_some_and(|v| *v == DisableHandTracking::Both)
})
.run_if(xr_only()),
update_tracking_state_on_disable,
),
);
}
}
fn update_tracking_state_on_disable(
mut is_off: Local<bool>,
disabled_tracking: Option<Res<DisableHandTracking>>,
mut tracking_states: Query<&mut BoneTrackingStatus>,
) {
if !*is_off
&& disabled_tracking
.as_ref()
.is_some_and(|t| **t == DisableHandTracking::Both)
{
tracking_states
.par_iter_mut()
.for_each(|mut state| *state = BoneTrackingStatus::Emulated);
}
*is_off = disabled_tracking
.as_ref()
.is_some_and(|t| **t == DisableHandTracking::Both);
}
pub fn update_hand_bones(
disabled_tracking: Option<Res<DisableHandTracking>>,
hand_tracking: Option<Res<HandTrackingData>>,
xr_input: Res<XrInput>,
xr_frame_state: Res<XrFrameState>,
mut bones: Query<(
&mut Transform,
&Hand,
&HandBone,
&mut HandBoneRadius,
&mut BoneTrackingStatus,
)>,
) {
let hand_ref = match hand_tracking.as_ref() {
Some(h) => h.get_ref(&xr_input, &xr_frame_state),
None => {
warn!("No Handtracking data!");
return;
}
};
let left_hand_data = hand_ref.get_poses(Hand::Left);
let right_hand_data = hand_ref.get_poses(Hand::Right);
// if left_hand_data.is_none() || right_hand_data.is_none() {
// error!("something is very wrong for hand_tracking!! doesn't have data for both hands!");
// }
bones
.par_iter_mut()
.for_each(|(mut transform, hand, bone, mut radius, mut status)| {
match (&hand, disabled_tracking.as_ref().map(|d| d.as_ref())) {
(Hand::Left, Some(DisableHandTracking::OnlyLeft)) => {
*status = BoneTrackingStatus::Emulated;
return;
}
(Hand::Right, Some(DisableHandTracking::OnlyRight)) => {
*status = BoneTrackingStatus::Emulated;
return;
}
_ => {}
}
let bone_data = match (hand, &left_hand_data, &right_hand_data) {
(Hand::Left, Some(data), _) => data.get_joint(*bone),
(Hand::Right, _, Some(data)) => data.get_joint(*bone),
(hand, left_data, right_data) => {
*status = BoneTrackingStatus::Emulated;
return;
}
};
if *status == BoneTrackingStatus::Emulated {
*status = BoneTrackingStatus::Tracked;
}
radius.0 = bone_data.radius;
transform.translation = bone_data.position;
transform.rotation = bone_data.orientation;
});
}

View File

@@ -1,174 +0,0 @@
use bevy::prelude::*;
use openxr::FormFactor;
use crate::{
resources::{XrInstance, XrSession},
xr_init::{XrCleanup, XrPreSetup, XrSetup},
};
use self::{
common::{spawn_hand_entities, HandBoneRadius, HandsResource},
hand_tracking::{DisableHandTracking, HandTrackingData},
};
use super::{trackers::OpenXRTracker, Hand};
pub mod common;
pub mod emulated;
pub mod hand_tracking;
pub struct HandPlugin;
impl Plugin for HandPlugin {
fn build(&self, app: &mut App) {
app.add_systems(XrPreSetup, check_for_handtracking);
app.add_systems(XrSetup, spawn_hand_entities);
app.add_systems(XrCleanup, despawn_hand_entities);
}
}
#[allow(clippy::type_complexity)]
fn despawn_hand_entities(
mut commands: Commands,
hand_entities: Query<
Entity,
(
With<OpenXRTracker>,
With<HandBone>,
With<BoneTrackingStatus>,
),
>,
) {
for e in &hand_entities {
commands.entity(e).despawn_recursive();
}
commands.remove_resource::<HandsResource>()
}
fn check_for_handtracking(
mut commands: Commands,
instance: Res<XrInstance>,
session: Res<XrSession>,
) {
let hands = instance.exts().ext_hand_tracking.is_some()
&& instance
.supports_hand_tracking(instance.system(FormFactor::HEAD_MOUNTED_DISPLAY).unwrap())
.is_ok_and(|v| v);
if hands {
info!("handtracking!");
commands.insert_resource(HandTrackingData::new(&session).unwrap());
} else {
commands.insert_resource(DisableHandTracking::Both);
}
}
#[derive(Component, Debug, Clone, Copy, PartialEq)]
pub enum BoneTrackingStatus {
Emulated,
Tracked,
}
#[derive(Component, Debug, Clone, Copy)]
pub enum HandBone {
Palm,
Wrist,
ThumbMetacarpal,
ThumbProximal,
ThumbDistal,
ThumbTip,
IndexMetacarpal,
IndexProximal,
IndexIntermediate,
IndexDistal,
IndexTip,
MiddleMetacarpal,
MiddleProximal,
MiddleIntermediate,
MiddleDistal,
MiddleTip,
RingMetacarpal,
RingProximal,
RingIntermediate,
RingDistal,
RingTip,
LittleMetacarpal,
LittleProximal,
LittleIntermediate,
LittleDistal,
LittleTip,
}
impl HandBone {
pub fn is_finger(&self) -> bool {
!matches!(self, HandBone::Wrist | HandBone::Palm)
}
pub fn is_metacarpal(&self) -> bool {
matches!(
self,
HandBone::ThumbMetacarpal
| HandBone::IndexMetacarpal
| HandBone::MiddleMetacarpal
| HandBone::RingMetacarpal
| HandBone::LittleMetacarpal
)
}
pub const fn get_all_bones() -> [HandBone; 26] {
[
HandBone::Palm,
HandBone::Wrist,
HandBone::ThumbMetacarpal,
HandBone::ThumbProximal,
HandBone::ThumbDistal,
HandBone::ThumbTip,
HandBone::IndexMetacarpal,
HandBone::IndexProximal,
HandBone::IndexIntermediate,
HandBone::IndexDistal,
HandBone::IndexTip,
HandBone::MiddleMetacarpal,
HandBone::MiddleProximal,
HandBone::MiddleIntermediate,
HandBone::MiddleDistal,
HandBone::MiddleTip,
HandBone::RingMetacarpal,
HandBone::RingProximal,
HandBone::RingIntermediate,
HandBone::RingDistal,
HandBone::RingTip,
HandBone::LittleMetacarpal,
HandBone::LittleProximal,
HandBone::LittleIntermediate,
HandBone::LittleDistal,
HandBone::LittleTip,
]
}
pub fn get_index_from_bone(&self) -> usize {
match &self {
HandBone::Palm => 0,
HandBone::Wrist => 1,
HandBone::ThumbMetacarpal => 2,
HandBone::ThumbProximal => 3,
HandBone::ThumbDistal => 4,
HandBone::ThumbTip => 5,
HandBone::IndexMetacarpal => 6,
HandBone::IndexProximal => 7,
HandBone::IndexIntermediate => 8,
HandBone::IndexDistal => 9,
HandBone::IndexTip => 10,
HandBone::MiddleMetacarpal => 11,
HandBone::MiddleProximal => 12,
HandBone::MiddleIntermediate => 13,
HandBone::MiddleDistal => 14,
HandBone::MiddleTip => 15,
HandBone::RingMetacarpal => 16,
HandBone::RingProximal => 17,
HandBone::RingIntermediate => 18,
HandBone::RingDistal => 19,
HandBone::RingTip => 20,
HandBone::LittleMetacarpal => 21,
HandBone::LittleProximal => 22,
HandBone::LittleIntermediate => 23,
HandBone::LittleDistal => 24,
HandBone::LittleTip => 25,
}
}
}

View File

@@ -1,385 +0,0 @@
use std::f32::consts::PI;
use bevy::color::palettes;
use bevy::log::{info, warn};
use bevy::prelude::{
Color, Component, Entity, Event, EventReader, EventWriter, Gizmos, GlobalTransform, Quat,
Query, Transform, Vec3, With, Without,
};
use super::trackers::{AimPose, OpenXRTrackingRoot};
#[derive(Component)]
pub struct XRDirectInteractor;
#[derive(Component)]
pub struct XRRayInteractor;
#[derive(Component)]
pub struct XRSocketInteractor;
#[derive(Component)]
pub struct Touched(pub bool);
#[derive(Component, Clone, Copy, PartialEq, PartialOrd, Debug)]
pub enum XRInteractableState {
Idle,
Hover,
Select,
}
impl Default for XRInteractableState {
fn default() -> Self {
XRInteractableState::Idle
}
}
#[derive(Component)]
pub enum XRInteractorState {
Idle,
Selecting,
}
impl Default for XRInteractorState {
fn default() -> Self {
XRInteractorState::Idle
}
}
#[derive(Component)]
pub enum XRSelection {
Empty,
Full(Entity),
}
impl Default for XRSelection {
fn default() -> Self {
XRSelection::Empty
}
}
#[derive(Component)]
pub struct XRInteractable;
pub fn draw_socket_gizmos(
mut gizmos: Gizmos,
interactor_query: Query<(
&GlobalTransform,
&XRInteractorState,
Entity,
&XRSocketInteractor,
)>,
) {
for (global, state, _entity, _socket) in interactor_query.iter() {
let mut transform = global.compute_transform().clone();
transform.scale = Vec3::splat(0.1);
let color = match state {
XRInteractorState::Idle => palettes::css::BLUE,
XRInteractorState::Selecting => palettes::css::PURPLE,
};
gizmos.cuboid(transform, color)
}
}
pub fn draw_interaction_gizmos(
mut gizmos: Gizmos,
interactable_query: Query<
(&GlobalTransform, &XRInteractableState),
(With<XRInteractable>, Without<XRDirectInteractor>),
>,
interactor_query: Query<
(
&GlobalTransform,
&XRInteractorState,
Option<&XRDirectInteractor>,
Option<&XRRayInteractor>,
Option<&AimPose>,
),
Without<XRInteractable>,
>,
tracking_root_query: Query<&mut Transform, With<OpenXRTrackingRoot>>,
) {
let Ok(root) = tracking_root_query.get_single() else {
warn!("no or more than one tracking root");
return;
};
for (global_transform, interactable_state) in interactable_query.iter() {
let transform = global_transform.compute_transform();
let color = match interactable_state {
XRInteractableState::Idle => palettes::css::RED,
XRInteractableState::Hover => palettes::css::YELLOW,
XRInteractableState::Select => palettes::css::GREEN,
};
gizmos.sphere(transform.translation, transform.rotation, 0.1, color);
}
for (interactor_global_transform, interactor_state, direct, ray, aim) in interactor_query.iter()
{
let transform = interactor_global_transform.compute_transform();
match direct {
Some(_) => {
let mut local = transform.clone();
local.scale = Vec3::splat(0.1);
let quat = Quat::from_euler(
bevy::prelude::EulerRot::XYZ,
45.0 * (PI / 180.0),
0.0,
45.0 * (PI / 180.0),
);
local.rotation = quat;
let color = match interactor_state {
XRInteractorState::Idle => palettes::css::BLUE,
XRInteractorState::Selecting => palettes::css::PURPLE,
};
gizmos.cuboid(local, color);
}
None => (),
}
match ray {
Some(_) => match aim {
Some(aim) => {
let color = match interactor_state {
XRInteractorState::Idle => palettes::css::BLUE,
XRInteractorState::Selecting => palettes::css::PURPLE,
};
gizmos.ray(
root.translation + root.rotation.mul_vec3(aim.0.translation),
root.rotation.mul_vec3(*aim.0.forward()),
color,
);
}
None => todo!(),
},
None => (),
}
}
}
#[derive(Event)]
pub struct InteractionEvent {
pub interactor: Entity,
pub interactable: Entity,
pub interactable_state: XRInteractableState,
}
pub fn socket_interactions(
interactable_query: Query<
(&GlobalTransform, &mut XRInteractableState, Entity),
(With<XRInteractable>, Without<XRSocketInteractor>),
>,
interactor_query: Query<
(
&GlobalTransform,
&XRInteractorState,
Entity,
&XRSocketInteractor,
),
Without<XRInteractable>,
>,
mut writer: EventWriter<InteractionEvent>,
) {
for interactable in interactable_query.iter() {
//for the interactables
for socket in interactor_query.iter() {
let interactor_global_transform = socket.0;
let xr_interactable_global_transform = interactable.0;
let interactor_state = socket.1;
//check for sphere overlaps
let size = 0.1;
if interactor_global_transform
.compute_transform()
.translation
.distance_squared(
xr_interactable_global_transform
.compute_transform()
.translation,
)
< (size * size) * 2.0
{
//check for selections first
match interactor_state {
XRInteractorState::Idle => {
let event = InteractionEvent {
interactor: socket.2,
interactable: interactable.2,
interactable_state: XRInteractableState::Hover,
};
writer.send(event);
}
XRInteractorState::Selecting => {
let event = InteractionEvent {
interactor: socket.2,
interactable: interactable.2,
interactable_state: XRInteractableState::Select,
};
writer.send(event);
}
}
}
}
}
}
pub fn interactions(
interactable_query: Query<
(&GlobalTransform, Entity),
(With<XRInteractable>, Without<XRDirectInteractor>),
>,
interactor_query: Query<
(
&GlobalTransform,
&XRInteractorState,
Entity,
Option<&XRDirectInteractor>,
Option<&XRRayInteractor>,
Option<&AimPose>,
),
Without<XRInteractable>,
>,
tracking_root_query: Query<&mut Transform, With<OpenXRTrackingRoot>>,
mut writer: EventWriter<InteractionEvent>,
) {
for (xr_interactable_global_transform, interactable_entity) in interactable_query.iter() {
for (interactor_global_transform, interactor_state, interactor_entity, direct, ray, aim) in
interactor_query.iter()
{
match direct {
Some(_) => {
//check for sphere overlaps
let size = 0.1;
if interactor_global_transform
.compute_transform()
.translation
.distance_squared(
xr_interactable_global_transform
.compute_transform()
.translation,
)
< (size * size) * 2.0
{
//check for selections first
match interactor_state {
XRInteractorState::Idle => {
let event = InteractionEvent {
interactor: interactor_entity,
interactable: interactable_entity,
interactable_state: XRInteractableState::Hover,
};
writer.send(event);
}
XRInteractorState::Selecting => {
let event = InteractionEvent {
interactor: interactor_entity,
interactable: interactable_entity,
interactable_state: XRInteractableState::Select,
};
writer.send(event);
}
}
}
}
None => (),
}
match ray {
Some(_) => {
//check for ray-sphere intersection
let sphere_transform = xr_interactable_global_transform.compute_transform();
let center = sphere_transform.translation;
let radius: f32 = 0.1;
//I hate this but the aim pose needs the root for now
let root = tracking_root_query.get_single().unwrap();
match aim {
Some(aim) => {
let ray_origin =
root.translation + root.rotation.mul_vec3(aim.0.translation);
let ray_dir = root.rotation.mul_vec3(*aim.0.forward());
if ray_sphere_intersection(
center,
radius,
ray_origin,
ray_dir.normalize_or_zero(),
) {
//check for selections first
match interactor_state {
XRInteractorState::Idle => {
let event = InteractionEvent {
interactor: interactor_entity,
interactable: interactable_entity,
interactable_state: XRInteractableState::Hover,
};
writer.send(event);
}
XRInteractorState::Selecting => {
let event = InteractionEvent {
interactor: interactor_entity,
interactable: interactable_entity,
interactable_state: XRInteractableState::Select,
};
writer.send(event);
}
}
}
}
None => info!("no aim pose"),
}
}
None => (),
}
}
}
}
pub fn update_interactable_states(
mut events: EventReader<InteractionEvent>,
mut interactable_query: Query<
(Entity, &mut XRInteractableState, &mut Touched),
With<XRInteractable>,
>,
) {
//i very much dislike this
for (_entity, _state, mut touched) in interactable_query.iter_mut() {
*touched = Touched(false);
}
for event in events.read() {
//lets change the state
match interactable_query.get_mut(event.interactable) {
Ok((_entity, mut entity_state, mut touched)) => {
//since we have an event we were touched this frame, i hate this name
*touched = Touched(true);
if event.interactable_state > *entity_state {
// info!(
// "event.state: {:?}, interactable.state: {:?}",
// event.interactable_state, entity_state
// );
// info!("event has a higher state");
}
*entity_state = event.interactable_state;
}
Err(_) => {}
}
}
//lets go through all the untouched interactables and set them to idle
for (_entity, mut state, touched) in interactable_query.iter_mut() {
if !touched.0 {
*state = XRInteractableState::Idle;
}
}
}
fn ray_sphere_intersection(center: Vec3, radius: f32, ray_origin: Vec3, ray_dir: Vec3) -> bool {
let l = center - ray_origin;
let adj = l.dot(ray_dir);
let d2 = l.dot(l) - (adj * adj);
let radius2 = radius * radius;
if d2 > radius2 {
return false;
}
let thc = (radius2 - d2).sqrt();
let t0 = adj - thc;
let t1 = adj + thc;
if t0 < 0.0 && t1 < 0.0 {
return false;
}
// let distance = if t0 < t1 { t0 } else { t1 };
return true;
}

View File

@@ -1,126 +0,0 @@
pub mod actions;
pub mod controllers;
pub mod debug_gizmos;
pub mod hand_poses;
pub mod hands;
pub mod interactions;
pub mod oculus_touch;
pub mod prototype_locomotion;
pub mod trackers;
pub mod xr_camera;
use crate::resources::{XrInstance, XrSession};
use crate::xr_init::{xr_only, XrCleanup, XrPostSetup, XrPreSetup, XrSetup};
use crate::xr_input::oculus_touch::setup_oculus_controller;
use crate::xr_input::xr_camera::{xr_camera_head_sync, Eye, XRProjection, XrCameraBundle};
use crate::{locate_views, xr_wait_frame};
use bevy::app::{App, PostUpdate, Startup};
use bevy::ecs::entity::Entity;
use bevy::ecs::query::With;
use bevy::ecs::system::Query;
use bevy::hierarchy::DespawnRecursiveExt;
use bevy::log::{info, warn};
use bevy::math::Vec2;
use bevy::prelude::{BuildChildren, Component, Deref, DerefMut, IntoSystemConfigs, Resource};
use bevy::prelude::{Commands, Plugin, PreUpdate, Quat, Res, SpatialBundle, Update, Vec3};
use bevy::render::camera::CameraProjectionPlugin;
use bevy::render::extract_component::ExtractComponentPlugin;
use bevy::render::view::{update_frusta, VisibilitySystems};
use bevy::transform::TransformSystem;
use bevy::utils::HashMap;
use openxr::Binding;
use self::actions::{setup_oxr_actions, XrActionsPlugin};
use self::oculus_touch::{
init_subaction_path, post_action_setup_oculus_controller, ActionSets, OculusController,
};
use self::trackers::{
adopt_open_xr_trackers, update_open_xr_controllers, OpenXRLeftEye, OpenXRRightEye,
OpenXRTrackingRoot,
};
use self::xr_camera::{/* GlobalTransformExtract, TransformExtract, */ XrCamera};
#[derive(Copy, Clone)]
pub struct XrInputPlugin;
#[derive(Clone, Copy, Debug, Ord, PartialOrd, Eq, PartialEq, Component)]
pub enum Hand {
Left,
Right,
}
impl Plugin for XrInputPlugin {
fn build(&self, app: &mut App) {
app.add_systems(XrPostSetup, post_action_setup_oculus_controller);
app.add_systems(XrSetup, setup_oculus_controller);
app.add_systems(XrCleanup, cleanup_oculus_controller);
//adopt any new trackers
app.add_systems(PreUpdate, adopt_open_xr_trackers.run_if(xr_only()));
// app.add_systems(PreUpdate, action_set_system.run_if(xr_only()));
//update controller trackers
app.add_systems(Update, update_open_xr_controllers.run_if(xr_only()));
app.add_systems(XrPreSetup, init_subaction_path);
app.add_systems(XrSetup, setup_xr_root);
app.add_systems(XrCleanup, cleanup_xr_root);
}
}
fn cleanup_oculus_controller(mut commands: Commands) {
commands.remove_resource::<OculusController>();
}
fn cleanup_xr_root(
mut commands: Commands,
tracking_root_query: Query<Entity, With<OpenXRTrackingRoot>>,
) {
for e in &tracking_root_query {
commands.entity(e).despawn_recursive();
}
}
fn setup_xr_root(
mut commands: Commands,
tracking_root_query: Query<Entity, With<OpenXRTrackingRoot>>,
) {
if tracking_root_query.get_single().is_err() {
info!("Creating XrTrackingRoot!");
commands.spawn((SpatialBundle::default(), OpenXRTrackingRoot));
}
}
// pub fn action_set_system(action_sets: Res<ActionSets>, session: Res<XrSession>) {
// let mut active_action_sets = vec![];
// for i in &action_sets.0 {
// active_action_sets.push(openxr::ActiveActionSet::new(i));
// }
// //info!("action sets: {:#?}", action_sets.0.len());
// if let Err(err) = session.sync_actions(&active_action_sets) {
// warn!("{}", err);
// }
// }
pub trait Vec2Conv {
fn to_vec2(&self) -> Vec2;
}
impl Vec2Conv for openxr::Vector2f {
fn to_vec2(&self) -> Vec2 {
Vec2::new(self.x, self.y)
}
}
pub trait Vec3Conv {
fn to_vec3(&self) -> Vec3;
}
impl Vec3Conv for openxr::Vector3f {
fn to_vec3(&self) -> Vec3 {
Vec3::new(self.x, self.y, self.z)
}
}
pub trait QuatConv {
fn to_quat(&self) -> Quat;
}
impl QuatConv for openxr::Quaternionf {
fn to_quat(&self) -> Quat {
Quat::from_xyzw(self.x, self.y, self.z, self.w)
}
}

View File

@@ -1,546 +0,0 @@
use crate::input::XrInput;
use crate::resources::{XrInstance, XrSession};
use crate::xr_input::controllers::Handed;
use crate::xr_input::Hand;
use bevy::prelude::{default, Commands, Res, ResMut, Resource};
use openxr::{
ActionSet, AnyGraphics, FrameState, Instance, Path, Posef, Session, Space, SpaceLocation,
SpaceVelocity,
};
use std::sync::OnceLock;
use super::actions::{ActionHandednes, ActionType, SetupActionSets, XrActionSets, XrBinding};
pub fn post_action_setup_oculus_controller(
action_sets: Res<XrActionSets>,
mut controller: ResMut<OculusController>,
instance: Res<XrInstance>,
session: Res<XrSession>,
) {
let s = Session::<AnyGraphics>::clone(&session);
let left_path = instance.string_to_path("/user/hand/left").unwrap();
let right_path = instance.string_to_path("/user/hand/right").unwrap();
let grip_action = action_sets
.get_action_posef("oculus_input", "hand_pose")
.unwrap();
let aim_action = action_sets
.get_action_posef("oculus_input", "pointer_pose")
.unwrap();
controller.grip_space = Some(Handed {
left: grip_action
.create_space(s.clone(), left_path, Posef::IDENTITY)
.unwrap(),
right: grip_action
.create_space(s.clone(), right_path, Posef::IDENTITY)
.unwrap(),
});
controller.aim_space = Some(Handed {
left: aim_action
.create_space(s.clone(), left_path, Posef::IDENTITY)
.unwrap(),
right: aim_action
.create_space(s.clone(), right_path, Posef::IDENTITY)
.unwrap(),
})
}
pub fn setup_oculus_controller(
mut commands: Commands,
instance: Res<XrInstance>,
action_sets: ResMut<SetupActionSets>,
) {
let oculus_controller = OculusController::new(action_sets).unwrap();
commands.insert_resource(oculus_controller);
}
#[derive(Resource, Clone)]
pub struct ActionSets(pub Vec<ActionSet>);
pub struct OculusControllerRef<'a> {
oculus_controller: &'a OculusController,
action_sets: &'a XrActionSets,
session: &'a Session<AnyGraphics>,
frame_state: &'a FrameState,
xr_input: &'a XrInput,
}
pub static RIGHT_SUBACTION_PATH: OnceLock<Path> = OnceLock::new();
pub static LEFT_SUBACTION_PATH: OnceLock<Path> = OnceLock::new();
pub fn init_subaction_path(instance: Res<XrInstance>) {
let _ = LEFT_SUBACTION_PATH.set(instance.string_to_path("/user/hand/left").unwrap());
let _ = RIGHT_SUBACTION_PATH.set(instance.string_to_path("/user/hand/right").unwrap());
}
pub fn subaction_path(hand: Hand) -> Path {
*match hand {
Hand::Left => LEFT_SUBACTION_PATH.get().unwrap(),
Hand::Right => RIGHT_SUBACTION_PATH.get().unwrap(),
}
}
impl OculusControllerRef<'_> {
pub fn grip_space(&self, hand: Hand) -> (SpaceLocation, SpaceVelocity) {
let d = match hand {
Hand::Left => self
.oculus_controller
.grip_space
.as_ref()
.unwrap()
.left
.relate(
&self.xr_input.stage,
self.frame_state.predicted_display_time,
),
Hand::Right => self
.oculus_controller
.grip_space
.as_ref()
.unwrap()
.right
.relate(
&self.xr_input.stage,
self.frame_state.predicted_display_time,
),
};
match d {
Ok(d) => d,
Err(_) => (SpaceLocation::default(), SpaceVelocity::default()),
}
}
pub fn aim_space(&self, hand: Hand) -> (SpaceLocation, SpaceVelocity) {
let d = match hand {
Hand::Left => self
.oculus_controller
.aim_space
.as_ref()
.unwrap()
.left
.relate(
&self.xr_input.stage,
self.frame_state.predicted_display_time,
),
Hand::Right => self
.oculus_controller
.aim_space
.as_ref()
.unwrap()
.right
.relate(
&self.xr_input.stage,
self.frame_state.predicted_display_time,
),
};
match d {
Ok(d) => d,
Err(_) => (SpaceLocation::default(), SpaceVelocity::default()),
}
}
pub fn squeeze(&self, hand: Hand) -> f32 {
match &self
.action_sets
.get_action_f32("oculus_input", "squeeze")
.unwrap()
.state(&self.session, subaction_path(hand))
{
Ok(v) => v,
Err(_) => return default(),
}
.current_state
}
pub fn trigger(&self, hand: Hand) -> f32 {
match self
.action_sets
.get_action_f32("oculus_input", "trigger")
.unwrap()
.state(&self.session, subaction_path(hand))
{
Ok(v) => v,
Err(_) => return default(),
}
.current_state
}
pub fn trigger_touched(&self, hand: Hand) -> bool {
match self
.action_sets
.get_action_bool("oculus_input", "trigger_touched")
.unwrap()
.state(&self.session, subaction_path(hand))
{
Ok(v) => v,
Err(_) => return default(),
}
.current_state
}
pub fn x_button(&self) -> bool {
match self
.action_sets
.get_action_bool("oculus_input", "x_button")
.unwrap()
.state(&self.session, Path::NULL)
{
Ok(v) => v,
Err(_) => return default(),
}
.current_state
}
pub fn x_button_touched(&self) -> bool {
match self
.action_sets
.get_action_bool("oculus_input", "x_button_touch")
.unwrap()
.state(&self.session, Path::NULL)
{
Ok(v) => v,
Err(_) => return default(),
}
.current_state
}
pub fn y_button(&self) -> bool {
match self
.action_sets
.get_action_bool("oculus_input", "y_button")
.unwrap()
.state(&self.session, Path::NULL)
{
Ok(v) => v,
Err(_) => return default(),
}
.current_state
}
pub fn y_button_touched(&self) -> bool {
match self
.action_sets
.get_action_bool("oculus_input", "y_button_touch")
.unwrap()
.state(&self.session, Path::NULL)
{
Ok(v) => v,
Err(_) => return default(),
}
.current_state
}
pub fn menu_button(&self) -> bool {
match self
.action_sets
.get_action_bool("oculus_input", "menu_button")
.unwrap()
.state(&self.session, Path::NULL)
{
Ok(v) => v,
Err(_) => return default(),
}
.current_state
}
pub fn a_button(&self) -> bool {
match self
.action_sets
.get_action_bool("oculus_input", "a_button")
.unwrap()
.state(&self.session, Path::NULL)
{
Ok(v) => v,
Err(_) => return default(),
}
.current_state
}
pub fn a_button_touched(&self) -> bool {
match self
.action_sets
.get_action_bool("oculus_input", "a_button_touch")
.unwrap()
.state(&self.session, Path::NULL)
{
Ok(v) => v,
Err(_) => return default(),
}
.current_state
}
pub fn b_button(&self) -> bool {
match self
.action_sets
.get_action_bool("oculus_input", "b_button")
.unwrap()
.state(&self.session, Path::NULL)
{
Ok(v) => v,
Err(_) => return default(),
}
.current_state
}
pub fn b_button_touched(&self) -> bool {
match self
.action_sets
.get_action_bool("oculus_input", "b_button_touch")
.unwrap()
.state(&self.session, Path::NULL)
{
Ok(v) => v,
Err(_) => return default(),
}
.current_state
}
pub fn thumbstick_touch(&self, hand: Hand) -> bool {
match self
.action_sets
.get_action_bool("oculus_input", "thumbstick_touch")
.unwrap()
.state(&self.session, subaction_path(hand))
{
Ok(v) => v,
Err(_) => return default(),
}
.current_state
}
pub fn thumbstick(&self, hand: Hand) -> Thumbstick {
Thumbstick {
x: match self
.action_sets
.get_action_f32("oculus_input", "thumbstick_x")
.unwrap()
.state(&self.session, subaction_path(hand))
.map(|v| v.current_state)
{
Ok(v) => v,
Err(_) => default(),
},
y: match self
.action_sets
.get_action_f32("oculus_input", "thumbstick_y")
.unwrap()
.state(&self.session, subaction_path(hand))
.map(|v| v.current_state)
{
Ok(v) => v,
Err(_) => default(),
},
click: match self
.action_sets
.get_action_bool("oculus_input", "thumbstick_click")
.unwrap()
.state(&self.session, subaction_path(hand))
.map(|v| v.current_state)
{
Ok(v) => v,
Err(_) => default(),
},
}
}
pub fn thumbrest_touch(&self, hand: Hand) -> bool {
match self
.action_sets
.get_action_bool("oculus_input", "thumbrest_touch")
.unwrap()
.state(&self.session, subaction_path(hand))
{
Ok(v) => v,
Err(_) => return default(),
}
.current_state
}
}
#[derive(Copy, Clone, Debug)]
pub struct Thumbstick {
pub x: f32,
pub y: f32,
pub click: bool,
}
impl OculusController {
pub fn get_ref<'a>(
&'a self,
session: &'a Session<AnyGraphics>,
frame_state: &'a FrameState,
xr_input: &'a XrInput,
action_sets: &'a XrActionSets,
) -> OculusControllerRef {
OculusControllerRef {
oculus_controller: self,
session,
frame_state,
xr_input,
action_sets,
}
}
}
#[derive(Resource)]
pub struct OculusController {
pub grip_space: Option<Handed<Space>>,
pub aim_space: Option<Handed<Space>>,
}
impl OculusController {
pub fn new(mut action_sets: ResMut<SetupActionSets>) -> eyre::Result<Self> {
let action_set =
action_sets.add_action_set("oculus_input", "Oculus Touch Controller Input".into(), 0);
action_set.new_action(
"hand_pose",
"Hand Pose".into(),
ActionType::PoseF,
ActionHandednes::Double,
);
action_set.new_action(
"pointer_pose",
"Pointer Pose".into(),
ActionType::PoseF,
ActionHandednes::Double,
);
action_set.new_action(
"squeeze",
"Grip Pull".into(),
ActionType::F32,
ActionHandednes::Double,
);
action_set.new_action(
"trigger",
"Trigger Pull".into(),
ActionType::F32,
ActionHandednes::Double,
);
action_set.new_action(
"trigger_touched",
"Trigger Touch".into(),
ActionType::Bool,
ActionHandednes::Double,
);
action_set.new_action(
"haptic_feedback",
"Haptic Feedback".into(),
ActionType::Haptic,
ActionHandednes::Double,
);
action_set.new_action(
"x_button",
"X Button".into(),
ActionType::Bool,
ActionHandednes::Single,
);
action_set.new_action(
"x_button_touch",
"X Button Touch".into(),
ActionType::Bool,
ActionHandednes::Single,
);
action_set.new_action(
"y_button",
"Y Button".into(),
ActionType::Bool,
ActionHandednes::Single,
);
action_set.new_action(
"y_button_touch",
"Y Button Touch".into(),
ActionType::Bool,
ActionHandednes::Single,
);
action_set.new_action(
"a_button",
"A Button".into(),
ActionType::Bool,
ActionHandednes::Single,
);
action_set.new_action(
"a_button_touch",
"A Button Touch".into(),
ActionType::Bool,
ActionHandednes::Single,
);
action_set.new_action(
"b_button",
"B Button".into(),
ActionType::Bool,
ActionHandednes::Single,
);
action_set.new_action(
"b_button_touch",
"B Button Touch".into(),
ActionType::Bool,
ActionHandednes::Single,
);
action_set.new_action(
"menu_button",
"Menu Button".into(),
ActionType::Bool,
ActionHandednes::Single,
);
action_set.new_action(
"thumbstick_x",
"Thumbstick X".into(),
ActionType::F32,
ActionHandednes::Double,
);
action_set.new_action(
"thumbstick_y",
"Thumbstick y".into(),
ActionType::F32,
ActionHandednes::Double,
);
action_set.new_action(
"thumbstick_touch",
"Thumbstick Touch".into(),
ActionType::Bool,
ActionHandednes::Double,
);
action_set.new_action(
"thumbstick_click",
"Thumbstick Click".into(),
ActionType::Bool,
ActionHandednes::Double,
);
action_set.new_action(
"thumbrest_touch",
"Thumbrest Touch".into(),
ActionType::Bool,
ActionHandednes::Double,
);
let this = OculusController {
grip_space: None,
aim_space: None,
};
action_set.suggest_binding(
"/interaction_profiles/oculus/touch_controller",
&[
XrBinding::new("hand_pose", "/user/hand/left/input/grip/pose"),
XrBinding::new("hand_pose", "/user/hand/right/input/grip/pose"),
XrBinding::new("pointer_pose", "/user/hand/left/input/aim/pose"),
XrBinding::new("pointer_pose", "/user/hand/right/input/aim/pose"),
XrBinding::new("squeeze", "/user/hand/left/input/squeeze/value"),
XrBinding::new("squeeze", "/user/hand/right/input/squeeze/value"),
XrBinding::new("trigger", "/user/hand/left/input/trigger/value"),
XrBinding::new("trigger", "/user/hand/right/input/trigger/value"),
XrBinding::new("trigger_touched", "/user/hand/left/input/trigger/touch"),
XrBinding::new("trigger_touched", "/user/hand/right/input/trigger/touch"),
XrBinding::new("haptic_feedback", "/user/hand/left/output/haptic"),
XrBinding::new("haptic_feedback", "/user/hand/right/output/haptic"),
XrBinding::new("x_button", "/user/hand/left/input/x/click"),
XrBinding::new("x_button_touch", "/user/hand/left/input/x/touch"),
XrBinding::new("y_button", "/user/hand/left/input/y/click"),
XrBinding::new("y_button_touch", "/user/hand/left/input/y/touch"),
XrBinding::new("a_button", "/user/hand/right/input/a/click"),
XrBinding::new("a_button_touch", "/user/hand/right/input/a/touch"),
XrBinding::new("b_button", "/user/hand/right/input/b/click"),
XrBinding::new("b_button_touch", "/user/hand/right/input/b/touch"),
XrBinding::new("menu_button", "/user/hand/left/input/menu/click"),
XrBinding::new("thumbstick_x", "/user/hand/left/input/thumbstick/x"),
XrBinding::new("thumbstick_y", "/user/hand/left/input/thumbstick/y"),
XrBinding::new("thumbstick_x", "/user/hand/right/input/thumbstick/x"),
XrBinding::new("thumbstick_y", "/user/hand/right/input/thumbstick/y"),
XrBinding::new("thumbstick_click", "/user/hand/left/input/thumbstick/click"),
XrBinding::new(
"thumbstick_click",
"/user/hand/right/input/thumbstick/click",
),
XrBinding::new("thumbstick_touch", "/user/hand/left/input/thumbstick/touch"),
XrBinding::new(
"thumbstick_touch",
"/user/hand/right/input/thumbstick/touch",
),
XrBinding::new("thumbrest_touch", "/user/hand/left/input/thumbrest/touch"),
XrBinding::new("thumbrest_touch", "/user/hand/right/input/thumbrest/touch"),
],
);
Ok(this)
}
}

View File

@@ -1,175 +0,0 @@
use std::f32::consts::PI;
use bevy::{
color::palettes,
prelude::*,
time::{Time, Timer, TimerMode},
};
use crate::{
input::XrInput,
resources::{XrFrameState, XrInstance, XrSession, XrViews},
};
use super::{
actions::XrActionSets, oculus_touch::OculusController, trackers::OpenXRTrackingRoot, Hand,
QuatConv, Vec3Conv,
};
pub enum LocomotionType {
Head,
Hand,
}
pub enum RotationType {
Smooth,
Snap,
}
#[derive(Resource)]
pub struct RotationTimer {
pub timer: Timer,
}
#[derive(Resource)]
pub struct PrototypeLocomotionConfig {
pub locomotion_type: LocomotionType,
pub locomotion_speed: f32,
pub rotation_type: RotationType,
pub snap_angle: f32,
pub smooth_rotation_speed: f32,
pub rotation_stick_deadzone: f32,
pub rotation_timer: RotationTimer,
}
impl Default for PrototypeLocomotionConfig {
fn default() -> Self {
Self {
locomotion_type: LocomotionType::Head,
locomotion_speed: 1.0,
rotation_type: RotationType::Smooth,
snap_angle: 45.0 * (PI / 180.0),
smooth_rotation_speed: 0.5 * PI,
rotation_stick_deadzone: 0.2,
rotation_timer: RotationTimer {
timer: Timer::from_seconds(1.0, TimerMode::Once),
},
}
}
}
pub fn proto_locomotion(
time: Res<Time>,
mut tracking_root_query: Query<&mut Transform, With<OpenXRTrackingRoot>>,
oculus_controller: Res<OculusController>,
frame_state: Res<XrFrameState>,
xr_input: Res<XrInput>,
session: Res<XrSession>,
views: ResMut<XrViews>,
mut gizmos: Gizmos,
config_option: Option<ResMut<PrototypeLocomotionConfig>>,
action_sets: Res<XrActionSets>,
) {
let mut config = match config_option {
Some(c) => c,
None => {
info!("no locomotion config");
return;
}
};
//get controller
let controller = oculus_controller.get_ref(&session, &frame_state, &xr_input, &action_sets);
let root = tracking_root_query.get_single_mut();
match root {
Ok(mut position) => {
//get the stick input and do some maths
let stick = controller.thumbstick(Hand::Left);
let input = stick.x * *position.right() + stick.y * *position.forward();
let reference_quat;
match config.locomotion_type {
LocomotionType::Head => {
let views = views.first();
match views {
Some(view) => {
reference_quat = view.pose.orientation.to_quat();
}
None => return,
}
}
LocomotionType::Hand => {
let grip = controller.grip_space(Hand::Left);
reference_quat = grip.0.pose.orientation.to_quat();
}
}
let (yaw, _pitch, _roll) = reference_quat.to_euler(EulerRot::YXZ);
let reference_quat = Quat::from_axis_angle(*position.up(), yaw);
let locomotion_vec = reference_quat.mul_vec3(input);
position.translation += locomotion_vec * config.locomotion_speed * time.delta_seconds();
//now time for rotation
match config.rotation_type {
RotationType::Smooth => {
//once again with the math
let control_stick = controller.thumbstick(Hand::Right);
let rot_input = -control_stick.x; //why is this negative i dont know
if rot_input.abs() <= config.rotation_stick_deadzone {
return;
}
let smoth_rot = Quat::from_axis_angle(
*position.up(),
rot_input * config.smooth_rotation_speed * time.delta_seconds(),
);
//apply rotation
let views = views.first();
match views {
Some(view) => {
let mut hmd_translation = view.pose.position.to_vec3();
hmd_translation.y = 0.0;
let local = position.translation;
let global = position.rotation.mul_vec3(hmd_translation) + local;
gizmos.circle(global, position.up(), 0.1, palettes::css::GREEN);
position.rotate_around(global, smoth_rot);
}
None => return,
}
}
RotationType::Snap => {
//tick the timer
config.rotation_timer.timer.tick(time.delta());
if config.rotation_timer.timer.finished() {
//now we can snap turn?
//once again with the math
let control_stick = controller.thumbstick(Hand::Right);
let rot_input = -control_stick.x;
if rot_input.abs() <= config.rotation_stick_deadzone {
return;
}
let dir: f32 = match rot_input > 0.0 {
true => 1.0,
false => -1.0,
};
let smoth_rot =
Quat::from_axis_angle(*position.up(), config.snap_angle * dir);
//apply rotation
let v = views;
let views = v.first();
match views {
Some(view) => {
let mut hmd_translation = view.pose.position.to_vec3();
hmd_translation.y = 0.0;
let local = position.translation;
let global = position.rotation.mul_vec3(hmd_translation) + local;
gizmos.circle(global, position.up(), 0.1, palettes::css::GREEN);
position.rotate_around(global, smoth_rot);
}
None => return,
}
config.rotation_timer.timer.reset();
}
}
}
}
Err(_) => info!("too many tracking roots"),
}
}

View File

@@ -1,143 +0,0 @@
use bevy::hierarchy::Parent;
use bevy::log::{debug, info};
use bevy::math::Quat;
use bevy::prelude::{
Added, BuildChildren, Commands, Component, Entity, Query, Res, Transform, Vec3, With, Without,
};
use crate::{
input::XrInput,
resources::{XrFrameState, XrSession},
};
use super::{actions::XrActionSets, oculus_touch::OculusController, Hand, QuatConv, Vec3Conv};
#[derive(Component)]
pub struct OpenXRTrackingRoot;
#[derive(Component)]
pub struct OpenXRTracker;
#[derive(Component)]
pub struct OpenXRLeftEye;
#[derive(Component)]
pub struct OpenXRRightEye;
#[derive(Component)]
pub struct OpenXRHMD;
#[derive(Component)]
pub struct OpenXRLeftController;
#[derive(Component)]
pub struct OpenXRRightController;
#[derive(Component)]
pub struct OpenXRController;
#[derive(Component)]
pub struct AimPose(pub Transform);
pub fn adopt_open_xr_trackers(
query: Query<Entity, (With<OpenXRTracker>, Without<Parent>)>,
mut commands: Commands,
tracking_root_query: Query<Entity, With<OpenXRTrackingRoot>>,
) {
let root = tracking_root_query.get_single();
match root {
Ok(root) => {
// info!("root is");
for tracker in query.iter() {
info!("we got a new tracker");
commands.entity(root).add_child(tracker);
}
}
Err(_) => info!("root isnt spawned yet?"),
}
}
pub fn verify_quat(mut quat: Quat) -> Quat {
if quat.length() == 0.0 || !quat.is_finite() {
quat = Quat::IDENTITY;
}
quat.normalize()
}
pub fn update_open_xr_controllers(
oculus_controller: Res<OculusController>,
mut left_controller_query: Query<
(&mut Transform, Option<&mut AimPose>),
(With<OpenXRLeftController>, Without<OpenXRRightController>),
>,
mut right_controller_query: Query<
(&mut Transform, Option<&mut AimPose>),
(With<OpenXRRightController>, Without<OpenXRLeftController>),
>,
frame_state: Res<XrFrameState>,
xr_input: Res<XrInput>,
session: Res<XrSession>,
action_sets: Res<XrActionSets>,
) {
//get controller
let controller = oculus_controller.get_ref(&session, &frame_state, &xr_input, &action_sets);
//get left controller
let left_grip_space = controller.grip_space(Hand::Left);
let left_aim_space = controller.aim_space(Hand::Left);
let left_postion = left_grip_space.0.pose.position.to_vec3();
//TODO figure out how to not get the entity multiple times
let left_aim_pose = left_controller_query.get_single_mut();
//set aim pose
match left_aim_pose {
Ok(left_entity) => match left_entity.1 {
Some(mut pose) => {
*pose = AimPose(Transform {
translation: left_aim_space.0.pose.position.to_vec3(),
rotation: verify_quat(left_aim_space.0.pose.orientation.to_quat()),
scale: Vec3::splat(1.0),
});
}
None => (),
},
Err(_) => debug!("no left controlelr entity found"),
}
//set translation
let left_translation = left_controller_query.get_single_mut();
match left_translation {
Ok(mut left_entity) => left_entity.0.translation = left_postion,
Err(_) => (),
}
//set rotation
let left_rotataion = left_controller_query.get_single_mut();
match left_rotataion {
Ok(mut left_entity) => {
left_entity.0.rotation = verify_quat(left_grip_space.0.pose.orientation.to_quat())
}
Err(_) => (),
}
//get right controller
let right_grip_space = controller.grip_space(Hand::Right);
let right_aim_space = controller.aim_space(Hand::Right);
let right_postion = right_grip_space.0.pose.position.to_vec3();
let right_aim_pose = right_controller_query.get_single_mut();
match right_aim_pose {
Ok(right_entity) => match right_entity.1 {
Some(mut pose) => {
*pose = AimPose(Transform {
translation: right_aim_space.0.pose.position.to_vec3(),
rotation: verify_quat(right_aim_space.0.pose.orientation.to_quat()),
scale: Vec3::splat(1.0),
});
}
None => (),
},
Err(_) => debug!("no right controlelr entity found"),
}
//set translation
let right_translation = right_controller_query.get_single_mut();
match right_translation {
Ok(mut right_entity) => right_entity.0.translation = right_postion,
Err(_) => (),
}
//set rotation
let right_rotataion = right_controller_query.get_single_mut();
match right_rotataion {
Ok(mut right_entity) => {
right_entity.0.rotation = verify_quat(right_grip_space.0.pose.orientation.to_quat())
}
Err(_) => (),
}
}

View File

@@ -1,409 +0,0 @@
use crate::prelude::XrSystems;
use crate::xr_init::{xr_only, XrCleanup, XrSetup};
use crate::xr_input::{QuatConv, Vec3Conv};
use crate::{locate_views, xr_wait_frame, LEFT_XR_TEXTURE_HANDLE, RIGHT_XR_TEXTURE_HANDLE};
use bevy::core_pipeline::core_3d::graph::Core3d;
use bevy::core_pipeline::tonemapping::{DebandDither, Tonemapping};
use bevy::math::Vec3A;
use bevy::prelude::*;
use bevy::render::camera::{
CameraMainTextureUsages, CameraProjection, CameraProjectionPlugin, CameraRenderGraph,
RenderTarget,
};
use bevy::render::extract_component::{ExtractComponent, ExtractComponentPlugin};
use bevy::render::primitives::Frustum;
use bevy::render::render_resource::TextureUsages;
use bevy::render::view::{
update_frusta, ColorGrading, ExtractedView, VisibilitySystems, VisibleEntities,
};
use bevy::render::{Render, RenderApp, RenderSet};
use bevy::transform::TransformSystem;
use openxr::Fovf;
use super::trackers::{OpenXRLeftEye, OpenXRRightEye, OpenXRTracker, OpenXRTrackingRoot};
pub struct XrCameraPlugin;
impl Plugin for XrCameraPlugin {
fn build(&self, app: &mut App) {
app.add_plugins(CameraProjectionPlugin::<XRProjection>::default());
app.add_systems(
PreUpdate,
xr_camera_head_sync
.run_if(xr_only())
.after(xr_wait_frame)
.after(locate_views),
);
// a little late latching
app.add_systems(
PostUpdate,
xr_camera_head_sync
.before(TransformSystem::TransformPropagate)
.run_if(xr_only()),
);
app.add_systems(
PostUpdate,
update_frusta::<XRProjection>
.after(TransformSystem::TransformPropagate)
.before(VisibilitySystems::UpdateFrusta),
);
app.add_systems(
PostUpdate,
update_root_transform_components
.after(TransformSystem::TransformPropagate)
.xr_only(),
);
app.add_systems(XrSetup, setup_xr_cameras);
app.add_systems(XrCleanup, cleanup_xr_cameras);
app.add_plugins(ExtractComponentPlugin::<XrCamera>::default());
app.add_plugins(ExtractComponentPlugin::<XRProjection>::default());
app.add_plugins(ExtractComponentPlugin::<RootTransform>::default());
// app.add_plugins(ExtractComponentPlugin::<TransformExtract>::default());
// app.add_plugins(ExtractComponentPlugin::<GlobalTransformExtract>::default());
let render_app = app.sub_app_mut(RenderApp);
render_app.add_systems(
Render,
(locate_views, xr_camera_head_sync_render_world)
.chain()
.run_if(xr_only())
.in_set(RenderSet::PrepareAssets),
// .after(xr_wait_frame)
// .after(locate_views),
);
}
}
// might be unnesesary since it should be parented to the root
fn cleanup_xr_cameras(mut commands: Commands, entities: Query<Entity, With<XrCamera>>) {
for e in &entities {
commands.entity(e).despawn_recursive();
}
}
fn setup_xr_cameras(mut commands: Commands) {
commands.spawn((
XrCameraBundle::new(Eye::Right),
OpenXRRightEye,
OpenXRTracker,
));
commands.spawn((XrCameraBundle::new(Eye::Left), OpenXRLeftEye, OpenXRTracker));
}
#[derive(Bundle)]
pub struct XrCamerasBundle {
pub left: XrCameraBundle,
pub right: XrCameraBundle,
}
impl XrCamerasBundle {
pub fn new() -> Self {
Self::default()
}
}
impl Default for XrCamerasBundle {
fn default() -> Self {
Self {
left: XrCameraBundle::new(Eye::Left),
right: XrCameraBundle::new(Eye::Right),
}
}
}
#[derive(Bundle)]
pub struct XrCameraBundle {
pub camera: Camera,
pub camera_render_graph: CameraRenderGraph,
pub xr_projection: XRProjection,
pub visible_entities: VisibleEntities,
pub frustum: Frustum,
pub transform: Transform,
pub global_transform: GlobalTransform,
pub camera_3d: Camera3d,
pub tonemapping: Tonemapping,
pub dither: DebandDither,
pub color_grading: ColorGrading,
pub main_texture_usages: CameraMainTextureUsages,
pub xr_camera_type: XrCamera,
pub root_transform: RootTransform,
}
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, Ord, PartialOrd, Component, ExtractComponent)]
pub struct XrCamera(Eye);
#[derive(Component, ExtractComponent, Clone, Copy, Debug, Default, Deref, DerefMut)]
pub struct RootTransform(pub GlobalTransform);
fn update_root_transform_components(
mut component_query: Query<&mut RootTransform>,
root_query: Query<&GlobalTransform, With<OpenXRTrackingRoot>>,
) {
let root = match root_query.get_single() {
Ok(v) => v,
Err(err) => {
warn!("No or too many XrTracking Roots: {}", err);
return;
}
};
component_query
.par_iter_mut()
.for_each(|mut root_transform| **root_transform = *root);
}
// #[derive(Component)]
// pub(super) struct TransformExtract;
//
// impl ExtractComponent for TransformExtract {
// type Query = Read<Transform>;
//
// type Filter = ();
//
// type Out = Transform;
//
// fn extract_component(item: bevy::ecs::query::QueryItem<'_, Self::Query>) -> Option<Self::Out> {
// Some(*item)
// }
// }
//
// #[derive(Component)]
// pub(super) struct GlobalTransformExtract;
//
// impl ExtractComponent for GlobalTransformExtract {
// type Query = Read<GlobalTransform>;
//
// type Filter = ();
//
// type Out = GlobalTransform;
//
// fn extract_component(item: bevy::ecs::query::QueryItem<'_, Self::Query>) -> Option<Self::Out> {
// Some(*item)
// }
// }
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, Ord, PartialOrd)]
pub enum Eye {
Left = 0,
Right = 1,
}
impl XrCameraBundle {
pub fn new(eye: Eye) -> Self {
Self {
camera: Camera {
order: -1,
target: RenderTarget::TextureView(match eye {
Eye::Left => LEFT_XR_TEXTURE_HANDLE,
Eye::Right => RIGHT_XR_TEXTURE_HANDLE,
}),
viewport: None,
..default()
},
camera_render_graph: CameraRenderGraph::new(Core3d),
xr_projection: Default::default(),
visible_entities: Default::default(),
frustum: Default::default(),
transform: Default::default(),
global_transform: Default::default(),
camera_3d: Default::default(),
tonemapping: Default::default(),
dither: DebandDither::Enabled,
color_grading: Default::default(),
xr_camera_type: XrCamera(eye),
main_texture_usages: CameraMainTextureUsages(
TextureUsages::RENDER_ATTACHMENT
| TextureUsages::TEXTURE_BINDING
| TextureUsages::COPY_SRC,
),
root_transform: default(),
}
}
}
#[derive(Debug, Clone, Component, Reflect, ExtractComponent)]
#[reflect(Component, Default)]
pub struct XRProjection {
pub near: f32,
pub far: f32,
#[reflect(ignore)]
pub fov: Fovf,
}
impl Default for XRProjection {
fn default() -> Self {
Self {
near: 0.1,
far: 1000.,
fov: Default::default(),
}
}
}
impl XRProjection {
pub fn new(near: f32, far: f32, fov: Fovf) -> Self {
XRProjection { near, far, fov }
}
}
impl CameraProjection for XRProjection {
// =============================================================================
// math code adapted from
// https://github.com/KhronosGroup/OpenXR-SDK-Source/blob/master/src/common/xr_linear.h
// Copyright (c) 2017 The Khronos Group Inc.
// Copyright (c) 2016 Oculus VR, LLC.
// SPDX-License-Identifier: Apache-2.0
// =============================================================================
fn get_clip_from_view(&self) -> Mat4 {
// symmetric perspective for debugging
// let x_fov = (self.fov.angle_left.abs() + self.fov.angle_right.abs());
// let y_fov = (self.fov.angle_up.abs() + self.fov.angle_down.abs());
// return Mat4::perspective_infinite_reverse_rh(y_fov, x_fov / y_fov, self.near);
let fov = self.fov;
let is_vulkan_api = false; // FIXME wgpu probably abstracts this
let near_z = self.near;
let far_z = -1.; // use infinite proj
// let far_z = self.far;
let tan_angle_left = fov.angle_left.tan();
let tan_angle_right = fov.angle_right.tan();
let tan_angle_down = fov.angle_down.tan();
let tan_angle_up = fov.angle_up.tan();
let tan_angle_width = tan_angle_right - tan_angle_left;
// Set to tanAngleDown - tanAngleUp for a clip space with positive Y
// down (Vulkan). Set to tanAngleUp - tanAngleDown for a clip space with
// positive Y up (OpenGL / D3D / Metal).
// const float tanAngleHeight =
// graphicsApi == GRAPHICS_VULKAN ? (tanAngleDown - tanAngleUp) : (tanAngleUp - tanAngleDown);
let tan_angle_height = if is_vulkan_api {
tan_angle_down - tan_angle_up
} else {
tan_angle_up - tan_angle_down
};
// Set to nearZ for a [-1,1] Z clip space (OpenGL / OpenGL ES).
// Set to zero for a [0,1] Z clip space (Vulkan / D3D / Metal).
// const float offsetZ =
// (graphicsApi == GRAPHICS_OPENGL || graphicsApi == GRAPHICS_OPENGL_ES) ? nearZ : 0;
// FIXME handle enum of graphics apis
let offset_z = 0.;
let mut cols: [f32; 16] = [0.0; 16];
if far_z <= near_z {
// place the far plane at infinity
cols[0] = 2. / tan_angle_width;
cols[4] = 0.;
cols[8] = (tan_angle_right + tan_angle_left) / tan_angle_width;
cols[12] = 0.;
cols[1] = 0.;
cols[5] = 2. / tan_angle_height;
cols[9] = (tan_angle_up + tan_angle_down) / tan_angle_height;
cols[13] = 0.;
cols[2] = 0.;
cols[6] = 0.;
cols[10] = -1.;
cols[14] = -(near_z + offset_z);
cols[3] = 0.;
cols[7] = 0.;
cols[11] = -1.;
cols[15] = 0.;
// bevy uses the _reverse_ infinite projection
// https://dev.theomader.com/depth-precision/
let z_reversal = Mat4::from_cols_array_2d(&[
[1f32, 0., 0., 0.],
[0., 1., 0., 0.],
[0., 0., -1., 0.],
[0., 0., 1., 1.],
]);
return z_reversal * Mat4::from_cols_array(&cols);
} else {
// normal projection
cols[0] = 2. / tan_angle_width;
cols[4] = 0.;
cols[8] = (tan_angle_right + tan_angle_left) / tan_angle_width;
cols[12] = 0.;
cols[1] = 0.;
cols[5] = 2. / tan_angle_height;
cols[9] = (tan_angle_up + tan_angle_down) / tan_angle_height;
cols[13] = 0.;
cols[2] = 0.;
cols[6] = 0.;
cols[10] = -(far_z + offset_z) / (far_z - near_z);
cols[14] = -(far_z * (near_z + offset_z)) / (far_z - near_z);
cols[3] = 0.;
cols[7] = 0.;
cols[11] = -1.;
cols[15] = 0.;
}
Mat4::from_cols_array(&cols)
}
fn update(&mut self, _width: f32, _height: f32) {}
fn far(&self) -> f32 {
self.far
}
fn get_frustum_corners(&self, z_near: f32, z_far: f32) -> [Vec3A; 8] {
let tan_angle_left = self.fov.angle_left.tan();
let tan_angle_right = self.fov.angle_right.tan();
let tan_angle_bottom = self.fov.angle_down.tan();
let tan_angle_top = self.fov.angle_up.tan();
// NOTE: These vertices are in the specific order required by [`calculate_cascade`].
[
Vec3A::new(tan_angle_right, tan_angle_bottom, 1.0) * z_near, // bottom right
Vec3A::new(tan_angle_right, tan_angle_top, 1.0) * z_near, // top right
Vec3A::new(tan_angle_left, tan_angle_top, 1.0) * z_near, // top left
Vec3A::new(tan_angle_left, tan_angle_bottom, 1.0) * z_near, // bottom left
Vec3A::new(tan_angle_right, tan_angle_bottom, 1.0) * z_far, // bottom right
Vec3A::new(tan_angle_right, tan_angle_top, 1.0) * z_far, // top right
Vec3A::new(tan_angle_left, tan_angle_top, 1.0) * z_far, // top left
Vec3A::new(tan_angle_left, tan_angle_bottom, 1.0) * z_far, // bottom left
]
}
}
pub fn xr_camera_head_sync(
views: Res<crate::resources::XrViews>,
mut query: Query<(&mut Transform, &XrCamera, &mut XRProjection)>,
) {
//TODO calculate HMD position
for (mut transform, camera_type, mut xr_projection) in query.iter_mut() {
let view_idx = camera_type.0 as usize;
let view = match views.get(view_idx) {
Some(views) => views,
None => continue,
};
xr_projection.fov = view.fov;
transform.rotation = view.pose.orientation.to_quat();
transform.translation = view.pose.position.to_vec3();
}
}
pub fn xr_camera_head_sync_render_world(
views: Res<crate::resources::XrViews>,
mut query: Query<(&mut ExtractedView, &XrCamera, &RootTransform)>,
) {
for (mut extracted_view, camera_type, root) in query.iter_mut() {
let view_idx = camera_type.0 as usize;
let view = match views.get(view_idx) {
Some(views) => views,
None => continue,
};
let mut transform = Transform::IDENTITY;
transform.rotation = view.pose.orientation.to_quat();
transform.translation = view.pose.position.to_vec3();
extracted_view.world_from_view = root.mul_transform(transform).into();
}
}