From c8ddbaf89bd3fb124504fb28a906b2087e0567e6 Mon Sep 17 00:00:00 2001 From: soupday <79094830+soupday@users.noreply.github.com> Date: Fri, 27 Sep 2024 13:19:41 +0100 Subject: [PATCH 1/2] 2.1.10 Support for exporting MDProps. Exact object name matching for receive update materials. --- btp/cc.py | 83 +++++++++++++++++++++++++++++++++---------------- btp/exporter.py | 9 +++--- btp/importer.py | 41 +++++++++++++----------- btp/link.py | 22 ++++++------- btp/utils.py | 10 ++++++ btp/vars.py | 2 +- 6 files changed, 104 insertions(+), 63 deletions(-) diff --git a/btp/cc.py b/btp/cc.py index e7980f4..ff8af56 100644 --- a/btp/cc.py +++ b/btp/cc.py @@ -263,7 +263,7 @@ def parse(self): def json(self): return self.mesh_json - def find_material_name(self, search_mat_name): + def find_material_name(self, search_mat_name, exact=False): try_names = set() try_names.add(search_mat_name) try_names.add(safe_export_name(search_mat_name)) @@ -275,6 +275,8 @@ def find_material_name(self, search_mat_name): trunc_mat_name = json_mat_name[:-13] if json_mat_name in try_names or (trunc_mat_name and trunc_mat_name in try_names): return json_mat_name + if exact: + return None # try a partial match, but only if there is only one result partial_mat_name = None partial_mat_count = 0 @@ -293,8 +295,8 @@ def find_material_name(self, search_mat_name): return partial_mat_name return None - def find_material(self, search_mat_name): - mat_name = self.find_material_name(search_mat_name) + def find_material(self, search_mat_name, exact=False): + mat_name = self.find_material_name(search_mat_name, exact=exact) cc_mat_json: CCMaterialJson = None if mat_name: cc_mat_json = self.materials[mat_name] @@ -347,7 +349,7 @@ def parse(self): def json(self): return self.physics_mesh_json - def find_material_name(self, search_mat_name): + def find_material_name(self, search_mat_name, exact=False): try_names = set() try_names.add(search_mat_name) try_names.add(safe_export_name(search_mat_name)) @@ -359,6 +361,8 @@ def find_material_name(self, search_mat_name): trunc_mat_name = json_mat_name[:-13] if json_mat_name in try_names or (trunc_mat_name and trunc_mat_name in try_names): return json_mat_name + if exact: + return None # try a partial match, but only if there is only one result partial_mat_name = None partial_mat_count = 0 @@ -377,8 +381,8 @@ def find_material_name(self, search_mat_name): return partial_mat_name return None - def find_material(self, search_mat_name): - mat_name = self.find_material_name(search_mat_name) + def find_material(self, search_mat_name, exact=False): + mat_name = self.find_material_name(search_mat_name, exact=exact) cc_mat_json: CCPhysicsMaterialJson = None if mat_name: cc_mat_json = self.materials[mat_name] @@ -531,7 +535,7 @@ def get_character_json(self): utils.log("Could not find character json: " + self.character_id) return None - def find_source_mesh_name(self, imported_mesh_name, imported_obj_name, rl_meshes): + def find_source_mesh_name(self, imported_mesh_name, imported_obj_name, rl_meshes, exact=False): try_names = set() try_names.add(imported_mesh_name) try_names.add(safe_export_name(imported_mesh_name)) @@ -544,6 +548,8 @@ def find_source_mesh_name(self, imported_mesh_name, imported_obj_name, rl_meshes for mesh_name in rl_meshes: if mesh_name in try_names: return mesh_name + if exact: + return None # try a partial match, but only if there is only one result partial_mesh_match = None partial_mesh_count = 0 @@ -559,17 +565,14 @@ def find_source_mesh_name(self, imported_mesh_name, imported_obj_name, rl_meshes return partial_mesh_match return None - def find_mesh_name(self, search_mesh_name, search_obj_name = None): - return self.find_source_mesh_name(search_mesh_name, search_obj_name, self.meshes) - - def find_mesh(self, search_mesh_name, search_obj_name = None): - mesh_name = self.find_mesh_name(search_mesh_name, search_obj_name) + def find_mesh(self, search_mesh_name, search_obj_name=None, exact=False): + mesh_name = self.find_source_mesh_name(search_mesh_name, search_obj_name, self.meshes, exact=exact) cc_mesh_json: CCMeshJson = None if mesh_name: cc_mesh_json = self.meshes[mesh_name] return cc_mesh_json - def find_physics_mesh_name(self, search_mesh_name, search_obj_name = None): + def find_physics_mesh_name(self, search_mesh_name, search_obj_name, rl_meshes, exact=False): try_names = set() if search_mesh_name: try_names.add(search_mesh_name) @@ -580,10 +583,25 @@ def find_physics_mesh_name(self, search_mesh_name, search_obj_name = None): for mesh_name in self.physics_meshes: if mesh_name in try_names: return mesh_name + if exact: + return None + # try a partial match, but only if there is only one result + partial_mesh_match = None + partial_mesh_count = 0 + for mesh_name in rl_meshes: + for try_name in try_names: + if try_name in mesh_name: + partial_mesh_count += 1 + if not partial_mesh_match: + partial_mesh_match = mesh_name + # only count 1 match per try set + break + if partial_mesh_count == 1: + return partial_mesh_match return None - def find_physics_mesh(self, search_mesh_name, search_obj_name = None): - mesh_name = self.find_physics_mesh_name(search_mesh_name, search_obj_name) + def find_physics_mesh(self, search_mesh_name, search_obj_name=None, exact=False): + mesh_name = self.find_physics_mesh_name(search_mesh_name, search_obj_name, self.physics_meshes, exact=exact) cc_physics_mesh_json: CCPhysicsMeshJson = None if mesh_name: cc_physics_mesh_json = self.physics_meshes[mesh_name] @@ -615,7 +633,8 @@ class CCMeshMaterial(): def __init__(self, actor = None, obj = None, mesh_name = None, mat_name = None, duf_mesh = None, duf_material = None, - physx_object = None, cc_json_data = None): + physx_object = None, cc_json_data = None, + exact=False): self.actor = actor self.obj = obj self.actor_name = actor.GetName() @@ -628,7 +647,7 @@ def __init__(self, actor = None, obj = None, self.duf_material = duf_material self.json_data = cc_json_data if self.json_data: - self.find_json_data() + self.find_json_data(exact) def material_component(self): if not self.mat_component and self.actor: @@ -866,20 +885,20 @@ def set_physics_param(self, json_param_name, param_value, folder = None): else: PC.SetSoftPhysXProperty(self.mesh_name, self.mat_name, phys_param_name, float(param_value)) - def find_json_data(self): + def find_json_data(self, exact): if self.json_data: - self.mesh_json = self.json_data.find_mesh(self.mesh_name, self.obj_name) + self.mesh_json = self.json_data.find_mesh(self.mesh_name, self.obj_name, exact=exact) if self.mesh_json: self.json_mesh_name = self.mesh_json.name - self.mat_json = self.mesh_json.find_material(self.mat_name) + self.mat_json = self.mesh_json.find_material(self.mat_name, exact=exact) if self.mat_json: self.json_mat_name = self.mat_json.name else: utils.log_warn(f"Material JSON {self.mat_name} not found!") if self.physx_object: - self.physx_mesh_json = self.json_data.find_physics_mesh(self.json_mesh_name) + self.physx_mesh_json = self.json_data.find_physics_mesh(self.json_mesh_name, exact=exact) if self.physx_mesh_json: - self.physx_mat_json = self.physx_mesh_json.find_material(self.json_mat_name) + self.physx_mat_json = self.physx_mesh_json.find_material(self.json_mat_name, exact=exact) else: utils.log_warn(f"Mesh JSON {self.obj_name}/{self.mesh_name} not found!") @@ -933,7 +952,8 @@ def get_selected_mesh_materials(exclude_mesh_names=None, exclude_material_names= def get_avatar_mesh_materials(avatar, exclude_mesh_names=None, exclude_material_names=None, - mesh_filter=None, material_filter=None, json_data=None): + mesh_filter=None, material_filter=None, json_data=None, + exact=False): mesh_materials = [] @@ -976,7 +996,8 @@ def get_avatar_mesh_materials(avatar, exclude_mesh_names=None, exclude_material_ physics_object = get_actor_physics_object(avatar, mesh_name, mat_name) M = CCMeshMaterial(actor=avatar, obj=obj, mesh_name=mesh_name, mat_name=mat_name, - physx_object=physics_object, cc_json_data=json_data) + physx_object=physics_object, cc_json_data=json_data, + exact=exact) mesh_materials.append(M) @@ -1366,7 +1387,7 @@ def get_object_type(obj): T = type(obj) if T is RIAvatar or T is RILightAvatar: return "AVATAR" - elif T is RIProp: + elif T is RIProp or T is RIMDProp: return "PROP" elif T is RIAccessory: return "ACCESSORY" @@ -1728,6 +1749,16 @@ def print_node_tree(obj): print_nodes(node) +def is_prop(obj): + T = type(obj) + return (T is RIProp or T is RIMDProp) + + +def is_avatar(obj): + T = type(obj) + return (T is RIAvatar or T is RILightAvatar) + + IGNORE_NODES = ["RL_BoneRoot", "IKSolverDummy", "NodeForExpressionLookAtSolver"] def get_actor_objects(actor): @@ -1746,7 +1777,7 @@ def get_actor_objects(actor): if avatar.GetAvatarType() != EAvatarType_Standard: objects.append(avatar) - elif actor and type(actor) is RIProp: + elif actor and (T is RIProp or T is RIMDProp): child_objects = RScene.FindChildObjects(actor, EObjectType_Avatar) for obj in child_objects: name = obj.GetName() diff --git a/btp/exporter.py b/btp/exporter.py index 095c070..079ef99 100644 --- a/btp/exporter.py +++ b/btp/exporter.py @@ -125,8 +125,8 @@ def __init__(self, objects, no_window=False): self.create_options_window() def collect_objects(self, objects): - self.avatars = [ o for o in objects if (type(o) is RIAvatar or type(o) is RILightAvatar) ] - self.props = [ o for o in objects if type(o) is RIProp ] + self.avatars = [ o for o in objects if cc.is_avatar(o) ] + self.props = [ o for o in objects if cc.is_prop(o) ] def clear_objects(self): self.avatars = [] @@ -181,8 +181,7 @@ def set_base_path(self, file_path, create=False, show=False): def set_multi_paths(self, object, motion_only=False): base_path = self.base_path ext = ".iCCX" - T = type(object) - if T is RIAvatar or T is RILightAvatar or T is RIProp: + if cc.is_avatar(object) or cc.is_prop(object): ext = ".Fbx" name = object.GetName() if motion_only: @@ -672,6 +671,8 @@ def export_fbx(self): export = "ANIMATION" elif self.option_current_animation and num_frames > 0: export = "CURRENT_POSE" + elif self.option_current_pose: + export = "CURRENT_POSE" else: export = "BIND" diff --git a/btp/importer.py b/btp/importer.py index 7c2312b..ffe1497 100644 --- a/btp/importer.py +++ b/btp/importer.py @@ -304,21 +304,22 @@ def import_fbx(self): return objects def update_materials(self, obj): - if type(obj) is RLPy.RIAvatar or type(obj) is RLPy.RILightAvatar: + # NOTE: RILightAvatars and RIMDProps do not have material components so can't be updated. + if type(obj) is RLPy.RIAvatar: self.avatar = obj - self.rebuild_materials() + self.rebuild_materials(update=True) elif type(obj) is RLPy.RIProp: objects = set() objects.add(obj) child_objects = RLPy.RGlobal.FindChildObjects(obj, RLPy.EObjectType_Prop) - for obj in child_objects: - if type(obj) is RLPy.RIProp: - objects.add(obj) + for child in child_objects: + if cc.is_prop(child): + objects.add(child) for obj in objects: self.avatar = obj - self.rebuild_materials() + self.rebuild_materials(update=True) - def rebuild_materials(self): + def rebuild_materials(self, update=False): """Material reconstruction process. """ @@ -328,7 +329,7 @@ def rebuild_materials(self): utils.start_timer() self.create_progress_window() - cc_mesh_materials = cc.get_avatar_mesh_materials(avatar, json_data=json_data) + cc_mesh_materials = cc.get_avatar_mesh_materials(avatar, json_data=json_data, exact=update) utils.log("Rebuilding character materials and texures:") self.update_shaders(cc_mesh_materials) self.update_progress(0, "Done Initializing!", True) @@ -338,19 +339,21 @@ def rebuild_materials(self): self.import_custom_textures(cc_mesh_materials) self.import_physics(cc_mesh_materials) - if self.character_type == "HUMANOID": - self.option_import_hik = True - self.option_import_profile = True - # user optional for importing custom facial expressions as the import profile will load the old ones. - # and it's slow... - #self.option_import_expressions = True + if not update: # do not update HIK / facial profiles on material updates - if self.option_import_hik: - self.import_hik_profile() + if self.character_type == "HUMANOID": + self.option_import_hik = True + self.option_import_profile = True + # user optional for importing custom facial expressions as the import profile will load the old ones. + # and it's slow... + #self.option_import_expressions = True - if self.character_type == "STANDARD" or self.character_type == "HUMANOID": - if self.option_import_profile or self.option_import_expressions: - self.import_facial_profile() + if self.option_import_hik: + self.import_hik_profile() + + if self.character_type == "STANDARD" or self.character_type == "HUMANOID": + if self.option_import_profile or self.option_import_expressions: + self.import_facial_profile() self.final(cc_mesh_materials) diff --git a/btp/link.py b/btp/link.py index 71dc0df..2ce65b5 100644 --- a/btp/link.py +++ b/btp/link.py @@ -150,29 +150,25 @@ def end_editing(self, time): def get_skeleton_component(self) -> RISkeletonComponent: if self.object: - T = type(self.object) - if T is RIAvatar or T is RILightAvatar or T is RIProp: + if cc.is_avatar(self.object) or cc.is_prop(self.object): return self.object.GetSkeletonComponent() return None def get_face_component(self) -> RIFaceComponent: if self.object: - T = type(self.object) - if T is RIAvatar or T is RILightAvatar: + if cc.is_avatar(self.object): return self.object.GetFaceComponent() return None def get_viseme_component(self) -> RIVisemeComponent: if self.object: - T = type(self.object) - if T is RIAvatar or T is RILightAvatar: + if cc.is_avatar(self.object): return self.object.GetVisemeComponent() return None def get_morph_component(self) -> RIMorphComponent: if self.object: - T = type(self.object) - if T is RIAvatar or T is RILightAvatar or T is RIProp: + if cc.is_avatar(self.object) or cc.is_prop(self.object): return self.object.GetMorphComponent() return None @@ -289,7 +285,7 @@ def get_actor_type(obj): T = type(obj) if T is RIAvatar or T is RILightAvatar: return "AVATAR" - elif T is RIProp: + elif T is RIProp or T is RIMDProp: return "PROP" elif T is RILight: return "LIGHT" @@ -302,10 +298,10 @@ def get_type(self): return self.get_actor_type(self.object) def is_avatar(self): - return type(self.object) is RIAvatar or type(self.object) is RILightAvatar + return cc.is_avatar(self.object) def is_prop(self): - return type(self.object) is RIProp + return cc.is_prop(self.object) def is_light(self): return type(self.object) is RILight @@ -1495,7 +1491,7 @@ def update_ui(self): T = type(prop_or_avatar) if T is RIAvatar or T is RILightAvatar: avatar = prop_or_avatar - elif T is RIProp: + elif T is RIProp or T is RIMDProp: prop = prop_or_avatar elif T is RILight or T is RISpotLight or T is RIPointLight or T is RIDirectionalLight: light = first @@ -1533,7 +1529,7 @@ def update_ui(self): generation == EAvatarGeneration_CC_Game_Base_One): num_rigable += 1 - elif T is RIProp and prop_or_avatar not in props_and_avatars: + elif (T is RIProp or T is RIMDProp) and prop_or_avatar not in props_and_avatars: props_and_avatars.append(prop_or_avatar) props.append(prop_or_avatar) num_props += 1 diff --git a/btp/utils.py b/btp/utils.py index 4014ee8..4b215f8 100644 --- a/btp/utils.py +++ b/btp/utils.py @@ -250,6 +250,16 @@ def name_contains_distinct_keywords(name : str, keywords : list): return False +def name_is_split_mesh(name): + if (len(name) >= 4 and + name[-1].isdigit() and + name[-2].isdigit() and + name[-3] == "S" and + name[-4] == "_"): + return True + return False + + def make_folder(path): folder, file = os.path.split(path) os.makedirs(folder, exist_ok=True) diff --git a/btp/vars.py b/btp/vars.py index 152add7..16fb068 100644 --- a/btp/vars.py +++ b/btp/vars.py @@ -16,7 +16,7 @@ from RLPy import * -VERSION = "2.1.9" +VERSION = "2.1.10" #DEV = True DEV = False From d3fb12953766b86a7b7f5f4c1677cc6c9d2205a8 Mon Sep 17 00:00:00 2001 From: soupday <79094830+soupday@users.noreply.github.com> Date: Sat, 28 Sep 2024 15:04:05 +0100 Subject: [PATCH 2/2] 2.1.10 Readme --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 1e9c384..e5d2328 100644 --- a/README.md +++ b/README.md @@ -65,6 +65,10 @@ Known Issues Changelog ========= +### 2.1.10 +- Support for exporting MDProps. +- Update materials through the datalink will use exact name matching and will no longer update materials on partial name matches. + ### 2.1.9 - Some UI Restructure. - Fix to exporting Lite Avatars.