Added: Initial world generation tool
This commit is contained in:
@@ -0,0 +1,79 @@
|
||||
package fluvial
|
||||
|
||||
// A monotone bucket priority queue, which is what priority-flood actually needs.
|
||||
//
|
||||
// The flood pops cells in non-decreasing elevation and never pushes anything below the cell it just popped:
|
||||
// a neighbour lower than the current front is raised to it and goes to the FIFO instead. That "monotone"
|
||||
// property is exactly the condition under which a bucket queue beats a binary heap, because the read cursor
|
||||
// only ever moves forward and both operations become an append and a scan. The heap was costing about
|
||||
// log2(3.2M) = 22 comparisons and as many cache misses per operation, on two thirds of the solve's runtime.
|
||||
//
|
||||
// Elevations are quantised into fixed-width buckets. Cells inside one bucket pop in an arbitrary but
|
||||
// deterministic order (last in, first out), so a spill point can be wrong by at most one bucket width. At a
|
||||
// centimetre against a 2 km elevation range that is far below the millimetre-per-cell epsilon the flood adds
|
||||
// anyway, and it is the same approximation an integer-elevation priority-flood makes by construction.
|
||||
type bucketPQ struct {
|
||||
lo float64
|
||||
width float64
|
||||
buckets [][]int32
|
||||
cur int
|
||||
count int
|
||||
}
|
||||
|
||||
const bucketWidthM = 0.01
|
||||
|
||||
func newBucketPQ(loM, hiM float64) *bucketPQ {
|
||||
if hiM <= loM {
|
||||
hiM = loM + 1
|
||||
}
|
||||
// Headroom above the top: the flood raises cells by epsilon as it fills, so the highest key pushed can
|
||||
// sit slightly above the terrain's own maximum.
|
||||
n := int((hiM-loM)/bucketWidthM) + 64
|
||||
return &bucketPQ{lo: loM, width: bucketWidthM, buckets: make([][]int32, n)}
|
||||
}
|
||||
|
||||
func (q *bucketPQ) reset() {
|
||||
for i := range q.buckets {
|
||||
q.buckets[i] = q.buckets[i][:0]
|
||||
}
|
||||
q.cur = 0
|
||||
q.count = 0
|
||||
}
|
||||
|
||||
func (q *bucketPQ) len() int { return q.count }
|
||||
|
||||
func (q *bucketPQ) push(elev float32, idx int32) {
|
||||
b := int((float64(elev) - q.lo) / q.width)
|
||||
if b < q.cur {
|
||||
b = q.cur // monotone: never behind the cursor, whatever rounding says
|
||||
}
|
||||
if b >= len(q.buckets) {
|
||||
b = len(q.buckets) - 1
|
||||
}
|
||||
q.buckets[b] = append(q.buckets[b], idx)
|
||||
q.count++
|
||||
}
|
||||
|
||||
// pop returns the lowest cell. The cursor only moves forward, so the total scan cost over a whole flood is
|
||||
// the number of buckets, not the number of pops.
|
||||
func (q *bucketPQ) pop() int32 {
|
||||
for q.cur < len(q.buckets) && len(q.buckets[q.cur]) == 0 {
|
||||
q.cur++
|
||||
}
|
||||
if q.cur >= len(q.buckets) {
|
||||
return -1
|
||||
}
|
||||
b := q.buckets[q.cur]
|
||||
v := b[len(b)-1]
|
||||
q.buckets[q.cur] = b[:len(b)-1]
|
||||
q.count--
|
||||
return v
|
||||
}
|
||||
|
||||
// frontElev is the elevation the cursor is at, which the FIFO compares itself against.
|
||||
func (q *bucketPQ) frontElev() float32 {
|
||||
for q.cur < len(q.buckets) && len(q.buckets[q.cur]) == 0 {
|
||||
q.cur++
|
||||
}
|
||||
return float32(q.lo + float64(q.cur)*q.width)
|
||||
}
|
||||
Reference in New Issue
Block a user