137 lines
7.4 KiB
C#
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
|