move to single crate for webxr and openxr
This commit is contained in:
10
bevy_xr_api/Cargo.toml
Normal file
10
bevy_xr_api/Cargo.toml
Normal 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"
|
||||
12
bevy_xr_api/macros/Cargo.toml
Normal file
12
bevy_xr_api/macros/Cargo.toml
Normal 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"
|
||||
117
bevy_xr_api/macros/src/lib.rs
Normal file
117
bevy_xr_api/macros/src/lib.rs
Normal 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
107
bevy_xr_api/src/actions.rs
Normal 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
134
bevy_xr_api/src/camera.rs
Normal 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
4
bevy_xr_api/src/lib.rs
Normal 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
127
bevy_xr_api/src/session.rs
Normal 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
6
bevy_xr_api/src/types.rs
Normal file
@@ -0,0 +1,6 @@
|
||||
use bevy::math::{Quat, Vec3};
|
||||
|
||||
pub struct Pose {
|
||||
pub position: Vec3,
|
||||
pub orientation: Quat
|
||||
}
|
||||
Reference in New Issue
Block a user