made lake placment more dynamic
This commit is contained in:
@@ -72,26 +72,6 @@ func GenerateLakes(width, height, numLakes int, lakeSizeLower, lakeSizeUpper flo
|
|||||||
var allLakes [][]image.Point
|
var allLakes [][]image.Point
|
||||||
randSrc := rand.New(rand.NewSource(seed))
|
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)
|
totalArea := float64(width * height)
|
||||||
noiseGen := opensimplex.New(seed)
|
noiseGen := opensimplex.New(seed)
|
||||||
|
|
||||||
@@ -101,13 +81,20 @@ func GenerateLakes(width, height, numLakes int, lakeSizeLower, lakeSizeUpper flo
|
|||||||
angle float64
|
angle float64
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate each lake
|
type lakeData struct {
|
||||||
for i := range numLakes {
|
blobs []lakeBlob
|
||||||
if i >= len(chunkIndices) {
|
primaryRadius float64
|
||||||
break
|
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 {
|
getRadius := func() float64 {
|
||||||
s := lakeSizeLower
|
s := lakeSizeLower
|
||||||
@@ -122,16 +109,13 @@ func GenerateLakes(width, height, numLakes int, lakeSizeLower, lakeSizeUpper flo
|
|||||||
return math.Sqrt(pixels / math.Pi)
|
return math.Sqrt(pixels / math.Pi)
|
||||||
}
|
}
|
||||||
|
|
||||||
var blobs []lakeBlob
|
|
||||||
var primaryRadius float64
|
|
||||||
|
|
||||||
switch lakeShape {
|
switch lakeShape {
|
||||||
case "oval":
|
case "oval":
|
||||||
r1 := getRadius()
|
r1 := getRadius()
|
||||||
r2 := getRadius()
|
r2 := getRadius()
|
||||||
angle := randSrc.Float64() * math.Pi * 2
|
angle := randSrc.Float64() * math.Pi * 2
|
||||||
blobs = append(blobs, lakeBlob{0, 0, r1, r2, angle})
|
l.blobs = append(l.blobs, lakeBlob{0, 0, r1, r2, angle})
|
||||||
primaryRadius = (r1 + r2) / 2
|
l.primaryRadius = (r1 + r2) / 2
|
||||||
case "procedural":
|
case "procedural":
|
||||||
complexity := 2 + randSrc.Intn(3) // 2 to 4 blobs
|
complexity := 2 + randSrc.Intn(3) // 2 to 4 blobs
|
||||||
r1 := getRadius()
|
r1 := getRadius()
|
||||||
@@ -140,11 +124,11 @@ func GenerateLakes(width, height, numLakes int, lakeSizeLower, lakeSizeUpper flo
|
|||||||
r2 = getRadius()
|
r2 = getRadius()
|
||||||
}
|
}
|
||||||
angle := randSrc.Float64() * math.Pi * 2
|
angle := randSrc.Float64() * math.Pi * 2
|
||||||
blobs = append(blobs, lakeBlob{0, 0, r1, r2, angle})
|
l.blobs = append(l.blobs, lakeBlob{0, 0, r1, r2, angle})
|
||||||
primaryRadius = (r1 + r2) / 2
|
l.primaryRadius = (r1 + r2) / 2
|
||||||
|
|
||||||
for k := 1; k < complexity; k++ {
|
for k := 1; k < complexity; k++ {
|
||||||
parent := blobs[randSrc.Intn(len(blobs))]
|
parent := l.blobs[randSrc.Intn(len(l.blobs))]
|
||||||
subR1 := getRadius() * 0.7
|
subR1 := getRadius() * 0.7
|
||||||
subR2 := subR1
|
subR2 := subR1
|
||||||
if randSrc.Float64() > 0.5 {
|
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
|
dist := (parent.a + subR1) * 0.6 // Overlap
|
||||||
newX := parent.dx + math.Cos(dir)*dist
|
newX := parent.dx + math.Cos(dir)*dist
|
||||||
newY := parent.dy + math.Sin(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"
|
default: // "circle"
|
||||||
r := getRadius()
|
r := getRadius()
|
||||||
blobs = append(blobs, lakeBlob{0, 0, r, r, 0})
|
l.blobs = append(l.blobs, lakeBlob{0, 0, r, r, 0})
|
||||||
primaryRadius = r
|
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
|
estimatedArea := 0.0
|
||||||
for _, b := range blobs {
|
maxBlobDist := 0.0
|
||||||
|
for _, b := range l.blobs {
|
||||||
estimatedArea += math.Pi * b.a * b.b
|
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 {
|
if len(l.blobs) > 1 {
|
||||||
estimatedArea *= 0.8 // Heuristic for overlap reduction
|
estimatedArea *= 0.8
|
||||||
}
|
}
|
||||||
targetPixelsPerLake := int(estimatedArea)
|
l.targetPixels = int(estimatedArea)
|
||||||
if targetPixelsPerLake <= 0 {
|
if l.targetPixels <= 0 {
|
||||||
targetPixelsPerLake = 1
|
l.targetPixels = 1
|
||||||
|
}
|
||||||
|
l.boundingRadius = maxBlobDist * 1.2
|
||||||
|
lakesToPlace[i] = l
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
chunkIndex := chunkIndices[i]
|
// Fallback: Orbit existing lakes (Tangent placement)
|
||||||
chunkGridX := chunkIndex % gridDim
|
if !placed && len(placedLakes) > 0 {
|
||||||
chunkGridY := chunkIndex / gridDim
|
indices := randSrc.Perm(len(placedLakes))
|
||||||
|
for _, idx := range indices {
|
||||||
|
targetLake := placedLakes[idx]
|
||||||
|
targetDist := targetLake.boundingRadius + l.boundingRadius // Touching
|
||||||
|
|
||||||
chunkRect := image.Rect(
|
const angleSteps = 36
|
||||||
chunkGridX*chunkWidth,
|
startAngle := randSrc.Float64() * 2 * math.Pi
|
||||||
chunkGridY*chunkHeight,
|
|
||||||
(chunkGridX+1)*chunkWidth,
|
|
||||||
(chunkGridY+1)*chunkHeight,
|
|
||||||
)
|
|
||||||
|
|
||||||
// Initialize priority queue growth algorithm
|
for k := 0; k < angleSteps; k++ {
|
||||||
pq := &priorityQueue{}
|
angle := startAngle + (float64(k)/float64(angleSteps))*2*math.Pi
|
||||||
heap.Init(pq)
|
cx := int(float64(targetLake.center.X) + math.Cos(angle)*targetDist)
|
||||||
visited := make(map[image.Point]bool)
|
cy := int(float64(targetLake.center.Y) + math.Sin(angle)*targetDist)
|
||||||
|
|
||||||
// Start growth at chunk center
|
margin := int(l.boundingRadius / 2)
|
||||||
startPt := image.Point{
|
if cx < -margin || cx >= width+margin || cy < -margin || cy >= height+margin {
|
||||||
X: chunkRect.Min.X + chunkWidth/2,
|
continue
|
||||||
Y: chunkRect.Min.Y + chunkHeight/2,
|
}
|
||||||
|
|
||||||
|
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 !startPt.In(chunkRect) {
|
|
||||||
|
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
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// Setup noise generation for natural lake shapes
|
valid := true
|
||||||
seedX := randSrc.Float64() * 10000.0
|
for j, other := range placedLakes {
|
||||||
seedY := randSrc.Float64() * 10000.0
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
noiseFreq := 0.01 + (0.2 / (primaryRadius + 1.0))
|
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
|
||||||
|
|
||||||
|
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 {
|
getScore := func(pt image.Point) float64 {
|
||||||
dxGlobal := float64(pt.X - startPt.X)
|
dxGlobal := float64(pt.X - l.center.X)
|
||||||
dyGlobal := float64(pt.Y - startPt.Y)
|
dyGlobal := float64(pt.Y - l.center.Y)
|
||||||
|
|
||||||
minNormalizedDist := 1e9
|
minNormalizedDist := 1e9
|
||||||
|
|
||||||
for _, b := range blobs {
|
for _, b := range l.blobs {
|
||||||
bdx := dxGlobal - b.dx
|
bdx := dxGlobal - b.dx
|
||||||
bdy := dyGlobal - b.dy
|
bdy := dyGlobal - b.dy
|
||||||
|
|
||||||
@@ -241,19 +365,22 @@ func GenerateLakes(width, height, numLakes int, lakeSizeLower, lakeSizeUpper flo
|
|||||||
return -distPenalty
|
return -distPenalty
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pq := &priorityQueue{}
|
||||||
|
heap.Init(pq)
|
||||||
heap.Push(pq, &lakePixel{point: startPt, score: getScore(startPt)})
|
heap.Push(pq, &lakePixel{point: startPt, score: getScore(startPt)})
|
||||||
visited[startPt] = true
|
globalVisited[startPt] = true
|
||||||
|
|
||||||
// Grow lake to target size
|
|
||||||
lakeCount := 0
|
lakeCount := 0
|
||||||
for pq.Len() > 0 && lakeCount < targetPixelsPerLake {
|
for pq.Len() > 0 && lakeCount < l.targetPixels {
|
||||||
current := heap.Pop(pq).(*lakePixel)
|
current := heap.Pop(pq).(*lakePixel)
|
||||||
|
|
||||||
canvas.Set(current.point.X, current.point.Y, color.RGBA{R: 0, G: 0, B: 255, A: 255})
|
// Only draw if on canvas
|
||||||
currentLake = append(currentLake, current.point)
|
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++
|
lakeCount++
|
||||||
|
|
||||||
// Add neighboring pixels to growth queue
|
|
||||||
for dy := -1; dy <= 1; dy++ {
|
for dy := -1; dy <= 1; dy++ {
|
||||||
for dx := -1; dx <= 1; dx++ {
|
for dx := -1; dx <= 1; dx++ {
|
||||||
if dx == 0 && dy == 0 {
|
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}
|
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
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
visited[neighbor] = true
|
globalVisited[neighbor] = true
|
||||||
heap.Push(pq, &lakePixel{
|
heap.Push(pq, &lakePixel{
|
||||||
point: neighbor,
|
point: neighbor,
|
||||||
score: getScore(neighbor),
|
score: getScore(neighbor),
|
||||||
|
|||||||
Reference in New Issue
Block a user