Houdini 21.0 Heightfields and terrains

Abelian sandpiles: Advanced

Erode heightfields with sand and wind.

On this page

This script is a more advanced version of the basic Abelian sandpile solver. With four cardinal directions, the results sometimes have an artificial or mechanical look, as if the hillsides were cut into blocks.

This script has some additions and extensions:

Setup

If you already went through the basic script, you can save the project under a new name and reuse the setup. Otherwise, please follow the descriptions on the page that provides the basic script.

Direction wrangle

This wrangle contains the custom parameters for adjusting the solver. The script here also determines, in which direction the sand should move and writes this direction to the delta layer. The problem is that heightfield layer’s aren’t capable of storing vector values. Therefore, it’s necessary to represent the eight possible directions as numbers. Parameters that are required to control the sand transport will also be stored as detail attributes.

Use ⌃ Ctrl + C to copy the code below. With ⌃ Ctrl + V you can paste the script to the first wrangle’s VEXpression field.

// ============================================================================
// EROSION DIRECTION WRANGLE – Determines sand sliding direction based on slope,
// wind influence, and user-defined thresholds. This is the first step in the
// erosion-sedimentation cycle.
// ============================================================================

// --- Retrieve simulation parameters from the UI ---
float grid_resolution = chf("grid_resolution"); // Size of each grid cell
float slope_threshold = chf("slope_threshold"); // Minimum slope required for sand to slide
float scale_factor = chf("scale_factor"); // Multiplier controlling erosion intensity
float sedimentation_scale = chf("sedimentation_scale"); // Controls how much sediment accumulates in low areas
float wind_angle_deg = chf("wind_direction"); // Wind direction in degrees (0° = +X, 90° = +Y)
float wind_bias_strength = chf("wind_bias_strength"); // Strength of wind influence on sand movement

// --- Compute the effective slope threshold for sliding ---
float sliding_threshold = grid_resolution * slope_threshold;

/* 
   Convert wind angle from degrees to radians and generate a unit vector
   pointing in the wind direction. This vector will be used to bias sand
   movement toward the wind.
*/
float wind_angle_rad = radians(wind_angle_deg);
vector base_wind_dir = set(cos(wind_angle_rad), sin(wind_angle_rad), 0);

/*
   Define the 8 possible neighbor directions:
   - 4 cardinal directions (left, right, up, down)
   - 4 diagonal directions (top-left, top-right, bottom-left, bottom-right)
   These are used to evaluate slope and determine possible sliding directions.
*/
vector dirs[] = array(
    set(-1,  0, 0), set( 1,  0, 0),
    set( 0, -1, 0), set( 0,  1, 0),
    set(-1, -1, 0), set( 1, -1, 0),
    set(-1,  1, 0), set( 1,  1, 0)
);

// --- Initialize array to store valid sliding directions ---
int slide_indices[];

// --- Cache current cell height and grid position ---
float h_self = @height;
int x = @ix;
int y = @iy;

/*
   Loop through all neighboring cells to evaluate slope.
   If the slope toward a neighbor exceeds the adjusted threshold,
   that direction is considered a valid candidate for sand sliding.
*/
for (int i = 0; i < len(dirs); i++) {
    vector dir = dirs[i];
    int nx = x + int(dir.x);
    int ny = y + int(dir.y);
    float h_neighbor = volumeindex(0, "height", set(nx, ny, 0));

    float slope = h_self - h_neighbor;
    float distance = length(dir); // 1.0 for cardinal, ~1.414 for diagonal
    float adjusted_threshold = sliding_threshold * distance;

    if (slope > adjusted_threshold) {
        append(slide_indices, i); // Store index of valid direction
    }
}

/*
   If there are valid sliding directions, choose one.
   The selection is biased toward the wind direction using a dot product.
   Directions aligned with the wind receive higher weights.
*/
int direction_code = -1;
if (len(slide_indices) > 0) {
    float weights[] = {};

    for (int i = 0; i < len(slide_indices); i++) {
        vector dir = dirs[slide_indices[i]];
        float bias = dot(normalize(dir), base_wind_dir); // Alignment with wind
        float weight = 1 + bias * wind_bias_strength;    // Wind bias scaling
        append(weights, weight);
    }

    /*
       Randomly select one direction from the weighted list.
       This introduces a stochastic approach while still favoring wind-aligned movement.
    */
    int pick = sample_discrete(weights, rand(@ix * 17 + @iy * 43 + @Time));
    direction_code = slide_indices[pick];
}

// --- Store the chosen direction as a float attribute for use in the next wrangle ---
@delta = float(direction_code);

/*
   On the first frame, store global parameters as detail attributes.
   These will be accessed in Wrangle 2 to ensure consistent values across voxels.
*/
if (@Frame == 1) {
    setdetailattrib(0, "sliding_threshold", sliding_threshold, "set");
    setdetailattrib(0, "scale_factor", scale_factor, "set");
    setdetailattrib(0, "sedimentation_scale", sedimentation_scale, "set");
}

Parameters

The first script above contains definitions for six custom parameters. To add the parameters to the wrangle’s UI, click the Creates sparse parameter for each unique call of ch() button. Note that the values in the table below are just suggestions and it’s likely that you have to adjust them with your terrain.

Parameter Description Value
Grid Resolution This parameter makes the erosion structures resolution-independent. You should use the value from the HeightField SOP’s Grid Space parameter. n/a
Slope Threshold Minimum slope required for sand to slide. Smaller values will consider less steep areas and you’ll get erosion in more areas. With very small values (<0.3), the results might look artificial. 0.7
Scale Factor With large height differences, you might see noise or other artifacts. Scale Factor parameter reduces this effect. You shouldn’t change the values unless you want to achieve a certain result. 0.25
Wind Direction The solver uses a basic wind model to avoid noise patterns on the sediment structure. Here you can just the wind’s direction in degrees. Note that the impact of the wind model is limited. 60
Wind Bias Strength This parameter controls the maximum influence of the wind on a voxel. The combination of Wind Direction and high Wind Bias Strength (>50) can help to reduce the terracing effect. 1
Sedimentation Scale With 0, you can turn off sedimentation completely. A value of 1 means mass-conservation and the amount of eroded sand equals the amount of sediment. Values greater than 1 will fill the terrain faster, values smaller than 1 will reduce the amount of sediment. 1

Erosion wrangle

This script reads the encoded direction values of the delta layer to determine voxels with sliding sand. The script also updates the terrain’s height layer and applies the sedimentation process based on the custom parameters from the first wrangle.

Use ⌃ Ctrl + C to copy the code below. With ⌃ Ctrl + V you can paste the script to the second wrangle’s VEXpression field.

// ============================================================================
// EROSION APPLICATION & SEDIMENTATION WRANGLE – Applies sand movement based on
// previously computed directions, and adds sediment in low-lying regions.
// This is the second step in the erosion-sedimentation cycle.
// ============================================================================

// --- Retrieve global parameters stored in Wrangle W1 ---
float sliding_threshold     = detail(0, "sliding_threshold") * detail(0, "scale_factor");
float sedimentation_scale   = detail(0, "sedimentation_scale");

/*
   Define the 8 neighboring directions:
   - 4 cardinal directions (left, right, up, down)
   - 4 diagonal directions (top-left, top-right, bottom-left, bottom-right)
   These are used to evaluate incoming and outgoing sand flow.
*/
vector dirs[] = array(
    set(-1,  0, 0), set( 1,  0, 0),
    set( 0, -1, 0), set( 0,  1, 0),
    set(-1, -1, 0), set( 1, -1, 0),
    set(-1,  1, 0), set( 1,  1, 0)
);

// --- Cache current voxel position ---
int x = @ix;
int y = @iy;

// --- Initialize height change accumulator ---
float height_increment = 0;

// --- Retrieve own sliding direction from previous wrangle ---
int my_delta = int(@delta);
vector my_dir = my_delta >= 0 ? dirs[my_delta] : {0, 0, 0};

/*
   Loop through all neighbors to check if any of them are sliding sand
   toward this cell. If a neighbor's sliding direction points toward us,
   we receive sand from them.
*/
for (int i = 0; i < len(dirs); i++) {
    int nx = x + int(dirs[i].x);
    int ny = y + int(dirs[i].y);
    float neighbor_delta = volumeindex(0, "delta", set(nx, ny, 0));

    if (neighbor_delta >= 0) {
        vector neighbor_dir = dirs[int(neighbor_delta)];

        /*
           If the neighbor's direction is opposite to the current direction,
           it means sand is flowing into this cell.
        */
        if (length(neighbor_dir + dirs[i]) < 0.01) {
            height_increment += sliding_threshold;
        }
    }
}

/*
   If this cell is actively sliding sand away (i.e. has a valid direction),
   subtract sand from its height.
*/
if (my_delta != -1) {
    height_increment -= sliding_threshold;
}

/*
   If this cell is not sliding (i.e. stable), check if it lies in a depression.
   If the average height of its neighbors is higher, we simulate sedimentation
   by adding sand proportionally.
*/
if (@delta == -1) {
    float h_self = @height;
    float h_sum = 0;
    int count = 0;

    for (int i = 0; i < len(dirs); i++) {
        int nx = x + int(dirs[i].x);
        int ny = y + int(dirs[i].y);
        float h_neighbor = volumeindex(0, "height", set(nx, ny, 0));
        h_sum += h_neighbor;
        count++;
    }

    float h_avg = h_sum / count;
    float sediment_amount = clamp(h_avg - h_self, 0, sliding_threshold);

    // Add sediment scaled by user-defined factor
    height_increment += sediment_amount * sedimentation_scale;
}

// --- Apply accumulated height change ---
@height += height_increment;

Result

Once you've adjusted the parameters, you can go to the Playbar and click the Play button to start the simulation. The script should be rather fast and in most cases you won’t have to simulate for more than 50-70 frames.

The video shows how a terrain is transformed from rough and rocky mountains to an eroded landscape with debris mounds. To avoid a very smooth look, you can terminate the network with another HeightField Distort by Noise SOP with moderate settings to keep the sandy appearance.

When you look closely at the right part of the terrain, you can see how the sand is flowing down. To enhance this effect, the solver’s Sedimentation Scale was set to 1.6.

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