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; /// /// Studio low-poly candidate for any character (generalized from GreatHornStudioTrial): binds the Studio mesh to the /// ORIGINAL avatar, controller and finished clips. No animation regeneration. Every entry takes /// (actor, kind, originalPrefabFile, height); the candidate lives at Characters/{kind}/{actor}Studio. /// public static class StudioTrial { sealed class Ctx { public string Actor, Kind, Prefab; public float Height; public string Original=>"Assets/Bundles/Characters/"+Kind+"/"+Actor; public string Trial=>"Assets/Bundles/Characters/"+Kind+"/"+Actor+"Studio"; public string Name=>Actor+"Studio"; public string Animated=>Trial+"/"+Name+"_"+(Kind=="Enemy"?"Enemy":"Companion")+".prefab"; public string Scene=>"Assets/Bundles/Scenes/Review/Studio_"+Actor+"_Review.unity"; public string Out=>Path.Combine(Repo,"Tools/output/studio_trial",Actor); } static Ctx C(string actor,string kind,string prefab,float height)=>new Ctx{Actor=actor,Kind=kind,Prefab=prefab,Height=height}; 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(Ctx c,string name,object data){Directory.CreateDirectory(c.Out);File.WriteAllText(Path.Combine(c.Out,name),JsonConvert.SerializeObject(data,Formatting.Indented));} static Vector3[] Vertices(GameObject go) { return go.GetComponentsInChildren().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(Ctx c,string name)=>AssetDatabase.LoadAssetAtPath(c.Original+"/Clips/"+name+".anim"); static Animator Reference(Ctx c)=>AssetDatabase.LoadAssetAtPath(c.Original+"/"+c.Prefab).GetComponentInChildren(); public static string Build(string actor,string kind,string prefab,float height)=>BuildCore(C(actor,kind,prefab,height),true); public static string Prepare(string actor,string kind,string prefab,float height)=>BuildCore(C(actor,kind,prefab,height),false); static string BuildCore(Ctx c,bool verify) { Check(!EditorApplication.isPlaying,"Author trial in Edit Mode"); string fbx=c.Trial+"/Rigged/"+c.Name+"_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=Reference(c); var scene=EditorSceneManager.NewPreviewScene(); try { var root=new GameObject(Path.GetFileNameWithoutExtension(c.Animated));SceneManager.MoveGameObjectToScene(root,scene); var model=(GameObject)PrefabUtility.InstantiatePrefab(AssetDatabase.LoadAssetAtPath(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();if(anim==null)anim=model.AddComponent();anim.avatar=reference.avatar;anim.runtimeAnimatorController=reference.runtimeAnimatorController;anim.applyRootMotion=false;anim.cullingMode=AnimatorCullingMode.AlwaysAnimate; var material=AssetDatabase.LoadAssetAtPath(c.Trial+"/"+c.Name+".mat");foreach(var skin in model.GetComponentsInChildren()){skin.sharedMaterial=material;skin.updateWhenOffscreen=true;} var expected=reference.GetComponentsInChildren().Where(t=>t.name.StartsWith("bone_")).ToDictionary(t=>AnimationUtility.CalculateTransformPath(t,reference.transform)); var actual=model.GetComponentsInChildren().Where(t=>t.name.StartsWith("bone_")).ToDictionary(t=>AnimationUtility.CalculateTransformPath(t,model.transform)); Check(expected.Count>0&&expected.Keys.OrderBy(x=>x).SequenceEqual(actual.Keys.OrderBy(x=>x)),"Original skeleton paths changed ("+expected.Count+" vs "+actual.Count+")"); 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,c.Animated); } finally{EditorSceneManager.ClosePreviewScene(scene);} AssetDatabase.SaveAssets();return verify?Verify(c):"Prepared original skeleton/controller candidate; sole constraints pending"; } public static string Verify(string actor,string kind,string prefab,float height)=>Verify(C(actor,kind,prefab,height)); static string Verify(Ctx c) { var scene=EditorSceneManager.NewPreviewScene(); try { var go=(GameObject)PrefabUtility.InstantiatePrefab(AssetDatabase.LoadAssetAtPath(c.Animated),scene);var anim=go.GetComponentInChildren();anim.enabled=false; var reference=Reference(c);Check(anim.runtimeAnimatorController==reference.runtimeAnimatorController&&anim.avatar==reference.avatar,"Expected exact original controller/avatar"); int boneCount=reference.GetComponentsInChildren().Count(t=>t.name.StartsWith("bone_")); var bind=Vertices(go);var bounds=Bounds(bind);int triangles=go.GetComponentsInChildren().Sum(s=>s.sharedMesh.triangles.Length/3);int target=(int?)Newtonsoft.Json.Linq.JObject.Parse(File.ReadAllText(Path.Combine(c.Out,"trial.json")))["targetTriangles"]??2000;Check(triangles<=target*1.3f,"Studio target tolerance exceeded");Check(Mathf.Abs(bounds.size.y-c.Height)<.003f&&Mathf.Abs(bounds.min.y)<.003f,"Bind ground/height changed "+bounds); var staticGo=(GameObject)PrefabUtility.InstantiatePrefab(AssetDatabase.LoadAssetAtPath(c.Trial+"/"+c.Name+".prefab"),scene);var staticPoints=staticGo.GetComponentsInChildren().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().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();var root=go.GetComponentsInChildren().Single(t=>t.name=="bone_0"); foreach(string name in new[]{"Idle","Walk","Attack"}) { var clip=Clip(c,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>c.Height*.8f,"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(c,"unity_verification.json",new{status="pass",triangles,bones=boneCount,bindError,bindHeight=bounds.size.y,originalAvatar=true,originalController=true,originalClips=true,rows}); return "PASS original "+boneCount+"-bone bind, exact original avatar/controller/3 clips, "+triangles+" tris, bindError="+bindError.ToString("F5")+", 121-frame motion samples"; } finally{EditorSceneManager.ClosePreviewScene(scene);} } /// Sole/foot vertices with their own four bone influences, sampled over the 363 original action frames. public static string ExportFootConstraints(string actor,string kind,string prefab,float height) { var c=C(actor,kind,prefab,height);var scene=EditorSceneManager.NewPreviewScene(); try { var go=(GameObject)PrefabUtility.InstantiatePrefab(AssetDatabase.LoadAssetAtPath(c.Animated),scene);var anim=go.GetComponentInChildren();anim.enabled=false;var skin=go.GetComponentInChildren();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{var w=weights[i];return new[]{w.boneIndex0,w.boneIndex1,w.boneIndex2,w.boneIndex3};}).ToArray(); var own=indices.Select(i=>{var w=weights[i];return new[]{w.weight0,w.weight1,w.weight2,w.weight3};}).ToArray(); var frames=new List(); foreach(string name in new[]{"Idle","Walk","Attack"})for(int f=0;f<=120;f++) { var clip=Clip(c,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(c,"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=own[j]}).ToArray(),frames}); return "Exported "+indices.Length+" sole/foot vertices x 363 original action frames"; } finally{EditorSceneManager.ClosePreviewScene(scene);} } static void Label(Transform parent,string text,Vector3 position,float size) {var label=new GameObject("Label_"+text).AddComponent();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);} /// Two-row comparison (original above, Studio below): Static / Idle / Walk / Attack. public static string BuildReview(string actor,string kind,string prefab,float height) { var c=C(actor,kind,prefab,height); Check(!EditorApplication.isPlaying,"Review authoring requires Edit Mode");var previous=SceneManager.GetActiveScene();var scene=EditorSceneManager.NewScene(NewSceneSetup.EmptyScene,NewSceneMode.Additive); // GreatHorn used 1.8m columns at 0.8m height; keep that ratio so the ortho camera frames all four. float spacing=c.Height*2.25f; try { SceneManager.SetActiveScene(scene);var root=new GameObject(c.Name+"Comparison");root.transform.position=new Vector3(320,0,0); var names=new[]{"Static","Idle","Walk","Attack"}; for(int row=0;row<2;row++) { string folder=row==0?c.Original:c.Trial,actor0=row==0?c.Actor:c.Name; for(int col=0;col<4;col++) { string path=col==0?folder+"/"+actor0+".prefab":(row==0?c.Original+"/"+c.Prefab:c.Animated); var go=(GameObject)PrefabUtility.InstantiatePrefab(AssetDatabase.LoadAssetAtPath(path),scene);go.name=(row==0?"Original_":"Studio_")+names[col];go.transform.SetParent(root.transform,false);go.transform.localPosition=new Vector3((col-1.5f)*spacing,0,row==0?spacing*.75f:-spacing*.75f); var anim=go.GetComponentInChildren();if(anim!=null){anim.enabled=false;Clip(c,names[col]).SampleAnimation(anim.gameObject,Clip(c,names[col]).length*(col==1?.5f:col==2?.25f:.45f));} Label(go.transform,(row==0?"Original":"Studio")+" / "+names[col],new Vector3(0,-.22f*c.Height/.8f,-.1f),.025f*c.Height/.8f); } } 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(spacing*.5f,1,spacing*.32f);floor.GetComponent().sharedMaterial=AssetDatabase.LoadAssetAtPath("Assets/Bundles/Scenes/Review/EnemyReviewFloor.mat");Object.DestroyImmediate(floor.GetComponent()); var camera=new GameObject(c.Name+"Camera").AddComponent();camera.transform.SetParent(root.transform,false);camera.transform.localPosition=new Vector3(0,7*c.Height/.8f,-10*c.Height/.8f);camera.transform.LookAt(root.transform.position+new Vector3(0,c.Height*.56f,0));camera.orthographic=true;camera.orthographicSize=2.05f*c.Height/.8f;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;camera.enabled=false; var light=new GameObject("StudioReviewLight").AddComponent();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(true))t.gameObject.layer=27; EditorSceneManager.SaveScene(scene,c.Scene); } finally{SceneManager.SetActiveScene(previous);EditorSceneManager.CloseScene(scene,true);} return c.Scene; } public static async Task Capture(string actor,string kind,string prefab,float height) { var c=C(actor,kind,prefab,height); Check(!EditorApplication.isPlaying,"Capture in Edit Mode");var scene=EditorSceneManager.OpenScene(c.Scene,OpenSceneMode.Additive);var root=scene.GetRootGameObjects().Single();var camera=root.GetComponentInChildren();var rt=new RenderTexture(1800,900,24);var tex=new Texture2D(1800,900,TextureFormat.RGB24,false);var old=RenderTexture.active; try { foreach(var anim in root.GetComponentsInChildren()){anim.enabled=false;string name=anim.transform.parent.name.Split('_').Last();var clip=Clip(c,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/"+c.Actor.ToLowerInvariant()+"_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 Play(string actor,string kind,string prefab,float height) { var c=C(actor,kind,prefab,height); Check(EditorApplication.isPlaying,"Enter Play Mode first");var go=Object.Instantiate(AssetDatabase.LoadAssetAtPath(c.Animated));var anim=go.GetComponentInChildren();go.transform.position=new Vector3(320,0,0);var root=go.GetComponentsInChildren().Single(t=>t.name=="bone_0");var seen=new HashSet();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