Animation & Easing functions in Unity (Tween)
從 GamJam 活動得到的啟發,紀錄在 Unity 常用的程式製作插值 (Lerp) 動畫的技巧,例如控制方塊在三秒內,由紅色轉換成藍色,不用 Animator 建立 Animation Clip,而是用程式實現。
完成這項任務,通常需要這三個
- Input
- 遊戲時間,例如從現在開始到三秒後演出前述動畫
- 硬體數值,例如根據溫度感應器的數值,決定方塊是藍色或是紅色的插值顏色
- Easing function (緩動函數?),後有說明,通常沒特殊需求會是線性函數 f(x) = x
- Output
- 任何有興趣數值的插值
- Vector3,啟動與終點位置,位置插值動畫
- Color,顏色,顏色插值動畫
- Alpha,透明度,淡入淡出效果
關於 Input
最常使用遊戲時間來作為輸入,控制動畫在幾秒內演完。因此一個簡單使用 Coroutine 例子,限制在現在遊戲時間經過 duration 秒後結束:
IEnumerator Example(float duration)
{
var timeStart = Time.time;
var timeEnd = timeStart + duration;
while (Time.time < timeEnd)
{
var t = Mathf.InverseLerp(timeStart, timeEnd, Time.time);
yield return null;
}
}
輸入數值最後得根據極值 (min, max),normalized to 0-1 的數值,使用 Unity 提供的 Mathf.InverseLerp(min, max, value)
最為方便。
關於 Output
藉由上述例子,輸入數值 normalized [0, 1] 後,使用該數值 t
來做插值動畫。
位移插值,Vector3
位移變化,Vector3,從 posStart 到 posEnd:
IEnumerator Example2(float duration, Vector3 posStart, Vector3 posEnd)
{
var timeStart = Time.time;
var timeEnd = timeStart + duration;
while (Time.time < timeEnd)
{
var t = Mathf.InverseLerp(timeStart, timeEnd, Time.time);
var position = Vector3.LerpUnclamped(posStart, posEnd, t);
this.transform.localPosition = position;
yield return null;
}
this.transform.localPosition = posEnd;
}
在 Unity 所提供的 Lerp 函數,都會執行 t = Mathf.Clamp01(t)
,限制 t 值域為 [0-1],若不想經過此步驟,應使用 Unclamped 版本的 Lerp 函數。
// Clamped
transform.localPosition = Vector3.Lerp(pa, pb, t);
// No clamped
transform.localPosition = Vector3.LerpUnclamped(pa, pb, t);
旋轉插值,Quternion:
// Clamped
transform.localRotation = Quaternion.Lerp(qa, qb, t);
// No clamped
transform.localRotation = Quaternion.LerpUnclamped(qa, qb, t);
透明度插值,float:
// No clamped
var f = Mathf.Lerp(a, b, t);
// Clamped
var f = Mathf.Lerp01(a, b, t);
var c = material.color;
c.a = f;
material.color = c;
顏色插值,Color:
// Clamped
material.color = Color.Lerp(ca, cb, t);
// No clamped
material.color = Color.LerpUnclamped(ca, cb, t);
自訂插值
也可針對自訂資料結構,撰寫插值函數 (Lerp function) 使用,例如 ColorYCbCr 資料結構,並實作 Lerp:
// 定義 YCbCr
public struct ColorYCbCr
{
public float y { get; set; }
public float cb { get; set; }
public float cr { get; set; }
public float a { get; set; }
public static ColorYCbCr FromRGBColor(Color c)
{
var y = c.r * 0.29900f + c.g * 0.58700f + c.b * 0.11400f;
var cb = c.r * -0.16874f + c.g * -0.33126f + c.b * 0.50000f + 0.5f;
var cr = c.r * 0.50000f + c.g * -0.41869f + c.b * -0.08131f + 0.5f;
return new ColorYCbCr() {
y = y,
cb = cb,
cr = cr,
a = c.a,
};
}
public static ColorYCbCr Unclamped(ColorYCbCr a, ColorYCbCr b, float t)
{
return new ColorYCbCr() {
y = Mathf.Lerp(a.y, b.y, t),
cb = Mathf.Lerp(a.cb, b.cb, t),
cr = Mathf.Lerp(a.cr, b.cr, t),
a = Mathf.Lerp(a.a, b.a, t),
};
}
public static ColorYCbCr Lerp(ColorYCbCr a, ColorYCbCr b, float t)
{
t = Mathf.Clamp01(t);
return Unclamped(a, b, t);
}
public Color ToRGBColor()
{
var r = y + +(this.cr - 0.5f) * 1.40200f;
var g = y + (this.cb - 0.5f) * -0.34414f + (this.cr - 0.5f) * -0.71414f;
var b = y + (this.cb - 0.5f) * 1.77200f;
return new Color(r, g, b, this.a);
}
}
// No clamped
material.color = ColorYCbCr.Lerp(ca, cb, t);
// Clamped
material.color = ColorYCbCr.LerpUnclamped(ca, cb, t);
關於 Easing function
將上述 t
替換成 v = f(t)
,來計算差值動畫,這函數 f(t)
便是 Easing function,修改以上位移範例:
IEnumerator Example3(float duration, Vector3 posStart, Vector3 posEnd)
{
var timeStart = Time.time;
var timeEnd = timeStart + duration;
while (Time.time < timeEnd)
{
var t = Mathf.InverseLerp(timeStart, timeEnd, Time.time);
var v = f(t);
var position = Vector3.LerpUnclamped(posStart, posEnd, v);
this.transform.localPosition = position;
yield return null;
}
this.transform.localPosition = posEnd;
}
將函數 f(t)
考慮替換成以下這幾個 Easing functions:
// Linear
float LinearEase(float t)
{
return t;
}
// CubicIn
float CubicEaseIn(float t)
{
return t * t * t;
}
// CubicOut
float CubicEaseOut(float t)
{
return ((t = t - 1) * t * t + 1);
}
可以從上圖中,動畫感覺完全不同。該些 Easing function 定義不同時間時 (t
),其中改變值為多少 f(t)
。考慮物體動畫通常不會立即開始立即結束,也不會以等速改變,而是會有速度上變化。例如開啟抽屜時,一開始會快速移動抽屜,然後慢下到停止。因此選擇不同的 Easing function 會帶給不同的感覺變化。
Easing function,Robert Penner 已經有些定義,可以參考以下列表:
關於 C# Easing 實作,可以考慮:
這些實作中,參數採用 Easing(time, from, to, duration)
的形式,若套用上述的 t
,則是 Easing(t, 0, 1, 1)
。
或是自行定義 Easing function 來使用,例如使用 Cubic Bézier curves 來定義。
Tween 簡易範例
定義 DoTween Coroutine,傳入時間,傳入 Lerp 函數以及 Easing function (如果需要):
IEnumerator DoTween(float duration, System.Action lerp, System.Func easing = null)
{
var timeStart = Time.time;
var timeEnd = timeStart + this.duration;
while (Time.time < timeEnd)
{
var t = Mathf.InverseLerp(timeStart, timeEnd, Time.time);
var v = easing == null ? t : easing(t);
lerp(v);
yield return null;
}
lerp(easing == null ? 1 : easing(1));
}
使用 DoTween Coroutine 的位移範例
void Start()
{
var startPosition = Vector3.zero;
var endPosition = Vector3.one;
this.DoTransform(this.transform, startPosition, endPosition, 10);
}
void DoTransform(Transform transform, Vector3 start, Vector3 end, float duration)
{
this.StartCoroutine(
DoTween(
duration,
(v) => { transform.localPosition = Vector3.LerpUnclamped(start, end, v); }
)
);
}
結語
這是一個很簡單容易的技巧,如果問最重要的要點是什麼,Easing function,這概念值得學習記住,在網頁 CSS 中可以用,在 JQuery 可以用。
這整套方式在 Unity asset store 上找得到完整系統實作,Tween,參考列表:
沒有留言: