diff --git a/blender_manifest.toml b/blender_manifest.toml index 7b8d826..a6f7d48 100644 --- a/blender_manifest.toml +++ b/blender_manifest.toml @@ -3,21 +3,21 @@ schema_version = "1.0.0" id = "avatar_toolkit" -version = "0.1.2" +version = "0.2.0" name = "Avatar Toolkit" tagline = "A modern tool for importing and optimizing models for VRChat, Resonite, and other similar games." maintainer = "Team NekoNeo" type = "add-on" -blender_version_min = "4.3.0" +blender_version_min = "4.4.0" license = [ "SPDX:GPL-3.0-or-later", ] wheels = [ - "./wheels/lz4-4.3.3-cp311-cp311-macosx_11_0_arm64.whl", - "./wheels/lz4-4.3.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", - "./wheels/lz4-4.3.3-cp311-cp311-win_amd64.whl" + "./wheels/lz4-4.4.3-cp311-cp311-macosx_11_0_arm64.whl", + "./wheels/lz4-4.4.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", + "./wheels/lz4-4.4.3-cp311-cp311-win_amd64.whl" ] diff --git a/core/importers/import_pmd.py b/core/importers/import_pmd.py deleted file mode 100644 index 33e0e71..0000000 --- a/core/importers/import_pmd.py +++ /dev/null @@ -1,271 +0,0 @@ -import bpy -import struct -import mathutils -import traceback -import os - -from bpy.types import Material, Operator, Context, Object, Image, Mesh, MeshUVLoopLayer, Float2AttributeValue, ShaderNodeTexImage, ShaderNodeBsdfPrincipled, ShaderNodeOutputMaterial - -def read_pmd_header(file): - # Read PMD header information - magic = file.read(3) - if magic != b'Pmd': - raise ValueError("Invalid PMD 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): - 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) - - # 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: - 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)) - - 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_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) - - # 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('