Houdini 21.0 Heightfields and terrains

VEX scripts for heightfields

Customize terrains, masks, and layers through VEX.

On this page

This collection contains several useful VEX snippets as a starting point for more complex scripts. Most scripts let you create different types of masks, but you can also control the height layer. Before you start, please consider a couple of things.

Tip

The Removing scatter points page also contains various VEX scripts and techniques you might find interesting.

Getting voxel values

The process of reading out a heightfield’s voxel value is always the same. However, the type of data, returned by the function, depends on a layer’s content. When you go over a height layer, fo example, you will get height values. If it’s a mask layer, you’ll get the intensity of the mask at each voxel, and so on.

However, height and mask are special cases, because they're build-in layers and evaluated automatically just by calling them as attributes @height and @mask. But you can also read out the values of this layers manually. The following function reads the mask layer from input 0, and through all voxels.

float mask_value = volumeindex(0, "mask", set(i@ix, i@iy, i@iz));

Here’s a more generic form with @P instead of the grid’s inidices.

signature layer_value = volumeindex(int input, "layer_name", @P);

Circle mask

Circles are certainly one of the most often required shapes for masking, e.g. if you want to create craters, cut out a patch, and whatnot. Fortunately, it only takes a few lines of VEX code to create such a mask.

vector position = chv("position");
vector offset = set(position.x, 0, position.z);

if (length(v@P - offset) < chf("radius")) {
    @mask = 1;
}

The first line reads the values from a custom position vector. This vector defines the circle’s center. The offset vector uses only the X and Z components of position, because heightfields don’t have a Y value. If a voxel lies within the circle with a given custom radius, it’ll be added to the mask,

You can blur, invert, shrink or expand the result with appropriate nodes like any other mask.

Ring mask

Instead of a circle, you can create a ring by defining a custom inner and outer radius.

vector position = chv("position");
vector offset = set(position.x, 0, position.z);
float midpoint = length(v@P - offset);

if (midpoint > chf("inner_radius") && midpoint < chf("outer_radius")) {
    @mask = 1;
}

The code is very similar to the script from Circle mask. The main difference is that you compare against two values here, not just a single Radius. And, instead of writing length(v@P - offset) twice inside the if-clause, the term is now a separate midpoint variable.

Oval mask

Instead of a circle you can also create an oval mask. You need two parameters to define the oval’s X and Y dimensions. An offset defines the distance from the terrain’s default origin at [0,0], and you can also rotate the shape.

float a = chf("x_dimension");
float b = chf("z_dimension");
vector offset = chv("offset");
float rotation = radians(chf("rotation")); // Degree -> radians

// Position of the current voxel relative to its offset
vector pos = set(@P.x - offset.x, @P.z - offset.z);

// Rotate the ellipse
vector rotated_pos;
rotated_pos.x = cos(rotation) * pos.x - sin(rotation) * pos.y;
rotated_pos.y = sin(rotation) * pos.x + cos(rotation) * pos.y;

// Test ellipse equation with transformed coordinates
float ellipse_equation = pow(rotated_pos.x / a, 2) + pow(rotated_pos.y / b, 2);

if (ellipse_equation <= 1.0) {
    @mask = 1.0;
}    

The first four lines define the parameters for customizing the ellipse.

Then, the script calculates a position vector with the offset. This vector determines the oval’s center. The rotated_pos vector stores the rotated position values. The ellipse_equation defines the inner area of the ellipse and if this value is smaller than or equal to 1.0, a voxel is inside the ellipse and part of the mask.

Height mask

You can achieve this effect with the HeightField Mask by Feature SOP, but here’s a short and handy script for masking voxels inside a lower and an upper limit. You can, for example, add this snippet to other scripts or write your masking tool with custom parameters.

if (@height >= chf("lower_limit") && @height <= chf("upper_limit") ) {
    @mask = 1;
}

If the terrain’s height is between the values of the custom channel for lower_limit and upper_limit, the voxel will be added to the mask.

Gradient mask

Some of Houdini’s heightfield nodes don’t provide gradient masks, so they could be a good addition for your arsenal of tools.

vector gradient = volumegradient(0, "height", v@P);

if (fit01(length(gradient), 0, 1) > chf("threshold")) {
    @mask = 1;
}

The first line reads the volumegradient from the height layer at the wrangle’s first (0) input, using the voxel @P positions.

The length of the gradientvector from line 1 is remapped to a 0, 1 range to make it easier to compare. If a voxel’s gradient value is greater than a custom threshold, the voxel will be part of @mask.

Clipping

This short script caps the heightfield above a custom threshold. If height is greater than or equal to the threshold value, the maximum height will be the adjusted value.

float threshold = chf("threshold");

if (@height >= threshold) {
    @height = threshold;
}

3D Worley noise mask

Houdini’s VEX language provides a wide range of different noise types. You can use the different noise types to define masks, but also to create terrains. You can find an example for the latter case directly below.

// Variables
vector frequency = chv("frequency");
float amplitude = chf("amplitude");
vector offset = chv("offset");
int seed;
float f1, f2, f3, f4;

// Noise values
wnoise(v@P * frequency + offset, seed, f1, f2, f3, f4);
float noise_value = f1 * amplitude;

// Create mask
if (noise_value > chf("threshold")) {
    @mask = 1;
}  

Applying masks to snoise

In this script, the terrain’s height is calculated through a snoise function. An already existing mask layer attenuates the height values and the noise’s element size.

You can, for example, add an upstream HeightField Mask Noise SOP to create an initial mask.

float amplitude = chf("amplitude");
float element_size = chf("element_size") * @mask;
int turbulence = chi("turbulence");
int period_x = chi("period_X");
int period_y = chi("period_Y");
int period_z = chi("period_Z");
float attenuation = chf("attenuation");

@height = snoise(@P * element_size, period_x ,period_y, period_z, turbulence, 0, attenuation) * amplitude * @mask;

The lines with leading float and int statements create the custom parameters for configuring the snoise function below.

The script calculates the terrain’s height values with a fully parametric snoise function. The noise is then multiplied with amplitude * @mask at the given position @P.

Ramp to heightfield

This script converts a ramp into a heightfield. You can draw a curve through a ramp and the terrain will follow the curve’s shape. You can also define a custom height_factor to scale the terrain.

vector bounding_box = relbbox(0, @P);
float elevation = chramp("remap", bounding_box.z);

@height += elevation * chf("height_factor");

The script creates a bounding box around the heightfield and samples the positions @P inside. The Z position is added to custom ramp to calculate the terrain’s elevation.

The final @height is summed up from the product of elevation and a custom height_factor.

Curve follows terrain

You can transform a curve so that it follow a terrain. The script runs inside a HeightField Wrangle SOP. In fact, this method is pretty much what the HeightField Project SOP, when you project geometry onto a terrain.

  1. Inside the Geometry OBJ with the terrain, add a Curve SOP. You can also use one of the predefined tools for polygons, Bezier curves or splines.

  2. Hover the mouse cursor over the viewport and press Enter to turn on the node’s drawing mode.

  3. Also on the viewport, press 1 to change perspective to top and draw the curve in the XZ plane.

  4. With polygons, -click to draw the curve points. With Bezier curves or splines, -click and drag the mouse to add curvature.

  5. Once you're happy with the curve, press ESC to leave the drawing mode.

  6. Lay down a Resample SOP and connect its input with the output of the curve. This node increases the number of curve points to get a sufficient amount of sampling points. The default Length should create enough resolution.

  7. Connect the resample node’s output to the wrangle’s first input and the heightfield to the second input. The connection order is important to get correct results.

vector hit_pos, hit_uv;
int prim = intersect(1, @P, {0, -1000, 0}, hit_pos, hit_uv);

if (prim >= 0) {
   @P = hit_pos; 
   @N = prim(1, "N", prim)
}

The centerpiece of the script is the intersect function. This function sends out a ray from the curve towards the heightfield at input 1. Direction and maximum distance to search for intersections are defined in {0, -1000}.

If the ray hits the terrain, the pos vector will contain the position of the intersection. With a direction of 0, the pos vector’s Y component equals the terrain’s height.

The last line transfers the pos vector to a curve point’s position @P. The uv vector is not used here, but it’s a mandatory argument of the intersect function.

Combining masks

You can combine various masks and apply a ramp to modify the result and create something entirely new.

  1. Add a HeightField Wrangle SOP somewhere in your network, but downstream of the masks/layers you want to combine.

  2. Use ⌃ Ctrl + C and ⌃ Ctrl + V to copy and paste the code below to the wrangle’s VEXpression field.

    This example code below uses @slope and @occlusion layers. You must replace the layers with the actual names from your network. If you want to add more layers, just append them to the formula through multiplication: @layer_1 * @layer_2 * ... * @layer_n

  3. Click the Create spare parameter… button to create the ramp.

@mask = @slope * @occlusion; // Use your actual layer names here

Heightfields and terrains

Creation

Scattering

Masking

  • Masking

    Define zones of interest and detail.

  • Light masks

    Create masks from the sunlit areas on a terrain.

Natural effects

  • Erosion

    Turn mountains into dust.

  • Slump

    When mountains crumble to rocks.

  • Flow fields

    Let it flow (down the mountain).

VEX

Texturing

Shallow Water Solver