Files
AIGame/AIClient/AgentScripts/VerifyLmGirlAnimationLab.cs
T

120 lines
8.3 KiB
C#

#if UNITY_EDITOR
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using AIGame.Review;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using UnityEditor;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;
public static class VerifyLmGirlAnimationLab
{
const string Root="Assets/Review/CharacterAnimationLab";
static string Output=>Path.Combine(Directory.GetParent(Application.dataPath).Parent.FullName,"Tools/output/lmgirl_tpose_studio/rig_mixamo41");
static void Check(bool ok,string message){if(!ok)throw new InvalidOperationException(message);}
static CharacterAnimationLab Viewer()=>UnityEngine.Object.FindObjectOfType<CharacterAnimationLab>();
public static string Verify()
{
var data=JObject.Parse(File.ReadAllText(Path.Combine(Output,"unity_review_data.json")));
var expected=JObject.Parse(File.ReadAllText(Path.Combine(Output,"walking_comparison.json")));
var root=PrefabUtility.LoadPrefabContents(Root+"/LmGirl/Generated/LmGirlAnimationLab.prefab");
var baked=new Mesh();var quality=QualitySettings.skinWeights;
try
{
QualitySettings.skinWeights=SkinWeights.Unlimited;
var v=root.GetComponent<CharacterAnimationLab>();v.playing=false;v.inPlace=false;v.motion=0;
// Historical bind/weight baseline, deliberately excluding the later
// foot-path correction. Active corrected playback has a gait audit.
foreach(var actor in v.actors)actor.walkingClip=null;
foreach(var actor in v.actors)actor.rig.transform.localScale=Vector3.one;
Check(v.clips[0]==AssetDatabase.LoadAssetAtPath<AnimationClip>(Root+"/Generated/Walking.anim"),"Walking clip must be the same asset.");
Check(v.actors.Length==4&&v.actors[0].renderer.bones.Length==33,"Website 33-bone actor required.");
Check(v.actors.Skip(1).All(a=>a.renderer.bones.Length==41&&a.translationOffsets.Length==41),"TokenRig 41 bones/offsets required.");
var results=new List<object>();
foreach(var variant in data["variants"])
{
string name=(string)variant["name"];
var mesh=AssetDatabase.LoadAssetAtPath<Mesh>(Root+"/LmGirl/Generated/"+name+".asset");
Check(mesh.triangles.Length/3==4999,"Topology changed.");
v.actors[1].renderer.sharedMesh=mesh;
float[] maxima=new float[3];int[] masks={1,2,4};
for(int frame=0;frame<32;frame++)
{
v.time=frame/30f;v.Sample();v.actors[1].renderer.BakeMesh(baked);var points=baked.vertices;
foreach(var edge in variant["auditEdges"])
{
float ratio=Vector3.Distance(points[(int)edge[0]],points[(int)edge[1]])/(float)edge[2];
for(int r=0;r<3;r++)if(((int)edge[3]&masks[r])!=0)maxima[r]=Mathf.Max(maxima[r],ratio);
}
}
string[] regions={"all","hands","neck"};float error=0;
for(int r=0;r<3;r++)error=Mathf.Max(error,Mathf.Abs(maxima[r]-(float)expected[name]["maxStretch"][regions[r]]));
Check(error<.003f,name+" native comparison mismatch: "+error);
results.Add(new{variant=name,frames=32,maxStretch=maxima[0],hands=maxima[1],neck=maxima[2],maxDifference=error});
}
var website=JObject.Parse(File.ReadAllText(Path.Combine(Output,"website/unity_review_data.json")));
var websiteExpected=JObject.Parse(File.ReadAllText(Path.Combine(Output,"website/walking_comparison.json")));
Check(v.actors[0].renderer.sharedMesh.GetBonesPerVertex().ToArray().Max()==5,"Website influences were truncated.");
float[] siteMax=new float[3];int[] siteMasks={1,2,4};
for(int frame=0;frame<32;frame++)
{
v.time=frame/30f;v.Sample();v.actors[0].renderer.BakeMesh(baked);var points=baked.vertices;
foreach(var edge in website["variants"][0]["auditEdges"])
{
float ratio=Vector3.Distance(points[(int)edge[0]],points[(int)edge[1]])/(float)edge[2];
for(int r=0;r<3;r++)if(((int)edge[3]&siteMasks[r])!=0)siteMax[r]=Mathf.Max(siteMax[r],ratio);
}
}
string[] siteRegions={"all","hands","neck"};float siteError=0;
for(int r=0;r<3;r++)siteError=Mathf.Max(siteError,Mathf.Abs(siteMax[r]-(float)websiteExpected["maxStretch"][siteRegions[r]]));
Check(siteError<.003f,"Website rest-axis mapping mismatch: "+siteError);
results.Add(new{variant="mixamo",frames=32,maxStretch=siteMax[0],hands=siteMax[1],neck=siteMax[2],maxDifference=siteError});
var report=new{passed=true,samples=192,bones=41,websiteBones=33,websiteMaxInfluences=5,triangles=4999,sameWalkingClip=true,results};
File.WriteAllText(Path.Combine(Output,"unity_verification.json"),JsonConvert.SerializeObject(report,Formatting.Indented));
return JsonConvert.SerializeObject(report);
}
finally{QualitySettings.skinWeights=quality;UnityEngine.Object.DestroyImmediate(baked);PrefabUtility.UnloadPrefabContents(root);}
}
public static string Controls()
{
var v=Viewer();Check(Application.isPlaying,"Play Mode required.");
var buttons=v.ui.GetComponentsInChildren<Button>(true).ToDictionary(b=>b.name);
void Click(string name)=>buttons["Btn_"+name].onClick.Invoke();
Check(buttons.Count==22,"22 serialized buttons required.");
v.motion=0;v.time=0;v.playing=true;v.inPlace=true;v.Sample();Click("Play");Check(!v.playing,"Pause");
Click("Next");Check(Mathf.Abs(v.time-1f/30)<.00001f,"Next frame");Click("Previous");Check(v.time==0,"Previous frame");
Click("Breathe");Check(v.motion==1,"Breathing");Click("Happy");Check(v.motion==2,"Happy");Click("Walk");
foreach(var speed in new[]{"Speed025","Speed05","Speed1","Speed2"})Click(speed);
Check(v.speed==2,"Speed");Click("Speed1");
var seek=v.ui.GetComponentInChildren<AnimationLabSeekBar>();var corners=new Vector3[4];((RectTransform)seek.transform).GetWorldCorners(corners);
seek.OnPointerDown(new PointerEventData(EventSystem.current){position=RectTransformUtility.WorldToScreenPoint(null,Vector3.Lerp(corners[0],corners[2],.5f))});
Check(Mathf.Abs(v.time-v.Duration*.5f)<.001f&&!v.playing,"Pointer seek");
v.Seek(1);var stationary=v.actors[0].hips.localPosition;Click("Travel");
Check(Vector3.Distance(v.actors[0].hips.localPosition-stationary,v.travel[0]*v.actors[0].translationScale)<.00001f,"Proportional travel");Click("Travel");
Click("Focus4");Click("Detail");Click("Detail");Check(v.focus==3&&v.detail==2&&v.actors[3].rightHand,"Fourth actor hand close-up");
Check(v.actors.Count(a=>a.renderer.enabled)==1,"Focused view must isolate the selected actor.");
Click("Material");Check(v.actors.All(a=>a.renderer.sharedMaterial==v.clayMaterial),"Clay");Click("Material");
Click("Side");Click("Back");Click("Front");Click("All");
Check(v.actors.All(a=>a.renderer.enabled),"All-view must restore four actors.");
var first=v.actors[1].renderer.bones;
foreach(var actor in v.actors.Skip(2))for(int i=0;i<41;i++)
{
Check(Vector3.Distance(first[i].localPosition,actor.renderer.bones[i].localPosition)<.00001f,"Pose translations differ.");
Check(Mathf.Abs(Quaternion.Dot(first[i].localRotation,actor.renderer.bones[i].localRotation))>.99999f,"Pose rotations differ.");
}
var result=new{passed=true,buttons=22,checks=new[]{"pause","frame step","three clips","four speeds","pointer seek","scaled in-place travel","fourth actor focus/detail","materials","views","TokenRig 41-bone pose synchronization"}};
File.WriteAllText(Path.Combine(Output,"unity_controls.json"),JsonConvert.SerializeObject(result,Formatting.Indented));
v.time=0;v.playing=true;v.Sample();return JsonConvert.SerializeObject(result);
}
public static string Pose(int frame,int focus,int detail,int yaw,bool textured)
{
var v=Viewer();v.motion=0;v.time=frame/30f;v.playing=false;v.inPlace=true;v.focus=focus;v.detail=detail;v.yaw=yaw;v.textured=textured;v.Sample();
return JsonConvert.SerializeObject(new{v.time,v.focus,v.detail,v.yaw,v.textured});
}
}
#endif