144 lines
5.8 KiB
Python
144 lines
5.8 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
DEPRECATED — do not use. Kept only to explain what it did and why it was wrong.
|
|
|
|
This discarded a character's skeleton so autorig.py could refit the library one.
|
|
It "solved" a bone-NAMING problem by destroying the asset: on Taila it cost 21
|
|
skirt bones, ~50 hair bones, 8 limb twist bones, split 18 per-part meshes into
|
|
one blob, and turned 17 both-legs-at-once vertices into 2817. Every runtime
|
|
weight repair in characters/skin_leg_repair.gd exists to undo its output.
|
|
|
|
The naming problem is solved properly in tools/rig_map.py, which pairs two
|
|
skeletons by resolved ROLE instead of by name, so tools/retarget.py can move the
|
|
animation library onto a character's own rig and keep everything. Use that.
|
|
|
|
Original description follows.
|
|
|
|
Strip an existing rig from a character GLB so tools/autorig.py re-rigs it on
|
|
the animation-library skeleton.
|
|
|
|
Some downloaded characters ship pre-rigged (VRoid J_Bip_*, Mixamo, custom
|
|
names). tools/merge_animations.py retargets by bone NAME from the library
|
|
armature, so a foreign skeleton silently produces flat 2-key clips — a
|
|
rest-pose statue (see docs/ASSET_SOURCES.md). The fix is to discard the
|
|
foreign rig entirely and let autorig fit the library's DEF-* skeleton.
|
|
|
|
The mesh keeps its REST pose: armature modifiers are removed (not applied),
|
|
so whatever pose the file rests in (usually T/A-pose) is what autorig sees.
|
|
|
|
Usage:
|
|
blender --background --python tools/strip_rig.py -- <input.glb> <output.glb>
|
|
"""
|
|
import bpy
|
|
import sys
|
|
|
|
argv = sys.argv
|
|
argv = argv[argv.index("--") + 1:] if "--" in argv else []
|
|
if len(argv) < 2:
|
|
print("Usage: blender --background --python tools/strip_rig.py -- <in.glb> <out.glb>")
|
|
sys.exit(1)
|
|
|
|
INPUT, OUTPUT = argv[0], argv[1]
|
|
|
|
bpy.ops.wm.read_factory_settings(use_empty=True)
|
|
if INPUT.lower().endswith((".glb", ".gltf")):
|
|
bpy.ops.import_scene.gltf(filepath=INPUT)
|
|
elif INPUT.lower().endswith(".fbx"):
|
|
bpy.ops.import_scene.fbx(filepath=INPUT)
|
|
else:
|
|
print(f"ERROR: unsupported input {INPUT}")
|
|
sys.exit(1)
|
|
|
|
all_meshes = [o for o in bpy.data.objects if o.type == "MESH"]
|
|
if not all_meshes:
|
|
print("ERROR: no meshes in input")
|
|
sys.exit(1)
|
|
|
|
|
|
def is_skinned(obj) -> bool:
|
|
if any(m.type == "ARMATURE" for m in obj.modifiers):
|
|
return True
|
|
return obj.parent is not None and obj.parent.type == "ARMATURE"
|
|
|
|
|
|
# The CHARACTER is whatever was skinned to the rig. Anything else is scene
|
|
# dressing (bases, dioramas, floating props) that must not become part of
|
|
# the player model.
|
|
meshes = [o for o in all_meshes if is_skinned(o)]
|
|
if not meshes:
|
|
print("WARNING: no skinned meshes found — keeping all meshes")
|
|
meshes = all_meshes
|
|
else:
|
|
for obj in [o for o in all_meshes if o not in meshes]:
|
|
print(f"Dropping unskinned prop mesh: {obj.name}")
|
|
bpy.data.objects.remove(obj, do_unlink=True)
|
|
|
|
# Drop armature modifiers and parenting, keep the rest-pose mesh data.
|
|
for obj in meshes:
|
|
for mod in [m for m in obj.modifiers if m.type == "ARMATURE"]:
|
|
obj.modifiers.remove(mod)
|
|
if obj.parent and obj.parent.type == "ARMATURE":
|
|
world = obj.matrix_world.copy()
|
|
obj.parent = None
|
|
obj.matrix_world = world
|
|
# Old vertex groups reference the dead rig; autorig makes fresh ones.
|
|
obj.vertex_groups.clear()
|
|
|
|
for obj in [o for o in bpy.data.objects if o.type == "ARMATURE"]:
|
|
bpy.data.objects.remove(obj, do_unlink=True)
|
|
|
|
# Join into one mesh so autorig binds everything (hair/clothes included).
|
|
bpy.ops.object.select_all(action="DESELECT")
|
|
for obj in meshes:
|
|
obj.select_set(True)
|
|
bpy.context.view_layer.objects.active = meshes[0]
|
|
if len(meshes) > 1:
|
|
bpy.ops.object.join()
|
|
|
|
# Recentre: feet on the ground at the world origin (imports sometimes place
|
|
# the character far off-origin inside a larger scene).
|
|
joined = bpy.context.view_layer.objects.active
|
|
bpy.ops.object.transform_apply(location=True, rotation=True, scale=True)
|
|
xs = [v.co.x for v in joined.data.vertices]
|
|
ys = [v.co.y for v in joined.data.vertices]
|
|
zs = [v.co.z for v in joined.data.vertices]
|
|
cx = (min(xs) + max(xs)) / 2.0
|
|
cy = (min(ys) + max(ys)) / 2.0
|
|
floor = min(zs) # Blender is Z-up; glTF exporter converts to Y-up
|
|
for v in joined.data.vertices:
|
|
v.co.x -= cx
|
|
v.co.y -= cy
|
|
v.co.z -= floor
|
|
|
|
# Normalize materials: some anime models ship UNLIT (black base color, albedo
|
|
# in the emissive texture). Our pipeline shades with the toon shader off the
|
|
# ALBEDO, so rewire each material's first image texture into Principled Base
|
|
# Color and drop the emission trick — else the character renders pitch black.
|
|
for mat in joined.data.materials:
|
|
if not mat or not mat.use_nodes:
|
|
continue
|
|
nt = mat.node_tree
|
|
tex_node = next((n for n in nt.nodes if n.type == "TEX_IMAGE" and n.image), None)
|
|
principled = next((n for n in nt.nodes if n.type == "BSDF_PRINCIPLED"), None)
|
|
if principled is None:
|
|
principled = nt.nodes.new("ShaderNodeBsdfPrincipled")
|
|
out = next((n for n in nt.nodes if n.type == "OUTPUT_MATERIAL"), None)
|
|
if out is None:
|
|
out = nt.nodes.new("ShaderNodeOutputMaterial")
|
|
for l in list(out.inputs["Surface"].links):
|
|
nt.links.remove(l)
|
|
nt.links.new(principled.outputs["BSDF"], out.inputs["Surface"])
|
|
if tex_node:
|
|
base = principled.inputs["Base Color"]
|
|
for l in list(base.links):
|
|
nt.links.remove(l)
|
|
nt.links.new(tex_node.outputs["Color"], base)
|
|
print(f"Material '{mat.name}': routed texture '{tex_node.image.name}' to base color")
|
|
# Kill the emission trick so the toon shader owns the look.
|
|
if "Emission Strength" in principled.inputs:
|
|
principled.inputs["Emission Strength"].default_value = 0.0
|
|
|
|
bpy.ops.export_scene.gltf(filepath=OUTPUT, export_format="GLB", export_skins=False,
|
|
export_animations=False, export_apply=True)
|
|
print(f"Done. Stripped rig -> {OUTPUT}")
|