Mapbox Newsletter WEEKLY TIPSの解説 -「3Dモデルを追加」
はじめに
この記事は、先日配信されたMapbox NewsletterのWEEKLY TIPSで紹介されていた「3Dモデルを追加」についての解説です。このサンプルではThree.jsを使ってCustomLayerInterface
を構成する方法を紹介しています。また、Newsletterの購読はこちらからお申し込みいただけます。
以下が本サンプルのデモです。
Three.jsとは
Three.jsはWebGLベースの3Dライブラリです。「カスタムスタイルレイヤーを追加」ではWebGLを直接使ってカスタムレイヤーを作成していましたが、一つの三角形を描くのにもかなりの量のコードが必要でした。Three.jsを使うことで、より便利にカスタムレイヤーを作成することができます。
コードを確認
まずExamplesのコードを見に行きましょう。
日本語サイト
英語サイト
基本的に同じコードですが、英語版はスタイルがMapbox Light v11にアップグレードされているのでこちらを使用します。Mapbox Light v10ではデフォルトのプロジェクションがWebメルカトルであるのに対し、Mapbox Light v11ではGlobe(3D表示された地球)なので、印象がかなり異なります。また、英語版はMapbox GL JS v3が使用されています。
HTML
まずHTMLを見ていきましょう。
以下ではThree.jsを読み込んでいます。途中、glTF形式の3Dモデルを読み込むため、addonのGLTFLoader.js
も読み込んでいます。
<script src="https://unpkg.com/three@0.126.0/build/three.min.js"></script>
<script src="https://unpkg.com/three@0.126.0/examples/js/loaders/GLTFLoader.js"></script>
以下は地図を表示するエレメントです。
<div id="map"></div>
Mapの作成
JavaScriptのコードを見ていきます。以下のコードはいつも通り、Mapオブジェクトを作成しています。container
で地図を表示するHTMLエレメントのidを指定します。
const map = new mapboxgl.Map({
container: 'map',
// Choose from Mapbox's core styles, or make your own style with Mapbox Studio
style: 'mapbox://styles/mapbox/light-v11',
zoom: 18,
center: [148.9819, -35.3981],
pitch: 60,
antialias: true // create the gl context with MSAA antialiasing, so custom layers are antialiased
});
3Dモデルのための設定
後で使用するためのパラメータを先に作成しています。
3Dモデルを配置する座標、高度を指定しています。
// parameters to ensure the model is georeferenced correctly on the map
const modelOrigin = [148.9819, -35.39847];
const modelAltitude = 0;
WebGLではPCのスクリーン横方向がX軸、縦方向がY軸、画面手前方向がZ軸です。今回使用する3DモデルはY軸が上になります。しかし、地図は真上から見る物なので、上はZ軸方向になります。そこでX軸で90度回転させます。
const modelRotate = [Math.PI / 2, 0, 0];
座標と標高から、WebGL上での座標を計算します。
const modelAsMercatorCoordinate = mapboxgl.MercatorCoordinate.fromLngLat(
modelOrigin,
modelAltitude
);
以上の値を後で使いやすい形にまとめます。
// transformation parameters to position, rotate and scale the 3D model onto the map
const modelTransform = {
translateX: modelAsMercatorCoordinate.x,
translateY: modelAsMercatorCoordinate.y,
translateZ: modelAsMercatorCoordinate.z,
rotateX: modelRotate[0],
rotateY: modelRotate[1],
rotateZ: modelRotate[2],
/* Since the 3D model is in real world meters, a scale transform needs to be
* applied since the CustomLayerInterface expects units in MercatorCoordinates.
*/
scale: modelAsMercatorCoordinate.meterInMercatorCoordinateUnits()
};
MercatorCoordinate#meterInMercatorCoordinateUnits()
は、3Dモデルが指定している現実の長さ(メートル単位)を地図上での縮尺に応じた長さに変換する値を計算します。
Three.jsの取得
Three.jsを取得しています。
const THREE = window.THREE;
レイヤーの作成
途中を飛ばして最後の部分を確認します。customLayer
はカスタムレイヤーが定義されている変数です。Symbolレイヤー等と同じ様にMap#addLayerメソッドで作成します。第2引数にwaterway-label
レイヤーを指定しているので、自作レイヤーはこのレイヤーの下に描画されます。
map.on('style.load', () => {
map.addLayer(customLayer, 'waterway-label');
});
カスタムレイヤーの定義
それではcustomLayer
の中身を見ていきましょう。
id
とtype
を指定するのは他のレイヤーと同じです。CustomLayerInterface
のときはcustom
を指定します。また、3Dモデルを使用するのでrenderingMode
に3d
を指定します。
id: 'customLayer',
type: 'custom',
renderingMode: '3d',
renderingMode
は深度バッファの制御に使用されます。下図のように、3d
に設定すると深度バッファを用いて3Dオブジェクトの重なりが正しく描画されますが、2d
(デフォルト)の場合は正しく描画されません。
3d |
2d |
---|---|
onAdd
以下のonAdd
の中身を見ていきます。
onAdd: function (map, gl) {
...
},
まず、Three.jsのオブジェクトであるカメラとシーンを作成しています。
this.camera = new THREE.Camera();
this.scene = new THREE.Scene();
指向性ライトを2つ作成しています。斜め上下から光を当てています。
// create two three.js lights to illuminate the model
const directionalLight = new THREE.DirectionalLight(0xffffff);
directionalLight.position.set(0, -70, 100).normalize();
this.scene.add(directionalLight);
const directionalLight2 = new THREE.DirectionalLight(0xffffff);
directionalLight2.position.set(0, 70, 100).normalize();
this.scene.add(directionalLight2);
glTFの3Dモデルを読み込んでいます。
const loader = new THREE.GLTFLoader();
loader.load(
'https://docs.mapbox.com/mapbox-gl-js/assets/34M_17/34M_17.gltf',
(gltf) => {
this.scene.add(gltf.scene);
}
);
最後にThree.jsのレンダラーを作成しています。autoClear
をfalse
にしておかないと、地図が正しく描画されません。
this.renderer = new THREE.WebGLRenderer({
canvas: map.getCanvas(),
context: gl,
antialias: true
});
this.renderer.autoClear = false;
render
以下のrender
の中身を見ていきます。
render: function (gl, matrix) {
...
}
modelRotate
で定義した値を使って、各軸における回転行列を作成します。
const rotationX = new THREE.Matrix4().makeRotationAxis(
new THREE.Vector3(1, 0, 0),
modelTransform.rotateX
);
const rotationY = new THREE.Matrix4().makeRotationAxis(
new THREE.Vector3(0, 1, 0),
modelTransform.rotateY
);
const rotationZ = new THREE.Matrix4().makeRotationAxis(
new THREE.Vector3(0, 0, 1),
modelTransform.rotateZ
);
Mapbox GL JSのカメラ行列(m
)と、座標変換行列(l
)から、Three.jsのカメラ行列を計算します。
const m = new THREE.Matrix4().fromArray(matrix);
const l = new THREE.Matrix4()
.makeTranslation(
modelTransform.translateX,
modelTransform.translateY,
modelTransform.translateZ
)
.scale(
new THREE.Vector3(
modelTransform.scale,
-modelTransform.scale,
modelTransform.scale
)
)
.multiply(rotationX)
.multiply(rotationY)
.multiply(rotationZ);
this.camera.projectionMatrix = m.multiply(l);
最後に描画処理を行います。複数のライブラリでWebGLを共有しているときはresetState
を呼ぶ必要があるようです。また、triggerRepaint
を実行しないと、glTFロード後に3Dモデルが描画されません。
this.renderer.resetState();
this.renderer.render(this.scene, this.camera);
this.map.triggerRepaint();
まとめ
Three.jsを使うことで、複雑な3Dモデルも表示できることがわかりました。
Discussion