made lake placment more dynamic

This commit is contained in:
Grimsace
2026-02-19 11:38:37 -06:00
parent 7a4cd749c1
commit baf5a14c9f
+209 -77
View File
@@ -72,26 +72,6 @@ func GenerateLakes(width, height, numLakes int, lakeSizeLower, lakeSizeUpper flo
var allLakes [][]image.Point
randSrc := rand.New(rand.NewSource(seed))
// Divide the image into a grid to distribute lakes evenly
gridDim := int(math.Ceil(math.Sqrt(float64(numLakes))))
if gridDim == 0 {
return canvas, nil
}
chunkWidth := width / gridDim
chunkHeight := height / gridDim
if chunkWidth == 0 || chunkHeight == 0 {
return canvas, nil
}
// Shuffle chunk indices for random lake placement
chunkIndices := make([]int, gridDim*gridDim)
for i := range chunkIndices {
chunkIndices[i] = i
}
randSrc.Shuffle(len(chunkIndices), func(i, j int) {
chunkIndices[i], chunkIndices[j] = chunkIndices[j], chunkIndices[i]
})
totalArea := float64(width * height)
noiseGen := opensimplex.New(seed)
@@ -101,13 +81,20 @@ func GenerateLakes(width, height, numLakes int, lakeSizeLower, lakeSizeUpper flo
angle float64
}
// Generate each lake
for i := range numLakes {
if i >= len(chunkIndices) {
break
type lakeData struct {
blobs []lakeBlob
primaryRadius float64
boundingRadius float64
targetPixels int
center image.Point
placed bool
}
var currentLake []image.Point
lakesToPlace := make([]*lakeData, numLakes)
// Phase 1: Generate parameters for all lakes
for i := range numLakes {
l := &lakeData{}
getRadius := func() float64 {
s := lakeSizeLower
@@ -122,16 +109,13 @@ func GenerateLakes(width, height, numLakes int, lakeSizeLower, lakeSizeUpper flo
return math.Sqrt(pixels / math.Pi)
}
var blobs []lakeBlob
var primaryRadius float64
switch lakeShape {
case "oval":
r1 := getRadius()
r2 := getRadius()
angle := randSrc.Float64() * math.Pi * 2
blobs = append(blobs, lakeBlob{0, 0, r1, r2, angle})
primaryRadius = (r1 + r2) / 2
l.blobs = append(l.blobs, lakeBlob{0, 0, r1, r2, angle})
l.primaryRadius = (r1 + r2) / 2
case "procedural":
complexity := 2 + randSrc.Intn(3) // 2 to 4 blobs
r1 := getRadius()
@@ -140,11 +124,11 @@ func GenerateLakes(width, height, numLakes int, lakeSizeLower, lakeSizeUpper flo
r2 = getRadius()
}
angle := randSrc.Float64() * math.Pi * 2
blobs = append(blobs, lakeBlob{0, 0, r1, r2, angle})
primaryRadius = (r1 + r2) / 2
l.blobs = append(l.blobs, lakeBlob{0, 0, r1, r2, angle})
l.primaryRadius = (r1 + r2) / 2
for k := 1; k < complexity; k++ {
parent := blobs[randSrc.Intn(len(blobs))]
parent := l.blobs[randSrc.Intn(len(l.blobs))]
subR1 := getRadius() * 0.7
subR2 := subR1
if randSrc.Float64() > 0.5 {
@@ -155,67 +139,207 @@ func GenerateLakes(width, height, numLakes int, lakeSizeLower, lakeSizeUpper flo
dist := (parent.a + subR1) * 0.6 // Overlap
newX := parent.dx + math.Cos(dir)*dist
newY := parent.dy + math.Sin(dir)*dist
blobs = append(blobs, lakeBlob{newX, newY, subR1, subR2, subAngle})
l.blobs = append(l.blobs, lakeBlob{newX, newY, subR1, subR2, subAngle})
}
default: // "circle"
r := getRadius()
blobs = append(blobs, lakeBlob{0, 0, r, r, 0})
primaryRadius = r
l.blobs = append(l.blobs, lakeBlob{0, 0, r, r, 0})
l.primaryRadius = r
}
// Calculate estimated target pixels based on blobs (rough approximation)
// Since we grow until count is reached, we can just sum areas and discount for overlap
estimatedArea := 0.0
for _, b := range blobs {
maxBlobDist := 0.0
for _, b := range l.blobs {
estimatedArea += math.Pi * b.a * b.b
dist := math.Sqrt(b.dx*b.dx+b.dy*b.dy) + math.Max(b.a, b.b)
if dist > maxBlobDist {
maxBlobDist = dist
}
if len(blobs) > 1 {
estimatedArea *= 0.8 // Heuristic for overlap reduction
}
targetPixelsPerLake := int(estimatedArea)
if targetPixelsPerLake <= 0 {
targetPixelsPerLake = 1
if len(l.blobs) > 1 {
estimatedArea *= 0.8
}
l.targetPixels = int(estimatedArea)
if l.targetPixels <= 0 {
l.targetPixels = 1
}
l.boundingRadius = maxBlobDist * 1.2
lakesToPlace[i] = l
}
chunkIndex := chunkIndices[i]
chunkGridX := chunkIndex % gridDim
chunkGridY := chunkIndex / gridDim
// Phase 2: Initial Placement (Tight Packing)
var placedLakes []*lakeData
for i, l := range lakesToPlace {
placed := false
// Try random placement first
for attempt := 0; attempt < 100; attempt++ {
cx := randSrc.Intn(width)
cy := randSrc.Intn(height)
chunkRect := image.Rect(
chunkGridX*chunkWidth,
chunkGridY*chunkHeight,
(chunkGridX+1)*chunkWidth,
(chunkGridY+1)*chunkHeight,
)
// Initialize priority queue growth algorithm
pq := &priorityQueue{}
heap.Init(pq)
visited := make(map[image.Point]bool)
// Start growth at chunk center
startPt := image.Point{
X: chunkRect.Min.X + chunkWidth/2,
Y: chunkRect.Min.Y + chunkHeight/2,
}
if !startPt.In(chunkRect) {
// Relaxed boundary check: center can be anywhere, but let's keep it somewhat reasonable
// Allow center to be outside by radius/2
margin := int(l.boundingRadius / 2)
if cx < -margin || cx >= width+margin || cy < -margin || cy >= height+margin {
continue
}
// Setup noise generation for natural lake shapes
overlap := false
for _, other := range placedLakes {
dx := float64(cx - other.center.X)
dy := float64(cy - other.center.Y)
dist := math.Sqrt(dx*dx + dy*dy)
if dist < (l.boundingRadius + other.boundingRadius) {
overlap = true
break
}
}
if !overlap {
l.center = image.Point{X: cx, Y: cy}
l.placed = true
placed = true
break
}
}
// Fallback: Orbit existing lakes (Tangent placement)
if !placed && len(placedLakes) > 0 {
indices := randSrc.Perm(len(placedLakes))
for _, idx := range indices {
targetLake := placedLakes[idx]
targetDist := targetLake.boundingRadius + l.boundingRadius // Touching
const angleSteps = 36
startAngle := randSrc.Float64() * 2 * math.Pi
for k := 0; k < angleSteps; k++ {
angle := startAngle + (float64(k)/float64(angleSteps))*2*math.Pi
cx := int(float64(targetLake.center.X) + math.Cos(angle)*targetDist)
cy := int(float64(targetLake.center.Y) + math.Sin(angle)*targetDist)
margin := int(l.boundingRadius / 2)
if cx < -margin || cx >= width+margin || cy < -margin || cy >= height+margin {
continue
}
overlap := false
for _, other := range placedLakes {
dx := float64(cx - other.center.X)
dy := float64(cy - other.center.Y)
dist := math.Sqrt(dx*dx + dy*dy)
if dist < (l.boundingRadius + other.boundingRadius) { // Touching check
overlap = true
break
}
}
if !overlap {
l.center = image.Point{X: cx, Y: cy}
l.placed = true
placed = true
break
}
}
if placed {
break
}
}
}
if placed {
placedLakes = append(placedLakes, l)
} else {
// Discard lake if it really can't fit
lakesToPlace[i] = nil
}
}
// Phase 3: Scattering (Relaxation)
avgDim := float64(width+height) / 2.0
minGap := avgDim * 0.01
iterations := len(placedLakes) * 100
for k := 0; k < iterations; k++ {
if len(placedLakes) == 0 {
break
}
idx := randSrc.Intn(len(placedLakes))
l := placedLakes[idx]
// Propose new random position
cx := randSrc.Intn(width)
cy := randSrc.Intn(height)
margin := int(l.boundingRadius / 2)
if cx < -margin || cx >= width+margin || cy < -margin || cy >= height+margin {
continue
}
valid := true
for j, other := range placedLakes {
if idx == j {
continue
}
dx := float64(cx - other.center.X)
dy := float64(cy - other.center.Y)
dist := math.Sqrt(dx*dx + dy*dy)
if dist < (l.boundingRadius + other.boundingRadius + minGap) {
valid = false
break
}
}
if valid {
l.center = image.Point{X: cx, Y: cy}
}
}
// Phase 4: Grow lakes at final positions
globalVisited := make(map[image.Point]bool)
seedX := randSrc.Float64() * 10000.0
seedY := randSrc.Float64() * 10000.0
noiseFreq := 0.01 + (0.2 / (primaryRadius + 1.0))
for _, l := range placedLakes {
if l == nil || !l.placed {
continue
}
var currentLake []image.Point
startPt := l.center
// Check if start point is within strict bounds for drawing initiation
if !startPt.In(image.Rect(0, 0, width, height)) {
// Try to find a point within the lake radius that is on the map
found := false
for r := 0; r < int(l.boundingRadius); r++ {
for angle := 0.0; angle < 2*math.Pi; angle += 0.5 {
nx := startPt.X + int(float64(r)*math.Cos(angle))
ny := startPt.Y + int(float64(r)*math.Sin(angle))
pt := image.Point{nx, ny}
if pt.In(image.Rect(0, 0, width, height)) {
startPt = pt
found = true
break
}
}
if found {
break
}
}
if !found {
continue // Lake is completely off-screen or unplaceable
}
}
noiseFreq := 0.01 + (0.2 / (l.primaryRadius + 1.0))
// Score function determines which pixels to add to lake
getScore := func(pt image.Point) float64 {
dxGlobal := float64(pt.X - startPt.X)
dyGlobal := float64(pt.Y - startPt.Y)
dxGlobal := float64(pt.X - l.center.X)
dyGlobal := float64(pt.Y - l.center.Y)
minNormalizedDist := 1e9
for _, b := range blobs {
for _, b := range l.blobs {
bdx := dxGlobal - b.dx
bdy := dyGlobal - b.dy
@@ -241,19 +365,22 @@ func GenerateLakes(width, height, numLakes int, lakeSizeLower, lakeSizeUpper flo
return -distPenalty
}
pq := &priorityQueue{}
heap.Init(pq)
heap.Push(pq, &lakePixel{point: startPt, score: getScore(startPt)})
visited[startPt] = true
globalVisited[startPt] = true
// Grow lake to target size
lakeCount := 0
for pq.Len() > 0 && lakeCount < targetPixelsPerLake {
for pq.Len() > 0 && lakeCount < l.targetPixels {
current := heap.Pop(pq).(*lakePixel)
// Only draw if on canvas
if current.point.In(image.Rect(0, 0, width, height)) {
canvas.Set(current.point.X, current.point.Y, color.RGBA{R: 0, G: 0, B: 255, A: 255})
currentLake = append(currentLake, current.point)
}
lakeCount++
// Add neighboring pixels to growth queue
for dy := -1; dy <= 1; dy++ {
for dx := -1; dx <= 1; dx++ {
if dx == 0 && dy == 0 {
@@ -261,11 +388,16 @@ func GenerateLakes(width, height, numLakes int, lakeSizeLower, lakeSizeUpper flo
}
neighbor := image.Point{X: current.point.X + dx, Y: current.point.Y + dy}
if !neighbor.In(chunkRect) || visited[neighbor] {
if globalVisited[neighbor] {
continue
}
// Allow growth slightly off-screen to ensure shape consistency, but don't track too far
if neighbor.X < -int(l.boundingRadius) || neighbor.X >= width+int(l.boundingRadius) ||
neighbor.Y < -int(l.boundingRadius) || neighbor.Y >= height+int(l.boundingRadius) {
continue
}
visited[neighbor] = true
globalVisited[neighbor] = true
heap.Push(pq, &lakePixel{
point: neighbor,
score: getScore(neighbor),