diff --git a/core/common.py b/core/common.py index 6b060f9..14e4914 100644 --- a/core/common.py +++ b/core/common.py @@ -119,12 +119,49 @@ def get_armatures_that_are_not_selected(self, context: Context) -> List[Tuple[st return [(obj.name, obj.name, "") for obj in bpy.data.objects if ((obj.type == 'ARMATURE') and (obj.name != context.scene.selected_armature))] def get_selected_armature(context: Context) -> Optional[Object]: - if context.scene.selected_armature: - armature = bpy.data.objects.get(context.scene.selected_armature) - if is_valid_armature(armature): - return armature + try: + if hasattr(context.scene, 'selected_armature'): + armature_name = context.scene.selected_armature + if isinstance(armature_name, bytes): + try: + armature_name = armature_name.decode('utf-8') + except UnicodeDecodeError: + try: + armature_name = armature_name.decode('gbk') # For Chinese characters + except UnicodeDecodeError: + try: + armature_name = armature_name.decode('shift-jis') + except UnicodeDecodeError: + armature_name = armature_name.decode('latin1') + + if armature_name: + armature = bpy.data.objects.get(str(armature_name)) + if is_valid_armature(armature): + return armature + except Exception: + pass return None +def get_merge_armature_source(context: Context) -> Optional[Object]: + try: + if hasattr(context.scene, 'merge_armature_source'): + source_name = context.scene.merge_armature_source + if isinstance(source_name, bytes): + try: + source_name = source_name.decode('utf-8') + except UnicodeDecodeError: + try: + source_name = source_name.decode('shift-jis') + except UnicodeDecodeError: + source_name = source_name.decode('latin1', errors='ignore') + + if source_name: + return bpy.data.objects.get(str(source_name)) + except Exception: + pass + return None + + def set_selected_armature(context: Context, armature: Optional[Object]) -> None: context.scene.selected_armature = armature.name if armature else "" diff --git a/core/import_pmx.py b/core/import_pmx.py index 19595ee..33865bc 100644 --- a/core/import_pmx.py +++ b/core/import_pmx.py @@ -4,25 +4,108 @@ import bpy import struct import traceback import mathutils - from mathutils import Matrix, Vector -from bpy.types import Material, Operator, Context, Object, Image, Mesh, MeshUVLoopLayer, Float2AttributeValue, ShaderNodeTexImage, ShaderNodeBsdfPrincipled, ShaderNodeOutputMaterial +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 -def replace_char(string, index, character): - temp = list(string) - temp[index] = character - return "".join(temp) +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 + +class PMXMorph: + def __init__(self, name, english_name, panel, morph_type, offsets): + self.name = name + self.english_name = english_name + self.panel = panel + self.morph_type = morph_type + self.offsets = offsets + +class PMXRigidBody: + def __init__(self, name, bone_index, group, shape_type, size, position, rotation, mass, linear_damping, angular_damping, restitution, friction, mode): + self.name = name + self.bone_index = bone_index + self.group = group + self.shape_type = shape_type + self.size = size + self.position = position + self.rotation = rotation + self.mass = mass + self.linear_damping = linear_damping + self.angular_damping = angular_damping + self.restitution = restitution + self.friction = friction + self.mode = mode + +class PMXJoint: + def __init__(self, name, joint_type, rigid_body_a, rigid_body_b, position, rotation, linear_limit_min, linear_limit_max, angular_limit_min, angular_limit_max, spring_constant_translation, spring_constant_rotation): + self.name = name + self.joint_type = joint_type + self.rigid_body_a = rigid_body_a + self.rigid_body_b = rigid_body_b + self.position = position + self.rotation = rotation + self.linear_limit_min = linear_limit_min + self.linear_limit_max = linear_limit_max + self.angular_limit_min = angular_limit_min + self.angular_limit_max = angular_limit_max + self.spring_constant_translation = spring_constant_translation + self.spring_constant_rotation = spring_constant_rotation def read_pmx_header(file: BufferedReader): - # Read PMX header information magic = file.read(4) if magic != b'PMX ': raise ValueError("Invalid PMX file") version = struct.unpack('= 0: + constraint = pose_bone.constraints.new('COPY_ROTATION') + constraint.name = "MMD Rotation" + constraint.target = armature_obj + constraint.subtarget = bones[bone_data.inherit_parent_index].name + constraint.influence = bone_data.inherit_influence + constraint.target_space = 'LOCAL' + constraint.owner_space = 'LOCAL' + + # Then handle IK constraints + for bone_data in bones: + pose_bone = armature_obj.pose.bones.get(bone_data.name) + if not pose_bone: + continue + + # Skip non-deforming bones + if not pose_bone.bone.use_deform: + continue + + if bone_data.flag & 0x0020: # IK + if bone_data.ik_target_index >= 0: + constraint = pose_bone.constraints.new('IK') + constraint.name = "MMD IK" + constraint.target = armature_obj + constraint.subtarget = bones[bone_data.ik_target_index].name + constraint.chain_count = min(len(bone_data.ik_links), 3) + constraint.iterations = min(bone_data.ik_loop_count, 8) + constraint.use_tail = False + constraint.use_stretch = False + + # Configure IK chain + for link_bone_index, has_limits, angle_limits in bone_data.ik_links: + link_pose_bone = armature_obj.pose.bones.get(bones[link_bone_index].name) + if link_pose_bone and link_pose_bone.bone.use_deform: + link_pose_bone.rotation_mode = 'XYZ' + link_pose_bone.use_ik_limit_x = True + link_pose_bone.use_ik_limit_y = True + link_pose_bone.use_ik_limit_z = True + + if has_limits and angle_limits: + min_angles, max_angles = angle_limits + link_pose_bone.ik_min_x = max(-1.4, min_angles[0]) + link_pose_bone.ik_max_x = min(1.4, max_angles[0]) + link_pose_bone.ik_min_y = max(-1.4, min_angles[1]) + link_pose_bone.ik_max_y = min(1.4, max_angles[1]) + link_pose_bone.ik_min_z = max(-1.4, min_angles[2]) + link_pose_bone.ik_max_z = min(1.4, max_angles[2]) + + # Reset pose to default state + bpy.ops.pose.select_all(action='SELECT') + bpy.ops.pose.transforms_clear() + bpy.ops.pose.select_all(action='DESELECT') + + bpy.ops.object.mode_set(mode='OBJECT') + +def setup_physics(obj: bpy.types.Object, armature_obj: bpy.types.Object, rigid_bodies: list[PMXRigidBody], joints: list[PMXJoint]): + """Set up physics for PMX model""" + # Create rigid body collection if it doesn't exist + if 'RigidBodies' not in bpy.data.collections: + rigid_body_collection = bpy.data.collections.new('RigidBodies') + bpy.context.scene.collection.children.link(rigid_body_collection) + else: + rigid_body_collection = bpy.data.collections['RigidBodies'] + + # Create rigid bodies + for rb in rigid_bodies: + # Create mesh based on shape type + if rb.shape_type == 0: # Sphere + bpy.ops.mesh.primitive_uv_sphere_add(radius=rb.size[0]) + elif rb.shape_type == 1: # Box + bpy.ops.mesh.primitive_cube_add() + bpy.context.active_object.scale = rb.size + elif rb.shape_type == 2: # Capsule + bpy.ops.mesh.primitive_cylinder_add(radius=rb.size[0], depth=rb.size[1]) + + rb_obj = bpy.context.active_object + rb_obj.name = f"RB_{rb.name}" + rb_obj.location = rb.position + rb_obj.rotation_euler = rb.rotation + + # Set up rigid body physics + rb_obj.rigid_body.type = 'ACTIVE' if rb.mode == 0 else 'PASSIVE' + rb_obj.rigid_body.mass = rb.mass + rb_obj.rigid_body.linear_damping = rb.linear_damping + rb_obj.rigid_body.angular_damping = rb.angular_damping + rb_obj.rigid_body.restitution = rb.restitution + rb_obj.rigid_body.friction = rb.friction + + # Parent to bone if specified + if rb.bone_index >= 0: + rb_obj.parent = armature_obj + rb_obj.parent_type = 'BONE' + rb_obj.parent_bone = bones[rb.bone_index].name + + # Move to rigid body collection + rigid_body_collection.objects.link(rb_obj) + bpy.context.scene.collection.objects.unlink(rb_obj) + + # Create joints + for joint in joints: + empty = bpy.data.objects.new(f"Joint_{joint.name}", None) + empty.empty_display_type = 'ARROWS' + empty.location = joint.position + empty.rotation_euler = joint.rotation + bpy.context.scene.collection.objects.link(empty) + + # Set up constraint + constraint = empty.constraints.new('RIGID_BODY_JOINT') + constraint.target = rigid_bodies[joint.rigid_body_a] + constraint.child = rigid_bodies[joint.rigid_body_b] + constraint.use_limit_lin_x = True + constraint.use_limit_lin_y = True + constraint.use_limit_lin_z = True + constraint.use_limit_ang_x = True + constraint.use_limit_ang_y = True + constraint.use_limit_ang_z = True + + # Set limits + constraint.limit_lin_x_lower = joint.linear_limit_min[0] + constraint.limit_lin_x_upper = joint.linear_limit_max[0] + constraint.limit_lin_y_lower = joint.linear_limit_min[1] + constraint.limit_lin_y_upper = joint.linear_limit_max[1] + constraint.limit_lin_z_lower = joint.linear_limit_min[2] + constraint.limit_lin_z_upper = joint.linear_limit_max[2] + constraint.limit_ang_x_lower = joint.angular_limit_min[0] + constraint.limit_ang_x_upper = joint.angular_limit_max[0] + constraint.limit_ang_y_lower = joint.angular_limit_min[1] + constraint.limit_ang_y_upper = joint.angular_limit_max[1] + constraint.limit_ang_z_lower = joint.angular_limit_min[2] + constraint.limit_ang_z_upper = joint.angular_limit_max[2] + +def create_armature(model_name: str, bones: list[PMXBone]) -> bpy.types.Object: + # Handle CJK characters in model name + if isinstance(model_name, bytes): + try: + model_name = model_name.decode('gbk') # Try Chinese encoding first + except UnicodeDecodeError: + try: + model_name = model_name.decode('utf-8') + except UnicodeDecodeError: + try: + model_name = model_name.decode('shift-jis') + except UnicodeDecodeError: + model_name = model_name.decode('latin1') + + 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 proper names and types edit_bones = [] - for i, bone_data in enumerate(bones_data): - try: - print(f"Creating bone {i}: {bone_data[0]}") - bone = armature_obj.data.edit_bones.new(bone_data[0]) + for i, bone_data in enumerate(bones): + bone_name = bone_data.name if bone_data.name else bone_data.english_name + if not bone_name: + bone_name = f"bone_{i}" - # Convert and scale head position - head_pos = Vector(bone_data[2]).xzy * scale - bone.head = head_pos - - # Handle tail position - if bone_data[6][0] is not None: - tail_pos = Vector(bone_data[6]).xzy * scale - bone.tail = tail_pos - print(f"Using defined tail position for bone {bone_data[0]}") - else: - # Set a default tail position if not provided - bone.tail = head_pos + Vector((0, 0.1, 0)) * scale - print(f"Using default tail position for bone {bone_data[0]}") - - # Set parent if exists - if bone_data[3] != -1: - parent_bone = armature_obj.data.edit_bones[bones_data[bone_data[3]][0]] - bone.parent = parent_bone - print(f"Parented bone {bone_data[0]} to {parent_bone.name}") - - edit_bones.append(bone) - - except Exception as e: - print(f"Error creating bone {i}: {str(e)}") - continue + edit_bone = armature.edit_bones.new(bone_name) + edit_bone.head = Vector(bone_data.position) - # Set bone hierarchy - for i, bone_data in enumerate(bones_data): - if bone_data[3] != -1: - edit_bones[i].parent = edit_bones[bone_data[3]] - - # Apply final transforms - bpy.ops.object.mode_set(mode='OBJECT') - armature_obj.rotation_euler[0] = 1.5708 - armature_obj.rotation_euler[2] = 3.14159 - armature_obj.select_set(True) - bpy.ops.object.transform_apply(location=True, rotation=True, scale=True) - - return edit_bones - -def read_morph(file: BufferedReader, morph_struct, morph_bytesize, vertex_struct, vertex_size, bone_struct, bone_size, material_struct, material_size, rigid_struct, rigid_size): - morph_name = str(file.read(struct.unpack('= 0 else None + if parent_pos: + direction = Vector(bone_data.position) - Vector(parent_pos) + if direction.length > 0.001: + edit_bone.tail = edit_bone.head + direction.normalized() * 0.1 else: - face_funct = lambda: read_data('<3i',12) - for _ in range(face_count // 3): - faces.append(face_funct()) - print("stage 6") - # Read textures - texture_count = struct.unpack(' 0.001: + edit_bone.tail = edit_bone.head + direction.normalized() * 0.1 + else: + edit_bone.tail = edit_bone.head + Vector((0, 0.1, 0)) else: - bone.tail = bone.head + Vector((0, 0.1 * scale)) + edit_bone.tail = edit_bone.head + Vector((0, 0.1, 0)) + + elif is_rotation_influenced: + # Handle rotation influenced bones + if bone_data.inherit_parent_index >= 0: + target_pos = bones[bone_data.inherit_parent_index].position + direction = Vector(target_pos) - Vector(edit_bone.head) + if direction.length > 0.001: + edit_bone.tail = edit_bone.head + direction.normalized() * 0.08 + else: + edit_bone.tail = edit_bone.head + Vector((0, 0.08, 0)) + else: + edit_bone.tail = edit_bone.head + Vector((0, 0.08, 0)) + + else: + # Standard bones + if bone_data.tail_position[0] is not None: + edit_bone.tail = Vector(bone_data.tail_position) + else: + child_positions = [bones[j].position for j in range(len(bones)) + if bones[j].parent_index == i] + if child_positions: + avg_child_pos = Vector((0, 0, 0)) + for pos in child_positions: + avg_child_pos += Vector(pos) + avg_child_pos /= len(child_positions) + edit_bone.tail = avg_child_pos + else: + bone_length = 0.1 if bone_data.layer == 0 else 0.05 + edit_bone.tail = edit_bone.head + Vector((0, bone_length, 0)) - # Assign bone weights to the mesh - for i, vertex in enumerate(vertices): - for j in range(0, len(vertex[3])): - if vertex[3][j] != -1 and vertex[3][j] < len(bones): - bone_name = bones[vertex[3][j]][0] - weight = vertex[4][j] - - vertex_group = obj.vertex_groups.get(bone_name) - if not vertex_group: - vertex_group = obj.vertex_groups.new(name=bone_name) - - vertex_group.add([i], weight, 'REPLACE') + edit_bones.append(edit_bone) - # Assign morphs to the mesh - for morph_data in morphs: - morph_name = morph_data[0] - morph_type = morph_data[3] + # 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 - if morph_type == 1: # Vertex morph - shape_key = obj.shape_key_add(name=morph_name) - for offset_data in morph_data[4]: - vertex_index = offset_data[0] - offset = offset_data[1] - shape_key.data[vertex_index].co += mathutils.Vector(offset) + # 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 - #ROTATE LAST! - armature_obj.rotation_euler[0] = 1.5708 - armature_obj.rotation_euler[2] = 3.14159 - armature_obj.select_set(True) - obj.select_set(True) - bpy.ops.object.transform_apply(location=True, rotation=True, scale=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_3x3 = Matrix((x_axis, y_axis, z_axis)).to_3x3() + matrix_4x4 = matrix_3x3.to_4x4() + edit_bone.matrix = matrix_4x4 + + 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) - print(f"Successfully imported PMX file: {filepath}") - print(f"Model Name: {model_name}") - print(f"Model English Name: {model_english_name}") - print(f"Model Comment: {model_comment}") - print(f"Model English Comment: {model_english_comment}") + # 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): + wm = bpy.context.window_manager + wm.progress_begin(0, 100) + + try: + with open(filepath, 'rb') as file: + # Read header (5%) + wm.progress_update(5) + 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 (10%) + wm.progress_update(10) + 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 (25%) + vertex_count = struct.unpack(' bool: - return (common.get_selected_armature(context) is not None) and (context.scene.merge_armature_source is not None) + def poll(cls, context): + return (common.get_selected_armature(context) is not None) and (common.get_merge_armature_source(context) is not None) def make_active(self, obj: bpy.types.Object, context: Context): context.view_layer.objects.active = obj