Character hair physics implementation
This commit is contained in:
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user