Files
AIGame/AIClient/AgentScripts/VerifyLmGirlGait.cs
T

113 lines
6.9 KiB
C#

#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<CharacterAnimationLab>();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<object>();
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<Mesh>(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<CharacterAnimationLab>();
for(int i=0;i<v.actors.Length;i++)v.actors[i].walkingClip=corrected?
AssetDatabase.LoadAssetAtPath<AnimationClip>(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