From 0b70c4fef4f58312ffea54bc15c492858c37d508 Mon Sep 17 00:00:00 2001 From: "Etoile (@porkyoot)" Date: Mon, 29 Jun 2026 09:27:43 +0200 Subject: [PATCH] feat: align keyboard, screens, and apps flat in front of user on tracking start --- wayvr/src/backend/openvr/mod.rs | 15 ++++++ wayvr/src/backend/openxr/mod.rs | 14 ++++++ wayvr/src/overlays/keyboard/mod.rs | 4 +- wayvr/src/state.rs | 2 + wayvr/src/windowing/mod.rs | 21 ++++++++ wayvr/src/windowing/window.rs | 78 ++++++++++++++++++++++++++++-- 6 files changed, 128 insertions(+), 6 deletions(-) diff --git a/wayvr/src/backend/openvr/mod.rs b/wayvr/src/backend/openvr/mod.rs index a44696b1..fc8341fe 100644 --- a/wayvr/src/backend/openvr/mod.rs +++ b/wayvr/src/backend/openvr/mod.rs @@ -6,6 +6,7 @@ use std::{ }; use anyhow::{Result, anyhow}; +use glam::{Affine3A, Vec3}; use ovr_overlay::{ TrackedDeviceIndex, sys::{ETrackedDeviceProperty, EVRApplicationType, EVREventType}, @@ -35,6 +36,7 @@ use crate::{ subsystem::notifications::NotificationManager, windowing::{ backend::{RenderResources, RenderTarget, ShouldRender}, + get_flat_hmd, manager::OverlayWindowManager, }, }; @@ -257,6 +259,19 @@ pub fn openvr_run( ); app.input_state.post_update(&app.session); + let hmd_is_valid = app.input_state.hmd.translation.length_squared() > 1e-6; + if !app.anchor_initialized && hmd_is_valid { + log::info!("HMD tracking has started! Initializing app.anchor flat in front of user."); + let hmd = get_flat_hmd(&app.input_state.hmd); + let anchor_local = Affine3A::from_translation(Vec3::new(0.0, 0.0, -1.0)); + app.anchor = hmd * anchor_local; + app.anchor_initialized = true; + + for o in overlays.values_mut() { + o.config.reset(&mut app, true); + } + } + if app .input_state .pointers diff --git a/wayvr/src/backend/openxr/mod.rs b/wayvr/src/backend/openxr/mod.rs index ceecfc74..993d8115 100644 --- a/wayvr/src/backend/openxr/mod.rs +++ b/wayvr/src/backend/openxr/mod.rs @@ -27,6 +27,7 @@ use crate::{ subsystem::notifications::NotificationManager, windowing::{ backend::{RenderResources, RenderTarget, ShouldRender}, + get_flat_hmd, manager::OverlayWindowManager, }, }; @@ -277,6 +278,19 @@ pub fn openxr_run( input_source.update(&xr_state, &mut app)?; app.input_state.post_update(&app.session); + let hmd_is_valid = app.input_state.hmd.translation.length_squared() > 1e-6; + if !app.anchor_initialized && hmd_is_valid { + log::info!("HMD tracking has started! Initializing app.anchor flat in front of user."); + let hmd = get_flat_hmd(&app.input_state.hmd); + let anchor_local = Affine3A::from_translation(Vec3::new(0.0, 0.0, -1.0)); + app.anchor = hmd * anchor_local; + app.anchor_initialized = true; + + for o in overlays.values_mut() { + o.config.reset(&mut app, true); + } + } + if let Some(ref mut blocker) = blocker { blocker.update(&mut app); } diff --git a/wayvr/src/overlays/keyboard/mod.rs b/wayvr/src/overlays/keyboard/mod.rs index 966f5baa..eaa0eca8 100644 --- a/wayvr/src/overlays/keyboard/mod.rs +++ b/wayvr/src/overlays/keyboard/mod.rs @@ -105,13 +105,13 @@ pub fn create_keyboard(app: &mut AppState, wayland: bool) -> anyhow::Result, pub anchor: Affine3A, pub anchor_grabbed: bool, + pub anchor_initialized: bool, pub wgui_globals: WguiGlobals, pub wgui_theme: Rc, @@ -170,6 +171,7 @@ impl AppState { screens: smallvec![], anchor: Affine3A::IDENTITY, anchor_grabbed: false, + anchor_initialized: false, wgui_globals: WguiGlobals::new( assets, &lang_provider, diff --git a/wayvr/src/windowing/mod.rs b/wayvr/src/windowing/mod.rs index 2e820cfd..33652543 100644 --- a/wayvr/src/windowing/mod.rs +++ b/wayvr/src/windowing/mod.rs @@ -45,3 +45,24 @@ pub fn snap_upright(transform: Affine3A, up_dir: Vec3A) -> Affine3A { transform } } + +pub fn get_flat_hmd(hmd: &Affine3A) -> Affine3A { + let forward = -hmd.z_axis; + let forward_h = Vec3A::new(forward.x, 0.0, forward.z); + let forward_h = if forward_h.length_squared() > 1e-6 { + forward_h.normalize() + } else { + Vec3A::new(0.0, 0.0, -1.0) + }; + + let col_z = -forward_h; + let col_y = Vec3A::Y; + let col_x = col_y.cross(col_z).normalize(); + + Affine3A::from_cols( + col_x, + col_y, + col_z, + hmd.translation, + ) +} diff --git a/wayvr/src/windowing/window.rs b/wayvr/src/windowing/window.rs index 9b48ac7f..a2031516 100644 --- a/wayvr/src/windowing/window.rs +++ b/wayvr/src/windowing/window.rs @@ -8,7 +8,7 @@ use crate::{ subsystem::input::KeyboardFocus, windowing::{ backend::{FrameMeta, OverlayBackend, RenderResources, ShouldRender}, - snap_upright, + snap_upright, get_flat_hmd, }, }; @@ -218,19 +218,33 @@ impl OverlayWindowConfig { return; }; + if self.category == OverlayCategory::Keyboard + || self.category == OverlayCategory::Screen + || self.category == OverlayCategory::WayVR + { + state.positioning = Positioning::Floating; + state.saved_transform = None; + } + let cur_transform = state .saved_transform .unwrap_or(self.default_state.transform); let (parent_transform, align_to_hmd) = match state.positioning { - Positioning::Floating | Positioning::FollowHead { .. } => (app.input_state.hmd, false), + Positioning::Floating | Positioning::FollowHead { .. } => { + if hard_reset { + (get_flat_hmd(&app.input_state.hmd), false) + } else { + (app.input_state.hmd, false) + } + } Positioning::FollowHand { hand, align_to_hmd, .. } => (app.input_state.pointers[hand as usize].pose, align_to_hmd), Positioning::Anchored => (app.anchor, false), Positioning::Static => { if hard_reset { - (app.input_state.hmd, false) + (get_flat_hmd(&app.input_state.hmd), false) } else { return; } @@ -244,7 +258,12 @@ impl OverlayWindowConfig { state.transform = parent_transform * cur_transform; if align_to_hmd || (state.grabbable && hard_reset) { - realign(&mut state.transform, &app.input_state.hmd); + let hmd = if hard_reset { + get_flat_hmd(&app.input_state.hmd) + } else { + app.input_state.hmd + }; + realign(&mut state.transform, &hmd); } self.dirty = true; } @@ -252,6 +271,14 @@ impl OverlayWindowConfig { pub fn realign(transform: &mut Affine3A, hmd: &Affine3A) { let to_hmd = hmd.translation - transform.translation; + if to_hmd.length_squared() < 1e-6 { + log::warn!("realign: to_hmd is too short, using direct HMD alignment"); + let scale = transform.x_axis.length(); + let rot = Mat3A::from_quat(Quat::from_axis_angle(Vec3::Y, PI)); + transform.matrix3 = Mat3A::from_cols(hmd.x_axis, hmd.y_axis, hmd.z_axis).mul_scalar(scale) * rot; + return; + } + let up_dir: Vec3A; if hmd.x_axis.dot(Vec3A::Y).abs() > 0.2 { @@ -300,3 +327,46 @@ pub fn save_transform(state: &mut OverlayWindowState, app: &mut AppState) { state.saved_transform = Some(parent_transform.inverse() * state.transform); } + +#[cfg(test)] +mod tests { + use super::*; + use crate::windowing::get_flat_hmd; + use glam::{Affine3A, Quat, Vec3, Vec3A}; + + #[test] + fn test_get_flat_hmd_removes_pitch_roll() { + let translation = Vec3::new(1.0, 2.0, 3.0); + let rotation = Quat::from_euler(glam::EulerRot::YXZ, 0.5, 0.3, 0.2); + let hmd = Affine3A::from_rotation_translation(rotation, translation); + + let flat_hmd = get_flat_hmd(&hmd); + + assert_eq!(flat_hmd.translation, Vec3A::from(translation)); + assert!((flat_hmd.y_axis - Vec3A::Y).length() < 1e-5); + + assert!((flat_hmd.x_axis.length() - 1.0).abs() < 1e-5); + assert!((flat_hmd.y_axis.length() - 1.0).abs() < 1e-5); + assert!((flat_hmd.z_axis.length() - 1.0).abs() < 1e-5); + assert!(flat_hmd.x_axis.dot(flat_hmd.y_axis).abs() < 1e-5); + assert!(flat_hmd.y_axis.dot(flat_hmd.z_axis).abs() < 1e-5); + assert!(flat_hmd.z_axis.dot(flat_hmd.x_axis).abs() < 1e-5); + } + + #[test] + fn test_realign_safety_guard() { + let hmd = Affine3A::from_translation(Vec3::new(0.5, 1.0, -1.0)); + let mut transform = Affine3A::from_scale_rotation_translation( + Vec3::splat(1.5), + Quat::IDENTITY, + Vec3::new(0.5, 1.0, -1.0), + ); + + realign(&mut transform, &hmd); + + assert!(transform.x_axis.x.is_finite()); + assert!(transform.y_axis.y.is_finite()); + assert!(transform.z_axis.z.is_finite()); + } +} +