| On this page |
Abelian Sandpile Model(アベリアン砂山モデル) は、自己組織化ダイナミカルシステムの例であり、1987年に発表されました。 このシステムは、砂山が不安定になり斜面を滑り落ちる際にルールを適用します。 この処理は砂山の傾斜に依存しています。
このモデルは2次元の手法であるため、HoudiniのHeight Fieldに完全に適しています。
2Dメソッドを定式化したら、地形のheightレイヤを更新することができます。
Solver SOPを活用することで、砂がその砂山から滑り落ちて、その側面を侵食する様子を模倣することができます。
滑り落ちる砂を模倣する基本スクリプトには、2つのHeightField Wrangle SOPが必要になります。
1つ目のHeightField Wrangle SOPは、地形のボクセルをループして、上側、下側、左側、右側の隣接ボクセルを検索します。
その後、ランダム関数を用いて滑り方向を決定します。
砂を受け入れるボクセルとしてソルバが下側の隣接ボクセルを選んだと仮定します。
これは、下側のボクセルの
height情報を更新する必要があることを意味します。
しかし、VEXでは隣接ボクセルに直接書き込みをすることが許可されていないため、この更新は不可能です。
その解決策として、この情報を別のレイヤに書き込んで、2つ目のHeightField Wrangle SOPでその値を読み込んで、現在処理中のボクセルにその値を適用します。
Note
このVEXサンプルの主な目的は、より高度な独自の解決方法を紹介することです。 ただし、Abelian Sandpile Model(アベリアン砂山モデル)の深追いはしません。 その詳細な解説はインターネット上に優れた資料が多数存在します。 このスクリプトにはコメントが付けられており、各コードブロックで何が実行されるのかを説明しています。
地形 ¶
砂山の性質上、地形は過度に大きくするべきではありません。 約100m×100mのサイズが自然な限界となりますが、物理的な障壁ではありません。 もちろん大規模な地形でもソルバを実行することができますが、シミュレーション時間が伸びたり(場合によっては)予期せぬ結果が生じる可能性があります。 大規模地形では、シミュレーション時間を短縮するために、HeightField SOPの Grid Spacing 値を高く設定することを推奨します。
ソルバは高低差が大きいほど良好な結果を得られます。 デフォルトで比較的平坦な地形では変化がほとんど見られません。 一方、急勾配の丘陵では面白い段丘効果も得られます。
シーンのセットアップ ¶
まず基本的なHeight Fieldセットアップから始めます。
HeightField Distort by Noise SOPを活用することで、地形の見栄えを向上させ、ディテールを高めることができます。
HeightField SOPの Size パラメータには、30から100までの間で値を選択してください。
HeightField Noise SOPによって、地形を形成することができます。
eightField Noise SOPの Amplitude パラメータは、地形の高さを制御し、 Element Size パラメータは“スパイク”の量を調整します。
Offset パラメータを使用してフラクタルパターンをシフトさせると、異なる景観を生成することができます。
次に、HeightField Copy Layer SOPを追加します。
このノードでは、砂の方向値を収めるレイヤを作成します。
このノードの Destination フィールドに
deltaを設定します。
たいていの場合、シミュレーション後にこのレイヤを除去したいので、HeightField Layer Clear SOPを配置して、そのノードを最後のノードにします。
Layer 1 フィールドに
deltaと入力します。
ソルバのセットアップ ¶
Solver SOPを配置し、 1番目 の入力をそのHeightField Copy Layer SOPの出力に接続し、そのHeightField Layer Clear SOPの入力をそのSolver SOPの出力に接続します。
このSolver SOPは、指定された Sub Steps の数だけシミュレーションを実行します。
この数は、1フレームあたりに地形に対してWrangleが適用される回数を決定します。
Abelianソルバの場合、5程度の値が良い結果をもたらします。
砂の輸送速度を遅くまたは速くしたい場合は、この値を減らすか増やしてください。
次に、このSolver SOPをダブルクリックして中に入って、HeightField Wrangle SOPを2つ追加します。
1つ目のHeightField Wrangle SOPの 1番目 の入力をPrev_Frameノードの出力に接続し、2つ目のHeightField Wrangle SOPの 1番目 の入力をその1つ目のHeightField Wrangle SOPの出力に接続し、OUTノードの入力をその2つ目のHeightField Wrangle SOPの出力に接続します。
これで動作させるソルバのセットアップが完了しました。次にコードを追加します。
方向を決めるWrangle ¶
1つ目のHeightField Wrangle SOPでは、ソルバ調整用のカスタムパラメータを含めます。
以下のスクリプトは、砂の移動方向を決定し、その方向をdeltaレイヤに書き込みます。
この問題は、Height Fieldレイヤにはベクトル値を格納できないことです。
そのため、あり得る8つの方向を数値で表現する必要があります。
砂の輸送制御に必要なパラメータもDetailアトリビュートとして格納します。
以下のコードを⌃ Ctrl + Cでコピーします。 そのコードを 1つ目 のHeightField Wrangle SOPの VEXpression フィールドに⌃ Ctrl + Vでペーストします。
//============================================================================ // 侵食方向Wrangle // 勾配とユーザ定義の閾値に基づいて砂が滑る方向を決定します。 // これは、侵食/堆積サイクルにおける第1工程です。 //============================================================================ float grid_resolution = chf("grid_resolution"); // 各グリッドセルのサイズ float slope_threshold = chf("slope_threshold"); // 砂が滑るために必要な最小勾配 float scale_factor = chf("scale_factor"); // 極端な結果を回避するためのスケール係数 // --- 滑りのための有効な勾配閾値を計算します --- float sliding_threshold = grid_resolution * slope_threshold; // --- 現在のセルの高さとグリッド位置をキャッシュ化します。 --- int x = @ix; int y = @iy; /* 4つの標準方位(左、右、上、下)を定義します。 これらの方位は、勾配を評価し、あり得る滑べり方向を決定するために使用されます。 */ vector dirs[] = array( set(-1, 0, 0), // 左 set( 1, 0, 0), // 右 set( 0, -1, 0), // 下 set( 0, 1, 0) // 上 ); vector slide_dirs[] = array(); /* すべての隣接セルをループして、勾配を評価します。 隣接セルに向かった勾配が調整済みの閾値を超えると、 その方向は砂が滑る有効な候補方向と見なされます。 */ foreach (vector dir; dirs) { int nx = x + int(dir.x); int ny = y + int(dir.y); vector neighbor_pos = set(nx, ny, 0); // グリッド位置 float h_neighbor = volumeindex(0, "height", neighbor_pos); // 隣接セルの高さ int unstable = (@height - h_neighbor) > sliding_threshold; if (unstable) { append(slide_dirs, dir); // 方向を保存 } } // --- 有効な滑り方向が存在した場合、そのどれかを選択します。 int direction_code = -1; // 安定状態 if (len(slide_dirs) > 0) { float seed_x = @Time + v@P.x * @height; float seed_y = @Time + v@P.y * @height; float rand = noise(set(seed_x, seed_y, 0)); int index = int(fit01(rand, 0, len(slide_dirs) - 0.001)); vector chosen_dir = slide_dirs[index]; if (chosen_dir == set(-1, 0, 0)) direction_code = 0; else if (chosen_dir == set(1, 0, 0)) direction_code = 1; else if (chosen_dir == set(0, -1, 0)) direction_code = 2; else if (chosen_dir == set(0, 1, 0)) direction_code = 3; } @delta = float(direction_code); // 整数レイヤとしての方向。 @mask = direction_code != -1 ? 1.0 : 0.0; // 不安定なセルをマークします。 //--- 後で利用できるように、重要な値をDetailアトリビュートとして保存します。 setdetailattrib(geoself(), "sliding_threshold", sliding_threshold, "set"); setdetailattrib(geoself(), "scale_factor", scale_factor, "set");
パラメータ ¶
上記の最初のスクリプトには、3つのカスタムパラメータの定義が含まれています。
これらのパラメータをWrangleのUIに追加するために、 Creates sparse parameter for each unique call of ch() ボタンをクリックします。
Note
下記のテーブルに記載されている値はあくまで目安であり、地形に応じて調整が必要となる可能性があります。
| パラメータ | 説明 | 値 |
|---|---|---|
| Grid Resolution | このパラメータは、侵食構造を解像度非依存にします。HeightField SOPの Grid Space パラメータの値を使用してください。 |
n/a
|
| Slope Threshold |
砂が滑るために必要な最小勾配。値を小さくすると緩やかな斜面も考慮され、より広い範囲で侵食が発生します。非常に小さい値(<0.3)では、結果が不自然に見える可能性があります。
|
0.7
|
| Scale Factor | 高低差が大きい場合、ノイズやその他のアーティファクトが発生する可能性があります。 Scale Factor パラメータはこの影響を軽減します。特定の結果を得る必要がある場合を除き、値を変更するべきではありません。 |
0.25
|
浸食Wrangle ¶
このスクリプトは、deltaレイヤの符号化された方向値を読み込み、砂が滑るボクセルを判定します。
また、地形のheightレイヤを更新して、最初のWrangleで設定したカスタムパラメータに基づいて堆積処理を適用します。
int opposite[] = ...のコード行は、標準方位を反転させます。
2つのスクリプトの“視点”が異なっているので、このコード行が必要になります。具体的には:
-
前のスクリプトは、現在のセルがどの方向に滑るかを決めます。
-
一方、このスクリプトでは、隣接するセルが現在処理中のセルの方向に滑っているかどうかをチェックします。
以下のコードを⌃ Ctrl + Cでコピーします。 そのコードを 2つ目 のHeightField Wrangle SOPの VEXpression フィールドに⌃ Ctrl + Vでペーストします。
// ============================================================================ // 浸食の適用 // 事前に計算された方向に基づいて砂の移動を適用します。 // これは、侵食/堆積サイクルにおける第2工程です。 // ============================================================================ float sliding_threshold = detail(0, "sliding_threshold") * detail(0, "scale_factor"); /* 4つの標準方位(左、右、上、下)を定義します。 これらの方位は、勾配を評価し、あり得る滑べり方向を決定するために使用されます。 */ vector dirs[] = array( set(-1, 0, 0), // 左 set( 1, 0, 0), // 右 set( 0, -1, 0), // 下 set( 0, 1, 0) // 上 ); float height_increment = 0; int x = @ix; int y = @iy; int opposite[] = array(1, 0, 3, 2); // 正しい結果を得るために方向を反転します: 左 -> 右, 下 -> 上。 // --- deltaレイヤの値を読み込み、それらの値を現在処理中のセルに適用します --- for (int i = 0; i < len(dirs); i++) { int nx = x + int(dirs[i].x); int ny = y + int(dirs[i].y); float neighbor_delta = volumeindex(0, "delta", set(nx, ny, 0)); if (int(neighbor_delta) == opposite[i]) { height_increment += sliding_threshold; } } if (@delta != -1) { height_increment -= sliding_threshold; } // --- 累積した高さの変化を適用します --- @height += height_increment;
結果 ¶
パラメータ調整後、プレイバーに移動して、 Play ボタンをクリックしてシミュレーションを開始してください。
このスクリプトの実行速度は比較的速く、ほとんどの場合50~70フレーム以上のシミュレーションは必要ありません。
以下の動画では、荒々しい岩山の地形が、側面が平坦化した侵食地形へと変化する様子をご覧いただけます。