From b551aac97d391501d5cf688fa7cf6fdaf4c8fea9 Mon Sep 17 00:00:00 2001 From: Yusarina Date: Mon, 25 Nov 2024 21:27:49 +0000 Subject: [PATCH 1/5] Re-write Better, faster and working slightly better. --- core/import_pmx.py | 914 +++++++++++++++++++-------------------------- 1 file changed, 391 insertions(+), 523 deletions(-) diff --git a/core/import_pmx.py b/core/import_pmx.py index 19595ee..8c0ab3e 100644 --- a/core/import_pmx.py +++ b/core/import_pmx.py @@ -4,25 +4,69 @@ 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 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(' 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') - 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]) - - # 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 + # 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) - # 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: + 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 - # Assign vertex normals - custom_normals = [(Vector(i[1]).xzy).normalized() for i in vertices] - mesh.normals_split_custom_set_from_vertices(custom_normals) + edit_bones.append(edit_bone) + + # Second pass: Set up hierarchy and orientations + for i, bone_data in enumerate(bones): + edit_bone = edit_bones[i] - # Assign UV coordinates - uv_layer = mesh.uv_layers.new() - loop_indices_orig = tuple(i for f in faces for i in f) - uv_table = {vi:v for vi, v in enumerate([i[2] for i in vertices])} - uv_layer.data.foreach_set('uv', tuple(v for i in loop_indices_orig for v in uv_table[i])) - - cur_polygon_index: int = 0 - - # Assign materials - for material_data in materials: - material: bpy.types.Material - if material_data[0] in bpy.data.materials: - material = bpy.data.materials[material_data[0]] - else: - material = bpy.data.materials.new(material_data[0]) - - material.use_nodes = True - for node in [node for node in material.node_tree.nodes]: - material.node_tree.nodes.remove(node) + # Parent bones + if bone_data.parent_index >= 0: + parent_bone = edit_bones[bone_data.parent_index] + edit_bone.parent = parent_bone - # Create main nodes - principled_node = material.node_tree.nodes.new(type="ShaderNodeBsdfPrincipled") - principled_node.location = (0, 300) + # 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) - output_node = material.node_tree.nodes.new(type="ShaderNodeOutputMaterial") - output_node.location = (300, 300) + # 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 - # Set up main texture - albedo_node = material.node_tree.nodes.new(type="ShaderNodeTexImage") - albedo_node.location = (-600, 400) - if textures[material_data[9]] in bpy.data.images: - albedo_node.image = bpy.data.images[textures[material_data[9]]] - else: - albedo_node.image = bpy.data.images.new(name=textures[material_data[9]], width=32, height=32) - albedo_node.image.filepath = os.path.join(os.path.dirname(filepath), textures[material_data[9]]) - albedo_node.image.source = 'FILE' - albedo_node.extension = 'REPEAT' - albedo_node.image.reload() - # Set material properties - principled_node.inputs["Base Color"].default_value = material_data[2] - principled_node.inputs["Specular IOR Level"].default_value = material_data[4] - principled_node.inputs["Roughness"].default_value = 0.5 - principled_node.inputs["Metallic"].default_value = 0.0 +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') - # Handle transparency - if material_data[2][3] < 0.99: - material.blend_method = 'HASHED' - material.use_backface_culling = False - material.alpha_threshold = 0.5 - material.show_transparent_back = True - - # Create mix shader for transparency - mix_shader = material.node_tree.nodes.new(type='ShaderNodeMixShader') - mix_shader.location = (100, 300) - transparent_node = material.node_tree.nodes.new(type='ShaderNodeBsdfTransparent') - transparent_node.location = (-200, 200) +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 - # Connect nodes for transparency - material.node_tree.links.new(mix_shader.inputs[0], albedo_node.outputs["Alpha"]) - material.node_tree.links.new(mix_shader.inputs[1], transparent_node.outputs[0]) - material.node_tree.links.new(mix_shader.inputs[2], principled_node.outputs[0]) - material.node_tree.links.new(output_node.inputs["Surface"], mix_shader.outputs[0]) - else: - material.blend_method = 'OPAQUE' - material.node_tree.links.new(output_node.inputs["Surface"], principled_node.outputs[0]) - - # Connect color - material.node_tree.links.new(principled_node.inputs["Base Color"], albedo_node.outputs["Color"]) - - if not (material.name in mesh.materials): - mesh.materials.append(material) - - end: int = cur_polygon_index + material_data[15] - 1 - for face in mesh.polygons.items()[cur_polygon_index:end]: - face[1].material_index = mesh.materials.find(material.name) +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 - cur_polygon_index = cur_polygon_index + material_data[15] - - # Create armature and assign bones - armature = bpy.data.armatures.new(model_name + "_Armature") - armature_obj = bpy.data.objects.new(model_name + "_Armature", armature) - bpy.context.collection.objects.link(armature_obj) - obj.parent = armature_obj - modifier = obj.modifiers.new("Armature", 'ARMATURE') - modifier.object = armature_obj - - bpy.context.view_layer.objects.active = armature_obj - bpy.ops.object.mode_set(mode='EDIT') - - print("Starting bone creation...") - print(f"Total bones to create: {len(bones)}") - - # Create the bones using our create_bones function - edit_bones = create_bones(armature_obj, bones, scale) - - # Now we can safely scale and position bones - for bone in armature.edit_bones: - bone_data = next(b for b in bones if b[0] == bone.name) - bone.head = Vector(bone_data[2]).xzy * scale - if bone_data[6][0] is not None: - bone.tail = Vector(bone_data[6]).xzy * scale - else: - bone.tail = bone.head + Vector((0, 0.1 * scale)) - - # 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') - - # Assign morphs to the mesh - for morph_data in morphs: - morph_name = morph_data[0] - morph_type = morph_data[3] + # 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(' Date: Mon, 25 Nov 2024 21:40:32 +0000 Subject: [PATCH 2/5] Fix expression bones and other bones. --- core/import_pmx.py | 93 +++++++++++++++++++++++++++++++--------------- 1 file changed, 63 insertions(+), 30 deletions(-) diff --git a/core/import_pmx.py b/core/import_pmx.py index 8c0ab3e..9d53ae8 100644 --- a/core/import_pmx.py +++ b/core/import_pmx.py @@ -280,48 +280,81 @@ def create_armature(model_name: str, bones: list[PMXBone]) -> bpy.types.Object: 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 + # First pass: Create bones with proper names and types + edit_bones = [] for i, bone_data in enumerate(bones): - bone_name = f"bone_{i}" + bone_name = bone_data.name if bone_data.name else bone_data.english_name + if not bone_name: + 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 + # Handle different bone types based on flags and names + is_expression = bool(bone_data.flag & 0x0004) + is_rotation_influenced = bool(bone_data.flag & 0x0100) + is_ik = bool(bone_data.flag & 0x0020) + is_twist = "twist" in bone_name.lower() + + if is_twist: + # Twist bones need specific handling + parent_pos = bones[bone_data.parent_index].position if bone_data.parent_index >= 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: + edit_bone.tail = edit_bone.head + Vector((0, 0.05, 0)) + else: + edit_bone.tail = edit_bone.head + Vector((0, 0.05, 0)) + + elif is_expression: + edit_bone.tail = edit_bone.head + Vector((0, 0.02, 0)) + edit_bone.use_deform = False + + elif is_ik: + if bone_data.ik_links: + target_pos = bones[bone_data.ik_links[0][0]].position + direction = Vector(target_pos) - Vector(edit_bone.head) + if direction.length > 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: + 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: - # 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 + 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: - # 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_bone.tail = edit_bone.head + Vector((0, bone_length, 0)) edit_bones.append(edit_bone) - + + # Second pass: Set up hierarchy and orientations for i, bone_data in enumerate(bones): edit_bone = edit_bones[i] From e83bd31b4f70fa6cf4489040f70dd90727dae2fc Mon Sep 17 00:00:00 2001 From: Yusarina Date: Tue, 26 Nov 2024 00:13:47 +0000 Subject: [PATCH 3/5] Improvements - Shapekeys now import - Constraints should now import. --- core/import_pmx.py | 142 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 142 insertions(+) diff --git a/core/import_pmx.py b/core/import_pmx.py index 9d53ae8..952b90b 100644 --- a/core/import_pmx.py +++ b/core/import_pmx.py @@ -61,6 +61,14 @@ class PMXMaterial: 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 + def read_pmx_header(file: BufferedReader): magic = file.read(4) if magic != b'PMX ': @@ -106,6 +114,35 @@ def replace_char(string, index, character): temp[index] = character return "".join(temp) +def read_morph(file: BufferedReader, vertex_struct, vertex_size): + try: + name_length = 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 create_armature(model_name: str, bones: list[PMXBone]) -> bpy.types.Object: armature = bpy.data.armatures.new(f"{model_name}_Armature") armature_obj = bpy.data.objects.new(f"{model_name}_Armature", armature) @@ -476,6 +590,12 @@ def import_pmx(filepath: str): bones = [] for _ in range(bone_count): bones.append(read_bone(file, bone_struct, bone_size)) + + # Read morphs + morph_count = struct.unpack(' Date: Tue, 26 Nov 2024 00:43:17 +0000 Subject: [PATCH 4/5] Import Rigidbodies and Material Fixes - Rigidbodies should now import - Fixes in material import, we toon shader added. - We now validate pmx data. - Error messages improvements. --- core/import_pmx.py | 257 +++++++++++++++++++++++++++++++++++++++------ 1 file changed, 223 insertions(+), 34 deletions(-) diff --git a/core/import_pmx.py b/core/import_pmx.py index 952b90b..c0640b6 100644 --- a/core/import_pmx.py +++ b/core/import_pmx.py @@ -69,6 +69,37 @@ class PMXMorph: 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): magic = file.read(4) if magic != b'PMX ': @@ -143,6 +174,24 @@ def read_morph(file: BufferedReader, vertex_struct, vertex_size): except: return PMXMorph("", "", 0, 0, []) +def validate_pmx_data(header_data, vertices, faces, materials, bones): + """Validate PMX data integrity""" + if not vertices: + raise ValueError("No vertices found in PMX file") + if not faces: + raise ValueError("No faces found in PMX file") + if not materials: + raise ValueError("No materials found in PMX file") + if not bones: + raise ValueError("No bones found in PMX file") + return True + +def handle_import_error(context, error_msg): + """Handle import errors with user feedback""" + context.window_manager.progress_end() + bpy.ops.ui.popup_menu(message=error_msg) + return {'CANCELLED'} + def read_vertex(file: BufferedReader, string_build, byte_size, additional_uvs): position = struct.unpack('<3f', file.read(12)) normal = struct.unpack('<3f', file.read(12)) @@ -218,7 +267,7 @@ def read_material(file: BufferedReader, string_build, byte_size): texture_index, sphere_texture_index, sphere_mode, toon_sharing_flag, toon_texture_index, comment, surface_count) -def create_material_nodes(material: bpy.types.Material, texture_path: str, diffuse_color, specular_color, specular_strength): +def create_material_nodes(material: bpy.types.Material, texture_path: str, diffuse_color, specular_color, specular_strength, toon_texture_path=None): material.use_nodes = True nodes = material.node_tree.nodes links = material.node_tree.links @@ -231,21 +280,32 @@ def create_material_nodes(material: bpy.types.Material, texture_path: str, diffu principled.inputs["Specular IOR Level"].default_value = specular_strength principled.inputs["Specular Tint"].default_value = (*specular_color, 1.0) + # Handle transparency + if diffuse_color[3] < 1.0: + material.blend_method = 'HASHED' + principled.inputs["Alpha"].default_value = diffuse_color[3] + output = nodes.new("ShaderNodeOutputMaterial") output.location = (300, 0) - if texture_path: + # Main texture + if texture_path and os.path.exists(texture_path): texture = nodes.new("ShaderNodeTexImage") texture.location = (-300, 0) - - if os.path.exists(texture_path): - if texture_path in bpy.data.images: - texture.image = bpy.data.images[texture_path] - else: - texture.image = bpy.data.images.load(texture_path) - - links.new(texture.outputs["Color"], principled.inputs["Base Color"]) - links.new(texture.outputs["Alpha"], principled.inputs["Alpha"]) + texture.image = bpy.data.images.load(texture_path) + links.new(texture.outputs["Color"], principled.inputs["Base Color"]) + links.new(texture.outputs["Alpha"], principled.inputs["Alpha"]) + + # Toon texture + if toon_texture_path and os.path.exists(toon_texture_path): + toon = nodes.new("ShaderNodeTexImage") + toon.location = (-300, -300) + toon.image = bpy.data.images.load(toon_texture_path) + mix = nodes.new("ShaderNodeMixRGB") + mix.location = (-50, -150) + mix.blend_type = 'MULTIPLY' + links.new(toon.outputs["Color"], mix.inputs[2]) + links.new(mix.outputs["Color"], principled.inputs["Base Color"]) links.new(principled.outputs["BSDF"], output.inputs["Surface"]) @@ -384,7 +444,81 @@ def create_bone_constraints(armature_obj: bpy.types.Object, bones: list[PMXBone] 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: armature = bpy.data.armatures.new(f"{model_name}_Armature") @@ -542,26 +676,34 @@ def assign_materials(obj: bpy.types.Object, materials: list[PMXMaterial], textur 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 + # 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 + # 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 + # Read vertices (25%) vertex_count = struct.unpack(' Date: Tue, 26 Nov 2024 01:07:51 +0000 Subject: [PATCH 5/5] Bug fixes - First attempt to get Chinese Characters to work, kinda working, but also not. - Small bug fixes. --- core/common.py | 45 ++++++++++++++++++++++++++++++--- core/import_pmx.py | 20 ++++++++++++--- functions/armature_modifying.py | 4 +-- 3 files changed, 59 insertions(+), 10 deletions(-) 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 c0640b6..33865bc 100644 --- a/core/import_pmx.py +++ b/core/import_pmx.py @@ -521,6 +521,19 @@ def setup_physics(obj: bpy.types.Object, armature_obj: bpy.types.Object, rigid_b 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) @@ -602,7 +615,6 @@ def create_armature(model_name: str, bones: list[PMXBone]) -> bpy.types.Object: edit_bones.append(edit_bone) - # Second pass: Set up hierarchy and orientations for i, bone_data in enumerate(bones): edit_bone = edit_bones[i] @@ -625,14 +637,14 @@ def create_armature(model_name: str, bones: list[PMXBone]) -> bpy.types.Object: 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 + 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 = {} diff --git a/functions/armature_modifying.py b/functions/armature_modifying.py index 34fde6b..2797cc7 100644 --- a/functions/armature_modifying.py +++ b/functions/armature_modifying.py @@ -264,8 +264,8 @@ class AvatarToolkit_OT_MergeArmatures(Operator): bl_options = {'REGISTER', 'UNDO'} @classmethod - def poll(cls, context: Context) -> 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