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:
udx
2026-10-24 18:04:41 +08:00
co-authored by Cursor
parent 6ebc9401ce
commit 45e62132c8
1432 changed files with 2971856 additions and 2798 deletions
@@ -0,0 +1,109 @@
"""Export exact comparison mesh/bind/pose data for Pipeline-authored Unity assets.
Blender Z-up -> Unity Y-up is (x,z,y), so anatomical -Y faces Unity -Z.
All asset creation is subsequently performed inside Unity through Pipeline.
"""
import json
import sys
from pathlib import Path
import bpy
import numpy as np
from math import pi
from mathutils import Matrix
root=Path(sys.argv[sys.argv.index('--')+1])
folder=root/'Tools/output/campguide_studio/rig_mixamo41'
base=root/'AIClient/Assets/Bundles/Characters/Human/test/Boy_mixamo'
out=folder/'unity_review'
out.mkdir(exist_ok=True)
C=Matrix(((1,0,0,0),(0,0,1,0),(0,1,0,0),(0,0,0,1)))
def trs(m):
p,q,s=m.decompose()
return list(p)+[q.x,q.y,q.z,q.w]+list(s)
result={'coordinateSystem':'Unity Y-up, -Z front; positions in metres','variants':[],'bones':[],'clips':[]}
names=None
for label,path in [('Mixamo',base/'CampGuideStudio.fbx'),('TokenRigRaw',folder/'tokenrig_mixamo41.blend'),('TokenRigLocal',folder/'tokenrig_mixamo41_local.blend')]:
bpy.ops.wm.read_factory_settings(use_empty=True)
if path.suffix=='.blend':bpy.ops.wm.open_mainfile(filepath=str(path))
else:bpy.ops.import_scene.fbx(filepath=str(path))
arm=next(o for o in bpy.data.objects if o.type=='ARMATURE')
mesh=next(o for o in bpy.data.objects if o.type=='MESH')
pts=np.array([mesh.matrix_world@v.co for v in mesh.data.vertices])
normalize=Matrix.Rotation(pi/2,4,'X') if np.argmax(np.ptp(pts,axis=0))==1 else Matrix.Identity(4)
world={b.name:C@normalize@arm.matrix_world@b.matrix_local@C for b in arm.data.bones}
if names is None:
names=[b.name for b in arm.data.bones]
for b in arm.data.bones:
parent=b.parent.name if b.parent else None
local=world[parent].inverted()@world[b.name] if parent else world[b.name]
result['bones'].append({'name':b.name,'parent':names.index(parent) if parent else -1,
'rest':trs(local),'bindpose':[list(row) for row in world[b.name].inverted()]})
assert set(names)==set(arm.data.bones.keys())
weights=[]
for v in mesh.data.vertices:
w={mesh.vertex_groups[g.group].name:g.weight for g in v.groups if g.weight>1e-8}
total=sum(w.values())
weights.append([[names.index(n),value/total] for n,value in sorted(w.items(),key=lambda x:-x[1])])
transform=C@normalize@mesh.matrix_world
normal_transform=transform.to_3x3().inverted().transposed()
mesh.data.calc_loop_triangles()
uv_layer=mesh.data.uv_layers.active
vertices=[];normals=[];uv=[];influences=[];triangles=[];lookup={};first={}
for triangle in mesh.data.loop_triangles:
ids=[]
for loop_index in triangle.loops:
loop=mesh.data.loops[loop_index]
vi=loop.vertex_index
n=normal_transform@mesh.data.corner_normals[loop_index].vector
n.normalize()
tex=list(uv_layer.data[loop_index].uv) if uv_layer else [0,0]
key=(vi,*[round(x,6) for x in tex],*[round(x,6) for x in n])
if key not in lookup:
index=len(vertices);lookup[key]=index;first.setdefault(vi,index)
vertices.append(list(transform@mesh.data.vertices[vi].co));normals.append(list(n));uv.append(tex);influences.append(weights[vi])
ids.append(lookup[key])
triangles.extend([ids[0],ids[2],ids[1]])
# A reflection reverses the winding; the reversed triangle order above is
# checked visually in Unity and does not alter the comparison edge lengths.
raw=np.array([normalize@mesh.matrix_world@v.co for v in mesh.data.vertices])
height=float(np.ptp(raw[:,2]));z=(raw[:,2]-raw[:,2].min())/height
hand=(np.abs(raw[:,0])>height*.255)&(z>.55)&(z<.67)
neck=(z>.67)&(z<.78)&(np.abs(raw[:,0])<height*.12)
head=z>.78
edges=[]
for edge in mesh.data.edges:
a,b=edge.vertices
length=float(np.linalg.norm(raw[a]-raw[b]))
if length<=height*.002:continue
mask=1|(2 if hand[a] and hand[b] else 0)|(4 if neck[a] and neck[b] else 0)|(8 if head[a] and head[b] else 0)
edges.append([first[a],first[b],length,mask])
result['variants'].append({'name':label,'vertices':vertices,'normals':normals,'uv':uv,'triangles':triangles,
'weights':influences,'auditEdges':edges,'originalVertices':len(mesh.data.vertices),'trianglesCount':len(triangles)//3})
print('MESH',label,len(vertices),len(triangles)//3)
for label in ['Walking','Breathing Idle','Happy Idle']:
bpy.ops.wm.read_factory_settings(use_empty=True)
bpy.ops.import_scene.fbx(filepath=str(base/('CampGuideStudio@'+label+'.fbx')))
arm=next(o for o in bpy.data.objects if o.type=='ARMATURE')
first,last=map(int,arm.animation_data.action.frame_range)
samples=[];previous={}
for frame in range(first,last+1):
bpy.context.scene.frame_set(frame);bpy.context.view_layer.update()
world={n:C@arm.matrix_world@arm.pose.bones[n].matrix@C for n in names}
sample=[]
for i,n in enumerate(names):
parent=result['bones'][i]['parent']
local=world[names[parent]].inverted()@world[n] if parent>=0 else world[n]
values=trs(local)
if n in previous and sum(a*b for a,b in zip(previous[n],values[3:7]))<0:
values[3:7]=[-x for x in values[3:7]]
previous[n]=values[3:7]
sample.append(values)
samples.append(sample)
result['clips'].append({'name':label.replace(' ','_'),'fps':30,'samples':samples})
print('CLIP',label,len(samples))
(out/'review_data.json').write_text(json.dumps(result,separators=(',',':')),encoding='utf-8')
print('EXPORTED',out/'review_data.json')