from io import BufferedReader import os import bpy import struct import traceback import mathutils from mathutils import Matrix, Vector class PMXVertex: def __init__(self, position, normal, uv, bone_indices, bone_weights, edge_scale, additional_uvs): self.position = position self.normal = normal self.uv = uv self.bone_indices = bone_indices self.bone_weights = bone_weights self.edge_scale = edge_scale self.additional_uvs = additional_uvs class PMXBone: def __init__(self, name, english_name, position, parent_index, layer, flag, tail_position, inherit_parent_index, inherit_influence, fixed_axis, local_x, local_z, external_key, ik_target_index, ik_loop_count, ik_limit_rad, ik_links): self.name = name self.english_name = english_name self.position = position self.parent_index = parent_index self.layer = layer self.flag = flag self.tail_position = tail_position self.inherit_parent_index = inherit_parent_index self.inherit_influence = inherit_influence self.fixed_axis = fixed_axis self.local_x = local_x self.local_z = local_z self.external_key = external_key self.ik_target_index = ik_target_index self.ik_loop_count = ik_loop_count self.ik_limit_rad = ik_limit_rad self.ik_links = ik_links class PMXMaterial: def __init__(self, name, english_name, diffuse, specular, specular_strength, ambient, flag, edge_color, edge_size, texture_index, sphere_texture_index, sphere_mode, toon_sharing_flag, toon_texture_index, comment, surface_count): self.name = name self.english_name = english_name self.diffuse = diffuse self.specular = specular self.specular_strength = specular_strength self.ambient = ambient self.flag = flag self.edge_color = edge_color self.edge_size = edge_size self.texture_index = texture_index self.sphere_texture_index = sphere_texture_index self.sphere_mode = sphere_mode self.toon_sharing_flag = toon_sharing_flag self.toon_texture_index = toon_texture_index self.comment = comment self.surface_count = surface_count def read_pmx_header(file: BufferedReader): magic = file.read(4) if magic != b'PMX ': raise ValueError("Invalid PMX file") version = struct.unpack(' bpy.types.Object: armature = bpy.data.armatures.new(f"{model_name}_Armature") armature_obj = bpy.data.objects.new(f"{model_name}_Armature", armature) bpy.context.collection.objects.link(armature_obj) bpy.context.view_layer.objects.active = armature_obj bpy.ops.object.mode_set(mode='EDIT') # First pass: Create bones with correct positions and sizes edit_bones = [] # Using a list instead of dict for indexed access for i, bone_data in enumerate(bones): bone_name = f"bone_{i}" edit_bone = armature.edit_bones.new(bone_name) edit_bone.head = Vector(bone_data.position) # Calculate proper tail position with enhanced logic if bone_data.tail_position[0] is not None: edit_bone.tail = Vector(bone_data.tail_position) else: # Check for special bone types using flags if bone_data.flag & 0x0020: # IK bone bone_length = 0.1 elif bone_data.flag & 0x0100: # Rotation influenced bone_length = 0.08 elif bone_data.flag & 0x0200: # Movement influenced bone_length = 0.08 else: # Find child bones child_positions = [bones[j].position for j in range(len(bones)) if bones[j].parent_index == i] if child_positions: # Use closest child position closest_child = min(child_positions, key=lambda p: (Vector(p) - Vector(bone_data.position)).length) edit_bone.tail = Vector(closest_child) continue else: # Default length based on bone layer bone_length = 0.1 if bone_data.layer == 0 else 0.05 # Apply calculated length direction = Vector((0, bone_length, 0)) if bone_data.parent_index >= 0: parent_pos = Vector(bones[bone_data.parent_index].position) if (Vector(bone_data.position) - parent_pos).length > 0.001: direction = (Vector(bone_data.position) - parent_pos).normalized() * bone_length edit_bone.tail = edit_bone.head + direction edit_bones.append(edit_bone) # Second pass: Set up hierarchy and orientations for i, bone_data in enumerate(bones): edit_bone = edit_bones[i] # Parent bones if bone_data.parent_index >= 0: parent_bone = edit_bones[bone_data.parent_index] edit_bone.parent = parent_bone # Connect bones only if they should be connected if (Vector(bone_data.position) - Vector(parent_bone.tail)).length < 0.01: edit_bone.use_connect = True # Handle bone orientation if bone_data.fixed_axis != [0.0, 0.0, 0.0]: edit_bone.align_roll(Vector(bone_data.fixed_axis)) elif bone_data.local_x != [0.0, 0.0, 0.0]: x_axis = Vector(bone_data.local_x).normalized() z_axis = Vector(bone_data.local_z).normalized() y_axis = z_axis.cross(x_axis) # Create and apply orientation matrix matrix = Matrix((x_axis, y_axis, z_axis)).to_3x3() edit_bone.matrix = matrix bpy.ops.object.mode_set(mode='OBJECT') return armature_obj def assign_vertex_weights(obj: bpy.types.Object, vertices: list[PMXVertex], bones: list[PMXBone]): # Pre-create vertex groups vertex_groups = {} for bone in bones: vertex_groups[bone.name] = obj.vertex_groups.new(name=bone.name) # Batch assign weights for vertex_index, vertex in enumerate(vertices): for bone_idx, weight in zip(vertex.bone_indices, vertex.bone_weights): if bone_idx != -1 and weight > 0: vertex_groups[bones[bone_idx].name].add([vertex_index], weight, 'REPLACE') def assign_materials(obj: bpy.types.Object, materials: list[PMXMaterial], textures: list[str], base_path: str): current_face_index = 0 for material in materials: # Create or get material mat_name = material.name or f"Material_{len(obj.data.materials)}" if mat_name in bpy.data.materials: mat = bpy.data.materials[mat_name] else: mat = bpy.data.materials.new(name=mat_name) # Set up material nodes texture_path = None if material.texture_index >= 0 and material.texture_index < len(textures): texture_path = os.path.join(base_path, textures[material.texture_index]) create_material_nodes(mat, texture_path, material.diffuse, material.specular, material.specular_strength) # Assign material to mesh if mat.name not in obj.data.materials: obj.data.materials.append(mat) # Assign faces to material mat_index = obj.data.materials.find(mat.name) for face in obj.data.polygons[current_face_index:current_face_index + material.surface_count]: face.material_index = mat_index current_face_index += material.surface_count def import_pmx(filepath: str): try: with open(filepath, 'rb') as file: # Read header header_data = read_pmx_header(file) version, encoding, additional_uvs, vertex_index_size, texture_index_size, \ material_index_size, bone_index_size, morph_index_size, rigid_body_index_size, \ model_name, model_english_name, model_comment, model_english_comment = header_data # Set up index size formats vertex_struct, vertex_size = read_index_size(vertex_index_size, 'BHi') bone_struct, bone_size = read_index_size(bone_index_size, 'bhi') texture_struct, texture_size = read_index_size(texture_index_size, 'bhi') # Read vertices vertex_count = struct.unpack('