1use mittens_engine::engine::ecs::component::{
2 AmbientLightComponent, BackgroundColorComponent, Camera3DComponent, CameraXRComponent,
3 ClockComponent, ColorComponent, DirectionalLightComponent, EditorComponent, EmissiveComponent,
4 GLTFComponent, InputComponent, InputTransformModeComponent, InputXRComponent, MeshComponent,
5 PointerComponent, RaycastableComponent, RenderableComponent, SkinnedMeshComponent,
6 TransformComponent,
7};
8use mittens_engine::{engine, utils};
9use std::collections::{HashMap, HashSet};
10
11#[path = "example_util/mod.rs"]
12mod example_util;
13
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 let clock = universe
23 .world
24 .add_component(ClockComponent::new().with_bpm(60.0));
25 universe.add(clock);
26
27 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 let ambient = universe
39 .world
40 .add_component(AmbientLightComponent::rgb(0.10, 0.10, 0.12));
41 universe.add(ambient);
42
43 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 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 let pointer = universe.world.add_component(PointerComponent::new());
65 let _ = universe.attach(camera3d, pointer);
66
67 example_util::spawn_desktop_camera_controls_hint(&mut universe, rig_transform);
69
70 universe.add(input);
71
72 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 let model_uri = "assets/models/pc-rei.hoodie.glb";
99
100 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 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 universe.add(xr_input);
130 universe.add(editor_root);
131
132 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; 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 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 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 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 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 {
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 let _ = universe.attach(editor_root, transform);
225 };
226
227 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 {
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 universe
302 .systems
303 .gltf
304 .flush_mesh_imports_only(&mut universe.render_assets);
305
306 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 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}
385
386fn select_named_joints(
387 world: &engine::ecs::World,
388 all_joints: &[(usize, engine::ecs::ComponentId)],
389 names: &[String],
390) -> Option<Vec<(usize, engine::ecs::ComponentId)>> {
391 if names.is_empty() {
392 return None;
393 }
394
395 let mut out: Vec<(usize, engine::ecs::ComponentId)> = Vec::new();
396 for wanted in names {
397 let mut found: Option<(usize, engine::ecs::ComponentId)> = None;
398 for &(node_index, joint_tx) in all_joints.iter() {
399 let name = world
400 .get_component_record(joint_tx)
401 .map(|r| r.name.as_str())
402 .unwrap_or("");
403 if name == wanted {
404 found = Some((node_index, joint_tx));
405 break;
406 }
407 }
408
409 if let Some(v) = found {
410 out.push(v);
411 } else {
412 println!(
413 "[vtuber-joints-example] warning: target joint '{}' not found (check names via REPL tree)",
414 wanted
415 );
416 }
417 }
418
419 if out.is_empty() { None } else { Some(out) }
420}
421
422fn debug_print_selected_joint_influence(
423 universe: &engine::Universe,
424 mesh_key: &str,
425 model_root: engine::ecs::ComponentId,
426 selected: &[(usize, engine::ecs::ComponentId)],
427) {
428 let Some(renderable) = find_renderable_by_mesh_key(&universe.world, model_root, mesh_key)
429 else {
430 println!(
431 "[vtuber-joints-example] influence: mesh_key='{}' renderable not found",
432 mesh_key
433 );
434 return;
435 };
436
437 let renderable_handle = universe
438 .world
439 .get_component_by_id_as::<RenderableComponent>(renderable)
440 .and_then(|r| r.get_handle());
441 println!(
442 "[vtuber-joints-example] influence: mesh_key='{}' renderable={:?} instance_handle={:?}",
443 mesh_key, renderable, renderable_handle
444 );
445 let Some(skin_id) = find_skin_id_for_renderable(&universe.world, renderable) else {
446 println!(
447 "[vtuber-joints-example] influence: mesh_key='{}' has no skin_id",
448 mesh_key
449 );
450 return;
451 };
452 let Some(skin) = universe.visuals.skin(skin_id) else {
453 println!(
454 "[vtuber-joints-example] influence: skin_id={:?} missing in visuals",
455 skin_id
456 );
457 return;
458 };
459 let Some(cpu_h) = universe.render_assets.imported_mesh(mesh_key) else {
460 println!(
461 "[vtuber-joints-example] influence: mesh_key='{}' missing in RenderAssets (did meshes register?)",
462 mesh_key
463 );
464 return;
465 };
466 let Some(cpu) = universe.render_assets.cpu_mesh(cpu_h) else {
467 println!(
468 "[vtuber-joints-example] influence: mesh_key='{}' cpu mesh handle invalid",
469 mesh_key
470 );
471 return;
472 };
473 let (Some(joints0), Some(weights0)) = (cpu.joints0.as_ref(), cpu.weights0.as_ref()) else {
474 println!(
475 "[vtuber-joints-example] influence: mesh_key='{}' has no skin attributes",
476 mesh_key
477 );
478 return;
479 };
480
481 let joint_count = skin.joint_count();
482 let totals = compute_total_weight_per_joint(joints0, weights0, joint_count);
483
484 println!(
485 "[vtuber-joints-example] influence: mesh_key='{}' verts={} joints={}",
486 mesh_key,
487 cpu.vertices.len(),
488 joint_count
489 );
490
491 for &(node_index, joint_tx) in selected.iter() {
492 let skin_joint_index = skin
494 .joint_node_indices
495 .iter()
496 .position(|&ni| ni == node_index);
497
498 let name = universe
499 .world
500 .get_component_record(joint_tx)
501 .map(|r| r.name.as_str())
502 .unwrap_or("<unknown>");
503
504 match skin_joint_index {
505 Some(j) => {
506 let total = totals.get(j).copied().unwrap_or(0.0);
507 println!(
508 " influence: name='{}' node_index={} skin_joint_index={} total_weight={:.3}",
509 name, node_index, j, total
510 );
511 }
512 None => {
513 println!(
514 " influence: name='{}' node_index={} skin_joint_index=<none>",
515 name, node_index
516 );
517 }
518 }
519 }
520}
521
522fn compute_total_weight_per_joint(
523 joints0: &[[u16; 4]],
524 weights0: &[[f32; 4]],
525 joint_count: usize,
526) -> Vec<f32> {
527 let mut totals = vec![0.0f32; joint_count];
528 if joint_count == 0 {
529 return totals;
530 }
531 for (jv, wv) in joints0.iter().zip(weights0.iter()) {
532 for lane in 0..4 {
533 let j = jv[lane] as usize;
534 if j >= joint_count {
535 continue;
536 }
537 let w = wv[lane];
538 if w > 0.0 {
539 totals[j] += w;
540 }
541 }
542 }
543 totals
544}
545
546fn select_body_prim0_influencers(
547 universe: &engine::Universe,
548 model_root: engine::ecs::ComponentId,
549 mesh_key: &str,
550 count: usize,
551 node_index_to_transform: &HashMap<usize, engine::ecs::ComponentId>,
552) -> Option<Vec<(usize, engine::ecs::ComponentId)>> {
553 if count == 0 {
554 return Some(Vec::new());
555 }
556
557 let renderable = find_renderable_by_mesh_key(&universe.world, model_root, mesh_key)?;
558 let skin_id = find_skin_id_for_renderable(&universe.world, renderable)?;
559 let skin = universe.visuals.skin(skin_id)?;
560
561 let cpu_h = universe.render_assets.imported_mesh(mesh_key)?;
562 let cpu = universe.render_assets.cpu_mesh(cpu_h)?;
563 let joints0 = cpu.joints0.as_ref()?;
564 let weights0 = cpu.weights0.as_ref()?;
565
566 let joint_count = skin.joint_count();
567 if joint_count == 0 {
568 return None;
569 }
570
571 let mut total_weight_per_joint: Vec<f32> = vec![0.0; joint_count];
572 for (jv, wv) in joints0.iter().zip(weights0.iter()) {
573 for lane in 0..4 {
574 let j = jv[lane] as usize;
575 if j >= joint_count {
576 continue;
577 }
578 let w = wv[lane];
579 if w > 0.0 {
580 total_weight_per_joint[j] += w;
581 }
582 }
583 }
584
585 let mut ranked: Vec<(usize, f32)> =
586 total_weight_per_joint.iter().copied().enumerate().collect();
587 ranked.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
588
589 let min_total: f32 = 10.0;
590
591 println!(
592 "[vtuber-joints-example] auto joints for mesh_key='{}': verts={} joint_count={} min_total={}",
593 mesh_key,
594 cpu.vertices.len(),
595 joint_count,
596 min_total
597 );
598
599 let mut out: Vec<(usize, engine::ecs::ComponentId)> = Vec::with_capacity(count);
600 let mut seen: HashSet<engine::ecs::ComponentId> = HashSet::new();
601
602 for (joint_index, total) in ranked.into_iter() {
603 if out.len() >= count {
604 break;
605 }
606 if total < min_total {
607 continue;
608 }
609
610 let node_index = *skin.joint_node_indices.get(joint_index)?;
611 let Some(&joint_tx) = node_index_to_transform.get(&node_index) else {
612 continue;
613 };
614 if !seen.insert(joint_tx) {
615 continue;
616 }
617
618 let name = universe
619 .world
620 .get_component_record(joint_tx)
621 .map(|n| n.name.clone())
622 .unwrap_or_else(|| "<unknown>".to_string());
623
624 println!(
625 " auto[{:<2}] joint_index={:<3} total_weight={:<10.3} node_index={:<3} name={} tx={:?}",
626 out.len(),
627 joint_index,
628 total,
629 node_index,
630 name,
631 joint_tx
632 );
633
634 out.push((node_index, joint_tx));
635 }
636
637 if out.is_empty() { None } else { Some(out) }
638}
639
640fn find_renderable_by_mesh_key(
641 world: &engine::ecs::World,
642 root: engine::ecs::ComponentId,
643 mesh_key: &str,
644) -> Option<engine::ecs::ComponentId> {
645 let mut stack = vec![root];
646 while let Some(node) = stack.pop() {
647 if let Some(m) = world.get_component_by_id_as::<MeshComponent>(node) {
648 if m.key == mesh_key {
649 return find_parent_renderable(world, node);
650 }
651 }
652 for &ch in world.children_of(node) {
653 stack.push(ch);
654 }
655 }
656 None
657}
658
659fn find_parent_renderable(
660 world: &engine::ecs::World,
661 mut cid: engine::ecs::ComponentId,
662) -> Option<engine::ecs::ComponentId> {
663 while let Some(parent) = world.parent_of(cid) {
664 if world
665 .get_component_by_id_as::<RenderableComponent>(parent)
666 .is_some()
667 {
668 return Some(parent);
669 }
670 cid = parent;
671 }
672 None
673}
674
675fn find_skin_id_for_renderable(
676 world: &engine::ecs::World,
677 renderable: engine::ecs::ComponentId,
678) -> Option<engine::graphics::SkinId> {
679 let mut stack = vec![renderable];
680 while let Some(node) = stack.pop() {
681 if let Some(sm) = world.get_component_by_id_as::<SkinnedMeshComponent>(node) {
682 if let Some(id) = sm.skin_id {
683 return Some(id);
684 }
685 }
686 for &ch in world.children_of(node) {
687 stack.push(ch);
688 }
689 }
690 None
691}
692
693fn collect_joint_transforms(
694 world: &engine::ecs::World,
695 skinned_mesh: &engine::ecs::system::SkinnedMeshSystem,
696 visuals: &engine::graphics::VisualWorld,
697 gltf_component: engine::ecs::ComponentId,
698 root: engine::ecs::ComponentId,
699) -> Vec<(usize, engine::ecs::ComponentId)> {
700 fn find_first_skin_id_in_subtree(
701 world: &engine::ecs::World,
702 root: engine::ecs::ComponentId,
703 ) -> Option<engine::graphics::SkinId> {
704 let mut stack = vec![root];
705 while let Some(node) = stack.pop() {
706 if let Some(sm) = world.get_component_by_id_as::<SkinnedMeshComponent>(node) {
707 if let Some(id) = sm.skin_id {
708 return Some(id);
709 }
710 }
711 for &ch in world.children_of(node) {
712 stack.push(ch);
713 }
714 }
715 None
716 }
717
718 let Some(skin_id) = find_first_skin_id_in_subtree(world, root) else {
719 return Vec::new();
720 };
721 let Some(skin) = visuals.skin(skin_id) else {
722 return Vec::new();
723 };
724 let Some(joints) = skinned_mesh.instance_joints_for_skin(gltf_component, skin_id) else {
725 return Vec::new();
726 };
727
728 let joint_count = skin.joint_count().min(joints.len());
729 let mut out: Vec<(usize, engine::ecs::ComponentId)> = Vec::with_capacity(joint_count);
730 for i in 0..joint_count {
731 let Some(joint_tx) = joints[i] else {
732 continue;
733 };
734 let node_index = skin.joint_node_indices[i];
735 out.push((node_index, joint_tx));
736 }
737
738 out
739}
740
741fn select_joint_range(
742 joints: &[(usize, engine::ecs::ComponentId)],
743 offset: usize,
744 count: usize,
745) -> Vec<(usize, engine::ecs::ComponentId)> {
746 if joints.is_empty() || count == 0 {
747 return Vec::new();
748 }
749 let len = joints.len();
750 let start = offset % len;
751 let n = count.min(len);
752
753 (0..n).map(|i| joints[(start + i) % len]).collect()
754}
755
756fn quat_from_axis_angle(axis: [f32; 3], angle_rad: f32) -> [f32; 4] {
757 let [ax, ay, az] = axis;
758 let len2 = ax * ax + ay * ay + az * az;
759 if len2 <= 0.0 {
760 return [0.0, 0.0, 0.0, 1.0];
761 }
762 let inv_len = len2.sqrt().recip();
763 let (ax, ay, az) = (ax * inv_len, ay * inv_len, az * inv_len);
764
765 let half = 0.5 * angle_rad;
766 let (s, c) = half.sin_cos();
767 [ax * s, ay * s, az * s, c]
768}
769
770fn quat_conjugate(q: [f32; 4]) -> [f32; 4] {
771 [-q[0], -q[1], -q[2], q[3]]
772}
773
774fn quat_normalize(q: [f32; 4]) -> [f32; 4] {
775 let len2 = q[0] * q[0] + q[1] * q[1] + q[2] * q[2] + q[3] * q[3];
776 if len2 <= 0.0 {
777 return [0.0, 0.0, 0.0, 1.0];
778 }
779 let inv = len2.sqrt().recip();
780 [q[0] * inv, q[1] * inv, q[2] * inv, q[3] * inv]
781}
782
783fn quat_rotate_vec3(q: [f32; 4], v: [f32; 3]) -> [f32; 3] {
784 let qv = [v[0], v[1], v[2], 0.0];
786 let t = quat_mul(q, qv);
787 let out = quat_mul(t, quat_conjugate(q));
788 [out[0], out[1], out[2]]
789}
790
791fn quat_mul(a: [f32; 4], b: [f32; 4]) -> [f32; 4] {
792 let (ax, ay, az, aw) = (a[0], a[1], a[2], a[3]);
793 let (bx, by, bz, bw) = (b[0], b[1], b[2], b[3]);
794 [
795 aw * bx + ax * bw + ay * bz - az * by,
796 aw * by - ax * bz + ay * bw + az * bx,
797 aw * bz + ax * by - ay * bx + az * bw,
798 aw * bw - ax * bx - ay * by - az * bz,
799 ]
800}