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,194 @@
|
||||
"""Measure bone trajectories independently of skinning for the shared walk."""
|
||||
import json
|
||||
import sys
|
||||
import math
|
||||
from pathlib import Path
|
||||
import bpy
|
||||
import numpy as np
|
||||
from mathutils import Matrix,Vector,Quaternion
|
||||
|
||||
ROOT=Path(__file__).resolve().parents[2]
|
||||
FOLDER=ROOT/'Tools/output/lmgirl_tpose_studio/rig_mixamo41'
|
||||
SOURCE=json.loads((ROOT/'Tools/output/campguide_studio/rig_mixamo41/unity_review/review_data.json').read_text())
|
||||
|
||||
def quaternion(q):return Quaternion((q[3],*q[:3]))
|
||||
def sample(data,frame):
|
||||
source_indices={b['name']:i for i,b in enumerate(SOURCE['bones'])}
|
||||
values=[SOURCE['clips'][0]['samples'][frame][source_indices[b['name']]] for b in data['bones']]
|
||||
source_rot=[];mapped=[];world=[]
|
||||
for i,(b,v) in enumerate(zip(data['bones'],values)):
|
||||
parent=b['parent'];q=quaternion(v[3:7]);source_rot.append(source_rot[parent]@q if parent>=0 else q)
|
||||
mapped.append(source_rot[i]@quaternion(data['bindRotationCorrections'][i]) if data.get('bindRotationCorrections') else source_rot[i])
|
||||
q=mapped[parent].inverted()@mapped[i] if parent>=0 else mapped[i]
|
||||
p=Vector(v[:3])
|
||||
if data.get('translationOffsets'):
|
||||
p*=data['translationScale']
|
||||
if data.get('translationDeltaRotations'):p=quaternion(data['translationDeltaRotations'][i])@p
|
||||
p+=Vector(data['translationOffsets'][i])
|
||||
local=Matrix.LocRotScale(p,q,Vector(v[7:10]));world.append(world[parent]@local if parent>=0 else local)
|
||||
return {b['name'].split(':')[-1]:world[i] for i,b in enumerate(data['bones'])}
|
||||
|
||||
def audit():
|
||||
results={}
|
||||
for label,data in [('boy',SOURCE),('girl_tokenrig',json.loads((FOLDER/'unity_review_data.json').read_text())),
|
||||
('girl_mixamo',json.loads((FOLDER/'website/unity_review_data.json').read_text()))]:
|
||||
samples=[]
|
||||
rest={b['name'].split(':')[-1]:Matrix(b['bindpose']).inverted() for b in data['bones']}
|
||||
for f in range(32):
|
||||
pose=sample(data,f);hips=pose['Hips'].translation
|
||||
lateral=(pose['LeftUpLeg'].translation-pose['RightUpLeg'].translation).normalized()
|
||||
feet={side:list(pose[side+'Foot'].translation-hips) for side in ['Left','Right']}
|
||||
gap=(pose['LeftFoot'].translation-pose['RightFoot'].translation).dot(lateral)
|
||||
knees=(pose['LeftLeg'].translation-pose['RightLeg'].translation).dot(lateral)
|
||||
samples.append({'frame':f+1,'footSignedGap':gap,'kneeSignedGap':knees,'feetRelativeHips':feet})
|
||||
results[label]={'restHipWidth':(rest['LeftUpLeg'].translation-rest['RightUpLeg'].translation).length,
|
||||
'restAnkleWidth':(rest['LeftFoot'].translation-rest['RightFoot'].translation).length,
|
||||
'leftRestLegVector':list(rest['LeftFoot'].translation-rest['LeftUpLeg'].translation),
|
||||
'minimumFootSignedGap':min(s['footSignedGap'] for s in samples),'minimumKneeSignedGap':min(s['kneeSignedGap'] for s in samples),
|
||||
'crossedFrames':[s['frame'] for s in samples if s['footSignedGap']<0],
|
||||
'samples':samples}
|
||||
(FOLDER/'gait_diagnosis_before.json').write_text(json.dumps(results,indent=2),encoding='utf-8')
|
||||
print(json.dumps({k:{n:v for n,v in d.items() if n!='samples'} for k,d in results.items()},indent=2))
|
||||
|
||||
def corrected_motion(data,label):
|
||||
names=[b['name'].split(':')[-1] for b in data['bones']];indices={n:i for i,n in enumerate(names)}
|
||||
parents=[b['parent'] for b in data['bones']]
|
||||
rest={b['name'].split(':')[-1]:Matrix(b['bindpose']).inverted() for b in data['bones']}
|
||||
source_rest={b['name'].split(':')[-1]:Matrix(b['bindpose']).inverted() for b in SOURCE['bones']}
|
||||
target_width=(rest['LeftUpLeg'].translation-rest['RightUpLeg'].translation).length
|
||||
source_width=(source_rest['LeftUpLeg'].translation-source_rest['RightUpLeg'].translation).length
|
||||
scale=Vector((target_width/source_width,data['translationScale'],data['translationScale']))
|
||||
samples=[];tracks=[];previous={}
|
||||
for frame in range(32):
|
||||
source=sample(SOURCE,frame);initial=sample(data,frame)
|
||||
local=[]
|
||||
for i,n in enumerate(names):
|
||||
m=initial[names[parents[i]]].inverted()@initial[n] if parents[i]>=0 else initial[n]
|
||||
p,q,s=m.decompose();local.append([p.copy(),q.copy(),s.copy()])
|
||||
def fk():
|
||||
result=[]
|
||||
for i,(p,q,s) in enumerate(local):
|
||||
m=Matrix.LocRotScale(p,q,s);result.append(result[parents[i]]@m if parents[i]>=0 else m)
|
||||
return result
|
||||
world=fk();target_hips=world[indices['Hips']].translation.copy();source_hips=source['Hips'].translation.copy()
|
||||
goals={};errors={}
|
||||
for side in ['Left','Right']:
|
||||
upper=indices[side+'UpLeg'];lower=indices[side+'Leg'];foot=indices[side+'Foot']
|
||||
source_foot=source[side+'Foot'].translation-source_hips;source_knee=source[side+'Leg'].translation-source_hips
|
||||
goal=target_hips+Vector(tuple(source_foot[i]*scale[i] for i in range(3)))
|
||||
hint=target_hips+Vector(tuple(source_knee[i]*scale[i] for i in range(3)))
|
||||
h=world[upper].translation.copy();k=world[lower].translation.copy();f=world[foot].translation.copy()
|
||||
foot_q=world[foot].to_quaternion().copy();a=(k-h).length;b=(f-k).length
|
||||
direction=goal-h;wanted_distance=direction.length;direction.normalize()
|
||||
distance=max(abs(a-b)+1e-7,min(wanted_distance,a+b-1e-7))
|
||||
reachable=h+direction*distance
|
||||
bend=hint-h-direction*(hint-h).dot(direction)
|
||||
if bend.length<1e-7:bend=k-h-direction*(k-h).dot(direction)
|
||||
if bend.length<1e-7:bend=Vector((0,0,-1))-direction*direction.dot(Vector((0,0,-1)))
|
||||
bend.normalize();along=(a*a+distance*distance-b*b)/(2*distance)
|
||||
desired_knee=h+direction*along+bend*math.sqrt(max(0,a*a-along*along))
|
||||
upper_q=(k-h).rotation_difference(desired_knee-h)@world[upper].to_quaternion()
|
||||
local[upper][1]=world[parents[upper]].to_quaternion().inverted()@upper_q
|
||||
world=fk()
|
||||
lower_q=(world[foot].translation-world[lower].translation).rotation_difference(reachable-world[lower].translation)@world[lower].to_quaternion()
|
||||
local[lower][1]=world[parents[lower]].to_quaternion().inverted()@lower_q
|
||||
world=fk();local[foot][1]=world[parents[foot]].to_quaternion().inverted()@foot_q;world=fk()
|
||||
goals[side]=list(goal);errors[side]=(world[foot].translation-goal).length
|
||||
values=[]
|
||||
for i,(p,q,s) in enumerate(local):
|
||||
if i in previous and previous[i].dot(q)<0:q.negate()
|
||||
previous[i]=q.copy();values.append(list(p)+[q.x,q.y,q.z,q.w]+list(s))
|
||||
samples.append(values)
|
||||
lateral=(world[indices['LeftUpLeg']].translation-world[indices['RightUpLeg']].translation).normalized()
|
||||
gap=(world[indices['LeftFoot']].translation-world[indices['RightFoot']].translation).dot(lateral)
|
||||
tracks.append({'frame':frame+1,'footSignedGap':gap,'goalErrors':errors,'goals':goals,
|
||||
'positions':{n:list(world[i].translation) for n,i in indices.items() if n in ['Hips','LeftUpLeg','RightUpLeg','LeftLeg','RightLeg','LeftFoot','RightFoot']}})
|
||||
return {'name':'Walking_LmGirl'+('Mixamo33' if label=='mixamo' else '41'),'fps':30,'samples':samples}, {
|
||||
'footMotionScale':list(scale),'minimumFootSignedGap':min(t['footSignedGap'] for t in tracks),
|
||||
'crossedFrames':[t['frame'] for t in tracks if t['footSignedGap']<0],
|
||||
'maxUnreachableGoalError':max(max(t['goalErrors'].values()) for t in tracks),'tracks':tracks}
|
||||
|
||||
def correct():
|
||||
result={'method':'Pelvis-relative source ankle paths scaled by target hip width and hip-to-ankle span; analytic two-bone IK with source knee hints, fixed segment lengths and preserved mapped foot world rotations.',
|
||||
'sourceClipUnmodified':True,'sourceClips':'CampGuide native Walking','motions':{},'reports':{}}
|
||||
for label,path in [('tokenrig',FOLDER/'unity_review_data.json'),('mixamo',FOLDER/'website/unity_review_data.json')]:
|
||||
result['motions'][label],result['reports'][label]=corrected_motion(json.loads(path.read_text()),label)
|
||||
result['deformation']={}
|
||||
for label,path in [('tokenrig',FOLDER/'unity_review_data.json'),('mixamo',FOLDER/'website/unity_review_data.json')]:
|
||||
data=json.loads(path.read_text());motion=result['motions'][label];parents=[b['parent'] for b in data['bones']]
|
||||
for variant in data['variants']:
|
||||
points=np.array(variant['vertices']);p4=np.column_stack((points,np.ones(len(points))))
|
||||
weights=np.zeros((len(points),len(parents)))
|
||||
for i,row in enumerate(variant['weights']):
|
||||
for j,w in row:weights[i,j]=w
|
||||
edges=np.array(variant['auditEdges']);ids=edges[:,:2].astype(int);lengths=edges[:,2];masks=edges[:,3].astype(int)
|
||||
maxima={name:0 for name in ['all','hands','neck']};over=maxima.copy();floors=[]
|
||||
for values in motion['samples']:
|
||||
world=[]
|
||||
for i,v in enumerate(values):
|
||||
local=Matrix.LocRotScale(Vector(v[:3]),quaternion(v[3:7]),Vector(v[7:10]));world.append(world[parents[i]]@local if parents[i]>=0 else local)
|
||||
deformed=np.zeros_like(points)
|
||||
for i,b in enumerate(data['bones']):deformed+=weights[:,i,None]*(p4@np.array(world[i]@Matrix(b['bindpose'])).T)[:,:3]
|
||||
ratios=np.linalg.norm(deformed[ids[:,0]]-deformed[ids[:,1]],axis=1)/lengths;floors.append(float(deformed[:,1].min()))
|
||||
for name,mask in [('all',1),('hands',2),('neck',4)]:
|
||||
region=ratios[(masks&mask)!=0];maxima[name]=max(maxima[name],float(region.max()));over[name]=max(over[name],int((region>2).sum()))
|
||||
result['deformation'][variant['name']]={'maxStretch':maxima,'maxEdgesOver2x':over,'floorMinRange':[min(floors),max(floors)]}
|
||||
(FOLDER/'gait_correction.json').write_text(json.dumps(result,indent=2),encoding='utf-8')
|
||||
print(json.dumps({k:{n:v for n,v in d.items() if n!='tracks'} for k,d in result['reports'].items()},indent=2))
|
||||
|
||||
def export_corrected():
|
||||
result=json.loads((FOLDER/'gait_correction.json').read_text());C=Matrix(((1,0,0,0),(0,0,1,0),(0,1,0,0),(0,0,0,1)))
|
||||
receipts=[]
|
||||
for tag in ['raw','post','clean','clean_eps','local']:
|
||||
bpy.ops.wm.open_mainfile(filepath=str(FOLDER/('lmgirl_'+tag+'.blend')))
|
||||
arm=next(o for o in bpy.context.scene.objects if o.type=='ARMATURE');arm.animation_data_clear();arm.data.pose_position='POSE'
|
||||
names=[b.name for b in arm.data.bones];data=json.loads((FOLDER/'unity_review_data.json').read_text());parents=[b['parent'] for b in data['bones']]
|
||||
for b in arm.pose.bones:b.rotation_mode='QUATERNION'
|
||||
for f,values in enumerate(result['motions']['tokenrig']['samples'],1):
|
||||
bpy.context.scene.frame_set(f);world=[]
|
||||
for i,v in enumerate(values):
|
||||
m=Matrix.LocRotScale(Vector(v[:3]),quaternion(v[3:7]),Vector(v[7:10]));world.append(world[parents[i]]@m if parents[i]>=0 else m)
|
||||
for i,n in enumerate(names):
|
||||
b=arm.pose.bones[n];b.matrix=arm.matrix_world.inverted()@C@world[i]@C;bpy.context.view_layer.update()
|
||||
for attr in ['location','rotation_quaternion','scale']:b.keyframe_insert(attr,frame=f)
|
||||
bpy.context.scene.frame_start=1;bpy.context.scene.frame_end=32;bpy.context.scene.render.fps=30
|
||||
path=FOLDER/('Walking_'+tag+'_corrected.glb')
|
||||
bpy.ops.export_scene.gltf(filepath=str(path),export_format='GLB',export_skins=True,export_animations=True,
|
||||
export_animation_mode='ACTIVE_ACTIONS',export_force_sampling=True,export_def_bones=False)
|
||||
receipts.append({'file':str(path),'frames':[1,32],'source':result['motions']['tokenrig']['name']})
|
||||
(FOLDER/'gait_export_manifest.json').write_text(json.dumps(receipts,indent=2),encoding='utf-8')
|
||||
|
||||
def verify_exports():
|
||||
from mathutils.kdtree import KDTree
|
||||
data=json.loads((FOLDER/'unity_review_data.json').read_text());result=json.loads((FOLDER/'gait_correction.json').read_text());receipts=[]
|
||||
for tag in ['raw','post','clean','clean_eps','local']:
|
||||
variant=next(v for v in data['variants'] if v['name']==tag)
|
||||
bpy.ops.wm.read_factory_settings(use_empty=True);bpy.context.scene.render.fps=30
|
||||
bpy.ops.import_scene.gltf(filepath=str(FOLDER/('Walking_'+tag+'_corrected.glb')))
|
||||
arm=next(o for o in bpy.context.scene.objects if o.type=='ARMATURE');mesh=next(o for o in bpy.context.scene.objects if o.type=='MESH')
|
||||
# Mesh raw geometry is Blender Z-up; convert to native review Y-up.
|
||||
points=np.array([mesh.matrix_world@v.co for v in mesh.data.vertices]);points=points[:,[0,2,1]]
|
||||
tree=KDTree(len(points))
|
||||
for i,p in enumerate(points):tree.insert(Vector(p),i)
|
||||
tree.balance();mapping=[]
|
||||
for p in variant['vertices']:
|
||||
_,index,distance=tree.find(Vector(p));assert distance<1e-5;mapping.append(index)
|
||||
edges=np.array(variant['auditEdges']);ids=edges[:,:2].astype(int);max_stretch=0;min_gap=100
|
||||
for frame in range(1,33):
|
||||
bpy.context.scene.frame_set(frame);bpy.context.view_layer.update()
|
||||
obj=mesh.evaluated_get(bpy.context.evaluated_depsgraph_get());geometry=obj.to_mesh()
|
||||
deformed=np.array([obj.matrix_world@v.co for v in geometry.vertices])[mapping];obj.to_mesh_clear()
|
||||
max_stretch=max(max_stretch,float((np.linalg.norm(deformed[ids[:,0]]-deformed[ids[:,1]],axis=1)/edges[:,2]).max()))
|
||||
p={n:arm.matrix_world@arm.pose.bones['mixamorig:'+n].head for n in ['LeftUpLeg','RightUpLeg','LeftFoot','RightFoot']}
|
||||
lateral=(p['LeftUpLeg']-p['RightUpLeg']).normalized();min_gap=min(min_gap,(p['LeftFoot']-p['RightFoot']).dot(lateral))
|
||||
error=abs(max_stretch-result['deformation'][tag]['maxStretch']['all']);assert error<.003,(tag,error)
|
||||
assert min_gap>.08,(tag,min_gap)
|
||||
receipts.append({'variant':tag,'maxStretch':max_stretch,'error':error,'minimumFootSignedGap':min_gap})
|
||||
(FOLDER/'gait_export_verification.json').write_text(json.dumps({'passed':True,'results':receipts},indent=2),encoding='utf-8')
|
||||
print('GAIT_EXPORTS_VERIFIED',len(receipts))
|
||||
|
||||
if __name__=='__main__':
|
||||
if '--correct' in sys.argv:correct()
|
||||
elif '--export' in sys.argv:export_corrected()
|
||||
elif '--verify-exports' in sys.argv:verify_exports()
|
||||
else:audit()
|
||||
Reference in New Issue
Block a user