1use glam::{Quat, Vec3A};
2use serde::{Deserialize, Serialize};
3
4use mmd_anim_runtime::{
5 AnimationClip, BoneAnimationBinding, BoneIndex, InterpolationScalar, InterpolationVector3,
6 ModelArena, MorphAnimationBinding, MorphIndex, MorphKeyframe, MorphTrack, MovableBoneKeyframe,
7 MovableBoneTrack, PropertyAnimationBinding, PropertyKeyframe,
8};
9
10use crate::binary::{
11 ByteReader, write_f32_le as write_f32, write_fixed_bytes, write_u32_le as write_u32,
12};
13use crate::error::ImportError;
14use crate::normalize::normalize_vmd_name;
15use crate::sjis::{decode_sjis_fixed_trimmed, encode_sjis};
16use thiserror::Error;
17
18#[cfg(test)]
19use mmd_anim_runtime::BoneInit;
20
21mod reduced;
22pub use reduced::{
23 VmdExportMorphKind, VmdExportName, VmdPoseExport, VmdPoseExportBindings, VmdPoseExportError,
24 VmdPoseExportReport, export_reduced_pose_to_vmd,
25};
26
27type Reader<'a> = ByteReader<'a>;
28
29const VMD_MAGIC: [u8; 30] = *b"Vocaloid Motion Data 0002\0\0\0\0\0";
30const VMD_MAGIC_PREFIX: &[u8] = b"Vocaloid Motion Data 0002\0";
31
32impl<'a> ByteReader<'a> {
33 fn read_optional_u32_le(&mut self) -> Result<Option<u32>, ImportError> {
34 if self.remaining() == 0 {
35 Ok(None)
36 } else {
37 self.read_u32_le().map(Some)
38 }
39 }
40
41 fn read_record_count(&mut self, record_size: usize) -> Result<usize, ImportError> {
42 let count = self.read_u32_le()? as usize;
43 self.require_record_bytes(count, record_size)?;
44 Ok(count)
45 }
46
47 fn skip_optional_ignored_records(&mut self, record_size: usize) -> Result<bool, ImportError> {
48 let Some(count) = self.read_optional_u32_le()? else {
49 return Ok(false);
50 };
51 let Some(bytes) = (count as usize).checked_mul(record_size) else {
52 self.pos = self.data.len();
53 return Ok(false);
54 };
55 if bytes > self.remaining() {
56 self.pos = self.data.len();
57 return Ok(false);
58 }
59 self.read_slice(bytes)?;
60 Ok(true)
61 }
62
63 fn read_optional_record_count(
64 &mut self,
65 record_size: usize,
66 ) -> Result<Option<usize>, ImportError> {
67 let Some(count) = self.read_optional_u32_le()? else {
68 return Ok(None);
69 };
70 let count = count as usize;
71 let Some(bytes) = count.checked_mul(record_size) else {
72 self.pos = self.data.len();
73 return Ok(None);
74 };
75 if bytes > self.remaining() {
76 self.pos = self.data.len();
77 return Ok(None);
78 }
79 Ok(Some(count))
80 }
81
82 fn read_quat(&mut self) -> Result<Quat, ImportError> {
83 Ok(Quat::from_xyzw(
84 self.read_f32_le()?,
85 self.read_f32_le()?,
86 self.read_f32_le()?,
87 self.read_f32_le()?,
88 ))
89 }
90
91 fn read_shifts_jis_name(&mut self) -> Result<Vec<u8>, ImportError> {
92 let raw = self.read_slice(15)?;
93 let end = raw.iter().position(|&b| b == 0).unwrap_or(raw.len());
94 Ok(raw[..end].to_vec())
95 }
96}
97
98#[derive(Debug, Clone)]
99pub struct VmdHeader {
100 pub model_name_bytes: [u8; 20],
101}
102
103#[derive(Debug, Clone)]
104pub enum VmdBoneImportMode {
105 ByName(Vec<u8>),
106 ByIndex(u32),
107}
108
109#[derive(Debug, Clone)]
110pub struct VmdBoneKeyframeRaw {
111 pub bone_mode: VmdBoneImportMode,
112 pub frame: u32,
113 pub position: Vec3A,
114 pub rotation: Quat,
115 pub interpolation: [u8; 64],
116 pub bone_name_normalized: Vec<u8>,
117}
118
119#[derive(Debug, Clone)]
120pub struct VmdIkEntry {
121 pub name_bytes: Vec<u8>,
122 pub enabled: u8,
123 pub name_normalized: Vec<u8>,
124}
125
126#[derive(Debug, Clone)]
127pub struct VmdPropertyIkFrame {
128 pub frame: u32,
129 pub show: u8,
130 pub entries: Vec<VmdIkEntry>,
131}
132
133#[derive(Debug, Clone)]
134pub struct VmdImportResult {
135 pub bone_keyframes: Vec<VmdBoneKeyframeRaw>,
136 pub morph_keyframes: Vec<(Vec<u8>, u32, f32)>,
137 pub property_keyframes: Vec<PropertyKeyframe>,
138 pub property_ik_frames: Vec<VmdPropertyIkFrame>,
139}
140
141pub fn read_header(data: &[u8]) -> Result<(VmdHeader, usize), ImportError> {
142 let mut r = Reader::new(data);
143
144 let magic = r.read_slice(30)?;
145 if magic != VMD_MAGIC && !magic.starts_with(VMD_MAGIC_PREFIX) {
146 return Err(ImportError::InvalidVmdMagic);
147 }
148
149 let model_name_bytes: [u8; 20] = r
150 .read_slice(20)?
151 .try_into()
152 .map_err(|_| ImportError::InvalidVmdModelName)?;
153
154 Ok((VmdHeader { model_name_bytes }, r.pos))
155}
156
157pub fn import_vmd_motion(data: &[u8]) -> Result<VmdImportResult, ImportError> {
158 let (_header, pos) = read_header(data)?;
159 let mut r = Reader { data, pos };
160
161 let bone_count = r.read_record_count(111)?;
162 let mut bone_keyframes = Vec::with_capacity(bone_count);
163 for _ in 0..bone_count {
164 let bone_name = r.read_shifts_jis_name()?;
165 let frame = r.read_u32_le()?;
166 let position = r.read_vec3()?;
167 let rotation = r.read_quat()?;
168 let interpolation: [u8; 64] = r.read_slice(64)?.try_into().unwrap();
169
170 let bone_name_normalized = normalize_vmd_name(&bone_name);
171
172 bone_keyframes.push(VmdBoneKeyframeRaw {
173 bone_mode: VmdBoneImportMode::ByName(bone_name),
174 frame,
175 position,
176 rotation,
177 interpolation,
178 bone_name_normalized,
179 });
180 }
181
182 let Some(morph_count) = r.read_optional_u32_le()? else {
183 return Ok(VmdImportResult {
184 bone_keyframes,
185 morph_keyframes: Vec::new(),
186 property_keyframes: Vec::new(),
187 property_ik_frames: Vec::new(),
188 });
189 };
190 let morph_count = morph_count as usize;
191 r.require_record_bytes(morph_count, 23)?;
192 let mut morph_keyframes = Vec::with_capacity(morph_count);
193 for _ in 0..morph_count {
194 let morph_name = r.read_shifts_jis_name()?;
195 let frame = r.read_u32_le()?;
196 let weight = r.read_f32_le()?;
197 morph_keyframes.push((morph_name, frame, weight));
198 }
199
200 if !r.skip_optional_ignored_records(61)? {
201 return Ok(VmdImportResult {
202 bone_keyframes,
203 morph_keyframes,
204 property_keyframes: Vec::new(),
205 property_ik_frames: Vec::new(),
206 });
207 }
208
209 if !r.skip_optional_ignored_records(28)? {
210 return Ok(VmdImportResult {
211 bone_keyframes,
212 morph_keyframes,
213 property_keyframes: Vec::new(),
214 property_ik_frames: Vec::new(),
215 });
216 }
217
218 if !r.skip_optional_ignored_records(9)? {
219 return Ok(VmdImportResult {
220 bone_keyframes,
221 morph_keyframes,
222 property_keyframes: Vec::new(),
223 property_ik_frames: Vec::new(),
224 });
225 }
226
227 let Some(show_ik_count) = r.read_optional_u32_le()? else {
228 return Ok(VmdImportResult {
229 bone_keyframes,
230 morph_keyframes,
231 property_keyframes: Vec::new(),
232 property_ik_frames: Vec::new(),
233 });
234 };
235 let show_ik_count = show_ik_count as usize;
236 r.require_record_bytes(show_ik_count, 9)?;
237 let mut property_keyframes = Vec::with_capacity(show_ik_count);
238 let mut property_ik_frames = Vec::with_capacity(show_ik_count);
239 for _ in 0..show_ik_count {
240 let frame = r.read_u32_le()?;
241 let show = r.read_u8()?;
242 let ik_count = r.read_u32_le()? as usize;
243 r.require_record_bytes(ik_count, 21)?;
244 let mut ik_enabled = Vec::with_capacity(ik_count);
245 let mut ik_entries = Vec::with_capacity(ik_count);
246 for _ in 0..ik_count {
247 let ik_name_bytes = r.read_slice(20)?.to_vec();
248 let enabled = r.read_u8()?;
249 let end = ik_name_bytes
250 .iter()
251 .position(|&b| b == 0)
252 .unwrap_or(ik_name_bytes.len());
253 let name_normalized = normalize_vmd_name(&ik_name_bytes[..end]);
254 ik_enabled.push(enabled);
255 ik_entries.push(VmdIkEntry {
256 name_bytes: ik_name_bytes,
257 enabled,
258 name_normalized,
259 });
260 }
261 property_keyframes.push(PropertyKeyframe::new(
262 frame,
263 ik_enabled.iter().map(|&b| b != 0).collect(),
264 ));
265 property_ik_frames.push(VmdPropertyIkFrame {
266 frame,
267 show,
268 entries: ik_entries,
269 });
270 }
271
272 Ok(VmdImportResult {
273 bone_keyframes,
274 morph_keyframes,
275 property_keyframes,
276 property_ik_frames,
277 })
278}
279
280#[derive(Debug, Clone, Serialize, Deserialize)]
281#[serde(rename_all = "camelCase")]
282pub struct VmdParsedAnimation {
283 #[serde(default = "default_vmd_kind", skip_deserializing)]
284 pub kind: &'static str,
285 pub metadata: VmdParsedMetadata,
286 pub bone_frames: Vec<VmdParsedBoneFrame>,
287 pub morph_frames: Vec<VmdParsedMorphFrame>,
288 pub camera_frames: Vec<VmdParsedCameraFrame>,
289 pub light_frames: Vec<VmdParsedLightFrame>,
290 pub self_shadow_frames: Vec<VmdParsedSelfShadowFrame>,
291 pub property_frames: Vec<VmdParsedPropertyFrame>,
292}
293
294#[derive(Debug, Clone, Serialize, Deserialize)]
295#[serde(rename_all = "camelCase")]
296pub struct VmdParsedMetadata {
297 #[serde(default = "default_vmd_format", skip_deserializing)]
298 pub format: &'static str,
299 pub model_name: String,
300 #[serde(default, skip_serializing_if = "Vec::is_empty")]
301 pub model_name_bytes: Vec<u8>,
302 pub counts: VmdParsedCounts,
303 pub max_frame: u32,
304}
305
306#[derive(Debug, Clone, Serialize, Deserialize)]
307pub struct VmdParsedCounts {
308 pub bones: usize,
309 pub morphs: usize,
310 pub cameras: usize,
311 pub lights: usize,
312 #[serde(rename = "selfShadows")]
313 pub self_shadows: usize,
314 pub properties: usize,
315}
316
317#[derive(Debug, Clone, Serialize, Deserialize)]
318#[serde(rename_all = "camelCase")]
319pub struct VmdParsedBoneFrame {
320 pub bone_name: String,
321 #[serde(default, skip_serializing_if = "Vec::is_empty")]
322 pub bone_name_bytes: Vec<u8>,
323 pub frame: u32,
324 pub translation: [f32; 3],
325 pub rotation: [f32; 4],
326 pub interpolation: Vec<u8>,
327}
328
329#[derive(Debug, Clone, Serialize, Deserialize)]
330#[serde(rename_all = "camelCase")]
331pub struct VmdParsedMorphFrame {
332 pub morph_name: String,
333 #[serde(default, skip_serializing_if = "Vec::is_empty")]
334 pub morph_name_bytes: Vec<u8>,
335 pub frame: u32,
336 pub weight: f32,
337}
338
339#[derive(Debug, Clone, Serialize, Deserialize)]
340#[serde(rename_all = "camelCase")]
341pub struct VmdParsedCameraFrame {
342 pub frame: u32,
343 pub distance: f32,
344 pub position: [f32; 3],
345 pub rotation: [f32; 3],
346 pub interpolation: [u8; 24],
347 pub fov: u32,
348 pub perspective: bool,
349}
350
351#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
352#[serde(rename_all = "camelCase")]
353pub struct VmdCameraState {
354 pub distance: f32,
355 pub position: [f32; 3],
356 pub rotation: [f32; 3],
357 pub fov: f32,
358 pub perspective: bool,
359}
360
361#[derive(Debug, Clone, Serialize, Deserialize)]
362#[serde(rename_all = "camelCase")]
363pub struct VmdParsedLightFrame {
364 pub frame: u32,
365 pub color: [f32; 3],
366 pub direction: [f32; 3],
367}
368
369#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
370#[serde(rename_all = "camelCase")]
371pub struct VmdLightState {
372 pub color: [f32; 3],
373 pub direction: [f32; 3],
374}
375
376#[derive(Debug, Clone, Serialize, Deserialize)]
377#[serde(rename_all = "camelCase")]
378pub struct VmdParsedSelfShadowFrame {
379 pub frame: u32,
380 pub mode: u8,
381 pub distance: f32,
382}
383
384#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
385#[serde(rename_all = "camelCase")]
386pub struct VmdSelfShadowState {
387 pub mode: u8,
388 pub distance: f32,
389}
390
391#[derive(Debug, Clone, Serialize, Deserialize)]
392#[serde(rename_all = "camelCase")]
393pub struct VmdParsedPropertyFrame {
394 pub frame: u32,
395 pub visible: bool,
396 pub ik_states: Vec<VmdParsedIkState>,
397}
398
399#[derive(Debug, Clone, Serialize, Deserialize)]
400#[serde(rename_all = "camelCase")]
401pub struct VmdParsedIkState {
402 pub bone_name: String,
403 #[serde(default, skip_serializing_if = "Vec::is_empty")]
404 pub bone_name_bytes: Vec<u8>,
405 pub enabled: bool,
406}
407
408pub fn parse_vmd_animation(data: &[u8]) -> Result<VmdParsedAnimation, ImportError> {
409 let (_header, pos) = read_header(data)?;
410 let mut r = Reader { data, pos };
411 let model_name = decode_sjis_fixed(&_header.model_name_bytes);
412 let model_name_bytes = trim_fixed_bytes(&_header.model_name_bytes).to_vec();
413 let mut max_frame = 0u32;
414
415 let bone_count = r.read_record_count(111)?;
416 let mut bone_frames = Vec::with_capacity(bone_count);
417 for _ in 0..bone_count {
418 let bone_name_bytes = r.read_slice(15)?;
419 let bone_name = decode_sjis_fixed(bone_name_bytes);
420 let frame = r.read_u32_le()?;
421 max_frame = max_frame.max(frame);
422 let position = r.read_vec3()?;
423 let rotation = r.read_quat()?;
424 let interpolation: [u8; 64] = r.read_slice(64)?.try_into().unwrap();
425 bone_frames.push(VmdParsedBoneFrame {
426 bone_name,
427 bone_name_bytes: trim_fixed_bytes(bone_name_bytes).to_vec(),
428 frame,
429 translation: [position.x, position.y, position.z],
430 rotation: [rotation.x, rotation.y, rotation.z, rotation.w],
431 interpolation: interpolation.to_vec(),
432 });
433 }
434
435 let Some(morph_count) = r.read_optional_u32_le()? else {
436 return Ok(vmd_parsed_animation(
437 model_name,
438 model_name_bytes,
439 max_frame,
440 VmdParsedSections {
441 bone_frames,
442 morph_frames: Vec::new(),
443 camera_frames: Vec::new(),
444 light_frames: Vec::new(),
445 self_shadow_frames: Vec::new(),
446 property_frames: Vec::new(),
447 },
448 ));
449 };
450 let morph_count = morph_count as usize;
451 r.require_record_bytes(morph_count, 23)?;
452 let mut morph_frames = Vec::with_capacity(morph_count);
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);
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);
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);
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 count = count as usize;
692 r.require_record_bytes(count, 9)?;
693 let mut frames = Vec::with_capacity(count);
694 for _ in 0..count {
695 let frame = r.read_u32_le()?;
696 *max_frame = (*max_frame).max(frame);
697 let visible = r.read_u8()? != 0;
698 let ik_count = r.read_u32_le()? as usize;
699 r.require_record_bytes(ik_count, 21)?;
700 let mut ik_states = Vec::with_capacity(ik_count);
701 for _ in 0..ik_count {
702 let bone_name_bytes = r.read_slice(20)?;
703 ik_states.push(VmdParsedIkState {
704 bone_name: decode_sjis_fixed(bone_name_bytes),
705 bone_name_bytes: trim_fixed_bytes(bone_name_bytes).to_vec(),
706 enabled: r.read_u8()? != 0,
707 });
708 }
709 frames.push(VmdParsedPropertyFrame {
710 frame,
711 visible,
712 ik_states,
713 });
714 }
715 Ok(frames)
716}
717
718pub fn sample_vmd_camera_frames(
719 frames: &[VmdParsedCameraFrame],
720 frame: f32,
721) -> Option<VmdCameraState> {
722 if frames.is_empty() {
723 return None;
724 }
725
726 let mut sorted: Vec<&VmdParsedCameraFrame> = frames.iter().collect();
727 sorted.sort_by_key(|keyframe| keyframe.frame);
728
729 let mut index = 0usize;
730 while index + 1 < sorted.len() && sorted[index + 1].frame as f32 <= frame {
731 index += 1;
732 }
733
734 let previous = sorted[index];
735 let next = sorted.get(index + 1).copied().unwrap_or(previous);
736 let t = interpolation_ratio(previous.frame, next.frame, frame);
737 let interpolation = decode_camera_interpolation(&next.interpolation);
738
739 let distance_t = interpolation.distance.evaluate(t);
740 let position_x_t = interpolation.position.x.evaluate(t);
741 let position_y_t = interpolation.position.y.evaluate(t);
742 let position_z_t = interpolation.position.z.evaluate(t);
743 let rotation_t = interpolation.rotation.evaluate(t);
744 let fov_t = interpolation.fov.evaluate(t);
745
746 Some(VmdCameraState {
747 distance: lerp(previous.distance, next.distance, distance_t),
748 position: [
749 lerp(previous.position[0], next.position[0], position_x_t),
750 lerp(previous.position[1], next.position[1], position_y_t),
751 lerp(previous.position[2], next.position[2], position_z_t),
752 ],
753 rotation: [
754 lerp(previous.rotation[0], next.rotation[0], rotation_t),
755 lerp(previous.rotation[1], next.rotation[1], rotation_t),
756 lerp(previous.rotation[2], next.rotation[2], rotation_t),
757 ],
758 fov: lerp(previous.fov as f32, next.fov as f32, fov_t),
759 perspective: if t < 1.0 {
760 previous.perspective
761 } else {
762 next.perspective
763 },
764 })
765}
766
767pub fn sample_vmd_light_frames(
768 frames: &[VmdParsedLightFrame],
769 frame: f32,
770) -> Option<VmdLightState> {
771 if frames.is_empty() {
772 return None;
773 }
774
775 let mut sorted: Vec<&VmdParsedLightFrame> = frames.iter().collect();
776 sorted.sort_by_key(|keyframe| keyframe.frame);
777
778 let mut index = 0usize;
779 while index + 1 < sorted.len() && sorted[index + 1].frame as f32 <= frame {
780 index += 1;
781 }
782
783 let previous = sorted[index];
784 let next = sorted.get(index + 1).copied().unwrap_or(previous);
785 let t = interpolation_ratio(previous.frame, next.frame, frame);
786
787 Some(VmdLightState {
788 color: [
789 lerp(previous.color[0], next.color[0], t),
790 lerp(previous.color[1], next.color[1], t),
791 lerp(previous.color[2], next.color[2], t),
792 ],
793 direction: [
794 lerp(previous.direction[0], next.direction[0], t),
795 lerp(previous.direction[1], next.direction[1], t),
796 lerp(previous.direction[2], next.direction[2], t),
797 ],
798 })
799}
800
801pub fn sample_vmd_self_shadow_frames(
802 frames: &[VmdParsedSelfShadowFrame],
803 frame: f32,
804) -> Option<VmdSelfShadowState> {
805 if frames.is_empty() {
806 return None;
807 }
808
809 let mut sorted: Vec<&VmdParsedSelfShadowFrame> = frames.iter().collect();
810 sorted.sort_by_key(|keyframe| keyframe.frame);
811
812 let mut index = 0usize;
813 while index + 1 < sorted.len() && sorted[index + 1].frame as f32 <= frame {
814 index += 1;
815 }
816
817 let previous = sorted[index];
818 let next = sorted.get(index + 1).copied().unwrap_or(previous);
819 let t = interpolation_ratio(previous.frame, next.frame, frame);
820
821 Some(VmdSelfShadowState {
822 mode: if t < 1.0 { previous.mode } else { next.mode },
823 distance: lerp(previous.distance, next.distance, t),
824 })
825}
826
827struct CameraInterpolation {
828 position: InterpolationVector3,
829 rotation: InterpolationScalar,
830 distance: InterpolationScalar,
831 fov: InterpolationScalar,
832}
833
834fn decode_camera_interpolation(interpolation: &[u8; 24]) -> CameraInterpolation {
835 CameraInterpolation {
836 position: InterpolationVector3 {
837 x: decode_camera_interpolation_scalar(interpolation, 0),
838 y: decode_camera_interpolation_scalar(interpolation, 1),
839 z: decode_camera_interpolation_scalar(interpolation, 2),
840 },
841 rotation: decode_camera_interpolation_scalar(interpolation, 3),
842 distance: decode_camera_interpolation_scalar(interpolation, 4),
843 fov: decode_camera_interpolation_scalar(interpolation, 5),
844 }
845}
846
847fn decode_camera_interpolation_scalar(
848 interpolation: &[u8; 24],
849 channel: usize,
850) -> InterpolationScalar {
851 let offset = channel * 4;
852 decode_interpolation_scalar([
853 interpolation[offset],
854 interpolation[offset + 1],
855 interpolation[offset + 2],
856 interpolation[offset + 3],
857 ])
858}
859
860fn interpolation_ratio(previous_frame: u32, next_frame: u32, frame: f32) -> f32 {
861 if next_frame <= previous_frame {
862 return 0.0;
863 }
864 let span = next_frame - previous_frame;
865 if span <= 1 {
866 return if frame >= next_frame as f32 { 1.0 } else { 0.0 };
867 }
868 ((frame - previous_frame as f32) / span as f32).clamp(0.0, 1.0)
869}
870
871fn lerp(a: f32, b: f32, t: f32) -> f32 {
872 a + (b - a) * t
873}
874
875fn decode_sjis_fixed(bytes: &[u8]) -> String {
876 decode_sjis_fixed_trimmed(bytes)
877}
878
879fn trim_fixed_bytes(bytes: &[u8]) -> &[u8] {
880 let end = bytes.iter().position(|&b| b == 0).unwrap_or(bytes.len());
881 &bytes[..end]
882}
883
884fn write_fixed_name_bytes(out: &mut Vec<u8>, value: &str, raw_bytes: &[u8], len: usize) {
885 if raw_bytes.is_empty() {
886 let encoded = encode_sjis(value);
887 write_fixed_bytes(out, &encoded, len);
888 } else {
889 write_fixed_bytes(out, raw_bytes, len);
890 }
891}
892
893fn decode_interpolation_scalar(data: [u8; 4]) -> InterpolationScalar {
894 InterpolationScalar {
895 x1: data[0].min(127),
896 y1: data[1].min(127),
897 x2: data[2].min(127),
898 y2: data[3].min(127),
899 }
900}
901
902fn decode_bone_interpolation(
903 interpolation: &[u8; 64],
904) -> (InterpolationVector3, InterpolationScalar) {
905 let position = InterpolationVector3 {
906 x: decode_interpolation_scalar([
907 interpolation[0],
908 interpolation[4],
909 interpolation[8],
910 interpolation[12],
911 ]),
912 y: decode_interpolation_scalar([
913 interpolation[1],
914 interpolation[5],
915 interpolation[9],
916 interpolation[13],
917 ]),
918 z: decode_interpolation_scalar([
919 interpolation[2],
920 interpolation[6],
921 interpolation[10],
922 interpolation[14],
923 ]),
924 };
925 let rotation = decode_interpolation_scalar([
926 interpolation[3],
927 interpolation[7],
928 interpolation[11],
929 interpolation[15],
930 ]);
931 (position, rotation)
932}
933
934fn decode_mmd_registered_bone_interpolation(
938 interpolation: &[u8; 64],
939) -> (InterpolationVector3, InterpolationScalar) {
940 let position = InterpolationVector3 {
941 x: decode_interpolation_scalar([
942 interpolation[0],
943 interpolation[4],
944 interpolation[8],
945 interpolation[12],
946 ]),
947 y: decode_interpolation_scalar([
948 interpolation[16],
949 interpolation[20],
950 interpolation[24],
951 interpolation[28],
952 ]),
953 z: decode_interpolation_scalar([
954 interpolation[32],
955 interpolation[36],
956 interpolation[40],
957 interpolation[44],
958 ]),
959 };
960 let rotation = decode_interpolation_scalar([
961 interpolation[48],
962 interpolation[52],
963 interpolation[56],
964 interpolation[60],
965 ]);
966 (position, rotation)
967}
968
969pub fn build_clip_from_import(
970 result: VmdImportResult,
971 bone_name_to_index: &dyn Fn(&[u8]) -> Option<BoneIndex>,
972 morph_name_to_index: &dyn Fn(&[u8]) -> Option<MorphIndex>,
973) -> AnimationClip {
974 let mut bone_tracks_map: std::collections::BTreeMap<u32, Vec<MovableBoneKeyframe>> =
975 std::collections::BTreeMap::new();
976
977 for kf in result.bone_keyframes {
978 let bone_index = match &kf.bone_mode {
979 VmdBoneImportMode::ByName(_name) => {
980 match bone_name_to_index(&kf.bone_name_normalized) {
981 Some(idx) => idx,
982 None => continue,
983 }
984 }
985 VmdBoneImportMode::ByIndex(idx) => BoneIndex(*idx),
986 };
987
988 let (pos_interp, rot_interp) = decode_bone_interpolation(&kf.interpolation);
989
990 bone_tracks_map
991 .entry(bone_index.0)
992 .or_default()
993 .push(MovableBoneKeyframe {
994 frame: kf.frame,
995 position: kf.position,
996 rotation: kf.rotation,
997 position_interpolation: pos_interp,
998 rotation_interpolation: rot_interp,
999 });
1000 }
1001
1002 let bone_tracks: Vec<BoneAnimationBinding> = bone_tracks_map
1003 .into_iter()
1004 .map(|(bone_idx, kfs)| BoneAnimationBinding {
1005 bone: BoneIndex(bone_idx),
1006 track: MovableBoneTrack::from_keyframes(kfs),
1007 })
1008 .collect();
1009
1010 let mut morph_tracks_map: std::collections::BTreeMap<u32, Vec<MorphKeyframe>> =
1011 std::collections::BTreeMap::new();
1012
1013 for (morph_name, frame, weight) in result.morph_keyframes {
1014 let morph_name_normalized = normalize_vmd_name(&morph_name);
1015 let morph_index = match morph_name_to_index(&morph_name_normalized) {
1016 Some(idx) => idx,
1017 None => continue,
1018 };
1019 morph_tracks_map
1020 .entry(morph_index.0)
1021 .or_default()
1022 .push(MorphKeyframe::new(frame, weight));
1023 }
1024
1025 let morph_tracks: Vec<MorphAnimationBinding> = morph_tracks_map
1026 .into_iter()
1027 .map(|(morph_idx, kfs)| MorphAnimationBinding {
1028 morph: MorphIndex(morph_idx),
1029 track: MorphTrack::from_keyframes(kfs),
1030 })
1031 .collect();
1032
1033 let property_track = if result.property_keyframes.is_empty() {
1034 None
1035 } else {
1036 Some(PropertyAnimationBinding::from_keyframes(
1037 result.property_keyframes,
1038 ))
1039 };
1040
1041 AnimationClip::new_full(bone_tracks, morph_tracks, property_track)
1042}
1043
1044pub fn build_property_binding_with_ik_resolver(
1045 ik_frames: &[VmdPropertyIkFrame],
1046 ik_name_to_solver_index: &dyn Fn(&[u8]) -> Option<usize>,
1047 solver_count: usize,
1048) -> Option<PropertyAnimationBinding> {
1049 if solver_count == 0 || ik_frames.is_empty() {
1050 return None;
1051 }
1052
1053 let keyframes: Vec<PropertyKeyframe> = ik_frames
1054 .iter()
1055 .map(|frame| {
1056 let mut ik_enabled = vec![1u8; solver_count];
1057 for entry in &frame.entries {
1058 match ik_name_to_solver_index(&entry.name_normalized) {
1059 Some(idx) if idx < solver_count => {
1060 ik_enabled[idx] = entry.enabled;
1061 }
1062 _ => {}
1063 }
1064 }
1065 PropertyKeyframe {
1066 frame: frame.frame,
1067 ik_enabled: ik_enabled.into_boxed_slice(),
1068 }
1069 })
1070 .collect();
1071
1072 Some(PropertyAnimationBinding::from_keyframes(keyframes))
1073}
1074
1075#[derive(Debug, Clone, Copy)]
1077pub struct VmdClipBuildOptions {
1078 pub honor_property_ik: bool,
1084}
1085
1086#[derive(Debug, Error, Clone, PartialEq, Eq)]
1088pub enum VmdClipBuildError {
1089 #[error(
1092 "VMD bone map contains bone index {bone_index:?} outside model bone count {bone_count}"
1093 )]
1094 BoneIndexOutOfRange {
1095 bone_index: BoneIndex,
1096 bone_count: usize,
1097 },
1098}
1099
1100impl Default for VmdClipBuildOptions {
1101 fn default() -> Self {
1102 Self {
1103 honor_property_ik: true,
1104 }
1105 }
1106}
1107
1108pub fn build_pair_clip(
1109 result: &VmdImportResult,
1110 bone_name_to_index: &std::collections::HashMap<Vec<u8>, BoneIndex>,
1111 morph_name_to_index: &std::collections::HashMap<Vec<u8>, MorphIndex>,
1112 ik_solver_bone_name_to_index: &std::collections::HashMap<Vec<u8>, usize>,
1113 solver_count: usize,
1114) -> AnimationClip {
1115 build_pair_clip_with_options(
1116 result,
1117 bone_name_to_index,
1118 morph_name_to_index,
1119 ik_solver_bone_name_to_index,
1120 solver_count,
1121 VmdClipBuildOptions::default(),
1122 )
1123}
1124
1125pub fn build_mmd_registered_pair_clip(
1133 model: &ModelArena,
1134 result: &VmdImportResult,
1135 bone_name_to_index: &std::collections::HashMap<Vec<u8>, BoneIndex>,
1136 morph_name_to_index: &std::collections::HashMap<Vec<u8>, MorphIndex>,
1137 ik_solver_bone_name_to_index: &std::collections::HashMap<Vec<u8>, usize>,
1138 solver_count: usize,
1139) -> Result<AnimationClip, VmdClipBuildError> {
1140 build_mmd_registered_pair_clip_with_options(
1141 model,
1142 result,
1143 bone_name_to_index,
1144 morph_name_to_index,
1145 ik_solver_bone_name_to_index,
1146 solver_count,
1147 VmdClipBuildOptions::default(),
1148 )
1149}
1150
1151pub fn build_pair_clip_with_options(
1152 result: &VmdImportResult,
1153 bone_name_to_index: &std::collections::HashMap<Vec<u8>, BoneIndex>,
1154 morph_name_to_index: &std::collections::HashMap<Vec<u8>, MorphIndex>,
1155 ik_solver_bone_name_to_index: &std::collections::HashMap<Vec<u8>, usize>,
1156 solver_count: usize,
1157 options: VmdClipBuildOptions,
1158) -> AnimationClip {
1159 build_pair_clip_with_registration_policy(
1160 result,
1161 bone_name_to_index,
1162 morph_name_to_index,
1163 ik_solver_bone_name_to_index,
1164 solver_count,
1165 options,
1166 VmdRegistrationPolicy::Raw,
1167 )
1168}
1169
1170pub fn build_mmd_registered_pair_clip_with_options(
1173 model: &ModelArena,
1174 result: &VmdImportResult,
1175 bone_name_to_index: &std::collections::HashMap<Vec<u8>, BoneIndex>,
1176 morph_name_to_index: &std::collections::HashMap<Vec<u8>, MorphIndex>,
1177 ik_solver_bone_name_to_index: &std::collections::HashMap<Vec<u8>, usize>,
1178 solver_count: usize,
1179 options: VmdClipBuildOptions,
1180) -> Result<AnimationClip, VmdClipBuildError> {
1181 validate_bone_map(model, bone_name_to_index)?;
1182 Ok(build_pair_clip_with_registration_policy(
1183 result,
1184 bone_name_to_index,
1185 morph_name_to_index,
1186 ik_solver_bone_name_to_index,
1187 solver_count,
1188 options,
1189 VmdRegistrationPolicy::MmdRegistered(model),
1190 ))
1191}
1192
1193#[derive(Clone, Copy)]
1194enum VmdRegistrationPolicy<'a> {
1195 Raw,
1196 MmdRegistered(&'a ModelArena),
1197}
1198
1199fn validate_bone_map(
1200 model: &ModelArena,
1201 bone_name_to_index: &std::collections::HashMap<Vec<u8>, BoneIndex>,
1202) -> Result<(), VmdClipBuildError> {
1203 let bone_count = model.bone_count();
1204 if let Some(&bone_index) = bone_name_to_index
1205 .values()
1206 .find(|bone_index| bone_index.as_usize() >= bone_count)
1207 {
1208 return Err(VmdClipBuildError::BoneIndexOutOfRange {
1209 bone_index,
1210 bone_count,
1211 });
1212 }
1213 Ok(())
1214}
1215
1216fn build_pair_clip_with_registration_policy(
1217 result: &VmdImportResult,
1218 bone_name_to_index: &std::collections::HashMap<Vec<u8>, BoneIndex>,
1219 morph_name_to_index: &std::collections::HashMap<Vec<u8>, MorphIndex>,
1220 ik_solver_bone_name_to_index: &std::collections::HashMap<Vec<u8>, usize>,
1221 solver_count: usize,
1222 options: VmdClipBuildOptions,
1223 registration_policy: VmdRegistrationPolicy<'_>,
1224) -> AnimationClip {
1225 let mut bone_tracks_map: std::collections::BTreeMap<u32, Vec<MovableBoneKeyframe>> =
1226 std::collections::BTreeMap::new();
1227
1228 for kf in &result.bone_keyframes {
1229 let bone_index = match bone_name_to_index.get(&kf.bone_name_normalized) {
1230 Some(idx) => *idx,
1231 None => continue,
1232 };
1233
1234 bone_tracks_map
1235 .entry(bone_index.0)
1236 .or_default()
1237 .push(build_mapped_bone_keyframe(
1238 kf,
1239 bone_index,
1240 registration_policy,
1241 ));
1242 }
1243
1244 let bone_tracks: Vec<BoneAnimationBinding> = bone_tracks_map
1245 .into_iter()
1246 .map(|(bone_idx, kfs)| BoneAnimationBinding {
1247 bone: BoneIndex(bone_idx),
1248 track: MovableBoneTrack::from_keyframes(kfs),
1249 })
1250 .collect();
1251
1252 let mut morph_tracks_map: std::collections::BTreeMap<u32, Vec<MorphKeyframe>> =
1253 std::collections::BTreeMap::new();
1254
1255 for (morph_name, frame, weight) in &result.morph_keyframes {
1256 let morph_name_normalized = normalize_vmd_name(morph_name);
1257 let morph_index = match morph_name_to_index.get(&morph_name_normalized) {
1258 Some(idx) => *idx,
1259 None => continue,
1260 };
1261 morph_tracks_map
1262 .entry(morph_index.0)
1263 .or_default()
1264 .push(MorphKeyframe::new(*frame, *weight));
1265 }
1266
1267 let morph_tracks: Vec<MorphAnimationBinding> = morph_tracks_map
1268 .into_iter()
1269 .map(|(morph_idx, kfs)| MorphAnimationBinding {
1270 morph: MorphIndex(morph_idx),
1271 track: MorphTrack::from_keyframes(kfs),
1272 })
1273 .collect();
1274
1275 let property_track = if options.honor_property_ik {
1276 build_property_binding_with_ik_resolver(
1277 &result.property_ik_frames,
1278 &|name| ik_solver_bone_name_to_index.get(name).copied(),
1279 solver_count,
1280 )
1281 } else {
1282 None
1283 };
1284
1285 AnimationClip::new_full(bone_tracks, morph_tracks, property_track)
1286}
1287
1288fn build_mapped_bone_keyframe(
1289 kf: &VmdBoneKeyframeRaw,
1290 bone_index: BoneIndex,
1291 registration_policy: VmdRegistrationPolicy<'_>,
1292) -> MovableBoneKeyframe {
1293 let (position_interpolation, rotation_interpolation) = match registration_policy {
1294 VmdRegistrationPolicy::Raw => decode_bone_interpolation(&kf.interpolation),
1295 VmdRegistrationPolicy::MmdRegistered(_) => {
1296 decode_mmd_registered_bone_interpolation(&kf.interpolation)
1297 }
1298 };
1299 let rotation = match registration_policy {
1300 VmdRegistrationPolicy::Raw => kf.rotation,
1301 VmdRegistrationPolicy::MmdRegistered(model) => {
1302 model.fixed_axis(bone_index).map_or(kf.rotation, |axis| {
1303 project_rotation_to_fixed_axis(kf.rotation, axis)
1304 })
1305 }
1306 };
1307 MovableBoneKeyframe {
1308 frame: kf.frame,
1309 position: kf.position,
1310 rotation,
1311 position_interpolation,
1312 rotation_interpolation,
1313 }
1314}
1315
1316fn project_rotation_to_fixed_axis(rotation: Quat, axis: Vec3A) -> Quat {
1317 let axis = axis.normalize();
1318 let vector = Vec3A::new(rotation.x, rotation.y, rotation.z);
1319 let sign = if vector.dot(axis) < 0.0 { -1.0 } else { 1.0 };
1320 let projected = axis * vector.length() * sign;
1321 Quat::from_xyzw(projected.x, projected.y, projected.z, rotation.w)
1322}
1323
1324#[cfg(test)]
1325mod tests {
1326 use super::*;
1327 #[allow(unused_imports)]
1328 use serde_json;
1329
1330 fn build_vmd_header_bytes() -> Vec<u8> {
1331 let mut buf = Vec::new();
1332 buf.extend_from_slice(&VMD_MAGIC);
1333 buf.extend_from_slice(&[0u8; 20]);
1334 buf
1335 }
1336
1337 #[test]
1338 fn rejects_impossible_vmd_bone_count_before_allocation() {
1339 let mut buf = build_vmd_header_bytes();
1340 buf.extend_from_slice(&u32::MAX.to_le_bytes());
1341
1342 assert!(matches!(
1343 import_vmd_motion(&buf),
1344 Err(ImportError::UnexpectedEof(_))
1345 ));
1346 assert!(matches!(
1347 parse_vmd_animation(&buf),
1348 Err(ImportError::UnexpectedEof(_))
1349 ));
1350 }
1351
1352 #[test]
1353 fn rejects_impossible_vmd_property_ik_count_before_allocation() {
1354 let mut buf = build_vmd_header_bytes();
1355 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!(
1366 import_vmd_motion(&buf),
1367 Err(ImportError::UnexpectedEof(_))
1368 ));
1369 assert!(matches!(
1370 parse_vmd_animation(&buf),
1371 Err(ImportError::UnexpectedEof(_))
1372 ));
1373 }
1374
1375 #[test]
1376 fn parses_vmd_header() {
1377 let header_bytes = build_vmd_header_bytes();
1378 let (header, pos) = read_header(&header_bytes).unwrap();
1379 assert_eq!(header.model_name_bytes, [0u8; 20]);
1380 assert!(pos > 0);
1381 }
1382
1383 #[test]
1384 fn rejects_bad_vmd_magic() {
1385 let mut buf = build_vmd_header_bytes();
1386 buf[0] = 0xFF;
1387 assert_eq!(read_header(&buf).unwrap_err(), ImportError::InvalidVmdMagic);
1388 }
1389
1390 #[test]
1391 fn accepts_vmd_magic_with_nonzero_padding_bytes() {
1392 let mut buf = build_vmd_header_bytes();
1393 buf[26..30].copy_from_slice(b"JKLM");
1394
1395 let (_header, pos) = read_header(&buf).unwrap();
1396 assert_eq!(pos, 50);
1397 }
1398
1399 #[test]
1400 fn parses_minimal_vmd_motion() {
1401 let mut buf = build_vmd_header_bytes();
1402
1403 buf.extend_from_slice(&0u32.to_le_bytes());
1404 buf.extend_from_slice(&0u32.to_le_bytes());
1405 buf.extend_from_slice(&0u32.to_le_bytes());
1406 buf.extend_from_slice(&0u32.to_le_bytes());
1407 buf.extend_from_slice(&0u32.to_le_bytes());
1408 buf.extend_from_slice(&0u32.to_le_bytes());
1409
1410 let result = import_vmd_motion(&buf).unwrap();
1411 assert!(result.bone_keyframes.is_empty());
1412 assert!(result.morph_keyframes.is_empty());
1413 assert!(result.property_keyframes.is_empty());
1414 }
1415
1416 #[test]
1417 fn accepts_vmd_without_optional_tail_sections() {
1418 let mut buf = build_vmd_header_bytes();
1419 buf.extend_from_slice(&0u32.to_le_bytes());
1420
1421 let result = import_vmd_motion(&buf).unwrap();
1422 assert!(result.bone_keyframes.is_empty());
1423 assert!(result.morph_keyframes.is_empty());
1424 assert!(result.property_keyframes.is_empty());
1425 assert!(result.property_ik_frames.is_empty());
1426 }
1427
1428 #[test]
1429 fn accepts_vmd_ending_after_shadow_section() {
1430 let mut buf = build_vmd_header_bytes();
1431 buf.extend_from_slice(&0u32.to_le_bytes());
1432 buf.extend_from_slice(&0u32.to_le_bytes());
1433 buf.extend_from_slice(&0u32.to_le_bytes());
1434 buf.extend_from_slice(&0u32.to_le_bytes());
1435 buf.extend_from_slice(&0u32.to_le_bytes());
1436
1437 let result = import_vmd_motion(&buf).unwrap();
1438 assert!(result.bone_keyframes.is_empty());
1439 assert!(result.morph_keyframes.is_empty());
1440 assert!(result.property_keyframes.is_empty());
1441 assert!(result.property_ik_frames.is_empty());
1442 }
1443
1444 #[test]
1445 fn rejects_partial_optional_count() {
1446 let mut buf = build_vmd_header_bytes();
1447 buf.extend_from_slice(&0u32.to_le_bytes());
1448 buf.extend_from_slice(&[1, 2, 3]);
1449
1450 assert_eq!(
1451 import_vmd_motion(&buf).unwrap_err(),
1452 ImportError::UnexpectedEof(1)
1453 );
1454 }
1455
1456 #[test]
1457 fn ignores_truncated_unused_tail_sections() {
1458 let mut buf = build_vmd_header_bytes();
1459 buf.extend_from_slice(&0u32.to_le_bytes());
1460 buf.extend_from_slice(&0u32.to_le_bytes());
1461 buf.extend_from_slice(&0u32.to_le_bytes());
1462 buf.extend_from_slice(&0u32.to_le_bytes());
1463 buf.extend_from_slice(&10u32.to_le_bytes());
1464 buf.extend_from_slice(&[0u8; 8]);
1465
1466 let result = import_vmd_motion(&buf).unwrap();
1467 assert!(result.bone_keyframes.is_empty());
1468 assert!(result.morph_keyframes.is_empty());
1469 assert!(result.property_keyframes.is_empty());
1470 assert!(result.property_ik_frames.is_empty());
1471 }
1472
1473 #[test]
1474 fn parsed_animation_ignores_implausible_optional_tail() {
1475 let mut buf = build_vmd_header_bytes();
1476 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());
1484 buf.extend_from_slice(&[0u8; 57]);
1485
1486 let parsed = parse_vmd_animation(&buf).unwrap();
1487 assert!(parsed.camera_frames.is_empty());
1488 assert!(parsed.light_frames.is_empty());
1489 assert!(parsed.self_shadow_frames.is_empty());
1490 assert!(parsed.property_frames.is_empty());
1491 }
1492
1493 #[test]
1494 fn parses_single_bone_keyframe() {
1495 let mut buf = build_vmd_header_bytes();
1496
1497 buf.extend_from_slice(&1u32.to_le_bytes());
1498 let mut bone_name = [0u8; 15];
1499 bone_name[..4].copy_from_slice(b"Bone");
1500 buf.extend_from_slice(&bone_name);
1501 buf.extend_from_slice(&10u32.to_le_bytes());
1502 buf.extend_from_slice(&1.0f32.to_le_bytes());
1503 buf.extend_from_slice(&2.0f32.to_le_bytes());
1504 buf.extend_from_slice(&3.0f32.to_le_bytes());
1505 buf.extend_from_slice(&0.0f32.to_le_bytes());
1506 buf.extend_from_slice(&0.0f32.to_le_bytes());
1507 buf.extend_from_slice(&0.0f32.to_le_bytes());
1508 buf.extend_from_slice(&1.0f32.to_le_bytes());
1509 buf.extend_from_slice(&[20u8; 64]);
1510
1511 buf.extend_from_slice(&0u32.to_le_bytes());
1512 buf.extend_from_slice(&0u32.to_le_bytes());
1513 buf.extend_from_slice(&0u32.to_le_bytes());
1514 buf.extend_from_slice(&0u32.to_le_bytes());
1515 buf.extend_from_slice(&0u32.to_le_bytes());
1516
1517 let result = import_vmd_motion(&buf).unwrap();
1518 assert_eq!(result.bone_keyframes.len(), 1);
1519 assert_eq!(result.bone_keyframes[0].frame, 10);
1520 assert!((result.bone_keyframes[0].position.x - 1.0).abs() < 0.001);
1521 assert!((result.bone_keyframes[0].position.y - 2.0).abs() < 0.001);
1522 assert!((result.bone_keyframes[0].position.z - 3.0).abs() < 0.001);
1523 assert_eq!(&result.bone_keyframes[0].bone_name_normalized[..], b"Bone");
1524 }
1525
1526 #[test]
1527 fn exports_parsed_vmd_animation_for_roundtrip() {
1528 let mut buf = build_vmd_header_bytes();
1529
1530 buf.extend_from_slice(&1u32.to_le_bytes());
1531 let mut bone_name = [0u8; 15];
1532 bone_name[..4].copy_from_slice(b"Bone");
1533 buf.extend_from_slice(&bone_name);
1534 buf.extend_from_slice(&10u32.to_le_bytes());
1535 buf.extend_from_slice(&1.0f32.to_le_bytes());
1536 buf.extend_from_slice(&2.0f32.to_le_bytes());
1537 buf.extend_from_slice(&3.0f32.to_le_bytes());
1538 buf.extend_from_slice(&0.1f32.to_le_bytes());
1539 buf.extend_from_slice(&0.2f32.to_le_bytes());
1540 buf.extend_from_slice(&0.3f32.to_le_bytes());
1541 buf.extend_from_slice(&0.4f32.to_le_bytes());
1542 buf.extend_from_slice(&[20u8; 64]);
1543
1544 buf.extend_from_slice(&1u32.to_le_bytes());
1545 let mut morph_name = [0u8; 15];
1546 morph_name[..5].copy_from_slice(b"Smile");
1547 buf.extend_from_slice(&morph_name);
1548 buf.extend_from_slice(&11u32.to_le_bytes());
1549 buf.extend_from_slice(&0.75f32.to_le_bytes());
1550
1551 buf.extend_from_slice(&1u32.to_le_bytes());
1552 buf.extend_from_slice(&12u32.to_le_bytes());
1553 buf.extend_from_slice(&30.0f32.to_le_bytes());
1554 buf.extend_from_slice(&1.0f32.to_le_bytes());
1555 buf.extend_from_slice(&2.0f32.to_le_bytes());
1556 buf.extend_from_slice(&3.0f32.to_le_bytes());
1557 buf.extend_from_slice(&0.1f32.to_le_bytes());
1558 buf.extend_from_slice(&0.2f32.to_le_bytes());
1559 buf.extend_from_slice(&0.3f32.to_le_bytes());
1560 buf.extend_from_slice(&[30u8; 24]);
1561 buf.extend_from_slice(&45u32.to_le_bytes());
1562 buf.push(0);
1563
1564 buf.extend_from_slice(&1u32.to_le_bytes());
1565 buf.extend_from_slice(&13u32.to_le_bytes());
1566 buf.extend_from_slice(&1.0f32.to_le_bytes());
1567 buf.extend_from_slice(&0.5f32.to_le_bytes());
1568 buf.extend_from_slice(&0.25f32.to_le_bytes());
1569 buf.extend_from_slice(&(-1.0f32).to_le_bytes());
1570 buf.extend_from_slice(&(-0.5f32).to_le_bytes());
1571 buf.extend_from_slice(&(-0.25f32).to_le_bytes());
1572
1573 buf.extend_from_slice(&1u32.to_le_bytes());
1574 buf.extend_from_slice(&14u32.to_le_bytes());
1575 buf.push(2);
1576 buf.extend_from_slice(&0.6f32.to_le_bytes());
1577
1578 buf.extend_from_slice(&1u32.to_le_bytes());
1579 buf.extend_from_slice(&15u32.to_le_bytes());
1580 buf.push(1);
1581 buf.extend_from_slice(&1u32.to_le_bytes());
1582 let mut ik_name = [0u8; 20];
1583 ik_name[..2].copy_from_slice(b"IK");
1584 buf.extend_from_slice(&ik_name);
1585 buf.push(1);
1586
1587 let parsed = parse_vmd_animation(&buf).unwrap();
1588 let exported = export_vmd_animation(&parsed);
1589 let reparsed = parse_vmd_animation(&exported).unwrap();
1590
1591 assert_vmd_roundtrip_eq(&parsed, &reparsed);
1592 }
1593
1594 fn assert_vmd_roundtrip_eq(left: &VmdParsedAnimation, right: &VmdParsedAnimation) {
1595 assert_eq!(left.metadata.model_name, right.metadata.model_name);
1596 assert_eq!(left.metadata.max_frame, right.metadata.max_frame);
1597 assert_eq!(left.bone_frames.len(), right.bone_frames.len());
1598 assert_eq!(left.morph_frames.len(), right.morph_frames.len());
1599 assert_eq!(left.camera_frames.len(), right.camera_frames.len());
1600 assert_eq!(left.light_frames.len(), right.light_frames.len());
1601 assert_eq!(
1602 left.self_shadow_frames.len(),
1603 right.self_shadow_frames.len()
1604 );
1605 assert_eq!(left.property_frames.len(), right.property_frames.len());
1606 assert_eq!(
1607 left.bone_frames[0].bone_name,
1608 right.bone_frames[0].bone_name
1609 );
1610 assert_eq!(left.bone_frames[0].frame, right.bone_frames[0].frame);
1611 assert_eq!(
1612 left.bone_frames[0].translation,
1613 right.bone_frames[0].translation
1614 );
1615 assert_eq!(left.bone_frames[0].rotation, right.bone_frames[0].rotation);
1616 assert_eq!(
1617 left.bone_frames[0].interpolation,
1618 right.bone_frames[0].interpolation
1619 );
1620 assert_eq!(
1621 left.morph_frames[0].morph_name,
1622 right.morph_frames[0].morph_name
1623 );
1624 assert_eq!(left.morph_frames[0].frame, right.morph_frames[0].frame);
1625 assert_eq!(left.morph_frames[0].weight, right.morph_frames[0].weight);
1626 assert_eq!(left.camera_frames[0].frame, right.camera_frames[0].frame);
1627 assert_eq!(
1628 left.camera_frames[0].position,
1629 right.camera_frames[0].position
1630 );
1631 assert_eq!(
1632 left.camera_frames[0].rotation,
1633 right.camera_frames[0].rotation
1634 );
1635 assert_eq!(
1636 left.camera_frames[0].interpolation,
1637 right.camera_frames[0].interpolation
1638 );
1639 assert_eq!(
1640 left.camera_frames[0].perspective,
1641 right.camera_frames[0].perspective
1642 );
1643 assert_eq!(left.light_frames[0].color, right.light_frames[0].color);
1644 assert_eq!(
1645 left.light_frames[0].direction,
1646 right.light_frames[0].direction
1647 );
1648 assert_eq!(
1649 left.self_shadow_frames[0].mode,
1650 right.self_shadow_frames[0].mode
1651 );
1652 assert_eq!(
1653 left.self_shadow_frames[0].distance,
1654 right.self_shadow_frames[0].distance
1655 );
1656 assert_eq!(
1657 left.property_frames[0].visible,
1658 right.property_frames[0].visible
1659 );
1660 assert_eq!(
1661 left.property_frames[0].ik_states[0].bone_name,
1662 right.property_frames[0].ik_states[0].bone_name
1663 );
1664 assert_eq!(
1665 left.property_frames[0].ik_states[0].enabled,
1666 right.property_frames[0].ik_states[0].enabled
1667 );
1668 }
1669
1670 #[test]
1671 fn decodes_raw_vmd_bone_interpolation_as_strided_curves() {
1672 let mut interpolation = [0u8; 64];
1673 for (index, value) in interpolation.iter_mut().enumerate() {
1674 *value = index as u8;
1675 }
1676
1677 let (position, rotation) = decode_bone_interpolation(&interpolation);
1678
1679 assert_eq!(
1680 position.x,
1681 InterpolationScalar {
1682 x1: 0,
1683 y1: 4,
1684 x2: 8,
1685 y2: 12
1686 }
1687 );
1688 assert_eq!(
1689 position.y,
1690 InterpolationScalar {
1691 x1: 1,
1692 y1: 5,
1693 x2: 9,
1694 y2: 13
1695 }
1696 );
1697 assert_eq!(
1698 position.z,
1699 InterpolationScalar {
1700 x1: 2,
1701 y1: 6,
1702 x2: 10,
1703 y2: 14
1704 }
1705 );
1706 assert_eq!(
1707 rotation,
1708 InterpolationScalar {
1709 x1: 3,
1710 y1: 7,
1711 x2: 11,
1712 y2: 15
1713 }
1714 );
1715 }
1716
1717 #[test]
1718 fn decodes_mmd_registered_bone_interpolation_as_block_curves() {
1719 let mut interpolation = [0u8; 64];
1720 for (index, value) in interpolation.iter_mut().enumerate() {
1721 *value = index as u8;
1722 }
1723
1724 let (position, rotation) = decode_mmd_registered_bone_interpolation(&interpolation);
1725
1726 assert_eq!(
1727 position.x,
1728 InterpolationScalar {
1729 x1: 0,
1730 y1: 4,
1731 x2: 8,
1732 y2: 12
1733 }
1734 );
1735 assert_eq!(
1736 position.y,
1737 InterpolationScalar {
1738 x1: 16,
1739 y1: 20,
1740 x2: 24,
1741 y2: 28
1742 }
1743 );
1744 assert_eq!(
1745 position.z,
1746 InterpolationScalar {
1747 x1: 32,
1748 y1: 36,
1749 x2: 40,
1750 y2: 44
1751 }
1752 );
1753 assert_eq!(
1754 rotation,
1755 InterpolationScalar {
1756 x1: 48,
1757 y1: 52,
1758 x2: 56,
1759 y2: 60
1760 }
1761 );
1762 }
1763
1764 #[test]
1765 fn decodes_raw_vmd_camera_interpolation_as_contiguous_curves() {
1766 let mut interpolation = [0u8; 24];
1767 for (index, value) in interpolation.iter_mut().enumerate() {
1768 *value = index as u8;
1769 }
1770
1771 let decoded = decode_camera_interpolation(&interpolation);
1772
1773 assert_eq!(
1774 decoded.position.x,
1775 InterpolationScalar {
1776 x1: 0,
1777 y1: 1,
1778 x2: 2,
1779 y2: 3
1780 }
1781 );
1782 assert_eq!(
1783 decoded.position.y,
1784 InterpolationScalar {
1785 x1: 4,
1786 y1: 5,
1787 x2: 6,
1788 y2: 7
1789 }
1790 );
1791 assert_eq!(
1792 decoded.position.z,
1793 InterpolationScalar {
1794 x1: 8,
1795 y1: 9,
1796 x2: 10,
1797 y2: 11
1798 }
1799 );
1800 assert_eq!(
1801 decoded.rotation,
1802 InterpolationScalar {
1803 x1: 12,
1804 y1: 13,
1805 x2: 14,
1806 y2: 15
1807 }
1808 );
1809 assert_eq!(
1810 decoded.distance,
1811 InterpolationScalar {
1812 x1: 16,
1813 y1: 17,
1814 x2: 18,
1815 y2: 19
1816 }
1817 );
1818 assert_eq!(
1819 decoded.fov,
1820 InterpolationScalar {
1821 x1: 20,
1822 y1: 21,
1823 x2: 22,
1824 y2: 23
1825 }
1826 );
1827 }
1828
1829 #[test]
1830 fn skips_camera_light_shadow_and_reads_ik_property_names() {
1831 let mut buf = build_vmd_header_bytes();
1832
1833 buf.extend_from_slice(&0u32.to_le_bytes());
1834 buf.extend_from_slice(&0u32.to_le_bytes());
1835 buf.extend_from_slice(&1u32.to_le_bytes());
1836 buf.extend_from_slice(&[0u8; 61]);
1837 buf.extend_from_slice(&1u32.to_le_bytes());
1838 buf.extend_from_slice(&[0u8; 28]);
1839 buf.extend_from_slice(&1u32.to_le_bytes());
1840 buf.extend_from_slice(&0u32.to_le_bytes());
1841 buf.push(1);
1842 buf.extend_from_slice(&0.5f32.to_le_bytes());
1843 buf.extend_from_slice(&1u32.to_le_bytes());
1844 buf.extend_from_slice(&24u32.to_le_bytes());
1845 buf.push(1);
1846 buf.extend_from_slice(&1u32.to_le_bytes());
1847 let mut ik_name = [0u8; 20];
1848 ik_name[..6].copy_from_slice(b"LeftIK");
1849 buf.extend_from_slice(&ik_name);
1850 buf.push(0);
1851
1852 let result = import_vmd_motion(&buf).unwrap();
1853 assert_eq!(result.property_keyframes.len(), 1);
1854 assert_eq!(result.property_keyframes[0].frame, 24);
1855 assert_eq!(&*result.property_keyframes[0].ik_enabled, &[0]);
1856
1857 assert_eq!(result.property_ik_frames.len(), 1);
1858 assert_eq!(result.property_ik_frames[0].frame, 24);
1859 assert_eq!(result.property_ik_frames[0].show, 1);
1860 assert_eq!(result.property_ik_frames[0].entries.len(), 1);
1861 let name_end = result.property_ik_frames[0].entries[0]
1862 .name_bytes
1863 .iter()
1864 .position(|&b| b == 0)
1865 .unwrap_or(20);
1866 assert_eq!(
1867 &result.property_ik_frames[0].entries[0].name_bytes[..name_end],
1868 b"LeftIK"
1869 );
1870 assert_eq!(result.property_ik_frames[0].entries[0].enabled, 0);
1871 assert_eq!(
1872 &result.property_ik_frames[0].entries[0].name_normalized[..],
1873 b"LeftIK"
1874 );
1875 }
1876
1877 #[test]
1878 fn builds_clip_from_bone_track() {
1879 let kfs = vec![
1880 VmdBoneKeyframeRaw {
1881 bone_mode: VmdBoneImportMode::ByName(b"BoneA".to_vec()),
1882 frame: 0,
1883 position: Vec3A::ZERO,
1884 rotation: Quat::IDENTITY,
1885 interpolation: [
1886 20, 20, 107, 107, 20, 20, 107, 107, 20, 20, 107, 107, 20, 20, 107, 107, 20, 20,
1887 107, 107, 20, 20, 107, 107, 20, 20, 107, 107, 20, 20, 107, 107, 20, 20, 107,
1888 107, 20, 20, 107, 107, 20, 20, 107, 107, 20, 20, 107, 107, 20, 20, 107, 107,
1889 20, 20, 107, 107, 20, 20, 107, 107, 20, 20, 107, 107,
1890 ],
1891 bone_name_normalized: b"BoneA".to_vec(),
1892 },
1893 VmdBoneKeyframeRaw {
1894 bone_mode: VmdBoneImportMode::ByName(b"BoneA".to_vec()),
1895 frame: 30,
1896 position: Vec3A::new(10.0, 0.0, 0.0),
1897 rotation: Quat::IDENTITY,
1898 interpolation: [
1899 20, 20, 107, 107, 20, 20, 107, 107, 20, 20, 107, 107, 20, 20, 107, 107, 20, 20,
1900 107, 107, 20, 20, 107, 107, 20, 20, 107, 107, 20, 20, 107, 107, 20, 20, 107,
1901 107, 20, 20, 107, 107, 20, 20, 107, 107, 20, 20, 107, 107, 20, 20, 107, 107,
1902 20, 20, 107, 107, 20, 20, 107, 107, 20, 20, 107, 107,
1903 ],
1904 bone_name_normalized: b"BoneA".to_vec(),
1905 },
1906 ];
1907
1908 fn lookup(name: &[u8]) -> Option<BoneIndex> {
1909 if name == b"BoneA" {
1910 Some(BoneIndex(0))
1911 } else {
1912 None
1913 }
1914 }
1915 fn morph_lookup(_name: &[u8]) -> Option<MorphIndex> {
1916 None
1917 }
1918
1919 let result = VmdImportResult {
1920 bone_keyframes: kfs,
1921 morph_keyframes: Vec::new(),
1922 property_keyframes: Vec::new(),
1923 property_ik_frames: Vec::new(),
1924 };
1925
1926 let _clip = build_clip_from_import(result, &lookup, &morph_lookup);
1927 }
1928
1929 #[test]
1930 fn build_clip_from_import_resolves_morph_by_normalized_name() {
1931 let sjis_morph = vec![0x83, 0x65, 0x83, 0x58, 0x83, 0x67];
1932 let result = VmdImportResult {
1933 bone_keyframes: Vec::new(),
1934 morph_keyframes: vec![(sjis_morph, 0, 1.0)],
1935 property_keyframes: Vec::new(),
1936 property_ik_frames: Vec::new(),
1937 };
1938
1939 fn morph_lookup(name: &[u8]) -> Option<MorphIndex> {
1940 if name == b"\xE3\x83\x86\xE3\x82\xB9\xE3\x83\x88" {
1941 Some(MorphIndex(0))
1942 } else {
1943 None
1944 }
1945 }
1946 fn bone_lookup(_name: &[u8]) -> Option<BoneIndex> {
1947 None
1948 }
1949
1950 let clip = build_clip_from_import(result, &bone_lookup, &morph_lookup);
1951 assert_eq!(clip.morph_track_count(), 1);
1952 }
1953
1954 fn ik_name_bytes(name: &str) -> [u8; 20] {
1955 let mut buf = [0u8; 20];
1956 let name_bytes = name.as_bytes();
1957 let len = name_bytes.len().min(20);
1958 buf[..len].copy_from_slice(&name_bytes[..len]);
1959 buf
1960 }
1961
1962 #[test]
1963 fn parses_property_ik_entry_names() {
1964 let mut buf = build_vmd_header_bytes();
1965
1966 buf.extend_from_slice(&0u32.to_le_bytes());
1967 buf.extend_from_slice(&0u32.to_le_bytes());
1968 buf.extend_from_slice(&0u32.to_le_bytes());
1969 buf.extend_from_slice(&0u32.to_le_bytes());
1970 buf.extend_from_slice(&0u32.to_le_bytes());
1971 buf.extend_from_slice(&1u32.to_le_bytes());
1972 buf.extend_from_slice(&10u32.to_le_bytes());
1973 buf.push(0);
1974 buf.extend_from_slice(&2u32.to_le_bytes());
1975 buf.extend_from_slice(&ik_name_bytes("LeftLegIK"));
1976 buf.push(1);
1977 buf.extend_from_slice(&ik_name_bytes("RightLegIK"));
1978 buf.push(0);
1979
1980 let result = import_vmd_motion(&buf).unwrap();
1981 assert_eq!(result.property_ik_frames.len(), 1);
1982 assert_eq!(result.property_ik_frames[0].frame, 10);
1983 assert_eq!(result.property_ik_frames[0].entries.len(), 2);
1984
1985 let name1: Vec<u8> = result.property_ik_frames[0].entries[0]
1986 .name_bytes
1987 .iter()
1988 .copied()
1989 .take_while(|&b| b != 0)
1990 .collect();
1991 assert_eq!(&name1[..], b"LeftLegIK");
1992 assert_eq!(result.property_ik_frames[0].entries[0].enabled, 1);
1993 assert_eq!(
1994 &result.property_ik_frames[0].entries[0].name_normalized[..],
1995 b"LeftLegIK"
1996 );
1997
1998 let name2: Vec<u8> = result.property_ik_frames[0].entries[1]
1999 .name_bytes
2000 .iter()
2001 .copied()
2002 .take_while(|&b| b != 0)
2003 .collect();
2004 assert_eq!(&name2[..], b"RightLegIK");
2005 assert_eq!(result.property_ik_frames[0].entries[1].enabled, 0);
2006 }
2007
2008 #[test]
2009 fn reorders_ik_enabled_to_solver_order() {
2010 let frames = vec![VmdPropertyIkFrame {
2011 frame: 0,
2012 show: 0,
2013 entries: vec![
2014 VmdIkEntry {
2015 name_bytes: {
2016 let mut buf = [0u8; 20];
2017 buf[..9].copy_from_slice(b"RightLegI");
2018 buf.to_vec()
2019 },
2020 enabled: 1,
2021 name_normalized: b"RightLegI".to_vec(),
2022 },
2023 VmdIkEntry {
2024 name_bytes: {
2025 let mut buf = [0u8; 20];
2026 buf[..8].copy_from_slice(b"LeftLegI");
2027 buf.to_vec()
2028 },
2029 enabled: 0,
2030 name_normalized: b"LeftLegI".to_vec(),
2031 },
2032 ],
2033 }];
2034
2035 fn ik_resolver(name: &[u8]) -> Option<usize> {
2036 match name {
2037 b"LeftLegI" => Some(0),
2038 b"RightLegI" => Some(1),
2039 _ => None,
2040 }
2041 }
2042
2043 let binding = build_property_binding_with_ik_resolver(&frames, &ik_resolver, 2).unwrap();
2044 let sample = binding.sample(0.0).unwrap();
2045 assert_eq!(sample, &[0, 1]);
2046 }
2047
2048 #[test]
2049 fn unknown_and_unmentioned_ik_names_keep_default_enabled_state() {
2050 let frames = vec![VmdPropertyIkFrame {
2051 frame: 5,
2052 show: 0,
2053 entries: vec![
2054 VmdIkEntry {
2055 name_bytes: {
2056 let mut buf = [0u8; 20];
2057 buf[..7].copy_from_slice(b"KnownIK");
2058 buf.to_vec()
2059 },
2060 enabled: 1,
2061 name_normalized: b"KnownIK".to_vec(),
2062 },
2063 VmdIkEntry {
2064 name_bytes: {
2065 let mut buf = [0u8; 20];
2066 buf[..9].copy_from_slice(b"UnknownIK");
2067 buf.to_vec()
2068 },
2069 enabled: 1,
2070 name_normalized: b"UnknownIK".to_vec(),
2071 },
2072 ],
2073 }];
2074
2075 fn ik_resolver(name: &[u8]) -> Option<usize> {
2076 if name == b"KnownIK" { Some(0) } else { None }
2077 }
2078
2079 let binding = build_property_binding_with_ik_resolver(&frames, &ik_resolver, 2).unwrap();
2080 let sample = binding.sample(5.0).unwrap();
2081 assert_eq!(sample, &[1, 1]);
2082 }
2083
2084 #[test]
2085 fn empty_ik_frames_returns_none() {
2086 fn ik_resolver(_name: &[u8]) -> Option<usize> {
2087 None
2088 }
2089 let binding = build_property_binding_with_ik_resolver(&[], &ik_resolver, 2);
2090 assert!(binding.is_none());
2091 }
2092
2093 #[test]
2094 fn zero_solver_count_returns_none() {
2095 let frames = vec![VmdPropertyIkFrame {
2096 frame: 0,
2097 show: 0,
2098 entries: vec![VmdIkEntry {
2099 name_bytes: ik_name_bytes("IK").to_vec(),
2100 enabled: 1,
2101 name_normalized: b"IK".to_vec(),
2102 }],
2103 }];
2104 fn ik_resolver(_name: &[u8]) -> Option<usize> {
2105 Some(0)
2106 }
2107 let binding = build_property_binding_with_ik_resolver(&frames, &ik_resolver, 0);
2108 assert!(binding.is_none());
2109 }
2110
2111 #[test]
2112 fn build_property_binding_resolves_ik_by_normalized_name() {
2113 let frames = vec![VmdPropertyIkFrame {
2114 frame: 0,
2115 show: 0,
2116 entries: vec![VmdIkEntry {
2117 name_bytes: vec![0x8D, 0xB6, 0x91, 0xAB],
2118 enabled: 1,
2119 name_normalized: vec![0xE5, 0xB7, 0xA6, 0xE8, 0xB6, 0xB3],
2120 }],
2121 }];
2122
2123 fn ik_resolver(name: &[u8]) -> Option<usize> {
2124 if name == b"\xE5\xB7\xA6\xE8\xB6\xB3" {
2125 Some(0)
2126 } else {
2127 None
2128 }
2129 }
2130
2131 let binding = build_property_binding_with_ik_resolver(&frames, &ik_resolver, 1).unwrap();
2132 let sample = binding.sample(0.0).unwrap();
2133 assert_eq!(sample, &[1]);
2134 }
2135
2136 #[test]
2137 fn build_clip_from_import_resolves_bone_by_normalized_name() {
2138 let sjis_name: Vec<u8> = vec![0x8D, 0xB6, 0x91, 0xAB];
2139 let utf8_name: Vec<u8> = vec![0xE5, 0xB7, 0xA6, 0xE8, 0xB6, 0xB3];
2140
2141 let kf = VmdBoneKeyframeRaw {
2142 bone_mode: VmdBoneImportMode::ByName(sjis_name),
2143 frame: 0,
2144 position: Vec3A::ZERO,
2145 rotation: Quat::IDENTITY,
2146 interpolation: [20u8; 64],
2147 bone_name_normalized: utf8_name.clone(),
2148 };
2149
2150 fn lookup(name: &[u8]) -> Option<BoneIndex> {
2151 if name == b"\xE5\xB7\xA6\xE8\xB6\xB3" {
2152 Some(BoneIndex(0))
2153 } else {
2154 None
2155 }
2156 }
2157 fn morph_lookup(_name: &[u8]) -> Option<MorphIndex> {
2158 None
2159 }
2160
2161 let result = VmdImportResult {
2162 bone_keyframes: vec![kf],
2163 morph_keyframes: Vec::new(),
2164 property_keyframes: Vec::new(),
2165 property_ik_frames: Vec::new(),
2166 };
2167
2168 let clip = build_clip_from_import(result, &lookup, &morph_lookup);
2169 assert_eq!(
2170 clip.bone_track_count(),
2171 1,
2172 "bone should be resolved via normalized UTF-8 name, not raw Shift-JIS bytes"
2173 );
2174 }
2175
2176 #[test]
2177 fn japanese_vmd_name_matches_pmx_utf8_name_via_normalization() {
2178 let sjis_name: &[u8] = &[0x8D, 0xB6, 0x91, 0xAB];
2179 let utf8_name: &[u8] = &[0xE5, 0xB7, 0xA6, 0xE8, 0xB6, 0xB3];
2180
2181 let mut pmx_buf = Vec::new();
2182 pmx_buf.extend_from_slice(b"PMX ");
2183 pmx_buf.extend_from_slice(&2.0f32.to_le_bytes());
2184 pmx_buf.push(8);
2185 pmx_buf.push(1);
2186 pmx_buf.push(0);
2187 pmx_buf.push(4);
2188 pmx_buf.push(1);
2189 pmx_buf.push(1);
2190 pmx_buf.push(2);
2191 pmx_buf.push(1);
2192 pmx_buf.push(1);
2193 for _ in 0..4 {
2194 pmx_buf.extend_from_slice(&0i32.to_le_bytes());
2195 }
2196 pmx_buf.extend_from_slice(&0i32.to_le_bytes());
2197 pmx_buf.extend_from_slice(&0i32.to_le_bytes());
2198 pmx_buf.extend_from_slice(&0i32.to_le_bytes());
2199 pmx_buf.extend_from_slice(&0i32.to_le_bytes());
2200 pmx_buf.extend_from_slice(&1i32.to_le_bytes());
2201 pmx_buf.extend_from_slice(&(utf8_name.len() as i32).to_le_bytes());
2202 pmx_buf.extend_from_slice(utf8_name);
2203 pmx_buf.extend_from_slice(&0i32.to_le_bytes());
2204 pmx_buf.extend_from_slice(&0.0f32.to_le_bytes());
2205 pmx_buf.extend_from_slice(&0.0f32.to_le_bytes());
2206 pmx_buf.extend_from_slice(&0.0f32.to_le_bytes());
2207 pmx_buf.extend_from_slice(&(-1i16).to_le_bytes());
2208 pmx_buf.extend_from_slice(&0i32.to_le_bytes());
2209 pmx_buf.extend_from_slice(&0u16.to_le_bytes());
2210 pmx_buf.extend_from_slice(&0.0f32.to_le_bytes());
2211 pmx_buf.extend_from_slice(&0.0f32.to_le_bytes());
2212 pmx_buf.extend_from_slice(&0.0f32.to_le_bytes());
2213 pmx_buf.extend_from_slice(&0i32.to_le_bytes());
2214
2215 let pmx = crate::pmx::import_pmx_runtime(&pmx_buf).unwrap();
2216
2217 let mut vmd_buf = build_vmd_header_bytes();
2218 vmd_buf.extend_from_slice(&1u32.to_le_bytes());
2219 let mut bone_name = [0u8; 15];
2220 bone_name[..sjis_name.len()].copy_from_slice(sjis_name);
2221 vmd_buf.extend_from_slice(&bone_name);
2222 vmd_buf.extend_from_slice(&0u32.to_le_bytes());
2223 vmd_buf.extend_from_slice(&0.0f32.to_le_bytes());
2224 vmd_buf.extend_from_slice(&0.0f32.to_le_bytes());
2225 vmd_buf.extend_from_slice(&0.0f32.to_le_bytes());
2226 vmd_buf.extend_from_slice(&0.0f32.to_le_bytes());
2227 vmd_buf.extend_from_slice(&0.0f32.to_le_bytes());
2228 vmd_buf.extend_from_slice(&0.0f32.to_le_bytes());
2229 vmd_buf.extend_from_slice(&1.0f32.to_le_bytes());
2230 vmd_buf.extend_from_slice(&[20u8; 64]);
2231 vmd_buf.extend_from_slice(&0u32.to_le_bytes());
2232 vmd_buf.extend_from_slice(&0u32.to_le_bytes());
2233 vmd_buf.extend_from_slice(&0u32.to_le_bytes());
2234 vmd_buf.extend_from_slice(&0u32.to_le_bytes());
2235 vmd_buf.extend_from_slice(&0u32.to_le_bytes());
2236
2237 let vmd = import_vmd_motion(&vmd_buf).unwrap();
2238
2239 let normalized = normalize_vmd_name(sjis_name);
2240 assert_eq!(&normalized[..], utf8_name);
2241
2242 assert!(
2243 pmx.bone_name_to_index.contains_key(&normalized),
2244 "PMX should have normalized bone name key"
2245 );
2246
2247 let clip = build_pair_clip(
2248 &vmd,
2249 &pmx.bone_name_to_index,
2250 &pmx.morph_name_to_index,
2251 &pmx.ik_solver_bone_name_to_index,
2252 pmx.model.ik_count(),
2253 );
2254
2255 assert_eq!(clip.bone_track_count(), 1);
2256 assert_eq!(clip.morph_track_count(), 0);
2257 assert!(!clip.has_property_track());
2258 }
2259
2260 #[test]
2261 fn japanese_vmd_name_matches_pmx_utf16le_name_via_decoded_key() {
2262 let sjis_name: &[u8] = &[0x8D, 0xB6, 0x91, 0xAB];
2263 let utf8_name: &[u8] = &[0xE5, 0xB7, 0xA6, 0xE8, 0xB6, 0xB3];
2264 let utf16le_name: Vec<u8> = "\u{5DE6}\u{8DB3}"
2265 .encode_utf16()
2266 .flat_map(u16::to_le_bytes)
2267 .collect();
2268
2269 let mut pmx_buf = Vec::new();
2270 pmx_buf.extend_from_slice(b"PMX ");
2271 pmx_buf.extend_from_slice(&2.0f32.to_le_bytes());
2272 pmx_buf.push(8);
2273 pmx_buf.push(0);
2274 pmx_buf.push(0);
2275 pmx_buf.push(4);
2276 pmx_buf.push(1);
2277 pmx_buf.push(1);
2278 pmx_buf.push(2);
2279 pmx_buf.push(1);
2280 pmx_buf.push(1);
2281 for _ in 0..4 {
2282 pmx_buf.extend_from_slice(&0i32.to_le_bytes());
2283 }
2284 pmx_buf.extend_from_slice(&0i32.to_le_bytes());
2285 pmx_buf.extend_from_slice(&0i32.to_le_bytes());
2286 pmx_buf.extend_from_slice(&0i32.to_le_bytes());
2287 pmx_buf.extend_from_slice(&0i32.to_le_bytes());
2288 pmx_buf.extend_from_slice(&1i32.to_le_bytes());
2289 pmx_buf.extend_from_slice(&(utf16le_name.len() as i32).to_le_bytes());
2290 pmx_buf.extend_from_slice(&utf16le_name);
2291 pmx_buf.extend_from_slice(&0i32.to_le_bytes());
2292 pmx_buf.extend_from_slice(&0.0f32.to_le_bytes());
2293 pmx_buf.extend_from_slice(&0.0f32.to_le_bytes());
2294 pmx_buf.extend_from_slice(&0.0f32.to_le_bytes());
2295 pmx_buf.extend_from_slice(&(-1i16).to_le_bytes());
2296 pmx_buf.extend_from_slice(&0i32.to_le_bytes());
2297 pmx_buf.extend_from_slice(&0u16.to_le_bytes());
2298 pmx_buf.extend_from_slice(&0.0f32.to_le_bytes());
2299 pmx_buf.extend_from_slice(&0.0f32.to_le_bytes());
2300 pmx_buf.extend_from_slice(&0.0f32.to_le_bytes());
2301 pmx_buf.extend_from_slice(&0i32.to_le_bytes());
2302
2303 let pmx = crate::pmx::import_pmx_runtime(&pmx_buf).unwrap();
2304
2305 let mut vmd_buf = build_vmd_header_bytes();
2306 vmd_buf.extend_from_slice(&1u32.to_le_bytes());
2307 let mut bone_name = [0u8; 15];
2308 bone_name[..sjis_name.len()].copy_from_slice(sjis_name);
2309 vmd_buf.extend_from_slice(&bone_name);
2310 vmd_buf.extend_from_slice(&0u32.to_le_bytes());
2311 vmd_buf.extend_from_slice(&0.0f32.to_le_bytes());
2312 vmd_buf.extend_from_slice(&0.0f32.to_le_bytes());
2313 vmd_buf.extend_from_slice(&0.0f32.to_le_bytes());
2314 vmd_buf.extend_from_slice(&0.0f32.to_le_bytes());
2315 vmd_buf.extend_from_slice(&0.0f32.to_le_bytes());
2316 vmd_buf.extend_from_slice(&0.0f32.to_le_bytes());
2317 vmd_buf.extend_from_slice(&1.0f32.to_le_bytes());
2318 vmd_buf.extend_from_slice(&[20u8; 64]);
2319
2320 let vmd = import_vmd_motion(&vmd_buf).unwrap();
2321 let normalized = normalize_vmd_name(sjis_name);
2322 assert_eq!(&normalized[..], utf8_name);
2323 assert!(
2324 pmx.bone_name_to_index.contains_key(&utf16le_name),
2325 "PMX should retain the raw UTF-16LE name key"
2326 );
2327 assert!(
2328 pmx.bone_name_to_index.contains_key(&normalized),
2329 "PMX should also index UTF-16LE names by decoded UTF-8 bytes"
2330 );
2331
2332 let clip = build_pair_clip(
2333 &vmd,
2334 &pmx.bone_name_to_index,
2335 &pmx.morph_name_to_index,
2336 &pmx.ik_solver_bone_name_to_index,
2337 pmx.model.ik_count(),
2338 );
2339
2340 assert_eq!(clip.bone_track_count(), 1);
2341 }
2342
2343 #[test]
2344 fn build_pair_clip_default_includes_property_ik() {
2345 let result = VmdImportResult {
2347 bone_keyframes: Vec::new(),
2348 morph_keyframes: Vec::new(),
2349 property_keyframes: Vec::new(),
2350 property_ik_frames: vec![VmdPropertyIkFrame {
2351 frame: 0,
2352 show: 0,
2353 entries: vec![VmdIkEntry {
2354 name_bytes: ik_name_bytes("LeftLegIK").to_vec(),
2355 enabled: 0,
2356 name_normalized: b"LeftLegIK".to_vec(),
2357 }],
2358 }],
2359 };
2360
2361 let bone_name_to_index: std::collections::HashMap<Vec<u8>, BoneIndex> =
2362 std::collections::HashMap::new();
2363 let morph_name_to_index: std::collections::HashMap<Vec<u8>, MorphIndex> =
2364 std::collections::HashMap::new();
2365 let mut ik_solver_bone_name_to_index: std::collections::HashMap<Vec<u8>, usize> =
2366 std::collections::HashMap::new();
2367 ik_solver_bone_name_to_index.insert(b"LeftLegIK".to_vec(), 0);
2368
2369 let clip = build_pair_clip(
2370 &result,
2371 &bone_name_to_index,
2372 &morph_name_to_index,
2373 &ik_solver_bone_name_to_index,
2374 2, );
2376
2377 assert!(
2378 clip.has_property_track(),
2379 "default build_pair_clip should include property IK track"
2380 );
2381 }
2382
2383 #[test]
2384 fn build_pair_clip_with_options_omits_property_ik() {
2385 let result = VmdImportResult {
2387 bone_keyframes: Vec::new(),
2388 morph_keyframes: Vec::new(),
2389 property_keyframes: Vec::new(),
2390 property_ik_frames: vec![VmdPropertyIkFrame {
2391 frame: 5,
2392 show: 0,
2393 entries: vec![VmdIkEntry {
2394 name_bytes: ik_name_bytes("RightLegIK").to_vec(),
2395 enabled: 0,
2396 name_normalized: b"RightLegIK".to_vec(),
2397 }],
2398 }],
2399 };
2400
2401 let bone_name_to_index: std::collections::HashMap<Vec<u8>, BoneIndex> =
2402 std::collections::HashMap::new();
2403 let morph_name_to_index: std::collections::HashMap<Vec<u8>, MorphIndex> =
2404 std::collections::HashMap::new();
2405 let mut ik_solver_bone_name_to_index: std::collections::HashMap<Vec<u8>, usize> =
2406 std::collections::HashMap::new();
2407 ik_solver_bone_name_to_index.insert(b"RightLegIK".to_vec(), 1);
2408
2409 let clip = build_pair_clip_with_options(
2410 &result,
2411 &bone_name_to_index,
2412 &morph_name_to_index,
2413 &ik_solver_bone_name_to_index,
2414 2,
2415 VmdClipBuildOptions {
2416 honor_property_ik: false,
2417 },
2418 );
2419
2420 assert!(
2421 !clip.has_property_track(),
2422 "build_pair_clip_with_options(honor_property_ik: false) should omit property IK track"
2423 );
2424 }
2425
2426 fn fixed_axis_test_keyframe(rotation: Quat) -> VmdBoneKeyframeRaw {
2427 VmdBoneKeyframeRaw {
2428 bone_mode: VmdBoneImportMode::ByIndex(7),
2429 frame: 12,
2430 position: Vec3A::new(1.0, 2.0, 3.0),
2431 rotation,
2432 interpolation: [20u8; 64],
2433 bone_name_normalized: b"FixedAxis".to_vec(),
2434 }
2435 }
2436
2437 fn assert_quat_near(actual: Quat, expected: Quat) {
2438 for (actual, expected) in [actual.x, actual.y, actual.z, actual.w]
2439 .into_iter()
2440 .zip([expected.x, expected.y, expected.z, expected.w])
2441 {
2442 assert!((actual - expected).abs() < 1.0e-6);
2443 }
2444 }
2445
2446 fn test_model(fixed_axis: Option<Vec3A>) -> ModelArena {
2447 let mut bones: Vec<_> = (0..8).map(|_| BoneInit::new(None, Vec3A::ZERO)).collect();
2448 if let Some(axis) = fixed_axis {
2449 bones[7] = BoneInit::new(None, Vec3A::ZERO).with_fixed_axis(axis);
2450 }
2451 ModelArena::new(bones).unwrap()
2452 }
2453
2454 #[test]
2455 fn model_fixed_axis_projects_off_axis_positive_dot_and_preserves_keyframe_data() {
2456 let source = fixed_axis_test_keyframe(Quat::from_xyzw(0.3, 0.4, 0.0, 0.8));
2457 let model = test_model(Some(Vec3A::new(0.0, 3.0, 0.0)));
2458 let mapped = build_mapped_bone_keyframe(
2459 &source,
2460 BoneIndex(7),
2461 VmdRegistrationPolicy::MmdRegistered(&model),
2462 );
2463
2464 assert_quat_near(mapped.rotation, Quat::from_xyzw(0.0, 0.5, 0.0, 0.8));
2465 assert_eq!(mapped.frame, source.frame);
2466 assert_eq!(mapped.position, source.position);
2467 let (position_interpolation, rotation_interpolation) =
2468 decode_mmd_registered_bone_interpolation(&source.interpolation);
2469 assert_eq!(mapped.position_interpolation, position_interpolation);
2470 assert_eq!(mapped.rotation_interpolation, rotation_interpolation);
2471 }
2472
2473 #[test]
2474 fn model_fixed_axis_projects_off_axis_negative_dot_with_negative_sign() {
2475 let source = fixed_axis_test_keyframe(Quat::from_xyzw(0.3, -0.4, 0.0, 0.8));
2476 let model = test_model(Some(Vec3A::new(0.0, 3.0, 0.0)));
2477 let mapped = build_mapped_bone_keyframe(
2478 &source,
2479 BoneIndex(7),
2480 VmdRegistrationPolicy::MmdRegistered(&model),
2481 );
2482
2483 assert_quat_near(mapped.rotation, Quat::from_xyzw(0.0, -0.5, 0.0, 0.8));
2484 }
2485
2486 #[test]
2487 fn model_fixed_axis_leaves_already_axis_aligned_rotation_unchanged() {
2488 let source = fixed_axis_test_keyframe(Quat::from_xyzw(0.0, -0.5, 0.0, 0.8));
2489 let model = test_model(Some(Vec3A::new(0.0, 3.0, 0.0)));
2490 let mapped = build_mapped_bone_keyframe(
2491 &source,
2492 BoneIndex(7),
2493 VmdRegistrationPolicy::MmdRegistered(&model),
2494 );
2495
2496 assert_quat_near(mapped.rotation, source.rotation);
2497 }
2498
2499 #[test]
2500 fn model_fixed_axis_leaves_non_fixed_bone_rotation_unchanged() {
2501 let source = fixed_axis_test_keyframe(Quat::from_xyzw(0.3, 0.4, 0.1, 0.8));
2502 let model = test_model(None);
2503 let mapped = build_mapped_bone_keyframe(
2504 &source,
2505 BoneIndex(7),
2506 VmdRegistrationPolicy::MmdRegistered(&model),
2507 );
2508
2509 assert_eq!(mapped.rotation, source.rotation);
2510 }
2511
2512 #[test]
2513 fn model_aware_bone_keyframe_uses_registered_interpolation_for_all_mapped_bones() {
2514 let mut source = fixed_axis_test_keyframe(Quat::IDENTITY);
2515 for (index, value) in source.interpolation.iter_mut().enumerate() {
2516 *value = index as u8;
2517 }
2518 let model = test_model(None);
2519 let raw = build_mapped_bone_keyframe(&source, BoneIndex(7), VmdRegistrationPolicy::Raw);
2520 let model_aware = build_mapped_bone_keyframe(
2521 &source,
2522 BoneIndex(7),
2523 VmdRegistrationPolicy::MmdRegistered(&model),
2524 );
2525 let (raw_position, raw_rotation) = decode_bone_interpolation(&source.interpolation);
2526 let (registered_position, registered_rotation) =
2527 decode_mmd_registered_bone_interpolation(&source.interpolation);
2528
2529 assert_eq!(raw.position_interpolation, raw_position);
2530 assert_eq!(raw.rotation_interpolation, raw_rotation);
2531 assert_eq!(model_aware.position_interpolation, registered_position);
2532 assert_eq!(model_aware.rotation_interpolation, registered_rotation);
2533 assert_ne!(
2534 raw.position_interpolation,
2535 model_aware.position_interpolation
2536 );
2537 assert_ne!(
2538 raw.rotation_interpolation,
2539 model_aware.rotation_interpolation
2540 );
2541 }
2542
2543 #[test]
2544 fn public_mmd_registered_builder_projects_fixed_axis_rotation() {
2545 let source = fixed_axis_test_keyframe(Quat::from_xyzw(0.3, 0.4, 0.0, 0.8));
2546 let model = test_model(Some(Vec3A::Y));
2547 let result = VmdImportResult {
2548 bone_keyframes: vec![source],
2549 morph_keyframes: Vec::new(),
2550 property_keyframes: Vec::new(),
2551 property_ik_frames: Vec::new(),
2552 };
2553 let mut bone_name_to_index = std::collections::HashMap::new();
2554 bone_name_to_index.insert(b"FixedAxis".to_vec(), BoneIndex(7));
2555 let morph_name_to_index = std::collections::HashMap::new();
2556 let ik_solver_bone_name_to_index = std::collections::HashMap::new();
2557
2558 let clip = build_mmd_registered_pair_clip_with_options(
2559 &model,
2560 &result,
2561 &bone_name_to_index,
2562 &morph_name_to_index,
2563 &ik_solver_bone_name_to_index,
2564 0,
2565 VmdClipBuildOptions::default(),
2566 )
2567 .unwrap();
2568 let sample = clip.sample_at(12.0);
2569 let rotation = sample.bone_samples()[0].rotation;
2570 assert_quat_near(rotation, Quat::from_xyzw(0.0, 0.5, 0.0, 0.8).normalize());
2571 }
2572
2573 #[test]
2574 fn public_mmd_registered_builder_uses_registered_interpolation_layout() {
2575 let mut first = fixed_axis_test_keyframe(Quat::IDENTITY);
2576 first.frame = 0;
2577 first.position = Vec3A::ZERO;
2578 let mut second = fixed_axis_test_keyframe(Quat::IDENTITY);
2579 second.frame = 10;
2580 second.position = Vec3A::Y;
2581 for (index, value) in second.interpolation.iter_mut().enumerate() {
2582 *value = index as u8;
2583 }
2584 let result = VmdImportResult {
2585 bone_keyframes: vec![first, second],
2586 morph_keyframes: Vec::new(),
2587 property_keyframes: Vec::new(),
2588 property_ik_frames: Vec::new(),
2589 };
2590 let mut bone_name_to_index = std::collections::HashMap::new();
2591 bone_name_to_index.insert(b"FixedAxis".to_vec(), BoneIndex(7));
2592 let morph_name_to_index = std::collections::HashMap::new();
2593 let ik_solver_bone_name_to_index = std::collections::HashMap::new();
2594 let model = test_model(None);
2595
2596 let registered = build_mmd_registered_pair_clip(
2597 &model,
2598 &result,
2599 &bone_name_to_index,
2600 &morph_name_to_index,
2601 &ik_solver_bone_name_to_index,
2602 0,
2603 )
2604 .unwrap();
2605 let raw = build_pair_clip(
2606 &result,
2607 &bone_name_to_index,
2608 &morph_name_to_index,
2609 &ik_solver_bone_name_to_index,
2610 0,
2611 );
2612 let registered_position = registered.sample_at(5.0).bone_samples()[0].position;
2613 let raw_position = raw.sample_at(5.0).bone_samples()[0].position;
2614 assert_ne!(registered_position, raw_position);
2615 }
2616
2617 #[test]
2618 fn public_mmd_registered_builder_rejects_out_of_range_bone_map() {
2619 let model = test_model(None);
2620 let result = VmdImportResult {
2621 bone_keyframes: Vec::new(),
2622 morph_keyframes: Vec::new(),
2623 property_keyframes: Vec::new(),
2624 property_ik_frames: Vec::new(),
2625 };
2626 let mut bone_name_to_index = std::collections::HashMap::new();
2627 bone_name_to_index.insert(b"Invalid".to_vec(), BoneIndex(8));
2628 let morph_name_to_index = std::collections::HashMap::new();
2629 let ik_solver_bone_name_to_index = std::collections::HashMap::new();
2630
2631 let error = build_mmd_registered_pair_clip(
2632 &model,
2633 &result,
2634 &bone_name_to_index,
2635 &morph_name_to_index,
2636 &ik_solver_bone_name_to_index,
2637 0,
2638 )
2639 .unwrap_err();
2640 assert_eq!(
2641 error,
2642 VmdClipBuildError::BoneIndexOutOfRange {
2643 bone_index: BoneIndex(8),
2644 bone_count: 8,
2645 }
2646 );
2647 }
2648
2649 type SyntheticBoneFrame<'a> = (&'a str, u32, [f32; 3], [f32; 4], [u8; 64]);
2655 type SyntheticMorphFrame<'a> = (&'a str, u32, f32);
2656 type SyntheticPropertyFrame<'a> = (&'a [(&'a str, bool)], u32, bool);
2657
2658 fn assert_near(actual: f32, expected: f32) {
2659 let delta = (actual - expected).abs();
2660 assert!(
2661 delta < 1.0e-4,
2662 "actual={actual:?} expected={expected:?} delta={delta:?}"
2663 );
2664 }
2665
2666 fn assert_vec3_near(actual: [f32; 3], expected: [f32; 3]) {
2667 for (actual, expected) in actual.into_iter().zip(expected) {
2668 assert_near(actual, expected);
2669 }
2670 }
2671
2672 fn simple_camera_vmd_fixture() -> &'static [u8] {
2673 include_bytes!("../../fixtures/vmd/simple_camera.vmd")
2674 }
2675
2676 fn make_vmd_bytes(
2677 model_name_ascii: &str,
2678 bone_frames: &[SyntheticBoneFrame<'_>],
2679 morph_frames: &[SyntheticMorphFrame<'_>],
2680 property_frames: &[SyntheticPropertyFrame<'_>],
2681 ) -> Vec<u8> {
2682 let mut v = Vec::new();
2683 v.extend_from_slice(&VMD_MAGIC);
2684
2685 let mut name_buf = [0u8; 20];
2686 let nb = model_name_ascii.as_bytes();
2687 name_buf[..nb.len().min(20)].copy_from_slice(&nb[..nb.len().min(20)]);
2688 v.extend_from_slice(&name_buf);
2689
2690 v.extend_from_slice(&(bone_frames.len() as u32).to_le_bytes());
2691 for (name, frame, trans, rot, interp) in bone_frames {
2692 let mut nb = [0u8; 15];
2693 let b = name.as_bytes();
2694 nb[..b.len().min(15)].copy_from_slice(&b[..b.len().min(15)]);
2695 v.extend_from_slice(&nb);
2696 v.extend_from_slice(&frame.to_le_bytes());
2697 for &x in trans {
2698 v.extend_from_slice(&x.to_le_bytes());
2699 }
2700 for &x in rot {
2701 v.extend_from_slice(&x.to_le_bytes());
2702 }
2703 v.extend_from_slice(interp);
2704 }
2705
2706 v.extend_from_slice(&(morph_frames.len() as u32).to_le_bytes());
2707 for (name, frame, weight) in morph_frames {
2708 let mut nb = [0u8; 15];
2709 let b = name.as_bytes();
2710 nb[..b.len().min(15)].copy_from_slice(&b[..b.len().min(15)]);
2711 v.extend_from_slice(&nb);
2712 v.extend_from_slice(&frame.to_le_bytes());
2713 v.extend_from_slice(&weight.to_le_bytes());
2714 }
2715
2716 v.extend_from_slice(&0u32.to_le_bytes());
2718 v.extend_from_slice(&0u32.to_le_bytes());
2719 v.extend_from_slice(&0u32.to_le_bytes());
2720
2721 v.extend_from_slice(&(property_frames.len() as u32).to_le_bytes());
2722 for (ik_states, frame, visible) in property_frames {
2723 v.extend_from_slice(&frame.to_le_bytes());
2724 v.push(u8::from(*visible));
2725 v.extend_from_slice(&(ik_states.len() as u32).to_le_bytes());
2726 for (ik_name, enabled) in *ik_states {
2727 let mut nb = [0u8; 20];
2728 let b = ik_name.as_bytes();
2729 nb[..b.len().min(20)].copy_from_slice(&b[..b.len().min(20)]);
2730 v.extend_from_slice(&nb);
2731 v.push(u8::from(*enabled));
2732 }
2733 }
2734 v
2735 }
2736
2737 fn json_keys(value: &serde_json::Value) -> Vec<String> {
2738 let mut keys = value
2739 .as_object()
2740 .unwrap()
2741 .keys()
2742 .map(ToOwned::to_owned)
2743 .collect::<Vec<_>>();
2744 keys.sort();
2745 keys
2746 }
2747
2748 #[test]
2749 fn vmd_animation_json_top_level_schema_is_stable() {
2750 let vmd = make_vmd_bytes("miku", &[], &[], &[]);
2751 let parsed = parse_vmd_animation(&vmd).unwrap();
2752 let keys = json_keys(&serde_json::to_value(&parsed).unwrap());
2753
2754 assert_eq!(
2755 keys,
2756 vec![
2757 "boneFrames",
2758 "cameraFrames",
2759 "kind",
2760 "lightFrames",
2761 "metadata",
2762 "morphFrames",
2763 "propertyFrames",
2764 "selfShadowFrames",
2765 ]
2766 );
2767 }
2768
2769 #[test]
2770 fn parses_simple_camera_vmd_fixture() {
2771 let parsed = parse_vmd_animation(simple_camera_vmd_fixture()).unwrap();
2772
2773 assert_eq!(parsed.metadata.model_name, "camera_fixture");
2774 assert_eq!(parsed.metadata.counts.bones, 0);
2775 assert_eq!(parsed.metadata.counts.morphs, 0);
2776 assert_eq!(parsed.metadata.counts.cameras, 2);
2777 assert_eq!(parsed.metadata.counts.lights, 0);
2778 assert_eq!(parsed.metadata.counts.self_shadows, 0);
2779 assert_eq!(parsed.metadata.counts.properties, 0);
2780 assert_eq!(parsed.metadata.max_frame, 45);
2781 assert_eq!(parsed.camera_frames.len(), 2);
2782
2783 let first = &parsed.camera_frames[0];
2784 assert_eq!(first.frame, 0);
2785 assert_eq!(first.distance, -30.5);
2786 assert_eq!(first.position, [1.0, 2.0, 3.0]);
2787 assert_eq!(first.rotation, [0.1, -0.2, 0.3]);
2788 assert_eq!(
2789 first.interpolation,
2790 [
2791 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40,
2792 41, 42, 43
2793 ]
2794 );
2795 assert_eq!(first.fov, 35);
2796 assert!(first.perspective);
2797
2798 let second = &parsed.camera_frames[1];
2799 assert_eq!(second.frame, 45);
2800 assert_eq!(second.distance, -50.0);
2801 assert_eq!(second.position, [-1.5, 10.0, 0.25]);
2802 assert_eq!(second.rotation, [-0.3, 0.0, 1.2]);
2803 assert_eq!(
2804 second.interpolation,
2805 [
2806 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 127, 127, 127, 127, 127, 127, 127, 127, 127,
2807 127, 127, 127
2808 ]
2809 );
2810 assert_eq!(second.fov, 60);
2811 assert!(!second.perspective);
2812 }
2813
2814 #[test]
2815 fn roundtrip_simple_camera_vmd_fixture_parse_export_parse() {
2816 let parsed = parse_vmd_animation(simple_camera_vmd_fixture()).unwrap();
2817 let exported = export_vmd_animation(&parsed);
2818 let reparsed = parse_vmd_animation(&exported).unwrap();
2819
2820 assert_eq!(reparsed.metadata.model_name, parsed.metadata.model_name);
2821 assert_eq!(reparsed.metadata.counts.cameras, 2);
2822 assert_eq!(reparsed.metadata.max_frame, 45);
2823 assert_eq!(reparsed.camera_frames.len(), parsed.camera_frames.len());
2824 for (left, right) in parsed.camera_frames.iter().zip(&reparsed.camera_frames) {
2825 assert_eq!(left.frame, right.frame);
2826 assert_eq!(left.distance, right.distance);
2827 assert_eq!(left.position, right.position);
2828 assert_eq!(left.rotation, right.rotation);
2829 assert_eq!(left.interpolation, right.interpolation);
2830 assert_eq!(left.fov, right.fov);
2831 assert_eq!(left.perspective, right.perspective);
2832 }
2833 }
2834
2835 #[test]
2836 fn samples_simple_camera_vmd_fixture_with_channel_interpolation() {
2837 let parsed = parse_vmd_animation(simple_camera_vmd_fixture()).unwrap();
2838 let camera = sample_vmd_camera_frames(&parsed.camera_frames, 22.5).unwrap();
2839
2840 assert_near(camera.distance, -40.25);
2841 assert_vec3_near(camera.position, [-0.25, 6.0, 1.625]);
2842 assert_vec3_near(camera.rotation, [-0.1, -0.1, 0.75]);
2843 assert_near(camera.fov, 47.5);
2844 assert!(camera.perspective);
2845
2846 let last = sample_vmd_camera_frames(&parsed.camera_frames, 45.0).unwrap();
2847 assert!(!last.perspective);
2848 }
2849
2850 #[test]
2851 fn samples_vmd_light_frames_linearly() {
2852 let frames = vec![
2853 VmdParsedLightFrame {
2854 frame: 30,
2855 color: [1.0, 0.5, 0.0],
2856 direction: [0.0, -1.0, 0.0],
2857 },
2858 VmdParsedLightFrame {
2859 frame: 10,
2860 color: [0.0, 0.0, 1.0],
2861 direction: [1.0, 0.0, 0.0],
2862 },
2863 ];
2864
2865 let light = sample_vmd_light_frames(&frames, 20.0).unwrap();
2866
2867 assert_vec3_near(light.color, [0.5, 0.25, 0.5]);
2868 assert_vec3_near(light.direction, [0.5, -0.5, 0.0]);
2869 }
2870
2871 #[test]
2872 fn samples_vmd_self_shadow_frames_linearly_with_stepped_mode() {
2873 let frames = vec![
2874 VmdParsedSelfShadowFrame {
2875 frame: 10,
2876 mode: 1,
2877 distance: 20.0,
2878 },
2879 VmdParsedSelfShadowFrame {
2880 frame: 30,
2881 mode: 2,
2882 distance: 60.0,
2883 },
2884 ];
2885
2886 let middle = sample_vmd_self_shadow_frames(&frames, 20.0).unwrap();
2887 assert_eq!(middle.mode, 1);
2888 assert_near(middle.distance, 40.0);
2889
2890 let last = sample_vmd_self_shadow_frames(&frames, 30.0).unwrap();
2891 assert_eq!(last.mode, 2);
2892 assert_near(last.distance, 60.0);
2893 }
2894
2895 #[test]
2896 fn roundtrip_bone_frame_parse_export_parse() {
2897 let interp = [20u8; 64];
2898 let vmd = make_vmd_bytes(
2899 "miku",
2900 &[("arm", 30, [1.0, 0.5, -0.5], [0.0, 0.0, 0.0, 1.0], interp)],
2901 &[],
2902 &[],
2903 );
2904 let parsed = parse_vmd_animation(&vmd).unwrap();
2905 assert_eq!(parsed.bone_frames.len(), 1);
2906 assert_eq!(parsed.bone_frames[0].bone_name, "arm");
2907 assert_eq!(parsed.bone_frames[0].frame, 30);
2908 assert_eq!(parsed.bone_frames[0].translation, [1.0, 0.5, -0.5]);
2909 assert_eq!(parsed.bone_frames[0].rotation, [0.0, 0.0, 0.0, 1.0]);
2910 assert_eq!(parsed.bone_frames[0].interpolation, vec![20u8; 64]);
2911
2912 let exported = export_vmd_animation(&parsed);
2913 let reparsed = parse_vmd_animation(&exported).unwrap();
2914 assert_eq!(reparsed.metadata.model_name, parsed.metadata.model_name);
2915 assert_eq!(reparsed.metadata.max_frame, 30);
2916 assert_eq!(reparsed.bone_frames.len(), 1);
2917 assert_eq!(reparsed.bone_frames[0].bone_name, "arm");
2918 assert_eq!(reparsed.bone_frames[0].frame, 30);
2919 assert_eq!(reparsed.bone_frames[0].translation, [1.0, 0.5, -0.5]);
2920 assert_eq!(reparsed.bone_frames[0].rotation, [0.0, 0.0, 0.0, 1.0]);
2921 assert_eq!(reparsed.bone_frames[0].interpolation, vec![20u8; 64]);
2922 }
2923
2924 #[test]
2925 fn roundtrip_morph_frame_parse_export_parse() {
2926 let vmd = make_vmd_bytes("miku", &[], &[("blink", 15, 0.75)], &[]);
2927 let parsed = parse_vmd_animation(&vmd).unwrap();
2928 let exported = export_vmd_animation(&parsed);
2929 let reparsed = parse_vmd_animation(&exported).unwrap();
2930 assert_eq!(reparsed.morph_frames.len(), 1);
2931 assert_eq!(reparsed.morph_frames[0].morph_name, "blink");
2932 assert_eq!(reparsed.morph_frames[0].frame, 15);
2933 assert!((reparsed.morph_frames[0].weight - 0.75f32).abs() < 1e-6);
2934 }
2935
2936 #[test]
2937 fn roundtrip_property_ik_frames_parse_export_parse() {
2938 let vmd = make_vmd_bytes(
2939 "camera",
2940 &[],
2941 &[],
2942 &[(&[("leftIK", true), ("rightIK", false)], 20, true)],
2943 );
2944 let parsed = parse_vmd_animation(&vmd).unwrap();
2945 let exported = export_vmd_animation(&parsed);
2946 let reparsed = parse_vmd_animation(&exported).unwrap();
2947 assert_eq!(reparsed.property_frames.len(), 1);
2948 assert_eq!(reparsed.property_frames[0].frame, 20);
2949 assert!(reparsed.property_frames[0].visible);
2950 assert_eq!(reparsed.property_frames[0].ik_states.len(), 2);
2951 assert_eq!(reparsed.property_frames[0].ik_states[0].bone_name, "leftIK");
2952 assert!(reparsed.property_frames[0].ik_states[0].enabled);
2953 assert_eq!(
2954 reparsed.property_frames[0].ik_states[1].bone_name,
2955 "rightIK"
2956 );
2957 assert!(!reparsed.property_frames[0].ik_states[1].enabled);
2958 }
2959
2960 #[test]
2961 fn roundtrip_json_dto_bone_and_morph() {
2962 let interp = [20u8; 64];
2963 let vmd = make_vmd_bytes(
2964 "miku",
2965 &[("spine", 60, [0.0, 1.0, 0.0], [0.1, 0.2, 0.3, 0.9], interp)],
2966 &[("mouth", 30, 0.8)],
2967 &[],
2968 );
2969 let parsed = parse_vmd_animation(&vmd).unwrap();
2970 let json = serde_json::to_string(&parsed).unwrap();
2971 let from_json: VmdParsedAnimation = serde_json::from_str(&json).unwrap();
2972 let exported = export_vmd_animation(&from_json);
2973 let reparsed = parse_vmd_animation(&exported).unwrap();
2974
2975 assert_eq!(
2976 reparsed.bone_frames[0].bone_name,
2977 parsed.bone_frames[0].bone_name
2978 );
2979 assert_eq!(reparsed.bone_frames[0].frame, parsed.bone_frames[0].frame);
2980 assert_eq!(
2981 reparsed.bone_frames[0].translation,
2982 parsed.bone_frames[0].translation
2983 );
2984 assert_eq!(
2985 reparsed.bone_frames[0].rotation,
2986 parsed.bone_frames[0].rotation
2987 );
2988 assert_eq!(
2989 reparsed.morph_frames[0].morph_name,
2990 parsed.morph_frames[0].morph_name
2991 );
2992 assert_eq!(reparsed.morph_frames[0].frame, parsed.morph_frames[0].frame);
2993 assert!((reparsed.morph_frames[0].weight - parsed.morph_frames[0].weight).abs() < 1e-6);
2994 }
2995
2996 fn expected_sjis_name_bytes(value: &str, len: usize) -> Vec<u8> {
2997 let (encoded, _, _) = encoding_rs::SHIFT_JIS.encode(value);
2998 encoded.as_ref()[..encoded.len().min(len)].to_vec()
2999 }
3000
3001 #[test]
3002 fn export_json_dto_encodes_shift_jis_when_raw_name_bytes_are_missing() {
3003 let animation = vmd_parsed_animation(
3004 "初音ミク".to_owned(),
3005 Vec::new(),
3006 30,
3007 VmdParsedSections {
3008 bone_frames: vec![VmdParsedBoneFrame {
3009 bone_name: "左足".to_owned(),
3010 bone_name_bytes: Vec::new(),
3011 frame: 10,
3012 translation: [1.0, 2.0, 3.0],
3013 rotation: [0.0, 0.0, 0.0, 1.0],
3014 interpolation: vec![20; 64],
3015 }],
3016 morph_frames: vec![VmdParsedMorphFrame {
3017 morph_name: "笑い".to_owned(),
3018 morph_name_bytes: Vec::new(),
3019 frame: 20,
3020 weight: 0.75,
3021 }],
3022 camera_frames: Vec::new(),
3023 light_frames: Vec::new(),
3024 self_shadow_frames: Vec::new(),
3025 property_frames: vec![VmdParsedPropertyFrame {
3026 frame: 30,
3027 visible: true,
3028 ik_states: vec![VmdParsedIkState {
3029 bone_name: "右足IK".to_owned(),
3030 bone_name_bytes: Vec::new(),
3031 enabled: false,
3032 }],
3033 }],
3034 },
3035 );
3036 let json = serde_json::to_string(&animation).unwrap();
3037 assert!(!json.contains("modelNameBytes"));
3038 assert!(!json.contains("boneNameBytes"));
3039 assert!(!json.contains("morphNameBytes"));
3040
3041 let from_json: VmdParsedAnimation = serde_json::from_str(&json).unwrap();
3042 let exported = export_vmd_animation(&from_json);
3043 let reparsed = parse_vmd_animation(&exported).unwrap();
3044
3045 assert_eq!(reparsed.metadata.model_name, "初音ミク");
3046 assert!(!reparsed.metadata.model_name_bytes.is_empty());
3047 assert_eq!(
3048 reparsed.metadata.model_name_bytes,
3049 expected_sjis_name_bytes("初音ミク", 20)
3050 );
3051 assert_eq!(reparsed.bone_frames[0].bone_name, "左足");
3052 assert!(!reparsed.bone_frames[0].bone_name_bytes.is_empty());
3053 assert_eq!(
3054 reparsed.bone_frames[0].bone_name_bytes,
3055 expected_sjis_name_bytes("左足", 15)
3056 );
3057 assert_eq!(reparsed.morph_frames[0].morph_name, "笑い");
3058 assert!(!reparsed.morph_frames[0].morph_name_bytes.is_empty());
3059 assert_eq!(
3060 reparsed.morph_frames[0].morph_name_bytes,
3061 expected_sjis_name_bytes("笑い", 15)
3062 );
3063 assert_eq!(reparsed.property_frames[0].ik_states[0].bone_name, "右足IK");
3064 assert!(
3065 !reparsed.property_frames[0].ik_states[0]
3066 .bone_name_bytes
3067 .is_empty()
3068 );
3069 assert_eq!(
3070 reparsed.property_frames[0].ik_states[0].bone_name_bytes,
3071 expected_sjis_name_bytes("右足IK", 20)
3072 );
3073 assert!(!reparsed.property_frames[0].ik_states[0].enabled);
3074 }
3075
3076 #[test]
3077 fn export_json_dto_uses_encoding_rs_replacement_for_non_shift_jis_names() {
3078 let animation = vmd_parsed_animation(
3079 "miku".to_owned(),
3080 Vec::new(),
3081 1,
3082 VmdParsedSections {
3083 bone_frames: vec![VmdParsedBoneFrame {
3084 bone_name: "左足🧪".to_owned(),
3085 bone_name_bytes: Vec::new(),
3086 frame: 1,
3087 translation: [0.0, 0.0, 0.0],
3088 rotation: [0.0, 0.0, 0.0, 1.0],
3089 interpolation: vec![20; 64],
3090 }],
3091 morph_frames: Vec::new(),
3092 camera_frames: Vec::new(),
3093 light_frames: Vec::new(),
3094 self_shadow_frames: Vec::new(),
3095 property_frames: Vec::new(),
3096 },
3097 );
3098 let json = serde_json::to_string(&animation).unwrap();
3099 let from_json: VmdParsedAnimation = serde_json::from_str(&json).unwrap();
3100 let exported = export_vmd_animation(&from_json);
3101 let reparsed = parse_vmd_animation(&exported).unwrap();
3102
3103 assert_eq!(
3104 reparsed.bone_frames[0].bone_name_bytes,
3105 expected_sjis_name_bytes("左足🧪", 15)
3106 );
3107 assert_eq!(
3108 reparsed.bone_frames[0].bone_name,
3109 encoding_rs::SHIFT_JIS
3110 .decode(&expected_sjis_name_bytes("左足🧪", 15))
3111 .0
3112 .trim()
3113 );
3114 }
3115}