Tooling
This commit is contained in:
@@ -0,0 +1,720 @@
|
||||
package detail
|
||||
|
||||
import (
|
||||
"math"
|
||||
"sort"
|
||||
|
||||
"salty/terrain/internal/dt"
|
||||
"salty/terrain/internal/field"
|
||||
"salty/terrain/internal/manifest"
|
||||
"salty/terrain/internal/noise"
|
||||
"salty/terrain/internal/world"
|
||||
)
|
||||
|
||||
// Pass 11b: the shore at two metres.
|
||||
//
|
||||
// The coastal pass on the geology grid (internal/coast) decides where the shore *is*: it lays the shelf,
|
||||
// planes a platform within a reach of the waterline, leaves a cliff where that reach ends, and carries the
|
||||
// sediment it cut along the shore into the bays. All of that is right and almost none of it is visible,
|
||||
// because the surf reach is 110 m and a geology cell is 8: a beach is fourteen cells wide, a berm is a
|
||||
// quarter of one cell high, and a wave-cut notch is a fifth of one.
|
||||
//
|
||||
// The surf reach is the only length in the generator set by physics rather than by the canvas - it is how far
|
||||
// a wave runs up, and a wave does not know how big the map is - so it does not shrink when the cell does. At
|
||||
// the 2 m detail cell the same 110 m is 55 cells, which is enough to hold a real profile. That is the whole
|
||||
// argument for this being a pass of its own rather than a knob on the one above.
|
||||
//
|
||||
// Everything here is measured against that reach and against the exposure the geology pass computed, so the
|
||||
// two cannot disagree about where the shore is: this pass re-evaluates the same
|
||||
// reach = SurfReachM * (0.35 + 0.65*exposure) that plane() used, and draws the profile the geology grid was
|
||||
// too coarse to hold.
|
||||
//
|
||||
// It is local, which is what lets it run per tile: nothing here reads or writes further from the waterline
|
||||
// than two surf reaches, which is 220 m against a tile margin of 244. Measured rather than reasoned - the
|
||||
// pass reaches 110 to 136 m on the fixtures in TestThePassFitsInsideTheTileMargin - but the 220 is a hard
|
||||
// limit rather than a measurement, because past it a cell has no stretch of shore to belong to at all.
|
||||
|
||||
// CoastalParams is pass 11b's input.
|
||||
type CoastalParams struct {
|
||||
Cfg manifest.CoastDetail
|
||||
Surf manifest.Coast // the geology pass's own numbers: the reach and the platform grade come from it
|
||||
Seed int64
|
||||
Frame world.Frame
|
||||
PeriodM float64 // the detail noise period, for the crenulation lattice
|
||||
|
||||
SeaLevelM float64
|
||||
|
||||
// Exposure is the geology pass's fetch field sampled onto this tile, 0 sheltered to 1 open water.
|
||||
//
|
||||
// It cannot be computed here and must not be: fetch is cast fifteen hundred metres in sixteen directions
|
||||
// and a tile is five kilometres across, so a tile has no way of knowing whether the water in front of it
|
||||
// is a bay or an ocean. It is exactly the quantity D-53's rule says has to come from the pass that ran
|
||||
// over the whole cylinder. Nil means the bake predates the field, and then every coast is treated as
|
||||
// fully exposed - which is what the geology pass's own percentiles say most coast is anyway.
|
||||
Exposure []float32
|
||||
|
||||
Hardness *Hardness
|
||||
}
|
||||
|
||||
// CoastalStats is what the pass moved, for the tile record. The cliff branch conserves: what it cuts off the
|
||||
// face it lays at the foot, per stretch of shore, and ScreeM3 is reported beside CutM3 so a run where the two
|
||||
// have drifted apart says so rather than quietly losing rock.
|
||||
type CoastalStats struct {
|
||||
ShoreCells int `json:"shore_cells"`
|
||||
CliffFrac float64 `json:"cliff_fraction"`
|
||||
CutM3 float64 `json:"cliff_cut_m3"`
|
||||
ScreeM3 float64 `json:"scree_laid_m3"`
|
||||
BeachM3 float64 `json:"beach_net_m3"`
|
||||
|
||||
// How high the land stands behind this tile's shore, over its waterline cells. It is the input the
|
||||
// beach-or-cliff decision is made from, so it is reported rather than left to be inferred from the
|
||||
// fraction: a run with no cliffs anywhere is either a coast with no cliffs on it or a threshold in the
|
||||
// wrong place, and these two numbers are the only thing that tells the two apart.
|
||||
BackshoreP50M float64 `json:"backshore_p50_m"`
|
||||
BackshoreP90M float64 `json:"backshore_p90_m"`
|
||||
}
|
||||
|
||||
// coastalTaper is how far past the surf reach the profile fades out, as a fraction of the reach. The taper
|
||||
// exists so the pass hands back to the droplets rather than ending in a line across the ground.
|
||||
const coastalTaper = 0.5
|
||||
|
||||
// beachFace is the slope of the swash face of a sand beach, which is what sets where the berm crest sits: a
|
||||
// berm bh metres high has its crest bh/beachFace metres inland. 1:10 is the ordinary figure for medium sand,
|
||||
// and it is the one number here that is a property of the sediment rather than of the wave.
|
||||
const beachFace = 0.1
|
||||
|
||||
// RunCoastal cuts the shore profile. Height is modified in place; land is the detail land mask as the passes
|
||||
// above left it and is not updated - the waterline this pass works from is the one they agreed on.
|
||||
func RunCoastal(h *field.Field, land []bool, p CoastalParams) CoastalStats {
|
||||
var st CoastalStats
|
||||
cfg := p.Cfg
|
||||
if !cfg.Enabled {
|
||||
return st
|
||||
}
|
||||
reachMax := p.Surf.SurfReachM
|
||||
if reachMax <= 0 {
|
||||
return st
|
||||
}
|
||||
w, ht := h.W, h.H
|
||||
cellM := h.CellM
|
||||
|
||||
// The shoreline, which is not the land mask's boundary.
|
||||
//
|
||||
// On a coastal plain the ground crosses sea level at a grade of about one in a hundred, so whether a cell
|
||||
// is land is decided by centimetres over a strip forty metres wide and the mask's boundary is a band of
|
||||
// speckle rather than a curve. Everything this pass does is measured from that boundary, and measuring
|
||||
// from speckle went wrong twice: it put a separate two-metre berm on every island in the band, and - less
|
||||
// visibly and worse - it wrecked the backshore, because a cell two hundred metres inland had its nearest
|
||||
// waterline cell in a puddle beside it rather than out at the coast, so the real shore was left measuring
|
||||
// the height of the land behind almost nothing.
|
||||
//
|
||||
// So the shoreline is derived: the signed distance to the raw boundary, smoothed, thresholded back. That
|
||||
// is a curve, it is within a few metres of the mask's own boundary, and everything below is measured from
|
||||
// it. Taking the waterline on the land side of it is a half-cell choice, recorded rather than hidden.
|
||||
rough := boundaryOf(land, w, ht)
|
||||
sd := signedDistance(rough, land, w, ht, cellM)
|
||||
smoothShore(sd, w, ht, int(cfg.ShoreSmoothM/cellM+0.5))
|
||||
wet := make([]bool, len(sd))
|
||||
for i, v := range sd {
|
||||
wet[i] = v > 0
|
||||
}
|
||||
line := boundaryOf(wet, w, ht)
|
||||
shore := make([]int32, 0, 4096)
|
||||
for i, on := range line {
|
||||
if on {
|
||||
shore = append(shore, int32(i))
|
||||
}
|
||||
}
|
||||
if len(shore) == 0 {
|
||||
return st
|
||||
}
|
||||
st.ShoreCells = len(shore)
|
||||
|
||||
// One transform, seeded on the waterline itself, answers both halves of every question this pass asks:
|
||||
// how far a cell is from the shore, and which stretch of shore it belongs to. The geology pass needs two
|
||||
// because it wants the sea side and the land side to answer different things; here they answer the same.
|
||||
//
|
||||
// wrapX is false and has to be: a tile is a rectangle cut out of the cylinder with a margin on it, and
|
||||
// the seam is the tiling's business rather than the pass's. A tile that wrapped its own left edge onto
|
||||
// its own right would be inventing a shore.
|
||||
d2, near := dt.Transform(line, w, ht, false)
|
||||
|
||||
// Per stretch of shore: how open it is, how far the surf reaches, how high the land behind it stands, and
|
||||
// how far the whole profile is displaced in or out. Indexed by slot rather than by cell, which is the
|
||||
// same economy the geology pass keeps - a tile has millions of cells and thousands of shore cells.
|
||||
n := len(shore)
|
||||
expo := make([]float64, n)
|
||||
reach := make([]float64, n)
|
||||
cren := make([]float64, n)
|
||||
|
||||
crenNoise := p.crenulation(h)
|
||||
for s, ci := range shore {
|
||||
e := 1.0
|
||||
if p.Exposure != nil {
|
||||
e = float64(p.Exposure[ci])
|
||||
if e < 0 {
|
||||
e = 0
|
||||
} else if e > 1 {
|
||||
e = 1
|
||||
}
|
||||
}
|
||||
expo[s] = e
|
||||
reach[s] = reachMax * (0.35 + 0.65*e)
|
||||
if crenNoise != nil {
|
||||
cren[s] = cfg.CrenulationM * (2*float64(crenNoise.Data[ci]) - 1)
|
||||
}
|
||||
}
|
||||
|
||||
// The signed distance to that shoreline, which needs no smoothing of its own: the curve it is measured
|
||||
// from is already smooth.
|
||||
dist := make([]float32, len(d2))
|
||||
for i := range d2 {
|
||||
dm := math.Sqrt(float64(d2[i])) * cellM
|
||||
if wet[i] {
|
||||
dist[i] = float32(dm)
|
||||
} else {
|
||||
dist[i] = float32(-dm)
|
||||
}
|
||||
}
|
||||
|
||||
// Which stretch of shore each cell belongs to.
|
||||
slot := make([]int32, len(d2))
|
||||
// Two surf reaches is the outer limit of the whole pass, on both sides, and it is a limit rather than a
|
||||
// consequence: it is the window the backshore is measured in, so it is the furthest any cell has a stretch
|
||||
// of shore to belong to at all, and it is what makes the margin claim one number. 220 m at the default
|
||||
// reach, against a tile margin of 244.
|
||||
backOuter := 2 * reachMax
|
||||
for i := range d2 {
|
||||
dm := math.Sqrt(float64(d2[i])) * cellM
|
||||
slot[i] = -1
|
||||
if dm > backOuter || near[i] < 0 {
|
||||
continue
|
||||
}
|
||||
if s := slotOf(shore, near[i]); s >= 0 {
|
||||
slot[i] = int32(s)
|
||||
}
|
||||
}
|
||||
|
||||
back := marchBackshore(h, dist, wet, shore, reach, p.SeaLevelM)
|
||||
cliff := make([]float64, n)
|
||||
for s := range back {
|
||||
cliff[s] = cliffiness(back[s], cfg.CliffFromM, cfg.CliffToM)
|
||||
st.CliffFrac += cliff[s]
|
||||
}
|
||||
st.CliffFrac /= float64(n)
|
||||
st.BackshoreP50M, st.BackshoreP90M = percentiles(back)
|
||||
|
||||
// The roughness fade, before the profile is drawn on top of it.
|
||||
//
|
||||
// The profile is only a few tens of metres wide, so on its own the ground goes from a drawn beach to full
|
||||
// dune amplitude and droplet rills within the width of its taper, and the beach reads as a ribbon laid on
|
||||
// the terrain rather than as part of it. This blends the surface towards a smoothed copy of itself over a
|
||||
// wider band: the relief is untouched - the smoothing radius is metres, not tens of them - and what fades
|
||||
// is the metre-scale texture, so the backshore comes out smoother than the hillside behind it. Which is
|
||||
// what a backshore is: sand and dune over whatever the hillside is made of.
|
||||
smoothShoreRoughness(h, dist, wet, reachMax, cfg.SmoothReachM)
|
||||
|
||||
// The profile. Two targets blended by how high the land behind stands, and the result blended into the
|
||||
// surface by how far the cell is from the shore, so the pass fades out rather than ending in a line.
|
||||
cut := make([]float64, n)
|
||||
for i := range dist {
|
||||
s := slot[i]
|
||||
if s < 0 {
|
||||
continue
|
||||
}
|
||||
x := float64(dist[i]) - cren[s]
|
||||
r := reach[s]
|
||||
now := float64(h.Data[i])
|
||||
bh := p.Surf.BermM * (0.35 + 0.65*expo[s])
|
||||
|
||||
// The two branches carry their own reach as well as their own shape, which the first version of this
|
||||
// did not: a beach is over within a few tens of metres of the water, and holding its berm out to the
|
||||
// full surf reach cut a ninety-metre terrace into the land behind every beach on the map.
|
||||
crest := bh / beachFace
|
||||
face := math.Min(back[s], cfg.CliffMaxM)
|
||||
wb := branchWeight(x, crest, math.Min(crest+cfg.BermBackM, backOuter), r*0.5, math.Min(r, backOuter))
|
||||
wc := branchWeight(x, r,
|
||||
math.Min(r+face/max64(cfg.CliffGrade, 1e-3), backOuter),
|
||||
r*0.5, math.Min(r*(1+coastalTaper), backOuter))
|
||||
if wb <= 0 && wc <= 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
// A beach is a veneer of sediment, not a landform that fills a fjord. Without the cap the equilibrium
|
||||
// profile is a *target depth*, so a shore with forty metres of water a hundred metres off it - a
|
||||
// drowned valley, which is an ordinary thing on a real coast - gets thirty-seven metres of sand
|
||||
// invented to bring the floor up to the curve. Capped, the beach is a few metres of sediment laid on
|
||||
// whatever is there, and where the water is deep it simply runs out. That is what a steep-to shore is.
|
||||
tb := beachTarget(x, bh, cfg.DeanA, p.SeaLevelM)
|
||||
if fill := now + cfg.BeachFillM; tb > fill {
|
||||
tb = fill
|
||||
}
|
||||
tc := cliffTarget(x, r, face, p.Surf.PlatformGrade, cfg.CliffGrade, p.SeaLevelM)
|
||||
|
||||
// The platform is rock, and rock does not plane flat: hard bands stand out as ledges and reefs and
|
||||
// soft ones cut down into runnels. It goes into the cliff target *before* the clamp below, which is
|
||||
// the difference between a ledge and a wall built out of the sea: a band that resisted is rock the
|
||||
// surf did not take, so it is still below where the ground started.
|
||||
if p.Hardness != nil && cfg.PlatformReliefM > 0 {
|
||||
if win := platformWindow(x, r); win > 0 {
|
||||
hard := p.Hardness.At(i, now/cellM)
|
||||
tc += cfg.PlatformReliefM * (2*hard - 1) * win
|
||||
}
|
||||
}
|
||||
|
||||
// The cliff branch never builds, on either side of the waterline. A shore platform and the face above
|
||||
// it are what is left after the sea took rock away, so a target above the ground is the pass
|
||||
// proposing to invent a headland, and the honest answer to that is to leave the ground where it is.
|
||||
// It is also what keeps the platform from being laid out across deep water: it planes what is
|
||||
// shallower than it and passes over what is not.
|
||||
if tc > now {
|
||||
tc = now
|
||||
}
|
||||
|
||||
dCliff := cliff[s] * wc * (tc - now) // never positive, by the clamp above
|
||||
dBeach := (1 - cliff[s]) * wb * (tb - now)
|
||||
h.Data[i] = float32(now + dCliff + dBeach)
|
||||
cut[s] -= dCliff
|
||||
st.BeachM3 += dBeach
|
||||
}
|
||||
area := cellM * cellM
|
||||
for _, c := range cut {
|
||||
st.CutM3 += c * area
|
||||
}
|
||||
st.BeachM3 *= area
|
||||
|
||||
st.ScreeM3 = layScree(h, dist, shore, reach, cut, cfg, area)
|
||||
return st
|
||||
}
|
||||
|
||||
// cliffiness is how much of a cliff a stretch of shore is: 0 where the land behind it is at beach height, 1
|
||||
// where it stands a cliff's worth above the water, smooth in between so the two profiles do not switch over
|
||||
// from one shore cell to the next.
|
||||
func cliffiness(backM, from, to float64) float64 {
|
||||
if to <= from {
|
||||
if backM >= to {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
t := (backM - from) / (to - from)
|
||||
if t <= 0 {
|
||||
return 0
|
||||
}
|
||||
if t >= 1 {
|
||||
return 1
|
||||
}
|
||||
return noise.Smoothstep(t)
|
||||
}
|
||||
|
||||
// beachTarget is the equilibrium beach: a swash face rising to a berm crest above water, and Dean's profile
|
||||
// below it.
|
||||
//
|
||||
// depth = A * x^(2/3) is the standard equilibrium profile, and A is a property of the sand rather than of the
|
||||
// wave - it is the shape a beach returns to whatever the last storm did to it, which is exactly the right
|
||||
// thing for a generator to draw, because what a generator has is the long-run average and never the storm.
|
||||
// The berm is the other half: its crest sits at the wave runup limit, runup scales with wave height and wave
|
||||
// height with fetch, so a berm on an exposed coast stands higher than one at the back of a bay. That is why
|
||||
// the crest height arrives already scaled by exposure.
|
||||
func beachTarget(x, bermM, deanA, seaLevelM float64) float64 {
|
||||
if x >= 0 {
|
||||
crest := bermM / beachFace
|
||||
if crest <= 0 {
|
||||
return seaLevelM
|
||||
}
|
||||
if x >= crest {
|
||||
return seaLevelM + bermM
|
||||
}
|
||||
return seaLevelM + bermM*x/crest
|
||||
}
|
||||
return seaLevelM - deanA*math.Pow(-x, 2.0/3.0)
|
||||
}
|
||||
|
||||
// cliffTarget is a shore platform out to the foot and a face above it, up to faceM high.
|
||||
//
|
||||
// faceM is capped rather than being the backshore itself, and the cap is what stops the pass carving a
|
||||
// seventy-degree wall four hundred metres up a coastal range: the only other thing that stops the face is the
|
||||
// ground rising faster than it does, and ground behind a mountain coast does. A sea cliff is what the surf
|
||||
// undercut; above that height the face is a hillslope and it belongs to the solve.
|
||||
//
|
||||
// The foot is at the surf reach, which is not a choice: it is where plane() stopped cutting on the geology
|
||||
// grid, so the cliff is already there and already in the right place. What this does is give it a *face*. At
|
||||
// 8 m the step from the platform to the backshore is one cell, and upsampled by four it is a four-cell ramp
|
||||
// at whatever angle the interpolation chose; at 2 m the same height can stand at the angle a cliff stands at.
|
||||
//
|
||||
// Seaward of the waterline the platform simply continues at its own grade, which is what a shore platform
|
||||
// does - it is cut across the intertidal and runs on a little way below low water before the sea floor takes
|
||||
// over.
|
||||
func cliffTarget(x, reachM, faceM, platformGrade, cliffGrade, seaLevelM float64) float64 {
|
||||
if x < 0 {
|
||||
return seaLevelM - platformGrade*(-x)
|
||||
}
|
||||
if x <= reachM {
|
||||
return seaLevelM + platformGrade*x
|
||||
}
|
||||
foot := seaLevelM + platformGrade*reachM
|
||||
t := foot + cliffGrade*(x-reachM)
|
||||
if top := seaLevelM + faceM; t > top {
|
||||
return top
|
||||
}
|
||||
return t
|
||||
}
|
||||
|
||||
// branchWeight is how much of a branch's target a cell takes: all of it inside that branch's core, and
|
||||
// smoothstepping to none at its outer limit, so the pass hands back to the droplets and the noise instead of
|
||||
// ending in a line across the ground.
|
||||
func branchWeight(x, coreLand, outLand, coreSea, outSea float64) float64 {
|
||||
if x >= 0 {
|
||||
return taperTo(x, coreLand, outLand)
|
||||
}
|
||||
return taperTo(-x, coreSea, outSea)
|
||||
}
|
||||
|
||||
func taperTo(d, core, out float64) float64 {
|
||||
if d <= core {
|
||||
return 1
|
||||
}
|
||||
if d >= out || out <= core {
|
||||
return 0
|
||||
}
|
||||
return noise.Smoothstep((out - d) / (out - core))
|
||||
}
|
||||
|
||||
// platformWindow fades the strata relief in across the shore platform and out at both ends of it: nothing at
|
||||
// the foot of the cliff, where the face takes over, and nothing where the platform runs out under water.
|
||||
//
|
||||
// It reaches seaward as well as inland, because a shore platform does: it is cut across the intertidal and
|
||||
// carries on a little below low water, and that submerged half is where the ledges and the reefs are.
|
||||
func platformWindow(x, reachM float64) float64 {
|
||||
if reachM <= 0 {
|
||||
return 0
|
||||
}
|
||||
lo, hi := -reachM*0.5, reachM
|
||||
if x <= lo || x >= hi {
|
||||
return 0
|
||||
}
|
||||
t := (x - lo) / (hi - lo)
|
||||
return noise.Smoothstep(math.Min(t*4, 1)) * noise.Smoothstep(math.Min((1-t)*4, 1))
|
||||
}
|
||||
|
||||
// layScree puts back what the face lost, at the foot, at the angle of repose.
|
||||
//
|
||||
// The cliff branch only ever cuts, so it has a volume to account for, and a cliff that shed its face into
|
||||
// nothing would be the one place in this generator where rock disappears. It goes where it goes on a real
|
||||
// coast: an apron at the foot, thickest against the face and thinning seaward, at the angle blocky debris
|
||||
// stands at. The volume is matched per stretch of shore rather than per tile, so the apron under a cliff is
|
||||
// the apron that cliff produced.
|
||||
//
|
||||
// Marched along the shore normal, for the same reason marchBackshore is: a stretch of shore inside a bay owns
|
||||
// no cells at all a hundred metres out, because the nearest-shore wedges converge there, so an apron scattered
|
||||
// over those cells simply had nowhere to go. Measured on region 11 before the change, the aprons gained 2085
|
||||
// of the 3030 cubic metres the faces lost and the rest was silently dropped. A march has a line of cells to
|
||||
// put it on whatever the coast does, and the normalisation is the same one: a stretch of shore owns a strip
|
||||
// one cell wide, so a scattered wedge and a marched line cover the same area on a straight coast and agree.
|
||||
func layScree(h *field.Field, dist []float32, shore []int32, reach, cut []float64,
|
||||
cfg manifest.CoastDetail, area float64) float64 {
|
||||
|
||||
if cfg.ScreeDeg <= 0 || cfg.ScreeReachM <= 0 {
|
||||
return 0
|
||||
}
|
||||
w, ht := h.W, h.H
|
||||
cellM := h.CellM
|
||||
at := func(x, y int) float64 {
|
||||
if x < 0 {
|
||||
x = 0
|
||||
} else if x >= w {
|
||||
x = w - 1
|
||||
}
|
||||
if y < 0 {
|
||||
y = 0
|
||||
} else if y >= ht {
|
||||
y = ht - 1
|
||||
}
|
||||
return float64(dist[y*w+x])
|
||||
}
|
||||
var laid float64
|
||||
var line [128]int32
|
||||
var wgt [128]float64
|
||||
for s, ci := range shore {
|
||||
if cut[s] <= 0 {
|
||||
continue
|
||||
}
|
||||
x, y := int(ci)%w, int(ci)/w
|
||||
dx := at(x+1, y) - at(x-1, y)
|
||||
dy := at(x, y+1) - at(x, y-1)
|
||||
l := math.Hypot(dx, dy)
|
||||
if l < 1e-9 {
|
||||
continue
|
||||
}
|
||||
dx, dy = dx/l, dy/l
|
||||
lo := int((reach[s]-cfg.ScreeReachM)/cellM + 0.5)
|
||||
hi := int(reach[s]/cellM + 0.5)
|
||||
if lo < 0 {
|
||||
lo = 0
|
||||
}
|
||||
nsteps, total := 0, 0.0
|
||||
for t := lo; t <= hi && nsteps < len(line); t++ {
|
||||
px := x + int(math.Round(dx*float64(t)))
|
||||
py := y + int(math.Round(dy*float64(t)))
|
||||
if px < 0 || px >= w || py < 0 || py >= ht {
|
||||
break
|
||||
}
|
||||
v := screeWedge(float64(t)*cellM, reach[s], cfg.ScreeReachM)
|
||||
if v <= 0 {
|
||||
continue
|
||||
}
|
||||
line[nsteps], wgt[nsteps] = int32(py*w+px), v
|
||||
total += v
|
||||
nsteps++
|
||||
}
|
||||
if total <= 0 {
|
||||
continue
|
||||
}
|
||||
for k := 0; k < nsteps; k++ {
|
||||
add := cut[s] * wgt[k] / total
|
||||
h.Data[line[k]] += float32(add)
|
||||
laid += add
|
||||
}
|
||||
}
|
||||
return laid * area
|
||||
}
|
||||
|
||||
// crenulation is the noise that moves the whole profile in and out along the shore.
|
||||
//
|
||||
// It is applied to the *distance* rather than to the height, which is what makes it a crenulate coastline
|
||||
// rather than a rough one: the profile stays a profile and the shoreline wanders. And it is read at the
|
||||
// nearest waterline cell rather than at the cell being written, so it varies along the shore and not across
|
||||
// it - read per cell, a two-dimensional noise field would ripple the profile in the cross-shore direction
|
||||
// too, and a beach with corrugations up its face is not a beach.
|
||||
func (p CoastalParams) crenulation(h *field.Field) *field.Field {
|
||||
if p.Cfg.CrenulationM <= 0 || p.Cfg.CrenulationWaveM <= 0 || p.PeriodM <= 0 {
|
||||
return nil
|
||||
}
|
||||
f := p.Frame
|
||||
u, v := noise.WorldUV(f.W, f.H, h.CellM, f.OriginXM(), f.OriginYM(), p.PeriodM)
|
||||
base := int(p.PeriodM/p.Cfg.CrenulationWaveM + 0.5)
|
||||
if base < 2 {
|
||||
base = 2
|
||||
}
|
||||
return noise.FBMAt(u, v, noise.NewSource(p.Seed, srcCoastal),
|
||||
noise.Params{BaseCells: base, Octaves: 3, Gain: 0.5})
|
||||
}
|
||||
|
||||
// slotOf is where a waterline cell sits in the shore list, which is sorted because it was built by scanning.
|
||||
// -1 for a cell that is not on the list, which the distance transform should never hand back and which is
|
||||
// cheaper to rule out here than to debug as an index out of range at planet scale.
|
||||
func slotOf(shore []int32, cell int32) int {
|
||||
k := sort.Search(len(shore), func(k int) bool { return shore[k] >= cell })
|
||||
if k < len(shore) && shore[k] == cell {
|
||||
return k
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
// smoothShore blurs a signed distance field, in place.
|
||||
//
|
||||
// Smoothing the *distance* is the point, and it is worth saying what the two obvious alternatives do instead.
|
||||
// Smoothing the mask only moves the speckle around: it is a majority vote over a band that is half land and
|
||||
// half water, so it produces different speckle. Smoothing the heightmap flattens the berm along with it. The
|
||||
// distance is the one field whose smoothing has exactly the wanted effect - the shoreline becomes a curve, a
|
||||
// few metres from where the mask put it, and nothing else about the ground changes at all.
|
||||
//
|
||||
// Two passes rather than one, because one leaves a box kernel's corners in the isolines and they show in a
|
||||
// hillshade on ground this flat.
|
||||
func smoothShore(sd []float32, w, h, radius int) {
|
||||
field.BoxSmooth(sd, w, h, radius, 2)
|
||||
}
|
||||
|
||||
// percentiles sorts a copy and reads the median and the P90 off it. A few thousand shore cells a tile, so a
|
||||
// sort is nothing; this is the one place in the detail passes where that is true, and it is why there is no
|
||||
// histogram here the way there is in internal/stats.
|
||||
func percentiles(v []float64) (p50, p90 float64) {
|
||||
if len(v) == 0 {
|
||||
return 0, 0
|
||||
}
|
||||
c := append([]float64(nil), v...)
|
||||
sort.Float64s(c)
|
||||
return c[len(c)/2], c[int(float64(len(c)-1)*0.9)]
|
||||
}
|
||||
|
||||
func max64(a, b float64) float64 {
|
||||
if a > b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
// boundaryOf is the cells of a mask that are orthogonally against a cell that is not, which is to say its
|
||||
// edge on the inside.
|
||||
func boundaryOf(mask []bool, w, h int) []bool {
|
||||
out := make([]bool, len(mask))
|
||||
for y := 0; y < h; y++ {
|
||||
for x := 0; x < w; x++ {
|
||||
i := y*w + x
|
||||
if !mask[i] {
|
||||
continue
|
||||
}
|
||||
if (x > 0 && !mask[i-1]) || (x < w-1 && !mask[i+1]) ||
|
||||
(y > 0 && !mask[i-w]) || (y < h-1 && !mask[i+w]) {
|
||||
out[i] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// signedDistance is metres to the nearest boundary cell, positive inside the mask.
|
||||
//
|
||||
// Distance2 rather than Transform, because this one is thrown away after it has been smoothed and thresholded
|
||||
// back into a shoreline: nothing asks it which stretch of shore a cell belongs to, and the feature index and
|
||||
// the scratch it needs are two more arrays of four bytes a cell.
|
||||
func signedDistance(boundary, mask []bool, w, h int, cellM float64) []float32 {
|
||||
d2 := dt.Distance2(boundary, w, h, false)
|
||||
out := make([]float32, len(d2))
|
||||
for i := range d2 {
|
||||
d := float32(math.Sqrt(float64(d2[i])) * cellM)
|
||||
if mask[i] {
|
||||
out[i] = d
|
||||
} else {
|
||||
out[i] = -d
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// marchBackshore is how high the land stands behind each stretch of shore: the mean height between one and
|
||||
// two surf reaches inland, walked in along the shore normal.
|
||||
//
|
||||
// It is the window measureBackshore uses on the geology grid and for the same reason - it is clear of
|
||||
// everything the surf planed, whatever the exposure there was - and it is what decides whether a stretch of
|
||||
// shore is a beach or the foot of a cliff.
|
||||
//
|
||||
// **Walked rather than gathered**, and that is the whole of this function. The obvious implementation is to
|
||||
// scatter every cell in the band onto the stretch of shore nearest to it, which costs one pass and no marches
|
||||
// at all; it was the first one, and it is wrong in a way that only shows up on a real coastline. A cell two
|
||||
// hundred metres inland belongs to exactly one shore cell, so on a concave shore - the inside of every bay,
|
||||
// which is half of any coastline - the wedges converge and most shore cells are left owning nothing at all in
|
||||
// the band. Their backshore then reads zero, which is not "the land behind is at sea level", it is "I did not
|
||||
// look", and the two are indistinguishable afterwards. Measured on region 11: the median backshore over
|
||||
// 69 km of waterline read 0.0 m while the mean height of the land 110 to 220 m inland was 1.9 m.
|
||||
//
|
||||
// A march gives every stretch of shore its own samples, whichever way the coast bends. Where it walks off the
|
||||
// land - a spit narrower than a surf reach - the count stops rising, and a backshore of zero then means what
|
||||
// it says.
|
||||
func marchBackshore(h *field.Field, dist []float32, wet []bool, shore []int32, reach []float64,
|
||||
seaLevelM float64) []float64 {
|
||||
|
||||
w, ht := h.W, h.H
|
||||
cellM := h.CellM
|
||||
at := func(x, y int) float64 {
|
||||
if x < 0 {
|
||||
x = 0
|
||||
} else if x >= w {
|
||||
x = w - 1
|
||||
}
|
||||
if y < 0 {
|
||||
y = 0
|
||||
} else if y >= ht {
|
||||
y = ht - 1
|
||||
}
|
||||
return float64(dist[y*w+x])
|
||||
}
|
||||
out := make([]float64, len(shore))
|
||||
for s, ci := range shore {
|
||||
x, y := int(ci)%w, int(ci)/w
|
||||
// Inland is up the gradient of the signed distance, which is smooth here because the shoreline it is
|
||||
// measured from is a curve rather than the raw mask's boundary.
|
||||
dx := at(x+1, y) - at(x-1, y)
|
||||
dy := at(x, y+1) - at(x, y-1)
|
||||
l := math.Hypot(dx, dy)
|
||||
if l < 1e-9 {
|
||||
continue
|
||||
}
|
||||
dx, dy = dx/l, dy/l
|
||||
lo := int(reach[s]/cellM + 0.5)
|
||||
hi := 2 * lo
|
||||
var sum float64
|
||||
var count int
|
||||
for t := lo; t <= hi; t++ {
|
||||
px := x + int(math.Round(dx*float64(t)))
|
||||
py := y + int(math.Round(dy*float64(t)))
|
||||
if px < 0 || px >= w || py < 0 || py >= ht {
|
||||
break
|
||||
}
|
||||
j := py*w + px
|
||||
if !wet[j] {
|
||||
break
|
||||
}
|
||||
sum += float64(h.Data[j]) - seaLevelM
|
||||
count++
|
||||
}
|
||||
if count > 0 {
|
||||
out[s] = sum / float64(count)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// screeWedge is the shape of the apron along the march: a wedge under the foot of the cliff, thickest against
|
||||
// the face and thinning to nothing a scree reach seaward of it. Zero past the foot, because an apron lying
|
||||
// *on* the cliff is not an apron.
|
||||
func screeWedge(x, reachM, screeM float64) float64 {
|
||||
if x > reachM {
|
||||
return 0
|
||||
}
|
||||
d := reachM - x
|
||||
if d >= screeM {
|
||||
return 0
|
||||
}
|
||||
return 1 - d/screeM
|
||||
}
|
||||
|
||||
// smoothShoreRoughness damps the metre-scale texture near the shore, in place.
|
||||
//
|
||||
// A blur of a few cells, mixed in by how close a cell is to the waterline. The radius is what keeps it a
|
||||
// *roughness* fade rather than a shape one: at six metres it takes the top off the detail noise and the
|
||||
// droplet rills and leaves everything the solve built, which is tens of metres across at the very least.
|
||||
//
|
||||
// Full strength within half a surf reach either side, then off over reachM more. Both sides on purpose - the
|
||||
// shallows get the same treatment as the backshore, because a shore is a *place* rather than a line and it is
|
||||
// smoother than either the land or the sea bed away from it.
|
||||
//
|
||||
// **Masked, and that is not a detail.** A plain blur across the waterline does not damp texture, it bridges
|
||||
// the shoreline: the step there is a landform and not roughness. Measured on a fixture with forty metres of
|
||||
// water against the land, an unmasked blur lifted the sea floor by twenty metres, which is a beach the size
|
||||
// of the drowned valley it was supposed to leave alone.
|
||||
func smoothShoreRoughness(h *field.Field, dist []float32, wet []bool, surfReachM, reachM float64) {
|
||||
if reachM <= 0 {
|
||||
return
|
||||
}
|
||||
radius := int(shoreRoughM/h.CellM + 0.5)
|
||||
if radius < 1 {
|
||||
return
|
||||
}
|
||||
soft := append([]float32(nil), h.Data...)
|
||||
dry := make([]bool, len(wet))
|
||||
for i, on := range wet {
|
||||
dry[i] = !on
|
||||
}
|
||||
field.BoxSmoothMasked(soft, wet, h.W, h.H, radius, 2)
|
||||
field.BoxSmoothMasked(soft, dry, h.W, h.H, radius, 2)
|
||||
|
||||
core := surfReachM * 0.5
|
||||
out := core + reachM
|
||||
for i := range h.Data {
|
||||
d := math.Abs(float64(dist[i]))
|
||||
if d >= out {
|
||||
continue
|
||||
}
|
||||
w := 1.0
|
||||
if d > core {
|
||||
w = noise.Smoothstep((out - d) / (out - core))
|
||||
}
|
||||
h.Data[i] += float32(w * (float64(soft[i]) - float64(h.Data[i])))
|
||||
}
|
||||
}
|
||||
|
||||
// shoreRoughM is the wavelength the shore fade takes off. It is deliberately short: this is meant to remove
|
||||
// the texture the detail passes added and nothing the solve built, and the solve's finest feature is a gully
|
||||
// tens of metres across.
|
||||
const shoreRoughM = 6
|
||||
Reference in New Issue
Block a user