2672 lines
112 KiB
Python
2672 lines
112 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 copy
|
|
import shutil
|
|
import re
|
|
import mathutils
|
|
import math
|
|
|
|
import bpy
|
|
from filecmp import cmp
|
|
|
|
from . import (hik, rigging, rigutils, bake, shaders, physics, rigidbody, wrinkle, bones, modifiers,
|
|
imageutils, meshutils, nodeutils, jsonutils, utils, params, vars)
|
|
|
|
UNPACK_INDEX = 1001
|
|
|
|
|
|
def get_export_armature(chr_cache, objects):
|
|
arm = None
|
|
if chr_cache:
|
|
arm = chr_cache.get_armature()
|
|
if arm:
|
|
return arm
|
|
|
|
arm = utils.get_armature_from_objects(objects)
|
|
return arm
|
|
|
|
def check_valid_export_fbx(chr_cache, objects):
|
|
report = []
|
|
check_valid = True
|
|
check_warn = False
|
|
arm = get_export_armature(chr_cache, objects)
|
|
|
|
standard = False
|
|
if chr_cache:
|
|
standard = chr_cache.is_standard()
|
|
|
|
if not objects:
|
|
message = f"ERROR: Nothing to export!"
|
|
report.append(message)
|
|
utils.log_warn(message)
|
|
check_valid = False
|
|
|
|
if standard and not arm:
|
|
if chr_cache:
|
|
message = f"ERROR: Character {chr_cache.character_name} has no armature!"
|
|
else:
|
|
message = f"ERROR: Character has no armature!"
|
|
report.append(message)
|
|
utils.log_warn(message)
|
|
check_valid = False
|
|
|
|
else:
|
|
obj : bpy.types.Object
|
|
for obj in objects:
|
|
if obj != arm and utils.object_exists_is_mesh(obj):
|
|
if standard:
|
|
armature_mod : bpy.types.ArmatureModifier = modifiers.get_object_modifier(obj, "ARMATURE")
|
|
if armature_mod is None:
|
|
message = f"ERROR: Object: {obj.name} does not have an armature modifier."
|
|
report.append(message)
|
|
utils.log_warn(message)
|
|
check_valid = False
|
|
if obj.parent != arm:
|
|
message = f"ERROR: Object: {obj.name} is not parented to character armature."
|
|
report.append(message)
|
|
utils.log_warn(message)
|
|
check_valid = False
|
|
if armature_mod and armature_mod.object != arm:
|
|
message = f"ERROR: Object: {obj.name}'s armature modifier is not set to this character's armature."
|
|
report.append(message)
|
|
utils.log_warn(message)
|
|
check_valid = False
|
|
if len(obj.vertex_groups) == 0:
|
|
message = f"ERROR: Object: {obj.name} has no vertex groups."
|
|
report.append(message)
|
|
utils.log_warn(message)
|
|
check_valid = False
|
|
# doesn't seem to be an issue anymore
|
|
if False and obj.type == "MESH" and obj.data and len(obj.data.vertices) < 150:
|
|
message = f"WARNING: Object: {obj.name} has a low number of vertices (less than 150), this is can cause CTD issues with CC3's importer."
|
|
report.append(message)
|
|
utils.log_warn(message)
|
|
message = f" (if CC3 crashes when importing this character, consider increasing vertex count or joining this object to another.)"
|
|
report.append(message)
|
|
utils.log_warn(message)
|
|
check_warn = True
|
|
|
|
return check_valid, check_warn, report
|
|
|
|
|
|
def remove_modifiers_for_export(chr_cache, objects, reset_pose, rig=None):
|
|
if not rig:
|
|
rig = get_export_armature(chr_cache, objects)
|
|
if not rig:
|
|
return
|
|
rig.data.pose_position = "POSE"
|
|
obj : bpy.types.Object
|
|
for obj in objects:
|
|
if reset_pose:
|
|
if obj.type == "MESH" and obj.data.shape_keys and obj.data.shape_keys.key_blocks:
|
|
utils.safe_set_action(obj.data.shape_keys, None)
|
|
if chr_cache:
|
|
obj_cache = chr_cache.get_object_cache(obj)
|
|
if obj_cache:
|
|
if obj_cache.object_type == "OCCLUSION" or obj_cache.object_type == "TEARLINE" or obj_cache.object_type == "EYE":
|
|
mod : bpy.types.Modifier
|
|
for mod in obj.modifiers:
|
|
if vars.NODE_PREFIX in mod.name:
|
|
obj.modifiers.remove(mod)
|
|
if reset_pose:
|
|
utils.safe_set_action(rig, None)
|
|
bones.clear_pose(rig)
|
|
|
|
|
|
def restore_modifiers(chr_cache, objects):
|
|
obj : bpy.types.Object
|
|
for obj in objects:
|
|
obj_cache = chr_cache.get_object_cache(obj)
|
|
if obj_cache:
|
|
if obj_cache.object_type == "OCCLUSION":
|
|
modifiers.add_eye_occlusion_modifiers(obj)
|
|
elif obj_cache.object_type == "TEARLINE":
|
|
modifiers.add_tearline_modifiers(obj)
|
|
elif obj_cache.object_type == "EYE":
|
|
modifiers.add_eye_modifiers(obj)
|
|
|
|
|
|
def prep_export(context, chr_cache, new_name, objects, json_data, old_path, new_path,
|
|
copy_textures, revert_duplicates, apply_fixes, as_blend_file, bake_values,
|
|
materials=None, sync=False, force_bake=False):
|
|
prefs = vars.prefs()
|
|
|
|
if sync:
|
|
revert_duplicates=False
|
|
apply_fixes = False
|
|
as_blend_file = False
|
|
bake_values = True
|
|
copy_textures = False
|
|
|
|
bake_nodes = prefs.export_bake_nodes
|
|
bake_bump_to_normal = prefs.export_bake_bump_to_normal
|
|
if force_bake:
|
|
bake_nodes = True
|
|
bake_bump_to_normal = True
|
|
|
|
utils.log_info(f"Prepping Export: {new_name}")
|
|
|
|
if as_blend_file:
|
|
if prefs.export_unity_remove_objects:
|
|
# remove everything not part of the character for blend file exports.
|
|
arm = get_export_armature(chr_cache, objects)
|
|
for obj in bpy.data.objects:
|
|
if not (obj == arm or obj.parent == arm or chr_cache.has_object(obj)):
|
|
utils.log_info(f"Removing {obj.name} from blend file")
|
|
bpy.data.objects.remove(obj)
|
|
|
|
if not chr_cache or not json_data:
|
|
return None
|
|
|
|
objects_map = {}
|
|
physics_map = {}
|
|
mats_processed = {}
|
|
images_processed = {}
|
|
|
|
# old path might be blank, so try to use blend file path or export target path
|
|
base_path = old_path
|
|
if not base_path:
|
|
base_path = utils.local_path()
|
|
if not base_path:
|
|
base_path = new_path
|
|
|
|
# reset and unlock shape keys
|
|
if not sync:
|
|
utils.reset_shape_keys(objects)
|
|
|
|
# update character name in json data
|
|
old_name = chr_cache.get_character_id()
|
|
if new_name != old_name:
|
|
if (old_name in json_data.keys() and
|
|
old_name in json_data[old_name]["Object"].keys() and
|
|
new_name not in json_data.keys()):
|
|
# rename the object and character keys
|
|
json_data[old_name]["Object"][new_name] = json_data[old_name]["Object"].pop(chr_cache.get_character_id())
|
|
json_data[new_name] = json_data.pop(old_name)
|
|
|
|
chr_json = json_data[new_name]["Object"][new_name]
|
|
|
|
# create soft physics json if none
|
|
physics_json = jsonutils.add_json_path(chr_json, "Physics/Soft Physics/Meshes")
|
|
|
|
# set custom JSON data
|
|
json_data[new_name]["Blender_Project"] = True
|
|
if not copy_textures:
|
|
json_data[new_name]["Import_Dir"] = chr_cache.get_import_dir()
|
|
json_data[new_name]["Import_Name"] = chr_cache.get_character_id()
|
|
else:
|
|
json_data[new_name].pop("Import_Dir", None)
|
|
json_data[new_name].pop("Import_Name", None)
|
|
|
|
if not chr_cache.link_id:
|
|
chr_cache.link_id = utils.generate_random_id(20)
|
|
json_data[new_name]["Link_ID"] = chr_cache.link_id
|
|
|
|
if chr_cache.is_non_standard():
|
|
set_non_standard_generation(json_data, new_name, chr_cache.non_standard_type, chr_cache.generation)
|
|
|
|
# unpack embedded textures.
|
|
if chr_cache.import_embedded:
|
|
unpack_embedded_textures(chr_cache, chr_json, objects, base_path)
|
|
|
|
if revert_duplicates:
|
|
# get a list of all cached materials in the export back to CC3
|
|
export_mats = []
|
|
for obj in objects:
|
|
obj_cache = chr_cache.get_object_cache(obj)
|
|
if obj_cache and obj.type == "MESH":
|
|
for mat in obj.data.materials:
|
|
mat_cache = chr_cache.get_material_cache(mat)
|
|
if mat and mat_cache and mat not in export_mats:
|
|
export_mats.append(mat)
|
|
|
|
# CC3 will replace any ' ' or '.' with underscores on export, so the only .00X suffix is from Blender
|
|
# get a use count of each material source name (stripped of any blender duplicate name suffixes)
|
|
mat_count = {}
|
|
for mat in export_mats:
|
|
mat_name = mat.name
|
|
mat_safe_name = utils.safe_export_name(utils.strip_name(mat_name), is_material=True)
|
|
if mat_safe_name in mat_count.keys():
|
|
mat_count[mat_safe_name] += 1
|
|
else:
|
|
mat_count[mat_safe_name] = 1
|
|
|
|
# determine a single source of any duplicate material names, prefer an exact match
|
|
mat_remap = {}
|
|
for mat_safe_name in mat_count.keys():
|
|
count = mat_count[mat_safe_name]
|
|
if count > 1:
|
|
for mat in export_mats:
|
|
if mat.name == mat_safe_name:
|
|
mat_remap[mat_safe_name] = mat
|
|
break
|
|
elif mat.name.startswith(mat_safe_name):
|
|
mat_remap[mat_safe_name] = mat
|
|
|
|
obj_names = []
|
|
obj : bpy.types.Object
|
|
for obj in objects:
|
|
|
|
if not utils.object_exists_is_mesh(obj):
|
|
continue
|
|
|
|
utils.log_info(f"Object: {obj.name} / {obj.data.name}")
|
|
utils.log_indent()
|
|
|
|
obj_name = obj.name
|
|
obj_cache = chr_cache.get_object_cache(obj)
|
|
is_split = chr_cache.is_split_object(obj)
|
|
split_source_name = obj_cache.source_name if (is_split and obj_cache) else None
|
|
source_changed = False
|
|
is_new_object = False
|
|
|
|
if obj_cache and not is_split:
|
|
obj_expected_source_name = utils.safe_export_name(utils.strip_name(obj_name))
|
|
obj_source_name = obj_cache.source_name
|
|
utils.log_info(f"Object source name: {obj_source_name}")
|
|
source_changed = obj_expected_source_name != obj_source_name
|
|
if source_changed:
|
|
obj_safe_name = utils.safe_export_name(obj_name)
|
|
utils.log_info(f"Object name changed from source, using: {obj_safe_name}")
|
|
else:
|
|
obj_safe_name = obj_source_name
|
|
else:
|
|
is_new_object = True
|
|
obj_safe_name = utils.safe_export_name(obj_name, is_split=is_split)
|
|
obj_source_name = obj_safe_name
|
|
obj["rl_do_not_restore_name"] = True
|
|
|
|
# if the Object name has been changed in some way
|
|
if obj_name != obj_safe_name or obj.data.name != obj_safe_name:
|
|
new_obj_name = obj_safe_name
|
|
if is_new_object or source_changed or new_obj_name in obj_names:
|
|
new_obj_name = utils.make_unique_name_in(obj_safe_name, bpy.data.objects.keys())
|
|
elif new_obj_name in obj_names:
|
|
# if multiple objects imported had the same name there will be duplicate source names:
|
|
# so if the new name is already in use, create a new unique name
|
|
# this will also trigger a new json object to be created which is needed
|
|
# as json object names should be unique and it's not possible in Blender to export
|
|
# two different objects with the same name.
|
|
new_obj_name = utils.make_unique_name_in(obj_safe_name, bpy.data.objects.keys())
|
|
utils.log_info(f"Using new safe Object & Mesh name: {obj_name} to {new_obj_name}")
|
|
if source_changed:
|
|
if jsonutils.rename_json_key(chr_json["Meshes"], obj_source_name, new_obj_name):
|
|
utils.log_info(f"Updating Object source json name: {obj_source_name} to {new_obj_name}")
|
|
if physics_json and jsonutils.rename_json_key(physics_json, obj_source_name, new_obj_name):
|
|
utils.log_info(f"Updating Physics Object source json name: {obj_source_name} to {new_obj_name}")
|
|
obj_source_name = new_obj_name
|
|
if not sync:
|
|
utils.force_object_name(obj, new_obj_name)
|
|
utils.force_mesh_name(obj.data, new_obj_name)
|
|
obj_name = new_obj_name
|
|
obj_safe_name = new_obj_name
|
|
|
|
obj_names.append(obj_name)
|
|
|
|
# fetch or create the object json
|
|
obj_json = jsonutils.get_object_json(chr_json, obj_source_name)
|
|
physics_mesh_json = jsonutils.get_physics_mesh_json(physics_json, obj_source_name)
|
|
if not obj_json:
|
|
utils.log_info(f"Adding Object Json: {obj_name}")
|
|
obj_json = copy.deepcopy(params.JSON_MESH_DATA)
|
|
chr_json["Meshes"][obj_name] = obj_json
|
|
if not physics_mesh_json and obj_cache and obj_cache.cloth_physics == "ON":
|
|
utils.log_info(f"Adding Physics Object Json: {obj_name}")
|
|
physics_mesh_json = copy.deepcopy(params.JSON_PHYSICS_MESH)
|
|
physics_json[obj_name] = physics_mesh_json
|
|
|
|
# store the json keys
|
|
obj_key = jsonutils.get_object_json_key(chr_json, obj_json)
|
|
objects_map.setdefault(obj_key, [])
|
|
if physics_mesh_json:
|
|
physics_mesh_key = jsonutils.get_physics_mesh_json_key(physics_json, physics_mesh_json)
|
|
physics_map.setdefault(physics_mesh_key, [])
|
|
|
|
for slot in obj.material_slots:
|
|
mat = slot.material
|
|
if mat is None: continue
|
|
if materials and mat not in materials: continue
|
|
mat_name = mat.name
|
|
mat_cache = chr_cache.get_material_cache(mat)
|
|
source_changed = False
|
|
new_material = False
|
|
|
|
utils.log_info(f"Material: {mat.name}")
|
|
utils.log_indent()
|
|
|
|
if mat.name not in mats_processed.keys():
|
|
mats_processed[mat.name] = { "processed": False, "write_back": False, "copied": False, "remapped": False }
|
|
mat_data = mats_processed[mat.name]
|
|
|
|
if mat_cache:
|
|
mat_expected_source_name = (utils.safe_export_name(utils.strip_name(mat_name), is_material=True)
|
|
if revert_duplicates else
|
|
utils.safe_export_name(mat_name, is_material=True))
|
|
mat_source_name = mat_cache.source_name
|
|
source_changed = mat_expected_source_name != mat_source_name
|
|
if source_changed:
|
|
mat_safe_name = utils.safe_export_name(mat_name, is_material=True)
|
|
else:
|
|
mat_safe_name = mat_source_name
|
|
else:
|
|
new_material = True
|
|
mat_safe_name = utils.safe_export_name(mat_name, is_material=True)
|
|
mat_source_name = mat_safe_name
|
|
|
|
if mat_name != mat_safe_name:
|
|
new_mat_name = mat_safe_name
|
|
if new_material or source_changed:
|
|
new_mat_name = utils.make_unique_name_in(mat_safe_name, bpy.data.materials.keys())
|
|
utils.log_info(f"Using new safe Material name: {mat_name} to {new_mat_name}")
|
|
if source_changed:
|
|
if jsonutils.rename_json_key(obj_json["Materials"], mat_source_name, new_mat_name):
|
|
utils.log_info(f"Updating material json name: {mat_source_name} to {new_mat_name}")
|
|
if physics_mesh_json and jsonutils.rename_json_key(physics_mesh_json["Materials"], mat_source_name, new_mat_name):
|
|
utils.log_info(f"Updating physics material json name: {mat_source_name} to {new_mat_name}")
|
|
if not sync:
|
|
utils.force_material_name(mat, new_mat_name)
|
|
mat_name = new_mat_name
|
|
mat_safe_name = new_mat_name
|
|
mat_source_name = new_mat_name
|
|
|
|
# fetch or create the material json
|
|
write_json = prefs.export_json_changes
|
|
write_physics_json = write_json
|
|
write_textures = prefs.export_texture_changes
|
|
write_physics_textures = write_textures
|
|
mat_json = jsonutils.get_material_json(obj_json, mat)
|
|
physics_mat_json = jsonutils.get_physics_material_json(physics_mesh_json, mat)
|
|
|
|
# the object and materials may have been split from it's origin,
|
|
# so try to find the material in the source object json
|
|
if obj_cache and mat_cache and not mat_json and split_source_name:
|
|
split_obj_json = jsonutils.get_object_json(chr_json, split_source_name)
|
|
if split_obj_json:
|
|
split_mat_json = jsonutils.get_material_json(split_obj_json, mat_source_name)
|
|
if split_mat_json:
|
|
utils.log_info(f"Copying Material Json: {mat_safe_name} from split source material: {split_source_name} / {mat_source_name}")
|
|
mat_json = copy.deepcopy(split_mat_json)
|
|
if mat_json:
|
|
obj_json["Materials"][mat_safe_name] = mat_json
|
|
write_json = True
|
|
write_textures = True
|
|
|
|
# then look for same material in source character objects
|
|
if mat_cache and not mat_json:
|
|
for other_obj_cache in chr_cache.object_cache:
|
|
other = other_obj_cache.get_object()
|
|
if utils.object_exists_is_mesh(other):
|
|
if mat.name in other.data.materials:
|
|
other_source_name = other_obj_cache.source_name
|
|
other_obj_json = jsonutils.get_object_json(chr_json, other_source_name)
|
|
if other_obj_json:
|
|
other_mat_json = jsonutils.get_material_json(other_obj_json, mat_source_name)
|
|
if other_mat_json:
|
|
utils.log_info(f"Copying Material Json: {mat_safe_name} from existing material Json in Obj: {other_source_name} / {mat_source_name}")
|
|
mat_json = copy.deepcopy(other_mat_json)
|
|
break
|
|
if mat_json:
|
|
obj_json["Materials"][mat_safe_name] = mat_json
|
|
write_json = True
|
|
write_textures = True
|
|
|
|
# finally try to find a mat_json of the same shader type
|
|
# with the same source material name in any mesh in the json
|
|
if mat_cache and not mat_json:
|
|
for o_json_name, o_json in chr_json["Meshes"].items():
|
|
for m_json_name, m_json in o_json["Materials"].items():
|
|
if m_json_name.lower() == mat_source_name.lower():
|
|
shader_name = params.get_rl_shader_name(mat_cache)
|
|
m_shader_name = jsonutils.get_custom_shader(m_json)
|
|
if shader_name == m_shader_name:
|
|
utils.log_info(f"Copying Material Json: {mat_safe_name} from existing material Json of same name and type: {o_json_name} / {m_json_name}")
|
|
mat_json = copy.deepcopy(m_json)
|
|
break
|
|
if mat_json:
|
|
break
|
|
if mat_json:
|
|
obj_json["Materials"][mat_safe_name] = mat_json
|
|
write_json = True
|
|
write_textures = True
|
|
|
|
# if still no json, try to create the material json data from the mat_cache shader def
|
|
if mat_cache and not mat_json:
|
|
shader_name = params.get_shader_name(mat_cache)
|
|
json_template = params.get_mat_shader_template(mat_cache)
|
|
utils.log_info(f"Adding Material Json: {mat_name} for Shader: {shader_name}")
|
|
if json_template:
|
|
mat_json = copy.deepcopy(json_template)
|
|
obj_json["Materials"][mat_safe_name] = mat_json
|
|
write_json = True
|
|
write_textures = True
|
|
|
|
# fallback default to PBR material json data
|
|
if not mat_json:
|
|
utils.log_info(f"Adding Default PBR Material Json: {mat_name}")
|
|
mat_json = copy.deepcopy(params.JSON_PBR_MATERIAL)
|
|
obj_json["Materials"][mat_safe_name] = mat_json
|
|
write_json = True
|
|
write_textures = True
|
|
|
|
material_physics_enabled = physics.is_cloth_physics_enabled(mat_cache, mat, obj)
|
|
if physics_mesh_json and not physics_mat_json and material_physics_enabled:
|
|
physics_mat_json = copy.deepcopy(params.JSON_PHYSICS_MATERIAL)
|
|
physics_mesh_json["Materials"][mat_safe_name] = physics_mat_json
|
|
write_physics_json = True
|
|
write_physics_textures = True
|
|
|
|
# store the json keys
|
|
mat_key = jsonutils.get_material_json_key(obj_json, mat_json)
|
|
objects_map[obj_key].append(mat_key)
|
|
if physics_mat_json:
|
|
physics_mat_key = jsonutils.get_physics_material_json_key(physics_mesh_json, physics_mat_json)
|
|
physics_map[physics_mesh_key].append(physics_mat_key)
|
|
|
|
if mat_cache:
|
|
utils.log_info("Writing Json:")
|
|
utils.log_indent()
|
|
# update the json parameters with any changes
|
|
if write_textures:
|
|
write_back_textures(context, mat_json, mat, mat_cache, base_path, old_name, bake_values, mat_data,
|
|
bake_nodes, bake_bump_to_normal, images_processed)
|
|
if write_json:
|
|
write_back_json(mat_json, mat, mat_cache)
|
|
if write_physics_json:
|
|
# there isn't a meaningful way to convert between Blender physics and RL PhysX
|
|
pass
|
|
if write_physics_textures:
|
|
write_back_physics_weightmap(physics_mat_json, obj, mat, mat_cache, base_path, old_name, mat_data)
|
|
if not sync and revert_duplicates:
|
|
# replace duplicate materials with a reference to a single source material
|
|
# (this is to ensure there are no duplicate suffixes in the fbx export)
|
|
if mat_count[mat_safe_name] > 1:
|
|
new_mat = mat_remap[mat_safe_name]
|
|
slot.material = new_mat
|
|
mat = new_mat
|
|
mat_name = new_mat.name
|
|
if mat_name != mat_safe_name:
|
|
utils.log_info(f"Reverting material name: {mat_name} to {mat_safe_name}")
|
|
utils.force_material_name(mat, mat_safe_name)
|
|
utils.log_recess()
|
|
else:
|
|
# add pbr material to json for non-cached base object/material
|
|
write_pbr_material_to_json(context, mat, mat_json, base_path, old_name, bake_values)
|
|
|
|
# copy or remap the texture paths
|
|
utils.log_info("Finalizing Texture Paths:")
|
|
utils.log_indent()
|
|
if copy_textures:
|
|
images_copied = []
|
|
for channel in mat_json["Textures"].keys():
|
|
copy_and_update_texture_path(mat_json["Textures"][channel], "Texture Path", old_path, new_path, old_name, new_name, as_blend_file, mat_name, mat_data, images_copied)
|
|
if "Custom Shader" in mat_json.keys():
|
|
for channel in mat_json["Custom Shader"]["Image"].keys():
|
|
copy_and_update_texture_path(mat_json["Custom Shader"]["Image"][channel], "Texture Path", old_path, new_path, old_name, new_name, as_blend_file, mat_name, mat_data, images_copied)
|
|
if physics_mat_json:
|
|
copy_and_update_texture_path(physics_mat_json, "Weight Map Path", old_path, new_path, old_name, new_name, as_blend_file, mat_name, mat_data, images_copied)
|
|
if "Wrinkle" in mat_json.keys():
|
|
for channel in mat_json["Wrinkle"]["Textures"].keys():
|
|
copy_and_update_texture_path(mat_json["Wrinkle"]["Textures"][channel], "Texture Path", old_path, new_path, old_name, new_name, as_blend_file, mat_name, mat_data, images_copied)
|
|
|
|
else:
|
|
for channel in mat_json["Textures"].keys():
|
|
remap_texture_path(mat_json["Textures"][channel], "Texture Path", old_path, new_path, mat_data)
|
|
if "Custom Shader" in mat_json.keys():
|
|
for channel in mat_json["Custom Shader"]["Image"].keys():
|
|
remap_texture_path(mat_json["Custom Shader"]["Image"][channel], "Texture Path", old_path, new_path, mat_data)
|
|
if physics_mat_json:
|
|
remap_texture_path(physics_mat_json, "Weight Map Path", old_path, new_path, mat_data)
|
|
if "Wrinkle" in mat_json.keys():
|
|
for channel in mat_json["Wrinkle"]["Textures"].keys():
|
|
remap_texture_path(mat_json["Wrinkle"]["Textures"][channel], "Texture Path", old_path, new_path, mat_data)
|
|
|
|
mat_data["processed"] = True
|
|
# texure paths
|
|
utils.log_recess()
|
|
|
|
# material
|
|
utils.log_recess()
|
|
|
|
# object
|
|
utils.log_recess()
|
|
|
|
if apply_fixes and prefs.export_legacy_bone_roll_fix:
|
|
if obj.type == "ARMATURE":
|
|
if utils.object_mode():
|
|
utils.set_active_object(obj)
|
|
if utils.set_mode("EDIT"):
|
|
utils.log_info("Applying upper and lower teeth bones roll fix.")
|
|
bone = obj.data.edit_bones["CC_Base_Teeth01"]
|
|
bone.roll = 0
|
|
bone = obj.data.edit_bones["CC_Base_Teeth02"]
|
|
bone.roll = 0
|
|
utils.object_mode()
|
|
|
|
if sync:
|
|
# find all mesh/material keys not used
|
|
meshes_json = jsonutils.get_json(json_data, f"{new_name}/Object/{new_name}/Meshes")
|
|
del_keys = []
|
|
for obj_key in objects_map:
|
|
mat_keys = objects_map[obj_key]
|
|
obj_json = jsonutils.get_json(json_data, f"{new_name}/Object/{new_name}/Meshes/{obj_key}")
|
|
for key in obj_json["Materials"]:
|
|
if key not in mat_keys:
|
|
utils.log_detail(f"Removing: material {obj_key}/{key}")
|
|
del_keys.append((obj_json["Materials"], key))
|
|
for key in meshes_json:
|
|
if key not in objects_map:
|
|
utils.log_detail(f"Removing: object {key}")
|
|
del_keys.append((meshes_json, key))
|
|
# find all physics mesh/material keys not used
|
|
physics_meshes_json = jsonutils.get_json(json_data, f"{new_name}/Object/{new_name}/Physics/Soft Physics/Meshes")
|
|
for physics_mesh_key in physics_map:
|
|
physics_mat_keys = physics_map[physics_mesh_key]
|
|
physics_mesh_json = jsonutils.get_json(json_data, f"{new_name}/Object/{new_name}/Physics/Soft Physics/Meshes/{physics_mesh_key}")
|
|
for key in physics_mesh_json["Materials"]:
|
|
if key not in physics_mat_keys:
|
|
utils.log_detail(f"Removing: physics material {physics_mesh_key}/{key}")
|
|
del_keys.append((physics_mesh_json["Materials"], key))
|
|
for key in physics_meshes_json:
|
|
if key not in physics_map:
|
|
utils.log_detail(f"Removing: physics object {key}")
|
|
del_keys.append((physics_meshes_json, key))
|
|
# remove the keys
|
|
for dictionary, key in del_keys:
|
|
if key in dictionary:
|
|
del(dictionary[key])
|
|
|
|
|
|
# as the baking system can deselect everything, reselect the export objects here.
|
|
utils.try_select_objects(objects, True)
|
|
|
|
|
|
def remap_texture_path(tex_info, path_key, old_path, new_path, mat_data):
|
|
|
|
# at this point all the image paths have been re-written as absolute paths
|
|
# (except those not used in the Blender material shaders)
|
|
|
|
if path_key in tex_info.keys():
|
|
if tex_info[path_key]:
|
|
tex_path = tex_info[path_key]
|
|
if os.path.isabs(tex_path):
|
|
abs_path = tex_path
|
|
else:
|
|
abs_path = os.path.normpath(os.path.join(old_path, tex_path))
|
|
rel_path = utils.relpath(abs_path, new_path)
|
|
tex_info[path_key] = os.path.normpath(rel_path)
|
|
utils.log_info(f"Remapping JSON texture path to: {tex_info[path_key]}")
|
|
return
|
|
|
|
|
|
def copy_and_update_texture_path(tex_info, path_key, old_path, new_path, old_name, new_name, as_blend_file, mat_name, mat_data, images_copied):
|
|
"""keep the same relative folder structure and copy the textures to their target folder.
|
|
update the images in the blend file with the new location."""
|
|
|
|
# at this point all the image paths have been re-written as absolute paths
|
|
sep = os.path.sep
|
|
old_tex_base = os.path.join(old_path, f"textures{sep}{old_name}")
|
|
old_fbm_base = os.path.join(old_path, f"{old_name}.fbm")
|
|
|
|
if path_key in tex_info.keys():
|
|
|
|
tex_path : str = tex_info[path_key]
|
|
if tex_path:
|
|
|
|
if not os.path.isabs(tex_path):
|
|
tex_path = os.path.normpath(os.path.join(old_path, tex_path))
|
|
|
|
old_abs_path = os.path.normpath(tex_path)
|
|
|
|
# old_path will only be set from a successful import from CC/iC
|
|
# so it should have expected the CC/iC folder structure
|
|
if old_path:
|
|
rel_tex_path = utils.relpath(os.path.normpath(tex_path), old_path)
|
|
|
|
# only remap the tex_path if it is inside the expected texture folders
|
|
if utils.path_is_parent(old_tex_base, old_abs_path) or utils.path_is_parent(old_fbm_base, old_abs_path):
|
|
if old_name != new_name:
|
|
rel_tex_path = rel_tex_path.replace(f"textures{sep}{old_name}{sep}{old_name}{sep}", f"textures{sep}{new_name}{sep}{new_name}{sep}")
|
|
rel_tex_path = rel_tex_path.replace(f"textures{sep}{old_name}{sep}", f"textures{sep}{new_name}{sep}")
|
|
rel_tex_path = rel_tex_path.replace(f"{old_name}.fbm{sep}", f"{new_name}.fbm{sep}")
|
|
|
|
new_abs_path = os.path.normpath(os.path.join(new_path, rel_tex_path))
|
|
new_rel_path = os.path.normpath(utils.relpath(new_abs_path, new_path))
|
|
|
|
utils.log_info(f"Remapping JSON texture path to: {new_rel_path}")
|
|
|
|
else:
|
|
|
|
# otherwise put the textures in folders in the textures/CHARACTER_NAME/Extras/MATERIAL_NAME/ folder
|
|
dir, file = os.path.split(tex_path)
|
|
extras_dir = f"textures{sep}{new_name}{sep}Extras{sep}{mat_name}"
|
|
new_rel_path = os.path.normpath(os.path.join(extras_dir, file))
|
|
new_abs_path = os.path.normpath(os.path.join(new_path, new_rel_path))
|
|
|
|
utils.log_info(f"Setting JSON texture path to: {new_rel_path}")
|
|
|
|
copy_file = False
|
|
if os.path.exists(old_abs_path):
|
|
if os.path.exists(new_abs_path):
|
|
if not cmp(old_abs_path, new_abs_path):
|
|
copy_file = True
|
|
else:
|
|
copy_file = True
|
|
|
|
if copy_file:
|
|
# make sure path exists
|
|
dir_path = os.path.dirname(new_abs_path)
|
|
os.makedirs(dir_path, exist_ok=True)
|
|
# copy the texture
|
|
utils.log_info(f"Copying texture: {old_abs_path}")
|
|
utils.log_info(f" to: {new_abs_path}")
|
|
shutil.copyfile(old_abs_path, new_abs_path)
|
|
|
|
# update the json texture path with the new relative path
|
|
tex_info[path_key] = new_rel_path
|
|
|
|
# update images with changed file path (if it changed, and only if exporting as blend file)
|
|
if as_blend_file and os.path.exists(old_abs_path) and os.path.exists(new_abs_path):
|
|
# if the original path and new path are different
|
|
if os.path.normpath(old_abs_path) != os.path.normpath(new_abs_path):
|
|
image : bpy.types.Image
|
|
for image in bpy.data.images:
|
|
# for each image not already copied
|
|
if image and image.filepath and image not in images_copied:
|
|
image_file_path = bpy.path.abspath(image.filepath)
|
|
if os.path.exists(image_file_path):
|
|
# if this is the image specified in the json path
|
|
if os.path.samefile(image_file_path, old_abs_path):
|
|
utils.log_info(f"Updating .blend Image: {image.name}")
|
|
utils.log_info(f" to: {new_abs_path}")
|
|
image.filepath = new_abs_path
|
|
images_copied.append(image)
|
|
|
|
|
|
def restore_export(export_changes : list):
|
|
if not export_changes:
|
|
return
|
|
# undo everything prep_export did
|
|
# (but don't bother with the json data as it is temporary)
|
|
for info in export_changes:
|
|
op = info[0]
|
|
if op == "OBJECT_RENAME":
|
|
obj = info[1]
|
|
utils.force_object_name(obj, info[2])
|
|
if obj.type == "MESH" and obj.data:
|
|
utils.force_mesh_name(obj.data, info[3])
|
|
if obj.type == "ARMATURE" and obj.data:
|
|
utils.force_armature_name(obj.data, info[3])
|
|
elif op == "MATERIAL_RENAME":
|
|
mat = info[1]
|
|
utils.force_material_name(mat, info[2])
|
|
elif op == "MATERIAL_SLOT_REPLACE":
|
|
slot = info[1]
|
|
slot.material = info[2]
|
|
slot.material = info[2]
|
|
return
|
|
|
|
|
|
def get_prop_value(mat_cache, prop_name, default):
|
|
parameters = mat_cache.parameters
|
|
try:
|
|
return eval("parameters." + prop_name, None, locals())
|
|
except:
|
|
return default
|
|
|
|
|
|
def write_back_json(mat_json, mat, mat_cache):
|
|
shader_name = params.get_shader_name(mat_cache)
|
|
shader_def = params.get_shader_def(shader_name)
|
|
|
|
if mat_json is None:
|
|
return
|
|
|
|
if shader_def:
|
|
if "vars" in shader_def.keys():
|
|
for var_def in shader_def["vars"]:
|
|
prop_name = var_def[0]
|
|
prop_default = var_def[1]
|
|
func = var_def[2]
|
|
if func == "":
|
|
args = var_def[3:]
|
|
json_var = args[0]
|
|
if json_var and json_var != "":
|
|
prop_value = get_prop_value(mat_cache, prop_name, prop_default)
|
|
jsonutils.set_material_json_var(mat_json, json_var, prop_value)
|
|
|
|
if "export" in shader_def.keys():
|
|
for export_def in shader_def["export"]:
|
|
json_var = export_def[0]
|
|
json_default = export_def[1]
|
|
func = export_def[2]
|
|
args = export_def[3:]
|
|
json_value = shaders.eval_parameters_func(mat_cache.parameters, func, args, json_default)
|
|
jsonutils.set_material_json_var(mat_json, json_var, json_value)
|
|
|
|
|
|
def write_back_textures(context, mat_json: dict, mat, mat_cache, base_path, old_name, bake_values, mat_data,
|
|
bake_nodes, bake_bump_to_normal, images_processed):
|
|
global UNPACK_INDEX
|
|
prefs = vars.prefs()
|
|
|
|
if mat_json is None:
|
|
return
|
|
|
|
shader_name = params.get_shader_name(mat_cache)
|
|
rl_shader_name = params.get_rl_shader_name(mat_cache)
|
|
shader_def = params.get_shader_def(shader_name)
|
|
bsdf_node, shader_node, mix_node = nodeutils.get_shader_nodes(mat, shader_name)
|
|
has_custom_shader = "Custom Shader" in mat_json.keys()
|
|
|
|
unpack_path = os.path.join(base_path, "textures", old_name, "Unpack")
|
|
bake_path = os.path.join(base_path, "textures", old_name, "Baked")
|
|
custom_path = os.path.join(base_path, "textures", old_name, "Custom")
|
|
|
|
bake.init_bake()
|
|
UNPACK_INDEX = 1001
|
|
|
|
# determine if we are combining bump maps into normal maps:
|
|
normal_socket = params.get_shader_texture_socket(shader_def, "NORMAL")
|
|
bump_socket = params.get_shader_texture_socket(shader_def, "BUMP")
|
|
normal_connected = normal_socket and nodeutils.has_connected_input(shader_node, normal_socket)
|
|
bump_combining = False
|
|
if bake_bump_to_normal and bake_nodes:
|
|
bump_combining = normal_connected and bump_socket and nodeutils.has_connected_input(shader_node, bump_socket)
|
|
|
|
if shader_def and shader_node:
|
|
|
|
if "textures" in shader_def.keys():
|
|
|
|
for tex_def in shader_def["textures"]:
|
|
tex_type = tex_def[2]
|
|
shader_socket = tex_def[0]
|
|
tex_id = params.get_texture_json_id(tex_type)
|
|
is_pbr_texture = tex_type in params.PBR_TYPES
|
|
is_pbr_shader = shader_name == "rl_pbr_shader" or shader_name == "rl_sss_shader"
|
|
tex_node = nodeutils.get_node_connected_to_input(shader_node, shader_socket)
|
|
|
|
tex_info = None
|
|
bake_value_texture = False
|
|
bake_shader_socket = ""
|
|
bake_value_size = 64
|
|
|
|
roughness_modified = False
|
|
if tex_type == "ROUGHNESS":
|
|
roughness = 0.5
|
|
if not nodeutils.has_connected_input(shader_node, "Roughness Map"):
|
|
roughness = nodeutils.get_node_input_value(shader_node, "Roughness Map", 0.5)
|
|
def_min = 0
|
|
def_max = 1
|
|
def_pow = 1
|
|
#if shader_name == "rl_sss_shader":
|
|
# def_pow = 0.75
|
|
roughness_min = nodeutils.get_node_input_value(shader_node, "Roughness Min", def_min)
|
|
roughness_max = nodeutils.get_node_input_value(shader_node, "Roughness Max", def_max)
|
|
roughness_pow = nodeutils.get_node_input_value(shader_node, "Roughness Power", def_pow)
|
|
if roughness_min != def_min or roughness_max != def_max or roughness != 0.5:
|
|
roughness_modified = True
|
|
|
|
# find or generate tex_info json.
|
|
if is_pbr_texture:
|
|
|
|
# CC3 cannot set metallic or roughness values without textures, so must bake a small value texture
|
|
if not tex_node:
|
|
|
|
if tex_type == "DIFFUSE":
|
|
if bake_values:
|
|
bake_value_texture = True
|
|
bake_shader_socket = "Base Color"
|
|
|
|
if tex_type == "ROUGHNESS":
|
|
if bake_values and roughness_modified:
|
|
bake_value_texture = True
|
|
bake_shader_socket = "Roughness"
|
|
elif not bake_values:
|
|
mat_json["Roughness_Value"] = roughness
|
|
|
|
elif tex_type == "METALLIC":
|
|
metallic = nodeutils.get_node_input_value(shader_node, "Metallic Map", 0)
|
|
if bake_values and metallic > 0:
|
|
bake_value_texture = True
|
|
bake_shader_socket = "Metallic"
|
|
elif not bake_values:
|
|
mat_json["Metallic_Value"] = metallic
|
|
|
|
# fetch the tex_info data for the channel
|
|
if tex_id in mat_json["Textures"]:
|
|
tex_info = mat_json["Textures"][tex_id]
|
|
|
|
# or create a new tex_info if missing or baking a new texture
|
|
elif tex_node or bake_value_texture:
|
|
tex_info = copy.deepcopy(params.JSON_PBR_TEX_INFO)
|
|
location, rotation, scale = nodeutils.get_image_node_mapping(tex_node)
|
|
tex_info["Tiling"] = [scale[0], scale[1]]
|
|
tex_info["Offset"] = [location[0], location[1]]
|
|
mat_json["Textures"][tex_id] = tex_info
|
|
|
|
# note: strength values for textures defined in the shader vars are written after in write_back_json()
|
|
|
|
elif has_custom_shader:
|
|
if tex_id in mat_json["Custom Shader"]["Image"]:
|
|
tex_info = mat_json["Custom Shader"]["Image"][tex_id]
|
|
elif tex_node:
|
|
tex_info = copy.deepcopy(params.JSON_CUSTOM_TEX_INFO)
|
|
mat_json["Custom Shader"]["Image"][tex_id] = tex_info
|
|
|
|
# if bump and normal are connected and we are combining them,
|
|
# remove bump maps from the Json and don't process it:
|
|
if tex_info and tex_type == "BUMP" and bump_combining:
|
|
tex_info = None
|
|
del mat_json["Textures"][tex_id]
|
|
|
|
if tex_info:
|
|
|
|
processed_image = None
|
|
if tex_type in mat_data.keys():
|
|
processed_image = mat_data[tex_type]
|
|
if processed_image:
|
|
utils.log_info(f"Reusing already processed material image: {processed_image.name}")
|
|
|
|
if tex_node or bake_value_texture:
|
|
|
|
image : bpy.types.Image = None
|
|
|
|
# re-use the already processed image if available
|
|
if processed_image:
|
|
image = processed_image
|
|
|
|
else:
|
|
|
|
# if it needs a value texture, bake the value
|
|
if bake_value_texture:
|
|
|
|
# turn off ao for diffuse bakes
|
|
if tex_type == "DIFFUSE":
|
|
ao = nodeutils.get_node_input_value(shader_node, "AO Strength", 1.0)
|
|
nodeutils.set_node_input_value(shader_node, "AO Strength", 0)
|
|
|
|
image = bake.bake_node_socket_input(context, bsdf_node, bake_shader_socket,
|
|
mat, tex_id, bake_path,
|
|
override_size=bake_value_size)
|
|
|
|
if tex_type == "DIFFUSE":
|
|
ao = nodeutils.get_node_input_value(shader_node, "AO Strength", ao)
|
|
|
|
elif nodeutils.is_texture_pack_system(tex_node):
|
|
|
|
utils.log_info(f"Texture: {tex_id} for socket: {shader_socket} is connected to a texture pack. Skipping.")
|
|
continue
|
|
|
|
elif wrinkle.is_wrinkle_system(tex_node):
|
|
|
|
utils.log_info(f"Texture: {tex_id} for socket: {shader_socket} is connected to the wrinkle shader. Skipping.")
|
|
continue
|
|
|
|
# if there is an image texture link to the socket
|
|
elif tex_node and tex_node.type == "TEX_IMAGE":
|
|
|
|
# bake roughnesss min/max adjustments (but not power)
|
|
if tex_type == "ROUGHNESS" and roughness_modified:
|
|
roughness_pow = nodeutils.get_node_input_value(shader_node, "Roughness Power", def_pow)
|
|
nodeutils.set_node_input_value(shader_node, "Roughness Power", 1.0)
|
|
image = bake.bake_node_socket_input(context, bsdf_node, "Roughness",
|
|
mat, tex_id, bake_path,
|
|
size_override_node=shader_node,
|
|
size_override_socket="Roughness Map")
|
|
nodeutils.set_node_input_value(shader_node, "Roughness Power", roughness_pow)
|
|
|
|
# if there is a normal and a bump map connected, combine into a normal
|
|
elif bake_nodes and tex_type == "NORMAL" and bump_combining:
|
|
image = bake.bake_rl_bump_and_normal(context, shader_node, bsdf_node,
|
|
mat, tex_id, bake_path,
|
|
normal_socket_name=shader_socket,
|
|
bump_socket_name=bump_socket)
|
|
|
|
# otherwise use the image texture
|
|
else:
|
|
image = tex_node.image
|
|
|
|
elif bake_nodes:
|
|
|
|
# if something is connected to the shader socket but is not a texture image
|
|
# and baking is enabled: then bake the socket input into a texture for exporting:
|
|
if tex_type == "NORMAL" and bump_combining:
|
|
image = bake.bake_rl_bump_and_normal(context, shader_node, bsdf_node, mat, tex_id, bake_path,
|
|
normal_socket_name = shader_socket,
|
|
bump_socket_name = bump_socket)
|
|
else:
|
|
|
|
utils.log_info(f"Baking Socket Input: {shader_node.name} {shader_socket}")
|
|
image = bake.bake_node_socket_input(context, shader_node, shader_socket,
|
|
mat, tex_id, bake_path)
|
|
|
|
tex_info["Texture Path"] = ""
|
|
mat_data[tex_type] = image
|
|
|
|
if image:
|
|
|
|
try_unpack_image(image, unpack_path, True)
|
|
|
|
if not image.filepath:
|
|
try:
|
|
# image is not saved?
|
|
if image.file_format:
|
|
format = image.file_format
|
|
else:
|
|
format = "PNG"
|
|
imageutils.save_image_to_format_dir(image, format, custom_path, image.name)
|
|
except:
|
|
utils.log_warn(f"Unable to save unsaved image: {image.name} to custom image dir!")
|
|
|
|
if image.filepath:
|
|
|
|
image_data = None
|
|
if image in images_processed.keys():
|
|
image_data = images_processed[image]
|
|
else:
|
|
abs_image_path = os.path.normpath(bpy.path.abspath(image.filepath))
|
|
image_data = { "old_path": abs_image_path }
|
|
images_processed[image] = image_data
|
|
|
|
abs_image_path = image_data["old_path"]
|
|
|
|
tex_info["Texture Path"] = abs_image_path
|
|
utils.log_info(f"{mat.name}/{tex_id}: Source texture path: {abs_image_path}")
|
|
|
|
elif not tex_node:
|
|
tex_info["Texture Path"] = ""
|
|
|
|
|
|
mat_data["write_back"] = True
|
|
|
|
|
|
def write_back_physics_weightmap(physics_mat_json : dict, obj, mat, mat_cache, base_path, old_name, mat_data):
|
|
global UNPACK_INDEX
|
|
prefs = vars.prefs()
|
|
|
|
if physics_mat_json is None:
|
|
return
|
|
|
|
unpack_path = os.path.join(base_path, "textures", old_name, "Unpack")
|
|
UNPACK_INDEX = 1001
|
|
|
|
image = physics.get_weight_map_from_modifiers(obj, mat)
|
|
|
|
if image:
|
|
|
|
mat_data["WEIGHTMAP"] = image
|
|
|
|
try_unpack_image(image, unpack_path, True)
|
|
|
|
if image.filepath:
|
|
abs_image_path = bpy.path.abspath(image.filepath)
|
|
if abs_image_path:
|
|
utils.log_info(f"{mat.name}: Using new weight map texture path: {abs_image_path}")
|
|
physics_mat_json["Weight Map Path"] = abs_image_path
|
|
|
|
|
|
def get_unique_path(path):
|
|
if os.path.exists(path):
|
|
dir, file = os.path.split(path)
|
|
name, ext = os.path.splitext(file)
|
|
index = 1001
|
|
file = name + "_" + str(index) + ext
|
|
path = os.path.join(dir, file)
|
|
while os.path.exists(path):
|
|
index += 1
|
|
file = name + "_" + str(index) + ext
|
|
path = os.path.join(dir, file)
|
|
return path
|
|
|
|
|
|
def try_unpack_image(image, folder, index_suffix = False):
|
|
global UNPACK_INDEX
|
|
try:
|
|
if image.packed_file:
|
|
if image.filepath:
|
|
temp_dir, name = os.path.split(bpy.path.abspath(image.filepath))
|
|
else:
|
|
name = image.name
|
|
if image.file_format == "PNG":
|
|
name = name + ".png"
|
|
elif image.file_format == "JPEG":
|
|
name = name + ".jpg"
|
|
else:
|
|
name = name + "." + image.file_format.lower()
|
|
if index_suffix:
|
|
root, ext = os.path.splitext(name)
|
|
name = root + "_" + str(UNPACK_INDEX) + ext
|
|
UNPACK_INDEX += 1
|
|
image_path = os.path.join(folder, name)
|
|
utils.log_info(f"Unpacking image: {name}")
|
|
if not os.path.exists(folder):
|
|
os.makedirs(folder)
|
|
image.unpack(method = "REMOVE")
|
|
image.filepath_raw = image_path
|
|
image.save()
|
|
return True
|
|
except:
|
|
utils.log_warn(f"Unable to unpack image: {name}")
|
|
return False
|
|
|
|
|
|
def unpack_embedded_textures(chr_cache, chr_json, objects, base_path):
|
|
prefs = vars.prefs()
|
|
|
|
unpack_folder = None
|
|
if chr_cache:
|
|
unpack_folder = os.path.join(base_path, "textures", chr_cache.get_character_id(), "Unpack")
|
|
else:
|
|
unpack_folder = os.path.join(base_path, "textures", "Unpack")
|
|
|
|
if unpack_folder:
|
|
utils.log_info(f"Unpacking embedded textures to: {unpack_folder}")
|
|
if not os.path.exists(unpack_folder):
|
|
os.makedirs(unpack_folder, exist_ok=True)
|
|
|
|
obj : bpy.types.Object
|
|
for obj in objects:
|
|
obj_json = jsonutils.get_object_json(chr_json, obj)
|
|
|
|
if obj_json and utils.object_exists_is_mesh(obj):
|
|
|
|
for slot in obj.material_slots:
|
|
mat = slot.material
|
|
if mat is None: continue
|
|
mat_json = jsonutils.get_material_json(obj_json, mat)
|
|
mat_cache = chr_cache.get_material_cache(mat)
|
|
if mat_cache and mat_json:
|
|
for tex_mapping in mat_cache.texture_mappings:
|
|
image : bpy.types.Image = tex_mapping.image
|
|
|
|
if image:
|
|
try_unpack_image(image, unpack_folder)
|
|
abs_image_path = bpy.path.abspath(image.filepath)
|
|
|
|
# fix the texture json data path:
|
|
try:
|
|
tex_type = tex_mapping.texture_type
|
|
tex_id = params.get_texture_json_id(tex_type)
|
|
if tex_id in mat_json["Textures"]:
|
|
tex_info = mat_json["Textures"][tex_id]
|
|
|
|
# the fbx importer will assign the diffuse alpha to the opacity channel, even if
|
|
# there is an opacity texture present.
|
|
# this means it will incorrectly set the opacity with the diffuse
|
|
# though this will be corrected later by the texture write back,
|
|
# if no write back this will be wrong, so remove the opacity Json data
|
|
if not prefs.export_texture_changes:
|
|
dir, name = os.path.split(abs_image_path)
|
|
if "_Diffuse" in name and tex_type == "ALPHA":
|
|
utils.log_info(f"Diffuse connected to Alpha, removing Opacity data from Json.")
|
|
del mat_json["Textures"][tex_id]
|
|
tex_info = None
|
|
|
|
if tex_info:
|
|
tex_info["Texture Path"] = abs_image_path
|
|
utils.log_info(f"Updating embedded image Json data: {abs_image_path}")
|
|
except:
|
|
utils.log_warn(f"Unable to update embedded image Json: {image.name}")
|
|
|
|
|
|
def get_export_objects(chr_cache, include_selected = True, only_objects=None):
|
|
"""Fetch all the objects in the character (or try to)"""
|
|
collider_collection = rigidbody.get_rigidbody_collider_collection()
|
|
objects = []
|
|
selected = bpy.context.selected_objects.copy()
|
|
|
|
if chr_cache:
|
|
arm = chr_cache.get_armature()
|
|
if arm:
|
|
utils.unhide(arm)
|
|
if arm not in objects:
|
|
objects.append(arm)
|
|
chr_objects = chr_cache.get_cache_objects()
|
|
for obj in chr_objects:
|
|
obj_cache = chr_cache.get_object_cache(obj)
|
|
if obj_cache.is_mesh() and not obj_cache.disabled:
|
|
if obj.parent == arm:
|
|
utils.unhide(obj)
|
|
if obj not in objects:
|
|
objects.append(obj)
|
|
|
|
child_objects = utils.get_child_objects(arm)
|
|
for obj in child_objects:
|
|
if utils.object_exists_is_mesh(obj):
|
|
# exclude rigid body colliders (parented to armature)
|
|
if collider_collection and obj.name in collider_collection.objects:
|
|
utils.log_info(f" Excluding Rigidbody Collider Object: {obj.name}")
|
|
continue
|
|
# exclude collider proxies
|
|
if chr_cache.is_collision_object(obj):
|
|
utils.log_info(f" Excluding Collider Proxy Object: {obj.name}")
|
|
continue
|
|
# exclude sculpt objects
|
|
if chr_cache.is_sculpt_object(obj):
|
|
utils.log_info(f" Excluding Sculpt Object: {obj.name}")
|
|
continue
|
|
# add child mesh objects
|
|
if obj not in objects:
|
|
utils.log_info(f" Including Child Mesh Object: {obj.name}")
|
|
objects.append(obj)
|
|
elif utils.object_exists_is_empty(obj):
|
|
utils.log_info(f" Including Child Empty Transform: {obj.name}")
|
|
objects.append(obj)
|
|
else:
|
|
arm = utils.get_armature_from_objects(objects)
|
|
if arm:
|
|
utils.unhide(arm)
|
|
if arm not in objects:
|
|
objects.append(arm)
|
|
utils.log_info(f"Character Armature: {arm.name}")
|
|
child_objects = utils.get_child_objects(arm)
|
|
for obj in child_objects:
|
|
if utils.object_exists_is_mesh(obj):
|
|
if obj not in objects:
|
|
# exclude rigid body colliders (parented to armature)
|
|
if collider_collection and obj.name in collider_collection.objects:
|
|
utils.log_info(f" Excluding Rigidbody Collider Object: {obj.name}")
|
|
continue
|
|
utils.log_info(f" Including Child Object: {obj.name}")
|
|
objects.append(obj)
|
|
elif utils.object_exists_is_empty(obj):
|
|
utils.log_info(f" Including Child Empty Transform: {obj.name}")
|
|
objects.append(obj)
|
|
|
|
# include selected objects last
|
|
if include_selected:
|
|
for obj in selected:
|
|
if obj not in objects:
|
|
objects.append(obj)
|
|
|
|
# make sure all export objects are valid
|
|
clean_objects = [ obj for obj in objects
|
|
if utils.object_exists(obj) and
|
|
(obj == arm or obj.type == "MESH" or obj.type == "EMPTY") ]
|
|
|
|
if only_objects:
|
|
to_remove = [ o for o in clean_objects if o not in only_objects ]
|
|
for o in to_remove:
|
|
clean_objects.remove(o)
|
|
|
|
for obj in clean_objects:
|
|
utils.log_info(f"Export Object: {obj.name} ({obj.type})")
|
|
|
|
return clean_objects
|
|
|
|
|
|
def set_T_pose(arm, chr_json):
|
|
utils.log_info("Putting character in T-Pose.")
|
|
|
|
if utils.edit_mode_to(arm):
|
|
left_arm_edit = bones.get_edit_bone(arm, ["CC_Base_L_Upperarm", "L_Upperarm", "upperarm_l"])
|
|
right_arm_edit = bones.get_edit_bone(arm, ["CC_Base_R_Upperarm", "R_Upperarm", "upperarm_r"])
|
|
# test for A-pose
|
|
world_x = mathutils.Vector((1, 0, 0))
|
|
a_pose = False
|
|
if left_arm_edit and world_x.dot(left_arm_edit.y_axis) < 0.9:
|
|
a_pose = True
|
|
if right_arm_edit and world_x.dot(right_arm_edit.y_axis) > -0.9:
|
|
a_pose = True
|
|
# Set T-pose
|
|
if a_pose:
|
|
bones.clear_pose(arm)
|
|
left_arm_pose = bones.get_pose_bone(arm, ["CC_Base_L_Upperarm", "L_Upperarm", "upperarm_l"])
|
|
right_arm_pose = bones.get_pose_bone(arm, ["CC_Base_R_Upperarm", "R_Upperarm", "upperarm_r"])
|
|
angle = 30.0 * math.pi / 180.0
|
|
if left_arm_pose:
|
|
left_arm_pose.rotation_mode = "XYZ"
|
|
left_arm_pose.rotation_euler = [0,0,angle]
|
|
left_arm_pose.rotation_mode = "QUATERNION"
|
|
if right_arm_pose:
|
|
right_arm_pose.rotation_mode = "XYZ"
|
|
right_arm_pose.rotation_euler = [0,0,-angle]
|
|
right_arm_pose.rotation_mode = "QUATERNION"
|
|
if chr_json:
|
|
chr_json["Bind_Pose"] = "APose"
|
|
return True
|
|
if chr_json:
|
|
chr_json["Bind_Pose"] = "TPose"
|
|
return False
|
|
|
|
|
|
def clear_animation_data(obj: bpy.types.Object):
|
|
if obj.type == "ARMATURE" or obj.type == "MESH":
|
|
# remove action
|
|
utils.safe_set_action(obj, None)
|
|
# remove strips
|
|
# this removes drivers too...
|
|
#obj.animation_data_clear()
|
|
ad = obj.animation_data
|
|
if ad:
|
|
while ad.nla_tracks:
|
|
ad.nla_tracks.remove(ad.nla_tracks[0])
|
|
if obj.type == "MESH":
|
|
# remove shape key action
|
|
utils.safe_set_action(obj.data.shape_keys, None)
|
|
# remove shape key strips
|
|
if obj.data.shape_keys and obj.data.shape_keys.animation_data:
|
|
obj.data.shape_keys.animation_data_clear()
|
|
ad = obj.data.shape_keys.animation_data
|
|
if ad:
|
|
while ad.nla_tracks:
|
|
ad.nla_tracks.remove(ad.nla_tracks[0])
|
|
|
|
|
|
def create_T_pose_action(arm, objects, export_strips):
|
|
|
|
# remove all actions from objects
|
|
for obj in objects:
|
|
clear_animation_data(obj)
|
|
|
|
bpy.context.scene.frame_start = 1
|
|
bpy.context.scene.frame_end = 2
|
|
|
|
# create T-Pose action
|
|
if "0_T-Pose" not in bpy.data.actions and utils.pose_mode_to(arm):
|
|
action : bpy.types.Action = bpy.data.actions.new("0_T-Pose")
|
|
utils.safe_set_action(arm, action)
|
|
|
|
bones.select_all_bones(arm, select=True, clear_active=True)
|
|
|
|
# go to first frame
|
|
bpy.data.scenes["Scene"].frame_current = 1
|
|
|
|
# apply first keyframe
|
|
bpy.ops.anim.keyframe_insert_menu(type='BUILTIN_KSI_LocRot')
|
|
|
|
# make a second keyframe
|
|
bpy.data.scenes["Scene"].frame_current = 2
|
|
bpy.ops.anim.keyframe_insert_menu(type='BUILTIN_KSI_LocRot')
|
|
|
|
# or re-use T-Pose action
|
|
else:
|
|
action = bpy.data.actions["0_T-Pose"]
|
|
utils.safe_set_action(arm, action)
|
|
|
|
# push T-Pose to NLA if exporting strips
|
|
if export_strips:
|
|
utils.log_info(f"Adding {action.name} to NLA strips")
|
|
if obj.animation_data is None:
|
|
obj.animation_data_create()
|
|
if len(obj.animation_data.nla_tracks) == 0:
|
|
track = arm.animation_data.nla_tracks.new()
|
|
else:
|
|
track = arm.animation_data.nla_tracks[0]
|
|
track.strips.new(action.name, int(action.frame_range[0]), action)
|
|
utils.safe_set_action(arm, None)
|
|
|
|
|
|
def set_character_generation(json_data, chr_cache, name):
|
|
if chr_cache and chr_cache.is_standard():
|
|
set_standard_generation(json_data, name, chr_cache.generation)
|
|
else:
|
|
set_non_standard_generation(json_data, name, chr_cache.non_standard_type, chr_cache.generation)
|
|
|
|
|
|
def set_non_standard_generation(json_data, character_id, character_type, generation):
|
|
RL_HUMANOID_GENERATIONS = [
|
|
"ActorCore", "ActorBuild", "ActorScan", "AccuRig", "GameBase"
|
|
]
|
|
if character_type == "HUMANOID":
|
|
if generation not in RL_HUMANOID_GENERATIONS:
|
|
generation = "Humanoid"
|
|
elif character_type == "CREATURE":
|
|
generation = "Creature"
|
|
elif character_type == "PROP":
|
|
generation = "Prop"
|
|
else:
|
|
generation = "Unknown"
|
|
|
|
utils.log_info(f"Generation: {generation}")
|
|
jsonutils.set_character_generation_json(json_data, character_id, generation)
|
|
|
|
|
|
def set_standard_generation(json_data, character_id, generation):
|
|
# currently is doesn't really matter what the standard generation string is
|
|
# generation in the CC4 plugin is only used to detect non-standard characters.
|
|
jsonutils.set_character_generation_json(json_data, character_id, generation)
|
|
|
|
|
|
def prep_non_standard_export(context, objects, dir, name, character_type):
|
|
global UNPACK_INDEX
|
|
bake.init_bake()
|
|
UNPACK_INDEX = 5001
|
|
|
|
# prefer to bake and unpack textures next to blend file, otherwise at the destination.
|
|
blend_path = utils.local_path()
|
|
if blend_path:
|
|
dir = blend_path
|
|
utils.log_info(f"Texture Root Dir: {dir}")
|
|
|
|
json_data = jsonutils.generate_character_base_json_data(name)
|
|
|
|
set_non_standard_generation(json_data, name, character_type, "Unknown")
|
|
|
|
done = {}
|
|
objects_json = json_data[name]["Object"][name]["Meshes"]
|
|
|
|
for obj in objects:
|
|
|
|
if obj.type == "MESH" and obj not in done.keys():
|
|
|
|
utils.log_info(f"Adding Object Json: {obj.name}")
|
|
export_name = utils.safe_export_name(obj.name)
|
|
|
|
if export_name != obj.name:
|
|
utils.log_info(f"Updating Object name: {obj.name} to {export_name}")
|
|
obj.name = export_name
|
|
|
|
mesh_json = copy.deepcopy(params.JSON_MESH_DATA)
|
|
done[obj] = mesh_json
|
|
objects_json[obj.name] = mesh_json
|
|
|
|
for slot in obj.material_slots:
|
|
|
|
mat = slot.material
|
|
if mat is None: continue
|
|
|
|
if mat not in done.keys():
|
|
|
|
utils.log_info(f"Adding Material Json: {mat.name}")
|
|
|
|
export_name = utils.safe_export_name(mat.name, is_material=True)
|
|
if export_name != mat.name:
|
|
utils.log_info(f"Updating Material name: {mat.name} to {export_name}")
|
|
mat.name = export_name
|
|
|
|
mat_json = copy.deepcopy(params.JSON_PBR_MATERIAL)
|
|
done[mat] = mat_json
|
|
|
|
mesh_json["Materials"][mat.name] = mat_json
|
|
|
|
write_pbr_material_to_json(context, mat, mat_json, dir, name, True)
|
|
|
|
else:
|
|
|
|
mesh_json["Materials"][mat.name] = done[mat]
|
|
|
|
# select all the export objects
|
|
utils.try_select_objects(objects, True)
|
|
|
|
return json_data
|
|
|
|
#[ socket_path, node_label_match, source_type, tex_channel, strength_socket_path ]
|
|
BSDF_TEXTURES = [
|
|
["Base Color", "", "BSDF", "Base Color", ""],
|
|
["Metallic", "", "BSDF", "Metallic", ""],
|
|
["Specular", "", "BSDF", "Specular", ""],
|
|
["Roughness", "", "BSDF", "Roughness", ""],
|
|
["Emission", "", "BSDF", "Glow", "Emission Strength"],
|
|
["Alpha", "", "BSDF", "Opacity", ""],
|
|
["Normal:Color", "", "BSDF", "Normal", "Normal:Strength"],
|
|
["Normal:Normal:Color", "", "BSDF", "Normal", ["Normal:Normal:Strength", "Normal:Strength"]],
|
|
["Normal:Height", "", "BSDF", "Bump", ["Normal:Distance", "Normal:Strength"]],
|
|
["Base Color:Color2", "ao|occlusion", "BSDF", "AO", "Base Color:Fac"],
|
|
["Occlusion", "", "GLTF", "AO", "Occlusion:Fac"],
|
|
]
|
|
|
|
BSDF_TEXTURE_KEYWORDS = {
|
|
"Base Color": ["basecolor", "diffuse", "albedo", "base color", "base colour", "basecolour", ".d$", "_d$"],
|
|
"Metallic": ["metallic", "metal", "metalness", ".m$", "_m$"],
|
|
"Specular": ["specular", "spec", "specmap", ".s$", "_s$"],
|
|
"Roughness": ["roughness", "rough", ".r$", "_r$"],
|
|
"Glow": ["emissive", "emission", "glow", "emit", ".e$", "_e$", ".g$", "_g$"],
|
|
"Opacity": ["alpha", "opacity", ".a$", "_a$"],
|
|
"Normal": ["normal", "nrm", ".n$", "_n$"],
|
|
"Bump": ["bump", "height", ".b$", "_b$"],
|
|
"AO": ["occlusion", "lightmap", "intensity", ".ao$", "_ao$"],
|
|
}
|
|
|
|
def write_pbr_material_to_json(context, mat, mat_json, path, name, bake_values):
|
|
if not mat.node_tree or not mat.node_tree.nodes:
|
|
return
|
|
|
|
unpack_path = os.path.join(path, "textures", name, "Unpack")
|
|
bake_path = os.path.join(path, "textures", name, "Baked")
|
|
bsdf_node = nodeutils.get_bsdf_node(mat)
|
|
gltf_node = nodeutils.find_node_group_by_keywords(mat.node_tree.nodes, "glTF Settings", "glTF Material Output")
|
|
|
|
if bsdf_node:
|
|
try:
|
|
base_color_socket = nodeutils.input_socket(bsdf_node, "Base Color")
|
|
clearcoat_socket = nodeutils.input_socket(bsdf_node, "Clearcoat")
|
|
roughness_socket = nodeutils.input_socket(bsdf_node, "Roughness")
|
|
metallic_socket = nodeutils.input_socket(bsdf_node, "Metallic")
|
|
specular_socket = nodeutils.input_socket(bsdf_node, "Specular")
|
|
alpha_socket = nodeutils.input_socket(bsdf_node, "Alpha")
|
|
emission_socket = nodeutils.input_socket(bsdf_node, "Emission")
|
|
emission_strength_socket = nodeutils.input_socket(bsdf_node, "Emission Strength")
|
|
|
|
roughness_value = roughness_socket.default_value
|
|
metallic_value = metallic_socket.default_value
|
|
bake_roughness = False
|
|
bake_metallic = False
|
|
specular_value = specular_socket.default_value
|
|
diffuse_color = (1,1,1,1)
|
|
alpha_value = 1.0
|
|
if not base_color_socket.is_linked:
|
|
diffuse_color = base_color_socket.default_value
|
|
if not alpha_socket.is_linked:
|
|
alpha_value = alpha_socket.default_value
|
|
mat_json["Diffuse Color"] = jsonutils.convert_from_color(diffuse_color)
|
|
mat_json["Specular Color"] = jsonutils.convert_from_color(
|
|
utils.linear_to_srgb((specular_value, specular_value, specular_value, 1.0))
|
|
)
|
|
mat_json["Opacity"] = alpha_value
|
|
if bake_values:
|
|
if roughness_value != 0.5:
|
|
bake_roughness = True
|
|
if metallic_value > 0:
|
|
bake_metallic = True
|
|
elif not bake_values:
|
|
mat_json["Roughness_Value"] = roughness_value
|
|
mat_json["Metallic_Value"] = metallic_value
|
|
except:
|
|
utils.log_warn("Unable to set BSDF parameters!")
|
|
|
|
socket_mapping = {}
|
|
for socket_trace, match, node_type, tex_id, strength_trace in BSDF_TEXTURES:
|
|
if node_type == "BSDF":
|
|
n = bsdf_node
|
|
elif node_type == "GLTF":
|
|
n = gltf_node
|
|
else:
|
|
n = None
|
|
if n:
|
|
linked_node, linked_socket = nodeutils.trace_input_sockets(n, socket_trace)
|
|
strength = 1.0
|
|
if type(strength_trace) is list:
|
|
for st in strength_trace:
|
|
strength *= float(nodeutils.trace_input_value(n, st, 1.0))
|
|
else:
|
|
strength = float(nodeutils.trace_input_value(n, strength_trace, 1.0))
|
|
if tex_id == "Bump":
|
|
strength = min(200, max(0, strength * 10000.0))
|
|
elif tex_id == "Normal":
|
|
strength = min(200, max(0, strength * 100))
|
|
else:
|
|
strength = min(100, max(0, strength * 100))
|
|
if linked_node and linked_socket:
|
|
if match:
|
|
if re.match(match, linked_node.label) or re.match(match, linked_node.name):
|
|
socket_mapping[tex_id] = [linked_node, linked_socket, False, strength]
|
|
else:
|
|
socket_mapping[tex_id] = [linked_node, linked_socket, False, strength]
|
|
else:
|
|
if tex_id == "Roughness" and bake_roughness:
|
|
socket_mapping[tex_id] = [bsdf_node, "Roughness", True, strength]
|
|
elif tex_id == "Metallic" and bake_metallic:
|
|
socket_mapping[tex_id] = [bsdf_node, "Metallic", True, strength]
|
|
|
|
|
|
write_or_bake_tex_data_to_json(context, socket_mapping, mat, mat_json, bsdf_node, path, bake_path, unpack_path)
|
|
|
|
else:
|
|
# if there is no BSDF shader node, try to match textures by name (both image node name and image name)
|
|
socket_mapping = {}
|
|
for node in mat.node_tree.nodes:
|
|
if node.type == "TEX_IMAGE" and node.image:
|
|
for tex_id in BSDF_TEXTURE_KEYWORDS:
|
|
for key in BSDF_TEXTURE_KEYWORDS[tex_id]:
|
|
if re.match(key, node.image.name.lower()) or re.match(key, node.label.lower()) or re.match(key, node.name.lower()):
|
|
socket_mapping[tex_id] = [node, "Color", False, 100.0]
|
|
|
|
write_or_bake_tex_data_to_json(context, socket_mapping, mat, mat_json, None, path, bake_path, unpack_path)
|
|
|
|
return
|
|
|
|
|
|
def write_or_bake_tex_data_to_json(context, socket_mapping, mat, mat_json, bsdf_node, path, bake_path, unpack_path):
|
|
|
|
combine_normals = False
|
|
if bsdf_node and "Normal" in socket_mapping and "Bump" in socket_mapping:
|
|
combine_normals = True
|
|
|
|
for tex_id in socket_mapping:
|
|
|
|
# don't add bump maps if combining normals
|
|
if combine_normals and tex_id == "Bump":
|
|
continue
|
|
|
|
# cc3/4 only supports one normal or bump, so favor the normal map
|
|
if tex_id == "Bump" and "Normal" in socket_mapping:
|
|
continue
|
|
|
|
node, socket_name, bake_value, strength = socket_mapping[tex_id]
|
|
socket = nodeutils.input_socket(node, socket_name)
|
|
utils.log_info(f"Adding Texture Channel: {tex_id} strength - {strength}")
|
|
|
|
tex_node = None
|
|
image = None
|
|
if node.type == "TEX_IMAGE":
|
|
tex_node = node
|
|
image = node.image
|
|
try_unpack_image(image, unpack_path, True)
|
|
|
|
else:
|
|
if tex_id == "Normal" and combine_normals:
|
|
image = bake.bake_bsdf_normal(context, bsdf_node, mat, tex_id, bake_path)
|
|
else:
|
|
if bake_value:
|
|
image = bake.pack_value_image(socket.default_value, mat, tex_id, bake_path)
|
|
else:
|
|
image = bake.bake_node_socket_output(context, node, socket, mat, tex_id, bake_path)
|
|
|
|
tex_info = copy.deepcopy(params.JSON_PBR_TEX_INFO)
|
|
if image.filepath:
|
|
abs_image_path = bpy.path.abspath(image.filepath)
|
|
if abs_image_path:
|
|
utils.log_info(f"{mat.name}/{tex_id}: Using new texture path: {abs_image_path}")
|
|
tex_info["Texture Path"] = abs_image_path
|
|
if tex_node:
|
|
location, rotation, scale = nodeutils.get_image_node_mapping(tex_node)
|
|
tex_info["Tiling"] = [scale[0], scale[1]]
|
|
tex_info["Offset"] = [location[0], location[1]]
|
|
tex_info["Strength"] = strength
|
|
mat_json["Textures"][tex_id] = tex_info
|
|
|
|
|
|
def update_facial_profile_json(chr_cache, all_objects, json_data, chr_name):
|
|
|
|
# for standard characters, ignore clothing, accessories and hair objects
|
|
objects = []
|
|
if chr_cache:
|
|
for obj in all_objects:
|
|
if utils.object_exists_is_mesh(obj):
|
|
obj_cache = chr_cache.get_object_cache(obj)
|
|
if obj_cache and (obj_cache.object_type == "DEFAULT" or
|
|
obj_cache.object_type == "HAIR"):
|
|
continue
|
|
objects.append(obj)
|
|
|
|
# non-standard characters, consider everything
|
|
else:
|
|
for obj in all_objects:
|
|
if utils.object_exists_is_mesh(obj):
|
|
objects.append(obj)
|
|
|
|
categories_json = jsonutils.get_facial_profile_categories_json(json_data, chr_name)
|
|
new_categories = {}
|
|
done_shapes = []
|
|
|
|
# cull existing facial expressions
|
|
if categories_json:
|
|
for category in categories_json.keys():
|
|
new_shapes = []
|
|
new_categories[category] = new_shapes
|
|
shape_names = categories_json[category]
|
|
for shape_name in shape_names:
|
|
if shape_name not in done_shapes:
|
|
if meshutils.objects_have_shape_key(objects, shape_name):
|
|
new_shapes.append(shape_name)
|
|
done_shapes.append(shape_name)
|
|
|
|
# add visemes
|
|
visemes = []
|
|
VISEME_NAMES = meshutils.get_viseme_profile(objects)
|
|
for viseme_name in VISEME_NAMES:
|
|
if viseme_name not in done_shapes:
|
|
if meshutils.objects_have_shape_key(objects, viseme_name):
|
|
visemes.append(viseme_name)
|
|
done_shapes.append(viseme_name)
|
|
if visemes:
|
|
new_categories["Visemes"] = visemes
|
|
|
|
# all remaining shapes go into custom expressions
|
|
custom = []
|
|
for obj in objects:
|
|
if obj.type == "MESH" and obj.data.shape_keys:
|
|
i = 0
|
|
for key_block in obj.data.shape_keys.key_blocks:
|
|
shape_name = key_block.name
|
|
# ignore tearline and eye occlusion adjustment shape keys (these are not facial expressions)
|
|
if chr_cache and (shape_name.startswith("TL ") or shape_name.startswith("EO ")):
|
|
continue
|
|
if i > 0 and shape_name not in done_shapes:
|
|
custom.append(shape_name)
|
|
done_shapes.append(shape_name)
|
|
i += 1
|
|
if custom:
|
|
new_categories["Custom"] = custom
|
|
|
|
# apply changes
|
|
jsonutils.set_facial_profile_categories_json(json_data, chr_name, new_categories)
|
|
|
|
|
|
def export_copy_asset_file(chr_cache, dir, name, ext, old_path=None):
|
|
try:
|
|
try_paths = [
|
|
old_path,
|
|
os.path.join(chr_cache.get_import_dir(), chr_cache.get_character_id() + ext),
|
|
utils.local_path(chr_cache.get_character_id() + ext),
|
|
]
|
|
for old_path in try_paths:
|
|
if old_path and os.path.exists(old_path):
|
|
new_path = os.path.join(dir, name + ext)
|
|
if not utils.is_same_path(new_path, old_path):
|
|
utils.log_info(f"Copying {ext} file: {old_path} to: {new_path}")
|
|
shutil.copyfile(old_path, new_path)
|
|
return os.path.relpath(new_path, dir)
|
|
except Exception as e:
|
|
utils.log_error(f"Unable to copy {ext} file: {old_path} to: {new_path}", e)
|
|
return None
|
|
|
|
|
|
def is_arp_installed():
|
|
try:
|
|
bl_options = bpy.ops.arp.arp_export_fbx_panel.bl_options
|
|
if bl_options is not None:
|
|
utils.log_info("ARP is installed.")
|
|
return True
|
|
else:
|
|
utils.log_info("ARP is NOT installed.")
|
|
return False
|
|
except:
|
|
utils.log_info("ARP is NOT installed.")
|
|
return False
|
|
|
|
|
|
def is_arp_rig(rig):
|
|
if utils.object_exists_is_armature(rig):
|
|
if "c_pos" in rig.data.bones and "c_traj" in rig.data.bones and "c_root.x" in rig.data.bones:
|
|
utils.log_info("Rig is ARP")
|
|
return True
|
|
utils.log_info("Rig is NOT ARP")
|
|
return False
|
|
|
|
|
|
def export_arp(file_path, arm, objects):
|
|
utils.log_info("Attempting to export ARP rig...")
|
|
try:
|
|
bpy.data.scenes["Scene"].arp_engine_type = "UNITY"
|
|
bpy.data.scenes["Scene"].arp_export_rig_type = "HUMANOID"
|
|
bpy.data.scenes["Scene"].arp_bake_anim = False
|
|
bpy.data.scenes["Scene"].arp_ge_sel_only = True
|
|
bpy.data.scenes["Scene"].arp_ge_sel_bones_only = False
|
|
bpy.data.scenes["Scene"].arp_keep_bend_bones = False
|
|
bpy.data.scenes["Scene"].arp_export_twist = True
|
|
bpy.data.scenes["Scene"].arp_export_noparent = False
|
|
bpy.data.scenes["Scene"].arp_export_renaming = False
|
|
bpy.data.scenes["Scene"].arp_use_tspace = False
|
|
bpy.data.scenes["Scene"].arp_fix_fbx_rot = False
|
|
bpy.data.scenes["Scene"].arp_fix_fbx_matrix = True
|
|
bpy.data.scenes["Scene"].arp_init_fbx_rot = False
|
|
bpy.data.scenes["Scene"].arp_bone_axis_primary_export = "Y"
|
|
bpy.data.scenes["Scene"].arp_bone_axis_secondary_export = "X"
|
|
bpy.data.scenes["Scene"].arp_export_rig_name = "root"
|
|
bpy.data.scenes["Scene"].arp_export_tex = False
|
|
bpy.data.scenes["Scene"].arp_units_x100 = True
|
|
bpy.data.scenes["Scene"].arp_global_scale = 1.0
|
|
# select all objects
|
|
utils.log_info(f"Selecting all character objects.")
|
|
utils.try_select_objects(objects, True)
|
|
# make sure the armature is active
|
|
utils.log_info(f"Setting Armature: {arm.name} active")
|
|
utils.set_active_object(arm)
|
|
# invoke
|
|
utils.log_info("Invoking ARP Export:")
|
|
bpy.ops.arp.arp_export_fbx_panel(filepath=file_path, check_existing = False)
|
|
return True
|
|
except Exception as e:
|
|
utils.log_info(f"ARP export failed: {str(e)}")
|
|
return False
|
|
|
|
|
|
def obj_export(file_path, use_selection=False, use_animation=False, global_scale=100,
|
|
use_vertex_colors=False, use_vertex_groups=False, apply_modifiers=True,
|
|
keep_vertex_order=False, use_materials=False):
|
|
if utils.B330():
|
|
bpy.ops.wm.obj_export(filepath=file_path,
|
|
global_scale=global_scale,
|
|
export_selected_objects=use_selection,
|
|
export_animation=use_animation,
|
|
export_materials=use_materials,
|
|
export_colors=use_vertex_colors,
|
|
export_vertex_groups=use_vertex_groups,
|
|
apply_modifiers=apply_modifiers)
|
|
else:
|
|
bpy.ops.export_scene.obj(filepath=file_path,
|
|
global_scale=global_scale,
|
|
use_selection=use_selection,
|
|
use_materials=use_materials,
|
|
use_animation=use_animation,
|
|
use_vertex_groups=use_vertex_groups,
|
|
use_mesh_modifiers=apply_modifiers,
|
|
keep_vertex_order=keep_vertex_order)
|
|
|
|
|
|
def export_standard(self, context, chr_cache, file_path, include_selected):
|
|
"""Exports standard character (not rigified, not generic) to CC3/4 with json data,
|
|
texture paths are relative to source character, as an .fbx file.
|
|
"""
|
|
|
|
props = vars.props()
|
|
prefs = vars.prefs()
|
|
|
|
utils.start_timer()
|
|
|
|
utils.log_info("")
|
|
utils.log_info("Exporting Character Model to CC:")
|
|
utils.log_info("--------------------------------")
|
|
|
|
utils.object_mode()
|
|
|
|
chr_cache.check_paths()
|
|
|
|
export_anim = False
|
|
dir, file = os.path.split(file_path)
|
|
name, ext = os.path.splitext(file)
|
|
|
|
# store mode state
|
|
mode_selection_state = utils.store_mode_selection_state()
|
|
|
|
utils.log_info("Export to: " + file_path)
|
|
utils.log_info("Exporting as: " + ext)
|
|
|
|
if utils.is_file_ext(ext, "FBX"):
|
|
|
|
json_data = chr_cache.get_json_data()
|
|
if not json_data:
|
|
json_data = jsonutils.generate_character_base_json_data(name)
|
|
set_character_generation(json_data, chr_cache, name)
|
|
|
|
# export objects
|
|
objects = get_export_objects(chr_cache, include_selected)
|
|
arm = get_export_armature(chr_cache, objects)
|
|
|
|
# store states and settings
|
|
armature_settings = bones.store_armature_settings(arm, include_pose=True)
|
|
object_state = utils.store_object_state(objects)
|
|
|
|
# restore quaternion rotation modes
|
|
rigutils.reset_rotation_modes(arm)
|
|
|
|
utils.log_info("Preparing character for export:")
|
|
utils.log_indent()
|
|
|
|
# avatar's should be exported back to CC4 in rest pose.
|
|
# props should be exported back with animation.
|
|
use_rest_pose = chr_cache.is_avatar()
|
|
remove_modifiers_for_export(chr_cache, objects, use_rest_pose)
|
|
|
|
revert_duplicates = prefs.export_legacy_revert_material_names
|
|
prep_export(context, chr_cache, name, objects, json_data, chr_cache.get_import_dir(),
|
|
dir, self.include_textures, revert_duplicates, True, False, True)
|
|
|
|
# attempt any custom exports (ARP)
|
|
custom_export = False
|
|
if is_arp_installed() and is_arp_rig(arm):
|
|
custom_export = export_arp(file_path, arm, objects)
|
|
|
|
# double check custom export
|
|
if not os.path.exists(file_path):
|
|
custom_export = False
|
|
|
|
# proceed with normal export
|
|
if not custom_export:
|
|
bpy.ops.export_scene.fbx(filepath=file_path,
|
|
use_selection = True,
|
|
bake_anim = export_anim,
|
|
bake_anim_simplify_factor=self.animation_simplify,
|
|
add_leaf_bones = False,
|
|
mesh_smooth_type = ("FACE" if self.export_face_smoothing else "OFF"),
|
|
use_mesh_modifiers = False)
|
|
|
|
utils.log_recess()
|
|
utils.log_info("")
|
|
utils.log_info("Copying Fbx Key.")
|
|
|
|
export_copy_asset_file(chr_cache, dir, name, ".fbxkey", chr_cache.get_import_key_file())
|
|
hik_path = export_copy_asset_file(chr_cache, dir, name, ".3dxProfile")
|
|
fac_path = export_copy_asset_file(chr_cache, dir, name, ".ccFacialProfile")
|
|
if hik_path:
|
|
jsonutils.set_json(json_data, "HIK/Profile_Path", hik_path)
|
|
if fac_path:
|
|
jsonutils.set_json(json_data, "Facial_Profile/Profile_Path", fac_path)
|
|
|
|
utils.log_info("Writing Json Data.")
|
|
|
|
# write HIK profile for VRM
|
|
if chr_cache and chr_cache.is_import_type("VRM"):
|
|
hik_path = os.path.join(dir, name + ".3dxProfile")
|
|
if hik.generate_hik_profile(arm, name, hik_path, hik.VRM_HIK_PROFILE_TEMPLATE):
|
|
if json_data:
|
|
json_data[name]["HIK"] = {}
|
|
json_data[name]["HIK"]["Profile_Path"] = os.path.relpath(hik_path, dir)
|
|
|
|
if json_data:
|
|
update_facial_profile_json(chr_cache, objects, json_data, name)
|
|
new_json_path = os.path.join(dir, name + ".json")
|
|
jsonutils.write_json(json_data, new_json_path)
|
|
|
|
# restore states and settings
|
|
utils.restore_object_state(object_state)
|
|
bones.restore_armature_settings(arm, armature_settings, include_pose=True)
|
|
|
|
restore_modifiers(chr_cache, objects)
|
|
|
|
else:
|
|
|
|
# don't bring anything else with an obj morph export
|
|
bpy.ops.object.select_all(action='DESELECT')
|
|
|
|
# select all the imported objects (should be just one)
|
|
for p in chr_cache.object_cache:
|
|
if p.object is not None and p.object.type == "MESH" and not p.disabled:
|
|
utils.unhide(p.object)
|
|
p.object.select_set(True)
|
|
|
|
obj_export(file_path, use_selection=True,
|
|
global_scale=100,
|
|
use_materials=False,
|
|
keep_vertex_order=True,
|
|
use_vertex_colors=True,
|
|
use_vertex_groups=True,
|
|
apply_modifiers=True)
|
|
|
|
#export_copy_obj_key(chr_cache, dir, name)
|
|
export_copy_asset_file(chr_cache, dir, name, ".ObjKey", chr_cache.get_import_key_file())
|
|
|
|
# restore mode state
|
|
utils.restore_mode_selection_state(mode_selection_state)
|
|
|
|
utils.log_recess()
|
|
utils.log_timer("Done Character Export.")
|
|
|
|
|
|
def export_non_standard(self, context, file_path, include_selected):
|
|
"""Exports non-standard character (unconverted and not rigified) to CC4 with json data and textures, as an .fbx file.
|
|
"""
|
|
|
|
props = vars.props()
|
|
prefs = vars.prefs()
|
|
|
|
utils.start_timer()
|
|
|
|
utils.log_info("")
|
|
utils.log_info("Exporting Non-Standard Model to CC:")
|
|
utils.log_info("-----------------------------------")
|
|
|
|
utils.object_mode()
|
|
|
|
export_anim = False
|
|
dir, file = os.path.split(file_path)
|
|
name, ext = os.path.splitext(file)
|
|
|
|
# store mode state
|
|
mode_selection_state = utils.store_mode_selection_state()
|
|
|
|
# export objects
|
|
objects = get_export_objects(None, include_selected)
|
|
arm = get_export_armature(None, objects)
|
|
|
|
# store states and settings
|
|
armature_settings = bones.store_armature_settings(arm, include_pose=True)
|
|
object_state = utils.store_object_state(objects)
|
|
|
|
# restore quaternion rotation modes
|
|
rigutils.reset_rotation_modes(arm)
|
|
|
|
utils.log_info("Generating JSON data for export:")
|
|
utils.log_indent()
|
|
json_data = prep_non_standard_export(context, objects, dir, name, prefs.export_non_standard_mode)
|
|
|
|
utils.log_recess()
|
|
utils.log_info("Preparing character for export:")
|
|
utils.log_indent()
|
|
|
|
remove_modifiers_for_export(None, objects, True)
|
|
|
|
# attempt any custom exports (ARP)
|
|
arp_export = False
|
|
if is_arp_installed() and is_arp_rig(arm):
|
|
arp_export = export_arp(file_path, arm, objects)
|
|
|
|
# double check custom export
|
|
if not os.path.exists(file_path):
|
|
arp_export = False
|
|
|
|
# proceed with normal export
|
|
if not arp_export:
|
|
bpy.ops.export_scene.fbx(filepath=file_path,
|
|
use_selection = True,
|
|
bake_anim = export_anim,
|
|
bake_anim_simplify_factor=self.animation_simplify,
|
|
add_leaf_bones = False,
|
|
use_mesh_modifiers = True,
|
|
mesh_smooth_type = ("FACE" if self.export_face_smoothing else "OFF"),
|
|
use_armature_deform_only = True)
|
|
|
|
utils.log_recess()
|
|
utils.log_info("")
|
|
|
|
# write json data
|
|
if json_data:
|
|
utils.log_info("Writing Json Data.")
|
|
update_facial_profile_json(None, objects, json_data, name)
|
|
new_json_path = os.path.join(dir, name + ".json")
|
|
jsonutils.write_json(json_data, new_json_path)
|
|
|
|
# restore states and settings
|
|
utils.restore_object_state(object_state)
|
|
bones.restore_armature_settings(arm, armature_settings, include_pose=True)
|
|
|
|
# restore mode state
|
|
utils.restore_mode_selection_state(mode_selection_state)
|
|
|
|
utils.log_recess()
|
|
if arp_export:
|
|
utils.log_timer("Done Non-standard ARP Export.")
|
|
self.report({'INFO'}, "Export Non-standard (ARP) Done!")
|
|
else:
|
|
utils.log_timer("Done Non-standard Export.")
|
|
self.report({'INFO'}, "Export Non-standard Done!")
|
|
|
|
|
|
|
|
|
|
def export_to_unity(self, context, chr_cache, export_anim, file_path, include_selected):
|
|
"""Exports CC3/4 character (not rigified) for Unity with json data and textures,
|
|
as either a .blend file or .fbx file.
|
|
"""
|
|
|
|
props = vars.props()
|
|
prefs = vars.prefs()
|
|
|
|
utils.start_timer()
|
|
|
|
utils.log_info("")
|
|
utils.log_info("Exporting Character Model to UNITY:")
|
|
utils.log_info("-----------------------------------")
|
|
|
|
utils.object_mode()
|
|
|
|
chr_cache.check_paths()
|
|
|
|
dir, file = os.path.split(file_path)
|
|
name, ext = os.path.splitext(file)
|
|
|
|
# store mode state
|
|
mode_selection_state = utils.store_mode_selection_state()
|
|
|
|
utils.log_info("Export to: " + file_path)
|
|
utils.log_info("Exporting as: " + ext)
|
|
|
|
json_data = chr_cache.get_json_data()
|
|
if not json_data:
|
|
json_data = jsonutils.generate_character_base_json_data(name)
|
|
set_character_generation(json_data, chr_cache, name)
|
|
|
|
utils.log_info("Preparing character for export:")
|
|
utils.log_indent()
|
|
|
|
# remove any collision proxies
|
|
if utils.is_file_ext(ext, "BLEND"):
|
|
for obj in chr_cache.get_cache_objects():
|
|
proxy = chr_cache.get_collision_proxy(obj)
|
|
if proxy:
|
|
utils.delete_object_tree(proxy)
|
|
|
|
# export objects
|
|
objects = get_export_objects(chr_cache, include_selected)
|
|
arm = get_export_armature(chr_cache, objects)
|
|
|
|
# store states and settings
|
|
armature_settings = bones.store_armature_settings(arm, include_pose=True)
|
|
object_state = utils.store_object_state(objects)
|
|
|
|
# restore quaternion rotation modes
|
|
rigutils.reset_rotation_modes(arm)
|
|
|
|
export_actions = False
|
|
export_strips = True
|
|
|
|
# FBX exports only T-pose as a strip
|
|
if utils.is_file_ext(ext, "FBX"):
|
|
export_actions = False
|
|
export_strips = True
|
|
# blend file exports make the T-pose as an action
|
|
else:
|
|
export_actions = True
|
|
export_strips = False
|
|
|
|
as_blend_file = utils.is_file_ext(ext, "BLEND")
|
|
|
|
# remove custom material modifiers
|
|
remove_modifiers_for_export(chr_cache, objects, True)
|
|
|
|
prep_export(context, chr_cache, name, objects, json_data, chr_cache.get_import_dir(), dir, self.include_textures, False, False, as_blend_file, False)
|
|
|
|
# make the T-pose as an action
|
|
utils.safe_set_action(arm, None)
|
|
chr_json = None
|
|
if json_data:
|
|
try:
|
|
chr_json = json_data[name]["Object"][name]
|
|
except: pass
|
|
set_T_pose(arm, chr_json)
|
|
create_T_pose_action(arm, objects, export_strips)
|
|
|
|
if utils.is_file_ext(ext, "FBX"):
|
|
# export as fbx
|
|
bpy.ops.export_scene.fbx(filepath=file_path,
|
|
use_selection = True,
|
|
bake_anim = export_anim,
|
|
bake_anim_use_all_actions=export_actions,
|
|
bake_anim_use_nla_strips=export_strips,
|
|
bake_anim_simplify_factor=self.animation_simplify,
|
|
use_armature_deform_only=True,
|
|
add_leaf_bones = False,
|
|
mesh_smooth_type = ("FACE" if self.export_face_smoothing else "OFF"),
|
|
use_mesh_modifiers = True,
|
|
#apply_scale_options="FBX_SCALE_UNITS",
|
|
object_types={'EMPTY', 'MESH', 'ARMATURE'},
|
|
use_space_transform=True,
|
|
#armature_nodetype="ROOT",
|
|
)
|
|
|
|
restore_modifiers(chr_cache, objects)
|
|
|
|
elif utils.is_file_ext(ext, "BLEND"):
|
|
# store Unity project paths
|
|
props.unity_file_path = file_path
|
|
props.unity_project_path = utils.search_up_path(file_path, "Assets")
|
|
chr_cache.change_import_file(file_path)
|
|
# save blend file at filepath
|
|
bpy.ops.wm.save_as_mainfile(filepath=file_path)
|
|
bpy.ops.file.make_paths_relative()
|
|
bpy.ops.wm.save_as_mainfile(filepath=file_path)
|
|
# restore some lighting
|
|
shading = utils.get_view_3d_shading(context)
|
|
if shading:
|
|
shading.use_scene_lights = False
|
|
shading.use_scene_world = False
|
|
shading.studiolight_intensity = 1.0
|
|
|
|
#export_copy_fbx_key(chr_cache, dir, name)
|
|
export_copy_asset_file(chr_cache, dir, name, ".fbxkey", chr_cache.get_import_key_file())
|
|
|
|
utils.log_recess()
|
|
utils.log_info("")
|
|
|
|
if json_data:
|
|
utils.log_info("Writing Json Data.")
|
|
update_facial_profile_json(chr_cache, objects, json_data, name)
|
|
new_json_path = os.path.join(dir, name + ".json")
|
|
jsonutils.write_json(json_data, new_json_path)
|
|
|
|
# restore states and settings (but only for FBX export)
|
|
if utils.is_file_ext(ext, "FBX"):
|
|
utils.restore_object_state(object_state)
|
|
bones.restore_armature_settings(arm, armature_settings, include_pose=True)
|
|
# restore mode state
|
|
utils.restore_mode_selection_state(mode_selection_state)
|
|
|
|
utils.log_recess()
|
|
utils.log_timer("Done Character Export.")
|
|
|
|
|
|
def update_to_unity(self, context, chr_cache, export_anim, include_selected):
|
|
props = vars.props()
|
|
prefs = vars.prefs()
|
|
|
|
utils.start_timer()
|
|
|
|
utils.log_info("")
|
|
utils.log_info("Updating Character Model for UNITY:")
|
|
utils.log_info("-----------------------------------")
|
|
|
|
utils.object_mode()
|
|
|
|
chr_cache.check_paths()
|
|
|
|
# update the file path (it may have been moved inside the unity project)
|
|
if props.unity_file_path.lower().endswith(".fbx"):
|
|
pass
|
|
else:
|
|
props.unity_file_path = bpy.data.filepath
|
|
props.unity_project_path = utils.search_up_path(props.unity_file_path, "Assets")
|
|
|
|
dir, name = os.path.split(props.unity_file_path)
|
|
name, ext = os.path.splitext(name)
|
|
|
|
# keep the file paths up to date with the blend file location
|
|
# Note: the textures and json file *must* maintain their relative paths to the blend/model file
|
|
if not utils.is_file_ext(ext, "BLEND"):
|
|
utils.log_error("Update to Unity can only be called for Blend file exports!")
|
|
return
|
|
|
|
chr_cache.change_import_file(props.unity_file_path)
|
|
|
|
json_data = chr_cache.get_json_data()
|
|
|
|
utils.log_info("Preparing character for export:")
|
|
utils.log_indent()
|
|
|
|
# remove any collision proxies
|
|
if utils.is_file_ext(ext, "BLEND"):
|
|
for obj in chr_cache.get_cache_objects():
|
|
proxy = chr_cache.get_collision_proxy(obj)
|
|
if proxy:
|
|
utils.delete_object_tree(proxy)
|
|
|
|
objects = get_export_objects(chr_cache, include_selected)
|
|
|
|
as_blend_file = True
|
|
|
|
# remove custom material modifiers
|
|
remove_modifiers_for_export(chr_cache, objects, True)
|
|
|
|
prep_export(context, chr_cache, name, objects, json_data, chr_cache.get_import_dir(), dir, True, False, False, as_blend_file, False)
|
|
|
|
# make the T-pose as an action
|
|
arm = get_export_armature(chr_cache, objects)
|
|
utils.safe_set_action(arm, None)
|
|
chr_json = None
|
|
if json_data:
|
|
try:
|
|
chr_json = json_data[name]["Object"][name]
|
|
except: pass
|
|
set_T_pose(arm, chr_json)
|
|
create_T_pose_action(arm, objects, False)
|
|
|
|
# save blend file at filepath
|
|
bpy.ops.file.make_paths_relative()
|
|
bpy.ops.wm.save_mainfile()
|
|
|
|
utils.log_recess()
|
|
utils.log_info("")
|
|
|
|
if json_data:
|
|
utils.log_info("Writing Json Data.")
|
|
update_facial_profile_json(chr_cache, objects, json_data, name)
|
|
new_json_path = os.path.join(dir, name + ".json")
|
|
jsonutils.write_json(json_data, new_json_path)
|
|
|
|
utils.log_recess()
|
|
utils.log_timer("Done Character Export.")
|
|
|
|
|
|
def export_rigify(self, context, chr_cache, export_anim, file_path, include_selected):
|
|
props = vars.props()
|
|
prefs = vars.prefs()
|
|
|
|
utils.start_timer()
|
|
|
|
utils.log_info("")
|
|
utils.log_info("Exporting Rigified Character Model:")
|
|
utils.log_info("-----------------------------------")
|
|
|
|
utils.object_mode()
|
|
|
|
dir, file = os.path.split(file_path)
|
|
name, ext = os.path.splitext(file)
|
|
|
|
# store mode state
|
|
mode_selection_state = utils.store_mode_selection_state()
|
|
|
|
utils.log_info("Export to: " + file_path)
|
|
utils.log_info("Exporting as: " + ext)
|
|
|
|
json_data = None
|
|
include_textures = self.include_textures
|
|
if prefs.rigify_export_mode == "MOTION":
|
|
include_textures = False
|
|
else:
|
|
json_data = chr_cache.get_json_data()
|
|
|
|
utils.log_info("Preparing character for export:")
|
|
utils.log_indent()
|
|
|
|
# export objects
|
|
objects = get_export_objects(chr_cache, include_selected)
|
|
arm = chr_cache.get_armature()
|
|
export_rig = None
|
|
|
|
# store states and settings
|
|
armature_settings = bones.store_armature_settings(arm, include_pose=True)
|
|
object_state = utils.store_object_state(objects)
|
|
|
|
# restore quaternion rotation modes
|
|
rigutils.reset_rotation_modes(arm)
|
|
|
|
export_actions = False
|
|
export_strips = True
|
|
baked_actions = []
|
|
export_rig, vertex_group_map, t_pose_action = rigging.prep_rigify_export(chr_cache,
|
|
export_anim, baked_actions,
|
|
include_t_pose=prefs.rigify_export_t_pose,
|
|
objects=objects,
|
|
bone_naming = prefs.rigify_export_naming)
|
|
if export_rig:
|
|
rigify_rig = chr_cache.get_armature()
|
|
objects.remove(rigify_rig)
|
|
objects.append(export_rig)
|
|
|
|
use_anim = export_anim
|
|
if prefs.rigify_export_t_pose:
|
|
use_anim = True
|
|
|
|
# remove custom material modifiers
|
|
remove_modifiers_for_export(chr_cache, objects, True, rig=export_rig)
|
|
|
|
prep_export(context, chr_cache, name, objects, json_data, chr_cache.get_import_dir(), dir,
|
|
include_textures, False, False, False, False)
|
|
|
|
# for motion only exports, select armature and any mesh objects that have shape key animations
|
|
if prefs.rigify_export_mode == "MOTION":
|
|
utils.clear_selected_objects()
|
|
rigging.select_motion_export_objects(objects)
|
|
|
|
armature_object, armature_data = rigutils.rename_armature(export_rig, name)
|
|
|
|
# export as fbx
|
|
bpy.ops.export_scene.fbx(filepath=file_path,
|
|
use_selection = True,
|
|
bake_anim = use_anim,
|
|
bake_anim_use_all_actions=export_actions,
|
|
bake_anim_use_nla_strips=export_strips,
|
|
bake_anim_simplify_factor=self.animation_simplify,
|
|
use_armature_deform_only=True,
|
|
add_leaf_bones = False,
|
|
#axis_forward = "-Y",
|
|
#axis_up = "Z",
|
|
mesh_smooth_type = ("FACE" if self.export_face_smoothing else "OFF"),
|
|
use_mesh_modifiers = True)
|
|
|
|
if prefs.rigify_export_t_pose:
|
|
bones.clear_pose(export_rig)
|
|
|
|
# put t-pose back on armature
|
|
utils.safe_set_action(export_rig, t_pose_action)
|
|
|
|
bpy.context.view_layer.update()
|
|
|
|
# write HIK profile for RIGIFY
|
|
hik_path = os.path.join(dir, name + ".3dxProfile")
|
|
if prefs.rigify_export_naming == "METARIG":
|
|
hik_template = hik.RIGIFY_METARIG_PROFILE_TEMPLATE
|
|
elif prefs.rigify_export_naming == "CC":
|
|
hik_template = hik.RIGIFY_CC_BASE_PROFILE_TEMPLATE
|
|
else:
|
|
hik_template = hik.RIGIFY_BASE_PROFILE_TEMPLATE
|
|
if hik.generate_hik_profile(export_rig, name, hik_path, hik_template):
|
|
if json_data:
|
|
json_data[name]["HIK"] = {}
|
|
json_data[name]["HIK"]["Profile_Path"] = os.path.relpath(hik_path, dir)
|
|
|
|
# clear armature actions
|
|
utils.safe_set_action(export_rig, None)
|
|
|
|
rigutils.restore_armature_names(armature_object, armature_data, name)
|
|
|
|
restore_modifiers(chr_cache, objects)
|
|
|
|
# clean up rigify export
|
|
rigging.finish_rigify_export(chr_cache, export_rig, baked_actions, vertex_group_map,
|
|
objects=objects)
|
|
|
|
utils.log_recess()
|
|
utils.log_info("")
|
|
|
|
if json_data:
|
|
utils.log_info("Writing Json Data.")
|
|
update_facial_profile_json(chr_cache, objects, json_data, name)
|
|
new_json_path = os.path.join(dir, name + ".json")
|
|
jsonutils.write_json(json_data, new_json_path)
|
|
|
|
# restore states and settings
|
|
utils.restore_object_state(object_state)
|
|
bones.restore_armature_settings(arm, armature_settings, include_pose=True)
|
|
|
|
# restore mode state
|
|
utils.restore_mode_selection_state(mode_selection_state)
|
|
|
|
utils.log_recess()
|
|
utils.log_timer("Done Rigify Export.")
|
|
|
|
|
|
def export_as_accessory(file_path, filename_ext):
|
|
dir, file = os.path.split(file_path)
|
|
name, ext = os.path.splitext(file)
|
|
|
|
# store selection
|
|
old_selection = bpy.context.selected_objects
|
|
old_active = utils.get_active_object()
|
|
|
|
if utils.is_file_ext(ext, "FBX"):
|
|
bpy.ops.export_scene.fbx(filepath=file_path,
|
|
use_selection = True,
|
|
bake_anim = False,
|
|
add_leaf_bones=False,
|
|
)
|
|
elif utils.is_file_ext(ext, "OBJ"):
|
|
obj_export(file_path, use_selection=True,
|
|
global_scale=100,
|
|
use_animation=False,
|
|
use_materials=True,
|
|
keep_vertex_order=True,
|
|
use_vertex_colors=True,
|
|
use_vertex_groups=False,
|
|
apply_modifiers=True)
|
|
|
|
# restore selection
|
|
bpy.ops.object.select_all(action='DESELECT')
|
|
for obj in old_selection:
|
|
obj.select_set(True)
|
|
bpy.context.view_layer.objects.active = old_active
|
|
|
|
|
|
def export_as_replace_mesh(file_path):
|
|
dir, file = os.path.split(file_path)
|
|
name, ext = os.path.splitext(file)
|
|
|
|
# store selection
|
|
state = utils.store_mode_selection_state()
|
|
|
|
obj_export(file_path, use_selection=True,
|
|
global_scale=100,
|
|
use_animation=False,
|
|
use_materials=True,
|
|
keep_vertex_order=True,
|
|
use_vertex_colors=True,
|
|
use_vertex_groups=False,
|
|
apply_modifiers=True)
|
|
|
|
# restore selection
|
|
utils.restore_mode_selection_state(state)
|
|
|
|
|
|
class CC3Export(bpy.types.Operator):
|
|
"""Export CC3 Character"""
|
|
bl_idname = "cc3.exporter"
|
|
bl_label = "Export"
|
|
bl_options = {"REGISTER"}
|
|
|
|
filepath: bpy.props.StringProperty(
|
|
name="File Path",
|
|
description="Filepath used for exporting the file",
|
|
maxlen=1024,
|
|
subtype='FILE_PATH',
|
|
)
|
|
|
|
filter_glob: bpy.props.StringProperty(
|
|
default="*.fbx;*.obj;*.blend",
|
|
options={"HIDDEN"},
|
|
)
|
|
|
|
animation_simplify: bpy.props.FloatProperty(
|
|
default=1.0,
|
|
min=0.0, max=10.0,
|
|
name="Simplify Animation",
|
|
description="How much to simplify baked values (0.0 to disable, higher values for more simplification)",
|
|
)
|
|
|
|
param: bpy.props.StringProperty(
|
|
name = "param",
|
|
default = "",
|
|
options={"HIDDEN"}
|
|
)
|
|
|
|
filename_ext = ".fbx" # ExportHelper mixin class uses this
|
|
|
|
link_id_override: bpy.props.StringProperty(
|
|
name = "link_id_override",
|
|
default = "",
|
|
options={"HIDDEN"}
|
|
)
|
|
|
|
include_anim: bpy.props.BoolProperty(name = "Export Animation", default = True,
|
|
description="Export current timeline animation with the character")
|
|
include_selected: bpy.props.BoolProperty(name = "Include Selected", default = True,
|
|
description="Include any additional selected objects with the character. Note: They will need to be correctly parented and weighted")
|
|
include_textures: bpy.props.BoolProperty(name = "Include Textures", default = False,
|
|
description="Copy textures with the character, if exporting to a new location")
|
|
export_face_smoothing: bpy.props.BoolProperty(name = "Face Smoothing Groups", default = False,
|
|
description="Export FBX with face smoothing groups. (Can solve blocky faces / split normals issues in game engines)")
|
|
|
|
check_valid = True
|
|
check_report = []
|
|
check_warn = False
|
|
|
|
def error_report(self):
|
|
# error report
|
|
if not self.check_valid:
|
|
utils.message_box_multi("Export Check: Invalid Export", "ERROR", self.check_report)
|
|
elif self.check_warn:
|
|
utils.message_box_multi("Export Check: Some Warnings", "INFO", self.check_report)
|
|
|
|
def execute(self, context):
|
|
props = vars.props()
|
|
prefs = vars.prefs()
|
|
chr_cache = props.get_context_character_cache(context)
|
|
if self.link_id_override:
|
|
chr_cache = props.find_character_by_link_id(self.link_id_override)
|
|
self.include_selected = False
|
|
|
|
if chr_cache and self.param == "EXPORT_CC3":
|
|
|
|
export_standard(self, context, chr_cache, self.filepath, self.include_selected)
|
|
self.report({'INFO'}, "Export to CC3 Done!")
|
|
self.error_report()
|
|
|
|
elif chr_cache and self.param == "EXPORT_UNITY":
|
|
|
|
export_to_unity(self, context, chr_cache, self.include_anim, self.filepath, self.include_selected)
|
|
self.report({'INFO'}, "Export to Unity Done!")
|
|
self.error_report()
|
|
|
|
elif self.param == "UPDATE_UNITY":
|
|
|
|
# only called when updating .blend file exports
|
|
update_to_unity(self, context, chr_cache, self.include_anim, True)
|
|
self.report({'INFO'}, "Update to Unity Done!")
|
|
self.error_report()
|
|
|
|
elif chr_cache and self.param == "EXPORT_RIGIFY":
|
|
|
|
export_rigify(self, context, chr_cache, self.include_anim, self.filepath, self.include_selected)
|
|
self.report({'INFO'}, "Export from Rigified Done!")
|
|
self.error_report()
|
|
|
|
elif self.param == "EXPORT_NON_STANDARD":
|
|
|
|
export_non_standard(self, context, self.filepath, self.include_selected)
|
|
self.error_report()
|
|
|
|
|
|
elif self.param == "EXPORT_ACCESSORY":
|
|
|
|
export_as_accessory(self.filepath, self.filename_ext)
|
|
self.report({'INFO'}, message="Export Accessory Done!")
|
|
|
|
elif self.param == "EXPORT_MESH":
|
|
|
|
export_as_replace_mesh(self.filepath)
|
|
self.report({'INFO'}, message="Export Mesh Replacement Done!")
|
|
|
|
elif self.param == "CHECK_EXPORT":
|
|
|
|
if chr_cache and chr_cache.is_import_type("FBX"):
|
|
chr_cache = props.get_context_character_cache(context)
|
|
objects = get_export_objects(chr_cache, True)
|
|
self.check_valid, self.check_warn, self.check_report = check_valid_export_fbx(chr_cache, objects)
|
|
if not self.check_valid or self.check_warn:
|
|
self.error_report()
|
|
else:
|
|
utils.message_box("No issues detected.", "Export Check", "INFO")
|
|
|
|
else:
|
|
pass
|
|
|
|
return {"FINISHED"}
|
|
|
|
|
|
def invoke(self, context, event):
|
|
prefs = vars.prefs()
|
|
|
|
self.check_report = []
|
|
self.check_valid = True
|
|
self.check_warn = False
|
|
|
|
# bypass modal for direct functions
|
|
if self.param == "UPDATE_UNITY":
|
|
return self.execute(context)
|
|
|
|
if self.param == "CHECK_EXPORT":
|
|
return self.execute(context)
|
|
|
|
props = vars.props()
|
|
chr_cache = props.get_context_character_cache(context)
|
|
if self.link_id_override:
|
|
chr_cache = props.find_character_by_link_id(self.link_id_override)
|
|
|
|
# menu export
|
|
if self.param == "EXPORT_MENU":
|
|
if chr_cache and chr_cache.rigified:
|
|
self.param = "EXPORT_RIGIFY"
|
|
else:
|
|
self.param = "EXPORT_CC3"
|
|
|
|
# determine export format
|
|
export_format = "fbx"
|
|
export_suffix = ""
|
|
if self.param == "EXPORT_MESH":
|
|
export_format = "obj"
|
|
export_suffix = "_mesh"
|
|
elif self.param == "EXPORT_ACCESSORY":
|
|
export_suffix = "_accessory"
|
|
elif self.param == "EXPORT_NON_STANDARD":
|
|
export_format = "fbx"
|
|
elif self.param == "EXPORT_RIGIFY":
|
|
export_format = "fbx"
|
|
if prefs.rigify_export_mode == "MOTION":
|
|
export_suffix = "_motion"
|
|
elif self.param == "EXPORT_UNITY":
|
|
if prefs.export_unity_mode == "FBX":
|
|
export_format = "fbx"
|
|
else:
|
|
export_format = "blend"
|
|
elif chr_cache:
|
|
export_format = utils.get_file_ext(chr_cache.get_import_type())
|
|
if export_format != "obj":
|
|
export_format = "fbx"
|
|
if chr_cache.rigified:
|
|
export_format = "fbx"
|
|
self.filename_ext = "." + export_format
|
|
|
|
if chr_cache and (chr_cache.generation == "Generic" or
|
|
chr_cache.generation == "NonStandardGeneric" or
|
|
chr_cache.generation == "Unknown"):
|
|
self.include_textures = True
|
|
|
|
if self.param == "EXPORT_UNITY":
|
|
self.include_textures = True
|
|
if export_format == "fbx":
|
|
self.export_face_smoothing = True
|
|
|
|
if self.param == "EXPORT_RIGIFY":
|
|
if prefs.rigify_export_mode == "MOTION":
|
|
self.include_textures = False
|
|
self.include_anim = True
|
|
elif prefs.rigify_export_mode == "MESH":
|
|
self.include_textures = True
|
|
self.include_anim = False
|
|
self.export_face_smoothing = True
|
|
else:
|
|
self.include_textures = True
|
|
self.include_anim = True
|
|
self.export_face_smoothing = True
|
|
|
|
# perform checks and validation
|
|
require_export_check = (self.param == "EXPORT_CC3" or
|
|
self.param == "EXPORT_UNITY" or
|
|
self.param == "EXPORT_RIGIFY" or
|
|
self.param == "UPDATE_UNITY" or
|
|
self.param == "EXPORT_NON_STANDARD")
|
|
require_valid_export = (self.param == "EXPORT_CC3" or
|
|
self.param == "EXPORT_NON_STANDARD")
|
|
if require_export_check:
|
|
objects = get_export_objects(chr_cache, self.include_selected)
|
|
if export_format == "fbx":
|
|
self.check_valid, self.check_warn, self.check_report = check_valid_export_fbx(chr_cache, objects)
|
|
if require_valid_export:
|
|
if not self.check_valid:
|
|
self.error_report()
|
|
return {"FINISHED"}
|
|
|
|
# determine default file name
|
|
if not self.filepath:
|
|
default_file_path = context.blend_data.filepath
|
|
if default_file_path:
|
|
default_file_path = os.path.splitext(default_file_path)[0]
|
|
else:
|
|
if chr_cache:
|
|
default_file_path = chr_cache.get_character_id()
|
|
else:
|
|
default_file_path = "untitled"
|
|
|
|
self.filepath = default_file_path + export_suffix + self.filename_ext
|
|
|
|
context.window_manager.fileselect_add(self)
|
|
return {"RUNNING_MODAL"}
|
|
|
|
|
|
def check(self, context):
|
|
change_ext = False
|
|
filepath = self.filepath
|
|
if os.path.basename(filepath):
|
|
base, ext = os.path.splitext(filepath)
|
|
if ext != self.filename_ext:
|
|
filepath = bpy.path.ensure_ext(base, self.filename_ext)
|
|
else:
|
|
filepath = bpy.path.ensure_ext(filepath, self.filename_ext)
|
|
if filepath != self.filepath:
|
|
self.filepath = filepath
|
|
change_ext = True
|
|
return change_ext
|
|
|
|
@classmethod
|
|
def description(cls, context, properties):
|
|
|
|
if properties.param == "EXPORT_CC3":
|
|
return "Export full character to import back into CC3"
|
|
elif properties.param == "EXPORT_NON_STANDARD":
|
|
return "Export selected objects as a non-standard character (Humanoid, Creature or Prop) to CC4"
|
|
elif properties.param == "EXPORT_UNITY":
|
|
return "Export to / Save in Unity project.\n" \
|
|
"**Note: Pipeline Exports to Unity are exported as character only, without animations, with a T-Pose**"
|
|
elif properties.param == "EXPORT_RIGIFY":
|
|
return "Export rigified character and/or animation.\n"
|
|
elif properties.param == "EXPORT_ACCESSORY":
|
|
return "Export selected object(s) for import into CC3 as accessories"
|
|
elif properties.param == "EXPORT_MESH":
|
|
return "Export selected object as a mesh replacement. Use with Mesh > Replace Mesh, with the desired mesh to replace selected in CC4.\n" \
|
|
"**Mesh must have the same number of vertices as the original mesh to replace**"
|
|
elif properties.param == "CHECK_EXPORT":
|
|
return "Check for issues with the character for export. *Note* This will also test any selected objects as well as all objects attached to the character, as selected objects can also be exported with the character"
|
|
elif properties.param == "EXPORT_CC3_INVALID":
|
|
return "This standard character has altered topology of the base body mesh and will not re-import into Character Creator"
|
|
return ""
|
|
|
|
|
|
def menu_func_export(self, context):
|
|
self.layout.operator(CC3Export.bl_idname, text="Reallusion Character (.fbx, .obj)").param = "EXPORT_MENU"
|
|
|