#!/usr/bin/env python3 """Headless Blender script for GLB → VRM conversion. Invoked via: blender --background --python scripts/vrm_export.py -- \\ --input rigged_model.glb \\ --output avatar.vrm \\ --name "Avatar Name" \\ --author "DaviesTechLabs Pipeline" Requires: - Blender 4.x - VRM Add-on for Blender (https://vrm-addon-for-blender.info/en/) """ import argparse import sys # Blender's bpy is only available when running inside Blender try: import bpy # type: ignore[import-untyped] except ImportError: print("ERROR: This script must be run inside Blender.") print("Usage: blender --background --python scripts/vrm_export.py -- --input model.glb --output model.vrm") sys.exit(1) def parse_args() -> argparse.Namespace: """Parse arguments after the '--' separator in Blender CLI.""" argv = sys.argv[sys.argv.index("--") + 1 :] if "--" in sys.argv else [] parser = argparse.ArgumentParser(description="Convert rigged GLB to VRM") parser.add_argument("--input", required=True, help="Path to rigged GLB file") parser.add_argument("--output", required=True, help="Output VRM file path") parser.add_argument("--name", default="Generated Avatar", help="Avatar display name") parser.add_argument("--author", default="DaviesTechLabs Pipeline", help="Author name") return parser.parse_args(argv) def clear_scene() -> None: """Remove all objects from the scene.""" bpy.ops.wm.read_factory_settings(use_empty=True) def import_glb(path: str) -> None: """Import a GLB file into the current scene.""" bpy.ops.import_scene.gltf(filepath=path) print(f"Imported: {path}") print(f" Objects: {[obj.name for obj in bpy.data.objects]}") def find_armature() -> "bpy.types.Object": """Find the armature object in the scene.""" armatures = [obj for obj in bpy.data.objects if obj.type == "ARMATURE"] if not armatures: print("ERROR: No armature found in imported model.") print(" Available objects:", [obj.name for obj in bpy.data.objects]) sys.exit(1) if len(armatures) > 1: print(f"WARNING: Multiple armatures found, using first: {armatures[0].name}") return armatures[0] def configure_vrm_metadata(armature: "bpy.types.Object", name: str, author: str) -> None: """Set VRM 1.0 metadata on the armature.""" # The VRM Add-on stores metadata as a custom property armature["vrm_addon_extension"] = { "spec_version": "1.0", "vrm0": { "meta": { "title": name, "author": author, "allowedUserName": "Everyone", "violentUsageName": "Disallow", "sexualUsageName": "Disallow", "commercialUsageName": "Allow", "licenseName": "MIT", } }, } print(f" VRM metadata: name={name}, author={author}") def export_vrm(path: str) -> None: """Export the scene as VRM.""" bpy.ops.export_scene.vrm(filepath=path) print(f"Exported VRM: {path}") def main() -> None: args = parse_args() print(f"Converting GLB to VRM:") print(f" Input: {args.input}") print(f" Output: {args.output}") print(f" Name: {args.name}") clear_scene() import_glb(args.input) armature = find_armature() bpy.context.view_layer.objects.active = armature armature.select_set(True) configure_vrm_metadata(armature, args.name, args.author) export_vrm(args.output) print("Done.") if __name__ == "__main__": main()