1use std::collections::HashMap;
2
3use glam::{Mat4, Quat};
4use serde::{Deserialize, Serialize};
5
6use mmd_anim_runtime::{
7 AppendTransformInit, BoneIndex, BoneInit, BoneMorphOffset, GroupMorphOffset, IkAngleLimit,
8 IkLinkInit, IkSolverInit, LocalAxis, ModelArena, MorphIndex, MorphInit, MorphOffsetSpan,
9 RuntimeAppendTransformDescriptorV1, RuntimeBoneDescriptorV1,
10 RuntimeBoneMorphOffsetDescriptorV1, RuntimeGroupMorphOffsetDescriptorV1,
11 RuntimeIkLinkDescriptorV1, RuntimeIkSolverDescriptorV1, RuntimeModelDescriptorV1,
12 RuntimeMorphDescriptorV1, build_morph_init_from_offsets, compile_runtime_model_descriptor_v1,
13};
14
15use crate::binary::{
16 ByteReader, write_f32_le as write_f32, write_f32_slice_le as write_f32_slice,
17 write_i32_le as write_i32, write_u16_le as write_u16,
18};
19use crate::error::ImportError;
20use crate::sjis::encode_sjis;
21
22type Reader<'a> = ByteReader<'a>;
23
24mod json_f32;
25
26const PMX_MAGIC: [u8; 4] = [0x50, 0x4D, 0x58, 0x20];
27
28const BONE_FLAG_TAIL_INDEX: u16 = 0x0001;
29const BONE_FLAG_IK: u16 = 0x0020;
30const BONE_FLAG_LOCAL_APPEND: u16 = 0x0080;
31const BONE_FLAG_APPEND_ROTATE: u16 = 0x0100;
32const BONE_FLAG_APPEND_TRANSLATE: u16 = 0x0200;
33const BONE_FLAG_FIXED_AXIS: u16 = 0x0400;
34const BONE_FLAG_LOCAL_AXIS: u16 = 0x0800;
35const BONE_FLAG_TRANSFORM_AFTER_PHYSICS: u16 = 0x1000;
36const BONE_FLAG_EXTERNAL_PARENT: u16 = 0x2000;
37const PMX_SKINNING_MODE_BDEF1: &str = "bdef1";
38const PMX_SKINNING_MODE_BDEF2: &str = "bdef2";
39const PMX_SKINNING_MODE_BDEF4: &str = "bdef4";
40const PMX_SKINNING_MODE_SDEF: &str = "sdef";
41const PMX_SKINNING_MODE_QDEF: &str = "qdef";
42
43fn decode_utf16le_lossy(bytes: &[u8]) -> String {
44 let end = bytes.len().saturating_sub(bytes.len() % 2);
45 let units = bytes[..end]
46 .chunks_exact(2)
47 .map(|chunk| u16::from_le_bytes([chunk[0], chunk[1]]))
48 .take_while(|&code| code != 0);
49 std::char::decode_utf16(units)
50 .map(|item| item.unwrap_or(char::REPLACEMENT_CHARACTER))
51 .collect()
52}
53
54#[derive(Debug, Clone, Copy, PartialEq, Eq)]
55pub enum TextEncoding {
56 Utf16Le = 0,
57 Utf8 = 1,
58}
59
60#[derive(Debug, Clone)]
61pub struct PmxHeader {
62 pub version: f32,
63 pub encoding: TextEncoding,
64 pub extra_uv_count: u8,
65 pub vertex_index_size: u8,
66 pub texture_index_size: u8,
67 pub material_index_size: u8,
68 pub bone_index_size: u8,
69 pub morph_index_size: u8,
70 pub rigidbody_index_size: u8,
71}
72
73impl<'a> ByteReader<'a> {
74 fn read_vec2_array(&mut self) -> Result<[f32; 2], ImportError> {
75 Ok([self.read_f32_le()?, self.read_f32_le()?])
76 }
77
78 fn read_vec3_array(&mut self) -> Result<[f32; 3], ImportError> {
79 Ok([
80 self.read_f32_le()?,
81 self.read_f32_le()?,
82 self.read_f32_le()?,
83 ])
84 }
85
86 fn read_vec4_array(&mut self) -> Result<[f32; 4], ImportError> {
87 Ok([
88 self.read_f32_le()?,
89 self.read_f32_le()?,
90 self.read_f32_le()?,
91 self.read_f32_le()?,
92 ])
93 }
94
95 fn read_sized_index(&mut self, size: u8) -> Result<i32, ImportError> {
96 match size {
97 1 => Ok(self.read_u8()? as i8 as i32),
98 2 => Ok({
99 let b = self.read_bytes(2)?;
100 i16::from_le_bytes([b[0], b[1]]) as i32
101 }),
102 4 => self.read_i32_le(),
103 _ => Err(ImportError::InvalidIndexSize(size)),
104 }
105 }
106
107 fn read_index(&mut self, header: &PmxHeader) -> Result<i32, ImportError> {
108 self.read_sized_index(header.bone_index_size)
109 }
110
111 fn read_vertex_index(&mut self, size: u8) -> Result<u32, ImportError> {
112 match size {
113 1 => Ok(self.read_u8()? as u32),
114 2 => Ok(self.read_u16_le()? as u32),
115 4 => {
116 let raw = self.read_i32_le()?;
117 if raw < 0 {
118 Err(ImportError::SectionOverflow)
119 } else {
120 Ok(raw as u32)
121 }
122 }
123 _ => Err(ImportError::InvalidIndexSize(size)),
124 }
125 }
126
127 fn read_string(&mut self, encoding: TextEncoding) -> Result<String, ImportError> {
128 let len = self.read_i32_le()?;
129 if len <= 0 {
130 return Ok(String::new());
131 }
132 let bytes = self.read_bytes(len as usize)?;
133 match encoding {
134 TextEncoding::Utf8 => {
135 let end = bytes.iter().position(|&b| b == 0).unwrap_or(bytes.len());
136 Ok(String::from_utf8_lossy(&bytes[..end]).into_owned())
137 }
138 TextEncoding::Utf16Le => Ok(decode_utf16le_lossy(bytes)),
139 }
140 }
141
142 fn read_string_owned(
143 &mut self,
144 encoding: TextEncoding,
145 ) -> Result<(Vec<u8>, String), ImportError> {
146 let len = self.read_i32_le()?;
147 if len <= 0 {
148 return Ok((Vec::new(), String::new()));
149 }
150 let bytes = self.read_bytes(len as usize)?.to_vec();
151 let decoded = match encoding {
152 TextEncoding::Utf8 => {
153 let end = bytes.iter().position(|&b| b == 0).unwrap_or(bytes.len());
154 String::from_utf8_lossy(&bytes[..end]).into_owned()
155 }
156 TextEncoding::Utf16Le => decode_utf16le_lossy(&bytes),
157 };
158 Ok((bytes, decoded))
159 }
160
161 fn read_bone_index(&mut self, header: &PmxHeader) -> Result<Option<BoneIndex>, ImportError> {
162 let raw = self.read_sized_index(header.bone_index_size)?;
163 if raw < 0 {
164 Ok(None)
165 } else {
166 Ok(Some(BoneIndex(raw as u32)))
167 }
168 }
169}
170
171pub fn read_header(data: &[u8]) -> Result<(PmxHeader, usize), ImportError> {
172 let mut r = Reader::new(data);
173
174 let magic = r.read_bytes(4)?;
175 if magic != PMX_MAGIC {
176 return Err(ImportError::InvalidPmxMagic);
177 }
178
179 let version = r.read_f32_le()?;
180 if !(2.0..=2.2).contains(&version) {
181 return Err(ImportError::UnsupportedPmxVersion(version));
182 }
183
184 let data_count = r.read_u8()?;
185 if data_count < 8 {
186 return Err(ImportError::InvalidEncoding(data_count));
187 }
188
189 let encoding_byte = r.read_u8()?;
190 let encoding = match encoding_byte {
191 0 => TextEncoding::Utf16Le,
192 1 => TextEncoding::Utf8,
193 _ => return Err(ImportError::InvalidEncoding(encoding_byte)),
194 };
195
196 let extra_uv_count = r.read_u8()?;
197 let vertex_index_size = r.read_u8()?;
198 let texture_index_size = r.read_u8()?;
199 let material_index_size = r.read_u8()?;
200 let bone_index_size = r.read_u8()?;
201 let morph_index_size = r.read_u8()?;
202 let rigidbody_index_size = r.read_u8()?;
203 for size in [
204 vertex_index_size,
205 texture_index_size,
206 material_index_size,
207 bone_index_size,
208 morph_index_size,
209 rigidbody_index_size,
210 ] {
211 if !matches!(size, 1 | 2 | 4) {
212 return Err(ImportError::InvalidIndexSize(size));
213 }
214 }
215 if data_count > 8 {
216 r.skip((data_count - 8) as usize)?;
217 }
218
219 let header = PmxHeader {
220 version,
221 encoding,
222 extra_uv_count,
223 vertex_index_size,
224 texture_index_size,
225 material_index_size,
226 bone_index_size,
227 morph_index_size,
228 rigidbody_index_size,
229 };
230
231 Ok((header, r.pos))
232}
233
234pub fn skip_model_info(data: &[u8], header: &PmxHeader, pos: usize) -> Result<usize, ImportError> {
235 let mut r = Reader { data, pos };
236 let _model_name = r.read_string(header.encoding)?;
237 let _english_name = r.read_string(header.encoding)?;
238 let _comment = r.read_string(header.encoding)?;
239 let _english_comment = r.read_string(header.encoding)?;
240 Ok(r.pos)
241}
242
243pub fn skip_vertices(data: &[u8], header: &PmxHeader, pos: usize) -> Result<usize, ImportError> {
244 let mut r = Reader { data, pos };
245 let count = r.read_i32_le()?;
246 if count < 0 {
247 return Err(ImportError::SectionOverflow);
248 }
249 let count = count as usize;
250
251 for _ in 0..count {
252 r.skip(12)?;
253 r.skip(12)?;
254 r.skip(8)?;
255 r.skip(header.extra_uv_count as usize * 16)?;
256 let vertex_deform_type = r.read_u8()?;
257
258 let bone_indices: usize = match vertex_deform_type {
259 0 => 1,
260 1 => 2,
261 2 | 4 => 4,
262 3 => 2,
263 _ => return Err(ImportError::SectionOverflow),
264 };
265 for _ in 0..bone_indices {
266 r.read_sized_index(header.bone_index_size)?;
267 }
268
269 let weight_count: usize = match vertex_deform_type {
270 0 => 0,
271 1 => 1,
272 2 | 4 => 4,
273 3 => 1,
274 _ => return Err(ImportError::SectionOverflow),
275 };
276 r.skip(weight_count * 4)?;
277
278 if vertex_deform_type == 3 {
279 r.skip(36)?;
280 }
281
282 r.skip(4)?;
283 }
284
285 Ok(r.pos)
286}
287
288pub fn skip_faces(data: &[u8], vertex_index_size: u8, pos: usize) -> Result<usize, ImportError> {
289 let mut r = Reader { data, pos };
290 let count = r.read_i32_le()?;
291 if count < 0 {
292 return Err(ImportError::SectionOverflow);
293 }
294 let count = count as usize;
295 let bytes = count
296 .checked_mul(vertex_index_size as usize)
297 .ok_or(ImportError::SectionOverflow)?;
298 r.skip(bytes)?;
299 Ok(r.pos)
300}
301
302pub fn skip_textures(
303 data: &[u8],
304 encoding: TextEncoding,
305 pos: usize,
306) -> Result<usize, ImportError> {
307 let mut r = Reader { data, pos };
308 let count = r.read_i32_le()?;
309 if count < 0 {
310 return Err(ImportError::SectionOverflow);
311 }
312 for _ in 0..count {
313 let _path = r.read_string(encoding)?;
314 }
315 Ok(r.pos)
316}
317
318pub fn skip_materials(data: &[u8], header: &PmxHeader, pos: usize) -> Result<usize, ImportError> {
319 let mut r = Reader { data, pos };
320 let count = r.read_i32_le()?;
321 if count < 0 {
322 return Err(ImportError::SectionOverflow);
323 }
324 for _ in 0..count {
325 let _name = r.read_string(header.encoding)?;
326 let _english_name = r.read_string(header.encoding)?;
327 r.skip(16)?;
328 r.skip(16)?;
329 r.skip(12)?;
330 r.skip(1)?;
331 r.skip(16)?;
332 r.skip(4)?;
333 let _texture = r.read_sized_index(header.texture_index_size)?;
334 let _sphere_tex = r.read_sized_index(header.texture_index_size)?;
335 r.skip(1)?;
336 let _toon = r.read_u8()?;
337 if _toon == 0 {
338 r.read_sized_index(header.texture_index_size)?;
339 } else {
340 r.skip(1)?;
341 }
342 let _memo = r.read_string(header.encoding)?;
343 let _face_count = r.read_i32_le()?;
344 }
345 Ok(r.pos)
346}
347
348pub fn skip_morphs(data: &[u8], header: &PmxHeader, pos: usize) -> Result<usize, ImportError> {
349 let mut r = Reader { data, pos };
350 let count = r.read_i32_le()?;
351 if count < 0 {
352 return Err(ImportError::SectionOverflow);
353 }
354 for _ in 0..count {
355 let _name = r.read_string(header.encoding)?;
356 let _english_name = r.read_string(header.encoding)?;
357 r.skip(1)?;
358 let _morph_type = r.read_u8()?;
359 let _offset_count = r.read_i32_le()?;
360 match _morph_type {
361 0 => {
362 for _ in 0.._offset_count {
363 r.read_sized_index(header.morph_index_size)?;
364 r.skip(4)?;
365 }
366 }
367 1 => {
368 for _ in 0.._offset_count {
369 r.read_sized_index(header.vertex_index_size)?;
370 r.skip(12)?;
371 }
372 }
373 2 => {
374 for _ in 0.._offset_count {
375 r.read_sized_index(header.bone_index_size)?;
376 r.skip(12)?;
377 r.skip(16)?;
378 }
379 }
380 3..=7 => {
381 for _ in 0.._offset_count {
382 r.read_sized_index(header.vertex_index_size)?;
383 r.skip(16)?;
384 }
385 }
386 8 => {
387 for _ in 0.._offset_count {
388 r.read_sized_index(header.material_index_size)?;
389 r.skip(1)?;
390 r.skip(16)?;
391 r.skip(12)?;
392 r.skip(4)?;
393 r.skip(12)?;
394 r.skip(16)?;
395 r.skip(4)?;
396 r.skip(16)?;
397 r.skip(16)?;
398 r.skip(16)?;
399 }
400 }
401 9 => {
402 for _ in 0.._offset_count {
403 r.read_sized_index(header.rigidbody_index_size)?;
404 r.skip(1)?;
405 r.skip(12)?;
406 r.skip(12)?;
407 }
408 }
409 _ => return Err(ImportError::SectionOverflow),
410 }
411 }
412 Ok(r.pos)
413}
414
415#[derive(Debug, Clone)]
416pub struct PmxMorphNames {
417 pub name_bytes: Vec<Vec<u8>>,
418 pub names: Vec<String>,
419}
420
421pub fn read_morph_names(
422 data: &[u8],
423 header: &PmxHeader,
424 pos: usize,
425) -> Result<(PmxMorphNames, usize), ImportError> {
426 let mut r = Reader { data, pos };
427 let count = r.read_i32_le()?;
428 if count < 0 {
429 return Err(ImportError::SectionOverflow);
430 }
431 let count = count as usize;
432 r.require_record_bytes(count, 1)?;
433
434 let mut name_bytes = Vec::with_capacity(count);
435 let mut names = Vec::with_capacity(count);
436
437 for _ in 0..count {
438 let (raw, name) = r.read_string_owned(header.encoding)?;
439 let _english_name = r.read_string(header.encoding)?;
440 name_bytes.push(raw);
441 names.push(name);
442
443 r.skip(1)?;
444 let morph_type = r.read_u8()?;
445 let offset_count = r.read_i32_le()?;
446 if offset_count < 0 {
447 return Err(ImportError::SectionOverflow);
448 }
449 let offset_count = offset_count as usize;
450
451 let per_offset = match morph_type {
452 0 => header.morph_index_size as usize + 4,
453 1 => header.vertex_index_size as usize + 12,
454 2 => header.bone_index_size as usize + 12 + 16,
455 3..=7 => header.vertex_index_size as usize + 16,
456 8 => header.material_index_size as usize + 1 + 16 + 12 + 4 + 12 + 16 + 4 + 16 + 16 + 16,
457 9 => header.rigidbody_index_size as usize + 1 + 12 + 12,
458 _ => return Err(ImportError::SectionOverflow),
459 };
460 r.skip(
461 offset_count
462 .checked_mul(per_offset)
463 .ok_or(ImportError::SectionOverflow)?,
464 )?;
465 }
466
467 Ok((PmxMorphNames { name_bytes, names }, r.pos))
468}
469
470fn read_runtime_morph_descriptor(
475 data: &[u8],
476 header: &PmxHeader,
477 pos: usize,
478) -> Result<(PmxMorphNames, RuntimeMorphDescriptorV1, usize), ImportError> {
479 let mut r = Reader { data, pos };
480 let count = r.read_i32_le()?;
481 if count < 0 {
482 return Err(ImportError::SectionOverflow);
483 }
484 let count = count as usize;
485 r.require_record_bytes(count, 1)?;
486
487 let mut name_bytes = Vec::with_capacity(count);
488 let mut names = Vec::with_capacity(count);
489
490 let mut bone_offsets = Vec::new();
491 let mut group_offsets = Vec::new();
492
493 for morph_index in 0..count {
494 let (raw, name) = r.read_string_owned(header.encoding)?;
495 let _english_name = r.read_string(header.encoding)?;
496 name_bytes.push(raw);
497 names.push(name);
498
499 r.skip(1)?;
500 let morph_type = r.read_u8()?;
501 let offset_count = r.read_i32_le()?;
502 if offset_count < 0 {
503 return Err(ImportError::SectionOverflow);
504 }
505 let offset_count = offset_count as usize;
506
507 match morph_type {
508 0 => {
509 for _ in 0..offset_count {
510 let child_raw = r.read_sized_index(header.morph_index_size)?;
511 if child_raw < 0 {
512 return Err(ImportError::SectionOverflow);
513 }
514 let ratio = r.read_f32_le()?;
515 group_offsets.push(RuntimeGroupMorphOffsetDescriptorV1 {
516 morph_index: MorphIndex(morph_index as u32),
517 child_morph: MorphIndex(child_raw as u32),
518 ratio,
519 });
520 }
521 }
522 2 => {
523 for _ in 0..offset_count {
524 let bone_raw = r.read_sized_index(header.bone_index_size)?;
525 if bone_raw < 0 {
526 return Err(ImportError::SectionOverflow);
527 }
528 let pos = r.read_vec3()?;
529 let qx = r.read_f32_le()?;
530 let qy = r.read_f32_le()?;
531 let qz = r.read_f32_le()?;
532 let qw = r.read_f32_le()?;
533 bone_offsets.push(RuntimeBoneMorphOffsetDescriptorV1 {
534 morph_index: MorphIndex(morph_index as u32),
535 target_bone: BoneIndex(bone_raw as u32),
536 position_offset: pos,
537 rotation_offset: Quat::from_xyzw(qx, qy, qz, qw),
538 });
539 }
540 }
541 _ => {
542 let per_offset = match morph_type {
543 1 => header.vertex_index_size as usize + 12,
544 3..=7 => header.vertex_index_size as usize + 16,
545 8 => {
546 header.material_index_size as usize
547 + 1
548 + 16
549 + 12
550 + 4
551 + 12
552 + 16
553 + 4
554 + 16
555 + 16
556 + 16
557 }
558 9 => header.rigidbody_index_size as usize + 1 + 12 + 12,
559 _ => return Err(ImportError::SectionOverflow),
560 };
561 r.skip(
562 offset_count
563 .checked_mul(per_offset)
564 .ok_or(ImportError::SectionOverflow)?,
565 )?;
566 }
567 }
568 }
569
570 Ok((
571 PmxMorphNames { name_bytes, names },
572 RuntimeMorphDescriptorV1 {
573 morph_count: count as u32,
574 bone_offsets,
575 group_offsets,
576 },
577 r.pos,
578 ))
579}
580
581pub fn read_morph_offsets(
585 data: &[u8],
586 header: &PmxHeader,
587 pos: usize,
588) -> Result<(PmxMorphNames, MorphInit, usize), ImportError> {
589 let (names, descriptor, pos) = read_runtime_morph_descriptor(data, header, pos)?;
590 let morph_count = descriptor.morph_count as usize;
591 let bone_offsets = descriptor
592 .bone_offsets
593 .iter()
594 .map(|offset| {
595 (
596 offset.morph_index,
597 BoneMorphOffset {
598 target_bone: offset.target_bone,
599 position_offset: offset.position_offset,
600 rotation_offset: offset.rotation_offset,
601 },
602 )
603 })
604 .collect();
605 let group_offsets = descriptor
606 .group_offsets
607 .iter()
608 .map(|offset| {
609 (
610 offset.morph_index,
611 GroupMorphOffset {
612 child_morph: offset.child_morph,
613 ratio: offset.ratio,
614 },
615 )
616 })
617 .collect();
618 let mut morph =
619 build_morph_init_from_offsets(descriptor.morph_count, bone_offsets, group_offsets)
620 .map_err(ImportError::ModelBuildFailed)?;
621 morph.vertex_spans = vec![MorphOffsetSpan::default(); morph_count];
622 Ok((names, morph, pos))
623}
624
625#[derive(Debug, Clone)]
626pub struct PmxBoneImport {
627 pub bones: Vec<BoneInit>,
628 pub ik_solvers: Vec<IkSolverInit>,
629 pub append_transforms: Vec<AppendTransformInit>,
630 pub bone_name_bytes: Vec<Vec<u8>>,
631 pub bone_names: Vec<String>,
632}
633
634struct PmxBoneRead {
635 descriptor: RuntimeModelDescriptorV1,
636 bone_name_bytes: Vec<Vec<u8>>,
637 bone_names: Vec<String>,
638}
639
640fn read_bones_with_local_axes(
641 data: &[u8],
642 header: &PmxHeader,
643 pos: usize,
644) -> Result<(PmxBoneRead, usize), ImportError> {
645 let mut r = Reader { data, pos };
646 let count = r.read_i32_le()?;
647 if count < 0 {
648 return Err(ImportError::SectionOverflow);
649 }
650 let count = count as usize;
651 r.require_record_bytes(count, 1)?;
652
653 let mut bones = Vec::with_capacity(count);
654 let mut ik_solvers = Vec::new();
655 let mut append_transforms = Vec::new();
656
657 let mut bone_name_bytes = Vec::with_capacity(count);
658 let mut bone_names = Vec::with_capacity(count);
659
660 for _bone_i in 0..count {
661 let (name_bytes, name) = r.read_string_owned(header.encoding)?;
662 let _english_name = r.read_string(header.encoding)?;
663 let position = r.read_vec3()?;
664 let parent = r.read_bone_index(header)?;
665 let transform_order = r.read_i32_le()?;
666 let flags = r.read_u16_le()?;
667
668 if flags & BONE_FLAG_TAIL_INDEX != 0 {
669 r.read_index(header)?;
670 } else {
671 r.read_vec3()?;
672 }
673
674 let has_append =
675 flags & BONE_FLAG_APPEND_ROTATE != 0 || flags & BONE_FLAG_APPEND_TRANSLATE != 0;
676
677 if has_append {
678 let append_parent = r.read_bone_index(header)?;
679 let append_ratio = r.read_f32_le()?;
680
681 if let Some(source) = append_parent
685 && append_ratio.is_finite()
686 {
687 let target_idx = bones.len();
688 append_transforms.push(RuntimeAppendTransformDescriptorV1 {
689 target_bone: BoneIndex(target_idx as u32),
690 source_bone: source,
691 ratio: append_ratio,
692 affect_rotation: flags & BONE_FLAG_APPEND_ROTATE != 0,
693 affect_translation: flags & BONE_FLAG_APPEND_TRANSLATE != 0,
694 local: flags & BONE_FLAG_LOCAL_APPEND != 0,
695 });
696 }
697 }
698
699 let fixed_axis = if flags & BONE_FLAG_FIXED_AXIS != 0 {
700 let axis = r.read_vec3()?;
701 (axis.is_finite() && axis.length_squared() > f32::EPSILON).then_some(axis)
702 } else {
703 None
704 };
705
706 let local_axis = if flags & BONE_FLAG_LOCAL_AXIS != 0 {
707 let x = r.read_vec3()?;
708 let z = r.read_vec3()?;
709 let axis = LocalAxis::new(x, z);
710 axis.basis_quat().is_some().then_some(axis)
711 } else {
712 None
713 };
714
715 if flags & BONE_FLAG_EXTERNAL_PARENT != 0 {
716 r.read_i32_le()?;
717 }
718
719 if flags & BONE_FLAG_IK != 0 {
720 let ik_target = r.read_bone_index(header)?;
721 let ik_loop_count = r.read_i32_le()?;
722 let ik_limit_angle = r.read_f32_le()?;
723 let link_count = r.read_i32_le()?;
724 if link_count < 0 {
725 return Err(ImportError::SectionOverflow);
726 }
727 r.require_record_bytes(link_count as usize, header.bone_index_size as usize + 1)?;
728
729 let mut links = Vec::with_capacity(link_count as usize);
730 for _ in 0..link_count {
731 let link_bone = r.read_bone_index(header)?.unwrap_or(BoneIndex(0));
735 let has_limit = r.read_u8()?;
736 let angle_limit = if has_limit != 0 {
737 let min = r.read_vec3()?;
738 let max = r.read_vec3()?;
739 (min.is_finite() && max.is_finite() && min.cmple(max).bitmask() == 0b111)
743 .then(|| IkAngleLimit::new(min, max))
744 } else {
745 None
746 };
747 links.push(RuntimeIkLinkDescriptorV1 {
748 bone: link_bone,
749 angle_limit,
750 });
751 }
752
753 let ik_bone = BoneIndex(bones.len() as u32);
754 if let Some(target_bone) = ik_target
759 && ik_loop_count > 0
760 && ik_limit_angle.is_finite()
761 && ik_limit_angle >= 0.0
762 {
763 ik_solvers.push(RuntimeIkSolverDescriptorV1 {
764 ik_bone,
765 target_bone,
766 links,
767 iteration_count: ik_loop_count as u32,
768 limit_angle: ik_limit_angle,
769 });
770 }
771 }
772
773 bone_name_bytes.push(name_bytes);
774 bone_names.push(name);
775
776 bones.push(RuntimeBoneDescriptorV1 {
777 parent,
778 rest_position: position,
779 transform_order,
780 transform_after_physics: flags & BONE_FLAG_TRANSFORM_AFTER_PHYSICS != 0,
781 fixed_axis,
782 local_axis,
783 });
784 }
785
786 Ok((
787 PmxBoneRead {
788 descriptor: RuntimeModelDescriptorV1 {
789 bones,
790 ik_solvers,
791 append_transforms,
792 ..RuntimeModelDescriptorV1::default()
793 },
794 bone_name_bytes,
795 bone_names,
796 },
797 r.pos,
798 ))
799}
800
801fn legacy_bone_import(read: &PmxBoneRead) -> PmxBoneImport {
805 let bones = read
806 .descriptor
807 .bones
808 .iter()
809 .map(|descriptor| {
810 let rest_position = descriptor
811 .parent
812 .and_then(|parent| read.descriptor.bones.get(parent.as_usize()))
813 .map_or(descriptor.rest_position, |parent| {
814 descriptor.rest_position - parent.rest_position
815 });
816 BoneInit {
817 parent: descriptor.parent,
818 rest_position,
819 inverse_bind_matrix: Mat4::from_translation((-descriptor.rest_position).into()),
820 transform_order: descriptor.transform_order,
821 transform_after_physics: descriptor.transform_after_physics,
822 fixed_axis: descriptor.fixed_axis,
823 enforce_fixed_axis: false,
826 }
827 })
828 .collect();
829 let ik_solvers = read
830 .descriptor
831 .ik_solvers
832 .iter()
833 .map(|solver| IkSolverInit {
834 ik_bone: solver.ik_bone,
835 target_bone: solver.target_bone,
836 links: solver
837 .links
838 .iter()
839 .map(|link| IkLinkInit {
840 bone: link.bone,
841 angle_limit: link.angle_limit,
842 })
843 .collect(),
844 iteration_count: solver.iteration_count,
845 limit_angle: solver.limit_angle,
846 })
847 .collect();
848 let append_transforms = read
849 .descriptor
850 .append_transforms
851 .iter()
852 .map(|append| AppendTransformInit {
853 target_bone: append.target_bone,
854 source_bone: append.source_bone,
855 ratio: append.ratio,
856 affect_rotation: append.affect_rotation,
857 affect_translation: append.affect_translation,
858 local: append.local,
859 })
860 .collect();
861 PmxBoneImport {
862 bones,
863 ik_solvers,
864 append_transforms,
865 bone_name_bytes: read.bone_name_bytes.clone(),
866 bone_names: read.bone_names.clone(),
867 }
868}
869
870pub fn read_bones(
871 data: &[u8],
872 header: &PmxHeader,
873 pos: usize,
874) -> Result<(PmxBoneImport, usize), ImportError> {
875 let (bone_read, pos) = read_bones_with_local_axes(data, header, pos)?;
876 Ok((legacy_bone_import(&bone_read), pos))
877}
878
879pub fn import_pmx_model(data: &[u8]) -> Result<PmxBoneImport, ImportError> {
880 let (header, pos) = read_header(data)?;
881 let pos = skip_model_info(data, &header, pos)?;
882 let pos = skip_vertices(data, &header, pos)?;
883 let pos = skip_faces(data, header.vertex_index_size, pos)?;
884 let pos = skip_textures(data, header.encoding, pos)?;
885 let pos = skip_materials(data, &header, pos)?;
886 let (bones, _pos) = read_bones(data, &header, pos)?;
887 Ok(bones)
888}
889
890#[derive(Debug, Clone, Serialize, Deserialize)]
891#[serde(rename_all = "camelCase")]
892pub struct PmxParsedModel {
893 pub metadata: PmxParsedMetadata,
894 pub geometry: PmxParsedGeometry,
895 pub materials: Vec<PmxParsedMaterial>,
896 pub skeleton: PmxParsedSkeleton,
897 pub morphs: Vec<PmxParsedMorph>,
898 pub display_frames: Vec<PmxParsedDisplayFrame>,
899 pub rigid_bodies: Vec<PmxParsedRigidBody>,
900 pub joints: Vec<PmxParsedJoint>,
901 pub soft_bodies: Vec<PmxParsedSoftBody>,
902 pub diagnostics: Vec<PmxParserDiagnostic>,
903}
904
905#[derive(Debug, Clone, Serialize, Deserialize)]
906#[serde(rename_all = "camelCase")]
907pub struct PmxParsedMetadata {
908 #[serde(default = "default_pmx_format")]
909 pub format: String,
910 #[serde(with = "json_f32")]
911 pub version: f32,
912 pub encoding: String,
913 pub name: String,
914 pub english_name: String,
915 pub comment: String,
916 pub english_comment: String,
917 pub counts: PmxParsedCounts,
918 pub index_sizes: PmxParsedIndexSizes,
919 pub additional_uv_count: u8,
920}
921
922#[derive(Debug, Clone, Serialize, Deserialize)]
923#[serde(rename_all = "camelCase")]
924pub struct PmxParsedCounts {
925 pub vertices: usize,
926 pub faces: usize,
927 pub materials: usize,
928 pub bones: usize,
929 pub morphs: usize,
930 pub display_frames: usize,
931 pub rigid_bodies: usize,
932 pub joints: usize,
933 pub soft_bodies: usize,
934}
935
936#[derive(Debug, Clone, Serialize, Deserialize)]
937pub struct PmxParsedIndexSizes {
938 pub vertex: u8,
939 pub texture: u8,
940 pub material: u8,
941 pub bone: u8,
942 pub morph: u8,
943 #[serde(rename = "rigidBody")]
944 pub rigid_body: u8,
945}
946
947#[derive(Debug, Clone, Serialize, Deserialize, Default)]
948#[serde(rename_all = "camelCase")]
949pub struct PmxParsedSdef {
950 #[serde(default)]
951 pub skinning_modes: Vec<String>,
952 #[serde(with = "json_f32::vec_f32")]
953 pub enabled: Vec<f32>,
954 #[serde(with = "json_f32::vec_f32")]
955 pub c: Vec<f32>,
956 #[serde(with = "json_f32::vec_f32")]
957 pub r0: Vec<f32>,
958 #[serde(with = "json_f32::vec_f32")]
959 pub r1: Vec<f32>,
960 #[serde(with = "json_f32::vec_f32")]
961 pub rw0: Vec<f32>,
962 #[serde(with = "json_f32::vec_f32")]
963 pub rw1: Vec<f32>,
964}
965
966#[derive(Debug, Clone, Serialize, Deserialize, Default)]
967#[serde(rename_all = "camelCase")]
968pub struct PmxParsedQdef {
969 #[serde(with = "json_f32::vec_f32")]
970 pub enabled: Vec<f32>,
971}
972
973#[derive(Debug, Clone, Serialize, Deserialize)]
974#[serde(rename_all = "camelCase")]
975pub struct PmxParsedGeometry {
976 #[serde(with = "json_f32::vec_f32")]
977 pub positions: Vec<f32>,
978 #[serde(with = "json_f32::vec_f32")]
979 pub normals: Vec<f32>,
980 #[serde(with = "json_f32::vec_f32")]
981 pub uvs: Vec<f32>,
982 #[serde(with = "json_f32::nested_vec")]
983 pub additional_uvs: Vec<Vec<f32>>,
984 pub indices: Vec<u32>,
985 pub skin_indices: Vec<u32>,
986 #[serde(with = "json_f32::vec_f32")]
987 pub skin_weights: Vec<f32>,
988 #[serde(with = "json_f32::vec_f32")]
989 pub edge_scale: Vec<f32>,
990 pub material_groups: Vec<PmxParsedMaterialGroup>,
991 pub sdef: PmxParsedSdef,
992 pub qdef: PmxParsedQdef,
993}
994
995#[derive(Debug, Clone, Serialize, Deserialize)]
996#[serde(rename_all = "camelCase")]
997pub struct PmxParsedMaterialGroup {
998 pub start: usize,
999 pub count: usize,
1000 pub material_index: usize,
1001}
1002
1003#[derive(Debug, Clone, Serialize, Deserialize)]
1004#[serde(rename_all = "camelCase")]
1005pub struct PmxParsedMaterial {
1006 pub name: String,
1007 pub english_name: String,
1008 pub texture_path: String,
1009 pub sphere_texture_path: String,
1010 pub sphere_mode: String,
1011 pub toon_texture_path: String,
1012 pub shared_toon_index: Option<u8>,
1013 #[serde(with = "json_f32::array4")]
1014 pub diffuse: [f32; 4],
1015 #[serde(with = "json_f32::array3")]
1016 pub specular: [f32; 3],
1017 #[serde(with = "json_f32")]
1018 pub specular_power: f32,
1019 #[serde(with = "json_f32::array3")]
1020 pub ambient: [f32; 3],
1021 #[serde(with = "json_f32::array4")]
1022 pub edge_color: [f32; 4],
1023 #[serde(with = "json_f32")]
1024 pub edge_size: f32,
1025 pub flags: PmxParsedMaterialFlags,
1026 pub face_count: i32,
1027}
1028
1029#[derive(Debug, Clone, Serialize, Deserialize)]
1030#[serde(rename_all = "camelCase")]
1031pub struct PmxParsedMaterialFlags {
1032 pub double_sided: bool,
1033 pub ground_shadow: bool,
1034 pub self_shadow_map: bool,
1035 pub self_shadow: bool,
1036 pub edge: bool,
1037 pub vertex_color: bool,
1038 pub point_draw: bool,
1039 pub line_draw: bool,
1040}
1041
1042#[derive(Debug, Clone, Serialize, Deserialize)]
1043pub struct PmxParsedSkeleton {
1044 pub bones: Vec<PmxParsedBone>,
1045}
1046
1047#[derive(Debug, Clone, Serialize, Deserialize)]
1048#[serde(rename_all = "camelCase")]
1049pub struct PmxParsedBone {
1050 pub name: String,
1051 pub english_name: String,
1052 pub parent_index: i32,
1053 pub layer: i32,
1054 #[serde(with = "json_f32::array3")]
1055 pub position: [f32; 3],
1056 pub tail_index: i32,
1057 #[serde(with = "json_f32::option_array3")]
1058 pub tail_position: Option<[f32; 3]>,
1059 pub flags: PmxParsedBoneFlags,
1060 pub append_transform: Option<PmxParsedAppendTransform>,
1061 #[serde(with = "json_f32::option_array3")]
1062 pub fixed_axis: Option<[f32; 3]>,
1063 pub local_axis: Option<PmxParsedLocalAxis>,
1064 pub external_parent_key: Option<i32>,
1065 pub ik: Option<PmxParsedIk>,
1066}
1067
1068#[derive(Debug, Clone, Serialize, Deserialize)]
1069#[serde(rename_all = "camelCase")]
1070pub struct PmxParsedBoneFlags {
1071 pub indexed_tail: bool,
1072 pub rotatable: bool,
1073 pub translatable: bool,
1074 pub visible: bool,
1075 pub enabled: bool,
1076 pub ik: bool,
1077 pub append_local: bool,
1078 pub append_rotate: bool,
1079 pub append_translate: bool,
1080 pub fixed_axis: bool,
1081 pub local_axis: bool,
1082 pub transform_after_physics: bool,
1083 pub external_parent_transform: bool,
1084}
1085
1086#[derive(Debug, Clone, Serialize, Deserialize)]
1087#[serde(rename_all = "camelCase")]
1088pub struct PmxParsedAppendTransform {
1089 pub parent_index: i32,
1090 #[serde(with = "json_f32")]
1091 pub weight: f32,
1092}
1093
1094#[derive(Debug, Clone, Serialize, Deserialize)]
1095pub struct PmxParsedLocalAxis {
1096 #[serde(with = "json_f32::array3")]
1097 pub x: [f32; 3],
1098 #[serde(with = "json_f32::array3")]
1099 pub z: [f32; 3],
1100}
1101
1102#[derive(Debug, Clone, Serialize, Deserialize)]
1103#[serde(rename_all = "camelCase")]
1104pub struct PmxParsedIk {
1105 pub target_index: i32,
1106 pub loop_count: i32,
1107 #[serde(with = "json_f32")]
1108 pub limit_angle: f32,
1109 pub links: Vec<PmxParsedIkLink>,
1110}
1111
1112#[derive(Debug, Clone, Serialize, Deserialize)]
1113#[serde(rename_all = "camelCase")]
1114pub struct PmxParsedIkLink {
1115 pub bone_index: i32,
1116 pub limits: Option<PmxParsedIkLimit>,
1117}
1118
1119#[derive(Debug, Clone, Serialize, Deserialize)]
1120pub struct PmxParsedIkLimit {
1121 #[serde(with = "json_f32::array3")]
1122 pub lower: [f32; 3],
1123 #[serde(with = "json_f32::array3")]
1124 pub upper: [f32; 3],
1125}
1126
1127#[derive(Debug, Clone, Serialize, Deserialize)]
1128#[serde(rename_all = "camelCase")]
1129pub struct PmxRigSpec {
1130 pub bone_count: usize,
1131 pub ik_chain_count: usize,
1132 pub grant_count: usize,
1133 pub bones: Vec<PmxRigSpecBone>,
1134 pub ik_chains: Vec<PmxRigSpecIkChain>,
1135 pub grants: Vec<PmxRigSpecGrant>,
1136}
1137
1138#[derive(Debug, Clone, Serialize, Deserialize)]
1139#[serde(rename_all = "camelCase")]
1140pub struct PmxRigSpecBone {
1141 pub name: String,
1142 pub name_bytes: String,
1143 pub parent_index: i32,
1144 #[serde(with = "json_f32::array3")]
1145 pub rest_position: [f32; 3],
1146 pub deform_layer: i32,
1147 #[serde(with = "json_f32::option_array3")]
1148 pub fixed_axis: Option<[f32; 3]>,
1149 pub local_axis: Option<PmxRigSpecLocalAxis>,
1150 pub transform_after_physics: bool,
1151}
1152
1153#[derive(Debug, Clone, Serialize, Deserialize)]
1154pub struct PmxRigSpecLocalAxis {
1155 #[serde(with = "json_f32::array3")]
1156 pub x: [f32; 3],
1157 #[serde(with = "json_f32::array3")]
1158 pub z: [f32; 3],
1159}
1160
1161#[derive(Debug, Clone, Serialize, Deserialize)]
1162#[serde(rename_all = "camelCase")]
1163pub struct PmxRigSpecIkChain {
1164 pub controller_bone_index: i32,
1165 pub target_bone_index: i32,
1166 pub iteration_count: u32,
1167 #[serde(with = "json_f32")]
1168 pub limit_angle: f32,
1169 pub links: Vec<PmxRigSpecIkLink>,
1170}
1171
1172#[derive(Debug, Clone, Serialize, Deserialize)]
1173#[serde(rename_all = "camelCase")]
1174pub struct PmxRigSpecIkLink {
1175 pub bone_index: i32,
1176 pub has_angle_limit: bool,
1177 #[serde(with = "json_f32::array3")]
1178 pub angle_limit_min: [f32; 3],
1179 #[serde(with = "json_f32::array3")]
1180 pub angle_limit_max: [f32; 3],
1181}
1182
1183#[derive(Debug, Clone, Serialize, Deserialize)]
1184#[serde(rename_all = "camelCase")]
1185pub struct PmxRigSpecGrant {
1186 pub target_bone_index: i32,
1187 pub source_bone_index: i32,
1188 #[serde(with = "json_f32")]
1189 pub ratio: f32,
1190 pub affect_rotation: bool,
1191 pub affect_translation: bool,
1192 pub local: bool,
1193}
1194
1195pub fn parse_pmx_rig_spec(data: &[u8]) -> Result<PmxRigSpec, ImportError> {
1196 let model = parse_pmx_model(data)?;
1197 Ok(pmx_model_to_rig_spec(&model))
1198}
1199
1200pub fn pmx_model_to_rig_spec(model: &PmxParsedModel) -> PmxRigSpec {
1201 let mut bones = Vec::with_capacity(model.skeleton.bones.len());
1202 let mut ik_chains = Vec::new();
1203 let mut grants = Vec::new();
1204
1205 for (index, bone) in model.skeleton.bones.iter().enumerate() {
1206 bones.push(PmxRigSpecBone {
1207 name: bone.name.clone(),
1208 name_bytes: shift_jis_hex(&bone.name),
1209 parent_index: bone.parent_index,
1210 rest_position: bone.position,
1211 deform_layer: bone.layer,
1212 fixed_axis: bone.fixed_axis,
1213 local_axis: bone.local_axis.as_ref().map(|axis| PmxRigSpecLocalAxis {
1214 x: axis.x,
1215 z: axis.z,
1216 }),
1217 transform_after_physics: bone.flags.transform_after_physics,
1218 });
1219
1220 if let Some(ik) = &bone.ik {
1221 ik_chains.push(PmxRigSpecIkChain {
1222 controller_bone_index: index as i32,
1223 target_bone_index: ik.target_index,
1224 iteration_count: ik.loop_count.max(0) as u32,
1225 limit_angle: ik.limit_angle,
1226 links: ik
1227 .links
1228 .iter()
1229 .map(|link| {
1230 let (angle_limit_min, angle_limit_max) = link
1231 .limits
1232 .as_ref()
1233 .map_or(([0.0; 3], [0.0; 3]), |limit| (limit.lower, limit.upper));
1234 PmxRigSpecIkLink {
1235 bone_index: link.bone_index,
1236 has_angle_limit: link.limits.is_some(),
1237 angle_limit_min,
1238 angle_limit_max,
1239 }
1240 })
1241 .collect(),
1242 });
1243 }
1244
1245 if let Some(append) = &bone.append_transform {
1246 grants.push(PmxRigSpecGrant {
1247 target_bone_index: index as i32,
1248 source_bone_index: append.parent_index,
1249 ratio: append.weight,
1250 affect_rotation: bone.flags.append_rotate,
1251 affect_translation: bone.flags.append_translate,
1252 local: bone.flags.append_local,
1253 });
1254 }
1255 }
1256
1257 PmxRigSpec {
1258 bone_count: bones.len(),
1259 ik_chain_count: ik_chains.len(),
1260 grant_count: grants.len(),
1261 bones,
1262 ik_chains,
1263 grants,
1264 }
1265}
1266
1267fn shift_jis_hex(text: &str) -> String {
1268 bytes_to_hex(&encode_sjis(text))
1269}
1270
1271fn bytes_to_hex(bytes: &[u8]) -> String {
1272 const HEX: &[u8; 16] = b"0123456789abcdef";
1273 let mut out = String::with_capacity(bytes.len() * 2);
1274 for &byte in bytes {
1275 out.push(HEX[(byte >> 4) as usize] as char);
1276 out.push(HEX[(byte & 0x0f) as usize] as char);
1277 }
1278 out
1279}
1280
1281#[derive(Debug, Clone, Serialize, Deserialize)]
1282#[serde(rename_all = "camelCase")]
1283pub struct PmxParsedMorph {
1284 pub name: String,
1285 pub english_name: String,
1286 #[serde(default = "default_pmx_morph_panel")]
1287 pub panel: String,
1288 #[serde(rename = "type")]
1289 pub kind: String,
1290 pub vertex_offsets: Vec<PmxParsedVertexMorphOffset>,
1291 pub group_offsets: Vec<PmxParsedGroupMorphOffset>,
1292 pub bone_offsets: Vec<PmxParsedBoneMorphOffset>,
1293 pub uv_offsets: Vec<PmxParsedUvMorphOffset>,
1294 pub additional_uv_offsets: Vec<PmxParsedAdditionalUvMorphOffset>,
1295 pub material_offsets: Vec<PmxParsedMaterialMorphOffset>,
1296 pub flip_offsets: Vec<PmxParsedGroupMorphOffset>,
1297 pub impulse_offsets: Vec<PmxParsedImpulseMorphOffset>,
1298}
1299
1300#[derive(Debug, Clone, Serialize, Deserialize)]
1301#[serde(rename_all = "camelCase")]
1302pub struct PmxParsedVertexMorphOffset {
1303 pub vertex_index: u32,
1304 #[serde(with = "json_f32::array3")]
1305 pub position: [f32; 3],
1306}
1307
1308#[derive(Debug, Clone, Serialize, Deserialize)]
1309#[serde(rename_all = "camelCase")]
1310pub struct PmxParsedGroupMorphOffset {
1311 pub morph_index: i32,
1312 #[serde(with = "json_f32")]
1313 pub weight: f32,
1314}
1315
1316#[derive(Debug, Clone, Serialize, Deserialize)]
1317#[serde(rename_all = "camelCase")]
1318pub struct PmxParsedBoneMorphOffset {
1319 pub bone_index: i32,
1320 #[serde(with = "json_f32::array3")]
1321 pub translation: [f32; 3],
1322 #[serde(with = "json_f32::array4")]
1323 pub rotation: [f32; 4],
1324}
1325
1326#[derive(Debug, Clone, Serialize, Deserialize)]
1327#[serde(rename_all = "camelCase")]
1328pub struct PmxParsedUvMorphOffset {
1329 pub vertex_index: u32,
1330 #[serde(with = "json_f32::array4")]
1331 pub uv: [f32; 4],
1332}
1333
1334#[derive(Debug, Clone, Serialize, Deserialize)]
1335#[serde(rename_all = "camelCase")]
1336pub struct PmxParsedAdditionalUvMorphOffset {
1337 pub vertex_index: u32,
1338 pub uv_index: u8,
1339 #[serde(with = "json_f32::array4")]
1340 pub uv: [f32; 4],
1341}
1342
1343#[derive(Debug, Clone, Serialize, Deserialize)]
1344#[serde(rename_all = "camelCase")]
1345pub struct PmxParsedMaterialMorphOffset {
1346 pub material_index: i32,
1347 pub operation: String,
1348 #[serde(with = "json_f32::array4")]
1349 pub diffuse: [f32; 4],
1350 #[serde(with = "json_f32::array3")]
1351 pub specular: [f32; 3],
1352 #[serde(with = "json_f32")]
1353 pub specular_power: f32,
1354 #[serde(with = "json_f32::array3")]
1355 pub ambient: [f32; 3],
1356 #[serde(with = "json_f32::array4")]
1357 pub edge_color: [f32; 4],
1358 #[serde(with = "json_f32")]
1359 pub edge_size: f32,
1360 #[serde(with = "json_f32::array4")]
1361 pub texture_factor: [f32; 4],
1362 #[serde(with = "json_f32::array4")]
1363 pub sphere_texture_factor: [f32; 4],
1364 #[serde(with = "json_f32::array4")]
1365 pub toon_texture_factor: [f32; 4],
1366}
1367
1368#[derive(Debug, Clone, Serialize, Deserialize)]
1369#[serde(rename_all = "camelCase")]
1370pub struct PmxParsedImpulseMorphOffset {
1371 pub rigid_body_index: i32,
1372 pub local: bool,
1373 #[serde(with = "json_f32::array3")]
1374 pub velocity: [f32; 3],
1375 #[serde(with = "json_f32::array3")]
1376 pub torque: [f32; 3],
1377}
1378
1379#[derive(Debug, Clone, Serialize, Deserialize)]
1380#[serde(rename_all = "camelCase")]
1381pub struct PmxMaterialSplitResult {
1382 pub meshes: Vec<PmxMaterialSplitMesh>,
1383 pub manifest: PmxMaterialSplitManifest,
1384}
1385
1386#[derive(Debug, Clone, Serialize, Deserialize)]
1387#[serde(rename_all = "camelCase")]
1388pub struct PmxMaterialSplitMesh {
1389 pub original_material_index: usize,
1390 pub original_vertex_indices: Vec<u32>,
1391 pub geometry: PmxParsedGeometry,
1392 pub material: PmxParsedMaterial,
1393 pub morphs: Vec<PmxParsedMorph>,
1394 pub morph_index_map: Vec<PmxMaterialSplitMorphIndexMap>,
1395 pub diagnostics: Vec<PmxParserDiagnostic>,
1396}
1397
1398#[derive(Debug, Clone, Serialize, Deserialize)]
1399#[serde(rename_all = "camelCase")]
1400pub struct PmxMaterialSplit {
1401 pub mesh: PmxMaterialSplitMesh,
1402}
1403
1404#[derive(Debug, Clone, Serialize, Deserialize)]
1405#[serde(rename_all = "camelCase")]
1406pub struct PmxMaterialSplitManifest {
1407 pub mesh_count: usize,
1408 pub meshes: Vec<PmxMaterialSplitManifestMesh>,
1409 pub diagnostics: Vec<PmxParserDiagnostic>,
1410}
1411
1412#[derive(Debug, Clone, Serialize, Deserialize)]
1413#[serde(rename_all = "camelCase")]
1414pub struct PmxMaterialSplitManifestMesh {
1415 pub mesh_index: usize,
1416 pub original_material_index: usize,
1417 pub original_vertex_indices: Vec<u32>,
1418 pub morph_index_map: Vec<PmxMaterialSplitMorphIndexMap>,
1419 pub diagnostics: Vec<PmxParserDiagnostic>,
1420}
1421
1422#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1423#[serde(rename_all = "camelCase")]
1424pub struct PmxMaterialSplitMorphIndexMap {
1425 pub original_index: usize,
1426 pub local_index: usize,
1427}
1428
1429#[derive(Debug, Clone, Serialize, Deserialize)]
1430#[serde(rename_all = "camelCase")]
1431pub struct PmxParsedDisplayFrame {
1432 pub name: String,
1433 pub english_name: String,
1434 pub special: bool,
1435 pub frames: Vec<PmxParsedDisplayFrameElement>,
1436}
1437
1438#[derive(Debug, Clone, Serialize, Deserialize)]
1439pub struct PmxParsedDisplayFrameElement {
1440 #[serde(rename = "type")]
1441 pub kind: String,
1442 pub index: i32,
1443}
1444
1445#[derive(Debug, Clone, Serialize, Deserialize)]
1446#[serde(rename_all = "camelCase")]
1447pub struct PmxParsedRigidBody {
1448 pub name: String,
1449 pub english_name: String,
1450 pub bone_index: i32,
1451 pub group: u8,
1452 pub mask: u16,
1454 pub shape: String,
1455 #[serde(with = "json_f32::array3")]
1456 pub size: [f32; 3],
1457 #[serde(with = "json_f32::array3")]
1458 pub position: [f32; 3],
1459 #[serde(with = "json_f32::array3")]
1460 pub rotation: [f32; 3],
1461 #[serde(with = "json_f32")]
1462 pub mass: f32,
1463 #[serde(with = "json_f32")]
1464 pub linear_damping: f32,
1465 #[serde(with = "json_f32")]
1466 pub angular_damping: f32,
1467 #[serde(with = "json_f32")]
1468 pub restitution: f32,
1469 #[serde(with = "json_f32")]
1470 pub friction: f32,
1471 pub mode: String,
1472}
1473
1474#[derive(Debug, Clone, Serialize, Deserialize)]
1475#[serde(rename_all = "camelCase")]
1476pub struct PmxParsedJoint {
1477 pub name: String,
1478 pub english_name: String,
1479 #[serde(rename = "type")]
1480 pub kind: String,
1481 pub rigid_body_index_a: i32,
1482 pub rigid_body_index_b: i32,
1483 #[serde(with = "json_f32::array3")]
1484 pub position: [f32; 3],
1485 #[serde(with = "json_f32::array3")]
1486 pub rotation: [f32; 3],
1487 #[serde(with = "json_f32::array3")]
1488 pub translation_lower_limit: [f32; 3],
1489 #[serde(with = "json_f32::array3")]
1490 pub translation_upper_limit: [f32; 3],
1491 #[serde(with = "json_f32::array3")]
1492 pub rotation_lower_limit: [f32; 3],
1493 #[serde(with = "json_f32::array3")]
1494 pub rotation_upper_limit: [f32; 3],
1495 #[serde(with = "json_f32::array3")]
1496 pub spring_translation_factor: [f32; 3],
1497 #[serde(with = "json_f32::array3")]
1498 pub spring_rotation_factor: [f32; 3],
1499}
1500
1501#[derive(Debug, Clone, Serialize, Deserialize)]
1502#[serde(rename_all = "camelCase")]
1503pub struct PmxParsedSoftBody {
1504 pub name: String,
1505 pub english_name: String,
1506 #[serde(rename = "type")]
1507 pub kind: String,
1508 pub material_index: i32,
1509 pub collision_group: u8,
1510 pub collision_mask: u16,
1511 pub flags: u8,
1512 pub bending_constraints_distance: i32,
1513 pub cluster_count: i32,
1514 #[serde(with = "json_f32")]
1515 pub total_mass: f32,
1516 #[serde(with = "json_f32")]
1517 pub collision_margin: f32,
1518}
1519
1520#[derive(Debug, Clone, Serialize, Deserialize)]
1521pub struct PmxParserDiagnostic {
1522 pub level: String,
1523 pub code: String,
1524 pub message: String,
1525}
1526
1527#[derive(Debug, Clone, Serialize, Deserialize)]
1528#[serde(rename_all = "camelCase")]
1529pub struct PmxPartsDescriptor {
1530 #[serde(default = "default_pmx_parts_version")]
1531 #[serde(with = "json_f32")]
1532 pub version: f32,
1533 #[serde(default = "default_pmx_parts_encoding")]
1534 pub encoding: String,
1535 #[serde(default = "default_pmx_parts_name")]
1536 pub name: String,
1537 #[serde(default)]
1538 pub english_name: String,
1539 #[serde(default)]
1540 pub comment: String,
1541 #[serde(default)]
1542 pub english_comment: String,
1543 #[serde(default)]
1544 pub material_name: String,
1545 #[serde(default)]
1546 pub english_material_name: String,
1547 #[serde(default)]
1548 pub materials: Vec<PmxPartsMaterialDescriptor>,
1549 #[serde(default)]
1550 pub bones: Vec<PmxPartsBoneDescriptor>,
1551 #[serde(default)]
1552 pub morphs: Vec<PmxPartsMorphDescriptor>,
1553 #[serde(default)]
1554 pub display_frames: Vec<PmxPartsDisplayFrameDescriptor>,
1555 #[serde(default)]
1556 pub rigid_bodies: Vec<PmxPartsRigidBodyDescriptor>,
1557 #[serde(default)]
1558 pub joints: Vec<PmxPartsJointDescriptor>,
1559 #[serde(default)]
1560 pub index_sizes: PmxPartsIndexSizes,
1561}
1562
1563#[derive(Debug, Clone, Serialize, Deserialize)]
1564#[serde(rename_all = "camelCase")]
1565pub struct PmxPartsMaterialDescriptor {
1566 #[serde(default = "default_pmx_parts_material_name")]
1567 pub name: String,
1568 #[serde(default)]
1569 pub english_name: String,
1570 #[serde(default)]
1571 pub texture_path: String,
1572 #[serde(default)]
1573 pub sphere_texture_path: String,
1574 #[serde(default)]
1575 pub sphere_mode: String,
1576 #[serde(default)]
1577 pub toon_texture_path: String,
1578 #[serde(default = "default_pmx_parts_shared_toon_index")]
1579 pub shared_toon_index: Option<u8>,
1580 #[serde(default = "default_pmx_parts_diffuse")]
1581 #[serde(with = "json_f32::array4")]
1582 pub diffuse: [f32; 4],
1583 #[serde(default)]
1584 #[serde(with = "json_f32::array3")]
1585 pub specular: [f32; 3],
1586 #[serde(default = "default_pmx_parts_specular_power")]
1587 #[serde(with = "json_f32")]
1588 pub specular_power: f32,
1589 #[serde(default = "default_pmx_parts_ambient")]
1590 #[serde(with = "json_f32::array3")]
1591 pub ambient: [f32; 3],
1592 #[serde(default = "default_pmx_parts_edge_color")]
1593 #[serde(with = "json_f32::array4")]
1594 pub edge_color: [f32; 4],
1595 #[serde(default = "default_pmx_parts_edge_size")]
1596 #[serde(with = "json_f32")]
1597 pub edge_size: f32,
1598 #[serde(default)]
1599 pub flags: PmxPartsMaterialFlags,
1600 #[serde(default)]
1601 pub face_count: i32,
1602}
1603
1604#[derive(Debug, Clone, Serialize, Deserialize)]
1605#[serde(rename_all = "camelCase")]
1606pub struct PmxPartsMaterialFlags {
1607 #[serde(default = "default_true")]
1608 pub double_sided: bool,
1609 #[serde(default = "default_true")]
1610 pub ground_shadow: bool,
1611 #[serde(default = "default_true")]
1612 pub self_shadow_map: bool,
1613 #[serde(default = "default_true")]
1614 pub self_shadow: bool,
1615 #[serde(default)]
1616 pub edge: bool,
1617 #[serde(default)]
1618 pub vertex_color: bool,
1619 #[serde(default)]
1620 pub point_draw: bool,
1621 #[serde(default)]
1622 pub line_draw: bool,
1623}
1624
1625impl Default for PmxPartsMaterialFlags {
1626 fn default() -> Self {
1627 Self {
1628 double_sided: true,
1629 ground_shadow: true,
1630 self_shadow_map: true,
1631 self_shadow: true,
1632 edge: false,
1633 vertex_color: false,
1634 point_draw: false,
1635 line_draw: false,
1636 }
1637 }
1638}
1639
1640#[derive(Debug, Clone, Serialize, Deserialize)]
1641#[serde(rename_all = "camelCase")]
1642pub struct PmxPartsBoneDescriptor {
1643 #[serde(default = "default_pmx_parts_root_bone_name")]
1644 pub name: String,
1645 #[serde(default)]
1646 pub english_name: String,
1647 #[serde(default = "default_negative_index")]
1648 pub parent_index: i32,
1649 #[serde(default)]
1650 pub layer: i32,
1651 #[serde(default)]
1652 #[serde(with = "json_f32::array3")]
1653 pub position: [f32; 3],
1654 #[serde(default = "default_negative_index")]
1655 pub tail_index: i32,
1656 #[serde(default)]
1657 #[serde(with = "json_f32::option_array3")]
1658 pub tail_position: Option<[f32; 3]>,
1659 #[serde(default)]
1660 pub rotatable: bool,
1661 #[serde(default)]
1662 pub translatable: bool,
1663 #[serde(default = "default_true")]
1664 pub visible: bool,
1665 #[serde(default = "default_true")]
1666 pub enabled: bool,
1667}
1668
1669#[derive(Debug, Clone, Serialize, Deserialize)]
1670#[serde(rename_all = "camelCase")]
1671pub struct PmxPartsMorphDescriptor {
1672 #[serde(default)]
1673 pub name: String,
1674 #[serde(default)]
1675 pub english_name: String,
1676 #[serde(default = "default_pmx_morph_panel")]
1677 pub panel: String,
1678 #[serde(default = "default_pmx_parts_morph_kind", alias = "type")]
1679 pub kind: String,
1680 #[serde(default)]
1681 pub vertex_offsets: Vec<PmxPartsVertexMorphOffset>,
1682 #[serde(default)]
1683 pub group_offsets: Vec<PmxPartsGroupMorphOffset>,
1684}
1685
1686#[derive(Debug, Clone, Serialize, Deserialize)]
1687#[serde(rename_all = "camelCase")]
1688pub struct PmxPartsVertexMorphOffset {
1689 pub vertex_index: u32,
1690 #[serde(with = "json_f32::array3")]
1691 pub position: [f32; 3],
1692}
1693
1694#[derive(Debug, Clone, Serialize, Deserialize)]
1695#[serde(rename_all = "camelCase")]
1696pub struct PmxPartsGroupMorphOffset {
1697 pub morph_index: i32,
1698 #[serde(with = "json_f32")]
1699 pub weight: f32,
1700}
1701
1702#[derive(Debug, Clone, Serialize, Deserialize)]
1703#[serde(rename_all = "camelCase")]
1704pub struct PmxPartsDisplayFrameDescriptor {
1705 #[serde(default)]
1706 pub name: String,
1707 #[serde(default)]
1708 pub english_name: String,
1709 #[serde(default)]
1710 pub special: bool,
1711 #[serde(default)]
1712 pub frames: Vec<PmxPartsDisplayFrameItem>,
1713}
1714
1715#[derive(Debug, Clone, Serialize, Deserialize)]
1716#[serde(rename_all = "camelCase")]
1717pub struct PmxPartsDisplayFrameItem {
1718 #[serde(default = "default_pmx_parts_display_frame_kind")]
1719 pub kind: String,
1720 #[serde(default)]
1721 pub index: i32,
1722}
1723
1724#[derive(Debug, Clone, Serialize, Deserialize)]
1725#[serde(rename_all = "camelCase")]
1726pub struct PmxPartsRigidBodyDescriptor {
1727 #[serde(default)]
1728 pub name: String,
1729 #[serde(default)]
1730 pub english_name: String,
1731 #[serde(default = "default_negative_index")]
1732 pub bone_index: i32,
1733 #[serde(default)]
1734 pub group: u8,
1735 #[serde(default = "default_collision_mask_all")]
1737 pub mask: u16,
1738 #[serde(default = "default_pmx_parts_rigid_body_shape")]
1739 pub shape: String,
1740 #[serde(default = "default_unit_vec3")]
1741 #[serde(with = "json_f32::array3")]
1742 pub size: [f32; 3],
1743 #[serde(default)]
1744 #[serde(with = "json_f32::array3")]
1745 pub position: [f32; 3],
1746 #[serde(default)]
1747 #[serde(with = "json_f32::array3")]
1748 pub rotation: [f32; 3],
1749 #[serde(default = "default_pmx_parts_mass")]
1750 #[serde(with = "json_f32")]
1751 pub mass: f32,
1752 #[serde(default)]
1753 #[serde(with = "json_f32")]
1754 pub linear_damping: f32,
1755 #[serde(default)]
1756 #[serde(with = "json_f32")]
1757 pub angular_damping: f32,
1758 #[serde(default)]
1759 #[serde(with = "json_f32")]
1760 pub restitution: f32,
1761 #[serde(default)]
1762 #[serde(with = "json_f32")]
1763 pub friction: f32,
1764 #[serde(default = "default_pmx_parts_rigid_body_mode")]
1765 pub mode: String,
1766}
1767
1768#[derive(Debug, Clone, Serialize, Deserialize)]
1769#[serde(rename_all = "camelCase")]
1770pub struct PmxPartsJointDescriptor {
1771 #[serde(default)]
1772 pub name: String,
1773 #[serde(default)]
1774 pub english_name: String,
1775 #[serde(
1776 default = "default_pmx_parts_joint_kind",
1777 rename = "type",
1778 alias = "kind"
1779 )]
1780 pub kind: String,
1781 #[serde(default = "default_negative_index")]
1782 pub rigid_body_index_a: i32,
1783 #[serde(default = "default_negative_index")]
1784 pub rigid_body_index_b: i32,
1785 #[serde(default)]
1786 #[serde(with = "json_f32::array3")]
1787 pub position: [f32; 3],
1788 #[serde(default)]
1789 #[serde(with = "json_f32::array3")]
1790 pub rotation: [f32; 3],
1791 #[serde(default)]
1792 #[serde(with = "json_f32::array3")]
1793 pub translation_lower_limit: [f32; 3],
1794 #[serde(default)]
1795 #[serde(with = "json_f32::array3")]
1796 pub translation_upper_limit: [f32; 3],
1797 #[serde(default)]
1798 #[serde(with = "json_f32::array3")]
1799 pub rotation_lower_limit: [f32; 3],
1800 #[serde(default)]
1801 #[serde(with = "json_f32::array3")]
1802 pub rotation_upper_limit: [f32; 3],
1803 #[serde(default)]
1804 #[serde(with = "json_f32::array3")]
1805 pub spring_translation_factor: [f32; 3],
1806 #[serde(default)]
1807 #[serde(with = "json_f32::array3")]
1808 pub spring_rotation_factor: [f32; 3],
1809}
1810
1811#[derive(Debug, Clone, Serialize, Deserialize)]
1812#[serde(rename_all = "camelCase")]
1813pub struct PmxPartsIndexSizes {
1814 #[serde(default = "default_pmx_vertex_index_size")]
1815 pub vertex: u8,
1816 #[serde(default = "default_pmx_small_index_size")]
1817 pub texture: u8,
1818 #[serde(default = "default_pmx_small_index_size")]
1819 pub material: u8,
1820 #[serde(default = "default_pmx_small_index_size")]
1821 pub bone: u8,
1822 #[serde(default = "default_pmx_small_index_size")]
1823 pub morph: u8,
1824 #[serde(default = "default_pmx_small_index_size")]
1825 pub rigid_body: u8,
1826}
1827
1828impl Default for PmxPartsIndexSizes {
1829 fn default() -> Self {
1830 Self {
1831 vertex: default_pmx_vertex_index_size(),
1832 texture: default_pmx_small_index_size(),
1833 material: default_pmx_small_index_size(),
1834 bone: default_pmx_small_index_size(),
1835 morph: default_pmx_small_index_size(),
1836 rigid_body: default_pmx_small_index_size(),
1837 }
1838 }
1839}
1840
1841pub struct PmxPartsInput<'a> {
1842 pub descriptor: PmxPartsDescriptor,
1843 pub positions_xyz: &'a [f32],
1844 pub normals_xyz: &'a [f32],
1845 pub uvs_xy: &'a [f32],
1846 pub indices: &'a [u32],
1847 pub skin_indices: &'a [u32],
1848 pub skin_weights: &'a [f32],
1849 pub edge_scale: &'a [f32],
1850}
1851
1852fn default_pmx_format() -> String {
1853 "pmx".to_owned()
1854}
1855
1856fn default_pmx_parts_version() -> f32 {
1857 2.0
1858}
1859
1860fn default_pmx_parts_encoding() -> String {
1861 "utf-8".to_owned()
1862}
1863
1864fn default_pmx_parts_name() -> String {
1865 "model".to_owned()
1866}
1867
1868fn default_pmx_parts_root_bone_name() -> String {
1869 "root".to_owned()
1870}
1871
1872fn default_negative_index() -> i32 {
1873 -1
1874}
1875
1876fn default_pmx_parts_display_frame_kind() -> String {
1877 "bone".to_owned()
1878}
1879
1880fn default_pmx_parts_morph_kind() -> String {
1881 "vertex".to_owned()
1882}
1883
1884fn default_pmx_morph_panel() -> String {
1885 "other".to_owned()
1886}
1887
1888fn default_pmx_parts_rigid_body_shape() -> String {
1889 "sphere".to_owned()
1890}
1891
1892fn default_pmx_parts_rigid_body_mode() -> String {
1893 "static".to_owned()
1894}
1895
1896fn default_pmx_parts_joint_kind() -> String {
1897 "generic6dofSpring".to_owned()
1898}
1899
1900fn default_collision_mask_all() -> u16 {
1901 0xffff
1902}
1903
1904fn default_unit_vec3() -> [f32; 3] {
1905 [1.0, 1.0, 1.0]
1906}
1907
1908fn default_pmx_parts_mass() -> f32 {
1909 1.0
1910}
1911
1912fn default_pmx_parts_material_name() -> String {
1913 "material".to_owned()
1914}
1915
1916fn default_pmx_parts_shared_toon_index() -> Option<u8> {
1917 Some(0)
1918}
1919
1920fn default_pmx_parts_diffuse() -> [f32; 4] {
1921 [0.8, 0.8, 0.8, 1.0]
1922}
1923
1924fn default_pmx_parts_specular_power() -> f32 {
1925 1.0
1926}
1927
1928fn default_pmx_parts_ambient() -> [f32; 3] {
1929 [0.2, 0.2, 0.2]
1930}
1931
1932fn default_pmx_parts_edge_color() -> [f32; 4] {
1933 [0.0, 0.0, 0.0, 1.0]
1934}
1935
1936fn default_pmx_parts_edge_size() -> f32 {
1937 1.0
1938}
1939
1940fn default_true() -> bool {
1941 true
1942}
1943
1944fn default_pmx_vertex_index_size() -> u8 {
1945 4
1946}
1947
1948fn default_pmx_small_index_size() -> u8 {
1949 1
1950}
1951
1952pub fn build_pmx_model_from_parts(input: PmxPartsInput<'_>) -> Result<PmxParsedModel, String> {
1953 let vertex_count = validate_pmx_parts_geometry(
1954 input.positions_xyz,
1955 input.normals_xyz,
1956 input.uvs_xy,
1957 input.indices,
1958 input.skin_indices,
1959 input.skin_weights,
1960 input.edge_scale,
1961 )?;
1962 validate_pmx_parts_descriptor(
1963 &input.descriptor,
1964 vertex_count,
1965 input.indices,
1966 input.skin_indices,
1967 )?;
1968
1969 let skin_indices = if input.skin_indices.is_empty() {
1970 vec![0; vertex_count * 4]
1971 } else {
1972 input.skin_indices.to_vec()
1973 };
1974 let skin_weights = if input.skin_weights.is_empty() {
1975 default_pmx_skin_weights(vertex_count)
1976 } else {
1977 input.skin_weights.to_vec()
1978 };
1979 let edge_scale = if input.edge_scale.is_empty() {
1980 vec![1.0; vertex_count]
1981 } else {
1982 input.edge_scale.to_vec()
1983 };
1984 let skinning_modes = vec![PMX_SKINNING_MODE_BDEF4.to_owned(); vertex_count];
1985 let face_count = input.indices.len() / 3;
1986 let (materials, material_groups) = build_pmx_parts_materials(&input.descriptor, face_count);
1987 let bones = build_pmx_parts_bones(&input.descriptor);
1988 let morphs = build_pmx_parts_morphs(&input.descriptor);
1989 let display_frames = build_pmx_parts_display_frames(&input.descriptor);
1990 let rigid_bodies = build_pmx_parts_rigid_bodies(&input.descriptor);
1991 let joints = build_pmx_parts_joints(&input.descriptor);
1992
1993 let model = PmxParsedModel {
1994 metadata: PmxParsedMetadata {
1995 format: "pmx".to_owned(),
1996 version: input.descriptor.version,
1997 encoding: input.descriptor.encoding,
1998 name: input.descriptor.name,
1999 english_name: input.descriptor.english_name,
2000 comment: input.descriptor.comment,
2001 english_comment: input.descriptor.english_comment,
2002 counts: PmxParsedCounts {
2003 vertices: vertex_count,
2004 faces: face_count,
2005 materials: materials.len(),
2006 bones: bones.len(),
2007 morphs: morphs.len(),
2008 display_frames: display_frames.len(),
2009 rigid_bodies: rigid_bodies.len(),
2010 joints: joints.len(),
2011 soft_bodies: 0,
2012 },
2013 index_sizes: PmxParsedIndexSizes {
2014 vertex: input.descriptor.index_sizes.vertex,
2015 texture: input.descriptor.index_sizes.texture,
2016 material: input.descriptor.index_sizes.material,
2017 bone: input.descriptor.index_sizes.bone,
2018 morph: input.descriptor.index_sizes.morph,
2019 rigid_body: input.descriptor.index_sizes.rigid_body,
2020 },
2021 additional_uv_count: 0,
2022 },
2023 geometry: PmxParsedGeometry {
2024 positions: input.positions_xyz.to_vec(),
2025 normals: input.normals_xyz.to_vec(),
2026 uvs: input.uvs_xy.to_vec(),
2027 additional_uvs: Vec::new(),
2028 indices: input.indices.to_vec(),
2029 skin_indices,
2030 skin_weights,
2031 edge_scale,
2032 material_groups,
2033 sdef: PmxParsedSdef {
2034 skinning_modes,
2035 ..PmxParsedSdef::default()
2036 },
2037 qdef: PmxParsedQdef::default(),
2038 },
2039 materials,
2040 skeleton: PmxParsedSkeleton { bones },
2041 morphs,
2042 display_frames,
2043 rigid_bodies,
2044 joints,
2045 soft_bodies: Vec::new(),
2046 diagnostics: Vec::new(),
2047 };
2048 validate_pmx_export_model(&model)?;
2049 Ok(model)
2050}
2051
2052pub fn validate_pmx_export_model(model: &PmxParsedModel) -> Result<(), String> {
2053 let geometry = &model.geometry;
2054 if !geometry.positions.len().is_multiple_of(3) {
2055 return Err(format!(
2056 "PMX export positions length must be divisible by 3: {}",
2057 geometry.positions.len()
2058 ));
2059 }
2060 let vertex_count = geometry.positions.len() / 3;
2061 if geometry.normals.len() != vertex_count * 3 {
2062 return Err(format!(
2063 "PMX export normals length mismatch: expected {}, got {}",
2064 vertex_count * 3,
2065 geometry.normals.len()
2066 ));
2067 }
2068 if geometry.uvs.len() != vertex_count * 2 {
2069 return Err(format!(
2070 "PMX export uvs length mismatch: expected {}, got {}",
2071 vertex_count * 2,
2072 geometry.uvs.len()
2073 ));
2074 }
2075 if geometry.additional_uvs.len() != model.metadata.additional_uv_count as usize {
2076 return Err(format!(
2077 "PMX export additionalUvs set count mismatch: expected {}, got {}",
2078 model.metadata.additional_uv_count,
2079 geometry.additional_uvs.len()
2080 ));
2081 }
2082 for (index, values) in geometry.additional_uvs.iter().enumerate() {
2083 if values.len() != vertex_count * 4 {
2084 return Err(format!(
2085 "PMX export additionalUvs[{}] length mismatch: expected {}, got {}",
2086 index,
2087 vertex_count * 4,
2088 values.len()
2089 ));
2090 }
2091 }
2092 if !geometry.indices.len().is_multiple_of(3) {
2093 return Err(format!(
2094 "PMX export indices length must be divisible by 3: {}",
2095 geometry.indices.len()
2096 ));
2097 }
2098 let expected_skin_len = vertex_count * 4;
2099 if geometry.skin_indices.is_empty() != geometry.skin_weights.is_empty() {
2100 return Err(
2101 "PMX export skinIndices and skinWeights must be both empty or both provided".to_owned(),
2102 );
2103 }
2104 if !geometry.skin_indices.is_empty() && geometry.skin_indices.len() != expected_skin_len {
2105 return Err(format!(
2106 "PMX export skinIndices length mismatch: expected {}, got {}",
2107 expected_skin_len,
2108 geometry.skin_indices.len()
2109 ));
2110 }
2111 if !geometry.skin_weights.is_empty() && geometry.skin_weights.len() != expected_skin_len {
2112 return Err(format!(
2113 "PMX export skinWeights length mismatch: expected {}, got {}",
2114 expected_skin_len,
2115 geometry.skin_weights.len()
2116 ));
2117 }
2118 if !geometry.sdef.skinning_modes.is_empty()
2119 && geometry.sdef.skinning_modes.len() != vertex_count
2120 {
2121 return Err(format!(
2122 "PMX export skinningModes length mismatch: expected {}, got {}",
2123 vertex_count,
2124 geometry.sdef.skinning_modes.len()
2125 ));
2126 }
2127 for (index, mode) in geometry.sdef.skinning_modes.iter().enumerate() {
2128 if !matches!(
2129 mode.as_str(),
2130 PMX_SKINNING_MODE_BDEF1
2131 | PMX_SKINNING_MODE_BDEF2
2132 | PMX_SKINNING_MODE_BDEF4
2133 | PMX_SKINNING_MODE_SDEF
2134 | PMX_SKINNING_MODE_QDEF
2135 ) {
2136 return Err(format!(
2137 "PMX export skinningModes[{index}] must be bdef1, bdef2, bdef4, sdef, or qdef, got {mode}"
2138 ));
2139 }
2140 }
2141 Ok(())
2142}
2143
2144fn validate_pmx_parts_geometry(
2145 positions_xyz: &[f32],
2146 normals_xyz: &[f32],
2147 uvs_xy: &[f32],
2148 indices: &[u32],
2149 skin_indices: &[u32],
2150 skin_weights: &[f32],
2151 edge_scale: &[f32],
2152) -> Result<usize, String> {
2153 if !positions_xyz.len().is_multiple_of(3) {
2154 return Err(format!(
2155 "positions_xyz must contain vertex_count * 3 values, got {}",
2156 positions_xyz.len()
2157 ));
2158 }
2159 let vertex_count = positions_xyz.len() / 3;
2160 if vertex_count == 0 {
2161 return Err("PMX parts export requires at least one vertex".to_owned());
2162 }
2163 if normals_xyz.len() != vertex_count * 3 {
2164 return Err(format!(
2165 "normals_xyz must contain vertex_count * 3 values, expected {}, got {}",
2166 vertex_count * 3,
2167 normals_xyz.len()
2168 ));
2169 }
2170 if uvs_xy.len() != vertex_count * 2 {
2171 return Err(format!(
2172 "uvs_xy must contain vertex_count * 2 values, expected {}, got {}",
2173 vertex_count * 2,
2174 uvs_xy.len()
2175 ));
2176 }
2177 if !indices.len().is_multiple_of(3) {
2178 return Err(format!(
2179 "indices must contain triangle indices and be divisible by 3, got {}",
2180 indices.len()
2181 ));
2182 }
2183 if let Some(&index) = indices
2184 .iter()
2185 .find(|&&index| index as usize >= vertex_count)
2186 {
2187 return Err(format!(
2188 "indices contains out-of-range vertex index {index} for vertex_count {vertex_count}"
2189 ));
2190 }
2191 if !skin_indices.is_empty() && skin_indices.len() != vertex_count * 4 {
2192 return Err(format!(
2193 "skin_indices must contain vertex_count * 4 values when provided, expected {}, got {}",
2194 vertex_count * 4,
2195 skin_indices.len()
2196 ));
2197 }
2198 if !skin_weights.is_empty() && skin_weights.len() != vertex_count * 4 {
2199 return Err(format!(
2200 "skin_weights must contain vertex_count * 4 values when provided, expected {}, got {}",
2201 vertex_count * 4,
2202 skin_weights.len()
2203 ));
2204 }
2205 if skin_indices.is_empty() != skin_weights.is_empty() {
2206 return Err(
2207 "skin_indices and skin_weights must either both be provided or both be empty"
2208 .to_owned(),
2209 );
2210 }
2211 if !edge_scale.is_empty() && edge_scale.len() != vertex_count {
2212 return Err(format!(
2213 "edge_scale must contain vertex_count values when provided, expected {}, got {}",
2214 vertex_count,
2215 edge_scale.len()
2216 ));
2217 }
2218 Ok(vertex_count)
2219}
2220
2221fn validate_pmx_parts_descriptor(
2222 descriptor: &PmxPartsDescriptor,
2223 vertex_count: usize,
2224 indices: &[u32],
2225 skin_indices: &[u32],
2226) -> Result<(), String> {
2227 if descriptor.version < 2.0 {
2228 return Err(format!(
2229 "PMX parts export supports PMX 2.0 or newer, got {}",
2230 descriptor.version
2231 ));
2232 }
2233 if descriptor.encoding != "utf-8" && descriptor.encoding != "utf-16-le" {
2234 return Err(format!(
2235 "PMX parts export encoding must be utf-8 or utf-16-le, got {}",
2236 descriptor.encoding
2237 ));
2238 }
2239 validate_pmx_index_size("vertex", descriptor.index_sizes.vertex)?;
2240 validate_pmx_index_size("texture", descriptor.index_sizes.texture)?;
2241 validate_pmx_index_size("material", descriptor.index_sizes.material)?;
2242 validate_pmx_index_size("bone", descriptor.index_sizes.bone)?;
2243 validate_pmx_index_size("morph", descriptor.index_sizes.morph)?;
2244 validate_pmx_index_size("rigidBody", descriptor.index_sizes.rigid_body)?;
2245 if indices.len() / 3 > i32::MAX as usize {
2246 return Err(format!(
2247 "face count {} exceeds PMX i32 material face count limit",
2248 indices.len() / 3
2249 ));
2250 }
2251 if !pmx_vertex_index_size_can_hold(descriptor.index_sizes.vertex, vertex_count) {
2252 return Err(format!(
2253 "vertex index size {} cannot hold vertex_count {}",
2254 descriptor.index_sizes.vertex, vertex_count
2255 ));
2256 }
2257 if !indices.is_empty()
2258 && !pmx_vertex_index_size_can_hold(
2259 descriptor.index_sizes.vertex,
2260 indices.iter().copied().max().unwrap_or(0) as usize + 1,
2261 )
2262 {
2263 return Err(format!(
2264 "vertex index size {} cannot hold max index {}",
2265 descriptor.index_sizes.vertex,
2266 indices.iter().copied().max().unwrap_or(0)
2267 ));
2268 }
2269 validate_pmx_parts_materials(descriptor, indices.len() / 3)?;
2270 validate_pmx_parts_bones(descriptor)?;
2271 validate_pmx_parts_morphs(descriptor, vertex_count)?;
2272 validate_pmx_parts_skin_indices(descriptor, skin_indices)?;
2273 validate_pmx_parts_display_frames(descriptor)?;
2274 validate_pmx_parts_physics(descriptor)?;
2275 Ok(())
2276}
2277
2278fn validate_pmx_parts_materials(
2279 descriptor: &PmxPartsDescriptor,
2280 face_count: usize,
2281) -> Result<(), String> {
2282 if descriptor.materials.is_empty() {
2283 return Ok(());
2284 }
2285 if !pmx_signed_index_size_can_hold_count(
2286 descriptor.index_sizes.material,
2287 descriptor.materials.len(),
2288 ) {
2289 return Err(format!(
2290 "material index size {} cannot hold material count {}",
2291 descriptor.index_sizes.material,
2292 descriptor.materials.len()
2293 ));
2294 }
2295 let mut total = 0usize;
2296 for (index, material) in descriptor.materials.iter().enumerate() {
2297 if material.face_count < 0 {
2298 return Err(format!(
2299 "materials[{index}].faceCount must be non-negative, got {}",
2300 material.face_count
2301 ));
2302 }
2303 total += material.face_count as usize;
2304 }
2305 if total != face_count {
2306 return Err(format!(
2307 "materials faceCount sum must match index face count, expected {}, got {}",
2308 face_count, total
2309 ));
2310 }
2311 Ok(())
2312}
2313
2314fn validate_pmx_parts_bones(descriptor: &PmxPartsDescriptor) -> Result<(), String> {
2315 let bone_count = descriptor.bones.len().max(1);
2316 if !pmx_signed_index_size_can_hold_count(descriptor.index_sizes.bone, bone_count) {
2317 return Err(format!(
2318 "bone index size {} cannot hold bone count {}",
2319 descriptor.index_sizes.bone, bone_count
2320 ));
2321 }
2322 for (index, bone) in descriptor.bones.iter().enumerate() {
2323 if bone.parent_index < -1
2324 || (bone.parent_index != -1 && bone.parent_index as usize >= bone_count)
2325 {
2326 return Err(format!(
2327 "bones[{index}].parentIndex must be -1 or a valid bone index, got {}",
2328 bone.parent_index
2329 ));
2330 }
2331 if bone.tail_index < -1 || (bone.tail_index != -1 && bone.tail_index as usize >= bone_count)
2332 {
2333 return Err(format!(
2334 "bones[{index}].tailIndex must be -1 or a valid bone index, got {}",
2335 bone.tail_index
2336 ));
2337 }
2338 }
2339 Ok(())
2340}
2341
2342fn validate_pmx_parts_skin_indices(
2343 descriptor: &PmxPartsDescriptor,
2344 skin_indices: &[u32],
2345) -> Result<(), String> {
2346 if skin_indices.is_empty() {
2347 return Ok(());
2348 }
2349 let bone_count = descriptor.bones.len().max(1);
2350 if let Some(&index) = skin_indices
2351 .iter()
2352 .find(|&&index| index as usize >= bone_count)
2353 {
2354 return Err(format!(
2355 "skin_indices contains out-of-range bone index {index} for bone count {bone_count}"
2356 ));
2357 }
2358 Ok(())
2359}
2360
2361fn validate_pmx_parts_morphs(
2362 descriptor: &PmxPartsDescriptor,
2363 vertex_count: usize,
2364) -> Result<(), String> {
2365 if !pmx_signed_index_size_can_hold_count(descriptor.index_sizes.morph, descriptor.morphs.len())
2366 {
2367 return Err(format!(
2368 "morph index size {} cannot hold morph count {}",
2369 descriptor.index_sizes.morph,
2370 descriptor.morphs.len()
2371 ));
2372 }
2373 for (morph_index, morph) in descriptor.morphs.iter().enumerate() {
2374 if morph_panel_byte_checked(&morph.panel).is_none() {
2375 return Err(format!(
2376 "morphs[{morph_index}].panel must be system, brow, eye, mouth, or other, got {}",
2377 morph.panel
2378 ));
2379 }
2380 match morph.kind.as_str() {
2381 "vertex" => {
2382 for (offset_index, offset) in morph.vertex_offsets.iter().enumerate() {
2383 if offset.vertex_index as usize >= vertex_count {
2384 return Err(format!(
2385 "morphs[{morph_index}].vertexOffsets[{offset_index}].vertexIndex out of range: {} for vertex_count {vertex_count}",
2386 offset.vertex_index
2387 ));
2388 }
2389 }
2390 }
2391 "group" => {
2392 for (offset_index, offset) in morph.group_offsets.iter().enumerate() {
2393 if offset.morph_index < 0
2394 || offset.morph_index as usize >= descriptor.morphs.len()
2395 {
2396 return Err(format!(
2397 "morphs[{morph_index}].groupOffsets[{offset_index}].morphIndex out of range: {}",
2398 offset.morph_index
2399 ));
2400 }
2401 }
2402 }
2403 other => {
2404 return Err(format!(
2405 "morphs[{morph_index}].kind must be vertex or group, got {other}"
2406 ));
2407 }
2408 }
2409 }
2410 Ok(())
2411}
2412
2413fn validate_pmx_parts_display_frames(descriptor: &PmxPartsDescriptor) -> Result<(), String> {
2414 let bone_count = descriptor.bones.len().max(1);
2415 let morph_count = descriptor.morphs.len();
2416 for (frame_index, frame) in descriptor.display_frames.iter().enumerate() {
2417 for (item_index, item) in frame.frames.iter().enumerate() {
2418 match item.kind.as_str() {
2419 "bone" => {
2420 if item.index < 0 || item.index as usize >= bone_count {
2421 return Err(format!(
2422 "displayFrames[{frame_index}].frames[{item_index}].index must reference an existing bone, got {}",
2423 item.index
2424 ));
2425 }
2426 }
2427 "morph" => {
2428 if item.index < 0 || item.index as usize >= morph_count {
2429 return Err(format!(
2430 "displayFrames[{frame_index}].frames[{item_index}].index must reference an existing morph, got {}",
2431 item.index
2432 ));
2433 }
2434 }
2435 other => {
2436 return Err(format!(
2437 "displayFrames[{frame_index}].frames[{item_index}].kind must be bone or morph, got {other}"
2438 ));
2439 }
2440 }
2441 }
2442 }
2443 Ok(())
2444}
2445
2446fn validate_pmx_parts_physics(descriptor: &PmxPartsDescriptor) -> Result<(), String> {
2447 let bone_count = descriptor.bones.len().max(1);
2448 if !pmx_signed_index_size_can_hold_count(
2449 descriptor.index_sizes.rigid_body,
2450 descriptor.rigid_bodies.len(),
2451 ) {
2452 return Err(format!(
2453 "rigidBody index size {} cannot hold rigid body count {}",
2454 descriptor.index_sizes.rigid_body,
2455 descriptor.rigid_bodies.len()
2456 ));
2457 }
2458 for (index, body) in descriptor.rigid_bodies.iter().enumerate() {
2459 if body.bone_index < -1 || (body.bone_index != -1 && body.bone_index as usize >= bone_count)
2460 {
2461 return Err(format!(
2462 "rigidBodies[{index}].boneIndex must be -1 or a valid bone index, got {}",
2463 body.bone_index
2464 ));
2465 }
2466 if !matches!(body.shape.as_str(), "sphere" | "box" | "capsule") {
2467 return Err(format!(
2468 "rigidBodies[{index}].shape must be sphere, box, or capsule, got {}",
2469 body.shape
2470 ));
2471 }
2472 if !matches!(body.mode.as_str(), "static" | "dynamic" | "dynamicBone") {
2473 return Err(format!(
2474 "rigidBodies[{index}].mode must be static, dynamic, or dynamicBone, got {}",
2475 body.mode
2476 ));
2477 }
2478 }
2479 for (index, joint) in descriptor.joints.iter().enumerate() {
2480 validate_pmx_parts_rigid_body_ref(
2481 "rigidBodyIndexA",
2482 index,
2483 joint.rigid_body_index_a,
2484 descriptor.rigid_bodies.len(),
2485 )?;
2486 validate_pmx_parts_rigid_body_ref(
2487 "rigidBodyIndexB",
2488 index,
2489 joint.rigid_body_index_b,
2490 descriptor.rigid_bodies.len(),
2491 )?;
2492 if !matches!(
2493 joint.kind.as_str(),
2494 "generic6dofSpring" | "generic6dof" | "point2point" | "coneTwist" | "slider" | "hinge"
2495 ) {
2496 return Err(format!(
2497 "joints[{index}].kind has unsupported PMX joint type {}",
2498 joint.kind
2499 ));
2500 }
2501 }
2502 Ok(())
2503}
2504
2505fn validate_pmx_parts_rigid_body_ref(
2506 field: &str,
2507 joint_index: usize,
2508 value: i32,
2509 rigid_body_count: usize,
2510) -> Result<(), String> {
2511 if value < -1 || (value != -1 && value as usize >= rigid_body_count) {
2512 Err(format!(
2513 "joints[{joint_index}].{field} must be -1 or a valid rigid body index, got {value}"
2514 ))
2515 } else {
2516 Ok(())
2517 }
2518}
2519
2520fn validate_pmx_index_size(name: &str, value: u8) -> Result<(), String> {
2521 if matches!(value, 1 | 2 | 4) {
2522 Ok(())
2523 } else {
2524 Err(format!(
2525 "PMX {name} index size must be 1, 2, or 4, got {value}"
2526 ))
2527 }
2528}
2529
2530fn pmx_vertex_index_size_can_hold(size: u8, count: usize) -> bool {
2531 match size {
2532 1 => count <= u8::MAX as usize + 1,
2533 2 => count <= u16::MAX as usize + 1,
2534 4 => true,
2535 _ => false,
2536 }
2537}
2538
2539fn pmx_signed_index_size_can_hold_count(size: u8, count: usize) -> bool {
2540 match size {
2541 1 => count <= i8::MAX as usize + 1,
2542 2 => count <= i16::MAX as usize + 1,
2543 4 => true,
2544 _ => false,
2545 }
2546}
2547
2548fn default_pmx_skin_weights(vertex_count: usize) -> Vec<f32> {
2549 let mut weights = vec![0.0; vertex_count * 4];
2550 for vertex in 0..vertex_count {
2551 weights[vertex * 4] = 1.0;
2552 }
2553 weights
2554}
2555
2556fn build_pmx_parts_materials(
2557 descriptor: &PmxPartsDescriptor,
2558 face_count: usize,
2559) -> (Vec<PmxParsedMaterial>, Vec<PmxParsedMaterialGroup>) {
2560 if face_count == 0 {
2561 return (Vec::new(), Vec::new());
2562 }
2563 if descriptor.materials.is_empty() {
2564 return (
2565 vec![default_pmx_parts_material(
2566 &descriptor.material_name,
2567 &descriptor.english_material_name,
2568 face_count as i32,
2569 )],
2570 vec![PmxParsedMaterialGroup {
2571 start: 0,
2572 count: face_count * 3,
2573 material_index: 0,
2574 }],
2575 );
2576 }
2577
2578 let mut start = 0usize;
2579 let mut materials = Vec::with_capacity(descriptor.materials.len());
2580 let mut groups = Vec::with_capacity(descriptor.materials.len());
2581 for (index, material) in descriptor.materials.iter().enumerate() {
2582 let index_count = material.face_count as usize * 3;
2583 materials.push(pmx_parts_material_from_descriptor(material));
2584 groups.push(PmxParsedMaterialGroup {
2585 start,
2586 count: index_count,
2587 material_index: index,
2588 });
2589 start += index_count;
2590 }
2591 (materials, groups)
2592}
2593
2594fn pmx_parts_material_from_descriptor(material: &PmxPartsMaterialDescriptor) -> PmxParsedMaterial {
2595 PmxParsedMaterial {
2596 name: material.name.clone(),
2597 english_name: material.english_name.clone(),
2598 texture_path: material.texture_path.clone(),
2599 sphere_texture_path: material.sphere_texture_path.clone(),
2600 sphere_mode: if material.sphere_mode.is_empty() {
2601 "none".to_owned()
2602 } else {
2603 material.sphere_mode.clone()
2604 },
2605 toon_texture_path: material.toon_texture_path.clone(),
2606 shared_toon_index: material.shared_toon_index,
2607 diffuse: material.diffuse,
2608 specular: material.specular,
2609 specular_power: material.specular_power,
2610 ambient: material.ambient,
2611 edge_color: material.edge_color,
2612 edge_size: material.edge_size,
2613 flags: PmxParsedMaterialFlags {
2614 double_sided: material.flags.double_sided,
2615 ground_shadow: material.flags.ground_shadow,
2616 self_shadow_map: material.flags.self_shadow_map,
2617 self_shadow: material.flags.self_shadow,
2618 edge: material.flags.edge,
2619 vertex_color: material.flags.vertex_color,
2620 point_draw: material.flags.point_draw,
2621 line_draw: material.flags.line_draw,
2622 },
2623 face_count: material.face_count,
2624 }
2625}
2626
2627fn build_pmx_parts_bones(descriptor: &PmxPartsDescriptor) -> Vec<PmxParsedBone> {
2628 if descriptor.bones.is_empty() {
2629 return vec![default_pmx_parts_root_bone()];
2630 }
2631 descriptor
2632 .bones
2633 .iter()
2634 .map(pmx_parts_bone_from_descriptor)
2635 .collect()
2636}
2637
2638fn build_pmx_parts_display_frames(descriptor: &PmxPartsDescriptor) -> Vec<PmxParsedDisplayFrame> {
2639 descriptor
2640 .display_frames
2641 .iter()
2642 .map(|frame| PmxParsedDisplayFrame {
2643 name: frame.name.clone(),
2644 english_name: frame.english_name.clone(),
2645 special: frame.special,
2646 frames: frame
2647 .frames
2648 .iter()
2649 .map(|item| PmxParsedDisplayFrameElement {
2650 kind: item.kind.clone(),
2651 index: item.index,
2652 })
2653 .collect(),
2654 })
2655 .collect()
2656}
2657
2658fn build_pmx_parts_morphs(descriptor: &PmxPartsDescriptor) -> Vec<PmxParsedMorph> {
2659 descriptor
2660 .morphs
2661 .iter()
2662 .map(pmx_parts_morph_from_descriptor)
2663 .collect()
2664}
2665
2666fn build_pmx_parts_rigid_bodies(descriptor: &PmxPartsDescriptor) -> Vec<PmxParsedRigidBody> {
2667 descriptor
2668 .rigid_bodies
2669 .iter()
2670 .map(|body| PmxParsedRigidBody {
2671 name: body.name.clone(),
2672 english_name: body.english_name.clone(),
2673 bone_index: body.bone_index,
2674 group: body.group,
2675 mask: body.mask,
2676 shape: body.shape.clone(),
2677 size: body.size,
2678 position: body.position,
2679 rotation: body.rotation,
2680 mass: body.mass,
2681 linear_damping: body.linear_damping,
2682 angular_damping: body.angular_damping,
2683 restitution: body.restitution,
2684 friction: body.friction,
2685 mode: body.mode.clone(),
2686 })
2687 .collect()
2688}
2689
2690fn build_pmx_parts_joints(descriptor: &PmxPartsDescriptor) -> Vec<PmxParsedJoint> {
2691 descriptor
2692 .joints
2693 .iter()
2694 .map(|joint| PmxParsedJoint {
2695 name: joint.name.clone(),
2696 english_name: joint.english_name.clone(),
2697 kind: joint.kind.clone(),
2698 rigid_body_index_a: joint.rigid_body_index_a,
2699 rigid_body_index_b: joint.rigid_body_index_b,
2700 position: joint.position,
2701 rotation: joint.rotation,
2702 translation_lower_limit: joint.translation_lower_limit,
2703 translation_upper_limit: joint.translation_upper_limit,
2704 rotation_lower_limit: joint.rotation_lower_limit,
2705 rotation_upper_limit: joint.rotation_upper_limit,
2706 spring_translation_factor: joint.spring_translation_factor,
2707 spring_rotation_factor: joint.spring_rotation_factor,
2708 })
2709 .collect()
2710}
2711
2712fn pmx_parts_morph_from_descriptor(morph: &PmxPartsMorphDescriptor) -> PmxParsedMorph {
2713 let mut parsed = PmxParsedMorph {
2714 name: morph.name.clone(),
2715 english_name: morph.english_name.clone(),
2716 panel: morph.panel.clone(),
2717 kind: morph.kind.clone(),
2718 vertex_offsets: Vec::new(),
2719 group_offsets: Vec::new(),
2720 bone_offsets: Vec::new(),
2721 uv_offsets: Vec::new(),
2722 additional_uv_offsets: Vec::new(),
2723 material_offsets: Vec::new(),
2724 flip_offsets: Vec::new(),
2725 impulse_offsets: Vec::new(),
2726 };
2727 match morph.kind.as_str() {
2728 "vertex" => {
2729 parsed.vertex_offsets = morph
2730 .vertex_offsets
2731 .iter()
2732 .map(|offset| PmxParsedVertexMorphOffset {
2733 vertex_index: offset.vertex_index,
2734 position: offset.position,
2735 })
2736 .collect();
2737 }
2738 "group" => {
2739 parsed.group_offsets = morph
2740 .group_offsets
2741 .iter()
2742 .map(|offset| PmxParsedGroupMorphOffset {
2743 morph_index: offset.morph_index,
2744 weight: offset.weight,
2745 })
2746 .collect();
2747 }
2748 _ => unreachable!("PmxPartsMorphDescriptor was validated before build"),
2749 }
2750 parsed
2751}
2752
2753fn pmx_parts_bone_from_descriptor(bone: &PmxPartsBoneDescriptor) -> PmxParsedBone {
2754 let tail_position = if bone.tail_index >= 0 {
2755 None
2756 } else {
2757 Some(bone.tail_position.unwrap_or([0.0, 1.0, 0.0]))
2758 };
2759 PmxParsedBone {
2760 name: bone.name.clone(),
2761 english_name: bone.english_name.clone(),
2762 parent_index: bone.parent_index,
2763 layer: bone.layer,
2764 position: bone.position,
2765 tail_index: bone.tail_index,
2766 tail_position,
2767 flags: PmxParsedBoneFlags {
2768 indexed_tail: bone.tail_index >= 0,
2769 rotatable: bone.rotatable,
2770 translatable: bone.translatable,
2771 visible: bone.visible,
2772 enabled: bone.enabled,
2773 ik: false,
2774 append_local: false,
2775 append_rotate: false,
2776 append_translate: false,
2777 fixed_axis: false,
2778 local_axis: false,
2779 external_parent_transform: false,
2780 transform_after_physics: false,
2781 },
2782 append_transform: None,
2783 fixed_axis: None,
2784 local_axis: None,
2785 external_parent_key: None,
2786 ik: None,
2787 }
2788}
2789
2790fn default_pmx_parts_material(
2791 name: &str,
2792 english_name: &str,
2793 face_count: i32,
2794) -> PmxParsedMaterial {
2795 PmxParsedMaterial {
2796 name: if name.is_empty() {
2797 "material".to_owned()
2798 } else {
2799 name.to_owned()
2800 },
2801 english_name: english_name.to_owned(),
2802 texture_path: String::new(),
2803 sphere_texture_path: String::new(),
2804 sphere_mode: "none".to_owned(),
2805 toon_texture_path: String::new(),
2806 shared_toon_index: Some(0),
2807 diffuse: [0.8, 0.8, 0.8, 1.0],
2808 specular: [0.0, 0.0, 0.0],
2809 specular_power: 1.0,
2810 ambient: [0.2, 0.2, 0.2],
2811 edge_color: [0.0, 0.0, 0.0, 1.0],
2812 edge_size: 1.0,
2813 flags: PmxParsedMaterialFlags {
2814 double_sided: true,
2815 ground_shadow: true,
2816 self_shadow_map: true,
2817 self_shadow: true,
2818 edge: false,
2819 vertex_color: false,
2820 point_draw: false,
2821 line_draw: false,
2822 },
2823 face_count,
2824 }
2825}
2826
2827fn default_pmx_parts_root_bone() -> PmxParsedBone {
2828 PmxParsedBone {
2829 name: "root".to_owned(),
2830 english_name: "root".to_owned(),
2831 parent_index: -1,
2832 layer: 0,
2833 position: [0.0, 0.0, 0.0],
2834 tail_index: -1,
2835 tail_position: Some([0.0, 1.0, 0.0]),
2836 flags: PmxParsedBoneFlags {
2837 indexed_tail: false,
2838 rotatable: true,
2839 translatable: true,
2840 visible: true,
2841 enabled: true,
2842 ik: false,
2843 append_local: false,
2844 append_rotate: false,
2845 append_translate: false,
2846 fixed_axis: false,
2847 local_axis: false,
2848 transform_after_physics: false,
2849 external_parent_transform: false,
2850 },
2851 append_transform: None,
2852 fixed_axis: None,
2853 local_axis: None,
2854 external_parent_key: None,
2855 ik: None,
2856 }
2857}
2858
2859pub fn parse_pmx_model(data: &[u8]) -> Result<PmxParsedModel, ImportError> {
2860 let (header, pos) = read_header(data)?;
2861 let mut r = Reader { data, pos };
2862 let name = r.read_string(header.encoding)?;
2863 let english_name = r.read_string(header.encoding)?;
2864 let comment = r.read_string(header.encoding)?;
2865 let english_comment = r.read_string(header.encoding)?;
2866
2867 let vertex_count = read_section_count_with_min_record(&mut r, 12 + 12 + 8 + 1 + 4)?;
2868 let geometry = read_parsed_geometry(&mut r, &header, vertex_count)?;
2869 let index_count = geometry.indices.len();
2870 let textures = read_parsed_textures(&mut r, header.encoding)?;
2871 let materials = read_parsed_materials(&mut r, &header, &textures)?;
2872 let material_groups = build_material_groups(&materials);
2873 let bone_count = read_section_count_with_min_record(&mut r, 4 + 4 + 12)?;
2874 let skeleton = PmxParsedSkeleton {
2875 bones: read_parsed_bones(&mut r, &header, bone_count)?,
2876 };
2877 let morphs = read_parsed_morphs(&mut r, &header)?;
2878 let display_frames = read_parsed_display_frames(&mut r, &header)?;
2879 let rigid_bodies = if r.remaining() >= 4 {
2880 read_parsed_rigid_bodies(&mut r, &header)?
2881 } else {
2882 Vec::new()
2883 };
2884 let joints = if r.remaining() >= 4 {
2885 read_parsed_joints(&mut r, &header)?
2886 } else {
2887 Vec::new()
2888 };
2889 let soft_bodies = if header.version >= 2.05 && r.remaining() >= 4 {
2890 read_parsed_soft_bodies(&mut r, &header)?
2891 } else {
2892 Vec::new()
2893 };
2894
2895 let mut geometry = geometry;
2896 geometry.material_groups = material_groups;
2897 let mut diagnostics = Vec::new();
2898 if !soft_bodies.is_empty() {
2899 diagnostics.push(PmxParserDiagnostic {
2900 level: "warning".to_owned(),
2901 code: "PMX_SOFT_BODY_UNSUPPORTED".to_owned(),
2902 message: format!(
2903 "{} PMX soft bodies are parsed only as header records by mmd-anim.",
2904 soft_bodies.len()
2905 ),
2906 });
2907 }
2908 if r.remaining() > 0 {
2909 diagnostics.push(PmxParserDiagnostic {
2910 level: "warning".to_owned(),
2911 code: "PMX_TRAILING_DATA_UNPARSED".to_owned(),
2912 message: format!("{} trailing PMX bytes were left unparsed.", r.remaining()),
2913 });
2914 }
2915
2916 Ok(PmxParsedModel {
2917 metadata: PmxParsedMetadata {
2918 format: "pmx".to_owned(),
2919 version: header.version,
2920 encoding: match header.encoding {
2921 TextEncoding::Utf8 => "utf-8".to_owned(),
2922 TextEncoding::Utf16Le => "utf-16-le".to_owned(),
2923 },
2924 name,
2925 english_name,
2926 comment,
2927 english_comment,
2928 counts: PmxParsedCounts {
2929 vertices: vertex_count,
2930 faces: index_count / 3,
2931 materials: materials.len(),
2932 bones: skeleton.bones.len(),
2933 morphs: morphs.len(),
2934 display_frames: display_frames.len(),
2935 rigid_bodies: rigid_bodies.len(),
2936 joints: joints.len(),
2937 soft_bodies: soft_bodies.len(),
2938 },
2939 index_sizes: PmxParsedIndexSizes {
2940 vertex: header.vertex_index_size,
2941 texture: header.texture_index_size,
2942 material: header.material_index_size,
2943 bone: header.bone_index_size,
2944 morph: header.morph_index_size,
2945 rigid_body: header.rigidbody_index_size,
2946 },
2947 additional_uv_count: header.extra_uv_count,
2948 },
2949 geometry,
2950 materials,
2951 skeleton,
2952 morphs,
2953 display_frames,
2954 rigid_bodies,
2955 joints,
2956 soft_bodies,
2957 diagnostics,
2958 })
2959}
2960
2961pub fn export_pmx_model(model: &PmxParsedModel) -> Vec<u8> {
2962 let encoding = pmx_text_encoding(&model.metadata);
2963 let mut out = Vec::new();
2964 out.extend_from_slice(&PMX_MAGIC);
2965 write_f32(&mut out, model.metadata.version);
2966 out.push(8);
2967 out.push(encoding as u8);
2968 out.push(model.metadata.additional_uv_count);
2969 out.push(model.metadata.index_sizes.vertex);
2970 out.push(model.metadata.index_sizes.texture);
2971 out.push(model.metadata.index_sizes.material);
2972 out.push(model.metadata.index_sizes.bone);
2973 out.push(model.metadata.index_sizes.morph);
2974 out.push(model.metadata.index_sizes.rigid_body);
2975
2976 write_pmx_string(&mut out, &model.metadata.name, encoding);
2977 write_pmx_string(&mut out, &model.metadata.english_name, encoding);
2978 write_pmx_string(&mut out, &model.metadata.comment, encoding);
2979 write_pmx_string(&mut out, &model.metadata.english_comment, encoding);
2980
2981 let vertex_count = model.geometry.positions.len() / 3;
2982 write_i32(&mut out, vertex_count as i32);
2983 for index in 0..vertex_count {
2984 write_f32_slice(
2985 &mut out,
2986 &model.geometry.positions[index * 3..index * 3 + 3],
2987 );
2988 write_f32_slice(&mut out, &model.geometry.normals[index * 3..index * 3 + 3]);
2989 write_f32_slice(&mut out, &model.geometry.uvs[index * 2..index * 2 + 2]);
2990 for uv in &model.geometry.additional_uvs {
2991 write_f32_slice(&mut out, &uv[index * 4..index * 4 + 4]);
2992 }
2993 write_pmx_vertex_skinning(&mut out, model, index);
2994 write_f32(
2995 &mut out,
2996 model.geometry.edge_scale.get(index).copied().unwrap_or(1.0),
2997 );
2998 }
2999
3000 write_i32(&mut out, model.geometry.indices.len() as i32);
3001 for &index in &model.geometry.indices {
3002 write_vertex_index(&mut out, index, model.metadata.index_sizes.vertex);
3003 }
3004
3005 let textures = collect_pmx_textures(&model.materials);
3006 write_i32(&mut out, textures.len() as i32);
3007 for texture in &textures {
3008 write_pmx_string(&mut out, texture, encoding);
3009 }
3010
3011 write_i32(&mut out, model.materials.len() as i32);
3012 for material in &model.materials {
3013 write_pmx_string(&mut out, &material.name, encoding);
3014 write_pmx_string(&mut out, &material.english_name, encoding);
3015 write_f32_slice(&mut out, &material.diffuse);
3016 write_f32_slice(&mut out, &material.specular);
3017 write_f32(&mut out, material.specular_power);
3018 write_f32_slice(&mut out, &material.ambient);
3019 out.push(material_flag_bits(&material.flags));
3020 write_f32_slice(&mut out, &material.edge_color);
3021 write_f32(&mut out, material.edge_size);
3022 write_sized_index(
3023 &mut out,
3024 texture_index(&textures, &material.texture_path),
3025 model.metadata.index_sizes.texture,
3026 );
3027 write_sized_index(
3028 &mut out,
3029 texture_index(&textures, &material.sphere_texture_path),
3030 model.metadata.index_sizes.texture,
3031 );
3032 out.push(match material.sphere_mode.as_str() {
3033 "multiply" => 1,
3034 "add" => 2,
3035 "subTexture" => 3,
3036 _ => 0,
3037 });
3038 if let Some(shared) = material.shared_toon_index {
3039 out.push(1);
3040 out.push(shared);
3041 } else {
3042 out.push(0);
3043 write_sized_index(
3044 &mut out,
3045 texture_index(&textures, &material.toon_texture_path),
3046 model.metadata.index_sizes.texture,
3047 );
3048 }
3049 write_pmx_string(&mut out, "", encoding);
3050 write_i32(&mut out, material.face_count.saturating_mul(3));
3051 }
3052
3053 write_i32(&mut out, model.skeleton.bones.len() as i32);
3054 for bone in &model.skeleton.bones {
3055 write_pmx_string(&mut out, &bone.name, encoding);
3056 write_pmx_string(&mut out, &bone.english_name, encoding);
3057 write_f32_slice(&mut out, &bone.position);
3058 write_sized_index(&mut out, bone.parent_index, model.metadata.index_sizes.bone);
3059 write_i32(&mut out, bone.layer);
3060 let bits = bone_flag_bits(&bone.flags);
3061 write_u16(&mut out, bits);
3062 if bone.flags.indexed_tail {
3063 write_sized_index(&mut out, bone.tail_index, model.metadata.index_sizes.bone);
3064 } else {
3065 write_f32_slice(&mut out, &bone.tail_position.unwrap_or([0.0; 3]));
3066 }
3067 if bone.flags.append_rotate || bone.flags.append_translate {
3068 let append = bone.append_transform.as_ref();
3069 write_sized_index(
3070 &mut out,
3071 append.map(|value| value.parent_index).unwrap_or(-1),
3072 model.metadata.index_sizes.bone,
3073 );
3074 write_f32(&mut out, append.map(|value| value.weight).unwrap_or(0.0));
3075 }
3076 if bone.flags.fixed_axis {
3077 write_f32_slice(&mut out, &bone.fixed_axis.unwrap_or([0.0; 3]));
3078 }
3079 if bone.flags.local_axis {
3080 if let Some(axis) = &bone.local_axis {
3081 write_f32_slice(&mut out, &axis.x);
3082 write_f32_slice(&mut out, &axis.z);
3083 } else {
3084 write_f32_slice(&mut out, &[1.0, 0.0, 0.0]);
3085 write_f32_slice(&mut out, &[0.0, 0.0, 1.0]);
3086 }
3087 }
3088 if bone.flags.external_parent_transform {
3089 write_i32(&mut out, bone.external_parent_key.unwrap_or(0));
3090 }
3091 if bone.flags.ik {
3092 if let Some(ik) = &bone.ik {
3093 write_sized_index(&mut out, ik.target_index, model.metadata.index_sizes.bone);
3094 write_i32(&mut out, ik.loop_count);
3095 write_f32(&mut out, ik.limit_angle);
3096 write_i32(&mut out, ik.links.len() as i32);
3097 for link in &ik.links {
3098 write_sized_index(&mut out, link.bone_index, model.metadata.index_sizes.bone);
3099 out.push(u8::from(link.limits.is_some()));
3100 if let Some(limits) = &link.limits {
3101 write_f32_slice(&mut out, &limits.lower);
3102 write_f32_slice(&mut out, &limits.upper);
3103 }
3104 }
3105 } else {
3106 write_sized_index(&mut out, -1, model.metadata.index_sizes.bone);
3107 write_i32(&mut out, 0);
3108 write_f32(&mut out, 0.0);
3109 write_i32(&mut out, 0);
3110 }
3111 }
3112 }
3113
3114 write_i32(&mut out, model.morphs.len() as i32);
3115 for morph in &model.morphs {
3116 write_pmx_string(&mut out, &morph.name, encoding);
3117 write_pmx_string(&mut out, &morph.english_name, encoding);
3118 out.push(morph_panel_byte(&morph.panel));
3119 let morph_type = morph_type_byte(morph);
3120 out.push(morph_type);
3121 write_i32(&mut out, morph_offset_count(morph, morph_type) as i32);
3122 match morph_type {
3123 0 => write_group_morph_offsets(&mut out, &morph.group_offsets, model),
3124 1 => {
3125 for offset in &morph.vertex_offsets {
3126 write_vertex_index(
3127 &mut out,
3128 offset.vertex_index,
3129 model.metadata.index_sizes.vertex,
3130 );
3131 write_f32_slice(&mut out, &offset.position);
3132 }
3133 }
3134 2 => {
3135 for offset in &morph.bone_offsets {
3136 write_sized_index(&mut out, offset.bone_index, model.metadata.index_sizes.bone);
3137 write_f32_slice(&mut out, &offset.translation);
3138 write_f32_slice(&mut out, &offset.rotation);
3139 }
3140 }
3141 3 => write_uv_morph_offsets(&mut out, &morph.uv_offsets, model),
3142 4..=7 => {
3143 for offset in &morph.additional_uv_offsets {
3144 write_vertex_index(
3145 &mut out,
3146 offset.vertex_index,
3147 model.metadata.index_sizes.vertex,
3148 );
3149 write_f32_slice(&mut out, &offset.uv);
3150 }
3151 }
3152 8 => {
3153 for offset in &morph.material_offsets {
3154 write_sized_index(
3155 &mut out,
3156 offset.material_index,
3157 model.metadata.index_sizes.material,
3158 );
3159 out.push(if offset.operation == "multiply" { 0 } else { 1 });
3160 write_f32_slice(&mut out, &offset.diffuse);
3161 write_f32_slice(&mut out, &offset.specular);
3162 write_f32(&mut out, offset.specular_power);
3163 write_f32_slice(&mut out, &offset.ambient);
3164 write_f32_slice(&mut out, &offset.edge_color);
3165 write_f32(&mut out, offset.edge_size);
3166 write_f32_slice(&mut out, &offset.texture_factor);
3167 write_f32_slice(&mut out, &offset.sphere_texture_factor);
3168 write_f32_slice(&mut out, &offset.toon_texture_factor);
3169 }
3170 }
3171 9 => write_group_morph_offsets(&mut out, &morph.flip_offsets, model),
3172 10 => {
3173 for offset in &morph.impulse_offsets {
3174 write_sized_index(
3175 &mut out,
3176 offset.rigid_body_index,
3177 model.metadata.index_sizes.rigid_body,
3178 );
3179 out.push(u8::from(offset.local));
3180 write_f32_slice(&mut out, &offset.velocity);
3181 write_f32_slice(&mut out, &offset.torque);
3182 }
3183 }
3184 _ => {}
3185 }
3186 }
3187
3188 write_i32(&mut out, model.display_frames.len() as i32);
3189 for frame in &model.display_frames {
3190 write_pmx_string(&mut out, &frame.name, encoding);
3191 write_pmx_string(&mut out, &frame.english_name, encoding);
3192 out.push(u8::from(frame.special));
3193 write_i32(&mut out, frame.frames.len() as i32);
3194 for item in &frame.frames {
3195 if item.kind == "morph" {
3196 out.push(1);
3197 write_sized_index(&mut out, item.index, model.metadata.index_sizes.morph);
3198 } else {
3199 out.push(0);
3200 write_sized_index(&mut out, item.index, model.metadata.index_sizes.bone);
3201 }
3202 }
3203 }
3204
3205 write_i32(&mut out, model.rigid_bodies.len() as i32);
3206 for body in &model.rigid_bodies {
3207 write_pmx_string(&mut out, &body.name, encoding);
3208 write_pmx_string(&mut out, &body.english_name, encoding);
3209 write_sized_index(&mut out, body.bone_index, model.metadata.index_sizes.bone);
3210 out.push(body.group);
3211 write_u16(&mut out, body.mask);
3212 out.push(match body.shape.as_str() {
3213 "box" => 1,
3214 "capsule" => 2,
3215 _ => 0,
3216 });
3217 write_f32_slice(&mut out, &body.size);
3218 write_f32_slice(&mut out, &body.position);
3219 write_f32_slice(&mut out, &body.rotation);
3220 write_f32(&mut out, body.mass);
3221 write_f32(&mut out, body.linear_damping);
3222 write_f32(&mut out, body.angular_damping);
3223 write_f32(&mut out, body.restitution);
3224 write_f32(&mut out, body.friction);
3225 out.push(match body.mode.as_str() {
3226 "dynamic" => 1,
3227 "dynamicBone" => 2,
3228 _ => 0,
3229 });
3230 }
3231
3232 write_i32(&mut out, model.joints.len() as i32);
3233 for joint in &model.joints {
3234 write_pmx_string(&mut out, &joint.name, encoding);
3235 write_pmx_string(&mut out, &joint.english_name, encoding);
3236 out.push(match joint.kind.as_str() {
3237 "generic6dof" => 1,
3238 "point2point" => 2,
3239 "coneTwist" => 3,
3240 "slider" => 4,
3241 "hinge" => 5,
3242 _ => 0,
3243 });
3244 write_sized_index(
3245 &mut out,
3246 joint.rigid_body_index_a,
3247 model.metadata.index_sizes.rigid_body,
3248 );
3249 write_sized_index(
3250 &mut out,
3251 joint.rigid_body_index_b,
3252 model.metadata.index_sizes.rigid_body,
3253 );
3254 write_f32_slice(&mut out, &joint.position);
3255 write_f32_slice(&mut out, &joint.rotation);
3256 write_f32_slice(&mut out, &joint.translation_lower_limit);
3257 write_f32_slice(&mut out, &joint.translation_upper_limit);
3258 write_f32_slice(&mut out, &joint.rotation_lower_limit);
3259 write_f32_slice(&mut out, &joint.rotation_upper_limit);
3260 write_f32_slice(&mut out, &joint.spring_translation_factor);
3261 write_f32_slice(&mut out, &joint.spring_rotation_factor);
3262 }
3263
3264 if model.metadata.version >= 2.05 {
3265 write_i32(&mut out, model.soft_bodies.len() as i32);
3266 for soft_body in &model.soft_bodies {
3267 write_pmx_string(&mut out, &soft_body.name, encoding);
3268 write_pmx_string(&mut out, &soft_body.english_name, encoding);
3269 out.push(if soft_body.kind == "rope" { 1 } else { 0 });
3270 write_sized_index(
3271 &mut out,
3272 soft_body.material_index,
3273 model.metadata.index_sizes.material,
3274 );
3275 out.push(soft_body.collision_group);
3276 write_u16(&mut out, soft_body.collision_mask);
3277 out.push(soft_body.flags);
3278 write_i32(&mut out, soft_body.bending_constraints_distance);
3279 write_i32(&mut out, soft_body.cluster_count);
3280 write_f32(&mut out, soft_body.total_mass);
3281 write_f32(&mut out, soft_body.collision_margin);
3282 out.extend_from_slice(&[0u8; 4 + 12 * 4 + 6 * 4 + 4 * 4 + 3 * 4]);
3283 write_i32(&mut out, 0);
3284 write_i32(&mut out, 0);
3285 }
3286 }
3287
3288 out
3289}
3290
3291fn read_section_count(r: &mut Reader<'_>) -> Result<usize, ImportError> {
3292 let count = r.read_i32_le()?;
3293 if count < 0 {
3294 return Err(ImportError::SectionOverflow);
3295 }
3296 Ok(count as usize)
3297}
3298
3299fn read_section_count_with_min_record(
3300 r: &mut Reader<'_>,
3301 min_record_size: usize,
3302) -> Result<usize, ImportError> {
3303 let count = read_section_count(r)?;
3304 r.require_record_bytes(count, min_record_size)?;
3305 Ok(count)
3306}
3307
3308fn read_parsed_geometry(
3309 r: &mut Reader<'_>,
3310 header: &PmxHeader,
3311 vertex_count: usize,
3312) -> Result<PmxParsedGeometry, ImportError> {
3313 r.require_record_bytes(vertex_count, 12 + 12 + 8 + 1 + 4)?;
3314 let position_capacity = vertex_count
3315 .checked_mul(3)
3316 .ok_or(ImportError::SectionOverflow)?;
3317 let uv_capacity = vertex_count
3318 .checked_mul(2)
3319 .ok_or(ImportError::SectionOverflow)?;
3320 let skin_capacity = vertex_count
3321 .checked_mul(4)
3322 .ok_or(ImportError::SectionOverflow)?;
3323
3324 let mut positions = Vec::with_capacity(position_capacity);
3325 let mut normals = Vec::with_capacity(position_capacity);
3326 let mut uvs = Vec::with_capacity(uv_capacity);
3327 let mut additional_uvs =
3328 vec![Vec::with_capacity(skin_capacity); header.extra_uv_count as usize];
3329 let mut skin_indices = Vec::with_capacity(skin_capacity);
3330 let mut skin_weights = Vec::with_capacity(skin_capacity);
3331 let mut skinning_modes = Vec::with_capacity(vertex_count);
3332 let mut edge_scale = Vec::with_capacity(vertex_count);
3333 let mut sdef_enabled = vec![0.0f32; vertex_count];
3334 let mut sdef_c = vec![0.0f32; vertex_count * 3];
3335 let mut sdef_r0 = vec![0.0f32; vertex_count * 3];
3336 let mut sdef_r1 = vec![0.0f32; vertex_count * 3];
3337 let mut sdef_rw0 = vec![0.0f32; vertex_count * 3];
3338 let mut sdef_rw1 = vec![0.0f32; vertex_count * 3];
3339 let mut qdef_enabled = vec![0.0f32; vertex_count];
3340
3341 for vertex_i in 0..vertex_count {
3342 positions.extend_from_slice(&r.read_vec3_array()?);
3343 normals.extend_from_slice(&r.read_vec3_array()?);
3344 uvs.extend_from_slice(&r.read_vec2_array()?);
3345 for uv in additional_uvs.iter_mut() {
3346 uv.extend_from_slice(&r.read_vec4_array()?);
3347 }
3348 let weight_type = r.read_u8()?;
3349 let mut indices = [0u32; 4];
3350 let mut weights = [0.0f32; 4];
3351 match weight_type {
3352 0 => {
3353 skinning_modes.push(PMX_SKINNING_MODE_BDEF1.to_owned());
3354 indices[0] =
3355 normalize_nonnegative_index(r.read_sized_index(header.bone_index_size)?);
3356 weights[0] = 1.0;
3357 }
3358 1 | 3 => {
3359 skinning_modes.push(
3360 if weight_type == 3 {
3361 PMX_SKINNING_MODE_SDEF
3362 } else {
3363 PMX_SKINNING_MODE_BDEF2
3364 }
3365 .to_owned(),
3366 );
3367 indices[0] =
3368 normalize_nonnegative_index(r.read_sized_index(header.bone_index_size)?);
3369 indices[1] =
3370 normalize_nonnegative_index(r.read_sized_index(header.bone_index_size)?);
3371 let w = r.read_f32_le()?;
3372 weights[0] = w;
3373 weights[1] = 1.0 - w;
3374 if weight_type == 3 {
3375 let c = r.read_vec3_array()?;
3376 let r0 = r.read_vec3_array()?;
3377 let r1 = r.read_vec3_array()?;
3378 let w1 = 1.0 - w;
3379 let offset = vertex_i * 3;
3380 sdef_enabled[vertex_i] = 1.0;
3381 sdef_c[offset..offset + 3].copy_from_slice(&c);
3382 sdef_r0[offset..offset + 3].copy_from_slice(&r0);
3383 sdef_r1[offset..offset + 3].copy_from_slice(&r1);
3384 for k in 0..3 {
3385 let rw = r0[k] * w + r1[k] * w1;
3386 sdef_rw0[offset + k] = (c[k] * 2.0 + r0[k] - rw) * 0.5;
3387 sdef_rw1[offset + k] = (c[k] * 2.0 + r1[k] - rw) * 0.5;
3388 }
3389 }
3390 }
3391 2 => {
3392 skinning_modes.push(PMX_SKINNING_MODE_BDEF4.to_owned());
3393 for index in &mut indices {
3394 *index =
3395 normalize_nonnegative_index(r.read_sized_index(header.bone_index_size)?);
3396 }
3397 for weight in &mut weights {
3398 *weight = r.read_f32_le()?;
3399 }
3400 }
3401 4 => {
3402 skinning_modes.push(PMX_SKINNING_MODE_QDEF.to_owned());
3403 for index in &mut indices {
3404 *index =
3405 normalize_nonnegative_index(r.read_sized_index(header.bone_index_size)?);
3406 }
3407 for weight in &mut weights {
3408 *weight = r.read_f32_le()?;
3409 }
3410 qdef_enabled[vertex_i] = 1.0;
3411 }
3412 _ => return Err(ImportError::SectionOverflow),
3413 }
3414 skin_indices.extend_from_slice(&indices);
3415 skin_weights.extend_from_slice(&weights);
3416 edge_scale.push(r.read_f32_le()?);
3417 }
3418
3419 let index_count = read_section_count_with_min_record(r, header.vertex_index_size as usize)?;
3420 let mut indices = Vec::with_capacity(index_count);
3421 for _ in 0..index_count {
3422 indices.push(r.read_vertex_index(header.vertex_index_size)?);
3423 }
3424
3425 Ok(PmxParsedGeometry {
3426 positions,
3427 normals,
3428 uvs,
3429 additional_uvs,
3430 indices,
3431 skin_indices,
3432 skin_weights,
3433 edge_scale,
3434 material_groups: Vec::new(),
3435 sdef: PmxParsedSdef {
3436 skinning_modes,
3437 enabled: sdef_enabled,
3438 c: sdef_c,
3439 r0: sdef_r0,
3440 r1: sdef_r1,
3441 rw0: sdef_rw0,
3442 rw1: sdef_rw1,
3443 },
3444 qdef: PmxParsedQdef {
3445 enabled: qdef_enabled,
3446 },
3447 })
3448}
3449
3450fn normalize_nonnegative_index(index: i32) -> u32 {
3451 if index < 0 { 0 } else { index as u32 }
3452}
3453
3454pub fn parse_pmx_material_split(
3455 data: &[u8],
3456 _flags: u32,
3457) -> Result<PmxMaterialSplitResult, ImportError> {
3458 let model = parse_pmx_model(data)?;
3459 Ok(split_pmx_model_by_material(&model))
3460}
3461
3462pub fn split_pmx_model_by_material(model: &PmxParsedModel) -> PmxMaterialSplitResult {
3463 let mut meshes = Vec::with_capacity(model.geometry.material_groups.len());
3464 let mut manifest_meshes = Vec::with_capacity(model.geometry.material_groups.len());
3465 let mut diagnostics = Vec::new();
3466
3467 for (mesh_index, group) in model.geometry.material_groups.iter().enumerate() {
3468 if group.material_index >= model.materials.len() {
3469 let diagnostic = pmx_material_split_diagnostic(
3470 "PMX_MATERIAL_SPLIT_MATERIAL_INDEX_OUT_OF_RANGE",
3471 format!(
3472 "material group {mesh_index} references material {} but material count is {}",
3473 group.material_index,
3474 model.materials.len()
3475 ),
3476 );
3477 diagnostics.push(diagnostic);
3478 continue;
3479 }
3480
3481 let mesh = split_pmx_material_group(model, group, mesh_index);
3482 manifest_meshes.push(PmxMaterialSplitManifestMesh {
3483 mesh_index,
3484 original_material_index: mesh.original_material_index,
3485 original_vertex_indices: mesh.original_vertex_indices.clone(),
3486 morph_index_map: mesh.morph_index_map.clone(),
3487 diagnostics: mesh.diagnostics.clone(),
3488 });
3489 diagnostics.extend(mesh.diagnostics.iter().cloned());
3490 meshes.push(mesh);
3491 }
3492
3493 PmxMaterialSplitResult {
3494 manifest: PmxMaterialSplitManifest {
3495 mesh_count: meshes.len(),
3496 meshes: manifest_meshes,
3497 diagnostics,
3498 },
3499 meshes,
3500 }
3501}
3502
3503fn split_pmx_material_group(
3504 model: &PmxParsedModel,
3505 group: &PmxParsedMaterialGroup,
3506 mesh_index: usize,
3507) -> PmxMaterialSplitMesh {
3508 let mut diagnostics = Vec::new();
3509 let index_end = group.start.saturating_add(group.count);
3510 let index_slice = match model.geometry.indices.get(group.start..index_end) {
3511 Some(slice) => slice,
3512 None => {
3513 diagnostics.push(pmx_material_split_diagnostic(
3514 "PMX_MATERIAL_SPLIT_INDEX_RANGE_OUT_OF_RANGE",
3515 format!(
3516 "material group {mesh_index} index range {}..{} exceeds index count {}",
3517 group.start,
3518 index_end,
3519 model.geometry.indices.len()
3520 ),
3521 ));
3522 &[][..]
3523 }
3524 };
3525
3526 let vertex_count = model.geometry.positions.len() / 3;
3527 let mut original_vertex_indices = Vec::new();
3528 let mut vertex_remap = HashMap::<u32, u32>::new();
3529 let mut local_indices = Vec::with_capacity(index_slice.len());
3530 for &original_index in index_slice {
3531 if original_index as usize >= vertex_count {
3532 diagnostics.push(pmx_material_split_diagnostic(
3533 "PMX_MATERIAL_SPLIT_VERTEX_INDEX_OUT_OF_RANGE",
3534 format!(
3535 "material group {mesh_index} references vertex {original_index} but vertex count is {vertex_count}"
3536 ),
3537 ));
3538 continue;
3539 }
3540 let local_index = match vertex_remap.get(&original_index) {
3541 Some(&local_index) => local_index,
3542 None => {
3543 let local_index = original_vertex_indices.len() as u32;
3544 vertex_remap.insert(original_index, local_index);
3545 original_vertex_indices.push(original_index);
3546 local_index
3547 }
3548 };
3549 local_indices.push(local_index);
3550 }
3551
3552 let local_index_count = local_indices.len();
3553 let geometry = remap_pmx_geometry(
3554 &model.geometry,
3555 &original_vertex_indices,
3556 local_indices,
3557 local_index_count,
3558 );
3559 let material = {
3560 let mut material = model.materials[group.material_index].clone();
3561 material.face_count = (geometry.indices.len() / 3) as i32;
3562 material
3563 };
3564 let (morphs, morph_index_map, morph_diagnostics) =
3565 remap_pmx_material_split_morphs(model, &vertex_remap, group.material_index, mesh_index);
3566 diagnostics.extend(morph_diagnostics);
3567
3568 PmxMaterialSplitMesh {
3569 original_material_index: group.material_index,
3570 original_vertex_indices,
3571 geometry,
3572 material,
3573 morphs,
3574 morph_index_map,
3575 diagnostics,
3576 }
3577}
3578
3579fn remap_pmx_geometry(
3580 geometry: &PmxParsedGeometry,
3581 original_vertex_indices: &[u32],
3582 indices: Vec<u32>,
3583 local_index_count: usize,
3584) -> PmxParsedGeometry {
3585 PmxParsedGeometry {
3586 positions: gather_vertex_f32(&geometry.positions, 3, original_vertex_indices),
3587 normals: gather_vertex_f32(&geometry.normals, 3, original_vertex_indices),
3588 uvs: gather_vertex_f32(&geometry.uvs, 2, original_vertex_indices),
3589 additional_uvs: geometry
3590 .additional_uvs
3591 .iter()
3592 .map(|uvs| gather_vertex_f32(uvs, 4, original_vertex_indices))
3593 .collect(),
3594 indices,
3595 skin_indices: gather_vertex_u32(&geometry.skin_indices, 4, original_vertex_indices),
3596 skin_weights: gather_vertex_f32(&geometry.skin_weights, 4, original_vertex_indices),
3597 edge_scale: gather_vertex_f32(&geometry.edge_scale, 1, original_vertex_indices),
3598 material_groups: vec![PmxParsedMaterialGroup {
3599 start: 0,
3600 count: local_index_count,
3601 material_index: 0,
3602 }],
3603 sdef: PmxParsedSdef {
3604 skinning_modes: gather_vertex_string(
3605 &geometry.sdef.skinning_modes,
3606 original_vertex_indices,
3607 ),
3608 enabled: gather_vertex_f32(&geometry.sdef.enabled, 1, original_vertex_indices),
3609 c: gather_vertex_f32(&geometry.sdef.c, 3, original_vertex_indices),
3610 r0: gather_vertex_f32(&geometry.sdef.r0, 3, original_vertex_indices),
3611 r1: gather_vertex_f32(&geometry.sdef.r1, 3, original_vertex_indices),
3612 rw0: gather_vertex_f32(&geometry.sdef.rw0, 3, original_vertex_indices),
3613 rw1: gather_vertex_f32(&geometry.sdef.rw1, 3, original_vertex_indices),
3614 },
3615 qdef: PmxParsedQdef {
3616 enabled: gather_vertex_f32(&geometry.qdef.enabled, 1, original_vertex_indices),
3617 },
3618 }
3619}
3620
3621fn gather_vertex_f32(values: &[f32], stride: usize, original_vertex_indices: &[u32]) -> Vec<f32> {
3622 let mut out = Vec::with_capacity(original_vertex_indices.len() * stride);
3623 for &index in original_vertex_indices {
3624 let start = index as usize * stride;
3625 let end = start + stride;
3626 if let Some(slice) = values.get(start..end) {
3627 out.extend_from_slice(slice);
3628 }
3629 }
3630 out
3631}
3632
3633fn gather_vertex_string(values: &[String], original_vertex_indices: &[u32]) -> Vec<String> {
3634 let mut out = Vec::with_capacity(original_vertex_indices.len());
3635 for &index in original_vertex_indices {
3636 if let Some(value) = values.get(index as usize) {
3637 out.push(value.clone());
3638 }
3639 }
3640 out
3641}
3642
3643fn gather_vertex_u32(values: &[u32], stride: usize, original_vertex_indices: &[u32]) -> Vec<u32> {
3644 let mut out = Vec::with_capacity(original_vertex_indices.len() * stride);
3645 for &index in original_vertex_indices {
3646 let start = index as usize * stride;
3647 let end = start + stride;
3648 if let Some(slice) = values.get(start..end) {
3649 out.extend_from_slice(slice);
3650 }
3651 }
3652 out
3653}
3654
3655fn remap_pmx_material_split_morphs(
3656 model: &PmxParsedModel,
3657 vertex_remap: &HashMap<u32, u32>,
3658 original_material_index: usize,
3659 mesh_index: usize,
3660) -> (
3661 Vec<PmxParsedMorph>,
3662 Vec<PmxMaterialSplitMorphIndexMap>,
3663 Vec<PmxParserDiagnostic>,
3664) {
3665 let mut diagnostics = Vec::new();
3666 let mut direct_morphs = vec![None; model.morphs.len()];
3667 let mut survives = vec![false; model.morphs.len()];
3668 let mut morph_remap = HashMap::<usize, usize>::new();
3669
3670 for (original_index, morph) in model.morphs.iter().enumerate() {
3671 match morph.kind.as_str() {
3672 "vertex" | "uv" | "additionalUv" | "material" => {
3673 let remapped = remap_direct_pmx_material_split_morph(
3674 morph,
3675 vertex_remap,
3676 original_material_index,
3677 );
3678 if pmx_material_split_morph_has_offsets(&remapped) {
3679 direct_morphs[original_index] = Some(remapped);
3680 survives[original_index] = true;
3681 }
3682 }
3683 "bone" => {
3684 if !morph.bone_offsets.is_empty() {
3685 diagnostics.push(pmx_material_split_diagnostic(
3686 "PMX_MATERIAL_SPLIT_BONE_MORPH_SHARED_SKELETON",
3687 format!(
3688 "mesh {mesh_index} excludes bone morph {original_index} because skeleton morphs are shared"
3689 ),
3690 ));
3691 }
3692 }
3693 "impulse" => {
3694 if !morph.impulse_offsets.is_empty() {
3695 diagnostics.push(pmx_material_split_diagnostic(
3696 "PMX_MATERIAL_SPLIT_IMPULSE_MORPH_UNSUPPORTED",
3697 format!(
3698 "mesh {mesh_index} excludes impulse morph {original_index} because physics morphs are not exposed in the initial material split ABI"
3699 ),
3700 ));
3701 }
3702 }
3703 "group" | "flip" => {}
3704 _ => {
3705 diagnostics.push(pmx_material_split_diagnostic(
3706 "PMX_MATERIAL_SPLIT_MORPH_KIND_UNSUPPORTED",
3707 format!(
3708 "mesh {mesh_index} excludes morph {original_index} with unsupported kind {}",
3709 morph.kind
3710 ),
3711 ));
3712 }
3713 }
3714 }
3715
3716 let mut changed = true;
3717 while changed {
3718 changed = false;
3719 for (original_index, morph) in model.morphs.iter().enumerate() {
3720 if survives[original_index] || (morph.kind != "group" && morph.kind != "flip") {
3721 continue;
3722 }
3723 if pmx_material_split_reference_offsets(morph)
3724 .iter()
3725 .any(|offset| {
3726 offset.morph_index >= 0
3727 && survives
3728 .get(offset.morph_index as usize)
3729 .copied()
3730 .unwrap_or(false)
3731 })
3732 {
3733 survives[original_index] = true;
3734 changed = true;
3735 }
3736 }
3737 }
3738
3739 for (original_index, survives) in survives.iter().copied().enumerate() {
3740 if survives {
3741 let local_index = morph_remap.len();
3742 morph_remap.insert(original_index, local_index);
3743 }
3744 }
3745
3746 let mut morphs = Vec::with_capacity(morph_remap.len());
3747 for (original_index, morph) in model.morphs.iter().enumerate() {
3748 if !morph_remap.contains_key(&original_index) {
3749 continue;
3750 }
3751 let remapped = match direct_morphs[original_index].take() {
3752 Some(remapped) => remapped,
3753 None => remap_reference_pmx_material_split_morph(morph, &morph_remap),
3754 };
3755 morphs.push((original_index, remapped));
3756 }
3757
3758 let morph_index_map = morphs
3759 .iter()
3760 .enumerate()
3761 .map(
3762 |(local_index, (original_index, _))| PmxMaterialSplitMorphIndexMap {
3763 original_index: *original_index,
3764 local_index,
3765 },
3766 )
3767 .collect();
3768 let morphs = morphs
3769 .into_iter()
3770 .map(|(_, morph)| morph)
3771 .collect::<Vec<_>>();
3772
3773 (morphs, morph_index_map, diagnostics)
3774}
3775
3776fn remap_direct_pmx_material_split_morph(
3777 morph: &PmxParsedMorph,
3778 vertex_remap: &HashMap<u32, u32>,
3779 original_material_index: usize,
3780) -> PmxParsedMorph {
3781 let mut remapped = empty_pmx_material_split_morph_like(morph);
3782 remapped.vertex_offsets = morph
3783 .vertex_offsets
3784 .iter()
3785 .filter_map(|offset| {
3786 vertex_remap
3787 .get(&offset.vertex_index)
3788 .map(|&vertex_index| PmxParsedVertexMorphOffset {
3789 vertex_index,
3790 position: offset.position,
3791 })
3792 })
3793 .collect();
3794 remapped.uv_offsets = morph
3795 .uv_offsets
3796 .iter()
3797 .filter_map(|offset| {
3798 vertex_remap
3799 .get(&offset.vertex_index)
3800 .map(|&vertex_index| PmxParsedUvMorphOffset {
3801 vertex_index,
3802 uv: offset.uv,
3803 })
3804 })
3805 .collect();
3806 remapped.additional_uv_offsets = morph
3807 .additional_uv_offsets
3808 .iter()
3809 .filter_map(|offset| {
3810 vertex_remap.get(&offset.vertex_index).map(|&vertex_index| {
3811 PmxParsedAdditionalUvMorphOffset {
3812 vertex_index,
3813 uv_index: offset.uv_index,
3814 uv: offset.uv,
3815 }
3816 })
3817 })
3818 .collect();
3819 remapped.material_offsets = morph
3820 .material_offsets
3821 .iter()
3822 .filter_map(|offset| {
3823 if offset.material_index == -1
3824 || offset.material_index as usize == original_material_index
3825 {
3826 let mut remapped = offset.clone();
3827 if remapped.material_index >= 0 {
3828 remapped.material_index = 0;
3829 }
3830 Some(remapped)
3831 } else {
3832 None
3833 }
3834 })
3835 .collect();
3836 remapped
3837}
3838
3839fn remap_reference_pmx_material_split_morph(
3840 morph: &PmxParsedMorph,
3841 morph_remap: &HashMap<usize, usize>,
3842) -> PmxParsedMorph {
3843 let mut remapped = empty_pmx_material_split_morph_like(morph);
3844 if morph.kind == "group" {
3845 remapped.group_offsets =
3846 remap_pmx_material_split_morph_refs(&morph.group_offsets, morph_remap);
3847 } else {
3848 remapped.flip_offsets =
3849 remap_pmx_material_split_morph_refs(&morph.flip_offsets, morph_remap);
3850 }
3851 remapped
3852}
3853
3854fn pmx_material_split_reference_offsets(morph: &PmxParsedMorph) -> &[PmxParsedGroupMorphOffset] {
3855 if morph.kind == "group" {
3856 &morph.group_offsets
3857 } else {
3858 &morph.flip_offsets
3859 }
3860}
3861
3862fn remap_pmx_material_split_morph_refs(
3863 offsets: &[PmxParsedGroupMorphOffset],
3864 morph_remap: &HashMap<usize, usize>,
3865) -> Vec<PmxParsedGroupMorphOffset> {
3866 offsets
3867 .iter()
3868 .filter_map(|offset| {
3869 if offset.morph_index < 0 {
3870 return None;
3871 }
3872 morph_remap
3873 .get(&(offset.morph_index as usize))
3874 .map(|&morph_index| PmxParsedGroupMorphOffset {
3875 morph_index: morph_index as i32,
3876 weight: offset.weight,
3877 })
3878 })
3879 .collect()
3880}
3881
3882fn empty_pmx_material_split_morph_like(morph: &PmxParsedMorph) -> PmxParsedMorph {
3883 PmxParsedMorph {
3884 name: morph.name.clone(),
3885 english_name: morph.english_name.clone(),
3886 panel: morph.panel.clone(),
3887 kind: morph.kind.clone(),
3888 vertex_offsets: Vec::new(),
3889 group_offsets: Vec::new(),
3890 bone_offsets: Vec::new(),
3891 uv_offsets: Vec::new(),
3892 additional_uv_offsets: Vec::new(),
3893 material_offsets: Vec::new(),
3894 flip_offsets: Vec::new(),
3895 impulse_offsets: Vec::new(),
3896 }
3897}
3898
3899fn pmx_material_split_morph_has_offsets(morph: &PmxParsedMorph) -> bool {
3900 !morph.vertex_offsets.is_empty()
3901 || !morph.group_offsets.is_empty()
3902 || !morph.bone_offsets.is_empty()
3903 || !morph.uv_offsets.is_empty()
3904 || !morph.additional_uv_offsets.is_empty()
3905 || !morph.material_offsets.is_empty()
3906 || !morph.flip_offsets.is_empty()
3907 || !morph.impulse_offsets.is_empty()
3908}
3909
3910fn pmx_material_split_diagnostic(
3911 code: impl Into<String>,
3912 message: impl Into<String>,
3913) -> PmxParserDiagnostic {
3914 PmxParserDiagnostic {
3915 level: "info".to_owned(),
3916 code: code.into(),
3917 message: message.into(),
3918 }
3919}
3920
3921fn read_parsed_textures(
3922 r: &mut Reader<'_>,
3923 encoding: TextEncoding,
3924) -> Result<Vec<String>, ImportError> {
3925 let count = read_section_count_with_min_record(r, 4)?;
3926 let mut textures = Vec::with_capacity(count);
3927 for _ in 0..count {
3928 textures.push(r.read_string(encoding)?);
3929 }
3930 Ok(textures)
3931}
3932
3933fn read_parsed_materials(
3934 r: &mut Reader<'_>,
3935 header: &PmxHeader,
3936 textures: &[String],
3937) -> Result<Vec<PmxParsedMaterial>, ImportError> {
3938 let count = read_section_count_with_min_record(r, 4 + 4 + 16 + 16 + 12 + 1 + 16 + 4)?;
3939 let mut materials = Vec::with_capacity(count);
3940 for _ in 0..count {
3941 let name = r.read_string(header.encoding)?;
3942 let english_name = r.read_string(header.encoding)?;
3943 let diffuse = r.read_vec4_array()?;
3944 let specular = r.read_vec3_array()?;
3945 let specular_power = r.read_f32_le()?;
3946 let ambient = r.read_vec3_array()?;
3947 let flag_bits = r.read_u8()?;
3948 let edge_color = r.read_vec4_array()?;
3949 let edge_size = r.read_f32_le()?;
3950 let texture_index = r.read_sized_index(header.texture_index_size)?;
3951 let sphere_texture_index = r.read_sized_index(header.texture_index_size)?;
3952 let sphere_mode_raw = r.read_u8()?;
3953 let toon_flag = r.read_u8()?;
3954 let (toon_texture_path, shared_toon_index) = if toon_flag == 0 {
3955 let toon_texture_index = r.read_sized_index(header.texture_index_size)?;
3956 (texture_path(textures, toon_texture_index), None)
3957 } else {
3958 (String::new(), Some(r.read_u8()?))
3959 };
3960 let _memo = r.read_string(header.encoding)?;
3961 let face_count = r.read_i32_le()? / 3;
3962 materials.push(PmxParsedMaterial {
3963 name,
3964 english_name,
3965 texture_path: texture_path(textures, texture_index),
3966 sphere_texture_path: texture_path(textures, sphere_texture_index),
3967 sphere_mode: match sphere_mode_raw {
3968 1 => "multiply",
3969 2 => "add",
3970 3 => "subTexture",
3971 _ => "none",
3972 }
3973 .to_owned(),
3974 toon_texture_path,
3975 shared_toon_index,
3976 diffuse,
3977 specular,
3978 specular_power,
3979 ambient,
3980 edge_color,
3981 edge_size,
3982 flags: PmxParsedMaterialFlags {
3983 double_sided: flag_bits & 0x01 != 0,
3984 ground_shadow: flag_bits & 0x02 != 0,
3985 self_shadow_map: flag_bits & 0x04 != 0,
3986 self_shadow: flag_bits & 0x08 != 0,
3987 edge: flag_bits & 0x10 != 0,
3988 vertex_color: flag_bits & 0x20 != 0,
3989 point_draw: flag_bits & 0x40 != 0,
3990 line_draw: flag_bits & 0x80 != 0,
3991 },
3992 face_count,
3993 });
3994 }
3995 Ok(materials)
3996}
3997
3998fn texture_path(textures: &[String], index: i32) -> String {
3999 if index < 0 {
4000 String::new()
4001 } else {
4002 textures.get(index as usize).cloned().unwrap_or_default()
4003 }
4004}
4005
4006fn build_material_groups(materials: &[PmxParsedMaterial]) -> Vec<PmxParsedMaterialGroup> {
4007 let mut start = 0usize;
4008 materials
4009 .iter()
4010 .enumerate()
4011 .map(|(material_index, material)| {
4012 let count = (material.face_count.max(0) as usize) * 3;
4013 let group = PmxParsedMaterialGroup {
4014 start,
4015 count,
4016 material_index,
4017 };
4018 start += count;
4019 group
4020 })
4021 .collect()
4022}
4023
4024fn pmx_text_encoding(metadata: &PmxParsedMetadata) -> TextEncoding {
4025 match metadata.encoding.as_str() {
4026 "utf-16-le" => TextEncoding::Utf16Le,
4027 _ => TextEncoding::Utf8,
4028 }
4029}
4030
4031fn write_pmx_string(out: &mut Vec<u8>, text: &str, encoding: TextEncoding) {
4032 match encoding {
4033 TextEncoding::Utf8 => {
4034 write_i32(out, text.len() as i32);
4035 out.extend_from_slice(text.as_bytes());
4036 }
4037 TextEncoding::Utf16Le => {
4038 let bytes = text
4039 .encode_utf16()
4040 .flat_map(u16::to_le_bytes)
4041 .collect::<Vec<_>>();
4042 write_i32(out, bytes.len() as i32);
4043 out.extend_from_slice(&bytes);
4044 }
4045 }
4046}
4047
4048fn write_sized_index(out: &mut Vec<u8>, value: i32, size: u8) {
4049 match size {
4050 1 => out.push(value as i8 as u8),
4051 2 => out.extend_from_slice(&(value as i16).to_le_bytes()),
4052 4 => out.extend_from_slice(&value.to_le_bytes()),
4053 _ => {}
4054 }
4055}
4056
4057fn write_vertex_index(out: &mut Vec<u8>, value: u32, size: u8) {
4058 match size {
4059 1 => out.push(value as u8),
4060 2 => out.extend_from_slice(&(value as u16).to_le_bytes()),
4061 4 => out.extend_from_slice(&(value as i32).to_le_bytes()),
4062 _ => {}
4063 }
4064}
4065
4066fn write_pmx_vertex_skinning(out: &mut Vec<u8>, model: &PmxParsedModel, vertex_index: usize) {
4067 match pmx_vertex_skinning_mode(&model.geometry, vertex_index) {
4068 PMX_SKINNING_MODE_BDEF1 => {
4069 out.push(0);
4070 write_pmx_vertex_bone_index(out, model, vertex_index, 0);
4071 }
4072 PMX_SKINNING_MODE_BDEF2 => {
4073 out.push(1);
4074 write_pmx_vertex_bone_index(out, model, vertex_index, 0);
4075 write_pmx_vertex_bone_index(out, model, vertex_index, 1);
4076 write_f32(
4077 out,
4078 pmx_vertex_skin_weight(&model.geometry, vertex_index, 0),
4079 );
4080 }
4081 PMX_SKINNING_MODE_SDEF => {
4082 out.push(3);
4083 write_pmx_vertex_bone_index(out, model, vertex_index, 0);
4084 write_pmx_vertex_bone_index(out, model, vertex_index, 1);
4085 write_f32(
4086 out,
4087 pmx_vertex_skin_weight(&model.geometry, vertex_index, 0),
4088 );
4089 write_f32_slice(
4090 out,
4091 &pmx_vertex_sdef_vec3(&model.geometry.sdef.c, vertex_index),
4092 );
4093 write_f32_slice(
4094 out,
4095 &pmx_vertex_sdef_vec3(&model.geometry.sdef.r0, vertex_index),
4096 );
4097 write_f32_slice(
4098 out,
4099 &pmx_vertex_sdef_vec3(&model.geometry.sdef.r1, vertex_index),
4100 );
4101 }
4102 PMX_SKINNING_MODE_QDEF => {
4103 out.push(4);
4104 write_pmx_vertex_bdef4_skinning(out, model, vertex_index);
4105 }
4106 _ => {
4107 out.push(2);
4108 write_pmx_vertex_bdef4_skinning(out, model, vertex_index);
4109 }
4110 }
4111}
4112
4113fn pmx_vertex_skinning_mode(geometry: &PmxParsedGeometry, vertex_index: usize) -> &str {
4114 if let Some(mode) = geometry.sdef.skinning_modes.get(vertex_index) {
4115 return mode.as_str();
4116 }
4117 if geometry
4118 .sdef
4119 .enabled
4120 .get(vertex_index)
4121 .copied()
4122 .unwrap_or(0.0)
4123 > 0.5
4124 {
4125 PMX_SKINNING_MODE_SDEF
4126 } else if geometry
4127 .qdef
4128 .enabled
4129 .get(vertex_index)
4130 .copied()
4131 .unwrap_or(0.0)
4132 > 0.5
4133 {
4134 PMX_SKINNING_MODE_QDEF
4135 } else {
4136 PMX_SKINNING_MODE_BDEF4
4137 }
4138}
4139
4140fn write_pmx_vertex_bdef4_skinning(out: &mut Vec<u8>, model: &PmxParsedModel, vertex_index: usize) {
4141 for slot in 0..4 {
4142 write_pmx_vertex_bone_index(out, model, vertex_index, slot);
4143 }
4144 for slot in 0..4 {
4145 write_f32(
4146 out,
4147 pmx_vertex_skin_weight(&model.geometry, vertex_index, slot),
4148 );
4149 }
4150}
4151
4152fn write_pmx_vertex_bone_index(
4153 out: &mut Vec<u8>,
4154 model: &PmxParsedModel,
4155 vertex_index: usize,
4156 slot: usize,
4157) {
4158 write_sized_index(
4159 out,
4160 model
4161 .geometry
4162 .skin_indices
4163 .get(vertex_index * 4 + slot)
4164 .copied()
4165 .unwrap_or(0) as i32,
4166 model.metadata.index_sizes.bone,
4167 );
4168}
4169
4170fn pmx_vertex_skin_weight(geometry: &PmxParsedGeometry, vertex_index: usize, slot: usize) -> f32 {
4171 geometry
4172 .skin_weights
4173 .get(vertex_index * 4 + slot)
4174 .copied()
4175 .unwrap_or(if slot == 0 { 1.0 } else { 0.0 })
4176}
4177
4178fn pmx_vertex_sdef_vec3(values: &[f32], vertex_index: usize) -> [f32; 3] {
4179 let start = vertex_index * 3;
4180 [
4181 values.get(start).copied().unwrap_or(0.0),
4182 values.get(start + 1).copied().unwrap_or(0.0),
4183 values.get(start + 2).copied().unwrap_or(0.0),
4184 ]
4185}
4186
4187fn material_flag_bits(flags: &PmxParsedMaterialFlags) -> u8 {
4188 u8::from(flags.double_sided)
4189 | (u8::from(flags.ground_shadow) << 1)
4190 | (u8::from(flags.self_shadow_map) << 2)
4191 | (u8::from(flags.self_shadow) << 3)
4192 | (u8::from(flags.edge) << 4)
4193 | (u8::from(flags.vertex_color) << 5)
4194 | (u8::from(flags.point_draw) << 6)
4195 | (u8::from(flags.line_draw) << 7)
4196}
4197
4198fn bone_flag_bits(flags: &PmxParsedBoneFlags) -> u16 {
4199 u16::from(flags.indexed_tail)
4200 | (u16::from(flags.rotatable) << 1)
4201 | (u16::from(flags.translatable) << 2)
4202 | (u16::from(flags.visible) << 3)
4203 | (u16::from(flags.enabled) << 4)
4204 | if flags.ik { BONE_FLAG_IK } else { 0 }
4205 | if flags.append_local {
4206 BONE_FLAG_LOCAL_APPEND
4207 } else {
4208 0
4209 }
4210 | if flags.append_rotate {
4211 BONE_FLAG_APPEND_ROTATE
4212 } else {
4213 0
4214 }
4215 | if flags.append_translate {
4216 BONE_FLAG_APPEND_TRANSLATE
4217 } else {
4218 0
4219 }
4220 | if flags.fixed_axis {
4221 BONE_FLAG_FIXED_AXIS
4222 } else {
4223 0
4224 }
4225 | if flags.local_axis {
4226 BONE_FLAG_LOCAL_AXIS
4227 } else {
4228 0
4229 }
4230 | if flags.transform_after_physics {
4231 BONE_FLAG_TRANSFORM_AFTER_PHYSICS
4232 } else {
4233 0
4234 }
4235 | if flags.external_parent_transform {
4236 BONE_FLAG_EXTERNAL_PARENT
4237 } else {
4238 0
4239 }
4240}
4241
4242fn collect_pmx_textures(materials: &[PmxParsedMaterial]) -> Vec<String> {
4243 let mut textures = Vec::new();
4244 for texture in materials.iter().flat_map(|material| {
4245 [
4246 material.texture_path.as_str(),
4247 material.sphere_texture_path.as_str(),
4248 material.toon_texture_path.as_str(),
4249 ]
4250 }) {
4251 if !texture.is_empty() && !textures.iter().any(|existing| existing == texture) {
4252 textures.push(texture.to_owned());
4253 }
4254 }
4255 textures
4256}
4257
4258fn texture_index(textures: &[String], texture: &str) -> i32 {
4259 if texture.is_empty() {
4260 -1
4261 } else {
4262 textures
4263 .iter()
4264 .position(|candidate| candidate == texture)
4265 .map(|index| index as i32)
4266 .unwrap_or(-1)
4267 }
4268}
4269
4270fn morph_type_byte(morph: &PmxParsedMorph) -> u8 {
4271 match morph.kind.as_str() {
4272 "group" => 0,
4273 "vertex" => 1,
4274 "bone" => 2,
4275 "uv" => 3,
4276 "additionalUv" => morph
4277 .additional_uv_offsets
4278 .first()
4279 .map(|offset| offset.uv_index.saturating_add(4).min(7))
4280 .unwrap_or(4),
4281 "material" => 8,
4282 "flip" => 9,
4283 "impulse" => 10,
4284 _ => 0,
4285 }
4286}
4287
4288fn morph_panel_name(panel: u8) -> String {
4289 match panel {
4290 0 => "system",
4291 1 => "brow",
4292 2 => "eye",
4293 3 => "mouth",
4294 4 => "other",
4295 _ => "unknown",
4296 }
4297 .to_owned()
4298}
4299
4300fn morph_panel_byte(panel: &str) -> u8 {
4301 morph_panel_byte_checked(panel).unwrap_or(4)
4302}
4303
4304fn morph_panel_byte_checked(panel: &str) -> Option<u8> {
4305 match panel {
4306 "system" => Some(0),
4307 "brow" => Some(1),
4308 "eye" => Some(2),
4309 "mouth" => Some(3),
4310 "other" => Some(4),
4311 _ => None,
4312 }
4313}
4314
4315fn morph_offset_count(morph: &PmxParsedMorph, morph_type: u8) -> usize {
4316 match morph_type {
4317 0 => morph.group_offsets.len(),
4318 1 => morph.vertex_offsets.len(),
4319 2 => morph.bone_offsets.len(),
4320 3 => morph.uv_offsets.len(),
4321 4..=7 => morph.additional_uv_offsets.len(),
4322 8 => morph.material_offsets.len(),
4323 9 => morph.flip_offsets.len(),
4324 10 => morph.impulse_offsets.len(),
4325 _ => 0,
4326 }
4327}
4328
4329fn write_group_morph_offsets(
4330 out: &mut Vec<u8>,
4331 offsets: &[PmxParsedGroupMorphOffset],
4332 model: &PmxParsedModel,
4333) {
4334 for offset in offsets {
4335 write_sized_index(out, offset.morph_index, model.metadata.index_sizes.morph);
4336 write_f32(out, offset.weight);
4337 }
4338}
4339
4340fn write_uv_morph_offsets(
4341 out: &mut Vec<u8>,
4342 offsets: &[PmxParsedUvMorphOffset],
4343 model: &PmxParsedModel,
4344) {
4345 for offset in offsets {
4346 write_vertex_index(out, offset.vertex_index, model.metadata.index_sizes.vertex);
4347 write_f32_slice(out, &offset.uv);
4348 }
4349}
4350
4351fn read_parsed_bones(
4352 r: &mut Reader<'_>,
4353 header: &PmxHeader,
4354 count: usize,
4355) -> Result<Vec<PmxParsedBone>, ImportError> {
4356 r.require_record_bytes(count, 4 + 4 + 12)?;
4357 let mut bones = Vec::with_capacity(count);
4358 for _ in 0..count {
4359 let name = r.read_string(header.encoding)?;
4360 let english_name = r.read_string(header.encoding)?;
4361 let position = r.read_vec3_array()?;
4362 let parent_index = r.read_sized_index(header.bone_index_size)?;
4363 let layer = r.read_i32_le()?;
4364 let bits = r.read_u16_le()?;
4365 let flags = parsed_bone_flags(bits);
4366 let (tail_index, tail_position) = if flags.indexed_tail {
4367 (r.read_sized_index(header.bone_index_size)?, None)
4368 } else {
4369 (-1, Some(r.read_vec3_array()?))
4370 };
4371 let append_transform = if flags.append_rotate || flags.append_translate {
4372 Some(PmxParsedAppendTransform {
4373 parent_index: r.read_sized_index(header.bone_index_size)?,
4374 weight: r.read_f32_le()?,
4375 })
4376 } else {
4377 None
4378 };
4379 let fixed_axis = if flags.fixed_axis {
4380 Some(r.read_vec3_array()?)
4381 } else {
4382 None
4383 };
4384 let local_axis = if flags.local_axis {
4385 Some(PmxParsedLocalAxis {
4386 x: r.read_vec3_array()?,
4387 z: r.read_vec3_array()?,
4388 })
4389 } else {
4390 None
4391 };
4392 let external_parent_key = if flags.external_parent_transform {
4393 Some(r.read_i32_le()?)
4394 } else {
4395 None
4396 };
4397 let ik = if flags.ik {
4398 let target_index = r.read_sized_index(header.bone_index_size)?;
4399 let loop_count = r.read_i32_le()?;
4400 let limit_angle = r.read_f32_le()?;
4401 let link_count =
4402 read_section_count_with_min_record(r, header.bone_index_size as usize + 1)?;
4403 let mut links = Vec::with_capacity(link_count);
4404 for _ in 0..link_count {
4405 let bone_index = r.read_sized_index(header.bone_index_size)?;
4406 let has_limit = r.read_u8()? != 0;
4407 links.push(PmxParsedIkLink {
4408 bone_index,
4409 limits: if has_limit {
4410 Some(PmxParsedIkLimit {
4411 lower: r.read_vec3_array()?,
4412 upper: r.read_vec3_array()?,
4413 })
4414 } else {
4415 None
4416 },
4417 });
4418 }
4419 Some(PmxParsedIk {
4420 target_index,
4421 loop_count,
4422 limit_angle,
4423 links,
4424 })
4425 } else {
4426 None
4427 };
4428 bones.push(PmxParsedBone {
4429 name,
4430 english_name,
4431 parent_index,
4432 layer,
4433 position,
4434 tail_index,
4435 tail_position,
4436 flags,
4437 append_transform,
4438 fixed_axis,
4439 local_axis,
4440 external_parent_key,
4441 ik,
4442 });
4443 }
4444 Ok(bones)
4445}
4446
4447fn parsed_bone_flags(bits: u16) -> PmxParsedBoneFlags {
4448 PmxParsedBoneFlags {
4449 indexed_tail: bits & BONE_FLAG_TAIL_INDEX != 0,
4450 rotatable: bits & 0x0002 != 0,
4451 translatable: bits & 0x0004 != 0,
4452 visible: bits & 0x0008 != 0,
4453 enabled: bits & 0x0010 != 0,
4454 ik: bits & BONE_FLAG_IK != 0,
4455 append_local: bits & BONE_FLAG_LOCAL_APPEND != 0,
4456 append_rotate: bits & BONE_FLAG_APPEND_ROTATE != 0,
4457 append_translate: bits & BONE_FLAG_APPEND_TRANSLATE != 0,
4458 fixed_axis: bits & BONE_FLAG_FIXED_AXIS != 0,
4459 local_axis: bits & BONE_FLAG_LOCAL_AXIS != 0,
4460 transform_after_physics: bits & 0x1000 != 0,
4461 external_parent_transform: bits & BONE_FLAG_EXTERNAL_PARENT != 0,
4462 }
4463}
4464
4465fn read_parsed_morphs(
4466 r: &mut Reader<'_>,
4467 header: &PmxHeader,
4468) -> Result<Vec<PmxParsedMorph>, ImportError> {
4469 let count = read_section_count_with_min_record(r, 4 + 4 + 1 + 1 + 4)?;
4470 let mut morphs = Vec::with_capacity(count);
4471 for _ in 0..count {
4472 let name = r.read_string(header.encoding)?;
4473 let english_name = r.read_string(header.encoding)?;
4474 let panel = morph_panel_name(r.read_u8()?);
4475 let morph_type = r.read_u8()?;
4476 let min_offset_size = match morph_type {
4477 0 => header.morph_index_size as usize + 4,
4478 1 => header.vertex_index_size as usize + 12,
4479 2 => header.bone_index_size as usize + 12 + 16,
4480 3..=7 => header.vertex_index_size as usize + 16,
4481 8 => header.material_index_size as usize + 1 + 16 + 12 + 4 + 12 + 16 + 4 + 16 + 16 + 16,
4482 9 => header.morph_index_size as usize + 4,
4483 10 => header.rigidbody_index_size as usize + 1 + 12 + 12,
4484 _ => return Err(ImportError::SectionOverflow),
4485 };
4486 let offset_count = read_section_count_with_min_record(r, min_offset_size)?;
4487 let mut morph = PmxParsedMorph {
4488 name,
4489 english_name,
4490 panel,
4491 kind: match morph_type {
4492 0 => "group",
4493 1 => "vertex",
4494 2 => "bone",
4495 3 => "uv",
4496 4..=7 => "additionalUv",
4497 8 => "material",
4498 9 => "flip",
4499 10 => "impulse",
4500 _ => "unknown",
4501 }
4502 .to_owned(),
4503 vertex_offsets: Vec::new(),
4504 group_offsets: Vec::new(),
4505 bone_offsets: Vec::new(),
4506 uv_offsets: Vec::new(),
4507 additional_uv_offsets: Vec::new(),
4508 material_offsets: Vec::new(),
4509 flip_offsets: Vec::new(),
4510 impulse_offsets: Vec::new(),
4511 };
4512 for _ in 0..offset_count {
4513 match morph_type {
4514 0 => morph.group_offsets.push(PmxParsedGroupMorphOffset {
4515 morph_index: r.read_sized_index(header.morph_index_size)?,
4516 weight: r.read_f32_le()?,
4517 }),
4518 1 => morph.vertex_offsets.push(PmxParsedVertexMorphOffset {
4519 vertex_index: r.read_vertex_index(header.vertex_index_size)?,
4520 position: r.read_vec3_array()?,
4521 }),
4522 2 => morph.bone_offsets.push(PmxParsedBoneMorphOffset {
4523 bone_index: r.read_sized_index(header.bone_index_size)?,
4524 translation: r.read_vec3_array()?,
4525 rotation: r.read_vec4_array()?,
4526 }),
4527 3 => morph.uv_offsets.push(PmxParsedUvMorphOffset {
4528 vertex_index: r.read_vertex_index(header.vertex_index_size)?,
4529 uv: r.read_vec4_array()?,
4530 }),
4531 4..=7 => morph
4532 .additional_uv_offsets
4533 .push(PmxParsedAdditionalUvMorphOffset {
4534 vertex_index: r.read_vertex_index(header.vertex_index_size)?,
4535 uv_index: morph_type - 4,
4536 uv: r.read_vec4_array()?,
4537 }),
4538 8 => morph.material_offsets.push(PmxParsedMaterialMorphOffset {
4539 material_index: r.read_sized_index(header.material_index_size)?,
4540 operation: if r.read_u8()? == 0 {
4541 "multiply".to_owned()
4542 } else {
4543 "add".to_owned()
4544 },
4545 diffuse: r.read_vec4_array()?,
4546 specular: r.read_vec3_array()?,
4547 specular_power: r.read_f32_le()?,
4548 ambient: r.read_vec3_array()?,
4549 edge_color: r.read_vec4_array()?,
4550 edge_size: r.read_f32_le()?,
4551 texture_factor: r.read_vec4_array()?,
4552 sphere_texture_factor: r.read_vec4_array()?,
4553 toon_texture_factor: r.read_vec4_array()?,
4554 }),
4555 9 => morph.flip_offsets.push(PmxParsedGroupMorphOffset {
4556 morph_index: r.read_sized_index(header.morph_index_size)?,
4557 weight: r.read_f32_le()?,
4558 }),
4559 10 => morph.impulse_offsets.push(PmxParsedImpulseMorphOffset {
4560 rigid_body_index: r.read_sized_index(header.rigidbody_index_size)?,
4561 local: r.read_u8()? != 0,
4562 velocity: r.read_vec3_array()?,
4563 torque: r.read_vec3_array()?,
4564 }),
4565 _ => return Err(ImportError::SectionOverflow),
4566 }
4567 }
4568 morphs.push(morph);
4569 }
4570 Ok(morphs)
4571}
4572
4573fn read_parsed_display_frames(
4574 r: &mut Reader<'_>,
4575 header: &PmxHeader,
4576) -> Result<Vec<PmxParsedDisplayFrame>, ImportError> {
4577 let count = read_section_count_with_min_record(r, 4 + 4 + 1 + 4)?;
4578 let mut frames = Vec::with_capacity(count);
4579 for _ in 0..count {
4580 let name = r.read_string(header.encoding)?;
4581 let english_name = r.read_string(header.encoding)?;
4582 let special = r.read_u8()? == 1;
4583 let item_count = read_section_count_with_min_record(r, 1)?;
4584 let mut items = Vec::with_capacity(item_count);
4585 for _ in 0..item_count {
4586 let kind = r.read_u8()?;
4587 let index = match kind {
4588 0 => r.read_sized_index(header.bone_index_size)?,
4589 1 => r.read_sized_index(header.morph_index_size)?,
4590 _ => return Err(ImportError::SectionOverflow),
4591 };
4592 items.push(PmxParsedDisplayFrameElement {
4593 kind: if kind == 0 {
4594 "bone".to_owned()
4595 } else {
4596 "morph".to_owned()
4597 },
4598 index,
4599 });
4600 }
4601 frames.push(PmxParsedDisplayFrame {
4602 name,
4603 english_name,
4604 special,
4605 frames: items,
4606 });
4607 }
4608 Ok(frames)
4609}
4610
4611fn read_parsed_rigid_bodies(
4612 r: &mut Reader<'_>,
4613 header: &PmxHeader,
4614) -> Result<Vec<PmxParsedRigidBody>, ImportError> {
4615 let count = read_section_count_with_min_record(
4616 r,
4617 4 + 4 + header.bone_index_size as usize + 1 + 2 + 1 + 12 + 12 + 12 + 4 + 4 + 4 + 4 + 4 + 1,
4618 )?;
4619 let mut bodies = Vec::with_capacity(count);
4620 for _ in 0..count {
4621 bodies.push(PmxParsedRigidBody {
4622 name: r.read_string(header.encoding)?,
4623 english_name: r.read_string(header.encoding)?,
4624 bone_index: r.read_sized_index(header.bone_index_size)?,
4625 group: r.read_u8()?,
4626 mask: r.read_u16_le()?,
4627 shape: match r.read_u8()? {
4628 0 => "sphere",
4629 1 => "box",
4630 2 => "capsule",
4631 _ => "unknown",
4632 }
4633 .to_owned(),
4634 size: r.read_vec3_array()?,
4635 position: r.read_vec3_array()?,
4636 rotation: r.read_vec3_array()?,
4637 mass: r.read_f32_le()?,
4638 linear_damping: r.read_f32_le()?,
4639 angular_damping: r.read_f32_le()?,
4640 restitution: r.read_f32_le()?,
4641 friction: r.read_f32_le()?,
4642 mode: match r.read_u8()? {
4643 0 => "static",
4644 1 => "dynamic",
4645 2 => "dynamicBone",
4646 _ => "unknown",
4647 }
4648 .to_owned(),
4649 });
4650 }
4651 Ok(bodies)
4652}
4653
4654fn read_parsed_joints(
4655 r: &mut Reader<'_>,
4656 header: &PmxHeader,
4657) -> Result<Vec<PmxParsedJoint>, ImportError> {
4658 let count = read_section_count_with_min_record(
4659 r,
4660 4 + 4 + 1 + header.rigidbody_index_size as usize * 2 + 12 * 8,
4661 )?;
4662 let mut joints = Vec::with_capacity(count);
4663 for _ in 0..count {
4664 joints.push(PmxParsedJoint {
4665 name: r.read_string(header.encoding)?,
4666 english_name: r.read_string(header.encoding)?,
4667 kind: match r.read_u8()? {
4668 0 => "generic6dofSpring",
4669 1 => "generic6dof",
4670 2 => "point2point",
4671 3 => "coneTwist",
4672 4 => "slider",
4673 5 => "hinge",
4674 _ => "unknown",
4675 }
4676 .to_owned(),
4677 rigid_body_index_a: r.read_sized_index(header.rigidbody_index_size)?,
4678 rigid_body_index_b: r.read_sized_index(header.rigidbody_index_size)?,
4679 position: r.read_vec3_array()?,
4680 rotation: r.read_vec3_array()?,
4681 translation_lower_limit: r.read_vec3_array()?,
4682 translation_upper_limit: r.read_vec3_array()?,
4683 rotation_lower_limit: r.read_vec3_array()?,
4684 rotation_upper_limit: r.read_vec3_array()?,
4685 spring_translation_factor: r.read_vec3_array()?,
4686 spring_rotation_factor: r.read_vec3_array()?,
4687 });
4688 }
4689 Ok(joints)
4690}
4691
4692fn read_parsed_soft_bodies(
4693 r: &mut Reader<'_>,
4694 header: &PmxHeader,
4695) -> Result<Vec<PmxParsedSoftBody>, ImportError> {
4696 let count = read_section_count_with_min_record(
4697 r,
4698 4 + 4
4699 + 1
4700 + header.material_index_size as usize
4701 + 1
4702 + 2
4703 + 1
4704 + 4
4705 + 4
4706 + 4
4707 + 4
4708 + 4
4709 + 12 * 4
4710 + 6 * 4
4711 + 4 * 4
4712 + 3 * 4
4713 + 4
4714 + 4,
4715 )?;
4716 let mut soft_bodies = Vec::with_capacity(count);
4717 for _ in 0..count {
4718 let name = r.read_string(header.encoding)?;
4719 let english_name = r.read_string(header.encoding)?;
4720 let kind = match r.read_u8()? {
4721 0 => "triMesh",
4722 1 => "rope",
4723 _ => "unknown",
4724 }
4725 .to_owned();
4726 let material_index = r.read_sized_index(header.material_index_size)?;
4727 let collision_group = r.read_u8()?;
4728 let collision_mask = r.read_u16_le()?;
4729 let flags = r.read_u8()?;
4730 let bending_constraints_distance = r.read_i32_le()?;
4731 let cluster_count = r.read_i32_le()?;
4732 let total_mass = r.read_f32_le()?;
4733 let collision_margin = r.read_f32_le()?;
4734 r.skip(4 + 12 * 4 + 6 * 4 + 4 * 4 + 3 * 4)?;
4735 let anchor_count = read_section_count_with_min_record(
4736 r,
4737 header.rigidbody_index_size as usize + header.vertex_index_size as usize + 1,
4738 )?;
4739 for _ in 0..anchor_count {
4740 r.read_sized_index(header.rigidbody_index_size)?;
4741 r.read_vertex_index(header.vertex_index_size)?;
4742 r.skip(1)?;
4743 }
4744 let pin_count = read_section_count_with_min_record(r, header.vertex_index_size as usize)?;
4745 for _ in 0..pin_count {
4746 r.read_vertex_index(header.vertex_index_size)?;
4747 }
4748 soft_bodies.push(PmxParsedSoftBody {
4749 name,
4750 english_name,
4751 kind,
4752 material_index,
4753 collision_group,
4754 collision_mask,
4755 flags,
4756 bending_constraints_distance,
4757 cluster_count,
4758 total_mass,
4759 collision_margin,
4760 });
4761 }
4762 Ok(soft_bodies)
4763}
4764
4765#[derive(Debug)]
4766pub struct PmxRuntimeImport {
4767 pub model: ModelArena,
4768 pub bone_names: Vec<String>,
4769 pub bone_name_to_index: HashMap<Vec<u8>, BoneIndex>,
4770 pub morph_name_to_index: HashMap<Vec<u8>, MorphIndex>,
4771 pub ik_solver_bone_name_to_index: HashMap<Vec<u8>, usize>,
4772}
4773
4774pub fn import_pmx_runtime(data: &[u8]) -> Result<PmxRuntimeImport, ImportError> {
4775 let (header, pos) = read_header(data)?;
4776 let pos = skip_model_info(data, &header, pos)?;
4777 let pos = skip_vertices(data, &header, pos)?;
4778 let pos = skip_faces(data, header.vertex_index_size, pos)?;
4779 let pos = skip_textures(data, header.encoding, pos)?;
4780 let pos = skip_materials(data, &header, pos)?;
4781 let (bone_read, pos) = read_bones_with_local_axes(data, &header, pos)?;
4782 let PmxBoneRead {
4783 descriptor: bone_descriptor,
4784 bone_name_bytes,
4785 bone_names,
4786 } = bone_read;
4787 let (morph_names, morph_descriptor, _pos) = read_runtime_morph_descriptor(data, &header, pos)?;
4788
4789 let mut bone_name_to_index = HashMap::with_capacity(bone_name_bytes.len() * 2);
4790 for (i, bytes) in bone_name_bytes.iter().enumerate() {
4791 let index = BoneIndex(i as u32);
4792 bone_name_to_index.insert(bytes.clone(), index);
4793 let decoded = bone_names[i].as_bytes();
4794 if decoded != bytes.as_slice() && !decoded.is_empty() {
4795 bone_name_to_index.insert(decoded.to_vec(), index);
4796 }
4797 }
4798
4799 let mut morph_name_to_index = HashMap::with_capacity(morph_names.name_bytes.len() * 2);
4800 for (i, bytes) in morph_names.name_bytes.iter().enumerate() {
4801 let index = MorphIndex(i as u32);
4802 morph_name_to_index.insert(bytes.clone(), index);
4803 let decoded = morph_names.names[i].as_bytes();
4804 if decoded != bytes.as_slice() && !decoded.is_empty() {
4805 morph_name_to_index.insert(decoded.to_vec(), index);
4806 }
4807 }
4808
4809 let mut ik_solver_bone_name_to_index =
4810 HashMap::with_capacity(bone_descriptor.ik_solvers.len() * 2);
4811 for (solver_idx, solver) in bone_descriptor.ik_solvers.iter().enumerate() {
4812 let bone_idx = solver.ik_bone.as_usize();
4813 if bone_idx < bone_name_bytes.len() {
4814 let bytes = &bone_name_bytes[bone_idx];
4815 ik_solver_bone_name_to_index.insert(bytes.clone(), solver_idx);
4816 let decoded = bone_names[bone_idx].as_bytes();
4817 if decoded != bytes.as_slice() && !decoded.is_empty() {
4818 ik_solver_bone_name_to_index.insert(decoded.to_vec(), solver_idx);
4819 }
4820 }
4821 }
4822
4823 let descriptor = RuntimeModelDescriptorV1 {
4824 morphs: morph_descriptor,
4825 ..bone_descriptor
4826 };
4827 let model = compile_runtime_model_descriptor_v1(&descriptor).map_err(|error| {
4828 ImportError::ModelBuildFailed(
4829 mmd_anim_runtime::ModelBuildError::InvalidRuntimeDescriptor {
4830 path: error.path,
4831 reason: error.kind.to_string(),
4832 },
4833 )
4834 })?;
4835
4836 Ok(PmxRuntimeImport {
4837 model,
4838 bone_names,
4839 bone_name_to_index,
4840 morph_name_to_index,
4841 ik_solver_bone_name_to_index,
4842 })
4843}
4844
4845#[cfg(test)]
4846mod tests {
4847 use super::*;
4848 use glam::{Vec3, Vec3A};
4849 use mmd_anim_runtime::RuntimeInstance;
4850 use std::sync::Arc;
4851
4852 fn build_small_pmx_header_bytes(bone_index_size: u8, encoding: TextEncoding) -> Vec<u8> {
4853 let mut buf = Vec::new();
4854 buf.extend_from_slice(b"PMX ");
4855 buf.extend_from_slice(&2.0f32.to_le_bytes());
4856 buf.push(8);
4857 buf.push(encoding as u8);
4858 buf.push(0);
4859 buf.push(4);
4860 buf.push(1);
4861 buf.push(1);
4862 buf.push(bone_index_size);
4863 buf.push(1);
4864 buf.push(1);
4865 buf
4866 }
4867
4868 fn build_empty_model_info(_encoding: TextEncoding) -> Vec<u8> {
4869 let mut buf = Vec::new();
4870 for _ in 0..4 {
4871 buf.extend_from_slice(&0i32.to_le_bytes());
4872 }
4873 buf
4874 }
4875
4876 fn build_empty_vertex_section() -> Vec<u8> {
4877 vec![0u8, 0, 0, 0]
4878 }
4879
4880 fn build_empty_face_section() -> Vec<u8> {
4881 vec![0u8, 0, 0, 0]
4882 }
4883
4884 fn build_empty_texture_section() -> Vec<u8> {
4885 vec![0u8, 0, 0, 0]
4886 }
4887
4888 fn build_empty_material_section() -> Vec<u8> {
4889 vec![0u8, 0, 0, 0]
4890 }
4891
4892 #[test]
4893 fn rejects_impossible_pmx_vertex_count_before_allocation() {
4894 let mut buf = build_small_pmx_header_bytes(1, TextEncoding::Utf8);
4895 buf.extend_from_slice(&build_empty_model_info(TextEncoding::Utf8));
4896 buf.extend_from_slice(&i32::MAX.to_le_bytes());
4897
4898 assert!(matches!(
4899 parse_pmx_model(&buf),
4900 Err(ImportError::UnexpectedEof(_))
4901 ));
4902 assert!(matches!(
4903 import_pmx_runtime(&buf),
4904 Err(ImportError::UnexpectedEof(_))
4905 ));
4906 }
4907
4908 #[test]
4909 fn rejects_invalid_pmx_index_sizes_in_header() {
4910 let buf = build_small_pmx_header_bytes(3, TextEncoding::Utf8);
4911
4912 assert!(matches!(
4913 read_header(&buf),
4914 Err(ImportError::InvalidIndexSize(3))
4915 ));
4916 }
4917
4918 fn build_bone_name_bytes(name: &str) -> Vec<u8> {
4919 let mut buf = Vec::new();
4920 let name_bytes = name.as_bytes();
4921 buf.extend_from_slice(&(name_bytes.len() as i32).to_le_bytes());
4922 buf.extend_from_slice(name_bytes);
4923 buf
4924 }
4925
4926 fn build_bone_section_header(count: u32) -> Vec<u8> {
4927 let mut buf = Vec::new();
4928 buf.extend_from_slice(&count.to_le_bytes());
4929 buf
4930 }
4931
4932 fn build_morph_name_bytes(name: &str) -> Vec<u8> {
4933 let mut buf = Vec::new();
4934 let name_bytes = name.as_bytes();
4935 buf.extend_from_slice(&(name_bytes.len() as i32).to_le_bytes());
4936 buf.extend_from_slice(name_bytes);
4937 buf
4938 }
4939
4940 fn empty_pmx_flags() -> PmxParsedMaterialFlags {
4941 PmxParsedMaterialFlags {
4942 double_sided: false,
4943 ground_shadow: false,
4944 self_shadow_map: false,
4945 self_shadow: false,
4946 edge: false,
4947 vertex_color: false,
4948 point_draw: false,
4949 line_draw: false,
4950 }
4951 }
4952
4953 fn basic_bone_flags(indexed_tail: bool) -> PmxParsedBoneFlags {
4954 PmxParsedBoneFlags {
4955 indexed_tail,
4956 rotatable: true,
4957 translatable: false,
4958 visible: true,
4959 enabled: true,
4960 ik: false,
4961 append_local: false,
4962 append_rotate: false,
4963 append_translate: false,
4964 fixed_axis: false,
4965 local_axis: false,
4966 transform_after_physics: false,
4967 external_parent_transform: false,
4968 }
4969 }
4970
4971 fn parsed_pmx_fixture() -> PmxParsedModel {
4972 PmxParsedModel {
4973 metadata: PmxParsedMetadata {
4974 format: "pmx".to_owned(),
4975 version: 2.0,
4976 encoding: "utf-8".to_owned(),
4977 name: "model".to_owned(),
4978 english_name: "model-en".to_owned(),
4979 comment: "comment".to_owned(),
4980 english_comment: "comment-en".to_owned(),
4981 counts: PmxParsedCounts {
4982 vertices: 1,
4983 faces: 1,
4984 materials: 1,
4985 bones: 1,
4986 morphs: 1,
4987 display_frames: 1,
4988 rigid_bodies: 1,
4989 joints: 1,
4990 soft_bodies: 0,
4991 },
4992 index_sizes: PmxParsedIndexSizes {
4993 vertex: 4,
4994 texture: 1,
4995 material: 1,
4996 bone: 2,
4997 morph: 1,
4998 rigid_body: 1,
4999 },
5000 additional_uv_count: 1,
5001 },
5002 geometry: PmxParsedGeometry {
5003 positions: vec![1.0, 2.0, 3.0],
5004 normals: vec![0.0, 1.0, 0.0],
5005 uvs: vec![0.25, 0.75],
5006 additional_uvs: vec![vec![1.0, 2.0, 3.0, 4.0]],
5007 indices: vec![0, 0, 0],
5008 skin_indices: vec![0, 0, 0, 0],
5009 skin_weights: vec![1.0, 0.0, 0.0, 0.0],
5010 edge_scale: vec![1.25],
5011 material_groups: vec![PmxParsedMaterialGroup {
5012 start: 0,
5013 count: 3,
5014 material_index: 0,
5015 }],
5016 sdef: PmxParsedSdef {
5017 skinning_modes: vec![PMX_SKINNING_MODE_BDEF4.to_owned()],
5018 enabled: vec![0.0],
5019 c: vec![0.0, 0.0, 0.0],
5020 r0: vec![0.0, 0.0, 0.0],
5021 r1: vec![0.0, 0.0, 0.0],
5022 rw0: vec![0.0, 0.0, 0.0],
5023 rw1: vec![0.0, 0.0, 0.0],
5024 },
5025 qdef: PmxParsedQdef { enabled: vec![0.0] },
5026 },
5027 materials: vec![PmxParsedMaterial {
5028 name: "mat".to_owned(),
5029 english_name: "mat-en".to_owned(),
5030 texture_path: "tex.png".to_owned(),
5031 sphere_texture_path: "sphere.spa".to_owned(),
5032 sphere_mode: "add".to_owned(),
5033 toon_texture_path: "toon.bmp".to_owned(),
5034 shared_toon_index: None,
5035 diffuse: [0.1, 0.2, 0.3, 0.4],
5036 specular: [0.5, 0.6, 0.7],
5037 specular_power: 8.0,
5038 ambient: [0.8, 0.9, 1.0],
5039 edge_color: [0.0, 0.1, 0.2, 0.3],
5040 edge_size: 1.5,
5041 flags: PmxParsedMaterialFlags {
5042 double_sided: true,
5043 edge: true,
5044 ..empty_pmx_flags()
5045 },
5046 face_count: 1,
5047 }],
5048 skeleton: PmxParsedSkeleton {
5049 bones: vec![PmxParsedBone {
5050 name: "Root".to_owned(),
5051 english_name: "RootEn".to_owned(),
5052 parent_index: -1,
5053 layer: 0,
5054 position: [0.0, 1.0, 2.0],
5055 tail_index: -1,
5056 tail_position: Some([0.0, 2.0, 0.0]),
5057 flags: basic_bone_flags(false),
5058 append_transform: None,
5059 fixed_axis: None,
5060 local_axis: None,
5061 external_parent_key: None,
5062 ik: None,
5063 }],
5064 },
5065 morphs: vec![PmxParsedMorph {
5066 name: "Smile".to_owned(),
5067 english_name: "SmileEn".to_owned(),
5068 panel: "eye".to_owned(),
5069 kind: "vertex".to_owned(),
5070 vertex_offsets: vec![PmxParsedVertexMorphOffset {
5071 vertex_index: 0,
5072 position: [0.1, 0.2, 0.3],
5073 }],
5074 group_offsets: Vec::new(),
5075 bone_offsets: Vec::new(),
5076 uv_offsets: Vec::new(),
5077 additional_uv_offsets: Vec::new(),
5078 material_offsets: Vec::new(),
5079 flip_offsets: Vec::new(),
5080 impulse_offsets: Vec::new(),
5081 }],
5082 display_frames: vec![PmxParsedDisplayFrame {
5083 name: "RootFrame".to_owned(),
5084 english_name: "RootFrameEn".to_owned(),
5085 special: true,
5086 frames: vec![
5087 PmxParsedDisplayFrameElement {
5088 kind: "bone".to_owned(),
5089 index: 0,
5090 },
5091 PmxParsedDisplayFrameElement {
5092 kind: "morph".to_owned(),
5093 index: 0,
5094 },
5095 ],
5096 }],
5097 rigid_bodies: vec![PmxParsedRigidBody {
5098 name: "body".to_owned(),
5099 english_name: "body-en".to_owned(),
5100 bone_index: 0,
5101 group: 1,
5102 mask: 2,
5103 shape: "box".to_owned(),
5104 size: [1.0, 2.0, 3.0],
5105 position: [4.0, 5.0, 6.0],
5106 rotation: [0.1, 0.2, 0.3],
5107 mass: 10.0,
5108 linear_damping: 0.4,
5109 angular_damping: 0.5,
5110 restitution: 0.6,
5111 friction: 0.7,
5112 mode: "dynamicBone".to_owned(),
5113 }],
5114 joints: vec![PmxParsedJoint {
5115 name: "joint".to_owned(),
5116 english_name: "joint-en".to_owned(),
5117 kind: "generic6dofSpring".to_owned(),
5118 rigid_body_index_a: 0,
5119 rigid_body_index_b: -1,
5120 position: [1.0, 1.1, 1.2],
5121 rotation: [2.0, 2.1, 2.2],
5122 translation_lower_limit: [-1.0, -1.1, -1.2],
5123 translation_upper_limit: [1.0, 1.1, 1.2],
5124 rotation_lower_limit: [-0.1, -0.2, -0.3],
5125 rotation_upper_limit: [0.1, 0.2, 0.3],
5126 spring_translation_factor: [3.0, 3.1, 3.2],
5127 spring_rotation_factor: [4.0, 4.1, 4.2],
5128 }],
5129 soft_bodies: Vec::new(),
5130 diagnostics: Vec::new(),
5131 }
5132 }
5133
5134 fn material_morph_offset(material_index: i32) -> PmxParsedMaterialMorphOffset {
5135 PmxParsedMaterialMorphOffset {
5136 material_index,
5137 operation: "add".to_owned(),
5138 diffuse: [0.1, 0.2, 0.3, 0.4],
5139 specular: [0.5, 0.6, 0.7],
5140 specular_power: 0.8,
5141 ambient: [0.9, 1.0, 1.1],
5142 edge_color: [0.0, 0.1, 0.2, 0.3],
5143 edge_size: 0.4,
5144 texture_factor: [1.0, 0.0, 0.0, 1.0],
5145 sphere_texture_factor: [0.0, 1.0, 0.0, 1.0],
5146 toon_texture_factor: [0.0, 0.0, 1.0, 1.0],
5147 }
5148 }
5149
5150 fn material_split_fixture() -> PmxParsedModel {
5151 let mut model = parsed_pmx_fixture();
5152 model.metadata.counts.vertices = 4;
5153 model.metadata.counts.faces = 2;
5154 model.metadata.counts.materials = 2;
5155 model.metadata.counts.morphs = 5;
5156 model.metadata.additional_uv_count = 1;
5157 model.geometry = PmxParsedGeometry {
5158 positions: vec![
5159 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 1.0, 1.0, 0.0,
5163 ],
5164 normals: vec![
5165 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0,
5169 ],
5170 uvs: vec![0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 1.0, 1.0],
5171 additional_uvs: vec![vec![
5172 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 1.0, 0.0, 0.0,
5176 ]],
5177 indices: vec![0, 1, 2, 2, 3, 0],
5178 skin_indices: vec![
5179 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
5183 ],
5184 skin_weights: vec![
5185 1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0,
5189 ],
5190 edge_scale: vec![1.0, 1.1, 1.2, 1.3],
5191 material_groups: vec![
5192 PmxParsedMaterialGroup {
5193 start: 0,
5194 count: 3,
5195 material_index: 0,
5196 },
5197 PmxParsedMaterialGroup {
5198 start: 3,
5199 count: 3,
5200 material_index: 1,
5201 },
5202 ],
5203 sdef: PmxParsedSdef {
5204 skinning_modes: vec![
5205 PMX_SKINNING_MODE_BDEF4.to_owned(),
5206 PMX_SKINNING_MODE_SDEF.to_owned(),
5207 PMX_SKINNING_MODE_BDEF4.to_owned(),
5208 PMX_SKINNING_MODE_QDEF.to_owned(),
5209 ],
5210 enabled: vec![0.0, 1.0, 0.0, 0.0],
5211 c: vec![0.0; 12],
5212 r0: vec![0.0; 12],
5213 r1: vec![0.0; 12],
5214 rw0: vec![0.0; 12],
5215 rw1: vec![0.0; 12],
5216 },
5217 qdef: PmxParsedQdef {
5218 enabled: vec![0.0, 0.0, 0.0, 1.0],
5219 },
5220 };
5221 let mut material_b = model.materials[0].clone();
5222 material_b.name = "mat-b".to_owned();
5223 material_b.english_name = "mat-b-en".to_owned();
5224 model.materials = vec![model.materials[0].clone(), material_b];
5225 model.morphs = vec![
5226 PmxParsedMorph {
5227 name: "left-raise".to_owned(),
5228 english_name: "left-raise-en".to_owned(),
5229 panel: "brow".to_owned(),
5230 kind: "vertex".to_owned(),
5231 vertex_offsets: vec![PmxParsedVertexMorphOffset {
5232 vertex_index: 1,
5233 position: [0.0, 0.5, 0.0],
5234 }],
5235 group_offsets: Vec::new(),
5236 bone_offsets: Vec::new(),
5237 uv_offsets: Vec::new(),
5238 additional_uv_offsets: Vec::new(),
5239 material_offsets: Vec::new(),
5240 flip_offsets: Vec::new(),
5241 impulse_offsets: Vec::new(),
5242 },
5243 PmxParsedMorph {
5244 name: "right-raise".to_owned(),
5245 english_name: "right-raise-en".to_owned(),
5246 panel: "eye".to_owned(),
5247 kind: "vertex".to_owned(),
5248 vertex_offsets: vec![PmxParsedVertexMorphOffset {
5249 vertex_index: 3,
5250 position: [0.0, 0.75, 0.0],
5251 }],
5252 group_offsets: Vec::new(),
5253 bone_offsets: Vec::new(),
5254 uv_offsets: Vec::new(),
5255 additional_uv_offsets: Vec::new(),
5256 material_offsets: Vec::new(),
5257 flip_offsets: Vec::new(),
5258 impulse_offsets: Vec::new(),
5259 },
5260 PmxParsedMorph {
5261 name: "right-material".to_owned(),
5262 english_name: "right-material-en".to_owned(),
5263 panel: "other".to_owned(),
5264 kind: "material".to_owned(),
5265 vertex_offsets: Vec::new(),
5266 group_offsets: Vec::new(),
5267 bone_offsets: Vec::new(),
5268 uv_offsets: Vec::new(),
5269 additional_uv_offsets: Vec::new(),
5270 material_offsets: vec![material_morph_offset(1)],
5271 flip_offsets: Vec::new(),
5272 impulse_offsets: Vec::new(),
5273 },
5274 PmxParsedMorph {
5275 name: "all-material".to_owned(),
5276 english_name: "all-material-en".to_owned(),
5277 panel: "other".to_owned(),
5278 kind: "material".to_owned(),
5279 vertex_offsets: Vec::new(),
5280 group_offsets: Vec::new(),
5281 bone_offsets: Vec::new(),
5282 uv_offsets: Vec::new(),
5283 additional_uv_offsets: Vec::new(),
5284 material_offsets: vec![material_morph_offset(-1)],
5285 flip_offsets: Vec::new(),
5286 impulse_offsets: Vec::new(),
5287 },
5288 PmxParsedMorph {
5289 name: "combo".to_owned(),
5290 english_name: "combo-en".to_owned(),
5291 panel: "other".to_owned(),
5292 kind: "group".to_owned(),
5293 vertex_offsets: Vec::new(),
5294 group_offsets: vec![
5295 PmxParsedGroupMorphOffset {
5296 morph_index: 0,
5297 weight: 0.25,
5298 },
5299 PmxParsedGroupMorphOffset {
5300 morph_index: 1,
5301 weight: 0.5,
5302 },
5303 PmxParsedGroupMorphOffset {
5304 morph_index: 2,
5305 weight: 0.75,
5306 },
5307 PmxParsedGroupMorphOffset {
5308 morph_index: 3,
5309 weight: 1.0,
5310 },
5311 ],
5312 bone_offsets: Vec::new(),
5313 uv_offsets: Vec::new(),
5314 additional_uv_offsets: Vec::new(),
5315 material_offsets: Vec::new(),
5316 flip_offsets: Vec::new(),
5317 impulse_offsets: Vec::new(),
5318 },
5319 ];
5320 model
5321 }
5322
5323 fn ik_multi_axis_limit_pmx_fixture() -> &'static [u8] {
5324 include_bytes!("../../fixtures/pmx/ik_multi_axis_limit.pmx")
5325 }
5326
5327 fn model_descriptor_parity_pmx_fixture() -> &'static [u8] {
5328 include_bytes!("../../fixtures/pmx/model_descriptor_parity.pmx")
5329 }
5330
5331 fn assert_pmx_roundtrip_eq(left: &PmxParsedModel, right: &PmxParsedModel) {
5332 assert_eq!(
5333 serde_json::to_value(left).unwrap(),
5334 serde_json::to_value(right).unwrap()
5335 );
5336 }
5337
5338 fn json_keys(value: &serde_json::Value) -> Vec<String> {
5339 let mut keys = value
5340 .as_object()
5341 .unwrap()
5342 .keys()
5343 .map(ToOwned::to_owned)
5344 .collect::<Vec<_>>();
5345 keys.sort();
5346 keys
5347 }
5348
5349 #[test]
5350 fn pmx_material_split_remaps_geometry_and_prunes_morphs() {
5351 let model = material_split_fixture();
5352 let split = split_pmx_model_by_material(&model);
5353
5354 assert_eq!(split.meshes.len(), 2);
5355 assert_eq!(split.manifest.mesh_count, 2);
5356 assert_eq!(split.manifest.meshes[0].original_material_index, 0);
5357 assert_eq!(
5358 split.manifest.meshes[0].original_vertex_indices,
5359 vec![0, 1, 2]
5360 );
5361 assert_eq!(split.manifest.meshes[1].original_material_index, 1);
5362 assert_eq!(
5363 split.manifest.meshes[1].original_vertex_indices,
5364 vec![2, 3, 0]
5365 );
5366
5367 let left = &split.meshes[0];
5368 assert_eq!(left.geometry.indices, vec![0, 1, 2]);
5369 assert_eq!(
5370 left.geometry.positions,
5371 vec![0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0, 0.0]
5372 );
5373 assert_eq!(left.geometry.uvs, vec![0.0, 0.0, 1.0, 0.0, 0.0, 1.0]);
5374 assert_eq!(left.geometry.edge_scale, vec![1.0, 1.1, 1.2]);
5375 assert_eq!(left.geometry.sdef.enabled, vec![0.0, 1.0, 0.0]);
5376 assert_eq!(left.geometry.qdef.enabled, vec![0.0, 0.0, 0.0]);
5377 assert_eq!(
5378 left.morph_index_map,
5379 vec![
5380 PmxMaterialSplitMorphIndexMap {
5381 original_index: 0,
5382 local_index: 0,
5383 },
5384 PmxMaterialSplitMorphIndexMap {
5385 original_index: 3,
5386 local_index: 1,
5387 },
5388 PmxMaterialSplitMorphIndexMap {
5389 original_index: 4,
5390 local_index: 2,
5391 },
5392 ]
5393 );
5394 assert_eq!(left.morphs.len(), 3);
5395 assert_eq!(left.morphs[0].name, "left-raise");
5396 assert_eq!(left.morphs[0].vertex_offsets[0].vertex_index, 1);
5397 assert_eq!(left.morphs[1].name, "all-material");
5398 assert_eq!(left.morphs[1].material_offsets[0].material_index, -1);
5399 assert_eq!(left.morphs[2].name, "combo");
5400 assert_eq!(
5401 left.morphs[2]
5402 .group_offsets
5403 .iter()
5404 .map(|offset| (offset.morph_index, offset.weight))
5405 .collect::<Vec<_>>(),
5406 vec![(0, 0.25), (1, 1.0)]
5407 );
5408
5409 let right = &split.meshes[1];
5410 assert_eq!(right.geometry.indices, vec![0, 1, 2]);
5411 assert_eq!(
5412 right.geometry.positions,
5413 vec![0.0, 1.0, 0.0, 1.0, 1.0, 0.0, 0.0, 0.0, 0.0]
5414 );
5415 assert_eq!(right.geometry.uvs, vec![0.0, 1.0, 1.0, 1.0, 0.0, 0.0]);
5416 assert_eq!(right.geometry.edge_scale, vec![1.2, 1.3, 1.0]);
5417 assert_eq!(right.geometry.sdef.enabled, vec![0.0, 0.0, 0.0]);
5418 assert_eq!(right.geometry.qdef.enabled, vec![0.0, 1.0, 0.0]);
5419 assert_eq!(
5420 right.morph_index_map,
5421 vec![
5422 PmxMaterialSplitMorphIndexMap {
5423 original_index: 1,
5424 local_index: 0,
5425 },
5426 PmxMaterialSplitMorphIndexMap {
5427 original_index: 2,
5428 local_index: 1,
5429 },
5430 PmxMaterialSplitMorphIndexMap {
5431 original_index: 3,
5432 local_index: 2,
5433 },
5434 PmxMaterialSplitMorphIndexMap {
5435 original_index: 4,
5436 local_index: 3,
5437 },
5438 ]
5439 );
5440 assert_eq!(right.morphs.len(), 4);
5441 assert_eq!(right.morphs[0].name, "right-raise");
5442 assert_eq!(right.morphs[0].vertex_offsets[0].vertex_index, 1);
5443 assert_eq!(right.morphs[1].name, "right-material");
5444 assert_eq!(right.morphs[1].material_offsets[0].material_index, 0);
5445 assert_eq!(right.morphs[2].name, "all-material");
5446 assert_eq!(right.morphs[2].material_offsets[0].material_index, -1);
5447 assert_eq!(
5448 right.morphs[3]
5449 .group_offsets
5450 .iter()
5451 .map(|offset| (offset.morph_index, offset.weight))
5452 .collect::<Vec<_>>(),
5453 vec![(0, 0.5), (1, 0.75), (2, 1.0)]
5454 );
5455 }
5456
5457 #[test]
5458 fn pmx_material_split_preserves_forward_group_morph_references() {
5459 let mut model = material_split_fixture();
5460 model.morphs.push(PmxParsedMorph {
5461 name: "forward-combo".to_owned(),
5462 english_name: "forward-combo-en".to_owned(),
5463 panel: "other".to_owned(),
5464 kind: "group".to_owned(),
5465 vertex_offsets: Vec::new(),
5466 group_offsets: vec![PmxParsedGroupMorphOffset {
5467 morph_index: 6,
5468 weight: 0.8,
5469 }],
5470 bone_offsets: Vec::new(),
5471 uv_offsets: Vec::new(),
5472 additional_uv_offsets: Vec::new(),
5473 material_offsets: Vec::new(),
5474 flip_offsets: Vec::new(),
5475 impulse_offsets: Vec::new(),
5476 });
5477 model.morphs.push(PmxParsedMorph {
5478 name: "later-combo".to_owned(),
5479 english_name: "later-combo-en".to_owned(),
5480 panel: "other".to_owned(),
5481 kind: "group".to_owned(),
5482 vertex_offsets: Vec::new(),
5483 group_offsets: vec![PmxParsedGroupMorphOffset {
5484 morph_index: 0,
5485 weight: 0.4,
5486 }],
5487 bone_offsets: Vec::new(),
5488 uv_offsets: Vec::new(),
5489 additional_uv_offsets: Vec::new(),
5490 material_offsets: Vec::new(),
5491 flip_offsets: Vec::new(),
5492 impulse_offsets: Vec::new(),
5493 });
5494 model.metadata.counts.morphs = model.morphs.len();
5495
5496 let split = split_pmx_model_by_material(&model);
5497 let left = &split.meshes[0];
5498
5499 assert_eq!(
5500 left.morph_index_map,
5501 vec![
5502 PmxMaterialSplitMorphIndexMap {
5503 original_index: 0,
5504 local_index: 0,
5505 },
5506 PmxMaterialSplitMorphIndexMap {
5507 original_index: 3,
5508 local_index: 1,
5509 },
5510 PmxMaterialSplitMorphIndexMap {
5511 original_index: 4,
5512 local_index: 2,
5513 },
5514 PmxMaterialSplitMorphIndexMap {
5515 original_index: 5,
5516 local_index: 3,
5517 },
5518 PmxMaterialSplitMorphIndexMap {
5519 original_index: 6,
5520 local_index: 4,
5521 },
5522 ]
5523 );
5524 assert_eq!(left.morphs[3].name, "forward-combo");
5525 assert_eq!(
5526 left.morphs[3]
5527 .group_offsets
5528 .iter()
5529 .map(|offset| (offset.morph_index, offset.weight))
5530 .collect::<Vec<_>>(),
5531 vec![(4, 0.8)]
5532 );
5533 assert_eq!(left.morphs[4].name, "later-combo");
5534 assert_eq!(
5535 left.morphs[4]
5536 .group_offsets
5537 .iter()
5538 .map(|offset| (offset.morph_index, offset.weight))
5539 .collect::<Vec<_>>(),
5540 vec![(0, 0.4)]
5541 );
5542 }
5543
5544 #[test]
5545 fn pmx_model_json_top_level_schema_is_stable() {
5546 let parsed = parsed_pmx_fixture();
5547 let keys = json_keys(&serde_json::to_value(&parsed).unwrap());
5548
5549 assert_eq!(
5550 keys,
5551 vec![
5552 "diagnostics",
5553 "displayFrames",
5554 "geometry",
5555 "joints",
5556 "materials",
5557 "metadata",
5558 "morphs",
5559 "rigidBodies",
5560 "skeleton",
5561 "softBodies",
5562 ]
5563 );
5564 }
5565
5566 #[test]
5567 fn parses_pmx_header() {
5568 let header_bytes = build_small_pmx_header_bytes(4, TextEncoding::Utf8);
5569 let (header, pos) = read_header(&header_bytes).unwrap();
5570
5571 assert_eq!(header.version, 2.0);
5572 assert_eq!(header.encoding, TextEncoding::Utf8);
5573 assert_eq!(header.bone_index_size, 4);
5574 assert_eq!(header.vertex_index_size, 4);
5575 assert!(pos > 0);
5576 }
5577
5578 #[test]
5579 fn pmx_bone_flag_values_match_reference_layout() {
5580 assert_eq!(BONE_FLAG_TAIL_INDEX, 0x0001);
5581 assert_eq!(BONE_FLAG_IK, 0x0020);
5582 assert_eq!(BONE_FLAG_LOCAL_APPEND, 0x0080);
5583 assert_eq!(BONE_FLAG_APPEND_ROTATE, 0x0100);
5584 assert_eq!(BONE_FLAG_APPEND_TRANSLATE, 0x0200);
5585 assert_eq!(BONE_FLAG_FIXED_AXIS, 0x0400);
5586 assert_eq!(BONE_FLAG_LOCAL_AXIS, 0x0800);
5587 assert_eq!(BONE_FLAG_TRANSFORM_AFTER_PHYSICS, 0x1000);
5588 assert_eq!(BONE_FLAG_EXTERNAL_PARENT, 0x2000);
5589 }
5590
5591 #[test]
5592 fn parses_ik_multi_axis_limit_pmx_fixture() {
5593 let parsed = parse_pmx_model(ik_multi_axis_limit_pmx_fixture()).unwrap();
5594 assert_eq!(parsed.metadata.name, "ik_multi_axis_limit_fixture");
5595 assert_eq!(parsed.metadata.counts.bones, 3);
5596 assert_eq!(parsed.skeleton.bones[2].name, "ik_controller");
5597 let ik = parsed.skeleton.bones[2].ik.as_ref().unwrap();
5598 assert_eq!(ik.target_index, 1);
5599 assert_eq!(ik.loop_count, 1);
5600 assert_eq!(ik.links.len(), 1);
5601 assert_eq!(ik.links[0].bone_index, 0);
5602 let limits = ik.links[0].limits.as_ref().unwrap();
5603 assert_eq!(limits.lower, [0.0, -1.0, -1.0]);
5604 assert_eq!(limits.upper, [0.0, 1.0, 1.0]);
5605
5606 let runtime = import_pmx_runtime(ik_multi_axis_limit_pmx_fixture()).unwrap();
5607 assert_eq!(runtime.model.bone_count(), 3);
5608 assert_eq!(runtime.model.ik_count(), 1);
5609 let solver = &runtime.model.ik_solvers()[0];
5610 assert_eq!(solver.ik_bone.as_usize(), 2);
5611 assert_eq!(solver.target_bone.as_usize(), 1);
5612 let limit = solver.links[0].angle_limit.unwrap();
5613 assert_eq!(limit.min.to_array(), [0.0, -1.0, -1.0]);
5614 assert_eq!(limit.max.to_array(), [0.0, 1.0, 1.0]);
5615 }
5616
5617 #[test]
5618 fn runtime_descriptor_compiler_matches_legacy_pmx_payload() {
5619 let data = model_descriptor_parity_pmx_fixture();
5620 let parsed = parse_pmx_model(data).unwrap();
5621 assert_eq!(parsed.metadata.counts.bones, 3);
5622 assert_eq!(parsed.metadata.counts.morphs, 2);
5623 let imported = import_pmx_runtime(data).unwrap();
5624 assert_eq!(imported.bone_names.len(), 3);
5625 assert_eq!(
5626 imported.bone_name_to_index.get(b"root".as_slice()),
5627 Some(&BoneIndex(0))
5628 );
5629 assert_eq!(
5630 imported.bone_name_to_index.get(b"append_after".as_slice()),
5631 Some(&BoneIndex(1))
5632 );
5633 assert_eq!(
5634 imported.bone_name_to_index.get(b"ik_controller".as_slice()),
5635 Some(&BoneIndex(2))
5636 );
5637 assert_eq!(
5638 imported.morph_name_to_index.get(b"bone_morph".as_slice()),
5639 Some(&MorphIndex(0))
5640 );
5641 assert_eq!(
5642 imported.morph_name_to_index.get(b"group_morph".as_slice()),
5643 Some(&MorphIndex(1))
5644 );
5645 assert_eq!(
5646 imported
5647 .ik_solver_bone_name_to_index
5648 .get(b"ik_controller".as_slice()),
5649 Some(&0)
5650 );
5651 assert_eq!(imported.model.ik_count(), 1);
5652 assert_eq!(imported.model.append_transforms().len(), 1);
5653 assert_eq!(imported.model.morph_count(), 2);
5654 assert_eq!(imported.model.bone_morph_offsets().len(), 1);
5655 assert_eq!(imported.model.group_morph_offsets().len(), 1);
5656 assert!(imported.model.transform_after_physics(BoneIndex(1)));
5657 assert!(imported.model.fixed_axis(BoneIndex(0)).is_some());
5658 assert_eq!(imported.model.local_axis_count(), 2);
5659 let append = imported.model.append_transforms()[0];
5660 assert!(append.affect_rotation);
5661 assert!(append.affect_translation);
5662 assert!(append.local);
5663 let solver = &imported.model.ik_solvers()[0];
5664 let limit = solver.links[0].angle_limit.unwrap();
5665 assert_eq!(limit.min.to_array(), [0.0, -1.0, -1.0]);
5666 assert_eq!(limit.max.to_array(), [0.0, 1.0, 1.0]);
5667
5668 let (header, pos) = read_header(data).unwrap();
5671 let pos = skip_model_info(data, &header, pos).unwrap();
5672 let pos = skip_vertices(data, &header, pos).unwrap();
5673 let pos = skip_faces(data, header.vertex_index_size, pos).unwrap();
5674 let pos = skip_textures(data, header.encoding, pos).unwrap();
5675 let pos = skip_materials(data, &header, pos).unwrap();
5676 let (bone_read, pos) = read_bones_with_local_axes(data, &header, pos).unwrap();
5677 let local_axes = bone_read
5678 .descriptor
5679 .bones
5680 .iter()
5681 .map(|bone| bone.local_axis)
5682 .collect::<Vec<_>>();
5683 let legacy_import = legacy_bone_import(&bone_read);
5684 let (_, morph, _) = read_morph_offsets(data, &header, pos).unwrap();
5685 let legacy_model = ModelArena::new_with_morphs(
5686 legacy_import.bones,
5687 legacy_import.ik_solvers,
5688 legacy_import.append_transforms,
5689 morph,
5690 )
5691 .unwrap()
5692 .with_local_axes(local_axes);
5693
5694 assert_eq!(imported.model.bone_count(), legacy_model.bone_count());
5695 assert_eq!(imported.model.ik_count(), legacy_model.ik_count());
5696 assert_eq!(
5697 imported.model.append_transforms(),
5698 legacy_model.append_transforms()
5699 );
5700 assert_eq!(imported.model.morph_count(), legacy_model.morph_count());
5701 assert_eq!(
5702 imported.model.bone_morph_spans(),
5703 legacy_model.bone_morph_spans()
5704 );
5705 assert_eq!(
5706 imported.model.bone_morph_offsets(),
5707 legacy_model.bone_morph_offsets()
5708 );
5709 assert_eq!(
5710 imported.model.group_morph_spans(),
5711 legacy_model.group_morph_spans()
5712 );
5713 assert_eq!(
5714 imported.model.group_morph_offsets(),
5715 legacy_model.group_morph_offsets()
5716 );
5717 assert_eq!(imported.model.ik_solvers(), legacy_model.ik_solvers());
5718 for bone_index in 0..imported.model.bone_count() {
5719 let bone = BoneIndex(bone_index as u32);
5720 assert_eq!(
5721 imported.model.rest_position(bone),
5722 legacy_model.rest_position(bone)
5723 );
5724 assert_eq!(
5725 imported.model.inverse_bind_matrix(bone),
5726 legacy_model.inverse_bind_matrix(bone)
5727 );
5728 assert_eq!(
5729 imported.model.transform_order(bone),
5730 legacy_model.transform_order(bone)
5731 );
5732 assert_eq!(
5733 imported.model.transform_after_physics(bone),
5734 legacy_model.transform_after_physics(bone)
5735 );
5736 assert_eq!(
5737 imported.model.fixed_axis(bone),
5738 legacy_model.fixed_axis(bone)
5739 );
5740 assert_eq!(
5741 imported.model.local_axis(bone),
5742 legacy_model.local_axis(bone)
5743 );
5744 assert_eq!(
5745 imported.model.local_axis_basis(bone),
5746 legacy_model.local_axis_basis(bone)
5747 );
5748 }
5749
5750 let mut shared_runtime = RuntimeInstance::new(Arc::new(imported.model));
5751 let mut legacy_runtime = RuntimeInstance::new(Arc::new(legacy_model));
5752 shared_runtime.evaluate_current_pose();
5753 legacy_runtime.evaluate_current_pose();
5754 assert_eq!(
5755 shared_runtime.world_matrices(),
5756 legacy_runtime.world_matrices()
5757 );
5758 assert_eq!(
5759 shared_runtime.skinning_matrices(),
5760 legacy_runtime.skinning_matrices()
5761 );
5762 }
5763
5764 #[test]
5765 fn rejects_bad_magic() {
5766 let mut header_bytes = build_small_pmx_header_bytes(4, TextEncoding::Utf8);
5767 header_bytes[0] = 0xFF;
5768 assert_eq!(
5769 read_header(&header_bytes).unwrap_err(),
5770 ImportError::InvalidPmxMagic
5771 );
5772 }
5773
5774 #[test]
5775 fn reads_single_bone_no_parent() {
5776 let mut buf = Vec::new();
5777 buf.extend_from_slice(&build_small_pmx_header_bytes(4, TextEncoding::Utf8));
5778 buf.extend_from_slice(&build_empty_model_info(TextEncoding::Utf8));
5779 buf.extend_from_slice(&build_empty_vertex_section());
5780 buf.extend_from_slice(&build_empty_face_section());
5781 buf.extend_from_slice(&build_empty_texture_section());
5782 buf.extend_from_slice(&build_empty_material_section());
5783 buf.extend_from_slice(&build_bone_section_header(1));
5784
5785 buf.extend_from_slice(&build_bone_name_bytes("Root"));
5786 buf.extend_from_slice(&build_bone_name_bytes(""));
5787 buf.extend_from_slice(&0.0f32.to_le_bytes());
5788 buf.extend_from_slice(&1.0f32.to_le_bytes());
5789 buf.extend_from_slice(&2.0f32.to_le_bytes());
5790 buf.extend_from_slice(&(-1i32).to_le_bytes());
5791 buf.extend_from_slice(&0i32.to_le_bytes());
5792 buf.extend_from_slice(&0u16.to_le_bytes());
5793 buf.extend_from_slice(&0.0f32.to_le_bytes());
5794 buf.extend_from_slice(&0.0f32.to_le_bytes());
5795 buf.extend_from_slice(&0.0f32.to_le_bytes());
5796
5797 let result = import_pmx_model(&buf).unwrap();
5798 assert_eq!(result.bones.len(), 1);
5799 assert_eq!(result.bone_names.len(), 1);
5800 assert_eq!(result.bone_names[0], "Root");
5801 assert_eq!(&result.bone_name_bytes[0][..], b"Root");
5802 assert_eq!(result.bones[0].parent, None);
5803 assert!((result.bones[0].rest_position.x - 0.0).abs() < 0.001);
5804 assert!((result.bones[0].rest_position.y - 1.0).abs() < 0.001);
5805 assert!((result.bones[0].rest_position.z - 2.0).abs() < 0.001);
5806 assert!(result.ik_solvers.is_empty());
5807 assert!(result.append_transforms.is_empty());
5808 }
5809
5810 #[test]
5811 fn reads_bone_with_parent() {
5812 let mut buf = Vec::new();
5813 buf.extend_from_slice(&build_small_pmx_header_bytes(2, TextEncoding::Utf8));
5814 buf.extend_from_slice(&build_empty_model_info(TextEncoding::Utf8));
5815 buf.extend_from_slice(&build_empty_vertex_section());
5816 buf.extend_from_slice(&build_empty_face_section());
5817 buf.extend_from_slice(&build_empty_texture_section());
5818 buf.extend_from_slice(&build_empty_material_section());
5819 buf.extend_from_slice(&build_bone_section_header(2));
5820
5821 buf.extend_from_slice(&build_bone_name_bytes("Root"));
5822 buf.extend_from_slice(&build_bone_name_bytes(""));
5823 buf.extend_from_slice(&0.0f32.to_le_bytes());
5824 buf.extend_from_slice(&0.0f32.to_le_bytes());
5825 buf.extend_from_slice(&0.0f32.to_le_bytes());
5826 buf.extend_from_slice(&(-1i16).to_le_bytes());
5827 buf.extend_from_slice(&0i32.to_le_bytes());
5828 buf.extend_from_slice(&0u16.to_le_bytes());
5829 buf.extend_from_slice(&0.0f32.to_le_bytes());
5830 buf.extend_from_slice(&0.0f32.to_le_bytes());
5831 buf.extend_from_slice(&0.0f32.to_le_bytes());
5832
5833 buf.extend_from_slice(&build_bone_name_bytes("Child"));
5834 buf.extend_from_slice(&build_bone_name_bytes(""));
5835 buf.extend_from_slice(&1.0f32.to_le_bytes());
5836 buf.extend_from_slice(&0.0f32.to_le_bytes());
5837 buf.extend_from_slice(&0.0f32.to_le_bytes());
5838 buf.extend_from_slice(&0i16.to_le_bytes());
5839 buf.extend_from_slice(&0i32.to_le_bytes());
5840 buf.extend_from_slice(&0u16.to_le_bytes());
5841 buf.extend_from_slice(&0.0f32.to_le_bytes());
5842 buf.extend_from_slice(&0.0f32.to_le_bytes());
5843 buf.extend_from_slice(&0.0f32.to_le_bytes());
5844
5845 let result = import_pmx_model(&buf).unwrap();
5846 assert_eq!(result.bones.len(), 2);
5847 assert_eq!(result.bone_names[0], "Root");
5848 assert_eq!(result.bone_names[1], "Child");
5849 assert_eq!(result.bones[0].parent, None);
5850 assert_eq!(result.bones[1].parent, Some(BoneIndex(0)));
5851 }
5852
5853 #[test]
5854 fn reads_bone_with_append_transform() {
5855 let mut buf = Vec::new();
5856 buf.extend_from_slice(&build_small_pmx_header_bytes(2, TextEncoding::Utf8));
5857 buf.extend_from_slice(&build_empty_model_info(TextEncoding::Utf8));
5858 buf.extend_from_slice(&build_empty_vertex_section());
5859 buf.extend_from_slice(&build_empty_face_section());
5860 buf.extend_from_slice(&build_empty_texture_section());
5861 buf.extend_from_slice(&build_empty_material_section());
5862 buf.extend_from_slice(&build_bone_section_header(2));
5863
5864 buf.extend_from_slice(&build_bone_name_bytes("Root"));
5865 buf.extend_from_slice(&build_bone_name_bytes(""));
5866 buf.extend_from_slice(&0.0f32.to_le_bytes());
5867 buf.extend_from_slice(&0.0f32.to_le_bytes());
5868 buf.extend_from_slice(&0.0f32.to_le_bytes());
5869 buf.extend_from_slice(&(-1i16).to_le_bytes());
5870 buf.extend_from_slice(&0i32.to_le_bytes());
5871 buf.extend_from_slice(&0u16.to_le_bytes());
5872 buf.extend_from_slice(&0.0f32.to_le_bytes());
5873 buf.extend_from_slice(&0.0f32.to_le_bytes());
5874 buf.extend_from_slice(&0.0f32.to_le_bytes());
5875
5876 buf.extend_from_slice(&build_bone_name_bytes("AppendBone"));
5877 buf.extend_from_slice(&build_bone_name_bytes(""));
5878 buf.extend_from_slice(&1.0f32.to_le_bytes());
5879 buf.extend_from_slice(&0.0f32.to_le_bytes());
5880 buf.extend_from_slice(&0.0f32.to_le_bytes());
5881 buf.extend_from_slice(&0i16.to_le_bytes());
5882 buf.extend_from_slice(&0i32.to_le_bytes());
5883 buf.extend_from_slice(
5884 &(BONE_FLAG_LOCAL_APPEND | BONE_FLAG_APPEND_ROTATE | BONE_FLAG_APPEND_TRANSLATE)
5885 .to_le_bytes(),
5886 );
5887 buf.extend_from_slice(&0.0f32.to_le_bytes());
5888 buf.extend_from_slice(&0.0f32.to_le_bytes());
5889 buf.extend_from_slice(&0.0f32.to_le_bytes());
5890 buf.extend_from_slice(&0i16.to_le_bytes());
5891 buf.extend_from_slice(&0.5f32.to_le_bytes());
5892
5893 let result = import_pmx_model(&buf).unwrap();
5894 assert_eq!(result.bones.len(), 2);
5895 assert_eq!(result.append_transforms.len(), 1);
5896 assert_eq!(result.append_transforms[0].target_bone, BoneIndex(1));
5897 assert_eq!(result.append_transforms[0].source_bone, BoneIndex(0));
5898 assert!((result.append_transforms[0].ratio - 0.5).abs() < 0.001);
5899 assert!(result.append_transforms[0].affect_rotation);
5900 assert!(result.append_transforms[0].affect_translation);
5901 assert!(result.append_transforms[0].local);
5902 }
5903
5904 #[test]
5905 fn reads_ik_bone_with_links_and_angle_limits() {
5906 let mut buf = Vec::new();
5907 buf.extend_from_slice(&build_small_pmx_header_bytes(2, TextEncoding::Utf8));
5908 buf.extend_from_slice(&build_empty_model_info(TextEncoding::Utf8));
5909 buf.extend_from_slice(&build_empty_vertex_section());
5910 buf.extend_from_slice(&build_empty_face_section());
5911 buf.extend_from_slice(&build_empty_texture_section());
5912 buf.extend_from_slice(&build_empty_material_section());
5913 buf.extend_from_slice(&build_bone_section_header(3));
5914
5915 buf.extend_from_slice(&build_bone_name_bytes("Root"));
5916 buf.extend_from_slice(&build_bone_name_bytes(""));
5917 buf.extend_from_slice(&0.0f32.to_le_bytes());
5918 buf.extend_from_slice(&0.0f32.to_le_bytes());
5919 buf.extend_from_slice(&0.0f32.to_le_bytes());
5920 buf.extend_from_slice(&(-1i16).to_le_bytes());
5921 buf.extend_from_slice(&0i32.to_le_bytes());
5922 buf.extend_from_slice(&0u16.to_le_bytes());
5923 buf.extend_from_slice(&0.0f32.to_le_bytes());
5924 buf.extend_from_slice(&0.0f32.to_le_bytes());
5925 buf.extend_from_slice(&0.0f32.to_le_bytes());
5926
5927 buf.extend_from_slice(&build_bone_name_bytes("IKLink"));
5928 buf.extend_from_slice(&build_bone_name_bytes(""));
5929 buf.extend_from_slice(&2.0f32.to_le_bytes());
5930 buf.extend_from_slice(&0.0f32.to_le_bytes());
5931 buf.extend_from_slice(&0.0f32.to_le_bytes());
5932 buf.extend_from_slice(&0i16.to_le_bytes());
5933 buf.extend_from_slice(&0i32.to_le_bytes());
5934 buf.extend_from_slice(&0u16.to_le_bytes());
5935 buf.extend_from_slice(&0.0f32.to_le_bytes());
5936 buf.extend_from_slice(&0.0f32.to_le_bytes());
5937 buf.extend_from_slice(&0.0f32.to_le_bytes());
5938
5939 buf.extend_from_slice(&build_bone_name_bytes("IKBone"));
5940 buf.extend_from_slice(&build_bone_name_bytes(""));
5941 buf.extend_from_slice(&4.0f32.to_le_bytes());
5942 buf.extend_from_slice(&0.0f32.to_le_bytes());
5943 buf.extend_from_slice(&0.0f32.to_le_bytes());
5944 buf.extend_from_slice(&0i16.to_le_bytes());
5945 buf.extend_from_slice(&0i32.to_le_bytes());
5946 buf.extend_from_slice(&(BONE_FLAG_TAIL_INDEX | BONE_FLAG_IK).to_le_bytes());
5947 buf.extend_from_slice(&1i16.to_le_bytes());
5948 buf.extend_from_slice(&1i16.to_le_bytes());
5949 buf.extend_from_slice(&10i32.to_le_bytes());
5950 buf.extend_from_slice(&0.5f32.to_le_bytes());
5951 buf.extend_from_slice(&1i32.to_le_bytes());
5952 buf.extend_from_slice(&1i16.to_le_bytes());
5953 buf.push(1u8);
5954 buf.extend_from_slice(&(-1.0f32).to_le_bytes());
5955 buf.extend_from_slice(&(-1.0f32).to_le_bytes());
5956 buf.extend_from_slice(&(-1.0f32).to_le_bytes());
5957 buf.extend_from_slice(&1.0f32.to_le_bytes());
5958 buf.extend_from_slice(&1.0f32.to_le_bytes());
5959 buf.extend_from_slice(&1.0f32.to_le_bytes());
5960
5961 let result = import_pmx_model(&buf).unwrap();
5962 assert_eq!(result.bones.len(), 3);
5963 assert_eq!(result.ik_solvers.len(), 1);
5964 assert_eq!(result.ik_solvers[0].ik_bone, BoneIndex(2));
5965 assert_eq!(result.ik_solvers[0].target_bone, BoneIndex(1));
5966 assert_eq!(result.ik_solvers[0].links.len(), 1);
5967 assert_eq!(result.ik_solvers[0].links[0].bone, BoneIndex(1));
5968 assert!(result.ik_solvers[0].links[0].angle_limit.is_some());
5969 assert_eq!(result.ik_solvers[0].iteration_count, 10);
5970 assert!((result.ik_solvers[0].limit_angle - 0.5).abs() < 0.001);
5971 }
5972
5973 #[test]
5974 fn handles_utf16le_encoding() {
5975 let header_bytes = build_small_pmx_header_bytes(4, TextEncoding::Utf16Le);
5976 let (header, _) = read_header(&header_bytes).unwrap();
5977 assert_eq!(header.encoding, TextEncoding::Utf16Le);
5978 }
5979
5980 #[test]
5981 fn decodes_utf16le_surrogate_pairs() {
5982 let bytes = [0x3D, 0xD8, 0x00, 0xDE, 0x00, 0x00];
5983 assert_eq!(decode_utf16le_lossy(&bytes), "\u{1F600}");
5984 }
5985
5986 #[test]
5987 fn reports_eof_on_truncated_data() {
5988 let buf = vec![0x50, 0x4D, 0x58]; assert!(matches!(
5990 read_header(&buf).unwrap_err(),
5991 ImportError::UnexpectedEof(_)
5992 ));
5993 }
5994
5995 #[test]
5996 fn bone_names_retained_and_mapped_to_indices() {
5997 let mut buf = Vec::new();
5998 buf.extend_from_slice(&build_small_pmx_header_bytes(2, TextEncoding::Utf8));
5999 buf.extend_from_slice(&build_empty_model_info(TextEncoding::Utf8));
6000 buf.extend_from_slice(&build_empty_vertex_section());
6001 buf.extend_from_slice(&build_empty_face_section());
6002 buf.extend_from_slice(&build_empty_texture_section());
6003 buf.extend_from_slice(&build_empty_material_section());
6004 buf.extend_from_slice(&build_bone_section_header(2));
6005
6006 buf.extend_from_slice(&build_bone_name_bytes("Hips"));
6007 buf.extend_from_slice(&build_bone_name_bytes(""));
6008 buf.extend_from_slice(&0.0f32.to_le_bytes());
6009 buf.extend_from_slice(&0.0f32.to_le_bytes());
6010 buf.extend_from_slice(&0.0f32.to_le_bytes());
6011 buf.extend_from_slice(&(-1i16).to_le_bytes());
6012 buf.extend_from_slice(&0i32.to_le_bytes());
6013 buf.extend_from_slice(&0u16.to_le_bytes());
6014 buf.extend_from_slice(&0.0f32.to_le_bytes());
6015 buf.extend_from_slice(&0.0f32.to_le_bytes());
6016 buf.extend_from_slice(&0.0f32.to_le_bytes());
6017
6018 buf.extend_from_slice(&build_bone_name_bytes("Spine"));
6019 buf.extend_from_slice(&build_bone_name_bytes(""));
6020 buf.extend_from_slice(&1.0f32.to_le_bytes());
6021 buf.extend_from_slice(&0.0f32.to_le_bytes());
6022 buf.extend_from_slice(&0.0f32.to_le_bytes());
6023 buf.extend_from_slice(&0i16.to_le_bytes());
6024 buf.extend_from_slice(&0i32.to_le_bytes());
6025 buf.extend_from_slice(&0u16.to_le_bytes());
6026 buf.extend_from_slice(&0.0f32.to_le_bytes());
6027 buf.extend_from_slice(&0.0f32.to_le_bytes());
6028 buf.extend_from_slice(&0.0f32.to_le_bytes());
6029
6030 let result = import_pmx_model(&buf).unwrap();
6031 assert_eq!(result.bone_name_bytes.len(), 2);
6032 assert_eq!(&result.bone_name_bytes[0][..], b"Hips");
6033 assert_eq!(&result.bone_name_bytes[1][..], b"Spine");
6034 assert_eq!(result.bone_names[0], "Hips");
6035 assert_eq!(result.bone_names[1], "Spine");
6036
6037 let bone_map: HashMap<Vec<u8>, BoneIndex> = result
6038 .bone_name_bytes
6039 .iter()
6040 .enumerate()
6041 .map(|(i, b)| (b.clone(), BoneIndex(i as u32)))
6042 .collect();
6043
6044 assert_eq!(bone_map.get(b"Hips".as_slice()), Some(&BoneIndex(0)));
6045 assert_eq!(bone_map.get(b"Spine".as_slice()), Some(&BoneIndex(1)));
6046 assert_eq!(bone_map.get(b"Unknown".as_slice()), None);
6047 }
6048
6049 #[test]
6050 fn ik_solver_name_mapped_to_solver_index() {
6051 let mut buf = Vec::new();
6052 buf.extend_from_slice(&build_small_pmx_header_bytes(2, TextEncoding::Utf8));
6053 buf.extend_from_slice(&build_empty_model_info(TextEncoding::Utf8));
6054 buf.extend_from_slice(&build_empty_vertex_section());
6055 buf.extend_from_slice(&build_empty_face_section());
6056 buf.extend_from_slice(&build_empty_texture_section());
6057 buf.extend_from_slice(&build_empty_material_section());
6058 buf.extend_from_slice(&build_bone_section_header(3));
6059
6060 buf.extend_from_slice(&build_bone_name_bytes("Root"));
6061 buf.extend_from_slice(&build_bone_name_bytes(""));
6062 buf.extend_from_slice(&0.0f32.to_le_bytes());
6063 buf.extend_from_slice(&0.0f32.to_le_bytes());
6064 buf.extend_from_slice(&0.0f32.to_le_bytes());
6065 buf.extend_from_slice(&(-1i16).to_le_bytes());
6066 buf.extend_from_slice(&0i32.to_le_bytes());
6067 buf.extend_from_slice(&0u16.to_le_bytes());
6068 buf.extend_from_slice(&0.0f32.to_le_bytes());
6069 buf.extend_from_slice(&0.0f32.to_le_bytes());
6070 buf.extend_from_slice(&0.0f32.to_le_bytes());
6071
6072 buf.extend_from_slice(&build_bone_name_bytes("IKLink"));
6073 buf.extend_from_slice(&build_bone_name_bytes(""));
6074 buf.extend_from_slice(&2.0f32.to_le_bytes());
6075 buf.extend_from_slice(&0.0f32.to_le_bytes());
6076 buf.extend_from_slice(&0.0f32.to_le_bytes());
6077 buf.extend_from_slice(&0i16.to_le_bytes());
6078 buf.extend_from_slice(&0i32.to_le_bytes());
6079 buf.extend_from_slice(&0u16.to_le_bytes());
6080 buf.extend_from_slice(&0.0f32.to_le_bytes());
6081 buf.extend_from_slice(&0.0f32.to_le_bytes());
6082 buf.extend_from_slice(&0.0f32.to_le_bytes());
6083
6084 buf.extend_from_slice(&build_bone_name_bytes("LeftLegIK"));
6085 buf.extend_from_slice(&build_bone_name_bytes(""));
6086 buf.extend_from_slice(&4.0f32.to_le_bytes());
6087 buf.extend_from_slice(&0.0f32.to_le_bytes());
6088 buf.extend_from_slice(&0.0f32.to_le_bytes());
6089 buf.extend_from_slice(&0i16.to_le_bytes());
6090 buf.extend_from_slice(&0i32.to_le_bytes());
6091 buf.extend_from_slice(&(BONE_FLAG_TAIL_INDEX | BONE_FLAG_IK).to_le_bytes());
6092 buf.extend_from_slice(&1i16.to_le_bytes());
6093 buf.extend_from_slice(&1i16.to_le_bytes());
6094 buf.extend_from_slice(&10i32.to_le_bytes());
6095 buf.extend_from_slice(&0.5f32.to_le_bytes());
6096 buf.extend_from_slice(&1i32.to_le_bytes());
6097 buf.extend_from_slice(&1i16.to_le_bytes());
6098 buf.push(1u8);
6099 buf.extend_from_slice(&(-1.0f32).to_le_bytes());
6100 buf.extend_from_slice(&(-1.0f32).to_le_bytes());
6101 buf.extend_from_slice(&(-1.0f32).to_le_bytes());
6102 buf.extend_from_slice(&1.0f32.to_le_bytes());
6103 buf.extend_from_slice(&1.0f32.to_le_bytes());
6104 buf.extend_from_slice(&1.0f32.to_le_bytes());
6105
6106 let bone_import = import_pmx_model(&buf).unwrap();
6107 assert_eq!(bone_import.ik_solvers.len(), 1);
6108 assert_eq!(bone_import.ik_solvers[0].ik_bone, BoneIndex(2));
6109 assert_eq!(
6110 bone_import.bone_names[bone_import.ik_solvers[0].ik_bone.as_usize()],
6111 "LeftLegIK"
6112 );
6113
6114 let ik_name_map: HashMap<Vec<u8>, usize> = bone_import
6115 .ik_solvers
6116 .iter()
6117 .enumerate()
6118 .filter_map(|(solver_idx, solver)| {
6119 let bone_idx = solver.ik_bone.as_usize();
6120 if bone_idx < bone_import.bone_name_bytes.len() {
6121 Some((bone_import.bone_name_bytes[bone_idx].clone(), solver_idx))
6122 } else {
6123 None
6124 }
6125 })
6126 .collect();
6127
6128 assert_eq!(ik_name_map.get(b"LeftLegIK".as_slice()), Some(&0));
6129 }
6130
6131 #[test]
6132 fn reads_morph_names_without_payloads() {
6133 let (header, pos) =
6134 read_header(&build_small_pmx_header_bytes(4, TextEncoding::Utf8)).unwrap();
6135
6136 let mut buf = vec![0u8; pos];
6137 buf.extend_from_slice(&1i32.to_le_bytes());
6138 buf.extend_from_slice(&build_morph_name_bytes("Smile"));
6139 buf.extend_from_slice(&build_morph_name_bytes(""));
6140 buf.push(4u8);
6141 buf.push(0u8);
6142 buf.extend_from_slice(&0i32.to_le_bytes());
6143
6144 let (morphs, new_pos) = read_morph_names(&buf[..], &header, pos).unwrap();
6145 assert_eq!(morphs.name_bytes.len(), 1);
6146 assert_eq!(&morphs.name_bytes[0][..], b"Smile");
6147 assert_eq!(morphs.names[0], "Smile");
6148 assert!(new_pos > pos);
6149 }
6150
6151 #[test]
6152 fn reads_morph_type_zero_with_vertex_group_offsets() {
6153 let (header, pos) =
6154 read_header(&build_small_pmx_header_bytes(4, TextEncoding::Utf8)).unwrap();
6155 assert_eq!(header.vertex_index_size, 4);
6156 assert_eq!(header.morph_index_size, 1);
6157
6158 let mut buf = vec![0u8; pos];
6159 buf.extend_from_slice(&1i32.to_le_bytes());
6160 buf.extend_from_slice(&build_morph_name_bytes("Blink"));
6161 buf.extend_from_slice(&build_morph_name_bytes(""));
6162 buf.push(0u8);
6163 buf.push(0u8);
6164 buf.extend_from_slice(&2i32.to_le_bytes());
6165 buf.push(0u8);
6166 buf.extend_from_slice(&[0u8; 4]);
6167 buf.push(1u8);
6168 buf.extend_from_slice(&[0u8; 4]);
6169
6170 let (morphs, new_pos) = read_morph_names(&buf[..], &header, pos).unwrap();
6171 assert_eq!(morphs.name_bytes.len(), 1);
6172 assert_eq!(&morphs.name_bytes[0][..], b"Blink");
6173 assert_eq!(morphs.names[0], "Blink");
6174 assert_eq!(new_pos, buf.len());
6175 }
6176
6177 #[test]
6178 fn runtime_import_builds_model_arena_with_name_lookup() {
6179 let mut buf = Vec::new();
6180 buf.extend_from_slice(&build_small_pmx_header_bytes(2, TextEncoding::Utf8));
6181 buf.extend_from_slice(&build_empty_model_info(TextEncoding::Utf8));
6182 buf.extend_from_slice(&build_empty_vertex_section());
6183 buf.extend_from_slice(&build_empty_face_section());
6184 buf.extend_from_slice(&build_empty_texture_section());
6185 buf.extend_from_slice(&build_empty_material_section());
6186 buf.extend_from_slice(&build_bone_section_header(2));
6187
6188 buf.extend_from_slice(&build_bone_name_bytes("Hips"));
6189 buf.extend_from_slice(&build_bone_name_bytes(""));
6190 buf.extend_from_slice(&0.0f32.to_le_bytes());
6191 buf.extend_from_slice(&0.0f32.to_le_bytes());
6192 buf.extend_from_slice(&0.0f32.to_le_bytes());
6193 buf.extend_from_slice(&(-1i16).to_le_bytes());
6194 buf.extend_from_slice(&0i32.to_le_bytes());
6195 buf.extend_from_slice(&0u16.to_le_bytes());
6196 buf.extend_from_slice(&0.0f32.to_le_bytes());
6197 buf.extend_from_slice(&0.0f32.to_le_bytes());
6198 buf.extend_from_slice(&0.0f32.to_le_bytes());
6199
6200 buf.extend_from_slice(&build_bone_name_bytes("Spine"));
6201 buf.extend_from_slice(&build_bone_name_bytes(""));
6202 buf.extend_from_slice(&0.0f32.to_le_bytes());
6203 buf.extend_from_slice(&1.0f32.to_le_bytes());
6204 buf.extend_from_slice(&0.0f32.to_le_bytes());
6205 buf.extend_from_slice(&0i16.to_le_bytes());
6206 buf.extend_from_slice(&0i32.to_le_bytes());
6207 buf.extend_from_slice(&0u16.to_le_bytes());
6208 buf.extend_from_slice(&0.0f32.to_le_bytes());
6209 buf.extend_from_slice(&0.0f32.to_le_bytes());
6210 buf.extend_from_slice(&0.0f32.to_le_bytes());
6211 buf.extend_from_slice(&0i32.to_le_bytes());
6212
6213 let rt = import_pmx_runtime(&buf).unwrap();
6214 assert_eq!(rt.model.bone_count(), 2);
6215 assert_eq!(rt.model.parent_index(BoneIndex(1)), Some(BoneIndex(0)));
6216 assert_eq!(
6217 rt.bone_name_to_index.get(b"Hips".as_slice()),
6218 Some(&BoneIndex(0))
6219 );
6220 assert_eq!(
6221 rt.bone_name_to_index.get(b"Spine".as_slice()),
6222 Some(&BoneIndex(1))
6223 );
6224 assert!(rt.morph_name_to_index.is_empty());
6225 assert!(rt.ik_solver_bone_name_to_index.is_empty());
6226 }
6227
6228 #[test]
6229 fn parse_pmx_model_exposes_three_loader_shaped_sections() {
6230 let mut buf = Vec::new();
6231 buf.extend_from_slice(&build_small_pmx_header_bytes(2, TextEncoding::Utf8));
6232 buf.extend_from_slice(&build_empty_model_info(TextEncoding::Utf8));
6233 buf.extend_from_slice(&build_empty_vertex_section());
6234 buf.extend_from_slice(&build_empty_face_section());
6235 buf.extend_from_slice(&build_empty_texture_section());
6236 buf.extend_from_slice(&build_empty_material_section());
6237 buf.extend_from_slice(&build_bone_section_header(2));
6238
6239 buf.extend_from_slice(&build_bone_name_bytes("Root"));
6240 buf.extend_from_slice(&build_bone_name_bytes(""));
6241 buf.extend_from_slice(&0.0f32.to_le_bytes());
6242 buf.extend_from_slice(&0.0f32.to_le_bytes());
6243 buf.extend_from_slice(&0.0f32.to_le_bytes());
6244 buf.extend_from_slice(&(-1i16).to_le_bytes());
6245 buf.extend_from_slice(&0i32.to_le_bytes());
6246 buf.extend_from_slice(&0u16.to_le_bytes());
6247 buf.extend_from_slice(&0.0f32.to_le_bytes());
6248 buf.extend_from_slice(&0.0f32.to_le_bytes());
6249 buf.extend_from_slice(&0.0f32.to_le_bytes());
6250
6251 buf.extend_from_slice(&build_bone_name_bytes("Child"));
6252 buf.extend_from_slice(&build_bone_name_bytes(""));
6253 buf.extend_from_slice(&0.0f32.to_le_bytes());
6254 buf.extend_from_slice(&1.0f32.to_le_bytes());
6255 buf.extend_from_slice(&0.0f32.to_le_bytes());
6256 buf.extend_from_slice(&0i16.to_le_bytes());
6257 buf.extend_from_slice(&0i32.to_le_bytes());
6258 buf.extend_from_slice(&0u16.to_le_bytes());
6259 buf.extend_from_slice(&0.0f32.to_le_bytes());
6260 buf.extend_from_slice(&0.0f32.to_le_bytes());
6261 buf.extend_from_slice(&0.0f32.to_le_bytes());
6262
6263 buf.extend_from_slice(&0i32.to_le_bytes()); buf.extend_from_slice(&1i32.to_le_bytes()); buf.extend_from_slice(&build_bone_name_bytes("RootFrame"));
6266 buf.extend_from_slice(&build_bone_name_bytes(""));
6267 buf.push(1);
6268 buf.extend_from_slice(&1i32.to_le_bytes());
6269 buf.push(0);
6270 buf.extend_from_slice(&1i16.to_le_bytes());
6271 buf.extend_from_slice(&0i32.to_le_bytes()); buf.extend_from_slice(&0i32.to_le_bytes()); let parsed = parse_pmx_model(&buf).unwrap();
6275 assert_eq!(parsed.metadata.format, "pmx");
6276 assert_eq!(parsed.metadata.counts.bones, 2);
6277 assert_eq!(parsed.skeleton.bones[0].name, "Root");
6278 assert_eq!(parsed.skeleton.bones[1].parent_index, 0);
6279 assert_eq!(parsed.display_frames.len(), 1);
6280 assert_eq!(parsed.display_frames[0].frames[0].kind, "bone");
6281 assert_eq!(parsed.display_frames[0].frames[0].index, 1);
6282 assert!(parsed.diagnostics.is_empty());
6283 }
6284
6285 #[test]
6286 fn exports_pmx_semantic_roundtrip() {
6287 let parsed = parsed_pmx_fixture();
6288 let exported = export_pmx_model(&parsed);
6289 let reparsed = parse_pmx_model(&exported).unwrap();
6290
6291 assert_pmx_roundtrip_eq(&parsed, &reparsed);
6292 }
6293
6294 #[test]
6295 fn exports_all_pmx_morph_panels_without_falling_back_to_other() {
6296 for panel in ["system", "brow", "eye", "mouth", "other"] {
6297 let mut parsed = parsed_pmx_fixture();
6298 parsed.morphs[0].panel = panel.to_owned();
6299
6300 let exported = export_pmx_model(&parsed);
6301 let reparsed = parse_pmx_model(&exported).unwrap();
6302
6303 assert_eq!(reparsed.morphs[0].panel, panel);
6304 }
6305 }
6306
6307 #[test]
6308 fn exports_pmx_json_dto_semantic_roundtrip() {
6309 let parsed = parsed_pmx_fixture();
6310 let json = serde_json::to_string(&parsed).unwrap();
6311 let from_json: PmxParsedModel = serde_json::from_str(&json).unwrap();
6312 let exported = export_pmx_model(&from_json);
6313 let reparsed = parse_pmx_model(&exported).unwrap();
6314
6315 assert_pmx_roundtrip_eq(&parsed, &reparsed);
6316 }
6317
6318 #[test]
6319 fn pmx_json_dto_roundtrips_non_finite_f32_values() {
6320 let mut parsed = parsed_pmx_fixture();
6321 let finite_position = parsed.geometry.positions[1];
6322 parsed.geometry.positions[0] = f32::NAN;
6323 parsed.geometry.normals[1] = f32::INFINITY;
6324 parsed.geometry.uvs[0] = f32::NEG_INFINITY;
6325 parsed.materials[0].diffuse[0] = f32::INFINITY;
6326 parsed.materials[0].edge_size = f32::NAN;
6327 parsed.skeleton.bones[0].position[0] = f32::NEG_INFINITY;
6328 parsed.morphs[0].vertex_offsets[0].position[1] = f32::NAN;
6329
6330 let json = serde_json::to_string(&parsed).unwrap();
6331 let value: serde_json::Value = serde_json::from_str(&json).unwrap();
6332
6333 assert!(json.contains("\"NaN\""));
6334 assert!(json.contains("\"Infinity\""));
6335 assert!(json.contains("\"-Infinity\""));
6336 assert_eq!(value["geometry"]["positions"][0], serde_json::json!("NaN"));
6337 assert_eq!(
6338 value["geometry"]["normals"][1],
6339 serde_json::json!("Infinity")
6340 );
6341 assert_eq!(value["geometry"]["uvs"][0], serde_json::json!("-Infinity"));
6342 assert_eq!(
6343 value["geometry"]["positions"][1],
6344 serde_json::json!(finite_position)
6345 );
6346
6347 let from_json: PmxParsedModel = serde_json::from_str(&json).unwrap();
6348
6349 assert!(from_json.geometry.positions[0].is_nan());
6350 assert_eq!(from_json.geometry.normals[1], f32::INFINITY);
6351 assert_eq!(from_json.geometry.uvs[0], f32::NEG_INFINITY);
6352 assert_eq!(from_json.materials[0].diffuse[0], f32::INFINITY);
6353 assert!(from_json.materials[0].edge_size.is_nan());
6354 assert_eq!(from_json.skeleton.bones[0].position[0], f32::NEG_INFINITY);
6355 assert!(from_json.morphs[0].vertex_offsets[0].position[1].is_nan());
6356 assert_eq!(from_json.geometry.positions[1], finite_position);
6357 }
6358
6359 #[test]
6360 fn builds_pmx_parts_with_material_descriptors() {
6361 let descriptor: PmxPartsDescriptor = serde_json::from_value(serde_json::json!({
6362 "name": "material-parts",
6363 "materials": [
6364 {
6365 "name": "red",
6366 "englishName": "red-en",
6367 "diffuse": [1.0, 0.0, 0.0, 1.0],
6368 "ambient": [0.5, 0.0, 0.0],
6369 "flags": { "edge": true },
6370 "faceCount": 1
6371 },
6372 {
6373 "name": "blue",
6374 "diffuse": [0.0, 0.0, 1.0, 1.0],
6375 "faceCount": 1
6376 }
6377 ],
6378 "bones": [
6379 { "name": "root", "englishName": "root-en", "tailIndex": 1 },
6380 { "name": "child", "parentIndex": 0, "position": [0.0, 1.0, 0.0], "rotatable": true }
6381 ],
6382 "morphs": [
6383 {
6384 "name": "raise",
6385 "englishName": "raise-en",
6386 "kind": "vertex",
6387 "vertexOffsets": [{ "vertexIndex": 0, "position": [0.0, 0.1, 0.0] }]
6388 },
6389 {
6390 "name": "combo",
6391 "kind": "group",
6392 "groupOffsets": [{ "morphIndex": 0, "weight": 0.5 }]
6393 }
6394 ],
6395 "displayFrames": [
6396 {
6397 "name": "Root",
6398 "englishName": "Root-en",
6399 "special": true,
6400 "frames": [
6401 { "kind": "bone", "index": 0 },
6402 { "kind": "bone", "index": 1 },
6403 { "kind": "morph", "index": 0 }
6404 ]
6405 }
6406 ],
6407 "rigidBodies": [
6408 {
6409 "name": "body",
6410 "englishName": "body-en",
6411 "boneIndex": 1,
6412 "group": 2,
6413 "mask": 3,
6414 "shape": "box",
6415 "size": [1.0, 2.0, 3.0],
6416 "position": [0.0, 1.0, 0.0],
6417 "rotation": [0.1, 0.2, 0.3],
6418 "mass": 2.0,
6419 "linearDamping": 0.4,
6420 "angularDamping": 0.5,
6421 "restitution": 0.6,
6422 "friction": 0.7,
6423 "mode": "dynamicBone"
6424 }
6425 ],
6426 "joints": [
6427 {
6428 "name": "joint",
6429 "type": "generic6dofSpring",
6430 "rigidBodyIndexA": 0,
6431 "rigidBodyIndexB": -1,
6432 "position": [0.0, 1.0, 0.0],
6433 "translationLowerLimit": [-1.0, -1.0, -1.0],
6434 "translationUpperLimit": [1.0, 1.0, 1.0],
6435 "springTranslationFactor": [0.1, 0.2, 0.3]
6436 }
6437 ],
6438 "indexSizes": {
6439 "vertex": 1,
6440 "material": 1,
6441 "texture": 1,
6442 "bone": 1,
6443 "morph": 1,
6444 "rigidBody": 1
6445 }
6446 }))
6447 .unwrap();
6448 let model = build_pmx_model_from_parts(PmxPartsInput {
6449 descriptor,
6450 positions_xyz: &[
6451 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 1.0, 0.0, 0.0, 1.0, 0.0,
6455 ],
6456 normals_xyz: &[
6457 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0,
6461 ],
6462 uvs_xy: &[0.0, 0.0, 1.0, 0.0, 1.0, 1.0, 0.0, 1.0],
6463 indices: &[0, 1, 2, 0, 2, 3],
6464 skin_indices: &[0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0],
6465 skin_weights: &[
6466 0.75, 0.25, 0.0, 0.0, 0.5, 0.5, 0.0, 0.0, 0.5, 0.5, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0,
6467 ],
6468 edge_scale: &[],
6469 })
6470 .unwrap();
6471 let exported = export_pmx_model(&model);
6472 let reparsed = parse_pmx_model(&exported).unwrap();
6473
6474 assert_eq!(reparsed.metadata.name, "material-parts");
6475 assert_eq!(reparsed.metadata.counts.materials, 2);
6476 assert_eq!(reparsed.materials[0].name, "red");
6477 assert_eq!(reparsed.materials[0].english_name, "red-en");
6478 assert_eq!(reparsed.materials[0].face_count, 1);
6479 assert!(reparsed.materials[0].flags.edge);
6480 assert_eq!(reparsed.materials[1].name, "blue");
6481 assert_eq!(reparsed.materials[1].face_count, 1);
6482 assert_eq!(reparsed.geometry.material_groups.len(), 2);
6483 assert_eq!(reparsed.geometry.material_groups[0].start, 0);
6484 assert_eq!(reparsed.geometry.material_groups[0].count, 3);
6485 assert_eq!(reparsed.geometry.material_groups[1].start, 3);
6486 assert_eq!(reparsed.geometry.material_groups[1].count, 3);
6487 assert_eq!(reparsed.metadata.counts.bones, 2);
6488 assert_eq!(reparsed.skeleton.bones[0].name, "root");
6489 assert_eq!(reparsed.skeleton.bones[0].english_name, "root-en");
6490 assert_eq!(reparsed.skeleton.bones[0].tail_index, 1);
6491 assert!(reparsed.skeleton.bones[0].flags.indexed_tail);
6492 assert_eq!(reparsed.skeleton.bones[1].name, "child");
6493 assert_eq!(reparsed.skeleton.bones[1].parent_index, 0);
6494 assert!(reparsed.skeleton.bones[1].flags.rotatable);
6495 assert_eq!(reparsed.geometry.skin_indices[1], 1);
6496 assert_eq!(reparsed.geometry.skin_weights[1], 0.25);
6497 assert_eq!(reparsed.metadata.counts.morphs, 2);
6498 assert_eq!(reparsed.morphs[0].name, "raise");
6499 assert_eq!(reparsed.morphs[0].english_name, "raise-en");
6500 assert_eq!(reparsed.morphs[0].kind, "vertex");
6501 assert_eq!(reparsed.morphs[0].vertex_offsets[0].vertex_index, 0);
6502 assert_eq!(
6503 reparsed.morphs[0].vertex_offsets[0].position,
6504 [0.0, 0.1, 0.0]
6505 );
6506 assert_eq!(reparsed.morphs[1].name, "combo");
6507 assert_eq!(reparsed.morphs[1].kind, "group");
6508 assert_eq!(reparsed.morphs[1].group_offsets[0].morph_index, 0);
6509 assert_eq!(reparsed.morphs[1].group_offsets[0].weight, 0.5);
6510 assert_eq!(reparsed.display_frames.len(), 1);
6511 assert_eq!(reparsed.display_frames[0].name, "Root");
6512 assert_eq!(reparsed.display_frames[0].english_name, "Root-en");
6513 assert!(reparsed.display_frames[0].special);
6514 assert_eq!(reparsed.display_frames[0].frames.len(), 3);
6515 assert_eq!(reparsed.display_frames[0].frames[1].kind, "bone");
6516 assert_eq!(reparsed.display_frames[0].frames[1].index, 1);
6517 assert_eq!(reparsed.display_frames[0].frames[2].kind, "morph");
6518 assert_eq!(reparsed.display_frames[0].frames[2].index, 0);
6519 assert_eq!(reparsed.rigid_bodies.len(), 1);
6520 assert_eq!(reparsed.rigid_bodies[0].name, "body");
6521 assert_eq!(reparsed.rigid_bodies[0].english_name, "body-en");
6522 assert_eq!(reparsed.rigid_bodies[0].bone_index, 1);
6523 assert_eq!(reparsed.rigid_bodies[0].group, 2);
6524 assert_eq!(reparsed.rigid_bodies[0].mask, 3);
6525 assert_eq!(reparsed.rigid_bodies[0].shape, "box");
6526 assert_eq!(reparsed.rigid_bodies[0].mode, "dynamicBone");
6527 assert_eq!(reparsed.joints.len(), 1);
6528 assert_eq!(reparsed.joints[0].name, "joint");
6529 assert_eq!(reparsed.joints[0].kind, "generic6dofSpring");
6530 assert_eq!(reparsed.joints[0].rigid_body_index_a, 0);
6531 assert_eq!(reparsed.joints[0].rigid_body_index_b, -1);
6532 }
6533
6534 #[test]
6535 fn pmx_parts_rigidbody_mask_defaults_to_collide_with_all_groups() {
6536 let descriptor: PmxPartsDescriptor = serde_json::from_value(serde_json::json!({
6537 "bones": [{ "name": "root" }],
6538 "rigidBodies": [
6539 {
6540 "name": "body",
6541 "boneIndex": 0
6542 }
6543 ]
6544 }))
6545 .unwrap();
6546
6547 assert_eq!(descriptor.rigid_bodies[0].mask, 0xffff);
6548 }
6549
6550 #[test]
6551 fn rejects_pmx_parts_material_face_count_mismatch() {
6552 let descriptor: PmxPartsDescriptor = serde_json::from_value(serde_json::json!({
6553 "materials": [{ "name": "bad", "faceCount": 2 }]
6554 }))
6555 .unwrap();
6556 let error = build_pmx_model_from_parts(PmxPartsInput {
6557 descriptor,
6558 positions_xyz: &[0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0, 0.0],
6559 normals_xyz: &[0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0],
6560 uvs_xy: &[0.0, 0.0, 1.0, 0.0, 0.0, 1.0],
6561 indices: &[0, 1, 2],
6562 skin_indices: &[],
6563 skin_weights: &[],
6564 edge_scale: &[],
6565 })
6566 .unwrap_err();
6567
6568 assert!(error.contains("materials faceCount sum"));
6569 }
6570
6571 #[test]
6572 fn rejects_invalid_pmx_parts_morph_panel() {
6573 let descriptor: PmxPartsDescriptor = serde_json::from_value(serde_json::json!({
6574 "morphs": [{ "name": "typo", "kind": "vertex", "panel": "moutn" }]
6575 }))
6576 .unwrap();
6577 let error = build_pmx_model_from_parts(PmxPartsInput {
6578 descriptor,
6579 positions_xyz: &[0.0, 0.0, 0.0],
6580 normals_xyz: &[0.0, 1.0, 0.0],
6581 uvs_xy: &[0.0, 0.0],
6582 indices: &[],
6583 skin_indices: &[],
6584 skin_weights: &[],
6585 edge_scale: &[],
6586 })
6587 .unwrap_err();
6588
6589 assert_eq!(
6590 error,
6591 "morphs[0].panel must be system, brow, eye, mouth, or other, got moutn"
6592 );
6593 }
6594
6595 #[test]
6596 fn rejects_pmx_export_model_partial_skin_arrays() {
6597 let mut parsed = parsed_pmx_fixture();
6598 parsed.geometry.skin_weights.clear();
6599
6600 let error = validate_pmx_export_model(&parsed).unwrap_err();
6601
6602 assert!(error.contains("skinIndices and skinWeights"));
6603 }
6604
6605 #[test]
6608 fn exports_pmx_22_synthetic_roundtrip() {
6609 let mut parsed = parsed_pmx_fixture();
6610 parsed.metadata.version = 2.2;
6611
6612 let exported = export_pmx_model(&parsed);
6613 let reparsed = parse_pmx_model(&exported).unwrap();
6614
6615 assert_eq!(
6616 reparsed.metadata.version, 2.2,
6617 "PMX 2.2 version must survive the export-parse cycle"
6618 );
6619 assert_pmx_roundtrip_eq(&parsed, &reparsed);
6620 }
6621
6622 #[test]
6623 fn exports_pmx_soft_body_header_fields_with_diagnostic_for_pmx_21_and_22() {
6624 for version in [2.1, 2.2] {
6625 let mut parsed = parsed_pmx_fixture();
6626 parsed.metadata.version = version;
6627 parsed.soft_bodies = vec![PmxParsedSoftBody {
6628 name: "soft".to_owned(),
6629 english_name: "soft-en".to_owned(),
6630 kind: "rope".to_owned(),
6631 material_index: 0,
6632 collision_group: 1,
6633 collision_mask: 2,
6634 flags: 3,
6635 bending_constraints_distance: 4,
6636 cluster_count: 5,
6637 total_mass: 6.0,
6638 collision_margin: 0.7,
6639 }];
6640 let exported = export_pmx_model(&parsed);
6641 let reparsed = parse_pmx_model(&exported).unwrap();
6642
6643 assert_eq!(reparsed.metadata.version, version);
6644 assert_eq!(reparsed.metadata.counts.soft_bodies, 1);
6645 assert_eq!(reparsed.soft_bodies.len(), 1);
6646 assert_eq!(reparsed.soft_bodies[0].name, "soft");
6647 assert_eq!(reparsed.soft_bodies[0].english_name, "soft-en");
6648 assert_eq!(reparsed.soft_bodies[0].kind, "rope");
6649 assert_eq!(reparsed.soft_bodies[0].material_index, 0);
6650 assert_eq!(reparsed.soft_bodies[0].collision_group, 1);
6651 assert_eq!(reparsed.soft_bodies[0].collision_mask, 2);
6652 assert_eq!(reparsed.soft_bodies[0].flags, 3);
6653 assert_eq!(reparsed.soft_bodies[0].bending_constraints_distance, 4);
6654 assert_eq!(reparsed.soft_bodies[0].cluster_count, 5);
6655 assert_eq!(reparsed.soft_bodies[0].total_mass, 6.0);
6656 assert_eq!(reparsed.soft_bodies[0].collision_margin, 0.7);
6657 assert_eq!(reparsed.diagnostics.len(), 1);
6658 assert_eq!(reparsed.diagnostics[0].code, "PMX_SOFT_BODY_UNSUPPORTED");
6659 }
6660 }
6661
6662 #[test]
6663 fn exports_parsed_pmx_skeleton_display_roundtrip() {
6664 let mut buf = Vec::new();
6665 buf.extend_from_slice(&build_small_pmx_header_bytes(2, TextEncoding::Utf8));
6666 buf.extend_from_slice(&build_empty_model_info(TextEncoding::Utf8));
6667 buf.extend_from_slice(&build_empty_vertex_section());
6668 buf.extend_from_slice(&build_empty_face_section());
6669 buf.extend_from_slice(&build_empty_texture_section());
6670 buf.extend_from_slice(&build_empty_material_section());
6671 buf.extend_from_slice(&build_bone_section_header(2));
6672
6673 buf.extend_from_slice(&build_bone_name_bytes("Root"));
6674 buf.extend_from_slice(&build_bone_name_bytes(""));
6675 buf.extend_from_slice(&0.0f32.to_le_bytes());
6676 buf.extend_from_slice(&0.0f32.to_le_bytes());
6677 buf.extend_from_slice(&0.0f32.to_le_bytes());
6678 buf.extend_from_slice(&(-1i16).to_le_bytes());
6679 buf.extend_from_slice(&0i32.to_le_bytes());
6680 buf.extend_from_slice(&0u16.to_le_bytes());
6681 buf.extend_from_slice(&0.0f32.to_le_bytes());
6682 buf.extend_from_slice(&0.0f32.to_le_bytes());
6683 buf.extend_from_slice(&0.0f32.to_le_bytes());
6684
6685 buf.extend_from_slice(&build_bone_name_bytes("Child"));
6686 buf.extend_from_slice(&build_bone_name_bytes(""));
6687 buf.extend_from_slice(&0.0f32.to_le_bytes());
6688 buf.extend_from_slice(&1.0f32.to_le_bytes());
6689 buf.extend_from_slice(&0.0f32.to_le_bytes());
6690 buf.extend_from_slice(&0i16.to_le_bytes());
6691 buf.extend_from_slice(&0i32.to_le_bytes());
6692 buf.extend_from_slice(&0u16.to_le_bytes());
6693 buf.extend_from_slice(&0.0f32.to_le_bytes());
6694 buf.extend_from_slice(&0.0f32.to_le_bytes());
6695 buf.extend_from_slice(&0.0f32.to_le_bytes());
6696
6697 buf.extend_from_slice(&0i32.to_le_bytes()); buf.extend_from_slice(&1i32.to_le_bytes()); buf.extend_from_slice(&build_bone_name_bytes("RootFrame"));
6700 buf.extend_from_slice(&build_bone_name_bytes(""));
6701 buf.push(1);
6702 buf.extend_from_slice(&1i32.to_le_bytes());
6703 buf.push(0);
6704 buf.extend_from_slice(&1i16.to_le_bytes());
6705 buf.extend_from_slice(&0i32.to_le_bytes()); buf.extend_from_slice(&0i32.to_le_bytes()); let parsed = parse_pmx_model(&buf).unwrap();
6709 let exported = export_pmx_model(&parsed);
6710 let reparsed = parse_pmx_model(&exported).unwrap();
6711
6712 assert_pmx_roundtrip_eq(&parsed, &reparsed);
6713 }
6714
6715 #[test]
6716 fn runtime_import_ik_solver_name_lookup() {
6717 let mut buf = Vec::new();
6718 buf.extend_from_slice(&build_small_pmx_header_bytes(2, TextEncoding::Utf8));
6719 buf.extend_from_slice(&build_empty_model_info(TextEncoding::Utf8));
6720 buf.extend_from_slice(&build_empty_vertex_section());
6721 buf.extend_from_slice(&build_empty_face_section());
6722 buf.extend_from_slice(&build_empty_texture_section());
6723 buf.extend_from_slice(&build_empty_material_section());
6724 buf.extend_from_slice(&build_bone_section_header(3));
6725
6726 buf.extend_from_slice(&build_bone_name_bytes("Root"));
6727 buf.extend_from_slice(&build_bone_name_bytes(""));
6728 buf.extend_from_slice(&0.0f32.to_le_bytes());
6729 buf.extend_from_slice(&0.0f32.to_le_bytes());
6730 buf.extend_from_slice(&0.0f32.to_le_bytes());
6731 buf.extend_from_slice(&(-1i16).to_le_bytes());
6732 buf.extend_from_slice(&0i32.to_le_bytes());
6733 buf.extend_from_slice(&0u16.to_le_bytes());
6734 buf.extend_from_slice(&0.0f32.to_le_bytes());
6735 buf.extend_from_slice(&0.0f32.to_le_bytes());
6736 buf.extend_from_slice(&0.0f32.to_le_bytes());
6737
6738 buf.extend_from_slice(&build_bone_name_bytes("IKLink"));
6739 buf.extend_from_slice(&build_bone_name_bytes(""));
6740 buf.extend_from_slice(&2.0f32.to_le_bytes());
6741 buf.extend_from_slice(&0.0f32.to_le_bytes());
6742 buf.extend_from_slice(&0.0f32.to_le_bytes());
6743 buf.extend_from_slice(&0i16.to_le_bytes());
6744 buf.extend_from_slice(&0i32.to_le_bytes());
6745 buf.extend_from_slice(&0u16.to_le_bytes());
6746 buf.extend_from_slice(&0.0f32.to_le_bytes());
6747 buf.extend_from_slice(&0.0f32.to_le_bytes());
6748 buf.extend_from_slice(&0.0f32.to_le_bytes());
6749
6750 buf.extend_from_slice(&build_bone_name_bytes("LeftLegIK"));
6751 buf.extend_from_slice(&build_bone_name_bytes(""));
6752 buf.extend_from_slice(&4.0f32.to_le_bytes());
6753 buf.extend_from_slice(&0.0f32.to_le_bytes());
6754 buf.extend_from_slice(&0.0f32.to_le_bytes());
6755 buf.extend_from_slice(&0i16.to_le_bytes());
6756 buf.extend_from_slice(&0i32.to_le_bytes());
6757 buf.extend_from_slice(&(BONE_FLAG_TAIL_INDEX | BONE_FLAG_IK).to_le_bytes());
6758 buf.extend_from_slice(&1i16.to_le_bytes());
6759 buf.extend_from_slice(&1i16.to_le_bytes());
6760 buf.extend_from_slice(&10i32.to_le_bytes());
6761 buf.extend_from_slice(&0.5f32.to_le_bytes());
6762 buf.extend_from_slice(&1i32.to_le_bytes());
6763 buf.extend_from_slice(&1i16.to_le_bytes());
6764 buf.push(1u8);
6765 buf.extend_from_slice(&(-1.0f32).to_le_bytes());
6766 buf.extend_from_slice(&(-1.0f32).to_le_bytes());
6767 buf.extend_from_slice(&(-1.0f32).to_le_bytes());
6768 buf.extend_from_slice(&1.0f32.to_le_bytes());
6769 buf.extend_from_slice(&1.0f32.to_le_bytes());
6770 buf.extend_from_slice(&1.0f32.to_le_bytes());
6771 buf.extend_from_slice(&0i32.to_le_bytes());
6772
6773 let rt = import_pmx_runtime(&buf).unwrap();
6774 assert_eq!(rt.model.bone_count(), 3);
6775 assert_eq!(rt.model.ik_count(), 1);
6776 assert_eq!(
6777 rt.ik_solver_bone_name_to_index.get(b"LeftLegIK".as_slice()),
6778 Some(&0)
6779 );
6780 assert_eq!(
6781 rt.bone_name_to_index.get(b"LeftLegIK".as_slice()),
6782 Some(&BoneIndex(2))
6783 );
6784 assert_eq!(
6785 rt.ik_solver_bone_name_to_index.get(b"NoSuchIK".as_slice()),
6786 None
6787 );
6788 }
6789
6790 #[test]
6791 fn converts_absolute_pmx_positions_to_local_rest_positions() {
6792 let mut buf = Vec::new();
6793 buf.extend_from_slice(&build_small_pmx_header_bytes(2, TextEncoding::Utf8));
6794 buf.extend_from_slice(&build_empty_model_info(TextEncoding::Utf8));
6795 buf.extend_from_slice(&build_empty_vertex_section());
6796 buf.extend_from_slice(&build_empty_face_section());
6797 buf.extend_from_slice(&build_empty_texture_section());
6798 buf.extend_from_slice(&build_empty_material_section());
6799 buf.extend_from_slice(&build_bone_section_header(2));
6800
6801 buf.extend_from_slice(&build_bone_name_bytes("Parent"));
6802 buf.extend_from_slice(&build_bone_name_bytes(""));
6803 buf.extend_from_slice(&1.0f32.to_le_bytes());
6804 buf.extend_from_slice(&2.0f32.to_le_bytes());
6805 buf.extend_from_slice(&0.0f32.to_le_bytes());
6806 buf.extend_from_slice(&(-1i16).to_le_bytes());
6807 buf.extend_from_slice(&0i32.to_le_bytes());
6808 buf.extend_from_slice(&0u16.to_le_bytes());
6809 buf.extend_from_slice(&0.0f32.to_le_bytes());
6810 buf.extend_from_slice(&0.0f32.to_le_bytes());
6811 buf.extend_from_slice(&0.0f32.to_le_bytes());
6812
6813 buf.extend_from_slice(&build_bone_name_bytes("Child"));
6814 buf.extend_from_slice(&build_bone_name_bytes(""));
6815 buf.extend_from_slice(&1.0f32.to_le_bytes());
6816 buf.extend_from_slice(&3.0f32.to_le_bytes());
6817 buf.extend_from_slice(&0.0f32.to_le_bytes());
6818 buf.extend_from_slice(&0i16.to_le_bytes());
6819 buf.extend_from_slice(&0i32.to_le_bytes());
6820 buf.extend_from_slice(&0u16.to_le_bytes());
6821 buf.extend_from_slice(&0.0f32.to_le_bytes());
6822 buf.extend_from_slice(&0.0f32.to_le_bytes());
6823 buf.extend_from_slice(&0.0f32.to_le_bytes());
6824
6825 let result = import_pmx_model(&buf).unwrap();
6826 assert_eq!(result.bones.len(), 2);
6827 assert_eq!(result.bones[0].parent, None);
6828 assert!((result.bones[0].rest_position.x - 1.0).abs() < 0.001);
6829 assert!((result.bones[0].rest_position.y - 2.0).abs() < 0.001);
6830 assert!((result.bones[0].rest_position.z - 0.0).abs() < 0.001);
6831 assert_eq!(result.bones[1].parent, Some(BoneIndex(0)));
6832 assert!((result.bones[1].rest_position.x - 0.0).abs() < 0.001);
6833 assert!((result.bones[1].rest_position.y - 1.0).abs() < 0.001);
6834 assert!((result.bones[1].rest_position.z - 0.0).abs() < 0.001);
6835 }
6836
6837 #[test]
6838 fn root_inverse_bind_cancels_rest_world() {
6839 let mut buf = Vec::new();
6840 buf.extend_from_slice(&build_small_pmx_header_bytes(2, TextEncoding::Utf8));
6841 buf.extend_from_slice(&build_empty_model_info(TextEncoding::Utf8));
6842 buf.extend_from_slice(&build_empty_vertex_section());
6843 buf.extend_from_slice(&build_empty_face_section());
6844 buf.extend_from_slice(&build_empty_texture_section());
6845 buf.extend_from_slice(&build_empty_material_section());
6846 buf.extend_from_slice(&build_bone_section_header(1));
6847
6848 buf.extend_from_slice(&build_bone_name_bytes("Root"));
6849 buf.extend_from_slice(&build_bone_name_bytes(""));
6850 buf.extend_from_slice(&1.0f32.to_le_bytes());
6851 buf.extend_from_slice(&2.0f32.to_le_bytes());
6852 buf.extend_from_slice(&3.0f32.to_le_bytes());
6853 buf.extend_from_slice(&(-1i16).to_le_bytes());
6854 buf.extend_from_slice(&0i32.to_le_bytes());
6855 buf.extend_from_slice(&0u16.to_le_bytes());
6856 buf.extend_from_slice(&0.0f32.to_le_bytes());
6857 buf.extend_from_slice(&0.0f32.to_le_bytes());
6858 buf.extend_from_slice(&0.0f32.to_le_bytes());
6859
6860 let result = import_pmx_model(&buf).unwrap();
6861 let ibm = result.bones[0].inverse_bind_matrix;
6862 let rest_world = Mat4::from_translation(Vec3::new(1.0, 2.0, 3.0));
6863 assert_eq!(rest_world * ibm, Mat4::IDENTITY);
6864 }
6865
6866 #[test]
6867 fn reads_pmx_fixed_axis_bone_descriptor() {
6868 let mut buf = Vec::new();
6869 buf.extend_from_slice(&build_small_pmx_header_bytes(2, TextEncoding::Utf8));
6870 buf.extend_from_slice(&build_empty_model_info(TextEncoding::Utf8));
6871 buf.extend_from_slice(&build_empty_vertex_section());
6872 buf.extend_from_slice(&build_empty_face_section());
6873 buf.extend_from_slice(&build_empty_texture_section());
6874 buf.extend_from_slice(&build_empty_material_section());
6875 buf.extend_from_slice(&build_bone_section_header(1));
6876
6877 buf.extend_from_slice(&build_bone_name_bytes("Fixed"));
6878 buf.extend_from_slice(&build_bone_name_bytes(""));
6879 buf.extend_from_slice(&0.0f32.to_le_bytes());
6880 buf.extend_from_slice(&0.0f32.to_le_bytes());
6881 buf.extend_from_slice(&0.0f32.to_le_bytes());
6882 buf.extend_from_slice(&(-1i16).to_le_bytes());
6883 buf.extend_from_slice(&0i32.to_le_bytes());
6884 buf.extend_from_slice(&BONE_FLAG_FIXED_AXIS.to_le_bytes());
6885 buf.extend_from_slice(&0.0f32.to_le_bytes());
6886 buf.extend_from_slice(&0.0f32.to_le_bytes());
6887 buf.extend_from_slice(&0.0f32.to_le_bytes());
6888 buf.extend_from_slice(&0.0f32.to_le_bytes());
6889 buf.extend_from_slice(&1.0f32.to_le_bytes());
6890 buf.extend_from_slice(&0.0f32.to_le_bytes());
6891
6892 let result = import_pmx_model(&buf).unwrap();
6893 assert_eq!(result.bones[0].fixed_axis, Some(Vec3A::Y));
6894 assert!(!result.bones[0].enforce_fixed_axis);
6895 }
6896
6897 #[test]
6898 fn reads_pmx_local_axis_bone_descriptor_into_runtime_init() {
6899 let mut buf = Vec::new();
6900 buf.extend_from_slice(&build_small_pmx_header_bytes(2, TextEncoding::Utf8));
6901 buf.extend_from_slice(&build_empty_model_info(TextEncoding::Utf8));
6902 buf.extend_from_slice(&build_empty_vertex_section());
6903 buf.extend_from_slice(&build_empty_face_section());
6904 buf.extend_from_slice(&build_empty_texture_section());
6905 buf.extend_from_slice(&build_empty_material_section());
6906 buf.extend_from_slice(&build_bone_section_header(1));
6907
6908 buf.extend_from_slice(&build_bone_name_bytes("Local"));
6909 buf.extend_from_slice(&build_bone_name_bytes(""));
6910 buf.extend_from_slice(&0.0f32.to_le_bytes());
6911 buf.extend_from_slice(&0.0f32.to_le_bytes());
6912 buf.extend_from_slice(&0.0f32.to_le_bytes());
6913 buf.extend_from_slice(&(-1i16).to_le_bytes());
6914 buf.extend_from_slice(&0i32.to_le_bytes());
6915 buf.extend_from_slice(&BONE_FLAG_LOCAL_AXIS.to_le_bytes());
6916 buf.extend_from_slice(&0.0f32.to_le_bytes());
6918 buf.extend_from_slice(&0.0f32.to_le_bytes());
6919 buf.extend_from_slice(&0.0f32.to_le_bytes());
6920 buf.extend_from_slice(&0.0f32.to_le_bytes());
6922 buf.extend_from_slice(&1.0f32.to_le_bytes());
6923 buf.extend_from_slice(&0.0f32.to_le_bytes());
6924 buf.extend_from_slice(&0.0f32.to_le_bytes());
6925 buf.extend_from_slice(&0.0f32.to_le_bytes());
6926 buf.extend_from_slice(&1.0f32.to_le_bytes());
6927 buf.extend_from_slice(&0i32.to_le_bytes());
6929
6930 let result = import_pmx_runtime(&buf).expect("localAxis PMX must import for runtime");
6931 let model = result.model;
6932 let local = model
6933 .local_axis(BoneIndex(0))
6934 .expect("runtime import must retain PMX localAxis");
6935 assert_eq!(local.x, Vec3A::Y);
6936 assert_eq!(local.z, Vec3A::Z);
6937 let basis = model
6938 .local_axis_basis(BoneIndex(0))
6939 .expect("runtime import must precompute a localAxis basis");
6940 assert!((basis.mul_vec3a(Vec3A::X) - Vec3A::Y).length() < 1.0e-5);
6941 assert!((basis.mul_vec3a(Vec3A::Y) + Vec3A::X).length() < 1.0e-5);
6942 assert!((basis.mul_vec3a(Vec3A::Z) - Vec3A::Z).length() < 1.0e-5);
6943 }
6944
6945 #[test]
6946 fn runtime_import_drops_degenerate_local_axis() {
6947 let mut buf = Vec::new();
6948 buf.extend_from_slice(&build_small_pmx_header_bytes(2, TextEncoding::Utf8));
6949 buf.extend_from_slice(&build_empty_model_info(TextEncoding::Utf8));
6950 buf.extend_from_slice(&build_empty_vertex_section());
6951 buf.extend_from_slice(&build_empty_face_section());
6952 buf.extend_from_slice(&build_empty_texture_section());
6953 buf.extend_from_slice(&build_empty_material_section());
6954 buf.extend_from_slice(&build_bone_section_header(1));
6955 buf.extend_from_slice(&build_bone_name_bytes("DegenerateLocal"));
6956 buf.extend_from_slice(&build_bone_name_bytes(""));
6957 buf.extend_from_slice(&[0.0f32, 0.0, 0.0].map(f32::to_le_bytes).concat());
6958 buf.extend_from_slice(&(-1i16).to_le_bytes());
6959 buf.extend_from_slice(&0i32.to_le_bytes());
6960 buf.extend_from_slice(&BONE_FLAG_LOCAL_AXIS.to_le_bytes());
6961 buf.extend_from_slice(&[0.0f32, 0.0, 0.0].map(f32::to_le_bytes).concat());
6962 buf.extend_from_slice(&[0.0f32, 0.0, 0.0].map(f32::to_le_bytes).concat());
6965 buf.extend_from_slice(&[0.0f32, 0.0, 1.0].map(f32::to_le_bytes).concat());
6966 buf.extend_from_slice(&0i32.to_le_bytes());
6967
6968 let imported = import_pmx_runtime(&buf).expect("degenerate PMX axis is tolerated");
6969 assert!(imported.model.local_axis(BoneIndex(0)).is_none());
6970 }
6971
6972 #[test]
6973 fn runtime_import_drops_degenerate_fixed_axis() {
6974 let mut buf = Vec::new();
6975 buf.extend_from_slice(&build_small_pmx_header_bytes(2, TextEncoding::Utf8));
6976 buf.extend_from_slice(&build_empty_model_info(TextEncoding::Utf8));
6977 buf.extend_from_slice(&build_empty_vertex_section());
6978 buf.extend_from_slice(&build_empty_face_section());
6979 buf.extend_from_slice(&build_empty_texture_section());
6980 buf.extend_from_slice(&build_empty_material_section());
6981 buf.extend_from_slice(&build_bone_section_header(1));
6982 buf.extend_from_slice(&build_bone_name_bytes("DegenerateFixed"));
6983 buf.extend_from_slice(&build_bone_name_bytes(""));
6984 buf.extend_from_slice(&[0.0f32, 0.0, 0.0].map(f32::to_le_bytes).concat());
6985 buf.extend_from_slice(&(-1i16).to_le_bytes());
6986 buf.extend_from_slice(&0i32.to_le_bytes());
6987 buf.extend_from_slice(&BONE_FLAG_FIXED_AXIS.to_le_bytes());
6988 buf.extend_from_slice(&[0.0f32, 0.0, 0.0].map(f32::to_le_bytes).concat());
6989 buf.extend_from_slice(&[0.0f32, 0.0, 0.0].map(f32::to_le_bytes).concat());
6990 buf.extend_from_slice(&0i32.to_le_bytes());
6991
6992 let imported = import_pmx_runtime(&buf).expect("degenerate PMX axis is tolerated");
6993 assert!(imported.model.fixed_axis(BoneIndex(0)).is_none());
6994 }
6995
6996 #[test]
6997 fn nil_append_parent_is_sanitized_to_no_append() {
6998 let mut buf = Vec::new();
6999 buf.extend_from_slice(&build_small_pmx_header_bytes(2, TextEncoding::Utf8));
7000 buf.extend_from_slice(&build_empty_model_info(TextEncoding::Utf8));
7001 buf.extend_from_slice(&build_empty_vertex_section());
7002 buf.extend_from_slice(&build_empty_face_section());
7003 buf.extend_from_slice(&build_empty_texture_section());
7004 buf.extend_from_slice(&build_empty_material_section());
7005 buf.extend_from_slice(&build_bone_section_header(1));
7006 buf.extend_from_slice(&build_bone_name_bytes("NilAppend"));
7007 buf.extend_from_slice(&build_bone_name_bytes(""));
7008 buf.extend_from_slice(&[0.0f32, 0.0, 0.0].map(f32::to_le_bytes).concat());
7009 buf.extend_from_slice(&(-1i16).to_le_bytes());
7010 buf.extend_from_slice(&0i32.to_le_bytes());
7011 buf.extend_from_slice(&BONE_FLAG_APPEND_ROTATE.to_le_bytes());
7012 buf.extend_from_slice(&[0.0f32, 0.0, 0.0].map(f32::to_le_bytes).concat());
7014 buf.extend_from_slice(&(-1i16).to_le_bytes());
7015 buf.extend_from_slice(&0.5f32.to_le_bytes());
7016 buf.extend_from_slice(&0i32.to_le_bytes());
7017
7018 let imported = import_pmx_runtime(&buf).expect("nil append parent is tolerated");
7019 assert!(imported.model.append_transforms().is_empty());
7020 }
7021
7022 #[test]
7023 fn non_finite_append_ratio_is_sanitized_to_no_append() {
7024 for ratio in [f32::NAN, f32::INFINITY, f32::NEG_INFINITY] {
7025 let mut buf = Vec::new();
7026 buf.extend_from_slice(&build_small_pmx_header_bytes(2, TextEncoding::Utf8));
7027 buf.extend_from_slice(&build_empty_model_info(TextEncoding::Utf8));
7028 buf.extend_from_slice(&build_empty_vertex_section());
7029 buf.extend_from_slice(&build_empty_face_section());
7030 buf.extend_from_slice(&build_empty_texture_section());
7031 buf.extend_from_slice(&build_empty_material_section());
7032 buf.extend_from_slice(&build_bone_section_header(1));
7033 buf.extend_from_slice(&build_bone_name_bytes("NonFiniteAppend"));
7034 buf.extend_from_slice(&build_bone_name_bytes(""));
7035 buf.extend_from_slice(&[0.0f32, 0.0, 0.0].map(f32::to_le_bytes).concat());
7036 buf.extend_from_slice(&(-1i16).to_le_bytes());
7037 buf.extend_from_slice(&0i32.to_le_bytes());
7038 buf.extend_from_slice(&BONE_FLAG_APPEND_ROTATE.to_le_bytes());
7039 buf.extend_from_slice(&[0.0f32, 0.0, 0.0].map(f32::to_le_bytes).concat());
7040 buf.extend_from_slice(&0i16.to_le_bytes());
7042 buf.extend_from_slice(&ratio.to_le_bytes());
7043 buf.extend_from_slice(&0i32.to_le_bytes());
7044
7045 let imported = import_pmx_runtime(&buf).expect("non-finite append ratio is tolerated");
7046 assert!(imported.model.append_transforms().is_empty());
7047 }
7048 }
7049
7050 #[test]
7051 fn pmx_fixed_axis_constrains_ik_end_to_end() {
7052 let mut buf = Vec::new();
7053 buf.extend_from_slice(&build_small_pmx_header_bytes(2, TextEncoding::Utf8));
7054 buf.extend_from_slice(&build_empty_model_info(TextEncoding::Utf8));
7055 buf.extend_from_slice(&build_empty_vertex_section());
7056 buf.extend_from_slice(&build_empty_face_section());
7057 buf.extend_from_slice(&build_empty_texture_section());
7058 buf.extend_from_slice(&build_empty_material_section());
7059 buf.extend_from_slice(&build_bone_section_header(3));
7060
7061 buf.extend_from_slice(&build_bone_name_bytes("link"));
7064 buf.extend_from_slice(&build_bone_name_bytes(""));
7065 buf.extend_from_slice(&[0.0f32, 0.0, 0.0].map(f32::to_le_bytes).concat());
7066 buf.extend_from_slice(&(-1i16).to_le_bytes());
7067 buf.extend_from_slice(&0i32.to_le_bytes());
7068 buf.extend_from_slice(&BONE_FLAG_FIXED_AXIS.to_le_bytes());
7069 buf.extend_from_slice(&[0.0f32, 0.0, 0.0].map(f32::to_le_bytes).concat());
7070 buf.extend_from_slice(&[0.0f32, 0.0, 1.0].map(f32::to_le_bytes).concat());
7071
7072 buf.extend_from_slice(&build_bone_name_bytes("tip"));
7073 buf.extend_from_slice(&build_bone_name_bytes(""));
7074 buf.extend_from_slice(&[0.0f32, 1.0, 0.0].map(f32::to_le_bytes).concat());
7075 buf.extend_from_slice(&0i16.to_le_bytes());
7076 buf.extend_from_slice(&0i32.to_le_bytes());
7077 buf.extend_from_slice(&0u16.to_le_bytes());
7078 buf.extend_from_slice(&[0.0f32, 0.0, 0.0].map(f32::to_le_bytes).concat());
7079
7080 buf.extend_from_slice(&build_bone_name_bytes("ik"));
7081 buf.extend_from_slice(&build_bone_name_bytes(""));
7082 buf.extend_from_slice(&[0.0f32, 0.0, 1.0].map(f32::to_le_bytes).concat());
7083 buf.extend_from_slice(&(-1i16).to_le_bytes());
7084 buf.extend_from_slice(&0i32.to_le_bytes());
7085 buf.extend_from_slice(&(BONE_FLAG_TAIL_INDEX | BONE_FLAG_IK).to_le_bytes());
7086 buf.extend_from_slice(&1i16.to_le_bytes());
7087 buf.extend_from_slice(&1i16.to_le_bytes());
7088 buf.extend_from_slice(&16i32.to_le_bytes());
7089 buf.extend_from_slice(&std::f32::consts::PI.to_le_bytes());
7090 buf.extend_from_slice(&1i32.to_le_bytes());
7091 buf.extend_from_slice(&0i16.to_le_bytes());
7092 buf.push(0);
7093 buf.extend_from_slice(&0i32.to_le_bytes());
7094
7095 let imported = import_pmx_runtime(&buf).expect("fixed-axis IK PMX must import");
7096 let mut runtime = RuntimeInstance::new(Arc::new(imported.model));
7097 runtime.evaluate_current_pose();
7098
7099 let tip = Vec3A::from_vec4(runtime.world_matrices()[1].w_axis);
7100 assert!((tip - Vec3A::Y).length() < 1.0e-3, "tip={tip:?}");
7101 let rotation = runtime.pose().local_rotation(BoneIndex(0));
7102 assert!(rotation.x.abs() < 1.0e-4 && rotation.y.abs() < 1.0e-4);
7103 assert!(rotation.z.abs() < 1.0e-4, "rotation={rotation:?}");
7104 }
7105
7106 #[test]
7107 fn pmx_local_axis_rotates_ik_limit_frame() {
7108 let with_local_axis = import_pmx_runtime(&build_local_axis_limit_pmx(true)).unwrap();
7109 let without_local_axis = import_pmx_runtime(&build_local_axis_limit_pmx(false)).unwrap();
7110
7111 let mut local_runtime = RuntimeInstance::new(Arc::new(with_local_axis.model));
7112 local_runtime.evaluate_current_pose();
7113 let local_tip = Vec3A::from_vec4(local_runtime.world_matrices()[1].w_axis);
7114 assert!(
7115 (local_tip - Vec3A::Z).length() < 1.0e-3,
7116 "local-axis limit should reach +Z; tip={local_tip:?}"
7117 );
7118
7119 let mut unit_runtime = RuntimeInstance::new(Arc::new(without_local_axis.model));
7120 unit_runtime.evaluate_current_pose();
7121 let unit_tip = Vec3A::from_vec4(unit_runtime.world_matrices()[1].w_axis);
7122 assert!(
7123 (unit_tip - Vec3A::Z).length() > 0.2,
7124 "unit XYZ limit must not use the PMX local-axis frame; tip={unit_tip:?}"
7125 );
7126 }
7127
7128 #[test]
7129 fn non_positive_ik_loop_count_is_sanitized_to_no_solver() {
7130 for loop_count in [0, -1] {
7131 let imported = import_pmx_runtime(&build_local_axis_limit_pmx_with_loop_count(
7132 false, loop_count,
7133 ))
7134 .expect("malformed optional IK is tolerated");
7135 assert_eq!(imported.model.ik_count(), 0);
7136 }
7137 }
7138
7139 #[test]
7140 fn nil_ik_target_is_sanitized_to_no_solver() {
7141 let imported = import_pmx_runtime(&build_local_axis_limit_pmx_with_ik_indices(
7142 false, 16, -1, 0,
7143 ))
7144 .expect("nil optional IK target is tolerated");
7145 assert_eq!(imported.model.ik_count(), 0);
7146 }
7147
7148 #[test]
7149 fn nil_ik_link_preserves_legacy_bone_zero_fallback() {
7150 let imported = import_pmx_runtime(&build_local_axis_limit_pmx_with_ik_indices(
7151 false, 16, 1, -1,
7152 ))
7153 .expect("nil optional IK link is tolerated");
7154 assert_eq!(imported.model.ik_count(), 1);
7155 assert_eq!(imported.model.ik_solvers()[0].links[0].bone, BoneIndex(0));
7156 }
7157
7158 #[test]
7159 fn invalid_ik_limit_angle_is_sanitized_to_no_solver() {
7160 for limit_angle in [f32::NAN, f32::INFINITY, f32::NEG_INFINITY, -1.0] {
7161 let imported = import_pmx_runtime(&build_local_axis_limit_pmx_with_metadata(
7162 false,
7163 16,
7164 1,
7165 0,
7166 limit_angle,
7167 [-std::f32::consts::FRAC_PI_2, 0.0, 0.0],
7168 [0.0, 0.0, 0.0],
7169 ))
7170 .expect("invalid optional IK limit angle is tolerated");
7171 assert_eq!(imported.model.ik_count(), 0);
7172 }
7173 }
7174
7175 #[test]
7176 fn invalid_ik_angle_limit_is_dropped_but_solver_is_retained() {
7177 let cases = [
7178 ([f32::NAN, 0.0, 0.0], [1.0, 1.0, 1.0]),
7179 ([1.0, 0.0, 0.0], [0.0, 1.0, 1.0]),
7180 ];
7181 for (min, max) in cases {
7182 let imported = import_pmx_runtime(&build_local_axis_limit_pmx_with_metadata(
7183 false,
7184 16,
7185 1,
7186 0,
7187 std::f32::consts::PI,
7188 min,
7189 max,
7190 ))
7191 .expect("invalid optional IK angle limit is tolerated");
7192 assert_eq!(imported.model.ik_count(), 1);
7193 assert!(
7194 imported.model.ik_solvers()[0].links[0]
7195 .angle_limit
7196 .is_none()
7197 );
7198 }
7199 }
7200
7201 #[test]
7202 fn runtime_import_rejects_non_finite_derived_local_rest_position() {
7203 let mut buf = Vec::new();
7204 buf.extend_from_slice(&build_small_pmx_header_bytes(2, TextEncoding::Utf8));
7205 buf.extend_from_slice(&build_empty_model_info(TextEncoding::Utf8));
7206 buf.extend_from_slice(&build_empty_vertex_section());
7207 buf.extend_from_slice(&build_empty_face_section());
7208 buf.extend_from_slice(&build_empty_texture_section());
7209 buf.extend_from_slice(&build_empty_material_section());
7210 buf.extend_from_slice(&build_bone_section_header(2));
7211
7212 for (name, position, parent) in [("Parent", f32::MAX, -1i16), ("Child", -f32::MAX, 0i16)] {
7213 buf.extend_from_slice(&build_bone_name_bytes(name));
7214 buf.extend_from_slice(&build_bone_name_bytes(""));
7215 buf.extend_from_slice(&[position, 0.0, 0.0].map(f32::to_le_bytes).concat());
7216 buf.extend_from_slice(&parent.to_le_bytes());
7217 buf.extend_from_slice(&0i32.to_le_bytes());
7218 buf.extend_from_slice(&0u16.to_le_bytes());
7219 buf.extend_from_slice(&[0.0f32, 0.0, 0.0].map(f32::to_le_bytes).concat());
7220 }
7221 buf.extend_from_slice(&0i32.to_le_bytes());
7222
7223 let result = import_pmx_runtime(&buf);
7224 assert!(matches!(
7225 result,
7226 Err(ImportError::ModelBuildFailed(
7227 mmd_anim_runtime::ModelBuildError::InvalidRuntimeDescriptor { path, .. }
7228 )) if path == "bones[1].rest_position"
7229 ));
7230 }
7231
7232 fn build_local_axis_limit_pmx(has_local_axis: bool) -> Vec<u8> {
7233 build_local_axis_limit_pmx_with_loop_count(has_local_axis, 16)
7234 }
7235
7236 fn build_local_axis_limit_pmx_with_loop_count(
7237 has_local_axis: bool,
7238 ik_loop_count: i32,
7239 ) -> Vec<u8> {
7240 build_local_axis_limit_pmx_with_ik_indices(has_local_axis, ik_loop_count, 1, 0)
7241 }
7242
7243 fn build_local_axis_limit_pmx_with_ik_indices(
7244 has_local_axis: bool,
7245 ik_loop_count: i32,
7246 ik_target: i16,
7247 ik_link: i16,
7248 ) -> Vec<u8> {
7249 build_local_axis_limit_pmx_with_metadata(
7250 has_local_axis,
7251 ik_loop_count,
7252 ik_target,
7253 ik_link,
7254 std::f32::consts::PI,
7255 [-std::f32::consts::FRAC_PI_2, 0.0, 0.0],
7256 [0.0, 0.0, 0.0],
7257 )
7258 }
7259
7260 fn build_local_axis_limit_pmx_with_metadata(
7261 has_local_axis: bool,
7262 ik_loop_count: i32,
7263 ik_target: i16,
7264 ik_link: i16,
7265 ik_limit_angle: f32,
7266 angle_limit_min: [f32; 3],
7267 angle_limit_max: [f32; 3],
7268 ) -> Vec<u8> {
7269 let mut buf = Vec::new();
7270 buf.extend_from_slice(&build_small_pmx_header_bytes(2, TextEncoding::Utf8));
7271 buf.extend_from_slice(&build_empty_model_info(TextEncoding::Utf8));
7272 buf.extend_from_slice(&build_empty_vertex_section());
7273 buf.extend_from_slice(&build_empty_face_section());
7274 buf.extend_from_slice(&build_empty_texture_section());
7275 buf.extend_from_slice(&build_empty_material_section());
7276 buf.extend_from_slice(&build_bone_section_header(3));
7277
7278 buf.extend_from_slice(&build_bone_name_bytes("link"));
7279 buf.extend_from_slice(&build_bone_name_bytes(""));
7280 buf.extend_from_slice(&[0.0f32, 0.0, 0.0].map(f32::to_le_bytes).concat());
7281 buf.extend_from_slice(&(-1i16).to_le_bytes());
7282 buf.extend_from_slice(&0i32.to_le_bytes());
7283 buf.extend_from_slice(
7284 &(if has_local_axis {
7285 BONE_FLAG_LOCAL_AXIS
7286 } else {
7287 0
7288 })
7289 .to_le_bytes(),
7290 );
7291 buf.extend_from_slice(&[0.0f32, 0.0, 0.0].map(f32::to_le_bytes).concat());
7292 if has_local_axis {
7293 buf.extend_from_slice(&[0.0f32, 1.0, 0.0].map(f32::to_le_bytes).concat());
7295 buf.extend_from_slice(&[0.0f32, 0.0, 1.0].map(f32::to_le_bytes).concat());
7296 }
7297
7298 buf.extend_from_slice(&build_bone_name_bytes("tip"));
7299 buf.extend_from_slice(&build_bone_name_bytes(""));
7300 buf.extend_from_slice(&[1.0f32, 0.0, 0.0].map(f32::to_le_bytes).concat());
7301 buf.extend_from_slice(&0i16.to_le_bytes());
7302 buf.extend_from_slice(&0i32.to_le_bytes());
7303 buf.extend_from_slice(&0u16.to_le_bytes());
7304 buf.extend_from_slice(&[0.0f32, 0.0, 0.0].map(f32::to_le_bytes).concat());
7305
7306 buf.extend_from_slice(&build_bone_name_bytes("ik"));
7307 buf.extend_from_slice(&build_bone_name_bytes(""));
7308 buf.extend_from_slice(&[0.0f32, 0.0, 1.0].map(f32::to_le_bytes).concat());
7309 buf.extend_from_slice(&(-1i16).to_le_bytes());
7310 buf.extend_from_slice(&0i32.to_le_bytes());
7311 buf.extend_from_slice(&(BONE_FLAG_TAIL_INDEX | BONE_FLAG_IK).to_le_bytes());
7312 buf.extend_from_slice(&1i16.to_le_bytes());
7313 buf.extend_from_slice(&ik_target.to_le_bytes());
7314 buf.extend_from_slice(&ik_loop_count.to_le_bytes());
7315 buf.extend_from_slice(&ik_limit_angle.to_le_bytes());
7316 buf.extend_from_slice(&1i32.to_le_bytes());
7317 buf.extend_from_slice(&ik_link.to_le_bytes());
7318 buf.push(1);
7319 for value in angle_limit_min {
7320 buf.extend_from_slice(&value.to_le_bytes());
7321 }
7322 for value in angle_limit_max {
7323 buf.extend_from_slice(&value.to_le_bytes());
7324 }
7325 buf.extend_from_slice(&0i32.to_le_bytes());
7326 buf
7327 }
7328
7329 #[test]
7330 fn child_inverse_bind_uses_absolute_not_local_position() {
7331 let mut buf = Vec::new();
7332 buf.extend_from_slice(&build_small_pmx_header_bytes(2, TextEncoding::Utf8));
7333 buf.extend_from_slice(&build_empty_model_info(TextEncoding::Utf8));
7334 buf.extend_from_slice(&build_empty_vertex_section());
7335 buf.extend_from_slice(&build_empty_face_section());
7336 buf.extend_from_slice(&build_empty_texture_section());
7337 buf.extend_from_slice(&build_empty_material_section());
7338 buf.extend_from_slice(&build_bone_section_header(2));
7339
7340 buf.extend_from_slice(&build_bone_name_bytes("Root"));
7341 buf.extend_from_slice(&build_bone_name_bytes(""));
7342 buf.extend_from_slice(&1.0f32.to_le_bytes());
7343 buf.extend_from_slice(&0.0f32.to_le_bytes());
7344 buf.extend_from_slice(&0.0f32.to_le_bytes());
7345 buf.extend_from_slice(&(-1i16).to_le_bytes());
7346 buf.extend_from_slice(&0i32.to_le_bytes());
7347 buf.extend_from_slice(&0u16.to_le_bytes());
7348 buf.extend_from_slice(&0.0f32.to_le_bytes());
7349 buf.extend_from_slice(&0.0f32.to_le_bytes());
7350 buf.extend_from_slice(&0.0f32.to_le_bytes());
7351
7352 buf.extend_from_slice(&build_bone_name_bytes("Child"));
7353 buf.extend_from_slice(&build_bone_name_bytes(""));
7354 buf.extend_from_slice(&5.0f32.to_le_bytes());
7355 buf.extend_from_slice(&0.0f32.to_le_bytes());
7356 buf.extend_from_slice(&0.0f32.to_le_bytes());
7357 buf.extend_from_slice(&0i16.to_le_bytes());
7358 buf.extend_from_slice(&0i32.to_le_bytes());
7359 buf.extend_from_slice(&0u16.to_le_bytes());
7360 buf.extend_from_slice(&0.0f32.to_le_bytes());
7361 buf.extend_from_slice(&0.0f32.to_le_bytes());
7362 buf.extend_from_slice(&0.0f32.to_le_bytes());
7363
7364 let result = import_pmx_model(&buf).unwrap();
7365 assert_eq!(result.bones[1].parent, Some(BoneIndex(0)));
7366 assert_eq!(result.bones[1].rest_position, Vec3A::new(4.0, 0.0, 0.0));
7367 let ibm = result.bones[1].inverse_bind_matrix;
7368 let rest_world = Mat4::from_translation(Vec3::new(5.0, 0.0, 0.0));
7369 assert_eq!(rest_world * ibm, Mat4::IDENTITY);
7370 }
7371
7372 #[test]
7373 fn runtime_rest_pose_skinning_matrices_are_identity() {
7374 let mut buf = Vec::new();
7375 buf.extend_from_slice(&build_small_pmx_header_bytes(2, TextEncoding::Utf8));
7376 buf.extend_from_slice(&build_empty_model_info(TextEncoding::Utf8));
7377 buf.extend_from_slice(&build_empty_vertex_section());
7378 buf.extend_from_slice(&build_empty_face_section());
7379 buf.extend_from_slice(&build_empty_texture_section());
7380 buf.extend_from_slice(&build_empty_material_section());
7381 buf.extend_from_slice(&build_bone_section_header(2));
7382
7383 buf.extend_from_slice(&build_bone_name_bytes("Parent"));
7384 buf.extend_from_slice(&build_bone_name_bytes(""));
7385 buf.extend_from_slice(&1.0f32.to_le_bytes());
7386 buf.extend_from_slice(&2.0f32.to_le_bytes());
7387 buf.extend_from_slice(&0.0f32.to_le_bytes());
7388 buf.extend_from_slice(&(-1i16).to_le_bytes());
7389 buf.extend_from_slice(&0i32.to_le_bytes());
7390 buf.extend_from_slice(&0u16.to_le_bytes());
7391 buf.extend_from_slice(&0.0f32.to_le_bytes());
7392 buf.extend_from_slice(&0.0f32.to_le_bytes());
7393 buf.extend_from_slice(&0.0f32.to_le_bytes());
7394
7395 buf.extend_from_slice(&build_bone_name_bytes("Child"));
7396 buf.extend_from_slice(&build_bone_name_bytes(""));
7397 buf.extend_from_slice(&1.0f32.to_le_bytes());
7398 buf.extend_from_slice(&3.0f32.to_le_bytes());
7399 buf.extend_from_slice(&0.0f32.to_le_bytes());
7400 buf.extend_from_slice(&0i16.to_le_bytes());
7401 buf.extend_from_slice(&0i32.to_le_bytes());
7402 buf.extend_from_slice(&0u16.to_le_bytes());
7403 buf.extend_from_slice(&0.0f32.to_le_bytes());
7404 buf.extend_from_slice(&0.0f32.to_le_bytes());
7405 buf.extend_from_slice(&0.0f32.to_le_bytes());
7406
7407 buf.extend_from_slice(&0i32.to_le_bytes());
7408
7409 let rt = import_pmx_runtime(&buf).unwrap();
7410 let model = Arc::new(rt.model);
7411 let mut instance = RuntimeInstance::new(model);
7412 instance.evaluate_current_pose();
7413 let matrices = instance.skinning_matrices();
7414 assert_eq!(matrices.len(), 2);
7415 assert_eq!(matrices[0], Mat4::IDENTITY);
7416 assert_eq!(matrices[1], Mat4::IDENTITY);
7417 }
7418
7419 fn build_sdef_vertex_pmx() -> Vec<u8> {
7420 let mut buf = Vec::new();
7423 buf.extend_from_slice(b"PMX ");
7424 buf.extend_from_slice(&2.0f32.to_le_bytes());
7425 buf.push(8); buf.push(1); buf.push(0); buf.push(1); buf.push(1); buf.push(1); buf.push(1); buf.push(1); buf.push(1); buf.extend_from_slice(&build_empty_model_info(TextEncoding::Utf8));
7435 buf.extend_from_slice(&1i32.to_le_bytes());
7438 buf.extend_from_slice(&0.0f32.to_le_bytes()); buf.extend_from_slice(&0.0f32.to_le_bytes()); buf.extend_from_slice(&0.0f32.to_le_bytes()); buf.extend_from_slice(&0.0f32.to_le_bytes()); buf.extend_from_slice(&1.0f32.to_le_bytes()); buf.extend_from_slice(&0.0f32.to_le_bytes()); buf.extend_from_slice(&0.0f32.to_le_bytes()); buf.extend_from_slice(&0.0f32.to_le_bytes()); buf.push(3u8); buf.push(0u8); buf.push(0u8); buf.extend_from_slice(&0.25f32.to_le_bytes()); buf.extend_from_slice(&1.0f32.to_le_bytes()); buf.extend_from_slice(&2.0f32.to_le_bytes()); buf.extend_from_slice(&3.0f32.to_le_bytes()); buf.extend_from_slice(&4.0f32.to_le_bytes()); buf.extend_from_slice(&5.0f32.to_le_bytes()); buf.extend_from_slice(&6.0f32.to_le_bytes()); buf.extend_from_slice(&7.0f32.to_le_bytes()); buf.extend_from_slice(&8.0f32.to_le_bytes()); buf.extend_from_slice(&9.0f32.to_le_bytes()); buf.extend_from_slice(&1.0f32.to_le_bytes()); for _ in 0..8 {
7461 buf.extend_from_slice(&0i32.to_le_bytes()); }
7463 buf
7464 }
7465
7466 fn build_qdef_vertex_pmx() -> Vec<u8> {
7467 let mut buf = Vec::new();
7469 buf.extend_from_slice(b"PMX ");
7470 buf.extend_from_slice(&2.0f32.to_le_bytes());
7471 buf.push(8);
7472 buf.push(1); buf.push(0); buf.push(1); buf.push(1); buf.push(1); buf.push(1); buf.push(1); buf.push(1); buf.extend_from_slice(&build_empty_model_info(TextEncoding::Utf8));
7481 buf.extend_from_slice(&1i32.to_le_bytes());
7483 buf.extend_from_slice(&0.0f32.to_le_bytes()); buf.extend_from_slice(&0.0f32.to_le_bytes());
7485 buf.extend_from_slice(&0.0f32.to_le_bytes());
7486 buf.extend_from_slice(&0.0f32.to_le_bytes()); buf.extend_from_slice(&1.0f32.to_le_bytes());
7488 buf.extend_from_slice(&0.0f32.to_le_bytes());
7489 buf.extend_from_slice(&0.0f32.to_le_bytes()); buf.extend_from_slice(&0.0f32.to_le_bytes());
7491 buf.push(4u8); buf.push(0u8);
7493 buf.push(0u8);
7494 buf.push(0u8);
7495 buf.push(0u8); buf.extend_from_slice(&1.0f32.to_le_bytes()); buf.extend_from_slice(&0.0f32.to_le_bytes()); buf.extend_from_slice(&0.0f32.to_le_bytes()); buf.extend_from_slice(&0.0f32.to_le_bytes()); buf.extend_from_slice(&1.0f32.to_le_bytes()); for _ in 0..8 {
7502 buf.extend_from_slice(&0i32.to_le_bytes());
7503 }
7504 buf
7505 }
7506
7507 #[test]
7508 fn pmx_sdef_geometry_json_includes_sdef_fields() {
7509 let buf = build_sdef_vertex_pmx();
7510 let model = parse_pmx_model(&buf).unwrap();
7511 assert_eq!(
7512 model.geometry.sdef.skinning_modes,
7513 vec![PMX_SKINNING_MODE_SDEF.to_owned()]
7514 );
7515 assert_eq!(model.geometry.sdef.enabled.len(), 1);
7516 assert!((model.geometry.sdef.enabled[0] - 1.0).abs() < 1e-5);
7517 assert_eq!(model.geometry.sdef.c, vec![1.0, 2.0, 3.0]);
7518 assert_eq!(model.geometry.sdef.r0, vec![4.0, 5.0, 6.0]);
7519 assert_eq!(model.geometry.sdef.r1, vec![7.0, 8.0, 9.0]);
7520 let w = 0.25f32;
7525 let w1 = 0.75f32;
7526 let c = [1.0f32, 2.0, 3.0];
7527 let r0 = [4.0f32, 5.0, 6.0];
7528 let r1 = [7.0f32, 8.0, 9.0];
7529 for k in 0..3 {
7530 let rw = r0[k] * w + r1[k] * w1;
7531 let expected_rw0 = (c[k] * 2.0 + r0[k] - rw) * 0.5;
7532 let expected_rw1 = (c[k] * 2.0 + r1[k] - rw) * 0.5;
7533 assert!(
7534 (model.geometry.sdef.rw0[k] - expected_rw0).abs() < 1e-4,
7535 "rw0[{k}]: expected {expected_rw0}, got {}",
7536 model.geometry.sdef.rw0[k]
7537 );
7538 assert!(
7539 (model.geometry.sdef.rw1[k] - expected_rw1).abs() < 1e-4,
7540 "rw1[{k}]: expected {expected_rw1}, got {}",
7541 model.geometry.sdef.rw1[k]
7542 );
7543 }
7544 assert_eq!(model.geometry.qdef.enabled[0], 0.0);
7545 let json = serde_json::to_string(&model).unwrap();
7547 assert!(json.contains("\"sdef\""), "JSON must contain sdef key");
7548 assert!(json.contains("\"qdef\""), "JSON must contain qdef key");
7549 let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
7550 assert_eq!(parsed["geometry"]["sdef"]["enabled"][0], 1.0);
7551 }
7552
7553 #[test]
7554 fn pmx_qdef_vertex_marks_enabled() {
7555 let buf = build_qdef_vertex_pmx();
7556 let model = parse_pmx_model(&buf).unwrap();
7557 assert_eq!(
7558 model.geometry.sdef.skinning_modes,
7559 vec![PMX_SKINNING_MODE_QDEF.to_owned()]
7560 );
7561 assert_eq!(model.geometry.qdef.enabled.len(), 1);
7562 assert!((model.geometry.qdef.enabled[0] - 1.0).abs() < 1e-5);
7563 assert_eq!(model.geometry.sdef.enabled[0], 0.0);
7564 }
7565
7566 #[test]
7567 fn exports_parsed_pmx_deform_type_binary_roundtrip() {
7568 for original in [build_sdef_vertex_pmx(), build_qdef_vertex_pmx()] {
7569 let parsed = parse_pmx_model(&original).unwrap();
7570 let exported = export_pmx_model(&parsed);
7571 assert_eq!(exported, original);
7572 }
7573 }
7574}