use std::collections::HashMap;
use glam::{Mat4, Quat, Vec3A};
use serde::{Deserialize, Serialize};
use mmd_anim_runtime::{
AppendTransformInit, BoneIndex, BoneInit, BoneMorphOffset, GroupMorphOffset, IkAngleLimit,
IkLinkInit, IkSolverInit, ModelArena, MorphIndex, MorphInit, MorphOffsetSpan,
};
use crate::binary::{
ByteReader, write_f32_le as write_f32, write_f32_slice_le as write_f32_slice,
write_i32_le as write_i32, write_u16_le as write_u16,
};
use crate::error::ImportError;
use crate::sjis::encode_sjis;
type Reader<'a> = ByteReader<'a>;
mod json_f32;
const PMX_MAGIC: [u8; 4] = [0x50, 0x4D, 0x58, 0x20];
const BONE_FLAG_TAIL_INDEX: u16 = 0x0001;
const BONE_FLAG_IK: u16 = 0x0020;
const BONE_FLAG_LOCAL_APPEND: u16 = 0x0080;
const BONE_FLAG_APPEND_ROTATE: u16 = 0x0100;
const BONE_FLAG_APPEND_TRANSLATE: u16 = 0x0200;
const BONE_FLAG_FIXED_AXIS: u16 = 0x0400;
const BONE_FLAG_LOCAL_AXIS: u16 = 0x0800;
const BONE_FLAG_TRANSFORM_AFTER_PHYSICS: u16 = 0x1000;
const BONE_FLAG_EXTERNAL_PARENT: u16 = 0x2000;
const PMX_SKINNING_MODE_BDEF1: &str = "bdef1";
const PMX_SKINNING_MODE_BDEF2: &str = "bdef2";
const PMX_SKINNING_MODE_BDEF4: &str = "bdef4";
const PMX_SKINNING_MODE_SDEF: &str = "sdef";
const PMX_SKINNING_MODE_QDEF: &str = "qdef";
fn decode_utf16le_lossy(bytes: &[u8]) -> String {
let end = bytes.len().saturating_sub(bytes.len() % 2);
let units = bytes[..end]
.chunks_exact(2)
.map(|chunk| u16::from_le_bytes([chunk[0], chunk[1]]))
.take_while(|&code| code != 0);
std::char::decode_utf16(units)
.map(|item| item.unwrap_or(char::REPLACEMENT_CHARACTER))
.collect()
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TextEncoding {
Utf16Le = 0,
Utf8 = 1,
}
#[derive(Debug, Clone)]
pub struct PmxHeader {
pub version: f32,
pub encoding: TextEncoding,
pub extra_uv_count: u8,
pub vertex_index_size: u8,
pub texture_index_size: u8,
pub material_index_size: u8,
pub bone_index_size: u8,
pub morph_index_size: u8,
pub rigidbody_index_size: u8,
}
impl<'a> ByteReader<'a> {
fn read_vec2_array(&mut self) -> Result<[f32; 2], ImportError> {
Ok([self.read_f32_le()?, self.read_f32_le()?])
}
fn read_vec3_array(&mut self) -> Result<[f32; 3], ImportError> {
Ok([
self.read_f32_le()?,
self.read_f32_le()?,
self.read_f32_le()?,
])
}
fn read_vec4_array(&mut self) -> Result<[f32; 4], ImportError> {
Ok([
self.read_f32_le()?,
self.read_f32_le()?,
self.read_f32_le()?,
self.read_f32_le()?,
])
}
fn read_sized_index(&mut self, size: u8) -> Result<i32, ImportError> {
match size {
1 => Ok(self.read_u8()? as i8 as i32),
2 => Ok({
let b = self.read_bytes(2)?;
i16::from_le_bytes([b[0], b[1]]) as i32
}),
4 => self.read_i32_le(),
_ => Err(ImportError::InvalidIndexSize(size)),
}
}
fn read_index(&mut self, header: &PmxHeader) -> Result<i32, ImportError> {
self.read_sized_index(header.bone_index_size)
}
fn read_vertex_index(&mut self, size: u8) -> Result<u32, ImportError> {
match size {
1 => Ok(self.read_u8()? as u32),
2 => Ok(self.read_u16_le()? as u32),
4 => {
let raw = self.read_i32_le()?;
if raw < 0 {
Err(ImportError::SectionOverflow)
} else {
Ok(raw as u32)
}
}
_ => Err(ImportError::InvalidIndexSize(size)),
}
}
fn read_string(&mut self, encoding: TextEncoding) -> Result<String, ImportError> {
let len = self.read_i32_le()?;
if len <= 0 {
return Ok(String::new());
}
let bytes = self.read_bytes(len as usize)?;
match encoding {
TextEncoding::Utf8 => {
let end = bytes.iter().position(|&b| b == 0).unwrap_or(bytes.len());
Ok(String::from_utf8_lossy(&bytes[..end]).into_owned())
}
TextEncoding::Utf16Le => Ok(decode_utf16le_lossy(bytes)),
}
}
fn read_string_owned(
&mut self,
encoding: TextEncoding,
) -> Result<(Vec<u8>, String), ImportError> {
let len = self.read_i32_le()?;
if len <= 0 {
return Ok((Vec::new(), String::new()));
}
let bytes = self.read_bytes(len as usize)?.to_vec();
let decoded = match encoding {
TextEncoding::Utf8 => {
let end = bytes.iter().position(|&b| b == 0).unwrap_or(bytes.len());
String::from_utf8_lossy(&bytes[..end]).into_owned()
}
TextEncoding::Utf16Le => decode_utf16le_lossy(&bytes),
};
Ok((bytes, decoded))
}
fn read_bone_index(&mut self, header: &PmxHeader) -> Result<Option<BoneIndex>, ImportError> {
let raw = self.read_sized_index(header.bone_index_size)?;
if raw < 0 {
Ok(None)
} else {
Ok(Some(BoneIndex(raw as u32)))
}
}
}
pub fn read_header(data: &[u8]) -> Result<(PmxHeader, usize), ImportError> {
let mut r = Reader::new(data);
let magic = r.read_bytes(4)?;
if magic != PMX_MAGIC {
return Err(ImportError::InvalidPmxMagic);
}
let version = r.read_f32_le()?;
if !(2.0..=2.2).contains(&version) {
return Err(ImportError::UnsupportedPmxVersion(version));
}
let data_count = r.read_u8()?;
if data_count < 8 {
return Err(ImportError::InvalidEncoding(data_count));
}
let encoding_byte = r.read_u8()?;
let encoding = match encoding_byte {
0 => TextEncoding::Utf16Le,
1 => TextEncoding::Utf8,
_ => return Err(ImportError::InvalidEncoding(encoding_byte)),
};
let extra_uv_count = r.read_u8()?;
let vertex_index_size = r.read_u8()?;
let texture_index_size = r.read_u8()?;
let material_index_size = r.read_u8()?;
let bone_index_size = r.read_u8()?;
let morph_index_size = r.read_u8()?;
let rigidbody_index_size = r.read_u8()?;
for size in [
vertex_index_size,
texture_index_size,
material_index_size,
bone_index_size,
morph_index_size,
rigidbody_index_size,
] {
if !matches!(size, 1 | 2 | 4) {
return Err(ImportError::InvalidIndexSize(size));
}
}
if data_count > 8 {
r.skip((data_count - 8) as usize)?;
}
let header = PmxHeader {
version,
encoding,
extra_uv_count,
vertex_index_size,
texture_index_size,
material_index_size,
bone_index_size,
morph_index_size,
rigidbody_index_size,
};
Ok((header, r.pos))
}
pub fn skip_model_info(data: &[u8], header: &PmxHeader, pos: usize) -> Result<usize, ImportError> {
let mut r = Reader { data, pos };
let _model_name = r.read_string(header.encoding)?;
let _english_name = r.read_string(header.encoding)?;
let _comment = r.read_string(header.encoding)?;
let _english_comment = r.read_string(header.encoding)?;
Ok(r.pos)
}
pub fn skip_vertices(data: &[u8], header: &PmxHeader, pos: usize) -> Result<usize, ImportError> {
let mut r = Reader { data, pos };
let count = r.read_i32_le()?;
if count < 0 {
return Err(ImportError::SectionOverflow);
}
let count = count as usize;
for _ in 0..count {
r.skip(12)?;
r.skip(12)?;
r.skip(8)?;
r.skip(header.extra_uv_count as usize * 16)?;
let vertex_deform_type = r.read_u8()?;
let bone_indices: usize = match vertex_deform_type {
0 => 1,
1 => 2,
2 | 4 => 4,
3 => 2,
_ => return Err(ImportError::SectionOverflow),
};
for _ in 0..bone_indices {
r.read_sized_index(header.bone_index_size)?;
}
let weight_count: usize = match vertex_deform_type {
0 => 0,
1 => 1,
2 | 4 => 4,
3 => 1,
_ => return Err(ImportError::SectionOverflow),
};
r.skip(weight_count * 4)?;
if vertex_deform_type == 3 {
r.skip(36)?;
}
r.skip(4)?;
}
Ok(r.pos)
}
pub fn skip_faces(data: &[u8], vertex_index_size: u8, pos: usize) -> Result<usize, ImportError> {
let mut r = Reader { data, pos };
let count = r.read_i32_le()?;
if count < 0 {
return Err(ImportError::SectionOverflow);
}
let count = count as usize;
let bytes = count
.checked_mul(vertex_index_size as usize)
.ok_or(ImportError::SectionOverflow)?;
r.skip(bytes)?;
Ok(r.pos)
}
pub fn skip_textures(
data: &[u8],
encoding: TextEncoding,
pos: usize,
) -> Result<usize, ImportError> {
let mut r = Reader { data, pos };
let count = r.read_i32_le()?;
if count < 0 {
return Err(ImportError::SectionOverflow);
}
for _ in 0..count {
let _path = r.read_string(encoding)?;
}
Ok(r.pos)
}
pub fn skip_materials(data: &[u8], header: &PmxHeader, pos: usize) -> Result<usize, ImportError> {
let mut r = Reader { data, pos };
let count = r.read_i32_le()?;
if count < 0 {
return Err(ImportError::SectionOverflow);
}
for _ in 0..count {
let _name = r.read_string(header.encoding)?;
let _english_name = r.read_string(header.encoding)?;
r.skip(16)?;
r.skip(16)?;
r.skip(12)?;
r.skip(1)?;
r.skip(16)?;
r.skip(4)?;
let _texture = r.read_sized_index(header.texture_index_size)?;
let _sphere_tex = r.read_sized_index(header.texture_index_size)?;
r.skip(1)?;
let _toon = r.read_u8()?;
if _toon == 0 {
r.read_sized_index(header.texture_index_size)?;
} else {
r.skip(1)?;
}
let _memo = r.read_string(header.encoding)?;
let _face_count = r.read_i32_le()?;
}
Ok(r.pos)
}
pub fn skip_morphs(data: &[u8], header: &PmxHeader, pos: usize) -> Result<usize, ImportError> {
let mut r = Reader { data, pos };
let count = r.read_i32_le()?;
if count < 0 {
return Err(ImportError::SectionOverflow);
}
for _ in 0..count {
let _name = r.read_string(header.encoding)?;
let _english_name = r.read_string(header.encoding)?;
r.skip(1)?;
let _morph_type = r.read_u8()?;
let _offset_count = r.read_i32_le()?;
match _morph_type {
0 => {
for _ in 0.._offset_count {
r.read_sized_index(header.morph_index_size)?;
r.skip(4)?;
}
}
1 => {
for _ in 0.._offset_count {
r.read_sized_index(header.vertex_index_size)?;
r.skip(12)?;
}
}
2 => {
for _ in 0.._offset_count {
r.read_sized_index(header.bone_index_size)?;
r.skip(12)?;
r.skip(16)?;
}
}
3..=7 => {
for _ in 0.._offset_count {
r.read_sized_index(header.vertex_index_size)?;
r.skip(16)?;
}
}
8 => {
for _ in 0.._offset_count {
r.read_sized_index(header.material_index_size)?;
r.skip(1)?;
r.skip(16)?;
r.skip(12)?;
r.skip(4)?;
r.skip(12)?;
r.skip(16)?;
r.skip(4)?;
r.skip(16)?;
r.skip(16)?;
r.skip(16)?;
}
}
9 => {
for _ in 0.._offset_count {
r.read_sized_index(header.rigidbody_index_size)?;
r.skip(1)?;
r.skip(12)?;
r.skip(12)?;
}
}
_ => return Err(ImportError::SectionOverflow),
}
}
Ok(r.pos)
}
#[derive(Debug, Clone)]
pub struct PmxMorphNames {
pub name_bytes: Vec<Vec<u8>>,
pub names: Vec<String>,
}
pub fn read_morph_names(
data: &[u8],
header: &PmxHeader,
pos: usize,
) -> Result<(PmxMorphNames, usize), ImportError> {
let mut r = Reader { data, pos };
let count = r.read_i32_le()?;
if count < 0 {
return Err(ImportError::SectionOverflow);
}
let count = count as usize;
r.require_record_bytes(count, 1)?;
let mut name_bytes = Vec::with_capacity(count);
let mut names = Vec::with_capacity(count);
for _ in 0..count {
let (raw, name) = r.read_string_owned(header.encoding)?;
let _english_name = r.read_string(header.encoding)?;
name_bytes.push(raw);
names.push(name);
r.skip(1)?;
let morph_type = r.read_u8()?;
let offset_count = r.read_i32_le()?;
if offset_count < 0 {
return Err(ImportError::SectionOverflow);
}
let offset_count = offset_count as usize;
let per_offset = match morph_type {
0 => header.morph_index_size as usize + 4,
1 => header.vertex_index_size as usize + 12,
2 => header.bone_index_size as usize + 12 + 16,
3..=7 => header.vertex_index_size as usize + 16,
8 => header.material_index_size as usize + 1 + 16 + 12 + 4 + 12 + 16 + 4 + 16 + 16 + 16,
9 => header.rigidbody_index_size as usize + 1 + 12 + 12,
_ => return Err(ImportError::SectionOverflow),
};
r.skip(
offset_count
.checked_mul(per_offset)
.ok_or(ImportError::SectionOverflow)?,
)?;
}
Ok((PmxMorphNames { name_bytes, names }, r.pos))
}
pub fn read_morph_offsets(
data: &[u8],
header: &PmxHeader,
pos: usize,
) -> Result<(PmxMorphNames, MorphInit, usize), ImportError> {
let mut r = Reader { data, pos };
let count = r.read_i32_le()?;
if count < 0 {
return Err(ImportError::SectionOverflow);
}
let count = count as usize;
r.require_record_bytes(count, 1)?;
let mut name_bytes = Vec::with_capacity(count);
let mut names = Vec::with_capacity(count);
let mut bone_offsets_all: Vec<BoneMorphOffset> = Vec::new();
let mut bone_spans: Vec<MorphOffsetSpan> = Vec::with_capacity(count);
let mut group_offsets_all: Vec<GroupMorphOffset> = Vec::new();
let mut group_spans: Vec<MorphOffsetSpan> = Vec::with_capacity(count);
for _ in 0..count {
let (raw, name) = r.read_string_owned(header.encoding)?;
let _english_name = r.read_string(header.encoding)?;
name_bytes.push(raw);
names.push(name);
r.skip(1)?;
let morph_type = r.read_u8()?;
let offset_count = r.read_i32_le()?;
if offset_count < 0 {
return Err(ImportError::SectionOverflow);
}
let offset_count = offset_count as usize;
match morph_type {
0 => {
let start = group_offsets_all.len() as u32;
for _ in 0..offset_count {
let child_raw = r.read_sized_index(header.morph_index_size)?;
if child_raw < 0 {
return Err(ImportError::SectionOverflow);
}
let ratio = r.read_f32_le()?;
group_offsets_all.push(GroupMorphOffset {
child_morph: MorphIndex(child_raw as u32),
ratio,
});
}
group_spans.push(MorphOffsetSpan {
start,
count: offset_count as u32,
});
bone_spans.push(MorphOffsetSpan::default());
}
2 => {
let start = bone_offsets_all.len() as u32;
for _ in 0..offset_count {
let bone_raw = r.read_sized_index(header.bone_index_size)?;
if bone_raw < 0 {
return Err(ImportError::SectionOverflow);
}
let pos = r.read_vec3()?;
let qx = r.read_f32_le()?;
let qy = r.read_f32_le()?;
let qz = r.read_f32_le()?;
let qw = r.read_f32_le()?;
bone_offsets_all.push(BoneMorphOffset {
target_bone: BoneIndex(bone_raw as u32),
position_offset: pos,
rotation_offset: Quat::from_xyzw(qx, qy, qz, qw),
});
}
bone_spans.push(MorphOffsetSpan {
start,
count: offset_count as u32,
});
group_spans.push(MorphOffsetSpan::default());
}
_ => {
let per_offset = match morph_type {
1 => header.vertex_index_size as usize + 12,
3..=7 => header.vertex_index_size as usize + 16,
8 => {
header.material_index_size as usize
+ 1
+ 16
+ 12
+ 4
+ 12
+ 16
+ 4
+ 16
+ 16
+ 16
}
9 => header.rigidbody_index_size as usize + 1 + 12 + 12,
_ => return Err(ImportError::SectionOverflow),
};
r.skip(
offset_count
.checked_mul(per_offset)
.ok_or(ImportError::SectionOverflow)?,
)?;
bone_spans.push(MorphOffsetSpan::default());
group_spans.push(MorphOffsetSpan::default());
}
}
}
Ok((
PmxMorphNames { name_bytes, names },
MorphInit {
morph_count: count as u32,
vertex_spans: vec![MorphOffsetSpan::default(); count],
bone_offsets: bone_offsets_all,
bone_spans,
group_offsets: group_offsets_all,
group_spans,
..MorphInit::default()
},
r.pos,
))
}
#[derive(Debug, Clone)]
pub struct PmxBoneImport {
pub bones: Vec<BoneInit>,
pub ik_solvers: Vec<IkSolverInit>,
pub append_transforms: Vec<AppendTransformInit>,
pub bone_name_bytes: Vec<Vec<u8>>,
pub bone_names: Vec<String>,
}
pub fn read_bones(
data: &[u8],
header: &PmxHeader,
pos: usize,
) -> Result<(PmxBoneImport, usize), ImportError> {
let mut r = Reader { data, pos };
let count = r.read_i32_le()?;
if count < 0 {
return Err(ImportError::SectionOverflow);
}
let count = count as usize;
r.require_record_bytes(count, 1)?;
let mut bones = Vec::with_capacity(count);
let mut ik_solvers = Vec::new();
let mut append_transforms = Vec::new();
let mut bone_name_bytes = Vec::with_capacity(count);
let mut bone_names = Vec::with_capacity(count);
let mut absolute_positions: Vec<Vec3A> = Vec::with_capacity(count);
for _bone_i in 0..count {
let (name_bytes, name) = r.read_string_owned(header.encoding)?;
let _english_name = r.read_string(header.encoding)?;
let position = r.read_vec3()?;
absolute_positions.push(position);
let parent = r.read_bone_index(header)?;
let transform_order = r.read_i32_le()?;
let flags = r.read_u16_le()?;
if flags & BONE_FLAG_TAIL_INDEX != 0 {
r.read_index(header)?;
} else {
r.read_vec3()?;
}
let has_append =
flags & BONE_FLAG_APPEND_ROTATE != 0 || flags & BONE_FLAG_APPEND_TRANSLATE != 0;
if has_append {
let append_parent = r.read_bone_index(header)?;
let append_ratio = r.read_f32_le()?;
if let Some(source) = append_parent {
let target_idx = bones.len();
append_transforms.push(
AppendTransformInit::new(BoneIndex(target_idx as u32), source, append_ratio)
.with_rotation_if(flags & BONE_FLAG_APPEND_ROTATE != 0)
.with_translation_if(flags & BONE_FLAG_APPEND_TRANSLATE != 0)
.with_local_if(flags & BONE_FLAG_LOCAL_APPEND != 0),
);
}
}
let fixed_axis = if flags & BONE_FLAG_FIXED_AXIS != 0 {
Some(r.read_vec3()?)
} else {
None
};
if flags & BONE_FLAG_LOCAL_AXIS != 0 {
r.read_vec3()?;
r.read_vec3()?;
}
if flags & BONE_FLAG_EXTERNAL_PARENT != 0 {
r.read_i32_le()?;
}
if flags & BONE_FLAG_IK != 0 {
let ik_target = r.read_bone_index(header)?;
let ik_loop_count = r.read_i32_le()?;
let ik_limit_angle = r.read_f32_le()?;
let link_count = r.read_i32_le()?;
if link_count < 0 {
return Err(ImportError::SectionOverflow);
}
r.require_record_bytes(link_count as usize, header.bone_index_size as usize + 1)?;
let mut links = Vec::with_capacity(link_count as usize);
for _ in 0..link_count {
let link_bone = r.read_bone_index(header)?.unwrap_or(BoneIndex(0));
let has_limit = r.read_u8()?;
let angle_limit = if has_limit != 0 {
let min = r.read_vec3()?;
let max = r.read_vec3()?;
Some(IkAngleLimit::new(min, max))
} else {
None
};
links.push(match angle_limit {
Some(lim) => IkLinkInit::new(link_bone).with_angle_limit(lim),
None => IkLinkInit::new(link_bone),
});
}
let ik_bone = BoneIndex(bones.len() as u32);
if let Some(target) = ik_target {
ik_solvers.push(IkSolverInit {
ik_bone,
target_bone: target,
links,
iteration_count: ik_loop_count as u32,
limit_angle: ik_limit_angle,
});
}
}
bone_name_bytes.push(name_bytes);
bone_names.push(name);
bones.push(BoneInit {
parent,
rest_position: position,
inverse_bind_matrix: Mat4::from_translation((-position).into()),
transform_order,
transform_after_physics: flags & BONE_FLAG_TRANSFORM_AFTER_PHYSICS != 0,
fixed_axis,
enforce_fixed_axis: false,
});
}
for bone in bones.iter_mut() {
if let Some(parent_idx) = bone.parent
&& let Some(parent_abs) = absolute_positions.get(parent_idx.as_usize())
{
bone.rest_position -= parent_abs;
}
}
Ok((
PmxBoneImport {
bones,
ik_solvers,
append_transforms,
bone_name_bytes,
bone_names,
},
r.pos,
))
}
pub fn import_pmx_model(data: &[u8]) -> Result<PmxBoneImport, ImportError> {
let (header, pos) = read_header(data)?;
let pos = skip_model_info(data, &header, pos)?;
let pos = skip_vertices(data, &header, pos)?;
let pos = skip_faces(data, header.vertex_index_size, pos)?;
let pos = skip_textures(data, header.encoding, pos)?;
let pos = skip_materials(data, &header, pos)?;
let (bones, _pos) = read_bones(data, &header, pos)?;
Ok(bones)
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PmxParsedModel {
pub metadata: PmxParsedMetadata,
pub geometry: PmxParsedGeometry,
pub materials: Vec<PmxParsedMaterial>,
pub skeleton: PmxParsedSkeleton,
pub morphs: Vec<PmxParsedMorph>,
pub display_frames: Vec<PmxParsedDisplayFrame>,
pub rigid_bodies: Vec<PmxParsedRigidBody>,
pub joints: Vec<PmxParsedJoint>,
pub soft_bodies: Vec<PmxParsedSoftBody>,
pub diagnostics: Vec<PmxParserDiagnostic>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PmxParsedMetadata {
#[serde(default = "default_pmx_format")]
pub format: String,
#[serde(with = "json_f32")]
pub version: f32,
pub encoding: String,
pub name: String,
pub english_name: String,
pub comment: String,
pub english_comment: String,
pub counts: PmxParsedCounts,
pub index_sizes: PmxParsedIndexSizes,
pub additional_uv_count: u8,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PmxParsedCounts {
pub vertices: usize,
pub faces: usize,
pub materials: usize,
pub bones: usize,
pub morphs: usize,
pub display_frames: usize,
pub rigid_bodies: usize,
pub joints: usize,
pub soft_bodies: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PmxParsedIndexSizes {
pub vertex: u8,
pub texture: u8,
pub material: u8,
pub bone: u8,
pub morph: u8,
#[serde(rename = "rigidBody")]
pub rigid_body: u8,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct PmxParsedSdef {
#[serde(default)]
pub skinning_modes: Vec<String>,
#[serde(with = "json_f32::vec_f32")]
pub enabled: Vec<f32>,
#[serde(with = "json_f32::vec_f32")]
pub c: Vec<f32>,
#[serde(with = "json_f32::vec_f32")]
pub r0: Vec<f32>,
#[serde(with = "json_f32::vec_f32")]
pub r1: Vec<f32>,
#[serde(with = "json_f32::vec_f32")]
pub rw0: Vec<f32>,
#[serde(with = "json_f32::vec_f32")]
pub rw1: Vec<f32>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct PmxParsedQdef {
#[serde(with = "json_f32::vec_f32")]
pub enabled: Vec<f32>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PmxParsedGeometry {
#[serde(with = "json_f32::vec_f32")]
pub positions: Vec<f32>,
#[serde(with = "json_f32::vec_f32")]
pub normals: Vec<f32>,
#[serde(with = "json_f32::vec_f32")]
pub uvs: Vec<f32>,
#[serde(with = "json_f32::nested_vec")]
pub additional_uvs: Vec<Vec<f32>>,
pub indices: Vec<u32>,
pub skin_indices: Vec<u32>,
#[serde(with = "json_f32::vec_f32")]
pub skin_weights: Vec<f32>,
#[serde(with = "json_f32::vec_f32")]
pub edge_scale: Vec<f32>,
pub material_groups: Vec<PmxParsedMaterialGroup>,
pub sdef: PmxParsedSdef,
pub qdef: PmxParsedQdef,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PmxParsedMaterialGroup {
pub start: usize,
pub count: usize,
pub material_index: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PmxParsedMaterial {
pub name: String,
pub english_name: String,
pub texture_path: String,
pub sphere_texture_path: String,
pub sphere_mode: String,
pub toon_texture_path: String,
pub shared_toon_index: Option<u8>,
#[serde(with = "json_f32::array4")]
pub diffuse: [f32; 4],
#[serde(with = "json_f32::array3")]
pub specular: [f32; 3],
#[serde(with = "json_f32")]
pub specular_power: f32,
#[serde(with = "json_f32::array3")]
pub ambient: [f32; 3],
#[serde(with = "json_f32::array4")]
pub edge_color: [f32; 4],
#[serde(with = "json_f32")]
pub edge_size: f32,
pub flags: PmxParsedMaterialFlags,
pub face_count: i32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PmxParsedMaterialFlags {
pub double_sided: bool,
pub ground_shadow: bool,
pub self_shadow_map: bool,
pub self_shadow: bool,
pub edge: bool,
pub vertex_color: bool,
pub point_draw: bool,
pub line_draw: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PmxParsedSkeleton {
pub bones: Vec<PmxParsedBone>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PmxParsedBone {
pub name: String,
pub english_name: String,
pub parent_index: i32,
pub layer: i32,
#[serde(with = "json_f32::array3")]
pub position: [f32; 3],
pub tail_index: i32,
#[serde(with = "json_f32::option_array3")]
pub tail_position: Option<[f32; 3]>,
pub flags: PmxParsedBoneFlags,
pub append_transform: Option<PmxParsedAppendTransform>,
#[serde(with = "json_f32::option_array3")]
pub fixed_axis: Option<[f32; 3]>,
pub local_axis: Option<PmxParsedLocalAxis>,
pub external_parent_key: Option<i32>,
pub ik: Option<PmxParsedIk>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PmxParsedBoneFlags {
pub indexed_tail: bool,
pub rotatable: bool,
pub translatable: bool,
pub visible: bool,
pub enabled: bool,
pub ik: bool,
pub append_local: bool,
pub append_rotate: bool,
pub append_translate: bool,
pub fixed_axis: bool,
pub local_axis: bool,
pub transform_after_physics: bool,
pub external_parent_transform: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PmxParsedAppendTransform {
pub parent_index: i32,
#[serde(with = "json_f32")]
pub weight: f32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PmxParsedLocalAxis {
#[serde(with = "json_f32::array3")]
pub x: [f32; 3],
#[serde(with = "json_f32::array3")]
pub z: [f32; 3],
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PmxParsedIk {
pub target_index: i32,
pub loop_count: i32,
#[serde(with = "json_f32")]
pub limit_angle: f32,
pub links: Vec<PmxParsedIkLink>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PmxParsedIkLink {
pub bone_index: i32,
pub limits: Option<PmxParsedIkLimit>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PmxParsedIkLimit {
#[serde(with = "json_f32::array3")]
pub lower: [f32; 3],
#[serde(with = "json_f32::array3")]
pub upper: [f32; 3],
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PmxRigSpec {
pub bone_count: usize,
pub ik_chain_count: usize,
pub grant_count: usize,
pub bones: Vec<PmxRigSpecBone>,
pub ik_chains: Vec<PmxRigSpecIkChain>,
pub grants: Vec<PmxRigSpecGrant>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PmxRigSpecBone {
pub name: String,
pub name_bytes: String,
pub parent_index: i32,
#[serde(with = "json_f32::array3")]
pub rest_position: [f32; 3],
pub deform_layer: i32,
#[serde(with = "json_f32::option_array3")]
pub fixed_axis: Option<[f32; 3]>,
pub local_axis: Option<PmxRigSpecLocalAxis>,
pub transform_after_physics: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PmxRigSpecLocalAxis {
#[serde(with = "json_f32::array3")]
pub x: [f32; 3],
#[serde(with = "json_f32::array3")]
pub z: [f32; 3],
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PmxRigSpecIkChain {
pub controller_bone_index: i32,
pub target_bone_index: i32,
pub iteration_count: u32,
#[serde(with = "json_f32")]
pub limit_angle: f32,
pub links: Vec<PmxRigSpecIkLink>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PmxRigSpecIkLink {
pub bone_index: i32,
pub has_angle_limit: bool,
#[serde(with = "json_f32::array3")]
pub angle_limit_min: [f32; 3],
#[serde(with = "json_f32::array3")]
pub angle_limit_max: [f32; 3],
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PmxRigSpecGrant {
pub target_bone_index: i32,
pub source_bone_index: i32,
#[serde(with = "json_f32")]
pub ratio: f32,
pub affect_rotation: bool,
pub affect_translation: bool,
pub local: bool,
}
pub fn parse_pmx_rig_spec(data: &[u8]) -> Result<PmxRigSpec, ImportError> {
let model = parse_pmx_model(data)?;
Ok(pmx_model_to_rig_spec(&model))
}
pub fn pmx_model_to_rig_spec(model: &PmxParsedModel) -> PmxRigSpec {
let mut bones = Vec::with_capacity(model.skeleton.bones.len());
let mut ik_chains = Vec::new();
let mut grants = Vec::new();
for (index, bone) in model.skeleton.bones.iter().enumerate() {
bones.push(PmxRigSpecBone {
name: bone.name.clone(),
name_bytes: shift_jis_hex(&bone.name),
parent_index: bone.parent_index,
rest_position: bone.position,
deform_layer: bone.layer,
fixed_axis: bone.fixed_axis,
local_axis: bone.local_axis.as_ref().map(|axis| PmxRigSpecLocalAxis {
x: axis.x,
z: axis.z,
}),
transform_after_physics: bone.flags.transform_after_physics,
});
if let Some(ik) = &bone.ik {
ik_chains.push(PmxRigSpecIkChain {
controller_bone_index: index as i32,
target_bone_index: ik.target_index,
iteration_count: ik.loop_count.max(0) as u32,
limit_angle: ik.limit_angle,
links: ik
.links
.iter()
.map(|link| {
let (angle_limit_min, angle_limit_max) = link
.limits
.as_ref()
.map_or(([0.0; 3], [0.0; 3]), |limit| (limit.lower, limit.upper));
PmxRigSpecIkLink {
bone_index: link.bone_index,
has_angle_limit: link.limits.is_some(),
angle_limit_min,
angle_limit_max,
}
})
.collect(),
});
}
if let Some(append) = &bone.append_transform {
grants.push(PmxRigSpecGrant {
target_bone_index: index as i32,
source_bone_index: append.parent_index,
ratio: append.weight,
affect_rotation: bone.flags.append_rotate,
affect_translation: bone.flags.append_translate,
local: bone.flags.append_local,
});
}
}
PmxRigSpec {
bone_count: bones.len(),
ik_chain_count: ik_chains.len(),
grant_count: grants.len(),
bones,
ik_chains,
grants,
}
}
fn shift_jis_hex(text: &str) -> String {
bytes_to_hex(&encode_sjis(text))
}
fn bytes_to_hex(bytes: &[u8]) -> String {
const HEX: &[u8; 16] = b"0123456789abcdef";
let mut out = String::with_capacity(bytes.len() * 2);
for &byte in bytes {
out.push(HEX[(byte >> 4) as usize] as char);
out.push(HEX[(byte & 0x0f) as usize] as char);
}
out
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PmxParsedMorph {
pub name: String,
pub english_name: String,
#[serde(rename = "type")]
pub kind: String,
pub vertex_offsets: Vec<PmxParsedVertexMorphOffset>,
pub group_offsets: Vec<PmxParsedGroupMorphOffset>,
pub bone_offsets: Vec<PmxParsedBoneMorphOffset>,
pub uv_offsets: Vec<PmxParsedUvMorphOffset>,
pub additional_uv_offsets: Vec<PmxParsedAdditionalUvMorphOffset>,
pub material_offsets: Vec<PmxParsedMaterialMorphOffset>,
pub flip_offsets: Vec<PmxParsedGroupMorphOffset>,
pub impulse_offsets: Vec<PmxParsedImpulseMorphOffset>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PmxParsedVertexMorphOffset {
pub vertex_index: u32,
#[serde(with = "json_f32::array3")]
pub position: [f32; 3],
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PmxParsedGroupMorphOffset {
pub morph_index: i32,
#[serde(with = "json_f32")]
pub weight: f32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PmxParsedBoneMorphOffset {
pub bone_index: i32,
#[serde(with = "json_f32::array3")]
pub translation: [f32; 3],
#[serde(with = "json_f32::array4")]
pub rotation: [f32; 4],
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PmxParsedUvMorphOffset {
pub vertex_index: u32,
#[serde(with = "json_f32::array4")]
pub uv: [f32; 4],
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PmxParsedAdditionalUvMorphOffset {
pub vertex_index: u32,
pub uv_index: u8,
#[serde(with = "json_f32::array4")]
pub uv: [f32; 4],
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PmxParsedMaterialMorphOffset {
pub material_index: i32,
pub operation: String,
#[serde(with = "json_f32::array4")]
pub diffuse: [f32; 4],
#[serde(with = "json_f32::array3")]
pub specular: [f32; 3],
#[serde(with = "json_f32")]
pub specular_power: f32,
#[serde(with = "json_f32::array3")]
pub ambient: [f32; 3],
#[serde(with = "json_f32::array4")]
pub edge_color: [f32; 4],
#[serde(with = "json_f32")]
pub edge_size: f32,
#[serde(with = "json_f32::array4")]
pub texture_factor: [f32; 4],
#[serde(with = "json_f32::array4")]
pub sphere_texture_factor: [f32; 4],
#[serde(with = "json_f32::array4")]
pub toon_texture_factor: [f32; 4],
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PmxParsedImpulseMorphOffset {
pub rigid_body_index: i32,
pub local: bool,
#[serde(with = "json_f32::array3")]
pub velocity: [f32; 3],
#[serde(with = "json_f32::array3")]
pub torque: [f32; 3],
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PmxMaterialSplitResult {
pub meshes: Vec<PmxMaterialSplitMesh>,
pub manifest: PmxMaterialSplitManifest,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PmxMaterialSplitMesh {
pub original_material_index: usize,
pub original_vertex_indices: Vec<u32>,
pub geometry: PmxParsedGeometry,
pub material: PmxParsedMaterial,
pub morphs: Vec<PmxParsedMorph>,
pub morph_index_map: Vec<PmxMaterialSplitMorphIndexMap>,
pub diagnostics: Vec<PmxParserDiagnostic>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PmxMaterialSplit {
pub mesh: PmxMaterialSplitMesh,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PmxMaterialSplitManifest {
pub mesh_count: usize,
pub meshes: Vec<PmxMaterialSplitManifestMesh>,
pub diagnostics: Vec<PmxParserDiagnostic>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PmxMaterialSplitManifestMesh {
pub mesh_index: usize,
pub original_material_index: usize,
pub original_vertex_indices: Vec<u32>,
pub morph_index_map: Vec<PmxMaterialSplitMorphIndexMap>,
pub diagnostics: Vec<PmxParserDiagnostic>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct PmxMaterialSplitMorphIndexMap {
pub original_index: usize,
pub local_index: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PmxParsedDisplayFrame {
pub name: String,
pub english_name: String,
pub special: bool,
pub frames: Vec<PmxParsedDisplayFrameElement>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PmxParsedDisplayFrameElement {
#[serde(rename = "type")]
pub kind: String,
pub index: i32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PmxParsedRigidBody {
pub name: String,
pub english_name: String,
pub bone_index: i32,
pub group: u8,
pub mask: u16,
pub shape: String,
#[serde(with = "json_f32::array3")]
pub size: [f32; 3],
#[serde(with = "json_f32::array3")]
pub position: [f32; 3],
#[serde(with = "json_f32::array3")]
pub rotation: [f32; 3],
#[serde(with = "json_f32")]
pub mass: f32,
#[serde(with = "json_f32")]
pub linear_damping: f32,
#[serde(with = "json_f32")]
pub angular_damping: f32,
#[serde(with = "json_f32")]
pub restitution: f32,
#[serde(with = "json_f32")]
pub friction: f32,
pub mode: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PmxParsedJoint {
pub name: String,
pub english_name: String,
#[serde(rename = "type")]
pub kind: String,
pub rigid_body_index_a: i32,
pub rigid_body_index_b: i32,
#[serde(with = "json_f32::array3")]
pub position: [f32; 3],
#[serde(with = "json_f32::array3")]
pub rotation: [f32; 3],
#[serde(with = "json_f32::array3")]
pub translation_lower_limit: [f32; 3],
#[serde(with = "json_f32::array3")]
pub translation_upper_limit: [f32; 3],
#[serde(with = "json_f32::array3")]
pub rotation_lower_limit: [f32; 3],
#[serde(with = "json_f32::array3")]
pub rotation_upper_limit: [f32; 3],
#[serde(with = "json_f32::array3")]
pub spring_translation_factor: [f32; 3],
#[serde(with = "json_f32::array3")]
pub spring_rotation_factor: [f32; 3],
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PmxParsedSoftBody {
pub name: String,
pub english_name: String,
#[serde(rename = "type")]
pub kind: String,
pub material_index: i32,
pub collision_group: u8,
pub collision_mask: u16,
pub flags: u8,
pub bending_constraints_distance: i32,
pub cluster_count: i32,
#[serde(with = "json_f32")]
pub total_mass: f32,
#[serde(with = "json_f32")]
pub collision_margin: f32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PmxParserDiagnostic {
pub level: String,
pub code: String,
pub message: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PmxPartsDescriptor {
#[serde(default = "default_pmx_parts_version")]
#[serde(with = "json_f32")]
pub version: f32,
#[serde(default = "default_pmx_parts_encoding")]
pub encoding: String,
#[serde(default = "default_pmx_parts_name")]
pub name: String,
#[serde(default)]
pub english_name: String,
#[serde(default)]
pub comment: String,
#[serde(default)]
pub english_comment: String,
#[serde(default)]
pub material_name: String,
#[serde(default)]
pub english_material_name: String,
#[serde(default)]
pub materials: Vec<PmxPartsMaterialDescriptor>,
#[serde(default)]
pub bones: Vec<PmxPartsBoneDescriptor>,
#[serde(default)]
pub morphs: Vec<PmxPartsMorphDescriptor>,
#[serde(default)]
pub display_frames: Vec<PmxPartsDisplayFrameDescriptor>,
#[serde(default)]
pub rigid_bodies: Vec<PmxPartsRigidBodyDescriptor>,
#[serde(default)]
pub joints: Vec<PmxPartsJointDescriptor>,
#[serde(default)]
pub index_sizes: PmxPartsIndexSizes,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PmxPartsMaterialDescriptor {
#[serde(default = "default_pmx_parts_material_name")]
pub name: String,
#[serde(default)]
pub english_name: String,
#[serde(default)]
pub texture_path: String,
#[serde(default)]
pub sphere_texture_path: String,
#[serde(default)]
pub sphere_mode: String,
#[serde(default)]
pub toon_texture_path: String,
#[serde(default = "default_pmx_parts_shared_toon_index")]
pub shared_toon_index: Option<u8>,
#[serde(default = "default_pmx_parts_diffuse")]
#[serde(with = "json_f32::array4")]
pub diffuse: [f32; 4],
#[serde(default)]
#[serde(with = "json_f32::array3")]
pub specular: [f32; 3],
#[serde(default = "default_pmx_parts_specular_power")]
#[serde(with = "json_f32")]
pub specular_power: f32,
#[serde(default = "default_pmx_parts_ambient")]
#[serde(with = "json_f32::array3")]
pub ambient: [f32; 3],
#[serde(default = "default_pmx_parts_edge_color")]
#[serde(with = "json_f32::array4")]
pub edge_color: [f32; 4],
#[serde(default = "default_pmx_parts_edge_size")]
#[serde(with = "json_f32")]
pub edge_size: f32,
#[serde(default)]
pub flags: PmxPartsMaterialFlags,
#[serde(default)]
pub face_count: i32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PmxPartsMaterialFlags {
#[serde(default = "default_true")]
pub double_sided: bool,
#[serde(default = "default_true")]
pub ground_shadow: bool,
#[serde(default = "default_true")]
pub self_shadow_map: bool,
#[serde(default = "default_true")]
pub self_shadow: bool,
#[serde(default)]
pub edge: bool,
#[serde(default)]
pub vertex_color: bool,
#[serde(default)]
pub point_draw: bool,
#[serde(default)]
pub line_draw: bool,
}
impl Default for PmxPartsMaterialFlags {
fn default() -> Self {
Self {
double_sided: true,
ground_shadow: true,
self_shadow_map: true,
self_shadow: true,
edge: false,
vertex_color: false,
point_draw: false,
line_draw: false,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PmxPartsBoneDescriptor {
#[serde(default = "default_pmx_parts_root_bone_name")]
pub name: String,
#[serde(default)]
pub english_name: String,
#[serde(default = "default_negative_index")]
pub parent_index: i32,
#[serde(default)]
pub layer: i32,
#[serde(default)]
#[serde(with = "json_f32::array3")]
pub position: [f32; 3],
#[serde(default = "default_negative_index")]
pub tail_index: i32,
#[serde(default)]
#[serde(with = "json_f32::option_array3")]
pub tail_position: Option<[f32; 3]>,
#[serde(default)]
pub rotatable: bool,
#[serde(default)]
pub translatable: bool,
#[serde(default = "default_true")]
pub visible: bool,
#[serde(default = "default_true")]
pub enabled: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PmxPartsMorphDescriptor {
#[serde(default)]
pub name: String,
#[serde(default)]
pub english_name: String,
#[serde(default = "default_pmx_parts_morph_kind", alias = "type")]
pub kind: String,
#[serde(default)]
pub vertex_offsets: Vec<PmxPartsVertexMorphOffset>,
#[serde(default)]
pub group_offsets: Vec<PmxPartsGroupMorphOffset>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PmxPartsVertexMorphOffset {
pub vertex_index: u32,
#[serde(with = "json_f32::array3")]
pub position: [f32; 3],
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PmxPartsGroupMorphOffset {
pub morph_index: i32,
#[serde(with = "json_f32")]
pub weight: f32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PmxPartsDisplayFrameDescriptor {
#[serde(default)]
pub name: String,
#[serde(default)]
pub english_name: String,
#[serde(default)]
pub special: bool,
#[serde(default)]
pub frames: Vec<PmxPartsDisplayFrameItem>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PmxPartsDisplayFrameItem {
#[serde(default = "default_pmx_parts_display_frame_kind")]
pub kind: String,
#[serde(default)]
pub index: i32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PmxPartsRigidBodyDescriptor {
#[serde(default)]
pub name: String,
#[serde(default)]
pub english_name: String,
#[serde(default = "default_negative_index")]
pub bone_index: i32,
#[serde(default)]
pub group: u8,
#[serde(default)]
pub mask: u16,
#[serde(default = "default_pmx_parts_rigid_body_shape")]
pub shape: String,
#[serde(default = "default_unit_vec3")]
#[serde(with = "json_f32::array3")]
pub size: [f32; 3],
#[serde(default)]
#[serde(with = "json_f32::array3")]
pub position: [f32; 3],
#[serde(default)]
#[serde(with = "json_f32::array3")]
pub rotation: [f32; 3],
#[serde(default = "default_pmx_parts_mass")]
#[serde(with = "json_f32")]
pub mass: f32,
#[serde(default)]
#[serde(with = "json_f32")]
pub linear_damping: f32,
#[serde(default)]
#[serde(with = "json_f32")]
pub angular_damping: f32,
#[serde(default)]
#[serde(with = "json_f32")]
pub restitution: f32,
#[serde(default)]
#[serde(with = "json_f32")]
pub friction: f32,
#[serde(default = "default_pmx_parts_rigid_body_mode")]
pub mode: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PmxPartsJointDescriptor {
#[serde(default)]
pub name: String,
#[serde(default)]
pub english_name: String,
#[serde(
default = "default_pmx_parts_joint_kind",
rename = "type",
alias = "kind"
)]
pub kind: String,
#[serde(default = "default_negative_index")]
pub rigid_body_index_a: i32,
#[serde(default = "default_negative_index")]
pub rigid_body_index_b: i32,
#[serde(default)]
#[serde(with = "json_f32::array3")]
pub position: [f32; 3],
#[serde(default)]
#[serde(with = "json_f32::array3")]
pub rotation: [f32; 3],
#[serde(default)]
#[serde(with = "json_f32::array3")]
pub translation_lower_limit: [f32; 3],
#[serde(default)]
#[serde(with = "json_f32::array3")]
pub translation_upper_limit: [f32; 3],
#[serde(default)]
#[serde(with = "json_f32::array3")]
pub rotation_lower_limit: [f32; 3],
#[serde(default)]
#[serde(with = "json_f32::array3")]
pub rotation_upper_limit: [f32; 3],
#[serde(default)]
#[serde(with = "json_f32::array3")]
pub spring_translation_factor: [f32; 3],
#[serde(default)]
#[serde(with = "json_f32::array3")]
pub spring_rotation_factor: [f32; 3],
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PmxPartsIndexSizes {
#[serde(default = "default_pmx_vertex_index_size")]
pub vertex: u8,
#[serde(default = "default_pmx_small_index_size")]
pub texture: u8,
#[serde(default = "default_pmx_small_index_size")]
pub material: u8,
#[serde(default = "default_pmx_small_index_size")]
pub bone: u8,
#[serde(default = "default_pmx_small_index_size")]
pub morph: u8,
#[serde(default = "default_pmx_small_index_size")]
pub rigid_body: u8,
}
impl Default for PmxPartsIndexSizes {
fn default() -> Self {
Self {
vertex: default_pmx_vertex_index_size(),
texture: default_pmx_small_index_size(),
material: default_pmx_small_index_size(),
bone: default_pmx_small_index_size(),
morph: default_pmx_small_index_size(),
rigid_body: default_pmx_small_index_size(),
}
}
}
pub struct PmxPartsInput<'a> {
pub descriptor: PmxPartsDescriptor,
pub positions_xyz: &'a [f32],
pub normals_xyz: &'a [f32],
pub uvs_xy: &'a [f32],
pub indices: &'a [u32],
pub skin_indices: &'a [u32],
pub skin_weights: &'a [f32],
pub edge_scale: &'a [f32],
}
fn default_pmx_format() -> String {
"pmx".to_owned()
}
fn default_pmx_parts_version() -> f32 {
2.0
}
fn default_pmx_parts_encoding() -> String {
"utf-8".to_owned()
}
fn default_pmx_parts_name() -> String {
"model".to_owned()
}
fn default_pmx_parts_root_bone_name() -> String {
"root".to_owned()
}
fn default_negative_index() -> i32 {
-1
}
fn default_pmx_parts_display_frame_kind() -> String {
"bone".to_owned()
}
fn default_pmx_parts_morph_kind() -> String {
"vertex".to_owned()
}
fn default_pmx_parts_rigid_body_shape() -> String {
"sphere".to_owned()
}
fn default_pmx_parts_rigid_body_mode() -> String {
"static".to_owned()
}
fn default_pmx_parts_joint_kind() -> String {
"generic6dofSpring".to_owned()
}
fn default_unit_vec3() -> [f32; 3] {
[1.0, 1.0, 1.0]
}
fn default_pmx_parts_mass() -> f32 {
1.0
}
fn default_pmx_parts_material_name() -> String {
"material".to_owned()
}
fn default_pmx_parts_shared_toon_index() -> Option<u8> {
Some(0)
}
fn default_pmx_parts_diffuse() -> [f32; 4] {
[0.8, 0.8, 0.8, 1.0]
}
fn default_pmx_parts_specular_power() -> f32 {
1.0
}
fn default_pmx_parts_ambient() -> [f32; 3] {
[0.2, 0.2, 0.2]
}
fn default_pmx_parts_edge_color() -> [f32; 4] {
[0.0, 0.0, 0.0, 1.0]
}
fn default_pmx_parts_edge_size() -> f32 {
1.0
}
fn default_true() -> bool {
true
}
fn default_pmx_vertex_index_size() -> u8 {
4
}
fn default_pmx_small_index_size() -> u8 {
1
}
pub fn build_pmx_model_from_parts(input: PmxPartsInput<'_>) -> Result<PmxParsedModel, String> {
let vertex_count = validate_pmx_parts_geometry(
input.positions_xyz,
input.normals_xyz,
input.uvs_xy,
input.indices,
input.skin_indices,
input.skin_weights,
input.edge_scale,
)?;
validate_pmx_parts_descriptor(
&input.descriptor,
vertex_count,
input.indices,
input.skin_indices,
)?;
let skin_indices = if input.skin_indices.is_empty() {
vec![0; vertex_count * 4]
} else {
input.skin_indices.to_vec()
};
let skin_weights = if input.skin_weights.is_empty() {
default_pmx_skin_weights(vertex_count)
} else {
input.skin_weights.to_vec()
};
let edge_scale = if input.edge_scale.is_empty() {
vec![1.0; vertex_count]
} else {
input.edge_scale.to_vec()
};
let skinning_modes = vec![PMX_SKINNING_MODE_BDEF4.to_owned(); vertex_count];
let face_count = input.indices.len() / 3;
let (materials, material_groups) = build_pmx_parts_materials(&input.descriptor, face_count);
let bones = build_pmx_parts_bones(&input.descriptor);
let morphs = build_pmx_parts_morphs(&input.descriptor);
let display_frames = build_pmx_parts_display_frames(&input.descriptor);
let rigid_bodies = build_pmx_parts_rigid_bodies(&input.descriptor);
let joints = build_pmx_parts_joints(&input.descriptor);
let model = PmxParsedModel {
metadata: PmxParsedMetadata {
format: "pmx".to_owned(),
version: input.descriptor.version,
encoding: input.descriptor.encoding,
name: input.descriptor.name,
english_name: input.descriptor.english_name,
comment: input.descriptor.comment,
english_comment: input.descriptor.english_comment,
counts: PmxParsedCounts {
vertices: vertex_count,
faces: face_count,
materials: materials.len(),
bones: bones.len(),
morphs: morphs.len(),
display_frames: display_frames.len(),
rigid_bodies: rigid_bodies.len(),
joints: joints.len(),
soft_bodies: 0,
},
index_sizes: PmxParsedIndexSizes {
vertex: input.descriptor.index_sizes.vertex,
texture: input.descriptor.index_sizes.texture,
material: input.descriptor.index_sizes.material,
bone: input.descriptor.index_sizes.bone,
morph: input.descriptor.index_sizes.morph,
rigid_body: input.descriptor.index_sizes.rigid_body,
},
additional_uv_count: 0,
},
geometry: PmxParsedGeometry {
positions: input.positions_xyz.to_vec(),
normals: input.normals_xyz.to_vec(),
uvs: input.uvs_xy.to_vec(),
additional_uvs: Vec::new(),
indices: input.indices.to_vec(),
skin_indices,
skin_weights,
edge_scale,
material_groups,
sdef: PmxParsedSdef {
skinning_modes,
..PmxParsedSdef::default()
},
qdef: PmxParsedQdef::default(),
},
materials,
skeleton: PmxParsedSkeleton { bones },
morphs,
display_frames,
rigid_bodies,
joints,
soft_bodies: Vec::new(),
diagnostics: Vec::new(),
};
validate_pmx_export_model(&model)?;
Ok(model)
}
pub fn validate_pmx_export_model(model: &PmxParsedModel) -> Result<(), String> {
let geometry = &model.geometry;
if !geometry.positions.len().is_multiple_of(3) {
return Err(format!(
"PMX export positions length must be divisible by 3: {}",
geometry.positions.len()
));
}
let vertex_count = geometry.positions.len() / 3;
if geometry.normals.len() != vertex_count * 3 {
return Err(format!(
"PMX export normals length mismatch: expected {}, got {}",
vertex_count * 3,
geometry.normals.len()
));
}
if geometry.uvs.len() != vertex_count * 2 {
return Err(format!(
"PMX export uvs length mismatch: expected {}, got {}",
vertex_count * 2,
geometry.uvs.len()
));
}
if geometry.additional_uvs.len() != model.metadata.additional_uv_count as usize {
return Err(format!(
"PMX export additionalUvs set count mismatch: expected {}, got {}",
model.metadata.additional_uv_count,
geometry.additional_uvs.len()
));
}
for (index, values) in geometry.additional_uvs.iter().enumerate() {
if values.len() != vertex_count * 4 {
return Err(format!(
"PMX export additionalUvs[{}] length mismatch: expected {}, got {}",
index,
vertex_count * 4,
values.len()
));
}
}
if !geometry.indices.len().is_multiple_of(3) {
return Err(format!(
"PMX export indices length must be divisible by 3: {}",
geometry.indices.len()
));
}
let expected_skin_len = vertex_count * 4;
if geometry.skin_indices.is_empty() != geometry.skin_weights.is_empty() {
return Err(
"PMX export skinIndices and skinWeights must be both empty or both provided".to_owned(),
);
}
if !geometry.skin_indices.is_empty() && geometry.skin_indices.len() != expected_skin_len {
return Err(format!(
"PMX export skinIndices length mismatch: expected {}, got {}",
expected_skin_len,
geometry.skin_indices.len()
));
}
if !geometry.skin_weights.is_empty() && geometry.skin_weights.len() != expected_skin_len {
return Err(format!(
"PMX export skinWeights length mismatch: expected {}, got {}",
expected_skin_len,
geometry.skin_weights.len()
));
}
if !geometry.sdef.skinning_modes.is_empty()
&& geometry.sdef.skinning_modes.len() != vertex_count
{
return Err(format!(
"PMX export skinningModes length mismatch: expected {}, got {}",
vertex_count,
geometry.sdef.skinning_modes.len()
));
}
for (index, mode) in geometry.sdef.skinning_modes.iter().enumerate() {
if !matches!(
mode.as_str(),
PMX_SKINNING_MODE_BDEF1
| PMX_SKINNING_MODE_BDEF2
| PMX_SKINNING_MODE_BDEF4
| PMX_SKINNING_MODE_SDEF
| PMX_SKINNING_MODE_QDEF
) {
return Err(format!(
"PMX export skinningModes[{index}] must be bdef1, bdef2, bdef4, sdef, or qdef, got {mode}"
));
}
}
Ok(())
}
fn validate_pmx_parts_geometry(
positions_xyz: &[f32],
normals_xyz: &[f32],
uvs_xy: &[f32],
indices: &[u32],
skin_indices: &[u32],
skin_weights: &[f32],
edge_scale: &[f32],
) -> Result<usize, String> {
if !positions_xyz.len().is_multiple_of(3) {
return Err(format!(
"positions_xyz must contain vertex_count * 3 values, got {}",
positions_xyz.len()
));
}
let vertex_count = positions_xyz.len() / 3;
if vertex_count == 0 {
return Err("PMX parts export requires at least one vertex".to_owned());
}
if normals_xyz.len() != vertex_count * 3 {
return Err(format!(
"normals_xyz must contain vertex_count * 3 values, expected {}, got {}",
vertex_count * 3,
normals_xyz.len()
));
}
if uvs_xy.len() != vertex_count * 2 {
return Err(format!(
"uvs_xy must contain vertex_count * 2 values, expected {}, got {}",
vertex_count * 2,
uvs_xy.len()
));
}
if !indices.len().is_multiple_of(3) {
return Err(format!(
"indices must contain triangle indices and be divisible by 3, got {}",
indices.len()
));
}
if let Some(&index) = indices
.iter()
.find(|&&index| index as usize >= vertex_count)
{
return Err(format!(
"indices contains out-of-range vertex index {index} for vertex_count {vertex_count}"
));
}
if !skin_indices.is_empty() && skin_indices.len() != vertex_count * 4 {
return Err(format!(
"skin_indices must contain vertex_count * 4 values when provided, expected {}, got {}",
vertex_count * 4,
skin_indices.len()
));
}
if !skin_weights.is_empty() && skin_weights.len() != vertex_count * 4 {
return Err(format!(
"skin_weights must contain vertex_count * 4 values when provided, expected {}, got {}",
vertex_count * 4,
skin_weights.len()
));
}
if skin_indices.is_empty() != skin_weights.is_empty() {
return Err(
"skin_indices and skin_weights must either both be provided or both be empty"
.to_owned(),
);
}
if !edge_scale.is_empty() && edge_scale.len() != vertex_count {
return Err(format!(
"edge_scale must contain vertex_count values when provided, expected {}, got {}",
vertex_count,
edge_scale.len()
));
}
Ok(vertex_count)
}
fn validate_pmx_parts_descriptor(
descriptor: &PmxPartsDescriptor,
vertex_count: usize,
indices: &[u32],
skin_indices: &[u32],
) -> Result<(), String> {
if descriptor.version < 2.0 {
return Err(format!(
"PMX parts export supports PMX 2.0 or newer, got {}",
descriptor.version
));
}
if descriptor.encoding != "utf-8" && descriptor.encoding != "utf-16-le" {
return Err(format!(
"PMX parts export encoding must be utf-8 or utf-16-le, got {}",
descriptor.encoding
));
}
validate_pmx_index_size("vertex", descriptor.index_sizes.vertex)?;
validate_pmx_index_size("texture", descriptor.index_sizes.texture)?;
validate_pmx_index_size("material", descriptor.index_sizes.material)?;
validate_pmx_index_size("bone", descriptor.index_sizes.bone)?;
validate_pmx_index_size("morph", descriptor.index_sizes.morph)?;
validate_pmx_index_size("rigidBody", descriptor.index_sizes.rigid_body)?;
if indices.len() / 3 > i32::MAX as usize {
return Err(format!(
"face count {} exceeds PMX i32 material face count limit",
indices.len() / 3
));
}
if !pmx_vertex_index_size_can_hold(descriptor.index_sizes.vertex, vertex_count) {
return Err(format!(
"vertex index size {} cannot hold vertex_count {}",
descriptor.index_sizes.vertex, vertex_count
));
}
if !indices.is_empty()
&& !pmx_vertex_index_size_can_hold(
descriptor.index_sizes.vertex,
indices.iter().copied().max().unwrap_or(0) as usize + 1,
)
{
return Err(format!(
"vertex index size {} cannot hold max index {}",
descriptor.index_sizes.vertex,
indices.iter().copied().max().unwrap_or(0)
));
}
validate_pmx_parts_materials(descriptor, indices.len() / 3)?;
validate_pmx_parts_bones(descriptor)?;
validate_pmx_parts_morphs(descriptor, vertex_count)?;
validate_pmx_parts_skin_indices(descriptor, skin_indices)?;
validate_pmx_parts_display_frames(descriptor)?;
validate_pmx_parts_physics(descriptor)?;
Ok(())
}
fn validate_pmx_parts_materials(
descriptor: &PmxPartsDescriptor,
face_count: usize,
) -> Result<(), String> {
if descriptor.materials.is_empty() {
return Ok(());
}
if !pmx_signed_index_size_can_hold_count(
descriptor.index_sizes.material,
descriptor.materials.len(),
) {
return Err(format!(
"material index size {} cannot hold material count {}",
descriptor.index_sizes.material,
descriptor.materials.len()
));
}
let mut total = 0usize;
for (index, material) in descriptor.materials.iter().enumerate() {
if material.face_count < 0 {
return Err(format!(
"materials[{index}].faceCount must be non-negative, got {}",
material.face_count
));
}
total += material.face_count as usize;
}
if total != face_count {
return Err(format!(
"materials faceCount sum must match index face count, expected {}, got {}",
face_count, total
));
}
Ok(())
}
fn validate_pmx_parts_bones(descriptor: &PmxPartsDescriptor) -> Result<(), String> {
let bone_count = descriptor.bones.len().max(1);
if !pmx_signed_index_size_can_hold_count(descriptor.index_sizes.bone, bone_count) {
return Err(format!(
"bone index size {} cannot hold bone count {}",
descriptor.index_sizes.bone, bone_count
));
}
for (index, bone) in descriptor.bones.iter().enumerate() {
if bone.parent_index < -1
|| (bone.parent_index != -1 && bone.parent_index as usize >= bone_count)
{
return Err(format!(
"bones[{index}].parentIndex must be -1 or a valid bone index, got {}",
bone.parent_index
));
}
if bone.tail_index < -1 || (bone.tail_index != -1 && bone.tail_index as usize >= bone_count)
{
return Err(format!(
"bones[{index}].tailIndex must be -1 or a valid bone index, got {}",
bone.tail_index
));
}
}
Ok(())
}
fn validate_pmx_parts_skin_indices(
descriptor: &PmxPartsDescriptor,
skin_indices: &[u32],
) -> Result<(), String> {
if skin_indices.is_empty() {
return Ok(());
}
let bone_count = descriptor.bones.len().max(1);
if let Some(&index) = skin_indices
.iter()
.find(|&&index| index as usize >= bone_count)
{
return Err(format!(
"skin_indices contains out-of-range bone index {index} for bone count {bone_count}"
));
}
Ok(())
}
fn validate_pmx_parts_morphs(
descriptor: &PmxPartsDescriptor,
vertex_count: usize,
) -> Result<(), String> {
if !pmx_signed_index_size_can_hold_count(descriptor.index_sizes.morph, descriptor.morphs.len())
{
return Err(format!(
"morph index size {} cannot hold morph count {}",
descriptor.index_sizes.morph,
descriptor.morphs.len()
));
}
for (morph_index, morph) in descriptor.morphs.iter().enumerate() {
match morph.kind.as_str() {
"vertex" => {
for (offset_index, offset) in morph.vertex_offsets.iter().enumerate() {
if offset.vertex_index as usize >= vertex_count {
return Err(format!(
"morphs[{morph_index}].vertexOffsets[{offset_index}].vertexIndex out of range: {} for vertex_count {vertex_count}",
offset.vertex_index
));
}
}
}
"group" => {
for (offset_index, offset) in morph.group_offsets.iter().enumerate() {
if offset.morph_index < 0
|| offset.morph_index as usize >= descriptor.morphs.len()
{
return Err(format!(
"morphs[{morph_index}].groupOffsets[{offset_index}].morphIndex out of range: {}",
offset.morph_index
));
}
}
}
other => {
return Err(format!(
"morphs[{morph_index}].kind must be vertex or group, got {other}"
));
}
}
}
Ok(())
}
fn validate_pmx_parts_display_frames(descriptor: &PmxPartsDescriptor) -> Result<(), String> {
let bone_count = descriptor.bones.len().max(1);
let morph_count = descriptor.morphs.len();
for (frame_index, frame) in descriptor.display_frames.iter().enumerate() {
for (item_index, item) in frame.frames.iter().enumerate() {
match item.kind.as_str() {
"bone" => {
if item.index < 0 || item.index as usize >= bone_count {
return Err(format!(
"displayFrames[{frame_index}].frames[{item_index}].index must reference an existing bone, got {}",
item.index
));
}
}
"morph" => {
if item.index < 0 || item.index as usize >= morph_count {
return Err(format!(
"displayFrames[{frame_index}].frames[{item_index}].index must reference an existing morph, got {}",
item.index
));
}
}
other => {
return Err(format!(
"displayFrames[{frame_index}].frames[{item_index}].kind must be bone or morph, got {other}"
));
}
}
}
}
Ok(())
}
fn validate_pmx_parts_physics(descriptor: &PmxPartsDescriptor) -> Result<(), String> {
let bone_count = descriptor.bones.len().max(1);
if !pmx_signed_index_size_can_hold_count(
descriptor.index_sizes.rigid_body,
descriptor.rigid_bodies.len(),
) {
return Err(format!(
"rigidBody index size {} cannot hold rigid body count {}",
descriptor.index_sizes.rigid_body,
descriptor.rigid_bodies.len()
));
}
for (index, body) in descriptor.rigid_bodies.iter().enumerate() {
if body.bone_index < -1 || (body.bone_index != -1 && body.bone_index as usize >= bone_count)
{
return Err(format!(
"rigidBodies[{index}].boneIndex must be -1 or a valid bone index, got {}",
body.bone_index
));
}
if !matches!(body.shape.as_str(), "sphere" | "box" | "capsule") {
return Err(format!(
"rigidBodies[{index}].shape must be sphere, box, or capsule, got {}",
body.shape
));
}
if !matches!(body.mode.as_str(), "static" | "dynamic" | "dynamicBone") {
return Err(format!(
"rigidBodies[{index}].mode must be static, dynamic, or dynamicBone, got {}",
body.mode
));
}
}
for (index, joint) in descriptor.joints.iter().enumerate() {
validate_pmx_parts_rigid_body_ref(
"rigidBodyIndexA",
index,
joint.rigid_body_index_a,
descriptor.rigid_bodies.len(),
)?;
validate_pmx_parts_rigid_body_ref(
"rigidBodyIndexB",
index,
joint.rigid_body_index_b,
descriptor.rigid_bodies.len(),
)?;
if !matches!(
joint.kind.as_str(),
"generic6dofSpring" | "generic6dof" | "point2point" | "coneTwist" | "slider" | "hinge"
) {
return Err(format!(
"joints[{index}].kind has unsupported PMX joint type {}",
joint.kind
));
}
}
Ok(())
}
fn validate_pmx_parts_rigid_body_ref(
field: &str,
joint_index: usize,
value: i32,
rigid_body_count: usize,
) -> Result<(), String> {
if value < -1 || (value != -1 && value as usize >= rigid_body_count) {
Err(format!(
"joints[{joint_index}].{field} must be -1 or a valid rigid body index, got {value}"
))
} else {
Ok(())
}
}
fn validate_pmx_index_size(name: &str, value: u8) -> Result<(), String> {
if matches!(value, 1 | 2 | 4) {
Ok(())
} else {
Err(format!(
"PMX {name} index size must be 1, 2, or 4, got {value}"
))
}
}
fn pmx_vertex_index_size_can_hold(size: u8, count: usize) -> bool {
match size {
1 => count <= u8::MAX as usize + 1,
2 => count <= u16::MAX as usize + 1,
4 => true,
_ => false,
}
}
fn pmx_signed_index_size_can_hold_count(size: u8, count: usize) -> bool {
match size {
1 => count <= i8::MAX as usize + 1,
2 => count <= i16::MAX as usize + 1,
4 => true,
_ => false,
}
}
fn default_pmx_skin_weights(vertex_count: usize) -> Vec<f32> {
let mut weights = vec![0.0; vertex_count * 4];
for vertex in 0..vertex_count {
weights[vertex * 4] = 1.0;
}
weights
}
fn build_pmx_parts_materials(
descriptor: &PmxPartsDescriptor,
face_count: usize,
) -> (Vec<PmxParsedMaterial>, Vec<PmxParsedMaterialGroup>) {
if face_count == 0 {
return (Vec::new(), Vec::new());
}
if descriptor.materials.is_empty() {
return (
vec![default_pmx_parts_material(
&descriptor.material_name,
&descriptor.english_material_name,
face_count as i32,
)],
vec![PmxParsedMaterialGroup {
start: 0,
count: face_count * 3,
material_index: 0,
}],
);
}
let mut start = 0usize;
let mut materials = Vec::with_capacity(descriptor.materials.len());
let mut groups = Vec::with_capacity(descriptor.materials.len());
for (index, material) in descriptor.materials.iter().enumerate() {
let index_count = material.face_count as usize * 3;
materials.push(pmx_parts_material_from_descriptor(material));
groups.push(PmxParsedMaterialGroup {
start,
count: index_count,
material_index: index,
});
start += index_count;
}
(materials, groups)
}
fn pmx_parts_material_from_descriptor(material: &PmxPartsMaterialDescriptor) -> PmxParsedMaterial {
PmxParsedMaterial {
name: material.name.clone(),
english_name: material.english_name.clone(),
texture_path: material.texture_path.clone(),
sphere_texture_path: material.sphere_texture_path.clone(),
sphere_mode: if material.sphere_mode.is_empty() {
"none".to_owned()
} else {
material.sphere_mode.clone()
},
toon_texture_path: material.toon_texture_path.clone(),
shared_toon_index: material.shared_toon_index,
diffuse: material.diffuse,
specular: material.specular,
specular_power: material.specular_power,
ambient: material.ambient,
edge_color: material.edge_color,
edge_size: material.edge_size,
flags: PmxParsedMaterialFlags {
double_sided: material.flags.double_sided,
ground_shadow: material.flags.ground_shadow,
self_shadow_map: material.flags.self_shadow_map,
self_shadow: material.flags.self_shadow,
edge: material.flags.edge,
vertex_color: material.flags.vertex_color,
point_draw: material.flags.point_draw,
line_draw: material.flags.line_draw,
},
face_count: material.face_count,
}
}
fn build_pmx_parts_bones(descriptor: &PmxPartsDescriptor) -> Vec<PmxParsedBone> {
if descriptor.bones.is_empty() {
return vec![default_pmx_parts_root_bone()];
}
descriptor
.bones
.iter()
.map(pmx_parts_bone_from_descriptor)
.collect()
}
fn build_pmx_parts_display_frames(descriptor: &PmxPartsDescriptor) -> Vec<PmxParsedDisplayFrame> {
descriptor
.display_frames
.iter()
.map(|frame| PmxParsedDisplayFrame {
name: frame.name.clone(),
english_name: frame.english_name.clone(),
special: frame.special,
frames: frame
.frames
.iter()
.map(|item| PmxParsedDisplayFrameElement {
kind: item.kind.clone(),
index: item.index,
})
.collect(),
})
.collect()
}
fn build_pmx_parts_morphs(descriptor: &PmxPartsDescriptor) -> Vec<PmxParsedMorph> {
descriptor
.morphs
.iter()
.map(pmx_parts_morph_from_descriptor)
.collect()
}
fn build_pmx_parts_rigid_bodies(descriptor: &PmxPartsDescriptor) -> Vec<PmxParsedRigidBody> {
descriptor
.rigid_bodies
.iter()
.map(|body| PmxParsedRigidBody {
name: body.name.clone(),
english_name: body.english_name.clone(),
bone_index: body.bone_index,
group: body.group,
mask: body.mask,
shape: body.shape.clone(),
size: body.size,
position: body.position,
rotation: body.rotation,
mass: body.mass,
linear_damping: body.linear_damping,
angular_damping: body.angular_damping,
restitution: body.restitution,
friction: body.friction,
mode: body.mode.clone(),
})
.collect()
}
fn build_pmx_parts_joints(descriptor: &PmxPartsDescriptor) -> Vec<PmxParsedJoint> {
descriptor
.joints
.iter()
.map(|joint| PmxParsedJoint {
name: joint.name.clone(),
english_name: joint.english_name.clone(),
kind: joint.kind.clone(),
rigid_body_index_a: joint.rigid_body_index_a,
rigid_body_index_b: joint.rigid_body_index_b,
position: joint.position,
rotation: joint.rotation,
translation_lower_limit: joint.translation_lower_limit,
translation_upper_limit: joint.translation_upper_limit,
rotation_lower_limit: joint.rotation_lower_limit,
rotation_upper_limit: joint.rotation_upper_limit,
spring_translation_factor: joint.spring_translation_factor,
spring_rotation_factor: joint.spring_rotation_factor,
})
.collect()
}
fn pmx_parts_morph_from_descriptor(morph: &PmxPartsMorphDescriptor) -> PmxParsedMorph {
let mut parsed = PmxParsedMorph {
name: morph.name.clone(),
english_name: morph.english_name.clone(),
kind: morph.kind.clone(),
vertex_offsets: Vec::new(),
group_offsets: Vec::new(),
bone_offsets: Vec::new(),
uv_offsets: Vec::new(),
additional_uv_offsets: Vec::new(),
material_offsets: Vec::new(),
flip_offsets: Vec::new(),
impulse_offsets: Vec::new(),
};
match morph.kind.as_str() {
"vertex" => {
parsed.vertex_offsets = morph
.vertex_offsets
.iter()
.map(|offset| PmxParsedVertexMorphOffset {
vertex_index: offset.vertex_index,
position: offset.position,
})
.collect();
}
"group" => {
parsed.group_offsets = morph
.group_offsets
.iter()
.map(|offset| PmxParsedGroupMorphOffset {
morph_index: offset.morph_index,
weight: offset.weight,
})
.collect();
}
_ => unreachable!("PmxPartsMorphDescriptor was validated before build"),
}
parsed
}
fn pmx_parts_bone_from_descriptor(bone: &PmxPartsBoneDescriptor) -> PmxParsedBone {
let tail_position = if bone.tail_index >= 0 {
None
} else {
Some(bone.tail_position.unwrap_or([0.0, 1.0, 0.0]))
};
PmxParsedBone {
name: bone.name.clone(),
english_name: bone.english_name.clone(),
parent_index: bone.parent_index,
layer: bone.layer,
position: bone.position,
tail_index: bone.tail_index,
tail_position,
flags: PmxParsedBoneFlags {
indexed_tail: bone.tail_index >= 0,
rotatable: bone.rotatable,
translatable: bone.translatable,
visible: bone.visible,
enabled: bone.enabled,
ik: false,
append_local: false,
append_rotate: false,
append_translate: false,
fixed_axis: false,
local_axis: false,
external_parent_transform: false,
transform_after_physics: false,
},
append_transform: None,
fixed_axis: None,
local_axis: None,
external_parent_key: None,
ik: None,
}
}
fn default_pmx_parts_material(
name: &str,
english_name: &str,
face_count: i32,
) -> PmxParsedMaterial {
PmxParsedMaterial {
name: if name.is_empty() {
"material".to_owned()
} else {
name.to_owned()
},
english_name: english_name.to_owned(),
texture_path: String::new(),
sphere_texture_path: String::new(),
sphere_mode: "none".to_owned(),
toon_texture_path: String::new(),
shared_toon_index: Some(0),
diffuse: [0.8, 0.8, 0.8, 1.0],
specular: [0.0, 0.0, 0.0],
specular_power: 1.0,
ambient: [0.2, 0.2, 0.2],
edge_color: [0.0, 0.0, 0.0, 1.0],
edge_size: 1.0,
flags: PmxParsedMaterialFlags {
double_sided: true,
ground_shadow: true,
self_shadow_map: true,
self_shadow: true,
edge: false,
vertex_color: false,
point_draw: false,
line_draw: false,
},
face_count,
}
}
fn default_pmx_parts_root_bone() -> PmxParsedBone {
PmxParsedBone {
name: "root".to_owned(),
english_name: "root".to_owned(),
parent_index: -1,
layer: 0,
position: [0.0, 0.0, 0.0],
tail_index: -1,
tail_position: Some([0.0, 1.0, 0.0]),
flags: PmxParsedBoneFlags {
indexed_tail: false,
rotatable: true,
translatable: true,
visible: true,
enabled: true,
ik: false,
append_local: false,
append_rotate: false,
append_translate: false,
fixed_axis: false,
local_axis: false,
transform_after_physics: false,
external_parent_transform: false,
},
append_transform: None,
fixed_axis: None,
local_axis: None,
external_parent_key: None,
ik: None,
}
}
pub fn parse_pmx_model(data: &[u8]) -> Result<PmxParsedModel, ImportError> {
let (header, pos) = read_header(data)?;
let mut r = Reader { data, pos };
let name = r.read_string(header.encoding)?;
let english_name = r.read_string(header.encoding)?;
let comment = r.read_string(header.encoding)?;
let english_comment = r.read_string(header.encoding)?;
let vertex_count = read_section_count_with_min_record(&mut r, 12 + 12 + 8 + 1 + 4)?;
let geometry = read_parsed_geometry(&mut r, &header, vertex_count)?;
let index_count = geometry.indices.len();
let textures = read_parsed_textures(&mut r, header.encoding)?;
let materials = read_parsed_materials(&mut r, &header, &textures)?;
let material_groups = build_material_groups(&materials);
let bone_count = read_section_count_with_min_record(&mut r, 4 + 4 + 12)?;
let skeleton = PmxParsedSkeleton {
bones: read_parsed_bones(&mut r, &header, bone_count)?,
};
let morphs = read_parsed_morphs(&mut r, &header)?;
let display_frames = read_parsed_display_frames(&mut r, &header)?;
let rigid_bodies = if r.remaining() >= 4 {
read_parsed_rigid_bodies(&mut r, &header)?
} else {
Vec::new()
};
let joints = if r.remaining() >= 4 {
read_parsed_joints(&mut r, &header)?
} else {
Vec::new()
};
let soft_bodies = if header.version >= 2.05 && r.remaining() >= 4 {
read_parsed_soft_bodies(&mut r, &header)?
} else {
Vec::new()
};
let mut geometry = geometry;
geometry.material_groups = material_groups;
let mut diagnostics = Vec::new();
if !soft_bodies.is_empty() {
diagnostics.push(PmxParserDiagnostic {
level: "warning".to_owned(),
code: "PMX_SOFT_BODY_UNSUPPORTED".to_owned(),
message: format!(
"{} PMX soft bodies are parsed only as header records by mmd-anim.",
soft_bodies.len()
),
});
}
if r.remaining() > 0 {
diagnostics.push(PmxParserDiagnostic {
level: "warning".to_owned(),
code: "PMX_TRAILING_DATA_UNPARSED".to_owned(),
message: format!("{} trailing PMX bytes were left unparsed.", r.remaining()),
});
}
Ok(PmxParsedModel {
metadata: PmxParsedMetadata {
format: "pmx".to_owned(),
version: header.version,
encoding: match header.encoding {
TextEncoding::Utf8 => "utf-8".to_owned(),
TextEncoding::Utf16Le => "utf-16-le".to_owned(),
},
name,
english_name,
comment,
english_comment,
counts: PmxParsedCounts {
vertices: vertex_count,
faces: index_count / 3,
materials: materials.len(),
bones: skeleton.bones.len(),
morphs: morphs.len(),
display_frames: display_frames.len(),
rigid_bodies: rigid_bodies.len(),
joints: joints.len(),
soft_bodies: soft_bodies.len(),
},
index_sizes: PmxParsedIndexSizes {
vertex: header.vertex_index_size,
texture: header.texture_index_size,
material: header.material_index_size,
bone: header.bone_index_size,
morph: header.morph_index_size,
rigid_body: header.rigidbody_index_size,
},
additional_uv_count: header.extra_uv_count,
},
geometry,
materials,
skeleton,
morphs,
display_frames,
rigid_bodies,
joints,
soft_bodies,
diagnostics,
})
}
pub fn export_pmx_model(model: &PmxParsedModel) -> Vec<u8> {
let encoding = pmx_text_encoding(&model.metadata);
let mut out = Vec::new();
out.extend_from_slice(&PMX_MAGIC);
write_f32(&mut out, model.metadata.version);
out.push(8);
out.push(encoding as u8);
out.push(model.metadata.additional_uv_count);
out.push(model.metadata.index_sizes.vertex);
out.push(model.metadata.index_sizes.texture);
out.push(model.metadata.index_sizes.material);
out.push(model.metadata.index_sizes.bone);
out.push(model.metadata.index_sizes.morph);
out.push(model.metadata.index_sizes.rigid_body);
write_pmx_string(&mut out, &model.metadata.name, encoding);
write_pmx_string(&mut out, &model.metadata.english_name, encoding);
write_pmx_string(&mut out, &model.metadata.comment, encoding);
write_pmx_string(&mut out, &model.metadata.english_comment, encoding);
let vertex_count = model.geometry.positions.len() / 3;
write_i32(&mut out, vertex_count as i32);
for index in 0..vertex_count {
write_f32_slice(
&mut out,
&model.geometry.positions[index * 3..index * 3 + 3],
);
write_f32_slice(&mut out, &model.geometry.normals[index * 3..index * 3 + 3]);
write_f32_slice(&mut out, &model.geometry.uvs[index * 2..index * 2 + 2]);
for uv in &model.geometry.additional_uvs {
write_f32_slice(&mut out, &uv[index * 4..index * 4 + 4]);
}
write_pmx_vertex_skinning(&mut out, model, index);
write_f32(
&mut out,
model.geometry.edge_scale.get(index).copied().unwrap_or(1.0),
);
}
write_i32(&mut out, model.geometry.indices.len() as i32);
for &index in &model.geometry.indices {
write_vertex_index(&mut out, index, model.metadata.index_sizes.vertex);
}
let textures = collect_pmx_textures(&model.materials);
write_i32(&mut out, textures.len() as i32);
for texture in &textures {
write_pmx_string(&mut out, texture, encoding);
}
write_i32(&mut out, model.materials.len() as i32);
for material in &model.materials {
write_pmx_string(&mut out, &material.name, encoding);
write_pmx_string(&mut out, &material.english_name, encoding);
write_f32_slice(&mut out, &material.diffuse);
write_f32_slice(&mut out, &material.specular);
write_f32(&mut out, material.specular_power);
write_f32_slice(&mut out, &material.ambient);
out.push(material_flag_bits(&material.flags));
write_f32_slice(&mut out, &material.edge_color);
write_f32(&mut out, material.edge_size);
write_sized_index(
&mut out,
texture_index(&textures, &material.texture_path),
model.metadata.index_sizes.texture,
);
write_sized_index(
&mut out,
texture_index(&textures, &material.sphere_texture_path),
model.metadata.index_sizes.texture,
);
out.push(match material.sphere_mode.as_str() {
"multiply" => 1,
"add" => 2,
"subTexture" => 3,
_ => 0,
});
if let Some(shared) = material.shared_toon_index {
out.push(1);
out.push(shared);
} else {
out.push(0);
write_sized_index(
&mut out,
texture_index(&textures, &material.toon_texture_path),
model.metadata.index_sizes.texture,
);
}
write_pmx_string(&mut out, "", encoding);
write_i32(&mut out, material.face_count.saturating_mul(3));
}
write_i32(&mut out, model.skeleton.bones.len() as i32);
for bone in &model.skeleton.bones {
write_pmx_string(&mut out, &bone.name, encoding);
write_pmx_string(&mut out, &bone.english_name, encoding);
write_f32_slice(&mut out, &bone.position);
write_sized_index(&mut out, bone.parent_index, model.metadata.index_sizes.bone);
write_i32(&mut out, bone.layer);
let bits = bone_flag_bits(&bone.flags);
write_u16(&mut out, bits);
if bone.flags.indexed_tail {
write_sized_index(&mut out, bone.tail_index, model.metadata.index_sizes.bone);
} else {
write_f32_slice(&mut out, &bone.tail_position.unwrap_or([0.0; 3]));
}
if bone.flags.append_rotate || bone.flags.append_translate {
let append = bone.append_transform.as_ref();
write_sized_index(
&mut out,
append.map(|value| value.parent_index).unwrap_or(-1),
model.metadata.index_sizes.bone,
);
write_f32(&mut out, append.map(|value| value.weight).unwrap_or(0.0));
}
if bone.flags.fixed_axis {
write_f32_slice(&mut out, &bone.fixed_axis.unwrap_or([0.0; 3]));
}
if bone.flags.local_axis {
if let Some(axis) = &bone.local_axis {
write_f32_slice(&mut out, &axis.x);
write_f32_slice(&mut out, &axis.z);
} else {
write_f32_slice(&mut out, &[1.0, 0.0, 0.0]);
write_f32_slice(&mut out, &[0.0, 0.0, 1.0]);
}
}
if bone.flags.external_parent_transform {
write_i32(&mut out, bone.external_parent_key.unwrap_or(0));
}
if bone.flags.ik {
if let Some(ik) = &bone.ik {
write_sized_index(&mut out, ik.target_index, model.metadata.index_sizes.bone);
write_i32(&mut out, ik.loop_count);
write_f32(&mut out, ik.limit_angle);
write_i32(&mut out, ik.links.len() as i32);
for link in &ik.links {
write_sized_index(&mut out, link.bone_index, model.metadata.index_sizes.bone);
out.push(u8::from(link.limits.is_some()));
if let Some(limits) = &link.limits {
write_f32_slice(&mut out, &limits.lower);
write_f32_slice(&mut out, &limits.upper);
}
}
} else {
write_sized_index(&mut out, -1, model.metadata.index_sizes.bone);
write_i32(&mut out, 0);
write_f32(&mut out, 0.0);
write_i32(&mut out, 0);
}
}
}
write_i32(&mut out, model.morphs.len() as i32);
for morph in &model.morphs {
write_pmx_string(&mut out, &morph.name, encoding);
write_pmx_string(&mut out, &morph.english_name, encoding);
out.push(0);
let morph_type = morph_type_byte(morph);
out.push(morph_type);
write_i32(&mut out, morph_offset_count(morph, morph_type) as i32);
match morph_type {
0 => write_group_morph_offsets(&mut out, &morph.group_offsets, model),
1 => {
for offset in &morph.vertex_offsets {
write_vertex_index(
&mut out,
offset.vertex_index,
model.metadata.index_sizes.vertex,
);
write_f32_slice(&mut out, &offset.position);
}
}
2 => {
for offset in &morph.bone_offsets {
write_sized_index(&mut out, offset.bone_index, model.metadata.index_sizes.bone);
write_f32_slice(&mut out, &offset.translation);
write_f32_slice(&mut out, &offset.rotation);
}
}
3 => write_uv_morph_offsets(&mut out, &morph.uv_offsets, model),
4..=7 => {
for offset in &morph.additional_uv_offsets {
write_vertex_index(
&mut out,
offset.vertex_index,
model.metadata.index_sizes.vertex,
);
write_f32_slice(&mut out, &offset.uv);
}
}
8 => {
for offset in &morph.material_offsets {
write_sized_index(
&mut out,
offset.material_index,
model.metadata.index_sizes.material,
);
out.push(if offset.operation == "multiply" { 0 } else { 1 });
write_f32_slice(&mut out, &offset.diffuse);
write_f32_slice(&mut out, &offset.specular);
write_f32(&mut out, offset.specular_power);
write_f32_slice(&mut out, &offset.ambient);
write_f32_slice(&mut out, &offset.edge_color);
write_f32(&mut out, offset.edge_size);
write_f32_slice(&mut out, &offset.texture_factor);
write_f32_slice(&mut out, &offset.sphere_texture_factor);
write_f32_slice(&mut out, &offset.toon_texture_factor);
}
}
9 => write_group_morph_offsets(&mut out, &morph.flip_offsets, model),
10 => {
for offset in &morph.impulse_offsets {
write_sized_index(
&mut out,
offset.rigid_body_index,
model.metadata.index_sizes.rigid_body,
);
out.push(u8::from(offset.local));
write_f32_slice(&mut out, &offset.velocity);
write_f32_slice(&mut out, &offset.torque);
}
}
_ => {}
}
}
write_i32(&mut out, model.display_frames.len() as i32);
for frame in &model.display_frames {
write_pmx_string(&mut out, &frame.name, encoding);
write_pmx_string(&mut out, &frame.english_name, encoding);
out.push(u8::from(frame.special));
write_i32(&mut out, frame.frames.len() as i32);
for item in &frame.frames {
if item.kind == "morph" {
out.push(1);
write_sized_index(&mut out, item.index, model.metadata.index_sizes.morph);
} else {
out.push(0);
write_sized_index(&mut out, item.index, model.metadata.index_sizes.bone);
}
}
}
write_i32(&mut out, model.rigid_bodies.len() as i32);
for body in &model.rigid_bodies {
write_pmx_string(&mut out, &body.name, encoding);
write_pmx_string(&mut out, &body.english_name, encoding);
write_sized_index(&mut out, body.bone_index, model.metadata.index_sizes.bone);
out.push(body.group);
write_u16(&mut out, body.mask);
out.push(match body.shape.as_str() {
"box" => 1,
"capsule" => 2,
_ => 0,
});
write_f32_slice(&mut out, &body.size);
write_f32_slice(&mut out, &body.position);
write_f32_slice(&mut out, &body.rotation);
write_f32(&mut out, body.mass);
write_f32(&mut out, body.linear_damping);
write_f32(&mut out, body.angular_damping);
write_f32(&mut out, body.restitution);
write_f32(&mut out, body.friction);
out.push(match body.mode.as_str() {
"dynamic" => 1,
"dynamicBone" => 2,
_ => 0,
});
}
write_i32(&mut out, model.joints.len() as i32);
for joint in &model.joints {
write_pmx_string(&mut out, &joint.name, encoding);
write_pmx_string(&mut out, &joint.english_name, encoding);
out.push(match joint.kind.as_str() {
"generic6dof" => 1,
"point2point" => 2,
"coneTwist" => 3,
"slider" => 4,
"hinge" => 5,
_ => 0,
});
write_sized_index(
&mut out,
joint.rigid_body_index_a,
model.metadata.index_sizes.rigid_body,
);
write_sized_index(
&mut out,
joint.rigid_body_index_b,
model.metadata.index_sizes.rigid_body,
);
write_f32_slice(&mut out, &joint.position);
write_f32_slice(&mut out, &joint.rotation);
write_f32_slice(&mut out, &joint.translation_lower_limit);
write_f32_slice(&mut out, &joint.translation_upper_limit);
write_f32_slice(&mut out, &joint.rotation_lower_limit);
write_f32_slice(&mut out, &joint.rotation_upper_limit);
write_f32_slice(&mut out, &joint.spring_translation_factor);
write_f32_slice(&mut out, &joint.spring_rotation_factor);
}
if model.metadata.version >= 2.05 {
write_i32(&mut out, model.soft_bodies.len() as i32);
for soft_body in &model.soft_bodies {
write_pmx_string(&mut out, &soft_body.name, encoding);
write_pmx_string(&mut out, &soft_body.english_name, encoding);
out.push(if soft_body.kind == "rope" { 1 } else { 0 });
write_sized_index(
&mut out,
soft_body.material_index,
model.metadata.index_sizes.material,
);
out.push(soft_body.collision_group);
write_u16(&mut out, soft_body.collision_mask);
out.push(soft_body.flags);
write_i32(&mut out, soft_body.bending_constraints_distance);
write_i32(&mut out, soft_body.cluster_count);
write_f32(&mut out, soft_body.total_mass);
write_f32(&mut out, soft_body.collision_margin);
out.extend_from_slice(&[0u8; 4 + 12 * 4 + 6 * 4 + 4 * 4 + 3 * 4]);
write_i32(&mut out, 0);
write_i32(&mut out, 0);
}
}
out
}
fn read_section_count(r: &mut Reader<'_>) -> Result<usize, ImportError> {
let count = r.read_i32_le()?;
if count < 0 {
return Err(ImportError::SectionOverflow);
}
Ok(count as usize)
}
fn read_section_count_with_min_record(
r: &mut Reader<'_>,
min_record_size: usize,
) -> Result<usize, ImportError> {
let count = read_section_count(r)?;
r.require_record_bytes(count, min_record_size)?;
Ok(count)
}
fn read_parsed_geometry(
r: &mut Reader<'_>,
header: &PmxHeader,
vertex_count: usize,
) -> Result<PmxParsedGeometry, ImportError> {
r.require_record_bytes(vertex_count, 12 + 12 + 8 + 1 + 4)?;
let position_capacity = vertex_count
.checked_mul(3)
.ok_or(ImportError::SectionOverflow)?;
let uv_capacity = vertex_count
.checked_mul(2)
.ok_or(ImportError::SectionOverflow)?;
let skin_capacity = vertex_count
.checked_mul(4)
.ok_or(ImportError::SectionOverflow)?;
let mut positions = Vec::with_capacity(position_capacity);
let mut normals = Vec::with_capacity(position_capacity);
let mut uvs = Vec::with_capacity(uv_capacity);
let mut additional_uvs =
vec![Vec::with_capacity(skin_capacity); header.extra_uv_count as usize];
let mut skin_indices = Vec::with_capacity(skin_capacity);
let mut skin_weights = Vec::with_capacity(skin_capacity);
let mut skinning_modes = Vec::with_capacity(vertex_count);
let mut edge_scale = Vec::with_capacity(vertex_count);
let mut sdef_enabled = vec![0.0f32; vertex_count];
let mut sdef_c = vec![0.0f32; vertex_count * 3];
let mut sdef_r0 = vec![0.0f32; vertex_count * 3];
let mut sdef_r1 = vec![0.0f32; vertex_count * 3];
let mut sdef_rw0 = vec![0.0f32; vertex_count * 3];
let mut sdef_rw1 = vec![0.0f32; vertex_count * 3];
let mut qdef_enabled = vec![0.0f32; vertex_count];
for vertex_i in 0..vertex_count {
positions.extend_from_slice(&r.read_vec3_array()?);
normals.extend_from_slice(&r.read_vec3_array()?);
uvs.extend_from_slice(&r.read_vec2_array()?);
for uv in additional_uvs.iter_mut() {
uv.extend_from_slice(&r.read_vec4_array()?);
}
let weight_type = r.read_u8()?;
let mut indices = [0u32; 4];
let mut weights = [0.0f32; 4];
match weight_type {
0 => {
skinning_modes.push(PMX_SKINNING_MODE_BDEF1.to_owned());
indices[0] =
normalize_nonnegative_index(r.read_sized_index(header.bone_index_size)?);
weights[0] = 1.0;
}
1 | 3 => {
skinning_modes.push(
if weight_type == 3 {
PMX_SKINNING_MODE_SDEF
} else {
PMX_SKINNING_MODE_BDEF2
}
.to_owned(),
);
indices[0] =
normalize_nonnegative_index(r.read_sized_index(header.bone_index_size)?);
indices[1] =
normalize_nonnegative_index(r.read_sized_index(header.bone_index_size)?);
let w = r.read_f32_le()?;
weights[0] = w;
weights[1] = 1.0 - w;
if weight_type == 3 {
let c = r.read_vec3_array()?;
let r0 = r.read_vec3_array()?;
let r1 = r.read_vec3_array()?;
let w1 = 1.0 - w;
let offset = vertex_i * 3;
sdef_enabled[vertex_i] = 1.0;
sdef_c[offset..offset + 3].copy_from_slice(&c);
sdef_r0[offset..offset + 3].copy_from_slice(&r0);
sdef_r1[offset..offset + 3].copy_from_slice(&r1);
for k in 0..3 {
let rw = r0[k] * w + r1[k] * w1;
sdef_rw0[offset + k] = (c[k] * 2.0 + r0[k] - rw) * 0.5;
sdef_rw1[offset + k] = (c[k] * 2.0 + r1[k] - rw) * 0.5;
}
}
}
2 => {
skinning_modes.push(PMX_SKINNING_MODE_BDEF4.to_owned());
for index in &mut indices {
*index =
normalize_nonnegative_index(r.read_sized_index(header.bone_index_size)?);
}
for weight in &mut weights {
*weight = r.read_f32_le()?;
}
}
4 => {
skinning_modes.push(PMX_SKINNING_MODE_QDEF.to_owned());
for index in &mut indices {
*index =
normalize_nonnegative_index(r.read_sized_index(header.bone_index_size)?);
}
for weight in &mut weights {
*weight = r.read_f32_le()?;
}
qdef_enabled[vertex_i] = 1.0;
}
_ => return Err(ImportError::SectionOverflow),
}
skin_indices.extend_from_slice(&indices);
skin_weights.extend_from_slice(&weights);
edge_scale.push(r.read_f32_le()?);
}
let index_count = read_section_count_with_min_record(r, header.vertex_index_size as usize)?;
let mut indices = Vec::with_capacity(index_count);
for _ in 0..index_count {
indices.push(r.read_vertex_index(header.vertex_index_size)?);
}
Ok(PmxParsedGeometry {
positions,
normals,
uvs,
additional_uvs,
indices,
skin_indices,
skin_weights,
edge_scale,
material_groups: Vec::new(),
sdef: PmxParsedSdef {
skinning_modes,
enabled: sdef_enabled,
c: sdef_c,
r0: sdef_r0,
r1: sdef_r1,
rw0: sdef_rw0,
rw1: sdef_rw1,
},
qdef: PmxParsedQdef {
enabled: qdef_enabled,
},
})
}
fn normalize_nonnegative_index(index: i32) -> u32 {
if index < 0 { 0 } else { index as u32 }
}
pub fn parse_pmx_material_split(
data: &[u8],
_flags: u32,
) -> Result<PmxMaterialSplitResult, ImportError> {
let model = parse_pmx_model(data)?;
Ok(split_pmx_model_by_material(&model))
}
pub fn split_pmx_model_by_material(model: &PmxParsedModel) -> PmxMaterialSplitResult {
let mut meshes = Vec::with_capacity(model.geometry.material_groups.len());
let mut manifest_meshes = Vec::with_capacity(model.geometry.material_groups.len());
let mut diagnostics = Vec::new();
for (mesh_index, group) in model.geometry.material_groups.iter().enumerate() {
if group.material_index >= model.materials.len() {
let diagnostic = pmx_material_split_diagnostic(
"PMX_MATERIAL_SPLIT_MATERIAL_INDEX_OUT_OF_RANGE",
format!(
"material group {mesh_index} references material {} but material count is {}",
group.material_index,
model.materials.len()
),
);
diagnostics.push(diagnostic);
continue;
}
let mesh = split_pmx_material_group(model, group, mesh_index);
manifest_meshes.push(PmxMaterialSplitManifestMesh {
mesh_index,
original_material_index: mesh.original_material_index,
original_vertex_indices: mesh.original_vertex_indices.clone(),
morph_index_map: mesh.morph_index_map.clone(),
diagnostics: mesh.diagnostics.clone(),
});
diagnostics.extend(mesh.diagnostics.iter().cloned());
meshes.push(mesh);
}
PmxMaterialSplitResult {
manifest: PmxMaterialSplitManifest {
mesh_count: meshes.len(),
meshes: manifest_meshes,
diagnostics,
},
meshes,
}
}
fn split_pmx_material_group(
model: &PmxParsedModel,
group: &PmxParsedMaterialGroup,
mesh_index: usize,
) -> PmxMaterialSplitMesh {
let mut diagnostics = Vec::new();
let index_end = group.start.saturating_add(group.count);
let index_slice = match model.geometry.indices.get(group.start..index_end) {
Some(slice) => slice,
None => {
diagnostics.push(pmx_material_split_diagnostic(
"PMX_MATERIAL_SPLIT_INDEX_RANGE_OUT_OF_RANGE",
format!(
"material group {mesh_index} index range {}..{} exceeds index count {}",
group.start,
index_end,
model.geometry.indices.len()
),
));
&[][..]
}
};
let vertex_count = model.geometry.positions.len() / 3;
let mut original_vertex_indices = Vec::new();
let mut vertex_remap = HashMap::<u32, u32>::new();
let mut local_indices = Vec::with_capacity(index_slice.len());
for &original_index in index_slice {
if original_index as usize >= vertex_count {
diagnostics.push(pmx_material_split_diagnostic(
"PMX_MATERIAL_SPLIT_VERTEX_INDEX_OUT_OF_RANGE",
format!(
"material group {mesh_index} references vertex {original_index} but vertex count is {vertex_count}"
),
));
continue;
}
let local_index = match vertex_remap.get(&original_index) {
Some(&local_index) => local_index,
None => {
let local_index = original_vertex_indices.len() as u32;
vertex_remap.insert(original_index, local_index);
original_vertex_indices.push(original_index);
local_index
}
};
local_indices.push(local_index);
}
let local_index_count = local_indices.len();
let geometry = remap_pmx_geometry(
&model.geometry,
&original_vertex_indices,
local_indices,
local_index_count,
);
let material = {
let mut material = model.materials[group.material_index].clone();
material.face_count = (geometry.indices.len() / 3) as i32;
material
};
let (morphs, morph_index_map, morph_diagnostics) =
remap_pmx_material_split_morphs(model, &vertex_remap, group.material_index, mesh_index);
diagnostics.extend(morph_diagnostics);
PmxMaterialSplitMesh {
original_material_index: group.material_index,
original_vertex_indices,
geometry,
material,
morphs,
morph_index_map,
diagnostics,
}
}
fn remap_pmx_geometry(
geometry: &PmxParsedGeometry,
original_vertex_indices: &[u32],
indices: Vec<u32>,
local_index_count: usize,
) -> PmxParsedGeometry {
PmxParsedGeometry {
positions: gather_vertex_f32(&geometry.positions, 3, original_vertex_indices),
normals: gather_vertex_f32(&geometry.normals, 3, original_vertex_indices),
uvs: gather_vertex_f32(&geometry.uvs, 2, original_vertex_indices),
additional_uvs: geometry
.additional_uvs
.iter()
.map(|uvs| gather_vertex_f32(uvs, 4, original_vertex_indices))
.collect(),
indices,
skin_indices: gather_vertex_u32(&geometry.skin_indices, 4, original_vertex_indices),
skin_weights: gather_vertex_f32(&geometry.skin_weights, 4, original_vertex_indices),
edge_scale: gather_vertex_f32(&geometry.edge_scale, 1, original_vertex_indices),
material_groups: vec![PmxParsedMaterialGroup {
start: 0,
count: local_index_count,
material_index: 0,
}],
sdef: PmxParsedSdef {
skinning_modes: gather_vertex_string(
&geometry.sdef.skinning_modes,
original_vertex_indices,
),
enabled: gather_vertex_f32(&geometry.sdef.enabled, 1, original_vertex_indices),
c: gather_vertex_f32(&geometry.sdef.c, 3, original_vertex_indices),
r0: gather_vertex_f32(&geometry.sdef.r0, 3, original_vertex_indices),
r1: gather_vertex_f32(&geometry.sdef.r1, 3, original_vertex_indices),
rw0: gather_vertex_f32(&geometry.sdef.rw0, 3, original_vertex_indices),
rw1: gather_vertex_f32(&geometry.sdef.rw1, 3, original_vertex_indices),
},
qdef: PmxParsedQdef {
enabled: gather_vertex_f32(&geometry.qdef.enabled, 1, original_vertex_indices),
},
}
}
fn gather_vertex_f32(values: &[f32], stride: usize, original_vertex_indices: &[u32]) -> Vec<f32> {
let mut out = Vec::with_capacity(original_vertex_indices.len() * stride);
for &index in original_vertex_indices {
let start = index as usize * stride;
let end = start + stride;
if let Some(slice) = values.get(start..end) {
out.extend_from_slice(slice);
}
}
out
}
fn gather_vertex_string(values: &[String], original_vertex_indices: &[u32]) -> Vec<String> {
let mut out = Vec::with_capacity(original_vertex_indices.len());
for &index in original_vertex_indices {
if let Some(value) = values.get(index as usize) {
out.push(value.clone());
}
}
out
}
fn gather_vertex_u32(values: &[u32], stride: usize, original_vertex_indices: &[u32]) -> Vec<u32> {
let mut out = Vec::with_capacity(original_vertex_indices.len() * stride);
for &index in original_vertex_indices {
let start = index as usize * stride;
let end = start + stride;
if let Some(slice) = values.get(start..end) {
out.extend_from_slice(slice);
}
}
out
}
fn remap_pmx_material_split_morphs(
model: &PmxParsedModel,
vertex_remap: &HashMap<u32, u32>,
original_material_index: usize,
mesh_index: usize,
) -> (
Vec<PmxParsedMorph>,
Vec<PmxMaterialSplitMorphIndexMap>,
Vec<PmxParserDiagnostic>,
) {
let mut diagnostics = Vec::new();
let mut direct_morphs = vec![None; model.morphs.len()];
let mut survives = vec![false; model.morphs.len()];
let mut morph_remap = HashMap::<usize, usize>::new();
for (original_index, morph) in model.morphs.iter().enumerate() {
match morph.kind.as_str() {
"vertex" | "uv" | "additionalUv" | "material" => {
let remapped = remap_direct_pmx_material_split_morph(
morph,
vertex_remap,
original_material_index,
);
if pmx_material_split_morph_has_offsets(&remapped) {
direct_morphs[original_index] = Some(remapped);
survives[original_index] = true;
}
}
"bone" => {
if !morph.bone_offsets.is_empty() {
diagnostics.push(pmx_material_split_diagnostic(
"PMX_MATERIAL_SPLIT_BONE_MORPH_SHARED_SKELETON",
format!(
"mesh {mesh_index} excludes bone morph {original_index} because skeleton morphs are shared"
),
));
}
}
"impulse" => {
if !morph.impulse_offsets.is_empty() {
diagnostics.push(pmx_material_split_diagnostic(
"PMX_MATERIAL_SPLIT_IMPULSE_MORPH_UNSUPPORTED",
format!(
"mesh {mesh_index} excludes impulse morph {original_index} because physics morphs are not exposed in the initial material split ABI"
),
));
}
}
"group" | "flip" => {}
_ => {
diagnostics.push(pmx_material_split_diagnostic(
"PMX_MATERIAL_SPLIT_MORPH_KIND_UNSUPPORTED",
format!(
"mesh {mesh_index} excludes morph {original_index} with unsupported kind {}",
morph.kind
),
));
}
}
}
let mut changed = true;
while changed {
changed = false;
for (original_index, morph) in model.morphs.iter().enumerate() {
if survives[original_index] || (morph.kind != "group" && morph.kind != "flip") {
continue;
}
if pmx_material_split_reference_offsets(morph)
.iter()
.any(|offset| {
offset.morph_index >= 0
&& survives
.get(offset.morph_index as usize)
.copied()
.unwrap_or(false)
})
{
survives[original_index] = true;
changed = true;
}
}
}
for (original_index, survives) in survives.iter().copied().enumerate() {
if survives {
let local_index = morph_remap.len();
morph_remap.insert(original_index, local_index);
}
}
let mut morphs = Vec::with_capacity(morph_remap.len());
for (original_index, morph) in model.morphs.iter().enumerate() {
if !morph_remap.contains_key(&original_index) {
continue;
}
let remapped = match direct_morphs[original_index].take() {
Some(remapped) => remapped,
None => remap_reference_pmx_material_split_morph(morph, &morph_remap),
};
morphs.push((original_index, remapped));
}
let morph_index_map = morphs
.iter()
.enumerate()
.map(
|(local_index, (original_index, _))| PmxMaterialSplitMorphIndexMap {
original_index: *original_index,
local_index,
},
)
.collect();
let morphs = morphs
.into_iter()
.map(|(_, morph)| morph)
.collect::<Vec<_>>();
(morphs, morph_index_map, diagnostics)
}
fn remap_direct_pmx_material_split_morph(
morph: &PmxParsedMorph,
vertex_remap: &HashMap<u32, u32>,
original_material_index: usize,
) -> PmxParsedMorph {
let mut remapped = empty_pmx_material_split_morph_like(morph);
remapped.vertex_offsets = morph
.vertex_offsets
.iter()
.filter_map(|offset| {
vertex_remap
.get(&offset.vertex_index)
.map(|&vertex_index| PmxParsedVertexMorphOffset {
vertex_index,
position: offset.position,
})
})
.collect();
remapped.uv_offsets = morph
.uv_offsets
.iter()
.filter_map(|offset| {
vertex_remap
.get(&offset.vertex_index)
.map(|&vertex_index| PmxParsedUvMorphOffset {
vertex_index,
uv: offset.uv,
})
})
.collect();
remapped.additional_uv_offsets = morph
.additional_uv_offsets
.iter()
.filter_map(|offset| {
vertex_remap.get(&offset.vertex_index).map(|&vertex_index| {
PmxParsedAdditionalUvMorphOffset {
vertex_index,
uv_index: offset.uv_index,
uv: offset.uv,
}
})
})
.collect();
remapped.material_offsets = morph
.material_offsets
.iter()
.filter_map(|offset| {
if offset.material_index == -1
|| offset.material_index as usize == original_material_index
{
let mut remapped = offset.clone();
if remapped.material_index >= 0 {
remapped.material_index = 0;
}
Some(remapped)
} else {
None
}
})
.collect();
remapped
}
fn remap_reference_pmx_material_split_morph(
morph: &PmxParsedMorph,
morph_remap: &HashMap<usize, usize>,
) -> PmxParsedMorph {
let mut remapped = empty_pmx_material_split_morph_like(morph);
if morph.kind == "group" {
remapped.group_offsets =
remap_pmx_material_split_morph_refs(&morph.group_offsets, morph_remap);
} else {
remapped.flip_offsets =
remap_pmx_material_split_morph_refs(&morph.flip_offsets, morph_remap);
}
remapped
}
fn pmx_material_split_reference_offsets(morph: &PmxParsedMorph) -> &[PmxParsedGroupMorphOffset] {
if morph.kind == "group" {
&morph.group_offsets
} else {
&morph.flip_offsets
}
}
fn remap_pmx_material_split_morph_refs(
offsets: &[PmxParsedGroupMorphOffset],
morph_remap: &HashMap<usize, usize>,
) -> Vec<PmxParsedGroupMorphOffset> {
offsets
.iter()
.filter_map(|offset| {
if offset.morph_index < 0 {
return None;
}
morph_remap
.get(&(offset.morph_index as usize))
.map(|&morph_index| PmxParsedGroupMorphOffset {
morph_index: morph_index as i32,
weight: offset.weight,
})
})
.collect()
}
fn empty_pmx_material_split_morph_like(morph: &PmxParsedMorph) -> PmxParsedMorph {
PmxParsedMorph {
name: morph.name.clone(),
english_name: morph.english_name.clone(),
kind: morph.kind.clone(),
vertex_offsets: Vec::new(),
group_offsets: Vec::new(),
bone_offsets: Vec::new(),
uv_offsets: Vec::new(),
additional_uv_offsets: Vec::new(),
material_offsets: Vec::new(),
flip_offsets: Vec::new(),
impulse_offsets: Vec::new(),
}
}
fn pmx_material_split_morph_has_offsets(morph: &PmxParsedMorph) -> bool {
!morph.vertex_offsets.is_empty()
|| !morph.group_offsets.is_empty()
|| !morph.bone_offsets.is_empty()
|| !morph.uv_offsets.is_empty()
|| !morph.additional_uv_offsets.is_empty()
|| !morph.material_offsets.is_empty()
|| !morph.flip_offsets.is_empty()
|| !morph.impulse_offsets.is_empty()
}
fn pmx_material_split_diagnostic(
code: impl Into<String>,
message: impl Into<String>,
) -> PmxParserDiagnostic {
PmxParserDiagnostic {
level: "info".to_owned(),
code: code.into(),
message: message.into(),
}
}
fn read_parsed_textures(
r: &mut Reader<'_>,
encoding: TextEncoding,
) -> Result<Vec<String>, ImportError> {
let count = read_section_count_with_min_record(r, 4)?;
let mut textures = Vec::with_capacity(count);
for _ in 0..count {
textures.push(r.read_string(encoding)?);
}
Ok(textures)
}
fn read_parsed_materials(
r: &mut Reader<'_>,
header: &PmxHeader,
textures: &[String],
) -> Result<Vec<PmxParsedMaterial>, ImportError> {
let count = read_section_count_with_min_record(r, 4 + 4 + 16 + 16 + 12 + 1 + 16 + 4)?;
let mut materials = Vec::with_capacity(count);
for _ in 0..count {
let name = r.read_string(header.encoding)?;
let english_name = r.read_string(header.encoding)?;
let diffuse = r.read_vec4_array()?;
let specular = r.read_vec3_array()?;
let specular_power = r.read_f32_le()?;
let ambient = r.read_vec3_array()?;
let flag_bits = r.read_u8()?;
let edge_color = r.read_vec4_array()?;
let edge_size = r.read_f32_le()?;
let texture_index = r.read_sized_index(header.texture_index_size)?;
let sphere_texture_index = r.read_sized_index(header.texture_index_size)?;
let sphere_mode_raw = r.read_u8()?;
let toon_flag = r.read_u8()?;
let (toon_texture_path, shared_toon_index) = if toon_flag == 0 {
let toon_texture_index = r.read_sized_index(header.texture_index_size)?;
(texture_path(textures, toon_texture_index), None)
} else {
(String::new(), Some(r.read_u8()?))
};
let _memo = r.read_string(header.encoding)?;
let face_count = r.read_i32_le()? / 3;
materials.push(PmxParsedMaterial {
name,
english_name,
texture_path: texture_path(textures, texture_index),
sphere_texture_path: texture_path(textures, sphere_texture_index),
sphere_mode: match sphere_mode_raw {
1 => "multiply",
2 => "add",
3 => "subTexture",
_ => "none",
}
.to_owned(),
toon_texture_path,
shared_toon_index,
diffuse,
specular,
specular_power,
ambient,
edge_color,
edge_size,
flags: PmxParsedMaterialFlags {
double_sided: flag_bits & 0x01 != 0,
ground_shadow: flag_bits & 0x02 != 0,
self_shadow_map: flag_bits & 0x04 != 0,
self_shadow: flag_bits & 0x08 != 0,
edge: flag_bits & 0x10 != 0,
vertex_color: flag_bits & 0x20 != 0,
point_draw: flag_bits & 0x40 != 0,
line_draw: flag_bits & 0x80 != 0,
},
face_count,
});
}
Ok(materials)
}
fn texture_path(textures: &[String], index: i32) -> String {
if index < 0 {
String::new()
} else {
textures.get(index as usize).cloned().unwrap_or_default()
}
}
fn build_material_groups(materials: &[PmxParsedMaterial]) -> Vec<PmxParsedMaterialGroup> {
let mut start = 0usize;
materials
.iter()
.enumerate()
.map(|(material_index, material)| {
let count = (material.face_count.max(0) as usize) * 3;
let group = PmxParsedMaterialGroup {
start,
count,
material_index,
};
start += count;
group
})
.collect()
}
fn pmx_text_encoding(metadata: &PmxParsedMetadata) -> TextEncoding {
match metadata.encoding.as_str() {
"utf-16-le" => TextEncoding::Utf16Le,
_ => TextEncoding::Utf8,
}
}
fn write_pmx_string(out: &mut Vec<u8>, text: &str, encoding: TextEncoding) {
match encoding {
TextEncoding::Utf8 => {
write_i32(out, text.len() as i32);
out.extend_from_slice(text.as_bytes());
}
TextEncoding::Utf16Le => {
let bytes = text
.encode_utf16()
.flat_map(u16::to_le_bytes)
.collect::<Vec<_>>();
write_i32(out, bytes.len() as i32);
out.extend_from_slice(&bytes);
}
}
}
fn write_sized_index(out: &mut Vec<u8>, value: i32, size: u8) {
match size {
1 => out.push(value as i8 as u8),
2 => out.extend_from_slice(&(value as i16).to_le_bytes()),
4 => out.extend_from_slice(&value.to_le_bytes()),
_ => {}
}
}
fn write_vertex_index(out: &mut Vec<u8>, value: u32, size: u8) {
match size {
1 => out.push(value as u8),
2 => out.extend_from_slice(&(value as u16).to_le_bytes()),
4 => out.extend_from_slice(&(value as i32).to_le_bytes()),
_ => {}
}
}
fn write_pmx_vertex_skinning(out: &mut Vec<u8>, model: &PmxParsedModel, vertex_index: usize) {
match pmx_vertex_skinning_mode(&model.geometry, vertex_index) {
PMX_SKINNING_MODE_BDEF1 => {
out.push(0);
write_pmx_vertex_bone_index(out, model, vertex_index, 0);
}
PMX_SKINNING_MODE_BDEF2 => {
out.push(1);
write_pmx_vertex_bone_index(out, model, vertex_index, 0);
write_pmx_vertex_bone_index(out, model, vertex_index, 1);
write_f32(
out,
pmx_vertex_skin_weight(&model.geometry, vertex_index, 0),
);
}
PMX_SKINNING_MODE_SDEF => {
out.push(3);
write_pmx_vertex_bone_index(out, model, vertex_index, 0);
write_pmx_vertex_bone_index(out, model, vertex_index, 1);
write_f32(
out,
pmx_vertex_skin_weight(&model.geometry, vertex_index, 0),
);
write_f32_slice(
out,
&pmx_vertex_sdef_vec3(&model.geometry.sdef.c, vertex_index),
);
write_f32_slice(
out,
&pmx_vertex_sdef_vec3(&model.geometry.sdef.r0, vertex_index),
);
write_f32_slice(
out,
&pmx_vertex_sdef_vec3(&model.geometry.sdef.r1, vertex_index),
);
}
PMX_SKINNING_MODE_QDEF => {
out.push(4);
write_pmx_vertex_bdef4_skinning(out, model, vertex_index);
}
_ => {
out.push(2);
write_pmx_vertex_bdef4_skinning(out, model, vertex_index);
}
}
}
fn pmx_vertex_skinning_mode(geometry: &PmxParsedGeometry, vertex_index: usize) -> &str {
if let Some(mode) = geometry.sdef.skinning_modes.get(vertex_index) {
return mode.as_str();
}
if geometry
.sdef
.enabled
.get(vertex_index)
.copied()
.unwrap_or(0.0)
> 0.5
{
PMX_SKINNING_MODE_SDEF
} else if geometry
.qdef
.enabled
.get(vertex_index)
.copied()
.unwrap_or(0.0)
> 0.5
{
PMX_SKINNING_MODE_QDEF
} else {
PMX_SKINNING_MODE_BDEF4
}
}
fn write_pmx_vertex_bdef4_skinning(out: &mut Vec<u8>, model: &PmxParsedModel, vertex_index: usize) {
for slot in 0..4 {
write_pmx_vertex_bone_index(out, model, vertex_index, slot);
}
for slot in 0..4 {
write_f32(
out,
pmx_vertex_skin_weight(&model.geometry, vertex_index, slot),
);
}
}
fn write_pmx_vertex_bone_index(
out: &mut Vec<u8>,
model: &PmxParsedModel,
vertex_index: usize,
slot: usize,
) {
write_sized_index(
out,
model
.geometry
.skin_indices
.get(vertex_index * 4 + slot)
.copied()
.unwrap_or(0) as i32,
model.metadata.index_sizes.bone,
);
}
fn pmx_vertex_skin_weight(geometry: &PmxParsedGeometry, vertex_index: usize, slot: usize) -> f32 {
geometry
.skin_weights
.get(vertex_index * 4 + slot)
.copied()
.unwrap_or(if slot == 0 { 1.0 } else { 0.0 })
}
fn pmx_vertex_sdef_vec3(values: &[f32], vertex_index: usize) -> [f32; 3] {
let start = vertex_index * 3;
[
values.get(start).copied().unwrap_or(0.0),
values.get(start + 1).copied().unwrap_or(0.0),
values.get(start + 2).copied().unwrap_or(0.0),
]
}
fn material_flag_bits(flags: &PmxParsedMaterialFlags) -> u8 {
u8::from(flags.double_sided)
| (u8::from(flags.ground_shadow) << 1)
| (u8::from(flags.self_shadow_map) << 2)
| (u8::from(flags.self_shadow) << 3)
| (u8::from(flags.edge) << 4)
| (u8::from(flags.vertex_color) << 5)
| (u8::from(flags.point_draw) << 6)
| (u8::from(flags.line_draw) << 7)
}
fn bone_flag_bits(flags: &PmxParsedBoneFlags) -> u16 {
u16::from(flags.indexed_tail)
| (u16::from(flags.rotatable) << 1)
| (u16::from(flags.translatable) << 2)
| (u16::from(flags.visible) << 3)
| (u16::from(flags.enabled) << 4)
| if flags.ik { BONE_FLAG_IK } else { 0 }
| if flags.append_local {
BONE_FLAG_LOCAL_APPEND
} else {
0
}
| if flags.append_rotate {
BONE_FLAG_APPEND_ROTATE
} else {
0
}
| if flags.append_translate {
BONE_FLAG_APPEND_TRANSLATE
} else {
0
}
| if flags.fixed_axis {
BONE_FLAG_FIXED_AXIS
} else {
0
}
| if flags.local_axis {
BONE_FLAG_LOCAL_AXIS
} else {
0
}
| if flags.transform_after_physics {
BONE_FLAG_TRANSFORM_AFTER_PHYSICS
} else {
0
}
| if flags.external_parent_transform {
BONE_FLAG_EXTERNAL_PARENT
} else {
0
}
}
fn collect_pmx_textures(materials: &[PmxParsedMaterial]) -> Vec<String> {
let mut textures = Vec::new();
for texture in materials.iter().flat_map(|material| {
[
material.texture_path.as_str(),
material.sphere_texture_path.as_str(),
material.toon_texture_path.as_str(),
]
}) {
if !texture.is_empty() && !textures.iter().any(|existing| existing == texture) {
textures.push(texture.to_owned());
}
}
textures
}
fn texture_index(textures: &[String], texture: &str) -> i32 {
if texture.is_empty() {
-1
} else {
textures
.iter()
.position(|candidate| candidate == texture)
.map(|index| index as i32)
.unwrap_or(-1)
}
}
fn morph_type_byte(morph: &PmxParsedMorph) -> u8 {
match morph.kind.as_str() {
"group" => 0,
"vertex" => 1,
"bone" => 2,
"uv" => 3,
"additionalUv" => morph
.additional_uv_offsets
.first()
.map(|offset| offset.uv_index.saturating_add(4).min(7))
.unwrap_or(4),
"material" => 8,
"flip" => 9,
"impulse" => 10,
_ => 0,
}
}
fn morph_offset_count(morph: &PmxParsedMorph, morph_type: u8) -> usize {
match morph_type {
0 => morph.group_offsets.len(),
1 => morph.vertex_offsets.len(),
2 => morph.bone_offsets.len(),
3 => morph.uv_offsets.len(),
4..=7 => morph.additional_uv_offsets.len(),
8 => morph.material_offsets.len(),
9 => morph.flip_offsets.len(),
10 => morph.impulse_offsets.len(),
_ => 0,
}
}
fn write_group_morph_offsets(
out: &mut Vec<u8>,
offsets: &[PmxParsedGroupMorphOffset],
model: &PmxParsedModel,
) {
for offset in offsets {
write_sized_index(out, offset.morph_index, model.metadata.index_sizes.morph);
write_f32(out, offset.weight);
}
}
fn write_uv_morph_offsets(
out: &mut Vec<u8>,
offsets: &[PmxParsedUvMorphOffset],
model: &PmxParsedModel,
) {
for offset in offsets {
write_vertex_index(out, offset.vertex_index, model.metadata.index_sizes.vertex);
write_f32_slice(out, &offset.uv);
}
}
fn read_parsed_bones(
r: &mut Reader<'_>,
header: &PmxHeader,
count: usize,
) -> Result<Vec<PmxParsedBone>, ImportError> {
r.require_record_bytes(count, 4 + 4 + 12)?;
let mut bones = Vec::with_capacity(count);
for _ in 0..count {
let name = r.read_string(header.encoding)?;
let english_name = r.read_string(header.encoding)?;
let position = r.read_vec3_array()?;
let parent_index = r.read_sized_index(header.bone_index_size)?;
let layer = r.read_i32_le()?;
let bits = r.read_u16_le()?;
let flags = parsed_bone_flags(bits);
let (tail_index, tail_position) = if flags.indexed_tail {
(r.read_sized_index(header.bone_index_size)?, None)
} else {
(-1, Some(r.read_vec3_array()?))
};
let append_transform = if flags.append_rotate || flags.append_translate {
Some(PmxParsedAppendTransform {
parent_index: r.read_sized_index(header.bone_index_size)?,
weight: r.read_f32_le()?,
})
} else {
None
};
let fixed_axis = if flags.fixed_axis {
Some(r.read_vec3_array()?)
} else {
None
};
let local_axis = if flags.local_axis {
Some(PmxParsedLocalAxis {
x: r.read_vec3_array()?,
z: r.read_vec3_array()?,
})
} else {
None
};
let external_parent_key = if flags.external_parent_transform {
Some(r.read_i32_le()?)
} else {
None
};
let ik = if flags.ik {
let target_index = r.read_sized_index(header.bone_index_size)?;
let loop_count = r.read_i32_le()?;
let limit_angle = r.read_f32_le()?;
let link_count =
read_section_count_with_min_record(r, header.bone_index_size as usize + 1)?;
let mut links = Vec::with_capacity(link_count);
for _ in 0..link_count {
let bone_index = r.read_sized_index(header.bone_index_size)?;
let has_limit = r.read_u8()? != 0;
links.push(PmxParsedIkLink {
bone_index,
limits: if has_limit {
Some(PmxParsedIkLimit {
lower: r.read_vec3_array()?,
upper: r.read_vec3_array()?,
})
} else {
None
},
});
}
Some(PmxParsedIk {
target_index,
loop_count,
limit_angle,
links,
})
} else {
None
};
bones.push(PmxParsedBone {
name,
english_name,
parent_index,
layer,
position,
tail_index,
tail_position,
flags,
append_transform,
fixed_axis,
local_axis,
external_parent_key,
ik,
});
}
Ok(bones)
}
fn parsed_bone_flags(bits: u16) -> PmxParsedBoneFlags {
PmxParsedBoneFlags {
indexed_tail: bits & BONE_FLAG_TAIL_INDEX != 0,
rotatable: bits & 0x0002 != 0,
translatable: bits & 0x0004 != 0,
visible: bits & 0x0008 != 0,
enabled: bits & 0x0010 != 0,
ik: bits & BONE_FLAG_IK != 0,
append_local: bits & BONE_FLAG_LOCAL_APPEND != 0,
append_rotate: bits & BONE_FLAG_APPEND_ROTATE != 0,
append_translate: bits & BONE_FLAG_APPEND_TRANSLATE != 0,
fixed_axis: bits & BONE_FLAG_FIXED_AXIS != 0,
local_axis: bits & BONE_FLAG_LOCAL_AXIS != 0,
transform_after_physics: bits & 0x1000 != 0,
external_parent_transform: bits & BONE_FLAG_EXTERNAL_PARENT != 0,
}
}
fn read_parsed_morphs(
r: &mut Reader<'_>,
header: &PmxHeader,
) -> Result<Vec<PmxParsedMorph>, ImportError> {
let count = read_section_count_with_min_record(r, 4 + 4 + 1 + 1 + 4)?;
let mut morphs = Vec::with_capacity(count);
for _ in 0..count {
let name = r.read_string(header.encoding)?;
let english_name = r.read_string(header.encoding)?;
let _panel = r.read_u8()?;
let morph_type = r.read_u8()?;
let min_offset_size = match morph_type {
0 => header.morph_index_size as usize + 4,
1 => header.vertex_index_size as usize + 12,
2 => header.bone_index_size as usize + 12 + 16,
3..=7 => header.vertex_index_size as usize + 16,
8 => header.material_index_size as usize + 1 + 16 + 12 + 4 + 12 + 16 + 4 + 16 + 16 + 16,
9 => header.morph_index_size as usize + 4,
10 => header.rigidbody_index_size as usize + 1 + 12 + 12,
_ => return Err(ImportError::SectionOverflow),
};
let offset_count = read_section_count_with_min_record(r, min_offset_size)?;
let mut morph = PmxParsedMorph {
name,
english_name,
kind: match morph_type {
0 => "group",
1 => "vertex",
2 => "bone",
3 => "uv",
4..=7 => "additionalUv",
8 => "material",
9 => "flip",
10 => "impulse",
_ => "unknown",
}
.to_owned(),
vertex_offsets: Vec::new(),
group_offsets: Vec::new(),
bone_offsets: Vec::new(),
uv_offsets: Vec::new(),
additional_uv_offsets: Vec::new(),
material_offsets: Vec::new(),
flip_offsets: Vec::new(),
impulse_offsets: Vec::new(),
};
for _ in 0..offset_count {
match morph_type {
0 => morph.group_offsets.push(PmxParsedGroupMorphOffset {
morph_index: r.read_sized_index(header.morph_index_size)?,
weight: r.read_f32_le()?,
}),
1 => morph.vertex_offsets.push(PmxParsedVertexMorphOffset {
vertex_index: r.read_vertex_index(header.vertex_index_size)?,
position: r.read_vec3_array()?,
}),
2 => morph.bone_offsets.push(PmxParsedBoneMorphOffset {
bone_index: r.read_sized_index(header.bone_index_size)?,
translation: r.read_vec3_array()?,
rotation: r.read_vec4_array()?,
}),
3 => morph.uv_offsets.push(PmxParsedUvMorphOffset {
vertex_index: r.read_vertex_index(header.vertex_index_size)?,
uv: r.read_vec4_array()?,
}),
4..=7 => morph
.additional_uv_offsets
.push(PmxParsedAdditionalUvMorphOffset {
vertex_index: r.read_vertex_index(header.vertex_index_size)?,
uv_index: morph_type - 4,
uv: r.read_vec4_array()?,
}),
8 => morph.material_offsets.push(PmxParsedMaterialMorphOffset {
material_index: r.read_sized_index(header.material_index_size)?,
operation: if r.read_u8()? == 0 {
"multiply".to_owned()
} else {
"add".to_owned()
},
diffuse: r.read_vec4_array()?,
specular: r.read_vec3_array()?,
specular_power: r.read_f32_le()?,
ambient: r.read_vec3_array()?,
edge_color: r.read_vec4_array()?,
edge_size: r.read_f32_le()?,
texture_factor: r.read_vec4_array()?,
sphere_texture_factor: r.read_vec4_array()?,
toon_texture_factor: r.read_vec4_array()?,
}),
9 => morph.flip_offsets.push(PmxParsedGroupMorphOffset {
morph_index: r.read_sized_index(header.morph_index_size)?,
weight: r.read_f32_le()?,
}),
10 => morph.impulse_offsets.push(PmxParsedImpulseMorphOffset {
rigid_body_index: r.read_sized_index(header.rigidbody_index_size)?,
local: r.read_u8()? != 0,
velocity: r.read_vec3_array()?,
torque: r.read_vec3_array()?,
}),
_ => return Err(ImportError::SectionOverflow),
}
}
morphs.push(morph);
}
Ok(morphs)
}
fn read_parsed_display_frames(
r: &mut Reader<'_>,
header: &PmxHeader,
) -> Result<Vec<PmxParsedDisplayFrame>, ImportError> {
let count = read_section_count_with_min_record(r, 4 + 4 + 1 + 4)?;
let mut frames = Vec::with_capacity(count);
for _ in 0..count {
let name = r.read_string(header.encoding)?;
let english_name = r.read_string(header.encoding)?;
let special = r.read_u8()? == 1;
let item_count = read_section_count_with_min_record(r, 1)?;
let mut items = Vec::with_capacity(item_count);
for _ in 0..item_count {
let kind = r.read_u8()?;
let index = match kind {
0 => r.read_sized_index(header.bone_index_size)?,
1 => r.read_sized_index(header.morph_index_size)?,
_ => return Err(ImportError::SectionOverflow),
};
items.push(PmxParsedDisplayFrameElement {
kind: if kind == 0 {
"bone".to_owned()
} else {
"morph".to_owned()
},
index,
});
}
frames.push(PmxParsedDisplayFrame {
name,
english_name,
special,
frames: items,
});
}
Ok(frames)
}
fn read_parsed_rigid_bodies(
r: &mut Reader<'_>,
header: &PmxHeader,
) -> Result<Vec<PmxParsedRigidBody>, ImportError> {
let count = read_section_count_with_min_record(
r,
4 + 4 + header.bone_index_size as usize + 1 + 2 + 1 + 12 + 12 + 12 + 4 + 4 + 4 + 4 + 4 + 1,
)?;
let mut bodies = Vec::with_capacity(count);
for _ in 0..count {
bodies.push(PmxParsedRigidBody {
name: r.read_string(header.encoding)?,
english_name: r.read_string(header.encoding)?,
bone_index: r.read_sized_index(header.bone_index_size)?,
group: r.read_u8()?,
mask: r.read_u16_le()?,
shape: match r.read_u8()? {
0 => "sphere",
1 => "box",
2 => "capsule",
_ => "unknown",
}
.to_owned(),
size: r.read_vec3_array()?,
position: r.read_vec3_array()?,
rotation: r.read_vec3_array()?,
mass: r.read_f32_le()?,
linear_damping: r.read_f32_le()?,
angular_damping: r.read_f32_le()?,
restitution: r.read_f32_le()?,
friction: r.read_f32_le()?,
mode: match r.read_u8()? {
0 => "static",
1 => "dynamic",
2 => "dynamicBone",
_ => "unknown",
}
.to_owned(),
});
}
Ok(bodies)
}
fn read_parsed_joints(
r: &mut Reader<'_>,
header: &PmxHeader,
) -> Result<Vec<PmxParsedJoint>, ImportError> {
let count = read_section_count_with_min_record(
r,
4 + 4 + 1 + header.rigidbody_index_size as usize * 2 + 12 * 8,
)?;
let mut joints = Vec::with_capacity(count);
for _ in 0..count {
joints.push(PmxParsedJoint {
name: r.read_string(header.encoding)?,
english_name: r.read_string(header.encoding)?,
kind: match r.read_u8()? {
0 => "generic6dofSpring",
1 => "generic6dof",
2 => "point2point",
3 => "coneTwist",
4 => "slider",
5 => "hinge",
_ => "unknown",
}
.to_owned(),
rigid_body_index_a: r.read_sized_index(header.rigidbody_index_size)?,
rigid_body_index_b: r.read_sized_index(header.rigidbody_index_size)?,
position: r.read_vec3_array()?,
rotation: r.read_vec3_array()?,
translation_lower_limit: r.read_vec3_array()?,
translation_upper_limit: r.read_vec3_array()?,
rotation_lower_limit: r.read_vec3_array()?,
rotation_upper_limit: r.read_vec3_array()?,
spring_translation_factor: r.read_vec3_array()?,
spring_rotation_factor: r.read_vec3_array()?,
});
}
Ok(joints)
}
fn read_parsed_soft_bodies(
r: &mut Reader<'_>,
header: &PmxHeader,
) -> Result<Vec<PmxParsedSoftBody>, ImportError> {
let count = read_section_count_with_min_record(
r,
4 + 4
+ 1
+ header.material_index_size as usize
+ 1
+ 2
+ 1
+ 4
+ 4
+ 4
+ 4
+ 4
+ 12 * 4
+ 6 * 4
+ 4 * 4
+ 3 * 4
+ 4
+ 4,
)?;
let mut soft_bodies = Vec::with_capacity(count);
for _ in 0..count {
let name = r.read_string(header.encoding)?;
let english_name = r.read_string(header.encoding)?;
let kind = match r.read_u8()? {
0 => "triMesh",
1 => "rope",
_ => "unknown",
}
.to_owned();
let material_index = r.read_sized_index(header.material_index_size)?;
let collision_group = r.read_u8()?;
let collision_mask = r.read_u16_le()?;
let flags = r.read_u8()?;
let bending_constraints_distance = r.read_i32_le()?;
let cluster_count = r.read_i32_le()?;
let total_mass = r.read_f32_le()?;
let collision_margin = r.read_f32_le()?;
r.skip(4 + 12 * 4 + 6 * 4 + 4 * 4 + 3 * 4)?;
let anchor_count = read_section_count_with_min_record(
r,
header.rigidbody_index_size as usize + header.vertex_index_size as usize + 1,
)?;
for _ in 0..anchor_count {
r.read_sized_index(header.rigidbody_index_size)?;
r.read_vertex_index(header.vertex_index_size)?;
r.skip(1)?;
}
let pin_count = read_section_count_with_min_record(r, header.vertex_index_size as usize)?;
for _ in 0..pin_count {
r.read_vertex_index(header.vertex_index_size)?;
}
soft_bodies.push(PmxParsedSoftBody {
name,
english_name,
kind,
material_index,
collision_group,
collision_mask,
flags,
bending_constraints_distance,
cluster_count,
total_mass,
collision_margin,
});
}
Ok(soft_bodies)
}
#[derive(Debug)]
pub struct PmxRuntimeImport {
pub model: ModelArena,
pub bone_names: Vec<String>,
pub bone_name_to_index: HashMap<Vec<u8>, BoneIndex>,
pub morph_name_to_index: HashMap<Vec<u8>, MorphIndex>,
pub ik_solver_bone_name_to_index: HashMap<Vec<u8>, usize>,
}
pub fn import_pmx_runtime(data: &[u8]) -> Result<PmxRuntimeImport, ImportError> {
let (header, pos) = read_header(data)?;
let pos = skip_model_info(data, &header, pos)?;
let pos = skip_vertices(data, &header, pos)?;
let pos = skip_faces(data, header.vertex_index_size, pos)?;
let pos = skip_textures(data, header.encoding, pos)?;
let pos = skip_materials(data, &header, pos)?;
let (bone_import, pos) = read_bones(data, &header, pos)?;
let (morph_names, morph_init, _pos) = read_morph_offsets(data, &header, pos)?;
let mut bone_name_to_index = HashMap::with_capacity(bone_import.bone_name_bytes.len() * 2);
for (i, bytes) in bone_import.bone_name_bytes.iter().enumerate() {
let index = BoneIndex(i as u32);
bone_name_to_index.insert(bytes.clone(), index);
let decoded = bone_import.bone_names[i].as_bytes();
if decoded != bytes.as_slice() && !decoded.is_empty() {
bone_name_to_index.insert(decoded.to_vec(), index);
}
}
let mut morph_name_to_index = HashMap::with_capacity(morph_names.name_bytes.len() * 2);
for (i, bytes) in morph_names.name_bytes.iter().enumerate() {
let index = MorphIndex(i as u32);
morph_name_to_index.insert(bytes.clone(), index);
let decoded = morph_names.names[i].as_bytes();
if decoded != bytes.as_slice() && !decoded.is_empty() {
morph_name_to_index.insert(decoded.to_vec(), index);
}
}
let mut ik_solver_bone_name_to_index = HashMap::with_capacity(bone_import.ik_solvers.len() * 2);
for (solver_idx, solver) in bone_import.ik_solvers.iter().enumerate() {
let bone_idx = solver.ik_bone.as_usize();
if bone_idx < bone_import.bone_name_bytes.len() {
let bytes = &bone_import.bone_name_bytes[bone_idx];
ik_solver_bone_name_to_index.insert(bytes.clone(), solver_idx);
let decoded = bone_import.bone_names[bone_idx].as_bytes();
if decoded != bytes.as_slice() && !decoded.is_empty() {
ik_solver_bone_name_to_index.insert(decoded.to_vec(), solver_idx);
}
}
}
let model = ModelArena::new_with_morphs(
bone_import.bones,
bone_import.ik_solvers,
bone_import.append_transforms,
morph_init,
)
.map_err(ImportError::ModelBuildFailed)?;
Ok(PmxRuntimeImport {
model,
bone_names: bone_import.bone_names,
bone_name_to_index,
morph_name_to_index,
ik_solver_bone_name_to_index,
})
}
trait AppendTransformInitExt {
fn with_rotation_if(self, on: bool) -> Self;
fn with_translation_if(self, on: bool) -> Self;
fn with_local_if(self, on: bool) -> Self;
}
impl AppendTransformInitExt for AppendTransformInit {
fn with_rotation_if(mut self, on: bool) -> Self {
if on {
self.affect_rotation = true;
}
self
}
fn with_translation_if(mut self, on: bool) -> Self {
if on {
self.affect_translation = true;
}
self
}
fn with_local_if(mut self, on: bool) -> Self {
if on {
self.local = true;
}
self
}
}
#[cfg(test)]
mod tests {
use super::*;
use glam::Vec3;
use mmd_anim_runtime::RuntimeInstance;
use std::sync::Arc;
fn build_small_pmx_header_bytes(bone_index_size: u8, encoding: TextEncoding) -> Vec<u8> {
let mut buf = Vec::new();
buf.extend_from_slice(b"PMX ");
buf.extend_from_slice(&2.0f32.to_le_bytes());
buf.push(8);
buf.push(encoding as u8);
buf.push(0);
buf.push(4);
buf.push(1);
buf.push(1);
buf.push(bone_index_size);
buf.push(1);
buf.push(1);
buf
}
fn build_empty_model_info(_encoding: TextEncoding) -> Vec<u8> {
let mut buf = Vec::new();
for _ in 0..4 {
buf.extend_from_slice(&0i32.to_le_bytes());
}
buf
}
fn build_empty_vertex_section() -> Vec<u8> {
vec![0u8, 0, 0, 0]
}
fn build_empty_face_section() -> Vec<u8> {
vec![0u8, 0, 0, 0]
}
fn build_empty_texture_section() -> Vec<u8> {
vec![0u8, 0, 0, 0]
}
fn build_empty_material_section() -> Vec<u8> {
vec![0u8, 0, 0, 0]
}
#[test]
fn rejects_impossible_pmx_vertex_count_before_allocation() {
let mut buf = build_small_pmx_header_bytes(1, TextEncoding::Utf8);
buf.extend_from_slice(&build_empty_model_info(TextEncoding::Utf8));
buf.extend_from_slice(&i32::MAX.to_le_bytes());
assert!(matches!(
parse_pmx_model(&buf),
Err(ImportError::UnexpectedEof(_))
));
assert!(matches!(
import_pmx_runtime(&buf),
Err(ImportError::UnexpectedEof(_))
));
}
#[test]
fn rejects_invalid_pmx_index_sizes_in_header() {
let buf = build_small_pmx_header_bytes(3, TextEncoding::Utf8);
assert!(matches!(
read_header(&buf),
Err(ImportError::InvalidIndexSize(3))
));
}
fn build_bone_name_bytes(name: &str) -> Vec<u8> {
let mut buf = Vec::new();
let name_bytes = name.as_bytes();
buf.extend_from_slice(&(name_bytes.len() as i32).to_le_bytes());
buf.extend_from_slice(name_bytes);
buf
}
fn build_bone_section_header(count: u32) -> Vec<u8> {
let mut buf = Vec::new();
buf.extend_from_slice(&count.to_le_bytes());
buf
}
fn build_morph_name_bytes(name: &str) -> Vec<u8> {
let mut buf = Vec::new();
let name_bytes = name.as_bytes();
buf.extend_from_slice(&(name_bytes.len() as i32).to_le_bytes());
buf.extend_from_slice(name_bytes);
buf
}
fn empty_pmx_flags() -> PmxParsedMaterialFlags {
PmxParsedMaterialFlags {
double_sided: false,
ground_shadow: false,
self_shadow_map: false,
self_shadow: false,
edge: false,
vertex_color: false,
point_draw: false,
line_draw: false,
}
}
fn basic_bone_flags(indexed_tail: bool) -> PmxParsedBoneFlags {
PmxParsedBoneFlags {
indexed_tail,
rotatable: true,
translatable: false,
visible: true,
enabled: true,
ik: false,
append_local: false,
append_rotate: false,
append_translate: false,
fixed_axis: false,
local_axis: false,
transform_after_physics: false,
external_parent_transform: false,
}
}
fn parsed_pmx_fixture() -> PmxParsedModel {
PmxParsedModel {
metadata: PmxParsedMetadata {
format: "pmx".to_owned(),
version: 2.0,
encoding: "utf-8".to_owned(),
name: "model".to_owned(),
english_name: "model-en".to_owned(),
comment: "comment".to_owned(),
english_comment: "comment-en".to_owned(),
counts: PmxParsedCounts {
vertices: 1,
faces: 1,
materials: 1,
bones: 1,
morphs: 1,
display_frames: 1,
rigid_bodies: 1,
joints: 1,
soft_bodies: 0,
},
index_sizes: PmxParsedIndexSizes {
vertex: 4,
texture: 1,
material: 1,
bone: 2,
morph: 1,
rigid_body: 1,
},
additional_uv_count: 1,
},
geometry: PmxParsedGeometry {
positions: vec![1.0, 2.0, 3.0],
normals: vec![0.0, 1.0, 0.0],
uvs: vec![0.25, 0.75],
additional_uvs: vec![vec![1.0, 2.0, 3.0, 4.0]],
indices: vec![0, 0, 0],
skin_indices: vec![0, 0, 0, 0],
skin_weights: vec![1.0, 0.0, 0.0, 0.0],
edge_scale: vec![1.25],
material_groups: vec![PmxParsedMaterialGroup {
start: 0,
count: 3,
material_index: 0,
}],
sdef: PmxParsedSdef {
skinning_modes: vec![PMX_SKINNING_MODE_BDEF4.to_owned()],
enabled: vec![0.0],
c: vec![0.0, 0.0, 0.0],
r0: vec![0.0, 0.0, 0.0],
r1: vec![0.0, 0.0, 0.0],
rw0: vec![0.0, 0.0, 0.0],
rw1: vec![0.0, 0.0, 0.0],
},
qdef: PmxParsedQdef { enabled: vec![0.0] },
},
materials: vec![PmxParsedMaterial {
name: "mat".to_owned(),
english_name: "mat-en".to_owned(),
texture_path: "tex.png".to_owned(),
sphere_texture_path: "sphere.spa".to_owned(),
sphere_mode: "add".to_owned(),
toon_texture_path: "toon.bmp".to_owned(),
shared_toon_index: None,
diffuse: [0.1, 0.2, 0.3, 0.4],
specular: [0.5, 0.6, 0.7],
specular_power: 8.0,
ambient: [0.8, 0.9, 1.0],
edge_color: [0.0, 0.1, 0.2, 0.3],
edge_size: 1.5,
flags: PmxParsedMaterialFlags {
double_sided: true,
edge: true,
..empty_pmx_flags()
},
face_count: 1,
}],
skeleton: PmxParsedSkeleton {
bones: vec![PmxParsedBone {
name: "Root".to_owned(),
english_name: "RootEn".to_owned(),
parent_index: -1,
layer: 0,
position: [0.0, 1.0, 2.0],
tail_index: -1,
tail_position: Some([0.0, 2.0, 0.0]),
flags: basic_bone_flags(false),
append_transform: None,
fixed_axis: None,
local_axis: None,
external_parent_key: None,
ik: None,
}],
},
morphs: vec![PmxParsedMorph {
name: "Smile".to_owned(),
english_name: "SmileEn".to_owned(),
kind: "vertex".to_owned(),
vertex_offsets: vec![PmxParsedVertexMorphOffset {
vertex_index: 0,
position: [0.1, 0.2, 0.3],
}],
group_offsets: Vec::new(),
bone_offsets: Vec::new(),
uv_offsets: Vec::new(),
additional_uv_offsets: Vec::new(),
material_offsets: Vec::new(),
flip_offsets: Vec::new(),
impulse_offsets: Vec::new(),
}],
display_frames: vec![PmxParsedDisplayFrame {
name: "RootFrame".to_owned(),
english_name: "RootFrameEn".to_owned(),
special: true,
frames: vec![
PmxParsedDisplayFrameElement {
kind: "bone".to_owned(),
index: 0,
},
PmxParsedDisplayFrameElement {
kind: "morph".to_owned(),
index: 0,
},
],
}],
rigid_bodies: vec![PmxParsedRigidBody {
name: "body".to_owned(),
english_name: "body-en".to_owned(),
bone_index: 0,
group: 1,
mask: 2,
shape: "box".to_owned(),
size: [1.0, 2.0, 3.0],
position: [4.0, 5.0, 6.0],
rotation: [0.1, 0.2, 0.3],
mass: 10.0,
linear_damping: 0.4,
angular_damping: 0.5,
restitution: 0.6,
friction: 0.7,
mode: "dynamicBone".to_owned(),
}],
joints: vec![PmxParsedJoint {
name: "joint".to_owned(),
english_name: "joint-en".to_owned(),
kind: "generic6dofSpring".to_owned(),
rigid_body_index_a: 0,
rigid_body_index_b: -1,
position: [1.0, 1.1, 1.2],
rotation: [2.0, 2.1, 2.2],
translation_lower_limit: [-1.0, -1.1, -1.2],
translation_upper_limit: [1.0, 1.1, 1.2],
rotation_lower_limit: [-0.1, -0.2, -0.3],
rotation_upper_limit: [0.1, 0.2, 0.3],
spring_translation_factor: [3.0, 3.1, 3.2],
spring_rotation_factor: [4.0, 4.1, 4.2],
}],
soft_bodies: Vec::new(),
diagnostics: Vec::new(),
}
}
fn material_morph_offset(material_index: i32) -> PmxParsedMaterialMorphOffset {
PmxParsedMaterialMorphOffset {
material_index,
operation: "add".to_owned(),
diffuse: [0.1, 0.2, 0.3, 0.4],
specular: [0.5, 0.6, 0.7],
specular_power: 0.8,
ambient: [0.9, 1.0, 1.1],
edge_color: [0.0, 0.1, 0.2, 0.3],
edge_size: 0.4,
texture_factor: [1.0, 0.0, 0.0, 1.0],
sphere_texture_factor: [0.0, 1.0, 0.0, 1.0],
toon_texture_factor: [0.0, 0.0, 1.0, 1.0],
}
}
fn material_split_fixture() -> PmxParsedModel {
let mut model = parsed_pmx_fixture();
model.metadata.counts.vertices = 4;
model.metadata.counts.faces = 2;
model.metadata.counts.materials = 2;
model.metadata.counts.morphs = 5;
model.metadata.additional_uv_count = 1;
model.geometry = PmxParsedGeometry {
positions: vec![
0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 1.0, 1.0, 0.0,
],
normals: vec![
0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0,
],
uvs: vec![0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 1.0, 1.0],
additional_uvs: vec![vec![
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,
]],
indices: vec![0, 1, 2, 2, 3, 0],
skin_indices: vec![
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
],
skin_weights: vec![
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,
],
edge_scale: vec![1.0, 1.1, 1.2, 1.3],
material_groups: vec![
PmxParsedMaterialGroup {
start: 0,
count: 3,
material_index: 0,
},
PmxParsedMaterialGroup {
start: 3,
count: 3,
material_index: 1,
},
],
sdef: PmxParsedSdef {
skinning_modes: vec![
PMX_SKINNING_MODE_BDEF4.to_owned(),
PMX_SKINNING_MODE_SDEF.to_owned(),
PMX_SKINNING_MODE_BDEF4.to_owned(),
PMX_SKINNING_MODE_QDEF.to_owned(),
],
enabled: vec![0.0, 1.0, 0.0, 0.0],
c: vec![0.0; 12],
r0: vec![0.0; 12],
r1: vec![0.0; 12],
rw0: vec![0.0; 12],
rw1: vec![0.0; 12],
},
qdef: PmxParsedQdef {
enabled: vec![0.0, 0.0, 0.0, 1.0],
},
};
let mut material_b = model.materials[0].clone();
material_b.name = "mat-b".to_owned();
material_b.english_name = "mat-b-en".to_owned();
model.materials = vec![model.materials[0].clone(), material_b];
model.morphs = vec![
PmxParsedMorph {
name: "left-raise".to_owned(),
english_name: "left-raise-en".to_owned(),
kind: "vertex".to_owned(),
vertex_offsets: vec![PmxParsedVertexMorphOffset {
vertex_index: 1,
position: [0.0, 0.5, 0.0],
}],
group_offsets: Vec::new(),
bone_offsets: Vec::new(),
uv_offsets: Vec::new(),
additional_uv_offsets: Vec::new(),
material_offsets: Vec::new(),
flip_offsets: Vec::new(),
impulse_offsets: Vec::new(),
},
PmxParsedMorph {
name: "right-raise".to_owned(),
english_name: "right-raise-en".to_owned(),
kind: "vertex".to_owned(),
vertex_offsets: vec![PmxParsedVertexMorphOffset {
vertex_index: 3,
position: [0.0, 0.75, 0.0],
}],
group_offsets: Vec::new(),
bone_offsets: Vec::new(),
uv_offsets: Vec::new(),
additional_uv_offsets: Vec::new(),
material_offsets: Vec::new(),
flip_offsets: Vec::new(),
impulse_offsets: Vec::new(),
},
PmxParsedMorph {
name: "right-material".to_owned(),
english_name: "right-material-en".to_owned(),
kind: "material".to_owned(),
vertex_offsets: Vec::new(),
group_offsets: Vec::new(),
bone_offsets: Vec::new(),
uv_offsets: Vec::new(),
additional_uv_offsets: Vec::new(),
material_offsets: vec![material_morph_offset(1)],
flip_offsets: Vec::new(),
impulse_offsets: Vec::new(),
},
PmxParsedMorph {
name: "all-material".to_owned(),
english_name: "all-material-en".to_owned(),
kind: "material".to_owned(),
vertex_offsets: Vec::new(),
group_offsets: Vec::new(),
bone_offsets: Vec::new(),
uv_offsets: Vec::new(),
additional_uv_offsets: Vec::new(),
material_offsets: vec![material_morph_offset(-1)],
flip_offsets: Vec::new(),
impulse_offsets: Vec::new(),
},
PmxParsedMorph {
name: "combo".to_owned(),
english_name: "combo-en".to_owned(),
kind: "group".to_owned(),
vertex_offsets: Vec::new(),
group_offsets: vec![
PmxParsedGroupMorphOffset {
morph_index: 0,
weight: 0.25,
},
PmxParsedGroupMorphOffset {
morph_index: 1,
weight: 0.5,
},
PmxParsedGroupMorphOffset {
morph_index: 2,
weight: 0.75,
},
PmxParsedGroupMorphOffset {
morph_index: 3,
weight: 1.0,
},
],
bone_offsets: Vec::new(),
uv_offsets: Vec::new(),
additional_uv_offsets: Vec::new(),
material_offsets: Vec::new(),
flip_offsets: Vec::new(),
impulse_offsets: Vec::new(),
},
];
model
}
fn ik_multi_axis_limit_pmx_fixture() -> &'static [u8] {
include_bytes!("../../fixtures/pmx/ik_multi_axis_limit.pmx")
}
fn assert_pmx_roundtrip_eq(left: &PmxParsedModel, right: &PmxParsedModel) {
assert_eq!(
serde_json::to_value(left).unwrap(),
serde_json::to_value(right).unwrap()
);
}
fn json_keys(value: &serde_json::Value) -> Vec<String> {
let mut keys = value
.as_object()
.unwrap()
.keys()
.map(ToOwned::to_owned)
.collect::<Vec<_>>();
keys.sort();
keys
}
#[test]
fn pmx_material_split_remaps_geometry_and_prunes_morphs() {
let model = material_split_fixture();
let split = split_pmx_model_by_material(&model);
assert_eq!(split.meshes.len(), 2);
assert_eq!(split.manifest.mesh_count, 2);
assert_eq!(split.manifest.meshes[0].original_material_index, 0);
assert_eq!(
split.manifest.meshes[0].original_vertex_indices,
vec![0, 1, 2]
);
assert_eq!(split.manifest.meshes[1].original_material_index, 1);
assert_eq!(
split.manifest.meshes[1].original_vertex_indices,
vec![2, 3, 0]
);
let left = &split.meshes[0];
assert_eq!(left.geometry.indices, vec![0, 1, 2]);
assert_eq!(
left.geometry.positions,
vec![0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0, 0.0]
);
assert_eq!(left.geometry.uvs, vec![0.0, 0.0, 1.0, 0.0, 0.0, 1.0]);
assert_eq!(left.geometry.edge_scale, vec![1.0, 1.1, 1.2]);
assert_eq!(left.geometry.sdef.enabled, vec![0.0, 1.0, 0.0]);
assert_eq!(left.geometry.qdef.enabled, vec![0.0, 0.0, 0.0]);
assert_eq!(
left.morph_index_map,
vec![
PmxMaterialSplitMorphIndexMap {
original_index: 0,
local_index: 0,
},
PmxMaterialSplitMorphIndexMap {
original_index: 3,
local_index: 1,
},
PmxMaterialSplitMorphIndexMap {
original_index: 4,
local_index: 2,
},
]
);
assert_eq!(left.morphs.len(), 3);
assert_eq!(left.morphs[0].name, "left-raise");
assert_eq!(left.morphs[0].vertex_offsets[0].vertex_index, 1);
assert_eq!(left.morphs[1].name, "all-material");
assert_eq!(left.morphs[1].material_offsets[0].material_index, -1);
assert_eq!(left.morphs[2].name, "combo");
assert_eq!(
left.morphs[2]
.group_offsets
.iter()
.map(|offset| (offset.morph_index, offset.weight))
.collect::<Vec<_>>(),
vec![(0, 0.25), (1, 1.0)]
);
let right = &split.meshes[1];
assert_eq!(right.geometry.indices, vec![0, 1, 2]);
assert_eq!(
right.geometry.positions,
vec![0.0, 1.0, 0.0, 1.0, 1.0, 0.0, 0.0, 0.0, 0.0]
);
assert_eq!(right.geometry.uvs, vec![0.0, 1.0, 1.0, 1.0, 0.0, 0.0]);
assert_eq!(right.geometry.edge_scale, vec![1.2, 1.3, 1.0]);
assert_eq!(right.geometry.sdef.enabled, vec![0.0, 0.0, 0.0]);
assert_eq!(right.geometry.qdef.enabled, vec![0.0, 1.0, 0.0]);
assert_eq!(
right.morph_index_map,
vec![
PmxMaterialSplitMorphIndexMap {
original_index: 1,
local_index: 0,
},
PmxMaterialSplitMorphIndexMap {
original_index: 2,
local_index: 1,
},
PmxMaterialSplitMorphIndexMap {
original_index: 3,
local_index: 2,
},
PmxMaterialSplitMorphIndexMap {
original_index: 4,
local_index: 3,
},
]
);
assert_eq!(right.morphs.len(), 4);
assert_eq!(right.morphs[0].name, "right-raise");
assert_eq!(right.morphs[0].vertex_offsets[0].vertex_index, 1);
assert_eq!(right.morphs[1].name, "right-material");
assert_eq!(right.morphs[1].material_offsets[0].material_index, 0);
assert_eq!(right.morphs[2].name, "all-material");
assert_eq!(right.morphs[2].material_offsets[0].material_index, -1);
assert_eq!(
right.morphs[3]
.group_offsets
.iter()
.map(|offset| (offset.morph_index, offset.weight))
.collect::<Vec<_>>(),
vec![(0, 0.5), (1, 0.75), (2, 1.0)]
);
}
#[test]
fn pmx_material_split_preserves_forward_group_morph_references() {
let mut model = material_split_fixture();
model.morphs.push(PmxParsedMorph {
name: "forward-combo".to_owned(),
english_name: "forward-combo-en".to_owned(),
kind: "group".to_owned(),
vertex_offsets: Vec::new(),
group_offsets: vec![PmxParsedGroupMorphOffset {
morph_index: 6,
weight: 0.8,
}],
bone_offsets: Vec::new(),
uv_offsets: Vec::new(),
additional_uv_offsets: Vec::new(),
material_offsets: Vec::new(),
flip_offsets: Vec::new(),
impulse_offsets: Vec::new(),
});
model.morphs.push(PmxParsedMorph {
name: "later-combo".to_owned(),
english_name: "later-combo-en".to_owned(),
kind: "group".to_owned(),
vertex_offsets: Vec::new(),
group_offsets: vec![PmxParsedGroupMorphOffset {
morph_index: 0,
weight: 0.4,
}],
bone_offsets: Vec::new(),
uv_offsets: Vec::new(),
additional_uv_offsets: Vec::new(),
material_offsets: Vec::new(),
flip_offsets: Vec::new(),
impulse_offsets: Vec::new(),
});
model.metadata.counts.morphs = model.morphs.len();
let split = split_pmx_model_by_material(&model);
let left = &split.meshes[0];
assert_eq!(
left.morph_index_map,
vec![
PmxMaterialSplitMorphIndexMap {
original_index: 0,
local_index: 0,
},
PmxMaterialSplitMorphIndexMap {
original_index: 3,
local_index: 1,
},
PmxMaterialSplitMorphIndexMap {
original_index: 4,
local_index: 2,
},
PmxMaterialSplitMorphIndexMap {
original_index: 5,
local_index: 3,
},
PmxMaterialSplitMorphIndexMap {
original_index: 6,
local_index: 4,
},
]
);
assert_eq!(left.morphs[3].name, "forward-combo");
assert_eq!(
left.morphs[3]
.group_offsets
.iter()
.map(|offset| (offset.morph_index, offset.weight))
.collect::<Vec<_>>(),
vec![(4, 0.8)]
);
assert_eq!(left.morphs[4].name, "later-combo");
assert_eq!(
left.morphs[4]
.group_offsets
.iter()
.map(|offset| (offset.morph_index, offset.weight))
.collect::<Vec<_>>(),
vec![(0, 0.4)]
);
}
#[test]
fn pmx_model_json_top_level_schema_is_stable() {
let parsed = parsed_pmx_fixture();
let keys = json_keys(&serde_json::to_value(&parsed).unwrap());
assert_eq!(
keys,
vec![
"diagnostics",
"displayFrames",
"geometry",
"joints",
"materials",
"metadata",
"morphs",
"rigidBodies",
"skeleton",
"softBodies",
]
);
}
#[test]
fn parses_pmx_header() {
let header_bytes = build_small_pmx_header_bytes(4, TextEncoding::Utf8);
let (header, pos) = read_header(&header_bytes).unwrap();
assert_eq!(header.version, 2.0);
assert_eq!(header.encoding, TextEncoding::Utf8);
assert_eq!(header.bone_index_size, 4);
assert_eq!(header.vertex_index_size, 4);
assert!(pos > 0);
}
#[test]
fn pmx_bone_flag_values_match_reference_layout() {
assert_eq!(BONE_FLAG_TAIL_INDEX, 0x0001);
assert_eq!(BONE_FLAG_IK, 0x0020);
assert_eq!(BONE_FLAG_LOCAL_APPEND, 0x0080);
assert_eq!(BONE_FLAG_APPEND_ROTATE, 0x0100);
assert_eq!(BONE_FLAG_APPEND_TRANSLATE, 0x0200);
assert_eq!(BONE_FLAG_FIXED_AXIS, 0x0400);
assert_eq!(BONE_FLAG_LOCAL_AXIS, 0x0800);
assert_eq!(BONE_FLAG_TRANSFORM_AFTER_PHYSICS, 0x1000);
assert_eq!(BONE_FLAG_EXTERNAL_PARENT, 0x2000);
}
#[test]
fn parses_ik_multi_axis_limit_pmx_fixture() {
let parsed = parse_pmx_model(ik_multi_axis_limit_pmx_fixture()).unwrap();
assert_eq!(parsed.metadata.name, "ik_multi_axis_limit_fixture");
assert_eq!(parsed.metadata.counts.bones, 3);
assert_eq!(parsed.skeleton.bones[2].name, "ik_controller");
let ik = parsed.skeleton.bones[2].ik.as_ref().unwrap();
assert_eq!(ik.target_index, 1);
assert_eq!(ik.loop_count, 1);
assert_eq!(ik.links.len(), 1);
assert_eq!(ik.links[0].bone_index, 0);
let limits = ik.links[0].limits.as_ref().unwrap();
assert_eq!(limits.lower, [0.0, -1.0, -1.0]);
assert_eq!(limits.upper, [0.0, 1.0, 1.0]);
let runtime = import_pmx_runtime(ik_multi_axis_limit_pmx_fixture()).unwrap();
assert_eq!(runtime.model.bone_count(), 3);
assert_eq!(runtime.model.ik_count(), 1);
let solver = &runtime.model.ik_solvers()[0];
assert_eq!(solver.ik_bone.as_usize(), 2);
assert_eq!(solver.target_bone.as_usize(), 1);
let limit = solver.links[0].angle_limit.unwrap();
assert_eq!(limit.min.to_array(), [0.0, -1.0, -1.0]);
assert_eq!(limit.max.to_array(), [0.0, 1.0, 1.0]);
}
#[test]
fn rejects_bad_magic() {
let mut header_bytes = build_small_pmx_header_bytes(4, TextEncoding::Utf8);
header_bytes[0] = 0xFF;
assert_eq!(
read_header(&header_bytes).unwrap_err(),
ImportError::InvalidPmxMagic
);
}
#[test]
fn reads_single_bone_no_parent() {
let mut buf = Vec::new();
buf.extend_from_slice(&build_small_pmx_header_bytes(4, TextEncoding::Utf8));
buf.extend_from_slice(&build_empty_model_info(TextEncoding::Utf8));
buf.extend_from_slice(&build_empty_vertex_section());
buf.extend_from_slice(&build_empty_face_section());
buf.extend_from_slice(&build_empty_texture_section());
buf.extend_from_slice(&build_empty_material_section());
buf.extend_from_slice(&build_bone_section_header(1));
buf.extend_from_slice(&build_bone_name_bytes("Root"));
buf.extend_from_slice(&build_bone_name_bytes(""));
buf.extend_from_slice(&0.0f32.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(&(-1i32).to_le_bytes());
buf.extend_from_slice(&0i32.to_le_bytes());
buf.extend_from_slice(&0u16.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());
let result = import_pmx_model(&buf).unwrap();
assert_eq!(result.bones.len(), 1);
assert_eq!(result.bone_names.len(), 1);
assert_eq!(result.bone_names[0], "Root");
assert_eq!(&result.bone_name_bytes[0][..], b"Root");
assert_eq!(result.bones[0].parent, None);
assert!((result.bones[0].rest_position.x - 0.0).abs() < 0.001);
assert!((result.bones[0].rest_position.y - 1.0).abs() < 0.001);
assert!((result.bones[0].rest_position.z - 2.0).abs() < 0.001);
assert!(result.ik_solvers.is_empty());
assert!(result.append_transforms.is_empty());
}
#[test]
fn reads_bone_with_parent() {
let mut buf = Vec::new();
buf.extend_from_slice(&build_small_pmx_header_bytes(2, TextEncoding::Utf8));
buf.extend_from_slice(&build_empty_model_info(TextEncoding::Utf8));
buf.extend_from_slice(&build_empty_vertex_section());
buf.extend_from_slice(&build_empty_face_section());
buf.extend_from_slice(&build_empty_texture_section());
buf.extend_from_slice(&build_empty_material_section());
buf.extend_from_slice(&build_bone_section_header(2));
buf.extend_from_slice(&build_bone_name_bytes("Root"));
buf.extend_from_slice(&build_bone_name_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(&(-1i16).to_le_bytes());
buf.extend_from_slice(&0i32.to_le_bytes());
buf.extend_from_slice(&0u16.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(&build_bone_name_bytes("Child"));
buf.extend_from_slice(&build_bone_name_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(&0i16.to_le_bytes());
buf.extend_from_slice(&0i32.to_le_bytes());
buf.extend_from_slice(&0u16.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());
let result = import_pmx_model(&buf).unwrap();
assert_eq!(result.bones.len(), 2);
assert_eq!(result.bone_names[0], "Root");
assert_eq!(result.bone_names[1], "Child");
assert_eq!(result.bones[0].parent, None);
assert_eq!(result.bones[1].parent, Some(BoneIndex(0)));
}
#[test]
fn reads_bone_with_append_transform() {
let mut buf = Vec::new();
buf.extend_from_slice(&build_small_pmx_header_bytes(2, TextEncoding::Utf8));
buf.extend_from_slice(&build_empty_model_info(TextEncoding::Utf8));
buf.extend_from_slice(&build_empty_vertex_section());
buf.extend_from_slice(&build_empty_face_section());
buf.extend_from_slice(&build_empty_texture_section());
buf.extend_from_slice(&build_empty_material_section());
buf.extend_from_slice(&build_bone_section_header(2));
buf.extend_from_slice(&build_bone_name_bytes("Root"));
buf.extend_from_slice(&build_bone_name_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(&(-1i16).to_le_bytes());
buf.extend_from_slice(&0i32.to_le_bytes());
buf.extend_from_slice(&0u16.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(&build_bone_name_bytes("AppendBone"));
buf.extend_from_slice(&build_bone_name_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(&0i16.to_le_bytes());
buf.extend_from_slice(&0i32.to_le_bytes());
buf.extend_from_slice(
&(BONE_FLAG_LOCAL_APPEND | BONE_FLAG_APPEND_ROTATE | BONE_FLAG_APPEND_TRANSLATE)
.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(&0i16.to_le_bytes());
buf.extend_from_slice(&0.5f32.to_le_bytes());
let result = import_pmx_model(&buf).unwrap();
assert_eq!(result.bones.len(), 2);
assert_eq!(result.append_transforms.len(), 1);
assert_eq!(result.append_transforms[0].target_bone, BoneIndex(1));
assert_eq!(result.append_transforms[0].source_bone, BoneIndex(0));
assert!((result.append_transforms[0].ratio - 0.5).abs() < 0.001);
assert!(result.append_transforms[0].affect_rotation);
assert!(result.append_transforms[0].affect_translation);
assert!(result.append_transforms[0].local);
}
#[test]
fn reads_ik_bone_with_links_and_angle_limits() {
let mut buf = Vec::new();
buf.extend_from_slice(&build_small_pmx_header_bytes(2, TextEncoding::Utf8));
buf.extend_from_slice(&build_empty_model_info(TextEncoding::Utf8));
buf.extend_from_slice(&build_empty_vertex_section());
buf.extend_from_slice(&build_empty_face_section());
buf.extend_from_slice(&build_empty_texture_section());
buf.extend_from_slice(&build_empty_material_section());
buf.extend_from_slice(&build_bone_section_header(3));
buf.extend_from_slice(&build_bone_name_bytes("Root"));
buf.extend_from_slice(&build_bone_name_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(&(-1i16).to_le_bytes());
buf.extend_from_slice(&0i32.to_le_bytes());
buf.extend_from_slice(&0u16.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(&build_bone_name_bytes("IKLink"));
buf.extend_from_slice(&build_bone_name_bytes(""));
buf.extend_from_slice(&2.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(&0i16.to_le_bytes());
buf.extend_from_slice(&0i32.to_le_bytes());
buf.extend_from_slice(&0u16.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(&build_bone_name_bytes("IKBone"));
buf.extend_from_slice(&build_bone_name_bytes(""));
buf.extend_from_slice(&4.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(&0i16.to_le_bytes());
buf.extend_from_slice(&0i32.to_le_bytes());
buf.extend_from_slice(&(BONE_FLAG_TAIL_INDEX | BONE_FLAG_IK).to_le_bytes());
buf.extend_from_slice(&1i16.to_le_bytes());
buf.extend_from_slice(&1i16.to_le_bytes());
buf.extend_from_slice(&10i32.to_le_bytes());
buf.extend_from_slice(&0.5f32.to_le_bytes());
buf.extend_from_slice(&1i32.to_le_bytes());
buf.extend_from_slice(&1i16.to_le_bytes());
buf.push(1u8);
buf.extend_from_slice(&(-1.0f32).to_le_bytes());
buf.extend_from_slice(&(-1.0f32).to_le_bytes());
buf.extend_from_slice(&(-1.0f32).to_le_bytes());
buf.extend_from_slice(&1.0f32.to_le_bytes());
buf.extend_from_slice(&1.0f32.to_le_bytes());
buf.extend_from_slice(&1.0f32.to_le_bytes());
let result = import_pmx_model(&buf).unwrap();
assert_eq!(result.bones.len(), 3);
assert_eq!(result.ik_solvers.len(), 1);
assert_eq!(result.ik_solvers[0].ik_bone, BoneIndex(2));
assert_eq!(result.ik_solvers[0].target_bone, BoneIndex(1));
assert_eq!(result.ik_solvers[0].links.len(), 1);
assert_eq!(result.ik_solvers[0].links[0].bone, BoneIndex(1));
assert!(result.ik_solvers[0].links[0].angle_limit.is_some());
assert_eq!(result.ik_solvers[0].iteration_count, 10);
assert!((result.ik_solvers[0].limit_angle - 0.5).abs() < 0.001);
}
#[test]
fn handles_utf16le_encoding() {
let header_bytes = build_small_pmx_header_bytes(4, TextEncoding::Utf16Le);
let (header, _) = read_header(&header_bytes).unwrap();
assert_eq!(header.encoding, TextEncoding::Utf16Le);
}
#[test]
fn decodes_utf16le_surrogate_pairs() {
let bytes = [0x3D, 0xD8, 0x00, 0xDE, 0x00, 0x00];
assert_eq!(decode_utf16le_lossy(&bytes), "\u{1F600}");
}
#[test]
fn reports_eof_on_truncated_data() {
let buf = vec![0x50, 0x4D, 0x58]; assert!(matches!(
read_header(&buf).unwrap_err(),
ImportError::UnexpectedEof(_)
));
}
#[test]
fn bone_names_retained_and_mapped_to_indices() {
let mut buf = Vec::new();
buf.extend_from_slice(&build_small_pmx_header_bytes(2, TextEncoding::Utf8));
buf.extend_from_slice(&build_empty_model_info(TextEncoding::Utf8));
buf.extend_from_slice(&build_empty_vertex_section());
buf.extend_from_slice(&build_empty_face_section());
buf.extend_from_slice(&build_empty_texture_section());
buf.extend_from_slice(&build_empty_material_section());
buf.extend_from_slice(&build_bone_section_header(2));
buf.extend_from_slice(&build_bone_name_bytes("Hips"));
buf.extend_from_slice(&build_bone_name_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(&(-1i16).to_le_bytes());
buf.extend_from_slice(&0i32.to_le_bytes());
buf.extend_from_slice(&0u16.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(&build_bone_name_bytes("Spine"));
buf.extend_from_slice(&build_bone_name_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(&0i16.to_le_bytes());
buf.extend_from_slice(&0i32.to_le_bytes());
buf.extend_from_slice(&0u16.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());
let result = import_pmx_model(&buf).unwrap();
assert_eq!(result.bone_name_bytes.len(), 2);
assert_eq!(&result.bone_name_bytes[0][..], b"Hips");
assert_eq!(&result.bone_name_bytes[1][..], b"Spine");
assert_eq!(result.bone_names[0], "Hips");
assert_eq!(result.bone_names[1], "Spine");
let bone_map: HashMap<Vec<u8>, BoneIndex> = result
.bone_name_bytes
.iter()
.enumerate()
.map(|(i, b)| (b.clone(), BoneIndex(i as u32)))
.collect();
assert_eq!(bone_map.get(b"Hips".as_slice()), Some(&BoneIndex(0)));
assert_eq!(bone_map.get(b"Spine".as_slice()), Some(&BoneIndex(1)));
assert_eq!(bone_map.get(b"Unknown".as_slice()), None);
}
#[test]
fn ik_solver_name_mapped_to_solver_index() {
let mut buf = Vec::new();
buf.extend_from_slice(&build_small_pmx_header_bytes(2, TextEncoding::Utf8));
buf.extend_from_slice(&build_empty_model_info(TextEncoding::Utf8));
buf.extend_from_slice(&build_empty_vertex_section());
buf.extend_from_slice(&build_empty_face_section());
buf.extend_from_slice(&build_empty_texture_section());
buf.extend_from_slice(&build_empty_material_section());
buf.extend_from_slice(&build_bone_section_header(3));
buf.extend_from_slice(&build_bone_name_bytes("Root"));
buf.extend_from_slice(&build_bone_name_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(&(-1i16).to_le_bytes());
buf.extend_from_slice(&0i32.to_le_bytes());
buf.extend_from_slice(&0u16.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(&build_bone_name_bytes("IKLink"));
buf.extend_from_slice(&build_bone_name_bytes(""));
buf.extend_from_slice(&2.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(&0i16.to_le_bytes());
buf.extend_from_slice(&0i32.to_le_bytes());
buf.extend_from_slice(&0u16.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(&build_bone_name_bytes("LeftLegIK"));
buf.extend_from_slice(&build_bone_name_bytes(""));
buf.extend_from_slice(&4.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(&0i16.to_le_bytes());
buf.extend_from_slice(&0i32.to_le_bytes());
buf.extend_from_slice(&(BONE_FLAG_TAIL_INDEX | BONE_FLAG_IK).to_le_bytes());
buf.extend_from_slice(&1i16.to_le_bytes());
buf.extend_from_slice(&1i16.to_le_bytes());
buf.extend_from_slice(&10i32.to_le_bytes());
buf.extend_from_slice(&0.5f32.to_le_bytes());
buf.extend_from_slice(&1i32.to_le_bytes());
buf.extend_from_slice(&1i16.to_le_bytes());
buf.push(1u8);
buf.extend_from_slice(&(-1.0f32).to_le_bytes());
buf.extend_from_slice(&(-1.0f32).to_le_bytes());
buf.extend_from_slice(&(-1.0f32).to_le_bytes());
buf.extend_from_slice(&1.0f32.to_le_bytes());
buf.extend_from_slice(&1.0f32.to_le_bytes());
buf.extend_from_slice(&1.0f32.to_le_bytes());
let bone_import = import_pmx_model(&buf).unwrap();
assert_eq!(bone_import.ik_solvers.len(), 1);
assert_eq!(bone_import.ik_solvers[0].ik_bone, BoneIndex(2));
assert_eq!(
bone_import.bone_names[bone_import.ik_solvers[0].ik_bone.as_usize()],
"LeftLegIK"
);
let ik_name_map: HashMap<Vec<u8>, usize> = bone_import
.ik_solvers
.iter()
.enumerate()
.filter_map(|(solver_idx, solver)| {
let bone_idx = solver.ik_bone.as_usize();
if bone_idx < bone_import.bone_name_bytes.len() {
Some((bone_import.bone_name_bytes[bone_idx].clone(), solver_idx))
} else {
None
}
})
.collect();
assert_eq!(ik_name_map.get(b"LeftLegIK".as_slice()), Some(&0));
}
#[test]
fn reads_morph_names_without_payloads() {
let (header, pos) =
read_header(&build_small_pmx_header_bytes(4, TextEncoding::Utf8)).unwrap();
let mut buf = vec![0u8; pos];
buf.extend_from_slice(&1i32.to_le_bytes());
buf.extend_from_slice(&build_morph_name_bytes("Smile"));
buf.extend_from_slice(&build_morph_name_bytes(""));
buf.push(4u8);
buf.push(0u8);
buf.extend_from_slice(&0i32.to_le_bytes());
let (morphs, new_pos) = read_morph_names(&buf[..], &header, pos).unwrap();
assert_eq!(morphs.name_bytes.len(), 1);
assert_eq!(&morphs.name_bytes[0][..], b"Smile");
assert_eq!(morphs.names[0], "Smile");
assert!(new_pos > pos);
}
#[test]
fn reads_morph_type_zero_with_vertex_group_offsets() {
let (header, pos) =
read_header(&build_small_pmx_header_bytes(4, TextEncoding::Utf8)).unwrap();
assert_eq!(header.vertex_index_size, 4);
assert_eq!(header.morph_index_size, 1);
let mut buf = vec![0u8; pos];
buf.extend_from_slice(&1i32.to_le_bytes());
buf.extend_from_slice(&build_morph_name_bytes("Blink"));
buf.extend_from_slice(&build_morph_name_bytes(""));
buf.push(0u8);
buf.push(0u8);
buf.extend_from_slice(&2i32.to_le_bytes());
buf.push(0u8);
buf.extend_from_slice(&[0u8; 4]);
buf.push(1u8);
buf.extend_from_slice(&[0u8; 4]);
let (morphs, new_pos) = read_morph_names(&buf[..], &header, pos).unwrap();
assert_eq!(morphs.name_bytes.len(), 1);
assert_eq!(&morphs.name_bytes[0][..], b"Blink");
assert_eq!(morphs.names[0], "Blink");
assert_eq!(new_pos, buf.len());
}
#[test]
fn runtime_import_builds_model_arena_with_name_lookup() {
let mut buf = Vec::new();
buf.extend_from_slice(&build_small_pmx_header_bytes(2, TextEncoding::Utf8));
buf.extend_from_slice(&build_empty_model_info(TextEncoding::Utf8));
buf.extend_from_slice(&build_empty_vertex_section());
buf.extend_from_slice(&build_empty_face_section());
buf.extend_from_slice(&build_empty_texture_section());
buf.extend_from_slice(&build_empty_material_section());
buf.extend_from_slice(&build_bone_section_header(2));
buf.extend_from_slice(&build_bone_name_bytes("Hips"));
buf.extend_from_slice(&build_bone_name_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(&(-1i16).to_le_bytes());
buf.extend_from_slice(&0i32.to_le_bytes());
buf.extend_from_slice(&0u16.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(&build_bone_name_bytes("Spine"));
buf.extend_from_slice(&build_bone_name_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(&0i16.to_le_bytes());
buf.extend_from_slice(&0i32.to_le_bytes());
buf.extend_from_slice(&0u16.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(&0i32.to_le_bytes());
let rt = import_pmx_runtime(&buf).unwrap();
assert_eq!(rt.model.bone_count(), 2);
assert_eq!(rt.model.parent_index(BoneIndex(1)), Some(BoneIndex(0)));
assert_eq!(
rt.bone_name_to_index.get(b"Hips".as_slice()),
Some(&BoneIndex(0))
);
assert_eq!(
rt.bone_name_to_index.get(b"Spine".as_slice()),
Some(&BoneIndex(1))
);
assert!(rt.morph_name_to_index.is_empty());
assert!(rt.ik_solver_bone_name_to_index.is_empty());
}
#[test]
fn parse_pmx_model_exposes_three_loader_shaped_sections() {
let mut buf = Vec::new();
buf.extend_from_slice(&build_small_pmx_header_bytes(2, TextEncoding::Utf8));
buf.extend_from_slice(&build_empty_model_info(TextEncoding::Utf8));
buf.extend_from_slice(&build_empty_vertex_section());
buf.extend_from_slice(&build_empty_face_section());
buf.extend_from_slice(&build_empty_texture_section());
buf.extend_from_slice(&build_empty_material_section());
buf.extend_from_slice(&build_bone_section_header(2));
buf.extend_from_slice(&build_bone_name_bytes("Root"));
buf.extend_from_slice(&build_bone_name_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(&(-1i16).to_le_bytes());
buf.extend_from_slice(&0i32.to_le_bytes());
buf.extend_from_slice(&0u16.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(&build_bone_name_bytes("Child"));
buf.extend_from_slice(&build_bone_name_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(&0i16.to_le_bytes());
buf.extend_from_slice(&0i32.to_le_bytes());
buf.extend_from_slice(&0u16.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(&0i32.to_le_bytes()); buf.extend_from_slice(&1i32.to_le_bytes()); buf.extend_from_slice(&build_bone_name_bytes("RootFrame"));
buf.extend_from_slice(&build_bone_name_bytes(""));
buf.push(1);
buf.extend_from_slice(&1i32.to_le_bytes());
buf.push(0);
buf.extend_from_slice(&1i16.to_le_bytes());
buf.extend_from_slice(&0i32.to_le_bytes()); buf.extend_from_slice(&0i32.to_le_bytes());
let parsed = parse_pmx_model(&buf).unwrap();
assert_eq!(parsed.metadata.format, "pmx");
assert_eq!(parsed.metadata.counts.bones, 2);
assert_eq!(parsed.skeleton.bones[0].name, "Root");
assert_eq!(parsed.skeleton.bones[1].parent_index, 0);
assert_eq!(parsed.display_frames.len(), 1);
assert_eq!(parsed.display_frames[0].frames[0].kind, "bone");
assert_eq!(parsed.display_frames[0].frames[0].index, 1);
assert!(parsed.diagnostics.is_empty());
}
#[test]
fn exports_pmx_semantic_roundtrip() {
let parsed = parsed_pmx_fixture();
let exported = export_pmx_model(&parsed);
let reparsed = parse_pmx_model(&exported).unwrap();
assert_pmx_roundtrip_eq(&parsed, &reparsed);
}
#[test]
fn exports_pmx_json_dto_semantic_roundtrip() {
let parsed = parsed_pmx_fixture();
let json = serde_json::to_string(&parsed).unwrap();
let from_json: PmxParsedModel = serde_json::from_str(&json).unwrap();
let exported = export_pmx_model(&from_json);
let reparsed = parse_pmx_model(&exported).unwrap();
assert_pmx_roundtrip_eq(&parsed, &reparsed);
}
#[test]
fn pmx_json_dto_roundtrips_non_finite_f32_values() {
let mut parsed = parsed_pmx_fixture();
let finite_position = parsed.geometry.positions[1];
parsed.geometry.positions[0] = f32::NAN;
parsed.geometry.normals[1] = f32::INFINITY;
parsed.geometry.uvs[0] = f32::NEG_INFINITY;
parsed.materials[0].diffuse[0] = f32::INFINITY;
parsed.materials[0].edge_size = f32::NAN;
parsed.skeleton.bones[0].position[0] = f32::NEG_INFINITY;
parsed.morphs[0].vertex_offsets[0].position[1] = f32::NAN;
let json = serde_json::to_string(&parsed).unwrap();
let value: serde_json::Value = serde_json::from_str(&json).unwrap();
assert!(json.contains("\"NaN\""));
assert!(json.contains("\"Infinity\""));
assert!(json.contains("\"-Infinity\""));
assert_eq!(value["geometry"]["positions"][0], serde_json::json!("NaN"));
assert_eq!(
value["geometry"]["normals"][1],
serde_json::json!("Infinity")
);
assert_eq!(value["geometry"]["uvs"][0], serde_json::json!("-Infinity"));
assert_eq!(
value["geometry"]["positions"][1],
serde_json::json!(finite_position)
);
let from_json: PmxParsedModel = serde_json::from_str(&json).unwrap();
assert!(from_json.geometry.positions[0].is_nan());
assert_eq!(from_json.geometry.normals[1], f32::INFINITY);
assert_eq!(from_json.geometry.uvs[0], f32::NEG_INFINITY);
assert_eq!(from_json.materials[0].diffuse[0], f32::INFINITY);
assert!(from_json.materials[0].edge_size.is_nan());
assert_eq!(from_json.skeleton.bones[0].position[0], f32::NEG_INFINITY);
assert!(from_json.morphs[0].vertex_offsets[0].position[1].is_nan());
assert_eq!(from_json.geometry.positions[1], finite_position);
}
#[test]
fn builds_pmx_parts_with_material_descriptors() {
let descriptor: PmxPartsDescriptor = serde_json::from_value(serde_json::json!({
"name": "material-parts",
"materials": [
{
"name": "red",
"englishName": "red-en",
"diffuse": [1.0, 0.0, 0.0, 1.0],
"ambient": [0.5, 0.0, 0.0],
"flags": { "edge": true },
"faceCount": 1
},
{
"name": "blue",
"diffuse": [0.0, 0.0, 1.0, 1.0],
"faceCount": 1
}
],
"bones": [
{ "name": "root", "englishName": "root-en", "tailIndex": 1 },
{ "name": "child", "parentIndex": 0, "position": [0.0, 1.0, 0.0], "rotatable": true }
],
"morphs": [
{
"name": "raise",
"englishName": "raise-en",
"kind": "vertex",
"vertexOffsets": [{ "vertexIndex": 0, "position": [0.0, 0.1, 0.0] }]
},
{
"name": "combo",
"kind": "group",
"groupOffsets": [{ "morphIndex": 0, "weight": 0.5 }]
}
],
"displayFrames": [
{
"name": "Root",
"englishName": "Root-en",
"special": true,
"frames": [
{ "kind": "bone", "index": 0 },
{ "kind": "bone", "index": 1 },
{ "kind": "morph", "index": 0 }
]
}
],
"rigidBodies": [
{
"name": "body",
"englishName": "body-en",
"boneIndex": 1,
"group": 2,
"mask": 3,
"shape": "box",
"size": [1.0, 2.0, 3.0],
"position": [0.0, 1.0, 0.0],
"rotation": [0.1, 0.2, 0.3],
"mass": 2.0,
"linearDamping": 0.4,
"angularDamping": 0.5,
"restitution": 0.6,
"friction": 0.7,
"mode": "dynamicBone"
}
],
"joints": [
{
"name": "joint",
"type": "generic6dofSpring",
"rigidBodyIndexA": 0,
"rigidBodyIndexB": -1,
"position": [0.0, 1.0, 0.0],
"translationLowerLimit": [-1.0, -1.0, -1.0],
"translationUpperLimit": [1.0, 1.0, 1.0],
"springTranslationFactor": [0.1, 0.2, 0.3]
}
],
"indexSizes": {
"vertex": 1,
"material": 1,
"texture": 1,
"bone": 1,
"morph": 1,
"rigidBody": 1
}
}))
.unwrap();
let model = build_pmx_model_from_parts(PmxPartsInput {
descriptor,
positions_xyz: &[
0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 1.0, 0.0, 0.0, 1.0, 0.0,
],
normals_xyz: &[
0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0,
],
uvs_xy: &[0.0, 0.0, 1.0, 0.0, 1.0, 1.0, 0.0, 1.0],
indices: &[0, 1, 2, 0, 2, 3],
skin_indices: &[0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0],
skin_weights: &[
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,
],
edge_scale: &[],
})
.unwrap();
let exported = export_pmx_model(&model);
let reparsed = parse_pmx_model(&exported).unwrap();
assert_eq!(reparsed.metadata.name, "material-parts");
assert_eq!(reparsed.metadata.counts.materials, 2);
assert_eq!(reparsed.materials[0].name, "red");
assert_eq!(reparsed.materials[0].english_name, "red-en");
assert_eq!(reparsed.materials[0].face_count, 1);
assert!(reparsed.materials[0].flags.edge);
assert_eq!(reparsed.materials[1].name, "blue");
assert_eq!(reparsed.materials[1].face_count, 1);
assert_eq!(reparsed.geometry.material_groups.len(), 2);
assert_eq!(reparsed.geometry.material_groups[0].start, 0);
assert_eq!(reparsed.geometry.material_groups[0].count, 3);
assert_eq!(reparsed.geometry.material_groups[1].start, 3);
assert_eq!(reparsed.geometry.material_groups[1].count, 3);
assert_eq!(reparsed.metadata.counts.bones, 2);
assert_eq!(reparsed.skeleton.bones[0].name, "root");
assert_eq!(reparsed.skeleton.bones[0].english_name, "root-en");
assert_eq!(reparsed.skeleton.bones[0].tail_index, 1);
assert!(reparsed.skeleton.bones[0].flags.indexed_tail);
assert_eq!(reparsed.skeleton.bones[1].name, "child");
assert_eq!(reparsed.skeleton.bones[1].parent_index, 0);
assert!(reparsed.skeleton.bones[1].flags.rotatable);
assert_eq!(reparsed.geometry.skin_indices[1], 1);
assert_eq!(reparsed.geometry.skin_weights[1], 0.25);
assert_eq!(reparsed.metadata.counts.morphs, 2);
assert_eq!(reparsed.morphs[0].name, "raise");
assert_eq!(reparsed.morphs[0].english_name, "raise-en");
assert_eq!(reparsed.morphs[0].kind, "vertex");
assert_eq!(reparsed.morphs[0].vertex_offsets[0].vertex_index, 0);
assert_eq!(
reparsed.morphs[0].vertex_offsets[0].position,
[0.0, 0.1, 0.0]
);
assert_eq!(reparsed.morphs[1].name, "combo");
assert_eq!(reparsed.morphs[1].kind, "group");
assert_eq!(reparsed.morphs[1].group_offsets[0].morph_index, 0);
assert_eq!(reparsed.morphs[1].group_offsets[0].weight, 0.5);
assert_eq!(reparsed.display_frames.len(), 1);
assert_eq!(reparsed.display_frames[0].name, "Root");
assert_eq!(reparsed.display_frames[0].english_name, "Root-en");
assert!(reparsed.display_frames[0].special);
assert_eq!(reparsed.display_frames[0].frames.len(), 3);
assert_eq!(reparsed.display_frames[0].frames[1].kind, "bone");
assert_eq!(reparsed.display_frames[0].frames[1].index, 1);
assert_eq!(reparsed.display_frames[0].frames[2].kind, "morph");
assert_eq!(reparsed.display_frames[0].frames[2].index, 0);
assert_eq!(reparsed.rigid_bodies.len(), 1);
assert_eq!(reparsed.rigid_bodies[0].name, "body");
assert_eq!(reparsed.rigid_bodies[0].english_name, "body-en");
assert_eq!(reparsed.rigid_bodies[0].bone_index, 1);
assert_eq!(reparsed.rigid_bodies[0].group, 2);
assert_eq!(reparsed.rigid_bodies[0].mask, 3);
assert_eq!(reparsed.rigid_bodies[0].shape, "box");
assert_eq!(reparsed.rigid_bodies[0].mode, "dynamicBone");
assert_eq!(reparsed.joints.len(), 1);
assert_eq!(reparsed.joints[0].name, "joint");
assert_eq!(reparsed.joints[0].kind, "generic6dofSpring");
assert_eq!(reparsed.joints[0].rigid_body_index_a, 0);
assert_eq!(reparsed.joints[0].rigid_body_index_b, -1);
}
#[test]
fn rejects_pmx_parts_material_face_count_mismatch() {
let descriptor: PmxPartsDescriptor = serde_json::from_value(serde_json::json!({
"materials": [{ "name": "bad", "faceCount": 2 }]
}))
.unwrap();
let error = build_pmx_model_from_parts(PmxPartsInput {
descriptor,
positions_xyz: &[0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0, 0.0],
normals_xyz: &[0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0],
uvs_xy: &[0.0, 0.0, 1.0, 0.0, 0.0, 1.0],
indices: &[0, 1, 2],
skin_indices: &[],
skin_weights: &[],
edge_scale: &[],
})
.unwrap_err();
assert!(error.contains("materials faceCount sum"));
}
#[test]
fn rejects_pmx_export_model_partial_skin_arrays() {
let mut parsed = parsed_pmx_fixture();
parsed.geometry.skin_weights.clear();
let error = validate_pmx_export_model(&parsed).unwrap_err();
assert!(error.contains("skinIndices and skinWeights"));
}
#[test]
fn exports_pmx_22_synthetic_roundtrip() {
let mut parsed = parsed_pmx_fixture();
parsed.metadata.version = 2.2;
let exported = export_pmx_model(&parsed);
let reparsed = parse_pmx_model(&exported).unwrap();
assert_eq!(
reparsed.metadata.version, 2.2,
"PMX 2.2 version must survive the export-parse cycle"
);
assert_pmx_roundtrip_eq(&parsed, &reparsed);
}
#[test]
fn exports_pmx_soft_body_header_fields_with_diagnostic_for_pmx_21_and_22() {
for version in [2.1, 2.2] {
let mut parsed = parsed_pmx_fixture();
parsed.metadata.version = version;
parsed.soft_bodies = vec![PmxParsedSoftBody {
name: "soft".to_owned(),
english_name: "soft-en".to_owned(),
kind: "rope".to_owned(),
material_index: 0,
collision_group: 1,
collision_mask: 2,
flags: 3,
bending_constraints_distance: 4,
cluster_count: 5,
total_mass: 6.0,
collision_margin: 0.7,
}];
let exported = export_pmx_model(&parsed);
let reparsed = parse_pmx_model(&exported).unwrap();
assert_eq!(reparsed.metadata.version, version);
assert_eq!(reparsed.metadata.counts.soft_bodies, 1);
assert_eq!(reparsed.soft_bodies.len(), 1);
assert_eq!(reparsed.soft_bodies[0].name, "soft");
assert_eq!(reparsed.soft_bodies[0].english_name, "soft-en");
assert_eq!(reparsed.soft_bodies[0].kind, "rope");
assert_eq!(reparsed.soft_bodies[0].material_index, 0);
assert_eq!(reparsed.soft_bodies[0].collision_group, 1);
assert_eq!(reparsed.soft_bodies[0].collision_mask, 2);
assert_eq!(reparsed.soft_bodies[0].flags, 3);
assert_eq!(reparsed.soft_bodies[0].bending_constraints_distance, 4);
assert_eq!(reparsed.soft_bodies[0].cluster_count, 5);
assert_eq!(reparsed.soft_bodies[0].total_mass, 6.0);
assert_eq!(reparsed.soft_bodies[0].collision_margin, 0.7);
assert_eq!(reparsed.diagnostics.len(), 1);
assert_eq!(reparsed.diagnostics[0].code, "PMX_SOFT_BODY_UNSUPPORTED");
}
}
#[test]
fn exports_parsed_pmx_skeleton_display_roundtrip() {
let mut buf = Vec::new();
buf.extend_from_slice(&build_small_pmx_header_bytes(2, TextEncoding::Utf8));
buf.extend_from_slice(&build_empty_model_info(TextEncoding::Utf8));
buf.extend_from_slice(&build_empty_vertex_section());
buf.extend_from_slice(&build_empty_face_section());
buf.extend_from_slice(&build_empty_texture_section());
buf.extend_from_slice(&build_empty_material_section());
buf.extend_from_slice(&build_bone_section_header(2));
buf.extend_from_slice(&build_bone_name_bytes("Root"));
buf.extend_from_slice(&build_bone_name_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(&(-1i16).to_le_bytes());
buf.extend_from_slice(&0i32.to_le_bytes());
buf.extend_from_slice(&0u16.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(&build_bone_name_bytes("Child"));
buf.extend_from_slice(&build_bone_name_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(&0i16.to_le_bytes());
buf.extend_from_slice(&0i32.to_le_bytes());
buf.extend_from_slice(&0u16.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(&0i32.to_le_bytes()); buf.extend_from_slice(&1i32.to_le_bytes()); buf.extend_from_slice(&build_bone_name_bytes("RootFrame"));
buf.extend_from_slice(&build_bone_name_bytes(""));
buf.push(1);
buf.extend_from_slice(&1i32.to_le_bytes());
buf.push(0);
buf.extend_from_slice(&1i16.to_le_bytes());
buf.extend_from_slice(&0i32.to_le_bytes()); buf.extend_from_slice(&0i32.to_le_bytes());
let parsed = parse_pmx_model(&buf).unwrap();
let exported = export_pmx_model(&parsed);
let reparsed = parse_pmx_model(&exported).unwrap();
assert_pmx_roundtrip_eq(&parsed, &reparsed);
}
#[test]
fn runtime_import_ik_solver_name_lookup() {
let mut buf = Vec::new();
buf.extend_from_slice(&build_small_pmx_header_bytes(2, TextEncoding::Utf8));
buf.extend_from_slice(&build_empty_model_info(TextEncoding::Utf8));
buf.extend_from_slice(&build_empty_vertex_section());
buf.extend_from_slice(&build_empty_face_section());
buf.extend_from_slice(&build_empty_texture_section());
buf.extend_from_slice(&build_empty_material_section());
buf.extend_from_slice(&build_bone_section_header(3));
buf.extend_from_slice(&build_bone_name_bytes("Root"));
buf.extend_from_slice(&build_bone_name_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(&(-1i16).to_le_bytes());
buf.extend_from_slice(&0i32.to_le_bytes());
buf.extend_from_slice(&0u16.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(&build_bone_name_bytes("IKLink"));
buf.extend_from_slice(&build_bone_name_bytes(""));
buf.extend_from_slice(&2.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(&0i16.to_le_bytes());
buf.extend_from_slice(&0i32.to_le_bytes());
buf.extend_from_slice(&0u16.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(&build_bone_name_bytes("LeftLegIK"));
buf.extend_from_slice(&build_bone_name_bytes(""));
buf.extend_from_slice(&4.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(&0i16.to_le_bytes());
buf.extend_from_slice(&0i32.to_le_bytes());
buf.extend_from_slice(&(BONE_FLAG_TAIL_INDEX | BONE_FLAG_IK).to_le_bytes());
buf.extend_from_slice(&1i16.to_le_bytes());
buf.extend_from_slice(&1i16.to_le_bytes());
buf.extend_from_slice(&10i32.to_le_bytes());
buf.extend_from_slice(&0.5f32.to_le_bytes());
buf.extend_from_slice(&1i32.to_le_bytes());
buf.extend_from_slice(&1i16.to_le_bytes());
buf.push(1u8);
buf.extend_from_slice(&(-1.0f32).to_le_bytes());
buf.extend_from_slice(&(-1.0f32).to_le_bytes());
buf.extend_from_slice(&(-1.0f32).to_le_bytes());
buf.extend_from_slice(&1.0f32.to_le_bytes());
buf.extend_from_slice(&1.0f32.to_le_bytes());
buf.extend_from_slice(&1.0f32.to_le_bytes());
buf.extend_from_slice(&0i32.to_le_bytes());
let rt = import_pmx_runtime(&buf).unwrap();
assert_eq!(rt.model.bone_count(), 3);
assert_eq!(rt.model.ik_count(), 1);
assert_eq!(
rt.ik_solver_bone_name_to_index.get(b"LeftLegIK".as_slice()),
Some(&0)
);
assert_eq!(
rt.bone_name_to_index.get(b"LeftLegIK".as_slice()),
Some(&BoneIndex(2))
);
assert_eq!(
rt.ik_solver_bone_name_to_index.get(b"NoSuchIK".as_slice()),
None
);
}
#[test]
fn converts_absolute_pmx_positions_to_local_rest_positions() {
let mut buf = Vec::new();
buf.extend_from_slice(&build_small_pmx_header_bytes(2, TextEncoding::Utf8));
buf.extend_from_slice(&build_empty_model_info(TextEncoding::Utf8));
buf.extend_from_slice(&build_empty_vertex_section());
buf.extend_from_slice(&build_empty_face_section());
buf.extend_from_slice(&build_empty_texture_section());
buf.extend_from_slice(&build_empty_material_section());
buf.extend_from_slice(&build_bone_section_header(2));
buf.extend_from_slice(&build_bone_name_bytes("Parent"));
buf.extend_from_slice(&build_bone_name_bytes(""));
buf.extend_from_slice(&1.0f32.to_le_bytes());
buf.extend_from_slice(&2.0f32.to_le_bytes());
buf.extend_from_slice(&0.0f32.to_le_bytes());
buf.extend_from_slice(&(-1i16).to_le_bytes());
buf.extend_from_slice(&0i32.to_le_bytes());
buf.extend_from_slice(&0u16.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(&build_bone_name_bytes("Child"));
buf.extend_from_slice(&build_bone_name_bytes(""));
buf.extend_from_slice(&1.0f32.to_le_bytes());
buf.extend_from_slice(&3.0f32.to_le_bytes());
buf.extend_from_slice(&0.0f32.to_le_bytes());
buf.extend_from_slice(&0i16.to_le_bytes());
buf.extend_from_slice(&0i32.to_le_bytes());
buf.extend_from_slice(&0u16.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());
let result = import_pmx_model(&buf).unwrap();
assert_eq!(result.bones.len(), 2);
assert_eq!(result.bones[0].parent, None);
assert!((result.bones[0].rest_position.x - 1.0).abs() < 0.001);
assert!((result.bones[0].rest_position.y - 2.0).abs() < 0.001);
assert!((result.bones[0].rest_position.z - 0.0).abs() < 0.001);
assert_eq!(result.bones[1].parent, Some(BoneIndex(0)));
assert!((result.bones[1].rest_position.x - 0.0).abs() < 0.001);
assert!((result.bones[1].rest_position.y - 1.0).abs() < 0.001);
assert!((result.bones[1].rest_position.z - 0.0).abs() < 0.001);
}
#[test]
fn root_inverse_bind_cancels_rest_world() {
let mut buf = Vec::new();
buf.extend_from_slice(&build_small_pmx_header_bytes(2, TextEncoding::Utf8));
buf.extend_from_slice(&build_empty_model_info(TextEncoding::Utf8));
buf.extend_from_slice(&build_empty_vertex_section());
buf.extend_from_slice(&build_empty_face_section());
buf.extend_from_slice(&build_empty_texture_section());
buf.extend_from_slice(&build_empty_material_section());
buf.extend_from_slice(&build_bone_section_header(1));
buf.extend_from_slice(&build_bone_name_bytes("Root"));
buf.extend_from_slice(&build_bone_name_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(&(-1i16).to_le_bytes());
buf.extend_from_slice(&0i32.to_le_bytes());
buf.extend_from_slice(&0u16.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());
let result = import_pmx_model(&buf).unwrap();
let ibm = result.bones[0].inverse_bind_matrix;
let rest_world = Mat4::from_translation(Vec3::new(1.0, 2.0, 3.0));
assert_eq!(rest_world * ibm, Mat4::IDENTITY);
}
#[test]
fn reads_pmx_fixed_axis_bone_descriptor() {
let mut buf = Vec::new();
buf.extend_from_slice(&build_small_pmx_header_bytes(2, TextEncoding::Utf8));
buf.extend_from_slice(&build_empty_model_info(TextEncoding::Utf8));
buf.extend_from_slice(&build_empty_vertex_section());
buf.extend_from_slice(&build_empty_face_section());
buf.extend_from_slice(&build_empty_texture_section());
buf.extend_from_slice(&build_empty_material_section());
buf.extend_from_slice(&build_bone_section_header(1));
buf.extend_from_slice(&build_bone_name_bytes("Fixed"));
buf.extend_from_slice(&build_bone_name_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(&(-1i16).to_le_bytes());
buf.extend_from_slice(&0i32.to_le_bytes());
buf.extend_from_slice(&BONE_FLAG_FIXED_AXIS.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(&0.0f32.to_le_bytes());
buf.extend_from_slice(&1.0f32.to_le_bytes());
buf.extend_from_slice(&0.0f32.to_le_bytes());
let result = import_pmx_model(&buf).unwrap();
assert_eq!(result.bones[0].fixed_axis, Some(Vec3A::Y));
assert!(!result.bones[0].enforce_fixed_axis);
}
#[test]
fn child_inverse_bind_uses_absolute_not_local_position() {
let mut buf = Vec::new();
buf.extend_from_slice(&build_small_pmx_header_bytes(2, TextEncoding::Utf8));
buf.extend_from_slice(&build_empty_model_info(TextEncoding::Utf8));
buf.extend_from_slice(&build_empty_vertex_section());
buf.extend_from_slice(&build_empty_face_section());
buf.extend_from_slice(&build_empty_texture_section());
buf.extend_from_slice(&build_empty_material_section());
buf.extend_from_slice(&build_bone_section_header(2));
buf.extend_from_slice(&build_bone_name_bytes("Root"));
buf.extend_from_slice(&build_bone_name_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(&(-1i16).to_le_bytes());
buf.extend_from_slice(&0i32.to_le_bytes());
buf.extend_from_slice(&0u16.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(&build_bone_name_bytes("Child"));
buf.extend_from_slice(&build_bone_name_bytes(""));
buf.extend_from_slice(&5.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(&0i16.to_le_bytes());
buf.extend_from_slice(&0i32.to_le_bytes());
buf.extend_from_slice(&0u16.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());
let result = import_pmx_model(&buf).unwrap();
assert_eq!(result.bones[1].parent, Some(BoneIndex(0)));
assert_eq!(result.bones[1].rest_position, Vec3A::new(4.0, 0.0, 0.0));
let ibm = result.bones[1].inverse_bind_matrix;
let rest_world = Mat4::from_translation(Vec3::new(5.0, 0.0, 0.0));
assert_eq!(rest_world * ibm, Mat4::IDENTITY);
}
#[test]
fn runtime_rest_pose_skinning_matrices_are_identity() {
let mut buf = Vec::new();
buf.extend_from_slice(&build_small_pmx_header_bytes(2, TextEncoding::Utf8));
buf.extend_from_slice(&build_empty_model_info(TextEncoding::Utf8));
buf.extend_from_slice(&build_empty_vertex_section());
buf.extend_from_slice(&build_empty_face_section());
buf.extend_from_slice(&build_empty_texture_section());
buf.extend_from_slice(&build_empty_material_section());
buf.extend_from_slice(&build_bone_section_header(2));
buf.extend_from_slice(&build_bone_name_bytes("Parent"));
buf.extend_from_slice(&build_bone_name_bytes(""));
buf.extend_from_slice(&1.0f32.to_le_bytes());
buf.extend_from_slice(&2.0f32.to_le_bytes());
buf.extend_from_slice(&0.0f32.to_le_bytes());
buf.extend_from_slice(&(-1i16).to_le_bytes());
buf.extend_from_slice(&0i32.to_le_bytes());
buf.extend_from_slice(&0u16.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(&build_bone_name_bytes("Child"));
buf.extend_from_slice(&build_bone_name_bytes(""));
buf.extend_from_slice(&1.0f32.to_le_bytes());
buf.extend_from_slice(&3.0f32.to_le_bytes());
buf.extend_from_slice(&0.0f32.to_le_bytes());
buf.extend_from_slice(&0i16.to_le_bytes());
buf.extend_from_slice(&0i32.to_le_bytes());
buf.extend_from_slice(&0u16.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(&0i32.to_le_bytes());
let rt = import_pmx_runtime(&buf).unwrap();
let model = Arc::new(rt.model);
let mut instance = RuntimeInstance::new(model);
instance.evaluate_current_pose();
let matrices = instance.skinning_matrices();
assert_eq!(matrices.len(), 2);
assert_eq!(matrices[0], Mat4::IDENTITY);
assert_eq!(matrices[1], Mat4::IDENTITY);
}
fn build_sdef_vertex_pmx() -> Vec<u8> {
let mut buf = Vec::new();
buf.extend_from_slice(b"PMX ");
buf.extend_from_slice(&2.0f32.to_le_bytes());
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));
buf.extend_from_slice(&1i32.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(&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 {
buf.extend_from_slice(&0i32.to_le_bytes()); }
buf
}
fn build_qdef_vertex_pmx() -> Vec<u8> {
let mut buf = Vec::new();
buf.extend_from_slice(b"PMX ");
buf.extend_from_slice(&2.0f32.to_le_bytes());
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));
buf.extend_from_slice(&1i32.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(&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(4u8); buf.push(0u8);
buf.push(0u8);
buf.push(0u8);
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 {
buf.extend_from_slice(&0i32.to_le_bytes());
}
buf
}
#[test]
fn pmx_sdef_geometry_json_includes_sdef_fields() {
let buf = build_sdef_vertex_pmx();
let model = parse_pmx_model(&buf).unwrap();
assert_eq!(
model.geometry.sdef.skinning_modes,
vec![PMX_SKINNING_MODE_SDEF.to_owned()]
);
assert_eq!(model.geometry.sdef.enabled.len(), 1);
assert!((model.geometry.sdef.enabled[0] - 1.0).abs() < 1e-5);
assert_eq!(model.geometry.sdef.c, vec![1.0, 2.0, 3.0]);
assert_eq!(model.geometry.sdef.r0, vec![4.0, 5.0, 6.0]);
assert_eq!(model.geometry.sdef.r1, vec![7.0, 8.0, 9.0]);
let w = 0.25f32;
let w1 = 0.75f32;
let c = [1.0f32, 2.0, 3.0];
let r0 = [4.0f32, 5.0, 6.0];
let r1 = [7.0f32, 8.0, 9.0];
for k in 0..3 {
let rw = r0[k] * w + r1[k] * w1;
let expected_rw0 = (c[k] * 2.0 + r0[k] - rw) * 0.5;
let expected_rw1 = (c[k] * 2.0 + r1[k] - rw) * 0.5;
assert!(
(model.geometry.sdef.rw0[k] - expected_rw0).abs() < 1e-4,
"rw0[{k}]: expected {expected_rw0}, got {}",
model.geometry.sdef.rw0[k]
);
assert!(
(model.geometry.sdef.rw1[k] - expected_rw1).abs() < 1e-4,
"rw1[{k}]: expected {expected_rw1}, got {}",
model.geometry.sdef.rw1[k]
);
}
assert_eq!(model.geometry.qdef.enabled[0], 0.0);
let json = serde_json::to_string(&model).unwrap();
assert!(json.contains("\"sdef\""), "JSON must contain sdef key");
assert!(json.contains("\"qdef\""), "JSON must contain qdef key");
let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
assert_eq!(parsed["geometry"]["sdef"]["enabled"][0], 1.0);
}
#[test]
fn pmx_qdef_vertex_marks_enabled() {
let buf = build_qdef_vertex_pmx();
let model = parse_pmx_model(&buf).unwrap();
assert_eq!(
model.geometry.sdef.skinning_modes,
vec![PMX_SKINNING_MODE_QDEF.to_owned()]
);
assert_eq!(model.geometry.qdef.enabled.len(), 1);
assert!((model.geometry.qdef.enabled[0] - 1.0).abs() < 1e-5);
assert_eq!(model.geometry.sdef.enabled[0], 0.0);
}
#[test]
fn exports_parsed_pmx_deform_type_binary_roundtrip() {
for original in [build_sdef_vertex_pmx(), build_qdef_vertex_pmx()] {
let parsed = parse_pmx_model(&original).unwrap();
let exported = export_pmx_model(&parsed);
assert_eq!(exported, original);
}
}
}