はじめに ─ スマホで撮った部屋が即メタバース
✨ 斬新ポイント
- スマホ LiDAR → Three.js 直送:
多くの事例は Unity / Unreal → VRChat などですが、ブラウザ完結はレア。 - ノーコード&無料ツールのみ:
iPhone・無料アプリ・オープンソースのみで完結させる実験。 - 自動パイプライン:
npm run pipeline /path/to/scan
で 最小 1 click で公開ページ生成まで!
ゴール:
「自宅の部屋を 3D スキャン → URL を友達に送り、ブラウザで歩き回れる」
Step 0 ─ 準備:ハード・ソフト一覧
役割 | 推奨 / 代替 |
---|---|
LiDAR スキャン | iPhone 12 Pro 以降 / iPad Pro + Polycam(無料プラン可) |
3D データ変換 | Blender 3.6 LTS(無料) |
軽量化 | Blender Decimate |
WebGL 描画 | Three.js v0.160 |
シーン制御 | OrbitControls / FirstPersonControls / WebXRManager |
自動化スクリプト | Node.js 18 + three/examples/jsm + glb-pipeline |
# Node 環境
nvm install 18
nvm use 18
npm i -g serve gltf-pipeline
Step 1 ─ LiDARスキャン → .ply/.obj エクスポート
Polycam で部屋をスキャン
- スキャンモード → LiDAR を選択
- 部屋内をゆっくり 1 周(天井 / 床も忘れず)
- 完了後「処理」を押し、OBJ+Textures でエクスポート
.obj
+.mtl
+tex_000.jpg
等
🔰 ヒント:
- 床・壁・天井の角をできるだけ写す
- 照度が低いとノイズ増、照明を点ける
Blender で最小修正
File > Import > Wavefront (.obj)
- スケールを調整 (メートル単位なら
0.01
) - 不要オブジェクトを削除 (例:天井ファンのブレなど)
- File > Export > glTF 2.0 (.glb)
- Compression: Draco (バイナリ圧縮)
- 読込時に Three.js が自動で展開
Step 2 ─ Three.js でスキャンモデル表示
最小 HTML
<!doctype html><html lang="ja"><head>
<meta charset="utf-8"/>
<title>Lidar Room</title>
<style>body{margin:0;overflow:hidden}</style>
</head><body>
<canvas id="webgl"></canvas>
<script type="module">
import * as THREE from 'https://cdn.jsdelivr.net/npm/three@0.160.0/build/three.module.js';
import { GLTFLoader } from 'https://cdn.jsdelivr.net/npm/three@0.160.0/examples/jsm/loaders/GLTFLoader.js';
import { OrbitControls } from 'https://cdn.jsdelivr.net/npm/three@0.160.0/examples/jsm/controls/OrbitControls.js';
const renderer = new THREE.WebGLRenderer({canvas:document.getElementById('webgl'),antialias:true});
renderer.setSize(innerWidth, innerHeight);
const scene = new THREE.Scene();
scene.background = new THREE.Color(0xeeeeee);
const camera = new THREE.PerspectiveCamera(60, innerWidth/innerHeight, 0.1, 1000);
camera.position.set(0, 1.6, 3);
new OrbitControls(camera, renderer.domElement);
const hemi = new THREE.HemisphereLight(0xffffff, 0x444444, 1);
scene.add(hemi);
const loader = new GLTFLoader();
loader.load('/room.glb', gltf => {
scene.add(gltf.scene);
animate();
});
function animate() {
requestAnimationFrame(animate);
renderer.render(scene, camera);
}
window.addEventListener('resize', () => {
camera.aspect = innerWidth/innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(innerWidth, innerHeight);
});
</script></body></html>
コード解説
行 | 役割 |
---|---|
GLTFLoader | 圧縮 .glb を非同期で読み込み |
OrbitControls | マウスドラッグで回転/ズーム |
HemisphereLight | 全体を柔らかく照らしノイズをマスク |
Tips:LiDAR点群はノイズ多め&頂点多いため、Draco 圧縮 + 適切なライトが体感レスポンスを大きく向上させます。
床クリックでテレポート(Raycaster)
import { Raycaster, Vector2 } from 'https://cdn.jsdelivr.net/npm/three@0.160.0/build/three.module.js';
const ray = new Raycaster();
const mouse = new Vector2();
renderer.domElement.addEventListener('dblclick', e=>{
mouse.x = (e.clientX / innerWidth) * 2 - 1;
mouse.y = -(e.clientY / innerHeight) * 2 + 1;
ray.setFromCamera(mouse, camera);
const intersects = ray.intersectObjects(scene.children[0].children, true);
if(intersects.length){
const p = intersects[0].point;
// Y座標を腰の高さに固定
camera.position.set(p.x, 1.6, p.z);
}
});
Step 3 ─ FirstPersonControls と WebXR 対応
FirstPersonControls
import { FirstPersonControls } from 'https://cdn.jsdelivr.net/npm/three@0.160.0/examples/jsm/controls/FirstPersonControls.js';
const fp = new FirstPersonControls(camera, renderer.domElement);
fp.movementSpeed = 2; // m/s
fp.lookSpeed = 0.08;
fp.update( delta )
をanimate()
内で呼び、
WASD 移動 + マウス視点 のゲーム的操作が可能。
WebXR(VR モード)
renderer.xr.enabled = true;
import { VRButton } from 'https://cdn.jsdelivr.net/npm/three@0.160.0/examples/jsm/webxr/VRButton.js';
document.body.appendChild(VRButton.createButton(renderer));
function animate(){
renderer.setAnimationLoop(()=>{
fp.update(0.01);
renderer.render(scene, camera);
});
}
Meta Quest / Pico のブラウザからアクセスし「Enter VR」を押すと、制作した部屋を 現実サイズで歩き回れる!
自動軽量化パイプライン ── 重い LiDAR メッシュを “Web 向け” に最適化
スマホ LiDAR スキャンが出力する OBJ/PLY はポリゴン数が数百万にも達し、そのまま Three.js に読み込むと FPS が 10 以下 に落ちることもしばしばです。本章では CLI のみ で以下を完全自動化するスクリプトを作成し、最終的に 120 MB ➜ 9 MB(92 % 削減)まで圧縮します。
流れ
- MeshlabServer でポリゴン簡略化(Quadric Edge Collapse)
- Blender Python で細部の Decimate と Draco 圧縮付き glTF へ変換
- glTF‑Pipeline でさらにメッシュを最適化
MeshlabServer:ポリゴン数 1/4 へ
Meshlab には GUI 版と CLI 版 (meshlabserver) があります。CLI 版は .mlx
スクリプトを読み込み一発処理が可能です。
quadric.mlx
の中身
<!DOCTYPE FilterScript>
<FilterScript>
<filter name="Quadratic Edge Collapse Decimation">
<Param type="RichInt" value="25" name="TargetFaceNum"/><!-- % 残存率 -->
<Param type="RichFloat" value="0" name="TargetPerc"/>
<Param type="RichFloat" value="1" name="PreserveBoundaryWeight"/>
<Param type="RichFloat" value="1" name="PreserveNormalWeight"/>
</filter>
</FilterScript>
実行コマンド
meshlabserver -i scan.obj \
-o scan_q.obj \
-s quadric.mlx \
-om vn vt # 法線・UV を保持
解説:
-i/-o
… 入出力ファイル-s
… フィルタスクリプト-om
… 出力属性 (v:頂点 n:法線 t:UV)
これだけで 300 万 Face ➜ 750 k Face に。
Blender Python:Draco 圧縮付き glTF へ
Blender は GUI で触るイメージですが、-b
オプションで 完全ヘッドレス 実行が可能です。以下スクリプトは ①OBJ 読み込み → ②Decimate ratio 20 % → ③Draco 圧縮 GLB 書き出し を行います。
# blender_batch.py
import bpy, sys, os
infile, outfile = sys.argv[-2:]
bpy.ops.import_scene.obj(filepath=infile)
obj = bpy.context.selected_objects[0]
mod = obj.modifiers.new(name='Decimate', type='DECIMATE')
mod.ratio = 0.2 # 20 %
bpy.ops.export_scene.gltf(filepath=outfile,
export_format='GLB',
export_draco_mesh_compression_enable=True,
export_draco_mesh_compression_level=7)
実行:
blender -b -P blender_batch.py -- scan_q.obj scan.glb
ポイント解説
パラメータ | 意味 |
---|---|
export_draco_mesh_compression_enable | glTF 拡張で Draco 圧縮を有効化 |
compression_level 1–10 | 数字が大きいほど高圧縮(= 変形誤差↑) |
glTF‑Pipeline:メッシュ+テクスチャを最終最適化
gltf-pipeline -i scan.glb \
-o scan_opt.glb \
--draco.compressionLevel 10 \
--textureCompression etc1s
解説:
--textureCompression etc1s
… KTX2 / BasisU で GPU ネイティブ圧縮 → ロード・描画高速化- 再度 Draco 圧縮を掛けても二重にはならず、既存メッシュを再最適化
結果 : 120 MB ➜ 9.4 MB / ロード 1.2 s ➜ 0.15 s (M1 Mac Safari)
ルチユーザー同期 ― socket.io + three‑mesh‑bvh 詳解
リアルタイム多人数歩行には ポジション同期 と 衝突コスト削減 が鍵です。
サーバー実装:最小 60 行
// server.js
import express from 'express';
import { createServer } from 'http';
import { Server } from 'socket.io';
const app = express();
const http = createServer(app);
const io = new Server(http, { cors: { origin: '*' } });
const players = new Map();
io.on('connection', sock => {
players.set(sock.id,{x:0,y:1.6,z:0,rotY:0});
sock.emit('init', Object.fromEntries(players));
sock.broadcast.emit('join', {id:sock.id, ...players.get(sock.id)});
sock.on('move', p => {
players.set(sock.id,p);
sock.broadcast.emit('update',{id:sock.id,...p});
});
sock.on('disconnect', ()=>{
players.delete(sock.id);
io.emit('leave', sock.id);
});
});
http.listen(3001);
解説
- init … 既存プレイヤー一覧を新規クライアントへ
- join / leave … 出入り通知で Three.js 側に Mesh を追加/削除
- move …
requestAnimationFrame
内で 10 Hz 送信 (帯域 ×10↓)
three‑mesh‑bvh で衝突判定を O(log n)
import { MeshBVH, acceleratedRaycast } from 'three-mesh-bvh';
THREE.Mesh.prototype.raycast = acceleratedRaycast;
const glb = await loader.loadAsync('/scan_opt.glb');
glb.scene.traverse(m=>{
if(m.isMesh){
m.geometry.boundsTree = new MeshBVH(m.geometry,{maxLeafTris:50});
}
});
acceleratedRaycast
を上書き → Raycaster が BVH 使用- 移動ごとに 下方向の Ray を飛ばし、床と距離 <0.1 で stay, でなければ落下防止
React Three Fiber + Drei UI 融合(詳細解説)
import { Canvas, useFrame, useThree } from '@react-three/fiber';
import { Environment, Stats, OrbitControls } from '@react-three/drei';
/** プレイヤー自身のカメラ + 衝突 */
function Player() {
const { camera } = useThree();
const vel = useRef(new THREE.Vector3());
useFrame((_state, delta) => {
// WASD で vel を更新 … 省略
camera.position.addScaledVector(vel.current, delta);
});
return null;
}
ポイント
行 | 技術 | 解説 |
useFrame | R3F | 毎フレーム移動・同期 |
Environment | drei | HDRI ライト一発設置 |
Stats | drei | FPS モニタで負荷確認 |
GitHub Pages デプロイ完全自動化
vite.config.ts
の base
設定
export default defineConfig({ base:'/lidar-metaverse/' });
gh-pages
ブランチへ CI デプロイ
- run: |
pnpm run build
npx gh-pages -d dist -b gh-pages
解説:gh-pages
パッケージは dist をコミット+ push。GitHub Pages で即 HTTPS 配信。
🔚 まとめ & 次に挑戦すること
次のテーマ | 具体例 |
NeRF 拡張 | Luma AI でキャプチャ → Instant‑NGP で高速推論 |
PBR ライト | three-gpu-pathtracer で実写品質 |
AI コラボ | GPT‑4o による 音声案内エージェント を WebSpeech + socket.io で統合 |
これで スマホ → ブラウザメタバース の全自動パイプラインが完成しました。フロントは Three.js、バックエンドは Node.js、CI/CD は GitHub Actions。ぜひ自分の部屋や製品をスキャンして、URL を友達にシェアしてみてください!