Skip to main content

ControllerXRComponent

Struct ControllerXRComponent 

Source
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 TransformComponent as a child of this component.
  • the active XR runtime will drive that transform child from controller pose tracking.

Fields§

§enabled: bool§pose_valid: bool

Runtime-only: true after a valid controller pose was applied this frame.

§hand: ControllerHand§pose: ControllerPoseKind§component_id: Option<ComponentId>

Implementations§

Source§

impl ControllerXRComponent

Source

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}
Source

pub fn on_left_aim() -> Self

Source

pub fn on_right_aim() -> Self

Trait Implementations§

Source§

impl Clone for ControllerXRComponent

Source§

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)

Performs copy-assignment from source. Read more
Source§

impl Component for ControllerXRComponent

Source§

fn as_any(&self) -> &dyn Any

Source§

fn as_any_mut(&mut self) -> &mut dyn Any

Source§

fn name(&self) -> &'static str

Short debug/type name for this component kind (e.g. “transform”, “camera”).
Source§

fn set_id(&mut self, component: ComponentId)

Source§

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)

Called when component is removed from the World.
Source§

fn to_mms_ast(&self, _world: &World) -> ComponentExpression

Encode this component as an MMS Component Expression AST node. Read more
Source§

fn encode(&self) -> HashMap<String, Value>

Encode component data to a HashMap for serialization. Read more
Source§

fn decode(&mut self, _data: &HashMap<String, Value>) -> Result<(), String>

Decode component data from a HashMap after deserialization. Read more
Source§

impl Debug for ControllerXRComponent

Source§

fn fmt(&self, f: &mut Formatter<'_>) -> Result

Formats the value using the given formatter. Read more
Source§

impl Default for ControllerXRComponent

Source§

fn default() -> ControllerXRComponent

Returns the “default value” for a type. Read more

Auto Trait Implementations§

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> CloneToUninit for T
where T: Clone,

Source§

unsafe fn clone_to_uninit(&self, dest: *mut u8)

🔬This is a nightly-only experimental API. (clone_to_uninit)
Performs copy-assignment from self to dest. Read more
Source§

impl<T> Downcast for T
where T: Any,

Source§

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>

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)

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)

Convert &mut Trait (where Trait: Downcast) to &Any. This is needed since Rust cannot generate &mut Any’s vtable from &mut Trait’s.
Source§

impl<T> DowncastSync for T
where T: Any + Send + Sync,

Source§

fn into_any_arc(self: Arc<T>) -> Arc<dyn Any + Sync + Send>

Convert Arc<Trait> (where Trait: Downcast) to Arc<Any>. Arc<Any> can then be further downcast into Arc<ConcreteType> where ConcreteType implements Trait.
Source§

impl<S, T> Duplex<S> for T
where T: FromSample<S> + ToSample<S>,

Source§

impl<T> DynClone for T
where T: Clone,

Source§

fn __clone_box(&self, _: Private) -> *mut ()

Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<S> FromSample<S> for S

Source§

fn from_sample_(s: S) -> S

Source§

impl<T> Instrument for T

Source§

fn instrument(self, span: Span) -> Instrumented<Self>

Instruments this type with the provided Span, returning an Instrumented wrapper. Read more
Source§

fn in_current_span(self) -> Instrumented<Self>

Instruments this type with the current Span, returning an Instrumented wrapper. Read more
Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<F, T> IntoSample<T> for F
where T: FromSample<F>,

Source§

fn into_sample(self) -> T

Source§

impl<T> PolicyExt for T
where T: ?Sized,

Source§

fn and<P, B, E>(self, other: P) -> And<T, P>
where T: Sized + Policy<B, E>, P: Policy<B, E>,

Create a new Policy that returns Action::Follow only if self and other return Action::Follow. Read more
Source§

fn or<P, B, E>(self, other: P) -> Or<T, P>
where T: Sized + Policy<B, E>, P: Policy<B, E>,

Create a new Policy that returns Action::Follow if either self or other returns Action::Follow. Read more
Source§

impl<T> Same for T

Source§

type Output = T

Should always be Self
Source§

impl<T> ToOwned for T
where T: Clone,

Source§

type Owned = T

The resulting type after obtaining ownership.
Source§

fn to_owned(&self) -> T

Creates owned data from borrowed data, usually by cloning. Read more
Source§

fn clone_into(&self, target: &mut T)

Uses borrowed data to replace owned data, usually by cloning. Read more
Source§

impl<T, U> ToSample<U> for T
where U: FromSample<T>,

Source§

fn to_sample_(self) -> U

Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = Infallible

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.
Source§

impl<V, T> VZip<V> for T
where V: MultiLane<T>,

Source§

fn vzip(self) -> V

Source§

impl<T> WithSubscriber for T

Source§

fn with_subscriber<S>(self, subscriber: S) -> WithDispatch<Self>
where S: Into<Dispatch>,

Attaches the provided Subscriber to this type, returning a WithDispatch wrapper. Read more
Source§

fn with_current_subscriber(self) -> WithDispatch<Self>

Attaches the current default Subscriber to this type, returning a WithDispatch wrapper. Read more