Files
AIGame/AIClient/AgentScripts/FinalizeEnemyMotions.cs
T

161 lines
10 KiB
C#

#if UNITY_EDITOR
using System;
using System.IO;
using System.Linq;
using System.Text;
using UnityEditor;
using UnityEngine;
using Object=UnityEngine.Object;
/// <summary>Offline source-motion editing; saves assets, never adds runtime correction.</summary>
public static class FinalizeEnemyMotions
{
[Serializable] public sealed class Recipe
{
public string id,assetRoot,rootBone,chestBone,rightArm,leftArm,outputDir,profile,attackStyle,attackEvent,usage;
public float height,idleSeconds,idleBreath,attackSeconds,attackWindup,attackImpact,attackRecover;
public float attackLeanDegrees,attackArmDegrees,attackLunge,attackReach,idleSourceWeight,attackSourceWeight;
}
sealed class Pose
{
public Vector3[] p,s;
public Quaternion[] q;
public static Pose Read(Transform[] b)=>new Pose { p=b.Select(t=>t.localPosition).ToArray(),s=b.Select(t=>t.localScale).ToArray(),q=b.Select(t=>t.localRotation).ToArray() };
public void Write(Transform[] b) { for(int i=0;i<b.Length;i++) { b[i].localPosition=p[i];b[i].localRotation=q[i];b[i].localScale=s[i]; } }
}
static string Repo=>Path.GetFullPath(Path.Combine(Application.dataPath,"../.."));
static float Smooth(float t)=>Mathf.SmoothStep(0,1,Mathf.Clamp01(t));
static float Strike(Recipe r,float t)
{
if(t<r.attackWindup) return -.3f*Smooth(t/r.attackWindup);
if(t<r.attackImpact) return Mathf.Lerp(-.3f,1,Smooth((t-r.attackWindup)/(r.attackImpact-r.attackWindup)));
if(t<r.attackRecover) return 1-Smooth((t-r.attackImpact)/(r.attackRecover-r.attackImpact));
return 0;
}
static Vector3[] Vertices(SkinnedMeshRenderer skin)
{
var mesh=skin.sharedMesh;var bind=mesh.bindposes;var weights=mesh.boneWeights;
var matrices=skin.bones.Select((b,i)=>b.localToWorldMatrix*bind[i]).ToArray();
return mesh.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();
}
static float Bottom(GameObject model)=>model.GetComponentsInChildren<SkinnedMeshRenderer>().SelectMany(Vertices).Min(v=>v.y);
static void Save(AnimationClip clip,Transform model,Transform[] bones,Pose[] poses,float seconds,bool loop)
{
Undo.RecordObject(clip,"Author character motion");
clip.ClearCurves();clip.frameRate=30;
int count=poses.Length;
for(int f=1;f<count;f++) for(int b=0;b<bones.Length;b++)
if(Quaternion.Dot(poses[f-1].q[b],poses[f].q[b])<0) {var q=poses[f].q[b];poses[f].q[b]=new Quaternion(-q.x,-q.y,-q.z,-q.w);}
poses[count-1]=poses[0];
for(int b=0;b<bones.Length;b++)
{
string path=AnimationUtility.CalculateTransformPath(bones[b],model);
for(int c=0;c<10;c++)
{
var keys=new Keyframe[count];
for(int f=0;f<count;f++) keys[f]=new Keyframe(seconds*f/(count-1),c<3 ? poses[f].p[b][c] : c<7 ? poses[f].q[b][c-3] : poses[f].s[b][c-7]);
for(int f=0;f<count;f++)
{
int prev=f==0 ? (loop ? count-2 : 0) : f-1;
int next=f==count-1 ? (loop ? 1 : count-1) : f+1;
float tangent=(keys[next].value-keys[prev].value)*(count-1)/(2*seconds);
if(!loop && (f==0 || f==count-1)) tangent=0;
keys[f].inTangent=tangent;keys[f].outTangent=tangent;
}
string prop=c<3 ? "m_LocalPosition."+"xyz"[c] : c<7 ? "m_LocalRotation."+"xyzw"[c-3] : "m_LocalScale."+"xyz"[c-7];
AnimationUtility.SetEditorCurve(clip,EditorCurveBinding.FloatCurve(path,typeof(Transform),prop),new AnimationCurve(keys));
}
}
var settings=AnimationUtility.GetAnimationClipSettings(clip);
settings.startTime=0;settings.stopTime=seconds;settings.loopTime=loop;settings.loopBlend=false;
AnimationUtility.SetAnimationClipSettings(clip,settings);
AnimationUtility.SetAnimationEvents(clip,new AnimationEvent[0]);
EditorUtility.SetDirty(clip);
}
public static string Run(string recipePath="Config/Art/EnemyCharacters/RockBall.json")
{
var r=JsonUtility.FromJson<Recipe>(File.ReadAllText(Path.Combine(Repo,recipePath)));
if(string.IsNullOrEmpty(r.attackStyle))r.attackStyle="body_slam";
if(string.IsNullOrEmpty(r.attackEvent))r.attackEvent=r.usage=="companion_visual" ? "OnCompanionAttackImpact" : "OnEnemyAttackImpact";
if(r.attackStyle!="body_slam" && r.attackStyle!="ranged_cast")throw new ArgumentException("Unsupported attack style");
if(r.height<=0 || !(0<r.attackWindup && r.attackWindup<r.attackImpact && r.attackImpact<r.attackRecover && r.attackRecover<1))
throw new ArgumentException("Invalid dimensions / attack phase order");
var sb=new StringBuilder();
foreach(string name in new[] {"Idle","Attack"})
{
var model=Object.Instantiate(AssetDatabase.LoadAssetAtPath<GameObject>(r.assetRoot+"/Rigged/"+r.id+"_Rigged.fbx"));
try
{
var anim=model.GetComponent<Animator>();if(anim!=null) anim.enabled=false;
var bones=model.GetComponentsInChildren<Transform>().Where(t=>t!=model.transform && (t.name=="Armature" || t.name.StartsWith("bone_"))).ToArray();
var root=bones.First(t=>t.name==r.rootBone);
var chest=bones.FirstOrDefault(t=>t.name==r.chestBone);
var arms=new[] {bones.FirstOrDefault(t=>t.name==r.rightArm),bones.FirstOrDefault(t=>t.name==r.leftArm)};
if(r.profile=="biped_short" && (chest==null || arms.Any(t=>t==null))) throw new Exception("Missing profile bones");
var bind=Pose.Read(bones);var bindRootPosition=root.position;float bindBottom=Bottom(model);
var skins=model.GetComponentsInChildren<SkinnedMeshRenderer>();
float unitScale=(skins.SelectMany(Vertices).Max(v=>v.y)-bindBottom)/r.height;
var source=AssetDatabase.LoadAllAssetsAtPath(r.assetRoot+"/Anims/"+r.id+"_"+name+".fbx").OfType<AnimationClip>().First(c=>!c.name.StartsWith("__preview__"));
source.SampleAnimation(model,0);var reference=Pose.Read(bones);
bool idle=name=="Idle";float seconds=idle ? r.idleSeconds : r.attackSeconds;
int count=Mathf.RoundToInt(seconds*30)+1;var poses=new Pose[count];
for(int f=0;f<count;f++)
{
float t=(float)f/(count-1);float idleWeight=r.idleSourceWeight>0 ? r.idleSourceWeight : .2f;
float attackWeight=r.attackSourceWeight>0 ? r.attackSourceWeight : .35f;
float weight=Mathf.Pow(Mathf.Sin(Mathf.PI*t),2)*(idle ? idleWeight : attackWeight);
bind.Write(bones);source.SampleAnimation(model,t*source.length);var raw=Pose.Read(bones);
for(int b=0;b<bones.Length;b++)
{
bones[b].localPosition=bind.p[b]+(raw.p[b]-reference.p[b])*weight;
bones[b].localRotation=Quaternion.Slerp(bind.q[b],bind.q[b]*Quaternion.Inverse(reference.q[b])*raw.q[b],weight);
bones[b].localScale=bind.s[b];
}
root.position=new Vector3(bindRootPosition.x,root.position.y,bindRootPosition.z);
if(r.profile=="biped_short")
{
if(idle)
{
float breath=Mathf.Pow(Mathf.Sin(Mathf.PI*t),2);
chest.localScale=Vector3.Scale(chest.localScale,new Vector3(1+.007f*breath,1+r.idleBreath/r.height*breath,1+.007f*breath));
}
else
{
float strike=Strike(r,t);
root.rotation=Quaternion.AngleAxis(-r.attackLeanDegrees*strike,Vector3.right)*root.rotation;
root.position+=Vector3.back*(r.attackLunge*strike*unitScale);
foreach(var arm in arms)
{
float armSign=r.attackStyle=="ranged_cast" ? 1 : -1;
arm.rotation=Quaternion.AngleAxis(armSign*r.attackArmDegrees*strike,Vector3.right)*arm.rotation;
// A ranged cast presents both paws in front, without moving the actor.
// Small offline bone translation supplies reach for very short chibi arms.
if(r.attackStyle=="ranged_cast")arm.position+=Vector3.back*(r.attackReach*strike*unitScale);
}
}
}
root.position+=Vector3.up*(bindBottom-Bottom(model));
poses[f]=Pose.Read(bones);
}
poses[0]=bind;poses[count-1]=bind;
var clip=AssetDatabase.LoadAssetAtPath<AnimationClip>(r.assetRoot+"/Clips/"+name+".anim");
Save(clip,model.transform,bones,poses,seconds,idle);
if(!idle)
{
AnimationUtility.SetAnimationEvents(clip,new[] {new AnimationEvent {
time=r.attackImpact*seconds,functionName=r.attackEvent,messageOptions=SendMessageOptions.DontRequireReceiver
}});
}
sb.AppendLine($"{name}: {seconds:F3}s loop={idle} start/end=bind generated source + offline {r.profile}/{r.attackStyle} authoring");
}
finally {Object.DestroyImmediate(model);}
}
AssetDatabase.SaveAssets();
string report=Path.Combine(Repo,r.outputDir,"motion_processing.txt");Directory.CreateDirectory(Path.GetDirectoryName(report));File.WriteAllText(report,sb.ToString());
return sb.ToString();
}
}
#endif