WebGPUとRust WASM(XPBD)によるリアルタイム3D布シミュレーションの設計と高速化TIPS

ブラウザ上で毎秒60フレーム以上のスムーズな 3D 布(Cloth)物理シミュレーションを動かすデモ WebGPU Cloth Simulation を開発・公開しました。

本記事では、このシミュレータの内部構造、物理演算に使ったアルゴリズム(XPBD: Extended Position Based Dynamics)、WASMとWebGPU間のメモリ転送最適化、そして複雑な3Dモデル(Stanford Bunny)や自己衝突をリアルタイムに捌くための衝突検出高速化TIPSについて、技術者向けに詳しく解説します。


1. 全体アーキテクチャ

システム全体の構成は以下の通りです。物理演算とメッシュ構造の更新を Rust WebAssembly で行い、描写処理を WebGPU (WGSL) および WebGL2 (Fallback) で行う役割分離設計としています。

 +---------------------------------------------------------+
 |                      Browser Core                       |
 |  +--------------------+         +--------------------+  |
 |  |    Rust (WASM)     |         |   TypeScript UI    |  |
 |  |  - XPBD Physics    |         |  - Camera Control  |  |
 |  |  - Constraints     |<------->|  - User Drag Ray   |  |
 |  |  - Spatial Grid    |  Event  |  - Stats HUD       |  |
 |  +---------+----------+         +---------+----------+  |
 |            |                              |             |
 |            | Zero-Copy                    | GPU Uniforms|
 |            v                              v             |
 |  +---------------------------------------------------+  |
 |  |              WebGPU / WGSL Pipeline               |  |
 |  |  - Render Pipeline (Silk / Gold PBR / Heatmap)    |  |
 |  |  - Buffer Binding (device.queue.writeBuffer)      |  |
 |  +---------------------------------------------------+  |
 +---------------------------------------------------------+

サードパーティの重量な物理ライブラリや 3D エンジンには頼らず、コア物理演算・メッシュ処理・レンダリングパイプラインをすべてスクラッチで実装しています。


2. 布物理シミュレーションのアルゴリズム (XPBD)

布のシミュレーションには XPBD (Extended Position Based Dynamics) を採用しました。 従来の PBD (Position Based Dynamics) はタイムステップ依存の硬さ問題がありましたが、XPBD では柔軟なコンプライアンス(Compliance: 剛性の逆数 $\alpha = 1/k$)を取り入れることで、タイムステップ長やソルバーイテレーション数に依存しない安定した挙動を実現します。

2.1 粒子の運動方程式と Verlet 積分

各粒子 $i$ は位置 $\mathbf{x}_i$、速度 $\mathbf{v}_i$、逆質量 $w_i = 1/m_i$ を持ちます。タイムステップ $\Delta t$ ごとに外力(重力や風)を加算し、予測位置 $\mathbf{x}_i^*$ を更新します。

$$\mathbf{v}_i \leftarrow \mathbf{v}i + \Delta t \cdot w_i \mathbf{f}{\text{ext}}$$ $$\mathbf{x}_i^* \leftarrow \mathbf{x}_i + \Delta t \cdot \mathbf{v}_i$$

2.2 距離制約の求解 (Distance Constraint)

布の形状を維持するため、隣接する粒子対 $(i, j)$ 間に以下の 3 種類の制約をメッシュ上に配置しています:

  1. Structural (構造制約): 上下左右のグリッド接続(伸び縮み防止)
  2. Shear (せん断制約): 対角線方向の接続(歪み防止)
  3. Bending (曲げ制約): 1マス飛ばしの接続(折り曲げ抗力)

距離制約の関数は、初期距離 $d_0$ に対し $C(\mathbf{x}_1, \mathbf{x}_2) = |\mathbf{x}_1 - \mathbf{x}_2| - d_0 = 0$ です。 XPBD における Lagrange 乗数の増分 $\Delta \lambda$ は以下の式で求まります:

$$\Delta \lambda = \frac{-C(\mathbf{x}_1, \mathbf{x}_2) - \tilde{\alpha} \lambda}{w_1 + w_2 + \tilde{\alpha}}, \quad \text{where } \tilde{\alpha} = \frac{\alpha}{\Delta t^2}$$

位置補正量 $\Delta \mathbf{x}_1, \Delta \mathbf{x}_2$ は次のようになります:

$$\Delta \mathbf{x}_1 = w_1 \Delta \lambda \mathbf{n}, \quad \Delta \mathbf{x}_2 = -w_2 \Delta \lambda \mathbf{n} \quad \left(\mathbf{n} = \frac{\mathbf{x}_1 - \mathbf{x}_2}{|\mathbf{x}_1 - \mathbf{x}_2|}\right)$$

2.3 サブステッピング (Substepping) による伸び・振動の抑制

大きなフレームタイムステップ(例: $1/60$ 秒)のまま制約解法を回すと、布が不自然に引き伸ばされたり、超高剛性設定時に数値的振動が生じます。

そこで、1 フレームの物理更新 $\Delta t$ をさらに微小なサブステップ(本実装では 5 サブステップ、$\Delta t_{\text{sub}} \le 8\text{ms}$)に分割して解いています。これにより、少ないソルバーイテレーション数でも非常に張りがあり、自然な挙動の布を安定して維持できます。


3. WASM ↔ WebGPU 間の Zero-Copy メモリ共有

Rust で計算した全頂点データ(位置 $\mathbf{x}$、法線 $\mathbf{n}$、UV座標)を、毎フレーム JS/TS 経由で GPU に渡す際、オブジェクトの再生成やコピーのオーバーヘッドは厳禁です。

TypedArray ポインタ参照と直接転送

Rust 側の ClothSimulation では、パッキングした頂点バッファ (Vec<f32>) のリニアメモリ先頭ポインタと要素数を露出します。

// Rust (lib.rs)
#[wasm_bindgen]
impl ClothSimulation {
    pub fn vertex_buffer_ptr(&self) -> *const f32 {
        self.cloth.vertex_buffer_data.as_ptr()
    }
    pub fn vertex_buffer_len(&self) -> usize {
        self.cloth.vertex_buffer_data.len()
    }
}

JavaScript/TypeScript 側では、wasm.memory.buffer 上にビュー(Float32Array)を生成し、中間に余計なコピー配列を作らずに device.queue.writeBuffer に直接渡します。

// TypeScript (app.ts)
const ptr = wasmSim.vertex_buffer_ptr();
const len = wasmSim.vertex_buffer_len();
// WASM メモリを直で TypedArray ビューとして参照 (Zero-copy creation)
const floatArray = new Float32Array(wasm.memory.buffer, ptr, len);

// WebGPU バッファへ直接転送
device.queue.writeBuffer(
  gpuVertexBuffer,
  0,
  floatArray.buffer,
  floatArray.byteOffset,
  floatArray.byteLength
);

メモリコピーが完全に排除されるため、10,000頂点を超える大きなメッシュであっても 1ms 未満で GPU バッファの更新が完了します。


4. 衝突検出と応答の高速化 TIPS

本シミュレータの技術的ハイライトの1つが、Stanford Bunny(複雑な 3D OBJ メッシュ)との衝突 および 布自体の自己衝突 (Self-Collision) をリアルタイムに捌く処理です。

単にすべての粒子と全ポリゴンの距離を計算すると $\mathcal{O}(N \cdot M)$ の計算量となり、WebAssembly であってもフレームレートが大幅に低下します。これを回避するため、段階的なフィルタリング手法を導入しました。

[Cloth Particle]
       |
       v
+-------------------------------------------------------+
| Phase 1: Multi-Bounding Sphere (Broadphase)           |
| -> 大まかな球バウンディング判定(領域外なら瞬時にパス)|
+-------------------------------------------------------+
       | (Hit)
       v
+-------------------------------------------------------+
| Phase 2: 3D Spatial Hash Grid (Spatial Partitioning)  |
| -> 粒子の周辺セルに含まれるポリゴン候補のみを抽出      |
+-------------------------------------------------------+
       | (Candidates)
       v
+-------------------------------------------------------+
| Phase 3: Barycentric Nearest-Point Projection         |
| -> 三角形と点との最短距離 & バリセントリック投影・押し出し|
+-------------------------------------------------------+

4.1 ブロードフェーズ: Multi-Bounding Sphere

Stanford Bunny 全体を1つの AABB で囲むのではなく、頭部・耳・胴体・尻尾などの部位に合わせた 9 個の複合バウンディングスフィア(Multi-Sphere)を設定します。

粒子がこの球体のいずれの範囲外にあれば、後続の複雑な幾何計算を直ちにスキップします。

4.2 ナローフェーズ: 3D Spatial Grid (空間格子)

バウンディングスフィアを通過した粒子に対しては、3D 空間格子 (Spatial Hash Grid) を使用して近傍ポリゴンを特定します。

バニーの OBJ メッシュから構築した SpatialGrid は、3D 空間を等サイズのセル(例: [dx, dy, dz])に分割し、各セルに交差する三角形インデックスのリストを持たせます。

pub struct SpatialGrid {
    pub min_bounds: [f32; 3],
    pub max_bounds: [f32; 3],
    pub cell_size: [f32; 3],
    pub grid_res: [usize; 3],
    pub cells: Vec<Vec<usize>>, // セルごとの三角形インデックス
}

粒子の位置から該当する 3D セルインデックス (cx, cy, cz) を算出し、そのセル(および隣接 27 セル)に存在するポリゴンとのみ精密判定を行います。

4.3 バリセントリック座標系による最近傍点判定

点 $\mathbf{p}$ と 3D 三角形 $(\mathbf{v}_0, \mathbf{v}_1, \mathbf{v}_2)$ の最短距離計算には、バリセントリック座標系 (Barycentric Coordinates) の領域判定アルゴリズムを適用しています。

三角形の頂点領域・エッジ領域・面内領域の7つの領域に分け、最も近い点 $\mathbf{q}$ を正確に算出し、貫通している場合はマージン $r_{\text{margin}}$ 分だけ外側に押し出します。

pub fn closest_point_on_triangle(p: [f32; 3], v0: [f32; 3], v1: [f32; 3], v2: [f32; 3]) -> [f32; 3] {
    let ab = [v1[0] - v0[0], v1[1] - v0[1], v1[2] - v0[2]];
    let ac = [v2[0] - v0[0], v2[1] - v0[1], v2[2] - v0[2]];
    let ap = [p[0] - v0[0], p[1] - v0[1], p[2] - v0[2]];

    let d1 = dot3(ab, ap);
    let d2 = dot3(ac, ap);
    if d1 <= 0.0 && d2 <= 0.0 { return v0; } // Vertex v0 region

    let bp = [p[0] - v1[0], p[1] - v1[1], p[2] - v1[2]];
    let d3 = dot3(ab, bp);
    let d4 = dot3(ac, bp);
    if d3 >= 0.0 && d4 <= d3 { return v1; } // Vertex v1 region

    // ... Edge & Interior 領域の判定 ...
}

4.4 高速な自己衝突 (Self-Collision) 処理

布が折り重なった際の突き抜けを防ぐ自己衝突も、メッシュの 2D 格子トポロジーを活用した近接探索で高速化しています。

トポロジー的に隣接する粒子(1マス以内)は無条件で除外し、物理的距離が指定閾値 $d_{\text{min}}$ 未満に接近した非隣接粒子対に対してのみ、反発補正量を適用します。

// 布グリッド構造を利用した周辺セル近接判定 (cloth.rs)
for y2 in y_min..y_max {
    for x2 in x_min..x_max {
        let idx2 = y2 * width + x2;
        // トポロジー的に隣接している格子点はスキップ
        if idx1 >= idx2 || (x1.abs_diff(x2) <= 1 && y1.abs_diff(y2) <= 1) {
            continue;
        }
        // 距離判定と押し出し補正...
    }
}

5. WebGPU (WGSL) レンダリングとシェーディング

グラフィック面では、WebGPU の標準シェーダー言語である WGSL (WebGPU Shading Language) を用いて多様なマテリアル表現を行っています。

  1. PBR Velvet / Silk Mode: 視線角度に応じたフレネル効果と、ハイライトの立ち上がりによる絹・ベルベット質感
  2. Gold Metallic Mode: 金属反射と環境光近似による金の表現
  3. Stress / UV Heatmap Mode: 布の伸びや変形歪みを計算し、カラーグラデーションで可視化する物理解析モード
  4. Normal Field Mode: 各頂点・面の法線方向を RGB カラー空間にマッピングするデバッグ表示
// WGSL Fragment Shader (一部抜粋)
@fragment
fn fs_main(input: VertexOutput) -> @location(0) vec4<f32> {
    let mode = i32(uniforms.params.x);
    let N = normalize(input.normal);
    let L = normalize(uniforms.lightPos.xyz - input.worldPos);
    let V = normalize(uniforms.cameraPos.xyz - input.worldPos);
    let H = normalize(L + V);

    // Normal Map Mode
    if (mode == 3) {
        return vec4<f32>(N * 0.5 + vec3<f32>(0.5), 1.0);
    }
    // Silk PBR Shading
    let NdotL = max(dot(N, L), 0.0);
    let NdotH = max(dot(N, H), 0.0);
    let spec = pow(NdotH, 32.0);
    let silkColor = vec3<f32>(0.15, 0.45, 0.95);
    return vec4<f32>(silkColor * NdotL + vec3<f32>(spec * 0.6), 1.0);
}

6. まとめ

本プロジェクトでは、WebGPU と Rust WebAssembly というモダンな Web 技術を組み合わせることで、従来はブラウザでのリアルタイム実行が困難であった本格的な 3D 物理シミュレーションを 60 FPS で動作させることができました。

  • 物理解法: XPBD + サブステッピングによる高剛性・低振動な布挙動
  • メモリ伝送: Rust ↔ WASM ↔ WebGPU 間における徹底した Zero-Copy 設計
  • 衝突検出: Multi-Sphere + 3D Spatial Grid + バリセントリック座標による複雑形状とのリアルタイム衝突

デモサイトは以下で公開しています。ぜひブラウザ(Chrome / Edge / Safari 等)でお試しください!