Playable 动画系统
Playable 基本用法
Playable意思是可播放的,可运行的。Playable整体是树形结构,PlayableGraph相当于一个容器,所有元素都被包含在里面,图中的每个节点都是Playable,叶子节点的Playable包裹原始数据,相当于输入,中间的Mixer根据权重混合多个输入,最后汇总到根部的Output节点,然后由PlayableGraph播放。
Playable的核心类型
Playable的输出类型
这些不同类型的Playable都是结构体,所以它们之间不是继承关系,但是可以隐式转换,如
AnimationClipPlayable clipPlayable = AnimationClipPlayable.Create(playableGraph, clip);
//隐式转换
Playable playable = clipPlayable;
播放单个动画片段
官方示例
[RequireComponent(typeof(Animator))]
public class PlayAnimationSample : MonoBehaviour
{public AnimationClip clip;private PlayableGraph playableGraph;void Start(){//创建PlayableGraphplayableGraph = PlayableGraph.Create();playableGraph.SetTimeUpdateMode(DirectorUpdateMode.GameTime);//创建AnimationClipPlayable包裹AnimationClip,附加到PlayableGraph上var clipPlayable = AnimationClipPlayable.Create(playableGraph, clip);//创建输出节点并把Animator设为目标,Animator会处理PlayableGraphvar playableOutput = AnimationPlayableOutput.Create(playableGraph, "Animation", GetComponent<Animator>());//连接输入源playableOutput.SetSourcePlayable(clipPlayable);// Plays the Graph.playableGraph.Play();}void OnDisable(){playableGraph.Destroy();}
}
这样不使用Animator Controller,通过脚本就可以控制动画播放,而且Animator Controller是不允许运行时添加、删除动画的,使用Playable就可以运行时添加,删除动画。
使用PlayableGraph Visualizer查看Playable结构
创建动画混合树
[RequireComponent(typeof(Animator))]
public class MixAnimationSample : MonoBehaviour
{public AnimationClip clip0;public AnimationClip clip1;public float weight;private PlayableGraph playableGraph;private AnimationMixerPlayable mixerPlayable;private AnimationClipPlayable clipPlayable0;void Start(){playableGraph = PlayableGraph.Create();playableGraph.SetTimeUpdateMode(DirectorUpdateMode.GameTime);var playableOutput = AnimationPlayableOutput.Create(playableGraph, "Animation", GetComponent<Animator>());//创建AnimationMixerPlayable,2表示输入的数量mixerPlayable = AnimationMixerPlayable.Create(playableGraph, 2);playableOutput.SetSourcePlayable(mixerPlayable);clipPlayable0 = AnimationClipPlayable.Create(playableGraph, clip0);var clipPlayable1 = AnimationClipPlayable.Create(playableGraph, clip1);//连接两个Playable,clipPlayable是源头,mixerPlayable是目标//clipPlayable0和clipPlayable1的默认输出端口号是0,分别连接到mixerPlayable输入端口0和1playableGraph.Connect(clipPlayable0, 0, mixerPlayable, 0);playableGraph.Connect(clipPlayable1, 0, mixerPlayable, 1);playableGraph.Play();}void Update(){//保证所有输入源的权重和为1weight = Mathf.Clamp01(weight);mixerPlayable.SetInputWeight(0, 1.0f-weight);mixerPlayable.SetInputWeight(1, weight);//切换输入的状态if (Input.GetKeyDown(KeyCode.Space)){if (clipPlayable0.GetPlayState() == PlayState.Playing){clipPlayable0.Pause();}else{clipPlayable0.Play();clipPlayable0.SetTime(0f);}}}void OnDisable(){playableGraph.Destroy();}
}
调整权重在两个动画之间过渡,我们还可以修改某个输入节点的状态
大型的RPG或FPS游戏,没必要把大量的动画都添加到Graph中,我们可以预先创建好需要的子树,然后根据需要在添加到Graph中
混合AnimationClip和AnimatorController
[RequireComponent(typeof(Animator))]
public class RuntimeControllerSample : MonoBehaviour
{public AnimationClip clip;public RuntimeAnimatorController controller;public float weight;private PlayableGraph playableGraph;private AnimationMixerPlayable mixerPlayable;void Start(){playableGraph = PlayableGraph.Create();var playableOutput = AnimationPlayableOutput.Create(playableGraph, "Animation", GetComponent<Animator>());mixerPlayable = AnimationMixerPlayable.Create(playableGraph, 2);playableOutput.SetSourcePlayable(mixerPlayable);var clipPlayable = AnimationClipPlayable.Create(playableGraph, clip);var ctrlPlayable = AnimatorControllerPlayable.Create(playableGraph, controller);playableGraph.Connect(clipPlayable, 0, mixerPlayable, 0);playableGraph.Connect(ctrlPlayable, 0, mixerPlayable, 1);playableGraph.Play();}void Update(){weight = Mathf.Clamp01(weight);mixerPlayable.SetInputWeight(0, 1.0f-weight);mixerPlayable.SetInputWeight(1, weight);}void OnDisable(){playableGraph.Destroy();}
}
AnimationClipPlayable包裹AnimationClip,而AnimationrControllerPlayable则包裹RuntimeAnimationrController
每个角色都有的动画如走,跑,跳用Animator管理,角色的特殊动画用Playable和Animator融合
多个输出
[RequireComponent(typeof(Animator))]
[RequireComponent(typeof(AudioSource))]
public class MultiOutputSample : MonoBehaviour
{public AnimationClip animationClip;public AudioClip audioClip;private PlayableGraph playableGraph;void Start(){playableGraph = PlayableGraph.Create();var animationOutput = AnimationPlayableOutput.Create(playableGraph, "Animation", GetComponent<Animator>());var audioOutput = AudioPlayableOutput.Create(playableGraph, "Audio", GetComponent<AudioSource>());var animationClipPlayable = AnimationClipPlayable.Create(playableGraph, animationClip);var audioClipPlayable = AudioClipPlayable.Create(playableGraph, audioClip, true);animationOutput.SetSourcePlayable(animationClipPlayable);audioOutput.SetSourcePlayable(audioClipPlayable);playableGraph.Play();}void OnDisable(){playableGraph.Destroy();}
}
两个输出对象分别是Animator和AudioSource
自定义PlayableBehaviour实现动画队列
PlayableBehaviour 是一个用于实现自定义 Playable 的基类,它可以让开发者通过继承该类来自定义 Playable 行为,可以用于在播放过程中控制动画的逻辑
public class PlayQueuePlayable : PlayableBehaviour
{private int m_CurrentClipIndex = -1;private float m_TimeToNextClip;private Playable mixer;public void Initialize(AnimationClip[] clipsToPlay, Playable owner, PlayableGraph graph){owner.SetInputCount(1);mixer = AnimationMixerPlayable.Create(graph, clipsToPlay.Length);graph.Connect(mixer, 0, owner, 0);owner.SetInputWeight(0, 1);for (int clipIndex = 0; clipIndex < mixer.GetInputCount(); ++clipIndex){graph.Connect(AnimationClipPlayable.Create(graph, clipsToPlay[clipIndex]), 0, mixer, clipIndex);mixer.SetInputWeight(clipIndex, 1.0f);}}/// <summary>/// 每帧调用/// </summary>public override void PrepareFrame(Playable owner, FrameData info){if (mixer.GetInputCount() == 0)return;m_TimeToNextClip -= (float)info.deltaTime;if (m_TimeToNextClip <= 0.0f){m_CurrentClipIndex++;if (m_CurrentClipIndex >= mixer.GetInputCount())m_CurrentClipIndex = 0;//切换到下一个动画片段var currentClip = (AnimationClipPlayable)mixer.GetInput(m_CurrentClipIndex);currentClip.SetTime(0);m_TimeToNextClip = currentClip.GetAnimationClip().length;}//当前片段权重设为1,其他为0for (int clipIndex = 0; clipIndex < mixer.GetInputCount(); ++clipIndex){mixer.SetInputWeight(clipIndex, clipIndex == m_CurrentClipIndex ? 1.0f : 0.0f);}}
}[RequireComponent(typeof (Animator))]
public class PlayQueueSample : MonoBehaviour
{public AnimationClip[] clipsToPlay;private PlayableGraph playableGraph;void Start(){playableGraph = PlayableGraph.Create();var playQueuePlayable = ScriptPlayable<PlayQueuePlayable>.Create(playableGraph);var playQueue = playQueuePlayable.GetBehaviour();playQueue.Initialize(clipsToPlay, playQueuePlayable, playableGraph);var playableOutput = AnimationPlayableOutput.Create(playableGraph, "Animation", GetComponent<Animator>());playableOutput.SetSourcePlayable(playQueuePlayable);playableOutput.SetSourceInputPort(0);playableGraph.Play();}void OnDisable(){playableGraph.Destroy();}
}
ScriptPlayable< T>.Create 是一个静态方法,用于创建一个ScriptPlayable< T>实例,并添加到PlayableGraph中。ScriptPlayable< T> 是一个结构体,用于创建自定义的 Playable 行为,其中T需要继承 PlayableBehaviour 。ScriptPlayable 结构体还提供了一些静态方法,用于创建和管理可播放对象。
ScriptPlayable< T>实例实际上是将泛型 T 包装在一个结构体中。这个结构体提供了一些方法,使得 T 类型能够被 PlayableGraph 所使用。
随机切换动画
实现从一个默认动画随机切换到另一个动画,这两个动画之间需要做融合,且播放完动画后切会默认动画,大致的流程如下
RandomSelector是一个随机选择器,Mixer是一个混合器,通过调整权重来实现切换
为了方便管理动画,把每个动画片段包裹到AnimUnit,管理动画状态,并输出信息
使用适配器实现多态,AnimAdapter里面有一个AnimBehaviour的引用,适配器本身没有功能,它的具体功能取决于引用AnimBehaviour的哪一个子类
/// <summary>
/// 适配器
/// </summary>
public class AnimAdapter : PlayableBehaviour
{private AnimBehaviour _behaviour;public void Init(AnimBehaviour behaviour){_behaviour = behaviour;}public void Enable(){_behaviour?.Enable();}public void Disable(){_behaviour?.Disable();}public override void PrepareFrame(Playable playable, FrameData info){_behaviour?.Execute(playable, info);}public float GetEnterTime(){return _behaviour.GetEnterTime();}public override void OnGraphStop(Playable playable){base.OnGraphStop(playable);_behaviour?.Stop();}
}
/// <summary>
/// 组件基类
/// </summary>
public abstract class AnimBehaviour
{public bool enable { get; protected set; }public float remainTime { get; protected set; }//记录这个AnimBehaviour属于那个AnimAdapterprotected Playable _adapterPlayable;protected float _enterTime;protected float _clipLength;public AnimBehaviour(){}public AnimBehaviour(PlayableGraph graph, float enterTime = 0){_adapterPlayable = ScriptPlayable<AnimAdapter>.Create(graph);((ScriptPlayable<AnimAdapter>)_adapterPlayable).GetBehaviour().Init(this);_enterTime = enterTime;_clipLength = float.NaN;}public virtual void Enable(){enable = true;remainTime = GetClipLength();}public virtual void Disable(){enable = false;}public virtual void Execute(Playable playable, FrameData info){if (!enable)return;remainTime = remainTime > 0 ? remainTime - info.deltaTime : 0;}public virtual void Stop(){}public Playable GetAnimAdapterPlayable(){return _adapterPlayable;}public virtual void AddInput(Playable playable){}public void AddInput(AnimBehaviour behaviour){AddInput(behaviour.GetAnimAdapterPlayable());}public virtual float GetEnterTime(){return _enterTime;}public virtual float GetClipLength(){return _clipLength;}
}
/// <summary>
/// 输出的子节点,作为一个空节点,隔开输出和实际的输入
/// Enable就启用所有子节点,Disable就禁用所有子节点
/// </summary>
public class Root : AnimBehaviour
{public Root(PlayableGraph graph) : base(graph){}public override void AddInput(Playable playable){_adapterPlayable.AddInput(playable, 0, 1);}public override void Enable(){base.Enable();for (int i = 0; i < _adapterPlayable.GetInputCount(); ++i){AnimHelper.Enable(_adapterPlayable.GetInput(i));}_adapterPlayable.SetTime(0f);_adapterPlayable.Play();}public override void Disable(){base.Disable();for (int i = 0; i < _adapterPlayable.GetInputCount(); ++i){AnimHelper.Disable(_adapterPlayable.GetInput(i));}_adapterPlayable.Pause();}
}
public class AnimHelper
{public static void Enable(Playable playable){var adapter = GetAdapter(playable);if (adapter != null){adapter.Enable();}}public static void Enable(AnimationMixerPlayable mixer, int index){Enable(mixer.GetInput(index));}public static void Disable(Playable playable){var adapter = GetAdapter(playable);if (adapter != null){adapter.Disable();}}public static void Disable(AnimationMixerPlayable mixer, int index){Disable(mixer.GetInput(index));}public static AnimAdapter GetAdapter(Playable playable){//检查playbble类型是否继承AnimAdapterif (typeof(AnimAdapter).IsAssignableFrom(playable.GetPlayableType())){return ((ScriptPlayable<AnimAdapter>)playable).GetBehaviour();}return null;}public static void SetOutput(PlayableGraph graph, Animator animator, AnimBehaviour behaviour){Root root = new Root(graph);root.AddInput(behaviour);var output = AnimationPlayableOutput.Create(graph, "Anim", animator);output.SetSourcePlayable(root.GetAnimAdapterPlayable());}public static void Start(PlayableGraph graph, AnimBehaviour behaviour){graph.Play();behaviour.Enable();}public static void Start(PlayableGraph graph){graph.Play();//获取output的子节点,即root节点GetAdapter(graph.GetOutputByType<AnimationPlayableOutput>(0).GetSourcePlayable()).Enable();}public static ComputeShader LoadCompute(string name){ComputeShader computeShader = Resources.Load<ComputeShader>("Compute/" + name);//拷贝一份实例,不然多个对象公用一个shader数据会冲突return Object.Instantiate(computeShader);}
}
AnimUnit 组件
/// <summary>
/// 包裹AnimationClipPlayable
/// </summary>
public class AnimUnit : AnimBehaviour
{private AnimationClipPlayable _clipPlayable;public AnimUnit(PlayableGraph graph, AnimationClip clip, float enterTime = 0) : base(graph, enterTime){_clipPlayable = AnimationClipPlayable.Create(graph, clip);_adapterPlayable.AddInput(_clipPlayable, 0, 1f);_clipLength = clip.length;Disable();}public override void Enable(){base.Enable();_adapterPlayable.SetTime(0);_clipPlayable.SetTime(0);_adapterPlayable.Play();_clipPlayable.Play();}public override void Disable(){base.Disable();_adapterPlayable.Pause();_clipPlayable.Pause();}
}
随机动画选择器组件 RandomSelector
/// <summary>
/// 动画选择器基类
/// </summary>
public class AnimSelector : AnimBehaviour
{public int currentIndex { get; protected set; }public int clipCount { get; protected set; }private AnimationMixerPlayable _mixer;private List<float> _enterTimes;private List<float> _clipLengths;public AnimSelector(PlayableGraph graph) : base(graph){_mixer = AnimationMixerPlayable.Create(graph);_adapterPlayable.AddInput(_mixer, 0, 1f);currentIndex = -1;_enterTimes = new List<float>();_clipLengths = new List<float>();}public override void AddInput(Playable playable){_mixer.AddInput(playable, 0);clipCount++;}public void AddInput(AnimationClip clip, float enterTime){AddInput(new AnimUnit(_adapterPlayable.GetGraph(), clip, enterTime));_enterTimes.Add(enterTime);_clipLengths.Add(clip.length);}public override void Enable(){base.Enable();if (currentIndex < 0 || currentIndex >= clipCount)return;_mixer.SetInputWeight(currentIndex, 1f);AnimHelper.Enable(_mixer, currentIndex);_adapterPlayable.SetTime(0);_adapterPlayable.Play();_mixer.SetTime(0);_mixer.Play();}public override void Disable(){base.Disable();if (currentIndex < 0 || currentIndex >= clipCount)return;_mixer.SetInputWeight(currentIndex, 0f);AnimHelper.Disable(_mixer, currentIndex);_adapterPlayable.Pause();_mixer.Pause();currentIndex = -1;}/// <summary>/// 根据条件,选择一个动画/// </summary>public virtual int Select(){return currentIndex;}/// <summary>/// 直接指定索引/// </summary>public void Select(int index){currentIndex = index;}public override float GetEnterTime(){if(currentIndex >= 0 && currentIndex < _enterTimes.Count)return _enterTimes[currentIndex];return 0;}public override float GetClipLength(){if(currentIndex >= 0 && currentIndex < _clipLengths.Count)return _clipLengths[currentIndex];return 0;}
}
/// <summary>
/// 随机动画选择器
/// </summary>
public class RandomSelector : AnimSelector
{public RandomSelector(PlayableGraph graph) : base(graph){}public override int Select(){currentIndex = Random.Range(0, clipCount);return currentIndex;}
}
1D混合树组件 Mixer
简单的动画混合,根据切换的时间计算速度Speed,当前动画权重递减,目标动画权重递增
速度 * 时间 = 权重
速度 = 权重 / 时间
cur动画切换到tar(绿)动画,被tar(红)打断,此时如果cur的权重 > tar(绿)的权重,tar(绿)的权重要按照2倍的速度递减,tar(红)的权重 = 1 - cur权重 - tar(绿)权重
如果频繁打断动画,就可能有多个动画的权重需要递减到0,此时需要一个数组(del)保存被打断的动画
tar(黄)权重 = 1 - cur权重 - del数组内所有动画权重
切换打断时,如果cur(蓝)权重 < tar(绿)权重,就交换cur和tar
public class Mixer : AnimBehaviour
{public int inputCount { get; private set; }public int currentIndex => _currentIndex;public bool IsTransition => _isTransition;private AnimationMixerPlayable _mixerPlayable;//当前动画索引private int _currentIndex;//目标动画索引private int _targetIndex;//递减列表private List<int> _declineList;private float _timeToNext;//当前权重的递减速度private float _currentSpeed;//递减列表中权重的递减速度private float _declineSpeed;//是否在切换中private bool _isTransition;public Mixer(PlayableGraph graph) : base(graph){_mixerPlayable = AnimationMixerPlayable.Create(graph, 0, true);//连接到adapter上_adapterPlayable.AddInput(_mixerPlayable, 0, 1);_targetIndex = -1;_declineList = new List<int>();}public override void AddInput(Playable playable){base.AddInput(playable);_mixerPlayable.AddInput(playable, 0, 0f);inputCount++;if(inputCount == 1){_mixerPlayable.SetInputWeight(0, 1f);_currentIndex = 0;}}public override void Enable(){base.Enable();if (inputCount > 0){AnimHelper.Enable(_mixerPlayable, 0);}_adapterPlayable.SetTime(0);_mixerPlayable.SetTime(0);_adapterPlayable.Play();_mixerPlayable.Play();_mixerPlayable.SetInputWeight(0, 1f);_currentIndex = 0;_targetIndex = -1;}public override void Disable(){base.Disable();_adapterPlayable.Pause();_mixerPlayable.Pause();for (int i = 0; i < inputCount; ++i){_mixerPlayable.SetInputWeight(i, 0);AnimHelper.Disable(_mixerPlayable, i);}}public override void Execute(Playable playable, FrameData info){base.Execute(playable, info);if (!enable || !_isTransition || _targetIndex < 0)return;if (_timeToNext > 0f){_timeToNext -= info.deltaTime;//所有递减动画的权重之和float declineWeight = 0;for (int i = 0; i < _declineList.Count; ++i){float w = ModifyWeight(_declineList[i], -info.deltaTime * _declineSpeed);if (w <= 0f){AnimHelper.Disable(_mixerPlayable, _declineList[i]);_declineList.Remove(_declineList[i]);}else{declineWeight += w;}}float curWeight = ModifyWeight(_currentIndex, -info.deltaTime * _currentSpeed);SetWeight(_targetIndex, 1 - declineWeight - curWeight);return;}//切换完成后_isTransition = false;AnimHelper.Disable(_mixerPlayable, _currentIndex);_currentIndex = _targetIndex;_targetIndex = -1;}/// <summary>/// 切换动画/// </summary>public void TransitionTo(int index){if (_isTransition && _targetIndex >= 0){//切换中if (index == _targetIndex)return;if (index == _currentIndex){_currentIndex = _targetIndex;}else if (GetWeight(_currentIndex) > GetWeight(_targetIndex)){//被打断时,当前权重大于目标权重_declineList.Add(_targetIndex);}else{//被打断时,当前权重小于目标权重,交换_declineList.Add(_currentIndex);_currentIndex = _targetIndex;}}else{if (index == _currentIndex) return;}_targetIndex = index;//传入的targetIndex有可能已在列表里面,需要移除_declineList.Remove(_targetIndex);AnimHelper.Enable(_mixerPlayable, _targetIndex);// _timeToNext = GetTargetEnterTime(_targetIndex);_timeToNext = GetTargetEnterTime(_targetIndex) * (1f - GetWeight(_targetIndex));_currentSpeed = GetWeight(_currentIndex) / _timeToNext;_declineSpeed = 2f / _timeToNext;_isTransition = true;}public float GetWeight(int index){return index >= 0 && index < inputCount ? _mixerPlayable.GetInputWeight(index) : 0;}public void SetWeight(int index, float weight){if (index >= 0 && index < inputCount){_mixerPlayable.SetInputWeight(index, weight);}}/// <summary>/// 获取切换时间/// </summary>private float GetTargetEnterTime(int index){return ((ScriptPlayable<AnimAdapter>)_mixerPlayable.GetInput(index)).GetBehaviour().GetEnterTime();}/// <summary>/// 调整权重/// </summary>private float ModifyWeight(int index, float delta){if (index < 0 || index >= inputCount)return 0;float weight = Mathf.Clamp01(GetWeight(index) + delta);_mixerPlayable.SetInputWeight(index, weight);return weight;}
}
测试脚本
public class RandomSelectorExample : MonoBehaviour
{public bool isTransition;public float remainTime;public AnimationClip[] clips;private PlayableGraph _graph;private Mixer _mixer;private RandomSelector _randomSelector;void Start(){_graph = PlayableGraph.Create();var idle = new AnimUnit(_graph, clips[0], 0.5f);_randomSelector = new RandomSelector(_graph);for(int i = 1; i < clips.Length; i++){_randomSelector.AddInput(clips[i], 0.5f);}_mixer = new Mixer(_graph);_mixer.AddInput(idle);_mixer.AddInput(_randomSelector);_randomSelector.Select();AnimHelper.SetOutput(_graph, GetComponent<Animator>(), _mixer);AnimHelper.Start(_graph);}void Update(){if (Input.GetKeyDown(KeyCode.Space)){_randomSelector.Select();_mixer.TransitionTo(1);}isTransition = _mixer.IsTransition;remainTime = _randomSelector.remainTime;if (!_mixer.IsTransition && _randomSelector.remainTime < 0.5f && _mixer.currentIndex != 0){_mixer.TransitionTo(0);}}void OnDestroy(){_graph.Destroy();}
}
运行时按空格键随机选择一个动画播放,播放完切换到idle
2D混合树组件 BlendTree2D
使用 Compute Shader 把计算移到 GPU 上,在 Resources 目录下新建 Compute Shader “BlendTree2D”
// Each #kernel tells which function to compile; you can have many kernels
// 定义主函数名称
#pragma kernel Computestruct DataPair
{float x;float y;float weight;
};float pointerX;
float pointerY;
//很小的数,防止除以0
float eps;
//定义结构化缓存
RWStructuredBuffer<DataPair> dataBuffer;float mdistance(DataPair data)
{return abs(pointerX - data.x) + abs(pointerY - data.y) + eps;
}//声明XYZ三个维度线程组中的线程数量
[numthreads(16,1,1)]
void Compute (uint3 id : SV_DispatchThreadID)
{dataBuffer[id.x].weight = 1 / mdistance(dataBuffer[id.x]);
}
[Serializable]
public struct BlendClip2D
{public AnimationClip clip;public Vector2 pos;
}public class BlendTree2D : AnimBehaviour
{private struct DataPair{public float x;public float y;public float weight;}private AnimationMixerPlayable _mixer;private DataPair[] _dataPairs;//把权重的计算移到GPU上private ComputeShader _computeShader;//传递数据private ComputeBuffer _computeBuffer;//shader中定义的计算主函数private int _kernel;private int _clipCount;private Vector2 _lastPointer;private int _pointerX;private int _pointerY;private float _total;public BlendTree2D(PlayableGraph graph, BlendClip2D[] clips, float enterTime = 0f, float eps = 1e-5f) : base(graph, enterTime){_mixer = AnimationMixerPlayable.Create(graph);_dataPairs = new DataPair[clips.Length];_adapterPlayable.AddInput(_mixer, 0, 1f);for (int i = 0; i < clips.Length; i++){var clip = clips[i].clip;var clipPlayable = AnimationClipPlayable.Create(graph, clip);_mixer.AddInput(clipPlayable, 0);_dataPairs[i].x = clips[i].pos.x;_dataPairs[i].y = clips[i].pos.y;}_computeShader = AnimHelper.LoadCompute("BlendTree2D");//stride需要设置为4的倍数_computeBuffer = new ComputeBuffer(_dataPairs.Length, 12);_kernel = _computeShader.FindKernel("Compute");_computeShader.SetBuffer(_kernel, "dataBuffer", _computeBuffer);_computeShader.SetFloat("eps", eps);_pointerX = Shader.PropertyToID("pointerX");_pointerY = Shader.PropertyToID("pointerY");_clipCount = clips.Length;_lastPointer.Set(1, 1);SetPointer(0,0);}public override void Enable(){base.Enable();_adapterPlayable.SetTime(0);_adapterPlayable.Play();_mixer.SetTime(0);_mixer.Play();for (int i = 0; i < _clipCount; i++){_mixer.GetInput(i).SetTime(0);_mixer.GetInput(i).Play();}//初始化权重SetPointer(0, 0);}public override void Disable(){base.Disable();_adapterPlayable.Pause();_mixer.Pause();for (int i = 0; i < _clipCount; i++){_mixer.GetInput(i).Pause();}}public void SetPointer(Vector2 input){SetPointer(input.x, input.y);}public void SetPointer(float x, float y){if (_lastPointer.x == x && _lastPointer.y == y)return;_lastPointer.Set(x, y);_computeShader.SetFloat(_pointerX, x);_computeShader.SetFloat(_pointerY, y);_computeBuffer.SetData(_dataPairs);//运行计算着色器,以 X、Y 和 Z 维度中的指定计算着色器线程组启动_computeShader.Dispatch(_kernel, _clipCount, 1, 1);_computeBuffer.GetData(_dataPairs);_total = 0;int i;for (i = 0; i < _clipCount; ++i){_total += _dataPairs[i].weight;}for (i = 0; i < _clipCount; ++i){_mixer.SetInputWeight(i, _dataPairs[i].weight / _total);}}public override void Stop(){base.Stop();_computeBuffer.Dispose();}
}
测试脚本
public class BlendTree2DExample : MonoBehaviour
{public Vector2 pointer;public BlendClip2D[] clips;private PlayableGraph _graph;private BlendTree2D _blendTree2D;void Start(){_graph = PlayableGraph.Create();_blendTree2D = new BlendTree2D(_graph, clips);AnimHelper.SetOutput(_graph, GetComponent<Animator>(), _blendTree2D);AnimHelper.Start(_graph);}void Update(){_blendTree2D.SetPointer(pointer);}void OnDestroy(){_graph.Destroy();}
}
运行时修改Pointer就会根据距离在动画片段之间做混合
参考
Playable 动画系统
相关文章:

Playable 动画系统
Playable 基本用法 Playable意思是可播放的,可运行的。Playable整体是树形结构,PlayableGraph相当于一个容器,所有元素都被包含在里面,图中的每个节点都是Playable,叶子节点的Playable包裹原始数据,相当于输…...

深入理解Linux内核--虚拟文件
虚拟文件系统(VFS)的作用 虚拟文件系统(Virtual Filesystem)也可以称之为虚拟文件系统转换(Virtual Filesystem Switch,VFS), 是一个内核软件层, 用来处理与Unix标准文件系统相关的所有系统调用。 其健壮性表现在能为各种文件系统提供一个通用的接口。VFS支持的文件…...
记一次 .NET 某外贸ERP 内存暴涨分析
一:背景 1. 讲故事 上周有位朋友找到我,说他的 API 被多次调用后出现了内存暴涨,让我帮忙看下是怎么回事?看样子是有些担心,但也不是特别担心,那既然找到我,就给他分析一下吧。 二࿱…...

关于安卓打包生成aar,jar实现(一)
关于安卓打包生成aar,jar方式 背景 在开发的过程中,主项目引入三方功能的方式有很多,主要是以下几个方面: (1)直接引入源代码module(优点:方便修改源码,易于维护&#…...
QString字符串与16进制QByteArray的转化,QByteArray16进制数字组合拼接,Qt16进制与10进制的转化
文章目录 QString转16进制QByteArry16进制QByteArray转QStringQByteArray16进制数拼接Qt16进制与10进制的转化在串口通信中,常常使用QByetArray储存数据,QByteArray可以看成字节数组,每个索引位置储存一个字节也就是8位的数据,可以储存两位16进制数,可以用uint8取其中的数…...

ElasticSearch安装与启动
ElasticSearch安装与启动 【服务端安装】 1.1、下载ES压缩包 目前ElasticSearch最新的版本是7.6.2(截止2020.4.1),我们选择6.8.1版本,建议使用JDK1.8及以上。 ElasticSearch分为Linux和Window版本,基于我们主要学习…...

JavaWeb中Json传参的条件
JavaWeb中我们常用json进行参数传递 对应的注释为RequestBody 但是json传参是有条件的 最主要是你指定的实体类和对应的json参数能否匹配 1.属性和对应的json参数名称对应 2.对应实体类实现了Serializable接口,可以进行序列化和反序列化,这个才是实体类转…...

包装类+初识泛型
目录 1 .包装类 1.1 基本数据类型对应的包装类 1.2.1装箱 1.2.2拆箱 2.初识泛型 2.1什么是泛型 2.2泛型类 2.3裸类型 2.4泛型的上界 2.5泛型方法 1 .包装类 基本数据类型所对应的类类型 在 Java 中,由于基本类型不是继承自 Object ,为了在泛型…...
基于改进的长短期神经网络电池电容预测,基于DBN+LSTM+SVM的电池电容预测
目录 背影 摘要 LSTM的基本定义 LSTM实现的步骤 基于长短期神经网络LSTM的客电池电容预测 完整代码: 基于长短期神经网络LSTM的公交站客流量预测资源-CSDN文库 https://download.csdn.net/download/abc991835105/88184734 效果图 结果分析 展望 参考论文 背影 为增加电动车行…...
Python 2.x 中如何使用pandas模块进行数据分析
Python 2.x 中如何使用pandas模块进行数据分析 概述: 在数据分析和数据处理过程中,pandas是一个非常强大且常用的Python库。它提供了数据结构和数据分析工具,可以实现快速高效的数据处理和分析。本文将介绍如何在Python 2.x中使用pandas进行数据分析&am…...

获取Spring中bean工具类
获取Spring中bean工具类 工具类 package com.geekmice.springbootselfexercise.utils;import org.springframework.beans.BeansException; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; import org…...
【实战篇】亿级高并发电商项目(新建 ego_pojo、ego_mapper、ego_api、ego_provider、搭建后台项目 )十五
目录 八、 搭建 Provide 1 新建 ego_pojo 2 新建 ego_mapper 2.1编写 pom.xml 2.2新建配置文件 编辑...

【Plex】FRP内网穿透后 App无法使用问题
能搜索到这个文章的,应该都看过这位同学的分析【Plex】FRP内网穿透后 App无法使用问题_plex frp无效_Fu1co的博客-CSDN博客 这个是必要的过程,但是设置之后仍然app端无法访问,原因是因为网络端口的问题 这个里面的这个公开端口,可…...

[管理与领导-11]:IT基层管理者 - 目标与落实 - 过程管理失控,结果总难达成的问题思考:如何把过程管控做得更好?
目录 前言: 第1章 问题与现象 1.1 总有意想不到的事发生:意外事件 1.2 总有计划变更:意外影响 1.3 总有一错再错,没有复盘、总结与反思,没有流程与改进 第2章 背后的原因 2.1 缺乏及时的过程检查 - 缺乏异常检测…...
用php语言写一个chatgpt3.5模型的例子
当然可以!使用PHP语言调用OpenAI API与ChatGPT-3.5模型进行交互。首先,确保你已经安装了PHP 7.2或更新版本,并具备可用的OpenAI API密钥。 下面是一个基本的PHP示例,展示了如何使用OpenAI API与ChatGPT-3.5模型进行对话ÿ…...

PHP实现保质期计算器
1.php实现保质期计算, 保质期日期可选,天 、月、年 2. laravel示例 /*** 保质期计算器* return void*/public function expirationDateCal(){$produce_date $this->request(produce_date); // 生产日期$warranty_date $this->reques…...

【独立版】新零售社区团购电商系统生鲜水果商城兴盛优选十荟团源码
【独立版】新零售社区团购电商系统生鲜水果商城兴盛优选十荟团源码...
C++系列十:其他-1. Lua
系列文章目录 Lua 系列文章目录前言Lua介绍:参考链接: 基本语法:函数、迭代器table、userdata、模块元素、元方法:协程、文件读写面向对象、垃圾回收 前言 我写这个博客的一个问题?(●’◡’●) 居然是 取名太难了。 …...

不知道打仗之害,就不知道打仗之利
不知道打仗之害,就不知道打仗之利 【安志强趣讲《孙子兵法》第7讲】 【原文】 夫钝兵挫锐,屈力殚货,则诸侯乘其弊而起,虽有智者,不能善其后矣。 【注释】 屈力殚货:屈力,指力量消耗,…...
【leetcode】242. 有效的字母异位词(easy)
给定两个字符串 s 和 t ,编写一个函数来判断 t 是否是 s 的字母异位词。 注意:若 s 和 t 中每个字符出现的次数都相同,则称 s 和 t 互为字母异位词。 思路: 先比较两字符串长度是否相同,如果不同直接返回false。创建…...

深入浅出Asp.Net Core MVC应用开发系列-AspNetCore中的日志记录
ASP.NET Core 是一个跨平台的开源框架,用于在 Windows、macOS 或 Linux 上生成基于云的新式 Web 应用。 ASP.NET Core 中的日志记录 .NET 通过 ILogger API 支持高性能结构化日志记录,以帮助监视应用程序行为和诊断问题。 可以通过配置不同的记录提供程…...

微软PowerBI考试 PL300-选择 Power BI 模型框架【附练习数据】
微软PowerBI考试 PL300-选择 Power BI 模型框架 20 多年来,Microsoft 持续对企业商业智能 (BI) 进行大量投资。 Azure Analysis Services (AAS) 和 SQL Server Analysis Services (SSAS) 基于无数企业使用的成熟的 BI 数据建模技术。 同样的技术也是 Power BI 数据…...

Day131 | 灵神 | 回溯算法 | 子集型 子集
Day131 | 灵神 | 回溯算法 | 子集型 子集 78.子集 78. 子集 - 力扣(LeetCode) 思路: 笔者写过很多次这道题了,不想写题解了,大家看灵神讲解吧 回溯算法套路①子集型回溯【基础算法精讲 14】_哔哩哔哩_bilibili 完…...

(转)什么是DockerCompose?它有什么作用?
一、什么是DockerCompose? DockerCompose可以基于Compose文件帮我们快速的部署分布式应用,而无需手动一个个创建和运行容器。 Compose文件是一个文本文件,通过指令定义集群中的每个容器如何运行。 DockerCompose就是把DockerFile转换成指令去运行。 …...

Android 之 kotlin 语言学习笔记三(Kotlin-Java 互操作)
参考官方文档:https://developer.android.google.cn/kotlin/interop?hlzh-cn 一、Java(供 Kotlin 使用) 1、不得使用硬关键字 不要使用 Kotlin 的任何硬关键字作为方法的名称 或字段。允许使用 Kotlin 的软关键字、修饰符关键字和特殊标识…...
是否存在路径(FIFOBB算法)
题目描述 一个具有 n 个顶点e条边的无向图,该图顶点的编号依次为0到n-1且不存在顶点与自身相连的边。请使用FIFOBB算法编写程序,确定是否存在从顶点 source到顶点 destination的路径。 输入 第一行两个整数,分别表示n 和 e 的值(1…...

Unity | AmplifyShaderEditor插件基础(第七集:平面波动shader)
目录 一、👋🏻前言 二、😈sinx波动的基本原理 三、😈波动起来 1.sinx节点介绍 2.vertexPosition 3.集成Vector3 a.节点Append b.连起来 4.波动起来 a.波动的原理 b.时间节点 c.sinx的处理 四、🌊波动优化…...
#Uniapp篇:chrome调试unapp适配
chrome调试设备----使用Android模拟机开发调试移动端页面 Chrome://inspect/#devices MuMu模拟器Edge浏览器:Android原生APP嵌入的H5页面元素定位 chrome://inspect/#devices uniapp单位适配 根路径下 postcss.config.js 需要装这些插件 “postcss”: “^8.5.…...
A2A JS SDK 完整教程:快速入门指南
目录 什么是 A2A JS SDK?A2A JS 安装与设置A2A JS 核心概念创建你的第一个 A2A JS 代理A2A JS 服务端开发A2A JS 客户端使用A2A JS 高级特性A2A JS 最佳实践A2A JS 故障排除 什么是 A2A JS SDK? A2A JS SDK 是一个专为 JavaScript/TypeScript 开发者设计的强大库ÿ…...

力扣热题100 k个一组反转链表题解
题目: 代码: func reverseKGroup(head *ListNode, k int) *ListNode {cur : headfor i : 0; i < k; i {if cur nil {return head}cur cur.Next}newHead : reverse(head, cur)head.Next reverseKGroup(cur, k)return newHead }func reverse(start, end *ListNode) *ListN…...