three-geoをコードリーディングする

動機
- three.jsを知っておきたい
- DEMで遊びたい
目標
- 参考とするライブラリとしてthree-geoを読む

three-geoとは
- Three.jsをベースとした地図表示ライブラリ
- Mapbox Maps APIからDEMを取得し、メッシュを作成
exampleを実行した例
examples/simple-viewer
実行にはAPI Keyが必要

examples/simple-viewer
をざっくり眺める
(async () => {
THREE.Object3D.DefaultUp = new THREE.Vector3(0, 0, 1);
const canvas = document.getElementById("canvas");
const camera = new THREE.PerspectiveCamera(75, canvas.width/canvas.height, 0.1, 1000);
camera.position.set(0, 0, 1.5);
const renderer = new THREE.WebGLRenderer({ canvas });
const controls = new THREE.OrbitControls(camera, renderer.domElement);
const scene = new THREE.Scene();
const walls = new THREE.LineSegments(
new THREE.EdgesGeometry(new THREE.BoxBufferGeometry(1, 1, 1)),
new THREE.LineBasicMaterial({color: 0xcccccc}));
walls.position.set(0, 0, 0);
scene.add(walls);
scene.add(new THREE.AxesHelper(1));
const stats = new Stats();
stats.showPanel(1); // 0: fps, 1: ms, 2: mb, 3+: custom
document.body.appendChild(stats.dom);
const render = () => {
stats.update();
renderer.render(scene, camera);
};
controls.addEventListener('change', render);
render(); // first time
const ioToken = 'pk.eyJ1IjoiamRldmVsIiwiYSI6ImNqemFwaGJoZjAyc3MzbXA1OGNuODBxa2EifQ.7M__SgfWZGJuEiSqbBXdoQ';
const tgeo = new ThreeGeo({
tokenMapbox: 'pk.秘密', // <---- set your Mapbox API token here
});
if (!window.location.origin.startsWith('https://w3reality.github.io') && tgeo.tokenMapbox === ioToken) {
const warning = 'Please set your Mapbox API token in ThreeGeo constructor.';
alert(warning);
throw warning;
}
const terrain = await tgeo.getTerrainRgb(
[46.5763, 7.9904], // [lat, lng]
5.0, // radius of bounding circle (km)
12); // zoom resolution
scene.add(terrain);
render();
})();

tgeo.getTerrainRgb
はTHREE.Group
クラスを返す
このためscene.add(terrain);
で直接追加できる

THREE.Group
は物体を入れ子構造にできる
上記事で紹介されているTHREE.Object3d
と同じだが、Group
のほうが推奨されているらしい

tgeo
はThreeGeo
オブジェクトのインスタンス
src/index.js
では、定義とデフォルト値が以下のように設定される
const defaults = {
unitsSide: 1.0,
tokenMapbox: '',
isNode: false,
isDebug: false,
apiVector: 'mapbox-terrain-vector',
apiRgb: 'mapbox-terrain-rgb',
apiSatellite: 'mapbox-satellite',
};
exampleではtokenMapboxのみを上書き

getTerrainRgb
async getTerrainRgb(origin, radius, zoom, _cb=undefined) {
const { rgbDem: objs, debug } = await this.getTerrain(origin, radius, zoom, {
// Set dummy callbacks to trigger rgb DEM fetching
onRgbDem: () => {},
onSatelliteMat: () => {},
});
return _cb ? _cb(objs) : ThreeGeo._createDemGroup('dem-rgb', objs, debug);
}
予想できる流れは
-
getTerrain
でMapboxから衛星データを取得 -
_createDemGroup
でTHREE.Group
のインスタンスとして作成?

getTerrain
はPromiseを返す
getTerrain(origin, radius, zoom, cbs={}) {
return new Promise((res, rej) => {
...
try{
...
if (onRgbDem) {
(new RgbModel({
unitsPerMeter, projectCoord,
token, isNode, isDebug, apiRgb, apiSatellite,
onRgbDem, onSatelliteMat, watcher,
})).fetch(zpCovered, bbox);
}
if (onVectorDem) {
(new VectorModel({
unitsPerMeter, projectCoord,
token, isNode, isDebug, apiVector,
onVectorDem, watcher,
})).fetch(zpCovered, bbox, radius);
}
} catch (err) {
...
}
}
引数に与えられた各種パラメータを整理し、RgbModel
あるいはVectorModel
に整形
それぞれのクラスのメソッドfetch
で衛星データをMapboxのAPIから取得し返すものと思われる

getTerrainRgb
を見返すと、getTerrain
はobjs
を返している
_createDemGroup
はそれを単にまとめる静的メソッドだった
static _createDemGroup(name, objs, debug) {
const group = new THREE.Group();
group.name = name;
group.userData['debug'] = () => {
if (!debug) console.warn('Use the `isDebug` option to enable `.userData.debug()`.');
return debug;
};
for (let obj of objs) { group.add(obj); }
return group;
}

今はRgbModel
に着目する

RgbModel
fetch(zpCovered, bbox) {
// e.g. satellite's zoom: 14
// dem's zoom: 12 (=14-2)
const zpEle = Fetch.getZoomposEle(zpCovered);
console.log('RgbModel: zpEle:', zpEle);
let count = 0;
zpEle.forEach(async zoompos => {
const tile = await Fetch.fetchTile(zoompos, this.apiRgb, this.token, this.isNode);
if (tile !== null) {
this.addTile(tile, zoompos, zpCovered, bbox);
} else {
console.log(`fetchTile() failed for rgb dem of zp: ${zoompos} (count: ${count}/${zpEle.length})`);
}
count++;
if (count === zpEle.length) {
this.build();
}
});
}
指定されたboundingboxをカバーする最小限のzoompos
(zpCovered) の構成から、それぞれに対応する各tileを取得し、データを取得
zpCoveredはこのライブラリを使って作られていた
getZoomposEle
とは何だろう?
// e.g. satellite's zoom: 14
// dem's zoom: 12 (=14-2)
この文言も気になる
DEMを持ってくる段階でzoomの値を調整する必要がある?

tileを取得するときは、fetchTile
で生のデータを取得、addTile
で整形して追加

一旦終了。現在の関心は
- addTileは何をするのか
- THREE.Meshがどこで作成されているのか
Fetch.getZoomposEle
(fetch.js
) が何をする関数なのかについては、気が向いたとき有識者に伺ってみたい

addTiles()
はタイルの生データから標高などの具体値を取得しthis.dataEleCovered
に格納しているようだが、独特かつ説明なしのコンテキストが多く解釈しづらい
build()
からみてみる

build() {
...
const meshes = RgbModel._build(
this.dataEleCovered, this.apiSatellite,
this.token, this.isNode, onSatelliteMatWrapper);
this.onRgbDem(meshes); // legacy API
...
}
_build
の流れとしては、addTile()
で作成したデータからMeshを作成し、外から与えられたコールバックonRgbDem
に渡す

_build()
static _build(dataEle, apiSatellite, token, isNode, onSatelliteMatWrapper) {
...
const objs = [];
dataEle.forEach(([zoompos, arr, zoomposEle]) => {
...
let geom = new THREE.PlaneBufferGeometry(
1, 1, cSegments[0], cSegments[1]);
geom.attributes.position.array = new Float32Array(arr);
// test identifying a 127x1 "belt"
// let geom = new THREE.PlaneBufferGeometry(1, 1, 127, 1);
// let arrBelt = arr;
// arrBelt.length = 128*2*3;
// geom.attributes.position.array = new Float32Array(arrBelt);
let plane = new THREE.Mesh(geom,
new THREE.MeshBasicMaterial({
wireframe: true,
color: 0xcccccc,
}));
plane.name = `dem-rgb-${zoompos.join('/')}`;
const _toTile = zp => [zp[1], zp[2], zp[0]];
plane.userData.threeGeo = {
tile: _toTile(zoompos),
srcDem: {
tile: _toTile(zoomposEle),
uri: Fetch.getUriMapbox(token, 'mapbox-terrain-rgb', zoomposEle),
},
};
objs.push(plane);
this.resolveTex(zoompos, apiSatellite, token, isNode, tex => {
//console.log(`resolve tex done for ${zoompos}`);
if (tex) {
plane.material = new THREE.MeshBasicMaterial({
side: THREE.FrontSide,
// side: THREE.DoubleSide,
map: tex,
});
}
if (onSatelliteMatWrapper) {
onSatelliteMatWrapper(plane, objs);
}
});
});
return objs;
}

THREE.PlaneBufferGeometry
を作成
cSegmentは一つのタイルの幅 (別タイルとのつなぎ目も考慮されているように見える)
THREE.PlaneBufferGeometry
から仮のテスクチャとともにThree.Mesh
を作成
objs
に格納し返り値にする

resolveTex
はテスクチャを作成し、各plane
のデフォルトのテクスチャを上書きしている
内部ではfetchTile
がまた呼び出されているため、航空画像を取得すると思われる
航空画像データを表すtex
がTHREE.DataTexture
形式で与えられ、マテリアルTHREE.MeshBasicMaterial
に用いる画像として渡される
THREE.MeshBasicMaterial
については以下を参考

核心が見えたので、締める

まとめ
- 地形を
THREE.PlaneBufferGeometry
をジオメトリとしてTHREE.Mesh
で表現 -
THREE.MeshBasicMaterial
でマテリアルを定義 - テクスチャは
THREE.DataTexture
から作成
これだけを知るにはメモしたことが多すぎた