Files
AIGame/AIClient/AgentScripts/GreatHornStudioTrial.cs
T

188 lines
20 KiB
C#

using System;
using System.IO;
using System.Linq;
using System.Collections.Generic;
using System.Threading.Tasks;
using Newtonsoft.Json;
using UnityEditor;
using UnityEditor.SceneManagement;
using UnityEngine;
using UnityEngine.SceneManagement;
using Object=UnityEngine.Object;
/// <summary>Isolated candidate uses the ORIGINAL avatar, controller and finished clips. No animation regeneration.</summary>
public static class GreatHornStudioTrial
{
const string Original="Assets/Bundles/Characters/Enemy/GreatHorn";
const string Trial="Assets/Bundles/Characters/Enemy/GreatHornStudio";
const string ScenePath="Assets/Bundles/Scenes/Review/Enemy_GreatHornStudio_Review.unity";
static string Repo=>Path.GetFullPath(Path.Combine(Application.dataPath,"../.."));
static void Check(bool value,string message){if(!value)throw new Exception(message);}
static void Write(string name,object data){string dir=Path.Combine(Repo,"Tools/output/enemy_studio_trial/GreatHorn");Directory.CreateDirectory(dir);File.WriteAllText(Path.Combine(dir,name),JsonConvert.SerializeObject(data,Formatting.Indented));}
static Vector3[] Vertices(GameObject go)
{
return go.GetComponentsInChildren<SkinnedMeshRenderer>().SelectMany(s=>{
var m=s.sharedMesh;var weights=m.boneWeights;var binds=m.bindposes;var matrices=s.bones.Select((b,i)=>b.localToWorldMatrix*binds[i]).ToArray();
return m.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();
}
static Bounds Bounds(Vector3[] points){var b=new Bounds(points[0],Vector3.zero);foreach(var p in points)b.Encapsulate(p);return b;}
static AnimationClip Clip(string name)=>AssetDatabase.LoadAssetAtPath<AnimationClip>(Original+"/Clips/"+name+".anim");
public static string Inspect()
{
var prefab=AssetDatabase.LoadAssetAtPath<GameObject>(Original+"/GreatHorn_Enemy.prefab");var anim=prefab.GetComponentInChildren<Animator>();
return JsonConvert.SerializeObject(new{model=anim.name,scale=anim.transform.localScale.ToString("F6"),position=anim.transform.localPosition.ToString("F6"),rotation=anim.transform.localEulerAngles.ToString("F6"),avatar=AssetDatabase.GetAssetPath(anim.avatar),controller=AssetDatabase.GetAssetPath(anim.runtimeAnimatorController),bones=prefab.GetComponentsInChildren<Transform>().Where(t=>t.name.StartsWith("bone_")).Select(t=>new{name=t.name,path=AnimationUtility.CalculateTransformPath(t,anim.transform),position=t.localPosition.ToString("F6"),rotation=t.localRotation.ToString("F6")}).ToArray()},Formatting.Indented);
}
public static string Build()=>BuildCore(true);
public static string Prepare()=>BuildCore(false);
static string BuildCore(bool verify)
{
Check(!EditorApplication.isPlaying,"Author trial in Edit Mode");
string fbx=Trial+"/Rigged/GreatHornStudio_Rigged.fbx";
AssetDatabase.ImportAsset(fbx,ImportAssetOptions.ForceUpdate);
var imp=(ModelImporter)AssetImporter.GetAtPath(fbx);imp.animationType=ModelImporterAnimationType.Generic;imp.avatarSetup=ModelImporterAvatarSetup.CreateFromThisModel;imp.importAnimation=false;imp.globalScale=1;imp.useFileScale=false;imp.bakeAxisConversion=true;imp.materialImportMode=ModelImporterMaterialImportMode.None;imp.meshCompression=ModelImporterMeshCompression.Off;imp.SaveAndReimport();
var reference=AssetDatabase.LoadAssetAtPath<GameObject>(Original+"/GreatHorn_Enemy.prefab").GetComponentInChildren<Animator>();
var scene=EditorSceneManager.NewPreviewScene();
try
{
var root=new GameObject("GreatHornStudio_Enemy");SceneManager.MoveGameObjectToScene(root,scene);
var model=(GameObject)PrefabUtility.InstantiatePrefab(AssetDatabase.LoadAssetAtPath<GameObject>(fbx),scene);model.transform.SetParent(root.transform,false);model.name="Model";
model.transform.localPosition=reference.transform.localPosition;model.transform.localRotation=reference.transform.localRotation;model.transform.localScale=reference.transform.localScale;
var anim=model.GetComponent<Animator>();if(anim==null)anim=model.AddComponent<Animator>();anim.avatar=reference.avatar;anim.runtimeAnimatorController=reference.runtimeAnimatorController;anim.applyRootMotion=false;anim.cullingMode=AnimatorCullingMode.AlwaysAnimate;
var material=AssetDatabase.LoadAssetAtPath<Material>(Trial+"/GreatHornStudio.mat");foreach(var skin in model.GetComponentsInChildren<SkinnedMeshRenderer>()) {skin.sharedMaterial=material;skin.updateWhenOffscreen=true;}
var expected=reference.GetComponentsInChildren<Transform>().Where(t=>t.name.StartsWith("bone_")).ToDictionary(t=>AnimationUtility.CalculateTransformPath(t,reference.transform));
var actual=model.GetComponentsInChildren<Transform>().Where(t=>t.name.StartsWith("bone_")).ToDictionary(t=>AnimationUtility.CalculateTransformPath(t,model.transform));
Check(expected.Count==22&&expected.Keys.OrderBy(x=>x).SequenceEqual(actual.Keys.OrderBy(x=>x)),"Original skeleton paths changed");
foreach(var entry in expected){var t=actual[entry.Key];Check(Vector3.Distance(t.localPosition,entry.Value.localPosition)<.0001f&&Quaternion.Angle(t.localRotation,entry.Value.localRotation)<.01f&&Vector3.Distance(t.localScale,entry.Value.localScale)<.0001f,"Bind transform changed "+entry.Key);}
PrefabUtility.SaveAsPrefabAsset(root,Trial+"/GreatHornStudio_Enemy.prefab");
}
finally{EditorSceneManager.ClosePreviewScene(scene);}
AssetDatabase.SaveAssets();return verify?Verify():"Prepared original skeleton/controller candidate; sole constraints pending";
}
public static string Verify()
{
var scene=EditorSceneManager.NewPreviewScene();
try
{
var go=(GameObject)PrefabUtility.InstantiatePrefab(AssetDatabase.LoadAssetAtPath<GameObject>(Trial+"/GreatHornStudio_Enemy.prefab"),scene);var anim=go.GetComponentInChildren<Animator>();anim.enabled=false;
var reference=AssetDatabase.LoadAssetAtPath<GameObject>(Original+"/GreatHorn_Enemy.prefab").GetComponentInChildren<Animator>();Check(anim.runtimeAnimatorController==reference.runtimeAnimatorController&&anim.avatar==reference.avatar,"Expected exact original controller/avatar");
var bind=Vertices(go);var bounds=Bounds(bind);int triangles=go.GetComponentsInChildren<SkinnedMeshRenderer>().Sum(s=>s.sharedMesh.triangles.Length/3);Check(triangles<=2600,"Studio target tolerance exceeded");Check(Mathf.Abs(bounds.size.y-.8f)<.003f&&Mathf.Abs(bounds.min.y)<.003f,"Bind ground/height changed "+bounds);
var staticGo=(GameObject)PrefabUtility.InstantiatePrefab(AssetDatabase.LoadAssetAtPath<GameObject>(Trial+"/GreatHornStudio.prefab"),scene);var staticPoints=staticGo.GetComponentsInChildren<MeshFilter>().SelectMany(m=>m.sharedMesh.vertices.Select(v=>m.transform.TransformPoint(v))).ToArray();float bindError=bind.Max(v=>staticPoints.Min(p=>Vector3.Distance(v,p)));Check(bindError<.002f,"Weight transfer changed static geometry "+bindError);
Check(go.GetComponentsInChildren<SkinnedMeshRenderer>().All(s=>s.sharedMesh.boneWeights.All(w=>Mathf.Abs(w.weight0+w.weight1+w.weight2+w.weight3-1)<.001f)),"Invalid weights");
var rows=new List<object>();var root=go.GetComponentsInChildren<Transform>().Single(t=>t.name=="bone_0");
foreach(string name in new[]{"Idle","Walk","Attack"})
{
var clip=Clip(name);Check(anim.runtimeAnimatorController.animationClips.Contains(clip),"Not original clip "+name);
foreach(var binding in AnimationUtility.GetCurveBindings(clip))Check(string.IsNullOrEmpty(binding.path)||anim.transform.Find(binding.path)!=null,"Unbound original curve "+binding.path);
Vector3[] start=null,end=null;float minY=float.MaxValue,maxY=float.MinValue,movement=0,tilt=0;Vector3 rootStart=Vector3.zero;float drift=0;
for(int frame=0;frame<=120;frame++) {clip.SampleAnimation(anim.gameObject,clip.length*frame/120);var points=Vertices(go);Check(points.All(p=>!float.IsNaN(p.x)&&!float.IsInfinity(p.x)),"Invalid mesh");var b=Bounds(points);if(frame==0){start=points;rootStart=root.position;}if(frame==120)end=points;minY=Mathf.Min(minY,b.min.y);maxY=Mathf.Max(maxY,b.min.y);movement=Mathf.Max(movement,points.Select((p,i)=>Vector3.Distance(p,start[i])).Max());tilt=Mathf.Max(tilt,Vector3.Angle(root.up,Vector3.up));drift=Mathf.Max(drift,Vector2.Distance(new Vector2(root.position.x,root.position.z),new Vector2(rootStart.x,rootStart.z)));Check(b.size.y>.64f,"Collapsed mesh "+name);}
float seam=start.Select((p,i)=>Vector3.Distance(p,end[i])).Max();Check(minY>=-.003f&&maxY<=.003f,"Ground contact failed "+name+" "+minY+".."+maxY);Check(tilt<25&&seam<.001f,"Orientation/seam failed "+name);Check(movement>.0002f,"Motion invisible "+name);
rows.Add(new{name,originalClip=AssetDatabase.GetAssetPath(clip),frames=121,minY,maxY,movement,tilt,drift,seam});
}
Write("unity_verification.json",new{status="pass",triangles,bones=22,bindError,bindHeight=bounds.size.y,originalAvatar=true,originalController=true,originalClips=true,rows});return "PASS original 22-bone bind, exact original avatar/controller/3 clips, static geometry, weights and 121-frame motion samples";
}
finally{EditorSceneManager.ClosePreviewScene(scene);}
}
static void Label(Transform parent,string text,Vector3 position,float size)
{var label=new GameObject("Label_"+text).AddComponent<TextMesh>();label.transform.SetParent(parent,false);label.transform.localPosition=position;label.transform.localRotation=Quaternion.Euler(12,0,0);label.text=text;label.fontSize=60;label.characterSize=size;label.anchor=TextAnchor.MiddleCenter;label.color=new Color(.87f,.93f,1);}
public static string BuildReview()
{
Check(!EditorApplication.isPlaying,"Review authoring requires Edit Mode");var previous=SceneManager.GetActiveScene();var scene=EditorSceneManager.NewScene(NewSceneSetup.EmptyScene,NewSceneMode.Additive);
try
{
SceneManager.SetActiveScene(scene);var root=new GameObject("GreatHornStudioComparison");root.transform.position=new Vector3(300,0,0);
var names=new[]{"Static","Idle","Walk","Attack"};
for(int row=0;row<2;row++)
{
string folder=row==0?Original:Trial,actor=row==0?"GreatHorn":"GreatHornStudio";
for(int col=0;col<4;col++)
{
var asset=AssetDatabase.LoadAssetAtPath<GameObject>(folder+"/"+actor+(col==0?".prefab":"_Enemy.prefab"));var go=(GameObject)PrefabUtility.InstantiatePrefab(asset,scene);go.name=(row==0?"Original_":"Studio_")+names[col];go.transform.SetParent(root.transform,false);go.transform.localPosition=new Vector3((col-1.5f)*1.8f,0,row==0?1.3f:-1.3f);
var anim=go.GetComponentInChildren<Animator>();if(anim!=null){anim.enabled=false;Clip(names[col]).SampleAnimation(anim.gameObject,Clip(names[col]).length*(col==1?.5f:col==2?.25f:.45f));}
Label(go.transform,(row==0?"Original":"Studio")+" / "+names[col],new Vector3(0,-.22f,-.1f),.025f);
if(anim!=null)
{
anim.enabled=true;var cycle=go.AddComponent<CharacterReviewAnimCycle>();cycle.enabled=false;var so=new SerializedObject(cycle);so.FindProperty("fixedMotion").intValue=col-1;so.ApplyModifiedPropertiesWithoutUndo();cycle.enabled=true;
}
}
}
var floor=GameObject.CreatePrimitive(PrimitiveType.Plane);floor.name="Ground_Y0";floor.transform.SetParent(root.transform,false);floor.transform.localPosition=new Vector3(0,-.002f,0);floor.transform.localScale=new Vector3(.85f,1,.55f);floor.GetComponent<Renderer>().sharedMaterial=AssetDatabase.LoadAssetAtPath<Material>("Assets/Bundles/Scenes/Review/EnemyReviewFloor.mat");Object.DestroyImmediate(floor.GetComponent<Collider>());
var camera=new GameObject("GreatHornStudioCamera").AddComponent<Camera>();camera.transform.SetParent(root.transform,false);camera.transform.localPosition=new Vector3(0,7,-10);camera.transform.LookAt(root.transform.position+new Vector3(0,.45f,0));camera.orthographic=true;camera.orthographicSize=2.05f;camera.aspect=2;camera.nearClipPlane=.1f;camera.farClipPlane=100;camera.cullingMask=1<<27;camera.backgroundColor=new Color(.075f,.105f,.14f);camera.clearFlags=CameraClearFlags.SolidColor;camera.depth=85;
var light=new GameObject("StudioReviewLight").AddComponent<Light>();light.transform.SetParent(root.transform,false);light.type=LightType.Directional;light.transform.rotation=Quaternion.Euler(40,-35,0);light.intensity=1.1f;light.cullingMask=1<<27;
foreach(var t in root.GetComponentsInChildren<Transform>(true))t.gameObject.layer=27;
EditorSceneManager.SaveScene(scene,ScenePath);
}
finally{SceneManager.SetActiveScene(previous);EditorSceneManager.CloseScene(scene,true);}
return ScenePath;
}
public static string Diagnose()
{
var scene=EditorSceneManager.NewPreviewScene();var rows=new List<object>();
try
{
foreach(string folder in new[]{Original,Trial})
{
string actor=folder==Original?"GreatHorn":"GreatHornStudio";var go=(GameObject)PrefabUtility.InstantiatePrefab(AssetDatabase.LoadAssetAtPath<GameObject>(folder+"/"+actor+"_Enemy.prefab"),scene);var anim=go.GetComponentInChildren<Animator>();anim.enabled=false;
foreach(string name in new[]{"Idle","Walk","Attack"})
{
var clip=Clip(name);float minY=float.MaxValue,maxY=float.MinValue;int minFrame=0,minVertex=0;Vector3 minimum=Vector3.zero;
for(int f=0;f<=120;f++){clip.SampleAnimation(anim.gameObject,clip.length*f/120);var points=Vertices(go);int index=Enumerable.Range(0,points.Length).OrderBy(i=>points[i].y).First();if(points[index].y<minY){minY=points[index].y;minFrame=f;minVertex=index;minimum=points[index];}maxY=Mathf.Max(maxY,points[index].y);}
var skin=go.GetComponentInChildren<SkinnedMeshRenderer>();var w=skin.sharedMesh.boneWeights[minVertex];
rows.Add(new{actor,name,minY,maxY,minFrame,minVertex,point=new[]{minimum.x,minimum.y,minimum.z},weights=new[]{new{bone=skin.bones[w.boneIndex0].name,value=w.weight0},new{bone=skin.bones[w.boneIndex1].name,value=w.weight1},new{bone=skin.bones[w.boneIndex2].name,value=w.weight2},new{bone=skin.bones[w.boneIndex3].name,value=w.weight3}}});
}
Object.DestroyImmediate(go);
}
Write("ground_diagnostics.json",rows);return JsonConvert.SerializeObject(rows,Formatting.Indented);
}
finally{EditorSceneManager.ClosePreviewScene(scene);}
}
public static string ExportFootConstraints()
{
var scene=EditorSceneManager.NewPreviewScene();
try
{
var go=(GameObject)PrefabUtility.InstantiatePrefab(AssetDatabase.LoadAssetAtPath<GameObject>(Trial+"/GreatHornStudio_Enemy.prefab"),scene);var anim=go.GetComponentInChildren<Animator>();anim.enabled=false;var skin=go.GetComponentInChildren<SkinnedMeshRenderer>();var mesh=skin.sharedMesh;var bind=Vertices(go);var bounds=Bounds(bind);var weights=mesh.boneWeights;
var meshVertices=mesh.vertices;var indices=Enumerable.Range(0,bind.Length).Where(i=>bind[i].y<.112f).ToArray();var sets=new List<int[]>();var original=new List<float[]>();
foreach(int i in indices)
{
var w=weights[i];var pairs=new[]{(w.boneIndex0,w.weight0),(w.boneIndex1,w.weight1),(w.boneIndex2,w.weight2),(w.boneIndex3,w.weight3)};float left=pairs.Where(p=>new[]{"bone_18","bone_19","bone_20","bone_21"}.Contains(skin.bones[p.Item1].name)).Sum(p=>p.Item2);int start=left>.5f?18:14;
var bones=Enumerable.Range(start,4).Select(n=>Array.FindIndex(skin.bones,b=>b.name=="bone_"+n)).ToArray();sets.Add(bones);original.Add(bones.Select(b=>pairs.Where(p=>p.Item1==b).Sum(p=>p.Item2)).ToArray());
}
var frames=new List<float[][]>();
foreach(string name in new[]{"Idle","Walk","Attack"})for(int f=0;f<=120;f++)
{
var clip=Clip(name);clip.SampleAnimation(anim.gameObject,clip.length*f/120);var matrices=skin.bones.Select((b,i)=>b.localToWorldMatrix*mesh.bindposes[i]).ToArray();
frames.Add(indices.Select((v,j)=>sets[j].Select(b=>matrices[b].MultiplyPoint3x4(meshVertices[v]).y).ToArray()).ToArray());
}
Write("foot_constraints.json",new{modelScale=anim.transform.localScale.x,vertices=indices.Select((v,j)=>new{index=v,normalizedBind=new[]{(bind[v].x-bounds.center.x)/bounds.size.y,(bind[v].y-bounds.min.y)/bounds.size.y,(bind[v].z-bounds.center.z)/bounds.size.y},bones=sets[j].Select(b=>skin.bones[b].name).ToArray(),weights=original[j]}).ToArray(),frames});return "Exported "+indices.Length+" sole/foot vertices x 363 original action frames";
}
finally{EditorSceneManager.ClosePreviewScene(scene);}
}
public static async Task<string> Capture()
{
Check(!EditorApplication.isPlaying,"Capture in Edit Mode");var scene=EditorSceneManager.OpenScene(ScenePath,OpenSceneMode.Additive);var root=scene.GetRootGameObjects().Single();var camera=root.GetComponentInChildren<Camera>();var rt=new RenderTexture(1800,900,24);var tex=new Texture2D(1800,900,TextureFormat.RGB24,false);var old=RenderTexture.active;
try
{
camera.enabled=false;
foreach(var cycle in root.GetComponentsInChildren<CharacterReviewAnimCycle>())cycle.enabled=false;
foreach(var anim in root.GetComponentsInChildren<Animator>()){anim.enabled=false;string name=anim.transform.parent.name.Split('_').Last();var clip=Clip(name);clip.SampleAnimation(anim.gameObject,clip.length*(name=="Idle"?.5f:name=="Walk"?.25f:.45f));}
await Task.Delay(100);camera.targetTexture=rt;camera.Render();RenderTexture.active=rt;tex.ReadPixels(new Rect(0,0,1800,900),0,0);tex.Apply();string asset="Assets/Screenshots/greathorn_studio_comparison.png";File.WriteAllBytes(Path.Combine(Application.dataPath,"../",asset),tex.EncodeToPNG());AssetDatabase.ImportAsset(asset);return asset;
}
finally{RenderTexture.active=old;camera.targetTexture=null;rt.Release();Object.DestroyImmediate(rt);Object.DestroyImmediate(tex);EditorSceneManager.CloseScene(scene,true);}
}
public static async Task<string> Play()
{
Check(EditorApplication.isPlaying,"Enter Play Mode first");var go=Object.Instantiate(AssetDatabase.LoadAssetAtPath<GameObject>(Trial+"/GreatHornStudio_Enemy.prefab"));var anim=go.GetComponentInChildren<Animator>();go.transform.position=new Vector3(300,0,0);var root=go.GetComponentsInChildren<Transform>().Single(t=>t.name=="bone_0");var seen=new HashSet<string>();float maxTilt=0;
try
{
for(int phase=0;phase<3;phase++)
{
anim.SetInteger("Motion",phase==1?1:0);if(phase==2)anim.SetTrigger("Attack");var until=DateTime.UtcNow.AddSeconds(2);
while(DateTime.UtcNow<until){var s=anim.GetCurrentAnimatorStateInfo(0);foreach(var name in new[]{"Idle","Walk","Attack"})if(s.IsName(name))seen.Add(name);maxTilt=Mathf.Max(maxTilt,Vector3.Angle(root.up,Vector3.up));Check(Vector3.Distance(go.transform.position,new Vector3(300,0,0))<.001f,"Root drift");await Task.Delay(50);}
}
Check(seen.Count==3&&maxTilt<25,"Live animation missing/wrong orientation");Write("play_verification.json",new{status="pass",seen,maxTilt,originalController=AssetDatabase.GetAssetPath(anim.runtimeAnimatorController),rootPinned=true});return "PASS Play: original Idle/Walk/Attack on Studio mesh; maxTilt="+maxTilt;
}
finally{Object.Destroy(go);}
}
}