123 lines
8.1 KiB
C#
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
|