[Next.js #13] OBJモデルを“粒子化”:PointCloud+GLSL揺らぎでモデルを再構築

はじめに

Reddit でモデルを粒子化するデモ動画があり、興味を持ったので真似て実装してみました。
コードも公開されてるようですが、そちらは見てないです。(本家の方が多機能)

今回もインスペクタを付けて、動的にシェーダーの変数値を変更/更新できるようにしてます。

Next.jsのプロジェクト内で作成してますが、ほぼバニラ Three.js で動かしてるので、JavaScriptカテゴリにしようかと思いましたが、今後もデフォルトのThree.jsでアプリ制作する事が増えると思うので、Next.jsにしてます。

アイデアと技術スタックを事前にAIと相談して、たたき台を作って、そこからカスタムして完成させていますが、モデルを粒子化させてそれを動かすだけなのでコード量はそんなに多くないです。

モデルデータはここからお借りしています。

制作者:printable_models 様

静止画:

動画:

前回の記事:

1. Mesh を Point Cloud に変換する理由

3D モデルを 粒子(Point Cloud) で再構成すると、 通常の Mesh では絶対に出せない 生き物のような“揺らぎ” を作れる。

粒子化のメリットは明確で、特に WebGL で効く。

メリット

  • モデルのシルエットを保ったまま動かせる → Mesh の頂点変形より直感的で、破綻が起きにくい。
  • シェーダーで自由に変形できる → sin・noise・FBM などを直接ぶち込める。
  • GPU で全部処理できて軽い → Point Cloud は WebGL でも高速に動く。
  • 揺らぎ・浮遊感が簡単に作れる → アート表現と相性が最高。

今回の記事では、

OBJ 読み込み → 粒子化 → GLSL アニメーション → モデル表示切替(Points / Mesh)

ここまでを、Vanilla Three.js(Next.js 内)で再現。

2. 概要:処理パイプライン

今回実装した粒子化システムは、非常にシンプルな 1本道のパイプラインで成立している。

OBJ / MTL 読み込み
Geometry の頂点を basePosition として複製
size / color / amplitude などの属性を付与
ShaderMaterial + Points で描画
GLSL(Vertex Shader)側で粒子を揺らす
GUI(lil-gui)でパラメータをリアルタイム調整
"Original / Particles" の表示切り替え

これは Point Cloud アニメーションの定番構成で、 WebGL / Three.js / R3F / Unity Shader Graph すべて同じ発想で応用できる。

特に今回のポイントは:

  • basePosition(元の頂点)を複製して保持すること → 粒子を揺らしても、モデル形状が崩壊しない。
  • size / color などの属性を BufferAttribute で追加 → 全粒子を GPU だけで処理できる。
  • sin による擬似ノイズ揺らぎ → 今後 FBM / Perlin / Worley に簡単に差し替え可能。
  • GUI でアーティスト作業が一気に快適になる

このパイプラインを理解しておけば、 次のステップで ノイズ揺らぎ / 爆散 / 流体風 / 軌跡 / Dissolve などに即応用できる。

3. basePosition を持たせる理由

モデル形状を壊さず、各粒子だけを揺らしたりアニメさせたいなら、 basePosition を必ず保持する必要がある。 OBJ + MTL の注意

ポイントはこれ:

geometry.attributes.position  // GPU が描画に使う「現在の位置」
basePosition                  // 元のモデル形状をそのまま保存
offset                        // シェーダーで加算するアニメ量

pos = basePosition + offset;

なぜ basePosition が必須?

basePosition がないと —— 粒子は自分の“元の居場所”を忘れる。

つまり:

  • アニメーションするほど 原点へ吸い込まれる
  • 全体が ボコボコに崩れて形状が消える
  • “粒子モデル”ではなく、ただの ノイズの塊になる

逆に basePosition を持っていれば:

形状維持しながら揺らせる

(シルエットがそのまま残る)

粒子単位で自由に動かせる

(sin / noise / FBM / curl すべて OK)

モデルの「ボリューム」を保てる

(雲・煙・バラバラ分解・波紋などの演出が可能)

4. Shader の核心

Vertex のコア部分(粒子の揺らぎ)

attribute vec3 basePosition;
attribute float size;

uniform float uTime;
uniform float uAmplitude;
uniform float uFrequency;
uniform float uSpeed;

void main() {
  float t = uTime * uSpeed;

  vec3 offset = vec3(
    sin(t + basePosition.x * uFrequency),
    sin(t + basePosition.y * uFrequency * 1.3),
    sin(t + basePosition.z * uFrequency * 0.7)
  ) * uAmplitude;

  vec3 pos = basePosition + offset;

  gl_PointSize = size;
  gl_Position = projectionMatrix * modelViewMatrix * vec4(pos, 1.0);
}

なぜ揺らぐのか

1)basePosition をそのまま使う

→ モデルの形が崩れない。 → 粒子が“元の位置を中心に”動く。

2)x / y / z で周波数を変える

→ 単調な波にならず、“有機的な揺れ”が生まれる。

3)sin() だけで十分 “生き物感” が出る

もしもっと自然にしたいなら、 後で FBM / Perlin / Curl Noise を混ぜれば “プロの揺らぎ” になる。


この1ブロックを書ければ応用無限

  • モデルの分解演出
  • ワイヤーフレームの震え
  • 霧・雲の粒子化
  • 爆散・再構築エフェクト
  • ポリゴンから点群への遷移アニメ

全部この “basePosition + offset” の変形で実現できる。

5. Fragment:点スプライトのフェード表現

float d = length(gl_PointCoord - vec2(0.5));
float alpha = smoothstep(0.5, 0.0, d);
gl_FragColor = vec4(uColor, alpha);

何をしているか

1)gl_PointCoord:点スプライト内のUV(0〜1) → vec2(0.5) を中心として距離 d を作る。

2)smoothstep(0.5 → 0.0):外側をフェードアウト → 粒子が“丸く”見える。 → 縁が滑らかになるので Additive Blend との相性が最強。


なぜ丸く見えるのか?

smoothstep が “距離 0.5 付近から透明になる” 境界を作るから。

距離 d が小さい → 中心 → 不透明
距離 d が大きい → 外側 → 透明

つまり GLSL 内だけで 丸いテクスチャを生成している。

テクスチャ不要。 画像もいらない。 これが「GPU粒子の基本形」。


応用:プロっぽい粒子にする方法

ここからただの点ではなく、演出になる:

  • pow(alpha, 2.0) → 柔らかい発光
  • 色を mix(color1, color2, alpha) で変化
  • uTime を混ぜて “揺れる発光”
  • 中心を明るくし、外側を青に → 雲/魔法/火花になる

6. lil-gui でリアルタイム調整

今回コントロールできるのは次の 6 パラメータ。

▶ size            (粒子サイズ)
▶ amplitude       (揺れの大きさ)
▶ frequency       (揺れの周波数)
▶ speed           (アニメ速度)
▶ color           (発光色)
▶ showOriginal    (モデル / 粒子 表示切替)

size / amplitude / frequency / speed はすべて Shader の uniform に直結しており、 値を動かすと 即座に粒子アニメーションに反映される。

showOriginal は「元の OBJ モデル」と「粒子シェーダー版」を Group ごとに visible 切り替えするだけなので軽い。


UI スクリーンショットを貼るだけで十分

  • lil-gui は Next.js / Vanilla どちらでも動く
  • 自作エディタのようにモデルを“調整しながら楽しめる”
  • uniform の変化をリアルタイムで確認できるため学習効果も高い

記事では UI のキャプチャ + 最低限の解説だけで成立する。

7. OBJ + MTL の注意点

OBJ/MTL は古い形式だけど、 “点群化(PointCloud 化)する用途では最強クラスに扱いやすい”。

理由はこれ。


OBJ の利点

1. Geometry が素直(壊れにくい)

OBJ は 純粋な頂点+法線+UV だけで構成されているので、 geometry.attributes.position がそのまま “モデル形状そのもの”。

粒子化するときに必須の:

basePosition = geometry.attributes.position

が 変換なしで使える。


2. スケールの謎補正が発生しない

GLB はシーン階層が複雑で、親ノードにスケールがかかっていたりする。

例:

Scene
  └─ Armature (scale: 100)
      └─ Mesh (scale: 1)

この構造のまま点群化すると、 突然モデルが巨大化・崩壊・中心がズレる など混乱しやすい。

OBJ は階層構造ほぼ無し → 点群化が安全。


3. ウェイト・ボーンが無いので安心

GLB は humanoid の場合ボーン階層が必ず付いてくる。 点群化では不要な情報なので、OBJ の方がクリーン。


MTL の注意点

MTL(マテリアルファイル)はテクスチャパスだけ記録しているため:

  • パスがズレると真っ黒になる
  • ローダーの materials.preload() が必須
  • Ambient / Diffuse の影響を強く受ける

今回は 粒子化が本体なので、 “元モデルの見た目が多少崩れても OK” という前提で使用。


今回 OBJ を使った理由(結論)

✔ 頂点さえあれば良い
✔ 階層の複雑さが無い
✔ 値が破壊されにくい
✔ WebGL の PointCloud との相性が最強

GLB はアニメ・リグ・ウェイトが必要な場合は最強。
だが “点群化” には OBJ が一番安全。

8. Original / Particles の切り替え

粒子化すると「元の形状と比べたい」場面が必ず出てくる。 そこで、モデル(Mesh)と粒子(Points)を 1 ボタンで切り替えできるようにした。

originalGroup.visible  = params.showModel;
particleGroup.visible  = !params.showModel;

showModel は lil-gui のチェックボックスに連動。


この仕組みのメリット

  • 粒子化しても 輪郭が正しく保たれているか確認できる
  • basePosition のズレ / スケール破壊の デバッグが一瞬で終わる
  • 記事の読者が「元のモデルと比較」しやすい
  • 実際の作品でも “変形アニメ → 元形状に戻す” に応用できる

設計の基本は「Group を分ける」こと

scene
 ├─ originalGroup  (普通の Mesh)
 └─ particleGroup  (PointCloud + Shader)

Mesh と Points を混ぜないことで、 visible の ON/OFF だけで高速・安全に切り替えができる。

9. まとめ:Mesh は “構造”、Particles は “魂” を動かす

3Dモデルをそのまま動かすより、 粒子に置き換えた瞬間、モデルは“生命感”を持つ。

理由はシンプルで、粒子は:

  • シルエットを維持したまま動く
  • 1点ずつ別々の時間・揺らぎを持てる
  • Shader が全てを支配する(=応用無限)
  • 軽量で Web でも強い

つまり、 Mesh = 形状(Structure) Particles = 動きの魂(Spirit)

という明確な分業になる。


粒子化すると、表現の“幅”が一気に広がる

  • モデルの輪郭を波で崩す
  • 爆散 → 再構築(再生演出)
  • 音に合わせて振動させる VJ 的表現
  • 魔法陣 / 変身エフェクト
  • WebXR でも軽く動く

普通の Mesh では絶対に出せない世界。


今回やったことの価値

OBJ/MTL 読み込み → 点群化 → basePosition → Shader揺らぎ → lil-gui でリアルタイム制御 → Original/Particles の切り替え

これは、WebGL の中でも “応用テク” に分類される領域で、 Three.js という枠を一段超えた 実装と言ってよい。