美文网首页征服Unity3dUnity基础入门分享
一个日本人写的插件:Breath Controller

一个日本人写的插件:Breath Controller

作者: 恶毒的狗 | 来源:发表于2020-01-09 16:56 被阅读0次

Breath Controller

今天无意发现一个日本人写的 呼吸控制器,挺好玩的,可以从他的 主页 下载源代码。

image

这个插件目前只支持 人形动画,不过只需要简单的几行修改就可以支持 Generic动画 了,文章的最后会给出代码。

好了,二话不说,先套到我们的 小甜甜 身上看看效果:

听轻音乐

image

听摇滚

image

实现原理

Breath Controller 是程序控制的呼吸动画,作者区分了 吸气呼气休息 三个状态,我们可以调整这3个状态的持续时长:

image

代码就是不断地循环这3个状态以模拟 呼吸动画

void OnInhaling() 
{
    if (this.RotateBone()) 
    {
        this.phase = Phase.Exhaling;
        this.SetEase();
    }
}

void OnExhaling() 
{
    if (this.RotateBone()) 
    {
        this.phase = Phase.Rest;
        this.restEndTime = Time.time + (this.restDuration * this.durationRate);
    }
}

void OnRest() 
{
    this.RotateBone();
    if (this.restEndTime <= Time.time) 
    {
        this.phase = Phase.Inhaling;
        this.SetEase();
    }
}

呼吸动画 主要涉及 脊椎 这4根骨骼的旋转计算,如下图:

image

这里额外标注出了 左肩右肩,这是因为根据骨骼的父子关系,脊椎 或者 的运动也会带动 肩膀 的运动,作者不希望 肩膀 受到呼吸的影响,所以这里在计算完呼吸的运动后会对 肩膀 做一个复位操作,伪代码大致如下:

void RotateBone() 
 {
    // Backup Shoulder(or UpperArm) rotation.
    var originLeftShoulderRotation = this.LeftShoulder.rotation;
    var originRightShoulderRotation = this.RightShoulder.rotation;

    // Rotate Spine, Cheast, Neck, Head
    // TODO: 旋转脊椎,胸,颈,头        

    // Rotate Shoulder or UpperArm
    this.LeftShoulder.rotation = originLeftShoulderRotation;
    this.RightShoulder.rotation = originRightShoulderRotation;
}

好了,下面看一下 旋转骨骼 的实现细节。

作者给出的旋转参数不多,最主要的是每根骨骼 吸气呼气 的最大旋转角度,如下图:

image

这里提到的旋转,作者用了 Transform.Rotate 这个函数:

public void Rotate(Vector3 eulers, Space relativeTo = Space.Self);

Applies a rotation of eulerAngles.z degrees around the z-axis, eulerAngles.x degrees around the x-axis, and eulerAngles.y degrees around the y-axis (in that order).

这里用 欧拉角 来描述旋转,并且旋转只会绕某一个轴进行。程序在初始化的时候会按照 骨骼朝向和角色朝向的匹配程度 来确定旋转方向,从而确定旋转轴。

关于旋转轴,下图应该看得比较清楚:

image

旋转的核心代码如下:

// Rotate Spine, Cheast, Neck, Head
int finishCnt = 0;
for (int i = 0; i < this.Segments.Length; i++) 
{
    var seg = this.Segments[i];

    if (this.hasController) 
    {
        seg.transform.Rotate(new Vector3(
            seg.x.UpdateEase(this.phase, this.InhalingMethod, this.durationRate),
            seg.y.UpdateEase(this.phase, this.InhalingMethod, this.durationRate),
            seg.z.UpdateEase(this.phase, this.InhalingMethod, this.durationRate)));
    } 
    else 
    {
        var lastEaseValueX = seg.x.lastEaseValue;
        var lastEaseValueY = seg.y.lastEaseValue;
        var lastEaseValueZ = seg.z.lastEaseValue;
        seg.transform.Rotate(new Vector3(
            seg.x.UpdateEase(this.phase, this.InhalingMethod, this.durationRate) - lastEaseValueX,
            seg.y.UpdateEase(this.phase, this.InhalingMethod, this.durationRate) - lastEaseValueY,
            seg.z.UpdateEase(this.phase, this.InhalingMethod, this.durationRate) - lastEaseValueZ));
    }

    if (seg.x.IsFinishEase(this.durationRate) &&
            seg.y.IsFinishEase(this.durationRate) &&
            seg.z.IsFinishEase(this.durationRate)) 
    {
        finishCnt++;
    }
}

代码比较简单,唯一要注意的是这里区分了是否有 Animator,如果有,AnimatorUpdate 会复位动作,所以这时旋转角度不用减去 lastEaseValue

最后,对于旋转角度的插值,作者给出了不同的插值算法,并且开放了吸气的插值算法给我们选择:

正弦插值

image
float easeOutSine(float t, float b, float c, float d) 
{
    if (t >= d) return c + b;
    return c * Mathf.Sin(t / d * (Mathf.PI / 2)) + b;
}

分段二次插值

image
float easeInOutQuad(float t, float b, float c, float d) 
{
    if (t >= d) return c + b;
    t /= d / 2;
    if (t < 1) return c / 2 * t * t + b;
    t--;
    return -c / 2 * (t * (t - 2) - 1) + b;
}

想象一下 深吸一口气,是不是下图的 正弦插值 更加合适呢,:)

image

和DynamicBone一起工作

Breath ControllerDynamicBone 一样,都是在 LateUpdate 里去更新骨骼,如果两者一起工作的时候,我们必须保证 Breath Controller 先更新,DynamicBone 后更新,不然 DynamicBone 就不会对呼吸生效了。

这里我们人为的指定一下脚本执行顺序即可:

image

非人形动画的支持

Breath Controller 目前的版本只支持 人形动画,如果需要支持 Generic动画,我们可以手动指定呼吸计算所需要的骨骼。

这里偷个懒,我在所有 Animator.GetBoneTransform 逻辑的后面都加一个判断,如果取不到就用手动指定的骨骼即可。

修改后的代码如下:

using UnityEngine;
using System.Collections;

/**
BreathController

Copyright (c) 2015 Toshiaki Aizawa (https://twitter.com/xflutexx)

This software is released under the MIT License.
 http://opensource.org/licenses/mit-license.php …
*/
namespace Mebiustos.BreathController {
    public class BreathController : MonoBehaviour {
        public const float InitialDurationInhale = 1.2f; // 1.3
        public const float InitialDurationExhale = 2.4f; // 2.7
        public const float InitialDurationRest = 0.2f;
        public const float InitialAngleSpineInhale = 2f;
        public const float InitialAngleSpineExhale = -2f;
        public const float InitialAngleChestInhale = -3f;
        public const float InitialAngleChestExhale = 3f;
        public const float InitialAngleNeckInhale = 0.5f;
        public const float InitialAngleNeckExhale = -0.5f;
        public const float InitialAngleHeadInhale = 0.5f;
        public const float InitialAngleHeadExhale = -0.5f;
        public const HalingMethod InitialMethodInhale = HalingMethod.EaseOutSine;

        [System.Serializable]
        public class Segment {
            public HumanBodyBones Bone;

            public Angle x = new Angle();
            public Angle y = new Angle();
            public Angle z = new Angle();

            [System.NonSerialized]
            public Transform transform;
        }

        [System.Serializable]
        public class Angle {
            public float max;
            public float min;
            public float maxDuration;
            public float minDuration;

            float startTime;
            float startValue;
            float changeInValue;
            float durationTime;

            public float lastEaseValue;

            public void SetEase(float startValue, float changeInValue, float durationTime) {
                this.startTime = Time.time;
                this.startValue = startValue;
                this.changeInValue = changeInValue;
                this.durationTime = durationTime;
            }

            public float UpdateEase(Phase status, HalingMethod inhalingMethod, float durationRate) {
                if (status == Phase.Inhaling) {
                    if (inhalingMethod == HalingMethod.EaseOutSine)
                        this.lastEaseValue = easeOutSine(Time.time - this.startTime, this.startValue, this.changeInValue, this.durationTime * durationRate);
                    else
                        this.lastEaseValue = easeInOutQuad(Time.time - this.startTime, this.startValue, this.changeInValue, this.durationTime * durationRate);
                    return this.lastEaseValue;
                } else {
                    this.lastEaseValue = easeInOutQuad(Time.time - this.startTime, this.startValue, this.changeInValue, this.durationTime * durationRate);
                    return this.lastEaseValue;
                }
            }

            public bool IsFinishEase(float durationRate) {
                if (this.durationTime == 0) return true;
                return Time.time - this.startTime >= this.durationTime * durationRate;
            }

            /// <summary>
            /// </summary>
            /// <param name="t">current time</param>
            /// <param name="b">start value</param>
            /// <param name="c">change in value</param>
            /// <param name="d">duration</param>
            /// <returns></returns>
            float easeInOutQuad(float t, float b, float c, float d) {
                if (t >= d) return c + b;
                t /= d / 2;
                if (t < 1) return c / 2 * t * t + b;
                t--;
                return -c / 2 * (t * (t - 2) - 1) + b;
            }
            float easeOutCubic(float t, float b, float c, float d) {
                if (t >= d) return c + b;
                t /= d;
                t--;
                return c * (t * t * t + 1) + b;
            }
            float easeOutQuart(float t, float b, float c, float d) {
                if (t >= d) return c + b;
                t /= d;
                t--;
                return -c * (t * t * t * t - 1) + b;
            }
            float easeInOutQuart(float t, float b, float c, float d) {
                if (t >= d) return c + b;
                t /= d / 2;
                if (t < 1) return c / 2 * t * t * t * t + b;
                t -= 2;
                return -c / 2 * (t * t * t * t - 2) + b;
            }
            float easeOutSine(float t, float b, float c, float d) {
                if (t >= d) return c + b;
                return c * Mathf.Sin(t / d * (Mathf.PI / 2)) + b;
            }
            float easeInOutSine(float t, float b, float c, float d) {
                if (t >= d) return c + b;
                return -c / 2 * (Mathf.Cos(Mathf.PI * t / d) - 1) + b;
            }
            float easeOutExpo(float t, float b, float c, float d) {
                if (t >= d) return c + b;
                return c * (-Mathf.Pow(2, -10 * t / d) + 1) + b;
            }
        }

        [Header("Basic Config")]
        public float durationRate = 1;
        public float effectRate = 1;

        [Header("Generic Bones")]
        public Transform genericLeftShoulder;
        public Transform genericRightShoulder;
        public Transform genericHead;
        public Transform genericNeck;
        public Transform genericSpine;
        public Transform genericChest;

        Segment[] Segments;
        Transform LeftShoulder;
        Transform RightShoulder;
        public enum Phase {
            Inhaling,
            Exhaling,
            Rest
        }
        Phase phase;
        float restEndTime;
        bool hasController;

        void OnEnable() {
            var anim = GetComponent<Animator>();
            this.hasController = anim.runtimeAnimatorController != null;
            if (!this.hasController)
                Debug.LogWarning("Not found 'Animator Controller' : " + this.gameObject.name);

            this.phase = Phase.Inhaling;

            this.InitializeSegments(anim);
            this.InitializeSoulders(anim);

            this.SetEase();
        }

        void LateUpdate() {
            if (this.hasController)
                switch (phase) {
                    case Phase.Inhaling: OnInhaling(); break;
                    case Phase.Exhaling: OnExhaling(); break;
                    case Phase.Rest: OnRest(); break;
                }
        }

        void OnInhaling() {
            if (this.RotateBone()) {
                this.phase = Phase.Exhaling;
                this.SetEase();
            }
        }

        void OnExhaling() {
            if (this.RotateBone()) {
                this.phase = Phase.Rest;
                this.restEndTime = Time.time + (this.restDuration * this.durationRate);
            }
        }

        void OnRest() {
            this.RotateBone();
            if (this.restEndTime <= Time.time) {
                this.phase = Phase.Inhaling;
                this.SetEase();
            }
        }

        /// <summary>
        /// Bone Rotate
        /// </summary>
        /// <returns>IsReadyToNextPhase</returns>
        bool RotateBone() {
            // Backup Shoulder(or UpperArm) rotation.
            var originLeftShoulderRotation = this.LeftShoulder.rotation;
            var originRightShoulderRotation = this.RightShoulder.rotation;

            // Rotate Spine, Cheast, Neck, Head
            int finishCnt = 0;
            for (int i = 0; i < this.Segments.Length; i++) {
                var seg = this.Segments[i];

                if (this.hasController) {
                    seg.transform.Rotate(new Vector3(
                        seg.x.UpdateEase(this.phase, this.InhalingMethod, this.durationRate),
                        seg.y.UpdateEase(this.phase, this.InhalingMethod, this.durationRate),
                        seg.z.UpdateEase(this.phase, this.InhalingMethod, this.durationRate)
                        ));
                } else {
                    var lastEaseValueX = seg.x.lastEaseValue;
                    var lastEaseValueY = seg.y.lastEaseValue;
                    var lastEaseValueZ = seg.z.lastEaseValue;
                    seg.transform.Rotate(new Vector3(
                        seg.x.UpdateEase(this.phase, this.InhalingMethod, this.durationRate) - lastEaseValueX,
                        seg.y.UpdateEase(this.phase, this.InhalingMethod, this.durationRate) - lastEaseValueY,
                        seg.z.UpdateEase(this.phase, this.InhalingMethod, this.durationRate) - lastEaseValueZ)
                        );
                }

                if (seg.x.IsFinishEase(this.durationRate) &&
                    seg.y.IsFinishEase(this.durationRate) &&
                    seg.z.IsFinishEase(this.durationRate)) {
                    finishCnt++;
                }
            }

            // Rotate Shoulder or UpperArm
            this.LeftShoulder.rotation = originLeftShoulderRotation;
            this.RightShoulder.rotation = originRightShoulderRotation;

            // return IsReadyToNextPhase
            return finishCnt >= Segments.Length;
        }

        void SetEase() {
            for (int i = 0; i < this.Segments.Length; i++) {
                var seg = this.Segments[i];
                if (this.phase == Phase.Inhaling) {
                    seg.x.SetEase(seg.x.lastEaseValue, (seg.x.max * this.effectRate) - seg.x.lastEaseValue, seg.x.maxDuration);
                    seg.y.SetEase(seg.y.lastEaseValue, (seg.y.max * this.effectRate) - seg.y.lastEaseValue, seg.y.maxDuration);
                    seg.z.SetEase(seg.z.lastEaseValue, (seg.z.max * this.effectRate) - seg.z.lastEaseValue, seg.z.maxDuration);
                    //Debug.Log("duration:" + seg.z.maxDuration);
                } else {
                    seg.x.SetEase(seg.x.lastEaseValue, (seg.x.min * this.effectRate) - seg.x.lastEaseValue, seg.x.minDuration);
                    seg.y.SetEase(seg.y.lastEaseValue, (seg.y.min * this.effectRate) - seg.y.lastEaseValue, seg.y.minDuration);
                    seg.z.SetEase(seg.z.lastEaseValue, (seg.z.min * this.effectRate) - seg.z.lastEaseValue, seg.z.minDuration);
                    //Debug.Log("duration:" + seg.z.minDuration);
                }
            }
        }

        [Header("Advanced Config")]
        public float maxDuration = BreathController.InitialDurationInhale;
        public float minDuration = BreathController.InitialDurationExhale;
        public float restDuration = BreathController.InitialDurationRest;

        public float SpineInhaleAngle = BreathController.InitialAngleSpineInhale;
        public float SpineExhaleAngle = BreathController.InitialAngleSpineExhale;
        public float ChestInhaleAngle = BreathController.InitialAngleChestInhale;
        public float ChestExhaleAngle = BreathController.InitialAngleChestExhale;
        public float NeckInhaleAngle = BreathController.InitialAngleNeckInhale;
        public float NeckExhaleAngle = BreathController.InitialAngleNeckExhale;
        public float HeadInhaleAngle = BreathController.InitialAngleHeadInhale;
        public float HeadExhaleAngle = BreathController.InitialAngleHeadExhale;
        public enum HalingMethod {
            EaseOutSine,
            EaseInOutQuad
        }
        public HalingMethod InhalingMethod = BreathController.InitialMethodInhale;

        void InitializeSegments(Animator anim) {
            this.Segments = new BreathController.Segment[4];
            BreathController.Segment seg;

            // spine
            seg = new BreathController.Segment();
            seg.Bone = HumanBodyBones.Spine;
            seg.transform = anim.GetBoneTransform(seg.Bone);
            if(seg.transform == null)
                seg.transform = genericSpine;
            this.Segments[0] = seg;

            // chest
            seg = new BreathController.Segment();
            seg.Bone = HumanBodyBones.Chest;
            seg.transform = anim.GetBoneTransform(seg.Bone);
            if (seg.transform == null)
                seg.transform = genericChest;
            this.Segments[1] = seg;

            // neck
            seg = new BreathController.Segment();
            seg.Bone = HumanBodyBones.Neck;
            seg.transform = anim.GetBoneTransform(seg.Bone);
            if (seg.transform == null)
                seg.transform = genericNeck;
            this.Segments[2] = seg;

            // head
            seg = new BreathController.Segment();
            seg.Bone = HumanBodyBones.Head;
            seg.transform = anim.GetBoneTransform(seg.Bone);
            if (seg.transform == null)
                seg.transform = genericHead;
            this.Segments[3] = seg;

            var originRotation = this.transform.rotation;
            this.transform.rotation = Quaternion.identity;

            InitAngleConfig(anim, this.Segments[0], this.SpineInhaleAngle, this.SpineExhaleAngle);
            InitAngleConfig(anim, this.Segments[1], this.ChestInhaleAngle, this.ChestExhaleAngle);
            InitAngleConfig(anim, this.Segments[2], this.NeckInhaleAngle, this.NeckExhaleAngle);
            InitAngleConfig(anim, this.Segments[3], this.HeadInhaleAngle, this.HeadExhaleAngle);

            this.transform.rotation = originRotation;
        }

        enum vect {forward, right, up};
        void InitAngleConfig(Animator anim, Segment segment, float inhaleAngle, float exhaleAngle) {
            var btra = anim.GetBoneTransform(segment.Bone);

            if(btra == null)
            {
                if(segment.Bone == HumanBodyBones.Chest)
                {
                    btra = genericChest;
                }
                else if(segment.Bone == HumanBodyBones.Neck)
                {
                    btra = genericNeck;
                }
                else if(segment.Bone == HumanBodyBones.Head)
                {
                    btra = genericHead;
                }
                else if(segment.Bone == HumanBodyBones.Spine)
                {
                    btra = genericSpine;
                }
            }

            var forwardDot = Vector3.Dot(transform.right, transform.InverseTransformDirection(btra.forward));
            var rightDot = Vector3.Dot(transform.right, transform.InverseTransformDirection(btra.right));
            var upDot = Vector3.Dot(transform.right, transform.InverseTransformDirection(btra.up));

            //Debug.Log("---- " + this.gameObject.name + " (" + btra.gameObject.name + ")");
            //Debug.Log("Forward Dot:" + forwardDot);
            //Debug.Log("Right   Dot:" + rightDot);
            //Debug.Log("Up      Dot:" + upDot);

            float min = 1;
            vect bestvec = 0;
            float machv;

            machv = 1 - Mathf.Abs(forwardDot);
            if (machv < min) {
                bestvec = vect.forward;
                min = machv;
            }

            machv = 1 - Mathf.Abs(rightDot);
            if (machv < min) {
                bestvec = vect.right;
                min = machv;
            }

            machv = 1 - Mathf.Abs(upDot);
            if (machv < min) {
                bestvec = vect.up;
                min = machv;
            }

            switch (bestvec) {
                case vect.forward:
                    segment.z.max = inhaleAngle * Mathf.Sign(forwardDot);
                    segment.z.min = exhaleAngle * Mathf.Sign(forwardDot);
                    segment.z.maxDuration = this.maxDuration;
                    segment.z.minDuration = this.minDuration;
                    break;
                case vect.right:
                    segment.x.max = inhaleAngle * Mathf.Sign(rightDot);
                    segment.x.min = exhaleAngle * Mathf.Sign(rightDot);
                    segment.x.maxDuration = this.maxDuration;
                    segment.x.minDuration = this.minDuration;
                    break;
                case vect.up:
                    segment.y.max = inhaleAngle * Mathf.Sign(upDot);
                    segment.y.min = exhaleAngle * Mathf.Sign(upDot);
                    segment.y.maxDuration = this.maxDuration;
                    segment.y.minDuration = this.minDuration;
                    break;
            }
        }

        private void InitializeSoulders(Animator anim) {
            this.LeftShoulder = anim.GetBoneTransform(HumanBodyBones.LeftShoulder);
            this.RightShoulder = anim.GetBoneTransform(HumanBodyBones.RightShoulder);

            if (LeftShoulder == null)
                this.LeftShoulder = anim.GetBoneTransform(HumanBodyBones.LeftUpperArm);

            if (LeftShoulder == null)
                this.LeftShoulder = genericLeftShoulder;
            
            if (RightShoulder == null)
                this.RightShoulder = anim.GetBoneTransform(HumanBodyBones.RightUpperArm);

            if (RightShoulder == null)
                this.RightShoulder = genericRightShoulder;
        }
    }
}

个人主页

本文的个人主页链接:https://baddogzz.github.io/2020/01/08/Breath-Controller/

好了,拜拜。

相关文章

网友评论

    本文标题:一个日本人写的插件:Breath Controller

    本文链接:https://www.haomeiwen.com/subject/dpayactx.html