Доброго времени дня или ночи!
В наших краях как всегда зима пришла неожиданно, но в космосе времен года нет, так что снова поговорим о различных космических штуках на webgl. Предыдущую статью о карте галактики можно прочитать здесь. Сегодня же речь пойдет про карта звездной системы.
Как всегда, рассказ пойдет по шагам. Итак…
Шаг 0 — Что мы хотим сделать?
Карта звездной системы — какая информация в ней должна быть? Ну первое — это звезда. Второе — планеты, третье — луны.
Добавим и фантастический элемент — Jumpgate, или Прыжковые Врата или в среде космосимщиков их зовут просто гейтами. Классический элемент из космической фантастики, существующий для быстрого перемещения на большие расстояния.
Также сделаем маркер нашего положения в звездной системе.
Ещё несколько условностей: ничего раскрашивать не будем, пусть почти всё будет черно-белое. Второе — не будем усложнять и добавлять вращение планет и лун. Это можно сделать очень легко, но на мой взгляд не в тему.
Шаг 1 — Звезда и фон
Снова рисуем Прямоугольник Малевича:
renderer.setClearColor(0x000000);
Нарисуем также схематичную звезду, просто белый круг.
Нарисовать круг легко, а вот разместить сложнее. Я обычно не описываю подробности работы c Three.js, но здесь нужно упомянуть эту деталь, вдруг ещё кто-то столкнется.
Дело в том, что нужно чтобы при приближении камеры объекты не увеличивались визуально(так как при приближении получается мазня из пиксельарта), но расстояния между точками визуальны были больше(например есть очень близкое скопление объектов и нам нужно поглядеть каждый в отдельности). Объект THREE.Mesh такой возможности не дает, здесь лучше, на мой взгляд, использовать THREE.ParticleSystem.
Приведу полный листинг функции с комментариями:
this.addStar = function(pos) {
//Задаем радиус круга
var radius = 50;
//Создаем канвас в ширину и в высоту равный диаметру круга
var canvas = document.createElement('canvas');
canvas.width = radius*2;
canvas.height = radius*2;
//Задаем хар-ки круга и рисуем
var ctx = canvas.getContext('2d');
ctx.fillStyle = "#fff";
ctx.beginPath();
ctx.arc(radius,radius,radius,0,2*Math.PI);
ctx.fill();
//Создаем текстуру на основе канваса
var texture = new THREE.Texture(canvas)
texture.needsUpdate = true;
//Создаем точку расположения звезды
var vertex = new THREE.Vector3();
vertex.x = pos.x;
vertex.y = pos.y;
vertex.z = 0;
//Создаем геометрию
var geometry = new THREE.Geometry();
//В список вершин геометрии заносим выше созданную точку
geometry.vertices.push( vertex );
//Создаем материал
material = new THREE.ParticleBasicMaterial( { size: 215, map: texture, transparent: true } );
//Создаем систему частиц и добавляем её в сцену
particles = new THREE.ParticleSystem( geometry, material );
particles.sortParticles = true;
this.scene.add(particles);
}
Аналогично добавляются все остальные объекты звездной системы.
Шаг 2 — Планеты
Рисуем маркеры планет, особого придумывать ничего не будем, сделаем простой круг с обводкой.
var radius = 15;
//Задаем хар-ки и рисуем
var ctx = canvas.getContext('2d');
ctx.strokeStyle = "#666";
ctx.fillStyle = "#444";
ctx.lineWidth = 3;
ctx.beginPath();
ctx.arc(radius,radius,radius-2,0,2*Math.PI);
ctx.fill();
ctx.stroke();
Просто точка не интересна, нужны ещё орбиты. Построим их руками обычным алгоритмом построения окружностей, нужно только чуть-чуть вспомнить геометрию с тригонометрией.
this.addOrbit = function(pos, radius)
{
//Задаем размер сегмента.
var resolution = 170;
var size = 360 / resolution;
//Геометрия и материал
var geometry = new THREE.Geometry();
var material = new THREE.LineBasicMaterial( { color: 0x777777} );
for(var i = 0; i <= resolution; i++) {
//Вычисляем угол сегмента, т.е. наш размер * смещение * 1 радиан.
var segment = ( i * size ) * Math.PI / 180;
//Вычисляем кординаты как косинуссинус угла нашего сегмента, что есть координата хy для единичного радиуса, и умножаем на радиус, т.е. отдаляем координату.
geometry.vertices.push( new THREE.Vertex( new THREE.Vector3( Math.cos( segment ) * radius, Math.sin( segment ) * radius ), 0 ) );
}
//Создаем линию и устанавливаем её позицию
var line = new THREE.Line( geometry, material );
line.position.x = pos.x;
line.position.y = pos.y;
this.scene.add(line);
return line;
}
Уже лучше, но улучшим ещё. Добавляем названия планетам в формате "<название звездной системы> Planet <номер планеты от звезды>". Функцию добавления текста не привожу, она не интересная. Те же самые системы частиц и канвас в который отрисовывается текст.
Шаг 3 — Луны
Здесь всё просто, код рисования аналогичный, просто ставить позицию не в центр, а на определенную планету + смещение от планеты:
var vertex = new THREE.Vector3();
vertex.x = planetPos.x + pos.x;
vertex.y = planetPos.y + pos.y;
vertex.z = 0;
Плюс также вешаем орбиту:
//Ставим центр окружности на позицию планеты, а радиус задаем как длину вектора позиции луны
moon.orbit = this.addOrbit(planetPos, vectorLength(pos));
Шаг 4 — Jumpgates
На картинке для примера Jumpgate из сериала Вавилон-5.
Пока что мы сами собственный гейт рисовать не будем, а просто сделаем красный маркер-треугольник, и повесим название.
//Задаем хар-ки и рисуем
var ctx = canvas.getContext('2d');
ctx.strokeStyle = "#333";
ctx.fillStyle = "#f00";
ctx.lineWidth = 7;
ctx.beginPath();
ctx.moveTo(len/2, 0);
ctx.lineTo(0, len);
ctx.lineTo(len, len);
ctx.closePath();
ctx.stroke();
ctx.fill();
Но просто так треугольник не нужен, у нас же карта, т.е. нужно нарисовать связи между гейтами. Прыгнули в один — в каком месте пространства мы окажемся? Вот и карта пригодится.
Построить связи не сложно, но мы немножко усложним.
Сделем следующую структуру:
jgs:{
0: {type:"jg", names:
[
"Jumpgate Main Shipyard",
], id: 0, pos: {x:500, y:-50}, link: 1},
1: {type:"jg", names:
[
"Jumpgate Solar Plants"
], id: 1, pos: {x:-440, y:400}, link: 2},
2: {type:"jg", names:
[
"Jumpgate Trade Station"
], id: 2, pos: {x:200, y:-130}, link: 0},
3: {type:"jg", names:
[
"Jumpgate Ore Mining"
], id: 3, pos: {x:-300, y:-280}, link: 1},
4: {type:"jg", names:
[
"Jumpgate Cross"
], id: 4, pos: {x:-640, y:10}, link: 2}
},
У нас везде поле link — оно указывает на id, на который идет связь.
//Строим связи между вратами
for (var v in list) {
//Строим связь между двумя гейтами(туда и обратно)
list[data.jgs[v].id].link.push(this.addLine(data.jgs[v].pos, data.jgs[data.jgs[v].link].pos));
list[data.jgs[v].link].link.push(this.addLine(data.jgs[v].pos, data.jgs[data.jgs[v].link].pos));
}
Т.е. добавляем линии начиная от позиции одного гейта заканчивая позицией другого. И в обратную сторону тоже, это нам пригодится.
Результат стараний:
Шаг 5 — Добавляем интерактивности
Добавим в функции добавления планет, лун, гейтов подобные строки:
Jumpgate:
this.domEvent.bind(particles, "mouseover", function(e){
e.target.material.color.setHex(0xffffff);
//Подсечиваем линки в одну и обратную сторону
for (var i = 0; i < e.target.link.length; i++) {
e.target.link[i].material.color.setHex(0xffffff);
}
for (var i = 0; i < e.target.link2.length; i++) {
e.target.link2[i].material.color.setHex(0xffffff);
}
//Светим орбиту
e.target.orbit.hover();
});
this.domEvent.bind(particles, "mouseout", function(e){
e.target.material.color.setHex(0xaaaaaa);
//Гасим линки с одной и другой стороны
for (var i = 0; i < e.target.link.length; i++) {
e.target.link[i].material.color.setHex(0xaaaaaa);
}
for (var i = 0; i < e.target.link2.length; i++) {
e.target.link2[i].material.color.setHex(0xaaaaaa);
}
//Гасим орбиту
e.target.orbit.unhover();
});
Луны:
this.domEvent.bind(particles, "mouseover", function(e){
e.target.material.color.setHex(0xffffff);
e.target.orbit.hover();
});
this.domEvent.bind(particles, "mouseout", function(e){
e.target.material.color.setHex(0xaaaaaa);
e.target.orbit.unhover();
});
Планеты:
this.domEvent.bind(particles, "mouseover", function(e){
e.target.material.color.setHex(0xffffff);
e.target.orbit.hover();
});
this.domEvent.bind(particles, "mouseout", function(e){
e.target.material.color.setHex(0xaaaaaa);
e.target.orbit.unhover();
});
Здесь мы добавляем обработчики mouseover, mouseout, и подсвечиваем для планет и лун — орбиты, для врат — связи и орбиты.
Three.js из коробки не поддерживает проверку пересечений с системой частиц, поэтому я погуглил, подумал, и вкрутил в файл three.js, в функцию проверки на пересечения с вершинами системы частиц:
Выглядит так:
Шаг 6 — Где мы?
Добавляем маркер нашего положения в системе. Не будем что-то выдумывать сверхъестественное, и просто добавим квадрат и надпись.
Заключение
Ну вроде как всё что хотели сделали. Конечно можно многое ещё добавить, например, двойные звезды, астероидные пояса, газовые облака, но, как говорится, лучшее враг хорошего. Как всегда весь код на гитхабе, ссылка на демку тут же. Работает в Chrome, Firefox, должен работать в Safari, Опере, и ещё целой кучи Хромо-образных браузеров. IE11 не поддерживается.
Демо: magistravsh.github.io/starsystemmap/
Код: github.com/MagistrAVSH/starsystemmap/
В следующий раз надеюсь уже написать о генерации различных объектов, кораблей, станций, можете чего-то ещё :)
Автор: Magistr_AVSH