pub struct AvatarControlComponent {Show 27 fields
pub head_bone: String,
pub left_hand_bone: Option<String>,
pub right_hand_bone: Option<String>,
pub left_upper_arm_bone: Option<String>,
pub left_lower_arm_bone: Option<String>,
pub right_upper_arm_bone: Option<String>,
pub right_lower_arm_bone: Option<String>,
pub left_arm_pole_direction: [f32; 3],
pub right_arm_pole_direction: [f32; 3],
pub body_yaw_threshold: f32,
pub body_yaw_rate: f32,
pub forward_plus_z: bool,
pub forward_plus_z_overridden: bool,
pub initial_body_yaw: f32,
pub initial_body_yaw_overridden: bool,
pub hand_rotation_smoothing: Option<f32>,
pub camera_bone: Option<String>,
pub avatar_height: Option<f32>,
pub hips_bone: Option<String>,
pub eye_height_from_head_bone: Option<f32>,
pub head_ik_eye_height: Option<f32>,
pub hand_grip_rotation_left: Option<[f32; 4]>,
pub hand_grip_rotation_right: Option<[f32; 4]>,
pub calibrate_hand_transforms: bool,
pub skip_body_pipeline: bool,
pub ik_debug: bool,
pub neck_bone: Option<String>,
/* private fields */
}Expand description
Coordinates all pose drivers for a humanoid avatar.
Design rule: every transform driver that moves this avatar’s bones must be a
child of (or otherwise routed through) this component. This includes the primary
body/head driver (Input / InputXR) and any hand controllers (ControllerXR).
Uncoordinated drivers that bypass this component and write directly to armature bones
are the root cause of the torso-rotation bug in the old two-input design.
Multiple drivers are fine; what matters is that they all appear in this node’s
subtree so AvatarControlSystem can discover and route them during init.
§Controller discovery
Hand controllers are discovered automatically by topology: any ControllerXRComponent
that is a direct child of this component is registered as a hand driver.
Its hand field (Left / Right) determines which hand bone it drives.
The bone is displaced under the controller’s first TransformComponent child
(the driven transform written by OpenXRSystem).
If no controller is present for a configured hand bone, a plain
TransformComponent splice is inserted instead (for IK-only or static setups).
§Topology (after init)
Input (or InputXR) ← primary driver
└── driven_t
└── AvatarControlComponent
├── model_root (TransformComponent, Y offset)
│ └── GLTFComponent
│ └── [armature]
│ J_Bip_C_Neck
│ └── splice_head ← injected by system
│ └── J_Bip_C_Head (displaced; aim-driven by driven_t)
│ left_lower_arm
│ └── ControllerXR (Left, Grip) ← moved here by system
│ └── controller_driven_t
│ └── J_Bip_L_Hand (displaced)
│ right_lower_arm
│ └── ControllerXR (Right, Grip)
│ └── controller_driven_t
│ └── J_Bip_R_Hand (displaced)
├── ControllerXR (Left, Grip) { T } ← declared here; re-parented on init
└── ControllerXR (Right, Grip) { T }Fields§
§head_bone: StringName of the bone to displace for head rotation. Default: “J_Bip_C_Head”.
This bone receives the HMD/Input world rotation directly via an AimConstraint
IK chain. Rotating the head bone (not the neck) is critical for VR/desktop:
rotating the neck twists the entire torso from the neck up, which looks wrong.
The head’s rotation is isolated from the spine so the body can yaw-follow
underneath independently.
left_hand_bone: Option<String>Name of the left hand bone to splice. None = no left hand splice.
right_hand_bone: Option<String>Name of the right hand bone to splice. None = no right hand splice.
left_upper_arm_bone: Option<String>Explicit left upper arm bone name for TwoBoneIK.
If None and left_hand_bone is set, topology derivation fills it in.
left_lower_arm_bone: Option<String>Explicit left lower arm bone name for TwoBoneIK.
If None and left_hand_bone is set, topology derivation fills it in.
right_upper_arm_bone: Option<String>Explicit right upper arm bone name for TwoBoneIK.
If None and right_hand_bone is set, topology derivation fills it in.
right_lower_arm_bone: Option<String>Explicit right lower arm bone name for TwoBoneIK.
If None and right_hand_bone is set, topology derivation fills it in.
left_arm_pole_direction: [f32; 3]Body-local pole hint for the left elbow in the 2-bone arm IK solve.
Transformed to world-space each tick by the solver using the model root
rotation, so the elbow stays anatomically correct when the body turns.
Default [-1, 0, -1] (elbow out + slightly back).
right_arm_pole_direction: [f32; 3]Body-local pole hint for the right elbow in the 2-bone arm IK solve.
Transformed to world-space each tick by the solver using the model root
rotation, so the elbow stays anatomically correct when the body turns.
Default [1, 0, -1].
body_yaw_threshold: f32Yaw delta (radians) that triggers body rotation. Default: π/4 (45°).
body_yaw_rate: f32Body rotation rate (radians/sec). Default: 3.0.
forward_plus_z: boolUse +Z as the authored forward axis override.
When not explicitly overridden, AVC keeps the shared XR-style default
(false) for both desktop and XR. This override remains available for
assets that were authored with a different convention.
forward_plus_z_overridden: boolWhether forward_plus_z was explicitly authored as an override.
initial_body_yaw: f32Initial body yaw (radians) seeded into the YawFollow pipeline op.
When not explicitly overridden, AVC uses the shared default π.
initial_body_yaw_overridden: boolWhether initial_body_yaw was explicitly authored as an override.
hand_rotation_smoothing: Option<f32>Optional rotation smoothing for hand pose drivers (ControllerXR etc.).
Applied to the rotation channel of each discovered hand driver’s pipeline.
Equivalent to QuatTemporalFilter smoothing_factor. None = no smoothing pipeline.
camera_bone: Option<String>Bone used as the camera anchor and as the source for auto-calibrating model_root.y.
When set, AvatarControlSystem will:
- Measure this bone’s local Y height above model_root in the GLTF rest pose.
- Override model_root’s Y translation to
-bone_local_y, so the bone sits exactly atdriven_t’s world position (= HMD height in XR; body origin on desktop). - Re-parent any
Camera3DComponentorCameraXRComponentdirect children of this AVC under this bone, giving them the bone’s world transform each tick.
Typically the same as head_bone (e.g. "J_Bip_C_Head") so the camera
inherits both the head’s world position (eye height) and rotation.
If None, no auto-calibration or camera re-parenting is performed.
avatar_height: Option<f32>Explicit avatar height (metres) used to set model_root.y = -avatar_height. Overrides the camera_bone auto-calibration if both are set. Use this when the camera bone lookup fails or the mesh height is known in advance.
hips_bone: Option<String>Name of the hips bone — the FABRIK spine chain root. Default: "J_Bip_C_Hips".
Resolved against model_root once during try_init_splices. If None or
not found, no spine FABRIK chain is wired — head bone falls back to FK
(visible detachment from neck under pitch).
eye_height_from_head_bone: Option<f32>Vertical distance (metres) from the head bone pivot to the eyes.
VRM J_Bip_C_Head pivot sits at the skull base; the eye line is typically
~0.08 m above that. When this is set, AVC shifts model_root.y down by
this amount so the EYES (not the bone pivot) land at driven_t’s world Y
— i.e. at HMD height in VR, or at the desktop input height.
Without this, the avatar’s eyes sit above the HMD eye position and the face/hair mesh swings into the XR camera frustum when pitching down.
Applies on top of either camera_bone auto-calibration or
avatar_height override. Default: None (no adjustment).
head_ik_eye_height: Option<f32>Vertical offset (metres) used exclusively for the head IK target calculation.
This is decoupled from the camera position transform (T { CXR } wrapper)
so the camera can be positioned freely without affecting how the FABRIK solver
bends the spine. Typically set to a small value like 0.04–0.08 to account for
the gap between the head bone pivot and the eye position, causing the spine to
bend so the head lands at the right height relative to the HMD.
When set, the FABRIK target_position_offset uses this value (Y-only) instead of
reading the camera transform’s translation. If None, no offset is applied to
the IK target (the head bone pivot chases the HMD position directly).
Default: None.
hand_grip_rotation_left: Option<[f32; 4]>Local-space rotation offset applied to the left hand IK target (after grip pose).
Use this to correct for the gap between how the runtime reports grip orientation
and what the armature expects for the hand bone. For standard VRM rigs with
Vive-family controllers, quat_rotation_y(PI/2) (90° CW from above) is typical.
None = no offset (raw grip pose drives the hand bone directly).
hand_grip_rotation_right: Option<[f32; 4]>Local-space rotation offset applied to the right hand IK target (after grip pose).
For standard VRM rigs with Vive-family controllers, quat_rotation_y(-PI/2)
(90° CCW from above) is typical.
None = no offset.
calibrate_hand_transforms: boolEnable interactive capture of live hand grip offsets.
When enabled, pressing Enter captures the current controller-to-hand
rotation offset for the first initialized AVC in the world and prints
MMS-ready hand_grip_rotation_left/right([...]) lines to the console.
skip_body_pipeline: boolDebug/diagnostic flag: skip creation of the body-rotation pipeline entirely.
When true, model_root stays directly under AVC and only head rotation is applied.
Use this to isolate whether torso-twist bugs originate in the body pipeline.
ik_debug: boolDebug/diagnostic flag: when enabled, arm TwoBoneIK chains spawn overlay visualizations for the actual target vector, transformed pole vector, bend-plane normal, and solved elbow direction used by the solver.
neck_bone: Option<String>Name of the neck bone used by the Phase 2 rest-pin. When set and
the bone is found under model_root, the body-follow system records
its rest local translation at init and restores it each tick if any
other system perturbs it. Default: "J_Bip_C_Neck".
Implementations§
Source§impl AvatarControlComponent
impl AvatarControlComponent
Sourcepub fn new() -> Self
pub fn new() -> Self
Examples found in repository?
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}Sourcepub fn with_head_bone(self, name: impl Into<String>) -> Self
pub fn with_head_bone(self, name: impl Into<String>) -> Self
Examples found in repository?
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}Sourcepub fn with_left_hand_bone(self, name: impl Into<String>) -> Self
pub fn with_left_hand_bone(self, name: impl Into<String>) -> Self
Examples found in repository?
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}Sourcepub fn with_right_hand_bone(self, name: impl Into<String>) -> Self
pub fn with_right_hand_bone(self, name: impl Into<String>) -> Self
Examples found in repository?
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 with_left_upper_arm_bone(self, name: impl Into<String>) -> Self
pub fn with_left_lower_arm_bone(self, name: impl Into<String>) -> Self
pub fn with_right_upper_arm_bone(self, name: impl Into<String>) -> Self
pub fn with_right_lower_arm_bone(self, name: impl Into<String>) -> Self
Sourcepub fn with_left_arm_pole_direction(self, dir: [f32; 3]) -> Self
pub fn with_left_arm_pole_direction(self, dir: [f32; 3]) -> Self
Override the left elbow pole direction (body-local).
Sourcepub fn with_right_arm_pole_direction(self, dir: [f32; 3]) -> Self
pub fn with_right_arm_pole_direction(self, dir: [f32; 3]) -> Self
Override the right elbow pole direction (body-local).
pub fn with_body_yaw_threshold(self, t: f32) -> Self
pub fn with_body_yaw_rate(self, r: f32) -> Self
Sourcepub fn with_initial_yaw(self, yaw: f32) -> Self
pub fn with_initial_yaw(self, yaw: f32) -> Self
Override the initial body yaw (radians) seeded into the YawFollow pipeline op.
Use std::f32::consts::PI for rigs that face -Z at rest.
Examples found in repository?
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}Sourcepub fn with_forward_plus_z(self) -> Self
pub fn with_forward_plus_z(self) -> Self
Use +Z as the authored forward axis override.
Sourcepub fn with_hand_rotation_smoothing(self, factor: f32) -> Self
pub fn with_hand_rotation_smoothing(self, factor: f32) -> Self
Enable rotation smoothing for hand pose drivers.
Set to e.g. 220.0 for smooth VR controller rotation.
Examples found in repository?
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}Sourcepub fn with_body_pipeline_disabled(self) -> Self
pub fn with_body_pipeline_disabled(self) -> Self
Skip creation of the body-rotation pipeline. Only head rotation will be applied. Use to isolate whether torso-twist bugs originate in the body pipeline.
Sourcepub fn with_ik_debug(self) -> Self
pub fn with_ik_debug(self) -> Self
Enable TwoBoneIK debug visualizations for chains owned by this AVC.
Sourcepub fn with_camera_bone(self, name: impl Into<String>) -> Self
pub fn with_camera_bone(self, name: impl Into<String>) -> Self
Set the bone used as the camera anchor and for auto-calibrating model_root.y.
AvatarControlSystem will measure this bone’s local Y in the rest pose and set
model_root.y = -bone_local_y so the bone sits at driven_t’s world position.
Any Camera3DComponent or CameraXRComponent direct children of this AVC are
re-parented under this bone during init.
Examples found in repository?
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}Sourcepub fn with_avatar_height(self, height: f32) -> Self
pub fn with_avatar_height(self, height: f32) -> Self
Explicitly set model_root.y = -height during init, bypassing camera_bone
auto-calibration. Use when the bone lookup is unreliable or the mesh height
is known in advance. Camera re-parenting still uses camera_bone if set.
Sourcepub fn with_eye_height_from_head_bone(self, dy: f32) -> Self
pub fn with_eye_height_from_head_bone(self, dy: f32) -> Self
Shift model_root.y down so the avatar’s EYES (not the head bone pivot)
land at driven_t’s world Y. Default eye offset for VRM is ~0.08.
Sourcepub fn with_hips_bone(self, name: impl Into<String>) -> Self
pub fn with_hips_bone(self, name: impl Into<String>) -> Self
Set the hips bone name — root of the spine FABRIK chain.
Default (when unset): "J_Bip_C_Hips".
Sourcepub fn with_neck_bone(self, name: impl Into<String>) -> Self
pub fn with_neck_bone(self, name: impl Into<String>) -> Self
Override the neck bone name used by the Phase 2 rest-pin. Pass None
to disable the pin entirely.
Sourcepub fn without_neck_pin(self) -> Self
pub fn without_neck_pin(self) -> Self
Disable the neck rest-pin.
Sourcepub fn with_head_ik_eye_height(self, dy: f32) -> Self
pub fn with_head_ik_eye_height(self, dy: f32) -> Self
Set the vertical offset for the head IK target calculation (metres).
Decoupled from the camera position so spine bending and camera positioning
can be controlled independently. Default: None.
Sourcepub fn with_hand_grip_rotation_left(self, q: [f32; 4]) -> Self
pub fn with_hand_grip_rotation_left(self, q: [f32; 4]) -> Self
Set a rotation offset applied to the left hand grip IK target.
Quaternion in [x, y, z, w] order.
Sourcepub fn with_hand_grip_rotation_right(self, q: [f32; 4]) -> Self
pub fn with_hand_grip_rotation_right(self, q: [f32; 4]) -> Self
Set a rotation offset applied to the right hand grip IK target.
pub fn with_calibrate_hand_transforms(self) -> Self
Trait Implementations§
Source§impl Clone for AvatarControlComponent
impl Clone for AvatarControlComponent
Source§fn clone(&self) -> AvatarControlComponent
fn clone(&self) -> AvatarControlComponent
1.0.0 (const: unstable) · Source§fn clone_from(&mut self, source: &Self)
fn clone_from(&mut self, source: &Self)
source. Read moreSource§impl Component for AvatarControlComponent
impl Component for AvatarControlComponent
Source§fn name(&self) -> &'static str
fn name(&self) -> &'static str
fn set_id(&mut self, id: ComponentId)
Source§fn init(&mut self, emit: &mut dyn SignalEmitter, component: ComponentId)
fn init(&mut self, emit: &mut dyn SignalEmitter, component: ComponentId)
fn as_any(&self) -> &dyn Any
fn as_any_mut(&mut self) -> &mut dyn Any
Source§fn to_mms_ast(&self, _world: &World) -> ComponentExpression
fn to_mms_ast(&self, _world: &World) -> ComponentExpression
Source§fn cleanup(&mut self, _emit: &mut dyn SignalEmitter, _component: ComponentId)
fn cleanup(&mut self, _emit: &mut dyn SignalEmitter, _component: ComponentId)
Source§impl Debug for AvatarControlComponent
impl Debug for AvatarControlComponent
Auto Trait Implementations§
impl Freeze for AvatarControlComponent
impl RefUnwindSafe for AvatarControlComponent
impl Send for AvatarControlComponent
impl Sync for AvatarControlComponent
impl Unpin for AvatarControlComponent
impl UnsafeUnpin for AvatarControlComponent
impl UnwindSafe for AvatarControlComponent
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
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>
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>
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)
&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)
&mut Trait (where Trait: Downcast) to &Any. This is needed since Rust cannot
generate &mut Any’s vtable from &mut Trait’s.