move to single crate for webxr and openxr

This commit is contained in:
awtterpip
2024-04-15 18:13:17 -05:00
parent 3296a0a4ba
commit 5e769ef52b
25 changed files with 576 additions and 747 deletions

10
bevy_xr_api/Cargo.toml Normal file
View File

@@ -0,0 +1,10 @@
[package]
name = "bevy_xr_api"
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_xr_macros.path = "macros"

View File

@@ -0,0 +1,12 @@
[package]
name = "bevy_xr_macros"
version = "0.1.0"
edition = "2021"
[lib]
proc-macro = true
[dependencies]
proc-macro2 = "1.0"
quote = "1.0"
syn = "2.0"

View File

@@ -0,0 +1,117 @@
use proc_macro::TokenStream;
use proc_macro2::Span;
use quote::quote;
use syn::punctuated::Punctuated;
use syn::spanned::Spanned;
use syn::token::{Comma, Eq};
use syn::{parse_macro_input, parse_quote, AttrStyle, DeriveInput, Expr, Type};
use syn::parse::{Parse, ParseStream};
mod kw {
syn::custom_keyword!(action_type);
syn::custom_keyword!(name);
syn::custom_keyword!(pretty_name);
}
enum AttributeInput {
Kind(Type, Span),
Name(Expr, Span),
PrettyName(Expr, Span),
}
impl Parse for AttributeInput {
fn parse(input: ParseStream) -> syn::Result<Self> {
if let Ok(field) = input.parse::<kw::action_type>() {
input.parse::<Eq>()?;
Ok(Self::Kind(input.parse()?, field.span()))
} else if let Ok(field) = input.parse::<kw::name>() {
input.parse::<Eq>()?;
Ok(Self::Name(input.parse()?, field.span()))
} else if let Ok(field) = input.parse::<kw::pretty_name>() {
input.parse::<Eq>()?;
Ok(Self::PrettyName(input.parse()?, field.span()))
} else {
Err(input.error("expected 'action_type', 'name' or 'pretty_name'"))
}
}
}
#[proc_macro_derive(Action, attributes(action))]
pub fn derive_action(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let item = input.ident;
let mut attributes = vec![];
for attribute in input.attrs {
if let AttrStyle::Inner(token) = attribute.style {
return TokenStream::from(syn::Error::new(token.span, "This derive macro does not accept inner attributes").to_compile_error());
};
let parsed_attributes = match attribute.parse_args_with(Punctuated::<AttributeInput, Comma>::parse_terminated) {
Ok(inner) => inner,
Err(e) => return TokenStream::from(e.to_compile_error()),
};
for attr in parsed_attributes {
attributes.push(attr);
}
}
let mut kind: Option<Type> = None;
let mut name: Option<Expr> = None;
let mut pretty_name: Option<Expr> = None;
for attribute in attributes {
match attribute {
AttributeInput::Kind(ty, span) => {
if kind.is_some() {
return syn::Error::new(span, "attribute 'action_type' is defined more than once").to_compile_error().into();
}
kind = Some(ty);
},
AttributeInput::Name(expr, span) => {
if name.is_some() {
return syn::Error::new(span, "attribute 'name' is defined more than once").to_compile_error().into();
}
name = Some(expr);
},
AttributeInput::PrettyName(expr, span) => {
if pretty_name.is_some() {
return syn::Error::new(span, "attribute 'pretty_name' is defined more than once").to_compile_error().into();
}
pretty_name = Some(expr);
},
}
};
if kind.is_none() {
panic!("action_type isn't specified")
}
if name.is_none() {
name = Some(parse_quote! {
std::stringify!(#item)
});
}
if pretty_name.is_none() {
pretty_name = name.clone();
}
let kind = kind.unwrap();
let name = name.unwrap();
let pretty_name = pretty_name.unwrap();
let expanded = quote! {
impl bevy_xr::actions::Action for #item {
type ActionType = #kind;
fn info() -> bevy_xr::actions::ActionInfo {
bevy_xr::actions::ActionInfo {
pretty_name: #pretty_name,
name: #name,
action_type: <Self::ActionType as bevy_xr::actions::ActionTy>::TYPE,
type_id: std::any::TypeId::of::<Self>(),
}
}
}
};
TokenStream::from(expanded)
}

107
bevy_xr_api/src/actions.rs Normal file
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 use bevy_xr_macros::Action;
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());
}
}
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
}
}

134
bevy_xr_api/src/camera.rs Normal file
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),
}
}
}

4
bevy_xr_api/src/lib.rs Normal file
View File

@@ -0,0 +1,4 @@
pub mod actions;
pub mod camera;
pub mod session;
pub mod types;

127
bevy_xr_api/src/session.rs Normal file
View File

@@ -0,0 +1,127 @@
use std::sync::{Arc, RwLock};
use bevy::prelude::*;
pub struct XrSessionPlugin;
impl Plugin for XrSessionPlugin {
fn build(&self, app: &mut App) {
app.add_event::<CreateXrSession>()
.add_event::<DestroyXrSession>()
.add_event::<BeginXrSession>()
.add_event::<EndXrSession>()
.add_systems(
PreUpdate,
handle_session.run_if(resource_exists::<XrSharedStatus>),
);
}
}
#[derive(Resource, Clone)]
pub struct XrSharedStatus(Arc<RwLock<XrStatus>>);
impl XrSharedStatus {
pub fn new(status: XrStatus) -> Self {
Self(Arc::new(RwLock::new(status)))
}
pub fn get(&self) -> XrStatus {
*self.0.read().unwrap()
}
pub fn set(&self, status: XrStatus) {
*self.0.write().unwrap() = status;
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[repr(u8)]
pub enum XrStatus {
/// An XR session is not available here
Unavailable,
/// An XR session is available and ready to be created with a [`CreateXrSession`] event.
Available,
/// An XR session is created but not ready to begin.
Idle,
/// An XR session has been created and is ready to start rendering with a [`BeginXrSession`] event, or
Ready,
/// The XR session is running and can be stopped with an [`EndXrSession`] event.
Running,
/// The XR session is in the process of being stopped.
Stopping,
/// The XR session is in the process of being destroyed
Exiting,
}
pub fn handle_session(
status: Res<XrSharedStatus>,
mut previous_status: Local<Option<XrStatus>>,
mut create_session: EventWriter<CreateXrSession>,
mut begin_session: EventWriter<BeginXrSession>,
mut end_session: EventWriter<EndXrSession>,
mut destroy_session: EventWriter<DestroyXrSession>,
) {
let current_status = status.get();
if *previous_status != Some(current_status) {
match current_status {
XrStatus::Unavailable => {}
XrStatus::Available => {
create_session.send_default();
}
XrStatus::Idle => {}
XrStatus::Ready => {
begin_session.send_default();
}
XrStatus::Running => {}
XrStatus::Stopping => {
end_session.send_default();
}
XrStatus::Exiting => {
destroy_session.send_default();
}
}
}
*previous_status = Some(current_status);
}
/// A [`Condition`](bevy::ecs::schedule::Condition) system that says if the XR session is available. Returns true as long as [`XrStatus`] exists and isn't [`Unavailable`](XrStatus::Unavailable).
pub fn session_available(status: Option<Res<XrSharedStatus>>) -> bool {
status.is_some_and(|s| s.get() != XrStatus::Unavailable)
}
/// A [`Condition`](bevy::ecs::schedule::Condition) system that says if the XR session is ready or running
pub fn session_created(status: Option<Res<XrSharedStatus>>) -> bool {
matches!(
status.as_deref().map(XrSharedStatus::get),
Some(XrStatus::Ready | XrStatus::Running)
)
}
/// A [`Condition`](bevy::ecs::schedule::Condition) system that says if the XR session is running
pub fn session_running(status: Option<Res<XrSharedStatus>>) -> bool {
matches!(
status.as_deref().map(XrSharedStatus::get),
Some(XrStatus::Running)
)
}
/// A function that returns a [`Condition`](bevy::ecs::schedule::Condition) system that says if an the [`XrStatus`] is in a specific state
pub fn status_equals(status: XrStatus) -> impl FnMut(Option<Res<XrSharedStatus>>) -> bool {
move |state: Option<Res<XrSharedStatus>>| state.is_some_and(|s| s.get() == status)
}
/// Event sent to backends to create an XR session. Should only be called in the [`XrStatus::Available`] state.
#[derive(Event, Clone, Copy, Default)]
pub struct CreateXrSession;
/// Event sent to the backends to destroy an XR session.
#[derive(Event, Clone, Copy, Default)]
pub struct DestroyXrSession;
/// Event sent to backends to begin an XR session. Should only be called in the [`XrStatus::Ready`] state.
#[derive(Event, Clone, Copy, Default)]
pub struct BeginXrSession;
/// Event sent to backends to end an XR session. Should only be called in the [`XrStatus::Running`] state.
#[derive(Event, Clone, Copy, Default)]
pub struct EndXrSession;

6
bevy_xr_api/src/types.rs Normal file
View File

@@ -0,0 +1,6 @@
use bevy::math::{Quat, Vec3};
pub struct Pose {
pub position: Vec3,
pub orientation: Quat
}