Тектонічні плити — сферична геометрія та реальне зміщення

Накладання земних тектонічних плит на сферу за допомогою карт зміщення та плавної анімації меж. Three.js, сферична геометрія та значно більше тригонометрії, ніж я очікував.

Виклик: глобус, що рухається

Симуляція тектонічних плит мала чітку візуальну мету: глобус Землі з видимими межами плит, які повільно дрейфують, зіштовхуються та занурюються одна під одну в реальному часі. Просто описати. Дивно складно реалізувати.

Головна складність — не в механіці плит, а в геометрії сфери. Усе, що легко на плоскій площині, стає помірно незручним на сфері: відстані, інтерполяція, UV-розгортка, обчислення нормалей після зміщення.

Правильна побудова сфери

Three.js постачає SphereGeometry, яка загалом непогана, але має проблему полюсної сингулярності: UV-координати стягуються на північному та південному полюсах, викликаючи шви текстури та спотворення. Для глобуса це виглядає як потворні артефакти навколо Антарктиди та Арктики.

Стандартне рішення — використати куб-сферу: почати з куба, грані якого поділені на сегменти, а потім спроєктувати всі вершини на одиничну сферу. Це розподіляє вершини набагато рівномірніше.

// Проєкція вершини куба на сферу
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)
  };
}

Карти зміщення для рельєфу

Висота рельєфу керується картою зміщення — текстурою у відтінках сірого, де білий = висока висота, чорний = океанічне дно. У вершинному шейдері кожна вершина зсувається назовні вздовж своєї нормалі на величину, пропорційну значенню текстури:

// Фрагмент вершинного шейдера
float height = texture2D(displacementMap, vUv).r;
vec3 displaced = position + normal * height * displacementScale;
gl_Position = projectionMatrix * modelViewMatrix * vec4(displaced, 1.0);

Після зміщення нормалі потрібно перерахувати — зміщені нормалі вказують у неправильному напрямку та дають некоректне освітлення. Three.js вміє обчислювати нормалі у дотичному просторі з карти normalMap, але для процедурного зміщення я зробив це аналітично прямо в шейдері, використовуючи часткові похідні.

Межі плит і дрейф

Кожній з приблизно 15 великих плит Землі присвоюється вектор кутової швидкості (реальні геологічні значення, зменшені у 10⁸ разів для придатної для перегляду візуальної швидкості). Належність кожного текселя до плити зберігається в окремій масковій текстурі.

На кожному кадрі я обертаю текстурну область кожної плити на її кутову швидкість за допомогою матриці обертання у фрагментному шейдері. Межі плит підсвічуються обчисленням градієнта текстури ідентифікаторів плит — різкий перепад значення означає межу.

Пастка тригонометрії

Сферична геометрія має репутацію підступної — і заслужено. Я провів більшу частину одного дня, з'ясовуючи, чому мої обчислення відстані трохи хибні. Проблема була в тому, що я обчислював евклідові відстані між парами довгота/широта замість відстаней по дузі великого кола.

На сфері найкоротший шлях між двома точками — це дуга великого кола. Формула гаверсинуса обчислює це коректно:

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));
}

Спробуйте симуляцію за адресою /tectonic-plates/. Можна перемикати між реалістичною та прискореною швидкістю дрейфу.