Введение
Генерация процедурных зданий при помощи Blueprint — соблазнительная идея. Использование стандартизированных модулей и автоматическое размещение вполне логичны, ведь, в конце концов, это же архитектура. Но как нам при текстурировании добиться естественного разнообразия вместо повторений?
Это здание было создано всего из одного модуля, автоматически скопированного в Construction Blueprint. Идея заключается в том, чтобы материал не требовал практически никакого ввода данных вручную. Для всего здания используется только один материал (за исключением окон). Его функции используют для управления рандомизацией цвета вершин и позиции пикселей в мировом пространстве.
![](https://habrastorage.org/getpro/habr/post_images/452/ed9/89f/452ed989f29ac36e17450f299cd42763.jpg)
Единственный модуль — всё, что нам нужно
![](https://habrastorage.org/getpro/habr/post_images/846/50b/59f/84650b59f1d4b9c511f0cd02223fb4ca.jpg)
Никакого размещения вручную или скриптами. Вся рандомизация выполняется в материале
Описываемый в этом туториале материал:
- Имеет слой зависящей от высоты грязи, которая покрывает объект только до указанной абсолютной высоты
- Выбирает цвет объектов для каждого этажа и сегмента случайным образом
- Немного смещает позиции мелких объектов, тоже случайным образом
- Позволяет пользователю выбрать 2 цвета для стен, а также величину их разрушенности
![](https://habrastorage.org/getpro/habr/post_images/714/288/13b/71428813b6dd48d00d880ab8373a4e09.jpg)
Цвет вершин как данные
03:34
Кроме позиций вершин и нормалей игровые движки обычно предоставляю доступ и к другим значениям, например, к цвету вершин. При затенении треугольника меша цвет интерполируется между вершинами. Можно нарисовать его в 3D-редакторе или воспользоваться вкладкой Paint в UE. Если делать это в движке, то у вас появится возможность модифицировать отдельный экземпляр меша в мире. Такой способ использования я объяснял в туториале о рисовании вершин. Однако в данном случае мы будем придерживаться импортированного цвета, потому что я решил использовать RGB-каналы в качестве точных масок, управляющих рандомизацией.
![](https://habrastorage.org/getpro/habr/post_images/d04/645/84f/d0464584f79ede1d4e6cb59f180ece8f.jpg)
Не забывайте, что цвет в 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.
![](https://habrastorage.org/getpro/habr/post_images/36a/37c/41f/36a37c41f9768c13dc189800d6885c53.jpg)
Результат работы blueprint: 4 этажа, 2 сегмента.
Весь процесс выполняется в Construction Script, т.е. во время редактирования уровня. Благодаря этому получившийся меш будет вести себя как любой другой статический объект. Например, он будет учитываться при построении освещения.
![](https://habrastorage.org/getpro/habr/post_images/1ff/a6a/3bd/1ffa6a3bd4ee53c609b3b734b9fad082.png)
Сначала измеряются размеры меша. Длина границ означает половину размера объекта. Мы можем вычислить его один раз и сохранить, потому что для всех сегментов значение будет одинаковым.
![](https://habrastorage.org/getpro/habr/post_images/1e6/475/253/1e6475253c221036c1d38ddcd0aaeae7.png)
Остальная часть кода выполняется в двух циклах. Внешний цикл создаёт этажи в целом, внутренний цикл создаёт сегменты в пределах текущего этажа. Расположение вычисляется по индексам итераций циклов, умноженным на ширину и высоту меша.
![](https://habrastorage.org/getpro/habr/post_images/f6d/b5e/a15/f6db5ea15ff1ec34d57e1fe8f840d1b3.png)
![](https://habrastorage.org/getpro/habr/post_images/b11/149/09e/b1114909edea2c5ee08b2a32742d3968.png)
К каждому сегменту добавляется новый статический компонент-меш. В некоторых случаях полезно будет использовать экземпляры. Добавление компонентов приводит к увеличению количества вызовов отрисовки, что в «тяжёлых» сценах может вызывать проблемы.
![](https://habrastorage.org/getpro/habr/post_images/e3f/738/655/e3f738655b4ce5d15c36af33b2b84c67.png)
Вот и всё. При задании новых NumberOfFloors и NumberOfSegments здание будет автоматически обновляться.
Два цвета стен, замаскированные цветом вершин
21:16
![](https://habrastorage.org/getpro/habr/post_images/528/2df/2a5/5282df2a5426419e8c5e1b77d25c67c6.jpg)
К обоим цветам обеспечен доступ как к параметрам. Красный канал цвета вершин используется как коэффициент смешения между ними.
![](https://habrastorage.org/getpro/habr/post_images/6c2/ccc/1de/6c2ccc1ded71aabd11f7858ef18276fe.png)
Упаковывание и смешение каналов
14:22
![](https://habrastorage.org/getpro/habr/post_images/f30/20d/57b/f3020d57b13577a9fd99f296b216e863.jpg)
Мы хотим, чтобы цвета стен влияли только на сами стены, но не затрагивали оконные рамы и кондиционеры. Также они не должны влиять на повреждённые участки. Это можно реализовать упаковкой маски в альфа-канал базового цвета. Другими словами, текстура базового цвета имеет прозрачный фон, и именно там воздействуют цвета стен.
Кстати, я упаковал текстуры metalness, roughness и occlusion (все в оттенках серого) в каналы R/G/B одной текстуры. Это в три раза снижает количество сэмплеров, файлов и Lerp — отличная оптимизация, не требующая никаких компромиссов. Туториал по этой технике можно найти в примечаниях.
![](https://habrastorage.org/getpro/habr/post_images/770/35a/8cb/77035a8cbf1324546e0e83c2012fdfeb.png)
Шум из текстуры
![](https://habrastorage.org/getpro/habr/post_images/fe5/273/895/fe5273895a46a8b5ce8c3be9cc3a4563.jpg)
Такой паттерн шума использовался для смешения между первым (чистым) и вторым (повреждённым) наборами текстур. Вместо вычисления его в реальном времени, что для высококачественного шума будет затратной процедурой, я просто загружаю его из текстуры. Нод WorldAlignedTexture проецирует её с трёх сторон (он генерирует UV-координаты процедурно).
![](https://habrastorage.org/getpro/habr/post_images/7bf/cbe/46f/7bfcbe46f272add70a52189d83dd2af8.png)
Зависящая от высоты грязь. Функция Remap
18:00
![](https://habrastorage.org/getpro/habr/post_images/6cc/468/e2e/6cc468e2e8e5cc877ea15c4c60d08d89.jpg)
Грязь — это плоский цвет, применённый градиентом в мировом пространстве. Компонент Z позиции пикселя в мире преобразуется в интервал 0-1. Это даёт нам полезную маску — коэффициент Lerp. Исходные минимум и максимум (например, от 150 до 700 см) передаются пользователем как скалярные параметры. Добавляется небольшой шум, чтобы сделать переход более естественным.
![](https://habrastorage.org/getpro/habr/post_images/938/f88/f49/938f88f4943ce485baeaad8552bff562.jpg)
![](https://habrastorage.org/getpro/habr/post_images/649/957/d4c/649957d4c40e938f066c047c201be84c.png)
Функция TAA_Remap_01_Clamped создана мной. Я использую её почти во всех шейдерах. Она преобразует значение в исходном интервале в интервал 0-1. Отлично подходит для создания масок на основании расстояния (от камеры, от земли или даже для фигур в UV-пространстве).
![](https://habrastorage.org/getpro/habr/post_images/7a3/a62/b3d/7a3a62b3d2d43495fe49a6dfac5ae4bf.png)
Рандомизация цветов, сокрытие элементов
22:55
![](https://habrastorage.org/getpro/habr/post_images/355/4da/42f/3554da42fc4175eda5835837b819aa31.jpg)
Получение случайного значения из опорной точки позиции сегмента позволяет сдвигать цветовую палитру для мелких объектов. Палитра — это текстура с выстроенными по горизонтали цветами, поэтому перемещение используемой для чтения даёт нам итоговый цвет.
![](https://habrastorage.org/getpro/habr/post_images/24b/c58/528/24bc58528ef7b0b014901e8e648e0f47.png)
![](https://habrastorage.org/getpro/habr/post_images/e82/5f0/949/e825f0949c1b0531ba264540bb4c52db.png)
Текстура палитры в увеличенном масштабе. Её обычный размер 8?1 пиксель с отключенным сжатием
![](https://habrastorage.org/getpro/habr/post_images/f32/376/582/f32376582181d8d017a5b37583432969.png)
Зелёный канал цвета вершин служит значением рандомизации и маской. Значение 0 обозначает «не применять здесь цвет».
![](https://habrastorage.org/getpro/habr/post_images/dac/ab4/351/dacab4351f9c9fbe8f25e3b09e7ec847.jpg)
Применение локального смещения позиции
Последняя функция — это случайное перемещение объектов по оси X. Для этого к вершинам прибавляется управляемое материалом смещение позиции. При использовании этого трюка нужно быть внимательным к двум вещам — сложным коллизиям (для стрельбы) и полям расстояний. И тот, и другой параметр не знают о том, что было выполнено такое смещение.
Я снова возьму синий канал и прибавлю случайное число — тот же шум на основе позиции сегмента, который мы использовали ранее. Давайте преобразуем его из интервала 0-1 в интервал от -0.5 до 0.5, чтобы движение выполнялось в обоих направлениях. Затем мы умножим его на PositionOffsetStrength. Нод Append добавит оставшиеся оси (постоянный 0 по Y и Z).
Довольно неожиданно то, что Unreal требует, чтобы на выход смещение выводилось в мировых координатах. Мы же вычислили локальную позицию. Как её преобразовать?
Это можно сделать, преобразовав пространство этой новой локальной позиции в мировое пространство при помощи нода Transform. Затем я вычту исходную позицию вершины в мире из этой новой позиции вершины, получив на выходе мировое смещение вместо позиции. Соединим это с выходом материала World Position Offset, и на этом работа закончена.
![](https://habrastorage.org/getpro/habr/post_images/d42/819/976/d42819976db34e96c5975f9356bc1926.png)
Готовый материал
Надеюсь, из этого туториала вы научились чему-то новому. Вот скриншот всего графа нодов материала:
![](https://habrastorage.org/getpro/habr/post_images/750/9ed/5ad/7509ed5ade041ded8202d6f99f5eb96a.png)
Файлы проекта и обсуждение
Скачать файлы проекта можно бесплатно (или за донат, при желании): файлы проекта. Если у вас есть отзывы или вопросы, то присоединяйтесь к обсуждению в Reddit.
Дополнительное чтение
- Простой способ упаковки текстур в RGB-каналы — как сохранить три текстуры в оттенках серого в каналы одного RGB-изображения при помощи Photoshop. Упаковка позволяет экономить пространство и, что гораздо важнее, получать три текстуры за одну операцию считывания. Это стоит усилий, потому что считывание текстур из памяти — одна из самых затратных по времени операций GPU.
Комментарии (3)
Pythonpy
13.12.2019 10:02Очень интересно, но почему бы не добавить ещё генерацию текстур на балконы. Паттерн разложения краски повторяется, было бы ещё реалистичнее если каждый балкон был уникален)
Segmentq
13.12.2019 10:53У вас похоже перебор с кондиционерами, на комнату и наружный блок и оконный кондиционер. Еще почему-то на веревках висит один набор шмотья: носки и полотенца.
tas
Шайтанома! Обожаю такие статьи!