pub struct ClockComponent {
pub bpm: f64,
/* private fields */
}Expand description
Controls the global beat clock tempo (BPM).
Intended to be singleton-like: the most recently registered ClockComponent wins.
Fields§
§bpm: f64Implementations§
Source§impl ClockComponent
impl ClockComponent
Sourcepub fn new() -> Self
pub fn new() -> Self
Examples found in repository?
examples/text-animation.rs (line 97)
6fn main() {
7 mittens_engine::example_support::ensure_model_assets();
8 utils::logger::init();
9
10 let world = engine::ecs::World::default();
11 let mut universe = engine::Universe::new(world);
12
13 // Input-driven camera rig.
14 // Topology: I { T { C3D } }
15 let input = universe
16 .world
17 .add_component(engine::ecs::component::InputComponent::new().with_speed(1.5));
18 let camera3d = universe
19 .world
20 .add_component(engine::ecs::component::Camera3DComponent::new());
21 let rig_transform = universe.world.add_component(
22 engine::ecs::component::TransformComponent::new().with_position(0.0, 0.0, 2.5),
23 );
24 let input_mode = universe.world.add_component(
25 engine::ecs::component::InputTransformModeComponent::forward_z().with_roll_axis_y(),
26 );
27 let _ = universe.attach(input, input_mode);
28 let _ = universe.attach(input, rig_transform);
29 let _ = universe.attach(rig_transform, camera3d);
30
31 // Small on-screen help.
32 example_util::spawn_desktop_camera_controls_hint(&mut universe, rig_transform);
33 universe.add(input);
34
35 // Light so we can see non-emissive materials.
36 let light_transform = universe.world.add_component(
37 engine::ecs::component::TransformComponent::new().with_position(0.0, 0.0, 2.0),
38 );
39 let light = universe.world.add_component(
40 engine::ecs::component::PointLightComponent::new()
41 .with_distance(25.0)
42 .with_color(1.0, 1.0, 1.0),
43 );
44 let _ = universe.attach(light_transform, light);
45 universe.add(light_transform);
46
47 use engine::ecs::component::{
48 ColorComponent, TextComponent, TextShadowComponent, TextureComponent,
49 TextureFilteringComponent, TransformComponent, TransparentCutoutComponent,
50 };
51
52 // Styled text root.
53 let text_root = universe.world.add_component(
54 TransformComponent::new()
55 .with_position(0.0, 0.0, 0.0)
56 .with_scale(0.25, 0.25, 1.0),
57 );
58
59 // Color must be an ancestor of the glyph renderables.
60 let color_id = universe
61 .world
62 .add_component(ColorComponent::rgba(1.0, 1.0, 1.0, 1.0));
63 let _ = universe.attach(text_root, color_id);
64
65 // This is the component we animate via SetText.
66 let text_id = universe.world.add_component(TextComponent::new(">w<"));
67 let _ = universe.attach(color_id, text_id);
68
69 // Route into cutout pass for cleaner edges.
70 let cutout = universe
71 .world
72 .add_component(TransparentCutoutComponent::new());
73 let _ = universe.attach(text_id, cutout);
74
75 // Font atlas.
76 let tex = universe.world.add_component(TextureComponent::with_uri(
77 "assets/textures/font_system.dds",
78 ));
79 let _ = universe.attach(text_id, tex);
80
81 let shadow = universe.world.add_component(
82 TextShadowComponent::new()
83 .with_scale(1.35)
84 .with_offset([0.06, -0.06, 0.0015]),
85 );
86 let filtering = universe
87 .world
88 .add_component(TextureFilteringComponent::nearest_magnification());
89
90 let _ = universe.attach(text_id, shadow);
91 let _ = universe.attach(text_id, filtering);
92
93 universe.add(text_root);
94
95 let clock_component = universe
96 .world
97 .add_component(engine::ecs::component::ClockComponent::new().with_bpm(140.0));
98 let _ = universe.add(clock_component);
99
100 // Looping animation: 4 beats long, 4 keyframes.
101 let anim = universe
102 .world
103 .add_component(engine::ecs::component::AnimationComponent::new());
104
105 let faces = [">w<", "^w^", "-_-", "o_o"];
106 for (i, &face) in faces.iter().enumerate() {
107 let kf = universe
108 .world
109 .add_component(engine::ecs::component::KeyframeComponent::new(i as f64));
110
111 // Each keyframe emits a SetText intent.
112 let action = universe
113 .world
114 .add_component(engine::ecs::component::ActionComponent::new(
115 engine::ecs::IntentValue::SetText {
116 component_ids: vec![text_id],
117 text: face.to_string(),
118 },
119 ));
120
121 let _ = universe.attach(anim, kf);
122 let _ = universe.attach(kf, action);
123 }
124
125 universe.add(anim);
126
127 // Process init-time registrations (Text expands into glyph subtrees here).
128 universe.systems.process_commands(
129 &mut universe.world,
130 &mut universe.visuals,
131 &mut universe.render_assets,
132 &mut universe.command_queue,
133 );
134
135 engine::Windowing::run_app(universe).expect("Windowing failed");
136}More examples
examples/mesh-factory-example.rs (line 18)
6fn main() {
7 mittens_engine::example_support::ensure_model_assets();
8 utils::logger::init();
9
10 const LABEL_WRAP_AT: usize = 13;
11
12 let world = engine::ecs::World::default();
13 let mut universe = engine::Universe::new(world);
14
15 // add a clock
16 let clock = universe
17 .world
18 .add_component(engine::ecs::component::ClockComponent::new().with_bpm(60.0));
19 universe.add(clock);
20
21 // Input-driven camera rig.
22 // Topology: I { T { C3D } }
23 let input = universe
24 .world
25 .add_component(engine::ecs::component::InputComponent::new().with_speed(1.5));
26 let camera3d = universe
27 .world
28 .add_component(engine::ecs::component::Camera3DComponent::new());
29 let rig_transform = universe.world.add_component(
30 engine::ecs::component::TransformComponent::new().with_position(0.0, 0.0, 11.0),
31 );
32 let input_mode = universe.world.add_component(
33 engine::ecs::component::InputTransformModeComponent::forward_z().with_roll_axis_y(),
34 );
35 let _ = universe.attach(input, input_mode);
36 let _ = universe.attach(input, rig_transform);
37 let _ = universe.attach(rig_transform, camera3d);
38
39 // Topology: I { T { C3D } } — add a small camera-attached controls hint.
40 example_util::spawn_desktop_camera_controls_hint(&mut universe, rig_transform);
41 universe.add(input);
42
43 // Light.
44 let light_transform = universe.world.add_component(
45 engine::ecs::component::TransformComponent::new().with_position(0.0, 2.0, 2.0),
46 );
47 let light = universe.world.add_component(
48 engine::ecs::component::PointLightComponent::new()
49 .with_distance(50.0)
50 .with_color(1.0, 1.0, 1.0),
51 );
52 let _ = universe.attach(light_transform, light);
53 universe.add(light_transform);
54
55 fn spawn_labeled_mesh(
56 universe: &mut engine::Universe,
57 x: f32,
58 y: f32,
59 label: &str,
60 mesh: engine::graphics::primitives::CpuMeshHandle,
61 scale: [f32; 3],
62 color: [f32; 4],
63 ) {
64 use engine::ecs::component::{
65 ActionComponent, AnimationComponent, AnimationState, ColorComponent, EmissiveComponent,
66 KeyframeComponent, RenderableComponent, TextComponent, TransformComponent,
67 };
68 use engine::graphics::primitives::{MaterialHandle, Renderable};
69
70 // Mesh.
71 let root = universe.world.add_component(
72 TransformComponent::new()
73 .with_position(x, y, 0.0)
74 .with_scale(scale[0], scale[1], scale[2]),
75 );
76
77 // Spin each shape around its own +Y axis using AnimationComponent + keyframes.
78 // We fill [0, 2) beats densely so it looks smooth.
79 let anim = universe
80 .world
81 .add_component(AnimationComponent::new().with_state(AnimationState::Looping));
82 let _ = universe.attach(root, anim);
83
84 let steps: usize = 64;
85 for i in 0..steps {
86 let beat = (i as f64) * (2.0 / (steps as f64));
87 let kf = universe.world.add_component(KeyframeComponent::new(beat));
88 let _ = universe.attach(anim, kf);
89
90 // Full turn over 2 beats.
91 let angle = (std::f64::consts::TAU * (beat / 2.0)) as f32;
92 let rotation = utils::math::quat_from_axis_angle([0.0, 1.0, 0.0], angle);
93
94 let action_cid = universe.world.add_component(ActionComponent::new(
95 engine::ecs::IntentValue::UpdateTransform {
96 component_ids: vec![root],
97 translation: [x, y, 0.0],
98 rotation_quat_xyzw: rotation,
99 scale,
100 },
101 ));
102 let _ = universe.attach(kf, action_cid);
103 }
104
105 let renderable = universe
106 .world
107 .add_component(RenderableComponent::new(Renderable::new(
108 mesh,
109 MaterialHandle::TOON_MESH,
110 )));
111 let color_c = universe
112 .world
113 .add_component(ColorComponent::rgba(color[0], color[1], color[2], color[3]));
114 let emissive = universe.world.add_component(EmissiveComponent::on());
115
116 let _ = universe.attach(root, renderable);
117 let _ = universe.attach(renderable, color_c);
118 let _ = universe.attach(renderable, emissive);
119
120 universe.add(root);
121
122 // Label (separate transform so we can scale text independently).
123 let text_root = universe.world.add_component(
124 TransformComponent::new()
125 .with_position(x, y + 0.75, 0.05)
126 .with_scale(0.09, 0.09, 1.0),
127 );
128 let text = universe
129 .world
130 .add_component(TextComponent::with_word_wrap_tokens(
131 label,
132 LABEL_WRAP_AT,
133 ["::", "(", ")", ",", "."],
134 ));
135 let text_color = universe
136 .world
137 .add_component(ColorComponent::rgba(1.0, 1.0, 1.0, 1.0));
138 let text_emissive = universe.world.add_component(EmissiveComponent::on());
139 let _ = universe.attach(text_root, text);
140 let _ = universe.attach(text, text_color);
141 let _ = universe.attach(text, text_emissive);
142 universe.add(text_root);
143 }
144
145 // Built-in meshes (stable ids).
146 let tri = universe
147 .render_assets
148 .get_mesh(engine::graphics::BuiltinMeshType::Triangle2D);
149 let quad = universe
150 .render_assets
151 .get_mesh(engine::graphics::BuiltinMeshType::Quad2D);
152 let cube = universe
153 .render_assets
154 .get_mesh(engine::graphics::BuiltinMeshType::Cube);
155 let tetra = universe
156 .render_assets
157 .get_mesh(engine::graphics::BuiltinMeshType::Tetrahedron);
158 let sphere = universe
159 .render_assets
160 .get_mesh(engine::graphics::BuiltinMeshType::Sphere);
161 let cone = universe
162 .render_assets
163 .get_mesh(engine::graphics::BuiltinMeshType::Cone);
164 let circle = universe
165 .render_assets
166 .get_mesh(engine::graphics::BuiltinMeshType::Circle2D);
167
168 // Layout.
169 let y = 0.0;
170 let dx = 1.8;
171 let x0 = -dx * 3.0;
172
173 spawn_labeled_mesh(
174 &mut universe,
175 x0 + dx * 0.0,
176 y,
177 "Triangle2D\nMeshFactory::triangle_2d()",
178 tri,
179 [1.0, 1.0, 1.0],
180 [1.0, 1.0, 1.0, 1.0],
181 );
182 spawn_labeled_mesh(
183 &mut universe,
184 x0 + dx * 1.0,
185 y,
186 "Quad2D\nMeshFactory::quad_2d()",
187 quad,
188 [1.0, 1.0, 1.0],
189 [1.0, 1.0, 1.0, 1.0],
190 );
191 spawn_labeled_mesh(
192 &mut universe,
193 x0 + dx * 2.0,
194 y,
195 "Cube\nMeshFactory::cube()",
196 cube,
197 [0.9, 0.9, 0.9],
198 [1.0, 1.0, 1.0, 1.0],
199 );
200 spawn_labeled_mesh(
201 &mut universe,
202 x0 + dx * 3.0,
203 y,
204 "Tetrahedron\nMeshFactory::tetrahedron()",
205 tetra,
206 [1.0, 1.0, 1.0],
207 [1.0, 1.0, 1.0, 1.0],
208 );
209 spawn_labeled_mesh(
210 &mut universe,
211 x0 + dx * 4.0,
212 y,
213 "Sphere\nMeshFactory::sphere()",
214 sphere,
215 [1.0, 1.0, 1.0],
216 [1.0, 1.0, 1.0, 1.0],
217 );
218 spawn_labeled_mesh(
219 &mut universe,
220 x0 + dx * 5.0,
221 y,
222 "Cone\nMeshFactory::cone(32)",
223 cone,
224 [1.0, 1.0, 1.0],
225 [1.0, 1.0, 1.0, 1.0],
226 );
227 spawn_labeled_mesh(
228 &mut universe,
229 x0 + dx * 6.0,
230 y,
231 "Circle2D\nMeshFactory::circle_2d(0.45, 0.5, 64)",
232 circle,
233 [1.0, 1.0, 1.0],
234 [1.0, 1.0, 1.0, 1.0],
235 );
236
237 // Process init-time registrations (Text expands into glyph subtrees here).
238 universe.systems.process_commands(
239 &mut universe.world,
240 &mut universe.visuals,
241 &mut universe.render_assets,
242 &mut universe.command_queue,
243 );
244
245 engine::Windowing::run_app(universe).expect("Windowing failed");
246}examples/animation-for-topology.rs (line 137)
80fn main() {
81 mittens_engine::example_support::ensure_model_assets();
82 utils::logger::init();
83
84 let world = engine::ecs::World::default();
85 let mut universe = engine::Universe::new(world);
86
87 // Minimal scene with a camera so the window opens.
88 let clear = universe
89 .world
90 .add_component(engine::ecs::component::BackgroundColorComponent::new());
91 let clear_c = universe
92 .world
93 .add_component(engine::ecs::component::ColorComponent::rgba(
94 0.06, 0.06, 0.07, 1.0,
95 ));
96 let _ = universe.world.add_child(clear, clear_c);
97 universe.add(clear);
98
99 // Input-driven camera rig.
100 let input = universe
101 .world
102 .add_component(engine::ecs::component::InputComponent::new().with_speed(2.0));
103 let rig_transform = universe.world.add_component(
104 engine::ecs::component::TransformComponent::new().with_position(0.0, 0.0, 9.0),
105 );
106 let input_mode = universe.world.add_component(
107 engine::ecs::component::InputTransformModeComponent::forward_z().with_roll_axis_y(),
108 );
109 let camera3d = universe.world.add_component(
110 engine::ecs::component::Camera3DComponent::new()
111 .with_far(250.0)
112 .with_fov(70.0),
113 );
114 let _ = universe.attach(input, input_mode);
115 let _ = universe.attach(input, rig_transform);
116 let _ = universe.attach(rig_transform, camera3d);
117
118 // Topology: I { T { C3D } } — add a small camera-attached controls hint.
119 example_util::spawn_desktop_camera_controls_hint(&mut universe, rig_transform);
120 universe.add(input);
121
122 // Light so we can see non-emissive materials.
123 let light_tx = universe.world.add_component(
124 engine::ecs::component::TransformComponent::new().with_position(2.0, 3.5, 6.0),
125 );
126 let light = universe.world.add_component(
127 engine::ecs::component::PointLightComponent::new()
128 .with_distance(30.0)
129 .with_color(1.0, 1.0, 1.0),
130 );
131 let _ = universe.attach(light_tx, light);
132 universe.add(light_tx);
133
134 // ClockComponent drives the animation timeline in beats.
135 let clock = universe
136 .world
137 .add_component(engine::ecs::component::ClockComponent::new().with_bpm(140.0));
138 universe.add(clock);
139
140 // Root for all visualization objects.
141 let viz_root = universe.world.add_component(
142 engine::ecs::component::TransformComponent::new().with_position(0.0, 0.0, 0.0),
143 );
144 universe.add(viz_root);
145
146 let anchor_count = 16usize;
147 let cube_pool_size = 8usize;
148
149 // Three grids side-by-side.
150 let layout_a = GridLayout {
151 origin: (-6.0, 0.0, 0.0),
152 spacing: 1.1,
153 };
154 let layout_b = GridLayout {
155 origin: (0.0, 0.0, 0.0),
156 spacing: 1.1,
157 };
158 let layout_c = GridLayout {
159 origin: (6.0, 0.0, 0.0),
160 spacing: 1.1,
161 };
162
163 // --- Grid A: detach + re-attach (reparent) ---
164 let grid_a_root = universe
165 .world
166 .add_component(engine::ecs::component::TransformComponent::new());
167 let _ = universe.attach(viz_root, grid_a_root);
168
169 let mut anchors_a: Vec<engine::ecs::ComponentId> = Vec::with_capacity(anchor_count);
170 for i in 0..anchor_count {
171 let (x, y, z) = grid_anchor_local(layout_a, i);
172 let anchor = universe.world.add_component(
173 engine::ecs::component::TransformComponent::new().with_position(x, y, z),
174 );
175 let _ = universe.attach(grid_a_root, anchor);
176 anchors_a.push(anchor);
177 spawn_emissive_marker_cube(
178 &mut universe,
179 anchor,
180 (0.0, -0.35, 0.0),
181 0.06,
182 [0.18, 0.18, 0.22, 1.0],
183 );
184 }
185
186 let mut cubes_a: Vec<engine::ecs::ComponentId> = Vec::with_capacity(cube_pool_size);
187 for i in 0..cube_pool_size {
188 let t = (i as f32) / ((cube_pool_size - 1) as f32).max(1.0);
189 let rgba = [0.10, 0.40 + 0.50 * t, 0.90 - 0.70 * t, 1.0];
190 cubes_a.push(spawn_detached_cube_prefab(&mut universe, 0.22, rgba));
191 }
192
193 let anim_a = universe
194 .world
195 .add_component(engine::ecs::component::AnimationComponent::new());
196 for i in 0..anchor_count {
197 let cube = cubes_a[i % cube_pool_size];
198 let parent = anchors_a[i];
199
200 // Explicit detach+attach to test topology changes via animations.
201
202 // Put them on separate keyframes so ordering is time-deterministic.
203 let beat_detach = i as f64;
204 let beat_attach = i as f64 + 0.05;
205
206 let kf_detach = universe
207 .world
208 .add_component(engine::ecs::component::KeyframeComponent::new(beat_detach));
209 let _ = universe.attach(anim_a, kf_detach);
210 let detach_action =
211 universe
212 .world
213 .add_component(engine::ecs::component::ActionComponent::new(
214 engine::ecs::IntentValue::Detach {
215 component_ids: vec![cube],
216 },
217 ));
218 let _ = universe.attach(kf_detach, detach_action);
219
220 let kf_attach = universe
221 .world
222 .add_component(engine::ecs::component::KeyframeComponent::new(beat_attach));
223 let _ = universe.attach(anim_a, kf_attach);
224 let attach_action =
225 universe
226 .world
227 .add_component(engine::ecs::component::ActionComponent::new(
228 engine::ecs::IntentValue::Attach {
229 parents: vec![parent],
230 child: cube,
231 },
232 ));
233 let _ = universe.attach(kf_attach, attach_action);
234 }
235 universe.add(anim_a);
236
237 // --- Grid B: continuously spawn (attach_clone) + delete-behind (remove_child) ---
238 //
239 // Uses the new actions:
240 // - `Action::attach_clone(parent, prefab_root)`
241 // - `Action::remove_child(parent, index)`
242 //
243 // This avoids needing a pre-built pool of cube ComponentIds.
244 let grid_b_root = universe
245 .world
246 .add_component(engine::ecs::component::TransformComponent::new());
247 let _ = universe.attach(viz_root, grid_b_root);
248
249 let mut anchors_b: Vec<engine::ecs::ComponentId> = Vec::with_capacity(anchor_count);
250 for i in 0..anchor_count {
251 let (x, y, z) = grid_anchor_local(layout_b, i);
252 let anchor = universe.world.add_component(
253 engine::ecs::component::TransformComponent::new().with_position(x, y, z),
254 );
255 let _ = universe.attach(grid_b_root, anchor);
256 anchors_b.push(anchor);
257
258 // Marker is attached under the grid root (not under the anchor), so anchor child index 0
259 // remains reserved for the dynamic cube subtree.
260 spawn_emissive_marker_cube(
261 &mut universe,
262 grid_b_root,
263 (x, y - 0.35, z),
264 0.06,
265 [0.22, 0.18, 0.18, 1.0],
266 );
267 }
268
269 // Detached prefab that will be cloned on demand (reddish cube).
270 let prefab_b = spawn_detached_cube_prefab(&mut universe, 0.20, [0.90, 0.25, 0.15, 1.0]);
271
272 let steps_b = 32usize;
273 let window = 8usize;
274
275 let anim_b = universe
276 .world
277 .add_component(engine::ecs::component::AnimationComponent::new());
278
279 // Phase 1: each beat, spawn a cube under an anchor; delete-behind by removing child(0).
280 for i in 0..steps_b {
281 let parent = anchors_b[i % anchor_count];
282
283 let beat_attach = i as f64;
284 let beat_remove = i as f64 + 0.05;
285
286 let kf_attach = universe
287 .world
288 .add_component(engine::ecs::component::KeyframeComponent::new(beat_attach));
289 let _ = universe.attach(anim_b, kf_attach);
290
291 let attach_action =
292 universe
293 .world
294 .add_component(engine::ecs::component::ActionComponent::new(
295 engine::ecs::IntentValue::AttachClone {
296 parents: vec![parent],
297 prefab_root: prefab_b,
298 },
299 ));
300 let _ = universe.attach(kf_attach, attach_action);
301
302 if i >= window {
303 let remove_parent = anchors_b[(i - window) % anchor_count];
304 let kf_remove = universe
305 .world
306 .add_component(engine::ecs::component::KeyframeComponent::new(beat_remove));
307 let _ = universe.attach(anim_b, kf_remove);
308
309 let remove_action =
310 universe
311 .world
312 .add_component(engine::ecs::component::ActionComponent::new(
313 engine::ecs::IntentValue::RemoveChild {
314 parents: vec![remove_parent],
315 index: 0,
316 },
317 ));
318 let _ = universe.attach(kf_remove, remove_action);
319 }
320 }
321
322 // Phase 2: cleanup tail so the loop doesn't accumulate cubes.
323 for j in 0..window {
324 let remove_parent = anchors_b[(steps_b - window + j) % anchor_count];
325 let beat_remove = steps_b as f64 + j as f64 + 0.05;
326
327 let kf_remove = universe
328 .world
329 .add_component(engine::ecs::component::KeyframeComponent::new(beat_remove));
330 let _ = universe.attach(anim_b, kf_remove);
331
332 let remove_action =
333 universe
334 .world
335 .add_component(engine::ecs::component::ActionComponent::new(
336 engine::ecs::IntentValue::RemoveChild {
337 parents: vec![remove_parent],
338 index: 0,
339 },
340 ));
341 let _ = universe.attach(kf_remove, remove_action);
342 }
343
344 universe.add(anim_b);
345
346 // --- Grid C: move cubes via set_position (no topology changes) ---
347 let grid_c_root = universe
348 .world
349 .add_component(engine::ecs::component::TransformComponent::new());
350 let _ = universe.attach(viz_root, grid_c_root);
351
352 let mut anchors_c: Vec<(f32, f32, f32)> = Vec::with_capacity(anchor_count);
353 for i in 0..anchor_count {
354 let (x, y, z) = grid_anchor_local(layout_c, i);
355 anchors_c.push((x, y, z));
356
357 let anchor = universe.world.add_component(
358 engine::ecs::component::TransformComponent::new().with_position(x, y, z),
359 );
360 let _ = universe.attach(grid_c_root, anchor);
361 spawn_emissive_marker_cube(
362 &mut universe,
363 anchor,
364 (0.0, -0.35, 0.0),
365 0.06,
366 [0.18, 0.22, 0.18, 1.0],
367 );
368 }
369
370 let mut cubes_c: Vec<engine::ecs::ComponentId> = Vec::with_capacity(cube_pool_size);
371 for i in 0..cube_pool_size {
372 let t = (i as f32) / ((cube_pool_size - 1) as f32).max(1.0);
373 let rgba = [0.25, 0.85 - 0.55 * t, 0.25 + 0.55 * t, 1.0];
374 let cube_root = spawn_detached_cube_prefab(&mut universe, 0.22, rgba);
375 let _ = universe.attach(grid_c_root, cube_root);
376 cubes_c.push(cube_root);
377 }
378
379 let anim_c = universe
380 .world
381 .add_component(engine::ecs::component::AnimationComponent::new());
382 for i in 0..anchor_count {
383 let kf = universe
384 .world
385 .add_component(engine::ecs::component::KeyframeComponent::new(i as f64));
386 let _ = universe.attach(anim_c, kf);
387
388 let cube = cubes_c[i % cube_pool_size];
389 let (x, y, z) = anchors_c[i];
390 let setpos_action =
391 universe
392 .world
393 .add_component(engine::ecs::component::ActionComponent::new(
394 engine::ecs::IntentValue::SetPosition {
395 component_ids: vec![cube],
396 position: [x, y, z],
397 },
398 ));
399 let _ = universe.attach(kf, setpos_action);
400 }
401 universe.add(anim_c);
402
403 universe.systems.process_commands(
404 &mut universe.world,
405 &mut universe.visuals,
406 &mut universe.render_assets,
407 &mut universe.command_queue,
408 );
409
410 engine::Windowing::run_app(universe).expect("Windowing failed");
411}examples/vtuber-joints-example.rs (line 24)
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/animation-example.rs (line 63)
6fn main() {
7 mittens_engine::example_support::ensure_model_assets();
8 utils::logger::init();
9
10 let world = engine::ecs::World::default();
11 let mut universe = engine::Universe::new(world);
12
13 // Minimal scene with a camera so the window opens.
14 let clear = universe
15 .world
16 .add_component(engine::ecs::component::BackgroundColorComponent::new());
17 let clear_c = universe
18 .world
19 .add_component(engine::ecs::component::ColorComponent::rgba(
20 0.08, 0.08, 0.08, 1.0,
21 ));
22 let _ = universe.world.add_child(clear, clear_c);
23 universe.add(clear);
24
25 // Input-driven camera rig.
26 let input = universe
27 .world
28 .add_component(engine::ecs::component::InputComponent::new().with_speed(2.0));
29 let rig_transform = universe.world.add_component(
30 engine::ecs::component::TransformComponent::new().with_position(2.0, 0.0, 7.0),
31 );
32 let input_mode = universe.world.add_component(
33 engine::ecs::component::InputTransformModeComponent::forward_z().with_roll_axis_y(),
34 );
35 let camera3d = universe.world.add_component(
36 engine::ecs::component::Camera3DComponent::new()
37 .with_far(250.0)
38 .with_fov(70.0),
39 );
40 let _ = universe.attach(input, input_mode);
41 let _ = universe.attach(input, rig_transform);
42 let _ = universe.attach(rig_transform, camera3d);
43
44 // Topology: I { T { C3D } } — add a small camera-attached controls hint.
45 example_util::spawn_desktop_camera_controls_hint(&mut universe, rig_transform);
46 universe.add(input);
47
48 // Light so we can see non-emissive materials (even though our cubes are emissive).
49 let light_tx = universe.world.add_component(
50 engine::ecs::component::TransformComponent::new().with_position(0.0, 2.5, 4.0),
51 );
52 let light = universe.world.add_component(
53 engine::ecs::component::PointLightComponent::new()
54 .with_distance(25.0)
55 .with_color(1.0, 1.0, 1.0),
56 );
57 let _ = universe.attach(light_tx, light);
58 universe.add(light_tx);
59
60 // ClockComponent sets global tempo.
61 let clock = universe
62 .world
63 .add_component(engine::ecs::component::ClockComponent::new().with_bpm(128.0));
64 universe.add(clock);
65
66 // Audio output + oscillators driven by scheduled actions.
67 let audio_out = universe
68 .world
69 .add_component(engine::ecs::component::AudioOutputComponent::new());
70 universe.add(audio_out);
71
72 // Noise oscillator: short white-noise hits.
73 let osc_noise = engine::ecs::component::AudioOscillator::noise()
74 .with_frequency(0.0)
75 .with_amplitude(0.06)
76 .with_enabled(false);
77 let osc_noise_comp =
78 universe
79 .world
80 .add_component(engine::ecs::component::AudioOscillatorComponent::single(
81 osc_noise,
82 ));
83 let _ = universe.attach(audio_out, osc_noise_comp);
84
85 // Drum oscillator: retriggers (phase + sweep) every enable.
86 let osc_drum = engine::ecs::component::AudioOscillator::drum()
87 // A low-ish pitch scale; actual pitch is set by scheduled notes.
88 .with_frequency(32.0)
89 .with_amplitude(0.40)
90 .with_enabled(false);
91 let osc_drum_comp =
92 universe
93 .world
94 .add_component(engine::ecs::component::AudioOscillatorComponent::single(
95 osc_drum,
96 ));
97
98 let _ = universe.attach(audio_out, osc_drum_comp);
99
100 // --- Visual layout helpers ---
101 fn spawn_text(universe: &mut engine::Universe, pos: (f32, f32, f32), scale: f32, text: &str) {
102 let tx = universe.world.add_component(
103 engine::ecs::component::TransformComponent::new()
104 .with_position(pos.0, pos.1, pos.2)
105 .with_scale(scale, scale, 1.0),
106 );
107 let t =
108 universe
109 .world
110 .add_component(engine::ecs::component::TextComponent::with_word_wrap(
111 text, 38,
112 ));
113 let _ = universe.attach(tx, t);
114 universe.add(tx);
115 }
116
117 fn spawn_emissive_cube(
118 universe: &mut engine::Universe,
119 parent: engine::ecs::ComponentId,
120 pos: (f32, f32, f32),
121 scale: f32,
122 rgba: [f32; 4],
123 ) {
124 let tx = universe.world.add_component(
125 engine::ecs::component::TransformComponent::new()
126 .with_position(pos.0, pos.1, pos.2)
127 .with_scale(scale, scale, scale),
128 );
129 let r = universe
130 .world
131 .add_component(engine::ecs::component::RenderableComponent::cube());
132 let c = universe
133 .world
134 .add_component(engine::ecs::component::ColorComponent::rgba(
135 rgba[0], rgba[1], rgba[2], rgba[3],
136 ));
137 let e = universe
138 .world
139 .add_component(engine::ecs::component::EmissiveComponent::on());
140 let _ = universe.attach(parent, tx);
141 let _ = universe.attach(tx, r);
142 let _ = universe.attach(r, c);
143 let _ = universe.attach(r, e);
144 }
145
146 fn spawn_op_cube(
147 universe: &mut engine::Universe,
148 parent: engine::ecs::ComponentId,
149 pos: (f32, f32, f32),
150 scale: f32,
151 base_rgba: [f32; 4],
152 ) -> engine::ecs::ComponentId {
153 let tx = universe.world.add_component(
154 engine::ecs::component::TransformComponent::new()
155 .with_position(pos.0, pos.1, pos.2)
156 .with_scale(scale, scale, scale),
157 );
158 let r = universe
159 .world
160 .add_component(engine::ecs::component::RenderableComponent::cube());
161 let c = universe
162 .world
163 .add_component(engine::ecs::component::ColorComponent::rgba(
164 base_rgba[0],
165 base_rgba[1],
166 base_rgba[2],
167 base_rgba[3],
168 ));
169 let e = universe
170 .world
171 .add_component(engine::ecs::component::EmissiveComponent::on());
172
173 let _ = universe.attach(parent, tx);
174 let _ = universe.attach(tx, r);
175 let _ = universe.attach(r, c);
176 let _ = universe.attach(r, e);
177
178 tx
179 }
180
181 // --- HUD / lanes ---
182 let lane_x = -2.6_f32;
183 let lane_title_z = -0.4_f32;
184 let lane_cfg_z = -0.4_f32;
185 let _lane_cube_z = -0.8_f32;
186 let drum_y = 0.9_f32;
187 let noise_y = -0.4_f32;
188
189 spawn_text(
190 &mut universe,
191 (lane_x, drum_y + 0.55, lane_title_z),
192 0.09,
193 "Audio Source A: Drum (kick)",
194 );
195 spawn_text(
196 &mut universe,
197 (lane_x, drum_y + 0.25, lane_cfg_z),
198 0.07,
199 "type=Drum\namp=0.40\nbase_freq=32Hz\nkick: C0 dur=0.12 vel=0.90\nlookahead=0.10s",
200 );
201
202 spawn_text(
203 &mut universe,
204 (lane_x, noise_y + 0.55, lane_title_z),
205 0.09,
206 "Audio Source B: Noise",
207 );
208 spawn_text(
209 &mut universe,
210 (lane_x, noise_y + 0.25, lane_cfg_z),
211 0.07,
212 "type=Noise\namp=0.06\nnoise: C9 dur=0.06 vel=0.25\noffset=+0.5 beats\nlookahead=0.10s",
213 );
214
215 // Representative cubes for the two audio sources.
216 let viz_root = universe.world.add_component(
217 engine::ecs::component::TransformComponent::new().with_position(0.0, 0.0, 0.0),
218 );
219 universe.add(viz_root);
220 spawn_emissive_cube(
221 &mut universe,
222 viz_root,
223 (lane_x - 0.28, drum_y + 0.55, lane_title_z),
224 0.16,
225 [1.00, 0.90, 0.10, 1.0],
226 );
227 spawn_emissive_cube(
228 &mut universe,
229 viz_root,
230 (lane_x - 0.28, noise_y + 0.55, lane_title_z),
231 0.16,
232 [1.00, 1.00, 1.00, 1.0],
233 );
234
235 // Timeline roots so we can "reset all" via a single set_color action.
236 let kick_timeline_root = universe.world.add_component(
237 engine::ecs::component::TransformComponent::new().with_position(0.0, 0.0, 0.0),
238 );
239 let noise_timeline_root = universe.world.add_component(
240 engine::ecs::component::TransformComponent::new().with_position(0.0, 0.0, 0.0),
241 );
242 let _ = universe.attach(viz_root, kick_timeline_root);
243 let _ = universe.attach(viz_root, noise_timeline_root);
244
245 // Animation with 16 keyframes: each keyframe schedules a kick at offset 0.0,
246 // and a noise hit at offset 0.5.
247 let anim = universe
248 .world
249 .add_component(engine::ecs::component::AnimationComponent::new());
250
251 let kick_dur = 0.12_f32;
252 let noise_dur = 0.06_f32;
253
254 // Requested palette:
255 // - kick: dark yellow -> bright yellow
256 // - noise: grey -> white
257 let kick_dark = [0.35, 0.28, 0.02, 1.0];
258 let kick_bright = [1.00, 0.90, 0.10, 1.0];
259 let noise_dark = [0.35, 0.35, 0.35, 1.0];
260 let noise_bright = [1.00, 1.00, 1.00, 1.0];
261
262 let beat_spacing = 0.38_f32;
263 for i in 0..16 {
264 let kf_beat = i as f64;
265 let kf = universe
266 .world
267 .add_component(engine::ecs::component::KeyframeComponent::new(kf_beat));
268
269 let kick = universe
270 .world
271 .add_component(engine::ecs::component::ActionComponent::new(
272 engine::ecs::IntentValue::AudioSchedulePlay {
273 component_ids: vec![osc_drum_comp],
274 beat_offset: 0.0,
275 beat_context: None,
276 note: Some(
277 engine::ecs::component::MusicNote::c(0, kick_dur).with_velocity(0.9),
278 ),
279 gain: None,
280 rate: None,
281 duration: None,
282 },
283 ));
284
285 let noise = universe
286 .world
287 .add_component(engine::ecs::component::ActionComponent::new(
288 engine::ecs::IntentValue::AudioSchedulePlay {
289 component_ids: vec![osc_noise_comp],
290 beat_offset: 0.5,
291 beat_context: None,
292 note: Some(
293 engine::ecs::component::MusicNote::c(9, noise_dur).with_velocity(0.25),
294 ),
295 gain: None,
296 rate: None,
297 duration: None,
298 },
299 ));
300
301 let _ = universe.attach(anim, kf);
302 let _ = universe.attach(kf, kick);
303 let _ = universe.attach(kf, noise);
304
305 // Each keyframe drives visualization purely via normal actions:
306 // 1) reset all timeline cubes to their base colors
307 // 2) brighten the current beat's cubes
308 let reset_kick_lane =
309 universe
310 .world
311 .add_component(engine::ecs::component::ActionComponent::new(
312 engine::ecs::IntentValue::SetColor {
313 component_ids: vec![kick_timeline_root],
314 rgba: kick_dark,
315 },
316 ));
317 let reset_noise_lane =
318 universe
319 .world
320 .add_component(engine::ecs::component::ActionComponent::new(
321 engine::ecs::IntentValue::SetColor {
322 component_ids: vec![noise_timeline_root],
323 rgba: noise_dark,
324 },
325 ));
326 let _ = universe.attach(kf, reset_kick_lane);
327 let _ = universe.attach(kf, reset_noise_lane);
328
329 // Timeline cubes: one cube per scheduled audio operation.
330 // Kick (offset 0.0) on the drum lane.
331 let kick_x = (kf_beat as f32) * beat_spacing;
332 let kick_cube = spawn_op_cube(
333 &mut universe,
334 kick_timeline_root,
335 (kick_x, drum_y, -1.3),
336 0.10,
337 kick_dark,
338 );
339
340 // Noise (offset 0.5) on the noise lane.
341 let noise_x = ((kf_beat + 0.5) as f32) * beat_spacing;
342 let noise_cube = spawn_op_cube(
343 &mut universe,
344 noise_timeline_root,
345 (noise_x, noise_y, -1.3),
346 0.10,
347 noise_dark,
348 );
349
350 let brighten_kick =
351 universe
352 .world
353 .add_component(engine::ecs::component::ActionComponent::new(
354 engine::ecs::IntentValue::SetColor {
355 component_ids: vec![kick_cube],
356 rgba: kick_bright,
357 },
358 ));
359 let brighten_noise =
360 universe
361 .world
362 .add_component(engine::ecs::component::ActionComponent::new(
363 engine::ecs::IntentValue::SetColor {
364 component_ids: vec![noise_cube],
365 rgba: noise_bright,
366 },
367 ));
368 let _ = universe.attach(kf, brighten_kick);
369 let _ = universe.attach(kf, brighten_noise);
370 }
371
372 universe.add(anim);
373
374 universe.systems.process_commands(
375 &mut universe.world,
376 &mut universe.visuals,
377 &mut universe.render_assets,
378 &mut universe.command_queue,
379 );
380
381 engine::Windowing::run_app(universe).expect("Windowing failed");
382}examples/audio-graph-example.rs (line 114)
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}Sourcepub fn with_bpm(self, bpm: f64) -> Self
pub fn with_bpm(self, bpm: f64) -> Self
Examples found in repository?
examples/text-animation.rs (line 97)
6fn main() {
7 mittens_engine::example_support::ensure_model_assets();
8 utils::logger::init();
9
10 let world = engine::ecs::World::default();
11 let mut universe = engine::Universe::new(world);
12
13 // Input-driven camera rig.
14 // Topology: I { T { C3D } }
15 let input = universe
16 .world
17 .add_component(engine::ecs::component::InputComponent::new().with_speed(1.5));
18 let camera3d = universe
19 .world
20 .add_component(engine::ecs::component::Camera3DComponent::new());
21 let rig_transform = universe.world.add_component(
22 engine::ecs::component::TransformComponent::new().with_position(0.0, 0.0, 2.5),
23 );
24 let input_mode = universe.world.add_component(
25 engine::ecs::component::InputTransformModeComponent::forward_z().with_roll_axis_y(),
26 );
27 let _ = universe.attach(input, input_mode);
28 let _ = universe.attach(input, rig_transform);
29 let _ = universe.attach(rig_transform, camera3d);
30
31 // Small on-screen help.
32 example_util::spawn_desktop_camera_controls_hint(&mut universe, rig_transform);
33 universe.add(input);
34
35 // Light so we can see non-emissive materials.
36 let light_transform = universe.world.add_component(
37 engine::ecs::component::TransformComponent::new().with_position(0.0, 0.0, 2.0),
38 );
39 let light = universe.world.add_component(
40 engine::ecs::component::PointLightComponent::new()
41 .with_distance(25.0)
42 .with_color(1.0, 1.0, 1.0),
43 );
44 let _ = universe.attach(light_transform, light);
45 universe.add(light_transform);
46
47 use engine::ecs::component::{
48 ColorComponent, TextComponent, TextShadowComponent, TextureComponent,
49 TextureFilteringComponent, TransformComponent, TransparentCutoutComponent,
50 };
51
52 // Styled text root.
53 let text_root = universe.world.add_component(
54 TransformComponent::new()
55 .with_position(0.0, 0.0, 0.0)
56 .with_scale(0.25, 0.25, 1.0),
57 );
58
59 // Color must be an ancestor of the glyph renderables.
60 let color_id = universe
61 .world
62 .add_component(ColorComponent::rgba(1.0, 1.0, 1.0, 1.0));
63 let _ = universe.attach(text_root, color_id);
64
65 // This is the component we animate via SetText.
66 let text_id = universe.world.add_component(TextComponent::new(">w<"));
67 let _ = universe.attach(color_id, text_id);
68
69 // Route into cutout pass for cleaner edges.
70 let cutout = universe
71 .world
72 .add_component(TransparentCutoutComponent::new());
73 let _ = universe.attach(text_id, cutout);
74
75 // Font atlas.
76 let tex = universe.world.add_component(TextureComponent::with_uri(
77 "assets/textures/font_system.dds",
78 ));
79 let _ = universe.attach(text_id, tex);
80
81 let shadow = universe.world.add_component(
82 TextShadowComponent::new()
83 .with_scale(1.35)
84 .with_offset([0.06, -0.06, 0.0015]),
85 );
86 let filtering = universe
87 .world
88 .add_component(TextureFilteringComponent::nearest_magnification());
89
90 let _ = universe.attach(text_id, shadow);
91 let _ = universe.attach(text_id, filtering);
92
93 universe.add(text_root);
94
95 let clock_component = universe
96 .world
97 .add_component(engine::ecs::component::ClockComponent::new().with_bpm(140.0));
98 let _ = universe.add(clock_component);
99
100 // Looping animation: 4 beats long, 4 keyframes.
101 let anim = universe
102 .world
103 .add_component(engine::ecs::component::AnimationComponent::new());
104
105 let faces = [">w<", "^w^", "-_-", "o_o"];
106 for (i, &face) in faces.iter().enumerate() {
107 let kf = universe
108 .world
109 .add_component(engine::ecs::component::KeyframeComponent::new(i as f64));
110
111 // Each keyframe emits a SetText intent.
112 let action = universe
113 .world
114 .add_component(engine::ecs::component::ActionComponent::new(
115 engine::ecs::IntentValue::SetText {
116 component_ids: vec![text_id],
117 text: face.to_string(),
118 },
119 ));
120
121 let _ = universe.attach(anim, kf);
122 let _ = universe.attach(kf, action);
123 }
124
125 universe.add(anim);
126
127 // Process init-time registrations (Text expands into glyph subtrees here).
128 universe.systems.process_commands(
129 &mut universe.world,
130 &mut universe.visuals,
131 &mut universe.render_assets,
132 &mut universe.command_queue,
133 );
134
135 engine::Windowing::run_app(universe).expect("Windowing failed");
136}More examples
examples/mesh-factory-example.rs (line 18)
6fn main() {
7 mittens_engine::example_support::ensure_model_assets();
8 utils::logger::init();
9
10 const LABEL_WRAP_AT: usize = 13;
11
12 let world = engine::ecs::World::default();
13 let mut universe = engine::Universe::new(world);
14
15 // add a clock
16 let clock = universe
17 .world
18 .add_component(engine::ecs::component::ClockComponent::new().with_bpm(60.0));
19 universe.add(clock);
20
21 // Input-driven camera rig.
22 // Topology: I { T { C3D } }
23 let input = universe
24 .world
25 .add_component(engine::ecs::component::InputComponent::new().with_speed(1.5));
26 let camera3d = universe
27 .world
28 .add_component(engine::ecs::component::Camera3DComponent::new());
29 let rig_transform = universe.world.add_component(
30 engine::ecs::component::TransformComponent::new().with_position(0.0, 0.0, 11.0),
31 );
32 let input_mode = universe.world.add_component(
33 engine::ecs::component::InputTransformModeComponent::forward_z().with_roll_axis_y(),
34 );
35 let _ = universe.attach(input, input_mode);
36 let _ = universe.attach(input, rig_transform);
37 let _ = universe.attach(rig_transform, camera3d);
38
39 // Topology: I { T { C3D } } — add a small camera-attached controls hint.
40 example_util::spawn_desktop_camera_controls_hint(&mut universe, rig_transform);
41 universe.add(input);
42
43 // Light.
44 let light_transform = universe.world.add_component(
45 engine::ecs::component::TransformComponent::new().with_position(0.0, 2.0, 2.0),
46 );
47 let light = universe.world.add_component(
48 engine::ecs::component::PointLightComponent::new()
49 .with_distance(50.0)
50 .with_color(1.0, 1.0, 1.0),
51 );
52 let _ = universe.attach(light_transform, light);
53 universe.add(light_transform);
54
55 fn spawn_labeled_mesh(
56 universe: &mut engine::Universe,
57 x: f32,
58 y: f32,
59 label: &str,
60 mesh: engine::graphics::primitives::CpuMeshHandle,
61 scale: [f32; 3],
62 color: [f32; 4],
63 ) {
64 use engine::ecs::component::{
65 ActionComponent, AnimationComponent, AnimationState, ColorComponent, EmissiveComponent,
66 KeyframeComponent, RenderableComponent, TextComponent, TransformComponent,
67 };
68 use engine::graphics::primitives::{MaterialHandle, Renderable};
69
70 // Mesh.
71 let root = universe.world.add_component(
72 TransformComponent::new()
73 .with_position(x, y, 0.0)
74 .with_scale(scale[0], scale[1], scale[2]),
75 );
76
77 // Spin each shape around its own +Y axis using AnimationComponent + keyframes.
78 // We fill [0, 2) beats densely so it looks smooth.
79 let anim = universe
80 .world
81 .add_component(AnimationComponent::new().with_state(AnimationState::Looping));
82 let _ = universe.attach(root, anim);
83
84 let steps: usize = 64;
85 for i in 0..steps {
86 let beat = (i as f64) * (2.0 / (steps as f64));
87 let kf = universe.world.add_component(KeyframeComponent::new(beat));
88 let _ = universe.attach(anim, kf);
89
90 // Full turn over 2 beats.
91 let angle = (std::f64::consts::TAU * (beat / 2.0)) as f32;
92 let rotation = utils::math::quat_from_axis_angle([0.0, 1.0, 0.0], angle);
93
94 let action_cid = universe.world.add_component(ActionComponent::new(
95 engine::ecs::IntentValue::UpdateTransform {
96 component_ids: vec![root],
97 translation: [x, y, 0.0],
98 rotation_quat_xyzw: rotation,
99 scale,
100 },
101 ));
102 let _ = universe.attach(kf, action_cid);
103 }
104
105 let renderable = universe
106 .world
107 .add_component(RenderableComponent::new(Renderable::new(
108 mesh,
109 MaterialHandle::TOON_MESH,
110 )));
111 let color_c = universe
112 .world
113 .add_component(ColorComponent::rgba(color[0], color[1], color[2], color[3]));
114 let emissive = universe.world.add_component(EmissiveComponent::on());
115
116 let _ = universe.attach(root, renderable);
117 let _ = universe.attach(renderable, color_c);
118 let _ = universe.attach(renderable, emissive);
119
120 universe.add(root);
121
122 // Label (separate transform so we can scale text independently).
123 let text_root = universe.world.add_component(
124 TransformComponent::new()
125 .with_position(x, y + 0.75, 0.05)
126 .with_scale(0.09, 0.09, 1.0),
127 );
128 let text = universe
129 .world
130 .add_component(TextComponent::with_word_wrap_tokens(
131 label,
132 LABEL_WRAP_AT,
133 ["::", "(", ")", ",", "."],
134 ));
135 let text_color = universe
136 .world
137 .add_component(ColorComponent::rgba(1.0, 1.0, 1.0, 1.0));
138 let text_emissive = universe.world.add_component(EmissiveComponent::on());
139 let _ = universe.attach(text_root, text);
140 let _ = universe.attach(text, text_color);
141 let _ = universe.attach(text, text_emissive);
142 universe.add(text_root);
143 }
144
145 // Built-in meshes (stable ids).
146 let tri = universe
147 .render_assets
148 .get_mesh(engine::graphics::BuiltinMeshType::Triangle2D);
149 let quad = universe
150 .render_assets
151 .get_mesh(engine::graphics::BuiltinMeshType::Quad2D);
152 let cube = universe
153 .render_assets
154 .get_mesh(engine::graphics::BuiltinMeshType::Cube);
155 let tetra = universe
156 .render_assets
157 .get_mesh(engine::graphics::BuiltinMeshType::Tetrahedron);
158 let sphere = universe
159 .render_assets
160 .get_mesh(engine::graphics::BuiltinMeshType::Sphere);
161 let cone = universe
162 .render_assets
163 .get_mesh(engine::graphics::BuiltinMeshType::Cone);
164 let circle = universe
165 .render_assets
166 .get_mesh(engine::graphics::BuiltinMeshType::Circle2D);
167
168 // Layout.
169 let y = 0.0;
170 let dx = 1.8;
171 let x0 = -dx * 3.0;
172
173 spawn_labeled_mesh(
174 &mut universe,
175 x0 + dx * 0.0,
176 y,
177 "Triangle2D\nMeshFactory::triangle_2d()",
178 tri,
179 [1.0, 1.0, 1.0],
180 [1.0, 1.0, 1.0, 1.0],
181 );
182 spawn_labeled_mesh(
183 &mut universe,
184 x0 + dx * 1.0,
185 y,
186 "Quad2D\nMeshFactory::quad_2d()",
187 quad,
188 [1.0, 1.0, 1.0],
189 [1.0, 1.0, 1.0, 1.0],
190 );
191 spawn_labeled_mesh(
192 &mut universe,
193 x0 + dx * 2.0,
194 y,
195 "Cube\nMeshFactory::cube()",
196 cube,
197 [0.9, 0.9, 0.9],
198 [1.0, 1.0, 1.0, 1.0],
199 );
200 spawn_labeled_mesh(
201 &mut universe,
202 x0 + dx * 3.0,
203 y,
204 "Tetrahedron\nMeshFactory::tetrahedron()",
205 tetra,
206 [1.0, 1.0, 1.0],
207 [1.0, 1.0, 1.0, 1.0],
208 );
209 spawn_labeled_mesh(
210 &mut universe,
211 x0 + dx * 4.0,
212 y,
213 "Sphere\nMeshFactory::sphere()",
214 sphere,
215 [1.0, 1.0, 1.0],
216 [1.0, 1.0, 1.0, 1.0],
217 );
218 spawn_labeled_mesh(
219 &mut universe,
220 x0 + dx * 5.0,
221 y,
222 "Cone\nMeshFactory::cone(32)",
223 cone,
224 [1.0, 1.0, 1.0],
225 [1.0, 1.0, 1.0, 1.0],
226 );
227 spawn_labeled_mesh(
228 &mut universe,
229 x0 + dx * 6.0,
230 y,
231 "Circle2D\nMeshFactory::circle_2d(0.45, 0.5, 64)",
232 circle,
233 [1.0, 1.0, 1.0],
234 [1.0, 1.0, 1.0, 1.0],
235 );
236
237 // Process init-time registrations (Text expands into glyph subtrees here).
238 universe.systems.process_commands(
239 &mut universe.world,
240 &mut universe.visuals,
241 &mut universe.render_assets,
242 &mut universe.command_queue,
243 );
244
245 engine::Windowing::run_app(universe).expect("Windowing failed");
246}examples/animation-for-topology.rs (line 137)
80fn main() {
81 mittens_engine::example_support::ensure_model_assets();
82 utils::logger::init();
83
84 let world = engine::ecs::World::default();
85 let mut universe = engine::Universe::new(world);
86
87 // Minimal scene with a camera so the window opens.
88 let clear = universe
89 .world
90 .add_component(engine::ecs::component::BackgroundColorComponent::new());
91 let clear_c = universe
92 .world
93 .add_component(engine::ecs::component::ColorComponent::rgba(
94 0.06, 0.06, 0.07, 1.0,
95 ));
96 let _ = universe.world.add_child(clear, clear_c);
97 universe.add(clear);
98
99 // Input-driven camera rig.
100 let input = universe
101 .world
102 .add_component(engine::ecs::component::InputComponent::new().with_speed(2.0));
103 let rig_transform = universe.world.add_component(
104 engine::ecs::component::TransformComponent::new().with_position(0.0, 0.0, 9.0),
105 );
106 let input_mode = universe.world.add_component(
107 engine::ecs::component::InputTransformModeComponent::forward_z().with_roll_axis_y(),
108 );
109 let camera3d = universe.world.add_component(
110 engine::ecs::component::Camera3DComponent::new()
111 .with_far(250.0)
112 .with_fov(70.0),
113 );
114 let _ = universe.attach(input, input_mode);
115 let _ = universe.attach(input, rig_transform);
116 let _ = universe.attach(rig_transform, camera3d);
117
118 // Topology: I { T { C3D } } — add a small camera-attached controls hint.
119 example_util::spawn_desktop_camera_controls_hint(&mut universe, rig_transform);
120 universe.add(input);
121
122 // Light so we can see non-emissive materials.
123 let light_tx = universe.world.add_component(
124 engine::ecs::component::TransformComponent::new().with_position(2.0, 3.5, 6.0),
125 );
126 let light = universe.world.add_component(
127 engine::ecs::component::PointLightComponent::new()
128 .with_distance(30.0)
129 .with_color(1.0, 1.0, 1.0),
130 );
131 let _ = universe.attach(light_tx, light);
132 universe.add(light_tx);
133
134 // ClockComponent drives the animation timeline in beats.
135 let clock = universe
136 .world
137 .add_component(engine::ecs::component::ClockComponent::new().with_bpm(140.0));
138 universe.add(clock);
139
140 // Root for all visualization objects.
141 let viz_root = universe.world.add_component(
142 engine::ecs::component::TransformComponent::new().with_position(0.0, 0.0, 0.0),
143 );
144 universe.add(viz_root);
145
146 let anchor_count = 16usize;
147 let cube_pool_size = 8usize;
148
149 // Three grids side-by-side.
150 let layout_a = GridLayout {
151 origin: (-6.0, 0.0, 0.0),
152 spacing: 1.1,
153 };
154 let layout_b = GridLayout {
155 origin: (0.0, 0.0, 0.0),
156 spacing: 1.1,
157 };
158 let layout_c = GridLayout {
159 origin: (6.0, 0.0, 0.0),
160 spacing: 1.1,
161 };
162
163 // --- Grid A: detach + re-attach (reparent) ---
164 let grid_a_root = universe
165 .world
166 .add_component(engine::ecs::component::TransformComponent::new());
167 let _ = universe.attach(viz_root, grid_a_root);
168
169 let mut anchors_a: Vec<engine::ecs::ComponentId> = Vec::with_capacity(anchor_count);
170 for i in 0..anchor_count {
171 let (x, y, z) = grid_anchor_local(layout_a, i);
172 let anchor = universe.world.add_component(
173 engine::ecs::component::TransformComponent::new().with_position(x, y, z),
174 );
175 let _ = universe.attach(grid_a_root, anchor);
176 anchors_a.push(anchor);
177 spawn_emissive_marker_cube(
178 &mut universe,
179 anchor,
180 (0.0, -0.35, 0.0),
181 0.06,
182 [0.18, 0.18, 0.22, 1.0],
183 );
184 }
185
186 let mut cubes_a: Vec<engine::ecs::ComponentId> = Vec::with_capacity(cube_pool_size);
187 for i in 0..cube_pool_size {
188 let t = (i as f32) / ((cube_pool_size - 1) as f32).max(1.0);
189 let rgba = [0.10, 0.40 + 0.50 * t, 0.90 - 0.70 * t, 1.0];
190 cubes_a.push(spawn_detached_cube_prefab(&mut universe, 0.22, rgba));
191 }
192
193 let anim_a = universe
194 .world
195 .add_component(engine::ecs::component::AnimationComponent::new());
196 for i in 0..anchor_count {
197 let cube = cubes_a[i % cube_pool_size];
198 let parent = anchors_a[i];
199
200 // Explicit detach+attach to test topology changes via animations.
201
202 // Put them on separate keyframes so ordering is time-deterministic.
203 let beat_detach = i as f64;
204 let beat_attach = i as f64 + 0.05;
205
206 let kf_detach = universe
207 .world
208 .add_component(engine::ecs::component::KeyframeComponent::new(beat_detach));
209 let _ = universe.attach(anim_a, kf_detach);
210 let detach_action =
211 universe
212 .world
213 .add_component(engine::ecs::component::ActionComponent::new(
214 engine::ecs::IntentValue::Detach {
215 component_ids: vec![cube],
216 },
217 ));
218 let _ = universe.attach(kf_detach, detach_action);
219
220 let kf_attach = universe
221 .world
222 .add_component(engine::ecs::component::KeyframeComponent::new(beat_attach));
223 let _ = universe.attach(anim_a, kf_attach);
224 let attach_action =
225 universe
226 .world
227 .add_component(engine::ecs::component::ActionComponent::new(
228 engine::ecs::IntentValue::Attach {
229 parents: vec![parent],
230 child: cube,
231 },
232 ));
233 let _ = universe.attach(kf_attach, attach_action);
234 }
235 universe.add(anim_a);
236
237 // --- Grid B: continuously spawn (attach_clone) + delete-behind (remove_child) ---
238 //
239 // Uses the new actions:
240 // - `Action::attach_clone(parent, prefab_root)`
241 // - `Action::remove_child(parent, index)`
242 //
243 // This avoids needing a pre-built pool of cube ComponentIds.
244 let grid_b_root = universe
245 .world
246 .add_component(engine::ecs::component::TransformComponent::new());
247 let _ = universe.attach(viz_root, grid_b_root);
248
249 let mut anchors_b: Vec<engine::ecs::ComponentId> = Vec::with_capacity(anchor_count);
250 for i in 0..anchor_count {
251 let (x, y, z) = grid_anchor_local(layout_b, i);
252 let anchor = universe.world.add_component(
253 engine::ecs::component::TransformComponent::new().with_position(x, y, z),
254 );
255 let _ = universe.attach(grid_b_root, anchor);
256 anchors_b.push(anchor);
257
258 // Marker is attached under the grid root (not under the anchor), so anchor child index 0
259 // remains reserved for the dynamic cube subtree.
260 spawn_emissive_marker_cube(
261 &mut universe,
262 grid_b_root,
263 (x, y - 0.35, z),
264 0.06,
265 [0.22, 0.18, 0.18, 1.0],
266 );
267 }
268
269 // Detached prefab that will be cloned on demand (reddish cube).
270 let prefab_b = spawn_detached_cube_prefab(&mut universe, 0.20, [0.90, 0.25, 0.15, 1.0]);
271
272 let steps_b = 32usize;
273 let window = 8usize;
274
275 let anim_b = universe
276 .world
277 .add_component(engine::ecs::component::AnimationComponent::new());
278
279 // Phase 1: each beat, spawn a cube under an anchor; delete-behind by removing child(0).
280 for i in 0..steps_b {
281 let parent = anchors_b[i % anchor_count];
282
283 let beat_attach = i as f64;
284 let beat_remove = i as f64 + 0.05;
285
286 let kf_attach = universe
287 .world
288 .add_component(engine::ecs::component::KeyframeComponent::new(beat_attach));
289 let _ = universe.attach(anim_b, kf_attach);
290
291 let attach_action =
292 universe
293 .world
294 .add_component(engine::ecs::component::ActionComponent::new(
295 engine::ecs::IntentValue::AttachClone {
296 parents: vec![parent],
297 prefab_root: prefab_b,
298 },
299 ));
300 let _ = universe.attach(kf_attach, attach_action);
301
302 if i >= window {
303 let remove_parent = anchors_b[(i - window) % anchor_count];
304 let kf_remove = universe
305 .world
306 .add_component(engine::ecs::component::KeyframeComponent::new(beat_remove));
307 let _ = universe.attach(anim_b, kf_remove);
308
309 let remove_action =
310 universe
311 .world
312 .add_component(engine::ecs::component::ActionComponent::new(
313 engine::ecs::IntentValue::RemoveChild {
314 parents: vec![remove_parent],
315 index: 0,
316 },
317 ));
318 let _ = universe.attach(kf_remove, remove_action);
319 }
320 }
321
322 // Phase 2: cleanup tail so the loop doesn't accumulate cubes.
323 for j in 0..window {
324 let remove_parent = anchors_b[(steps_b - window + j) % anchor_count];
325 let beat_remove = steps_b as f64 + j as f64 + 0.05;
326
327 let kf_remove = universe
328 .world
329 .add_component(engine::ecs::component::KeyframeComponent::new(beat_remove));
330 let _ = universe.attach(anim_b, kf_remove);
331
332 let remove_action =
333 universe
334 .world
335 .add_component(engine::ecs::component::ActionComponent::new(
336 engine::ecs::IntentValue::RemoveChild {
337 parents: vec![remove_parent],
338 index: 0,
339 },
340 ));
341 let _ = universe.attach(kf_remove, remove_action);
342 }
343
344 universe.add(anim_b);
345
346 // --- Grid C: move cubes via set_position (no topology changes) ---
347 let grid_c_root = universe
348 .world
349 .add_component(engine::ecs::component::TransformComponent::new());
350 let _ = universe.attach(viz_root, grid_c_root);
351
352 let mut anchors_c: Vec<(f32, f32, f32)> = Vec::with_capacity(anchor_count);
353 for i in 0..anchor_count {
354 let (x, y, z) = grid_anchor_local(layout_c, i);
355 anchors_c.push((x, y, z));
356
357 let anchor = universe.world.add_component(
358 engine::ecs::component::TransformComponent::new().with_position(x, y, z),
359 );
360 let _ = universe.attach(grid_c_root, anchor);
361 spawn_emissive_marker_cube(
362 &mut universe,
363 anchor,
364 (0.0, -0.35, 0.0),
365 0.06,
366 [0.18, 0.22, 0.18, 1.0],
367 );
368 }
369
370 let mut cubes_c: Vec<engine::ecs::ComponentId> = Vec::with_capacity(cube_pool_size);
371 for i in 0..cube_pool_size {
372 let t = (i as f32) / ((cube_pool_size - 1) as f32).max(1.0);
373 let rgba = [0.25, 0.85 - 0.55 * t, 0.25 + 0.55 * t, 1.0];
374 let cube_root = spawn_detached_cube_prefab(&mut universe, 0.22, rgba);
375 let _ = universe.attach(grid_c_root, cube_root);
376 cubes_c.push(cube_root);
377 }
378
379 let anim_c = universe
380 .world
381 .add_component(engine::ecs::component::AnimationComponent::new());
382 for i in 0..anchor_count {
383 let kf = universe
384 .world
385 .add_component(engine::ecs::component::KeyframeComponent::new(i as f64));
386 let _ = universe.attach(anim_c, kf);
387
388 let cube = cubes_c[i % cube_pool_size];
389 let (x, y, z) = anchors_c[i];
390 let setpos_action =
391 universe
392 .world
393 .add_component(engine::ecs::component::ActionComponent::new(
394 engine::ecs::IntentValue::SetPosition {
395 component_ids: vec![cube],
396 position: [x, y, z],
397 },
398 ));
399 let _ = universe.attach(kf, setpos_action);
400 }
401 universe.add(anim_c);
402
403 universe.systems.process_commands(
404 &mut universe.world,
405 &mut universe.visuals,
406 &mut universe.render_assets,
407 &mut universe.command_queue,
408 );
409
410 engine::Windowing::run_app(universe).expect("Windowing failed");
411}examples/vtuber-joints-example.rs (line 24)
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/animation-example.rs (line 63)
6fn main() {
7 mittens_engine::example_support::ensure_model_assets();
8 utils::logger::init();
9
10 let world = engine::ecs::World::default();
11 let mut universe = engine::Universe::new(world);
12
13 // Minimal scene with a camera so the window opens.
14 let clear = universe
15 .world
16 .add_component(engine::ecs::component::BackgroundColorComponent::new());
17 let clear_c = universe
18 .world
19 .add_component(engine::ecs::component::ColorComponent::rgba(
20 0.08, 0.08, 0.08, 1.0,
21 ));
22 let _ = universe.world.add_child(clear, clear_c);
23 universe.add(clear);
24
25 // Input-driven camera rig.
26 let input = universe
27 .world
28 .add_component(engine::ecs::component::InputComponent::new().with_speed(2.0));
29 let rig_transform = universe.world.add_component(
30 engine::ecs::component::TransformComponent::new().with_position(2.0, 0.0, 7.0),
31 );
32 let input_mode = universe.world.add_component(
33 engine::ecs::component::InputTransformModeComponent::forward_z().with_roll_axis_y(),
34 );
35 let camera3d = universe.world.add_component(
36 engine::ecs::component::Camera3DComponent::new()
37 .with_far(250.0)
38 .with_fov(70.0),
39 );
40 let _ = universe.attach(input, input_mode);
41 let _ = universe.attach(input, rig_transform);
42 let _ = universe.attach(rig_transform, camera3d);
43
44 // Topology: I { T { C3D } } — add a small camera-attached controls hint.
45 example_util::spawn_desktop_camera_controls_hint(&mut universe, rig_transform);
46 universe.add(input);
47
48 // Light so we can see non-emissive materials (even though our cubes are emissive).
49 let light_tx = universe.world.add_component(
50 engine::ecs::component::TransformComponent::new().with_position(0.0, 2.5, 4.0),
51 );
52 let light = universe.world.add_component(
53 engine::ecs::component::PointLightComponent::new()
54 .with_distance(25.0)
55 .with_color(1.0, 1.0, 1.0),
56 );
57 let _ = universe.attach(light_tx, light);
58 universe.add(light_tx);
59
60 // ClockComponent sets global tempo.
61 let clock = universe
62 .world
63 .add_component(engine::ecs::component::ClockComponent::new().with_bpm(128.0));
64 universe.add(clock);
65
66 // Audio output + oscillators driven by scheduled actions.
67 let audio_out = universe
68 .world
69 .add_component(engine::ecs::component::AudioOutputComponent::new());
70 universe.add(audio_out);
71
72 // Noise oscillator: short white-noise hits.
73 let osc_noise = engine::ecs::component::AudioOscillator::noise()
74 .with_frequency(0.0)
75 .with_amplitude(0.06)
76 .with_enabled(false);
77 let osc_noise_comp =
78 universe
79 .world
80 .add_component(engine::ecs::component::AudioOscillatorComponent::single(
81 osc_noise,
82 ));
83 let _ = universe.attach(audio_out, osc_noise_comp);
84
85 // Drum oscillator: retriggers (phase + sweep) every enable.
86 let osc_drum = engine::ecs::component::AudioOscillator::drum()
87 // A low-ish pitch scale; actual pitch is set by scheduled notes.
88 .with_frequency(32.0)
89 .with_amplitude(0.40)
90 .with_enabled(false);
91 let osc_drum_comp =
92 universe
93 .world
94 .add_component(engine::ecs::component::AudioOscillatorComponent::single(
95 osc_drum,
96 ));
97
98 let _ = universe.attach(audio_out, osc_drum_comp);
99
100 // --- Visual layout helpers ---
101 fn spawn_text(universe: &mut engine::Universe, pos: (f32, f32, f32), scale: f32, text: &str) {
102 let tx = universe.world.add_component(
103 engine::ecs::component::TransformComponent::new()
104 .with_position(pos.0, pos.1, pos.2)
105 .with_scale(scale, scale, 1.0),
106 );
107 let t =
108 universe
109 .world
110 .add_component(engine::ecs::component::TextComponent::with_word_wrap(
111 text, 38,
112 ));
113 let _ = universe.attach(tx, t);
114 universe.add(tx);
115 }
116
117 fn spawn_emissive_cube(
118 universe: &mut engine::Universe,
119 parent: engine::ecs::ComponentId,
120 pos: (f32, f32, f32),
121 scale: f32,
122 rgba: [f32; 4],
123 ) {
124 let tx = universe.world.add_component(
125 engine::ecs::component::TransformComponent::new()
126 .with_position(pos.0, pos.1, pos.2)
127 .with_scale(scale, scale, scale),
128 );
129 let r = universe
130 .world
131 .add_component(engine::ecs::component::RenderableComponent::cube());
132 let c = universe
133 .world
134 .add_component(engine::ecs::component::ColorComponent::rgba(
135 rgba[0], rgba[1], rgba[2], rgba[3],
136 ));
137 let e = universe
138 .world
139 .add_component(engine::ecs::component::EmissiveComponent::on());
140 let _ = universe.attach(parent, tx);
141 let _ = universe.attach(tx, r);
142 let _ = universe.attach(r, c);
143 let _ = universe.attach(r, e);
144 }
145
146 fn spawn_op_cube(
147 universe: &mut engine::Universe,
148 parent: engine::ecs::ComponentId,
149 pos: (f32, f32, f32),
150 scale: f32,
151 base_rgba: [f32; 4],
152 ) -> engine::ecs::ComponentId {
153 let tx = universe.world.add_component(
154 engine::ecs::component::TransformComponent::new()
155 .with_position(pos.0, pos.1, pos.2)
156 .with_scale(scale, scale, scale),
157 );
158 let r = universe
159 .world
160 .add_component(engine::ecs::component::RenderableComponent::cube());
161 let c = universe
162 .world
163 .add_component(engine::ecs::component::ColorComponent::rgba(
164 base_rgba[0],
165 base_rgba[1],
166 base_rgba[2],
167 base_rgba[3],
168 ));
169 let e = universe
170 .world
171 .add_component(engine::ecs::component::EmissiveComponent::on());
172
173 let _ = universe.attach(parent, tx);
174 let _ = universe.attach(tx, r);
175 let _ = universe.attach(r, c);
176 let _ = universe.attach(r, e);
177
178 tx
179 }
180
181 // --- HUD / lanes ---
182 let lane_x = -2.6_f32;
183 let lane_title_z = -0.4_f32;
184 let lane_cfg_z = -0.4_f32;
185 let _lane_cube_z = -0.8_f32;
186 let drum_y = 0.9_f32;
187 let noise_y = -0.4_f32;
188
189 spawn_text(
190 &mut universe,
191 (lane_x, drum_y + 0.55, lane_title_z),
192 0.09,
193 "Audio Source A: Drum (kick)",
194 );
195 spawn_text(
196 &mut universe,
197 (lane_x, drum_y + 0.25, lane_cfg_z),
198 0.07,
199 "type=Drum\namp=0.40\nbase_freq=32Hz\nkick: C0 dur=0.12 vel=0.90\nlookahead=0.10s",
200 );
201
202 spawn_text(
203 &mut universe,
204 (lane_x, noise_y + 0.55, lane_title_z),
205 0.09,
206 "Audio Source B: Noise",
207 );
208 spawn_text(
209 &mut universe,
210 (lane_x, noise_y + 0.25, lane_cfg_z),
211 0.07,
212 "type=Noise\namp=0.06\nnoise: C9 dur=0.06 vel=0.25\noffset=+0.5 beats\nlookahead=0.10s",
213 );
214
215 // Representative cubes for the two audio sources.
216 let viz_root = universe.world.add_component(
217 engine::ecs::component::TransformComponent::new().with_position(0.0, 0.0, 0.0),
218 );
219 universe.add(viz_root);
220 spawn_emissive_cube(
221 &mut universe,
222 viz_root,
223 (lane_x - 0.28, drum_y + 0.55, lane_title_z),
224 0.16,
225 [1.00, 0.90, 0.10, 1.0],
226 );
227 spawn_emissive_cube(
228 &mut universe,
229 viz_root,
230 (lane_x - 0.28, noise_y + 0.55, lane_title_z),
231 0.16,
232 [1.00, 1.00, 1.00, 1.0],
233 );
234
235 // Timeline roots so we can "reset all" via a single set_color action.
236 let kick_timeline_root = universe.world.add_component(
237 engine::ecs::component::TransformComponent::new().with_position(0.0, 0.0, 0.0),
238 );
239 let noise_timeline_root = universe.world.add_component(
240 engine::ecs::component::TransformComponent::new().with_position(0.0, 0.0, 0.0),
241 );
242 let _ = universe.attach(viz_root, kick_timeline_root);
243 let _ = universe.attach(viz_root, noise_timeline_root);
244
245 // Animation with 16 keyframes: each keyframe schedules a kick at offset 0.0,
246 // and a noise hit at offset 0.5.
247 let anim = universe
248 .world
249 .add_component(engine::ecs::component::AnimationComponent::new());
250
251 let kick_dur = 0.12_f32;
252 let noise_dur = 0.06_f32;
253
254 // Requested palette:
255 // - kick: dark yellow -> bright yellow
256 // - noise: grey -> white
257 let kick_dark = [0.35, 0.28, 0.02, 1.0];
258 let kick_bright = [1.00, 0.90, 0.10, 1.0];
259 let noise_dark = [0.35, 0.35, 0.35, 1.0];
260 let noise_bright = [1.00, 1.00, 1.00, 1.0];
261
262 let beat_spacing = 0.38_f32;
263 for i in 0..16 {
264 let kf_beat = i as f64;
265 let kf = universe
266 .world
267 .add_component(engine::ecs::component::KeyframeComponent::new(kf_beat));
268
269 let kick = universe
270 .world
271 .add_component(engine::ecs::component::ActionComponent::new(
272 engine::ecs::IntentValue::AudioSchedulePlay {
273 component_ids: vec![osc_drum_comp],
274 beat_offset: 0.0,
275 beat_context: None,
276 note: Some(
277 engine::ecs::component::MusicNote::c(0, kick_dur).with_velocity(0.9),
278 ),
279 gain: None,
280 rate: None,
281 duration: None,
282 },
283 ));
284
285 let noise = universe
286 .world
287 .add_component(engine::ecs::component::ActionComponent::new(
288 engine::ecs::IntentValue::AudioSchedulePlay {
289 component_ids: vec![osc_noise_comp],
290 beat_offset: 0.5,
291 beat_context: None,
292 note: Some(
293 engine::ecs::component::MusicNote::c(9, noise_dur).with_velocity(0.25),
294 ),
295 gain: None,
296 rate: None,
297 duration: None,
298 },
299 ));
300
301 let _ = universe.attach(anim, kf);
302 let _ = universe.attach(kf, kick);
303 let _ = universe.attach(kf, noise);
304
305 // Each keyframe drives visualization purely via normal actions:
306 // 1) reset all timeline cubes to their base colors
307 // 2) brighten the current beat's cubes
308 let reset_kick_lane =
309 universe
310 .world
311 .add_component(engine::ecs::component::ActionComponent::new(
312 engine::ecs::IntentValue::SetColor {
313 component_ids: vec![kick_timeline_root],
314 rgba: kick_dark,
315 },
316 ));
317 let reset_noise_lane =
318 universe
319 .world
320 .add_component(engine::ecs::component::ActionComponent::new(
321 engine::ecs::IntentValue::SetColor {
322 component_ids: vec![noise_timeline_root],
323 rgba: noise_dark,
324 },
325 ));
326 let _ = universe.attach(kf, reset_kick_lane);
327 let _ = universe.attach(kf, reset_noise_lane);
328
329 // Timeline cubes: one cube per scheduled audio operation.
330 // Kick (offset 0.0) on the drum lane.
331 let kick_x = (kf_beat as f32) * beat_spacing;
332 let kick_cube = spawn_op_cube(
333 &mut universe,
334 kick_timeline_root,
335 (kick_x, drum_y, -1.3),
336 0.10,
337 kick_dark,
338 );
339
340 // Noise (offset 0.5) on the noise lane.
341 let noise_x = ((kf_beat + 0.5) as f32) * beat_spacing;
342 let noise_cube = spawn_op_cube(
343 &mut universe,
344 noise_timeline_root,
345 (noise_x, noise_y, -1.3),
346 0.10,
347 noise_dark,
348 );
349
350 let brighten_kick =
351 universe
352 .world
353 .add_component(engine::ecs::component::ActionComponent::new(
354 engine::ecs::IntentValue::SetColor {
355 component_ids: vec![kick_cube],
356 rgba: kick_bright,
357 },
358 ));
359 let brighten_noise =
360 universe
361 .world
362 .add_component(engine::ecs::component::ActionComponent::new(
363 engine::ecs::IntentValue::SetColor {
364 component_ids: vec![noise_cube],
365 rgba: noise_bright,
366 },
367 ));
368 let _ = universe.attach(kf, brighten_kick);
369 let _ = universe.attach(kf, brighten_noise);
370 }
371
372 universe.add(anim);
373
374 universe.systems.process_commands(
375 &mut universe.world,
376 &mut universe.visuals,
377 &mut universe.render_assets,
378 &mut universe.command_queue,
379 );
380
381 engine::Windowing::run_app(universe).expect("Windowing failed");
382}examples/audio-graph-example.rs (line 114)
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}pub fn id(&self) -> Option<ComponentId>
Trait Implementations§
Source§impl Clone for ClockComponent
impl Clone for ClockComponent
Source§fn clone(&self) -> ClockComponent
fn clone(&self) -> ClockComponent
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 ClockComponent
impl Component for ClockComponent
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 ClockComponent
Source§impl Debug for ClockComponent
impl Debug for ClockComponent
Auto Trait Implementations§
impl Freeze for ClockComponent
impl RefUnwindSafe for ClockComponent
impl Send for ClockComponent
impl Sync for ClockComponent
impl Unpin for ClockComponent
impl UnsafeUnpin for ClockComponent
impl UnwindSafe for ClockComponent
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.