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