Présentation du globe 3D des merveilles du monde
Si vous avez consulté le site Merveilles du monde de Google récemment lancé sur un navigateur compatible avec WebGL, vous avez peut-être remarqué un globe en rotation en bas de l'écran. Cet article vous explique comment fonctionne le globe et comment nous l'avons créé.
Pour vous donner un bref aperçu, le globe des merveilles du monde est une version fortement modifiée du globe WebGL par l'équipe Google Data Arts. Nous avons pris le globe d'origine, supprimé les éléments du graphique à barres, modifié les nuanceurs, ajouté des repères HTML cliquables et la géométrie des continents de Natural Earth à partir de la démonstration GlobeTweeter de Mozilla (un grand merci à Cédric Pinson !). Le tout pour créer un globe animé qui correspond au schéma de couleurs du site et lui ajoute une touche de sophistication.
Le brief de conception du globe était de créer une carte animée attrayante avec des repères cliquables placés sur les sites du patrimoine mondial. Je me suis donc mis à chercher un endroit approprié. La première chose qui m'est venue à l'esprit était le globe WebGL créé par l'équipe Google Data Arts. C'est un globe et il a l'air cool. De quoi d'autre avez-vous besoin ?
Configurer le globe WebGL
La première étape de la création du widget de globe a consisté à télécharger le globe WebGL et à le mettre en service. Le globe WebGL est disponible en ligne sur Google Code. Il est simple à télécharger et à exécuter. Téléchargez et décompressez le fichier ZIP, accédez-y et exécutez un serveur Web de base: python -m SimpleHTTPServer
. (Notez que l'encodage UTF-8 n'est pas activé par défaut. Vous pouvez l'utiliser.) Si vous accédez à http://localhost:8000/globe/globe.html
, vous devriez voir le globe WebGL.
Une fois le globe WebGL opérationnel, il était temps de supprimer toutes les parties inutiles. J'ai modifié le code HTML pour supprimer les éléments de l'interface utilisateur et j'ai supprimé la configuration du graphique à barres du globe de la fonction d'initialisation du globe. À la fin de ce processus, j'avais un globe WebGL très basique à l'écran. Vous pouvez la faire pivoter, et c'est plutôt cool, mais c'est à peu près tout.
Pour supprimer les éléments inutiles, j'ai supprimé tous les éléments d'interface utilisateur de l'index.html du globe et modifié le script d'initialisation pour qu'il ressemble à ceci:
if(!Detector.webgl){
Detector.addGetWebGLMessage();
} else {
var container = document.getElementById('container');
var globe = new DAT.Globe(container);
globe.animate();
}
Ajouter la géométrie du continent
Nous voulions placer la caméra près de la surface du globe, mais lorsque nous avons testé le globe en zoomant, le manque de résolution de la texture est devenu évident. En zoomant, la texture du globe WebGL devient pixellisée et floue. Nous aurions pu utiliser une image plus grande, mais cela aurait ralenti le téléchargement et l'exécution du globe. Nous avons donc opté pour une représentation vectorielle des terres émergées et des frontières.
Pour la géométrie des terres émergées, j'ai utilisé la démonstration Open Source GlobeTweeter et chargé le modèle 3D dans Three.js. Une fois le modèle chargé et le rendu effectué, il était temps de commencer à peaufiner l'apparence du globe. Le premier problème était que le modèle de masse terrestre du globe n'était pas assez sphérique pour être aligné sur le globe WebGL. J'ai donc fini par écrire un algorithme de division rapide des mailles qui a rendu le modèle de masse terrestre plus sphérique.
Avec un modèle sphérique des terres émergées, j'ai pu le placer légèrement en décalage par rapport à la surface du globe, créant ainsi des continents flottants entourés d'une ligne noire de 2 pixels en dessous pour créer une sorte d'ombre. J'ai également expérimenté des contours de couleur néon pour créer une sorte d'effet Tron.
Avec le rendu du globe et des terres émergées, j'ai commencé à tester différents styles pour le globe. Comme nous voulions opter pour un look monochrome sobre, j'ai choisi un globe et des terres en niveaux de gris. En plus des contours néon mentionnés ci-dessus, j'ai essayé un globe sombre avec des masses terrestres sombres sur un arrière-plan clair, ce qui est plutôt sympa. Mais le contraste était trop faible pour être facilement lisible et il ne correspondait pas à l'ambiance du projet. Je l'ai donc abandonné.
Une autre idée que j'ai eue pour le globe était de lui donner l'apparence de porcelaine émaillée. Je n'ai pas réussi à l'essayer, car je n'ai pas réussi à écrire un nuanceur pour obtenir l'aspect de la porcelaine (un éditeur de matériaux visuels serait bien). Le plus proche que j'ai essayé était un globe blanc lumineux avec des masses terrestres noires. C'est plutôt bien, mais le contraste est trop élevé. Et ça ne fait pas très joli. Encore un à la poubelle.
Les nuanceurs des globes noirs et blancs utilisent une sorte de faux éclairage rétroéclairé diffus. La luminosité du globe dépend de la distance de la surface normale au plan de l'écran. Par conséquent, les pixels au milieu du globe qui pointent vers l'écran sont sombres, et les pixels sur les bords du globe sont clairs. Combiné à un arrière-plan clair, le globe reflète l'arrière-plan lumineux diffus, ce qui donne un aspect de showroom élégant. Le globe noir utilise également la texture du globe WebGL comme carte de brillance, de sorte que les plates-formes continentales (zones d'eau peu profonde) paraissent brillantes par rapport aux autres parties du globe.
Voici à quoi ressemble le nuanceur de l'océan pour le globe noir. Un nuanceur de sommet très basique et un nuanceur de fragment "oh, ça a l'air plutôt sympa ajuster ajuster".
'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')
}
Nous avons finalement opté pour un globe sombre avec des masses terrestres gris clair éclairées par le haut. Il était le plus proche du brief de conception, et il était agréable et lisible. De plus, le faible contraste du globe fait ressortir les repères et le reste du contenu. La version ci-dessous utilise des océans complètement noirs, tandis que la version de production présente des océans gris foncé et des repères légèrement différents.
Créer les repères avec CSS
En parlant de repères, maintenant que le globe et les masses terrestres fonctionnent, j'ai commencé à travailler sur les repères géographiques. J'ai choisi d'utiliser des éléments HTML de style CSS pour les repères, afin de les créer et de les styliser plus facilement, et de pouvoir éventuellement les réutiliser dans la carte 2D sur laquelle l'équipe travaillait. À l'époque, je ne savais pas comment rendre les repères WebGL cliquables facilement et je ne voulais pas écrire de code supplémentaire pour charger / créer les modèles de repères. Avec le recul, les repères CSS fonctionnaient bien, mais avaient tendance à rencontrer occasionnellement des problèmes de performances lorsque les compilateurs et les moteurs de rendu du navigateur étaient en période de flux. D'un point de vue des performances, il aurait été préférable de créer les repères en WebGL. Mais les repères CSS ont économisé beaucoup de temps de développement.
Les repères CSS se composent de deux divs positionnés de manière absolue avec la propriété CSS transform. L'arrière-plan des repères est un dégradé CSS, et la partie triangulaire du repère est un élément div pivoté. Les repères sont dotés d'une petite ombre portée pour les faire ressortir de l'arrière-plan. Le plus grand problème des repères était de les faire fonctionner suffisamment bien. Aussi triste que cela puisse paraître, dessiner quelques dizaines de divs qui se déplacent et changent d'indice Z à chaque frame est un excellent moyen de déclencher toutes sortes de pièges de rendu du navigateur.
La façon dont les repères sont synchronisés avec la scène 3D n'est pas trop compliquée. Chaque repère est associé à un objet Object3D dans la scène Three.js, qui permet de le suivre. Pour obtenir les coordonnées d'espace à l'écran, je prends les matrices Three.js pour le globe et le repère, et je les multiplie par un vecteur nul. Je peux ainsi obtenir la position de la scène du repère. Pour obtenir la position à l'écran du repère, je projette la position de la scène via la caméra. Le vecteur projeté obtenu contient les coordonnées d'espace à l'écran du repère, prêtes à être utilisées 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;
}
Au final, l'approche la plus rapide consistait à utiliser des transformations CSS pour déplacer les repères, et non à utiliser un fondu d'opacité, car cela déclenchait un chemin lent sur Firefox et conservait tous les repères dans le DOM, sans les supprimer lorsqu'ils se trouvaient derrière le globe. Nous avons également essayé d'utiliser des transformations 3D au lieu d'indices Z, mais pour une raison quelconque, cela ne fonctionnait pas correctement dans l'application (mais cela fonctionnait dans un cas de test réduit, allez savoir pourquoi). À ce stade, nous étions à quelques jours du lancement, nous avons donc dû laisser cette partie à la maintenance post-lancement.
Lorsque vous cliquez sur un repère, une liste de noms de lieux cliquables s'affiche. Il s'agit de choses DOM HTML normales. L'écriture a donc été très facile. Tous les liens et le rendu du texte fonctionnent sans effort de notre part.
Réduire la taille des fichiers
La démonstration fonctionnait et était connectée au reste du site des merveilles du monde, mais il restait un gros problème à résoudre. La maille au format JSON pour les terres émergées du globe mesurait environ 3 Mo. Ne convient pas à la page d'accueil d'un site vitrine. Heureusement, la compression du maillage avec gzip l'a ramené à 350 ko. Mais 350 ko, c'est encore un peu trop. Après quelques e-mails, nous avons réussi à recruter Won Chun, qui a travaillé sur la compression des énormes maillages de Google Body, pour nous aider à compresser le maillage. Il a réduit la taille du maillage, passant d'une grande liste plate de triangles donnés en coordonnées JSON à des coordonnées compressées de 11 bits avec des triangles indexés, et a réduit la taille du fichier à 95 ko compressé.
L'utilisation de mailles compressées permet non seulement d'économiser de la bande passante, mais aussi d'accélérer l'analyse des mailles. Convertir trois mégaoctets de nombres en chaînes en nombres natifs nécessite beaucoup plus de travail que d'analyser cent kilo-octets de données binaires. La réduction de taille de 250 ko obtenue pour la page est très pratique, et le temps de chargement initial est inférieur à une seconde sur une connexion de 2 Mbit/s. Plus rapide et plus petit, c'est génial !
En même temps, je testais le chargement des fichiers de forme Natural Earth d'origine à partir desquels le maillage GlobeTweeter est dérivé. J'ai réussi à charger les fichiers de forme, mais pour les afficher en tant que terres plates, il faut les trianguler (avec des trous pour les lacs, bien sûr). J'ai triangulé les formes à l'aide des utilitaires THREE.js, mais pas les trous. Les mailles obtenues avaient des arêtes très longues, ce qui a nécessité de les diviser en tris plus petits. Bref, je n'ai pas réussi à le faire fonctionner à temps, mais le format de fichier de forme compressé plus loin vous aurait donné un modèle de masse terrestre de 8 ko. Tant pis, peut-être la prochaine fois.
Prochains ajouts
Les animations des repères pourraient être améliorées. Maintenant, lorsque les bateaux passent au-delà de l'horizon, l'effet est un peu maladroit. De plus, une animation sympa pour l'ouverture du repère serait la bienvenue.
En termes de performances, deux éléments manquent : l'optimisation de l'algorithme de division de maillage et l'accélération des repères. À part cela, tout va bien. Youpi !
Résumé
Dans cet article, je vous ai expliqué comment nous avons créé le globe 3D pour le projet "Merveilles du monde" de Google. J'espère que vous avez apprécié les exemples et que vous allez essayer de créer votre propre widget de globe personnalisé.