Ollin

SurfaceScatter

Trees standing on a globe. surfacePoints scatters them over the skin by area, and alignment stands each one up. The spread parameter compares an even covering, a plain draw, and the vertex-list shortcut.

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

/// Standing things on a surface, and the shortcut that gets it wrong.
///
/// A forest covers a globe. `surfacePoints` picks the spots over the skin
/// itself, so a spot is as likely anywhere the surface has the same area, and
/// `alignment` turns each tree to stand along the normal it landed on.
///
/// `spread` is the comparison worth watching. `vertices` takes the shortcut of
/// picking from the mesh's vertex list, and the trees line up in the globe's own
/// rows, crowd toward the poles where the rings grow small, and land twice on
/// the same vertex: the reading says the closest pair is 0.000 apart. `plain`
/// picks over the surface, correctly, but with no regard for the trees already
/// placed, so it clumps and leaves clearings. `even` keeps them apart, and the
/// closest pair sits near the spacing the count deserves.
///
/// `stand` turns the alignment off, so every tree points the same way and the
/// globe reads as a pincushion.
@main
final class SurfaceScatter: Sketch {

    enum Spread: String, CaseIterable, ParamOption { case even, plain, vertices }

    @Param(120 ... 2400, icon: "tree") var count = 500
    @Param(icon: "slider.horizontal.3") var spread = Spread.even
    @Param(icon: "arrow.up.and.down") var stand = true
    @Param(icon: "circle.dotted") var showSpots = false

    /// The surface everything lands on. A globe is the honest test: its
    /// triangles are tiny at the poles and wide at the equator, so a scatter
    /// that reads the vertex list cannot help but crowd the top and bottom.
    private let globe = Mesh.sphere(radius: 1.8, segments: 48, rings: 24)
    private let tree = Mesh.cone(radius: 0.1, height: 0.24, segments: 12)

    private var placements: [MeshInstance] = []
    private var spots: [SurfaceSample] = []
    private var closest = 0.0
    private var builtFor: (count: Int, spread: Spread, stand: Bool)?

    override func draw() {
        background(Color(hex: 0x0B0D14))
        environment(.courtyard.rotated(time * 0.04).lightingOnly())
        lightingPreset(.studio)
        cameraShowcase(.autoOrbit(period: 40), radius: 6.2, elevation: 0.5)

        if builtFor == nil || builtFor! != (count, spread, stand) {
            place()
            builtFor = (count, spread, stand)
        }

        castShadows()
        fill(Color(hex: 0x2E4A6B))
        material(.dielectric(roughness: 0.6))
        drawMesh(globe)

        fill(Color(hex: 0x7FD48A))
        material(.dielectric(roughness: 0.5))
        drawMesh(tree, instances: placements)

        if showSpots {
            noStroke()
            drawPointCloud(PointCloud(positions: spots.map(\.position),
                                      color: Color(hex: 0xFFC84A), size: 0.03))
        }

        let gap = String(format: "%.3f", closest)
        drawCaption("\(spread.rawValue) placement: \(placements.count) trees, closest pair \(gap)")
    }

    /// Pick the spots, then turn a tree to stand on each one.
    private func place() {
        seed(11)
        switch spread {
        case .even:
            spots = surfacePoints(on: globe, count: count)
        case .plain:
            spots = surfacePoints(on: globe, count: count, scatter: .random)
        case .vertices:
            // The shortcut: pick from the vertex list and take the normal
            // stored there. Every spot is a real point of the surface, and the
            // covering is still wrong.
            spots = (0 ..< count).map { _ in
                let i = Int(random(Double(globe.positions.count)))
                return SurfaceSample(position: globe.positions[i],
                                     normal: globe.normals[i],
                                     uv: .zero, triangle: 0,
                                     barycentric: Vector3(1, 0, 0))
            }
        }

        placements = spots.map { spot in
            // A cone is built about its middle, so lift it half its height to
            // put its base on the skin.
            let up = stand ? spot.normal : .unitY
            return MeshInstance(position: spot.position + up * 0.11,
                                rotation: stand ? spot.alignment(spin: random(.tau)) : .zero,
                                scale: 0.7 + random(0.5))
        }
        closest = closestPair(spots.map(\.position))
    }

    /// The distance between the two nearest spots, which is the number that
    /// separates an even covering from a clumped one.
    private func closestPair(_ points: [Vector3]) -> Double {
        var best = Double.infinity
        for i in points.indices {
            for j in (i + 1) ..< points.count {
                let d = (points[i] - points[j]).lengthSquared
                if d < best { best = d }
            }
        }
        return best.isFinite ? best.squareRoot() : 0
    }
}

↑↓ move ↩ open