2525 lines
101 KiB
Python
2525 lines
101 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 bpy
|
|
import re
|
|
import os
|
|
|
|
from . import (springbones, rigidbody, materials, modifiers, meshutils, geom, bones, physics, rigutils,
|
|
shaders, basic, imageutils, nodeutils, jsonutils, utils, vars)
|
|
|
|
from mathutils import Vector, Matrix, Quaternion
|
|
import mathutils.geometry
|
|
|
|
MANDATORY_OBJECTS = ["BODY", "TEETH", "TONGUE", "TEARLINE", "OCCLUSION", "EYE"]
|
|
|
|
|
|
def select_character(chr_cache, all=False):
|
|
if chr_cache:
|
|
rig = chr_cache.get_armature()
|
|
objects = chr_cache.get_all_objects(include_children=True)
|
|
rig_already_active = (rig and utils.get_active_object() == rig)
|
|
objects_already_selected = True
|
|
for obj in objects:
|
|
if obj not in bpy.context.selected_objects:
|
|
objects_already_selected = False
|
|
if all:
|
|
utils.try_select_objects(objects, clear_selection=True)
|
|
elif rig:
|
|
utils.try_select_object(rig, clear_selection=True)
|
|
else:
|
|
utils.try_select_objects(objects, clear_selection=True)
|
|
try:
|
|
if rig:
|
|
utils.set_active_object(rig)
|
|
if rig_already_active and (not all or objects_already_selected):
|
|
rig.show_in_front = not rig.show_in_front
|
|
else:
|
|
rig.show_in_front = False
|
|
except:
|
|
pass
|
|
|
|
|
|
def duplicate_character(chr_cache):
|
|
props = vars.props()
|
|
|
|
objects = chr_cache.get_cache_objects()
|
|
state = utils.store_object_state(objects)
|
|
rigutils.clear_all_actions(objects)
|
|
tmp = utils.force_visible_in_scene("TMP_Duplicate", *objects)
|
|
utils.try_select_objects(objects, clear_selection=True)
|
|
bpy.ops.object.duplicate()
|
|
objects = bpy.context.selected_objects.copy()
|
|
utils.restore_object_state(state)
|
|
utils.restore_visible_in_scene(tmp)
|
|
|
|
# duplicate materials
|
|
dup_mats = {}
|
|
for obj in objects:
|
|
if obj.type == "MESH":
|
|
mat: bpy.types.Material
|
|
for mat in obj.data.materials:
|
|
if mat not in dup_mats:
|
|
dup_mats[mat] = mat.copy()
|
|
for slot in obj.material_slots:
|
|
if slot.material:
|
|
slot.material = dup_mats[slot.material]
|
|
|
|
# copy chr_cache
|
|
old_cache = chr_cache
|
|
chr_cache = props.add_character_cache()
|
|
utils.copy_property_group(old_cache, chr_cache)
|
|
chr_cache.link_id = utils.generate_random_id(20)
|
|
|
|
for obj_cache in chr_cache.object_cache:
|
|
old_id = obj_cache.object_id
|
|
old_obj = obj_cache.object
|
|
new_id = utils.generate_random_id(20)
|
|
obj_cache.object_id = new_id
|
|
for obj in objects:
|
|
if utils.get_rl_object_id(obj) == old_id:
|
|
utils.set_rl_object_id(obj, new_id)
|
|
obj_cache.object = obj
|
|
if obj.type == "ARMATURE":
|
|
action = utils.safe_get_action(old_obj)
|
|
utils.safe_set_action(obj, action)
|
|
elif obj.type == "MESH" and utils.object_has_shape_keys(obj):
|
|
action = utils.safe_get_action(old_obj.data.shape_keys)
|
|
utils.safe_set_action(obj.data.shape_keys, action)
|
|
|
|
all_mat_cache = chr_cache.get_all_materials_cache(include_disabled=True)
|
|
for mat_cache in all_mat_cache:
|
|
old_id = mat_cache.material_id
|
|
new_id = utils.generate_random_id(20)
|
|
mat_cache.material_id = new_id
|
|
for obj in objects:
|
|
if obj.type == "MESH":
|
|
for mat in obj.data.materials:
|
|
if "rl_material_id" in mat and mat["rl_material_id"] == old_id:
|
|
mat["rl_material_id"] = new_id
|
|
mat_cache.material = mat
|
|
|
|
chr_rig = utils.get_armature_from_objects(objects)
|
|
character_name = utils.unique_object_name(utils.un_suffix_name(old_cache.character_name))
|
|
utils.log_info(f"Using character name: {character_name}")
|
|
chr_cache.character_name = character_name
|
|
if chr_cache.rigified:
|
|
rig_name = character_name + "_Rigify"
|
|
chr_rig["rig_id"] = utils.generate_random_id(20)
|
|
# copy the meta-rig too, if it's still there
|
|
if utils.object_exists_is_armature(chr_cache.rig_meta_rig):
|
|
tmp = utils.force_visible_in_scene("TMP_Duplicate", chr_cache.rig_meta_rig)
|
|
utils.set_active_object(chr_cache.rig_meta_rig, deselect_all=True)
|
|
bpy.ops.object.duplicate()
|
|
meta_rig = utils.get_active_object()
|
|
meta_rig.name = character_name + "_metarig"
|
|
utils.restore_visible_in_scene(tmp)
|
|
chr_cache.rig_meta_rig = meta_rig
|
|
utils.set_active_object(chr_rig)
|
|
else:
|
|
rig_name = character_name
|
|
|
|
chr_rig.name = rig_name
|
|
chr_rig.data.name = rig_name
|
|
|
|
return objects
|
|
|
|
|
|
def get_character_objects(arm):
|
|
"""Fetch all the objects in the character (or try to)"""
|
|
objects = []
|
|
if arm.type == "ARMATURE":
|
|
objects.append(arm)
|
|
for obj in arm.children:
|
|
if utils.object_exists_is_mesh(obj):
|
|
if obj not in objects:
|
|
objects.append(obj)
|
|
return objects
|
|
|
|
|
|
def get_generic_rig(objects):
|
|
props = vars.props()
|
|
arm = utils.get_armature_from_objects(objects)
|
|
if arm:
|
|
chr_cache = props.get_character_cache(arm, None)
|
|
if not chr_cache:
|
|
return arm
|
|
return None
|
|
|
|
|
|
def make_prop_armature(objects):
|
|
|
|
utils.object_mode()
|
|
|
|
# find the all the root empties and determine if there is one single root
|
|
roots = []
|
|
single_empty_root = None
|
|
for obj in objects:
|
|
|
|
# reset all transforms
|
|
#bpy.ops.object.transform_apply(location=True, rotation=True, scale=True)
|
|
|
|
# find single root
|
|
if obj.parent is None or obj.parent not in objects:
|
|
if obj.type == "EMPTY":
|
|
roots.append(obj)
|
|
single_empty_root = obj
|
|
if len(roots) > 1:
|
|
single_empty_root = False
|
|
single_empty_root = None
|
|
else:
|
|
single_empty_root = None
|
|
|
|
arm_name = "Prop_" + utils.generate_random_id(8)
|
|
if single_empty_root:
|
|
arm_name = single_empty_root.name
|
|
|
|
arm_data = bpy.data.armatures.new(arm_name)
|
|
arm = bpy.data.objects.new(arm_name, arm_data)
|
|
bpy.context.collection.objects.link(arm)
|
|
if single_empty_root:
|
|
arm.location = utils.object_world_location(single_empty_root)
|
|
utils.clear_selected_objects()
|
|
|
|
root_bone : bpy.types.EditBone = None
|
|
bone : bpy.types.EditBone = None
|
|
root_bone_name = None
|
|
tail_vector = Vector((0,0.5,0))
|
|
tail_translate = Matrix.Translation(-tail_vector)
|
|
|
|
if utils.edit_mode_to(arm):
|
|
|
|
if single_empty_root:
|
|
root_bone = arm.data.edit_bones.new(single_empty_root.name)
|
|
root_bone.head = arm.matrix_local @ utils.object_world_location(single_empty_root)
|
|
root_bone.tail = arm.matrix_local @ utils.object_world_location(single_empty_root, tail_vector)
|
|
root_bone.roll = 0
|
|
root_bone_name = root_bone.name
|
|
else:
|
|
root_bone = arm.data.edit_bones.new("Root")
|
|
root_bone.head = Vector((0,0,0))
|
|
root_bone.tail = Vector((0,0,0)) + tail_vector
|
|
root_bone.roll = 0
|
|
root_bone_name = root_bone.name
|
|
|
|
for obj in objects:
|
|
if obj.type == "EMPTY" and obj.name not in arm.data.edit_bones:
|
|
bone = arm.data.edit_bones.new(obj.name)
|
|
bone.head = arm.matrix_local @ utils.object_world_location(obj)
|
|
bone.tail = arm.matrix_local @ utils.object_world_location(obj, tail_vector)
|
|
|
|
for obj in objects:
|
|
if obj.type == "EMPTY" and obj.parent:
|
|
bone = arm.data.edit_bones[obj.name]
|
|
if obj.parent.name in arm.data.edit_bones:
|
|
parent = arm.data.edit_bones[obj.parent.name]
|
|
bone.parent = parent
|
|
elif bone != bone.parent:
|
|
bone.parent = root_bone
|
|
else:
|
|
bone.parent = None
|
|
|
|
utils.object_mode_to(arm)
|
|
|
|
obj : bpy.types.Object
|
|
for obj in objects:
|
|
if obj.type == "MESH":
|
|
if obj.parent and obj.parent.name in arm.data.bones:
|
|
parent_name = obj.parent.name
|
|
parent_bone : bpy.types.Bone = arm.data.bones[parent_name]
|
|
omw = obj.matrix_world.copy()
|
|
obj.parent = arm
|
|
obj.parent_type = 'BONE'
|
|
obj.parent_bone = parent_name
|
|
# by re-applying (a copy of) the original matrix_world, blender
|
|
# works out the correct parent inverse transforms from the bone
|
|
obj.matrix_world = omw
|
|
elif root_bone_name:
|
|
parent_bone = arm.data.bones[root_bone_name]
|
|
omw = obj.matrix_world.copy()
|
|
obj.parent = arm
|
|
obj.parent_type = 'BONE'
|
|
obj.parent_bone = root_bone_name
|
|
# by re-applying (a copy of) the original matrix_world, blender
|
|
# works out the correct parent inverse transforms from the bone
|
|
obj.matrix_world = omw
|
|
|
|
# remove the empties and move all objects into the same collection as the armature
|
|
collections = utils.get_object_scene_collections(arm)
|
|
for obj in objects:
|
|
if obj.type == "EMPTY":
|
|
bpy.data.objects.remove(obj)
|
|
else:
|
|
utils.move_object_to_scene_collections(obj, collections)
|
|
|
|
# finally force the armature name again (as it may have been taken by the original object)
|
|
arm.name = arm_name
|
|
|
|
return arm
|
|
|
|
|
|
def convert_generic_to_non_standard(objects, file_path=None, type_override=None, name_override=None, link_id=None):
|
|
props = vars.props()
|
|
prefs = vars.prefs()
|
|
|
|
utils.log_info("")
|
|
utils.log_info("Converting Generic Character:")
|
|
utils.log_info("-----------------------------")
|
|
|
|
# get generic objects
|
|
non_chr_objects = [ obj for obj in objects
|
|
if props.get_object_cache(obj) is None
|
|
and (obj.type == "MESH"
|
|
or obj.type == "EMPTY")]
|
|
active_non_chr_object = utils.get_active_object()
|
|
if active_non_chr_object not in non_chr_objects:
|
|
active_non_chr_object = non_chr_objects[0]
|
|
|
|
# select all child objects of the current selected objects
|
|
utils.try_select_objects(non_chr_objects, True)
|
|
for obj in non_chr_objects:
|
|
utils.try_select_child_objects(obj)
|
|
|
|
objects = bpy.context.selected_objects
|
|
chr_rig = utils.get_armature_from_objects(objects)
|
|
|
|
utils.log_info(f"Generic character objects:")
|
|
for obj in objects:
|
|
utils.log_info(f" - {obj.name} ({obj.type})")
|
|
|
|
# determine character type
|
|
if type_override:
|
|
chr_type = type_override
|
|
else:
|
|
if chr_rig:
|
|
chr_type = "HUMANOID"
|
|
else:
|
|
chr_type = "PROP"
|
|
|
|
utils.log_info(f"Generic character type: {chr_type}")
|
|
|
|
# determine character name
|
|
chr_name = "Unnamed"
|
|
if file_path:
|
|
dir, file = os.path.split(file_path)
|
|
name, ext = os.path.splitext(file)
|
|
chr_name = name
|
|
elif name_override:
|
|
chr_name = name_override
|
|
dir = ""
|
|
else:
|
|
if chr_type == "HUMANOID":
|
|
chr_name = "Humanoid"
|
|
elif chr_type == "CREATURE":
|
|
chr_name = "Creature"
|
|
else:
|
|
chr_name = "Prop"
|
|
chr_name = utils.unique_object_name(chr_name, chr_rig)
|
|
|
|
utils.log_info(f"Generic character name: {chr_name}")
|
|
|
|
# if no rig: generate one from the objects and empty parent transforms
|
|
if not chr_rig:
|
|
utils.log_info(f"Generating Prop Rig...")
|
|
chr_rig = make_prop_armature(objects)
|
|
|
|
# now treat the armature as any generic character
|
|
objects = get_character_objects(chr_rig)
|
|
|
|
utils.log_info(f"Creating Character Data...")
|
|
|
|
chr_rig.name = chr_name
|
|
chr_rig.data.name = chr_name
|
|
chr_cache = props.import_cache.add()
|
|
chr_cache.import_file = ""
|
|
chr_cache.character_name = chr_name
|
|
chr_cache.import_embedded = False
|
|
chr_cache.generation = "Unknown"
|
|
chr_cache.non_standard_type = chr_type
|
|
if not link_id:
|
|
link_id = utils.generate_random_id(20)
|
|
chr_cache.link_id = link_id
|
|
|
|
chr_cache.add_object_cache(chr_rig)
|
|
|
|
utils.log_info(f"Adding Character Objects:")
|
|
|
|
# add child objects to chr_cache
|
|
for obj in objects:
|
|
if utils.object_exists_is_mesh(obj):
|
|
add_object_to_character(chr_cache, obj, reparent=False,
|
|
no_materials=not prefs.auto_convert_materials)
|
|
|
|
return chr_cache
|
|
|
|
|
|
def link_override(obj: bpy.types.Object):
|
|
if obj:
|
|
collections = utils.get_object_scene_collections(obj)
|
|
override = obj.override_create(remap_local_usages=True)
|
|
coll: bpy.types.Collection
|
|
for coll in collections:
|
|
try:
|
|
coll.objects.link(override)
|
|
except: ...
|
|
|
|
|
|
def link_or_append_rl_character(op, context, blend_file, link=False):
|
|
props = vars.props()
|
|
prefs = vars.prefs()
|
|
|
|
utils.log_info("")
|
|
utils.log_info("Link/Append Reallusion Character")
|
|
utils.log_info("--------------------------------")
|
|
|
|
# if linking, reload any existing library link for this blend file
|
|
# otherwise it remembers if objects have been previously deleted.
|
|
existing_lib = None
|
|
if link:
|
|
for lib in bpy.data.libraries:
|
|
if os.path.samefile(lib.filepath, blend_file):
|
|
lib.reload()
|
|
existing_lib = lib
|
|
break
|
|
|
|
# link or append character data from blend file
|
|
with bpy.data.libraries.load(blend_file, link=link) as (src, dst):
|
|
dst.scenes = src.scenes
|
|
dst.objects = src.objects
|
|
|
|
ignore = []
|
|
keep = []
|
|
|
|
# find the add-on character data in the blend file
|
|
src_props = None
|
|
for scene in dst.scenes:
|
|
if "CC3ImportProps" in scene:
|
|
src_props = scene.CC3ImportProps
|
|
utils.log_info(f"Found Add-on Import Properties")
|
|
break
|
|
|
|
has_rigid_body = False
|
|
|
|
if src_props:
|
|
|
|
for src_cache in src_props.import_cache:
|
|
|
|
character_name = src_cache.character_name
|
|
import_file = src_cache.import_file
|
|
chr_rig = src_cache.get_armature()
|
|
chr_objects = src_cache.get_all_objects(include_armature=False,
|
|
include_children=True)
|
|
meta_rig = src_cache.rig_meta_rig
|
|
src_rig = src_cache.rig_original_rig
|
|
widgets = []
|
|
objects = []
|
|
|
|
utils.log_info(f"Character Data Found: {character_name}")
|
|
|
|
# keep the character rig
|
|
if chr_rig:
|
|
utils.log_info(f"Character rig: {chr_rig.name}")
|
|
keep.append(chr_rig)
|
|
objects.append(chr_rig)
|
|
|
|
# keep the meta rig
|
|
if meta_rig:
|
|
utils.log_info(f"Meta-rig: {meta_rig.name}")
|
|
keep.append(meta_rig)
|
|
objects.append(meta_rig)
|
|
|
|
# ignore the source rig
|
|
if src_rig:
|
|
ignore.append(src_rig)
|
|
|
|
# keep all child objects of the rigify rig
|
|
for obj in dst.objects:
|
|
if obj in chr_objects:
|
|
utils.log_info(f" - Character Object: {obj.name}")
|
|
keep.append(obj)
|
|
objects.append(obj)
|
|
|
|
# find the widgets
|
|
widget_prefix = f"WGT-{character_name}_rig"
|
|
widget_collection_name = f"WGT_{character_name}_rig"
|
|
for obj in dst.objects:
|
|
if obj.name.startswith(widget_prefix):
|
|
keep.append(obj)
|
|
widgets.append(obj)
|
|
|
|
# TODO remove all actions or keep them? or get all of them?
|
|
|
|
# link overrides
|
|
if link and False:
|
|
overrides = {}
|
|
for obj in objects:
|
|
override = obj.override_create(remap_local_usages=True)
|
|
if obj == meta_rig:
|
|
meta_rig = override
|
|
if obj == chr_rig:
|
|
chr_rig = override
|
|
if obj == src_rig:
|
|
src_rig = override
|
|
overrides[obj] = override
|
|
for obj in overrides:
|
|
override = overrides[obj]
|
|
try:
|
|
if hasattr(override, "parent"):
|
|
if override.parent and override.parent in overrides:
|
|
override.parent = overrides[override.parent]
|
|
except: ...
|
|
if override.type == "MESH":
|
|
for mod in override.modifiers:
|
|
if mod:
|
|
if hasattr(mod, "object") and mod.object in overrides:
|
|
mod.object = overrides[mod.object]
|
|
elif override.type == "EMPTY":
|
|
con = override.rigid_body_constraint
|
|
if con:
|
|
if hasattr(con, "target") and con.target in overrides:
|
|
con.target = overrides[con.target]
|
|
if hasattr(con, "object") and con.object in overrides:
|
|
con.object = overrides[con.object]
|
|
if hasattr(con, "object1") and con.object1 in overrides:
|
|
con.object1 = overrides[con.object1]
|
|
if hasattr(con, "object2") and con.object2 in overrides:
|
|
con.object2 = overrides[con.object2]
|
|
objects = list(overrides.values())
|
|
|
|
# after deciding what to keep, check the character has not already been linked
|
|
if link and (props.get_character_cache(chr_rig, None) or
|
|
props.get_character_cache_from_objects(chr_objects)):
|
|
op.report({"ERROR"}, "Character already linked!")
|
|
continue
|
|
|
|
# put all the character objects in the character collection
|
|
character_collection = utils.create_collection(character_name)
|
|
for obj in objects:
|
|
character_collection.objects.link(obj)
|
|
|
|
# put the widgets in the widget sub-collection
|
|
if widgets:
|
|
widget_collection = utils.create_collection(widget_collection_name,
|
|
existing=False,
|
|
parent_collection=character_collection)
|
|
for widget in widgets:
|
|
widget_collection.objects.link(widget)
|
|
utils.hide(widget)
|
|
|
|
# hide the widget sub-collection
|
|
lc = utils.find_layer_collection(widget_collection.name)
|
|
lc.exclude = True
|
|
|
|
# hide the meta rig
|
|
if meta_rig:
|
|
utils.hide(meta_rig)
|
|
|
|
# create the character cache and rebuild from the source data
|
|
chr_cache = props.add_character_cache(copy_from=src_cache)
|
|
rebuild_character_cache(chr_cache, chr_rig, chr_objects, src_cache)
|
|
|
|
# hide any colliders
|
|
rigidbody.hide_colliders(chr_rig)
|
|
|
|
# get rigidy body systems and hide them
|
|
if chr_rig:
|
|
parent_modes = springbones.get_all_parent_modes(chr_cache, chr_rig)
|
|
for parent_mode in parent_modes:
|
|
rig_prefix = springbones.get_spring_rig_prefix(parent_mode)
|
|
rigid_body_system = rigidbody.get_spring_rigid_body_system(chr_rig, rig_prefix)
|
|
if rigid_body_system:
|
|
if link:
|
|
rigidbody.remove_existing_rigid_body_system(chr_rig, rig_prefix, rigid_body_system.name)
|
|
else:
|
|
has_rigid_body = True
|
|
utils.hide_tree(rigid_body_system, hide=True)
|
|
|
|
|
|
# clean up unused objects
|
|
for obj in dst.objects:
|
|
if obj not in keep:
|
|
utils.delete_object(obj)
|
|
|
|
# init rigidy body world if needed
|
|
if has_rigid_body:
|
|
rigidbody.init_rigidbody_world()
|
|
|
|
|
|
|
|
def reconnect_rl_character_to_fbx(chr_rig, fbx_path):
|
|
props = vars.props()
|
|
prefs = vars.prefs()
|
|
|
|
objects = get_character_objects(chr_rig)
|
|
|
|
utils.log_info("")
|
|
utils.log_info("Re-connecting Character to Source FBX")
|
|
utils.log_info("-------------------------------------")
|
|
|
|
chr_cache = props.add_character_cache()
|
|
|
|
rig_name = chr_rig.name
|
|
character_name = rig_name
|
|
if "_Rigify" in character_name:
|
|
character_name = character_name.replace("_Rigify", "")
|
|
|
|
utils.log_info(f"Using character name: {character_name}")
|
|
|
|
if "rl_generation" in chr_rig:
|
|
generation = chr_rig["rl_generation"]
|
|
else:
|
|
generation = rigutils.get_rig_generation(chr_rig)
|
|
|
|
meta_rig_name = character_name + "_metarig"
|
|
meta_rig = None
|
|
if meta_rig_name in bpy.data.objects:
|
|
if utils.object_exists_is_armature(bpy.data.objects[meta_rig_name]):
|
|
meta_rig = bpy.data.objects[meta_rig_name]
|
|
|
|
chr_cache.import_file = fbx_path
|
|
chr_cache.character_name = character_name
|
|
chr_cache.import_embedded = False
|
|
chr_cache.generation = generation
|
|
chr_cache.non_standard_type = "HUMANOID"
|
|
chr_cache.rigified = True
|
|
chr_cache.rig_meta_rig = meta_rig
|
|
chr_cache.rigified_full_face_rig = character_has_bones(chr_rig, ["nose", "lip.T", "lip.B"])
|
|
chr_cache.add_object_cache(chr_rig)
|
|
|
|
rebuild_character_cache(chr_cache, chr_rig, objects)
|
|
|
|
return chr_cache
|
|
|
|
|
|
def reconnect_rl_character_to_blend(chr_rig, blend_file):
|
|
props = vars.props()
|
|
prefs = vars.prefs()
|
|
|
|
objects = get_character_objects(chr_rig)
|
|
|
|
utils.log_info("")
|
|
utils.log_info("Re-connecting Character to Blend File:")
|
|
utils.log_info("--------------------------------------")
|
|
|
|
rig_name = chr_rig.name
|
|
character_name = rig_name
|
|
if "_Rigify" in character_name:
|
|
character_name = character_name.replace("_Rigify", "")
|
|
utils.log_info(f"Using character name: {character_name}")
|
|
|
|
# link or append character data from blend file
|
|
with bpy.data.libraries.load(blend_file) as (src, dst):
|
|
dst.scenes = src.scenes
|
|
|
|
# find the add-on character data in the blend file
|
|
src_props = None
|
|
src_cache = None
|
|
for scene in dst.scenes:
|
|
if "CC3ImportProps" in scene:
|
|
src_props = scene.CC3ImportProps
|
|
utils.log_info(f"Found Add-on Import Properties")
|
|
break
|
|
|
|
if src_props:
|
|
|
|
# try to find the source cache by import file
|
|
if "rl_import_file" in chr_rig:
|
|
import_file = chr_rig["rl_import_file"]
|
|
for chr_cache in src_props.import_cache:
|
|
if chr_cache.import_file == import_file:
|
|
utils.log_info(f"Found matching source character fbx: {chr_cache.character_name}")
|
|
src_cache = chr_cache
|
|
break
|
|
|
|
# try to find the source cache by character name
|
|
for chr_cache in src_props.import_cache:
|
|
if chr_cache.character_name == character_name:
|
|
utils.log_info(f"Found matching source character name: {chr_cache.character_name}")
|
|
src_cache = chr_cache
|
|
break
|
|
|
|
if src_cache:
|
|
|
|
# create the character cache and rebuild from the source data
|
|
chr_cache = props.add_character_cache(copy_from=src_cache)
|
|
# can't match objects accurately, so don't try (as they are no longer the same linked objects)
|
|
rebuild_character_cache(chr_cache, chr_rig, objects, src_cache=src_cache)
|
|
return chr_cache
|
|
|
|
return None
|
|
|
|
|
|
def rebuild_character_cache(chr_cache, chr_rig, objects, src_cache=None):
|
|
props = vars.props()
|
|
prefs = vars.prefs()
|
|
|
|
if chr_rig:
|
|
chr_cache.add_object_cache(chr_rig)
|
|
|
|
utils.log_info("")
|
|
utils.log_info("Re-building Character Cache:")
|
|
utils.log_info("----------------------------")
|
|
|
|
errors = []
|
|
json_data = jsonutils.read_json(chr_cache.import_file, errors)
|
|
chr_json = jsonutils.get_character_json(json_data, chr_cache.get_character_id())
|
|
|
|
# add child objects to chr_cache
|
|
processed = []
|
|
defaults = []
|
|
for obj in objects:
|
|
obj_id = utils.get_rl_object_id(obj)
|
|
if utils.object_exists_is_mesh(obj) and obj not in processed:
|
|
processed.append(obj)
|
|
src_obj_cache = src_cache.get_object_cache(obj, by_id=obj_id) if src_cache else None
|
|
utils.log_info(f"Object: {obj.name} {obj_id} {src_obj_cache}")
|
|
obj_json = jsonutils.get_object_json(chr_json, obj)
|
|
obj_cache = chr_cache.add_object_cache(obj, copy_from=src_obj_cache)
|
|
for mat in obj.data.materials:
|
|
if mat and mat.node_tree is not None:
|
|
mat_id = mat["rl_material_id"] if "rl_material_id" in mat else None
|
|
src_mat_cache = src_cache.get_material_cache(mat, by_id=mat_id) if src_cache else None
|
|
utils.log_info(f"Material: {mat.name} {mat_id} {src_mat_cache}")
|
|
if src_obj_cache and src_mat_cache:
|
|
object_type = src_obj_cache.object_type
|
|
material_type = src_mat_cache.material_type
|
|
elif "rl_object_type" in obj and "rl_material_type" in mat:
|
|
object_type = obj["rl_object_type"]
|
|
material_type = mat["rl_material_type"]
|
|
else:
|
|
object_type, material_type = materials.detect_materials(chr_cache, obj, mat, obj_json)
|
|
if obj_cache.object_type != "BODY":
|
|
obj_cache.set_object_type(object_type)
|
|
if mat not in processed:
|
|
mat_cache = chr_cache.add_material_cache(mat, material_type, copy_from=src_mat_cache)
|
|
mat_cache.dir = imageutils.get_material_tex_dir(chr_cache, obj, mat)
|
|
physics.detect_physics(chr_cache, obj, obj_cache, mat, mat_cache, chr_json)
|
|
processed.append(mat)
|
|
if not src_obj_cache or not src_mat_cache:
|
|
defaults.append(mat)
|
|
|
|
# re-initialize the shader parameters (if not copied over)
|
|
if defaults:
|
|
shaders.init_character_property_defaults(chr_cache, chr_json, only=defaults)
|
|
if not src_cache:
|
|
basic.init_basic_default(chr_cache)
|
|
|
|
return chr_cache
|
|
|
|
|
|
def parent_to_rig(rig, obj):
|
|
"""For if the object is not parented to the rig and/or does not have an armature modifier set to the rig.
|
|
"""
|
|
|
|
if rig and obj and rig.type == "ARMATURE" and obj.type == "MESH":
|
|
|
|
if obj.parent != rig:
|
|
|
|
# clear any parenting
|
|
if obj.parent:
|
|
if utils.set_active_object(obj):
|
|
bpy.ops.object.parent_clear(type = "CLEAR_KEEP_TRANSFORM")
|
|
|
|
# parent to rig
|
|
if rig:
|
|
if utils.try_select_objects([rig, obj]):
|
|
if utils.set_active_object(rig):
|
|
bpy.ops.object.parent_set(type = "OBJECT", keep_transform = True)
|
|
|
|
# add or update armature modifier
|
|
arm_mod = modifiers.get_object_modifier(obj, "ARMATURE")
|
|
if not arm_mod:
|
|
arm_mod: bpy.types.ArmatureModifier = modifiers.get_armature_modifier(obj, create=True, armature=rig)
|
|
modifiers.move_mod_first(obj, arm_mod)
|
|
|
|
# update armature modifier rig
|
|
if arm_mod and arm_mod.object != rig:
|
|
arm_mod.object = rig
|
|
|
|
utils.clear_selected_objects()
|
|
utils.set_active_object(obj)
|
|
|
|
|
|
def add_object_to_character(chr_cache, obj : bpy.types.Object, reparent=True, no_materials=False):
|
|
props = vars.props()
|
|
|
|
if chr_cache and utils.object_exists_is_mesh(obj):
|
|
|
|
obj_cache = chr_cache.get_object_cache(obj)
|
|
|
|
if not obj_cache:
|
|
|
|
# convert the object name to remove any duplicate suffixes:
|
|
obj_name = utils.unique_object_name(obj.name, obj)
|
|
if obj.name != obj_name:
|
|
obj.name = obj_name
|
|
|
|
# add the object into the object cache
|
|
obj_cache = chr_cache.add_object_cache(obj)
|
|
if "rl_object_type" in obj:
|
|
obj_cache.set_object_type(obj["rl_object_type"])
|
|
else:
|
|
obj_cache.set_object_type("DEFAULT")
|
|
obj_cache.user_added = True
|
|
|
|
obj_cache.disabled = False
|
|
|
|
if not no_materials:
|
|
add_missing_materials_to_character(chr_cache, obj, obj_cache)
|
|
|
|
utils.clear_selected_objects()
|
|
|
|
if reparent:
|
|
arm = chr_cache.get_armature()
|
|
if arm:
|
|
parent_to_rig(arm, obj)
|
|
|
|
|
|
def remove_object_from_character(chr_cache, obj):
|
|
props = vars.props()
|
|
|
|
if utils.object_exists_is_mesh(obj):
|
|
|
|
obj_cache = chr_cache.get_object_cache(obj)
|
|
|
|
if obj_cache and obj_cache.object_type not in MANDATORY_OBJECTS:
|
|
|
|
obj_cache.disabled = True
|
|
|
|
# unparent from character
|
|
arm = chr_cache.get_armature()
|
|
if arm:
|
|
if utils.try_select_objects([arm, obj]):
|
|
if utils.set_active_object(arm):
|
|
bpy.ops.object.parent_clear(type = "CLEAR_KEEP_TRANSFORM")
|
|
|
|
# remove armature modifier
|
|
arm_mod : bpy.types.ArmatureModifier = modifiers.get_object_modifier(obj, "ARMATURE")
|
|
if arm_mod:
|
|
obj.modifiers.remove(arm_mod)
|
|
|
|
#utils.hide(obj)
|
|
|
|
utils.clear_selected_objects()
|
|
# don't reselect the removed object as this may cause
|
|
# onfusion when using checking function immediately after...
|
|
#utils.set_active_object(obj)
|
|
|
|
|
|
def copy_objects_character_to_character(context_obj, chr_cache, objects, reparent = True):
|
|
props = vars.props()
|
|
|
|
arm = chr_cache.get_armature()
|
|
if not arm:
|
|
return
|
|
|
|
context_collections = utils.get_object_scene_collections(context_obj)
|
|
|
|
to_copy = {}
|
|
for obj in objects:
|
|
if utils.object_exists_is_mesh(obj):
|
|
cc = props.get_character_cache(obj, None)
|
|
if cc != chr_cache:
|
|
if cc not in to_copy:
|
|
to_copy[cc] = []
|
|
to_copy[cc].append(obj)
|
|
|
|
copied_objects = []
|
|
|
|
for cc in to_copy:
|
|
for o in to_copy[cc]:
|
|
oc = cc.get_object_cache(o)
|
|
|
|
# copy object
|
|
obj = utils.duplicate_object(o)
|
|
utils.move_object_to_scene_collections(obj, context_collections)
|
|
copied_objects.append(obj)
|
|
|
|
# convert the object name to remove any duplicate suffixes:
|
|
obj_name = utils.unique_object_name(obj.name, obj)
|
|
if obj.name != obj_name:
|
|
obj.name = obj_name
|
|
|
|
# add the object into the object cache
|
|
obj_cache = chr_cache.add_object_cache(obj, copy_from=oc, user=True)
|
|
obj_cache.user_added = True
|
|
obj_cache.disabled = False
|
|
|
|
add_missing_materials_to_character(chr_cache, obj, obj_cache)
|
|
|
|
if reparent:
|
|
parent_to_rig(arm, obj)
|
|
|
|
utils.clear_selected_objects()
|
|
utils.try_select_objects(copied_objects, make_active=True)
|
|
|
|
|
|
def get_accessory_root(chr_cache, object):
|
|
"""Accessories can be identified by them having only vertex groups not listed in the bone mappings for this generation."""
|
|
|
|
if not chr_cache or not object:
|
|
return None
|
|
|
|
# none of this works if rigified...
|
|
if chr_cache.rigified:
|
|
return None
|
|
|
|
if not chr_cache or not object or not utils.object_exists_is_mesh(object):
|
|
return None
|
|
|
|
rig = chr_cache.get_armature()
|
|
bone_mapping = chr_cache.get_rig_bone_mapping()
|
|
|
|
if not rig or not bone_mapping:
|
|
return None
|
|
|
|
accessory_root = None
|
|
|
|
# accessories can be identified by them having only vertex groups not listed in the bone mappings for this generation.
|
|
for vg in object.vertex_groups:
|
|
|
|
# if even one vertex groups belongs to the character bones, it will not import into cc4 as an accessory
|
|
if bones.bone_mapping_contains_bone(bone_mapping, vg.name):
|
|
return None
|
|
|
|
else:
|
|
bone = bones.get_bone(rig, vg.name)
|
|
if bone:
|
|
root = bones.get_accessory_root_bone(bone_mapping, bone)
|
|
if root:
|
|
accessory_root = root
|
|
|
|
return accessory_root
|
|
|
|
|
|
def make_accessory(chr_cache, objects):
|
|
prefs = vars.prefs()
|
|
|
|
rig = chr_cache.get_armature()
|
|
|
|
# store parent objects (as the parenting is destroyed when adding objects to character)
|
|
obj_data = {}
|
|
for obj in objects:
|
|
if obj.type == "MESH":
|
|
obj_data[obj] = {
|
|
"parent_object": obj.parent,
|
|
"matrix_world": obj.matrix_world.copy()
|
|
}
|
|
|
|
# add any non character objects to character
|
|
for obj in objects:
|
|
obj_cache = chr_cache.get_object_cache(obj)
|
|
if not obj_cache:
|
|
utils.log_info(f"Adding {obj.name} to character.")
|
|
add_object_to_character(chr_cache, obj, True,
|
|
no_materials=not prefs.auto_convert_materials)
|
|
obj_cache = chr_cache.get_object_cache(obj)
|
|
else:
|
|
parent_to_rig(rig, obj)
|
|
|
|
cursor_pos = bpy.context.scene.cursor.location
|
|
if utils.try_select_objects(objects, True, "MESH", True):
|
|
if utils.set_mode("EDIT"):
|
|
bpy.ops.mesh.select_all(action='SELECT')
|
|
bpy.ops.view3d.snap_cursor_to_selected()
|
|
|
|
if rig and utils.edit_mode_to(rig, only_this = True):
|
|
|
|
# add accessory named bone to rig
|
|
accessory_root = rig.data.edit_bones.new("Accessory")
|
|
root_head = rig.matrix_world.inverted() @ bpy.context.scene.cursor.location
|
|
root_tail = rig.matrix_world.inverted() @ (bpy.context.scene.cursor.location + Vector((0, 1/4, 0)))
|
|
piv_tail = rig.matrix_world.inverted() @ (bpy.context.scene.cursor.location + Vector((0, 1/10000, 0)))
|
|
utils.log_info(f"Adding accessory root bone: {accessory_root.name}/({root_head})")
|
|
accessory_root.head = root_head
|
|
accessory_root.tail = root_tail
|
|
|
|
default_parent = bones.get_rl_edit_bone(rig, chr_cache.accessory_parent_bone)
|
|
accessory_root.parent = default_parent
|
|
|
|
for obj in objects:
|
|
if obj.type == "MESH":
|
|
|
|
# add object bone to rig
|
|
obj_bone = rig.data.edit_bones.new(obj.name)
|
|
obj_head = rig.matrix_world.inverted() @ (obj.matrix_world @ Vector((0, 0, 0)))
|
|
obj_tail = rig.matrix_world.inverted() @ ((obj.matrix_world @ Vector((0, 0, 0))) + Vector((0, 1/8, 0)))
|
|
utils.log_info(f"Adding object bone: {obj_bone.name}/({obj_head})")
|
|
obj_bone.head = obj_head
|
|
obj_bone.tail = obj_tail
|
|
|
|
# add pivot bone to rig
|
|
#piv_bone = rig.data.edit_bones.new("CC_Base_Pivot")
|
|
#utils.log_info(f"Adding pivot bone: {piv_bone.name}/({root_head})")
|
|
#piv_bone.head = root_head + Vector((0, 1/100, 0))
|
|
#piv_bone.tail = piv_tail + Vector((0, 1/100, 0))
|
|
#piv_bone.parent = obj_bone
|
|
|
|
# add deformation bone to rig
|
|
def_bone = rig.data.edit_bones.new(obj.name)
|
|
utils.log_info(f"Adding deformation bone: {def_bone.name}/({obj_head})")
|
|
def_head = rig.matrix_world.inverted() @ ((obj.matrix_world @ Vector((0, 0, 0))) + Vector((0, 1/32, 0)))
|
|
def_tail = rig.matrix_world.inverted() @ ((obj.matrix_world @ Vector((0, 0, 0))) + Vector((0, 1/32 + 1/16, 0)))
|
|
def_bone.head = def_head
|
|
def_bone.tail = def_tail
|
|
def_bone.parent = obj_bone
|
|
|
|
# remove all vertex groups from object
|
|
obj.vertex_groups.clear()
|
|
|
|
# add vertex groups for object bone
|
|
vg = meshutils.add_vertex_group(obj, def_bone.name)
|
|
meshutils.set_vertex_group(obj, vg, 1.0)
|
|
|
|
obj_data[obj]["bone"] = obj_bone
|
|
obj_data[obj]["def_bone"] = def_bone
|
|
|
|
# parent the object bone to the accessory bone (or object transform parent bone)
|
|
for obj in objects:
|
|
if obj.type == "MESH" and obj in obj_data.keys():
|
|
# fetch the object's bone
|
|
obj_bone = obj_data[obj]["bone"]
|
|
# find the parent bone to the object (if exists)
|
|
parent_bone = None
|
|
obj_parent = obj_data[obj]["parent_object"]
|
|
if obj_parent and obj_parent in obj_data.keys():
|
|
parent_bone = obj_data[obj_parent]["bone"]
|
|
# parent the bone
|
|
if parent_bone:
|
|
utils.log_info(f"Parenting {obj.name} to {parent_bone.name}")
|
|
obj_bone.parent = parent_bone
|
|
else:
|
|
utils.log_info(f"Parenting {obj.name} to {accessory_root.name}")
|
|
obj_bone.parent = accessory_root
|
|
|
|
# object mode to save new bones
|
|
utils.object_mode()
|
|
|
|
|
|
bpy.context.scene.cursor.location = cursor_pos
|
|
return
|
|
|
|
def clean_up_character_data(chr_cache):
|
|
|
|
props = vars.props()
|
|
|
|
current_mats = []
|
|
current_objects = []
|
|
arm = chr_cache.get_armature()
|
|
report = []
|
|
|
|
chr_cache.validate(report)
|
|
chr_cache.clean_up()
|
|
|
|
if len(report) > 0:
|
|
utils.message_box_multi("Cleanup Report", "INFO", report)
|
|
else:
|
|
utils.message_box("Nothing to clean up.", "Cleanup Report", "INFO")
|
|
|
|
|
|
def has_missing_materials(chr_cache):
|
|
missing_materials = False
|
|
if chr_cache:
|
|
for obj in chr_cache.get_cache_objects():
|
|
obj_cache = chr_cache.get_object_cache(obj)
|
|
obj = obj_cache.get_mesh()
|
|
if obj and not chr_cache.has_all_materials(obj.data.materials):
|
|
missing_materials = True
|
|
return missing_materials
|
|
|
|
|
|
def add_missing_materials_to_character(chr_cache, obj, obj_cache):
|
|
props = vars.props()
|
|
|
|
if chr_cache and obj and obj_cache and obj.type == "MESH":
|
|
|
|
obj_name = obj.name
|
|
|
|
# add a default material if none exists...
|
|
if len(obj.data.materials) == 0:
|
|
mat_name = utils.unique_material_name(obj_name)
|
|
mat = bpy.data.materials.new(mat_name)
|
|
obj.data.materials.append(mat)
|
|
|
|
for mat in obj.data.materials:
|
|
if mat:
|
|
mat_cache = chr_cache.get_material_cache(mat)
|
|
|
|
if not mat_cache:
|
|
add_material_to_character(chr_cache, obj, obj_cache, mat, update_name=True)
|
|
|
|
|
|
def add_material_to_character(chr_cache, obj, obj_cache, mat, update_name = False):
|
|
props = vars.props()
|
|
|
|
if chr_cache and obj and obj_cache and mat:
|
|
|
|
# find existing cache in character
|
|
mat_cache = chr_cache.get_material_cache(mat)
|
|
if mat_cache:
|
|
return mat_cache
|
|
|
|
# find existing cache in any other character
|
|
existing_mat_cache = props.get_material_cache(mat)
|
|
if existing_mat_cache:
|
|
# copy it
|
|
mat_cache = chr_cache.add_material_cache(mat, copy_from=existing_mat_cache)
|
|
return mat_cache
|
|
|
|
if materials.is_rl_material(mat):
|
|
mat_cache = materials.reconstruct_material_cache(chr_cache, mat)
|
|
return mat_cache
|
|
|
|
# convert the material name to remove any duplicate suffixes:
|
|
if update_name:
|
|
mat_name = utils.unique_material_name(mat.name, mat)
|
|
if mat.name != mat_name:
|
|
mat.name = mat_name
|
|
|
|
# make sure there are nodes:
|
|
if not mat.use_nodes:
|
|
mat.use_nodes = True
|
|
|
|
# add the material into the material cache
|
|
mat_cache = chr_cache.add_material_cache(mat, "DEFAULT", is_user=True)
|
|
mat_cache.user_added = True
|
|
|
|
# convert any existing PrincipledBSDF based material to a rl_pbr shader material
|
|
# can treat existing textures as embedded textures, so they will be picked up by the material builder.
|
|
materials.detect_embedded_textures(chr_cache, obj, obj_cache, mat, mat_cache)
|
|
# finally connect up the pbr shader...
|
|
#shaders.connect_pbr_shader(obj, mat, None)
|
|
convert_to_rl_pbr(mat, mat_cache)
|
|
|
|
return mat_cache
|
|
|
|
|
|
def convert_to_rl_pbr(mat, mat_cache):
|
|
shader_group = "rl_pbr_shader"
|
|
shader_name = "rl_pbr_shader"
|
|
shader_id = "(" + str(shader_name) + ")"
|
|
bsdf_id = "(" + str(shader_name) + "_BSDF)"
|
|
|
|
group_node: bpy.types.Node = None
|
|
bsdf_node: bpy.types.Node = None
|
|
output_node: bpy.types.Node = None
|
|
gltf_node: bpy.types.Node = None
|
|
too_complex = False
|
|
|
|
nodes = mat.node_tree.nodes
|
|
links = mat.node_tree.links
|
|
|
|
n : bpy.types.ShaderNode
|
|
for n in nodes:
|
|
|
|
if n.type == "BSDF_PRINCIPLED":
|
|
|
|
if not bsdf_node:
|
|
utils.log_info("Found BSDF: " + n.name)
|
|
bsdf_node = n
|
|
else:
|
|
too_complex = True
|
|
|
|
elif n.type == "GROUP" and n.node_tree and shader_name in n.name and vars.VERSION_STRING in n.node_tree.name:
|
|
|
|
if not group_node:
|
|
utils.log_info("Found Shader Node: " + n.name)
|
|
group_node = n
|
|
else:
|
|
too_complex = True
|
|
|
|
elif n.type == "GROUP" and n.node_tree and ("glTF Settings" in n.node_tree.name or
|
|
"glTF Material Output" in n.node_tree.name):
|
|
if not gltf_node:
|
|
gltf_node = n
|
|
utils.log_info("GLTF settings node found: " + n.name)
|
|
else:
|
|
too_complex = True
|
|
|
|
elif n.type == "OUTPUT_MATERIAL":
|
|
|
|
if output_node:
|
|
nodes.remove(n)
|
|
else:
|
|
output_node = n
|
|
|
|
if too_complex:
|
|
utils.log_warn(f"Material {mat.name} is too complex to convert!")
|
|
return
|
|
|
|
# move all the nodes back to accomodate the group shader node
|
|
for n in nodes:
|
|
loc = n.location
|
|
n.location = [loc[0] - 600, loc[1]]
|
|
|
|
# make group node if none
|
|
# ensure correct names so find_shader_nodes can find them
|
|
if not group_node:
|
|
group = nodeutils.get_node_group(shader_group)
|
|
group_node = nodes.new("ShaderNodeGroup")
|
|
group_node.node_tree = group
|
|
group_node.name = utils.unique_name(shader_id)
|
|
group_node.label = "Pbr Shader"
|
|
group_node.width = 240
|
|
group_node.location = (-400, 0)
|
|
|
|
# make bsdf node if none
|
|
if not bsdf_node:
|
|
bsdf_node = nodes.new("ShaderNodeBsdfPrincipled")
|
|
bsdf_node.name = utils.unique_name(bsdf_id)
|
|
bsdf_node.label = "Pbr Shader"
|
|
bsdf_node.width = 240
|
|
bsdf_node.location = (200, 400)
|
|
|
|
# make output node if none
|
|
if not output_node:
|
|
output_node = nodes.new("ShaderNodeOutputMaterial")
|
|
output_node.location = (900, -400)
|
|
|
|
# remap bsdf socket inputs to shader group node sockets
|
|
# [ [bsdf_socket_name<:parent_socket_name>, node_name_match, node_source, group_socket, strength_value_trace, prop_name], ]
|
|
SOCKETS = [
|
|
["Base Color", "", "BSDF", "Diffuse Map", "", ""],
|
|
["Metallic", "", "BSDF", "Metallic Map", "", ""],
|
|
["Specular", "", "BSDF", "Specular Map", "", ""],
|
|
["Roughness", "", "BSDF", "Roughness Map", "", ""],
|
|
["Emission", "", "BSDF", "Emission Map", "", ""],
|
|
["Alpha", "", "BSDF", "Alpha Map", "", ""],
|
|
["Normal:Color", "", "BSDF", "Normal Map", "Normal:Strength", "default_normal_strength"], # normal image > normal map (Color) > BSDF (Normal)
|
|
["Normal:Normal:Color", "", "BSDF", "Normal Map", ["Normal:Normal:Strength", "Normal:Strength"], "default_normal_strength"], # normal image > normal map (Color) > bump map (Normal) > BSDF (Normal)
|
|
["Normal:Height", "", "BSDF", "Bump Map", ["Normal:Distance", "Normal:Strength"], "default_bump_strength"], # bump image > bump map (Height) > BSDF (Normal)
|
|
["Base Color:Color2", "ao|occlusion", "BSDF", "AO Map", "Base Color:Fac", "default_ao_strength"],
|
|
["Base Color:Color1", "ao|occlusion", "BSDF", "Diffuse Map", "", ""],
|
|
#["Base Color:Color2", "#mixmultiply", "BSDF", "AO Map", "Base Color:Fac", "default_ao_strength"],
|
|
#["Base Color:Color1", "#mixmultiply", "BSDF", "Diffuse Map", "", ""],
|
|
# blender 3.0+
|
|
#["Base Color:B", "#mixmultiply", "BSDF", "AO Map", "Base Color:Factor", "default_ao_strength"],
|
|
#["Base Color:A", "#mixmultiply", "BSDF", "Diffuse Map", "", ""],
|
|
["Base Color:B", "ao|occlusion", "BSDF", "AO Map", "Base Color:Factor", "default_ao_strength"],
|
|
["Base Color:A", "ao|occlusion", "BSDF", "Diffuse Map", "", ""],
|
|
# gltf
|
|
["Occlusion", "", "GLTF", "AO Map", "", ""],
|
|
["Occlusion:Color2", "", "GLTF", "AO Map", "Occlusion:Fac", "default_ao_strength"],
|
|
["Occlusion:B", "", "GLTF", "AO Map", "Occlusion:Factor", "default_ao_strength"],
|
|
]
|
|
|
|
EMBEDDED = [
|
|
["DIFFUSE", "Diffuse Map"],
|
|
["SPECULAR", "Specular Map"],
|
|
["METALLIC", "Metallic Map"],
|
|
["ROUGHNESS", "Roughness Map"],
|
|
["EMISSION", "Emission Map"],
|
|
["ALPHA", "Alpha Map"],
|
|
["BUMP", "Bump Map"],
|
|
["NORMAL", "Normal Map"],
|
|
]
|
|
|
|
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")
|
|
transmission_socket = nodeutils.input_socket(bsdf_node, "Transmission")
|
|
emission_strength_socket = nodeutils.input_socket(bsdf_node, "Emission Strength")
|
|
clearcoat_value = clearcoat_socket.default_value
|
|
roughness_value = roughness_socket.default_value
|
|
metallic_value = metallic_socket.default_value
|
|
specular_value = specular_socket.default_value
|
|
alpha_value = alpha_socket.default_value
|
|
|
|
if gltf_node:
|
|
# bug in Blender 4.0 gltf occlusion is not connected from occlusion strength node
|
|
if utils.B400():
|
|
gltf_occlusion_socket = nodeutils.input_socket(gltf_node, "Occlusion")
|
|
occlusion_strength_node = nodeutils.find_node_by_type_and_keywords(nodes, "MIX", "Occlusion Strength")
|
|
if gltf_occlusion_socket and not gltf_occlusion_socket.is_linked:
|
|
nodeutils.link_nodes(links, occlusion_strength_node, "Result", gltf_node, gltf_occlusion_socket)
|
|
|
|
if utils.B293():
|
|
emission_value = nodeutils.get_node_input_value(bsdf_node, emission_strength_socket, 0.0)
|
|
else:
|
|
if emission_socket.is_linked:
|
|
emission_value = nodeutils.get_node_input_value(bsdf_node, emission_strength_socket, 1.0)
|
|
else:
|
|
emission_value = 0.0
|
|
emission_color = nodeutils.get_node_input_value(bsdf_node, emission_socket, (0,0,0))
|
|
|
|
if not base_color_socket.is_linked:
|
|
diffuse_color = base_color_socket.default_value
|
|
mat_cache.parameters.default_diffuse_color = diffuse_color
|
|
|
|
if transmission_socket.is_linked:
|
|
nodeutils.unlink_node_input(links, bsdf_node, transmission_socket)
|
|
|
|
mat_cache.parameters.default_roughness = roughness_value
|
|
# a rough approximation for the clearcoat
|
|
mat_cache.parameters.default_roughness_power = 1.0 + clearcoat_value
|
|
mat_cache.parameters.default_metallic = metallic_value
|
|
mat_cache.parameters.default_specular = specular_value
|
|
mat_cache.parameters.default_emission_strength = emission_value / vars.EMISSION_SCALE
|
|
mat_cache.parameters.default_emissive_color = emission_color
|
|
if emission_strength_socket:
|
|
emission_strength_socket.default_value = 1.0
|
|
clearcoat_socket.default_value = 0.0
|
|
if not alpha_socket.is_linked:
|
|
mat_cache.parameters.default_opacity = alpha_value
|
|
except:
|
|
utils.log_warn("Unable to set material cache defaults!")
|
|
|
|
socket_mapping = {}
|
|
for socket_trace, match, node_type, group_socket, strength_trace, strength_prop in SOCKETS:
|
|
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)
|
|
linked_to = nodeutils.get_node_connected_to_output(linked_node, linked_socket)
|
|
|
|
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 group_socket == "Bump Map":
|
|
strength = min(2, max(0, strength * 100.0))
|
|
elif group_socket == "Normal Map":
|
|
strength = min(2, max(0, strength))
|
|
else:
|
|
strength = min(1, max(0, strength))
|
|
|
|
if linked_node and linked_socket:
|
|
if match:
|
|
found = False
|
|
if match[0] == "#" and linked_to:
|
|
if match[1:] == "mixmultiply" and linked_to.type == "MIX" and linked_to.blend_type == "MULTIPLY":
|
|
found = True
|
|
else:
|
|
if re.match(match, linked_node.label) or re.match(match, linked_node.name):
|
|
found = True
|
|
elif linked_node.type == "TEX_IMAGE" and re.match(match, linked_node.image.name):
|
|
found = True
|
|
if found:
|
|
socket_mapping[group_socket] = [linked_node, linked_socket, strength, strength_prop]
|
|
else:
|
|
socket_mapping[group_socket] = [linked_node, linked_socket, strength, strength_prop]
|
|
|
|
for tex_type, group_socket in EMBEDDED:
|
|
if group_socket not in socket_mapping:
|
|
cache_mapping = mat_cache.get_texture_mapping(tex_type)
|
|
if cache_mapping:
|
|
linked_node = nodeutils.find_node_by_image(nodes, cache_mapping.image)
|
|
socket = "Color"
|
|
if tex_type == "ALPHA" and mat_cache.alpha_is_diffuse:
|
|
socket = "Alpha"
|
|
socket_mapping[group_socket] = [linked_node, socket, 1.0, ""]
|
|
|
|
# connect the shader group node sockets
|
|
for socket_name in socket_mapping:
|
|
linked_info = socket_mapping[socket_name]
|
|
linked_node = linked_info[0]
|
|
linked_socket = linked_info[1]
|
|
strength = linked_info[2]
|
|
strength_prop = linked_info[3]
|
|
nodeutils.link_nodes(links, linked_node, linked_socket, group_node, socket_name)
|
|
if strength_prop:
|
|
utils.log_info(f"setting {strength_prop} = {strength}")
|
|
shaders.exec_prop(strength_prop, mat_cache, strength)
|
|
|
|
if bsdf_node and group_node and mat_cache:
|
|
shaders.apply_prop_matrix(bsdf_node, group_node, mat_cache, "rl_pbr_shader")
|
|
|
|
# connect all group_node outputs to BSDF inputs:
|
|
for socket in group_node.outputs:
|
|
to_socket = nodeutils.input_socket(bsdf_node, socket.name)
|
|
nodeutils.link_nodes(links, group_node, socket.name, bsdf_node, to_socket)
|
|
|
|
# connect bsdf to output node
|
|
nodeutils.link_nodes(links, bsdf_node, "BSDF", output_node, "Surface")
|
|
|
|
# connect the displacement to the output
|
|
nodeutils.link_nodes(links, group_node, "Displacement", output_node, "Displacement")
|
|
|
|
# use alpha hashing by default
|
|
materials.set_material_alpha(mat, "HASHED")
|
|
|
|
return
|
|
|
|
|
|
def character_has_bones(arm, bone_list: list):
|
|
if not arm: return False
|
|
if not bone_list: return False
|
|
for bone_name in bone_list:
|
|
if not utils.find_pose_bone_in_armature(arm, bone_name):
|
|
return False
|
|
return True
|
|
|
|
|
|
def character_has_materials(arm, material_list: list):
|
|
if not arm: return False
|
|
if not material_list: return False
|
|
for material_name in material_list:
|
|
material_name = material_name.lower()
|
|
has_material = False
|
|
for obj in arm.children:
|
|
if utils.object_exists_is_mesh(obj):
|
|
for mat in obj.data.materials:
|
|
mat_name = utils.strip_name(mat.name).lower()
|
|
if mat_name == material_name:
|
|
has_material = True
|
|
if not has_material:
|
|
return False
|
|
return True
|
|
|
|
|
|
def get_character_material_names(arm):
|
|
mat_names = []
|
|
if arm:
|
|
for obj in arm.children:
|
|
if utils.object_exists_is_mesh(obj):
|
|
for mat in obj.data.materials:
|
|
mat_name = mat.name #utils.strip_name(mat.name)
|
|
if mat_name not in mat_names:
|
|
mat_names.append(mat_name)
|
|
return mat_names
|
|
|
|
|
|
def get_character_object_names(arm):
|
|
obj_names = []
|
|
if arm:
|
|
for obj in arm.children:
|
|
if utils.object_exists_is_mesh(obj):
|
|
obj_name = obj.name #utils.strip_name(obj.name)
|
|
if obj_name not in obj_names:
|
|
obj_names.append(obj_name)
|
|
return obj_names
|
|
|
|
|
|
def get_combined_body(chr_cache):
|
|
combined = None
|
|
if chr_cache:
|
|
utils.object_mode()
|
|
body_objects = chr_cache.get_objects_of_type("BODY")
|
|
if len(body_objects) == 1:
|
|
combined = body_objects[0]
|
|
else:
|
|
copies = []
|
|
for body in body_objects:
|
|
copy = utils.duplicate_object(body)
|
|
copies.append(copy)
|
|
if copies:
|
|
combined = copies[0]
|
|
utils.try_select_objects(copies, clear_selection=True)
|
|
utils.set_active_object(combined)
|
|
bpy.ops.object.join()
|
|
if utils.edit_mode_to(combined):
|
|
bpy.ops.mesh.select_all(action = 'SELECT')
|
|
bpy.ops.mesh.remove_doubles()
|
|
utils.object_mode()
|
|
combined.name = body_objects[0].name + "_Combined"
|
|
combined["rl_combined_body"] = True
|
|
return combined
|
|
|
|
|
|
def finish_combined_body(combined):
|
|
if "rl_combined_body" in combined:
|
|
utils.delete_object(combined)
|
|
|
|
|
|
def remove_list_body_objects(chr_cache, objects):
|
|
body_objects = chr_cache.get_objects_of_type("BODY")
|
|
for body in body_objects:
|
|
if body in objects:
|
|
objects.remove(body)
|
|
return objects
|
|
|
|
|
|
def transfer_skin_weights(chr_cache, objects, body_override=None):
|
|
|
|
if not utils.object_mode():
|
|
return
|
|
|
|
arm = chr_cache.get_armature()
|
|
if not arm: return
|
|
|
|
body = body_override if body_override else get_combined_body(chr_cache)
|
|
if not body: return
|
|
|
|
objects = remove_list_body_objects(chr_cache, objects)
|
|
|
|
if arm.data.pose_position == "POSE":
|
|
|
|
# Transfer weights in place (in pose mode)
|
|
|
|
# apply pose to the body mesh (copy)
|
|
body_copy = utils.duplicate_object(body)
|
|
body_copy.shape_key_clear()
|
|
modifiers.apply_modifier(body_copy, type="ARMATURE")
|
|
|
|
# apply pose to the object meshes (copies)
|
|
objects_copy = []
|
|
for obj in objects:
|
|
obj_copy = utils.duplicate_object(obj)
|
|
obj_copy.shape_key_clear()
|
|
modifiers.apply_modifier(obj_copy, type="ARMATURE")
|
|
objects_copy.append(obj_copy)
|
|
|
|
# transfer weights from body_copy to obj_copy
|
|
utils.set_only_active_object(obj_copy)
|
|
utils.try_select_object(body_copy)
|
|
bpy.ops.object.data_transfer(use_reverse_transfer=True,
|
|
data_type='VGROUP_WEIGHTS',
|
|
use_create=True,
|
|
vert_mapping='POLYINTERP_NEAREST',
|
|
use_object_transform=True,
|
|
layers_select_src='NAME',
|
|
layers_select_dst='ALL',
|
|
mix_mode='REPLACE')
|
|
#utils.set_mode("WEIGHT_PAINT")
|
|
#bpy.ops.object.vertex_group_smooth(group_select_mode='ALL',
|
|
# factor=0.5, repeat=6, expand=0.5)
|
|
#utils.object_mode()
|
|
|
|
# make a copy of the armature and apply the current pose as the rest pose
|
|
arm_posed = utils.duplicate_object(arm)
|
|
utils.set_only_active_object(arm_posed)
|
|
utils.pose_mode_to(arm_posed)
|
|
bpy.ops.pose.armature_apply(selected=False)
|
|
|
|
# parent all the copied meshes to the posed armature
|
|
utils.try_select_object(body_copy, True)
|
|
utils.try_select_objects(objects_copy)
|
|
utils.set_active_object(arm_posed)
|
|
bpy.ops.object.parent_set(type="OBJECT", keep_transform=True)
|
|
# and add armature modifiers
|
|
modifiers.get_armature_modifier(body_copy, create=True, armature=arm_posed)
|
|
for obj_copy in objects_copy:
|
|
modifiers.get_armature_modifier(obj_copy, create=True, armature=arm_posed)
|
|
|
|
# make another copy of the armature and clear the pose
|
|
arm_rest = utils.duplicate_object(arm)
|
|
utils.set_only_active_object(arm_rest)
|
|
utils.safe_set_action(arm_rest, None)
|
|
utils.pose_mode_to(arm_rest)
|
|
bpy.ops.pose.select_all(action='SELECT')
|
|
bpy.ops.pose.transforms_clear()
|
|
|
|
# constrain the pose on the posed armature to the rest pose on the rest armature
|
|
# this poses the pose armature in the original bind pose
|
|
utils.set_only_active_object(arm_posed)
|
|
utils.pose_mode_to(arm_posed)
|
|
for pose_bone in arm_posed.pose.bones:
|
|
bones.add_copy_transforms_constraint(arm_rest, arm_posed, pose_bone.name, pose_bone.name)
|
|
|
|
# then visually apply that pose
|
|
# (this should pose the posed armature in the same pose as the original bind pose)
|
|
# *not needed
|
|
#utils.pose_mode_to(arm_posed)
|
|
#bpy.ops.pose.select_all(action='SELECT')
|
|
#bpy.ops.pose.visual_transform_apply()
|
|
|
|
# now apply the armature modifiers on the copied meshes
|
|
# so their base shape is now in the original bind pose
|
|
utils.set_only_active_object(body_copy)
|
|
modifiers.apply_modifier(body_copy, type="ARMATURE")
|
|
for obj_copy in objects_copy:
|
|
utils.set_only_active_object(obj_copy)
|
|
modifiers.apply_modifier(obj_copy, type="ARMATURE")
|
|
|
|
# parent the objects back to the original armature
|
|
utils.try_select_object(body_copy, True)
|
|
utils.try_select_objects(objects_copy)
|
|
utils.set_active_object(arm)
|
|
bpy.ops.object.parent_set(type="OBJECT", keep_transform=True)
|
|
# and add armature modifiers
|
|
modifiers.get_armature_modifier(body_copy, create=True, armature=arm)
|
|
# copy the new vertex positions and weights back to the original objects
|
|
for obj_copy in objects_copy:
|
|
modifiers.get_armature_modifier(obj_copy, create=True, armature=arm)
|
|
geom.copy_vertex_positions_and_weights(obj_copy, obj)
|
|
|
|
# done!
|
|
utils.delete_armature_object(arm_posed)
|
|
utils.delete_armature_object(arm_rest)
|
|
utils.delete_mesh_object(body_copy)
|
|
for obj_copy in objects_copy:
|
|
utils.delete_mesh_object(obj_copy)
|
|
|
|
else:
|
|
|
|
for obj in objects:
|
|
if obj.type == "MESH":
|
|
|
|
# remove all bone vertex groups from obj
|
|
for bone in arm.data.bones:
|
|
if bone.name in obj.vertex_groups:
|
|
vg = obj.vertex_groups[bone.name]
|
|
obj.vertex_groups.remove(vg)
|
|
|
|
if utils.try_select_object(body, True) and utils.set_active_object(obj):
|
|
|
|
bpy.ops.object.data_transfer(use_reverse_transfer=True,
|
|
data_type='VGROUP_WEIGHTS',
|
|
use_create=True,
|
|
vert_mapping='POLYINTERP_NEAREST',
|
|
use_object_transform=True,
|
|
layers_select_src='NAME',
|
|
layers_select_dst='ALL',
|
|
mix_mode='REPLACE')
|
|
|
|
if obj.parent != arm:
|
|
if utils.try_select_objects([arm, obj]) and utils.set_active_object(arm):
|
|
bpy.ops.object.parent_set(type = "OBJECT", keep_transform = True)
|
|
|
|
# add or update armature modifier
|
|
arm_mod : bpy.types.ArmatureModifier = modifiers.get_armature_modifier(obj, create=True, armature=arm)
|
|
if arm_mod:
|
|
modifiers.move_mod_first(obj, arm_mod)
|
|
arm_mod.object = arm
|
|
|
|
if not body_override:
|
|
finish_combined_body(body)
|
|
|
|
|
|
def normalize_skin_weights(chr_cache, objects):
|
|
|
|
if not utils.object_mode():
|
|
return
|
|
|
|
arm = chr_cache.get_armature()
|
|
if arm is None:
|
|
return
|
|
|
|
body = None
|
|
# TODO if the body mesh has been split, this isn't going to work...
|
|
for obj_cache in chr_cache.object_cache:
|
|
if obj_cache.object_type == "BODY":
|
|
body = obj_cache.get_object()
|
|
|
|
# don't allow normalize all to body mesh
|
|
if body and body in objects:
|
|
objects.remove(body)
|
|
|
|
selected = bpy.context.selected_objects.copy()
|
|
|
|
for obj in objects:
|
|
if obj.type == "MESH":
|
|
|
|
if utils.try_select_object(obj, True) and utils.set_active_object(obj):
|
|
|
|
bpy.ops.object.vertex_group_normalize_all()
|
|
|
|
utils.clear_selected_objects()
|
|
utils.try_select_objects(selected)
|
|
|
|
|
|
def blend_skin_weights(chr_cache, objects):
|
|
props = vars.props()
|
|
prefs = vars.prefs()
|
|
|
|
if not utils.object_mode():
|
|
return
|
|
|
|
arm = chr_cache.get_armature()
|
|
if not arm: return
|
|
body = get_combined_body(chr_cache)
|
|
if not body: return
|
|
|
|
objects = remove_list_body_objects(chr_cache, objects)
|
|
|
|
for obj in objects:
|
|
pose_mode = arm.data.pose_position
|
|
arm.data.pose_position = "REST"
|
|
bpy.context.view_layer.update()
|
|
bm_obj = geom.get_bmesh(obj.data)
|
|
layer_map = prep_deformation_layers(arm, body, obj, bm_obj)
|
|
transfer_skin_weights(chr_cache, objects, body_override=body)
|
|
bm_obj.free()
|
|
bm_obj = geom.get_bmesh(obj.data)
|
|
post_deformation_layers(obj, bm_obj, layer_map)
|
|
vert_map = geom.map_body_weight_blends(body, obj, bm_obj)
|
|
apply_weight_blend(obj, bm_obj, vert_map, layer_map,
|
|
prefs.weight_blend_distance_min,
|
|
prefs.weight_blend_distance_max,
|
|
prefs.weight_blend_distance_range,
|
|
prefs.weight_blend_use_range,
|
|
prefs.weight_blend_selected_only)
|
|
arm.data.pose_position = pose_mode
|
|
bpy.context.view_layer.update()
|
|
|
|
|
|
def prep_deformation_layers(arm, body: bpy.types.Object, obj: bpy.types.Object, bm_obj):
|
|
layer_map = {}
|
|
bones = []
|
|
for vg in body.vertex_groups:
|
|
if vg.name in obj.vertex_groups:
|
|
if vg.name in arm.data.bones and arm.data.bones[vg.name].use_deform:
|
|
bones.append(vg.name)
|
|
# TODO don't include face bones or twist parent bones
|
|
utils.log_info(f"Adding vertex group {vg.name} to {obj.name}")
|
|
meshutils.add_vertex_group(obj, vg.name)
|
|
id = utils.generate_random_id(4)
|
|
for i, vg in enumerate(obj.vertex_groups):
|
|
if vg.name in bones:
|
|
bone_name = vg.name
|
|
blend_weights = geom.fetch_vertex_layer_weights(bm_obj, i)
|
|
layer_map[bone_name] = { "blend": blend_weights }
|
|
return layer_map
|
|
|
|
|
|
def post_deformation_layers(obj: bpy.types.Object, bm_obj, layer_map):
|
|
"""Move the body transfered weights into new groups,
|
|
and create empty vertex groups for those bones"""
|
|
for bone_name in layer_map:
|
|
i = obj.vertex_groups.keys().index(bone_name)
|
|
skin_weights = geom.fetch_vertex_layer_weights(bm_obj, i)
|
|
layer_map[bone_name]["skin"] = skin_weights
|
|
|
|
|
|
def clean_up_blend_vertex_groups(obj, bm_obj, layer_map):
|
|
# TODO remove zero weights from vertex groups?
|
|
return
|
|
|
|
|
|
def apply_weight_blend(obj, bm_obj, vert_map, layer_map,
|
|
weight_blend_distance_min,
|
|
weight_blend_distance_max,
|
|
weight_blend_distance_range,
|
|
weight_blend_use_range,
|
|
weight_blend_selected_only):
|
|
d0 = weight_blend_distance_min
|
|
d1 = weight_blend_distance_max
|
|
if weight_blend_use_range:
|
|
max_d1 = 0
|
|
for v_idx in vert_map:
|
|
distance = vert_map[v_idx]
|
|
if distance > max_d1:
|
|
max_d1 = distance
|
|
d1 = max(d0, utils.lerp(d0, max_d1, weight_blend_distance_range / 100.0))
|
|
utils.log_info(f"Using weight blend range: {d0} to {d1}")
|
|
bm_obj.verts.layers.deform.verify()
|
|
obj_dl = bm_obj.verts.layers.deform.active
|
|
for bone_name in layer_map:
|
|
blend_weights = layer_map[bone_name]["blend"]
|
|
skin_weights = layer_map[bone_name]["skin"]
|
|
bone_layer = obj.vertex_groups.keys().index(bone_name)
|
|
for v_idx in vert_map:
|
|
distance = vert_map[v_idx]
|
|
if weight_blend_selected_only and not bm_obj.verts[v_idx].select:
|
|
distance = -1
|
|
if distance == -1:
|
|
try:
|
|
blended_weight = blend_weights[v_idx]
|
|
except:
|
|
blended_weight = 0
|
|
else:
|
|
try:
|
|
w0 = skin_weights[v_idx]
|
|
except:
|
|
w0 = 0
|
|
try:
|
|
w1 = blend_weights[v_idx]
|
|
except:
|
|
w1 = 0
|
|
blended_weight = utils.map_smoothstep(d0, d1, w0, w1, distance)
|
|
if blended_weight >= 0.0001:
|
|
bm_obj.verts[v_idx][obj_dl][bone_layer] = blended_weight
|
|
else:
|
|
if bone_layer in bm_obj.verts[v_idx][obj_dl]:
|
|
del bm_obj.verts[v_idx][obj_dl][bone_layer]
|
|
bm_obj.to_mesh(obj.data)
|
|
return
|
|
|
|
|
|
def calc_key_delta(arm, obj, key: bpy.types.ShapeKey, basis: bpy.types.ShapeKey):
|
|
delta = 0
|
|
scale = obj.scale * arm.scale if arm else obj.scale
|
|
if len(key.points) == len(basis.points):
|
|
for i in range(0, len(key.points)):
|
|
key_co: Vector = key.points[i].co
|
|
basis_co: Vector = basis.points[i].co
|
|
delta += abs(((key_co - basis_co) * scale).length)
|
|
return delta
|
|
|
|
|
|
def remove_empty_shapekeys_vertex_groups(chr_cache):
|
|
key_count = 0
|
|
group_count = 0
|
|
if chr_cache:
|
|
utils.log_info(f"Cleaning empty shape keys and vertex groups in character: {chr_cache.character_name}")
|
|
objects = chr_cache.get_cache_objects()
|
|
body_objects = chr_cache.get_objects_of_type("BODY")
|
|
arm = chr_cache.get_armature()
|
|
obj: bpy.types.Object
|
|
for obj in objects:
|
|
empty_keys = []
|
|
empty_groups = []
|
|
if obj not in body_objects and obj.type == "MESH":
|
|
if obj.data.shape_keys:
|
|
key_blocks = obj.data.shape_keys.key_blocks
|
|
if key_blocks and len(key_blocks) >= 2 and "Basis" in key_blocks:
|
|
basis = key_blocks["Basis"]
|
|
for key in key_blocks:
|
|
if key != basis:
|
|
delta = calc_key_delta(arm, obj, key, basis)
|
|
# if overall vertex delta sum is less than 1mm, consider it empty
|
|
if delta < 0.001:
|
|
empty_keys.append(key.name)
|
|
for key_name in empty_keys:
|
|
key = key_blocks[key_name]
|
|
utils.log_info(f" - Removing empty shape key: {obj.name} - {key.name}")
|
|
key.driver_remove("value")
|
|
obj.shape_key_remove(key)
|
|
key_count += 1
|
|
for vg in obj.vertex_groups:
|
|
w = meshutils.total_vertex_group_weight(obj, vg)
|
|
if w < 0.001:
|
|
empty_groups.append(vg)
|
|
for vg in empty_groups:
|
|
obj.vertex_groups.remove(vg)
|
|
group_count += 1
|
|
|
|
return key_count, group_count
|
|
|
|
|
|
def convert_to_non_standard(chr_cache):
|
|
if chr_cache.generation == "G3Plus" or chr_cache.generation == "G3":
|
|
chr_cache.generation = "ActorBuild"
|
|
elif chr_cache.generation == "GameBase":
|
|
chr_cache.generation = "GameBase"
|
|
chr_cache.non_standard_type = "HUMANOID"
|
|
|
|
|
|
def match_materials(chr_cache):
|
|
|
|
chr_objects = []
|
|
chr_materials = []
|
|
|
|
objects = chr_cache.get_cache_objects()
|
|
for obj in objects:
|
|
obj_cache = chr_cache.get_object_cache(obj)
|
|
chr_objects.append(obj)
|
|
for mat in obj.data.materials:
|
|
chr_materials.append(mat)
|
|
|
|
utils.log_info(f"Matching existing materials:")
|
|
utils.log_indent()
|
|
|
|
for obj in chr_objects:
|
|
|
|
obj_cache = chr_cache.get_object_cache(obj)
|
|
|
|
# objects imported from accurig will cause a duplication of names, so strip the numerical suffix
|
|
# also accurig uses the *mesh* names, not the object names.
|
|
mesh_source_name = utils.strip_name(obj.data.name)
|
|
|
|
utils.log_info(f"Mesh: {obj.name} / {mesh_source_name}")
|
|
utils.log_indent()
|
|
|
|
for slot in obj.material_slots:
|
|
mat = slot.material
|
|
if mat is None: continue
|
|
|
|
# again strip the numerical duplication suffix from the accurig imported material names
|
|
mat_source_name = utils.strip_name(mat.name)
|
|
|
|
slot_assigned = False
|
|
assigned_mat = None
|
|
|
|
# try to match the materials from an object with a matching source name (not part of the imported character)
|
|
for existing_obj in bpy.data.objects:
|
|
|
|
# convert the existing object name into a reallusion safe name
|
|
existing_mesh_source_name = utils.safe_export_name(existing_obj.data.name)
|
|
|
|
if (existing_mesh_source_name == mesh_source_name and
|
|
existing_obj not in chr_objects and
|
|
existing_obj.type == "MESH"):
|
|
|
|
utils.log_info(f"Existing mesh match: {existing_obj.name} / {existing_mesh_source_name}")
|
|
|
|
for existing_mat in existing_obj.data.materials:
|
|
|
|
# convert the existing material name into a reallusion safe name
|
|
existing_mat_source_name = utils.safe_export_name(existing_mat.name, True)
|
|
|
|
if existing_mat_source_name == mat_source_name:
|
|
utils.log_info(f"Assigning existing object / material: {existing_mat.name}")
|
|
slot.material = existing_mat
|
|
slot_assigned = True
|
|
assigned_mat = existing_mat
|
|
break
|
|
|
|
if slot_assigned:
|
|
break
|
|
|
|
# failing that, try to match any existing material by name (not part of the imported character)
|
|
if not slot_assigned:
|
|
for existing_mat in bpy.data.materials:
|
|
if existing_mat not in chr_materials:
|
|
existing_mat_source_name = utils.safe_export_name(existing_mat.name, True)
|
|
if existing_mat_source_name == mat_source_name:
|
|
utils.log_info(f"Assigning existing material: {existing_mat.name}")
|
|
slot.material = existing_mat
|
|
slot_assigned = True
|
|
assigned_mat = existing_mat
|
|
break
|
|
|
|
#if slot_assigned and assigned_mat:
|
|
# add_material_to_character(chr_cache, obj, obj_cache, assigned_mat, update_name=False)
|
|
|
|
utils.log_recess()
|
|
|
|
utils.log_recess()
|
|
|
|
|
|
def get_generic_context(context):
|
|
props = vars.props()
|
|
chr_cache, obj, mat, obj_cache, mat_cache = utils.get_context_character(context, strict=True)
|
|
non_chr_objects = [ obj for obj in context.selected_objects
|
|
if props.get_object_cache(obj) is None
|
|
and (obj.type == "MESH"
|
|
or obj.type == "EMPTY")]
|
|
generic_rig = None
|
|
rig = None
|
|
if chr_cache:
|
|
rig = chr_cache.get_armature()
|
|
else:
|
|
generic_rig = get_generic_rig(context.selected_objects)
|
|
if generic_rig:
|
|
rig = generic_rig
|
|
return chr_cache, generic_rig, non_chr_objects
|
|
|
|
|
|
class CC3OperatorCharacter(bpy.types.Operator):
|
|
"""CC3 Character Functions"""
|
|
bl_idname = "cc3.character"
|
|
bl_label = "Character Functions"
|
|
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
|
|
|
param: bpy.props.StringProperty(
|
|
name = "param",
|
|
default = ""
|
|
)
|
|
|
|
def execute(self, context):
|
|
props = vars.props()
|
|
prefs = vars.prefs()
|
|
|
|
if self.param == "ADD_PBR":
|
|
chr_cache = props.get_context_character_cache(context)
|
|
objects = context.selected_objects.copy()
|
|
for obj in objects:
|
|
add_object_to_character(chr_cache, obj,
|
|
no_materials=not prefs.auto_convert_materials)
|
|
|
|
elif self.param == "COPY_TO_CHARACTER":
|
|
chr_cache = props.get_context_character_cache(context)
|
|
objects = context.selected_objects.copy()
|
|
copy_objects_character_to_character(context.object, chr_cache, objects)
|
|
|
|
elif self.param == "REMOVE_OBJECT":
|
|
chr_cache = props.get_context_character_cache(context)
|
|
objects = context.selected_objects.copy()
|
|
for obj in objects:
|
|
remove_object_from_character(chr_cache, obj)
|
|
|
|
elif self.param == "ADD_MATERIALS":
|
|
chr_cache = props.get_context_character_cache(context)
|
|
obj = context.active_object
|
|
obj_cache = chr_cache.get_object_cache(obj)
|
|
add_missing_materials_to_character(chr_cache, obj, obj_cache)
|
|
|
|
elif self.param == "CLEAN_UP_DATA":
|
|
chr_cache = props.get_context_character_cache(context)
|
|
obj = context.active_object
|
|
clean_up_character_data(chr_cache)
|
|
|
|
elif self.param == "TRANSFER_WEIGHTS":
|
|
chr_cache = props.get_context_character_cache(context)
|
|
objects = [ obj for obj in bpy.context.selected_objects if obj.type == "MESH" ]
|
|
mode_selection = utils.store_mode_selection_state()
|
|
transfer_skin_weights(chr_cache, objects)
|
|
utils.restore_mode_selection_state(mode_selection)
|
|
|
|
elif self.param == "BLEND_WEIGHTS":
|
|
chr_cache = props.get_context_character_cache(context)
|
|
objects = [ obj for obj in bpy.context.selected_objects if obj.type == "MESH" ]
|
|
mode_selection = utils.store_mode_selection_state()
|
|
blend_skin_weights(chr_cache, objects)
|
|
utils.restore_mode_selection_state(mode_selection)
|
|
|
|
elif self.param == "NORMALIZE_WEIGHTS":
|
|
chr_cache = props.get_context_character_cache(context)
|
|
objects = [ obj for obj in bpy.context.selected_objects if obj.type == "MESH" ]
|
|
normalize_skin_weights(chr_cache, objects)
|
|
|
|
elif self.param == "CONVERT_TO_NON_STANDARD":
|
|
chr_cache = props.get_context_character_cache(context)
|
|
convert_to_non_standard(chr_cache)
|
|
self.report({'INFO'}, message="Convert to Non-standard complete!")
|
|
|
|
elif self.param == "CONVERT_FROM_GENERIC":
|
|
objects = context.selected_objects.copy()
|
|
if convert_generic_to_non_standard(objects):
|
|
self.report({'INFO'}, message="Generic character converted to Non-Standard!")
|
|
else:
|
|
self.report({'ERROR'}, message="Invalid generic character selection!")
|
|
|
|
elif self.param == "MATCH_MATERIALS":
|
|
chr_cache = props.get_context_character_cache(context)
|
|
match_materials(chr_cache)
|
|
|
|
elif self.param == "CONVERT_ACCESSORY":
|
|
chr_cache = props.get_context_character_cache(context)
|
|
objects = bpy.context.selected_objects.copy()
|
|
make_accessory(chr_cache, objects)
|
|
|
|
elif self.param == "SELECT_ACTOR_ALL":
|
|
chr_cache = props.get_context_character_cache(context)
|
|
select_character(chr_cache, all=True)
|
|
|
|
elif self.param == "SELECT_ACTOR_RIG":
|
|
chr_cache = props.get_context_character_cache(context)
|
|
select_character(chr_cache)
|
|
|
|
elif self.param == "DUPLICATE":
|
|
chr_cache = props.get_context_character_cache(context)
|
|
objects = duplicate_character(chr_cache)
|
|
utils.try_select_objects(objects, clear_selection=True)
|
|
bpy.ops.transform.translate("INVOKE_DEFAULT")
|
|
|
|
elif self.param == "REGENERATE_LINK_ID":
|
|
chr_cache = props.get_context_character_cache(context)
|
|
if chr_cache:
|
|
chr_cache.link_id = utils.generate_random_id(20)
|
|
|
|
elif self.param == "CLEAN_SHAPE_KEYS":
|
|
chr_cache = props.get_context_character_cache(context)
|
|
key_count, group_count = remove_empty_shapekeys_vertex_groups(chr_cache)
|
|
report = ""
|
|
if key_count > 0:
|
|
report += f"{key_count} empty shape keys removed."
|
|
if group_count > 0:
|
|
if report:
|
|
report += " "
|
|
report += f"{group_count} empty vertex groups removed."
|
|
if report:
|
|
self.report({"INFO"}, report)
|
|
|
|
return {"FINISHED"}
|
|
|
|
@classmethod
|
|
def description(cls, context, properties):
|
|
|
|
if properties.param == "ADD_PBR":
|
|
return "Add object to the character with pbr materials and parent to the character armature with an armature modifier"
|
|
elif properties.param == "COPY_TO_CHARACTER":
|
|
return "Copy the objects from another character into the active selected character"
|
|
elif properties.param == "REMOVE_OBJECT":
|
|
return "Unparent the object and remove from the character. Unparented objects will *not* be included in the export"
|
|
elif properties.param == "ADD_MATERIALS":
|
|
return "Add any new materials to the character data that are in this object but not in the character data"
|
|
elif properties.param == "CLEAN_UP_DATA":
|
|
return "Remove any objects from the character data that are no longer part of the character and remove any materials from the character that are no longer in the character objects"
|
|
elif properties.param == "TRANSFER_WEIGHTS":
|
|
return "Transfer skin weights from the character body to the selected objects.\n**THIS OPERATES IN ARMATURE REST MODE**"
|
|
elif properties.param == "BLEND_WEIGHTS":
|
|
return "Blend the skin weights from the character body with the weights currently on the selected objects. Weights are blended based on the distance from the surface of the body, governed by the min and max blend distance parameters.\n**THIS OPERATES IN ARMATURE REST MODE**"
|
|
elif properties.param == "NORMALIZE_WEIGHTS":
|
|
return "Recalculate the weights in the vertex groups so they all add up to 1.0 for each vertex, so each vertex is fully weighted across all the bones influencing it"
|
|
elif properties.param == "CONVERT_TO_NON_STANDARD":
|
|
return "Convert character to a non-standard Humanoid, Creature or Prop"
|
|
elif properties.param == "CONVERT_FROM_GENERIC":
|
|
return "Convert character from generic armature and objects to Non-Standard character with Reallusion materials."
|
|
elif properties.param == "MATCH_MATERIALS":
|
|
return "Restore the materials to a character sent to AccuRig"
|
|
elif properties.param == "CONVERT_ACCESSORY":
|
|
return "Convert the selected mesh objects into a compatible accessory with CC4"
|
|
elif properties.param == "SELECT_ACTOR_ALL":
|
|
return "Select all objects and armatures in the character or prop"
|
|
elif properties.param == "SELECT_ACTOR_RIG":
|
|
return "Select the just the parent armature for the character or prop"
|
|
elif properties.param == "DUPLICATE":
|
|
return "Duplicate the character / prop objects and meta-data to create a fully independent copy of the character or prop"
|
|
elif properties.param == "CLEAN_SHAPE_KEYS":
|
|
return "Clean up empty shape keys and vertex groups in character objects"
|
|
return ""
|
|
|
|
|
|
class CC3OperatorTransferCharacterGeometry(bpy.types.Operator):
|
|
"""Transfer Character Geometry:
|
|
Copy base mesh shapes (e.g. After Sculpting) from active character to
|
|
target character, for all *body* mesh objects in the characters, without
|
|
destroying existing facial expression shape keys in the target Character.
|
|
Source and target characters must have the same UV topology.
|
|
"""
|
|
|
|
bl_idname = "cc3.transfer_character"
|
|
bl_label = "Transfer Character Geometry"
|
|
bl_options = {"REGISTER", "UNDO"}
|
|
|
|
def execute(self, context):
|
|
props = vars.props()
|
|
prefs = vars.prefs()
|
|
|
|
active = utils.get_active_object()
|
|
selected = bpy.context.selected_objects.copy()
|
|
|
|
shape_key_name = None
|
|
if props.geom_transfer_layer == "SHAPE_KEY":
|
|
shape_key_name = props.geom_transfer_layer_name
|
|
|
|
src_chr = props.get_character_cache(active, None)
|
|
selected_characters = []
|
|
for dst_obj in selected:
|
|
selected_character = props.get_character_cache(dst_obj, None)
|
|
if selected_character not in selected_characters and selected_character != src_chr:
|
|
selected_characters.append(selected_character)
|
|
|
|
if src_chr and selected_characters:
|
|
|
|
src_objects = src_chr.get_all_objects(include_armature=False,
|
|
include_children=True,
|
|
of_type="MESH")
|
|
src_arm = src_chr.get_armature()
|
|
utils.object_mode_to(src_arm)
|
|
utils.clear_selected_objects()
|
|
dst_objects_transferred = []
|
|
|
|
for src_obj in src_objects:
|
|
src_base_name = utils.strip_name(src_obj.name)
|
|
for dst_chr in selected_characters:
|
|
dst_objects = dst_chr.get_all_objects(include_armature=False,
|
|
include_children=True,
|
|
of_type="MESH")
|
|
dst_arm = dst_chr.get_armature()
|
|
for dst_obj in dst_objects:
|
|
dst_base_name = utils.strip_name(dst_obj.name)
|
|
if src_base_name == dst_base_name:
|
|
if len(src_obj.data.vertices) == len(dst_obj.data.vertices):
|
|
if len(src_obj.data.polygons) == len(dst_obj.data.polygons):
|
|
geom.copy_vert_positions_by_uv_id(src_obj, dst_obj, 5,
|
|
shape_key_name=shape_key_name,
|
|
flatten_udim=False)
|
|
if shape_key_name:
|
|
for sk in dst_obj.data.shape_keys.key_blocks:
|
|
sk.value = 0.0
|
|
dst_obj.data.shape_keys.key_blocks[-1].value = 1.0
|
|
dst_objects_transferred.append(dst_obj)
|
|
|
|
# shape key copy does not support copying the bind pose
|
|
if not shape_key_name:
|
|
bones.copy_rig_bind_pose(src_arm, dst_arm)
|
|
dst_objects_transferred.append(dst_arm)
|
|
|
|
utils.object_mode_to(dst_arm)
|
|
utils.try_select_objects(dst_objects_transferred, clear_selection=True)
|
|
utils.set_active_object(dst_arm)
|
|
|
|
self.report(type={"INFO"}, message="Done!")
|
|
|
|
else:
|
|
self.report(type={"ERROR"}, message="Needs active and other selected characters!")
|
|
|
|
|
|
return {"FINISHED"}
|
|
|
|
@classmethod
|
|
def description(cls, context, properties):
|
|
return """Transfer Character Geometry:
|
|
Copy base mesh shapes (e.g. After Sculpting) from active character to
|
|
target character, for all *body* mesh objects in the characters, without
|
|
destroying existing facial expression shape keys in the target Character.
|
|
Source and target characters must have the same UV topology"""
|
|
|
|
|
|
class CC3OperatorTransferMeshGeometry(bpy.types.Operator):
|
|
"""Transfer Mesh Geometry:
|
|
Copy base mesh shape (e.g. After Sculpting) from active mesh to target
|
|
mesh without destroying any existing shape keys in the target mesh.
|
|
Source and target meshes must have the same UV topology.
|
|
"""
|
|
|
|
bl_idname = "cc3.transfer_mesh"
|
|
bl_label = "Transfer Mesh Geometry"
|
|
bl_options = {"REGISTER", "UNDO"}
|
|
|
|
def execute(self, context):
|
|
props = vars.props()
|
|
prefs = vars.prefs()
|
|
|
|
active = utils.get_active_object()
|
|
selected = bpy.context.selected_objects.copy()
|
|
|
|
utils.object_mode_to(active)
|
|
|
|
shape_key_name = None
|
|
if props.geom_transfer_layer == "SHAPE_KEY":
|
|
shape_key_name = props.geom_transfer_layer_name
|
|
|
|
if active and len(selected) >= 2:
|
|
for obj in selected:
|
|
if obj != active:
|
|
geom.copy_vert_positions_by_uv_id(active, obj, 5, shape_key_name=shape_key_name,
|
|
flatten_udim=False)
|
|
|
|
self.report(type={"INFO"}, message="Done!")
|
|
|
|
else:
|
|
self.report(type={"ERROR"}, message="Needs active and other selected meshes!")
|
|
|
|
return {"FINISHED"}
|
|
|
|
@classmethod
|
|
def description(cls, context, properties):
|
|
return """Transfer Mesh Geometry:
|
|
Copy base mesh shape (e.g. After Sculpting) from active mesh to target
|
|
mesh without destroying any existing shape keys in the target mesh.
|
|
Source and target meshes must have the same UV topology"""
|
|
|
|
|
|
class CCICCharacterLink(bpy.types.Operator):
|
|
"""Reconnect a linked or appended character to the source fbx and json data."""
|
|
|
|
bl_idname = "ccic.characterlink"
|
|
bl_label = "Character Linker"
|
|
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
|
|
|
param: bpy.props.StringProperty(
|
|
name = "param",
|
|
default = "",
|
|
options={"HIDDEN"}
|
|
)
|
|
|
|
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="*.blend",
|
|
options={"HIDDEN"}
|
|
)
|
|
|
|
def execute(self, context):
|
|
chr_rig = utils.get_context_armature(context)
|
|
|
|
if self.param == "CONNECT":
|
|
if chr_rig and self.filepath:
|
|
path, ext = os.path.splitext(self.filepath)
|
|
if utils.is_file_ext(ext, "BLEND"):
|
|
reconnect_rl_character_to_blend(chr_rig, self.filepath)
|
|
else:
|
|
reconnect_rl_character_to_fbx(chr_rig, self.filepath)
|
|
elif self.param == "LINK":
|
|
link_or_append_rl_character(self, context, self.filepath, link=True)
|
|
elif self.param == "APPEND":
|
|
link_or_append_rl_character(self, context, self.filepath, link=False)
|
|
return {"FINISHED"}
|
|
|
|
|
|
def invoke(self, context, event):
|
|
props = vars.props()
|
|
prefs = vars.prefs()
|
|
chr_cache = props.get_context_character_cache(context)
|
|
chr_rig = utils.get_context_armature(context)
|
|
|
|
if self.param == "CONNECT":
|
|
self.filter_glob = "*.fbx;*.blend"
|
|
if chr_rig and not chr_cache:
|
|
context.window_manager.fileselect_add(self)
|
|
return {"RUNNING_MODAL"}
|
|
|
|
if self.param == "LINK" or self.param == "APPEND":
|
|
self.filter_glob = "*.blend"
|
|
context.window_manager.fileselect_add(self)
|
|
return {"RUNNING_MODAL"}
|
|
|
|
return {"FINISHED"}
|
|
|
|
@classmethod
|
|
def description(cls, context, properties):
|
|
if properties.param == "CONNECT":
|
|
return """Reconnect a linked or appended character to the source fbx and json data."""
|
|
elif properties.param == "LINK":
|
|
return """Link to an existing reallusion characer in a separate blend file."""
|
|
elif properties.param == "APPEND":
|
|
return """Append an existing reallusion characer in a separate blend file."""
|
|
|
|
|
|
class CCICCharacterRename(bpy.types.Operator):
|
|
bl_idname = "ccic.rename_character"
|
|
bl_label = "Edit Character"
|
|
name: bpy.props.StringProperty(name="Name", default="")
|
|
non_standard_type: bpy.props.EnumProperty(items=[
|
|
("HUMANOID","Humanoid","Non standard character is a Humanoid"),
|
|
("CREATURE","Creature","Non standard character is a Creature"),
|
|
("PROP","Prop","Non standard character is a Prop"),
|
|
], default="PROP", name = "Type")
|
|
|
|
def execute(self, context):
|
|
props = vars.props()
|
|
chr_cache = props.get_context_character_cache(context)
|
|
rig = chr_cache.get_armature()
|
|
rig_name = utils.unique_object_name(self.name, rig)
|
|
if rig:
|
|
rig.name = rig_name
|
|
rig.data.name = rig_name
|
|
name = rig.name
|
|
chr_cache.character_name = name
|
|
if chr_cache.is_non_standard():
|
|
chr_cache.non_standard_type = self.non_standard_type
|
|
return {"FINISHED"}
|
|
|
|
def invoke(self, context, event):
|
|
props = vars.props()
|
|
prefs = vars.prefs()
|
|
chr_cache = props.get_context_character_cache(context)
|
|
self.name = chr_cache.character_name
|
|
self.non_standard_type = chr_cache.non_standard_type
|
|
return context.window_manager.invoke_props_dialog(self)
|
|
|
|
def draw(self, context):
|
|
props = vars.props()
|
|
chr_cache = props.get_context_character_cache(context)
|
|
|
|
layout = self.layout
|
|
|
|
split = layout.split(factor=0.25)
|
|
col_1 = split.column()
|
|
col_2 = split.column()
|
|
col_1.label(text="Name:")
|
|
col_2.prop(self, "name", text="")
|
|
col_1.separator()
|
|
col_2.separator()
|
|
if chr_cache.is_non_standard():
|
|
col_1.label(text="Type:")
|
|
row = col_2.row()
|
|
row.prop(self, "non_standard_type", expand=True)
|
|
else:
|
|
#CONVERT_TO_NON_STANDARD
|
|
col_1.label(text="")
|
|
row = col_2.row()
|
|
row.operator("cc3.character", text="Convert to Humanoid").param = "CONVERT_TO_NON_STANDARD"
|
|
col_1.separator()
|
|
col_2.separator()
|
|
col_1.label(text="Link ID:")
|
|
col_2.label(text=chr_cache.link_id)
|
|
col_1.separator()
|
|
col_2.separator()
|
|
col_1.label(text="")
|
|
col_2.operator("cc3.character", text="Regenerate Link ID").param = "REGENERATE_LINK_ID"
|
|
|
|
layout.separator()
|
|
|
|
@classmethod
|
|
def description(cls, context, properties):
|
|
return "Edit the character name and non-standard type"
|
|
|
|
|
|
class CCICCharacterConvertGeneric(bpy.types.Operator):
|
|
bl_idname = "ccic.convert_generic"
|
|
bl_label = "Convert Generic Character"
|
|
name: bpy.props.StringProperty(name="Name", default="")
|
|
non_standard_type: bpy.props.EnumProperty(items=[
|
|
("HUMANOID","Humanoid","Non standard character is a Humanoid"),
|
|
("CREATURE","Creature","Non standard character is a Creature"),
|
|
("PROP","Prop","Non standard character is a Prop"),
|
|
], default="PROP", name = "Type")
|
|
|
|
def execute(self, context):
|
|
props = vars.props()
|
|
prefs = vars.prefs()
|
|
|
|
objects = context.selected_objects.copy()
|
|
|
|
if convert_generic_to_non_standard(objects, type_override=self.non_standard_type, name_override=self.name):
|
|
self.report({'INFO'}, message="Generic character converted to Non-Standard!")
|
|
else:
|
|
self.report({'ERROR'}, message="Invalid generic character selection!")
|
|
|
|
return {"FINISHED"}
|
|
|
|
def invoke(self, context, event):
|
|
props = vars.props()
|
|
prefs = vars.prefs()
|
|
|
|
chr_cache, generic_rig, non_chr_objects = get_generic_context(context)
|
|
|
|
if chr_cache or not (generic_rig or non_chr_objects):
|
|
self.report({'ERROR'}, message="Invalid generic character selection!")
|
|
return {"FINISHED"}
|
|
|
|
chr_name = "Unknown"
|
|
if generic_rig:
|
|
chr_name = utils.unique_object_name(utils.un_suffix_name(generic_rig.name), generic_rig)
|
|
else:
|
|
active = utils.get_active_object()
|
|
if active in non_chr_objects:
|
|
chr_name = active.name
|
|
else:
|
|
chr_name = non_chr_objects[0].name
|
|
chr_name = utils.unique_object_name(utils.un_suffix_name(chr_name))
|
|
|
|
self.name = chr_name
|
|
self.non_standard_type = prefs.convert_non_standard_type
|
|
|
|
return context.window_manager.invoke_props_dialog(self)
|
|
|
|
@classmethod
|
|
def description(cls, context, properties):
|
|
return "Convert the armature, child objects and / or selected objects into a character or prop.\n" \
|
|
"All materials will be converted to work with Reallusion shaders if possible. \n\n" \
|
|
"Note: Materials must be based on the Principled BSDF shader to successfully convert"
|
|
|
|
|
|
class CCICWeightTransferBlend(bpy.types.Operator):
|
|
"""Weight Transfer Blend Operator"""
|
|
bl_idname = "ccic.weight_transfer"
|
|
bl_label = "Weight Transfer Blend"
|
|
bl_options = {"REGISTER", "UNDO"}
|
|
|
|
weight_blend_distance_min: bpy.props.FloatProperty(default=0.015, min=0.0, soft_max=0.05, max=1.0,
|
|
subtype="DISTANCE", precision=3,
|
|
name="Blend Min Distance",
|
|
description="Distance for full body weights")
|
|
weight_blend_distance_max: bpy.props.FloatProperty(default=0.05, min=0.0, soft_max=0.25, max=1.0,
|
|
subtype="DISTANCE", precision=3,
|
|
name="Blend Max Distance",
|
|
description="Distance for full source blend weights")
|
|
weight_blend_distance_range: bpy.props.FloatProperty(default=25, min=0, max=100, subtype="PERCENTAGE",
|
|
name="Blend Range",
|
|
description="Range from Blend Min Distance to the maximum body distance for each mesh to use as the Blend Max Distance")
|
|
weight_blend_use_range: bpy.props.BoolProperty(default=True,
|
|
name="Use Auto Range",
|
|
description="Use an automatically calculated Distance Blend Max based on a percentage of the largest distance to the selected mesh from the body. Otherwise use a fixed distance for the Distance Blend Max")
|
|
weight_blend_selected_only: bpy.props.BoolProperty(default=False,
|
|
name="Selected Vertices",
|
|
description="Only blender the weights for the selected vertices in each mesh")
|
|
|
|
chr_cache = None
|
|
objects = {}
|
|
|
|
@classmethod
|
|
def poll(cls, context):
|
|
props = vars.props()
|
|
return props.get_context_character_cache(context) is not None
|
|
|
|
def draw(self, context):
|
|
layout = self.layout
|
|
column = layout.column(align=True)
|
|
grid = column.grid_flow(columns=2, align=True)
|
|
grid.prop(self, "weight_blend_use_range")
|
|
grid.prop(self, "weight_blend_selected_only")
|
|
column.prop(self, "weight_blend_distance_min", slider=True)
|
|
if self.weight_blend_use_range:
|
|
column.prop(self, "weight_blend_distance_range", slider=True)
|
|
else:
|
|
column.prop(self, "weight_blend_distance_max", slider=True)
|
|
|
|
def begin_blend_skin_weights(self):
|
|
arm = self.chr_cache.get_armature()
|
|
if not arm: return
|
|
body = get_combined_body(self.chr_cache)
|
|
if not body: return
|
|
|
|
self.objects = remove_list_body_objects(self.chr_cache, self.objects)
|
|
|
|
for obj_name in self.objects:
|
|
obj = bpy.data.objects[obj_name]
|
|
pose_mode = arm.data.pose_position
|
|
arm.data.pose_position = "REST"
|
|
#bpy.context.view_layer.update()
|
|
bm_obj = geom.get_bmesh(obj.data)
|
|
layer_map = prep_deformation_layers(arm, body, obj, bm_obj)
|
|
transfer_skin_weights(self.chr_cache, [obj], body_override=body)
|
|
bm_obj.free()
|
|
bm_obj = geom.get_bmesh(obj.data)
|
|
post_deformation_layers(obj, bm_obj, layer_map)
|
|
vert_map = geom.map_body_weight_blends(body, obj, bm_obj)
|
|
self.objects[obj_name] = (bm_obj, layer_map, vert_map)
|
|
arm.data.pose_position = pose_mode
|
|
|
|
finish_combined_body(body)
|
|
|
|
def do_blend_skin_weights(self):
|
|
arm = self.chr_cache.get_armature()
|
|
if not arm or not self.objects: return
|
|
|
|
for obj_name in self.objects:
|
|
obj = bpy.data.objects[obj_name]
|
|
bm_obj, layer_map, vert_map = self.objects[obj_name]
|
|
pose_mode = arm.data.pose_position
|
|
arm.data.pose_position = "REST"
|
|
apply_weight_blend(obj, bm_obj, vert_map, layer_map,
|
|
self.weight_blend_distance_min,
|
|
self.weight_blend_distance_max,
|
|
self.weight_blend_distance_range,
|
|
self.weight_blend_use_range,
|
|
self.weight_blend_selected_only)
|
|
arm.data.pose_position = pose_mode
|
|
|
|
def collect_objects(self, context):
|
|
props = vars.props()
|
|
self.chr_cache = props.get_context_character_cache(context)
|
|
self.objects = {}
|
|
if self.chr_cache:
|
|
body_objects = self.chr_cache.get_objects_of_type("BODY")
|
|
for obj in context.selected_objects:
|
|
if obj in body_objects: continue
|
|
if utils.object_exists_is_mesh(obj) and self.chr_cache.has_object(obj):
|
|
self.objects[obj.name] = None
|
|
|
|
def execute(self, context):
|
|
props = vars.props()
|
|
self.chr_cache = props.get_context_character_cache(context)
|
|
self.do_blend_skin_weights()
|
|
return {'FINISHED'}
|
|
|
|
def invoke(self, context, event):
|
|
prefs = vars.prefs()
|
|
utils.set_mode("OBJECT")
|
|
self.weight_blend_distance_max = prefs.weight_blend_distance_max
|
|
self.weight_blend_distance_min = prefs.weight_blend_distance_min
|
|
self.weight_blend_distance_range = prefs.weight_blend_distance_range
|
|
self.weight_blend_use_range = prefs.weight_blend_use_range
|
|
self.weight_blend_selected_only = prefs.weight_blend_selected_only
|
|
self.collect_objects(context)
|
|
mode_selection = utils.store_mode_selection_state()
|
|
# TODO will have to store blend and skin weights separately so we don't have to
|
|
# use the vertex groups (as cant delete them when operator ends)
|
|
self.begin_blend_skin_weights()
|
|
self.do_blend_skin_weights()
|
|
utils.restore_mode_selection_state(mode_selection)
|
|
return context.window_manager.invoke_props_popup(self, event)
|
|
|
|
|
|
@classmethod
|
|
def description(cls, context, properties):
|
|
return "Blend the skin weights from the character body with the weights currently on the selected objects. Weights are blended based on the distance from the surface of the body, governed by the min and max blend distance parameters.\n**THIS OPERATES IN ARMATURE REST MODE**"
|