Введение
Генерация процедурных зданий при помощи Blueprint — соблазнительная идея. Использование стандартизированных модулей и автоматическое размещение вполне логичны, ведь, в конце концов, это же архитектура. Но как нам при текстурировании добиться естественного разнообразия вместо повторений?
Это здание было создано всего из одного модуля, автоматически скопированного в Construction Blueprint. Идея заключается в том, чтобы материал не требовал практически никакого ввода данных вручную. Для всего здания используется только один материал (за исключением окон). Его функции используют для управления рандомизацией цвета вершин и позиции пикселей в мировом пространстве.

Единственный модуль — всё, что нам нужно

Никакого размещения вручную или скриптами. Вся рандомизация выполняется в материале
Описываемый в этом туториале материал:
- Имеет слой зависящей от высоты грязи, которая покрывает объект только до указанной абсолютной высоты
- Выбирает цвет объектов для каждого этажа и сегмента случайным образом
- Немного смещает позиции мелких объектов, тоже случайным образом
- Позволяет пользователю выбрать 2 цвета для стен, а также величину их разрушенности

Цвет вершин как данные
03:34
Кроме позиций вершин и нормалей игровые движки обычно предоставляю доступ и к другим значениям, например, к цвету вершин. При затенении треугольника меша цвет интерполируется между вершинами. Можно нарисовать его в 3D-редакторе или воспользоваться вкладкой Paint в UE. Если делать это в движке, то у вас появится возможность модифицировать отдельный экземпляр меша в мире. Такой способ использования я объяснял в туториале о рисовании вершин. Однако в данном случае мы будем придерживаться импортированного цвета, потому что я решил использовать RGB-каналы в качестве точных масок, управляющих рандомизацией.

Не забывайте, что цвет в 3D-графике — это всего лишь трёхкомпонентный вектор. Его компонентами являются яркость красного, зелёного и синего в интервале 0 – 1. Значение произвольно, потому что цвет вершины — это просто данные. Вместо того, чтобы использовать их напрямую для раскрашивания текстур, я решил упаковать в каждый канал цвета вершины маски:
- Красный канал — маска основного и вторичного цветов краски стен. Полигоны со значением 0 используют основной цвет, с 1 — вторичный цвет.
- Зелёный канал — используется для выбора цвета из палитры. Это позволяет управлять цветовой вариацией мелких предметов, например, сохнущего белья. Значение между 0 и 1 округляется до индекса (UV-позиции) в текстуре палитры.
- Синий канал – смещение позиций вершин для перемещения вершин по горизонтали. Это значит, что 0 будет использоваться для стен (перемещения нет), а значения до 1 могут быть назначены кондиционеру или белью. Также этот канал управляет видимостью (маска непрозрачности). Если значение больше 0, то прибавляется случайное значение, своё для каждого сегмента здения, чтобы создать вариативность.
В любом серьёзном 3D-редакторе существует функция рисования цвета вершин. Вы с лёгкостью найдёте инструкции о том, как это делается в вашем редакторе. Только не забудьте сказать Unreal Engine в окне импорта меша, чтобы он заменил (Replace) цвет вершин (а не игнорировал (Ignore) его).
Как я говорил выше, цвет в данном проекте — это набор точных значений. Для подобной задачи, да и практически любой технической задачи по игровому арту я предпочитаю использовать Houdini. Однако схожий результат можно достичь (с несколько большими усилиями) в любом другом 3D-редакторе. Просто обратите внимание на то, что каждый цветовой канал должен обозначать в шейдере.
Я выделил весь процесс назначения значений каналам цвета вершин в отдельный туториал: Store data in vertex color using Houdini. В этом туториале я применяю хитрые инструменты Houdini, чтобы сделать процесс более эффективным.
Construction Blueprint
07:57
Созданный мной Blueprint прост. Он просто создаёт плоскую стену здания, дублируя меш по горизонтали и вертикали. Он имеет изменяемые переменные MeshWall (Static Mesh), NumberOfFloors (integer), NumberOfSegments (integer) и Material.

Результат работы blueprint: 4 этажа, 2 сегмента.
Весь процесс выполняется в Construction Script, т.е. во время редактирования уровня. Благодаря этому получившийся меш будет вести себя как любой другой статический объект. Например, он будет учитываться при построении освещения.

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

Остальная часть кода выполняется в двух циклах. Внешний цикл создаёт этажи в целом, внутренний цикл создаёт сегменты в пределах текущего этажа. Расположение вычисляется по индексам итераций циклов, умноженным на ширину и высоту меша.


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

Вот и всё. При задании новых NumberOfFloors и NumberOfSegments здание будет автоматически обновляться.
Два цвета стен, замаскированные цветом вершин
21:16

К обоим цветам обеспечен доступ как к параметрам. Красный канал цвета вершин используется как коэффициент смешения между ними.

Упаковывание и смешение каналов
14:22

Мы хотим, чтобы цвета стен влияли только на сами стены, но не затрагивали оконные рамы и кондиционеры. Также они не должны влиять на повреждённые участки. Это можно реализовать упаковкой маски в альфа-канал базового цвета. Другими словами, текстура базового цвета имеет прозрачный фон, и именно там воздействуют цвета стен.
Кстати, я упаковал текстуры metalness, roughness и occlusion (все в оттенках серого) в каналы R/G/B одной текстуры. Это в три раза снижает количество сэмплеров, файлов и Lerp — отличная оптимизация, не требующая никаких компромиссов. Туториал по этой технике можно найти в примечаниях.

Шум из текстуры

Такой паттерн шума использовался для смешения между первым (чистым) и вторым (повреждённым) наборами текстур. Вместо вычисления его в реальном времени, что для высококачественного шума будет затратной процедурой, я просто загружаю его из текстуры. Нод WorldAlignedTexture проецирует её с трёх сторон (он генерирует UV-координаты процедурно).

Зависящая от высоты грязь. Функция Remap
18:00

Грязь — это плоский цвет, применённый градиентом в мировом пространстве. Компонент Z позиции пикселя в мире преобразуется в интервал 0-1. Это даёт нам полезную маску — коэффициент Lerp. Исходные минимум и максимум (например, от 150 до 700 см) передаются пользователем как скалярные параметры. Добавляется небольшой шум, чтобы сделать переход более естественным.


Функция TAA_Remap_01_Clamped создана мной. Я использую её почти во всех шейдерах. Она преобразует значение в исходном интервале в интервал 0-1. Отлично подходит для создания масок на основании расстояния (от камеры, от земли или даже для фигур в UV-пространстве).

Рандомизация цветов, сокрытие элементов
22:55

Получение случайного значения из опорной точки позиции сегмента позволяет сдвигать цветовую палитру для мелких объектов. Палитра — это текстура с выстроенными по горизонтали цветами, поэтому перемещение используемой для чтения даёт нам итоговый цвет.


Текстура палитры в увеличенном масштабе. Её обычный размер 8×1 пиксель с отключенным сжатием

Зелёный канал цвета вершин служит значением рандомизации и маской. Значение 0 обозначает «не применять здесь цвет».

Применение локального смещения позиции
Последняя функция — это случайное перемещение объектов по оси X. Для этого к вершинам прибавляется управляемое материалом смещение позиции. При использовании этого трюка нужно быть внимательным к двум вещам — сложным коллизиям (для стрельбы) и полям расстояний. И тот, и другой параметр не знают о том, что было выполнено такое смещение.
Я снова возьму синий канал и прибавлю случайное число — тот же шум на основе позиции сегмента, который мы использовали ранее. Давайте преобразуем его из интервала 0-1 в интервал от -0.5 до 0.5, чтобы движение выполнялось в обоих направлениях. Затем мы умножим его на PositionOffsetStrength. Нод Append добавит оставшиеся оси (постоянный 0 по Y и Z).
Довольно неожиданно то, что Unreal требует, чтобы на выход смещение выводилось в мировых координатах. Мы же вычислили локальную позицию. Как её преобразовать?
Это можно сделать, преобразовав пространство этой новой локальной позиции в мировое пространство при помощи нода Transform. Затем я вычту исходную позицию вершины в мире из этой новой позиции вершины, получив на выходе мировое смещение вместо позиции. Соединим это с выходом материала World Position Offset, и на этом работа закончена.

Готовый материал
Надеюсь, из этого туториала вы научились чему-то новому. Вот скриншот всего графа нодов материала:

Файлы проекта и обсуждение
Скачать файлы проекта можно бесплатно (или за донат, при желании): файлы проекта. Если у вас есть отзывы или вопросы, то присоединяйтесь к обсуждению в Reddit.
Дополнительное чтение
- Простой способ упаковки текстур в RGB-каналы — как сохранить три текстуры в оттенках серого в каналы одного RGB-изображения при помощи Photoshop. Упаковка позволяет экономить пространство и, что гораздо важнее, получать три текстуры за одну операцию считывания. Это стоит усилий, потому что считывание текстур из памяти — одна из самых затратных по времени операций GPU.