關於 web service, unity, blogger 等軟體工程筆記

Animation & Easing functions in Unity (Tween)

Edit icon 沒有留言
Easing functions

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 套用在位移動畫

套用在位移動畫

Easing 套用在透明度動畫

套用在透明度動畫

可以從上圖中,動畫感覺完全不同。該些 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,參考列表:

沒有留言: