Procedural environment with Houdini and Unreal Engine 4 (UE4)

September 13, 2020 -
Back

Why to create a modular level procedurally inside UE4?

Producing environments is a highly consuming task. It involves creating a meaningful Level Design layout and proper Art dressing on top of it.

This is addressed in a linear workflow traditionally (first, do the design, then art) but this is an stiff and outdated way to work, due to any layout change leads to destroying existing artwork.

"Remember that sci-fi room you filled in with teleport cells, robotic doors and a ton of pipes in two weeks time? Well, now it is double its size, you need to rearrange everything"

Thinking of ultra-planned projects to minimize changes is just naive idea.

Producing a game or a movie is an alive entity and needs to evolve as it progresses. That´s why Houdini engine comes in handy: it is able to run any tool (aka HDA, otl and Houdini digital asset) inside other platfoms like UE4, Unity, Maya, Max and Cinema4D. So, what about designing an hda to produce those modular levels inside Unreal Engine on the fly?

do level design and dressing at once


Designing an HDA for modular level building

You can find a few projects about creating modular environment with Houdini and Unreal Engine 4 (UE4), like this excellent tutorial by Simon Verstraete

After some research, I realized that procedural artists in charge of  creating the tool, already know the measurements of the wall modules and/or stick to round values (1m, 1.5m, 2m and so on), which eases things a lot because you can hardcode those distances in Houdini and rely on them to build your system. But wait, that means you would need to tweak the tool depending on your inputs! What if you add a new wall module 1.75m width?!

Once I spotted that, I decided to aim for a more powerful approach: design the tool so that adapts to different module widths, which would boost the HDA’s flexibility. I also included the ability of creating fully populated rooms.

With this approach, I can change those measurements in different projects very easily, so that the tool will pick the modules randomly, align and finally scale them to assemble the walls perfectly.

VEX calculations, despite of not being too complex, became a bit challenging till I found a good approach, though. You can check technical breakdown at the bottom of this page.

The results: Game-ready modular level generation with a curve and boxes as inputs

Modular level builder - HDA Technical Breakdown

Due to VEX wrangles play a key role, I added an in-detail section at the bottom of the page including most important code snippets. 

The goal is designing a houdini tool (HDA) that allows the user to create finished game ready environment with minimum effort and maximum flexibility.

 

Design guidelines:

  • Rely on in-engine curve editor for creating layout.
  • Able to creating ellaborated silhouettes.
  • The assets will be fed dynamically from editor’s content browser. That means, the tool must be unaware of the modules it is gonna be receiving as input.
  • Modular (for this demo, I used Megascan assets)

The idea is to be able to allow the user to create rooms from simple shapes, which will be processed and connected to the floor layout (explained in “Floor layout” tab). 

To do so, I firstly processed every shape coming from the editor as “override input”, so that I could generate a grid based floor.

First thing I do is creating a box for every input, which is easily done with a “for each connected piece” loop containing a bound node. This will ensure the creation of a simple box, no matter the shape used by the user to feed the tool with (you can drag any existing uasset as input, even a T-Rex, which might lead to unstability of the tool).

Then removing all faces except one, dividing it using the “bricker” option, according to the selected size. Finally, using the “snap” node set to grid.

A good practice I got used to some time ago is trying to solve as much as I can through VEX, like removing the overlapping faces in this example.

Sometimes it is overkill, because you might spend a full evening solving something to later finding a node doing exactly that in a more efficient way, which might seem I lost my time.

However, the more I do it, the more I think it was a good decission, because this forced me to be more and more proficient with this scripting language.

</p><p>vector pos={0};<br />vector uv;<br />int intersection=intersect(0,@P,{0,-100000,0},pos, uv);</p><p>if(intersection!=@primnum) removeprim(0,@primnum,1);<br />i@intersection=intersection;</p><p>

I start copying a box, which dimensions are promoted to parameter. (This will allow tweaking the corridors width later). The curve is resampled to points spaced 1m. However, this doesn´t ensure the points will be correctly aligned to grid. (A point can be sitting in position P=(1.25, 2.01, 35.6), no matter its distance to its neighbour point) To fix this, I preferred to rely on this rounding trick, which snaps the points based on the “grid” variable. It is a cheap and pretty powerful way for moving the points around, you get unexpected point placements when playing with the treshold, by the way.

//Ground curve
@P.y=0;

//
float grid=chf("Floor_grid_unit_size");
@P.x=rint(@P.x*grid)/grid;
@P.z=rint(@P.z*grid)/grid;

  Then I remove overlapping, assign basic normals and, finally, created a point in the middle of each primitive with this code. Tthis way I found out that despite of not existing  such “@P” attribute for primitives, it actually works as primitive centroid when fed into a vex function. (There are many different ways to work when snapping stuff to the grid, and I found this approach working nicely). I use this points to copy a grid later. I finally merged this grid with the previous one created within the “overrides” section.
This is the heart of the tool. Below a visual overview of the process. I start by isolating the “inbetween” points from the corner points with the group node and some auxiliary primitive, like a platonic shape. I tend to use less geo possible and set this shape to be a pyramid because it is the simplest closed shape, but I am not sure wheter this is the most efficient way of grouping points with second-input geo -like an sphere set to primitive. Once I resampled the line, I tell each point: “You will be assigned to instance a module of X meters width”. With some VEX, I report the system the possibilities (check red arrow) and assign this “width_pick” randomly. PS: I forgot promoting this to a parameter so that this array of suitable widths is assembled dinamically based on user input.

//Goal: oofset points within a prim according to possible offsets
//(aka modules_widths)

//1. Find first and last point of prim
//2. Move all prim points to first point position
//3. Find the prim axis
//4. For each point, offset it randomly according to
//the axis+suitable offsets
//5. Do this for all points
//6. Measure both original and current prim (offsetted)
//7. Remove points with measure higher than original prim

float wall_widths[]={1.5,2,4};
float seed,pick;
seed=chf("Seed");

//input possible offsets
int pts[]=primpoints(0,@primnum);
int primpts=len(pts)-1;
int pt0=vertexpoint(0,primvertex(0,@primnum,0));
int pt1=vertexpoint(0,primvertex(0,@primnum,primpts-1));
vector prim_axis=normalize(point(0,"P",pt1)-point(0,"P",pt0));

float perimeter=primintrinsic(0,"measuredperimeter",@primnum);

vector tempZeroPos,newPos;
float tempWidth;

foreach(int pt;pts)
{
tempZeroPos=point(0,"P",pt0);
setpointattrib(0,"P",pt,tempZeroPos,"set");
pick=int(rint(fit01(rand(seed+pt+@primnum),0,len(wall_widths)-1)));

tempWidth=wall_widths[pick];

setpointattrib(0,"width_pick",pt,tempWidth,"set");
setpointattrib(0,"prim_axis",pt,prim_axis,"set");
setpointattrib(0,"perimeter",pt,perimeter,"set");
}

//f@pick_width=wall_widths[pick]
//v@prim_axis=prim_axis;

i@pt0=pt0;
i@pt1=pt1;
i@primpts=primpts;
f[]@wall_widths=wall_widths;

Doing the offset comes next, and it involves grabbing previous points random widths and accumulate those. As a result, points are offsetted. Comparing resulting expanded prim and original unexpanded y can tell which points are beyond original bounds. In this case I tried using the xyzdist() VEX function. It is super-handy and one of my favourite functions due to it provides you with a ton of information: the distance from a point to closest geo, the prim number and its parametric uvs.

int pts=len(primpoints(0,@primnum))-1;
vector cen=v@P;

float widths[]=f[]@widths;
float tempWidth;

float offset=0;
int pt_counter=0;
int startPt=i@pt0; //first point of each prim

vector startPos=point(0,"P",i@pt0); ////Grab position of each first point
vector prevPos=startPos;
vector newPos={0};

foreach(float w;widths)
{
offset=widths[pt_counter-1];
newPos=prevPos+v@prim_axis*(offset);
setpointattrib(0,"P",startPt+pt_counter,newPos,"set");
prevPos=newPos;
pt_counter++;
}

f@new_perimeter=offset;
i@pts=pts;
f[]@widths=widths;
f@new_perimeter=offset;

After removing passing points, I also needed to adjust final point, which required more VEX scripting than expected. Compared the original primitive with its “crippled” version (removing passing points) which threw a gap. Comparing the gap measurement with the available module widths resulted into four different possibilities that needed different actions:

//possibilities:
//1. Gap inferior to min width
//2. Gap equals min width
//3. Gap major to min width
//4. Gap inferior than last pt widht_pick

int pts[]=primpoints(0,@primnum);
int last_pt=vertexpoint(0,primvertex(0,@primnum,len(pts)-1));

float gap=f@gap;
float min_width=min(f[]@wall_widths);
float widths[]=f[]@wall_widths;

//1. Gap minor to min width
if(gap&lt;min_width) { removepoint(0,last_pt); float width_pick=point(0,"width_pick",last_pt-1); float k=(gap+width_pick)/width_pick; vector scale=set(k,1,1); setpointattrib(0,"scale",last_pt-1,scale,"set"); } //2. Gap equals min width if(gap==min_width) { setpointattrib(0,"width_pick",last_pt,min_width,"set"); } //3. Gap major to min width //4. Gap inferior than widht_pick float width_pick=point(0,"width_pick",last_pt); if(width_pick&gt;gap)
{
//Check if existing matching w value for that gap first
int solved=0;
foreach(float w;widths)
{
if(w==gap)
{
setpointattrib(0,"width_pick",last_pt,w,"set");
solved=1;
break;
}
else
{
}
}

if(solved==0)
{
int index=0;
float closest_width;
float min_difference=9999999;
float difference;
foreach(float w;widths)
{
difference=abs(w-gap);
if(difference&lt;min_difference)
{
min_difference=difference;
closest_width=widths[index];
}
index++;
}

setpointattrib(0,"width_pick",last_pt,closest_width,"set");
float k=gap/closest_width;
vector scale=set(k,1,1);
setpointattrib(0,"scale",last_pt,scale,"set");

}

}

The only thing remaining was to split the points according to its random width assignment and pair that with the proper unreal_instance attribute.
Instancing the props was the easiest part. I used the grid layout (and its perimeter) resulting from combining the corridors and rooms as starting point to place the elements (pillars, tables, chairs, benches, decorative props, etc) in order of importance. For instance: pillars where rated higher, so wherever there is not a pillar, a table could be placed. Wherever there is no pillar nor table, a chair, and so on. In other words, I kept removing available areas as I kept spawning uassets, like in the following example. Removing first and last point of each prim (tips):

int pts=len(primpoints(0,@primnum));
int last_pt=vertexpoint(0,primvertex(0,@primnum,pts-1));
int first_pt=vertexpoint(0,primvertex(0,@primnum,0));

setpointgroup(0,"last_pt",last_pt,1,"set");
setpointgroup(0,"first_pt",first_pt,1,"set");

Removing points too close to corridor:

int neirs[]=nearpoints(1,v@P,chf("Search_radius"));
if(len(neirs)&gt;1) removepoint(0,@ptnum);

//I consider a gap between pts of at least 1m

Whenever facing tasks I´ve met before I try to vary my approach as much as I can to force myself to use different VEX functions and approaches.  For either removing or grouping points based on its distance to another geometry, I usually use xyzdist() and nearpoints(), 

Q: Does it work in other engines like Unity?

A:  Houdini Engine plugin nicely connects with Unreal and Unity (but I read somewhere sideFx provides the Houdini API to build your own custom plugin). So I´d focus my answer on Unity: Yes,  it might work, but I would need to care about two subjects: interface-related and marshalling data-related.

For example, there are special attributes used to commnunicate between Unreal and Houdini, like “unreal_instance”. In the case of Unity, it is called “unity_instance”.

Regarding interface, you need to know the way other platforms display the data, which will affect the way you tailor the tool. For instance: 3dsMax is not able to make graphic representation of a ramp, whilst Maya does not support to type-in values lower than 0.001.

From my experience, it is a bad practice to aim for a tool to work in all platforms supported by the Houdini Engine plugin, which leads to misfunctionalities 

Q: What would I change if creating the tool from scratch?

A:  The more hdas I create, the more I realize that authoring a tool is like climbing a mountain: only when you reach the top you realize about other suitable paths and dangers avoided, you get the big picture. Therefore, chances are I would solved things differently. Many times I feel tempted of redoing some parts or even restarting from scratch, but time is limited so I query myself: does the tool meet design requirements and behaviour is reasonably good? If the answer is yes, the job is done.

Q: You got any question? Drop me a line!

Leave a Reply

Your email address will not be published. Required fields are marked *


Play Cover Track Title
Track Authors