Files

306 lines
20 KiB
C#

#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<object>();
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<Avatar>().Single();
if (!avatar.isValid || !avatar.isHuman) throw new Exception(name + " invalid Humanoid Avatar");
var model = AssetDatabase.LoadAssetAtPath<GameObject>(path);
var bones = model.GetComponentsInChildren<Transform>(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<AnimatorController>(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<Animator>();
if (!animator) animator = go.AddComponent<Animator>();
animator.avatar = avatar; animator.runtimeAnimatorController = controller;
animator.applyRootMotion = false; animator.cullingMode = AnimatorCullingMode.AlwaysAnimate;
foreach (var skin in go.GetComponentsInChildren<SkinnedMeshRenderer>()) 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<SkinnedMeshRenderer>().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<Material>(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<Renderer>();
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<BotAnimationLab>();
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<GameObject>(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<Animator>();
var skins = go.GetComponentsInChildren<SkinnedMeshRenderer>();
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<Renderer>().sharedMaterial = color;
UnityEngine.Object.DestroyImmediate(pedestal.GetComponent<Collider>());
}
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<Renderer>().sharedMaterial = Material("Floor", new Color(.79f,.84f,.87f));
UnityEngine.Object.DestroyImmediate(floor.GetComponent<Collider>());
var background = new GameObject("BackgroundCamera").AddComponent<Camera>(); 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>(); 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<AudioListener>();
viewer.reviewCamera = camera;
viewer.comparisonCameras = new Camera[4];
for (int i = 0; i < 4; i++)
{
var cell = new GameObject("CompareCamera_" + Names[i]).AddComponent<Camera>(); 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>(); 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<GameObject>(uiPath), root.transform);
viewer.ui = ui.transform; ui.GetComponentInChildren<BotAnimationSeekBar>(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<SceneAsset>(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<object>();
foreach (string name in Names)
{
var scene = EditorSceneManager.NewPreviewScene();
var mesh = new Mesh();
try
{
var go = (GameObject)PrefabUtility.InstantiatePrefab(AssetDatabase.LoadAssetAtPath<GameObject>(Common + "/Prefabs/" + name + ".prefab"), scene);
var a = go.GetComponent<Animator>();
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<object>();
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<SkinnedMeshRenderer>())
{
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<string>();
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<BotAnimationLab>();
if (!v || v.actors.Length != 4) throw new Exception("Bot scene not active");
var buttons = v.ui.GetComponentsInChildren<Button>(true).ToDictionary(b => b.name);
void Click(string n) => buttons["Btn_" + n].onClick.Invoke();
void Tick(float seconds) { for (int i = 0; i < Mathf.CeilToInt(seconds * 60); i++) v.Tick(1f / 60); }
void All(string state) { foreach (var actor in v.actors) Expect(actor.animator, state); }
Click("Idle"); v.Seek(0); Click("Walk"); Tick(.4f); All("Walk");
Click("Idle"); Tick(.4f); All("Idle");
foreach (string attack in new[] { "Attack01", "Attack02" })
{
Click(attack); Tick(.2f); All(attack); Tick(6); All("Idle");
}
Click("Walk"); Tick(.4f); Click("Play");
if (v.playing) throw new Exception("Pause failed");
v.Seek(.2f); float time = v.TimePosition;
Click("Next"); if (v.TimePosition <= time) throw new Exception("Next frame failed");
Click("Previous"); if (Mathf.Abs(v.TimePosition - time) > .002f) throw new Exception("Previous frame failed");
var seek = v.ui.GetComponentInChildren<BotAnimationSeekBar>(true);
Canvas.ForceUpdateCanvases(); var corners = new Vector3[4]; ((RectTransform)seek.transform).GetWorldCorners(corners);
seek.OnPointerDown(new PointerEventData(EventSystem.current) { position = RectTransformUtility.WorldToScreenPoint(null, Vector3.Lerp(corners[0], corners[2], .5f)) });
if (Mathf.Abs(v.TimePosition / v.Duration - .5f) > .002f) throw new Exception("Pointer seek failed");
foreach (string speed in new[] { "Speed025", "Speed05", "Speed1", "Speed2" }) Click(speed);
if (v.speed != 2) throw new Exception("Speed failed"); Click("Speed1");
foreach (int i in Enumerable.Range(0,4))
{
Click("Focus" + (i + 1));
if (v.actors.Where((a,j) => a.skins.Any(s => s.enabled) != (j == i)).Any()) throw new Exception("Focus failed");
}
Click("Detail"); Click("Detail"); if (v.detail != 2) throw new Exception("Hand view failed");
Click("All"); Click("Material");
if (!v.actors.All(a => a.skins.All(s => s.sharedMaterial == v.clayMaterial))) throw new Exception("Clay failed");
Click("Material"); Click("Side"); Click("Back"); Click("Front");
Click("Loop"); if (!v.repeatAttacks) throw new Exception("Loop toggle failed");
Click("Attack01"); Tick(6);
if (v.actors.Any(a=>a.animator.GetCurrentAnimatorStateInfo(0).IsName("Base Layer.Idle") && !a.animator.IsInTransition(0))) throw new Exception("Attack repeat failed");
Click("Loop"); Click("Walk"); Tick(.4f); v.Seek(.25f); v.RefreshView();
if (v.ui.GetComponentsInChildren<TMP_Text>(true).Any(t => t.text.Contains("Infinity") || t.text.Contains("NaN"))) throw new Exception("Invalid time display");
var result = new { passed = true, buttons = buttons.Count, checks = new[] { "locomotion controller transitions", "attack triggers and auto return", "pause", "next/previous frame", "pointer seek", "four speeds", "four actor focus", "hand detail", "gray/color", "front/side/back", "repeat attacks" } };
Report("play_controls", result); return JsonConvert.SerializeObject(result);
}
public static string Pose(int motion, int focus, float phase)
{
var v = UnityEngine.Object.FindObjectOfType<BotAnimationLab>();
v.selectedMotion = motion; v.SetFocus(focus); v.Seek(phase); v.RefreshView();
return JsonConvert.SerializeObject(new { v.selectedMotion, v.focus, phase });
}
}
#endif