Files
AIGame/AIClient/AgentScripts/VerifyCharacterAnimationLab.cs
T

123 lines
8.1 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.UI;
public static class VerifyCharacterAnimationLab
{
static string Repo => Directory.GetParent(Application.dataPath).Parent.FullName;
static string Output => Path.Combine(Repo,"Tools/output/campguide_studio/rig_mixamo41/unity_review");
const string Root="Assets/Review/CharacterAnimationLab";
public static string Verify()
{
var data=JObject.Parse(File.ReadAllText(Path.Combine(Output,"review_data.json")));
var expected=JArray.Parse(File.ReadAllText(Path.Combine(Output,"../comparison.json"))).Concat(JArray.Parse(File.ReadAllText(Path.Combine(Output,"../comparison_local.json")))).ToArray();
var prefab=PrefabUtility.LoadPrefabContents(Root+"/Generated/CharacterAnimationLab.prefab");
var previous=QualitySettings.skinWeights;var baked=new Mesh();
try
{
QualitySettings.skinWeights=SkinWeights.Unlimited;
var viewer=prefab.GetComponent<CharacterAnimationLab>();
viewer.inPlace=false;viewer.yaw=0;viewer.playing=false;
// BakeMesh includes the presentation scale in this hierarchy. Audit
// at the original 1.135m source scale used for the Blender metrics.
foreach(var actor in viewer.actors)actor.rig.transform.localScale=Vector3.one;
var results=new List<object>();
string[] prefixes={"Mixamo","TokenRig41","TokenRigLocal"};
string[] labels={"Walking","Breathing Idle","Happy Idle"};
for(int c=0;c<3;c++)
{
viewer.motion=c;
var max=new float[3,4];var over=new int[3];
int frames=((JArray)data["clips"][c]["samples"]).Count;
for(int frame=0;frame<frames;frame++)
{
viewer.time=frame/30f;viewer.Sample();
for(int actor=0;actor<3;actor++)
{
viewer.actors[actor].renderer.BakeMesh(baked);
var vertices=baked.vertices;
foreach(var e in data["variants"][actor]["auditEdges"])
{
float ratio=Vector3.Distance(vertices[(int)e[0]],vertices[(int)e[1]])/(float)e[2];int mask=(int)e[3];
for(int region=0;region<4;region++)if((mask&(1<<region))!=0)max[actor,region]=Mathf.Max(max[actor,region],ratio);
if(ratio>2)over[actor]++;
}
}
}
for(int actor=0;actor<3;actor++)
{
var reference=expected.First(x=>(string)x["label"]==prefixes[actor]+" "+labels[c]);
string[] regions={"all","hands","neck","head"};
float worstError=0;
for(int region=0;region<4;region++)worstError=Mathf.Max(worstError,Mathf.Abs(max[actor,region]-(float)reference["animation"]["summary"][regions[region]]["max_edge_stretch"]));
if(worstError>.003f)throw new InvalidOperationException(prefixes[actor]+" "+labels[c]+" native skinning mismatch "+worstError+" actual="+max[actor,0]);
results.Add(new {variant=prefixes[actor],motion=labels[c],frames,maxStretch=max[actor,0],handMax=max[actor,1],neckMax=max[actor,2],maxDifferenceFromBlender=worstError});
}
}
int buttons=viewer.ui.GetComponentsInChildren<Button>(true).Length;
if(buttons!=21 || viewer.actors.Any(a=>a.renderer.bones.Length!=41))throw new InvalidOperationException("Missing controls or bones: "+buttons);
File.WriteAllText(Path.Combine(Output,"unity_verification.json"),JsonConvert.SerializeObject(new {passed=true,buttons,bones=41,triangles=3000,samples=results},Formatting.Indented));
return JsonConvert.SerializeObject(results);
}
finally{QualitySettings.skinWeights=previous;UnityEngine.Object.DestroyImmediate(baked);PrefabUtility.UnloadPrefabContents(prefab);}
}
static CharacterAnimationLab Viewer()=>UnityEngine.Object.FindObjectOfType<CharacterAnimationLab>();
public static string State()
{
var v=Viewer();if(!v)return "No comparison viewer loaded";
return JsonConvert.SerializeObject(new {v.motion,v.time,v.playing,v.focus,v.detail,v.inPlace,v.speed,clip=v.clips[v.motion].name,bones=v.actors.Select(a=>a.renderer.bones.Length),positions=v.actors.Select(a=>a.hips.localPosition.ToString("F5"))});
}
public static string Pose(int motion,int frame,int focus,int detail,int view,bool textured)
{
var v=Viewer();v.motion=motion;v.playing=false;v.time=frame/30f;v.focus=focus;v.detail=detail;v.yaw=view;v.textured=textured;v.Sample();return State();
}
public static string Reset()
{
var v=Viewer();v.motion=0;v.time=0;v.focus=-1;v.detail=0;v.yaw=0;v.speed=1;v.textured=true;v.inPlace=true;v.playing=true;v.Sample();return State();
}
public static string Controls()
{
var v=Viewer();if(!Application.isPlaying)throw new InvalidOperationException("Play Mode required.");
var buttons=v.ui.GetComponentsInChildren<Button>(true).ToDictionary(b=>b.name);
void Click(string n)=>buttons["Btn_"+n].onClick.Invoke();
void Check(bool ok,string message){if(!ok)throw new InvalidOperationException(message);}
Reset();Click("Play");Check(!v.playing,"Pause listener");
float paused=v.time;v.Sample();Check(Mathf.Abs(v.time-paused)<.00001f,"Paused clock changed");
Click("Restart");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&&v.FrameCount==299,"Breathing clip");
Click("Speed025");Check(v.speed==.25f,"Quarter speed");
var seek=v.ui.GetComponentInChildren<AnimationLabSeekBar>();
var rect=(RectTransform)seek.transform;var corners=new Vector3[4];rect.GetWorldCorners(corners);
var screen=RectTransformUtility.WorldToScreenPoint(null,Vector3.Lerp(corners[0],corners[2],.5f));
seek.OnPointerDown(new UnityEngine.EventSystems.PointerEventData(UnityEngine.EventSystems.EventSystem.current){position=screen});
Check(Mathf.Abs(v.time-v.Duration*.5f)<.001f&&!v.playing,"Progress pointer seek");
Click("Side");Check(v.actors.All(a=>Quaternion.Angle(a.rig.transform.localRotation,Quaternion.Euler(0,90,0))<.001f),"Synchronized view angle");
Click("Focus2");Click("Detail");Click("Detail");Check(v.focus==1&&v.detail==2,"Hand close-up");
Click("Material");Check(v.actors.All(a=>a.renderer.sharedMaterial==v.clayMaterial),"Shared clay material");
Click("Walk");v.Seek(1);Click("Travel");var moving=v.actors[0].hips.localPosition;Click("Travel");
Check(Vector3.Distance(moving-v.actors[0].hips.localPosition,v.travel[0])<.00001f,"In-place travel removal");
Click("All");Click("Front");Click("Happy");Check(v.motion==2&&v.focus<0&&v.detail==0,"All-view and Happy Idle");
var first=v.actors[0].renderer.bones;
float maxPoseError=0;
foreach(var actor in v.actors.Skip(1))for(int i=0;i<first.Length;i++)
{
maxPoseError=Mathf.Max(maxPoseError,Vector3.Distance(first[i].localPosition,actor.renderer.bones[i].localPosition));
Check(Quaternion.Angle(first[i].localRotation,actor.renderer.bones[i].localRotation)<.01f,"Bone rotations out of sync");
}
Check(maxPoseError<.00001f,"Bone positions out of sync");
var result=new {passed=true,checks=new[]{"pause freezes clock","previous/next frame","clip selection","speed","pointer seek","synchronized angle","hand focus","material switch","in-place travel","all-view","41-bone synchronization"},maxPoseError};
File.WriteAllText(Path.Combine(Output,"unity_controls.json"),JsonConvert.SerializeObject(result,Formatting.Indented));
Reset();return JsonConvert.SerializeObject(result);
}
}
#endif