LoginSignup
2
1

More than 1 year has passed since last update.

mapboxで描画した3Dマップ上にビジュアライゼーションを投影する

Posted at

はじめに

Reactで作ったWebアプリに描画されたmapboxの地図上でビジュアライゼーションの表示を行います。
ビジュアライゼーションの描画にはdeck.glを利用します。
同時に3Dモデルも地図上に表示します。

deck.glについて

mapboxでのdeck.glの使い方は何パターンかあるのですが、今回はmapbox-glをベースにdeck.glのレイヤーを乗せて表示する形を試します。
このパターンだと@deck.gl/mapboxというサブモジュールを使うパターンになるのですが、ドキュメントには

Mapbox 2.0's terrain feature is currently not supported.

と注意書きがあり、3D地形表示には対応していないようです。
ですが今回は3D地形表示状態でもdeck.glでのレイヤー表示ができるかも含めて試しました。

本来の3D地形表示をサポートするdeck.glの利用パターンは、deck.glをベースに表示対象のマップを mapbox にするという使い方になります。
(つまりmapbox-glベースではなく、deck.glベースで実装する)
TerrainLayer
このパターンだとmapbox-glで3Dモデルの表示に使っているThree.jsは、使うことができなくなります。
その場合は代わりにluma.glを使うようです。

最終的な完成形

  • 地形を有効にした状態
    完成形

    3D 地形表示を有効にした状態でもビジュアライゼーションは表示できましたが、高さ情報を意識できていないように見えました。

  • 平面の状態
    完成形2

環境構築

こちらの記事の初期セットアップをベースにします
【React/mapboxの地図上で3D モデルを動かす】(2) mapboxの地図上に3Dモデルを表示する

package install

yarn add three @mapbox/mapbox-gl-language mapbox-gl @deck.gl/mapbox deck.gl
yarn add -D @types/three @types/mapbox-gl

実装

  • deck.glのライブラリのMapboxLayerを使ってmapbox-glにレイヤーとしてビジュアライゼーションを追加しています。
  • typescriptの場合はtypedディレクトリの中をimportします。(Using deck.gl with TypeScript)
  • 今回使用したビジュアライゼーションはArcLayerIconLayerです。
App.tsx
import { useEffect, useRef, useState } from "react";
import mapboxgl from "mapbox-gl";
import "mapbox-gl/dist/mapbox-gl.css";
import "./App.css";
import { droneLayer } from "./droneLayer";
import { MapboxLayer } from "@deck.gl/mapbox/typed";
import { ArcLayer, IconLayer } from "@deck.gl/layers/typed";

mapboxgl.accessToken = "YOUR MAPBOX TOKEN";

const App = () => {
  const mapContainer = useRef<HTMLDivElement>(null);
  const map = useRef<mapboxgl.Map | null>(null);
  const [lng, setLng] = useState(6.5873);
  const [lat, setLat] = useState(45.3967);
  const [zoom, setZoom] = useState(13.85);
  const exaggeration = 1.0;
  let dronePosition: mapboxgl.LngLatLike = [6.56674, 45.39881];
  let droneHeight = 100;

  // この中でアニメーションを描画していく
  const animation = (frame: number) => {
    if (!map.current) return;

    // ドローンの現在位置の標高を取得
    const elevation =
      Math.floor(
        map.current.queryTerrainElevation(dronePosition, {
          exaggerated: false,
        }) || 0
      ) * exaggeration;

    if (map.current.getLayer("drone-model")) {
      let height =
        elevation + droneHeight + Math.sin(droneHeight + frame * 0.01) * 0.5;
      // 上下の揺れだけ表現
      droneLayer.updateLngLat({ altitude: height });
    }
    requestAnimationFrame(animation);
  };

  useEffect(() => {
    // マップの初期セットアップ
    if (map.current) return;
    map.current = new mapboxgl.Map({
      container: mapContainer.current as HTMLElement,
      style: "mapbox://styles/mapbox/streets-v11",
      center: [lng, lat],
      zoom: zoom,
      pitch: 76,
      bearing: 150,
      antialias: true,
    });
    animation(0);
  }, []);

  useEffect(() => {
    // マップ読み込み後
    if (!map.current) return;
    map.current.on("move", () => {
      if (map.current) {
        setLng(Number(map.current.getCenter().lng.toFixed(4)));
        setLat(Number(map.current.getCenter().lat.toFixed(4)));
        setZoom(Number(map.current.getZoom().toFixed(2)));
      }
    });

    map.current.on("load", () => {
      // モデルを描画する
      if (map.current) {
        if (!map.current.getLayer("drone-model")) {
          map.current.addLayer(droneLayer);
        }

        if (!map.current.getSource("mapbox-dem")) {
          // 標高データの追加
          map.current.addSource("mapbox-dem", {
            type: "raster-dem",
            url: "mapbox://mapbox.terrain-rgb",
            tileSize: 512,
            maxzoom: 14,
          });
          map.current.setTerrain({ source: "mapbox-dem", exaggeration });
        }
        if (!map.current.getLayer("my-archlayer")) {
          const myArcLayer = new MapboxLayer<ArcLayer>({
            id: "my-archlayer",
            // @ts-ignore
            type: ArcLayer,
            data: [
              {
                position: [6.56674, 45.39881],
                size: 5,
              },
              {
                position: [6.6025, 45.40753],
                size: 5,
              },
            ],
            getSourcePosition: (d) => [6.56674, 45.39881],
            getTargetPosition: (d) => [6.6025, 45.40753],
            getSourceColor: [0, 128, 200],
            getTargetColor: [200, 0, 80],
            getWidth: 1,
          });
          map.current.addLayer(myArcLayer);
        }
        if (!map.current.getLayer("my-iconlayer")) {
          const ICON_MAPPING = {
            marker: { x: 0, y: 0, width: 128, height: 128, mask: true },
          };
          const myIconLayer = new MapboxLayer<IconLayer>({
            id: "my-iconlayer",
            // @ts-ignore
            type: IconLayer,
            data: [
              {
                name: "ふもと",
                exits: 4214,
                coordinates: [6.56674, 45.39881],
              },
              {
                name: "頂上",
                exits: 3002,
                coordinates: [6.6025, 45.40753],
              },
            ],
            pickable: false,
            iconAtlas:
              "https://raw.githubusercontent.com/visgl/deck.gl-data/master/website/icon-atlas.png",
            iconMapping: ICON_MAPPING,
            getIcon: (d) => "marker",
            sizeScale: 10,
            getPosition: (d) => d.coordinates,
            getSize: (d) => 5,
            getColor: (d) => [Math.sqrt(d.exits), 140, 0],
          });
          map.current.addLayer(myIconLayer);
        }
      }
    });
  });

  return (
    <div>
      <div style={{ position: "relative" }}>
        <div className="sidebar">
          Longitude: {lng} | Latitude: {lat} | Zoom: {zoom}
        </div>
        <div ref={mapContainer} className="map-container" />
      </div>
    </div>
  );
};
export default App;

まとめ

mapboxの3D地図上でビジュアライゼーションを表示することができました。

参考

deck.gl
【React/mapboxの地図上で3D モデルを動かす】(2) mapboxの地図上に3Dモデルを表示する

2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1