Compila experiencias de mapas 3D con la vista de superposición de WebGL

1. Antes de comenzar

En este codelab, aprenderás a utilizar las funciones basadas en la tecnología de WebGL disponibles en la API de Maps JavaScript para controlar y renderizar experiencias tridimensionales en el mapa vectorial.

Pin 3D final

Requisitos previos

En este codelab, se supone que tienes conocimientos intermedios de JavaScript y la API de Maps JavaScript. Para familiarizarte con los aspectos básicos del uso de la API de Maps JS, consulta el codelab Cómo agregar un mapa a tu sitio web (JavaScript).

Qué aprenderás

  • Cómo generar un ID de mapa con el mapa vectorial para JavaScript habilitado
  • Cómo controlar el mapa con la inclinación y la rotación programáticas
  • Cómo renderizar objetos 3D en el mapa con WebGLOverlayView y Three.js
  • Cómo animar los movimientos de cámara con moveCamera

Otros requisitos

  • Una cuenta de Google Cloud Platform con facturación habilitada
  • Una clave de API de Google Maps Platform con la API de Maps JavaScript habilitada
  • Conocimientos intermedios de JavaScript, HTML y CSS
  • El editor de texto o IDE que prefieras
  • Node.js

2. Prepárate

Para este paso, debes habilitar la API de Maps JavaScript.

Configura Google Maps Platform

Si todavía no tienes una cuenta de Google Cloud Platform y un proyecto con la facturación habilitada, consulta la guía Cómo comenzar a utilizar Google Maps Platform para crear una cuenta de facturación y un proyecto.

  1. En Cloud Console, haz clic en el menú desplegable del proyecto y selecciona el proyecto que deseas usar para este codelab.

  1. Habilita las API y los SDK de Google Maps Platform necesarios para este codelab en Google Cloud Marketplace. Para hacerlo, sigue los pasos que se indican en este video o esta documentación.
  2. Genera una clave de API en la página Credenciales de Cloud Console. Puedes seguir los pasos que se indican en este video o esta documentación. Todas las solicitudes a Google Maps Platform requieren una clave de API.

Configuración de Node.js

Si todavía no lo tienes, ve a https://nodejs.org/ para descargar y, luego, instalar el entorno de ejecución de Node.js en tu computadora.

Node.js incluye el administrador de paquetes npm, que necesitas para instalar las dependencias que requiere este codelab.

Descarga la plantilla de proyecto inicial

Antes de comenzar este codelab, sigue estos pasos para descargar la plantilla de proyecto inicial y el código completo de la solución:

  1. Descarga o bifurca el repositorio de GitHub correspondiente a este codelab, que puedes encontrar en https://github.com/googlecodelabs/maps-platform-101-webgl/. El proyecto inicial se encuentra en el directorio /starter y tiene la estructura de archivos básica que utilizarás para completar el codelab. Todo lo que necesitas para este proyecto se encuentra en el directorio /starter/src.
  2. Una vez que hayas descargado el proyecto inicial, ejecuta npm install en el directorio /starter. Esto instalará todas las dependencias necesarias que se mencionan en el archivo package.json.
  3. Una vez instaladas las dependencias, ejecuta npm start en el directorio.

El proyecto inicial se configuró para que uses webpack-dev-server, que compila y ejecuta el código que escribes de manera local. webpack-dev-server también vuelve a cargar tu app automáticamente en el navegador cada vez que realices cambios en el código.

Si deseas ver el código completo de la solución en ejecución, puedes completar los pasos de configuración detallados arriba para el contenido del directorio /solution.

Agrega tu clave de API

La app de base incluye todo el código necesario para cargar el mapa con el cargador de la API de JS, de modo que lo único que debes hacer es proporcionar la clave de API y el ID de mapa. El cargador de la API de JS es una biblioteca simple que abstrae el método tradicional para cargar la API de Maps JS intercalado en la plantilla HTML con una etiqueta script, lo que te permitirá controlar todo en código JavaScript.

Para agregar tu clave de API, haz lo siguiente en el proyecto inicial:

  1. Abre app.js.
  2. En el objeto apiOptions, establece tu clave de API como el valor de apiOptions.apiKey.

3. Genera y utiliza un ID de mapa

Para utilizar las funciones basadas en WebGL de la API de Maps JavaScript, debes generar un ID de mapa con el mapa vectorial habilitado.

Cómo generar un ID de mapa

Generación de un ID de mapa

  1. En Google Cloud Console, ve a “Google Maps Platform” > "Administración de mapas" (Map Management).
  2. Haz clic en "CREAR ID DE MAPA NUEVO" (CREATE NEW MAP ID).
  3. En el campo "Nombre del mapa" (Map name), ingresa un nombre para tu ID de mapa.
  4. En el menú desplegable "Tipo de mapa" (Map type), selecciona "JavaScript". Aparecerá la sección "JavaScript Options".
  5. En "JavaScript Options", marca el botón de selección "Vector" y las casillas de verificación junto a "Inclinación" (Tilt) y "Rotación" (Rotation).
  6. Opcional. En el campo "Descripción" (Description), ingresa una descripción para tu clave de API.
  7. Haz clic en el botón "Siguiente" (Next). Aparecerá la página "Detalles del ID de mapa" (Map ID Details).

    Página de detalles del mapa
  8. Copia el ID de mapa. Lo utilizarás en el paso siguiente para cargar el mapa.

Cómo utilizar un ID de mapa

Para cargar el mapa vectorial, debes proporcionar un ID de mapa como propiedad en las opciones al crear la instancia correspondiente. También puedes proporcionar el mismo ID de mapa al cargar la API de Maps JavaScript.

Para cargar el mapa con tu ID de mapa, haz lo siguiente:

  1. Establece tu ID de mapa como el valor de mapOptions.mapId.

    Si proporcionas el ID de mapa al crear la instancia, le indicas a Google Maps Platform cuál de tus mapas debe cargar para una instancia determinada. Puedes volver a utilizar el mismo ID de mapa en varias apps o en varias vistas dentro de una misma app.
    const mapOptions = {
      "tilt": 0,
      "heading": 0,
      "zoom": 18,
      "center": { lat: 35.6594945, lng: 139.6999859 },
      "mapId": "YOUR_MAP_ID"
    };
    

Verifica la app que se ejecuta en tu navegador. El mapa vectorial con la inclinación y la rotación habilitadas debería cargarse correctamente. Para comprobar que la inclinación y la rotación estén habilitadas, mantén presionada la tecla Mayúsculas y arrastra el mapa con el mouse o las teclas de flecha.

Si el mapa no se carga, verifica que hayas proporcionado una clave de API válida en apiOptions. Si el mapa no se inclina ni rota, verifica que hayas proporcionado un ID de mapa con las funciones de inclinación y rotación habilitadas en apiOptions y mapOptions.

Mapa inclinado

Tu archivo app.js ahora debería verse de la siguiente manera:

    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. Implementa WebGLOverlayView

WebGLOverlayView te brinda acceso directo al mismo contexto de renderización de WebGL que se utiliza para procesar el mapa base vectorial. Esto significa que puedes renderizar objetos 2D y 3D directamente en el mapa con WebGL, así como bibliotecas de gráficos populares basadas en WebGL.

WebGLOverlayView expone cinco hooks en el ciclo de vida del contexto de renderización de WebGL del mapa que puedes utilizar. A continuación, se incluye una breve descripción de cada hook y para qué sirven:

  • onAdd(): Es el hook al que se llama cuando se agrega la superposición a un mapa mediante una llamada a setMap en una instancia de WebGLOverlayView. Aquí es donde debes realizar cualquier trabajo relacionado con WebGL que no requiera acceso directo al contexto de WebGL.
  • onContextRestored(): Es el hook al que se llama cuando el contexto de WebGL ya está disponible, pero antes de llevar a cabo cualquier renderización. Aquí es donde debes inicializar objetos, vincular un estado y realizar cualquier otra acción que requiera acceso al contexto de WebGL, pero que pueda completarse fuera de la llamada a onDraw(). Esto te permite configurar todo lo que necesitas sin agregar sobrecarga adicional a la renderización real del mapa, que ya exige bastante a la GPU.
  • onDraw(): Es el hook al que se llama una vez por fotograma cuando WebGL comienza a renderizar el mapa y cualquier otro elemento que hayas solicitado. Debes realizar la menor cantidad posible de trabajo en onDraw() para evitar causar problemas de rendimiento en la renderización del mapa.
  • onContextLost(): Es el hook al que se llama cuando se pierde el contexto de renderización de WebGL por cualquier motivo.
  • onRemove(): Es el hook al que se llama cuando se quita la superposición del mapa mediante una llamada a setMap(null) en una instancia de WebGLOverlayView.

En este paso, crearás una instancia de WebGLOverlayView y, luego, implementarás tres de sus hooks de ciclo de vida: onAdd, onContextRestored y onDraw. Para mantener la información organizada y asegurarnos de que sea más fácil de seguir, todo el código de la superposición se controlará en la función initWebGLOverlayView() que se proporciona en la plantilla inicial de este codelab.

  1. Crea una instancia de WebGLOverlayView().

    La API de Maps JS proporciona la superposición en google.maps.WebGLOverlayView. Comienza por crear una instancia. Para ello, agrega lo siguiente a initWebGLOverlayView():
    const webGLOverlayView = new google.maps.WebGLOverlayView();
    
  2. Implementa los hooks de ciclo de vida.

    Para ello, agrega lo siguiente a initWebGLOverlayView():
    webGLOverlayView.onAdd = () => {};
    webGLOverlayView.onContextRestored = ({gl}) => {};
    webGLOverlayView.onDraw = ({gl, coordinateTransformer}) => {};
    
  3. Agrega la instancia de superposición al mapa.

    Ahora, llama a setMap() en la instancia de superposición y pasa el mapa. Para ello, agrega lo siguiente a initWebGLOverlayView():
    webGLOverlayView.setMap(map)
    
  4. Llama a initWebGLOverlayView.

    El último paso es ejecutar initWebGLOverlayView(). Para ello, agrega lo siguiente a la función invocada de inmediato en la parte inferior de app.js:
    initWebGLOverlayView(map);
    

La función initWebGLOverlayView y la función invocada de inmediato ahora deberían verse de la siguiente manera:

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

Eso es todo lo que necesitas para implementar WebGLOverlayView. A continuación, configurarás todo lo que necesitas para renderizar un objeto 3D en el mapa con Three.js.

5. Configura una escena de Three.js

Utilizar WebGL puede resultar muy complejo porque requiere que definas todos los aspectos de cada objeto de forma manual y mucho más. Para facilitar este proceso, en este codelab utilizarás Three.js, una popular biblioteca de gráficos que proporciona una capa de abstracción simplificada sobre WebGL. Three.js incluye una amplia variedad de funciones útiles que permiten hacer de todo, desde crear un procesador de WebGL y dibujar formas comunes de objetos 2D y 3D hasta controlar las cámaras, las transformaciones de los objetos y mucho más.

Existen tres tipos de objetos básicos en Three.js necesarios para mostrar cualquier elemento:

  • Escena: Es un "contenedor" donde se renderizan y muestran todos los objetos, fuentes de luz, texturas, etcétera.
  • Cámara: Representa el punto de vista de la escena. Hay varios tipos de cámaras disponibles, y es posible agregar una o más a una sola escena.
  • Procesador: Controla el procesamiento y la visualización de todos los objetos de la escena. En Three.js, WebGLRenderer es el más usado, pero hay algunos otros disponibles como resguardo en caso de que el cliente no admita WebGL.

En este paso, cargarás todas las dependencias necesarias para Three.js y configurarás una escena básica.

  1. Carga Three.js.

    Necesitarás dos dependencias para este codelab: la biblioteca Three.js y el cargador glTF, una clase que te permite cargar objetos 3D en el formato GL Trasmission Format (glTF). Three.js ofrece cargadores especializados para diversos formatos de objetos 3D, pero se recomienda utilizar glTF.

    En el siguiente código, se importa toda la biblioteca Three.js. En una app de producción, es probable que desees importar solo las clases que necesitas, pero, para este codelab, importa toda la biblioteca a fin de simplificar el proceso. También ten presente que el cargador glTF no se incluye en la biblioteca predeterminada, sino que debe importarse desde una ruta de acceso aparte en la dependencia. Esta es la ruta donde puedes acceder a todos los cargadores proporcionados por Three.js.

    Para importar Three.js y el cargador glTF, agrega lo siguiente a la parte superior de app.js:
    import * as THREE from 'three';
    import {GLTFLoader} from 'three/examples/jsm/loaders/GLTFLoader.js';
    
  2. Crea una escena de Three.js.

    Para ello, agrega lo siguiente al hook onAdd a fin de crear una instancia de la clase Scene de Three.js:
    scene = new THREE.Scene();
    
  3. Agrega una cámara a la escena.

    Como se mencionó anteriormente, la cámara representa el punto de vista de la escena y determina la manera en la que Three.js controla la renderización visual de los objetos dentro de una escena. Si no hay una cámara, la escena no podrá visualizarse, ya que los objetos no se renderizarán y, por ende, no se mostrarán.

    Three.js ofrece diversas cámaras que afectan la manera en la que el procesador trata los objetos en relación con aspectos como la perspectiva y la profundidad. En esta escena, utilizarás la cámara de perspectiva (PerspectiveCamera), que es el tipo de cámara más usado en Three.js y que está diseñado para emular la forma en que el ojo humano percibiría la escena. Esto significa que los objetos más alejados de la cámara se verán más pequeños que los que estén más cerca, que la escena tendrá un punto de fuga y mucho más.

    Para incluir una cámara de perspectiva en la escena, agrega lo siguiente al hook onAdd:
    camera = new THREE.PerspectiveCamera();
    
    Con PerspectiveCamera, también puedes configurar los atributos que conforman el punto de vista, incluidos los planos de recorte cercano y lejano, la relación de aspecto y el campo de visión (fov). En conjunto, estos atributos conforman lo que se conoce como la pirámide truncada de visualización, un concepto importante que debe comprenderse cuando se trabaja en 3D, pero que está fuera del alcance de este codelab. La configuración predeterminada de PerspectiveCamera será suficiente.
  4. Agrega fuentes de luz a la escena.

    De forma predeterminada, los objetos renderizados en una escena de Three.js aparecerán negros, sin importar las texturas que se les apliquen. Esto se debe a que las escenas de Three.js imitan el comportamiento de los objetos en el mundo real, donde la visibilidad de los colores depende de la luz que se refleja sobre las superficies. En pocas palabras, si no hay luz, no hay color.

    Three.js proporciona diversos tipos de luz, de los cuales utilizarás dos:

  5. Luz ambiente (AmbientLight): Proporciona una fuente de luz difusa que ilumina de manera uniforme todos los objetos de la escena desde todos los ángulos. Esto dará a la escena una cantidad de luz de referencia para garantizar que se vean las texturas de todos los objetos.
  6. Luz direccional (DirectionalLight): Proporciona una luz que proviene de una dirección de la escena. A diferencia de lo que ocurriría en el mundo real con una luz colocada en una posición específica, los rayos de luz que emanan de DirectionalLight son todos paralelos y no se dispersan ni se difunden a medida que se alejan de la fuente de luz.

    Puedes configurar el color y la intensidad de cada luz para crear efectos de iluminación agregados. Por ejemplo, en el siguiente código, la luz ambiente da una luz blanca suave a toda la escena, mientras que la luz direccional proporciona una luz secundaria que ilumina los objetos en un ángulo descendente. En el caso de la luz direccional, el ángulo se establece con position.set(x, y ,z), donde cada valor es relativo al eje en cuestión. Por ejemplo, position.set(0,1,0) posicionaría la luz directamente sobre la escena en el eje Y apuntando hacia abajo.

    Para incluir las fuentes de luz en la escena, agrega lo siguiente al 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);
    

El hook onAdd ahora debería verse de la siguiente manera:

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

La escena ya está configurada y lista para renderizarse. A continuación, configurarás el procesador de WebGL y renderizarás la escena.

6. Renderiza la escena

Es hora de renderizar la escena. Hasta este punto, todo lo que creaste con Three.js se inicializó en código, pero básicamente no existe porque aún no se procesó en el contexto de renderización de WebGL. WebGL renderiza contenido 2D y 3D en el navegador mediante la API de Canvas. Si ya usaste la API de Canvas alguna vez, es probable que estés familiarizado con el contexto (context) de un lienzo HTML, que es donde se renderiza todo. Lo que quizás no sepas es que se trata de una interfaz que expone el contexto de renderización de gráficos de OpenGL mediante la API de WebGLRenderingContext en el navegador.

Para facilitar el uso del procesador de WebGL y poder renderizar las escenas en el navegador, Three.js ofrece el wrapper WebGLRenderer, que permite configurar el contexto de renderización de WebGL de manera relativamente sencilla. Sin embargo, en el caso de los mapas, no basta con renderizar la escena de Three.js en el navegador junto con el mapa en cuestión. Three.js se debe procesar exactamente en el mismo contexto de renderización que el mapa, de modo que el mapa y cualquier objeto de la escena de Three.js coincidan en el mismo espacio del mundo. Esto permite al procesador controlar las interacciones entre los objetos del mapa y los de la escena, como la oclusión, que es una forma sofisticada de decir que un objeto ocultará lo que haya detrás de él.

Suena bastante complicado, ¿verdad? Por suerte, Three.js viene a simplificar las cosas otra vez.

  1. Configura el procesador de WebGL.

    Al crear una nueva instancia del WebGLRenderer en Three.js, puedes proporcionarle el contexto de renderización de WebGL específico en el que deseas que procese tu escena. ¿Recuerdas el argumento gl que se pasa al hook onContextRestored? Ese objeto gl es el contexto de renderización de WebGL del mapa. Lo único que debes hacer es proporcionar el contexto, su lienzo y sus atributos a la instancia de WebGLRenderer. Toda esta información está disponible a través del objeto gl. En este código, la propiedad autoClear del procesador también se establece en false para que el procesador no borre su resultado en todos los fotogramas.

    Para configurar el procesador, agrega lo siguiente al hook onContextRestored:
    renderer = new THREE.WebGLRenderer({
      canvas: gl.canvas,
      context: gl,
      ...gl.getContextAttributes(),
    });
    renderer.autoClear = false;
    
  2. Renderiza la escena.

    Tras configurar el procesador, llama a requestRedraw en la instancia de WebGLOverlayView para indicarle a la superposición que se debe repetir el dibujo al renderizar el fotograma siguiente. Luego, llama a render en el procesador y pásale la escena y la cámara de Three.js para renderizarlas. Por último, borra el estado del contexto de renderización de WebGL. Este paso es importante para evitar conflictos relacionados con el estado de GL, ya que el uso de la vista de superposición de WebGL se basa en el estado de GL compartido. Si el estado de GL no se restablece al final de cada llamada de dibujo, es posible que los conflictos relacionados impidan el correcto funcionamiento del procesador.

    Para hacer esto, agrega lo siguiente al hook onDraw, de modo que se ejecute en cada fotograma:
    webGLOverlayView.requestRedraw();
    renderer.render(scene, camera);
    renderer.resetState();
    

Los hooks onContextRestored y onDraw ahora deberían verse de la siguiente manera:

    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. Renderiza un modelo 3D en el mapa

Muy bien, ya tienes todas las piezas listas: configuraste la vista de superposición de WebGL y creaste una escena de Three.js. Sin embargo, hay un problema: no hay nada en ella. Esto significa que llegó la hora de renderizar un objeto 3D en la escena. Para ello, utilizarás el cargador glTF que importaste antes.

Los modelos 3D están disponibles en diversos formatos, pero, en Three.js, se prefiere el formato glTF debido a su tamaño y su rendimiento en el entorno de ejecución. En este codelab, ya se te proporciona un modelo en /src/pin.gltf para que lo renderices en la escena.

  1. Crea una instancia de cargador de modelos.

    Agrega lo siguiente a onAdd:
    loader = new GLTFLoader();
    
  2. Carga un modelo 3D.

    Los cargadores de modelos son asíncronos y ejecutan una devolución de llamada cuando el modelo se carga por completo. Para cargar pin.gltf, agrega lo siguiente a onAdd:
    const source = "pin.gltf";
    loader.load(
      source,
      gltf => {}
    );
    
  3. Agrega el modelo a la escena.

    Ahora puedes agregar el modelo a la escena. Para ello, agrega lo siguiente a la devolución de llamada loader. Ten en cuenta que se agrega gltf.scene, no gltf:
    scene.add(gltf.scene);
    
  4. Configura la matriz de proyección de la cámara.

    Lo último que debes hacer para que el modelo se renderice correctamente en el mapa es configurar la matriz de proyección de la cámara en la escena de Three.js. La matriz de proyección se especifica como un array Matrix4 de Three.js, el cual define un punto en un espacio tridimensional junto con una serie de transformaciones, como rotaciones, cizallamientos, ajustes a escala y mucho más.

    En el caso de WebGLOverlayView, se utiliza la matriz de proyección para indicarle al procesador dónde y cómo renderizar la escena de Three.js en relación con el mapa base. Pero hay un problema. Las ubicaciones en el mapa se especifican como pares de coordenadas de latitud y longitud, mientras que las ubicaciones en la escena de Three.js son coordenadas de Vector3. Como has de suponer, calcular la conversión entre los dos sistemas no es nada trivial. Para hacerlo, WebGLOverlayView pasa un objeto coordinateTransformer al hook de ciclo de vida de OnDraw que contiene una función llamada fromLatLngAltitude. fromLatLngAltitude toma un objeto LatLngAltitude o LatLngAltitudeLiteral y, de manera opcional, un conjunto de argumentos que definen una transformación para la escena. Luego, los convierte automáticamente en una matriz de proyección de la vista del modelo (MVP). Lo único que debes hacer es especificar en qué lugar del mapa deseas que se renderice la escena de Three.js y cómo quieres que se transforme, y WebGLOverlayView se encargará del resto. Luego, puedes convertir la matriz de MVP en un array Matrix4 de Three.js y configurar la matriz de proyección de la cámara en consecuencia.

    En el siguiente código, el segundo argumento le indica a la vista de superposición de WebGL que establezca la altitud de la escena de Three.js en 120 metros sobre el suelo, lo que hará que el modelo parezca flotar.

    Para configurar la matriz de proyección de la cámara, agrega lo siguiente al 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. Transforma el modelo.

    Notarás que el pin no está perpendicular al mapa. En los gráficos 3D, además de los ejes X, Y y Z que determinan la orientación en el espacio del mundo, hay un conjunto de ejes aparte que determinan el espacio de cada objeto.

    Este modelo no se creó con lo que normalmente consideraríamos la "parte superior" del pin hacia arriba en relación con el eje Y, por lo que debes transformar el objeto para darle la orientación deseada en el espacio del mundo. Para ello, llama a rotation.set en dicho modelo. Ten en cuenta que, en Three.js, la rotación se especifica en radianes, no en grados. En general, es más fácil pensar en grados, por lo que debe realizarse la conversión adecuada con la fórmula degrees * Math.PI/180.

    Además, el modelo es un poco pequeño, por lo que también deberías agrandarlo a escala de manera uniforme respecto de todos los ejes. Para ello, llama a scale.set(x, y ,z).

    Para rotar el modelo y agrandarlo a escala, agrega lo siguiente en la devolución de llamada loader de onAdd antes de scene.add(gltf.scene), que agrega el glTF a la escena:
    gltf.scene.scale.set(25,25,25);
    gltf.scene.rotation.x = 180 * Math.PI/180;
    

Ahora, el pin se encuentra en posición vertical respecto del mapa.

Pin vertical

Los hooks onAdd y onDraw ahora deberían verse de la siguiente manera:

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

A continuación, veremos las animaciones de la cámara.

8. Anima la cámara

Ahora que renderizaste un modelo en el mapa y puedes mover todo de forma tridimensional, debes controlar ese movimiento de manera programática. La función moveCamera te permite establecer las propiedades de centro, zoom, inclinación y orientación del mapa de forma simultánea, lo que te brinda un control detallado de la experiencia del usuario. Además, se puede llamar a moveCamera en un bucle de animación para crear transiciones fluidas entre los fotogramas a una velocidad de casi 60 fotogramas por segundo.

  1. Espera a que se cargue el modelo.

    Para crear una experiencia del usuario sin inconvenientes, te recomendamos no mover la cámara hasta que el modelo glTF se haya cargado. Para ello, agrega el controlador de eventos onLoad del cargador al hook onContextRestored:
    loader.manager.onLoad = () => {}
    
  2. Crea un bucle de animación.

    Hay más de una forma de hacerlo, como usar setInterval o requestAnimationFrame. En este caso, utilizarás la función setAnimationLoop del procesador de Three.js, que llamará automáticamente a cualquier código que declares en su devolución de llamada cada vez que Three.js renderice un fotograma nuevo. Para crear el bucle de animación, agrega lo siguiente al controlador de eventos onLoad del paso anterior:
    renderer.setAnimationLoop(() => {});
    
  3. Establece la posición de la cámara en el bucle de animación.

    Luego, llama a moveCamera para actualizar el mapa. En este punto, las propiedades del objeto mapOptions que se utilizó para cargar el mapa se usan para definir la posición de la cámara:
    map.moveCamera({
      "tilt": mapOptions.tilt,
      "heading": mapOptions.heading,
      "zoom": mapOptions.zoom
    });
    
  4. Actualiza la cámara en cada fotograma.

    Este es el último paso. Actualiza el objeto mapOptions al final de cada fotograma a fin de configurar la posición de la cámara para el fotograma siguiente. En este código, se utiliza una sentencia if para aumentar la inclinación hasta alcanzar el valor máximo de 67.5. Luego, se cambia un poco la orientación en cada fotograma hasta que la cámara haya completado una rotación total de 360 grados. Una vez que se completa la animación deseada, se pasa null a setAnimationLoop para cancelar la animación, de modo que no se ejecute para siempre.
    if (mapOptions.tilt < 67.5) {
      mapOptions.tilt += 0.5
    } else if (mapOptions.heading <= 360) {
      mapOptions.heading += 0.2;
    } else {
      renderer.setAnimationLoop(null)
    }
    

El hook onContextRestored ahora debería verse de la siguiente manera:

    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. Felicitaciones

Si todo salió según lo previsto, ahora deberías tener un mapa con un gran pin 3D similar al siguiente:

Pin 3D final

Qué aprendiste

En este codelab, aprendiste varias cosas, entre las que se destacan las siguientes:

  • Cómo implementar WebGLOverlayView y sus hooks de ciclo de vida
  • Cómo integrar Three.js en el mapa
  • Cuáles son los conceptos básicos para crear una escena de Three.js, incluidas las cámaras y la iluminación
  • Cómo cargar y manipular modelos 3D con Three.js
  • Cómo controlar y animar la cámara del mapa con moveCamera

Qué sigue

WebGL (y los gráficos por computadora en general) es un tema complejo, por lo que siempre hay mucho que aprender. A continuación, se incluyen algunos recursos que pueden ser útiles para comenzar: