Spaces:
Build error
Build error
LogicalTrue
Fix EXT_texture_webp GLB import error in Blender by upgrading to Blender 4.2 LTS and sanitizing glTF extension headers
48672d3 | 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 -- <input_glb> <output_fbx>") | |
| sys.exit(1) | |
| run_blender_rigging(args[0], args[1]) | |