44 lines
3.6 KiB
Python
44 lines
3.6 KiB
Python
"""Fit only offending new sole vertices by 1mm in game space; leave rig, UV and clips unchanged."""
|
|
import bpy,sys,json,hashlib
|
|
from pathlib import Path
|
|
from mathutils import Vector
|
|
from mathutils.kdtree import KDTree
|
|
import numpy as np
|
|
|
|
argv=sys.argv[sys.argv.index('--')+1:]
|
|
folder=Path(argv[0]).resolve()
|
|
name=argv[1] if len(argv)>1 else 'GreatHornStudio'
|
|
# GreatHorn's reviewed fit touched exactly two soles; other actors fit whatever their own constraints flag.
|
|
expected=int(argv[2]) if len(argv)>2 else (2 if name=='GreatHornStudio' else -1)
|
|
rigged=folder/(name+'_Rigged.fbx')
|
|
bpy.ops.wm.read_factory_settings(use_empty=True);bpy.ops.import_scene.fbx(filepath=str(rigged))
|
|
arm=next(o for o in bpy.context.scene.objects if o.type=='ARMATURE');low=next(o for o in bpy.context.scene.objects if o.type=='MESH');arm.animation_data_clear();arm.data.pose_position='REST';bpy.context.view_layer.update()
|
|
world=[low.matrix_world@v.co for v in low.data.vertices];lo=Vector([min(v[i] for v in world) for i in range(3)]);hi=Vector([max(v[i] for v in world) for i in range(3)]);center=Vector(((lo.x+hi.x)/2,(lo.y+hi.y)/2,lo.z));height=hi.z-lo.z
|
|
data=json.loads((folder/'foot_constraints.json').read_text(encoding='utf-8'));frames=np.array(data['frames']);targets=[]
|
|
for j,v in enumerate(data['vertices']):
|
|
weights=np.array(v['weights'])
|
|
if weights.sum()>.95 and (frames[:,j,:]@weights).min()<-.003:targets.append(v)
|
|
assert expected<0 or len(targets)==expected,'Re-inspect a changed sole fit instead of applying stale edits'
|
|
normal=[(p-center)/height for p in world];candidates=[]
|
|
for sx in (-1,1):
|
|
for sz in (-1,1):
|
|
tree=KDTree(len(normal))
|
|
for i,p in enumerate(normal):tree.insert(Vector((sx*p.x,p.z,sz*p.y)),i)
|
|
tree.balance();errors=[tree.find(Vector(v['normalizedBind']))[2] for v in data['vertices']];candidates.append((max(errors),sx,sz,tree))
|
|
error,sx,sz,tree=min(candidates,key=lambda x:x[0]);assert error<.0001,('Unity/Blender bind coordinate mismatch',error)
|
|
uv=lambda:hashlib.sha256(np.array([tuple(v.uv) for v in low.data.uv_layers.active.data],dtype='<f4').tobytes()).hexdigest()
|
|
uv_before=uv();geometry_before=[v.co.copy() for v in low.data.vertices];bone_before={b.name:list(b.matrix_local) for b in arm.data.bones}
|
|
lift_game=.001;model_scale=data.get('modelScale',.758561)
|
|
delta=low.matrix_world.inverted().to_3x3()@Vector((0,0,lift_game/model_scale));edited=set()
|
|
for target in targets:
|
|
matches=tree.find_range(Vector(target['normalizedBind']),.00001);assert matches
|
|
for _,index,_ in matches:low.data.vertices[index].co+=delta;edited.add(index)
|
|
low.data.update();assert uv()==uv_before
|
|
for bone in arm.data.bones:assert all((a-b).length<1e-8 for a,b in zip(bone.matrix_local,bone_before[bone.name]))
|
|
arm.data.pose_position='POSE'
|
|
for bone in arm.pose.bones:bone.matrix_basis.identity()
|
|
bpy.ops.object.select_all(action='DESELECT');arm.select_set(True);low.select_set(True);bpy.context.view_layer.objects.active=arm
|
|
bpy.ops.export_scene.fbx(filepath=str(rigged),use_selection=True,object_types={'ARMATURE','MESH'},add_leaf_bones=False,bake_anim=False,armature_nodetype='NULL',axis_forward='-Z',axis_up='Y',apply_scale_options='FBX_SCALE_ALL',path_mode='COPY',embed_textures=True,mesh_smooth_type='FACE')
|
|
report=dict(method='local new sole fit to immutable original clips',gameSpaceLift=lift_game,blenderVertices=sorted(edited),unityVertices=[v['index'] for v in targets],coordinateMatchMax=error,uvUnchanged=uv()==uv_before,uvSha256=uv_before,bonesUnchanged=True,originalAnimationsUnchanged=True)
|
|
(folder/'sole_fit.json').write_text(json.dumps(report,indent=2),encoding='utf-8');print('MT_SUMMARY '+json.dumps(report))
|