#if UNITY_EDITOR using System; using System.IO; using System.Linq; using System.Collections.Generic; using Newtonsoft.Json; using AIGame.EditorTools; using AIGame.Review; using TMPro; using UnityEditor; using UnityEditor.Animations; using UnityEditor.SceneManagement; using UnityEngine; using UnityEngine.SceneManagement; using UnityEngine.EventSystems; using UnityEngine.Rendering; using UnityEngine.UI; public static class BuildBotAnimationLab { public const string Common = "Assets/Bundles/Characters/Human/Common"; public const string Root = "Assets/Review/BotAnimationLab"; public const string ScenePath = Root + "/BotAnimationLab.unity"; static readonly string[] Names = { "XBot", "YBot", "XBot25", "XBot41" }; static string Repo => Directory.GetParent(Application.dataPath).Parent.FullName; static string Output => Path.Combine(Repo, "Tools/output/bot_animation_lab"); static void Folder(string path) { if (AssetDatabase.IsValidFolder(path)) return; Folder(Path.GetDirectoryName(path).Replace('\\', '/')); AssetDatabase.CreateFolder(Path.GetDirectoryName(path).Replace('\\', '/'), Path.GetFileName(path)); } static void Report(string name, object report) { Directory.CreateDirectory(Output); File.WriteAllText(Path.Combine(Output, name + ".json"), JsonConvert.SerializeObject(report, Formatting.Indented)); } public static string Configure() { if (EditorApplication.isPlayingOrWillChangePlaymode) throw new Exception("Edit Mode required"); Folder(Root); Folder(Common + "/Prefabs"); var reports = new List(); float referenceHeight = 0; foreach (string name in Names) { string path = Common + "/" + name + ".fbx"; AssetDatabase.ImportAsset(path, ImportAssetOptions.ForceSynchronousImport); var importer = (ModelImporter)AssetImporter.GetAtPath(path); if (importer.animationType != ModelImporterAnimationType.Human || importer.avatarSetup != ModelImporterAvatarSetup.CreateFromThisModel) { importer.animationType = ModelImporterAnimationType.Human; importer.avatarSetup = ModelImporterAvatarSetup.CreateFromThisModel; importer.SaveAndReimport(); } var avatar = AssetDatabase.LoadAllAssetsAtPath(path).OfType().Single(); if (!avatar.isValid || !avatar.isHuman) throw new Exception(name + " invalid Humanoid Avatar"); var model = AssetDatabase.LoadAssetAtPath(path); var bones = model.GetComponentsInChildren(true).Where(t => t.name.StartsWith("mixamorig:")).Select(t => t.name).ToArray(); var mapped = importer.humanDescription.human; string controllerPath = Common + "/" + (name == "YBot" ? "YBot" : "XBot") + ".controller"; var controller = AssetDatabase.LoadAssetAtPath(controllerPath); if (!controller) throw new Exception("Missing controller " + controllerPath); var preview = EditorSceneManager.NewPreviewScene(); try { var go = (GameObject)PrefabUtility.InstantiatePrefab(model, preview); go.name = name; float sourceHeight = BoundsOf(go).size.y; if (name == "XBot") referenceHeight = sourceHeight; if (name == "XBot25" || name == "XBot41") go.transform.localScale *= referenceHeight / sourceHeight; var animator = go.GetComponent(); if (!animator) animator = go.AddComponent(); animator.avatar = avatar; animator.runtimeAnimatorController = controller; animator.applyRootMotion = false; animator.cullingMode = AnimatorCullingMode.AlwaysAnimate; foreach (var skin in go.GetComponentsInChildren()) skin.updateWhenOffscreen = true; string prefab = Common + "/Prefabs/" + name + ".prefab"; PrefabUtility.SaveAsPrefabAsset(go, prefab); reports.Add(new { name, path, bones = bones.Length, boneNames = bones, mapped = mapped.Select(b => new { b.humanName, b.boneName }), avatar = avatar.name, avatarValid = avatar.isValid, avatarHuman = avatar.isHuman, controller = controllerPath, prefab, sourceHeight, prefabHeight = BoundsOf(go).size.y, prefabScale = new[] { go.transform.localScale.x, go.transform.localScale.y, go.transform.localScale.z }, renderers = go.GetComponentsInChildren().Select(r => new { r.name, skinBones = r.bones.Length, material = r.sharedMaterial ? r.sharedMaterial.name : null }).ToArray() }); } finally { EditorSceneManager.ClosePreviewScene(preview); } } Report("avatar_configuration", reports); return JsonConvert.SerializeObject(reports); } static Material Material(string name, Color color) { Folder(Root + "/Generated"); string path = Root + "/Generated/" + name + ".mat"; var material = AssetDatabase.LoadAssetAtPath(path); if (!material) { var shader = Shader.Find("Universal Render Pipeline/Lit") ?? Shader.Find("Standard"); material = new Material(shader) { name = name }; AssetDatabase.CreateAsset(material, path); } material.color = color; if (material.HasProperty("_Smoothness")) material.SetFloat("_Smoothness", .35f); EditorUtility.SetDirty(material); AssetDatabase.SaveAssetIfDirty(material); return material; } static Bounds BoundsOf(GameObject go) { var renderers = go.GetComponentsInChildren(); var bounds = renderers[0].bounds; foreach (var r in renderers.Skip(1)) bounds.Encapsulate(r.bounds); return bounds; } public static string Build() { if (EditorApplication.isPlayingOrWillChangePlaymode) throw new Exception("Edit Mode required"); for (int i = 0; i < SceneManager.sceneCount; i++) if (SceneManager.GetSceneAt(i).isDirty) throw new Exception("Unsaved scene: " + SceneManager.GetSceneAt(i).path); Configure(); string uiPath = JsonUIPrefabBuilder.BuildFromJsonFile(Path.Combine(Repo, "Doc/UIPrefabCreater/UI_BotAnimationLab.json")); if (string.IsNullOrEmpty(uiPath)) throw new Exception("UI JSON generation failed"); var scene = EditorSceneManager.NewScene(NewSceneSetup.EmptyScene, NewSceneMode.Single); var root = new GameObject("BotAnimationLab"); var viewer = root.AddComponent(); viewer.actors = new BotAnimationLab.Actor[4]; viewer.clayMaterial = Material("Clay", new Color(.65f, .69f, .74f)); Color[] colors = { new Color(.20f,.60f,.90f), new Color(.96f,.55f,.18f), new Color(.28f,.72f,.43f), new Color(.66f,.42f,.85f) }; for (int i = 0; i < Names.Length; i++) { string name = Names[i]; var stage = new GameObject("Stage_" + name); stage.transform.SetParent(root.transform, false); stage.transform.localPosition = new Vector3((1.5f - i) * 20f, 0, 0); var go = (GameObject)PrefabUtility.InstantiatePrefab(AssetDatabase.LoadAssetAtPath(Common + "/Prefabs/" + name + ".prefab"), stage.transform); var bounds = BoundsOf(go); go.transform.localScale *= 1.6f / bounds.size.y; bounds = BoundsOf(go); go.transform.position -= new Vector3(bounds.center.x - stage.transform.position.x, bounds.min.y, bounds.center.z); var a = go.GetComponent(); var skins = go.GetComponentsInChildren(); var color = Material(name + "Color", colors[i]); var joints = Material(name + "Joints", colors[i] * .6f); foreach (var skin in skins) skin.sharedMaterial = skin.name.Contains("Joint") ? joints : color; var controller = (AnimatorController)a.runtimeAnimatorController; var states = controller.layers[0].stateMachine.states.Select(s => s.state).ToArray(); viewer.actors[i] = new BotAnimationLab.Actor { label = name, animator = a, stage = stage.transform, skins = skins, originalMaterials = skins.Select(s => s.sharedMaterial).ToArray(), clips = BotAnimationLab.StateNames.Select(n => (AnimationClip)states.Single(s => s.name == n).motion).ToArray() }; var pedestal = GameObject.CreatePrimitive(PrimitiveType.Cylinder); pedestal.name = "Pedestal_" + name; pedestal.transform.SetParent(root.transform, false); pedestal.transform.position = stage.transform.position + new Vector3(0,-.035f,0); pedestal.transform.localScale = new Vector3(1.25f,.035f,1.25f); pedestal.GetComponent().sharedMaterial = color; UnityEngine.Object.DestroyImmediate(pedestal.GetComponent()); } var floor = GameObject.CreatePrimitive(PrimitiveType.Plane); floor.name = "ReviewFloor"; floor.transform.SetParent(root.transform, false); floor.transform.position = new Vector3(0,-.08f,0); floor.transform.localScale = new Vector3(10,1,1); floor.GetComponent().sharedMaterial = Material("Floor", new Color(.79f,.84f,.87f)); UnityEngine.Object.DestroyImmediate(floor.GetComponent()); var background = new GameObject("BackgroundCamera").AddComponent(); background.transform.SetParent(root.transform, false); background.clearFlags = CameraClearFlags.SolidColor; background.backgroundColor = new Color(.82f,.89f,.94f); background.depth = -10; background.cullingMask = 0; var camera = new GameObject("ReviewCamera").AddComponent(); camera.transform.SetParent(root.transform, false); camera.tag = "MainCamera"; camera.rect = new Rect(0,.335f,1,.54f); camera.orthographic = true; camera.clearFlags = CameraClearFlags.SolidColor; camera.backgroundColor = background.backgroundColor; camera.nearClipPlane = .05f; camera.farClipPlane = 40; camera.allowHDR = false; camera.gameObject.AddComponent(); viewer.reviewCamera = camera; viewer.comparisonCameras = new Camera[4]; for (int i = 0; i < 4; i++) { var cell = new GameObject("CompareCamera_" + Names[i]).AddComponent(); cell.transform.SetParent(root.transform, false); cell.rect = new Rect((i % 2) * .5f, i < 2 ? .605f : .335f, .5f, .27f); cell.orthographic = true; cell.clearFlags = CameraClearFlags.SolidColor; cell.backgroundColor = background.backgroundColor; cell.nearClipPlane = .05f; cell.farClipPlane = 40; cell.allowHDR = false; viewer.comparisonCameras[i] = cell; } foreach (var config in new[] { ("KeyLight", new Vector3(35, -40, 0), 1.3f), ("FillLight", new Vector3(30, 140, 0), .6f) }) { var light = new GameObject(config.Item1).AddComponent(); light.transform.SetParent(root.transform, false); light.type = LightType.Directional; light.transform.rotation = Quaternion.Euler(config.Item2); light.intensity = config.Item3; } RenderSettings.ambientMode = AmbientMode.Flat; RenderSettings.ambientLight = new Color(.65f,.68f,.73f); RenderSettings.fog = false; var ui = (GameObject)PrefabUtility.InstantiatePrefab(AssetDatabase.LoadAssetAtPath(uiPath), root.transform); viewer.ui = ui.transform; ui.GetComponentInChildren(true).viewer = viewer; var events = new GameObject("EventSystem", typeof(EventSystem), typeof(StandaloneInputModule)); events.transform.SetParent(root.transform, false); viewer.RefreshView(); EditorSceneManager.MarkSceneDirty(scene); EditorSceneManager.SaveScene(scene, ScenePath); EditorGUIUtility.PingObject(AssetDatabase.LoadAssetAtPath(ScenePath)); return Verify(); } static void Advance(Animator a, float seconds) { a.speed = 1; for (int i = 0; i < Mathf.CeilToInt(seconds * 60); i++) a.Update(1f / 60); a.speed = 0; } static void Expect(Animator a, string name) { if (!a.GetCurrentAnimatorStateInfo(0).IsName("Base Layer." + name) || a.IsInTransition(0)) throw new Exception("Expected " + name); } public static string Verify() { var reports = new List(); foreach (string name in Names) { var scene = EditorSceneManager.NewPreviewScene(); var mesh = new Mesh(); try { var go = (GameObject)PrefabUtility.InstantiatePrefab(AssetDatabase.LoadAssetAtPath(Common + "/Prefabs/" + name + ".prefab"), scene); var a = go.GetComponent(); if (!a.avatar || !a.avatar.isHuman || !a.avatar.isValid) throw new Exception(name + " invalid Avatar"); var controller = (AnimatorController)a.runtimeAnimatorController; string expected = Common + "/" + (name == "YBot" ? "YBot" : "XBot") + ".controller"; if (AssetDatabase.GetAssetPath(controller) != expected) throw new Exception(name + " wrong controller"); a.Rebind(); a.speed = 0; a.Update(0); var states = new List(); foreach (var child in controller.layers[0].stateMachine.states) { var state = child.state; float minGap = float.MaxValue, maxGap = float.MinValue, motion = 0; Vector3 first = Vector3.zero; for (int i = 0; i < 25; i++) { a.Play("Base Layer." + state.name, 0, i / 25f); a.Update(0); Vector3 hip = a.GetBoneTransform(HumanBodyBones.Hips).position; Vector3 hand = a.GetBoneTransform(HumanBodyBones.LeftHand).position - hip; Vector3 foot = a.GetBoneTransform(HumanBodyBones.LeftFoot).position - hip; if (i == 0) first = hand + foot; motion = Mathf.Max(motion, Vector3.Distance(first, hand + foot)); float gap = go.transform.InverseTransformPoint(a.GetBoneTransform(HumanBodyBones.RightFoot).position).x - go.transform.InverseTransformPoint(a.GetBoneTransform(HumanBodyBones.LeftFoot).position).x; minGap = Mathf.Min(minGap, gap); maxGap = Mathf.Max(maxGap, gap); foreach (var skin in go.GetComponentsInChildren()) { skin.BakeMesh(mesh); if (mesh.vertices.Any(v => float.IsNaN(v.sqrMagnitude) || float.IsInfinity(v.sqrMagnitude))) throw new Exception(name + " invalid skin"); } } if (motion < .00001f) throw new Exception(name + " has no motion in " + state.name); states.Add(new { state = state.name, motion, minAnkleGap = minGap, maxAnkleGap = maxGap, samples = 25, source = AssetDatabase.GetAssetPath(state.motion) }); } var checks = new List(); a.SetInteger("Motion", 0); a.Play("Base Layer.Idle", 0, 0); a.Update(0); a.SetInteger("Motion", 1); Advance(a,.4f); Expect(a,"Walk"); checks.Add("Idle -> Walk"); a.SetInteger("Motion", 0); Advance(a,.4f); Expect(a,"Idle"); checks.Add("Walk -> Idle"); foreach (int locomotion in new[] { 0, 1 }) foreach (string attack in new[] { "Attack01", "Attack02" }) { string rest = locomotion == 0 ? "Idle" : "Walk"; a.SetInteger("Motion", locomotion); a.Play("Base Layer." + rest, 0, 0); a.Update(0); a.SetTrigger(attack); Advance(a,.2f); Expect(a,attack); float length = ((AnimationClip)controller.layers[0].stateMachine.states.Single(s=>s.state.name==attack).state.motion).length; Advance(a,length+.4f); Expect(a,rest); checks.Add(rest + " -> " + attack + " -> " + rest); } reports.Add(new { actor = name, controller = expected, avatar = a.avatar.name, validHumanoid = a.isHuman, states, checks }); } finally { UnityEngine.Object.DestroyImmediate(mesh); EditorSceneManager.ClosePreviewScene(scene); } } var result = new { passed = true, scene = ScenePath, samples = 400, transitionPaths = 24, actors = reports, verification = "Isolated Editor Animator and BakeMesh" }; Report("playback_validation", result); return JsonConvert.SerializeObject(result); } public static string Controls() { if (!Application.isPlaying) throw new Exception("Play Mode required"); var v = UnityEngine.Object.FindObjectOfType(); if (!v || v.actors.Length != 4) throw new Exception("Bot scene not active"); var buttons = v.ui.GetComponentsInChildren