| On this page |
The Abelian sandpile model is an example for a self-organizing dynamical system and was introduced in 1987. The system applies rules when a sandpile becomes unstable and starts to slide downhill. This process depends on the pile’s slope.
The model is a 2D approach and therefore perfectly suited for Houdini’s heightfields. Once you've formulated the 2D methods, you can update the terrain’s height layer. With the help of a Solver SOP, you can simulate sand sliding down a pile and eroding its sides.
A basic script that simulates sliding sand requires two HeightField Wrangle SOPs. The first wrangle iterates over the terrain’s voxels and looks for its top, bottom, left and right neighbors. Then, the script determines the sliding direction with the help of a random function. Lets assume, the solver determined bottom as the voxel that will receive the sand. This means that the bottom voxel’s
height information must be updated. This, however, is not possible, because VEX doesn’t allow writing into neighbor voxels directly. The solution is to write this information to a separate layer, read out the value in a second wrangle, and apply it to the currently processed voxel.
Note
The main purpose of this VEX example is to show you a more advanced custom soltion. However, we don’t go into the depths of the Abelian sandpile model, and you can find very good explanations on the internet. The script is also commented and explains what happens in each code block.
Terrain ¶
Due to the nature of sandpiles, the terrain should not be too large. A size of approximately 100 m by 100 m is a natural limit, but not a physical barrier. Of course, you can also run the solver on large terrains, but at the cost of longer simulation times and (maybe) unexpected results. With large terrains we also recommend higher Grid Spacing values on the HeightField SOP to cut down simulation times.
The solver will also profit from strong in differences in height. A terrain that is relatively flat by default will hardly show any changes. With steep hills, on the other hand, you can also achieve an interesting terracing effect.
Scene setup ¶
You start with a basic heightfield. A HeightField Distort by Noise SOP can help to improve the terrain’s visual believability and increase the level of detail
For the heightfield’s Size, choose values between 30 and 100. With the HeightField Noise SOP’s you can shape the terrain. Amplitude controls the terrain’s height, Element Size creates more or less “spikes”. Use Offset to shift the fractal pattern and create different landscapes.
Then, add a HeightField Copy to Layer SOP. This node creates the layer that buffers the sand’s direction values. Set the node’s Destination to
delta. In many cases you want to get rid of this layer after the simulation, so lay down a HeightField Layer Clear SOP and make it the last node. For Layer 1, enter
delta.
Solver setup ¶
Put the Solver SOP between the copy and the clear nodes to connect its first input.
The node executes the simulation with a given number of Sub Steps. This number determines how often the wrangles will be applied to the terrain per frame. For the Abelian solver, a value around 5 gives good results. If you want the sand transport to become slower or faster, decrease or increase this value.
Then, double-click the solver to dive inside and add two HeightField Wrangle SOPs. The first inputs of the wrangle must be chained between the Prev_Frame and the OUT nodes. Now you have a working solver setup and it’s time to add the code.
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, // and user-defined thresholds. This is the first step in the // erosion-sedimentation cycle. // ============================================================================ 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"); // Scale factor to avoid extreme results // --- Compute the effective slope threshold for sliding --- float sliding_threshold = grid_resolution * slope_threshold; // --- Cache current cell height and grid position --- int x = @ix; int y = @iy; /* Define 4 cardinal directions (left, right, up, down). These are used to evaluate slope and determine possible sliding directions. */ vector dirs[] = array( set(-1, 0, 0), // links set( 1, 0, 0), // rechts set( 0, -1, 0), // unten set( 0, 1, 0) // oben ); vector slide_dirs[] = array(); /* 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. */ foreach (vector dir; dirs) { int nx = x + int(dir.x); int ny = y + int(dir.y); vector neighbor_pos = set(nx, ny, 0); // Grid position float h_neighbor = volumeindex(0, "height", neighbor_pos); // Neighbor height int unstable = (@height - h_neighbor) > sliding_threshold; if (unstable) { append(slide_dirs, dir); // Save direction } } // --- If there are valid sliding directions, choose one. int direction_code = -1; // Stable condition if (len(slide_dirs) > 0) { float seed_x = @Time + v@P.x * @height; float seed_y = @Time + v@P.y * @height; float rand = noise(set(seed_x, seed_y, 0)); int index = int(fit01(rand, 0, len(slide_dirs) - 0.001)); vector chosen_dir = slide_dirs[index]; if (chosen_dir == set(-1, 0, 0)) direction_code = 0; else if (chosen_dir == set(1, 0, 0)) direction_code = 1; else if (chosen_dir == set(0, -1, 0)) direction_code = 2; else if (chosen_dir == set(0, 1, 0)) direction_code = 3; } @delta = float(direction_code); // Direction as integer layer @mask = direction_code != -1 ? 1.0 : 0.0; // Mark instable cells //--- Save important values as detail attributes for further use. setdetailattrib(geoself(), "sliding_threshold", sliding_threshold, "set"); setdetailattrib(geoself(), "scale_factor", scale_factor, "set");
Parameters ¶
The first script above contains definitions for three 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
|
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 – Applies sand movement based on previously computed d // irections. This is the second step in the erosion-sedimentation cycle. // ============================================================================ float sliding_threshold = detail(0, "sliding_threshold") * detail(0, "scale_factor"); /* Define 4 cardinal directions (left, right, up, down). These are used to evaluate slope and determine possible sliding directions. */ vector dirs[] = array( set(-1, 0, 0), // Left set( 1, 0, 0), // Right set( 0, -1, 0), // Bottom set( 0, 1, 0) // Top ); float height_increment = 0; int x = @ix; int y = @iy; int opposite[] = array(1, 0, 3, 2); // Flip directions: left -> right, bottom -> top for correct results // --- Read the delta layer values and apply them to the currently process cell --- 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 (int(neighbor_delta) == opposite[i]) { height_increment += sliding_threshold; } } if (@delta != -1) { height_increment -= sliding_threshold; } // --- 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 flattened sides.