Web向けインタラクティブ3Dスキャンビューワ:PhotogrammetryとGaussian Splatting対応

はじめに


近年では、3Dでオブジェクトや環境をキャプチャできるデバイスが増え、より手頃な価格で利用できるようになってきました。

このような3Dスキャンにおいて、Photogrammetry(フォトグラメトリー)やGaussian Splatting(ガウシアン・スプラッティング)は、特に人気の高い形式です。

 

そこで私たちは、追加のインストールなしで、Webブラウザ上でそのまま動作する3Dスキャンデータのインタラクティブなビューワを試作しました。

 

このようなビューワは、さまざまな業界で活用でき、スキャンしたオブジェクトを魅力的かつ簡単に3D表示することが可能になります。

使用した技術


このプロジェクトでは、3Dスキャンデータの表示に以下のオープンソースコンポーネントを活用しました:

  • model-viewer(Google製) Photogrammetry(フォトグラメトリー)で得られた.glbファイルをWebブラウザ上で簡単に表示できるJavaScriptライブラリです。<model-viewer>タグ一つでインタラクティブな3D表示が可能になります。
  • aframe-gaussian-splatting(quadjr氏によるA-Frame拡張) Gaussian Splattingで生成された.plyファイルを表示するためのA-Frameベースのコンポーネントです。私たちの実装では、このライブラリのコードを直接変更せず、HTMLとJavaScriptの外部ロジックから制御・拡張しました。

 

実装の概要


本アプリケーションは、1ページ内に2種類の3Dスキャン形式(PhotogrammetryとGaussian Splatting)を同時に表示できる構成で開発されました。

 

具体的には、以下の2つのファイル形式に対応しています:

  • .glb:Photogrammetry(フォトグラメトリー)で生成されたモデル
  • .ply:Gaussian Splatting(ガウシアン・スプラッティング)で生成されたモデル

両方のビューワは同一ページ内に独立して配置されており、ページを再読み込みすることなく、複数ファイルを切り替えながら比較・確認することができます。

 

また、ユーザーが簡単に試せるように、ドラッグ&ドロップ操作でファイルを読み込むUIを実装しました。手元にあるファイルをすぐに表示できるため、体験性の高い仕組みになっています。

依存ライブラリとスクリプトの読み込み

このビューワは1ページで完結する構成であるため、使用するライブラリをすべてCDNまたはローカルから読み込む必要があります。

  1.     <!-- model-viewer for GLB -->
  2.     <script type="module" src="https://unpkg.com/@google/model-viewer/dist/model-viewer.min.js"></script>
  3.     <!-- A-Frame and Gaussian Splatting plugin -->
  4.     <script src="https://aframe.io/releases/1.4.2/aframe.min.js"></script>
  5.     <script src="./aframe-gaussian-splatting-main/index.js"></script>

補足ポイント:

  • model-viewerはCDNから直接読み込んでおり、リポジトリのクローンや改変は不要です。
  • 一方、.plyファイルの表示に使用したaframe-gaussian-splattingはローカルにクローンしてプロジェクトに組み込んでいます(ただしコードの改変は行っていません)。

1. .glbファイルの表示(Photogrammetry)

Photogrammetryで作成された.glbファイルの表示には、Google製の <model-viewer> ライブラリを使用しています。

 

 

実装のポイント:

- ドラッグ&ドロップによる読み込み対応 

ユーザーが.glbファイルをビューワにドラッグ&ドロップすると、自動的に表示されるように設定しています:

  1. <h1>GLB Viewer</h1>
  2. <model-viewer id="viewer" alt="3D model" camera-controls auto-rotate ar ar-modes="webxr scene-viewer quick-look">
  3.         <div class="drop-text" id="viewerDropText">
  4.             Drop your <strong>&nbsp;.glb&nbsp;</strong> file here
  5.         </div>
  6. </model-viewer>
  1. <script>
  2.         // GLB drag & drop
  3.         const viewer = document.getElementById('viewer');
  4.         const viewerDropTxt = document.getElementById('viewerDropText');
  5.         viewer.addEventListener('dragover', e => e.preventDefault());
  6.         viewer.addEventListener('drop', e => {
  7.             e.preventDefault();
  8.             const file = e.dataTransfer.files[0];
  9.             if (file && file.name.toLowerCase().endsWith('.glb')) {
  10.                 viewer.src = URL.createObjectURL(file);
  11.             }
  12.             viewerDropTxt.classList.add('hidden');
  13.         });
  14. </script>

- 直感的なUI構成

ファイルをドロップするよう促すガイドテキストを表示し、読み込み完了後は非表示に切り替わります。

- ライブラリの改変なし

<model-viewer>のソースコードには一切手を加えず、すべてHTMLとJavaScript側で制御しています。

2. .plyファイルの表示(Gaussian Splatting)

Gaussian Splattingで生成された.plyファイルは、描画処理がより複雑なため、実装難易度は高めです。

表示には aframe-gaussian-splatting を利用し、ライブラリ本体には改変を加えずに統合しました。

 

実装のポイント:

- ドラッグ&ドロップによる読み込み

.plyファイルをA-Frameベースのビューワにドロップすることで、JavaScript経由で読み込まれます。

- カメラ初期化処理の追加

新しいモデルを読み込む際にカメラの位置・回転を初期化し、常に見やすい位置から表示を開始するようにしました。

- Canvasへのフォーカス制御

キーボード操作が有効になるよう、クリックやタップ時に自動でCanvasにフォーカスを当てる処理を追加しました。

  1. <h1>Gaussian Splat Viewer (.ply)</h1>
  2. <div class="viewer-container" id="ply-drop-zone">
  3.         <a-scene embedded stats="false" style="width:100%; height:100%; margin:0;" tabindex="0" id="plyScene">
  4.             <a-entity id="plyEntity" gaussian_splatting position="0 0.5 0" scale="1 1 1">
  5.             </a-entity>
  6.             <!-- Fixed camera behind looking at origin -->
  7.             <a-entity id="plyCamera" camera position="0 0.5 0.2" look-controls="pointerLockEnabled: false"></a-entity>
  8.             <a-sky color="#000"></a-sky>
  9.         </a-scene>
  10.         <!-- Drop your .ply file here -->
  11.         <div class="drop-text" id="plyDropText">
  12.             Drop your <strong>&nbsp;.ply&nbsp;</strong> file here
  13.         </div>
  14. </div>
  15. <script>
  16.         // GLB drag & drop
  17.         const viewer = document.getElementById('viewer');
  18.         const viewerDropTxt = document.getElementById('viewerDropText');
  19.         viewer.addEventListener('dragover', e => e.preventDefault());
  20.         viewer.addEventListener('drop', e => {
  21.             e.preventDefault();
  22.             const file = e.dataTransfer.files[0];
  23.             if (file && file.name.toLowerCase().endsWith('.glb')) {
  24.                 viewer.src = URL.createObjectURL(file);
  25.             }
  26.             viewerDropTxt.classList.add('hidden');
  27.         });
  28.         // PLY drag & drop
  29.         const plyDrop = document.getElementById('ply-drop-zone');
  30.         const plyEntity = document.getElementById('plyEntity');
  31.         const plyDropText = document.getElementById('plyDropText');
  32.         const plyScene = document.getElementById('plyScene');
  33.         // 1) Wait for A-Frame to render the canvas
  34.         plyScene.addEventListener('renderstart', () => {
  35.             // 1.1) Capture the actual <canvas>
  36.             const canvas = plyScene.renderer.domElement;
  37.             // 1.2) Make it focusable
  38.             canvas.setAttribute('tabindex', '0');
  39.             canvas.style.outline = 'none';
  40.             // 1.3) On click or touch, give it focus
  41.             ['mousedown', 'touchstart', 'click'].forEach(evt =>
  42.                 canvas.addEventListener(evt, () => canvas.focus())
  43.             );
  44.         });
  45.         // 2) On container click, focus the canvas
  46.         plyDrop.addEventListener('click', () => {
  47.             const canvas = plyScene.renderer.domElement;
  48.             if (canvas) canvas.focus();
  49.         });
  50.         // Reference to camera entity
  51.         const cameraEl = document.getElementById('plyCamera');
  52.         let cameraObj;
  53.         // 3) Drag & drop PLY
  54.         plyDrop.addEventListener('dragover', e => e.preventDefault());
  55.         plyDrop.addEventListener('drop', e => {
  56.             e.preventDefault();
  57.             const file = e.dataTransfer.files[0];
  58.             if (!file || !file.name.toLowerCase().endsWith('.ply')) {
  59.                 alert('Please drop a valid .ply file.');
  60.                 return;
  61.             }
  62.             plyDropText.classList.add('hidden');
  63.             // Clean scene
  64.             while (plyEntity.object3D.children.length) {
  65.                 plyEntity.object3D.remove(plyEntity.object3D.children[0]);
  66.             }
  67.             // Reset camera position & rotation
  68.             const camEl = document.getElementById('plyCamera');
  69.             camEl.setAttribute('position', '0 0.5 0.2');
  70.             camEl.object3D.rotation.set(0, 0, 0);
  71.             const lc = camEl.components['look-controls'];
  72.             if (lc) {
  73.                 lc.pitchObject.rotation.set(0, 0, 0);
  74.                 lc.yawObject.rotation.set(0, 0, 0);
  75.             }
  76.             camEl.setAttribute('rotation', '0 0 0');
  77.             // Load new splat
  78.             const blobURL = URL.createObjectURL(file);
  79.             plyEntity.setAttribute('gaussian_splatting', `src: ${blobURL}`);
  80.             // Refocus canvas for keyboard
  81.             const canvas = plyScene.renderer.domElement;
  82.             if (canvas) canvas.focus();
  83.         });
  84. </script>

- キーボード操作によるカメラ移動

W/A/S/DやShiftキーを使ってカメラを移動する独自のキーボード制御機能を実装しました。自由に視点を動かすことができます。

  1. <script>
  2.         // 4) Manual keyboard handler
  3.         let keyboardEnabled = false;
  4.         plyDrop.addEventListener('click', () => {
  5.             if (!keyboardEnabled) {
  6.                 keyboardEnabled = true;
  7.                 cameraObj = cameraEl.object3D;
  8.                 window.addEventListener('keydown', onKeyDown);
  9.                 plyDropText.textContent = 'Keyboard enabled: use W/A/S/D or arrows';
  10.             }
  11.         });
  12.         function onKeyDown(e) {
  13.             const step = 0.02;
  14.             if (!cameraObj) return;
  15.             switch (e.key.toLowerCase()) {
  16.                 case 'w':
  17.                     if (e.shiftKey) {
  18.                         cameraObj.translateY(step);
  19.                     } else {
  20.                         cameraObj.translateZ(-step);
  21.                     }
  22.                     break;
  23.                 case 's':
  24.                     if (e.shiftKey) {
  25.                         cameraObj.translateY(-step);
  26.                     } else {
  27.                         cameraObj.translateZ(step);
  28.                     }
  29.                     break;
  30.                 case 'a':
  31.                     cameraObj.translateX(-step);
  32.                     break;
  33.                 case 'd':
  34.                     cameraObj.translateX(step);
  35.                     break;
  36.             }
  37.         }
  38. </script>

- A-Frameのステータスパネルを非表示化

デフォルトで表示される開発者向けパネル(stats)を削除し、すっきりしたUIを維持しています。

  1. <script>
  2.         // Remove any A-Frame stats panel
  3.         function hideAFrameStats() {
  4.             document.querySelectorAll('#stats, .stats, .a-stats').forEach(el => el.remove());
  5.         }
  6.         const sceneEl = document.getElementById('plyScene');
  7.         sceneEl.addEventListener('renderstart', hideAFrameStats);
  8.         sceneEl.addEventListener('enter-vr', hideAFrameStats);
  9.         sceneEl.addEventListener('exit-vr', hideAFrameStats);
  10.         document.addEventListener('fullscreenchange', hideAFrameStats);
  11. </script>

結果


まとめ


本プロジェクトでは、追加のソフトウェアやインストールなしに、Webブラウザ上で3Dスキャンデータをインタラクティブに表示できる仕組みを実現しました。

 

.glb形式のモデルには<model-viewer>、.ply形式のGaussian Splattingモデルにはaframe-gaussian-splattingを活用することで、軽量かつ柔軟性の高いソリューションを構築し、複数のフォーマットを即時に表示することが可能となりました。

 

このようなビューワは、以下のような幅広い分野での活用が期待されます:

  • ECサイトでの商品表示(立体的な確認による購買促進)
  • 文化財・博物館における3D資料のオンライン展示
  • 医療・産業用途におけるスキャンデータの確認・共有

 

ただし、いくつかの課題も存在します。

  • 元となる3Dスキャンモデルの解像度や品質が不十分であれば、ユーザー体験を損ねてしまう可能性があります。
  • 逆に、高精細なモデルはファイルサイズが非常に大きくなり、Webブラウザでの表示に時間がかかったり、モバイル環境での実用性が低くなる場合もあります。

そのため、本技術は大きな可能性を秘めている一方で、すべての利用シーンに最適とは限らないことも念頭に置く必要があります。
用途に応じて、表示品質とデータ容量のバランスを見極めることが重要です。

上記ブログの内容に少しでも興味がありましたら、お気軽にご連絡ください。

弊社のエンジニアがフレンドリーに対応させていただきます。