1use glam::{Quat, Vec3A};
2use serde::{Deserialize, Serialize};
3
4use mmd_anim_runtime::{
5 AnimationClip, BoneAnimationBinding, BoneIndex, InterpolationScalar, InterpolationVector3,
6 MorphAnimationBinding, MorphIndex, MorphKeyframe, MorphTrack, MovableBoneKeyframe,
7 MovableBoneTrack, PropertyAnimationBinding, PropertyKeyframe,
8};
9
10use crate::error::ImportError;
11use crate::normalize::normalize_vmd_name;
12
13const VMD_MAGIC: [u8; 30] = *b"Vocaloid Motion Data 0002\0\0\0\0\0";
14const VMD_MAGIC_PREFIX: &[u8] = b"Vocaloid Motion Data 0002\0";
15
16struct Reader<'a> {
17 data: &'a [u8],
18 pos: usize,
19}
20
21impl<'a> Reader<'a> {
22 fn new(data: &'a [u8]) -> Self {
23 Self { data, pos: 0 }
24 }
25
26 fn remaining(&self) -> usize {
27 self.data.len().saturating_sub(self.pos)
28 }
29
30 fn require(&self, n: usize) -> Result<(), ImportError> {
31 if self.remaining() >= n {
32 Ok(())
33 } else {
34 Err(ImportError::UnexpectedEof(
35 n.saturating_sub(self.remaining()),
36 ))
37 }
38 }
39
40 fn read_slice(&mut self, n: usize) -> Result<&'a [u8], ImportError> {
41 self.require(n)?;
42 let slice = &self.data[self.pos..self.pos + n];
43 self.pos += n;
44 Ok(slice)
45 }
46
47 fn read_u8(&mut self) -> Result<u8, ImportError> {
48 Ok(self.read_slice(1)?[0])
49 }
50
51 fn read_u32_le(&mut self) -> Result<u32, ImportError> {
52 let b = self.read_slice(4)?;
53 Ok(u32::from_le_bytes([b[0], b[1], b[2], b[3]]))
54 }
55
56 fn read_optional_u32_le(&mut self) -> Result<Option<u32>, ImportError> {
57 if self.remaining() == 0 {
58 Ok(None)
59 } else {
60 self.read_u32_le().map(Some)
61 }
62 }
63
64 fn skip_optional_ignored_records(&mut self, record_size: usize) -> Result<bool, ImportError> {
65 let Some(count) = self.read_optional_u32_le()? else {
66 return Ok(false);
67 };
68 let Some(bytes) = (count as usize).checked_mul(record_size) else {
69 self.pos = self.data.len();
70 return Ok(false);
71 };
72 if bytes > self.remaining() {
73 self.pos = self.data.len();
74 return Ok(false);
75 }
76 self.read_slice(bytes)?;
77 Ok(true)
78 }
79
80 fn read_optional_record_count(
81 &mut self,
82 record_size: usize,
83 ) -> Result<Option<u32>, ImportError> {
84 let Some(count) = self.read_optional_u32_le()? else {
85 return Ok(None);
86 };
87 let Some(bytes) = (count as usize).checked_mul(record_size) else {
88 self.pos = self.data.len();
89 return Ok(None);
90 };
91 if bytes > self.remaining() {
92 self.pos = self.data.len();
93 return Ok(None);
94 }
95 Ok(Some(count))
96 }
97
98 fn read_f32_le(&mut self) -> Result<f32, ImportError> {
99 let b = self.read_slice(4)?;
100 Ok(f32::from_le_bytes([b[0], b[1], b[2], b[3]]))
101 }
102
103 fn read_vec3(&mut self) -> Result<Vec3A, ImportError> {
104 Ok(Vec3A::new(
105 self.read_f32_le()?,
106 self.read_f32_le()?,
107 self.read_f32_le()?,
108 ))
109 }
110
111 fn read_quat(&mut self) -> Result<Quat, ImportError> {
112 Ok(Quat::from_xyzw(
113 self.read_f32_le()?,
114 self.read_f32_le()?,
115 self.read_f32_le()?,
116 self.read_f32_le()?,
117 ))
118 }
119
120 fn read_shifts_jis_name(&mut self) -> Result<Vec<u8>, ImportError> {
121 let raw = self.read_slice(15)?;
122 let end = raw.iter().position(|&b| b == 0).unwrap_or(raw.len());
123 Ok(raw[..end].to_vec())
124 }
125}
126
127#[derive(Debug, Clone)]
128pub struct VmdHeader {
129 pub model_name_bytes: [u8; 20],
130}
131
132#[derive(Debug, Clone)]
133pub enum VmdBoneImportMode {
134 ByName(Vec<u8>),
135 ByIndex(u32),
136}
137
138#[derive(Debug, Clone)]
139pub struct VmdBoneKeyframeRaw {
140 pub bone_mode: VmdBoneImportMode,
141 pub frame: u32,
142 pub position: Vec3A,
143 pub rotation: Quat,
144 pub interpolation: [u8; 64],
145 pub bone_name_normalized: Vec<u8>,
146}
147
148#[derive(Debug, Clone)]
149pub struct VmdIkEntry {
150 pub name_bytes: Vec<u8>,
151 pub enabled: u8,
152 pub name_normalized: Vec<u8>,
153}
154
155#[derive(Debug, Clone)]
156pub struct VmdPropertyIkFrame {
157 pub frame: u32,
158 pub show: u8,
159 pub entries: Vec<VmdIkEntry>,
160}
161
162#[derive(Debug, Clone)]
163pub struct VmdImportResult {
164 pub bone_keyframes: Vec<VmdBoneKeyframeRaw>,
165 pub morph_keyframes: Vec<(Vec<u8>, u32, f32)>,
166 pub property_keyframes: Vec<PropertyKeyframe>,
167 pub property_ik_frames: Vec<VmdPropertyIkFrame>,
168}
169
170pub fn read_header(data: &[u8]) -> Result<(VmdHeader, usize), ImportError> {
171 let mut r = Reader::new(data);
172
173 let magic = r.read_slice(30)?;
174 if magic != VMD_MAGIC && !magic.starts_with(VMD_MAGIC_PREFIX) {
175 return Err(ImportError::InvalidVmdMagic);
176 }
177
178 let model_name_bytes: [u8; 20] = r
179 .read_slice(20)?
180 .try_into()
181 .map_err(|_| ImportError::InvalidVmdModelName)?;
182
183 Ok((VmdHeader { model_name_bytes }, r.pos))
184}
185
186pub fn import_vmd_motion(data: &[u8]) -> Result<VmdImportResult, ImportError> {
187 let (_header, pos) = read_header(data)?;
188 let mut r = Reader { data, pos };
189
190 let bone_count = r.read_u32_le()? as usize;
191 let mut bone_keyframes = Vec::with_capacity(bone_count);
192 for _ in 0..bone_count {
193 let bone_name = r.read_shifts_jis_name()?;
194 let frame = r.read_u32_le()?;
195 let position = r.read_vec3()?;
196 let rotation = r.read_quat()?;
197 let interpolation: [u8; 64] = r.read_slice(64)?.try_into().unwrap();
198
199 let bone_name_normalized = normalize_vmd_name(&bone_name);
200
201 bone_keyframes.push(VmdBoneKeyframeRaw {
202 bone_mode: VmdBoneImportMode::ByName(bone_name),
203 frame,
204 position,
205 rotation,
206 interpolation,
207 bone_name_normalized,
208 });
209 }
210
211 let Some(morph_count) = r.read_optional_u32_le()? else {
212 return Ok(VmdImportResult {
213 bone_keyframes,
214 morph_keyframes: Vec::new(),
215 property_keyframes: Vec::new(),
216 property_ik_frames: Vec::new(),
217 });
218 };
219 let morph_count = morph_count as usize;
220 let mut morph_keyframes = Vec::with_capacity(morph_count);
221 for _ in 0..morph_count {
222 let morph_name = r.read_shifts_jis_name()?;
223 let frame = r.read_u32_le()?;
224 let weight = r.read_f32_le()?;
225 morph_keyframes.push((morph_name, frame, weight));
226 }
227
228 if !r.skip_optional_ignored_records(61)? {
229 return Ok(VmdImportResult {
230 bone_keyframes,
231 morph_keyframes,
232 property_keyframes: Vec::new(),
233 property_ik_frames: Vec::new(),
234 });
235 }
236
237 if !r.skip_optional_ignored_records(28)? {
238 return Ok(VmdImportResult {
239 bone_keyframes,
240 morph_keyframes,
241 property_keyframes: Vec::new(),
242 property_ik_frames: Vec::new(),
243 });
244 }
245
246 if !r.skip_optional_ignored_records(9)? {
247 return Ok(VmdImportResult {
248 bone_keyframes,
249 morph_keyframes,
250 property_keyframes: Vec::new(),
251 property_ik_frames: Vec::new(),
252 });
253 }
254
255 let Some(show_ik_count) = r.read_optional_u32_le()? else {
256 return Ok(VmdImportResult {
257 bone_keyframes,
258 morph_keyframes,
259 property_keyframes: Vec::new(),
260 property_ik_frames: Vec::new(),
261 });
262 };
263 let show_ik_count = show_ik_count as usize;
264 let mut property_keyframes = Vec::with_capacity(show_ik_count);
265 let mut property_ik_frames = Vec::with_capacity(show_ik_count);
266 for _ in 0..show_ik_count {
267 let frame = r.read_u32_le()?;
268 let show = r.read_u8()?;
269 let ik_count = r.read_u32_le()? as usize;
270 let mut ik_enabled = Vec::with_capacity(ik_count);
271 let mut ik_entries = Vec::with_capacity(ik_count);
272 for _ in 0..ik_count {
273 let ik_name_bytes = r.read_slice(20)?.to_vec();
274 let enabled = r.read_u8()?;
275 let end = ik_name_bytes
276 .iter()
277 .position(|&b| b == 0)
278 .unwrap_or(ik_name_bytes.len());
279 let name_normalized = normalize_vmd_name(&ik_name_bytes[..end]);
280 ik_enabled.push(enabled);
281 ik_entries.push(VmdIkEntry {
282 name_bytes: ik_name_bytes,
283 enabled,
284 name_normalized,
285 });
286 }
287 property_keyframes.push(PropertyKeyframe::new(
288 frame,
289 ik_enabled.iter().map(|&b| b != 0).collect(),
290 ));
291 property_ik_frames.push(VmdPropertyIkFrame {
292 frame,
293 show,
294 entries: ik_entries,
295 });
296 }
297
298 Ok(VmdImportResult {
299 bone_keyframes,
300 morph_keyframes,
301 property_keyframes,
302 property_ik_frames,
303 })
304}
305
306#[derive(Debug, Clone, Serialize, Deserialize)]
307#[serde(rename_all = "camelCase")]
308pub struct VmdParsedAnimation {
309 #[serde(default = "default_vmd_kind", skip_deserializing)]
310 pub kind: &'static str,
311 pub metadata: VmdParsedMetadata,
312 pub bone_frames: Vec<VmdParsedBoneFrame>,
313 pub morph_frames: Vec<VmdParsedMorphFrame>,
314 pub camera_frames: Vec<VmdParsedCameraFrame>,
315 pub light_frames: Vec<VmdParsedLightFrame>,
316 pub self_shadow_frames: Vec<VmdParsedSelfShadowFrame>,
317 pub property_frames: Vec<VmdParsedPropertyFrame>,
318}
319
320#[derive(Debug, Clone, Serialize, Deserialize)]
321#[serde(rename_all = "camelCase")]
322pub struct VmdParsedMetadata {
323 #[serde(default = "default_vmd_format", skip_deserializing)]
324 pub format: &'static str,
325 pub model_name: String,
326 #[serde(default, skip_serializing_if = "Vec::is_empty")]
327 pub model_name_bytes: Vec<u8>,
328 pub counts: VmdParsedCounts,
329 pub max_frame: u32,
330}
331
332#[derive(Debug, Clone, Serialize, Deserialize)]
333pub struct VmdParsedCounts {
334 pub bones: usize,
335 pub morphs: usize,
336 pub cameras: usize,
337 pub lights: usize,
338 #[serde(rename = "selfShadows")]
339 pub self_shadows: usize,
340 pub properties: usize,
341}
342
343#[derive(Debug, Clone, Serialize, Deserialize)]
344#[serde(rename_all = "camelCase")]
345pub struct VmdParsedBoneFrame {
346 pub bone_name: String,
347 #[serde(default, skip_serializing_if = "Vec::is_empty")]
348 pub bone_name_bytes: Vec<u8>,
349 pub frame: u32,
350 pub translation: [f32; 3],
351 pub rotation: [f32; 4],
352 pub interpolation: Vec<u8>,
353}
354
355#[derive(Debug, Clone, Serialize, Deserialize)]
356#[serde(rename_all = "camelCase")]
357pub struct VmdParsedMorphFrame {
358 pub morph_name: String,
359 #[serde(default, skip_serializing_if = "Vec::is_empty")]
360 pub morph_name_bytes: Vec<u8>,
361 pub frame: u32,
362 pub weight: f32,
363}
364
365#[derive(Debug, Clone, Serialize, Deserialize)]
366#[serde(rename_all = "camelCase")]
367pub struct VmdParsedCameraFrame {
368 pub frame: u32,
369 pub distance: f32,
370 pub position: [f32; 3],
371 pub rotation: [f32; 3],
372 pub interpolation: [u8; 24],
373 pub fov: u32,
374 pub perspective: bool,
375}
376
377#[derive(Debug, Clone, Serialize, Deserialize)]
378#[serde(rename_all = "camelCase")]
379pub struct VmdParsedLightFrame {
380 pub frame: u32,
381 pub color: [f32; 3],
382 pub direction: [f32; 3],
383}
384
385#[derive(Debug, Clone, Serialize, Deserialize)]
386#[serde(rename_all = "camelCase")]
387pub struct VmdParsedSelfShadowFrame {
388 pub frame: u32,
389 pub mode: u8,
390 pub distance: f32,
391}
392
393#[derive(Debug, Clone, Serialize, Deserialize)]
394#[serde(rename_all = "camelCase")]
395pub struct VmdParsedPropertyFrame {
396 pub frame: u32,
397 pub visible: bool,
398 pub ik_states: Vec<VmdParsedIkState>,
399}
400
401#[derive(Debug, Clone, Serialize, Deserialize)]
402#[serde(rename_all = "camelCase")]
403pub struct VmdParsedIkState {
404 pub bone_name: String,
405 #[serde(default, skip_serializing_if = "Vec::is_empty")]
406 pub bone_name_bytes: Vec<u8>,
407 pub enabled: bool,
408}
409
410pub fn parse_vmd_animation(data: &[u8]) -> Result<VmdParsedAnimation, ImportError> {
411 let (_header, pos) = read_header(data)?;
412 let mut r = Reader { data, pos };
413 let model_name = decode_sjis_fixed(&_header.model_name_bytes);
414 let model_name_bytes = trim_fixed_bytes(&_header.model_name_bytes).to_vec();
415 let mut max_frame = 0u32;
416
417 let bone_count = r.read_u32_le()? as usize;
418 let mut bone_frames = Vec::with_capacity(bone_count);
419 for _ in 0..bone_count {
420 let bone_name_bytes = r.read_slice(15)?;
421 let bone_name = decode_sjis_fixed(bone_name_bytes);
422 let frame = r.read_u32_le()?;
423 max_frame = max_frame.max(frame);
424 let position = r.read_vec3()?;
425 let rotation = r.read_quat()?;
426 let interpolation: [u8; 64] = r.read_slice(64)?.try_into().unwrap();
427 bone_frames.push(VmdParsedBoneFrame {
428 bone_name,
429 bone_name_bytes: trim_fixed_bytes(bone_name_bytes).to_vec(),
430 frame,
431 translation: [position.x, position.y, position.z],
432 rotation: [rotation.x, rotation.y, rotation.z, rotation.w],
433 interpolation: interpolation.to_vec(),
434 });
435 }
436
437 let Some(morph_count) = r.read_optional_u32_le()? else {
438 return Ok(vmd_parsed_animation(
439 model_name,
440 model_name_bytes,
441 max_frame,
442 VmdParsedSections {
443 bone_frames,
444 morph_frames: Vec::new(),
445 camera_frames: Vec::new(),
446 light_frames: Vec::new(),
447 self_shadow_frames: Vec::new(),
448 property_frames: Vec::new(),
449 },
450 ));
451 };
452 let mut morph_frames = Vec::with_capacity(morph_count as usize);
453 for _ in 0..morph_count {
454 let morph_name_bytes = r.read_slice(15)?;
455 let morph_name = decode_sjis_fixed(morph_name_bytes);
456 let frame = r.read_u32_le()?;
457 max_frame = max_frame.max(frame);
458 morph_frames.push(VmdParsedMorphFrame {
459 morph_name,
460 morph_name_bytes: trim_fixed_bytes(morph_name_bytes).to_vec(),
461 frame,
462 weight: r.read_f32_le()?,
463 });
464 }
465
466 let camera_frames = read_parsed_camera_frames(&mut r, &mut max_frame)?;
467 let light_frames = read_parsed_light_frames(&mut r, &mut max_frame)?;
468 let self_shadow_frames = read_parsed_self_shadow_frames(&mut r, &mut max_frame)?;
469 let property_frames = read_parsed_property_frames(&mut r, &mut max_frame)?;
470
471 Ok(vmd_parsed_animation(
472 model_name,
473 model_name_bytes,
474 max_frame,
475 VmdParsedSections {
476 bone_frames,
477 morph_frames,
478 camera_frames,
479 light_frames,
480 self_shadow_frames,
481 property_frames,
482 },
483 ))
484}
485
486fn default_vmd_kind() -> &'static str {
487 "vmd"
488}
489
490fn default_vmd_format() -> &'static str {
491 "vmd"
492}
493
494pub fn export_vmd_animation(animation: &VmdParsedAnimation) -> Vec<u8> {
495 let mut out = Vec::new();
496 out.extend_from_slice(&VMD_MAGIC);
497 write_fixed_name_bytes(
498 &mut out,
499 &animation.metadata.model_name,
500 &animation.metadata.model_name_bytes,
501 20,
502 );
503
504 write_u32(&mut out, animation.bone_frames.len() as u32);
505 for frame in &animation.bone_frames {
506 write_fixed_name_bytes(&mut out, &frame.bone_name, &frame.bone_name_bytes, 15);
507 write_u32(&mut out, frame.frame);
508 write_f32(&mut out, frame.translation[0]);
509 write_f32(&mut out, frame.translation[1]);
510 write_f32(&mut out, frame.translation[2]);
511 write_f32(&mut out, frame.rotation[0]);
512 write_f32(&mut out, frame.rotation[1]);
513 write_f32(&mut out, frame.rotation[2]);
514 write_f32(&mut out, frame.rotation[3]);
515 write_fixed_bytes(&mut out, &frame.interpolation, 64);
516 }
517
518 write_u32(&mut out, animation.morph_frames.len() as u32);
519 for frame in &animation.morph_frames {
520 write_fixed_name_bytes(&mut out, &frame.morph_name, &frame.morph_name_bytes, 15);
521 write_u32(&mut out, frame.frame);
522 write_f32(&mut out, frame.weight);
523 }
524
525 write_u32(&mut out, animation.camera_frames.len() as u32);
526 for frame in &animation.camera_frames {
527 write_u32(&mut out, frame.frame);
528 write_f32(&mut out, frame.distance);
529 write_f32(&mut out, frame.position[0]);
530 write_f32(&mut out, frame.position[1]);
531 write_f32(&mut out, frame.position[2]);
532 write_f32(&mut out, frame.rotation[0]);
533 write_f32(&mut out, frame.rotation[1]);
534 write_f32(&mut out, frame.rotation[2]);
535 out.extend_from_slice(&frame.interpolation);
536 write_u32(&mut out, frame.fov);
537 out.push(if frame.perspective { 0 } else { 1 });
538 }
539
540 write_u32(&mut out, animation.light_frames.len() as u32);
541 for frame in &animation.light_frames {
542 write_u32(&mut out, frame.frame);
543 write_f32(&mut out, frame.color[0]);
544 write_f32(&mut out, frame.color[1]);
545 write_f32(&mut out, frame.color[2]);
546 write_f32(&mut out, frame.direction[0]);
547 write_f32(&mut out, frame.direction[1]);
548 write_f32(&mut out, frame.direction[2]);
549 }
550
551 write_u32(&mut out, animation.self_shadow_frames.len() as u32);
552 for frame in &animation.self_shadow_frames {
553 write_u32(&mut out, frame.frame);
554 out.push(frame.mode);
555 write_f32(&mut out, frame.distance);
556 }
557
558 write_u32(&mut out, animation.property_frames.len() as u32);
559 for frame in &animation.property_frames {
560 write_u32(&mut out, frame.frame);
561 out.push(u8::from(frame.visible));
562 write_u32(&mut out, frame.ik_states.len() as u32);
563 for state in &frame.ik_states {
564 write_fixed_name_bytes(&mut out, &state.bone_name, &state.bone_name_bytes, 20);
565 out.push(u8::from(state.enabled));
566 }
567 }
568
569 out
570}
571
572struct VmdParsedSections {
573 bone_frames: Vec<VmdParsedBoneFrame>,
574 morph_frames: Vec<VmdParsedMorphFrame>,
575 camera_frames: Vec<VmdParsedCameraFrame>,
576 light_frames: Vec<VmdParsedLightFrame>,
577 self_shadow_frames: Vec<VmdParsedSelfShadowFrame>,
578 property_frames: Vec<VmdParsedPropertyFrame>,
579}
580
581fn vmd_parsed_animation(
582 model_name: String,
583 model_name_bytes: Vec<u8>,
584 max_frame: u32,
585 sections: VmdParsedSections,
586) -> VmdParsedAnimation {
587 VmdParsedAnimation {
588 kind: "vmd",
589 metadata: VmdParsedMetadata {
590 format: "vmd",
591 model_name,
592 model_name_bytes,
593 counts: VmdParsedCounts {
594 bones: sections.bone_frames.len(),
595 morphs: sections.morph_frames.len(),
596 cameras: sections.camera_frames.len(),
597 lights: sections.light_frames.len(),
598 self_shadows: sections.self_shadow_frames.len(),
599 properties: sections.property_frames.len(),
600 },
601 max_frame,
602 },
603 bone_frames: sections.bone_frames,
604 morph_frames: sections.morph_frames,
605 camera_frames: sections.camera_frames,
606 light_frames: sections.light_frames,
607 self_shadow_frames: sections.self_shadow_frames,
608 property_frames: sections.property_frames,
609 }
610}
611
612fn read_parsed_camera_frames(
613 r: &mut Reader<'_>,
614 max_frame: &mut u32,
615) -> Result<Vec<VmdParsedCameraFrame>, ImportError> {
616 let Some(count) = r.read_optional_record_count(61)? else {
617 return Ok(Vec::new());
618 };
619 let mut frames = Vec::with_capacity(count as usize);
620 for _ in 0..count {
621 let frame = r.read_u32_le()?;
622 *max_frame = (*max_frame).max(frame);
623 frames.push(VmdParsedCameraFrame {
624 frame,
625 distance: r.read_f32_le()?,
626 position: {
627 let p = r.read_vec3()?;
628 [p.x, p.y, p.z]
629 },
630 rotation: {
631 let p = r.read_vec3()?;
632 [p.x, p.y, p.z]
633 },
634 interpolation: r.read_slice(24)?.try_into().unwrap(),
635 fov: r.read_u32_le()?,
636 perspective: r.read_u8()? == 0,
637 });
638 }
639 Ok(frames)
640}
641
642fn read_parsed_light_frames(
643 r: &mut Reader<'_>,
644 max_frame: &mut u32,
645) -> Result<Vec<VmdParsedLightFrame>, ImportError> {
646 let Some(count) = r.read_optional_record_count(28)? else {
647 return Ok(Vec::new());
648 };
649 let mut frames = Vec::with_capacity(count as usize);
650 for _ in 0..count {
651 let frame = r.read_u32_le()?;
652 *max_frame = (*max_frame).max(frame);
653 let color = r.read_vec3()?;
654 let direction = r.read_vec3()?;
655 frames.push(VmdParsedLightFrame {
656 frame,
657 color: [color.x, color.y, color.z],
658 direction: [direction.x, direction.y, direction.z],
659 });
660 }
661 Ok(frames)
662}
663
664fn read_parsed_self_shadow_frames(
665 r: &mut Reader<'_>,
666 max_frame: &mut u32,
667) -> Result<Vec<VmdParsedSelfShadowFrame>, ImportError> {
668 let Some(count) = r.read_optional_record_count(9)? else {
669 return Ok(Vec::new());
670 };
671 let mut frames = Vec::with_capacity(count as usize);
672 for _ in 0..count {
673 let frame = r.read_u32_le()?;
674 *max_frame = (*max_frame).max(frame);
675 frames.push(VmdParsedSelfShadowFrame {
676 frame,
677 mode: r.read_u8()?,
678 distance: r.read_f32_le()?,
679 });
680 }
681 Ok(frames)
682}
683
684fn read_parsed_property_frames(
685 r: &mut Reader<'_>,
686 max_frame: &mut u32,
687) -> Result<Vec<VmdParsedPropertyFrame>, ImportError> {
688 let Some(count) = r.read_optional_u32_le()? else {
689 return Ok(Vec::new());
690 };
691 let mut frames = Vec::with_capacity(count as usize);
692 for _ in 0..count {
693 let frame = r.read_u32_le()?;
694 *max_frame = (*max_frame).max(frame);
695 let visible = r.read_u8()? != 0;
696 let ik_count = r.read_u32_le()? as usize;
697 let mut ik_states = Vec::with_capacity(ik_count);
698 for _ in 0..ik_count {
699 let bone_name_bytes = r.read_slice(20)?;
700 ik_states.push(VmdParsedIkState {
701 bone_name: decode_sjis_fixed(bone_name_bytes),
702 bone_name_bytes: trim_fixed_bytes(bone_name_bytes).to_vec(),
703 enabled: r.read_u8()? != 0,
704 });
705 }
706 frames.push(VmdParsedPropertyFrame {
707 frame,
708 visible,
709 ik_states,
710 });
711 }
712 Ok(frames)
713}
714
715fn decode_sjis_fixed(bytes: &[u8]) -> String {
716 let end = bytes.iter().position(|&b| b == 0).unwrap_or(bytes.len());
717 let (decoded, _, _) = encoding_rs::SHIFT_JIS.decode(&bytes[..end]);
718 decoded.trim().to_owned()
719}
720
721fn trim_fixed_bytes(bytes: &[u8]) -> &[u8] {
722 let end = bytes.iter().position(|&b| b == 0).unwrap_or(bytes.len());
723 &bytes[..end]
724}
725
726fn write_u32(out: &mut Vec<u8>, value: u32) {
727 out.extend_from_slice(&value.to_le_bytes());
728}
729
730fn write_f32(out: &mut Vec<u8>, value: f32) {
731 out.extend_from_slice(&value.to_le_bytes());
732}
733
734fn write_fixed_name_bytes(out: &mut Vec<u8>, value: &str, raw_bytes: &[u8], len: usize) {
735 if raw_bytes.is_empty() {
736 let (encoded, _, _) = encoding_rs::SHIFT_JIS.encode(value);
737 write_fixed_bytes(out, encoded.as_ref(), len);
738 } else {
739 write_fixed_bytes(out, raw_bytes, len);
740 }
741}
742
743fn write_fixed_bytes(out: &mut Vec<u8>, value: &[u8], len: usize) {
744 let copied = value.len().min(len);
745 out.extend_from_slice(&value[..copied]);
746 out.resize(out.len() + len - copied, 0);
747}
748
749fn decode_interpolation_scalar(data: [u8; 4]) -> InterpolationScalar {
750 InterpolationScalar {
751 x1: data[0].min(127),
752 y1: data[1].min(127),
753 x2: data[2].min(127),
754 y2: data[3].min(127),
755 }
756}
757
758fn decode_bone_interpolation(
759 interpolation: &[u8; 64],
760) -> (InterpolationVector3, InterpolationScalar) {
761 let position = InterpolationVector3 {
762 x: decode_interpolation_scalar([
763 interpolation[0],
764 interpolation[4],
765 interpolation[8],
766 interpolation[12],
767 ]),
768 y: decode_interpolation_scalar([
769 interpolation[1],
770 interpolation[5],
771 interpolation[9],
772 interpolation[13],
773 ]),
774 z: decode_interpolation_scalar([
775 interpolation[2],
776 interpolation[6],
777 interpolation[10],
778 interpolation[14],
779 ]),
780 };
781 let rotation = decode_interpolation_scalar([
782 interpolation[3],
783 interpolation[7],
784 interpolation[11],
785 interpolation[15],
786 ]);
787 (position, rotation)
788}
789
790pub fn build_clip_from_import(
791 result: VmdImportResult,
792 bone_name_to_index: &dyn Fn(&[u8]) -> Option<BoneIndex>,
793 morph_name_to_index: &dyn Fn(&[u8]) -> Option<MorphIndex>,
794) -> AnimationClip {
795 let mut bone_tracks_map: std::collections::BTreeMap<u32, Vec<MovableBoneKeyframe>> =
796 std::collections::BTreeMap::new();
797
798 for kf in result.bone_keyframes {
799 let bone_index = match &kf.bone_mode {
800 VmdBoneImportMode::ByName(_name) => {
801 match bone_name_to_index(&kf.bone_name_normalized) {
802 Some(idx) => idx,
803 None => continue,
804 }
805 }
806 VmdBoneImportMode::ByIndex(idx) => BoneIndex(*idx),
807 };
808
809 let (pos_interp, rot_interp) = decode_bone_interpolation(&kf.interpolation);
810
811 bone_tracks_map
812 .entry(bone_index.0)
813 .or_default()
814 .push(MovableBoneKeyframe {
815 frame: kf.frame,
816 position: kf.position,
817 rotation: kf.rotation,
818 position_interpolation: pos_interp,
819 rotation_interpolation: rot_interp,
820 });
821 }
822
823 let bone_tracks: Vec<BoneAnimationBinding> = bone_tracks_map
824 .into_iter()
825 .map(|(bone_idx, kfs)| BoneAnimationBinding {
826 bone: BoneIndex(bone_idx),
827 track: MovableBoneTrack::from_keyframes(kfs),
828 })
829 .collect();
830
831 let mut morph_tracks_map: std::collections::BTreeMap<u32, Vec<MorphKeyframe>> =
832 std::collections::BTreeMap::new();
833
834 for (morph_name, frame, weight) in result.morph_keyframes {
835 let morph_name_normalized = normalize_vmd_name(&morph_name);
836 let morph_index = match morph_name_to_index(&morph_name_normalized) {
837 Some(idx) => idx,
838 None => continue,
839 };
840 morph_tracks_map
841 .entry(morph_index.0)
842 .or_default()
843 .push(MorphKeyframe::new(frame, weight));
844 }
845
846 let morph_tracks: Vec<MorphAnimationBinding> = morph_tracks_map
847 .into_iter()
848 .map(|(morph_idx, kfs)| MorphAnimationBinding {
849 morph: MorphIndex(morph_idx),
850 track: MorphTrack::from_keyframes(kfs),
851 })
852 .collect();
853
854 let property_track = if result.property_keyframes.is_empty() {
855 None
856 } else {
857 Some(PropertyAnimationBinding::from_keyframes(
858 result.property_keyframes,
859 ))
860 };
861
862 AnimationClip::new_full(bone_tracks, morph_tracks, property_track)
863}
864
865pub fn build_property_binding_with_ik_resolver(
866 ik_frames: &[VmdPropertyIkFrame],
867 ik_name_to_solver_index: &dyn Fn(&[u8]) -> Option<usize>,
868 solver_count: usize,
869) -> Option<PropertyAnimationBinding> {
870 if solver_count == 0 || ik_frames.is_empty() {
871 return None;
872 }
873
874 let keyframes: Vec<PropertyKeyframe> = ik_frames
875 .iter()
876 .map(|frame| {
877 let mut ik_enabled = vec![1u8; solver_count];
878 for entry in &frame.entries {
879 match ik_name_to_solver_index(&entry.name_normalized) {
880 Some(idx) if idx < solver_count => {
881 ik_enabled[idx] = entry.enabled;
882 }
883 _ => {}
884 }
885 }
886 PropertyKeyframe {
887 frame: frame.frame,
888 ik_enabled: ik_enabled.into_boxed_slice(),
889 }
890 })
891 .collect();
892
893 Some(PropertyAnimationBinding::from_keyframes(keyframes))
894}
895
896#[derive(Debug, Clone, Copy)]
898pub struct VmdClipBuildOptions {
899 pub honor_property_ik: bool,
905}
906
907impl Default for VmdClipBuildOptions {
908 fn default() -> Self {
909 Self {
910 honor_property_ik: true,
911 }
912 }
913}
914
915pub fn build_pair_clip(
916 result: &VmdImportResult,
917 bone_name_to_index: &std::collections::HashMap<Vec<u8>, BoneIndex>,
918 morph_name_to_index: &std::collections::HashMap<Vec<u8>, MorphIndex>,
919 ik_solver_bone_name_to_index: &std::collections::HashMap<Vec<u8>, usize>,
920 solver_count: usize,
921) -> AnimationClip {
922 build_pair_clip_with_options(
923 result,
924 bone_name_to_index,
925 morph_name_to_index,
926 ik_solver_bone_name_to_index,
927 solver_count,
928 VmdClipBuildOptions::default(),
929 )
930}
931
932pub fn build_pair_clip_with_options(
933 result: &VmdImportResult,
934 bone_name_to_index: &std::collections::HashMap<Vec<u8>, BoneIndex>,
935 morph_name_to_index: &std::collections::HashMap<Vec<u8>, MorphIndex>,
936 ik_solver_bone_name_to_index: &std::collections::HashMap<Vec<u8>, usize>,
937 solver_count: usize,
938 options: VmdClipBuildOptions,
939) -> AnimationClip {
940 let mut bone_tracks_map: std::collections::BTreeMap<u32, Vec<MovableBoneKeyframe>> =
941 std::collections::BTreeMap::new();
942
943 for kf in &result.bone_keyframes {
944 let bone_index = match bone_name_to_index.get(&kf.bone_name_normalized) {
945 Some(idx) => *idx,
946 None => continue,
947 };
948
949 let (pos_interp, rot_interp) = decode_bone_interpolation(&kf.interpolation);
950
951 bone_tracks_map
952 .entry(bone_index.0)
953 .or_default()
954 .push(MovableBoneKeyframe {
955 frame: kf.frame,
956 position: kf.position,
957 rotation: kf.rotation,
958 position_interpolation: pos_interp,
959 rotation_interpolation: rot_interp,
960 });
961 }
962
963 let bone_tracks: Vec<BoneAnimationBinding> = bone_tracks_map
964 .into_iter()
965 .map(|(bone_idx, kfs)| BoneAnimationBinding {
966 bone: BoneIndex(bone_idx),
967 track: MovableBoneTrack::from_keyframes(kfs),
968 })
969 .collect();
970
971 let mut morph_tracks_map: std::collections::BTreeMap<u32, Vec<MorphKeyframe>> =
972 std::collections::BTreeMap::new();
973
974 for (morph_name, frame, weight) in &result.morph_keyframes {
975 let morph_name_normalized = normalize_vmd_name(morph_name);
976 let morph_index = match morph_name_to_index.get(&morph_name_normalized) {
977 Some(idx) => *idx,
978 None => continue,
979 };
980 morph_tracks_map
981 .entry(morph_index.0)
982 .or_default()
983 .push(MorphKeyframe::new(*frame, *weight));
984 }
985
986 let morph_tracks: Vec<MorphAnimationBinding> = morph_tracks_map
987 .into_iter()
988 .map(|(morph_idx, kfs)| MorphAnimationBinding {
989 morph: MorphIndex(morph_idx),
990 track: MorphTrack::from_keyframes(kfs),
991 })
992 .collect();
993
994 let property_track = if options.honor_property_ik {
995 build_property_binding_with_ik_resolver(
996 &result.property_ik_frames,
997 &|name| ik_solver_bone_name_to_index.get(name).copied(),
998 solver_count,
999 )
1000 } else {
1001 None
1002 };
1003
1004 AnimationClip::new_full(bone_tracks, morph_tracks, property_track)
1005}
1006
1007#[cfg(test)]
1008mod tests {
1009 use super::*;
1010 #[allow(unused_imports)]
1011 use serde_json;
1012
1013 fn build_vmd_header_bytes() -> Vec<u8> {
1014 let mut buf = Vec::new();
1015 buf.extend_from_slice(&VMD_MAGIC);
1016 buf.extend_from_slice(&[0u8; 20]);
1017 buf
1018 }
1019
1020 #[test]
1021 fn parses_vmd_header() {
1022 let header_bytes = build_vmd_header_bytes();
1023 let (header, pos) = read_header(&header_bytes).unwrap();
1024 assert_eq!(header.model_name_bytes, [0u8; 20]);
1025 assert!(pos > 0);
1026 }
1027
1028 #[test]
1029 fn rejects_bad_vmd_magic() {
1030 let mut buf = build_vmd_header_bytes();
1031 buf[0] = 0xFF;
1032 assert_eq!(read_header(&buf).unwrap_err(), ImportError::InvalidVmdMagic);
1033 }
1034
1035 #[test]
1036 fn accepts_vmd_magic_with_nonzero_padding_bytes() {
1037 let mut buf = build_vmd_header_bytes();
1038 buf[26..30].copy_from_slice(b"JKLM");
1039
1040 let (_header, pos) = read_header(&buf).unwrap();
1041 assert_eq!(pos, 50);
1042 }
1043
1044 #[test]
1045 fn parses_minimal_vmd_motion() {
1046 let mut buf = build_vmd_header_bytes();
1047
1048 buf.extend_from_slice(&0u32.to_le_bytes());
1049 buf.extend_from_slice(&0u32.to_le_bytes());
1050 buf.extend_from_slice(&0u32.to_le_bytes());
1051 buf.extend_from_slice(&0u32.to_le_bytes());
1052 buf.extend_from_slice(&0u32.to_le_bytes());
1053 buf.extend_from_slice(&0u32.to_le_bytes());
1054
1055 let result = import_vmd_motion(&buf).unwrap();
1056 assert!(result.bone_keyframes.is_empty());
1057 assert!(result.morph_keyframes.is_empty());
1058 assert!(result.property_keyframes.is_empty());
1059 }
1060
1061 #[test]
1062 fn accepts_vmd_without_optional_tail_sections() {
1063 let mut buf = build_vmd_header_bytes();
1064 buf.extend_from_slice(&0u32.to_le_bytes());
1065
1066 let result = import_vmd_motion(&buf).unwrap();
1067 assert!(result.bone_keyframes.is_empty());
1068 assert!(result.morph_keyframes.is_empty());
1069 assert!(result.property_keyframes.is_empty());
1070 assert!(result.property_ik_frames.is_empty());
1071 }
1072
1073 #[test]
1074 fn accepts_vmd_ending_after_shadow_section() {
1075 let mut buf = build_vmd_header_bytes();
1076 buf.extend_from_slice(&0u32.to_le_bytes());
1077 buf.extend_from_slice(&0u32.to_le_bytes());
1078 buf.extend_from_slice(&0u32.to_le_bytes());
1079 buf.extend_from_slice(&0u32.to_le_bytes());
1080 buf.extend_from_slice(&0u32.to_le_bytes());
1081
1082 let result = import_vmd_motion(&buf).unwrap();
1083 assert!(result.bone_keyframes.is_empty());
1084 assert!(result.morph_keyframes.is_empty());
1085 assert!(result.property_keyframes.is_empty());
1086 assert!(result.property_ik_frames.is_empty());
1087 }
1088
1089 #[test]
1090 fn rejects_partial_optional_count() {
1091 let mut buf = build_vmd_header_bytes();
1092 buf.extend_from_slice(&0u32.to_le_bytes());
1093 buf.extend_from_slice(&[1, 2, 3]);
1094
1095 assert_eq!(
1096 import_vmd_motion(&buf).unwrap_err(),
1097 ImportError::UnexpectedEof(1)
1098 );
1099 }
1100
1101 #[test]
1102 fn ignores_truncated_unused_tail_sections() {
1103 let mut buf = build_vmd_header_bytes();
1104 buf.extend_from_slice(&0u32.to_le_bytes());
1105 buf.extend_from_slice(&0u32.to_le_bytes());
1106 buf.extend_from_slice(&0u32.to_le_bytes());
1107 buf.extend_from_slice(&0u32.to_le_bytes());
1108 buf.extend_from_slice(&10u32.to_le_bytes());
1109 buf.extend_from_slice(&[0u8; 8]);
1110
1111 let result = import_vmd_motion(&buf).unwrap();
1112 assert!(result.bone_keyframes.is_empty());
1113 assert!(result.morph_keyframes.is_empty());
1114 assert!(result.property_keyframes.is_empty());
1115 assert!(result.property_ik_frames.is_empty());
1116 }
1117
1118 #[test]
1119 fn parsed_animation_ignores_implausible_optional_tail() {
1120 let mut buf = build_vmd_header_bytes();
1121 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());
1129 buf.extend_from_slice(&[0u8; 57]);
1130
1131 let parsed = parse_vmd_animation(&buf).unwrap();
1132 assert!(parsed.camera_frames.is_empty());
1133 assert!(parsed.light_frames.is_empty());
1134 assert!(parsed.self_shadow_frames.is_empty());
1135 assert!(parsed.property_frames.is_empty());
1136 }
1137
1138 #[test]
1139 fn parses_single_bone_keyframe() {
1140 let mut buf = build_vmd_header_bytes();
1141
1142 buf.extend_from_slice(&1u32.to_le_bytes());
1143 let mut bone_name = [0u8; 15];
1144 bone_name[..4].copy_from_slice(b"Bone");
1145 buf.extend_from_slice(&bone_name);
1146 buf.extend_from_slice(&10u32.to_le_bytes());
1147 buf.extend_from_slice(&1.0f32.to_le_bytes());
1148 buf.extend_from_slice(&2.0f32.to_le_bytes());
1149 buf.extend_from_slice(&3.0f32.to_le_bytes());
1150 buf.extend_from_slice(&0.0f32.to_le_bytes());
1151 buf.extend_from_slice(&0.0f32.to_le_bytes());
1152 buf.extend_from_slice(&0.0f32.to_le_bytes());
1153 buf.extend_from_slice(&1.0f32.to_le_bytes());
1154 buf.extend_from_slice(&[20u8; 64]);
1155
1156 buf.extend_from_slice(&0u32.to_le_bytes());
1157 buf.extend_from_slice(&0u32.to_le_bytes());
1158 buf.extend_from_slice(&0u32.to_le_bytes());
1159 buf.extend_from_slice(&0u32.to_le_bytes());
1160 buf.extend_from_slice(&0u32.to_le_bytes());
1161
1162 let result = import_vmd_motion(&buf).unwrap();
1163 assert_eq!(result.bone_keyframes.len(), 1);
1164 assert_eq!(result.bone_keyframes[0].frame, 10);
1165 assert!((result.bone_keyframes[0].position.x - 1.0).abs() < 0.001);
1166 assert!((result.bone_keyframes[0].position.y - 2.0).abs() < 0.001);
1167 assert!((result.bone_keyframes[0].position.z - 3.0).abs() < 0.001);
1168 assert_eq!(&result.bone_keyframes[0].bone_name_normalized[..], b"Bone");
1169 }
1170
1171 #[test]
1172 fn exports_parsed_vmd_animation_for_roundtrip() {
1173 let mut buf = build_vmd_header_bytes();
1174
1175 buf.extend_from_slice(&1u32.to_le_bytes());
1176 let mut bone_name = [0u8; 15];
1177 bone_name[..4].copy_from_slice(b"Bone");
1178 buf.extend_from_slice(&bone_name);
1179 buf.extend_from_slice(&10u32.to_le_bytes());
1180 buf.extend_from_slice(&1.0f32.to_le_bytes());
1181 buf.extend_from_slice(&2.0f32.to_le_bytes());
1182 buf.extend_from_slice(&3.0f32.to_le_bytes());
1183 buf.extend_from_slice(&0.1f32.to_le_bytes());
1184 buf.extend_from_slice(&0.2f32.to_le_bytes());
1185 buf.extend_from_slice(&0.3f32.to_le_bytes());
1186 buf.extend_from_slice(&0.4f32.to_le_bytes());
1187 buf.extend_from_slice(&[20u8; 64]);
1188
1189 buf.extend_from_slice(&1u32.to_le_bytes());
1190 let mut morph_name = [0u8; 15];
1191 morph_name[..5].copy_from_slice(b"Smile");
1192 buf.extend_from_slice(&morph_name);
1193 buf.extend_from_slice(&11u32.to_le_bytes());
1194 buf.extend_from_slice(&0.75f32.to_le_bytes());
1195
1196 buf.extend_from_slice(&1u32.to_le_bytes());
1197 buf.extend_from_slice(&12u32.to_le_bytes());
1198 buf.extend_from_slice(&30.0f32.to_le_bytes());
1199 buf.extend_from_slice(&1.0f32.to_le_bytes());
1200 buf.extend_from_slice(&2.0f32.to_le_bytes());
1201 buf.extend_from_slice(&3.0f32.to_le_bytes());
1202 buf.extend_from_slice(&0.1f32.to_le_bytes());
1203 buf.extend_from_slice(&0.2f32.to_le_bytes());
1204 buf.extend_from_slice(&0.3f32.to_le_bytes());
1205 buf.extend_from_slice(&[30u8; 24]);
1206 buf.extend_from_slice(&45u32.to_le_bytes());
1207 buf.push(0);
1208
1209 buf.extend_from_slice(&1u32.to_le_bytes());
1210 buf.extend_from_slice(&13u32.to_le_bytes());
1211 buf.extend_from_slice(&1.0f32.to_le_bytes());
1212 buf.extend_from_slice(&0.5f32.to_le_bytes());
1213 buf.extend_from_slice(&0.25f32.to_le_bytes());
1214 buf.extend_from_slice(&(-1.0f32).to_le_bytes());
1215 buf.extend_from_slice(&(-0.5f32).to_le_bytes());
1216 buf.extend_from_slice(&(-0.25f32).to_le_bytes());
1217
1218 buf.extend_from_slice(&1u32.to_le_bytes());
1219 buf.extend_from_slice(&14u32.to_le_bytes());
1220 buf.push(2);
1221 buf.extend_from_slice(&0.6f32.to_le_bytes());
1222
1223 buf.extend_from_slice(&1u32.to_le_bytes());
1224 buf.extend_from_slice(&15u32.to_le_bytes());
1225 buf.push(1);
1226 buf.extend_from_slice(&1u32.to_le_bytes());
1227 let mut ik_name = [0u8; 20];
1228 ik_name[..2].copy_from_slice(b"IK");
1229 buf.extend_from_slice(&ik_name);
1230 buf.push(1);
1231
1232 let parsed = parse_vmd_animation(&buf).unwrap();
1233 let exported = export_vmd_animation(&parsed);
1234 let reparsed = parse_vmd_animation(&exported).unwrap();
1235
1236 assert_vmd_roundtrip_eq(&parsed, &reparsed);
1237 }
1238
1239 fn assert_vmd_roundtrip_eq(left: &VmdParsedAnimation, right: &VmdParsedAnimation) {
1240 assert_eq!(left.metadata.model_name, right.metadata.model_name);
1241 assert_eq!(left.metadata.max_frame, right.metadata.max_frame);
1242 assert_eq!(left.bone_frames.len(), right.bone_frames.len());
1243 assert_eq!(left.morph_frames.len(), right.morph_frames.len());
1244 assert_eq!(left.camera_frames.len(), right.camera_frames.len());
1245 assert_eq!(left.light_frames.len(), right.light_frames.len());
1246 assert_eq!(
1247 left.self_shadow_frames.len(),
1248 right.self_shadow_frames.len()
1249 );
1250 assert_eq!(left.property_frames.len(), right.property_frames.len());
1251 assert_eq!(
1252 left.bone_frames[0].bone_name,
1253 right.bone_frames[0].bone_name
1254 );
1255 assert_eq!(left.bone_frames[0].frame, right.bone_frames[0].frame);
1256 assert_eq!(
1257 left.bone_frames[0].translation,
1258 right.bone_frames[0].translation
1259 );
1260 assert_eq!(left.bone_frames[0].rotation, right.bone_frames[0].rotation);
1261 assert_eq!(
1262 left.bone_frames[0].interpolation,
1263 right.bone_frames[0].interpolation
1264 );
1265 assert_eq!(
1266 left.morph_frames[0].morph_name,
1267 right.morph_frames[0].morph_name
1268 );
1269 assert_eq!(left.morph_frames[0].frame, right.morph_frames[0].frame);
1270 assert_eq!(left.morph_frames[0].weight, right.morph_frames[0].weight);
1271 assert_eq!(left.camera_frames[0].frame, right.camera_frames[0].frame);
1272 assert_eq!(
1273 left.camera_frames[0].position,
1274 right.camera_frames[0].position
1275 );
1276 assert_eq!(
1277 left.camera_frames[0].rotation,
1278 right.camera_frames[0].rotation
1279 );
1280 assert_eq!(
1281 left.camera_frames[0].interpolation,
1282 right.camera_frames[0].interpolation
1283 );
1284 assert_eq!(
1285 left.camera_frames[0].perspective,
1286 right.camera_frames[0].perspective
1287 );
1288 assert_eq!(left.light_frames[0].color, right.light_frames[0].color);
1289 assert_eq!(
1290 left.light_frames[0].direction,
1291 right.light_frames[0].direction
1292 );
1293 assert_eq!(
1294 left.self_shadow_frames[0].mode,
1295 right.self_shadow_frames[0].mode
1296 );
1297 assert_eq!(
1298 left.self_shadow_frames[0].distance,
1299 right.self_shadow_frames[0].distance
1300 );
1301 assert_eq!(
1302 left.property_frames[0].visible,
1303 right.property_frames[0].visible
1304 );
1305 assert_eq!(
1306 left.property_frames[0].ik_states[0].bone_name,
1307 right.property_frames[0].ik_states[0].bone_name
1308 );
1309 assert_eq!(
1310 left.property_frames[0].ik_states[0].enabled,
1311 right.property_frames[0].ik_states[0].enabled
1312 );
1313 }
1314
1315 #[test]
1316 fn decodes_raw_vmd_bone_interpolation_as_strided_curves() {
1317 let mut interpolation = [0u8; 64];
1318 for (index, value) in interpolation.iter_mut().enumerate().take(16) {
1319 *value = index as u8;
1320 }
1321
1322 let (position, rotation) = decode_bone_interpolation(&interpolation);
1323
1324 assert_eq!(
1325 position.x,
1326 InterpolationScalar {
1327 x1: 0,
1328 y1: 4,
1329 x2: 8,
1330 y2: 12
1331 }
1332 );
1333 assert_eq!(
1334 position.y,
1335 InterpolationScalar {
1336 x1: 1,
1337 y1: 5,
1338 x2: 9,
1339 y2: 13
1340 }
1341 );
1342 assert_eq!(
1343 position.z,
1344 InterpolationScalar {
1345 x1: 2,
1346 y1: 6,
1347 x2: 10,
1348 y2: 14
1349 }
1350 );
1351 assert_eq!(
1352 rotation,
1353 InterpolationScalar {
1354 x1: 3,
1355 y1: 7,
1356 x2: 11,
1357 y2: 15
1358 }
1359 );
1360 }
1361
1362 #[test]
1363 fn skips_camera_light_shadow_and_reads_ik_property_names() {
1364 let mut buf = build_vmd_header_bytes();
1365
1366 buf.extend_from_slice(&0u32.to_le_bytes());
1367 buf.extend_from_slice(&0u32.to_le_bytes());
1368 buf.extend_from_slice(&1u32.to_le_bytes());
1369 buf.extend_from_slice(&[0u8; 61]);
1370 buf.extend_from_slice(&1u32.to_le_bytes());
1371 buf.extend_from_slice(&[0u8; 28]);
1372 buf.extend_from_slice(&1u32.to_le_bytes());
1373 buf.extend_from_slice(&0u32.to_le_bytes());
1374 buf.push(1);
1375 buf.extend_from_slice(&0.5f32.to_le_bytes());
1376 buf.extend_from_slice(&1u32.to_le_bytes());
1377 buf.extend_from_slice(&24u32.to_le_bytes());
1378 buf.push(1);
1379 buf.extend_from_slice(&1u32.to_le_bytes());
1380 let mut ik_name = [0u8; 20];
1381 ik_name[..6].copy_from_slice(b"LeftIK");
1382 buf.extend_from_slice(&ik_name);
1383 buf.push(0);
1384
1385 let result = import_vmd_motion(&buf).unwrap();
1386 assert_eq!(result.property_keyframes.len(), 1);
1387 assert_eq!(result.property_keyframes[0].frame, 24);
1388 assert_eq!(&*result.property_keyframes[0].ik_enabled, &[0]);
1389
1390 assert_eq!(result.property_ik_frames.len(), 1);
1391 assert_eq!(result.property_ik_frames[0].frame, 24);
1392 assert_eq!(result.property_ik_frames[0].show, 1);
1393 assert_eq!(result.property_ik_frames[0].entries.len(), 1);
1394 let name_end = result.property_ik_frames[0].entries[0]
1395 .name_bytes
1396 .iter()
1397 .position(|&b| b == 0)
1398 .unwrap_or(20);
1399 assert_eq!(
1400 &result.property_ik_frames[0].entries[0].name_bytes[..name_end],
1401 b"LeftIK"
1402 );
1403 assert_eq!(result.property_ik_frames[0].entries[0].enabled, 0);
1404 assert_eq!(
1405 &result.property_ik_frames[0].entries[0].name_normalized[..],
1406 b"LeftIK"
1407 );
1408 }
1409
1410 #[test]
1411 fn builds_clip_from_bone_track() {
1412 let kfs = vec![
1413 VmdBoneKeyframeRaw {
1414 bone_mode: VmdBoneImportMode::ByName(b"BoneA".to_vec()),
1415 frame: 0,
1416 position: Vec3A::ZERO,
1417 rotation: Quat::IDENTITY,
1418 interpolation: [
1419 20, 20, 107, 107, 20, 20, 107, 107, 20, 20, 107, 107, 20, 20, 107, 107, 20, 20,
1420 107, 107, 20, 20, 107, 107, 20, 20, 107, 107, 20, 20, 107, 107, 20, 20, 107,
1421 107, 20, 20, 107, 107, 20, 20, 107, 107, 20, 20, 107, 107, 20, 20, 107, 107,
1422 20, 20, 107, 107, 20, 20, 107, 107, 20, 20, 107, 107,
1423 ],
1424 bone_name_normalized: b"BoneA".to_vec(),
1425 },
1426 VmdBoneKeyframeRaw {
1427 bone_mode: VmdBoneImportMode::ByName(b"BoneA".to_vec()),
1428 frame: 30,
1429 position: Vec3A::new(10.0, 0.0, 0.0),
1430 rotation: Quat::IDENTITY,
1431 interpolation: [
1432 20, 20, 107, 107, 20, 20, 107, 107, 20, 20, 107, 107, 20, 20, 107, 107, 20, 20,
1433 107, 107, 20, 20, 107, 107, 20, 20, 107, 107, 20, 20, 107, 107, 20, 20, 107,
1434 107, 20, 20, 107, 107, 20, 20, 107, 107, 20, 20, 107, 107, 20, 20, 107, 107,
1435 20, 20, 107, 107, 20, 20, 107, 107, 20, 20, 107, 107,
1436 ],
1437 bone_name_normalized: b"BoneA".to_vec(),
1438 },
1439 ];
1440
1441 fn lookup(name: &[u8]) -> Option<BoneIndex> {
1442 if name == b"BoneA" {
1443 Some(BoneIndex(0))
1444 } else {
1445 None
1446 }
1447 }
1448 fn morph_lookup(_name: &[u8]) -> Option<MorphIndex> {
1449 None
1450 }
1451
1452 let result = VmdImportResult {
1453 bone_keyframes: kfs,
1454 morph_keyframes: Vec::new(),
1455 property_keyframes: Vec::new(),
1456 property_ik_frames: Vec::new(),
1457 };
1458
1459 let _clip = build_clip_from_import(result, &lookup, &morph_lookup);
1460 }
1461
1462 #[test]
1463 fn build_clip_from_import_resolves_morph_by_normalized_name() {
1464 let sjis_morph = vec![0x83, 0x65, 0x83, 0x58, 0x83, 0x67];
1465 let result = VmdImportResult {
1466 bone_keyframes: Vec::new(),
1467 morph_keyframes: vec![(sjis_morph, 0, 1.0)],
1468 property_keyframes: Vec::new(),
1469 property_ik_frames: Vec::new(),
1470 };
1471
1472 fn morph_lookup(name: &[u8]) -> Option<MorphIndex> {
1473 if name == b"\xE3\x83\x86\xE3\x82\xB9\xE3\x83\x88" {
1474 Some(MorphIndex(0))
1475 } else {
1476 None
1477 }
1478 }
1479 fn bone_lookup(_name: &[u8]) -> Option<BoneIndex> {
1480 None
1481 }
1482
1483 let clip = build_clip_from_import(result, &bone_lookup, &morph_lookup);
1484 assert_eq!(clip.morph_track_count(), 1);
1485 }
1486
1487 fn ik_name_bytes(name: &str) -> [u8; 20] {
1488 let mut buf = [0u8; 20];
1489 let name_bytes = name.as_bytes();
1490 let len = name_bytes.len().min(20);
1491 buf[..len].copy_from_slice(&name_bytes[..len]);
1492 buf
1493 }
1494
1495 #[test]
1496 fn parses_property_ik_entry_names() {
1497 let mut buf = build_vmd_header_bytes();
1498
1499 buf.extend_from_slice(&0u32.to_le_bytes());
1500 buf.extend_from_slice(&0u32.to_le_bytes());
1501 buf.extend_from_slice(&0u32.to_le_bytes());
1502 buf.extend_from_slice(&0u32.to_le_bytes());
1503 buf.extend_from_slice(&0u32.to_le_bytes());
1504 buf.extend_from_slice(&1u32.to_le_bytes());
1505 buf.extend_from_slice(&10u32.to_le_bytes());
1506 buf.push(0);
1507 buf.extend_from_slice(&2u32.to_le_bytes());
1508 buf.extend_from_slice(&ik_name_bytes("LeftLegIK"));
1509 buf.push(1);
1510 buf.extend_from_slice(&ik_name_bytes("RightLegIK"));
1511 buf.push(0);
1512
1513 let result = import_vmd_motion(&buf).unwrap();
1514 assert_eq!(result.property_ik_frames.len(), 1);
1515 assert_eq!(result.property_ik_frames[0].frame, 10);
1516 assert_eq!(result.property_ik_frames[0].entries.len(), 2);
1517
1518 let name1: Vec<u8> = result.property_ik_frames[0].entries[0]
1519 .name_bytes
1520 .iter()
1521 .copied()
1522 .take_while(|&b| b != 0)
1523 .collect();
1524 assert_eq!(&name1[..], b"LeftLegIK");
1525 assert_eq!(result.property_ik_frames[0].entries[0].enabled, 1);
1526 assert_eq!(
1527 &result.property_ik_frames[0].entries[0].name_normalized[..],
1528 b"LeftLegIK"
1529 );
1530
1531 let name2: Vec<u8> = result.property_ik_frames[0].entries[1]
1532 .name_bytes
1533 .iter()
1534 .copied()
1535 .take_while(|&b| b != 0)
1536 .collect();
1537 assert_eq!(&name2[..], b"RightLegIK");
1538 assert_eq!(result.property_ik_frames[0].entries[1].enabled, 0);
1539 }
1540
1541 #[test]
1542 fn reorders_ik_enabled_to_solver_order() {
1543 let frames = vec![VmdPropertyIkFrame {
1544 frame: 0,
1545 show: 0,
1546 entries: vec![
1547 VmdIkEntry {
1548 name_bytes: {
1549 let mut buf = [0u8; 20];
1550 buf[..9].copy_from_slice(b"RightLegI");
1551 buf.to_vec()
1552 },
1553 enabled: 1,
1554 name_normalized: b"RightLegI".to_vec(),
1555 },
1556 VmdIkEntry {
1557 name_bytes: {
1558 let mut buf = [0u8; 20];
1559 buf[..8].copy_from_slice(b"LeftLegI");
1560 buf.to_vec()
1561 },
1562 enabled: 0,
1563 name_normalized: b"LeftLegI".to_vec(),
1564 },
1565 ],
1566 }];
1567
1568 fn ik_resolver(name: &[u8]) -> Option<usize> {
1569 match name {
1570 b"LeftLegI" => Some(0),
1571 b"RightLegI" => Some(1),
1572 _ => None,
1573 }
1574 }
1575
1576 let binding = build_property_binding_with_ik_resolver(&frames, &ik_resolver, 2).unwrap();
1577 let sample = binding.sample(0.0).unwrap();
1578 assert_eq!(sample, &[0, 1]);
1579 }
1580
1581 #[test]
1582 fn unknown_and_unmentioned_ik_names_keep_default_enabled_state() {
1583 let frames = vec![VmdPropertyIkFrame {
1584 frame: 5,
1585 show: 0,
1586 entries: vec![
1587 VmdIkEntry {
1588 name_bytes: {
1589 let mut buf = [0u8; 20];
1590 buf[..7].copy_from_slice(b"KnownIK");
1591 buf.to_vec()
1592 },
1593 enabled: 1,
1594 name_normalized: b"KnownIK".to_vec(),
1595 },
1596 VmdIkEntry {
1597 name_bytes: {
1598 let mut buf = [0u8; 20];
1599 buf[..9].copy_from_slice(b"UnknownIK");
1600 buf.to_vec()
1601 },
1602 enabled: 1,
1603 name_normalized: b"UnknownIK".to_vec(),
1604 },
1605 ],
1606 }];
1607
1608 fn ik_resolver(name: &[u8]) -> Option<usize> {
1609 if name == b"KnownIK" { Some(0) } else { None }
1610 }
1611
1612 let binding = build_property_binding_with_ik_resolver(&frames, &ik_resolver, 2).unwrap();
1613 let sample = binding.sample(5.0).unwrap();
1614 assert_eq!(sample, &[1, 1]);
1615 }
1616
1617 #[test]
1618 fn empty_ik_frames_returns_none() {
1619 fn ik_resolver(_name: &[u8]) -> Option<usize> {
1620 None
1621 }
1622 let binding = build_property_binding_with_ik_resolver(&[], &ik_resolver, 2);
1623 assert!(binding.is_none());
1624 }
1625
1626 #[test]
1627 fn zero_solver_count_returns_none() {
1628 let frames = vec![VmdPropertyIkFrame {
1629 frame: 0,
1630 show: 0,
1631 entries: vec![VmdIkEntry {
1632 name_bytes: ik_name_bytes("IK").to_vec(),
1633 enabled: 1,
1634 name_normalized: b"IK".to_vec(),
1635 }],
1636 }];
1637 fn ik_resolver(_name: &[u8]) -> Option<usize> {
1638 Some(0)
1639 }
1640 let binding = build_property_binding_with_ik_resolver(&frames, &ik_resolver, 0);
1641 assert!(binding.is_none());
1642 }
1643
1644 #[test]
1645 fn build_property_binding_resolves_ik_by_normalized_name() {
1646 let frames = vec![VmdPropertyIkFrame {
1647 frame: 0,
1648 show: 0,
1649 entries: vec![VmdIkEntry {
1650 name_bytes: vec![0x8D, 0xB6, 0x91, 0xAB],
1651 enabled: 1,
1652 name_normalized: vec![0xE5, 0xB7, 0xA6, 0xE8, 0xB6, 0xB3],
1653 }],
1654 }];
1655
1656 fn ik_resolver(name: &[u8]) -> Option<usize> {
1657 if name == b"\xE5\xB7\xA6\xE8\xB6\xB3" {
1658 Some(0)
1659 } else {
1660 None
1661 }
1662 }
1663
1664 let binding = build_property_binding_with_ik_resolver(&frames, &ik_resolver, 1).unwrap();
1665 let sample = binding.sample(0.0).unwrap();
1666 assert_eq!(sample, &[1]);
1667 }
1668
1669 #[test]
1670 fn build_clip_from_import_resolves_bone_by_normalized_name() {
1671 let sjis_name: Vec<u8> = vec![0x8D, 0xB6, 0x91, 0xAB];
1672 let utf8_name: Vec<u8> = vec![0xE5, 0xB7, 0xA6, 0xE8, 0xB6, 0xB3];
1673
1674 let kf = VmdBoneKeyframeRaw {
1675 bone_mode: VmdBoneImportMode::ByName(sjis_name),
1676 frame: 0,
1677 position: Vec3A::ZERO,
1678 rotation: Quat::IDENTITY,
1679 interpolation: [20u8; 64],
1680 bone_name_normalized: utf8_name.clone(),
1681 };
1682
1683 fn lookup(name: &[u8]) -> Option<BoneIndex> {
1684 if name == b"\xE5\xB7\xA6\xE8\xB6\xB3" {
1685 Some(BoneIndex(0))
1686 } else {
1687 None
1688 }
1689 }
1690 fn morph_lookup(_name: &[u8]) -> Option<MorphIndex> {
1691 None
1692 }
1693
1694 let result = VmdImportResult {
1695 bone_keyframes: vec![kf],
1696 morph_keyframes: Vec::new(),
1697 property_keyframes: Vec::new(),
1698 property_ik_frames: Vec::new(),
1699 };
1700
1701 let clip = build_clip_from_import(result, &lookup, &morph_lookup);
1702 assert_eq!(
1703 clip.bone_track_count(),
1704 1,
1705 "bone should be resolved via normalized UTF-8 name, not raw Shift-JIS bytes"
1706 );
1707 }
1708
1709 #[test]
1710 fn japanese_vmd_name_matches_pmx_utf8_name_via_normalization() {
1711 let sjis_name: &[u8] = &[0x8D, 0xB6, 0x91, 0xAB];
1712 let utf8_name: &[u8] = &[0xE5, 0xB7, 0xA6, 0xE8, 0xB6, 0xB3];
1713
1714 let mut pmx_buf = Vec::new();
1715 pmx_buf.extend_from_slice(b"PMX ");
1716 pmx_buf.extend_from_slice(&2.0f32.to_le_bytes());
1717 pmx_buf.push(8);
1718 pmx_buf.push(1);
1719 pmx_buf.push(0);
1720 pmx_buf.push(4);
1721 pmx_buf.push(1);
1722 pmx_buf.push(1);
1723 pmx_buf.push(2);
1724 pmx_buf.push(1);
1725 pmx_buf.push(1);
1726 for _ in 0..4 {
1727 pmx_buf.extend_from_slice(&0i32.to_le_bytes());
1728 }
1729 pmx_buf.extend_from_slice(&0i32.to_le_bytes());
1730 pmx_buf.extend_from_slice(&0i32.to_le_bytes());
1731 pmx_buf.extend_from_slice(&0i32.to_le_bytes());
1732 pmx_buf.extend_from_slice(&0i32.to_le_bytes());
1733 pmx_buf.extend_from_slice(&1i32.to_le_bytes());
1734 pmx_buf.extend_from_slice(&(utf8_name.len() as i32).to_le_bytes());
1735 pmx_buf.extend_from_slice(utf8_name);
1736 pmx_buf.extend_from_slice(&0i32.to_le_bytes());
1737 pmx_buf.extend_from_slice(&0.0f32.to_le_bytes());
1738 pmx_buf.extend_from_slice(&0.0f32.to_le_bytes());
1739 pmx_buf.extend_from_slice(&0.0f32.to_le_bytes());
1740 pmx_buf.extend_from_slice(&(-1i16).to_le_bytes());
1741 pmx_buf.extend_from_slice(&0i32.to_le_bytes());
1742 pmx_buf.extend_from_slice(&0u16.to_le_bytes());
1743 pmx_buf.extend_from_slice(&0.0f32.to_le_bytes());
1744 pmx_buf.extend_from_slice(&0.0f32.to_le_bytes());
1745 pmx_buf.extend_from_slice(&0.0f32.to_le_bytes());
1746 pmx_buf.extend_from_slice(&0i32.to_le_bytes());
1747
1748 let pmx = crate::pmx::import_pmx_runtime(&pmx_buf).unwrap();
1749
1750 let mut vmd_buf = build_vmd_header_bytes();
1751 vmd_buf.extend_from_slice(&1u32.to_le_bytes());
1752 let mut bone_name = [0u8; 15];
1753 bone_name[..sjis_name.len()].copy_from_slice(sjis_name);
1754 vmd_buf.extend_from_slice(&bone_name);
1755 vmd_buf.extend_from_slice(&0u32.to_le_bytes());
1756 vmd_buf.extend_from_slice(&0.0f32.to_le_bytes());
1757 vmd_buf.extend_from_slice(&0.0f32.to_le_bytes());
1758 vmd_buf.extend_from_slice(&0.0f32.to_le_bytes());
1759 vmd_buf.extend_from_slice(&0.0f32.to_le_bytes());
1760 vmd_buf.extend_from_slice(&0.0f32.to_le_bytes());
1761 vmd_buf.extend_from_slice(&0.0f32.to_le_bytes());
1762 vmd_buf.extend_from_slice(&1.0f32.to_le_bytes());
1763 vmd_buf.extend_from_slice(&[20u8; 64]);
1764 vmd_buf.extend_from_slice(&0u32.to_le_bytes());
1765 vmd_buf.extend_from_slice(&0u32.to_le_bytes());
1766 vmd_buf.extend_from_slice(&0u32.to_le_bytes());
1767 vmd_buf.extend_from_slice(&0u32.to_le_bytes());
1768 vmd_buf.extend_from_slice(&0u32.to_le_bytes());
1769
1770 let vmd = import_vmd_motion(&vmd_buf).unwrap();
1771
1772 let normalized = normalize_vmd_name(sjis_name);
1773 assert_eq!(&normalized[..], utf8_name);
1774
1775 assert!(
1776 pmx.bone_name_to_index.contains_key(&normalized),
1777 "PMX should have normalized bone name key"
1778 );
1779
1780 let clip = build_pair_clip(
1781 &vmd,
1782 &pmx.bone_name_to_index,
1783 &pmx.morph_name_to_index,
1784 &pmx.ik_solver_bone_name_to_index,
1785 pmx.model.ik_count(),
1786 );
1787
1788 assert_eq!(clip.bone_track_count(), 1);
1789 assert_eq!(clip.morph_track_count(), 0);
1790 assert!(!clip.has_property_track());
1791 }
1792
1793 #[test]
1794 fn japanese_vmd_name_matches_pmx_utf16le_name_via_decoded_key() {
1795 let sjis_name: &[u8] = &[0x8D, 0xB6, 0x91, 0xAB];
1796 let utf8_name: &[u8] = &[0xE5, 0xB7, 0xA6, 0xE8, 0xB6, 0xB3];
1797 let utf16le_name: Vec<u8> = "\u{5DE6}\u{8DB3}"
1798 .encode_utf16()
1799 .flat_map(u16::to_le_bytes)
1800 .collect();
1801
1802 let mut pmx_buf = Vec::new();
1803 pmx_buf.extend_from_slice(b"PMX ");
1804 pmx_buf.extend_from_slice(&2.0f32.to_le_bytes());
1805 pmx_buf.push(8);
1806 pmx_buf.push(0);
1807 pmx_buf.push(0);
1808 pmx_buf.push(4);
1809 pmx_buf.push(1);
1810 pmx_buf.push(1);
1811 pmx_buf.push(2);
1812 pmx_buf.push(1);
1813 pmx_buf.push(1);
1814 for _ in 0..4 {
1815 pmx_buf.extend_from_slice(&0i32.to_le_bytes());
1816 }
1817 pmx_buf.extend_from_slice(&0i32.to_le_bytes());
1818 pmx_buf.extend_from_slice(&0i32.to_le_bytes());
1819 pmx_buf.extend_from_slice(&0i32.to_le_bytes());
1820 pmx_buf.extend_from_slice(&0i32.to_le_bytes());
1821 pmx_buf.extend_from_slice(&1i32.to_le_bytes());
1822 pmx_buf.extend_from_slice(&(utf16le_name.len() as i32).to_le_bytes());
1823 pmx_buf.extend_from_slice(&utf16le_name);
1824 pmx_buf.extend_from_slice(&0i32.to_le_bytes());
1825 pmx_buf.extend_from_slice(&0.0f32.to_le_bytes());
1826 pmx_buf.extend_from_slice(&0.0f32.to_le_bytes());
1827 pmx_buf.extend_from_slice(&0.0f32.to_le_bytes());
1828 pmx_buf.extend_from_slice(&(-1i16).to_le_bytes());
1829 pmx_buf.extend_from_slice(&0i32.to_le_bytes());
1830 pmx_buf.extend_from_slice(&0u16.to_le_bytes());
1831 pmx_buf.extend_from_slice(&0.0f32.to_le_bytes());
1832 pmx_buf.extend_from_slice(&0.0f32.to_le_bytes());
1833 pmx_buf.extend_from_slice(&0.0f32.to_le_bytes());
1834 pmx_buf.extend_from_slice(&0i32.to_le_bytes());
1835
1836 let pmx = crate::pmx::import_pmx_runtime(&pmx_buf).unwrap();
1837
1838 let mut vmd_buf = build_vmd_header_bytes();
1839 vmd_buf.extend_from_slice(&1u32.to_le_bytes());
1840 let mut bone_name = [0u8; 15];
1841 bone_name[..sjis_name.len()].copy_from_slice(sjis_name);
1842 vmd_buf.extend_from_slice(&bone_name);
1843 vmd_buf.extend_from_slice(&0u32.to_le_bytes());
1844 vmd_buf.extend_from_slice(&0.0f32.to_le_bytes());
1845 vmd_buf.extend_from_slice(&0.0f32.to_le_bytes());
1846 vmd_buf.extend_from_slice(&0.0f32.to_le_bytes());
1847 vmd_buf.extend_from_slice(&0.0f32.to_le_bytes());
1848 vmd_buf.extend_from_slice(&0.0f32.to_le_bytes());
1849 vmd_buf.extend_from_slice(&0.0f32.to_le_bytes());
1850 vmd_buf.extend_from_slice(&1.0f32.to_le_bytes());
1851 vmd_buf.extend_from_slice(&[20u8; 64]);
1852
1853 let vmd = import_vmd_motion(&vmd_buf).unwrap();
1854 let normalized = normalize_vmd_name(sjis_name);
1855 assert_eq!(&normalized[..], utf8_name);
1856 assert!(
1857 pmx.bone_name_to_index.contains_key(&utf16le_name),
1858 "PMX should retain the raw UTF-16LE name key"
1859 );
1860 assert!(
1861 pmx.bone_name_to_index.contains_key(&normalized),
1862 "PMX should also index UTF-16LE names by decoded UTF-8 bytes"
1863 );
1864
1865 let clip = build_pair_clip(
1866 &vmd,
1867 &pmx.bone_name_to_index,
1868 &pmx.morph_name_to_index,
1869 &pmx.ik_solver_bone_name_to_index,
1870 pmx.model.ik_count(),
1871 );
1872
1873 assert_eq!(clip.bone_track_count(), 1);
1874 }
1875
1876 #[test]
1877 fn build_pair_clip_default_includes_property_ik() {
1878 let result = VmdImportResult {
1880 bone_keyframes: Vec::new(),
1881 morph_keyframes: Vec::new(),
1882 property_keyframes: Vec::new(),
1883 property_ik_frames: vec![VmdPropertyIkFrame {
1884 frame: 0,
1885 show: 0,
1886 entries: vec![VmdIkEntry {
1887 name_bytes: ik_name_bytes("LeftLegIK").to_vec(),
1888 enabled: 0,
1889 name_normalized: b"LeftLegIK".to_vec(),
1890 }],
1891 }],
1892 };
1893
1894 let bone_name_to_index: std::collections::HashMap<Vec<u8>, BoneIndex> =
1895 std::collections::HashMap::new();
1896 let morph_name_to_index: std::collections::HashMap<Vec<u8>, MorphIndex> =
1897 std::collections::HashMap::new();
1898 let mut ik_solver_bone_name_to_index: std::collections::HashMap<Vec<u8>, usize> =
1899 std::collections::HashMap::new();
1900 ik_solver_bone_name_to_index.insert(b"LeftLegIK".to_vec(), 0);
1901
1902 let clip = build_pair_clip(
1903 &result,
1904 &bone_name_to_index,
1905 &morph_name_to_index,
1906 &ik_solver_bone_name_to_index,
1907 2, );
1909
1910 assert!(
1911 clip.has_property_track(),
1912 "default build_pair_clip should include property IK track"
1913 );
1914 }
1915
1916 #[test]
1917 fn build_pair_clip_with_options_omits_property_ik() {
1918 let result = VmdImportResult {
1920 bone_keyframes: Vec::new(),
1921 morph_keyframes: Vec::new(),
1922 property_keyframes: Vec::new(),
1923 property_ik_frames: vec![VmdPropertyIkFrame {
1924 frame: 5,
1925 show: 0,
1926 entries: vec![VmdIkEntry {
1927 name_bytes: ik_name_bytes("RightLegIK").to_vec(),
1928 enabled: 0,
1929 name_normalized: b"RightLegIK".to_vec(),
1930 }],
1931 }],
1932 };
1933
1934 let bone_name_to_index: std::collections::HashMap<Vec<u8>, BoneIndex> =
1935 std::collections::HashMap::new();
1936 let morph_name_to_index: std::collections::HashMap<Vec<u8>, MorphIndex> =
1937 std::collections::HashMap::new();
1938 let mut ik_solver_bone_name_to_index: std::collections::HashMap<Vec<u8>, usize> =
1939 std::collections::HashMap::new();
1940 ik_solver_bone_name_to_index.insert(b"RightLegIK".to_vec(), 1);
1941
1942 let clip = build_pair_clip_with_options(
1943 &result,
1944 &bone_name_to_index,
1945 &morph_name_to_index,
1946 &ik_solver_bone_name_to_index,
1947 2,
1948 VmdClipBuildOptions {
1949 honor_property_ik: false,
1950 },
1951 );
1952
1953 assert!(
1954 !clip.has_property_track(),
1955 "build_pair_clip_with_options(honor_property_ik: false) should omit property IK track"
1956 );
1957 }
1958
1959 type SyntheticBoneFrame<'a> = (&'a str, u32, [f32; 3], [f32; 4], [u8; 64]);
1965 type SyntheticMorphFrame<'a> = (&'a str, u32, f32);
1966 type SyntheticPropertyFrame<'a> = (&'a [(&'a str, bool)], u32, bool);
1967
1968 fn make_vmd_bytes(
1969 model_name_ascii: &str,
1970 bone_frames: &[SyntheticBoneFrame<'_>],
1971 morph_frames: &[SyntheticMorphFrame<'_>],
1972 property_frames: &[SyntheticPropertyFrame<'_>],
1973 ) -> Vec<u8> {
1974 let mut v = Vec::new();
1975 v.extend_from_slice(&VMD_MAGIC);
1976
1977 let mut name_buf = [0u8; 20];
1978 let nb = model_name_ascii.as_bytes();
1979 name_buf[..nb.len().min(20)].copy_from_slice(&nb[..nb.len().min(20)]);
1980 v.extend_from_slice(&name_buf);
1981
1982 v.extend_from_slice(&(bone_frames.len() as u32).to_le_bytes());
1983 for (name, frame, trans, rot, interp) in bone_frames {
1984 let mut nb = [0u8; 15];
1985 let b = name.as_bytes();
1986 nb[..b.len().min(15)].copy_from_slice(&b[..b.len().min(15)]);
1987 v.extend_from_slice(&nb);
1988 v.extend_from_slice(&frame.to_le_bytes());
1989 for &x in trans {
1990 v.extend_from_slice(&x.to_le_bytes());
1991 }
1992 for &x in rot {
1993 v.extend_from_slice(&x.to_le_bytes());
1994 }
1995 v.extend_from_slice(interp);
1996 }
1997
1998 v.extend_from_slice(&(morph_frames.len() as u32).to_le_bytes());
1999 for (name, frame, weight) in morph_frames {
2000 let mut nb = [0u8; 15];
2001 let b = name.as_bytes();
2002 nb[..b.len().min(15)].copy_from_slice(&b[..b.len().min(15)]);
2003 v.extend_from_slice(&nb);
2004 v.extend_from_slice(&frame.to_le_bytes());
2005 v.extend_from_slice(&weight.to_le_bytes());
2006 }
2007
2008 v.extend_from_slice(&0u32.to_le_bytes());
2010 v.extend_from_slice(&0u32.to_le_bytes());
2011 v.extend_from_slice(&0u32.to_le_bytes());
2012
2013 v.extend_from_slice(&(property_frames.len() as u32).to_le_bytes());
2014 for (ik_states, frame, visible) in property_frames {
2015 v.extend_from_slice(&frame.to_le_bytes());
2016 v.push(u8::from(*visible));
2017 v.extend_from_slice(&(ik_states.len() as u32).to_le_bytes());
2018 for (ik_name, enabled) in *ik_states {
2019 let mut nb = [0u8; 20];
2020 let b = ik_name.as_bytes();
2021 nb[..b.len().min(20)].copy_from_slice(&b[..b.len().min(20)]);
2022 v.extend_from_slice(&nb);
2023 v.push(u8::from(*enabled));
2024 }
2025 }
2026 v
2027 }
2028
2029 fn json_keys(value: &serde_json::Value) -> Vec<String> {
2030 let mut keys = value
2031 .as_object()
2032 .unwrap()
2033 .keys()
2034 .map(ToOwned::to_owned)
2035 .collect::<Vec<_>>();
2036 keys.sort();
2037 keys
2038 }
2039
2040 #[test]
2041 fn vmd_animation_json_top_level_schema_is_stable() {
2042 let vmd = make_vmd_bytes("miku", &[], &[], &[]);
2043 let parsed = parse_vmd_animation(&vmd).unwrap();
2044 let keys = json_keys(&serde_json::to_value(&parsed).unwrap());
2045
2046 assert_eq!(
2047 keys,
2048 vec![
2049 "boneFrames",
2050 "cameraFrames",
2051 "kind",
2052 "lightFrames",
2053 "metadata",
2054 "morphFrames",
2055 "propertyFrames",
2056 "selfShadowFrames",
2057 ]
2058 );
2059 }
2060
2061 #[test]
2062 fn roundtrip_bone_frame_parse_export_parse() {
2063 let interp = [20u8; 64];
2064 let vmd = make_vmd_bytes(
2065 "miku",
2066 &[("arm", 30, [1.0, 0.5, -0.5], [0.0, 0.0, 0.0, 1.0], interp)],
2067 &[],
2068 &[],
2069 );
2070 let parsed = parse_vmd_animation(&vmd).unwrap();
2071 assert_eq!(parsed.bone_frames.len(), 1);
2072 assert_eq!(parsed.bone_frames[0].bone_name, "arm");
2073 assert_eq!(parsed.bone_frames[0].frame, 30);
2074 assert_eq!(parsed.bone_frames[0].translation, [1.0, 0.5, -0.5]);
2075 assert_eq!(parsed.bone_frames[0].rotation, [0.0, 0.0, 0.0, 1.0]);
2076 assert_eq!(parsed.bone_frames[0].interpolation, vec![20u8; 64]);
2077
2078 let exported = export_vmd_animation(&parsed);
2079 let reparsed = parse_vmd_animation(&exported).unwrap();
2080 assert_eq!(reparsed.metadata.model_name, parsed.metadata.model_name);
2081 assert_eq!(reparsed.metadata.max_frame, 30);
2082 assert_eq!(reparsed.bone_frames.len(), 1);
2083 assert_eq!(reparsed.bone_frames[0].bone_name, "arm");
2084 assert_eq!(reparsed.bone_frames[0].frame, 30);
2085 assert_eq!(reparsed.bone_frames[0].translation, [1.0, 0.5, -0.5]);
2086 assert_eq!(reparsed.bone_frames[0].rotation, [0.0, 0.0, 0.0, 1.0]);
2087 assert_eq!(reparsed.bone_frames[0].interpolation, vec![20u8; 64]);
2088 }
2089
2090 #[test]
2091 fn roundtrip_morph_frame_parse_export_parse() {
2092 let vmd = make_vmd_bytes("miku", &[], &[("blink", 15, 0.75)], &[]);
2093 let parsed = parse_vmd_animation(&vmd).unwrap();
2094 let exported = export_vmd_animation(&parsed);
2095 let reparsed = parse_vmd_animation(&exported).unwrap();
2096 assert_eq!(reparsed.morph_frames.len(), 1);
2097 assert_eq!(reparsed.morph_frames[0].morph_name, "blink");
2098 assert_eq!(reparsed.morph_frames[0].frame, 15);
2099 assert!((reparsed.morph_frames[0].weight - 0.75f32).abs() < 1e-6);
2100 }
2101
2102 #[test]
2103 fn roundtrip_property_ik_frames_parse_export_parse() {
2104 let vmd = make_vmd_bytes(
2105 "camera",
2106 &[],
2107 &[],
2108 &[(&[("leftIK", true), ("rightIK", false)], 20, true)],
2109 );
2110 let parsed = parse_vmd_animation(&vmd).unwrap();
2111 let exported = export_vmd_animation(&parsed);
2112 let reparsed = parse_vmd_animation(&exported).unwrap();
2113 assert_eq!(reparsed.property_frames.len(), 1);
2114 assert_eq!(reparsed.property_frames[0].frame, 20);
2115 assert!(reparsed.property_frames[0].visible);
2116 assert_eq!(reparsed.property_frames[0].ik_states.len(), 2);
2117 assert_eq!(reparsed.property_frames[0].ik_states[0].bone_name, "leftIK");
2118 assert!(reparsed.property_frames[0].ik_states[0].enabled);
2119 assert_eq!(
2120 reparsed.property_frames[0].ik_states[1].bone_name,
2121 "rightIK"
2122 );
2123 assert!(!reparsed.property_frames[0].ik_states[1].enabled);
2124 }
2125
2126 #[test]
2127 fn roundtrip_json_dto_bone_and_morph() {
2128 let interp = [20u8; 64];
2129 let vmd = make_vmd_bytes(
2130 "miku",
2131 &[("spine", 60, [0.0, 1.0, 0.0], [0.1, 0.2, 0.3, 0.9], interp)],
2132 &[("mouth", 30, 0.8)],
2133 &[],
2134 );
2135 let parsed = parse_vmd_animation(&vmd).unwrap();
2136 let json = serde_json::to_string(&parsed).unwrap();
2137 let from_json: VmdParsedAnimation = serde_json::from_str(&json).unwrap();
2138 let exported = export_vmd_animation(&from_json);
2139 let reparsed = parse_vmd_animation(&exported).unwrap();
2140
2141 assert_eq!(
2142 reparsed.bone_frames[0].bone_name,
2143 parsed.bone_frames[0].bone_name
2144 );
2145 assert_eq!(reparsed.bone_frames[0].frame, parsed.bone_frames[0].frame);
2146 assert_eq!(
2147 reparsed.bone_frames[0].translation,
2148 parsed.bone_frames[0].translation
2149 );
2150 assert_eq!(
2151 reparsed.bone_frames[0].rotation,
2152 parsed.bone_frames[0].rotation
2153 );
2154 assert_eq!(
2155 reparsed.morph_frames[0].morph_name,
2156 parsed.morph_frames[0].morph_name
2157 );
2158 assert_eq!(reparsed.morph_frames[0].frame, parsed.morph_frames[0].frame);
2159 assert!((reparsed.morph_frames[0].weight - parsed.morph_frames[0].weight).abs() < 1e-6);
2160 }
2161
2162 fn expected_sjis_name_bytes(value: &str, len: usize) -> Vec<u8> {
2163 let (encoded, _, _) = encoding_rs::SHIFT_JIS.encode(value);
2164 encoded.as_ref()[..encoded.len().min(len)].to_vec()
2165 }
2166
2167 #[test]
2168 fn export_json_dto_encodes_shift_jis_when_raw_name_bytes_are_missing() {
2169 let animation = vmd_parsed_animation(
2170 "初音ミク".to_owned(),
2171 Vec::new(),
2172 30,
2173 VmdParsedSections {
2174 bone_frames: vec![VmdParsedBoneFrame {
2175 bone_name: "左足".to_owned(),
2176 bone_name_bytes: Vec::new(),
2177 frame: 10,
2178 translation: [1.0, 2.0, 3.0],
2179 rotation: [0.0, 0.0, 0.0, 1.0],
2180 interpolation: vec![20; 64],
2181 }],
2182 morph_frames: vec![VmdParsedMorphFrame {
2183 morph_name: "笑い".to_owned(),
2184 morph_name_bytes: Vec::new(),
2185 frame: 20,
2186 weight: 0.75,
2187 }],
2188 camera_frames: Vec::new(),
2189 light_frames: Vec::new(),
2190 self_shadow_frames: Vec::new(),
2191 property_frames: vec![VmdParsedPropertyFrame {
2192 frame: 30,
2193 visible: true,
2194 ik_states: vec![VmdParsedIkState {
2195 bone_name: "右足IK".to_owned(),
2196 bone_name_bytes: Vec::new(),
2197 enabled: false,
2198 }],
2199 }],
2200 },
2201 );
2202 let json = serde_json::to_string(&animation).unwrap();
2203 assert!(!json.contains("modelNameBytes"));
2204 assert!(!json.contains("boneNameBytes"));
2205 assert!(!json.contains("morphNameBytes"));
2206
2207 let from_json: VmdParsedAnimation = serde_json::from_str(&json).unwrap();
2208 let exported = export_vmd_animation(&from_json);
2209 let reparsed = parse_vmd_animation(&exported).unwrap();
2210
2211 assert_eq!(reparsed.metadata.model_name, "初音ミク");
2212 assert!(!reparsed.metadata.model_name_bytes.is_empty());
2213 assert_eq!(
2214 reparsed.metadata.model_name_bytes,
2215 expected_sjis_name_bytes("初音ミク", 20)
2216 );
2217 assert_eq!(reparsed.bone_frames[0].bone_name, "左足");
2218 assert!(!reparsed.bone_frames[0].bone_name_bytes.is_empty());
2219 assert_eq!(
2220 reparsed.bone_frames[0].bone_name_bytes,
2221 expected_sjis_name_bytes("左足", 15)
2222 );
2223 assert_eq!(reparsed.morph_frames[0].morph_name, "笑い");
2224 assert!(!reparsed.morph_frames[0].morph_name_bytes.is_empty());
2225 assert_eq!(
2226 reparsed.morph_frames[0].morph_name_bytes,
2227 expected_sjis_name_bytes("笑い", 15)
2228 );
2229 assert_eq!(reparsed.property_frames[0].ik_states[0].bone_name, "右足IK");
2230 assert!(
2231 !reparsed.property_frames[0].ik_states[0]
2232 .bone_name_bytes
2233 .is_empty()
2234 );
2235 assert_eq!(
2236 reparsed.property_frames[0].ik_states[0].bone_name_bytes,
2237 expected_sjis_name_bytes("右足IK", 20)
2238 );
2239 assert!(!reparsed.property_frames[0].ik_states[0].enabled);
2240 }
2241
2242 #[test]
2243 fn export_json_dto_uses_encoding_rs_replacement_for_non_shift_jis_names() {
2244 let animation = vmd_parsed_animation(
2245 "miku".to_owned(),
2246 Vec::new(),
2247 1,
2248 VmdParsedSections {
2249 bone_frames: vec![VmdParsedBoneFrame {
2250 bone_name: "左足🧪".to_owned(),
2251 bone_name_bytes: Vec::new(),
2252 frame: 1,
2253 translation: [0.0, 0.0, 0.0],
2254 rotation: [0.0, 0.0, 0.0, 1.0],
2255 interpolation: vec![20; 64],
2256 }],
2257 morph_frames: Vec::new(),
2258 camera_frames: Vec::new(),
2259 light_frames: Vec::new(),
2260 self_shadow_frames: Vec::new(),
2261 property_frames: Vec::new(),
2262 },
2263 );
2264 let json = serde_json::to_string(&animation).unwrap();
2265 let from_json: VmdParsedAnimation = serde_json::from_str(&json).unwrap();
2266 let exported = export_vmd_animation(&from_json);
2267 let reparsed = parse_vmd_animation(&exported).unwrap();
2268
2269 assert_eq!(
2270 reparsed.bone_frames[0].bone_name_bytes,
2271 expected_sjis_name_bytes("左足🧪", 15)
2272 );
2273 assert_eq!(
2274 reparsed.bone_frames[0].bone_name,
2275 encoding_rs::SHIFT_JIS
2276 .decode(&expected_sjis_name_bytes("左足🧪", 15))
2277 .0
2278 .trim()
2279 );
2280 }
2281}