💬

自然なまばたきのアニメーションをプロシージャルに生成する

2023/04/01に公開

まばたき(Blink)の自然なアニメーションをプロシージャルに生成する方法の考察です。

まばたきのアニメーションはもちろん通常のアニメーションデータとして作成することも可能ですが、細かいパラメータを動的に調整したり、乱数を組み込んだりとカスタマイズしたい場合にはプロシージャルに、つまり何かの関数を用いてアルゴリズム的にプログラムで生成したい場合もあります。

本記事は後者を目的とするものです。

まばたきの動きのモデル化

まばたきは閉じる時の動きと開く時の動きが異なるため、それぞれ別の関数で近似を行うことを考えます。

下記のような振る舞いをする近似関数を求めることを期待します。

  • 閉じる時 -> 指数関数で急激に開く
  • 開く時 -> 二次関数で緩やかに開く

参考になる良い資料がぱっと出てこなかったのですが、こちらのEye movementのような動きをイメージしています。

https://jins-meme.github.io/sdkdoc2/basics/feature.html

近似関数を、時刻tを[0, 1]にスケールし、t = t_cで目を閉じるとして、それぞれパラメータを用いて下記のように仮定してみます。

\begin{align} f(t) = \begin{cases} \alpha e^{ \beta t} - \gamma \ & \ (0 \le t \le t_c) \\ - a (t - t_c)(1 - t) + b (1 - t) + c \ & \ (t_c \le t \le 1) \end{cases} \end{align}

それぞれのパラメータは正と仮定しておきます(そうなるよう符号を置いておきます)。

開く時の近似関数のパラメータの置き方が一見奇妙に見えるかもしれませんが、これは境界条件の都合ですのでいったん気にしないでください。

まばたきの動きは、

  1. 開いている
  2. 閉じる
  3. また開く

という流れですので、これを境界条件として式で表現すると下記になります。

\begin{align} \begin{cases} f(0) &= 0 \\ f(t_c) &= 1 \\ f(1) &= 0 \end{cases} \end{align}

この条件の元でパラメータを絞ってみましょう。

閉じる時の近似関数

まずは、閉じる時の近似関数に境界条件を適用します。

\begin{align} f(0) &= 0 = \alpha - \gamma \\ f(t_c) &= 1 = \alpha e^{\beta t_c} - \gamma \end{align}

簡単な連立方程式を解くと、

\begin{align} \alpha = \gamma = \frac{1}{e^{\beta t_c} - 1} \end{align}

となりますのでこれを代入すると、閉じる時(0 \le t \le t_c)の近似関数は、\betat_cをパラメータとしたtの関数

\begin{align} f(t) = \frac{e^{\beta t} - 1}{e^{\beta t_c} - 1} \end{align}

と表現できます。

開く時の近似関数

開く時の近似関数にも境界条件を適用してパタメータを絞ってみましょう。

\begin{align} f(t_c) &= 1 = b (t_o - t_c) + c \\ f(1) &= 0 = c \end{align}

パラメータの置き方のおかげで計算が楽に済みます。

\begin{align} b &= \frac{1}{1 - t_c} \\ c &= 0 \end{align}

これを代入すると、開く時(t_c \le t \le 1)の近似関数は、at_cをパラメータとしたtの関数

\begin{align} f(t) = - a (t - t_c)(1 - t) + \frac{1 - t}{1 - t_c} \end{align}

と表現できます。

結果

最終的なまばたきの動きの近似関数は、\beta \ge 0at_cをパラメータとしたtの関数

\begin{align} f(t) = \begin{cases} \frac{e^{\beta t} - 1}{e^{\beta t_c} - 1} \ & \ (0 \le t \le t_c) \\ - a (t - t_c)(1 - t) + \frac{1 - t}{1 - t_c} \ & \ (t_c \le t \le 1) \end{cases} \end{align}

として表現することができます。

これを、

\begin{align} \beta &= 10, \\ a &= 1, \\ t_c &= 0.2 \end{align}

としてプロットしたのがこちらのグラフです。

当初の期待通り、

  • 閉じる時 -> 指数関数で急激に開く
  • 開く時 -> 二次関数で緩やかに開く

のような振る舞いが再現できることが分かると思います。

注意が必要なのは、パラメータのaは[-1, 1]の範囲に設定しないと縦軸の値が[0, 1]の範囲からはみ出てしまいます。

当初の参考のグラフと比べると大まかには再現できていますが、目を開く時の関数形はもう少し滑らかなものにしてもいいかもしれませんね。

https://jins-meme.github.io/sdkdoc2/basics/feature.html

フレームレートを考慮した現実的な時間間隔

Unityなどのゲームエンジンの標準的なフレームレート(1秒間当たりのフレーム数)を60と仮定すると、フレーム当たりの時間は、

\begin{align} dt = 1 / FPS = 1 / 60 s = 0.01666 s = 16.66 ms \end{align}

Wikipediaによると

1回のまばたきの速さは平均で100 - 150ミリ秒だと言われている。

とのことなので、おおよそ10フレーム分くらいになります。

パラメータにも依りますが、10フレーム分だと閉じる間の近似関数でわざわざ計算コストの高い指数関数は使わなくても良いかもしれないですね。

もし計算コストが気になる場合は適宜簡略化してみていただければと思います。

乱数を使用した自然なまばたきのアニメーションの生成

より自然なまばたきの挙動を再現するために、まばたきのスピード(時間)と間隔に不規則性を持たせることを考えます。

スピードは要するに時間軸のスケールで調整ができます。

乱数を用いてスピードと間隔をある程度分散する際には、一様乱数よりは正規分布に従う乱数の方が自然になりそうという仮定をします。

また、まばたきの間も全く目が動かないと少し怖いので、小さく周期的な振動を加えてみます。

するとまばたきのアニメーションの生成の流れを下記のように考えることができます。

  1. パラメータ\betaat_cを指定します
  2. フレームレート(1秒あたりのフレーム数)を指定します
  3. 一回のまばたきにかかる時間を正規分布に従う乱数で生成し、その時間でスケールされたまばたきのアニメーションを、フレーム分割した各時刻で計算します
  4. 次のまばたきまでの間隔の時間を正規分布に従う乱数で生成し、正弦波をフレーム分割してアニメーション生成をします
  5. 4~6を繰り返して、一定時間分のまばたきのアニメーションを作成します

パラメータ\betaat_cは本来なら実験での観測値に対してフィッティングをして決めるのが理想ではありますが、研究ではないのでグラフを描きながらいい感じのパラメータに調整します。

Unityにおける実装

ライブラリとしては未完成ですが、こちらで実装をしています。

https://github.com/mochi-neko/facial-expressions-unity/blob/main/Assets/Mochineko/FacialExpressions/Blink/ProbabilisticEyelidAnimationGenerator.cs

https://github.com/mochi-neko/facial-expressions-unity/blob/main/Assets/Mochineko/FacialExpressions/Blink/BlinkFunction.cs

VRMのモデルに適用した例

今回のアルゴリズムをVRMに適用した例はこちらです。

https://twitter.com/mochi_neko_7/status/1642150855200894976?s=20

比較として、一次関数で表現、かつ一様乱数を使用した例も作ってみました。

https://twitter.com/mochi_neko_7/status/1642151133287419904?s=20

発展

開く時の近似関数はもう少しなだらかなものを使用してみても良いかもしれません。

逆三角関数系か、シグモイド関数とか。

今回は平常時のまばたきの挙動を再現しましたが、ウィンクや任意の表情操作などと排他制御をするならStateMachineと組み合わせると良さそうです。

あとは感情の状態によってまばたきの頻度や速度を調整するのも面白そうです。

乱数で生成しているパラメータを調整すれば簡単に、かつリアルタイムにアニメーションが生成できるので、今調整しているLipSyncが一段落したら試してみようと思います。

Discussion