diff --git a/src/backend/openxr/input.rs b/src/backend/openxr/input.rs index f5415a6..543e560 100644 --- a/src/backend/openxr/input.rs +++ b/src/backend/openxr/input.rs @@ -10,7 +10,6 @@ type XrSession = xr::Session; pub(super) struct OpenXrInputSource { action_set: xr::ActionSet, hands: [OpenXrHand; 2], - pub(super) stage: xr::Space, } pub(super) struct OpenXrHand { @@ -45,18 +44,12 @@ impl OpenXrInputSource { xr.session.attach_action_sets(&[&action_set]).unwrap(); - let stage = xr - .session - .create_reference_space(xr::ReferenceSpaceType::STAGE, xr::Posef::IDENTITY) - .unwrap(); - Self { action_set, hands: [ OpenXrHand::new(&xr, left_source), OpenXrHand::new(&xr, right_source), ], - stage, } } @@ -68,7 +61,7 @@ impl OpenXrInputSource { for i in 0..2 { self.hands[i].update( &mut state.input_state.pointers[i], - &self.stage, + &xr.stage, &xr.session, xr.predicted_display_time, ); diff --git a/src/backend/openxr/lines.rs b/src/backend/openxr/lines.rs index e69de29..73b7ec9 100644 --- a/src/backend/openxr/lines.rs +++ b/src/backend/openxr/lines.rs @@ -0,0 +1,137 @@ +use glam::{Affine3A, Vec3, Vec3A}; +use idmap::IdMap; +use openxr as xr; +use std::{ + f32::consts::PI, + sync::{ + atomic::{AtomicUsize, Ordering}, + Arc, + }, +}; + +use vulkano::{command_buffer::CommandBufferUsage, format::Format, image::view::ImageView}; + +use crate::graphics::{WlxCommandBuffer, WlxGraphics}; + +use super::{ + swapchain::{create_swapchain_render_data, SwapchainRenderData}, + transform_to_posef, XrState, +}; + +static AUTO_INCREMENT: AtomicUsize = AtomicUsize::new(1); +pub(super) const LINE_WIDTH: f32 = 0.002; + +pub(super) struct LinePool { + lines: IdMap, + colors: Vec>, +} + +impl LinePool { + pub(super) fn new(graphics: Arc) -> Self { + let mut command_buffer = graphics.create_command_buffer(CommandBufferUsage::OneTimeSubmit); + + // TODO customizable colors + let colors = [ + [0xff, 0xff, 0xff, 0xff], + [0x00, 0x60, 0x80, 0xff], + [0xB0, 0x30, 0x00, 0xff], + [0x60, 0x00, 0x80, 0xff], + ]; + + let views = colors + .into_iter() + .map(|color| { + let tex = command_buffer.texture2d(1, 1, Format::R8G8B8A8_UNORM, &color); + ImageView::new_default(tex).unwrap() + }) + .collect::>(); + + command_buffer.build_and_execute_now(); + + LinePool { + lines: IdMap::new(), + colors: views, + } + } + + pub(super) fn allocate(&mut self, xr: &XrState, graphics: Arc) -> usize { + let id = AUTO_INCREMENT.fetch_add(1, Ordering::Relaxed); + + let srd = create_swapchain_render_data(xr, graphics, [1, 1, 1]); + self.lines.insert( + id, + LineContainer { + swapchain: srd, + maybe_line: None, + }, + ); + id + } + + pub(super) fn draw_from(&mut self, id: usize, mut from: Affine3A, len: f32, color: usize) { + if len < 0.01 { + return; + } + + debug_assert!(color < self.colors.len()); + + let Some(line) = self.lines.get_mut(&id) else { + log::warn!("Line {} not found", id); + return; + }; + + let rotation = Affine3A::from_axis_angle(Vec3::X, PI * 1.5); + + from.translation = from.translation + from.transform_vector3a(Vec3A::NEG_Z) * (len * 0.5); + let transform = from * rotation; + + let posef = transform_to_posef(&transform); + + line.maybe_line = Some(Line { + view: self.colors[color].clone(), + pose: posef, + length: len, + }); + } + + pub(super) fn present_xr<'a>( + &'a mut self, + xr: &'a XrState, + command_buffer: &mut WlxCommandBuffer, + ) -> Vec> { + let mut quads = Vec::new(); + + for line in self.lines.values_mut() { + if let Some(inner) = line.maybe_line.take() { + let quad = xr::CompositionLayerQuad::new() + .pose(inner.pose) + .sub_image( + line.swapchain + .acquire_present_release(command_buffer, inner.view), + ) + .eye_visibility(xr::EyeVisibility::BOTH) + .layer_flags(xr::CompositionLayerFlags::CORRECT_CHROMATIC_ABERRATION) + .space(&xr.stage) + .size(xr::Extent2Df { + width: LINE_WIDTH, + height: inner.length, + }); + + quads.push(quad); + } + } + + quads + } +} + +pub(super) struct Line { + pub(super) view: Arc, + pub(super) pose: xr::Posef, + pub(super) length: f32, +} + +struct LineContainer { + swapchain: SwapchainRenderData, + maybe_line: Option, +} diff --git a/src/backend/openxr/mod.rs b/src/backend/openxr/mod.rs index 3d7d121..20f0509 100644 --- a/src/backend/openxr/mod.rs +++ b/src/backend/openxr/mod.rs @@ -9,11 +9,14 @@ use std::{ use anyhow::{bail, ensure}; use glam::{Affine3A, Quat, Vec3}; use openxr as xr; -use vulkano::{Handle, VulkanObject}; -use xr::{CompositionLayerFlags, EyeVisibility}; +use vulkano::{command_buffer::CommandBufferUsage, Handle, VulkanObject}; use crate::{ - backend::{common::OverlayContainer, input::interact, openxr::overlay::OpenXrOverlayData}, + backend::{ + common::OverlayContainer, + input::interact, + openxr::{lines::LinePool, overlay::OpenXrOverlayData}, + }, graphics::WlxGraphics, state::AppState, }; @@ -23,6 +26,7 @@ use super::common::BackendError; mod input; mod lines; mod overlay; +mod swapchain; const VIEW_TYPE: xr::ViewConfigurationType = xr::ViewConfigurationType::PRIMARY_STEREO; const VIEW_COUNT: u32 = 2; @@ -32,6 +36,7 @@ struct XrState { system: xr::SystemId, session: xr::Session, predicted_display_time: xr::Time, + stage: Arc, } pub fn openxr_run(running: Arc) -> Result<(), BackendError> { @@ -54,6 +59,7 @@ pub fn openxr_run(running: Arc) -> Result<(), BackendError> { }; let mut overlays = OverlayContainer::::new(&mut app_state); + let mut lines = LinePool::new(app_state.graphics.clone()); app_state.hid_provider.set_desktop_extent(overlays.extent); @@ -77,13 +83,23 @@ pub fn openxr_run(running: Arc) -> Result<(), BackendError> { .unwrap() }; + let stage = session + .create_reference_space(xr::ReferenceSpaceType::STAGE, xr::Posef::IDENTITY) + .unwrap(); + let mut xr_state = XrState { instance: xr_instance, system, session, predicted_display_time: xr::Time::from_nanos(0), + stage: Arc::new(stage), }; + let pointer_lines = [ + lines.allocate(&xr_state, app_state.graphics.clone()), + lines.allocate(&xr_state, app_state.graphics.clone()), + ]; + let input_source = input::OpenXrInputSource::new(&xr_state); let mut session_running = false; @@ -164,17 +180,26 @@ pub fn openxr_run(running: Arc) -> Result<(), BackendError> { .locate_views( VIEW_TYPE, xr_frame_state.predicted_display_time, - &input_source.stage, + &xr_state.stage, ) .unwrap(); app_state.input_state.hmd = hmd_pose_from_views(&views); - let _pointer_lengths = interact(&mut overlays, &mut app_state); - - //TODO lines + let pointer_lengths = interact(&mut overlays, &mut app_state); + for (idx, len) in pointer_lengths.iter().enumerate() { + lines.draw_from( + pointer_lines[idx], + app_state.input_state.pointers[idx].pose, + *len, + 0, + ); + } let mut layers = vec![]; + let mut command_buffer = app_state + .graphics + .create_command_buffer(CommandBufferUsage::OneTimeSubmit); for o in overlays.iter_mut() { if !o.state.want_visible { @@ -186,23 +211,18 @@ pub fn openxr_run(running: Arc) -> Result<(), BackendError> { o.data.init = true; } o.render(&mut app_state); - let transform = o.state.transform; - let Some((sub_image, extent)) = o.present_xr(&xr_state, &mut app_state) else { - continue; + if let Some(quad) = o.present_xr(&xr_state, &mut command_buffer) { + layers.push(quad); }; + } - let quad = xr::CompositionLayerQuad::new() - .pose(transform_to_posef(&transform)) - .sub_image(sub_image) - .eye_visibility(EyeVisibility::BOTH) - .layer_flags(CompositionLayerFlags::CORRECT_CHROMATIC_ABERRATION) - .space(&input_source.stage) - .size(extent); - + for quad in lines.present_xr(&xr_state, &mut command_buffer) { layers.push(quad); } + command_buffer.build_and_execute_now(); + let frame_ref = layers .iter() .map(|f| f as &xr::CompositionLayerBase) @@ -328,7 +348,7 @@ fn quat_lerp(a: Quat, mut b: Quat, t: f32) -> Quat { fn transform_to_posef(transform: &Affine3A) -> xr::Posef { let translation = transform.translation; - let rotation = Quat::from_mat3a(&transform.matrix3); + let rotation = Quat::from_affine3(transform).normalize(); xr::Posef { orientation: xr::Quaternionf { diff --git a/src/backend/openxr/overlay.rs b/src/backend/openxr/overlay.rs index 21a9901..3e7c961 100644 --- a/src/backend/openxr/overlay.rs +++ b/src/backend/openxr/overlay.rs @@ -1,29 +1,27 @@ -use std::sync::Arc; - -use super::XrState; -use crate::{backend::overlay::OverlayData, graphics::WlxPipeline, state::AppState}; -use ash::vk::{self}; use openxr as xr; -use vulkano::{ - command_buffer::CommandBufferUsage, - image::{sampler::Filter, view::ImageView, ImageCreateInfo, ImageUsage}, - render_pass::{Framebuffer, FramebufferCreateInfo}, - Handle, +use std::sync::Arc; +use xr::{CompositionLayerFlags, EyeVisibility}; + +use super::{swapchain::SwapchainRenderData, transform_to_posef, XrState}; +use crate::{ + backend::{openxr::swapchain::create_swapchain_render_data, overlay::OverlayData}, + graphics::WlxCommandBuffer, }; +use vulkano::image::view::ImageView; #[derive(Default)] pub struct OpenXrOverlayData { last_view: Option>, - inner: Option, + pub(super) swapchain: Option, pub(super) init: bool, } impl OverlayData { - pub(super) fn present_xr( - &mut self, - xr: &XrState, - state: &mut AppState, - ) -> Option<(xr::SwapchainSubImage, xr::Extent2Df)> { + pub(super) fn present_xr<'a>( + &'a mut self, + xr: &'a XrState, + command_buffer: &mut WlxCommandBuffer, + ) -> Option> { if let Some(new_view) = self.view() { self.data.last_view = Some(new_view); } @@ -35,152 +33,35 @@ impl OverlayData { return None; }; - let data = self.data.inner.get_or_insert_with(|| { + let data = self.data.swapchain.get_or_insert_with(|| { let extent = self.backend.extent(); - - let swapchain = xr - .session - .create_swapchain(&xr::SwapchainCreateInfo { - create_flags: xr::SwapchainCreateFlags::EMPTY, - usage_flags: xr::SwapchainUsageFlags::COLOR_ATTACHMENT - | xr::SwapchainUsageFlags::SAMPLED, - format: state.graphics.native_format as _, - sample_count: 1, - width: extent[0], - height: extent[1], - face_count: 1, - array_size: 1, - mip_count: 1, - }) - .unwrap(); - - let framebuffers: Vec = swapchain - .enumerate_images() - .unwrap() - .into_iter() - .map(|handle| { - let vk_image = vk::Image::from_raw(handle); - // thanks @yshui - let raw_image = unsafe { - vulkano::image::sys::RawImage::from_handle( - state.graphics.device.clone(), - vk_image, - ImageCreateInfo { - format: state.graphics.native_format, - extent, - usage: ImageUsage::COLOR_ATTACHMENT | ImageUsage::TRANSFER_DST, - ..Default::default() - }, - ) - .unwrap() - }; - // SAFETY: OpenXR guarantees that the image is a swapchain image, thus has memory backing it. - let image = Arc::new(unsafe { raw_image.assume_bound() }); - let view = ImageView::new_default(image).unwrap(); - - // HACK: maybe not create one pipeline per image? - - let shaders = state.graphics.shared_shaders.read().unwrap(); - - let pipeline = state.graphics.create_pipeline( - view.clone(), - shaders.get("vert_common").unwrap().clone(), - shaders.get("frag_srgb").unwrap().clone(), - state.graphics.native_format, - ); - - let inner = Framebuffer::new( - pipeline.render_pass.clone(), - FramebufferCreateInfo { - attachments: vec![view.clone()], - extent: [view.image().extent()[0] as _, view.image().extent()[1] as _], - layers: 1, - ..Default::default() - }, - ) - .unwrap(); - - XrFramebuffer { - inner, - view, - pipeline, - } - }) - .collect(); + let srd = create_swapchain_render_data(xr, command_buffer.graphics.clone(), extent); log::info!( "{}: Created swapchain {}x{}, {} images, {} MB", self.state.name, extent[0], extent[1], - framebuffers.len(), - extent[0] * extent[1] * 4 * framebuffers.len() as u32 / 1024 / 1024 + srd.images.len(), + extent[0] * extent[1] * 4 * srd.images.len() as u32 / 1024 / 1024 ); - - XrOverlayData { - swapchain, - framebuffers, - extent, - } + srd }); - let idx = data.swapchain.acquire_image().unwrap(); + let sub_image = data.acquire_present_release(command_buffer, my_view); + let posef = transform_to_posef(&self.state.transform); - data.swapchain.wait_image(xr::Duration::INFINITE).unwrap(); - - let frame = &data.framebuffers[idx as usize]; - let mut command_buffer = state - .graphics - .create_command_buffer(CommandBufferUsage::OneTimeSubmit) - .begin_render_pass(&frame.pipeline); - - let set = frame - .pipeline - .uniform_sampler(0, my_view.clone(), Filter::Linear); - let pass = frame.pipeline.create_pass( - [ - my_view.image().extent()[0] as _, - my_view.image().extent()[1] as _, - ], - state.graphics.quad_verts.clone(), - state.graphics.quad_indices.clone(), - vec![set], - ); - - command_buffer.run_ref(&pass); - command_buffer.end_render_pass().build_and_execute_now(); - - data.swapchain.release_image().unwrap(); - - let extent = xr::Extent2Df { - width: self.state.width, - height: (data.extent[1] as f32 / data.extent[0] as f32) * self.state.width, - }; - - Some(( - xr::SwapchainSubImage::new() - .swapchain(&data.swapchain) - .image_rect(xr::Rect2Di { - offset: xr::Offset2Di { x: 0, y: 0 }, - extent: xr::Extent2Di { - width: data.extent[0] as _, - height: data.extent[1] as _, - }, - }) - .image_array_index(0), - extent, - )) + let quad = xr::CompositionLayerQuad::new() + .pose(posef) + .sub_image(sub_image) + .eye_visibility(EyeVisibility::BOTH) + .layer_flags(CompositionLayerFlags::CORRECT_CHROMATIC_ABERRATION) + .space(&xr.stage) + .size(xr::Extent2Df { + width: self.state.width, + height: (self.backend.extent()[1] as f32 / self.backend.extent()[0] as f32) + * self.state.width, + }); + Some(quad) } } - -struct XrOverlayData { - swapchain: xr::Swapchain, - extent: [u32; 3], - framebuffers: Vec, -} - -struct XrFramebuffer { - inner: Arc, - view: Arc, - pipeline: Arc, -} diff --git a/src/backend/openxr/swapchain.rs b/src/backend/openxr/swapchain.rs new file mode 100644 index 0000000..6dae33c --- /dev/null +++ b/src/backend/openxr/swapchain.rs @@ -0,0 +1,149 @@ +use std::sync::Arc; + +use ash::vk; +use openxr as xr; + +use vulkano::{ + image::{sampler::Filter, view::ImageView, ImageCreateInfo, ImageUsage}, + render_pass::{Framebuffer, FramebufferCreateInfo}, + Handle, +}; + +use crate::graphics::{WlxCommandBuffer, WlxGraphics, WlxPipeline}; + +use super::XrState; + +pub(super) fn create_swapchain_render_data( + xr: &XrState, + graphics: Arc, + extent: [u32; 3], +) -> SwapchainRenderData { + let swapchain = xr + .session + .create_swapchain(&xr::SwapchainCreateInfo { + create_flags: xr::SwapchainCreateFlags::EMPTY, + usage_flags: xr::SwapchainUsageFlags::COLOR_ATTACHMENT + | xr::SwapchainUsageFlags::SAMPLED, + format: graphics.native_format as _, + sample_count: 1, + width: extent[0], + height: extent[1], + face_count: 1, + array_size: 1, + mip_count: 1, + }) + .unwrap(); + + let sips: Vec = swapchain + .enumerate_images() + .unwrap() + .into_iter() + .map(|handle| { + let vk_image = vk::Image::from_raw(handle); + // thanks @yshui + let raw_image = unsafe { + vulkano::image::sys::RawImage::from_handle( + graphics.device.clone(), + vk_image, + ImageCreateInfo { + format: graphics.native_format, + extent, + usage: ImageUsage::COLOR_ATTACHMENT | ImageUsage::TRANSFER_DST, + ..Default::default() + }, + ) + .unwrap() + }; + // SAFETY: OpenXR guarantees that the image is a swapchain image, thus has memory backing it. + let image = Arc::new(unsafe { raw_image.assume_bound() }); + let view = ImageView::new_default(image).unwrap(); + + // HACK: maybe not create one pipeline per image? + + let shaders = graphics.shared_shaders.read().unwrap(); + + let pipeline = graphics.create_pipeline( + view.clone(), + shaders.get("vert_common").unwrap().clone(), + shaders.get("frag_srgb").unwrap().clone(), + graphics.native_format, + ); + + let buffer = Framebuffer::new( + pipeline.render_pass.clone(), + FramebufferCreateInfo { + attachments: vec![view.clone()], + extent: [view.image().extent()[0] as _, view.image().extent()[1] as _], + layers: 1, + ..Default::default() + }, + ) + .unwrap(); + + SwapchainImagePipeline { + buffer, + view, + pipeline, + } + }) + .collect(); + + SwapchainRenderData { + swapchain, + images: sips, + extent, + } +} + +pub(super) struct SwapchainRenderData { + pub(super) swapchain: xr::Swapchain, + pub(super) extent: [u32; 3], + pub(super) images: Vec, +} + +pub(super) struct SwapchainImagePipeline { + pub(super) view: Arc, + pub(super) buffer: Arc, + pub(super) pipeline: Arc, +} + +impl SwapchainRenderData { + pub(super) fn acquire_present_release( + &mut self, + command_buffer: &mut WlxCommandBuffer, + view: Arc, + ) -> xr::SwapchainSubImage { + let idx = self.swapchain.acquire_image().unwrap() as usize; + self.swapchain.wait_image(xr::Duration::INFINITE).unwrap(); + + let image = &mut self.images[idx]; + let pipeline = image.pipeline.clone(); + command_buffer.begin_render_pass(&pipeline); + + let target_extent = image.pipeline.view.image().extent(); + let set = image + .pipeline + .uniform_sampler(0, view.clone(), Filter::Linear); + let pass = image.pipeline.create_pass( + [target_extent[0] as _, target_extent[1] as _], + command_buffer.graphics.quad_verts.clone(), + command_buffer.graphics.quad_indices.clone(), + vec![set], + ); + command_buffer.run_ref(&pass); + command_buffer.end_render_pass(); + + self.swapchain.release_image().unwrap(); + + xr::SwapchainSubImage::new() + .swapchain(&self.swapchain) + .image_rect(xr::Rect2Di { + offset: xr::Offset2Di { x: 0, y: 0 }, + extent: xr::Extent2Di { + width: target_extent[0] as _, + height: target_extent[1] as _, + }, + }) + .image_array_index(0) + } +} diff --git a/src/graphics.rs b/src/graphics.rs index 687b047..71d0423 100644 --- a/src/graphics.rs +++ b/src/graphics.rs @@ -737,12 +737,12 @@ impl WlxGraphics { } pub struct WlxCommandBuffer { - graphics: Arc, - command_buffer: RecordingCommandBuffer, + pub graphics: Arc, + pub command_buffer: RecordingCommandBuffer, } impl WlxCommandBuffer { - pub fn begin_render_pass(mut self, pipeline: &WlxPipeline) -> Self { + pub fn begin_render_pass(&mut self, pipeline: &WlxPipeline) { self.command_buffer .begin_render_pass( RenderPassBeginInfo { @@ -755,7 +755,6 @@ impl WlxCommandBuffer { }, ) .unwrap(); - self } pub fn run_ref(&mut self, pass: &WlxPass) -> &mut Self { @@ -832,11 +831,10 @@ impl WlxCommandBuffer { } impl WlxCommandBuffer { - pub fn end_render_pass(mut self) -> Self { + pub fn end_render_pass(&mut self) { self.command_buffer .end_render_pass(SubpassEndInfo::default()) .unwrap(); - self } pub fn build(self) -> Arc { diff --git a/src/gui/mod.rs b/src/gui/mod.rs index 8ae698c..b72ac78 100644 --- a/src/gui/mod.rs +++ b/src/gui/mod.rs @@ -320,28 +320,30 @@ impl Canvas { let mut cmd_buffer = self .canvas .graphics - .create_command_buffer(CommandBufferUsage::OneTimeSubmit) - .begin_render_pass(&self.canvas.pipeline_bg_color); + .create_command_buffer(CommandBufferUsage::OneTimeSubmit); + cmd_buffer.begin_render_pass(&self.canvas.pipeline_bg_color); for c in self.controls.iter_mut() { if let Some(fun) = c.on_render_bg { fun(c, &self.canvas, app, &mut cmd_buffer); } } - cmd_buffer.end_render_pass().build_and_execute_now() + cmd_buffer.end_render_pass(); + cmd_buffer.build_and_execute_now(); } fn render_fg(&mut self, app: &mut AppState) { let mut cmd_buffer = self .canvas .graphics - .create_command_buffer(CommandBufferUsage::OneTimeSubmit) - .begin_render_pass(&self.canvas.pipeline_fg_glyph); + .create_command_buffer(CommandBufferUsage::OneTimeSubmit); + cmd_buffer.begin_render_pass(&self.canvas.pipeline_fg_glyph); for c in self.controls.iter_mut() { if let Some(fun) = c.on_render_fg { fun(c, &self.canvas, app, &mut cmd_buffer); } } - cmd_buffer.end_render_pass().build_and_execute_now() + cmd_buffer.end_render_pass(); + cmd_buffer.build_and_execute_now(); } } @@ -424,8 +426,8 @@ impl OverlayRenderer for Canvas { let mut cmd_buffer = self .canvas .graphics - .create_command_buffer(CommandBufferUsage::OneTimeSubmit) - .begin_render_pass(&self.canvas.pipeline_final); + .create_command_buffer(CommandBufferUsage::OneTimeSubmit); + cmd_buffer.begin_render_pass(&self.canvas.pipeline_final); // static background cmd_buffer.run_ref(&self.pass_bg); @@ -446,7 +448,8 @@ impl OverlayRenderer for Canvas { // mostly static text cmd_buffer.run_ref(&self.pass_fg); - cmd_buffer.end_render_pass().build_and_execute_now(); + cmd_buffer.end_render_pass(); + cmd_buffer.build_and_execute_now(); /* self.canvas diff --git a/src/overlays/screen.rs b/src/overlays/screen.rs index ccfcd37..1190f0b 100644 --- a/src/overlays/screen.rs +++ b/src/overlays/screen.rs @@ -158,8 +158,8 @@ impl ScreenPipeline { let mut command_buffer = self .graphics - .create_command_buffer(CommandBufferUsage::OneTimeSubmit) - .begin_render_pass(&self.pipeline); + .create_command_buffer(CommandBufferUsage::OneTimeSubmit); + command_buffer.begin_render_pass(&self.pipeline); let set0 = self.pipeline.uniform_sampler( 0, @@ -177,9 +177,10 @@ impl ScreenPipeline { vec![set0], ); command_buffer.run_ref(&pass); + command_buffer.end_render_pass(); { - let mut exec = command_buffer.end_render_pass().build_and_execute(); + let mut exec = command_buffer.build_and_execute(); exec.flush().unwrap(); exec.cleanup_finished(); } @@ -417,7 +418,7 @@ where grabbable: true, spawn_rotation: Quat::from_axis_angle(axis, angle), spawn_point: vec3a(0., 0.5, -1.), - width: 1.5, + width: 1., interaction_transform, ..Default::default() },