Better, faster and working slightly better.
This commit is contained in:
Yusarina
2024-11-25 21:27:49 +00:00
parent 155c40d4d4
commit b551aac97d
+322 -454
View File
@@ -4,25 +4,69 @@ import bpy
import struct import struct
import traceback import traceback
import mathutils import mathutils
from mathutils import Matrix, Vector 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): class PMXBone:
temp = list(string) def __init__(self, name, english_name, position, parent_index, layer, flag,
temp[index] = character tail_position, inherit_parent_index, inherit_influence,
return "".join(temp) 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): def read_pmx_header(file: BufferedReader):
# Read PMX header information
magic = file.read(4) magic = file.read(4)
if magic != b'PMX ': if magic != b'PMX ':
raise ValueError("Invalid PMX file") raise ValueError("Invalid PMX file")
version = struct.unpack('<f', file.read(4))[0] version = struct.unpack('<f', file.read(4))[0]
# Read additional header fields
data_size = struct.unpack('<b', file.read(1))[0] data_size = struct.unpack('<b', file.read(1))[0]
encoding = struct.unpack('<b', file.read(1))[0] encoding = struct.unpack('<b', file.read(1))[0]
additional_uvs = struct.unpack('<b', file.read(1))[0] additional_uvs = struct.unpack('<b', file.read(1))[0]
@@ -32,67 +76,79 @@ def read_pmx_header(file: BufferedReader):
bone_index_size = struct.unpack('<b', file.read(1))[0] bone_index_size = struct.unpack('<b', file.read(1))[0]
morph_index_size = struct.unpack('<b', file.read(1))[0] morph_index_size = struct.unpack('<b', file.read(1))[0]
rigid_body_index_size = struct.unpack('<b', file.read(1))[0] rigid_body_index_size = struct.unpack('<b', file.read(1))[0]
print(rigid_body_index_size)
# Read model name and comments
model_name = str(file.read(struct.unpack('<i', file.read(4))[0]), 'utf-16-le', errors='replace') model_name = str(file.read(struct.unpack('<i', file.read(4))[0]), 'utf-16-le', errors='replace')
model_english_name = str(file.read(struct.unpack('<i', file.read(4))[0]), 'utf-16-le', errors='replace') model_english_name = str(file.read(struct.unpack('<i', file.read(4))[0]), 'utf-16-le', errors='replace')
model_comment = str(file.read(struct.unpack('<i', file.read(4))[0]), 'utf-16-le', errors='replace') model_comment = str(file.read(struct.unpack('<i', file.read(4))[0]), 'utf-16-le', errors='replace')
print(model_name)
print(model_english_name)
print(model_comment)
model_english_comment = str(file.read(struct.unpack('<i', file.read(4))[0]), 'utf-16-le', errors='replace') model_english_comment = str(file.read(struct.unpack('<i', file.read(4))[0]), 'utf-16-le', errors='replace')
print(model_english_comment)
return 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 return (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)
def read_index_size(index, types):
struct_format = "<??"
byte_size = 0
if index == 1:
struct_format = replace_char(struct_format, 2, types[0])
byte_size = 1
elif index == 2:
struct_format = replace_char(struct_format, 2, types[1])
byte_size = 2
else:
struct_format = replace_char(struct_format, 2, types[2])
byte_size = 4
return struct_format, byte_size
def replace_char(string, index, character):
temp = list(string)
temp[index] = character
return "".join(temp)
def read_vertex(file: BufferedReader, string_build, byte_size, additional_uvs): def read_vertex(file: BufferedReader, string_build, byte_size, additional_uvs):
position = struct.unpack('<3f', file.read(12)) position = struct.unpack('<3f', file.read(12))
normal = struct.unpack('<3f', file.read(12)) normal = struct.unpack('<3f', file.read(12))
uv = struct.unpack('<2f', file.read(8)) uv = struct.unpack('<2f', file.read(8))
uv = [uv[0],(1.0-uv[1])-1.0] uv = [uv[0], (1.0-uv[1])-1.0]
additional_uv_read = [] additional_uv_read = []
for i in range(0,additional_uvs): for _ in range(additional_uvs):
additional_uv_read.append(struct.unpack('<4f', file.read(16))) additional_uv_read.append(struct.unpack('<4f', file.read(16)))
weight_deform_type = struct.unpack('<B', file.read(1))[0] weight_deform_type = struct.unpack('<B', file.read(1))[0]
C_num = [] bone_indices = []
R0_num = [] bone_weights = []
R1_num = []
#in the if-else chain, multiplying byte_size by a number should reflect the string_build's 1st (not 0th) character which is how many bone indices there are. if weight_deform_type == 0: # BDEF1
if weight_deform_type == 0: #BDEF 1 string_build = replace_char(string_build, 1, '1')
string_build = replace_char(string_build,1,'1') #how many bone indices there are
bone_indices = list(struct.unpack(string_build, file.read(byte_size*1))) bone_indices = list(struct.unpack(string_build, file.read(byte_size*1)))
bone_weights = [1.0] bone_weights = [1.0]
elif weight_deform_type == 1: #BDEF2 elif weight_deform_type == 1: # BDEF2
string_build = replace_char(string_build,1,'2') #how many bone indices there are string_build = replace_char(string_build, 1, '2')
bone_indices = list(struct.unpack(string_build, file.read(byte_size*2))) bone_indices = list(struct.unpack(string_build, file.read(byte_size*2)))
bone_1_weight = struct.unpack('<f', file.read(4))[0] weight = struct.unpack('<f', file.read(4))[0]
bone_weights = [bone_1_weight, 1.0-bone_1_weight] bone_weights = [weight, 1.0-weight]
elif weight_deform_type == 2: #BDEF4 elif weight_deform_type == 2: # BDEF4
string_build = replace_char(string_build,1,'4') #how many bone indices there are string_build = replace_char(string_build, 1, '4')
bone_indices = list(struct.unpack(string_build, file.read(byte_size*4))) bone_indices = list(struct.unpack(string_build, file.read(byte_size*4)))
bone_weights = list(struct.unpack('<4f', file.read(4*4))) bone_weights = list(struct.unpack('<4f', file.read(16)))
elif weight_deform_type == 3: #SDEF elif weight_deform_type == 3: # SDEF
string_build = replace_char(string_build,1,'2') #how many bone indices there are string_build = replace_char(string_build, 1, '2')
bone_indices = list(struct.unpack(string_build, file.read(byte_size*2))) bone_indices = list(struct.unpack(string_build, file.read(byte_size*2)))
bone_1_weight = struct.unpack('<f', file.read(4))[0] weight = struct.unpack('<f', file.read(4))[0]
bone_weights = [bone_1_weight, 1.0-bone_1_weight] bone_weights = [weight, 1.0-weight]
C_num = struct.unpack('<3f', file.read(12)) # Skip SDEF data as we don't use it
R0_num = struct.unpack('<3f', file.read(12)) file.read(36) # 3 vectors of 3 floats each (C, R0, R1)
R1_num = struct.unpack('<3f', file.read(12)) elif weight_deform_type == 4: # QDEF
elif weight_deform_type == 4: #QDEF string_build = replace_char(string_build, 1, '4')
string_build = replace_char(string_build,1,'4') #how many bone indices there are
bone_indices = list(struct.unpack(string_build, file.read(byte_size*4))) bone_indices = list(struct.unpack(string_build, file.read(byte_size*4)))
bone_weights = list(struct.unpack('<4f', file.read(4*4))) bone_weights = list(struct.unpack('<4f', file.read(16)))
else:
raise IOError("Unsupported weight deform type \""+str(weight_deform_type)+"\" for file!")
edge_scale = struct.unpack('<f', file.read(4))[0] edge_scale = struct.unpack('<f', file.read(4))[0]
return position, normal, uv, bone_indices, bone_weights, edge_scale, additional_uv_read
return PMXVertex(position, normal, uv, bone_indices, bone_weights, edge_scale, additional_uv_read)
def read_material(file: BufferedReader, string_build, byte_size): def read_material(file: BufferedReader, string_build, byte_size):
material_name = str(file.read(struct.unpack('<i', file.read(4))[0]), 'utf-16-le', errors='replace') material_name = str(file.read(struct.unpack('<i', file.read(4))[0]), 'utf-16-le', errors='replace')
@@ -106,8 +162,7 @@ def read_material(file: BufferedReader, string_build, byte_size):
flag = struct.unpack('<b', file.read(1))[0] flag = struct.unpack('<b', file.read(1))[0]
edge_color = struct.unpack('<4f', file.read(16)) edge_color = struct.unpack('<4f', file.read(16))
edge_size = struct.unpack('<f', file.read(4))[0] edge_size = struct.unpack('<f', file.read(4))[0]
#this is bad don't do this, replaced it.. - @989onan
#texture_index = struct.unpack(f'<{texture_index_size}B', file.read(texture_index_size))[0]
texture_index = struct.unpack(replace_char(string_build, 1, '1'), file.read(byte_size))[0] texture_index = struct.unpack(replace_char(string_build, 1, '1'), file.read(byte_size))[0]
sphere_texture_index = struct.unpack(replace_char(string_build, 1, '1'), file.read(byte_size))[0] sphere_texture_index = struct.unpack(replace_char(string_build, 1, '1'), file.read(byte_size))[0]
sphere_mode = struct.unpack('<b', file.read(1))[0] sphere_mode = struct.unpack('<b', file.read(1))[0]
@@ -118,66 +173,86 @@ def read_material(file: BufferedReader, string_build, byte_size):
else: else:
toon_texture_index = struct.unpack('<b', file.read(1))[0] toon_texture_index = struct.unpack('<b', file.read(1))[0]
comment = str(file.read(struct.unpack('<i', file.read(4))[0]), 'utf-16-le', errors='replace') comment = str(file.read(struct.unpack('<i', file.read(4))[0]), 'utf-16-le', errors='replace')
surface_count = int(struct.unpack('<i', file.read(4))[0]/3) surface_count = int(struct.unpack('<i', file.read(4))[0]/3)
return material_name, material_english_name, diffuse_color, specular_color, specular_strength, ambient_color, flag, edge_color, edge_size, texture_index, sphere_texture_index, sphere_mode, toon_sharing_flag, toon_texture_index, comment, surface_count return PMXMaterial(material_name, material_english_name, diffuse_color, specular_color,
specular_strength, ambient_color, flag, edge_color, edge_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):
material.use_nodes = True
nodes = material.node_tree.nodes
links = material.node_tree.links
nodes.clear()
principled = nodes.new("ShaderNodeBsdfPrincipled")
principled.location = (0, 0)
principled.inputs["Base Color"].default_value = diffuse_color
principled.inputs["Specular IOR Level"].default_value = specular_strength
principled.inputs["Specular Tint"].default_value = (*specular_color, 1.0)
output = nodes.new("ShaderNodeOutputMaterial")
output.location = (300, 0)
if 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"])
links.new(principled.outputs["BSDF"], output.inputs["Surface"])
def read_bone(file: BufferedReader, string_build, byte_size): def read_bone(file: BufferedReader, string_build, byte_size):
try: bone_name = str(file.read(struct.unpack('<i', file.read(4))[0]), 'utf-16-le', errors='replace')
# Read bone name and validate bone_english_name = str(file.read(struct.unpack('<i', file.read(4))[0]), 'utf-16-le', errors='replace')
name_length = struct.unpack('<i', file.read(4))[0]
if not 0 <= name_length <= 512:
raise ValueError(f"Invalid bone name length {name_length}")
bone_name = str(file.read(name_length), 'utf-16-le', errors='replace')
# Read English name
eng_name_length = struct.unpack('<i', file.read(4))[0]
bone_english_name = str(file.read(eng_name_length), 'utf-16-le', errors='replace')
# Read position and indices
position = struct.unpack('<3f', file.read(12)) position = struct.unpack('<3f', file.read(12))
parent_bone_index = struct.unpack(replace_char(string_build, 1, '1'), file.read(byte_size))[0] parent_bone_index = struct.unpack(replace_char(string_build, 1, '1'), file.read(byte_size))[0]
layer = struct.unpack('<i', file.read(4))[0] layer = struct.unpack('<i', file.read(4))[0]
flag = struct.unpack('<H', file.read(2))[0] flag = struct.unpack('<H', file.read(2))[0]
# Initialize bone properties with defaults tail_position = [None, None, None]
tail_position = [0.0, 0.0, 0.0] inherit_bone_parent_index = 0
tail_index = -1
inherit_bone_parent_index = -1
inherit_bone_parent_influence = 0.0 inherit_bone_parent_influence = 0.0
fixed_axis = [0.0, 0.0, 0.0] fixed_axis = [0.0, 0.0, 0.0]
local_x_vector = [0.0, 0.0, 0.0] local_x_vector = [0.0, 0.0, 0.0]
local_z_vector = [0.0, 0.0, 0.0] local_z_vector = [0.0, 0.0, 0.0]
external_key = 0 external_key = 0
ik_target_bone_index = -1 ik_target_bone_index = 0
ik_loop_count = 0 ik_loop_count = -1
ik_limit_radian = 0.0 ik_limit_radian = 0.0
ik_links = [] ik_links = []
# Read flag-dependent data if not (flag & 0x0001):
if not (flag & 0x0001): # Connection not by offset
tail_position = struct.unpack('<3f', file.read(12)) tail_position = struct.unpack('<3f', file.read(12))
else: else:
tail_index = struct.unpack(replace_char(string_build, 1, '1'), file.read(byte_size))[0] tail_index = struct.unpack(replace_char(string_build, 1, '1'), file.read(byte_size))[0]
if flag & 0x0100 or flag & 0x0200: # Has inheritance if flag & 0x0100 or flag & 0x0200:
inherit_bone_parent_index = struct.unpack(replace_char(string_build, 1, '1'), file.read(byte_size))[0] inherit_bone_parent_index = struct.unpack(replace_char(string_build, 1, '1'), file.read(byte_size))[0]
inherit_bone_parent_influence = struct.unpack('<f', file.read(4))[0] inherit_bone_parent_influence = struct.unpack('<f', file.read(4))[0]
if flag & 0x0400: # Has fixed axis if flag & 0x0400:
fixed_axis = struct.unpack('<3f', file.read(12)) fixed_axis = struct.unpack('<3f', file.read(12))
if flag & 0x0800: # Has local coordinate if flag & 0x0800:
local_x_vector = struct.unpack('<3f', file.read(12)) local_x_vector = struct.unpack('<3f', file.read(12))
local_z_vector = struct.unpack('<3f', file.read(12)) local_z_vector = struct.unpack('<3f', file.read(12))
if flag & 0x2000: # Has external parent deform if flag & 0x2000:
external_key = struct.unpack('<i', file.read(4))[0] external_key = struct.unpack('<i', file.read(4))[0]
if flag & 0x0020: # Has IK if flag & 0x0020:
ik_target_bone_index = struct.unpack(replace_char(string_build, 1, '1'), file.read(byte_size))[0] ik_target_bone_index = struct.unpack(replace_char(string_build, 1, '1'), file.read(byte_size))[0]
ik_loop_count = struct.unpack('<i', file.read(4))[0] ik_loop_count = struct.unpack('<i', file.read(4))[0]
ik_limit_radian = struct.unpack('<f', file.read(4))[0] ik_limit_radian = struct.unpack('<f', file.read(4))[0]
@@ -185,433 +260,226 @@ def read_bone(file: BufferedReader, string_build, byte_size):
for _ in range(ik_link_count): for _ in range(ik_link_count):
ik_link_bone_index = struct.unpack(replace_char(string_build, 1, '1'), file.read(byte_size))[0] ik_link_bone_index = struct.unpack(replace_char(string_build, 1, '1'), file.read(byte_size))[0]
has_limits = struct.unpack('<b', file.read(1))[0] ik_link_limit = struct.unpack('<b', file.read(1))[0]
if has_limits: if ik_link_limit == 1:
limit_min = struct.unpack('<3f', file.read(12)) angle_limit = (struct.unpack('<3f', file.read(12)), struct.unpack('<3f', file.read(12)))
limit_max = struct.unpack('<3f', file.read(12)) ik_links.append((ik_link_bone_index, True, angle_limit))
else: else:
limit_min = limit_max = None ik_links.append((ik_link_bone_index, False, None))
ik_links.append((ik_link_bone_index, limit_min, limit_max))
return bone_name, bone_english_name, position, parent_bone_index, layer, flag, tail_position, inherit_bone_parent_index, inherit_bone_parent_influence, fixed_axis, local_x_vector, local_z_vector, external_key, ik_target_bone_index, ik_loop_count, ik_limit_radian, ik_links return PMXBone(bone_name, bone_english_name, position, parent_bone_index, layer,
flag, tail_position, inherit_bone_parent_index, inherit_bone_parent_influence,
fixed_axis, local_x_vector, local_z_vector, external_key,
ik_target_bone_index, ik_loop_count, ik_limit_radian, ik_links)
except Exception as e: def create_armature(model_name: str, bones: list[PMXBone]) -> bpy.types.Object:
print(f"Error reading bone data: {str(e)}") armature = bpy.data.armatures.new(f"{model_name}_Armature")
print(f"Current file position: {file.tell()}") armature_obj = bpy.data.objects.new(f"{model_name}_Armature", armature)
raise bpy.context.collection.objects.link(armature_obj)
def set_bone_local_axis(bone, local_x, local_z):
# Convert from MMD to Blender coordinate system
x_axis = Vector(local_x).xzy
z_axis = Vector(local_z).xzy
y_axis = z_axis.cross(x_axis)
# Create rotation matrix from axes
matrix = Matrix([x_axis, y_axis, z_axis]).transposed()
bone.matrix_local = matrix.to_4x4()
def finalize_armature(armature_obj):
# Apply MMD to Blender space conversion
armature_obj.rotation_euler[0] = 1.5708 # 90 degrees in radians
armature_obj.rotation_euler[2] = 3.14159 # 180 degrees in radians
# Apply scale to armature
armature_obj.scale = (scale, scale, scale)
# Apply transforms
bpy.ops.object.transform_apply(location=True, rotation=True, scale=True)
def create_bones(armature_obj, bones_data, scale=0.08):
bpy.context.view_layer.objects.active = armature_obj bpy.context.view_layer.objects.active = armature_obj
bpy.ops.object.mode_set(mode='EDIT') bpy.ops.object.mode_set(mode='EDIT')
edit_bones = [] # First pass: Create bones with correct positions and sizes
for i, bone_data in enumerate(bones_data): edit_bones = [] # Using a list instead of dict for indexed access
try: for i, bone_data in enumerate(bones):
print(f"Creating bone {i}: {bone_data[0]}") bone_name = f"bone_{i}"
bone = armature_obj.data.edit_bones.new(bone_data[0]) edit_bone = armature.edit_bones.new(bone_name)
edit_bone.head = Vector(bone_data.position)
# Convert and scale head position # Calculate proper tail position with enhanced logic
head_pos = Vector(bone_data[2]).xzy * scale if bone_data.tail_position[0] is not None:
bone.head = head_pos edit_bone.tail = Vector(bone_data.tail_position)
# 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: else:
# Set a default tail position if not provided # Check for special bone types using flags
bone.tail = head_pos + Vector((0, 0.1, 0)) * scale if bone_data.flag & 0x0020: # IK bone
print(f"Using default tail position for bone {bone_data[0]}") bone_length = 0.1
elif bone_data.flag & 0x0100: # Rotation influenced
# Set parent if exists bone_length = 0.08
if bone_data[3] != -1: elif bone_data.flag & 0x0200: # Movement influenced
parent_bone = armature_obj.data.edit_bones[bones_data[bone_data[3]][0]] bone_length = 0.08
bone.parent = parent_bone else:
print(f"Parented bone {bone_data[0]} to {parent_bone.name}") # Find child bones
child_positions = [bones[j].position for j in range(len(bones))
edit_bones.append(bone) if bones[j].parent_index == i]
if child_positions:
except Exception as e: # Use closest child position
print(f"Error creating bone {i}: {str(e)}") closest_child = min(child_positions,
key=lambda p: (Vector(p) - Vector(bone_data.position)).length)
edit_bone.tail = Vector(closest_child)
continue continue
# 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('<i', file.read(4))[0]), 'utf-16-le', errors='replace')
morph_english_name = str(file.read(struct.unpack('<i', file.read(4))[0]), 'utf-16-le', errors='replace')
panel = struct.unpack('<b', file.read(1))[0]
morph_type = struct.unpack('<b', file.read(1))[0]
offset_size = struct.unpack('<i', file.read(4))[0]
morph_data = []
for _ in range(offset_size):
if morph_type == 0: # Group
morph_index = struct.unpack(replace_char(morph_struct, 1, '1'), file.read(morph_bytesize))[0]
morph_value = struct.unpack('<f', file.read(4))[0]
morph_data.append((morph_index, morph_value))
elif morph_type == 1: # Vertex
vertex_index = struct.unpack(replace_char(vertex_struct, 1, '1'), file.read(vertex_size))[0]
position_offset = struct.unpack('<3f', file.read(12))
morph_data.append((vertex_index, position_offset))
elif morph_type == 2: # Bone
bone_index = struct.unpack(bone_struct, file.read(bone_size))[0]
position_offset = struct.unpack('<3f', file.read(12))
rotation_offset = struct.unpack('<4f', file.read(16))
morph_data.append((bone_index, position_offset, rotation_offset))
elif morph_type == 3: # UV
vertex_index = struct.unpack(replace_char(vertex_struct, 1, '1'), file.read(vertex_size))[0]
uv_offset = struct.unpack('<4f', file.read(16))
morph_data.append((vertex_index, uv_offset))
elif morph_type == 4: # UV extended1
vertex_index = struct.unpack(replace_char(vertex_struct, 1, '1'), file.read(vertex_size))[0]
uv_offset = struct.unpack('<4f', file.read(16))
morph_data.append((vertex_index, uv_offset))
elif morph_type == 5: # UV extended2
vertex_index = struct.unpack(replace_char(vertex_struct, 1, '1'), file.read(vertex_size))[0]
uv_offset = struct.unpack('<4f', file.read(16))
morph_data.append((vertex_index, uv_offset))
elif morph_type == 6: # UV extended3
vertex_index = struct.unpack(replace_char(vertex_struct, 1, '1'), file.read(vertex_size))[0]
uv_offset = struct.unpack('<4f', file.read(16))
morph_data.append((vertex_index, uv_offset))
elif morph_type == 7: # UV extended4
vertex_index = struct.unpack(replace_char(vertex_struct, 1, '1'), file.read(vertex_size))[0]
uv_offset = struct.unpack('<4f', file.read(16))
morph_data.append((vertex_index, uv_offset))
elif morph_type == 8: # Material
material_index = struct.unpack(replace_char(material_struct, 1, '1'), file.read(material_size))[0]
offset_type = struct.unpack('<b', file.read(1))[0]
diffuse_offset = struct.unpack('<4f', file.read(16))
specular_offset = struct.unpack('<3f', file.read(12))
specular_factor_offset = struct.unpack('<f', file.read(4))[0]
ambient_offset = struct.unpack('<3f', file.read(12))
edge_color_offset = struct.unpack('<4f', file.read(16))
edge_size_offset = struct.unpack('<f', file.read(4))[0]
texture_factor_offset = struct.unpack('<4f', file.read(16))
sphere_texture_factor_offset = struct.unpack('<4f', file.read(16))
toon_texture_factor_offset = struct.unpack('<4f', file.read(16))
morph_data.append((material_index, offset_type, diffuse_offset, specular_offset, specular_factor_offset, ambient_offset, edge_color_offset, edge_size_offset, texture_factor_offset, sphere_texture_factor_offset, toon_texture_factor_offset))
elif morph_type == 9: # Flip
morph_index = struct.unpack(replace_char(morph_struct, 1, '1'), file.read(morph_bytesize))[0]
morph_value = struct.unpack('<f', file.read(4))[0]
morph_data.append((morph_index, morph_value))
elif morph_type == 10: # Impulse
morph_index = struct.unpack(replace_char(rigid_struct, 1, '1'), file.read(rigid_size))[0]
local_flag = struct.unpack('<b', file.read(1))[0]
movement_speed = struct.unpack('<3f', file.read(12))
rotation_torque = struct.unpack('<3f', file.read(12))
morph_data.append((morph_index, local_flag, movement_speed, rotation_torque))
return morph_name, morph_english_name, panel, morph_type, morph_data
def read_index_size(index, types):
struct = "<??"
byte_size = 0
if index == 1:
struct = replace_char(struct, 2, types[0])
byte_size = 1
elif index == 2:
struct = replace_char(struct,2,types[1])
byte_size = 2
else: else:
struct = replace_char(struct,2,types[2]) # Default length based on bone layer
byte_size = 4 bone_length = 0.1 if bone_data.layer == 0 else 0.05
return struct, byte_size # 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
def import_pmx(filepath, scale=0.08): edit_bones.append(edit_bone)
try:
faces: list[tuple[int,int,int]] = [] # Second pass: Set up hierarchy and orientations
vertices = [] for i, bone_data in enumerate(bones):
textures = [] edit_bone = edit_bones[i]
materials = []
bones = [] # Parent bones
morphs = [] 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: 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
with open(filepath, mode='rb') as file: # Set up index size formats
print("stage 1")
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 = read_pmx_header(file)
print("stage 2")
# Read vertices
print("fix 3")
vertex_count = struct.unpack('<i', file.read(4))[0]
print("stage 3")
print(vertex_count)
#====== Start reading index sizes and create helper prebuilts =====
morph_struct, morph_size = read_index_size(morph_index_size, 'bhi')
vertex_struct, vertex_size = read_index_size(vertex_index_size, 'BHi') vertex_struct, vertex_size = read_index_size(vertex_index_size, 'BHi')
bone_struct, bone_size = read_index_size(bone_index_size, 'bhi') bone_struct, bone_size = read_index_size(bone_index_size, 'bhi')
material_struct, material_size = read_index_size(material_index_size, 'bhi')
texture_struct, texture_size = read_index_size(texture_index_size, 'bhi') texture_struct, texture_size = read_index_size(texture_index_size, 'bhi')
rigid_struct, rigid_size = read_index_size(rigid_body_index_size, 'bhi')
# Read vertices
vertex_count = struct.unpack('<i', file.read(4))[0]
vertices = []
for _ in range(vertex_count): for _ in range(vertex_count):
position, normal, uv, bone_indices, bone_weights, edge_scale, additional_uv_read = read_vertex(file, bone_struct, bone_size, additional_uvs) vertices.append(read_vertex(file, bone_struct, bone_size, additional_uvs))
vertices.append((position, normal, uv, bone_indices, bone_weights, edge_scale))
# Read faces # Read faces
print("stage 4") face_count = struct.unpack('<i', file.read(4))[0] // 3
face_count = struct.unpack('<i', file.read(4))[0] faces = []
print("stage 5") for _ in range(face_count):
def read_data(data, length):
return list(struct.unpack(data, file.read(length)))
face_funct = lambda: print("invalid face funct")
if vertex_index_size == 1: if vertex_index_size == 1:
face_funct = lambda: read_data('<3B',3) faces.append(struct.unpack('<3B', file.read(3)))
elif vertex_index_size == 2: elif vertex_index_size == 2:
face_funct = lambda: read_data('<3H',6) faces.append(struct.unpack('<3H', file.read(6)))
else: else:
face_funct = lambda: read_data('<3i',12) faces.append(struct.unpack('<3i', file.read(12)))
for _ in range(face_count // 3):
faces.append(face_funct())
print("stage 6")
# Read textures # Read textures
texture_count = struct.unpack('<i', file.read(4))[0] texture_count = struct.unpack('<i', file.read(4))[0]
textures = []
for _ in range(texture_count): for _ in range(texture_count):
texture_path = str(file.read(struct.unpack('<i', file.read(4))[0]), 'utf-16-le', errors='replace') texture_path = str(file.read(struct.unpack('<i', file.read(4))[0]), 'utf-16-le', errors='replace')
textures.append(texture_path) textures.append(texture_path)
print("stage 7")
# Read materials # Read materials
material_count = struct.unpack('<i', file.read(4))[0] material_count = struct.unpack('<i', file.read(4))[0]
print("material count "+str(material_count)) materials = []
for _ in range(material_count): for _ in range(material_count):
material_name, material_english_name, diffuse_color, specular_color, specular_strength, ambient_color, flag, edge_color, edge_size, texture_index, sphere_texture_index, sphere_mode, toon_sharing_flag, toon_texture_index, comment, surface_count = read_material(file, texture_struct, texture_size) materials.append(read_material(file, texture_struct, texture_size))
materials.append((material_name, material_english_name, diffuse_color, specular_color, specular_strength, ambient_color, flag, edge_color, edge_size, texture_index, sphere_texture_index, sphere_mode, toon_sharing_flag, toon_texture_index, comment, surface_count))
print("stage 8")
# Read bones # Read bones
bone_count = struct.unpack('<i', file.read(4))[0] bone_count = struct.unpack('<i', file.read(4))[0]
print(f"Starting to read {bone_count} bones") bones = []
bones_read = 0 for _ in range(bone_count):
bones.append(read_bone(file, bone_struct, bone_size))
print("bone count: "+str(bone_count))
for i in range(bone_count):
try:
bone_name, bone_english_name, position, parent_bone_index, layer, flag, tail_position, inherit_bone_parent_index, inherit_bone_parent_influence, fixed_axis, local_x_vector, local_z_vector, external_key, ik_target_bone_index, ik_loop_count, ik_limit_radian, ik_links = read_bone(file, bone_struct, bone_size)
bones.append((bone_name, bone_english_name, position, parent_bone_index, layer, flag, tail_position, inherit_bone_parent_index, inherit_bone_parent_influence, fixed_axis, local_x_vector, local_z_vector, external_key, ik_target_bone_index, ik_loop_count, ik_limit_radian, ik_links))
print(f"Successfully read bone {i}: {bone_name}")
bones_read += 1
except Exception as e:
print(f"Error reading bone {i}: {str(e)}")
print(f"Bytes read position: {file.tell()}")
break
print(f"Finished reading bones. Total bones read: {bones_read}")
# Read morphs
morph_count = struct.unpack('<i', file.read(4))[0]
print("morph count: "+str(morph_count))
for _ in range(morph_count):
morph_name, morph_english_name, panel, morph_type, morph_data = read_morph(file, morph_struct, morph_size, vertex_struct, vertex_size, bone_struct, bone_size, material_struct, material_size, rigid_struct, rigid_size)
morphs.append((morph_name, morph_english_name, panel, morph_type, morph_data))
print("finished reading file!")
except Exception as e:
print(str(e))
# Create mesh and object
mesh = bpy.data.meshes.new(model_name) mesh = bpy.data.meshes.new(model_name)
scaled_vertices = [(Vector(v[0]).xzy * scale) for v in vertices] mesh.from_pydata([v.position for v in vertices], [], faces)
mesh.from_pydata(scaled_vertices, [], faces)
mesh.update() mesh.update()
obj = bpy.data.objects.new(model_name, mesh) obj = bpy.data.objects.new(model_name, mesh)
bpy.context.collection.objects.link(obj) bpy.context.collection.objects.link(obj)
# Assign vertex normals # Create and set up armature
custom_normals = [(Vector(i[1]).xzy).normalized() for i in vertices] armature_obj = create_armature(model_name, bones)
mesh.normals_split_custom_set_from_vertices(custom_normals)
# 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)
# Create main nodes
principled_node = material.node_tree.nodes.new(type="ShaderNodeBsdfPrincipled")
principled_node.location = (0, 300)
output_node = material.node_tree.nodes.new(type="ShaderNodeOutputMaterial")
output_node.location = (300, 300)
# 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
# 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)
# 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)
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 obj.parent = armature_obj
modifier = obj.modifiers.new("Armature", 'ARMATURE')
modifier.object = armature_obj
bpy.context.view_layer.objects.active = armature_obj # Add armature modifier
bpy.ops.object.mode_set(mode='EDIT') mod = obj.modifiers.new(name="Armature", type='ARMATURE')
mod.object = armature_obj
print("Starting bone creation...") # Assign materials and weights
print(f"Total bones to create: {len(bones)}") base_path = os.path.dirname(filepath)
assign_materials(obj, materials, textures, base_path)
assign_vertex_weights(obj, vertices, bones)
# Create the bones using our create_bones function # Set proper scale and orientation
edit_bones = create_bones(armature_obj, bones, scale) armature_obj.scale = (0.08, 0.08, 0.08)
armature_obj.rotation_euler = (1.5708, 0, 0)
# Now we can safely scale and position bones # Select both armature and mesh
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]
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)
#ROTATE LAST!
armature_obj.rotation_euler[0] = 1.5708
armature_obj.rotation_euler[2] = 3.14159
armature_obj.select_set(True) armature_obj.select_set(True)
obj.select_set(True) obj.select_set(True)
bpy.context.view_layer.objects.active = armature_obj
# Apply transforms
bpy.ops.object.transform_apply(location=True, rotation=True, scale=True) bpy.ops.object.transform_apply(location=True, rotation=True, scale=True)
print(f"Successfully imported PMX file: {filepath}") return {'FINISHED'}
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}")
except Exception as e: except Exception as e:
print(f"Error importing PMX file: {filepath}") print(f"Error importing PMX: {str(e)}")
print(f"Error details hhh: {traceback.format_exc()}") traceback.print_exc()
return {'CANCELLED'}