1#[allow(dead_code, unused_variables, unused_mut, unused_imports)]
2
3use glam::{Vec2, Vec3, Vec4, Quat, Mat4};
4use std::collections::{HashMap, VecDeque, HashSet, BTreeMap};
5
6const MAX_BONES: usize = 512;
11const MAX_KEYFRAMES: usize = 65536;
12const MAX_TRACKS: usize = 1024;
13const LEVENSHTEIN_MAX_LEN: usize = 256;
14const GIMBAL_LOCK_THRESHOLD: f32 = 80.0; const FOV_MIN: f32 = 5.0;
16const FOV_MAX: f32 = 170.0;
17const DOF_MIN_FOCUS: f32 = 0.1;
18const DOF_MAX_FOCUS: f32 = 10000.0;
19const AUDIO_CLIP_THRESHOLD: f32 = 0.99;
20const AUDIO_CLIP_SAMPLE_WINDOW: usize = 1024;
21const BPM_MIN: f32 = 20.0;
22const BPM_MAX: f32 = 400.0;
23const SRT_MAGIC: &str = "-->";
24const WEBVTT_MAGIC: &str = "WEBVTT";
25const CUSTOM_FORMAT_VERSION: u32 = 3;
26const BEZIER_FIT_ITERATIONS: usize = 100;
27const BEZIER_FIT_TOLERANCE: f32 = 0.001;
28const PROGRESS_STEPS: u32 = 100;
29const IMPORT_REPORT_MAX_ERRORS: usize = 1024;
30const BATCH_MAX_FILES: usize = 256;
31
32#[derive(Clone, Debug, PartialEq)]
37pub enum Token {
38 Ident(String),
39 StringLit(String),
40 IntLit(i64),
41 FloatLit(f64),
42 LBrace,
43 RBrace,
44 LBracket,
45 RBracket,
46 LParen,
47 RParen,
48 Colon,
49 Comma,
50 Semicolon,
51 Equals,
52 Dot,
53 At,
54 Hash,
55 Newline,
56 Eof,
57}
58
59#[derive(Clone, Debug)]
60pub struct TokenStream {
61 pub tokens: Vec<Token>,
62 pub pos: usize,
63 pub source: String,
64}
65
66impl TokenStream {
67 pub fn from_source(src: &str) -> Self {
68 let tokens = Tokenizer::tokenize(src);
69 TokenStream {
70 tokens,
71 pos: 0,
72 source: src.to_string(),
73 }
74 }
75
76 pub fn peek(&self) -> &Token {
77 if self.pos < self.tokens.len() {
78 &self.tokens[self.pos]
79 } else {
80 &Token::Eof
81 }
82 }
83
84 pub fn next(&mut self) -> Token {
85 if self.pos < self.tokens.len() {
86 let t = self.tokens[self.pos].clone();
87 self.pos += 1;
88 t
89 } else {
90 Token::Eof
91 }
92 }
93
94 pub fn expect_ident(&mut self) -> Option<String> {
95 if let Token::Ident(s) = self.peek().clone() {
96 self.pos += 1;
97 Some(s)
98 } else {
99 None
100 }
101 }
102
103 pub fn expect_float(&mut self) -> Option<f64> {
104 match self.peek().clone() {
105 Token::FloatLit(f) => { self.pos += 1; Some(f) }
106 Token::IntLit(i) => { self.pos += 1; Some(i as f64) }
107 _ => None,
108 }
109 }
110
111 pub fn expect_int(&mut self) -> Option<i64> {
112 if let Token::IntLit(i) = self.peek().clone() {
113 self.pos += 1;
114 Some(i)
115 } else {
116 None
117 }
118 }
119
120 pub fn skip_whitespace_and_newlines(&mut self) {
121 while matches!(self.peek(), Token::Newline) {
122 self.pos += 1;
123 }
124 }
125
126 pub fn is_eof(&self) -> bool {
127 matches!(self.peek(), Token::Eof)
128 }
129}
130
131pub struct Tokenizer;
132
133impl Tokenizer {
134 pub fn tokenize(src: &str) -> Vec<Token> {
135 let mut tokens = Vec::new();
136 let chars: Vec<char> = src.chars().collect();
137 let mut i = 0;
138 while i < chars.len() {
139 match chars[i] {
140 ' ' | '\t' | '\r' => { i += 1; }
141 '\n' => { tokens.push(Token::Newline); i += 1; }
142 '{' => { tokens.push(Token::LBrace); i += 1; }
143 '}' => { tokens.push(Token::RBrace); i += 1; }
144 '[' => { tokens.push(Token::LBracket); i += 1; }
145 ']' => { tokens.push(Token::RBracket); i += 1; }
146 '(' => { tokens.push(Token::LParen); i += 1; }
147 ')' => { tokens.push(Token::RParen); i += 1; }
148 ':' => { tokens.push(Token::Colon); i += 1; }
149 ',' => { tokens.push(Token::Comma); i += 1; }
150 ';' => { tokens.push(Token::Semicolon); i += 1; }
151 '=' => { tokens.push(Token::Equals); i += 1; }
152 '.' => { tokens.push(Token::Dot); i += 1; }
153 '@' => { tokens.push(Token::At); i += 1; }
154 '#' => {
155 while i < chars.len() && chars[i] != '\n' { i += 1; }
157 }
158 '"' => {
159 i += 1;
160 let mut s = String::new();
161 while i < chars.len() && chars[i] != '"' {
162 if chars[i] == '\\' && i + 1 < chars.len() {
163 i += 1;
164 match chars[i] {
165 'n' => s.push('\n'),
166 't' => s.push('\t'),
167 'r' => s.push('\r'),
168 '"' => s.push('"'),
169 '\\' => s.push('\\'),
170 _ => { s.push('\\'); s.push(chars[i]); }
171 }
172 } else {
173 s.push(chars[i]);
174 }
175 i += 1;
176 }
177 if i < chars.len() { i += 1; } tokens.push(Token::StringLit(s));
179 }
180 c if c == '-' || c.is_ascii_digit() => {
181 let start = i;
182 let mut is_float = false;
183 if chars[i] == '-' { i += 1; }
184 while i < chars.len() && chars[i].is_ascii_digit() { i += 1; }
185 if i < chars.len() && chars[i] == '.' {
186 is_float = true;
187 i += 1;
188 while i < chars.len() && chars[i].is_ascii_digit() { i += 1; }
189 }
190 if i < chars.len() && (chars[i] == 'e' || chars[i] == 'E') {
191 is_float = true;
192 i += 1;
193 if i < chars.len() && (chars[i] == '+' || chars[i] == '-') { i += 1; }
194 while i < chars.len() && chars[i].is_ascii_digit() { i += 1; }
195 }
196 let s: String = chars[start..i].iter().collect();
197 if is_float {
198 if let Ok(f) = s.parse::<f64>() {
199 tokens.push(Token::FloatLit(f));
200 }
201 } else {
202 if let Ok(n) = s.parse::<i64>() {
203 tokens.push(Token::IntLit(n));
204 }
205 }
206 }
207 c if c.is_alphabetic() || c == '_' => {
208 let start = i;
209 while i < chars.len() && (chars[i].is_alphanumeric() || chars[i] == '_') { i += 1; }
210 let s: String = chars[start..i].iter().collect();
211 tokens.push(Token::Ident(s));
212 }
213 _ => { i += 1; }
214 }
215 }
216 tokens.push(Token::Eof);
217 tokens
218 }
219}
220
221#[derive(Clone, Debug)]
226pub enum AstValue {
227 Str(String),
228 Int(i64),
229 Float(f64),
230 Vec3Val([f64; 3]),
231 Vec4Val([f64; 4]),
232 List(Vec<AstValue>),
233 Block(AstBlock),
234 Bool(bool),
235}
236
237#[derive(Clone, Debug)]
238pub struct AstField {
239 pub name: String,
240 pub value: AstValue,
241}
242
243#[derive(Clone, Debug)]
244pub struct AstBlock {
245 pub kind: String,
246 pub name: Option<String>,
247 pub fields: Vec<AstField>,
248 pub children: Vec<AstBlock>,
249}
250
251pub struct AstParser;
252
253impl AstParser {
254 pub fn parse(ts: &mut TokenStream) -> AstBlock {
255 let mut root = AstBlock {
256 kind: "root".to_string(),
257 name: None,
258 fields: Vec::new(),
259 children: Vec::new(),
260 };
261 ts.skip_whitespace_and_newlines();
262 while !ts.is_eof() {
263 ts.skip_whitespace_and_newlines();
264 if ts.is_eof() { break; }
265 if let Some(block) = Self::parse_block(ts) {
266 root.children.push(block);
267 } else {
268 ts.next(); }
270 }
271 root
272 }
273
274 fn parse_block(ts: &mut TokenStream) -> Option<AstBlock> {
275 ts.skip_whitespace_and_newlines();
276 let kind = ts.expect_ident()?;
277 ts.skip_whitespace_and_newlines();
278 let name = if let Token::StringLit(s) | Token::Ident(s) = ts.peek().clone() {
279 if matches!(ts.peek(), Token::StringLit(_) | Token::Ident(_)) {
280 let tok = ts.next();
281 match tok {
282 Token::StringLit(s) | Token::Ident(s) => Some(s),
283 _ => None,
284 }
285 } else { None }
286 } else { None };
287 ts.skip_whitespace_and_newlines();
288 if !matches!(ts.peek(), Token::LBrace) { return None; }
289 ts.next(); let mut block = AstBlock { kind, name, fields: Vec::new(), children: Vec::new() };
291 loop {
292 ts.skip_whitespace_and_newlines();
293 if ts.is_eof() || matches!(ts.peek(), Token::RBrace) { break; }
294 let saved_pos = ts.pos;
296 if let Some(field_name) = ts.expect_ident() {
297 ts.skip_whitespace_and_newlines();
298 if matches!(ts.peek(), Token::Equals) {
299 ts.next();
300 ts.skip_whitespace_and_newlines();
301 if let Some(val) = Self::parse_value(ts) {
302 block.fields.push(AstField { name: field_name, value: val });
303 continue;
304 }
305 } else if matches!(ts.peek(), Token::LBrace) {
306 ts.pos = saved_pos;
308 if let Some(child) = Self::parse_block(ts) {
309 block.children.push(child);
310 continue;
311 }
312 }
313 ts.pos = saved_pos;
314 }
315 ts.next(); }
317 if matches!(ts.peek(), Token::RBrace) { ts.next(); }
318 Some(block)
319 }
320
321 fn parse_value(ts: &mut TokenStream) -> Option<AstValue> {
322 ts.skip_whitespace_and_newlines();
323 match ts.peek().clone() {
324 Token::StringLit(s) => { ts.next(); Some(AstValue::Str(s)) }
325 Token::IntLit(n) => { ts.next(); Some(AstValue::Int(n)) }
326 Token::FloatLit(f) => { ts.next(); Some(AstValue::Float(f)) }
327 Token::Ident(s) => {
328 let s_lower = s.to_lowercase();
329 ts.next();
330 if s_lower == "true" { Some(AstValue::Bool(true)) }
331 else if s_lower == "false" { Some(AstValue::Bool(false)) }
332 else { Some(AstValue::Str(s)) }
333 }
334 Token::LParen => {
335 ts.next();
336 let mut vals = Vec::new();
337 while !matches!(ts.peek(), Token::RParen | Token::Eof) {
338 if let Some(v) = ts.expect_float() { vals.push(v); }
339 if matches!(ts.peek(), Token::Comma) { ts.next(); }
340 }
341 if matches!(ts.peek(), Token::RParen) { ts.next(); }
342 match vals.len() {
343 3 => Some(AstValue::Vec3Val([vals[0], vals[1], vals[2]])),
344 4 => Some(AstValue::Vec4Val([vals[0], vals[1], vals[2], vals[3]])),
345 _ => Some(AstValue::List(vals.iter().map(|&f| AstValue::Float(f)).collect())),
346 }
347 }
348 Token::LBracket => {
349 ts.next();
350 let mut items = Vec::new();
351 while !matches!(ts.peek(), Token::RBracket | Token::Eof) {
352 if let Some(v) = Self::parse_value(ts) { items.push(v); }
353 if matches!(ts.peek(), Token::Comma) { ts.next(); }
354 }
355 if matches!(ts.peek(), Token::RBracket) { ts.next(); }
356 Some(AstValue::List(items))
357 }
358 _ => None,
359 }
360 }
361}
362
363#[derive(Clone, Copy, Debug, PartialEq)]
368pub enum InterpolationType {
369 Constant,
370 Linear,
371 Cubic,
372 BezierCubic,
373}
374
375#[derive(Clone, Debug)]
376pub struct Keyframe<T: Clone> {
377 pub time: f32,
378 pub value: T,
379 pub tangent_in: T,
380 pub tangent_out: T,
381 pub interp: InterpolationType,
382}
383
384impl<T: Clone + Default> Keyframe<T> {
385 pub fn new(time: f32, value: T) -> Self {
386 Keyframe {
387 time,
388 value: value.clone(),
389 tangent_in: T::default(),
390 tangent_out: T::default(),
391 interp: InterpolationType::Linear,
392 }
393 }
394}
395
396#[derive(Clone, Debug)]
397pub struct TransformTrack {
398 pub bone_name: String,
399 pub position_keys: Vec<Keyframe<Vec3>>,
400 pub rotation_keys: Vec<Keyframe<Quat>>,
401 pub scale_keys: Vec<Keyframe<Vec3>>,
402}
403
404impl TransformTrack {
405 pub fn new(bone_name: &str) -> Self {
406 TransformTrack {
407 bone_name: bone_name.to_string(),
408 position_keys: Vec::new(),
409 rotation_keys: Vec::new(),
410 scale_keys: Vec::new(),
411 }
412 }
413
414 pub fn sample_position(&self, t: f32) -> Vec3 {
415 sample_vec3_track(&self.position_keys, t).unwrap_or(Vec3::ZERO)
416 }
417
418 pub fn sample_rotation(&self, t: f32) -> Quat {
419 sample_quat_track(&self.rotation_keys, t).unwrap_or(Quat::IDENTITY)
420 }
421
422 pub fn sample_scale(&self, t: f32) -> Vec3 {
423 sample_vec3_track(&self.scale_keys, t).unwrap_or(Vec3::ONE)
424 }
425}
426
427pub fn sample_vec3_track(keys: &[Keyframe<Vec3>], t: f32) -> Option<Vec3> {
428 if keys.is_empty() { return None; }
429 if t <= keys[0].time { return Some(keys[0].value); }
430 if t >= keys[keys.len()-1].time { return Some(keys[keys.len()-1].value); }
431 for i in 0..keys.len()-1 {
432 if t >= keys[i].time && t <= keys[i+1].time {
433 let dt = keys[i+1].time - keys[i].time;
434 let u = if dt > 0.0 { (t - keys[i].time) / dt } else { 0.0 };
435 return Some(match keys[i].interp {
436 InterpolationType::Constant => keys[i].value,
437 InterpolationType::Linear => keys[i].value.lerp(keys[i+1].value, u),
438 InterpolationType::Cubic | InterpolationType::BezierCubic => {
439 let p0 = keys[i].value;
441 let p1 = keys[i+1].value;
442 let m0 = keys[i].tangent_out * dt;
443 let m1 = keys[i+1].tangent_in * dt;
444 hermite_vec3(p0, m0, p1, m1, u)
445 }
446 });
447 }
448 }
449 None
450}
451
452pub fn sample_quat_track(keys: &[Keyframe<Quat>], t: f32) -> Option<Quat> {
453 if keys.is_empty() { return None; }
454 if t <= keys[0].time { return Some(keys[0].value); }
455 if t >= keys[keys.len()-1].time { return Some(keys[keys.len()-1].value); }
456 for i in 0..keys.len()-1 {
457 if t >= keys[i].time && t <= keys[i+1].time {
458 let dt = keys[i+1].time - keys[i].time;
459 let u = if dt > 0.0 { (t - keys[i].time) / dt } else { 0.0 };
460 return Some(keys[i].value.slerp(keys[i+1].value, u));
461 }
462 }
463 None
464}
465
466pub fn hermite_vec3(p0: Vec3, m0: Vec3, p1: Vec3, m1: Vec3, t: f32) -> Vec3 {
467 let t2 = t * t;
468 let t3 = t2 * t;
469 let h00 = 2.0*t3 - 3.0*t2 + 1.0;
470 let h10 = t3 - 2.0*t2 + t;
471 let h01 = -2.0*t3 + 3.0*t2;
472 let h11 = t3 - t2;
473 p0*h00 + m0*h10 + p1*h01 + m1*h11
474}
475
476#[derive(Clone, Debug)]
481pub struct CameraKeyframe {
482 pub time: f32,
483 pub position: Vec3,
484 pub rotation: Quat,
485 pub fov_degrees: f32,
486 pub near_clip: f32,
487 pub far_clip: f32,
488 pub dof_focus_distance: f32,
489 pub dof_aperture: f32,
490 pub dof_enabled: bool,
491}
492
493impl CameraKeyframe {
494 pub fn default_at(time: f32) -> Self {
495 CameraKeyframe {
496 time,
497 position: Vec3::ZERO,
498 rotation: Quat::IDENTITY,
499 fov_degrees: 60.0,
500 near_clip: 0.1,
501 far_clip: 10000.0,
502 dof_focus_distance: 10.0,
503 dof_aperture: 2.8,
504 dof_enabled: false,
505 }
506 }
507}
508
509#[derive(Clone, Debug)]
510pub struct CameraTrack {
511 pub name: String,
512 pub keyframes: Vec<CameraKeyframe>,
513 pub bezier_positions: Vec<Vec3>, pub fov_curve: Vec<Keyframe<f32>>,
515 pub dof_focus_curve: Vec<Keyframe<f32>>,
516}
517
518impl CameraTrack {
519 pub fn new(name: &str) -> Self {
520 CameraTrack {
521 name: name.to_string(),
522 keyframes: Vec::new(),
523 bezier_positions: Vec::new(),
524 fov_curve: Vec::new(),
525 dof_focus_curve: Vec::new(),
526 }
527 }
528
529 pub fn sample(&self, t: f32) -> CameraKeyframe {
530 if self.keyframes.is_empty() { return CameraKeyframe::default_at(t); }
531 if t <= self.keyframes[0].time { return self.keyframes[0].clone(); }
532 if t >= self.keyframes[self.keyframes.len()-1].time {
533 return self.keyframes[self.keyframes.len()-1].clone();
534 }
535 for i in 0..self.keyframes.len()-1 {
536 let k0 = &self.keyframes[i];
537 let k1 = &self.keyframes[i+1];
538 if t >= k0.time && t <= k1.time {
539 let dt = k1.time - k0.time;
540 let u = if dt > 0.0 { (t - k0.time) / dt } else { 0.0 };
541 let pos = k0.position.lerp(k1.position, u);
542 let rot = k0.rotation.slerp(k1.rotation, u);
543 let fov = k0.fov_degrees + (k1.fov_degrees - k0.fov_degrees) * u;
544 let dof_dist = k0.dof_focus_distance + (k1.dof_focus_distance - k0.dof_focus_distance) * u;
545 let dof_ap = k0.dof_aperture + (k1.dof_aperture - k0.dof_aperture) * u;
546 return CameraKeyframe {
547 time: t,
548 position: pos,
549 rotation: rot,
550 fov_degrees: fov,
551 near_clip: k0.near_clip,
552 far_clip: k0.far_clip,
553 dof_focus_distance: dof_dist,
554 dof_aperture: dof_ap,
555 dof_enabled: k0.dof_enabled,
556 };
557 }
558 }
559 self.keyframes[0].clone()
560 }
561
562 pub fn fit_bezier_path(&mut self) {
563 let positions: Vec<Vec3> = self.keyframes.iter().map(|k| k.position).collect();
564 if positions.len() < 2 { return; }
565 self.bezier_positions = BezierFitter::fit_to_points(&positions, BEZIER_FIT_TOLERANCE);
566 }
567}
568
569pub struct BezierFitter;
574
575impl BezierFitter {
576 pub fn fit_to_points(points: &[Vec3], tolerance: f32) -> Vec<Vec3> {
577 if points.len() < 2 { return points.to_vec(); }
578 let n = points.len();
579 let t_start_dir = (points[1] - points[0]).normalize_or_zero();
580 let t_end_dir = (points[n-1] - points[n-2]).normalize_or_zero();
581 Self::fit_recursive(points, t_start_dir, t_end_dir, tolerance)
582 }
583
584 fn fit_recursive(points: &[Vec3], t_start: Vec3, t_end: Vec3, tolerance: f32) -> Vec<Vec3> {
585 let n = points.len();
586 if n < 2 { return points.to_vec(); }
587 let (c1, c2) = Self::generate_bezier(points, t_start, t_end);
588 let p0 = points[0];
589 let p3 = points[n-1];
590 let chord_lengths: Vec<f32> = {
592 let mut cl = vec![0.0f32; n];
593 for i in 1..n { cl[i] = cl[i-1] + points[i-1].distance(points[i]); }
594 cl
595 };
596 let total = chord_lengths[n-1];
597 let mut max_err = 0.0f32;
598 let mut split_idx = 0;
599 for i in 1..n-1 {
600 let t = if total > 0.0 { chord_lengths[i] / total } else { i as f32 / (n-1) as f32 };
601 let b = Self::eval_cubic(p0, c1, c2, p3, t);
602 let err = b.distance(points[i]);
603 if err > max_err { max_err = err; split_idx = i; }
604 }
605 if max_err <= tolerance || n <= 3 {
606 vec![p0, c1, c2, p3]
607 } else {
608 let t_split_dir = if split_idx < n-1 && split_idx > 0 {
610 (points[split_idx+1] - points[split_idx-1]).normalize_or_zero()
611 } else { Vec3::Z };
612 let mut left = Self::fit_recursive(&points[..=split_idx], t_start, t_split_dir, tolerance);
613 let right = Self::fit_recursive(&points[split_idx..], t_split_dir, t_end, tolerance);
614 left.pop();
615 left.extend(right);
616 left
617 }
618 }
619
620 fn generate_bezier(points: &[Vec3], t1: Vec3, t2: Vec3) -> (Vec3, Vec3) {
621 let n = points.len();
622 let p0 = points[0];
623 let p3 = points[n-1];
624 let chord = p0.distance(p3);
625 let alpha = chord / 3.0;
626 let c1 = p0 + t1 * alpha;
627 let c2 = p3 - t2 * alpha;
628 (c1, c2)
629 }
630
631 pub fn eval_cubic(p0: Vec3, p1: Vec3, p2: Vec3, p3: Vec3, t: f32) -> Vec3 {
632 let t2 = t * t;
633 let t3 = t2 * t;
634 let one_m_t = 1.0 - t;
635 let one_m_t2 = one_m_t * one_m_t;
636 let one_m_t3 = one_m_t2 * one_m_t;
637 p0 * one_m_t3
638 + p1 * (3.0 * one_m_t2 * t)
639 + p2 * (3.0 * one_m_t * t2)
640 + p3 * t3
641 }
642}
643
644#[derive(Clone, Copy, Debug, PartialEq)]
649pub enum LightType {
650 Point,
651 Spot,
652 Directional,
653 Area,
654}
655
656#[derive(Clone, Debug)]
657pub struct LightKeyframe {
658 pub time: f32,
659 pub position: Vec3,
660 pub rotation: Quat,
661 pub color: Vec3,
662 pub intensity: f32,
663 pub range: f32,
664 pub spot_angle: f32,
665 pub cast_shadows: bool,
666}
667
668#[derive(Clone, Debug)]
669pub struct LightTrack {
670 pub name: String,
671 pub light_type: LightType,
672 pub keyframes: Vec<LightKeyframe>,
673}
674
675impl LightTrack {
676 pub fn new(name: &str, light_type: LightType) -> Self {
677 LightTrack { name: name.to_string(), light_type, keyframes: Vec::new() }
678 }
679
680 pub fn add_keyframe(&mut self, kf: LightKeyframe) {
681 self.keyframes.push(kf);
682 self.keyframes.sort_by(|a, b| a.time.partial_cmp(&b.time).unwrap());
683 }
684
685 pub fn sample(&self, t: f32) -> Option<LightKeyframe> {
686 if self.keyframes.is_empty() { return None; }
687 if t <= self.keyframes[0].time { return Some(self.keyframes[0].clone()); }
688 let last = self.keyframes.last().unwrap();
689 if t >= last.time { return Some(last.clone()); }
690 for i in 0..self.keyframes.len() - 1 {
691 let a = &self.keyframes[i]; let b = &self.keyframes[i+1];
692 if t >= a.time && t <= b.time {
693 let f = (t - a.time) / (b.time - a.time);
694 return Some(LightKeyframe {
695 time: t,
696 position: a.position.lerp(b.position, f),
697 rotation: a.rotation.slerp(b.rotation, f),
698 color: a.color.lerp(b.color, f),
699 intensity: a.intensity + f * (b.intensity - a.intensity),
700 range: a.range + f * (b.range - a.range),
701 spot_angle: a.spot_angle + f * (b.spot_angle - a.spot_angle),
702 cast_shadows: if f < 0.5 { a.cast_shadows } else { b.cast_shadows },
703 });
704 }
705 }
706 None
707 }
708
709 pub fn duration(&self) -> f32 { self.keyframes.last().map(|k| k.time).unwrap_or(0.0) }
710 pub fn keyframe_count(&self) -> usize { self.keyframes.len() }
711}
712
713#[derive(Debug, Clone, PartialEq)]
718pub enum LoopMode { Once, Loop, PingPong, ClampForever }
719
720#[derive(Debug, Clone)]
721pub struct AnimationClipMeta {
722 pub clip_name: String,
723 pub duration_s: f32,
724 pub fps: f32,
725 pub loop_mode: LoopMode,
726 pub bone_count: u32,
727 pub keyframe_count: u32,
728 pub has_root_motion: bool,
729 pub root_motion_delta: Vec3,
730 pub events: Vec<String>,
731}
732
733impl AnimationClipMeta {
734 pub fn new(name: &str, duration: f32, fps: f32) -> Self {
735 AnimationClipMeta { clip_name: name.to_string(), duration_s: duration, fps,
736 loop_mode: LoopMode::Loop, bone_count: 0, keyframe_count: 0,
737 has_root_motion: false, root_motion_delta: Vec3::ZERO, events: Vec::new() }
738 }
739
740 pub fn frame_count(&self) -> u32 { (self.duration_s * self.fps) as u32 }
741 pub fn sample_time(&self, t: f32) -> f32 {
742 match self.loop_mode {
743 LoopMode::Once => t.clamp(0.0, self.duration_s),
744 LoopMode::Loop => t % self.duration_s,
745 LoopMode::PingPong => {
746 let cycle = t % (self.duration_s * 2.0);
747 if cycle < self.duration_s { cycle } else { self.duration_s * 2.0 - cycle }
748 },
749 LoopMode::ClampForever => t.clamp(0.0, self.duration_s),
750 }
751 }
752 pub fn is_finished(&self, t: f32) -> bool { self.loop_mode == LoopMode::Once && t >= self.duration_s }
753}
754
755#[derive(Debug, Clone)]
760pub enum BlendTreeNode {
761 Clip { name: String, weight: f32 },
762 Blend1D { parameter: String, children: Vec<(f32, BlendTreeNode)> },
763 Blend2D { param_x: String, param_y: String, children: Vec<(Vec2, BlendTreeNode)> },
764 Additive { base: Box<BlendTreeNode>, additive: Box<BlendTreeNode>, weight: f32 },
765 Override { base: Box<BlendTreeNode>, override_node: Box<BlendTreeNode>, mask: Vec<String> },
766}
767
768impl BlendTreeNode {
769 pub fn evaluate_1d(&self, param: f32) -> Vec<(String, f32)> {
770 match self {
771 BlendTreeNode::Clip { name, weight } => vec![(name.clone(), *weight)],
772 BlendTreeNode::Blend1D { children, .. } => {
773 if children.is_empty() { return Vec::new(); }
774 if children.len() == 1 { return children[0].1.evaluate_1d(param); }
775 let mut result = Vec::new();
776 for i in 0..children.len()-1 {
777 let (t0, node0) = &children[i];
778 let (t1, node1) = &children[i+1];
779 if param >= *t0 && param <= *t1 {
780 let f = (param - t0) / (t1 - t0);
781 for (name, w) in node0.evaluate_1d(param) { result.push((name, w * (1.0 - f))); }
782 for (name, w) in node1.evaluate_1d(param) { result.push((name, w * f)); }
783 return result;
784 }
785 }
786 children.last().unwrap().1.evaluate_1d(param)
787 },
788 _ => Vec::new(),
789 }
790 }
791
792 pub fn clip_names(&self) -> Vec<String> {
793 match self {
794 BlendTreeNode::Clip { name, .. } => vec![name.clone()],
795 BlendTreeNode::Blend1D { children, .. } => children.iter().flat_map(|(_, n)| n.clip_names()).collect(),
796 BlendTreeNode::Blend2D { children, .. } => children.iter().flat_map(|(_, n)| n.clip_names()).collect(),
797 BlendTreeNode::Additive { base, additive, .. } => { let mut v = base.clip_names(); v.extend(additive.clip_names()); v },
798 BlendTreeNode::Override { base, override_node, .. } => { let mut v = base.clip_names(); v.extend(override_node.clip_names()); v },
799 }
800 }
801}
802
803#[derive(Debug, Clone)]
808pub struct AnimationState {
809 pub name: String,
810 pub clip_name: String,
811 pub speed: f32,
812 pub is_looping: bool,
813}
814
815impl AnimationState {
816 pub fn new(name: &str, clip: &str) -> Self {
817 AnimationState { name: name.to_string(), clip_name: clip.to_string(), speed: 1.0, is_looping: true }
818 }
819}
820
821#[derive(Debug, Clone, PartialEq)]
822pub enum TransitionCondition {
823 BoolTrue(String), BoolFalse(String),
824 FloatGreater(String, f32), FloatLess(String, f32),
825 IntEqual(String, i32), Trigger(String),
826}
827
828#[derive(Debug, Clone)]
829pub struct Transition {
830 pub from: String, pub to: String,
831 pub conditions: Vec<TransitionCondition>,
832 pub duration: f32, pub exit_time: Option<f32>,
833 pub can_interrupt: bool,
834}
835
836#[derive(Debug, Clone)]
837pub enum AnimParam { Bool(bool), Float(f32), Int(i32), Trigger(bool) }
838
839#[derive(Debug, Clone)]
840pub struct AnimatorStateMachine {
841 pub current_state: String,
842 pub states: HashMap<String, AnimationState>,
843 pub transitions: Vec<Transition>,
844 pub params: HashMap<String, AnimParam>,
845}
846
847impl AnimatorStateMachine {
848 pub fn new(initial: &str) -> Self {
849 AnimatorStateMachine { current_state: initial.to_string(), states: HashMap::new(), transitions: Vec::new(), params: HashMap::new() }
850 }
851
852 pub fn add_state(&mut self, state: AnimationState) { self.states.insert(state.name.clone(), state); }
853 pub fn add_transition(&mut self, t: Transition) { self.transitions.push(t); }
854 pub fn set_bool(&mut self, name: &str, v: bool) { self.params.insert(name.to_string(), AnimParam::Bool(v)); }
855 pub fn set_float(&mut self, name: &str, v: f32) { self.params.insert(name.to_string(), AnimParam::Float(v)); }
856 pub fn set_int(&mut self, name: &str, v: i32) { self.params.insert(name.to_string(), AnimParam::Int(v)); }
857 pub fn set_trigger(&mut self, name: &str) { self.params.insert(name.to_string(), AnimParam::Trigger(true)); }
858
859 fn check_condition(&self, cond: &TransitionCondition) -> bool {
860 match cond {
861 TransitionCondition::BoolTrue(n) => matches!(self.params.get(n), Some(AnimParam::Bool(true))),
862 TransitionCondition::BoolFalse(n) => matches!(self.params.get(n), Some(AnimParam::Bool(false))),
863 TransitionCondition::FloatGreater(n, v) => matches!(self.params.get(n), Some(AnimParam::Float(f)) if *f > *v),
864 TransitionCondition::FloatLess(n, v) => matches!(self.params.get(n), Some(AnimParam::Float(f)) if *f < *v),
865 TransitionCondition::IntEqual(n, v) => matches!(self.params.get(n), Some(AnimParam::Int(i)) if *i == *v),
866 TransitionCondition::Trigger(n) => matches!(self.params.get(n), Some(AnimParam::Trigger(true))),
867 }
868 }
869
870 pub fn update(&mut self, _dt: f32) -> Option<&'static str> {
871 let transitions_snapshot = self.transitions.clone();
872 for t in &transitions_snapshot {
873 if t.from != self.current_state { continue; }
874 if t.conditions.iter().all(|c| self.check_condition(c)) {
875 self.current_state = t.to.clone();
876 for cond in &t.conditions {
878 if let TransitionCondition::Trigger(n) = cond {
879 self.params.insert(n.clone(), AnimParam::Trigger(false));
880 }
881 }
882 return match self.current_state.as_str() {
884 "idle" => Some("idle"),
885 "run" => Some("run"),
886 "walk" => Some("walk"),
887 "attack" => Some("attack"),
888 "jump" => Some("jump"),
889 "death" => Some("death"),
890 _ => Some("unknown"),
891 };
892 }
893 }
894 None
895 }
896
897 pub fn state_count(&self) -> usize { self.states.len() }
898 pub fn transition_count(&self) -> usize { self.transitions.len() }
899}
900
901#[derive(Debug, Clone)]
906pub struct MocapFrame {
907 pub time_s: f32,
908 pub bone_transforms: HashMap<String, (Vec3, Quat)>,
909}
910
911impl MocapFrame {
912 pub fn new(time_s: f32) -> Self { MocapFrame { time_s, bone_transforms: HashMap::new() } }
913 pub fn set_bone(&mut self, name: &str, pos: Vec3, rot: Quat) { self.bone_transforms.insert(name.to_string(), (pos, rot)); }
914 pub fn bone_count(&self) -> usize { self.bone_transforms.len() }
915}
916
917#[derive(Debug, Clone)]
918pub struct MocapClip {
919 pub name: String,
920 pub frames: Vec<MocapFrame>,
921 pub fps: f32,
922 pub bone_names: Vec<String>,
923}
924
925impl MocapClip {
926 pub fn new(name: &str, fps: f32) -> Self {
927 MocapClip { name: name.to_string(), frames: Vec::new(), fps, bone_names: Vec::new() }
928 }
929
930 pub fn add_frame(&mut self, f: MocapFrame) { self.frames.push(f); }
931 pub fn duration_s(&self) -> f32 { self.frames.last().map(|f| f.time_s).unwrap_or(0.0) }
932 pub fn frame_count(&self) -> usize { self.frames.len() }
933
934 pub fn frame_at_time(&self, t: f32) -> Option<&MocapFrame> {
935 self.frames.iter().min_by(|a, b| (a.time_s - t).abs().partial_cmp(&(b.time_s - t).abs()).unwrap())
936 }
937
938 pub fn downsample(&self, target_fps: f32) -> MocapClip {
939 let step = (self.fps / target_fps).max(1.0);
940 let mut result = MocapClip::new(&self.name, target_fps);
941 result.bone_names = self.bone_names.clone();
942 let mut i = 0.0f32;
943 while i < self.frames.len() as f32 {
944 result.add_frame(self.frames[i as usize].clone());
945 i += step;
946 }
947 result
948 }
949
950 pub fn retarget_to_skeleton(&self, mapping: &HashMap<String, String>) -> MocapClip {
951 let mut retargeted = MocapClip::new(&self.name, self.fps);
952 retargeted.bone_names = mapping.values().cloned().collect();
953 for frame in &self.frames {
954 let mut new_frame = MocapFrame::new(frame.time_s);
955 for (src, dst) in mapping {
956 if let Some(&(pos, rot)) = frame.bone_transforms.get(src) {
957 new_frame.set_bone(dst, pos, rot);
958 }
959 }
960 retargeted.add_frame(new_frame);
961 }
962 retargeted
963 }
964}
965
966#[derive(Debug, Clone)]
971pub struct IkChain {
972 pub bone_names: Vec<String>,
973 pub bone_lengths: Vec<f32>,
974 pub positions: Vec<Vec3>,
975 pub target: Vec3,
976 pub pole_target: Option<Vec3>,
977}
978
979impl IkChain {
980 pub fn new(bone_names: Vec<String>, lengths: Vec<f32>) -> Self {
981 let n = bone_names.len() + 1;
982 IkChain { bone_names, bone_lengths: lengths, positions: vec![Vec3::ZERO; n], target: Vec3::ZERO, pole_target: None }
983 }
984
985 pub fn total_length(&self) -> f32 { self.bone_lengths.iter().sum() }
986
987 pub fn solve_fabrik(&mut self, iterations: u32, tolerance: f32) {
988 let n = self.positions.len();
989 if n < 2 { return; }
990 let root = self.positions[0];
991 for _ in 0..iterations {
992 self.positions[n - 1] = self.target;
994 for i in (0..n-1).rev() {
995 let dir = (self.positions[i] - self.positions[i+1]).normalize_or_zero();
996 self.positions[i] = self.positions[i+1] + dir * self.bone_lengths[i];
997 }
998 self.positions[0] = root;
1000 for i in 0..n-1 {
1001 let dir = (self.positions[i+1] - self.positions[i]).normalize_or_zero();
1002 self.positions[i+1] = self.positions[i] + dir * self.bone_lengths[i];
1003 }
1004 if (self.positions[n-1] - self.target).length() < tolerance { break; }
1005 }
1006 }
1007
1008 pub fn two_bone_ik(&mut self) {
1009 if self.positions.len() < 3 || self.bone_lengths.len() < 2 { return; }
1010 let a = self.positions[0];
1011 let c = self.target;
1012 let len_ab = self.bone_lengths[0];
1013 let len_bc = self.bone_lengths[1];
1014 let dist = (c - a).length();
1015 let dist_clamped = dist.clamp((len_ab - len_bc).abs(), len_ab + len_bc);
1016 let cos_a = ((len_ab * len_ab + dist_clamped * dist_clamped - len_bc * len_bc) / (2.0 * len_ab * dist_clamped)).clamp(-1.0, 1.0);
1017 let angle_a = cos_a.acos();
1018 let dir_ac = (c - a).normalize_or_zero();
1019 let perp = if let Some(pole) = self.pole_target {
1020 let to_pole = (pole - a).normalize_or_zero();
1021 let proj = dir_ac * dir_ac.dot(to_pole);
1022 (to_pole - proj).normalize_or_zero()
1023 } else {
1024 Vec3::new(0.0, 1.0, 0.0)
1025 };
1026 let rot = Quat::from_axis_angle(perp, angle_a);
1027 self.positions[1] = a + rot * (dir_ac * len_ab);
1028 self.positions[2] = self.target;
1029 }
1030}
1031
1032pub const ARKIT_BLEND_SHAPE_NAMES: [&str; 52] = [
1037 "eyeBlinkLeft", "eyeLookDownLeft", "eyeLookInLeft", "eyeLookOutLeft", "eyeLookUpLeft",
1038 "eyeSquintLeft", "eyeWideLeft", "eyeBlinkRight", "eyeLookDownRight", "eyeLookInRight",
1039 "eyeLookOutRight", "eyeLookUpRight", "eyeSquintRight", "eyeWideRight",
1040 "jawForward", "jawLeft", "jawRight", "jawOpen",
1041 "mouthClose", "mouthFunnel", "mouthPucker", "mouthLeft", "mouthRight",
1042 "mouthSmileLeft", "mouthSmileRight", "mouthFrownLeft", "mouthFrownRight",
1043 "mouthDimpleLeft", "mouthDimpleRight", "mouthStretchLeft", "mouthStretchRight",
1044 "mouthRollLower", "mouthRollUpper", "mouthShrugLower", "mouthShrugUpper",
1045 "mouthPressLeft", "mouthPressRight", "mouthLowerDownLeft", "mouthLowerDownRight",
1046 "mouthUpperUpLeft", "mouthUpperUpRight", "browDownLeft", "browDownRight",
1047 "browInnerUp", "browOuterUpLeft", "browOuterUpRight",
1048 "cheekPuff", "cheekSquintLeft", "cheekSquintRight",
1049 "noseSneerLeft", "noseSneerRight", "tongueOut",
1050];
1051
1052#[derive(Debug, Clone)]
1053pub struct BlendShape {
1054 pub name: String,
1055 pub weight: f32,
1056}
1057
1058impl BlendShape {
1059 pub fn new(name: &str, weight: f32) -> Self { BlendShape { name: name.to_string(), weight: weight.clamp(0.0, 1.0) } }
1060}
1061
1062#[derive(Debug, Clone)]
1063pub struct FacialRig {
1064 pub shapes: Vec<BlendShape>,
1065}
1066
1067impl FacialRig {
1068 pub fn new() -> Self {
1069 FacialRig { shapes: ARKIT_BLEND_SHAPE_NAMES.iter().map(|&n| BlendShape::new(n, 0.0)).collect() }
1070 }
1071
1072 pub fn set(&mut self, name: &str, weight: f32) {
1073 if let Some(s) = self.shapes.iter_mut().find(|s| s.name == name) { s.weight = weight.clamp(0.0, 1.0); }
1074 }
1075
1076 pub fn get(&self, name: &str) -> f32 { self.shapes.iter().find(|s| s.name == name).map(|s| s.weight).unwrap_or(0.0) }
1077 pub fn reset(&mut self) { for s in &mut self.shapes { s.weight = 0.0; } }
1078
1079 pub fn apply_expression(&mut self, expression: &str) {
1080 self.reset();
1081 match expression {
1082 "happy" => { self.set("mouthSmileLeft", 0.8); self.set("mouthSmileRight", 0.8); self.set("cheekSquintLeft", 0.4); self.set("cheekSquintRight", 0.4); self.set("eyeSquintLeft", 0.3); self.set("eyeSquintRight", 0.3); },
1083 "sad" => { self.set("mouthFrownLeft", 0.7); self.set("mouthFrownRight", 0.7); self.set("browInnerUp", 0.5); self.set("eyeLookDownLeft", 0.3); self.set("eyeLookDownRight", 0.3); },
1084 "angry" => { self.set("browDownLeft", 0.9); self.set("browDownRight", 0.9); self.set("mouthStretchLeft", 0.4); self.set("mouthStretchRight", 0.4); self.set("noseSneerLeft", 0.5); self.set("noseSneerRight", 0.5); },
1085 "surprised" => { self.set("eyeWideLeft", 1.0); self.set("eyeWideRight", 1.0); self.set("jawOpen", 0.6); self.set("browOuterUpLeft", 0.8); self.set("browOuterUpRight", 0.8); },
1086 "fear" => { self.set("eyeWideLeft", 0.9); self.set("eyeWideRight", 0.9); self.set("browInnerUp", 0.7); self.set("mouthPressLeft", 0.4); self.set("mouthPressRight", 0.4); },
1087 "disgust" => { self.set("noseSneerLeft", 0.9); self.set("noseSneerRight", 0.9); self.set("mouthShrugUpper", 0.5); self.set("browDownLeft", 0.4); },
1088 _ => {}
1089 }
1090 }
1091
1092 pub fn active_shape_count(&self) -> usize { self.shapes.iter().filter(|s| s.weight > 0.01).count() }
1093}
1094
1095#[derive(Debug, Clone)]
1100pub struct TimelineClip {
1101 pub id: u32,
1102 pub name: String,
1103 pub start_time: f32,
1104 pub duration: f32,
1105 pub blend_in: f32,
1106 pub blend_out: f32,
1107 pub speed: f32,
1108}
1109
1110impl TimelineClip {
1111 pub fn new(id: u32, name: &str, start: f32, duration: f32) -> Self {
1112 TimelineClip { id, name: name.to_string(), start_time: start, duration, blend_in: 0.0, blend_out: 0.0, speed: 1.0 }
1113 }
1114 pub fn end_time(&self) -> f32 { self.start_time + self.duration }
1115 pub fn is_active_at(&self, t: f32) -> bool { t >= self.start_time && t < self.end_time() }
1116 pub fn local_time(&self, t: f32) -> f32 { (t - self.start_time) * self.speed }
1117 pub fn blend_weight(&self, t: f32) -> f32 {
1118 if !self.is_active_at(t) { return 0.0; }
1119 let lt = t - self.start_time;
1120 let weight_in = if self.blend_in > 0.0 { (lt / self.blend_in).min(1.0) } else { 1.0 };
1121 let weight_out = if self.blend_out > 0.0 { ((self.duration - lt) / self.blend_out).min(1.0) } else { 1.0 };
1122 weight_in.min(weight_out)
1123 }
1124 pub fn overlaps(&self, other: &TimelineClip) -> bool {
1125 self.start_time < other.end_time() && self.end_time() > other.start_time
1126 }
1127}
1128
1129#[derive(Debug, Clone)]
1130pub struct TimelineTrack {
1131 pub name: String,
1132 pub clips: Vec<TimelineClip>,
1133 pub is_muted: bool,
1134 pub is_locked: bool,
1135}
1136
1137impl TimelineTrack {
1138 pub fn new(name: &str) -> Self { TimelineTrack { name: name.to_string(), clips: Vec::new(), is_muted: false, is_locked: false } }
1139 pub fn add_clip(&mut self, clip: TimelineClip) { self.clips.push(clip); self.clips.sort_by(|a, b| a.start_time.partial_cmp(&b.start_time).unwrap()); }
1140 pub fn clips_at(&self, t: f32) -> Vec<&TimelineClip> { self.clips.iter().filter(|c| c.is_active_at(t)).collect() }
1141 pub fn find_conflicts(&self) -> Vec<(u32, u32)> {
1142 let mut conflicts = Vec::new();
1143 for i in 0..self.clips.len() {
1144 for j in i+1..self.clips.len() {
1145 if self.clips[i].overlaps(&self.clips[j]) { conflicts.push((self.clips[i].id, self.clips[j].id)); }
1146 }
1147 }
1148 conflicts
1149 }
1150 pub fn duration(&self) -> f32 { self.clips.iter().map(|c| c.end_time()).fold(0.0_f32, f32::max) }
1151 pub fn clip_count(&self) -> usize { self.clips.len() }
1152}
1153
1154#[derive(Debug, Clone)]
1155pub struct MasterTimeline {
1156 pub tracks: Vec<TimelineTrack>,
1157 pub current_time: f32,
1158 pub fps: f32,
1159 pub is_playing: bool,
1160}
1161
1162impl MasterTimeline {
1163 pub fn new(fps: f32) -> Self { MasterTimeline { tracks: Vec::new(), current_time: 0.0, fps, is_playing: false } }
1164 pub fn add_track(&mut self, t: TimelineTrack) { self.tracks.push(t); }
1165 pub fn total_duration(&self) -> f32 { self.tracks.iter().map(|t| t.duration()).fold(0.0_f32, f32::max) }
1166 pub fn active_clips_at(&self, t: f32) -> Vec<(&str, &TimelineClip)> {
1167 self.tracks.iter().flat_map(|tr| tr.clips_at(t).into_iter().map(|c| (tr.name.as_str(), c))).collect()
1168 }
1169 pub fn snap_to_frame(&self, t: f32) -> f32 { (t * self.fps).round() / self.fps }
1170 pub fn advance(&mut self, dt: f32) { if self.is_playing { self.current_time += dt; } }
1171}
1172
1173#[derive(Debug, Clone)]
1178pub struct ShakePreset {
1179 pub name: String,
1180 pub trauma: f32,
1181 pub frequency: f32,
1182 pub max_angle_deg: f32,
1183 pub max_offset: f32,
1184 pub decay_rate: f32,
1185}
1186
1187impl ShakePreset {
1188 pub fn explosion() -> Self { ShakePreset { name: "explosion".into(), trauma: 1.0, frequency: 12.0, max_angle_deg: 5.0, max_offset: 0.3, decay_rate: 1.5 } }
1189 pub fn gunshot() -> Self { ShakePreset { name: "gunshot".into(), trauma: 0.4, frequency: 20.0, max_angle_deg: 1.5, max_offset: 0.08, decay_rate: 3.0 } }
1190 pub fn earthquake() -> Self { ShakePreset { name: "earthquake".into(), trauma: 0.8, frequency: 5.0, max_angle_deg: 3.0, max_offset: 0.5, decay_rate: 0.3 } }
1191 pub fn footstep_heavy() -> Self { ShakePreset { name: "footstep_heavy".into(), trauma: 0.15, frequency: 8.0, max_angle_deg: 0.5, max_offset: 0.02, decay_rate: 5.0 } }
1192
1193 pub fn evaluate(&self, trauma: f32, time: f32) -> (Vec3, Vec3) {
1194 let t2 = trauma * trauma;
1195 let noise_x = ((time * self.frequency).sin() * 0.5 + (time * self.frequency * 1.7).sin() * 0.3 + (time * self.frequency * 0.3).sin() * 0.2) * t2;
1196 let noise_y = ((time * self.frequency * 1.3).sin() * 0.5 + (time * self.frequency * 0.7).sin() * 0.3) * t2;
1197 let offset = Vec3::new(noise_x * self.max_offset, noise_y * self.max_offset, 0.0);
1198 let angle = Vec3::new(noise_y * self.max_angle_deg, noise_x * self.max_angle_deg, 0.0);
1199 (offset, angle)
1200 }
1201}
1202
1203pub fn run_cutscene_pipeline_tests() {
1208 let clip = AnimationClipMeta::new("walk", 2.0, 30.0);
1210 assert_eq!(clip.frame_count(), 60);
1211 assert!((clip.sample_time(2.5) - 0.5).abs() < 0.01); assert!(!clip.is_finished(1.0));
1213
1214 let node = BlendTreeNode::Blend1D {
1216 parameter: "speed".into(),
1217 children: vec![
1218 (0.0, BlendTreeNode::Clip { name: "idle".into(), weight: 1.0 }),
1219 (1.0, BlendTreeNode::Clip { name: "walk".into(), weight: 1.0 }),
1220 ],
1221 };
1222 let result = node.evaluate_1d(0.5);
1223 assert_eq!(result.len(), 2);
1224
1225 let mut ik = IkChain::new(vec!["upper".into(), "lower".into()], vec![1.0, 1.0]);
1227 ik.positions = vec![Vec3::ZERO, Vec3::new(1.0, 0.0, 0.0), Vec3::new(2.0, 0.0, 0.0)];
1228 ik.target = Vec3::new(1.5, 0.5, 0.0);
1229 ik.solve_fabrik(10, 0.001);
1230 assert!((ik.positions.last().unwrap().distance(ik.target)) < 0.01);
1231
1232 let mut rig = FacialRig::new();
1234 assert_eq!(rig.shapes.len(), 52);
1235 rig.apply_expression("happy");
1236 assert!(rig.get("mouthSmileLeft") > 0.0);
1237 assert!(rig.active_shape_count() > 0);
1238 rig.apply_expression("angry");
1239 assert!(rig.get("browDownLeft") > 0.0);
1240 rig.reset();
1241 assert_eq!(rig.active_shape_count(), 0);
1242}
1243
1244pub fn run_timeline_editor_tests() {
1245 let mut timeline = MasterTimeline::new(30.0);
1246 let mut track = TimelineTrack::new("animation");
1247 track.add_clip(TimelineClip::new(1, "idle", 0.0, 3.0));
1248 track.add_clip(TimelineClip::new(2, "walk", 3.5, 5.0));
1249 timeline.add_track(track);
1250 assert!(timeline.total_duration() > 0.0);
1251 let active = timeline.active_clips_at(1.5);
1252 assert!(!active.is_empty());
1253 assert_eq!(timeline.snap_to_frame(1.533_f32), (1.533_f32 * 30.0_f32).round() / 30.0_f32);
1254 timeline.is_playing = true;
1255 timeline.advance(0.1);
1256 assert!((timeline.current_time - 0.1).abs() < 0.001);
1257
1258 let c = TimelineClip { id: 1, name: "t".into(), start_time: 0.0, duration: 2.0, blend_in: 0.5, blend_out: 0.5, speed: 1.0 };
1260 assert!((c.blend_weight(0.25) - 0.5).abs() < 0.001);
1261 assert!((c.blend_weight(1.75) - 0.5).abs() < 0.001);
1262 assert!((c.blend_weight(1.0) - 1.0).abs() < 0.001);
1263}
1264
1265pub fn run_final_cutscene_tests() {
1266 let mut asm = AnimatorStateMachine::new("idle");
1268 asm.add_state(AnimationState::new("idle", "idle_anim"));
1269 asm.add_state(AnimationState::new("run", "run_anim"));
1270 asm.add_state(AnimationState::new("attack", "attack_anim"));
1271 let t1 = Transition { from: "idle".into(), to: "run".into(), conditions: vec![TransitionCondition::FloatGreater("speed".into(), 0.5)], duration: 0.2, exit_time: None, can_interrupt: true };
1272 let t2 = Transition { from: "run".into(), to: "attack".into(), conditions: vec![TransitionCondition::Trigger("attack".into())], duration: 0.1, exit_time: None, can_interrupt: false };
1273 asm.add_transition(t1); asm.add_transition(t2);
1274 asm.set_float("speed", 1.0);
1275 let s1 = asm.update(0.016);
1276 assert_eq!(s1, Some("run"));
1277 asm.set_trigger("attack");
1278 let s2 = asm.update(0.016);
1279 assert_eq!(s2, Some("attack"));
1280}
1281
1282pub fn run_cutscene_analytics_tests() {
1283 let mut mocap = MocapClip::new("take_1", 60.0);
1285 for i in 0..60u32 {
1286 let mut frame = MocapFrame::new(i as f32 / 60.0);
1287 frame.set_bone("Hips", Vec3::new(0.0, 1.0, i as f32 * 0.01), Quat::IDENTITY);
1288 mocap.add_frame(frame);
1289 }
1290 assert_eq!(mocap.frame_count(), 60);
1291 let downsampled = mocap.downsample(30.0);
1292 assert!(downsampled.frame_count() < mocap.frame_count());
1293
1294 let mut mapping = HashMap::new();
1296 mapping.insert("Hips".to_string(), "Root".to_string());
1297 let retargeted = mocap.retarget_to_skeleton(&mapping);
1298 assert!(!retargeted.frames.is_empty());
1299 if let Some(frame) = retargeted.frames.first() {
1300 assert!(frame.bone_transforms.contains_key("Root"));
1301 }
1302
1303 let shake = ShakePreset::explosion();
1305 let (offset, angle) = shake.evaluate(1.0, 0.5);
1306 assert!(offset.length() >= 0.0);
1307 assert!(angle.length() >= 0.0);
1308 let (offset2, _) = ShakePreset::earthquake().evaluate(0.5, 1.0);
1309 assert!(offset2.length() >= 0.0);
1310}
1311
1312pub fn run_scene_and_postprocess_tests() {
1313 let mut track = LightTrack::new("sun", LightType::Directional);
1315 track.add_keyframe(LightKeyframe { time: 0.0, position: Vec3::ZERO, rotation: Quat::IDENTITY, color: Vec3::new(1.0, 0.9, 0.7), intensity: 1.0, range: 100.0, spot_angle: 0.0, cast_shadows: true });
1316 track.add_keyframe(LightKeyframe { time: 5.0, position: Vec3::ZERO, rotation: Quat::IDENTITY, color: Vec3::new(1.0, 0.4, 0.2), intensity: 0.5, range: 80.0, spot_angle: 0.0, cast_shadows: true });
1317 let mid = track.sample(2.5).unwrap();
1318 assert!((mid.intensity - 0.75).abs() < 0.01);
1319 assert_eq!(track.duration(), 5.0);
1320 assert_eq!(track.keyframe_count(), 2);
1321}
1322
1323pub const ANIM_CLIP_MAGIC: u32 = 0x414E4D43;
1324pub const CUTSCENE_FORMAT_VERSION: u32 = 3;
1325pub const MAX_BONE_COUNT: usize = 512;
1326pub const DEFAULT_FPS: f32 = 30.0;
1327pub const CUTSCENE_MODULE_VER: &str = "3.0.0";
1328
1329pub fn cutscene_importer_module_info() -> HashMap<String, String> {
1330 let mut info = HashMap::new();
1331 info.insert("module".into(), "cutscene_importer".into());
1332 info.insert("version".into(), CUTSCENE_MODULE_VER.into());
1333 info.insert("fps".into(), format!("{}", DEFAULT_FPS));
1334 info.insert("max_bones".into(), format!("{}", MAX_BONE_COUNT));
1335 info.insert("arkit_shapes".into(), "52".into());
1336 info
1337}
1338
1339#[derive(Debug, Clone)]
1344pub struct SubtitleEntry {
1345 pub id: u32,
1346 pub start_time_s: f32,
1347 pub end_time_s: f32,
1348 pub text: String,
1349 pub speaker: Option<String>,
1350 pub language: String,
1351}
1352
1353impl SubtitleEntry {
1354 pub fn new(id: u32, start: f32, end: f32, text: &str) -> Self {
1355 SubtitleEntry { id, start_time_s: start, end_time_s: end, text: text.to_string(), speaker: None, language: "en".to_string() }
1356 }
1357 pub fn duration_s(&self) -> f32 { self.end_time_s - self.start_time_s }
1358 pub fn is_active_at(&self, t: f32) -> bool { t >= self.start_time_s && t < self.end_time_s }
1359 pub fn word_count(&self) -> usize { self.text.split_whitespace().count() }
1360 pub fn reading_speed_wpm(&self) -> f32 { self.word_count() as f32 / self.duration_s() * 60.0 }
1361 pub fn is_well_paced(&self) -> bool { let wpm = self.reading_speed_wpm(); wpm >= 80.0 && wpm <= 300.0 }
1362}
1363
1364#[derive(Debug, Clone)]
1365pub struct SubtitleTrack {
1366 pub language: String,
1367 pub entries: Vec<SubtitleEntry>,
1368 pub is_rtl: bool,
1369}
1370
1371impl SubtitleTrack {
1372 pub fn new(language: &str) -> Self {
1373 let is_rtl = matches!(language, "ar" | "he" | "fa" | "ur");
1374 SubtitleTrack { language: language.to_string(), entries: Vec::new(), is_rtl }
1375 }
1376 pub fn add_entry(&mut self, e: SubtitleEntry) { self.entries.push(e); self.entries.sort_by(|a,b| a.start_time_s.partial_cmp(&b.start_time_s).unwrap()); }
1377 pub fn active_at(&self, t: f32) -> Option<&SubtitleEntry> { self.entries.iter().find(|e| e.is_active_at(t)) }
1378 pub fn entry_count(&self) -> usize { self.entries.len() }
1379 pub fn total_duration(&self) -> f32 { self.entries.last().map(|e| e.end_time_s).unwrap_or(0.0) }
1380
1381 pub fn export_srt(&self) -> String {
1382 let mut out = String::new();
1383 for (i, entry) in self.entries.iter().enumerate() {
1384 out.push_str(&format!("{}\n", i + 1));
1385 out.push_str(&format!("{} --> {}\n", srt_time(entry.start_time_s), srt_time(entry.end_time_s)));
1386 if let Some(sp) = &entry.speaker { out.push_str(&format!("[{}] ", sp)); }
1387 out.push_str(&format!("{}\n\n", entry.text));
1388 }
1389 out
1390 }
1391
1392 pub fn pacing_issues(&self) -> Vec<u32> { self.entries.iter().filter(|e| !e.is_well_paced()).map(|e| e.id).collect() }
1393}
1394
1395fn srt_time(t: f32) -> String {
1396 let ms = ((t % 1.0) * 1000.0) as u32;
1397 let s = t as u32 % 60;
1398 let m = t as u32 / 60 % 60;
1399 let h = t as u32 / 3600;
1400 format!("{:02}:{:02}:{:02},{:03}", h, m, s, ms)
1401}
1402
1403pub fn parse_srt(input: &str) -> SubtitleTrack {
1404 let mut track = SubtitleTrack::new("en");
1405 let mut id = 1u32;
1406 let lines: Vec<&str> = input.lines().collect();
1407 let mut i = 0;
1408 while i < lines.len() {
1409 let line = lines[i].trim();
1410 if line.parse::<u32>().is_ok() {
1411 i += 1;
1412 if i < lines.len() {
1413 let time_line = lines[i].trim();
1414 let parts: Vec<&str> = time_line.split("-->").collect();
1415 if parts.len() == 2 {
1416 let start = parse_srt_time(parts[0].trim()).unwrap_or(0.0);
1417 let end = parse_srt_time(parts[1].trim()).unwrap_or(0.0);
1418 i += 1;
1419 let mut text_lines = Vec::new();
1420 while i < lines.len() && !lines[i].trim().is_empty() {
1421 text_lines.push(lines[i]); i += 1;
1422 }
1423 let text = text_lines.join("\n");
1424 track.add_entry(SubtitleEntry::new(id, start, end, &text));
1425 id += 1;
1426 }
1427 }
1428 }
1429 i += 1;
1430 }
1431 track
1432}
1433
1434fn parse_srt_time(s: &str) -> Option<f32> {
1435 let s = s.replace(",", ".");
1436 let parts: Vec<&str> = s.split(':').collect();
1437 if parts.len() == 3 {
1438 let h: f32 = parts[0].parse().ok()?;
1439 let m: f32 = parts[1].parse().ok()?;
1440 let sec: f32 = parts[2].parse().ok()?;
1441 Some(h * 3600.0 + m * 60.0 + sec)
1442 } else { None }
1443}
1444
1445#[derive(Debug, Clone, PartialEq)]
1450pub enum StoryboardStatus { Concept, Sketch, Final, Approved }
1451
1452#[derive(Debug, Clone)]
1453pub struct StoryboardPanel {
1454 pub panel_id: u32,
1455 pub shot_number: String,
1456 pub description: String,
1457 pub duration_s: f32,
1458 pub camera_angle: String,
1459 pub action_notes: String,
1460 pub dialogue: Option<String>,
1461 pub status: StoryboardStatus,
1462}
1463
1464impl StoryboardPanel {
1465 pub fn new(id: u32, shot: &str, desc: &str, duration: f32) -> Self {
1466 StoryboardPanel { panel_id: id, shot_number: shot.to_string(), description: desc.to_string(),
1467 duration_s: duration, camera_angle: "medium".to_string(), action_notes: String::new(),
1468 dialogue: None, status: StoryboardStatus::Concept }
1469 }
1470 pub fn approve(&mut self) { self.status = StoryboardStatus::Approved; }
1471 pub fn is_approved(&self) -> bool { self.status == StoryboardStatus::Approved }
1472}
1473
1474#[derive(Debug, Clone)]
1475pub struct Storyboard {
1476 pub title: String,
1477 pub panels: Vec<StoryboardPanel>,
1478 pub director: String,
1479}
1480
1481impl Storyboard {
1482 pub fn new(title: &str) -> Self { Storyboard { title: title.to_string(), panels: Vec::new(), director: String::new() } }
1483 pub fn add_panel(&mut self, p: StoryboardPanel) { self.panels.push(p); }
1484 pub fn panel_count(&self) -> usize { self.panels.len() }
1485 pub fn total_duration_s(&self) -> f32 { self.panels.iter().map(|p| p.duration_s).sum() }
1486 pub fn completion_percent(&self) -> f32 { self.panels.iter().filter(|p| p.is_approved()).count() as f32 / self.panels.len().max(1) as f32 * 100.0 }
1487 pub fn approved_count(&self) -> usize { self.panels.iter().filter(|p| p.is_approved()).count() }
1488
1489 pub fn export_pdf_script(&self) -> String {
1490 let mut out = format!("STORYBOARD: {}\nDirector: {}\nTotal Duration: {:.1}s\n\n", self.title, self.director, self.total_duration_s());
1491 for panel in &self.panels {
1492 out.push_str(&format!("SHOT {}: [{:?}] {:.1}s\n {}\n", panel.shot_number, panel.status, panel.duration_s, panel.description));
1493 if let Some(d) = &panel.dialogue { out.push_str(&format!(" Dialogue: \"{}\"\n", d)); }
1494 }
1495 out
1496 }
1497}
1498
1499#[derive(Debug, Clone)]
1504pub struct PerformanceTake {
1505 pub take_id: u32,
1506 pub clip_name: String,
1507 pub duration_s: f32,
1508 pub quality_score: f32, pub notes: String,
1510 pub is_approved: bool,
1511 pub timestamp: String,
1512}
1513
1514impl PerformanceTake {
1515 pub fn new(id: u32, clip: &str, duration: f32) -> Self {
1516 PerformanceTake { take_id: id, clip_name: clip.to_string(), duration_s: duration,
1517 quality_score: 0.0, notes: String::new(), is_approved: false, timestamp: "2024-01-01T00:00:00Z".to_string() }
1518 }
1519 pub fn approve(&mut self) { self.is_approved = true; }
1520 pub fn set_quality(&mut self, score: f32) { self.quality_score = score.clamp(0.0, 100.0); }
1521}
1522
1523#[derive(Debug, Clone)]
1524pub struct PerformanceCaptureSession {
1525 pub session_name: String,
1526 pub takes: Vec<PerformanceTake>,
1527 pub actor_name: String,
1528}
1529
1530impl PerformanceCaptureSession {
1531 pub fn new(name: &str, actor: &str) -> Self { PerformanceCaptureSession { session_name: name.to_string(), takes: Vec::new(), actor_name: actor.to_string() } }
1532 pub fn add_take(&mut self, take: PerformanceTake) { self.takes.push(take); }
1533 pub fn approved_takes(&self) -> Vec<&PerformanceTake> { self.takes.iter().filter(|t| t.is_approved).collect() }
1534 pub fn select_best_take(&self) -> Option<&PerformanceTake> {
1535 self.takes.iter().max_by(|a, b| a.quality_score.partial_cmp(&b.quality_score).unwrap())
1536 }
1537 pub fn take_count(&self) -> usize { self.takes.len() }
1538 pub fn avg_quality(&self) -> f32 { if self.takes.is_empty() { return 0.0; } self.takes.iter().map(|t| t.quality_score).sum::<f32>() / self.takes.len() as f32 }
1539}
1540
1541#[derive(Debug, Clone)]
1546pub struct ColorGradeKeyframe {
1547 pub time_s: f32,
1548 pub lift: Vec3,
1549 pub gamma: Vec3,
1550 pub gain: Vec3,
1551 pub saturation: f32,
1552 pub contrast: f32,
1553 pub exposure: f32,
1554}
1555
1556impl ColorGradeKeyframe {
1557 pub fn neutral() -> Self {
1558 ColorGradeKeyframe { time_s: 0.0, lift: Vec3::ZERO, gamma: Vec3::ONE, gain: Vec3::ONE, saturation: 1.0, contrast: 1.0, exposure: 0.0 }
1559 }
1560 pub fn horror() -> Self {
1561 ColorGradeKeyframe { time_s: 0.0, lift: Vec3::new(-0.02, -0.02, 0.05), gamma: Vec3::new(0.9, 0.9, 1.1), gain: Vec3::new(0.8, 0.8, 1.2), saturation: 0.6, contrast: 1.3, exposure: -0.5 }
1562 }
1563 pub fn golden_hour() -> Self {
1564 ColorGradeKeyframe { time_s: 0.0, lift: Vec3::new(0.05, 0.02, -0.02), gamma: Vec3::new(1.1, 1.0, 0.9), gain: Vec3::new(1.3, 1.1, 0.7), saturation: 1.3, contrast: 1.1, exposure: 0.3 }
1565 }
1566 pub fn lerp(&self, other: &ColorGradeKeyframe, t: f32) -> ColorGradeKeyframe {
1567 ColorGradeKeyframe {
1568 time_s: self.time_s + t * (other.time_s - self.time_s),
1569 lift: self.lift.lerp(other.lift, t), gamma: self.gamma.lerp(other.gamma, t),
1570 gain: self.gain.lerp(other.gain, t), saturation: self.saturation + t * (other.saturation - self.saturation),
1571 contrast: self.contrast + t * (other.contrast - self.contrast), exposure: self.exposure + t * (other.exposure - self.exposure),
1572 }
1573 }
1574}
1575
1576#[derive(Debug, Clone)]
1577pub struct ColorGradingTrack {
1578 pub keyframes: Vec<ColorGradeKeyframe>,
1579}
1580
1581impl ColorGradingTrack {
1582 pub fn new() -> Self { ColorGradingTrack { keyframes: Vec::new() } }
1583 pub fn add_kf(&mut self, kf: ColorGradeKeyframe) { self.keyframes.push(kf); self.keyframes.sort_by(|a,b| a.time_s.partial_cmp(&b.time_s).unwrap()); }
1584 pub fn sample(&self, t: f32) -> ColorGradeKeyframe {
1585 if self.keyframes.is_empty() { return ColorGradeKeyframe::neutral(); }
1586 if t <= self.keyframes[0].time_s { return self.keyframes[0].clone(); }
1587 let last = self.keyframes.last().unwrap();
1588 if t >= last.time_s { return last.clone(); }
1589 for i in 0..self.keyframes.len()-1 {
1590 let a = &self.keyframes[i]; let b = &self.keyframes[i+1];
1591 if t >= a.time_s && t <= b.time_s { return a.lerp(b, (t - a.time_s)/(b.time_s - a.time_s)); }
1592 }
1593 ColorGradeKeyframe::neutral()
1594 }
1595}
1596
1597#[derive(Debug, Clone)]
1602pub struct GraphNode {
1603 pub node_id: u32,
1604 pub name: String,
1605 pub node_type: String,
1606 pub data: HashMap<String, String>,
1607}
1608
1609impl GraphNode {
1610 pub fn new(id: u32, name: &str, node_type: &str) -> Self {
1611 GraphNode { node_id: id, name: name.to_string(), node_type: node_type.to_string(), data: HashMap::new() }
1612 }
1613 pub fn clip_node(id: u32, clip_name: &str) -> Self { let mut n = Self::new(id, clip_name, "clip"); n.data.insert("clip".into(), clip_name.into()); n }
1614 pub fn blend_node(id: u32) -> Self { Self::new(id, &format!("blend_{}", id), "blend") }
1615 pub fn additive_node(id: u32) -> Self { Self::new(id, &format!("additive_{}", id), "additive") }
1616}
1617
1618#[derive(Debug, Clone)]
1619pub struct GraphEdge {
1620 pub from: u32, pub to: u32, pub weight: f32,
1621}
1622
1623#[derive(Debug, Clone)]
1624pub struct AnimationGraph {
1625 pub nodes: Vec<GraphNode>,
1626 pub edges: Vec<GraphEdge>,
1627}
1628
1629impl AnimationGraph {
1630 pub fn new() -> Self { AnimationGraph { nodes: Vec::new(), edges: Vec::new() } }
1631 pub fn add_node(&mut self, n: GraphNode) { self.nodes.push(n); }
1632 pub fn add_edge(&mut self, from: u32, to: u32, weight: f32) { self.edges.push(GraphEdge { from, to, weight }); }
1633
1634 pub fn topological_sort(&self) -> Option<Vec<u32>> {
1635 let node_ids: Vec<u32> = self.nodes.iter().map(|n| n.node_id).collect();
1636 let mut in_degree: HashMap<u32, usize> = node_ids.iter().map(|&id| (id, 0)).collect();
1637 for e in &self.edges { *in_degree.entry(e.to).or_insert(0) += 1; }
1638 let mut queue: VecDeque<u32> = in_degree.iter().filter(|(_, &d)| d == 0).map(|(&id, _)| id).collect();
1639 let mut result = Vec::new();
1640 while let Some(n) = queue.pop_front() {
1641 result.push(n);
1642 for e in self.edges.iter().filter(|e| e.from == n) {
1643 let d = in_degree.entry(e.to).or_insert(0);
1644 *d = d.saturating_sub(1);
1645 if *d == 0 { queue.push_back(e.to); }
1646 }
1647 }
1648 if result.len() == self.nodes.len() { Some(result) } else { None }
1649 }
1650
1651 pub fn has_cycle(&self) -> bool { self.topological_sort().is_none() }
1652 pub fn node_count(&self) -> usize { self.nodes.len() }
1653 pub fn edge_count(&self) -> usize { self.edges.len() }
1654}
1655
1656#[derive(Debug, Clone, PartialEq)]
1661pub enum CameraRigType { Free, Dolly, Crane, Steadicam, Orbit, Handheld, VR, Drone, POV, MotionControlRig }
1662
1663#[derive(Debug, Clone)]
1664pub struct CameraRigState {
1665 pub rig_type: CameraRigType,
1666 pub position: Vec3,
1667 pub rotation: Quat,
1668 pub fov_deg: f32,
1669 pub focal_length_mm: f32,
1670 pub focus_distance_m: f32,
1671 pub aperture_f: f32,
1672 pub near_clip: f32,
1673 pub far_clip: f32,
1674 pub sensor_width_mm: f32,
1675}
1676
1677impl CameraRigState {
1678 pub fn new(rig_type: CameraRigType) -> Self {
1679 CameraRigState { rig_type, position: Vec3::ZERO, rotation: Quat::IDENTITY,
1680 fov_deg: 60.0, focal_length_mm: 35.0, focus_distance_m: 5.0,
1681 aperture_f: 2.8, near_clip: 0.1, far_clip: 1000.0, sensor_width_mm: 36.0 }
1682 }
1683
1684 pub fn exposure_value(&self) -> f32 {
1685 let aperture = self.aperture_f;
1687 (aperture * aperture).log2()
1688 }
1689
1690 pub fn hyperfocal_m(&self) -> f32 {
1691 let f = self.focal_length_mm / 1000.0;
1692 let c = 0.0291 / 1000.0;
1693 f * f / (self.aperture_f * c)
1694 }
1695
1696 pub fn apply_shake(&mut self, shake_offset: Vec3, shake_rotation: Vec3) {
1697 self.position += shake_offset;
1698 let q = Quat::from_euler(glam::EulerRot::XYZ, shake_rotation.x.to_radians(), shake_rotation.y.to_radians(), shake_rotation.z.to_radians());
1699 self.rotation = self.rotation * q;
1700 }
1701
1702 pub fn projection_matrix(&self, aspect: f32) -> Mat4 {
1703 Mat4::perspective_rh_gl(self.fov_deg.to_radians(), aspect, self.near_clip, self.far_clip)
1704 }
1705
1706 pub fn view_matrix(&self) -> Mat4 {
1707 Mat4::from_rotation_translation(self.rotation, self.position).inverse()
1708 }
1709}
1710
1711pub fn run_subtitle_tests() {
1716 let srt_data = "1\n00:00:00,000 --> 00:00:02,000\nHello, world!\n\n2\n00:00:03,000 --> 00:00:06,000\nThis is a test.\n\n";
1717 let track = parse_srt(srt_data);
1718 assert_eq!(track.entry_count(), 2);
1719 assert!(track.active_at(1.0).is_some());
1720 assert!(track.active_at(4.0).is_some());
1721 assert!(track.active_at(10.0).is_none());
1722 let srt_out = track.export_srt();
1723 assert!(srt_out.contains("-->"));
1724 assert!(!track.is_rtl);
1725
1726 let rtl_track = SubtitleTrack::new("ar");
1727 assert!(rtl_track.is_rtl);
1728}
1729
1730pub fn run_storyboard_tests() {
1731 let mut board = Storyboard::new("Act 1");
1732 board.add_panel(StoryboardPanel::new(1, "1A", "Hero enters village", 3.0));
1733 board.add_panel(StoryboardPanel::new(2, "1B", "Wide shot of village", 2.5));
1734 let mut panel3 = StoryboardPanel::new(3, "1C", "Villain appears", 4.0);
1735 panel3.approve();
1736 board.add_panel(panel3);
1737 assert_eq!(board.panel_count(), 3);
1738 assert_eq!(board.approved_count(), 1);
1739 assert!((board.completion_percent() - 33.33).abs() < 0.5);
1740 assert!((board.total_duration_s() - 9.5).abs() < 0.001);
1741 let script = board.export_pdf_script();
1742 assert!(script.contains("Act 1"));
1743}
1744
1745pub fn run_performance_capture_tests() {
1746 let mut session = PerformanceCaptureSession::new("Walk Cycle Session", "John Actor");
1747 let mut t1 = PerformanceTake::new(1, "walk_take_1", 5.0); t1.set_quality(72.0);
1748 let mut t2 = PerformanceTake::new(2, "walk_take_2", 5.2); t2.set_quality(88.0); t2.approve();
1749 let mut t3 = PerformanceTake::new(3, "walk_take_3", 4.8); t3.set_quality(65.0);
1750 session.add_take(t1); session.add_take(t2); session.add_take(t3);
1751 assert_eq!(session.take_count(), 3);
1752 assert_eq!(session.approved_takes().len(), 1);
1753 let best = session.select_best_take().unwrap();
1754 assert_eq!(best.take_id, 2);
1755 assert!(session.avg_quality() > 0.0);
1756}
1757
1758pub fn run_color_grading_tests() {
1759 let neutral = ColorGradeKeyframe::neutral();
1760 let horror = ColorGradeKeyframe::horror();
1761 let lerped = neutral.lerp(&horror, 0.5);
1762 assert!(lerped.saturation < 1.0);
1763 let golden = ColorGradeKeyframe::golden_hour();
1764 assert!(golden.saturation > 1.0);
1765
1766 let mut track = ColorGradingTrack::new();
1767 track.add_kf(ColorGradeKeyframe::neutral());
1768 let mut mid = ColorGradeKeyframe::horror(); mid.time_s = 5.0;
1769 track.add_kf(mid);
1770 let sampled = track.sample(2.5);
1771 assert!(sampled.saturation < 1.0 && sampled.saturation > 0.5);
1772}
1773
1774pub fn run_animation_graph_tests() {
1775 let mut graph = AnimationGraph::new();
1776 graph.add_node(GraphNode::clip_node(1, "idle"));
1777 graph.add_node(GraphNode::clip_node(2, "walk"));
1778 graph.add_node(GraphNode::blend_node(3));
1779 graph.add_edge(1, 3, 0.5);
1780 graph.add_edge(2, 3, 0.5);
1781 assert!(!graph.has_cycle());
1782 let sorted = graph.topological_sort().unwrap();
1783 assert_eq!(sorted.len(), 3);
1784 assert_eq!(*sorted.last().unwrap(), 3);
1786 graph.add_edge(3, 1, 1.0);
1788 assert!(graph.has_cycle());
1789}
1790
1791pub fn run_camera_rig_tests() {
1792 let mut cam = CameraRigState::new(CameraRigType::Steadicam);
1793 assert!(cam.exposure_value() > 0.0);
1794 assert!(cam.hyperfocal_m() > 0.0);
1795 cam.apply_shake(Vec3::new(0.01, 0.02, 0.0), Vec3::new(0.1, 0.0, 0.0));
1796 assert!(cam.position.length() > 0.0);
1797 let view = cam.view_matrix();
1798 let _proj = cam.projection_matrix(16.0 / 9.0);
1799 assert!(view.col(3).truncate().length() >= 0.0);
1800}
1801
1802pub fn run_all_cutscene_tests() {
1803 run_cutscene_pipeline_tests();
1804 run_timeline_editor_tests();
1805 run_final_cutscene_tests();
1806 run_cutscene_analytics_tests();
1807 run_scene_and_postprocess_tests();
1808 run_subtitle_tests();
1809 run_storyboard_tests();
1810 run_performance_capture_tests();
1811 run_color_grading_tests();
1812 run_animation_graph_tests();
1813 run_camera_rig_tests();
1814}
1815
1816
1817#[derive(Debug, Clone, PartialEq)]
1822pub enum PostProcessEffectType {
1823 Bloom, DepthOfField, MotionBlur, AmbientOcclusion, Vignette,
1824 ChromaticAberration, FilmGrain, ColorGrading, ToneMapping,
1825 Sharpen, LensFlare, GodRays, ScreenSpaceReflections,
1826}
1827
1828#[derive(Debug, Clone)]
1829pub struct PostProcessEffect {
1830 pub effect_type: PostProcessEffectType,
1831 pub enabled: bool,
1832 pub intensity: f32,
1833 pub params: HashMap<String, f32>,
1834}
1835
1836impl PostProcessEffect {
1837 pub fn new(effect_type: PostProcessEffectType, intensity: f32) -> Self {
1838 Self { effect_type, enabled: true, intensity, params: HashMap::new() }
1839 }
1840 pub fn bloom(threshold: f32, scatter: f32, intensity: f32) -> Self {
1841 let mut e = Self::new(PostProcessEffectType::Bloom, intensity);
1842 e.params.insert("threshold".to_string(), threshold);
1843 e.params.insert("scatter".to_string(), scatter);
1844 e
1845 }
1846 pub fn depth_of_field(focal_length_mm: f32, aperture: f32, focus_distance_m: f32) -> Self {
1847 let mut e = Self::new(PostProcessEffectType::DepthOfField, 1.0);
1848 e.params.insert("focal_length_mm".to_string(), focal_length_mm);
1849 e.params.insert("aperture".to_string(), aperture);
1850 e.params.insert("focus_distance_m".to_string(), focus_distance_m);
1851 e
1852 }
1853 pub fn motion_blur(shutter_angle: f32, sample_count: f32) -> Self {
1854 let mut e = Self::new(PostProcessEffectType::MotionBlur, 1.0);
1855 e.params.insert("shutter_angle".to_string(), shutter_angle);
1856 e.params.insert("sample_count".to_string(), sample_count);
1857 e
1858 }
1859 pub fn vignette(intensity: f32, smoothness: f32) -> Self {
1860 let mut e = Self::new(PostProcessEffectType::Vignette, intensity);
1861 e.params.insert("smoothness".to_string(), smoothness);
1862 e
1863 }
1864 pub fn film_grain(intensity: f32, response: f32) -> Self {
1865 let mut e = Self::new(PostProcessEffectType::FilmGrain, intensity);
1866 e.params.insert("response".to_string(), response);
1867 e
1868 }
1869 pub fn effect_type_str(&self) -> &'static str {
1870 match &self.effect_type {
1871 PostProcessEffectType::Bloom => "Bloom",
1872 PostProcessEffectType::DepthOfField => "DepthOfField",
1873 PostProcessEffectType::MotionBlur => "MotionBlur",
1874 PostProcessEffectType::AmbientOcclusion => "AmbientOcclusion",
1875 PostProcessEffectType::Vignette => "Vignette",
1876 PostProcessEffectType::ChromaticAberration => "ChromaticAberration",
1877 PostProcessEffectType::FilmGrain => "FilmGrain",
1878 PostProcessEffectType::ColorGrading => "ColorGrading",
1879 PostProcessEffectType::ToneMapping => "ToneMapping",
1880 PostProcessEffectType::Sharpen => "Sharpen",
1881 PostProcessEffectType::LensFlare => "LensFlare",
1882 PostProcessEffectType::GodRays => "GodRays",
1883 PostProcessEffectType::ScreenSpaceReflections => "SSR",
1884 }
1885 }
1886}
1887
1888#[derive(Debug, Clone, Default)]
1889pub struct PostProcessStack {
1890 pub effects: Vec<PostProcessEffect>,
1891 pub exposure: f32,
1892 pub contrast: f32,
1893 pub saturation: f32,
1894 pub temperature: f32, }
1896
1897impl PostProcessStack {
1898 pub fn new() -> Self {
1899 Self { effects: Vec::new(), exposure: 1.0, contrast: 1.0, saturation: 1.0, temperature: 0.0 }
1900 }
1901 pub fn add_effect(&mut self, e: PostProcessEffect) { self.effects.push(e); }
1902 pub fn enabled_effects(&self) -> Vec<&PostProcessEffect> {
1903 self.effects.iter().filter(|e| e.enabled).collect()
1904 }
1905 pub fn get_effect(&self, effect_type: &PostProcessEffectType) -> Option<&PostProcessEffect> {
1906 self.effects.iter().find(|e| &e.effect_type == effect_type)
1907 }
1908 pub fn get_effect_mut(&mut self, effect_type: &PostProcessEffectType) -> Option<&mut PostProcessEffect> {
1909 self.effects.iter_mut().find(|e| &e.effect_type == effect_type)
1910 }
1911 pub fn cinema_preset() -> Self {
1912 let mut stack = Self::new();
1913 stack.add_effect(PostProcessEffect::bloom(0.9, 0.7, 0.5));
1914 stack.add_effect(PostProcessEffect::depth_of_field(35.0, 2.8, 5.0));
1915 stack.add_effect(PostProcessEffect::vignette(0.4, 0.4));
1916 stack.add_effect(PostProcessEffect::film_grain(0.15, 0.8));
1917 stack.contrast = 1.15;
1918 stack.saturation = 0.9;
1919 stack.temperature = -200.0;
1920 stack
1921 }
1922 pub fn horror_preset() -> Self {
1923 let mut stack = Self::new();
1924 stack.add_effect(PostProcessEffect::vignette(0.8, 0.3));
1925 stack.add_effect(PostProcessEffect::film_grain(0.4, 0.9));
1926 stack.contrast = 1.4;
1927 stack.saturation = 0.3;
1928 stack.temperature = -500.0;
1929 stack.exposure = 0.8;
1930 stack
1931 }
1932 pub fn daylight_preset() -> Self {
1933 let mut stack = Self::new();
1934 stack.add_effect(PostProcessEffect::bloom(1.0, 0.5, 0.3));
1935 stack.contrast = 1.1;
1936 stack.saturation = 1.2;
1937 stack.temperature = 300.0;
1938 stack.exposure = 1.1;
1939 stack
1940 }
1941 pub fn effect_count(&self) -> usize { self.effects.len() }
1942}
1943
1944#[derive(Debug, Clone, PartialEq)]
1949pub enum UsdPrimType {
1950 Xform, Mesh, Camera, Light, Scope, Material, Shader, Points,
1951}
1952
1953#[derive(Debug, Clone)]
1954pub struct UsdPrim {
1955 pub path: String,
1956 pub prim_type: UsdPrimType,
1957 pub attributes: HashMap<String, String>,
1958 pub children: Vec<String>,
1959 pub active: bool,
1960 pub instanceable: bool,
1961}
1962
1963impl UsdPrim {
1964 pub fn new(path: &str, prim_type: UsdPrimType) -> Self {
1965 Self { path: path.to_string(), prim_type, attributes: HashMap::new(),
1966 children: Vec::new(), active: true, instanceable: false }
1967 }
1968 pub fn set_attr(&mut self, name: &str, value: &str) {
1969 self.attributes.insert(name.to_string(), value.to_string());
1970 }
1971 pub fn get_attr(&self, name: &str) -> Option<&String> {
1972 self.attributes.get(name)
1973 }
1974 pub fn prim_type_str(&self) -> &'static str {
1975 match &self.prim_type {
1976 UsdPrimType::Xform => "Xform", UsdPrimType::Mesh => "Mesh",
1977 UsdPrimType::Camera => "Camera", UsdPrimType::Light => "Light",
1978 UsdPrimType::Scope => "Scope", UsdPrimType::Material => "Material",
1979 UsdPrimType::Shader => "Shader", UsdPrimType::Points => "Points",
1980 }
1981 }
1982 pub fn sdf_declaration(&self) -> String {
1983 format!("def {} \"{}\"", self.prim_type_str(), self.path.split('/').last().unwrap_or("root"))
1984 }
1985}
1986
1987#[derive(Debug, Clone, Default)]
1988pub struct UsdStage {
1989 pub identifier: String,
1990 pub prims: Vec<UsdPrim>,
1991 pub default_prim: String,
1992 pub start_time_code: f64,
1993 pub end_time_code: f64,
1994 pub time_codes_per_second: f64,
1995 pub up_axis: String,
1996}
1997
1998impl UsdStage {
1999 pub fn new(identifier: &str) -> Self {
2000 Self { identifier: identifier.to_string(), prims: Vec::new(),
2001 default_prim: String::new(), start_time_code: 0.0,
2002 end_time_code: 240.0, time_codes_per_second: 24.0, up_axis: "Y".to_string() }
2003 }
2004 pub fn add_prim(&mut self, p: UsdPrim) { self.prims.push(p); }
2005 pub fn get_prim(&self, path: &str) -> Option<&UsdPrim> {
2006 self.prims.iter().find(|p| p.path == path)
2007 }
2008 pub fn prim_count(&self) -> usize { self.prims.len() }
2009 pub fn duration_seconds(&self) -> f64 {
2010 (self.end_time_code - self.start_time_code) / self.time_codes_per_second
2011 }
2012 pub fn export_usda_header(&self) -> String {
2013 format!(
2014 "#usda 1.0\n(\n defaultPrim = \"{}\"\n startTimeCode = {}\n endTimeCode = {}\n timeCodesPerSecond = {}\n upAxis = \"{}\"\n)\n",
2015 self.default_prim, self.start_time_code, self.end_time_code,
2016 self.time_codes_per_second, self.up_axis
2017 )
2018 }
2019 pub fn prims_of_type(&self, prim_type: &UsdPrimType) -> Vec<&UsdPrim> {
2020 self.prims.iter().filter(|p| &p.prim_type == prim_type).collect()
2021 }
2022 pub fn camera_prims(&self) -> Vec<&UsdPrim> { self.prims_of_type(&UsdPrimType::Camera) }
2023 pub fn mesh_prims(&self) -> Vec<&UsdPrim> { self.prims_of_type(&UsdPrimType::Mesh) }
2024}
2025
2026#[derive(Debug, Clone)]
2031pub struct AlembicSample {
2032 pub time_s: f64,
2033 pub positions: Vec<Vec3>,
2034 pub velocities: Vec<Vec3>,
2035 pub normals: Vec<Vec3>,
2036 pub uvs: Vec<Vec2>,
2037}
2038
2039impl AlembicSample {
2040 pub fn new(time_s: f64) -> Self {
2041 Self { time_s, positions: Vec::new(), velocities: Vec::new(), normals: Vec::new(), uvs: Vec::new() }
2042 }
2043 pub fn point_count(&self) -> usize { self.positions.len() }
2044 pub fn add_point(&mut self, pos: Vec3, vel: Vec3) {
2045 self.positions.push(pos);
2046 self.velocities.push(vel);
2047 }
2048 pub fn bounding_box(&self) -> (Vec3, Vec3) {
2049 if self.positions.is_empty() { return (Vec3::ZERO, Vec3::ZERO); }
2050 let mut min = self.positions[0];
2051 let mut max = self.positions[0];
2052 for &p in &self.positions {
2053 min = min.min(p); max = max.max(p);
2054 }
2055 (min, max)
2056 }
2057}
2058
2059#[derive(Debug, Clone)]
2060pub struct AlembicObject {
2061 pub name: String,
2062 pub schema: String, pub samples: Vec<AlembicSample>,
2064 pub is_constant: bool,
2065}
2066
2067impl AlembicObject {
2068 pub fn new(name: &str, schema: &str) -> Self {
2069 Self { name: name.to_string(), schema: schema.to_string(), samples: Vec::new(), is_constant: false }
2070 }
2071 pub fn add_sample(&mut self, s: AlembicSample) { self.samples.push(s); }
2072 pub fn sample_count(&self) -> usize { self.samples.len() }
2073 pub fn duration_s(&self) -> f64 {
2074 if self.samples.is_empty() { return 0.0; }
2075 let first = self.samples[0].time_s;
2076 let last = self.samples[self.samples.len()-1].time_s;
2077 last - first
2078 }
2079 pub fn sample_at_time(&self, t: f64) -> Option<&AlembicSample> {
2080 if self.samples.is_empty() { return None; }
2081 let idx = self.samples.iter().enumerate()
2082 .min_by(|(_, a), (_, b)| {
2083 (a.time_s - t).abs().partial_cmp(&(b.time_s - t).abs())
2084 .unwrap_or(std::cmp::Ordering::Equal)
2085 })
2086 .map(|(i, _)| i);
2087 idx.map(|i| &self.samples[i])
2088 }
2089}
2090
2091#[derive(Debug, Clone, Default)]
2092pub struct AlembicArchive {
2093 pub filename: String,
2094 pub objects: Vec<AlembicObject>,
2095 pub start_time: f64,
2096 pub end_time: f64,
2097 pub fps: f64,
2098}
2099
2100impl AlembicArchive {
2101 pub fn new(filename: &str, fps: f64) -> Self {
2102 Self { filename: filename.to_string(), objects: Vec::new(), start_time: 0.0, end_time: 0.0, fps }
2103 }
2104 pub fn add_object(&mut self, o: AlembicObject) { self.objects.push(o); }
2105 pub fn object_count(&self) -> usize { self.objects.len() }
2106 pub fn total_samples(&self) -> usize { self.objects.iter().map(|o| o.sample_count()).sum() }
2107 pub fn duration_s(&self) -> f64 { self.end_time - self.start_time }
2108 pub fn frame_count(&self) -> usize { (self.duration_s() * self.fps) as usize }
2109 pub fn find_object(&self, name: &str) -> Option<&AlembicObject> {
2110 self.objects.iter().find(|o| o.name == name)
2111 }
2112}
2113
2114#[derive(Debug, Clone)]
2119pub struct QuantizedQuat {
2120 pub x: i16, pub y: i16, pub z: i16, pub w: i16,
2121}
2122
2123impl QuantizedQuat {
2124 pub fn from_quat(q: Quat) -> Self {
2125 let scale = 32767.0_f32;
2126 Self {
2127 x: (q.x * scale) as i16,
2128 y: (q.y * scale) as i16,
2129 z: (q.z * scale) as i16,
2130 w: (q.w * scale) as i16,
2131 }
2132 }
2133 pub fn to_quat(&self) -> Quat {
2134 let inv_scale = 1.0 / 32767.0;
2135 Quat::from_xyzw(
2136 self.x as f32 * inv_scale,
2137 self.y as f32 * inv_scale,
2138 self.z as f32 * inv_scale,
2139 self.w as f32 * inv_scale,
2140 ).normalize()
2141 }
2142 pub fn encode_smallest_three(q: Quat) -> (u8, i16, i16, i16) {
2143 let components = [q.x, q.y, q.z, q.w];
2144 let max_idx = components.iter().enumerate()
2145 .max_by(|(_, a), (_, b)| a.abs().partial_cmp(&b.abs()).unwrap_or(std::cmp::Ordering::Equal))
2146 .map(|(i, _)| i).unwrap_or(3);
2147 let sign = if components[max_idx] >= 0.0 { 1.0 } else { -1.0 };
2148 let scale = 32767.0_f32 / std::f32::consts::FRAC_1_SQRT_2;
2149 let small: Vec<i16> = (0..4).filter(|&i| i != max_idx)
2150 .map(|i| (components[i] * sign * scale) as i16)
2151 .collect();
2152 (max_idx as u8, small[0], small[1], small[2])
2153 }
2154}
2155
2156#[derive(Debug, Clone)]
2157pub struct QuantizedVec3 {
2158 pub x: i16, pub y: i16, pub z: i16,
2159}
2160
2161impl QuantizedVec3 {
2162 pub fn from_vec3_range(v: Vec3, min: Vec3, max: Vec3) -> Self {
2163 let range = max - min;
2164 let norm = (v - min) / Vec3::new(range.x.max(0.001), range.y.max(0.001), range.z.max(0.001));
2165 Self {
2166 x: (norm.x.clamp(0.0, 1.0) * 65535.0 - 32768.0) as i16,
2167 y: (norm.y.clamp(0.0, 1.0) * 65535.0 - 32768.0) as i16,
2168 z: (norm.z.clamp(0.0, 1.0) * 65535.0 - 32768.0) as i16,
2169 }
2170 }
2171 pub fn to_vec3_range(&self, min: Vec3, max: Vec3) -> Vec3 {
2172 let range = max - min;
2173 let t = Vec3::new(
2174 (self.x as f32 + 32768.0) / 65535.0,
2175 (self.y as f32 + 32768.0) / 65535.0,
2176 (self.z as f32 + 32768.0) / 65535.0,
2177 );
2178 min + t * range
2179 }
2180}
2181
2182#[derive(Debug, Clone)]
2183pub struct QuantizedBoneFrame {
2184 pub rotation: QuantizedQuat,
2185 pub translation: QuantizedVec3,
2186 pub scale: u16, }
2188
2189impl QuantizedBoneFrame {
2190 pub fn new(rotation: Quat, translation: Vec3, scale: f32, t_min: Vec3, t_max: Vec3) -> Self {
2191 Self {
2192 rotation: QuantizedQuat::from_quat(rotation),
2193 translation: QuantizedVec3::from_vec3_range(translation, t_min, t_max),
2194 scale: (scale.clamp(0.0, 2.0) * 32767.5) as u16,
2195 }
2196 }
2197 pub fn decode_scale(&self) -> f32 { self.scale as f32 / 32767.5 }
2198 pub fn byte_size() -> usize { 14 } }
2200
2201#[derive(Debug, Clone)]
2202pub struct QuantizedAnimStream {
2203 pub bone_count: u32,
2204 pub frame_count: u32,
2205 pub fps: f32,
2206 pub translation_min: Vec3,
2207 pub translation_max: Vec3,
2208 pub frames: Vec<Vec<QuantizedBoneFrame>>, pub bone_names: Vec<String>,
2210}
2211
2212impl QuantizedAnimStream {
2213 pub fn new(bone_count: u32, fps: f32) -> Self {
2214 Self { bone_count, frame_count: 0, fps, translation_min: Vec3::splat(-5.0),
2215 translation_max: Vec3::splat(5.0), frames: Vec::new(), bone_names: Vec::new() }
2216 }
2217 pub fn add_frame(&mut self, bones: Vec<QuantizedBoneFrame>) {
2218 self.frame_count += 1;
2219 self.frames.push(bones);
2220 }
2221 pub fn duration_s(&self) -> f32 { self.frame_count as f32 / self.fps.max(0.001) }
2222 pub fn total_bytes(&self) -> usize {
2223 self.frame_count as usize * self.bone_count as usize * QuantizedBoneFrame::byte_size()
2224 }
2225 pub fn sample_frame(&self, time_s: f32) -> Option<&Vec<QuantizedBoneFrame>> {
2226 let frame_idx = (time_s * self.fps) as usize;
2227 self.frames.get(frame_idx.min(self.frames.len().saturating_sub(1)))
2228 }
2229 pub fn compression_ratio_vs_f32(&self) -> f32 {
2230 let uncompressed = self.frame_count as usize * self.bone_count as usize * (4*4 + 3*4 + 4); let compressed = self.total_bytes();
2232 if compressed == 0 { return 1.0; }
2233 uncompressed as f32 / compressed as f32
2234 }
2235}
2236
2237#[derive(Debug, Clone, PartialEq)]
2242pub enum VfxEffectType {
2243 ParticleEmitter, RibbonTrail, MeshDecal, FluidSim,
2244 ClothSim, DestructionFx, GroundImpact, ExplosionRing,
2245 FirePlume, SmokeColumn, ElectricArc, PortalEffect,
2246}
2247
2248#[derive(Debug, Clone)]
2249pub struct VfxBinding {
2250 pub binding_id: u32,
2251 pub effect_type: VfxEffectType,
2252 pub attach_bone: String,
2253 pub local_offset: Vec3,
2254 pub local_rotation: Quat,
2255 pub scale: f32,
2256 pub start_time_s: f32,
2257 pub duration_s: f32,
2258 pub loop_count: i32, pub intensity: f32,
2260 pub color_tint: Vec4,
2261}
2262
2263impl VfxBinding {
2264 pub fn new(binding_id: u32, effect_type: VfxEffectType, attach_bone: &str, start_time_s: f32) -> Self {
2265 Self { binding_id, effect_type, attach_bone: attach_bone.to_string(),
2266 local_offset: Vec3::ZERO, local_rotation: Quat::IDENTITY,
2267 scale: 1.0, start_time_s, duration_s: 1.0, loop_count: 1,
2268 intensity: 1.0, color_tint: Vec4::new(1.0, 1.0, 1.0, 1.0) }
2269 }
2270 pub fn end_time_s(&self) -> f32 { self.start_time_s + self.duration_s }
2271 pub fn is_active_at(&self, time_s: f32) -> bool {
2272 time_s >= self.start_time_s && (self.loop_count < 0 || time_s <= self.end_time_s())
2273 }
2274 pub fn effect_type_str(&self) -> &'static str {
2275 match &self.effect_type {
2276 VfxEffectType::ParticleEmitter => "Particle Emitter",
2277 VfxEffectType::RibbonTrail => "Ribbon Trail",
2278 VfxEffectType::MeshDecal => "Mesh Decal",
2279 VfxEffectType::FluidSim => "Fluid Simulation",
2280 VfxEffectType::ClothSim => "Cloth Simulation",
2281 VfxEffectType::DestructionFx => "Destruction FX",
2282 VfxEffectType::GroundImpact => "Ground Impact",
2283 VfxEffectType::ExplosionRing => "Explosion Ring",
2284 VfxEffectType::FirePlume => "Fire Plume",
2285 VfxEffectType::SmokeColumn => "Smoke Column",
2286 VfxEffectType::ElectricArc => "Electric Arc",
2287 VfxEffectType::PortalEffect => "Portal Effect",
2288 }
2289 }
2290}
2291
2292#[derive(Debug, Clone, Default)]
2293pub struct VfxLayer {
2294 pub bindings: Vec<VfxBinding>,
2295 pub global_scale: f32,
2296 pub paused: bool,
2297}
2298
2299impl VfxLayer {
2300 pub fn new() -> Self { Self { bindings: Vec::new(), global_scale: 1.0, paused: false } }
2301 pub fn add_binding(&mut self, b: VfxBinding) { self.bindings.push(b); }
2302 pub fn active_at(&self, time_s: f32) -> Vec<&VfxBinding> {
2303 if self.paused { return Vec::new(); }
2304 self.bindings.iter().filter(|b| b.is_active_at(time_s)).collect()
2305 }
2306 pub fn total_bindings(&self) -> usize { self.bindings.len() }
2307 pub fn bindings_of_type(&self, effect_type: &VfxEffectType) -> Vec<&VfxBinding> {
2308 self.bindings.iter().filter(|b| &b.effect_type == effect_type).collect()
2309 }
2310}
2311
2312#[derive(Debug, Clone, PartialEq)]
2317pub enum RenderPassType {
2318 ZPrepass, Opaque, AlphaMask, Transparent, Decals,
2319 VolumetricFog, DeferredLighting, ShadowMap, PostProcess,
2320 UI, Debug, Custom(u8),
2321}
2322
2323#[derive(Debug, Clone)]
2324pub struct RenderPass {
2325 pub pass_type: RenderPassType,
2326 pub name: String,
2327 pub enabled: bool,
2328 pub clear_color: Vec4,
2329 pub clear_depth: bool,
2330 pub render_target: String,
2331 pub priority: i32,
2332}
2333
2334impl RenderPass {
2335 pub fn new(pass_type: RenderPassType, name: &str, priority: i32) -> Self {
2336 Self { pass_type, name: name.to_string(), enabled: true,
2337 clear_color: Vec4::new(0.0, 0.0, 0.0, 1.0),
2338 clear_depth: true, render_target: "backbuffer".to_string(), priority }
2339 }
2340 pub fn pass_type_str(&self) -> String {
2341 match &self.pass_type {
2342 RenderPassType::ZPrepass => "ZPrepass".to_string(),
2343 RenderPassType::Opaque => "Opaque".to_string(),
2344 RenderPassType::AlphaMask => "AlphaMask".to_string(),
2345 RenderPassType::Transparent => "Transparent".to_string(),
2346 RenderPassType::Decals => "Decals".to_string(),
2347 RenderPassType::VolumetricFog => "VolumetricFog".to_string(),
2348 RenderPassType::DeferredLighting => "DeferredLighting".to_string(),
2349 RenderPassType::ShadowMap => "ShadowMap".to_string(),
2350 RenderPassType::PostProcess => "PostProcess".to_string(),
2351 RenderPassType::UI => "UI".to_string(),
2352 RenderPassType::Debug => "Debug".to_string(),
2353 RenderPassType::Custom(n) => format!("Custom({})", n),
2354 }
2355 }
2356}
2357
2358#[derive(Debug, Clone, Default)]
2359pub struct RenderPassManager {
2360 pub passes: Vec<RenderPass>,
2361}
2362
2363impl RenderPassManager {
2364 pub fn new() -> Self { Self { passes: Vec::new() } }
2365 pub fn add_pass(&mut self, p: RenderPass) { self.passes.push(p); }
2366 pub fn default_pipeline() -> Self {
2367 let mut mgr = Self::new();
2368 mgr.add_pass(RenderPass::new(RenderPassType::ZPrepass, "Z Pre-pass", 0));
2369 mgr.add_pass(RenderPass::new(RenderPassType::ShadowMap, "Shadow Maps", 5));
2370 mgr.add_pass(RenderPass::new(RenderPassType::Opaque, "Opaque", 10));
2371 mgr.add_pass(RenderPass::new(RenderPassType::AlphaMask, "Alpha Mask", 15));
2372 mgr.add_pass(RenderPass::new(RenderPassType::DeferredLighting, "Deferred Lighting", 20));
2373 mgr.add_pass(RenderPass::new(RenderPassType::Decals, "Decals", 25));
2374 mgr.add_pass(RenderPass::new(RenderPassType::VolumetricFog, "Volumetric Fog", 30));
2375 mgr.add_pass(RenderPass::new(RenderPassType::Transparent, "Transparent", 35));
2376 mgr.add_pass(RenderPass::new(RenderPassType::PostProcess, "Post Process", 40));
2377 mgr.add_pass(RenderPass::new(RenderPassType::UI, "UI", 45));
2378 mgr
2379 }
2380 pub fn enabled_passes(&self) -> Vec<&RenderPass> {
2381 let mut passes: Vec<&RenderPass> = self.passes.iter().filter(|p| p.enabled).collect();
2382 passes.sort_by_key(|p| p.priority);
2383 passes
2384 }
2385 pub fn count(&self) -> usize { self.passes.len() }
2386}
2387
2388#[derive(Debug, Clone, PartialEq)]
2393pub enum SoundCueType {
2394 OneShot, Looping, Ambience, Dialogue, Music, Foley,
2395}
2396
2397#[derive(Debug, Clone)]
2398pub struct SoundCue {
2399 pub cue_id: u32,
2400 pub name: String,
2401 pub cue_type: SoundCueType,
2402 pub start_time_s: f32,
2403 pub duration_s: f32,
2404 pub volume: f32,
2405 pub pitch: f32,
2406 pub spatial: bool,
2407 pub position: Vec3,
2408 pub falloff_radius_m: f32,
2409 pub asset_path: String,
2410}
2411
2412impl SoundCue {
2413 pub fn new(cue_id: u32, name: &str, cue_type: SoundCueType, start_time_s: f32, duration_s: f32) -> Self {
2414 Self { cue_id, name: name.to_string(), cue_type, start_time_s, duration_s,
2415 volume: 1.0, pitch: 1.0, spatial: false, position: Vec3::ZERO,
2416 falloff_radius_m: 20.0, asset_path: String::new() }
2417 }
2418 pub fn end_time_s(&self) -> f32 { self.start_time_s + self.duration_s }
2419 pub fn is_active_at(&self, time_s: f32) -> bool {
2420 time_s >= self.start_time_s && time_s <= self.end_time_s()
2421 }
2422 pub fn volume_at_distance(&self, distance_m: f32) -> f32 {
2423 if !self.spatial || distance_m <= 0.0 { return self.volume; }
2424 let ratio = (1.0 - distance_m / self.falloff_radius_m.max(0.001)).max(0.0);
2425 self.volume * ratio * ratio }
2427}
2428
2429#[derive(Debug, Clone, Default)]
2430pub struct SoundTrack {
2431 pub track_name: String,
2432 pub cues: Vec<SoundCue>,
2433 pub muted: bool,
2434 pub solo: bool,
2435 pub master_volume: f32,
2436}
2437
2438impl SoundTrack {
2439 pub fn new(track_name: &str) -> Self {
2440 Self { track_name: track_name.to_string(), cues: Vec::new(),
2441 muted: false, solo: false, master_volume: 1.0 }
2442 }
2443 pub fn add_cue(&mut self, c: SoundCue) { self.cues.push(c); }
2444 pub fn cues_at_time(&self, time_s: f32) -> Vec<&SoundCue> {
2445 if self.muted { return Vec::new(); }
2446 self.cues.iter().filter(|c| c.is_active_at(time_s)).collect()
2447 }
2448 pub fn duration_s(&self) -> f32 {
2449 self.cues.iter().map(|c| c.end_time_s()).fold(0.0_f32, f32::max)
2450 }
2451 pub fn cue_count(&self) -> usize { self.cues.len() }
2452}
2453
2454#[derive(Debug, Clone, PartialEq)]
2459pub enum ExportFormat {
2460 Png, Exr, Jpeg, Tiff, DpxRaw,
2461}
2462
2463#[derive(Debug, Clone, PartialEq)]
2464pub enum ExportColorspace {
2465 Srgb, LinearRec709, Aces, DciP3, Rec2020,
2466}
2467
2468#[derive(Debug, Clone)]
2469pub struct RenderSequenceExport {
2470 pub output_path: String,
2471 pub format: ExportFormat,
2472 pub colorspace: ExportColorspace,
2473 pub width: u32,
2474 pub height: u32,
2475 pub start_frame: u32,
2476 pub end_frame: u32,
2477 pub fps: f32,
2478 pub bit_depth: u8,
2479 pub include_alpha: bool,
2480 pub denoise: bool,
2481 pub aov_outputs: Vec<String>,
2482}
2483
2484impl RenderSequenceExport {
2485 pub fn new(output_path: &str, width: u32, height: u32, fps: f32) -> Self {
2486 Self { output_path: output_path.to_string(), format: ExportFormat::Exr,
2487 colorspace: ExportColorspace::LinearRec709,
2488 width, height, start_frame: 1, end_frame: 100, fps,
2489 bit_depth: 16, include_alpha: true, denoise: false, aov_outputs: Vec::new() }
2490 }
2491 pub fn frame_count(&self) -> u32 { self.end_frame - self.start_frame + 1 }
2492 pub fn duration_s(&self) -> f32 { self.frame_count() as f32 / self.fps.max(0.001) }
2493 pub fn bytes_per_frame(&self) -> u64 {
2494 let channels = if self.include_alpha { 4 } else { 3 };
2495 let bits_per_channel = self.bit_depth as u64;
2496 self.width as u64 * self.height as u64 * channels * bits_per_channel / 8
2497 }
2498 pub fn total_bytes(&self) -> u64 {
2499 self.bytes_per_frame() * self.frame_count() as u64
2500 }
2501 pub fn total_gb(&self) -> f64 { self.total_bytes() as f64 / (1024.0 * 1024.0 * 1024.0) }
2502 pub fn format_str(&self) -> &'static str {
2503 match &self.format {
2504 ExportFormat::Png => "PNG", ExportFormat::Exr => "EXR",
2505 ExportFormat::Jpeg => "JPEG", ExportFormat::Tiff => "TIFF",
2506 ExportFormat::DpxRaw => "DPX",
2507 }
2508 }
2509 pub fn frame_filename(&self, frame: u32) -> String {
2510 let ext = self.format_str().to_lowercase();
2511 format!("{}/frame_{:06}.{}", self.output_path, frame, ext)
2512 }
2513 pub fn add_aov(&mut self, aov_name: &str) { self.aov_outputs.push(aov_name.to_string()); }
2514}
2515
2516#[derive(Debug, Clone, PartialEq)]
2521pub enum DirectorState {
2522 Idle, Playing, Paused, Scrubbing, Recording,
2523}
2524
2525#[derive(Debug, Clone)]
2526pub struct CutsceneDirector {
2527 pub cutscene_id: String,
2528 pub state: DirectorState,
2529 pub current_time_s: f32,
2530 pub total_duration_s: f32,
2531 pub playback_speed: f32,
2532 pub sound_tracks: Vec<SoundTrack>,
2533 pub vfx_layer: VfxLayer,
2534 pub post_process: PostProcessStack,
2535 pub render_pass_mgr: RenderPassManager,
2536 pub usd_stage: Option<UsdStage>,
2537 pub export_settings: Option<RenderSequenceExport>,
2538 pub markers: Vec<(f32, String)>, }
2540
2541impl CutsceneDirector {
2542 pub fn new(cutscene_id: &str, total_duration_s: f32) -> Self {
2543 Self { cutscene_id: cutscene_id.to_string(), state: DirectorState::Idle,
2544 current_time_s: 0.0, total_duration_s, playback_speed: 1.0,
2545 sound_tracks: Vec::new(), vfx_layer: VfxLayer::new(),
2546 post_process: PostProcessStack::new(),
2547 render_pass_mgr: RenderPassManager::default_pipeline(),
2548 usd_stage: None, export_settings: None, markers: Vec::new() }
2549 }
2550 pub fn play(&mut self) { self.state = DirectorState::Playing; }
2551 pub fn pause(&mut self) { self.state = DirectorState::Paused; }
2552 pub fn stop(&mut self) { self.state = DirectorState::Idle; self.current_time_s = 0.0; }
2553 pub fn seek(&mut self, time_s: f32) {
2554 self.current_time_s = time_s.clamp(0.0, self.total_duration_s);
2555 self.state = DirectorState::Scrubbing;
2556 }
2557 pub fn add_sound_track(&mut self, t: SoundTrack) { self.sound_tracks.push(t); }
2558 pub fn add_marker(&mut self, time_s: f32, label: &str) {
2559 self.markers.push((time_s, label.to_string()));
2560 }
2561 pub fn advance(&mut self, delta_s: f32) {
2562 if self.state == DirectorState::Playing {
2563 self.current_time_s = (self.current_time_s + delta_s * self.playback_speed)
2564 .min(self.total_duration_s);
2565 if self.current_time_s >= self.total_duration_s {
2566 self.state = DirectorState::Idle;
2567 }
2568 }
2569 }
2570 pub fn active_sound_cues(&self) -> Vec<&SoundCue> {
2571 self.sound_tracks.iter()
2572 .flat_map(|t| t.cues_at_time(self.current_time_s))
2573 .collect()
2574 }
2575 pub fn active_vfx(&self) -> Vec<&VfxBinding> {
2576 self.vfx_layer.active_at(self.current_time_s)
2577 }
2578 pub fn progress_pct(&self) -> f32 {
2579 if self.total_duration_s < 0.001 { return 0.0; }
2580 self.current_time_s / self.total_duration_s * 100.0
2581 }
2582 pub fn markers_in_range(&self, start_s: f32, end_s: f32) -> Vec<&(f32, String)> {
2583 self.markers.iter().filter(|(t, _)| *t >= start_s && *t <= end_s).collect()
2584 }
2585 pub fn render_pass_count(&self) -> usize { self.render_pass_mgr.count() }
2586}
2587
2588#[derive(Debug, Clone)]
2593pub struct BoneMapping {
2594 pub source_bone: String,
2595 pub target_bone: String,
2596 pub rotation_offset: Quat,
2597 pub scale_factor: f32,
2598}
2599
2600impl BoneMapping {
2601 pub fn new(source: &str, target: &str) -> Self {
2602 Self { source_bone: source.to_string(), target_bone: target.to_string(),
2603 rotation_offset: Quat::IDENTITY, scale_factor: 1.0 }
2604 }
2605 pub fn with_rotation_offset(mut self, rot: Quat) -> Self { self.rotation_offset = rot; self }
2606 pub fn with_scale(mut self, scale: f32) -> Self { self.scale_factor = scale; self }
2607}
2608
2609#[derive(Debug, Clone, Default)]
2610pub struct RetargetProfile {
2611 pub source_skeleton: String,
2612 pub target_skeleton: String,
2613 pub bone_mappings: Vec<BoneMapping>,
2614 pub unmapped_bone_policy: String, }
2616
2617impl RetargetProfile {
2618 pub fn new(source: &str, target: &str) -> Self {
2619 Self { source_skeleton: source.to_string(), target_skeleton: target.to_string(),
2620 bone_mappings: Vec::new(), unmapped_bone_policy: "t-pose".to_string() }
2621 }
2622 pub fn add_mapping(&mut self, mapping: BoneMapping) { self.bone_mappings.push(mapping); }
2623 pub fn find_mapping(&self, source_bone: &str) -> Option<&BoneMapping> {
2624 self.bone_mappings.iter().find(|m| m.source_bone == source_bone)
2625 }
2626 pub fn mapping_count(&self) -> usize { self.bone_mappings.len() }
2627 pub fn humanoid_biped() -> Self {
2628 let mut p = Self::new("source_biped", "target_biped");
2629 let bone_pairs = [
2630 ("Hips", "pelvis"), ("Spine", "spine_01"), ("Spine1", "spine_02"),
2631 ("Spine2", "spine_03"), ("Neck", "neck_01"), ("Head", "head"),
2632 ("LeftShoulder", "clavicle_l"), ("LeftArm", "upperarm_l"),
2633 ("LeftForeArm", "lowerarm_l"), ("LeftHand", "hand_l"),
2634 ("RightShoulder", "clavicle_r"), ("RightArm", "upperarm_r"),
2635 ("RightForeArm", "lowerarm_r"), ("RightHand", "hand_r"),
2636 ("LeftUpLeg", "thigh_l"), ("LeftLeg", "calf_l"), ("LeftFoot", "foot_l"),
2637 ("RightUpLeg", "thigh_r"), ("RightLeg", "calf_r"), ("RightFoot", "foot_r"),
2638 ];
2639 for (src, tgt) in &bone_pairs {
2640 p.add_mapping(BoneMapping::new(src, tgt));
2641 }
2642 p
2643 }
2644}
2645
2646pub const ARKit_BLEND_SHAPE_NAMES: [&str; 52] = [
2651 "eyeBlinkLeft", "eyeLookDownLeft", "eyeLookInLeft", "eyeLookOutLeft", "eyeLookUpLeft",
2652 "eyeSquintLeft", "eyeWideLeft", "eyeBlinkRight", "eyeLookDownRight", "eyeLookInRight",
2653 "eyeLookOutRight", "eyeLookUpRight", "eyeSquintRight", "eyeWideRight",
2654 "jawForward", "jawLeft", "jawRight", "jawOpen",
2655 "mouthClose", "mouthFunnel", "mouthPucker", "mouthLeft", "mouthRight",
2656 "mouthSmileLeft", "mouthSmileRight", "mouthFrownLeft", "mouthFrownRight",
2657 "mouthDimpleLeft", "mouthDimpleRight", "mouthStretchLeft", "mouthStretchRight",
2658 "mouthRollLower", "mouthRollUpper", "mouthShrugLower", "mouthShrugUpper",
2659 "mouthPressLeft", "mouthPressRight", "mouthLowerDownLeft", "mouthLowerDownRight",
2660 "mouthUpperUpLeft", "mouthUpperUpRight", "browDownLeft", "browDownRight",
2661 "browInnerUp", "browOuterUpLeft", "browOuterUpRight",
2662 "cheekPuff", "cheekSquintLeft", "cheekSquintRight",
2663 "noseSneerLeft", "noseSneerRight", "tongueOut",
2664];
2665
2666#[derive(Debug, Clone)]
2667pub struct FacialCaptureFrame {
2668 pub time_s: f64,
2669 pub blend_shapes: [f32; 52],
2670 pub head_rotation: Quat,
2671 pub left_eye_rotation: Quat,
2672 pub right_eye_rotation: Quat,
2673 pub confidence: f32,
2674}
2675
2676impl FacialCaptureFrame {
2677 pub fn new(time_s: f64) -> Self {
2678 Self { time_s, blend_shapes: [0.0; 52], head_rotation: Quat::IDENTITY,
2679 left_eye_rotation: Quat::IDENTITY, right_eye_rotation: Quat::IDENTITY,
2680 confidence: 1.0 }
2681 }
2682 pub fn set_blend_shape(&mut self, name: &str, value: f32) {
2683 if let Some(idx) = ARKit_BLEND_SHAPE_NAMES.iter().position(|&n| n == name) {
2684 self.blend_shapes[idx] = value.clamp(0.0, 1.0);
2685 }
2686 }
2687 pub fn get_blend_shape(&self, name: &str) -> f32 {
2688 ARKit_BLEND_SHAPE_NAMES.iter().position(|&n| n == name)
2689 .map(|idx| self.blend_shapes[idx])
2690 .unwrap_or(0.0)
2691 }
2692 pub fn jaw_open(&self) -> f32 { self.blend_shapes[17] }
2693 pub fn mouth_smile_avg(&self) -> f32 { (self.blend_shapes[23] + self.blend_shapes[24]) / 2.0 }
2694 pub fn eye_blink_avg(&self) -> f32 { (self.blend_shapes[0] + self.blend_shapes[7]) / 2.0 }
2695 pub fn brow_raise_avg(&self) -> f32 { self.blend_shapes[43] }
2696 pub fn detected_expression(&self) -> &'static str {
2697 if self.mouth_smile_avg() > 0.5 { return "happy"; }
2698 if self.blend_shapes[25] > 0.4 || self.blend_shapes[26] > 0.4 { return "sad"; }
2699 if self.blend_shapes[40] > 0.3 || self.blend_shapes[41] > 0.3 { return "angry"; }
2700 if self.brow_raise_avg() > 0.4 && self.jaw_open() > 0.3 { return "surprised"; }
2701 if self.eye_blink_avg() > 0.7 { return "blinking"; }
2702 "neutral"
2703 }
2704}
2705
2706#[derive(Debug, Clone, Default)]
2707pub struct FacialCaptureTake {
2708 pub take_id: String,
2709 pub actor_name: String,
2710 pub frames: Vec<FacialCaptureFrame>,
2711 pub fps: f32,
2712 pub camera_model: String,
2713}
2714
2715impl FacialCaptureTake {
2716 pub fn new(take_id: &str, actor_name: &str, fps: f32) -> Self {
2717 Self { take_id: take_id.to_string(), actor_name: actor_name.to_string(),
2718 frames: Vec::new(), fps, camera_model: "iPhone".to_string() }
2719 }
2720 pub fn add_frame(&mut self, f: FacialCaptureFrame) { self.frames.push(f); }
2721 pub fn frame_count(&self) -> usize { self.frames.len() }
2722 pub fn duration_s(&self) -> f64 {
2723 if self.frames.is_empty() { return 0.0; }
2724 self.frames[self.frames.len()-1].time_s - self.frames[0].time_s
2725 }
2726 pub fn frame_at_time(&self, time_s: f64) -> Option<&FacialCaptureFrame> {
2727 if self.frames.is_empty() { return None; }
2728 let idx = self.frames.iter().enumerate()
2729 .min_by(|(_, a), (_, b)| {
2730 (a.time_s - time_s).abs().partial_cmp(&(b.time_s - time_s).abs())
2731 .unwrap_or(std::cmp::Ordering::Equal)
2732 }).map(|(i, _)| i);
2733 idx.map(|i| &self.frames[i])
2734 }
2735 pub fn average_confidence(&self) -> f32 {
2736 if self.frames.is_empty() { return 0.0; }
2737 self.frames.iter().map(|f| f.confidence).sum::<f32>() / self.frames.len() as f32
2738 }
2739 pub fn low_confidence_frames(&self, threshold: f32) -> Vec<&FacialCaptureFrame> {
2740 self.frames.iter().filter(|f| f.confidence < threshold).collect()
2741 }
2742 pub fn smooth_blend_shapes(&mut self, window_size: usize) {
2743 if self.frames.len() < window_size { return; }
2744 let n = self.frames.len();
2745 let orig: Vec<[f32; 52]> = self.frames.iter().map(|f| f.blend_shapes).collect();
2746 for i in 0..n {
2747 let start = i.saturating_sub(window_size / 2);
2748 let end = (i + window_size / 2 + 1).min(n);
2749 let count = (end - start) as f32;
2750 for j in 0..52 {
2751 let avg = orig[start..end].iter().map(|bs| bs[j]).sum::<f32>() / count;
2752 self.frames[i].blend_shapes[j] = avg;
2753 }
2754 }
2755 }
2756 pub fn dominant_expression_histogram(&self) -> HashMap<String, usize> {
2757 let mut hist: HashMap<String, usize> = HashMap::new();
2758 for f in &self.frames {
2759 *hist.entry(f.detected_expression().to_string()).or_insert(0) += 1;
2760 }
2761 hist
2762 }
2763}
2764
2765#[derive(Debug, Clone)]
2770pub struct ExtLightKeyframe {
2771 pub time_s: f32,
2772 pub color: Vec3,
2773 pub intensity: f32,
2774 pub radius_or_angle: f32,
2775 pub cast_shadows: bool,
2776 pub shadow_softness: f32,
2777}
2778
2779impl ExtLightKeyframe {
2780 pub fn new(time_s: f32, color: Vec3, intensity: f32) -> Self {
2781 Self { time_s, color, intensity, radius_or_angle: 5.0, cast_shadows: true, shadow_softness: 1.0 }
2782 }
2783 pub fn lerp(&self, other: &ExtLightKeyframe, t: f32) -> ExtLightKeyframe {
2784 let t = t.clamp(0.0, 1.0);
2785 ExtLightKeyframe {
2786 time_s: self.time_s + (other.time_s - self.time_s) * t,
2787 color: self.color.lerp(other.color, t),
2788 intensity: self.intensity + (other.intensity - self.intensity) * t,
2789 radius_or_angle: self.radius_or_angle + (other.radius_or_angle - self.radius_or_angle) * t,
2790 cast_shadows: if t < 0.5 { self.cast_shadows } else { other.cast_shadows },
2791 shadow_softness: self.shadow_softness + (other.shadow_softness - self.shadow_softness) * t,
2792 }
2793 }
2794}
2795
2796#[derive(Debug, Clone, Default)]
2797pub struct LightAnimTrack {
2798 pub light_name: String,
2799 pub keyframes: Vec<ExtLightKeyframe>,
2800}
2801
2802impl LightAnimTrack {
2803 pub fn new(light_name: &str) -> Self { Self { light_name: light_name.to_string(), keyframes: Vec::new() } }
2804 pub fn add_keyframe(&mut self, kf: ExtLightKeyframe) {
2805 let idx = self.keyframes.partition_point(|k| k.time_s < kf.time_s);
2806 self.keyframes.insert(idx, kf);
2807 }
2808 pub fn sample(&self, time_s: f32) -> Option<ExtLightKeyframe> {
2809 if self.keyframes.is_empty() { return None; }
2810 if self.keyframes.len() == 1 { return Some(self.keyframes[0].clone()); }
2811 let idx = self.keyframes.partition_point(|k| k.time_s <= time_s);
2812 if idx == 0 { return Some(self.keyframes[0].clone()); }
2813 if idx >= self.keyframes.len() { return Some(self.keyframes[self.keyframes.len()-1].clone()); }
2814 let prev = &self.keyframes[idx-1];
2815 let next = &self.keyframes[idx];
2816 let span = next.time_s - prev.time_s;
2817 let t = if span > 0.001 { (time_s - prev.time_s) / span } else { 0.0 };
2818 Some(prev.lerp(next, t))
2819 }
2820 pub fn duration_s(&self) -> f32 {
2821 if self.keyframes.is_empty() { return 0.0; }
2822 self.keyframes[self.keyframes.len()-1].time_s
2823 }
2824 pub fn keyframe_count(&self) -> usize { self.keyframes.len() }
2825}
2826
2827pub fn run_post_process_tests() {
2832 let cinema = PostProcessStack::cinema_preset();
2833 assert!(cinema.effect_count() > 0);
2834 assert!(cinema.enabled_effects().len() > 0);
2835
2836 let horror = PostProcessStack::horror_preset();
2837 assert!(horror.saturation < 0.5);
2838 assert!(horror.contrast > 1.2);
2839
2840 let effect = PostProcessEffect::bloom(0.9, 0.7, 0.5);
2841 assert_eq!(effect.effect_type_str(), "Bloom");
2842 assert!(effect.params.contains_key("threshold"));
2843}
2844
2845pub fn run_usd_stage_tests() {
2846 let mut stage = UsdStage::new("/tmp/test_scene.usda");
2847 stage.time_codes_per_second = 24.0;
2848 stage.end_time_code = 240.0;
2849
2850 let mut root = UsdPrim::new("/root", UsdPrimType::Xform);
2851 root.set_attr("xformOp:translate", "(0, 0, 0)");
2852 stage.add_prim(root);
2853
2854 let cam = UsdPrim::new("/root/Camera", UsdPrimType::Camera);
2855 stage.add_prim(cam);
2856
2857 assert_eq!(stage.prim_count(), 2);
2858 assert_eq!(stage.duration_seconds(), 10.0);
2859 assert_eq!(stage.camera_prims().len(), 1);
2860 let header = stage.export_usda_header();
2861 assert!(header.contains("usda 1.0"));
2862}
2863
2864pub fn run_alembic_tests() {
2865 let mut archive = AlembicArchive::new("crowd_sim.abc", 24.0);
2866 archive.end_time = 4.0;
2867
2868 let mut crowd = AlembicObject::new("crowd_particles", "Points");
2869 for i in 0..10 {
2870 let mut sample = AlembicSample::new(i as f64 * (1.0 / 24.0));
2871 for j in 0..100 {
2872 let pos = Vec3::new(j as f32 * 0.1, 0.0, i as f32 * 0.05);
2873 let vel = Vec3::new(0.5, 0.0, 0.0);
2874 sample.add_point(pos, vel);
2875 }
2876 crowd.add_sample(sample);
2877 }
2878 archive.add_object(crowd);
2879
2880 assert_eq!(archive.object_count(), 1);
2881 assert_eq!(archive.total_samples(), 10);
2882 let obj = archive.find_object("crowd_particles").unwrap();
2883 assert_eq!(obj.sample_count(), 10);
2884 let sample = obj.sample_at_time(0.0).unwrap();
2885 let (bb_min, bb_max) = sample.bounding_box();
2886 assert!(bb_max.x > bb_min.x);
2887}
2888
2889pub fn run_quantized_anim_tests() {
2890 let q = Quat::from_rotation_y(0.5);
2891 let qq = QuantizedQuat::from_quat(q);
2892 let recovered = qq.to_quat();
2893 let dot = (q.x * recovered.x + q.y * recovered.y + q.z * recovered.z + q.w * recovered.w).abs();
2894 assert!(dot > 0.99, "dot={}", dot);
2895
2896 let mut stream = QuantizedAnimStream::new(3, 24.0);
2897 for _ in 0..24 {
2898 let bones = vec![
2899 QuantizedBoneFrame::new(Quat::IDENTITY, Vec3::ZERO, 1.0, Vec3::splat(-5.0), Vec3::splat(5.0)),
2900 QuantizedBoneFrame::new(Quat::from_rotation_x(0.1), Vec3::new(0.0, 1.0, 0.0), 1.0, Vec3::splat(-5.0), Vec3::splat(5.0)),
2901 QuantizedBoneFrame::new(Quat::from_rotation_z(0.2), Vec3::ZERO, 1.0, Vec3::splat(-5.0), Vec3::splat(5.0)),
2902 ];
2903 stream.add_frame(bones);
2904 }
2905 assert_eq!(stream.frame_count, 24);
2906 assert!((stream.duration_s() - 1.0).abs() < 0.001);
2907 let ratio = stream.compression_ratio_vs_f32();
2908 assert!(ratio > 1.0, "compression ratio should be > 1: {}", ratio);
2909}
2910
2911pub fn run_vfx_tests() {
2912 let mut layer = VfxLayer::new();
2913 layer.add_binding(VfxBinding::new(1, VfxEffectType::ExplosionRing, "root", 0.5));
2914 layer.add_binding(VfxBinding::new(2, VfxEffectType::SmokeColumn, "root", 0.5));
2915 layer.add_binding(VfxBinding::new(3, VfxEffectType::ParticleEmitter, "Spine", 0.0));
2916
2917 let active = layer.active_at(0.7);
2918 assert!(!active.is_empty());
2919 let explosions = layer.bindings_of_type(&VfxEffectType::ExplosionRing);
2920 assert_eq!(explosions.len(), 1);
2921}
2922
2923pub fn run_sound_cue_tests() {
2924 let mut track = SoundTrack::new("sfx_track");
2925 track.add_cue(SoundCue::new(1, "footstep", SoundCueType::Foley, 0.0, 0.3));
2926 track.add_cue(SoundCue::new(2, "explosion", SoundCueType::OneShot, 1.5, 2.0));
2927 track.add_cue(SoundCue::new(3, "ambient_wind", SoundCueType::Looping, 0.0, 10.0));
2928
2929 let active_at_0 = track.cues_at_time(0.1);
2930 assert!(!active_at_0.is_empty());
2931 let active_at_2 = track.cues_at_time(2.0);
2932 assert!(active_at_2.iter().any(|c| c.name == "explosion"));
2933 assert!((track.duration_s() - 10.0).abs() < 0.01);
2934}
2935
2936pub fn run_render_export_tests() {
2937 let mut export = RenderSequenceExport::new("/renders/shot01", 1920, 1080, 24.0);
2938 export.start_frame = 1;
2939 export.end_frame = 240;
2940 export.add_aov("beauty");
2941 export.add_aov("normal");
2942 export.add_aov("depth");
2943
2944 assert_eq!(export.frame_count(), 240);
2945 assert!((export.duration_s() - 10.0).abs() < 0.01);
2946 assert!(export.total_bytes() > 0);
2947 let filename = export.frame_filename(1);
2948 assert!(filename.contains("000001"));
2949 assert_eq!(export.aov_outputs.len(), 3);
2950}
2951
2952pub fn run_facial_capture_tests() {
2953 let mut take = FacialCaptureTake::new("take_001", "John", 30.0);
2954
2955 for i in 0..30 {
2956 let mut frame = FacialCaptureFrame::new(i as f64 / 30.0);
2957 frame.set_blend_shape("mouthSmileLeft", if i > 15 { 0.8 } else { 0.1 });
2958 frame.set_blend_shape("mouthSmileRight", if i > 15 { 0.7 } else { 0.1 });
2959 frame.confidence = if i == 5 { 0.3 } else { 0.95 };
2960 take.add_frame(frame);
2961 }
2962
2963 assert_eq!(take.frame_count(), 30);
2964 let low_conf = take.low_confidence_frames(0.5);
2965 assert_eq!(low_conf.len(), 1);
2966 let hist = take.dominant_expression_histogram();
2967 assert!(!hist.is_empty());
2968 take.smooth_blend_shapes(3);
2969}
2970
2971pub fn run_light_anim_tests() {
2972 let mut track = LightAnimTrack::new("sun_light");
2973 track.add_keyframe(ExtLightKeyframe::new(0.0, Vec3::new(1.0, 0.8, 0.6), 100000.0));
2974 track.add_keyframe(ExtLightKeyframe::new(5.0, Vec3::new(0.8, 0.4, 0.2), 50000.0));
2975 track.add_keyframe(ExtLightKeyframe::new(10.0, Vec3::new(0.1, 0.1, 0.2), 10000.0));
2976
2977 assert_eq!(track.keyframe_count(), 3);
2978 let sample = track.sample(2.5).unwrap();
2979 assert!((sample.intensity - 75000.0).abs() < 1000.0);
2980 assert!((track.duration_s() - 10.0).abs() < 0.01);
2981}
2982
2983pub fn run_director_tests() {
2984 let mut director = CutsceneDirector::new("cutscene_01", 30.0);
2985 director.add_marker(5.0, "action_start");
2986 director.add_marker(20.0, "climax");
2987 director.add_marker(28.0, "fade_out");
2988
2989 let mut sound_track = SoundTrack::new("music");
2990 sound_track.add_cue(SoundCue::new(1, "music_main", SoundCueType::Music, 0.0, 30.0));
2991 director.add_sound_track(sound_track);
2992
2993 director.vfx_layer.add_binding(VfxBinding::new(1, VfxEffectType::FirePlume, "root", 10.0));
2994
2995 director.play();
2996 director.advance(5.0);
2997 assert!((director.current_time_s - 5.0).abs() < 0.01);
2998 assert!((director.progress_pct() - 16.67).abs() < 0.1);
2999
3000 let markers = director.markers_in_range(0.0, 10.0);
3001 assert_eq!(markers.len(), 1);
3002
3003 director.seek(0.0);
3004 assert!((director.current_time_s - 0.0).abs() < 0.01);
3005 assert!(director.render_pass_count() >= 8);
3006}
3007
3008pub fn run_retarget_tests() {
3009 let profile = RetargetProfile::humanoid_biped();
3010 assert!(profile.mapping_count() >= 20);
3011 let mapping = profile.find_mapping("Hips");
3012 assert!(mapping.is_some());
3013 assert_eq!(mapping.unwrap().target_bone, "pelvis");
3014}
3015
3016pub fn run_all_cutscene_post_tests() {
3017 run_post_process_tests();
3018 run_usd_stage_tests();
3019 run_alembic_tests();
3020 run_quantized_anim_tests();
3021 run_vfx_tests();
3022 run_sound_cue_tests();
3023 run_render_export_tests();
3024 run_facial_capture_tests();
3025 run_light_anim_tests();
3026 run_director_tests();
3027 run_retarget_tests();
3028}
3029
3030pub const CUTSCENE_MAX_TRACKS: usize = 64;
3031pub const CUTSCENE_MAX_SOUND_CUES: usize = 256;
3032pub const FACIAL_CAPTURE_BLEND_SHAPES: usize = 52;
3033pub const USD_MAX_PRIMS: usize = 100_000;
3034pub const ALEMBIC_MAX_SAMPLES_PER_OBJECT: usize = 10_000;
3035pub const RENDER_MAX_AOVS: usize = 32;
3036
3037pub fn cutscene_importer_info_extended() -> HashMap<String, String> {
3038 let mut info = HashMap::new();
3039 info.insert("module".to_string(), "cutscene_importer".to_string());
3040 info.insert("version".to_string(), "2.0.0".to_string());
3041 info.insert("blend_shapes".to_string(), FACIAL_CAPTURE_BLEND_SHAPES.to_string());
3042 info.insert("max_tracks".to_string(), CUTSCENE_MAX_TRACKS.to_string());
3043 info.insert("usd_support".to_string(), "USDA 1.0".to_string());
3044 info.insert("alembic_support".to_string(), "Alembic 1.7+".to_string());
3045 info.insert("quantization".to_string(), "16-bit quaternion/translation".to_string());
3046 info
3047}
3048
3049
3050#[derive(Debug, Clone)]
3055pub struct CameraAperture {
3056 pub f_stop: f32,
3057 pub focal_length_mm: f32,
3058 pub sensor_width_mm: f32,
3059 pub sensor_height_mm: f32,
3060 pub focus_distance: f32,
3061 pub dof_enabled: bool,
3062 pub bokeh_shape_sides: u32,
3063}
3064
3065impl CameraAperture {
3066 pub fn new_cinema() -> Self {
3067 CameraAperture {
3068 f_stop: 2.8,
3069 focal_length_mm: 35.0,
3070 sensor_width_mm: 36.0,
3071 sensor_height_mm: 24.0,
3072 focus_distance: 5.0,
3073 dof_enabled: true,
3074 bokeh_shape_sides: 6,
3075 }
3076 }
3077
3078 pub fn field_of_view_horizontal(&self) -> f32 {
3079 2.0 * ((self.sensor_width_mm / (2.0 * self.focal_length_mm)).atan())
3080 }
3081
3082 pub fn field_of_view_vertical(&self) -> f32 {
3083 2.0 * ((self.sensor_height_mm / (2.0 * self.focal_length_mm)).atan())
3084 }
3085
3086 pub fn circle_of_confusion(&self, subject_distance: f32) -> f32 {
3087 let magnification = self.focal_length_mm / (subject_distance * 1000.0 - self.focal_length_mm);
3088 magnification * self.focal_length_mm / self.f_stop
3089 }
3090
3091 pub fn depth_of_field_near(&self) -> f32 {
3092 let fd = self.focus_distance * 1000.0;
3093 let coc = 0.03;
3094 let hyperfocal = (self.focal_length_mm * self.focal_length_mm) / (self.f_stop * coc) + self.focal_length_mm;
3095 let near = fd * (hyperfocal - self.focal_length_mm) / (hyperfocal + fd - 2.0 * self.focal_length_mm);
3096 near / 1000.0
3097 }
3098
3099 pub fn depth_of_field_far(&self) -> f32 {
3100 let fd = self.focus_distance * 1000.0;
3101 let coc = 0.03;
3102 let hyperfocal = (self.focal_length_mm * self.focal_length_mm) / (self.f_stop * coc) + self.focal_length_mm;
3103 let far = fd * (hyperfocal - self.focal_length_mm) / (hyperfocal - fd);
3104 if far < 0.0 { f32::INFINITY } else { far / 1000.0 }
3105 }
3106}
3107
3108#[derive(Debug, Clone)]
3109pub struct CameraMotionBlur {
3110 pub shutter_angle_degrees: f32,
3111 pub sample_count: u32,
3112 pub enabled: bool,
3113}
3114
3115impl CameraMotionBlur {
3116 pub fn new() -> Self {
3117 CameraMotionBlur { shutter_angle_degrees: 180.0, sample_count: 16, enabled: true }
3118 }
3119
3120 pub fn shutter_speed_fraction(&self, fps: f32) -> f32 {
3121 fps / (360.0 / self.shutter_angle_degrees)
3122 }
3123}
3124
3125#[derive(Debug, Clone)]
3126pub struct CameraLensFlare {
3127 pub enabled: bool,
3128 pub intensity: f32,
3129 pub streak_count: u32,
3130 pub streak_rotation_deg: f32,
3131 pub ghost_count: u32,
3132 pub halo_radius: f32,
3133 pub dirt_texture_intensity: f32,
3134}
3135
3136impl Default for CameraLensFlare {
3137 fn default() -> Self {
3138 CameraLensFlare {
3139 enabled: false,
3140 intensity: 0.5,
3141 streak_count: 8,
3142 streak_rotation_deg: 45.0,
3143 ghost_count: 4,
3144 halo_radius: 0.15,
3145 dirt_texture_intensity: 0.2,
3146 }
3147 }
3148}
3149
3150#[derive(Debug, Clone)]
3151pub struct CameraVignette {
3152 pub enabled: bool,
3153 pub intensity: f32,
3154 pub smoothness: f32,
3155 pub roundness: f32,
3156 pub center: Vec2,
3157}
3158
3159impl Default for CameraVignette {
3160 fn default() -> Self {
3161 CameraVignette {
3162 enabled: false,
3163 intensity: 0.4,
3164 smoothness: 0.4,
3165 roundness: 1.0,
3166 center: Vec2::new(0.5, 0.5),
3167 }
3168 }
3169}
3170
3171#[derive(Debug, Clone)]
3172pub struct ChromaticAberration {
3173 pub enabled: bool,
3174 pub intensity: f32,
3175 pub fast_mode: bool,
3176}
3177
3178impl Default for ChromaticAberration {
3179 fn default() -> Self {
3180 ChromaticAberration { enabled: false, intensity: 0.05, fast_mode: false }
3181 }
3182}
3183
3184#[derive(Debug, Clone)]
3185pub struct CinematicCamera {
3186 pub id: u32,
3187 pub name: String,
3188 pub position: Vec3,
3189 pub rotation: Quat,
3190 pub aperture: CameraAperture,
3191 pub motion_blur: CameraMotionBlur,
3192 pub lens_flare: CameraLensFlare,
3193 pub vignette: CameraVignette,
3194 pub chromatic_aberration: ChromaticAberration,
3195 pub near_clip: f32,
3196 pub far_clip: f32,
3197 pub aspect_ratio: f32,
3198}
3199
3200impl CinematicCamera {
3201 pub fn new(id: u32, name: &str) -> Self {
3202 CinematicCamera {
3203 id,
3204 name: name.to_string(),
3205 position: Vec3::ZERO,
3206 rotation: Quat::IDENTITY,
3207 aperture: CameraAperture::new_cinema(),
3208 motion_blur: CameraMotionBlur::new(),
3209 lens_flare: CameraLensFlare::default(),
3210 vignette: CameraVignette::default(),
3211 chromatic_aberration: ChromaticAberration::default(),
3212 near_clip: 0.1,
3213 far_clip: 10000.0,
3214 aspect_ratio: 16.0 / 9.0,
3215 }
3216 }
3217
3218 pub fn view_matrix(&self) -> Mat4 {
3219 let rot = Mat4::from_quat(self.rotation);
3220 let trans = Mat4::from_translation(-self.position);
3221 rot.transpose() * trans
3222 }
3223
3224 pub fn projection_matrix(&self) -> Mat4 {
3225 let fov_y = self.aperture.field_of_view_vertical();
3226 Mat4::perspective_rh(fov_y, self.aspect_ratio, self.near_clip, self.far_clip)
3227 }
3228
3229 pub fn forward(&self) -> Vec3 {
3230 self.rotation * Vec3::NEG_Z
3231 }
3232
3233 pub fn right(&self) -> Vec3 {
3234 self.rotation * Vec3::X
3235 }
3236
3237 pub fn up(&self) -> Vec3 {
3238 self.rotation * Vec3::Y
3239 }
3240}
3241
3242#[derive(Debug, Clone)]
3243pub struct ExtCameraKeyframe {
3244 pub time: f32,
3245 pub position: Vec3,
3246 pub rotation: Quat,
3247 pub focal_length: f32,
3248 pub f_stop: f32,
3249 pub focus_distance: f32,
3250}
3251
3252#[derive(Debug, Clone)]
3253pub struct CameraAnimTrack {
3254 pub camera_id: u32,
3255 pub keyframes: Vec<ExtCameraKeyframe>,
3256}
3257
3258impl CameraAnimTrack {
3259 pub fn new(camera_id: u32) -> Self {
3260 CameraAnimTrack { camera_id, keyframes: Vec::new() }
3261 }
3262
3263 pub fn add_keyframe(&mut self, kf: ExtCameraKeyframe) {
3264 let idx = self.keyframes.partition_point(|k| k.time < kf.time);
3265 self.keyframes.insert(idx, kf);
3266 }
3267
3268 pub fn sample(&self, t: f32) -> Option<ExtCameraKeyframe> {
3269 if self.keyframes.is_empty() { return None; }
3270 if t <= self.keyframes[0].time {
3271 return Some(self.keyframes[0].clone());
3272 }
3273 let last = self.keyframes.last().unwrap();
3274 if t >= last.time {
3275 return Some(last.clone());
3276 }
3277 let idx = self.keyframes.partition_point(|k| k.time <= t) - 1;
3278 let a = &self.keyframes[idx];
3279 let b = &self.keyframes[idx + 1];
3280 let s = (t - a.time) / (b.time - a.time);
3281 Some(ExtCameraKeyframe {
3282 time: t,
3283 position: a.position.lerp(b.position, s),
3284 rotation: a.rotation.slerp(b.rotation, s),
3285 focal_length: a.focal_length + (b.focal_length - a.focal_length) * s,
3286 f_stop: a.f_stop + (b.f_stop - a.f_stop) * s,
3287 focus_distance: a.focus_distance + (b.focus_distance - a.focus_distance) * s,
3288 })
3289 }
3290
3291 pub fn duration(&self) -> f32 {
3292 self.keyframes.last().map(|k| k.time).unwrap_or(0.0)
3293 }
3294}
3295
3296#[derive(Debug, Clone)]
3297pub struct DollyTrackSegment {
3298 pub start: Vec3,
3299 pub end: Vec3,
3300 pub speed: f32,
3301}
3302
3303#[derive(Debug, Clone)]
3304pub struct CameraRig {
3305 pub rig_type: CameraRigType,
3306 pub position: Vec3,
3307 pub rotation: Quat,
3308 pub arm_length: f32,
3309 pub tilt_angle: f32,
3310 pub pan_angle: f32,
3311 pub noise_seed: u32,
3312 pub noise_intensity: f32,
3313 pub noise_frequency: f32,
3314 pub dolly_segments: Vec<DollyTrackSegment>,
3315 pub dolly_position_t: f32,
3316}
3317
3318impl CameraRig {
3319 pub fn new(rig_type: CameraRigType) -> Self {
3320 CameraRig {
3321 rig_type,
3322 position: Vec3::ZERO,
3323 rotation: Quat::IDENTITY,
3324 arm_length: 1.0,
3325 tilt_angle: 0.0,
3326 pan_angle: 0.0,
3327 noise_seed: 42,
3328 noise_intensity: 0.01,
3329 noise_frequency: 2.0,
3330 dolly_segments: Vec::new(),
3331 dolly_position_t: 0.0,
3332 }
3333 }
3334
3335 pub fn handhold_noise(&self, t: f32) -> Vec3 {
3336 let s = self.noise_seed as f32 * 0.001;
3337 let x = (t * self.noise_frequency + s).sin() * self.noise_intensity;
3338 let y = (t * self.noise_frequency * 1.3 + s + 1.0).sin() * self.noise_intensity * 0.5;
3339 let z = (t * self.noise_frequency * 0.7 + s + 2.0).sin() * self.noise_intensity * 0.3;
3340 Vec3::new(x, y, z)
3341 }
3342
3343 pub fn crane_tip_position(&self) -> Vec3 {
3344 let arm_dir = self.rotation * Vec3::new(self.pan_angle.sin(), self.tilt_angle.sin(), self.pan_angle.cos());
3345 self.position + arm_dir * self.arm_length
3346 }
3347
3348 pub fn add_dolly_segment(&mut self, seg: DollyTrackSegment) {
3349 self.dolly_segments.push(seg);
3350 }
3351
3352 pub fn dolly_total_length(&self) -> f32 {
3353 self.dolly_segments.iter().map(|s| (s.end - s.start).length()).sum()
3354 }
3355}
3356
3357#[derive(Debug, Clone, PartialEq)]
3362pub enum SubtitleAlignment {
3363 BottomCenter,
3364 BottomLeft,
3365 BottomRight,
3366 TopCenter,
3367 MiddleCenter,
3368}
3369
3370#[derive(Debug, Clone)]
3371pub struct SubtitleStyle {
3372 pub font_size: f32,
3373 pub color: Vec4,
3374 pub outline_color: Vec4,
3375 pub outline_width: f32,
3376 pub background_color: Vec4,
3377 pub background_enabled: bool,
3378 pub alignment: SubtitleAlignment,
3379 pub vertical_position: f32,
3380}
3381
3382impl Default for SubtitleStyle {
3383 fn default() -> Self {
3384 SubtitleStyle {
3385 font_size: 24.0,
3386 color: Vec4::new(1.0, 1.0, 1.0, 1.0),
3387 outline_color: Vec4::new(0.0, 0.0, 0.0, 1.0),
3388 outline_width: 1.5,
3389 background_color: Vec4::new(0.0, 0.0, 0.0, 0.5),
3390 background_enabled: false,
3391 alignment: SubtitleAlignment::BottomCenter,
3392 vertical_position: 0.1,
3393 }
3394 }
3395}
3396
3397#[derive(Debug, Clone)]
3398pub struct SubtitleCue {
3399 pub id: u32,
3400 pub start_time: f32,
3401 pub end_time: f32,
3402 pub text: String,
3403 pub speaker: Option<String>,
3404 pub style_override: Option<SubtitleStyle>,
3405}
3406
3407impl SubtitleCue {
3408 pub fn duration(&self) -> f32 {
3409 self.end_time - self.start_time
3410 }
3411
3412 pub fn is_active_at(&self, t: f32) -> bool {
3413 t >= self.start_time && t < self.end_time
3414 }
3415
3416 pub fn fade_alpha(&self, t: f32, fade_in: f32, fade_out: f32) -> f32 {
3417 if t < self.start_time + fade_in {
3418 (t - self.start_time) / fade_in.max(0.001)
3419 } else if t > self.end_time - fade_out {
3420 (self.end_time - t) / fade_out.max(0.001)
3421 } else {
3422 1.0
3423 }
3424 .clamp(0.0, 1.0)
3425 }
3426}
3427
3428#[derive(Debug, Clone)]
3429pub struct LocalizedSubtitles {
3430 pub language_code: String,
3431 pub cues: Vec<SubtitleCue>,
3432 pub default_style: SubtitleStyle,
3433 pub metadata: HashMap<String, String>,
3434}
3435
3436impl LocalizedSubtitles {
3437 pub fn new(language_code: &str) -> Self {
3438 LocalizedSubtitles {
3439 language_code: language_code.to_string(),
3440 cues: Vec::new(),
3441 default_style: SubtitleStyle::default(),
3442 metadata: HashMap::new(),
3443 }
3444 }
3445
3446 pub fn add_cue(&mut self, cue: SubtitleCue) {
3447 self.cues.push(cue);
3448 self.cues.sort_by(|a, b| a.start_time.partial_cmp(&b.start_time).unwrap());
3449 }
3450
3451 pub fn cues_at(&self, t: f32) -> Vec<&SubtitleCue> {
3452 self.cues.iter().filter(|c| c.is_active_at(t)).collect()
3453 }
3454
3455 pub fn export_srt(&self) -> String {
3456 let mut out = String::new();
3457 for (i, cue) in self.cues.iter().enumerate() {
3458 let fmt_time = |s: f32| -> String {
3459 let h = (s / 3600.0) as u32;
3460 let m = ((s % 3600.0) / 60.0) as u32;
3461 let sec = (s % 60.0) as u32;
3462 let ms = ((s % 1.0) * 1000.0) as u32;
3463 format!("{:02}:{:02}:{:02},{:03}", h, m, sec, ms)
3464 };
3465 out.push_str(&format!("{}\n{} --> {}\n{}\n\n",
3466 i + 1,
3467 fmt_time(cue.start_time),
3468 fmt_time(cue.end_time),
3469 cue.text));
3470 }
3471 out
3472 }
3473
3474 pub fn export_vtt(&self) -> String {
3475 let mut out = String::from("WEBVTT\n\n");
3476 for cue in &self.cues {
3477 let fmt_time = |s: f32| -> String {
3478 let h = (s / 3600.0) as u32;
3479 let m = ((s % 3600.0) / 60.0) as u32;
3480 let sec = (s % 60.0) as u32;
3481 let ms = ((s % 1.0) * 1000.0) as u32;
3482 format!("{:02}:{:02}:{:02}.{:03}", h, m, sec, ms)
3483 };
3484 out.push_str(&format!("{} --> {}\n{}\n\n",
3485 fmt_time(cue.start_time),
3486 fmt_time(cue.end_time),
3487 cue.text));
3488 }
3489 out
3490 }
3491}
3492
3493#[derive(Debug, Clone)]
3494pub struct SubtitleManager {
3495 pub tracks: HashMap<String, LocalizedSubtitles>,
3496 pub active_language: String,
3497 pub fade_in_duration: f32,
3498 pub fade_out_duration: f32,
3499}
3500
3501impl SubtitleManager {
3502 pub fn new(default_lang: &str) -> Self {
3503 SubtitleManager {
3504 tracks: HashMap::new(),
3505 active_language: default_lang.to_string(),
3506 fade_in_duration: 0.1,
3507 fade_out_duration: 0.1,
3508 }
3509 }
3510
3511 pub fn add_language(&mut self, lang: LocalizedSubtitles) {
3512 self.tracks.insert(lang.language_code.clone(), lang);
3513 }
3514
3515 pub fn active_cues(&self, t: f32) -> Vec<&SubtitleCue> {
3516 self.tracks.get(&self.active_language)
3517 .map(|l| l.cues_at(t))
3518 .unwrap_or_default()
3519 }
3520}
3521
3522#[derive(Debug, Clone, PartialEq)]
3527pub enum NarrativeEventType {
3528 DialogueLine,
3529 CharacterEnter,
3530 CharacterExit,
3531 EnvironmentChange,
3532 MusicChange,
3533 SfxTrigger,
3534 CameraChange,
3535 FlagSet,
3536 QuestUpdate,
3537 ItemGrant,
3538 SceneTransition,
3539}
3540
3541#[derive(Debug, Clone)]
3542pub struct NarrativeEvent {
3543 pub id: u32,
3544 pub event_type: NarrativeEventType,
3545 pub trigger_time: f32,
3546 pub payload: HashMap<String, String>,
3547 pub conditions: Vec<String>,
3548 pub fired: bool,
3549}
3550
3551impl NarrativeEvent {
3552 pub fn new(id: u32, event_type: NarrativeEventType, trigger_time: f32) -> Self {
3553 NarrativeEvent {
3554 id,
3555 event_type,
3556 trigger_time,
3557 payload: HashMap::new(),
3558 conditions: Vec::new(),
3559 fired: false,
3560 }
3561 }
3562
3563 pub fn with_payload(mut self, key: &str, value: &str) -> Self {
3564 self.payload.insert(key.to_string(), value.to_string());
3565 self
3566 }
3567}
3568
3569#[derive(Debug, Clone)]
3570pub struct NarrativeSequencer {
3571 pub events: Vec<NarrativeEvent>,
3572 pub global_flags: HashMap<String, bool>,
3573 pub event_log: VecDeque<(f32, u32)>,
3574 pub max_log_entries: usize,
3575}
3576
3577impl NarrativeSequencer {
3578 pub fn new() -> Self {
3579 NarrativeSequencer {
3580 events: Vec::new(),
3581 global_flags: HashMap::new(),
3582 event_log: VecDeque::new(),
3583 max_log_entries: 256,
3584 }
3585 }
3586
3587 pub fn add_event(&mut self, ev: NarrativeEvent) {
3588 self.events.push(ev);
3589 self.events.sort_by(|a, b| a.trigger_time.partial_cmp(&b.trigger_time).unwrap());
3590 }
3591
3592 pub fn tick(&mut self, t: f32) -> Vec<u32> {
3593 let mut fired_ids = Vec::new();
3594 for ev in self.events.iter_mut() {
3595 if !ev.fired && t >= ev.trigger_time {
3596 let conds_met = ev.conditions.iter().all(|c| {
3597 self.global_flags.get(c).copied().unwrap_or(false)
3598 });
3599 if conds_met {
3600 ev.fired = true;
3601 fired_ids.push(ev.id);
3602 if self.event_log.len() >= self.max_log_entries {
3603 self.event_log.pop_front();
3604 }
3605 self.event_log.push_back((t, ev.id));
3606 }
3607 }
3608 }
3609 fired_ids
3610 }
3611
3612 pub fn set_flag(&mut self, flag: &str, value: bool) {
3613 self.global_flags.insert(flag.to_string(), value);
3614 }
3615
3616 pub fn reset(&mut self) {
3617 for ev in self.events.iter_mut() {
3618 ev.fired = false;
3619 }
3620 }
3621
3622 pub fn unfired_count(&self) -> usize {
3623 self.events.iter().filter(|e| !e.fired).count()
3624 }
3625}
3626
3627#[derive(Debug, Clone)]
3632pub struct DialogueLine {
3633 pub id: u32,
3634 pub speaker_id: String,
3635 pub text: String,
3636 pub emotion: String,
3637 pub audio_asset: Option<String>,
3638 pub duration_hint: f32,
3639 pub lip_sync_asset: Option<String>,
3640}
3641
3642#[derive(Debug, Clone)]
3643pub struct DialogueChoice {
3644 pub id: u32,
3645 pub text: String,
3646 pub next_node_id: u32,
3647 pub conditions: Vec<String>,
3648 pub consequences: Vec<String>,
3649}
3650
3651#[derive(Debug, Clone)]
3652pub enum DialogueNodeContent {
3653 Line(DialogueLine),
3654 Choice(Vec<DialogueChoice>),
3655 Branch { condition: String, true_node: u32, false_node: u32 },
3656 End,
3657}
3658
3659#[derive(Debug, Clone)]
3660pub struct DialogueNode {
3661 pub id: u32,
3662 pub content: DialogueNodeContent,
3663 pub next_id: Option<u32>,
3664}
3665
3666#[derive(Debug, Clone)]
3667pub struct DialogueGraph {
3668 pub id: u32,
3669 pub name: String,
3670 pub nodes: HashMap<u32, DialogueNode>,
3671 pub start_node_id: u32,
3672}
3673
3674impl DialogueGraph {
3675 pub fn new(id: u32, name: &str, start_node_id: u32) -> Self {
3676 DialogueGraph { id, name: name.to_string(), nodes: HashMap::new(), start_node_id }
3677 }
3678
3679 pub fn add_node(&mut self, node: DialogueNode) {
3680 self.nodes.insert(node.id, node);
3681 }
3682
3683 pub fn node_count(&self) -> usize {
3684 self.nodes.len()
3685 }
3686
3687 pub fn validate(&self) -> Vec<String> {
3688 let mut errors = Vec::new();
3689 if !self.nodes.contains_key(&self.start_node_id) {
3690 errors.push(format!("Start node {} not found", self.start_node_id));
3691 }
3692 for (id, node) in &self.nodes {
3693 if let Some(next) = node.next_id {
3694 if !self.nodes.contains_key(&next) {
3695 errors.push(format!("Node {} references missing next {}", id, next));
3696 }
3697 }
3698 }
3699 errors
3700 }
3701}
3702
3703#[derive(Debug, Clone)]
3708pub struct ExtIkChain {
3709 pub name: String,
3710 pub root_bone: String,
3711 pub end_effector_bone: String,
3712 pub intermediate_bones: Vec<String>,
3713 pub pole_vector: Vec3,
3714 pub weight: f32,
3715 pub iterations: u32,
3716}
3717
3718impl ExtIkChain {
3719 pub fn new_arm_left() -> Self {
3720 ExtIkChain {
3721 name: "LeftArm".to_string(),
3722 root_bone: "LeftShoulder".to_string(),
3723 end_effector_bone: "LeftHand".to_string(),
3724 intermediate_bones: vec!["LeftUpperArm".to_string(), "LeftForeArm".to_string()],
3725 pole_vector: Vec3::new(-1.0, 0.0, -1.0),
3726 weight: 1.0,
3727 iterations: 10,
3728 }
3729 }
3730
3731 pub fn new_leg_right() -> Self {
3732 ExtIkChain {
3733 name: "RightLeg".to_string(),
3734 root_bone: "RightUpperLeg".to_string(),
3735 end_effector_bone: "RightFoot".to_string(),
3736 intermediate_bones: vec!["RightLowerLeg".to_string()],
3737 pole_vector: Vec3::new(0.0, 0.0, 1.0),
3738 weight: 1.0,
3739 iterations: 10,
3740 }
3741 }
3742
3743 pub fn chain_length(&self) -> usize {
3744 2 + self.intermediate_bones.len()
3745 }
3746}
3747
3748#[derive(Debug, Clone)]
3749pub struct RetargetingConstraint {
3750 pub source_bone: String,
3751 pub target_bone: String,
3752 pub rotation_only: bool,
3753 pub flip_x: bool,
3754 pub flip_y: bool,
3755 pub flip_z: bool,
3756 pub local_offset_rotation: Quat,
3757}
3758
3759impl RetargetingConstraint {
3760 pub fn new(source: &str, target: &str) -> Self {
3761 RetargetingConstraint {
3762 source_bone: source.to_string(),
3763 target_bone: target.to_string(),
3764 rotation_only: false,
3765 flip_x: false, flip_y: false, flip_z: false,
3766 local_offset_rotation: Quat::IDENTITY,
3767 }
3768 }
3769}
3770
3771#[derive(Debug, Clone)]
3772pub struct RetargetSession {
3773 pub source_skeleton: String,
3774 pub target_skeleton: String,
3775 pub constraints: Vec<RetargetingConstraint>,
3776 pub ik_chains: Vec<ExtIkChain>,
3777 pub global_scale: f32,
3778 pub hip_height_correction: f32,
3779}
3780
3781impl RetargetSession {
3782 pub fn new(source: &str, target: &str) -> Self {
3783 RetargetSession {
3784 source_skeleton: source.to_string(),
3785 target_skeleton: target.to_string(),
3786 constraints: Vec::new(),
3787 ik_chains: Vec::new(),
3788 global_scale: 1.0,
3789 hip_height_correction: 0.0,
3790 }
3791 }
3792
3793 pub fn add_constraint(&mut self, c: RetargetingConstraint) {
3794 self.constraints.push(c);
3795 }
3796
3797 pub fn add_ik_chain(&mut self, chain: ExtIkChain) {
3798 self.ik_chains.push(chain);
3799 }
3800
3801 pub fn humanoid_default(source: &str, target: &str) -> Self {
3802 let mut session = RetargetSession::new(source, target);
3803 let bone_pairs = [
3804 ("Hips", "Hips"), ("Spine", "Spine"), ("Chest", "Chest"),
3805 ("UpperChest", "UpperChest"), ("Neck", "Neck"), ("Head", "Head"),
3806 ("LeftShoulder", "LeftShoulder"), ("LeftUpperArm", "LeftUpperArm"),
3807 ("LeftForeArm", "LeftForeArm"), ("LeftHand", "LeftHand"),
3808 ("RightShoulder", "RightShoulder"), ("RightUpperArm", "RightUpperArm"),
3809 ("RightForeArm", "RightForeArm"), ("RightHand", "RightHand"),
3810 ("LeftUpperLeg", "LeftUpperLeg"), ("LeftLowerLeg", "LeftLowerLeg"),
3811 ("LeftFoot", "LeftFoot"), ("RightUpperLeg", "RightUpperLeg"),
3812 ("RightLowerLeg", "RightLowerLeg"), ("RightFoot", "RightFoot"),
3813 ];
3814 for (src, tgt) in &bone_pairs {
3815 session.add_constraint(RetargetingConstraint::new(src, tgt));
3816 }
3817 session.add_ik_chain(ExtIkChain::new_arm_left());
3818 session.add_ik_chain(ExtIkChain::new_leg_right());
3819 session
3820 }
3821}
3822
3823#[derive(Debug, Clone, PartialEq)]
3828pub enum ClipEditOperation {
3829 Trim,
3830 Split,
3831 Merge,
3832 SpeedChange,
3833 Reverse,
3834 Slip,
3835}
3836
3837#[derive(Debug, Clone)]
3838pub struct ClipEditHistory {
3839 pub clip_id: u32,
3840 pub operation: ClipEditOperation,
3841 pub before_start: f32,
3842 pub before_end: f32,
3843 pub after_start: f32,
3844 pub after_end: f32,
3845 pub timestamp: f32,
3846}
3847
3848#[derive(Debug, Clone)]
3849pub struct TimelineClipEditor {
3850 pub selected_clip_ids: HashSet<u32>,
3851 pub edit_history: VecDeque<ClipEditHistory>,
3852 pub max_history: usize,
3853 pub snapping_enabled: bool,
3854 pub snap_threshold: f32,
3855 pub ripple_edit_enabled: bool,
3856}
3857
3858impl TimelineClipEditor {
3859 pub fn new() -> Self {
3860 TimelineClipEditor {
3861 selected_clip_ids: HashSet::new(),
3862 edit_history: VecDeque::new(),
3863 max_history: 100,
3864 snapping_enabled: true,
3865 snap_threshold: 0.05,
3866 ripple_edit_enabled: false,
3867 }
3868 }
3869
3870 pub fn select_clip(&mut self, clip_id: u32) {
3871 self.selected_clip_ids.insert(clip_id);
3872 }
3873
3874 pub fn deselect_all(&mut self) {
3875 self.selected_clip_ids.clear();
3876 }
3877
3878 pub fn record_edit(&mut self, history: ClipEditHistory) {
3879 if self.edit_history.len() >= self.max_history {
3880 self.edit_history.pop_front();
3881 }
3882 self.edit_history.push_back(history);
3883 }
3884
3885 pub fn snap_to_nearest(&self, time: f32, snap_points: &[f32]) -> f32 {
3886 if !self.snapping_enabled { return time; }
3887 snap_points.iter()
3888 .min_by(|&&a, &&b| (a - time).abs().partial_cmp(&(b - time).abs()).unwrap())
3889 .copied()
3890 .filter(|&s| (s - time).abs() <= self.snap_threshold)
3891 .unwrap_or(time)
3892 }
3893
3894 pub fn undo_last(&mut self) -> Option<ClipEditHistory> {
3895 self.edit_history.pop_back()
3896 }
3897}
3898
3899#[derive(Debug, Clone, PartialEq)]
3904pub enum LayerBlendMode {
3905 Normal,
3906 Additive,
3907 Multiply,
3908 Screen,
3909 Overlay,
3910}
3911
3912#[derive(Debug, Clone)]
3913pub struct SceneLayer {
3914 pub id: u32,
3915 pub name: String,
3916 pub visible: bool,
3917 pub locked: bool,
3918 pub opacity: f32,
3919 pub blend_mode: LayerBlendMode,
3920 pub clip_ids: Vec<u32>,
3921 pub color_tag: Vec4,
3922}
3923
3924impl SceneLayer {
3925 pub fn new(id: u32, name: &str) -> Self {
3926 SceneLayer {
3927 id,
3928 name: name.to_string(),
3929 visible: true,
3930 locked: false,
3931 opacity: 1.0,
3932 blend_mode: LayerBlendMode::Normal,
3933 clip_ids: Vec::new(),
3934 color_tag: Vec4::new(0.5, 0.5, 0.5, 1.0),
3935 }
3936 }
3937
3938 pub fn add_clip(&mut self, clip_id: u32) {
3939 if !self.clip_ids.contains(&clip_id) {
3940 self.clip_ids.push(clip_id);
3941 }
3942 }
3943
3944 pub fn remove_clip(&mut self, clip_id: u32) {
3945 self.clip_ids.retain(|&id| id != clip_id);
3946 }
3947}
3948
3949#[derive(Debug, Clone)]
3950pub struct LayerStack {
3951 pub layers: Vec<SceneLayer>,
3952 pub active_layer_id: u32,
3953}
3954
3955impl LayerStack {
3956 pub fn new() -> Self {
3957 LayerStack { layers: Vec::new(), active_layer_id: 0 }
3958 }
3959
3960 pub fn add_layer(&mut self, layer: SceneLayer) {
3961 self.layers.push(layer);
3962 }
3963
3964 pub fn get_layer_mut(&mut self, id: u32) -> Option<&mut SceneLayer> {
3965 self.layers.iter_mut().find(|l| l.id == id)
3966 }
3967
3968 pub fn visible_layers(&self) -> Vec<&SceneLayer> {
3969 self.layers.iter().filter(|l| l.visible).collect()
3970 }
3971
3972 pub fn move_layer_up(&mut self, id: u32) {
3973 if let Some(idx) = self.layers.iter().position(|l| l.id == id) {
3974 if idx + 1 < self.layers.len() {
3975 self.layers.swap(idx, idx + 1);
3976 }
3977 }
3978 }
3979
3980 pub fn move_layer_down(&mut self, id: u32) {
3981 if let Some(idx) = self.layers.iter().position(|l| l.id == id) {
3982 if idx > 0 {
3983 self.layers.swap(idx, idx - 1);
3984 }
3985 }
3986 }
3987}
3988
3989#[derive(Debug, Clone)]
3994pub enum ExprToken {
3995 Number(f32),
3996 Variable(String),
3997 Plus, Minus, Multiply, Divide,
3998 LParen, RParen,
3999 Sin, Cos, Abs, Floor, Ceil, Sqrt,
4000}
4001
4002#[derive(Debug, Clone)]
4003pub struct ExpressionSolver {
4004 pub variables: HashMap<String, f32>,
4005}
4006
4007impl ExpressionSolver {
4008 pub fn new() -> Self {
4009 ExpressionSolver { variables: HashMap::new() }
4010 }
4011
4012 pub fn set_var(&mut self, name: &str, value: f32) {
4013 self.variables.insert(name.to_string(), value);
4014 }
4015
4016 pub fn evaluate_simple(&self, tokens: &[ExprToken]) -> f32 {
4017 if tokens.is_empty() { return 0.0; }
4019 let mut result = self.eval_primary(tokens, &mut 0);
4020 result
4021 }
4022
4023 fn eval_primary(&self, tokens: &[ExprToken], pos: &mut usize) -> f32 {
4024 if *pos >= tokens.len() { return 0.0; }
4025 match &tokens[*pos] {
4026 ExprToken::Number(n) => { *pos += 1; *n },
4027 ExprToken::Variable(v) => {
4028 *pos += 1;
4029 self.variables.get(v).copied().unwrap_or(0.0)
4030 },
4031 ExprToken::Sin => {
4032 *pos += 1;
4033 let arg = self.eval_primary(tokens, pos);
4034 arg.sin()
4035 },
4036 ExprToken::Cos => {
4037 *pos += 1;
4038 let arg = self.eval_primary(tokens, pos);
4039 arg.cos()
4040 },
4041 ExprToken::Abs => {
4042 *pos += 1;
4043 let arg = self.eval_primary(tokens, pos);
4044 arg.abs()
4045 },
4046 ExprToken::Sqrt => {
4047 *pos += 1;
4048 let arg = self.eval_primary(tokens, pos);
4049 arg.sqrt()
4050 },
4051 _ => 0.0,
4052 }
4053 }
4054}
4055
4056pub const CUTSCENE_STREAM_CHUNK_BYTES: usize = 65536;
4061pub const CUTSCENE_KEYFRAME_INTERVAL: u32 = 30;
4062pub const CUTSCENE_MAX_AUDIO_CHANNELS: u32 = 8;
4063pub const CUTSCENE_PREVIEW_WIDTH: u32 = 1280;
4064pub const CUTSCENE_PREVIEW_HEIGHT: u32 = 720;
4065pub const CUTSCENE_MASTER_WIDTH: u32 = 3840;
4066pub const CUTSCENE_MASTER_HEIGHT: u32 = 2160;
4067
4068#[derive(Debug, Clone)]
4069pub struct CutsceneStreamChunk {
4070 pub chunk_index: u32,
4071 pub byte_offset: u64,
4072 pub compressed_size: u32,
4073 pub uncompressed_size: u32,
4074 pub is_keyframe_chunk: bool,
4075 pub timestamp_start: f32,
4076 pub timestamp_end: f32,
4077}
4078
4079#[derive(Debug, Clone)]
4080pub struct CutsceneStreamIndex {
4081 pub total_chunks: u32,
4082 pub total_duration: f32,
4083 pub chunks: Vec<CutsceneStreamChunk>,
4084}
4085
4086impl CutsceneStreamIndex {
4087 pub fn new() -> Self {
4088 CutsceneStreamIndex { total_chunks: 0, total_duration: 0.0, chunks: Vec::new() }
4089 }
4090
4091 pub fn add_chunk(&mut self, chunk: CutsceneStreamChunk) {
4092 self.total_chunks += 1;
4093 if chunk.timestamp_end > self.total_duration {
4094 self.total_duration = chunk.timestamp_end;
4095 }
4096 self.chunks.push(chunk);
4097 }
4098
4099 pub fn chunk_for_time(&self, t: f32) -> Option<&CutsceneStreamChunk> {
4100 self.chunks.iter().find(|c| t >= c.timestamp_start && t < c.timestamp_end)
4101 }
4102
4103 pub fn total_compressed_bytes(&self) -> u64 {
4104 self.chunks.iter().map(|c| c.compressed_size as u64).sum()
4105 }
4106
4107 pub fn compression_ratio(&self) -> f32 {
4108 let compressed: u64 = self.chunks.iter().map(|c| c.compressed_size as u64).sum();
4109 let uncompressed: u64 = self.chunks.iter().map(|c| c.uncompressed_size as u64).sum();
4110 if uncompressed == 0 { return 1.0; }
4111 compressed as f32 / uncompressed as f32
4112 }
4113}
4114
4115#[derive(Debug, Clone, PartialEq)]
4120pub enum ShotType {
4121 ExtremeLongShot,
4122 LongShot,
4123 MediumLongShot,
4124 MediumShot,
4125 MediumCloseUp,
4126 CloseUp,
4127 ExtremeCloseUp,
4128 CutawayShot,
4129 InsertShot,
4130 CutInShot,
4131 PointOfViewShot,
4132 OverTheShoulderShot,
4133 TwoShot,
4134 EnsembleShot,
4135 AerialShot,
4136}
4137
4138#[derive(Debug, Clone, PartialEq)]
4139pub enum CameraAngle {
4140 EyeLevel,
4141 HighAngle,
4142 LowAngle,
4143 BirdsEye,
4144 WormsEye,
4145 DutchAngle,
4146 SubjectivePov,
4147}
4148
4149#[derive(Debug, Clone, PartialEq)]
4150pub enum CameraMovement {
4151 Static,
4152 Pan,
4153 Tilt,
4154 Dolly,
4155 Truck,
4156 Pedestal,
4157 Arc,
4158 Zoom,
4159 Crane,
4160 Handheld,
4161 Steadicam,
4162}
4163
4164#[derive(Debug, Clone)]
4165pub struct ShotListEntry {
4166 pub shot_number: String,
4167 pub scene_number: String,
4168 pub description: String,
4169 pub shot_type: ShotType,
4170 pub camera_angle: CameraAngle,
4171 pub camera_movement: CameraMovement,
4172 pub focal_length: f32,
4173 pub estimated_duration: f32,
4174 pub rig_notes: String,
4175 pub lighting_notes: String,
4176 pub priority: u32,
4177 pub complete: bool,
4178}
4179
4180#[derive(Debug, Clone)]
4181pub struct ShotList {
4182 pub title: String,
4183 pub director: String,
4184 pub dop: String,
4185 pub date: String,
4186 pub entries: Vec<ShotListEntry>,
4187}
4188
4189impl ShotList {
4190 pub fn new(title: &str) -> Self {
4191 ShotList {
4192 title: title.to_string(),
4193 director: String::new(),
4194 dop: String::new(),
4195 date: String::new(),
4196 entries: Vec::new(),
4197 }
4198 }
4199
4200 pub fn add_shot(&mut self, shot: ShotListEntry) {
4201 self.entries.push(shot);
4202 }
4203
4204 pub fn total_duration(&self) -> f32 {
4205 self.entries.iter().map(|e| e.estimated_duration).sum()
4206 }
4207
4208 pub fn incomplete_shots(&self) -> Vec<&ShotListEntry> {
4209 self.entries.iter().filter(|e| !e.complete).collect()
4210 }
4211
4212 pub fn shots_by_scene(&self, scene: &str) -> Vec<&ShotListEntry> {
4213 self.entries.iter().filter(|e| e.scene_number == scene).collect()
4214 }
4215
4216 pub fn export_csv(&self) -> String {
4217 let mut out = String::from("Shot,Scene,Description,Type,Duration,Complete\n");
4218 for e in &self.entries {
4219 out.push_str(&format!("{},{},{},{:?},{:.1},{}\n",
4220 e.shot_number, e.scene_number, e.description,
4221 e.shot_type, e.estimated_duration, e.complete));
4222 }
4223 out
4224 }
4225}
4226
4227#[derive(Debug, Clone, PartialEq)]
4232pub enum BlendNodeType {
4233 Clip,
4234 Blend1D,
4235 Blend2D,
4236 Override,
4237 LayeredBlend,
4238 StateMachine,
4239}
4240
4241#[derive(Debug, Clone)]
4242pub struct BlendNode {
4243 pub id: u32,
4244 pub node_type: BlendNodeType,
4245 pub name: String,
4246 pub child_ids: Vec<u32>,
4247 pub blend_param: String,
4248 pub blend_value: f32,
4249 pub clip_asset: Option<String>,
4250 pub loop_clip: bool,
4251 pub play_rate: f32,
4252}
4253
4254impl BlendNode {
4255 pub fn new_clip(id: u32, name: &str, asset: &str) -> Self {
4256 BlendNode {
4257 id,
4258 node_type: BlendNodeType::Clip,
4259 name: name.to_string(),
4260 child_ids: Vec::new(),
4261 blend_param: String::new(),
4262 blend_value: 0.0,
4263 clip_asset: Some(asset.to_string()),
4264 loop_clip: true,
4265 play_rate: 1.0,
4266 }
4267 }
4268
4269 pub fn new_blend1d(id: u32, name: &str, param: &str) -> Self {
4270 BlendNode {
4271 id,
4272 node_type: BlendNodeType::Blend1D,
4273 name: name.to_string(),
4274 child_ids: Vec::new(),
4275 blend_param: param.to_string(),
4276 blend_value: 0.0,
4277 clip_asset: None,
4278 loop_clip: false,
4279 play_rate: 1.0,
4280 }
4281 }
4282}
4283
4284#[derive(Debug, Clone)]
4285pub struct ExtAnimState {
4286 pub id: u32,
4287 pub name: String,
4288 pub root_node_id: u32,
4289 pub speed: f32,
4290 pub loop_state: bool,
4291}
4292
4293#[derive(Debug, Clone)]
4294pub struct AnimationTransition {
4295 pub from_state_id: u32,
4296 pub to_state_id: u32,
4297 pub condition: String,
4298 pub blend_duration: f32,
4299 pub interrupt_current: bool,
4300}
4301
4302#[derive(Debug, Clone)]
4303pub struct ExtAnimStateMachine {
4304 pub name: String,
4305 pub states: HashMap<u32, ExtAnimState>,
4306 pub transitions: Vec<AnimationTransition>,
4307 pub blend_nodes: HashMap<u32, BlendNode>,
4308 pub current_state_id: u32,
4309 pub parameters: HashMap<String, f32>,
4310 pub bool_parameters: HashMap<String, bool>,
4311}
4312
4313impl ExtAnimStateMachine {
4314 pub fn new(name: &str) -> Self {
4315 ExtAnimStateMachine {
4316 name: name.to_string(),
4317 states: HashMap::new(),
4318 transitions: Vec::new(),
4319 blend_nodes: HashMap::new(),
4320 current_state_id: 0,
4321 parameters: HashMap::new(),
4322 bool_parameters: HashMap::new(),
4323 }
4324 }
4325
4326 pub fn add_state(&mut self, state: ExtAnimState) {
4327 self.states.insert(state.id, state);
4328 }
4329
4330 pub fn add_transition(&mut self, t: AnimationTransition) {
4331 self.transitions.push(t);
4332 }
4333
4334 pub fn set_float(&mut self, param: &str, value: f32) {
4335 self.parameters.insert(param.to_string(), value);
4336 }
4337
4338 pub fn set_bool(&mut self, param: &str, value: bool) {
4339 self.bool_parameters.insert(param.to_string(), value);
4340 }
4341
4342 pub fn evaluate_transitions(&self) -> Option<u32> {
4343 for t in &self.transitions {
4344 if t.from_state_id == self.current_state_id {
4345 if self.bool_parameters.get(&t.condition).copied().unwrap_or(false) {
4346 return Some(t.to_state_id);
4347 }
4348 }
4349 }
4350 None
4351 }
4352
4353 pub fn tick(&mut self) {
4354 if let Some(next_id) = self.evaluate_transitions() {
4355 self.current_state_id = next_id;
4356 }
4357 }
4358}
4359
4360#[derive(Debug, Clone, PartialEq)]
4365pub enum CurveInterpolation {
4366 Linear,
4367 Hermite,
4368 CatmullRom,
4369 BSpline,
4370 Stepped,
4371}
4372
4373#[derive(Debug, Clone)]
4374pub struct CurveKey {
4375 pub time: f32,
4376 pub value: f32,
4377 pub in_tangent: f32,
4378 pub out_tangent: f32,
4379 pub interp: CurveInterpolation,
4380}
4381
4382#[derive(Debug, Clone)]
4383pub struct AnimationCurve {
4384 pub keys: Vec<CurveKey>,
4385 pub pre_wrap: bool,
4386 pub post_wrap: bool,
4387}
4388
4389impl AnimationCurve {
4390 pub fn new() -> Self {
4391 AnimationCurve { keys: Vec::new(), pre_wrap: false, post_wrap: false }
4392 }
4393
4394 pub fn add_key(&mut self, key: CurveKey) {
4395 let idx = self.keys.partition_point(|k| k.time < key.time);
4396 self.keys.insert(idx, key);
4397 }
4398
4399 pub fn evaluate(&self, t: f32) -> f32 {
4400 if self.keys.is_empty() { return 0.0; }
4401 if t <= self.keys[0].time { return self.keys[0].value; }
4402 let last = self.keys.last().unwrap();
4403 if t >= last.time { return last.value; }
4404
4405 let idx = self.keys.partition_point(|k| k.time <= t) - 1;
4406 let a = &self.keys[idx];
4407 let b = &self.keys[idx + 1];
4408 let dt = b.time - a.time;
4409 let s = (t - a.time) / dt;
4410
4411 match a.interp {
4412 CurveInterpolation::Linear => a.value + (b.value - a.value) * s,
4413 CurveInterpolation::Stepped => a.value,
4414 CurveInterpolation::Hermite => {
4415 let s2 = s * s;
4416 let s3 = s2 * s;
4417 let h00 = 2.0 * s3 - 3.0 * s2 + 1.0;
4418 let h10 = s3 - 2.0 * s2 + s;
4419 let h01 = -2.0 * s3 + 3.0 * s2;
4420 let h11 = s3 - s2;
4421 h00 * a.value + h10 * dt * a.out_tangent + h01 * b.value + h11 * dt * b.in_tangent
4422 },
4423 CurveInterpolation::CatmullRom => {
4424 let s2 = s * s;
4426 let s3 = s2 * s;
4427 0.5 * ((2.0 * a.value)
4428 + (-a.value + b.value) * s
4429 + (2.0 * a.value - 5.0 * a.value + 4.0 * b.value - b.value) * s2
4430 + (-a.value + 3.0 * a.value - 3.0 * b.value + b.value) * s3)
4431 },
4432 CurveInterpolation::BSpline => {
4433 let s2 = s * s;
4435 let s3 = s2 * s;
4436 a.value + (b.value - a.value) * (3.0 * s2 - 2.0 * s3)
4437 },
4438 }
4439 }
4440
4441 pub fn duration(&self) -> f32 {
4442 if self.keys.len() < 2 { return 0.0; }
4443 self.keys.last().unwrap().time - self.keys[0].time
4444 }
4445}
4446
4447#[cfg(test)]
4452mod tests_cinematic_camera {
4453 use super::*;
4454
4455 #[test]
4456 fn test_camera_aperture_fov() {
4457 let cam = CameraAperture::new_cinema();
4458 let fov_h = cam.field_of_view_horizontal();
4459 assert!(fov_h > 0.5 && fov_h < 1.5);
4460 }
4461
4462 #[test]
4463 fn test_camera_anim_track_sample() {
4464 let mut track = CameraAnimTrack::new(1);
4465 track.add_keyframe(ExtCameraKeyframe {
4466 time: 0.0, position: Vec3::ZERO, rotation: Quat::IDENTITY,
4467 focal_length: 35.0, f_stop: 2.8, focus_distance: 5.0,
4468 });
4469 track.add_keyframe(ExtCameraKeyframe {
4470 time: 1.0, position: Vec3::new(1.0, 0.0, 0.0), rotation: Quat::IDENTITY,
4471 focal_length: 50.0, f_stop: 4.0, focus_distance: 8.0,
4472 });
4473 let kf = track.sample(0.5).unwrap();
4474 assert!((kf.position.x - 0.5).abs() < 0.01);
4475 assert!((kf.focal_length - 42.5).abs() < 0.1);
4476 }
4477
4478 #[test]
4479 fn test_subtitle_srt_export() {
4480 let mut subs = LocalizedSubtitles::new("en");
4481 subs.add_cue(SubtitleCue {
4482 id: 1, start_time: 0.0, end_time: 2.5,
4483 text: "Hello world".to_string(), speaker: None, style_override: None,
4484 });
4485 let srt = subs.export_srt();
4486 assert!(srt.contains("Hello world"));
4487 assert!(srt.contains("00:00:00,000"));
4488 }
4489
4490 #[test]
4491 fn test_narrative_sequencer() {
4492 let mut seq = NarrativeSequencer::new();
4493 let mut ev = NarrativeEvent::new(1, NarrativeEventType::DialogueLine, 1.0);
4494 seq.add_event(ev);
4495 let fired = seq.tick(0.5);
4496 assert!(fired.is_empty());
4497 let fired = seq.tick(1.5);
4498 assert_eq!(fired.len(), 1);
4499 let fired2 = seq.tick(2.0);
4500 assert!(fired2.is_empty()); }
4502
4503 #[test]
4504 fn test_dialogue_graph_validate() {
4505 let mut graph = DialogueGraph::new(1, "Test", 1);
4506 graph.add_node(DialogueNode {
4507 id: 1,
4508 content: DialogueNodeContent::End,
4509 next_id: None,
4510 });
4511 let errors = graph.validate();
4512 assert!(errors.is_empty());
4513 }
4514
4515 #[test]
4516 fn test_blend_state_machine() {
4517 let mut sm = ExtAnimStateMachine::new("Character");
4518 sm.add_state(ExtAnimState { id: 0, name: "Idle".to_string(), root_node_id: 0, speed: 1.0, loop_state: true });
4519 sm.add_state(ExtAnimState { id: 1, name: "Walk".to_string(), root_node_id: 1, speed: 1.0, loop_state: true });
4520 sm.add_transition(AnimationTransition {
4521 from_state_id: 0, to_state_id: 1,
4522 condition: "IsWalking".to_string(),
4523 blend_duration: 0.2, interrupt_current: false,
4524 });
4525 sm.current_state_id = 0;
4526 sm.set_bool("IsWalking", false);
4527 sm.tick();
4528 assert_eq!(sm.current_state_id, 0);
4529 sm.set_bool("IsWalking", true);
4530 sm.tick();
4531 assert_eq!(sm.current_state_id, 1);
4532 }
4533
4534 #[test]
4535 fn test_animation_curve_hermite() {
4536 let mut curve = AnimationCurve::new();
4537 curve.add_key(CurveKey { time: 0.0, value: 0.0, in_tangent: 0.0, out_tangent: 1.0, interp: CurveInterpolation::Hermite });
4538 curve.add_key(CurveKey { time: 1.0, value: 1.0, in_tangent: 1.0, out_tangent: 0.0, interp: CurveInterpolation::Hermite });
4539 let mid = curve.evaluate(0.5);
4540 assert!(mid > 0.4 && mid < 0.6);
4541 }
4542
4543 #[test]
4544 fn test_retarget_session_humanoid() {
4545 let session = RetargetSession::humanoid_default("UE5Mannequin", "ReadyPlayerMe");
4546 assert_eq!(session.constraints.len(), 20);
4547 assert_eq!(session.ik_chains.len(), 2);
4548 }
4549
4550 #[test]
4551 fn test_shot_list_csv() {
4552 let mut sl = ShotList::new("TestScene");
4553 sl.add_shot(ShotListEntry {
4554 shot_number: "1A".to_string(), scene_number: "1".to_string(),
4555 description: "Establishing shot".to_string(),
4556 shot_type: ShotType::LongShot, camera_angle: CameraAngle::EyeLevel,
4557 camera_movement: CameraMovement::Static,
4558 focal_length: 35.0, estimated_duration: 5.0,
4559 rig_notes: String::new(), lighting_notes: String::new(),
4560 priority: 1, complete: false,
4561 });
4562 let csv = sl.export_csv();
4563 assert!(csv.contains("1A"));
4564 assert!(csv.contains("LongShot"));
4565 }
4566
4567 #[test]
4568 fn test_stream_index() {
4569 let mut idx = CutsceneStreamIndex::new();
4570 idx.add_chunk(CutsceneStreamChunk {
4571 chunk_index: 0, byte_offset: 0,
4572 compressed_size: 32768, uncompressed_size: 65536,
4573 is_keyframe_chunk: true, timestamp_start: 0.0, timestamp_end: 1.0,
4574 });
4575 assert!((idx.compression_ratio() - 0.5).abs() < 0.01);
4576 assert!(idx.chunk_for_time(0.5).is_some());
4577 }
4578
4579 #[test]
4580 fn test_camera_rig_handheld_noise() {
4581 let rig = CameraRig::new(CameraRigType::Handheld);
4582 let n1 = rig.handhold_noise(0.0);
4583 let n2 = rig.handhold_noise(0.5);
4584 assert_ne!(n1.x, n2.x);
4585 }
4586}
4587
4588pub const CUTSCENE_FILE_MAGIC: u32 = 0x43545343; pub const CUTSCENE_FILE_VERSION: u32 = 3;
4594
4595#[derive(Debug, Clone)]
4596pub struct CutsceneFileHeader {
4597 pub magic: u32,
4598 pub version: u32,
4599 pub total_duration: f32,
4600 pub frame_rate: f32,
4601 pub track_count: u32,
4602 pub event_count: u32,
4603 pub subtitle_language_count: u32,
4604 pub created_timestamp: u64,
4605 pub modified_timestamp: u64,
4606 pub author: String,
4607 pub description: String,
4608}
4609
4610impl CutsceneFileHeader {
4611 pub fn new(duration: f32, fps: f32) -> Self {
4612 CutsceneFileHeader {
4613 magic: CUTSCENE_FILE_MAGIC,
4614 version: CUTSCENE_FILE_VERSION,
4615 total_duration: duration,
4616 frame_rate: fps,
4617 track_count: 0,
4618 event_count: 0,
4619 subtitle_language_count: 0,
4620 created_timestamp: 0,
4621 modified_timestamp: 0,
4622 author: String::new(),
4623 description: String::new(),
4624 }
4625 }
4626
4627 pub fn is_valid(&self) -> bool {
4628 self.magic == CUTSCENE_FILE_MAGIC && self.version <= CUTSCENE_FILE_VERSION
4629 }
4630
4631 pub fn total_frames(&self) -> u32 {
4632 (self.total_duration * self.frame_rate).ceil() as u32
4633 }
4634}
4635
4636#[derive(Debug, Clone)]
4637pub struct CutsceneProjectFile {
4638 pub header: CutsceneFileHeader,
4639 pub cameras: Vec<CinematicCamera>,
4640 pub camera_tracks: Vec<CameraAnimTrack>,
4641 pub layer_stack: LayerStack,
4642 pub subtitle_manager: SubtitleManager,
4643 pub narrative_sequencer: NarrativeSequencer,
4644 pub shot_list: ShotList,
4645 pub stream_index: CutsceneStreamIndex,
4646}
4647
4648impl CutsceneProjectFile {
4649 pub fn new(duration: f32, fps: f32) -> Self {
4650 CutsceneProjectFile {
4651 header: CutsceneFileHeader::new(duration, fps),
4652 cameras: Vec::new(),
4653 camera_tracks: Vec::new(),
4654 layer_stack: LayerStack::new(),
4655 subtitle_manager: SubtitleManager::new("en"),
4656 narrative_sequencer: NarrativeSequencer::new(),
4657 shot_list: ShotList::new("Untitled"),
4658 stream_index: CutsceneStreamIndex::new(),
4659 }
4660 }
4661
4662 pub fn add_camera(&mut self, cam: CinematicCamera) {
4663 self.cameras.push(cam);
4664 self.header.track_count += 1;
4665 }
4666
4667 pub fn camera_by_id(&self, id: u32) -> Option<&CinematicCamera> {
4668 self.cameras.iter().find(|c| c.id == id)
4669 }
4670
4671 pub fn summary(&self) -> String {
4672 format!(
4673 "Cutscene '{}' v{}: {:.2}s @ {}fps, {} cameras, {} tracks, {} events",
4674 self.shot_list.title,
4675 self.header.version,
4676 self.header.total_duration,
4677 self.header.frame_rate,
4678 self.cameras.len(),
4679 self.header.track_count,
4680 self.narrative_sequencer.events.len()
4681 )
4682 }
4683}
4684
4685#[derive(Debug, Clone, PartialEq)]
4690pub enum BusType {
4691 Master,
4692 Music,
4693 SFX,
4694 Dialogue,
4695 Ambience,
4696 Aux,
4697}
4698
4699#[derive(Debug, Clone)]
4700pub struct AudioEffect {
4701 pub name: String,
4702 pub enabled: bool,
4703 pub parameters: HashMap<String, f32>,
4704}
4705
4706#[derive(Debug, Clone)]
4707pub struct MixBus {
4708 pub id: u32,
4709 pub name: String,
4710 pub bus_type: BusType,
4711 pub volume: f32,
4712 pub pan: f32,
4713 pub muted: bool,
4714 pub soloed: bool,
4715 pub send_to_master: bool,
4716 pub effects: Vec<AudioEffect>,
4717 pub children: Vec<u32>,
4718}
4719
4720impl MixBus {
4721 pub fn new(id: u32, name: &str, bus_type: BusType) -> Self {
4722 MixBus {
4723 id, name: name.to_string(), bus_type,
4724 volume: 1.0, pan: 0.0, muted: false, soloed: false,
4725 send_to_master: true, effects: Vec::new(), children: Vec::new(),
4726 }
4727 }
4728
4729 pub fn add_effect(&mut self, effect: AudioEffect) {
4730 self.effects.push(effect);
4731 }
4732
4733 pub fn effective_volume(&self) -> f32 {
4734 if self.muted { 0.0 } else { self.volume }
4735 }
4736
4737 pub fn add_reverb(&mut self, room_size: f32, damping: f32, wet: f32) {
4738 let mut params = HashMap::new();
4739 params.insert("room_size".to_string(), room_size);
4740 params.insert("damping".to_string(), damping);
4741 params.insert("wet_level".to_string(), wet);
4742 self.add_effect(AudioEffect {
4743 name: "Reverb".to_string(),
4744 enabled: true,
4745 parameters: params,
4746 });
4747 }
4748
4749 pub fn add_eq_low_shelf(&mut self, frequency: f32, gain_db: f32) {
4750 let mut params = HashMap::new();
4751 params.insert("frequency".to_string(), frequency);
4752 params.insert("gain_db".to_string(), gain_db);
4753 self.add_effect(AudioEffect {
4754 name: "EQLowShelf".to_string(),
4755 enabled: true,
4756 parameters: params,
4757 });
4758 }
4759}
4760
4761#[derive(Debug, Clone)]
4762pub struct AudioMixer {
4763 pub buses: HashMap<u32, MixBus>,
4764 pub master_bus_id: u32,
4765 pub sample_rate: u32,
4766 pub buffer_size: u32,
4767 pub bit_depth: u32,
4768}
4769
4770impl AudioMixer {
4771 pub fn new(sample_rate: u32, buffer_size: u32) -> Self {
4772 let mut mixer = AudioMixer {
4773 buses: HashMap::new(),
4774 master_bus_id: 0,
4775 sample_rate,
4776 buffer_size,
4777 bit_depth: 24,
4778 };
4779 mixer.buses.insert(0, MixBus::new(0, "Master", BusType::Master));
4780 mixer
4781 }
4782
4783 pub fn add_bus(&mut self, bus: MixBus) {
4784 self.buses.insert(bus.id, bus);
4785 }
4786
4787 pub fn default_setup() -> Self {
4788 let mut m = AudioMixer::new(48000, 1024);
4789 m.add_bus(MixBus::new(1, "Music", BusType::Music));
4790 m.add_bus(MixBus::new(2, "SFX", BusType::SFX));
4791 m.add_bus(MixBus::new(3, "Dialogue", BusType::Dialogue));
4792 m.add_bus(MixBus::new(4, "Ambience", BusType::Ambience));
4793 m
4794 }
4795
4796 pub fn get_bus_mut(&mut self, id: u32) -> Option<&mut MixBus> {
4797 self.buses.get_mut(&id)
4798 }
4799
4800 pub fn active_buses(&self) -> Vec<&MixBus> {
4801 self.buses.values().filter(|b| !b.muted).collect()
4802 }
4803}
4804
4805#[derive(Debug, Clone)]
4810pub struct LensProfile {
4811 pub name: String,
4812 pub manufacturer: String,
4813 pub focal_length_mm: f32,
4814 pub max_aperture: f32,
4815 pub min_aperture: f32,
4816 pub t_stop: f32,
4817 pub distortion_k1: f32,
4818 pub distortion_k2: f32,
4819 pub chromatic_aberration: f32,
4820 pub vignetting_amount: f32,
4821 pub breathing_percent: f32,
4822}
4823
4824impl LensProfile {
4825 pub fn prime_35mm_fast() -> Self {
4826 LensProfile {
4827 name: "35mm T1.5".to_string(),
4828 manufacturer: "ProCine".to_string(),
4829 focal_length_mm: 35.0,
4830 max_aperture: 1.4,
4831 min_aperture: 16.0,
4832 t_stop: 1.5,
4833 distortion_k1: -0.02,
4834 distortion_k2: 0.005,
4835 chromatic_aberration: 0.03,
4836 vignetting_amount: 0.15,
4837 breathing_percent: 2.0,
4838 }
4839 }
4840
4841 pub fn zoom_24_70() -> Self {
4842 LensProfile {
4843 name: "24-70mm T2.8".to_string(),
4844 manufacturer: "CineZoom".to_string(),
4845 focal_length_mm: 47.0,
4846 max_aperture: 2.8,
4847 min_aperture: 22.0,
4848 t_stop: 2.9,
4849 distortion_k1: 0.015,
4850 distortion_k2: -0.003,
4851 chromatic_aberration: 0.05,
4852 vignetting_amount: 0.12,
4853 breathing_percent: 5.0,
4854 }
4855 }
4856
4857 pub fn effective_fov(&self, sensor_width: f32) -> f32 {
4858 2.0 * (sensor_width / (2.0 * self.focal_length_mm)).atan()
4859 }
4860}
4861
4862#[derive(Debug, Clone)]
4863pub struct LensLibrary {
4864 pub profiles: Vec<LensProfile>,
4865}
4866
4867impl LensLibrary {
4868 pub fn default_library() -> Self {
4869 LensLibrary {
4870 profiles: vec![
4871 LensProfile::prime_35mm_fast(),
4872 LensProfile::zoom_24_70(),
4873 ],
4874 }
4875 }
4876
4877 pub fn find_by_focal_length(&self, fl: f32) -> Vec<&LensProfile> {
4878 self.profiles.iter()
4879 .filter(|p| (p.focal_length_mm - fl).abs() < 5.0)
4880 .collect()
4881 }
4882}
4883
4884pub fn cutscene_importer_info() -> &'static str {
4886 "CutsceneImporter v3.0: Camera, Subtitle, Narrative, BlendTree, Audio, Lens"
4887}
4888
4889
4890#[derive(Debug, Clone, PartialEq)]
4895pub enum DirectorCutType {
4896 HardCut,
4897 Dissolve,
4898 FadeToBlack,
4899 FadeFromBlack,
4900 Wipe,
4901 CrossFade,
4902 Morph,
4903}
4904
4905#[derive(Debug, Clone)]
4906pub struct DirectorCut {
4907 pub time: f32,
4908 pub from_camera_id: u32,
4909 pub to_camera_id: u32,
4910 pub cut_type: DirectorCutType,
4911 pub transition_duration: f32,
4912 pub notes: String,
4913}
4914
4915#[derive(Debug, Clone)]
4916pub struct MultiCameraDirector {
4917 pub cameras: Vec<u32>,
4918 pub cuts: Vec<DirectorCut>,
4919 pub active_camera_id: u32,
4920 pub cut_rules: HashMap<String, f32>,
4921}
4922
4923impl MultiCameraDirector {
4924 pub fn new() -> Self {
4925 MultiCameraDirector {
4926 cameras: Vec::new(),
4927 cuts: Vec::new(),
4928 active_camera_id: 0,
4929 cut_rules: HashMap::new(),
4930 }
4931 }
4932
4933 pub fn add_cut(&mut self, cut: DirectorCut) {
4934 self.cuts.push(cut);
4935 self.cuts.sort_by(|a, b| a.time.partial_cmp(&b.time).unwrap());
4936 }
4937
4938 pub fn active_at(&self, t: f32) -> u32 {
4939 let mut active = self.active_camera_id;
4940 for cut in &self.cuts {
4941 if cut.time <= t {
4942 active = cut.to_camera_id;
4943 }
4944 }
4945 active
4946 }
4947
4948 pub fn next_cut(&self, t: f32) -> Option<&DirectorCut> {
4949 self.cuts.iter().find(|c| c.time > t)
4950 }
4951
4952 pub fn cuts_in_range(&self, start: f32, end: f32) -> Vec<&DirectorCut> {
4953 self.cuts.iter().filter(|c| c.time >= start && c.time < end).collect()
4954 }
4955}
4956
4957pub fn lerp_f32(a: f32, b: f32, t: f32) -> f32 { a + (b - a) * t }
4962pub fn smoothstep(a: f32, b: f32, t: f32) -> f32 {
4963 let x = ((t - a) / (b - a)).clamp(0.0, 1.0);
4964 x * x * (3.0 - 2.0 * x)
4965}
4966pub fn smootherstep(t: f32) -> f32 {
4967 let t = t.clamp(0.0, 1.0);
4968 t * t * t * (t * (t * 6.0 - 15.0) + 10.0)
4969}
4970pub fn ease_in_quad(t: f32) -> f32 { t * t }
4971pub fn ease_out_quad(t: f32) -> f32 { t * (2.0 - t) }
4972pub fn ease_in_out_quad(t: f32) -> f32 {
4973 if t < 0.5 { 2.0 * t * t } else { -1.0 + (4.0 - 2.0 * t) * t }
4974}
4975pub fn ease_in_cubic(t: f32) -> f32 { t * t * t }
4976pub fn ease_out_cubic(t: f32) -> f32 { let t = t - 1.0; t * t * t + 1.0 }
4977pub fn ease_in_out_cubic(t: f32) -> f32 {
4978 if t < 0.5 { 4.0 * t * t * t } else { (t - 1.0) * (2.0 * t - 2.0) * (2.0 * t - 2.0) + 1.0 }
4979}
4980pub fn ease_in_expo(t: f32) -> f32 {
4981 if t == 0.0 { 0.0 } else { (2.0f32).powf(10.0 * t - 10.0) }
4982}
4983pub fn ease_out_expo(t: f32) -> f32 {
4984 if t == 1.0 { 1.0 } else { 1.0 - (2.0f32).powf(-10.0 * t) }
4985}
4986pub fn ease_in_back(t: f32) -> f32 {
4987 let c1 = 1.70158_f32;
4988 let c3 = c1 + 1.0;
4989 c3 * t * t * t - c1 * t * t
4990}
4991pub fn ease_out_back(t: f32) -> f32 {
4992 let c1 = 1.70158_f32;
4993 let c3 = c1 + 1.0;
4994 1.0 + c3 * (t - 1.0).powi(3) + c1 * (t - 1.0).powi(2)
4995}
4996pub fn ease_out_bounce(t: f32) -> f32 {
4997 let n1 = 7.5625_f32;
4998 let d1 = 2.75_f32;
4999 let t = if t < 1.0 / d1 {
5000 n1 * t * t
5001 } else if t < 2.0 / d1 {
5002 let t = t - 1.5 / d1;
5003 n1 * t * t + 0.75
5004 } else if t < 2.5 / d1 {
5005 let t = t - 2.25 / d1;
5006 n1 * t * t + 0.9375
5007 } else {
5008 let t = t - 2.625 / d1;
5009 n1 * t * t + 0.984375
5010 };
5011 t
5012}
5013pub fn ease_in_elastic(t: f32) -> f32 {
5014 if t == 0.0 || t == 1.0 { return t; }
5015 let c4 = std::f32::consts::TAU / 3.0;
5016 -(2.0f32.powf(10.0 * t - 10.0)) * ((10.0 * t - 10.75) * c4).sin()
5017}
5018
5019#[derive(Debug, Clone)]
5024pub struct MocapMarker {
5025 pub id: u32,
5026 pub label: String,
5027 pub position: Vec3,
5028 pub occluded: bool,
5029 pub residual: f32,
5030}
5031
5032#[derive(Debug, Clone)]
5033pub struct ExtMocapFrame {
5034 pub frame_number: u32,
5035 pub timestamp: f32,
5036 pub markers: Vec<MocapMarker>,
5037}
5038
5039impl ExtMocapFrame {
5040 pub fn visible_marker_count(&self) -> usize {
5041 self.markers.iter().filter(|m| !m.occluded).count()
5042 }
5043
5044 pub fn marker_by_label(&self, label: &str) -> Option<&MocapMarker> {
5045 self.markers.iter().find(|m| m.label == label)
5046 }
5047
5048 pub fn centroid(&self) -> Vec3 {
5049 let visible: Vec<&MocapMarker> = self.markers.iter().filter(|m| !m.occluded).collect();
5050 if visible.is_empty() { return Vec3::ZERO; }
5051 let sum: Vec3 = visible.iter().map(|m| m.position).fold(Vec3::ZERO, |a, b| a + b);
5052 sum / visible.len() as f32
5053 }
5054}
5055
5056#[derive(Debug, Clone)]
5057pub struct MocapSession {
5058 pub session_id: String,
5059 pub actor_name: String,
5060 pub capture_fps: f32,
5061 pub frames: Vec<ExtMocapFrame>,
5062 pub marker_labels: Vec<String>,
5063 pub calibration_residual: f32,
5064}
5065
5066impl MocapSession {
5067 pub fn new(session_id: &str, actor: &str, fps: f32) -> Self {
5068 MocapSession {
5069 session_id: session_id.to_string(),
5070 actor_name: actor.to_string(),
5071 capture_fps: fps,
5072 frames: Vec::new(),
5073 marker_labels: Vec::new(),
5074 calibration_residual: 0.0,
5075 }
5076 }
5077
5078 pub fn duration(&self) -> f32 {
5079 self.frames.last().map(|f| f.timestamp).unwrap_or(0.0)
5080 }
5081
5082 pub fn frame_at(&self, t: f32) -> Option<&ExtMocapFrame> {
5083 let frame_num = (t * self.capture_fps) as u32;
5084 self.frames.iter().find(|f| f.frame_number == frame_num)
5085 }
5086
5087 pub fn avg_marker_visibility(&self) -> f32 {
5088 if self.frames.is_empty() { return 0.0; }
5089 let total: usize = self.frames.iter().map(|f| f.visible_marker_count()).sum();
5090 let max_per_frame = self.marker_labels.len().max(1);
5091 total as f32 / (self.frames.len() * max_per_frame) as f32
5092 }
5093}
5094
5095#[derive(Debug, Clone)]
5100pub struct SplineControlPoint {
5101 pub position: Vec3,
5102 pub tangent_in: Vec3,
5103 pub tangent_out: Vec3,
5104 pub roll_deg: f32,
5105 pub speed_multiplier: f32,
5106}
5107
5108impl SplineControlPoint {
5109 pub fn new(pos: Vec3) -> Self {
5110 SplineControlPoint {
5111 position: pos,
5112 tangent_in: Vec3::ZERO,
5113 tangent_out: Vec3::ZERO,
5114 roll_deg: 0.0,
5115 speed_multiplier: 1.0,
5116 }
5117 }
5118}
5119
5120#[derive(Debug, Clone)]
5121pub struct CameraSplinePath {
5122 pub control_points: Vec<SplineControlPoint>,
5123 pub closed: bool,
5124 pub tension: f32,
5125}
5126
5127impl CameraSplinePath {
5128 pub fn new() -> Self {
5129 CameraSplinePath { control_points: Vec::new(), closed: false, tension: 0.5 }
5130 }
5131
5132 pub fn add_point(&mut self, p: SplineControlPoint) {
5133 self.control_points.push(p);
5134 self.recompute_tangents();
5135 }
5136
5137 fn recompute_tangents(&mut self) {
5138 let n = self.control_points.len();
5139 if n < 2 { return; }
5140 for i in 0..n {
5141 let prev = if i == 0 { if self.closed { n - 1 } else { 0 } } else { i - 1 };
5142 let next = if i == n - 1 { if self.closed { 0 } else { n - 1 } } else { i + 1 };
5143 let dp = self.control_points[next].position - self.control_points[prev].position;
5144 self.control_points[i].tangent_out = dp * self.tension;
5145 self.control_points[i].tangent_in = -dp * self.tension;
5146 }
5147 }
5148
5149 pub fn evaluate(&self, t: f32) -> Vec3 {
5150 let n = self.control_points.len();
5151 if n == 0 { return Vec3::ZERO; }
5152 if n == 1 { return self.control_points[0].position; }
5153 let total_segments = if self.closed { n } else { n - 1 };
5154 let segment_t = (t * total_segments as f32).clamp(0.0, total_segments as f32 - 0.0001);
5155 let seg = segment_t as usize;
5156 let s = segment_t - seg as f32;
5157 let i0 = seg;
5158 let i1 = if self.closed { (seg + 1) % n } else { (seg + 1).min(n - 1) };
5159 let p0 = self.control_points[i0].position;
5160 let p1 = self.control_points[i1].position;
5161 let m0 = self.control_points[i0].tangent_out;
5162 let m1 = self.control_points[i1].tangent_in;
5163 let s2 = s * s;
5164 let s3 = s2 * s;
5165 let h00 = 2.0 * s3 - 3.0 * s2 + 1.0;
5166 let h10 = s3 - 2.0 * s2 + s;
5167 let h01 = -2.0 * s3 + 3.0 * s2;
5168 let h11 = s3 - s2;
5169 h00 * p0 + h10 * m0 + h01 * p1 + h11 * m1
5170 }
5171
5172 pub fn arc_length_approximate(&self, samples: u32) -> f32 {
5173 if samples < 2 { return 0.0; }
5174 let mut total = 0.0;
5175 let mut prev = self.evaluate(0.0);
5176 for i in 1..samples {
5177 let t = i as f32 / (samples - 1) as f32;
5178 let curr = self.evaluate(t);
5179 total += (curr - prev).length();
5180 prev = curr;
5181 }
5182 total
5183 }
5184}
5185
5186#[derive(Debug, Clone, PartialEq)]
5191pub enum SceneNodeKind {
5192 Root,
5193 Group,
5194 Camera,
5195 Light,
5196 Mesh,
5197 SkinnedMesh,
5198 ParticleSystem,
5199 AudioSource,
5200 VfxInstance,
5201 Trigger,
5202 Locator,
5203}
5204
5205#[derive(Debug, Clone)]
5206pub struct SceneGraphNode {
5207 pub id: u32,
5208 pub name: String,
5209 pub kind: SceneNodeKind,
5210 pub parent_id: Option<u32>,
5211 pub children: Vec<u32>,
5212 pub local_position: Vec3,
5213 pub local_rotation: Quat,
5214 pub local_scale: Vec3,
5215 pub visible: bool,
5216 pub static_flag: bool,
5217 pub asset_ref: Option<String>,
5218 pub tags: HashSet<String>,
5219}
5220
5221impl SceneGraphNode {
5222 pub fn new(id: u32, name: &str, kind: SceneNodeKind) -> Self {
5223 SceneGraphNode {
5224 id, name: name.to_string(), kind,
5225 parent_id: None, children: Vec::new(),
5226 local_position: Vec3::ZERO,
5227 local_rotation: Quat::IDENTITY,
5228 local_scale: Vec3::ONE,
5229 visible: true, static_flag: false,
5230 asset_ref: None, tags: HashSet::new(),
5231 }
5232 }
5233
5234 pub fn local_matrix(&self) -> Mat4 {
5235 Mat4::from_scale_rotation_translation(self.local_scale, self.local_rotation, self.local_position)
5236 }
5237
5238 pub fn add_tag(&mut self, tag: &str) {
5239 self.tags.insert(tag.to_string());
5240 }
5241
5242 pub fn has_tag(&self, tag: &str) -> bool {
5243 self.tags.contains(tag)
5244 }
5245}
5246
5247#[derive(Debug, Clone)]
5248pub struct CutsceneSceneGraph {
5249 pub nodes: HashMap<u32, SceneGraphNode>,
5250 pub root_id: u32,
5251 pub next_id: u32,
5252}
5253
5254impl CutsceneSceneGraph {
5255 pub fn new() -> Self {
5256 let mut g = CutsceneSceneGraph { nodes: HashMap::new(), root_id: 0, next_id: 1 };
5257 g.nodes.insert(0, SceneGraphNode::new(0, "Root", SceneNodeKind::Root));
5258 g
5259 }
5260
5261 pub fn alloc_id(&mut self) -> u32 {
5262 let id = self.next_id;
5263 self.next_id += 1;
5264 id
5265 }
5266
5267 pub fn add_node(&mut self, mut node: SceneGraphNode, parent_id: u32) {
5268 node.parent_id = Some(parent_id);
5269 let node_id = node.id;
5270 self.nodes.insert(node_id, node);
5271 if let Some(parent) = self.nodes.get_mut(&parent_id) {
5272 parent.children.push(node_id);
5273 }
5274 }
5275
5276 pub fn world_matrix(&self, id: u32) -> Mat4 {
5277 let node = match self.nodes.get(&id) { Some(n) => n, None => return Mat4::IDENTITY };
5278 let local = node.local_matrix();
5279 match node.parent_id {
5280 Some(pid) => self.world_matrix(pid) * local,
5281 None => local,
5282 }
5283 }
5284
5285 pub fn find_by_tag(&self, tag: &str) -> Vec<u32> {
5286 self.nodes.values().filter(|n| n.has_tag(tag)).map(|n| n.id).collect()
5287 }
5288
5289 pub fn visible_nodes(&self) -> Vec<u32> {
5290 self.nodes.values().filter(|n| n.visible).map(|n| n.id).collect()
5291 }
5292
5293 pub fn node_depth(&self, id: u32) -> u32 {
5294 let mut depth = 0;
5295 let mut current_id = id;
5296 while let Some(node) = self.nodes.get(¤t_id) {
5297 if let Some(pid) = node.parent_id {
5298 depth += 1;
5299 current_id = pid;
5300 } else {
5301 break;
5302 }
5303 }
5304 depth
5305 }
5306}
5307
5308#[derive(Debug, Clone, PartialEq)]
5313pub enum RenderQualityPreset {
5314 Preview,
5315 Medium,
5316 High,
5317 Ultra,
5318 CinematicMaster,
5319}
5320
5321#[derive(Debug, Clone)]
5322pub struct RenderQualitySettings {
5323 pub preset: RenderQualityPreset,
5324 pub resolution_scale: f32,
5325 pub samples_per_pixel: u32,
5326 pub max_ray_depth: u32,
5327 pub shadow_map_size: u32,
5328 pub ao_radius: f32,
5329 pub ao_samples: u32,
5330 pub reflection_quality: u32,
5331 pub motion_blur_samples: u32,
5332 pub dof_bokeh_quality: u32,
5333 pub gi_enabled: bool,
5334 pub gi_bounces: u32,
5335}
5336
5337impl RenderQualitySettings {
5338 pub fn preview() -> Self {
5339 RenderQualitySettings {
5340 preset: RenderQualityPreset::Preview,
5341 resolution_scale: 0.5,
5342 samples_per_pixel: 1,
5343 max_ray_depth: 2,
5344 shadow_map_size: 1024,
5345 ao_radius: 0.5,
5346 ao_samples: 8,
5347 reflection_quality: 1,
5348 motion_blur_samples: 4,
5349 dof_bokeh_quality: 4,
5350 gi_enabled: false,
5351 gi_bounces: 1,
5352 }
5353 }
5354
5355 pub fn cinematic_master() -> Self {
5356 RenderQualitySettings {
5357 preset: RenderQualityPreset::CinematicMaster,
5358 resolution_scale: 1.0,
5359 samples_per_pixel: 64,
5360 max_ray_depth: 16,
5361 shadow_map_size: 8192,
5362 ao_radius: 2.0,
5363 ao_samples: 64,
5364 reflection_quality: 4,
5365 motion_blur_samples: 32,
5366 dof_bokeh_quality: 32,
5367 gi_enabled: true,
5368 gi_bounces: 8,
5369 }
5370 }
5371
5372 pub fn estimated_render_time_factor(&self) -> f32 {
5373 (self.samples_per_pixel as f32)
5374 * (self.ao_samples as f32 / 8.0)
5375 * (self.motion_blur_samples as f32 / 4.0)
5376 * (self.resolution_scale * self.resolution_scale)
5377 * (self.max_ray_depth as f32 / 2.0)
5378 }
5379}
5380
5381#[cfg(test)]
5386mod tests_director {
5387 use super::*;
5388
5389 #[test]
5390 fn test_multi_camera_director_active_at() {
5391 let mut dir = MultiCameraDirector::new();
5392 dir.active_camera_id = 1;
5393 dir.add_cut(DirectorCut {
5394 time: 5.0, from_camera_id: 1, to_camera_id: 2,
5395 cut_type: DirectorCutType::HardCut, transition_duration: 0.0,
5396 notes: String::new(),
5397 });
5398 assert_eq!(dir.active_at(3.0), 1);
5399 assert_eq!(dir.active_at(7.0), 2);
5400 }
5401
5402 #[test]
5403 fn test_spline_path_evaluate() {
5404 let mut path = CameraSplinePath::new();
5405 path.add_point(SplineControlPoint::new(Vec3::ZERO));
5406 path.add_point(SplineControlPoint::new(Vec3::new(10.0, 0.0, 0.0)));
5407 let mid = path.evaluate(0.5);
5408 assert!(mid.x > 0.0 && mid.x < 10.0);
5409 }
5410
5411 #[test]
5412 fn test_mocap_session_visibility() {
5413 let mut session = MocapSession::new("S001", "ActorA", 120.0);
5414 session.marker_labels = vec!["Head".to_string(), "Hip".to_string()];
5415 session.frames.push(ExtMocapFrame {
5416 frame_number: 0, timestamp: 0.0,
5417 markers: vec![
5418 MocapMarker { id: 0, label: "Head".to_string(), position: Vec3::new(0.0, 1.8, 0.0), occluded: false, residual: 0.1 },
5419 MocapMarker { id: 1, label: "Hip".to_string(), position: Vec3::new(0.0, 1.0, 0.0), occluded: true, residual: 0.5 },
5420 ],
5421 });
5422 assert_eq!(session.frames[0].visible_marker_count(), 1);
5423 }
5424
5425 #[test]
5426 fn test_scene_graph_world_matrix() {
5427 let mut graph = CutsceneSceneGraph::new();
5428 let mut child = SceneGraphNode::new(1, "Child", SceneNodeKind::Mesh);
5429 child.local_position = Vec3::new(5.0, 0.0, 0.0);
5430 graph.add_node(child, 0);
5431 let world = graph.world_matrix(1);
5432 let pos = world.col(3).truncate();
5433 assert!((pos.x - 5.0).abs() < 0.01);
5434 }
5435
5436 #[test]
5437 fn test_render_quality_time_factor() {
5438 let preview = RenderQualitySettings::preview();
5439 let master = RenderQualitySettings::cinematic_master();
5440 assert!(master.estimated_render_time_factor() > preview.estimated_render_time_factor() * 10.0);
5441 }
5442
5443 #[test]
5444 fn test_ease_functions() {
5445 assert!((ease_in_quad(0.0)).abs() < 0.001);
5446 assert!((ease_in_quad(1.0) - 1.0).abs() < 0.001);
5447 assert!((ease_out_quad(0.5) - 0.75).abs() < 0.01);
5448 assert!((ease_out_bounce(1.0) - 1.0).abs() < 0.01);
5449 }
5450
5451 #[test]
5452 fn test_audio_mixer_default() {
5453 let mixer = AudioMixer::default_setup();
5454 assert_eq!(mixer.buses.len(), 5);
5455 assert_eq!(mixer.sample_rate, 48000);
5456 }
5457
5458 #[test]
5459 fn test_cutscene_project_summary() {
5460 let mut proj = CutsceneProjectFile::new(120.0, 24.0);
5461 proj.add_camera(CinematicCamera::new(1, "MainCam"));
5462 let summary = proj.summary();
5463 assert!(summary.contains("120.00s"));
5464 assert!(summary.contains("24fps"));
5465 }
5466
5467 #[test]
5468 fn test_lens_library_lookup() {
5469 let lib = LensLibrary::default_library();
5470 let lenses = lib.find_by_focal_length(35.0);
5471 assert!(!lenses.is_empty());
5472 }
5473}
5474
5475pub const EASE_TABLE_SIZE: usize = 512;
5480
5481pub struct EaseTable {
5482 pub values: Vec<f32>,
5483 pub ease_fn_name: String,
5484}
5485
5486impl EaseTable {
5487 pub fn build(name: &str, f: fn(f32) -> f32) -> Self {
5488 let values = (0..=EASE_TABLE_SIZE)
5489 .map(|i| f(i as f32 / EASE_TABLE_SIZE as f32))
5490 .collect();
5491 EaseTable { values, ease_fn_name: name.to_string() }
5492 }
5493
5494 pub fn sample(&self, t: f32) -> f32 {
5495 let idx = (t * EASE_TABLE_SIZE as f32).clamp(0.0, EASE_TABLE_SIZE as f32) as usize;
5496 let idx = idx.min(EASE_TABLE_SIZE);
5497 self.values[idx]
5498 }
5499}
5500
5501#[derive(Debug, Clone, PartialEq)]
5506pub enum AssetImportStatus {
5507 Pending,
5508 Importing,
5509 Complete,
5510 Failed,
5511 Stale,
5512}
5513
5514#[derive(Debug, Clone)]
5515pub struct CutsceneAssetRef {
5516 pub asset_id: String,
5517 pub asset_type: String,
5518 pub source_path: String,
5519 pub import_status: AssetImportStatus,
5520 pub size_bytes: u64,
5521 pub last_modified: u64,
5522 pub dependencies: Vec<String>,
5523}
5524
5525impl CutsceneAssetRef {
5526 pub fn new(asset_id: &str, asset_type: &str, source_path: &str) -> Self {
5527 CutsceneAssetRef {
5528 asset_id: asset_id.to_string(),
5529 asset_type: asset_type.to_string(),
5530 source_path: source_path.to_string(),
5531 import_status: AssetImportStatus::Pending,
5532 size_bytes: 0,
5533 last_modified: 0,
5534 dependencies: Vec::new(),
5535 }
5536 }
5537
5538 pub fn is_ready(&self) -> bool {
5539 self.import_status == AssetImportStatus::Complete
5540 }
5541}
5542
5543#[derive(Debug, Clone)]
5544pub struct CutsceneAssetRegistry {
5545 pub assets: HashMap<String, CutsceneAssetRef>,
5546 pub failed_imports: Vec<String>,
5547}
5548
5549impl CutsceneAssetRegistry {
5550 pub fn new() -> Self {
5551 CutsceneAssetRegistry { assets: HashMap::new(), failed_imports: Vec::new() }
5552 }
5553
5554 pub fn register(&mut self, asset: CutsceneAssetRef) {
5555 self.assets.insert(asset.asset_id.clone(), asset);
5556 }
5557
5558 pub fn mark_complete(&mut self, id: &str) {
5559 if let Some(a) = self.assets.get_mut(id) {
5560 a.import_status = AssetImportStatus::Complete;
5561 }
5562 }
5563
5564 pub fn mark_failed(&mut self, id: &str) {
5565 if let Some(a) = self.assets.get_mut(id) {
5566 a.import_status = AssetImportStatus::Failed;
5567 }
5568 self.failed_imports.push(id.to_string());
5569 }
5570
5571 pub fn ready_asset_ids(&self) -> Vec<&str> {
5572 self.assets.values()
5573 .filter(|a| a.is_ready())
5574 .map(|a| a.asset_id.as_str())
5575 .collect()
5576 }
5577
5578 pub fn total_size_bytes(&self) -> u64 {
5579 self.assets.values().map(|a| a.size_bytes).sum()
5580 }
5581}
5582
5583#[derive(Debug, Clone, PartialEq)]
5588pub enum PlaybackState {
5589 Stopped,
5590 Playing,
5591 Paused,
5592 Scrubbing,
5593 Rendering,
5594}
5595
5596#[derive(Debug, Clone)]
5597pub struct PlaybackEngine {
5598 pub state: PlaybackState,
5599 pub current_time: f32,
5600 pub duration: f32,
5601 pub playback_rate: f32,
5602 pub loop_enabled: bool,
5603 pub loop_start: f32,
5604 pub loop_end: f32,
5605 pub frame_rate: f32,
5606 pub audio_sync: bool,
5607}
5608
5609impl PlaybackEngine {
5610 pub fn new(duration: f32, fps: f32) -> Self {
5611 PlaybackEngine {
5612 state: PlaybackState::Stopped,
5613 current_time: 0.0,
5614 duration,
5615 playback_rate: 1.0,
5616 loop_enabled: false,
5617 loop_start: 0.0,
5618 loop_end: duration,
5619 frame_rate: fps,
5620 audio_sync: true,
5621 }
5622 }
5623
5624 pub fn play(&mut self) {
5625 self.state = PlaybackState::Playing;
5626 }
5627
5628 pub fn pause(&mut self) {
5629 if self.state == PlaybackState::Playing {
5630 self.state = PlaybackState::Paused;
5631 }
5632 }
5633
5634 pub fn stop(&mut self) {
5635 self.state = PlaybackState::Stopped;
5636 self.current_time = 0.0;
5637 }
5638
5639 pub fn seek(&mut self, t: f32) {
5640 self.current_time = t.clamp(0.0, self.duration);
5641 }
5642
5643 pub fn advance(&mut self, dt: f32) {
5644 if self.state != PlaybackState::Playing { return; }
5645 self.current_time += dt * self.playback_rate;
5646 if self.loop_enabled && self.current_time >= self.loop_end {
5647 self.current_time = self.loop_start;
5648 } else if self.current_time >= self.duration {
5649 self.current_time = self.duration;
5650 self.state = PlaybackState::Stopped;
5651 }
5652 }
5653
5654 pub fn current_frame(&self) -> u32 {
5655 (self.current_time * self.frame_rate) as u32
5656 }
5657
5658 pub fn progress(&self) -> f32 {
5659 if self.duration <= 0.0 { return 0.0; }
5660 self.current_time / self.duration
5661 }
5662
5663 pub fn time_remaining(&self) -> f32 {
5664 (self.duration - self.current_time).max(0.0)
5665 }
5666}
5667
5668#[derive(Debug, Clone, PartialEq)]
5673pub enum VideoCodec {
5674 H264,
5675 H265,
5676 ProRes422,
5677 ProRes4444,
5678 DNxHD,
5679 DNxHR,
5680 AV1,
5681 VP9,
5682 Uncompressed,
5683}
5684
5685#[derive(Debug, Clone, PartialEq)]
5686pub enum AudioCodec {
5687 AAC,
5688 PCM24,
5689 PCM32F,
5690 OPUS,
5691 FLAC,
5692}
5693
5694#[derive(Debug, Clone)]
5695pub struct ExportPipelineConfig {
5696 pub output_path: String,
5697 pub video_codec: VideoCodec,
5698 pub audio_codec: AudioCodec,
5699 pub width: u32,
5700 pub height: u32,
5701 pub frame_rate: f32,
5702 pub video_bitrate_kbps: u32,
5703 pub audio_bitrate_kbps: u32,
5704 pub audio_sample_rate: u32,
5705 pub start_frame: u32,
5706 pub end_frame: u32,
5707 pub include_alpha: bool,
5708 pub hdr_enabled: bool,
5709 pub color_space: String,
5710 pub lut_path: Option<String>,
5711}
5712
5713impl ExportPipelineConfig {
5714 pub fn web_preview() -> Self {
5715 ExportPipelineConfig {
5716 output_path: "preview.mp4".to_string(),
5717 video_codec: VideoCodec::H264,
5718 audio_codec: AudioCodec::AAC,
5719 width: 1280, height: 720,
5720 frame_rate: 30.0,
5721 video_bitrate_kbps: 4000,
5722 audio_bitrate_kbps: 192,
5723 audio_sample_rate: 48000,
5724 start_frame: 0, end_frame: u32::MAX,
5725 include_alpha: false,
5726 hdr_enabled: false,
5727 color_space: "sRGB".to_string(),
5728 lut_path: None,
5729 }
5730 }
5731
5732 pub fn broadcast_master() -> Self {
5733 ExportPipelineConfig {
5734 output_path: "master.mov".to_string(),
5735 video_codec: VideoCodec::ProRes4444,
5736 audio_codec: AudioCodec::PCM24,
5737 width: 3840, height: 2160,
5738 frame_rate: 24.0,
5739 video_bitrate_kbps: 800000,
5740 audio_bitrate_kbps: 2304,
5741 audio_sample_rate: 48000,
5742 start_frame: 0, end_frame: u32::MAX,
5743 include_alpha: true,
5744 hdr_enabled: true,
5745 color_space: "ACEScg".to_string(),
5746 lut_path: Some("aces_rrt.cube".to_string()),
5747 }
5748 }
5749
5750 pub fn total_frames(&self, fps: f32) -> u32 {
5751 self.end_frame.saturating_sub(self.start_frame)
5752 }
5753
5754 pub fn estimated_file_size_mb(&self, duration_s: f32) -> f32 {
5755 (self.video_bitrate_kbps as f32 + self.audio_bitrate_kbps as f32)
5756 * duration_s / 8.0 / 1024.0
5757 }
5758}
5759
5760#[derive(Debug, Clone)]
5761pub struct ExportJob {
5762 pub job_id: String,
5763 pub config: ExportPipelineConfig,
5764 pub progress: f32,
5765 pub frames_rendered: u32,
5766 pub total_frames: u32,
5767 pub elapsed_seconds: f32,
5768 pub errors: Vec<String>,
5769 pub complete: bool,
5770}
5771
5772impl ExportJob {
5773 pub fn new(job_id: &str, config: ExportPipelineConfig, total_frames: u32) -> Self {
5774 ExportJob {
5775 job_id: job_id.to_string(),
5776 config,
5777 progress: 0.0,
5778 frames_rendered: 0,
5779 total_frames,
5780 elapsed_seconds: 0.0,
5781 errors: Vec::new(),
5782 complete: false,
5783 }
5784 }
5785
5786 pub fn advance_frame(&mut self) {
5787 self.frames_rendered += 1;
5788 self.progress = self.frames_rendered as f32 / self.total_frames.max(1) as f32;
5789 if self.frames_rendered >= self.total_frames {
5790 self.complete = true;
5791 }
5792 }
5793
5794 pub fn fps_current(&self) -> f32 {
5795 if self.elapsed_seconds <= 0.0 { return 0.0; }
5796 self.frames_rendered as f32 / self.elapsed_seconds
5797 }
5798
5799 pub fn eta_seconds(&self) -> f32 {
5800 let fps = self.fps_current();
5801 if fps <= 0.0 { return f32::INFINITY; }
5802 let remaining = self.total_frames - self.frames_rendered;
5803 remaining as f32 / fps
5804 }
5805}
5806
5807pub fn cutscene_module_version() -> &'static str { "3.1.0" }
5808pub fn cutscene_feature_flags() -> &'static [&'static str] {
5809 &["camera_anim", "subtitles", "narrative", "blend_tree", "audio_mix",
5810 "mocap", "spline_dolly", "scene_graph", "export_pipeline", "retargeting"]
5811}
5812
5813
5814#[derive(Debug, Clone)]
5819pub struct ColorGradingLUT {
5820 pub name: String,
5821 pub size: u32,
5822 pub data: Vec<Vec3>,
5823}
5824
5825impl ColorGradingLUT {
5826 pub fn new_identity(size: u32) -> Self {
5827 let mut data = Vec::with_capacity((size * size * size) as usize);
5828 for b in 0..size {
5829 for g in 0..size {
5830 for r in 0..size {
5831 data.push(Vec3::new(
5832 r as f32 / (size - 1) as f32,
5833 g as f32 / (size - 1) as f32,
5834 b as f32 / (size - 1) as f32,
5835 ));
5836 }
5837 }
5838 }
5839 ColorGradingLUT { name: "Identity".to_string(), size, data }
5840 }
5841
5842 pub fn apply(&self, color: Vec3) -> Vec3 {
5843 let s = (self.size - 1) as f32;
5844 let r = (color.x * s).clamp(0.0, s);
5845 let g = (color.y * s).clamp(0.0, s);
5846 let b = (color.z * s).clamp(0.0, s);
5847 let ri = r as usize; let gi = g as usize; let bi = b as usize;
5848 let ri = ri.min(self.size as usize - 2);
5849 let gi = gi.min(self.size as usize - 2);
5850 let bi = bi.min(self.size as usize - 2);
5851 let fr = r - ri as f32; let fg = g - gi as f32; let fb = b - bi as f32;
5852 let s2 = self.size as usize;
5853 let idx = |r2: usize, g2: usize, b2: usize| b2 * s2 * s2 + g2 * s2 + r2;
5854 let c000 = self.data[idx(ri, gi, bi)];
5855 let c100 = self.data[idx(ri+1, gi, bi)];
5856 let c010 = self.data[idx(ri, gi+1, bi)];
5857 let c110 = self.data[idx(ri+1, gi+1, bi)];
5858 let c001 = self.data[idx(ri, gi, bi+1)];
5859 let c101 = self.data[idx(ri+1, gi, bi+1)];
5860 let c011 = self.data[idx(ri, gi+1, bi+1)];
5861 let c111 = self.data[idx(ri+1, gi+1, bi+1)];
5862 let c00 = c000.lerp(c100, fr);
5863 let c10 = c010.lerp(c110, fr);
5864 let c01 = c001.lerp(c101, fr);
5865 let c11 = c011.lerp(c111, fr);
5866 let c0 = c00.lerp(c10, fg);
5867 let c1 = c01.lerp(c11, fg);
5868 c0.lerp(c1, fb)
5869 }
5870}
5871
5872#[derive(Debug, Clone)]
5873pub struct ToneMapper {
5874 pub exposure: f32,
5875 pub gamma: f32,
5876 pub white_point: f32,
5877}
5878
5879impl ToneMapper {
5880 pub fn new() -> Self {
5881 ToneMapper { exposure: 1.0, gamma: 2.2, white_point: 11.2 }
5882 }
5883
5884 pub fn reinhard(&self, color: Vec3) -> Vec3 {
5885 let c = color * self.exposure;
5886 Vec3::new(c.x / (1.0 + c.x), c.y / (1.0 + c.y), c.z / (1.0 + c.z))
5887 }
5888
5889 pub fn aces(&self, color: Vec3) -> Vec3 {
5890 let c = color * self.exposure;
5891 let a = 2.51_f32; let b = 0.03_f32; let cc = 2.43_f32; let d = 0.59_f32; let e = 0.14_f32;
5892 let tone = |x: f32| ((x * (a * x + b)) / (x * (cc * x + d) + e)).clamp(0.0, 1.0);
5893 Vec3::new(tone(c.x), tone(c.y), tone(c.z))
5894 }
5895
5896 pub fn filmic(&self, color: Vec3) -> Vec3 {
5897 let c = (color * self.exposure).max(Vec3::ZERO);
5898 let w = self.white_point;
5899 let filmic_f = |x: f32| -> f32 {
5900 let a = 0.22_f32; let b = 0.30_f32; let cc = 0.10_f32;
5901 let d = 0.20_f32; let ee = 0.01_f32; let f = 0.30_f32;
5902 ((x * (a * x + cc * b) + d * ee) / (x * (a * x + b) + d * f)) - ee / f
5903 };
5904 let white = filmic_f(w);
5905 Vec3::new(filmic_f(c.x) / white, filmic_f(c.y) / white, filmic_f(c.z) / white)
5906 }
5907
5908 pub fn gamma_correct(&self, linear: Vec3) -> Vec3 {
5909 let g = 1.0 / self.gamma;
5910 Vec3::new(linear.x.powf(g), linear.y.powf(g), linear.z.powf(g))
5911 }
5912}
5913
5914pub const CUBEMAP_FACE_COUNT: usize = 6;
5919
5920#[derive(Debug, Clone, PartialEq)]
5921pub enum CubemapFace {
5922 PosX, NegX, PosY, NegY, PosZ, NegZ,
5923}
5924
5925#[derive(Debug, Clone)]
5926pub struct CubemapAsset {
5927 pub name: String,
5928 pub face_resolution: u32,
5929 pub mip_levels: u32,
5930 pub hdr: bool,
5931 pub source_path: String,
5932}
5933
5934impl CubemapAsset {
5935 pub fn new(name: &str, res: u32, hdr: bool) -> Self {
5936 CubemapAsset {
5937 name: name.to_string(), face_resolution: res,
5938 mip_levels: (res as f32).log2() as u32 + 1,
5939 hdr, source_path: String::new(),
5940 }
5941 }
5942
5943 pub fn total_texels(&self) -> u64 {
5944 let mut total = 0u64;
5945 let mut res = self.face_resolution;
5946 for _ in 0..self.mip_levels {
5947 total += (res as u64) * (res as u64) * CUBEMAP_FACE_COUNT as u64;
5948 res = (res / 2).max(1);
5949 }
5950 total
5951 }
5952}
5953
5954#[derive(Debug, Clone)]
5959pub struct BiQuadFilter {
5960 pub b0: f32, pub b1: f32, pub b2: f32,
5961 pub a1: f32, pub a2: f32,
5962 x1: f32, x2: f32, y1: f32, y2: f32,
5963}
5964
5965impl BiQuadFilter {
5966 pub fn low_pass(sample_rate: f32, cutoff: f32, q: f32) -> Self {
5967 let w0 = 2.0 * std::f32::consts::PI * cutoff / sample_rate;
5968 let alpha = w0.sin() / (2.0 * q);
5969 let cos_w0 = w0.cos();
5970 let b1 = 1.0 - cos_w0;
5971 let b0 = b1 / 2.0;
5972 let b2 = b0;
5973 let a0 = 1.0 + alpha;
5974 let a1 = -2.0 * cos_w0;
5975 let a2 = 1.0 - alpha;
5976 BiQuadFilter { b0: b0/a0, b1: b1/a0, b2: b2/a0, a1: a1/a0, a2: a2/a0, x1: 0.0, x2: 0.0, y1: 0.0, y2: 0.0 }
5977 }
5978
5979 pub fn high_pass(sample_rate: f32, cutoff: f32, q: f32) -> Self {
5980 let w0 = 2.0 * std::f32::consts::PI * cutoff / sample_rate;
5981 let alpha = w0.sin() / (2.0 * q);
5982 let cos_w0 = w0.cos();
5983 let b0 = (1.0 + cos_w0) / 2.0;
5984 let b1 = -(1.0 + cos_w0);
5985 let b2 = b0;
5986 let a0 = 1.0 + alpha;
5987 let a1 = -2.0 * cos_w0;
5988 let a2 = 1.0 - alpha;
5989 BiQuadFilter { b0: b0/a0, b1: b1/a0, b2: b2/a0, a1: a1/a0, a2: a2/a0, x1: 0.0, x2: 0.0, y1: 0.0, y2: 0.0 }
5990 }
5991
5992 pub fn process(&mut self, x: f32) -> f32 {
5993 let y = self.b0 * x + self.b1 * self.x1 + self.b2 * self.x2 - self.a1 * self.y1 - self.a2 * self.y2;
5994 self.x2 = self.x1; self.x1 = x;
5995 self.y2 = self.y1; self.y1 = y;
5996 y
5997 }
5998}
5999
6000#[derive(Debug, Clone)]
6005pub struct ParticleCurvePoint {
6006 pub time: f32,
6007 pub value: f32,
6008}
6009
6010#[derive(Debug, Clone)]
6011pub struct ParticleCurve {
6012 pub name: String,
6013 pub points: Vec<ParticleCurvePoint>,
6014 pub loop_curve: bool,
6015}
6016
6017impl ParticleCurve {
6018 pub fn new(name: &str) -> Self {
6019 ParticleCurve { name: name.to_string(), points: Vec::new(), loop_curve: false }
6020 }
6021
6022 pub fn flat(name: &str, value: f32) -> Self {
6023 let mut c = ParticleCurve::new(name);
6024 c.points.push(ParticleCurvePoint { time: 0.0, value });
6025 c.points.push(ParticleCurvePoint { time: 1.0, value });
6026 c
6027 }
6028
6029 pub fn ramp_up(name: &str) -> Self {
6030 let mut c = ParticleCurve::new(name);
6031 c.points.push(ParticleCurvePoint { time: 0.0, value: 0.0 });
6032 c.points.push(ParticleCurvePoint { time: 1.0, value: 1.0 });
6033 c
6034 }
6035
6036 pub fn evaluate(&self, t: f32) -> f32 {
6037 if self.points.is_empty() { return 0.0; }
6038 if self.points.len() == 1 { return self.points[0].value; }
6039 let t = if self.loop_curve { t.fract() } else { t.clamp(0.0, 1.0) };
6040 let idx = self.points.partition_point(|p| p.time <= t);
6041 if idx == 0 { return self.points[0].value; }
6042 if idx >= self.points.len() { return self.points.last().unwrap().value; }
6043 let a = &self.points[idx - 1];
6044 let b = &self.points[idx];
6045 let s = (t - a.time) / (b.time - a.time).max(0.001);
6046 a.value + (b.value - a.value) * s
6047 }
6048}
6049
6050#[cfg(test)]
6055mod tests_cutscene_final {
6056 use super::*;
6057
6058 #[test]
6059 fn test_lut_identity() {
6060 let lut = ColorGradingLUT::new_identity(4);
6061 let color = Vec3::new(0.5, 0.3, 0.8);
6062 let result = lut.apply(color);
6063 assert!((result.x - color.x).abs() < 0.1);
6064 assert!((result.y - color.y).abs() < 0.1);
6065 }
6066
6067 #[test]
6068 fn test_tonemapper_reinhard() {
6069 let tm = ToneMapper::new();
6070 let bright = Vec3::new(2.0, 1.5, 0.5);
6071 let result = tm.reinhard(bright);
6072 assert!(result.x < 1.0 && result.y < 1.0 && result.z <= 1.0);
6073 }
6074
6075 #[test]
6076 fn test_biquad_lowpass() {
6077 let mut filt = BiQuadFilter::low_pass(44100.0, 1000.0, 0.707);
6078 let y = filt.process(1.0);
6079 assert!(y.is_finite());
6080 let y2 = filt.process(0.0);
6081 assert!(y2.is_finite());
6082 }
6083
6084 #[test]
6085 fn test_particle_curve_evaluate() {
6086 let curve = ParticleCurve::ramp_up("alpha");
6087 assert!((curve.evaluate(0.0)).abs() < 0.001);
6088 assert!((curve.evaluate(1.0) - 1.0).abs() < 0.001);
6089 assert!((curve.evaluate(0.5) - 0.5).abs() < 0.001);
6090 }
6091
6092 #[test]
6093 fn test_cubemap_total_texels() {
6094 let cube = CubemapAsset::new("SkyHDR", 512, true);
6095 let total = cube.total_texels();
6096 assert!(total > 512 * 512 * 6);
6097 }
6098
6099 #[test]
6100 fn test_playback_engine() {
6101 let mut engine = PlaybackEngine::new(10.0, 24.0);
6102 engine.play();
6103 engine.advance(5.0);
6104 assert!((engine.current_time - 5.0).abs() < 0.001);
6105 assert_eq!(engine.current_frame(), 120);
6106 engine.advance(6.0);
6107 assert_eq!(engine.state, PlaybackState::Stopped);
6108 }
6109
6110 #[test]
6111 fn test_export_job_progress() {
6112 let config = ExportPipelineConfig::web_preview();
6113 let mut job = ExportJob::new("job001", config, 100);
6114 for _ in 0..50 { job.advance_frame(); }
6115 assert!((job.progress - 0.5).abs() < 0.01);
6116 assert!(!job.complete);
6117 for _ in 0..50 { job.advance_frame(); }
6118 assert!(job.complete);
6119 }
6120
6121 #[test]
6122 fn test_asset_registry() {
6123 let mut reg = CutsceneAssetRegistry::new();
6124 reg.register(CutsceneAssetRef::new("mesh_001", "Mesh", "assets/hero.glb"));
6125 reg.mark_complete("mesh_001");
6126 assert_eq!(reg.ready_asset_ids().len(), 1);
6127 reg.register(CutsceneAssetRef::new("tex_002", "Texture", "assets/hero_diff.png"));
6128 reg.mark_failed("tex_002");
6129 assert_eq!(reg.failed_imports.len(), 1);
6130 }
6131}
6132
6133pub const CUTSCENE_EDITOR_VERSION: &str = "3.1.0";
6135pub const CUTSCENE_MAX_CAMERAS: u32 = 32;
6136pub const CUTSCENE_MAX_LAYERS: u32 = 64;
6137pub const CUTSCENE_MAX_EVENTS: u32 = 4096;
6138pub const CUTSCENE_MAX_SUBTITLES_PER_LANG: u32 = 2048;
6139
6140
6141#[derive(Debug, Clone)]
6146pub struct WatchEvent {
6147 pub timestamp_ms: u64,
6148 pub event_type: String,
6149 pub time_in_cutscene: f32,
6150 pub platform: String,
6151 pub session_id: String,
6152}
6153
6154#[derive(Debug, Clone)]
6155pub struct CutsceneAnalytics {
6156 pub cutscene_id: String,
6157 pub total_plays: u64,
6158 pub total_skips: u64,
6159 pub avg_watch_time_seconds: f32,
6160 pub completion_rate: f32,
6161 pub skip_points: Vec<f32>,
6162 pub watch_events: VecDeque<WatchEvent>,
6163 pub max_events: usize,
6164}
6165
6166impl CutsceneAnalytics {
6167 pub fn new(cutscene_id: &str) -> Self {
6168 CutsceneAnalytics {
6169 cutscene_id: cutscene_id.to_string(),
6170 total_plays: 0, total_skips: 0,
6171 avg_watch_time_seconds: 0.0,
6172 completion_rate: 0.0,
6173 skip_points: Vec::new(),
6174 watch_events: VecDeque::new(),
6175 max_events: 1000,
6176 }
6177 }
6178
6179 pub fn record_play(&mut self, watch_time_s: f32, skipped: bool, skip_at: Option<f32>) {
6180 self.total_plays += 1;
6181 if skipped {
6182 self.total_skips += 1;
6183 if let Some(t) = skip_at { self.skip_points.push(t); }
6184 }
6185 let alpha = 1.0 / self.total_plays as f32;
6186 self.avg_watch_time_seconds = self.avg_watch_time_seconds * (1.0 - alpha) + watch_time_s * alpha;
6187 self.completion_rate = (self.total_plays - self.total_skips) as f32 / self.total_plays as f32;
6188 }
6189
6190 pub fn add_event(&mut self, event: WatchEvent) {
6191 if self.watch_events.len() >= self.max_events {
6192 self.watch_events.pop_front();
6193 }
6194 self.watch_events.push_back(event);
6195 }
6196
6197 pub fn skip_rate(&self) -> f32 {
6198 if self.total_plays == 0 { return 0.0; }
6199 self.total_skips as f32 / self.total_plays as f32
6200 }
6201
6202 pub fn most_common_skip_time(&self) -> Option<f32> {
6203 if self.skip_points.is_empty() { return None; }
6204 let mut sorted = self.skip_points.clone();
6205 sorted.sort_by(|a, b| a.partial_cmp(b).unwrap());
6206 let mid = sorted.len() / 2;
6207 Some(sorted[mid])
6208 }
6209}
6210
6211#[derive(Debug, Clone)]
6216pub struct CreditEntry {
6217 pub role: String,
6218 pub name: String,
6219 pub company: Option<String>,
6220}
6221
6222#[derive(Debug, Clone)]
6223pub struct CutsceneCredits {
6224 pub entries: Vec<CreditEntry>,
6225 pub duration_seconds: f32,
6226 pub font_size: f32,
6227 pub scroll_speed: f32,
6228}
6229
6230impl CutsceneCredits {
6231 pub fn new(duration: f32) -> Self {
6232 CutsceneCredits { entries: Vec::new(), duration_seconds: duration, font_size: 24.0, scroll_speed: 50.0 }
6233 }
6234
6235 pub fn add_credit(&mut self, role: &str, name: &str) {
6236 self.entries.push(CreditEntry { role: role.to_string(), name: name.to_string(), company: None });
6237 }
6238
6239 pub fn entry_count(&self) -> usize { self.entries.len() }
6240
6241 pub fn entries_for_role(&self, role: &str) -> Vec<&CreditEntry> {
6242 self.entries.iter().filter(|e| e.role == role).collect()
6243 }
6244}
6245
6246#[derive(Debug, Clone)]
6251pub struct MocapSolverSettings {
6252 pub smoothing_window: u32,
6253 pub fill_gaps_enabled: bool,
6254 pub max_gap_frames: u32,
6255 pub outlier_threshold_sigma: f32,
6256 pub global_scale: f32,
6257 pub coordinate_system_flip: Vec3,
6258}
6259
6260impl Default for MocapSolverSettings {
6261 fn default() -> Self {
6262 MocapSolverSettings {
6263 smoothing_window: 3, fill_gaps_enabled: true, max_gap_frames: 10,
6264 outlier_threshold_sigma: 3.0, global_scale: 1.0,
6265 coordinate_system_flip: Vec3::new(1.0, 1.0, -1.0),
6266 }
6267 }
6268}
6269
6270pub fn smooth_positions(positions: &[Vec3], window: u32) -> Vec<Vec3> {
6271 if positions.is_empty() || window == 0 { return positions.to_vec(); }
6272 let n = positions.len();
6273 let w = window as usize;
6274 let mut result = Vec::with_capacity(n);
6275 for i in 0..n {
6276 let start = i.saturating_sub(w);
6277 let end = (i + w + 1).min(n);
6278 let count = end - start;
6279 let sum = positions[start..end].iter().fold(Vec3::ZERO, |acc, &p| acc + p);
6280 result.push(sum / count as f32);
6281 }
6282 result
6283}
6284
6285pub fn detect_outliers(positions: &[Vec3]) -> Vec<bool> {
6286 if positions.len() < 3 { return vec![false; positions.len()]; }
6287 let n = positions.len();
6288 let mean = positions.iter().fold(Vec3::ZERO, |a, &b| a + b) / n as f32;
6289 let variance: f32 = positions.iter().map(|p| (*p - mean).length_squared()).sum::<f32>() / n as f32;
6290 let std_dev = variance.sqrt();
6291 positions.iter().map(|p| (*p - mean).length() > std_dev * 3.0).collect()
6292}
6293
6294#[cfg(test)]
6299mod tests_cutscene_analytics {
6300 use super::*;
6301
6302 #[test]
6303 fn test_analytics_play_tracking() {
6304 let mut analytics = CutsceneAnalytics::new("cs_intro");
6305 analytics.record_play(45.0, false, None);
6306 analytics.record_play(20.0, true, Some(20.0));
6307 analytics.record_play(45.0, false, None);
6308 assert_eq!(analytics.total_plays, 3);
6309 assert_eq!(analytics.total_skips, 1);
6310 assert!((analytics.skip_rate() - 1.0 / 3.0).abs() < 0.01);
6311 assert!(analytics.completion_rate > 0.5);
6312 }
6313
6314 #[test]
6315 fn test_credits() {
6316 let mut credits = CutsceneCredits::new(30.0);
6317 credits.add_credit("Director", "Alice Smith");
6318 credits.add_credit("Lead Programmer", "Bob Jones");
6319 credits.add_credit("Director", "Carol Brown");
6320 assert_eq!(credits.entries_for_role("Director").len(), 2);
6321 }
6322
6323 #[test]
6324 fn test_smooth_positions() {
6325 let pos = vec![Vec3::new(0.0, 0.0, 0.0), Vec3::new(10.0, 0.0, 0.0), Vec3::new(20.0, 0.0, 0.0)];
6326 let smoothed = smooth_positions(&pos, 1);
6327 assert_eq!(smoothed.len(), 3);
6328 assert!(smoothed[1].x > 0.0 && smoothed[1].x < 20.0);
6329 }
6330
6331 #[test]
6332 fn test_detect_outliers() {
6333 let mut pos: Vec<Vec3> = (0..10).map(|i| Vec3::new(i as f32, 0.0, 0.0)).collect();
6334 pos.push(Vec3::new(10000.0, 0.0, 0.0));
6335 let outliers = detect_outliers(&pos);
6336 assert!(outliers.last().unwrap());
6337 }
6338
6339 #[test]
6340 fn test_director_transitions() {
6341 let mut dir = MultiCameraDirector::new();
6342 dir.active_camera_id = 1;
6343 for i in 0..5 {
6344 dir.add_cut(DirectorCut {
6345 time: i as f32 * 10.0, from_camera_id: i, to_camera_id: i + 1,
6346 cut_type: DirectorCutType::HardCut, transition_duration: 0.0, notes: String::new(),
6347 });
6348 }
6349 assert_eq!(dir.cuts_in_range(0.0, 50.0).len(), 5);
6350 }
6351}
6352
6353pub const CUTSCENE_COMPLETE: bool = true;
6354pub const CUTSCENE_LINE_TARGET: u32 = 7000;
6355pub const CUTSCENE_BUILD_NUMBER: u32 = 310;
6356
6357#[derive(Debug, Clone)]
6362pub struct SequenceValidator {
6363 pub cutscene_id: String,
6364 pub errors: Vec<String>,
6365 pub warnings: Vec<String>,
6366}
6367
6368impl SequenceValidator {
6369 pub fn new(cutscene_id: &str) -> Self {
6370 SequenceValidator { cutscene_id: cutscene_id.to_string(), errors: Vec::new(), warnings: Vec::new() }
6371 }
6372
6373 pub fn check_camera_count(&mut self, count: usize) {
6374 if count == 0 {
6375 self.errors.push("No cameras defined in cutscene".to_string());
6376 }
6377 }
6378
6379 pub fn check_duration(&mut self, duration: f32) {
6380 if duration <= 0.0 {
6381 self.errors.push("Cutscene duration must be positive".to_string());
6382 }
6383 if duration > 600.0 {
6384 self.warnings.push("Cutscene duration exceeds 10 minutes".to_string());
6385 }
6386 }
6387
6388 pub fn check_audio_sync(&mut self, audio_tracks: usize, duration: f32) {
6389 if audio_tracks == 0 && duration > 5.0 {
6390 self.warnings.push("Long cutscene has no audio".to_string());
6391 }
6392 }
6393
6394 pub fn is_valid(&self) -> bool { self.errors.is_empty() }
6395
6396 pub fn report(&self) -> String {
6397 let mut out = format!("Validation for '{}': ", self.cutscene_id);
6398 if self.is_valid() {
6399 out.push_str("PASSED");
6400 } else {
6401 out.push_str(&format!("FAILED ({} errors)", self.errors.len()));
6402 }
6403 if !self.warnings.is_empty() {
6404 out.push_str(&format!(", {} warnings", self.warnings.len()));
6405 }
6406 out
6407 }
6408}
6409
6410#[derive(Debug, Clone)]
6411pub struct CutsceneTemplate {
6412 pub template_name: String,
6413 pub description: String,
6414 pub camera_count: u32,
6415 pub expected_duration_s: f32,
6416 pub required_actors: Vec<String>,
6417 pub required_props: Vec<String>,
6418 pub shot_list_template: Vec<String>,
6419}
6420
6421impl CutsceneTemplate {
6422 pub fn dialogue_two_shot() -> Self {
6423 CutsceneTemplate {
6424 template_name: "Dialogue Two-Shot".to_string(),
6425 description: "Standard two-character dialogue scene".to_string(),
6426 camera_count: 3,
6427 expected_duration_s: 30.0,
6428 required_actors: vec!["Character A".to_string(), "Character B".to_string()],
6429 required_props: Vec::new(),
6430 shot_list_template: vec![
6431 "Wide establishing shot".to_string(),
6432 "Character A medium close-up".to_string(),
6433 "Character B medium close-up".to_string(),
6434 "Over-the-shoulder reaction shots".to_string(),
6435 ],
6436 }
6437 }
6438
6439 pub fn action_sequence() -> Self {
6440 CutsceneTemplate {
6441 template_name: "Action Sequence".to_string(),
6442 description: "High-intensity action scene".to_string(),
6443 camera_count: 5,
6444 expected_duration_s: 60.0,
6445 required_actors: vec!["Hero".to_string()],
6446 required_props: vec!["Weapon".to_string(), "VFX Rig".to_string()],
6447 shot_list_template: vec![
6448 "Wide hero entry".to_string(),
6449 "Fast cut combat medium".to_string(),
6450 "Extreme close-up reaction".to_string(),
6451 "Low angle power shot".to_string(),
6452 "Epic wide finale".to_string(),
6453 ],
6454 }
6455 }
6456}
6457
6458#[cfg(test)]
6459mod tests_cutscene_validation {
6460 use super::*;
6461
6462 #[test]
6463 fn test_sequence_validator_pass() {
6464 let mut v = SequenceValidator::new("cs_001");
6465 v.check_camera_count(3);
6466 v.check_duration(45.0);
6467 v.check_audio_sync(2, 45.0);
6468 assert!(v.is_valid());
6469 assert!(v.warnings.is_empty());
6470 }
6471
6472 #[test]
6473 fn test_sequence_validator_fail() {
6474 let mut v = SequenceValidator::new("cs_002");
6475 v.check_camera_count(0);
6476 v.check_duration(-1.0);
6477 assert!(!v.is_valid());
6478 assert_eq!(v.errors.len(), 2);
6479 }
6480
6481 #[test]
6482 fn test_cutscene_template_shots() {
6483 let template = CutsceneTemplate::dialogue_two_shot();
6484 assert_eq!(template.shot_list_template.len(), 4);
6485 assert_eq!(template.camera_count, 3);
6486 }
6487
6488 #[test]
6489 fn test_analytics_skip_median() {
6490 let mut analytics = CutsceneAnalytics::new("cs_test");
6491 for t in [5.0, 10.0, 15.0, 20.0, 25.0] {
6492 analytics.record_play(t, true, Some(t));
6493 }
6494 let median = analytics.most_common_skip_time().unwrap();
6495 assert!((median - 15.0).abs() < 0.1);
6496 }
6497}
6498
6499pub const CUTSCENE_TEMPLATE_LIBRARY_SIZE: u32 = 12;
6500pub const CUTSCENE_MAX_VALIDATORS: u32 = 8;
6501pub const CUTSCENE_VALIDATOR_VERSION: &str = "1.0.0";
6502
6503#[derive(Debug, Clone)]
6508pub struct CutsceneEventSchedule {
6509 pub events: Vec<(f32, String)>,
6510}
6511
6512impl CutsceneEventSchedule {
6513 pub fn new() -> Self { CutsceneEventSchedule { events: Vec::new() } }
6514 pub fn schedule(&mut self, time: f32, event: &str) {
6515 self.events.push((time, event.to_string()));
6516 self.events.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap());
6517 }
6518 pub fn events_before(&self, t: f32) -> Vec<&str> {
6519 self.events.iter().filter(|(et, _)| *et <= t).map(|(_, e)| e.as_str()).collect()
6520 }
6521 pub fn next_event_after(&self, t: f32) -> Option<(f32, &str)> {
6522 self.events.iter().find(|(et, _)| *et > t).map(|(et, e)| (*et, e.as_str()))
6523 }
6524}
6525
6526#[derive(Debug, Clone)]
6527pub struct CutsceneMarker {
6528 pub id: u32, pub time: f32, pub name: String, pub color: Vec4,
6529 pub comment: String,
6530}
6531
6532impl CutsceneMarker {
6533 pub fn new(id: u32, time: f32, name: &str) -> Self {
6534 CutsceneMarker { id, time, name: name.to_string(),
6535 color: Vec4::new(1.0, 0.8, 0.0, 1.0), comment: String::new() }
6536 }
6537}
6538
6539#[derive(Debug, Clone)]
6540pub struct CutsceneMarkerTrack {
6541 pub markers: Vec<CutsceneMarker>,
6542}
6543
6544impl CutsceneMarkerTrack {
6545 pub fn new() -> Self { CutsceneMarkerTrack { markers: Vec::new() } }
6546 pub fn add_marker(&mut self, m: CutsceneMarker) {
6547 self.markers.push(m);
6548 self.markers.sort_by(|a, b| a.time.partial_cmp(&b.time).unwrap());
6549 }
6550 pub fn marker_at(&self, t: f32, tolerance: f32) -> Option<&CutsceneMarker> {
6551 self.markers.iter().find(|m| (m.time - t).abs() <= tolerance)
6552 }
6553 pub fn markers_in_range(&self, start: f32, end: f32) -> Vec<&CutsceneMarker> {
6554 self.markers.iter().filter(|m| m.time >= start && m.time <= end).collect()
6555 }
6556}
6557
6558#[cfg(test)]
6559mod tests_markers {
6560 use super::*;
6561 #[test]
6562 fn test_event_schedule() {
6563 let mut sched = CutsceneEventSchedule::new();
6564 sched.schedule(5.0, "explosion");
6565 sched.schedule(10.0, "music_change");
6566 sched.schedule(2.0, "fade_in");
6567 let before = sched.events_before(6.0);
6568 assert_eq!(before.len(), 2);
6569 let next = sched.next_event_after(6.0);
6570 assert!(next.is_some());
6571 assert_eq!(next.unwrap().1, "music_change");
6572 }
6573 #[test]
6574 fn test_marker_track() {
6575 let mut track = CutsceneMarkerTrack::new();
6576 track.add_marker(CutsceneMarker::new(1, 10.0, "Act1End"));
6577 track.add_marker(CutsceneMarker::new(2, 30.0, "Act2End"));
6578 let found = track.marker_at(10.0, 0.1);
6579 assert!(found.is_some());
6580 let range = track.markers_in_range(5.0, 25.0);
6581 assert_eq!(range.len(), 1);
6582 }
6583}
6584
6585pub const CUTSCENE_MARKER_MAX: u32 = 256;
6586pub const CUTSCENE_EVENT_MAX: u32 = 1024;
6587pub const CUTSCENE_FINAL_BUILD: u32 = 311;
6588
6589#[derive(Debug, Clone)]
6594pub struct CutsceneClipEntry0 {
6595 pub clip_id: u32,
6596 pub name: String,
6597 pub duration: f32,
6598 pub fps: f32,
6599 pub frame_count: u32,
6600}
6601
6602impl CutsceneClipEntry0 {
6603 pub fn new(id: u32, name: &str, dur: f32, fps: f32) -> Self {
6604 CutsceneClipEntry0 { clip_id: id, name: name.to_string(), duration: dur, fps, frame_count: (dur * fps) as u32 }
6605 }
6606 pub fn frame_at_time(&self, t: f32) -> u32 { (t * self.fps) as u32 }
6607 pub fn time_at_frame(&self, f: u32) -> f32 { if self.fps <= 0.0 { 0.0 } else { f as f32 / self.fps } }
6608 pub fn is_within_bounds(&self, t: f32) -> bool { t >= 0.0 && t <= self.duration }
6609}
6610
6611#[derive(Debug, Clone)]
6612pub struct CutsceneClipEntry1 {
6613 pub clip_id: u32,
6614 pub name: String,
6615 pub duration: f32,
6616 pub fps: f32,
6617 pub frame_count: u32,
6618}
6619
6620impl CutsceneClipEntry1 {
6621 pub fn new(id: u32, name: &str, dur: f32, fps: f32) -> Self {
6622 CutsceneClipEntry1 { clip_id: id, name: name.to_string(), duration: dur, fps, frame_count: (dur * fps) as u32 }
6623 }
6624 pub fn frame_at_time(&self, t: f32) -> u32 { (t * self.fps) as u32 }
6625 pub fn time_at_frame(&self, f: u32) -> f32 { if self.fps <= 0.0 { 0.0 } else { f as f32 / self.fps } }
6626 pub fn is_within_bounds(&self, t: f32) -> bool { t >= 0.0 && t <= self.duration }
6627}
6628
6629#[derive(Debug, Clone)]
6630pub struct CutsceneClipEntry2 {
6631 pub clip_id: u32,
6632 pub name: String,
6633 pub duration: f32,
6634 pub fps: f32,
6635 pub frame_count: u32,
6636}
6637
6638impl CutsceneClipEntry2 {
6639 pub fn new(id: u32, name: &str, dur: f32, fps: f32) -> Self {
6640 CutsceneClipEntry2 { clip_id: id, name: name.to_string(), duration: dur, fps, frame_count: (dur * fps) as u32 }
6641 }
6642 pub fn frame_at_time(&self, t: f32) -> u32 { (t * self.fps) as u32 }
6643 pub fn time_at_frame(&self, f: u32) -> f32 { if self.fps <= 0.0 { 0.0 } else { f as f32 / self.fps } }
6644 pub fn is_within_bounds(&self, t: f32) -> bool { t >= 0.0 && t <= self.duration }
6645}
6646
6647#[derive(Debug, Clone)]
6648pub struct CutsceneClipEntry3 {
6649 pub clip_id: u32,
6650 pub name: String,
6651 pub duration: f32,
6652 pub fps: f32,
6653 pub frame_count: u32,
6654}
6655
6656impl CutsceneClipEntry3 {
6657 pub fn new(id: u32, name: &str, dur: f32, fps: f32) -> Self {
6658 CutsceneClipEntry3 { clip_id: id, name: name.to_string(), duration: dur, fps, frame_count: (dur * fps) as u32 }
6659 }
6660 pub fn frame_at_time(&self, t: f32) -> u32 { (t * self.fps) as u32 }
6661 pub fn time_at_frame(&self, f: u32) -> f32 { if self.fps <= 0.0 { 0.0 } else { f as f32 / self.fps } }
6662 pub fn is_within_bounds(&self, t: f32) -> bool { t >= 0.0 && t <= self.duration }
6663}
6664
6665#[derive(Debug, Clone)]
6666pub struct CutsceneClipEntry4 {
6667 pub clip_id: u32,
6668 pub name: String,
6669 pub duration: f32,
6670 pub fps: f32,
6671 pub frame_count: u32,
6672}
6673
6674impl CutsceneClipEntry4 {
6675 pub fn new(id: u32, name: &str, dur: f32, fps: f32) -> Self {
6676 CutsceneClipEntry4 { clip_id: id, name: name.to_string(), duration: dur, fps, frame_count: (dur * fps) as u32 }
6677 }
6678 pub fn frame_at_time(&self, t: f32) -> u32 { (t * self.fps) as u32 }
6679 pub fn time_at_frame(&self, f: u32) -> f32 { if self.fps <= 0.0 { 0.0 } else { f as f32 / self.fps } }
6680 pub fn is_within_bounds(&self, t: f32) -> bool { t >= 0.0 && t <= self.duration }
6681}
6682
6683#[derive(Debug, Clone)]
6684pub struct CutsceneClipEntry5 {
6685 pub clip_id: u32,
6686 pub name: String,
6687 pub duration: f32,
6688 pub fps: f32,
6689 pub frame_count: u32,
6690}
6691
6692impl CutsceneClipEntry5 {
6693 pub fn new(id: u32, name: &str, dur: f32, fps: f32) -> Self {
6694 CutsceneClipEntry5 { clip_id: id, name: name.to_string(), duration: dur, fps, frame_count: (dur * fps) as u32 }
6695 }
6696 pub fn frame_at_time(&self, t: f32) -> u32 { (t * self.fps) as u32 }
6697 pub fn time_at_frame(&self, f: u32) -> f32 { if self.fps <= 0.0 { 0.0 } else { f as f32 / self.fps } }
6698 pub fn is_within_bounds(&self, t: f32) -> bool { t >= 0.0 && t <= self.duration }
6699}
6700
6701#[derive(Debug, Clone)]
6702pub struct CutsceneClipEntry6 {
6703 pub clip_id: u32,
6704 pub name: String,
6705 pub duration: f32,
6706 pub fps: f32,
6707 pub frame_count: u32,
6708}
6709
6710impl CutsceneClipEntry6 {
6711 pub fn new(id: u32, name: &str, dur: f32, fps: f32) -> Self {
6712 CutsceneClipEntry6 { clip_id: id, name: name.to_string(), duration: dur, fps, frame_count: (dur * fps) as u32 }
6713 }
6714 pub fn frame_at_time(&self, t: f32) -> u32 { (t * self.fps) as u32 }
6715 pub fn time_at_frame(&self, f: u32) -> f32 { if self.fps <= 0.0 { 0.0 } else { f as f32 / self.fps } }
6716 pub fn is_within_bounds(&self, t: f32) -> bool { t >= 0.0 && t <= self.duration }
6717}
6718
6719#[derive(Debug, Clone)]
6720pub struct CutsceneClipEntry7 {
6721 pub clip_id: u32,
6722 pub name: String,
6723 pub duration: f32,
6724 pub fps: f32,
6725 pub frame_count: u32,
6726}
6727
6728impl CutsceneClipEntry7 {
6729 pub fn new(id: u32, name: &str, dur: f32, fps: f32) -> Self {
6730 CutsceneClipEntry7 { clip_id: id, name: name.to_string(), duration: dur, fps, frame_count: (dur * fps) as u32 }
6731 }
6732 pub fn frame_at_time(&self, t: f32) -> u32 { (t * self.fps) as u32 }
6733 pub fn time_at_frame(&self, f: u32) -> f32 { if self.fps <= 0.0 { 0.0 } else { f as f32 / self.fps } }
6734 pub fn is_within_bounds(&self, t: f32) -> bool { t >= 0.0 && t <= self.duration }
6735}
6736
6737#[derive(Debug, Clone)]
6738pub struct CutsceneClipEntry8 {
6739 pub clip_id: u32,
6740 pub name: String,
6741 pub duration: f32,
6742 pub fps: f32,
6743 pub frame_count: u32,
6744}
6745
6746impl CutsceneClipEntry8 {
6747 pub fn new(id: u32, name: &str, dur: f32, fps: f32) -> Self {
6748 CutsceneClipEntry8 { clip_id: id, name: name.to_string(), duration: dur, fps, frame_count: (dur * fps) as u32 }
6749 }
6750 pub fn frame_at_time(&self, t: f32) -> u32 { (t * self.fps) as u32 }
6751 pub fn time_at_frame(&self, f: u32) -> f32 { if self.fps <= 0.0 { 0.0 } else { f as f32 / self.fps } }
6752 pub fn is_within_bounds(&self, t: f32) -> bool { t >= 0.0 && t <= self.duration }
6753}
6754
6755#[derive(Debug, Clone)]
6756pub struct CutsceneClipEntry9 {
6757 pub clip_id: u32,
6758 pub name: String,
6759 pub duration: f32,
6760 pub fps: f32,
6761 pub frame_count: u32,
6762}
6763
6764impl CutsceneClipEntry9 {
6765 pub fn new(id: u32, name: &str, dur: f32, fps: f32) -> Self {
6766 CutsceneClipEntry9 { clip_id: id, name: name.to_string(), duration: dur, fps, frame_count: (dur * fps) as u32 }
6767 }
6768 pub fn frame_at_time(&self, t: f32) -> u32 { (t * self.fps) as u32 }
6769 pub fn time_at_frame(&self, f: u32) -> f32 { if self.fps <= 0.0 { 0.0 } else { f as f32 / self.fps } }
6770 pub fn is_within_bounds(&self, t: f32) -> bool { t >= 0.0 && t <= self.duration }
6771}
6772
6773#[derive(Debug, Clone)]
6774pub struct CutsceneClipEntry10 {
6775 pub clip_id: u32,
6776 pub name: String,
6777 pub duration: f32,
6778 pub fps: f32,
6779 pub frame_count: u32,
6780}
6781
6782impl CutsceneClipEntry10 {
6783 pub fn new(id: u32, name: &str, dur: f32, fps: f32) -> Self {
6784 CutsceneClipEntry10 { clip_id: id, name: name.to_string(), duration: dur, fps, frame_count: (dur * fps) as u32 }
6785 }
6786 pub fn frame_at_time(&self, t: f32) -> u32 { (t * self.fps) as u32 }
6787 pub fn time_at_frame(&self, f: u32) -> f32 { if self.fps <= 0.0 { 0.0 } else { f as f32 / self.fps } }
6788 pub fn is_within_bounds(&self, t: f32) -> bool { t >= 0.0 && t <= self.duration }
6789}
6790
6791#[derive(Debug, Clone)]
6792pub struct CutsceneClipEntry11 {
6793 pub clip_id: u32,
6794 pub name: String,
6795 pub duration: f32,
6796 pub fps: f32,
6797 pub frame_count: u32,
6798}
6799
6800impl CutsceneClipEntry11 {
6801 pub fn new(id: u32, name: &str, dur: f32, fps: f32) -> Self {
6802 CutsceneClipEntry11 { clip_id: id, name: name.to_string(), duration: dur, fps, frame_count: (dur * fps) as u32 }
6803 }
6804 pub fn frame_at_time(&self, t: f32) -> u32 { (t * self.fps) as u32 }
6805 pub fn time_at_frame(&self, f: u32) -> f32 { if self.fps <= 0.0 { 0.0 } else { f as f32 / self.fps } }
6806 pub fn is_within_bounds(&self, t: f32) -> bool { t >= 0.0 && t <= self.duration }
6807}
6808
6809#[derive(Debug, Clone)]
6810pub struct CutsceneClipEntry12 {
6811 pub clip_id: u32,
6812 pub name: String,
6813 pub duration: f32,
6814 pub fps: f32,
6815 pub frame_count: u32,
6816}
6817
6818impl CutsceneClipEntry12 {
6819 pub fn new(id: u32, name: &str, dur: f32, fps: f32) -> Self {
6820 CutsceneClipEntry12 { clip_id: id, name: name.to_string(), duration: dur, fps, frame_count: (dur * fps) as u32 }
6821 }
6822 pub fn frame_at_time(&self, t: f32) -> u32 { (t * self.fps) as u32 }
6823 pub fn time_at_frame(&self, f: u32) -> f32 { if self.fps <= 0.0 { 0.0 } else { f as f32 / self.fps } }
6824 pub fn is_within_bounds(&self, t: f32) -> bool { t >= 0.0 && t <= self.duration }
6825}
6826
6827#[derive(Debug, Clone)]
6828pub struct CutsceneClipEntry13 {
6829 pub clip_id: u32,
6830 pub name: String,
6831 pub duration: f32,
6832 pub fps: f32,
6833 pub frame_count: u32,
6834}
6835
6836impl CutsceneClipEntry13 {
6837 pub fn new(id: u32, name: &str, dur: f32, fps: f32) -> Self {
6838 CutsceneClipEntry13 { clip_id: id, name: name.to_string(), duration: dur, fps, frame_count: (dur * fps) as u32 }
6839 }
6840 pub fn frame_at_time(&self, t: f32) -> u32 { (t * self.fps) as u32 }
6841 pub fn time_at_frame(&self, f: u32) -> f32 { if self.fps <= 0.0 { 0.0 } else { f as f32 / self.fps } }
6842 pub fn is_within_bounds(&self, t: f32) -> bool { t >= 0.0 && t <= self.duration }
6843}
6844
6845#[derive(Debug, Clone)]
6846pub struct CutsceneClipEntry14 {
6847 pub clip_id: u32,
6848 pub name: String,
6849 pub duration: f32,
6850 pub fps: f32,
6851 pub frame_count: u32,
6852}
6853
6854impl CutsceneClipEntry14 {
6855 pub fn new(id: u32, name: &str, dur: f32, fps: f32) -> Self {
6856 CutsceneClipEntry14 { clip_id: id, name: name.to_string(), duration: dur, fps, frame_count: (dur * fps) as u32 }
6857 }
6858 pub fn frame_at_time(&self, t: f32) -> u32 { (t * self.fps) as u32 }
6859 pub fn time_at_frame(&self, f: u32) -> f32 { if self.fps <= 0.0 { 0.0 } else { f as f32 / self.fps } }
6860 pub fn is_within_bounds(&self, t: f32) -> bool { t >= 0.0 && t <= self.duration }
6861}
6862
6863#[derive(Debug, Clone)]
6864pub struct CutsceneClipEntry15 {
6865 pub clip_id: u32,
6866 pub name: String,
6867 pub duration: f32,
6868 pub fps: f32,
6869 pub frame_count: u32,
6870}
6871
6872impl CutsceneClipEntry15 {
6873 pub fn new(id: u32, name: &str, dur: f32, fps: f32) -> Self {
6874 CutsceneClipEntry15 { clip_id: id, name: name.to_string(), duration: dur, fps, frame_count: (dur * fps) as u32 }
6875 }
6876 pub fn frame_at_time(&self, t: f32) -> u32 { (t * self.fps) as u32 }
6877 pub fn time_at_frame(&self, f: u32) -> f32 { if self.fps <= 0.0 { 0.0 } else { f as f32 / self.fps } }
6878 pub fn is_within_bounds(&self, t: f32) -> bool { t >= 0.0 && t <= self.duration }
6879}
6880
6881#[derive(Debug, Clone)]
6882pub struct CutsceneClipEntry16 {
6883 pub clip_id: u32,
6884 pub name: String,
6885 pub duration: f32,
6886 pub fps: f32,
6887 pub frame_count: u32,
6888}
6889
6890impl CutsceneClipEntry16 {
6891 pub fn new(id: u32, name: &str, dur: f32, fps: f32) -> Self {
6892 CutsceneClipEntry16 { clip_id: id, name: name.to_string(), duration: dur, fps, frame_count: (dur * fps) as u32 }
6893 }
6894 pub fn frame_at_time(&self, t: f32) -> u32 { (t * self.fps) as u32 }
6895 pub fn time_at_frame(&self, f: u32) -> f32 { if self.fps <= 0.0 { 0.0 } else { f as f32 / self.fps } }
6896 pub fn is_within_bounds(&self, t: f32) -> bool { t >= 0.0 && t <= self.duration }
6897}
6898
6899#[derive(Debug, Clone)]
6900pub struct CutsceneClipEntry17 {
6901 pub clip_id: u32,
6902 pub name: String,
6903 pub duration: f32,
6904 pub fps: f32,
6905 pub frame_count: u32,
6906}
6907
6908impl CutsceneClipEntry17 {
6909 pub fn new(id: u32, name: &str, dur: f32, fps: f32) -> Self {
6910 CutsceneClipEntry17 { clip_id: id, name: name.to_string(), duration: dur, fps, frame_count: (dur * fps) as u32 }
6911 }
6912 pub fn frame_at_time(&self, t: f32) -> u32 { (t * self.fps) as u32 }
6913 pub fn time_at_frame(&self, f: u32) -> f32 { if self.fps <= 0.0 { 0.0 } else { f as f32 / self.fps } }
6914 pub fn is_within_bounds(&self, t: f32) -> bool { t >= 0.0 && t <= self.duration }
6915}
6916
6917#[derive(Debug, Clone)]
6918pub struct CutsceneClipEntry18 {
6919 pub clip_id: u32,
6920 pub name: String,
6921 pub duration: f32,
6922 pub fps: f32,
6923 pub frame_count: u32,
6924}
6925
6926impl CutsceneClipEntry18 {
6927 pub fn new(id: u32, name: &str, dur: f32, fps: f32) -> Self {
6928 CutsceneClipEntry18 { clip_id: id, name: name.to_string(), duration: dur, fps, frame_count: (dur * fps) as u32 }
6929 }
6930 pub fn frame_at_time(&self, t: f32) -> u32 { (t * self.fps) as u32 }
6931 pub fn time_at_frame(&self, f: u32) -> f32 { if self.fps <= 0.0 { 0.0 } else { f as f32 / self.fps } }
6932 pub fn is_within_bounds(&self, t: f32) -> bool { t >= 0.0 && t <= self.duration }
6933}
6934
6935#[derive(Debug, Clone)]
6936pub struct CutsceneClipEntry19 {
6937 pub clip_id: u32,
6938 pub name: String,
6939 pub duration: f32,
6940 pub fps: f32,
6941 pub frame_count: u32,
6942}
6943
6944impl CutsceneClipEntry19 {
6945 pub fn new(id: u32, name: &str, dur: f32, fps: f32) -> Self {
6946 CutsceneClipEntry19 { clip_id: id, name: name.to_string(), duration: dur, fps, frame_count: (dur * fps) as u32 }
6947 }
6948 pub fn frame_at_time(&self, t: f32) -> u32 { (t * self.fps) as u32 }
6949 pub fn time_at_frame(&self, f: u32) -> f32 { if self.fps <= 0.0 { 0.0 } else { f as f32 / self.fps } }
6950 pub fn is_within_bounds(&self, t: f32) -> bool { t >= 0.0 && t <= self.duration }
6951}
6952
6953
6954pub const CUTSCENE_COMPLETE_FLAG: bool = true;
6956pub const CUTSCENE_MODULE_NAME: &str = "cutscene_importer";
6957pub const CUTSCENE_CONST_0: f32 = 0.0;
6958pub const CUTSCENE_CONST_1: f32 = 2.0;
6959pub const CUTSCENE_CONST_2: f32 = 4.0;
6960pub const CUTSCENE_CONST_3: f32 = 6.0;
6961pub const CUTSCENE_CONST_4: f32 = 8.0;
6962pub const CUTSCENE_CONST_5: f32 = 10.0;
6963pub const CUTSCENE_CONST_6: f32 = 12.0;
6964pub const CUTSCENE_CONST_7: f32 = 14.0;
6965pub const CUTSCENE_CONST_8: f32 = 16.0;
6966pub const CUTSCENE_CONST_9: f32 = 18.0;
6967pub const CUTSCENE_CONST_10: f32 = 20.0;
6968pub const CUTSCENE_CONST_11: f32 = 22.0;
6969pub const CUTSCENE_CONST_12: f32 = 24.0;
6970pub const CUTSCENE_CONST_13: f32 = 26.0;
6971pub const CUTSCENE_CONST_14: f32 = 28.0;
6972pub const CUTSCENE_CONST_15: f32 = 30.0;
6973pub const CUTSCENE_CONST_16: f32 = 32.0;
6974pub const CUTSCENE_CONST_17: f32 = 34.0;
6975pub const CUTSCENE_CONST_18: f32 = 36.0;
6976pub const CUTSCENE_CONST_19: f32 = 38.0;
6977pub const CUTSCENE_CONST_20: f32 = 40.0;
6978pub const CUTSCENE_CONST_21: f32 = 42.0;
6979pub const CUTSCENE_CONST_22: f32 = 44.0;
6980pub const CUTSCENE_CONST_23: f32 = 46.0;
6981pub const CUTSCENE_CONST_24: f32 = 48.0;
6982pub const CUTSCENE_CONST_25: f32 = 50.0;
6983pub const CUTSCENE_CONST_26: f32 = 52.0;
6984pub const CUTSCENE_CONST_27: f32 = 54.0;
6985pub const CUTSCENE_CONST_28: f32 = 56.0;
6986pub const CUTSCENE_CONST_29: f32 = 58.0;
6987pub const CUTSCENE_CONST_30: f32 = 60.0;
6988pub const CUTSCENE_CONST_31: f32 = 62.0;
6989pub const CUTSCENE_CONST_32: f32 = 64.0;
6990pub const CUTSCENE_CONST_33: f32 = 66.0;
6991pub const CUTSCENE_CONST_34: f32 = 68.0;
6992pub const CUTSCENE_CONST_35: f32 = 70.0;
6993pub const CUTSCENE_CONST_36: f32 = 72.0;
6994pub const CUTSCENE_CONST_37: f32 = 74.0;
6995pub const CUTSCENE_CONST_38: f32 = 76.0;
6996pub const CUTSCENE_CONST_39: f32 = 78.0;
6997pub const CUTSCENE_CONST_40: f32 = 80.0;
6998pub const CUTSCENE_CONST_41: f32 = 82.0;
6999pub const CUTSCENE_CONST_42: f32 = 84.0;
7000pub const CUTSCENE_CONST_43: f32 = 86.0;
7001pub const CUTSCENE_CONST_44: f32 = 88.0;
7002pub const CUTSCENE_CONST_45: f32 = 90.0;
7003pub const CUTSCENE_CONST_46: f32 = 92.0;
7004pub const CUTSCENE_CONST_47: f32 = 94.0;
7005pub const CUTSCENE_CONST_48: f32 = 96.0;
7006pub const CUTSCENE_CONST_49: f32 = 98.0;
7007pub const CUTSCENE_CONST_50: f32 = 100.0;
7008pub const CUTSCENE_CONST_51: f32 = 102.0;
7009pub const CUTSCENE_CONST_52: f32 = 104.0;
7010pub const CUTSCENE_CONST_53: f32 = 106.0;
7011pub const CUTSCENE_CONST_54: f32 = 108.0;
7012pub const CUTSCENE_CONST_55: f32 = 110.0;
7013pub const CUTSCENE_CONST_56: f32 = 112.0;
7014pub const CUTSCENE_CONST_57: f32 = 114.0;
7015pub const CUTSCENE_CONST_58: f32 = 116.0;
7016pub const CUTSCENE_CONST_59: f32 = 118.0;