86 lines
4.8 KiB
Python
86 lines
4.8 KiB
Python
"""Blender regression: real characters, legacy input and repeated FBX conversion."""
|
|
import sys,json,math,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[3]
|
|
sys.path.insert(0,str(ROOT/'Tools/model_tools'))
|
|
from bl_decimate import prepare_game_export,export_fbx_source_yup,validate_fbx_yup_scene
|
|
|
|
OUT=ROOT/'Tools/output/lmgirl_tpose_studio/axis_pipeline_audit/regression'
|
|
OUT.mkdir(parents=True,exist_ok=True)
|
|
LEGACY=OUT.parent/'legacy/LmGirlTpose.fbx'
|
|
if not LEGACY.is_file():LEGACY=ROOT/'Tools/output/lmgirl_tpose_studio/static/LmGirlTpose.fbx'
|
|
|
|
def points(obj):return np.array([obj.matrix_world@v.co for v in obj.data.vertices])
|
|
def uv(obj):return np.array([tuple(v.uv) for v in obj.data.uv_layers.active.data])
|
|
def snapshot(obj):return dict(name=obj.name,vertices=np.array([tuple(v.co) for v in obj.data.vertices]),uv=uv(obj),matrix=np.array(obj.matrix_world),objects=len(bpy.data.objects),meshes=len(bpy.data.meshes))
|
|
def unchanged(obj,before):
|
|
assert obj.name==before['name']
|
|
assert np.array_equal(np.array([tuple(v.co) for v in obj.data.vertices]),before['vertices'])
|
|
assert np.array_equal(uv(obj),before['uv'])
|
|
assert np.array_equal(np.array(obj.matrix_world),before['matrix'])
|
|
assert len(bpy.data.objects)==before['objects'] and len(bpy.data.meshes)==before['meshes']
|
|
assert bpy.context.view_layer.objects.active==obj and set(bpy.context.selected_objects)=={obj}
|
|
|
|
def run(label,path,restore):
|
|
bpy.ops.wm.read_factory_settings(use_empty=True)
|
|
if path.suffix=='.fbx':bpy.ops.import_scene.fbx(filepath=str(path),use_anim=False)
|
|
else:bpy.ops.import_scene.gltf(filepath=str(path),merge_vertices=False)
|
|
obj=next(o for o in bpy.context.scene.objects if o.type=='MESH')
|
|
prepare_game_export(obj,restore_axis=restore)
|
|
before=snapshot(obj)
|
|
conversion=Matrix.Rotation(math.pi/2,4,'X')
|
|
expected=np.array([conversion@obj.matrix_world@v.co for v in obj.data.vertices])
|
|
target=OUT/(label+'.fbx')
|
|
export_fbx_source_yup(str(target));unchanged(obj,before)
|
|
tree,_=parse_fbx.parse(str(target))
|
|
settings=next(e for e in tree.elems if e.id==b'GlobalSettings')
|
|
props=next(e for e in settings.elems if e.id==b'Properties70')
|
|
globals={p.props[0].decode():list(p.props[4:]) for p in props.elems}
|
|
assert globals['UpAxis']==[1] and globals['UpAxisSign']==[1]
|
|
objects=next(e for e in tree.elems if e.id==b'Objects')
|
|
for model in [e for e in objects.elems if e.id==b'Model']:
|
|
props=next(e for e in model.elems if e.id==b'Properties70')
|
|
values={p.props[0].decode():list(p.props[4:]) for p in props.elems}
|
|
assert max(abs(v) for v in values.get('Lcl Rotation',[0,0,0]))<1e-5
|
|
assert all(abs(v-1)<1e-5 for v in values.get('Lcl Scaling',[1,1,1]))
|
|
bpy.ops.wm.read_factory_settings(use_empty=True)
|
|
bpy.ops.import_scene.fbx(filepath=str(target),use_anim=False)
|
|
obj=next(o for o in bpy.context.scene.objects if o.type=='MESH')
|
|
actual=points(obj);error=float(np.max(np.abs(actual-expected)))
|
|
assert actual.shape==expected.shape and error<2e-6,(label,error)
|
|
assert np.array_equal(uv(obj),before['uv']),(label,'UV changed')
|
|
assert np.argmax(np.ptp(actual,axis=0))==2,(label,'horizontal world geometry')
|
|
assert abs(actual[:,2].min())<2e-6
|
|
assert abs((actual[:,0].min()+actual[:,0].max())/2)<2e-6
|
|
assert abs((actual[:,1].min()+actual[:,1].max())/2)<2e-6
|
|
result=dict(case=label,path=str(target.relative_to(ROOT)),vertices=len(actual),max_position_error_m=error,
|
|
uv_unchanged=True,source_unchanged=True,upright=True,origin_bottom_center=True,zero_fbx_node_rotation=True)
|
|
return target,result
|
|
|
|
results=[]
|
|
girl=ROOT/'Tools/output/lmgirl_tpose_studio/model_ex.glb'
|
|
boy=ROOT/'Tools/output/campguide_studio/model_ex.glb'
|
|
_,r=run('boy_from_glb',boy,True);results.append(r)
|
|
path,r=run('girl_from_glb',girl,True);results.append(r)
|
|
for i in range(1,3):path,r=run('girl_repeat_'+str(i),path,None);results.append(r)
|
|
_,r=run('girl_from_legacy',LEGACY,None);results.append(r)
|
|
# Export failure must also restore selection, names, geometry and temporary data.
|
|
obj=next(o for o in bpy.context.scene.objects if o.type=='MESH')
|
|
prepare_game_export(obj);before=snapshot(obj)
|
|
try:export_fbx_source_yup(str(OUT/'missing_directory'/'fail.fbx'))
|
|
except (RuntimeError,OSError):pass
|
|
else:raise AssertionError('Expected export failure')
|
|
unchanged(obj,before)
|
|
results.append(dict(case='failure_cleanup',passed=True))
|
|
try:validate_fbx_yup_scene(str(LEGACY))
|
|
except ValueError:pass
|
|
else:raise AssertionError('Legacy rotated FBX passed the export guard')
|
|
results.append(dict(case='reject_legacy_rotation',passed=True))
|
|
(OUT/'results.json').write_text(json.dumps(results,indent=2),encoding='utf-8')
|
|
print('FBX_AXIS_REGRESSION '+json.dumps(results))
|