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