Ollin

Terrain

Generated terrain, then weathered. A heightfield is grown by diamond-square subdivision, then eroded by tens of thousands of simulated raindrops that carve ravines and build sediment fans.

Run
swift run --package-path Examples Example-3D-Geometry-Terrain
Source
Examples/3D/Geometry/Terrain/Sketch.swift
import Ollin

/// Generated terrain, weathered. A `Heightfield` grows by diamond-square
/// subdivision, then erodes: tens of thousands of simulated raindrops carve
/// ravines and build sediment fans (`.hydraulic`), and a thermal pass settles
/// the slopes that stand too steep (`.thermal`). Both the raw and the eroded
/// fields become meshes in `setup()`, with a texture that colors each
/// sample by its height, so the `weathered` toggle flips between them and the
/// carving reads directly. Everything reproduces from the seed parameter.
@main
final class Terrain3D: Sketch {
    private var rawMesh = Mesh(positions: [], indices: [])
    private var erodedMesh = Mesh(positions: [], indices: [])

    @Param(icon: "cloud.rain") var weathered = true
    @Param(1 ... 99, icon: "dice") var terrainSeed = 7.0

    private var builtSeed = 0.0

    override func setup() {
        rebuild()
    }

    override func draw() {
        if terrainSeed.rounded() != builtSeed { rebuild() }

        background(Color(hex: 0x0A0D12))
        lightingPreset(.goldenHour)
        cameraShowcase(.orbitAndRise(period: .tau / 0.12), radius: 14.5,
                       elevation: 0.6, fieldOfView: .pi / 4)
        drawMesh(weathered ? erodedMesh : rawMesh)
    }

    private func rebuild() {
        builtSeed = terrainSeed.rounded()
        let seed = Int(builtSeed)

        let land = Heightfield.diamondSquare(size: 257, roughness: 0.55, seed: seed)
        let eroded = land
            .eroded(.hydraulic(drops: 50_000), seed: seed)
            .eroded(.thermal(talus: 0.012, iterations: 30))

        rawMesh = terrainMesh(land)
        erodedMesh = terrainMesh(eroded)
    }

    /// The field as a mesh with a height-colored texture: each sample's
    /// height walks a ramp from valley green through rock to snow.
    private func terrainMesh(_ field: Heightfield) -> Mesh {
        let ramp = Ramp([Color(hex: 0x2E4A33), Color(hex: 0x5C6B48),
                         Color(hex: 0x8A7E66), Color(hex: 0x9C948C),
                         Color(hex: 0xEDEFF2)])
        var pixels = [UInt8]()
        pixels.reserveCapacity(field.values.count * 4)
        for value in field.values {
            let c = ramp.color(at: clamp(value, 0, 1))
            pixels.append(UInt8((c.red * 255).rounded()))
            pixels.append(UInt8((c.green * 255).rounded()))
            pixels.append(UInt8((c.blue * 255).rounded()))
            pixels.append(255)
        }
        let texture = Image(width: field.columns, height: field.rows,
                            premultipliedRGBA: pixels)
        let mesh = field.mesh(width: 10, depth: 10, height: 2.2)
        guard let texture else { return mesh }
        return mesh.textured(texture)
    }
}

↑↓ move ↩ open