Globo 3D do Making of the World Wonders

Ilmari Heikkinen

Introdução ao globo 3D das maravilhas do mundo

Se você acessou o site Google World Wonders lançado recentemente em um navegador compatível com WebGL, talvez tenha notado um globo giratório na parte de baixo da tela. Neste artigo, explicamos como o globo funciona e o que usamos para criá-lo.

Para ter uma visão geral rápida, o globo do World Wonders é uma versão muito modificada do globo do WebGL pela equipe de artes de dados do Google. Pegamos o globo original, removemos os bits do gráfico de barras, mudamos os shaders, adicionamos marcadores HTML clicáveis e a geometria do continente Natural Earth da demonstração do GlobeTweeter da Mozilla (um grande agradecimento a Cedric Pinson!). Tudo para criar um globo animado que combina com o esquema de cores do site e adiciona um toque extra de sofisticação.

O objetivo do briefing de design do globo era ter um mapa animado bonito com marcadores clicáveis colocados em cima dos Patrimônios Mundiais da UNESCO. Com isso em mente, comecei a procurar algo adequado. A primeira coisa que me veio à mente foi o globo WebGL criado pela equipe de artes de dados do Google. É um globo e parece legal. O que mais você precisa?

Como configurar o globo da WebGL

A primeira etapa para criar o widget de globo foi fazer o download do globo WebGL e colocá-lo em funcionamento. O globo WebGL está on-line no Google Code e é fácil de fazer o download e executar. Faça o download e extraia o arquivo zip, entre nele e execute um servidor da Web básico: python -m SimpleHTTPServer. Ele não tem o UTF-8 ativado por padrão, mas você pode usar esse recurso. Agora, se você navegar até http://localhost:8000/globe/globe.html, o globo do WebGL vai aparecer.

Com o globo WebGL em funcionamento, era hora de cortar todas as partes desnecessárias. Editei o HTML para remover os bits da interface e removi a configuração do gráfico de barras do globo da função de inicialização do globo. No final desse processo, eu tinha um globo WebGL muito básico na tela. Você pode girar e ele fica legal, mas é só isso.

Para cortar o que não era necessário, excluí todos os elementos da interface do index.html do globo e editei o script de inicialização para ficar assim:

if(!Detector.webgl){
  Detector.addGetWebGLMessage();
} else {
  var container = document.getElementById('container');
  var globe = new DAT.Globe(container);
  globe.animate();
}

Como adicionar a geometria do continente

Queríamos que a câmera ficasse próxima à superfície do globo, mas, quando testamos o globo com zoom, a falta de resolução da textura ficou aparente. Com o zoom, a textura do globo do WebGL fica em blocos e desfocada. Poderíamos usar uma imagem maior, mas isso tornaria o globo mais lento para fazer o download e executar, então optamos por uma representação vetorial das massas de terra e fronteiras.

Para a geometria da massa de terra, usei a demonstração de código aberto GlobeTweeter e carreguei o modelo 3D para o Three.js. Com o modelo carregado e renderizado, era hora de começar a refinar a aparência do globo. O primeiro problema era que o modelo de massa de terra do globo não era esférico o suficiente para ficar nivelado com o globo do WebGL. Então, acabei escrevendo um algoritmo rápido de divisão de malha que tornou o modelo de massa de terra mais esférico.

Com um modelo de massa de terra esférica, consegui posicioná-lo um pouco afastado da superfície do globo, criando continentes flutuantes delineados com uma linha preta de 2 px abaixo deles para uma espécie de sombra. Também experimentei contornos de cores neon para criar uma espécie de estilo Tron.

Com a renderização do globo e das massas de terra, comecei a experimentar diferentes visuais para o globo. Como queríamos um visual monocromático, usei um globo em tons de cinza e massas de terra. Além dos contornos de néon mencionados, tentei um globo escuro com massas de terra escuras em um fundo claro, o que ficou bem legal. Mas o contraste era muito baixo para ser facilmente legível e não se encaixava no projeto, então eu descartei isso.

Outra ideia que tive para o globo foi fazer com que ele parecesse porcelana esmaltada. Não consegui testar esse, porque não consegui escrever um sombreador para fazer o efeito de porcelana (seria bom ter um editor de recursos visuais). A coisa mais próxima que tentei foi um globo branco brilhante com massas de terra pretas. É legal, mas tem muito contraste. E não parece muito legal. Mais um para o lixo.

Os shaders nos globos pretos e brancos estão usando uma espécie de iluminação de fundo difusa falsa. A luminosidade do globo depende da distância da superfície normal ao plano da tela. Assim, os pixels no meio do globo que apontam para a tela ficam escuros, e os pixels nas bordas do globo ficam claros. Combinado com um plano de fundo claro, você tem uma aparência em que o globo está refletindo o plano de fundo brilhante, criando um visual de showroom elegante. O globo negro também usa a textura do globo WebGL como um mapa de brilho, para que as plataformas continentais (áreas de água rasa) pareçam brilhantes em comparação com as outras partes do globo.

Confira como o sombreador do oceano para o globo negro fica. Um sombreador de vértice muito básico e um sombreador de fragmentos hackeado "oh, isso parece legal tweak tweak".

    'ocean' : {
      uniforms: {
        'texture': { type: 't', value: 0, texture: null }
      },
      vertexShader: [
        'varying vec3 vNormal;',
        'varying vec2 vUv;',
        'void main() {',
          'gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );',
          'vNormal = normalize( normalMatrix * normal );',
          'vUv = uv;',
        '}'
      ].join('\n'),
      fragmentShader: [
        'uniform sampler2D texture;',
        'varying vec3 vNormal;',
        'varying vec2 vUv;',
        'void main() {',
          'vec3 diffuse = texture2D( texture, vUv ).xyz;',
          'float intensity = pow(1.05 - dot( vNormal, vec3( 0.0, 0.0, 1.0 ) ), 4.0);',
          'float i = 0.8-pow(clamp(dot( vNormal, vec3( 0, 0, 1.0 )), 0.0, 1.0), 1.5);',
          'vec3 atmosphere = vec3( 1.0, 1.0, 1.0 ) * intensity;',
          'float d = clamp(pow(max(0.0,(diffuse.r-0.062)*10.0), 2.0)*5.0, 0.0, 1.0);',
          'gl_FragColor = vec4( (d*vec3(i)) + ((1.0-d)*diffuse) + atmosphere, 1.0 );',
        '}'
      ].join('\n')
    }

No final, optamos por um globo escuro com massas de terra cinza-claro iluminadas de cima. Ele estava mais próximo do briefing de design e parecia bonito e legível. Além disso, ter um contraste um pouco baixo no globo faz com que os marcadores e o restante do conteúdo se destaquem mais na comparação. A versão abaixo usa oceanos totalmente pretos, enquanto a versão de produção tem oceanos cinza-escuro e marcadores ligeiramente diferentes.

Criar marcadores com CSS

Falando em marcadores, com o globo e as massas de terra funcionando, comecei a trabalhar nos marcadores de lugar. Eu decidi usar elementos HTML no estilo CSS para os marcadores, para facilitar a criação e o estilo deles e, possivelmente, reutilizar os marcadores no mapa 2D em que a equipe estava trabalhando. Na época, eu também não sabia como tornar os marcadores WebGL clicáveis e não queria escrever código extra para carregar / criar os modelos de marcadores. Os marcadores CSS funcionaram bem, mas ocasionalmente tinham problemas de desempenho quando os compositores e renderizadores do navegador estavam em períodos de mudança. Do ponto de vista do desempenho, usar marcadores no WebGL seria uma opção melhor. Por outro lado, os marcadores CSS economizaram muito tempo de desenvolvimento.

Os marcadores CSS consistem em alguns divs posicionados de forma absoluta com a propriedade de transformação do CSS. O plano de fundo dos marcadores é um gradiente CSS, e a parte triangular do marcador é um div girado. Os marcadores têm uma pequena sombra para se destacarem do plano de fundo. O maior problema com os marcadores era fazer com que eles funcionassem bem. Por mais triste que possa parecer, desenhar algumas dezenas de divs que se movem e mudam o índice z em cada frame é uma ótima maneira de acionar todos os tipos de armadilhas de renderização do navegador.

A forma como os marcadores são sincronizados com a cena 3D não é muito complicada. Cada marcador tem um Object3D correspondente na cena do Three.js, que é usado para rastrear os marcadores. Para conseguir as coordenadas do espaço da tela, uso as matrizes do Three.js para o globo e o marcador e multiplico um vetor zero com elas. Com isso, consigo a posição da cena do marcador. Para saber a posição do marcador na tela, projeto a posição da cena pela câmera. O vetor projetado resultante tem as coordenadas do espaço da tela para o marcador, pronto para uso no CSS.

var mat = new THREE.Matrix4();
var v = new THREE.Vector3();

for (var i=0; i<locations.length; i++) {
  mat.copy(scene.matrix);
  mat.multiplySelf(locations[i].point.matrix);
  v.set(0,0,0);
  mat.multiplyVector3(v);
  projector.projectVector(v, camera);
  var x = w * (v.x + 1) / 2; // Screen coords are between -1 .. 1, so we transform them to pixels.
  var y = h - h * (v.y + 1) / 2; // The y coordinate is flipped in WebGL.
  var z = v.z;
}

No final, a abordagem mais rápida foi usar transformações CSS para mover os marcadores, não usar o desbotamento de opacidade, já que ele aciona um caminho lento no Firefox e mantém todos os marcadores no DOM, sem removê-los quando eles ficam atrás do globo. Também testamos o uso de transformações 3D em vez de índices Z, mas, por algum motivo, elas não funcionaram corretamente no app (mas funcionaram em um caso de teste reduzido, imagine só). Como faltavam alguns dias para o lançamento, tivemos que deixar essa parte para a manutenção pós-lançamento.

Quando você clica em um marcador, ele se expande para uma lista de nomes de lugares clicáveis. Isso é tudo normal no DOM do HTML, então foi muito fácil escrever. Todos os links e renderizações de texto funcionam sem nenhum trabalho extra da nossa parte.

Reduzir o tamanho do arquivo

Com a demonstração funcionando e conectada ao resto do site do World Wonders, ainda havia um grande problema a ser resolvido. A malha no formato JSON para as massas terrestres do globo tinha cerca de 3 MB. Não é bom para a página inicial de um site de apresentação. A boa notícia é que a compactação da malha com gzip reduziu o tamanho para 350 KB. Mas 350 kB ainda é um pouco grande. Depois de alguns e-mails, conseguimos recrutar Won Chun, que trabalhou na compactação das grandes malhas do Google Body, para nos ajudar. Ele comprimiu a malha de uma grande lista plana de triângulos fornecida como coordenadas JSON para coordenadas de 11 bits compactadas com triângulos indexados e reduziu o tamanho do arquivo para 95 kB compactado com GZIP.

O uso de malhas compactadas não apenas economiza largura de banda, mas também torna a análise mais rápida. Transformar 3 MB de números convertidos em string em números nativos exige muito mais trabalho do que analisar 100 kB de dados binários. A redução de 250 kB no tamanho da página é muito útil, além de reduzir o tempo de carregamento inicial para menos de um segundo em uma conexão de 2 Mbps. Mais rápido e menor, incrível!

Ao mesmo tempo, eu estava testando o carregamento dos Shapefiles originais do Natural Earth, de onde a malha do GlobeTweeter é derivada. Consegui carregar os shapefiles, mas renderizá-los como massas de terra planas requer triangulação (com furos para lagos, claro). Consegui triangular as formas usando os utilitários do THREE.js, mas não os buracos. As malhas resultantes tinham bordas muito longas, o que exigia a divisão da malha em tris menores. Resumindo, não consegui fazer com que ele funcionasse a tempo, mas o legal é que o formato de shapefile compactado teria gerado um modelo de terra de 8 kB. Talvez na próxima.

Trabalhos futuros

Uma coisa que poderia ser melhorada é a qualidade das animações dos marcadores. Agora, quando eles passam pelo horizonte, o efeito é um pouco pegajoso. Além disso, seria bom ter uma animação legal para a abertura do marcador.

Em termos de desempenho, as duas coisas que faltam são otimizar o algoritmo de divisão de malhas e tornar os marcadores mais rápidos. Fora isso, tudo bem. Viva!

Resumo

Neste artigo, descrevi como criamos o globo 3D para o projeto das Maravilhas do Mundo do Google. Espero que você tenha gostado dos exemplos e tente criar seu próprio widget de globo personalizado.

Referências