Ship the WeChat hot-update catalogs and the camp, TideWhale T2, UI atlas, and animation-lab work.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,83 @@
|
||||
"""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)
|
||||
|
||||
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',ROOT/'Tools/output/lmgirl_tpose_studio/static/LmGirlTpose.fbx',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(ROOT/'Tools/output/lmgirl_tpose_studio/static/LmGirlTpose.fbx'))
|
||||
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))
|
||||
Reference in New Issue
Block a user