🕒

Mapbox Newsletter WEEKLY TIPSの解説 -「3Dモデルを追加」

2024/03/19に公開

はじめに

この記事は、先日配信された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の中身を見ていきましょう。

idtypeを指定するのは他のレイヤーと同じです。CustomLayerInterfaceのときはcustomを指定します。また、3Dモデルを使用するのでrenderingMode3dを指定します。

id: 'customLayer',
type: 'custom',
renderingMode: '3d',

renderingModeは深度バッファの制御に使用されます。下図のように、3dに設定すると深度バッファを用いて3Dオブジェクトの重なりが正しく描画されますが、2d(デフォルト)の場合は正しく描画されません。

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のレンダラーを作成しています。autoClearfalseにしておかないと、地図が正しく描画されません。

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モデルも表示できることがわかりました。

GitHubで編集を提案
マップボックス・ジャパン合同会社

Discussion