JavaScript

スマホ3Dスキャンから自動メタバース化まで—LiDAR×Three.js完全フルスタックガイド

はじめに ─ スマホで撮った部屋が即メタバース

✨ 斬新ポイント

  • スマホ LiDARThree.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 で部屋をスキャン

  1. スキャンモード → LiDAR を選択
  2. 部屋内をゆっくり 1 周(天井 / 床も忘れず)
  3. 完了後「処理」を押し、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 % 削減)まで圧縮します。

流れ

  1. MeshlabServer でポリゴン簡略化(Quadric Edge Collapse)
  2. Blender Python で細部の Decimate と Draco 圧縮付き glTF へ変換
  3. 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_enableglTF 拡張で 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 を追加/削除
  • moverequestAnimationFrame 内で 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;
}

ポイント

技術解説
useFrameR3F毎フレーム移動・同期
EnvironmentdreiHDRI ライト一発設置
StatsdreiFPS モニタで負荷確認

GitHub Pages デプロイ完全自動化

vite.config.tsbase 設定

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 を友達にシェアしてみてください!