Files
AIGame/AIClient/AgentScripts/VerifyAnimalWalk.cs
T

137 lines
7.4 KiB
C#

#if UNITY_EDITOR
using System;
using System.IO;
using System.Linq;
using System.Text;
using UnityEditor;
using UnityEngine;
using Object = UnityEngine.Object;
public static class VerifyAnimalWalk
{
const string Root = "Assets/Bundles/Characters/Animal/RockBall";
public static Vector3[] WorldVertices(GameObject go)
{
return go.GetComponentsInChildren<SkinnedMeshRenderer>().SelectMany(s =>
{
var mesh = s.sharedMesh;
var poses = mesh.bindposes;
var matrices = s.bones.Select((b,i) => b.localToWorldMatrix * poses[i]).ToArray();
var weights = mesh.boneWeights;
return mesh.vertices.Select((v,i) =>
{
var w = weights[i];
return matrices[w.boneIndex0].MultiplyPoint3x4(v) * w.weight0
+ matrices[w.boneIndex1].MultiplyPoint3x4(v) * w.weight1
+ matrices[w.boneIndex2].MultiplyPoint3x4(v) * w.weight2
+ matrices[w.boneIndex3].MultiplyPoint3x4(v) * w.weight3;
}).ToArray();
}).ToArray();
}
public static Bounds GeometryBounds(GameObject go)
{
var points = WorldVertices(go);
var result = new Bounds(points[0], Vector3.zero);
foreach (var p in points) result.Encapsulate(p);
return result;
}
public static string Inspect() => Check(false);
public static string Run() => Check(true);
public static string Live()
{
if(!EditorApplication.isPlaying) throw new Exception("Live check requires Play Mode");
var unit=GameObject.Find("WalkVerified");
if(unit==null) throw new Exception("Open RockBallWalkReview first");
var animator=unit.GetComponentInChildren<Animator>();
var state=animator.GetCurrentAnimatorStateInfo(0);
var bounds=GeometryBounds(unit);
if(!state.IsName("Walk") || animator.applyRootMotion || bounds.min.y < -.01f || bounds.min.y > .01f)
throw new Exception($"Unexpected live gait: {state.shortNameHash} bottom={bounds.min.y}");
return $"PASS Play Mode Walk time={state.normalizedTime:F3} bottom={bounds.min.y:F5} height={bounds.size.y:F5}";
}
static string Check(bool strict)
{
var sb = new StringBuilder();
var go = Object.Instantiate(AssetDatabase.LoadAssetAtPath<GameObject>(Root + "/RockBall_Rigged.prefab"));
try
{
var anim = go.GetComponentInChildren<Animator>();
anim.enabled = false;
var model = anim.gameObject;
var bones = model.GetComponentsInChildren<Transform>().Where(t => t.name.StartsWith("bone_")).ToArray();
var bind = GeometryBounds(go);
sb.AppendLine($"BIND height={bind.size.y:F5} minY={bind.min.y:F5} size={bind.size}");
var restPositions = WorldVertices(go);
var staticGo = Object.Instantiate(AssetDatabase.LoadAssetAtPath<GameObject>(Root + "/RockBall.prefab"));
float shapeError;
try
{
var points = staticGo.GetComponentsInChildren<MeshFilter>().SelectMany(f => f.sharedMesh.vertices.Select(v => f.transform.TransformPoint(v))).ToArray();
shapeError = restPositions.Max(v => points.Min(p => Vector3.Distance(v,p)));
}
finally { Object.DestroyImmediate(staticGo); }
sb.AppendLine($"BIND vs approved static maxVertexError={shapeError:F6}m bones={bones.Length}");
var clip = AssetDatabase.LoadAssetAtPath<AnimationClip>(Root + "/Clips/Walk.anim");
var root = bones.First(t => t.name == "bone_0");
float minY = float.MaxValue, maxBottom = float.MinValue, tilt = 0, minHeight = float.MaxValue;
float motion = 0, seam = 0;
Vector3[] start = null;
var rotations = new Quaternion[bones.Length];
var rightFoot = bones.First(t => t.name == "bone_9");
var leftFoot = bones.First(t => t.name == "bone_11");
float minFootPhase = float.MaxValue, maxFootPhase = float.MinValue;
var rightSamples = new Vector3[61];
var leftSamples = new Vector3[61];
for (int f = 0; f <= 60; f++)
{
clip.SampleAnimation(model, clip.length * f / 60f);
var bounds = GeometryBounds(go);
minY = Mathf.Min(minY, bounds.min.y);
maxBottom = Mathf.Max(maxBottom, bounds.min.y);
minHeight = Mathf.Min(minHeight, bounds.size.y);
tilt = Mathf.Max(tilt, Vector3.Angle(root.up, Vector3.up));
rightSamples[f] = rightFoot.position-root.position;
leftSamples[f] = leftFoot.position-root.position;
float phase = rightSamples[f].z-leftSamples[f].z;
minFootPhase = Mathf.Min(minFootPhase,phase);
maxFootPhase = Mathf.Max(maxFootPhase,phase);
if (f == 0)
{
start = WorldVertices(go);
for (int b = 0; b < bones.Length; b++) rotations[b] = bones[b].localRotation;
}
for (int b = 0; b < bones.Length; b++) motion = Mathf.Max(motion, Quaternion.Angle(rotations[b], bones[b].localRotation));
if (f == 60)
{
var end = WorldVertices(go);
seam = end.Select((v,i) => Vector3.Distance(v,start[i])).Max();
}
if (f % 15 == 0) sb.AppendLine($"Walk {f}/60 minY={bounds.min.y:F5} height={bounds.size.y:F5} rootTilt={Vector3.Angle(root.up,Vector3.up):F2}");
}
sb.AppendLine($"RESULT len={clip.length:F4} loop={AnimationUtility.GetAnimationClipSettings(clip).loopTime} minY={minY:F5} maxBottom={maxBottom:F5} minHeight={minHeight:F5} maxRootTilt={tilt:F2} maxJointMotion={motion:F2} seamVertexMax={seam:F6}");
float rightStride = rightSamples.Max(p=>p.z)-rightSamples.Min(p=>p.z);
float leftStride = leftSamples.Max(p=>p.z)-leftSamples.Min(p=>p.z);
sb.AppendLine($"FEET rightStride={rightStride:F5}m leftStride={leftStride:F5}m relativePhaseRange={maxFootPhase-minFootPhase:F5}m");
// Verify the real controller through several loops as well as clip sampling.
anim.enabled = true;
anim.Rebind();
anim.SetInteger("Motion",1);
anim.Play("Walk",0,0);
anim.Update(0);
for (int i = 0; i < 180; i++) anim.Update(1f/30f);
var state = anim.GetCurrentAnimatorStateInfo(0);
sb.AppendLine($"ANIMATOR stateWalk={state.IsName("Walk")} normalizedTime={state.normalizedTime:F2}");
if (strict && (shapeError > .002f || Mathf.Abs(bind.size.y-.55f) > .002f || Mathf.Abs(bind.min.y) > .002f
|| minY < -.01f || maxBottom > .025f || minHeight < .46f || tilt > 15f || motion < 5f || seam > .002f
|| rightStride < .005f || leftStride < .005f || maxFootPhase-minFootPhase < .01f || !state.IsName("Walk")))
throw new Exception(sb.ToString());
var report = Path.GetFullPath(Path.Combine(Application.dataPath,"../../Tools/output/rockball_walk_audit/verification.txt"));
Directory.CreateDirectory(Path.GetDirectoryName(report));
File.WriteAllText(report,sb.ToString());
return (strict ? "PASS\n" : "INSPECT\n") + sb;
}
finally { Object.DestroyImmediate(go); }
}
}
#endif