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()
    {
        //创建PlayableGraph
        playableGraph = PlayableGraph.Create();
        playableGraph.SetTimeUpdateMode(DirectorUpdateMode.GameTime);
        
        //创建AnimationClipPlayable包裹AnimationClip,附加到PlayableGraph上
        var clipPlayable = AnimationClipPlayable.Create(playableGraph, clip);
        
        //创建输出节点并把Animator设为目标,Animator会处理PlayableGraph
        var 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和1
        playableGraph.Connect(clipPlayable0, 0, mixerPlayable, 0);
        playableGraph.Connect(clipPlayable1, 0, mixerPlayable, 1);
        
        playableGraph.Play();
    }

    void Update()
    {
        //保证所有输入源的权重和为1
        weight = 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,其他为0
        for (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属于那个AnimAdapter
    protected 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类型是否继承AnimAdapter
        if (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 Compute

struct 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 动画系统

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/70441.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Vue+SpringBoot后台管理系统:Vue3+TypeScript项目搭建(一)

写在开始:一个搬砖程序员的随缘记录文章目录 一、Node安装二、Vue CLI安装三、相关的版本四、创建Vue3TypeScript项目五、Vue项目初始化六、项目启动 一、Node安装 查看Note版本 node -v查看npm版本 npm -v然后将npm升级至最新版本 npm -g install npm将npm下载源换至http:…

RS-232标准

目录 1、概述2、RS-232接口的特点3、RS-232接口协议【仿真】 1、概述 RS-232接口是在1970年由美国电子工业协会(EIA)联合贝尔系统、调制解调器厂家及计算机终端生产厂家共同制定的用于串行通讯的标准。它的全名是“数据终端设备(DTE)和数据通讯设备(DCE)之间串行二进制数据交换…

C语言 二级指针和多级指针

什么是二级指针&#xff1f; 假设&#xff1a; int a 10;int * p &a;如上&#xff0c;p是指针变量&#xff0c;寄存的是a的地址&#xff0c;指向的是元素a 那么&#xff0c;指针变量p有地址吗&#xff1f;指针变量p的指针指向的是&#xff1f; int * * pp &p; …

【Spring Boot 源码学习】自动装配流程源码解析(上)

自动装配流程源码解析&#xff08;上&#xff09; 引言往期内容主要内容1. 自动配置开关2. 加载自动配置组件3. 自动配置组件去重 总结 引言 上篇博文&#xff0c;笔者带大家从整体上了解了AutoConfigurationImportSelector 自动装配逻辑的核心功能及流程&#xff0c;由于篇幅…

Visual Studio 2022安装教程(英文版)

文章目录 1.下载安装 1.下载 官网地址&#xff1a;https://visualstudio.microsoft.com/zh-hans/vs/ 选择第一个社区版本&#xff1a;Community 2022 安装 1.将下载好的文件保存到桌面&#xff0c;双击点开 2.等待visual studio installer配置好 3.点击安装后会来到配件选…

KeePass CVE-2023-32784:进程内存转储检测

KeePass CVE-2023-32784&#xff1a;进程内存转储检测 KeePass 是一种流行的开源密码管理器&#xff0c;可以在 Windows、Mac 或 Linux 上运行。该漏洞允许从正在运行的进程的内存中以明文形式提取主密钥。主密钥将允许攻击者访问所有存储的凭据 强烈建议更新到KeePass 2.54以…

机器学习基础之《特征工程(4)—特征降维》

一、什么是特征降维 降维是指在某些限定条件下&#xff0c;降低随机变量&#xff08;特征&#xff09;个数&#xff0c;得到一组“不相关”主变量的过程 1、降维 降低维度 ndarry 维数&#xff1a;嵌套的层数 0维&#xff1a;标量&#xff0c;具体的数0 1 2 3... …

认识http的方法、Header、状态码以及简单实现一个http的业务逻辑

文章目录 http的方法http状态码http重定向http常见Header实现简单业务逻辑Protocol.hppUtil.hppServer.hppServer.cc 效果 http的方法 方法说明支持的HTTP版本GET获取资源1.0/1.1POST传输实体主体1.0/1.1PUT传输文件1.0/1.1HEAD获得报文首部1.0/1.1DELETE删除文件1.0/1.1OPTIO…

【将回声引入信号中】在语音或音频文件中引入混响或简单回声,以研究回声延迟和回波幅度对生成的回波信号感知的影响(Matlab代码实现)

&#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;欢迎来到本博客❤️❤️&#x1f4a5;&#x1f4a5; &#x1f3c6;博主优势&#xff1a;&#x1f31e;&#x1f31e;&#x1f31e;博客内容尽量做到思维缜密&#xff0c;逻辑清晰&#xff0c;为了方便读者。 ⛳️座右铭&a…

【杨辉三角的两种解法——(超级详细)】

杨辉三角 1.杨辉三角简介&#x1f575;️ 杨辉三角&#xff0c;是二项式系数在三角形中的一种几何排列。在欧洲&#xff0c;这个表叫做帕斯卡三角形。帕斯卡&#xff08;1623----1662&#xff09;是在1654年发现这一规律的&#xff0c;比杨辉要迟393年&#xff0c;比贾宪迟600…

分布式 - 消息队列Kafka:Kafka消费者的分区分配策略

文章目录 1. 环境准备2. range 范围分区策略介绍3. round-robin 轮询分区策略4. sticky 粘性分区策略5. 自定义分区分配策略 1. 环境准备 创建主题 test 有5个分区&#xff0c;准备 3 个消费者并进行消费&#xff0c;观察消费分配情况。然后再停止其中一个消费者&#xff0c;再…

fastadmin 自定义搜索分类和时间范围

1.分类搜索&#xff0c;分类信息获取----php 2.对应html页面&#xff0c;页面底部加搜索提交代码&#xff08;这里需要注意&#xff1a;红框内容&#xff09; 图上代码----方便直接复制使用 <script id"countrySearch" type"text/html"><!--form…

python之matplotlib入门初体验:使用Matplotlib进行简单的图形绘制

目录 绘制简单的折线图1.1 修改标签文字和线条粗细1.2 校正图形1.3 使用内置样式1.4 使用scatter()绘制散点图并设置样式1.5 使用scatter()绘制一系列点1.6 python循环自动计算数据1.7 自定义颜色1.8 使用颜色映射1.9 自动保存图表练习题 绘制简单的折线图 绘制一个简单折线图…

GPT-3.5 人工智能还是人工智障?——西红柿炒钢丝球!!

人工智能还是人工智障&#xff1f;——西红柿炒钢丝球 西红柿炒钢丝球的 基本信息西红柿炒钢丝球的 详细制作方法材料步骤 备注幕后花絮。。。。。。。。。关于GPT-3.5&#xff0c;你的看法&#xff1a; 西红柿炒钢丝球的 基本信息 西红柿炒钢丝球是一道具有悠久历史的传统中式…

springboot汽车租赁后台java出租客户管理jsp源代码mysql

本项目为前几天收费帮学妹做的一个项目&#xff0c;Java EE JSP项目&#xff0c;在工作环境中基本使用不到&#xff0c;但是很多学校把这个当作编程入门的项目来做&#xff0c;故分享出本项目供初学者参考。 一、项目描述 springboot汽车租赁后台 系统有1权限&#xff1a;管理…

阿里巴巴面试题---考察对底层源代码的熟悉程度

题目如图所示: 很多人可能会觉得两个输出都会是false,因为我们都会觉得""比较的是引用类型的地址,虽然放入的值都一样但是重新创造了新对象,地址不一样,所以结果都是false. 然而,当我们运行程序会发现结果都是false. 下面,我们来分析为什么是这样的结果. 我们知道…

OneFlow 中的 Softmax

Softmax 是深度学习模型中的常见算子。PyTorch 的 Softmax 算子直接调用 cuDNN 的接口。而 OneFlow 内部针对输入数据的类别数量&#xff0c;采用3个 kernel 来分别处理&#xff0c;在多数情况下都可以获得比 cuDNN 更优的性能表现。测试结果可见 如何实现一个高效的Softmax CU…

未来混合动力汽车的发展:技术探索与前景展望

随着环境保护意识的增强和对能源消耗的关注&#xff0c;混合动力汽车成为了汽车行业的研发热点。混合动力汽车融合了传统燃油动力和电力动力系统&#xff0c;通过优化能源利用效率&#xff0c;既降低了燃油消耗和排放&#xff0c;又提供了更长的续航里程。本文将探讨混合动力汽…

配置docker,案例复现

配置docker(系统为centos) 1.检查操作系统环境: docker要求CentOS系统的内核版本高于 3.10 &#xff0c;通过 uname -r 命令查看你当前的内核版本是否支持安装docker 2.查看你是否拥有旧的版本&#xff0c;有的话卸载&#xff0c;没有的话直接略过该步骤 sudo yum remove d…

【快应用】list组件属性的运用指导

【关键词】 list、瀑布流、刷新、页面布局 【问题背景】 1、 页面部分内容需要瀑布流格式展示&#xff0c;在使用lsit列表组件设置columns进行多列渲染时&#xff0c;此时在里面加入刷新动画时&#xff0c;动画只占了list组件的一列&#xff0c;并没有完全占据一行宽度&…