はじめに
近年では、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またはローカルから読み込む必要があります。
-
<!-- model-viewer for GLB --> -
<script type="module" src="https://unpkg.com/@google/model-viewer/dist/model-viewer.min.js"></script> -
<!-- A-Frame and Gaussian Splatting plugin --> -
<script src="https://aframe.io/releases/1.4.2/aframe.min.js"></script> -
<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ファイルをビューワにドラッグ&ドロップすると、自動的に表示されるように設定しています:
-
<h1>GLB Viewer</h1> -
<model-viewer id="viewer" alt="3D model" camera-controls auto-rotate ar ar-modes="webxr scene-viewer quick-look"> -
<div class="drop-text" id="viewerDropText"> -
Drop your <strong> .glb </strong> file here -
</div> -
</model-viewer>
-
<script> -
// GLB drag & drop -
const viewer = document.getElementById('viewer'); -
const viewerDropTxt = document.getElementById('viewerDropText'); -
viewer.addEventListener('dragover', e => e.preventDefault()); -
viewer.addEventListener('drop', e => { -
e.preventDefault(); -
const file = e.dataTransfer.files[0]; -
if (file && file.name.toLowerCase().endsWith('.glb')) { -
viewer.src = URL.createObjectURL(file); -
} -
viewerDropTxt.classList.add('hidden'); -
}); -
</script>
- 直感的なUI構成
ファイルをドロップするよう促すガイドテキストを表示し、読み込み完了後は非表示に切り替わります。
- ライブラリの改変なし
<model-viewer>のソースコードには一切手を加えず、すべてHTMLとJavaScript側で制御しています。
2. .plyファイルの表示(Gaussian Splatting)
Gaussian Splattingで生成された.plyファイルは、描画処理がより複雑なため、実装難易度は高めです。
表示には aframe-gaussian-splatting を利用し、ライブラリ本体には改変を加えずに統合しました。
実装のポイント:
- ドラッグ&ドロップによる読み込み
.plyファイルをA-Frameベースのビューワにドロップすることで、JavaScript経由で読み込まれます。
- カメラ初期化処理の追加
新しいモデルを読み込む際にカメラの位置・回転を初期化し、常に見やすい位置から表示を開始するようにしました。
- Canvasへのフォーカス制御
キーボード操作が有効になるよう、クリックやタップ時に自動でCanvasにフォーカスを当てる処理を追加しました。
-
<h1>Gaussian Splat Viewer (.ply)</h1> -
<div class="viewer-container" id="ply-drop-zone"> -
<a-scene embedded stats="false" style="width:100%; height:100%; margin:0;" tabindex="0" id="plyScene"> -
<a-entity id="plyEntity" gaussian_splatting position="0 0.5 0" scale="1 1 1"> -
</a-entity> -
<!-- Fixed camera behind looking at origin --> -
<a-entity id="plyCamera" camera position="0 0.5 0.2" look-controls="pointerLockEnabled: false"></a-entity> -
<a-sky color="#000"></a-sky> -
</a-scene> -
<!-- Drop your .ply file here --> -
<div class="drop-text" id="plyDropText"> -
Drop your <strong> .ply </strong> file here -
</div> -
</div> -
<script> -
// GLB drag & drop -
const viewer = document.getElementById('viewer'); -
const viewerDropTxt = document.getElementById('viewerDropText'); -
viewer.addEventListener('dragover', e => e.preventDefault()); -
viewer.addEventListener('drop', e => { -
e.preventDefault(); -
const file = e.dataTransfer.files[0]; -
if (file && file.name.toLowerCase().endsWith('.glb')) { -
viewer.src = URL.createObjectURL(file); -
} -
viewerDropTxt.classList.add('hidden'); -
}); -
// PLY drag & drop -
const plyDrop = document.getElementById('ply-drop-zone'); -
const plyEntity = document.getElementById('plyEntity'); -
const plyDropText = document.getElementById('plyDropText'); -
const plyScene = document.getElementById('plyScene'); -
// 1) Wait for A-Frame to render the canvas -
plyScene.addEventListener('renderstart', () => { -
// 1.1) Capture the actual <canvas> -
const canvas = plyScene.renderer.domElement; -
// 1.2) Make it focusable -
canvas.setAttribute('tabindex', '0'); -
canvas.style.outline = 'none'; -
// 1.3) On click or touch, give it focus -
['mousedown', 'touchstart', 'click'].forEach(evt => -
canvas.addEventListener(evt, () => canvas.focus()) -
); -
}); -
// 2) On container click, focus the canvas -
plyDrop.addEventListener('click', () => { -
const canvas = plyScene.renderer.domElement; -
if (canvas) canvas.focus(); -
}); -
// Reference to camera entity -
const cameraEl = document.getElementById('plyCamera'); -
let cameraObj; -
// 3) Drag & drop PLY -
plyDrop.addEventListener('dragover', e => e.preventDefault()); -
plyDrop.addEventListener('drop', e => { -
e.preventDefault(); -
const file = e.dataTransfer.files[0]; -
if (!file || !file.name.toLowerCase().endsWith('.ply')) { -
alert('Please drop a valid .ply file.'); -
return; -
} -
plyDropText.classList.add('hidden'); -
// Clean scene -
while (plyEntity.object3D.children.length) { -
plyEntity.object3D.remove(plyEntity.object3D.children[0]); -
} -
// Reset camera position & rotation -
const camEl = document.getElementById('plyCamera'); -
camEl.setAttribute('position', '0 0.5 0.2'); -
camEl.object3D.rotation.set(0, 0, 0); -
const lc = camEl.components['look-controls']; -
if (lc) { -
lc.pitchObject.rotation.set(0, 0, 0); -
lc.yawObject.rotation.set(0, 0, 0); -
} -
camEl.setAttribute('rotation', '0 0 0'); -
// Load new splat -
const blobURL = URL.createObjectURL(file); -
plyEntity.setAttribute('gaussian_splatting', `src: ${blobURL}`); -
// Refocus canvas for keyboard -
const canvas = plyScene.renderer.domElement; -
if (canvas) canvas.focus(); -
}); -
</script>
- キーボード操作によるカメラ移動
W/A/S/DやShiftキーを使ってカメラを移動する独自のキーボード制御機能を実装しました。自由に視点を動かすことができます。
-
<script> -
// 4) Manual keyboard handler -
let keyboardEnabled = false; -
plyDrop.addEventListener('click', () => { -
if (!keyboardEnabled) { -
keyboardEnabled = true; -
cameraObj = cameraEl.object3D; -
window.addEventListener('keydown', onKeyDown); -
plyDropText.textContent = 'Keyboard enabled: use W/A/S/D or arrows'; -
} -
}); -
function onKeyDown(e) { -
const step = 0.02; -
if (!cameraObj) return; -
switch (e.key.toLowerCase()) { -
case 'w': -
if (e.shiftKey) { -
cameraObj.translateY(step); -
} else { -
cameraObj.translateZ(-step); -
} -
break; -
case 's': -
if (e.shiftKey) { -
cameraObj.translateY(-step); -
} else { -
cameraObj.translateZ(step); -
} -
break; -
case 'a': -
cameraObj.translateX(-step); -
break; -
case 'd': -
cameraObj.translateX(step); -
break; -
} -
} -
</script>
- A-Frameのステータスパネルを非表示化
デフォルトで表示される開発者向けパネル(stats)を削除し、すっきりしたUIを維持しています。
-
<script> -
// Remove any A-Frame stats panel -
function hideAFrameStats() { -
document.querySelectorAll('#stats, .stats, .a-stats').forEach(el => el.remove()); -
} -
const sceneEl = document.getElementById('plyScene'); -
sceneEl.addEventListener('renderstart', hideAFrameStats); -
sceneEl.addEventListener('enter-vr', hideAFrameStats); -
sceneEl.addEventListener('exit-vr', hideAFrameStats); -
document.addEventListener('fullscreenchange', hideAFrameStats); -
</script>
結果
まとめ
本プロジェクトでは、追加のソフトウェアやインストールなしに、Webブラウザ上で3Dスキャンデータをインタラクティブに表示できる仕組みを実現しました。
.glb形式のモデルには<model-viewer>、.ply形式のGaussian Splattingモデルにはaframe-gaussian-splattingを活用することで、軽量かつ柔軟性の高いソリューションを構築し、複数のフォーマットを即時に表示することが可能となりました。
このようなビューワは、以下のような幅広い分野での活用が期待されます:
- ECサイトでの商品表示(立体的な確認による購買促進)
- 文化財・博物館における3D資料のオンライン展示
- 医療・産業用途におけるスキャンデータの確認・共有
ただし、いくつかの課題も存在します。
- 元となる3Dスキャンモデルの解像度や品質が不十分であれば、ユーザー体験を損ねてしまう可能性があります。
- 逆に、高精細なモデルはファイルサイズが非常に大きくなり、Webブラウザでの表示に時間がかかったり、モバイル環境での実用性が低くなる場合もあります。
そのため、本技術は大きな可能性を秘めている一方で、すべての利用シーンに最適とは限らないことも念頭に置く必要があります。
用途に応じて、表示品質とデータ容量のバランスを見極めることが重要です。
上記ブログの内容に少しでも興味がありましたら、お気軽にご連絡ください。
弊社のエンジニアがフレンドリーに対応させていただきます。

