Création du globe 3D des merveilles du monde

Ilmari Heikkinen

Présentation du globe 3D World Wonders

Si vous avez consulté le site Google World Wonders récemment lancé sur un navigateur compatible WebGL, vous avez peut-être vu, en bas de l'écran, un magnifique globe qui tourne sur lui-même. Cet article vous explique le fonctionnement du globe et ce que nous avons utilisé pour le créer.

Pour vous donner un aperçu rapide, le globe World Wonders est une version optimisée du globe WebGL réalisée 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 sophistiqués et ajouté la géométrie naturelle des continents à partir de la démo GlobeTweeter de Mozilla (un grand merci à Cedric Pinson !) Le tout pour créer un beau globe animé qui correspond au jeu de couleurs du site et ajoute une couche de sophistication supplémentaire au site.

Le brief de conception pour le monde était de proposer une belle carte animée avec des marqueurs cliquables placés au-dessus des sites du patrimoine mondial. En gardant cela à l'esprit, j'ai commencé à chercher quelque chose qui convienne. Nous avons d'abord pensé au globe WebGL construit par l'équipe Google Data Arts. C'est un globe et ça a l'air cool. Qu'est-ce qu'il nous faut d'autre, hein ?

Configurer le globe WebGL

Pour créer le widget globe, la première étape a été de télécharger le globe WebGL et de le rendre opérationnel. Il est disponible en ligne sur Google Code. Il est facile à télécharger et à exécuter. Téléchargez et extrayez le fichier ZIP, utilisez la commande cd pour y accéder 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 maintenant voir le globe WebGL.

Le globe WebGL était opérationnel, il était temps de couper toutes les parties inutiles. J'ai modifié le code HTML pour supprimer les bits de l'interface utilisateur et supprimé les opérations de configuration du graphique à barres représentant un globe dans la fonction d'initialisation du globe. À la fin de ce processus, un globe WebGL s'affichait à l'écran. Tu peux faire tourner la caméra pour qu'elle ait l'air cool, mais c'est tout.

Pour supprimer les éléments inutiles, j'ai supprimé tous les éléments de l'interface utilisateur du fichier index.html du globe, puis modifié le script d'initialisation pour qu'il se présente comme suit:

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 que l'appareil photo soit proche de la surface du globe, mais lorsque nous avons testé le zoom sur le globe, le manque de résolution des textures est apparu. Si vous faites un zoom avant, la texture du globe WebGL devient floue et cubique. Nous aurions pu utiliser une image plus grande, mais cela ralentirait le téléchargement et l'exécution du globe terrestre. Nous avons donc choisi d'utiliser une représentation vectorielle des masses terrestres et des frontières.

Pour la géométrie de la masse terrestre, j'ai utilisé la démo Open Source GlobeTweeter et j'ai chargé le modèle 3D dans Three.js. Une fois le modèle chargé et rendu, 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é avec le globe WebGL. J'ai donc fini par écrire un algorithme de division rapide du maillage, qui rendait le modèle de masse terrestre plus sphérique.

Avec un modèle de masse terrestre sphérique, j'ai pu le placer légèrement décalé par rapport à la surface du globe, créant ainsi des continents flottants entourés d'une ligne noire de 2 pixels en dessous, représentant une sorte d'ombre. J'ai également testé les contours aux couleurs fluo pour donner un style Tron.

J'ai commencé à tester différents styles pour le globe terrestre et le rendu des masses terrestres. Pour adopter un look monochrome sobre, j'ai choisi de choisir un globe terrestre en nuances de gris et des masses terrestres. En plus des contours de néons mentionnés précédemment, j'ai testé un globe terrestre sombre avec des masses terrestres sombres sur un fond clair, ce qui a l'air plutôt cool. Mais c'était trop faible pour être facilement lisible et cela ne correspondait pas à l'aspect du projet, donc je l'ai mis au rebut.

Une autre pensée précoce que j'ai eue pour le look du globe était de le faire ressembler à de la porcelaine émaillée. Celui que je n'ai pas réussi à essayer, car je n'ai pas réussi à écrire un nuanceur pour créer l'aspect "porcelaine" (l'éditeur de matériel visuel serait bien). La chose la plus proche que j'ai essayée, c'est ce globe blanc lumineux avec des masses terrestres noires. Le résultat est un peu clair, mais trop contrasté. Et il n’a pas l’air super beau. Voici un autre exemple pour le tas de mémoire.

Les nuanceurs des globes noir et blanc utilisent une sorte de faux éclairage diffus. La luminosité du globe dépend de la distance entre la surface normale et le plan de l'écran. Ainsi, les pixels situés au milieu du globe qui pointent vers l'écran sont sombres, tandis que les pixels situés aux bords du globe sont clairs. Avec un arrière-plan clair, vous obtenez un rendu d'une salle d'exposition élégante où le globe reflète l'arrière-plan clair et lumineux. Le globe noir utilise également la texture du globe WebGL comme carte brillante, afin que les étagères continentales (zones d'eaux peu profondes) soient brillantes par rapport aux autres parties du globe.

Voici à quoi ressemble le nuanceur d'océan pour le globe noir. Nuanceur de sommets très basique et nuanceur de fragments "oh ça a l'air plutôt parfait pour 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')
    }

À la fin, nous avons réalisé un globe terrestre gris clair illuminé par le ciel. Elle était la plus proche du brief de conception, et elle était agréable et lisible. En outre, le fait que le globe présente un faible contraste permet aux repères et au reste du contenu de se démarquer davantage. La version ci-dessous utilise des océans entièrement 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, le globe et les masses terrestres fonctionnent. J'ai commencé à travailler sur les repères. J'ai choisi d'utiliser des éléments HTML de style CSS pour les repères afin de faciliter leur création et leur style, et pour potentiellement les réutiliser dans la carte 2D sur laquelle l'équipe travaillait. À ce moment-là, je ne savais pas non plus de moyen simple de rendre les repères WebGL cliquables, 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 ils avaient tendance à rencontrer occasionnellement des problèmes de performances lorsque les compositeurs et les moteurs de rendu de navigateur étaient dans des périodes de changement. En termes de performances, utiliser les repères dans WebGL aurait été une meilleure option. Là encore, les marqueurs CSS ont permis de gagner beaucoup de temps de développement.

Les repères CSS sont constitués de quelques éléments div positionnés en position absolue avec la propriété de transformation CSS. L'arrière-plan des repères est en dégradé CSS, tandis que la partie triangle du repère est un élément div en rotation. Les repères ont une petite ombre projetée qui les fait ressortir de l'arrière-plan. Le principal problème avec les repères était qu'ils soient suffisamment performants. Aussi, que cela puisse paraître, le fait de dessiner quelques dizaines de tags div qui se déplacent et modifient leur z-index sur chaque image est un très bon moyen de déclencher toutes sortes de pièges liés à l'affichage dans le navigateur.

La synchronisation des repères avec la scène 3D n'est pas trop compliquée. Chaque repère possède un objet Object3D correspondant dans la scène Three.js, qui permet de suivre les repères. Pour obtenir les coordonnées d'espace à l'écran, je prends les matrices Three.js du globe et le repère, et je multiplie un vecteur nul par ces matrices. J'obtiens alors la position du repère sur la scène. Pour connaître la position du repère à l'écran, je projette la position de la scène via la caméra. Le vecteur projeté obtenu possède les coordonnées d'espace à l'écran du repère, prêt à être utilisé 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;
}

En fin de compte, l'approche la plus rapide consistait à utiliser des transformations CSS pour déplacer les repères, et non à utiliser le fondu d'opacité, car il déclenchait un chemin lent dans Firefox, et de conserver tous les repères dans le DOM, au lieu de les supprimer lorsqu'ils passaient derrière le globe. Nous avons également testé l'utilisation de transformations 3D au lieu de z-index, mais pour une raison quelconque, cela ne fonctionnait pas directement dans l'application (mais cela fonctionnait dans un scénario de test réduit). À ce stade, quelques jours après le lancement, nous avons dû laisser cette partie à la maintenance post-lancement.

Lorsque vous cliquez sur un repère, la liste des noms de lieux cliquables s'affiche. C'est tout ce qui est normal du DOM HTML, donc c'était super facile à écrire. Les liens et le rendu du texte fonctionnent sans effort supplémentaire de notre part.

Presser la taille du fichier

Alors que la démo fonctionnait et que l'utilisateur ait accès au reste du site du World Wonders, il y avait encore un gros problème à résoudre. La taille du maillage au format JSON pour les masses terrestres du globe était d'environ 3 Mo. Ne convient pas à la page d'accueil d'un site Showcase. L'avantage, c'est que la compression du maillage à l'aide de gzip l'a ramené à 350 ko. Mais bon, 350 ko, c'est quand même un peu gros. Dans quelques e-mails par la suite, nous avons réussi à recruter Won Chun, qui s'est occupé de la compression des énormes maillages Google Body, pour nous aider à les compresser. Il a réduit le maillage d'une longue liste plate de triangles fournis sous forme de coordonnées JSON à des coordonnées compressées 11 bits avec des triangles indexés. Il a ainsi réduit la taille du fichier à 95 Ko au format gzip.

L'utilisation de maillages compressés permet non seulement d'économiser de la bande passante, mais est également plus rapide à analyser. Convertir 3 mégaoctets de nombres convertis en nombres natifs demande beaucoup plus de travail que d'analyser une centaine de ko de données binaires. De plus, la taille réduite de 250 Ko pour la page est très pratique, et le temps de chargement initial est inférieur à une seconde avec une connexion de 2 Mbit/s. Plus rapide et plus petit, superbe sauce !

En même temps, je griffonnais les fichiers de formes Natural Earth d'origine dont est dérivé le maillage GlobeTweeter. J'ai réussi à charger les fichiers de forme, mais pour les rendre sous forme de masses terrestres plates, il faut les trianguler (avec des trous pour les lacs, natch). J'ai triangulé les formes à l'aide d'utilitaires THREE.js, mais pas les trous. Les maillages résultants avaient des bords très longs, ce qui nécessitait de diviser le maillage en trois tris plus petits. En bref, je n'ai pas réussi à le faire fonctionner à temps, mais le plus intéressant, c'était que le format Shapefile encore compressé vous aurait fourni un modèle de masse terrestre de 8 Ko. La prochaine fois peut-être.

Travaux futurs

Rendre les animations de repères plus agréables est une chose qui pourrait demander un peu plus de travail. Maintenant, quand ils dépassent l'horizon, l'effet est un peu délicat. En outre, il serait intéressant d'avoir une animation sympa pour l'ouverture du repère.

En termes de performances, les deux choses qui manquent sont l'optimisation de l'algorithme de division du maillage et l'accélération des repères. À part cela, les choses sont dandy. Hourra !

Résumé

Dans cet article, j'ai décrit comment nous avons construit le globe 3D pour le projet Google World Wonders. J'espère que ces exemples vous ont plu et que vous allez essayer de créer votre propre widget de globe personnalisé.

Références