This commit is contained in:
adman
2026-07-16 01:13:31 +02:00
parent b966925212
commit 5c5c391aa6
14 changed files with 441 additions and 120 deletions
+230 -17
View File
@@ -3,6 +3,7 @@ package data
import (
"fmt"
"math"
"sync"
)
// MultiNoisePreset names the two built-in multi-noise parameter lists.
@@ -32,6 +33,10 @@ type ParameterPoint struct {
// MultiNoiseData holds the parameter lists for each preset.
type MultiNoiseData struct {
Presets map[string][]ParameterPoint
bvls map[string]*bvhNode
bvhOnce sync.Once
bvhErr error
}
// raw JSON shapes.
@@ -95,25 +100,20 @@ func (m *MultiNoiseData) Lookup(preset string, t, h, c, e, d, w int64) (string,
return "", fmt.Errorf("multi-noise preset %s has no entries", preset)
}
bestBiome := ""
bestDist := int64(math.MaxInt64)
for _, p := range points {
dist := distanceToRange(t, p.Temperature) +
distanceToRange(h, p.Humidity) +
distanceToRange(c, p.Continentalness) +
distanceToRange(e, p.Erosion) +
distanceToRange(d, p.Depth) +
distanceToRange(w, p.Weirdness) +
p.Offset
if dist < bestDist {
bestDist = dist
bestBiome = p.Biome
}
m.bvhOnce.Do(func() { m.bvhErr = m.buildBVHs() })
if m.bvhErr != nil {
return "", m.bvhErr
}
return bestBiome, nil
root := m.bvls[preset]
if root == nil {
return "", fmt.Errorf("no BVH for preset: %s", preset)
}
q := point6{t, h, c, e, d, w}
best := result{dist: int64(math.MaxInt64)}
root.lookup(q, points, &best)
return best.biome, nil
}
func distanceToRange(value int64, r ParamRange) int64 {
@@ -127,3 +127,216 @@ func distanceToRange(value int64, r ParamRange) int64 {
}
return 0
}
// point6 is a quantized climate sample in six dimensions.
type point6 struct{ t, h, c, e, d, w int64 }
func (q point6) dist(p ParameterPoint) int64 {
return distanceToRange(q.t, p.Temperature) +
distanceToRange(q.h, p.Humidity) +
distanceToRange(q.c, p.Continentalness) +
distanceToRange(q.e, p.Erosion) +
distanceToRange(q.d, p.Depth) +
distanceToRange(q.w, p.Weirdness) +
p.Offset
}
type result struct {
dist int64
biome string
}
// bvhNode is a bounding-volume-hierarchy node for fast nearest-box search.
type bvhNode struct {
bounds ParameterPoint
left *bvhNode
right *bvhNode
leaf []int // indices into original slice; non-nil only for leaves
}
func (m *MultiNoiseData) buildBVHs() error {
m.bvls = make(map[string]*bvhNode, len(m.Presets))
for preset, points := range m.Presets {
indices := make([]int, len(points))
for i := range indices {
indices[i] = i
}
m.bvls[preset] = buildBVH(points, indices, 0)
}
return nil
}
func buildBVH(points []ParameterPoint, indices []int, depth int) *bvhNode {
node := &bvhNode{}
node.bounds = boundsOf(points, indices)
// Small leaves avoid BVH overhead for tiny lists.
if len(indices) <= 16 {
node.leaf = make([]int, len(indices))
copy(node.leaf, indices)
return node
}
// Pick axis by largest variance.
axis := depth % 6
if axis < 0 {
axis = 0
}
// Partition by median of the chosen axis' center.
mid := len(indices) / 2
quickSelectByAxis(points, indices, axis, mid)
leftIdx := indices[:mid]
rightIdx := indices[mid:]
// Degenerate split: fall back to leaf to avoid infinite recursion.
if len(leftIdx) == 0 || len(rightIdx) == 0 {
node.leaf = make([]int, len(indices))
copy(node.leaf, indices)
return node
}
node.left = buildBVH(points, leftIdx, depth+1)
node.right = buildBVH(points, rightIdx, depth+1)
return node
}
func boundsOf(points []ParameterPoint, indices []int) ParameterPoint {
if len(indices) == 0 {
return ParameterPoint{}
}
b := points[indices[0]]
for _, i := range indices[1:] {
p := points[i]
if p.Temperature.Min < b.Temperature.Min {
b.Temperature.Min = p.Temperature.Min
}
if p.Temperature.Max > b.Temperature.Max {
b.Temperature.Max = p.Temperature.Max
}
if p.Humidity.Min < b.Humidity.Min {
b.Humidity.Min = p.Humidity.Min
}
if p.Humidity.Max > b.Humidity.Max {
b.Humidity.Max = p.Humidity.Max
}
if p.Continentalness.Min < b.Continentalness.Min {
b.Continentalness.Min = p.Continentalness.Min
}
if p.Continentalness.Max > b.Continentalness.Max {
b.Continentalness.Max = p.Continentalness.Max
}
if p.Erosion.Min < b.Erosion.Min {
b.Erosion.Min = p.Erosion.Min
}
if p.Erosion.Max > b.Erosion.Max {
b.Erosion.Max = p.Erosion.Max
}
if p.Depth.Min < b.Depth.Min {
b.Depth.Min = p.Depth.Min
}
if p.Depth.Max > b.Depth.Max {
b.Depth.Max = p.Depth.Max
}
if p.Weirdness.Min < b.Weirdness.Min {
b.Weirdness.Min = p.Weirdness.Min
}
if p.Weirdness.Max > b.Weirdness.Max {
b.Weirdness.Max = p.Weirdness.Max
}
if p.Offset < b.Offset {
b.Offset = p.Offset
}
}
return b
}
func (n *bvhNode) lowerBound(q point6) int64 {
return distanceToRange(q.t, n.bounds.Temperature) +
distanceToRange(q.h, n.bounds.Humidity) +
distanceToRange(q.c, n.bounds.Continentalness) +
distanceToRange(q.e, n.bounds.Erosion) +
distanceToRange(q.d, n.bounds.Depth) +
distanceToRange(q.w, n.bounds.Weirdness) +
n.bounds.Offset
}
func (n *bvhNode) lookup(q point6, points []ParameterPoint, best *result) {
if n.lowerBound(q) >= best.dist {
return
}
if n.leaf != nil {
for _, idx := range n.leaf {
p := points[idx]
d := q.dist(p)
if d < best.dist {
best.dist = d
best.biome = p.Biome
}
}
return
}
// Visit the child with the smaller lower bound first for better pruning.
lbL := n.left.lowerBound(q)
lbR := n.right.lowerBound(q)
if lbL < lbR {
n.left.lookup(q, points, best)
n.right.lookup(q, points, best)
} else {
n.right.lookup(q, points, best)
n.left.lookup(q, points, best)
}
}
func quickSelectByAxis(points []ParameterPoint, indices []int, axis, k int) {
if len(indices) <= 1 {
return
}
for {
pivot := points[indices[len(indices)/2]]
pivotVal := axisCenter(pivot, axis)
left, right := 0, len(indices)-1
for left <= right {
for axisCenter(points[indices[left]], axis) < pivotVal {
left++
}
for axisCenter(points[indices[right]], axis) > pivotVal {
right--
}
if left <= right {
indices[left], indices[right] = indices[right], indices[left]
left++
right--
}
}
if k <= right {
indices = indices[:right+1]
} else if k >= left {
indices = indices[left:]
k -= left
} else {
return
}
}
}
func axisCenter(p ParameterPoint, axis int) int64 {
switch axis {
case 0:
return (p.Temperature.Min + p.Temperature.Max) / 2
case 1:
return (p.Humidity.Min + p.Humidity.Max) / 2
case 2:
return (p.Continentalness.Min + p.Continentalness.Max) / 2
case 3:
return (p.Erosion.Min + p.Erosion.Max) / 2
case 4:
return (p.Depth.Min + p.Depth.Max) / 2
case 5:
return (p.Weirdness.Min + p.Weirdness.Max) / 2
}
return 0
}
+42
View File
@@ -1,6 +1,7 @@
package data
import (
"math"
"testing"
)
@@ -32,3 +33,44 @@ func TestMultiNoiseLookup_NetherOrigin(t *testing.T) {
t.Errorf("nether origin = %s, want minecraft:nether_wastes", biome)
}
}
func TestMultiNoiseLookup_BVHMatchesBruteForce(t *testing.T) {
data, err := LoadMultiNoise("1.21.11")
if err != nil {
t.Fatalf("LoadMultiNoise: %v", err)
}
brute := func(preset string, q point6) (string, int64) {
best := result{dist: int64(math.MaxInt64)}
for _, p := range data.Presets[preset] {
d := q.dist(p)
if d < best.dist {
best.dist = d
best.biome = p.Biome
}
}
return best.biome, best.dist
}
presets := []string{MultiNoiseOverworld, MultiNoiseNether}
for _, preset := range presets {
for i := 0; i < 1000; i++ {
q := point6{
t: int64(i%20000 - 10000),
h: int64((i*7)%20000 - 10000),
c: int64((i*13)%20000 - 10000),
e: int64((i*17)%20000 - 10000),
d: int64((i*3)%20000 - 10000),
w: int64((i*11)%20000 - 10000),
}
wantBiome, wantDist := brute(preset, q)
gotBiome, err := data.Lookup(preset, q.t, q.h, q.c, q.e, q.d, q.w)
if err != nil {
t.Fatalf("Lookup(%s): %v", preset, err)
}
if gotBiome != wantBiome {
t.Errorf("preset=%s query=%+v got=%s want=%s dist=%d", preset, q, gotBiome, wantBiome, wantDist)
}
}
}
}