#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().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(); 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(Root + "/RockBall_Rigged.prefab")); try { var anim = go.GetComponentInChildren(); anim.enabled = false; var model = anim.gameObject; var bones = model.GetComponentsInChildren().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(Root + "/RockBall.prefab")); float shapeError; try { var points = staticGo.GetComponentsInChildren().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(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