# Copyright (C) 2021 Victor Soupday # This file is part of CC/iC Blender Tools # # CC/iC Blender Tools is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # CC/iC Blender Tools is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with CC/iC Blender Tools. If not, see . import os import shutil import bpy from enum import IntEnum, IntFlag from . import (characters, hik, rigging, rigutils, bones, bake, imageutils, jsonutils, materials, modifiers, wrinkle, drivers, meshutils, nodeutils, physics, rigidbody, colorspace, scene, channel_mixer, shaders, basic, properties, utils, vars) debug_counter = 0 def delete_import(chr_cache): props = vars.props() chr_cache.invalidate() chr_cache.delete() chr_cache.clean_up() utils.remove_from_collection(props.import_cache, chr_cache) utils.clean_up_unused() def process_material(chr_cache, obj_cache, obj, mat, obj_json, processed_images): props = vars.props() prefs = vars.prefs() mat_cache = chr_cache.get_material_cache(mat) mat_json = jsonutils.get_material_json(obj_json, mat) if not mat_cache: return # don't process user added materials if mat_cache.user_added: return if not mat.use_nodes: mat.use_nodes = True # store the material type and id mat_cache.check_id() if chr_cache.setup_mode == "ADVANCED": if mat_cache.is_cornea() or mat_cache.is_eye(): shaders.connect_eye_shader(obj_cache, obj, mat, obj_json, mat_json, processed_images) elif mat_cache.is_tearline(): shaders.connect_tearline_shader(obj_cache, obj, mat, mat_json, processed_images) elif mat_cache.is_eye_occlusion(): shaders.connect_eye_occlusion_shader(obj_cache, obj, mat, mat_json, processed_images) elif mat_cache.is_skin() or mat_cache.is_nails(): shaders.connect_skin_shader(chr_cache, obj_cache, obj, mat, mat_json, processed_images) elif mat_cache.is_teeth(): shaders.connect_teeth_shader(obj_cache, obj, mat, mat_json, processed_images) elif mat_cache.is_tongue(): shaders.connect_tongue_shader(obj_cache, obj, mat, mat_json, processed_images) elif mat_cache.is_hair(): shaders.connect_hair_shader(obj_cache, obj, mat, mat_json, processed_images) elif mat_cache.is_sss(): shaders.connect_sss_shader(obj_cache, obj, mat, mat_json, processed_images) else: shaders.connect_pbr_shader(obj_cache, obj, mat, mat_json, processed_images) # optional pack channels if prefs.build_limit_textures or prefs.build_pack_texture_channels: bake.pack_shader_channels(chr_cache, mat_cache) elif props.wrinkle_mode and mat_json and "Wrinkle" in mat_json.keys(): bake.pack_shader_channels(chr_cache, mat_cache) else: nodeutils.clear_cursor() nodeutils.reset_cursor() if mat_cache.is_eye_occlusion(): basic.connect_eye_occlusion_material(obj, mat, mat_json, processed_images) elif mat_cache.is_tearline(): basic.connect_tearline_material(obj, mat, mat_json, processed_images) elif mat_cache.is_cornea(): basic.connect_basic_eye_material(obj, mat, mat_json, processed_images) else: basic.connect_basic_material(obj, mat, mat_json, processed_images) nodeutils.move_new_nodes(-600, 0) # apply cached alpha settings if mat_cache is not None: if mat_cache.alpha_mode != "NONE": materials.apply_alpha_override(obj, mat, mat_cache.alpha_mode) if mat_cache.culling_sides > 0: materials.apply_backface_culling(obj, mat, mat_cache.culling_sides) # apply any channel mixers if mat_cache is not None: if mat_cache.mixer_settings: mixer_settings = mat_cache.mixer_settings if mixer_settings.rgb_image or mixer_settings.id_image: channel_mixer.rebuild_mixers(chr_cache, mat, mixer_settings) def process_object(chr_cache, obj, obj_cache, objects_processed, chr_json, processed_materials, processed_images): props = vars.props() prefs = vars.prefs() if obj is None or obj in objects_processed: return objects_processed.append(obj) obj_json = jsonutils.get_object_json(chr_json, obj_cache.source_name) physics_json = None utils.log_info("") utils.log_info("Processing Object: " + obj.name + ", Type: " + obj.type) utils.log_indent() if obj.type == "MESH": mesh : bpy.types.Mesh = obj.data # Turn off auto smoothing if not utils.B401(): mesh.use_auto_smooth = False # Auto apply armature modifier settings if prefs.build_armature_edit_modifier or prefs.build_armature_preserve_volume: mod_arm = modifiers.get_object_modifier(obj, "ARMATURE") if mod_arm: if prefs.build_armature_edit_modifier: mod_arm.show_in_editmode = True mod_arm.show_on_cage = True if prefs.build_armature_preserve_volume: mod_arm.use_deform_preserve_volume = True # Set to smooth shading (disabled as may not be needed anymore) #meshutils.set_shading(obj, True) # remove any modifiers for refractive eyes modifiers.remove_eye_modifiers(obj) # store the object type and id # store the material type and id if obj_cache: obj_cache.check_id() # process any materials found in the mesh object for slot in obj.material_slots: mat = slot.material if mat and mat not in objects_processed: utils.log_info("") utils.log_info("Processing Material: " + mat.name) utils.log_indent() process_material(chr_cache, obj_cache, obj, mat, obj_json, processed_images) if processed_materials is not None: first = materials.find_duplicate_material(chr_cache, mat, processed_materials) if first: utils.log_info(f"Found duplicate material, re-using {first.name} instead.") slot.material = first else: processed_materials.append(mat) utils.log_recess() objects_processed.append(mat) # setup special modifiers for displacement, UV warp, etc... if obj_cache and chr_cache.setup_mode == "ADVANCED": if obj_cache.is_eye(): modifiers.add_eye_modifiers(obj) elif obj_cache.is_eye_occlusion(): modifiers.add_eye_occlusion_modifiers(obj) elif obj_cache.is_tearline(): modifiers.add_tearline_modifiers(obj) elif obj.type == "ARMATURE": # set the frame range of the scene to the active action on the armature if props.physics_mode: scene.fetch_anim_range(bpy.context, expand=True) obj["rl_import_file"] = chr_cache.import_file obj["rl_generation"] = chr_cache.generation utils.log_recess() def cache_object_materials(chr_cache, obj, chr_json, processed): props = vars.props() if obj is None or obj in processed: return obj_json = jsonutils.get_object_json(chr_json, obj) obj_cache = chr_cache.add_object_cache(obj) if obj.type == "MESH": utils.log_info(f"Caching Object: {obj.name}") utils.log_indent() for mat in obj.data.materials: if mat and mat.node_tree is not None: object_type, material_type = materials.detect_materials(chr_cache, obj, mat, obj_json) if obj_cache.object_type != "BODY": obj_cache.set_object_type(object_type) if mat not in processed: mat_cache = chr_cache.add_material_cache(mat, material_type) mat_cache.dir = imageutils.get_material_tex_dir(chr_cache, obj, mat) utils.log_indent() materials.detect_embedded_textures(chr_cache, obj, obj_cache, mat, mat_cache) materials.detect_mixer_masks(chr_cache, obj, obj_cache, mat, mat_cache) physics.detect_physics(chr_cache, obj, obj_cache, mat, mat_cache, chr_json) utils.log_recess() processed.append(mat) utils.log_recess() processed.append(obj) def apply_edit_shapekeys(obj): """For objects with shapekeys, set the active visible and edit mode shapekey to the basis. """ # shapekeys data path: # utils.get_active_object().data.shape_keys.key_blocks['Basis'] if obj.type == "MESH": shape_keys = obj.data.shape_keys if shape_keys is not None: blocks = shape_keys.key_blocks if blocks is not None: # if the object has shape keys if len(blocks) > 0: try: # set the active shapekey to the basis and apply shape keys in edit mode. obj.active_shape_key_index = 0 obj.show_only_shape_key = False obj.use_shape_key_edit_mode = True except Exception as e: utils.log_error("Unable to set shape key edit mode!", e) def init_shape_key_range(obj): #utils.get_active_object().data.shape_keys.key_blocks['Basis'] if obj.type == "MESH": shape_keys: bpy.types.Key = obj.data.shape_keys if shape_keys is not None: blocks = shape_keys.key_blocks if blocks is not None: if len(blocks) > 0: for block in blocks: # expand the range of the shape key slider to include negative values... if "Eye" in block.name and "_Look_" in block.name: block.slider_min = -1.0 block.slider_max = 1.0 else: block.slider_min = -1.5 block.slider_max = 1.5 # re-set a value in the shapekey action keyframes to force # the shapekey action to update to the new ranges: try: action = utils.safe_get_action(shape_keys) if action: co = action.fcurves[0].keyframe_points[0].co action.fcurves[0].keyframe_points[0].co = co except: pass def detect_generation(chr_cache, json_data, character_id): generation = "Unknown" if json_data: avatar_type = jsonutils.get_json(json_data, f"{character_id}/Avatar_Type") json_generation = jsonutils.get_character_generation_json(json_data, chr_cache.get_character_id()) if json_generation and json_generation in vars.CHARACTER_GENERATION: generation = vars.CHARACTER_GENERATION[json_generation] elif avatar_type == "NonHuman": generation = "Creature" elif avatar_type == "NonStandard": generation = "Humanoid" elif json_generation is not None and json_generation == "": generation = "Humanoid" elif json_generation is None: generation = "Prop" arm = chr_cache.get_armature() material_names = characters.get_character_material_names(arm) object_names = characters.get_character_object_names(arm) # some ActorScan characters are GameBase in disguise... if characters.character_has_bones(arm, ["root", "pelvis", "spine_03", "CC_Base_FacialBone"]): generation = "GameBase" if generation in ["Unknown", "Humanoid", "Creature"]: utils.log_info(f"Determining generation from armature...") if len(material_names) == 1 and rigutils.is_ActorCore_armature(arm): generation = "ActorScan" utils.log_info(" - ActorScan found!") elif characters.character_has_materials(arm, ["Ga_Skin_Body"]) and rigutils.is_ActorCore_armature(arm): generation = "ActorBuild" utils.log_info(" - ActorBuild found!") elif characters.character_has_materials(arm, ["Ga_Skin_Body"]) and rigutils.is_GameBase_armature(arm): generation = "GameBase" utils.log_info(" - GameBase found!") elif rigutils.is_rl_armature(arm): generation = "AccuRig" utils.log_info(" - AccuRig found!") else: utils.log_info(" - Not found...") if generation == "Unknown" and arm: if utils.find_pose_bone_in_armature(arm, "RootNode_0_", "RL_BoneRoot"): generation = "ActorCore" elif utils.find_pose_bone_in_armature(arm, "CC_Base_L_Pinky3", "L_Pinky3"): generation = "G3" elif utils.find_pose_bone_in_armature(arm, "pinky_03_l"): generation = "GameBase" elif utils.find_pose_bone_in_armature(arm, "CC_Base_L_Finger42", "L_Finger42"): generation = "G1" utils.log_info(f"Generation could be: {generation} detected from pose bones.") if generation == "Unknown": for obj_cache in chr_cache.object_cache: obj = obj_cache.get_object() if obj_cache.is_mesh(): name = obj.name.lower() if "cc_game_body" in name or "cc_game_tongue" in name: generation = "GameBase" elif "cc_base_body" in name: if utils.object_has_material(obj, "ga_skin_body"): generation = "GameBase" elif utils.object_has_material(obj, "std_skin_body"): generation = "G3" elif utils.object_has_material(obj, "skin_body"): generation = "G1" if generation != "Unknown": utils.log_info(f"Generation could be: {generation} detected from materials.") if generation == "Unknown" or generation == "G3": for obj_cache in chr_cache.object_cache: obj = obj_cache.get_object() if obj_cache.is_mesh() and obj.name == "CC_Base_Body": # try vertex count if len(obj.data.vertices) == 14164: utils.log_info("Generation: G3Plus detected by vertex count.") generation = "G3Plus" elif len(obj.data.vertices) == 13286: utils.log_info("Generation: G3 detected by vertex count.") generation = "G3" #try UV map test elif materials.test_for_material_uv_coords(obj, 0, [[0.5, 0.763], [0.7973, 0.6147], [0.1771, 0.0843], [0.912, 0.0691]]): utils.log_info("Generation: G3Plus detected by UV test.") generation = "G3Plus" elif materials.test_for_material_uv_coords(obj, 0, [[0.5, 0.034365], [0.957562, 0.393431], [0.5, 0.931725], [0.275117, 0.961283]]): utils.log_info("Generation: G3 detected by UV test.") generation = "G3" utils.log_info(f"Detected Character Generation: {generation}") return generation def is_iclone_temp_motion(name : str): u_idx = name.find('_', 0) if u_idx == -1: return False if not name[:u_idx].isdigit(): return False search = "TempMotion" if utils.partial_match(name, "TempMotion", u_idx + 1): return True else: return False def purge_imported_material(mat, imported_images: list): if utils.material_exists(mat): if mat.node_tree and mat.node_tree.nodes: for node in mat.node_tree.nodes: if node.type == "TEX_IMAGE": if node.image: if node.image in imported_images: imported_images.remove(node.image) bpy.data.images.remove(node.image) bpy.data.materials.remove(mat) def purge_imported_object(obj, imported_images): if utils.object_exists(obj): if obj.type == "MESH": for mat in obj.data.materials: purge_imported_material(mat, imported_images) utils.delete_object_tree(obj) def remap_action_names(arm, objects, actions, source_id, motion_prefix=""): key_map = {} num_keys = 0 rig_id = rigutils.get_rig_id(arm) utils.log_info(f"Remap Action Names:") utils.log_info(f"Armature: {source_id} => {rig_id}") # don't change the armature id if it exists rl_arm_id = utils.get_rl_object_id(arm) if not rl_arm_id: rl_arm_id = utils.generate_random_id(20) utils.set_rl_object_id(arm, rl_arm_id) # find all motions for this armature armature_actions = [] shapekey_actions = [] motion_ids = set() motion_sets = {} for action in actions: split = action.name.split("|") action_arm_id = split[0] motion_id = split[-1] if action_arm_id == source_id: utils.log_info(f"Motion ID: {motion_id}") motion_ids.add(motion_id) armature_actions.append(action) motion_sets[motion_id] = rigutils.generate_motion_set(arm, motion_id, motion_prefix) # determine how each shape key id relates to each object in the import for obj in objects: if obj.type == "MESH": obj_id = rigutils.get_action_obj_id(obj) if obj.data.shape_keys: obj_action = utils.safe_get_action(obj.data.shape_keys) if obj_action: actions.append(obj_action) key_map[obj_id] = obj.data.shape_keys.name utils.log_info(f"ShapeKey: {obj.data.shape_keys.name} belongs to: {obj_id}") num_keys += 1 # rename all actions associated with this armature and it's motions for action in actions: split = action.name.split("|") action_key_name = split[0] motion_id = split[-1] if motion_id in motion_ids: set_id, set_generation = motion_sets[motion_id] if action in armature_actions: action_name = rigutils.make_armature_action_name(rig_id, motion_id, motion_prefix) utils.log_info(f"Renaming action: {action.name} to {action_name}") action.name = action_name rigutils.add_motion_set_data(action, set_id, set_generation, rl_arm_id=rl_arm_id) armature_actions.append(action) else: for obj_id, key_name in key_map.items(): if action_key_name == key_name: action_name = rigutils.make_key_action_name(rig_id, motion_id, obj_id, motion_prefix) utils.log_info(f"Renaming action: {action.name} to {action_name}") action.name = action_name rigutils.add_motion_set_data(action, set_id, set_generation, obj_id=obj_id) shapekey_actions.append(action) return armature_actions, shapekey_actions def process_root_bones(arm, json_data, name): root_bones = jsonutils.get_json(json_data, f"{name}/Root Bones") if root_bones: for root_def in root_bones: name = root_def["Name"] type = root_def["Type"] sub_link_id = root_def["Link_ID"] if name in arm.pose.bones: pose_bone = arm.pose.bones[name] pose_bone["root_id"] = sub_link_id pose_bone["root_type"] = type def process_rl_import(file_path, import_flags, armatures, rl_armatures, objects: list, actions, json_data, report, link_id, only_objects=None, motion_prefix=""): props = vars.props() prefs = vars.prefs() utils.log_info("") utils.log_info("Processing Reallusion Import:") utils.log_info("-----------------------------") dir, file = os.path.split(file_path) name, ext = os.path.splitext(file) imported_characters = [] if armatures and (len(armatures) > 1 or len(rl_armatures) > 1): report.append("Multiple armatures detected in Fbx is not fully supported!") utils.log_warn("Multiple armatures detected in Fbx is not fully supported!") utils.log_warn("Character exports from iClone to Blender do not fully support multiple characters.") utils.log_warn("Characters should be exported individually for best results.") if not objects: report.append("No objects in import!") utils.log_error("No objects in import!") return None try: # try to override the import dir with the directory specified in the json: # when exporting from Blender without copying textures, these custom fields # tell us where the textures were originally and under what name import_dir = json_data[name]["Import_Dir"] import_name = json_data[name]["Import_Name"] utils.log_info(f"Using original Import Dir: {import_dir}") utils.log_info(f"Using original Import Name: {import_name}") except: import_name = name import_dir = dir processed = [] chr_json = jsonutils.get_character_json(json_data, name) multi_import = (len(rl_armatures) + len(armatures) > 1) if ImportFlags.FBX in import_flags: for i, arm in enumerate(rl_armatures): # actual name of character # multiple character imports name the armatures after the character # single character imports just name the armature 'armature' so use the file name character_name = name source_id = "Armature" if len(rl_armatures) > 1: source_id = arm.name character_name = utils.safe_export_name(arm.name) armature_objects = utils.get_child_objects(arm, include_parent=True) utils.log_info(f"Generating Character Data: {character_name}") utils.log_indent() chr_cache = props.import_cache.add() chr_cache.import_file = file_path chr_cache.import_flags = import_flags # display name of character chr_cache.character_name = character_name arm["rl_import_file"] = file_path rigutils.fix_cc3_standard_rig(arm) # link_id json_link_id = jsonutils.get_json(json_data, f"{name}/Link_ID") if not link_id and json_link_id: link_id = json_link_id if multi_import or not link_id: link_id = utils.generate_random_id(20) chr_cache.link_id = link_id # root bones process_root_bones(arm, json_data, name) # determine the main texture dir if os.path.exists(chr_cache.get_tex_dir()): chr_cache.import_embedded = False else: chr_cache.import_embedded = True arm.name = character_name arm.data.name = character_name # in case of duplicate names: character_name contains the name currently in Blender. # get_character_id() is the original name. chr_cache.character_name = arm.name # add armature to object_cache chr_cache.add_object_cache(arm) # assign bone collections bones.assign_rl_base_collections(arm) # delete accessory colliders, currently they are useless as # accessories don't export with any physics data or weightmaps. physics.delete_accessory_colliders(arm, objects) # add child objects to object_cache for obj in objects: if obj.type == "MESH" and obj.parent and obj.parent == arm: if only_objects: source_name = utils.strip_name(obj.name) if source_name not in only_objects: continue chr_cache.add_object_cache(obj) # remame actions utils.log_info("Renaming actions:") utils.log_indent() remap_action_names(arm, armature_objects, actions, source_id, motion_prefix=motion_prefix) utils.log_recess() # determine character generation chr_cache.generation = detect_generation(chr_cache, json_data, chr_cache.get_character_id()) utils.log_info("Generation: " + chr_cache.character_name + " (" + chr_cache.generation + ")") arm["rl_generation"] = chr_cache.generation # cache materials for obj_cache in chr_cache.object_cache: if obj_cache.is_mesh(): obj = obj_cache.get_object() cache_object_materials(chr_cache, obj, chr_json, processed) shaders.init_character_property_defaults(chr_cache, chr_json) basic.init_basic_default(chr_cache) # set preserve volume on armature modifiers for obj in objects: if obj.type == "MESH": arm_mod = modifiers.get_object_modifier(obj, "ARMATURE") if arm_mod: arm_mod.use_deform_preserve_volume = False # material setup mode chr_cache.setup_mode = props.setup_mode # character render target chr_cache.render_target = prefs.render_target imported_characters.append(chr_cache.link_id) utils.log_recess() # any none character armatures should be scenes or props for i, arm in enumerate(armatures): character_name = name source_id = "Armature" if len(armatures) > 1: source_id = arm.name character_name = utils.safe_export_name(arm.name) armature_objects = utils.get_child_objects(arm, include_parent=True) utils.log_info(f"Generating Scene/Prop Data: {character_name}") utils.log_indent() chr_cache = props.import_cache.add() chr_cache.import_file = file_path chr_cache.import_flags = import_flags # display name of character chr_cache.character_name = character_name chr_id = chr_cache.get_character_id() # link_id if multi_import: link_id = utils.generate_random_id(20) json_link_id = jsonutils.get_json(json_data, f"{name}/Link_ID") if not multi_import and json_link_id: chr_cache.link_id = json_link_id else: chr_cache.link_id = link_id # root bones process_root_bones(arm, json_data, name) # determine the main texture dir if os.path.exists(chr_cache.get_tex_dir()): chr_cache.import_embedded = False else: chr_cache.import_embedded = True arm.name = character_name arm.data.name = character_name # in case of duplicate names: character_name contains the name currently in Blender. # import_name contains the original name. chr_cache.character_name = arm.name # add armature to object_cache chr_cache.add_object_cache(arm) # add child objects to object_cache for obj in objects: if obj.type == "MESH" and obj.parent and obj.parent == arm: chr_cache.add_object_cache(obj) # remame actions utils.log_info("Renaming actions:") utils.log_indent() remap_action_names(arm, armature_objects, actions, source_id, motion_prefix=motion_prefix) utils.log_recess() # determine character generation chr_cache.generation = "Prop" chr_cache.non_standard_type = "PROP" # cache materials for obj_cache in chr_cache.object_cache: if obj_cache.is_mesh(): obj = obj_cache.get_object() cache_object_materials(chr_cache, obj, chr_json, processed) shaders.init_character_property_defaults(chr_cache, chr_json) basic.init_basic_default(chr_cache) # material setup mode chr_cache.setup_mode = props.setup_mode # character render target chr_cache.render_target = prefs.render_target json_avatar_type = jsonutils.get_json(json_data, f"{chr_id}/Avatar_Type") if json_avatar_type and json_avatar_type == "Prop": rigutils.custom_prop_rig(arm) imported_characters.append(chr_cache.link_id) utils.log_recess() elif ImportFlags.OBJ in import_flags: character_name = name utils.log_info(f"Generating Character Data: {character_name}") utils.log_indent() chr_cache = props.import_cache.add() chr_cache.import_file = file_path chr_cache.import_flags = import_flags # display name of character chr_cache.character_name = character_name # link_id (OBJ exports don't have json) chr_cache.link_id = link_id # determine the main texture dir chr_cache.import_embedded = False for obj in objects: if utils.object_exists_is_mesh(obj): chr_cache.add_object_cache(obj) for obj_cache in chr_cache.object_cache: # scale obj import by 1/100 obj = obj_cache.get_object() if obj: obj.scale = (0.01, 0.01, 0.01) # objkey import is usually a single mesh with no materials # but this is overridable in the pipeline plugin if obj.data.materials and len(obj.data.materials) > 0: cache_object_materials(chr_cache, obj, json_data, processed) shaders.init_character_property_defaults(chr_cache, chr_json) basic.init_basic_default(chr_cache) # material setup mode chr_cache.setup_mode = props.setup_mode # character render target chr_cache.render_target = prefs.render_target imported_characters.append(chr_cache.link_id) utils.log_info("") return imported_characters def obj_import(file_path, split_objects=False, split_groups=False, vgroups=False): split_mode="ON" if (split_objects or split_groups) else "OFF" if utils.B350(): bpy.ops.wm.obj_import(filepath=file_path, use_split_objects=split_objects, use_split_groups=split_groups, import_vertex_groups=vgroups) else: bpy.ops.import_scene.obj(filepath=file_path, split_mode=split_mode, use_split_objects=split_objects, use_split_groups=split_groups, use_groups_as_vgroups=vgroups) # # class ImportFlags(IntFlag): NONE = 0 FBX = 1 OBJ = 2 GLB = 4 VRM = 8 USD = 16 RL = 1024 KEY = 2048 RL_FBX = RL | FBX RL_OBJ = RL | OBJ RL_FBX_KEY = RL_FBX | KEY RL_OBJ_KEY = RL_OBJ | KEY # Import operator # class CC3Import(bpy.types.Operator): """Import CC3 Character and build materials""" bl_idname = "cc3.importer" bl_label = "Import" bl_options = {"REGISTER", "UNDO"} filepath: bpy.props.StringProperty( name="Filepath", description="Filepath of the model to import.", subtype="FILE_PATH" ) directory: bpy.props.StringProperty(subtype='DIR_PATH') files: bpy.props.CollectionProperty( type=bpy.types.OperatorFileListElement, options={'HIDDEN', 'SKIP_SAVE'} ) link_id: bpy.props.StringProperty( default="", name="Link ID", description="Link ID override", options={"HIDDEN"}, ) process_only: bpy.props.StringProperty( default="", options={"HIDDEN"}, ) no_build: bpy.props.BoolProperty( default=False, name="No Build", description="Don't build materials", options={"HIDDEN"}, ) no_rigify: bpy.props.BoolProperty( default=False, name="Don't Rigify", description="Don't Rigify Character", options={"HIDDEN"}, ) filter_glob: bpy.props.StringProperty( default="*.fbx;*.obj;*.glb;*.gltf;*.vrm;*.usd*", options={"HIDDEN"}, ) param: bpy.props.StringProperty( name = "param", default = "", options={"HIDDEN"} ) motion_prefix: bpy.props.StringProperty( name = "Motion Prefix", default = "" ) use_fake_user: bpy.props.BoolProperty( name = "Use Fake User", default = True ) use_anim: bpy.props.BoolProperty(name = "Import Animation", description = "Import animation with character.\nWarning: long animations take a very long time to import in Blender 2.83", default = True) zoom: bpy.props.BoolProperty( default=False, name="Zoom View", description="Zoom view to imported character", ) count = 0 running = False imported = False built = False lighting = False timer = None clock = 0 invoked = False imported_character_ids: list = None imported_materials = [] imported_images = [] import_report = [] import_warn_level = 0 is_morph = False def read_json_data(self, file_path, stage = 0): # if not fbx, return no json without error path, ext = os.path.splitext(file_path) if not utils.is_file_ext(ext, "FBX"): return None errors = [] # importer operator should always read the original intended json data json_data = jsonutils.read_json(file_path, errors, no_local=True) msg = None if "NO_JSON" in errors: msg = "Character has no Json data, using default values." elif "CORRUPT" in errors: if stage == 0: msg = "Corrupted Json data! \nThis character will not set up correctly!" else: msg = "Corrupted Json data! \nThis character will not have been set up correctly!" elif "PATH_FAILED" in errors: if stage == 0: msg = "Unable to locate Json file path! \nThis character will not set up correctly!" else: msg = "Unable to locate Json file path! \nThis character will not have been set up correctly!" if msg and msg not in self.import_report: self.import_report.append(msg) return json_data def import_character(self, context): props = vars.props() prefs = vars.prefs() utils.start_timer() utils.log_info("") utils.log_info("Importing Character Model:") utils.log_info("--------------------------") import_anim = self.use_anim # multi selected files file_paths = self.get_file_paths() for filepath in file_paths: # override link id only if not multi import if len(file_paths) > 1: self.link_id = "" import_flags, param = self.detect_import_mode_from_files(filepath) dir, file = os.path.split(filepath) name, ext = os.path.splitext(file) imported = None actions = None json_data = self.read_json_data(filepath, stage = 0) json_generation = jsonutils.get_character_generation_json(json_data, name) avatar_type = jsonutils.get_json(json_data, f"{name}/Avatar_Type") only_objects = utils.names_to_list(self.process_only, "|") if ImportFlags.FBX in import_flags: # invoke the fbx importer old_objects = utils.get_set(bpy.data.objects) old_images = utils.get_set(bpy.data.images) old_actions = utils.get_set(bpy.data.actions) # in ACES color space, this will fail trying to set up the textures as it tries to use 'Non-Color' space. # But the mesh is really all we need, so just keep going... if colorspace.is_aces(): try: bpy.ops.import_scene.fbx(filepath=filepath, directory=dir, use_anim=import_anim, use_image_search=False, use_custom_normals=True) except: utils.log_warn("FBX Import Error: This may be due to color space differences. Continuing...") else: try: bpy.ops.import_scene.fbx(filepath=filepath, directory=dir, use_anim=import_anim, use_image_search=False, use_custom_normals=True) except: utils.log_error("FBX Import Error due to bad mesh?") imported = utils.get_set_new(bpy.data.objects, old_objects) actions = utils.get_set_new(bpy.data.actions, old_actions) self.imported_images = utils.get_set_new(bpy.data.images, old_images) remove_objects = [] if only_objects: for obj in imported: if obj.type == "ARMATURE": continue source_name = utils.strip_name(obj.name) if source_name in only_objects: continue remove_objects.append(obj) for obj in remove_objects: imported.remove(obj) purge_imported_object(obj, self.imported_images) for action in actions: action.use_fake_user = self.use_fake_user armatures, rl_armatures, import_flags = self.get_character_armatures(imported, avatar_type, json_generation, import_flags) # detect characters and objects imported_character_ids = None if ImportFlags.RL in import_flags: imported_character_ids = process_rl_import(filepath, import_flags, armatures, rl_armatures, imported, actions, json_data, self.import_report, self.link_id, only_objects=only_objects, motion_prefix=self.motion_prefix) elif prefs.import_auto_convert: chr_cache = characters.convert_generic_to_non_standard(imported, filepath, link_id=self.link_id) imported_character_ids = [chr_cache.link_id] # add the imported characters if imported_character_ids: self.imported_character_ids.extend(imported_character_ids) if imported_character_ids and ImportFlags.RL in import_flags: imported_characters = props.get_characters_by_link_id(imported_character_ids) for chr_cache in imported_characters: # set up the collision shapes and store their bind positions in the json data rigidbody.build_rigid_body_colliders(chr_cache, json_data, first_import = True) # remove the colliders for now (only needed for spring bones) rigidbody.remove_rigid_body_colliders(chr_cache.get_armature()) utils.log_timer("Done .Fbx Import.") elif ImportFlags.OBJ in import_flags: # invoke the obj importer old_objects = utils.get_set(bpy.data.objects) old_images = utils.get_set(bpy.data.images) if ImportFlags.RL in import_flags and param == "IMPORT_MORPH": obj_import(filepath, split_objects=False, split_groups=False, vgroups=True) else: obj_import(filepath, split_objects=True, split_groups=True, vgroups=False) imported = utils.get_set_new(bpy.data.objects, old_objects) self.imported_images = utils.get_set_new(bpy.data.images, old_images) # detect characters and objects armatures = [] rl_armatures = [] imported_character_ids = None if ImportFlags.RL in import_flags: imported_character_ids = process_rl_import(filepath, import_flags, armatures, rl_armatures, imported, actions, json_data, self.import_report, self.link_id, motion_prefix=self.motion_prefix) elif prefs.import_auto_convert: chr_cache = characters.convert_generic_to_non_standard(imported, filepath, link_id=self.link_id) imported_character_ids = [ chr_cache.link_id ] # add the imported characters if imported_character_ids: self.imported_character_ids.extend(imported_character_ids) #if param == "IMPORT_MORPH": # if self.imported_character.get_tex_dir() != "": # reconstruct_obj_materials(obj) # pass utils.log_timer("Done .Obj Import.") elif ImportFlags.GLB in import_flags: # invoke the GLTF importer old_images = utils.get_set(bpy.data.images) bpy.ops.import_scene.gltf(filepath=filepath) imported = bpy.context.selected_objects.copy() self.imported_images = utils.get_set_new(bpy.data.images, old_images) chr_cache = None if prefs.import_auto_convert: chr_cache = characters.convert_generic_to_non_standard(imported, filepath, link_id=self.link_id) # add the imported characters if chr_cache: self.imported_character_ids.append(chr_cache.link_id) utils.log_timer("Done .GLTF Import.") elif ImportFlags.VRM in import_flags: # copy .vrm to .glb glb_path = os.path.join(dir, name + "_temp.glb") shutil.copyfile(filepath, glb_path) filepath = glb_path # invoke the GLTF importer old_images = utils.get_set(bpy.data.images) bpy.ops.import_scene.gltf(filepath = filepath, bone_heuristic="TEMPERANCE") imported = bpy.context.selected_objects.copy() self.imported_images = utils.get_set_new(bpy.data.images, old_images) # find the armature and rotate it 180 degrees in Z armature : bpy.types.Object = utils.get_armature_from_objects(imported) hik.fix_armature(armature) utils.try_select_objects(imported) os.remove(glb_path) chr_cache = None if prefs.import_auto_convert: chr_cache = characters.convert_generic_to_non_standard(imported, filepath, link_id=self.link_id) # add the imported characters if chr_cache: self.imported_character_ids.append(chr_cache.link_id) utils.log_timer("Done .vrm Import.") elif ImportFlags.USD in import_flags: # invoke the USD importer old_images = utils.get_set(bpy.data.images) bpy.ops.wm.usd_import(filepath=filepath) imported = bpy.context.selected_objects.copy() self.imported_images = utils.get_set_new(bpy.data.images, old_images) chr_cache = None if prefs.import_auto_convert: chr_cache = characters.convert_generic_to_non_standard(imported, filepath, link_id=self.link_id) # add the imported characters if chr_cache: self.imported_character_ids.append(chr_cache.link_id) utils.log_timer("Done .USD Import?") def build_materials(self, context): objects_processed = [] props: properties.CC3ImportProps = vars.props() prefs = vars.prefs() utils.start_timer() utils.log_info("") utils.log_info("Building Character Materials:") utils.log_info("-----------------------------") nodeutils.check_node_groups() if self.imported_character_ids: on_import = True imported_characters = props.get_characters_by_link_id(self.imported_character_ids) else: on_import = False chr_cache = props.get_context_character_cache(context) imported_characters = [ chr_cache ] chr_cache: properties.CC3CharacterCache for chr_cache in imported_characters: if ImportFlags.RL not in ImportFlags(chr_cache.import_flags): continue json_data = self.read_json_data(chr_cache.import_file, stage = 1) if not on_import: # when rebuilding, use the currently selected render target chr_cache.render_target = prefs.render_target chr_json = jsonutils.get_character_json(json_data, chr_cache.get_character_id()) if self.param == "BUILD": chr_cache.check_material_types(chr_json) if prefs.import_deduplicate: processed_images = [] processed_materials = [] else: processed_images = None processed_materials = None if props.build_mode == "IMPORTED": chr_objects = chr_cache.get_cache_objects() for obj in chr_objects: obj_cache = chr_cache.get_object_cache(obj) if obj and obj_cache: process_object(chr_cache, obj, obj_cache, objects_processed, chr_json, processed_materials, processed_images) # setup default physics if props.physics_mode: utils.log_info("") physics.apply_all_physics(chr_cache) chr_cache.build_count += 1 # only processes the selected objects that are listed in the import_cache (character) elif props.build_mode == "SELECTED": for obj in bpy.context.selected_objects: obj_cache = chr_cache.get_object_cache(obj) if obj_cache: process_object(chr_cache, obj, obj_cache, objects_processed, chr_json, processed_materials, processed_images) chr_cache.build_count += 1 # enable SSR if prefs.refractive_eyes == "SSR": if not utils.B420(): bpy.context.scene.eevee.use_ssr = True bpy.context.scene.eevee.use_ssr_refraction = True utils.log_timer("Done Build.", "s") def build_drivers(self, context, rebuild_wrinkle=False): props: properties.CC3ImportProps = vars.props() prefs = vars.prefs() utils.start_timer() utils.log_info("") utils.log_info("Building Character Drivers:") utils.log_info("---------------------------") if self.imported_character_ids: imported_characters = props.get_characters_by_link_id(self.imported_character_ids) else: chr_cache = props.get_context_character_cache(context) imported_characters = [ chr_cache ] chr_cache: properties.CC3CharacterCache for chr_cache in imported_characters: if ImportFlags.RL not in ImportFlags(chr_cache.import_flags): continue json_data = self.read_json_data(chr_cache.import_file, stage=1) chr_json = jsonutils.get_character_json(json_data, chr_cache.get_character_id()) if chr_cache.rigified: drivers.clear_facial_shape_key_bone_drivers(chr_cache) rigging.add_shape_key_drivers(chr_cache, chr_cache.get_armature()) else: objects = chr_cache.get_all_objects(include_armature=False, of_type="MESH") facial_profile, viseme_profile = meshutils.get_facial_profile(objects) utils.log_info(f"Facial Profile: {facial_profile}") utils.log_info(f"Viseme Profile: {viseme_profile}") if facial_profile == "Std" or facial_profile == "Ext" or facial_profile == "ExPlus": drivers.add_facial_shape_key_bone_drivers(chr_cache, prefs.build_shape_key_bone_drivers_jaw, prefs.build_shape_key_bone_drivers_eyes, prefs.build_shape_key_bone_drivers_head) driver_objects = chr_cache.get_all_objects(include_armature=False, of_type="MESH", only_selected=(props.build_mode=="SELECTED")) drivers.add_body_shape_key_drivers(chr_cache, prefs.build_body_key_drivers, driver_objects) if rebuild_wrinkle: wrinkle.build_wrinkle_drivers(chr_cache, chr_json, wrinkle_shader_name=wrinkle.WRINKLE_SHADER_NAME) utils.log_timer("Done Build.", "s") def remove_drivers(self, context): props: properties.CC3ImportProps = vars.props() utils.start_timer() utils.log_info("") utils.log_info("Building Character Drivers:") utils.log_info("---------------------------") if self.imported_character_ids: imported_characters = props.get_characters_by_link_id(self.imported_character_ids) else: chr_cache = props.get_context_character_cache(context) imported_characters = [ chr_cache ] chr_cache: properties.CC3CharacterCache for chr_cache in imported_characters: if ImportFlags.RL not in ImportFlags(chr_cache.import_flags): continue drivers.clear_facial_shape_key_bone_drivers(chr_cache) driver_objects = chr_cache.get_all_objects(include_armature=False, of_type="MESH", only_selected=(props.build_mode=="SELECTED")) drivers.add_body_shape_key_drivers(chr_cache, False, driver_objects) utils.log_timer("Done Build.", "s") def detect_import_mode_from_files(self, filepath): # detect if we are importing a character for morph/accessory editing (i.e. has a key file) dir, file = os.path.split(filepath) name, ext = os.path.splitext(file) textures_path = os.path.join(dir, "textures", name) json_path = os.path.join(dir, name + ".json") import_flags = ImportFlags.NONE param = self.param if utils.is_file_ext(ext, "OBJ"): import_flags = import_flags | ImportFlags.OBJ obj_key_path = os.path.join(dir, name + ".ObjKey") if os.path.exists(obj_key_path): import_flags = import_flags | ImportFlags.RL import_flags = import_flags | ImportFlags.KEY param = "IMPORT_MORPH" self.is_morph = True utils.log_info("Importing as character morph with ObjKey. (nude character with bind pose)") return import_flags, param elif utils.is_file_ext(ext, "FBX"): import_flags = import_flags | ImportFlags.FBX obj_key_path = os.path.join(dir, name + ".fbxkey") if os.path.exists(obj_key_path): import_flags = import_flags | ImportFlags.RL import_flags = import_flags | ImportFlags.KEY param = "IMPORT_MORPH" self.is_morph = True utils.log_info("Importing as editable character with fbxkey.") return import_flags, param elif utils.is_file_ext(ext, "GLB") or utils.is_file_ext(ext, "GLTF"): import_flags = import_flags | ImportFlags.GLB utils.log_info("Importing generic GLB/GLTF character.") return import_flags, param elif utils.is_file_ext(ext, "VRM"): import_flags = import_flags | ImportFlags.VRM utils.log_info("Importing generic VRM character.") return import_flags, param elif utils.is_file_ext(ext, "USD") or utils.is_file_ext(ext, "USDZ"): import_flags = import_flags | ImportFlags.USD utils.log_info("Importing Universal Scene Descriptor file.") return import_flags, param if os.path.exists(json_path) or os.path.exists(textures_path): import_flags = import_flags | ImportFlags.RL utils.log_info("Importing RL character without key file.") else: utils.log_info("Importing generic character.") param = "IMPORT_QUALITY" return import_flags, param def get_character_armatures(self, objects, avatar_type, json_generation, import_flags): armatures = [] rl_armatures = [] if not avatar_type: if json_generation is not None and json_generation == "": avatar_type = "NoneStandard" elif json_generation is None: avatar_type = "None" for obj in objects: if utils.object_exists_is_armature(obj): if (avatar_type == "Standard" or avatar_type == "NonHuman" or avatar_type == "NonStandard" or avatar_type == "StandardSeries" or rigutils.is_GameBase_armature(obj) or rigutils.is_ActorCore_armature(obj) or rigutils.is_G3_armature(obj) or rigutils.is_iClone_armature(obj)): utils.log_info(f"RL character armature found: {obj.name}") import_flags = import_flags | ImportFlags.RL if obj not in rl_armatures: rl_armatures.append(obj) else: if obj not in armatures: armatures.append(obj) return armatures, rl_armatures, import_flags def do_import_report(self, context, stage = 0): if stage == 0: # FBX import and JSON report if self.import_report: utils.report_multi(self, "ERROR", self.import_report) elif stage == 1: if self.import_report: utils.report_multi(self, "ERROR", self.import_report) else: self.report({'INFO'}, "All Done!") self.import_report = [] def run_import(self, context): self.import_character(context) self.imported = True def run_build(self, context): if self.imported_character_ids: self.build_materials(context) self.build_drivers(context) self.built = True def run_finish(self, context): props = vars.props() prefs = vars.prefs() if self.imported_character_ids: rl_import = False imported_characters = props.get_characters_by_link_id(self.imported_character_ids) for chr_cache in imported_characters: if ImportFlags.RL in ImportFlags(chr_cache.import_flags): rl_import = True # for any objects with shape keys expand the slider range to -1.0 <> 1.0 # Character Creator and iClone both use negative ranges extensively. for obj in chr_cache.get_cache_objects(): obj_cache = chr_cache.get_object_cache(obj) if obj_cache and obj_cache.is_mesh(): init_shape_key_range(obj) if rl_import: # use portrait lighting for quality mode if self.param == "IMPORT_QUALITY": if props.lighting_mode: scene.setup_scene_default(context, prefs.quality_lighting) if prefs.refractive_eyes == "SSR": if not utils.B420(): bpy.context.scene.eevee.use_ssr = True bpy.context.scene.eevee.use_ssr_refraction = True # set a minimum of 100 max transparency bounces: if bpy.context.scene.cycles.transparent_max_bounces < 100: bpy.context.scene.cycles.transparent_max_bounces = 100 if self.zoom: bpy.ops.object.select_all(action='DESELECT') for chr_cache in imported_characters: chr_cache.select_all(only=False) scene.zoom_to_selected() # clean up unused images from the import if len(self.imported_images) > 0: utils.log_info("Cleaning up unused images:") img: bpy.types.Image = None for img in self.imported_images: num_users = img.users if (img.use_fake_user and img.users == 1) or img.users == 0: utils.log_info("Removing Image: " + img.name) bpy.data.images.remove(img) utils.clean_collection(bpy.data.images) props.lighting_mode = False if props.rigify_mode and not self.no_rigify: for chr_cache in imported_characters: if chr_cache.can_be_rigged(): cc3_rig = chr_cache.get_armature() bpy.ops.cc3.rigifier(param="ALL") rigging.full_retarget_source_rig_action(self, chr_cache, cc3_rig, use_ui_options=True) self.imported_character_ids = None self.imported_materials = [] self.imported_images = [] self.lighting = True def modal(self, context, event): # 60 second timeout if event.type == 'TIMER': self.clock = self.clock + 1 if self.clock > 600: self.cancel(context) self.report({'INFO'}, "Import operator timed out!") return {'CANCELLED'} if event.type == 'TIMER' and self.clock > 10 and not self.running: self.count += 1 if self.count > 99: self.count = 0 context.window_manager.progress_update(self.count) if not self.imported: self.running = True self.run_import(context) self.do_import_report(context, stage = 0) self.clock = 0 self.running = False elif not self.no_build and not self.built: self.running = True self.run_build(context) self.clock = 0 self.running = False elif not self.no_build and not self.lighting: self.running = True self.run_finish(context) self.clock = 0 self.running = False if self.imported and (self.no_build or (self.built and self.lighting)): self.cancel(context) self.do_import_report(context, stage = 1) return {'FINISHED'} return {'PASS_THROUGH'} def cancel(self, context): if self.timer is not None: context.window_manager.event_timer_remove(self.timer) self.timer = None context.window_manager.progress_end() def get_file_paths(self): file_paths = [] if not self.files or len(self.files) == 0: if os.path.exists(self.filepath): file_paths.append(self.filepath) else: for file in self.files: filepath = os.path.join(self.directory, file.name) if os.path.exists(filepath): file_paths.append(filepath) return file_paths def execute(self, context): props = vars.props() prefs = vars.prefs() self.imported_character_ids = [] self.imported_materials = [] self.imported_images = [] self.import_report = [] context.window_manager.progress_begin(0, 99) self.count = 0 # import character if "IMPORT" in self.param: file_paths = self.get_file_paths() if file_paths: if self.invoked and self.timer is None: self.imported = False self.built = False self.lighting = False self.running = False self.clock = 0 self.report({'INFO'}, "Importing Character, please wait for import to finish and materials to build...") bpy.context.window_manager.modal_handler_add(self) self.timer = context.window_manager.event_timer_add(0.1, window = bpy.context.window) return {'PASS_THROUGH'} elif not self.invoked: self.run_import(context) if not self.no_build: self.run_build(context) self.run_finish(context) self.do_import_report(context, stage = 1) return {'FINISHED'} else: utils.log_error(f"Invalid filepaths!") # build materials elif self.param == "BUILD": chr_cache = props.get_context_character_cache(context) if chr_cache: mode_selection = utils.store_mode_selection_state() utils.object_mode() self.build_materials(context) self.build_drivers(context) self.do_import_report(context, stage = 1) utils.restore_mode_selection_state(mode_selection) elif self.param == "BUILD_DRIVERS": chr_cache = props.get_context_character_cache(context) if chr_cache: mode_selection = utils.store_mode_selection_state() utils.object_mode() self.build_drivers(context, rebuild_wrinkle=True) self.report({"INFO"}, "Drivers Rebuilt!") utils.restore_mode_selection_state(mode_selection) elif self.param == "REMOVE_DRIVERS": chr_cache = props.get_context_character_cache(context) if chr_cache: mode_selection = utils.store_mode_selection_state() utils.object_mode() self.remove_drivers(context) self.report({"INFO"}, "Drivers Removed!") utils.restore_mode_selection_state(mode_selection) # rebuild the node groups for advanced materials elif self.param == "REBUILD_NODE_GROUPS": utils.object_mode() nodeutils.rebuild_node_groups() utils.clean_collection(bpy.data.images) self.build_materials(context) self.build_drivers(context) self.do_import_report(context, stage = 1) elif self.param == "DELETE_CHARACTER": chr_cache = props.get_context_character_cache(context) if chr_cache: delete_import(chr_cache) elif self.param == "REBUILD_EEVEE": chr_cache = props.get_context_character_cache(context) if chr_cache: utils.object_mode() prefs.render_target = "EEVEE" prefs.refractive_eyes = "PARALLAX" if chr_cache.render_target != "EEVEE": utils.log_info("Character is currently build for Cycles Rendering.") utils.log_info("Rebuilding Character for Eevee Rendering...") self.build_materials(context) self.build_drivers(context) elif self.param == "REBUILD_BAKE": chr_cache = props.get_context_character_cache(context) if chr_cache: utils.object_mode() prefs.render_target = "EEVEE" if chr_cache.render_target != "EEVEE": utils.log_info("Character is currently build for Cycles Rendering.") utils.log_info("Rebuilding Character for Eevee Rendering...") self.build_materials(context) self.build_drivers(context) elif self.param == "REBUILD_CYCLES": chr_cache = props.get_context_character_cache(context) if chr_cache: utils.object_mode() prefs.render_target = "CYCLES" prefs.refractive_eyes = "SSR" if chr_cache.render_target != "CYCLES": utils.log_info("Character is currently build for Eevee Rendering.") utils.log_info("Rebuilding Character for Cycles Rendering...") self.build_materials(context) self.build_drivers(context) return {"FINISHED"} def invoke(self, context, event): if "IMPORT" in self.param: context.window_manager.fileselect_add(self) self.invoked = True return {"RUNNING_MODAL"} return self.execute(context) @classmethod def description(cls, context, properties): if "IMPORT" in properties.param: return "Import a new .fbx or .obj character exported by Character Creator 3.\n" \ "Notes for exporting from CC3:\n" \ " - For round trip-editing (exporting character back to CC3), export as FBX: 'Mesh Only' or 'Mesh and Motion' with Calibration, from CC3, as this guarantees generation of the .fbxkey file needed to re-import the character back to CC3.\n" \ " - For creating morph sliders, export as OBJ: Nude Character in Bind Pose from CC3, as this is the only way to generate the .ObjKey file for morph slider creation in CC3.\n" \ " - FBX export with motion in 'Current Pose' or 'Custom Motion' does not export an .fbxkey and cannot be exported back to CC3.\n" \ " - OBJ export 'Character with Current Pose' does not create an .objkey and cannot be exported back to CC3.\n" \ " - OBJ export 'Nude Character in Bind Pose' .obj does not export any materials" elif properties.param == "BUILD": return "Rebuild materials and drivers for the current imported character with the current build settings" elif properties.param == "BUILD_DRIVERS": return "Rebuild the facial expression shape-key and bone drivers for the current imported character with the current build settings" elif properties.param == "REMOVE_DRIVERS": return "Remove the facial expression shape-key and bone drivers for the current imported character" elif properties.param == "DELETE_CHARACTER": return "Removes the character and any associated objects, meshes, materials, nodes, images, armature actions and shapekeys. Basically deletes everything not nailed down.\n**Do not press this if there is anything you want to keep!**" elif properties.param == "REBUILD_NODE_GROUPS": return "Rebuilds the shader node groups for for all material shaders" return "" class CC3ImportAnimations(bpy.types.Operator): """Import CC3 animations""" bl_idname = "cc3.anim_importer" bl_label = "Import Animations" bl_options = {"REGISTER", "UNDO", 'PRESET'} filepath: bpy.props.StringProperty( name="Filepath", description="Filepath of the fbx to import.", subtype="FILE_PATH" ) directory: bpy.props.StringProperty(subtype='DIR_PATH') files: bpy.props.CollectionProperty( type=bpy.types.OperatorFileListElement, options={'HIDDEN', 'SKIP_SAVE'} ) filter_glob: bpy.props.StringProperty( default="*.fbx", options={"HIDDEN"} ) remove_meshes: bpy.props.BoolProperty( default = True, description="Remove all imported mesh objects.", name="Remove Meshes", ) remove_materials_images: bpy.props.BoolProperty( default = True, description="Remove all imported materials and image textures.", name="Remove Materials & Images", ) remove_shape_keys: bpy.props.BoolProperty( default = False, description="Remove Shapekey actions along with their meshes.", name="Remove Shapekey Actions", ) param: bpy.props.StringProperty( name = "param", default = "", options={"HIDDEN"} ) motion_prefix: bpy.props.StringProperty( name = "Motion Prefix", default = "" ) use_fake_user: bpy.props.BoolProperty( name = "Use Fake User", default = True ) def import_animation_fbx(self, dir, file): path = os.path.join(dir, file) name = file[:-4] utils.log_info(f"Importing Fbx file: {path}") # invoke the fbx importer old_objects = utils.get_set(bpy.data.objects) old_images = utils.get_set(bpy.data.images) old_actions = utils.get_set(bpy.data.actions) old_materials = utils.get_set(bpy.data.materials) bpy.ops.import_scene.fbx(filepath=path, directory=dir, use_anim=True, use_image_search=False) objects = utils.get_set_new(bpy.data.objects, old_objects) actions = utils.get_set_new(bpy.data.actions, old_actions) images = utils.get_set_new(bpy.data.images, old_images) materials = utils.get_set_new(bpy.data.materials, old_materials) for action in actions: action.use_fake_user = self.use_fake_user utils.log_info("Renaming actions:") utils.log_indent() # find all armatures armatures = [] for obj in objects: if obj.type == "ARMATURE": armatures.append(obj) # assign animation sets for arm in armatures: armature_objects = utils.get_child_objects(arm) source_id = arm.name # just one armature is always named 'Armature' if len(armatures) == 1: arm.name = name arm.data.name = name shapekey_actions = remap_action_names(arm, armature_objects, actions, source_id, motion_prefix=self.motion_prefix)[1] utils.log_recess() utils.log_info("Cleaning up:") utils.log_indent() for obj in objects: if obj.type == "ARMATURE": obj.name = name if obj.data: obj.data.name = name if self.remove_meshes: # only interested in actions, delete the rest for obj in objects: if obj.type != "ARMATURE": utils.log_info(f"Removing Object: {obj.name}") utils.delete_mesh_object(obj) # and optionally remove the shape keys if self.remove_shape_keys: for action in shapekey_actions: utils.log_info(f"Removing Shapekey Action: {action.name}") bpy.data.actions.remove(action) if self.remove_materials_images: for img in images: utils.log_info(f"Removing Image: {img.name}") bpy.data.images.remove(img) for mat in materials: utils.log_info(f"Removing Material: {mat.name}") bpy.data.materials.remove(mat) utils.log_recess() return def execute(self, context): props = vars.props() prefs = vars.prefs() utils.start_timer() utils.log_info("") utils.log_info("Importing FBX Animations:") utils.log_info("-------------------------") for fbx_file in self.files: self.import_animation_fbx(self.directory, fbx_file.name) if not self.files and self.filepath: dir, file = os.path.split(self.filepath) self.import_animation_fbx(dir, file) utils.log_timer("Done Build.", "s") return {"FINISHED"} def invoke(self, context, event): context.window_manager.fileselect_add(self) return {"RUNNING_MODAL"} @classmethod def description(cls, context, properties): return "" def menu_func_import(self, context): self.layout.operator(CC3Import.bl_idname, text="Reallusion Character (.fbx, .obj, .vrm)").param = "IMPORT_MENU" def menu_func_import_animation(self, context): self.layout.operator(CC3ImportAnimations.bl_idname, text="Reallusion Animation (.fbx)")