import bpy, os, re, math, bmesh, struct, shutil, mathutils from . import fileutil # アドオン情報 bl_info = { "name" : "CM3D2 Converter", "author" : "@saidenka_cm3d2", "version" : (2017, 5, 16, 21, 39, 32), "blender" : (2, 78, 0), "location" : "ファイル > インポート/エクスポート > CM3D2 Model (.model)", "description" : "カスタムメイド3D2の専用ファイルのインポート/エクスポートを行います", "warning" : "", "wiki_url" : "https://github.com/CM3Duser/Blender-CM3D2-Converter", "tracker_url" : "https://twitter.com/saidenka_cm3d2", "category" : "Import-Export" } addon_name = "CM3D2 Converter" preview_collections = {} # このアドオンの設定値群を呼び出す def preferences(): return bpy.context.user_preferences.addons[__name__.split('.')[0]].preferences # データ名末尾の「.001」などを削除 def remove_serial_number(name, enable=True): return re.sub(r'\.\d{3,}$', "", name) if enable else name # 文字列の左右端から空白を削除 def line_trim(line, enable=True): return line.strip(' \t\r\n') if enable else line # CM3D2専用ファイル用の文字列書き込み def write_str(file, raw_str): b_str = format(len(raw_str.encode('utf-8')), 'b') for i in range(9): if 7 < len(b_str): file.write( struct.pack('<B', int("1"+b_str[-7:], 2)) ) b_str = b_str[:-7] else: file.write( struct.pack('<B', int(b_str, 2)) ) break file.write(raw_str.encode('utf-8')) # CM3D2専用ファイル用の文字列読み込み def read_str(file, total_b = ""): for i in range(9): b_str = format(struct.unpack('<B', file.read(1))[0], '08b') total_b = b_str[1:] + total_b if b_str[0] == '0': break return file.read(int(total_b, 2)).decode('utf-8') # ボーン/ウェイト名を Blender → CM3D2 def encode_bone_name(name, enable=True): return re.sub(r'([_ ])\*([_ ].*)\.([rRlL])$', r'\1\3\2', name) if name.count('*') == 1 and enable else name # ボーン/ウェイト名を CM3D2 → Blender def decode_bone_name(name, enable=True): return re.sub(r'([_ ])([rRlL])([_ ].*)$', r'\1*\3.\2', name) if enable else name # CM3D2用マテリアルを設定に合わせて装飾 def decorate_material(mate, enable=True, me=None, mate_index=-1): if not enable: return if 'shader1' not in mate: return shader = mate['shader1'] if 'CM3D2/Man' == shader: mate.use_shadeless = True mate.diffuse_color = (0, 1, 1) elif 'CM3D2/Mosaic' == shader: mate.use_transparency = True mate.transparency_method = 'RAYTRACE' mate.alpha = 0.25 mate.raytrace_transparency.ior = 2 elif 'CM3D2_Debug/Debug_CM3D2_Normal2Color' == shader: mate.use_tangent_shading = True mate.diffuse_color = (0.5, 0.5, 1) else: if '/Toony_' in shader: mate.diffuse_shader = 'TOON' mate.diffuse_toon_smooth = 0.01 mate.diffuse_toon_size = 1.2 if 'Trans' in shader: mate.use_transparency = True mate.alpha = 0.0 mate.texture_slots[0].use_map_alpha = True if 'Unlit/' in shader: mate.emit = 0.5 if '_NoZ' in shader: mate.offset_z = 9999 is_colored = False is_textured = [False, False, False, False] rimcolor, rimpower, rimshift = mathutils.Color((1, 1, 1)), 0.0, 0.0 for slot in mate.texture_slots: if not slot: continue if not slot.texture: continue tex = slot.texture tex_name = remove_serial_number(tex.name) slot.use_map_color_diffuse = False if tex_name == '_MainTex': slot.use_map_color_diffuse = True if 'image' in dir(tex): img = tex.image if len(img.pixels): if me: color = mathutils.Color(get_image_average_color_uv(img, me, mate_index)[:3]) else: color = mathutils.Color(get_image_average_color(img)[:3]) mate.diffuse_color = color is_colored = True elif tex_name == '_RimColor': rimcolor = slot.color[:] if not is_colored: mate.diffuse_color = slot.color[:] mate.diffuse_color.v += 0.5 elif tex_name == '_Shininess': mate.specular_intensity = slot.diffuse_color_factor elif tex_name == '_RimPower': rimpower = slot.diffuse_color_factor elif tex_name == '_RimShift': rimshift = slot.diffuse_color_factor for index, name in enumerate(['_MainTex', '_ToonRamp', '_ShadowTex', '_ShadowRateToon']): if tex_name == name: if 'image' in dir(tex): if tex.image: if len(tex.image.pixels): is_textured[index] = tex set_texture_color(slot) # よりオリジナルに近く描画するノード作成 if all(is_textured): mate.use_nodes = True mate.use_shadeless = True node_tree = mate.node_tree for node in node_tree.nodes[:]: node_tree.nodes.remove(node) mate_node = node_tree.nodes.new('ShaderNodeExtendedMaterial') mate_node.location = (0, 0) mate_node.material = mate if "CM3D2 Shade" in bpy.context.blend_data.materials: shade_mate = bpy.context.blend_data.materials["CM3D2 Shade"] else: shade_mate = bpy.context.blend_data.materials.new("CM3D2 Shade") shade_mate.diffuse_color = (1, 1, 1) shade_mate.diffuse_intensity = 1 shade_mate.specular_intensity = 1 shade_mate_node = node_tree.nodes.new('ShaderNodeExtendedMaterial') shade_mate_node.location = (234.7785, -131.8243) shade_mate_node.material = shade_mate toon_node = node_tree.nodes.new('ShaderNodeValToRGB') toon_node.location = (571.3662, -381.0965) toon_img = is_textured[1].image toon_w, toon_h = toon_img.size[0], toon_img.size[1] for i in range(32 - 2): toon_node.color_ramp.elements.new(0.0) for i in range(32): pos = i / (32 - 1) toon_node.color_ramp.elements[i].position = pos x = int( (toon_w / (32 - 1)) * i ) pixel_index = x * toon_img.channels toon_node.color_ramp.elements[i].color = toon_img.pixels[pixel_index:pixel_index+4] toon_node.color_ramp.interpolation = 'EASE' shadow_rate_node = node_tree.nodes.new('ShaderNodeValToRGB') shadow_rate_node.location = (488.2785, 7.8446) shadow_rate_img = is_textured[3].image shadow_rate_w, shadow_rate_h = shadow_rate_img.size[0], shadow_rate_img.size[1] for i in range(32 - 2): shadow_rate_node.color_ramp.elements.new(0.0) for i in range(32): pos = i / (32 - 1) shadow_rate_node.color_ramp.elements[i].position = pos x = int( (shadow_rate_w / (32)) * i ) pixel_index = x * shadow_rate_img.channels shadow_rate_node.color_ramp.elements[i].color = shadow_rate_img.pixels[pixel_index:pixel_index+4] shadow_rate_node.color_ramp.interpolation = 'EASE' geometry_node = node_tree.nodes.new('ShaderNodeGeometry') geometry_node.location = (323.4597, -810.8045) shadow_texture_node = node_tree.nodes.new('ShaderNodeTexture') shadow_texture_node.location = (626.0117, -666.0227) shadow_texture_node.texture = is_textured[2] invert_node = node_tree.nodes.new('ShaderNodeInvert') invert_node.location = (805.6814, -132.9144) shadow_mix_node = node_tree.nodes.new('ShaderNodeMixRGB') shadow_mix_node.location = (1031.2714, -201.5598) toon_mix_node = node_tree.nodes.new('ShaderNodeMixRGB') toon_mix_node.location = (1257.5538, -308.8037) toon_mix_node.blend_type = 'MULTIPLY' toon_mix_node.inputs[0].default_value = 1.0 specular_mix_node = node_tree.nodes.new('ShaderNodeMixRGB') specular_mix_node.location = (1473.2079, -382.7421) specular_mix_node.blend_type = 'SCREEN' specular_mix_node.inputs[0].default_value = mate.specular_intensity normal_node = node_tree.nodes.new('ShaderNodeNormal') normal_node.location = (912.1372, -590.8748) rim_ramp_node = node_tree.nodes.new('ShaderNodeValToRGB') rim_ramp_node.location = (1119.0664, -570.0284) rim_ramp_node.color_ramp.elements[0].color = list(rimcolor[:]) + [1.0] rim_ramp_node.color_ramp.elements[0].position = rimshift rim_ramp_node.color_ramp.elements[1].color = (0, 0, 0, 1) rim_ramp_node.color_ramp.elements[1].position = (rimshift) + ((1.0 - (rimpower * 0.03333)) * 0.5) rim_power_node = node_tree.nodes.new('ShaderNodeHueSaturation') rim_power_node.location = (1426.6332, -575.6142) #rim_power_node.inputs[2].default_value = rimpower * 0.1 rim_mix_node = node_tree.nodes.new('ShaderNodeMixRGB') rim_mix_node.location = (1724.7024, -451.9624) rim_mix_node.blend_type = 'ADD' out_node = node_tree.nodes.new('ShaderNodeOutput') out_node.location = (1957.4023, -480.5365) node_tree.links.new(shadow_mix_node.inputs[1], mate_node.outputs[0]) node_tree.links.new(shadow_rate_node.inputs[0], shade_mate_node.outputs[3]) node_tree.links.new(invert_node.inputs[1], shadow_rate_node.outputs[0]) node_tree.links.new(shadow_mix_node.inputs[0], invert_node.outputs[0]) node_tree.links.new(toon_node.inputs[0], shade_mate_node.outputs[3]) node_tree.links.new(shadow_texture_node.inputs[0], geometry_node.outputs[4]) node_tree.links.new(shadow_mix_node.inputs[2], shadow_texture_node.outputs[1]) node_tree.links.new(toon_node.inputs[0], shade_mate_node.outputs[3]) node_tree.links.new(toon_mix_node.inputs[1], shadow_mix_node.outputs[0]) node_tree.links.new(toon_mix_node.inputs[2], toon_node.outputs[0]) node_tree.links.new(specular_mix_node.inputs[1], toon_mix_node.outputs[0]) node_tree.links.new(specular_mix_node.inputs[2], shade_mate_node.outputs[4]) node_tree.links.new(normal_node.inputs[0], mate_node.outputs[2]) node_tree.links.new(rim_ramp_node.inputs[0], normal_node.outputs[1]) node_tree.links.new(rim_power_node.inputs[4], rim_ramp_node.outputs[0]) node_tree.links.new(rim_mix_node.inputs[2], rim_power_node.outputs[0]) node_tree.links.new(rim_mix_node.inputs[0], shadow_rate_node.outputs[0]) node_tree.links.new(rim_mix_node.inputs[1], specular_mix_node.outputs[0]) node_tree.links.new(out_node.inputs[0], rim_mix_node.outputs[0]) node_tree.links.new(out_node.inputs[1], mate_node.outputs[1]) for node in node_tree.nodes[:]: node.select = False node_tree.nodes.active = mate_node node_tree.nodes.active.select = True else: mate.use_nodes = False mate.use_shadeless = False # 画像のおおよその平均色を取得 def get_image_average_color(img, sample_count=10): if not len(img.pixels): return mathutils.Color([0, 0, 0]) pixel_count = img.size[0] * img.size[1] channels = img.channels max_s = 0.0 max_s_color, average_color = mathutils.Color([0, 0, 0]), mathutils.Color([0, 0, 0]) seek_interval = pixel_count / sample_count for sample_index in range(sample_count): index = int(seek_interval * sample_index) * channels color = mathutils.Color(img.pixels[index:index+3]) average_color += color if max_s < color.s: max_s_color, max_s = color, color.s average_color /= sample_count output_color = (average_color + max_s_color) / 2 output_color.s *= 1.5 return max_s_color # 画像のおおよその平均色を取得 (UV版) def get_image_average_color_uv(img, me=None, mate_index=-1, sample_count=10): if not len(img.pixels): return mathutils.Color([0, 0, 0]) img_width, img_height, img_channel = img.size[0], img.size[1], img.channels bm = bmesh.new() bm.from_mesh(me) uv_lay = bm.loops.layers.uv.active uvs = [l[uv_lay].uv[:] for f in bm.faces if f.material_index == mate_index for l in f.loops] bm.free() if len(uvs) <= sample_count: return get_image_average_color(img) average_color = mathutils.Color([0, 0, 0]) max_s = 0.0 max_s_color = mathutils.Color([0, 0, 0]) seek_interval = len(uvs) / sample_count for sample_index in range(sample_count): uv_index = int(seek_interval * sample_index) x, y = uvs[uv_index] x = math.modf(x)[0] if x < 0.0: x += 1.0 y = math.modf(y)[0] if y < 0.0: y += 1.0 x, y = int(x * img_width), int(y * img_height) pixel_index = ((y * img_width) + x) * img_channel color = mathutils.Color(img.pixels[pixel_index:pixel_index+3]) average_color += color if max_s < color.s: max_s_color, max_s = color, color.s average_color /= sample_count output_color = (average_color + max_s_color) / 2 output_color.s *= 1.5 return output_color # CM3D2のインストールフォルダを取得+α def default_cm3d2_dir(base_dir, file_name, new_ext): if not base_dir: if preferences().cm3d2_path: base_dir = os.path.join(preferences().cm3d2_path, "GameData", "*." + new_ext) else: try: import winreg with winreg.OpenKey(winreg.HKEY_CURRENT_USER, r'Software\KISS\カスタムメイド3D2') as key: base_dir = winreg.QueryValueEx(key, 'InstallPath')[0] preferences().cm3d2_path = base_dir base_dir = os.path.join(base_dir, "GameData", "*." + new_ext) except: pass if file_name: base_dir = os.path.join(os.path.split(base_dir)[0], file_name) base_dir = os.path.splitext(base_dir)[0] + "." + new_ext return base_dir # 一時ファイル書き込みと自動バックアップを行うファイルオブジェクトを返す def open_temporary(filepath, mode, is_backup=False): backup_ext = preferences().backup_ext if is_backup and backup_ext: backup_filepath = filepath + '.' + backup_ext else: backup_filepath = None return fileutil.TemporaryFileWriter(filepath, mode, backup_filepath=backup_filepath) # ファイルを上書きするならバックアップ処理 def file_backup(filepath, enable=True): backup_ext = preferences().backup_ext if enable and backup_ext and os.path.exists(filepath): shutil.copyfile(filepath, filepath+"."+backup_ext) # サブフォルダを再帰的に検索してリスト化 def fild_tex_all_files(dir): for root, dirs, files in os.walk(dir): for file in files: if os.path.splitext(file)[1].lower() == ".tex": yield os.path.join(root, file) elif os.path.splitext(file)[1].lower() == ".png": yield os.path.join(root, file) # テクスチャ置き場のパスのリストを返す def get_default_tex_paths(): default_paths = [preferences().default_tex_path0, preferences().default_tex_path1, preferences().default_tex_path2, preferences().default_tex_path3] if not any(default_paths): cm3d2_dir = preferences().cm3d2_path if not cm3d2_dir: try: import winreg with winreg.OpenKey(winreg.HKEY_CURRENT_USER, r'Software\KISS\カスタムメイド3D2') as key: cm3d2_dir = winreg.QueryValueEx(key, 'InstallPath')[0] except: return [] target_dir = [os.path.join(cm3d2_dir, "GameData", "texture")] target_dir.append(os.path.join(cm3d2_dir, "GameData", "texture2")) target_dir.append(os.path.join(cm3d2_dir, "Sybaris", "GameData")) target_dir.append(os.path.join(cm3d2_dir, "Mod")) tex_dirs = [path for path in target_dir if os.path.isdir(path)] for index, path in enumerate(tex_dirs): preferences().__setattr__('default_tex_path' + str(index), path) else: tex_dirs = [preferences().__getattribute__('default_tex_path' + str(i)) for i in range(4) if preferences().__getattribute__('default_tex_path' + str(i))] return tex_dirs # テクスチャ置き場の全ファイルを返す def get_tex_storage_files(): files = [] tex_dirs = get_default_tex_paths() for tex_dir in tex_dirs: tex_dir = bpy.path.abspath(tex_dir) files.extend(fild_tex_all_files(tex_dir)) return files # テクスチャを検索して空の画像へ置換 def replace_cm3d2_tex(img, pre_files=[]): source_png_name = remove_serial_number(img.name).lower() + ".png" source_tex_name = remove_serial_number(img.name).lower() + ".tex" tex_dirs = get_default_tex_paths() for tex_dir in tex_dirs: if len(pre_files): files = pre_files else: files = fild_tex_all_files(tex_dir) for path in files: path = bpy.path.abspath(path) file_name = os.path.basename(path).lower() if file_name == source_png_name: img.filepath = path img.reload() return True elif file_name == source_tex_name: try: tex_data = load_cm3d2tex(path) if tex_data is None: return False with open(png_path, 'wb') as png_file: png_file.write(tex_data[-1]) img.filepath = png_path img.reload() return True except: return False if len(pre_files): return False return False # texファイルの読み込み def load_cm3d2tex(path, skip_data=False): with open(path, 'rb') as file: header_ext = read_str(file) if header_ext != 'CM3D2_TEX': return None version = struct.unpack('<i', file.read(4))[0] read_str(file) # default value tex_format = 5 uv_rects = None data = None if version >= 1010: if version >= 1011: num_rect = struct.unpack('<i', file.read(4))[0] uv_rects = [] for i in range(num_rect): # x, y, w, h uv_rects.append( struct.unpack('<4f', file.read(4 * 4)) ) width = struct.unpack('<i', file.read(4))[0] height = struct.unpack('<i', file.read(4))[0] tex_format = struct.unpack('<i', file.read(4))[0] # if tex_format == 10 or tex_format == 12: return None if not skip_data: png_size = struct.unpack('<i', file.read(4))[0] data = file.read(png_size) return version, tex_format, uv_rects, data # col f タイプの設定値を値に合わせて着色 def set_texture_color(slot): if not slot or not slot.texture or slot.use: return type = 'col' if slot.use_rgb_to_intensity else 'f' tex = slot.texture base_name = remove_serial_number(tex.name) tex.type = 'BLEND' if 'progression' in dir(tex): tex.progression = 'DIAGONAL' tex.use_color_ramp = True tex.use_preview_alpha = True elements = tex.color_ramp.elements element_count = 4 if element_count < len(elements): for i in range(len(elements) - element_count): elements.remove(elements[-1]) elif len(elements) < element_count: for i in range(element_count - len(elements)): elements.new(1.0) elements[0].position, elements[1].position, elements[2].position, elements[3].position = 0.2, 0.21, 0.25, 0.26 if type == 'col': elements[0].color = [0.2, 1, 0.2, 1] elements[-1].color = slot.color[:] + (slot.diffuse_color_factor, ) if 0.3 < mathutils.Color(slot.color[:3]).v: elements[1].color, elements[2].color = [0, 0, 0, 1], [0, 0, 0, 1] else: elements[1].color, elements[2].color = [1, 1, 1, 1], [1, 1, 1, 1] elif type == 'f': elements[0].color = [0.2, 0.2, 1, 1] multi = 1.0 if base_name == '_OutlineWidth': multi = 200 elif base_name == '_RimPower': multi = 1.0 / 30.0 value = slot.diffuse_color_factor * multi elements[-1].color = [value, value, value, 1] if 0.3 < value: elements[1].color, elements[2].color = [0, 0, 0, 1], [0, 0, 0, 1] else: elements[1].color, elements[2].color = [1, 1, 1, 1], [1, 1, 1, 1] # 必要なエリアタイプを設定を変更してでも取得 def get_request_area(context, request_type, except_types=['VIEW_3D', 'PROPERTIES', 'INFO', 'USER_PREFERENCES']): request_areas = [(a, a.width * a.height) for a in context.screen.areas if a.type == request_type] candidate_areas = [(a, a.width * a.height) for a in context.screen.areas if a.type not in except_types] return_areas = request_areas[:] if len(request_areas) else candidate_areas if not len(return_areas): return None return_areas.sort(key=lambda i: i[1]) return_area = return_areas[-1][0] return_area.type = request_type return return_area # 複数のデータを完全に削除 def remove_data(target_data): try: target_data = target_data[:] except: target_data = [target_data] for data in target_data: if data.__class__.__name__ == 'Object': if data.name in bpy.context.scene.objects: bpy.context.scene.objects.unlink(data) for data in target_data: if 'users' in dir(data) and 'user_clear' in dir(data): if data.users: data.user_clear() for data in target_data: for data_str in dir(bpy.data): if data_str[-1] != "s": continue try: if data.__class__.__name__ == eval('bpy.data.%s[0].__class__.__name__' % data_str): exec('bpy.data.%s.remove(data, do_unlink=True)' % data_str) break except: pass # オブジェクトのマテリアルを削除/復元するクラス class material_restore: def __init__(self, ob): override = bpy.context.copy() override['object'] = ob self.object = ob self.slots = [] for slot in ob.material_slots: if slot: self.slots.append(slot.material) else: self.slots.append(None) self.mesh_data = [] for index, slot in enumerate(ob.material_slots): self.mesh_data.append([]) for face in ob.data.polygons: if face.material_index == index: self.mesh_data[-1].append(face.index) for slot in ob.material_slots[:]: bpy.ops.object.material_slot_remove(override) def restore(self): override = bpy.context.copy() override['object'] = self.object for slot in self.object.material_slots[:]: bpy.ops.object.material_slot_remove(override) for index, mate in enumerate(self.slots): bpy.ops.object.material_slot_add(override) slot = self.object.material_slots[index] if slot: slot.material = mate for face_index in self.mesh_data[index]: self.object.data.polygons[face_index].material_index = index # 現在のレイヤー内のオブジェクトをレンダリングしなくする/戻す class hide_render_restore: def __init__(self, render_objects=[]): try: render_objects = render_objects[:] except: render_objects = [render_objects] if not len(render_objects): render_objects = bpy.context.selected_objects[:] self.render_objects = render_objects[:] self.render_object_names = [ob.name for ob in render_objects] self.rendered_objects = [] for ob in render_objects: if ob.hide_render: self.rendered_objects.append(ob) ob.hide_render = False self.hide_rendered_objects = [] for ob in bpy.data.objects: for layer_index, is_used in enumerate(bpy.context.scene.layers): if not is_used: continue if ob.layers[layer_index] and is_used and ob.name not in self.render_object_names and not ob.hide_render: self.hide_rendered_objects.append(ob) ob.hide_render = True break def restore(self): for ob in self.rendered_objects: ob.hide_render = True for ob in self.hide_rendered_objects: ob.hide_render = False # 指定エリアに変数をセット def set_area_space_attr(area, attr_name, value): if not area: return for space in area.spaces: if space.type == area.type: space.__setattr__(attr_name, value) break # スムーズなグラフを返す1 def in_out_quad_blend(f): if f <= 0.5: return 2.0 * math.sqrt(f) f -= 0.5 return 2.0 * f * (1.0 - f) + 0.5 # スムーズなグラフを返す2 def bezier_blend(f): return math.sqrt(f) * (3.0 - 2.0 * f) # 三角関数でスムーズなグラフを返す def trigonometric_smooth(x): return math.sin((x-0.5)*math.pi)*0.5+0.5 # エクスポート例外クラス class CM3D2ExportException(Exception): pass