Представим ситуацию: Мы создаем в Unreal Engine 4 (или UE5) сцену пещеры, в которой живут пещерные люди. Модель нарисована, основные объекты размещены, но не хватает иллюзии того что тут действительно живут люди: не хватает хаоса. Чтобы это исправить необходимо по пещере разбросать камушки, палки, остатки от трапез наших неандертальцев. Первый вариант это разместить руками. Вариант хороший, потом даже можно отметить, что все размещено вручную, каждый камешек положен на «свое» место. Но я человек ленивый любящий все автоматизировать, и мне захотелось такие процессы повесить на плечи «роботов». Первое что приходит в голову это подключить к проекту Houdini Engine и с его помощью разместить нужные объекты, но как быть если к проекту нет возможности подключить Houdini Engine (ответ почему, выходит за рамки этой статьи, просто примем это как вводные данные). Можно изобрести свой интерфейс, используя Python, скажем так, создать свой Houdini Engine на минималках.

Рабочая локация
Рабочая локация

Для обкатки технологии упростим задачу: нам необходимо в созданной пещере разместить ракушки. У нас есть три мешки, у одной из мешок семь вариантов материалов.

Создадим историю: кто-то из людей притащил к костру горсть ракушек (мидий) и сидел ковырял и ел, соответственно разбрасывал осколки рядом с местом где ел, так как ему это понравилось, он еще несколько раз повторил трапезу.

Вот что мы получаем из этой истории: нам необходимо выбрать случайным образом зоны, где он сидел и ел, и в этих зонах раскидать модельки ракушек, с рандомными углами поворота и масштабом.

Задача ясна, приступим.

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

Плагины
Плагины

Для спавна объектов наша команда написала следующий скрипт:

import unreal
#Сама функция
def place_static_mesh_with_material(static_mesh_path, material_path, location, rotation, scale):
  static_mesh = unreal.EditorAssetLibrary.load_asset(static_mesh_path)
  actor = unreal.EditorLevelLibrary.spawn_actor_from_class(unreal.StaticMeshActor, location, rotation)
  mesh_component = actor.get_components_by_class(unreal.StaticMeshComponent)[0]
  mesh_component.set_static_mesh(static_mesh)
  material = unreal.EditorAssetLibrary.load_asset(material_path)
  mesh_component.set_material(0, material)
  mesh_component.set_relative_scale3d(scale)

#Описываем меши и материалы, которые будем использовать в скрипте
sm_shell1 = "/Game/Dvuglaz/Meshes/Props/Shells/SM_Shell.SM_Shell";
mat_shell1_1 = "/Game/Dvuglaz/Meshes/Props/Shells/Materials/MI_Shell.MI_Shell"

#Тут добавляем фунциии
place_static_mesh_with_material(sm_shell1, mat_shell1_1, unreal.Vector(-1083,-2219,297), unreal.Rotator(58,60,122), unreal.Vector(1.07212,1.07212,1.07212))

Сохраняем в файл и выполняем скрипт.

File → Execute Python Script...
File → Execute Python Script...

По условию у нас три мешки и несколько материалов, необходимо создать ссылки на эти объекты. Создадим три мешки, и материалы, для второй мешки определим 7 материалов.

sm_shell1 = "/Game/Dvuglaz/Meshes/Props/Shells/SM_Shell.SM_Shell";
sm_shell2 = "/Game/Dvuglaz/Meshes/Props/Shells/SM_Shell_Andontia_full.SM_Shell_Andontia_full";
sm_shell3 = "/Game/Dvuglaz/Meshes/Props/Shells/SM_Shell_Andontia_half.SM_Shell_Andontia_half";

mat_shell1_1 = "/Game/Dvuglaz/Meshes/Props/Shells/Materials/MI_Shell.MI_Shell"
mat_shell2_1 = "/Game/Dvuglaz/Meshes/Props/Shells/Materials/MI_Shell_Andontia2_1.MI_Shell_Andontia2_1"
mat_shell2_2 = "/Game/Dvuglaz/Meshes/Props/Shells/Materials/MI_Shell_Andontia2_2.MI_Shell_Andontia2_2"
mat_shell2_3 = "/Game/Dvuglaz/Meshes/Props/Shells/Materials/MI_Shell_Andontia2_3.MI_Shell_Andontia2_3"
mat_shell2_4 = "/Game/Dvuglaz/Meshes/Props/Shells/Materials/MI_Shell_Andontia2_4.MI_Shell_Andontia2_4"
mat_shell2_5 = "/Game/Dvuglaz/Meshes/Props/Shells/Materials/MI_Shell_Andontia2_5.MI_Shell_Andontia2_5"
mat_shell2_6 = "/Game/Dvuglaz/Meshes/Props/Shells/Materials/MI_Shell_Andontia2_6.MI_Shell_Andontia2_6"
mat_shell2_7 = "/Game/Dvuglaz/Meshes/Props/Shells/Materials/MI_Shell_Andontia2_7.MI_Shell_Andontia2_7"
mat_shell3_1 = "/Game/Dvuglaz/Meshes/Props/Shells/Materials/MI_Shell_Andontia2_Full.MI_Shell_Andontia2_Full"

Осталось за малым создать список из функций place_static_mesh_with_material, в параметрах которых будут указаны координаты, вращение и масштаб. Вот тут нам и понадобится генератор!

Для создания генератора произведем подготовительные работы. Первым делом нам необходимо выгрузить всю сцену в FBX.

File → Export All
File → Export All

На выходе получится большой файл, в нашем случае это файл 214 МБ. Unreal выгружает все, не только модели но и файлы коллизии, свет и прочие элементы.

Файл локации
Файл локации

Перед расстановкой ракушек подготовим почву (во всех смыслах этого слова). Мне такие процессы удобнее делать в Maya, так как тут представлен список всех элементов.

Outliner со всеми объектами
Outliner со всеми объектами

Первым делом можно смело удалить все коллизии, у них есть префикс UCX_,UBX_, USP_ и UCP_), используя механизм выделения по имени, выберем все коллизии.

Выделение по имени
Выделение по имени
Выбраны все коллизии
Выбраны все коллизии

После того как все коллизии будут выбраны - удаляем их.

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

Инверсия - удобный инструмент
Инверсия - удобный инструмент
Все что осталось от сцены
Все что осталось от сцены

Полученные объекты экспортируем в новый FBX и открываем в Houdini.

Экспорт в FBX
Экспорт в FBX

В Houdini откроем этот файл и начнем реализовывать ранее описанную логику по расбрасыванию раковин.

Открываем файл нодой File
Открываем файл нодой File

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

Создания поверхности на основе моделей
Создания поверхности на основе моделей

В видео показаны все параметры нод.

Следующим этапом разместим на полученном рельефе зоны, где сидел наш человек.

Размещение зон, где будут разбросаны предметы
Размещение зон, где будут разбросаны предметы

Ну и последним действием на полученных зонах мы создадим 30 точек.

Параметры ноды Scatter
Параметры ноды Scatter
Точки на зонах
Точки на зонах

Когда точки котовы, разместим в них параметры, с помощью которых можно будет перенести обратно в Unreal Engine. Для этого добавим ноду Attribute Wrangle. В этой ноде напишем код, который сгенерирует функции спавна объектов.

В этом коде мы создаем три вектора Loc, Rot, Scl с случайными значениями (положение, вращение и масштаб), также выбираем одну из трех мешек и если это вторая(sm_shell2), то ей присваиваем случайный материал.

s@Loc = "unreal.Vector("+itoa(@P.x)+","+itoa(@P.z)+","+itoa(@P.y)+")";

vector rot;
rot.x = fit01((rand(@ptnum+87)),-180,180);
rot.y = fit01((rand(@ptnum+823)),-180,180);
rot.z = fit01((rand(@ptnum+586)),-180,180);
s@Rot = "unreal.Rotator("+itoa(rot.x)+","+itoa(rot.y)+","+itoa(rot.z)+")";

float scl = fit01((rand(@ptnum+652)),0.7,1.5);
s@Scl = "unreal.Vector("+sprintf("%g,%g,%g",scl,scl,scl)+")";

int type = rint(fit01(rand(@ptnum+467),0,3));

if (type==0){
    s@Name = "sm_shell1";
    s@Mat = "mat_shell1_1";
}
else{
    if (type == 1){
        s@Name = "sm_shell2";
        int imat = rint(fit01(rand(@ptnum+357),1,7));
        s@Mat = "mat_shell2_"+itoa(imat);   
    }
    else{
        s@Name = "sm_shell3";
        s@Mat = "mat_shell3_1";   
        }
}

s@Code="place_static_mesh_with_material("+s@Name+", "+s@Mat+", "+s@Loc+", "+s@Rot+", "+s@Scl+") ";

Результатом скрипта будут созданы параметры Loc, Rot, Scl, Name и Mat и также на основе этих параметров будет создан параметр Code.

Таблица параметров
Таблица параметров

Скопируем значения параметра Code и добавим в нижнюю часть скрипта.

import unreal

#Сама функция
def place_static_mesh_with_material(static_mesh_path, material_path, location, rotation, scale):
    static_mesh = unreal.EditorAssetLibrary.load_asset(static_mesh_path)
    actor = unreal.EditorLevelLibrary.spawn_actor_from_class(unreal.StaticMeshActor, location, rotation)
    mesh_component = actor.get_components_by_class(unreal.StaticMeshComponent)[0]
    mesh_component.set_static_mesh(static_mesh)
    material = unreal.EditorAssetLibrary.load_asset(material_path)
    mesh_component.set_material(0, material)
    mesh_component.set_relative_scale3d(scale)

# Описываем меши и материалы, которые будем использовать в скрипте
sm_shell1 = "/Game/Dvuglaz/Meshes/Props/Shells/SM_Shell.SM_Shell";
sm_shell2 = "/Game/Dvuglaz/Meshes/Props/Shells/SM_Shell_Andontia_full.SM_Shell_Andontia_full";
sm_shell3 = "/Game/Dvuglaz/Meshes/Props/Shells/SM_Shell_Andontia_half.SM_Shell_Andontia_half";

mat_shell1_1 = "/Game/Dvuglaz/Meshes/Props/Shells/Materials/MI_Shell.MI_Shell"
mat_shell2_1 = "/Game/Dvuglaz/Meshes/Props/Shells/Materials/MI_Shell_Andontia2_1.MI_Shell_Andontia2_1"
mat_shell2_2 = "/Game/Dvuglaz/Meshes/Props/Shells/Materials/MI_Shell_Andontia2_2.MI_Shell_Andontia2_2"
mat_shell2_3 = "/Game/Dvuglaz/Meshes/Props/Shells/Materials/MI_Shell_Andontia2_3.MI_Shell_Andontia2_3"
mat_shell2_4 = "/Game/Dvuglaz/Meshes/Props/Shells/Materials/MI_Shell_Andontia2_4.MI_Shell_Andontia2_4"
mat_shell2_5 = "/Game/Dvuglaz/Meshes/Props/Shells/Materials/MI_Shell_Andontia2_5.MI_Shell_Andontia2_5"
mat_shell2_6 = "/Game/Dvuglaz/Meshes/Props/Shells/Materials/MI_Shell_Andontia2_6.MI_Shell_Andontia2_6"
mat_shell2_7 = "/Game/Dvuglaz/Meshes/Props/Shells/Materials/MI_Shell_Andontia2_7.MI_Shell_Andontia2_7"
mat_shell3_1 = "/Game/Dvuglaz/Meshes/Props/Shells/Materials/MI_Shell_Andontia2_Full.MI_Shell_Andontia2_Full"


#Тут добавляем фунциии из генератора

place_static_mesh_with_material(sm_shell2, mat_shell2_3, unreal.Vector(-574,-2452,315), unreal.Rotator(-83,15,93), unreal.Vector(1.22731,1.22731,1.22731)) 
place_static_mesh_with_material(sm_shell3, mat_shell3_1, unreal.Vector(-190,-2225,303), unreal.Rotator(55,111,-147), unreal.Vector(0.709104,0.709104,0.709104)) 
place_static_mesh_with_material(sm_shell1, mat_shell1_1, unreal.Vector(-1083,-2219,297), unreal.Rotator(58,60,122), unreal.Vector(1.07212,1.07212,1.07212))
#  ......
place_static_mesh_with_material(sm_shell3, mat_shell2_6, unreal.Vector(-617,-2511,319), unreal.Rotator(144,169,-116), unreal.Vector(0.929268,0.929268,0.929268)) 
place_static_mesh_with_material(sm_shell1, mat_shell1_1, unreal.Vector(-985,-1881,283), unreal.Rotator(-60,41,155), unreal.Vector(1.41147,1.41147,1.41147)) 


Теперь у нас есть код спавна 30 элементов, осталось выполнить скрипт в проекте.

File → Execute Python Script...
File → Execute Python Script...

На нашей сцене появятся разбросанные ракушки. При желании можно некоторые объекты поправить вручную.

Ракушки на сцене
Ракушки на сцене

Если вы раньше не практиковали подобные деяния, то вам может показаться сложными все эти этапы, кажется что быстрее разместить вручную, но если вы один раз подготовили рельеф, написали скрипт, теперь можно на карте размещать любые предметы (кости, камни, ветки), все что для этого нужно - изменить параметры нод Scatter в Houdini. У этого метода есть небольшое преимущество даже перед Houdini Engine - мы строим поверхность только из объектов, которые представляют из себя рельеф, убрав лишние элементы, такие как ветки, большие камни, шкуры и другие.

Спасибо за уделенное внимание.

Денис Береженко.

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


  1. rtdllnn
    09.04.2024 05:17
    +2

    В UE5 уже есть встроенный Procedural Content Generation.


  1. SHUstri
    09.04.2024 05:17
    +1

    Спасибо за статью!

    Но осталось два вопроса:

    1. а Houdini стал таки бесплатным? Бесплатная версия полнофункциональная? Раньше, вроде, только за деньги был.

    2. А чем не подошёл встроенный еще в EU4 Foliage tool? Он вроде как раз для "разбрасывания" мусора (объектов) по площадям, включая элементы рандомизации.