一個日本人寫的插件:Breath Controller

Breath Controller

今天無意發(fā)現(xiàn)一個日本人寫的 呼吸控制器,挺好玩的,可以從他的 主頁 下載源代碼。

image

這個插件目前只支持 人形動畫,不過只需要簡單的幾行修改就可以支持 Generic動畫 了,文章的最后會給出代碼。

好了,二話不說,先套到我們的 小甜甜 身上看看效果:

聽輕音樂

image

聽搖滾

image

實現(xiàn)原理

Breath Controller 是程序控制的呼吸動畫,作者區(qū)分了 吸氣呼氣,休息 三個狀態(tài),我們可以調(diào)整這3個狀態(tài)的持續(xù)時長:

image

代碼就是不斷地循環(huán)這3個狀態(tài)以模擬 呼吸動畫

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根骨骼的旋轉(zhuǎn)計算,如下圖:

image

這里額外標注出了 左肩右肩,這是因為根據(jù)骨骼的父子關(guān)系,脊椎 或者 的運動也會帶動 肩膀 的運動,作者不希望 肩膀 受到呼吸的影響,所以這里在計算完呼吸的運動后會對 肩膀 做一個復(fù)位操作,偽代碼大致如下:

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

    // Rotate Spine, Cheast, Neck, Head
    // TODO: 旋轉(zhuǎn)脊椎,胸,頸,頭        

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

好了,下面看一下 旋轉(zhuǎn)骨骼 的實現(xiàn)細節(jié)。

作者給出的旋轉(zhuǎn)參數(shù)不多,最主要的是每根骨骼 吸氣呼氣 的最大旋轉(zhuǎn)角度,如下圖:

image

這里提到的旋轉(zhuǎn),作者用了 Transform.Rotate 這個函數(shù):

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).

這里用 歐拉角 來描述旋轉(zhuǎn),并且旋轉(zhuǎn)只會繞某一個軸進行。程序在初始化的時候會按照 骨骼朝向和角色朝向的匹配程度 來確定旋轉(zhuǎn)方向,從而確定旋轉(zhuǎn)軸。

關(guān)于旋轉(zhuǎn)軸,下圖應(yīng)該看得比較清楚:

image

旋轉(zhuǎn)的核心代碼如下:

// 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++;
    }
}

代碼比較簡單,唯一要注意的是這里區(qū)分了是否有 Animator,如果有,AnimatorUpdate 會復(fù)位動作,所以這時旋轉(zhuǎn)角度不用減去 lastEaseValue。

最后,對于旋轉(zhuǎn)角度的插值,作者給出了不同的插值算法,并且開放了吸氣的插值算法給我們選擇:

正弦插值

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 就不會對呼吸生效了。

這里我們?nèi)藶榈闹付ㄒ幌履_本執(zhí)行順序即可:

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/。

好了,拜拜。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容