Виклик: глобус, що рухається
Симуляція тектонічних плит мала чітку візуальну мету: глобус Землі з видимими межами плит, які повільно дрейфують, зіштовхуються та занурюються одна під одну в реальному часі. Просто описати. Дивно складно реалізувати.
Головна складність — не в механіці плит, а в геометрії сфери. Усе, що легко на плоскій площині, стає помірно незручним на сфері: відстані, інтерполяція, 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/. Можна перемикати між реалістичною та прискореною швидкістю дрейфу.