WebGL Overlay View で構築する 3D マップ エクスペリエンス

1. 始める前に

この Codelab では、WebGL を使った Maps JavaScript API の機能による、3 次元的なベクターマップの制御とレンダリングについて解説します。

最終的な 3D ピンの表示

前提条件

この Codelab は、JavaScript と Maps JavaScript API に関する中級レベルの知識を前提とした内容です。Maps JS API の使い方の基礎を学ぶ場合は、Codelab「ウェブサイトに地図を追加する(JavaScript)」がおすすめです。

学習する内容

  • JavaScript 用ベクター地図が有効化されたマップ ID の生成
  • プログラマティックな傾斜と回転を使った地図のコントロール
  • WebGLOverlayViewThree.js による地図上の 3D オブジェクトのレンダリング
  • moveCamera によるカメラ移動のアニメーション化

必要なもの

  • 課金が有効になっている Google Cloud Platform アカウント
  • Maps JavaScript API を有効にした Google Maps Platform API キー
  • JavaScript、HTML、CSS に関する中級レベルの知識
  • 任意のテキスト エディタまたは IDE
  • Node.js

2. 準備

以下の有効化の手順では、Maps JavaScript API を有効にする必要があります。

Google Maps Platform をセットアップする

課金を有効にした Google Cloud Platform アカウントとプロジェクトをまだ作成していない場合は、Google Maps Platform スタートガイドに沿って請求先アカウントとプロジェクトを作成してください。

  1. Cloud Console で、プロジェクトのプルダウン メニューをクリックし、この Codelab に使用するプロジェクトを選択します。

  1. Google Cloud Marketplace で、この Codelab に必要な Google Maps Platform API と SDK を有効にします。詳しい手順については、こちらの動画またはドキュメントをご覧ください。
  2. Cloud Console の [認証情報] ページで API キーを生成します。詳しい手順については、こちらの動画またはドキュメントをご覧ください。Google Maps Platform へのすべてのリクエストで API キーが必要になります。

Node.js のセットアップ

まだお持ちでない場合は、https://nodejs.org/ から Node.js ランタイムをパソコンにダウンロードしてインストールします。

この Codelab で使用する依存関係をインストールするには、Node.js に付属している NPM パッケージ マネージャーが必要です。

プロジェクトのスターター テンプレートをダウンロードする

Codelab の作業を始める前に、スターター プロジェクト テンプレートとソリューション コード全体をダウンロードしましょう。手順は次のとおりです。

  1. https://github.com/googlecodelabs/maps-platform-101-webgl/ で、この Codelab 用の GitHub リポジトリをダウンロードまたはフォークします。スターター プロジェクトは /starter ディレクトリに配置されており、この Codelab の実行に必要な基本的なファイル構造が含まれています。作業対象のすべてのコンポーネントは /starter/src ディレクトリにあります。
  2. スターター プロジェクトをダウンロードしたら、/starter ディレクトリで npm install を実行します。package.json に記載されている必要な依存関係がすべてインストールされます。
  3. 依存関係をインストールしたら、同じディレクトリで npm start を実行します。

このスターター プロジェクトは webpack-dev-server を使用するように設定されています。webpack-dev-server により、ローカルに記述したコードがコンパイルされ、実行されます。また、コードを変更するたびに、ブラウザ上でアプリが自動的に再読み込みされます。

ソリューション コード全体を実行する場合は、上記のセットアップ手順を /solution ディレクトリで実施します。

API キーを追加する

スターター アプリには、JS API Loader で地図を読み込むために必要なコードがすべて含まれており、あとは API キーとマップ ID を組み込むだけで使用できます。Maps JS API は HTML テンプレート内で script タグを使ってインラインで読み込むのが従来のやり方でしたが、JS API Loader ではこの処理がシンプルなライブラリとして抽象化されており、JavaScript コード内ですべてを完結させることができます。

API キーを追加するには、スターター プロジェクトで以下を行います。

  1. app.js を開きます。
  2. apiOptions オブジェクトで、API キーを apiOptions.apiKey の値に設定します。

3. マップ ID を生成・使用する

Maps JavaScript API の WebGL ベースの機能を利用するには、ベクター地図が有効化されたマップ ID が必要です。

マップ ID の生成

マップ ID の生成

  1. Google Cloud Console で、[Google Maps Platform] > [マップ管理] を開きます。
  2. [CREATE MAP ID] をクリックします。
  3. マップ ID に付ける名前を [名前] 欄に入力します。
  4. [地図の種類] プルダウンから「JavaScript」を選択します。JavaScript 用の設定が表示されます。
  5. ラジオボタン [ベクター] を選択し、[チルト] と [ローテーション] のチェックボックスをオンにします。
  6. (省略可)[説明] 欄に API キーの説明を入力します。
  7. [保存] ボタンをクリックします。[マップ ID の詳細] ページが表示されます。

    [マップ ID の詳細] ページ
  8. マップ ID をコピーします。この ID は、次のステップで地図を読み込む際に使用します。

マップ ID の使用

ベクター地図を読み込むには、地図をインスタンス化する際のオプションで、プロパティとしてマップ ID を指定する必要があります。Maps JavaScript API を読み込む際に同じマップ ID を指定することも可能です。

マップ ID を指定して地図を読み込む手順は次のとおりです。

  1. マップ ID を mapOptions.mapId の値として設定します。

    地図をインスタンス化する際にマップ ID を指定することにより、そのインスタンスに対してどの地図を読み込むべきか、Google Maps Platform に伝えることができます。同じマップ ID を、複数のアプリや同じアプリ内の複数のビューで再利用しても問題ありません。
    const mapOptions = {
      "tilt": 0,
      "heading": 0,
      "zoom": 18,
      "center": { lat: 35.6594945, lng: 139.6999859 },
      "mapId": "YOUR_MAP_ID"
    };
    

ブラウザ内で実行されているアプリを確認しましょう。傾斜と回転を有効化したベクター地図が読み込まれるはずです。傾斜と回転が有効になっていることを確認するには、Shift キーを押したまま、マウスでドラッグするか、キーボードの矢印キーを押します。

地図が読み込まれない場合は、apiOptions で指定した API キーに間違いがないか確認しましょう。地図の傾斜や回転が機能しない場合は、apiOptionsmapOptions で指定したマップ ID で傾斜(チルト)と回転(ローテーション)が有効になっているか確認します。

傾斜させた地図

app.js ファイルは以下のようになります。

    import { Loader } from '@googlemaps/js-api-loader';

    const apiOptions = {
      "apiKey": 'YOUR_API_KEY',
      "version": "beta"
    };

    const mapOptions = {
      "tilt": 0,
      "heading": 0,
      "zoom": 18,
      "center": { lat: 35.6594945, lng: 139.6999859 },
      "mapId": "YOUR_MAP_ID"
    }

    async function initMap() {
      const mapDiv = document.getElementById("map");
      const apiLoader = new Loader(apiOptions);
      await apiLoader.load();
      return new google.maps.Map(mapDiv, mapOptions);
    }

    function initWebGLOverlayView (map) {
      let scene, renderer, camera, loader;
      // WebGLOverlayView code goes here
    }

    (async () => {
      const map = await initMap();
    })();

4. WebGLOverlayView を実装する

WebGLOverlayView を使用すると、ベクター基本地図のレンダリングに使用されているものと同じ WebGL レンダリング コンテキストに直接アクセスできます。つまり、WebGL や人気のある WebGL ベースのグラフィック ライブラリを使って、2D や 3D のオブジェクトを地図上で直接レンダリングできるということです。

WebGLOverlayView では、WebGL レンダリング コンテキストのライフサイクルにアクセスできるフックが 5 種類提供されています。以下、各フックの簡単な説明と用途を示します。

  • onAdd(): WebGLOverlayView インスタンスで setMap を呼び出すことにより地図にオーバーレイが追加された際に呼び出されます。WebGL コンテキストへの直接アクセスを必要としない処理はここで扱いましょう。
  • onContextRestored(): WebGL コンテキストが利用可能になり、かつまだ何もレンダリングされていない段階で呼び出されます。オブジェクトの初期化、ステートのバインディング、その他 WebGL コンテキストへのアクセスは必要とするものの onDraw() 呼び出しの外で実行可能な処理は、ここで扱いましょう。これにより、もともと GPU の負荷が大きい地図自体のレンダリングに余分な負荷を上乗せすることなく、必要なセットアップを済ませることができます。
  • onDraw(): WebGL が地図およびその他のリクエスト内容のレンダリングを開始後、1 フレームに 1 回呼び出されます。地図のレンダリングでパフォーマンスの問題が生じないよう、onDraw() 内で行う処理は最小限に抑えましょう。
  • onContextLost(): なんらかの理由でレンダリング コンテキストが失われたときに呼び出されます。
  • onRemove(): WebGLOverlayView インスタンスで setMap(null) を呼び出すことによりオーバーレイが地図から削除されたときに呼び出されます。

このステップでは、WebGLOverlayView のインスタンスを作成し、そのライフサイクル フックのうち 3 種類(onAddonContextRestoredonDraw)を実装します。説明をわかりやすくするため、オーバーレイのコードはすべて、本 Codelab のスターター テンプレートに用意されている initWebGLOverlayView() 関数内で扱います。

  1. WebGLOverlayView() インスタンスを作成します。

    オーバーレイは Maps JS API により google.maps.WebGLOverlayView に用意されています。まず、次のコードを initWebGLOverlayView() に追記してインスタンスを作成します。
    const webGLOverlayView = new google.maps.WebGLOverlayView();
    
  2. ライフサイクル フックを実装します。

    ライフサイクル フックを実装するには、次のコードを initWebGLOverlayView() に追記します。
    webGLOverlayView.onAdd = () => {};
    webGLOverlayView.onContextRestored = ({gl}) => {};
    webGLOverlayView.onDraw = ({gl, coordinateTransformer}) => {};
    
  3. オーバーレイ インスタンスを地図に追加します。

    次に、オーバーレイ インスタンスで setMap() を呼び出して地図を受け渡すため、次のコードを initWebGLOverlayView() に追記します。
    webGLOverlayView.setMap(map)
    
  4. initWebGLOverlayView を呼び出します。

    最後に、initWebGLOverlayView() を実行するため、app.js の末尾にある即時実行関数に次のコードを追加します。
    initWebGLOverlayView(map);
    

initWebGLOverlayView と即時実行関数は次のようになります。

    async function initWebGLOverlayView (map) {
      let scene, renderer, camera, loader;
      const webGLOverlayView = new google.maps.WebGLOverlayView();

      webGLOverlayView.onAdd = () => {}
      webGLOverlayView.onContextRestored = ({gl}) => {}
      webGLOverlayView.onDraw = ({gl, coordinateTransformer}) => {}
      webGLOverlayView.setMap(map);
    }

    (async () => {
      const map = await initMap();
      initWebGLOverlayView(map);
    })();

WebGLOverlayView を実装する手順はこれで完了です。次は、Three.js を使って地図上で 3D オブジェクトをレンダリングするために必要なセットアップを行います。

5. three.js のシーンをセットアップする

WebGL を使用する際は、あらゆるオブジェクトのあらゆる面を手動で定義することをはじめ、さまざまな要件があり、複雑な作業になりがちです。こういった手間を避けるため、本 Codelab では Three.js を使用します。Three.js は人気のあるグラフィック ライブラリで、WebGL に抽象化レイヤを追加するものです。Three.js には多数の便利な機能が付属しており、たとえば WebGL レンダラの作成、一般的な 2D / 3D オブジェクトの図形描画、カメラの制御、オブジェクトの変形など、さまざまなことができます。

Three.js には 3 つの基本的なオブジェクト タイプがあり、何を表示する場合にもこれらが必要になります。

  • シーン: 「容器」のようなもので、オブジェクト、光源、テクスチャ等のレンダリングと表示はすべてこの中で行います。
  • カメラ: シーンの視点を表すカメラです。カメラには複数の種類があり、同じシーンに複数のカメラを追加することも可能です。
  • レンダラ: シーン内のすべてのオブジェクトの処理と表示を担当するレンダラです。Three.js では WebGLRenderer を使用するのが最も一般的ですが、クライアントが WebGL に未対応の場合にフォールバックとして使用できるレンダラが他にもいくつか提供されています。

このステップでは、Three.js に必要な依存関係をすべて読み込み、基本的なシーンのセットアップを行います。

  1. three.js を読み込みます。

    この Codelab では依存関係として、Three.js ライブラリと GLTFLoader の 2 つが必要となります。GLTFLoader は、glTF(GL Transmission Format)形式の 3D オブジェクトの読み込みを可能にするクラスです。Three.js はさまざまな 3D オブジェクト形式に特化したローダを提供していますが、glTF の使用が推奨されます。

    下のコードでは、Three.js ライブラリ全体がインポートされています。実際のアプリでは必要なクラスだけをインポートすることが多いのですが、この Codelab では手順をシンプルにするためライブラリ全体をインポートしています。また、GLTFLoader はデフォルトのライブラリに含まれておらず、依存関係内の別のパスからインポートする必要がある点にも注意しましょう。Three.js が提供するローダは、すべてこのパスからアクセスできます。

    Three.js と GLTFLoader をインポートするには、次のコードを app.js の先頭に追加します。
    import * as THREE from 'three';
    import {GLTFLoader} from 'three/examples/jsm/loaders/GLTFLoader.js';
    
  2. three.js のシーンを作成します。

    シーンを作成するには、Three.js の Scene クラスをインスタンス化するため、次のコードを onAdd フックに追記します。
    scene = new THREE.Scene();
    
  3. シーンにカメラを追加します。

    前述のとおり、カメラはシーンを閲覧する際の視点を表すもので、Three.js がシーン内のオブジェクトを視覚的にレンダリングする方法は、カメラによって決まります。カメラがなければ、そのシーンは「見られていない」ことになり、シーン内にオブジェクトがあってもレンダリングされず、よって表示されません。

    Three.js にはさまざまなカメラが用意されており、カメラの種類によって、レンダラによるオブジェクトの扱いに影響する要素(視野、奥行きなど)が変化します。このシーンでは PerspectiveCamera を使用します。これは Three.js でも最も一般的に使用されるカメラタイプで、肉眼による視界を再現するように設計されています。このため、たとえばカメラから遠いオブジェクトは近いオブジェクトよりも小さく見え、シーンには消失点が存在することになります。

    PerspectiveCamera 型のカメラをシーンに追加するには、次のコードを onAdd フックに追記します。
    camera = new THREE.PerspectiveCamera();
    
    PerspectiveCamera では、視点を構成する各種属性、たとえばニアプレーンとファープレーン、アスペクト比、視野(FOV)を調整することも可能です。これらの属性を総合したものが視錐台(Frustum)で、3D を扱う際にはぜひ理解しておきたい重要な概念ですが、本 Codelab のテーマからは外れます。PerspectiveCamera の設定はデフォルトのままで問題ありません。
  4. シーンに光源を追加します。

    Three.js のシーン内でレンダリングされるオブジェクトは、付与されたテクスチャを問わず、デフォルトでは真っ黒に表示されます。これは、Three.js のシーンが現実世界の物体のふるまいを再現しており、色の見え方はその物体(オブジェクト)が反射する光によって決まるためです。つまり、光がなければ色もないということです。

    Three.js にはさまざまな種類の光源が用意されており、今回は次の 2 種類を使用します。

  5. AmbientLight: シーン内の全オブジェクトをすべての角度から均等に照らす拡散光源です。シーンの基本的な光量を確保し、各オブジェクトのテクスチャが見えるようにするのに役立ちます。
  6. DirectionalLight: シーン内を特定の方向から照らす光源です。現実世界の光源とは異なり、DirectionalLight が発する光線はすべて平行光線で、光源から遠ざかっても広がったり散乱したりすることはありません。

    各光源の色味や強度を調整することにより、総合的な照明効果を生み出すことが可能です。たとえば下のコードでは、AmbientLight 型の光源でシーン全体を白く柔らかい光で満たし、DirectionalLight 型の光源で各オブジェクトを下向きに照らし出す二次的な照明を当てています。DirectionalLight 型の光源の角度は position.set(x, y ,z) で設定します。各値は空間の軸に対応しており、たとえば position.set(0,1,0) なら、シーンの直上、y 軸上からまっすぐ下を照らす光源になります。

    シーンに光源を追加するには、次のコードを onAdd フックに追記します。
    const ambientLight = new THREE.AmbientLight( 0xffffff, 0.75 );
    scene.add(ambientLight);
    const directionalLight = new THREE.DirectionalLight(0xffffff, 0.25);
    directionalLight.position.set(0.5, -1, 0.5);
    scene.add(directionalLight);
    

onAdd フックは次のようになっているはずです。

    webGLOverlayView.onAdd = () => {
      scene = new THREE.Scene();
      camera = new THREE.PerspectiveCamera();
      const ambientLight = new THREE.AmbientLight( 0xffffff, 0.75 );
      scene.add(ambientLight);
      const directionalLight = new THREE.DirectionalLight(0xffffff, 0.25);
      directionalLight.position.set(0.5, -1, 0.5);
      scene.add(directionalLight);
    }

これでシーンのセットアップが完了し、レンダリングできる状態になりました。次は WebGL レンダラの設定を行い、実際にシーンをレンダリングしましょう。

6 シーンをレンダリングする

それではシーンをレンダリングしましょう。ここまでに Three.js で作成したものは、すべてコード内で初期化されてはいるものの、WebGL レンダリング コンテキストにレンダリングされていないため、事実上まだ存在していない状態です。WebGL は、ブラウザ内での 2D および 3D コンテンツのレンダリングに Canvas API を使用します。Canvas API を使ったことがある方なら、レンダリングの場となる HTML キャンバスの context についてはよくご存知でしょう。実はこれは、ブラウザ内で WebGLRenderingContext API を通して OpenGL グラフィック レンダリング コンテキストにアクセスするインターフェースなのです。

WebGL レンダラの扱いを容易にするため Three.js に用意されているのが WebGLRenderer です。これは、Three.js がブラウザ内でシーンをレンダリングするために必要な WebGL レンダリング コンテキストの設定を比較的容易にするラッパーです。ただ、地図に関して言えば、Three.js のシーンを単に地図と並行してレンダリングするだけでは不十分です。Three.js のレンダリングは地図とまったく同じレンダリング コンテキストで行い、地図と Three.js のシーン内の全オブジェクトが同一の空間でレンダリングされるようにする必要があります。これにより、レンダラが地図上のオブジェクトとシーン内のオブジェクトの相互作用を処理できるようになります。その一例がオクルージョン、つまりオブジェクトの後ろにあるオブジェクトが見えなくなることです。

なかなか複雑そうですが、ここでも Three.js なら心配はありません。

  1. WebGL レンダラのセットアップを行います。

    Three.js の WebGLRenderer の新しいインスタンスを作成する際、シーンを組み込む WebGL レンダリング コンテキストを指定することが可能です。onContextRestored フックに渡される gl 引数のことを覚えていますか?あの gl オブジェクトが、地図の WebGL レンダリング コンテキストなのです。WebGLRenderer インスタンスにコンテキストとそのキャンバスおよび属性を受け渡せばよく、これらはいずれも gl オブジェクトを通して参照できます。このコードではさらに、レンダラがフレームごとに出力をクリアしてしまわないよう、レンダラの autoClear プロパティが false に設定されています。

    レンダラの設定を行うには、次のコードを onContextRestored フックに追記します。
    renderer = new THREE.WebGLRenderer({
      canvas: gl.canvas,
      context: gl,
      ...gl.getContextAttributes(),
    });
    renderer.autoClear = false;
    
  2. シーンのレンダリングを行います。

    レンダラの設定が済んだら、WebGLOverlayView インスタンスで requestRedraw を呼び出して次のフレームのレンダリング時に再描画が必要なことをオーバーレイに伝え、次にレンダラで render を呼び出して Three.js のシーンとカメラを渡し、レンダリングさせます。最後に、WebGL レンダリング コンテキストのステートをクリアします。WebGL Overlay View の利用は GL ステートの共有に依存しており、GL ステートの競合を防ぐためにはこのステップが重要です。描画呼び出しの終了ごとにステートがリセットされなければ、GL ステートの競合によりレンダラが失敗することがあります。

    そのためには、次のコードを onDraw フックに追記し、フレームごとに実行されるようにします。
    webGLOverlayView.requestRedraw();
    renderer.render(scene, camera);
    renderer.resetState();
    

onContextRestored フックと onDraw フックは次のようになっているはずです。

    webGLOverlayView.onContextRestored = ({gl}) => {
      renderer = new THREE.WebGLRenderer({
        canvas: gl.canvas,
        context: gl,
        ...gl.getContextAttributes(),
      });

      renderer.autoClear = false;
    }

    webGLOverlayView.onDraw = ({gl, transformer}) => {
      webGLOverlayView.requestRedraw();
      renderer.render(scene, camera);
      renderer.resetState();
    }

7. 地図上で 3D モデルをレンダリングする

WebGL Overlay View のセットアップが済み、Three.js のシーンも作成できました。これで布陣は完璧ですが、強いて言うならまだ中身がありません。次はいよいよシーン内で 3D オブジェクトをレンダリングしてみましょう。先ほどインポートした GLTFLoader を使います。

3D モデルにはさまざまな形式がありますが、Three.js の場合はサイズや実行時のパフォーマンスから glTF 形式が推奨されます。この Codelab では、シーン内でレンダリングできるモデルがあらかじめ /src/pin.gltf に用意されています。

  1. モデルローダのインスタンスを作成します。

    次のコードを onAdd に追記します。
    loader = new GLTFLoader();
    
  2. 3D モデルを読み込みます。

    モデルローダは非同期的で、モデルの読み込みが完了してからコールバックを実行します。pin.gltf を読み込むには、次のコードを onAdd に追記します。
    const source = "pin.gltf";
    loader.load(
      source,
      gltf => {}
    );
    
  3. モデルをシーンに追加します。

    次のコードを loader コールバックに追記することにより、モデルをシーンに追加することができます。追加するのは gltf ではなく gltf.scene である点に注意しましょう。
    scene.add(gltf.scene);
    
  4. カメラの射影行列の設定を行います。

    モデルを地図上で正しくレンダリングするために必要な最後の手順は、Three.js のシーン内のカメラの射影行列(projection matrix)を設定することです。射影行列は、Three.js の Matrix4 配列として指定します。これは、3 次元空間内の点と変形処理(回転、シアー、拡大縮小など)を定義する形式です。

    WebGLOverlayView の場合、Three.js のシーンをレンダリングする位置と方法を基本地図との相対値でレンダラに伝えるために、射影行列が使用されます。問題は、地図上の位置が緯度と経度のペアによる座標で指定されるのに対し、Three.js のシーン内の位置は Vector3 座標で指定されることです。お察しのとおり、この 2 種類の座標系の間の変換計算は簡単なことではありません。この問題を解決するため WebGLOverlayView は、ライフサイクル フック OnDrawcoordinateTransformer オブジェクトを渡します。このオブジェクトには fromLatLngAltitude という関数が含まれています。fromLatLngAltitudeLatLngAltitude または LatLngAltitudeLiteral オブジェクトと、省略可能な引数群としてシーンの変形を定義する値を受け取り、それらを MVP(モデル・ビュー・射影)行列に変換します。Three.js のシーンを地図上のどこでレンダリングするか、またどのように変形するかを指定すれば、あとの処理は WebGLOverlayView に任せることが可能です。その後、MVP 行列を Three.js の Matrix4 配列に変換し、カメラの射影行列をその値に設定することができます。

    下のコードでは、2 番目の引数が Three.js のシーンの高度を地上 120 メートルに設定するよう WebGL Overlay View に指示しており、このためこのモデルは空中に浮遊しているように表示されます。

    カメラの射影行列を設定するには、次のコードを onDraw フックに追記します。
    const latLngAltitudeLiteral = {
        lat: mapOptions.center.lat,
        lng: mapOptions.center.lng,
        altitude: 120
    }
    const matrix = transformer.fromLatLngAltitude(latLngAltitudeLiteral);
    camera.projectionMatrix = new THREE.Matrix4().fromArray(matrix);
    
  5. モデルを変形させます。

    ピンをよく見ると、地図に対して直立していないことがわかります。3D グラフィックでは、世界空間が x、y、z の三軸を持つだけでなく、各オブジェクトも独自のオブジェクト空間と座標軸を持ちます。

    このモデルの場合、通常はピンの「上」とされる部分が y 軸に沿って上を向くようには作成されていないため、世界空間を基準として正しい向きになるよう、rotation.set を呼び出してオブジェクトを変形させる必要があります。なお、Three.js において、回転は角度ではなくラジアンで指定される点に注意しましょう。通常は角度で考えたほうがわかりやすいため、degrees * Math.PI/180 という数式を使って適切な変換を行う必要があります。

    また、このままではモデルがやや小さいため、scale.set(x, y ,z) を呼び出して、全軸方向に均等に拡大します。

    モデルの回転と拡大を行うには、次のコードを onAddloader コールバックに追加します。追加位置は、glTF をシーンに追加する scene.add(gltf.scene)にする必要があります。
    gltf.scene.scale.set(25,25,25);
    gltf.scene.rotation.x = 180 * Math.PI/180;
    

これで、ピンの向きが地図から直立するよう調整されました。

直立したピン

onAdd フックと onDraw フックは次のようになっているはずです。

    webGLOverlayView.onAdd = () => {
      scene = new THREE.Scene();
      camera = new THREE.PerspectiveCamera();
      const ambientLight = new THREE.AmbientLight( 0xffffff, 0.75 ); // soft white light
      scene.add( ambientLight );
      const directionalLight = new THREE.DirectionalLight(0xffffff, 0.25);
      directionalLight.position.set(0.5, -1, 0.5);
      scene.add(directionalLight);

      loader = new GLTFLoader();
      const source = 'pin.gltf';
      loader.load(
        source,
        gltf => {
          gltf.scene.scale.set(25,25,25);
          gltf.scene.rotation.x = 180 * Math.PI/180;
          scene.add(gltf.scene);
        }
      );
    }

    webGLOverlayView.onDraw = ({gl, transformer}) => {
      const latLngAltitudeLiteral = {
        lat: mapOptions.center.lat,
        lng: mapOptions.center.lng,
        altitude: 100
      }

      const matrix = transformer.fromLatLngAltitude(latLngAltitudeLiteral);
      camera.projectionMatrix = new THREE.Matrix4().fromArray(matrix);

      webGLOverlayView.requestRedraw();
      renderer.render(scene, camera);
      renderer.resetState();
    }

次は、カメラの動きをアニメーション表示させましょう。

8. カメラの動きをアニメーション表示する

地図上でモデルをレンダリングして、各要素を 3 次元的に動かせるようになったので、次はその動きをプログラマティックに制御することを考えてみましょう。moveCamera 関数を使うと、地図の中心、ズーム、前後傾斜(tilt)、機首方位(heading)の各プロパティを同時に設定することにより、ユーザー エクスペリエンスを細かくコントロールできます。また、moveCamera をアニメーション ループ内で呼び出すことにより、秒間 60 フレーム近いフレームレートでフレーム間をなめらかにつなぐことができます。

  1. モデルが読み込まれるのを待ちます。

    シームレスなユーザー エクスペリエンスを構築するため、カメラの移動を始めるのは glTF モデルの読み込みが完了するまで待ってからにするのがいいでしょう。次のとおり、ローダの onLoad イベント ハンドラを onContextRestored フックに追記します。
    loader.manager.onLoad = () => {}
    
  2. アニメーション ループを作成します。

    アニメーション ループを作成する方法は 1 つだけではなく、たとえば setInterval を使う方法や requestAnimationFrame を使う方法があります。ここでは Tree.js レンダラの setAnimationLoop 関数を使用しましょう。この関数は、コールバックで宣言したすべてのコードを、Three.js が新しいフレームをレンダリングするたびに自動的に呼び出します。アニメーション ループを作成するには、前のステップの onLoad イベント ハンドラに次のコードを追加します。
    renderer.setAnimationLoop(() => {});
    
  3. アニメーション ループ内でカメラの位置を設定します。

    次に、moveCamera を呼び出して地図を更新します。ここでは、地図の読み込みに使用した mapOptions オブジェクトの各プロパティを使って、カメラの位置を定義しています。
    map.moveCamera({
      "tilt": mapOptions.tilt,
      "heading": mapOptions.heading,
      "zoom": mapOptions.zoom
    });
    
  4. フレームごとにカメラを更新します。

    最後のステップです。各フレームの終わりで mapOptions オブジェクトを更新して、次のフレームのカメラ位置を設定しましょう。このコードでは if 文により、前後傾斜(tilt)が上限の 67.5 に達するまで加算されていき、カメラが 360 度の回転を終えるまで機首方位(heading)がフレームごとに変化していきます。アニメーションが永久に続いてしまわないよう、所定のアニメーション動作が完了すると setAnimationLoopnull が渡され、アニメーションがキャンセルされるようになっています。
    if (mapOptions.tilt < 67.5) {
      mapOptions.tilt += 0.5
    } else if (mapOptions.heading <= 360) {
      mapOptions.heading += 0.2;
    } else {
      renderer.setAnimationLoop(null)
    }
    

onContextRestored フックは次のようになっているはずです。

    webGLOverlayView.onContextRestored = ({gl}) => {
      renderer = new THREE.WebGLRenderer({
        canvas: gl.canvas,
        context: gl,
        ...gl.getContextAttributes(),
      });

      renderer.autoClear = false;

      loader.manager.onLoad = () => {
        renderer.setAnimationLoop(() => {
           map.moveCamera({
            "tilt": mapOptions.tilt,
            "heading": mapOptions.heading,
            "zoom": mapOptions.zoom
          });

          if (mapOptions.tilt < 67.5) {
            mapOptions.tilt += 0.5
          } else if (mapOptions.heading <= 360) {
            mapOptions.heading += 0.2;
          } else {
            renderer.setAnimationLoop(null)
          }
        });
      }
    }

9. 完了

すべて計画どおりに完了していれば、大きな 3D のピンが表示された次のような地図が完成しているはずです。

最終的な 3D ピンの表示

学習した内容

この Codelab では幅広い内容を学習しました。以下はそのハイライトです。

  • WebGLOverlayView とそのライフサイクル フックの実装
  • 地図への Three.js の組み込み
  • Three.js のシーン(カメラと光源を含む)作成の基礎
  • Three.js を使った 3D モデルの読み込みと操作
  • moveCamera を使った地図のカメラの制御とアニメーション表示

次のステップ

WebGL もコンピュータ グラフィックス全般も複雑な分野で、学ぶべきことは尽きません。以下、基本的なリソースをいくつかご紹介します。