見出し画像

たった30分で360度写真が歩き回れる3D空間に変わる!Marble.AI × PlayCanvas × Gaussian Splatting実装ガイド

こんにちは!チャリセです

皆さん、3D Gaussian Splatting(GSplat)ってご存知ですか?

最近、X(旧Twitter)のタイムラインや技術ブログで頻繁に見かけるようになったこの技術。実は今、Web3D界隈でちょっとした革命が起きているんです。フォトグラメトリよりも手軽に、そして圧倒的に綺麗な実写空間をWebブラウザ上に再現できる技術として、あちこちで3D Gaussian Splattingの話題を見かけるようになって、早速試してみることにしました。

実際、PlayCanvas自体が3D Gaussian Splattingに本気で取り組んでいるのが印象的です。SuperSplat Editorという専用のWebエディタを公開したり、gsplatコンポーネントをエンジンにネイティブ実装したり、さらにはSplatTransformというCLIツールまで開発してオープンソース化しています。これだけ充実したエコシステムが整っているのを見て、「今が試すベストタイミングだ」と確信しました。

特に興味深かったのが、PlayCanvasが独自に開発したSOG(Splat Optimized for GPU)形式です。PLYファイルを15-20倍圧縮できるという触れ込みで、「本当にそんなに圧縮できるの?画質は大丈夫?」と半信半疑でした。

そこで、話題のMarble.AIで360度写真から3DGSデータを生成し、実際にPLYからSOGへの変換、そしてPlayCanvasのランタイムで動的に読み込むところまで、一通り試してみることにしました。

今回は、その過程で得られた知見と、実際に動作するコードの重要な部分を共有します。特に、多くの開発者が気になっているであろう「なぜSOG変換が必要なのか」「ランタイムでどうやって動的に読み込むのか」について、詳しく解説していきたいと思います。

3D Gaussian Splattingとは?

まず、3D Gaussian Splattingについて簡単に説明します。

従来の3Dモデルがポリゴン(三角形の面)で形を作るのに対し、GSplatは無数の半透明な楕円体(ガウス分布を持つ点)で3D空間を表現します。霧の粒子が集まって形を作るようなイメージです。

GSplatの最大の特徴:

  • 写真から直接生成できるため、照明や質感が完璧に再現される

  • GPUで高速にレンダリングできる(60FPS以上)

  • 視点を自由に動かせる真の3D表現

今回のワークフロー全体像

実装する流れは以下の通りです:

  1. Marble.AIで360度画像をアップロード → PLYファイルとGLBファイル(コリジョン用)を取得

  2. PLYファイルをSOG形式に変換(ファイルサイズを20分の1に圧縮)

  3. ファイルをWebサーバーにアップロード

  4. PlayCanvasでランタイムに動的読み込み

  5. 物理エンジンと統合(コリジョンメッシュの適用)

それでは、各ステップを詳しく見ていきましょう。

ステップ1:Marble.AIで360度画像から3DGSデータを生成

Marble.AIとは

Marble.AIは、World Labs社が開発した画期的なサービスです。1枚の360度写真から、わずか3-5分で高品質な3D Gaussian Splatデータを生成できます。

実際の手順

  1. https://marble.worldlabs.ai にアクセス

  2. 360度写真をアップロード(JPEGまたはPNG、8K推奨)

  3. 10-15分待つ

  4. 生成完了後、以下の2つのファイルをダウンロード:

    • PLYファイル:GSplatのメインデータ

    • GLBファイル:物理演算用のコリジョンメッシュ

360度画像アップロードしてPLY化
PLYとGLBのダウンロード

ポイント: PLYファイルには視覚情報のみで物理的な形状データが含まれないため、壁や床として機能させるには別途GLBファイルが必要です。

ステップ2:PLYをSOGに変換する理由と方法

なぜSOG変換が必要なのか?

実際に生成されたPLYファイルのサイズを見て驚きました:

sample.ply: 129MB

これをそのままWebで配信すると:

  • 読み込み時間:30-60秒(高速回線でも)

  • メモリ使用量:モバイルではメモリ不足でクラッシュ

  • CDNコスト:転送量が莫大

SOG形式の威力

SOGはPlayCanvasが開発したWeb配信に最適化された圧縮形式です。実際の圧縮結果:

sample.ply (129MB) → sample_output.sog (21.4MB) 

読み込み時間も45秒から4.2秒に短縮。これは革命的です。

# 1. SplatTransformをインストール
npm install -g @playcanvas/splat-transform

# 2. PLYからSOGに変換
splat-transform sample.ply sample_output.sog

画質を確認しましたが、視覚的な劣化はほとんど感じられませんでした。これは本当に素晴らしい圧縮技術です。

ステップ3:サーバーへのアップロード

変換したSOGファイルとGLBファイルをWebサーバーにアップロードし、そのURLを使ってPlayCanvasのランタイムからダウンロード・表示させます。

重要:CORS設定
PlayCanvasのランタイムから外部URLのファイルをダウンロードするには、CORS(Cross-Origin Resource Sharing)の設定が必須です。

ステップ4:PlayCanvasでの実装 - コード解説

ここからが本題です。実際のコードを見ながら、どのように実装するか解説します。

基本的なスクリプト構造

var LoadGaussianSplat = pc.createScript('loadGaussianSplat');

// スクリプト属性の定義(PlayCanvasエディタで設定可能)
LoadGaussianSplat.attributes.add('splatUrl', {
    type: 'string',
    title: 'Splat URL',
    description: 'SOGまたはPLYファイルのURL'
});

LoadGaussianSplat.attributes.add('fileType', {
    type: 'string',
    title: 'File Type',
    enum: [
        { 'auto': 'Auto Detect' },
        { 'sog': 'SOG File (Compressed)' },
        { 'ply': 'PLY File (Uncompressed)' }
    ],
    default: 'auto'
});

SOGファイルのランタイム読み込み(核心部分)

ここが最も重要な部分です。PlayCanvasでGSplatファイルを動的に読み込む方法:

LoadGaussianSplat.prototype.initialize = function() {
    console.log('GSplat読み込み開始:', this.splatUrl);
    
    // ステップ1: ファイルタイプの判定
    let fileExtension = this.fileType;
    if (fileExtension === 'auto') {
        // URLの拡張子から自動判定
        const url = this.splatUrl.toLowerCase();
        if (url.includes('.sog')) {
            fileExtension = 'sog';
        } else if (url.includes('.ply')) {
            fileExtension = 'ply';
        }
    }
    
    // ステップ2: PlayCanvasのアセットとして登録
    const assetName = 'splat_' + Date.now();  // ユニークな名前を生成
    const assetOptions = {
        url: this.splatUrl
    };
    
    // SOGの場合、フォーマットを明示的に指定
    if (fileExtension === 'sog') {
        assetOptions.format = 'sog';
        console.log('SOG形式検出 - 高速読み込みモード');
    }
    
    // ステップ3: gsplatタイプのアセットを作成
    // ここがポイント!'gsplat'はPlayCanvas専用のアセットタイプ
    const asset = new pc.Asset(assetName, 'gsplat', assetOptions);
    
    // ステップ4: アセットレジストリに追加して読み込み開始
    this.app.assets.add(asset);
    
    // ステップ5: 読み込み完了時の処理を設定
    asset.on('load', () => {
        console.log('GSplat読み込み完了!');
        this.onSplatLoaded(asset);
    });
    
    // ステップ6: 実際の読み込みを開始
    this.app.assets.load(asset);
    this.splatAsset = asset;  // 後で参照するため保存
};

コードの重要ポイント解説:

  1. pc.Assetクラスの使用:PlayCanvasでランタイムにアセットを作成する標準的な方法

  2. 'gsplat'タイプ:PlayCanvasがGaussian Splat用に用意した特別なアセットタイプ

  3. 非同期処理:loadイベントで読み込み完了を検知

  4. フォーマット指定:SOGの場合は明示的にformat: 'sog'を指定することで最適化

GSplatコンポーネントへの適用

読み込み完了後、エンティティに表示する処理:

ototype.onSplatLoaded = function(asset) {
    // gsplatコンポーネントをエンティティに追加
    if (!this.entity.gsplat) {
        this.entity.addComponent('gsplat', {
            asset: asset.id  // 読み込んだアセットのIDを指定
        });
    }
    
    // スケールの調整(必要に応じて)
    this.entity.setLocalScale(this.scale, this.scale, this.scale);
    
    // マテリアルの設定
    if (this.entity.gsplat && this.entity.gsplat.material) {
        // ブレンディングを有効化(透明感のある表現のため)
        this.entity.gsplat.material.blend = true;
    }
    
    // カスタムイベントを発火(他のスクリプトから利用可能)
    this.app.fire('splat:loaded', this.entity);
};

ステップ5:コリジョンメッシュの統合

GSplatは見た目だけなので、物理的な衝突判定を追加する必要があります。

GLBファイルの読み込み

LoadGaussianSplat.prototype.loadCollisionMesh = function() {
    console.log('コリジョンメッシュ読み込み:', this.collisionUrl);
    
    // GLBファイルは'model'タイプとして読み込む
    const collisionAsset = new pc.Asset('collision_' + Date.now(), 'model', {
        url: this.collisionUrl
    });
    
    this.app.assets.add(collisionAsset);
    
    collisionAsset.on('load', () => {
        console.log('コリジョンメッシュ読み込み完了');
        this.setupCollision(collisionAsset);
    });
    
    this.app.assets.load(collisionAsset);
};

物理エンジンへの組み込み

LoadGaussianSplat.prototype.setupCollision = function(collisionAsset) {
    // コリジョン用の子エンティティを作成
    this.collisionEntity = new pc.Entity('CollisionMesh');
    
    // ポイント1: メッシュコリジョンを設定
    this.collisionEntity.addComponent('collision', {
        type: 'mesh',  // GLBの形状をそのまま使用
        asset: collisionAsset.id
    });
    
    // ポイント2: 静的リジッドボディとして設定
    this.collisionEntity.addComponent('rigidbody', {
        type: 'static',  // 動かない壁・床として機能
        mass: 0          // 静的オブジェクトは質量0
    });
    
    // デバッグ用:コリジョンメッシュを半透明で表示
    if (this.showCollisionMesh) {
        this.collisionEntity.addComponent('model', {
            asset: collisionAsset.id
        });
        // 半透明化処理...
    }
    
    // 親エンティティに追加
    this.entity.addChild(this.collisionEntity);
    
    console.log('物理コリジョン設定完了');
};

重要なポイント:

  • type: 'mesh':複雑な形状のコリジョンを実現

  • type: 'static':重力の影響を受けない固定オブジェクト

  • 親子関係:GSplatエンティティの子として配置

その結果、以下のようにPlaycanvasに問題なくSplatデータとコライダーをランタイム中に読み込みことができました!

Playcanvasランタイムアングル1
Playcanvasランタイムアングル2
Playcanvasランタイムアングル3

3D Gaussian SplattingとPlayCanvasの組み合わせは、想像以上に強力でした。特に:

  1. SOG形式の圧縮効率が素晴らしい(20倍圧縮で品質維持)

  2. ランタイム読み込みが簡単に実装できる

  3. 物理エンジンとの統合もスムーズ

これにより、フォトリアリスティックな3D空間を、誰でも簡単にWebで公開できる時代が来たと実感しました。

ぜひ一度お試しください!!
Happy Coding!!


参考リンク:


いいなと思ったら応援しよう!