This commit is contained in:
Rainer Leit
2026-09-25 17:02:24 +03:00
parent cc43ed8dc8
commit 9597629951
2149 changed files with 460234 additions and 1770 deletions
+90
View File
@@ -0,0 +1,90 @@
// Package detail is the pipeline below the geology grid: the passes that decide how the ground reads to
// somebody standing on it.
//
// Every one of them is local, which is what makes the detail grid tileable at all (internal/tile): noise is
// pointwise, thermal weathering propagates a cell at a time, and a droplet travels at most its lifetime in
// cells. And every one of them is a port of tuned numpy from Scripts/Authoring/heightmap_erosion.py rather
// than a reimplementation. Docs/Terrain.md is explicit about which of its constants are lessons rather than
// choices, and they all carry across unchanged:
//
// - the droplet slope gate at 0.25, which must sit well above the median lowland slope or the meadows come
// out brushed with rills;
// - the per-step cut cap, because droplets share cells and a crowd in one runs away to infinity without it;
// - the load cap, which bounds the mound a droplet leaves where it stops;
// - cuts through a 3x3 brush and deposits on the droplet's own cell, because spreading the deposit makes a
// pit's rim rise faster than its floor, so the pit never fills and every droplet feeds a mound;
// - and thermal weathering shedding half the *largest* excess rather than half the mean.
//
// What does not carry across is how the randomness is drawn. The numpy picks spawn cells from an RNG stream,
// which is index-dependent: the same cell would get different droplets depending on which tile it fell in and
// every seam would show. Here everything is a hash of the absolute world position.
package detail
import (
"math"
"salty/terrain/internal/noise"
"salty/terrain/internal/world"
)
// Hardness is rock hardness in [0, 1] as a function of position and *elevation*: horizontal bands with a slow
// tilt, and a slow change of rock type across the map. Erosion is scaled by (1 - hardness), so a hard band
// holds a shelf on a cut face.
//
// It is orthogonal to the lithology field the fluvial solve uses and both are kept, which is the point:
// lithology varies with where you are and enters the solve at geology resolution; strata varies with how deep
// you have cut and scales the droplets at detail resolution. One puts different rock in different valleys,
// the other puts ledges on a cliff.
type Hardness struct {
W, H int
period float64 // vertical period in cell heights
contrast float64
classes *Classes
tilt []float32
kind []float32
}
// Pass indices for the detail passes' seeded sources, above everything uplift and coast use.
const (
srcTilt = 40
srcKind = 41
srcDetail = 42
srcDroplet = 43
srcCoastal = 44
)
// NewHardness builds the two fields on world coordinates, so two tiles covering the same rock agree.
//
// noisePeriodM is the world period rather than the detail passes' short one: where the rock changes and how
// the bands tilt are kilometre-scale properties, and a lattice coarse enough for them costs nothing.
func NewHardness(f world.Frame, seed int64, noisePeriodM, strataPeriodM, contrast float64, classes *Classes) *Hardness {
u, v := noise.WorldUV(f.W, f.H, f.P.CellM, f.OriginXM(), f.OriginYM(), noisePeriodM)
tilt := noise.FBMAt(u, v, noise.NewSource(seed, srcTilt), noise.Params{BaseCells: 96, Octaves: 3, Gain: 0.5})
kind := noise.FBMAt(u, v, noise.NewSource(seed, srcKind), noise.Params{BaseCells: 64, Octaves: 3, Gain: 0.5})
period := strataPeriodM / f.P.CellM
if period < 1e-3 {
period = 1e-3
}
return &Hardness{W: f.W, H: f.H, period: period, contrast: contrast, classes: classes,
tilt: tilt.Data, kind: kind.Data}
}
// At is the hardness at cell i for material standing at heightCells, in cell heights.
func (hd *Hardness) At(i int, heightCells float64) float64 {
if hd == nil {
return 0
}
contrast := hd.classes.contrast(i, hd.contrast)
if contrast == 0 {
return 0
}
band := 0.5 + 0.5*math.Sin(2*math.Pi*(heightCells/hd.period+float64(hd.tilt[i])*2))
v := 0.5 + contrast*(band-0.5)*(0.4+0.8*float64(hd.kind[i]))
if v < 0.05 {
return 0.05
}
if v > 0.95 {
return 0.95
}
return v
}