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)) } }