"""Create an upright Mixamo upload FBX from the legacy Y-up-in-Blender mesh. Do not replace the original: existing rigs and review assets use its coordinates. """ import sys,math,json,hashlib from pathlib import Path import bpy import numpy as np from mathutils import Matrix from io_scene_fbx import parse_fbx ROOT=Path(__file__).resolve().parents[2] SOURCE=ROOT/'Tools/output/lmgirl_tpose_studio/static/LmGirlTpose.fbx' OUT=ROOT/'Tools/output/lmgirl_tpose_studio/mixamo_upload' TARGET=OUT/'LmGirlTpose_Mixamo.fbx' def digest(path):return hashlib.sha256(path.read_bytes()).hexdigest() def vertices(o):return np.array([o.matrix_world@v.co for v in o.data.vertices],dtype=float) def uv(o):return np.array([tuple(v.uv) for v in o.data.uv_layers.active.data],dtype=np.float32) def triangles(o):return sum(len(p.vertices)-2 for p in o.data.polygons) OUT.mkdir(parents=True,exist_ok=True) before_hash=digest(SOURCE) bpy.ops.wm.read_factory_settings(use_empty=True) bpy.ops.import_scene.fbx(filepath=str(SOURCE),use_anim=False) meshes=[o for o in bpy.context.scene.objects if o.type=='MESH'] assert len(meshes)==1 and not any(o.type=='ARMATURE' for o in bpy.context.scene.objects) obj=meshes[0];old_points=vertices(obj);old_uv=uv(obj);old_triangles=triangles(obj) assert np.argmax(np.ptp(old_points,axis=0))==1,'This repair is only for the legacy Y-up Blender source' # Blender's exporter expects a Z-up scene. Restore that before its Y-up conversion. rotation=Matrix.Rotation(math.pi/2,4,'X') obj.matrix_world=rotation@obj.matrix_world bpy.context.view_layer.update() bpy.ops.object.select_all(action='DESELECT');obj.select_set(True);bpy.context.view_layer.objects.active=obj bpy.ops.object.transform_apply(location=True,rotation=True,scale=True) expected=vertices(obj) assert np.argmax(np.ptp(expected,axis=0))==2 bpy.ops.export_scene.fbx(filepath=str(TARGET),use_selection=True,object_types={'MESH'}, axis_forward='-Z',axis_up='Y',bake_space_transform=True, apply_unit_scale=True,apply_scale_options='FBX_SCALE_ALL',bake_anim=False, path_mode='STRIP',embed_textures=False) bpy.ops.wm.read_factory_settings(use_empty=True) bpy.ops.import_scene.fbx(filepath=str(TARGET),use_anim=False) restored=next(o for o in bpy.context.scene.objects if o.type=='MESH') points=vertices(restored);new_uv=uv(restored) assert points.shape==expected.shape and np.max(np.abs(points-expected))<2e-6 assert np.array_equal(new_uv,old_uv),'UVs changed' assert triangles(restored)==old_triangles tree,_=parse_fbx.parse(str(TARGET)) objects=next(e for e in tree.elems if e.id==b'Objects') model=next(e for e in objects.elems if e.id==b'Model') props=next(e for e in model.elems if e.id==b'Properties70') node_props={e.props[0].decode():list(e.props[4:]) for e in props.elems} assert max(abs(v) for v in node_props.get('Lcl Rotation',[0,0,0]))<1e-5 assert all(abs(v-1)<1e-5 for v in node_props.get('Lcl Scaling',[1,1,1])) geometry=next(e for e in objects.elems if e.id==b'Geometry') raw_points=np.array(next(e for e in geometry.elems if e.id==b'Vertices').props[0]).reshape(-1,3) assert np.max(np.abs(raw_points-old_points))<2e-6,'Y-up geometry/front changed' assert digest(SOURCE)==before_hash,'Original source changed' report=dict(source=str(SOURCE.relative_to(ROOT)),output=str(TARGET.relative_to(ROOT)), source_sha256=before_hash,output_sha256=digest(TARGET),source_unchanged=True, repair='Restore Blender Z-up scene (+90 X), apply object transforms, export Y-up with baked space conversion', vertices=len(points),triangles=old_triangles,uv_unchanged=True, raw_fbx_node_rotation_degrees=node_props.get('Lcl Rotation',[0,0,0]), raw_fbx_node_scale=node_props.get('Lcl Scaling',[1,1,1]), raw_yup_geometry_max_error_m=float(np.max(np.abs(raw_points-old_points))), max_roundtrip_position_error_m=float(np.max(np.abs(points-expected))), blender_roundtrip_rotation_degrees=[math.degrees(v) for v in restored.rotation_euler], blender_roundtrip_bounds_min=points.min(0).tolist(),blender_roundtrip_bounds_max=points.max(0).tolist(), online_validation='Not uploaded to Mixamo; local FBX metadata and independent reimport checked') (OUT/'repair.json').write_text(json.dumps(report,indent=2),encoding='utf-8') print(json.dumps(report,indent=2))