147 lines
13 KiB
C#
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
|