当前位置: 首页 > news >正文

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意思是可播放的&#xff0c;可运行的。Playable整体是树形结构&#xff0c;PlayableGraph相当于一个容器&#xff0c;所有元素都被包含在里面&#xff0c;图中的每个节点都是Playable&#xff0c;叶子节点的Playable包裹原始数据&#xff0c;相当于输…...

深入理解Linux内核--虚拟文件

虚拟文件系统(VFS)的作用 虚拟文件系统(Virtual Filesystem)也可以称之为虚拟文件系统转换(Virtual Filesystem Switch,VFS), 是一个内核软件层&#xff0c; 用来处理与Unix标准文件系统相关的所有系统调用。 其健壮性表现在能为各种文件系统提供一个通用的接口。VFS支持的文件…...

记一次 .NET 某外贸ERP 内存暴涨分析

一&#xff1a;背景 1. 讲故事 上周有位朋友找到我&#xff0c;说他的 API 被多次调用后出现了内存暴涨&#xff0c;让我帮忙看下是怎么回事&#xff1f;看样子是有些担心&#xff0c;但也不是特别担心&#xff0c;那既然找到我&#xff0c;就给他分析一下吧。 二&#xff1…...

关于安卓打包生成aar,jar实现(一)

关于安卓打包生成aar&#xff0c;jar方式 背景 在开发的过程中&#xff0c;主项目引入三方功能的方式有很多&#xff0c;主要是以下几个方面&#xff1a; &#xff08;1&#xff09;直接引入源代码module&#xff08;优点&#xff1a;方便修改源码&#xff0c;易于维护&#…...

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&#xff08;截止2020.4.1&#xff09;&#xff0c;我们选择6.8.1版本&#xff0c;建议使用JDK1.8及以上。 ElasticSearch分为Linux和Window版本&#xff0c;基于我们主要学习…...

JavaWeb中Json传参的条件

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

包装类+初识泛型

目录 1 .包装类 1.1 基本数据类型对应的包装类 1.2.1装箱 ​1.2.2拆箱 2.初识泛型 2.1什么是泛型 2.2泛型类 2.3裸类型 2.4泛型的上界 2.5泛型方法 1 .包装类 基本数据类型所对应的类类型 在 Java 中&#xff0c;由于基本类型不是继承自 Object &#xff0c;为了在泛型…...

基于改进的长短期神经网络电池电容预测,基于DBN+LSTM+SVM的电池电容预测

目录 背影 摘要 LSTM的基本定义 LSTM实现的步骤 基于长短期神经网络LSTM的客电池电容预测 完整代码: 基于长短期神经网络LSTM的公交站客流量预测资源-CSDN文库 https://download.csdn.net/download/abc991835105/88184734 效果图 结果分析 展望 参考论文 背影 为增加电动车行…...

Python 2.x 中如何使用pandas模块进行数据分析

Python 2.x 中如何使用pandas模块进行数据分析 概述: 在数据分析和数据处理过程中&#xff0c;pandas是一个非常强大且常用的Python库。它提供了数据结构和数据分析工具&#xff0c;可以实现快速高效的数据处理和分析。本文将介绍如何在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无法使用问题

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

[管理与领导-11]:IT基层管理者 - 目标与落实 - 过程管理失控,结果总难达成的问题思考:如何把过程管控做得更好?

目录 前言&#xff1a; 第1章 问题与现象 1.1 总有意想不到的事发生&#xff1a;意外事件 1.2 总有计划变更&#xff1a;意外影响 1.3 总有一错再错&#xff0c;没有复盘、总结与反思&#xff0c;没有流程与改进 第2章 背后的原因 2.1 缺乏及时的过程检查 - 缺乏异常检测…...

用php语言写一个chatgpt3.5模型的例子

当然可以&#xff01;使用PHP语言调用OpenAI API与ChatGPT-3.5模型进行交互。首先&#xff0c;确保你已经安装了PHP 7.2或更新版本&#xff0c;并具备可用的OpenAI API密钥。 下面是一个基本的PHP示例&#xff0c;展示了如何使用OpenAI API与ChatGPT-3.5模型进行对话&#xff…...

PHP实现保质期计算器

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

【独立版】新零售社区团购电商系统生鲜水果商城兴盛优选十荟团源码

【独立版】新零售社区团购电商系统生鲜水果商城兴盛优选十荟团源码...

C++系列十:其他-1. Lua

系列文章目录 Lua 系列文章目录前言Lua介绍&#xff1a;参考链接&#xff1a; 基本语法&#xff1a;函数、迭代器table、userdata、模块元素、元方法&#xff1a;协程、文件读写面向对象、垃圾回收 前言 我写这个博客的一个问题&#xff1f;(●’◡’●) 居然是 取名太难了。 …...

不知道打仗之害,就不知道打仗之利

不知道打仗之害&#xff0c;就不知道打仗之利 【安志强趣讲《孙子兵法》第7讲】 【原文】 夫钝兵挫锐&#xff0c;屈力殚货&#xff0c;则诸侯乘其弊而起&#xff0c;虽有智者&#xff0c;不能善其后矣。 【注释】 屈力殚货&#xff1a;屈力&#xff0c;指力量消耗&#xff0c;…...

【leetcode】242. 有效的字母异位词(easy)

给定两个字符串 s 和 t &#xff0c;编写一个函数来判断 t 是否是 s 的字母异位词。 注意&#xff1a;若 s 和 t 中每个字符出现的次数都相同&#xff0c;则称 s 和 t 互为字母异位词。 思路&#xff1a; 先比较两字符串长度是否相同&#xff0c;如果不同直接返回false。创建…...

synchronized 学习

学习源&#xff1a; https://www.bilibili.com/video/BV1aJ411V763?spm_id_from333.788.videopod.episodes&vd_source32e1c41a9370911ab06d12fbc36c4ebc 1.应用场景 不超卖&#xff0c;也要考虑性能问题&#xff08;场景&#xff09; 2.常见面试问题&#xff1a; sync出…...

在鸿蒙HarmonyOS 5中实现抖音风格的点赞功能

下面我将详细介绍如何使用HarmonyOS SDK在HarmonyOS 5中实现类似抖音的点赞功能&#xff0c;包括动画效果、数据同步和交互优化。 1. 基础点赞功能实现 1.1 创建数据模型 // VideoModel.ets export class VideoModel {id: string "";title: string ""…...

ffmpeg(四):滤镜命令

FFmpeg 的滤镜命令是用于音视频处理中的强大工具&#xff0c;可以完成剪裁、缩放、加水印、调色、合成、旋转、模糊、叠加字幕等复杂的操作。其核心语法格式一般如下&#xff1a; ffmpeg -i input.mp4 -vf "滤镜参数" output.mp4或者带音频滤镜&#xff1a; ffmpeg…...

[10-3]软件I2C读写MPU6050 江协科技学习笔记(16个知识点)

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16...

用docker来安装部署freeswitch记录

今天刚才测试一个callcenter的项目&#xff0c;所以尝试安装freeswitch 1、使用轩辕镜像 - 中国开发者首选的专业 Docker 镜像加速服务平台 编辑下面/etc/docker/daemon.json文件为 {"registry-mirrors": ["https://docker.xuanyuan.me"] }同时可以进入轩…...

精益数据分析(97/126):邮件营销与用户参与度的关键指标优化指南

精益数据分析&#xff08;97/126&#xff09;&#xff1a;邮件营销与用户参与度的关键指标优化指南 在数字化营销时代&#xff0c;邮件列表效度、用户参与度和网站性能等指标往往决定着创业公司的增长成败。今天&#xff0c;我们将深入解析邮件打开率、网站可用性、页面参与时…...

iOS性能调优实战:借助克魔(KeyMob)与常用工具深度洞察App瓶颈

在日常iOS开发过程中&#xff0c;性能问题往往是最令人头疼的一类Bug。尤其是在App上线前的压测阶段或是处理用户反馈的高发期&#xff0c;开发者往往需要面对卡顿、崩溃、能耗异常、日志混乱等一系列问题。这些问题表面上看似偶发&#xff0c;但背后往往隐藏着系统资源调度不当…...

Java求职者面试指南:计算机基础与源码原理深度解析

Java求职者面试指南&#xff1a;计算机基础与源码原理深度解析 第一轮提问&#xff1a;基础概念问题 1. 请解释什么是进程和线程的区别&#xff1f; 面试官&#xff1a;进程是程序的一次执行过程&#xff0c;是系统进行资源分配和调度的基本单位&#xff1b;而线程是进程中的…...

【C++特殊工具与技术】优化内存分配(一):C++中的内存分配

目录 一、C 内存的基本概念​ 1.1 内存的物理与逻辑结构​ 1.2 C 程序的内存区域划分​ 二、栈内存分配​ 2.1 栈内存的特点​ 2.2 栈内存分配示例​ 三、堆内存分配​ 3.1 new和delete操作符​ 4.2 内存泄漏与悬空指针问题​ 4.3 new和delete的重载​ 四、智能指针…...

MySQL 8.0 事务全面讲解

以下是一个结合两次回答的 MySQL 8.0 事务全面讲解&#xff0c;涵盖了事务的核心概念、操作示例、失败回滚、隔离级别、事务性 DDL 和 XA 事务等内容&#xff0c;并修正了查看隔离级别的命令。 MySQL 8.0 事务全面讲解 一、事务的核心概念&#xff08;ACID&#xff09; 事务是…...