pub struct DirectionalLightComponent {
pub intensity: f32,
pub color: [f32; 3],
/* private fields */
}Expand description
Directional light (infinite distance light).
The renderer interprets this light’s world position as a direction vector. In other words: set the node’s translation to the direction you want (it will be normalized on the GPU).
Fields§
§intensity: f32§color: [f32; 3]Linear RGB color in 0..1.
Implementations§
Source§impl DirectionalLightComponent
impl DirectionalLightComponent
Sourcepub fn new() -> Self
pub fn new() -> Self
Examples found in repository?
examples/pointer-events.rs (line 383)
349fn main() {
350 mittens_engine::example_support::ensure_model_assets();
351 utils::logger::init();
352
353 let world = engine::ecs::World::default();
354 let mut universe = engine::Universe::new(world);
355
356 use engine::ecs::component::{
357 AmbientLightComponent, BackgroundColorComponent, Camera3DComponent, ColorComponent,
358 DirectionalLightComponent, InputComponent, InputTransformModeComponent, PointerComponent,
359 TransformComponent,
360 };
361
362 // Background.
363 let bg = universe
364 .world
365 .add_component(BackgroundColorComponent::new());
366 let bg_c = universe
367 .world
368 .add_component(ColorComponent::rgba(0.12, 0.12, 0.16, 1.0));
369 let _ = universe.world.add_child(bg, bg_c);
370 universe.add(bg);
371
372 // Lighting.
373 let ambient = universe
374 .world
375 .add_component(AmbientLightComponent::rgb(0.30, 0.30, 0.32));
376 universe.add(ambient);
377
378 let sun_t = universe
379 .world
380 .add_component(TransformComponent::new().with_position(2.0, 4.0, 3.0));
381 let sun = universe
382 .world
383 .add_component(DirectionalLightComponent::new());
384 let _ = universe.attach(sun_t, sun);
385 universe.add(sun_t);
386
387 // Camera + pointer rig.
388 let input = universe
389 .world
390 .add_component(InputComponent::new().with_speed(3.0));
391 let input_mode = universe.world.add_component(
392 InputTransformModeComponent::forward_z()
393 .with_fps_rotation()
394 .with_roll_axis_y(),
395 );
396 let _ = universe.attach(input, input_mode);
397
398 let cam_t = universe
399 .world
400 .add_component(TransformComponent::new().with_position(0.0, 0.5, 4.5));
401 let cam = universe
402 .world
403 .add_component(Camera3DComponent::new().with_fov(70.0));
404 let pointer = universe.world.add_component(PointerComponent::new());
405
406 let _ = universe.attach(input, cam_t);
407 let _ = universe.attach(cam_t, cam);
408 let _ = universe.attach(cam, pointer);
409
410 example_util::spawn_desktop_camera_controls_hint(&mut universe, cam_t);
411 universe.add(input);
412
413 // Scene root for all interactive objects.
414 let scene = universe.world.add_component(TransformComponent::new());
415 universe.add(scene);
416
417 // --- LEFT: click-only cubes ---
418 let click_group = universe
419 .world
420 .add_component(TransformComponent::new().with_position(-2.2, 0.0, 0.0));
421 let _ = universe.attach(scene, click_group);
422
423 for (i, dy) in [-0.55_f32, 0.0, 0.55].iter().enumerate() {
424 spawn_click_cube(&mut universe, click_group, [0.0, *dy, 0.0]);
425 let _ = i;
426 }
427 spawn_label(
428 &mut universe,
429 click_group,
430 [-0.25, -1.05, 0.0],
431 "click\ncycles color",
432 );
433
434 // --- MID: drag-only cube ---
435 let drag_cube_root = universe
436 .world
437 .add_component(TransformComponent::new().with_position(0.0, 0.0, 0.0));
438 let _ = universe.attach(scene, drag_cube_root);
439
440 spawn_drag_cube(
441 &mut universe,
442 drag_cube_root,
443 [0.0, 0.0, 0.0],
444 [0.35, 0.85, 0.55, 1.0],
445 );
446 spawn_label(
447 &mut universe,
448 drag_cube_root,
449 [-0.25, -0.80, 0.0],
450 "drag\nto move",
451 );
452
453 // --- RIGHT: drag + click cube ---
454 let both_root = universe
455 .world
456 .add_component(TransformComponent::new().with_position(2.2, 0.0, 0.0));
457 let _ = universe.attach(scene, both_root);
458
459 spawn_both_cube(&mut universe, both_root, [0.0, 0.0, 0.0]);
460 spawn_label(
461 &mut universe,
462 both_root,
463 [-0.30, -0.80, 0.0],
464 "drag to move\nclick for color",
465 );
466
467 universe.systems.process_commands(
468 &mut universe.world,
469 &mut universe.visuals,
470 &mut universe.render_assets,
471 &mut universe.command_queue,
472 );
473
474 universe.enable_repl();
475 engine::Windowing::run_app(universe).expect("Windowing failed");
476}More examples
examples/button-press.rs (line 474)
439fn main() {
440 mittens_engine::example_support::ensure_model_assets();
441 utils::logger::init();
442
443 let world = engine::ecs::World::default();
444 let mut universe = engine::Universe::new(world);
445
446 // Minimal lit scene + pointer raycaster.
447 use engine::ecs::component::{
448 AmbientLightComponent, BackgroundColorComponent, Camera3DComponent,
449 DirectionalLightComponent, InputComponent, InputTransformModeComponent, PointerComponent,
450 TransformComponent,
451 };
452
453 let bg = universe
454 .world
455 .add_component(BackgroundColorComponent::new());
456 let bg_c = universe
457 .world
458 .add_component(engine::ecs::component::ColorComponent::rgba(
459 0.92, 0.92, 0.96, 1.0,
460 ));
461 let _ = universe.world.add_child(bg, bg_c);
462 universe.add(bg);
463
464 let ambient = universe
465 .world
466 .add_component(AmbientLightComponent::rgb(0.35, 0.35, 0.35));
467 universe.add(ambient);
468
469 let sun_t = universe
470 .world
471 .add_component(TransformComponent::new().with_position(1.0, 1.0, 1.0));
472 let sun = universe
473 .world
474 .add_component(DirectionalLightComponent::new());
475 let _ = universe.attach(sun_t, sun);
476 universe.add(sun_t);
477
478 let input = universe
479 .world
480 .add_component(InputComponent::new().with_speed(2.5));
481 let input_mode = universe.world.add_component(
482 InputTransformModeComponent::forward_z()
483 .with_fps_rotation()
484 .with_roll_axis_y(),
485 );
486 let _ = universe.attach(input, input_mode);
487
488 let rig_t = universe
489 .world
490 .add_component(TransformComponent::new().with_position(0.0, 0.0, 2.5));
491 let _ = universe.attach(input, rig_t);
492
493 let cam = universe
494 .world
495 .add_component(Camera3DComponent::new().with_far(600.0).with_fov(70.0));
496 let _ = universe.attach(rig_t, cam);
497
498 let pointer = universe.world.add_component(PointerComponent::new());
499 let _ = universe.attach(cam, pointer);
500
501 example_util::spawn_desktop_camera_controls_hint(&mut universe, rig_t);
502 universe.add(input);
503
504 // The button.
505 let button_root = spawn_button(
506 &mut universe,
507 [0.0, 0.0, 0.0],
508 [1.20, 0.45],
509 ButtonBorder::new(0.025, 0.025, 0.025, 0.025),
510 "click me",
511 0.18,
512 [0.15, 0.15, 0.20, 1.0],
513 [0.85, 0.85, 0.92, 1.0],
514 );
515
516 // A small 3-cube stack to the left of the button for gizmo testing:
517 // [cyan]
518 // [yellow][light brown]
519 //
520 // These live under an EditorComponent subtree so clicking attaches the editor gizmo.
521 {
522 use engine::ecs::component::{EditorComponent, TransformComponent};
523
524 let editor_root = universe.world.add_component(EditorComponent::new());
525
526 // Position the stack in world space (left of the button).
527 let stack_root = universe
528 .world
529 .add_component(TransformComponent::new().with_position(-1.55, 0.0, 0.0));
530 let _ = universe.attach(editor_root, stack_root);
531
532 let cube = 0.22_f32;
533 let gap = 0.04_f32;
534 let step = cube + gap;
535 let y_bottom = -0.5 * step;
536 let y_top = 0.5 * step;
537 let x_left = -0.5 * step;
538 let x_right = 0.5 * step;
539
540 let yellow = [1.0, 0.92, 0.22, 1.0];
541 let light_brown = [0.80, 0.66, 0.46, 1.0];
542 let cyan = [0.20, 0.95, 1.0, 1.0];
543
544 let _bottom_left = spawn_raycastable_colored_cube(
545 &mut universe,
546 stack_root,
547 [x_left, y_bottom, 0.0],
548 [cube, cube, 0.06],
549 yellow,
550 );
551 let _bottom_right = spawn_raycastable_colored_cube(
552 &mut universe,
553 stack_root,
554 [x_right, y_bottom, 0.0],
555 [cube, cube, 0.06],
556 light_brown,
557 );
558 let _top = spawn_raycastable_colored_cube(
559 &mut universe,
560 stack_root,
561 [0.0, y_top, 0.0],
562 [cube, cube, 0.06],
563 cyan,
564 );
565
566 universe.add(editor_root);
567 }
568
569 universe.add_signal_handler(
570 engine::ecs::SignalKind::DragStart,
571 button_root,
572 button_press_handler,
573 );
574 universe.add_signal_handler(
575 engine::ecs::SignalKind::DragEnd,
576 button_root,
577 button_press_handler,
578 );
579
580 universe.systems.process_commands(
581 &mut universe.world,
582 &mut universe.visuals,
583 &mut universe.render_assets,
584 &mut universe.command_queue,
585 );
586
587 universe.enable_repl();
588 engine::Windowing::run_app(universe).expect("Windowing failed");
589}examples/vtuber-example.rs (line 63)
11fn main() {
12 mittens_engine::example_support::ensure_model_assets();
13 utils::logger::init();
14
15 let world = engine::ecs::World::default();
16 let mut universe = engine::Universe::new(world);
17
18 // Light pink background.
19 let background = universe
20 .world
21 .add_component(BackgroundColorComponent::new());
22 let background_c = universe
23 .world
24 .add_component(ColorComponent::rgba(1.0, 0.82, 0.90, 1.0));
25 let _ = universe.world.add_child(background, background_c);
26 universe.add(background);
27
28 // Small ambient so shadowed areas aren't pure black.
29 let ambient = universe
30 .world
31 .add_component(AmbientLightComponent::rgb(0.10, 0.10, 0.12));
32 universe.add(ambient);
33
34 // --- Camera rig (WASD + mouse) ---
35 // InputComponent is the root, and it owns a Transform (the camera rig).
36 let input = universe
37 .world
38 .add_component(InputComponent::new().with_speed(1.5));
39 let input_mode = universe.world.add_component(
40 InputTransformModeComponent::forward_z()
41 .with_fps_rotation()
42 .with_roll_axis_y(),
43 );
44 let _ = universe.attach(input, input_mode);
45
46 // Start slightly pulled back looking towards the origin.
47 let rig_transform = universe
48 .world
49 .add_component(TransformComponent::new().with_position(0.0, 0.0, 6.0));
50 let _ = universe.attach(input, rig_transform);
51
52 let camera3d = universe.world.add_component(Camera3DComponent::new());
53 let _ = universe.attach(rig_transform, camera3d);
54
55 // Topology: I { T { C3D } } — add a small camera-attached controls hint.
56 example_util::spawn_desktop_camera_controls_hint(&mut universe, rig_transform);
57
58 universe.add(input);
59
60 // --- lighting ---
61 // Directional key light (slightly down + forward Z).
62 let sun = universe.world.add_component(
63 DirectionalLightComponent::new()
64 .with_intensity(1.2)
65 .with_color(1.0, 0.98, 0.94),
66 );
67 let sun_dir = universe
68 .world
69 .add_component(TransformComponent::new().with_position(0.0, -0.35, 1.0));
70 let _ = universe.attach(sun_dir, sun);
71 universe.add(sun_dir);
72
73 let light_transform = universe.world.add_component(
74 TransformComponent::new()
75 .with_position(1.0, 6.0, 3.0)
76 .with_scale(0.1, 0.1, 0.1),
77 );
78 let light = universe.world.add_component(
79 engine::ecs::component::PointLightComponent::new()
80 .with_distance(120.0)
81 .with_color(1.0, 1.0, 1.0),
82 );
83 let _ = universe.attach(light_transform, light);
84 universe.add(light_transform);
85
86 // --- VTuber model ---
87 let model_root = universe.world.add_component(TransformComponent::new());
88 let model = universe
89 .world
90 .add_component(GLTFComponent::new("assets/models/pc-rei.hoodie.glb"));
91 // emissive for pc-rei
92 let emissive = universe
93 .world
94 .add_component(engine::ecs::component::EmissiveComponent::on());
95
96 let _ = universe.attach(model, emissive);
97
98 let xr_input = universe.world.add_component(InputXRComponent::on());
99 let xr_gamepad = universe
100 .world
101 .add_component(engine::ecs::component::InputXRGamepadComponent::new().speed(1.5));
102 let xr_head = universe.world.add_component(TransformComponent::new());
103 let xr_camera = universe.world.add_component(CameraXRComponent::on());
104 let _ = universe.attach(xr_input, xr_head);
105 let _ = universe.attach(xr_input, xr_gamepad);
106 let _ = universe.attach(xr_head, xr_camera);
107 let _ = universe.attach(xr_head, model_root);
108
109 let _ = universe.attach(model_root, model);
110 universe.add(xr_input);
111 universe.add(model_root);
112
113 // --- Background clouds (occluded + lit) ---
114 let bg_root = universe.world.add_component(
115 engine::ecs::component::BackgroundComponent::new().with_occlusion_and_lighting(),
116 );
117 universe.add(bg_root);
118 let mut cloud_params = example_util::CloudRingParams::default();
119 cloud_params.cloud_count = 8; // +3 clusters
120 cloud_params.angle_jitter = 0.35;
121 cloud_params.high_y_probability = 0.5;
122 cloud_params.high_y_multiplier = 1.5;
123 cloud_params.seed = 0x57_55_B0_01u32;
124 example_util::spawn_cloud_ring(&mut universe, bg_root, cloud_params);
125
126 // --- Simple environment ---
127 let spawn_cube = |universe: &mut engine::Universe,
128 position: (f32, f32, f32),
129 scale: (f32, f32, f32),
130 color: (f32, f32, f32, f32)| {
131 let transform = universe.world.add_component(
132 TransformComponent::new()
133 .with_position(position.0, position.1, position.2)
134 .with_scale(scale.0, scale.1, scale.2),
135 );
136 let renderable = universe.world.add_component(RenderableComponent::cube());
137 let color = universe
138 .world
139 .add_component(ColorComponent::rgba(color.0, color.1, color.2, color.3));
140
141 let _ = universe.attach(transform, renderable);
142 let _ = universe.attach(renderable, color);
143
144 universe.add(transform);
145 };
146
147 // floor
148 spawn_cube(
149 &mut universe,
150 (0.0, -0.05, 0.0),
151 (10.0, 0.1, 10.0),
152 (0.92, 0.92, 0.92, 1.0),
153 );
154
155 // back wall
156 spawn_cube(
157 &mut universe,
158 (0.0, 1.5, -5.0),
159 (10.0, 3.0, 1.0),
160 (0.95, 0.94, 0.96, 1.0),
161 );
162
163 // desk
164 spawn_cube(
165 &mut universe,
166 (0.0, 0.35, 1.0),
167 (1.0, 0.75, 0.5),
168 (0.75, 0.70, 0.65, 1.0),
169 );
170
171 let xr_root = universe
172 .world
173 .add_component(engine::ecs::component::XrComponent::on());
174 universe.add(xr_root);
175
176 universe.systems.process_commands(
177 &mut universe.world,
178 &mut universe.visuals,
179 &mut universe.render_assets,
180 &mut universe.command_queue,
181 );
182
183 universe.enable_repl();
184 engine::Windowing::run_app(universe).expect("Windowing failed");
185}examples/gestures-and-gizmos.rs (line 81)
13fn build_gestures_and_gizmos_scene(universe: &mut engine::Universe) -> Scene {
14 use engine::ecs::component::{
15 BackgroundColorComponent, BackgroundComponent, Camera3DComponent, ColorComponent,
16 DirectionalLightComponent, InputComponent, InputTransformModeComponent, PointerComponent,
17 RaycastableComponent, RenderableComponent, TransformComponent, TransformGizmoComponent,
18 };
19 use engine::graphics::BuiltinMeshType;
20 use engine::graphics::primitives::{MaterialHandle, Renderable};
21
22 let tri_mesh = universe.render_assets.get_mesh(BuiltinMeshType::Triangle2D);
23 let cube_mesh = universe.render_assets.get_mesh(BuiltinMeshType::Cube);
24 let tetra_mesh = universe
25 .render_assets
26 .get_mesh(BuiltinMeshType::Tetrahedron);
27
28 // BackgroundColor { C.rgba }
29 let bg_color = universe
30 .world
31 .add_component(BackgroundColorComponent::new());
32 let bg_color_c = universe
33 .world
34 .add_component(ColorComponent::rgba(0.90, 0.90, 0.90, 1.0));
35 let _ = universe.world.add_child(bg_color, bg_color_c);
36 universe.add(bg_color);
37
38 // ambient light
39 let ambient = universe
40 .world
41 .add_component(AmbientLightComponent::rgb(0.25, 0.25, 0.25));
42 universe.add(ambient);
43
44 // ground plane
45 let ground_tx = universe.world.add_component(
46 TransformComponent::new()
47 .with_position(0.0, -2.5, 0.0)
48 .with_scale(20.0, 1.0, 20.0),
49 );
50 let ground_r = universe
51 .world
52 .add_component(RenderableComponent::new(Renderable::new(
53 universe.render_assets.get_mesh(BuiltinMeshType::Cube),
54 MaterialHandle::TOON_MESH,
55 )));
56 let ground_c = universe
57 .world
58 .add_component(ColorComponent::rgba(0.75, 0.75, 0.75, 1.0));
59 let _ = universe.attach(ground_tx, ground_r);
60 let _ = universe.attach(ground_r, ground_c);
61 let _ = universe.add(ground_tx);
62
63 // Background {
64 // with_occlusion_and_lighting()
65 // // using the example utils to add clouds to the background
66 // }
67 let bg_root = universe
68 .world
69 .add_component(BackgroundComponent::new().with_occlusion_and_lighting());
70 universe.add(bg_root);
71
72 // DirectionalLight {
73 // T { translate [1, 1, 1] }
74 // }
75 // Directional lights encode their direction in the node's world position.
76 let sun_t = universe
77 .world
78 .add_component(TransformComponent::new().with_position(1.0, 1.0, 1.0));
79 let sun = universe
80 .world
81 .add_component(DirectionalLightComponent::new());
82 let _ = universe.attach(sun_t, sun);
83 universe.add(sun_t);
84
85 // i = input
86 // t = transform
87 // c3d = camera3d
88 //
89 // I {
90 // T {
91 // C3D { with_fps_rotation().with_roll_axis_y() }
92 // }
93 // }
94 let input = universe
95 .world
96 .add_component(InputComponent::new().with_speed(2.5));
97 let input_mode = universe.world.add_component(
98 InputTransformModeComponent::forward_z()
99 .with_fps_rotation()
100 .with_roll_axis_y(),
101 );
102
103 let _ = universe.attach(input, input_mode);
104
105 // Forward is -Z, so put the camera at +Z looking toward the origin.
106 let rig_t = universe
107 .world
108 .add_component(TransformComponent::new().with_position(0.0, 0.0, 3.5));
109 let _ = universe.attach(input, rig_t);
110
111 let cam = universe
112 .world
113 .add_component(Camera3DComponent::new().with_far(600.0).with_fov(70.0));
114 let _ = universe.attach(rig_t, cam);
115
116 // Opt-in: treat this camera rig as a pointer source.
117 let pointer = universe.world.add_component(PointerComponent::new());
118 let _ = universe.attach(cam, pointer);
119
120 // Topology: I { T { C3D } } — add a small camera-attached controls hint.
121 example_util::spawn_desktop_camera_controls_hint(universe, rig_t);
122
123 fn spawn_shape_with_gizmo(
124 universe: &mut engine::Universe,
125 mesh: engine::graphics::primitives::CpuMeshHandle,
126 pos: [f32; 3],
127 scale: [f32; 3],
128 rot_euler: [f32; 3],
129 color: [f32; 4],
130 ) {
131 let t = universe.world.add_component(
132 TransformComponent::new()
133 .with_position(pos[0], pos[1], pos[2])
134 .with_scale(scale[0], scale[1], scale[2])
135 .with_rotation_euler(rot_euler[0], rot_euler[1], rot_euler[2]),
136 );
137 let r = universe
138 .world
139 .add_component(RenderableComponent::new(Renderable::new(
140 mesh,
141 MaterialHandle::TOON_MESH,
142 )));
143 let c = universe
144 .world
145 .add_component(ColorComponent::rgba(color[0], color[1], color[2], color[3]));
146 let rc = universe
147 .world
148 .add_component(RaycastableComponent::enabled());
149 let g = universe.world.add_component(TransformGizmoComponent::new());
150
151 let _ = universe.attach(t, r);
152 let _ = universe.attach(r, c);
153 let _ = universe.attach(r, rc);
154 let _ = universe.attach(t, g);
155
156 universe.add(t);
157 }
158
159 fn spawn_shape_raycastable_no_gizmo(
160 universe: &mut engine::Universe,
161 mesh: engine::graphics::primitives::CpuMeshHandle,
162 pos: [f32; 3],
163 scale: [f32; 3],
164 rot_euler: [f32; 3],
165 color: [f32; 4],
166 ) {
167 let t = universe.world.add_component(
168 TransformComponent::new()
169 .with_position(pos[0], pos[1], pos[2])
170 .with_scale(scale[0], scale[1], scale[2])
171 .with_rotation_euler(rot_euler[0], rot_euler[1], rot_euler[2]),
172 );
173 let r = universe
174 .world
175 .add_component(RenderableComponent::new(Renderable::new(
176 mesh,
177 MaterialHandle::TOON_MESH,
178 )));
179 let c = universe
180 .world
181 .add_component(ColorComponent::rgba(color[0], color[1], color[2], color[3]));
182 let rc = universe
183 .world
184 .add_component(RaycastableComponent::enabled());
185
186 let _ = universe.attach(t, r);
187 let _ = universe.attach(r, c);
188 let _ = universe.attach(r, rc);
189
190 universe.add(t);
191 }
192
193 spawn_shape_with_gizmo(
194 universe,
195 tri_mesh,
196 [-1.2, 0.0, 0.0],
197 [0.65, 0.65, 0.65],
198 [0.0, 0.0, 0.0],
199 [0.2, 0.9, 0.25, 1.0],
200 );
201 spawn_shape_with_gizmo(
202 universe,
203 cube_mesh,
204 [0.0, 0.0, 0.0],
205 [0.55, 0.55, 0.55],
206 [0.0, 0.0, 0.0],
207 [0.95, 0.25, 0.2, 1.0],
208 );
209 spawn_shape_with_gizmo(
210 universe,
211 tetra_mesh,
212 [1.2, 0.0, 0.0],
213 [0.7, 0.7, 0.7],
214 [0.0, 0.0, 0.0],
215 [0.2, 0.55, 1.0, 1.0],
216 );
217
218 // Standalone tetrahedron: no gizmo, but explicitly raycastable.
219 // This helps isolate tetra picking vs gizmo-handle interception.
220 spawn_shape_raycastable_no_gizmo(
221 universe,
222 tetra_mesh,
223 [2.6, -0.6, 0.0],
224 [0.95, 0.95, 0.95],
225 [0.0, 0.0, 0.0],
226 [0.85, 0.85, 1.0, 1.0],
227 );
228
229 universe.add(input);
230
231 Scene { bg_root }
232}examples/background-occlusion-example.rs (line 75)
19fn main() {
20 mittens_engine::example_support::ensure_model_assets();
21 utils::logger::init();
22
23 let world = engine::ecs::World::default();
24 let mut universe = engine::Universe::new(world);
25
26 // Light purple-red background so the occlusion reads.
27 let clear = universe
28 .world
29 .add_component(engine::ecs::component::BackgroundColorComponent::new());
30 let clear_c = universe
31 .world
32 .add_component(engine::ecs::component::ColorComponent::rgba(
33 0.62, 0.38, 0.56, 1.0,
34 ));
35 let _ = universe.world.add_child(clear, clear_c);
36 universe.add(clear);
37
38 // A bit of ambient so the cluster volume reads.
39 let ambient = universe
40 .world
41 .add_component(engine::ecs::component::AmbientLightComponent::rgb(
42 0.20, 0.15, 0.3,
43 ));
44 universe.add(ambient);
45
46 // --- Camera rig (WASD/QE) ---
47 let input = universe
48 .world
49 .add_component(engine::ecs::component::InputComponent::new().with_speed(2.0));
50 let input_mode = universe.world.add_component(
51 engine::ecs::component::InputTransformModeComponent::forward_z().with_roll_axis_y(),
52 );
53 let _ = universe.attach(input, input_mode);
54
55 let rig_transform = universe.world.add_component(
56 engine::ecs::component::TransformComponent::new().with_position(0.0, 0.0, 6.0),
57 );
58 let _ = universe.attach(input, rig_transform);
59
60 let camera3d = universe.world.add_component(
61 engine::ecs::component::Camera3DComponent::new()
62 .with_far(200.0)
63 .with_fov(70.0),
64 );
65 let _ = universe.attach(rig_transform, camera3d);
66
67 // Topology: I { T { C3D } } — add a small camera-attached controls hint.
68 example_util::spawn_desktop_camera_controls_hint(&mut universe, rig_transform);
69
70 // Key directional light toward the cloud volume.
71 //
72 // Directional lights encode their direction in the node's world position.
73 // The shader normalizes this vector.
74 let sun = universe.world.add_component(
75 engine::ecs::component::DirectionalLightComponent::new()
76 .with_intensity(1.6)
77 .with_color(1.0, 0.98, 0.92),
78 );
79 // Pointing from the clouds back toward the camera a bit (+Z), and slightly from above.
80 let sun_dir = universe.world.add_component(
81 engine::ecs::component::TransformComponent::new().with_position(0.15, 0.65, 0.75),
82 );
83 let _ = universe.attach(sun_dir, sun);
84
85 // A couple point lights to fill/shade the cloud volume.
86 let light_a = universe.world.add_component(
87 engine::ecs::component::PointLightComponent::new()
88 .with_distance(80.0)
89 .with_intensity(3.0)
90 .with_color(0.9, 0.95, 1.0),
91 );
92 let light_a_tx = universe.world.add_component(
93 engine::ecs::component::TransformComponent::new().with_position(8.0, 8.0, 5.0),
94 );
95 let _ = universe.attach(light_a_tx, light_a);
96
97 let light_b = universe.world.add_component(
98 engine::ecs::component::PointLightComponent::new()
99 .with_distance(80.0)
100 .with_intensity(2.2)
101 .with_color(1.0, 0.9, 0.85),
102 );
103 let light_b_tx = universe.world.add_component(
104 engine::ecs::component::TransformComponent::new().with_position(-10.0, 4.0, -6.0),
105 );
106 let _ = universe.attach(light_b_tx, light_b);
107
108 universe.add(input);
109 universe.add(sun_dir);
110 universe.add(light_a_tx);
111 universe.add(light_b_tx);
112
113 let cube_mesh = universe
114 .render_assets
115 .get_mesh(engine::graphics::BuiltinMeshType::Cube);
116
117 // --- Background occluded+lit world ---
118 // Renderables under this node participate in a background stage that depth-writes for
119 // self-occlusion + uses the normal lighting shader inputs.
120 let bg_root = universe.world.add_component(
121 engine::ecs::component::BackgroundComponent::new().with_occlusion_and_lighting(),
122 );
123 universe.add(bg_root);
124
125 // Create overlapping cube clusters like cloud puffs.
126 // Place them generally in front of the camera (negative Z).
127 // Spawn two groups on opposite X sides.
128 let cluster_count: u32 = 9;
129 for (group_i, group_x) in [-16.0_f32, 16.0_f32].into_iter().enumerate() {
130 let group_tx = universe.world.add_component(
131 engine::ecs::component::TransformComponent::new().with_position(group_x, 0.0, 0.0),
132 );
133 let _ = universe.attach(bg_root, group_tx);
134
135 for cluster_i in 0..cluster_count {
136 let seed = (0xC10u32 ^ (group_i as u32).wrapping_mul(0x9E37_79B9))
137 ^ (cluster_i.wrapping_mul(7919));
138
139 let cx = (rand01(seed ^ 0xA341_316C) - 0.5) * 18.0;
140 let cy = (rand01(seed ^ 0xC801_3EA4) - 0.5) * 10.0 + 2.0;
141 let cz = -18.0 - rand01(seed ^ 0xB529_7A4D) * 26.0;
142
143 let center_tx = universe.world.add_component(
144 engine::ecs::component::TransformComponent::new().with_position(cx, cy, cz),
145 );
146 let _ = universe.attach(group_tx, center_tx);
147
148 let puffs = 18u32;
149 for puff_i in 0..puffs {
150 let puff_seed = seed ^ puff_i.wrapping_mul(1_103_515_245);
151
152 // Slightly ellipsoidal distribution.
153 let ox = (rand01(puff_seed ^ 0x68bc_21eb) - 0.5) * 7.0;
154 let oy = (rand01(puff_seed ^ 0x02e5_be93) - 0.5) * 3.0;
155 let oz = (rand01(puff_seed ^ 0xa1d3_4f2b) - 0.5) * 7.0;
156
157 let base = 0.7 + rand01(puff_seed ^ 0x9e37_79b9) * 2.6;
158 let sx = base * (0.7 + rand01(puff_seed ^ 0x243f_6a88) * 0.8);
159 let sy = base * (0.6 + rand01(puff_seed ^ 0x85a3_08d3) * 0.9);
160 let sz = base * (0.7 + rand01(puff_seed ^ 0x1319_8a2e) * 0.8);
161
162 let tx = universe.world.add_component(
163 engine::ecs::component::TransformComponent::new()
164 .with_position(ox, oy, oz)
165 .with_scale(sx, sy, sz),
166 );
167 let renderable =
168 universe
169 .world
170 .add_component(engine::ecs::component::RenderableComponent::new(
171 engine::graphics::primitives::Renderable::new(
172 cube_mesh,
173 engine::graphics::primitives::MaterialHandle::TOON_MESH,
174 ),
175 ));
176
177 // Slight blue-grey variation.
178 let t = rand01(puff_seed ^ 0x7f4a_7c15);
179 let r = 0.55 + 0.10 * t;
180 let g = 0.58 + 0.10 * t;
181 let b = 0.66 + 0.12 * t;
182 let color = universe
183 .world
184 .add_component(engine::ecs::component::ColorComponent::rgba(r, g, b, 1.0));
185
186 let _ = universe.attach(center_tx, tx);
187 let _ = universe.attach(tx, renderable);
188 let _ = universe.attach(renderable, color);
189 }
190 }
191 }
192
193 // Foreground reference cube (should never be occluded by background depth).
194 let fg_tx = universe.world.add_component(
195 engine::ecs::component::TransformComponent::new()
196 .with_position(0.0, -0.5, -4.0)
197 .with_scale(1.2, 0.8, 1.2),
198 );
199 let fg_renderable =
200 universe
201 .world
202 .add_component(engine::ecs::component::RenderableComponent::new(
203 engine::graphics::primitives::Renderable::new(
204 cube_mesh,
205 engine::graphics::primitives::MaterialHandle::TOON_MESH,
206 ),
207 ));
208 let fg_color = universe
209 .world
210 .add_component(engine::ecs::component::ColorComponent::rgba(
211 0.9, 0.2, 0.2, 1.0,
212 ));
213
214 universe.add(fg_tx);
215 let _ = universe.attach(fg_tx, fg_renderable);
216 let _ = universe.attach(fg_renderable, fg_color);
217
218 // Foreground opaque floor: 16x16 larger white cubes, 10 units below the reference cube.
219 let floor_root = universe.world.add_component(
220 engine::ecs::component::TransformComponent::new().with_position(0.0, -10.5, -10.0),
221 );
222 universe.add(floor_root);
223
224 let spacing = 10_f32;
225 let half = 5_f32;
226 for x in 0..16u32 {
227 for z in 0..64u32 {
228 let px = (x as f32 - half) * spacing;
229 let pz = (z as f32 * -1.0 + half) * spacing;
230 let tx = universe.world.add_component(
231 engine::ecs::component::TransformComponent::new()
232 .with_position(px, -5.0, pz)
233 .with_scale(5.0, 0.5, 5.0),
234 );
235 let renderable =
236 universe
237 .world
238 .add_component(engine::ecs::component::RenderableComponent::new(
239 engine::graphics::primitives::Renderable::new(
240 cube_mesh,
241 engine::graphics::primitives::MaterialHandle::TOON_MESH,
242 ),
243 ));
244 let color = universe
245 .world
246 .add_component(engine::ecs::component::ColorComponent::rgba(
247 1.0, 1.0, 1.0, 1.0,
248 ));
249
250 let _ = universe.attach(floor_root, tx);
251 let _ = universe.attach(tx, renderable);
252 let _ = universe.attach(renderable, color);
253 }
254 }
255
256 universe.systems.process_commands(
257 &mut universe.world,
258 &mut universe.visuals,
259 &mut universe.render_assets,
260 &mut universe.command_queue,
261 );
262
263 universe.enable_repl();
264 engine::Windowing::run_app(universe).expect("Windowing failed");
265}examples/vr-input.rs (line 249)
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}Additional examples can be found in:
Sourcepub fn with_intensity(self, intensity: f32) -> Self
pub fn with_intensity(self, intensity: f32) -> Self
Examples found in repository?
examples/vtuber-example.rs (line 64)
11fn main() {
12 mittens_engine::example_support::ensure_model_assets();
13 utils::logger::init();
14
15 let world = engine::ecs::World::default();
16 let mut universe = engine::Universe::new(world);
17
18 // Light pink background.
19 let background = universe
20 .world
21 .add_component(BackgroundColorComponent::new());
22 let background_c = universe
23 .world
24 .add_component(ColorComponent::rgba(1.0, 0.82, 0.90, 1.0));
25 let _ = universe.world.add_child(background, background_c);
26 universe.add(background);
27
28 // Small ambient so shadowed areas aren't pure black.
29 let ambient = universe
30 .world
31 .add_component(AmbientLightComponent::rgb(0.10, 0.10, 0.12));
32 universe.add(ambient);
33
34 // --- Camera rig (WASD + mouse) ---
35 // InputComponent is the root, and it owns a Transform (the camera rig).
36 let input = universe
37 .world
38 .add_component(InputComponent::new().with_speed(1.5));
39 let input_mode = universe.world.add_component(
40 InputTransformModeComponent::forward_z()
41 .with_fps_rotation()
42 .with_roll_axis_y(),
43 );
44 let _ = universe.attach(input, input_mode);
45
46 // Start slightly pulled back looking towards the origin.
47 let rig_transform = universe
48 .world
49 .add_component(TransformComponent::new().with_position(0.0, 0.0, 6.0));
50 let _ = universe.attach(input, rig_transform);
51
52 let camera3d = universe.world.add_component(Camera3DComponent::new());
53 let _ = universe.attach(rig_transform, camera3d);
54
55 // Topology: I { T { C3D } } — add a small camera-attached controls hint.
56 example_util::spawn_desktop_camera_controls_hint(&mut universe, rig_transform);
57
58 universe.add(input);
59
60 // --- lighting ---
61 // Directional key light (slightly down + forward Z).
62 let sun = universe.world.add_component(
63 DirectionalLightComponent::new()
64 .with_intensity(1.2)
65 .with_color(1.0, 0.98, 0.94),
66 );
67 let sun_dir = universe
68 .world
69 .add_component(TransformComponent::new().with_position(0.0, -0.35, 1.0));
70 let _ = universe.attach(sun_dir, sun);
71 universe.add(sun_dir);
72
73 let light_transform = universe.world.add_component(
74 TransformComponent::new()
75 .with_position(1.0, 6.0, 3.0)
76 .with_scale(0.1, 0.1, 0.1),
77 );
78 let light = universe.world.add_component(
79 engine::ecs::component::PointLightComponent::new()
80 .with_distance(120.0)
81 .with_color(1.0, 1.0, 1.0),
82 );
83 let _ = universe.attach(light_transform, light);
84 universe.add(light_transform);
85
86 // --- VTuber model ---
87 let model_root = universe.world.add_component(TransformComponent::new());
88 let model = universe
89 .world
90 .add_component(GLTFComponent::new("assets/models/pc-rei.hoodie.glb"));
91 // emissive for pc-rei
92 let emissive = universe
93 .world
94 .add_component(engine::ecs::component::EmissiveComponent::on());
95
96 let _ = universe.attach(model, emissive);
97
98 let xr_input = universe.world.add_component(InputXRComponent::on());
99 let xr_gamepad = universe
100 .world
101 .add_component(engine::ecs::component::InputXRGamepadComponent::new().speed(1.5));
102 let xr_head = universe.world.add_component(TransformComponent::new());
103 let xr_camera = universe.world.add_component(CameraXRComponent::on());
104 let _ = universe.attach(xr_input, xr_head);
105 let _ = universe.attach(xr_input, xr_gamepad);
106 let _ = universe.attach(xr_head, xr_camera);
107 let _ = universe.attach(xr_head, model_root);
108
109 let _ = universe.attach(model_root, model);
110 universe.add(xr_input);
111 universe.add(model_root);
112
113 // --- Background clouds (occluded + lit) ---
114 let bg_root = universe.world.add_component(
115 engine::ecs::component::BackgroundComponent::new().with_occlusion_and_lighting(),
116 );
117 universe.add(bg_root);
118 let mut cloud_params = example_util::CloudRingParams::default();
119 cloud_params.cloud_count = 8; // +3 clusters
120 cloud_params.angle_jitter = 0.35;
121 cloud_params.high_y_probability = 0.5;
122 cloud_params.high_y_multiplier = 1.5;
123 cloud_params.seed = 0x57_55_B0_01u32;
124 example_util::spawn_cloud_ring(&mut universe, bg_root, cloud_params);
125
126 // --- Simple environment ---
127 let spawn_cube = |universe: &mut engine::Universe,
128 position: (f32, f32, f32),
129 scale: (f32, f32, f32),
130 color: (f32, f32, f32, f32)| {
131 let transform = universe.world.add_component(
132 TransformComponent::new()
133 .with_position(position.0, position.1, position.2)
134 .with_scale(scale.0, scale.1, scale.2),
135 );
136 let renderable = universe.world.add_component(RenderableComponent::cube());
137 let color = universe
138 .world
139 .add_component(ColorComponent::rgba(color.0, color.1, color.2, color.3));
140
141 let _ = universe.attach(transform, renderable);
142 let _ = universe.attach(renderable, color);
143
144 universe.add(transform);
145 };
146
147 // floor
148 spawn_cube(
149 &mut universe,
150 (0.0, -0.05, 0.0),
151 (10.0, 0.1, 10.0),
152 (0.92, 0.92, 0.92, 1.0),
153 );
154
155 // back wall
156 spawn_cube(
157 &mut universe,
158 (0.0, 1.5, -5.0),
159 (10.0, 3.0, 1.0),
160 (0.95, 0.94, 0.96, 1.0),
161 );
162
163 // desk
164 spawn_cube(
165 &mut universe,
166 (0.0, 0.35, 1.0),
167 (1.0, 0.75, 0.5),
168 (0.75, 0.70, 0.65, 1.0),
169 );
170
171 let xr_root = universe
172 .world
173 .add_component(engine::ecs::component::XrComponent::on());
174 universe.add(xr_root);
175
176 universe.systems.process_commands(
177 &mut universe.world,
178 &mut universe.visuals,
179 &mut universe.render_assets,
180 &mut universe.command_queue,
181 );
182
183 universe.enable_repl();
184 engine::Windowing::run_app(universe).expect("Windowing failed");
185}More examples
examples/background-occlusion-example.rs (line 76)
19fn main() {
20 mittens_engine::example_support::ensure_model_assets();
21 utils::logger::init();
22
23 let world = engine::ecs::World::default();
24 let mut universe = engine::Universe::new(world);
25
26 // Light purple-red background so the occlusion reads.
27 let clear = universe
28 .world
29 .add_component(engine::ecs::component::BackgroundColorComponent::new());
30 let clear_c = universe
31 .world
32 .add_component(engine::ecs::component::ColorComponent::rgba(
33 0.62, 0.38, 0.56, 1.0,
34 ));
35 let _ = universe.world.add_child(clear, clear_c);
36 universe.add(clear);
37
38 // A bit of ambient so the cluster volume reads.
39 let ambient = universe
40 .world
41 .add_component(engine::ecs::component::AmbientLightComponent::rgb(
42 0.20, 0.15, 0.3,
43 ));
44 universe.add(ambient);
45
46 // --- Camera rig (WASD/QE) ---
47 let input = universe
48 .world
49 .add_component(engine::ecs::component::InputComponent::new().with_speed(2.0));
50 let input_mode = universe.world.add_component(
51 engine::ecs::component::InputTransformModeComponent::forward_z().with_roll_axis_y(),
52 );
53 let _ = universe.attach(input, input_mode);
54
55 let rig_transform = universe.world.add_component(
56 engine::ecs::component::TransformComponent::new().with_position(0.0, 0.0, 6.0),
57 );
58 let _ = universe.attach(input, rig_transform);
59
60 let camera3d = universe.world.add_component(
61 engine::ecs::component::Camera3DComponent::new()
62 .with_far(200.0)
63 .with_fov(70.0),
64 );
65 let _ = universe.attach(rig_transform, camera3d);
66
67 // Topology: I { T { C3D } } — add a small camera-attached controls hint.
68 example_util::spawn_desktop_camera_controls_hint(&mut universe, rig_transform);
69
70 // Key directional light toward the cloud volume.
71 //
72 // Directional lights encode their direction in the node's world position.
73 // The shader normalizes this vector.
74 let sun = universe.world.add_component(
75 engine::ecs::component::DirectionalLightComponent::new()
76 .with_intensity(1.6)
77 .with_color(1.0, 0.98, 0.92),
78 );
79 // Pointing from the clouds back toward the camera a bit (+Z), and slightly from above.
80 let sun_dir = universe.world.add_component(
81 engine::ecs::component::TransformComponent::new().with_position(0.15, 0.65, 0.75),
82 );
83 let _ = universe.attach(sun_dir, sun);
84
85 // A couple point lights to fill/shade the cloud volume.
86 let light_a = universe.world.add_component(
87 engine::ecs::component::PointLightComponent::new()
88 .with_distance(80.0)
89 .with_intensity(3.0)
90 .with_color(0.9, 0.95, 1.0),
91 );
92 let light_a_tx = universe.world.add_component(
93 engine::ecs::component::TransformComponent::new().with_position(8.0, 8.0, 5.0),
94 );
95 let _ = universe.attach(light_a_tx, light_a);
96
97 let light_b = universe.world.add_component(
98 engine::ecs::component::PointLightComponent::new()
99 .with_distance(80.0)
100 .with_intensity(2.2)
101 .with_color(1.0, 0.9, 0.85),
102 );
103 let light_b_tx = universe.world.add_component(
104 engine::ecs::component::TransformComponent::new().with_position(-10.0, 4.0, -6.0),
105 );
106 let _ = universe.attach(light_b_tx, light_b);
107
108 universe.add(input);
109 universe.add(sun_dir);
110 universe.add(light_a_tx);
111 universe.add(light_b_tx);
112
113 let cube_mesh = universe
114 .render_assets
115 .get_mesh(engine::graphics::BuiltinMeshType::Cube);
116
117 // --- Background occluded+lit world ---
118 // Renderables under this node participate in a background stage that depth-writes for
119 // self-occlusion + uses the normal lighting shader inputs.
120 let bg_root = universe.world.add_component(
121 engine::ecs::component::BackgroundComponent::new().with_occlusion_and_lighting(),
122 );
123 universe.add(bg_root);
124
125 // Create overlapping cube clusters like cloud puffs.
126 // Place them generally in front of the camera (negative Z).
127 // Spawn two groups on opposite X sides.
128 let cluster_count: u32 = 9;
129 for (group_i, group_x) in [-16.0_f32, 16.0_f32].into_iter().enumerate() {
130 let group_tx = universe.world.add_component(
131 engine::ecs::component::TransformComponent::new().with_position(group_x, 0.0, 0.0),
132 );
133 let _ = universe.attach(bg_root, group_tx);
134
135 for cluster_i in 0..cluster_count {
136 let seed = (0xC10u32 ^ (group_i as u32).wrapping_mul(0x9E37_79B9))
137 ^ (cluster_i.wrapping_mul(7919));
138
139 let cx = (rand01(seed ^ 0xA341_316C) - 0.5) * 18.0;
140 let cy = (rand01(seed ^ 0xC801_3EA4) - 0.5) * 10.0 + 2.0;
141 let cz = -18.0 - rand01(seed ^ 0xB529_7A4D) * 26.0;
142
143 let center_tx = universe.world.add_component(
144 engine::ecs::component::TransformComponent::new().with_position(cx, cy, cz),
145 );
146 let _ = universe.attach(group_tx, center_tx);
147
148 let puffs = 18u32;
149 for puff_i in 0..puffs {
150 let puff_seed = seed ^ puff_i.wrapping_mul(1_103_515_245);
151
152 // Slightly ellipsoidal distribution.
153 let ox = (rand01(puff_seed ^ 0x68bc_21eb) - 0.5) * 7.0;
154 let oy = (rand01(puff_seed ^ 0x02e5_be93) - 0.5) * 3.0;
155 let oz = (rand01(puff_seed ^ 0xa1d3_4f2b) - 0.5) * 7.0;
156
157 let base = 0.7 + rand01(puff_seed ^ 0x9e37_79b9) * 2.6;
158 let sx = base * (0.7 + rand01(puff_seed ^ 0x243f_6a88) * 0.8);
159 let sy = base * (0.6 + rand01(puff_seed ^ 0x85a3_08d3) * 0.9);
160 let sz = base * (0.7 + rand01(puff_seed ^ 0x1319_8a2e) * 0.8);
161
162 let tx = universe.world.add_component(
163 engine::ecs::component::TransformComponent::new()
164 .with_position(ox, oy, oz)
165 .with_scale(sx, sy, sz),
166 );
167 let renderable =
168 universe
169 .world
170 .add_component(engine::ecs::component::RenderableComponent::new(
171 engine::graphics::primitives::Renderable::new(
172 cube_mesh,
173 engine::graphics::primitives::MaterialHandle::TOON_MESH,
174 ),
175 ));
176
177 // Slight blue-grey variation.
178 let t = rand01(puff_seed ^ 0x7f4a_7c15);
179 let r = 0.55 + 0.10 * t;
180 let g = 0.58 + 0.10 * t;
181 let b = 0.66 + 0.12 * t;
182 let color = universe
183 .world
184 .add_component(engine::ecs::component::ColorComponent::rgba(r, g, b, 1.0));
185
186 let _ = universe.attach(center_tx, tx);
187 let _ = universe.attach(tx, renderable);
188 let _ = universe.attach(renderable, color);
189 }
190 }
191 }
192
193 // Foreground reference cube (should never be occluded by background depth).
194 let fg_tx = universe.world.add_component(
195 engine::ecs::component::TransformComponent::new()
196 .with_position(0.0, -0.5, -4.0)
197 .with_scale(1.2, 0.8, 1.2),
198 );
199 let fg_renderable =
200 universe
201 .world
202 .add_component(engine::ecs::component::RenderableComponent::new(
203 engine::graphics::primitives::Renderable::new(
204 cube_mesh,
205 engine::graphics::primitives::MaterialHandle::TOON_MESH,
206 ),
207 ));
208 let fg_color = universe
209 .world
210 .add_component(engine::ecs::component::ColorComponent::rgba(
211 0.9, 0.2, 0.2, 1.0,
212 ));
213
214 universe.add(fg_tx);
215 let _ = universe.attach(fg_tx, fg_renderable);
216 let _ = universe.attach(fg_renderable, fg_color);
217
218 // Foreground opaque floor: 16x16 larger white cubes, 10 units below the reference cube.
219 let floor_root = universe.world.add_component(
220 engine::ecs::component::TransformComponent::new().with_position(0.0, -10.5, -10.0),
221 );
222 universe.add(floor_root);
223
224 let spacing = 10_f32;
225 let half = 5_f32;
226 for x in 0..16u32 {
227 for z in 0..64u32 {
228 let px = (x as f32 - half) * spacing;
229 let pz = (z as f32 * -1.0 + half) * spacing;
230 let tx = universe.world.add_component(
231 engine::ecs::component::TransformComponent::new()
232 .with_position(px, -5.0, pz)
233 .with_scale(5.0, 0.5, 5.0),
234 );
235 let renderable =
236 universe
237 .world
238 .add_component(engine::ecs::component::RenderableComponent::new(
239 engine::graphics::primitives::Renderable::new(
240 cube_mesh,
241 engine::graphics::primitives::MaterialHandle::TOON_MESH,
242 ),
243 ));
244 let color = universe
245 .world
246 .add_component(engine::ecs::component::ColorComponent::rgba(
247 1.0, 1.0, 1.0, 1.0,
248 ));
249
250 let _ = universe.attach(floor_root, tx);
251 let _ = universe.attach(tx, renderable);
252 let _ = universe.attach(renderable, color);
253 }
254 }
255
256 universe.systems.process_commands(
257 &mut universe.world,
258 &mut universe.visuals,
259 &mut universe.render_assets,
260 &mut universe.command_queue,
261 );
262
263 universe.enable_repl();
264 engine::Windowing::run_app(universe).expect("Windowing failed");
265}examples/vr-input.rs (line 250)
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}examples/font-example.rs (line 42)
13fn main() {
14 mittens_engine::example_support::ensure_model_assets();
15 utils::logger::init();
16
17 let world = engine::ecs::World::default();
18 let mut universe = engine::Universe::new(world);
19
20 // Dark background so the font texture pops.
21 let background = universe
22 .world
23 .add_component(BackgroundColorComponent::new());
24 let background_c = universe
25 .world
26 .add_component(ColorComponent::rgba(0.20, 0.2, 0.20, 1.0));
27 let _ = universe.world.add_child(background, background_c);
28 universe.add(background);
29
30 // Ambient so text is readable even without explicit lights.
31 let ambient = universe
32 .world
33 .add_component(AmbientLightComponent::rgb(0.50, 0.50, 0.50));
34 universe.add(ambient);
35
36 let directional_tx = universe
37 .world
38 .add_component(TransformComponent::new().with_position(0.0, 0.5, 1.0));
39 let directional_light = universe.world.add_component(
40 engine::ecs::component::DirectionalLightComponent::new()
41 .with_color(1.0, 1.0, 1.0)
42 .with_intensity(0.8),
43 );
44 let _ = universe.attach(directional_tx, directional_light);
45 universe.add(directional_tx);
46
47 // --- background clouds ---
48 // Background stage (occluded + lit) so the cloud volume self-occludes but won't occlude
49 // the foreground text (renderer clears depth before foreground).
50 let bg_root = universe
51 .world
52 .add_component(BackgroundComponent::new().with_occlusion_and_lighting());
53 universe.add(bg_root);
54
55 let mut bg_cloud_params = example_util::CloudRingParams::default();
56 bg_cloud_params.cloud_count = 6;
57 bg_cloud_params.seed = 0xF0_17_C10u32;
58 example_util::spawn_cloud_ring(&mut universe, bg_root, bg_cloud_params);
59
60 // I {
61 // // not fps rotation, just relative rotation
62 // with_forward_z()
63 // with_roll_axis_y()
64 // C3D {}
65 // }
66 let input = universe
67 .world
68 .add_component(InputComponent::new().with_speed(2.0));
69 let input_mode = universe
70 .world
71 .add_component(InputTransformModeComponent::forward_z().with_roll_axis_y());
72 let _ = universe.attach(input, input_mode);
73
74 let rig_transform = universe
75 .world
76 .add_component(TransformComponent::new().with_position(1.8, -0.5, 2.5));
77 let _ = universe.attach(input, rig_transform);
78
79 let camera = universe.world.add_component(Camera3DComponent::new());
80 let _ = universe.attach(rig_transform, camera);
81
82 // Click-to-pick: treat this camera rig as a pointer source.
83 let pointer = universe.world.add_component(PointerComponent::new());
84 let _ = universe.attach(camera, pointer);
85
86 // Topology: I { T { C3D } } — add a small camera-attached controls hint.
87 example_util::spawn_desktop_camera_controls_hint(&mut universe, rig_transform);
88
89 universe.add(input);
90
91 // --- foreground clouds ---
92 // Normal foreground renderables (not background stage).
93 // Offset the ring forward (negative Z) so several clusters are in view.
94 let fg_cloud_root = universe
95 .world
96 .add_component(TransformComponent::new().with_position(0.0, -6.0, -10.0));
97 universe.add(fg_cloud_root);
98
99 let mut fg_cloud_params = example_util::CloudRingParams::default();
100 fg_cloud_params.cloud_count = 4;
101 fg_cloud_params.radius = 9.0;
102 fg_cloud_params.center_y = 1.0;
103 fg_cloud_params.puffs_per_cloud = 22;
104 fg_cloud_params.angle_jitter = 0.35;
105 fg_cloud_params.high_y_probability = 0.35;
106 fg_cloud_params.high_y_multiplier = 1.4;
107 fg_cloud_params.seed = 0xF0_17_C102u32;
108 example_util::spawn_cloud_ring(&mut universe, fg_cloud_root, fg_cloud_params);
109
110 // T {
111 // with_translation(0,0, -2)
112 // TXT {
113 // "ababaabbaabbaaabbbaaabbbaaaabbbbaaaaabbbbbababababa"
114 // TextureComponent { assets/images/test.font_system.png }
115 // }
116 // }
117 fn estimate_text_height_world(text: &str, scale: f32) -> f32 {
118 let line_count = text.lines().count().max(1) as f32;
119 // Text quads are ~1 unit tall per line in text-space.
120 // Add some padding so blocks don't feel cramped.
121 let pad_lines = 1.25;
122 (line_count + pad_lines) * scale
123 }
124
125 fn spawn_text_block(
126 universe: &mut engine::Universe,
127 position: (f32, f32, f32),
128 scale: f32,
129 text: &str,
130 font_texture_uri: Option<&str>,
131 text_color_rgba: Option<[f32; 4]>,
132 shadow: Option<TextShadowComponent>,
133 filtering: TextureFilteringComponent,
134 ) -> f32 {
135 // T_root { T_scale { [Color] { TXT { shadow, filtering } } } }
136 let text_root = universe.world.add_component(
137 TransformComponent::new().with_position(position.0, position.1, position.2),
138 );
139
140 let text_scale = universe
141 .world
142 .add_component(TransformComponent::new().with_scale(scale, scale, 1.0));
143 let _ = universe.attach(text_root, text_scale);
144
145 let text_parent = if let Some([r, g, b, a]) = text_color_rgba {
146 let color = universe
147 .world
148 .add_component(ColorComponent::rgba(r, g, b, a));
149 let _ = universe.attach(text_scale, color);
150 color
151 } else {
152 text_scale
153 };
154
155 let text_c = universe.world.add_component(TextComponent::new(text));
156 let _ = universe.attach(text_parent, text_c);
157
158 // Explicit opt-in: make the glyph renderables pickable.
159 // TextSystem will propagate this to all spawned glyph quads.
160 let raycastable = universe
161 .world
162 .add_component(RaycastableComponent::enabled());
163 let _ = universe.attach(text_c, raycastable);
164
165 // Route glyph quads into the alpha-to-coverage cutout pass.
166 let cutout = universe
167 .world
168 .add_component(TransparentCutoutComponent::new());
169 let _ = universe.attach(text_c, cutout);
170
171 if let Some(shadow) = shadow {
172 let shadow_id = universe.world.add_component(shadow);
173 let _ = universe.attach(text_c, shadow_id);
174 }
175
176 // Optional: override the font atlas for this text block.
177 // TextSystem will propagate this to all glyph renderables.
178 if let Some(uri) = font_texture_uri {
179 let tex = universe
180 .world
181 .add_component(TextureComponent::with_uri(uri));
182 let _ = universe.attach(text_c, tex);
183 }
184
185 let filtering_id = universe.world.add_component(filtering);
186 let _ = universe.attach(text_c, filtering_id);
187
188 universe.add(text_root);
189
190 estimate_text_height_world(text, scale)
191 }
192
193 // --- text blocks ---
194 // Multi-line samples at different scales.
195 // (Text literals omitted in the README/snippet; see constants below.)
196 const TEXT_BIG: &str = "CAT ENGINE\nfont example\nBIG TEXT";
197 const TEXT_MED: &str = "multi-line\ntext block\nmedium";
198 const TEXT_SMALL: &str = "small\ntext";
199 const TEXT_TINY: &str = "tiny\ntext\n(zoom in)";
200
201 // Stack vertically; advance by (measured height + gap) so big text gets more room.
202 let x = -1.2;
203 let z = -2.0;
204 let mut y = 1.2;
205 let gap = 0.15;
206
207 let shadow_crisp = Some(
208 TextShadowComponent::new()
209 .with_scale(1.35)
210 .with_offset([0.06, -0.06, 0.0015]),
211 );
212
213 y -= spawn_text_block(
214 &mut universe,
215 (x, y, z),
216 0.55,
217 TEXT_BIG,
218 None,
219 Some([1.0, 1.0, 1.0, 1.0]),
220 shadow_crisp,
221 TextureFilteringComponent::nearest_magnification(),
222 ) + gap;
223 y -= spawn_text_block(
224 &mut universe,
225 (x, y, z),
226 0.25,
227 TEXT_MED,
228 None,
229 Some([0.60, 0.95, 1.00, 1.0]),
230 Some(
231 TextShadowComponent::new()
232 .with_rgba([0.0, 0.0, 0.15, 1.0])
233 .with_scale(1.20)
234 .with_offset([0.05, -0.04, 0.0015]),
235 ),
236 TextureFilteringComponent::linear(),
237 ) + gap;
238 y -= spawn_text_block(
239 &mut universe,
240 (x, y, z),
241 0.14,
242 TEXT_SMALL,
243 None,
244 Some([1.0, 0.88, 0.35, 1.0]),
245 Some(
246 TextShadowComponent::new()
247 .with_rgba([0.15, 0.0, 0.0, 1.0])
248 .with_scale(1.55)
249 .with_offset([0.08, -0.08, 0.0015]),
250 ),
251 TextureFilteringComponent::nearest(),
252 ) + gap;
253 let _ = spawn_text_block(
254 &mut universe,
255 (x, y, z),
256 0.08,
257 TEXT_TINY,
258 None,
259 Some([0.90, 1.0, 0.70, 1.0]),
260 shadow_crisp,
261 TextureFilteringComponent::nearest_magnification(),
262 );
263
264 // Left block: explicit multi-line text using the default font_system atlas.
265 const TEXT_LEFT: &str = "even though there's hexes\nto the solar plexus in my lexus\ni'm feelin' reckless,\nwhen i'm eating breakfast";
266 let _ = spawn_text_block(
267 &mut universe,
268 (x - 8.1, 1.1, z),
269 0.22,
270 TEXT_LEFT,
271 Some("assets/textures/font_system.dds"),
272 Some([0.95, 0.95, 0.95, 1.0]),
273 Some(
274 TextShadowComponent::new()
275 .with_scale(1.25)
276 .with_offset([0.05, -0.05, 0.0015]),
277 ),
278 TextureFilteringComponent::nearest_magnification(),
279 );
280
281 // Alt atlas: put it *behind* the original stack (slightly farther from the camera)
282 // and tint it dark grey.
283 let alt_atlas = Some("assets/textures/font_system.0.0.dds");
284 let alt_z = z - 0.05;
285 let alt_grey = Some([0.25, 0.25, 0.25, 1.0]);
286
287 let mut y_alt = 1.2;
288 y_alt -= spawn_text_block(
289 &mut universe,
290 (x, y_alt, alt_z),
291 0.55,
292 TEXT_BIG,
293 alt_atlas,
294 alt_grey,
295 Some(
296 TextShadowComponent::new()
297 .with_rgba([0.0, 0.0, 0.0, 1.0])
298 .with_scale(1.15)
299 .with_offset([0.03, -0.03, 0.0015]),
300 ),
301 TextureFilteringComponent::linear(),
302 ) + gap;
303 y_alt -= spawn_text_block(
304 &mut universe,
305 (x, y_alt, alt_z),
306 0.25,
307 TEXT_MED,
308 alt_atlas,
309 alt_grey,
310 Some(
311 TextShadowComponent::new()
312 .with_rgba([0.0, 0.0, 0.0, 1.0])
313 .with_scale(1.15)
314 .with_offset([0.03, -0.03, 0.0015]),
315 ),
316 TextureFilteringComponent::linear(),
317 ) + gap;
318 y_alt -= spawn_text_block(
319 &mut universe,
320 (x, y_alt, alt_z),
321 0.14,
322 TEXT_SMALL,
323 alt_atlas,
324 alt_grey,
325 Some(
326 TextShadowComponent::new()
327 .with_rgba([0.0, 0.0, 0.0, 1.0])
328 .with_scale(1.15)
329 .with_offset([0.03, -0.03, 0.0015]),
330 ),
331 TextureFilteringComponent::linear(),
332 ) + gap;
333 let _ = spawn_text_block(
334 &mut universe,
335 (x, y_alt, alt_z),
336 0.08,
337 TEXT_TINY,
338 alt_atlas,
339 alt_grey,
340 Some(
341 TextShadowComponent::new()
342 .with_rgba([0.0, 0.0, 0.0, 1.0])
343 .with_scale(1.15)
344 .with_offset([0.03, -0.03, 0.0015]),
345 ),
346 TextureFilteringComponent::linear(),
347 );
348
349 universe.enable_repl();
350
351 let xr_input = universe.world.add_component(InputXRComponent::on());
352 let xr_gamepad = universe
353 .world
354 .add_component(engine::ecs::component::InputXRGamepadComponent::new().speed(1.5));
355 let xr_head = universe.world.add_component(TransformComponent::new());
356 let xr_camera = universe.world.add_component(CameraXRComponent::on());
357 let _ = universe.attach(xr_input, xr_head);
358 let _ = universe.attach(xr_input, xr_gamepad);
359 let _ = universe.attach(xr_head, xr_camera);
360 universe.add(xr_input);
361
362 // Add an OpenXR component so OpenXRSystem initializes and starts polling events.
363 let xr_root = universe
364 .world
365 .add_component(engine::ecs::component::XrComponent::on());
366 universe.add(xr_root);
367
368 // Process init-time registrations (Text expands into glyph subtrees here).
369 universe.systems.process_commands(
370 &mut universe.world,
371 &mut universe.visuals,
372 &mut universe.render_assets,
373 &mut universe.command_queue,
374 );
375
376 engine::Windowing::run_app(universe).expect("Windowing failed");
377}examples/vtuber-joints-example.rs (line 75)
14fn main() {
15 mittens_engine::example_support::ensure_model_assets();
16 utils::logger::init();
17
18 let world = engine::ecs::World::default();
19 let mut universe = engine::Universe::new(world);
20
21 // Slow the global beat clock so beat-based animations run half as fast.
22 let clock = universe
23 .world
24 .add_component(ClockComponent::new().with_bpm(60.0));
25 universe.add(clock);
26
27 // Light pink background.
28 let background = universe
29 .world
30 .add_component(BackgroundColorComponent::new());
31 let background_c = universe
32 .world
33 .add_component(ColorComponent::rgba(1.0, 0.82, 0.90, 1.0));
34 let _ = universe.world.add_child(background, background_c);
35 universe.add(background);
36
37 // Small ambient so shadowed areas aren't pure black.
38 let ambient = universe
39 .world
40 .add_component(AmbientLightComponent::rgb(0.10, 0.10, 0.12));
41 universe.add(ambient);
42
43 // --- Camera rig (WASD + mouse) ---
44 let input = universe
45 .world
46 .add_component(InputComponent::new().with_speed(1.5));
47 let input_mode = universe.world.add_component(
48 InputTransformModeComponent::forward_z()
49 .with_fps_rotation()
50 .with_roll_axis_y(),
51 );
52 let _ = universe.attach(input, input_mode);
53
54 // Start slightly pulled back looking towards the origin.
55 let rig_transform = universe
56 .world
57 .add_component(TransformComponent::new().with_position(0.0, 0.0, 6.0));
58 let _ = universe.attach(input, rig_transform);
59
60 let camera3d = universe.world.add_component(Camera3DComponent::new());
61 let _ = universe.attach(rig_transform, camera3d);
62
63 // Pointer so gizmos can be interacted with.
64 let pointer = universe.world.add_component(PointerComponent::new());
65 let _ = universe.attach(camera3d, pointer);
66
67 // Topology: I { T { C3D } } — add a small camera-attached controls hint.
68 example_util::spawn_desktop_camera_controls_hint(&mut universe, rig_transform);
69
70 universe.add(input);
71
72 // --- lighting ---
73 let sun = universe.world.add_component(
74 DirectionalLightComponent::new()
75 .with_intensity(1.2)
76 .with_color(1.0, 0.98, 0.94),
77 );
78 let sun_dir = universe
79 .world
80 .add_component(TransformComponent::new().with_position(0.0, -0.35, 1.0));
81 let _ = universe.attach(sun_dir, sun);
82 universe.add(sun_dir);
83
84 let light_transform = universe.world.add_component(
85 TransformComponent::new()
86 .with_position(1.0, 6.0, 3.0)
87 .with_scale(0.1, 0.1, 0.1),
88 );
89 let light = universe.world.add_component(
90 engine::ecs::component::PointLightComponent::new()
91 .with_distance(120.0)
92 .with_color(1.0, 1.0, 1.0),
93 );
94 let _ = universe.attach(light_transform, light);
95 universe.add(light_transform);
96
97 // --- VTuber model ---
98 let model_uri = "assets/models/pc-rei.hoodie.glb";
99
100 // Wrap the model subtree in an editor root so transform-only glTF nodes can be visualized
101 // (and thus raycasted/selected) without affecting non-editor scenes.
102 let editor_root = universe.world.add_component(EditorComponent::new());
103
104 let model_root = universe.world.add_component(TransformComponent::new());
105 let model = universe.world.add_component(GLTFComponent::new(model_uri));
106
107 // emissive for pc-rei
108 let emissive = universe.world.add_component(EmissiveComponent::on());
109 let _ = universe.attach(model, emissive);
110
111 let xr_input = universe.world.add_component(InputXRComponent::on());
112 let xr_gamepad = universe
113 .world
114 .add_component(engine::ecs::component::InputXRGamepadComponent::new().speed(1.5));
115 let xr_head = universe.world.add_component(TransformComponent::new());
116 let xr_camera = universe.world.add_component(CameraXRComponent::on());
117 let _ = universe.attach(xr_input, xr_head);
118 let _ = universe.attach(xr_input, xr_gamepad);
119 let _ = universe.attach(xr_head, xr_camera);
120 let xr_pointer = universe.world.add_component(PointerComponent::new());
121 let _ = universe.attach(xr_camera, xr_pointer);
122 let _ = universe.attach(xr_head, editor_root);
123
124 let _ = universe.attach(model_root, model);
125
126 let _ = universe.attach(editor_root, model_root);
127
128 // Initialize the editor root so GLTFComponent gets registered.
129 universe.add(xr_input);
130 universe.add(editor_root);
131
132 // --- Background clouds (occluded + lit) ---
133 let bg_root = universe.world.add_component(
134 engine::ecs::component::BackgroundComponent::new().with_occlusion_and_lighting(),
135 );
136 universe.add(bg_root);
137 let mut cloud_params = example_util::CloudRingParams::default();
138 cloud_params.cloud_count = 8; // +3 clusters
139 cloud_params.angle_jitter = 0.35;
140 cloud_params.high_y_probability = 0.5;
141 cloud_params.high_y_multiplier = 1.5;
142 cloud_params.seed = 0x57_55_B0_01u32;
143 example_util::spawn_cloud_ring(&mut universe, bg_root, cloud_params);
144
145 // --- Simple environment ---
146 let spawn_cube = |universe: &mut engine::Universe,
147 position: (f32, f32, f32),
148 scale: (f32, f32, f32),
149 color: (f32, f32, f32, f32)| {
150 let transform = universe.world.add_component(
151 TransformComponent::new()
152 .with_position(position.0, position.1, position.2)
153 .with_scale(scale.0, scale.1, scale.2),
154 );
155 let renderable = universe.world.add_component(RenderableComponent::cube());
156 let color = universe
157 .world
158 .add_component(ColorComponent::rgba(color.0, color.1, color.2, color.3));
159
160 let _ = universe.attach(transform, renderable);
161 let _ = universe.attach(renderable, color);
162
163 universe.add(transform);
164 };
165
166 // floor
167 spawn_cube(
168 &mut universe,
169 (0.0, -0.05, 0.0),
170 (10.0, 0.1, 10.0),
171 (0.92, 0.92, 0.92, 1.0),
172 );
173
174 // back wall
175 spawn_cube(
176 &mut universe,
177 (-3.0, 1.5, -5.0),
178 (3.0, 3.0, 1.0),
179 (0.95, 0.94, 0.96, 1.0),
180 );
181
182 // desk
183 spawn_cube(
184 &mut universe,
185 (0.0, 0.35, 1.0),
186 (1.0, 0.75, 0.5),
187 (0.75, 0.70, 0.65, 1.0),
188 );
189
190 // --- Editor-side stacked cubes (inside the editor subtree for picking/gizmos) ---
191 {
192 let spawn_editor_cube = |universe: &mut engine::Universe,
193 editor_root: engine::ecs::ComponentId,
194 name: &str,
195 position: (f32, f32, f32),
196 scale: (f32, f32, f32),
197 color: (f32, f32, f32, f32)| {
198 let transform = universe.world.add_component_boxed_named(
199 format!("{name}_t"),
200 Box::new(
201 TransformComponent::new()
202 .with_position(position.0, position.1, position.2)
203 .with_scale(scale.0, scale.1, scale.2),
204 ),
205 );
206 let renderable = universe.world.add_component_boxed_named(
207 format!("{name}_r"),
208 Box::new(RenderableComponent::cube()),
209 );
210 let color_comp = universe.world.add_component_boxed_named(
211 format!("{name}_color"),
212 Box::new(ColorComponent::rgba(color.0, color.1, color.2, color.3)),
213 );
214 let raycastable = universe.world.add_component_boxed_named(
215 format!("{name}_raycastable"),
216 Box::new(RaycastableComponent::enabled()),
217 );
218
219 let _ = universe.world.add_child(transform, renderable);
220 let _ = universe.world.add_child(renderable, color_comp);
221 let _ = universe.world.add_child(renderable, raycastable);
222
223 // One attach into the initialized editor subtree triggers init for the new subtree.
224 let _ = universe.attach(editor_root, transform);
225 };
226
227 // Place the stack beside the desk (a bit to the right).
228 let stack_x = 1.35;
229 let stack_z = 1.0;
230 let s = 0.25;
231 let half = 0.5 * s;
232 let light_brown = (0.80, 0.72, 0.55, 1.0);
233 let cyan = (0.20, 1.00, 1.00, 1.0);
234
235 spawn_editor_cube(
236 &mut universe,
237 editor_root,
238 "editor_stack_0",
239 (stack_x, half, stack_z),
240 (s, s, s),
241 light_brown,
242 );
243 spawn_editor_cube(
244 &mut universe,
245 editor_root,
246 "editor_stack_1",
247 (stack_x, half + 1.0 * s, stack_z),
248 (s, s, s),
249 light_brown,
250 );
251 spawn_editor_cube(
252 &mut universe,
253 editor_root,
254 "editor_stack_2",
255 (stack_x, half + 2.0 * s, stack_z),
256 (s, s, s),
257 light_brown,
258 );
259 spawn_editor_cube(
260 &mut universe,
261 editor_root,
262 "editor_stack_top",
263 (stack_x, half + 3.0 * s, stack_z),
264 (s, s, s),
265 cyan,
266 );
267 }
268
269 let xr_root = universe
270 .world
271 .add_component(engine::ecs::component::XrComponent::on());
272 universe.add(xr_root);
273
274 universe.systems.process_commands(
275 &mut universe.world,
276 &mut universe.visuals,
277 &mut universe.render_assets,
278 &mut universe.command_queue,
279 );
280
281 // Spawn the glTF subtree once up-front so joint ComponentIds exist.
282 {
283 let systems = &mut universe.systems;
284 systems.gltf.tick_with_queue(
285 &mut universe.world,
286 &mut universe.visuals,
287 &mut systems.skinned_mesh,
288 &mut universe.command_queue,
289 0.0,
290 );
291 }
292 universe.systems.process_commands(
293 &mut universe.world,
294 &mut universe.visuals,
295 &mut universe.render_assets,
296 &mut universe.command_queue,
297 );
298
299 // Register imported meshes into RenderAssets early so we can inspect skin weights
300 // (textures will still be uploaded later during normal rendering).
301 universe
302 .systems
303 .gltf
304 .flush_mesh_imports_only(&mut universe.render_assets);
305
306 // --- Joint printout + animation binding (in the example) ---
307 let all_joints = collect_joint_transforms(
308 &universe.world,
309 &universe.systems.skinned_mesh,
310 &universe.visuals,
311 model,
312 model_root,
313 );
314 println!("[vtuber-joints-example] joints found: {}", all_joints.len());
315 for (i, (node_index, joint_tx)) in all_joints.iter().enumerate() {
316 println!(" joint[{i:03}] node_index={node_index} transform={joint_tx:?}");
317 }
318
319 let node_index_to_transform: HashMap<usize, engine::ecs::ComponentId> =
320 all_joints.iter().copied().collect();
321
322 // Example settings are hardcoded to keep this example simple.
323 let joint_offset: usize = 0;
324 let wiggle_count: usize = 16;
325
326 let target_mesh_key = "pc-rei.hoodie:Body_(merged).baked:prim0".to_string();
327 let target_joint_names: Vec<String> = vec![
328 "J_Bip_L_UpperArm".to_string(),
329 "J_Bip_R_UpperArm".to_string(),
330 ];
331
332 let print_transform_updates: bool = false;
333
334 let selected_joint_transforms: Vec<(usize, engine::ecs::ComponentId)> =
335 select_named_joints(&universe.world, &all_joints, &target_joint_names)
336 .or_else(|| {
337 select_body_prim0_influencers(
338 &universe,
339 model_root,
340 &target_mesh_key,
341 wiggle_count,
342 &node_index_to_transform,
343 )
344 })
345 .unwrap_or_else(|| select_joint_range(&all_joints, joint_offset, wiggle_count));
346 println!(
347 "[vtuber-joints-example] wiggle selection: target_mesh_key='{}' offset={} count={} selected={}",
348 target_mesh_key,
349 joint_offset,
350 wiggle_count,
351 selected_joint_transforms.len()
352 );
353
354 debug_print_selected_joint_influence(
355 &universe,
356 &target_mesh_key,
357 model_root,
358 &selected_joint_transforms,
359 );
360
361 println!("[vtuber-joints-example] selected joints:");
362 for (i, (node_index, joint_tx)) in selected_joint_transforms.iter().enumerate() {
363 let name = universe
364 .world
365 .get_component_record(*joint_tx)
366 .map(|n| n.name.as_str())
367 .unwrap_or("<unknown>");
368 println!(" sel[{i:02}] node_index={node_index} name={name} transform={joint_tx:?}");
369 }
370
371 if print_transform_updates {
372 println!("[vtuber-joints-example] note: joint animation disabled");
373 }
374
375 universe.systems.process_commands(
376 &mut universe.world,
377 &mut universe.visuals,
378 &mut universe.render_assets,
379 &mut universe.command_queue,
380 );
381
382 universe.enable_repl();
383 engine::Windowing::run_app(universe).expect("Windowing failed");
384}examples/audio-graph-example.rs (line 85)
6fn main() {
7 mittens_engine::example_support::ensure_model_assets();
8 utils::logger::init();
9
10 // Debug toggle: allow isolating stack overflows to audio vs. non-audio init.
11 // Set `CAT_AUDIO_EXAMPLE_DISABLE_AUDIO=1` to skip creating audio components and
12 // skip scheduling audio notes. The UI/text/cube visualization will still spawn.
13 let audio_enabled = std::env::var("CAT_AUDIO_EXAMPLE_DISABLE_AUDIO")
14 .ok()
15 .as_deref()
16 != Some("1");
17
18 // If set, we still build audio graph components but we don't start the CPAL output stream.
19 // This helps distinguish "audio graph / scheduling" issues from "CPAL backend" issues.
20 let audio_output_enabled = std::env::var("CAT_AUDIO_EXAMPLE_AUDIO_OUTPUT_OFF")
21 .ok()
22 .as_deref()
23 != Some("1");
24
25 println!("[audio-graph-example] start");
26
27 let world = engine::ecs::World::default();
28 let mut universe = engine::Universe::new(world);
29
30 println!("[audio-graph-example] universe created");
31
32 // Minimal scene with a camera so the window opens (copied from animation-example).
33 let clear = universe
34 .world
35 .add_component(engine::ecs::component::BackgroundColorComponent::new());
36 let clear_c = universe
37 .world
38 .add_component(engine::ecs::component::ColorComponent::rgba(
39 0.07, 0.07, 0.07, 1.0,
40 ));
41 let _ = universe.world.add_child(clear, clear_c);
42 universe.add(clear);
43
44 // Ambient light so unlit areas aren't pitch black.
45 // Keep it dark to match the background clear color.
46 // (User request) 2.5x brighter.
47 let ambient = universe
48 .world
49 .add_component(engine::ecs::component::AmbientLightComponent::rgb(
50 0.075, 0.075, 0.075,
51 ));
52 universe.add(ambient);
53
54 // Input-driven camera rig.
55 let input = universe
56 .world
57 .add_component(engine::ecs::component::InputComponent::new().with_speed(2.0));
58 let rig_transform = universe.world.add_component(
59 engine::ecs::component::TransformComponent::new().with_position(2.0, 0.0, 7.0),
60 );
61 let input_mode = universe.world.add_component(
62 engine::ecs::component::InputTransformModeComponent::forward_z().with_roll_axis_y(),
63 );
64 let camera3d = universe.world.add_component(
65 engine::ecs::component::Camera3DComponent::new()
66 .with_far(250.0)
67 .with_fov(70.0),
68 );
69 let _ = universe.attach(input, input_mode);
70 let _ = universe.attach(input, rig_transform);
71 let _ = universe.attach(rig_transform, camera3d);
72
73 // Topology: I { T { C3D } } — add a small camera-attached controls hint.
74 example_util::spawn_desktop_camera_controls_hint(&mut universe, rig_transform);
75 universe.add(input);
76
77 // Directional light (sun-ish). Note: the renderer interprets the node's world position
78 // as a direction vector (see DirectionalLightComponent docs).
79 let light_tx = universe.world.add_component(
80 engine::ecs::component::TransformComponent::new().with_position(0.2, 0.7, 1.0),
81 );
82 let light = universe.world.add_component(
83 engine::ecs::component::DirectionalLightComponent::new()
84 .with_color(1.0, 1.0, 1.0)
85 .with_intensity(0.35),
86 );
87 let _ = universe.attach(light_tx, light);
88 universe.add(light_tx);
89
90 // --- Background clouds (occluded + lit) ---
91 // Mirrors the vtuber example: use a BackgroundComponent stage so the cloud volume
92 // self-occludes and is lit, but renders as background.
93 let bg_root = universe.world.add_component(
94 engine::ecs::component::BackgroundComponent::new().with_occlusion_and_lighting(),
95 );
96 universe.add(bg_root);
97 let mut cloud_params = example_util::CloudRingParams::default();
98 cloud_params.cloud_count = 7;
99 cloud_params.radius = 22.0;
100 // Move the ring up by ~one cloud height. The cloud generator uses a ~4.0 unit
101 // vertical spread for puff offsets, so +4.0 is a good "one height" bump.
102 cloud_params.center_y = 6.0;
103 cloud_params.puffs_per_cloud = 26;
104 cloud_params.angle_jitter = 0.0;
105 cloud_params.high_y_probability = 0.0;
106 cloud_params.high_y_multiplier = 1.0;
107 cloud_params.seed = 0xA0_D1_0C_01u32;
108 example_util::spawn_cloud_ring(&mut universe, bg_root, cloud_params);
109
110 // ClockComponent sets global tempo.
111 if audio_enabled {
112 let clock = universe
113 .world
114 .add_component(engine::ecs::component::ClockComponent::new().with_bpm(128.0));
115 universe.add(clock);
116 }
117
118 if audio_enabled {
119 println!("[audio-graph-example] clock added");
120 } else {
121 println!("[audio-graph-example] audio disabled; skipping clock");
122 }
123
124 // Audio output + 2 oscillator sources.
125 let audio_out = if audio_enabled {
126 let audio_out_comp = if audio_output_enabled {
127 engine::ecs::component::AudioOutputComponent::new()
128 } else {
129 engine::ecs::component::AudioOutputComponent::off()
130 };
131 let audio_out = universe.world.add_component(audio_out_comp);
132 universe.add(audio_out);
133 Some(audio_out)
134 } else {
135 None
136 };
137
138 match (audio_enabled, audio_output_enabled) {
139 (false, _) => println!("[audio-graph-example] audio disabled; skipping audio output"),
140 (true, true) => println!("[audio-graph-example] audio output added (CPAL on)"),
141 (true, false) => println!("[audio-graph-example] audio output added (CPAL off)"),
142 }
143
144 // Track A: square lead.
145 let osc_a_comp =
146 if audio_enabled {
147 let osc_a = engine::ecs::component::AudioOscillator::square()
148 .with_frequency(110.0)
149 .with_amplitude(0.12)
150 .with_enabled(false);
151 let osc_a_comp = universe.world.add_component(
152 engine::ecs::component::AudioOscillatorComponent::single(osc_a),
153 );
154 if let Some(audio_out) = audio_out {
155 let _ = universe.attach(audio_out, osc_a_comp);
156 }
157 Some(osc_a_comp)
158 } else {
159 None
160 };
161
162 if audio_enabled {
163 println!("[audio-graph-example] track A created");
164 } else {
165 println!("[audio-graph-example] audio disabled; skipping track A");
166 }
167
168 // Effect tree A (single chain):
169 // osc_a
170 // Gain
171 // BandPass
172 // Limiter
173 let mut bp_a_comp: Option<engine::ecs::ComponentId> = None;
174 if let Some(osc_a_comp) = osc_a_comp {
175 let gain_a = universe
176 .world
177 .add_component(engine::ecs::component::AudioGainComponent::new(3.2));
178 let bp_a = universe.world.add_component(
179 engine::ecs::component::AudioBandPassFilterComponent::new(120.0, 3.0, 0.40),
180 );
181 bp_a_comp = Some(bp_a);
182 let lim_a =
183 universe
184 .world
185 .add_component(engine::ecs::component::AudioLimiterComponent::new(
186 4.0, 80.0, 0.90,
187 ));
188
189 let _ = universe.attach(osc_a_comp, gain_a);
190 let _ = universe.attach(gain_a, bp_a);
191 let _ = universe.attach(bp_a, lim_a);
192 }
193
194 if audio_enabled {
195 println!("[audio-graph-example] track A effects attached");
196 }
197
198 // --- Visual layout helpers (copied/adapted from animation-example) ---
199 fn spawn_text(
200 universe: &mut engine::Universe,
201 pos: (f32, f32, f32),
202 scale: f32,
203 wrap_cols: usize,
204 text: &str,
205 ) {
206 let tx = universe.world.add_component(
207 engine::ecs::component::TransformComponent::new()
208 .with_position(pos.0, pos.1, pos.2)
209 .with_scale(scale, scale, 1.0),
210 );
211 let t =
212 universe
213 .world
214 .add_component(engine::ecs::component::TextComponent::with_word_wrap(
215 text, wrap_cols,
216 ));
217 let _ = universe.attach(tx, t);
218
219 // TextSystem looks for an immediate TextureFilteringComponent child.
220 let filtering = universe
221 .world
222 .add_component(engine::ecs::component::TextureFilteringComponent::nearest());
223 let _ = universe.attach(t, filtering);
224
225 // TextSystem also supports styling from immediate Emissive children.
226 let emissive = universe
227 .world
228 .add_component(engine::ecs::component::EmissiveComponent::on());
229 let _ = universe.attach(t, emissive);
230
231 universe.add(tx);
232 }
233
234 fn spawn_emissive_cube(
235 universe: &mut engine::Universe,
236 parent: engine::ecs::ComponentId,
237 pos: (f32, f32, f32),
238 scale: f32,
239 rgba: [f32; 4],
240 ) {
241 let tx = universe.world.add_component(
242 engine::ecs::component::TransformComponent::new()
243 .with_position(pos.0, pos.1, pos.2)
244 .with_scale(scale, scale, scale),
245 );
246 let r = universe
247 .world
248 .add_component(engine::ecs::component::RenderableComponent::cube());
249 let c = universe
250 .world
251 .add_component(engine::ecs::component::ColorComponent::rgba(
252 rgba[0], rgba[1], rgba[2], rgba[3],
253 ));
254 let e = universe
255 .world
256 .add_component(engine::ecs::component::EmissiveComponent::on());
257 let _ = universe.attach(parent, tx);
258 let _ = universe.attach(tx, r);
259 let _ = universe.attach(r, c);
260 let _ = universe.attach(r, e);
261 }
262
263 fn spawn_op_cube(
264 universe: &mut engine::Universe,
265 parent: engine::ecs::ComponentId,
266 pos: (f32, f32, f32),
267 scale: f32,
268 base_rgba: [f32; 4],
269 ) -> engine::ecs::ComponentId {
270 let tx = universe.world.add_component(
271 engine::ecs::component::TransformComponent::new()
272 .with_position(pos.0, pos.1, pos.2)
273 .with_scale(scale, scale, scale),
274 );
275 let r = universe
276 .world
277 .add_component(engine::ecs::component::RenderableComponent::cube());
278 let c = universe
279 .world
280 .add_component(engine::ecs::component::ColorComponent::rgba(
281 base_rgba[0],
282 base_rgba[1],
283 base_rgba[2],
284 base_rgba[3],
285 ));
286 let e = universe
287 .world
288 .add_component(engine::ecs::component::EmissiveComponent::on());
289
290 let _ = universe.attach(parent, tx);
291 let _ = universe.attach(tx, r);
292 let _ = universe.attach(r, c);
293 let _ = universe.attach(r, e);
294
295 tx
296 }
297
298 // --- HUD / lane (single track) ---
299 let lane_x = -2.6_f32;
300 let lane_title_z = -0.4_f32;
301 let lane_cfg_z = -0.4_f32;
302 let lane_pat_z = -1.3_f32;
303 let lane_graph_z = -1.15_f32;
304
305 // Content for pattern/chain/graph starts at x=0; keep section labels aligned there.
306 // This also keeps them from overlapping the config block (which is anchored at lane_x).
307 let lane_labels_x = lane_x + 1.6_f32;
308
309 let track_a_y = 0.9_f32;
310
311 spawn_text(
312 &mut universe,
313 (lane_x, track_a_y + 0.55, lane_title_z),
314 0.09,
315 42,
316 "Track A: AudioOscillator::square()",
317 );
318 spawn_text(
319 &mut universe,
320 (lane_x, track_a_y + 0.25, lane_cfg_z),
321 0.07,
322 48,
323 "oscillators=1\nfrequency_hz=110.0\namplitude=0.12\nenabled=false\nlookahead=0.10s",
324 );
325
326 let viz_root = universe.world.add_component(
327 engine::ecs::component::TransformComponent::new().with_position(0.0, 0.0, 0.0),
328 );
329 universe.add(viz_root);
330
331 println!("[audio-graph-example] viz root added");
332
333 // Small identifier cubes next to titles.
334 spawn_emissive_cube(
335 &mut universe,
336 viz_root,
337 (lane_x - 0.28, track_a_y + 0.55, lane_title_z),
338 0.16,
339 [0.85, 0.40, 1.00, 1.0],
340 );
341
342 // Pattern roots so we can reset via one SetColor action.
343 let track_a_pattern_root = universe.world.add_component(
344 engine::ecs::component::TransformComponent::new().with_position(0.0, 0.0, 0.0),
345 );
346 let _ = universe.attach(viz_root, track_a_pattern_root);
347
348 // Labels for pattern/chain/graph sections.
349 spawn_text(
350 &mut universe,
351 (lane_labels_x, track_a_y, lane_title_z),
352 0.06,
353 42,
354 "pattern",
355 );
356 spawn_text(
357 &mut universe,
358 (lane_labels_x, track_a_y - 0.40, lane_title_z),
359 0.06,
360 42,
361 "graph",
362 );
363
364 // --- Processing chain + graph visualization (compiled graph → cubes + labels) ---
365 use engine::ecs::system::audio_graph_compiler::{
366 AudioGraphCompiler, AudioGraphNode, AudioGraphNodeKind,
367 };
368
369 fn effect_grey_for_depth(depth: usize) -> f32 {
370 // depth=1 => medium grey; deeper => lighter, capped.
371 let base = 0.55;
372 let step = 0.13;
373 let d = depth.saturating_sub(1) as f32;
374 (base + step * d).min(0.92)
375 }
376
377 fn node_rgba(node: &AudioGraphNode, depth: usize) -> [f32; 4] {
378 match node.kind {
379 AudioGraphNodeKind::OscillatorSource { .. } => [1.0, 0.78, 0.22, 1.0],
380 _ => {
381 let g = effect_grey_for_depth(depth);
382 [g, g, g, 1.0]
383 }
384 }
385 }
386
387 fn node_label(node: &AudioGraphNode) -> String {
388 match &node.kind {
389 AudioGraphNodeKind::OscillatorSource { voices } => {
390 format!("OscillatorSource voices={voices}")
391 }
392 AudioGraphNodeKind::Gain { gain } => {
393 format!("Gain gain={gain:.3}")
394 }
395 AudioGraphNodeKind::LowPass {
396 cutoff_hz,
397 resonance,
398 } => {
399 format!("LowPass cutoff={cutoff_hz:.1}Hz res={resonance:.3}")
400 }
401 AudioGraphNodeKind::BandPass {
402 center_hz,
403 bandwidth_octaves,
404 resonance,
405 } => {
406 format!(
407 "BandPass center={center_hz:.1}Hz bw={bandwidth_octaves:.3}oct res={resonance:.3}"
408 )
409 }
410 AudioGraphNodeKind::HighPass {
411 cutoff_hz,
412 resonance,
413 } => {
414 format!("HighPass cutoff={cutoff_hz:.1}Hz res={resonance:.3}")
415 }
416 AudioGraphNodeKind::Limiter {
417 attack_ms,
418 release_ms,
419 threshold,
420 } => {
421 format!("Limiter atk={attack_ms:.1}ms rel={release_ms:.1}ms thr={threshold:.3}")
422 }
423 AudioGraphNodeKind::ClipSource => "ClipSource".to_string(),
424 }
425 }
426
427 fn compute_layout(
428 node: &AudioGraphNode,
429 depth: usize,
430 x_cursor: &mut i32,
431 out: &mut std::collections::HashMap<*const AudioGraphNode, (i32, usize)>,
432 ) -> i32 {
433 // Depth-only layout:
434 // - keep children directly below their parent on X
435 // - use Z sibling offsets (in spawn_graph_tree) to show branching
436 let _ = x_cursor;
437 for ch in node.children.iter() {
438 let _ = compute_layout(ch, depth + 1, x_cursor, out);
439 }
440
441 let my_x = 0;
442 out.insert(node as *const AudioGraphNode, (my_x, depth));
443 my_x
444 }
445
446 fn spawn_graph_tree(
447 universe: &mut engine::Universe,
448 parent: engine::ecs::ComponentId,
449 node: &AudioGraphNode,
450 bp_component: Option<engine::ecs::ComponentId>,
451 bp_label_out: &mut Option<engine::ecs::ComponentId>,
452 origin: (f32, f32, f32),
453 layout: &std::collections::HashMap<*const AudioGraphNode, (i32, usize)>,
454 dx: f32,
455 dy: f32,
456 cube_scale: f32,
457 sibling_index: usize,
458 sibling_count: usize,
459 ) {
460 let Some((x_unit, depth)) = layout.get(&(node as *const AudioGraphNode)).copied() else {
461 return;
462 };
463
464 let x = origin.0 + (x_unit as f32) * dx;
465 let y = origin.1 - (depth as f32) * dy;
466
467 // Push siblings “behind” each other along Z to reduce overlap between
468 // a sibling's cube and another node's label.
469 let dz_sibling = 0.18;
470 let z = origin.2 - (sibling_index as f32) * dz_sibling;
471
472 // Keep text slightly in front of its cube.
473 let z_text = z + 0.03;
474
475 let rgba = node_rgba(node, depth);
476 let cube_tx = spawn_op_cube(universe, parent, (x, y, z), cube_scale, rgba);
477 let _ = cube_tx;
478
479 // Label next to the cube.
480 let label = node_label(node);
481 let tx = universe.world.add_component(
482 engine::ecs::component::TransformComponent::new()
483 .with_position(x + 0.14, y + 0.02, z_text)
484 .with_scale(0.06, 0.06, 1.0),
485 );
486 let t =
487 universe
488 .world
489 .add_component(engine::ecs::component::TextComponent::with_word_wrap(
490 &label, 25,
491 ));
492 let _ = universe.attach(parent, tx);
493 let _ = universe.attach(tx, t);
494
495 if bp_component == Some(node.component) {
496 *bp_label_out = Some(t);
497 }
498
499 let filtering = universe
500 .world
501 .add_component(engine::ecs::component::TextureFilteringComponent::nearest());
502 let _ = universe.attach(t, filtering);
503
504 let emissive = universe
505 .world
506 .add_component(engine::ecs::component::EmissiveComponent::on());
507 let _ = universe.attach(t, emissive);
508
509 universe.add(tx);
510
511 // Mix/branch label if branching.
512 if node.children.len() > 1 {
513 let mut weights: Vec<f32> = Vec::with_capacity(node.children.len());
514 for i in 0..node.children.len() {
515 let w = node
516 .mix
517 .as_ref()
518 .map(|m| m.weights.get(i).copied().unwrap_or(1.0))
519 .unwrap_or(1.0);
520 weights.push(w);
521 }
522 let mix_label = if node.mix.is_some() {
523 format!("Mix weights={weights:?}")
524 } else {
525 format!("Mix <implicit> w={weights:?}")
526 };
527 let mix_tx = universe.world.add_component(
528 engine::ecs::component::TransformComponent::new()
529 .with_position(x + 0.14, y - 0.11, z_text)
530 .with_scale(0.05, 0.05, 1.0),
531 );
532 let mix_t = universe.world.add_component(
533 engine::ecs::component::TextComponent::with_word_wrap(&mix_label, 25),
534 );
535 let _ = universe.attach(parent, mix_tx);
536 let _ = universe.attach(mix_tx, mix_t);
537
538 let filtering = universe
539 .world
540 .add_component(engine::ecs::component::TextureFilteringComponent::nearest());
541 let _ = universe.attach(mix_t, filtering);
542
543 let emissive = universe
544 .world
545 .add_component(engine::ecs::component::EmissiveComponent::on());
546 let _ = universe.attach(mix_t, emissive);
547
548 universe.add(mix_tx);
549 }
550
551 let child_count = node.children.len().max(1);
552 for (i, ch) in node.children.iter().enumerate() {
553 // Each node's children form a sibling group; offset them in Z.
554 // For the root node, sibling_index is 0.
555 let _ = sibling_count;
556 spawn_graph_tree(
557 universe,
558 parent,
559 ch,
560 bp_component,
561 bp_label_out,
562 origin,
563 layout,
564 dx,
565 dy,
566 cube_scale,
567 i,
568 child_count,
569 );
570 }
571 }
572
573 // Compile + display chain and graph for the track.
574 let compiled_a = if audio_enabled {
575 println!("[audio-graph-example] compiling graph...");
576 let Some(osc_a_comp) = osc_a_comp else {
577 panic!("audio_enabled but track A was not created");
578 };
579 let compiled_a =
580 AudioGraphCompiler::compile(&universe.world, osc_a_comp).expect("compile A");
581 println!("[audio-graph-example] graph compiled");
582 Some(compiled_a)
583 } else {
584 println!("[audio-graph-example] audio disabled; skipping graph compilation");
585 None
586 };
587
588 // Full compiled graph visualization (tree layout + mix labels).
589 let mut bp_graph_label_text: Option<engine::ecs::ComponentId> = None;
590 if let Some(compiled_a) = &compiled_a {
591 let mut x_cursor = 0;
592 let mut layout: std::collections::HashMap<*const AudioGraphNode, (i32, usize)> =
593 std::collections::HashMap::new();
594 let _root_x = compute_layout(&compiled_a.root, 0, &mut x_cursor, &mut layout);
595
596 // With depth-only X layout, we keep the tree centered at x=0.
597 // Pull the graph up now that we don't show the separate chain view.
598 let origin = (0.0, track_a_y - 0.40, lane_graph_z);
599
600 spawn_graph_tree(
601 &mut universe,
602 viz_root,
603 &compiled_a.root,
604 bp_a_comp,
605 &mut bp_graph_label_text,
606 origin,
607 &layout,
608 0.45,
609 0.28,
610 0.08,
611 0,
612 1,
613 );
614 } else {
615 spawn_text(
616 &mut universe,
617 (0.0, track_a_y - 0.40, lane_graph_z),
618 0.06,
619 60,
620 "(audio disabled)",
621 );
622 }
623
624 // Animation with 16 keyframes drives scheduled notes + pattern highlights.
625 let anim = universe
626 .world
627 .add_component(engine::ecs::component::AnimationComponent::new());
628
629 let dur_a = 0.85_f32;
630 let beat_spacing = 0.38_f32;
631
632 let a_dark = [0.18, 0.08, 0.22, 1.0];
633 let a_bright = [0.85, 0.40, 1.00, 1.0];
634
635 for i in 0..16 {
636 let kf_beat = i as f64;
637 let kf = universe
638 .world
639 .add_component(engine::ecs::component::KeyframeComponent::new(kf_beat));
640
641 let _ = universe.attach(anim, kf);
642
643 // Scheduled audio note (sample-accurate via lookahead).
644 // Pulse every beat.
645 if audio_enabled {
646 let Some(osc_a_comp) = osc_a_comp else {
647 panic!("audio_enabled but track A was not created");
648 };
649
650 let note_a = engine::ecs::component::MusicNote::c(1, dur_a).with_velocity(0.80);
651
652 let act_a = universe
653 .world
654 .add_component(engine::ecs::component::ActionComponent::new(
655 engine::ecs::IntentValue::AudioSchedulePlay {
656 component_ids: vec![osc_a_comp],
657 beat_offset: 0.0,
658 beat_context: None,
659 note: Some(note_a),
660 gain: None,
661 rate: None,
662 duration: None,
663 },
664 ));
665
666 let _ = universe.attach(kf, act_a);
667 }
668
669 // Keyframed band-pass center.
670 // This is an immediate parameter update applied RT-side (no graph rebuild).
671 if audio_enabled {
672 if let Some(bp_a_comp) = bp_a_comp {
673 let t = (i as f32) / 15.0;
674 let center_hz = 10.0 + t * (1000.0 - 10.0);
675
676 let bp_center =
677 universe
678 .world
679 .add_component(engine::ecs::component::ActionComponent::new(
680 engine::ecs::IntentValue::AudioBandPassSetCenterHz {
681 component_ids: vec![bp_a_comp],
682 center_hz,
683 },
684 ));
685 let _ = universe.attach(kf, bp_center);
686
687 // Update the BandPass node label in the graph visualization.
688 if let Some(text_id) = bp_graph_label_text {
689 let label =
690 universe
691 .world
692 .add_component(engine::ecs::component::ActionComponent::new(
693 engine::ecs::IntentValue::SetText {
694 component_ids: vec![text_id],
695 text: format!("BandPass center={center_hz:.1}Hz"),
696 },
697 ));
698 let _ = universe.attach(kf, label);
699 }
700 }
701 }
702
703 // Visualization reset per keyframe.
704 let reset_a = universe
705 .world
706 .add_component(engine::ecs::component::ActionComponent::new(
707 engine::ecs::IntentValue::SetColor {
708 component_ids: vec![track_a_pattern_root],
709 rgba: a_dark,
710 },
711 ));
712 let _ = universe.attach(kf, reset_a);
713
714 // Pattern cube per step for each track.
715 let x = (kf_beat as f32) * beat_spacing;
716 let cube_a = spawn_op_cube(
717 &mut universe,
718 track_a_pattern_root,
719 (x, track_a_y, lane_pat_z),
720 0.10,
721 a_dark,
722 );
723
724 let bright_a = universe
725 .world
726 .add_component(engine::ecs::component::ActionComponent::new(
727 engine::ecs::IntentValue::SetColor {
728 component_ids: vec![cube_a],
729 rgba: a_bright,
730 },
731 ));
732 let _ = universe.attach(kf, bright_a);
733 }
734 universe.add(anim);
735
736 println!("[audio-graph-example] animation created");
737
738 // Keep window open.
739 println!("[audio-graph-example] processing commands...");
740 universe.systems.process_commands(
741 &mut universe.world,
742 &mut universe.visuals,
743 &mut universe.render_assets,
744 &mut universe.command_queue,
745 );
746
747 println!("[audio-graph-example] commands processed; launching window");
748
749 engine::Windowing::run_app(universe).expect("Windowing failed");
750}Additional examples can be found in:
Sourcepub fn with_color(self, r: f32, g: f32, b: f32) -> Self
pub fn with_color(self, r: f32, g: f32, b: f32) -> Self
Examples found in repository?
examples/vtuber-example.rs (line 65)
11fn main() {
12 mittens_engine::example_support::ensure_model_assets();
13 utils::logger::init();
14
15 let world = engine::ecs::World::default();
16 let mut universe = engine::Universe::new(world);
17
18 // Light pink background.
19 let background = universe
20 .world
21 .add_component(BackgroundColorComponent::new());
22 let background_c = universe
23 .world
24 .add_component(ColorComponent::rgba(1.0, 0.82, 0.90, 1.0));
25 let _ = universe.world.add_child(background, background_c);
26 universe.add(background);
27
28 // Small ambient so shadowed areas aren't pure black.
29 let ambient = universe
30 .world
31 .add_component(AmbientLightComponent::rgb(0.10, 0.10, 0.12));
32 universe.add(ambient);
33
34 // --- Camera rig (WASD + mouse) ---
35 // InputComponent is the root, and it owns a Transform (the camera rig).
36 let input = universe
37 .world
38 .add_component(InputComponent::new().with_speed(1.5));
39 let input_mode = universe.world.add_component(
40 InputTransformModeComponent::forward_z()
41 .with_fps_rotation()
42 .with_roll_axis_y(),
43 );
44 let _ = universe.attach(input, input_mode);
45
46 // Start slightly pulled back looking towards the origin.
47 let rig_transform = universe
48 .world
49 .add_component(TransformComponent::new().with_position(0.0, 0.0, 6.0));
50 let _ = universe.attach(input, rig_transform);
51
52 let camera3d = universe.world.add_component(Camera3DComponent::new());
53 let _ = universe.attach(rig_transform, camera3d);
54
55 // Topology: I { T { C3D } } — add a small camera-attached controls hint.
56 example_util::spawn_desktop_camera_controls_hint(&mut universe, rig_transform);
57
58 universe.add(input);
59
60 // --- lighting ---
61 // Directional key light (slightly down + forward Z).
62 let sun = universe.world.add_component(
63 DirectionalLightComponent::new()
64 .with_intensity(1.2)
65 .with_color(1.0, 0.98, 0.94),
66 );
67 let sun_dir = universe
68 .world
69 .add_component(TransformComponent::new().with_position(0.0, -0.35, 1.0));
70 let _ = universe.attach(sun_dir, sun);
71 universe.add(sun_dir);
72
73 let light_transform = universe.world.add_component(
74 TransformComponent::new()
75 .with_position(1.0, 6.0, 3.0)
76 .with_scale(0.1, 0.1, 0.1),
77 );
78 let light = universe.world.add_component(
79 engine::ecs::component::PointLightComponent::new()
80 .with_distance(120.0)
81 .with_color(1.0, 1.0, 1.0),
82 );
83 let _ = universe.attach(light_transform, light);
84 universe.add(light_transform);
85
86 // --- VTuber model ---
87 let model_root = universe.world.add_component(TransformComponent::new());
88 let model = universe
89 .world
90 .add_component(GLTFComponent::new("assets/models/pc-rei.hoodie.glb"));
91 // emissive for pc-rei
92 let emissive = universe
93 .world
94 .add_component(engine::ecs::component::EmissiveComponent::on());
95
96 let _ = universe.attach(model, emissive);
97
98 let xr_input = universe.world.add_component(InputXRComponent::on());
99 let xr_gamepad = universe
100 .world
101 .add_component(engine::ecs::component::InputXRGamepadComponent::new().speed(1.5));
102 let xr_head = universe.world.add_component(TransformComponent::new());
103 let xr_camera = universe.world.add_component(CameraXRComponent::on());
104 let _ = universe.attach(xr_input, xr_head);
105 let _ = universe.attach(xr_input, xr_gamepad);
106 let _ = universe.attach(xr_head, xr_camera);
107 let _ = universe.attach(xr_head, model_root);
108
109 let _ = universe.attach(model_root, model);
110 universe.add(xr_input);
111 universe.add(model_root);
112
113 // --- Background clouds (occluded + lit) ---
114 let bg_root = universe.world.add_component(
115 engine::ecs::component::BackgroundComponent::new().with_occlusion_and_lighting(),
116 );
117 universe.add(bg_root);
118 let mut cloud_params = example_util::CloudRingParams::default();
119 cloud_params.cloud_count = 8; // +3 clusters
120 cloud_params.angle_jitter = 0.35;
121 cloud_params.high_y_probability = 0.5;
122 cloud_params.high_y_multiplier = 1.5;
123 cloud_params.seed = 0x57_55_B0_01u32;
124 example_util::spawn_cloud_ring(&mut universe, bg_root, cloud_params);
125
126 // --- Simple environment ---
127 let spawn_cube = |universe: &mut engine::Universe,
128 position: (f32, f32, f32),
129 scale: (f32, f32, f32),
130 color: (f32, f32, f32, f32)| {
131 let transform = universe.world.add_component(
132 TransformComponent::new()
133 .with_position(position.0, position.1, position.2)
134 .with_scale(scale.0, scale.1, scale.2),
135 );
136 let renderable = universe.world.add_component(RenderableComponent::cube());
137 let color = universe
138 .world
139 .add_component(ColorComponent::rgba(color.0, color.1, color.2, color.3));
140
141 let _ = universe.attach(transform, renderable);
142 let _ = universe.attach(renderable, color);
143
144 universe.add(transform);
145 };
146
147 // floor
148 spawn_cube(
149 &mut universe,
150 (0.0, -0.05, 0.0),
151 (10.0, 0.1, 10.0),
152 (0.92, 0.92, 0.92, 1.0),
153 );
154
155 // back wall
156 spawn_cube(
157 &mut universe,
158 (0.0, 1.5, -5.0),
159 (10.0, 3.0, 1.0),
160 (0.95, 0.94, 0.96, 1.0),
161 );
162
163 // desk
164 spawn_cube(
165 &mut universe,
166 (0.0, 0.35, 1.0),
167 (1.0, 0.75, 0.5),
168 (0.75, 0.70, 0.65, 1.0),
169 );
170
171 let xr_root = universe
172 .world
173 .add_component(engine::ecs::component::XrComponent::on());
174 universe.add(xr_root);
175
176 universe.systems.process_commands(
177 &mut universe.world,
178 &mut universe.visuals,
179 &mut universe.render_assets,
180 &mut universe.command_queue,
181 );
182
183 universe.enable_repl();
184 engine::Windowing::run_app(universe).expect("Windowing failed");
185}More examples
examples/background-occlusion-example.rs (line 77)
19fn main() {
20 mittens_engine::example_support::ensure_model_assets();
21 utils::logger::init();
22
23 let world = engine::ecs::World::default();
24 let mut universe = engine::Universe::new(world);
25
26 // Light purple-red background so the occlusion reads.
27 let clear = universe
28 .world
29 .add_component(engine::ecs::component::BackgroundColorComponent::new());
30 let clear_c = universe
31 .world
32 .add_component(engine::ecs::component::ColorComponent::rgba(
33 0.62, 0.38, 0.56, 1.0,
34 ));
35 let _ = universe.world.add_child(clear, clear_c);
36 universe.add(clear);
37
38 // A bit of ambient so the cluster volume reads.
39 let ambient = universe
40 .world
41 .add_component(engine::ecs::component::AmbientLightComponent::rgb(
42 0.20, 0.15, 0.3,
43 ));
44 universe.add(ambient);
45
46 // --- Camera rig (WASD/QE) ---
47 let input = universe
48 .world
49 .add_component(engine::ecs::component::InputComponent::new().with_speed(2.0));
50 let input_mode = universe.world.add_component(
51 engine::ecs::component::InputTransformModeComponent::forward_z().with_roll_axis_y(),
52 );
53 let _ = universe.attach(input, input_mode);
54
55 let rig_transform = universe.world.add_component(
56 engine::ecs::component::TransformComponent::new().with_position(0.0, 0.0, 6.0),
57 );
58 let _ = universe.attach(input, rig_transform);
59
60 let camera3d = universe.world.add_component(
61 engine::ecs::component::Camera3DComponent::new()
62 .with_far(200.0)
63 .with_fov(70.0),
64 );
65 let _ = universe.attach(rig_transform, camera3d);
66
67 // Topology: I { T { C3D } } — add a small camera-attached controls hint.
68 example_util::spawn_desktop_camera_controls_hint(&mut universe, rig_transform);
69
70 // Key directional light toward the cloud volume.
71 //
72 // Directional lights encode their direction in the node's world position.
73 // The shader normalizes this vector.
74 let sun = universe.world.add_component(
75 engine::ecs::component::DirectionalLightComponent::new()
76 .with_intensity(1.6)
77 .with_color(1.0, 0.98, 0.92),
78 );
79 // Pointing from the clouds back toward the camera a bit (+Z), and slightly from above.
80 let sun_dir = universe.world.add_component(
81 engine::ecs::component::TransformComponent::new().with_position(0.15, 0.65, 0.75),
82 );
83 let _ = universe.attach(sun_dir, sun);
84
85 // A couple point lights to fill/shade the cloud volume.
86 let light_a = universe.world.add_component(
87 engine::ecs::component::PointLightComponent::new()
88 .with_distance(80.0)
89 .with_intensity(3.0)
90 .with_color(0.9, 0.95, 1.0),
91 );
92 let light_a_tx = universe.world.add_component(
93 engine::ecs::component::TransformComponent::new().with_position(8.0, 8.0, 5.0),
94 );
95 let _ = universe.attach(light_a_tx, light_a);
96
97 let light_b = universe.world.add_component(
98 engine::ecs::component::PointLightComponent::new()
99 .with_distance(80.0)
100 .with_intensity(2.2)
101 .with_color(1.0, 0.9, 0.85),
102 );
103 let light_b_tx = universe.world.add_component(
104 engine::ecs::component::TransformComponent::new().with_position(-10.0, 4.0, -6.0),
105 );
106 let _ = universe.attach(light_b_tx, light_b);
107
108 universe.add(input);
109 universe.add(sun_dir);
110 universe.add(light_a_tx);
111 universe.add(light_b_tx);
112
113 let cube_mesh = universe
114 .render_assets
115 .get_mesh(engine::graphics::BuiltinMeshType::Cube);
116
117 // --- Background occluded+lit world ---
118 // Renderables under this node participate in a background stage that depth-writes for
119 // self-occlusion + uses the normal lighting shader inputs.
120 let bg_root = universe.world.add_component(
121 engine::ecs::component::BackgroundComponent::new().with_occlusion_and_lighting(),
122 );
123 universe.add(bg_root);
124
125 // Create overlapping cube clusters like cloud puffs.
126 // Place them generally in front of the camera (negative Z).
127 // Spawn two groups on opposite X sides.
128 let cluster_count: u32 = 9;
129 for (group_i, group_x) in [-16.0_f32, 16.0_f32].into_iter().enumerate() {
130 let group_tx = universe.world.add_component(
131 engine::ecs::component::TransformComponent::new().with_position(group_x, 0.0, 0.0),
132 );
133 let _ = universe.attach(bg_root, group_tx);
134
135 for cluster_i in 0..cluster_count {
136 let seed = (0xC10u32 ^ (group_i as u32).wrapping_mul(0x9E37_79B9))
137 ^ (cluster_i.wrapping_mul(7919));
138
139 let cx = (rand01(seed ^ 0xA341_316C) - 0.5) * 18.0;
140 let cy = (rand01(seed ^ 0xC801_3EA4) - 0.5) * 10.0 + 2.0;
141 let cz = -18.0 - rand01(seed ^ 0xB529_7A4D) * 26.0;
142
143 let center_tx = universe.world.add_component(
144 engine::ecs::component::TransformComponent::new().with_position(cx, cy, cz),
145 );
146 let _ = universe.attach(group_tx, center_tx);
147
148 let puffs = 18u32;
149 for puff_i in 0..puffs {
150 let puff_seed = seed ^ puff_i.wrapping_mul(1_103_515_245);
151
152 // Slightly ellipsoidal distribution.
153 let ox = (rand01(puff_seed ^ 0x68bc_21eb) - 0.5) * 7.0;
154 let oy = (rand01(puff_seed ^ 0x02e5_be93) - 0.5) * 3.0;
155 let oz = (rand01(puff_seed ^ 0xa1d3_4f2b) - 0.5) * 7.0;
156
157 let base = 0.7 + rand01(puff_seed ^ 0x9e37_79b9) * 2.6;
158 let sx = base * (0.7 + rand01(puff_seed ^ 0x243f_6a88) * 0.8);
159 let sy = base * (0.6 + rand01(puff_seed ^ 0x85a3_08d3) * 0.9);
160 let sz = base * (0.7 + rand01(puff_seed ^ 0x1319_8a2e) * 0.8);
161
162 let tx = universe.world.add_component(
163 engine::ecs::component::TransformComponent::new()
164 .with_position(ox, oy, oz)
165 .with_scale(sx, sy, sz),
166 );
167 let renderable =
168 universe
169 .world
170 .add_component(engine::ecs::component::RenderableComponent::new(
171 engine::graphics::primitives::Renderable::new(
172 cube_mesh,
173 engine::graphics::primitives::MaterialHandle::TOON_MESH,
174 ),
175 ));
176
177 // Slight blue-grey variation.
178 let t = rand01(puff_seed ^ 0x7f4a_7c15);
179 let r = 0.55 + 0.10 * t;
180 let g = 0.58 + 0.10 * t;
181 let b = 0.66 + 0.12 * t;
182 let color = universe
183 .world
184 .add_component(engine::ecs::component::ColorComponent::rgba(r, g, b, 1.0));
185
186 let _ = universe.attach(center_tx, tx);
187 let _ = universe.attach(tx, renderable);
188 let _ = universe.attach(renderable, color);
189 }
190 }
191 }
192
193 // Foreground reference cube (should never be occluded by background depth).
194 let fg_tx = universe.world.add_component(
195 engine::ecs::component::TransformComponent::new()
196 .with_position(0.0, -0.5, -4.0)
197 .with_scale(1.2, 0.8, 1.2),
198 );
199 let fg_renderable =
200 universe
201 .world
202 .add_component(engine::ecs::component::RenderableComponent::new(
203 engine::graphics::primitives::Renderable::new(
204 cube_mesh,
205 engine::graphics::primitives::MaterialHandle::TOON_MESH,
206 ),
207 ));
208 let fg_color = universe
209 .world
210 .add_component(engine::ecs::component::ColorComponent::rgba(
211 0.9, 0.2, 0.2, 1.0,
212 ));
213
214 universe.add(fg_tx);
215 let _ = universe.attach(fg_tx, fg_renderable);
216 let _ = universe.attach(fg_renderable, fg_color);
217
218 // Foreground opaque floor: 16x16 larger white cubes, 10 units below the reference cube.
219 let floor_root = universe.world.add_component(
220 engine::ecs::component::TransformComponent::new().with_position(0.0, -10.5, -10.0),
221 );
222 universe.add(floor_root);
223
224 let spacing = 10_f32;
225 let half = 5_f32;
226 for x in 0..16u32 {
227 for z in 0..64u32 {
228 let px = (x as f32 - half) * spacing;
229 let pz = (z as f32 * -1.0 + half) * spacing;
230 let tx = universe.world.add_component(
231 engine::ecs::component::TransformComponent::new()
232 .with_position(px, -5.0, pz)
233 .with_scale(5.0, 0.5, 5.0),
234 );
235 let renderable =
236 universe
237 .world
238 .add_component(engine::ecs::component::RenderableComponent::new(
239 engine::graphics::primitives::Renderable::new(
240 cube_mesh,
241 engine::graphics::primitives::MaterialHandle::TOON_MESH,
242 ),
243 ));
244 let color = universe
245 .world
246 .add_component(engine::ecs::component::ColorComponent::rgba(
247 1.0, 1.0, 1.0, 1.0,
248 ));
249
250 let _ = universe.attach(floor_root, tx);
251 let _ = universe.attach(tx, renderable);
252 let _ = universe.attach(renderable, color);
253 }
254 }
255
256 universe.systems.process_commands(
257 &mut universe.world,
258 &mut universe.visuals,
259 &mut universe.render_assets,
260 &mut universe.command_queue,
261 );
262
263 universe.enable_repl();
264 engine::Windowing::run_app(universe).expect("Windowing failed");
265}examples/vr-input.rs (line 251)
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}examples/font-example.rs (line 41)
13fn main() {
14 mittens_engine::example_support::ensure_model_assets();
15 utils::logger::init();
16
17 let world = engine::ecs::World::default();
18 let mut universe = engine::Universe::new(world);
19
20 // Dark background so the font texture pops.
21 let background = universe
22 .world
23 .add_component(BackgroundColorComponent::new());
24 let background_c = universe
25 .world
26 .add_component(ColorComponent::rgba(0.20, 0.2, 0.20, 1.0));
27 let _ = universe.world.add_child(background, background_c);
28 universe.add(background);
29
30 // Ambient so text is readable even without explicit lights.
31 let ambient = universe
32 .world
33 .add_component(AmbientLightComponent::rgb(0.50, 0.50, 0.50));
34 universe.add(ambient);
35
36 let directional_tx = universe
37 .world
38 .add_component(TransformComponent::new().with_position(0.0, 0.5, 1.0));
39 let directional_light = universe.world.add_component(
40 engine::ecs::component::DirectionalLightComponent::new()
41 .with_color(1.0, 1.0, 1.0)
42 .with_intensity(0.8),
43 );
44 let _ = universe.attach(directional_tx, directional_light);
45 universe.add(directional_tx);
46
47 // --- background clouds ---
48 // Background stage (occluded + lit) so the cloud volume self-occludes but won't occlude
49 // the foreground text (renderer clears depth before foreground).
50 let bg_root = universe
51 .world
52 .add_component(BackgroundComponent::new().with_occlusion_and_lighting());
53 universe.add(bg_root);
54
55 let mut bg_cloud_params = example_util::CloudRingParams::default();
56 bg_cloud_params.cloud_count = 6;
57 bg_cloud_params.seed = 0xF0_17_C10u32;
58 example_util::spawn_cloud_ring(&mut universe, bg_root, bg_cloud_params);
59
60 // I {
61 // // not fps rotation, just relative rotation
62 // with_forward_z()
63 // with_roll_axis_y()
64 // C3D {}
65 // }
66 let input = universe
67 .world
68 .add_component(InputComponent::new().with_speed(2.0));
69 let input_mode = universe
70 .world
71 .add_component(InputTransformModeComponent::forward_z().with_roll_axis_y());
72 let _ = universe.attach(input, input_mode);
73
74 let rig_transform = universe
75 .world
76 .add_component(TransformComponent::new().with_position(1.8, -0.5, 2.5));
77 let _ = universe.attach(input, rig_transform);
78
79 let camera = universe.world.add_component(Camera3DComponent::new());
80 let _ = universe.attach(rig_transform, camera);
81
82 // Click-to-pick: treat this camera rig as a pointer source.
83 let pointer = universe.world.add_component(PointerComponent::new());
84 let _ = universe.attach(camera, pointer);
85
86 // Topology: I { T { C3D } } — add a small camera-attached controls hint.
87 example_util::spawn_desktop_camera_controls_hint(&mut universe, rig_transform);
88
89 universe.add(input);
90
91 // --- foreground clouds ---
92 // Normal foreground renderables (not background stage).
93 // Offset the ring forward (negative Z) so several clusters are in view.
94 let fg_cloud_root = universe
95 .world
96 .add_component(TransformComponent::new().with_position(0.0, -6.0, -10.0));
97 universe.add(fg_cloud_root);
98
99 let mut fg_cloud_params = example_util::CloudRingParams::default();
100 fg_cloud_params.cloud_count = 4;
101 fg_cloud_params.radius = 9.0;
102 fg_cloud_params.center_y = 1.0;
103 fg_cloud_params.puffs_per_cloud = 22;
104 fg_cloud_params.angle_jitter = 0.35;
105 fg_cloud_params.high_y_probability = 0.35;
106 fg_cloud_params.high_y_multiplier = 1.4;
107 fg_cloud_params.seed = 0xF0_17_C102u32;
108 example_util::spawn_cloud_ring(&mut universe, fg_cloud_root, fg_cloud_params);
109
110 // T {
111 // with_translation(0,0, -2)
112 // TXT {
113 // "ababaabbaabbaaabbbaaabbbaaaabbbbaaaaabbbbbababababa"
114 // TextureComponent { assets/images/test.font_system.png }
115 // }
116 // }
117 fn estimate_text_height_world(text: &str, scale: f32) -> f32 {
118 let line_count = text.lines().count().max(1) as f32;
119 // Text quads are ~1 unit tall per line in text-space.
120 // Add some padding so blocks don't feel cramped.
121 let pad_lines = 1.25;
122 (line_count + pad_lines) * scale
123 }
124
125 fn spawn_text_block(
126 universe: &mut engine::Universe,
127 position: (f32, f32, f32),
128 scale: f32,
129 text: &str,
130 font_texture_uri: Option<&str>,
131 text_color_rgba: Option<[f32; 4]>,
132 shadow: Option<TextShadowComponent>,
133 filtering: TextureFilteringComponent,
134 ) -> f32 {
135 // T_root { T_scale { [Color] { TXT { shadow, filtering } } } }
136 let text_root = universe.world.add_component(
137 TransformComponent::new().with_position(position.0, position.1, position.2),
138 );
139
140 let text_scale = universe
141 .world
142 .add_component(TransformComponent::new().with_scale(scale, scale, 1.0));
143 let _ = universe.attach(text_root, text_scale);
144
145 let text_parent = if let Some([r, g, b, a]) = text_color_rgba {
146 let color = universe
147 .world
148 .add_component(ColorComponent::rgba(r, g, b, a));
149 let _ = universe.attach(text_scale, color);
150 color
151 } else {
152 text_scale
153 };
154
155 let text_c = universe.world.add_component(TextComponent::new(text));
156 let _ = universe.attach(text_parent, text_c);
157
158 // Explicit opt-in: make the glyph renderables pickable.
159 // TextSystem will propagate this to all spawned glyph quads.
160 let raycastable = universe
161 .world
162 .add_component(RaycastableComponent::enabled());
163 let _ = universe.attach(text_c, raycastable);
164
165 // Route glyph quads into the alpha-to-coverage cutout pass.
166 let cutout = universe
167 .world
168 .add_component(TransparentCutoutComponent::new());
169 let _ = universe.attach(text_c, cutout);
170
171 if let Some(shadow) = shadow {
172 let shadow_id = universe.world.add_component(shadow);
173 let _ = universe.attach(text_c, shadow_id);
174 }
175
176 // Optional: override the font atlas for this text block.
177 // TextSystem will propagate this to all glyph renderables.
178 if let Some(uri) = font_texture_uri {
179 let tex = universe
180 .world
181 .add_component(TextureComponent::with_uri(uri));
182 let _ = universe.attach(text_c, tex);
183 }
184
185 let filtering_id = universe.world.add_component(filtering);
186 let _ = universe.attach(text_c, filtering_id);
187
188 universe.add(text_root);
189
190 estimate_text_height_world(text, scale)
191 }
192
193 // --- text blocks ---
194 // Multi-line samples at different scales.
195 // (Text literals omitted in the README/snippet; see constants below.)
196 const TEXT_BIG: &str = "CAT ENGINE\nfont example\nBIG TEXT";
197 const TEXT_MED: &str = "multi-line\ntext block\nmedium";
198 const TEXT_SMALL: &str = "small\ntext";
199 const TEXT_TINY: &str = "tiny\ntext\n(zoom in)";
200
201 // Stack vertically; advance by (measured height + gap) so big text gets more room.
202 let x = -1.2;
203 let z = -2.0;
204 let mut y = 1.2;
205 let gap = 0.15;
206
207 let shadow_crisp = Some(
208 TextShadowComponent::new()
209 .with_scale(1.35)
210 .with_offset([0.06, -0.06, 0.0015]),
211 );
212
213 y -= spawn_text_block(
214 &mut universe,
215 (x, y, z),
216 0.55,
217 TEXT_BIG,
218 None,
219 Some([1.0, 1.0, 1.0, 1.0]),
220 shadow_crisp,
221 TextureFilteringComponent::nearest_magnification(),
222 ) + gap;
223 y -= spawn_text_block(
224 &mut universe,
225 (x, y, z),
226 0.25,
227 TEXT_MED,
228 None,
229 Some([0.60, 0.95, 1.00, 1.0]),
230 Some(
231 TextShadowComponent::new()
232 .with_rgba([0.0, 0.0, 0.15, 1.0])
233 .with_scale(1.20)
234 .with_offset([0.05, -0.04, 0.0015]),
235 ),
236 TextureFilteringComponent::linear(),
237 ) + gap;
238 y -= spawn_text_block(
239 &mut universe,
240 (x, y, z),
241 0.14,
242 TEXT_SMALL,
243 None,
244 Some([1.0, 0.88, 0.35, 1.0]),
245 Some(
246 TextShadowComponent::new()
247 .with_rgba([0.15, 0.0, 0.0, 1.0])
248 .with_scale(1.55)
249 .with_offset([0.08, -0.08, 0.0015]),
250 ),
251 TextureFilteringComponent::nearest(),
252 ) + gap;
253 let _ = spawn_text_block(
254 &mut universe,
255 (x, y, z),
256 0.08,
257 TEXT_TINY,
258 None,
259 Some([0.90, 1.0, 0.70, 1.0]),
260 shadow_crisp,
261 TextureFilteringComponent::nearest_magnification(),
262 );
263
264 // Left block: explicit multi-line text using the default font_system atlas.
265 const TEXT_LEFT: &str = "even though there's hexes\nto the solar plexus in my lexus\ni'm feelin' reckless,\nwhen i'm eating breakfast";
266 let _ = spawn_text_block(
267 &mut universe,
268 (x - 8.1, 1.1, z),
269 0.22,
270 TEXT_LEFT,
271 Some("assets/textures/font_system.dds"),
272 Some([0.95, 0.95, 0.95, 1.0]),
273 Some(
274 TextShadowComponent::new()
275 .with_scale(1.25)
276 .with_offset([0.05, -0.05, 0.0015]),
277 ),
278 TextureFilteringComponent::nearest_magnification(),
279 );
280
281 // Alt atlas: put it *behind* the original stack (slightly farther from the camera)
282 // and tint it dark grey.
283 let alt_atlas = Some("assets/textures/font_system.0.0.dds");
284 let alt_z = z - 0.05;
285 let alt_grey = Some([0.25, 0.25, 0.25, 1.0]);
286
287 let mut y_alt = 1.2;
288 y_alt -= spawn_text_block(
289 &mut universe,
290 (x, y_alt, alt_z),
291 0.55,
292 TEXT_BIG,
293 alt_atlas,
294 alt_grey,
295 Some(
296 TextShadowComponent::new()
297 .with_rgba([0.0, 0.0, 0.0, 1.0])
298 .with_scale(1.15)
299 .with_offset([0.03, -0.03, 0.0015]),
300 ),
301 TextureFilteringComponent::linear(),
302 ) + gap;
303 y_alt -= spawn_text_block(
304 &mut universe,
305 (x, y_alt, alt_z),
306 0.25,
307 TEXT_MED,
308 alt_atlas,
309 alt_grey,
310 Some(
311 TextShadowComponent::new()
312 .with_rgba([0.0, 0.0, 0.0, 1.0])
313 .with_scale(1.15)
314 .with_offset([0.03, -0.03, 0.0015]),
315 ),
316 TextureFilteringComponent::linear(),
317 ) + gap;
318 y_alt -= spawn_text_block(
319 &mut universe,
320 (x, y_alt, alt_z),
321 0.14,
322 TEXT_SMALL,
323 alt_atlas,
324 alt_grey,
325 Some(
326 TextShadowComponent::new()
327 .with_rgba([0.0, 0.0, 0.0, 1.0])
328 .with_scale(1.15)
329 .with_offset([0.03, -0.03, 0.0015]),
330 ),
331 TextureFilteringComponent::linear(),
332 ) + gap;
333 let _ = spawn_text_block(
334 &mut universe,
335 (x, y_alt, alt_z),
336 0.08,
337 TEXT_TINY,
338 alt_atlas,
339 alt_grey,
340 Some(
341 TextShadowComponent::new()
342 .with_rgba([0.0, 0.0, 0.0, 1.0])
343 .with_scale(1.15)
344 .with_offset([0.03, -0.03, 0.0015]),
345 ),
346 TextureFilteringComponent::linear(),
347 );
348
349 universe.enable_repl();
350
351 let xr_input = universe.world.add_component(InputXRComponent::on());
352 let xr_gamepad = universe
353 .world
354 .add_component(engine::ecs::component::InputXRGamepadComponent::new().speed(1.5));
355 let xr_head = universe.world.add_component(TransformComponent::new());
356 let xr_camera = universe.world.add_component(CameraXRComponent::on());
357 let _ = universe.attach(xr_input, xr_head);
358 let _ = universe.attach(xr_input, xr_gamepad);
359 let _ = universe.attach(xr_head, xr_camera);
360 universe.add(xr_input);
361
362 // Add an OpenXR component so OpenXRSystem initializes and starts polling events.
363 let xr_root = universe
364 .world
365 .add_component(engine::ecs::component::XrComponent::on());
366 universe.add(xr_root);
367
368 // Process init-time registrations (Text expands into glyph subtrees here).
369 universe.systems.process_commands(
370 &mut universe.world,
371 &mut universe.visuals,
372 &mut universe.render_assets,
373 &mut universe.command_queue,
374 );
375
376 engine::Windowing::run_app(universe).expect("Windowing failed");
377}examples/vtuber-joints-example.rs (line 76)
14fn main() {
15 mittens_engine::example_support::ensure_model_assets();
16 utils::logger::init();
17
18 let world = engine::ecs::World::default();
19 let mut universe = engine::Universe::new(world);
20
21 // Slow the global beat clock so beat-based animations run half as fast.
22 let clock = universe
23 .world
24 .add_component(ClockComponent::new().with_bpm(60.0));
25 universe.add(clock);
26
27 // Light pink background.
28 let background = universe
29 .world
30 .add_component(BackgroundColorComponent::new());
31 let background_c = universe
32 .world
33 .add_component(ColorComponent::rgba(1.0, 0.82, 0.90, 1.0));
34 let _ = universe.world.add_child(background, background_c);
35 universe.add(background);
36
37 // Small ambient so shadowed areas aren't pure black.
38 let ambient = universe
39 .world
40 .add_component(AmbientLightComponent::rgb(0.10, 0.10, 0.12));
41 universe.add(ambient);
42
43 // --- Camera rig (WASD + mouse) ---
44 let input = universe
45 .world
46 .add_component(InputComponent::new().with_speed(1.5));
47 let input_mode = universe.world.add_component(
48 InputTransformModeComponent::forward_z()
49 .with_fps_rotation()
50 .with_roll_axis_y(),
51 );
52 let _ = universe.attach(input, input_mode);
53
54 // Start slightly pulled back looking towards the origin.
55 let rig_transform = universe
56 .world
57 .add_component(TransformComponent::new().with_position(0.0, 0.0, 6.0));
58 let _ = universe.attach(input, rig_transform);
59
60 let camera3d = universe.world.add_component(Camera3DComponent::new());
61 let _ = universe.attach(rig_transform, camera3d);
62
63 // Pointer so gizmos can be interacted with.
64 let pointer = universe.world.add_component(PointerComponent::new());
65 let _ = universe.attach(camera3d, pointer);
66
67 // Topology: I { T { C3D } } — add a small camera-attached controls hint.
68 example_util::spawn_desktop_camera_controls_hint(&mut universe, rig_transform);
69
70 universe.add(input);
71
72 // --- lighting ---
73 let sun = universe.world.add_component(
74 DirectionalLightComponent::new()
75 .with_intensity(1.2)
76 .with_color(1.0, 0.98, 0.94),
77 );
78 let sun_dir = universe
79 .world
80 .add_component(TransformComponent::new().with_position(0.0, -0.35, 1.0));
81 let _ = universe.attach(sun_dir, sun);
82 universe.add(sun_dir);
83
84 let light_transform = universe.world.add_component(
85 TransformComponent::new()
86 .with_position(1.0, 6.0, 3.0)
87 .with_scale(0.1, 0.1, 0.1),
88 );
89 let light = universe.world.add_component(
90 engine::ecs::component::PointLightComponent::new()
91 .with_distance(120.0)
92 .with_color(1.0, 1.0, 1.0),
93 );
94 let _ = universe.attach(light_transform, light);
95 universe.add(light_transform);
96
97 // --- VTuber model ---
98 let model_uri = "assets/models/pc-rei.hoodie.glb";
99
100 // Wrap the model subtree in an editor root so transform-only glTF nodes can be visualized
101 // (and thus raycasted/selected) without affecting non-editor scenes.
102 let editor_root = universe.world.add_component(EditorComponent::new());
103
104 let model_root = universe.world.add_component(TransformComponent::new());
105 let model = universe.world.add_component(GLTFComponent::new(model_uri));
106
107 // emissive for pc-rei
108 let emissive = universe.world.add_component(EmissiveComponent::on());
109 let _ = universe.attach(model, emissive);
110
111 let xr_input = universe.world.add_component(InputXRComponent::on());
112 let xr_gamepad = universe
113 .world
114 .add_component(engine::ecs::component::InputXRGamepadComponent::new().speed(1.5));
115 let xr_head = universe.world.add_component(TransformComponent::new());
116 let xr_camera = universe.world.add_component(CameraXRComponent::on());
117 let _ = universe.attach(xr_input, xr_head);
118 let _ = universe.attach(xr_input, xr_gamepad);
119 let _ = universe.attach(xr_head, xr_camera);
120 let xr_pointer = universe.world.add_component(PointerComponent::new());
121 let _ = universe.attach(xr_camera, xr_pointer);
122 let _ = universe.attach(xr_head, editor_root);
123
124 let _ = universe.attach(model_root, model);
125
126 let _ = universe.attach(editor_root, model_root);
127
128 // Initialize the editor root so GLTFComponent gets registered.
129 universe.add(xr_input);
130 universe.add(editor_root);
131
132 // --- Background clouds (occluded + lit) ---
133 let bg_root = universe.world.add_component(
134 engine::ecs::component::BackgroundComponent::new().with_occlusion_and_lighting(),
135 );
136 universe.add(bg_root);
137 let mut cloud_params = example_util::CloudRingParams::default();
138 cloud_params.cloud_count = 8; // +3 clusters
139 cloud_params.angle_jitter = 0.35;
140 cloud_params.high_y_probability = 0.5;
141 cloud_params.high_y_multiplier = 1.5;
142 cloud_params.seed = 0x57_55_B0_01u32;
143 example_util::spawn_cloud_ring(&mut universe, bg_root, cloud_params);
144
145 // --- Simple environment ---
146 let spawn_cube = |universe: &mut engine::Universe,
147 position: (f32, f32, f32),
148 scale: (f32, f32, f32),
149 color: (f32, f32, f32, f32)| {
150 let transform = universe.world.add_component(
151 TransformComponent::new()
152 .with_position(position.0, position.1, position.2)
153 .with_scale(scale.0, scale.1, scale.2),
154 );
155 let renderable = universe.world.add_component(RenderableComponent::cube());
156 let color = universe
157 .world
158 .add_component(ColorComponent::rgba(color.0, color.1, color.2, color.3));
159
160 let _ = universe.attach(transform, renderable);
161 let _ = universe.attach(renderable, color);
162
163 universe.add(transform);
164 };
165
166 // floor
167 spawn_cube(
168 &mut universe,
169 (0.0, -0.05, 0.0),
170 (10.0, 0.1, 10.0),
171 (0.92, 0.92, 0.92, 1.0),
172 );
173
174 // back wall
175 spawn_cube(
176 &mut universe,
177 (-3.0, 1.5, -5.0),
178 (3.0, 3.0, 1.0),
179 (0.95, 0.94, 0.96, 1.0),
180 );
181
182 // desk
183 spawn_cube(
184 &mut universe,
185 (0.0, 0.35, 1.0),
186 (1.0, 0.75, 0.5),
187 (0.75, 0.70, 0.65, 1.0),
188 );
189
190 // --- Editor-side stacked cubes (inside the editor subtree for picking/gizmos) ---
191 {
192 let spawn_editor_cube = |universe: &mut engine::Universe,
193 editor_root: engine::ecs::ComponentId,
194 name: &str,
195 position: (f32, f32, f32),
196 scale: (f32, f32, f32),
197 color: (f32, f32, f32, f32)| {
198 let transform = universe.world.add_component_boxed_named(
199 format!("{name}_t"),
200 Box::new(
201 TransformComponent::new()
202 .with_position(position.0, position.1, position.2)
203 .with_scale(scale.0, scale.1, scale.2),
204 ),
205 );
206 let renderable = universe.world.add_component_boxed_named(
207 format!("{name}_r"),
208 Box::new(RenderableComponent::cube()),
209 );
210 let color_comp = universe.world.add_component_boxed_named(
211 format!("{name}_color"),
212 Box::new(ColorComponent::rgba(color.0, color.1, color.2, color.3)),
213 );
214 let raycastable = universe.world.add_component_boxed_named(
215 format!("{name}_raycastable"),
216 Box::new(RaycastableComponent::enabled()),
217 );
218
219 let _ = universe.world.add_child(transform, renderable);
220 let _ = universe.world.add_child(renderable, color_comp);
221 let _ = universe.world.add_child(renderable, raycastable);
222
223 // One attach into the initialized editor subtree triggers init for the new subtree.
224 let _ = universe.attach(editor_root, transform);
225 };
226
227 // Place the stack beside the desk (a bit to the right).
228 let stack_x = 1.35;
229 let stack_z = 1.0;
230 let s = 0.25;
231 let half = 0.5 * s;
232 let light_brown = (0.80, 0.72, 0.55, 1.0);
233 let cyan = (0.20, 1.00, 1.00, 1.0);
234
235 spawn_editor_cube(
236 &mut universe,
237 editor_root,
238 "editor_stack_0",
239 (stack_x, half, stack_z),
240 (s, s, s),
241 light_brown,
242 );
243 spawn_editor_cube(
244 &mut universe,
245 editor_root,
246 "editor_stack_1",
247 (stack_x, half + 1.0 * s, stack_z),
248 (s, s, s),
249 light_brown,
250 );
251 spawn_editor_cube(
252 &mut universe,
253 editor_root,
254 "editor_stack_2",
255 (stack_x, half + 2.0 * s, stack_z),
256 (s, s, s),
257 light_brown,
258 );
259 spawn_editor_cube(
260 &mut universe,
261 editor_root,
262 "editor_stack_top",
263 (stack_x, half + 3.0 * s, stack_z),
264 (s, s, s),
265 cyan,
266 );
267 }
268
269 let xr_root = universe
270 .world
271 .add_component(engine::ecs::component::XrComponent::on());
272 universe.add(xr_root);
273
274 universe.systems.process_commands(
275 &mut universe.world,
276 &mut universe.visuals,
277 &mut universe.render_assets,
278 &mut universe.command_queue,
279 );
280
281 // Spawn the glTF subtree once up-front so joint ComponentIds exist.
282 {
283 let systems = &mut universe.systems;
284 systems.gltf.tick_with_queue(
285 &mut universe.world,
286 &mut universe.visuals,
287 &mut systems.skinned_mesh,
288 &mut universe.command_queue,
289 0.0,
290 );
291 }
292 universe.systems.process_commands(
293 &mut universe.world,
294 &mut universe.visuals,
295 &mut universe.render_assets,
296 &mut universe.command_queue,
297 );
298
299 // Register imported meshes into RenderAssets early so we can inspect skin weights
300 // (textures will still be uploaded later during normal rendering).
301 universe
302 .systems
303 .gltf
304 .flush_mesh_imports_only(&mut universe.render_assets);
305
306 // --- Joint printout + animation binding (in the example) ---
307 let all_joints = collect_joint_transforms(
308 &universe.world,
309 &universe.systems.skinned_mesh,
310 &universe.visuals,
311 model,
312 model_root,
313 );
314 println!("[vtuber-joints-example] joints found: {}", all_joints.len());
315 for (i, (node_index, joint_tx)) in all_joints.iter().enumerate() {
316 println!(" joint[{i:03}] node_index={node_index} transform={joint_tx:?}");
317 }
318
319 let node_index_to_transform: HashMap<usize, engine::ecs::ComponentId> =
320 all_joints.iter().copied().collect();
321
322 // Example settings are hardcoded to keep this example simple.
323 let joint_offset: usize = 0;
324 let wiggle_count: usize = 16;
325
326 let target_mesh_key = "pc-rei.hoodie:Body_(merged).baked:prim0".to_string();
327 let target_joint_names: Vec<String> = vec![
328 "J_Bip_L_UpperArm".to_string(),
329 "J_Bip_R_UpperArm".to_string(),
330 ];
331
332 let print_transform_updates: bool = false;
333
334 let selected_joint_transforms: Vec<(usize, engine::ecs::ComponentId)> =
335 select_named_joints(&universe.world, &all_joints, &target_joint_names)
336 .or_else(|| {
337 select_body_prim0_influencers(
338 &universe,
339 model_root,
340 &target_mesh_key,
341 wiggle_count,
342 &node_index_to_transform,
343 )
344 })
345 .unwrap_or_else(|| select_joint_range(&all_joints, joint_offset, wiggle_count));
346 println!(
347 "[vtuber-joints-example] wiggle selection: target_mesh_key='{}' offset={} count={} selected={}",
348 target_mesh_key,
349 joint_offset,
350 wiggle_count,
351 selected_joint_transforms.len()
352 );
353
354 debug_print_selected_joint_influence(
355 &universe,
356 &target_mesh_key,
357 model_root,
358 &selected_joint_transforms,
359 );
360
361 println!("[vtuber-joints-example] selected joints:");
362 for (i, (node_index, joint_tx)) in selected_joint_transforms.iter().enumerate() {
363 let name = universe
364 .world
365 .get_component_record(*joint_tx)
366 .map(|n| n.name.as_str())
367 .unwrap_or("<unknown>");
368 println!(" sel[{i:02}] node_index={node_index} name={name} transform={joint_tx:?}");
369 }
370
371 if print_transform_updates {
372 println!("[vtuber-joints-example] note: joint animation disabled");
373 }
374
375 universe.systems.process_commands(
376 &mut universe.world,
377 &mut universe.visuals,
378 &mut universe.render_assets,
379 &mut universe.command_queue,
380 );
381
382 universe.enable_repl();
383 engine::Windowing::run_app(universe).expect("Windowing failed");
384}examples/audio-graph-example.rs (line 84)
6fn main() {
7 mittens_engine::example_support::ensure_model_assets();
8 utils::logger::init();
9
10 // Debug toggle: allow isolating stack overflows to audio vs. non-audio init.
11 // Set `CAT_AUDIO_EXAMPLE_DISABLE_AUDIO=1` to skip creating audio components and
12 // skip scheduling audio notes. The UI/text/cube visualization will still spawn.
13 let audio_enabled = std::env::var("CAT_AUDIO_EXAMPLE_DISABLE_AUDIO")
14 .ok()
15 .as_deref()
16 != Some("1");
17
18 // If set, we still build audio graph components but we don't start the CPAL output stream.
19 // This helps distinguish "audio graph / scheduling" issues from "CPAL backend" issues.
20 let audio_output_enabled = std::env::var("CAT_AUDIO_EXAMPLE_AUDIO_OUTPUT_OFF")
21 .ok()
22 .as_deref()
23 != Some("1");
24
25 println!("[audio-graph-example] start");
26
27 let world = engine::ecs::World::default();
28 let mut universe = engine::Universe::new(world);
29
30 println!("[audio-graph-example] universe created");
31
32 // Minimal scene with a camera so the window opens (copied from animation-example).
33 let clear = universe
34 .world
35 .add_component(engine::ecs::component::BackgroundColorComponent::new());
36 let clear_c = universe
37 .world
38 .add_component(engine::ecs::component::ColorComponent::rgba(
39 0.07, 0.07, 0.07, 1.0,
40 ));
41 let _ = universe.world.add_child(clear, clear_c);
42 universe.add(clear);
43
44 // Ambient light so unlit areas aren't pitch black.
45 // Keep it dark to match the background clear color.
46 // (User request) 2.5x brighter.
47 let ambient = universe
48 .world
49 .add_component(engine::ecs::component::AmbientLightComponent::rgb(
50 0.075, 0.075, 0.075,
51 ));
52 universe.add(ambient);
53
54 // Input-driven camera rig.
55 let input = universe
56 .world
57 .add_component(engine::ecs::component::InputComponent::new().with_speed(2.0));
58 let rig_transform = universe.world.add_component(
59 engine::ecs::component::TransformComponent::new().with_position(2.0, 0.0, 7.0),
60 );
61 let input_mode = universe.world.add_component(
62 engine::ecs::component::InputTransformModeComponent::forward_z().with_roll_axis_y(),
63 );
64 let camera3d = universe.world.add_component(
65 engine::ecs::component::Camera3DComponent::new()
66 .with_far(250.0)
67 .with_fov(70.0),
68 );
69 let _ = universe.attach(input, input_mode);
70 let _ = universe.attach(input, rig_transform);
71 let _ = universe.attach(rig_transform, camera3d);
72
73 // Topology: I { T { C3D } } — add a small camera-attached controls hint.
74 example_util::spawn_desktop_camera_controls_hint(&mut universe, rig_transform);
75 universe.add(input);
76
77 // Directional light (sun-ish). Note: the renderer interprets the node's world position
78 // as a direction vector (see DirectionalLightComponent docs).
79 let light_tx = universe.world.add_component(
80 engine::ecs::component::TransformComponent::new().with_position(0.2, 0.7, 1.0),
81 );
82 let light = universe.world.add_component(
83 engine::ecs::component::DirectionalLightComponent::new()
84 .with_color(1.0, 1.0, 1.0)
85 .with_intensity(0.35),
86 );
87 let _ = universe.attach(light_tx, light);
88 universe.add(light_tx);
89
90 // --- Background clouds (occluded + lit) ---
91 // Mirrors the vtuber example: use a BackgroundComponent stage so the cloud volume
92 // self-occludes and is lit, but renders as background.
93 let bg_root = universe.world.add_component(
94 engine::ecs::component::BackgroundComponent::new().with_occlusion_and_lighting(),
95 );
96 universe.add(bg_root);
97 let mut cloud_params = example_util::CloudRingParams::default();
98 cloud_params.cloud_count = 7;
99 cloud_params.radius = 22.0;
100 // Move the ring up by ~one cloud height. The cloud generator uses a ~4.0 unit
101 // vertical spread for puff offsets, so +4.0 is a good "one height" bump.
102 cloud_params.center_y = 6.0;
103 cloud_params.puffs_per_cloud = 26;
104 cloud_params.angle_jitter = 0.0;
105 cloud_params.high_y_probability = 0.0;
106 cloud_params.high_y_multiplier = 1.0;
107 cloud_params.seed = 0xA0_D1_0C_01u32;
108 example_util::spawn_cloud_ring(&mut universe, bg_root, cloud_params);
109
110 // ClockComponent sets global tempo.
111 if audio_enabled {
112 let clock = universe
113 .world
114 .add_component(engine::ecs::component::ClockComponent::new().with_bpm(128.0));
115 universe.add(clock);
116 }
117
118 if audio_enabled {
119 println!("[audio-graph-example] clock added");
120 } else {
121 println!("[audio-graph-example] audio disabled; skipping clock");
122 }
123
124 // Audio output + 2 oscillator sources.
125 let audio_out = if audio_enabled {
126 let audio_out_comp = if audio_output_enabled {
127 engine::ecs::component::AudioOutputComponent::new()
128 } else {
129 engine::ecs::component::AudioOutputComponent::off()
130 };
131 let audio_out = universe.world.add_component(audio_out_comp);
132 universe.add(audio_out);
133 Some(audio_out)
134 } else {
135 None
136 };
137
138 match (audio_enabled, audio_output_enabled) {
139 (false, _) => println!("[audio-graph-example] audio disabled; skipping audio output"),
140 (true, true) => println!("[audio-graph-example] audio output added (CPAL on)"),
141 (true, false) => println!("[audio-graph-example] audio output added (CPAL off)"),
142 }
143
144 // Track A: square lead.
145 let osc_a_comp =
146 if audio_enabled {
147 let osc_a = engine::ecs::component::AudioOscillator::square()
148 .with_frequency(110.0)
149 .with_amplitude(0.12)
150 .with_enabled(false);
151 let osc_a_comp = universe.world.add_component(
152 engine::ecs::component::AudioOscillatorComponent::single(osc_a),
153 );
154 if let Some(audio_out) = audio_out {
155 let _ = universe.attach(audio_out, osc_a_comp);
156 }
157 Some(osc_a_comp)
158 } else {
159 None
160 };
161
162 if audio_enabled {
163 println!("[audio-graph-example] track A created");
164 } else {
165 println!("[audio-graph-example] audio disabled; skipping track A");
166 }
167
168 // Effect tree A (single chain):
169 // osc_a
170 // Gain
171 // BandPass
172 // Limiter
173 let mut bp_a_comp: Option<engine::ecs::ComponentId> = None;
174 if let Some(osc_a_comp) = osc_a_comp {
175 let gain_a = universe
176 .world
177 .add_component(engine::ecs::component::AudioGainComponent::new(3.2));
178 let bp_a = universe.world.add_component(
179 engine::ecs::component::AudioBandPassFilterComponent::new(120.0, 3.0, 0.40),
180 );
181 bp_a_comp = Some(bp_a);
182 let lim_a =
183 universe
184 .world
185 .add_component(engine::ecs::component::AudioLimiterComponent::new(
186 4.0, 80.0, 0.90,
187 ));
188
189 let _ = universe.attach(osc_a_comp, gain_a);
190 let _ = universe.attach(gain_a, bp_a);
191 let _ = universe.attach(bp_a, lim_a);
192 }
193
194 if audio_enabled {
195 println!("[audio-graph-example] track A effects attached");
196 }
197
198 // --- Visual layout helpers (copied/adapted from animation-example) ---
199 fn spawn_text(
200 universe: &mut engine::Universe,
201 pos: (f32, f32, f32),
202 scale: f32,
203 wrap_cols: usize,
204 text: &str,
205 ) {
206 let tx = universe.world.add_component(
207 engine::ecs::component::TransformComponent::new()
208 .with_position(pos.0, pos.1, pos.2)
209 .with_scale(scale, scale, 1.0),
210 );
211 let t =
212 universe
213 .world
214 .add_component(engine::ecs::component::TextComponent::with_word_wrap(
215 text, wrap_cols,
216 ));
217 let _ = universe.attach(tx, t);
218
219 // TextSystem looks for an immediate TextureFilteringComponent child.
220 let filtering = universe
221 .world
222 .add_component(engine::ecs::component::TextureFilteringComponent::nearest());
223 let _ = universe.attach(t, filtering);
224
225 // TextSystem also supports styling from immediate Emissive children.
226 let emissive = universe
227 .world
228 .add_component(engine::ecs::component::EmissiveComponent::on());
229 let _ = universe.attach(t, emissive);
230
231 universe.add(tx);
232 }
233
234 fn spawn_emissive_cube(
235 universe: &mut engine::Universe,
236 parent: engine::ecs::ComponentId,
237 pos: (f32, f32, f32),
238 scale: f32,
239 rgba: [f32; 4],
240 ) {
241 let tx = universe.world.add_component(
242 engine::ecs::component::TransformComponent::new()
243 .with_position(pos.0, pos.1, pos.2)
244 .with_scale(scale, scale, scale),
245 );
246 let r = universe
247 .world
248 .add_component(engine::ecs::component::RenderableComponent::cube());
249 let c = universe
250 .world
251 .add_component(engine::ecs::component::ColorComponent::rgba(
252 rgba[0], rgba[1], rgba[2], rgba[3],
253 ));
254 let e = universe
255 .world
256 .add_component(engine::ecs::component::EmissiveComponent::on());
257 let _ = universe.attach(parent, tx);
258 let _ = universe.attach(tx, r);
259 let _ = universe.attach(r, c);
260 let _ = universe.attach(r, e);
261 }
262
263 fn spawn_op_cube(
264 universe: &mut engine::Universe,
265 parent: engine::ecs::ComponentId,
266 pos: (f32, f32, f32),
267 scale: f32,
268 base_rgba: [f32; 4],
269 ) -> engine::ecs::ComponentId {
270 let tx = universe.world.add_component(
271 engine::ecs::component::TransformComponent::new()
272 .with_position(pos.0, pos.1, pos.2)
273 .with_scale(scale, scale, scale),
274 );
275 let r = universe
276 .world
277 .add_component(engine::ecs::component::RenderableComponent::cube());
278 let c = universe
279 .world
280 .add_component(engine::ecs::component::ColorComponent::rgba(
281 base_rgba[0],
282 base_rgba[1],
283 base_rgba[2],
284 base_rgba[3],
285 ));
286 let e = universe
287 .world
288 .add_component(engine::ecs::component::EmissiveComponent::on());
289
290 let _ = universe.attach(parent, tx);
291 let _ = universe.attach(tx, r);
292 let _ = universe.attach(r, c);
293 let _ = universe.attach(r, e);
294
295 tx
296 }
297
298 // --- HUD / lane (single track) ---
299 let lane_x = -2.6_f32;
300 let lane_title_z = -0.4_f32;
301 let lane_cfg_z = -0.4_f32;
302 let lane_pat_z = -1.3_f32;
303 let lane_graph_z = -1.15_f32;
304
305 // Content for pattern/chain/graph starts at x=0; keep section labels aligned there.
306 // This also keeps them from overlapping the config block (which is anchored at lane_x).
307 let lane_labels_x = lane_x + 1.6_f32;
308
309 let track_a_y = 0.9_f32;
310
311 spawn_text(
312 &mut universe,
313 (lane_x, track_a_y + 0.55, lane_title_z),
314 0.09,
315 42,
316 "Track A: AudioOscillator::square()",
317 );
318 spawn_text(
319 &mut universe,
320 (lane_x, track_a_y + 0.25, lane_cfg_z),
321 0.07,
322 48,
323 "oscillators=1\nfrequency_hz=110.0\namplitude=0.12\nenabled=false\nlookahead=0.10s",
324 );
325
326 let viz_root = universe.world.add_component(
327 engine::ecs::component::TransformComponent::new().with_position(0.0, 0.0, 0.0),
328 );
329 universe.add(viz_root);
330
331 println!("[audio-graph-example] viz root added");
332
333 // Small identifier cubes next to titles.
334 spawn_emissive_cube(
335 &mut universe,
336 viz_root,
337 (lane_x - 0.28, track_a_y + 0.55, lane_title_z),
338 0.16,
339 [0.85, 0.40, 1.00, 1.0],
340 );
341
342 // Pattern roots so we can reset via one SetColor action.
343 let track_a_pattern_root = universe.world.add_component(
344 engine::ecs::component::TransformComponent::new().with_position(0.0, 0.0, 0.0),
345 );
346 let _ = universe.attach(viz_root, track_a_pattern_root);
347
348 // Labels for pattern/chain/graph sections.
349 spawn_text(
350 &mut universe,
351 (lane_labels_x, track_a_y, lane_title_z),
352 0.06,
353 42,
354 "pattern",
355 );
356 spawn_text(
357 &mut universe,
358 (lane_labels_x, track_a_y - 0.40, lane_title_z),
359 0.06,
360 42,
361 "graph",
362 );
363
364 // --- Processing chain + graph visualization (compiled graph → cubes + labels) ---
365 use engine::ecs::system::audio_graph_compiler::{
366 AudioGraphCompiler, AudioGraphNode, AudioGraphNodeKind,
367 };
368
369 fn effect_grey_for_depth(depth: usize) -> f32 {
370 // depth=1 => medium grey; deeper => lighter, capped.
371 let base = 0.55;
372 let step = 0.13;
373 let d = depth.saturating_sub(1) as f32;
374 (base + step * d).min(0.92)
375 }
376
377 fn node_rgba(node: &AudioGraphNode, depth: usize) -> [f32; 4] {
378 match node.kind {
379 AudioGraphNodeKind::OscillatorSource { .. } => [1.0, 0.78, 0.22, 1.0],
380 _ => {
381 let g = effect_grey_for_depth(depth);
382 [g, g, g, 1.0]
383 }
384 }
385 }
386
387 fn node_label(node: &AudioGraphNode) -> String {
388 match &node.kind {
389 AudioGraphNodeKind::OscillatorSource { voices } => {
390 format!("OscillatorSource voices={voices}")
391 }
392 AudioGraphNodeKind::Gain { gain } => {
393 format!("Gain gain={gain:.3}")
394 }
395 AudioGraphNodeKind::LowPass {
396 cutoff_hz,
397 resonance,
398 } => {
399 format!("LowPass cutoff={cutoff_hz:.1}Hz res={resonance:.3}")
400 }
401 AudioGraphNodeKind::BandPass {
402 center_hz,
403 bandwidth_octaves,
404 resonance,
405 } => {
406 format!(
407 "BandPass center={center_hz:.1}Hz bw={bandwidth_octaves:.3}oct res={resonance:.3}"
408 )
409 }
410 AudioGraphNodeKind::HighPass {
411 cutoff_hz,
412 resonance,
413 } => {
414 format!("HighPass cutoff={cutoff_hz:.1}Hz res={resonance:.3}")
415 }
416 AudioGraphNodeKind::Limiter {
417 attack_ms,
418 release_ms,
419 threshold,
420 } => {
421 format!("Limiter atk={attack_ms:.1}ms rel={release_ms:.1}ms thr={threshold:.3}")
422 }
423 AudioGraphNodeKind::ClipSource => "ClipSource".to_string(),
424 }
425 }
426
427 fn compute_layout(
428 node: &AudioGraphNode,
429 depth: usize,
430 x_cursor: &mut i32,
431 out: &mut std::collections::HashMap<*const AudioGraphNode, (i32, usize)>,
432 ) -> i32 {
433 // Depth-only layout:
434 // - keep children directly below their parent on X
435 // - use Z sibling offsets (in spawn_graph_tree) to show branching
436 let _ = x_cursor;
437 for ch in node.children.iter() {
438 let _ = compute_layout(ch, depth + 1, x_cursor, out);
439 }
440
441 let my_x = 0;
442 out.insert(node as *const AudioGraphNode, (my_x, depth));
443 my_x
444 }
445
446 fn spawn_graph_tree(
447 universe: &mut engine::Universe,
448 parent: engine::ecs::ComponentId,
449 node: &AudioGraphNode,
450 bp_component: Option<engine::ecs::ComponentId>,
451 bp_label_out: &mut Option<engine::ecs::ComponentId>,
452 origin: (f32, f32, f32),
453 layout: &std::collections::HashMap<*const AudioGraphNode, (i32, usize)>,
454 dx: f32,
455 dy: f32,
456 cube_scale: f32,
457 sibling_index: usize,
458 sibling_count: usize,
459 ) {
460 let Some((x_unit, depth)) = layout.get(&(node as *const AudioGraphNode)).copied() else {
461 return;
462 };
463
464 let x = origin.0 + (x_unit as f32) * dx;
465 let y = origin.1 - (depth as f32) * dy;
466
467 // Push siblings “behind” each other along Z to reduce overlap between
468 // a sibling's cube and another node's label.
469 let dz_sibling = 0.18;
470 let z = origin.2 - (sibling_index as f32) * dz_sibling;
471
472 // Keep text slightly in front of its cube.
473 let z_text = z + 0.03;
474
475 let rgba = node_rgba(node, depth);
476 let cube_tx = spawn_op_cube(universe, parent, (x, y, z), cube_scale, rgba);
477 let _ = cube_tx;
478
479 // Label next to the cube.
480 let label = node_label(node);
481 let tx = universe.world.add_component(
482 engine::ecs::component::TransformComponent::new()
483 .with_position(x + 0.14, y + 0.02, z_text)
484 .with_scale(0.06, 0.06, 1.0),
485 );
486 let t =
487 universe
488 .world
489 .add_component(engine::ecs::component::TextComponent::with_word_wrap(
490 &label, 25,
491 ));
492 let _ = universe.attach(parent, tx);
493 let _ = universe.attach(tx, t);
494
495 if bp_component == Some(node.component) {
496 *bp_label_out = Some(t);
497 }
498
499 let filtering = universe
500 .world
501 .add_component(engine::ecs::component::TextureFilteringComponent::nearest());
502 let _ = universe.attach(t, filtering);
503
504 let emissive = universe
505 .world
506 .add_component(engine::ecs::component::EmissiveComponent::on());
507 let _ = universe.attach(t, emissive);
508
509 universe.add(tx);
510
511 // Mix/branch label if branching.
512 if node.children.len() > 1 {
513 let mut weights: Vec<f32> = Vec::with_capacity(node.children.len());
514 for i in 0..node.children.len() {
515 let w = node
516 .mix
517 .as_ref()
518 .map(|m| m.weights.get(i).copied().unwrap_or(1.0))
519 .unwrap_or(1.0);
520 weights.push(w);
521 }
522 let mix_label = if node.mix.is_some() {
523 format!("Mix weights={weights:?}")
524 } else {
525 format!("Mix <implicit> w={weights:?}")
526 };
527 let mix_tx = universe.world.add_component(
528 engine::ecs::component::TransformComponent::new()
529 .with_position(x + 0.14, y - 0.11, z_text)
530 .with_scale(0.05, 0.05, 1.0),
531 );
532 let mix_t = universe.world.add_component(
533 engine::ecs::component::TextComponent::with_word_wrap(&mix_label, 25),
534 );
535 let _ = universe.attach(parent, mix_tx);
536 let _ = universe.attach(mix_tx, mix_t);
537
538 let filtering = universe
539 .world
540 .add_component(engine::ecs::component::TextureFilteringComponent::nearest());
541 let _ = universe.attach(mix_t, filtering);
542
543 let emissive = universe
544 .world
545 .add_component(engine::ecs::component::EmissiveComponent::on());
546 let _ = universe.attach(mix_t, emissive);
547
548 universe.add(mix_tx);
549 }
550
551 let child_count = node.children.len().max(1);
552 for (i, ch) in node.children.iter().enumerate() {
553 // Each node's children form a sibling group; offset them in Z.
554 // For the root node, sibling_index is 0.
555 let _ = sibling_count;
556 spawn_graph_tree(
557 universe,
558 parent,
559 ch,
560 bp_component,
561 bp_label_out,
562 origin,
563 layout,
564 dx,
565 dy,
566 cube_scale,
567 i,
568 child_count,
569 );
570 }
571 }
572
573 // Compile + display chain and graph for the track.
574 let compiled_a = if audio_enabled {
575 println!("[audio-graph-example] compiling graph...");
576 let Some(osc_a_comp) = osc_a_comp else {
577 panic!("audio_enabled but track A was not created");
578 };
579 let compiled_a =
580 AudioGraphCompiler::compile(&universe.world, osc_a_comp).expect("compile A");
581 println!("[audio-graph-example] graph compiled");
582 Some(compiled_a)
583 } else {
584 println!("[audio-graph-example] audio disabled; skipping graph compilation");
585 None
586 };
587
588 // Full compiled graph visualization (tree layout + mix labels).
589 let mut bp_graph_label_text: Option<engine::ecs::ComponentId> = None;
590 if let Some(compiled_a) = &compiled_a {
591 let mut x_cursor = 0;
592 let mut layout: std::collections::HashMap<*const AudioGraphNode, (i32, usize)> =
593 std::collections::HashMap::new();
594 let _root_x = compute_layout(&compiled_a.root, 0, &mut x_cursor, &mut layout);
595
596 // With depth-only X layout, we keep the tree centered at x=0.
597 // Pull the graph up now that we don't show the separate chain view.
598 let origin = (0.0, track_a_y - 0.40, lane_graph_z);
599
600 spawn_graph_tree(
601 &mut universe,
602 viz_root,
603 &compiled_a.root,
604 bp_a_comp,
605 &mut bp_graph_label_text,
606 origin,
607 &layout,
608 0.45,
609 0.28,
610 0.08,
611 0,
612 1,
613 );
614 } else {
615 spawn_text(
616 &mut universe,
617 (0.0, track_a_y - 0.40, lane_graph_z),
618 0.06,
619 60,
620 "(audio disabled)",
621 );
622 }
623
624 // Animation with 16 keyframes drives scheduled notes + pattern highlights.
625 let anim = universe
626 .world
627 .add_component(engine::ecs::component::AnimationComponent::new());
628
629 let dur_a = 0.85_f32;
630 let beat_spacing = 0.38_f32;
631
632 let a_dark = [0.18, 0.08, 0.22, 1.0];
633 let a_bright = [0.85, 0.40, 1.00, 1.0];
634
635 for i in 0..16 {
636 let kf_beat = i as f64;
637 let kf = universe
638 .world
639 .add_component(engine::ecs::component::KeyframeComponent::new(kf_beat));
640
641 let _ = universe.attach(anim, kf);
642
643 // Scheduled audio note (sample-accurate via lookahead).
644 // Pulse every beat.
645 if audio_enabled {
646 let Some(osc_a_comp) = osc_a_comp else {
647 panic!("audio_enabled but track A was not created");
648 };
649
650 let note_a = engine::ecs::component::MusicNote::c(1, dur_a).with_velocity(0.80);
651
652 let act_a = universe
653 .world
654 .add_component(engine::ecs::component::ActionComponent::new(
655 engine::ecs::IntentValue::AudioSchedulePlay {
656 component_ids: vec![osc_a_comp],
657 beat_offset: 0.0,
658 beat_context: None,
659 note: Some(note_a),
660 gain: None,
661 rate: None,
662 duration: None,
663 },
664 ));
665
666 let _ = universe.attach(kf, act_a);
667 }
668
669 // Keyframed band-pass center.
670 // This is an immediate parameter update applied RT-side (no graph rebuild).
671 if audio_enabled {
672 if let Some(bp_a_comp) = bp_a_comp {
673 let t = (i as f32) / 15.0;
674 let center_hz = 10.0 + t * (1000.0 - 10.0);
675
676 let bp_center =
677 universe
678 .world
679 .add_component(engine::ecs::component::ActionComponent::new(
680 engine::ecs::IntentValue::AudioBandPassSetCenterHz {
681 component_ids: vec![bp_a_comp],
682 center_hz,
683 },
684 ));
685 let _ = universe.attach(kf, bp_center);
686
687 // Update the BandPass node label in the graph visualization.
688 if let Some(text_id) = bp_graph_label_text {
689 let label =
690 universe
691 .world
692 .add_component(engine::ecs::component::ActionComponent::new(
693 engine::ecs::IntentValue::SetText {
694 component_ids: vec![text_id],
695 text: format!("BandPass center={center_hz:.1}Hz"),
696 },
697 ));
698 let _ = universe.attach(kf, label);
699 }
700 }
701 }
702
703 // Visualization reset per keyframe.
704 let reset_a = universe
705 .world
706 .add_component(engine::ecs::component::ActionComponent::new(
707 engine::ecs::IntentValue::SetColor {
708 component_ids: vec![track_a_pattern_root],
709 rgba: a_dark,
710 },
711 ));
712 let _ = universe.attach(kf, reset_a);
713
714 // Pattern cube per step for each track.
715 let x = (kf_beat as f32) * beat_spacing;
716 let cube_a = spawn_op_cube(
717 &mut universe,
718 track_a_pattern_root,
719 (x, track_a_y, lane_pat_z),
720 0.10,
721 a_dark,
722 );
723
724 let bright_a = universe
725 .world
726 .add_component(engine::ecs::component::ActionComponent::new(
727 engine::ecs::IntentValue::SetColor {
728 component_ids: vec![cube_a],
729 rgba: a_bright,
730 },
731 ));
732 let _ = universe.attach(kf, bright_a);
733 }
734 universe.add(anim);
735
736 println!("[audio-graph-example] animation created");
737
738 // Keep window open.
739 println!("[audio-graph-example] processing commands...");
740 universe.systems.process_commands(
741 &mut universe.world,
742 &mut universe.visuals,
743 &mut universe.render_assets,
744 &mut universe.command_queue,
745 );
746
747 println!("[audio-graph-example] commands processed; launching window");
748
749 engine::Windowing::run_app(universe).expect("Windowing failed");
750}Additional examples can be found in:
pub fn id(&self) -> Option<ComponentId>
Trait Implementations§
Source§impl Clone for DirectionalLightComponent
impl Clone for DirectionalLightComponent
Source§fn clone(&self) -> DirectionalLightComponent
fn clone(&self) -> DirectionalLightComponent
Returns a duplicate of the value. Read more
1.0.0 (const: unstable) · Source§fn clone_from(&mut self, source: &Self)
fn clone_from(&mut self, source: &Self)
Performs copy-assignment from
source. Read moreSource§impl Component for DirectionalLightComponent
impl Component for DirectionalLightComponent
fn set_id(&mut self, component: ComponentId)
Source§fn name(&self) -> &'static str
fn name(&self) -> &'static str
Short debug/type name for this component kind (e.g. “transform”, “camera”).
Source§fn init(&mut self, emit: &mut dyn SignalEmitter, component: ComponentId)
fn init(&mut self, emit: &mut dyn SignalEmitter, component: ComponentId)
Called when component is added to the World
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
Encode this component as an MMS Component Expression AST node. Read more
Source§fn cleanup(&mut self, _emit: &mut dyn SignalEmitter, _component: ComponentId)
fn cleanup(&mut self, _emit: &mut dyn SignalEmitter, _component: ComponentId)
Called when component is removed from the World.
impl Copy for DirectionalLightComponent
Source§impl Debug for DirectionalLightComponent
impl Debug for DirectionalLightComponent
Auto Trait Implementations§
impl Freeze for DirectionalLightComponent
impl RefUnwindSafe for DirectionalLightComponent
impl Send for DirectionalLightComponent
impl Sync for DirectionalLightComponent
impl Unpin for DirectionalLightComponent
impl UnsafeUnpin for DirectionalLightComponent
impl UnwindSafe for DirectionalLightComponent
Blanket Implementations§
Source§impl<T> BorrowMut<T> for Twhere
T: ?Sized,
impl<T> BorrowMut<T> for Twhere
T: ?Sized,
Source§fn borrow_mut(&mut self) -> &mut T
fn borrow_mut(&mut self) -> &mut T
Mutably borrows from an owned value. Read more
Source§impl<T> CloneToUninit for Twhere
T: Clone,
impl<T> CloneToUninit for Twhere
T: Clone,
Source§impl<T> Downcast for Twhere
T: Any,
impl<T> Downcast for Twhere
T: Any,
Source§fn into_any(self: Box<T>) -> Box<dyn Any>
fn into_any(self: Box<T>) -> Box<dyn Any>
Convert
Box<dyn Trait> (where Trait: Downcast) to Box<dyn Any>. Box<dyn Any> can
then be further downcast into Box<ConcreteType> where ConcreteType implements Trait.Source§fn into_any_rc(self: Rc<T>) -> Rc<dyn Any>
fn into_any_rc(self: Rc<T>) -> Rc<dyn Any>
Convert
Rc<Trait> (where Trait: Downcast) to Rc<Any>. Rc<Any> can then be
further downcast into Rc<ConcreteType> where ConcreteType implements Trait.Source§fn as_any(&self) -> &(dyn Any + 'static)
fn as_any(&self) -> &(dyn Any + 'static)
Convert
&Trait (where Trait: Downcast) to &Any. This is needed since Rust cannot
generate &Any’s vtable from &Trait’s.Source§fn as_any_mut(&mut self) -> &mut (dyn Any + 'static)
fn as_any_mut(&mut self) -> &mut (dyn Any + 'static)
Convert
&mut Trait (where Trait: Downcast) to &Any. This is needed since Rust cannot
generate &mut Any’s vtable from &mut Trait’s.