Character hair physics implementation

This commit is contained in:
2026-06-16 01:22:20 +03:00
parent 765dffbed0
commit 0a5edacf8a
26 changed files with 1629 additions and 225 deletions
+17 -34
View File
@@ -459,41 +459,24 @@ function(add_shape_key_propagation INPUT_BLEND OUTPUT_BLEND)
)
endfunction()
add_shape_key_propagation(
${CMAKE_CURRENT_SOURCE_DIR}/edited-normal-male.blend
${CMAKE_CURRENT_BINARY_DIR}/edited-normal-male-shapes.blend
)
add_shape_key_propagation(
${CMAKE_CURRENT_SOURCE_DIR}/edited-normal-female.blend
${CMAKE_CURRENT_BINARY_DIR}/edited-normal-female-shapes.blend
)
# male
set(SLOT_LIST "bottom" "top" "feet" "hair")
set(SLOT_INPUT "edited-normal-male-shapes.blend")
list(GET SLOT_LIST -1 LAST_SLOT)
foreach (SLOT ${SLOT_LIST})
set(SLOT_OUTPUT "edited-normal-male-consolidated-${SLOT}.blend")
if (SLOT STREQUAL LAST_SLOT)
set(SLOT_OUTPUT "edited-normal-male-consolidated.blend")
endif()
add_clothes_pipeline("${CMAKE_CURRENT_BINARY_DIR}/${SLOT_INPUT}"
"${CMAKE_CURRENT_BINARY_DIR}/clothes/clothes-male-${SLOT}_weighted.blend"
"${CMAKE_CURRENT_BINARY_DIR}/${SLOT_OUTPUT}"
# male and female clothes pipeline
foreach (SEX ${SEX_LIST})
add_shape_key_propagation(
${CMAKE_CURRENT_SOURCE_DIR}/edited-normal-${SEX}.blend
${CMAKE_CURRENT_BINARY_DIR}/edited-normal-${SEX}-shapes.blend
)
set(SLOT_INPUT ${SLOT_OUTPUT})
endforeach()
# female
set(SLOT_INPUT "edited-normal-female-shapes.blend")
foreach (SLOT ${SLOT_LIST})
set(SLOT_OUTPUT "edited-normal-female-consolidated-${SLOT}.blend")
if (SLOT STREQUAL LAST_SLOT)
set(SLOT_OUTPUT "edited-normal-female-consolidated.blend")
endif()
add_clothes_pipeline("${CMAKE_CURRENT_BINARY_DIR}/${SLOT_INPUT}"
"${CMAKE_CURRENT_BINARY_DIR}/clothes/clothes-female-${SLOT}_weighted.blend"
"${CMAKE_CURRENT_BINARY_DIR}/${SLOT_OUTPUT}"
)
set(SLOT_INPUT ${SLOT_OUTPUT})
set(SLOT_INPUT "edited-normal-${SEX}-shapes.blend")
foreach (SLOT ${SLOT_LIST})
set(SLOT_OUTPUT "edited-normal-${SEX}-consolidated-${SLOT}.blend")
if (SLOT STREQUAL LAST_SLOT)
set(SLOT_OUTPUT "edited-normal-${SEX}-consolidated.blend")
endif()
add_clothes_pipeline("${CMAKE_CURRENT_BINARY_DIR}/${SLOT_INPUT}"
"${CMAKE_CURRENT_BINARY_DIR}/clothes/clothes-${SEX}-${SLOT}_weighted.blend"
"${CMAKE_CURRENT_BINARY_DIR}/${SLOT_OUTPUT}"
)
set(SLOT_INPUT ${SLOT_OUTPUT})
endforeach()
endforeach()
Binary file not shown.
+26 -43
View File
@@ -151,16 +151,19 @@ def process_clothing_pair(clothing_obj, target_obj, whitelist, is_clothing_copy=
print(f"Processing: {clothing_name_for_final} -> {target_name} (using copy)")
# NEW CODE: Store custom properties from clothing before they might be lost
# Store custom properties from clothing before they might be lost
clothing_props = {}
for prop_name in ["ref_shapes", "ref_ray_length"]:
for prop_name in ["ref_shapes", "ref_ray_length",
"has_own_armature", "own_armature_name"]:
if prop_name in clothing_obj:
clothing_props[prop_name] = clothing_obj[prop_name]
print(f" Stored property '{prop_name}' = {clothing_obj[prop_name]} from clothing")
# Also store metadata tags for export pipeline
clothing_meta = {}
for prop_name in ["ref_layer", "ref_part", "garment_id", "tags", "age", "sex", "slot"]:
for prop_name in ["ref_layer", "ref_part", "garment_id", "tags",
"age", "sex", "slot",
"has_own_armature", "own_armature_name"]:
if prop_name in clothing_obj:
clothing_meta[prop_name] = clothing_obj[prop_name]
print(f" Stored meta '{prop_name}' = {clothing_obj[prop_name]} from clothing")
@@ -181,18 +184,31 @@ def process_clothing_pair(clothing_obj, target_obj, whitelist, is_clothing_copy=
whitelist.add(master_arm)
# Handle clothing armature (if it's not a copy for layer 2)
has_own_arm = clothing_obj.get("has_own_armature", False)
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)
# Keep hair armatures (objects with own skeleton)
if not has_own_arm:
if old_arm not in whitelist:
bpy.data.objects.remove(old_arm, do_unlink=True)
else:
whitelist.add(old_arm) # preserve hair skeleton
elif not is_clothing_copy and has_own_arm:
# Hair has own skeleton but parent is None (or not an armature);
# find the armature by name and preserve it.
own_arm_name = clothing_obj.get("own_armature_name", "")
old_arm = bpy.data.objects.get(own_arm_name)
if old_arm and old_arm.type == 'ARMATURE' and old_arm not in whitelist:
whitelist.add(old_arm)
print(f" Preserved hair armature '{old_arm.name}' by name for {clothing_name_for_final}")
# 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:
if master_arm and not clothing_obj.get("has_own_armature", False):
clothing_obj.parent = master_arm
for mod in clothing_obj.modifiers:
if mod.type == 'ARMATURE':
@@ -210,11 +226,6 @@ def process_clothing_pair(clothing_obj, target_obj, whitelist, is_clothing_copy=
bpy.ops.object.join()
# ---- Fix: ensure clothing faces keep the clothing's material ----
# After join, Blender may have assigned clothing faces to a
# pre-existing slot with the same base name but older texture
# references (loaded from the body blend). Find the slot that
# actually holds the clothing's original material data block
# and reassign any faces that landed in a stale look-alike slot.
if clothing_mat:
clothing_slot = None
stale_slot = None
@@ -238,15 +249,12 @@ def process_clothing_pair(clothing_obj, target_obj, whitelist, is_clothing_copy=
f"{stale_slot} -> clothing slot {clothing_slot}")
# ----------------------------------------------------------------
# NEW CODE: Copy stored custom properties to combined object
# After joining, new_target is the active object and contains the combined mesh
# Copy stored custom properties to combined object
for prop_name, prop_value in clothing_props.items():
new_target[prop_name] = prop_value
print(f" Copied property '{prop_name}' = {prop_value} to combined object")
# NEW CODE: Aggregate clothing metadata onto combined object
# Collect from all clothing objects processed for this target
# Use JSON string since Blender ID properties don't support Python lists/sets
# Aggregate clothing metadata onto combined object
if "_combined_meta" in new_target:
meta = json.loads(new_target["_combined_meta"])
else:
@@ -297,17 +305,14 @@ def run_batch_combine():
all_objs = bpy.data.objects
# Separate body objects (no ref_layer property)
# 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
@@ -316,7 +321,6 @@ def run_batch_combine():
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 = []
@@ -335,7 +339,6 @@ def run_batch_combine():
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 = []
@@ -343,7 +346,6 @@ def run_batch_combine():
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}.")
@@ -362,16 +364,11 @@ def run_batch_combine():
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
# PROCESS LAYER 1
print("\n=== PROCESSING LAYER 1 CLOTHING ===")
for clothing_obj in clothing_layer1:
if "ref_part" not in clothing_obj:
@@ -385,7 +382,6 @@ def run_batch_combine():
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)
@@ -399,27 +395,20 @@ def run_batch_combine():
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)
@@ -429,7 +418,7 @@ def run_batch_combine():
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)
# PROCESS LAYER 2 (Second Pass): Combine directly with body parts
print("\n=== PROCESSING LAYER 2 CLOTHING (Second Pass - Direct to Body) ===")
layer2_results_direct = []
@@ -447,7 +436,6 @@ def run_batch_combine():
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)
@@ -455,7 +443,6 @@ def run_batch_combine():
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 ===")
@@ -464,7 +451,6 @@ def run_batch_combine():
print(f"Layer 2 direct to body results: {len(layer2_results_direct)}")
print(f"Total combined objects: {len(all_results)}")
# Finalize aggregated metadata on all combined objects
for obj in all_results:
whitelist.add(obj)
if "_combined_meta" in obj:
@@ -490,16 +476,13 @@ def run_batch_combine():
print(f"Finalized metadata for {obj.name}: layer={obj['layer']}, garments={obj['garments']}, tags={obj['clothing_tags']}")
# Remove temporary meta property
del obj["_combined_meta"]
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()
+41 -20
View File
@@ -16,13 +16,10 @@ def process_append(source_files, output_path):
print(f"Warning: Source file not found: {file_path}")
continue
# ---- Pre-import materials from source blend ----
# Blender silently reuses existing materials when appending
# objects, even if the incoming one has different properties
# (texture references). Force materials to be imported first
# so texture changes in the source .blend survive.
# ---- Pre-import materials and armatures from source blend ----
with bpy.data.libraries.load(file_path, link=False) as (data_from, data_to):
data_to.materials = data_from.materials
data_to.objects = data_from.objects
imported_materials = {}
for mat in data_to.materials:
if mat is None:
@@ -33,15 +30,26 @@ def process_append(source_files, output_path):
f"different data; incoming material will "
f"replace it on appended objects.")
imported_materials[mat.name] = mat
# ----------------------------------------------------
with bpy.data.libraries.load(file_path) as (data_from, data_to):
data_to.objects = data_from.objects
# Also track imported armatures
imported_armatures = {}
for obj in data_to.objects:
if obj is None:
continue
if obj.type == 'ARMATURE':
imported_armatures[obj.name] = obj
# ----------------------------------------------------
for obj in data_to.objects:
if obj is None: continue
# Check criteria
# Keep armatures from the source blend
if obj.type == 'ARMATURE':
bpy.context.collection.objects.link(obj)
print(f"Imported armature '{obj.name}' from source blend")
continue
# Check criteria for body part meshes
has_props = all(p in obj.keys() for p in required_props)
if obj.type == 'MESH' and has_props:
# 1. Link to the scene root temporarily
@@ -61,17 +69,36 @@ def process_append(source_files, output_path):
# 2. Synchronize Names
obj.data.name = obj.name
# 3. Find Target Armature
# 3. Armature handling
has_own_arm = obj.get("has_own_armature", False)
if has_own_arm:
own_arm_name = obj.get("own_armature_name", "")
own_arm = bpy.data.objects.get(own_arm_name)
if not own_arm and own_arm_name in imported_armatures:
own_arm = imported_armatures[own_arm_name]
if own_arm:
# Keep hair's own armature; don't reparent
# to body armature.
# Fix the Armature modifier to point to the
# hair armature (combine_clothes may have
# changed it to the body rig).
arm_mod = next((m for m in obj.modifiers
if m.type == 'ARMATURE'), None)
if arm_mod:
arm_mod.object = own_arm
obj.parent = own_arm
print(f"Processed {obj.name}: keeping own "
f"armature '{own_arm_name}'")
continue
# Normal body part: parent to body armature
arm_name = obj.get("sex")
arm_obj = bpy.data.objects.get(arm_name)
if arm_obj and arm_obj.type == 'ARMATURE':
# A. Handle Collections: Move mesh to armature's collections
# Remove from all current collections first
# A. Handle Collections
for col in obj.users_collection:
col.objects.unlink(obj)
# Link to every collection the armature belongs to
for col in arm_obj.users_collection:
col.objects.link(obj)
@@ -82,28 +109,22 @@ def process_append(source_files, output_path):
arm_mod = next((m for m in obj.modifiers if m.type == 'ARMATURE'), None)
if not arm_mod:
arm_mod = obj.modifiers.new(name="Armature", type='ARMATURE')
arm_mod.object = arm_obj
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)
# 4. Recursive Purge of all unlinked data (Materials, Textures, Meshes)
bpy.ops.outliner.orphans_purge(do_local_ids=True, do_linked_ids=True, do_recursive=True)
# 5. Fix seams across ALL body parts in the consolidated scene.
# This synchronizes shape key offsets and normals for matching vertices
# across base body parts and combined clothing variants.
print("\nFixing seams across all consolidated body parts...")
fix_seams_across_objects()
fix_normals_across_objects()
+81 -21
View File
@@ -72,7 +72,7 @@ def process_batch():
# 3. Locate Reference Library
target_lib_name = f"normal_{age}_{sex}.blend"
target_lib_path = os.path.join(lib_directory, target_lib_name)
rig_name = str(sex)
rig_name = str(sex)
if not os.path.exists(target_lib_path):
if target_lib_name == "normal_adult_male.blend":
@@ -94,11 +94,48 @@ def process_batch():
source_mesh = bpy.data.objects.get(ref_mesh_name)
rig = bpy.data.objects.get(rig_name)
# ---- Detect hair with its own skeleton ----
has_own_arm = False
own_arm_name = None
own_arm = None
print(f" Checking {name} modifiers for own armature:")
for mod in clothing.modifiers:
print(f" type={mod.type} object={mod.object}")
if mod.type == 'ARMATURE':
arm = mod.object
if arm is None:
print(f" mod.object is None, trying by name...")
continue
print(f" arm.name='{arm.name}' rig_name='{rig_name}'")
if arm.name != rig_name:
has_own_arm = True
own_arm_name = arm.name
own_arm = arm
break
if has_own_arm and own_arm:
clothing["has_own_armature"] = True
clothing["own_armature_name"] = own_arm_name
# Ensure hair armature is linked to scene so parent/modifier
# references survive the save.
try:
bpy.context.collection.objects.link(own_arm)
except RuntimeError:
pass # Already linked
print(f" DETECTED own armature '{own_arm_name}' on {name}")
else:
print(f" No own armature on {name} (rig='{rig_name}')")
# -------------------------------------------
# 5. Prep Objects (Apply Scale & Clear Animation)
bpy.context.view_layer.objects.active = clothing
bpy.ops.object.transform_apply(location=False, rotation=True, scale=True)
for item in [clothing, rig]:
# Don't clear animation on the hair armature if it has
# its own skeleton (we want to preserve hair animations)
items_to_clear = [clothing]
if not has_own_arm:
items_to_clear.append(rig)
for item in items_to_clear:
if item.animation_data: item.animation_data_clear()
if item.type == 'ARMATURE':
bpy.context.view_layer.objects.active = item
@@ -106,30 +143,54 @@ def process_batch():
bpy.ops.pose.transforms_clear()
bpy.ops.object.mode_set(mode='OBJECT')
# 6. Weight Transfer
clothing.vertex_groups.clear()
dt = clothing.modifiers.new(name="WT", type='DATA_TRANSFER')
dt.object = source_mesh
dt.use_vert_data = True
dt.data_types_verts = {'VGROUP_WEIGHTS'}
dt.layers_vgroup_select_src = 'ALL'
dt.vert_mapping = 'POLYINTERP_NEAREST'
bpy.context.view_layer.objects.active = clothing
# "Generate Data Layers" step
bpy.ops.object.datalayout_transfer(modifier=dt.name)
bpy.ops.object.modifier_apply(modifier=dt.name)
remove_empty_vertex_groups(clothing, threshold=0.001)
# Also clear the body rig's animation (always)
if has_own_arm:
for item in [rig]:
if item.animation_data: item.animation_data_clear()
if item.type == 'ARMATURE':
bpy.context.view_layer.objects.active = item
bpy.ops.object.mode_set(mode='POSE')
bpy.ops.pose.transforms_clear()
bpy.ops.object.mode_set(mode='OBJECT')
# Set garment_id from object name if not already set
if "garment_id" not in clothing:
clothing["garment_id"] = clothing.name
# 6. Weight Transfer
if has_own_arm:
# Hair with its own skeleton already has correct vertex
# groups for its hair bones; body weight transfer would
# destroy them.
print(f"Skipped weight transfer for {name}: has own skeleton")
else:
clothing.vertex_groups.clear()
dt = clothing.modifiers.new(name="WT", type='DATA_TRANSFER')
dt.object = source_mesh
dt.use_vert_data = True
dt.data_types_verts = {'VGROUP_WEIGHTS'}
dt.layers_vgroup_select_src = 'ALL'
dt.vert_mapping = 'POLYINTERP_NEAREST'
bpy.context.view_layer.objects.active = clothing
# "Generate Data Layers" step
bpy.ops.object.datalayout_transfer(modifier=dt.name)
bpy.ops.object.modifier_apply(modifier=dt.name)
remove_empty_vertex_groups(clothing, threshold=0.001)
# 7. Final Parenting
clothing.parent = rig
arm_mod = clothing.modifiers.new(name="Armature", type='ARMATURE')
arm_mod.object = rig
if has_own_arm:
# Hair with its own skeleton: keep the existing Armature
# modifier (which points to the hair skeleton). Just
# parent to the hair armature; do NOT add a body
# Armature modifier.
clothing.parent = own_arm
print(f"Parented {name} to hair armature '{own_arm_name}'")
else:
clothing.parent = rig
arm_mod = clothing.modifiers.new(name="Armature", type='ARMATURE')
arm_mod.object = rig
# 8. Cleanup Reference Mesh (keep the Rig!)
bpy.data.objects.remove(source_mesh, do_unlink=True)
@@ -147,4 +208,3 @@ def process_batch():
if __name__ == "__main__":
process_batch()
+160 -1
View File
@@ -248,6 +248,27 @@ for mapping in[CommandLineMapping()]:
discovered.append(ob.name)
mapping.objs += discovered
print(f"Auto-discovered {len(discovered)} body part objects: {discovered}")
# ---- Separate hair with its own skeleton ----
hair_own_skel = []
hair_own_skel_objs = [] # save object references
for name in list(mapping.objs):
obj = bpy.data.objects.get(name)
if obj:
has_it = obj.get("has_own_armature", False)
own_arm = obj.get("own_armature_name", "")
print(f" Object '{name}': has_own_armature="
f"{has_it}, own_armature_name='{own_arm}'")
if has_it:
hair_own_skel.append(name)
hair_own_skel_objs.append(obj)
mapping.objs.remove(name)
if hair_own_skel:
print(f"Hair with own skeleton (exported separately): "
f"{hair_own_skel}")
else:
print(f"No hair with own skeleton detected")
# ---------------------------------------------
else:
bpy.ops.wm.append(
filepath=os.path.join(mapping.blend_path, mapping.inner_path),
@@ -266,6 +287,8 @@ for mapping in[CommandLineMapping()]:
bpy.data.objects.remove(ob)
# Remove auto-discovered objects that weren't meant for export
elif mapping.auto_discover and ob.type == 'MESH' and ob.name not in mapping.objs:
if ob.get("has_own_armature", False):
continue # exported separately, don't rename
if not ob.name.endswith("-noimp"):
ob.name = ob.name + "-noimp"
@@ -294,11 +317,28 @@ for mapping in[CommandLineMapping()]:
bpy.data.actions.remove(act)
for obj in bpy.data.objects:
if obj.type == 'MESH':
# Skip hair-with-own-skeleton objects — they're
# exported separately and must not be renamed
if obj.get("has_own_armature", False):
continue
if not obj.name in mapping.objs and obj.parent is None:
if not obj.name.endswith("-noimp"):
obj.name = obj.name + "-noimp"
bpy.ops.wm.save_as_mainfile(filepath=(basepath + "/assets/blender/scripts/" + mapping.outfile))
# Hide hair-with-own-skeleton objects from the body glTF export.
# The glTF exporter crashes when sampling animations for hair armatures
# that are not part of the main rig, so we exclude them here and export
# hair separately via OGRE later.
for obj in hair_own_skel_objs:
obj.hide_viewport = True
obj.hide_render = True
arm_name = obj.get("own_armature_name", "")
hair_arm = bpy.data.objects.get(arm_name)
if hair_arm:
hair_arm.hide_viewport = True
hair_arm.hide_render = True
os.makedirs(os.path.dirname(mapping.gltf_path), exist_ok=True)
bpy.ops.export_scene.gltf(filepath=mapping.gltf_path,
use_selection=False,
@@ -317,7 +357,7 @@ for mapping in[CommandLineMapping()]:
use_mesh_edges=False,
use_mesh_vertices=False,
export_cameras=False,
use_visible=False,
use_visible=True,
use_renderable=False,
export_yup=True,
export_animations=True,
@@ -331,6 +371,16 @@ for mapping in[CommandLineMapping()]:
export_lights=False,
export_skins=True)
print("exported to: " + mapping.gltf_path)
# Unhide hair objects for OGRE export
for obj in hair_own_skel_objs:
obj.hide_viewport = False
obj.hide_render = False
arm_name = obj.get("own_armature_name", "")
hair_arm = bpy.data.objects.get(arm_name)
if hair_arm:
hair_arm.hide_viewport = False
hair_arm.hide_render = False
obj_names = mapping.objs
prefix = mapping.armature_name + "_"
@@ -556,6 +606,115 @@ for mapping in[CommandLineMapping()]:
else:
print(f"WARNING: fix_ogre_mesh_seams.py not found at {fix_script}")
# ---- Export hair with own skeleton ----
print(f"\n=== HAIR DEBUG: auto_discover={mapping.auto_discover} "
f"hair_own_skel={hair_own_skel}")
if mapping.auto_discover and hair_own_skel:
# Collect hair meshes grouped by their armature name
hair_by_arm = {}
for obj in hair_own_skel_objs:
arm_name = obj.get("own_armature_name", "")
print(f" HAIR DEBUG: obj '{obj.name}' "
f"own_armature_name='{arm_name}' "
f"has_own_armature={obj.get('has_own_armature', False)}")
if arm_name not in hair_by_arm:
hair_by_arm[arm_name] = []
hair_by_arm[arm_name].append(obj)
print(f" HAIR DEBUG: hair_by_arm has {len(hair_by_arm)} groups")
body_arm_name = mapping.armature_name # save "male"/"female"
body_prefix = body_arm_name + "_"
# Debug: list all armatures in the scene
all_arms = [o.name for o in bpy.data.objects
if o.type == 'ARMATURE']
print(f" HAIR DEBUG: armatures in scene: {all_arms}")
for arm_name, hair_objs in hair_by_arm.items():
hair_arm = bpy.data.objects.get(arm_name)
print(f" HAIR DEBUG: arm_name='{arm_name}' "
f"hair_arm_found={hair_arm is not None}")
if not hair_arm or hair_arm.type != 'ARMATURE':
print(f" WARNING: Armature '{arm_name}' not found "
f"for hair, skipping")
continue
hair_obj_names = [o.name for o in hair_objs]
print(f" Exporting hair with skeleton '{arm_name}': "
f"{hair_obj_names}")
# Sync armature data name
hair_arm.data.name = hair_arm.name
print(f" Armature data name: '{hair_arm.data.name}'")
# Write body_part JSON for each hair mesh
json_dir = os.path.dirname(mapping.gltf_path)
for obj in hair_objs:
new_mesh_name = body_prefix + obj.name
obj.data.name = new_mesh_name
save_data = {
"age": obj.get("age", ""),
"sex": obj.get("sex", ""),
"slot": obj.get("slot", ""),
"mesh": obj.data.name + ".mesh",
"layer": int(obj.get("layer", 0)),
"garments": [],
"tags": [],
"shape_keys": [],
"own_skeleton": True,
"attach_to_bone": "mixamorig:Head"
}
garments = obj.get("garments", "")
if garments:
save_data["garments"] = [
g.strip()
for g in str(garments).split(";")
if g.strip()]
clothing_tags = obj.get("clothing_tags", "")
if clothing_tags:
save_data["tags"] = [
t.strip()
for t in str(clothing_tags).split(";")
if t.strip()]
save_file = (json_dir + "/body_part_" +
obj.data.name + ".json")
with open(save_file, 'w') as f:
json.dump(save_data, f, indent=2)
print(f" Wrote {save_file}")
# OGRE export for hair with its own skeleton
# OGRE export for hair with its own skeleton
hair_scene = mapping.gltf_path.replace(
".glb", "_hair_" + arm_name + ".scene")
bpy.ops.ogre.export(
filepath=hair_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')
print(f" Exported hair scene: {hair_scene}")
# Fix alpha_rejection in hair .material files
_mat_files = _glob.glob(
os.path.join(ogre_export_dir, "*.material"))
for _mf in _mat_files:
with open(_mf, 'r') as _f:
_content = _f.read()
if 'alpha_rejection' in _content:
_new = _re.sub(
r'alpha_rejection (\S+) (\d+\.?\d*)',
lambda m: 'alpha_rejection ' +
m.group(1) + ' ' +
str(int(float(m.group(2)))),
_content)
if _new != _content:
with open(_mf, 'w') as _f:
_f.write(_new)
# -----------------------------------------
bpy.ops.wm.read_homefile(use_empty=True)
time.sleep(2)
bpy.ops.wm.quit_blender()