はじめに
2023年5月11日にGoogleからMap Tiles API を通じて利用できるPhotorealistic 3D Tilesが実験的にリリースされました。3D Tilesのサービス提供地域は、日本を含む49か国の2,500以上の都市になります。また、Photorealistic 3D Tilesは、現時点では実験的な位置付けのため、無償で提供されています。本記事では、deck.glを用いた、Photorealistic 3D Tilesの利用について説明します。
Photorealistic 3D Tilesの特徴
- 3Dメッシュモデルで提供
- Google Earthと同じ3Dマップソースを使用
- CesiumJSやdeck.gl等のWeb地図ライブラリーでレンダリングできる。
- Map Tiles API(Photorealistic 3D Tiles)の使用量上限は1日あたり最大250,000タイルリクエスト
前提条件
- Google Maps PlatformのAPIキーを取得済みであること。
※Google Maps Platform APIキーの取得・発行についてはこちらを参照してください。
https://www.zenrin-datacom.net/business/gmapsapi/api_key/index.html
deck.glでPhotorealistic 3D Tilesを表示してみる
- Googleのサンプルコードを参考に、deck.glでPhotorealistic 3D Tilesを表示します。
- APIキーは、ご自身で用意してください。
deck.glでPhotorealistic 3D Tilesに洪水浸水想定区域データを重ねてみる
- deck.glには、3Dモデルに2DのデータをオーバーレイできるTerrainExtensionという拡張機能があります(v8.9で新たに追加された機能ですが、実験的な位置付けの機能になります)。
- この拡張機能は、2Dのデータ(geojson)を3Dモデルの表面にテクスチャのように貼り付けます。
- Googleのサンプルコードとdeck.glのTerrainExtensionを用いて、Photorealistic 3D Tilesと国土数値情報の洪水浸水想定区域データを重ねて表示します。
コード
- APIキーと国土数値情報の洪水浸水想定区域データ(geojson)は、ご自身で用意してください。
<!DOCTYPE html>
<html>
<head>
<title>Google 3D tiles example</title>
<script src="https://unpkg.com/deck.gl@latest/dist.min.js"></script>
<script src="https://d3js.org/d3.v5.min.js"></script>
<style>
body {
margin: 0;
padding: 0;
}
#map {
position: absolute;
top: 0;
bottom: 0;
width: 100%;
}
#credits {
position: absolute;
bottom: 0;
right: 0;
padding: 2px;
font-size: 15px;
color: white;
text-shadow: -1px 0 black, 0 1px black, 1px 0 black, 0 -1px black;
}
</style>
</head>
<body>
<div id="map"></div>
<div id="credits"></div>
<script>
// 【参考】https://developers.google.com/maps/documentation/tile/use-renderer?hl=ja
const GOOGLE_API_KEY = 'YOUR_API_KEY';
const TILESET_URL = `https://tile.googleapis.com/v1/3dtiles/root.json`;
const data = d3.json('./A31-20-21_8303020033.geojson');
const creditsElement = document.getElementById('credits');
const deckgl = new deck.DeckGL({
container: 'map',
initialViewState: {
latitude: 36.368086,
longitude: 140.475681,
zoom: 15,
pitch: 60,
height: 200
},
controller: true,
layers: [
new deck.Tile3DLayer({
id: 'google-3d-tiles',
data: TILESET_URL,
loadOptions: {
fetch: {
headers: {
'X-GOOG-API-KEY': GOOGLE_API_KEY
}
}
},
onTilesetLoad: tileset3d => {
tileset3d.options.onTraversalComplete = selectedTiles => {
const credits = new Set();
selectedTiles.forEach(tile => {
const { copyright } = tile.content.gltf.asset;
copyright.split(';').forEach(credits.add, credits);
creditsElement.innerHTML = [...credits].join('; ');
});
return selectedTiles;
}
},
operation: 'terrain+draw'
}),
new deck.GeoJsonLayer({
id: 'max-kouzui',
data: data,
stroked: false,
filled: true,
getFillColor: ({ properties }) => {
// 浸水深ランクで色分け
const { A31_205 } = properties;
if (A31_205 == 1)
return [247, 245, 169]
else if (A31_205 == 2)
return [255, 216, 192]
else if (A31_205 == 3)
return [255, 183, 183]
else if (A31_205 == 4)
return [255, 145, 145]
else if (A31_205 == 5)
return [242, 133, 201]
else if (A31_205 == 6)
return [220, 122, 220]
},
opacity: 0.4,
extensions: [new deck._TerrainExtension()]
})
]
});
</script>
</body>
</html>
deck.glでPhotorealistic 3D Tilesにポリゴンを重ねて、ざっくりとした高さを測ってみる
- Googleのサンプルコードを参考に、Photorealistic 3D Tilesと都道府県ポリゴンデータを重ねて、ざっくりとした建物の高さがわかる地図を作ります(後述のとおり、標高は考慮されていないのであくまでも建物の高さの目安がわかるだけのものです)。
- 都道府県ポリゴンデータは、以下のgeojsonを使用させていただきました。
コード
- APIキーと都道府県ポリゴンデータ(geojson)は、ご自身で用意してください。
- deck.glのGeoJsonLayerのgetElevationで設定できる高さは、ジオイド高を含む楕円体高のようです。
出典)https://www.mlit.go.jp/plateau/learning/tpc03-4/#p3_5_4
<!DOCTYPE html>
<html>
<head>
<title>Google 3D tiles example</title>
<script src="https://unpkg.com/deck.gl@latest/dist.min.js"></script>
<script src="https://d3js.org/d3.v5.min.js"></script>
<style>
body {
margin: 0;
padding: 0;
}
#map {
position: absolute;
top: 0;
bottom: 0;
width: 100%;
}
#credits {
position: absolute;
bottom: 0;
right: 0;
padding: 2px;
font-size: 15px;
color: white;
text-shadow: -1px 0 black, 0 1px black, 1px 0 black, 0 -1px black;
}
#control-panel {
position: absolute;
top: 0;
left: 0;
margin: 12px;
padding: 20px;
font-size: 12px;
line-height: 1.5;
z-index: 1;
background: #fff;
font-family: Helvetica, Arial, sans-serif;
box-shadow: 0 0 4px rgba(0, 0, 0, 0.15);
}
label {
display: inline-block;
width: 140px;
}
</style>
</head>
<body>
<div id="control-panel">
<div>
<label>getElevation</label>
<input id="getElevation" type="range" min="0" max="300" step="0.1" value="0"></input>
<span id="getElevation-value"></span>
</div>
<div>
<label>opacity</label>
<input id="opacity" type="range" min="0" max="1" step="0.1" value="0.3"></input>
<span id="opacity-value"></span>
</div>
</div>
<div id="map"></div>
<div id="credits"></div>
<script>
// 【参考】https://developers.google.com/maps/documentation/tile/use-renderer?hl=ja
const GOOGLE_API_KEY = 'YOUR_API_KEY';
const TILESET_URL = `https://tile.googleapis.com/v1/3dtiles/root.json`;
const data = d3.json('./prefectures.geojson');
const creditsElement = document.getElementById('credits');
const OPTIONS = ['getElevation', 'opacity'];
const deckgl = new deck.DeckGL({
container: 'map',
initialViewState: {
latitude: 35.6812362,
longitude: 139.7671248,
zoom: 15,
// bearing: 90,
pitch: 60,
height: 200
},
controller: true
});
const renderLayer = () => {
const options = {};
OPTIONS.forEach(key => {
const value = document.getElementById(key).value;
document.getElementById(key + '-value').innerHTML = value;
options[key] = Number(value);
});
const tile3dLayer = new deck.Tile3DLayer({
id: 'google-3d-tiles',
data: TILESET_URL,
loadOptions: {
fetch: {
headers: {
'X-GOOG-API-KEY': GOOGLE_API_KEY
}
}
},
onTilesetLoad: tileset3d => {
tileset3d.options.onTraversalComplete = selectedTiles => {
const credits = new Set();
selectedTiles.forEach(tile => {
const { copyright } = tile.content.gltf.asset;
copyright.split(';').forEach(credits.add, credits);
creditsElement.innerHTML = [...credits].join('; ');
});
return selectedTiles;
}
},
operation: 'terrain+draw'
});
const geoJsonLayer = new deck.GeoJsonLayer({
id: 'prefectures',
data: data,
stroked: false,
filled: true,
extruded: true,
// wireframe: true,
getFillColor: d => [0, 170, 255],
getElevation: 0,
opacity: 0.3,
...options
});
deckgl.setProps({
layers: [tile3dLayer, geoJsonLayer]
});
}
renderLayer();
OPTIONS.forEach(key => {
document.getElementById(key).oninput = renderLayer;
});
</script>
</body>
</html>
deck.glでPhotorealistic 3D Tilesに太陽の位置に応じて変化する影をつけてみる
- Googleのサンプルコード、deck.glのSunLight及び下記のGitHubのソースコード(Chee Aunさんに感謝です)を参考に、Photorealistic 3D Tilesに太陽の位置に応じて変化する影をつけます。
- deck.glのSunLightは、現時点では実験的な位置付けの機能ですが、太陽光をシミュレートできる機能です。
コード
- APIキーは、ご自身で用意してください。
<!DOCTYPE html>
<html>
<head>
<title>Google 3D tiles example</title>
<script src="https://unpkg.com/deck.gl@latest/dist.min.js"></script>
<style>
body {
margin: 0;
padding: 0;
}
#map {
position: absolute;
top: 0;
bottom: 0;
width: 100%;
}
#credits {
position: absolute;
bottom: 0;
right: 0;
padding: 2px;
font-size: 15px;
color: white;
text-shadow: -1px 0 black, 0 1px black, 1px 0 black, 0 -1px black;
}
#control-panel {
position: absolute;
top: 0;
left: 0;
margin: 12px;
padding: 20px;
font-size: 12px;
line-height: 1.5;
z-index: 1;
background: #fff;
font-family: Helvetica, Arial, sans-serif;
box-shadow: 0 0 4px rgba(0, 0, 0, 0.15);
}
</style>
</head>
<body>
<div id="control-panel">
<p><b>Shadows</b></p>
<div id="time-display"></div>
<input type="range" min="0" max="1440" step="5" value="480" id="time-slider" />
</div>
<div id="map"></div>
<div id="credits"></div>
<script>
// 【参考】https://developers.google.com/maps/documentation/tile/use-renderer?hl=ja
const GOOGLE_API_KEY = 'YOUR_API_KEY';
const TILESET_URL = `https://tile.googleapis.com/v1/3dtiles/root.json`;
const creditsElement = document.getElementById('credits');
// 太陽光を作成
const sunLight = new deck._SunLight({
// timestamp: 1554927200000,
color: [255, 255, 255],
intensity: 1.0,
_shadow: true // 影を生成
});
// 環境光を作成
const ambientLight = new deck.AmbientLight({
color: [255, 255, 255],
intensity: 1.0,
});
// ライティングエフェクトを作成
const lightingEffect = new deck.LightingEffect({ ambientLight, sunLight });
// deck.glのキャンバスを初期化
const deckgl = new deck.DeckGL({
container: 'map',
initialViewState: {
latitude: 35.6585805,
longitude: 139.7454329,
zoom: 16,
pitch: 60,
height: 200
},
controller: true,
effects: [lightingEffect],
layers: [
new deck.Tile3DLayer({
id: 'google-3d-tiles',
data: TILESET_URL,
loadOptions: {
fetch: {
headers: {
'X-GOOG-API-KEY': GOOGLE_API_KEY
}
}
},
onTilesetLoad: tileset3d => {
tileset3d.options.onTraversalComplete = selectedTiles => {
const credits = new Set();
selectedTiles.forEach(tile => {
const { copyright } = tile.content.gltf.asset;
copyright.split(';').forEach(credits.add, credits);
creditsElement.innerHTML = [...credits].join('; ');
});
return selectedTiles;
}
}
})
],
// 影を表示
parameters: {
depthTest: true
}
});
// スライダーを初期化
const slider = document.getElementById('time-slider');
const timeDisplay = document.getElementById('time-display');
// スライダーの値から取得した時間をUnixタイムスタンプで返す関数を定義
function getTimeFromSliderValue(value) {
const hours = Math.floor(value / 60);
const minutes = value % 60;
const date = new Date(); // 現在の年月日と時間を取得
date.setHours(hours, minutes, 0, 0); // 現在の日付に対して時間、分を設定
return date.getTime(); // Unixタイムスタンプを返す
}
// スライダーの値が変更されたときに実行するイベントリスナーを追加
slider.addEventListener('input', (e) => {
const value = parseInt(e.target.value, 10);
const timestamp = getTimeFromSliderValue(value);
// console.log(timestamp);
sunLight.timestamp = timestamp; // 取得した時間を太陽光のタイムスタンプとして設定
const date = new Date(timestamp);
const hours = date.getHours();
const minutes = date.getMinutes();
timeDisplay.innerHTML = `${hours}:${('' + minutes).padStart(
2,
'0',
)} (Timezone: UTC${date.getTimezoneOffset() < 0 ? '+' : ''}${-date.getTimezoneOffset() / 60
})`; // 取得した時間と分を表示
deckgl.redraw(true); // シーンを再描画
});
slider.dispatchEvent(new Event('input')); // スライダーのinputイベントを発火
</script>
</body>
</html>
deck.glとluma.glでPhotorealistic 3D Tilesを手描きのインクスケッチ風にしてみる
- Googleのサンプルコード、deck.glのPostProcessEffect及び下記のGitHubのソースコード(Chee Aunさんに感謝です)を参考に、Photorealistic 3D Tilesを手描きのインクスケッチ風にしてみます。
- deck.glのPostProcessEffectとluma.glのinkを組み合わせると3Dモデルを手描きのインクスケッチ風にできます。
コード
- APIキーは、ご自身で用意してください。
- luma.glのinkのstrengthは0~1の範囲の値になります。
<!DOCTYPE html>
<html>
<head>
<title>Google 3D tiles example</title>
<script src="https://unpkg.com/deck.gl@latest/dist.min.js"></script>
<script src="https://unpkg.com/@luma.gl/shadertools@8.5.20/dist/dist.min.js"></script>
<style>
body {
margin: 0;
padding: 0;
}
#map {
position: absolute;
top: 0;
bottom: 0;
width: 100%;
}
#credits {
position: absolute;
bottom: 0;
right: 0;
padding: 2px;
font-size: 15px;
color: white;
text-shadow: -1px 0 black, 0 1px black, 1px 0 black, 0 -1px black;
}
#control-panel {
position: absolute;
top: 0;
left: 0;
margin: 12px;
padding: 20px;
font-size: 12px;
line-height: 1.5;
z-index: 1;
background: #fff;
font-family: Helvetica, Arial, sans-serif;
box-shadow: 0 0 4px rgba(0, 0, 0, 0.15);
}
</style>
</head>
<body>
<div id="control-panel">
<p><b>inkEffect</b></p>
<div id="inkEffect-display"></div>
<input type="range" min="0" max="1" step="0.1" value="0.3" id="inkEffect-slider" />
</div>
<div id="map"></div>
<div id="credits"></div>
<script>
// 【参考】https://developers.google.com/maps/documentation/tile/use-renderer?hl=ja
const GOOGLE_API_KEY = 'YOUR_API_KEY';
const TILESET_URL = `https://tile.googleapis.com/v1/3dtiles/root.json`;
const creditsElement = document.getElementById('credits');
const inkEffect = new deck.PostProcessEffect(luma.ink, {
strength: 0.3
});
// deck.glのキャンバスを初期化
const deckgl = new deck.DeckGL({
container: 'map',
initialViewState: {
latitude: 35.6585805,
longitude: 139.7454329,
zoom: 16,
pitch: 60,
height: 200
},
controller: true,
effects: [inkEffect],
layers: [
new deck.Tile3DLayer({
id: 'google-3d-tiles',
data: TILESET_URL,
loadOptions: {
fetch: {
headers: {
'X-GOOG-API-KEY': GOOGLE_API_KEY
}
}
},
onTilesetLoad: tileset3d => {
tileset3d.options.onTraversalComplete = selectedTiles => {
const credits = new Set();
selectedTiles.forEach(tile => {
const { copyright } = tile.content.gltf.asset;
copyright.split(';').forEach(credits.add, credits);
creditsElement.innerHTML = [...credits].join('; ');
});
return selectedTiles;
}
}
})
]
});
// スライダーを初期化
const slider = document.getElementById('inkEffect-slider');
const inkEffectDisplay = document.getElementById('inkEffect-display');
// スライダーの値が変更されたときに実行するイベントリスナーを追加
slider.addEventListener('input', (e) => {
const value = parseFloat(e.target.value);
inkEffect.setProps({
strength: value// 効果の強度を変更
});
inkEffectDisplay.innerHTML = value
deckgl.redraw(true); // シーンを再描画
});
slider.dispatchEvent(new Event('input')); // スライダーのinputイベントを発火
</script>
</body>
</html>
deck.glのTripsLayerとPhotorealistic 3D Tilesを重ねて表示してみる
- deck.glのTripsLayerで表示するデータは、座標(緯度、経度)とタイムスタンプ(UnixTime)を持つJSON形式のデータになります。
- なお、使用するJSON形式のデータは、公共交通オープンデータセンターが公開している、東京都交通局バスロケーション情報(GTFS-RT)を加工したデータになります。
- JSON形式のデータは以下より取得できます(データの取得から加工までを行うPythonスクリプトもあわせて公開しています)。
コード
- APIキーは、ご自身で用意してください。
<!DOCTYPE html>
<html>
<head>
<title>deck.gl Photorealistic 3D Tiles example</title>
<meta charset="utf-8" />
<script src="https://unpkg.com/deck.gl@latest/dist.min.js"></script>
<script src="https://d3js.org/d3.v5.min.js"></script>
<style>
body {
margin: 0;
padding: 0;
}
#map {
position: absolute;
top: 0;
bottom: 0;
width: 100%;
}
#credits {
position: absolute;
bottom: 0;
right: 0;
padding: 2px;
font-size: 15px;
color: white;
text-shadow: -1px 0 black, 0 1px black, 1px 0 black, 0 -1px black;
}
#credits a {
color: white;
pointer-events: auto;
}
</style>
</head>
<body>
<div id="map"></div>
<div id="credits"></div>
<script>
// 3DタイルセットのURL
const GOOGLE_API_KEY = 'YOUR_API_KEY';
const TILESET_URL = `https://tile.googleapis.com/v1/3dtiles/root.json`;
// クレジットを表示するHTML要素
const creditsElement = document.getElementById('credits');
// DeckGLインスタンスを生成
const deckgl = new deck.DeckGL({
container: 'map',
initialViewState: {
latitude: 35.6812362,
longitude: 139.7671248,
zoom: 16,
bearing: 90,
pitch: 60,
height: 200
},
controller: { minZoom: 8 }
});
// GTFSリアルタイムデータ
const gtfsrt = d3.json('data/ToeiBus_VehiclePosition.json');
// アニメーションループの長さと速度、高さのオフセットを設定
const LOOP_LENGTH = 6000;
const ANIMATION_SPEED = 5;
const HEIGHT_OFFSET = 50;
// 初期化関数
async function initialize() {
// 時間を初期化
let time = 0;
// アニメーション関数
function animate() {
// 時間を進行させる
time = (time + ANIMATION_SPEED) % LOOP_LENGTH;
// 次のフレームを要求する
window.requestAnimationFrame(animate);
}
// 一定間隔(50ms)でレイヤーを更新する
setInterval(() => {
deckgl.setProps({
layers: [
// 3Dタイルレイヤー
new deck.Tile3DLayer({
id: 'google-3d-tiles',
data: TILESET_URL,
opacity: 1,
loadOptions: {
fetch: {
headers: {
'X-GOOG-API-KEY': GOOGLE_API_KEY
}
}
},
onTilesetLoad: tileset3d => {
tileset3d.options.onTraversalComplete = selectedTiles => {
const credits = new Set();
selectedTiles.forEach(tile => {
// タイルの著作権情報を取得し、クレジットに追加
const { copyright } = tile.content.gltf.asset;
copyright.split(';').forEach(credits.add, credits);
// クレジット情報を表示
creditsElement.innerHTML = [...credits].join('; ') +
`· <a href="https://ckan.odpt.org/dataset/b_bus_gtfs_rt-toei" target="_blank">東京都交通局・公共交通オープンデータ協議会</a>`;
});
return selectedTiles;
}
},
operation: 'terrain+draw'
}),
// トリップレイヤー
new deck.TripsLayer({
id: 'trips-layer',
data: gtfsrt,
getPath: d => d.path.map(coord => [coord[0], coord[1], HEIGHT_OFFSET]), // 経路を取得
getTimestamps: d => d.timestamps.map(p => p - 1684378982), // タイムスタンプを取得
getColor: [0, 255, 0],
getWidth: 2,
opacity: 1.0,
widthMinPixels: 6,
rounded: true,
fadeTrail: true, // フェードトレイルを有効
trailLength: 120, // トレイルの長さを設定
currentTime: time,
shadowEnabled: false,
material: {
polygonOffset: [-1, -1]
}
})
]
});
}, 50);
// アニメーションを開始
window.requestAnimationFrame(animate);
}
// 初期化関数を実行
initialize();
</script>
</body>
</html>
参考文献