594 lines
No EOL
21 KiB
Python
594 lines
No EOL
21 KiB
Python
# -*- coding: utf-8 -*-
|
||
|
||
import csv
|
||
import bpy
|
||
import re
|
||
import os
|
||
from math import pi
|
||
from mathutils import Vector
|
||
# Based on powroupi the MMD Translation script combined with a Hogarth-MMD Translation csv that has been modified to select names as close as possible
|
||
# This instead uses a predefined list that is Hifi Compatable.
|
||
|
||
# Clone of shorthand to full
|
||
jp_half_to_full_tuples = (
|
||
('ヴ', 'ヴ'), ('ガ', 'ガ'), ('ギ', 'ギ'), ('グ', 'グ'), ('ゲ', 'ゲ'),
|
||
('ゴ', 'ゴ'), ('ザ', 'ザ'), ('ジ', 'ジ'), ('ズ', 'ズ'), ('ゼ', 'ゼ'),
|
||
('ゾ', 'ゾ'), ('ダ', 'ダ'), ('ヂ', 'ヂ'), ('ヅ', 'ヅ'), ('デ', 'デ'),
|
||
('ド', 'ド'), ('バ', 'バ'), ('パ', 'パ'), ('ビ', 'ビ'), ('ピ', 'ピ'),
|
||
('ブ', 'ブ'), ('プ', 'プ'), ('ベ', 'ベ'), ('ペ', 'ペ'), ('ボ', 'ボ'),
|
||
('ポ', 'ポ'), ('。', '。'), ('「', '「'), ('」', '」'), ('、', '、'),
|
||
('・', '・'), ('ヲ', 'ヲ'), ('ァ', 'ァ'), ('ィ', 'ィ'), ('ゥ', 'ゥ'),
|
||
('ェ', 'ェ'), ('ォ', 'ォ'), ('ャ', 'ャ'), ('ュ', 'ュ'), ('ョ', 'ョ'),
|
||
('ッ', 'ッ'), ('ー', 'ー'), ('ア', 'ア'), ('イ', 'イ'), ('ウ', 'ウ'),
|
||
('エ', 'エ'), ('オ', 'オ'), ('カ', 'カ'), ('キ', 'キ'), ('ク', 'ク'),
|
||
('ケ', 'ケ'), ('コ', 'コ'), ('サ', 'サ'), ('シ', 'シ'), ('ス', 'ス'),
|
||
('セ', 'セ'), ('ソ', 'ソ'), ('タ', 'タ'), ('チ', 'チ'), ('ツ', 'ツ'),
|
||
('テ', 'テ'), ('ト', 'ト'), ('ナ', 'ナ'), ('ニ', 'ニ'), ('ヌ', 'ヌ'),
|
||
('ネ', 'ネ'), ('ノ', 'ノ'), ('ハ', 'ハ'), ('ヒ', 'ヒ'), ('フ', 'フ'),
|
||
('ヘ', 'ヘ'), ('ホ', 'ホ'), ('マ', 'マ'), ('ミ', 'ミ'), ('ム', 'ム'),
|
||
('メ', 'メ'), ('モ', 'モ'), ('ヤ', 'ヤ'), ('ユ', 'ユ'), ('ヨ', 'ヨ'),
|
||
('ラ', 'ラ'), ('リ', 'リ'), ('ル', 'ル'), ('レ', 'レ'), ('ロ', 'ロ'),
|
||
('ワ', 'ワ'), ('ン', 'ン'),
|
||
)
|
||
|
||
|
||
bones_to_remove = [
|
||
"Center",
|
||
"Groove",
|
||
"CenterTop",
|
||
"ParentNode",
|
||
"Eyes",
|
||
"Dummy",
|
||
"Root",
|
||
"Base",
|
||
"ControlNode"
|
||
]
|
||
|
||
contains_to_remove = [
|
||
"_dummy_",
|
||
"_shadow_",
|
||
"IK",
|
||
"Dummy",
|
||
"Top",
|
||
"Tip",
|
||
"Twist",
|
||
"ShoulderP",
|
||
"ShoulderC",
|
||
"LegD",
|
||
"FootD",
|
||
"mmd_edge",
|
||
"mmd_vertex",
|
||
"Node",
|
||
"Center",
|
||
"Control"
|
||
|
||
]
|
||
|
||
bones_to_correct = [
|
||
"Arm",
|
||
"LeftHand",
|
||
"RightHand",
|
||
"Finger",
|
||
"Thumb",
|
||
"Leg",
|
||
"Foot",
|
||
"Shoulder"
|
||
]
|
||
|
||
bones_to_correct_rolls = [
|
||
135,
|
||
215,
|
||
-215,
|
||
135,
|
||
55,
|
||
180,
|
||
180,
|
||
-180
|
||
]
|
||
|
||
parent_structure = {
|
||
#Bone : Parent
|
||
"LeftToe": "LeftFoot",
|
||
"RightToe": "RightFoot",
|
||
"LeftFoot": "LeftLeg",
|
||
"RightFoot": "RightLeg",
|
||
"LeftLeg": "LeftUpLeg",
|
||
"RightLeg": "RightUpLeg",
|
||
"LeftUpLeg": "Hips",
|
||
"RightUpLeg": "Hips",
|
||
"Spine1": "Spine",
|
||
"Spine2": "Spine1",
|
||
"Spine": "Hips",
|
||
"Neck": "Spine2",
|
||
"Head": "Neck",
|
||
"LeftShoulder": "Spine2",
|
||
"RightShoulder": "Spine2",
|
||
"LeftArm": "LeftShoulder",
|
||
"RightArm": "RightShoulder",
|
||
"LeftForeArm": "LeftArm",
|
||
"RightForeArm": "RightArm",
|
||
"LeftHand": "LeftForeArm",
|
||
"RightHand": "RightForeArm",
|
||
"RightEye": "Head",
|
||
"LeftEye": "Head"
|
||
}
|
||
|
||
# Simplified Translator based on powroupi MMDTranslation
|
||
class MMDTranslator:
|
||
def __init__(self):
|
||
self.translation_dict = []
|
||
split_pattern = re.compile(",\s")
|
||
remove_end = re.compile(",$")
|
||
|
||
print(__file__)
|
||
try:
|
||
local = os.path.dirname(os.path.abspath(__file__))
|
||
filename = os.path.join(local, 'mmd_hifi_dict.csv')
|
||
with open(filename, 'r', encoding='utf-8', errors='ignore') as f:
|
||
try:
|
||
stream = csv.reader(
|
||
f, delimiter=',', quotechar='"', skipinitialspace=True)
|
||
self.translation_dict = [tuple(row)
|
||
for row in stream if len(row) >= 2]
|
||
finally:
|
||
f.close()
|
||
print("Translation File Loaded")
|
||
except FileNotFoundError: #Probably just developing then
|
||
print("Reading Editor file: This only should show when developing")
|
||
stream = bpy.data.texts["mmd_hifi_dict.csv"].lines
|
||
for line in stream:
|
||
body = line.body
|
||
body = body.replace('"', '')
|
||
body = re.sub(remove_end, "", body)
|
||
|
||
tup = split_pattern.split(body)
|
||
self.translation_dict.append( tuple(tup))
|
||
|
||
print("Done Reading List")
|
||
print("|||||||||||||||||||||||||||||||||||||||||")
|
||
|
||
|
||
@staticmethod
|
||
def replace_from_tuples(name, tuples):
|
||
for pair in tuples:
|
||
if pair[0] in name:
|
||
updated_name = name.replace(pair[0], pair[1])
|
||
name = updated_name
|
||
return name
|
||
|
||
def half_to_full(self, name):
|
||
return self.replace_from_tuples(name, jp_half_to_full_tuples)
|
||
|
||
def is_translated(self, name):
|
||
try:
|
||
name.encode('ascii', errors='strict')
|
||
except UnicodeEncodeError:
|
||
return False
|
||
return True
|
||
|
||
def translate(self, name):
|
||
full_name = self.half_to_full(name) # First Updates short hand to full
|
||
|
||
translated_name = self.replace_from_tuples(full_name, self.translation_dict)
|
||
|
||
return purge_string(translated_name)
|
||
|
||
|
||
def purge_string(string):
|
||
try:
|
||
str_bytes = os.fsencode(string)
|
||
return str_bytes.decode("utf-8", "replace")
|
||
except UnicodeEncodeError:
|
||
print("Could not purge string", string)
|
||
return string
|
||
|
||
|
||
def delete_self_and_children(me):
|
||
|
||
print(bpy.context.mode)
|
||
if bpy.context.mode != 'OBJECT':
|
||
bpy.ops.object.mode_set(mode='OBJECT')
|
||
|
||
me.hide = False
|
||
me.hide_select = False
|
||
me.select = True
|
||
bpy.context.scene.objects.active = me
|
||
|
||
for child in me.children:
|
||
child.hide = False
|
||
child.hide_select = False
|
||
child.select = True
|
||
|
||
bpy.ops.object.delete()
|
||
|
||
|
||
|
||
#####################################################
|
||
# Armature Fixes:
|
||
|
||
def translate_bones(Translator, bones):
|
||
for idx, bone in enumerate(bones):
|
||
bone.hide = False
|
||
bone.hide_select = False
|
||
|
||
translated = Translator.translate(bone.name)
|
||
bone.name = translated
|
||
|
||
|
||
def has_removable(val):
|
||
for word in contains_to_remove:
|
||
if word in val:
|
||
return True
|
||
return False
|
||
|
||
def correct_bone_parents(bones):
|
||
|
||
keys = parent_structure.keys();
|
||
|
||
for bone in bones:
|
||
if bone.name == "Hips":
|
||
bone.parent = None
|
||
else:
|
||
parent = parent_structure.get(bone.name)
|
||
if parent is not None:
|
||
parent_bone = bones.get(parent)
|
||
if parent_bone is not None:
|
||
bone.parent = parent_bone
|
||
|
||
|
||
def correct_bone_rotations(obj):
|
||
if "Eye" in obj.name:
|
||
print(" ", obj.name)
|
||
bone_head = Vector(obj.head)
|
||
bone_head.z += 0.05
|
||
obj.tail = bone_head
|
||
elif "Hips" == obj.name:
|
||
bone_head = Vector(obj.head)
|
||
bone_tail = Vector(obj.tail)
|
||
if bone_head.z > bone_tail.z:
|
||
obj.head = bone_tail
|
||
obj.tail = bone_head
|
||
else:
|
||
print("Hips already correct")
|
||
else:
|
||
for idx, name in enumerate(bones_to_correct):
|
||
if name in obj.name:
|
||
print(" Updating Roll of",obj.name, "with", bones_to_correct_rolls[idx], "current", obj.roll)
|
||
obj.roll = bones_to_correct_rolls[idx] * (pi/180)
|
||
|
||
def clean_up_bones(obj):
|
||
_to_remove = []
|
||
|
||
pose_bones = obj.pose.bones
|
||
|
||
bpy.ops.object.mode_set(mode='EDIT')
|
||
updated_context = bpy.data.objects[obj.name]
|
||
edit_bones = updated_context.data.edit_bones
|
||
|
||
bpy.ops.object.mode_set(mode='OBJECT')
|
||
for bone in obj.data.bones:
|
||
pose_bone = pose_bones.get(bone.name)
|
||
|
||
bpy.ops.object.mode_set(mode='EDIT') # Oddly needed to get the edit bone reference
|
||
edit_bone = edit_bones.get(bone.name)
|
||
|
||
print(" Bone ", bone.name)
|
||
|
||
|
||
if pose_bone is not None and edit_bone is not None:
|
||
if has_removable(bone.name) or bone.name in bones_to_remove:
|
||
if bone.name != "HeadTop" and edit_bone not in _to_remove: #There is an odd chance of duplicates, not sure why!
|
||
_to_remove.append(edit_bone)
|
||
print(" - Added to removal list", bone.name)
|
||
else:
|
||
print(" + Setting Pose Mode for", pose_bone)
|
||
|
||
print(" - Removing Constraints from", pose_bone.name)
|
||
for constraint in pose_bone.constraints:
|
||
pose_bone.constraints.remove(constraint)
|
||
|
||
print(" # Check Rotations")
|
||
correct_bone_rotations(edit_bone)
|
||
|
||
|
||
|
||
print(" Cleaning ", len(_to_remove), " unusable bones")
|
||
|
||
for removal_bone in _to_remove:
|
||
if removal_bone is not None:
|
||
print(" Remove Bone:", removal_bone)
|
||
edit_bones.remove(removal_bone)
|
||
|
||
|
||
bpy.ops.object.mode_set(mode='EDIT')
|
||
edit_bones = updated_context.data.edit_bones
|
||
|
||
print("Manipulating", obj.name)
|
||
if edit_bones.get("Spine1") is None:
|
||
print("Couldnt Detect Spine1, Creating Out of Spine2")
|
||
spine = edit_bones.get("Spine2")
|
||
spine.select = True
|
||
bpy.ops.armature.subdivide()
|
||
spine.name = "Spine1"
|
||
spine = edit_bones.get("Spine2.001")
|
||
spine.name = "Spine2"
|
||
|
||
|
||
edit_bones = updated_context.data.edit_bones
|
||
correct_bone_parents(edit_bones)
|
||
|
||
bpy.ops.object.mode_set(mode='OBJECT')
|
||
|
||
|
||
def convert_bones(Translator, obj):
|
||
bones = obj.data.bones
|
||
|
||
print("Translating ", len(bones), " bones")
|
||
# Translate Bones first. Have to do separate from the next loop
|
||
translate_bones(Translator, bones)
|
||
print("Cleaning Up Bones")
|
||
clean_up_bones(obj)
|
||
|
||
|
||
def has_armature_as_child(me):
|
||
for child in me.children:
|
||
if child.type == "ARMATURE":
|
||
return True
|
||
return False
|
||
|
||
|
||
#####################################################
|
||
# Material Fixes
|
||
|
||
texture_override = re.compile("^((toon-?)|[a-zA-Z]{1,2})\d*\.?\d*$")
|
||
sphere_test = re.compile("Sphere")
|
||
spa_file_test = re.compile("(/spa|h/)|(.spa|h$)")
|
||
toon_file_test = re.compile("/toon/")
|
||
def clean_textures(Translator, material):
|
||
name = []
|
||
|
||
for idx, texture_slot in enumerate(material.texture_slots):
|
||
if texture_slot is not None and texture_slot.texture is not None and texture_slot.texture.image is not None:
|
||
translated_name = Translator.translate(texture_slot.name)
|
||
print( "# Checking", translated_name)
|
||
image = texture_slot.texture.image.filepath_raw
|
||
if not texture_slot.use or texture_override.match(translated_name) or sphere_test.search(translated_name) is not None or spa_file_test.search(image) is not None or toon_file_test.search(image) is not None:
|
||
print(" - Removing Texture", translated_name, image)
|
||
|
||
try:
|
||
bpy.data.textures.remove(texture_slot.texture)
|
||
except (RuntimeError, TypeError): # catch exception if there are remaining texture users or texture is None
|
||
pass
|
||
|
||
material.texture_slots.clear(idx)
|
||
else:
|
||
texture_slot.texture.name = translated_name
|
||
name.append(translated_name)
|
||
|
||
return name
|
||
|
||
def merge_textures(unique_textures, materials_slots):
|
||
|
||
for key in unique_textures.keys():
|
||
material_list = unique_textures[key]
|
||
|
||
if material_list is None:
|
||
continue
|
||
|
||
print("Creating new material Texture", key)
|
||
n = material_list.pop(0)
|
||
first_material = materials_slots.get(n)
|
||
if first_material is None:
|
||
print("Could not find", first_material)
|
||
continue
|
||
root_material = key + "_material"
|
||
first_material.material.name = root_material
|
||
root_index = materials_slots.find(root_material)
|
||
|
||
|
||
|
||
bpy.ops.object.mode_set(mode='EDIT')
|
||
bpy.ops.mesh.select_all(action='DESELECT')
|
||
if len(material_list) > 0:
|
||
|
||
for material in material_list:
|
||
index = materials_slots.find(material)
|
||
if index > -1:
|
||
print(" + Selecting", key, material)
|
||
bpy.context.object.active_material_index = index
|
||
bpy.ops.object.material_slot_select()
|
||
|
||
print(" + Selecting", key)
|
||
bpy.context.object.active_material_index = root_index
|
||
bpy.ops.object.material_slot_select()
|
||
|
||
print(" + Assigning to", key)
|
||
bpy.ops.object.material_slot_assign()
|
||
|
||
|
||
bpy.ops.object.mode_set(mode='OBJECT')
|
||
print(" - Clean up", key)
|
||
if len(material_list) > 0:
|
||
for material in material_list:
|
||
index = materials_slots.find(material)
|
||
|
||
if index > -1:
|
||
print(" - Deleting", material)
|
||
bpy.context.object.active_material_index = index
|
||
bpy.ops.object.material_slot_remove()
|
||
|
||
|
||
def make_materials_fullbright(materials_slots):
|
||
for material_slot in materials_slots:
|
||
print(material_slot)
|
||
material = material_slot.material
|
||
material.specular_shader = 'WARDISO'
|
||
material.use_shadeless = True
|
||
material.specular_color = (0,0,0)
|
||
|
||
|
||
def clean_materials(Translator, materials_slots):
|
||
try:
|
||
print("#######################################")
|
||
print("Cleaning Materials")
|
||
|
||
_unique_textures = {}
|
||
for material_slot in materials_slots:
|
||
if material_slot is not None and material_slot.material is not None:
|
||
material_slot.material.name = Translator.translate(material_slot.name)
|
||
texture_names = clean_textures(Translator, material_slot.material)
|
||
|
||
for texture_name in texture_names:
|
||
if texture_name is not None and _unique_textures.get(texture_name) is None:
|
||
_unique_textures[texture_name] = [material_slot.material.name]
|
||
elif texture_name is not None:
|
||
_unique_textures[texture_name].append(material_slot.material.name)
|
||
|
||
|
||
print("Found", len(_unique_textures.keys()), "unique textures from", len(materials_slots), "slots")
|
||
|
||
merge_textures(_unique_textures, materials_slots)
|
||
except Exception as args:
|
||
print("ERROR OCCURRED WHILE TRYING TO PROCESS TEXTURES", args)
|
||
# make_materials_fullbright(materials_slots) # enable this only if fullbright avatars every become supported
|
||
|
||
#####################################################
|
||
# Mesh Fixes:
|
||
|
||
def translate_shape_keys(Translator, shape_keys):
|
||
print(" Translating shapekeys ", shape_keys.items())
|
||
for key in shape_keys.items():
|
||
print(key)
|
||
shape_key = key[1]
|
||
shape_key.name = Translator.translate(shape_key.name)
|
||
|
||
if shape_key.name == "GoUp":
|
||
shape_key.name = "BrowsU_C"
|
||
elif shape_key.name == "GoDown":
|
||
shape_key.name = "BrowsD_C"
|
||
|
||
|
||
def mix_weights(a,b):
|
||
print(" Mixing",a,b)
|
||
bpy.ops.object.modifier_add(type='VERTEX_WEIGHT_MIX')
|
||
bpy.context.object.modifiers["VertexWeightMix"].vertex_group_a = a
|
||
bpy.context.object.modifiers["VertexWeightMix"].vertex_group_b = b
|
||
bpy.context.object.modifiers["VertexWeightMix"].mix_mode = 'ADD'
|
||
|
||
bpy.context.object.modifiers["VertexWeightMix"].mix_set = 'OR'
|
||
bpy.ops.object.modifier_apply(apply_as='DATA', modifier="VertexWeightMix")
|
||
|
||
|
||
def fix_vertex_groups(obj):
|
||
vertex_groups = obj.vertex_groups
|
||
arm_re = re.compile("ArmTwist\d?$")
|
||
hand_re = re.compile("HandTwist\d?$")
|
||
|
||
shoulder_re = re.compile("ShoulderP|C$")
|
||
leg_re = re.compile("LegD$")
|
||
foot_re = re.compile("FootD$")
|
||
|
||
remove_list = []
|
||
for vertex_group in vertex_groups:
|
||
if "IK" in vertex_group.name:
|
||
remove_list.append(vertex_group.name)
|
||
|
||
if "Eyes" == vertex_group.name:
|
||
mix_weights("Head", "Eyes")
|
||
remove_list.append(vertex_group.name)
|
||
|
||
elif "ArmTwist" in vertex_group.name:
|
||
root = re.sub(arm_re, "Arm", vertex_group.name)
|
||
parent = vertex_groups.get(root)
|
||
if parent is not None:
|
||
mix_weights(root, vertex_group.name)
|
||
remove_list.append(vertex_group.name)
|
||
|
||
elif re.search(shoulder_re, vertex_group.name) != None:
|
||
root = re.sub(shoulder_re, "Shoulder", vertex_group.name)
|
||
parent = vertex_groups.get(root)
|
||
if parent is not None:
|
||
mix_weights(root, vertex_group.name)
|
||
remove_list.append(vertex_group.name)
|
||
elif re.search(leg_re, vertex_group.name) != None:
|
||
root = re.sub(leg_re, "Leg", vertex_group.name)
|
||
parent = vertex_groups.get(root)
|
||
if parent is not None:
|
||
mix_weights(root, vertex_group.name)
|
||
remove_list.append(vertex_group.name)
|
||
|
||
elif re.search(foot_re, vertex_group.name) != None:
|
||
root = re.sub(foot_re, "Foot", vertex_group.name)
|
||
parent = vertex_groups.get(root)
|
||
if parent is not None:
|
||
mix_weights(root, vertex_group.name)
|
||
remove_list.append(vertex_group.name)
|
||
|
||
elif "HandTwist" in vertex_group.name:
|
||
root = re.sub(hand_re, "ForeArm", vertex_group.name)
|
||
parent = vertex_groups.get(root)
|
||
if parent is not None:
|
||
mix_weights(root, vertex_group.name)
|
||
remove_list.append(vertex_group.name)
|
||
|
||
print(" Cleaning Unused vertex groups")
|
||
for group in remove_list:
|
||
print("Removing VertexGroups", group)
|
||
vertex_groups.remove(vertex_groups.get(group))
|
||
|
||
def clean_mesh(Translator, obj):
|
||
bpy.ops.object.mode_set(mode='OBJECT')
|
||
print(" Converting",obj.name,"Mesh")
|
||
translate_shape_keys(Translator, obj.data.shape_keys.key_blocks)
|
||
fix_vertex_groups(obj)
|
||
|
||
#### --------------------
|
||
|
||
def parse_mmd_avatar_hifi():
|
||
# Should Probably have a confirmation dialog when using this.
|
||
original_type = bpy.context.area.type
|
||
bpy.context.area.type = 'VIEW_3D'
|
||
|
||
Translator = MMDTranslator()
|
||
# Change mode to object mode
|
||
|
||
marked_for_purge = []
|
||
marked_for_deletion = []
|
||
|
||
bpy.ops.object.mode_set(mode='OBJECT')
|
||
|
||
for scene in bpy.data.scenes:
|
||
for obj in scene.objects:
|
||
bpy.ops.object.select_all(action='DESELECT')
|
||
if obj is not None:
|
||
obj.select = True
|
||
bpy.context.scene.objects.active = obj
|
||
# Delete joints and rigid bodies items. Perhaps later convert this to flow.
|
||
print("Reading", obj.name)
|
||
if obj.name == "joints" or obj.name == "rigidbodies":
|
||
marked_for_purge.append(obj)
|
||
elif obj.type == "EMPTY" and obj.parent is None:
|
||
marked_for_deletion.append(obj)
|
||
|
||
elif obj.type == 'ARMATURE':
|
||
print(obj.name, obj.type)
|
||
convert_bones(Translator, obj)
|
||
|
||
elif obj.type == 'MESH' and obj.parent is not None and obj.parent.type == 'ARMATURE':
|
||
clean_mesh(Translator, obj)
|
||
bpy.ops.object.mode_set(mode='OBJECT')
|
||
clean_materials(Translator, obj.material_slots)
|
||
|
||
# translate armature names!
|
||
|
||
bpy.ops.object.select_all(action='DESELECT')
|
||
for deletion in marked_for_deletion:
|
||
deletion.select = True
|
||
bpy.context.scene.objects.active = deletion
|
||
bpy.ops.object.delete()
|
||
|
||
bpy.ops.object.select_all(action='DESELECT')
|
||
for deletion in marked_for_purge:
|
||
delete_self_and_children(deletion)
|
||
|
||
bpy.context.area.type = original_type |