Impresionantes esferas de puntos con WebGL

Los hermosos e interactivos globos WebGL han tenido un momento en el centro de atención últimamente con Stripe y GitHub presentándolos de manera destacada en sus páginas de inicio. Más tarde, cada uno escribió una publicación de blog sobre cómo lo hicieron (el de Stripe está aquí y el de GitHub aquí si tiene curiosidad).
Ambos globos están hechos principalmente de puntos, lo que me hizo pensar en las diversas formas en que los puntos se pueden distribuir en la superficie de una esfera. El empaquetamiento de esferas es un rompecabezas complejo bajo deliberación activa de los matemáticos, por lo que para los fines de este artículo me he limitado a exponer algunos enfoques básicos y cómo lograrlos en WebGL.
Preparando la escena
Antes de continuar, necesitamos establecer una escena WebGL básica en la que construir la esfera. Estoy usando Three.js como el marco de elección de facto para interactuar con la API de WebGL. Trataré de mantener los fragmentos de código en este artículo lo más concisos y relevantes posible; explore cualquiera de los Sandboxes incrustados para obtener el código completo.
Después de crear una escena, establecemos una dotGeometries
matriz que eventualmente contendrá las geometrías de todos nuestros puntos. Luego creamos un vector en blanco, un punto 3D en el espacio dentro de la escena, cuya posición se reasignará cada vez que creemos un punto.
// 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);
El enfoque básico
La forma más fácil de agregar puntos a una esfera es simplemente definir el número de líneas de latitud y longitud que nos gustaría que tenga la esfera y luego distribuir los puntos a lo largo de ellas. Hay un par de cosas importantes a tener en cuenta aquí.
En primer lugar, estamos definiendo los ángulos phi
y para cada punto. theta
Estos ángulos forman parte del sistema de coordenadas esféricas, un sistema para definir exactamente dónde se encuentra un punto en el espacio 3D en relación con su origen (que en nuestro caso es el centro de nuestra esfera).
En segundo lugar, phi
y theta
ambos se miden en radianes, no en grados. La clave para esto es recordar que hay π radianes en 180º . Entonces, para encontrar phi
aquí, todo lo que tenemos que hacer es dividir π por el número de líneas de latitud. Pero para encontrar theta
, necesitamos dividir 2 * π
por el número de líneas de longitud porque queremos que nuestras líneas de longitud continúen alrededor de los 360º completos de la esfera.
// 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 interactúas con la esfera para rotarla, notarás que los anillos en la parte superior e inferior están mucho más densos que los del medio. Esto se debe a que no hemos variado la cantidad de puntos en cada línea de latitud. Aquí es donde entra en juego el empaquetamiento de esferas.
El enfoque de la filotaxis
Si alguna vez ha mirado la cabeza de un girasol o la base de una piña, habrá notado un patrón inusual y distintivo. Este patrón, creado por un arreglo basado en la secuencia de Fibonacci , se conoce como filotaxis. Podemos usarlo aquí para colocar nuestros puntos de tal manera que aparezcan espaciados mucho más uniformemente sobre la superficie de la esfera.
Esta vez, en lugar de definir el número de líneas de latitud y longitud, simplemente definimos el número total de puntos que queremos que aparezcan en la esfera. En lugar de recorrer las líneas de latitud, los puntos se representarán en una única espiral continua de un polo de la esfera al otro.
// 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);
...
}
Esto es mucho más satisfactorio. Pero, ¿qué pasa si queremos empaquetar los puntos de la manera más uniforme posible, pero aún tenemos la libertad de definir el número de líneas de latitud?
El enfoque lineal
Esta vez definiremos la cantidad de líneas de latitud requeridas, pero la cantidad de puntos también se escalará en función de la circunferencia de la línea de latitud en la que estén posicionados. Para darnos un mayor control sobre el espaciado, también definiremos un parámetro de densidad de puntos.
La parte complicada aquí es calcular el radio de cada línea de latitud. Una vez que tenemos eso, es relativamente simple averiguar cuántos puntos mostrar en él y luego encontrar phi
y theta
para cada uno de manera similar al primer enfoque.
// 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;
...
}
}
Así que hemos cubierto cómo mostrar los puntos en la esfera. Pero, ¿qué hay de lograr efectos más complejos?
Enmascaramiento de forma
Descubrir cómo mostrar los puntos en patrones cada vez más complicados puede convertirse rápidamente en un dolor de cabeza matemático. Sin embargo, al usar uno de los arreglos de empaque anteriores en combinación con una imagen de máscara, podemos lograr algunos efectos extraordinarios.
Para hacer esto, primero necesitaremos crear un elemento de lienzo HTML y dibujar nuestra imagen de máscara en él. Este elemento en realidad no se representará en pantalla; es solo un método conveniente para extraer los datos de píxeles de una imagen . Solo necesitamos hacer esto una vez, así que lo haremos por adelantado y luego pasaremos los datos de imagen extraídos a nuestra renderScene
función.
// 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);
}
Podemos usar imágenes de máscaras más complejas para lograr formas como este efecto de tierra:
O incluso para representar texto:
Eso es un envoltorio
He usado estas técnicas de mapeo esférico en varios lugares como la base de obras maestras de WebGL. Ojalá te inspiren a hacer lo mismo. Si te ha gustado este artículo o te ha ayudado de alguna manera, ¡házmelo saber! Mi sitio web está aquí .