このマスタークラスでは、Houdiniで流体ソルバを構築する方法を解説します。従来の Building Fluid Solvers from Scratch と同様の考え方ですが、今回はCopernicusで行います。CopernicusはHoudini内の2D画像処理システムで、本収録で使用しているバージョンはHoudini 21です。

📎 添付ファイルは、該当のチュートリアルページからダウンロードできます。
🙇‍♂️ お詫び

画面収録が右側に数ピクセルずれてしまい、一部が欠けて見える状態になっています。このクリッピング不具合について、お詫びします。

導入:Copernicusで流体ソルバを自作する

Copernicusで流体ソルバを作っていきます。これは、昔DOPsでやった「スクラッチから流体ソルバを作る」シリーズの、続編/置き換え版みたいなものです。ただし今回は、新しい2D画像処理システムであるCopernicusの中でやり直します。
流体ソルバには大きく2タイプあります。
◯Eulerian(オイラー系):固定グリッド(格子)上の値を更新して流体を動かす。
◯Lagrangian(ラグランジュ系):粒子を流体の代表として粒子位置を動かす。
コンポジット/2D画像処理の世界では、ラグランジュ的な考え方(=粒子/点を動かす的な発想)が相性良いので、今回はそれに近い形で見ていきます。
まずGeometryノードを置き、その中にCOPネット(Copernicusノードが使える状態)を作ります。最初はシンプルに、Mandril(マンドリル)の画像を読み込みます(いわゆるサル画像)。

まずはDistortで動かし、Blockでフィードバック(反復)させる

Copernicusには最初からDistortノードがあるので、これで画像を少し変形(移流っぽく)させます。Distort量を調整すると、一定距離だけ画像が動くのが見えます。

この動きを毎フレーム積み重ねて繰り返すには、フィードバックが必要です。Houdini 21のCopernicusにはBlock(Block Begin/End)があり、ブロック内をシミュレーションとして回せます。

Blockを置いたら、ブロックの入力/出力を指定します。入力も出力も、Color(単一のカラーレイヤ)にします。

File → Distort → Block End(Result)へ接続。これで、Mandrilが少し動くネットワークができました。

次にBlockのSimulate(シミュレーション)をONにします。再生すると、ブロック内部で結果が次フレームへフィードバックされ、動きが積み重なっていきます。

角度を変えると、フィードバックの方向が変わります。途中で角度を変えると、バーがオレンジになって、今見えている結果は、最初からクックし直した結果とは一致しない(途中変更の影響が混ざっている)ことを示します。キャッシュはBlock End側で制御できます。

GPUで速い:キャッシュOFFでも最初から再クックが現実的

CopernicusはGPU上で動いていて、歪みもGPUなので、とにかく速いです。描画時間の方が重く感じるくらいです。

キャッシュを完全にOFFにしてフレームを進めると、キャッシュ無し=毎回最初から再クックになります。普通は、90フレームを毎回最初からは最悪ですが、ここでは速すぎてリアルタイムに近い感覚で90フレームを再クックして結果を即確認できます。パラメータを変えた影響が、時間をかけて効いてくるタイプの調整にすごく便利です。

さらにLive Simulationという、再生バーとは独立してずっと回り続けるモードもあります。

LiveをONにすると、ネットワークが連続でクックされ続け、角度やスケールなどを触ると即座に挙動が追従します。遊びながら調整するのに最高です。

速度場を作る:Fractal Noise → Slope Direction → DistortのDirectionへ

単に一方向へ動くだけだとつまらないので、いろんな方向へ動くようにします。

DistortにはDirection(方向)入力があり、ここにUV(2成分ベクトル)を入れられます。2Dで欲しいのはまさに2方向なので都合がいいです。

そこで、次のことを行います。

Fractal Noiseを作る。
それをSlope Directionに入れる。

Slope Directionは、傾斜に沿った方向ベクトル場を作ります(角度設定により、上り/下りや、等高線方向(横切り)を作れます)。

このベクトル場をDistortのDirectionに入れて再生すると、Mandrilが流れます……が、見た目がモザイク状の大きな塊になってしまいます。

ベクトル可視化(Visualizer)と発散(Divergence)の問題

Slope Directionの見た目が、紫/シアン/黄色っぽいのは、生データではなく、ベクトル用Visualizerが有効だからです。ベクトルは、次のように表示されます。

色相(Hue)=方向。
明るさ=大きさ(Magnitude)。

これにより、負値が全部黒になって潰れるのを防いでいます。

なぜモザイク状に固まるかを理解するため、矢印を散布(Scatter Shape)して方向を可視化(Quiver plot風)します。

矢印の向きをSlope Directionで上書きし、さらに速度の大きさ(length)で矢印サイズも変えると、次が見えてきます。

外へ向かう領域(発散)。
内へ集まる領域(収束)。

暗いところでは外へ押し出す向きになっていて、色が抜け、別の場所(収束先)へ集まって塊になります。これがDivergence(発散)です。流体っぽく流れ続けるには、これは邪魔です。

解決1:角度を90°にして等高線方向=回転へ

いまは上り方向に進む角度なので、発散/収束が出やすいです。角度を変えていくと、90°で斜面を横切る(等高線方向)になります。

山の周りを同じ高さで回るイメージで、局所的に循環(渦)になりやすく、発散/収束が減ります。

90°にして再生すると、塊で固まるのではなく、小さな渦の周りに回転するようになります。

解決2:Project Non-Divergent(非発散化投影)で発散成分を除去

もう一つの方法は、速度場を非発散(divergence-free)に投影して、発散を消してしまうことです。

ここではProject Non-Divergent(Multigrid)を使い、Periodic(ラップしているので周期境界)に切り替え、反復回数を増やすとより正しくなります。

角度0°の速度場はほぼ発散だらけなので、投影すると矢印がほとんど消える/弱くなるのが分かります。90°付近で一番残る(非発散成分が大きい)のも見えます。

運動量がない:速度自体も移流(advect)しないと渦が固定される

90°でうまく回るようになったけど、渦がその場に固定されがちで、流体らしく渦が移動する感じが弱いです。足りないのはMomentum(運動量)で、色(C)だけでなく速度(V)も一緒に運ぶ必要があります。

そのために、Blockの入出力に追加フィールド(UV=速度)を足してフィードバックします。

C(色)とV(速度)を両方フィードバックする。
さらにVもDistortで移流して更新し、次フレームへ返す。

ところが、速度を動かしていくと再び発散が出て、またステンドグラスっぽい固まり方をします。そこで、速度更新のたびにProject Non-Divergentを通して、常に非発散へスナップさせます。これで2D流体っぽい挙動になります。

2つのDistortを1つに:Cable Pack/UnpackでCとVを束ねる

色用Distortと速度用Distortで、同じ移動スケールを二重管理するのは面倒です。そこで、次のようにします。

Cable PackでCとVを束ねる。
それをDistort 1個に通す。
Cable Unpackで戻す。

こうしてDistortを1個に統一し、非発散化(Project Non-Divergent)は維持します。

力を入れる:青成分に浮力、Drag(抵抗)で減衰

Mandrilの青い部分が上へ行く、みたいな力を入れます。

CからBlueチャンネル抽出。
それを使って速度Vへ一定量加算(Bright/Add ConstantでShift)。
Scale by time stepをONにして時間刻み依存を整える。

速すぎるのでDrag(減衰)も追加します。Brightで速度をスケールダウンし、適切な値に調整します。キャッシュOFFで特定フレームまで一気に再クックし、36フレーム後どうなるかなど、時間スケールの見た目を見ながら調整できるのが強みです。

解像度が違っても動く:C=256、V=1Kでも暗黙サンプリング

面白い点として、Cフィールドが256×256、Vフィールドが1K×1Kのように解像度が違っても、Copernicusは暗黙にサンプリングして整合させてくれます。

逆に、入力画像をResampleで4倍にするとキャッシュOFFなら即再クックして、よりシャープな結果が得られます。2Dは高解像度にして、シャキッとした見た目を取りやすいです。

また、速度場(ノイズ)側を低解像度、テクスチャ(色)側を高解像度にして、粗い速度で細かい絵を運ぶ、といった組み合わせもできます。

BFECC(後進・前進誤差補償)でにじみを戻す

Distort(移流)は、周囲4ピクセルの補間でサンプルするので、少しずつぼけます。このぼけが毎フレーム蓄積して、最終的に泥っぽくなります。

そこでBFECC(Back and Forth Error Compensation and Correction)的な手順で誤差を推定して補正します。

手順はざっくり次の通りです。

前進(forward)で1ステップ移流する。
同じ速度で逆向きにして後退(backward)で戻す。
元と一致しない差が誤差なので差分を取る。
その誤差を前進させて補正項にする。
だいたい50%かけて足し戻す。

これで長時間でもシャープさが保たれます。ただし近似なので、負値が出たり境界で破綻が出ることがあります。本来はローカルクランプが正道ですが、ここでは簡易にグローバルClampを使います。

統計(Statistics)で元画像のmin/maxを取って、最終結果をその範囲にClampし、極端な破綻を抑えます(完璧ではないが改善はする)。

ソースを追加するパートへ:密度/温度/速度(D, T, V)構成に

次はソース(発生源)を足します。複雑なBFECC部分はいったん外して、シンプルな更新構造にします。

RGBのCではなく、Density(密度)/ Temperature(温度)/ Velocity(速度)の構成に切り替えます。Blockの入出力を次にします。

Density(Mono)。
Temperature(Mono)。
V(UV)。

Cable Pack/Unpackも3本対応にします。
温度で浮力を駆動したいので、浮力の元をTemperatureへ接続します。

最初のソースとしてSDF Shape(∞マークなど)→ SDF to Monoを作り、初期温度として入れると、煙/炎っぽく上へ立ち上がります

ここで重要です。ソースをブロック内に置くと、ファイル読み込み等のコンパイル不能要素が問題になる場合があります。そこでpassthrough(外部入力)を使って、ソースをブロック外でクックして毎フレーム流し込みます

密度は、移流後の密度に対してソースを最大値合成(Maximum)し、毎フレーム足します。密度は線形減衰(一定量ずつ減る)、温度は乗算的減衰(比率で冷える)にして、温度を寿命(age)として使えるようにします。温度をMono→RGB→Ramp(黒→オレンジ等)で炎色にすると、炎っぽく見せられます

SDFをOutlineにして厚みを上げれば、輪郭から炎が立つ演出もできます。Time Scaleで再生速度も変えられます

渦拘束(Vortex Confinement)で渦の細部を戻す

非発散化は重要ですが、同時にスピン(渦)も削ぎ落としがちです。そこでVortex Confinementを入れて渦を持ち上げます

やることは次の通りです。

速度場のDerivative(空間微分)を取り、2×2の偏微分行列を得る。
2Dのcurl(渦度)相当を作る(例:∂Vy/∂xと∂Vx/∂yの差)。
そのcurlの大きさの勾配(Slope Direction)を取る。
2Dではcurl方向は画面手前/奥なので、勾配に対して90°回す(cross-slope)。
Normalizeして、curlの大きさ(abs)でスケールする。
それを速度へ加算する(小さく。大きいと爆発)。

さらに、速度場を事前にBlurしてから微分すれば、どのスケールの渦を強調するかをコントロールできます。小さなBlur=細かい渦、大きいBlur=大きい渦です。

これを炎のネットワークに適用すると、縁に細かい動きが増えて、生っぽい炎になります。

ソースを荒らす:アニメ付きFractal Noiseで発生をランダム化

ソースが滑らかすぎると、どうしても層流っぽくなります。そこでソース自体をノイズで荒らします

Fractal Noiseを3D化し、AnimateをONにする。
Pulse lengthを短くして速く動かす。
Element sizeを小さくして細かくする。
それをソース(Mono)にMultiplyしてコントラスト調整する。

これで発生がランダムになり、炎の質感が増します

2D→3Dへ:CopernicusでVDB(疎ボリューム)を扱う

次は3Dへ進みます。Copernicusは画像処理ですが、Houdini 21では3D(ボリューム)処理にも拡張され、主にVDB(疎ボリューム)を扱います。

SOPでVDBを作り、COP側でGeometry to VDBでGPUへ持ち込みます。

2DレイヤからVDB from Layerで書き込もうとすると、最初はうまくいかない場合があります。原因はVDBの内部が、一定値のタイル(active tiles)として格納されていて、GPUがオンデマンドで葉ノードへ展開できないためです。そこでDensifyをONにして強制展開すると、書き込み可能になります(ただしメモリは増える。読むだけなら不要)。

2Dレイヤは無限に押し出された形(円→無限円柱)になるので、Transform 3Dで回転した円柱をMinで合成すると、交差したそれっぽい形が作れたりします。面白いのは、MinやBlurなど、2Dでのピクセル演算が、そのまま3Dのボクセル演算としてVDBにも効く点です(専用のVDB Minが要らず、普通のMinimumが効く)。

さらに球を作りたいなら、Position Map(座標ベクトルVDB)を作り、中心点からの距離(length)を取り、Remapを反転して閾値調整すれば球状密度が作れます中心点はピボットで動かせます

3D移流(自作Advect):Position Map + Velocity*dt → Position Sample

VDBを動かすには、2DのDistort相当が欲しいが、ここでは自分で作る流れを見せます

基本は次の通りです。

Position Map(各ボクセルの座標)を作る。
そこに速度*dtを加減算して、新しい参照位置を作る(dtは1/24など)。
Position Sampleで、元の密度を新しい位置からサンプルする(これが移流)。

Blockにして、DensityとVelocityをフィードバックし、DensityもVelocityも同じ方法で移流して更新します。符号が逆で動きが逆になるなら、加算/減算を入れ替えて修正します。

浮力、そして3Dも非発散投影が必須

初期速度をゼロにし、浮力(上向き加速)を速度へ加算します。 速度の可視化を見ると、時間とともに上向き速度が増えていきます

しかし密度を見ると、上へ移動はするが、新しい領域がうまく埋まらない/下が空になるなど、問題が出ます。原因はやはり非発散でない速度場です。 そこで3D用のPyro Project Non-Divergent(Electrostatic)を使って速度を補正します。

補正後は速度に循環が生まれ、煙がそれらしく上昇します。 反復回数など品質と速度のトレードオフ設定もできます。

VDBの疎を活かす:必要な葉だけアクティブにして追従させる

いまは密度が小さな煙でも、大きな立方体全体を計算していて無駄が多いです。 VDBの疎性を活かします。

VDB Leaf Points:葉(leaf)ごとにポイントを作る。
VDB Activate From Points:そのポイントが入っている葉をアクティブ化して、新しいトポロジーを作る。

ただし、どの葉が必要かを決める必要があります。 密度がゼロに近い葉は不要です。理想は葉内の512ボクセルを調べることですが、ここでは簡易に密度をBlurして、近傍に密度があれば1になるようなマスクを作りOpenCLで各ポイント位置の密度をサンプルして、閾値以下ならinvalid=1にして寄与させないようにします(Activate From Pointsはinvalid属性を見て無効化できる)。

新トポロジーに合わせてVDB Reshapeで密度/速度を同じ疎構造に揃えます。最初は上昇とともに消えてしまう(上に新しい葉が無い)ので、Activate側でLeaf Dilationを増やし、進行方向に余裕を持たせると、前方をアクティブ化しながら追従できるようになります。 さらにClip範囲で画面外を切れば、無駄計算も減らせます

別ソース例:Snailモデルから発生させ、色付き煙として合成

ネットワークを複製し、今度はディスクからSnailモデルを読み込みFill Connectedで一部を選びVDB from LayerとTransform/Rampで薄い帯状の密度ソースを作ります

Blockにsource(float VDB)の追加入力を作り、毎フレーム密度に加算(Brightでshift=1、Scale by time step)して連続発生させます

レンダリングはRasterize Volumeで、Diffuse ColorをRamp(infraredなど)で色付けし、元のSnailを背景にOverで重ねます密度が弱ければ加算量を上げます

ノイズを足す:ソース密度ノイズ+ランダム速度(Disturbance的)

まずソース密度にFractal Noise 3Dを掛けてパルスを荒らします(アニメは \(\TeX\) などで駆動)。

次に速度へノイズを入れたいので、Random RGBで-1〜1のランダム方向を作ってVDB from LayerでベクトルVDB化し、Blockに速度用source入力を追加します。

ソース領域ではBlendで、初期速度をランダムに置き換える。
あるいは密度の反転マスクを使い、空気側へAddでノイズを足す(Disturbance的)。

強すぎると荒れすぎるので、かなり弱くダイヤルします。

炎の見せ方としては、RasterizeをFireにし、Emission/Colorを調整し、密度側にRampで中抜きを作って中空っぽい炎にすることもできます。
ボクセルサイズを小さくすれば解像度が上がり、見た目が一気にリッチになります。

まとめ:高レベルノードもある(Pyro Configure Fire)

もちろん、ここまで深く自作しなくても、Copernicusには高レベルのPyro系ノードが用意されています。 たとえばPyro Configure Fireを置けば、それらしいセットアップが得られ中身を見ると今日やった原理に近い構造で組まれているのが分かります。

これで、以上となります。 こういう中身の仕組みを掘ってみたり、Copernicus内でVDBを触って遊ぶと、面白い表現がいろいろ作れるはずです。 ありがとうございました。