N. Joseph Burnette - R&D in Tech Art
Posted on
Uncategorized

Unified Scattering Workflows

Author

There are a lot of tools for in-engine foliage scattering out there. When I started building my own solution (pre-UE PCG days) artists had a lot of feature requests that went beyond just scattering on landscapes. The goal has always been to empower environment (ENV) teams to handle as much as possible directly inside the engine, minimizing the constant back-and-forth between DCCs. That meant building tools that didn’t just place foliage, but also provided deeper control, corrected geometry errors on the fly, and maintain iteration speed within a non-destructive workflow.

Below, I’ll walk through a few key features I’ve implemented for ENV artists within one compact and unified UI, along with insights into what drove each feature’s inclusion as well as a bit of what's going on under the hood.

Scatter-on-Scatter (non-destructive)

In this scenario, the object to be scattered on as well as the scattered objects both remain instances within the engine, however - the tool must dynamically seek each instanced object's actual geo, scatter points, and export that additional point cloud back to the engine. Each point references assets to be instanced.

enter image description here

The interesting thing is that Houdini Engine can be made to dynamically export the actual geometry for every instance out of the engine for processing in Houdini, without any intervention from the artist. There are two exact HDAs. One scatters rocks and the other takes the output of that and scatters flowers - directly within the game engine - and the results is flowers scattered on rocks.

enter image description here

First, in VEX we gather all the unique instances that is coming from the engine. Each instance contains the location of the geometry that is being instanced:

string client_name = detail(0,"client_name",0) + "_instance";

int uniqueinstances = nuniqueval(0,"point",client_name);

for (int i = 0; i < uniqueinstances; i++) {
    string val = uniqueval(0, "point", client_name, i);
    s@val = val; 
}    

i@uniqueinstances = uniqueinstances; 

Then in Python, populate an Object Merge node with the paths for all unique instances that were found:

node = hou.pwd()
geo = node.geometry()

importer = hou.node(hou.pwd().evalParm("spare_input0"))
override = hou.node(hou.pwd().evalParm("spare_input1"))


if importer.parm('numobj').eval() == 0:
    if geo.findGlobalAttrib("scatter") and geo.attribValue("scatter") == 1:
        if geo.iterPoints():
            client_name = geo.attribValue("client_name")
            attribute_name = "{}_instance".format(client_name)

            point = geo.iterPoints()[0]
            num_instances = point.attribValue('uniqueinstances')

            importer.parm('numobj').set(num_instances)
            override.parm('numobj').set(num_instances)

            instances = [pt.attribValue(attribute_name) for pt in geo.points()]
            unique_values = list(set(instances))

            for index, value in enumerate(unique_values, start=1):
                importer.parm(f'objpath{index}').set(value)
                override.parm(f'objpath{index}').set(value)

Note that the code above is structured to be engine-agnostic, and follows the same template as described here: Building Game-Engine-Agnostic Tool Ecosystems

On the node that's doing the final scattering, you'd want to ensure that "On Asset Input Cook" is checked on the HDA Asset Actor itself, to ensure that every change on the upstream node gets propagated downstream to the current connected node.

enter image description here

Hidden Faces & Small Islands

Artists generally want the scatter tool to handle various scenarios with minimal additional intervention. One such interesting scenario is the automatic culling of objects that are directly below a particular spawn point, such as spawn points that may be on a separate platform below the one that can be currently seen. Additionally, there should be options to cull 'small islands' and threshold parameters to control which small islands are culled.

enter image description here

By extension, the solution should deal with the surfaces in the event that they overlap: enter image description here

The implementation itself is simple enough to get the client's desired results, and runs fast enough using multi-threaded VEX on the Houdini side when outputting thousands of points (culling of islands is achieved with separate nodes). We first shoot rays down in -y, then keep track of the number of hits for each ray for sorting and culling later:

vector hit_pos[];
vector hit_uv[];
int hit_prim[];
string target_value = "hitprim";
float eps = ch('threshold');

vector min_pos = getbbox_min(1);
vector max_pos = getbbox_max(1);
float bbox_y = max_pos.y - min_pos.y;


vector dir = {0, 1, 0} * bbox_y;  // Ray direction (positive Y-axis)


intersect_all(@OpInput2, @P, dir, hit_pos, hit_prim, hit_uv, eps, eps);

int num_hits = len(hit_prim);

setpointattrib(geoself(), "hit_prim", @ptnum, hit_prim, "set"); //debug
setpointattrib(geoself(), "num_hits", @ptnum, num_hits, "set");

The data and geo that ENV artists work with in the engine is oftentimes very messy. Where this has come in extremely handy is when artists use hightfields which consists of multiple patches of landscapes haphazardly placed together to form one continuous landscape (surprisingly common!): enter image description here

Inside Faces (Airtight Geo)

Another common request is the culling of spawn points on inside faces of airtight geometry that are overlapping with each other: enter image description here

When it comes to optimising workflows in a non-destructive manner, there's a lot that can go into a foliage scatter tool. I'll probably add more to this post at a later date, such as scattering based on occlusion, based on surface analysis (slope, curvature) etc.