はじめに (対象読者・この記事でわかること)

この記事は、WebGLや3Dマップ表示に興味がある開発者、特にpy3dtilesやdeck.glを使用して3Dタイルを表示しようとしている方を対象としています。特に、大量の3Dモデルを効率的に表示するためにpy3dtilesでタイルセットを生成し、deck.glで表示する際にズームアウトすると一部のタイルしか表示されない問題に直面している開発者に向けています。

この記事を読むことで、py3dtilesで作成した3Dタイルをdeck.glで表示する際の基本的な設定方法を理解し、ズームアウト時に一部しか表示されない問題の根本原因を特定し、具体的な解決策を実装できるようになります。また、同様の問題に直面した際のデバッグ手法やベストプラクティスも学ぶことができます。

前提知識

この記事を読み進める上で、以下の知識があるとスムーズです。

  • HTML/CSSの基本的な知識
  • JavaScript/TypeScriptの基本的な知識
  • WebGLの基本的な概念
  • deck.glの基本的な使い方
  • py3dtilesの基本的な使い方

3Dタイル表示の概要と問題の背景

3Dタイルは、大量の3Dジオメトリデータを効率的に配信するための標準フォーマットです。特に地理空間情報を扱うアプリケーションで広く利用されており、py3dtilesはPythonライブラリとして3Dタイルの生成とマージを容易にしてくれます。

一方、deck.glはUberが開発したWebGLベースの可視化ライブラリで、大量のデータを効率的に描画することに特化しています。この二つを組み合わせることで、大規模な3DモデルをWebブラウザ上でスムーズに表示できます。

しかし、実際にこの二つを組み合わせて使用すると、ズームアウト時に一部の3Dタイルしか表示されない、という問題に多くの開発者が直面します。これは、3DタイルのLOD(Level of Detail)設定やデッキのカメラ制御、タイルの読み込みタイミングなど、様々な要因が複雑に絡み合っているためです。

この問題を放置すると、ユーザー体験が著しく低下し、アプリケーションの信頼性にも影響を与えます。そこで、この記事では問題の原因から具体的な解決策までを詳しく解説します。

問題解決のための具体的な手順と実装方法

ステップ1: 問題の再現と原因の特定

まず、問題を正確に再現し、その原因を特定することが重要です。以下の手順でデバッグを始めましょう。

  1. デモアプリケーションの作成: 最小限のコードで問題を再現するシンプルなアプリケーションを作成します。
Javascript
import React, { useEffect, useRef } from 'react'; import { Deck } from '@deck.gl/core'; import { Tile3DLayer } from '@deck.gl/geo-layers'; const Map3DTiles = () => { const deckRef = useRef(null); useEffect(() => { const deck = new Deck({ canvas: deckRef.current, initialViewState: { longitude: 139.6917, latitude: 35.6895, zoom: 14, pitch: 60, bearing: 0 }, controller: true, layers: [ new Tile3DLayer({ id: 'tile3d-layer', data: 'path/to/your/tileset.json', onTileLoad: (tile) => { console.log('Tile loaded:', tile); }, onTileError: (error, tile) => { console.error('Tile error:', error, tile); } }) ] }); return () => deck.finalize(); }, []); return <div ref={deckRef} style={{ width: '100%', height: '100vh' }} />; }; export default Map3DTiles;
  1. デバッグ情報の有効化: deck.glのデバッグモードを有効にし、タイルの読み込み状態を監視します。
Javascript
new Deck({ // ... 他の設定 ... debug: true, _debug: true, // 追加のデバッグ情報 layers: [ new Tile3DLayer({ // ... 他の設定 ... onTileLoad: (tile) => { console.log('Tile loaded:', tile, 'content:', tile.content); }, onTileError: (error, tile) => { console.error('Tile error:', error, tile); }, onTileStateChange: (tile, prevState) => { console.log('Tile state changed:', tile.id, prevState, tile.state); } }) ] });
  1. ズームアウト時の挙動確認: ズームアウト時にどのタイルが読み込まれ、どのタイルが読み込まれないかを確認します。特に、以下の点に注目してください。
  • ズームレベルが変わったときに、期待通りにタイルが読み込まれているか
  • タイルの読み込みがタイムアウトしていないか
  • エラーメッセージが出ていないか
  • タイルの状態が「loaded」ではなく「failed」や「parsing」のままになっていないか

ステップ2: py3dtilesでのタイル生成時の設定確認

問題がデック側ではなく、py3dtilesでのタイル生成時の設定にある場合もあります。以下の点を確認してください。

  1. タイルセットの構造: 生成されたtileset.jsonを確認し、タイルの階層構造が正しいかを確認します。
Json
{ "asset": { "version": "1.0" }, "geometricError": 500, "root": { "boundingVolume": { "region": [minLon, minLat, maxLon, maxLat, minHeight, maxHeight] }, "geometricError": 500, "refine": "REPLACE", "content": { "uri": "0/0/0.b3dm" } } }
  1. geometricErrorの設定: 各タイルのgeometricError値が適切か確認します。この値が大きすぎると、親タイルが子タイルよりも優先的に表示されてしまいます。

  2. refineプロパティ: 「REPLACE」または「ADD」が適切に設定されているか確認します。通常、3Dタイルでは「REPLACE」が使用されます。

  3. タイルの分割: タイルが適切なサイズに分割されているか確認します。タイルが大きすぎると、デック側で処理する負荷が増え、表示問題の原因になります。

ステップ3: deck.gl側の設定最適化

問題がデック側にある場合は、以下の設定を調整することで解決できる場合があります。

  1. Tile3DLayerの設定調整:
Javascript
new Tile3DLayer({ id: 'tile3d-layer', data: 'path/to/your/tileset.json', // 追加設定 maxZoom: 20, minZoom: 5, maxCacheSize: 1000, maxCacheByteSize: 50 * 1024 * 1024, // 50MB maxConcurrency: 6, refine: 'REPLACE', // パフォーマンス向上のための設定 pointCloud: false, wireframe: false, _subLayerProps: { pointCloud: { opacity: 1.0 } }, // デバッグ用 onTileLoad: (tile) => { console.log('Tile loaded:', tile); }, onTileError: (error, tile) => { console.error('Tile error:', error, tile); } })

特に重要なのは、maxConcurrencymaxCacheSizeの設定です。これらの値が小さすぎると、同時に読み込めるタイルの数が制限され、ズームアウト時に十分な数のタイルが読み込まれなくなります。

  1. viewStateの調整: カメラの初期位置や視錐体の設定を確認します。
Javascript
initialViewState: { longitude: 139.6917, latitude: 35.6895, zoom: 14, // 適切な初期ズームレベル pitch: 60, bearing: 0, // 追加設定 maxZoom: 20, minZoom: 5 }
  1. デックのオプション調整:
Javascript
new Deck({ // ... 他の設定 ... // パフォーマンス向上のための設定 _fpsCounter: true, _hoveredObject: null, webgl: { preserveDrawingBuffer: true, alpha: true, depth: true, stencil: true, antialias: true, premultipliedAlpha: true, preferWebGL1: true }, // カスタムタイムアウト設定 _animationLoop: { frameRate: 60, throttleFrameRate: 30 } });

ステップ4: カスタムローダーの実装

標準のローダーで問題が解決しない場合は、カスタムローダーを実装することで問題を解決できる場合があります。

Javascript
import { Tile3DLoader } from '@loaders.gl/3d-tiles'; import { globals } from '@loaders.gl/core'; // カスタムローダーの設定 globals.setWorkerURL('path/to/your/worker.js'); const customLoader = new Tile3DLoader({ maxConcurrency: 6, maxCacheSize: 1000, // カスタムオプション overrideMethods: { fetch: async (url, options) => { // カスタムfetchロジック return fetch(url, options); }, parse: async (arrayBuffer, options) => { // カスタムパースロジック return await Tile3DLoader.parse(arrayBuffer, options); } } }); new Tile3DLayer({ // ... 他の設定 ... loader: customLoader, onTileLoad: (tile) => { console.log('Tile loaded with custom loader:', tile); } });

ステップ5: タイルのプリロードとキャッシュ最適化

  1. プリロードの実装: ユーザーがズームアウトする前に、必要なタイルを事前に読み込むことで表示問題を解決できます。
Javascript
const preloadTiles = (deck, tileset, zoomLevels) => { const preloadedTiles = []; zoomLevels.forEach(zoom => { const tiles = getTilesForZoom(tileset, zoom); tiles.forEach(tile => { if (!isTileLoaded(deck, tile)) { deck.loadTile(tile); preloadedTiles.push(tile); } }); }); return preloadedTiles; }; // 使用例 const preloadedTiles = preloadTiles(deck, tileset, [10, 11, 12, 13]);
  1. キャッシュ戦略の最適化: タイルのキャッシュ戦略を最適化します。
Javascript
const cacheStrategy = { // どのタイルをキャッシュするか shouldCacheTile: (tile) => { return tile.state === 'loaded' && tile.content && tile.content.boundingVolume; }, // キャッシュの優先順位 getCachePriority: (tile) => { // ズームレベルが低いほど優先度を高くする return 10 - tile.content?.geometricError || 0; }, // キャッシュの最大サイズ maxCacheSize: 1000 };

ハマった点やエラー解決

  1. タイルの読み込みタイムアウト問題 - 症状: タイルが読み込まれず、コンソールにタイムアウトエラーが表示される - 原因: ネットワーク遅延またはタイルサイズが大きすぎる - 解決策: タイムアウト時間を延長するか、タイルサイズを小さく分割する

  2. メモリ不足問題 - 症状: ズームアウト時にブラウザがクラッシュする - 原因: 多くのタイルを同時にメモリに読み込んでいる - 解決策: maxCacheSizemaxCacheByteSizeを適切に設定し不要なタイルを解放する

  3. タイルの重なり問題 - 症状: タイルが重なって表示される - 原因: refineプロパティの設定ミス - 解決策: refineプロパティを「REPLACE」に設定する

  4. カメラの視錐体クリッピング問題 - 症状: 一部のタイルが表示されない - 原因: カメラの視錐体の設定が不適切 - 解決策: fovnearfarの値を調整する

解決策

これまでのステップを踏んで問題が解決しない場合は、以下の最終的な解決策を検討してください。

  1. タイルセットの再生成: py3dtilesを使い直して、より適切なパラメータでタイルセットを再生成します。特にgeometricError値とタイルの分割数を見直します。
Bash
# py3dtilesを使ったタイルセット再生成の例 py3dtiles merge --input-directory path/to/input --output-directory path/to/output --geometric-error 100 --max-tiles 100
  1. デックのバージョンアップ: deck.glの最新バージョンにアップデートし、既知のバグが修正されていないか確認します。
Bash
npm install @deck.gl/core@latest @deck.gl/geo-layers@latest
  1. カスタムシェーダーの実装: パフォーマンス問題が深刻な場合は、カスタムシェーダーを実装して描画処理を最適化します。
Javascript
const customShader = ` attribute vec3 positions; attribute vec3 normals; uniform mat4 modelViewMatrix; uniform mat4 projectionMatrix; varying vec3 vNormal; void main() { vNormal = normals; gl_Position = projectionMatrix * modelViewMatrix * vec4(positions, 1.0); } `; new Tile3DLayer({ // ... 他の設定 ... shading: { lightsPosition: [[-125, 200, 100]], ambientLight: [0.1, 0.1, 0.1], diffuseLight: [0.7, 0.7, 0.7], specularLight: [0.2, 0.2, 0.2], material: true, parameters: { depthTest: true, depthFunc: 0x0203, // LEqual blend: true, blendFunc: [0x0302, 0x0303], // SrcAlpha, OneMinusSrcAlpha blendEquation: 0x8006 // FUNC_ADD } } });
  1. サーバーサイドでのタイル最適化: タイル配信サーバーで圧縮や最適化を行い、クライアント側での負荷を軽減します。
Javascript
// サーバーサイドでのタイル最適化例 (Node.js) const express = require('express'); const compression = require('compression'); const app = express(); app.use(compression()); app.use('/tiles', express.static('path/to/tiles', { maxAge: '1y', etag: true, lastModified: true, setHeaders: (res, path) => { if (path.endsWith('.b3dm')) { res.set('Content-Encoding', 'gzip'); } } })); app.listen(3000, () => { console.log('Server running on port 3000'); });

まとめ

本記事では、py3dtilesで作成した3Dタイルをdeck.glで表示する際にズームアウトすると一部しか表示されない問題の原因と解決策について詳しく解説しました。

  • 要点1: この問題はタイルのLOD設定、デックのカメラ制御、タイルの読み込みタイミングなど複数の要因が絡み合っている
  • 要点2: 問題解決には、デバッグ情報の有効化、py3dtilesでのタイル生成時の設定確認、deck.gl側の設定最適化、カスタムローダーの実装などが必要
  • 要点3: 最終的にはタイルセットの再生成、デックのバージョンアップ、カスタムシェーダーの実装、サーバーサイドでの最適化などが有効な解決策となる

この記事を通して、3Dタイル表示におけるパフォーマンス問題の特定方法と具体的な解決手法を学ぶことができたはずです。これにより、ユーザーに快適な3Dマップ体験を提供できるようになります。

今後は、3Dタイルの動的生成やリアルタイム更新、さらに大規模な3Dモデルの効率的な表示方法についても記事にする予定です。

参考資料

参考にした記事、ドキュメント、書籍などは以下の通りです。