143 lines
5.7 KiB
Go
143 lines
5.7 KiB
Go
package detail
|
|
|
|
import (
|
|
"math"
|
|
|
|
"salty/terrain/internal/field"
|
|
"salty/terrain/internal/manifest"
|
|
"salty/terrain/internal/noise"
|
|
"salty/terrain/internal/world"
|
|
)
|
|
|
|
// Why the detail passes need a noise period of their own, and why it is short.
|
|
//
|
|
// noise.Lattice allocates cells² floats an octave, and the cell count is the period divided by the
|
|
// wavelength. Asking for an eight-metre finest octave on a hundred-kilometre period means a lattice of
|
|
// 12500² - one and a half gigabytes for the top octave alone - so world-period noise simply cannot reach
|
|
// detail wavelengths with this lattice.
|
|
//
|
|
// A short period can, and the cost is that the texture repeats. At a kilometre that is invisible: what
|
|
// repeats is a few metres of surface roughness, not anything with a shape, and the structure it sits on comes
|
|
// from the solve and from the paint, neither of which repeats at all. The period still has to divide the
|
|
// circumference exactly or the pattern breaks at the seam, which the manifest checks.
|
|
|
|
// DetailNoiseParams is pass 9.
|
|
type DetailNoiseParams struct {
|
|
Cfg manifest.Detail
|
|
Seed int64
|
|
Frame world.Frame
|
|
PeriodM float64 // the short period above; must divide the circumference
|
|
|
|
SeaLevelM float64
|
|
|
|
// Classes gives each cell its own amplitude. Nil means the manifest's pair everywhere.
|
|
Classes *Classes
|
|
}
|
|
|
|
// slopeFull is the slope at which detail noise reaches its full amplitude - about 27 degrees. Flat ground
|
|
// gets the low end and steep ground the high end, which is the same instinct as the droplets' slope gate: a
|
|
// meadow is smooth and a scree face is not, and noise applied evenly makes the meadow look like sandpaper.
|
|
const slopeFull = 0.5
|
|
|
|
// shoreTaperM is how far either side of the water the amplitude is faded in. A few metres of noise at the
|
|
// waterline turns the shallows into a scatter of one-cell islands, which is the same failure the coastal pass
|
|
// tapers its own sea-floor roughness to avoid.
|
|
const shoreTaperM = 12
|
|
|
|
// seabedAmp is how much of the flat-ground amplitude the sea bed gets. A sea bed is not a hillside: what is
|
|
// down there is bedform and scattered rock, and it is the shape of the shelf that carries the eye rather than
|
|
// its surface. It is a constant rather than a knob because the knob that matters is how deep the texture
|
|
// reaches, which is Detail.SeabedM, and two dials for one effect is one too many.
|
|
const seabedAmp = 0.45
|
|
|
|
// lattice builds the noise field both halves of this pass read, on world coordinates.
|
|
//
|
|
// BaseCells is chosen so the finest octave lands near two cells, which is as fine as a grid can carry.
|
|
func (p DetailNoiseParams) lattice(cellM float64) *field.Field {
|
|
oct := p.Cfg.Octaves
|
|
f := p.Frame
|
|
u, v := noise.WorldUV(f.W, f.H, cellM, f.OriginXM(), f.OriginYM(), p.PeriodM)
|
|
finest := 2 * cellM
|
|
base := int(p.PeriodM/(finest*math.Pow(2, float64(oct-1))) + 0.5)
|
|
if base < 2 {
|
|
base = 2
|
|
}
|
|
return noise.FBMAt(u, v, noise.NewSource(p.Seed, srcDetail),
|
|
noise.Params{BaseCells: base, Octaves: oct, Gain: 0.45})
|
|
}
|
|
|
|
func (p DetailNoiseParams) off() bool {
|
|
lo, hi := p.Cfg.AmplitudeM.Lo(), p.Cfg.AmplitudeM.Hi()
|
|
return p.Cfg.Octaves < 1 || (lo == 0 && hi == 0)
|
|
}
|
|
|
|
// RunDetailNoise adds surface texture at wavelengths the geology grid cannot hold.
|
|
//
|
|
// It is texture and nothing more. The relief, the valleys and the divides all came from the solve; this is
|
|
// what the ground does between them, and its amplitude is metres rather than tens of metres on purpose - the
|
|
// lesson from the first pipeline is that noise piled on top of erosion reads as noise, not as ground.
|
|
func RunDetailNoise(h *field.Field, land []bool, p DetailNoiseParams) {
|
|
if p.off() {
|
|
return
|
|
}
|
|
lo, hi := p.Cfg.AmplitudeM.Lo(), p.Cfg.AmplitudeM.Hi()
|
|
n := p.lattice(h.CellM)
|
|
slope := h.Slope()
|
|
for i := range h.Data {
|
|
if !land[i] {
|
|
continue
|
|
}
|
|
above := float64(h.Data[i]) - p.SeaLevelM
|
|
if above <= 0 {
|
|
continue
|
|
}
|
|
t := float64(slope.Data[i]) / slopeFull
|
|
if t > 1 {
|
|
t = 1
|
|
} else if t < 0 {
|
|
t = 0
|
|
}
|
|
cLo, cHi := p.Classes.amp(i, lo, hi)
|
|
amp := cLo + (cHi-cLo)*t
|
|
if above < shoreTaperM {
|
|
amp *= above / shoreTaperM
|
|
}
|
|
h.Data[i] += float32(amp * (2*float64(n.Data[i]) - 1))
|
|
}
|
|
}
|
|
|
|
// RunSeabedNoise is the same texture, under water.
|
|
//
|
|
// It is a second entry point rather than a branch inside the first because of *when* it can run. Passes 9 to
|
|
// 12 work with the sea flattened to sea level, so while they are running there is no sea bed to texture: the
|
|
// floor does not come back until the tile bake restores it, which is after pass 12 and just before the shore
|
|
// is drawn. So this runs there, on the same lattice, keyed the same way, and a cell gets the same value it
|
|
// would have got from one whole-world run.
|
|
//
|
|
// What it is for: a coast where the land is rough to the last cell and the water is glass from the first
|
|
// reads as a cut-out rather than as a shore, and the line between the two is the land mask's own boundary -
|
|
// the one thing in the picture that is a decision rather than a landform.
|
|
//
|
|
// Flat-ground amplitude only, and less of it: the slope term is what makes a scree face rough and there are
|
|
// no scree faces down here. Faded in from nothing at the waterline, so the pass cannot turn the shallows into
|
|
// a scatter of one-cell islands, and out to nothing at SeabedM.
|
|
func RunSeabedNoise(h *field.Field, p DetailNoiseParams) {
|
|
if p.off() || p.Cfg.SeabedM <= 0 {
|
|
return
|
|
}
|
|
lo, hi := p.Cfg.AmplitudeM.Lo(), p.Cfg.AmplitudeM.Hi()
|
|
n := p.lattice(h.CellM)
|
|
for i := range h.Data {
|
|
d := p.SeaLevelM - float64(h.Data[i])
|
|
if d <= 0 || d >= p.Cfg.SeabedM {
|
|
continue
|
|
}
|
|
cLo, _ := p.Classes.amp(i, lo, hi)
|
|
amp := cLo * seabedAmp * math.Min(d/shoreTaperM, 1) * (1 - noise.Smoothstep(d/p.Cfg.SeabedM))
|
|
if amp == 0 {
|
|
continue
|
|
}
|
|
h.Data[i] += float32(amp * (2*float64(n.Data[i]) - 1))
|
|
}
|
|
}
|