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