#if UNITY_EDITOR using System; using System.Collections.Generic; using System.IO; using System.Linq; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using UnityEditor; using UnityEngine; using AIGame.Review; public static class VerifyLmGirlGait { const string Root="Assets/Review/CharacterAnimationLab/LmGirl/Generated"; static string Output=>Path.Combine(Directory.GetParent(Application.dataPath).Parent.FullName,"Tools/output/lmgirl_tpose_studio/rig_mixamo41"); static Vector3 V(JToken t)=>new Vector3((float)t[0],(float)t[1],(float)t[2]); static void Check(bool valid,string message){if(!valid)throw new InvalidOperationException(message);} public static string Verify() { var expected=JObject.Parse(File.ReadAllText(Path.Combine(Output,"gait_correction.json"))); var data=JObject.Parse(File.ReadAllText(Path.Combine(Output,"unity_review_data.json"))); var website=JObject.Parse(File.ReadAllText(Path.Combine(Output,"website/unity_review_data.json"))); var root=PrefabUtility.LoadPrefabContents(Root+"/LmGirlAnimationLab.prefab"); var baked=new Mesh();var previousQuality=QualitySettings.skinWeights; try { QualitySettings.skinWeights=SkinWeights.Unlimited; var v=root.GetComponent();v.playing=false;v.inPlace=false;v.motion=0;v.focus=-1; Check(v.actors.All(a=>a.walkingClip&&Mathf.Abs(a.walkingClip.length-v.Duration)<.00001f),"Missing mapped walk or wrong timing."); Check(v.actors.Skip(1).All(a=>a.walkingClip==v.actors[1].walkingClip),"All TokenRig variants must share one corrected motion."); foreach(var a in v.actors)a.rig.transform.localScale=Vector3.one; var bones=v.actors.Select(a=>a.renderer.bones.ToDictionary(b=>b.name.Split(':').Last())).ToArray(); float maxPositionError=0,maxLengthDrift=0;var minGaps=Enumerable.Repeat(float.MaxValue,4).ToArray(); var lengths=new float[4,4]; for(int ai=0;ai<4;ai++) { var bind=ai==0?website:data; var rest=bind["bones"].ToDictionary(b=>(string)b["name"],b=>Matrix(b["bindpose"]).inverse.GetColumn(3)); int segment=0; foreach(string side in new[]{"Left","Right"})foreach(var pair in new[]{new[]{"UpLeg","Leg"},new[]{"Leg","Foot"}}) lengths[ai,segment++]=Vector3.Distance(rest["mixamorig:"+side+pair[0]],rest["mixamorig:"+side+pair[1]]); } for(int frame=0;frame<32;frame++) { v.time=frame/30f;v.Sample(); for(int ai=0;ai<4;ai++) { var a=v.actors[ai];var track=expected["reports"][ai==0?"mixamo":"tokenrig"]["tracks"][frame]; foreach(var p in (JObject)track["positions"]) { var actual=a.rig.transform.InverseTransformPoint(bones[ai][p.Key].position); maxPositionError=Mathf.Max(maxPositionError,Vector3.Distance(actual,V(p.Value))); } } } Check(maxPositionError<.00005f,"Native mapped pose disagrees with independent IK reference: "+maxPositionError); // 120Hz scan covers interpolation between original 30Hz keyframes. for(int frame=0;frame<=124;frame++) { v.time=frame/120f;v.Sample(); for(int ai=0;ai<4;ai++) { var b=bones[ai];var lateral=(b["LeftUpLeg"].position-b["RightUpLeg"].position).normalized; float gap=Vector3.Dot(b["LeftFoot"].position-b["RightFoot"].position,lateral);minGaps[ai]=Mathf.Min(minGaps[ai],gap); int segment=0; foreach(string side in new[]{"Left","Right"})foreach(var pair in new[]{new[]{"UpLeg","Leg"},new[]{"Leg","Foot"}}) maxLengthDrift=Mathf.Max(maxLengthDrift,Mathf.Abs(Vector3.Distance(b[side+pair[0]].position,b[side+pair[1]].position)-lengths[ai,segment++])); } } Check(minGaps.All(g=>g>.08f),"Feet cross or become too close: "+string.Join(",",minGaps)); Check(maxLengthDrift<.0001f,"Leg segment lengths changed: "+maxLengthDrift); var deformation=new List(); foreach(var variant in data["variants"].Concat(website["variants"])) { string name=(string)variant["name"];int ai=name=="mixamo"?0:1;var actor=v.actors[ai]; actor.renderer.sharedMesh=AssetDatabase.LoadAssetAtPath(Root+"/"+name+".asset"); float[] maxima=new float[3];int[] bits={1,2,4}; for(int frame=0;frame<32;frame++) { v.time=frame/30f;v.Sample();actor.renderer.BakeMesh(baked);var vertices=baked.vertices; foreach(var edge in variant["auditEdges"]) { float ratio=Vector3.Distance(vertices[(int)edge[0]],vertices[(int)edge[1]])/(float)edge[2]; for(int i=0;i<3;i++)if(((int)edge[3]&bits[i])!=0)maxima[i]=Mathf.Max(maxima[i],ratio); } } string[] regions={"all","hands","neck"};float error=0; for(int i=0;i<3;i++)error=Mathf.Max(error,Mathf.Abs(maxima[i]-(float)expected["deformation"][name]["maxStretch"][regions[i]])); Check(error<.003f,"Corrected skin mismatch "+name+": "+error); deformation.Add(new{variant=name,maxStretch=maxima[0],hands=maxima[1],neck=maxima[2],maxDifference=error}); } var result=new{passed=true,originalFrames=32,interpolationHz=120,interpolationSamples=125*4, minimumFootSignedGaps=minGaps,maxPositionError,maxLengthDrift,deformationSamples=192,deformation}; File.WriteAllText(Path.Combine(Output,"unity_gait_verification.json"),JsonConvert.SerializeObject(result,Formatting.Indented)); return JsonConvert.SerializeObject(result); } finally{QualitySettings.skinWeights=previousQuality;UnityEngine.Object.DestroyImmediate(baked);PrefabUtility.UnloadPrefabContents(root);} } static Matrix4x4 Matrix(JToken rows) { var m=new Matrix4x4();for(int r=0;r<4;r++)for(int c=0;c<4;c++)m[r,c]=(float)rows[r][c];return m; } public static string Pose(bool corrected,int frame,int focus) { var v=UnityEngine.Object.FindObjectOfType(); for(int i=0;i(Root+"/Walking_LmGirl"+(i==0?"Mixamo33":"41")+".anim"):null; v.motion=0;v.time=frame/30f;v.playing=false;v.inPlace=true;v.focus=focus;v.detail=0;v.yaw=0;v.textured=false;v.Sample(); return corrected?"Corrected foot-path mapping":"Previous angle-only mapping (temporary Play Mode preview)"; } } #endif