Wyzwanie: globus, który się porusza
Symulacja płyt tektonicznych miała jasny cel wizualny: globus Ziemi z widocznymi granicami płyt, które powoli dryfują, zderzają się i subdukują w czasie rzeczywistym. Proste do opisania. Zaskakująco trudne do zaimplementowania.
Główna trudność nie leży w mechanice płyt — leży w geometrii sfery. Wszystko, co jest łatwe na płaskiej płaszczyźnie, staje się umiarkowanie kłopotliwe na sferze: odległości, interpolacja, mapowanie UV, obliczanie normalnych po przemieszczeniu.
Poprawne zbudowanie sfery
Three.js dostarcza SphereGeometry, co jest w
porządku, ale ma problem z osobliwością biegunów: współrzędne UV
ściskają się na biegunie północnym i południowym, powodując szwy
tekstur i zniekształcenia. Dla globusa jest to widoczne jako
brzydkie artefakty wokół Antarktydy i Arktyki.
Standardowym rozwiązaniem jest użycie cube-sphere — zacznij od sześcianu, którego ściany są podzielone na siatkę, a następnie rzutuj wszystkie wierzchołki na sferę jednostkową. To rozkłada wierzchołki znacznie bardziej równomiernie.
// Rzutowanie wierzchołka sześcianu na sferę
function cubeToSphere(x, y, z) {
const x2 = x * x, y2 = y * y, z2 = z * z;
return {
x: x * Math.sqrt(1 - y2/2 - z2/2 + y2*z2/3),
y: y * Math.sqrt(1 - z2/2 - x2/2 + z2*x2/3),
z: z * Math.sqrt(1 - x2/2 - y2/2 + x2*y2/3)
};
}
Mapy przemieszczeń dla topografii
Wysokość terenu jest sterowana mapą przemieszczeń — teksturą w skali szarości, gdzie biel = wysoka elewacja, czerń = dno oceaniczne. W shaderze wierzchołków każdy wierzchołek jest wypychany na zewnątrz wzdłuż swojej normalnej o wartość proporcjonalną do wartości tekstury:
// Fragment shadera wierzchołków
float height = texture2D(displacementMap, vUv).r;
vec3 displaced = position + normal * height * displacementScale;
gl_Position = projectionMatrix * modelViewMatrix * vec4(displaced, 1.0);
Po przemieszczeniu normalne trzeba przeliczyć — przemieszczone
normalne wskazują w złym kierunku i dają nieprawidłowe
oświetlenie. Three.js potrafi obliczyć normalne w przestrzeni
stycznej z normalMap, ale dla proceduralnego
przemieszczenia zrobiłem to analitycznie w shaderze, używając
pochodnych cząstkowych.
Granice płyt i dryf
Każdej z ~15 głównych płyt Ziemi przypisany jest wektor prędkości kątowej (rzeczywiste wartości geologiczne, przeskalowane w dół o 10⁸ dla użytecznej prędkości wizualnej). Przynależność każdego teksela do płyty jest przechowywana w osobnej teksturze maski.
W każdej klatce obracam region tekstury każdej płyty o jej prędkość kątową za pomocą macierzy rotacji w shaderze fragmentów. Granice płyt są podświetlane przez obliczanie gradientu tekstury ID płyty — ostry gradient wartości oznacza granicę.
Pułapka trygonometrii
Geometria sferyczna ma reputację subtelnej i zasłużenie. Spędziłem większość popołudnia, debugując, dlaczego moje obliczenia odległości są lekko niepoprawne. Problem: obliczałem odległości euklidesowe między parami długości/szerokości geograficznej zamiast odległości po wielkim okręgu.
Na sferze najkrótsza droga między dwoma punktami to łuk wielkiego okręgu. Formuła haversine oblicza to poprawnie:
function haversineDistance(lat1, lon1, lat2, lon2, R = 6371) {
const dLat = (lat2 - lat1) * Math.PI / 180;
const dLon = (lon2 - lon1) * Math.PI / 180;
const a = Math.sin(dLat/2)**2 +
Math.cos(lat1 * Math.PI/180) *
Math.cos(lat2 * Math.PI/180) *
Math.sin(dLon/2)**2;
return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a));
}
Wypróbuj symulację pod adresem /tectonic-plates/. Możesz przełączać między realistyczną a przyspieszoną prędkością dryfu.