はじめに
近年では、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ブラウザでの表示に時間がかかったり、モバイル環境での実用性が低くなる場合もあります。
そのため、本技術は大きな可能性を秘めている一方で、すべての利用シーンに最適とは限らないことも念頭に置く必要があります。
用途に応じて、表示品質とデータ容量のバランスを見極めることが重要です。

上記ブログの内容に少しでも興味がありましたら、お気軽にご連絡ください。
弊社のエンジニアがフレンドリーに対応させていただきます。