Płyty tektoniczne — geometria sferyczna i prawdziwe przemieszczenie

Mapowanie płyt skorupy ziemskiej na sferze za pomocą map przemieszczeń i płynnych animacji granic. Three.js, geometria sferyczna i więcej trygonometrii, niż się spodziewałem.

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.