はじめに
このチュートリアルでは、Principal Component Analysis=PCA (主成分分析) を使って、不安定なアニメーションやシミュレーションのジッタを取り除く方法を紹介します。PCA は、データサイエンスや機械学習の現場でデータセットの次元を削減するために用いられる統計的手法です。このチュートリアルは、Content Library 内でリリースされたプロジェクトファイルの補足として作成されたものです。ファイルは、このページからもダウンロード可能です。
このプロジェクトは、トロントで行われた SideFX 主催の Houdini HIVE Horizon イベントの際に筆者が発表したプロジェクトの1つです。講演の全編はこちらでご覧になれます:
PCA Shenanigans and How to ML | Jakob Ringler | Houdini Horizon
以下のテキストで、より具体的なセットアップの詳細を解説しています。
問題点
左がジッタが含まれる入力、右が PCA のフィルタリングをかけた結果です。

PCA とは
PCA については、Stack Exchange で以下の解説を読むことをお勧めします。筆者も、概念を理解する上で大いに役立ちました:
ソリューション
コンテンツライブラリのファイルは一見するとかなり複雑に見えますが、実際に重要なのは中央にある3つのネットワークボックスです(紫、緑、色違いの紫)。セットアップも全く同じで、唯一の違いは、何がフィードされているかです。詳細は後ほど見ていきます。

PCA のセットアップ
このセットアップのコアはわずか数個のノードで構成されており、異なる工程で PCA ノードが3回使われています。ジッタの除去とフィルタリングは通常、前後のフレームを比較し、データ中のどの部分が残すべきシグナルで、どの部分が除去すべきノイズなのかを判断することで機能します。

フレームの統合

はじめに、見るべき全てのフレームを統合します。フレーム数は奇数が理想的です(例:前フレーム x 3、現フレーム x1、後フレーム x 3 で計7フレーム)。これで前後のフレームが等数になります。For Loop からの出力時は、異なるジオメトリが互いに積み重なっており、Timeshift からアクセスできます。このノードは、現フレームとイテレーション数を元にタイムシフトを実行します。
Pseudocode:
$F- ( NumIterations / 2 ) + Iteration
コンポーネントの計算
次に、PCA にコンポーネントを計算させます。PCA に関する簡易的な解説ではほとんどの場合、各サンプルは2D 空間の単一ポイントが用いられますが、ここでは単一フレームのジオメトリを丸ごとサンプルとして用いています (上の行)。つまり、ここではサンプルが7個あります (7フレーム分)。全てのポイント位置のベクトルが含まれる長大なリストを PCA に渡し (下の行)、与えられた全てのサンプルから最も重要な情報を特定させます。

PCA ノードは一連のアトリビュートデータの読み込みのみを行い、ID アトリビュートやピースアトリビュートを処理するためのオプションを備えていないため、ユーザが各サンプルの大きさを指定する必要があります: npoints(0) / NumIterations。
PCA は、1つ以上のコンポーネントを「生成」します。これは、ブレンドシェイプのようなものだと考えることができます。コンポーネントを1つ作成すれば、すべての入力サンプルに最も適合するブレンドシェイプが1つ得られます。複数のコンポーネントを作成すれば複数のブレンドシェイプが生成され、それらをブレンドしたり組み合わせたりして特定のポーズを再構築することができます。
投影と再構築
コンポーネントが揃ったので、元のポーズを新しいサブ空間に投影し、それぞれのウェイトを算出します。複雑に聞こえるかもしれませんが、実際の作業は、ジオメトリを1番目の入力、コンポーネントを2番目の入力に繋ぐだけです。次に、モードを Project に設定し、Points per Sample をジオメトリのポイント数 (npoints(0)) に設定します。0番目のコンポーネント(その他のコンポーネントの平均)である Include Mean Weight は、必ず無効にしてください。


それらのウェイトにフォールオフ (減衰) を掛け合わせることで、順位の高いコンポーネントによる影響を軽減するかゼロにします。
float range = 1-(float)@ptnum/@numpt;
float weightdecay = chramp("decay", range);
@weight *= weightdecay;
これらの新しい修正済みのウェイトを使うことで、入力ポイントの位置を再構築できます。それには、Mode を Reconstruct に設定し、Points per Sample を再びジオメトリのポイント数に設定します。ここでも同様に、Include Mean Weights を必ず無効にしてください。
これによってポイントクラウドが生成され、そのポイントの位置を入力ジオメトリにコピーすれば、ジッタが除去されたアニメーションになります。
v@P = point(1, "P", @ptnum);
この手法が機能するのは、PCA が生成するコンポーネントが重要度、つまり最終結果への影響の大きさに応じて並べられるためです。ここではこの特性を利用し、高順位のコンポーネントを除外することで、低重要度の情報をフィルタリングします。入力データに含まれるノイズ/ジッタはフレームごとの変動が大きく、大まかな形を残す上ではさほど重要ではありません。このような低重要度の情報は、高順位のコンポーネントに記録されていると想定されるため、それらを除いてジオメトリを再構築すれば、細かな動きやノイズ、ジッタを取り除くことができます。
結果
下図の通り、大部分のジッタが除去されました。どれだけ攻めた設定にするかによって、さらに多くの動きを取り除くことも可能です。

問題もひとつあります。微細な動きや急な方向転換まで取り除いてしまうと、アニメーション自体のジッタも除去されてしまいます。特に手を叩く時に、両手が空中にとどまってしまっているように見える点が顕著です。
改善方法
その問題に対処するための回避策はいくつかありますが、それには完全なリグが必要です。今回は外部データを使用しており、キャラクタとクロスの Alembic キャッシュしか手元にありませんが、ここではそれで充分に対処できます。
ガイドによるジッタの除去
簡単な方法として、ジッタを含まず、かつアニメーションに追従するガイドメッシュをジオメトリに追加するというものがあります。今回は、最初にシミュレーションを駆動していたキャラクタメッシュをそのまま追加するだけです。次に、両方のメッシュに同時にジッタ除去を行います。その後、元のキャラクタメッシュとジッタ除去後のメッシュの差分を用いて、クロスにポイントデフォームをかけます。

こちらがクロスシミュレーションの結果です。

アニメーションを保ったまま、同程度のジッタを取り除くことができました。
rest 空間におけるジッタ除去
理想的な対処法は、rest 空間でジッタ除去を行うことです。そのためには、各ポイントにローカル座標系を作成し、さらにそれにも変形を適用する必要があります (アトリビュートパラメータが * に設定されている場合、Bone Deform がデフォルトでこれを実行します)。
Point Wrangle "apply_local_coords" のコード:
v@axis_x = set(1, 0, 0);
v@axis_y = set(0, 1, 0);
v@axis_z = set(0, 0, 1);
setattribtypeinfo(geoself(), 'point', 'axis_x', 'vector');
setattribtypeinfo(geoself(), 'point', 'axis_y', 'vector');
setattribtypeinfo(geoself(), 'point', 'axis_z', 'vector');

これで、リニアブレンドスキニングよりも複雑な変形を適用できます。今回は Wrinkle デフォーマを使用しましたが、Vellum シミュレーションなど他の方法でも構いません。ジッタが生じる可能性があるのは、この段階です。
次に、この変位を rest ジオメトリに適用します。結果として得られたジオメトリには、入力アニメーションを除いた全ての変形データが含まれています。これで、フィルタリングや処理を行いやすくなり、最後にデフォルトの Bone Deform を使って正しいポーズに戻すことができます。
Point Wrangle "extract_local_disp" のコード:
matrix3 local_from_global = invert( set( v@axis_x, v@axis_y, v@axis_z ) );
vector global_delta = point(1,"P",@ptnum) - v@P;
vector local_delta = global_delta * local_from_global;
v@P = local_delta;
Point Wrangle "apply_disp" のコード
v@P += point(1, "P", @ptnum);

この方法を実際に活用できるのは、Houdini 内にリグ (ボーン+ウェイト付スキン) が存在している、キャラクタ/リグベースの変形に限られますが、最も精確な結果を得ることができます。ただし、先に紹介したポイントデフォームによる方法も精度が高く、ほとんどのケースで十分に対応可能です。
下図を見ると、ふたつの方法 (赤 & 緑) は非常によく一致しており、初歩的な方 (青) よりも、元のアニメーションをずっとよく保持しているのがわかります。

謝辞
このプロジェクトを支えてくれた、SideFX の優秀なチームの皆さんに感謝します。
全面的なサポート: Fianna Wong
ML ツール開発: Michiel Hagedoorn
コメント
Please log in to leave a comment.