import bpy import collections import configparser import datetime import mathutils import os import struct class VmdExporter(): scene = bpy.context.scene timeline_markers = bpy.context.scene.timeline_markers obj = bpy.context.active_object arm = None # export prop frame_start = scene.frame_start frame_end = scene.frame_end frame_size = frame_end - frame_start + 1 scale = 1.0 / 0.2 joint_opt = False # config file prop config_file_name = "config.ini" Section = collections.namedtuple("Section", "config bone bone_isolated") section = Section("config", "bone", "bone_isolated") ConfigKey = collections.namedtuple("ConfigKey", "folder file") config_key = ConfigKey("folder", "file") config_section_bone = [] config_section_bone_with_constraints = [] config_section_bone_isolated = {} export_bone_number = 0 # vmd data prop ipo_list = [] path = None meta = "Vocaloid Motion Data 0002" name = "" # internal data struct BonePair = collections.namedtuple("BonePair", "child parent") # option frame_offset = 0 option_marker_mode = False option_export_log = True # log text log = [] def execute(self): self.export_vmd() if self.option_export_log: if self.path is not None: self.export_log() def export_vmd(self): if self.check() is False: return if self.get_config() is False: return self.init_ipo_list() with open(self.path, "wb") as file: self.write_str(file, 30, self.meta) self.write_str(file, 20, self.name) self.export_all_bone_data(file) self.write_long(file, 0) # 表情キーフレーム数 self.write_long(file, 0) # カメラキーフレーム数 self.write_long(file, 0) # 照明キーフレーム数 self.write_long(file, 0) # セルフ影キーフレーム数 self.write_long(file, 0) # モデル表示・IK on/offキーフレーム数 def check(self): if self.obj is None: self.print_all('can not find object') return False if self.obj.type != 'ARMATURE': self.print_all('selected object is not armature : ' + self.obj.type) return False if self.obj.mode == 'EDIT': self.print_all('selected object is edit mode') return False self.arm = self.obj.pose # オブジェクト名じゃなくアーマチュア名を使用 self.name = self.obj.data.name if self.arm is None: self.print_all('can not find armature') return False if self.config_file_name not in bpy.context.blend_data.texts: self.print_all('can not find config file : name=' + self.config_file_name) return False print("check process : OK") return True def init_ipo_list(self): self.ipo_list = [20] * 2 self.ipo_list.extend([0] * 2) self.ipo_list.extend([20] * 4) self.ipo_list.extend([107] * 8) self.ipo_list.extend([20] * 7) self.ipo_list.extend([107] * 8) self.ipo_list.extend([0]) self.ipo_list.extend([20] * 6) self.ipo_list.extend([107] * 8) self.ipo_list.extend([0] * 2) self.ipo_list.extend([20] * 5) self.ipo_list.extend([107] * 8) self.ipo_list.extend([0] * 3) def get_config(self): text = bpy.context.blend_data.texts[self.config_file_name] config = configparser.ConfigParser(allow_no_value=True) config.optionxform = str try: config.read_string(text.as_string()) except Exception as ex: print("read config file error : " + str(ex)) return False # セクション存在確認 for section_name in self.section: if section_name not in config.sections(): print("can not find section : " + section_name) return False # configセクション取得 folder = config[self.section.config][self.config_key.folder] file = config[self.section.config][self.config_key.file] if not folder: print("can not find folder") return False if not file: print("can not find file") return False # フォルダ権限チェック if os.access(folder, os.W_OK) is False: print("permission denied : " + folder) return False # if os.path.exists(folder) is False: # os.makedirs(folder) self.path = os.path.join(folder, file) self.print_all("export file path : " + self.path) # ボーン名取得 self.config_section_bone = config[self.section.bone] self.config_section_bone_isolated = config[self.section.bone_isolated] # セクション間重複チェック bone_names_all = list(self.config_section_bone.keys()) + list(self.config_section_bone_isolated.keys()) if self.check_duplicate(bone_names_all) is False: return False # ボーン存在確認 if self.check_bone_exist(self.config_section_bone) is False: return False if self.check_bone_exist(self.config_section_bone_isolated) is False: return False if self.check_bone_exist(self.config_section_bone_isolated.values()) is False: return False self.export_bone_number = len(bone_names_all) self.print_all("export bone number : " + str(self.export_bone_number)) print("get config process : OK") return True def check_bone_exist(self, bone_names): for bone_name in bone_names: if bone_name not in self.arm.bones: self.print_all("can not find bone : " + bone_name) return False return True def check_duplicate(self, name_list): names_tmp = set() duplicated_names = [x for x in name_list if x in names_tmp or names_tmp.add(x)] if len(duplicated_names) > 0: for name in duplicated_names: self.print_all("duplicate name exists : " + name) return False return True def export_all_bone_data(self, file): if self.option_marker_mode: export_markers = [marker for marker in self.timeline_markers if (self.frame_start <= marker.frame and marker.frame <= self.frame_end)] self.write_long(file, len(export_markers) * self.export_bone_number) else: self.write_long(file, self.frame_size * self.export_bone_number) if self.export_bone_number <= 0: return # 出力するボーンが無い場合は、出力フレーム数(0)だけ出力して終了 # データ出力するボーンをリスト化 export_bones = [] export_bones_isolated = [] for bone_name in self.config_section_bone: export_bones.append(self.arm.bones[bone_name]) for bone_name in self.config_section_bone_isolated: export_bones_isolated.append(self.BonePair(self.arm.bones[bone_name], self.arm.bones[self.config_section_bone_isolated[bone_name]])) export_marker_frame = [] if self.option_marker_mode: export_marker_frame = [marker.frame for marker in self.timeline_markers if (self.frame_start <= marker.frame and marker.frame <= self.frame_end)] for i in range(self.frame_start, self.frame_end + 1): if self.option_marker_mode: if i not in export_marker_frame: self.print_all("skip bone frame : " + str(i)) continue self.scene.frame_set(i) self.print_all("export bone frame : " + str(self.scene.frame_current)) for bone in export_bones: self.export_bone_data(file, i, bone) for bone_pair in export_bones_isolated: self.export_bone_data_isolated(file, i, bone_pair) def export_bone_data(self, file, frame, bone): mat_edit_bone_local_inv = bone.bone.matrix_local.inverted() location_local = bone.matrix.to_translation() offset = bone.bone.matrix_local.to_translation() location_mmd = location_local - offset quaternion_mmd = (bone.matrix * mat_edit_bone_local_inv).to_quaternion() if bone.parent is not None: bone_parent = bone.parent mat_edit_bone_local_inv_parent = bone_parent.bone.matrix_local.inverted() location_local = (bone.parent.matrix.inverted() * bone.matrix).to_translation() * bone.parent.bone.matrix_local.inverted() offset = (bone.parent.bone.matrix_local.inverted() * bone.bone.matrix_local).to_translation() * bone.parent.bone.matrix_local.inverted() location_mmd = location_local - offset quaternion_parent = (bone_parent.matrix * mat_edit_bone_local_inv_parent).to_quaternion() quaternion_mmd = quaternion_parent.rotation_difference(quaternion_mmd) self.write_bone_data(file, bone.name, frame, location_mmd, quaternion_mmd) def export_bone_data_isolated(self, file, frame, bone_pair): bone_child = bone_pair.child quaternion_child = (bone_child.matrix * bone_child.bone.matrix_local.inverted()).to_quaternion() bone_parent = bone_pair.parent quaternion_parent = (bone_parent.matrix * bone_parent.bone.matrix_local.inverted()).to_quaternion() quaternion_mmd = quaternion_parent.rotation_difference(quaternion_child) location_mmd = None if self.joint_opt: location_mmd = mathutils.Vector((0.0, 0.0, 0.0)) else: location_local = (bone_parent.matrix.inverted() * bone_child.matrix).to_translation() * bone_parent.bone.matrix_local.inverted() offset = (bone_parent.bone.matrix_local.inverted() * bone_child.bone.matrix_local).to_translation() * bone_parent.bone.matrix_local.inverted() location_mmd = location_local - offset self.write_bone_data(file, bone_child.name, frame, location_mmd, quaternion_mmd) def write_bone_data(self, file, name, frame, vector, quaternion): self.print_log(name + " : " + str(vector) + " : " + str(quaternion)) self.write_bone_name(file, name) # ボーン名 self.write_long(file, frame + self.frame_offset) # フレーム番号 self.write_location(file, vector) self.write_quaternion(file, quaternion) self.write_ipo(file) # 補間 def write_location(self, file, vector): # self.print_all(vector) self.write_float(file, vector.x * self.scale) self.write_float(file, vector.z * self.scale) self.write_float(file, vector.y * self.scale) def write_quaternion(self, file, quaternion): # self.print_all(quaternion) self.write_float(file, -quaternion.x) self.write_float(file, -quaternion.z) self.write_float(file, -quaternion.y) self.write_float(file, quaternion.w) def write_ipo(self, file): for i in self.ipo_list: file.write(struct.pack("b", i)) def write_float(self, file, float): file.write(struct.pack("f", float)) def write_long(self, file, long): # unsigned long(DWORD) file.write(struct.pack("=L", long)) def write_int(self, file, int): file.write(struct.pack("i", int)) def write_bone_name(self, file, name): barray = bytearray(self.change_bone_name_to_mmd(name).encode('shift_jis')) self.write_bytearray(file, 15, barray) def write_str(self, file, array_size, str): barray = bytearray(str.encode('shift_jis')) self.write_bytearray(file, array_size, barray) def write_bytearray(self, file, array_size, barray): ba_base = bytearray(array_size) ba_base[:len(barray)] = barray file.write(ba_base) def change_bone_name_to_mmd(self, name): if name.endswith(".L"): return "左" + name[:-2] elif name.endswith(".R"): return "右" + name[:-2] else: return name def print_log(self, str): self.log.append(str) def print_all(self, str): print(str) self.print_log(str) def export_log(self): with open(self.path + ".log", "w", encoding="utf-8") as log: for line in self.log: print(line, sep=' : ', file=log) if __name__ == "__main__": print("----- start " + datetime.datetime.now().strftime("%H:%M:%S") + " -----") VmdExporter().execute() print("----- end " + datetime.datetime.now().strftime("%H:%M:%S") + " -----")