指数平滑化を使ったアニメーションの小技
- アニメーションに関する作業を始めて以来、ほとんどいつも使ってきたシンプルなアニメーション手法がある。
- この手法は、カメラの回転や移動、ターン制ゲーム内でのキャラクター移動、UI要素の移動、オーディオライブラリにおける音量変化の平滑化など、さまざまな場面で使われる。
- この手法は新しいものではなく、すでに聞いたことがあったり使ったことがあったりするかもしれないが、ここではいくつかの例と数学的な原理を説明する。
トグルボタン
- UIコンポーネントを作る際、たとえばトグルボタンを作ると仮定する。
- トグルボタンのスイッチ位置は状態に応じて計算され、オンなら
max_x、オフなら min_x に設定される。
- この方式はうまく機能するが、アニメーションがないとやや生気がないように見える。
- アニメーションは見た目の良さを加えるだけでなく、何が起きているのかをユーザーが理解する助けにもなる。
- トグルインジケーターを即座に新しい位置へ動かす代わりに、なめらかに移動するよう変更する。
- ここでアニメーションを更新する必要があり、これは一定速度で移動しているように見えるという欠点がある。
- そこでイージング関数を追加し、たとえば
3t^2-2t^3 や sqrt(t) のような関数を使える。
- こうしたイージング関数の違いは、アニメーションをゆっくり再生したときによくわかる。
- これで、スイッチ位置を更新する代わりにアニメーション状態を追跡しなければならなくなる。
sqrt を使う場合は、アニメーションの方向に応じて別のイージング関数を明示的に使う必要がある。
- どれがいちばん良く見えるかは好みの問題だが、
sqrt がもっとも良く見える。スイッチが非常に速く動き始め、目的地に近づくにつれてうまく減速するからだ。
- このバージョンの欠点は、もっとも単純なケースでもかなりの管理が必要であり、ユーザーがアニメーションの途中でクリックすると突然跳ねるような不連続性が生じることだ。
カメラ移動
- マップとカメラが周囲をスクロールしたり移動したりする状況を考える。
- ここでもアニメーションを追加するのが望ましい。
- 定速で補間するコードを示す。
- アニメーション完了後に発生する揺れは、
target.x - position.x が正と負の間で交互に変化するためだ。
sign(delta) の代わりに、デルタをクランプする関数が必要になる。
- この方法は、単純なことに対してかなり複雑だ。
- アニメーション速度がアニメーション完了より速いと不自然に見える。
- アニメーションが有効な間はユーザー入力を無視することもできるが、それはユーザーにとってかなり苛立たしい体験になる。
- 完璧な解決策は、もちろん指数平滑化だ。
- コードはトグルボタンの例と比べてほとんど変わらない。
内部の仕組み
1 - exp(- speed * dt) が何なのか、そしてどのように動作するのかを説明する。
- まずは単純なバージョンから始め、現在の
position と移動先の新しい位置 target の差に比例して速度を決めることで、移動速度を速くする。
- この方法は、現在位置と目標位置以外にいかなる状態も保持する必要がなく、
target が突然変わっても自動的に調整される。
- しかし小さな問題がある。
speed * dt が 1 より大きいと、位置が目標を通り過ぎてしまう。
- この問題を解決するために、値を 1 にクランプできる。
speed * dt が大きすぎる理由は、speed の値が大きすぎるか、dt が大きすぎるためだ。
- アニメーションでは、
dt を適用したときにすべてが完全に動作してくれるのが理想だ。
微分方程式 (ああ、いやだ)
- 問題を解決するための2段階のアプローチを示す。
position += (target - position) * speed * dt が小さな dt では機能するが、大きな dt では破綻するのは、微分方程式の数値解法でよくある典型的な問題だ。
- この方程式が何を解いているのかを見ていく。
position += (target - position) * (1 - exp(- speed * dt)) が、あらゆる dt に対して正しい式であることを説明する。
速度の選び方
- アニメーションは通常、継続時間の観点で考える。
- 指数の式を使うと、アニメーションは技術的には無限の時間をかけて完了する。
speed パラメータの意味は、1 / speed が position が target に e = 2.71828... 倍だけ近づくのにかかる時間だということだ。
指数平滑化
- 「指数平滑化」を検索すると、まったく無関係に見えるWikiの記事が見つかるかもしれないが、実際にはこの投稿で議論しているものと非常によく似た式が載っている。
dt が常に同じで、target が毎反復ごとに変化すると仮定すると、反復番号で値に添字を付けて position[i] = (target[i] - position[i - 1]) * factor のようなものを計算する。
最後の段落の見出し
- この投稿のアイデアは数か月前から持っていて、ようやく完成できてうれしい。
- 開発ログを見て、読んでくれてありがとう。
GN⁺の意見
- この記事は、アニメーションをなめらかで自然に見せるために使われる指数平滑化手法について説明している。この手法は、ユーザー体験を向上させ、インターフェースの直感性を高めるのに役立つ。
- 指数平滑化は、物理的な動きをシミュレートするうえでも有用であり、たとえばゲーム開発でキャラクターの動きやカメラ移動をより自然に見せるために使える。
- この手法は特に、ユーザーインターフェースの要素が状態変化を経験するとき、その変化を視覚的に表現するのに非常に効果的だ。たとえば、スライダーやスイッチの動きをよりなめらかに表現できる。
- 批判的な観点から見ると、指数平滑化はアニメーションの速度や継続時間を正確に制御しにくい場合がある。これは、デザイナーが特定のアニメーション効果を精密に調整したいときに制約となりうる。
- 類似の機能を提供する他のアニメーションライブラリやフレームワークとしては、GreenSock Animation Platform(GSAP) や anime.js などがあり、これらはさまざまなイージング関数とともに、より細かなアニメーション制御を提供する。
- 指数平滑化手法を導入する際には、アニメーションの自然さと制御の精密さのあいだでバランスを取る必要がある。この手法を選ぶことで得られる利点はユーザー体験の向上であり、欠点はアニメーションの厳密なタイミング調整が難しい可能性がある点だ。
1件のコメント
Hacker Newsの意見
1つ目のコメント要約:
smoothstep()関数ではなく、さまざまな入力を一貫して扱うステートレスな方法であることを強調している。2つ目のコメント要約:
3つ目のコメント要約:
4つ目のコメント要約:
5つ目のコメント要約:
lazy-easyという名前で同じ技術を書いたことがあると述べている。6つ目のコメント要約:
7つ目のコメント要約:
8つ目のコメント要約:
9つ目のコメント要約:
10個目のコメント要約: