原文鏈接:https://www.cnblogs.com/jingjiangtao/p/16666514.html
目的
Unity的AnimationClip.SetCurve()只在Editor中運行時有用,打包后運行時只對legacy的AnimationClip有用,對其它型別的影片Generic和Humanoid都不起作用,https://docs.unity3d.com/ScriptReference/AnimationClip.SetCurve.html,
所以如果想在運行時加載和播放影片,只能用自定義格式,
注意:此自定義格式的生成、加載和播放,都不涉及重定向,通過特定的模型生成的影片,只能在這個模型上播放,
注意:本文只實作了人體模型的自定義影片,其它模型的實作思路相同,
思路
自定義影片格式的生成和播放有三個步驟:
- 生成:把已經存在的影片片段轉換成自定義影片格式
- 序列化:保存和加載自定義影片格式
- 播放:播放自定義影片格式
大致程序就是,以一定的幀率記錄模型每個物體的position和rotation值,保存在自定義格式中,這種格式要能序列化,之后加載自定義格式,并以一定的幀率每幀設定物體的position和rotation,達到播放的效果,
第三方庫
MessagePack
https://github.com/neuecc/MessagePack-CSharp
MessagePack是一種資料交換格式,可以生成體積更小的序列化檔案,而且序列化和反序列化的速度更快,影片檔案資料量比較大,生成的檔案也比較大,所以最好選擇生成檔案體積更小的序列化方案,
實作
物體類
首先要定義物體類來保存影片資料,物體類的嵌套結構如下:
AnimDataSequence { id: int, length: float, frameRate: float, name: string, animPathSequences: List<AnimPathSequence> [ { path: string, localPosition: CurveVector3 { x: AnimationCurve, y: AnimationCurve, z: AnimationCurve }, localRotation: CurveQuaternion { x: AnimationCurve, y: AnimationCurve, z: AnimationCurve, w: AnimationCurve } }, ... ] }
最外層是AnimDataSequence類,id欄位保存影片id;length保存影片時長,單位秒;frameRate保存影片幀率;name保存影片名稱,animPathSequences是一個陣列,陣列的元素型別是AnimPathSequence,保存著模型中單個子物體位置和旋轉值的影片曲線,path表示這個子物體相對模型根節點的路徑,所以List<AnimationPathSequence>保存了模型的所有物體位置和旋轉值的影片曲線,
圖示:

每個物體類的代碼如下:
AnimDataSequence.cs
[MessagePackObject] public class AnimDataSequence { [Key(0)] public int id; [Key(1)] public float length; [Key(2)] public float frameRate = 30f; [Key(3)] public string name; [Key(4)] public List<AnimPathSequence> animPathSequences = new List<AnimPathSequence>(); public List<AnimSequenceFrame> GetFrame(float time) { List<AnimSequenceFrame> sequenceFrames = new List<AnimSequenceFrame>(animPathSequences.Count); foreach (AnimPathSequence sequence in animPathSequences) { AnimSequenceFrame frame = new AnimSequenceFrame(); frame.path = sequence.path; frame.localPosition = new Vector3( sequence.localPosition.x.Evaluate(time), sequence.localPosition.y.Evaluate(time), sequence.localPosition.z.Evaluate(time)); frame.localRotation = new Quaternion( sequence.localRotation.x.Evaluate(time), sequence.localRotation.y.Evaluate(time), sequence.localRotation.z.Evaluate(time), sequence.localRotation.w.Evaluate(time)); sequenceFrames.Add(frame); } return sequenceFrames; } }
GetFrame()函式獲取給定時間位置的一幀資料,回傳一個元素型別是AnimSequenceFrame的陣列,
[Serializable] public class AnimSequenceFrame { public string path; public Vector3 localPosition; public Quaternion localRotation; }
AnimSequenceFrame中,path表示這個子物體相對于模型根節點的路徑,localPosition和localRotation表示這個物體在這一幀的位置和旋轉值,
AnimPathSequence.cs
[MessagePackObject] public class AnimPathSequence { [Key(0)] public string path; [Key(1)] public CurveVector3 localPosition = new CurveVector3(); [Key(2)] public CurveQuaternion localRotation = new CurveQuaternion(); }
CurveQuaternion.cs
[MessagePackObject] public class CurveQuaternion { [Key(0)] public AnimationCurve x; [Key(1)] public AnimationCurve y; [Key(2)] public AnimationCurve z; [Key(3)] public AnimationCurve w; }
CurveVector3.cs
[MessagePackObject] public class CurveVector3 { [Key(0)] public AnimationCurve x; [Key(1)] public AnimationCurve y; [Key(2)] public AnimationCurve z; }
AnimationCurve是Unity自有的型別,表示影片曲線,可以被MessagePack序列化和反序列化,其實,自定義的影片資料,最終都是保存在AnimationCurve中的,
生成
用Unity的Playable API獲取影片指定時間的骨骼資料,保存到物體類物件中,最后保存成檔案,
public void SaveAnimSequence(AnimationClip clip, Animator animator, string filePath, float frameRate = 0) { // 創建PlayableGraph,用于播放影片 PlayableGraph playableGraph = PlayableGraph.Create("ConvertHumanoidAnimation"); playableGraph.SetTimeUpdateMode(DirectorUpdateMode.Manual); // 把影片片段連接到PlayableGraph中 AnimationPlayableOutput animPlayableOutput = AnimationPlayableOutput.Create(playableGraph, "AnimationOutput", animator); AnimationClipPlayable animClipPlayable = AnimationClipPlayable.Create(playableGraph, clip); animPlayableOutput.SetSourcePlayable(animClipPlayable); // 創建自定義影片物件,設定必要的引數 AnimDataSequence animSequence = new AnimDataSequence(); animSequence.length = clip.length; animSequence.name = clip.name; // 如果沒有傳幀率,就用原始影片片段的引數 animSequence.frameRate = frameRate == 0 ? clip.frameRate : frameRate; // 計算每幀的持續時間 float frameDuration = 1f / animSequence.frameRate; Transform root = animator.transform; List<Transform> bones = new List<Transform>(); // 獲取人體骨骼對應的物體Transform陣列 for (int i = 0; i < (int)HumanBodyBones.LastBone; i++) { Transform boneTransform = animator.GetBoneTransform((HumanBodyBones)i); if (boneTransform != null) { bones.Add(boneTransform); AnimPathSequence pathSequence = new AnimPathSequence(); animSequence.animPathSequences.Add(pathSequence); // 獲取相對路徑 pathSequence.path = Utils.RelativePath(root, boneTransform); pathSequence.localPosition.x = new AnimationCurve(); pathSequence.localPosition.y = new AnimationCurve(); pathSequence.localPosition.z = new AnimationCurve(); pathSequence.localRotation.x = new AnimationCurve(); pathSequence.localRotation.y = new AnimationCurve(); pathSequence.localRotation.z = new AnimationCurve(); pathSequence.localRotation.w = new AnimationCurve(); } } // 開始獲取影片對應的人體骨骼資料 float time = 0f; while (time <= clip.length) { // 根據時間點定位影片 animClipPlayable.SetTime(time); playableGraph.Evaluate(); for (int i = 0; i < bones.Count; i++) { Transform bone = bones[i]; AnimPathSequence pathSequence = animSequence.animPathSequences[i]; pathSequence.localPosition.x.AddKey(time, bone.localPosition.x); pathSequence.localPosition.y.AddKey(time, bone.localPosition.y); pathSequence.localPosition.z.AddKey(time, bone.localPosition.z); pathSequence.localRotation.x.AddKey(time, bone.localRotation.x); pathSequence.localRotation.y.AddKey(time, bone.localRotation.y); pathSequence.localRotation.z.AddKey(time, bone.localRotation.z); pathSequence.localRotation.w.AddKey(time, bone.localRotation.w); } // 下一幀的時間 time += frameDuration; } playableGraph.Destroy(); // 用MessagePack序列化并保存到檔案 byte[] bytes = MessagePackSerializer.Serialize(animSequence); File.WriteAllBytes(filePath, bytes); }
簡單起見,代碼最后直接把自定義影片資料保存到了檔案,實際使用可以不保存檔案,序列化后用于網路傳輸,
frameRate引數表示采樣的幀率,值越大,檔案體積越大,影片也越精確,需要取舍,
加載和播放
加載就是用MessagePack庫把MessagePack資料反序列化;播放就是每幀播放指定時間點的影片曲線;
public class PoseSequencePlay : MonoBehaviour { public bool IsPlaying => _isPlaying; public float CurrentTime { get { return _time; } set { if (value > _animDataSequence.length) return; _time = value; SetPose(); } } public bool Loop { get; set; } = false; public AnimDataSequence AnimDataSequence => _animDataSequence; protected Transform _character; protected AnimDataSequence _animDataSequence; protected float _time = 0f; protected bool _isPlaying = false; protected virtual void Update() { if (!_isPlaying) { return; } if (_time > _animDataSequence.length) { if (Loop) { Stop(); Play(); } else { _isPlaying = false; } return; } SetPose(); // 增加時間 _time += Time.deltaTime; } protected void SetPose() { if (_animDataSequence == null || _character == null) return; if (_time > _animDataSequence.length) return; // 獲取指定時間的序列資料 List<AnimSequenceFrame> frame = _animDataSequence.GetFrame(_time); // 將獲取到的資料賦值給對應的骨骼 foreach (AnimSequenceFrame frameOnePath in frame) { Transform boneTransform = _character.Find(frameOnePath.path); if (boneTransform != null) { boneTransform.localPosition = frameOnePath.localPosition; boneTransform.localRotation = frameOnePath.localRotation; } else { Debug.LogWarning($"[GestureGenerator] {frameOnePath.path} is Null"); } } } public virtual void PrepareData(string animDataSequenceFilePath, Transform character) { byte[] bytes = File.ReadAllBytes(animDataSequenceFilePath); AnimDataSequence sequence = MessagePackSerializer.Deserialize<AnimDataSequence>(bytes); PrepareData(sequence, character); } public virtual void PrepareData(byte[] sequenceBytes, Transform character) { AnimDataSequence sequence = MessagePackSerializer.Deserialize<AnimDataSequence>(sequenceBytes); PrepareData(sequence, character); } public virtual async Task PrepareDataAsync(string animDataSequenceFilePath, Transform character) { AnimDataSequence sequence = null; using (FileStream fileStream = File.OpenRead(animDataSequenceFilePath)) { sequence = await MessagePackSerializer.DeserializeAsync<AnimDataSequence>(fileStream); } PrepareData(sequence, character); } public virtual void PrepareData(AnimDataSequence sequence, Transform character) { _isPlaying = false; _time = 0f; _animDataSequence = sequence; _character = character; } public virtual void Play() { _isPlaying = true; } public virtual void Pause() { _isPlaying = false; } public virtual void Stop() { _isPlaying = false; _time = 0f; SetPose(); } public virtual void Clear() { Stop(); _animDataSequence = null; _character = null; } }
PoseSequencePlay是一個繼承了MonoBehaviour的類,需要掛載到Unity場景中運行,
加載的核心函式是PrepareData(),通過MessagePackSerializer.Deserialize()反序列化自定義影片資料,
播放的核心函式有兩個:Update()和SetPose(),SetPose()函式根據當前時間將人物模型設定為自定義影片序列中的姿勢,Update()函式每幀設定影片姿勢,并遞增時間,
如有錯誤,歡迎指正,謝謝!轉載請註明出處,本文鏈接:https://www.uj5u.com/net/505448.html
標籤:.NET技术
上一篇:Tkinter-在for回圈中將“row=”添加到標簽時,串列中的專案彼此重疊顯示
下一篇:C# 中針對 System.Runtime.InteropServices.ExternalException:“GDI+ 中發生一般性錯誤。”另一種圖片保存方案
