Pull to refresh

Карта галактики на Three.js/WebGL

Reading time 5 min
Views 19K

Доброго времени дня или ночи. В свободное время я занимаюсь разработкой игры на космическую тематику на Three.js/WebGL и задумал написать небольшую серию статей по некоторым компонентам игры, в этой статье мы поговорим о карте галактики. Рассказ пойдет уже привычным мне способом — по шагам.

Я не буду приводить код инициализации и подробности самого Three.js, в сети полно информации по этому поводу.
И наш первый шаг…

Шаг 1 — Черный-черный фон


Для начала нам надо сделать подложку. Делается это элементарно в одну строчку:
renderer.setClearColor(0x000000);

Это будет выглядеть так:

Очевидно это Прямоугольник Малевича и на этом мы первый шаг заканчиваем.
Ведь правда просто?

Шаг 2 — And the Sky Full of Stars


Прямоугольник Малевича — это просто замечательно, но надо всё таки добавить звезды.
Так как мы делаем карту галактики, нам нужна спираль похожая на галактику. Я выбрал логарифмическую спираль.
Сначала записываем все нужные нам переменные
//переменные для построения логарифмической спирали
var countStars = 20000;
var a = 1.1;
мar b = 0.17;
var windings = 3.7;
var tMax = 2.0 * Math.PI * windings;        
var drift = 0.3

Записываем алгоритм, который очень прост: проходимся в цикле и высчитываем координаты каждой звезды(формула в википедии) и немного рандомно их смещаем, чтобы было больше похоже на галактику.
//Строим логарифмическую спираль
for (var i = 0; i < countStars; i++) {
    //формула + рандомное смещение точек
    var t = tMax * Math.random();
    var x = a * Math.exp(b * t) * Math.cos(t);
    x = x + (drift*x*Math.random()) - (drift*x*Math.random());
    var y = a * Math.exp(b * t) * Math.sin(t);
    y = y + (drift*y*Math.random()) - (drift*y*Math.random())
    //Зеркально равномерно распределяем точки
    if (Math.random() > 0.5) {
        list.push({x:vec.x, y:vec.y});
    }
    else { //Отражение спирали
        list.push({x:-vec.x, y:-vec.y});
    }
}

Так как количество точек очень большое, их мы оформляем системой частиц, лагов будет куда меньше:
//геометрия
var geometry = new THREE.Geometry();
//Материал системы частиц
var material = new THREE.ParticleSystemMaterial({
      color: 0xeeeeee,
      size: 3
});
//Система частиц
var particleSystem = new THREE.ParticleSystem(
      geometry,
        material
);
//Добавляем звезды
for (var i = 0; i < list.length; i++) {
    addStar(list[i].x, list[i].y);  
}
scene.add(particleSystem);

Функция addStar на данном этапе:
var addStar = function(x, y) {
    var v = new THREE.Vector3();
    v.x = x * 10;
    v.y = y * 10;

    geometry.vertices.push(v);  
}

И у нас получилось…

Что-то в центре какая-то дырка, давайте поглядим поближе:

Отвратительно, но исправить не сложно, добавим два цикла.
Первым циклом генерим кольцо из точек:
for (var i = 0; i < 4000; i++) {
    var vec = {x:Math.sRandom(0.8, 1.7),y:0};
    var angle = Math.sRandom(0, Math.PI*2.5);
    vec = VectorRot(vec, angle);
     list.push({x:vec.x, y:vec.y});
}


Вторым циклом генерим круг из точек:
for (var i = 0; i < 4000; i++) {
    var vec = {x:Math.sRandom(0.001, 0.8),y:0};
    var angle = Math.sRandom(0, Math.PI*2.5);
    vec = VectorRot(vec, angle);
    list.push({x:vec.x, y:vec.y});
}


Отлично, у нас уже есть что-то похожее на галактику. Но нам нужны имена нашим звездам. У тебя %username% есть имя, а у звезды нет. Разве справедливо?

Шаг 3 — Звезда по имени %starname%


Ну давайте делать последовательно.
Нам нужно сделать функцию для генерирования названия. Нужен глобальный список звезд(координаты + название). Нужно модифицировать добавление звезд и включать туда генерирование название. Это по самому наличию названий. Также нужно функция для вывода названия звезды при событии mouseover. Проблема в том, что так как это система частиц просто так событие не повесить, значит нужно что-то придумать другое. Вариантов много, но я сделал следующее: нашел JS реализацию KDTree, и загнал в дерево все точки которые у нас есть(т.е. тот самый глобальный список), и написал в обработчике события mousemove следующее:
//класический способ перевода координат мыши в мировые координаты
var projector = new THREE.Projector();
var vector = new THREE.Vector3(
( e.pageX / window.innerWidth ) * 2 - 1,
- ( e.pageY / window.innerHeight ) * 2 + 1,
0.5 );

var pos = projector.unprojectVector( vector, e.data.self.camera );
//дальше пересоздаем отдельную сцену для названия
e.data.self.sceneNames = new THREE.Scene();
//Вытаскиваем из KDTree ближайшии звезды от позиции мыши
var items = e.data.self.tree.nearest({x:pos.x,y:pos.y}, 1, 100);
//Далее создаем собственно label
for (var i = 0; i < items.length; i++) {
    e.data.self.sceneNames.add(e.data.self.labelBasic(items[i][0].name,vector.x, vector.y, 60, "#f00"));
}

Каждый раз пересоздавать меш не слишком конечно оптимально, но производительности вполне хватает.
Весь код я приводить не буду, кто захочет сможет в конце зайти на гитхаб и поглядеть, а мы перед следующим шагом поглядим на картинки:


Шаг 4 — Вносим порядок в хаос


Итак, у нас уже есть галактика, названия звезд, мы можем их увидеть, но раз у нас карта, то нам нужно разбиение пространства. Если я скажу: «слухай, лети ка ты в систему TX-82 и купи мне кефира», то будет непонятно куда лететь, ибо а) не факт что система с названием TX-82 единственная, б) как найти систему среди over 20k звезд? в) не факт что кефир уже завезли.
Сделаем такое разбивку: есть квадранты, есть сектора. Вся галактика делится на 4*4=16 квадрантов, по 4 с каждой стороны. Каждый квадрант, в свою очередь, делиться на 4 сектора. Т.е. мы можем адресовать систему как квадрант #qX-qY — сектор (sx-sy) — система %starname%.
Делаем мы это банальными линиями, код опять же не привожу, он большой и не интересный — просто расчет координат начала и конца каждой линии. Кому интересно — добро пожаловать на гитхаб.
Результат, как вы могли догадаться, в шапке статьи находится. Но я приведу ещё одну картинку:

Только нужно ещё добавить обозначения — текст, вроде (1-1),(3-3),(2-3). Сетка есть, а обозначений нет. Добавляем.

Шаг 5 — Чип и Дейл


Спасем от непонимания, что это за числа выше на картинке. Или хотя бы попытаемся. Да. Две строчки HTML и CSS:
<span id="quad" style="position:absolute;left:100px;top:10px;color:white;font-family:Arial;font-size:19px">#x-y Quadrant</span>
<span id="sector" style="position:absolute;left:100px;top:30px;color:#555;font-family:Arial;font-size:19px">(sx-sy) Sector</span>


Шаг 6 — Где я?


И последнее что нам осталось — указать наше положении в галактики. Стукнул кирпич по голове, забыл где находишся. А нам ведь нужно сказать куда привезти кефир, что же нам делать? Открываем карту и спасибо технологиям:

Да, код такой:
//Добавить маркер
var addMarker = function(x, y) {
    //геометрия
    var g = new THREE.Geometry();
    //Материал системы частиц
    var m = new THREE.ParticleBasicMaterial({
          color: 0x550000,
          size: 35
    });
    for (var i = 0; i < 100; i++) {
        g.vertices.push({x:x,y:y});
    };
    
    //Система частиц
    var p = new THREE.ParticleSystem(
          g,
          m
    );

    this.sceneLabel.add(this.labelBasic(">>                ", x , y , 70, "#f00"));
    this.sceneLabel.add(this.labelBasic("                <<", x , y , 70, "#f00"));

    this.sceneLabel.add(this.labelBasic(this.points[this.here].name, x , y , 60, "#f00"));

    this.sceneLabel.add(p);
}

Заключение


Итак, мы сделали карту. Я не привел в статье многие вещи, например смещение карты по зажатой клавише, зум, не сильно детализировал работу с Three.js, на мой взгляд это вторично и не так интересно.
Гитхаб: github.com/MagistrAVSH/galmap
Демо работает в последних FF, Chrome, Opera. В IE11 работать будет плохо, вы не увидите надписей вообще, он криво поддерживает WebGL. magistravsh.github.io/galmap

На будущее есть идеи написать статьи про карту звездной системы, про генератор туманностей, генераторы различных объектов, и вообще на тему фантастического космоса :) Если вам это будет интересно — пишите.

Напоследок под спойлером ещё парочка скриншотов.
Скриншоты


Tags:
Hubs:
+14
Comments 11
Comments Comments 11

Articles