Superbes sphères de points avec WebGL

Les magnifiques globes WebGL interactifs ont eu un moment sous les projecteurs ces derniers temps, Stripe et GitHub les mettant en évidence sur leurs pages d'accueil. Chacun a ensuite écrit un article de blog sur la façon dont ils l'ont fait (Stripe est ici et GitHub est ici si vous êtes curieux).
Les deux globes sont constitués principalement de points, ce qui m'a fait réfléchir aux différentes façons dont les points peuvent être répartis sur la surface d'une sphère. L'emballage de sphères est un casse-tête complexe sous délibération active de la part des mathématiciens. Par conséquent, pour les besoins de cet article, je me suis limité à présenter quelques approches de base et comment les réaliser dans WebGL.
Mise en scène
Avant d'aller plus loin, nous devons établir une scène WebGL de base dans laquelle construire la sphère. J'utilise Three.js comme framework de facto de choix pour interagir avec l'API WebGL. Je m'efforcerai de garder les extraits de code de cet article aussi concis et pertinents que possible ; explorez l'un des bacs à sable intégrés pour le code complet.
Après avoir créé une scène, nous établissons un dotGeometries
tableau qui contiendra éventuellement les géométries de tous nos points. Ensuite, nous créons un vecteur vierge, un point 3D dans l'espace à l'intérieur de la scène, dont la position sera réaffectée à chaque fois que nous créerons un point.
// Set up the scene.
const scene = new THREE.Scene();
// Define an array to hold the geometries of all the dots.
const dotGeometries = [];
// Create a blank vector to be used by the dots.
const vector = new THREE.Vector3();
// We'll create and position the dots here!
// Merge all the dot geometries together into one buffer geometry.
const mergedDotGeometries = BufferGeometryUtils.mergeBufferGeometries(
dotGeometries
);
// Define the material for the dots.
const dotMaterial = new THREE.MeshBasicMaterial({
color: DOT_COLOR,
side: THREE.DoubleSide
});
// Create the dot mesh from the dot geometries and material.
const dotMesh = new THREE.Mesh(mergedDotGeometries, dotMaterial);
// Add the dot mesh to the scene.
scene.add(dotMesh);
L'approche de base
La façon la plus simple d'ajouter des points à une sphère est simplement de définir le nombre de lignes de latitude et de longitude que nous aimerions que la sphère ait, puis de répartir les points le long de celles-ci. Il y a quelques choses importantes à noter ici.
Tout d'abord, nous définissons les angles phi
et pour chaque point. theta
Ces angles font partie du système de coordonnées sphériques, un système permettant de définir exactement où se trouve un point dans l'espace 3D par rapport à son origine (qui dans notre cas est le centre de notre sphère).
Deuxièmement, phi
et theta
sont tous deux mesurés en radians, pas en degrés. La clé pour cela est de se rappeler qu'il y a π radians dans 180º . Donc, pour trouver phi
ici, il suffit de diviser π par le nombre de lignes de latitude. Mais pour trouver theta
, nous devons diviser 2 * π
par le nombre de lignes de longitude parce que nous voulons que nos lignes de longitude continuent sur les 360º complets de la sphère.
// Loop across the latitudes.
for (let lat = 0; lat < LATITUDE_COUNT; lat += 1) {
// Loop across the longitudes.
for (let lng = 0; lng < LONGITUDE_COUNT; lng += 1) {
// Create a geomtry for the dot.
const dotGeometry = new THREE.CircleGeometry(DOT_SIZE, 5);
// Defin the phi and theta angles for the dot.
const phi = (Math.PI / LATITUDE_COUNT) * lat;
const theta = ((2 * Math.PI) / LONGITUDE_COUNT) * lng;
// Set the vector using the spherical coordinates generated from the sphere radius, phi and theta.
vector.setFromSphericalCoords(SPHERE_RADIUS, phi, theta);
// Make sure the dot is facing in the right direction.
dotGeometry.lookAt(vector);
// Move the dot geometry into position.
dotGeometry.translate(vector.x, vector.y, vector.z);
// Push the positioned geometry into the array.
dotGeometries.push(dotGeometry);
}
}
Si vous interagissez avec la sphère pour la faire pivoter, vous remarquerez que les anneaux en haut et en bas sont beaucoup plus denses que ceux du milieu. C'est parce que nous n'avons pas fait varier le nombre de points sur chaque ligne de latitude. C'est là qu'intervient l'emballage de sphères.
L'approche phyllotaxie
Si vous avez déjà regardé la tête d'un tournesol ou la base d'une pomme de pin, vous aurez remarqué un motif inhabituel et distinctif. Ce modèle, créé par un arrangement basé sur la suite de Fibonacci , est connu sous le nom de phyllotaxie. Nous pouvons l'utiliser ici pour positionner nos points de manière à ce qu'ils apparaissent beaucoup plus régulièrement espacés sur la surface de la sphère.
Cette fois, au lieu de définir le nombre de lignes de latitude et de longitude, nous définissons simplement le nombre total de points que nous voulons voir apparaître sur la sphère. Au lieu de boucler sur les lignes de latitude, les points seront rendus en une seule spirale continue d'un pôle de la sphère à l'autre.
// Loop across the number of dots.
for (let dot = 0; dot < DOT_COUNT; dot += 1) {
// Create a geomtry for the dot.
const dotGeometry = new THREE.CircleGeometry(DOT_SIZE, 5);
// Work out the spherical coordinates of each dot, in a phyllotaxis pattern.
const phi = Math.acos(-1 + (2 * dot) / DOT_COUNT);
const theta = Math.sqrt(DOT_COUNT * Math.PI) * phi;
// Set the vector using the spherical coordinates generated from the sphere radius, phi and theta.
vector.setFromSphericalCoords(SPHERE_RADIUS, phi, theta);
...
}
C'est beaucoup plus satisfaisant. Mais que se passe-t-il si nous voulons regrouper les points aussi uniformément que possible, tout en ayant la liberté de définir le nombre de lignes de latitude ?
L'approche linéaire
Cette fois, nous allons définir le nombre de lignes de latitude requises, mais le nombre de points sera également mis à l'échelle en fonction de la circonférence de la ligne de latitude sur laquelle ils sont positionnés. Pour nous donner un meilleur contrôle sur l'espacement, nous définirons également un paramètre de densité de points.
La partie délicate ici calcule le rayon de chaque ligne de latitude. Une fois que nous avons cela, il est relativement simple de déterminer le nombre de points à afficher dessus, puis de trouver phi
et theta
pour chacun d'une manière similaire à la première approche.
// Loop across the latitude lines.
for (let lat = 0; lat < LATITUDE_COUNT; lat += 1) {
// Calculate the radius of the latitude line.
const radius =
Math.cos((-90 + (180 / LATITUDE_COUNT) * lat) * (Math.PI / 180)) *
SPHERE_RADIUS;
// Calculate the circumference of the latitude line.
const latitudeCircumference = radius * Math.PI * 2 * 2;
// Calculate the number of dots required for the latitude line.
const latitudeDotCount = Math.ceil(latitudeCircumference * DOT_DENSITY);
// Loop across the dot count for the latitude line.
for (let dot = 0; dot < latitudeDotCount; dot += 1) {
const dotGeometry = new THREE.CircleGeometry(DOT_SIZE, 5);
// Calculate the phi and theta angles for the dot.
const phi = (Math.PI / LATITUDE_COUNT) * lat;
const theta = ((2 * Math.PI) / latitudeDotCount) * dot;
...
}
}
Nous avons donc expliqué comment afficher les points sur la sphère. Mais qu'en est-il de l'obtention d'effets plus complexes ?
Masquage de forme
Comprendre comment afficher les points dans des motifs de plus en plus compliqués peut rapidement devenir un casse-tête mathématique. Cependant, en utilisant l'un des arrangements d'emballage ci-dessus en combinaison avec une image de masque, nous pouvons obtenir des effets extraordinaires.
Pour ce faire, nous devrons d'abord créer un élément de canevas HTML et dessiner notre image de masque dessus. Cet élément ne sera pas rendu à l'écran ; c'est juste une méthode pratique pour extraire les données de pixel d'une image . Nous n'avons besoin de le faire qu'une seule fois, nous le ferons donc dès le départ, puis nous transmettrons les données d'image extraites à notre renderScene
fonction.
// Initialise an image loader.
const imageLoader = new THREE.ImageLoader();
// Load the image used to determine where dots are displayed. The sphere
// cannot be initialised until this is complete.
imageLoader.load(MASK_IMAGE, (image) => {
// Create an HTML canvas, get its context and draw the image on it.
const tempCanvas = document.createElement("canvas");
tempCanvas.width = image.width;
tempCanvas.height = image.height;
const ctx = tempCanvas.getContext("2d");
ctx.drawImage(image, 0, 0);
// Read the image data from the canvas context.
const imageData = ctx.getImageData(0, 0, image.width, image.height);
renderScene(imageData);
});
// Utility function to convert a dot on a sphere into a UV point on a
// rectangular texture or image.
const spherePointToUV = (dotCenter, sphereCenter) => {
// Create a new vector and give it a direction from the center of the sphere
// to the center of the dot.
const newVector = new THREE.Vector3();
newVector.subVectors(sphereCenter, dotCenter).normalize();
// Calculate the UV coordinates of the dot and return them as a vector.
const uvX = 1 - (0.5 + Math.atan2(newVector.z, newVector.x) / (2 * Math.PI));
const uvY = 0.5 + Math.asin(newVector.y) / Math.PI;
return new THREE.Vector2(uvX, uvY);
};
// Utility function to sample the data of an image at a given point. Requires
// an imageData object.
const sampleImage = (imageData, uv) => {
// Calculate and return the data for the point, from the UV coordinates.
const point =
4 * Math.floor(uv.x * imageData.width) +
Math.floor(uv.y * imageData.height) * (4 * imageData.width);
return imageData.data.slice(point, point + 4);
};
// Move the dot geometry into position.
dotGeometry.translate(vector.x, vector.y, vector.z);
// Find the bounding sphere of the dot.
dotGeometry.computeBoundingSphere();
// Find the UV position of the dot on the land image.
const uv = spherePointToUV(
dotGeometry.boundingSphere.center,
new THREE.Vector3()
);
// Sample the pixel on the land image at the given UV position.
const sampledPixel = sampleImage(imageData, uv);
// If the pixel contains a color value (in other words, is not transparent),
// continue to create the dot. Otherwise don't bother.
if (sampledPixel[3]) {
// Push the positioned geometry into the array.
dotGeometries.push(dotGeometry);
}
Nous pouvons utiliser des images de masque plus complexes pour obtenir des formes telles que cet effet de terre :
Ou même pour rendre du texte :
C'est un enveloppement
J'ai utilisé ces techniques de cartographie sphérique à divers endroits comme base de pièces maîtresses WebGL. J'espère qu'ils vous inspireront à faire de même. Si vous avez apprécié cet article ou s'il vous a aidé d'une manière ou d'une autre, n'hésitez pas à me le faire savoir ! Mon site web est par ici .