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): try: with open(glb_path, "rb") as f: data = f.read() if len(data) < 20 or data[:4] != b'glTF': return json_len = int.from_bytes(data[12:16], byteorder='little') json_bytes = data[20:20+json_len] import json gltf_json = json.loads(json_bytes.decode('utf-8', errors='ignore')) modified = False for key in ['extensionsRequired', 'extensionsUsed']: if key in gltf_json and 'EXT_texture_webp' in gltf_json[key]: gltf_json[key].remove('EXT_texture_webp') modified = True if modified: new_bytes = json.dumps(gltf_json).encode('utf-8') if len(new_bytes) <= len(json_bytes): new_bytes = new_bytes.ljust(len(json_bytes), b' ') new_data = data[:20] + new_bytes + data[20+len(json_bytes):] with open(glb_path, "wb") as f: f.write(new_data) print(f"[Blender Rigging] Stripped EXT_texture_webp extension requirement from GLB.") except Exception as e: print(f"[Blender Rigging] Extension strip note: {e}") def run_blender_rigging(input_path, output_path): print(f"[Blender Rigging] Loading model: {input_path}") # 1. Reset scene bpy.ops.wm.read_factory_settings(use_empty=True) # 2. Import GLB strip_gltf_extensions(input_path) 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 humanoid bone joints # Let's map joints relative to the bounding box # z positions z_hips = min_z + height * 0.53 z_spine = min_z + height * 0.63 z_chest = min_z + height * 0.75 z_neck = min_z + height * 0.83 z_head = min_z + height * 0.95 # Arm z positions z_shoulder = min_z + height * 0.77 z_elbow = min_z + height * 0.75 z_wrist = min_z + height * 0.73 z_hand = min_z + height * 0.71 # Leg z positions z_hip_joint = min_z + height * 0.49 z_knee = min_z + height * 0.27 z_ankle = min_z + height * 0.07 z_foot_tip = min_z + height * 0.01 # X offsets (width-based) relative to center_x x_offset_leg = width * 0.14 x_offset_shoulder = width * 0.12 x_offset_elbow = width * 0.32 x_offset_wrist = width * 0.47 x_offset_hand = width * 0.54 # Left side X coordinates (negative X direction in standard front view) x_leg_l = center_x - x_offset_leg x_shoulder_l = center_x - x_offset_shoulder x_elbow_l = center_x - x_offset_elbow x_wrist_l = center_x - x_offset_wrist x_hand_l = center_x - x_offset_hand # Right side X coordinates (positive X direction in standard front view) x_leg_r = center_x + x_offset_leg x_shoulder_r = center_x + x_offset_shoulder x_elbow_r = center_x + x_offset_elbow x_wrist_r = center_x + x_offset_wrist x_hand_r = center_x + x_offset_hand # Y offsets (depth-based) y_center = center_y y_foot_tip = center_y - depth * 0.25 # foot goes forward (negative Y in Blender) # Joint positions dictionaries joints = { "Hips": ((center_x, y_center, z_hips), (center_x, y_center, z_spine)), "Spine": ((center_x, y_center, z_spine), (center_x, y_center, z_chest)), "Chest": ((center_x, y_center, z_chest), (center_x, y_center, z_neck)), "Neck": ((center_x, y_center, z_neck), (center_x, y_center, z_head)), # Left Arm "LeftShoulder": ((center_x, y_center, z_shoulder), (x_shoulder_l, y_center, z_shoulder)), "LeftArm": ((x_shoulder_l, y_center, z_shoulder), (x_elbow_l, y_center, z_elbow)), "LeftForeArm": ((x_elbow_l, y_center, z_elbow), (x_wrist_l, y_center, z_wrist)), "LeftHand": ((x_wrist_l, y_center, z_wrist), (x_hand_l, y_center, z_hand)), # Right Arm "RightShoulder": ((center_x, y_center, z_shoulder), (x_shoulder_r, y_center, z_shoulder)), "RightArm": ((x_shoulder_r, y_center, z_shoulder), (x_elbow_r, y_center, z_elbow)), "RightForeArm": ((x_elbow_r, y_center, z_elbow), (x_wrist_r, y_center, z_wrist)), "RightHand": ((x_wrist_r, y_center, z_wrist), (x_hand_r, y_center, z_hand)), # Left Leg "LeftUpLeg": ((x_leg_l, y_center, z_hip_joint), (x_leg_l, y_center, z_knee)), "LeftLeg": ((x_leg_l, y_center, z_knee), (x_leg_l, y_center, z_ankle)), "LeftFoot": ((x_leg_l, y_center, z_ankle), (x_leg_l, y_foot_tip, z_foot_tip)), # Right Leg "RightUpLeg": ((x_leg_r, y_center, z_hip_joint), (x_leg_r, y_center, z_knee)), "RightLeg": ((x_leg_r, y_center, z_knee), (x_leg_r, y_center, z_ankle)), "RightFoot": ((x_leg_r, y_center, z_ankle), (x_leg_r, y_foot_tip, z_foot_tip)) } # 6. Create Armature print("[Blender Rigging] Creating Armature...") arm_data = bpy.data.armatures.new(name="HumanoidArmature") rig_obj = bpy.data.objects.new(name="HumanoidRig", 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') # Build bones 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["Hips"] bone_objects["Chest"].parent = bone_objects["Spine"] bone_objects["Neck"].parent = bone_objects["Chest"] # Left Arm hierarchy bone_objects["LeftShoulder"].parent = bone_objects["Chest"] bone_objects["LeftArm"].parent = bone_objects["LeftShoulder"] bone_objects["LeftForeArm"].parent = bone_objects["LeftArm"] bone_objects["LeftHand"].parent = bone_objects["LeftForeArm"] # Right Arm hierarchy bone_objects["RightShoulder"].parent = bone_objects["Chest"] bone_objects["RightArm"].parent = bone_objects["RightShoulder"] bone_objects["RightForeArm"].parent = bone_objects["RightArm"] bone_objects["RightHand"].parent = bone_objects["RightForeArm"] # Left Leg hierarchy bone_objects["LeftUpLeg"].parent = bone_objects["Hips"] bone_objects["LeftLeg"].parent = bone_objects["LeftUpLeg"] bone_objects["LeftFoot"].parent = bone_objects["LeftLeg"] # Right Leg hierarchy bone_objects["RightUpLeg"].parent = bone_objects["Hips"] bone_objects["RightLeg"].parent = bone_objects["RightUpLeg"] bone_objects["RightFoot"].parent = bone_objects["RightLeg"] 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...") # Create a duplicate of the mesh to act as proxy 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" # Apply Voxel Remesh on the proxy to make it a single manifold shell 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}") # Parent the proxy mesh to the armature with automatic weights 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: # Transfer the weights from the proxy mesh to the original mesh_obj print("[Blender Rigging] Transferring skin weights from Voxel Proxy to original detailed mesh...") # Parent to Armature with empty groups (creates all vertex groups named after bones) # Armature MUST be the active object for this parenting to work and create vertex groups 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') # Now make mesh_obj the active object to configure and apply the modifier bpy.ops.object.select_all(action='DESELECT') mesh_obj.select_set(True) bpy.context.view_layer.objects.active = mesh_obj # Create Data Transfer modifier on original mesh 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' # Generate data layout (ensures vertex groups are populated) bpy.ops.object.datalayout_transfer(modifier="WeightTransfer") # Apply the Data Transfer modifier to bake the weights into the mesh_obj's vertex groups bpy.ops.object.modifier_apply(modifier="WeightTransfer") print("[Blender Rigging] ✓ Weights transferred successfully.") # Delete the proxy object bpy.ops.object.select_all(action='DESELECT') proxy_obj.select_set(True) bpy.ops.object.delete() print("[Blender Rigging] Deleted temporary Voxel Proxy.") else: # Fallback to direct parenting with empty weights if proxy rigging failed 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_mesh_blender.py -- ") sys.exit(1) run_blender_rigging(args[0], args[1])