Files
settled-reach/tooling/blender_extract_armature.py
T
jpmschweitzerandClaude Opus 4.6 23ec1ddebd feat(client): CharacterVisualDescriptor and compositor (#703, #704)
Add CharacterVisualDescriptor (11-variant BodyType enum, wire format
encode/decode, body_type_key mapping) and CharacterVisual compositor
(runtime 3D character assembler with slot architecture, BoneAttachment3D,
clothing coverage, recolor mask loading, skin tone tinting, bone
validation). Includes 64 unit tests and Blender utility scripts.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-20 07:48:47 +01:00

94 lines
2.9 KiB
Python

"""
blender_extract_armature.py
Usage: tooling/blender --background --python tooling/blender_extract_armature.py -- <input.gltf> <output.glb>
Loads a Quaternius GLTF file, strips all mesh objects and animation data,
and exports only the armature as a GLB skeleton file.
Requirements:
- 65-bone hierarchy preserved
- No mesh geometry
- No animation data
- Y-up, -Z forward (glTF default)
- 1 unit = 1 meter (Quaternius convention)
"""
import sys
import bpy
def extract_armature(input_path: str, output_path: str) -> None:
# Clear the default scene
bpy.ops.wm.read_factory_settings(use_empty=True)
# Import the GLTF/GLB
print(f"Loading: {input_path}")
bpy.ops.import_scene.gltf(filepath=input_path)
# Report what was imported
all_objects = list(bpy.context.scene.objects)
print(f"Imported {len(all_objects)} objects:")
for obj in all_objects:
print(f" {obj.name} ({obj.type})")
# Find armature objects
armatures = [obj for obj in all_objects if obj.type == 'ARMATURE']
if not armatures:
print("ERROR: No armature found in the imported file.")
sys.exit(1)
armature = armatures[0]
print(f"Armature: {armature.name}{len(armature.data.bones)} bones")
if len(armature.data.bones) < 60:
print(f"WARNING: Expected ~65 bones, found {len(armature.data.bones)}. Check compatibility.")
# Remove all non-armature objects (meshes, lights, cameras, empties)
bpy.ops.object.select_all(action='DESELECT')
for obj in all_objects:
if obj.type != 'ARMATURE':
obj.select_set(True)
bpy.ops.object.delete()
# Remove all animation data (skeleton only, no poses)
if armature.animation_data:
armature.animation_data_clear()
for action in list(bpy.data.actions):
bpy.data.actions.remove(action)
# Select only the armature for export
bpy.ops.object.select_all(action='DESELECT')
armature.select_set(True)
bpy.context.view_layer.objects.active = armature
# Export as GLB — skeleton only, no animations, no meshes
print(f"Exporting to: {output_path}")
bpy.ops.export_scene.gltf(
filepath=output_path,
use_selection=True,
export_format='GLB',
export_animations=False,
export_skins=True,
export_yup=True,
)
# Verify: report bone count and names
bones = sorted(armature.data.bones, key=lambda b: b.name)
print(f"Export complete. {len(bones)} bones:")
for bone in bones:
print(f" {bone.name}")
if __name__ == "__main__":
argv = sys.argv
if "--" not in argv:
print("Usage: tooling/blender --background --python tooling/blender_extract_armature.py -- <input.gltf> <output.glb>")
sys.exit(1)
args = argv[argv.index("--") + 1:]
if len(args) < 2:
print("ERROR: Provide input GLTF/GLB path and output GLB path.")
sys.exit(1)
extract_armature(args[0], args[1])