From 3a3edf785c970d278f3f174063edc319ae79b088 Mon Sep 17 00:00:00 2001 From: Sergey Lapin Date: Fri, 22 May 2026 18:09:26 +0300 Subject: [PATCH] Character pipeline fixes --- assets/blender/characters/CMakeLists.txt | 1 + assets/blender/characters/combine_clothes.py | 73 +++++++++++- assets/blender/characters/consolidate.py | 8 ++ .../blender/characters/garment_system_ui.py | 104 ++++++++++++++++++ .../blender/characters/transfer_shape_keys.py | 30 ++++- src/features/editScene/CMakeLists.txt | 1 + 6 files changed, 209 insertions(+), 8 deletions(-) diff --git a/assets/blender/characters/CMakeLists.txt b/assets/blender/characters/CMakeLists.txt index a21694f..af922ff 100644 --- a/assets/blender/characters/CMakeLists.txt +++ b/assets/blender/characters/CMakeLists.txt @@ -420,6 +420,7 @@ endfunction() weight_clothes(${CMAKE_CURRENT_SOURCE_DIR}/clothes-male-top.blend) weight_clothes(${CMAKE_CURRENT_SOURCE_DIR}/clothes-male-bottom.blend) +weight_clothes(${CMAKE_CURRENT_SOURCE_DIR}/clothes-male-feet.blend) weight_clothes(${CMAKE_CURRENT_SOURCE_DIR}/clothes-female-top.blend) weight_clothes(${CMAKE_CURRENT_SOURCE_DIR}/clothes-female-bottom.blend) weight_clothes(${CMAKE_CURRENT_SOURCE_DIR}/clothes-female-feet.blend) diff --git a/assets/blender/characters/combine_clothes.py b/assets/blender/characters/combine_clothes.py index ab5000a..874df4f 100644 --- a/assets/blender/characters/combine_clothes.py +++ b/assets/blender/characters/combine_clothes.py @@ -7,10 +7,16 @@ import mathutils from mathutils.bvhtree import BVHTree def load_blend_files(clothes_blend_path, body_blend_path): - """Load objects from blend files and return all loaded objects""" + """Load objects from blend files and return all loaded objects + + IMPORTANT: Body file must be loaded FIRST so its objects keep their original names. + The clothes file may contain reference meshes with the same names as body parts + (e.g., 'BodyTop' used for weight painting). If clothes are loaded first, the body's + real objects get renamed to 'BodyTop.001' etc., breaking the ref_part lookup. + """ loaded_objects = [] - for path in [clothes_blend_path, body_blend_path]: + for path in [body_blend_path, clothes_blend_path]: with bpy.data.libraries.load(path) as (data_from, data_to): data_to.objects = data_from.objects for obj in data_to.objects: @@ -265,11 +271,66 @@ def run_batch_combine(): all_objs = bpy.data.objects # Separate body objects (no ref_layer property) - body_objects = [o for o in all_objs if o.type == 'MESH' and "ref_layer" not in o] + # Filter out rig control shapes (cs_*), helper objects, and objects with no vertices + body_objects = [] + skipped_objects = [] + for o in all_objs: + if o.type != 'MESH' or "ref_layer" in o: + continue + # Skip rig control shapes and helper objects + if o.name.startswith("cs_") or o.name.startswith("WGT-") or o.name.startswith("MCH-") or o.name.startswith("ORG-"): + skipped_objects.append(o.name) + continue + # Skip objects with no vertices (empty meshes) + if len(o.data.vertices) == 0: + skipped_objects.append(o.name) + continue + body_objects.append(o) - # Separate clothing by layer - clothing_layer1 = [o for o in all_objs if o.type == 'MESH' and "ref_layer" in o and o["ref_layer"] == 1] - clothing_layer2 = [o for o in all_objs if o.type == 'MESH' and "ref_layer" in o and o["ref_layer"] == 2] + if skipped_objects: + print(f"Skipped {len(skipped_objects)} non-body mesh objects: {', '.join(skipped_objects[:10])}{'...' if len(skipped_objects) > 10 else ''}") + + # Filter body objects: those missing age/sex/slot are treated as helpers/references + required_body_props = {"age", "sex", "slot"} + valid_body_objects = [] + skipped_body_objects = [] + for body_obj in body_objects: + missing = [p for p in required_body_props if p not in body_obj] + if missing: + print(f"WARNING: Mesh object '{body_obj.name}' is missing required body properties {missing}.") + print(f" Treating as helper/reference object (not a body part).") + print(f" Available properties: {[k for k in body_obj.keys() if not k.startswith('_')]}") + skipped_body_objects.append(body_obj.name) + else: + valid_body_objects.append(body_obj) + + if skipped_body_objects: + print(f"Skipped {len(skipped_body_objects)} mesh objects treated as helpers: {', '.join(skipped_body_objects)}") + + body_objects = valid_body_objects + + # Separate clothing by layer, filtering out objects missing required clothing properties + required_clothing_props = {"ref_layer", "ref_part", "garment_id"} + clothing_layer1 = [] + clothing_layer2 = [] + skipped_clothing_objects = [] + for o in all_objs: + if o.type != 'MESH' or "ref_layer" not in o: + continue + # Check if this is a valid clothing object (has required clothing properties) + missing = [p for p in required_clothing_props if p not in o] + if missing: + print(f"WARNING: Mesh object '{o.name}' has ref_layer but is missing required clothing properties {missing}.") + print(f" Treating as helper/reference object (not clothing).") + skipped_clothing_objects.append(o.name) + continue + if o["ref_layer"] == 1: + clothing_layer1.append(o) + elif o["ref_layer"] == 2: + clothing_layer2.append(o) + + if skipped_clothing_objects: + print(f"Skipped {len(skipped_clothing_objects)} mesh objects treated as helpers: {', '.join(skipped_clothing_objects)}") print(f"Found {len(body_objects)} body objects") print(f"Found {len(clothing_layer1)} layer 1 clothing objects") diff --git a/assets/blender/characters/consolidate.py b/assets/blender/characters/consolidate.py index 1bffd3c..c316714 100644 --- a/assets/blender/characters/consolidate.py +++ b/assets/blender/characters/consolidate.py @@ -7,6 +7,7 @@ def process_append(source_files, output_path): for file_path in source_files: if not os.path.exists(file_path): + print(f"Warning: Source file not found: {file_path}") continue with bpy.data.libraries.load(file_path) as (data_from, data_to): @@ -50,6 +51,13 @@ def process_append(source_files, output_path): print(f"Processed {obj.name}: Parented and Modset to {arm_name}") else: print(f"Warning: Armature '{arm_name}' not found for {obj.name}") + elif obj.type == 'MESH': + # Object is a mesh but missing required properties - treat as helper/reference + missing = [p for p in required_props if p not in obj.keys()] + print(f"WARNING: Mesh object '{obj.name}' is missing required properties {missing}.") + print(f" Treating as helper/reference object (not a body part).") + print(f" Available properties: {[k for k in obj.keys() if not k.startswith('_')]}") + # Don't remove - keep helper/reference objects in the scene else: # Clean up data not meeting criteria bpy.data.objects.remove(obj, do_unlink=True) diff --git a/assets/blender/characters/garment_system_ui.py b/assets/blender/characters/garment_system_ui.py index 633067c..80aae96 100644 --- a/assets/blender/characters/garment_system_ui.py +++ b/assets/blender/characters/garment_system_ui.py @@ -272,6 +272,103 @@ class GARMENT_OT_sync_tags(bpy.types.Operator): return {"FINISHED"} +class GARMENT_OT_check_scene(bpy.types.Operator): + bl_idname = "garment.check_scene" + bl_label = "Check Scene" + bl_description = "Validate all garment-system objects for pipeline readiness" + bl_options = {"REGISTER"} + + def execute(self, context): + errors = [] + warnings = [] + body_count = 0 + cloth_count = 0 + ok_count = 0 + + required_body_props = {"age", "sex", "slot"} + required_clothing_props = {"ref_layer", "ref_part", "garment_id"} + + for obj in bpy.data.objects: + if obj.type != "MESH": + continue + + has_body_props = all(p in obj for p in required_body_props) + has_clothing_props = all(p in obj for p in required_clothing_props) + + if has_body_props and has_clothing_props: + errors.append(f"'{obj.name}': has BOTH body-part and clothing properties (mixed)") + continue + + if has_body_props: + body_count += 1 + # Check body part values are valid + age = str(obj.get("age", "")) + sex = str(obj.get("sex", "")) + slot = str(obj.get("slot", "")) + valid_ages = {"adult", "child", "teen", "baby"} + valid_sexes = {"male", "female"} + valid_slots = {"face", "bottom", "top", "feet", "hair"} + if age not in valid_ages: + errors.append(f"'{obj.name}': invalid age='{age}' (valid: {', '.join(sorted(valid_ages))})") + if sex not in valid_sexes: + errors.append(f"'{obj.name}': invalid sex='{sex}' (valid: {', '.join(sorted(valid_sexes))})") + if slot not in valid_slots: + errors.append(f"'{obj.name}': invalid slot='{slot}' (valid: {', '.join(sorted(valid_slots))})") + ok_count += 1 + + elif has_clothing_props: + cloth_count += 1 + # Check clothing values are valid + ref_sex = str(obj.get("ref_sex", "")) + ref_age = str(obj.get("ref_age", "")) + valid_ages = {"adult", "child", "teen", "baby"} + valid_sexes = {"male", "female"} + if ref_sex and ref_sex not in valid_sexes: + errors.append(f"'{obj.name}': invalid ref_sex='{ref_sex}' (valid: {', '.join(sorted(valid_sexes))})") + if ref_age and ref_age not in valid_ages: + errors.append(f"'{obj.name}': invalid ref_age='{ref_age}' (valid: {', '.join(sorted(valid_ages))})") + # Check ref_part points to an existing body object + ref_part = str(obj.get("ref_part", "")) + if ref_part and ref_part not in bpy.data.objects: + warnings.append(f"'{obj.name}': ref_part='{ref_part}' not found in scene") + ok_count += 1 + + else: + # Mesh object with no garment props - check if it should have them + if obj.name.startswith("Body") or any(x in obj.name.lower() for x in ("shoes", "pants", "shirt", "skirt", "dress", "hat", "hair", "top", "bottom")): + warnings.append(f"'{obj.name}': mesh object with no garment properties (may need age/sex/slot or ref_layer/ref_part/garment_id)") + + # Report results + self.report({"INFO"}, f"Check complete: {body_count} body parts, {cloth_count} clothing, {len(errors)} errors, {len(warnings)} warnings") + + # Print detailed results to console + print("\n" + "=" * 60) + print("GARMENT SYSTEM SCENE CHECK") + print("=" * 60) + print(f"Body parts: {body_count}") + print(f"Clothing: {cloth_count}") + print(f"OK objects: {ok_count}") + print(f"Errors: {len(errors)}") + print(f"Warnings: {len(warnings)}") + print("-" * 60) + + if errors: + print("\nERRORS (must fix before pipeline run):") + for e in errors: + print(f" [ERROR] {e}") + + if warnings: + print("\nWARNINGS (review recommended):") + for w in warnings: + print(f" [WARN] {w}") + + if not errors and not warnings: + print("\n ✓ Scene looks good! Pipeline should run successfully.") + print("=" * 60) + + return {"FINISHED"} + + # --------------------------------------------------------------------------- # Panel # --------------------------------------------------------------------------- @@ -352,6 +449,12 @@ class GARMENT_PT_panel(bpy.types.Panel): layout.separator() + # Check scene button + layout.separator() + row = layout.row(align=True) + row.scale_y = 1.5 + row.operator("garment.check_scene", icon="CHECKBOX_HLT") + # Scene overview box = layout.box() box.label(text="Scene Overview", icon="SCENE_DATA") @@ -379,6 +482,7 @@ classes = [ GARMENT_OT_auto_detect, GARMENT_OT_clear_props, GARMENT_OT_sync_tags, + GARMENT_OT_check_scene, GARMENT_PT_panel, ] diff --git a/assets/blender/characters/transfer_shape_keys.py b/assets/blender/characters/transfer_shape_keys.py index 08219f6..cedaa98 100644 --- a/assets/blender/characters/transfer_shape_keys.py +++ b/assets/blender/characters/transfer_shape_keys.py @@ -137,9 +137,10 @@ def load_target_file(target_path): # Store information about target objects without keeping references target_objects_info = [] + required_props = {'age', 'sex', 'slot'} for obj in bpy.data.objects: if obj.type == 'MESH' and obj.data: - if all(prop in obj for prop in ['age', 'sex', 'slot']): + if all(prop in obj for prop in required_props): obj_info = { 'name': obj.name, 'age': obj['age'], @@ -157,6 +158,24 @@ def load_target_file(target_path): if obj_info['ref_shapes']: print(f" - ref_shapes: '{obj_info['ref_shapes']}'") + if not target_objects_info: + print("\n" + "=" * 70) + print("WARNING: No target objects found with required properties (age, sex, slot)") + print("=" * 70) + print("\nThis likely means the combine_clothes.py step did not properly") + print("propagate age/sex/slot properties from body objects to combined objects,") + print("or the source blend file only contains helper/reference objects.") + print("\nAvailable mesh objects and their properties:") + for obj in bpy.data.objects: + if obj.type == 'MESH' and obj.data: + props = [k for k in obj.keys() if not k.startswith('_')] + has_req = all(p in obj for p in required_props) + marker = " [OK]" if has_req else " [MISSING age/sex/slot]" + print(f" - {obj.name}: {props}{marker}") + print("\nReturning empty target list. The pipeline will skip shape key transfer.") + print("=" * 70) + return target_objects_info, temp_target + return target_objects_info, temp_target def get_target_object_by_name(obj_name): @@ -1006,7 +1025,14 @@ def main(): if not target_objects_info: print("\nNo target objects found with required properties") - sys.exit(1) + print("Skipping shape key transfer. Saving target file as-is.") + # Save the target file as-is (no shape keys transferred) + bpy.ops.wm.save_as_mainfile(filepath=output_file) + print(f"\nSaved output (unchanged): {output_file}") + print("=" * 60) + print("Script completed (no shape keys transferred)") + print("=" * 60) + return print(f"\nFound {len(target_objects_info)} target objects to process") diff --git a/src/features/editScene/CMakeLists.txt b/src/features/editScene/CMakeLists.txt index 071773a..8cf8171 100644 --- a/src/features/editScene/CMakeLists.txt +++ b/src/features/editScene/CMakeLists.txt @@ -347,6 +347,7 @@ set(EDITSCENE_HEADERS ) add_executable(editSceneEditor ${EDITSCENE_SOURCES} ${EDITSCENE_HEADERS}) +add_dependencies(editSceneEditor morph) # Define JPH_DEBUG_RENDERER for physics debug drawing target_compile_definitions(editSceneEditor PRIVATE JPH_DEBUG_RENDERER)