Three.jsでの3Dガウシアンスプラッティング入門

はてなブックマークへ投稿
Xへポスト
URLをコピー
共有

2026年9月8日に公開されたThree.jsのr186で、3D Gaussian Splattingガウシアン・スプラッティング(以下、3DGS)をライブラリの機能で表示できるようになりました。3DGSは写真から立体を再構成する手法で、手元にあるものをスマートフォンで数十秒撮るだけで、ぐるりと見回せる3Dデータが作れます。

この記事では、スマートフォンでの撮影からThree.jsでの表示までをひととおり試します。

次のデモは、撮影したスパゲティを魔法陣から召喚する演出です。この演出は過去記事『エフェクト作成入門講座 Three.js編 RPGのセーブポイント風の魔法陣』のコードをそのまま使い、召喚される被写体として3DGSを組み込んでいます。[SUMMON]ボタンを押すと何度でも召喚できます。

[Load .spz]ボタンで手元のspzファイルの読み込みができ、[Flip]ボタンで上下反転できます。

Three.jsだけで表示できる

Three.jsには、3DGSを扱うGaussianSplatクラスと、専用のファイルを読み込むローダーがあります。表示に必要なコードは以下です。

// ファイルを読み込むとBufferGeometryが返る
const geometry = await new SPZLoader().loadAsync("./model.spz");
// GaussianSplatに渡して、ふつうのオブジェクトと同じようにシーンへ追加する
const splat = new GaussianSplat(geometry);
scene.add(splat);

3DGSの主なファイル形式である.spz.ply.splat.ksplatの4形式には専用のローダーがあり、いずれもBufferGeometryクラスのインスタンスを返します。あとはGaussianSplatクラスに渡せば表示できます。

この機能はWebGPU向けの実装なので、レンダラーにはWebGPURendererクラスを使います。WebGLRendererクラスでは動きません。WebGPURendererクラスの使い方は記事『WebGPU対応のThree.jsのはじめ方』を参照してください。

公式サンプルのwebgpu_gaussian_splatで、ブラウザー上ですぐに動作を確認できます。

3Dガウシアンスプラッティングとは

従来の3D表現であるメッシュは、三角形を大量に並べて面を作り、そこに画像を貼りつけます。一方、3DGSは三角形の代わりに、位置、大きさと向き、色、不透明度をもった半透明の粒を使います。粒は中心ほど濃く、周囲へ向かってなめらかに薄くなります。この濃淡にガウス分布を使っていることが名前の由来です。

粒は一様な球ではなく、平らな面の上では薄い円盤、細い枝の上では細長い塊というように、場所に応じて形が変わります。描画するときは、粒を画面上の楕円として描き、奥にあるものから順に重ねていきます。

点群と似ていますが、点群は位置と色だけをもった点の集まりなので、近づくと点と点の間に隙間ができ、ばらけて見えます。3DGSの粒は大きさと向きをもち、隣の粒となめらかに重なるので、隙間のない面として見えます。

3DGSは面を作らないぶん、毛や葉のような輪郭のはっきりしないものが得意で、つやのある表面の見え方も再現できます(鏡のように周囲が映り込むものは除きます)。

▼ 同じ形を三角形メッシュ(左)と粒の集まり(右)で表現した比較

三角形メッシュと3DGSの表現の違い

スマートフォンで撮影する

撮影にはスマートフォンのアプリを使う方法が簡単です。3DGSを生成できるアプリは結構たくさんあるのですが、その中で今回はNiantic Spatial社のアプリScaniverseスキャニバースの、個人向け機能「Scaniverse Classic」を使います。無料で、スマートフォン上で処理が完結し、.ply.spzのどちらでも書き出せるためです。

手順は次のとおりです。

  1. アプリを起動、[+]ボタンを押し、スプラット(Splat)モードを選ぶ
  2. 被写体のまわりをゆっくり一周する(高さを変えて2周から3周すると、上下の抜けが減る)
  3. 撮影を終えると端末上で処理が始まる(数分待つと完成する)
  4. [マップに投稿]ボタンが出るが、投稿しない場合はそのまま閉じる
  5. そのままだと周りの景色も入っている状態なので、編集でトリミングしておく
  6. 共有メニューから[モデルのエクスポート]を選択し、.spz形式で書き出す

▼ Scaniverseの画面。左から、起動直後のマップ、処理の完了、撮影結果の表示、共有メニューのエクスポート

Scaniverseの撮影から書き出しまでの画面

うまくいくのは、動かないもの、光沢が強すぎないもの、周囲をぐるりと回れるものです。逆に、鏡やガラス、風で揺れる植物、真っ白で模様のない壁は苦手です。最初の1回は、机の上に置いた小物のように、確実に一周できる被写体を選んでください。それでも何回かは失敗するかもしれません。1回の撮影は数十秒で済むので、周回の速さや被写体との距離を変えながら撮り直して、感覚をつかんでいきます。

書き出し形式は.spzをオススメします。SPZはScaniverseの開発元が公開した3DGS向けの圧縮形式で、同じデータを.plyで保存した場合とくらべてファイルサイズが約10分の1になります。Three.jsのSPZLoaderクラスはv1からv4までのSPZに対応しているので、書き出したファイルをそのまま読み込めます。

SuperSplatでゴミの粒を取り除く

書き出したデータに床や背景の粒が残っている場合は、被写体だけを使うために削除します。あわせて、再構成に失敗して被写体のまわりにノイズのように浮く粒(フローター)も削除します。

クリーンアップには、ブラウザーで動く無料のエディターSuperSplatスーパースプラットを使います。ファイルを読み込んだら、被写体からはみ出した粒を範囲選択で消していき、.spzで書き出し直します。被写体のまわりを漂うフローターが消えるので、見た目の印象は大きく変わります。

粒の選択も撮影と同じで、少し慣れが必要です。消しすぎたら読み込みなおせばよいので、コツコツ修正します。書き出したデータは、上下が反転していることがあります。次の章で向きを調整するときになおせます。

▼ クリーンアップ前(左)は皿の縁の外側にフローターが浮いている。クリーンアップ後(右)では消えている

SuperSplatでのクリーンアップ前後の比較

Three.jsで表示する

撮影したファイルをウェブページに表示します。基本のコードは次のとおりです。

import * as THREE from "three/webgpu";
import { SPZLoader } from "three/addons/loaders/SPZLoader.js";
import { GaussianSplat } from "three/addons/objects/GaussianSplat.js";

// 3DGSの表示はWebGPURendererを使う
const renderer = new THREE.WebGPURenderer();
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
// WebGPURendererは初期化が非同期なので、完了を待ってから描画を始める
await renderer.init();

// シーンとカメラの作成は省略

// .spzを読み込む
const geometry = await new SPZLoader().loadAsync("./model.spz");
// BufferGeometryが返るので、GaussianSplatに渡す
const splat = new GaussianSplat(geometry);
scene.add(splat);

renderer.setAnimationLoop(() => {
  renderer.render(scene, camera);
});

このコードに、後述する向きとカメラ位置の調整を加えたものが次のデモです。読み込んで表示するだけの最小構成です。

ライトの明るさは反映されない、背景は暗めに

3DGSのデータは、撮影時の光の当たり方をそのまま色として持っています。そのため上のコードにはライトを置いていませんし、追加しても明るさは変わりません。明るさを調整したい場合は、撮影時の環境か、レンダラーの露出(toneMappingExposureプロパティ)で対応します。

背景色は被写体に合わせて選びます。撮影した粒は輪郭がふんわりと途切れるので、白背景だと縁が溶けて見えます。暗めの背景のほうが形が締まって見えることが多いです。

// 粒の輪郭はふんわり途切れるので、暗めの背景のほうが形が締まって見える
scene.background = new THREE.Color(0x111111);

向きを調整する

読み込んだ直後は、上下が反転していたり、原点から離れた場所にあったりします。撮影アプリや形式によって座標系の取り方が違うためです。Three.jsの公式サンプルでも、モデルによって回転を指定したりしなかったりしています。

上下が逆さまなら、X軸まわりに180度回します。

// 読み込んだデータが逆さまならX軸まわりに180度回す
splat.rotation.x = Math.PI;

▼ 読み込み直後(左)は皿が逆さまでスパゲティが隠れている。上下を直したあと(右)

向きの調整前後の比較

見る角度で色が変わる

3DGSのデータには、粒ごとの色だけでなく、見る方向によって色を変えるための係数が入っています。球面調和関数きゅうめんちょうわかんすうと呼ばれるもので、撮影時に「この角度からはこう光って見えた」という情報を記録しています。

Three.jsはこの係数に対応しているので、読み込むだけで角度によって色が変わります。効果がわかりやすいのは、金属、濡れた表面、つややかな果物など、ハイライトの出る被写体です。逆に、つや消しの布や紙ではほとんど差が出ません。

▼ 球面調和関数あり(左)となし(右)の比較。なしでは麺や皿のつやが弱まり、全体が平板になる

球面調和関数の有無の比較

この係数も撮影時の見え方を記録したもので、物理的に正しい反射を計算しているわけではありません。シーンのライトに反応しない理由も同じです。

現時点でできないこと

便利な機能ですが、2026年9月現在、できないこともあります。

大きなシーンには向かない

Three.jsの実装には、カメラからの距離に応じて粗いデータに切り替える仕組み(LODエルオーディー、Level of Detailの略)や、必要な部分だけを順に読み込む仕組み(ストリーミング)がありません。そのため、ファイル全体を読み込みきってから表示することになります。粒数やファイルサイズが大きいデータでは、読み込み時間とメモリー使用量に注意が必要です。

深度を使うポストプロセスは正しく働かない

深度に関しては注意が必要です。Three.jsでの3DGSは、粒のある場所は背景と同じ奥行きとみなされます。被写界深度エフェクトを使った場合、3DGSにフォーカスを合わせても適切な距離としての効果が得られません。環境光遮蔽など、深度を参照するほかのポストプロセスも同じです。

▼ 同じ距離に置いた立方体とスパゲティに被写界深度のポストエフェクトを適用。本来は両方がくっきり写るはずが、深度をもたないスパゲティだけがボケる

被写界深度が粒に正しく効かない例

コラム:大きなシーンを扱うライブラリー

大きなシーンを扱いたい場合は、いまのところ専用のライブラリーを選ぶことになります。

Spark』はThree.js向けの3DGSライブラリーで、LODと、必要な範囲だけを読み込む仕組みを備えています。粗い状態をすぐ表示し、カメラの動きに合わせて細かくしていくため、巨大なシーンでもスムーズに表示できます。

PlayCanvas』はThree.jsとは別のエンジンなので乗り換えが前提になりますが、SOGという圧縮形式と、その派生でLODに対応したStreamed SOGを提供しています。

まとめ

手順そのものは難しくなく、時間がかかるのは撮影の試行錯誤くらいです。冒頭のデモの出現演出は、粒の色と不透明度を計算するシェーダーをTSLで差し替えて実現しています。

見せるだけであれば、SuperSplatからHTMLのビューアーを書き出す手もあります。コードを書かずに公開までできるので、撮ったものを共有したいだけならそれで十分ですが、既存のシーンに組み込みたいときや、カメラ操作やインタラクションを自分で作りたいときは、Three.jsに載せる方法がオススメです。

手元で撮った3DGSが、数行のコードでThree.jsのシーンに載るというのは嬉しいですね。個人的にも作例を増やしていきたいです。

Google検索でICS MEDIAの記事を見つけやすくしよう

優先するソースに追加すると、GoogleのトップニュースやAI検索でICS MEDIAの記事が表示されやすくなります。

Googleで優先するソースとして追加
SNSでシェアしよう
シェアいただくと、サイト運営の励みになります!
はてなブックマークへ投稿
Xへポスト
URLをコピー
共有
楢山 哲弘

フロントエンドエンジニア。Flashからソーシャルゲーム開発、GIS開発を経て、残りの人生をフロントエンド開発に注力しようとICSに入社しました。ジェネレーティブアートに興味があります。

この担当の記事一覧