import sys import os try: import bpy import mathutils except ImportError: print("[Error] This script must be run inside Blender's python environment.") sys.exit(1) def strip_gltf_extensions(glb_path): pass def run_blender_quadruped_rigging(input_path, output_path): print(f"[Blender Rigging] Loading quadruped model: {input_path}") # 1. Reset scene bpy.ops.wm.read_factory_settings(use_empty=True) # 2. Import GLB bpy.ops.import_scene.gltf(filepath=input_path) # 3. Find all mesh objects and join them into a single character mesh mesh_objs = [obj for obj in bpy.context.scene.objects if obj.type == 'MESH'] if not mesh_objs: print("[Error] No mesh found in imported GLTF.") sys.exit(1) print(f"[Blender Rigging] Found {len(mesh_objs)} meshes. Joining them...") bpy.ops.object.select_all(action='DESELECT') for obj in mesh_objs: obj.select_set(True) bpy.context.view_layer.objects.active = mesh_objs[0] bpy.ops.object.join() mesh_obj = bpy.context.active_object mesh_obj.name = "CharacterMesh" print(f"[Blender Rigging] Joined mesh: {mesh_obj.name}") # Ensure transforms are applied so bounding box calculations are correct bpy.ops.object.select_all(action='DESELECT') mesh_obj.select_set(True) bpy.context.view_layer.objects.active = mesh_obj bpy.ops.object.transform_apply(location=True, rotation=True, scale=True) # 4. Calculate bounding box bbox = [mesh_obj.matrix_world @ mathutils.Vector(corner) for corner in mesh_obj.bound_box] min_x = min([v.x for v in bbox]) max_x = max([v.x for v in bbox]) min_y = min([v.y for v in bbox]) max_y = max([v.y for v in bbox]) min_z = min([v.z for v in bbox]) max_z = max([v.z for v in bbox]) width = max_x - min_x depth = max_y - min_y height = max_z - min_z center_x = (min_x + max_x) / 2.0 center_y = (min_y + max_y) / 2.0 print(f"[Blender Rigging] Mesh Bounds: Height={height:.4f}, Width={width:.4f}, Depth={depth:.4f}") print(f"[Blender Rigging] Mesh Center: X={center_x:.4f}, Y={center_y:.4f}, Min Z={min_z:.4f}") # 5. Define relative quadruped bone joints # positive Y in Blender goes back, negative Y goes front z_spine = min_z + height * 0.65 z_head = min_z + height * 0.85 z_neck = min_z + height * 0.75 z_rear_hip = min_z + height * 0.60 z_rear_knee = min_z + height * 0.35 z_rear_ankle = min_z + height * 0.12 z_rear_foot = min_z + height * 0.02 z_front_shoulder = min_z + height * 0.62 z_front_elbow = min_z + height * 0.35 z_front_wrist = min_z + height * 0.12 z_front_foot = min_z + height * 0.02 y_rear = center_y + depth * 0.28 y_front = center_y - depth * 0.25 y_head = center_y - depth * 0.42 y_neck = center_y - depth * 0.30 y_tail_base = center_y + depth * 0.32 y_tail_tip = center_y + depth * 0.55 x_offset_rear = width * 0.22 x_offset_front = width * 0.22 x_rear_l = center_x - x_offset_rear x_rear_r = center_x + x_offset_rear x_front_l = center_x - x_offset_front x_front_r = center_x + x_offset_front joints = { # Spine & Head "Pelvis": ((center_x, y_rear, z_spine), (center_x, center_y, z_spine)), "Spine": ((center_x, center_y, z_spine), (center_x, y_front, z_spine)), "Neck": ((center_x, y_front, z_spine), (center_x, y_neck, z_neck)), "Head": ((center_x, y_neck, z_neck), (center_x, y_head, z_head)), "Tail": ((center_x, y_tail_base, z_spine), (center_x, y_tail_tip, z_spine - height * 0.1)), # Left Rear Leg "LeftUpLegRear": ((x_rear_l, y_rear, z_rear_hip), (x_rear_l, y_rear, z_rear_knee)), "LeftLegRear": ((x_rear_l, y_rear, z_rear_knee), (x_rear_l, y_rear, z_rear_ankle)), "LeftFootRear": ((x_rear_l, y_rear, z_rear_ankle), (x_rear_l, y_rear - depth * 0.05, z_rear_foot)), # Right Rear Leg "RightUpLegRear": ((x_rear_r, y_rear, z_rear_hip), (x_rear_r, y_rear, z_rear_knee)), "RightLegRear": ((x_rear_r, y_rear, z_rear_knee), (x_rear_r, y_rear, z_rear_ankle)), "RightFootRear": ((x_rear_r, y_rear, z_rear_ankle), (x_rear_r, y_rear - depth * 0.05, z_rear_foot)), # Left Front Leg "LeftUpLegFront": ((x_front_l, y_front, z_front_shoulder), (x_front_l, y_front, z_front_elbow)), "LeftLegFront": ((x_front_l, y_front, z_front_elbow), (x_front_l, y_front, z_front_wrist)), "LeftFootFront": ((x_front_l, y_front, z_front_wrist), (x_front_l, y_front - depth * 0.05, z_front_foot)), # Right Front Leg "RightUpLegFront": ((x_front_r, y_front, z_front_shoulder), (x_front_r, y_front, z_front_elbow)), "RightLegFront": ((x_front_r, y_front, z_front_elbow), (x_front_r, y_front, z_front_wrist)), "RightFootFront": ((x_front_r, y_front, z_front_wrist), (x_front_r, y_front - depth * 0.05, z_front_foot)) } # 6. Create Armature print("[Blender Rigging] Creating Armature...") arm_data = bpy.data.armatures.new(name="QuadrupedArmature") rig_obj = bpy.data.objects.new(name="QuadrupedRig", object_data=arm_data) bpy.context.scene.collection.objects.link(rig_obj) bpy.context.view_layer.objects.active = rig_obj bpy.ops.object.mode_set(mode='EDIT') edit_bones = arm_data.edit_bones bone_objects = {} for name, (head, tail) in joints.items(): bone = edit_bones.new(name) bone.head = head bone.tail = tail bone_objects[name] = bone # Setup hierarchy bone_objects["Spine"].parent = bone_objects["Pelvis"] bone_objects["Neck"].parent = bone_objects["Spine"] bone_objects["Head"].parent = bone_objects["Neck"] bone_objects["Tail"].parent = bone_objects["Pelvis"] # Rear legs hierarchy bone_objects["LeftUpLegRear"].parent = bone_objects["Pelvis"] bone_objects["LeftLegRear"].parent = bone_objects["LeftUpLegRear"] bone_objects["LeftFootRear"].parent = bone_objects["LeftLegRear"] bone_objects["RightUpLegRear"].parent = bone_objects["Pelvis"] bone_objects["RightLegRear"].parent = bone_objects["RightUpLegRear"] bone_objects["RightFootRear"].parent = bone_objects["RightLegRear"] # Front legs hierarchy bone_objects["LeftUpLegFront"].parent = bone_objects["Spine"] bone_objects["LeftLegFront"].parent = bone_objects["LeftUpLegFront"] bone_objects["LeftFootFront"].parent = bone_objects["LeftLegFront"] bone_objects["RightUpLegFront"].parent = bone_objects["Spine"] bone_objects["RightLegFront"].parent = bone_objects["RightUpLegFront"] bone_objects["RightFootFront"].parent = bone_objects["RightLegFront"] bpy.ops.object.mode_set(mode='OBJECT') # 7. Parent mesh to Armature using a Voxel Proxy for 100% reliable weighting print("[Blender Rigging] Creating watertight Voxel Proxy mesh for auto-weighting calculation...") bpy.ops.object.select_all(action='DESELECT') mesh_obj.select_set(True) bpy.context.view_layer.objects.active = mesh_obj bpy.ops.object.duplicate(linked=False) proxy_obj = bpy.context.active_object proxy_obj.name = "VoxelProxyMesh" bbox_size = max(proxy_obj.dimensions) voxel_size = max(0.003, bbox_size / 150.0) print(f"[Blender Rigging] Remeshing proxy with voxel size: {voxel_size:.4f}") try: proxy_obj.data.remesh_voxel_size = voxel_size bpy.ops.object.voxel_remesh() print("[Blender Rigging] ✓ Voxel Remesh completed on proxy.") except Exception as re_err: print(f"[Blender Rigging] Voxel Remesh failed on proxy: {re_err}") bpy.ops.object.select_all(action='DESELECT') proxy_obj.select_set(True) rig_obj.select_set(True) bpy.context.view_layer.objects.active = rig_obj proxy_rig_success = False try: bpy.ops.object.parent_set(type='ARMATURE_AUTO') print("[Blender Rigging] ✓ Parented Voxel Proxy with automatic weights successfully!") proxy_rig_success = True except Exception as parent_err: print(f"[Blender Rigging] Error: Auto weighting failed even on proxy: {parent_err}") if proxy_rig_success: print("[Blender Rigging] Transferring skin weights from Voxel Proxy to original detailed mesh...") bpy.ops.object.select_all(action='DESELECT') mesh_obj.select_set(True) rig_obj.select_set(True) bpy.context.view_layer.objects.active = rig_obj bpy.ops.object.parent_set(type='ARMATURE_NAME') bpy.ops.object.select_all(action='DESELECT') mesh_obj.select_set(True) bpy.context.view_layer.objects.active = mesh_obj dt_mod = mesh_obj.modifiers.new(name="WeightTransfer", type='DATA_TRANSFER') dt_mod.object = proxy_obj dt_mod.use_vert_data = True dt_mod.data_types_verts = {'VGROUP_WEIGHTS'} dt_mod.vert_mapping = 'POLYINTERP_NEAREST' bpy.ops.object.datalayout_transfer(modifier="WeightTransfer") bpy.ops.object.modifier_apply(modifier="WeightTransfer") print("[Blender Rigging] ✓ Weights transferred successfully.") bpy.ops.object.select_all(action='DESELECT') proxy_obj.select_set(True) bpy.ops.object.delete() print("[Blender Rigging] Deleted temporary Voxel Proxy.") else: print("[Blender Rigging] Falling back to default empty weights parenting on original mesh...") bpy.ops.object.select_all(action='DESELECT') mesh_obj.select_set(True) rig_obj.select_set(True) bpy.context.view_layer.objects.active = rig_obj bpy.ops.object.parent_set(type='ARMATURE') # 8. Export rigged model to FBX print(f"[Blender Rigging] Exporting rigged model to FBX: {output_path}") bpy.ops.object.select_all(action='DESELECT') mesh_obj.select_set(True) rig_obj.select_set(True) try: bpy.ops.export_scene.fbx( filepath=output_path, use_selection=True, path_mode='COPY', embed_textures=True, add_leaf_bones=False ) print("[Blender Rigging] ✓ Rigged FBX exported successfully.") except Exception as fbx_err: print(f"[Blender Rigging] Error: FBX export failed: {fbx_err}") sys.exit(1) # Export rigged GLB for Web visualization glb_output_path = output_path.replace('.fbx', '.glb') print(f"[Blender Rigging] Exporting rigged model to GLB: {glb_output_path}") try: bpy.ops.export_scene.gltf( filepath=glb_output_path, export_format='GLB', use_selection=True ) print("[Blender Rigging] ✓ Rigged GLB exported successfully.") except Exception as glb_err: print(f"[Blender Rigging] Warning: GLB export failed: {glb_err}") if __name__ == "__main__": try: args_idx = sys.argv.index("--") args = sys.argv[args_idx + 1:] except ValueError: args = [] if len(args) < 2: print("[Error] Usage: blender --background --python rig_quadruped_blender.py -- ") sys.exit(1) run_blender_rigging(args[0], args[1])