WebGL — одна из самых интересных новых технологий, которая способна удивительным образом преобразовать интернет. На базе этой технологии уже создано несколько движков, которые позволяют без лишних усилий создавать удивительные вещи, и наиболее известный из них Three.js. Познакомится с ним было моим давним желанием, и лучший способ сделать это — создать что-нибудь интересное. Первой идей было набросать “воодушевляющую” сцену на Three.js содержащую как большое количество полигонов, источников освещения и частиц, так и имеющую, при этом, какой-то осмысленный контекст. Вскоре, эта идея превратилась в желание создать бесконечный город в который можно было бы погрузиться сквозь браузер.

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

image


Построение дорог


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

1. Строится одна или несколько направляющих дорог из одной или нескольких точек, которые постепенно растут.
2. При росте, с определенной вероятностью, дорога может повернуть на некоторый градус, либо породить другую дорогу растущую ей перпендикулярно с погрешностью в несколько градусов.
3. Как только дорога достигает предельной длины, либо пересекается с другой дорогой, ее рост прекращается.

Выглядит это примерно так:


Результат работы такого алгоритма выглядит весьма естественно, однако имеет несколько серьезных недостатков:

1. Высокая временная сложность: для каждого момента времени построения, нужно пройтись по всем дорогам увеличив их на некую величину. И для каждой отдельной дороги, нужно пройтись по всем недостроенным дорогам, что бы найти возможные пересечения.
2. Невозможно воспроизвести участок дороги, если ключевые точки (из которых начинается построение) оказались за пределами видимости, а значит, необходимо создавать большое количество не нужных в конкретный момент данных.

Эти недостатки не позволяют строить город “на лету”. Поэтому нужно было придумать другой алгоритм, который был бы лишен этих недостатков и при этом давал достаточно схожий результат. Идея заключалась в том, чтобы не “выращивать” полотно дорог, а построить массив точек — пересечений дорог и уже после соединить их линиями. Детально алгоритм выглядит следующим образом:

1. Строится полотно равноудаленных (с некоторой случайной погрешностью) точек, которые будут является центрами нашего города. Для каждой точки определяется размеры и форма в пределах которой будет происходить дальнейшее построение.
2. Для каждой точки, в рамках определенной формы, строится свое полотно равноудаленных (на значительно меньшее расстояние и так же имеющие некоторую погрешность) точек, которые будут являться пересечением дорог.
3. Точки которые стоят слишком близко друг к другу удаляются.
4. Ближайшие точки соединяются.
5. Для каждой точки строится некоторое количество “зданий” равное количеству соединений у точки. (Здание занесено в кавычки, так так по идее это не само здание, а форма в пределах которой это здание может быть построено, с уверенностью, что оно не будет пересекаться с другими зданиями)

Таким образом весь город строится без тяжелого поиска пересечений, и может быть воссоздан из любой начальной точки. Однако алгоритм все же имеет недостатки:

1. Пересечения все же иногда происходят, хотя их вероятность достаточно низка.
2. Иногда возникают обособленные участки города не соединенные трассой с остальным городом.

Выглядит работа алгоритма следующим образом:
image
Красный — радиус просмотра
Желтый — предельный радиус построения


Построение зданий


Что бы ускорить вывод комплексной геометрии в Three.js существует модуль работы с буферной геометрией, который позволят создавать сцены с невероятным количеством элементов (пример). Однако, что бы все работало быстро, все здания необходимо хранить в одном едином меше, а значит и с единым материалом, которому требовалось передать несколько текстур зданий, чтобы хоть немного их разнообразить. И хотя передать массив текстур в шейдер проблемой не является, для этого в three.js существует специальный тип униформы, проблемой оказалось то, что в GLSL ES 1.0 (который используется для компиляции шейдеров в WebGL) нельзя в качестве индекса массива использовать не константу, а значит и использовать переданный номер текстуры для каждого конкретного здания.
Решение нашлось в том, что в качестве индекса можно использовать итератор цикла. Выглядит это примерно так:

const int max_tex_ind = 3; //Максимальное количество текстур
uniform sampler2D a_texture [max_tex_ind]; //Массив текстур
varying int indx; //Индекс используемой текстуры (индекс передается в вертексный шейдер, как параметр для каждой вершины)
...
void main() {
   vec3 tex_color;
   for (int i = 0; i < max_tex_ind; i++) { 
      if (i == indx) { 
         tex_color = texture2D(a_texture[i],uv).xyz;
      }
   }
   ...
}


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

Освещение


Для придания городу большей визуальной привлекательности я решил добавить освещение имитирующее свет уличных фонарей. Конечно для решения этой задачи не подходит стандартное освещение используемое в Three.js количество которого значительно ограничено, в то время, как в среднем на сцене присутствует ~8000 источников освещения. Однако все это освещение равноудалено от основания, а значит и обрабатывать каждую точку в отдельности как источник освещения совсем необязательно, вместо этого можно создать текстуру освещенности, еще на стадии генерации города. Так выглядит такая текстура:

image

Все что остается сделать непосредственно в шейдере, это найти интенсивность отражения света от плоскости и умножить ее на заданную в текстуре освещенность.

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

Плавное построение


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

Что же касается самой генерации, вся она была разделена на множество стадий, при этом для каждой стадии велась статистика (которую можно посмотреть зайдя в консоль браузера) того как много времени процессора занимает та или иная стадия на фрейм, и если это время было больше некоторого значения, стадия делилась на несколько подстадий до тех пор пока это не позволяло достичь стабильно высокого FPS.

Результат


Сама сцена и исходный код: тут
Видео версия:

Комментарии (27)


  1. aml
    29.02.2016 12:30
    +1

    This video is not available.


  1. jonywtf
    29.02.2016 13:11
    +2

    Как бы скорость передвижения увеличить… может скроллом? а то трудно ощутить его бесконечность за пару минут)


    1. AlexKrysin
      29.02.2016 13:14
      +4

      Скорость ограничена, так как при большей скорости, город просто не будет успевать генерироваться.


    1. Smi1e
      29.02.2016 14:26
      +2

      В консоли
      cam_control.speed = 500


  1. VCheese
    29.02.2016 13:52
    +7

    Не по СНиПу :)


  1. k12th
    29.02.2016 13:59

    Мне кажется, если темные окна сделать еще потемнее, то будет реалистичнее выглядеть, нет?


    1. wunderwaffel
      29.02.2016 15:07
      +1

      А ещё уличные фонари стоят друг напротив друга. Насколько мне подсказывает вид из окна, их чередуют — слева, справа, слева, справа...


      1. Lain_13
        01.03.2016 01:32

        И ещё должно быть место под тратуар. :)


        1. dolphin4ik
          01.03.2016 11:57
          +3

          И мусорок и туалетов нету как всегда..


  1. entomolog
    29.02.2016 15:45
    +3

    Спасибо, позновательно!
    На тему бесконечности, когда-то игрался с raymarching, из простейшей геометрии можно создавать "бесконечные" сцены: немного бесконечности


  1. DenimTornado
    29.02.2016 15:52

    На Маке жутко тупит и блинкает небо и что-то похожее на дома.
    Chrome Version 48.0.2564.116 (64-bit)
    MacOS X 10.11.3
    http://take.ms/WGHLh
    http://take.ms/vcTCi


    1. AlexKrysin
      29.02.2016 15:56

      Загляните в chrome://flags/ возможно отключено аппаратное ускорение.


      1. DenimTornado
        29.02.2016 15:58

        http://take.ms/tSS0E
        http://take.ms/ztVD3
        вроде как включено


    1. AterCattus
      01.03.2016 20:28

      В линухе на Хроме тоже лишь точки цветные


  1. Beholder
    29.02.2016 16:23

    Чем-то напомнило старую игру Darker.


  1. Milliard
    29.02.2016 17:36
    +1

    Гифка 35 Мб. Как так можно то?


  1. XanderBass
    01.03.2016 18:14

    Мде, вот и дожили мы до времён, когда SimCity можно запилить в браузере.


  1. Imbolc
    01.03.2016 18:29
    +2

    Не нашёл как стрелять


    1. sim31r
      05.03.2016 23:56

      ctrl+w, осторожно, оружие мощное, уничтожает всё без остатка.


  1. Kempston
    01.03.2016 18:30

    Еще бы добавить сглаживание текстурам, например:
    texture.anisotropy=5;


  1. SpaceEngineer
    02.03.2016 13:30

    а ещё было бы неплохо добавить interior mapping — техника простая, но здорово поднимет реализм.


  1. makc3d
    03.03.2016 00:45

    нужно пройтись по всем недостроенным дорогам, что бы найти возможные пересечения

    readPixel в битмапке, не?


  1. sim31r
    05.03.2016 23:52

    Удивительный проект. Работает в Firefox превосходно. Похоже на игру SkyLines, только масштаб больше.
    Покрутил город, в зданиях всё понравилось. Для идеальности, в моем представлении, поправил бы

    • Как-то странно выглядят пустые районы, по моему, в городе не может быть такой идеальной пустоты (если это не река), если там парк, могут быть несколько фонариков мерцающих, какие-то едва заметные дорожки.
    • Добавил бы автомобили редкие едущие вдоль дорог (без учета ПДД, это уже сложно и не нужно здесь).
    • И режим трекера, можно как заставку скринсейвер оставить, выглядит весьма эффектно. Чтобы по некой синусоиде камера шла вперед. Или следила за автомобилем, машинка едет внизу, камера следит за машиной, двигаясь над дорогой.
    • Хорошо бы дождь добавить, периодические грозы, луну, метеоритный дождь на небе, редкие вертолеты вдалеке (чтобы не прорисовывать модели детально, лишь бы узнаваемые были).


    1. SpaceEngineer
      06.03.2016 01:04

      https://www.shadertoy.com/view/XtsSWs

      Вся работа выполняется в пиксельном шейдере. Это конечно не оптимальное использование ресурсов, но всё равно впечатляет.
      На шейдертое ещё много всего интересного есть.


    1. AlexKrysin
      06.03.2016 01:10
      +1

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

      image


      1. sim31r
        06.03.2016 15:22

        Практическая ценность — экранная заставка может быть например, для какой-то фоновой анимации. Только нужен "автопилот". Анимацию с удовольствием крутил и ребенок первоклассник, минут 20, исследовал предел и для подъема и спуска )))


  1. SpaceEngineer
    06.03.2016 14:53

    Как это нет практической ценности? А игры с процедурной генерацией мира? Мне для SpaceEngine города скоро могут понадобиться :)