Files
AIGame/Tools/model_tools/bl_fix_lmgirl_mixamo_upload.py

76 lines
4.3 KiB
Python

"""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'
LEGACY=ROOT/'Tools/output/lmgirl_tpose_studio/axis_pipeline_audit/legacy/LmGirlTpose.fbx'
if LEGACY.is_file():SOURCE=LEGACY
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))