Implement the vanilla Aquifer; stop flooding caves
Every air block below y=63 was turned into water. That is one line of code and it cost the entire underground: no dry caves, no lava lakes, no air pockets, a solid block of water from the sea floor to bedrock. Vanilla decides fluid per position instead. Aquifer centres sit on a jittered 16x12x16 grid; each gets a fluid level and type from the floodedness and spread noises, with centres near open sky inheriting the sea and buried ones getting a much lower randomised level or nothing at all. A position takes its nearest centre's fluid unless the barrier noise raises enough pressure between the two or three nearest centres to seal it back to stone. Deep centres turn to lava. Porting it means fixing the order of generation, not just adding a file. Vanilla resolves stone/water/lava/air during the density pass and only then runs the surface rules over a finished column; we did it the other way round, which is what forced the unconditional flood in the first place. fillVanillaColumn now asks the aquifer per position, and applySurfaceRule walks the finished column carrying the bookkeeping SurfaceSystem carries: air resets the counters, a fluid records its water height, and stone gets a depth from the top of its run plus one from the bottom, found by looking ahead to the next non-stone block below. That last one fixes stone_depth's ceiling form, which had no bottom-up depth to work with and was testing the top-down one instead -- fourteen rules in the overworld tree use it to dress cave roofs. The floor form is unchanged: vanilla counts from 1 and compares against 1 + offset, we counted from 0 and compared against offset. The aquifer grid is built eagerly per chunk rather than lazily, because our columns fill concurrently; every cell is a pure function of its grid coordinate and every cell in the computed range gets consulted anyway. Cost is ~0.5% of chunk generation, most of it absorbed by the shared preliminary-surface cache. Inland caves go from 100% water to 3.8%, and lava exists for the first time. cmd/gendump grows a census that would have failed loudly before, and TestCavesAreDry guards it in the suite.
This commit is contained in:
parent
ed045ee09d
commit
21a10ab65e
7 changed files with 805 additions and 73 deletions
446
internal/worldgen/aquifer.go
Normal file
446
internal/worldgen/aquifer.go
Normal file
|
|
@ -0,0 +1,446 @@
|
|||
package worldgen
|
||||
|
||||
import (
|
||||
"math"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// aquifer.go ports net.minecraft.world.level.levelgen.Aquifer.NoiseBasedAquifer.
|
||||
//
|
||||
// The aquifer is what decides, for every position the density function leaves
|
||||
// empty, whether it becomes air, water or lava. Without it a generator has to
|
||||
// guess — the usual guess being "water everywhere below sea level", which
|
||||
// drowns every cave under y=63 and leaves no lava lakes anywhere.
|
||||
//
|
||||
// Vanilla scatters aquifer centres on a 16×12×16 grid, jittered by a positional
|
||||
// RNG. Each centre gets a FluidStatus: a fluid level and a fluid type. A
|
||||
// position takes the fluid of its nearest centre, unless the barrier noise
|
||||
// raises enough "pressure" between the two or three nearest centres to seal the
|
||||
// position off as stone instead. Centres near the open sky inherit the global
|
||||
// sea level, so oceans and lakes still fill normally; centres buried deep get a
|
||||
// randomised, usually much lower level, which is why caves are dry.
|
||||
|
||||
// Block-state network IDs the aquifer places. The worldgen package deliberately
|
||||
// does not import the world package; these match blockids.go.
|
||||
const (
|
||||
blockAir uint16 = 0
|
||||
blockWater uint16 = 86
|
||||
blockLava uint16 = 102
|
||||
)
|
||||
|
||||
// Aquifer grid geometry (Aquifer.NoiseBasedAquifer constants).
|
||||
const (
|
||||
aquiferXSpacing = 16
|
||||
aquiferYSpacing = 12
|
||||
aquiferZSpacing = 16
|
||||
aquiferXRange = 10
|
||||
aquiferYRange = 9
|
||||
aquiferZRange = 10
|
||||
|
||||
// WayBelowMinY is DimensionType.WAY_BELOW_MIN_Y (MIN_Y << 4, MIN_Y=-2032):
|
||||
// the "this aquifer holds nothing" sentinel fluid level.
|
||||
WayBelowMinY = -32512
|
||||
)
|
||||
|
||||
// deepDark is OverworldBiomeBuilder.isDeepDarkRegion's thresholds, kept at the
|
||||
// exact double values the float constants widen to.
|
||||
const (
|
||||
deepDarkErosionMax = -0.22499999403953552
|
||||
deepDarkDepthMin = 0.8999999761581421
|
||||
)
|
||||
|
||||
// FluidStatus is a fluid level plus the fluid filling up to it (Aquifer.FluidStatus).
|
||||
type FluidStatus struct {
|
||||
Level int
|
||||
Type uint16
|
||||
}
|
||||
|
||||
// At returns the fluid at blockY, or air above the level.
|
||||
func (f FluidStatus) At(blockY int) uint16 {
|
||||
if blockY < f.Level {
|
||||
return f.Type
|
||||
}
|
||||
return blockAir
|
||||
}
|
||||
|
||||
// FluidPicker is the dimension-wide fluid rule (Aquifer.FluidPicker): what a
|
||||
// position would hold if there were no aquifer at all.
|
||||
type FluidPicker func(x, y, z int) FluidStatus
|
||||
|
||||
// OverworldFluidPicker is NoiseBasedChunkGenerator.createFluidPicker: lava
|
||||
// below y=-54, sea water above it.
|
||||
func OverworldFluidPicker(seaLevel int) FluidPicker {
|
||||
lava := FluidStatus{Level: -54, Type: blockLava}
|
||||
sea := FluidStatus{Level: seaLevel, Type: blockWater}
|
||||
lavaBelow := min(-54, seaLevel)
|
||||
return func(_, y, _ int) FluidStatus {
|
||||
if y < lavaBelow {
|
||||
return lava
|
||||
}
|
||||
return sea
|
||||
}
|
||||
}
|
||||
|
||||
// surfaceSamplingOffsets is SURFACE_SAMPLING_OFFSETS_IN_CHUNKS: the thirteen
|
||||
// chunk offsets an aquifer centre probes to work out whether it is under open
|
||||
// sky or buried. The set is lopsided towards -X on purpose — it is vanilla's.
|
||||
var surfaceSamplingOffsets = [13][2]int{
|
||||
{0, 0}, {-2, -1}, {-1, -1}, {0, -1}, {1, -1}, {-3, 0}, {-2, 0},
|
||||
{-1, 0}, {1, 0}, {-2, 1}, {-1, 1}, {0, 1}, {1, 1},
|
||||
}
|
||||
|
||||
// Aquifer resolves fluid for one chunk. Its cell grid is computed up front so
|
||||
// the chunk's columns can be filled in parallel without locking.
|
||||
type Aquifer struct {
|
||||
od *OverworldDensity
|
||||
global FluidPicker
|
||||
|
||||
minGridX, minGridY, minGridZ int
|
||||
gridSizeX, gridSizeY, gridSizeZ int
|
||||
|
||||
locations []aquiferPos
|
||||
status []FluidStatus
|
||||
|
||||
// skipSamplingAboveY is the height above which the grid is irrelevant and
|
||||
// the global fluid rule answers directly.
|
||||
skipSamplingAboveY int
|
||||
}
|
||||
|
||||
type aquiferPos struct{ x, y, z int }
|
||||
|
||||
// NewAquifer builds the aquifer covering the given chunk.
|
||||
//
|
||||
// Vanilla fills the cell grid lazily as columns are generated; we fill it
|
||||
// eagerly because our columns are generated concurrently. That is not a
|
||||
// fidelity change: every cell's centre and status is a pure function of its
|
||||
// grid coordinate, and every cell in the range computed here is consulted by
|
||||
// some position in the chunk anyway.
|
||||
func NewAquifer(od *OverworldDensity, chunkX, chunkZ int, picker FluidPicker) *Aquifer {
|
||||
minBlockX, minBlockZ := chunkX*16, chunkZ*16
|
||||
maxBlockX, maxBlockZ := minBlockX+15, minBlockZ+15
|
||||
|
||||
a := &Aquifer{od: od, global: picker}
|
||||
a.minGridX = aquiferGridX(minBlockX - 5)
|
||||
maxGridX := aquiferGridX(maxBlockX-5) + 1
|
||||
a.gridSizeX = maxGridX - a.minGridX + 1
|
||||
a.minGridY = aquiferGridY(od.MinY+1) - 1
|
||||
maxGridY := aquiferGridY(od.MinY+od.Height+1) + 1
|
||||
a.gridSizeY = maxGridY - a.minGridY + 1
|
||||
a.minGridZ = aquiferGridZ(minBlockZ - 5)
|
||||
maxGridZ := aquiferGridZ(maxBlockZ-5) + 1
|
||||
a.gridSizeZ = maxGridZ - a.minGridZ + 1
|
||||
|
||||
n := a.gridSizeX * a.gridSizeY * a.gridSizeZ
|
||||
a.locations = make([]aquiferPos, n)
|
||||
a.status = make([]FluidStatus, n)
|
||||
|
||||
maxAdjusted := adjustSurfaceLevel(od.MaxPreliminarySurfaceLevel(
|
||||
fromAquiferGridX(a.minGridX, 0), fromAquiferGridZ(a.minGridZ, 0),
|
||||
fromAquiferGridX(maxGridX, 9), fromAquiferGridZ(maxGridZ, 9)))
|
||||
a.skipSamplingAboveY = fromAquiferGridY(aquiferGridY(maxAdjusted+12)+1, 11) - 1
|
||||
|
||||
// Cells above the highest consulted anchor are never read: computeSubstance
|
||||
// returns the global fluid before touching the grid once y climbs past
|
||||
// skipSamplingAboveY, and the anchor search reaches at most one cell higher.
|
||||
topUsedGridY := min(aquiferGridY(a.skipSamplingAboveY+1)+1, maxGridY)
|
||||
|
||||
var wg sync.WaitGroup
|
||||
for gy := a.minGridY; gy <= topUsedGridY; gy++ {
|
||||
wg.Add(1)
|
||||
go func(gy int) {
|
||||
defer wg.Done()
|
||||
for gz := a.minGridZ; gz < a.minGridZ+a.gridSizeZ; gz++ {
|
||||
for gx := a.minGridX; gx < a.minGridX+a.gridSizeX; gx++ {
|
||||
i := a.index(gx, gy, gz)
|
||||
r := od.AquiferRandom.At(gx, gy, gz)
|
||||
pos := aquiferPos{
|
||||
x: fromAquiferGridX(gx, int(r.NextIntN(aquiferXRange))),
|
||||
y: fromAquiferGridY(gy, int(r.NextIntN(aquiferYRange))),
|
||||
z: fromAquiferGridZ(gz, int(r.NextIntN(aquiferZRange))),
|
||||
}
|
||||
a.locations[i] = pos
|
||||
a.status[i] = a.computeFluid(pos.x, pos.y, pos.z)
|
||||
}
|
||||
}
|
||||
}(gy)
|
||||
}
|
||||
wg.Wait()
|
||||
return a
|
||||
}
|
||||
|
||||
func (a *Aquifer) index(gridX, gridY, gridZ int) int {
|
||||
x := gridX - a.minGridX
|
||||
y := gridY - a.minGridY
|
||||
z := gridZ - a.minGridZ
|
||||
return (y*a.gridSizeZ+z)*a.gridSizeX + x
|
||||
}
|
||||
|
||||
// ComputeSubstance decides what fills (x,y,z) given the final density there.
|
||||
// ok=false means the position stays the settings' default block (stone);
|
||||
// otherwise the returned state is the fluid — which may be air.
|
||||
//
|
||||
// Vanilla additionally tracks shouldScheduleFluidUpdate here, to mark positions
|
||||
// where two neighbouring aquifers disagree so the fluid flows on first tick. We
|
||||
// have no fluid ticking yet and the flag never affects the block placed, so it
|
||||
// is left out; the fourth-nearest centre, which only feeds that flag, is not
|
||||
// tracked either.
|
||||
func (a *Aquifer) ComputeSubstance(x, y, z int, density float64) (uint16, bool) {
|
||||
if density > 0 {
|
||||
return 0, false
|
||||
}
|
||||
global := a.global(x, y, z)
|
||||
if y > a.skipSamplingAboveY {
|
||||
return global.At(y), true
|
||||
}
|
||||
if global.At(y) == blockLava {
|
||||
return blockLava, true
|
||||
}
|
||||
|
||||
xAnchor := aquiferGridX(x - 5)
|
||||
yAnchor := aquiferGridY(y + 1)
|
||||
zAnchor := aquiferGridZ(z - 5)
|
||||
dist1, dist2, dist3 := math.MaxInt32, math.MaxInt32, math.MaxInt32
|
||||
idx1, idx2, idx3 := 0, 0, 0
|
||||
for dx := 0; dx <= 1; dx++ {
|
||||
for dy := -1; dy <= 1; dy++ {
|
||||
for dz := 0; dz <= 1; dz++ {
|
||||
i := a.index(xAnchor+dx, yAnchor+dy, zAnchor+dz)
|
||||
p := a.locations[i]
|
||||
ox, oy, oz := p.x-x, p.y-y, p.z-z
|
||||
d := ox*ox + oy*oy + oz*oz
|
||||
switch {
|
||||
case dist1 >= d:
|
||||
idx3, idx2, idx1 = idx2, idx1, i
|
||||
dist3, dist2, dist1 = dist2, dist1, d
|
||||
case dist2 >= d:
|
||||
idx3, idx2 = idx2, i
|
||||
dist3, dist2 = dist2, d
|
||||
case dist3 >= d:
|
||||
idx3, dist3 = i, d
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
closest1 := a.status[idx1]
|
||||
sim12 := aquiferSimilarity(dist1, dist2)
|
||||
fluid := closest1.At(y)
|
||||
if sim12 <= 0 {
|
||||
return fluid, true
|
||||
}
|
||||
// Water sitting directly on the global lava level always wins: it is what
|
||||
// makes the lava-lake shorelines steam rather than vanish.
|
||||
if fluid == blockWater && a.global(x, y-1, z).At(y-1) == blockLava {
|
||||
return fluid, true
|
||||
}
|
||||
|
||||
barrierNoise := math.NaN()
|
||||
closest2 := a.status[idx2]
|
||||
if density+sim12*a.calculatePressure(x, y, z, &barrierNoise, closest1, closest2) > 0 {
|
||||
return 0, false
|
||||
}
|
||||
closest3 := a.status[idx3]
|
||||
if sim13 := aquiferSimilarity(dist1, dist3); sim13 > 0 {
|
||||
if density+sim12*sim13*a.calculatePressure(x, y, z, &barrierNoise, closest1, closest3) > 0 {
|
||||
return 0, false
|
||||
}
|
||||
}
|
||||
if sim23 := aquiferSimilarity(dist2, dist3); sim23 > 0 {
|
||||
if density+sim12*sim23*a.calculatePressure(x, y, z, &barrierNoise, closest2, closest3) > 0 {
|
||||
return 0, false
|
||||
}
|
||||
}
|
||||
return fluid, true
|
||||
}
|
||||
|
||||
// aquiferSimilarity falls from 1 to 0 as the second distance pulls away from
|
||||
// the first; at or below 0 the nearest centre wins outright and no barrier is
|
||||
// evaluated.
|
||||
func aquiferSimilarity(distSqr1, distSqr2 int) float64 {
|
||||
return 1.0 - float64(distSqr2-distSqr1)/25.0
|
||||
}
|
||||
|
||||
// calculatePressure is the barrier between two aquifers: how hard the rock
|
||||
// between them resists being carved open. barrierNoise memoises the noise
|
||||
// sample across the (up to three) pressure evaluations at one position, exactly
|
||||
// as vanilla's MutableDouble does.
|
||||
func (a *Aquifer) calculatePressure(x, y, z int, barrierNoise *float64, s1, s2 FluidStatus) float64 {
|
||||
type1 := s1.At(y)
|
||||
type2 := s2.At(y)
|
||||
if (type1 == blockLava && type2 == blockWater) || (type1 == blockWater && type2 == blockLava) {
|
||||
return 2.0
|
||||
}
|
||||
fluidYDiff := s1.Level - s2.Level
|
||||
if fluidYDiff < 0 {
|
||||
fluidYDiff = -fluidYDiff
|
||||
}
|
||||
if fluidYDiff == 0 {
|
||||
return 0.0
|
||||
}
|
||||
averageFluidY := 0.5 * float64(s1.Level+s2.Level)
|
||||
howFarAboveAverage := float64(y) + 0.5 - averageFluidY
|
||||
baseValue := float64(fluidYDiff) / 2.0
|
||||
// Distance from the barrier's edge towards its middle; the biases below are
|
||||
// vanilla's, and they are asymmetric: rock reaches much further down from a
|
||||
// fluid surface than up from it.
|
||||
distanceFromEdge := baseValue - math.Abs(howFarAboveAverage)
|
||||
var gradient float64
|
||||
if howFarAboveAverage > 0 {
|
||||
if centerPoint := 0.0 + distanceFromEdge; centerPoint > 0 {
|
||||
gradient = centerPoint / 1.5
|
||||
} else {
|
||||
gradient = centerPoint / 2.5
|
||||
}
|
||||
} else {
|
||||
if centerPoint := 3.0 + distanceFromEdge; centerPoint > 0 {
|
||||
gradient = centerPoint / 3.0
|
||||
} else {
|
||||
gradient = centerPoint / 10.0
|
||||
}
|
||||
}
|
||||
var noiseValue float64
|
||||
if gradient >= -2.0 && gradient <= 2.0 {
|
||||
if math.IsNaN(*barrierNoise) {
|
||||
*barrierNoise = a.od.Barrier.Compute(FunctionContext{X: float64(x), Y: float64(y), Z: float64(z)})
|
||||
}
|
||||
noiseValue = *barrierNoise
|
||||
}
|
||||
return 2.0 * (noiseValue + gradient)
|
||||
}
|
||||
|
||||
// computeFluid decides one aquifer centre's fluid level and type.
|
||||
func (a *Aquifer) computeFluid(x, y, z int) FluidStatus {
|
||||
global := a.global(x, y, z)
|
||||
lowestPreliminarySurface := math.MaxInt32
|
||||
topOfCell := y + aquiferYSpacing
|
||||
bottomOfCell := y - aquiferYSpacing
|
||||
surfaceAtCentreIsUnderFluid := false
|
||||
for _, off := range surfaceSamplingOffsets {
|
||||
sampleX := x + off[0]*16
|
||||
sampleZ := z + off[1]*16
|
||||
preliminary := a.od.PreliminarySurfaceLevelAt(sampleX, sampleZ)
|
||||
adjusted := adjustSurfaceLevel(preliminary)
|
||||
start := off[0] == 0 && off[1] == 0
|
||||
// Wholly below the terrain: an ordinary underground aquifer, whose
|
||||
// level the noise decides.
|
||||
if start && bottomOfCell > adjusted {
|
||||
return global
|
||||
}
|
||||
pokesAboveSurface := topOfCell > adjusted
|
||||
if pokesAboveSurface || start {
|
||||
if atSurface := a.global(sampleX, adjusted, sampleZ); atSurface.At(adjusted) != blockAir {
|
||||
if start {
|
||||
surfaceAtCentreIsUnderFluid = true
|
||||
}
|
||||
// Breaking the surface under an ocean: take the ocean's level,
|
||||
// so sea floors do not dry out.
|
||||
if pokesAboveSurface {
|
||||
return atSurface
|
||||
}
|
||||
}
|
||||
}
|
||||
lowestPreliminarySurface = min(lowestPreliminarySurface, preliminary)
|
||||
}
|
||||
level := a.computeSurfaceLevel(x, y, z, global, lowestPreliminarySurface, surfaceAtCentreIsUnderFluid)
|
||||
return FluidStatus{Level: level, Type: a.computeFluidType(x, y, z, global, level)}
|
||||
}
|
||||
|
||||
func adjustSurfaceLevel(preliminarySurfaceLevel int) int { return preliminarySurfaceLevel + 8 }
|
||||
|
||||
// computeSurfaceLevel picks the aquifer's fluid level: the global one when the
|
||||
// floodedness noise says "fully flooded", a randomised low one when it says
|
||||
// "partially", and nothing at all otherwise — which is what leaves caves dry.
|
||||
func (a *Aquifer) computeSurfaceLevel(x, y, z int, global FluidStatus, lowestPreliminarySurface int, surfaceAtCentreIsUnderFluid bool) int {
|
||||
ctx := FunctionContext{X: float64(x), Y: float64(y), Z: float64(z)}
|
||||
var partiallyFloodedness, fullyFloodedness float64
|
||||
if a.isDeepDarkRegion(ctx) {
|
||||
// The deep dark is never flooded.
|
||||
partiallyFloodedness, fullyFloodedness = -1.0, -1.0
|
||||
} else {
|
||||
distanceBelowSurface := lowestPreliminarySurface + 8 - y
|
||||
floodednessFactor := 0.0
|
||||
if surfaceAtCentreIsUnderFluid {
|
||||
floodednessFactor = clampedMap(float64(distanceBelowSurface), 0.0, 64.0, 1.0, 0.0)
|
||||
}
|
||||
floodednessNoise := clamp(a.od.FluidLevelFloodedness.Compute(ctx), -1.0, 1.0)
|
||||
fullyFloodedThreshold := mapRange(floodednessFactor, 1.0, 0.0, -0.3, 0.8)
|
||||
partiallyFloodedThreshold := mapRange(floodednessFactor, 1.0, 0.0, -0.8, 0.4)
|
||||
partiallyFloodedness = floodednessNoise - partiallyFloodedThreshold
|
||||
fullyFloodedness = floodednessNoise - fullyFloodedThreshold
|
||||
}
|
||||
switch {
|
||||
case fullyFloodedness > 0:
|
||||
return global.Level
|
||||
case partiallyFloodedness > 0:
|
||||
return a.computeRandomizedFluidSurfaceLevel(x, y, z, lowestPreliminarySurface)
|
||||
default:
|
||||
return WayBelowMinY
|
||||
}
|
||||
}
|
||||
|
||||
// computeRandomizedFluidSurfaceLevel puts the water table somewhere in the
|
||||
// middle of a 40-block-tall cell, nudged by the spread noise and quantised to
|
||||
// three blocks so neighbouring cells share levels often enough to connect.
|
||||
func (a *Aquifer) computeRandomizedFluidSurfaceLevel(x, y, z, lowestPreliminarySurface int) int {
|
||||
const cellWidth, cellHeight, maxSpread = 16, 40, 10
|
||||
cellX := floorDivInt(x, cellWidth)
|
||||
cellY := floorDivInt(y, cellHeight)
|
||||
cellZ := floorDivInt(z, cellWidth)
|
||||
middleY := cellY*cellHeight + cellHeight/2
|
||||
spread := a.od.FluidLevelSpread.Compute(FunctionContext{X: float64(cellX), Y: float64(cellY), Z: float64(cellZ)}) * maxSpread
|
||||
return min(lowestPreliminarySurface, middleY+quantizeToMultiple(spread, 3))
|
||||
}
|
||||
|
||||
// computeFluidType turns deep aquifers into lava lakes.
|
||||
func (a *Aquifer) computeFluidType(x, y, z int, global FluidStatus, fluidSurfaceLevel int) uint16 {
|
||||
if fluidSurfaceLevel > -10 || fluidSurfaceLevel == WayBelowMinY || global.Type == blockLava {
|
||||
return global.Type
|
||||
}
|
||||
const cellWidth, cellHeight = 64, 40
|
||||
lavaNoise := a.od.Lava.Compute(FunctionContext{
|
||||
X: float64(floorDivInt(x, cellWidth)),
|
||||
Y: float64(floorDivInt(y, cellHeight)),
|
||||
Z: float64(floorDivInt(z, cellWidth)),
|
||||
})
|
||||
if math.Abs(lavaNoise) > 0.3 {
|
||||
return blockLava
|
||||
}
|
||||
return global.Type
|
||||
}
|
||||
|
||||
// isDeepDarkRegion is OverworldBiomeBuilder.isDeepDarkRegion.
|
||||
func (a *Aquifer) isDeepDarkRegion(ctx FunctionContext) bool {
|
||||
if a.od.Erosion == nil || a.od.Depth == nil {
|
||||
return false
|
||||
}
|
||||
return a.od.Erosion.Compute(ctx) < deepDarkErosionMax && a.od.Depth.Compute(ctx) > deepDarkDepthMin
|
||||
}
|
||||
|
||||
// ---- grid arithmetic ---------------------------------------------------
|
||||
|
||||
func aquiferGridX(blockCoord int) int { return blockCoord >> 4 }
|
||||
func aquiferGridZ(blockCoord int) int { return blockCoord >> 4 }
|
||||
func aquiferGridY(blockCoord int) int { return floorDivInt(blockCoord, aquiferYSpacing) }
|
||||
func fromAquiferGridX(grid, offset int) int { return grid<<4 + offset }
|
||||
func fromAquiferGridZ(grid, offset int) int { return grid<<4 + offset }
|
||||
func fromAquiferGridY(grid, offset int) int { return grid*aquiferYSpacing + offset }
|
||||
|
||||
// floorDivInt is Math.floorDiv: division rounding towards negative infinity.
|
||||
func floorDivInt(a, b int) int {
|
||||
q := a / b
|
||||
if a%b != 0 && (a < 0) != (b < 0) {
|
||||
q--
|
||||
}
|
||||
return q
|
||||
}
|
||||
|
||||
// quantizeToMultiple is Mth.quantize: round down to a multiple of factor.
|
||||
func quantizeToMultiple(value float64, factor int) int {
|
||||
return int(math.Floor(value/float64(factor))) * factor
|
||||
}
|
||||
|
||||
// mapRange is Mth.map: an unclamped linear remap (clampedMap is the clamped one).
|
||||
func mapRange(value, from0, to0, from1, to1 float64) float64 {
|
||||
t := (value - from0) / (to0 - from0)
|
||||
return from1 + t*(to1-from1)
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue