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
+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()