Atemberaubende Punktkugeln mit WebGL
Schöne, interaktive WebGL-Globen haben in letzter Zeit einen Moment im Rampenlicht gehabt, da sowohl Stripe als auch GitHub sie prominent auf ihren Homepages präsentieren. Jeder schrieb später einen Blogbeitrag darüber, wie sie das gemacht haben (Stripe ist hier und GitHub ist hier, wenn Sie neugierig sind).
Beide Globen bestehen hauptsächlich aus Punkten, was mich dazu brachte, über die verschiedenen Möglichkeiten nachzudenken, wie Punkte über die Oberfläche einer Kugel verteilt werden können. Das Packen von Kugeln ist ein komplexes Puzzle, das von Mathematikern aktiv erwogen wird. Für die Zwecke dieses Artikels habe ich mich darauf beschränkt, einige grundlegende Ansätze darzulegen und zu zeigen, wie sie in WebGL erreicht werden können.
Einrichten der Szene
Bevor wir fortfahren, müssen wir eine grundlegende WebGL-Szene einrichten, in der die Kugel konstruiert wird. Ich verwende Three.js als De-facto-Framework der Wahl für die Interaktion mit der WebGL-API. Ich werde versuchen, die Codeschnipsel in diesem Artikel so prägnant und relevant wie möglich zu halten; Durchsuchen Sie eine der eingebetteten Sandboxes nach dem vollständigen Code.
Nachdem wir eine Szene erstellt haben, erstellen wir ein dotGeometriesArray, das schließlich die Geometrien für alle unsere Punkte enthält. Dann erstellen wir einen leeren Vektor, einen 3D-Punkt im Raum innerhalb der Szene, dessen Position jedes Mal neu zugewiesen wird, wenn wir einen Punkt erstellen.
// 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);
Der grundlegende Ansatz
Der einfachste Weg, Punkte zu einer Kugel hinzuzufügen, besteht darin, einfach die Anzahl der Längen- und Breitengradlinien zu definieren, die die Kugel haben soll, und dann Punkte entlang dieser zu verteilen. Hier gibt es ein paar wichtige Dinge zu beachten.
Zuerst definieren wir die Winkel phiund thetafür jeden Punkt. Diese Winkel sind Teil des sphärischen Koordinatensystems, ein System zur genauen Definition, wo sich ein Punkt im 3D-Raum in Bezug auf seinen Ursprung (in unserem Fall der Mittelpunkt unserer Kugel) befindet.
Zweitens werden phiund thetabeide im Bogenmaß gemessen, nicht in Grad. Der Schlüssel dazu ist, sich daran zu erinnern, dass es π Bogenmaß in 180º gibt . Um hier zu finden phi, müssen wir also nur π durch die Anzahl der Breitengrade dividieren. Aber um zu finden theta, müssen wir 2 * πdurch die Anzahl der Längengrade dividieren, weil wir möchten, dass unsere Längengrade um die vollen 360º der Kugel verlaufen.
// 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);
}
}
Wenn Sie mit der Kugel interagieren, um sie zu drehen, werden Sie feststellen, dass die Ringe oben und unten viel dichter gepackt sind als die in der Mitte. Dies liegt daran, dass wir die Anzahl der Punkte auf jeder Breitengradlinie nicht variiert haben. Hier kommt die Kugelpackung ins Spiel.
Der Phyllotaxis-Ansatz
Wenn Sie jemals den Kopf einer Sonnenblume oder die Basis eines Tannenzapfens betrachtet haben, werden Sie ein ungewöhnliches und unverwechselbares Muster bemerkt haben. Dieses Muster, das durch eine auf der Fibonacci-Folge basierende Anordnung entsteht , ist als Phyllotaxis bekannt. Wir können es hier verwenden, um unsere Punkte so zu positionieren, dass sie viel gleichmäßiger über die Oberfläche der Kugel verteilt erscheinen.
Anstatt die Anzahl der Breiten- und Längenlinien zu definieren, definieren wir dieses Mal einfach die Gesamtzahl der Punkte, die auf der Kugel erscheinen sollen. Anstatt die Breitengradlinien zu durchlaufen, werden die Punkte in einer einzigen, kontinuierlichen Spirale von einem Pol der Kugel zum anderen gerendert.
// 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);
...
}
Das ist viel befriedigender. Was aber, wenn wir die Punkte möglichst gleichmäßig packen möchten, aber dennoch die Freiheit haben, die Anzahl der Breitengradlinien zu definieren?
Der lineare Ansatz
Dieses Mal definieren wir die Anzahl der erforderlichen Breitengradlinien, aber die Anzahl der Punkte wird auch basierend auf dem Umfang der Breitengradlinie skaliert, auf der sie positioniert sind. Um uns eine bessere Kontrolle über den Abstand zu geben, definieren wir auch einen Parameter für die Punktdichte.
Der fummelige Teil hier ist die Berechnung des Radius jeder Breitengradlinie. Sobald wir das haben, ist es relativ einfach herauszufinden, wie viele Punkte darauf angezeigt werden sollen, und dann phiund thetafür jeden auf ähnliche Weise wie beim ersten Ansatz zu finden.
// 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;
...
}
}
Wir haben also behandelt, wie die Punkte auf der Kugel angezeigt werden. Aber was ist mit komplexeren Effekten?
Formmaskierung
Herauszufinden, wie man die Punkte in immer komplizierteren Mustern darstellt, kann schnell zu mathematischen Kopfschmerzen führen. Durch Verwendung einer der obigen Packungsanordnungen in Kombination mit einem Maskenbild können wir jedoch einige außergewöhnliche Effekte erzielen.
Dazu müssen wir zunächst ein HTML-Canvas-Element erstellen und unser Maskenbild darauf zeichnen. Dieses Element wird nicht wirklich auf dem Bildschirm gerendert; es ist nur eine bequeme Methode, um die Pixeldaten aus einem Bild zu extrahieren . Wir müssen dies nur einmal tun, also tun wir es im Voraus und übergeben dann die extrahierten Bilddaten an unsere renderSceneFunktion.
// 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);
}
Wir können komplexere Maskenbilder verwenden, um Formen wie diesen Erdeffekt zu erzielen:
Oder sogar um Text zu rendern:
Das ist ein Wickel
Ich habe diese sphärischen Mapping-Techniken an verschiedenen Stellen als Grundlage für WebGL-Showpieces verwendet. Hoffentlich inspirieren sie Sie dazu, dasselbe zu tun. Wenn Ihnen dieser Artikel gefallen hat oder er Ihnen in irgendeiner Weise geholfen hat, lassen Sie es mich bitte wissen! Meine Website ist hier drüben .

![Was ist überhaupt eine verknüpfte Liste? [Teil 1]](https://post.nghiatu.com/assets/images/m/max/724/1*Xokk6XOjWyIGCBujkJsCzQ.jpeg)



































