Créer des cartes 3D avec la vue en superposition WebGL

1. Avant de commencer

Cet atelier de programmation vous explique comment contrôler et rendre des objets sur la carte vectorielle en trois dimensions grâce aux éléments géographiques utilisant WebGL de l'API Maps JavaScript.

Repère final en 3D

Conditions préalables

Cet atelier de programmation suppose des connaissances intermédiaires de JavaScript et de l'API Maps JavaScript. Pour découvrir les bases de l'utilisation de cette API, suivez l'atelier de programmation Ajouter une carte à votre site Web (JavaScript).

Points abordés

  • Générer un ID de carte, avec la carte vectorielle pour JavaScript activée
  • Contrôler la carte avec l'inclinaison et la rotation programmatiques
  • Afficher des objets en 3D sur la carte avec WebGLOverlayView et Three.js
  • Animer les mouvements de la caméra avec moveCamera

Prérequis

  • Un compte Google Cloud Platform pour lequel la facturation est activée
  • Une clé API Google Maps Platform pour laquelle l'API Maps JavaScript est activée
  • Des connaissances intermédiaires en JavaScript, HTML et CSS
  • Un éditeur de texte ou un IDE de votre choix
  • Node.js

2. Configuration

Pour réaliser l'étape ci-dessous, vous devez activer l'API Maps JavaScript.

Configurer Google Maps Platform

Si vous ne disposez pas encore d'un compte Google Cloud Platform ni d'un projet pour lequel la facturation est activée, consultez le guide Premiers pas avec Google Maps Platform pour savoir comment créer un compte de facturation et un projet.

  1. Dans Cloud Console, cliquez sur le menu déroulant des projets, puis sélectionnez celui que vous souhaitez utiliser pour cet atelier de programmation.

  1. Activez les API et les SDK Google Maps Platform requis pour cet atelier de programmation dans Google Cloud Marketplace. Pour ce faire, suivez les étapes indiquées dans cette vidéo ou dans cette documentation.
  2. Générez une clé API sur la page Identifiants de Cloud Console. Vous pouvez suivre la procédure décrite dans cette vidéo ou dans cette documentation. Toutes les requêtes envoyées à Google Maps Platform nécessitent une clé API.

Configurer Node.js

Si vous ne l'avez pas encore fait, accédez à https://nodejs.org/ afin de télécharger et d'installer l'environnement d'exécution Node.js sur votre ordinateur.

Node.js est fourni avec le gestionnaire de packages npm, dont vous avez besoin pour installer les dépendances pour cet atelier de programmation.

Télécharger le modèle de projet initial

Avant de commencer cet atelier de programmation, procédez comme suit pour télécharger le modèle de projet initial, ainsi que le code complet de la solution :

  1. Téléchargez ou dupliquez le dépôt GitHub de cet atelier de programmation depuis https://github.com/googlecodelabs/maps-platform-101-webgl/. Le projet initial se trouve dans le répertoire /starter et inclut la structure de base de fichier dont vous avez besoin pour suivre l'atelier de programmation. Tous les éléments avec lesquels vous devez travailler se trouvent dans le répertoire /starter/src.
  2. Après avoir téléchargé le projet initial, exécutez npm install dans le répertoire /starter. Cette opération installe toutes les dépendances nécessaires indiquées dans package.json.
  3. Une fois les dépendances installées, exécutez npm start dans le répertoire.

Le projet initial a été configuré pour vous permettre d'utiliser webpack-dev-server, qui compile et exécute le code que vous rédigez en local. webpack-dev-server actualise aussi automatiquement votre application dans le navigateur chaque fois que vous modifiez le code.

Si vous souhaitez voir le code complet de la solution s'exécuter, vous pouvez suivre la procédure de configuration ci-dessus dans le répertoire /solution.

Ajouter votre clé API

L'application de démarrage inclut l'ensemble du code nécessaire pour charger la carte avec le chargeur d'API JavaScript. Il vous suffit donc de fournir votre clé API et votre ID de carte. Le chargeur d'API JavaScript est une bibliothèque simple qui extrait la méthode traditionnelle de chargement de l'API Maps JavaScript intégrée dans le modèle HTML avec une balise script. Vous pouvez ainsi tout gérer dans le code JavaScript.

Pour ajouter votre clé API, procédez comme suit dans le projet initial :

  1. Ouvrez app.js.
  2. Dans l'objet apiOptions, définissez votre clé API en tant que valeur de apiOptions.apiKey.

3. Générer et utiliser un ID de carte

Pour utiliser les éléments géographiques WebGL de l'API Maps JavaScript, vous avez besoin d'un ID de carte, avec la carte vectorielle activée.

Générer un ID de carte

Génération d'ID de carte

  1. Dans Google Cloud Console, accédez à Google Maps Platform > Gestion des cartes.
  2. Cliquez sur "CRÉER UN ID DE CARTE".
  3. Dans le champ "Nom de la carte", saisissez le nom de votre ID de carte.
  4. Dans le menu déroulant "Type de carte", sélectionnez "JavaScript". Les options JavaScript s'affichent.
  5. Sous "Options JavaScript", cochez les cases "Vecteur", "Inclinaison" et "Rotation".
  6. (Facultatif) Dans le champ "Description", saisissez la description de votre clé API.
  7. Cliquez sur le bouton "Suivant". La page "Détails de l'ID de carte" s'affiche.

    Page "Détails de la carte"
  8. Copiez l'ID de carte. Vous l'utiliserez à l'étape suivante pour charger la carte.

Utiliser un ID de carte

Pour charger la carte vectorielle, vous devez fournir un ID de carte en tant que propriété dans les options lorsque vous instanciez la carte. Vous pouvez également fournir le même ID de carte lorsque vous chargez l'API Maps JavaScript.

Pour charger la carte avec votre ID de carte, procédez comme suit :

  1. Définissez votre ID de carte en tant que valeur de mapOptions.mapId.

    Fournir l'ID de carte au moment où vous instanciez la carte indique à Google Maps Platform quelles sont les cartes à charger pour une instance donnée. Vous pouvez réutiliser le même ID de carte dans plusieurs applications ou dans plusieurs vues d'une même application.
    const mapOptions = {
      "tilt": 0,
      "heading": 0,
      "zoom": 18,
      "center": { lat: 35.6594945, lng: 139.6999859 },
      "mapId": "YOUR_MAP_ID"
    };
    

Vérifiez l'application exécutée dans votre navigateur. La carte vectorielle (avec l'inclinaison et la rotation activées) doit se charger correctement. Pour vérifier si l'inclinaison et la rotation sont activées, maintenez la touche Maj enfoncée, et faites glisser la carte avec votre souris ou utilisez les touches fléchées du clavier.

Si la carte ne se charge pas, vérifiez que vous avez fourni une clé API valide dans apiOptions. Si l'inclinaison et la rotation ne fonctionnent pas sur la carte, vérifiez que vous avez fourni un ID de carte avec l'inclinaison et la rotation activées dans apiOptions et mapOptions.

Carte inclinée

Votre fichier app.js devrait désormais se présenter comme suit :

    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. Implémenter WebGLOverlayView

WebGLOverlayView vous permet d'accéder directement au même contexte de rendu WebGL que celui utilisé pour afficher la carte de base vectorielle. Ainsi, vous pouvez rendre des objets 2D et 3D directement sur la carte à l'aide de WebGL et avec des bibliothèques graphiques WebGL populaires.

WebGLOverlayView expose cinq hooks dans le cycle de vie du contexte de rendu WebGL de la carte que vous pouvez utiliser. Voici une brève description des différents hooks et de leur utilisation :

  • onAdd() : appelé lorsque la superposition est ajoutée à une carte en appelant setMap sur une instance WebGLOverlayView. C'est ici que vous devez effectuer vos tâches liées à WebGL qui ne nécessitent pas d'accéder directement au contexte WebGL.
  • onContextRestored() : appelé lorsque le contexte WebGL devient disponible, mais avant le rendu de tout objet. C'est ici que vous devez initialiser les objets, lier l'état et effectuer toute autre action nécessitant d'accéder au contexte WebGL, mais pouvant être exécutée en dehors de l'appel onDraw(). Vous pouvez ainsi configurer tout ce dont vous avez besoin sans trop surcharger le rendu réel de la carte, qui utilise déjà beaucoup le GPU.
  • onDraw() : appelé une fois par image, une fois que WebGL commence à rendre la carte et tout autre élément demandé. Vous devez travailler le moins possible dans onDraw() pour éviter tout problème de performances dans le rendu de la carte.
  • onContextLost() : appelé lorsque le contexte de rendu WebGL est perdu pour une raison quelconque.
  • onRemove() : appelé lorsque la superposition est supprimée de la carte en appelant setMap(null) sur une instance WebGLOverlayView.

Dans cette étape, vous allez créer une instance de WebGLOverlayView et implémenter trois de ses hooks de cycle de vie : onAdd, onContextRestored et onDraw. Pour que tout soit plus simple et plus facile à suivre, l'ensemble du code de superposition sera géré dans la fonction initWebGLOverlayView() fournie dans le modèle de projet initial de cet atelier de programmation.

  1. Créez une instance WebGLOverlayView().

    La superposition est fournie par l'API Maps JavaScript dans google.maps.WebGLOverlayView. Pour commencer, créez une instance en ajoutant le code suivant à initWebGLOverlayView() :
    const webGLOverlayView = new google.maps.WebGLOverlayView();
    
  2. Implémentez des hooks de cycle de vie.

    Pour implémenter les hooks de cycle de vie, ajoutez le code suivant à initWebGLOverlayView() :
    webGLOverlayView.onAdd = () => {};
    webGLOverlayView.onContextRestored = ({gl}) => {};
    webGLOverlayView.onDraw = ({gl, coordinateTransformer}) => {};
    
  3. Ajoutez l'instance de superposition à la carte.

    Appelez maintenant setMap() sur l'instance de superposition et transmettez-le dans la carte en ajoutant le code suivant à initWebGLOverlayView() :
    webGLOverlayView.setMap(map)
    
  4. Appelez initWebGLOverlayView.

    La dernière étape consiste à exécuter initWebGLOverlayView() en ajoutant le code suivant à la fonction appelée immédiatement en bas du fichier app.js :
    initWebGLOverlayView(map);
    

Votre fonction initWebGLOverlayView et la fonction appelée immédiatement devraient désormais se présenter comme suit :

    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);
    })();

Vous n'avez besoin de rien d'autre pour implémenter WebGLOverlayView. Vous allez ensuite configurer tout ce dont vous avez besoin pour rendre un objet 3D sur la carte à l'aide de Three.js.

5. Configurer une scène Three.js

Utiliser WebGL peut être très compliqué, car cette API nécessite de définir tous les aspects de chaque objet manuellement, très minutieusement. Pour vous faciliter la tâche, vous allez utiliser Three.js dans cet atelier de programmation. Il s'agit d'une bibliothèque graphique populaire dotée d'une couche d'abstraction simplifiée reposant sur WebGL. Three.js propose un large éventail de fonctions pratiques, permettant de créer un moteur de rendu WebGL, dessiner des formes courantes d'objets en 2D et 3D, contrôler les caméras, transformer des objets et bien plus encore.

Trois types d'objets de base sont nécessaires dans Three.js pour afficher n'importe quel élément :

  • Scène : un "conteneur" dans lequel tous les objets, sources de lumière, textures, etc. sont rendus et affichés.
  • Caméra : représente le point de vue de la scène. Plusieurs types de caméras sont disponibles, et vous pouvez ajouter une ou plusieurs caméras à une même scène.
  • Moteur de rendu : gère le traitement et l'affichage de tous les objets de la scène. Dans Three.js, WebGLRenderer est le moteur le plus utilisé, mais quelques autres sont disponibles en tant que solutions de secours au cas où le client ne serait pas compatible avec WebGL.

Dans cette étape, vous allez charger toutes les dépendances nécessaires à Three.js et configurer une scène de base.

  1. Chargez triple.js.

    Pour cet atelier de programmation, vous aurez besoin de deux dépendances : la bibliothèque Three.js et le chargeur glTF, une classe qui vous permet de charger des objets 3D au format glTF (GL Transmission Format). Three.js propose des chargeurs spécialisés pour de nombreux formats d'objets 3D. Toutefois, nous vous recommandons d'utiliser glTF.

    Dans le code ci-dessous, la totalité de la bibliothèque Three.js est importée. Dans une application de productivité, vous souhaiterez probablement n'importer que les classes dont vous avez besoin. Toutefois, pour cet atelier de programmation, importez l'intégralité de la bibliothèque pour faire plus simple. Notez également que le chargeur glTF n'est pas inclus dans la bibliothèque par défaut. De plus, il doit être importé à partir d'un chemin distinct dans la dépendance, à savoir le chemin qui vous permet d'accéder à tous les chargeurs fournis par Three.js.

    Pour importer Three.js et le chargeur glTF, ajoutez le code suivant en haut du fichier app.js :
    import * as THREE from 'three';
    import {GLTFLoader} from 'three/examples/jsm/loaders/GLTFLoader.js';
    
  2. Créez une scène Three.js.

    Pour créer une scène, instanciez la classe Scene de Three.js en ajoutant le code suivant au hook onAdd :
    scene = new THREE.Scene();
    
  3. Ajoutez une caméra à la scène.

    Comme indiqué précédemment, la caméra représente le point de vue de la scène et détermine comment Three.js gère le rendu visuel des objets d'une scène. Sans caméra, la scène n'est en fait pas "vue", ce qui signifie que les objets n'apparaissent pas, car ils ne sont pas rendus.

    Three.js propose plusieurs caméras différentes qui influent sur la manière dont le moteur de rendu traite les objets en fonction d'éléments tels que la perspective et la profondeur. Dans cette scène, vous allez utiliser PerspectiveCamera, le type de caméra le plus courant dans Three.js. Il est conçu pour émuler la façon dont l'œil humain percevrait la scène. Autrement dit, les objets plus éloignés de la caméra paraîtront plus petits que ceux qui sont plus proches, la scène comportera un point de fuite, etc.

    Pour ajouter une caméra de perspective à la scène, ajoutez le code suivant au hook onAdd :
    camera = new THREE.PerspectiveCamera();
    
    Avec PerspectiveCamera, vous pouvez également configurer les attributs qui composent le point de vue, y compris les plans éloignés et rapprochés, les proportions et le champ visuel. Collectivement, ces attributs font partie de ce que l'on appelle le frustum de vue, un concept important que vous devez comprendre pour travailler dans l'environnement 3D. Il ne fait toutefois pas l'objet de cet atelier de programmation. La configuration par défaut de PerspectiveCamera suffira.
  4. Ajoutez des sources de lumière à la scène.

    Par défaut, les objets rendus dans une scène Three.js apparaîtront en noir, quelles que soient les textures qui leur sont appliquées. En effet, une scène Three.js imite le comportement des objets dans le monde réel, où la visibilité des couleurs dépend de la lumière se réfléchissant sur un objet. En bref, pas de lumière, pas de couleur.

    Three.js propose plusieurs types d'éclairages différents. Vous en utiliserez deux :

  5. AmbientLight : fournit une source de lumière diffuse qui éclaire tous les objets de la scène de façon uniforme, sous tous les angles. La scène bénéficie ainsi d'une quantité de lumière de référence qui permet de rendre visibles les textures de tous les objets.
  6. DirectionalLight : fournit une lumière provenant d'une direction donnée dans la scène. Contrairement à la façon dont une lumière positionnée fonctionnerait dans le monde réel, les rayons lumineux émis par DirectionalLight sont tous parallèles. Ils ne se propagent pas et ne se diffusent pas lorsqu'ils s'éloignent de la source de lumière.

    Vous pouvez configurer la couleur et l'intensité de chaque lumière pour créer des effets d'éclairage combinés. Dans le code ci-dessous, par exemple, la lumière ambiante fournit une lumière blanche tamisée pour l'ensemble de la scène, tandis que la lumière directionnelle fournit une lumière secondaire qui touche les objets selon un angle descendant. Dans le cas de la lumière directionnelle, l'angle est défini à l'aide de position.set(x, y ,z), chaque valeur étant relative à l'axe respectif. Ainsi, position.set(0,1,0) placerait la lumière directement au-dessus de la scène sur l'axe y pointant directement vers le bas, par exemple.

    Pour ajouter les sources de lumière à la scène, ajoutez le code suivant au hook 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);
    

Votre hook onAdd devrait désormais se présenter comme suit :

    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);
    }

Votre scène est maintenant configurée et prête à être rendue. Vous allez ensuite configurer le moteur de rendu WebGL et rendre la scène.

6. Rendre la scène

Le moment est venu d'effectuer le rendu de votre scène. Jusqu'ici, tous les éléments que vous avez créés avec Three.js sont initialisés dans le code, mais n'existent pas dans l'absolu. En effet, ils n'ont pas encore été rendus dans le contexte de rendu WebGL. WebGL permet de rendre des contenus 2D et 3D dans le navigateur à l'aide de l'API Canvas. Si vous avez déjà utilisé l'API Canvas, vous connaissez probablement l'élément context d'un canevas HTML, où tout le rendu est effectué. Par contre, vous ignorez peut-être qu'il s'agit d'une interface qui affiche le contexte de rendu graphique OpenGL via l'API WebGLRenderingContext dans le navigateur.

Pour simplifier l'utilisation du moteur de rendu WebGL, Three.js fournit WebGLRenderer, un wrapper qui permet de configurer le contexte de rendu WebGL assez facilement afin que Three.js puisse afficher les scènes dans le navigateur. Toutefois, dans le cas de la carte, rendre uniquement la scène Three.js dans le navigateur avec la carte ne suffit pas. Three.js doit s'afficher dans exactement le même contexte de rendu que la carte, de sorte que la carte et tous les objets de la scène Three.js soient rendus dans le même espace mondial. Cela permet au moteur de rendu de gérer les interactions entre les objets sur la carte et les objets de la scène, comme une occlusion : ce terme élaboré signifie qu'un objet en masque d'autres derrière lui.

Cela semble assez compliqué, n'est-ce pas ? Heureusement, Three.js vient de nouveau à la rescousse.

  1. Configurez le moteur de rendu WebGL.

    Lorsque vous créez une instance du WebGLRenderer de Three.js, vous pouvez lui fournir le contexte de rendu WebGL spécifique dans lequel vous souhaitez qu'il affiche votre scène. Vous souvenez-vous de l'argument gl transmis au hook onContextRestored ? Cet objet gl correspond au contexte de rendu WebGL de la carte. Il vous suffit de fournir le contexte, son canevas et ses attributs à l'instance WebGLRenderer. Tous sont disponibles via l'objet gl. Dans ce code, la propriété autoClear du moteur de rendu est également définie sur false. Ainsi, le moteur n'efface pas son résultat à chaque image.

    Pour configurer le moteur de rendu, ajoutez le code suivant au hook onContextRestored :
    renderer = new THREE.WebGLRenderer({
      canvas: gl.canvas,
      context: gl,
      ...gl.getContextAttributes(),
    });
    renderer.autoClear = false;
    
  2. Effectuez le rendu de la scène.

    Une fois le moteur de rendu configuré, appelez requestRedraw sur l'instance WebGLOverlayView pour indiquer à la superposition qu'un nouveau dessin est nécessaire lorsque l'image suivante s'affiche. Ensuite, appelez render sur le moteur de rendu et transmettez-lui la caméra et la scène Three.js aux fins du rendu. Enfin, effacez l'état du contexte de rendu WebGL. Cette étape est importante pour éviter les conflits d'état GL, car l'utilisation de la vue en superposition WebGL repose sur l'état GL partagé. Si l'état n'est pas réinitialisé à la fin de chaque appel de dessin, les conflits d'état GL peuvent entraîner l'échec du moteur de rendu.

    Pour ce faire, ajoutez le code suivant au hook onDraw afin qu'il soit exécuté à chaque image :
    webGLOverlayView.requestRedraw();
    renderer.render(scene, camera);
    renderer.resetState();
    

Vos hooks onContextRestored et onDraw devraient désormais se présenter comme suit :

    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. Rendre un modèle 3D sur la carte

Bien, vous disposez de tous les éléments nécessaires. Vous avez configuré la vue en superposition WebGL et créé une scène Three.js, mais un problème est survenu : celle-ci n'affiche rien. Vous allez donc maintenant rendre un objet 3D dans la scène. Pour ce faire, vous allez utiliser le chargeur glTF que vous avez importé précédemment.

Les modèles 3D sont disponibles dans de nombreux formats différents. Cependant, pour Three.js, le format glTF est recommandé en raison de sa taille et de ses performances d'exécution. Dans cet atelier de programmation, un modèle à afficher dans la scène est déjà fourni dans /src/pin.gltf.

  1. Créez une instance de chargeur de modèle.

    Ajoutez le code suivant à onAdd :
    loader = new GLTFLoader();
    
  2. Chargez un modèle 3D.

    Les chargeurs de modèles sont asynchrones et exécutent un rappel une fois le modèle complètement chargé. Pour charger pin.gltf, ajoutez le code suivant à onAdd :
    const source = "pin.gltf";
    loader.load(
      source,
      gltf => {}
    );
    
  3. Ajoutez le modèle à la scène.

    Vous pouvez maintenant ajouter le modèle à la scène en ajoutant le code suivant au rappel loader. Notez que c'est gltf.scene qui est ajouté, et non gltf :
    scene.add(gltf.scene);
    
  4. Configurez la matrice de projection de la caméra.

    La dernière étape pour que le modèle s'affiche correctement sur la carte consiste à définir la matrice de projection de la caméra dans la scène Three.js. La matrice est définie comme un tableau Matrix4 dans Three.js, qui définit un point dans un espace tridimensionnel ainsi que des transformations telles que les rotations, le cisaillement, l'échelle, etc.

    Dans le cas de WebGLOverlayView, la matrice de projection permet d'indiquer au moteur de rendu où et comment rendre la scène Three.js par rapport à la carte de base. Il y a cependant un problème. Les emplacements sur la carte correspondent à des paires de coordonnées (latitude et longitude), tandis que les emplacements dans la scène Three.js sont des coordonnées de type Vector3. Comme vous vous en doutez sûrement, le calcul des conversions entre les deux systèmes n'est pas simple. Pour résoudre ce problème, WebGLOverlayView transmet un objet coordinateTransformer au hook de cycle de vie OnDraw contenant une fonction appelée fromLatLngAltitude. fromLatLngAltitude prend un objet LatLngAltitude ou LatLngAltitudeLiteral, et éventuellement un ensemble d'arguments qui définissent une transformation pour la scène, puis les convertit en une matrice de projection de vue de modèle pour vous. Il vous suffit de préciser où vous souhaitez que la scène Three.js soit rendue sur la carte, ainsi que la méthode de transformation. WebGLOverlayView s'occupe du reste. Vous pouvez ensuite convertir la matrice de projection de vue de modèle en tableau Matrix4 dans Three.js et définir la matrice de projection de la caméra sur ce tableau.

    Dans le code ci-dessous, le deuxième argument indique à la vue en superposition WebGL de définir l'altitude de la scène Three.js à 120 mètres au-dessus du sol, ce qui donne l'impression que le modèle flotte.

    Pour définir la matrice de projection de la caméra, ajoutez le code suivant au hook 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. Transformez le modèle.

    Vous constaterez que le repère n'est pas perpendiculaire à la carte. Dans les graphismes en 3D, en plus de l'espace mondial qui comporte ses propres axes x, y et z déterminant l'orientation, chaque objet dispose également de son propre espace objet avec un ensemble indépendant d'axes.

    Dans le cas de ce modèle, il n'a pas été créé avec ce que nous considérons normalement comme le "haut" du repère faisant face à l'axe y. Vous devez donc transformer l'objet pour l'orienter de la façon souhaitée par rapport à l'espace mondial en appelant rotation.set. Notez que dans Three.js, la rotation est indiquée en radians et non en degrés. Il est généralement plus facile de penser en degrés. Par conséquent, vous devez effectuer la conversion appropriée avec la formule degrees * Math.PI/180.

    De plus, comme le modèle est un peu petit, vous le mettrez à l'échelle de façon uniforme sur tous les axes en appelant scale.set(x, y ,z).

    Pour faire pivoter le modèle et le mettre à l'échelle, ajoutez le code suivant dans le rappel loader de onAdd avant scene.add(gltf.scene), qui ajoute le fichier glTF à la scène :
    gltf.scene.scale.set(25,25,25);
    gltf.scene.rotation.x = 180 * Math.PI/180;
    

Le repère se trouve maintenant à la verticale par rapport à la carte.

Repère à la verticale

Vos hooks onAdd et onDraw devraient désormais se présenter comme suit :

    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();
    }

Passons maintenant aux animations de la caméra.

8. Animer la caméra

Maintenant que vous avez rendu un modèle sur la carte et que vous pouvez déplacer tous les éléments de façon tridimensionnelle, vous souhaiterez sans doute pouvoir ensuite contrôler ce mouvement par programmation. La fonction moveCamera vous permet de définir simultanément les propriétés du centre, du zoom, de l'inclinaison et de la direction. Vous bénéficiez ainsi d'un contrôle précis sur l'expérience utilisateur. En outre, il est possible d'appeler moveCamera dans une boucle d'animation pour créer des transitions fluides entre les images, à une fréquence de près de 60 images par seconde.

  1. Attendez que le modèle soit chargé.

    Pour créer une expérience utilisateur fluide, attendez que le modèle glTF soit chargé pour commencer à déplacer la caméra. Pour ce faire, ajoutez le gestionnaire d'événements onLoad du chargeur au hook onContextRestored :
    loader.manager.onLoad = () => {}
    
  2. Créez une boucle d'animation.

    Il existe plusieurs façons de créer une boucle d'animation, par exemple en utilisant setInterval ou requestAnimationFrame. Dans ce cas, vous utiliserez la fonction setAnimationLoop du moteur de rendu Three.js, qui appelle automatiquement tout code déclaré dans son rappel chaque fois que Three.js rend une nouvelle image. Pour créer la boucle d'animation, ajoutez le code suivant au gestionnaire d'événements onLoad à l'étape précédente :
    renderer.setAnimationLoop(() => {});
    
  3. Définissez la position de la caméra dans la boucle d'animation.

    Ensuite, appelez moveCamera pour mettre à jour la carte. Ici, les propriétés de l'objet mapOptions utilisé pour charger la carte servent à définir la position de la caméra :
    map.moveCamera({
      "tilt": mapOptions.tilt,
      "heading": mapOptions.heading,
      "zoom": mapOptions.zoom
    });
    
  4. Mettez à jour la caméra à chaque image.

    Dernière étape. Mettez à jour l'objet mapOptions à la fin de chaque image pour définir la position de la caméra pour l'image suivante. Dans ce code, une instruction if est utilisée pour augmenter l'inclinaison jusqu'à ce que la valeur maximale de 67,5 soit atteinte. Ensuite, la direction est modifiée légèrement à chaque image, jusqu'à ce que la caméra ait effectué une rotation complète à 360 degrés. Une fois l'animation souhaitée terminée, null est transmis à setAnimationLoop afin d'annuler cette animation, de sorte qu'elle ne s'exécute pas éternellement.
    if (mapOptions.tilt < 67.5) {
      mapOptions.tilt += 0.5
    } else if (mapOptions.heading <= 360) {
      mapOptions.heading += 0.2;
    } else {
      renderer.setAnimationLoop(null)
    }
    

Votre hook onContextRestored devrait désormais se présenter comme suit :

    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. Félicitations

Si tout s'est déroulé comme prévu, vous devez normalement avoir une carte avec un grand repère 3D semblable à celui-ci :

Repère final en 3D

Ce que vous avez appris

Dans cet atelier de programmation, vous avez appris différentes choses, et particulièrement les points suivants :

  • Implémenter WebGLOverlayView et ses hooks de cycle de vie
  • Intégrer Three.js à la carte
  • Les bases pour créer une scène Three.js, y compris les caméras et l'éclairage
  • Charger et manipuler des modèles 3D à l'aide de Three.js
  • Contrôler et animer la caméra pour la carte à l'aide de moveCamera

Étapes suivantes

WebGL et l'infographie en général sont des sujets complexes, pour lesquels il y a toujours matière à apprendre. Pour commencer, familiarisez-vous avec les ressources suivantes :