Criar experiências de mapas 3D com a visualização de sobreposição do WebGL

1. Antes de começar

Este codelab ensina a usar os recursos da API Maps JavaScript com a tecnologia WebGL para renderizar e controlar o mapa vetorial em três dimensões.

Alfinete 3D final

Pré-requisitos

Este codelab pressupõe que você tem conhecimento intermediário de JavaScript e da API Maps JavaScript. Para aprender os conceitos básicos da API Maps JS, consulte o codelab Adicionar um mapa ao seu site (JavaScript).

O que você aprenderá

  • Gerar um ID do mapa com o mapa vetorial para JavaScript ativado
  • Controlar o mapa com inclinação e rotação programáticas
  • Renderizar objetos 3D no mapa com WebGLOverlayView e three.js (em inglês)
  • Animar movimentos de câmera com moveCamera

Recursos necessários

  • Uma conta do Google Cloud Platform com o faturamento ativado
  • Uma chave de API da Plataforma Google Maps com a API Maps JavaScript ativada
  • Conhecimento intermediário de JavaScript, HTML e CSS
  • Um editor de texto ou ambiente de desenvolvimento integrado de sua escolha
  • Node.js (link em inglês)

2. Começar a configuração

Para a etapa abaixo, é necessário ativar a API Maps JavaScript.

Configurar a Plataforma Google Maps

Caso você ainda não tenha uma conta do Google Cloud Platform e um projeto com faturamento ativado, veja como criá-los no guia da Plataforma Google Maps.

  1. No Console do Cloud, clique no menu suspenso do projeto e selecione o projeto que você quer usar neste codelab.

  1. Ative as APIs e os SDKs da Plataforma Google Maps necessários para este codelab no Google Cloud Marketplace. Para fazer isso, siga as etapas descritas neste vídeo ou nesta documentação.
  2. Gere uma chave de API na página Credenciais do Console do Cloud. Siga as etapas indicadas neste vídeo ou nesta documentação. Todas as solicitações feitas à Plataforma Google Maps exigem uma chave de API.

Configuração do Node.js

Acesse https://nodejs.org/ (em inglês), faça o download do ambiente de execução do Node.js e instale no computador, caso ainda não tenha feito isso.

O Node.js vem com o gerenciador de pacotes npm, que é necessário para instalar as dependências deste codelab.

Fazer o download do modelo inicial do projeto

Antes de iniciar este codelab, siga as instruções abaixo para fazer o download do modelo inicial do projeto e do código completo da solução.

  1. Faça o download ou crie uma bifurcação do repositório GitHub deste codelab em https://github.com/googlecodelabs/maps-platform-101-webgl/ (em inglês). O projeto inicial está localizado no diretório /starter e inclui a estrutura de arquivos básica necessária para concluir o codelab. Tudo o que você precisa para trabalhar está no diretório /starter/src.
  2. Depois de fazer o download do projeto inicial, execute npm install no diretório /starter. Isso instala todas as dependências necessárias listadas em package.json.
  3. Depois de instalar as dependências, execute npm start no diretório.

O projeto inicial foi configurado para que você possa usar o webpack-dev-server, que compila e executa o código escrito localmente. O webpack-dev-server também recarrega automaticamente o app no navegador sempre que você faz alterações no código.

Se você quiser ver o código completo da solução em execução, conclua as etapas de configuração acima no diretório /solution.

Adicionar sua chave de API

O app inicial inclui todo o código necessário para carregar o mapa com o JS API Loader (em inglês). Assim, basta informar a chave de API e o ID do mapa. O JS API Loader é uma biblioteca simples que abstrai o método tradicional de carregamento da API Maps JS in-line no modelo HTML com uma tag script. Assim, você pode processar tudo no código JavaScript.

Para adicionar a chave de API, faça o seguinte no projeto inicial:

  1. Abra app.js.
  2. No objeto apiOptions, defina sua chave de API como o valor de apiOptions.apiKey.

3. Gerar e usar um ID do mapa

Para usar os recursos baseados em WebGL da API Maps JavaScript, você precisa de um ID do mapa com o mapa vetorial ativado.

Como gerar um ID do mapa

Geração de IDs do mapa

  1. No Console do Google Cloud, acesse "Plataforma Google Maps" > "Gerenciamento de mapas".
  2. Clique em "CREATE MAP ID".
  3. No campo "Nome", insira um nome para este ID do mapa.
  4. Na lista suspensa "Tipo de mapa", selecione "JavaScript". As "Opções de JavaScript" serão exibidas.
  5. Selecione a opção "Vetor" e as caixas de seleção "Inclinação" e "Rotação".
  6. Opcional: no campo "Descrição", insira uma descrição para a chave de API.
  7. Clique no botão "Salvar". A página "Detalhes do ID do mapa" é exibida.

    Página "Detalhes do ID do mapa"
  8. Copie o ID do mapa. Você precisará dele para carregar o mapa na próxima etapa.

Como usar um ID do mapa

Para carregar o mapa vetorial, é necessário inserir um ID do mapa como uma propriedade nas opções ao instanciar o mapa. Você também pode inserir o mesmo ID do mapa ao carregar a API Maps JavaScript.

Para carregar o mapa com seu ID, faça o seguinte:

  1. Defina seu ID do mapa como o valor de mapOptions.mapId.

    Inserir o ID ao instanciar o mapa informa à Plataforma Google Maps quais dos seus mapas devem ser carregados em uma determinada instância. Você pode reutilizar o mesmo ID do mapa em vários apps ou várias visualizações no mesmo app.
    const mapOptions = {
      "tilt": 0,
      "heading": 0,
      "zoom": 18,
      "center": { lat: 35.6594945, lng: 139.6999859 },
      "mapId": "YOUR_MAP_ID"
    };
    

Verifique o app em execução no seu navegador. O mapa vetorial com inclinação e rotação ativados deve carregar sem problemas. Para verificar se a inclinação e a rotação estão ativadas, mantenha a tecla Shift pressionada e arraste com o mouse ou use as setas do teclado.

Se o mapa não carregar, verifique se você inseriu uma chave de API válida em apiOptions. Se o mapa não inclinar ou rotacionar, verifique se você inseriu um ID do mapa com inclinação e rotação ativadas em apiOptions e mapOptions.

Mapa inclinado

Seu arquivo app.js ficou assim:

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

O WebGLOverlayView oferece acesso direto ao mesmo contexto de renderização WebGL usado para renderizar o mapa básico vetorial. Isso significa que você pode renderizar objetos 2D e 3D diretamente no mapa usando WebGL, bem como bibliotecas de gráficos baseadas em WebGL.

O WebGLOverlayView expõe cinco hooks que você pode usar no ciclo de vida do contexto de renderização WebGL do mapa. Veja uma breve descrição de cada hook e como eles podem ser usados:

  • onAdd(): chamado quando a sobreposição é adicionada a um mapa. Para isso, chame setMap em uma instância WebGLOverlayView. É aqui que você deve fazer qualquer trabalho em WebGL que não exija acesso direto ao contexto WebGL.
  • onContextRestored(): chamado quando o contexto WebGL fica disponível, mas antes da renderização de qualquer elemento. É aqui que você precisa inicializar objetos, vincular o estado por binding e fazer qualquer outra coisa que precise de acesso ao contexto WebGL, mas que possa ser realizada fora da chamada onDraw(). Isso permite que você configure tudo o que precisa sem sobrecarregar a renderização do mapa, que já usa muita GPU.
  • onDraw(): chamado uma vez por frame quando o WebGL começa a renderizar o mapa e o que mais você tiver solicitado. Faça o mínimo de alterações possível no onDraw() para evitar problemas de desempenho na renderização do mapa.
  • onContextLost(): chamado quando o contexto de renderização WebGL é perdido por qualquer motivo.
  • onRemove(): chamado quando a sobreposição é removida do mapa. Para isso, chame setMap(null) em uma instância WebGLOverlayView.

Nesta etapa, você criará uma instância de WebGLOverlayView e implementará três hooks de ciclo de vida: onAdd, onContextRestored e onDraw. Para manter tudo limpo e fácil de acompanhar, todo o código da sobreposição será processado na função initWebGLOverlayView() fornecida no modelo inicial deste codelab.

  1. Crie uma instância WebGLOverlayView().

    A sobreposição é fornecida pela API Maps JS no google.maps.WebGLOverlayView. Para começar, crie uma instância anexando o código a seguir a initWebGLOverlayView():
    const webGLOverlayView = new google.maps.WebGLOverlayView();
    
  2. Implemente hooks de ciclo de vida.

    Para implementar os hooks de ciclo de vida, anexe o seguinte a initWebGLOverlayView():
    webGLOverlayView.onAdd = () => {};
    webGLOverlayView.onContextRestored = ({gl}) => {};
    webGLOverlayView.onDraw = ({gl, coordinateTransformer}) => {};
    
  3. Adicione a instância de sobreposição ao mapa.

    Agora, chame setMap() na instância de sobreposição e transmita o mapa anexando o seguinte a initWebGLOverlayView():
    webGLOverlayView.setMap(map)
    
  4. Chame initWebGLOverlayView.

    A última etapa é executar initWebGLOverlayView() adicionando o seguinte à função invocada imediatamente na parte de baixo do app.js:
    initWebGLOverlayView(map);
    

O initWebGLOverlayView e a função imediatamente invocada vão ficar assim:

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

Isso é tudo o que você precisa para implementar o WebGLOverlayView. Agora você vai configurar todo o necessário para renderizar um objeto 3D no mapa usando o three.js.

5. Configurar uma cena three.js

Usar o WebGL pode ser muito complicado, porque exige que você defina todos os aspectos de cada objeto manualmente. Para facilitar muito as coisas, para este codelab, você usará o three.js, uma biblioteca de gráficos bastante utilizada que insere uma camada de abstração simplificada por cima do WebGL. O three.js vem com uma ampla variedade de funções de conveniência que fazem de tudo, desde a criação de um renderizador WebGL até o desenho de formas comuns de objetos 2D e 3D, o controle de câmeras, transformações de objeto e muito mais.

Existem três tipos básicos de objeto no three.js necessários para exibir qualquer coisa:

  • Cena: um "contêiner" em que todos os objetos, fontes de luz, texturas etc. são renderizados e exibidos.
  • Câmera: representa o ponto de vista da cena. Vários tipos de câmera estão disponíveis, e uma ou mais câmeras podem ser adicionadas a uma única cena.
  • Renderizador: faz o processamento e a exibição de todos os objetos na cena. No three.js, WebGLRenderer é o renderizador mais usado, mas alguns outros estão disponíveis como contingência caso o cliente não seja compatível com o WebGL.

Nesta etapa, você carregará todas as dependências necessárias para o three.js e criará uma cena básica.

  1. Carregue o three.js.

    Você precisará de duas dependências para este codelab: a biblioteca three.js e o glTF Loader, uma classe que permite carregar objetos 3D no Formato de transferência GL, ou glTF (em inglês). O three.js oferece carregadores especializados para muitos formatos de objetos 3D diferentes, mas o uso do glTF é recomendado.

    No código abaixo, toda a biblioteca three.js é importada. Em um app de produção, é recomendável importar apenas as classes necessárias, mas, para este codelab, importe toda a biblioteca para simplificar. Observe também que o glTF Loader não está incluído na biblioteca padrão e precisa ser importado de um caminho separado na dependência. Esse é o caminho em que você pode acessar todos os carregadores fornecidos pelo three.js.

    Para importar o three.js e o glTF Loader, adicione o seguinte no início de app.js:
    import * as THREE from 'three';
    import {GLTFLoader} from 'three/examples/jsm/loaders/GLTFLoader.js';
    
  2. Crie uma cena three.js.

    Para criar uma cena, instancie a classe Scene do three.js anexando o seguinte ao hook onAdd:
    scene = new THREE.Scene();
    
  3. Adicione uma câmera à cena.

    Como mencionado anteriormente, a câmera representa a perspectiva da visualização da cena e determina como o three.js faz a renderização visual de objetos em uma cena. Sem uma câmera, a cena não é "vista", o que significa que os objetos não serão exibidos porque não serão renderizados.

    O three.js oferece várias câmeras diferentes que afetam o modo como o renderizador trata os objetos em relação a aspectos como perspectiva e profundidade. Na cena, você usará a PerspectiveCamera, o tipo de câmera mais usado no three.js, projetado para emular a forma como o olho humano perceberia a cena. Isso significa que os objetos mais distantes da câmera serão menores do que os que estão mais próximos, a cena terá um ponto de fuga, entre outros.

    Para adicionar uma câmera de perspectiva à cena, anexe o seguinte ao hook onAdd:
    camera = new THREE.PerspectiveCamera();
    
    Com a PerspectiveCamera, também é possível configurar os atributos que compõem o ponto de vista, incluindo as áreas próximas e distantes, a proporção e o campo de visão (FOV, na sigla em inglês). Coletivamente, esses atributos compõem o que é conhecido como frustum de visualização (link em inglês), um conceito que é importante compreender quando se trabalha em 3D, mas que não faz parte do escopo deste codelab. A configuração padrão de PerspectiveCamera já é suficiente.
  4. Adicione fontes de luz à cena.

    Por padrão, os objetos renderizados em uma cena three.js aparecerão pretos, independentemente das texturas aplicadas. Isso ocorre porque uma cena three.js imita como os objetos agem no mundo real, onde a visibilidade da cor depende da luz refletida por um objeto. Em resumo: sem luz, sem cor.

    O three.js oferece vários tipos diferentes de luz. Você usará dois:

  5. AmbientLight: aplica uma fonte de luz difusa que ilumina de maneira uniforme todos os objetos na cena, de todos os ângulos. Isso dará à cena uma quantidade de luz referencial para garantir que as texturas em todos os objetos fiquem visíveis.
  6. DirectionalLight: aplica uma luz vinda de uma direção específica na cena. Ao contrário de como uma luz posicionada funcionaria no mundo real, os raios de luz emitidos por DirectionalLight são todos paralelos e não se espalham nem difundem conforme se afastam da fonte de luz.

    Você pode configurar a cor e a intensidade de cada luz para agregar efeitos de iluminação. Por exemplo, no código abaixo, a luz ambiente emite uma tonalidade branca suave para toda a cena, enquanto a luz direcional é secundária e atinge objetos em um ângulo para baixo. No caso da luz direcional, o ângulo é definido usando position.set(x, y ,z), em que cada valor é relativo ao respectivo eixo. Por exemplo, position.set(0,1,0) colocaria a luz diretamente acima da cena no eixo y apontando para baixo.

    Para adicionar fontes de luz à cena, anexe o seguinte ao 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);
    

Seu hook onAdd ficou assim:

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

Sua cena está configurada e pronta para renderização. Em seguida, você vai configurar o renderizador WebGL e renderizar a cena.

6. Renderizar a cena

É hora de renderizar a cena. Até agora, tudo que você criou com o three.js está inicializado no código, mas essencialmente não existe porque ainda não foi renderizado no contexto de renderização WebGL. O WebGL renderiza o conteúdo 2D e 3D no navegador usando a API Canvas. Se você já usou a API Canvas, provavelmente conhece o context de uma tela HTML, que é onde tudo é renderizado. O que talvez você não saiba é que essa é uma interface que expõe o contexto de renderização dos gráficos OpenGL por meio da API WebGLRenderingContext no navegador.

Para facilitar o gerenciamento do renderizador do WebGL, o three.js oferece o WebGLRenderer (em inglês), um wrapper que facilita a configuração do contexto de renderização WebGL para que o three.js possa renderizar cenas no navegador. No entanto, no caso do mapa, não basta renderizar a cena three.js no navegador ao lado do mapa. O three.js precisa ser renderizado exatamente no mesmo contexto de renderização do mapa, de modo que o mapa e todos os objetos da cena three.js sejam renderizados no mesmo espaço mundial. Isso possibilita que o renderizador processe as interações entre os objetos no mapa e na cena, como a oclusão, que é uma maneira sofisticada de dizer que um objeto ocultará elementos atrás dele.

Parece complicado, não parece? Felizmente, o three.js vem de novo ao resgate.

  1. Configure o renderizador WebGL.

    Quando cria uma nova instância do WebGLRenderer do three.js, você pode dar a ela o contexto de renderização WebGL específico em que você quer renderizar a cena. Lembra do argumento gl que é transmitido para o hook onContextRestored? Esse objeto gl é o contexto de renderização WebGL do mapa. Tudo o que você precisa fazer é inserir o contexto, a tela e os atributos na instância WebGLRenderer, todos disponíveis pelo objeto gl. Nesse código, a propriedade autoClear do renderizador também é definida como false para que ele não apague a saída a cada frame.

    Para configurar o renderizador, anexe o seguinte ao hook onContextRestored:
    renderer = new THREE.WebGLRenderer({
      canvas: gl.canvas,
      context: gl,
      ...gl.getContextAttributes(),
    });
    renderer.autoClear = false;
    
  2. Renderize a cena.

    Depois de configurar o renderizador, chame requestRedraw na instância WebGLOverlayView para informar à sobreposição que é necessário redesenhar quando o próximo frame for renderizado. Em seguida, chame render no renderizador. e transmita a cena e a câmera do three.js para renderizar. Por fim, limpe o estado do contexto de renderização WebGL. Essa é uma etapa importante para evitar conflitos de estado GL, já que o uso da visualização de sobreposição do WebGL depende do estado GL compartilhado. Se o estado não for redefinido ao final de cada chamada de desenho, os conflitos de estado GL poderão causar uma falha no renderizador.

    Para fazer isso, anexe o seguinte ao hook onDraw para que ele seja executado em cada frame:
    webGLOverlayView.requestRedraw();
    renderer.render(scene, camera);
    renderer.resetState();
    

Os hooks onContextRestored e onDraw ficarão assim:

    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. Renderizar um modelo 3D no mapa

Você já colocou todas as peças no lugar. Você configurou a visualização de sobreposição do WebGL e criou uma cena three.js, mas há um problema: não há nada nela. Agora é hora de renderizar um objeto 3D na cena. Para isso, vamos usar o glTF Loader que você importou em uma etapa anterior.

Os modelos 3D têm vários formatos diferentes. No entanto, para o three.js, o formato glTF é o preferido devido ao tamanho e ao desempenho do ambiente de execução. Neste codelab, um modelo para você renderizar na cena já está disponível em /src/pin.gltf.

  1. Crie uma instância de carregador de modelo.

    Anexe o seguinte a onAdd:
    loader = new GLTFLoader();
    
  2. Carregue um modelo 3D.

    Os carregadores de modelo são assíncronos e executam um callback quando o modelo é totalmente carregado. Para carregar pin.gltf, anexe o seguinte a onAdd:
    const source = "pin.gltf";
    loader.load(
      source,
      gltf => {}
    );
    
  3. Adicione o modelo à cena.

    Agora, é possível adicionar o modelo à cena anexando o seguinte ao callback loader. Observe que o que está sendo adicionado é gltf.scene, e não gltf:
    scene.add(gltf.scene);
    
  4. Configure a matriz de projeção da câmera.

    A última coisa que você precisa para renderizar corretamente o modelo no mapa é definir a matriz de projeção da câmera na cena three.js. A matriz de projeção é especificada como uma matriz Matrix4 do three.js, que define um ponto em um espaço tridimensional com transformações, como rotações, distorção, escala e muito mais.

    No caso do WebGLOverlayView, a matriz de projeção é usada para informar ao renderizador onde e como renderizar a cena three.js em relação ao mapa básico. Mas há um problema. Os locais no mapa são especificados como pares de coordenadas de latitude e longitude, enquanto os locais na cena three.js são coordenadas Vector3. Como você pode ter imaginado, o cálculo da conversão entre os dois sistemas não é simples. Para resolver isso, o WebGLOverlayView transmite um objeto coordinateTransformer para o hook de ciclo de vida OnDraw que contém uma função chamada fromLatLngAltitude. O fromLatLngAltitude usa um objeto LatLngAltitude ou LatLngAltitudeLiteral e, opcionalmente, um conjunto de argumentos que definem uma transformação para a cena e a convertem em uma matriz de projeção de visualização do modelo (MVP, na sigla em inglês) para você. Tudo o que você precisa fazer é especificar no mapa onde quer que a cena three.js seja renderizada, além de como quer que ela seja transformada, e o WebGLOverlayView fará o resto. Depois, é possível converter a matriz de MVP em uma matriz Matrix4 do three.js e definir a matriz de projeção da câmera nela.

    No código abaixo, o segundo argumento diz à visualização de sobreposição do WebGL para definir a altitude da cena three.js em 120 metros acima do solo, o que fará com que o modelo pareça flutuar.

    Para definir a matriz de projeção da câmera, anexe o seguinte ao 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. Transforme o modelo.

    Você vai perceber que o alfinete não está perpendicular ao mapa. Nos gráficos 3D, além do espaço mundial ter os próprios eixos x, y e z que determinam a orientação, cada objeto também tem o próprio espaço, com um conjunto independente de eixos.

    No caso desse modelo, ele não foi criado com o que normalmente consideramos a "parte superior" do alfinete voltada para cima no eixo y. Portanto, é necessário transformar o objeto para orientá-lo na direção desejada relativa ao espaço mundial, chamando rotation.set nele. No three.js, a rotação é especificada em radianos, não em graus. Geralmente, é mais fácil pensar em graus. Por isso, a conversão apropriada precisa ser feita usando a fórmula degrees * Math.PI/180.

    Além disso, o modelo é pequeno, então você também irá escalonar de maneira uniforme em todos os eixos dele chamando scale.set(x, y ,z).

    Para girar e dimensionar o modelo, adicione o seguinte no callback loader de onAdd antes de scene.add(gltf.scene), que adiciona o glTF à cena:
    gltf.scene.scale.set(25,25,25);
    gltf.scene.rotation.x = 180 * Math.PI/180;
    

Agora o alfinete está na posição vertical em relação ao mapa.

Alfinete vertical

Os hooks onAdd e onDraw ficarão assim:

    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 seguir, veremos as animações da câmera.

8. Animar a câmera

Agora que você renderizou um modelo no mapa e consegue mover tudo nas três dimensões, o próximo passo será controlar esse movimento de forma programática. A função moveCamera permite definir as propriedades de centro, zoom, inclinação e direção do mapa simultaneamente, dando a você um controle preciso da experiência do usuário. Além disso, o moveCamera pode ser chamado em um loop de animação para criar transições fluidas a quase 60 frames por segundo.

  1. Aguarde o carregamento do modelo.

    Para criar uma experiência perfeita para o usuário, aguarde para começar a mover a câmera até que o modelo glTF seja carregado. Para fazer isso, anexe o manipulador de eventos onLoad do carregador ao hook onContextRestored:
    loader.manager.onLoad = () => {}
    
  2. Crie um loop de animação.

    Há mais de uma maneira de criar um loop de animação, como usar setInterval ou requestAnimationFrame. Neste caso, você usará a função setAnimationLoop do renderizador three.js, que chamará automaticamente qualquer código declarado no callback sempre que o three.js renderizar um novo frame. Para criar o loop de animação, adicione o seguinte ao manipulador de eventos onLoad da etapa anterior:
    renderer.setAnimationLoop(() => {});
    
  3. Defina a posição da câmera no loop de animação.

    Em seguida, chame moveCamera para atualizar o mapa. Aqui, as propriedades do objeto mapOptions, usado para carregar o mapa, irão definir a posição da câmera:
    map.moveCamera({
      "tilt": mapOptions.tilt,
      "heading": mapOptions.heading,
      "zoom": mapOptions.zoom
    });
    
  4. Atualize a câmera a cada frame.

    Última etapa! Atualize o objeto mapOptions no final de cada frame para definir a posição da câmera para o próximo frame. Neste código, uma instrução if é usada para aumentar a inclinação até que o valor máximo de 67,5 seja atingido. A direção é alterada um pouco a cada frame até a câmera concluir a rotação de 360 graus. Quando a animação desejada for concluída, null será transmitido para setAnimationLoop a fim de cancelar a animação para que ela não seja exibida para sempre.
    if (mapOptions.tilt < 67.5) {
      mapOptions.tilt += 0.5
    } else if (mapOptions.heading <= 360) {
      mapOptions.heading += 0.2;
    } else {
      renderer.setAnimationLoop(null)
    }
    

Seu hook onContextRestored ficou assim:

    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. Parabéns!

Se tudo correu de acordo com o plano, você terá um mapa com um grande alfinete 3D que tem esta aparência:

Alfinete 3D final

O que você aprendeu

Neste codelab, você aprendeu várias coisas. Veja os destaques:

  • Implementar WebGLOverlayView e seus hooks de ciclo de vida
  • Integrar o three.js ao mapa
  • Criar uma cena básica do three.js, incluindo câmeras e iluminação
  • Carregar e manipular modelos 3D usando o three.js
  • Controlar e animar a câmera para o mapa usando moveCamera

Qual é a próxima etapa?

O WebGL e os gráficos de computador em geral são assuntos complexos, por isso sempre há muito a aprender. Para começar, veja abaixo alguns recursos importantes: