Инструменты для создания процедурного арта в Unreal Engine 4
Арран Лангмид делится своей статьей о процедурной генерации меша в Unreal, где объясняет, как создавать простую геометрию и генерировать листву с помощью блюпринтов.
Введение
Одной из лучших сторон Unreal является его доступность для всех, кто склонен к творчеству. С первым выпуском Unreal Engine 4 люди (и я в их числе) обнаружили, что они могут создавать целые игры, не прикоснувшись ни к одной строчке C++. С тех пор люди расширяют границы возможностей того, что можно сделать с блюпринтами, и Epic отвечает на это, расширяя набор инструментов с каждым новым обновлением.
Невероятно мощной частью этого стал Procedural Mesh Component, который принимает массивы данных и выдает статический меш. Объедините его с функцией render to texture, и вы сможете процедурно создавать текстурированные ассеты полностью в движке! Почему это полезно:
- вы можете видеть, как именно будет выглядеть ассет в процессе его создания;
- вы можете видеть ассет в его окружении в процессе создания;
- обновления происходят в режиме реального времени.
Есть масса потенциальных возможностей для применения этого, и одно из них, которое я сейчас изучаю, — создание стилизованной листвы. Procedural Mesh Component, в отличии от традиционных инструментов моделирования, является отличным решением, так как деревья в игре состоят в основном из простых форм (цилиндров и плоскостей) и больше зависят от правильного расположения и небольшого изменения этих форм, чем от их индивидуальной сложности.
Создание простой геометрии
Треугольник
Начнем с самого простого и создадим треугольник. Создайте новый блюпринт класса Actor и добавьте procedural mesh component. Создайте новую функцию DrawTriangle и добавьте ее в Construction Script.
Здесь у нас есть три точки, которые представляют вершины нашего треугольника, и целочисленный порядок, необходимый для создания этого треугольника. Обратите внимание на порядок перечисления вершин: если сохранить 0, 1, 2, то треугольник будет обращен не в ту сторону.
Используйте функцию CreateMeshSection у нашего объекта procedural mesh component и подсоедините массивы к соответствующим входам:
Вуаля! Мы только что создали треугольник!
Плоскость
Теперь давайте немного усложним задачу и создадим плоскость. Прежде всего, нам нужно добавить еще несколько массивов. Нам нужно добавить:
- нормали, массив векторов (0,0,0);
- касательные, массив структур vector + boolean (0,0,0 + 0/1);
- UVs, массив двумерных векторов (0,0).
Мы немного схитрим, чтобы получить наши нормали и касательные. Как и раньше, сначала очистим наши старые данные массивов:
Затем создадим новую функцию DrawQuad и добавим два вложенных цикла (о ужас!). Эти циклы будут проходить по X и Y узлам нашей сетки:
Затем берем текущие значения X и Y, умножаем их на значение расстояния и добавляем эти данные в массив векторов. Для UV делим текущее значение X и Y на суммарное расстояние сетки по осям X и Y, чтобы получить нормализованное значение.
Затем мы можем использовать функцию CreateGridMeshTriangles для автоматического создания наших треугольников. (К сожалению, мы не сможем использовать эту функцию позже, но пока это самый простой способ создать сетку треугольников).
Нам также нужны нормали и касательные, поэтому мы можем снова схитрить и использовать функцию CalculateTangentsforMesh для их получения.
Вот финальная функция (воспользуйтесь ссылкой, чтобы рассмотреть получше):
Цилиндр
Когда у нас есть сетка, поменять форму с плоскости на цилиндр очень просто. Вместо того чтобы выравнивать вершины по сетке, мы обернем их вокруг оси.
Возьмем значения X и XLength, которые мы создали ранее, разделим одно на другое, чтобы получить нормализованное значение, а затем умножим на 360, чтобы получить наш угол. Мы хотим повернуть ось X вокруг оси Z, поэтому возьмите эти значения, вставьте в RotateVectorAroundAxis и умножьте на число для того, чтобы задать расстояние.
Поменяйте этот новый расчет массива вершин с исходным (x*float — мы уже делали ранее) вот так (ссылка для более детального рассмотрения).
Теперь наша плоскость — цилиндр!
А если мы возьмем нормализованное значение Y или Z и изменим его на 1-x, получится конус!
Чтение меша
Последний элемент, который нам нужен, это чтение меша. Функция GetSectionFromStaticMesh позволяет получить данные из любого меша и вывести их нам для использования. Мы можем взять любой меш, модифицировать его и затем распределить по поверхности или вдоль нее.
Теперь у нас есть все необходимое, чтобы начать создавать свой собственный процедурный контент. В следующей части мы рассмотрим некоторые уникальные особенности применения этих инструментов для создания простого генератора деревьев.
Генерация листвы
Инструменты для создания листвы можно разбить на пять отдельных блюпринтов:
- Tree Parent
- хранит данные, которые будут общими для всех дочерних элементов;
- от него наследуются блюпринты ствола, ветвей и листьев.
- Trunk
- наследуется от Tree Parent;
- меш, управляемый сплайном;
- часто используется для ствола;
- создает только один меш;
- дает большое количество пользовательского контроля.
- Branch
- наследуется от Tree Parent;
- порождает множество ветвей, либо вдоль поверхности, либо случайным образом;
- пользовательский контроль ограничен диапазоном случайных переменных;
- можно наслаивать одну на другую.
- Leaf
- наследуется от Tree Parent;
- считывает массив мешей и распределяет их либо вдоль ствола/ветви, либо случайным образом;
- не может иметь дочерних элементов.
- Compositor
Вы можете спросить, зачем делать это таким образом? Дело в том, что разбиение дерева на части имеет множество преимуществ:
- не нужно перерисовывать весь меш, когда вы вносите изменения в иерархию;
- возможность быстро разделять и скрывать элементы ассета;
- удаление секции без разрушения всего дерева;
- разбивка ассетов на компоненты, что позволяет копировать и вставлять части дерева в другие деревья;
- разделение генерации случайных чисел на несколько, чтобы вы получили больше контроля.
Все эти элементы собираются вместе, образуя модульный стек компонентов, с помощью которого пользователь может создать любое дерево, которое он захочет!
Базовый родитель (Tree Parent)
Чтобы не повторять одну и ту же работу, начнем с создания базового родителя, от которого будут наследоваться другие блюпринты. Это полезно, потому что все данные, которые мы поместим в родителя, будут общими для всех его дочерних элементов. Например, каждому типу ассетов понадобятся ссылка на материал и набор массивов для меша, поэтому эту информацию следует добавить в родительский компонент, чтобы она распространялась на его дочерние элементы.
Теперь добавим procedural mesh component в блюпринт Tree Parent. И нам также следует добавить ряд переменных, которые будут необходимы каждому дочернему компоненту.
- Material, материал для применения к мешу, это означает, что мы поддерживаем только один материал для каждого компонента.
- ProcMesh, структура, содержащая массивы, необходимые для создания меша: Vertex, Triangle, UV и Vertex Colour. Нормали генерируются позже со специальной функцией.
- Random Seed, добавляет случайности к частям меша.
- Parent, переменная Tree Parent. Используется для ссылки на своего родителя.
- Root, ссылается на инструмент композиции мешей. Полезно для получения порядка хранения.
- Child Refs, массив типов Tree Parent, сообщает дочерним объектам о необходимости обновления.
- Ts, массив массивов объектов Transform. Используется для хранения информации о ветвях. Считайте это скелетом дерева. Дочерние компоненты ссылаются на этот массив, чтобы получить локацию для своего создания.
Общая функция, сообщающая ассетам в массиве дочерних компонентов об обновлении. Она будет вызываться каждый раз при изменении компонента (но помним, что эти функции и касты еще не существуют, так как мы их пока не создали). Если мы не будем обновлять дочерние компоненты, то части дерева быстро отсоединятся.
Trunk
Блюпринт ствола наследуется от Tree Parent и основан на редактируемом сплайне, который позволяет получить максимум контроля. Его можно добавить к любому блюпринту, выбрав сплайн в выпадающем списке Add Component, так же как мы делали с procedural mesh component.
Здесь вы видите компоненты, которые мы уже унаследовали:
В Construction Script добавляем новую функцию — Draw.
Функция Draw проверяет, существует ли уже родительский ствол, и если существует, то функция перемещает компонент вдоль родительского сплайна. Если родительский ствол о отсутствует, она создает меш. И, наконец, указывает дочерним объектам, что их нужно обновить с помощью функции Update Children.
В Create Base Segments мы очищаем старые геометрические данные, как мы делали в части 1, рассчитываем новые данные, а затем перерисовываем меш. Это будет основной паттерн, который мы будем повторять во всех компонентах.
Для геометрии нам нужны два целых числа, одно для количества сегментов по длине дерева, другое для количества радиальных сегментов.
Получите длину сплайна и количество нужных нам сегментов длины, разделите их, чтобы получить расстояние между каждой точкой, и запишите значение в float под названием Segment distance.
Затем выполните цикл для каждого сегмента длины и вызовите функцию Get Transform At Distance Along Spline. Длина будет равна текущему индексу цикла, помноженному на расстояние между сегментами. Вы можете масштабировать X или Y для управления толщиной ствола. Умножьте полученный выше Transform на масштабированное значение, чтобы меш был равномерный.
Вам может понадобиться использовать опцию Swivel, чтобы совместить вращение объекта с вращением ветви.
Сохраните преобразование в массиве. Это действительно важно, так как нам нужен общий способ определения местоположения ветвей в пространстве, и не каждый блюпринт будет иметь сплайн-компонент. Позже это будет добавлено в массив Ts (созданный в TreeParent).
Затем мы рисуем вершины вокруг полученного объекта Trasnform в функции Draw Radius. Это похоже на то, как мы рисовали геометрию раньше. Нажмите на ссылку, чтобы увидеть полный граф.
Единственным существенным отличием здесь является способ создания массива треугольников. Это вроде бы не нужно для ствола, но абсолютно необходимо для работы нескольких ветвей. Функция grid mesh triangles ожидает, что будет только один меш, но если мы хотим сделать несколько отдельных мешей, нам нужно сместить счетчик треугольников.
Когда все это сделано, нам нужно взять массив Base Transforms, который мы создавали, и установить его в массив Ts. Индекс здесь нужен, по идее, чтобы указывать множество ветвей, но поскольку мы создаем только один сегмент, он просто устанавливается на значение 0.
После того как мы получили все данные, остается только нарисовать меш!
Branch
Надеюсь, вы уже поняли закономерность: очищаем старые данные и создаем новые. Блюпринт ветви должен взять родителя (если он существует) и распределить по нему несколько ветвей. Если родитель не существует, мы просто распределяем их по земле.
Ничего нового в очищении данных нет, поэтому перейдем к Make Branch Data. Основная часть этой функции — нахождение точки создания для каждой создаваемой ветви (ссылка).
Для начала выясним, есть ли у блюпринта родитель или нет. Если есть, мы получаем массив Ts родителя, это массив массивов трансформаций. Каждая ветвь — это новый массив преобразований. Это означает, что если родительская ветвь имеет 8 ветвей, а мы решили сделать 4 ветви, то всего получится 32 ветви. Если родительской ветвью является ствол, то в массиве T будет только одна запись преобразований.
Если у ветви нет родителя, продолжаем дальше и запускаем цикл по ветвям, вычисляем нормализованное расстояние.
Следующий шаг — выяснить начальную точку каждой ветви. Если нет родителя, просто получите начальную точку объекта. Если родитель есть, получите текущий массив преобразований и найдите местоположение на основе нормализованного расстояния. Здесь также можно добавить значение clamp, чтобы сузить диапазон.
Ниже приведена диаграмма, которая объяснит вам все намного проще, если вы успели запутаться. Верхнее значение здесь — это нормализованное расстояние, которое необходимо перевести в массив преобразований. Умножение на количество индексов преобразований дает нам ближайший индекс, а остаток — расстояние между ним и следующим индексом. Этот расчет даст нам приблизительное расстояние вдоль все длины ствола.
Сгенерируйте случайное местоположение между векторами min и max.
Объедините местоположение, измените вращение с помощью пользовательского макроса и измените масштаб.
Еще один важный элемент — выяснение того, вокруг чего вращается ветвь. Макрос принимает такие данные, как «вокруг…» / «вдоль родителя», «вращение на индекс» и т. д. С целью получить легко контролируемые результаты используйте rotate about index и векторы направления, дабы избежать блокировки кардана: gimbal lock (ссылка).
Как только мы получили начало координат, добавьте его в массив pivot и создайте ветвь.
В примере ствола мы использовали сплайн для получения трансформации, а затем построили геометрию вокруг нее. Мы не можем сделать это здесь, так как сплайн не существует. Вместо этого мы должны сгенерировать эти точки (ссылка).
Выбираем текущую длину массива вершин. Она нужна нам для смещения количества треугольников. Массив Triangle просто ссылается на индекс массива вершин, поэтому смещение необходимо, чтобы сохранить правильную ссылку на треугольник. Сгенерируйте случайную длину ветви между минимальной и максимальной.
Затем для каждой точки вдоль ветви:
Определите местоположение следующей точки. Возьмите последнее преобразование, сместите его, умножив на вектор направления. Немного подкорректируйте с помощью контролируемого вращения.
Создайте данные меша так же, как мы делали раньше.
Важной особенностью блюпринта ветви является то, что он может быть многослойным, или штабелируем. Мы должны иметь возможность строить ветви на ветвях, ветви на ветвях ветвей и т. д. Единственная проблема, с которой мы можем столкнуться, это то, что количество ветвей растет по экспоненте. Если в первом слое будет 6 ветвей, а во втором и третьем — по 4, то мы получим 96 ветвей, а не 14. Если мы добавим четвертый слой из 4 ветвей, то получим 384. Вся операция использует циклы, что означает, что мы получим много заминок, если числа станут слишком большими.
Лист
Карточки листьев не могут иметь дочерних элементов, поэтому они всегда будут находиться в конце соответствующей иерархии. Блюпринт пробегает по массиву мешей и распределяет их вдоль ствола или ветви. Полезный трюк, который мы можем здесь сделать, это рендеринг отдельных экземпляров вместо отрисовки геометрии, что значительно повышает производительность. Здесь мы добавляем открытый булев флаг, чтобы использовать эту оптимизацию.
Для этого компонента создайте массив мешей, которые могут быть заданы пользователем. Для каждого меша используйте функцию get section from static mesh, чтобы получить данные каждого меша и сохранить их для последующего использования (ссылка).
Как и в случае с блюпринтом ветки, очистите старые данные, проверьте наличие родителя, затем пройдите циклом по всем элементам массива Ts родительской ветви и по каждому листу на этой ветке.
Найдите точку создания листа, как и раньше, а затем создайте меш вручную или добавьте экземпляр существующего и заданного ранее.
Добавление данных меша очень просто (ссылка).
Мы получаем сдвиг на основе длины массива треугольников и выбираем меш для репликации.
Сдвигаем массив треугольников до значения, полученного выше. Трансформируем положение вершин и сохраняем все в массив.
Объединение
После сборки отдельных компонентов их нужно объединить в единый меш, который можно экспортировать. Вместо того, чтобы выполнять эту операцию в Construction Script, функции можно вывести в редактор в виде кнопок. Это позволяет повысить уровень контроля при выполнении скрипта.
В этой функции мы очищаем старые данные, создаем уникальный массив материалов (это позволяет избежать создания ассетов с дублирующимися материалами), при необходимости перестраиваем данные меша и выводим меш. В качестве оптимизации мы должны перестроить меш, чтобы избежать большого количества повторяющихся материалов.
Соберите компоненты в единый набор массивов (ссылка).
Проверьте, соответствует ли построенный нами массив материалов массиву компонентов по длине. Если материалов меньше, чем компонентов, мы знаем, что есть некоторые общие материалы.
В этот момент мы должны пройтись по данным треугольников каждого компонента и сместить их так, чтобы они совпадали с массивом вершин при объединении.
Наконец, возьмите эту новую структуру данных и создайте статический меш.
Нажмите на Procedural Mesh Component в Compositor и нажмите кнопку «Create Static Mesh«. Вам будет предложено выбрать местоположение, выбрать имя, и вот оно!
Заключение
Это лишь один пример того, какие объекты можно создавать с помощью Procedural Mesh Component. Существует огромное количество возможностей, и это не ограничивается только созданием в редакторе, вы можете использовать эту функцию и в игре.
Говоря о процедурных инструментах, нельзя не упомянуть о Houdini, потрясающем инструменте, который фокусируется исключительно на процедурной генерации контента. У него есть отличный плагин для UE4, который можно использовать для работы с ассетами Houdini Engine. Если вы, так же как и я, «заболели» процедурной генерацией контента, настоятельно рекомендую взглянуть на него. Если после прочтения этой статьи вы вдохновитесь на создание чего-либо (дерева или другого), буду рад посмотреть на это. Поделитесь со мной в twitter @ArranLangmead!
Примечание редактора:
На протяжении всей статьи автор часто обращается к функциям из готовой библиотеки, которую можно скачать здесь: https://bit.ly/3b4WOM0
Оригинал: https://80.lv/articles/building-procedural-art-tools-in-unreal-engine-4/