🍮

ぽよぽよ動くスタンプを作りたい[Shape Matching法]

に公開
3

こんにちは!先端技術開発グループ(WAND)の金田です。
今日から12月ということで、エクサウィザーズのエンジニアによるアドベントカレンダーを開催します。

ぜひ、クリスマスまでエクサウィザーズのアドベントカレンダーにお付き合いいただけると嬉しいです。
以下のようなメンバーで開催します。明日からの記事も、どうぞお楽しみに!

https://adventar.org/calendars/12259

初日のアイスブレイクは私金田が担当します。
私は普段業務やプライベートでteamsやslackを使用しています。
そこで多くの人がより楽しく業務を行うために行なっているのがスタンプのカスタムだと思います。
今はいろんなサイトがあり、文字のスタンプを簡易的に作成できるものやそれにいろんなエフェクトをつけてgifをつけられるものがあります。
私のお気に入りのサイトでは、入力画像に対してぽよぽよとした動き、縦横方向の単振動をつけてくれるものがあります。
これはそのサイトで作成したもので
alt text
可愛くていいなーと思ったので、私もこれを作ってみることにしました。

このようなぽよぽよとした動きを再現する物理シミュレーションは、過去に研究で扱ったことがあり、馴染み深い技術でもあります。今回はその経験を活かして、この ぽよぽよ した動きを実装しました。

弾性体シミュレーションとShape Matching

ぽよぽよ とした動きを実現するために、今回は簡易的な弾性体シミュレーションの手法を取り入れました。
弾性体とは、簡単にいうと、力を加えると変形し力を取り除くと元の形に戻ろうとする物体のことです。
この性質をシミュレーションすることで、柔らかい物体の動きを再現できます。

今回はMatthias Müllerらが2005年に発表した論文で提案されたShape Matchingという弾性体のシミュレーション手法を採用しました。
このモデルではFEM(有限要素法)のような厳密なエネルギーベースではなく、幾何的な制約で変形を再現しています。

Müller, M., Heidelberger, B., Teschner, M., & Gross, M. (2005). Meshless deformations based on shape matching.

この手法の大きな利点は、メッシュレスなアプローチであり、実装が比較的シンプルでありながら、リアルタイムで安定した動作が期待できる点です。

今回の実装では、柔軟な変形を表現するために、画像を小さな領域(クラスタ)に分割し、それぞれのクラスタに対して個別にこのShape Matchingを適用しています。

alt text

なぜ画像を分割するのか?

もし、画像全体をひとつの塊としてShape Matchingにかけてしまうと、画像全体に対して計算されるアフィン変換(変形勾配+並進)が1つだけになってしまうため、全体が均一に伸び縮みしたり回転したりするだけで、局所的な変形(曲がったり波打ったりする動き)が表現できません。これでは、目指しているぽよぽよ感が出ません。

そこで今回は、画像を全体として扱うのではなく、各点とその周囲(3x3の格子点)を小さなクラスタとして扱い、その小さな領域ごとにShape Matchingを行いました。

こうすることで、 局所的な変形 は許容しつつ 元の形に戻ろうとする 挙動が生まれ、全体として柔らかい弾性体のような動きを実現しています。

Shape Matchingの基本アイデア

Shape Matching法の基本的な考え方は以下のようになります。

  1. 変形する前の元の形(Rest Shape)を記録
  2. シミュレーションの各ステップで、バラバラに動いた点群の変形(Deformed Shape)を取得
  3. 現在の点群の形に最も近い元の形の向きとスケールを計算
  4. 各点を、その求められた Rest Shape に少しだけ引き戻す力を与える

この 引き戻す 処理が、物体が元の形を保とうとする力(弾性力)として働き、結果として弾性体のような挙動が生まれます。

アルゴリズムの解説

もう少し具体的に、数式を交えてアルゴリズムを見ていきましょう。

  1. 点群の設定
    まず、画像を格子状の点の集まりとして表現します。各点を質点とし、それぞれの質点に初期位置 \boldsymbol{q}_i と質量 m_i を設定します。これが変形なしの元の形です。各フレームでの各質点の位置を \boldsymbol{p}_i とします。

  2. 重心の計算
    元の形と変形後の形、それぞれの重心を計算します。

    \boldsymbol{q}_{cm} = \frac{\sum_i m_i \boldsymbol{q}_i}{\sum_i m_i}, \quad \boldsymbol{p}_{cm} = \frac{\sum_i m_i \boldsymbol{p}_i}{\sum_i m_i}
  3. 最適な回転行列の計算
    変形後の点群と、回転させた元の形状との距離の二乗和を最小化するような回転行列 R を求めます。
    この最小二乗問題の解は、以下の行列 A_{pq} を極分解した際の回転成分として得られることが知られています。

    A_{pq} = \sum_i m_i (\boldsymbol{p}_i - \boldsymbol{p}_{cm}) (\boldsymbol{q}_i - \boldsymbol{q}_{cm})^T

    そこでまずこの A_{pq} を計算し、極分解 A_{pq} = RS を行うことで回転行列 R を抽出します。

  4. 目標位置の計算
    抽出した回転行列 R を使って、各質点が本来あるべき 目標位置 \boldsymbol{g}_i を計算します。

    \boldsymbol{g}_i = R(\boldsymbol{q}_i - \boldsymbol{q}_{cm}) + \boldsymbol{p}_{cm}

    これは、元の形を現在の重心周りで回転させたものです。

  5. 体積保存の考慮(オプション)
    単純なShape Matchingだけでは、押しつぶされたときに体積が潰れてしまうことがあります。
    そこで、剛体としての回転行列 R に加えて、体積を保ちながらの変形を許容する行列 L も計算し、それらをブレンドすることで質感を調節します。

    まず、現在の点群に最もフィットする線形変換行列 A を計算します。これは**変形勾配(Deformation Gradient)**と呼ばれます。

    A = A_{pq} A_{qq}^{-1}

    ここで A_{qq} は、以下のように定義される元の形状に関する行列です。

    A_{qq} = \sum_i m_i (\boldsymbol{q}_i - \boldsymbol{q}_{cm}) (\boldsymbol{q}_i - \boldsymbol{q}_{cm})^T

    この A の行列式(面積拡大率)が1になるように正規化したものを L とします。

    L = \frac{1}{\sqrt{\det(A)}} A

    最終的に、回転行列 R とこの L を合成して目標位置を計算します。

    T = (1 - \alpha) R + \alpha L

    ここで \alpha はブレンド率です。この値を調整することで、元の形状に戻ろうとする剛体に近い性質と、体積を保ちながら自由に変形するバランスをコントロールできます。

実装コードのイメージ

実際のC++コードでは、各クラスタに対して以下のような処理を行っています。数式で求めた A_{pq} から回転 R を取り出し、目標位置を計算し、そこに向かう力を速度に加えることで弾性体のような動きを作っています。

また、位置の更新には陽的(Explicit)な積分法を用いています。Shape Matching法は幾何学的な制約に基づいて変形を扱うため、陰解法のような複雑な行列計算を必要とせず、陽的な手法で高速に計算できるのが特徴です。

// --- Step 1: Shape Matchingによる目標位置の計算 ---
for (const auto& cluster : clusters) {
    // 重心の計算
    Vec2 curCom  = computeCOM(cluster, currentPos);
    Vec2 restCom = computeCOM(cluster, restPos);

    // Apq から極分解で回転行列 R を抽出
    Mat2 Apq = calculateApq(cluster, curCom, restCom);
    Mat2 R   = polarRotation(Apq);

    // 変形勾配 A を計算し、体積保存変形 L を求める
    Mat2 A = calculateDeformationGradient(Apq, Aqq);
    Mat2 L = normalizeDeterminant(A);
    Mat2 M = blendTransforms(R, L, volumeBlend);

    // 目標位置の計算
    for (int idx : cluster) {
        Vec2 q = restPos[idx] - restCom;            
        Vec2 target = curCom + mulMatVec(M, q);     
        
        // 複数のクラスタの結果を足し合わせる
        accumPos[idx] += target;
        accumCount[idx] += 1.0f;
    }
}

// --- Step 2: 物理量の更新 ---
// 計算した目標位置に向かうように速度・位置を更新
for (int i = 0; i < nodeCount; ++i) {
    if (accumCount[i] > 0) {
        Vec2 goal = accumPos[i] / accumCount[i]; // 平均目標位置
        
        // 目標位置に向かう力を速度に加える
        Vec2 diff = goal - nodes[i].pos;
        nodes[i].vel += diff * (stiffness / dt);
    }

    // 重力などの外力を加算
    nodes[i].vel += gravity * dt;
    
    // 速度に基づいて位置を更新
    nodes[i].pos += nodes[i].vel * dt;
}

この一連の処理をシミュレーションの各ステップで繰り返すことで、画像全体がまるで一つの弾性体であるかのようにぽよぽよと動くアニメーションが生成されます。

alt text

こちらが結果になります。

alt text
alt text
alt text
alt text
alt text
alt text
alt text
alt text

まとめ

このように、Shape Matching法を用いることで、比較的単純なアルゴリズムでリアルな弾性体の動きをシミュレーションできます。物理シミュレーションと聞くと難しく感じるかもしれませんが、このようなテクニックを使えば、楽しくて魅力的な表現が実現できる良い例だと思います。

ぜひ皆さんも、身の回りのスタンプを楽しくぽよぽよ可愛くしてみてはいかがでしょうか。

alt text
※技術検証のための演出です

エクサウィザーズ Tech Blog

Discussion