23dfactory / backend /scripts /blender /rig_mesh_blender.py
LogicalTrue
Fix EXT_texture_webp GLB import error in Blender by upgrading to Blender 4.2 LTS and sanitizing glTF extension headers
48672d3
Raw
History Blame Contribute Delete
14 kB
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])