76 lines
4.3 KiB
Python
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))
|