1866 lines
73 KiB
Python
1866 lines
73 KiB
Python
# Copyright (C) 2021 Victor Soupday
|
|
# This file is part of CC/iC Blender Tools <https://github.com/soupday/cc_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 <https://www.gnu.org/licenses/>.
|
|
|
|
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)")
|
|
|