Files
Papay-Shooter/tools/strip_rig.py

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}")