Better, faster and working slightly better.
This commit is contained in:
Yusarina
2024-11-25 21:27:49 +00:00
parent 155c40d4d4
commit b551aac97d
+391 -523
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(16)))
bone_weights = list(struct.unpack('<4f', file.read(4*4)))
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,500 +173,313 @@ 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))
parent_bone_index = struct.unpack(replace_char(string_build, 1, '1'), file.read(byte_size))[0]
layer = struct.unpack('<i', file.read(4))[0]
flag = struct.unpack('<H', file.read(2))[0]
# Initialize bone properties with defaults
tail_position = [0.0, 0.0, 0.0]
tail_index = -1
inherit_bone_parent_index = -1
inherit_bone_parent_influence = 0.0
fixed_axis = [0.0, 0.0, 0.0]
local_x_vector = [0.0, 0.0, 0.0]
local_z_vector = [0.0, 0.0, 0.0]
external_key = 0
ik_target_bone_index = -1
ik_loop_count = 0
ik_limit_radian = 0.0
ik_links = []
# Read flag-dependent data
if not (flag & 0x0001): # Connection not by offset
tail_position = struct.unpack('<3f', file.read(12))
else:
tail_index = struct.unpack(replace_char(string_build, 1, '1'), file.read(byte_size))[0]
if flag & 0x0100 or flag & 0x0200: # Has inheritance
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]
if flag & 0x0400: # Has fixed axis
fixed_axis = struct.unpack('<3f', file.read(12))
if flag & 0x0800: # Has local coordinate
local_x_vector = struct.unpack('<3f', file.read(12))
local_z_vector = struct.unpack('<3f', file.read(12))
if flag & 0x2000: # Has external parent deform
external_key = struct.unpack('<i', file.read(4))[0]
if flag & 0x0020: # Has IK
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_limit_radian = struct.unpack('<f', file.read(4))[0]
ik_link_count = struct.unpack('<i', file.read(4))[0]
for _ in range(ik_link_count):
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]
if has_limits:
limit_min = struct.unpack('<3f', file.read(12))
limit_max = struct.unpack('<3f', file.read(12))
else:
limit_min = limit_max = 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
except Exception as e:
print(f"Error reading bone data: {str(e)}")
print(f"Current file position: {file.tell()}")
raise
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 position = struct.unpack('<3f', file.read(12))
matrix = Matrix([x_axis, y_axis, z_axis]).transposed() parent_bone_index = struct.unpack(replace_char(string_build, 1, '1'), file.read(byte_size))[0]
bone.matrix_local = matrix.to_4x4() layer = struct.unpack('<i', file.read(4))[0]
flag = struct.unpack('<H', file.read(2))[0]
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 tail_position = [None, None, None]
armature_obj.scale = (scale, scale, scale) inherit_bone_parent_index = 0
inherit_bone_parent_influence = 0.0
fixed_axis = [0.0, 0.0, 0.0]
local_x_vector = [0.0, 0.0, 0.0]
local_z_vector = [0.0, 0.0, 0.0]
external_key = 0
ik_target_bone_index = 0
ik_loop_count = -1
ik_limit_radian = 0.0
ik_links = []
if not (flag & 0x0001):
tail_position = struct.unpack('<3f', file.read(12))
else:
tail_index = struct.unpack(replace_char(string_build, 1, '1'), file.read(byte_size))[0]
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_influence = struct.unpack('<f', file.read(4))[0]
if flag & 0x0400:
fixed_axis = struct.unpack('<3f', file.read(12))
if flag & 0x0800:
local_x_vector = struct.unpack('<3f', file.read(12))
local_z_vector = struct.unpack('<3f', file.read(12))
if flag & 0x2000:
external_key = struct.unpack('<i', file.read(4))[0]
if flag & 0x0020:
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_limit_radian = struct.unpack('<f', file.read(4))[0]
ik_link_count = struct.unpack('<i', file.read(4))[0]
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_limit = struct.unpack('<b', file.read(1))[0]
if ik_link_limit == 1:
angle_limit = (struct.unpack('<3f', file.read(12)), struct.unpack('<3f', file.read(12)))
ik_links.append((ik_link_bone_index, True, angle_limit))
else:
ik_links.append((ik_link_bone_index, False, None))
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)
# Apply transforms def create_armature(model_name: str, bones: list[PMXBone]) -> bpy.types.Object:
bpy.ops.object.transform_apply(location=True, rotation=True, scale=True) armature = bpy.data.armatures.new(f"{model_name}_Armature")
armature_obj = bpy.data.objects.new(f"{model_name}_Armature", armature)
def create_bones(armature_obj, bones_data, scale=0.08): bpy.context.collection.objects.link(armature_obj)
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
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
# Set bone hierarchy # Calculate proper tail position with enhanced logic
for i, bone_data in enumerate(bones_data): if bone_data.tail_position[0] is not None:
if bone_data[3] != -1: edit_bone.tail = Vector(bone_data.tail_position)
edit_bones[i].parent = edit_bones[bone_data[3]] else:
# Check for special bone types using flags
# Apply final transforms if bone_data.flag & 0x0020: # IK bone
bpy.ops.object.mode_set(mode='OBJECT') bone_length = 0.1
armature_obj.rotation_euler[0] = 1.5708 elif bone_data.flag & 0x0100: # Rotation influenced
armature_obj.rotation_euler[2] = 3.14159 bone_length = 0.08
armature_obj.select_set(True) elif bone_data.flag & 0x0200: # Movement influenced
bpy.ops.object.transform_apply(location=True, rotation=True, scale=True) bone_length = 0.08
else:
return edit_bones # Find child bones
child_positions = [bones[j].position for j in range(len(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): if bones[j].parent_index == i]
morph_name = str(file.read(struct.unpack('<i', file.read(4))[0]), 'utf-16-le', errors='replace') if child_positions:
morph_english_name = str(file.read(struct.unpack('<i', file.read(4))[0]), 'utf-16-le', errors='replace') # Use closest child position
closest_child = min(child_positions,
panel = struct.unpack('<b', file.read(1))[0] key=lambda p: (Vector(p) - Vector(bone_data.position)).length)
morph_type = struct.unpack('<b', file.read(1))[0] edit_bone.tail = Vector(closest_child)
offset_size = struct.unpack('<i', file.read(4))[0] continue
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:
struct = replace_char(struct,2,types[2])
byte_size = 4
return struct, byte_size
def import_pmx(filepath, scale=0.08):
try:
faces: list[tuple[int,int,int]] = []
vertices = []
textures = []
materials = []
bones = []
morphs = []
try:
with open(filepath, mode='rb') as file:
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')
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')
rigid_struct, rigid_size = read_index_size(rigid_body_index_size, 'bhi')
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((position, normal, uv, bone_indices, bone_weights, edge_scale))
# Read faces
print("stage 4")
face_count = struct.unpack('<i', file.read(4))[0]
print("stage 5")
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:
face_funct = lambda: read_data('<3B',3)
elif vertex_index_size == 2:
face_funct = lambda: read_data('<3H',6)
else: else:
face_funct = lambda: read_data('<3i',12) # Default length based on bone layer
for _ in range(face_count // 3): bone_length = 0.1 if bone_data.layer == 0 else 0.05
faces.append(face_funct())
print("stage 6")
# Read textures
texture_count = struct.unpack('<i', file.read(4))[0]
for _ in range(texture_count):
texture_path = str(file.read(struct.unpack('<i', file.read(4))[0]), 'utf-16-le', errors='replace')
textures.append(texture_path)
print("stage 7")
# Read materials
material_count = struct.unpack('<i', file.read(4))[0]
print("material count "+str(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((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
bone_count = struct.unpack('<i', file.read(4))[0]
print(f"Starting to read {bone_count} bones")
bones_read = 0
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))
mesh = bpy.data.meshes.new(model_name) # Apply calculated length
scaled_vertices = [(Vector(v[0]).xzy * scale) for v in vertices] direction = Vector((0, bone_length, 0))
mesh.from_pydata(scaled_vertices, [], faces) if bone_data.parent_index >= 0:
mesh.update() parent_pos = Vector(bones[bone_data.parent_index].position)
if (Vector(bone_data.position) - parent_pos).length > 0.001:
obj = bpy.data.objects.new(model_name, mesh) direction = (Vector(bone_data.position) - parent_pos).normalized() * bone_length
bpy.context.collection.objects.link(obj) edit_bone.tail = edit_bone.head + direction
# Assign vertex normals edit_bones.append(edit_bone)
custom_normals = [(Vector(i[1]).xzy).normalized() for i in vertices]
mesh.normals_split_custom_set_from_vertices(custom_normals) # Second pass: Set up hierarchy and orientations
for i, bone_data in enumerate(bones):
edit_bone = edit_bones[i]
# Assign UV coordinates # Parent bones
uv_layer = mesh.uv_layers.new() if bone_data.parent_index >= 0:
loop_indices_orig = tuple(i for f in faces for i in f) parent_bone = edit_bones[bone_data.parent_index]
uv_table = {vi:v for vi, v in enumerate([i[2] for i in vertices])} edit_bone.parent = parent_bone
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 # Connect bones only if they should be connected
principled_node = material.node_tree.nodes.new(type="ShaderNodeBsdfPrincipled") if (Vector(bone_data.position) - Vector(parent_bone.tail)).length < 0.01:
principled_node.location = (0, 300) 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") # Create and apply orientation matrix
output_node.location = (300, 300) 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 def assign_vertex_weights(obj: bpy.types.Object, vertices: list[PMXVertex], bones: list[PMXBone]):
principled_node.inputs["Base Color"].default_value = material_data[2] # Pre-create vertex groups
principled_node.inputs["Specular IOR Level"].default_value = material_data[4] vertex_groups = {}
principled_node.inputs["Roughness"].default_value = 0.5 for bone in bones:
principled_node.inputs["Metallic"].default_value = 0.0 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 def assign_materials(obj: bpy.types.Object, materials: list[PMXMaterial], textures: list[str], base_path: str):
if material_data[2][3] < 0.99: current_face_index = 0
material.blend_method = 'HASHED'
material.use_backface_culling = False for material in materials:
material.alpha_threshold = 0.5 # Create or get material
material.show_transparent_back = True mat_name = material.name or f"Material_{len(obj.data.materials)}"
if mat_name in bpy.data.materials:
# Create mix shader for transparency mat = bpy.data.materials[mat_name]
mix_shader = material.node_tree.nodes.new(type='ShaderNodeMixShader') else:
mix_shader.location = (100, 300) mat = bpy.data.materials.new(name=mat_name)
transparent_node = material.node_tree.nodes.new(type='ShaderNodeBsdfTransparent')
transparent_node.location = (-200, 200) # 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 def import_pmx(filepath: str):
material.node_tree.links.new(mix_shader.inputs[0], albedo_node.outputs["Alpha"]) try:
material.node_tree.links.new(mix_shader.inputs[1], transparent_node.outputs[0]) with open(filepath, 'rb') as file:
material.node_tree.links.new(mix_shader.inputs[2], principled_node.outputs[0]) # Read header
material.node_tree.links.new(output_node.inputs["Surface"], mix_shader.outputs[0]) header_data = read_pmx_header(file)
else: version, encoding, additional_uvs, vertex_index_size, texture_index_size, \
material.blend_method = 'OPAQUE' material_index_size, bone_index_size, morph_index_size, rigid_body_index_size, \
material.node_tree.links.new(output_node.inputs["Surface"], principled_node.outputs[0]) model_name, model_english_name, model_comment, model_english_comment = header_data
# 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] # Set up index size formats
vertex_struct, vertex_size = read_index_size(vertex_index_size, 'BHi')
# Create armature and assign bones bone_struct, bone_size = read_index_size(bone_index_size, 'bhi')
armature = bpy.data.armatures.new(model_name + "_Armature") texture_struct, texture_size = read_index_size(texture_index_size, 'bhi')
armature_obj = bpy.data.objects.new(model_name + "_Armature", armature)
bpy.context.collection.objects.link(armature_obj) # Read vertices
obj.parent = armature_obj vertex_count = struct.unpack('<i', file.read(4))[0]
modifier = obj.modifiers.new("Armature", 'ARMATURE') vertices = []
modifier.object = armature_obj for _ in range(vertex_count):
vertices.append(read_vertex(file, bone_struct, bone_size, additional_uvs))
bpy.context.view_layer.objects.active = armature_obj
bpy.ops.object.mode_set(mode='EDIT') # Read faces
face_count = struct.unpack('<i', file.read(4))[0] // 3
print("Starting bone creation...") faces = []
print(f"Total bones to create: {len(bones)}") for _ in range(face_count):
if vertex_index_size == 1:
# Create the bones using our create_bones function faces.append(struct.unpack('<3B', file.read(3)))
edit_bones = create_bones(armature_obj, bones, scale) elif vertex_index_size == 2:
faces.append(struct.unpack('<3H', file.read(6)))
# Now we can safely scale and position bones else:
for bone in armature.edit_bones: faces.append(struct.unpack('<3i', file.read(12)))
bone_data = next(b for b in bones if b[0] == bone.name)
bone.head = Vector(bone_data[2]).xzy * scale # Read textures
if bone_data[6][0] is not None: texture_count = struct.unpack('<i', file.read(4))[0]
bone.tail = Vector(bone_data[6]).xzy * scale textures = []
else: for _ in range(texture_count):
bone.tail = bone.head + Vector((0, 0.1 * scale)) texture_path = str(file.read(struct.unpack('<i', file.read(4))[0]), 'utf-16-le', errors='replace')
textures.append(texture_path)
# Assign bone weights to the mesh
for i, vertex in enumerate(vertices): # Read materials
for j in range(0, len(vertex[3])): material_count = struct.unpack('<i', file.read(4))[0]
if vertex[3][j] != -1 and vertex[3][j] < len(bones): materials = []
bone_name = bones[vertex[3][j]][0] for _ in range(material_count):
weight = vertex[4][j] materials.append(read_material(file, texture_struct, texture_size))
vertex_group = obj.vertex_groups.get(bone_name) # Read bones
if not vertex_group: bone_count = struct.unpack('<i', file.read(4))[0]
vertex_group = obj.vertex_groups.new(name=bone_name) bones = []
for _ in range(bone_count):
vertex_group.add([i], weight, 'REPLACE') bones.append(read_bone(file, bone_struct, bone_size))
# Assign morphs to the mesh # Create mesh and object
for morph_data in morphs: mesh = bpy.data.meshes.new(model_name)
morph_name = morph_data[0] mesh.from_pydata([v.position for v in vertices], [], faces)
morph_type = morph_data[3] mesh.update()
obj = bpy.data.objects.new(model_name, mesh)
bpy.context.collection.objects.link(obj)
# Create and set up armature
armature_obj = create_armature(model_name, bones)
obj.parent = armature_obj
# Add armature modifier
mod = obj.modifiers.new(name="Armature", type='ARMATURE')
mod.object = armature_obj
# Assign materials and weights
base_path = os.path.dirname(filepath)
assign_materials(obj, materials, textures, base_path)
assign_vertex_weights(obj, vertices, bones)
# Set proper scale and orientation
armature_obj.scale = (0.08, 0.08, 0.08)
armature_obj.rotation_euler = (1.5708, 0, 0)
# Select both armature and mesh
armature_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)
return {'FINISHED'}
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)
obj.select_set(True)
bpy.ops.object.transform_apply(location=True, rotation=True, scale=True)
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}")
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'}