pub struct ControllerXRComponent {
pub enabled: bool,
pub pose_valid: bool,
pub hand: ControllerHand,
pub pose: ControllerPoseKind,
pub component_id: Option<ComponentId>,
}Expand description
Marker/config for an XR controller tracked pose.
Semantics:
- Attach a
TransformComponentas a child of this component. - the active XR runtime will drive that transform child from controller pose tracking.
Fields§
§enabled: bool§pose_valid: boolRuntime-only: true after a valid controller pose was applied this frame.
hand: ControllerHand§pose: ControllerPoseKind§component_id: Option<ComponentId>Implementations§
Source§impl ControllerXRComponent
impl ControllerXRComponent
Sourcepub fn new(
enabled: bool,
hand: ControllerHand,
pose: ControllerPoseKind,
) -> Self
pub fn new( enabled: bool, hand: ControllerHand, pose: ControllerPoseKind, ) -> Self
Examples found in repository?
examples/vr-input.rs (lines 123-127)
115fn spawn_controller_cube(
116 universe: &mut engine::Universe,
117 xr_rig: engine::ecs::ComponentId,
118 hand: ControllerHand,
119 color: (f32, f32, f32, f32),
120 rotation_smoothing: f32,
121 use_rotation_filter_pipeline: bool,
122) -> engine::ecs::ComponentId {
123 let controller_marker = universe.world.add_component(ControllerXRComponent::new(
124 true,
125 hand,
126 ControllerPoseKind::Aim,
127 ));
128 let _ = universe.attach(xr_rig, controller_marker);
129
130 // Transform driven by OpenXRSystem (TransformComponent child of ControllerXRComponent).
131 let controller_t = universe
132 .world
133 .add_component(TransformComponent::new().with_scale(0.06, 0.06, 0.12));
134 let _ = universe.attach(controller_marker, controller_t);
135
136 if !use_rotation_filter_pipeline {
137 let cube = universe.world.add_component(RenderableComponent::cube());
138 let cube_color = universe
139 .world
140 .add_component(ColorComponent::rgba(color.0, color.1, color.2, color.3));
141
142 let _ = universe.attach(controller_t, cube);
143 let _ = universe.attach(cube, cube_color);
144
145 return controller_marker;
146 }
147
148 let fork = universe
149 .world
150 .add_component(TransformForkTRSComponent::new());
151 let _ = universe.attach(controller_t, fork);
152
153 let map_translation = universe
154 .world
155 .add_component(TransformMapTranslationComponent::new());
156 let _ = universe.attach(fork, map_translation);
157
158 let map_rotation = universe
159 .world
160 .add_component(TransformMapRotationComponent::new());
161 let _ = universe.attach(fork, map_rotation);
162 let rotation_filter = universe.world.add_component(
163 QuatTemporalFilterComponent::new().with_smoothing_factor(rotation_smoothing),
164 );
165 let _ = universe.attach(map_rotation, rotation_filter);
166
167 let map_scale = universe
168 .world
169 .add_component(TransformMapScaleComponent::new());
170 let _ = universe.attach(fork, map_scale);
171
172 let filtered_t = universe.world.add_component(TransformComponent::new());
173 let _ = universe.attach(fork, filtered_t);
174
175 let cube = universe.world.add_component(RenderableComponent::cube());
176 let cube_color = universe
177 .world
178 .add_component(ColorComponent::rgba(color.0, color.1, color.2, color.3));
179
180 let _ = universe.attach(filtered_t, cube);
181 let _ = universe.attach(cube, cube_color);
182
183 controller_marker
184}
185
186fn main() {
187 mittens_engine::example_support::ensure_model_assets();
188 utils::logger::init();
189
190 let options = match parse_options() {
191 Ok(options) => options,
192 Err(message) => {
193 eprintln!("{message}");
194 std::process::exit(2);
195 }
196 };
197
198 println!(
199 "[vr-input] xr controller rotation filter pipeline: {}",
200 if options.xr_controller_rotation_filter {
201 "enabled"
202 } else {
203 "disabled"
204 }
205 );
206
207 let world = engine::ecs::World::default();
208 let mut universe = engine::Universe::new(world);
209
210 let renderer_settings = universe
211 .world
212 .add_component(RendererSettingsComponent::msaa_off().with_window_size(320, 240));
213 universe.add(renderer_settings);
214
215 let render_graph = universe.world.add_component(RenderGraphComponent::new());
216 let emissive_pass = universe.world.add_component(EmissivePassComponent::new());
217 let blur_pass = universe.world.add_component(
218 BlurPassComponent::new()
219 .with_radius_ndc(0.06)
220 .with_half_res(true),
221 );
222 let bloom = universe.world.add_component(
223 BloomComponent::new()
224 .with_intensity(0.95)
225 .with_emissive_scale(1.2),
226 );
227 let _ = universe.attach(emissive_pass, blur_pass);
228 let _ = universe.attach(render_graph, emissive_pass);
229 let _ = universe.attach(render_graph, bloom);
230 universe.add(render_graph);
231
232 // Sky base.
233 let background = universe
234 .world
235 .add_component(BackgroundColorComponent::new());
236 let background_c = universe
237 .world
238 .add_component(ColorComponent::rgba(0.62, 0.80, 1.00, 1.0));
239 let _ = universe.world.add_child(background, background_c);
240 universe.add(background);
241
242 // Lighting for the model.
243 let ambient = universe
244 .world
245 .add_component(AmbientLightComponent::rgb(0.18, 0.18, 0.22));
246 universe.add(ambient);
247
248 let sun = universe.world.add_component(
249 DirectionalLightComponent::new()
250 .with_intensity(1.1)
251 .with_color(1.0, 0.98, 0.95),
252 );
253 let sun_dir = universe
254 .world
255 .add_component(TransformComponent::new().with_position(0.15, -0.45, 1.0));
256 let _ = universe.attach(sun_dir, sun);
257 universe.add(sun_dir);
258
259 // --- Desktop camera rig (for debugging while in VR) ---
260 let input = universe
261 .world
262 .add_component(InputComponent::new().with_speed(1.5));
263 let input_mode = universe.world.add_component(
264 InputTransformModeComponent::forward_z()
265 .with_fps_rotation()
266 .with_roll_axis_y(),
267 );
268 let _ = universe.attach(input, input_mode);
269
270 let desktop_rig = universe
271 .world
272 .add_component(TransformComponent::new().with_position(0.0, 1.2, 3.5));
273 let _ = universe.attach(input, desktop_rig);
274
275 let camera3d = universe.world.add_component(Camera3DComponent::new());
276 let _ = universe.attach(desktop_rig, camera3d);
277
278 let pointer = universe.world.add_component(PointerComponent::new());
279 let _ = universe.attach(camera3d, pointer);
280
281 example_util::spawn_desktop_camera_controls_hint(&mut universe, desktop_rig);
282 universe.add(input);
283
284 // --- XR rig (Aim controller debug cubes only; camera has moved to AVC) ---
285 let xr_input = universe.world.add_component(InputXRComponent::on());
286 let xr_gamepad = universe.world.add_component(
287 mittens_engine::engine::ecs::component::InputXRGamepadComponent::new().speed(1.5),
288 );
289 let xr_rig = universe.world.add_component(TransformComponent::new());
290 let _ = universe.attach(xr_input, xr_rig);
291 let _ = universe.attach(xr_input, xr_gamepad);
292
293 // renderer stats
294 let renderer_stats = universe
295 .world
296 .add_component(RendererStatsComponent::new().with_camera_target(CameraTarget::Xr));
297 let render_stats_rig = universe
298 .world
299 .add_component(TransformComponent::new().with_position(0.0, 1.85, 0.6));
300 let _ = universe.attach(render_stats_rig, renderer_stats);
301 let _ = universe.attach(xr_rig, render_stats_rig);
302
303 universe.add(xr_input);
304
305 // Background "skybox" content.
306 spawn_sun_background(&mut universe);
307
308 // --- VTuber model — single-input topology ---
309 //
310 // InputXRComponent drives body translation and head rotation through AvatarControlSystem.
311 // AvatarControlSystem:
312 // - Splices a TransformComponent under J_Bip_C_Head's parent (the neck) to drive
313 // head rotation directly. Rotating the head — not the neck — isolates the spine
314 // so the torso doesn't twist with HMD yaw.
315 // - Strips rotation from model_root (body faces body_yaw, not raw HMD yaw).
316 // - Bakes the π Y handedness correction into the head rotation math.
317 // - Smoothly rotates body to follow head when yaw delta exceeds threshold.
318 // - Measures J_Bip_C_Head local Y in the rest pose and sets model_root.y = -bone_local_y,
319 // so the head bone sits at driven_t world Y (= HMD height) with no hardcoded constant.
320 // - Re-parents CameraXRComponent under J_Bip_C_Head for first-person alignment.
321 //
322 // Topology (after AVC init):
323 // editor_root
324 // └── avatar_input_xr (InputXRComponent)
325 // └── avatar_driven_t (TransformComponent)
326 // └── AvatarControlComponent
327 // ├── body_pipeline → pipeline_output
328 // │ └── model_root (y auto-calibrated from J_Bip_C_Head)
329 // │ └── GLTFComponent → ... → J_Bip_C_Head
330 // │ └── CameraXRComponent
331 // ├── CTLXR(Left, Grip) → re-parented to lower_arm
332 // └── CTLXR(Right, Grip) → re-parented to lower_arm
333
334 let editor_root = universe.world.add_component(EditorComponent::new());
335
336 let avatar_input_xr = universe.world.add_component(InputXRComponent::on());
337 let avatar_xr_gamepad = universe.world.add_component(
338 mittens_engine::engine::ecs::component::InputXRGamepadComponent::new().speed(1.5),
339 );
340 let avatar_driven_t = universe.world.add_component(TransformComponent::new());
341 let _ = universe.attach(avatar_input_xr, avatar_driven_t);
342 let _ = universe.attach(avatar_input_xr, avatar_xr_gamepad);
343
344 // AvatarControlComponent: -Z forward (OpenXR default), body starts facing -Z (π yaw).
345 // camera_bone triggers auto-calibration of model_root.y from J_Bip_C_Head rest pose height,
346 // and causes any CameraXR/Camera3D direct children of AVC to be re-parented to that bone.
347 let avatar_control = universe.world.add_component(
348 AvatarControlComponent::new()
349 .with_head_bone("J_Bip_C_Head")
350 .with_camera_bone("J_Bip_C_Head")
351 .with_left_hand_bone("J_Bip_L_Hand")
352 .with_right_hand_bone("J_Bip_R_Hand")
353 .with_initial_yaw(std::f32::consts::PI)
354 .with_hand_rotation_smoothing(220.0),
355 );
356 let _ = universe.attach(avatar_driven_t, avatar_control);
357
358 // CameraXR as a direct child of AVC — discovered and re-parented to J_Bip_C_Head at init.
359 let camera_xr = universe.world.add_component(CameraXRComponent::on());
360 let _ = universe.attach(avatar_control, camera_xr);
361 let head_pointer = universe.world.add_component(PointerComponent::new());
362 let _ = universe.attach(camera_xr, head_pointer);
363
364 // Grip controllers for hand bone splicing — children of AvatarControlComponent so
365 // AvatarControlSystem discovers them by topology. Each needs a TransformComponent
366 // child (driven_t) that OpenXRSystem writes each tick.
367 let left_grip = universe.world.add_component(ControllerXRComponent::new(
368 true,
369 ControllerHand::Left,
370 ControllerPoseKind::Grip,
371 ));
372 let left_grip_t = universe.world.add_component(TransformComponent::new());
373 let _ = universe.attach(left_grip, left_grip_t);
374 let left_pointer = universe.world.add_component(PointerComponent::new());
375 let _ = universe.attach(left_grip_t, left_pointer);
376 let _ = universe.attach(avatar_control, left_grip);
377
378 let right_grip = universe.world.add_component(ControllerXRComponent::new(
379 true,
380 ControllerHand::Right,
381 ControllerPoseKind::Grip,
382 ));
383 let right_grip_t = universe.world.add_component(TransformComponent::new());
384 let _ = universe.attach(right_grip, right_grip_t);
385 let right_pointer = universe.world.add_component(PointerComponent::new());
386 let _ = universe.attach(right_grip_t, right_pointer);
387 let _ = universe.attach(avatar_control, right_grip);
388
389 // model_root: no explicit Y offset — AvatarControlSystem calibrates it from J_Bip_C_Head.
390 let model_root = universe.world.add_component(TransformComponent::new());
391 let model = universe
392 .world
393 .add_component(GLTFComponent::new("assets/models/pc-rei.hoodie.glb"));
394 let emissive = universe.world.add_component(EmissiveComponent::on());
395 let _ = universe.attach(model, emissive);
396
397 let _ = universe.attach(editor_root, avatar_input_xr);
398 let _ = universe.attach(avatar_control, model_root);
399 let _ = universe.attach(model_root, model);
400 universe.add(editor_root);
401
402 // --- Controller debug cubes (tracked poses) ---
403 let _left = spawn_controller_cube(
404 &mut universe,
405 xr_rig,
406 ControllerHand::Left,
407 (0.10, 0.90, 1.00, 1.0),
408 220.0,
409 options.xr_controller_rotation_filter,
410 );
411 let _right = spawn_controller_cube(
412 &mut universe,
413 xr_rig,
414 ControllerHand::Right,
415 (1.00, 0.35, 0.35, 1.0),
416 220.0,
417 options.xr_controller_rotation_filter,
418 );
419
420 // Enable OpenXR runtime.
421 let xr_root = universe.world.add_component(XrComponent::on());
422 universe.add(xr_root);
423
424 universe.systems.process_commands(
425 &mut universe.world,
426 &mut universe.visuals,
427 &mut universe.render_assets,
428 &mut universe.command_queue,
429 );
430
431 // Force the glTF subtree to spawn so we can query the armature for bone markers.
432 {
433 let systems = &mut universe.systems;
434 systems.gltf.tick_with_queue(
435 &mut universe.world,
436 &mut universe.visuals,
437 &mut systems.skinned_mesh,
438 &mut universe.command_queue,
439 0.0,
440 );
441 }
442 universe.systems.process_commands(
443 &mut universe.world,
444 &mut universe.visuals,
445 &mut universe.render_assets,
446 &mut universe.command_queue,
447 );
448
449 // Bone markers for editor inspection.
450 let marker_joints: &[(&str, (f32, f32, f32, f32))] = &[
451 ("[name='J_Bip_C_Head']", (0.85, 0.20, 0.85, 0.9)),
452 ("[name='J_Bip_C_Neck']", (0.20, 0.85, 0.85, 0.9)),
453 ("[name='J_Bip_C_UpperChest']", (0.20, 0.20, 0.85, 0.9)),
454 ("[name='J_Bip_L_UpperArm']", (0.85, 0.85, 0.20, 0.9)),
455 ("[name='J_Bip_R_UpperArm']", (0.85, 0.60, 0.20, 0.9)),
456 ];
457
458 for &(selector, color) in marker_joints {
459 let Some(bone) = universe.find_component(model_root, selector) else {
460 continue;
461 };
462 let marker_t = universe
463 .world
464 .add_component(TransformComponent::new().with_scale(0.025, 0.025, 0.025));
465 let marker_r = universe.world.add_component(RenderableComponent::cube());
466 let marker_c = universe
467 .world
468 .add_component(ColorComponent::rgba(color.0, color.1, color.2, color.3));
469 let marker_rcast = universe
470 .world
471 .add_component(RaycastableComponent::enabled());
472 let _ = universe.world.add_child(marker_r, marker_c);
473 let _ = universe.world.add_child(marker_r, marker_rcast);
474 let _ = universe.world.add_child(marker_t, marker_r);
475 let _ = universe.attach(bone, marker_t);
476 }
477
478 universe.enable_repl();
479 engine::Windowing::run_app(universe).expect("Windowing failed");
480}pub fn on_left_aim() -> Self
pub fn on_right_aim() -> Self
Trait Implementations§
Source§impl Clone for ControllerXRComponent
impl Clone for ControllerXRComponent
Source§fn clone(&self) -> ControllerXRComponent
fn clone(&self) -> ControllerXRComponent
Returns a duplicate of the value. Read more
1.0.0 (const: unstable) · Source§fn clone_from(&mut self, source: &Self)
fn clone_from(&mut self, source: &Self)
Performs copy-assignment from
source. Read moreSource§impl Component for ControllerXRComponent
impl Component for ControllerXRComponent
fn as_any(&self) -> &dyn Any
fn as_any_mut(&mut self) -> &mut dyn Any
Source§fn name(&self) -> &'static str
fn name(&self) -> &'static str
Short debug/type name for this component kind (e.g. “transform”, “camera”).
fn set_id(&mut self, component: ComponentId)
Source§fn init(&mut self, emit: &mut dyn SignalEmitter, component: ComponentId)
fn init(&mut self, emit: &mut dyn SignalEmitter, component: ComponentId)
Called when component is added to the World
Source§fn cleanup(&mut self, emit: &mut dyn SignalEmitter, component: ComponentId)
fn cleanup(&mut self, emit: &mut dyn SignalEmitter, component: ComponentId)
Called when component is removed from the World.
Source§fn to_mms_ast(&self, _world: &World) -> ComponentExpression
fn to_mms_ast(&self, _world: &World) -> ComponentExpression
Encode this component as an MMS Component Expression AST node. Read more
Source§impl Debug for ControllerXRComponent
impl Debug for ControllerXRComponent
Source§impl Default for ControllerXRComponent
impl Default for ControllerXRComponent
Source§fn default() -> ControllerXRComponent
fn default() -> ControllerXRComponent
Returns the “default value” for a type. Read more
Auto Trait Implementations§
impl Freeze for ControllerXRComponent
impl RefUnwindSafe for ControllerXRComponent
impl Send for ControllerXRComponent
impl Sync for ControllerXRComponent
impl Unpin for ControllerXRComponent
impl UnsafeUnpin for ControllerXRComponent
impl UnwindSafe for ControllerXRComponent
Blanket Implementations§
Source§impl<T> BorrowMut<T> for Twhere
T: ?Sized,
impl<T> BorrowMut<T> for Twhere
T: ?Sized,
Source§fn borrow_mut(&mut self) -> &mut T
fn borrow_mut(&mut self) -> &mut T
Mutably borrows from an owned value. Read more
Source§impl<T> CloneToUninit for Twhere
T: Clone,
impl<T> CloneToUninit for Twhere
T: Clone,
Source§impl<T> Downcast for Twhere
T: Any,
impl<T> Downcast for Twhere
T: Any,
Source§fn into_any(self: Box<T>) -> Box<dyn Any>
fn into_any(self: Box<T>) -> Box<dyn Any>
Convert
Box<dyn Trait> (where Trait: Downcast) to Box<dyn Any>. Box<dyn Any> can
then be further downcast into Box<ConcreteType> where ConcreteType implements Trait.Source§fn into_any_rc(self: Rc<T>) -> Rc<dyn Any>
fn into_any_rc(self: Rc<T>) -> Rc<dyn Any>
Convert
Rc<Trait> (where Trait: Downcast) to Rc<Any>. Rc<Any> can then be
further downcast into Rc<ConcreteType> where ConcreteType implements Trait.Source§fn as_any(&self) -> &(dyn Any + 'static)
fn as_any(&self) -> &(dyn Any + 'static)
Convert
&Trait (where Trait: Downcast) to &Any. This is needed since Rust cannot
generate &Any’s vtable from &Trait’s.Source§fn as_any_mut(&mut self) -> &mut (dyn Any + 'static)
fn as_any_mut(&mut self) -> &mut (dyn Any + 'static)
Convert
&mut Trait (where Trait: Downcast) to &Any. This is needed since Rust cannot
generate &mut Any’s vtable from &mut Trait’s.