#!/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 -- """ 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 -- ") 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}")