Breath Controller
今天無意發(fā)現(xiàn)一個日本人寫的 呼吸控制器,挺好玩的,可以從他的 主頁 下載源代碼。
這個插件目前只支持 人形動畫,不過只需要簡單的幾行修改就可以支持 Generic動畫 了,文章的最后會給出代碼。
好了,二話不說,先套到我們的 小甜甜 身上看看效果:
聽輕音樂
聽搖滾
實現(xiàn)原理
Breath Controller 是程序控制的呼吸動畫,作者區(qū)分了 吸氣,呼氣,休息 三個狀態(tài),我們可以調(diào)整這3個狀態(tài)的持續(xù)時長:
代碼就是不斷地循環(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)計算,如下圖:
這里額外標注出了 左肩 和 右肩,這是因為根據(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)角度,如下圖:
這里提到的旋轉(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)該看得比較清楚:
旋轉(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,如果有,Animator 的 Update 會復(fù)位動作,所以這時旋轉(zhuǎn)角度不用減去 lastEaseValue。
最后,對于旋轉(zhuǎn)角度的插值,作者給出了不同的插值算法,并且開放了吸氣的插值算法給我們選擇:
正弦插值
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 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;
}
想象一下 深吸一口氣,是不是下圖的 正弦插值 更加合適呢,:)
和DynamicBone一起工作
Breath Controller 和 DynamicBone 一樣,都是在 LateUpdate 里去更新骨骼,如果兩者一起工作的時候,我們必須保證 Breath Controller 先更新,DynamicBone 后更新,不然 DynamicBone 就不會對呼吸生效了。
這里我們?nèi)藶榈闹付ㄒ幌履_本執(zhí)行順序即可:
非人形動畫的支持
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/。
好了,拜拜。