.webp)
.webp)
This was the workflow for the cliff generator, allowing the quick creation of multiple modular cliff assets with varying shapes and sizes. Here are the high level steps:
These are general features that are shared between the generators, including things for quality of life, workflows, and exporting. Some of these features were not planned for the start, but were solutions to issues that surfaced and weren't thought of during development. A lot of iterations were made, ensuring that using the tools were as easy as possible.
The export path parameters for FBX Output and Maps Baker is very basic, requiring the export path, file name with extension, and expressions for texture baking.
I wanted to simplify the process by automating some aspects and add some naming flexibility. Furthermore, the base shapes files provided are already named properly, so I can definitely put that in use.
string lowExportPath = chs("lowFilePath");
string lowCustomFileName = chs("customFileName");
string lowSuffix = chs("lowSuffix");
string fbxInputName = prim(1, "name", 0);
string fileName;
int lowFileNameSetting = chi("Low_File_Name_Setting");
int addSuffixSetting = chi("Add_Suffix");
lowCustomFileName = replace(lowCustomFileName, ".fbx", "");
if(lowFileNameSetting == 0)
{
fileName = fbxInputName;
}
else
{
if(len(lowCustomFileName) < 1)
{
lowCustomFileName = "NO_NAME";
} fileName = lowCustomFileName;
}
if(addSuffixSetting == 1)
{
fileName += "_" + lowSuffix;
}
s@lowFinalPath = lowExportPath + fileName + ".fbx";
When converting the mesh to high poly, VDB to polygon is used. However, VDB nodes only offer voxel size as a parameter, while setting a certain poly count is much more intuitive for me and other artists. Furthermore, there was an issue when selecting a larger asset, where the polycount would sky rocket and crashed my computer.
It would be much better if poly count is consistent no matter what size the object is. Because of that, I approximated the size of the voxel based on a target poly count and surface and programmed it with VEX.
int polyCount = chi("Poly_Count"); //value obtained from a parameter
float voxelDensity = polyCount/(f@totalArea * 1.25); //final polycount overshoots quite a bit, and I found increasing the final area by 25% results in a close enough poly count
float voxelArea = 1/voxelDensity;
float voxelSize = sqrt(voxelArea);

When converting VDB's to polygons, on a regular shaped mesh (like a cube), the end result is a mesh made out a grid of evenly sized squares. The size of those squares corresponds to the VDB's voxel size. Because of that, the amount of polygons can easily be deduced from the area of the mesh and the are of the square. However, for irregular shaped meshes, like the cliffs, I found that multiplying 1.25 to the mesh total area makes the final poly count close to the target.
This setting is not intended to be very accurate, but to make using the tool easier and safer. In those terms, I think it's good enough.


While tinkering around with parameters, I quickly found out that a detailed high poly mesh can take long time to update, increasing iteration time and causing frustration. Because of that, I made a low resolution toggle to make the mesh use a lower poly count version.
After optimizing the generator leter on in development, this feature was not as critical, but it was useful for the initial stages.

The total noise applied to the mesh is saved throughout the generation and is used to help guide the low poly mesh creation. To keep the protruding shapes, total noise is used as a weight parameter in the Polyreduce node. This makes the polygon density of the larger shapes higher, keeping its shape.

Some cliff meshes were used for decoration, others served as surfaces for the player to stand on. The latter requires the topside to be almost flat, so a noise height multiplier is added to allow this.



The first area of the game, contains cliffs with large and sharp pertruding shapes on their surface, emulating the effects of rock breakage. The end product is a modular kit of cliff meshes that can be arranged in different ways to create unique formations. A lot of iterations were made during this generator's development, making sure that it is as artist friendly as possible.
I was given reference images and found some my own to help create the Overworld Cliff Generator. The cliffs have sharp faces with a somewhat long rectangular shaped patterns. With the stylized look the game is going for, the cliffs should have clear shapes, with micro details coming mostly from the texture.

Images obtained from Quixel Megascans

With the high poly version base shape, the first set of noise is applied. This creates the larger shapes on the mesh. Then a second set of noise for smaller shapes in between the larger shapes. When combining the two sets of noise, the maximum value between the two in a given point is used. After that, a final layer of noise is applied to add some texture.
Sharp edges on rocks generally erode and break first from weathering. Because of that, I added edge wear to make the surface look more natural and breaks the pattern of the edges.





An earlier version of the generator used to do a voronoi fracture to create the surface shape. While it does create some interesting results, it wasn't really what the team was looking for, making look too busy. Furthermore, its difficult to make sure that the final silhouette doesn't steer too far away from the base shape given.



The second area of the game, taking place in an area filled with caves and near a volcano. The generator creates layers, flat shapes, and cracks on the cliff surface. The same as the overworld cliffs, different meshes can be combined to create unique formations. This generator reuses a lot of features from the overworld generator, especially quality of life and export features.
The cliffs in this section is mainly based on the Mammoth Cave, which reference images was given to me. The walls contains visible layers with clear seperation lines, with part of the wall being flat and mostly smooth, while other parts are rough.

The base shape is cut into somewhat evenly spaced pieces using deformed grids and a boolean node. For every layer or piece, convert to VDB with a large voxel size, then to convert to polygon. This smooths out the edges and makes the separation clearer.

To make sure that each layer has separated surface detail, each layer's noise position is offset by a piece attribute, or a index number for each layer. In VEX, it would look something like this:
v@alteredP = v@P + i@piece * 5.235; //5.235 is just a random constantLater on, the location attribute in the noise node will use alteredP instead of just P. This makes each layer's noise disconnected from one another.
An example where this is used is on the next step for a noise mask, which controls which parts have flat smoother surfaces and rough surfaces. The mask generated are disconnected for each surface, enforcing the idea that these are separate layers.

Using the noise mask, the mesh will have two different sets of noise applied: