diff --git a/assets/blender/characters/clothes-male-bottom.blend b/assets/blender/characters/clothes-male-bottom.blend
index 30f8cfa..b38ae27 100644
--- a/assets/blender/characters/clothes-male-bottom.blend
+++ b/assets/blender/characters/clothes-male-bottom.blend
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:bd5e3372ee3c2c66f392d1b524a846cf71e5c5a4e50d0e18e573ee06a27f2baa
-size 1285768
+oid sha256:6b99b0f6c88d970d4cfb72a13d1815a0aa579107c358bddff76194dac7b2d6d1
+size 1835992
diff --git a/assets/blender/characters/combine_clothes.py b/assets/blender/characters/combine_clothes.py
index e857b70..0920066 100644
--- a/assets/blender/characters/combine_clothes.py
+++ b/assets/blender/characters/combine_clothes.py
@@ -5,145 +5,344 @@ import os
import mathutils
from mathutils.bvhtree import BVHTree
-def run_batch_combine():
- try:
- args = sys.argv[sys.argv.index("--") + 1:]
- body_blend_path, clothes_blend_path, output_path = args[0], args[1], args[2]
- except (ValueError, IndexError):
- print("Usage: blender -b -P script.py --
")
- return
-
- bpy.ops.wm.read_homefile(use_empty=True)
-
- # 1. Append Files
+def load_blend_files(clothes_blend_path, body_blend_path):
+ """Load objects from blend files and return all loaded objects"""
+ loaded_objects = []
+
for path in [clothes_blend_path, body_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:
- if obj: bpy.context.collection.objects.link(obj)
+ if obj:
+ bpy.context.collection.objects.link(obj)
+ loaded_objects.append(obj)
+
+ return loaded_objects
- all_objs = bpy.data.objects
- clothing_meshes = [o for o in all_objs if o.type == 'MESH' and "ref_part" in o]
- whitelist = set()
+def setup_bvh_and_matrices(obj):
+ """Setup BVH tree and transformation matrices for an object"""
+ depsgraph = bpy.context.evaluated_depsgraph_get()
+ obj_eval = obj.evaluated_get(depsgraph)
+ bvh = BVHTree.FromObject(obj_eval, depsgraph)
+
+ return bvh
- for cloth in clothing_meshes:
- target_name = cloth["ref_part"]
- target_body = all_objs.get(target_name)
- if not target_body: continue
+def get_transformation_matrices(obj):
+ """Get transformation matrices for an object"""
+ m = obj.matrix_world
+ m_inv = m.inverted()
+ m_normal = m.to_3x3().inverted().transposed()
+
+ return m, m_inv, m_normal
- # --- STEP A: RAYCAST & INITIAL HIT LIST ---
- depsgraph = bpy.context.evaluated_depsgraph_get()
- cloth_eval = cloth.evaluated_get(depsgraph)
- bvh_cloth = BVHTree.FromObject(cloth_eval, depsgraph)
+def raycast_and_adjust_vertices(target_body, bvh_cloth):
+ """Raycast from body to cloth and adjust vertices that intersect"""
+ m_body, m_body_inv, m_body_normal = get_transformation_matrices(target_body)
+
+ num_verts = len(target_body.data.vertices)
+ hit_values = [0] * num_verts
+ has_shape_keys = target_body.data.shape_keys is not None
+
+ # Forward raycast (into cloth)
+ for i, v in enumerate(target_body.data.vertices):
+ v_world = m_body @ v.co
+ n_world = (m_body_normal @ v.normal).normalized()
+
+ # Raycast forward (into cloth) and backward (from inside cloth)
+ hit_f, _, _, _ = bvh_cloth.ray_cast(v_world, n_world, 0.015)
+ hit_b, _, _, _ = bvh_cloth.ray_cast(v_world, -n_world, 0.005)
+
+ if hit_f or hit_b:
+ hit_values[i] = 1
+ # Adjust vertex position to be slightly outside cloth
+ offset = -n_world * (0.005 if hit_f else 0.01)
+ new_co = m_body_inv @ (v_world + offset)
+ v.co = new_co
+
+ # Update shape keys if they exist
+ if has_shape_keys:
+ for kb in target_body.data.shape_keys.key_blocks:
+ kb.data[i].co = new_co
+
+ return hit_values
- m_body = target_body.matrix_world
- m_body_inv = m_body.inverted()
- m_body_normal = m_body.to_3x3().inverted().transposed()
-
- num_verts = len(target_body.data.vertices)
- hit_values = [0] * num_verts
- has_shape_keys = target_body.data.shape_keys is not None
-
- for i, v in enumerate(target_body.data.vertices):
- v_world = m_body @ v.co
- n_world = (m_body_normal @ v.normal).normalized()
- hit_f, _, _, _ = bvh_cloth.ray_cast(v_world, n_world, 0.015)
- hit_b, _, _, _ = bvh_cloth.ray_cast(v_world, -n_world, 0.005)
-
- if hit_f or hit_b:
- hit_values[i] = 1
- offset = -n_world * (0.005 if hit_f else 0.01)
- new_co = m_body_inv @ (v_world + offset)
- v.co = new_co
- if has_shape_keys:
- for kb in target_body.data.shape_keys.key_blocks:
- kb.data[i].co = new_co
-
- # --- STEP B: MULTI-LAYER PROTECTION & DELETE ---
- bm = bmesh.new()
- bm.from_mesh(target_body.data)
- bm.verts.ensure_lookup_table()
-
- # Phase 1: Identify "Layer 1 Border" (Immediate neighbors of visible verts)
- border_l1 = set()
- for v in bm.verts:
- if hit_values[v.index] == 0:
- for edge in v.link_edges:
- neighbor = edge.other_vert(v)
- if hit_values[neighbor.index] == 1:
- border_l1.add(neighbor.index)
-
- # Phase 2: Identify "Layer 2 Buffer" (Neighbors of Layer 1)
- border_l2 = set()
- for idx in border_l1:
- v = bm.verts[idx]
+def protect_and_remove_hidden_geometry(target_body, hit_values, threshold=4.0):
+ """Protect visible vertices and remove hidden geometry"""
+ bm = bmesh.new()
+ bm.from_mesh(target_body.data)
+ bm.verts.ensure_lookup_table()
+
+ # Phase 1: Identify "Layer 1 Border" (Immediate neighbors of visible verts)
+ border_l1 = set()
+ for v in bm.verts:
+ if hit_values[v.index] == 0: # Visible vertex
for edge in v.link_edges:
neighbor = edge.other_vert(v)
if hit_values[neighbor.index] == 1:
- border_l2.add(neighbor.index)
-
- # Merge all protected vertices (Layer 0 (visible) + Layer 1 + Layer 2)
- protected_indices = set(border_l1) | set(border_l2)
- for i, val in enumerate(hit_values):
- if val == 0: protected_indices.add(i)
-
- # Deletion logic
- to_delete = []
- THRESHOLD = 4.0 # Higher threshold for deep cleaning
+ border_l1.add(neighbor.index)
+
+ # Phase 2: Identify "Layer 2 Buffer" (Neighbors of Layer 1)
+ border_l2 = set()
+ for idx in border_l1:
+ v = bm.verts[idx]
+ for edge in v.link_edges:
+ neighbor = edge.other_vert(v)
+ if hit_values[neighbor.index] == 1:
+ border_l2.add(neighbor.index)
+
+ # Merge all protected vertices
+ protected_indices = set(border_l1) | set(border_l2)
+ for i, val in enumerate(hit_values):
+ if val == 0: # Visible vertices
+ protected_indices.add(i)
+
+ # Deletion logic
+ to_delete = []
+ for v in bm.verts:
+ if v.index in protected_indices:
+ continue
- for v in bm.verts:
- if v.index in protected_indices:
- continue
-
- # Sum hits of neighbors
- neighbor_hit_sum = hit_values[v.index]
- for edge in v.link_edges:
- neighbor = edge.other_vert(v)
- neighbor_hit_sum += hit_values[neighbor.index]
-
- if neighbor_hit_sum >= THRESHOLD:
- to_delete.append(v)
- elif len(v.link_edges) == 1:
- to_delete.append(v)
-
- bmesh.ops.delete(bm, geom=to_delete, context='VERTS')
- bm.to_mesh(target_body.data)
- bm.free()
- target_body.data.update()
-
- # --- STEP C: ARMATURE & JOIN ---
- master_arm = target_body.parent if (target_body.parent and target_body.parent.type == 'ARMATURE') else None
- if master_arm: whitelist.add(master_arm)
+ # Sum hits of neighbors
+ neighbor_hit_sum = hit_values[v.index]
+ for edge in v.link_edges:
+ neighbor = edge.other_vert(v)
+ neighbor_hit_sum += hit_values[neighbor.index]
- cloth_label, body_label = cloth.name, target_body.name
+ if neighbor_hit_sum >= threshold:
+ to_delete.append(v)
+ elif len(v.link_edges) == 1: # Loose vertices
+ to_delete.append(v)
+
+ # Perform deletion
+ bmesh.ops.delete(bm, geom=to_delete, context='VERTS')
+ bm.to_mesh(target_body.data)
+ bm.free()
+ target_body.data.update()
- if cloth.parent and cloth.parent.type == 'ARMATURE':
- old_arm = cloth.parent
- cloth.matrix_world = cloth.matrix_world.copy()
- cloth.parent = None
- if old_arm not in whitelist: bpy.data.objects.remove(old_arm, do_unlink=True)
+def process_clothing_pair(clothing_obj, target_obj, whitelist, is_clothing_copy=False, original_clothing_name=None):
+ """Process a clothing-body pair
- if master_arm:
- cloth.parent = master_arm
- for mod in cloth.modifiers:
- if mod.type == 'ARMATURE': mod.object = master_arm
+ Args:
+ clothing_obj: The clothing object to process
+ target_obj: The target object to combine with (body or combined object)
+ whitelist: Set of objects to keep
+ is_clothing_copy: Whether clothing_obj is a copy (for layer 2 processing)
+ original_clothing_name: Original name of clothing if it's a copy (for layer 2 naming)
+ """
+ # Create a copy of the target object
+ new_target = target_obj.copy()
+ new_target.data = target_obj.data.copy()
+ bpy.context.collection.objects.link(new_target)
- bpy.ops.object.select_all(action='DESELECT')
- cloth.select_set(True)
- target_body.select_set(True)
- bpy.context.view_layer.objects.active = target_body
- bpy.ops.object.join()
-
- target_body.name = f"{body_label}_{cloth_label}"
- whitelist.add(target_body)
+ # Copy custom properties
+ for key in target_obj.keys():
+ new_target[key] = target_obj[key]
- # 4. Final Cleanup
+ # Ensure the copy has the same transformations
+ new_target.matrix_world = target_obj.matrix_world.copy()
+
+ target_name = target_obj.name
+
+ # Determine the name to use for the clothing in the final combined object
+ if is_clothing_copy and original_clothing_name:
+ clothing_name_for_final = original_clothing_name
+ else:
+ clothing_name_for_final = clothing_obj.name
+
+ print(f"Processing: {clothing_name_for_final} -> {target_name} (using copy)")
+
+ # Step A: Raycast & adjust vertices
+ bvh_cloth = setup_bvh_and_matrices(clothing_obj)
+ hit_values = raycast_and_adjust_vertices(new_target, bvh_cloth)
+
+ # Step B: Remove hidden geometry
+ protect_and_remove_hidden_geometry(new_target, hit_values)
+
+ # Step C: Handle armature and join
+ master_arm = new_target.parent if (new_target.parent and new_target.parent.type == 'ARMATURE') else None
+ if master_arm:
+ whitelist.add(master_arm)
+
+ # Handle clothing armature (if it's not a copy for layer 2)
+ if not is_clothing_copy and clothing_obj.parent and clothing_obj.parent.type == 'ARMATURE':
+ old_arm = clothing_obj.parent
+ clothing_obj.matrix_world = clothing_obj.matrix_world.copy()
+ clothing_obj.parent = None
+ if old_arm not in whitelist:
+ bpy.data.objects.remove(old_arm, do_unlink=True)
+
+ # For layer 2 clothing copies, we don't need to handle armature separately
+ # as they'll inherit from the target
+
+ # Reparent to master armature if exists
+ if master_arm:
+ clothing_obj.parent = master_arm
+ for mod in clothing_obj.modifiers:
+ if mod.type == 'ARMATURE':
+ mod.object = master_arm
+
+ # Join clothing with target copy
+ bpy.ops.object.select_all(action='DESELECT')
+ clothing_obj.select_set(True)
+ new_target.select_set(True)
+ bpy.context.view_layer.objects.active = new_target
+ bpy.ops.object.join()
+
+ # Rename the combined object using the appropriate clothing name
+ new_target.name = f"{target_name}_{clothing_name_for_final}"
+ whitelist.add(new_target)
+
+ return new_target
+
+def cleanup_unused_objects(whitelist):
+ """Remove all objects not in whitelist"""
for obj in bpy.data.objects[:]:
if obj.type in {'MESH', 'ARMATURE'} and obj not in whitelist:
bpy.data.objects.remove(obj, do_unlink=True)
-
+
bpy.ops.outliner.orphans_purge(do_local_ids=True, do_linked_ids=True, do_recursive=True)
+
+def run_batch_combine():
+ """Main function to run the batch combine process"""
+ try:
+ args = sys.argv[sys.argv.index("--") + 1:]
+ body_blend_path, clothes_blend_path, output_path = args[0], args[1], args[2]
+ except (ValueError, IndexError):
+ print("Usage: blender -b -P script.py -- ")
+ return
+
+ # Start fresh
+ bpy.ops.wm.read_homefile(use_empty=True)
+
+ # Load objects from blend files
+ loaded_objects = load_blend_files(clothes_blend_path, body_blend_path)
+
+ # Categorize loaded objects
+ 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]
+
+ # 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]
+
+ print(f"Found {len(body_objects)} body objects")
+ print(f"Found {len(clothing_layer1)} layer 1 clothing objects")
+ print(f"Found {len(clothing_layer2)} layer 2 clothing objects")
+
+ # Create dictionary of body objects for quick lookup
+ body_objects_dict = {obj.name: obj for obj in body_objects}
+
+ # Track objects to keep
+ whitelist = set()
+
+ # List to store combined objects from layer 1
+ combined_objects = []
+
+ # PROCESS LAYER 1: Combine with original body parts
+ print("\n=== PROCESSING LAYER 1 CLOTHING ===")
+ for clothing_obj in clothing_layer1:
+ if "ref_part" not in clothing_obj:
+ print(f"Warning: Layer 1 clothing '{clothing_obj.name}' missing ref_part property, skipping")
+ continue
+
+ target_name = clothing_obj["ref_part"]
+ original_body = body_objects_dict.get(target_name)
+
+ if not original_body or original_body.type != 'MESH':
+ print(f"Warning: Target body '{target_name}' not found for clothing '{clothing_obj.name}', skipping")
+ continue
+
+ # Process the pair
+ result = process_clothing_pair(clothing_obj, original_body, whitelist)
+ if result:
+ combined_objects.append(result)
+ print(f"Added '{result.name}' to combined objects list")
+
+ print(f"Layer 1 complete. Created {len(combined_objects)} combined objects")
+
+ # PROCESS LAYER 2 (First Pass): Combine with layer 1 results
+ print("\n=== PROCESSING LAYER 2 CLOTHING (First Pass - Over Layer 1) ===")
+ layer2_results_over_l1 = []
+
+ for clothing_obj in clothing_layer2:
+ print(f"\nProcessing layer 2 clothing: {clothing_obj.name}")
+
+ # Store the original clothing name for later use
+ original_clothing_name = clothing_obj.name
+
+ # For each combined object from layer 1
+ for combined_obj in combined_objects:
+ # Create a copy of the layer 2 clothing
+ clothing_copy = clothing_obj.copy()
+ clothing_copy.data = clothing_obj.data.copy()
+ bpy.context.collection.objects.link(clothing_copy)
+
+ # Copy custom properties
+ for key in clothing_obj.keys():
+ clothing_copy[key] = clothing_obj[key]
+
+ # Set the ref_part to point to the combined object
+ clothing_copy["ref_part"] = combined_obj.name
+
+ print(f" Combining with layer 1 result: {combined_obj.name}")
+
+ # Process the pair
+ result = process_clothing_pair(clothing_copy, combined_obj, whitelist,
+ is_clothing_copy=True,
+ original_clothing_name=original_clothing_name)
+ if result:
+ layer2_results_over_l1.append(result)
+ print(f" Created: {result.name}")
+
+ print(f"Layer 2 first pass complete. Created {len(layer2_results_over_l1)} combined objects")
+
+ # PROCESS LAYER 2 (Second Pass): Combine directly with body parts (like layer 1)
+ print("\n=== PROCESSING LAYER 2 CLOTHING (Second Pass - Direct to Body) ===")
+ layer2_results_direct = []
+
+ for clothing_obj in clothing_layer2:
+ if "ref_part" not in clothing_obj:
+ print(f"Warning: Layer 2 clothing '{clothing_obj.name}' missing ref_part property, skipping")
+ continue
+
+ target_name = clothing_obj["ref_part"]
+ original_body = body_objects_dict.get(target_name)
+
+ if not original_body or original_body.type != 'MESH':
+ print(f"Warning: Target body '{target_name}' not found for clothing '{clothing_obj.name}', skipping")
+ continue
+
+ print(f"\nProcessing layer 2 clothing directly with body: {clothing_obj.name} -> {target_name}")
+
+ # Process directly with body part (no copy needed for the clothing itself)
+ result = process_clothing_pair(clothing_obj, original_body, whitelist, is_clothing_copy=False)
+ if result:
+ layer2_results_direct.append(result)
+ print(f" Created: {result.name}")
+
+ print(f"Layer 2 second pass complete. Created {len(layer2_results_direct)} combined objects")
+
+ # Add all results to combined objects list
+ all_results = combined_objects + layer2_results_over_l1 + layer2_results_direct
+
+ print(f"\n=== SUMMARY ===")
+ print(f"Layer 1 results: {len(combined_objects)}")
+ print(f"Layer 2 over layer 1 results: {len(layer2_results_over_l1)}")
+ print(f"Layer 2 direct to body results: {len(layer2_results_direct)}")
+ print(f"Total combined objects: {len(all_results)}")
+
+ # Final cleanup - keep all combined objects and their armatures
+ for obj in all_results:
+ whitelist.add(obj)
+
+ cleanup_unused_objects(whitelist)
+
+ # Save the result
bpy.ops.wm.save_as_mainfile(filepath=output_path)
+ print(f"\nSaved to: {output_path}")
+
if __name__ == "__main__":
run_batch_combine()
diff --git a/assets/blender/scripts/export_models2.py b/assets/blender/scripts/export_models2.py
index d75400e..556325a 100644
--- a/assets/blender/scripts/export_models2.py
+++ b/assets/blender/scripts/export_models2.py
@@ -326,7 +326,7 @@ for mapping in[CommandLineMapping()]:
armobj = bpy.data.objects.get(mapping.armature_name)
armobj.data.name = armobj.name
- bpy.ops.ogre.export(filepath=mapping.gltf_path.replace(".glb", ".scene"), EX_SELECTED_ONLY=False, EX_SHARED_ARMATURE=True, EX_LOD_GENERATION='0', EX_LOD_DISTANCE=20, EX_GENERATE_TANGENTS='4')
+ bpy.ops.ogre.export(filepath=mapping.gltf_path.replace(".glb", ".scene"), EX_SELECTED_ONLY=False, EX_SHARED_ARMATURE=True, EX_LOD_GENERATION='0', EX_LOD_DISTANCE=20, EX_LOD_LEVELS=4, EX_GENERATE_TANGENTS='4')
bpy.ops.wm.read_homefile(use_empty=True)
time.sleep(2)