1use glam::{Quat, Vec3A};
2use serde::{Deserialize, Serialize};
3use std::collections::HashSet;
4
5use mmd_anim_runtime::{
6 AnimationClip, BoneAnimationBinding, BoneIndex, InterpolationScalar, InterpolationVector3,
7 ModelArena, MorphAnimationBinding, MorphIndex, MorphKeyframe, MorphTrack, MovableBoneKeyframe,
8 MovableBoneTrack, PropertyAnimationBinding, PropertyKeyframe,
9};
10
11use crate::binary::{
12 ByteReader, write_f32_le as write_f32, write_fixed_bytes, write_u32_le as write_u32,
13};
14use crate::error::ImportError;
15use crate::normalize::normalize_vmd_name;
16use crate::sjis::{decode_sjis_fixed_trimmed, encode_sjis};
17use thiserror::Error;
18
19#[cfg(test)]
20use mmd_anim_runtime::BoneInit;
21
22mod reduced;
23pub use reduced::{
24 VmdExportMorphKind, VmdExportName, VmdPoseExport, VmdPoseExportBindings, VmdPoseExportError,
25 VmdPoseExportReport, export_reduced_pose_to_vmd,
26};
27
28type Reader<'a> = ByteReader<'a>;
29
30const VMD_MAGIC: [u8; 30] = *b"Vocaloid Motion Data 0002\0\0\0\0\0";
31const VMD_MAGIC_PREFIX: &[u8] = b"Vocaloid Motion Data 0002\0";
32
33impl<'a> ByteReader<'a> {
34 fn read_optional_u32_le(&mut self) -> Result<Option<u32>, ImportError> {
35 if self.remaining() == 0 {
36 Ok(None)
37 } else {
38 self.read_u32_le().map(Some)
39 }
40 }
41
42 fn read_record_count(&mut self, record_size: usize) -> Result<usize, ImportError> {
43 let count = self.read_u32_le()? as usize;
44 self.require_record_bytes(count, record_size)?;
45 Ok(count)
46 }
47
48 fn read_optional_record_count(
49 &mut self,
50 record_size: usize,
51 ) -> Result<Option<usize>, ImportError> {
52 let Some(count) = self.read_optional_u32_le()? else {
53 return Ok(None);
54 };
55 let count = count as usize;
56 let Some(bytes) = count.checked_mul(record_size) else {
57 self.pos = self.data.len();
58 return Ok(None);
59 };
60 if bytes > self.remaining() {
61 self.pos = self.data.len();
62 return Ok(None);
63 }
64 Ok(Some(count))
65 }
66}
67
68#[derive(Debug, Clone)]
69pub struct VmdHeader {
70 pub model_name_bytes: [u8; 20],
71}
72
73#[derive(Debug, Clone)]
74pub enum VmdBoneImportMode {
75 ByName(Vec<u8>),
76 ByIndex(u32),
77}
78
79#[derive(Debug, Clone)]
80pub struct VmdBoneKeyframeRaw {
81 pub bone_mode: VmdBoneImportMode,
82 pub frame: u32,
83 pub position: Vec3A,
84 pub rotation: Quat,
85 pub interpolation: [u8; 64],
86 pub bone_name_normalized: Vec<u8>,
87}
88
89#[derive(Debug, Clone)]
90pub struct VmdIkEntry {
91 pub name_bytes: Vec<u8>,
92 pub enabled: u8,
93 pub name_normalized: Vec<u8>,
94}
95
96#[derive(Debug, Clone)]
97pub struct VmdPropertyIkFrame {
98 pub frame: u32,
99 pub show: u8,
100 pub entries: Vec<VmdIkEntry>,
101}
102
103#[derive(Debug, Clone)]
104pub struct VmdImportResult {
105 pub bone_keyframes: Vec<VmdBoneKeyframeRaw>,
106 pub morph_keyframes: Vec<(Vec<u8>, u32, f32)>,
107 pub property_keyframes: Vec<PropertyKeyframe>,
108 pub property_ik_frames: Vec<VmdPropertyIkFrame>,
109}
110
111#[derive(Debug, Clone)]
119pub struct VmdSharedContext {
120 raw: VmdImportResult,
121 parsed: VmdParsedAnimation,
122 summary: VmdSharedContextSummary,
123}
124
125#[derive(Debug, Clone, Copy, PartialEq, Eq)]
131pub struct VmdSharedContextTrackSummary {
132 pub track_count: usize,
133 pub key_count: usize,
134}
135
136#[derive(Debug, Clone, Copy, PartialEq, Eq)]
143pub struct VmdSharedContextSummary {
144 pub target_model_name_bytes: [u8; 20],
145 pub max_frame: u32,
146 pub bones: VmdSharedContextTrackSummary,
147 pub morphs: VmdSharedContextTrackSummary,
148 pub cameras: VmdSharedContextTrackSummary,
149 pub lights: VmdSharedContextTrackSummary,
150 pub self_shadows: VmdSharedContextTrackSummary,
151 pub properties: VmdSharedContextTrackSummary,
152 pub property_ik_entry_count: usize,
153}
154
155#[derive(Debug, Error, PartialEq)]
161pub enum VmdSummaryError {
162 #[error("VMD summary parse failed: {0}")]
163 Parse(ImportError),
164 #[error("invalid VMD summary data: {0}")]
165 Invalid(&'static str),
166}
167
168impl VmdSharedContextSummary {
169 fn from_parsed(target_model_name_bytes: [u8; 20], parsed: &VmdParsedAnimation) -> Self {
170 Self {
171 target_model_name_bytes,
172 max_frame: parsed.metadata.max_frame,
173 bones: VmdSharedContextTrackSummary {
174 track_count: distinct_name_count(
175 parsed
176 .bone_frames
177 .iter()
178 .map(|frame| frame.bone_name_bytes.as_slice()),
179 ),
180 key_count: parsed.bone_frames.len(),
181 },
182 morphs: VmdSharedContextTrackSummary {
183 track_count: distinct_name_count(
184 parsed
185 .morph_frames
186 .iter()
187 .map(|frame| frame.morph_name_bytes.as_slice()),
188 ),
189 key_count: parsed.morph_frames.len(),
190 },
191 cameras: singleton_track_summary(parsed.camera_frames.len()),
192 lights: singleton_track_summary(parsed.light_frames.len()),
193 self_shadows: singleton_track_summary(parsed.self_shadow_frames.len()),
194 properties: singleton_track_summary(parsed.property_frames.len()),
195 property_ik_entry_count: parsed
196 .property_frames
197 .iter()
198 .map(|frame| frame.ik_states.len())
199 .sum(),
200 }
201 }
202}
203
204fn distinct_name_count<'a>(names: impl Iterator<Item = &'a [u8]>) -> usize {
205 names
206 .map(|name| name.to_vec())
207 .collect::<HashSet<_>>()
208 .len()
209}
210
211fn singleton_track_summary(key_count: usize) -> VmdSharedContextTrackSummary {
212 VmdSharedContextTrackSummary {
213 track_count: usize::from(key_count != 0),
214 key_count,
215 }
216}
217
218impl VmdSharedContext {
219 pub fn import_result(&self) -> &VmdImportResult {
220 &self.raw
221 }
222
223 pub fn parsed_animation(&self) -> &VmdParsedAnimation {
224 &self.parsed
225 }
226
227 pub fn summary(&self) -> &VmdSharedContextSummary {
228 &self.summary
229 }
230}
231
232pub fn parse_vmd_shared_context(data: &[u8]) -> Result<VmdSharedContext, ImportError> {
235 let scan = parse_vmd_scan(data, VmdScanOptions::BOTH)?;
236 let VmdScanResult {
237 raw,
238 parsed,
239 summary: _,
240 target_model_name_bytes,
241 } = scan;
242 let raw = raw.expect("combined VMD scan must produce raw output");
243 let parsed = parsed.expect("combined VMD scan must produce parsed output");
244 Ok(VmdSharedContext {
245 summary: VmdSharedContextSummary::from_parsed(target_model_name_bytes, &parsed),
246 raw,
247 parsed,
248 })
249}
250
251pub fn parse_vmd_summary(data: &[u8]) -> Result<VmdSharedContextSummary, VmdSummaryError> {
261 let scan = parse_vmd_scan(data, VmdScanOptions::SUMMARY).map_err(|error| match error {
262 ImportError::UnsupportedFormat {
263 format: "VMD",
264 detail,
265 } => VmdSummaryError::Invalid(detail),
266 error => VmdSummaryError::Parse(error),
267 })?;
268 Ok(scan
269 .summary
270 .expect("summary-only VMD scan must produce summary output"))
271}
272
273pub fn read_header(data: &[u8]) -> Result<(VmdHeader, usize), ImportError> {
274 let mut r = Reader::new(data);
275
276 let magic = r.read_slice(30)?;
277 if magic != VMD_MAGIC && !magic.starts_with(VMD_MAGIC_PREFIX) {
278 return Err(ImportError::InvalidVmdMagic);
279 }
280
281 let model_name_bytes: [u8; 20] = r
282 .read_slice(20)?
283 .try_into()
284 .map_err(|_| ImportError::InvalidVmdModelName)?;
285
286 Ok((VmdHeader { model_name_bytes }, r.pos))
287}
288
289pub fn import_vmd_motion(data: &[u8]) -> Result<VmdImportResult, ImportError> {
290 Ok(parse_vmd_scan(data, VmdScanOptions::RAW)?
291 .raw
292 .expect("raw-only VMD scan must produce raw output"))
293}
294
295#[derive(Debug, Clone, Serialize, Deserialize)]
296#[serde(rename_all = "camelCase")]
297pub struct VmdParsedAnimation {
298 #[serde(default = "default_vmd_kind", skip_deserializing)]
299 pub kind: &'static str,
300 pub metadata: VmdParsedMetadata,
301 pub bone_frames: Vec<VmdParsedBoneFrame>,
302 pub morph_frames: Vec<VmdParsedMorphFrame>,
303 pub camera_frames: Vec<VmdParsedCameraFrame>,
304 pub light_frames: Vec<VmdParsedLightFrame>,
305 pub self_shadow_frames: Vec<VmdParsedSelfShadowFrame>,
306 pub property_frames: Vec<VmdParsedPropertyFrame>,
307}
308
309#[derive(Debug, Clone, Serialize, Deserialize)]
310#[serde(rename_all = "camelCase")]
311pub struct VmdParsedMetadata {
312 #[serde(default = "default_vmd_format", skip_deserializing)]
313 pub format: &'static str,
314 pub model_name: String,
315 #[serde(default, skip_serializing_if = "Vec::is_empty")]
316 pub model_name_bytes: Vec<u8>,
317 pub counts: VmdParsedCounts,
318 pub max_frame: u32,
319}
320
321#[derive(Debug, Clone, Serialize, Deserialize)]
322pub struct VmdParsedCounts {
323 pub bones: usize,
324 pub morphs: usize,
325 pub cameras: usize,
326 pub lights: usize,
327 #[serde(rename = "selfShadows")]
328 pub self_shadows: usize,
329 pub properties: usize,
330}
331
332#[derive(Debug, Clone, Serialize, Deserialize)]
333#[serde(rename_all = "camelCase")]
334pub struct VmdParsedBoneFrame {
335 pub bone_name: String,
336 #[serde(default, skip_serializing_if = "Vec::is_empty")]
337 pub bone_name_bytes: Vec<u8>,
338 pub frame: u32,
339 pub translation: [f32; 3],
340 pub rotation: [f32; 4],
341 pub interpolation: Vec<u8>,
342}
343
344#[derive(Debug, Clone, Serialize, Deserialize)]
345#[serde(rename_all = "camelCase")]
346pub struct VmdParsedMorphFrame {
347 pub morph_name: String,
348 #[serde(default, skip_serializing_if = "Vec::is_empty")]
349 pub morph_name_bytes: Vec<u8>,
350 pub frame: u32,
351 pub weight: f32,
352}
353
354#[derive(Debug, Clone, Serialize, Deserialize)]
355#[serde(rename_all = "camelCase")]
356pub struct VmdParsedCameraFrame {
357 pub frame: u32,
358 pub distance: f32,
359 pub position: [f32; 3],
360 pub rotation: [f32; 3],
361 pub interpolation: [u8; 24],
362 pub fov: u32,
363 pub perspective: bool,
364}
365
366#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
367#[serde(rename_all = "camelCase")]
368pub struct VmdCameraState {
369 pub distance: f32,
370 pub position: [f32; 3],
371 pub rotation: [f32; 3],
372 pub fov: f32,
373 pub perspective: bool,
374}
375
376#[derive(Debug, Clone, Serialize, Deserialize)]
377#[serde(rename_all = "camelCase")]
378pub struct VmdParsedLightFrame {
379 pub frame: u32,
380 pub color: [f32; 3],
381 pub direction: [f32; 3],
382}
383
384#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
385#[serde(rename_all = "camelCase")]
386pub struct VmdLightState {
387 pub color: [f32; 3],
388 pub direction: [f32; 3],
389}
390
391#[derive(Debug, Clone, Serialize, Deserialize)]
392#[serde(rename_all = "camelCase")]
393pub struct VmdParsedSelfShadowFrame {
394 pub frame: u32,
395 pub mode: u8,
396 pub distance: f32,
397}
398
399#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
400#[serde(rename_all = "camelCase")]
401pub struct VmdSelfShadowState {
402 pub mode: u8,
403 pub distance: f32,
404}
405
406#[derive(Debug, Clone, Serialize, Deserialize)]
407#[serde(rename_all = "camelCase")]
408pub struct VmdParsedPropertyFrame {
409 pub frame: u32,
410 pub visible: bool,
411 pub ik_states: Vec<VmdParsedIkState>,
412}
413
414#[derive(Debug, Clone, Serialize, Deserialize)]
415#[serde(rename_all = "camelCase")]
416pub struct VmdParsedIkState {
417 pub bone_name: String,
418 #[serde(default, skip_serializing_if = "Vec::is_empty")]
419 pub bone_name_bytes: Vec<u8>,
420 pub enabled: bool,
421}
422
423#[derive(Clone, Copy)]
424struct VmdScanOptions {
425 build_raw: bool,
426 build_parsed: bool,
427 build_summary: bool,
428}
429
430impl VmdScanOptions {
431 const RAW: Self = Self {
432 build_raw: true,
433 build_parsed: false,
434 build_summary: false,
435 };
436 const PARSED: Self = Self {
437 build_raw: false,
438 build_parsed: true,
439 build_summary: false,
440 };
441 const BOTH: Self = Self {
442 build_raw: true,
443 build_parsed: true,
444 build_summary: false,
445 };
446 const SUMMARY: Self = Self {
447 build_raw: false,
448 build_parsed: false,
449 build_summary: true,
450 };
451}
452
453struct VmdScanResult {
454 raw: Option<VmdImportResult>,
455 parsed: Option<VmdParsedAnimation>,
456 summary: Option<VmdSharedContextSummary>,
457 target_model_name_bytes: [u8; 20],
458}
459
460struct VmdPropertyFrameReadResult {
461 parsed_frames: Option<Vec<VmdParsedPropertyFrame>>,
462 raw_keyframes: Option<Vec<PropertyKeyframe>>,
463 raw_ik_frames: Option<Vec<VmdPropertyIkFrame>>,
464}
465
466struct VmdSummaryAccumulator {
467 bone_names: HashSet<Vec<u8>>,
468 morph_names: HashSet<Vec<u8>>,
469 bone_key_count: usize,
470 morph_key_count: usize,
471 camera_key_count: usize,
472 light_key_count: usize,
473 self_shadow_key_count: usize,
474 property_key_count: usize,
475 property_ik_entry_count: usize,
476}
477
478impl VmdSummaryAccumulator {
479 fn new() -> Self {
480 Self {
481 bone_names: HashSet::new(),
482 morph_names: HashSet::new(),
483 bone_key_count: 0,
484 morph_key_count: 0,
485 camera_key_count: 0,
486 light_key_count: 0,
487 self_shadow_key_count: 0,
488 property_key_count: 0,
489 property_ik_entry_count: 0,
490 }
491 }
492
493 fn record_bone(&mut self, name: &[u8]) -> Result<(), ImportError> {
494 validate_summary_name(name, "empty bone name")?;
495 self.bone_names.insert(name.to_vec());
496 self.bone_key_count = self
497 .bone_key_count
498 .checked_add(1)
499 .ok_or(ImportError::SectionOverflow)?;
500 Ok(())
501 }
502
503 fn record_morph(&mut self, name: &[u8]) -> Result<(), ImportError> {
504 validate_summary_name(name, "empty morph name")?;
505 self.morph_names.insert(name.to_vec());
506 self.morph_key_count = self
507 .morph_key_count
508 .checked_add(1)
509 .ok_or(ImportError::SectionOverflow)?;
510 Ok(())
511 }
512
513 fn record_camera(&mut self) -> Result<(), ImportError> {
514 self.camera_key_count = self
515 .camera_key_count
516 .checked_add(1)
517 .ok_or(ImportError::SectionOverflow)?;
518 Ok(())
519 }
520
521 fn record_light(&mut self) -> Result<(), ImportError> {
522 self.light_key_count = self
523 .light_key_count
524 .checked_add(1)
525 .ok_or(ImportError::SectionOverflow)?;
526 Ok(())
527 }
528
529 fn record_self_shadow(&mut self) -> Result<(), ImportError> {
530 self.self_shadow_key_count = self
531 .self_shadow_key_count
532 .checked_add(1)
533 .ok_or(ImportError::SectionOverflow)?;
534 Ok(())
535 }
536
537 fn record_property(&mut self, ik_count: usize) -> Result<(), ImportError> {
538 self.property_key_count = self
539 .property_key_count
540 .checked_add(1)
541 .ok_or(ImportError::SectionOverflow)?;
542 self.property_ik_entry_count = self
543 .property_ik_entry_count
544 .checked_add(ik_count)
545 .ok_or(ImportError::SectionOverflow)?;
546 Ok(())
547 }
548
549 fn finish(self, target_model_name_bytes: [u8; 20], max_frame: u32) -> VmdSharedContextSummary {
550 VmdSharedContextSummary {
551 target_model_name_bytes,
552 max_frame,
553 bones: VmdSharedContextTrackSummary {
554 track_count: self.bone_names.len(),
555 key_count: self.bone_key_count,
556 },
557 morphs: VmdSharedContextTrackSummary {
558 track_count: self.morph_names.len(),
559 key_count: self.morph_key_count,
560 },
561 cameras: singleton_track_summary(self.camera_key_count),
562 lights: singleton_track_summary(self.light_key_count),
563 self_shadows: singleton_track_summary(self.self_shadow_key_count),
564 properties: singleton_track_summary(self.property_key_count),
565 property_ik_entry_count: self.property_ik_entry_count,
566 }
567 }
568}
569
570fn summary_invalid_error(detail: &'static str) -> ImportError {
571 ImportError::UnsupportedFormat {
572 format: "VMD",
573 detail,
574 }
575}
576
577fn validate_summary_name(name: &[u8], detail: &'static str) -> Result<(), ImportError> {
578 if vmd_fixed_name_is_blank(name) {
579 Err(summary_invalid_error(detail))
580 } else {
581 Ok(())
582 }
583}
584
585fn read_vmd_f32(
586 r: &mut Reader<'_>,
587 validate: bool,
588 field: &'static str,
589) -> Result<f32, ImportError> {
590 let value = r.read_f32_le()?;
591 if validate && !value.is_finite() {
592 return Err(summary_invalid_error(field));
593 }
594 Ok(value)
595}
596
597fn read_vmd_vec3(
598 r: &mut Reader<'_>,
599 validate: bool,
600 field: &'static str,
601) -> Result<Vec3A, ImportError> {
602 Ok(Vec3A::new(
603 read_vmd_f32(r, validate, field)?,
604 read_vmd_f32(r, validate, field)?,
605 read_vmd_f32(r, validate, field)?,
606 ))
607}
608
609fn read_vmd_quat(
610 r: &mut Reader<'_>,
611 validate: bool,
612 field: &'static str,
613) -> Result<Quat, ImportError> {
614 Ok(Quat::from_xyzw(
615 read_vmd_f32(r, validate, field)?,
616 read_vmd_f32(r, validate, field)?,
617 read_vmd_f32(r, validate, field)?,
618 read_vmd_f32(r, validate, field)?,
619 ))
620}
621
622fn vmd_fixed_name_is_blank(bytes: &[u8]) -> bool {
623 decode_sjis_fixed(bytes).trim().is_empty()
624}
625
626fn parse_vmd_scan(data: &[u8], options: VmdScanOptions) -> Result<VmdScanResult, ImportError> {
627 let (header, pos) = read_header(data)?;
628 let mut r = Reader { data, pos };
629 let model_name = options
630 .build_parsed
631 .then(|| decode_sjis_fixed(&header.model_name_bytes));
632 let model_name_bytes = options
633 .build_parsed
634 .then(|| trim_fixed_bytes(&header.model_name_bytes).to_vec());
635 let mut max_frame = 0u32;
636 let mut summary = options.build_summary.then(VmdSummaryAccumulator::new);
637
638 let bone_count = r.read_record_count(111)?;
639 let mut raw = options.build_raw.then(|| VmdImportResult {
640 bone_keyframes: Vec::with_capacity(bone_count),
641 morph_keyframes: Vec::new(),
642 property_keyframes: Vec::new(),
643 property_ik_frames: Vec::new(),
644 });
645 let mut bone_frames = options.build_parsed.then(|| Vec::with_capacity(bone_count));
646 for _ in 0..bone_count {
647 let bone_name_field = r.read_slice(15)?;
648 if let Some(summary) = summary.as_mut() {
649 summary.record_bone(trim_fixed_bytes(bone_name_field))?;
650 }
651 let raw_bone_name = options
652 .build_raw
653 .then(|| trim_fixed_bytes(bone_name_field).to_vec());
654 let raw_bone_name_normalized = raw_bone_name
655 .as_ref()
656 .map(|name_bytes| normalize_vmd_name(name_bytes));
657 let parsed_bone_name = options
658 .build_parsed
659 .then(|| decode_sjis_fixed(bone_name_field));
660 let parsed_bone_name_bytes = options
661 .build_parsed
662 .then(|| trim_fixed_bytes(bone_name_field).to_vec());
663 let frame = r.read_u32_le()?;
664 if options.build_parsed || options.build_summary {
665 max_frame = max_frame.max(frame);
666 }
667 let position = read_vmd_vec3(&mut r, options.build_summary, "bone translation")?;
668 let rotation = read_vmd_quat(&mut r, options.build_summary, "bone rotation")?;
669 let interpolation: [u8; 64] = r.read_slice(64)?.try_into().unwrap();
670 if let Some(raw) = raw.as_mut() {
671 raw.bone_keyframes.push(VmdBoneKeyframeRaw {
672 bone_mode: VmdBoneImportMode::ByName(
673 raw_bone_name.expect("raw VMD scan must build bone names"),
674 ),
675 frame,
676 position,
677 rotation,
678 interpolation,
679 bone_name_normalized: raw_bone_name_normalized
680 .expect("raw VMD scan must normalize bone names"),
681 });
682 }
683 if let Some(bone_frames) = bone_frames.as_mut() {
684 bone_frames.push(VmdParsedBoneFrame {
685 bone_name: parsed_bone_name.expect("parsed VMD scan must decode bone names"),
686 bone_name_bytes: parsed_bone_name_bytes
687 .expect("parsed VMD scan must retain bone name bytes"),
688 frame,
689 translation: [position.x, position.y, position.z],
690 rotation: [rotation.x, rotation.y, rotation.z, rotation.w],
691 interpolation: interpolation.to_vec(),
692 });
693 }
694 }
695
696 let Some(morph_count) = r.read_optional_u32_le()? else {
697 return Ok(vmd_scan_result(
698 model_name,
699 model_name_bytes,
700 header.model_name_bytes,
701 max_frame,
702 raw,
703 bone_frames.map(|bone_frames| VmdParsedSections {
704 bone_frames,
705 morph_frames: Vec::new(),
706 camera_frames: Vec::new(),
707 light_frames: Vec::new(),
708 self_shadow_frames: Vec::new(),
709 property_frames: Vec::new(),
710 }),
711 summary.map(|summary| summary.finish(header.model_name_bytes, max_frame)),
712 ));
713 };
714 let morph_count = morph_count as usize;
715 r.require_record_bytes(morph_count, 23)?;
716 if let Some(raw) = raw.as_mut() {
717 raw.morph_keyframes = Vec::with_capacity(morph_count);
718 }
719 let mut morph_frames = options
720 .build_parsed
721 .then(|| Vec::with_capacity(morph_count));
722 for _ in 0..morph_count {
723 let morph_name_field = r.read_slice(15)?;
724 if let Some(summary) = summary.as_mut() {
725 summary.record_morph(trim_fixed_bytes(morph_name_field))?;
726 }
727 let raw_morph_name = options
728 .build_raw
729 .then(|| trim_fixed_bytes(morph_name_field).to_vec());
730 let parsed_morph_name = options
731 .build_parsed
732 .then(|| decode_sjis_fixed(morph_name_field));
733 let parsed_morph_name_bytes = options
734 .build_parsed
735 .then(|| trim_fixed_bytes(morph_name_field).to_vec());
736 let frame = r.read_u32_le()?;
737 if options.build_parsed || options.build_summary {
738 max_frame = max_frame.max(frame);
739 }
740 let weight = read_vmd_f32(&mut r, options.build_summary, "morph weight")?;
741 if let Some(raw) = raw.as_mut() {
742 raw.morph_keyframes.push((
743 raw_morph_name.expect("raw VMD scan must build morph names"),
744 frame,
745 weight,
746 ));
747 }
748 if let Some(morph_frames) = morph_frames.as_mut() {
749 morph_frames.push(VmdParsedMorphFrame {
750 morph_name: parsed_morph_name.expect("parsed VMD scan must decode morph names"),
751 morph_name_bytes: parsed_morph_name_bytes
752 .expect("parsed VMD scan must retain morph name bytes"),
753 frame,
754 weight,
755 });
756 }
757 }
758
759 let camera_frames = if options.build_parsed || options.build_summary {
760 read_camera_frames(&mut r, &mut max_frame, options.build_parsed, &mut summary)?
761 } else {
762 skip_optional_records(&mut r, 61)?;
763 None
764 };
765 let light_frames = if options.build_parsed || options.build_summary {
766 read_light_frames(&mut r, &mut max_frame, options.build_parsed, &mut summary)?
767 } else {
768 skip_optional_records(&mut r, 28)?;
769 None
770 };
771 let self_shadow_frames = if options.build_parsed || options.build_summary {
772 read_self_shadow_frames(&mut r, &mut max_frame, options.build_parsed, &mut summary)?
773 } else {
774 skip_optional_records(&mut r, 9)?;
775 None
776 };
777 let property_frames = read_property_frames(&mut r, &mut max_frame, options, &mut summary)?;
778
779 if let Some(raw) = raw.as_mut() {
780 raw.property_keyframes = property_frames
781 .raw_keyframes
782 .expect("raw VMD scan must build property keyframes");
783 raw.property_ik_frames = property_frames
784 .raw_ik_frames
785 .expect("raw VMD scan must build property IK frames");
786 }
787 let parsed = bone_frames.map(|bone_frames| VmdParsedSections {
788 bone_frames,
789 morph_frames: morph_frames.expect("parsed VMD scan must build morph frames"),
790 camera_frames: camera_frames.expect("parsed VMD scan must build camera frames"),
791 light_frames: light_frames.expect("parsed VMD scan must build light frames"),
792 self_shadow_frames: self_shadow_frames
793 .expect("parsed VMD scan must build self-shadow frames"),
794 property_frames: property_frames
795 .parsed_frames
796 .expect("parsed VMD scan must build property frames"),
797 });
798
799 Ok(vmd_scan_result(
800 model_name,
801 model_name_bytes,
802 header.model_name_bytes,
803 max_frame,
804 raw,
805 parsed,
806 summary.map(|summary| summary.finish(header.model_name_bytes, max_frame)),
807 ))
808}
809
810pub fn parse_vmd_animation(data: &[u8]) -> Result<VmdParsedAnimation, ImportError> {
811 Ok(parse_vmd_scan(data, VmdScanOptions::PARSED)?
812 .parsed
813 .expect("parsed-only VMD scan must produce parsed output"))
814}
815
816fn default_vmd_kind() -> &'static str {
817 "vmd"
818}
819
820fn default_vmd_format() -> &'static str {
821 "vmd"
822}
823
824pub fn export_vmd_animation(animation: &VmdParsedAnimation) -> Vec<u8> {
825 let mut out = Vec::new();
826 out.extend_from_slice(&VMD_MAGIC);
827 write_fixed_name_bytes(
828 &mut out,
829 &animation.metadata.model_name,
830 &animation.metadata.model_name_bytes,
831 20,
832 );
833
834 write_u32(&mut out, animation.bone_frames.len() as u32);
835 for frame in &animation.bone_frames {
836 write_fixed_name_bytes(&mut out, &frame.bone_name, &frame.bone_name_bytes, 15);
837 write_u32(&mut out, frame.frame);
838 write_f32(&mut out, frame.translation[0]);
839 write_f32(&mut out, frame.translation[1]);
840 write_f32(&mut out, frame.translation[2]);
841 write_f32(&mut out, frame.rotation[0]);
842 write_f32(&mut out, frame.rotation[1]);
843 write_f32(&mut out, frame.rotation[2]);
844 write_f32(&mut out, frame.rotation[3]);
845 write_fixed_bytes(&mut out, &frame.interpolation, 64);
846 }
847
848 write_u32(&mut out, animation.morph_frames.len() as u32);
849 for frame in &animation.morph_frames {
850 write_fixed_name_bytes(&mut out, &frame.morph_name, &frame.morph_name_bytes, 15);
851 write_u32(&mut out, frame.frame);
852 write_f32(&mut out, frame.weight);
853 }
854
855 write_u32(&mut out, animation.camera_frames.len() as u32);
856 for frame in &animation.camera_frames {
857 write_u32(&mut out, frame.frame);
858 write_f32(&mut out, frame.distance);
859 write_f32(&mut out, frame.position[0]);
860 write_f32(&mut out, frame.position[1]);
861 write_f32(&mut out, frame.position[2]);
862 write_f32(&mut out, frame.rotation[0]);
863 write_f32(&mut out, frame.rotation[1]);
864 write_f32(&mut out, frame.rotation[2]);
865 out.extend_from_slice(&frame.interpolation);
866 write_u32(&mut out, frame.fov);
867 out.push(if frame.perspective { 0 } else { 1 });
868 }
869
870 write_u32(&mut out, animation.light_frames.len() as u32);
871 for frame in &animation.light_frames {
872 write_u32(&mut out, frame.frame);
873 write_f32(&mut out, frame.color[0]);
874 write_f32(&mut out, frame.color[1]);
875 write_f32(&mut out, frame.color[2]);
876 write_f32(&mut out, frame.direction[0]);
877 write_f32(&mut out, frame.direction[1]);
878 write_f32(&mut out, frame.direction[2]);
879 }
880
881 write_u32(&mut out, animation.self_shadow_frames.len() as u32);
882 for frame in &animation.self_shadow_frames {
883 write_u32(&mut out, frame.frame);
884 out.push(frame.mode);
885 write_f32(&mut out, frame.distance);
886 }
887
888 write_u32(&mut out, animation.property_frames.len() as u32);
889 for frame in &animation.property_frames {
890 write_u32(&mut out, frame.frame);
891 out.push(u8::from(frame.visible));
892 write_u32(&mut out, frame.ik_states.len() as u32);
893 for state in &frame.ik_states {
894 write_fixed_name_bytes(&mut out, &state.bone_name, &state.bone_name_bytes, 20);
895 out.push(u8::from(state.enabled));
896 }
897 }
898
899 out
900}
901
902struct VmdParsedSections {
903 bone_frames: Vec<VmdParsedBoneFrame>,
904 morph_frames: Vec<VmdParsedMorphFrame>,
905 camera_frames: Vec<VmdParsedCameraFrame>,
906 light_frames: Vec<VmdParsedLightFrame>,
907 self_shadow_frames: Vec<VmdParsedSelfShadowFrame>,
908 property_frames: Vec<VmdParsedPropertyFrame>,
909}
910
911fn vmd_parsed_animation(
912 model_name: String,
913 model_name_bytes: Vec<u8>,
914 max_frame: u32,
915 sections: VmdParsedSections,
916) -> VmdParsedAnimation {
917 VmdParsedAnimation {
918 kind: "vmd",
919 metadata: VmdParsedMetadata {
920 format: "vmd",
921 model_name,
922 model_name_bytes,
923 counts: VmdParsedCounts {
924 bones: sections.bone_frames.len(),
925 morphs: sections.morph_frames.len(),
926 cameras: sections.camera_frames.len(),
927 lights: sections.light_frames.len(),
928 self_shadows: sections.self_shadow_frames.len(),
929 properties: sections.property_frames.len(),
930 },
931 max_frame,
932 },
933 bone_frames: sections.bone_frames,
934 morph_frames: sections.morph_frames,
935 camera_frames: sections.camera_frames,
936 light_frames: sections.light_frames,
937 self_shadow_frames: sections.self_shadow_frames,
938 property_frames: sections.property_frames,
939 }
940}
941
942fn vmd_scan_result(
943 model_name: Option<String>,
944 model_name_bytes: Option<Vec<u8>>,
945 target_model_name_bytes: [u8; 20],
946 max_frame: u32,
947 raw: Option<VmdImportResult>,
948 sections: Option<VmdParsedSections>,
949 summary: Option<VmdSharedContextSummary>,
950) -> VmdScanResult {
951 VmdScanResult {
952 raw,
953 target_model_name_bytes,
954 summary,
955 parsed: sections.map(|sections| {
956 vmd_parsed_animation(
957 model_name.expect("parsed VMD scan must decode model name"),
958 model_name_bytes.expect("parsed VMD scan must retain model name bytes"),
959 max_frame,
960 sections,
961 )
962 }),
963 }
964}
965
966fn skip_optional_records(r: &mut Reader<'_>, record_size: usize) -> Result<(), ImportError> {
967 if let Some(count) = r.read_optional_record_count(record_size)? {
968 r.skip(count * record_size)?;
969 }
970 Ok(())
971}
972
973fn read_camera_frames(
974 r: &mut Reader<'_>,
975 max_frame: &mut u32,
976 build_parsed: bool,
977 summary: &mut Option<VmdSummaryAccumulator>,
978) -> Result<Option<Vec<VmdParsedCameraFrame>>, ImportError> {
979 let Some(count) = r.read_optional_record_count(61)? else {
980 return Ok(build_parsed.then(Vec::new));
981 };
982 let mut frames = build_parsed.then(|| Vec::with_capacity(count));
983 for _ in 0..count {
984 let frame = r.read_u32_le()?;
985 *max_frame = (*max_frame).max(frame);
986 let distance = read_vmd_f32(r, summary.is_some(), "camera distance")?;
987 let position = read_vmd_vec3(r, summary.is_some(), "camera position")?;
988 let rotation = read_vmd_vec3(r, summary.is_some(), "camera rotation")?;
989 let interpolation: [u8; 24] = r.read_slice(24)?.try_into().unwrap();
990 let fov = r.read_u32_le()?;
991 let perspective = r.read_u8()? == 0;
992 if let Some(summary) = summary.as_mut() {
993 summary.record_camera()?;
994 }
995 if let Some(frames) = frames.as_mut() {
996 frames.push(VmdParsedCameraFrame {
997 frame,
998 distance,
999 position: [position.x, position.y, position.z],
1000 rotation: [rotation.x, rotation.y, rotation.z],
1001 interpolation,
1002 fov,
1003 perspective,
1004 });
1005 }
1006 }
1007 Ok(frames)
1008}
1009
1010fn read_light_frames(
1011 r: &mut Reader<'_>,
1012 max_frame: &mut u32,
1013 build_parsed: bool,
1014 summary: &mut Option<VmdSummaryAccumulator>,
1015) -> Result<Option<Vec<VmdParsedLightFrame>>, ImportError> {
1016 let Some(count) = r.read_optional_record_count(28)? else {
1017 return Ok(build_parsed.then(Vec::new));
1018 };
1019 let mut frames = build_parsed.then(|| Vec::with_capacity(count));
1020 for _ in 0..count {
1021 let frame = r.read_u32_le()?;
1022 *max_frame = (*max_frame).max(frame);
1023 let color = read_vmd_vec3(r, summary.is_some(), "light color")?;
1024 let direction = read_vmd_vec3(r, summary.is_some(), "light direction")?;
1025 if let Some(summary) = summary.as_mut() {
1026 summary.record_light()?;
1027 }
1028 if let Some(frames) = frames.as_mut() {
1029 frames.push(VmdParsedLightFrame {
1030 frame,
1031 color: [color.x, color.y, color.z],
1032 direction: [direction.x, direction.y, direction.z],
1033 });
1034 }
1035 }
1036 Ok(frames)
1037}
1038
1039fn read_self_shadow_frames(
1040 r: &mut Reader<'_>,
1041 max_frame: &mut u32,
1042 build_parsed: bool,
1043 summary: &mut Option<VmdSummaryAccumulator>,
1044) -> Result<Option<Vec<VmdParsedSelfShadowFrame>>, ImportError> {
1045 let Some(count) = r.read_optional_record_count(9)? else {
1046 return Ok(build_parsed.then(Vec::new));
1047 };
1048 let mut frames = build_parsed.then(|| Vec::with_capacity(count));
1049 for _ in 0..count {
1050 let frame = r.read_u32_le()?;
1051 *max_frame = (*max_frame).max(frame);
1052 let mode = r.read_u8()?;
1053 let distance = read_vmd_f32(r, summary.is_some(), "self-shadow distance")?;
1054 if let Some(summary) = summary.as_mut() {
1055 summary.record_self_shadow()?;
1056 }
1057 if let Some(frames) = frames.as_mut() {
1058 frames.push(VmdParsedSelfShadowFrame {
1059 frame,
1060 mode,
1061 distance,
1062 });
1063 }
1064 }
1065 Ok(frames)
1066}
1067
1068fn read_property_frames(
1069 r: &mut Reader<'_>,
1070 max_frame: &mut u32,
1071 options: VmdScanOptions,
1072 summary: &mut Option<VmdSummaryAccumulator>,
1073) -> Result<VmdPropertyFrameReadResult, ImportError> {
1074 let Some(count) = r.read_optional_u32_le()? else {
1075 return Ok(VmdPropertyFrameReadResult {
1076 parsed_frames: options.build_parsed.then(Vec::new),
1077 raw_keyframes: options.build_raw.then(Vec::new),
1078 raw_ik_frames: options.build_raw.then(Vec::new),
1079 });
1080 };
1081 let count = count as usize;
1082 r.require_record_bytes(count, 9)?;
1083 let mut parsed_frames = options.build_parsed.then(|| Vec::with_capacity(count));
1084 let mut raw_keyframes = options.build_raw.then(|| Vec::with_capacity(count));
1085 let mut raw_ik_frames = options.build_raw.then(|| Vec::with_capacity(count));
1086 for _ in 0..count {
1087 let frame = r.read_u32_le()?;
1088 if options.build_parsed || options.build_summary {
1089 *max_frame = (*max_frame).max(frame);
1090 }
1091 let show = r.read_u8()?;
1092 let ik_count = r.read_u32_le()? as usize;
1093 r.require_record_bytes(ik_count, 21)?;
1094 if let Some(summary) = summary.as_mut() {
1095 summary.record_property(ik_count)?;
1096 }
1097 let mut raw_ik_enabled = options.build_raw.then(|| Vec::with_capacity(ik_count));
1098 let mut raw_ik_entries = options.build_raw.then(|| Vec::with_capacity(ik_count));
1099 let mut ik_states = options.build_parsed.then(|| Vec::with_capacity(ik_count));
1100 for _ in 0..ik_count {
1101 let name_field = r.read_slice(20)?;
1102 if options.build_summary {
1103 validate_summary_name(trim_fixed_bytes(name_field), "empty property IK name")?;
1104 }
1105 let raw_name_bytes = options.build_raw.then(|| name_field.to_vec());
1106 let raw_name_normalized = raw_name_bytes
1107 .as_ref()
1108 .map(|name_bytes| normalize_vmd_name(trim_fixed_bytes(name_bytes)));
1109 let parsed_name = options.build_parsed.then(|| decode_sjis_fixed(name_field));
1110 let parsed_name_bytes = options
1111 .build_parsed
1112 .then(|| trim_fixed_bytes(name_field).to_vec());
1113 let enabled = r.read_u8()?;
1114 if let Some(raw_ik_enabled) = raw_ik_enabled.as_mut() {
1115 raw_ik_enabled.push(enabled);
1116 }
1117 if let Some(raw_ik_entries) = raw_ik_entries.as_mut() {
1118 raw_ik_entries.push(VmdIkEntry {
1119 name_bytes: raw_name_bytes
1120 .expect("raw VMD scan must retain property name bytes"),
1121 enabled,
1122 name_normalized: raw_name_normalized
1123 .expect("raw VMD scan must normalize property names"),
1124 });
1125 }
1126 if let Some(ik_states) = ik_states.as_mut() {
1127 ik_states.push(VmdParsedIkState {
1128 bone_name: parsed_name.expect("parsed VMD scan must decode property names"),
1129 bone_name_bytes: parsed_name_bytes
1130 .expect("parsed VMD scan must retain property name bytes"),
1131 enabled: enabled != 0,
1132 });
1133 }
1134 }
1135 if let Some(parsed_frames) = parsed_frames.as_mut() {
1136 parsed_frames.push(VmdParsedPropertyFrame {
1137 frame,
1138 visible: show != 0,
1139 ik_states: ik_states.expect("parsed VMD scan must build IK states"),
1140 });
1141 }
1142 if let Some(raw_keyframes) = raw_keyframes.as_mut() {
1143 raw_keyframes.push(PropertyKeyframe::new(
1144 frame,
1145 raw_ik_enabled
1146 .expect("raw VMD scan must build IK enabled values")
1147 .into_iter()
1148 .map(|value| value != 0)
1149 .collect(),
1150 ));
1151 }
1152 if let Some(raw_ik_frames) = raw_ik_frames.as_mut() {
1153 raw_ik_frames.push(VmdPropertyIkFrame {
1154 frame,
1155 show,
1156 entries: raw_ik_entries.expect("raw VMD scan must build property IK entries"),
1157 });
1158 }
1159 }
1160 Ok(VmdPropertyFrameReadResult {
1161 parsed_frames,
1162 raw_keyframes,
1163 raw_ik_frames,
1164 })
1165}
1166
1167pub fn sample_vmd_camera_frames(
1168 frames: &[VmdParsedCameraFrame],
1169 frame: f32,
1170) -> Option<VmdCameraState> {
1171 if frames.is_empty() {
1172 return None;
1173 }
1174
1175 let mut sorted: Vec<&VmdParsedCameraFrame> = frames.iter().collect();
1176 sorted.sort_by_key(|keyframe| keyframe.frame);
1177
1178 let mut index = 0usize;
1179 while index + 1 < sorted.len() && sorted[index + 1].frame as f32 <= frame {
1180 index += 1;
1181 }
1182
1183 let previous = sorted[index];
1184 let next = sorted.get(index + 1).copied().unwrap_or(previous);
1185 let t = interpolation_ratio(previous.frame, next.frame, frame);
1186 let interpolation = decode_camera_interpolation(&next.interpolation);
1187
1188 let distance_t = interpolation.distance.evaluate(t);
1189 let position_x_t = interpolation.position.x.evaluate(t);
1190 let position_y_t = interpolation.position.y.evaluate(t);
1191 let position_z_t = interpolation.position.z.evaluate(t);
1192 let rotation_t = interpolation.rotation.evaluate(t);
1193 let fov_t = interpolation.fov.evaluate(t);
1194
1195 Some(VmdCameraState {
1196 distance: lerp(previous.distance, next.distance, distance_t),
1197 position: [
1198 lerp(previous.position[0], next.position[0], position_x_t),
1199 lerp(previous.position[1], next.position[1], position_y_t),
1200 lerp(previous.position[2], next.position[2], position_z_t),
1201 ],
1202 rotation: [
1203 lerp(previous.rotation[0], next.rotation[0], rotation_t),
1204 lerp(previous.rotation[1], next.rotation[1], rotation_t),
1205 lerp(previous.rotation[2], next.rotation[2], rotation_t),
1206 ],
1207 fov: lerp(previous.fov as f32, next.fov as f32, fov_t),
1208 perspective: if t < 1.0 {
1209 previous.perspective
1210 } else {
1211 next.perspective
1212 },
1213 })
1214}
1215
1216pub fn sample_vmd_light_frames(
1217 frames: &[VmdParsedLightFrame],
1218 frame: f32,
1219) -> Option<VmdLightState> {
1220 if frames.is_empty() {
1221 return None;
1222 }
1223
1224 let mut sorted: Vec<&VmdParsedLightFrame> = frames.iter().collect();
1225 sorted.sort_by_key(|keyframe| keyframe.frame);
1226
1227 let mut index = 0usize;
1228 while index + 1 < sorted.len() && sorted[index + 1].frame as f32 <= frame {
1229 index += 1;
1230 }
1231
1232 let previous = sorted[index];
1233 let next = sorted.get(index + 1).copied().unwrap_or(previous);
1234 let t = interpolation_ratio(previous.frame, next.frame, frame);
1235
1236 Some(VmdLightState {
1237 color: [
1238 lerp(previous.color[0], next.color[0], t),
1239 lerp(previous.color[1], next.color[1], t),
1240 lerp(previous.color[2], next.color[2], t),
1241 ],
1242 direction: [
1243 lerp(previous.direction[0], next.direction[0], t),
1244 lerp(previous.direction[1], next.direction[1], t),
1245 lerp(previous.direction[2], next.direction[2], t),
1246 ],
1247 })
1248}
1249
1250pub fn sample_vmd_self_shadow_frames(
1251 frames: &[VmdParsedSelfShadowFrame],
1252 frame: f32,
1253) -> Option<VmdSelfShadowState> {
1254 if frames.is_empty() {
1255 return None;
1256 }
1257
1258 let mut sorted: Vec<&VmdParsedSelfShadowFrame> = frames.iter().collect();
1259 sorted.sort_by_key(|keyframe| keyframe.frame);
1260
1261 let mut index = 0usize;
1262 while index + 1 < sorted.len() && sorted[index + 1].frame as f32 <= frame {
1263 index += 1;
1264 }
1265
1266 let previous = sorted[index];
1267 let next = sorted.get(index + 1).copied().unwrap_or(previous);
1268 let t = interpolation_ratio(previous.frame, next.frame, frame);
1269
1270 Some(VmdSelfShadowState {
1271 mode: if t < 1.0 { previous.mode } else { next.mode },
1272 distance: lerp(previous.distance, next.distance, t),
1273 })
1274}
1275
1276struct CameraInterpolation {
1277 position: InterpolationVector3,
1278 rotation: InterpolationScalar,
1279 distance: InterpolationScalar,
1280 fov: InterpolationScalar,
1281}
1282
1283fn decode_camera_interpolation(interpolation: &[u8; 24]) -> CameraInterpolation {
1284 CameraInterpolation {
1285 position: InterpolationVector3 {
1286 x: decode_camera_interpolation_scalar(interpolation, 0),
1287 y: decode_camera_interpolation_scalar(interpolation, 1),
1288 z: decode_camera_interpolation_scalar(interpolation, 2),
1289 },
1290 rotation: decode_camera_interpolation_scalar(interpolation, 3),
1291 distance: decode_camera_interpolation_scalar(interpolation, 4),
1292 fov: decode_camera_interpolation_scalar(interpolation, 5),
1293 }
1294}
1295
1296fn decode_camera_interpolation_scalar(
1297 interpolation: &[u8; 24],
1298 channel: usize,
1299) -> InterpolationScalar {
1300 let offset = channel * 4;
1301 decode_interpolation_scalar([
1302 interpolation[offset],
1303 interpolation[offset + 1],
1304 interpolation[offset + 2],
1305 interpolation[offset + 3],
1306 ])
1307}
1308
1309fn interpolation_ratio(previous_frame: u32, next_frame: u32, frame: f32) -> f32 {
1310 if next_frame <= previous_frame {
1311 return 0.0;
1312 }
1313 let span = next_frame - previous_frame;
1314 if span <= 1 {
1315 return if frame >= next_frame as f32 { 1.0 } else { 0.0 };
1316 }
1317 ((frame - previous_frame as f32) / span as f32).clamp(0.0, 1.0)
1318}
1319
1320fn lerp(a: f32, b: f32, t: f32) -> f32 {
1321 a + (b - a) * t
1322}
1323
1324fn decode_sjis_fixed(bytes: &[u8]) -> String {
1325 decode_sjis_fixed_trimmed(bytes)
1326}
1327
1328fn trim_fixed_bytes(bytes: &[u8]) -> &[u8] {
1329 let end = bytes.iter().position(|&b| b == 0).unwrap_or(bytes.len());
1330 &bytes[..end]
1331}
1332
1333fn write_fixed_name_bytes(out: &mut Vec<u8>, value: &str, raw_bytes: &[u8], len: usize) {
1334 if raw_bytes.is_empty() {
1335 let encoded = encode_sjis(value);
1336 write_fixed_bytes(out, &encoded, len);
1337 } else {
1338 write_fixed_bytes(out, raw_bytes, len);
1339 }
1340}
1341
1342fn decode_interpolation_scalar(data: [u8; 4]) -> InterpolationScalar {
1343 InterpolationScalar {
1344 x1: data[0].min(127),
1345 y1: data[1].min(127),
1346 x2: data[2].min(127),
1347 y2: data[3].min(127),
1348 }
1349}
1350
1351fn decode_bone_interpolation(
1352 interpolation: &[u8; 64],
1353) -> (InterpolationVector3, InterpolationScalar) {
1354 let position = InterpolationVector3 {
1355 x: decode_interpolation_scalar([
1356 interpolation[0],
1357 interpolation[4],
1358 interpolation[8],
1359 interpolation[12],
1360 ]),
1361 y: decode_interpolation_scalar([
1362 interpolation[1],
1363 interpolation[5],
1364 interpolation[9],
1365 interpolation[13],
1366 ]),
1367 z: decode_interpolation_scalar([
1368 interpolation[2],
1369 interpolation[6],
1370 interpolation[10],
1371 interpolation[14],
1372 ]),
1373 };
1374 let rotation = decode_interpolation_scalar([
1375 interpolation[3],
1376 interpolation[7],
1377 interpolation[11],
1378 interpolation[15],
1379 ]);
1380 (position, rotation)
1381}
1382
1383fn decode_mmd_registered_bone_interpolation(
1387 interpolation: &[u8; 64],
1388) -> (InterpolationVector3, InterpolationScalar) {
1389 let position = InterpolationVector3 {
1390 x: decode_interpolation_scalar([
1391 interpolation[0],
1392 interpolation[4],
1393 interpolation[8],
1394 interpolation[12],
1395 ]),
1396 y: decode_interpolation_scalar([
1397 interpolation[16],
1398 interpolation[20],
1399 interpolation[24],
1400 interpolation[28],
1401 ]),
1402 z: decode_interpolation_scalar([
1403 interpolation[32],
1404 interpolation[36],
1405 interpolation[40],
1406 interpolation[44],
1407 ]),
1408 };
1409 let rotation = decode_interpolation_scalar([
1410 interpolation[48],
1411 interpolation[52],
1412 interpolation[56],
1413 interpolation[60],
1414 ]);
1415 (position, rotation)
1416}
1417
1418pub fn build_clip_from_import(
1419 result: VmdImportResult,
1420 bone_name_to_index: &dyn Fn(&[u8]) -> Option<BoneIndex>,
1421 morph_name_to_index: &dyn Fn(&[u8]) -> Option<MorphIndex>,
1422) -> AnimationClip {
1423 let mut bone_tracks_map: std::collections::BTreeMap<u32, Vec<MovableBoneKeyframe>> =
1424 std::collections::BTreeMap::new();
1425
1426 for kf in result.bone_keyframes {
1427 let bone_index = match &kf.bone_mode {
1428 VmdBoneImportMode::ByName(_name) => {
1429 match bone_name_to_index(&kf.bone_name_normalized) {
1430 Some(idx) => idx,
1431 None => continue,
1432 }
1433 }
1434 VmdBoneImportMode::ByIndex(idx) => BoneIndex(*idx),
1435 };
1436
1437 let (pos_interp, rot_interp) = decode_bone_interpolation(&kf.interpolation);
1438
1439 bone_tracks_map
1440 .entry(bone_index.0)
1441 .or_default()
1442 .push(MovableBoneKeyframe {
1443 frame: kf.frame,
1444 position: kf.position,
1445 rotation: kf.rotation,
1446 position_interpolation: pos_interp,
1447 rotation_interpolation: rot_interp,
1448 });
1449 }
1450
1451 let bone_tracks: Vec<BoneAnimationBinding> = bone_tracks_map
1452 .into_iter()
1453 .map(|(bone_idx, kfs)| BoneAnimationBinding {
1454 bone: BoneIndex(bone_idx),
1455 track: MovableBoneTrack::from_keyframes(kfs),
1456 })
1457 .collect();
1458
1459 let mut morph_tracks_map: std::collections::BTreeMap<u32, Vec<MorphKeyframe>> =
1460 std::collections::BTreeMap::new();
1461
1462 for (morph_name, frame, weight) in result.morph_keyframes {
1463 let morph_name_normalized = normalize_vmd_name(&morph_name);
1464 let morph_index = match morph_name_to_index(&morph_name_normalized) {
1465 Some(idx) => idx,
1466 None => continue,
1467 };
1468 morph_tracks_map
1469 .entry(morph_index.0)
1470 .or_default()
1471 .push(MorphKeyframe::new(frame, weight));
1472 }
1473
1474 let morph_tracks: Vec<MorphAnimationBinding> = morph_tracks_map
1475 .into_iter()
1476 .map(|(morph_idx, kfs)| MorphAnimationBinding {
1477 morph: MorphIndex(morph_idx),
1478 track: MorphTrack::from_keyframes(kfs),
1479 })
1480 .collect();
1481
1482 let property_track = if result.property_keyframes.is_empty() {
1483 None
1484 } else {
1485 Some(PropertyAnimationBinding::from_keyframes(
1486 result.property_keyframes,
1487 ))
1488 };
1489
1490 AnimationClip::new_full(bone_tracks, morph_tracks, property_track)
1491}
1492
1493pub fn build_property_binding_with_ik_resolver(
1494 ik_frames: &[VmdPropertyIkFrame],
1495 ik_name_to_solver_index: &dyn Fn(&[u8]) -> Option<usize>,
1496 solver_count: usize,
1497) -> Option<PropertyAnimationBinding> {
1498 if solver_count == 0 || ik_frames.is_empty() {
1499 return None;
1500 }
1501
1502 let keyframes: Vec<PropertyKeyframe> = ik_frames
1503 .iter()
1504 .map(|frame| {
1505 let mut ik_enabled = vec![1u8; solver_count];
1506 for entry in &frame.entries {
1507 match ik_name_to_solver_index(&entry.name_normalized) {
1508 Some(idx) if idx < solver_count => {
1509 ik_enabled[idx] = entry.enabled;
1510 }
1511 _ => {}
1512 }
1513 }
1514 PropertyKeyframe {
1515 frame: frame.frame,
1516 ik_enabled: ik_enabled.into_boxed_slice(),
1517 }
1518 })
1519 .collect();
1520
1521 Some(PropertyAnimationBinding::from_keyframes(keyframes))
1522}
1523
1524#[derive(Debug, Clone, Copy)]
1526pub struct VmdClipBuildOptions {
1527 pub honor_property_ik: bool,
1533}
1534
1535#[derive(Debug, Error, Clone, PartialEq, Eq)]
1537pub enum VmdClipBuildError {
1538 #[error(
1541 "VMD bone map contains bone index {bone_index:?} outside model bone count {bone_count}"
1542 )]
1543 BoneIndexOutOfRange {
1544 bone_index: BoneIndex,
1545 bone_count: usize,
1546 },
1547}
1548
1549impl Default for VmdClipBuildOptions {
1550 fn default() -> Self {
1551 Self {
1552 honor_property_ik: true,
1553 }
1554 }
1555}
1556
1557pub fn build_pair_clip(
1558 result: &VmdImportResult,
1559 bone_name_to_index: &std::collections::HashMap<Vec<u8>, BoneIndex>,
1560 morph_name_to_index: &std::collections::HashMap<Vec<u8>, MorphIndex>,
1561 ik_solver_bone_name_to_index: &std::collections::HashMap<Vec<u8>, usize>,
1562 solver_count: usize,
1563) -> AnimationClip {
1564 build_pair_clip_with_options(
1565 result,
1566 bone_name_to_index,
1567 morph_name_to_index,
1568 ik_solver_bone_name_to_index,
1569 solver_count,
1570 VmdClipBuildOptions::default(),
1571 )
1572}
1573
1574pub fn build_mmd_registered_pair_clip(
1582 model: &ModelArena,
1583 result: &VmdImportResult,
1584 bone_name_to_index: &std::collections::HashMap<Vec<u8>, BoneIndex>,
1585 morph_name_to_index: &std::collections::HashMap<Vec<u8>, MorphIndex>,
1586 ik_solver_bone_name_to_index: &std::collections::HashMap<Vec<u8>, usize>,
1587 solver_count: usize,
1588) -> Result<AnimationClip, VmdClipBuildError> {
1589 build_mmd_registered_pair_clip_with_options(
1590 model,
1591 result,
1592 bone_name_to_index,
1593 morph_name_to_index,
1594 ik_solver_bone_name_to_index,
1595 solver_count,
1596 VmdClipBuildOptions::default(),
1597 )
1598}
1599
1600pub fn build_pair_clip_with_options(
1601 result: &VmdImportResult,
1602 bone_name_to_index: &std::collections::HashMap<Vec<u8>, BoneIndex>,
1603 morph_name_to_index: &std::collections::HashMap<Vec<u8>, MorphIndex>,
1604 ik_solver_bone_name_to_index: &std::collections::HashMap<Vec<u8>, usize>,
1605 solver_count: usize,
1606 options: VmdClipBuildOptions,
1607) -> AnimationClip {
1608 build_pair_clip_with_registration_policy(
1609 result,
1610 bone_name_to_index,
1611 morph_name_to_index,
1612 ik_solver_bone_name_to_index,
1613 solver_count,
1614 options,
1615 VmdRegistrationPolicy::Raw,
1616 )
1617}
1618
1619pub fn build_mmd_registered_pair_clip_with_options(
1622 model: &ModelArena,
1623 result: &VmdImportResult,
1624 bone_name_to_index: &std::collections::HashMap<Vec<u8>, BoneIndex>,
1625 morph_name_to_index: &std::collections::HashMap<Vec<u8>, MorphIndex>,
1626 ik_solver_bone_name_to_index: &std::collections::HashMap<Vec<u8>, usize>,
1627 solver_count: usize,
1628 options: VmdClipBuildOptions,
1629) -> Result<AnimationClip, VmdClipBuildError> {
1630 validate_bone_map(model, bone_name_to_index)?;
1631 Ok(build_pair_clip_with_registration_policy(
1632 result,
1633 bone_name_to_index,
1634 morph_name_to_index,
1635 ik_solver_bone_name_to_index,
1636 solver_count,
1637 options,
1638 VmdRegistrationPolicy::MmdRegistered(model),
1639 ))
1640}
1641
1642#[derive(Clone, Copy)]
1643enum VmdRegistrationPolicy<'a> {
1644 Raw,
1645 MmdRegistered(&'a ModelArena),
1646}
1647
1648fn validate_bone_map(
1649 model: &ModelArena,
1650 bone_name_to_index: &std::collections::HashMap<Vec<u8>, BoneIndex>,
1651) -> Result<(), VmdClipBuildError> {
1652 let bone_count = model.bone_count();
1653 if let Some(&bone_index) = bone_name_to_index
1654 .values()
1655 .find(|bone_index| bone_index.as_usize() >= bone_count)
1656 {
1657 return Err(VmdClipBuildError::BoneIndexOutOfRange {
1658 bone_index,
1659 bone_count,
1660 });
1661 }
1662 Ok(())
1663}
1664
1665fn build_pair_clip_with_registration_policy(
1666 result: &VmdImportResult,
1667 bone_name_to_index: &std::collections::HashMap<Vec<u8>, BoneIndex>,
1668 morph_name_to_index: &std::collections::HashMap<Vec<u8>, MorphIndex>,
1669 ik_solver_bone_name_to_index: &std::collections::HashMap<Vec<u8>, usize>,
1670 solver_count: usize,
1671 options: VmdClipBuildOptions,
1672 registration_policy: VmdRegistrationPolicy<'_>,
1673) -> AnimationClip {
1674 let mut bone_tracks_map: std::collections::BTreeMap<u32, Vec<MovableBoneKeyframe>> =
1675 std::collections::BTreeMap::new();
1676
1677 for kf in &result.bone_keyframes {
1678 let bone_index = match bone_name_to_index.get(&kf.bone_name_normalized) {
1679 Some(idx) => *idx,
1680 None => continue,
1681 };
1682
1683 bone_tracks_map
1684 .entry(bone_index.0)
1685 .or_default()
1686 .push(build_mapped_bone_keyframe(
1687 kf,
1688 bone_index,
1689 registration_policy,
1690 ));
1691 }
1692
1693 let bone_tracks: Vec<BoneAnimationBinding> = bone_tracks_map
1694 .into_iter()
1695 .map(|(bone_idx, kfs)| BoneAnimationBinding {
1696 bone: BoneIndex(bone_idx),
1697 track: MovableBoneTrack::from_keyframes(kfs),
1698 })
1699 .collect();
1700
1701 let mut morph_tracks_map: std::collections::BTreeMap<u32, Vec<MorphKeyframe>> =
1702 std::collections::BTreeMap::new();
1703
1704 for (morph_name, frame, weight) in &result.morph_keyframes {
1705 let morph_name_normalized = normalize_vmd_name(morph_name);
1706 let morph_index = match morph_name_to_index.get(&morph_name_normalized) {
1707 Some(idx) => *idx,
1708 None => continue,
1709 };
1710 morph_tracks_map
1711 .entry(morph_index.0)
1712 .or_default()
1713 .push(MorphKeyframe::new(*frame, *weight));
1714 }
1715
1716 let morph_tracks: Vec<MorphAnimationBinding> = morph_tracks_map
1717 .into_iter()
1718 .map(|(morph_idx, kfs)| MorphAnimationBinding {
1719 morph: MorphIndex(morph_idx),
1720 track: MorphTrack::from_keyframes(kfs),
1721 })
1722 .collect();
1723
1724 let property_track = if options.honor_property_ik {
1725 build_property_binding_with_ik_resolver(
1726 &result.property_ik_frames,
1727 &|name| ik_solver_bone_name_to_index.get(name).copied(),
1728 solver_count,
1729 )
1730 } else {
1731 None
1732 };
1733
1734 AnimationClip::new_full(bone_tracks, morph_tracks, property_track)
1735}
1736
1737fn build_mapped_bone_keyframe(
1738 kf: &VmdBoneKeyframeRaw,
1739 bone_index: BoneIndex,
1740 registration_policy: VmdRegistrationPolicy<'_>,
1741) -> MovableBoneKeyframe {
1742 let (position_interpolation, rotation_interpolation) = match registration_policy {
1743 VmdRegistrationPolicy::Raw => decode_bone_interpolation(&kf.interpolation),
1744 VmdRegistrationPolicy::MmdRegistered(_) => {
1745 decode_mmd_registered_bone_interpolation(&kf.interpolation)
1746 }
1747 };
1748 let rotation = match registration_policy {
1749 VmdRegistrationPolicy::Raw => kf.rotation,
1750 VmdRegistrationPolicy::MmdRegistered(model) => {
1751 model.fixed_axis(bone_index).map_or(kf.rotation, |axis| {
1752 project_rotation_to_fixed_axis(kf.rotation, axis)
1753 })
1754 }
1755 };
1756 MovableBoneKeyframe {
1757 frame: kf.frame,
1758 position: kf.position,
1759 rotation,
1760 position_interpolation,
1761 rotation_interpolation,
1762 }
1763}
1764
1765fn project_rotation_to_fixed_axis(rotation: Quat, axis: Vec3A) -> Quat {
1766 let axis = axis.normalize();
1767 let vector = Vec3A::new(rotation.x, rotation.y, rotation.z);
1768 let sign = if vector.dot(axis) < 0.0 { -1.0 } else { 1.0 };
1769 let projected = axis * vector.length() * sign;
1770 Quat::from_xyzw(projected.x, projected.y, projected.z, rotation.w)
1771}
1772
1773#[cfg(test)]
1774mod tests {
1775 use super::*;
1776 #[allow(unused_imports)]
1777 use serde_json;
1778
1779 fn build_vmd_header_bytes() -> Vec<u8> {
1780 let mut buf = Vec::new();
1781 buf.extend_from_slice(&VMD_MAGIC);
1782 buf.extend_from_slice(&[0u8; 20]);
1783 buf
1784 }
1785
1786 #[test]
1787 fn rejects_impossible_vmd_bone_count_before_allocation() {
1788 let mut buf = build_vmd_header_bytes();
1789 buf.extend_from_slice(&u32::MAX.to_le_bytes());
1790
1791 assert!(matches!(
1792 import_vmd_motion(&buf),
1793 Err(ImportError::UnexpectedEof(_))
1794 ));
1795 assert!(matches!(
1796 parse_vmd_animation(&buf),
1797 Err(ImportError::UnexpectedEof(_))
1798 ));
1799 }
1800
1801 #[test]
1802 fn rejects_impossible_vmd_property_ik_count_before_allocation() {
1803 let mut buf = build_vmd_header_bytes();
1804 buf.extend_from_slice(&0u32.to_le_bytes()); buf.extend_from_slice(&0u32.to_le_bytes()); buf.extend_from_slice(&0u32.to_le_bytes()); buf.extend_from_slice(&0u32.to_le_bytes()); buf.extend_from_slice(&0u32.to_le_bytes()); buf.extend_from_slice(&1u32.to_le_bytes()); buf.extend_from_slice(&0u32.to_le_bytes()); buf.push(1); buf.extend_from_slice(&u32::MAX.to_le_bytes()); assert!(matches!(
1815 import_vmd_motion(&buf),
1816 Err(ImportError::UnexpectedEof(_))
1817 ));
1818 assert!(matches!(
1819 parse_vmd_animation(&buf),
1820 Err(ImportError::UnexpectedEof(_))
1821 ));
1822 }
1823
1824 #[test]
1825 fn parses_vmd_header() {
1826 let header_bytes = build_vmd_header_bytes();
1827 let (header, pos) = read_header(&header_bytes).unwrap();
1828 assert_eq!(header.model_name_bytes, [0u8; 20]);
1829 assert!(pos > 0);
1830 }
1831
1832 #[test]
1833 fn rejects_bad_vmd_magic() {
1834 let mut buf = build_vmd_header_bytes();
1835 buf[0] = 0xFF;
1836 assert_eq!(read_header(&buf).unwrap_err(), ImportError::InvalidVmdMagic);
1837 }
1838
1839 #[test]
1840 fn accepts_vmd_magic_with_nonzero_padding_bytes() {
1841 let mut buf = build_vmd_header_bytes();
1842 buf[26..30].copy_from_slice(b"JKLM");
1843
1844 let (_header, pos) = read_header(&buf).unwrap();
1845 assert_eq!(pos, 50);
1846 }
1847
1848 #[test]
1849 fn parses_minimal_vmd_motion() {
1850 let mut buf = build_vmd_header_bytes();
1851
1852 buf.extend_from_slice(&0u32.to_le_bytes());
1853 buf.extend_from_slice(&0u32.to_le_bytes());
1854 buf.extend_from_slice(&0u32.to_le_bytes());
1855 buf.extend_from_slice(&0u32.to_le_bytes());
1856 buf.extend_from_slice(&0u32.to_le_bytes());
1857 buf.extend_from_slice(&0u32.to_le_bytes());
1858
1859 let result = import_vmd_motion(&buf).unwrap();
1860 assert!(result.bone_keyframes.is_empty());
1861 assert!(result.morph_keyframes.is_empty());
1862 assert!(result.property_keyframes.is_empty());
1863 }
1864
1865 #[test]
1866 fn accepts_vmd_without_optional_tail_sections() {
1867 let mut buf = build_vmd_header_bytes();
1868 buf.extend_from_slice(&0u32.to_le_bytes());
1869
1870 let result = import_vmd_motion(&buf).unwrap();
1871 assert!(result.bone_keyframes.is_empty());
1872 assert!(result.morph_keyframes.is_empty());
1873 assert!(result.property_keyframes.is_empty());
1874 assert!(result.property_ik_frames.is_empty());
1875 }
1876
1877 #[test]
1878 fn accepts_vmd_ending_after_shadow_section() {
1879 let mut buf = build_vmd_header_bytes();
1880 buf.extend_from_slice(&0u32.to_le_bytes());
1881 buf.extend_from_slice(&0u32.to_le_bytes());
1882 buf.extend_from_slice(&0u32.to_le_bytes());
1883 buf.extend_from_slice(&0u32.to_le_bytes());
1884 buf.extend_from_slice(&0u32.to_le_bytes());
1885
1886 let result = import_vmd_motion(&buf).unwrap();
1887 assert!(result.bone_keyframes.is_empty());
1888 assert!(result.morph_keyframes.is_empty());
1889 assert!(result.property_keyframes.is_empty());
1890 assert!(result.property_ik_frames.is_empty());
1891 }
1892
1893 #[test]
1894 fn rejects_partial_optional_count() {
1895 let mut buf = build_vmd_header_bytes();
1896 buf.extend_from_slice(&0u32.to_le_bytes());
1897 buf.extend_from_slice(&[1, 2, 3]);
1898
1899 assert_eq!(
1900 import_vmd_motion(&buf).unwrap_err(),
1901 ImportError::UnexpectedEof(1)
1902 );
1903 }
1904
1905 #[test]
1906 fn ignores_truncated_unused_tail_sections() {
1907 let mut buf = build_vmd_header_bytes();
1908 buf.extend_from_slice(&0u32.to_le_bytes());
1909 buf.extend_from_slice(&0u32.to_le_bytes());
1910 buf.extend_from_slice(&0u32.to_le_bytes());
1911 buf.extend_from_slice(&0u32.to_le_bytes());
1912 buf.extend_from_slice(&10u32.to_le_bytes());
1913 buf.extend_from_slice(&[0u8; 8]);
1914
1915 let result = import_vmd_motion(&buf).unwrap();
1916 assert!(result.bone_keyframes.is_empty());
1917 assert!(result.morph_keyframes.is_empty());
1918 assert!(result.property_keyframes.is_empty());
1919 assert!(result.property_ik_frames.is_empty());
1920 }
1921
1922 #[test]
1923 fn parsed_animation_ignores_implausible_optional_tail() {
1924 let mut buf = build_vmd_header_bytes();
1925 buf.extend_from_slice(&0u32.to_le_bytes()); buf.extend_from_slice(&0u32.to_le_bytes()); buf.extend_from_slice(&0u32.to_le_bytes()); buf.extend_from_slice(&300u32.to_le_bytes());
1933 buf.extend_from_slice(&[0u8; 57]);
1934
1935 let parsed = parse_vmd_animation(&buf).unwrap();
1936 assert!(parsed.camera_frames.is_empty());
1937 assert!(parsed.light_frames.is_empty());
1938 assert!(parsed.self_shadow_frames.is_empty());
1939 assert!(parsed.property_frames.is_empty());
1940 }
1941
1942 #[test]
1943 fn parses_single_bone_keyframe() {
1944 let mut buf = build_vmd_header_bytes();
1945
1946 buf.extend_from_slice(&1u32.to_le_bytes());
1947 let mut bone_name = [0u8; 15];
1948 bone_name[..4].copy_from_slice(b"Bone");
1949 buf.extend_from_slice(&bone_name);
1950 buf.extend_from_slice(&10u32.to_le_bytes());
1951 buf.extend_from_slice(&1.0f32.to_le_bytes());
1952 buf.extend_from_slice(&2.0f32.to_le_bytes());
1953 buf.extend_from_slice(&3.0f32.to_le_bytes());
1954 buf.extend_from_slice(&0.0f32.to_le_bytes());
1955 buf.extend_from_slice(&0.0f32.to_le_bytes());
1956 buf.extend_from_slice(&0.0f32.to_le_bytes());
1957 buf.extend_from_slice(&1.0f32.to_le_bytes());
1958 buf.extend_from_slice(&[20u8; 64]);
1959
1960 buf.extend_from_slice(&0u32.to_le_bytes());
1961 buf.extend_from_slice(&0u32.to_le_bytes());
1962 buf.extend_from_slice(&0u32.to_le_bytes());
1963 buf.extend_from_slice(&0u32.to_le_bytes());
1964 buf.extend_from_slice(&0u32.to_le_bytes());
1965
1966 let result = import_vmd_motion(&buf).unwrap();
1967 assert_eq!(result.bone_keyframes.len(), 1);
1968 assert_eq!(result.bone_keyframes[0].frame, 10);
1969 assert!((result.bone_keyframes[0].position.x - 1.0).abs() < 0.001);
1970 assert!((result.bone_keyframes[0].position.y - 2.0).abs() < 0.001);
1971 assert!((result.bone_keyframes[0].position.z - 3.0).abs() < 0.001);
1972 assert_eq!(&result.bone_keyframes[0].bone_name_normalized[..], b"Bone");
1973 }
1974
1975 #[test]
1976 fn exports_parsed_vmd_animation_for_roundtrip() {
1977 let mut buf = build_vmd_header_bytes();
1978
1979 buf.extend_from_slice(&1u32.to_le_bytes());
1980 let mut bone_name = [0u8; 15];
1981 bone_name[..4].copy_from_slice(b"Bone");
1982 buf.extend_from_slice(&bone_name);
1983 buf.extend_from_slice(&10u32.to_le_bytes());
1984 buf.extend_from_slice(&1.0f32.to_le_bytes());
1985 buf.extend_from_slice(&2.0f32.to_le_bytes());
1986 buf.extend_from_slice(&3.0f32.to_le_bytes());
1987 buf.extend_from_slice(&0.1f32.to_le_bytes());
1988 buf.extend_from_slice(&0.2f32.to_le_bytes());
1989 buf.extend_from_slice(&0.3f32.to_le_bytes());
1990 buf.extend_from_slice(&0.4f32.to_le_bytes());
1991 buf.extend_from_slice(&[20u8; 64]);
1992
1993 buf.extend_from_slice(&1u32.to_le_bytes());
1994 let mut morph_name = [0u8; 15];
1995 morph_name[..5].copy_from_slice(b"Smile");
1996 buf.extend_from_slice(&morph_name);
1997 buf.extend_from_slice(&11u32.to_le_bytes());
1998 buf.extend_from_slice(&0.75f32.to_le_bytes());
1999
2000 buf.extend_from_slice(&1u32.to_le_bytes());
2001 buf.extend_from_slice(&12u32.to_le_bytes());
2002 buf.extend_from_slice(&30.0f32.to_le_bytes());
2003 buf.extend_from_slice(&1.0f32.to_le_bytes());
2004 buf.extend_from_slice(&2.0f32.to_le_bytes());
2005 buf.extend_from_slice(&3.0f32.to_le_bytes());
2006 buf.extend_from_slice(&0.1f32.to_le_bytes());
2007 buf.extend_from_slice(&0.2f32.to_le_bytes());
2008 buf.extend_from_slice(&0.3f32.to_le_bytes());
2009 buf.extend_from_slice(&[30u8; 24]);
2010 buf.extend_from_slice(&45u32.to_le_bytes());
2011 buf.push(0);
2012
2013 buf.extend_from_slice(&1u32.to_le_bytes());
2014 buf.extend_from_slice(&13u32.to_le_bytes());
2015 buf.extend_from_slice(&1.0f32.to_le_bytes());
2016 buf.extend_from_slice(&0.5f32.to_le_bytes());
2017 buf.extend_from_slice(&0.25f32.to_le_bytes());
2018 buf.extend_from_slice(&(-1.0f32).to_le_bytes());
2019 buf.extend_from_slice(&(-0.5f32).to_le_bytes());
2020 buf.extend_from_slice(&(-0.25f32).to_le_bytes());
2021
2022 buf.extend_from_slice(&1u32.to_le_bytes());
2023 buf.extend_from_slice(&14u32.to_le_bytes());
2024 buf.push(2);
2025 buf.extend_from_slice(&0.6f32.to_le_bytes());
2026
2027 buf.extend_from_slice(&1u32.to_le_bytes());
2028 buf.extend_from_slice(&15u32.to_le_bytes());
2029 buf.push(1);
2030 buf.extend_from_slice(&1u32.to_le_bytes());
2031 let mut ik_name = [0u8; 20];
2032 ik_name[..2].copy_from_slice(b"IK");
2033 buf.extend_from_slice(&ik_name);
2034 buf.push(1);
2035
2036 let parsed = parse_vmd_animation(&buf).unwrap();
2037 let exported = export_vmd_animation(&parsed);
2038 let reparsed = parse_vmd_animation(&exported).unwrap();
2039
2040 assert_vmd_roundtrip_eq(&parsed, &reparsed);
2041 }
2042
2043 fn assert_vmd_roundtrip_eq(left: &VmdParsedAnimation, right: &VmdParsedAnimation) {
2044 assert_eq!(left.metadata.model_name, right.metadata.model_name);
2045 assert_eq!(left.metadata.max_frame, right.metadata.max_frame);
2046 assert_eq!(left.bone_frames.len(), right.bone_frames.len());
2047 assert_eq!(left.morph_frames.len(), right.morph_frames.len());
2048 assert_eq!(left.camera_frames.len(), right.camera_frames.len());
2049 assert_eq!(left.light_frames.len(), right.light_frames.len());
2050 assert_eq!(
2051 left.self_shadow_frames.len(),
2052 right.self_shadow_frames.len()
2053 );
2054 assert_eq!(left.property_frames.len(), right.property_frames.len());
2055 assert_eq!(
2056 left.bone_frames[0].bone_name,
2057 right.bone_frames[0].bone_name
2058 );
2059 assert_eq!(left.bone_frames[0].frame, right.bone_frames[0].frame);
2060 assert_eq!(
2061 left.bone_frames[0].translation,
2062 right.bone_frames[0].translation
2063 );
2064 assert_eq!(left.bone_frames[0].rotation, right.bone_frames[0].rotation);
2065 assert_eq!(
2066 left.bone_frames[0].interpolation,
2067 right.bone_frames[0].interpolation
2068 );
2069 assert_eq!(
2070 left.morph_frames[0].morph_name,
2071 right.morph_frames[0].morph_name
2072 );
2073 assert_eq!(left.morph_frames[0].frame, right.morph_frames[0].frame);
2074 assert_eq!(left.morph_frames[0].weight, right.morph_frames[0].weight);
2075 assert_eq!(left.camera_frames[0].frame, right.camera_frames[0].frame);
2076 assert_eq!(
2077 left.camera_frames[0].position,
2078 right.camera_frames[0].position
2079 );
2080 assert_eq!(
2081 left.camera_frames[0].rotation,
2082 right.camera_frames[0].rotation
2083 );
2084 assert_eq!(
2085 left.camera_frames[0].interpolation,
2086 right.camera_frames[0].interpolation
2087 );
2088 assert_eq!(
2089 left.camera_frames[0].perspective,
2090 right.camera_frames[0].perspective
2091 );
2092 assert_eq!(left.light_frames[0].color, right.light_frames[0].color);
2093 assert_eq!(
2094 left.light_frames[0].direction,
2095 right.light_frames[0].direction
2096 );
2097 assert_eq!(
2098 left.self_shadow_frames[0].mode,
2099 right.self_shadow_frames[0].mode
2100 );
2101 assert_eq!(
2102 left.self_shadow_frames[0].distance,
2103 right.self_shadow_frames[0].distance
2104 );
2105 assert_eq!(
2106 left.property_frames[0].visible,
2107 right.property_frames[0].visible
2108 );
2109 assert_eq!(
2110 left.property_frames[0].ik_states[0].bone_name,
2111 right.property_frames[0].ik_states[0].bone_name
2112 );
2113 assert_eq!(
2114 left.property_frames[0].ik_states[0].enabled,
2115 right.property_frames[0].ik_states[0].enabled
2116 );
2117 }
2118
2119 #[test]
2120 fn decodes_raw_vmd_bone_interpolation_as_strided_curves() {
2121 let mut interpolation = [0u8; 64];
2122 for (index, value) in interpolation.iter_mut().enumerate() {
2123 *value = index as u8;
2124 }
2125
2126 let (position, rotation) = decode_bone_interpolation(&interpolation);
2127
2128 assert_eq!(
2129 position.x,
2130 InterpolationScalar {
2131 x1: 0,
2132 y1: 4,
2133 x2: 8,
2134 y2: 12
2135 }
2136 );
2137 assert_eq!(
2138 position.y,
2139 InterpolationScalar {
2140 x1: 1,
2141 y1: 5,
2142 x2: 9,
2143 y2: 13
2144 }
2145 );
2146 assert_eq!(
2147 position.z,
2148 InterpolationScalar {
2149 x1: 2,
2150 y1: 6,
2151 x2: 10,
2152 y2: 14
2153 }
2154 );
2155 assert_eq!(
2156 rotation,
2157 InterpolationScalar {
2158 x1: 3,
2159 y1: 7,
2160 x2: 11,
2161 y2: 15
2162 }
2163 );
2164 }
2165
2166 #[test]
2167 fn decodes_mmd_registered_bone_interpolation_as_block_curves() {
2168 let mut interpolation = [0u8; 64];
2169 for (index, value) in interpolation.iter_mut().enumerate() {
2170 *value = index as u8;
2171 }
2172
2173 let (position, rotation) = decode_mmd_registered_bone_interpolation(&interpolation);
2174
2175 assert_eq!(
2176 position.x,
2177 InterpolationScalar {
2178 x1: 0,
2179 y1: 4,
2180 x2: 8,
2181 y2: 12
2182 }
2183 );
2184 assert_eq!(
2185 position.y,
2186 InterpolationScalar {
2187 x1: 16,
2188 y1: 20,
2189 x2: 24,
2190 y2: 28
2191 }
2192 );
2193 assert_eq!(
2194 position.z,
2195 InterpolationScalar {
2196 x1: 32,
2197 y1: 36,
2198 x2: 40,
2199 y2: 44
2200 }
2201 );
2202 assert_eq!(
2203 rotation,
2204 InterpolationScalar {
2205 x1: 48,
2206 y1: 52,
2207 x2: 56,
2208 y2: 60
2209 }
2210 );
2211 }
2212
2213 #[test]
2214 fn decodes_raw_vmd_camera_interpolation_as_contiguous_curves() {
2215 let mut interpolation = [0u8; 24];
2216 for (index, value) in interpolation.iter_mut().enumerate() {
2217 *value = index as u8;
2218 }
2219
2220 let decoded = decode_camera_interpolation(&interpolation);
2221
2222 assert_eq!(
2223 decoded.position.x,
2224 InterpolationScalar {
2225 x1: 0,
2226 y1: 1,
2227 x2: 2,
2228 y2: 3
2229 }
2230 );
2231 assert_eq!(
2232 decoded.position.y,
2233 InterpolationScalar {
2234 x1: 4,
2235 y1: 5,
2236 x2: 6,
2237 y2: 7
2238 }
2239 );
2240 assert_eq!(
2241 decoded.position.z,
2242 InterpolationScalar {
2243 x1: 8,
2244 y1: 9,
2245 x2: 10,
2246 y2: 11
2247 }
2248 );
2249 assert_eq!(
2250 decoded.rotation,
2251 InterpolationScalar {
2252 x1: 12,
2253 y1: 13,
2254 x2: 14,
2255 y2: 15
2256 }
2257 );
2258 assert_eq!(
2259 decoded.distance,
2260 InterpolationScalar {
2261 x1: 16,
2262 y1: 17,
2263 x2: 18,
2264 y2: 19
2265 }
2266 );
2267 assert_eq!(
2268 decoded.fov,
2269 InterpolationScalar {
2270 x1: 20,
2271 y1: 21,
2272 x2: 22,
2273 y2: 23
2274 }
2275 );
2276 }
2277
2278 #[test]
2279 fn skips_camera_light_shadow_and_reads_ik_property_names() {
2280 let mut buf = build_vmd_header_bytes();
2281
2282 buf.extend_from_slice(&0u32.to_le_bytes());
2283 buf.extend_from_slice(&0u32.to_le_bytes());
2284 buf.extend_from_slice(&1u32.to_le_bytes());
2285 buf.extend_from_slice(&[0u8; 61]);
2286 buf.extend_from_slice(&1u32.to_le_bytes());
2287 buf.extend_from_slice(&[0u8; 28]);
2288 buf.extend_from_slice(&1u32.to_le_bytes());
2289 buf.extend_from_slice(&0u32.to_le_bytes());
2290 buf.push(1);
2291 buf.extend_from_slice(&0.5f32.to_le_bytes());
2292 buf.extend_from_slice(&1u32.to_le_bytes());
2293 buf.extend_from_slice(&24u32.to_le_bytes());
2294 buf.push(1);
2295 buf.extend_from_slice(&1u32.to_le_bytes());
2296 let mut ik_name = [0u8; 20];
2297 ik_name[..6].copy_from_slice(b"LeftIK");
2298 buf.extend_from_slice(&ik_name);
2299 buf.push(0);
2300
2301 let result = import_vmd_motion(&buf).unwrap();
2302 assert_eq!(result.property_keyframes.len(), 1);
2303 assert_eq!(result.property_keyframes[0].frame, 24);
2304 assert_eq!(&*result.property_keyframes[0].ik_enabled, &[0]);
2305
2306 assert_eq!(result.property_ik_frames.len(), 1);
2307 assert_eq!(result.property_ik_frames[0].frame, 24);
2308 assert_eq!(result.property_ik_frames[0].show, 1);
2309 assert_eq!(result.property_ik_frames[0].entries.len(), 1);
2310 let name_end = result.property_ik_frames[0].entries[0]
2311 .name_bytes
2312 .iter()
2313 .position(|&b| b == 0)
2314 .unwrap_or(20);
2315 assert_eq!(
2316 &result.property_ik_frames[0].entries[0].name_bytes[..name_end],
2317 b"LeftIK"
2318 );
2319 assert_eq!(result.property_ik_frames[0].entries[0].enabled, 0);
2320 assert_eq!(
2321 &result.property_ik_frames[0].entries[0].name_normalized[..],
2322 b"LeftIK"
2323 );
2324 }
2325
2326 #[test]
2327 fn builds_clip_from_bone_track() {
2328 let kfs = vec![
2329 VmdBoneKeyframeRaw {
2330 bone_mode: VmdBoneImportMode::ByName(b"BoneA".to_vec()),
2331 frame: 0,
2332 position: Vec3A::ZERO,
2333 rotation: Quat::IDENTITY,
2334 interpolation: [
2335 20, 20, 107, 107, 20, 20, 107, 107, 20, 20, 107, 107, 20, 20, 107, 107, 20, 20,
2336 107, 107, 20, 20, 107, 107, 20, 20, 107, 107, 20, 20, 107, 107, 20, 20, 107,
2337 107, 20, 20, 107, 107, 20, 20, 107, 107, 20, 20, 107, 107, 20, 20, 107, 107,
2338 20, 20, 107, 107, 20, 20, 107, 107, 20, 20, 107, 107,
2339 ],
2340 bone_name_normalized: b"BoneA".to_vec(),
2341 },
2342 VmdBoneKeyframeRaw {
2343 bone_mode: VmdBoneImportMode::ByName(b"BoneA".to_vec()),
2344 frame: 30,
2345 position: Vec3A::new(10.0, 0.0, 0.0),
2346 rotation: Quat::IDENTITY,
2347 interpolation: [
2348 20, 20, 107, 107, 20, 20, 107, 107, 20, 20, 107, 107, 20, 20, 107, 107, 20, 20,
2349 107, 107, 20, 20, 107, 107, 20, 20, 107, 107, 20, 20, 107, 107, 20, 20, 107,
2350 107, 20, 20, 107, 107, 20, 20, 107, 107, 20, 20, 107, 107, 20, 20, 107, 107,
2351 20, 20, 107, 107, 20, 20, 107, 107, 20, 20, 107, 107,
2352 ],
2353 bone_name_normalized: b"BoneA".to_vec(),
2354 },
2355 ];
2356
2357 fn lookup(name: &[u8]) -> Option<BoneIndex> {
2358 if name == b"BoneA" {
2359 Some(BoneIndex(0))
2360 } else {
2361 None
2362 }
2363 }
2364 fn morph_lookup(_name: &[u8]) -> Option<MorphIndex> {
2365 None
2366 }
2367
2368 let result = VmdImportResult {
2369 bone_keyframes: kfs,
2370 morph_keyframes: Vec::new(),
2371 property_keyframes: Vec::new(),
2372 property_ik_frames: Vec::new(),
2373 };
2374
2375 let _clip = build_clip_from_import(result, &lookup, &morph_lookup);
2376 }
2377
2378 #[test]
2379 fn build_clip_from_import_resolves_morph_by_normalized_name() {
2380 let sjis_morph = vec![0x83, 0x65, 0x83, 0x58, 0x83, 0x67];
2381 let result = VmdImportResult {
2382 bone_keyframes: Vec::new(),
2383 morph_keyframes: vec![(sjis_morph, 0, 1.0)],
2384 property_keyframes: Vec::new(),
2385 property_ik_frames: Vec::new(),
2386 };
2387
2388 fn morph_lookup(name: &[u8]) -> Option<MorphIndex> {
2389 if name == b"\xE3\x83\x86\xE3\x82\xB9\xE3\x83\x88" {
2390 Some(MorphIndex(0))
2391 } else {
2392 None
2393 }
2394 }
2395 fn bone_lookup(_name: &[u8]) -> Option<BoneIndex> {
2396 None
2397 }
2398
2399 let clip = build_clip_from_import(result, &bone_lookup, &morph_lookup);
2400 assert_eq!(clip.morph_track_count(), 1);
2401 }
2402
2403 fn ik_name_bytes(name: &str) -> [u8; 20] {
2404 let mut buf = [0u8; 20];
2405 let name_bytes = name.as_bytes();
2406 let len = name_bytes.len().min(20);
2407 buf[..len].copy_from_slice(&name_bytes[..len]);
2408 buf
2409 }
2410
2411 #[test]
2412 fn parses_property_ik_entry_names() {
2413 let mut buf = build_vmd_header_bytes();
2414
2415 buf.extend_from_slice(&0u32.to_le_bytes());
2416 buf.extend_from_slice(&0u32.to_le_bytes());
2417 buf.extend_from_slice(&0u32.to_le_bytes());
2418 buf.extend_from_slice(&0u32.to_le_bytes());
2419 buf.extend_from_slice(&0u32.to_le_bytes());
2420 buf.extend_from_slice(&1u32.to_le_bytes());
2421 buf.extend_from_slice(&10u32.to_le_bytes());
2422 buf.push(0);
2423 buf.extend_from_slice(&2u32.to_le_bytes());
2424 buf.extend_from_slice(&ik_name_bytes("LeftLegIK"));
2425 buf.push(1);
2426 buf.extend_from_slice(&ik_name_bytes("RightLegIK"));
2427 buf.push(0);
2428
2429 let result = import_vmd_motion(&buf).unwrap();
2430 assert_eq!(result.property_ik_frames.len(), 1);
2431 assert_eq!(result.property_ik_frames[0].frame, 10);
2432 assert_eq!(result.property_ik_frames[0].entries.len(), 2);
2433
2434 let name1: Vec<u8> = result.property_ik_frames[0].entries[0]
2435 .name_bytes
2436 .iter()
2437 .copied()
2438 .take_while(|&b| b != 0)
2439 .collect();
2440 assert_eq!(&name1[..], b"LeftLegIK");
2441 assert_eq!(result.property_ik_frames[0].entries[0].enabled, 1);
2442 assert_eq!(
2443 &result.property_ik_frames[0].entries[0].name_normalized[..],
2444 b"LeftLegIK"
2445 );
2446
2447 let name2: Vec<u8> = result.property_ik_frames[0].entries[1]
2448 .name_bytes
2449 .iter()
2450 .copied()
2451 .take_while(|&b| b != 0)
2452 .collect();
2453 assert_eq!(&name2[..], b"RightLegIK");
2454 assert_eq!(result.property_ik_frames[0].entries[1].enabled, 0);
2455 }
2456
2457 #[test]
2458 fn shared_context_preserves_raw_property_bytes_and_values() {
2459 let mut buf = build_vmd_header_bytes();
2460 buf.extend_from_slice(&0u32.to_le_bytes()); buf.extend_from_slice(&0u32.to_le_bytes()); buf.extend_from_slice(&0u32.to_le_bytes()); buf.extend_from_slice(&0u32.to_le_bytes()); buf.extend_from_slice(&0u32.to_le_bytes()); buf.extend_from_slice(&1u32.to_le_bytes()); buf.extend_from_slice(&42u32.to_le_bytes());
2467 buf.push(0x7f); buf.extend_from_slice(&1u32.to_le_bytes()); let mut name_bytes = [0u8; 20];
2470 name_bytes[..2].copy_from_slice(b"IK");
2471 name_bytes[2] = 0;
2472 name_bytes[3] = 0xa5; name_bytes[19] = 0x5a;
2474 buf.extend_from_slice(&name_bytes);
2475 buf.push(0x7f); let shared = parse_vmd_shared_context(&buf).unwrap();
2478 let raw = shared.import_result();
2479 assert_eq!(raw.property_keyframes[0].frame, 42);
2480 assert_eq!(raw.property_keyframes[0].ik_enabled.as_ref(), &[1]);
2481 assert_eq!(raw.property_ik_frames[0].show, 0x7f);
2482 assert_eq!(
2483 raw.property_ik_frames[0].entries[0].name_bytes,
2484 name_bytes.to_vec()
2485 );
2486 assert_eq!(raw.property_ik_frames[0].entries[0].enabled, 0x7f);
2487 assert_eq!(
2488 raw.property_ik_frames[0].entries[0].name_normalized,
2489 normalize_vmd_name(b"IK")
2490 );
2491
2492 let parsed = shared.parsed_animation();
2493 assert!(parsed.property_frames[0].visible);
2494 assert_eq!(parsed.property_frames[0].ik_states[0].bone_name, "IK");
2495 assert!(parsed.property_frames[0].ik_states[0].enabled);
2496 }
2497
2498 #[test]
2499 fn shared_context_preserves_raw_bone_and_morph_bytes_order_and_interpolation() {
2500 let mut buf = build_vmd_header_bytes();
2501 buf.extend_from_slice(&2u32.to_le_bytes()); let mut first_bone_name = [0u8; 15];
2504 first_bone_name[..3].copy_from_slice(&[0x82, 0xa0, b'1']);
2505 first_bone_name[4] = 0xa5; buf.extend_from_slice(&first_bone_name);
2507 buf.extend_from_slice(&11u32.to_le_bytes());
2508 buf.extend_from_slice(&1.0f32.to_le_bytes());
2509 buf.extend_from_slice(&2.0f32.to_le_bytes());
2510 buf.extend_from_slice(&3.0f32.to_le_bytes());
2511 buf.extend_from_slice(&0.0f32.to_le_bytes());
2512 buf.extend_from_slice(&0.1f32.to_le_bytes());
2513 buf.extend_from_slice(&0.2f32.to_le_bytes());
2514 buf.extend_from_slice(&1.0f32.to_le_bytes());
2515 let first_interpolation = (0u8..64).collect::<Vec<_>>();
2516 buf.extend_from_slice(&first_interpolation);
2517
2518 let mut second_bone_name = [0u8; 15];
2519 second_bone_name[..4].copy_from_slice(b"bone");
2520 buf.extend_from_slice(&second_bone_name);
2521 buf.extend_from_slice(&3u32.to_le_bytes());
2522 buf.extend_from_slice(&4.0f32.to_le_bytes());
2523 buf.extend_from_slice(&5.0f32.to_le_bytes());
2524 buf.extend_from_slice(&6.0f32.to_le_bytes());
2525 buf.extend_from_slice(&0.3f32.to_le_bytes());
2526 buf.extend_from_slice(&0.4f32.to_le_bytes());
2527 buf.extend_from_slice(&0.5f32.to_le_bytes());
2528 buf.extend_from_slice(&0.6f32.to_le_bytes());
2529 let second_interpolation = (64u8..=127).collect::<Vec<_>>();
2530 buf.extend_from_slice(&second_interpolation);
2531
2532 buf.extend_from_slice(&2u32.to_le_bytes()); let mut first_morph_name = [0u8; 15];
2534 first_morph_name[..2].copy_from_slice(&[0x83, 0x65]);
2535 buf.extend_from_slice(&first_morph_name);
2536 buf.extend_from_slice(&20u32.to_le_bytes());
2537 buf.extend_from_slice(&0.25f32.to_le_bytes());
2538
2539 let mut second_morph_name = [0u8; 15];
2540 second_morph_name[..5].copy_from_slice(b"morph");
2541 buf.extend_from_slice(&second_morph_name);
2542 buf.extend_from_slice(&7u32.to_le_bytes());
2543 buf.extend_from_slice(&0.75f32.to_le_bytes());
2544
2545 buf.extend_from_slice(&[0u8; 16]);
2547
2548 let context = parse_vmd_shared_context(&buf).unwrap();
2549 let raw = context.import_result();
2550 assert_eq!(raw.bone_keyframes.len(), 2);
2551 assert_eq!(raw.morph_keyframes.len(), 2);
2552 match &raw.bone_keyframes[0].bone_mode {
2553 VmdBoneImportMode::ByName(name) => {
2554 assert_eq!(name, &vec![0x82, 0xa0, b'1']);
2555 }
2556 VmdBoneImportMode::ByIndex(_) => panic!("raw VMD key must retain its name"),
2557 }
2558 assert_eq!(raw.bone_keyframes[0].frame, 11);
2559 assert_eq!(
2560 raw.bone_keyframes[0].interpolation,
2561 [
2562 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22,
2563 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43,
2564 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63,
2565 ]
2566 );
2567 match &raw.bone_keyframes[1].bone_mode {
2568 VmdBoneImportMode::ByName(name) => assert_eq!(name, &b"bone".to_vec()),
2569 VmdBoneImportMode::ByIndex(_) => panic!("raw VMD key must retain its name"),
2570 }
2571 assert_eq!(raw.bone_keyframes[1].frame, 3);
2572 assert_eq!(
2573 raw.bone_keyframes[1].interpolation,
2574 [
2575 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84,
2576 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103,
2577 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119,
2578 120, 121, 122, 123, 124, 125, 126, 127,
2579 ]
2580 );
2581 assert_eq!(raw.morph_keyframes[0], (vec![0x83, 0x65], 20, 0.25f32));
2582 assert_eq!(raw.morph_keyframes[1], (b"morph".to_vec(), 7, 0.75f32));
2583 }
2584
2585 #[test]
2586 fn shared_context_summary_reports_target_and_channel_counts() {
2587 let animation = VmdParsedAnimation {
2588 kind: "vmd",
2589 metadata: VmdParsedMetadata {
2590 format: "vmd",
2591 model_name: "target".to_owned(),
2592 model_name_bytes: Vec::new(),
2593 counts: VmdParsedCounts {
2594 bones: 3,
2595 morphs: 2,
2596 cameras: 1,
2597 lights: 1,
2598 self_shadows: 1,
2599 properties: 1,
2600 },
2601 max_frame: 0,
2602 },
2603 bone_frames: vec![
2604 VmdParsedBoneFrame {
2605 bone_name: "bone".to_owned(),
2606 bone_name_bytes: Vec::new(),
2607 frame: 5,
2608 translation: [0.0; 3],
2609 rotation: [0.0, 0.0, 0.0, 1.0],
2610 interpolation: vec![0; 64],
2611 },
2612 VmdParsedBoneFrame {
2613 bone_name: "bone".to_owned(),
2614 bone_name_bytes: Vec::new(),
2615 frame: 10,
2616 translation: [0.0; 3],
2617 rotation: [0.0, 0.0, 0.0, 1.0],
2618 interpolation: vec![0; 64],
2619 },
2620 VmdParsedBoneFrame {
2621 bone_name: "other".to_owned(),
2622 bone_name_bytes: Vec::new(),
2623 frame: 15,
2624 translation: [0.0; 3],
2625 rotation: [0.0, 0.0, 0.0, 1.0],
2626 interpolation: vec![0; 64],
2627 },
2628 ],
2629 morph_frames: vec![
2630 VmdParsedMorphFrame {
2631 morph_name: "smile".to_owned(),
2632 morph_name_bytes: Vec::new(),
2633 frame: 20,
2634 weight: 0.5,
2635 },
2636 VmdParsedMorphFrame {
2637 morph_name: "smile".to_owned(),
2638 morph_name_bytes: Vec::new(),
2639 frame: 25,
2640 weight: 1.0,
2641 },
2642 ],
2643 camera_frames: vec![VmdParsedCameraFrame {
2644 frame: 30,
2645 distance: -40.0,
2646 position: [0.0; 3],
2647 rotation: [0.0; 3],
2648 interpolation: [0; 24],
2649 fov: 45,
2650 perspective: true,
2651 }],
2652 light_frames: vec![VmdParsedLightFrame {
2653 frame: 35,
2654 color: [1.0; 3],
2655 direction: [0.0; 3],
2656 }],
2657 self_shadow_frames: vec![VmdParsedSelfShadowFrame {
2658 frame: 40,
2659 mode: 1,
2660 distance: 10.0,
2661 }],
2662 property_frames: vec![VmdParsedPropertyFrame {
2663 frame: 42,
2664 visible: true,
2665 ik_states: vec![
2666 VmdParsedIkState {
2667 bone_name: "ik_a".to_owned(),
2668 bone_name_bytes: Vec::new(),
2669 enabled: true,
2670 },
2671 VmdParsedIkState {
2672 bone_name: "ik_b".to_owned(),
2673 bone_name_bytes: Vec::new(),
2674 enabled: false,
2675 },
2676 ],
2677 }],
2678 };
2679
2680 let bytes = export_vmd_animation(&animation);
2681 let shared = parse_vmd_shared_context(&bytes).unwrap();
2682 let summary_only = parse_vmd_summary(&bytes).unwrap();
2683 assert_eq!(summary_only, *shared.summary());
2684 let summary = shared.summary();
2685 let mut expected_name = [0u8; 20];
2686 expected_name[..6].copy_from_slice(b"target");
2687
2688 assert_eq!(summary.target_model_name_bytes, expected_name);
2689 assert_eq!(summary.max_frame, 42);
2690 assert_eq!(
2691 summary.bones,
2692 VmdSharedContextTrackSummary {
2693 track_count: 2,
2694 key_count: 3,
2695 }
2696 );
2697 assert_eq!(
2698 summary.morphs,
2699 VmdSharedContextTrackSummary {
2700 track_count: 1,
2701 key_count: 2,
2702 }
2703 );
2704 assert_eq!(
2705 summary.cameras,
2706 VmdSharedContextTrackSummary {
2707 track_count: 1,
2708 key_count: 1,
2709 }
2710 );
2711 assert_eq!(summary.lights.key_count, 1);
2712 assert_eq!(summary.self_shadows.key_count, 1);
2713 assert_eq!(summary.properties.key_count, 1);
2714 assert_eq!(summary.property_ik_entry_count, 2);
2715 }
2716
2717 fn summary_validation_vmd_bytes() -> Vec<u8> {
2718 let mut buf = build_vmd_header_bytes();
2719 buf.extend_from_slice(&1u32.to_le_bytes()); let mut bone_name = [0u8; 15];
2721 bone_name[..4].copy_from_slice(b"bone");
2722 buf.extend_from_slice(&bone_name);
2723 buf.extend_from_slice(&1u32.to_le_bytes());
2724 buf.extend_from_slice(&[0u8; 12]); buf.extend_from_slice(&[0u8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]); buf.extend_from_slice(&[0u8; 64]); buf.extend_from_slice(&1u32.to_le_bytes()); let mut morph_name = [0u8; 15];
2730 morph_name[..5].copy_from_slice(b"morph");
2731 buf.extend_from_slice(&morph_name);
2732 buf.extend_from_slice(&2u32.to_le_bytes());
2733 buf.extend_from_slice(&0.0f32.to_le_bytes());
2734
2735 buf.extend_from_slice(&1u32.to_le_bytes()); buf.extend_from_slice(&3u32.to_le_bytes());
2737 buf.extend_from_slice(&0.0f32.to_le_bytes()); buf.extend_from_slice(&[0u8; 12]); buf.extend_from_slice(&[0u8; 12]); buf.extend_from_slice(&[0u8; 24]); buf.extend_from_slice(&45u32.to_le_bytes());
2742 buf.push(0);
2743
2744 buf.extend_from_slice(&1u32.to_le_bytes()); buf.extend_from_slice(&4u32.to_le_bytes());
2746 buf.extend_from_slice(&[0u8; 12]); buf.extend_from_slice(&[0u8; 12]); buf.extend_from_slice(&1u32.to_le_bytes()); buf.extend_from_slice(&5u32.to_le_bytes());
2751 buf.push(1);
2752 buf.extend_from_slice(&0.0f32.to_le_bytes());
2753
2754 buf.extend_from_slice(&1u32.to_le_bytes()); buf.extend_from_slice(&6u32.to_le_bytes());
2756 buf.push(1);
2757 buf.extend_from_slice(&1u32.to_le_bytes()); let mut ik_name = [0u8; 20];
2759 ik_name[..2].copy_from_slice(b"ik");
2760 buf.extend_from_slice(&ik_name);
2761 buf.push(1);
2762 buf
2763 }
2764
2765 #[test]
2766 fn summary_scanner_rejects_empty_names_and_non_finite_values() {
2767 let mut empty_bone = summary_validation_vmd_bytes();
2768 empty_bone[54..69].fill(0);
2769 assert_eq!(
2770 parse_vmd_summary(&empty_bone),
2771 Err(VmdSummaryError::Invalid("empty bone name"))
2772 );
2773
2774 let mut empty_morph = summary_validation_vmd_bytes();
2775 empty_morph[169..184].fill(0);
2776 assert_eq!(
2777 parse_vmd_summary(&empty_morph),
2778 Err(VmdSummaryError::Invalid("empty morph name"))
2779 );
2780
2781 let non_finite_offsets = [
2782 (73, "bone translation"),
2783 (85, "bone rotation"),
2784 (188, "morph weight"),
2785 (200, "camera distance"),
2786 (204, "camera position"),
2787 (216, "camera rotation"),
2788 (265, "light color"),
2789 (277, "light direction"),
2790 (298, "self-shadow distance"),
2791 ];
2792 for (offset, field) in non_finite_offsets {
2793 let mut malformed = summary_validation_vmd_bytes();
2794 malformed[offset..offset + 4].copy_from_slice(&f32::NAN.to_le_bytes());
2795 assert_eq!(
2796 parse_vmd_summary(&malformed),
2797 Err(VmdSummaryError::Invalid(field)),
2798 "summary scanner must reject non-finite {field}"
2799 );
2800 }
2801
2802 let mut empty_property_ik = summary_validation_vmd_bytes();
2803 empty_property_ik[315..335].fill(0);
2804 assert_eq!(
2805 parse_vmd_summary(&empty_property_ik),
2806 Err(VmdSummaryError::Invalid("empty property IK name"))
2807 );
2808
2809 let mut whitespace_property_ik = summary_validation_vmd_bytes();
2810 whitespace_property_ik[315..335].fill(0x20);
2811 assert_eq!(
2812 parse_vmd_summary(&whitespace_property_ik),
2813 Err(VmdSummaryError::Invalid("empty property IK name"))
2814 );
2815 }
2816
2817 #[test]
2818 fn summary_scanner_preserves_optional_tail_absence_semantics() {
2819 let bytes = summary_validation_vmd_bytes();
2820 let context = parse_vmd_shared_context(&bytes).unwrap();
2821
2822 let mut truncated_camera_tail = bytes.clone();
2826 truncated_camera_tail[192..196].copy_from_slice(&10u32.to_le_bytes());
2827 let summary = parse_vmd_summary(&truncated_camera_tail).unwrap();
2828 let parsed_context = parse_vmd_shared_context(&truncated_camera_tail).unwrap();
2829 assert_eq!(summary, *parsed_context.summary());
2830 assert_eq!(summary.bones, context.summary().bones);
2831 assert_eq!(summary.morphs, context.summary().morphs);
2832 let empty_track_summary = VmdSharedContextTrackSummary {
2833 track_count: 0,
2834 key_count: 0,
2835 };
2836 assert_eq!(summary.cameras, empty_track_summary);
2837 assert_eq!(summary.lights, empty_track_summary);
2838 assert_eq!(summary.properties, empty_track_summary);
2839
2840 let mut partial_camera_count = build_vmd_header_bytes();
2841 partial_camera_count.extend_from_slice(&0u32.to_le_bytes()); partial_camera_count.extend_from_slice(&0u32.to_le_bytes()); partial_camera_count.extend_from_slice(&[1, 2, 3]);
2844 assert_eq!(
2845 parse_vmd_summary(&partial_camera_count),
2846 Err(VmdSummaryError::Parse(ImportError::UnexpectedEof(1)))
2847 );
2848 }
2849
2850 #[test]
2851 fn reorders_ik_enabled_to_solver_order() {
2852 let frames = vec![VmdPropertyIkFrame {
2853 frame: 0,
2854 show: 0,
2855 entries: vec![
2856 VmdIkEntry {
2857 name_bytes: {
2858 let mut buf = [0u8; 20];
2859 buf[..9].copy_from_slice(b"RightLegI");
2860 buf.to_vec()
2861 },
2862 enabled: 1,
2863 name_normalized: b"RightLegI".to_vec(),
2864 },
2865 VmdIkEntry {
2866 name_bytes: {
2867 let mut buf = [0u8; 20];
2868 buf[..8].copy_from_slice(b"LeftLegI");
2869 buf.to_vec()
2870 },
2871 enabled: 0,
2872 name_normalized: b"LeftLegI".to_vec(),
2873 },
2874 ],
2875 }];
2876
2877 fn ik_resolver(name: &[u8]) -> Option<usize> {
2878 match name {
2879 b"LeftLegI" => Some(0),
2880 b"RightLegI" => Some(1),
2881 _ => None,
2882 }
2883 }
2884
2885 let binding = build_property_binding_with_ik_resolver(&frames, &ik_resolver, 2).unwrap();
2886 let sample = binding.sample(0.0).unwrap();
2887 assert_eq!(sample, &[0, 1]);
2888 }
2889
2890 #[test]
2891 fn unknown_and_unmentioned_ik_names_keep_default_enabled_state() {
2892 let frames = vec![VmdPropertyIkFrame {
2893 frame: 5,
2894 show: 0,
2895 entries: vec![
2896 VmdIkEntry {
2897 name_bytes: {
2898 let mut buf = [0u8; 20];
2899 buf[..7].copy_from_slice(b"KnownIK");
2900 buf.to_vec()
2901 },
2902 enabled: 1,
2903 name_normalized: b"KnownIK".to_vec(),
2904 },
2905 VmdIkEntry {
2906 name_bytes: {
2907 let mut buf = [0u8; 20];
2908 buf[..9].copy_from_slice(b"UnknownIK");
2909 buf.to_vec()
2910 },
2911 enabled: 1,
2912 name_normalized: b"UnknownIK".to_vec(),
2913 },
2914 ],
2915 }];
2916
2917 fn ik_resolver(name: &[u8]) -> Option<usize> {
2918 if name == b"KnownIK" { Some(0) } else { None }
2919 }
2920
2921 let binding = build_property_binding_with_ik_resolver(&frames, &ik_resolver, 2).unwrap();
2922 let sample = binding.sample(5.0).unwrap();
2923 assert_eq!(sample, &[1, 1]);
2924 }
2925
2926 #[test]
2927 fn empty_ik_frames_returns_none() {
2928 fn ik_resolver(_name: &[u8]) -> Option<usize> {
2929 None
2930 }
2931 let binding = build_property_binding_with_ik_resolver(&[], &ik_resolver, 2);
2932 assert!(binding.is_none());
2933 }
2934
2935 #[test]
2936 fn zero_solver_count_returns_none() {
2937 let frames = vec![VmdPropertyIkFrame {
2938 frame: 0,
2939 show: 0,
2940 entries: vec![VmdIkEntry {
2941 name_bytes: ik_name_bytes("IK").to_vec(),
2942 enabled: 1,
2943 name_normalized: b"IK".to_vec(),
2944 }],
2945 }];
2946 fn ik_resolver(_name: &[u8]) -> Option<usize> {
2947 Some(0)
2948 }
2949 let binding = build_property_binding_with_ik_resolver(&frames, &ik_resolver, 0);
2950 assert!(binding.is_none());
2951 }
2952
2953 #[test]
2954 fn build_property_binding_resolves_ik_by_normalized_name() {
2955 let frames = vec![VmdPropertyIkFrame {
2956 frame: 0,
2957 show: 0,
2958 entries: vec![VmdIkEntry {
2959 name_bytes: vec![0x8D, 0xB6, 0x91, 0xAB],
2960 enabled: 1,
2961 name_normalized: vec![0xE5, 0xB7, 0xA6, 0xE8, 0xB6, 0xB3],
2962 }],
2963 }];
2964
2965 fn ik_resolver(name: &[u8]) -> Option<usize> {
2966 if name == b"\xE5\xB7\xA6\xE8\xB6\xB3" {
2967 Some(0)
2968 } else {
2969 None
2970 }
2971 }
2972
2973 let binding = build_property_binding_with_ik_resolver(&frames, &ik_resolver, 1).unwrap();
2974 let sample = binding.sample(0.0).unwrap();
2975 assert_eq!(sample, &[1]);
2976 }
2977
2978 #[test]
2979 fn build_clip_from_import_resolves_bone_by_normalized_name() {
2980 let sjis_name: Vec<u8> = vec![0x8D, 0xB6, 0x91, 0xAB];
2981 let utf8_name: Vec<u8> = vec![0xE5, 0xB7, 0xA6, 0xE8, 0xB6, 0xB3];
2982
2983 let kf = VmdBoneKeyframeRaw {
2984 bone_mode: VmdBoneImportMode::ByName(sjis_name),
2985 frame: 0,
2986 position: Vec3A::ZERO,
2987 rotation: Quat::IDENTITY,
2988 interpolation: [20u8; 64],
2989 bone_name_normalized: utf8_name.clone(),
2990 };
2991
2992 fn lookup(name: &[u8]) -> Option<BoneIndex> {
2993 if name == b"\xE5\xB7\xA6\xE8\xB6\xB3" {
2994 Some(BoneIndex(0))
2995 } else {
2996 None
2997 }
2998 }
2999 fn morph_lookup(_name: &[u8]) -> Option<MorphIndex> {
3000 None
3001 }
3002
3003 let result = VmdImportResult {
3004 bone_keyframes: vec![kf],
3005 morph_keyframes: Vec::new(),
3006 property_keyframes: Vec::new(),
3007 property_ik_frames: Vec::new(),
3008 };
3009
3010 let clip = build_clip_from_import(result, &lookup, &morph_lookup);
3011 assert_eq!(
3012 clip.bone_track_count(),
3013 1,
3014 "bone should be resolved via normalized UTF-8 name, not raw Shift-JIS bytes"
3015 );
3016 }
3017
3018 #[test]
3019 fn japanese_vmd_name_matches_pmx_utf8_name_via_normalization() {
3020 let sjis_name: &[u8] = &[0x8D, 0xB6, 0x91, 0xAB];
3021 let utf8_name: &[u8] = &[0xE5, 0xB7, 0xA6, 0xE8, 0xB6, 0xB3];
3022
3023 let mut pmx_buf = Vec::new();
3024 pmx_buf.extend_from_slice(b"PMX ");
3025 pmx_buf.extend_from_slice(&2.0f32.to_le_bytes());
3026 pmx_buf.push(8);
3027 pmx_buf.push(1);
3028 pmx_buf.push(0);
3029 pmx_buf.push(4);
3030 pmx_buf.push(1);
3031 pmx_buf.push(1);
3032 pmx_buf.push(2);
3033 pmx_buf.push(1);
3034 pmx_buf.push(1);
3035 for _ in 0..4 {
3036 pmx_buf.extend_from_slice(&0i32.to_le_bytes());
3037 }
3038 pmx_buf.extend_from_slice(&0i32.to_le_bytes());
3039 pmx_buf.extend_from_slice(&0i32.to_le_bytes());
3040 pmx_buf.extend_from_slice(&0i32.to_le_bytes());
3041 pmx_buf.extend_from_slice(&0i32.to_le_bytes());
3042 pmx_buf.extend_from_slice(&1i32.to_le_bytes());
3043 pmx_buf.extend_from_slice(&(utf8_name.len() as i32).to_le_bytes());
3044 pmx_buf.extend_from_slice(utf8_name);
3045 pmx_buf.extend_from_slice(&0i32.to_le_bytes());
3046 pmx_buf.extend_from_slice(&0.0f32.to_le_bytes());
3047 pmx_buf.extend_from_slice(&0.0f32.to_le_bytes());
3048 pmx_buf.extend_from_slice(&0.0f32.to_le_bytes());
3049 pmx_buf.extend_from_slice(&(-1i16).to_le_bytes());
3050 pmx_buf.extend_from_slice(&0i32.to_le_bytes());
3051 pmx_buf.extend_from_slice(&0u16.to_le_bytes());
3052 pmx_buf.extend_from_slice(&0.0f32.to_le_bytes());
3053 pmx_buf.extend_from_slice(&0.0f32.to_le_bytes());
3054 pmx_buf.extend_from_slice(&0.0f32.to_le_bytes());
3055 pmx_buf.extend_from_slice(&0i32.to_le_bytes());
3056
3057 let pmx = crate::pmx::import_pmx_runtime(&pmx_buf).unwrap();
3058
3059 let mut vmd_buf = build_vmd_header_bytes();
3060 vmd_buf.extend_from_slice(&1u32.to_le_bytes());
3061 let mut bone_name = [0u8; 15];
3062 bone_name[..sjis_name.len()].copy_from_slice(sjis_name);
3063 vmd_buf.extend_from_slice(&bone_name);
3064 vmd_buf.extend_from_slice(&0u32.to_le_bytes());
3065 vmd_buf.extend_from_slice(&0.0f32.to_le_bytes());
3066 vmd_buf.extend_from_slice(&0.0f32.to_le_bytes());
3067 vmd_buf.extend_from_slice(&0.0f32.to_le_bytes());
3068 vmd_buf.extend_from_slice(&0.0f32.to_le_bytes());
3069 vmd_buf.extend_from_slice(&0.0f32.to_le_bytes());
3070 vmd_buf.extend_from_slice(&0.0f32.to_le_bytes());
3071 vmd_buf.extend_from_slice(&1.0f32.to_le_bytes());
3072 vmd_buf.extend_from_slice(&[20u8; 64]);
3073 vmd_buf.extend_from_slice(&0u32.to_le_bytes());
3074 vmd_buf.extend_from_slice(&0u32.to_le_bytes());
3075 vmd_buf.extend_from_slice(&0u32.to_le_bytes());
3076 vmd_buf.extend_from_slice(&0u32.to_le_bytes());
3077 vmd_buf.extend_from_slice(&0u32.to_le_bytes());
3078
3079 let vmd = import_vmd_motion(&vmd_buf).unwrap();
3080
3081 let normalized = normalize_vmd_name(sjis_name);
3082 assert_eq!(&normalized[..], utf8_name);
3083
3084 assert!(
3085 pmx.bone_name_to_index.contains_key(&normalized),
3086 "PMX should have normalized bone name key"
3087 );
3088
3089 let clip = build_pair_clip(
3090 &vmd,
3091 &pmx.bone_name_to_index,
3092 &pmx.morph_name_to_index,
3093 &pmx.ik_solver_bone_name_to_index,
3094 pmx.model.ik_count(),
3095 );
3096
3097 assert_eq!(clip.bone_track_count(), 1);
3098 assert_eq!(clip.morph_track_count(), 0);
3099 assert!(!clip.has_property_track());
3100 }
3101
3102 #[test]
3103 fn japanese_vmd_name_matches_pmx_utf16le_name_via_decoded_key() {
3104 let sjis_name: &[u8] = &[0x8D, 0xB6, 0x91, 0xAB];
3105 let utf8_name: &[u8] = &[0xE5, 0xB7, 0xA6, 0xE8, 0xB6, 0xB3];
3106 let utf16le_name: Vec<u8> = "\u{5DE6}\u{8DB3}"
3107 .encode_utf16()
3108 .flat_map(u16::to_le_bytes)
3109 .collect();
3110
3111 let mut pmx_buf = Vec::new();
3112 pmx_buf.extend_from_slice(b"PMX ");
3113 pmx_buf.extend_from_slice(&2.0f32.to_le_bytes());
3114 pmx_buf.push(8);
3115 pmx_buf.push(0);
3116 pmx_buf.push(0);
3117 pmx_buf.push(4);
3118 pmx_buf.push(1);
3119 pmx_buf.push(1);
3120 pmx_buf.push(2);
3121 pmx_buf.push(1);
3122 pmx_buf.push(1);
3123 for _ in 0..4 {
3124 pmx_buf.extend_from_slice(&0i32.to_le_bytes());
3125 }
3126 pmx_buf.extend_from_slice(&0i32.to_le_bytes());
3127 pmx_buf.extend_from_slice(&0i32.to_le_bytes());
3128 pmx_buf.extend_from_slice(&0i32.to_le_bytes());
3129 pmx_buf.extend_from_slice(&0i32.to_le_bytes());
3130 pmx_buf.extend_from_slice(&1i32.to_le_bytes());
3131 pmx_buf.extend_from_slice(&(utf16le_name.len() as i32).to_le_bytes());
3132 pmx_buf.extend_from_slice(&utf16le_name);
3133 pmx_buf.extend_from_slice(&0i32.to_le_bytes());
3134 pmx_buf.extend_from_slice(&0.0f32.to_le_bytes());
3135 pmx_buf.extend_from_slice(&0.0f32.to_le_bytes());
3136 pmx_buf.extend_from_slice(&0.0f32.to_le_bytes());
3137 pmx_buf.extend_from_slice(&(-1i16).to_le_bytes());
3138 pmx_buf.extend_from_slice(&0i32.to_le_bytes());
3139 pmx_buf.extend_from_slice(&0u16.to_le_bytes());
3140 pmx_buf.extend_from_slice(&0.0f32.to_le_bytes());
3141 pmx_buf.extend_from_slice(&0.0f32.to_le_bytes());
3142 pmx_buf.extend_from_slice(&0.0f32.to_le_bytes());
3143 pmx_buf.extend_from_slice(&0i32.to_le_bytes());
3144
3145 let pmx = crate::pmx::import_pmx_runtime(&pmx_buf).unwrap();
3146
3147 let mut vmd_buf = build_vmd_header_bytes();
3148 vmd_buf.extend_from_slice(&1u32.to_le_bytes());
3149 let mut bone_name = [0u8; 15];
3150 bone_name[..sjis_name.len()].copy_from_slice(sjis_name);
3151 vmd_buf.extend_from_slice(&bone_name);
3152 vmd_buf.extend_from_slice(&0u32.to_le_bytes());
3153 vmd_buf.extend_from_slice(&0.0f32.to_le_bytes());
3154 vmd_buf.extend_from_slice(&0.0f32.to_le_bytes());
3155 vmd_buf.extend_from_slice(&0.0f32.to_le_bytes());
3156 vmd_buf.extend_from_slice(&0.0f32.to_le_bytes());
3157 vmd_buf.extend_from_slice(&0.0f32.to_le_bytes());
3158 vmd_buf.extend_from_slice(&0.0f32.to_le_bytes());
3159 vmd_buf.extend_from_slice(&1.0f32.to_le_bytes());
3160 vmd_buf.extend_from_slice(&[20u8; 64]);
3161
3162 let vmd = import_vmd_motion(&vmd_buf).unwrap();
3163 let normalized = normalize_vmd_name(sjis_name);
3164 assert_eq!(&normalized[..], utf8_name);
3165 assert!(
3166 pmx.bone_name_to_index.contains_key(&utf16le_name),
3167 "PMX should retain the raw UTF-16LE name key"
3168 );
3169 assert!(
3170 pmx.bone_name_to_index.contains_key(&normalized),
3171 "PMX should also index UTF-16LE names by decoded UTF-8 bytes"
3172 );
3173
3174 let clip = build_pair_clip(
3175 &vmd,
3176 &pmx.bone_name_to_index,
3177 &pmx.morph_name_to_index,
3178 &pmx.ik_solver_bone_name_to_index,
3179 pmx.model.ik_count(),
3180 );
3181
3182 assert_eq!(clip.bone_track_count(), 1);
3183 }
3184
3185 #[test]
3186 fn build_pair_clip_default_includes_property_ik() {
3187 let result = VmdImportResult {
3189 bone_keyframes: Vec::new(),
3190 morph_keyframes: Vec::new(),
3191 property_keyframes: Vec::new(),
3192 property_ik_frames: vec![VmdPropertyIkFrame {
3193 frame: 0,
3194 show: 0,
3195 entries: vec![VmdIkEntry {
3196 name_bytes: ik_name_bytes("LeftLegIK").to_vec(),
3197 enabled: 0,
3198 name_normalized: b"LeftLegIK".to_vec(),
3199 }],
3200 }],
3201 };
3202
3203 let bone_name_to_index: std::collections::HashMap<Vec<u8>, BoneIndex> =
3204 std::collections::HashMap::new();
3205 let morph_name_to_index: std::collections::HashMap<Vec<u8>, MorphIndex> =
3206 std::collections::HashMap::new();
3207 let mut ik_solver_bone_name_to_index: std::collections::HashMap<Vec<u8>, usize> =
3208 std::collections::HashMap::new();
3209 ik_solver_bone_name_to_index.insert(b"LeftLegIK".to_vec(), 0);
3210
3211 let clip = build_pair_clip(
3212 &result,
3213 &bone_name_to_index,
3214 &morph_name_to_index,
3215 &ik_solver_bone_name_to_index,
3216 2, );
3218
3219 assert!(
3220 clip.has_property_track(),
3221 "default build_pair_clip should include property IK track"
3222 );
3223 }
3224
3225 #[test]
3226 fn build_pair_clip_with_options_omits_property_ik() {
3227 let result = VmdImportResult {
3229 bone_keyframes: Vec::new(),
3230 morph_keyframes: Vec::new(),
3231 property_keyframes: Vec::new(),
3232 property_ik_frames: vec![VmdPropertyIkFrame {
3233 frame: 5,
3234 show: 0,
3235 entries: vec![VmdIkEntry {
3236 name_bytes: ik_name_bytes("RightLegIK").to_vec(),
3237 enabled: 0,
3238 name_normalized: b"RightLegIK".to_vec(),
3239 }],
3240 }],
3241 };
3242
3243 let bone_name_to_index: std::collections::HashMap<Vec<u8>, BoneIndex> =
3244 std::collections::HashMap::new();
3245 let morph_name_to_index: std::collections::HashMap<Vec<u8>, MorphIndex> =
3246 std::collections::HashMap::new();
3247 let mut ik_solver_bone_name_to_index: std::collections::HashMap<Vec<u8>, usize> =
3248 std::collections::HashMap::new();
3249 ik_solver_bone_name_to_index.insert(b"RightLegIK".to_vec(), 1);
3250
3251 let clip = build_pair_clip_with_options(
3252 &result,
3253 &bone_name_to_index,
3254 &morph_name_to_index,
3255 &ik_solver_bone_name_to_index,
3256 2,
3257 VmdClipBuildOptions {
3258 honor_property_ik: false,
3259 },
3260 );
3261
3262 assert!(
3263 !clip.has_property_track(),
3264 "build_pair_clip_with_options(honor_property_ik: false) should omit property IK track"
3265 );
3266 }
3267
3268 fn fixed_axis_test_keyframe(rotation: Quat) -> VmdBoneKeyframeRaw {
3269 VmdBoneKeyframeRaw {
3270 bone_mode: VmdBoneImportMode::ByIndex(7),
3271 frame: 12,
3272 position: Vec3A::new(1.0, 2.0, 3.0),
3273 rotation,
3274 interpolation: [20u8; 64],
3275 bone_name_normalized: b"FixedAxis".to_vec(),
3276 }
3277 }
3278
3279 fn assert_quat_near(actual: Quat, expected: Quat) {
3280 for (actual, expected) in [actual.x, actual.y, actual.z, actual.w]
3281 .into_iter()
3282 .zip([expected.x, expected.y, expected.z, expected.w])
3283 {
3284 assert!((actual - expected).abs() < 1.0e-6);
3285 }
3286 }
3287
3288 fn test_model(fixed_axis: Option<Vec3A>) -> ModelArena {
3289 let mut bones: Vec<_> = (0..8).map(|_| BoneInit::new(None, Vec3A::ZERO)).collect();
3290 if let Some(axis) = fixed_axis {
3291 bones[7] = BoneInit::new(None, Vec3A::ZERO).with_fixed_axis(axis);
3292 }
3293 ModelArena::new(bones).unwrap()
3294 }
3295
3296 #[test]
3297 fn model_fixed_axis_projects_off_axis_positive_dot_and_preserves_keyframe_data() {
3298 let source = fixed_axis_test_keyframe(Quat::from_xyzw(0.3, 0.4, 0.0, 0.8));
3299 let model = test_model(Some(Vec3A::new(0.0, 3.0, 0.0)));
3300 let mapped = build_mapped_bone_keyframe(
3301 &source,
3302 BoneIndex(7),
3303 VmdRegistrationPolicy::MmdRegistered(&model),
3304 );
3305
3306 assert_quat_near(mapped.rotation, Quat::from_xyzw(0.0, 0.5, 0.0, 0.8));
3307 assert_eq!(mapped.frame, source.frame);
3308 assert_eq!(mapped.position, source.position);
3309 let (position_interpolation, rotation_interpolation) =
3310 decode_mmd_registered_bone_interpolation(&source.interpolation);
3311 assert_eq!(mapped.position_interpolation, position_interpolation);
3312 assert_eq!(mapped.rotation_interpolation, rotation_interpolation);
3313 }
3314
3315 #[test]
3316 fn model_fixed_axis_projects_off_axis_negative_dot_with_negative_sign() {
3317 let source = fixed_axis_test_keyframe(Quat::from_xyzw(0.3, -0.4, 0.0, 0.8));
3318 let model = test_model(Some(Vec3A::new(0.0, 3.0, 0.0)));
3319 let mapped = build_mapped_bone_keyframe(
3320 &source,
3321 BoneIndex(7),
3322 VmdRegistrationPolicy::MmdRegistered(&model),
3323 );
3324
3325 assert_quat_near(mapped.rotation, Quat::from_xyzw(0.0, -0.5, 0.0, 0.8));
3326 }
3327
3328 #[test]
3329 fn model_fixed_axis_leaves_already_axis_aligned_rotation_unchanged() {
3330 let source = fixed_axis_test_keyframe(Quat::from_xyzw(0.0, -0.5, 0.0, 0.8));
3331 let model = test_model(Some(Vec3A::new(0.0, 3.0, 0.0)));
3332 let mapped = build_mapped_bone_keyframe(
3333 &source,
3334 BoneIndex(7),
3335 VmdRegistrationPolicy::MmdRegistered(&model),
3336 );
3337
3338 assert_quat_near(mapped.rotation, source.rotation);
3339 }
3340
3341 #[test]
3342 fn model_fixed_axis_leaves_non_fixed_bone_rotation_unchanged() {
3343 let source = fixed_axis_test_keyframe(Quat::from_xyzw(0.3, 0.4, 0.1, 0.8));
3344 let model = test_model(None);
3345 let mapped = build_mapped_bone_keyframe(
3346 &source,
3347 BoneIndex(7),
3348 VmdRegistrationPolicy::MmdRegistered(&model),
3349 );
3350
3351 assert_eq!(mapped.rotation, source.rotation);
3352 }
3353
3354 #[test]
3355 fn model_aware_bone_keyframe_uses_registered_interpolation_for_all_mapped_bones() {
3356 let mut source = fixed_axis_test_keyframe(Quat::IDENTITY);
3357 for (index, value) in source.interpolation.iter_mut().enumerate() {
3358 *value = index as u8;
3359 }
3360 let model = test_model(None);
3361 let raw = build_mapped_bone_keyframe(&source, BoneIndex(7), VmdRegistrationPolicy::Raw);
3362 let model_aware = build_mapped_bone_keyframe(
3363 &source,
3364 BoneIndex(7),
3365 VmdRegistrationPolicy::MmdRegistered(&model),
3366 );
3367 let (raw_position, raw_rotation) = decode_bone_interpolation(&source.interpolation);
3368 let (registered_position, registered_rotation) =
3369 decode_mmd_registered_bone_interpolation(&source.interpolation);
3370
3371 assert_eq!(raw.position_interpolation, raw_position);
3372 assert_eq!(raw.rotation_interpolation, raw_rotation);
3373 assert_eq!(model_aware.position_interpolation, registered_position);
3374 assert_eq!(model_aware.rotation_interpolation, registered_rotation);
3375 assert_ne!(
3376 raw.position_interpolation,
3377 model_aware.position_interpolation
3378 );
3379 assert_ne!(
3380 raw.rotation_interpolation,
3381 model_aware.rotation_interpolation
3382 );
3383 }
3384
3385 #[test]
3386 fn public_mmd_registered_builder_projects_fixed_axis_rotation() {
3387 let source = fixed_axis_test_keyframe(Quat::from_xyzw(0.3, 0.4, 0.0, 0.8));
3388 let model = test_model(Some(Vec3A::Y));
3389 let result = VmdImportResult {
3390 bone_keyframes: vec![source],
3391 morph_keyframes: Vec::new(),
3392 property_keyframes: Vec::new(),
3393 property_ik_frames: Vec::new(),
3394 };
3395 let mut bone_name_to_index = std::collections::HashMap::new();
3396 bone_name_to_index.insert(b"FixedAxis".to_vec(), BoneIndex(7));
3397 let morph_name_to_index = std::collections::HashMap::new();
3398 let ik_solver_bone_name_to_index = std::collections::HashMap::new();
3399
3400 let clip = build_mmd_registered_pair_clip_with_options(
3401 &model,
3402 &result,
3403 &bone_name_to_index,
3404 &morph_name_to_index,
3405 &ik_solver_bone_name_to_index,
3406 0,
3407 VmdClipBuildOptions::default(),
3408 )
3409 .unwrap();
3410 let sample = clip.sample_at(12.0);
3411 let rotation = sample.bone_samples()[0].rotation;
3412 assert_quat_near(rotation, Quat::from_xyzw(0.0, 0.5, 0.0, 0.8).normalize());
3413 }
3414
3415 #[test]
3416 fn public_mmd_registered_builder_uses_registered_interpolation_layout() {
3417 let mut first = fixed_axis_test_keyframe(Quat::IDENTITY);
3418 first.frame = 0;
3419 first.position = Vec3A::ZERO;
3420 let mut second = fixed_axis_test_keyframe(Quat::IDENTITY);
3421 second.frame = 10;
3422 second.position = Vec3A::Y;
3423 for (index, value) in second.interpolation.iter_mut().enumerate() {
3424 *value = index as u8;
3425 }
3426 let result = VmdImportResult {
3427 bone_keyframes: vec![first, second],
3428 morph_keyframes: Vec::new(),
3429 property_keyframes: Vec::new(),
3430 property_ik_frames: Vec::new(),
3431 };
3432 let mut bone_name_to_index = std::collections::HashMap::new();
3433 bone_name_to_index.insert(b"FixedAxis".to_vec(), BoneIndex(7));
3434 let morph_name_to_index = std::collections::HashMap::new();
3435 let ik_solver_bone_name_to_index = std::collections::HashMap::new();
3436 let model = test_model(None);
3437
3438 let registered = build_mmd_registered_pair_clip(
3439 &model,
3440 &result,
3441 &bone_name_to_index,
3442 &morph_name_to_index,
3443 &ik_solver_bone_name_to_index,
3444 0,
3445 )
3446 .unwrap();
3447 let raw = build_pair_clip(
3448 &result,
3449 &bone_name_to_index,
3450 &morph_name_to_index,
3451 &ik_solver_bone_name_to_index,
3452 0,
3453 );
3454 let registered_position = registered.sample_at(5.0).bone_samples()[0].position;
3455 let raw_position = raw.sample_at(5.0).bone_samples()[0].position;
3456 assert_ne!(registered_position, raw_position);
3457 }
3458
3459 #[test]
3460 fn public_mmd_registered_builder_rejects_out_of_range_bone_map() {
3461 let model = test_model(None);
3462 let result = VmdImportResult {
3463 bone_keyframes: Vec::new(),
3464 morph_keyframes: Vec::new(),
3465 property_keyframes: Vec::new(),
3466 property_ik_frames: Vec::new(),
3467 };
3468 let mut bone_name_to_index = std::collections::HashMap::new();
3469 bone_name_to_index.insert(b"Invalid".to_vec(), BoneIndex(8));
3470 let morph_name_to_index = std::collections::HashMap::new();
3471 let ik_solver_bone_name_to_index = std::collections::HashMap::new();
3472
3473 let error = build_mmd_registered_pair_clip(
3474 &model,
3475 &result,
3476 &bone_name_to_index,
3477 &morph_name_to_index,
3478 &ik_solver_bone_name_to_index,
3479 0,
3480 )
3481 .unwrap_err();
3482 assert_eq!(
3483 error,
3484 VmdClipBuildError::BoneIndexOutOfRange {
3485 bone_index: BoneIndex(8),
3486 bone_count: 8,
3487 }
3488 );
3489 }
3490
3491 type SyntheticBoneFrame<'a> = (&'a str, u32, [f32; 3], [f32; 4], [u8; 64]);
3497 type SyntheticMorphFrame<'a> = (&'a str, u32, f32);
3498 type SyntheticPropertyFrame<'a> = (&'a [(&'a str, bool)], u32, bool);
3499
3500 fn assert_near(actual: f32, expected: f32) {
3501 let delta = (actual - expected).abs();
3502 assert!(
3503 delta < 1.0e-4,
3504 "actual={actual:?} expected={expected:?} delta={delta:?}"
3505 );
3506 }
3507
3508 fn assert_vec3_near(actual: [f32; 3], expected: [f32; 3]) {
3509 for (actual, expected) in actual.into_iter().zip(expected) {
3510 assert_near(actual, expected);
3511 }
3512 }
3513
3514 fn simple_camera_vmd_fixture() -> &'static [u8] {
3515 include_bytes!("../../fixtures/vmd/simple_camera.vmd")
3516 }
3517
3518 fn make_vmd_bytes(
3519 model_name_ascii: &str,
3520 bone_frames: &[SyntheticBoneFrame<'_>],
3521 morph_frames: &[SyntheticMorphFrame<'_>],
3522 property_frames: &[SyntheticPropertyFrame<'_>],
3523 ) -> Vec<u8> {
3524 let mut v = Vec::new();
3525 v.extend_from_slice(&VMD_MAGIC);
3526
3527 let mut name_buf = [0u8; 20];
3528 let nb = model_name_ascii.as_bytes();
3529 name_buf[..nb.len().min(20)].copy_from_slice(&nb[..nb.len().min(20)]);
3530 v.extend_from_slice(&name_buf);
3531
3532 v.extend_from_slice(&(bone_frames.len() as u32).to_le_bytes());
3533 for (name, frame, trans, rot, interp) in bone_frames {
3534 let mut nb = [0u8; 15];
3535 let b = name.as_bytes();
3536 nb[..b.len().min(15)].copy_from_slice(&b[..b.len().min(15)]);
3537 v.extend_from_slice(&nb);
3538 v.extend_from_slice(&frame.to_le_bytes());
3539 for &x in trans {
3540 v.extend_from_slice(&x.to_le_bytes());
3541 }
3542 for &x in rot {
3543 v.extend_from_slice(&x.to_le_bytes());
3544 }
3545 v.extend_from_slice(interp);
3546 }
3547
3548 v.extend_from_slice(&(morph_frames.len() as u32).to_le_bytes());
3549 for (name, frame, weight) in morph_frames {
3550 let mut nb = [0u8; 15];
3551 let b = name.as_bytes();
3552 nb[..b.len().min(15)].copy_from_slice(&b[..b.len().min(15)]);
3553 v.extend_from_slice(&nb);
3554 v.extend_from_slice(&frame.to_le_bytes());
3555 v.extend_from_slice(&weight.to_le_bytes());
3556 }
3557
3558 v.extend_from_slice(&0u32.to_le_bytes());
3560 v.extend_from_slice(&0u32.to_le_bytes());
3561 v.extend_from_slice(&0u32.to_le_bytes());
3562
3563 v.extend_from_slice(&(property_frames.len() as u32).to_le_bytes());
3564 for (ik_states, frame, visible) in property_frames {
3565 v.extend_from_slice(&frame.to_le_bytes());
3566 v.push(u8::from(*visible));
3567 v.extend_from_slice(&(ik_states.len() as u32).to_le_bytes());
3568 for (ik_name, enabled) in *ik_states {
3569 let mut nb = [0u8; 20];
3570 let b = ik_name.as_bytes();
3571 nb[..b.len().min(20)].copy_from_slice(&b[..b.len().min(20)]);
3572 v.extend_from_slice(&nb);
3573 v.push(u8::from(*enabled));
3574 }
3575 }
3576 v
3577 }
3578
3579 fn json_keys(value: &serde_json::Value) -> Vec<String> {
3580 let mut keys = value
3581 .as_object()
3582 .unwrap()
3583 .keys()
3584 .map(ToOwned::to_owned)
3585 .collect::<Vec<_>>();
3586 keys.sort();
3587 keys
3588 }
3589
3590 #[test]
3591 fn vmd_animation_json_top_level_schema_is_stable() {
3592 let vmd = make_vmd_bytes("miku", &[], &[], &[]);
3593 let parsed = parse_vmd_animation(&vmd).unwrap();
3594 let keys = json_keys(&serde_json::to_value(&parsed).unwrap());
3595
3596 assert_eq!(
3597 keys,
3598 vec![
3599 "boneFrames",
3600 "cameraFrames",
3601 "kind",
3602 "lightFrames",
3603 "metadata",
3604 "morphFrames",
3605 "propertyFrames",
3606 "selfShadowFrames",
3607 ]
3608 );
3609 }
3610
3611 #[test]
3612 fn parses_simple_camera_vmd_fixture() {
3613 let parsed = parse_vmd_animation(simple_camera_vmd_fixture()).unwrap();
3614
3615 assert_eq!(parsed.metadata.model_name, "camera_fixture");
3616 assert_eq!(parsed.metadata.counts.bones, 0);
3617 assert_eq!(parsed.metadata.counts.morphs, 0);
3618 assert_eq!(parsed.metadata.counts.cameras, 2);
3619 assert_eq!(parsed.metadata.counts.lights, 0);
3620 assert_eq!(parsed.metadata.counts.self_shadows, 0);
3621 assert_eq!(parsed.metadata.counts.properties, 0);
3622 assert_eq!(parsed.metadata.max_frame, 45);
3623 assert_eq!(parsed.camera_frames.len(), 2);
3624
3625 let first = &parsed.camera_frames[0];
3626 assert_eq!(first.frame, 0);
3627 assert_eq!(first.distance, -30.5);
3628 assert_eq!(first.position, [1.0, 2.0, 3.0]);
3629 assert_eq!(first.rotation, [0.1, -0.2, 0.3]);
3630 assert_eq!(
3631 first.interpolation,
3632 [
3633 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40,
3634 41, 42, 43
3635 ]
3636 );
3637 assert_eq!(first.fov, 35);
3638 assert!(first.perspective);
3639
3640 let second = &parsed.camera_frames[1];
3641 assert_eq!(second.frame, 45);
3642 assert_eq!(second.distance, -50.0);
3643 assert_eq!(second.position, [-1.5, 10.0, 0.25]);
3644 assert_eq!(second.rotation, [-0.3, 0.0, 1.2]);
3645 assert_eq!(
3646 second.interpolation,
3647 [
3648 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 127, 127, 127, 127, 127, 127, 127, 127, 127,
3649 127, 127, 127
3650 ]
3651 );
3652 assert_eq!(second.fov, 60);
3653 assert!(!second.perspective);
3654 }
3655
3656 #[test]
3657 fn roundtrip_simple_camera_vmd_fixture_parse_export_parse() {
3658 let parsed = parse_vmd_animation(simple_camera_vmd_fixture()).unwrap();
3659 let exported = export_vmd_animation(&parsed);
3660 let reparsed = parse_vmd_animation(&exported).unwrap();
3661
3662 assert_eq!(reparsed.metadata.model_name, parsed.metadata.model_name);
3663 assert_eq!(reparsed.metadata.counts.cameras, 2);
3664 assert_eq!(reparsed.metadata.max_frame, 45);
3665 assert_eq!(reparsed.camera_frames.len(), parsed.camera_frames.len());
3666 for (left, right) in parsed.camera_frames.iter().zip(&reparsed.camera_frames) {
3667 assert_eq!(left.frame, right.frame);
3668 assert_eq!(left.distance, right.distance);
3669 assert_eq!(left.position, right.position);
3670 assert_eq!(left.rotation, right.rotation);
3671 assert_eq!(left.interpolation, right.interpolation);
3672 assert_eq!(left.fov, right.fov);
3673 assert_eq!(left.perspective, right.perspective);
3674 }
3675 }
3676
3677 #[test]
3678 fn samples_simple_camera_vmd_fixture_with_channel_interpolation() {
3679 let parsed = parse_vmd_animation(simple_camera_vmd_fixture()).unwrap();
3680 let camera = sample_vmd_camera_frames(&parsed.camera_frames, 22.5).unwrap();
3681
3682 assert_near(camera.distance, -40.25);
3683 assert_vec3_near(camera.position, [-0.25, 6.0, 1.625]);
3684 assert_vec3_near(camera.rotation, [-0.1, -0.1, 0.75]);
3685 assert_near(camera.fov, 47.5);
3686 assert!(camera.perspective);
3687
3688 let last = sample_vmd_camera_frames(&parsed.camera_frames, 45.0).unwrap();
3689 assert!(!last.perspective);
3690 }
3691
3692 #[test]
3693 fn samples_vmd_light_frames_linearly() {
3694 let frames = vec![
3695 VmdParsedLightFrame {
3696 frame: 30,
3697 color: [1.0, 0.5, 0.0],
3698 direction: [0.0, -1.0, 0.0],
3699 },
3700 VmdParsedLightFrame {
3701 frame: 10,
3702 color: [0.0, 0.0, 1.0],
3703 direction: [1.0, 0.0, 0.0],
3704 },
3705 ];
3706
3707 let light = sample_vmd_light_frames(&frames, 20.0).unwrap();
3708
3709 assert_vec3_near(light.color, [0.5, 0.25, 0.5]);
3710 assert_vec3_near(light.direction, [0.5, -0.5, 0.0]);
3711 }
3712
3713 #[test]
3714 fn samples_vmd_self_shadow_frames_linearly_with_stepped_mode() {
3715 let frames = vec![
3716 VmdParsedSelfShadowFrame {
3717 frame: 10,
3718 mode: 1,
3719 distance: 20.0,
3720 },
3721 VmdParsedSelfShadowFrame {
3722 frame: 30,
3723 mode: 2,
3724 distance: 60.0,
3725 },
3726 ];
3727
3728 let middle = sample_vmd_self_shadow_frames(&frames, 20.0).unwrap();
3729 assert_eq!(middle.mode, 1);
3730 assert_near(middle.distance, 40.0);
3731
3732 let last = sample_vmd_self_shadow_frames(&frames, 30.0).unwrap();
3733 assert_eq!(last.mode, 2);
3734 assert_near(last.distance, 60.0);
3735 }
3736
3737 #[test]
3738 fn roundtrip_bone_frame_parse_export_parse() {
3739 let interp = [20u8; 64];
3740 let vmd = make_vmd_bytes(
3741 "miku",
3742 &[("arm", 30, [1.0, 0.5, -0.5], [0.0, 0.0, 0.0, 1.0], interp)],
3743 &[],
3744 &[],
3745 );
3746 let parsed = parse_vmd_animation(&vmd).unwrap();
3747 assert_eq!(parsed.bone_frames.len(), 1);
3748 assert_eq!(parsed.bone_frames[0].bone_name, "arm");
3749 assert_eq!(parsed.bone_frames[0].frame, 30);
3750 assert_eq!(parsed.bone_frames[0].translation, [1.0, 0.5, -0.5]);
3751 assert_eq!(parsed.bone_frames[0].rotation, [0.0, 0.0, 0.0, 1.0]);
3752 assert_eq!(parsed.bone_frames[0].interpolation, vec![20u8; 64]);
3753
3754 let exported = export_vmd_animation(&parsed);
3755 let reparsed = parse_vmd_animation(&exported).unwrap();
3756 assert_eq!(reparsed.metadata.model_name, parsed.metadata.model_name);
3757 assert_eq!(reparsed.metadata.max_frame, 30);
3758 assert_eq!(reparsed.bone_frames.len(), 1);
3759 assert_eq!(reparsed.bone_frames[0].bone_name, "arm");
3760 assert_eq!(reparsed.bone_frames[0].frame, 30);
3761 assert_eq!(reparsed.bone_frames[0].translation, [1.0, 0.5, -0.5]);
3762 assert_eq!(reparsed.bone_frames[0].rotation, [0.0, 0.0, 0.0, 1.0]);
3763 assert_eq!(reparsed.bone_frames[0].interpolation, vec![20u8; 64]);
3764 }
3765
3766 #[test]
3767 fn roundtrip_morph_frame_parse_export_parse() {
3768 let vmd = make_vmd_bytes("miku", &[], &[("blink", 15, 0.75)], &[]);
3769 let parsed = parse_vmd_animation(&vmd).unwrap();
3770 let exported = export_vmd_animation(&parsed);
3771 let reparsed = parse_vmd_animation(&exported).unwrap();
3772 assert_eq!(reparsed.morph_frames.len(), 1);
3773 assert_eq!(reparsed.morph_frames[0].morph_name, "blink");
3774 assert_eq!(reparsed.morph_frames[0].frame, 15);
3775 assert!((reparsed.morph_frames[0].weight - 0.75f32).abs() < 1e-6);
3776 }
3777
3778 #[test]
3779 fn roundtrip_property_ik_frames_parse_export_parse() {
3780 let vmd = make_vmd_bytes(
3781 "camera",
3782 &[],
3783 &[],
3784 &[(&[("leftIK", true), ("rightIK", false)], 20, true)],
3785 );
3786 let parsed = parse_vmd_animation(&vmd).unwrap();
3787 let exported = export_vmd_animation(&parsed);
3788 let reparsed = parse_vmd_animation(&exported).unwrap();
3789 assert_eq!(reparsed.property_frames.len(), 1);
3790 assert_eq!(reparsed.property_frames[0].frame, 20);
3791 assert!(reparsed.property_frames[0].visible);
3792 assert_eq!(reparsed.property_frames[0].ik_states.len(), 2);
3793 assert_eq!(reparsed.property_frames[0].ik_states[0].bone_name, "leftIK");
3794 assert!(reparsed.property_frames[0].ik_states[0].enabled);
3795 assert_eq!(
3796 reparsed.property_frames[0].ik_states[1].bone_name,
3797 "rightIK"
3798 );
3799 assert!(!reparsed.property_frames[0].ik_states[1].enabled);
3800 }
3801
3802 #[test]
3803 fn roundtrip_json_dto_bone_and_morph() {
3804 let interp = [20u8; 64];
3805 let vmd = make_vmd_bytes(
3806 "miku",
3807 &[("spine", 60, [0.0, 1.0, 0.0], [0.1, 0.2, 0.3, 0.9], interp)],
3808 &[("mouth", 30, 0.8)],
3809 &[],
3810 );
3811 let parsed = parse_vmd_animation(&vmd).unwrap();
3812 let json = serde_json::to_string(&parsed).unwrap();
3813 let from_json: VmdParsedAnimation = serde_json::from_str(&json).unwrap();
3814 let exported = export_vmd_animation(&from_json);
3815 let reparsed = parse_vmd_animation(&exported).unwrap();
3816
3817 assert_eq!(
3818 reparsed.bone_frames[0].bone_name,
3819 parsed.bone_frames[0].bone_name
3820 );
3821 assert_eq!(reparsed.bone_frames[0].frame, parsed.bone_frames[0].frame);
3822 assert_eq!(
3823 reparsed.bone_frames[0].translation,
3824 parsed.bone_frames[0].translation
3825 );
3826 assert_eq!(
3827 reparsed.bone_frames[0].rotation,
3828 parsed.bone_frames[0].rotation
3829 );
3830 assert_eq!(
3831 reparsed.morph_frames[0].morph_name,
3832 parsed.morph_frames[0].morph_name
3833 );
3834 assert_eq!(reparsed.morph_frames[0].frame, parsed.morph_frames[0].frame);
3835 assert!((reparsed.morph_frames[0].weight - parsed.morph_frames[0].weight).abs() < 1e-6);
3836 }
3837
3838 fn expected_sjis_name_bytes(value: &str, len: usize) -> Vec<u8> {
3839 let (encoded, _, _) = encoding_rs::SHIFT_JIS.encode(value);
3840 encoded.as_ref()[..encoded.len().min(len)].to_vec()
3841 }
3842
3843 #[test]
3844 fn export_json_dto_encodes_shift_jis_when_raw_name_bytes_are_missing() {
3845 let animation = vmd_parsed_animation(
3846 "初音ミク".to_owned(),
3847 Vec::new(),
3848 30,
3849 VmdParsedSections {
3850 bone_frames: vec![VmdParsedBoneFrame {
3851 bone_name: "左足".to_owned(),
3852 bone_name_bytes: Vec::new(),
3853 frame: 10,
3854 translation: [1.0, 2.0, 3.0],
3855 rotation: [0.0, 0.0, 0.0, 1.0],
3856 interpolation: vec![20; 64],
3857 }],
3858 morph_frames: vec![VmdParsedMorphFrame {
3859 morph_name: "笑い".to_owned(),
3860 morph_name_bytes: Vec::new(),
3861 frame: 20,
3862 weight: 0.75,
3863 }],
3864 camera_frames: Vec::new(),
3865 light_frames: Vec::new(),
3866 self_shadow_frames: Vec::new(),
3867 property_frames: vec![VmdParsedPropertyFrame {
3868 frame: 30,
3869 visible: true,
3870 ik_states: vec![VmdParsedIkState {
3871 bone_name: "右足IK".to_owned(),
3872 bone_name_bytes: Vec::new(),
3873 enabled: false,
3874 }],
3875 }],
3876 },
3877 );
3878 let json = serde_json::to_string(&animation).unwrap();
3879 assert!(!json.contains("modelNameBytes"));
3880 assert!(!json.contains("boneNameBytes"));
3881 assert!(!json.contains("morphNameBytes"));
3882
3883 let from_json: VmdParsedAnimation = serde_json::from_str(&json).unwrap();
3884 let exported = export_vmd_animation(&from_json);
3885 let reparsed = parse_vmd_animation(&exported).unwrap();
3886
3887 assert_eq!(reparsed.metadata.model_name, "初音ミク");
3888 assert!(!reparsed.metadata.model_name_bytes.is_empty());
3889 assert_eq!(
3890 reparsed.metadata.model_name_bytes,
3891 expected_sjis_name_bytes("初音ミク", 20)
3892 );
3893 assert_eq!(reparsed.bone_frames[0].bone_name, "左足");
3894 assert!(!reparsed.bone_frames[0].bone_name_bytes.is_empty());
3895 assert_eq!(
3896 reparsed.bone_frames[0].bone_name_bytes,
3897 expected_sjis_name_bytes("左足", 15)
3898 );
3899 assert_eq!(reparsed.morph_frames[0].morph_name, "笑い");
3900 assert!(!reparsed.morph_frames[0].morph_name_bytes.is_empty());
3901 assert_eq!(
3902 reparsed.morph_frames[0].morph_name_bytes,
3903 expected_sjis_name_bytes("笑い", 15)
3904 );
3905 assert_eq!(reparsed.property_frames[0].ik_states[0].bone_name, "右足IK");
3906 assert!(
3907 !reparsed.property_frames[0].ik_states[0]
3908 .bone_name_bytes
3909 .is_empty()
3910 );
3911 assert_eq!(
3912 reparsed.property_frames[0].ik_states[0].bone_name_bytes,
3913 expected_sjis_name_bytes("右足IK", 20)
3914 );
3915 assert!(!reparsed.property_frames[0].ik_states[0].enabled);
3916 }
3917
3918 #[test]
3919 fn export_json_dto_uses_encoding_rs_replacement_for_non_shift_jis_names() {
3920 let animation = vmd_parsed_animation(
3921 "miku".to_owned(),
3922 Vec::new(),
3923 1,
3924 VmdParsedSections {
3925 bone_frames: vec![VmdParsedBoneFrame {
3926 bone_name: "左足🧪".to_owned(),
3927 bone_name_bytes: Vec::new(),
3928 frame: 1,
3929 translation: [0.0, 0.0, 0.0],
3930 rotation: [0.0, 0.0, 0.0, 1.0],
3931 interpolation: vec![20; 64],
3932 }],
3933 morph_frames: Vec::new(),
3934 camera_frames: Vec::new(),
3935 light_frames: Vec::new(),
3936 self_shadow_frames: Vec::new(),
3937 property_frames: Vec::new(),
3938 },
3939 );
3940 let json = serde_json::to_string(&animation).unwrap();
3941 let from_json: VmdParsedAnimation = serde_json::from_str(&json).unwrap();
3942 let exported = export_vmd_animation(&from_json);
3943 let reparsed = parse_vmd_animation(&exported).unwrap();
3944
3945 assert_eq!(
3946 reparsed.bone_frames[0].bone_name_bytes,
3947 expected_sjis_name_bytes("左足🧪", 15)
3948 );
3949 assert_eq!(
3950 reparsed.bone_frames[0].bone_name,
3951 encoding_rs::SHIFT_JIS
3952 .decode(&expected_sjis_name_bytes("左足🧪", 15))
3953 .0
3954 .trim()
3955 );
3956 }
3957}