Planar models can be built in different ways: for instance using an analytic function (left), or interpolation across discrete samples (right). Figure from [1].

Arches of Etretat (left) and deep overhangs and deep concavities carved into a coastal cliff (right). These impressive and memorable landforms cannot be modeled by planar models. Arches of Etretat (left) and deep overhangs and deep concavities carved into a coastal cliff (right). These impressive and memorable landforms cannot be modeled by planar models.

/*
 * Approximate SDF of a heightfield.
 * Positive outside the surface, negative inside.
 */
float SDFHeightfield(float3 p) {
    return sampleHeight(p.xz) - p.y;
}

Cool overhangs carved by the mechanism action of waves onto the cliffs, near Cyprus.

public class CoastalErosionSettings {
    // Minimum slope for the seed to be accepted
    public float seedMinSlope;

    // Acceptable altitude range for seeds
    public Vector2 seedElevationRange;

    // A random probability of acceptance for more variety
    public float seedAcceptanceProbability;

    // Sphere radii will be randomly computed from this min, max range
    public Vector2 radiusRange;

    // Number of carving step per seed, randomly within this range
    public Vector2Int growthStepRange;

    // Number of sphere carved at each growth step
    public Vector2Int growthSphereRange;
}

// Characterization of a seed point.
public class SeedPoint {
    public Vector3 point;
    public Vector3 normal;
}

/*
 * Compute the set of seeds for coastal erosion from the base heightfield.
 * Scan the heightfield and look for the steep parts around sea elevation.
 */
public List<Vector3> GetCoastalErosionSeeds(CoastalErosionSettings settings, ...) {
    List<Vector3> ret = new List<Vector3>();
    for (int x = 0; x < terrain.Width(); x++) {
        for (int y = 0; y < terrain.Height(); y++) {
            Vector3 point = terrain.Point(x, y);
            
            // Altitude
            if (!Utils.WithinRange(point.y, settings.seedElevationRange))
                continue;
            
            // Slope
            float slope = terrain.GetSlope(x, y);
            if (slope < settings.seedMinSlope)
                continue;

            // Probability
            float p = Random.value;
            if (p > settings.seedAcceptanceProbability)
                continue;

            ret.Add(new SeedPoint(point, terrain.Normal(x, y)));
        }
    }
    return ret;
}

/*
 * Returns a set of spheres more or less randomly positioned along a direction.
 */
public List<Sphere> GrowFromSeed(Vector3 seed, Vector3 direction, CoastalErosionSettings settings, ...) {
    List<Sphere> ret = new List<Sphere>();
    ret.Add(new Sphere(seed, Utils.RandomRange(settings.radiusRange)));

    // Growth dir slightly downward, and globally horizontal
    Vector3 growthDir = new Vector3(direction.x, -0.1f, direction.z);

    // Sphere positioning loop
    Vector3 p = seed;
    int nbStep = Utils.RandomRange(settings.growthStepRange);
    for (int i = 0; i < nbStep; i++) {
        // Random amount of sphere at each step around p, each with random radii
        int nbSpheres = Utils.RandomRange(settings.growthSphereRange);
        var newSpheres = Utils.RandomSpheresAroundPoint(p, nbSpheres, settings.radiusRange);
        ret.AddRange(newSpheres);

        // Move along direction
        p = p + direction;
    }
}

/*
 * Perform the coastal erosion algorithm. Returns a set of spheres that should be carved out of the
 * base heightfield in implicit form.
 */
public List<Sphere> CoastalErosion(CoastalErosionSettings settings, ...) {
    // Get seeds
    var seeds = GetCoastalErosionSeeds(settings, ...);

    // Carving with spheres, in the normal direction
    List<Sphere> ret = new List<Sphere>();
    foreach (var seed in seeds) {
        var newSpheres = GrowFromSeed(seed.point, -seed.normal, settings, ...);
        ret.AddRange(newSpheres);
    }

    return ret;
}

Coastal landforms generated with three sphere erosion passes. Although they would look better if manually authored, arches are sometimes created organically with this approach, and you also get to explore small sea caves. Coastal landforms generated with three sphere erosion passes. Although they would look better if manually authored, arches are sometimes created organically with this approach, and you also get to explore small sea caves. Coastal landforms generated with three sphere erosion passes. Although they would look better if manually authored, arches are sometimes created organically with this approach, and you also get to explore small sea caves. Coastal landforms generated with three sphere erosion passes. Although they would look better if manually authored, arches are sometimes created organically with this approach, and you also get to explore small sea caves.

float3 Warp(float3 p) {
    return p + float3(1, 0, 0); // constant offset
}

float SDF(float3 p) {
    float3 q = Warp(p):
    ... // we continue with the rest of the implicit function
}

// Your typical perlin or value noise
float3 Noise(float3 p) {
    ...
}

float3 Warp(float3 p) {
    // Warp amplitude from 1D noise
    float w = Noise(float3(0.0f, p.y, 0.0f) * frequency) * amplitude;
    
    // Warp direction, from the base heightfield
    float3 d = HeightfieldGradient(p);
    d = float3(d.x, 0.0f, d.z);

    // Final warp
    return p + d * w;
}

float SDF(float3 p) {
    float3 q = Warp(p):
    ...
}

A warping operator based on a 1D noise to create horizontal strata and overhangs, applied globally to the terrain. A warping operator based on a 1D noise to create horizontal strata and overhangs, applied globally to the terrain.

A cross view of our terrain mesh with wireframe, showing the typical triangle patterns of meshes computed using marching cubes.