Introducción al globo terráqueo en 3D de las Maravillas del Mundo
Si viste el sitio Google World Wonders lanzado recientemente en un navegador compatible con WebGL, es posible que hayas visto un globo terráqueo giratorio en la parte inferior de la pantalla. En este artículo, te contamos cómo funciona el globo terráqueo y qué usamos para crearlo.
Para brindarte una descripción general rápida, el globo terráqueo de Maravillas del mundo es una versión muy modificada del globo terráqueo de WebGL creada por el equipo de Google Data Arts. Tomamos el globo terráqueo original, quitamos los elementos del gráfico de barras, cambiamos los sombreadores, agregamos marcadores HTML en los que se puede hacer clic y la geometría del continente de Natural Earth de la demostración de GlobeTweeter de Mozilla (muchas gracias a Cedric Pinson). Todo para crear un globo animado atractivo que combine con el esquema de colores del sitio y le agregue una capa adicional de sofisticación.
El brief de diseño del globo terráqueo era tener un mapa animado atractivo con marcadores en los que se pudiera hacer clic sobre los sitios del Patrimonio Mundial. Con eso en mente, empecé a buscar algo adecuado. Lo primero que me vino a la mente fue el globo terráqueo de WebGL creado por el equipo de Google Data Arts. Es un globo terráqueo y se ve genial. ¿Qué más necesitas?
Cómo configurar el globo terráqueo de WebGL
El primer paso para crear el widget del globo fue descargar el globo de WebGL y ponerlo en funcionamiento. El globo terráqueo de WebGL está en línea en Google Code y es fácil de descargar y ejecutar. Descarga y extrae el archivo ZIP, ingresa a él y ejecuta un servidor web básico: python -m SimpleHTTPServer
. (Ten en cuenta que no tiene UTF-8 activado de forma predeterminada; puedes usarlo). Ahora, si navegas a http://localhost:8000/globe/globe.html
, deberías ver el globo terráqueo de WebGL.
Con el globo terráqueo de WebGL en funcionamiento, era hora de cortar todas las partes innecesarias. Edité el código HTML para quitar los fragmentos de la IU y quité el contenido de configuración del gráfico de barras del globo terráqueo de la función de inicialización del globo terráqueo. Al final de ese proceso, tenía un globo terráqueo WebGL muy básico en la pantalla. Puedes girarlo y se ve bien, pero eso es todo.
Para quitar los elementos innecesarios, borré todos los elementos de la IU del archivo index.html del globo terráqueo y edité la secuencia de comandos de inicialización para que se vea de la siguiente manera:
if(!Detector.webgl){
Detector.addGetWebGLMessage();
} else {
var container = document.getElementById('container');
var globe = new DAT.Globe(container);
globe.animate();
}
Agrega la geometría del continente
Queríamos tener la cámara cerca de la superficie del globo, pero cuando probamos el globo con zoom, se hizo evidente la falta de resolución de la textura. Cuando se acerca, la textura del globo terráqueo de WebGL se vuelve pixelada y desenfocada. Podríamos haber usado una imagen más grande, pero eso haría que el globo terráqueo fuera más lento de descargar y ejecutar, por lo que optamos por una representación vectorial de las masas continentales y los bordes.
Para la geometría de la masa terrestre, recurrí a la demostración de código abierto de GlobeTweeter y cargué el modelo 3D en Three.js. Con el modelo cargado y renderizado, era hora de comenzar a pulir el aspecto del globo. El primer problema fue que el modelo de masa terrestre del globo no era lo suficientemente esférico como para estar alineado con el globo de WebGL, así que terminé escribiendo un algoritmo rápido de división de malla que hizo que el modelo de masa terrestre fuera más esférico.
Con un modelo esférico de masa terrestre, pude colocarlo solo un poco desplazado de la superficie del globo, creando continentes flotantes delineados con una línea negra de 2 px debajo para crear una especie de sombra. También experimenté con contornos de colores neón para crear un estilo similar al de Tron.
Con el globo terráqueo y la renderización de las masas continentales, comencé a experimentar con diferentes aspectos para el globo. Como queríamos usar un aspecto monocromático discreto, me quedé con un globo terráqueo y masas continentales en escala de grises. Además de los contornos de neón mencionados anteriormente, probé un globo terráqueo oscuro con masas de tierra oscuras sobre un fondo claro, que se ve bastante bien. Sin embargo, el contraste era demasiado bajo para que se pudiera leer con facilidad y no se ajustaba al estilo del proyecto, así que lo descarté.
Otra idea que tuve al principio para el aspecto del globo terráqueo fue que pareciera porcelana vidriada. No pude probarlo porque no pude escribir un sombreador para lograr el aspecto de porcelana (sería bueno tener un editor de materiales visuales). Lo más parecido que probé fue este globo blanco brillante con masas de tierra negras. Es bastante atractivo, pero tiene un contraste demasiado alto. Y no se ve muy bien. Así que, otro para el basurero.
Los sombreadores de los globos en blanco y negro usan una especie de iluminación difusa con retroiluminación falsa. La luminosidad del globo depende de la distancia de la superficie normal al plano de la pantalla. Por lo tanto, los píxeles en el medio del globo que apuntan a la pantalla son oscuros y los píxeles en los bordes del globo son claros. Si se combina con un fondo claro, se obtiene un aspecto en el que el globo refleja el fondo brillante difuso, lo que crea un aspecto elegante de sala de exposición. El globo negro también usa la textura del globo de WebGL como mapa de brillo, de modo que las plataformas continentales (áreas de aguas poco profundas) se vean brillantes en comparación con las otras partes del globo.
Este es el aspecto del sombreador del océano para el globo negro. Un sombreador de vértices muy básico y un sombreador de fragmentos "oh, eso se ve bastante bien ajuste ajuste" hackeado.
'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')
}
Al final, elegimos un globo oscuro con masas de tierra de color gris claro iluminadas desde arriba. Era el más cercano al brief de diseño y se veía bien y legible. Además, si el globo tiene un contraste bajo, los marcadores y el resto del contenido se destacan más en comparación. En la siguiente versión, se usan océanos completamente negros, mientras que la versión de producción tiene océanos gris oscuro y marcadores ligeramente diferentes.
Cómo crear los marcadores con CSS
A propósito de los marcadores, con el globo terráqueo y las masas continentales en funcionamiento, comencé a trabajar en los marcadores de posición. Decidí usar elementos HTML de estilo CSS para los marcadores, para facilitar su creación y aplicación de diseño, y poder reutilizarlos en el mapa 2D en el que el equipo estaba trabajando. En ese momento, tampoco conocía una forma sencilla de hacer que los marcadores de WebGL fueran en los que se pudiera hacer clic y no quería escribir código adicional para cargar o crear los modelos de marcadores. En retrospectiva, los marcadores de CSS funcionaron bien, pero tenían la tendencia a tener problemas de rendimiento en ocasiones cuando los compositores y renderizadores del navegador estaban en períodos de cambio. Desde el punto de vista del rendimiento, habría sido mejor hacer los marcadores en WebGL. Por otro lado, los marcadores de CSS ahorraron mucho tiempo de desarrollo.
Los marcadores de CSS consisten en un par de divs con posición absoluta con la propiedad de transformación de CSS. El fondo de los marcadores es un gradiente CSS y la parte triangular del marcador es un div girado. Los marcadores tienen una pequeña sombra para destacarlos del fondo. El mayor problema con los marcadores era lograr que funcionaran lo suficientemente bien. Por más triste que parezca, dibujar unas docenas de divs que se mueven y cambian su índice z en cada fotograma es una buena manera de activar todo tipo de problemas de renderización del navegador.
La forma en que los marcadores se sincronizan con la escena en 3D no es demasiado complicada. Cada marcador tiene un Object3D correspondiente en la escena de Three.js, que se usa para hacer un seguimiento de los marcadores. Para obtener coordenadas de espacio de pantalla, tomo las matrices de Three.js para el globo y el marcador, y multiplico un vector cero con ellas. De ahí obtengo la posición de la escena del marcador. Para obtener la posición en pantalla del marcador, proyecto la posición de la escena a través de la cámara. El vector proyectado resultante tiene las coordenadas de espacio de pantalla del marcador, listas para usarse en 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;
}
Al final, el enfoque más rápido fue usar transformaciones CSS para mover los marcadores, no usar la atenuación de opacidad, ya que activaba una ruta lenta en Firefox y mantenía todos los marcadores en el DOM, sin quitarlos cuando se ocultaban detrás del globo. También experimentamos con el uso de transformaciones 3D en lugar de índices z, pero por algún motivo no funcionaron correctamente en la app (aunque sí funcionaron en un caso de prueba reducido, vaya a saber por qué), y en ese momento estábamos a pocos días del lanzamiento, por lo que tuvimos que dejar esa parte para el mantenimiento posterior al lanzamiento.
Cuando haces clic en un marcador, se expande en una lista de nombres de lugares en los que se puede hacer clic. Esto es todo el contenido normal del DOM de HTML, por lo que fue muy fácil de escribir. Todos los vínculos y la renderización de texto funcionan sin esfuerzo de nuestra parte.
Reduce el tamaño del archivo
Con la demostración funcionando y conectada al resto del sitio de World Wonders, aún quedaba un gran problema por resolver. La malla en formato JSON para las masas continentales del globo tenía un tamaño de alrededor de 3 megabytes. No es adecuado para la página principal de un sitio de presentación. Lo bueno fue que comprimir la malla con gzip la redujo a 350 KB. Pero 350 KB sigue siendo un poco grande. Después de un par de correos electrónicos, logramos reclutar a Won Chun, que trabajó en la compresión de las enormes mallas de Google Body, para que nos ayudara a comprimir la malla. Redujo la malla de una gran lista plana de triángulos que se proporcionaban como coordenadas JSON a coordenadas de 11 bits comprimidos con triángulos indexados y redujo el tamaño del archivo a 95 KB comprimidos.
El uso de mallas comprimidas no solo ahorra ancho de banda, sino que también se analizan más rápido. Convertir 3 megabytes de números con formato de cadena en números nativos requiere mucho más trabajo que analizar cien KB de datos binarios. La reducción de tamaño de 250 KB de la página es muy útil, ya que obtiene un tiempo de carga inicial inferior a un segundo en una conexión de 2 Mbps. Más rápido y más pequeño, ¡genial!
Al mismo tiempo, estaba probando la carga de los archivos Shapefile originales de Natural Earth a partir de los cuales se deriva la malla de GlobeTweeter. Pude cargar los archivos Shapefile, pero renderizarlos como masas de tierra planas requiere triangularlos (con agujeros para los lagos, por supuesto). Trianguló las formas con las utilidades de THREE.js, pero no los orificios. Además, las mallas resultantes tenían bordes muy largos, lo que requirió dividir la malla en tris más pequeños. En resumen, no pude hacerlo funcionar a tiempo, pero lo bueno es que el formato Shapefile comprimido habría generado un modelo de masa terrestre de 8 KB. Bueno, tal vez la próxima vez.
Trabajo futuro
Algo que podría mejorarse es hacer que las animaciones de los marcadores sean más atractivas. Ahora, cuando se van por el horizonte, el efecto es un poco cursi. Además, sería bueno tener una animación genial para la apertura del marcador.
En términos de rendimiento, lo que falta es optimizar el algoritmo de división de malla y hacer que los marcadores sean más rápidos. Aparte de eso, todo está bien. ¡Hurra!
Resumen
En este artículo, describí cómo construimos el globo terráqueo en 3D para el proyecto de Google World Wonders. Espero que hayas disfrutado de los ejemplos y que pruebes crear tu propio widget de globo personalizado.