1use crate::engine::ecs::component::{
2 QuatExtractYawComponent, QuatTemporalFilterComponent, QuatYawFollowComponent,
3 TransformCameraSpecificComponent, TransformCameraSpecificMode, TransformComponent,
4 TransformDropComponent, TransformForkTRSComponent, TransformMapRotationComponent,
5 TransformMapScaleComponent, TransformMapTranslationComponent, TransformMergeTRSComponent,
6 TransformParentComponent, TransformSampleAncestorComponent, Vector3TemporalFilterComponent,
7};
8use crate::engine::ecs::system::System;
9use crate::engine::ecs::{ComponentId, World};
10use crate::engine::graphics::VisualWorld;
11use crate::engine::graphics::primitives::TransformMatrix;
12use crate::engine::user_input::InputState;
13use crate::utils::math;
14use std::collections::{HashMap, VecDeque};
15use std::sync::OnceLock;
16
17#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18pub enum TransformPipelineInput {
19 ParentWorld,
20}
21
22#[derive(Debug, Clone, Copy, PartialEq)]
23pub enum TransformPipelineVec3Op {
24 Pass,
25 Drop,
26 TemporalSmooth {
27 smoothing_factor: f32,
28 },
29 SampleAncestorTranslation {
34 skip: usize,
35 },
36}
37
38#[derive(Debug, Clone, Copy, PartialEq)]
39pub enum TransformPipelineQuatOp {
40 Pass,
41 Drop,
42 TemporalFilter {
43 smoothing_factor: f32,
44 },
45 SampleAncestorRotation {
48 skip: usize,
49 },
50 ExtractYaw,
53 YawFollow {
58 threshold: f32,
59 rate: f32,
60 initial_yaw: f32,
61 forward_plus_z: bool,
62 },
63}
64
65#[derive(Debug, Clone, PartialEq)]
66pub struct TransformForkTrsStage {
67 pub translation_ops: Vec<TransformPipelineVec3Op>,
68 pub rotation_ops: Vec<TransformPipelineQuatOp>,
69 pub scale_ops: Vec<TransformPipelineVec3Op>,
70}
71
72#[derive(Debug, Clone, PartialEq)]
73pub enum TransformPipelineStage {
74 ForkTrs(TransformForkTrsStage),
75}
76
77#[derive(Debug, Clone, PartialEq)]
78pub struct TransformPipelinePlan {
79 pub owner_component: Option<ComponentId>,
80 pub input: TransformPipelineInput,
81 pub stages: Vec<TransformPipelineStage>,
82}
83
84#[derive(Debug, Clone, Copy, PartialEq)]
85pub struct TransformPipelineChannels {
86 pub translation: [f32; 3],
87 pub rotation_quat_xyzw: [f32; 4],
88 pub scale: [f32; 3],
89}
90
91#[derive(Debug, Clone, PartialEq, Eq, Hash)]
92struct TransformPipelineStageKey {
93 owner_component: Option<ComponentId>,
94 stage_path: Vec<usize>,
95}
96
97#[derive(Debug, Clone, PartialEq)]
98struct QuatTemporalState {
99 output_quat_xyzw: [f32; 4],
100 last_input_quat_xyzw: [f32; 4],
101 debug_window: QuatTemporalDebugWindow,
102}
103
104#[derive(Debug, Clone, PartialEq, Default)]
105struct QuatTemporalDebugWindow {
106 raw_step_deg: VecDeque<f32>,
107 filtered_step_deg: VecDeque<f32>,
108 lag_deg: VecDeque<f32>,
109 sample_count: u64,
110}
111
112#[derive(Debug, Clone, Copy, PartialEq)]
113struct Vec3TemporalState {
114 output_vec3: [f32; 3],
115}
116
117#[derive(Debug, Default)]
118pub struct TransformStreamSystem {
119 last_dt_sec: Option<f32>,
120 vec3_temporal_state: HashMap<TransformPipelineStageKey, Vec3TemporalState>,
121 quat_temporal_state: HashMap<TransformPipelineStageKey, QuatTemporalState>,
122 yaw_follow_state: HashMap<TransformPipelineStageKey, f32>,
123 stereoscopic_active: bool,
124 camera_specific_basis: HashMap<ComponentId, (TransformMatrix, TransformMatrix)>,
128}
129
130impl TransformStreamSystem {
131 pub fn new() -> Self {
132 Self::default()
133 }
134
135 pub fn is_transform_stream_boundary(&self, world: &World, cid: ComponentId) -> bool {
136 world
137 .get_component_by_id_as::<TransformForkTRSComponent>(cid)
138 .is_some()
139 || world
140 .get_component_by_id_as::<TransformParentComponent>(cid)
141 .is_some()
142 || Self::camera_specific_settings(world, cid).is_some()
143 }
144
145 pub fn set_stereoscopic_active(&mut self, active: bool) {
146 self.stereoscopic_active = active;
147 }
148
149 fn camera_specific_settings(
150 world: &World,
151 anchor: ComponentId,
152 ) -> Option<(Option<ComponentId>, Option<ComponentId>)> {
153 let mut mono = None;
154 let mut stereo = None;
155 for &child in world.children_of(anchor) {
156 let Some(c) = world.get_component_by_id_as::<TransformCameraSpecificComponent>(child)
157 else {
158 continue;
159 };
160 let settings = world.children_of(child).iter().copied().find(|&id| {
161 world
162 .get_component_by_id_as::<TransformComponent>(id)
163 .is_some()
164 });
165 match c.mode {
166 TransformCameraSpecificMode::Monoscopic => mono = settings,
167 TransformCameraSpecificMode::Stereoscopic => stereo = settings,
168 }
169 }
170 (mono.is_some() || stereo.is_some()).then_some((mono, stereo))
171 }
172
173 pub fn parse_component_tree(
174 &self,
175 world: &World,
176 root: ComponentId,
177 ) -> Option<TransformPipelinePlan> {
178 if world
179 .get_component_by_id_as::<TransformForkTRSComponent>(root)
180 .is_some()
181 {
182 return Some(TransformPipelinePlan {
183 owner_component: Some(root),
184 input: TransformPipelineInput::ParentWorld,
185 stages: vec![TransformPipelineStage::ForkTrs(
186 self.parse_fork_trs(world, root),
187 )],
188 });
189 }
190 None
191 }
192
193 pub fn evaluate_stream_node(
194 &mut self,
195 world: &World,
196 root: ComponentId,
197 input_world: TransformMatrix,
198 ) -> Option<(TransformMatrix, Vec<ComponentId>)> {
199 let rebased_world = Self::apply_transform_parent_basis(world, root, input_world);
200
201 if let Some((mono, stereo)) = Self::camera_specific_settings(world, root) {
202 let basis = match self.camera_specific_basis.get(&root).copied() {
203 Some((basis, previous_effective)) if input_world == previous_effective => basis,
204 _ => rebased_world,
205 };
206 let selected = if self.stereoscopic_active {
207 stereo
208 } else {
209 mono
210 };
211 let effective = selected
212 .and_then(|id| world.get_component_by_id_as::<TransformComponent>(id))
213 .map(|t| math::mat4_mul(basis, t.transform.model))
214 .unwrap_or(basis);
215 self.camera_specific_basis.insert(root, (basis, effective));
216 let outputs = world
217 .children_of(root)
218 .iter()
219 .copied()
220 .filter(|&id| {
221 world
222 .get_component_by_id_as::<TransformCameraSpecificComponent>(id)
223 .is_none()
224 })
225 .collect();
226 return Some((effective, outputs));
227 }
228
229 if let Some(block) = self.parse_component_tree(world, root) {
230 let outputs = self.downstream_children(world, root, &block);
231 let world_matrix = self.evaluate_block(&block, rebased_world, world, self.last_dt_sec);
232 return Some((world_matrix, outputs));
233 }
234
235 if world
236 .get_component_by_id_as::<TransformParentComponent>(root)
237 .is_some()
238 {
239 return Some((rebased_world, world.children_of(root).to_vec()));
240 }
241
242 None
243 }
244
245 fn apply_transform_parent_basis(
246 world: &World,
247 node: ComponentId,
248 current_world: TransformMatrix,
249 ) -> TransformMatrix {
250 world
251 .get_component_by_id_as::<TransformParentComponent>(node)
252 .and_then(|tp| tp.resolve_target_component(world))
253 .and_then(|target| Self::world_model(world, target))
254 .unwrap_or(current_world)
255 }
256
257 fn world_model(world: &World, cid: ComponentId) -> Option<TransformMatrix> {
258 if let Some(t) = world.get_component_by_id_as::<TransformComponent>(cid) {
259 return Some(t.transform.matrix_world);
260 }
261
262 let mut cur = cid;
263 while let Some(parent) = world.parent_of(cur) {
264 if let Some(t) = world.get_component_by_id_as::<TransformComponent>(parent) {
265 return Some(t.transform.matrix_world);
266 }
267 cur = parent;
268 }
269 None
270 }
271
272 fn downstream_children(
273 &self,
274 world: &World,
275 root: ComponentId,
276 _block: &TransformPipelinePlan,
277 ) -> Vec<ComponentId> {
278 world
279 .children_of(root)
280 .iter()
281 .copied()
282 .filter(|&child| {
283 world
284 .get_component_by_id_as::<TransformMapTranslationComponent>(child)
285 .is_none()
286 && world
287 .get_component_by_id_as::<TransformMapRotationComponent>(child)
288 .is_none()
289 && world
290 .get_component_by_id_as::<TransformMapScaleComponent>(child)
291 .is_none()
292 && world
293 .get_component_by_id_as::<TransformMergeTRSComponent>(child)
294 .is_none()
295 })
296 .collect()
297 }
298
299 pub fn pipeline_for_controller_rotation_smoothing(
300 owner_component: Option<ComponentId>,
301 smoothing_factor: f32,
302 ) -> TransformPipelinePlan {
303 TransformPipelinePlan {
304 owner_component,
305 input: TransformPipelineInput::ParentWorld,
306 stages: vec![TransformPipelineStage::ForkTrs(TransformForkTrsStage {
307 translation_ops: vec![TransformPipelineVec3Op::Pass],
308 rotation_ops: vec![TransformPipelineQuatOp::TemporalFilter { smoothing_factor }],
309 scale_ops: vec![TransformPipelineVec3Op::Pass],
310 })],
311 }
312 }
313
314 fn parse_fork_trs(&self, world: &World, node: ComponentId) -> TransformForkTrsStage {
315 let mut translation_ops = vec![TransformPipelineVec3Op::Pass];
316 let mut rotation_ops = vec![TransformPipelineQuatOp::Pass];
317 let mut scale_ops = vec![TransformPipelineVec3Op::Pass];
318
319 for &child in world.children_of(node) {
320 if world
321 .get_component_by_id_as::<TransformMapTranslationComponent>(child)
322 .is_some()
323 {
324 translation_ops = self.parse_vec3_ops(world, child);
325 continue;
326 }
327
328 if world
329 .get_component_by_id_as::<TransformMapRotationComponent>(child)
330 .is_some()
331 {
332 rotation_ops = self.parse_quat_ops(world, child);
333 continue;
334 }
335
336 if world
337 .get_component_by_id_as::<TransformMapScaleComponent>(child)
338 .is_some()
339 {
340 scale_ops = self.parse_vec3_ops(world, child);
341 continue;
342 }
343
344 if world
345 .get_component_by_id_as::<TransformMergeTRSComponent>(child)
346 .is_some()
347 {
348 debug_assert!(
349 world.children_of(child).is_empty(),
350 "TransformMergeTRS should not contain child stages; attach driven children directly under the TransformForkTRS root"
351 );
352 continue;
353 }
354 }
355
356 TransformForkTrsStage {
357 translation_ops,
358 rotation_ops,
359 scale_ops,
360 }
361 }
362
363 fn parse_vec3_ops(&self, world: &World, node: ComponentId) -> Vec<TransformPipelineVec3Op> {
364 let mut ops = Vec::new();
365 for &child in world.children_of(node) {
366 if world
367 .get_component_by_id_as::<TransformDropComponent>(child)
368 .is_some()
369 {
370 ops.push(TransformPipelineVec3Op::Drop);
371 continue;
372 }
373 if let Some(s) = world.get_component_by_id_as::<TransformSampleAncestorComponent>(child)
374 {
375 ops.push(TransformPipelineVec3Op::SampleAncestorTranslation { skip: s.skip });
376 continue;
377 }
378 if let Some(filter) =
379 world.get_component_by_id_as::<Vector3TemporalFilterComponent>(child)
380 {
381 ops.push(TransformPipelineVec3Op::TemporalSmooth {
382 smoothing_factor: filter.smoothing_factor,
383 });
384 }
385 }
386 if ops.is_empty() {
387 ops.push(TransformPipelineVec3Op::Pass);
388 }
389 ops
390 }
391
392 fn parse_quat_ops(&self, world: &World, node: ComponentId) -> Vec<TransformPipelineQuatOp> {
393 let mut ops = Vec::new();
394 for &child in world.children_of(node) {
395 if world
396 .get_component_by_id_as::<TransformDropComponent>(child)
397 .is_some()
398 {
399 ops.push(TransformPipelineQuatOp::Drop);
400 continue;
401 }
402 if let Some(s) = world.get_component_by_id_as::<TransformSampleAncestorComponent>(child)
403 {
404 ops.push(TransformPipelineQuatOp::SampleAncestorRotation { skip: s.skip });
405 continue;
406 }
407 if let Some(filter) = world.get_component_by_id_as::<QuatTemporalFilterComponent>(child)
408 {
409 ops.push(TransformPipelineQuatOp::TemporalFilter {
410 smoothing_factor: filter.smoothing_factor,
411 });
412 continue;
413 }
414 if world
415 .get_component_by_id_as::<QuatExtractYawComponent>(child)
416 .is_some()
417 {
418 ops.push(TransformPipelineQuatOp::ExtractYaw);
419 continue;
420 }
421 if let Some(c) = world.get_component_by_id_as::<QuatYawFollowComponent>(child) {
422 ops.push(TransformPipelineQuatOp::YawFollow {
423 threshold: c.threshold,
424 rate: c.rate,
425 initial_yaw: c.initial_yaw,
426 forward_plus_z: c.forward_plus_z,
427 });
428 }
429 }
430 if ops.is_empty() {
431 ops.push(TransformPipelineQuatOp::Pass);
432 }
433 ops
434 }
435
436 pub fn evaluate_block(
437 &mut self,
438 pipeline: &TransformPipelinePlan,
439 input_world: TransformMatrix,
440 world: &World,
441 dt_sec: Option<f32>,
442 ) -> TransformMatrix {
443 let mut channels = Self::decompose_matrix(input_world);
444 for (stage_index, stage) in pipeline.stages.iter().enumerate() {
445 let mut stage_path = vec![stage_index];
446 channels = self.evaluate_stage(
447 pipeline.owner_component,
448 stage,
449 channels,
450 &mut stage_path,
451 world,
452 dt_sec,
453 );
454 }
455 Self::recompose_matrix(channels)
456 }
457
458 fn evaluate_stage(
459 &mut self,
460 owner_component: Option<ComponentId>,
461 stage: &TransformPipelineStage,
462 input: TransformPipelineChannels,
463 stage_path: &mut Vec<usize>,
464 world: &World,
465 dt_sec: Option<f32>,
466 ) -> TransformPipelineChannels {
467 match stage {
468 TransformPipelineStage::ForkTrs(fork) => {
469 self.evaluate_fork_trs(owner_component, fork, input, stage_path, world, dt_sec)
470 }
471 }
472 }
473
474 fn evaluate_fork_trs(
475 &mut self,
476 owner_component: Option<ComponentId>,
477 fork: &TransformForkTrsStage,
478 input: TransformPipelineChannels,
479 stage_path: &[usize],
480 world: &World,
481 dt_sec: Option<f32>,
482 ) -> TransformPipelineChannels {
483 let translation = self.apply_vec3_ops(
484 owner_component,
485 &fork.translation_ops,
486 input.translation,
487 [0.0, 0.0, 0.0],
488 stage_path,
489 world,
490 dt_sec,
491 );
492 let rotation_quat_xyzw = self.apply_quat_ops(
493 owner_component,
494 &fork.rotation_ops,
495 input.rotation_quat_xyzw,
496 [0.0, 0.0, 0.0, 1.0],
497 stage_path,
498 world,
499 dt_sec,
500 );
501 let scale = self.apply_vec3_ops(
502 owner_component,
503 &fork.scale_ops,
504 input.scale,
505 [1.0, 1.0, 1.0],
506 stage_path,
507 world,
508 dt_sec,
509 );
510
511 TransformPipelineChannels {
512 translation,
513 rotation_quat_xyzw,
514 scale,
515 }
516 }
517
518 fn sample_ancestor_world(
521 world: &World,
522 owner: ComponentId,
523 skip: usize,
524 ) -> Option<TransformMatrix> {
525 let mut found = 0usize;
526 let mut cur = owner;
527 while let Some(parent) = world.parent_of(cur) {
528 if let Some(t) = world.get_component_by_id_as::<TransformComponent>(parent) {
529 if found == skip {
530 return Some(t.transform.matrix_world);
531 }
532 found += 1;
533 }
534 cur = parent;
535 }
536 None
537 }
538
539 fn apply_vec3_ops(
540 &mut self,
541 owner_component: Option<ComponentId>,
542 ops: &[TransformPipelineVec3Op],
543 input: [f32; 3],
544 dropped_value: [f32; 3],
545 stage_path: &[usize],
546 world: &World,
547 dt_sec: Option<f32>,
548 ) -> [f32; 3] {
549 let mut current = input;
550 for (op_index, op) in ops.iter().enumerate() {
551 current = match *op {
552 TransformPipelineVec3Op::Pass => current,
553 TransformPipelineVec3Op::Drop => dropped_value,
554 TransformPipelineVec3Op::SampleAncestorTranslation { skip } => owner_component
555 .and_then(|owner| Self::sample_ancestor_world(world, owner, skip))
556 .map(|m| [m[3][0], m[3][1], m[3][2]])
557 .unwrap_or(current),
558 TransformPipelineVec3Op::TemporalSmooth { smoothing_factor } => {
559 let mut full_path = stage_path.to_vec();
560 full_path.push(op_index);
561 let key = TransformPipelineStageKey {
562 owner_component,
563 stage_path: full_path,
564 };
565 let alpha = Self::alpha_from_smoothing_factor(smoothing_factor, dt_sec);
566 let previous = self
567 .vec3_temporal_state
568 .get(&key)
569 .map(|state| state.output_vec3)
570 .unwrap_or(current);
571 let filtered = Self::vec3_lerp(previous, current, alpha);
572 self.vec3_temporal_state.insert(
573 key,
574 Vec3TemporalState {
575 output_vec3: filtered,
576 },
577 );
578 filtered
579 }
580 };
581 }
582 current
583 }
584
585 fn apply_quat_ops(
586 &mut self,
587 owner_component: Option<ComponentId>,
588 ops: &[TransformPipelineQuatOp],
589 input: [f32; 4],
590 dropped_value: [f32; 4],
591 stage_path: &[usize],
592 world: &World,
593 dt_sec: Option<f32>,
594 ) -> [f32; 4] {
595 let mut current = Self::quat_normalize(input);
596 for (op_index, op) in ops.iter().enumerate() {
597 current = match *op {
598 TransformPipelineQuatOp::Pass => current,
599 TransformPipelineQuatOp::Drop => dropped_value,
600 TransformPipelineQuatOp::SampleAncestorRotation { skip } => owner_component
601 .and_then(|owner| Self::sample_ancestor_world(world, owner, skip))
602 .map(|m| Self::decompose_matrix(m).rotation_quat_xyzw)
603 .unwrap_or(current),
604 TransformPipelineQuatOp::ExtractYaw => {
605 let (qy, qw) = (current[1], current[3]);
607 let len = (qy * qy + qw * qw).sqrt().max(1e-8);
608 [0.0, qy / len, 0.0, qw / len]
609 }
610 TransformPipelineQuatOp::YawFollow {
611 threshold,
612 rate,
613 initial_yaw,
614 forward_plus_z,
615 } => {
616 let mut full_path = stage_path.to_vec();
617 full_path.push(op_index);
618 let key = TransformPipelineStageKey {
619 owner_component,
620 stage_path: full_path,
621 };
622 let head_yaw = Self::extract_yaw_from_quat(current, forward_plus_z);
623 let body_yaw = self
624 .yaw_follow_state
625 .get(&key)
626 .copied()
627 .unwrap_or(initial_yaw);
628 let new_body_yaw = if let Some(dt) = dt_sec {
629 let delta = Self::signed_yaw_diff(head_yaw, body_yaw);
630 if delta.abs() > threshold {
631 let target = head_yaw - delta.signum() * threshold;
632 let step = rate * dt;
633 Self::lerp_angle(
634 body_yaw,
635 target,
636 step.min(delta.abs()) / delta.abs().max(1e-9),
637 )
638 } else {
639 body_yaw
640 }
641 } else {
642 body_yaw
643 };
644 self.yaw_follow_state.insert(key, new_body_yaw);
645 Self::quat_rotation_y(new_body_yaw)
646 }
647 TransformPipelineQuatOp::TemporalFilter { smoothing_factor } => {
648 let mut full_path = stage_path.to_vec();
649 full_path.push(op_index);
650 let key = TransformPipelineStageKey {
651 owner_component,
652 stage_path: full_path.clone(),
653 };
654 let alpha = Self::alpha_from_smoothing_factor(smoothing_factor, dt_sec);
655 let previous_state = self.quat_temporal_state.get(&key).cloned();
656 let previous_output = previous_state
657 .as_ref()
658 .map(|state| state.output_quat_xyzw)
659 .unwrap_or(current);
660 let previous_input = previous_state
661 .as_ref()
662 .map(|state| state.last_input_quat_xyzw)
663 .unwrap_or(current);
664 let filtered = previous_state
665 .as_ref()
666 .map(|state| state.output_quat_xyzw)
667 .unwrap_or(current);
668
669 let mut next_state = previous_state.unwrap_or(QuatTemporalState {
670 output_quat_xyzw: filtered,
671 last_input_quat_xyzw: current,
672 debug_window: QuatTemporalDebugWindow::default(),
673 });
674 next_state.output_quat_xyzw = filtered;
675 next_state.last_input_quat_xyzw = current;
676
677 if Self::debug_quat_filter_enabled() {
678 let window_len = Self::debug_quat_filter_window_len();
679 let raw_step_deg = Self::quat_angle_degrees(previous_input, current);
680 let filtered_step_deg = Self::quat_angle_degrees(previous_output, filtered);
681 let lag_deg = Self::quat_angle_degrees(filtered, current);
682
683 Self::push_rolling_sample(
684 &mut next_state.debug_window.raw_step_deg,
685 raw_step_deg,
686 window_len,
687 );
688 Self::push_rolling_sample(
689 &mut next_state.debug_window.filtered_step_deg,
690 filtered_step_deg,
691 window_len,
692 );
693 Self::push_rolling_sample(
694 &mut next_state.debug_window.lag_deg,
695 lag_deg,
696 window_len,
697 );
698 next_state.debug_window.sample_count += 1;
699
700 if next_state.debug_window.sample_count % window_len as u64 == 0 {
701 let avg_raw = Self::rolling_avg(&next_state.debug_window.raw_step_deg);
702 let avg_filtered =
703 Self::rolling_avg(&next_state.debug_window.filtered_step_deg);
704 let avg_lag = Self::rolling_avg(&next_state.debug_window.lag_deg);
705 let max_raw = Self::rolling_max(&next_state.debug_window.raw_step_deg);
706 let max_filtered =
707 Self::rolling_max(&next_state.debug_window.filtered_step_deg);
708 let attenuation_pct = if avg_raw > 1e-4 {
709 (1.0 - (avg_filtered / avg_raw)).clamp(-10.0, 1.0) * 100.0
710 } else {
711 0.0
712 };
713
714 eprintln!(
715 "[TransformPipeline][QuatFilter] owner={owner_component:?} stage_path={full_path:?} smoothing_factor={smoothing_factor:.3} dt={:.5} alpha={alpha:.5} raw_avg_deg={avg_raw:.3} filtered_avg_deg={avg_filtered:.3} lag_avg_deg={avg_lag:.3} raw_max_deg={max_raw:.3} filtered_max_deg={max_filtered:.3} attenuation_pct={attenuation_pct:.1} window={} samples={}",
716 dt_sec.unwrap_or(0.0),
717 next_state.debug_window.raw_step_deg.len(),
718 next_state.debug_window.sample_count,
719 );
720 }
721 }
722
723 self.quat_temporal_state.insert(key, next_state);
724 filtered
725 }
726 };
727 }
728 Self::quat_normalize(current)
729 }
730
731 fn extract_yaw_from_quat(q: [f32; 4], forward_plus_z: bool) -> f32 {
732 let z = math::quat_rotate_vec3(q, [0.0, 0.0, 1.0]);
733 if forward_plus_z {
734 z[0].atan2(z[2])
735 } else {
736 (-z[0]).atan2(-z[2])
737 }
738 }
739
740 fn quat_rotation_y(yaw: f32) -> [f32; 4] {
741 let h = yaw * 0.5;
742 [0.0, h.sin(), 0.0, h.cos()]
743 }
744
745 fn signed_yaw_diff(a: f32, b: f32) -> f32 {
746 let pi = std::f32::consts::PI;
747 let mut d = (a - b) % (2.0 * pi);
748 if d > pi {
749 d -= 2.0 * pi;
750 }
751 if d < -pi {
752 d += 2.0 * pi;
753 }
754 d
755 }
756
757 fn lerp_angle(from: f32, to: f32, t: f32) -> f32 {
758 from + Self::signed_yaw_diff(to, from) * t.clamp(0.0, 1.0)
759 }
760
761 fn decompose_matrix(m: TransformMatrix) -> TransformPipelineChannels {
762 fn col3(m: TransformMatrix, c: usize) -> [f32; 3] {
763 [m[c][0], m[c][1], m[c][2]]
764 }
765
766 let translation = [m[3][0], m[3][1], m[3][2]];
767 let b0 = col3(m, 0);
768 let b1 = col3(m, 1);
769 let b2 = col3(m, 2);
770
771 let scale = [
772 math::vec3_len(b0).max(1e-8),
773 math::vec3_len(b1).max(1e-8),
774 math::vec3_len(b2).max(1e-8),
775 ];
776
777 let [x, y, z] = Self::orthonormalize_basis(b0, b1, b2);
778 let rotation_quat_xyzw = Self::quat_from_basis_columns(x, y, z);
779
780 TransformPipelineChannels {
781 translation,
782 rotation_quat_xyzw,
783 scale,
784 }
785 }
786
787 fn recompose_matrix(channels: TransformPipelineChannels) -> TransformMatrix {
788 let x = math::quat_rotate_vec3(channels.rotation_quat_xyzw, [1.0, 0.0, 0.0]);
789 let y = math::quat_rotate_vec3(channels.rotation_quat_xyzw, [0.0, 1.0, 0.0]);
790 let z = math::quat_rotate_vec3(channels.rotation_quat_xyzw, [0.0, 0.0, 1.0]);
791
792 let mut out = math::mat4_identity();
793 out[0][0] = x[0] * channels.scale[0];
794 out[0][1] = x[1] * channels.scale[0];
795 out[0][2] = x[2] * channels.scale[0];
796
797 out[1][0] = y[0] * channels.scale[1];
798 out[1][1] = y[1] * channels.scale[1];
799 out[1][2] = y[2] * channels.scale[1];
800
801 out[2][0] = z[0] * channels.scale[2];
802 out[2][1] = z[1] * channels.scale[2];
803 out[2][2] = z[2] * channels.scale[2];
804
805 out[3][0] = channels.translation[0];
806 out[3][1] = channels.translation[1];
807 out[3][2] = channels.translation[2];
808 out
809 }
810
811 fn orthonormalize_basis(b0: [f32; 3], b1: [f32; 3], b2: [f32; 3]) -> [[f32; 3]; 3] {
812 let x = math::vec3_normalize(b0);
813 let y_proj = math::vec3_sub(b1, math::vec3_scale(x, math::vec3_dot(b1, x)));
814 let mut y = math::vec3_normalize(y_proj);
815 let mut z = math::vec3_cross(x, y);
816 if math::vec3_len(z) < 1e-6 {
817 z = math::vec3_normalize(b2);
818 y = math::vec3_normalize(math::vec3_cross(z, x));
819 } else {
820 z = math::vec3_normalize(z);
821 y = math::vec3_normalize(math::vec3_cross(z, x));
822 }
823 [x, y, z]
824 }
825
826 fn quat_from_basis_columns(x: [f32; 3], y: [f32; 3], z: [f32; 3]) -> [f32; 4] {
827 let r00 = x[0];
828 let r01 = y[0];
829 let r02 = z[0];
830 let r10 = x[1];
831 let r11 = y[1];
832 let r12 = z[1];
833 let r20 = x[2];
834 let r21 = y[2];
835 let r22 = z[2];
836
837 let trace = r00 + r11 + r22;
838 let (qx, qy, qz, qw) = if trace > 0.0 {
839 let s = (trace + 1.0).sqrt() * 2.0;
840 ((r21 - r12) / s, (r02 - r20) / s, (r10 - r01) / s, 0.25 * s)
841 } else if r00 > r11 && r00 > r22 {
842 let s = (1.0 + r00 - r11 - r22).sqrt() * 2.0;
843 (0.25 * s, (r01 + r10) / s, (r02 + r20) / s, (r21 - r12) / s)
844 } else if r11 > r22 {
845 let s = (1.0 + r11 - r00 - r22).sqrt() * 2.0;
846 ((r01 + r10) / s, 0.25 * s, (r12 + r21) / s, (r02 - r20) / s)
847 } else {
848 let s = (1.0 + r22 - r00 - r11).sqrt() * 2.0;
849 ((r02 + r20) / s, (r12 + r21) / s, 0.25 * s, (r10 - r01) / s)
850 };
851
852 Self::quat_normalize([qx, qy, qz, qw])
853 }
854
855 fn quat_normalize(q: [f32; 4]) -> [f32; 4] {
856 let len = (q[0] * q[0] + q[1] * q[1] + q[2] * q[2] + q[3] * q[3]).sqrt();
857 if len > 0.0 {
858 [q[0] / len, q[1] / len, q[2] / len, q[3] / len]
859 } else {
860 [0.0, 0.0, 0.0, 1.0]
861 }
862 }
863
864 fn vec3_lerp(a: [f32; 3], b: [f32; 3], t: f32) -> [f32; 3] {
865 let one_minus_t = 1.0 - t;
866 [
867 a[0] * one_minus_t + b[0] * t,
868 a[1] * one_minus_t + b[1] * t,
869 a[2] * one_minus_t + b[2] * t,
870 ]
871 }
872
873 fn alpha_from_smoothing_factor(smoothing_factor: f32, dt_sec: Option<f32>) -> f32 {
874 match dt_sec {
875 Some(dt) if dt > 0.0 => 1.0 - (-smoothing_factor.max(0.0) * dt).exp(),
876 _ => smoothing_factor.clamp(0.0, 1.0),
877 }
878 }
879
880 fn debug_quat_filter_enabled() -> bool {
881 static ENABLED: OnceLock<bool> = OnceLock::new();
882 *ENABLED.get_or_init(|| {
883 std::env::var("CAT_DEBUG_QUAT_FILTER")
884 .ok()
885 .map(|value| {
886 let value = value.trim().to_ascii_lowercase();
887 matches!(value.as_str(), "1" | "true" | "yes" | "on")
888 })
889 .unwrap_or(false)
890 })
891 }
892
893 fn debug_quat_filter_window_len() -> usize {
894 static WINDOW_LEN: OnceLock<usize> = OnceLock::new();
895 *WINDOW_LEN.get_or_init(|| {
896 std::env::var("CAT_DEBUG_QUAT_FILTER_WINDOW")
897 .ok()
898 .and_then(|value| value.parse::<usize>().ok())
899 .map(|value| value.clamp(1, 600))
900 .unwrap_or(60)
901 })
902 }
903
904 fn push_rolling_sample(window: &mut VecDeque<f32>, value: f32, max_len: usize) {
905 if window.len() >= max_len {
906 let _ = window.pop_front();
907 }
908 window.push_back(value);
909 }
910
911 fn rolling_avg(window: &VecDeque<f32>) -> f32 {
912 if window.is_empty() {
913 0.0
914 } else {
915 window.iter().copied().sum::<f32>() / window.len() as f32
916 }
917 }
918
919 fn rolling_max(window: &VecDeque<f32>) -> f32 {
920 window.iter().copied().fold(0.0, f32::max)
921 }
922
923 fn quat_angle_degrees(a: [f32; 4], b: [f32; 4]) -> f32 {
924 let a = Self::quat_normalize(a);
925 let b = Self::quat_normalize(b);
926 let dot = (a[0] * b[0] + a[1] * b[1] + a[2] * b[2] + a[3] * b[3])
927 .abs()
928 .clamp(0.0, 1.0);
929 (2.0 * dot.acos()).to_degrees()
930 }
931}
932
933impl From<TransformMatrix> for TransformPipelineChannels {
934 fn from(value: TransformMatrix) -> Self {
935 TransformStreamSystem::decompose_matrix(value)
936 }
937}
938
939impl System for TransformStreamSystem {
940 fn tick(
941 &mut self,
942 _world: &mut World,
943 _visuals: &mut VisualWorld,
944 _input: &InputState,
945 dt_sec: f32,
946 ) {
947 self.last_dt_sec = Some(dt_sec);
948 }
949}
950
951#[cfg(test)]
952mod tests {
953 use super::*;
954 use crate::engine::ecs::World;
955 use crate::engine::ecs::component::{
956 QuatTemporalFilterComponent, TransformCameraSpecificComponent, TransformComponent,
957 TransformForkTRSComponent, TransformMapRotationComponent, TransformMapScaleComponent,
958 TransformMapTranslationComponent, Vector3TemporalFilterComponent,
959 };
960
961 #[test]
962 fn camera_specific_selects_modes_and_excludes_configuration_children() {
963 let mut world = World::default();
964 let anchor = world.add_component(TransformComponent::new());
965 let mono = world.add_component(TransformCameraSpecificComponent::active_monoscopic());
966 let mono_settings =
967 world.add_component(TransformComponent::new().with_position(0.0, 2.0, 0.0));
968 let stereo = world.add_component(TransformCameraSpecificComponent::active_stereoscopic());
969 let stereo_settings =
970 world.add_component(TransformComponent::new().with_position(0.0, 0.0, 3.0));
971 let output = world.add_component(TransformComponent::new());
972 world.add_child(anchor, mono).unwrap();
973 world.add_child(mono, mono_settings).unwrap();
974 world.add_child(anchor, stereo).unwrap();
975 world.add_child(stereo, stereo_settings).unwrap();
976 world.add_child(anchor, output).unwrap();
977 let input = TransformComponent::new()
978 .with_position(1.0, 0.0, 0.0)
979 .transform
980 .model;
981 let mut system = TransformStreamSystem::new();
982 let (m, outputs) = system.evaluate_stream_node(&world, anchor, input).unwrap();
983 assert_eq!([m[3][0], m[3][1], m[3][2]], [1.0, 2.0, 0.0]);
984 assert_eq!(outputs, vec![output]);
985 system.set_stereoscopic_active(true);
986 let (m, outputs) = system.evaluate_stream_node(&world, anchor, input).unwrap();
987 assert_eq!([m[3][0], m[3][1], m[3][2]], [1.0, 0.0, 3.0]);
988 assert_eq!(outputs, vec![output]);
989 }
990
991 #[test]
992 fn camera_specific_uses_generic_anchor_when_mode_has_no_settings() {
993 let mut world = World::default();
994 let anchor = world.add_component(TransformComponent::new());
995 let mono = world.add_component(TransformCameraSpecificComponent::active_monoscopic());
996 let settings = world.add_component(TransformComponent::new().with_scale(2.0, 2.0, 2.0));
997 world.add_child(anchor, mono).unwrap();
998 world.add_child(mono, settings).unwrap();
999 let input = TransformComponent::new()
1000 .with_position(4.0, 5.0, 6.0)
1001 .transform
1002 .model;
1003 let mut system = TransformStreamSystem::new();
1004 system.set_stereoscopic_active(true);
1005 assert_eq!(
1006 system
1007 .evaluate_stream_node(&world, anchor, input)
1008 .unwrap()
1009 .0,
1010 input
1011 );
1012 }
1013
1014 #[test]
1015 fn repeated_camera_specific_refresh_does_not_compound_scale() {
1016 let mut world = World::default();
1017 let anchor = world.add_component(TransformComponent::new());
1018 let mono = world.add_component(TransformCameraSpecificComponent::active_monoscopic());
1019 let settings = world.add_component(TransformComponent::new().with_scale(2.0, 2.0, 2.0));
1020 world.add_child(anchor, mono).unwrap();
1021 world.add_child(mono, settings).unwrap();
1022
1023 let mut system = TransformStreamSystem::new();
1024 let identity = TransformComponent::new().transform.model;
1025 let first = system
1026 .evaluate_stream_node(&world, anchor, identity)
1027 .unwrap()
1028 .0;
1029 let second = system
1030 .evaluate_stream_node(&world, anchor, first)
1031 .unwrap()
1032 .0;
1033 let third = system
1034 .evaluate_stream_node(&world, anchor, second)
1035 .unwrap()
1036 .0;
1037
1038 assert_eq!(first, second);
1039 assert_eq!(second, third);
1040 assert_eq!(third[0][0], 2.0);
1041 assert_eq!(third[1][1], 2.0);
1042 assert_eq!(third[2][2], 2.0);
1043 }
1044
1045 #[test]
1046 fn controller_rotation_pipeline_contains_temporal_quat_op() {
1047 let block = TransformStreamSystem::pipeline_for_controller_rotation_smoothing(None, 1.0);
1048 let stage = match &block.stages[0] {
1049 TransformPipelineStage::ForkTrs(stage) => stage,
1050 };
1051 assert_eq!(
1052 stage.rotation_ops,
1053 vec![TransformPipelineQuatOp::TemporalFilter {
1054 smoothing_factor: 1.0
1055 }]
1056 );
1057 }
1058
1059 #[test]
1060 fn parses_fork_root_component_tree() {
1061 let mut world = World::default();
1062 let fork = world.add_component(TransformForkTRSComponent::new());
1063 let map_translation = world.add_component(TransformMapTranslationComponent::new());
1064 let vec_filter =
1065 world.add_component(Vector3TemporalFilterComponent::new().with_smoothing_factor(12.0));
1066 let map_rotation = world.add_component(TransformMapRotationComponent::new());
1067 let quat_filter =
1068 world.add_component(QuatTemporalFilterComponent::new().with_smoothing_factor(18.0));
1069
1070 world.set_parent(map_translation, Some(fork)).unwrap();
1071 world.set_parent(vec_filter, Some(map_translation)).unwrap();
1072 world.set_parent(map_rotation, Some(fork)).unwrap();
1073 world.set_parent(quat_filter, Some(map_rotation)).unwrap();
1074
1075 let parser = TransformStreamSystem::new();
1076 let block = parser
1077 .parse_component_tree(&world, fork)
1078 .expect("pipeline block");
1079 assert_eq!(block.owner_component, Some(fork));
1080 assert_eq!(block.stages.len(), 1);
1081 let stage = match &block.stages[0] {
1082 TransformPipelineStage::ForkTrs(stage) => stage,
1083 };
1084 assert_eq!(
1085 stage.translation_ops,
1086 vec![TransformPipelineVec3Op::TemporalSmooth {
1087 smoothing_factor: 12.0
1088 }]
1089 );
1090 assert_eq!(
1091 stage.rotation_ops,
1092 vec![TransformPipelineQuatOp::TemporalFilter {
1093 smoothing_factor: 18.0
1094 }]
1095 );
1096 assert_eq!(stage.scale_ops, vec![TransformPipelineVec3Op::Pass]);
1097 }
1098
1099 #[test]
1100 fn fork_trs_defaults_missing_streams_to_pass() {
1101 let mut world = World::default();
1102 let fork = world.add_component(TransformForkTRSComponent::new());
1103 let map_scale = world.add_component(TransformMapScaleComponent::new());
1104
1105 world.set_parent(map_scale, Some(fork)).unwrap();
1106
1107 let parser = TransformStreamSystem::new();
1108 let block = parser
1109 .parse_component_tree(&world, fork)
1110 .expect("pipeline block");
1111 let stage = match &block.stages[0] {
1112 TransformPipelineStage::ForkTrs(stage) => stage,
1113 };
1114
1115 assert_eq!(stage.translation_ops, vec![TransformPipelineVec3Op::Pass]);
1116 assert_eq!(stage.rotation_ops, vec![TransformPipelineQuatOp::Pass]);
1117 assert_eq!(stage.scale_ops, vec![TransformPipelineVec3Op::Pass]);
1118 }
1119
1120 #[test]
1121 fn parses_fork_trs_as_root_pipeline() {
1122 let mut world = World::default();
1123 let fork = world.add_component(TransformForkTRSComponent::new());
1124 let map_rotation = world.add_component(TransformMapRotationComponent::new());
1125 let quat_filter =
1126 world.add_component(QuatTemporalFilterComponent::new().with_smoothing_factor(9.0));
1127
1128 world.set_parent(map_rotation, Some(fork)).unwrap();
1129 world.set_parent(quat_filter, Some(map_rotation)).unwrap();
1130
1131 let parser = TransformStreamSystem::new();
1132 let block = parser
1133 .parse_component_tree(&world, fork)
1134 .expect("fork root block");
1135 assert_eq!(block.owner_component, Some(fork));
1136 assert_eq!(block.stages.len(), 1);
1137 let stage = match &block.stages[0] {
1138 TransformPipelineStage::ForkTrs(stage) => stage,
1139 };
1140 assert_eq!(
1141 stage.rotation_ops,
1142 vec![TransformPipelineQuatOp::TemporalFilter {
1143 smoothing_factor: 9.0
1144 }]
1145 );
1146 }
1147
1148 #[test]
1149 fn fork_root_returns_non_map_children_as_downstream_children() {
1150 let mut world = World::default();
1151 let fork = world.add_component(TransformForkTRSComponent::new());
1152 let map_rotation = world.add_component(TransformMapRotationComponent::new());
1153 let quat_filter =
1154 world.add_component(QuatTemporalFilterComponent::new().with_smoothing_factor(9.0));
1155 let downstream = world.add_component(TransformComponent::new());
1156
1157 world.set_parent(map_rotation, Some(fork)).unwrap();
1158 world.set_parent(quat_filter, Some(map_rotation)).unwrap();
1159 world.set_parent(downstream, Some(fork)).unwrap();
1160
1161 let mut system = TransformStreamSystem::new();
1162 let ident = [
1163 [1.0, 0.0, 0.0, 0.0],
1164 [0.0, 1.0, 0.0, 0.0],
1165 [0.0, 0.0, 1.0, 0.0],
1166 [0.0, 0.0, 0.0, 1.0],
1167 ];
1168 let (_, children) = system
1169 .evaluate_stream_node(&world, fork, ident)
1170 .expect("fork root eval");
1171 assert_eq!(children, vec![downstream]);
1172 }
1173}