Доброго времени дня или ночи. В свободное время я занимаюсь разработкой игры на космическую тематику на 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/
На будущее есть идеи написать статьи про карту звездной системы, про генератор туманностей, генераторы различных объекты, и вообще на тему фантастического космоса :) Если вам это будет интересно — пишите.
Напоследок под спойлером ещё парочка скриншотов.
Автор: Magistr_AVSH