Oszałamiające kule kropkowe z WebGL

Piękne, interaktywne globusy WebGL znalazły się ostatnio w centrum uwagi, a zarówno Stripe , jak i GitHub prezentują je w widocznym miejscu na swoich stronach głównych. Każdy później napisał post na blogu o tym, jak to zrobił (Stripe jest tutaj, a GitHub tutaj , jeśli jesteś ciekawy).
Oba globusy składają się głównie z kropek, co skłoniło mnie do zastanowienia się nad różnymi sposobami rozmieszczenia kropek na powierzchni kuli. Pakowanie sfer jest złożoną zagadką, nad którą aktywnie zastanawiają się matematycy, więc na potrzeby tego artykułu ograniczyłem się do przedstawienia kilku podstawowych podejść i sposobów ich osiągnięcia w WebGL.
Ustawianie sceny
Zanim przejdziemy dalej, musimy stworzyć podstawową scenę WebGL, w której będziemy konstruować kulę. Używam Three.js jako de facto wybranego frameworka do interakcji z WebGL API. Postaram się, aby fragmenty kodu w tym artykule były jak najbardziej zwięzłe i trafne; eksploruj dowolne z osadzonych piaskownic, aby uzyskać pełny kod.
Po utworzeniu sceny tworzymy dotGeometries
tablicę, która docelowo będzie zawierała geometrie dla wszystkich naszych kropek. Następnie tworzymy pusty wektor, punkt 3D w przestrzeni wewnątrz sceny, którego pozycja będzie zmieniana za każdym razem, gdy tworzymy kropkę.
// 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);
Podstawowe podejście
Najłatwiejszym sposobem dodania kropek do kuli jest po prostu zdefiniowanie liczby linii szerokości i długości geograficznej, które ma mieć kula, a następnie rozmieszczenie kropek wzdłuż nich. Należy tutaj zwrócić uwagę na kilka ważnych rzeczy.
Najpierw definiujemy phi
i theta
kąty dla każdej kropki. Kąty te tworzą część sferycznego układu współrzędnych, systemu służącego do dokładnego określania położenia punktu w przestrzeni 3D w odniesieniu do jego początku (który w naszym przypadku jest środkiem naszej kuli).
Po drugie, phi
i theta
oba są mierzone w radianach, a nie w stopniach. Kluczem do tego jest pamiętanie, że w 180º jest π radianów . Więc aby znaleźć phi
tutaj wszystko, co musimy zrobić, to podzielić π przez liczbę linii szerokości geograficznej. Ale aby znaleźć theta
, musimy podzielić 2 * π
przez liczbę linii długości geograficznej, ponieważ chcemy, aby nasze linie długości geograficznej rozciągały się wokół pełnych 360º kuli.
// 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);
}
}
Jeśli wejdziesz w interakcję z kulą, aby ją obrócić, zauważysz, że pierścienie na górze i na dole są znacznie gęściej upakowane niż te pośrodku. Dzieje się tak, ponieważ nie zmieniliśmy liczby kropek na każdej linii szerokości geograficznej. W tym miejscu pojawia się pakowanie sferyczne.
Podejście filotaksji
Jeśli kiedykolwiek spojrzałeś na główkę słonecznika lub podstawę szyszki, zauważyłeś niezwykły i charakterystyczny wzór. Ten wzór, stworzony przez układ oparty na ciągu Fibonacciego , jest znany jako filotaksja. Możemy go tutaj użyć do ustawienia naszych kropek w taki sposób, aby wyglądały na znacznie bardziej równomiernie rozmieszczone na powierzchni kuli.
Tym razem zamiast określać liczbę linii szerokości i długości geograficznej, po prostu określamy całkowitą liczbę kropek, które chcemy, aby pojawiły się na kuli. Zamiast zapętlać się wzdłuż linii szerokości geograficznej, kropki będą renderowane jako pojedyncza, ciągła spirala od jednego bieguna kuli do drugiego.
// 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);
...
}
To jest o wiele bardziej satysfakcjonujące. Ale co, jeśli chcemy upakować kropki tak równomiernie, jak to możliwe, ale nadal mamy swobodę definiowania liczby linii szerokości geograficznej?
Podejście liniowe
Tym razem zdefiniujemy wymaganą liczbę linii szerokości geograficznej, ale liczba kropek będzie również skalowana na podstawie obwodu linii szerokości geograficznej, na której się znajdują. Aby zapewnić nam większą kontrolę nad odstępami, zdefiniujemy również parametr gęstości kropek.
Trudną częścią jest tutaj obliczenie promienia każdej linii szerokości geograficznej. Kiedy już to mamy, stosunkowo łatwo jest dowiedzieć się, ile kropek wyświetlić na nim, a następnie znaleźć phi
i theta
dla każdego w podobny sposób, jak w przypadku pierwszego podejścia.
// 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;
...
}
}
Omówiliśmy więc sposób wyświetlania kropek na kuli. Ale co z osiąganiem bardziej złożonych efektów?
Maskowanie kształtu
Zastanawianie się, jak wyświetlać kropki w coraz bardziej skomplikowanych wzorach, może szybko doprowadzić do matematycznego bólu głowy. Jednak stosując jeden z powyższych układów pakowania w połączeniu z obrazem maski możemy osiągnąć niezwykłe efekty.
Aby to zrobić, najpierw musimy utworzyć element płótna HTML i narysować na nim nasz obraz maski. Ten element w rzeczywistości nie będzie renderowany na ekranie; jest to po prostu wygodna metoda wyodrębniania danych pikseli z obrazu . Musimy to zrobić tylko raz, więc zrobimy to z góry, a następnie przekażemy wyodrębnione dane obrazu do naszej renderScene
funkcji.
// 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);
}
Możemy użyć bardziej złożonych obrazów masek, aby uzyskać kształty takie jak ten efekt ziemi:
Lub nawet renderować tekst:
To jest okład
Użyłem tych technik mapowania sferycznego w różnych miejscach jako podstawy wizytówek WebGL. Mam nadzieję, że zainspirują Cię do zrobienia tego samego. Jeśli podobał Ci się ten artykuł lub pomógł Ci w jakiś sposób, daj mi znać! Moja strona jest tutaj .