Boulder fields
Procedural rock populations with a geological basis. A rock group in
bottom.rocks becomes a boulder field when it names a preset or sets
any of the knobs below; a group with neither runs the plain rock
populator exactly as before. Implementation: simulator/boulder_field.py.
Presets
| Preset | Site | Lithology, shape | Sizes (m), alpha | Pattern | Burial |
|---|---|---|---|---|---|
glacial_till |
till plain, moraine | granite, rounded | 0.10 to 3.0, 2.8 | clusters, scale 6 m, 0.004 clusters per m2 | 30 to 70 % |
dropstones |
ice-rafted erratics on soft bed | granite, rounded | 0.5 to 4.0, 2.3 | sparse Poisson | 40 to 80 %, scour on |
talus |
scree at the foot of a cliff | basalt, angular | 0.2 to 2.0, 2.5 | band from y_min, 6 m e-fold |
0 to 20 % |
rockfall |
fan below a rock face | basalt, angular | 0.3 to 5.0, 2.2 | band from y_min, 15 m, big rocks travel furthest |
0 to 30 % |
cobble_bed |
current-swept lag deposit | sandstone, rounded | 0.05 to 0.4, 2.6 | near-uniform, long axes at 90 deg | 20 to 50 % |
slab_field |
broken sedimentary pavement | sandstone, platy | 0.3 to 3.0, 2.4 | clusters, scale 4 m, long axes at 0 deg | 10 to 40 % |
reef_rubble |
storm-broken carbonate | carbonate rubble, subangular | 0.05 to 0.8, 2.7 | patchy clusters, scale 3 m | 10 to 40 % |
basalt_columns |
columnar jointing | basalt, prismatic hexagons | 0.3 to 1.0 across, 2.0 | tight patches, scale 2 m | 0 to 30 % |
Each preset carries a default density. Setting density on the group
overrides it. Under a preset, density, d_min, d_max and alpha
left at their plain-rock defaults defer to the preset; any other value
overrides it. The runner prints a note when this happens, naming the
field, the default it ignored and the preset value it used.
bottom:
rocks:
- {bbox: [20, 30, 60, 70], preset: glacial_till, seed: 501}
- {bbox: [0, 0, 80, 12], preset: talus, source_edge: y_min, density: 2.0, seed: 502}
- {zones: [[1, 0], [1, 1]], preset: reef_rubble, burial_min: 0.3, burial_max: 0.6, seed: 503}
- {preset: dropstones, scour: {enabled: true, current_speed_ms: 0.7, current_heading_deg: 30}, seed: 504}
Knobs
Every knob is optional; null or absent means "the preset's value".
| Knob | Meaning |
|---|---|
lithology |
granite, basalt, limestone, sandstone, carbonate_rubble. Sets a target-strength offset, specular fraction, surface roughness and a default shape. |
shape |
rounded, subangular, angular, platy, prismatic. Zingg axis-ratio family plus facet count (below). |
cluster_scale_m, cluster_density |
Thomas cluster process: parents per m2 and the Gaussian spread of rocks around each parent. Set both or neither. Mutually exclusive with source_edge: a band from an edge and a cluster process are two different point processes, and asking for both raises. |
source_edge, band_width_m |
Exponential band of rocks from one bbox edge (x_min, x_max, y_min, y_max); the e-folding distance in metres. Mutually exclusive with the cluster knobs. |
size_grows_with_distance |
Rockfall sorting: larger rocks land further from the source edge. |
burial_min, burial_max |
Burial as a fraction of the rock's vertical semi-axis, drawn per rock. Exposed height sets the shadow length. |
heading_deg, heading_spread_deg |
Long-axis heading (degrees, NED) and its spread; omit heading_deg for uniform headings. |
scour, scour_d_min |
Steady-current scour block (enabled, current_speed_ms, current_heading_deg, sediment_type) applied around rocks at least scour_d_min metres across, largest first, at most 500 per group. The pit tapers to zero at the rock footprint at the sand's angle of repose (32 degrees) rather than ending in a hard edge. |
ts_offset_db, specular_fraction |
Override the lithology's acoustic values. |
Switching a preset feature off
null means "the preset's value", so turning a feature the preset has on
back off takes a value of its own:
| Off encoding | Effect |
|---|---|
cluster_density: 0 (or cluster_scale_m: 0) |
Clusters off; positions are a homogeneous Poisson field. |
source_edge: none |
No band from an edge; positions fill the region. |
heading_deg: uniform |
Uniform random long-axis headings. |
scour: {enabled: false} |
No scour pits. |
Shape families (Zingg 1935)
| Family | b/a | c/b | Clipping planes | Body |
|---|---|---|---|---|
| rounded | 0.65 to 1 | 0.55 to 1 | none | superellipsoid, exponent 2.0 to 2.6 |
| subangular | 0.60 to 1 | 0.50 to 1 | 2 to 4 | exponent 2.0 to 3.0 |
| angular | 0.55 to 1 | 0.45 to 1 | 4 to 8 | exponent 2.5 to 4.0 |
| platy | 0.60 to 1 | 0.15 to 0.35 | 2 to 5 | exponent 2.5 to 4.0 |
| prismatic | 1 | height 0.6 to 2.5 of width | 6 sides + tilted top | hexagonal column, exponent 12 |
Every body is convex. Angularity comes from random clipping planes, never
from a superellipsoid exponent below 2 (which would pinch the rock into a
diamond). Sizes follow a truncated power law p(D) ~ D^-alpha between
d_min and d_max; fragment populations have alpha between about 2 and
3.5 (Turcotte 1986). Wentworth (1922) size classes: cobble 64 to 256 mm,
boulder above 256 mm.
What the SAS image shows
Each rock is a bright highlight on its near face and an acoustic shadow behind it whose length is set by the exposed height. Burial shortens the shadow; scour adds a dark pit on the upstream side and a faint mound downstream; the pit slopes up to the rock at the sand's angle of repose (32 degrees) rather than cutting a hard edge. Clusters read as bright patches with speckled interiors; talus reads as a bright band that thins with distance from the source.
Limits
- Rocks seat on the local bed like every other object, writing only their
own footprint on a datum taken from the mean bed under it, so nothing
leaves a square pedestal (realism notes section 5);
PS_LEGACY_SEAT=1restores the old window stamp at z = 0 for one release. - No bedrock outcrops or ledges; talus and rockfall take a bbox edge as their source.
- No overhangs (the heightfield is column monotone).
- A group over 50 000 rocks prints a warning; over 200 000 raises.
- Scour is the steady-current model of
simulator/scour.py(no waves).
Performance
Cost of placing one rock: a 0.3 m body on a 0.05 m grid at 200 scatterers per m2, 50 repetitions each, measured on 2026-09-17 on the development machine (numpy 2.3.5, scipy 1.16.3, CPython 3.12.12, one core).
| Rock | ms per rock |
|---|---|
| plain rounded, not buried (the legacy placer) | 3.0 |
rounded, buried (sink_m > 0) |
3.5 |
| faceted, buried | 1.7 |
A buried rounded rock costs a little more than an unburied one because its mesh is a convex hull of surface samples rather than the analytic ring mesh. A faceted rock is cheaper than either: its hull is built from a coarser polar grid. Meshes dominate the per-rock cost, so a field of tens of thousands of rocks is minutes, not seconds.
References
- Wentworth, C.K. (1922). A scale of grade and class terms for clastic sediments. J. Geology 30(5).
- Zingg, T. (1935). Beitrag zur Schotteranalyse. Schweiz. Mineral. Petrogr. Mitt. 15.
- Turcotte, D.L. (1986). Fractals and fragmentation. J. Geophys. Res. 91(B2).
- Sumer, B.M. and Fredsoe, J. (2002). The Mechanics of Scour in the Marine Environment. World Scientific.