Playable 动画系统

news2024/11/23 16:57:24

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

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

c++ cpp cmake opencv 深度学习模型 推理 前向部署 代码示例示意

参考实现&#xff1a; https://github.com/spmallick/learnopencv/tree/master/AgeGender 文件结构&#xff1a; 具体实现&#xff1a; #include <opencv2/imgproc.hpp> #include <opencv2/highgui.hpp> #include <opencv2/dnn.hpp> #include <tuple&g…

机器学习深度学习——seq2seq实现机器翻译(数据集处理)

&#x1f468;‍&#x1f393;作者简介&#xff1a;一位即将上大四&#xff0c;正专攻机器学习的保研er &#x1f30c;上期文章&#xff1a;机器学习&&深度学习——从编码器-解码器架构到seq2seq&#xff08;机器翻译&#xff09; &#x1f4da;订阅专栏&#xff1a;机…

[数据集][目标检测]道路坑洼目标检测数据集VOC格式1510张2类别

数据集格式&#xff1a;Pascal VOC格式(不包含分割路径的txt文件和yolo格式的txt文件&#xff0c;仅仅包含jpg图片和对应的xml) 图片数量(jpg文件个数)&#xff1a;1510 标注数量(xml文件个数)&#xff1a;1510 标注类别数&#xff1a;2 标注类别名称:["keng","…

指针进阶大冒险:解锁C语言中的奇妙世界!

目录 引言 第一阶段&#xff1a;&#x1f50d; 独特的字符指针 什么是字符指针&#xff1f; 字符指针的用途 演示&#xff1a;使用字符指针拷贝字符串 字符指针与字符串常量 小试牛刀 第二阶段&#xff1a;&#x1f3af; 玩转指针数组 指针数组是什么&#xff1f; 指针…

操作系统—网络系统

什么是零拷贝 磁盘是计算机系统最慢的的硬件之一&#xff0c;所以有不少优化磁盘的方法&#xff0c;比如零拷贝、直接IO、异步IO等等&#xff0c;这些优化的目的是为了提高系统的吞吐量&#xff0c;另外操作系统内核中的磁盘高度缓存区&#xff0c;可以有效的减少磁盘的访问次…

HCIP---重发布技术

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 前言一、pandas是什么&#xff1f;二、使用步骤 1.引入库2.读入数据总结 前言 前文通过学习OSPF的不规则区域&#xff0c;了解了如何对不规则区域使用OSPF获取路由&…

JavaWeb-Servlet服务连接器(二)

目录 Request&#xff08;获取请求信息&#xff09; 1.获取请求行内容 2.解决乱码问题 3.获取请求头部分 4.获取请求体 5.其他功能 Request&#xff08;获取请求信息&#xff09; 工作流程&#xff1a; 1.通过请求的url的资源路径&#xff0c;tomcat会生成相应的Servlet实…

iOS 使用build setting中的配置项配置info.plist

如图所示&#xff0c;选中build setting&#xff0c;由于我们这里是想要获取版本号&#xff0c; 所以这里获取current project version 选中info&#xff0c;将bundle version 设置为$&#xff08;CURRENT_PROJECT_VERSION&#xff09;,注意&#xff0c;要使用下划线链接&…

【C++】常用容器-string容器

1.string基本概念 2.string构造函数 #include <iostream> using namespace std;//string容器 void test01() {string s1;//创建空字符串&#xff0c;调用无参构造函数cout << "str1 " << s1 << endl;//什么都不输出const char* str "…

阿里云服务器安装部署Docker使用教程

本文阿里云百科分享如何在云服务ECS实例上&#xff0c;部署并使用Docker。Docker是一款开源的应用容器引擎&#xff0c;具有可移植性、可扩展性、高安全性和可管理性等优势。开发者可将应用程序和依赖项打包到一个可移植的容器中&#xff0c;快速发布到Linux机器上并实现虚拟化…

react学习笔记——4. 虚拟dom中处理动态数据

如下需求 方式1&#xff1a; 直接在ul中使用{data}&#xff0c;是可以遍历数据的&#xff0c;然后如果将data改成下面形式&#xff0c;也是可以实现的。但是如果data是一个对象&#xff0c;则不能便利。 const data [<li>Angular</li>, <li>React</li&g…

c语言——统计分类

我们将一个班的成绩进行分类&#xff0c; 成绩60分以下的为c、成绩61-89分的为b&#xff0c;90分以上的为A //统计分类 /*我们将一个班的成绩进行分类&#xff0c; 成绩60分以下的为c、成绩61-89分的为b&#xff0c;90分以上的为A */ #include<stdio.h> int main() …

MyBatis的XML映射文件

Mybatis的开发有两种方式&#xff1a; 注解 XML配置文件 通过XML配置文件的形式来配置SQL语句&#xff0c;这份儿XML配置文件在MyBatis当中也称为XML映射文件。 导学&#xff1a;在MyBatis当中如何来定义一份儿XML映射文件&#xff1f; 在MyBatis当中&#xff0c;定义XML…

HCIP的BGP基础实验

一、实验需求 除R5的5.5.5.0环回外&#xff0c;其他所有的环回均可互相一访问。 二、实验步骤 1.配置ip 2.建立邻居关系 2.1 R1和R2建立直连的EBGP邻居关系 [r1]bgp 1 [r1-bgp]router-id 1.1.1.1 [r1-bgp]peer 12.1.1.2 as-number 2 要建的话双方都要建下面配置R2 [r2]bgp…

“冰箭卫士·IP发布会”首次亮相第14届海峡两岸(厦门)文博会

2023年8月6日,“冰箭卫士IP发布会”首次亮相海峡两岸文博会思明馆。此次发布会由厦门市文化创意产业协会、厦门理工&#xff08;集美区&#xff09;政产学研基地主办&#xff0c;厦门市文化创意产业协会IP设计研究院、厦门一笔之上文化发展有限公司、冰箭应急安全科技研究院承办…

springboot 设置自定义启动banner背景图 教程

springboot banner Spring Boot中的banner是在应用程序启动时显示的一个ASCII艺术字符或文本。它被用来给用户展示一些关于应用程序的信息&#xff0c;例如名称、版本号或者公司标志等。 使用Spring Boot的默认设置&#xff0c;如果项目中有一个名为“banner.txt”的文件放置…

交换排序——选择排序和冒泡排序的区别是什么?

今天重温一下算法&#xff0c;其实刚开始我觉得冒泡排序和选择排序是一样的&#xff0c;因为他们排序过程中都是通过相邻的数据比较找到最小/最大的数据&#xff0c;通过不断思考和学习才明白&#xff0c;两者还是有区别的。 冒泡排序 概念 冒泡排序(Bubble Sort)&#xff0…

【面试专题】Java核心基础篇①

&#x1f4c3;个人主页&#xff1a;个人主页 &#x1f525;系列专栏&#xff1a;Java面试专题 目录 1.面向对象的三大特性&#xff1f;分别解释下&#xff1f; 2.介绍一下Java的数据类型 3.说一说重写与重载的区别 4.说一说你对static关键字的理解 5.static修饰的类能不能…

ESP-01S Wi-Fi 模块:配置接线

ESP-01S Wi-Fi 模块&#xff1a;配置接线 参考&#xff1a;使用esp-01s与继电器配合实现远程开关灯 (zhihu.com) ESP-01S WiFi 模块 – 配置布线 - 技术探索 (techexplorations.com) 本文提供了将 ESP8266 Wi-Fi 模块与 Arduino Uno 配合使用的分步指南&#xff0c;重点介绍了…

湘大 XTU OJ 1291 Buying Gifts 题解(非常详细):枚举 维护最小值 排序

一、链接 1291 Buying Gifts 二、题目 题目描述 快到年末了&#xff0c;Boss Liu准备在年会上发些礼物&#xff0c;由于不想礼物的价格区别太大&#xff0c;Boss Liu希望最好的礼物与最差的礼物价格相差越小越好。 当然&#xff0c;如果存在相同的选择&#xff0c;Boss Liu…