move to api crate
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -3,3 +3,4 @@
|
|||||||
**/runtime_libs/arm64-v8a/*
|
**/runtime_libs/arm64-v8a/*
|
||||||
.vscode
|
.vscode
|
||||||
\.DS_Store
|
\.DS_Store
|
||||||
|
srcold
|
||||||
@@ -12,6 +12,8 @@ linked = ["openxr/linked"]
|
|||||||
vulkan = ["dep:ash"]
|
vulkan = ["dep:ash"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
xr_api = { path = "./xr_api" }
|
||||||
|
async-std = "1.12.0"
|
||||||
bevy = "0.12.1"
|
bevy = "0.12.1"
|
||||||
paste = "1.0.14"
|
paste = "1.0.14"
|
||||||
wgpu = "0.17.1"
|
wgpu = "0.17.1"
|
||||||
@@ -43,6 +45,7 @@ web-sys = { version = "0.3.61", features = [
|
|||||||
'HtmlCanvasElement',
|
'HtmlCanvasElement',
|
||||||
'WebGl2RenderingContext',
|
'WebGl2RenderingContext',
|
||||||
'WebGlFramebuffer',
|
'WebGlFramebuffer',
|
||||||
|
'GamepadHapticActuator',
|
||||||
## XR
|
## XR
|
||||||
'DomPointReadOnly',
|
'DomPointReadOnly',
|
||||||
'XrWebGlLayer',
|
'XrWebGlLayer',
|
||||||
@@ -81,3 +84,6 @@ web-sys = { version = "0.3.61", features = [
|
|||||||
'XrSystem',
|
'XrSystem',
|
||||||
] }
|
] }
|
||||||
wasm-bindgen-futures = "0.4"
|
wasm-bindgen-futures = "0.4"
|
||||||
|
|
||||||
|
[workspace]
|
||||||
|
members = ["xr_api"]
|
||||||
|
|||||||
@@ -1,35 +0,0 @@
|
|||||||
#[cfg(feature = "vulkan")]
|
|
||||||
pub mod vulkan;
|
|
||||||
|
|
||||||
use bevy::window::RawHandleWrapper;
|
|
||||||
use openxr::{FrameStream, FrameWaiter, Instance, Swapchain, ViewConfigurationType};
|
|
||||||
|
|
||||||
use crate::{error::XrError, types::GraphicsFeatures};
|
|
||||||
|
|
||||||
use super::OXrSession;
|
|
||||||
|
|
||||||
const VIEW_TYPE: ViewConfigurationType = ViewConfigurationType::PRIMARY_STEREO;
|
|
||||||
|
|
||||||
pub enum OXrGraphics {
|
|
||||||
#[cfg(feature = "vulkan")]
|
|
||||||
Vulkan {
|
|
||||||
swapchain: Swapchain<openxr::Vulkan>,
|
|
||||||
frame_stream: FrameStream<openxr::Vulkan>,
|
|
||||||
frame_waiter: FrameWaiter,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn init_oxr_graphics(
|
|
||||||
instance: Instance,
|
|
||||||
graphics: GraphicsFeatures,
|
|
||||||
window: Option<RawHandleWrapper>,
|
|
||||||
) -> Result<OXrSession, XrError> {
|
|
||||||
#[cfg(feature = "vulkan")]
|
|
||||||
if graphics.vulkan {
|
|
||||||
if let Ok(session) = vulkan::init_oxr_graphics(instance, window) {
|
|
||||||
return Ok(session);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Err(XrError {})
|
|
||||||
}
|
|
||||||
@@ -1,478 +0,0 @@
|
|||||||
use std::ffi::{c_void, CString};
|
|
||||||
use std::sync::{Arc, Mutex};
|
|
||||||
|
|
||||||
use ash::vk::{self, Handle};
|
|
||||||
use bevy::log::info;
|
|
||||||
use bevy::math::uvec2;
|
|
||||||
use bevy::render::renderer::{RenderAdapter, RenderAdapterInfo, RenderQueue};
|
|
||||||
use bevy::window::RawHandleWrapper;
|
|
||||||
use openxr as xr;
|
|
||||||
use openxr::Instance;
|
|
||||||
|
|
||||||
use crate::backend::graphics::{OXrGraphics, VIEW_TYPE};
|
|
||||||
use crate::backend::OXrSession;
|
|
||||||
use crate::error::XrError;
|
|
||||||
|
|
||||||
pub fn init_oxr_graphics(
|
|
||||||
xr_instance: Instance,
|
|
||||||
window: Option<RawHandleWrapper>,
|
|
||||||
) -> Result<OXrSession, XrError> {
|
|
||||||
use wgpu_hal::{api::Vulkan as V, Api};
|
|
||||||
|
|
||||||
let instance_props = xr_instance.properties()?;
|
|
||||||
let xr_system_id = xr_instance.system(xr::FormFactor::HEAD_MOUNTED_DISPLAY)?;
|
|
||||||
info!("created 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_mode = xr_instance.enumerate_environment_blend_modes(xr_system_id, VIEW_TYPE)?[0];
|
|
||||||
|
|
||||||
#[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() }.map_err(|_| XrError {})?;
|
|
||||||
let flags = wgpu_hal::InstanceFlags::empty();
|
|
||||||
let extensions = <V as Api>::Instance::required_extensions(&vk_entry, vk_target_version, flags)
|
|
||||||
.map_err(|_| XrError {})?;
|
|
||||||
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("Bevy").map_err(|_| XrError {})?;
|
|
||||||
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 _,
|
|
||||||
)
|
|
||||||
.map_err(|_| XrError {})?
|
|
||||||
.map_err(|_| XrError {})?;
|
|
||||||
|
|
||||||
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(())),
|
|
||||||
)
|
|
||||||
.map_err(|_| XrError {})?
|
|
||||||
};
|
|
||||||
|
|
||||||
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)
|
|
||||||
.ok_or(XrError {})?;
|
|
||||||
|
|
||||||
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 _,
|
|
||||||
)
|
|
||||||
.map_err(|_| XrError {})?
|
|
||||||
.map_err(|_| XrError {})?;
|
|
||||||
|
|
||||||
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,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
.map_err(|_| XrError {})?;
|
|
||||||
|
|
||||||
(
|
|
||||||
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,
|
|
||||||
features: wgpu_features,
|
|
||||||
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,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
.map_err(|_| XrError {})?;
|
|
||||||
|
|
||||||
let (session, frame_waiter, frame_stream) = unsafe {
|
|
||||||
xr_instance.create_session::<xr::Vulkan>(
|
|
||||||
xr_system_id,
|
|
||||||
&xr::vulkan::SessionCreateInfo {
|
|
||||||
instance: vk_instance_ptr,
|
|
||||||
physical_device: vk_physical_device_ptr,
|
|
||||||
device: vk_device_ptr,
|
|
||||||
queue_family_index,
|
|
||||||
queue_index: 0,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}?;
|
|
||||||
|
|
||||||
let views = xr_instance.enumerate_view_configuration_views(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);
|
|
||||||
|
|
||||||
let resolution = uvec2(
|
|
||||||
views[0].recommended_image_rect_width,
|
|
||||||
views[0].recommended_image_rect_height,
|
|
||||||
);
|
|
||||||
|
|
||||||
let swapchain = 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).ok_or(XrError {})?.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 = swapchain.enumerate_images().unwrap();
|
|
||||||
|
|
||||||
let buffers: Vec<_> = images
|
|
||||||
.into_iter()
|
|
||||||
.map(|color_image| {
|
|
||||||
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("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: 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("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: swapchain_format,
|
|
||||||
usage: wgpu::TextureUsages::RENDER_ATTACHMENT
|
|
||||||
| wgpu::TextureUsages::COPY_DST,
|
|
||||||
view_formats: &[],
|
|
||||||
},
|
|
||||||
)
|
|
||||||
};
|
|
||||||
texture
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
OXrSession {
|
|
||||||
graphics: OXrGraphics::Vulkan {
|
|
||||||
swapchain,
|
|
||||||
frame_stream,
|
|
||||||
frame_waiter,
|
|
||||||
},
|
|
||||||
device: wgpu_device.into(),
|
|
||||||
queue: RenderQueue(Arc::new(wgpu_queue)),
|
|
||||||
adapter_info: RenderAdapterInfo(wgpu_adapter.get_info()),
|
|
||||||
adapter: RenderAdapter(Arc::new(wgpu_adapter)),
|
|
||||||
session: session.into_any_graphics(),
|
|
||||||
blend_mode,
|
|
||||||
resolution,
|
|
||||||
format: swapchain_format,
|
|
||||||
buffers,
|
|
||||||
image_index: Mutex::new(0),
|
|
||||||
instance: xr_instance,
|
|
||||||
};
|
|
||||||
|
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn wgpu_to_vulkan(format: wgpu::TextureFormat) -> Option<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::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::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,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
#[cfg(not(target_family = "wasm"))]
|
|
||||||
pub(crate) mod graphics;
|
|
||||||
#[cfg(not(target_family = "wasm"))]
|
|
||||||
mod oxr;
|
|
||||||
#[cfg(not(target_family = "wasm"))]
|
|
||||||
pub(crate) mod oxr_utils;
|
|
||||||
pub mod traits;
|
|
||||||
#[cfg(target_family = "wasm")]
|
|
||||||
pub(crate) mod web_utils;
|
|
||||||
#[cfg(target_family = "wasm")]
|
|
||||||
mod webxr;
|
|
||||||
|
|
||||||
#[cfg(not(target_family = "wasm"))]
|
|
||||||
pub use oxr::*;
|
|
||||||
#[cfg(target_family = "wasm")]
|
|
||||||
pub use webxr::*;
|
|
||||||
|
|
||||||
macro_rules! xr_inner {
|
|
||||||
($res:ty, $oxr:ty, $webxr:ty) => {
|
|
||||||
paste::paste! {
|
|
||||||
pub enum [<$res Inner>] {
|
|
||||||
#[cfg(not(target_family = "wasm"))]
|
|
||||||
OpenXR($oxr),
|
|
||||||
#[cfg(target_family = "wasm")]
|
|
||||||
WebXR($webxr),
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(not(target_family = "wasm"))]
|
|
||||||
impl From<$oxr> for $res {
|
|
||||||
fn from(value: $oxr) -> $res {
|
|
||||||
$res(std::rc::Rc::new([<$res Inner>]::OpenXR(value)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(target_family = "wasm")]
|
|
||||||
impl From<$webxr> for $res {
|
|
||||||
fn from(value: $webxr) -> $res {
|
|
||||||
$res(std::rc::Rc::new([<$res Inner>]::WebXR(value)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
use crate::resources::*;
|
|
||||||
|
|
||||||
xr_inner!(XrEntry, OXrEntry, WebXrEntry);
|
|
||||||
xr_inner!(XrInstance, OXrInstance, WebXrInstance);
|
|
||||||
xr_inner!(XrSession, OXrSession, WebXrSession);
|
|
||||||
xr_inner!(XrInput, OXrAction, WebXrAction);
|
|
||||||
xr_inner!(XrController, OXrController, WebXrActionSet);
|
|
||||||
xr_inner!(XrActionSpace, OXrActionSpace, WebXrActionSpace);
|
|
||||||
xr_inner!(XrReferenceSpace, OXrReferenceSpace, WebXrReferenceSpace);
|
|
||||||
@@ -1,159 +0,0 @@
|
|||||||
use std::rc::Rc;
|
|
||||||
use std::sync::Mutex;
|
|
||||||
|
|
||||||
use bevy::math::UVec2;
|
|
||||||
use bevy::render::renderer::{RenderAdapter, RenderAdapterInfo, RenderDevice, RenderQueue};
|
|
||||||
|
|
||||||
use bevy::utils::HashMap;
|
|
||||||
use openxr::{
|
|
||||||
ActionSet, AnyGraphics, ApplicationInfo, Entry, EnvironmentBlendMode, ExtensionSet, Instance,
|
|
||||||
Session,
|
|
||||||
};
|
|
||||||
|
|
||||||
use super::graphics::OXrGraphics;
|
|
||||||
use super::traits::*;
|
|
||||||
use super::{oxr_utils::*, XrControllerInner};
|
|
||||||
|
|
||||||
use crate::backend::graphics::init_oxr_graphics;
|
|
||||||
use crate::error::XrError;
|
|
||||||
use crate::resources::*;
|
|
||||||
use crate::types::*;
|
|
||||||
|
|
||||||
pub struct OXrEntry(Entry);
|
|
||||||
|
|
||||||
impl XrEntryTrait for OXrEntry {
|
|
||||||
fn get_xr_entry(&self) -> Result<XrEntry, XrError> {
|
|
||||||
Ok(OXrEntry(xr_entry()?).into())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_available_features(&self) -> Result<FeatureList, XrError> {
|
|
||||||
let available_extensions = self.0.enumerate_extensions()?;
|
|
||||||
let mut feature_list = FeatureList::default();
|
|
||||||
feature_list.graphics.vulkan = available_extensions.khr_vulkan_enable2;
|
|
||||||
Ok(feature_list)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn create_instance(&self, features: FeatureList) -> Result<XrInstance, XrError> {
|
|
||||||
let mut enabled_extensions = ExtensionSet::default();
|
|
||||||
enabled_extensions.khr_vulkan_enable2 = features.graphics.vulkan;
|
|
||||||
let instance = self.0.create_instance(
|
|
||||||
&ApplicationInfo {
|
|
||||||
application_name: "Ambient",
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
&enabled_extensions,
|
|
||||||
&[],
|
|
||||||
)?;
|
|
||||||
|
|
||||||
Ok(OXrInstance {
|
|
||||||
enabled_features: features,
|
|
||||||
inner: instance,
|
|
||||||
}
|
|
||||||
.into())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct OXrInstance {
|
|
||||||
inner: Instance,
|
|
||||||
enabled_features: FeatureList,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl XrInstanceTrait for OXrInstance {
|
|
||||||
fn requested_features(&self) -> FeatureList {
|
|
||||||
self.enabled_features
|
|
||||||
}
|
|
||||||
|
|
||||||
fn create_session(&self, info: SessionCreateInfo) -> Result<XrSession, XrError> {
|
|
||||||
Ok(init_oxr_graphics(
|
|
||||||
self.inner.clone(),
|
|
||||||
self.enabled_features.graphics,
|
|
||||||
info.window,
|
|
||||||
)?
|
|
||||||
.into())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct OXrSession {
|
|
||||||
pub(crate) instance: Instance,
|
|
||||||
pub(crate) graphics: OXrGraphics,
|
|
||||||
pub(crate) device: RenderDevice,
|
|
||||||
pub(crate) queue: RenderQueue,
|
|
||||||
pub(crate) adapter: RenderAdapter,
|
|
||||||
pub(crate) adapter_info: RenderAdapterInfo,
|
|
||||||
pub(crate) session: Session<AnyGraphics>,
|
|
||||||
pub(crate) blend_mode: EnvironmentBlendMode,
|
|
||||||
pub(crate) resolution: UVec2,
|
|
||||||
pub(crate) format: wgpu::TextureFormat,
|
|
||||||
pub(crate) buffers: Vec<wgpu::Texture>,
|
|
||||||
pub(crate) image_index: Mutex<usize>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl XrSessionTrait for OXrSession {
|
|
||||||
fn sync_controllers(&self, (left, _): (XrController, XrController)) -> Result<(), XrError> {
|
|
||||||
let XrControllerInner::OpenXR(controller) = &**left;
|
|
||||||
self.session
|
|
||||||
.sync_actions(&[(&controller.action_set).into()])?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_reference_space(&self, info: ReferenceSpaceInfo) -> Result<XrReferenceSpace, XrError> {
|
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn create_controllers(
|
|
||||||
&self,
|
|
||||||
info: ActionSetCreateInfo,
|
|
||||||
) -> Result<(XrController, XrController), XrError> {
|
|
||||||
let instance = &self.instance;
|
|
||||||
let action_set = instance.create_action_set("controllers", "XR Controllers", 0)?;
|
|
||||||
let left_path = instance.string_to_path("/user/hand/left").unwrap();
|
|
||||||
let right_path = instance.string_to_path("/user/hand/right").unwrap();
|
|
||||||
let hand_subpaths = &[left_path, right_path];
|
|
||||||
use ActionPath::*;
|
|
||||||
let actions = &[
|
|
||||||
HandPose,
|
|
||||||
PointerPose,
|
|
||||||
GripPull,
|
|
||||||
TriggerPull,
|
|
||||||
TriggerTouch,
|
|
||||||
HapticFeedback,
|
|
||||||
PrimaryButton,
|
|
||||||
PrimaryButtonTouch,
|
|
||||||
SecondaryButton,
|
|
||||||
SecondaryButtonTouch,
|
|
||||||
MenuButton,
|
|
||||||
ThumbstickX,
|
|
||||||
ThumbstickY,
|
|
||||||
ThumbstickTouch,
|
|
||||||
ThumbstickClick,
|
|
||||||
ThumbrestTouch,
|
|
||||||
];
|
|
||||||
let action_map = Rc::new(create_actions(&action_set, actions, hand_subpaths)?);
|
|
||||||
Ok((
|
|
||||||
OXrController {
|
|
||||||
action_set: action_set.clone(),
|
|
||||||
actions: action_map.clone(),
|
|
||||||
side: Side::Left,
|
|
||||||
}
|
|
||||||
.into(),
|
|
||||||
OXrController {
|
|
||||||
action_set,
|
|
||||||
actions: action_map,
|
|
||||||
side: Side::Right,
|
|
||||||
}
|
|
||||||
.into(),
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct OXrAction {}
|
|
||||||
|
|
||||||
pub struct OXrController {
|
|
||||||
action_set: ActionSet,
|
|
||||||
actions: Rc<HashMap<ActionPath, TypedAction>>,
|
|
||||||
side: Side,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct OXrActionSpace {}
|
|
||||||
|
|
||||||
pub struct OXrReferenceSpace {}
|
|
||||||
@@ -1,132 +0,0 @@
|
|||||||
use crate::{
|
|
||||||
error::XrError,
|
|
||||||
types::{ActionPath, ActionSidedness, ActionType, ControllerType, Side},
|
|
||||||
};
|
|
||||||
use bevy::utils::HashMap;
|
|
||||||
use openxr::{Action, ActionSet, Binding, Entry, Haptic, Path, Posef};
|
|
||||||
|
|
||||||
#[cfg(feature = "linked")]
|
|
||||||
pub fn xr_entry() -> Result<openxr::Entry, XrError> {
|
|
||||||
Ok(Entry::linked())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(not(feature = "linked"))]
|
|
||||||
pub fn xr_entry() -> Result<openxr::Entry, XrError> {
|
|
||||||
unsafe { Entry::load().map_err(|_| XrError {}) }
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn create_actions(
|
|
||||||
action_set: &ActionSet,
|
|
||||||
actions: &[ActionPath],
|
|
||||||
hand_subpaths: &[Path],
|
|
||||||
) -> Result<HashMap<ActionPath, TypedAction>, XrError> {
|
|
||||||
let mut action_map = HashMap::new();
|
|
||||||
for action in actions {
|
|
||||||
let subaction_paths = match action.sidedness() {
|
|
||||||
ActionSidedness::Single => &[],
|
|
||||||
ActionSidedness::Double => hand_subpaths,
|
|
||||||
};
|
|
||||||
let name = action.action_name();
|
|
||||||
let localized_name = action.pretty_action_name();
|
|
||||||
let typed_action = match action.action_type() {
|
|
||||||
ActionType::Bool => TypedAction::Bool(action_set.create_action(
|
|
||||||
name,
|
|
||||||
localized_name,
|
|
||||||
subaction_paths,
|
|
||||||
)?),
|
|
||||||
ActionType::Float => {
|
|
||||||
TypedAction::F32(action_set.create_action(name, localized_name, subaction_paths)?)
|
|
||||||
}
|
|
||||||
ActionType::Haptics => TypedAction::Haptic(action_set.create_action(
|
|
||||||
name,
|
|
||||||
localized_name,
|
|
||||||
subaction_paths,
|
|
||||||
)?),
|
|
||||||
ActionType::Pose => TypedAction::PoseF(action_set.create_action(
|
|
||||||
name,
|
|
||||||
localized_name,
|
|
||||||
subaction_paths,
|
|
||||||
)?),
|
|
||||||
};
|
|
||||||
|
|
||||||
action_map.insert(*action, typed_action);
|
|
||||||
}
|
|
||||||
Ok(action_map)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub enum TypedAction {
|
|
||||||
F32(Action<f32>),
|
|
||||||
Bool(Action<bool>),
|
|
||||||
PoseF(Action<Posef>),
|
|
||||||
Haptic(Action<Haptic>),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TypedAction {
|
|
||||||
fn make_binding(&self, name: Path) -> Binding {
|
|
||||||
match self {
|
|
||||||
TypedAction::F32(a) => Binding::new(a, name),
|
|
||||||
TypedAction::Bool(a) => Binding::new(a, name),
|
|
||||||
TypedAction::PoseF(a) => Binding::new(a, name),
|
|
||||||
TypedAction::Haptic(a) => Binding::new(a, name),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ControllerType {
|
|
||||||
pub(crate) fn set_controller_bindings(
|
|
||||||
&self,
|
|
||||||
instance: &openxr::Instance,
|
|
||||||
bindings: &HashMap<ActionPath, TypedAction>,
|
|
||||||
) -> Result<(), XrError> {
|
|
||||||
match self {
|
|
||||||
ControllerType::OculusTouch => {
|
|
||||||
instance.suggest_interaction_profile_bindings(
|
|
||||||
instance.string_to_path("/interaction_profiles/oculus/touch_controller")?,
|
|
||||||
&[],
|
|
||||||
)?;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_binding_paths(&self, path: ActionPath) -> &[&'static str] {
|
|
||||||
match self {
|
|
||||||
ControllerType::OculusTouch => match path {
|
|
||||||
ActionPath::HandPose => &[
|
|
||||||
"/user/hand/left/input/grip/pose",
|
|
||||||
"/user/hand/right/input/grip/pose",
|
|
||||||
],
|
|
||||||
ActionPath::PointerPose => &[
|
|
||||||
"/user/hand/left/input/aim/pose",
|
|
||||||
"/user/hand/right/input/aim/pose",
|
|
||||||
],
|
|
||||||
ActionPath::GripPull => &[
|
|
||||||
"/user/hand/left/input/squeeze/value",
|
|
||||||
"/user/hand/right/input/squeeze/value",
|
|
||||||
],
|
|
||||||
ActionPath::TriggerPull => &[
|
|
||||||
"/user/hand/left/input/trigger/value",
|
|
||||||
"/user/hand/right/input/trigger/value",
|
|
||||||
],
|
|
||||||
ActionPath::TriggerTouch => &[
|
|
||||||
"/user/hand/left/input/trigger/touch",
|
|
||||||
"/user/hand/right/input/trigger/touch",
|
|
||||||
],
|
|
||||||
ActionPath::HapticFeedback => &[
|
|
||||||
"/user/hand/left/output/haptic",
|
|
||||||
"/user/hand/right/output/haptic",
|
|
||||||
],
|
|
||||||
ActionPath::PrimaryButton => &[],
|
|
||||||
ActionPath::PrimaryButtonTouch => todo!(),
|
|
||||||
ActionPath::SecondaryButton => todo!(),
|
|
||||||
ActionPath::SecondaryButtonTouch => todo!(),
|
|
||||||
ActionPath::MenuButton => todo!(),
|
|
||||||
ActionPath::ThumbstickX => todo!(),
|
|
||||||
ActionPath::ThumbstickY => todo!(),
|
|
||||||
ActionPath::ThumbstickTouch => todo!(),
|
|
||||||
ActionPath::ThumbstickClick => todo!(),
|
|
||||||
ActionPath::ThumbrestTouch => todo!(),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
use crate::error::XrError;
|
|
||||||
use crate::resources::*;
|
|
||||||
use crate::types::*;
|
|
||||||
|
|
||||||
use bevy::render::primitives::Aabb;
|
|
||||||
|
|
||||||
pub trait XrEntryTrait {
|
|
||||||
fn get_xr_entry(&self) -> Result<XrEntry, XrError>;
|
|
||||||
fn get_available_features(&self) -> Result<FeatureList, XrError>;
|
|
||||||
fn create_instance(&self, features: FeatureList) -> Result<XrInstance, XrError>;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub trait XrInstanceTrait {
|
|
||||||
fn requested_features(&self) -> FeatureList;
|
|
||||||
fn create_session(&self, info: SessionCreateInfo) -> Result<XrSession, XrError>;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub trait XrSessionTrait {
|
|
||||||
fn sync_controllers(&self, controllers: (XrController, XrController)) -> Result<(), XrError>;
|
|
||||||
fn get_reference_space(&self, info: ReferenceSpaceInfo) -> Result<XrReferenceSpace, XrError>;
|
|
||||||
fn create_controllers(
|
|
||||||
&self,
|
|
||||||
info: ActionSetCreateInfo,
|
|
||||||
) -> Result<(XrController, XrController), XrError>;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub trait XrControllerTrait {
|
|
||||||
fn get_action_space(&self, info: ActionSpaceInfo) -> Result<XrActionSpace, XrError>;
|
|
||||||
fn get_action(&self, id: ActionId) -> Option<XrInput>;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub trait XrInputTrait {
|
|
||||||
fn get_action_state(&self) -> ActionState;
|
|
||||||
fn get_action_bool(&self) -> Option<bool> {
|
|
||||||
if let ActionState::Bool(b) = self.get_action_state() {
|
|
||||||
Some(b)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fn get_action_float(&self) -> Option<f32> {
|
|
||||||
if let ActionState::Float(f) = self.get_action_state() {
|
|
||||||
Some(f)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fn get_action_haptic(&self) -> Option<Haptics> {
|
|
||||||
if let ActionState::Haptics(h) = self.get_action_state() {
|
|
||||||
Some(h)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub trait XrActionSpaceTrait {
|
|
||||||
fn locate(&self, base: XrReferenceSpace) -> Pose;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub trait XrReferenceSpaceTrait {
|
|
||||||
fn bounds(&self) -> Aabb;
|
|
||||||
}
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
use wasm_bindgen::{JsCast, JsValue};
|
|
||||||
use wasm_bindgen_futures::JsFuture;
|
|
||||||
use web_sys::{
|
|
||||||
js_sys::{Object, Promise, Reflect},
|
|
||||||
HtmlCanvasElement, WebGl2RenderingContext,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub fn get_canvas(canvas_id: &str) -> Result<HtmlCanvasElement, JsValue> {
|
|
||||||
let query = format!("#{}", canvas_id);
|
|
||||||
let window = web_sys::window().unwrap();
|
|
||||||
let document = window.document().unwrap();
|
|
||||||
let canvas = document
|
|
||||||
.query_selector(&query)
|
|
||||||
.unwrap()
|
|
||||||
.expect("bevy_webxr - could not find canvas");
|
|
||||||
let canvas = canvas.dyn_into::<HtmlCanvasElement>()?;
|
|
||||||
Ok(canvas)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn create_webgl_context(
|
|
||||||
xr_mode: bool,
|
|
||||||
canvas: &str,
|
|
||||||
) -> Result<WebGl2RenderingContext, JsValue> {
|
|
||||||
let canvas = get_canvas(canvas)?;
|
|
||||||
|
|
||||||
let gl: WebGl2RenderingContext = if xr_mode {
|
|
||||||
let gl_attribs = Object::new();
|
|
||||||
Reflect::set(
|
|
||||||
&gl_attribs,
|
|
||||||
&JsValue::from_str("xrCompatible"),
|
|
||||||
&JsValue::TRUE,
|
|
||||||
)?;
|
|
||||||
canvas
|
|
||||||
.get_context_with_context_options("webgl2", &gl_attribs)?
|
|
||||||
.unwrap()
|
|
||||||
.dyn_into()?
|
|
||||||
} else {
|
|
||||||
canvas.get_context("webgl2")?.unwrap().dyn_into()?
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(gl)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub trait PromiseRes {
|
|
||||||
fn resolve<T: From<JsValue>>(self) -> Result<T, JsValue>;
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PromiseRes for Promise {
|
|
||||||
fn resolve<T: From<JsValue>>(self) -> Result<T, JsValue> {
|
|
||||||
resolve_promise(self)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn resolve_promise<T: From<JsValue>>(promise: Promise) -> Result<T, JsValue> {
|
|
||||||
bevy::tasks::block_on(async move { JsFuture::from(promise).await.map(Into::into) })
|
|
||||||
}
|
|
||||||
@@ -1,90 +0,0 @@
|
|||||||
use web_sys::XrRenderStateInit;
|
|
||||||
use web_sys::XrWebGlLayer;
|
|
||||||
|
|
||||||
use super::traits::*;
|
|
||||||
use super::web_utils::*;
|
|
||||||
|
|
||||||
use crate::error::XrError;
|
|
||||||
use crate::resources::*;
|
|
||||||
use crate::types::*;
|
|
||||||
|
|
||||||
pub const BEVY_CANVAS_ID: &str = "bevy_canvas";
|
|
||||||
pub const BEVY_CANVAS_QUERY: &str = "canvas[data-bevy-webxr=\"bevy_canvas\"]";
|
|
||||||
|
|
||||||
pub struct WebXrEntry(web_sys::XrSystem);
|
|
||||||
|
|
||||||
impl XrEntryTrait for WebXrEntry {
|
|
||||||
fn get_xr_entry(&self) -> Result<XrEntry, XrError> {
|
|
||||||
if let Some(window) = web_sys::window() {
|
|
||||||
Ok(WebXrEntry(window.navigator().xr()).into())
|
|
||||||
} else {
|
|
||||||
Err(XrError {})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_available_features(&self) -> Result<FeatureList, XrError> {
|
|
||||||
Ok(FeatureList::default())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn create_instance(&self, features: FeatureList) -> Result<XrInstance, XrError> {
|
|
||||||
Ok(WebXrInstance {
|
|
||||||
xr: self.0.clone(),
|
|
||||||
features,
|
|
||||||
}
|
|
||||||
.into())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct WebXrInstance {
|
|
||||||
xr: web_sys::XrSystem,
|
|
||||||
features: FeatureList,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl XrInstanceTrait for WebXrInstance {
|
|
||||||
fn requested_features(&self) -> FeatureList {
|
|
||||||
self.features
|
|
||||||
}
|
|
||||||
|
|
||||||
fn create_session(&self, info: SessionCreateInfo) -> Result<XrSession, XrError> {
|
|
||||||
let session = self
|
|
||||||
.xr
|
|
||||||
.request_session(web_sys::XrSessionMode::ImmersiveVr)
|
|
||||||
.resolve()?;
|
|
||||||
|
|
||||||
let gl = create_webgl_context(true, &info.canvas.expect("Expected canvas string"))?;
|
|
||||||
let xr_gl_layer = XrWebGlLayer::new_with_web_gl2_rendering_context(&session, &gl)?;
|
|
||||||
let mut render_state_init = XrRenderStateInit::new();
|
|
||||||
render_state_init.base_layer(Some(&xr_gl_layer));
|
|
||||||
session.update_render_state_with_state(&render_state_init);
|
|
||||||
|
|
||||||
Ok(WebXrSession(session).into())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct WebXrSession(web_sys::XrSession);
|
|
||||||
|
|
||||||
impl XrSessionTrait for WebXrSession {
|
|
||||||
fn sync_actions(&self, action_set: XrActionSet) {
|
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_reference_space(&self, _info: ReferenceSpaceInfo) -> Result<XrReferenceSpace, XrError> {
|
|
||||||
let space = self
|
|
||||||
.0
|
|
||||||
.request_reference_space(web_sys::XrReferenceSpaceType::BoundedFloor)
|
|
||||||
.resolve()?;
|
|
||||||
Ok(WebXrReferenceSpace(space).into())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn create_action_set(&self, info: ActionSetCreateInfo) -> Result<XrActionSet, XrError> {
|
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct WebXrActionSet(Vec<XrAction>);
|
|
||||||
|
|
||||||
pub struct WebXrAction(web_sys::XrInputSource);
|
|
||||||
|
|
||||||
pub struct WebXrActionSpace(web_sys::XrJointSpace);
|
|
||||||
|
|
||||||
pub struct WebXrReferenceSpace(web_sys::XrBoundedReferenceSpace);
|
|
||||||
15
src/error.rs
15
src/error.rs
@@ -1,15 +0,0 @@
|
|||||||
pub struct XrError {}
|
|
||||||
|
|
||||||
#[cfg(target_family = "wasm")]
|
|
||||||
impl From<wasm_bindgen::JsValue> for XrError {
|
|
||||||
fn from(_: wasm_bindgen::JsValue) -> Self {
|
|
||||||
Self {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(not(target_family = "wasm"))]
|
|
||||||
impl From<openxr::sys::Result> for XrError {
|
|
||||||
fn from(_: openxr::sys::Result) -> Self {
|
|
||||||
Self {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
pub mod backend;
|
|
||||||
pub mod error;
|
|
||||||
mod resource_macros;
|
|
||||||
pub mod resources;
|
|
||||||
pub mod types;
|
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
use std::rc::Rc;
|
|
||||||
|
|
||||||
use bevy::prelude::{Deref, DerefMut};
|
|
||||||
|
|
||||||
use crate::backend;
|
|
||||||
|
|
||||||
macro_rules! xr_resources {
|
|
||||||
(
|
|
||||||
$(
|
|
||||||
$(#[$attr:meta])*
|
|
||||||
$name:ident;
|
|
||||||
)*
|
|
||||||
) => {
|
|
||||||
paste::paste! {
|
|
||||||
$(
|
|
||||||
$(#[$attr])*
|
|
||||||
#[derive(Clone, Deref, DerefMut)]
|
|
||||||
pub struct $name(pub(crate) Rc<backend::[<$name Inner>]>);
|
|
||||||
)*
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
xr_resources! {
|
|
||||||
XrEntry;
|
|
||||||
XrInstance;
|
|
||||||
XrSession;
|
|
||||||
XrInput;
|
|
||||||
XrController;
|
|
||||||
XrActionSpace;
|
|
||||||
XrReferenceSpace;
|
|
||||||
}
|
|
||||||
191
src/types.rs
191
src/types.rs
@@ -1,191 +0,0 @@
|
|||||||
use bevy::{
|
|
||||||
render::renderer::{RenderAdapter, RenderAdapterInfo, RenderDevice, RenderQueue},
|
|
||||||
window::RawHandleWrapper,
|
|
||||||
};
|
|
||||||
|
|
||||||
/// This struct stores all of the render resources required to initialize the bevy render plugin
|
|
||||||
///
|
|
||||||
/// Returned from [XrSessionTrait::get_render_resources](crate::backend::traits::XrSessionTrait::get_render_resources)
|
|
||||||
pub struct RenderResources {
|
|
||||||
pub adapter: RenderAdapter,
|
|
||||||
pub adapter_info: RenderAdapterInfo,
|
|
||||||
pub device: RenderDevice,
|
|
||||||
pub queue: RenderQueue,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Information used to create the [XrSession](crate::resources::XrSession)
|
|
||||||
///
|
|
||||||
/// Passed into [XrInstanceTrait::create_session](crate::backend::traits::XrInstanceTrait::create_session)
|
|
||||||
pub struct SessionCreateInfo {
|
|
||||||
/// This field is required to be [Some] when using WebXR
|
|
||||||
pub canvas: Option<String>,
|
|
||||||
pub window: Option<RawHandleWrapper>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Copy)]
|
|
||||||
pub enum ControllerType {
|
|
||||||
OculusTouch,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub struct ActionSetCreateInfo {
|
|
||||||
pub controller: ControllerType,
|
|
||||||
// TODO!() allow custom fields
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct ReferenceSpaceInfo {}
|
|
||||||
|
|
||||||
pub struct ActionSpaceInfo {}
|
|
||||||
|
|
||||||
#[derive(Clone, Copy, Default)]
|
|
||||||
pub struct FeatureList {
|
|
||||||
pub graphics: GraphicsFeatures,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Copy, Default)]
|
|
||||||
pub struct GraphicsFeatures {
|
|
||||||
pub vulkan: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct Pose;
|
|
||||||
|
|
||||||
pub struct Haptics;
|
|
||||||
|
|
||||||
pub enum ActionState {
|
|
||||||
Bool(bool),
|
|
||||||
Float(f32),
|
|
||||||
Haptics(Haptics),
|
|
||||||
Pose(Pose),
|
|
||||||
}
|
|
||||||
|
|
||||||
pub enum ActionType {
|
|
||||||
Bool,
|
|
||||||
Float,
|
|
||||||
Haptics,
|
|
||||||
Pose,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct ActionId {
|
|
||||||
pub action_path: ActionPath,
|
|
||||||
pub side: Option<Side>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Copy, Hash, PartialEq, Eq)]
|
|
||||||
pub enum ActionPath {
|
|
||||||
HandPose,
|
|
||||||
PointerPose,
|
|
||||||
GripPull,
|
|
||||||
TriggerPull,
|
|
||||||
TriggerTouch,
|
|
||||||
HapticFeedback,
|
|
||||||
PrimaryButton,
|
|
||||||
PrimaryButtonTouch,
|
|
||||||
SecondaryButton,
|
|
||||||
SecondaryButtonTouch,
|
|
||||||
MenuButton,
|
|
||||||
ThumbstickX,
|
|
||||||
ThumbstickY,
|
|
||||||
ThumbstickTouch,
|
|
||||||
ThumbstickClick,
|
|
||||||
ThumbrestTouch,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ActionPath {
|
|
||||||
pub fn action_name(&self) -> &'static str {
|
|
||||||
use ActionPath::*;
|
|
||||||
match self {
|
|
||||||
HandPose => "hand_pose",
|
|
||||||
PointerPose => "pointer_pose",
|
|
||||||
GripPull => "grip_pull",
|
|
||||||
TriggerPull => "trigger_pull",
|
|
||||||
TriggerTouch => "trigger_touch",
|
|
||||||
HapticFeedback => "haptic_feedback",
|
|
||||||
PrimaryButton => "primary_button",
|
|
||||||
PrimaryButtonTouch => "primary_button_touch",
|
|
||||||
SecondaryButton => "secondary_button",
|
|
||||||
SecondaryButtonTouch => "secondary_button_touch",
|
|
||||||
MenuButton => "menu_button",
|
|
||||||
ThumbstickX => "thumbstick_x",
|
|
||||||
ThumbstickY => "thumbstick_y",
|
|
||||||
ThumbstickTouch => "thumbstick_touch",
|
|
||||||
ThumbstickClick => "thumbstick_click",
|
|
||||||
ThumbrestTouch => "thumbrest_touch",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn pretty_action_name(&self) -> &'static str {
|
|
||||||
use ActionPath::*;
|
|
||||||
match self {
|
|
||||||
HandPose => "Hand Pose",
|
|
||||||
PointerPose => "Pointer Pose",
|
|
||||||
GripPull => "Grip Pull",
|
|
||||||
TriggerPull => "Trigger Pull",
|
|
||||||
TriggerTouch => "Trigger Touch",
|
|
||||||
HapticFeedback => "Haptic Feedback",
|
|
||||||
PrimaryButton => "Primary Button",
|
|
||||||
PrimaryButtonTouch => "Primary Button Touch",
|
|
||||||
SecondaryButton => "Secondary Button",
|
|
||||||
SecondaryButtonTouch => "Secondary Button Touch",
|
|
||||||
MenuButton => "Menu Button",
|
|
||||||
ThumbstickX => "Thumbstick X",
|
|
||||||
ThumbstickY => "Thumbstick Y",
|
|
||||||
ThumbstickTouch => "Thumbstick Touch",
|
|
||||||
ThumbstickClick => "Thumbstick Click",
|
|
||||||
ThumbrestTouch => "Thumbrest Touch",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn action_type(&self) -> ActionType {
|
|
||||||
use ActionPath::*;
|
|
||||||
match self {
|
|
||||||
HandPose => ActionType::Pose,
|
|
||||||
PointerPose => ActionType::Pose,
|
|
||||||
GripPull => ActionType::Float,
|
|
||||||
TriggerPull => ActionType::Float,
|
|
||||||
TriggerTouch => ActionType::Bool,
|
|
||||||
HapticFeedback => ActionType::Haptics,
|
|
||||||
PrimaryButton => ActionType::Bool,
|
|
||||||
PrimaryButtonTouch => ActionType::Bool,
|
|
||||||
SecondaryButton => ActionType::Bool,
|
|
||||||
SecondaryButtonTouch => ActionType::Bool,
|
|
||||||
MenuButton => ActionType::Bool,
|
|
||||||
ThumbstickX => ActionType::Float,
|
|
||||||
ThumbstickY => ActionType::Float,
|
|
||||||
ThumbstickTouch => ActionType::Bool,
|
|
||||||
ThumbstickClick => ActionType::Bool,
|
|
||||||
ThumbrestTouch => ActionType::Bool,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn sidedness(&self) -> ActionSidedness {
|
|
||||||
use ActionPath::*;
|
|
||||||
match self {
|
|
||||||
HandPose => ActionSidedness::Double,
|
|
||||||
PointerPose => ActionSidedness::Double,
|
|
||||||
GripPull => ActionSidedness::Double,
|
|
||||||
TriggerPull => ActionSidedness::Double,
|
|
||||||
TriggerTouch => ActionSidedness::Double,
|
|
||||||
HapticFeedback => ActionSidedness::Double,
|
|
||||||
PrimaryButton => ActionSidedness::Double,
|
|
||||||
PrimaryButtonTouch => ActionSidedness::Double,
|
|
||||||
SecondaryButton => ActionSidedness::Double,
|
|
||||||
SecondaryButtonTouch => ActionSidedness::Double,
|
|
||||||
MenuButton => ActionSidedness::Double,
|
|
||||||
ThumbstickX => ActionSidedness::Double,
|
|
||||||
ThumbstickY => ActionSidedness::Double,
|
|
||||||
ThumbstickTouch => ActionSidedness::Double,
|
|
||||||
ThumbstickClick => ActionSidedness::Double,
|
|
||||||
ThumbrestTouch => ActionSidedness::Double,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub enum ActionSidedness {
|
|
||||||
Single,
|
|
||||||
Double,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub enum Side {
|
|
||||||
Left,
|
|
||||||
Right,
|
|
||||||
}
|
|
||||||
69
xr_api/Cargo.toml
Normal file
69
xr_api/Cargo.toml
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
[package]
|
||||||
|
name = "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]
|
||||||
|
futures = "0.3.29"
|
||||||
|
thiserror = "1.0.51"
|
||||||
|
|
||||||
|
[target.'cfg(not(target_family = "wasm"))'.dependencies]
|
||||||
|
openxr = "0.17.1"
|
||||||
|
|
||||||
|
[target.'cfg(target_family = "wasm")'.dependencies]
|
||||||
|
wasm-bindgen = "0.2.87"
|
||||||
|
web-sys = { version = "0.3.61", features = [
|
||||||
|
# STANDARD
|
||||||
|
'console',
|
||||||
|
'Document',
|
||||||
|
'Element',
|
||||||
|
'Headers',
|
||||||
|
'Navigator',
|
||||||
|
'Window',
|
||||||
|
# IO
|
||||||
|
# 'Url',
|
||||||
|
# WEBGL
|
||||||
|
'Gpu',
|
||||||
|
'HtmlCanvasElement',
|
||||||
|
'WebGl2RenderingContext',
|
||||||
|
'WebGlFramebuffer',
|
||||||
|
## XR
|
||||||
|
'DomPointReadOnly',
|
||||||
|
'XrWebGlLayer',
|
||||||
|
'XrBoundedReferenceSpace',
|
||||||
|
'XrEye',
|
||||||
|
'XrFrame',
|
||||||
|
'XrHandedness',
|
||||||
|
'XrInputSource',
|
||||||
|
'XrInputSourceArray',
|
||||||
|
'XrInputSourceEvent',
|
||||||
|
'XrInputSourceEventInit',
|
||||||
|
'XrInputSourcesChangeEvent',
|
||||||
|
'XrJointPose',
|
||||||
|
'XrJointSpace',
|
||||||
|
'XrPose',
|
||||||
|
'XrReferenceSpace',
|
||||||
|
'XrReferenceSpaceEvent',
|
||||||
|
'XrReferenceSpaceEventInit',
|
||||||
|
'XrReferenceSpaceType',
|
||||||
|
'XrRenderState',
|
||||||
|
'XrRenderStateInit',
|
||||||
|
'XrRigidTransform',
|
||||||
|
'XrSession',
|
||||||
|
'XrSessionEvent',
|
||||||
|
'XrSessionEventInit',
|
||||||
|
'XrSessionInit',
|
||||||
|
'XrSessionMode',
|
||||||
|
'XrSpace',
|
||||||
|
'XrTargetRayMode',
|
||||||
|
'XrView',
|
||||||
|
'XrViewerPose',
|
||||||
|
'XrViewport',
|
||||||
|
'XrVisibilityState',
|
||||||
|
'XrWebGlLayer',
|
||||||
|
'XrWebGlLayerInit',
|
||||||
|
'XrSystem',
|
||||||
|
] }
|
||||||
|
wasm-bindgen-futures = "0.4"
|
||||||
111
xr_api/src/api.rs
Normal file
111
xr_api/src/api.rs
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
use std::ops::Deref;
|
||||||
|
use std::rc::Rc;
|
||||||
|
|
||||||
|
use crate::prelude::*;
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct Entry(Rc<dyn EntryTrait>);
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct Instance(Rc<dyn InstanceTrait>);
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct Session(Rc<dyn SessionTrait>);
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct Input(Rc<dyn InputTrait>);
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct Action<A: ActionType>(A::Inner);
|
||||||
|
|
||||||
|
impl Deref for Entry {
|
||||||
|
type Target = dyn EntryTrait;
|
||||||
|
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
&*self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Deref for Instance {
|
||||||
|
type Target = dyn InstanceTrait;
|
||||||
|
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
&*self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Deref for Session {
|
||||||
|
type Target = dyn SessionTrait;
|
||||||
|
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
&*self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Deref for Input {
|
||||||
|
type Target = dyn InputTrait;
|
||||||
|
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
&*self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<O, A> Deref for Action<A>
|
||||||
|
where
|
||||||
|
A: ActionType,
|
||||||
|
A::Inner: Deref<Target = O>,
|
||||||
|
{
|
||||||
|
type Target = O;
|
||||||
|
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
&*self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: EntryTrait + 'static> From<T> for Entry {
|
||||||
|
fn from(value: T) -> Self {
|
||||||
|
Self(Rc::new(value))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: InstanceTrait + 'static> From<T> for Instance {
|
||||||
|
fn from(value: T) -> Self {
|
||||||
|
Self(Rc::new(value))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: SessionTrait + 'static> From<T> for Session {
|
||||||
|
fn from(value: T) -> Self {
|
||||||
|
Self(Rc::new(value))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: InputTrait + 'static> From<T> for Input {
|
||||||
|
fn from(value: T) -> Self {
|
||||||
|
Self(Rc::new(value))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: HapticTrait + 'static> From<T> for Action<Haptic> {
|
||||||
|
fn from(value: T) -> Self {
|
||||||
|
Self(Rc::new(value))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: ActionInputTrait<f32> + 'static> From<T> for Action<f32> {
|
||||||
|
fn from(value: T) -> Self {
|
||||||
|
Self(Rc::new(value))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: ActionInputTrait<Pose> + 'static> From<T> for Action<Pose> {
|
||||||
|
fn from(value: T) -> Self {
|
||||||
|
Self(Rc::new(value))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: ActionInputTrait<bool> + 'static> From<T> for Action<bool> {
|
||||||
|
fn from(value: T) -> Self {
|
||||||
|
Self(Rc::new(value))
|
||||||
|
}
|
||||||
|
}
|
||||||
67
xr_api/src/api_traits.rs
Normal file
67
xr_api/src/api_traits.rs
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
use crate::prelude::*;
|
||||||
|
|
||||||
|
pub trait EntryTrait {
|
||||||
|
/// Return currently available extensions
|
||||||
|
fn available_extensions(&self) -> Result<ExtensionSet>;
|
||||||
|
/// Create an [Instance] with the enabled extensions.
|
||||||
|
fn create_instance(&self, exts: ExtensionSet) -> Result<Instance>;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait InstanceTrait {
|
||||||
|
/// Returns the [Entry] used to create this.
|
||||||
|
fn entry(&self) -> Entry;
|
||||||
|
/// Returns an [ExtensionSet] listing all enabled extensions.
|
||||||
|
fn enabled_extensions(&self) -> ExtensionSet;
|
||||||
|
/// Creates a [Session] with the requested properties
|
||||||
|
fn create_session(&self, info: SessionCreateInfo) -> Result<Session>;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait SessionTrait {
|
||||||
|
/// Returns the [Instance] used to create this.
|
||||||
|
fn instance(&self) -> &Instance;
|
||||||
|
/// Request input modules with the specified bindings.
|
||||||
|
fn create_input(&self, bindings: Bindings) -> Result<Input>;
|
||||||
|
/// Blocks until a rendering frame is available and then begins it.
|
||||||
|
fn begin_frame(&self) -> Result<()>;
|
||||||
|
/// Submits rendering work for this frame.
|
||||||
|
fn end_frame(&self) -> Result<()>;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait InputTrait {
|
||||||
|
fn get_haptics(&self, path: ActionId) -> Result<Action<Haptic>>;
|
||||||
|
fn get_pose(&self, path: ActionId) -> Result<Action<Pose>>;
|
||||||
|
fn get_float(&self, path: ActionId) -> Result<Action<f32>>;
|
||||||
|
fn get_bool(&self, path: ActionId) -> Result<Action<bool>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait ActionTrait {
|
||||||
|
fn id(&self) -> ActionId;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait ActionInputTrait<A> {}
|
||||||
|
|
||||||
|
pub trait HapticTrait {}
|
||||||
|
|
||||||
|
impl<T: InstanceTrait> EntryTrait for T {
|
||||||
|
fn available_extensions(&self) -> Result<ExtensionSet> {
|
||||||
|
self.entry().available_extensions()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_instance(&self, exts: ExtensionSet) -> Result<Instance> {
|
||||||
|
self.entry().create_instance(exts)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: SessionTrait> InstanceTrait for T {
|
||||||
|
fn entry(&self) -> Entry {
|
||||||
|
self.instance().entry()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn enabled_extensions(&self) -> ExtensionSet {
|
||||||
|
self.instance().enabled_extensions()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_session(&self, info: SessionCreateInfo) -> Result<Session> {
|
||||||
|
self.instance().create_session(info)
|
||||||
|
}
|
||||||
|
}
|
||||||
4
xr_api/src/backend/mod.rs
Normal file
4
xr_api/src/backend/mod.rs
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
#[cfg(not(target_family = "wasm"))]
|
||||||
|
pub mod oxr;
|
||||||
|
#[cfg(target_family = "wasm")]
|
||||||
|
pub mod webxr;
|
||||||
30
xr_api/src/backend/oxr.rs
Normal file
30
xr_api/src/backend/oxr.rs
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
use crate::prelude::*;
|
||||||
|
|
||||||
|
pub struct OXrEntry(openxr::Entry);
|
||||||
|
|
||||||
|
impl EntryTrait for OXrEntry {
|
||||||
|
fn available_extensions(&self) -> Result<ExtensionSet> {
|
||||||
|
// self.0.enumerate_extensions();
|
||||||
|
Ok(ExtensionSet::default())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_instance(&self, exts: ExtensionSet) -> Result<Instance> {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct OXrInstance(openxr::Instance);
|
||||||
|
|
||||||
|
impl InstanceTrait for OXrInstance {
|
||||||
|
fn entry(&self) -> Entry {
|
||||||
|
OXrEntry(self.0.entry().clone()).into()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn enabled_extensions(&self) -> ExtensionSet {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_session(&self, info: SessionCreateInfo) -> Result<Session> {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
}
|
||||||
0
xr_api/src/backend/oxr/utils.rs
Normal file
0
xr_api/src/backend/oxr/utils.rs
Normal file
181
xr_api/src/backend/webxr.rs
Normal file
181
xr_api/src/backend/webxr.rs
Normal file
@@ -0,0 +1,181 @@
|
|||||||
|
use std::sync::{
|
||||||
|
mpsc::{channel, Sender},
|
||||||
|
Mutex,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::prelude::*;
|
||||||
|
|
||||||
|
use wasm_bindgen::{closure::Closure, JsCast};
|
||||||
|
use web_sys::{js_sys, XrFrame, XrInputSource};
|
||||||
|
|
||||||
|
mod utils;
|
||||||
|
|
||||||
|
use utils::*;
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct WebXrEntry(web_sys::XrSystem);
|
||||||
|
|
||||||
|
impl EntryTrait for WebXrEntry {
|
||||||
|
fn available_extensions(&self) -> Result<ExtensionSet> {
|
||||||
|
Ok(ExtensionSet::default())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_instance(&self, exts: ExtensionSet) -> Result<Instance> {
|
||||||
|
Ok(WebXrInstance {
|
||||||
|
entry: self.clone(),
|
||||||
|
exts,
|
||||||
|
}
|
||||||
|
.into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct WebXrInstance {
|
||||||
|
entry: WebXrEntry,
|
||||||
|
exts: ExtensionSet,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl InstanceTrait for WebXrInstance {
|
||||||
|
fn entry(&self) -> Entry {
|
||||||
|
self.entry.clone().into()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn enabled_extensions(&self) -> ExtensionSet {
|
||||||
|
self.exts
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_session(&self, info: SessionCreateInfo) -> Result<Session> {
|
||||||
|
Ok(WebXrSession {
|
||||||
|
instance: self.clone().into(),
|
||||||
|
session: self
|
||||||
|
.entry
|
||||||
|
.0
|
||||||
|
.request_session(web_sys::XrSessionMode::ImmersiveVr)
|
||||||
|
.resolve()
|
||||||
|
.map_err(|_| XrError::Placeholder)?,
|
||||||
|
end_frame_sender: Mutex::default(),
|
||||||
|
}
|
||||||
|
.into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct WebXrSession {
|
||||||
|
instance: Instance,
|
||||||
|
session: web_sys::XrSession,
|
||||||
|
end_frame_sender: Mutex<Option<Sender<()>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SessionTrait for WebXrSession {
|
||||||
|
fn instance(&self) -> &Instance {
|
||||||
|
&self.instance
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_input(&self, bindings: Bindings) -> Result<Input> {
|
||||||
|
Ok(WebXrInput {
|
||||||
|
devices: self.session.input_sources(),
|
||||||
|
bindings,
|
||||||
|
}
|
||||||
|
.into())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn begin_frame(&self) -> Result<()> {
|
||||||
|
let mut end_frame_sender = self.end_frame_sender.lock().unwrap();
|
||||||
|
if end_frame_sender.is_some() {
|
||||||
|
Err(XrError::Placeholder)?
|
||||||
|
}
|
||||||
|
let (tx, rx) = channel::<()>();
|
||||||
|
let (tx_end, rx_end) = channel::<()>();
|
||||||
|
*end_frame_sender = Some(tx_end);
|
||||||
|
let on_frame: Closure<dyn FnMut(f64, XrFrame)> =
|
||||||
|
Closure::new(move |time: f64, frame: XrFrame| {
|
||||||
|
tx.send(()).ok();
|
||||||
|
rx_end.recv().ok();
|
||||||
|
});
|
||||||
|
|
||||||
|
self.session
|
||||||
|
.request_animation_frame(on_frame.as_ref().unchecked_ref());
|
||||||
|
|
||||||
|
rx.recv().ok();
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn end_frame(&self) -> Result<()> {
|
||||||
|
let mut end_frame_sender = self.end_frame_sender.lock().unwrap();
|
||||||
|
match std::mem::take(&mut *end_frame_sender) {
|
||||||
|
Some(sender) => sender.send(()).ok(),
|
||||||
|
None => Err(XrError::Placeholder)?,
|
||||||
|
};
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct WebXrInput {
|
||||||
|
devices: web_sys::XrInputSourceArray,
|
||||||
|
bindings: Bindings,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<web_sys::XrHandedness> for Handedness {
|
||||||
|
fn from(value: web_sys::XrHandedness) -> Self {
|
||||||
|
match value {
|
||||||
|
web_sys::XrHandedness::None => Handedness::None,
|
||||||
|
web_sys::XrHandedness::Left => Handedness::Left,
|
||||||
|
web_sys::XrHandedness::Right => Handedness::Right,
|
||||||
|
_ => todo!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WebXrInput {
|
||||||
|
fn get_controller(&self, handedness: Handedness) -> Option<web_sys::XrInputSource> {
|
||||||
|
js_sys::try_iter(&self.devices).ok()??.find_map(|dev| {
|
||||||
|
if let Ok(dev) = dev {
|
||||||
|
let dev: XrInputSource = dev.into();
|
||||||
|
if Into::<Handedness>::into(dev.handedness()) == handedness {
|
||||||
|
Some(dev)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl InputTrait for WebXrInput {
|
||||||
|
fn get_haptics(&self, path: ActionId) -> Result<Action<Haptic>> {
|
||||||
|
let haptics = self
|
||||||
|
.get_controller(path.handedness)
|
||||||
|
.ok_or(XrError::Placeholder)?
|
||||||
|
.gamepad()
|
||||||
|
.ok_or(XrError::Placeholder)?
|
||||||
|
.haptic_actuators()
|
||||||
|
.iter()
|
||||||
|
.next()
|
||||||
|
.ok_or(XrError::Placeholder)?
|
||||||
|
.into();
|
||||||
|
Ok(WebXrHaptics(haptics, path).into())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_pose(&self, path: ActionId) -> Result<Action<Pose>> {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_float(&self, path: ActionId) -> Result<Action<f32>> {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_bool(&self, path: ActionId) -> Result<Action<bool>> {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct WebXrHaptics(web_sys::GamepadHapticActuator, ActionId);
|
||||||
|
|
||||||
|
impl ActionTrait for WebXrHaptics {
|
||||||
|
fn id(&self) -> ActionId {
|
||||||
|
self.1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl HapticTrait for WebXrHaptics {}
|
||||||
17
xr_api/src/backend/webxr/utils.rs
Normal file
17
xr_api/src/backend/webxr/utils.rs
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
use wasm_bindgen::JsValue;
|
||||||
|
use wasm_bindgen_futures::JsFuture;
|
||||||
|
use web_sys::js_sys::Promise;
|
||||||
|
|
||||||
|
pub trait PromiseRes {
|
||||||
|
fn resolve<T: From<JsValue>>(self) -> Result<T, JsValue>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PromiseRes for Promise {
|
||||||
|
fn resolve<T: From<JsValue>>(self) -> Result<T, JsValue> {
|
||||||
|
resolve_promise(self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn resolve_promise<T: From<JsValue>>(promise: Promise) -> Result<T, JsValue> {
|
||||||
|
futures::executor::block_on(async move { JsFuture::from(promise).await.map(Into::into) })
|
||||||
|
}
|
||||||
16
xr_api/src/error.rs
Normal file
16
xr_api/src/error.rs
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
use std::fmt::Display;
|
||||||
|
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
pub type Result<T> = std::result::Result<T, XrError>;
|
||||||
|
|
||||||
|
#[derive(Error, Debug)]
|
||||||
|
pub enum XrError {
|
||||||
|
Placeholder,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for XrError {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
}
|
||||||
12
xr_api/src/lib.rs
Normal file
12
xr_api/src/lib.rs
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
pub mod api;
|
||||||
|
pub mod api_traits;
|
||||||
|
pub mod backend;
|
||||||
|
pub mod error;
|
||||||
|
pub mod types;
|
||||||
|
|
||||||
|
pub mod prelude {
|
||||||
|
pub use super::api::*;
|
||||||
|
pub use super::api_traits::*;
|
||||||
|
pub use super::error::*;
|
||||||
|
pub use super::types::*;
|
||||||
|
}
|
||||||
51
xr_api/src/types.rs
Normal file
51
xr_api/src/types.rs
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
use std::rc::Rc;
|
||||||
|
|
||||||
|
use crate::api_traits::{ActionInputTrait, HapticTrait};
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||||
|
pub struct ExtensionSet {}
|
||||||
|
|
||||||
|
pub enum SessionCreateInfo {}
|
||||||
|
|
||||||
|
pub struct Bindings {}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, PartialEq)]
|
||||||
|
pub struct ActionId {
|
||||||
|
pub handedness: Handedness,
|
||||||
|
pub device: Device,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, PartialEq)]
|
||||||
|
pub enum Handedness {
|
||||||
|
Left,
|
||||||
|
Right,
|
||||||
|
None,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, PartialEq)]
|
||||||
|
pub enum Device {
|
||||||
|
Controller,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Haptic;
|
||||||
|
pub struct Pose;
|
||||||
|
|
||||||
|
pub trait ActionType {
|
||||||
|
type Inner;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ActionType for Haptic {
|
||||||
|
type Inner = Rc<dyn HapticTrait>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ActionType for Pose {
|
||||||
|
type Inner = Rc<dyn ActionInputTrait<Pose>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ActionType for f32 {
|
||||||
|
type Inner = Rc<dyn ActionInputTrait<f32>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ActionType for bool {
|
||||||
|
type Inner = Rc<dyn ActionInputTrait<bool>>;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user