Skip to main content

vr_input/
vr-input.rs

1use mittens_engine::engine::ecs::component::{
2    AmbientLightComponent, AvatarControlComponent, BackgroundColorComponent, BloomComponent,
3    BlurPassComponent, Camera3DComponent, CameraXRComponent, ColorComponent, ControllerHand,
4    ControllerPoseKind, ControllerXRComponent, DirectionalLightComponent, EditorComponent,
5    EmissiveComponent, EmissivePassComponent, GLTFComponent, InputComponent,
6    InputTransformModeComponent, InputXRComponent, PointerComponent, QuatTemporalFilterComponent,
7    RaycastableComponent, RenderGraphComponent, RenderableComponent, RendererSettingsComponent,
8    RendererStatsComponent, TransformComponent, TransformForkTRSComponent,
9    TransformMapRotationComponent, TransformMapScaleComponent, TransformMapTranslationComponent,
10    XrComponent,
11};
12use mittens_engine::engine::graphics::BuiltinMeshType;
13use mittens_engine::engine::graphics::CameraTarget;
14use mittens_engine::engine::graphics::primitives::{MaterialHandle, Renderable};
15use mittens_engine::{engine, utils};
16
17#[path = "example_util/mod.rs"]
18mod example_util;
19
20#[derive(Debug, Clone, Copy)]
21struct VrInputOptions {
22    xr_controller_rotation_filter: bool,
23}
24
25impl Default for VrInputOptions {
26    fn default() -> Self {
27        Self {
28            xr_controller_rotation_filter: true,
29        }
30    }
31}
32
33fn parse_options() -> Result<VrInputOptions, String> {
34    let mut options = VrInputOptions::default();
35    let mut args = std::env::args().skip(1);
36
37    while let Some(arg) = args.next() {
38        match arg.as_str() {
39            "--xr-controller-rotation-filter" => {
40                options.xr_controller_rotation_filter = true;
41            }
42            "--no-xr-controller-rotation-filter" => {
43                options.xr_controller_rotation_filter = false;
44            }
45            "--help" | "-h" => {
46                return Err(
47                    "usage: cargo run --example vr-input -- [--xr-controller-rotation-filter | --no-xr-controller-rotation-filter]"
48                        .to_string(),
49                );
50            }
51            other => {
52                return Err(format!(
53                    "unknown arg: {other}\nusage: cargo run --example vr-input -- [--xr-controller-rotation-filter | --no-xr-controller-rotation-filter]"
54                ));
55            }
56        }
57    }
58
59    Ok(options)
60}
61
62fn spawn_sun_background(universe: &mut engine::Universe) {
63    let bg_root = universe
64        .world
65        .add_component(engine::ecs::component::BackgroundComponent::new());
66    universe.add(bg_root);
67
68    let circle_mesh = universe.render_assets.get_mesh(BuiltinMeshType::Circle2D);
69
70    // Big yellow disk.
71    let sun_t = universe.world.add_component(
72        TransformComponent::new()
73            .with_position(2.0, 1.5, -8.0)
74            .with_scale(3.5, 3.5, 3.5),
75    );
76    let sun_r = universe
77        .world
78        .add_component(RenderableComponent::new(Renderable::new(
79            circle_mesh,
80            MaterialHandle::TOON_MESH,
81        )));
82    let sun_color = universe
83        .world
84        .add_component(ColorComponent::rgba(1.0, 0.85, 0.15, 1.0));
85    let sun_emissive = universe.world.add_component(EmissiveComponent::on());
86
87    let _ = universe.attach(bg_root, sun_t);
88    let _ = universe.attach(sun_t, sun_r);
89    let _ = universe.attach(sun_r, sun_color);
90    let _ = universe.attach(sun_r, sun_emissive);
91
92    // Small white highlight disk.
93    let highlight_t = universe.world.add_component(
94        TransformComponent::new()
95            .with_position(-0.35, 0.35, -0.01)
96            .with_scale(0.45, 0.45, 0.45),
97    );
98    let highlight_r = universe
99        .world
100        .add_component(RenderableComponent::new(Renderable::new(
101            circle_mesh,
102            MaterialHandle::TOON_MESH,
103        )));
104    let highlight_color = universe
105        .world
106        .add_component(ColorComponent::rgba(1.0, 1.0, 1.0, 1.0));
107    let highlight_emissive = universe.world.add_component(EmissiveComponent::on());
108
109    let _ = universe.attach(sun_t, highlight_t);
110    let _ = universe.attach(highlight_t, highlight_r);
111    let _ = universe.attach(highlight_r, highlight_color);
112    let _ = universe.attach(highlight_r, highlight_emissive);
113}
114
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}