Files
AIGame/AIClient/AgentScripts/DiagnoseMixamoPlayback.cs

147 lines
13 KiB
C#

#if UNITY_EDITOR
using System;
using System.IO;
using System.Linq;
using Newtonsoft.Json;
using UnityEditor;
using UnityEngine;
using UnityEngine.SceneManagement;
using UnityEditor.SceneManagement;
using UnityEditor.Animations;
using UnityEngine.Animations;
using UnityEngine.Playables;
using System.Collections.Generic;
public static class DiagnoseMixamoPlayback
{
static readonly string[] Paths={"Assets/Bundles/Characters/Human/test/Boy_mixamo/CampGuideStudio.fbx","Assets/Bundles/Characters/Human/Common/Walking.fbx","Assets/Bundles/Characters/Human/Common/CampGuideStudio@Walking.fbx"};
static float[] V(Vector3 v)=>new[]{v.x,v.y,v.z};
static string PathOf(UnityEngine.Object o)=>o?AssetDatabase.GetAssetPath(o):null;
const string DiagnosticRoot="Assets/Review/CharacterAnimationLab/PlaybackDiagnostics";
static string OutputDirectory=>System.IO.Path.Combine(Directory.GetParent(Application.dataPath).Parent.FullName,"Tools/output/campguide_studio/playback_audit");
static void Folder(string path){if(AssetDatabase.IsValidFolder(path))return;var parent=System.IO.Path.GetDirectoryName(path).Replace('\\','/');Folder(parent);AssetDatabase.CreateFolder(parent,System.IO.Path.GetFileName(path));}
public static string PrepareVariants()
{
Folder(DiagnosticRoot);
var avatar=AssetDatabase.LoadAllAssetsAtPath(Paths[0]).OfType<Avatar>().Single();
foreach(var tag in new[]{"CopyBoyAvatar","Generic"})
{
string path=DiagnosticRoot+"/Walking_"+tag+".fbx";
if(!AssetDatabase.LoadMainAssetAtPath(path) && !AssetDatabase.CopyAsset(Paths[1],path))throw new Exception("Copy failed "+path);
var importer=(ModelImporter)AssetImporter.GetAtPath(path);
importer.animationType=tag=="Generic"?ModelImporterAnimationType.Generic:ModelImporterAnimationType.Human;
importer.avatarSetup=tag=="Generic"?ModelImporterAvatarSetup.NoAvatar:ModelImporterAvatarSetup.CopyFromOther;
importer.sourceAvatar=tag=="Generic"?null:avatar;
if(tag=="Generic")importer.animationCompression=ModelImporterAnimationCompression.Off;
importer.SaveAndReimport();
}
return "Prepared isolated generic and CopyBoyAvatar variants";
}
public static string FixWalking()
{
var importer=(ModelImporter)AssetImporter.GetAtPath(Paths[1]);
var before=new{path=Paths[1],animationType=importer.animationType.ToString(),avatarSetup=importer.avatarSetup.ToString(),sourceAvatar=PathOf(importer.sourceAvatar)};
Directory.CreateDirectory(OutputDirectory);
var backup=System.IO.Path.Combine(OutputDirectory,"walking_importer_before.json");
if(!File.Exists(backup))File.WriteAllText(backup,JsonConvert.SerializeObject(before,Formatting.Indented));
importer.animationType=ModelImporterAnimationType.Human;
importer.avatarSetup=ModelImporterAvatarSetup.CopyFromOther;
importer.sourceAvatar=AssetDatabase.LoadAllAssetsAtPath(Paths[0]).OfType<Avatar>().Single();
importer.SaveAndReimport();
var clip=AssetDatabase.LoadAllAssetsAtPath(Paths[1]).OfType<AnimationClip>().First(c=>!c.name.StartsWith("__preview__"));
if(!clip.humanMotion || !importer.sourceAvatar.isValid)throw new Exception("Invalid repaired Humanoid clip/avatar");
var report=new{path=Paths[1],avatarSetup=importer.avatarSetup.ToString(),sourceAvatar=PathOf(importer.sourceAvatar),clip=clip.name,clip.humanMotion,
scenes=Enumerable.Range(0,SceneManager.sceneCount).Select(i=>{var s=SceneManager.GetSceneAt(i);return new{s.path,s.isDirty};}).ToArray()};
string json=JsonConvert.SerializeObject(report,Formatting.Indented);File.WriteAllText(System.IO.Path.Combine(OutputDirectory,"fix.json"),json);return json;
}
public static string VerifyFix()
{
var importer=(ModelImporter)AssetImporter.GetAtPath(Paths[1]);
var avatar=AssetDatabase.LoadAllAssetsAtPath(Paths[0]).OfType<Avatar>().Single();
if(importer.avatarSetup!=ModelImporterAvatarSetup.CopyFromOther || importer.sourceAvatar!=avatar)throw new Exception("Wrong source avatar");
var controller=AssetDatabase.LoadAssetAtPath<AnimatorController>("Assets/Bundles/Characters/Human/Common/Man.controller");
var state=controller.layers[0].stateMachine.states.Single(s=>s.state.name=="walk").state;
if(PathOf(state.motion)!=Paths[1] || !(state.motion as AnimationClip).humanMotion)throw new Exception("Controller walk clip lost");
var animators=UnityEngine.Object.FindObjectsOfType<Animator>(true).Where(a=>a.runtimeAnimatorController==controller).ToArray();
var warnings=animators.Select(a=>new SerializedObject(a).FindProperty("m_WarningMessage")?.stringValue).Where(w=>!string.IsNullOrEmpty(w)).ToArray();
if(warnings.Length>0)throw new Exception(string.Join("\n",warnings));
var json=JsonConvert.SerializeObject(new{sourceAvatar=PathOf(avatar),controller=PathOf(controller),walk=PathOf(state.motion),footIK=state.iKOnFeet,
actorCount=animators.Length,bindingWarnings=warnings.Length,EditorApplication.isPlaying,
dirtyScenes=Enumerable.Range(0,SceneManager.sceneCount).Select(i=>{var s=SceneManager.GetSceneAt(i);return new{s.path,s.isDirty};}).ToArray()},Formatting.Indented);
File.WriteAllText(System.IO.Path.Combine(OutputDirectory,"validation.json"),json);return json;
}
class Sampled
{
public string tag;public GameObject unit;public Animator animator;public PlayableGraph graph;public AnimationClipPlayable playable;
public Transform[] bones;public List<Dictionary<string,float[]>> frames=new List<Dictionary<string,float[]>>();
}
public static string Compare()
{
var scene=EditorSceneManager.NewPreviewScene();var entries=new List<Sampled>();
var source=AssetDatabase.LoadAssetAtPath<GameObject>(Paths[0]);
var material=new Material(Shader.Find("Universal Render Pipeline/Lit"));material.color=new Color(.6f,.65f,.72f);
try
{
string[] tags={"Generic","CurrentHumanoid","CopyBoyAvatar"};
string[] paths={DiagnosticRoot+"/Walking_Generic.fbx",Paths[1],DiagnosticRoot+"/Walking_CopyBoyAvatar.fbx"};
for(int i=0;i<tags.Length;i++)
{
var unit=(GameObject)PrefabUtility.InstantiatePrefab(source,scene);unit.transform.position=new Vector3((i-1)*1.25f,0,0);
var animator=unit.GetComponent<Animator>();animator.runtimeAnimatorController=null;animator.applyRootMotion=false;animator.cullingMode=AnimatorCullingMode.AlwaysAnimate;
if(tags[i]=="Generic")animator.avatar=null;
var clip=AssetDatabase.LoadAllAssetsAtPath(paths[i]).OfType<AnimationClip>().First(c=>!c.name.StartsWith("__preview__"));
var graph=PlayableGraph.Create("MixamoDiagnostic"+tags[i]);graph.SetTimeUpdateMode(DirectorUpdateMode.Manual);
var playable=AnimationClipPlayable.Create(graph,clip);playable.SetApplyFootIK(false);playable.SetApplyPlayableIK(false);
var output=AnimationPlayableOutput.Create(graph,"Animation",animator);output.SetSourcePlayable(playable);graph.Play();
foreach(var r in unit.GetComponentsInChildren<Renderer>())r.sharedMaterial=material;
entries.Add(new Sampled{tag=tags[i],unit=unit,animator=animator,graph=graph,playable=playable,bones=unit.GetComponentsInChildren<Transform>().Where(t=>t.name.StartsWith("mixamorig:")).ToArray()});
}
for(int frame=0;frame<32;frame++)foreach(var e in entries)
{
e.playable.SetTime(frame/30.0);e.graph.Evaluate(0);
e.frames.Add(e.bones.ToDictionary(b=>b.name,b=>V(e.unit.transform.InverseTransformPoint(b.position))));
}
var reference=entries[0];var results=new List<object>();
foreach(var e in entries.Skip(1))
{
var errors=new Dictionary<string,float>();
for(int frame=0;frame<32;frame++)foreach(string bone in e.frames[frame].Keys)
{
Vector3 P(float[] p)=>new Vector3(p[0],p[1],p[2]);
var actual=P(e.frames[frame][bone])-P(e.frames[frame]["mixamorig:Hips"]);
var expected=P(reference.frames[frame][bone])-P(reference.frames[frame]["mixamorig:Hips"]);
float error=Vector3.Distance(actual,expected);if(!errors.ContainsKey(bone)||errors[bone]<error)errors[bone]=error;
}
results.Add(new{e.tag,maxJointErrorM=errors.Values.Max(),jointErrors=errors.OrderByDescending(p=>p.Value).ToArray()});
}
foreach(var e in entries){e.playable.SetTime(10/30.0);e.graph.Evaluate(0);var hips=e.bones.First(b=>b.name=="mixamorig:Hips");var p=hips.position;p.x=e.unit.transform.position.x;p.z=0;hips.position=p;}
var light=new GameObject("DiagnosticLight").AddComponent<Light>();SceneManager.MoveGameObjectToScene(light.gameObject,scene);light.type=LightType.Directional;light.intensity=1.4f;light.transform.rotation=Quaternion.Euler(40,140,0);
var camera=new GameObject("DiagnosticCamera").AddComponent<Camera>();SceneManager.MoveGameObjectToScene(camera.gameObject,scene);camera.scene=scene;camera.orthographic=true;camera.orthographicSize=.78f;
camera.transform.position=new Vector3(0,.62f,5);camera.transform.LookAt(new Vector3(0,.62f,0));camera.clearFlags=CameraClearFlags.SolidColor;camera.backgroundColor=new Color(.2f,.23f,.27f);camera.nearClipPlane=.01f;camera.farClipPlane=20;
var rt=new RenderTexture(1440,640,24);var tex=new Texture2D(1440,640,TextureFormat.RGB24,false);var previous=RenderTexture.active;
Directory.CreateDirectory(OutputDirectory);
try{camera.targetTexture=rt;camera.Render();RenderTexture.active=rt;tex.ReadPixels(new Rect(0,0,1440,640),0,0);tex.Apply();File.WriteAllBytes(System.IO.Path.Combine(OutputDirectory,"comparison.png"),tex.EncodeToPNG());}
finally{RenderTexture.active=previous;camera.targetTexture=null;rt.Release();UnityEngine.Object.DestroyImmediate(rt);UnityEngine.Object.DestroyImmediate(tex);}
var json=JsonConvert.SerializeObject(new{order=tags,comparison=results,samples=entries.Select(e=>new{e.tag,e.frames})},Formatting.Indented);File.WriteAllText(System.IO.Path.Combine(OutputDirectory,"comparison.json"),json);
return JsonConvert.SerializeObject(results);
}
finally{foreach(var e in entries)if(e.graph.IsValid())e.graph.Destroy();EditorSceneManager.ClosePreviewScene(scene);UnityEngine.Object.DestroyImmediate(material);}
}
public static string Inspect()
{
var result=new{EditorApplication.isPlaying,scenes=Enumerable.Range(0,SceneManager.sceneCount).Select(i=>{var s=SceneManager.GetSceneAt(i);return new{s.path,s.isDirty};}).ToArray(),
assets=Paths.Select(path=>{
var importer=(ModelImporter)AssetImporter.GetAtPath(path);var hd=importer.humanDescription;var model=AssetDatabase.LoadAssetAtPath<GameObject>(path);
return new{path,importer.animationType,importer.avatarSetup,sourceAvatar=PathOf(importer.sourceAvatar),importer.globalScale,importer.useFileScale,importer.bakeAxisConversion,
human=new{hd.armStretch,hd.legStretch,hd.upperArmTwist,hd.lowerArmTwist,hd.upperLegTwist,hd.lowerLegTwist,hd.feetSpacing,hd.hasTranslationDoF,
mapping=hd.human.Select(b=>new{b.humanName,b.boneName}).ToArray(),skeleton=hd.skeleton.Select(b=>new{b.name,position=V(b.position),rotation=V(b.rotation.eulerAngles),scale=V(b.scale)}).ToArray()},
bones=model.GetComponentsInChildren<Transform>(true).Where(t=>t.name.StartsWith("mixamorig:")).Select(t=>new{t.name,parent=t.parent?t.parent.name:null,world=V(model.transform.InverseTransformPoint(t.position)),local=V(t.localPosition),rotation=V(t.localEulerAngles)}).ToArray(),
clips=importer.clipAnimations.Select(c=>new{c.name,c.firstFrame,c.lastFrame,c.loopTime,c.loopPose,c.lockRootRotation,c.keepOriginalOrientation,c.rotationOffset,c.lockRootPositionXZ,c.keepOriginalPositionXZ,c.lockRootHeightY,c.heightFromFeet}).ToArray(),
animatorClips=AssetDatabase.LoadAllAssetsAtPath(path).OfType<AnimationClip>().Select(c=>new{c.name,c.humanMotion,c.length,settings=AnimationUtility.GetAnimationClipSettings(c)}).ToArray()};
}).ToArray()};
string json=JsonConvert.SerializeObject(result,Formatting.Indented);
string dir=System.IO.Path.Combine(Directory.GetParent(Application.dataPath).Parent.FullName,"Tools/output/campguide_studio/playback_audit");Directory.CreateDirectory(dir);File.WriteAllText(System.IO.Path.Combine(dir,"importers.json"),json);return "Saved "+dir+"/importers.json";
}
}
#endif