diff --git a/buildings.go b/buildings.go index 24fc471..1a358f2 100644 --- a/buildings.go +++ b/buildings.go @@ -16,11 +16,9 @@ func GenerateBuildings(img *image.RGBA, width, height int, settings *Settings, r return nil, nil } - // Initialize random number generator randSrc := rand.New(rand.NewSource(seed)) buildingColor := color.RGBA{R: 128, G: 128, B: 128, A: 255} // Gray color for buildings - // Create lookup maps for water and road pixels for efficient collision detection isWater := make(map[image.Point]bool) for _, p := range allWaterPixels { isWater[p] = true @@ -31,13 +29,11 @@ func GenerateBuildings(img *image.RGBA, width, height int, settings *Settings, r isRoad[p] = true } - // Initialize building data structures isBuilding := make(map[image.Point]bool) var buildings [][]image.Point var allBuildingPixels []image.Point var anchorPoints []image.Point - // Determine anchor points for building placement if len(roadPixels) > 0 { anchorPoints = roadPixels } else { @@ -64,7 +60,6 @@ func GenerateBuildings(img *image.RGBA, width, height int, settings *Settings, r return anchorPoints[i].X < anchorPoints[j].X }) - // Collect all land points for random placement landPoints := make([]image.Point, 0, width*height) for y := 0; y < height; y++ { for x := 0; x < width; x++ { @@ -86,10 +81,8 @@ func GenerateBuildings(img *image.RGBA, width, height int, settings *Settings, r // Select an anchor point for the new building var anchor image.Point if randSrc.Float64() > settings.BuildingDistribution/100.0 { - // Place near roads or other existing features anchor = anchorPoints[randSrc.Intn(len(anchorPoints))] } else { - // Place randomly on any available land if len(landPoints) == 0 { continue // No land to place buildings on } @@ -152,7 +145,6 @@ func GenerateBuildings(img *image.RGBA, width, height int, settings *Settings, r // getProceduralBuildingPixels generates a complex building by connecting multiple shapes. func getProceduralBuildingPixels(center image.Point, size float64, settings *Settings, isWater, isRoad, isBuilding map[image.Point]bool, width, height int, randSrc *rand.Rand) ([]image.Point, bool) { - // Determine complexity complexity := settings.MinBuildingComplexity if settings.BuildingComplexityRatio > randSrc.Float64()*100 { complexity = settings.MinBuildingComplexity + randSrc.Intn(settings.MaxBuildingComplexity-settings.MinBuildingComplexity+1) @@ -177,7 +169,6 @@ func getProceduralBuildingPixels(center image.Point, size float64, settings *Set newCenter = center buildingCenter = center } else { - // Place subsequent components near existing ones prevShape := shapeDescriptions[randSrc.Intn(len(shapeDescriptions))] angle := randSrc.Float64() * 2 * math.Pi dist := componentSize * (0.25 + randSrc.Float64()*0.5) // Overlap between 25% and 75% @@ -189,7 +180,6 @@ func getProceduralBuildingPixels(center image.Point, size float64, settings *Set shapeDescriptions = append(shapeDescriptions, shapeDescription{shape, newCenter, componentSize}) } - // Find the bounding box of the unscaled building var minX, minY, maxX, maxY int for i, sd := range shapeDescriptions { halfSize := int(sd.size / 2) @@ -252,7 +242,6 @@ func scalePixels(pixels []image.Point, finalSize float64) []image.Point { return pixels } - // Find the bounding box of the pixels minX, minY := pixels[0].X, pixels[0].Y maxX, maxY := pixels[0].X, pixels[0].Y for _, p := range pixels { @@ -274,7 +263,6 @@ func scalePixels(pixels []image.Point, finalSize float64) []image.Point { currentWidth := float64(maxX - minX) currentHeight := float64(maxY - minY) - // Determine the scaling factor scale := finalSize / math.Max(currentWidth, currentHeight) // Calculate the center of the bounding box @@ -351,7 +339,6 @@ func getComponentPixels(center image.Point, size float64, shape string, randSrc // chooseShape selects a building shape based on the provided ratios. func chooseShape(randSrc *rand.Rand, ratios map[string]float64) string { - // Create a slice of shapes and their cumulative weights var shapes []string var weights []float64 var cumulativeWeight float64 @@ -364,7 +351,6 @@ func chooseShape(randSrc *rand.Rand, ratios map[string]float64) string { // Generate a random number between 0 and the total weight randNum := randSrc.Float64() * cumulativeWeight - // Find the shape corresponding to the random number for i, weight := range weights { if randNum < weight { return shapes[i] @@ -407,7 +393,6 @@ func getBuildingPixels(center image.Point, size float64, shape string, isWater, } } case "rectangles": - // Create rectangles with varied aspect ratios longSide := size shortSide := randSrc.Float64()*(size-float64(halfSize)) + float64(halfSize) var w, h int @@ -418,7 +403,6 @@ func getBuildingPixels(center image.Point, size float64, shape string, isWater, } halfW, halfH := w/2, h/2 - // Check for collisions and gather pixels for y := center.Y - halfH; y <= center.Y+halfH; y++ { for x := center.X - halfW; x <= center.X+halfW; x++ { p := image.Point{X: x, Y: y} @@ -453,7 +437,6 @@ func FlattenBuildingAreas(heightMap *image.RGBA, buildings [][]image.Point, widt return heightMap } - // Create a copy of the heightmap to avoid modifying the original during processing. newHeightMap := image.NewRGBA(heightMap.Bounds()) copy(newHeightMap.Pix, heightMap.Pix) @@ -478,7 +461,6 @@ func FlattenBuildingAreas(heightMap *image.RGBA, buildings [][]image.Point, widt newHeightMap.Set(p.X, p.Y, avgColor) } - // Create a buffer around the building. buffer := make([]image.Point, 0) for _, p := range building { for y := p.Y - 5; y <= p.Y+5; y++ { diff --git a/custom_widgets.go b/custom_widgets.go index 9d1289a..d90c4cc 100644 --- a/custom_widgets.go +++ b/custom_widgets.go @@ -12,7 +12,7 @@ import ( "fyne.io/fyne/v2/widget" ) -// numericInputSlider is a custom widget that combines a slider and a text entry for numeric input. +// numericInputSlider combines a slider and text entry for numeric input type numericInputSlider struct { widget.BaseWidget value binding.Float @@ -52,7 +52,7 @@ func (r *numericInputSliderRenderer) Refresh() { func (r *numericInputSliderRenderer) Destroy() {} -// newNumericInputSlider creates a new numericInputSlider widget. +// newNumericInputSlider creates a new numericInputSlider widget func newNumericInputSlider(min, max float64, initialValue float64, format string, labelText string) *numericInputSlider { s := &numericInputSlider{ min: min, @@ -80,7 +80,7 @@ func newNumericInputSlider(min, max float64, initialValue float64, format string return s } -// validate checks the text entry for valid numeric input within the defined range. +// validate checks text entry for valid numeric input within the defined range func (s *numericInputSlider) validate(text string, onError func(bool)) { text = strings.TrimSuffix(text, "px") text = strings.TrimSuffix(text, "%") @@ -104,7 +104,7 @@ func (s *numericInputSlider) validate(text string, onError func(bool)) { s.value.Set(val) } -// CreateRenderer is a method required by the Fyne toolkit to render the widget. +// CreateRenderer renders the widget as required by Fyne toolkit func (s *numericInputSlider) CreateRenderer() fyne.WidgetRenderer { r := &numericInputSliderRenderer{ slider: s, diff --git a/rivers.go b/rivers.go index 52ce46b..9bfa518 100644 --- a/rivers.go +++ b/rivers.go @@ -11,81 +11,35 @@ import ( "github.com/ojrac/opensimplex-go" ) -// rivers.go -// -// New river roughening implementation that uses the heightmap to clip river edges, -// occasionally creates islands, and is designed to be efficient and multithreadable. -// -// This file exposes one main function intended to be called from the river generation -// pipeline in place of per-pixel painting: `RasterizeAndRoughenRiver`. It: -// - rasterizes the river centerline into a local mask (bounding box) -// - computes a fast distance field (chamfer approximation) from the centerline -// - evaluates a heightmap-aware stochastic rule to remove/add edge pixels to roughen -// - occasionally grows islands inside the river -// - writes final water pixels back to the provided canvas and updates the provided isWater map -// -// Usage (conceptual): -// addedPixels := RasterizeAndRoughenRiver(canvas, path, riverWidthPx, heightmap, isWater, seed) -// -// NOTE: Because the project already contained a `drawCircle` helper, this new pipeline -// is implemented as standalone routines in this file. To use it, replace the existing -// per-circle painting logic in `GenerateRivers` with a call to `RasterizeAndRoughenRiver`. -// -// The parameters below were chosen conservatively; tweak them to taste. - type riverParams struct { - EdgeBandRatio float64 // fraction of river radius used for roughening band (e.g. 0.6) - RoughnessStrength float64 // 0..1 how aggressive clipping is at the edge - IslandAttemptProb float64 // chance per-river to attempt islands - IslandSeedChance float64 // chance per-water-pixel to become an island seed candidate - MinIslandSize int // minimum island pixel count - MaxIslandSize int // maximum island pixel count - WaterLevelBias float64 // baseline water level in normalized height units [0..1]; small bias subtracted to favor water - NoiseFrequency float64 // frequency for simplex noise - KeepInnerFraction float64 // fraction of inner radius always kept as channel (0..1) - MaxWorkers int // concurrency limit (0 means runtime.NumCPU()) - MinWidthPx float64 // minimum river width in pixels (for sin wave amplitude calculation) - MaxWidthPx float64 // maximum river width in pixels (for sin wave amplitude calculation) + EdgeBandRatio float64 + RoughnessStrength float64 + IslandAttemptProb float64 + IslandSeedChance float64 + MinIslandSize int + MaxIslandSize int + WaterLevelBias float64 + NoiseFrequency float64 + KeepInnerFraction float64 + MaxWorkers int + MinWidthPx float64 + MaxWidthPx float64 } -// computeSinWaveEdgeOffset computes the radial offset for river edge roughening -// using dual sine waves. The larger wave has amplitude based on the difference -// between max and min river widths, and the smaller wave is a quarter of that amplitude. -// This creates realistic undulating river banks with both large and small-scale variations. +// computeSinWaveEdgeOffset computes dual sine wave edge roughening for realistic river banks func computeSinWaveEdgeOffset(absX, absY int, largeAmplitude, smallAmplitude float64) float64 { - // Use position to create phase for the sine waves - // Position phase creates variation as we move through the image positionPhase := float64(absX)*0.008 + float64(absY)*0.012 - - // Large wave: slower frequency for major width variations along the bank largeWave := math.Sin(positionPhase) * largeAmplitude - - // Small wave: faster frequency for subtle and natural bank details smallWave := math.Sin(positionPhase*3.5) * smallAmplitude - - // Return combined offset return largeWave + smallWave } -// RasterizeAndRoughenRiver rasterizes a river path, roughens edges using the heightmap and dual sin waves, -// optionally creates islands, paints the final water into `canvas`, and marks pixels in `isWater`. -// It returns a slice of image.Point containing all newly added water pixels for this river. -// -// Parameters: -// - canvas: destination image (will be modified) -// - path: ordered centerline points for the river -// - riverWidthPx: nominal width in pixels -// - heightmap: heightmap image used to guide roughening (expects 0..1 grayscale via RGBA() conversion) -// - isWater: map used to record already-water pixels (prevents painting over lakes/rivers). This map will be updated. -// - seed: random seed to make generation deterministic -// - minWidthPx: minimum river width in pixels (used for sin wave amplitude calculation) -// - maxWidthPx: maximum river width in pixels (used for sin wave amplitude calculation) +// RasterizeAndRoughenRiver rasterizes a river path with natural edge roughening and optional islands func RasterizeAndRoughenRiver(canvas *image.RGBA, path []image.Point, riverWidthPx float64, heightmap image.Image, isWater map[image.Point]bool, seed int64, minWidthPx, maxWidthPx float64) []image.Point { if canvas == nil || len(path) == 0 || riverWidthPx <= 0 { return nil } - // Default parameters - tweak as needed params := riverParams{ EdgeBandRatio: 0.6, RoughnessStrength: 0.65, @@ -95,7 +49,7 @@ func RasterizeAndRoughenRiver(canvas *image.RGBA, path []image.Point, riverWidth MaxIslandSize: 800, WaterLevelBias: 0.02, NoiseFrequency: 0.02, - KeepInnerFraction: 0.85, // keep central 85% of radius + KeepInnerFraction: 0.85, MaxWorkers: 0, MinWidthPx: minWidthPx, MaxWidthPx: maxWidthPx, @@ -104,10 +58,8 @@ func RasterizeAndRoughenRiver(canvas *image.RGBA, path []image.Point, riverWidth bounds := canvas.Bounds() imgW, imgH := bounds.Dx(), bounds.Dy() - // Precompute normalized height grid for faster sampling. heightGrid := precomputeHeightGrid(heightmap, imgW, imgH) - // Compute bounding box for path expanded by radius + edge band radius := riverWidthPx / 2.0 edgeBand := radius * params.EdgeBandRatio expand := int(math.Ceil(radius + edgeBand + 2)) @@ -151,11 +103,8 @@ func RasterizeAndRoughenRiver(canvas *image.RGBA, path []image.Point, riverWidth return nil } - // Create base raster mask inside bounding box. - // baseMask[i] == 1 means inside nominal river radius (before roughening). baseMask := make([]uint8, bw*bh) - // Rasterize simple circular stamping for each center point into baseMask radiusSq := radius * radius for _, c := range path { cx := c.X - minX @@ -178,20 +127,15 @@ func RasterizeAndRoughenRiver(canvas *image.RGBA, path []image.Point, riverWidth } } - // Compute distance field (approximate Euclidean) from centerline (distance 0 at pixels inside baseMask) dist := chamferDistanceField(baseMask, bw, bh) - // Prepare noise generator noise := opensimplex.New(seed) noiseFreq := params.NoiseFrequency - // Determine inner keep radius (always keep central channel) innerKeepRadius := radius * params.KeepInnerFraction - // Prepare final mask finalMask := make([]uint8, bw*bh) - // Concurrency setup workers := params.MaxWorkers if workers <= 0 { workers = runtime.NumCPU() @@ -200,18 +144,13 @@ func RasterizeAndRoughenRiver(canvas *image.RGBA, path []image.Point, riverWidth rowsPerWorker := (bh + workers - 1) / workers randBase := rand.New(rand.NewSource(seed)) - // Precompute some weights for the decision formula heightWeight := 2.0 * params.RoughnessStrength distWeight := params.RoughnessStrength noiseWeight := 0.5 * params.RoughnessStrength - // Compute sin wave amplitudes for realistic edge roughening - // Large amplitude is the difference between max and min river widths - // Small amplitude is a quarter of the large amplitude for subtle bank details largeAmplitude := params.MaxWidthPx - params.MinWidthPx smallAmplitude := largeAmplitude / 4.0 - // Evaluate per-pixel decision in parallel for wi := 0; wi < workers; wi++ { startY := wi * rowsPerWorker endY := startY + rowsPerWorker @@ -228,11 +167,8 @@ func RasterizeAndRoughenRiver(canvas *image.RGBA, path []image.Point, riverWidth for y := startY; y < endY; y++ { for x := 0; x < bw; x++ { idx := y*bw + x - // If already inside base mask, candidate for water if baseMask[idx] == 1 { - // If within inner keep radius: keep always d := dist[idx] - // dist is approximate pixels; we compare to innerKeepRadius absX := x + minX absY := y + minY if d <= float32(innerKeepRadius) { @@ -240,12 +176,9 @@ func RasterizeAndRoughenRiver(canvas *image.RGBA, path []image.Point, riverWidth continue } - // Apply sin wave offset for realistic edge roughening sinWaveOffset := computeSinWaveEdgeOffset(absX, absY, largeAmplitude, smallAmplitude) effectiveInnerRadius := innerKeepRadius + sinWaveOffset - // Compute influences - // normalizedDist: 0 at effectiveInnerRadius, 1 at effectiveInnerRadius + edgeBand normDist := float64((float32(d) - float32(effectiveInnerRadius)) / float32(edgeBand)) if normDist < 0 { normDist = 0 @@ -254,18 +187,15 @@ func RasterizeAndRoughenRiver(canvas *image.RGBA, path []image.Point, riverWidth normDist = 1 } - heightVal := sampleHeightGrid(heightGrid, imgW, imgH, absX, absY) // 0..1 - // Apply bias so slightly lower areas favor water + heightVal := sampleHeightGrid(heightGrid, imgW, imgH, absX, absY) heightAdj := float64(heightVal) - params.WaterLevelBias - noiseVal := noise.Eval2(float64(absX)*noiseFreq, float64(absY)*noiseFreq) // -1 .. 1 - noiseNorm := (noiseVal + 1.0) / 2.0 // 0..1 + noiseVal := noise.Eval2(float64(absX)*noiseFreq, float64(absY)*noiseFreq) + noiseNorm := (noiseVal + 1.0) / 2.0 score := distWeight*normDist + heightWeight*heightAdj + noiseWeight*(noiseNorm-0.5) - // Decision threshold: higher score means more likely land. threshold := 0.35 + 0.5*params.RoughnessStrength - // Small stochastic factor to add natural variance if localRand.Float64() < 0.0005 { score += (localRand.Float64() - 0.5) * 0.2 } @@ -282,14 +212,12 @@ func RasterizeAndRoughenRiver(canvas *image.RGBA, path []image.Point, riverWidth } wg.Wait() - // Optionally attempt islands with small probability randForIsland := rand.New(rand.NewSource(seed + 1234567)) tryIslands := randForIsland.Float64() < params.IslandAttemptProb if tryIslands { generateIslandsInMask(finalMask, bw, bh, minX, minY, heightGrid, imgW, imgH, ¶ms, seed+4242) } - // Paint finalMask to canvas and collect pixels (only those not already water) var added []image.Point for y := 0; y < bh; y++ { absY := y + minY @@ -308,17 +236,15 @@ func RasterizeAndRoughenRiver(canvas *image.RGBA, path []image.Point, riverWidth } } - // Small cleanup: remove tiny isolated water pixels (optional - lightweight) removeSpeckles(&finalMask, bw, bh, 2) return added } -// precomputeHeightGrid converts the heightmap to a float32 grid [0..1] sized width*height. +// precomputeHeightGrid converts heightmap to normalized float32 grid func precomputeHeightGrid(hmap image.Image, width, height int) []float32 { out := make([]float32, width*height) if hmap == nil { - // default flat for i := range out { out[i] = 0.5 } @@ -337,7 +263,7 @@ func precomputeHeightGrid(hmap image.Image, width, height int) []float32 { return out } -// sampleHeightGrid safe accessor +// sampleHeightGrid safely samples height at coordinates func sampleHeightGrid(grid []float32, width, height, x, y int) float32 { if x < 0 || x >= width || y < 0 || y >= height { return 0.5 @@ -345,14 +271,11 @@ func sampleHeightGrid(grid []float32, width, height, x, y int) float32 { return grid[y*width+x] } -// chamferDistanceField computes a fast approximate distance (in pixels) from any pixel to the nearest -// baseMask==1 pixel. Distance is zero for pixels inside baseMask. -// This is a two-pass chamfer approximation (float), cheap and parallel friendly. +// chamferDistanceField computes fast approximate distance from any pixel to centerline func chamferDistanceField(baseMask []uint8, w, h int) []float32 { const maxF = 1e6 dist := make([]float32, w*h) - // Initialize for i := 0; i < w*h; i++ { if baseMask[i] == 1 { dist[i] = 0 @@ -368,28 +291,24 @@ func chamferDistanceField(baseMask []uint8, w, h int) []float32 { if dist[i] == 0 { continue } - // check left if x > 0 { v := dist[i-1] + 1.0 if v < dist[i] { dist[i] = v } } - // check top if y > 0 { v := dist[i-w] + 1.0 if v < dist[i] { dist[i] = v } } - // check top-left if x > 0 && y > 0 { v := dist[i-w-1] + 1.41421356 if v < dist[i] { dist[i] = v } } - // check top-right if x < w-1 && y > 0 { v := dist[i-w+1] + 1.41421356 if v < dist[i] { @@ -403,28 +322,24 @@ func chamferDistanceField(baseMask []uint8, w, h int) []float32 { for y := h - 1; y >= 0; y-- { for x := w - 1; x >= 0; x-- { i := y*w + x - // check right if x < w-1 { v := dist[i+1] + 1.0 if v < dist[i] { dist[i] = v } } - // check bottom if y < h-1 { v := dist[i+w] + 1.0 if v < dist[i] { dist[i] = v } } - // check bottom-right if x < w-1 && y < h-1 { v := dist[i+w+1] + 1.41421356 if v < dist[i] { dist[i] = v } } - // check bottom-left if x > 0 && y < h-1 { v := dist[i+w-1] + 1.41421356 if v < dist[i] { @@ -437,15 +352,9 @@ func chamferDistanceField(baseMask []uint8, w, h int) []float32 { return dist } -// generateIslandsInMask will attempt to create small islands inside contiguous water areas. -// It modifies the mask in place (1=water, 0=land). The algorithm: -// - choose candidate water pixels with slightly higher-than-water height -// - use a small BFS flood constrained by height to form island patches -// - reject patches that touch the bounding box edge (we want enclosed islands) -// - enforce size limits +// generateIslandsInMask creates small islands inside water areas func generateIslandsInMask(mask []uint8, bw, bh, minX, minY int, heightGrid []float32, fullW, fullH int, params *riverParams, seed int64) { r := rand.New(rand.NewSource(seed)) - // Collect candidates type pt struct{ x, y int } candidates := make([]pt, 0) for y := 0; y < bh; y++ { @@ -457,7 +366,6 @@ func generateIslandsInMask(mask []uint8, bw, bh, minX, minY int, heightGrid []fl absX := x + minX absY := y + minY hv := sampleHeightGrid(heightGrid, fullW, fullH, absX, absY) - // candidate if slightly higher than local water bias if float64(hv) > params.WaterLevelBias+0.03 { if r.Float64() < params.IslandSeedChance { candidates = append(candidates, pt{x, y}) @@ -469,7 +377,6 @@ func generateIslandsInMask(mask []uint8, bw, bh, minX, minY int, heightGrid []fl return } - // Shuffle candidates to randomize island placement r.Shuffle(len(candidates), func(i, j int) { candidates[i], candidates[j] = candidates[j], candidates[i] }) visited := make([]uint8, bw*bh) @@ -479,10 +386,8 @@ func generateIslandsInMask(mask []uint8, bw, bh, minX, minY int, heightGrid []fl if visited[ci] != 0 { continue } - // BFS grow island maxSize := params.MaxIslandSize minSize := params.MinIslandSize - // randomize size a bit targetSize := minSize + r.Intn(maxSize-minSize+1) queue := []pt{{c.x, c.y}} @@ -494,13 +399,11 @@ func generateIslandsInMask(mask []uint8, bw, bh, minX, minY int, heightGrid []fl p := queue[qi] absX := p.x + minX absY := p.y + minY - // Height constraint: island must be above a modest threshold hv := sampleHeightGrid(heightGrid, fullW, fullH, absX, absY) if float64(hv) < params.WaterLevelBias+0.01 { continue } island = append(island, p) - // Expand for dy := -1; dy <= 1; dy++ { for dx := -1; dx <= 1; dx++ { nx, ny := p.x+dx, p.y+dy @@ -512,7 +415,6 @@ func generateIslandsInMask(mask []uint8, bw, bh, minX, minY int, heightGrid []fl if visited[nidx] != 0 { continue } - // Only grow into water pixels if mask[nidx] != 1 { continue } @@ -522,24 +424,19 @@ func generateIslandsInMask(mask []uint8, bw, bh, minX, minY int, heightGrid []fl } } - // If island touches bbox edge, reject it (we want enclosed islands) if touchesEdge { continue } - // size check if len(island) < minSize { continue } - // Carve the island: set mask pixels to 0 (land) for _, p := range island { mask[p.y*bw+p.x] = 0 } - // Optionally stop after creating a few islands to keep them rare if r.Float64() < 0.7 { - // keep creating more sometimes, break otherwise if r.Intn(3) == 0 { break } @@ -547,8 +444,7 @@ func generateIslandsInMask(mask []uint8, bw, bh, minX, minY int, heightGrid []fl } } -// removeSpeckles removes tiny isolated water components (erodes islands smaller than threshold). -// This is a simple pass that clears pixels that have fewer than minNeighbors water neighbors. +// removeSpeckles removes tiny isolated water pixels func removeSpeckles(mask *[]uint8, bw, bh, minNeighbors int) { arr := *mask out := make([]uint8, len(arr)) @@ -584,7 +480,7 @@ func removeSpeckles(mask *[]uint8, bw, bh, minNeighbors int) { *mask = arr } -// (Optional) utility used for debug or visualization - not used directly in pipeline. +// maskToPoints converts mask to point slice for visualization func maskToPoints(mask []uint8, bw, bh, minX, minY int) []image.Point { var pts []image.Point for y := 0; y < bh; y++ { @@ -597,7 +493,7 @@ func maskToPoints(mask []uint8, bw, bh, minX, minY int) []image.Point { return pts } -// small clamp helpers +// clamp01 clamps value to 0..1 range func clamp01(v float64) float64 { if v < 0 { return 0 diff --git a/roads.go b/roads.go index bab3abc..06dd427 100644 --- a/roads.go +++ b/roads.go @@ -11,20 +11,20 @@ import ( "unsafe" ) -// PointOfInterest represents a location on the map where roads may start, end, or intersect. +// PointOfInterest represents a location where roads may start, end, or intersect type PointOfInterest struct { X, Y int Connections int IsExit bool } -// PathPoint represents a single point in a road's path, with a flag to indicate if it's a bridge. +// PathPoint represents a single point in a road's path with bridge flag type PathPoint struct { Point image.Point IsBridge bool } -// Road represents a connection between two Points of Interest. +// Road represents a connection between two Points of Interest type Road struct { Start, End *PointOfInterest Width int @@ -32,9 +32,8 @@ type Road struct { Importance int } -// GenerateRoads is the main function for creating roads on the map. +// GenerateRoads creates roads on the map func GenerateRoads(width, height int, settings *Settings, noiseImg image.Image, allWaterPixels []image.Point, seed int64) ([]image.Point, []image.Point, *image.RGBA) { - // Step 1: Initialize a transparent image for drawing roads img := image.NewRGBA(image.Rect(0, 0, width, height)) for y := 0; y < height; y++ { for x := 0; x < width; x++ { @@ -42,23 +41,18 @@ func GenerateRoads(width, height int, settings *Settings, noiseImg image.Image, } } - // Step 2: Set up random number generator and colors randSrc := rand.New(rand.NewSource(seed)) roadColor := color.RGBA{R: 139, G: 69, B: 19, A: 255} bridgeColor := color.RGBA{R: 60, G: 42, B: 33, A: 255} - // Step 3: Generate Points of Interest (POIs) pois := generatePOIs(width, height, settings, allWaterPixels, randSrc) if len(pois) == 0 { return nil, nil, img } - // Step 4: Connect POIs to form roads roads := connectPOIs(pois, width, height, settings, randSrc, allWaterPixels) - // Step 5: Assign widths to the roads based on their importance assignRoadWidths(roads, settings) - // Step 6: Draw the roads on the image var allRoadPixels []image.Point var allBridgePixels []image.Point for _, road := range roads { @@ -70,7 +64,7 @@ func GenerateRoads(width, height int, settings *Settings, noiseImg image.Image, return allRoadPixels, allBridgePixels, img } -// generatePOIs creates the initial set of points where roads will originate. +// generatePOIs creates initial points where roads will originate func generatePOIs(width, height int, settings *Settings, allWaterPixels []image.Point, randSrc *rand.Rand) []*PointOfInterest { numPOIs := settings.NumRoads / 2 if numPOIs == 0 { @@ -91,33 +85,30 @@ func generatePOIs(width, height int, settings *Settings, allWaterPixels []image. centerX := width / 2 centerY := height / 2 - // Distribution affects the radius of POI generation maxRadius := math.Min(float64(width)/2, float64(height)/2) radius := maxRadius * (settings.RoadDistribution / 100.0) for i := 0; i < numPOIs; i++ { var x, y int found := false - for j := 0; j < 100; j++ { // Retries to find a land spot + for j := 0; j < 100; j++ { if i < numExits { - // Create POIs at the map edges side := randSrc.Intn(4) switch side { - case 0: // Top + case 0: x = randSrc.Intn(width) y = 0 - case 1: // Bottom + case 1: x = randSrc.Intn(width) y = height - 1 - case 2: // Left + case 2: x = 0 y = randSrc.Intn(height) - case 3: // Right + case 3: x = width - 1 y = randSrc.Intn(height) } } else { - // Create POIs within the map angle := randSrc.Float64() * 2 * math.Pi r := math.Sqrt(randSrc.Float64()) * radius x = int(float64(centerX) + r*math.Cos(angle)) @@ -138,7 +129,7 @@ func generatePOIs(width, height int, settings *Settings, allWaterPixels []image. return pois } -// connectPOIs creates roads by connecting the generated Points of Interest. +// connectPOIs creates roads by connecting Points of Interest func connectPOIs(pois []*PointOfInterest, width, height int, settings *Settings, randSrc *rand.Rand, allWaterPixels []image.Point) []*Road { if len(pois) < 2 { return nil @@ -151,7 +142,6 @@ func connectPOIs(pois []*PointOfInterest, width, height int, settings *Settings, visited := make(map[*PointOfInterest]bool) existingRoads := make(map[string]bool) - // Find the center-most POI to start connecting from centerX := width / 2 centerY := height / 2 var startNode *PointOfInterest @@ -174,11 +164,9 @@ func connectPOIs(pois []*PointOfInterest, width, height int, settings *Settings, visited[startNode] = true - // Use average dimension for controlling road path calculation avgDim := float64(width+height) / 2.0 numControlPoints := max(int(avgDim*0.03), 60) - // Connect all POIs using a minimum spanning tree-like algorithm for len(visited) < len(pois) { var closest *PointOfInterest var fromNode *PointOfInterest @@ -192,7 +180,6 @@ func connectPOIs(pois []*PointOfInterest, width, height int, settings *Settings, if !visited[other] { dist := math.Sqrt(math.Pow(float64(poi.X-other.X), 2) + math.Pow(float64(poi.Y-other.Y), 2)) - // Check if a road already exists between these two POIs key := fmt.Sprintf("%p-%p", poi, other) if uintptr(unsafe.Pointer(poi)) > uintptr(unsafe.Pointer(other)) { key = fmt.Sprintf("%p-%p", other, poi) @@ -201,7 +188,6 @@ func connectPOIs(pois []*PointOfInterest, width, height int, settings *Settings, continue } - // Avoid connecting two exit points directly if poi.IsExit && other.IsExit { continue } @@ -220,7 +206,6 @@ func connectPOIs(pois []*PointOfInterest, width, height int, settings *Settings, fromNode.Connections++ closest.Connections++ - // Add road to existing roads map to prevent duplicates key := fmt.Sprintf("%p-%p", fromNode, closest) if uintptr(unsafe.Pointer(fromNode)) > uintptr(unsafe.Pointer(closest)) { key = fmt.Sprintf("%p-%p", closest, fromNode) @@ -238,7 +223,6 @@ func connectPOIs(pois []*PointOfInterest, width, height int, settings *Settings, } }(fromNode, closest) } else { - // No more reachable POIs, break the loop break } } @@ -252,7 +236,6 @@ func connectPOIs(pois []*PointOfInterest, width, height int, settings *Settings, roads = append(roads, road) } - // Calculate road importance based on the number of connections at its endpoints for _, road := range roads { road.Importance = road.Start.Connections + road.End.Connections } @@ -260,13 +243,12 @@ func connectPOIs(pois []*PointOfInterest, width, height int, settings *Settings, return roads } -// assignRoadWidths sets the width of each road based on its importance. +// assignRoadWidths sets road width based on importance func assignRoadWidths(roads []*Road, settings *Settings) { if len(roads) == 0 { return } - // Sort roads by importance in descending order sort.Slice(roads, func(i, j int) bool { return roads[i].Importance > roads[j].Importance }) @@ -278,13 +260,12 @@ func assignRoadWidths(roads []*Road, settings *Settings) { widthStep = (maxWidth - minWidth) / float64(len(roads)-1) } - // Assign widths, with more important roads being wider for i, road := range roads { road.Width = int(maxWidth - float64(i)*widthStep) } } -// drawRoad draws a single road on the image, including bridges. +// drawRoad draws a single road on the image including bridges func drawRoad(img *image.RGBA, points []PathPoint, roadColor, bridgeColor color.Color, width int) ([]image.Point, []image.Point) { var roadPixels []image.Point var bridgePixels []image.Point @@ -306,7 +287,7 @@ func drawRoad(img *image.RGBA, points []PathPoint, roadColor, bridgeColor color. return roadPixels, bridgePixels } -// bresenhamRoad uses Bresenham's line algorithm to create a path between control points. +// bresenhamRoad creates a path between control points using Bresenham's algorithm func bresenhamRoad(path []image.Point) []image.Point { if len(path) < 2 { return path @@ -346,7 +327,7 @@ func bresenhamRoad(path []image.Point) []image.Point { return fullPath } -// calculateRoadPath computes the path for a road, including curves and bridges. +// calculateRoadPath computes the path for a road including curves and bridges func calculateRoadPath(start, end *PointOfInterest, curvyness, avgDim float64, randSrc *rand.Rand, numControlPoints int, allWaterPixels []image.Point) []PathPoint { dx := end.X - start.X dy := end.Y - start.Y @@ -361,7 +342,6 @@ func calculateRoadPath(start, end *PointOfInterest, curvyness, avgDim float64, r return []PathPoint{{Point: image.Point{X: start.X, Y: start.Y}, IsBridge: waterMap[image.Point{X: start.X, Y: start.Y}]}} } - // Adjust curviness based on the distance between the POIs distanceFactor := math.Min(1.0, dist/(avgDim*0.5)) adjustedCurvyness := curvyness * distanceFactor @@ -374,7 +354,6 @@ func calculateRoadPath(start, end *PointOfInterest, curvyness, avgDim float64, r return pathPoints } - // Use sine waves to create curves in the road type wave struct { amplitude float64 numWaves float64 @@ -389,21 +368,18 @@ func calculateRoadPath(start, end *PointOfInterest, curvyness, avgDim float64, r } baseNumWaves := (dist / mainWavelength) * adjustedCurvyness - // Main wave for overall curve waves[0] = wave{ amplitude: amp, numWaves: baseNumWaves * (0.75 + randSrc.Float64()*0.5), phase: randSrc.Float64() * 2 * math.Pi, } - // Smaller wave for minor detours and a more natural look waves[1] = wave{ amplitude: amp / 4, numWaves: baseNumWaves * 4 * (0.75 + randSrc.Float64()*0.5), phase: randSrc.Float64() * 2 * math.Pi, } - // Generate control points for the curve controlPoints := make([]image.Point, numControlPoints+1) for i := 0; i <= numControlPoints; i++ { t := float64(i) / float64(numControlPoints) @@ -426,7 +402,6 @@ func calculateRoadPath(start, end *PointOfInterest, curvyness, avgDim float64, r controlPoints[i] = image.Point{X: int(math.Round(x)), Y: int(math.Round(y))} } - // Create the final path using Bresenham's algorithm between control points points := bresenhamRoad(controlPoints) pathPoints := make([]PathPoint, len(points)) for i, p := range points { @@ -435,7 +410,7 @@ func calculateRoadPath(start, end *PointOfInterest, curvyness, avgDim float64, r return pathPoints } -// drawLine draws a line with a specified width on the image. +// drawLine draws a line with specified width on the image func drawLine(img *image.RGBA, x0, y0, x1, y1 int, col color.Color, width int) []image.Point { var points []image.Point dx := abs(x1 - x0) @@ -478,7 +453,7 @@ func drawLine(img *image.RGBA, x0, y0, x1, y1 int, col color.Color, width int) [ return points } -// abs returns the absolute value of an integer. +// abs returns the absolute value of an integer func abs(x int) int { if x < 0 { return -x diff --git a/seed.go b/seed.go index 4d6faea..21e8cbd 100644 --- a/seed.go +++ b/seed.go @@ -2,21 +2,19 @@ package main import "math/rand" -// SeedProvider is a simple struct that provides a stream of random seeds -// from a single initial seed. This ensures that the entire map generation -// process is deterministic if the same initial seed is used. +// SeedProvider provides a stream of random seeds from a single initial seed type SeedProvider struct { rand *rand.Rand } -// NewSeedProvider creates a new SeedProvider with the given initial seed. +// NewSeedProvider creates a new SeedProvider with the given initial seed func NewSeedProvider(seed int64) *SeedProvider { return &SeedProvider{ rand: rand.New(rand.NewSource(seed)), } } -// Next returns the next random seed in the sequence. +// Next returns the next random seed in the sequence func (sp *SeedProvider) Next() int64 { return sp.rand.Int63() } diff --git a/terrain.go b/terrain.go index 42dea6a..b278fbd 100644 --- a/terrain.go +++ b/terrain.go @@ -14,16 +14,14 @@ import ( "github.com/ojrac/opensimplex-go" ) -// Constants for Perlin noise generation const ( alpha = 2. beta = 2. n = 3 ) -// GenerateHeightmap creates a grayscale image representing the terrain's elevation using Perlin noise. +// GenerateHeightmap creates terrain elevation using Perlin noise func GenerateHeightmap(width, height, octaves int, scale float64, seed int64) image.Image { - // Initialize Perlin noise generator p := perlin.NewPerlin(alpha, beta, n, seed) img := image.NewGray(image.Rect(0, 0, width, height)) @@ -31,7 +29,6 @@ func GenerateHeightmap(width, height, octaves int, scale float64, seed int64) im scale = 100.0 } - // Use multiple goroutines to speed up noise generation numGoroutines := runtime.NumCPU() var wg sync.WaitGroup rowsPerGoroutine := height / numGoroutines @@ -47,7 +44,6 @@ func GenerateHeightmap(width, height, octaves int, scale float64, seed int64) im defer wg.Done() for y := startY; y < endY; y++ { for x := 0; x < width; x++ { - // Combine multiple octaves of noise for more detail var noise float64 frequency := 1.0 amplitude := 1.0 @@ -60,7 +56,6 @@ func GenerateHeightmap(width, height, octaves int, scale float64, seed int64) im frequency *= 2.0 } - // Normalize the noise value and set the pixel color noise /= maxAmplitude grayColor := uint8((noise + 1) * 127.5) img.SetGray(x, y, color.Gray{Y: grayColor}) @@ -73,13 +68,12 @@ func GenerateHeightmap(width, height, octaves int, scale float64, seed int64) im return img } -// ApplyRoughness adds a visual roughness effect to the heightmap. +// ApplyRoughness adds visual roughness effect to the heightmap func ApplyRoughness(heightmap image.Image, roughness float64) image.Image { bounds := heightmap.Bounds() composite := image.NewRGBA(bounds) draw.Draw(composite, bounds, heightmap, image.Point{}, draw.Src) - // The alpha value of the overlay determines the roughness effect alphaValue := 255 - uint8(roughness*2.55) overlay := image.NewUniform(color.RGBA{R: 128, G: 128, B: 128, A: alphaValue}) draw.Draw(composite, bounds, overlay, image.Point{}, draw.Over) @@ -87,23 +81,20 @@ func ApplyRoughness(heightmap image.Image, roughness float64) image.Image { return composite } -// DarkenLakeAreas applies a visual darkening effect to the heightmap where lakes exist. +// DarkenLakeAreas darkens the heightmap where lakes exist func DarkenLakeAreas(heightmap image.Image, lakePixels []image.Point) image.Image { bounds := heightmap.Bounds() width := bounds.Dx() - // Create a new black image to draw the lakes on lakeMask := image.NewRGBA(bounds) black := color.RGBA{0, 0, 0, 255} for _, p := range lakePixels { lakeMask.Set(p.X, p.Y, black) } - // Apply a Gaussian blur to the lake mask to create smooth edges blurRadius := float64(width) * 0.05 blurredLakeMask := imaging.Blur(lakeMask, blurRadius) - // Composite the blurred lake mask onto the heightmap with some opacity composite := image.NewRGBA(bounds) draw.Draw(composite, bounds, heightmap, image.Point{}, draw.Src) draw.DrawMask(composite, bounds, blurredLakeMask, image.Point{}, image.NewUniform(color.Alpha{192}), image.Point{}, draw.Over) @@ -111,33 +102,27 @@ func DarkenLakeAreas(heightmap image.Image, lakePixels []image.Point) image.Imag return composite } -// FlattenRoadAreas smoothens the terrain under roads. +// FlattenRoadAreas smooths terrain under roads func FlattenRoadAreas(heightmap image.Image, roadPixels []image.Point) image.Image { bounds := heightmap.Bounds() width := bounds.Dx() - // Create a mask with the road pixels roadMask := image.NewGray(bounds) for _, p := range roadPixels { roadMask.SetGray(p.X, p.Y, color.Gray{Y: 255}) } - // Blur the road mask to create a smooth transition blurRadius := float64(width) * 0.01 blurredRoadMask := imaging.Blur(roadMask, blurRadius) - // Blur the entire heightmap blurredHeightmap := imaging.Blur(heightmap, blurRadius) - // Create a new composite image composite := image.NewRGBA(bounds) - // Interpolate between the original and blurred heightmap based on the road mask for y := bounds.Min.Y; y < bounds.Max.Y; y++ { for x := bounds.Min.X; x < bounds.Max.X; x++ { maskAlpha, _, _, _ := blurredRoadMask.At(x, y).RGBA() if maskAlpha > 0 { - // Linearly interpolate between the original and blurred heightmap originalColor := heightmap.At(x, y) blurredColor := blurredHeightmap.At(x, y) @@ -161,12 +146,11 @@ func FlattenRoadAreas(heightmap image.Image, roadPixels []image.Point) image.Ima return composite } -// GenerateTrees places trees on the map. +// GenerateTrees places trees on the map based on coverage and noise func GenerateTrees(img *image.RGBA, lakePixels, roadPixels, buildingPixels []image.Point, minTreeSize, maxTreeSize, treeCoverage, treeClumpiness float64, seed int64) []image.Point { width := img.Bounds().Dx() height := img.Bounds().Dy() - // Step 1: Calculate the number of trees to place based on coverage percentage. avgTreeSize := (minTreeSize + maxTreeSize) / 2 if avgTreeSize <= 0 { return nil @@ -183,20 +167,18 @@ func GenerateTrees(img *image.RGBA, lakePixels, roadPixels, buildingPixels []ima return nil } - // Step 2: Generate a simplex noise map to guide tree placement. noise := opensimplex.New(seed) treeNoiseMap := image.NewGray(image.Rect(0, 0, width, height)) treeNoiseZoom := 0.05 for y := 0; y < height; y++ { for x := 0; x < width; x++ { val := noise.Eval2(float64(x)*treeNoiseZoom, float64(y)*treeNoiseZoom) - val = (val + 1) / 2 // Normalize to 0-1 + val = (val + 1) / 2 treeNoiseMap.SetGray(x, y, color.Gray{Y: uint8(val * 255)}) } } threshold := uint8(255 * (1 - (treeCoverage / 100.0))) - // Create lookup maps for water, roads, and buildings for efficient collision detection isLake := make(map[image.Point]bool) for _, p := range lakePixels { isLake[p] = true @@ -214,12 +196,11 @@ func GenerateTrees(img *image.RGBA, lakePixels, roadPixels, buildingPixels []ima randSrc := rand.New(rand.NewSource(seed)) - // Step 3: Determine initial points for clumps of trees. numClumpTrees := min(int(treeClumpiness), numTreesToPlace) initialPoints := make([]image.Point, 0, numClumpTrees) for range numClumpTrees { - for range 100 { // try 100 times to find a valid spot + for range 100 { p := image.Point{X: randSrc.Intn(width), Y: randSrc.Intn(height)} if treeNoiseMap.GrayAt(p.X, p.Y).Y >= threshold && !isLake[p] && !isRoad[p] && !isBuilding[p] { initialPoints = append(initialPoints, p) @@ -228,14 +209,12 @@ func GenerateTrees(img *image.RGBA, lakePixels, roadPixels, buildingPixels []ima } } - // Step 4: Place remaining trees using Poisson Disc Sampling for a natural distribution. minRadius := minTreeSize allPoints := poissonDiscSampling(width, height, minRadius, 30, initialPoints, func(p image.Point) bool { return treeNoiseMap.GrayAt(p.X, p.Y).Y >= threshold && !isLake[p] && !isRoad[p] && !isBuilding[p] }, seed) var treePixels []image.Point - // Step 5: Draw the trees on the image. numGoroutines := runtime.NumCPU() if len(allPoints) < numGoroutines { numGoroutines = len(allPoints) @@ -297,7 +276,7 @@ func GenerateTrees(img *image.RGBA, lakePixels, roadPixels, buildingPixels []ima return treePixels } -// poissonDiscSampling generates points that are randomly distributed but no closer than a given minimum radius. +// poissonDiscSampling generates randomly distributed points with minimum radius separation func poissonDiscSampling(width, height int, minRadius float64, k int, initialPoints []image.Point, isValid func(image.Point) bool, seed int64) []image.Point { randSrc := rand.New(rand.NewSource(seed)) points := initialPoints diff --git a/water.go b/water.go index f6b77d3..c069cd1 100644 --- a/water.go +++ b/water.go @@ -12,19 +12,16 @@ import ( "github.com/ojrac/opensimplex-go" ) -// lakePixel represents a potential pixel to be added to a lake during growth. -// It is used in a priority queue to determine the next pixel to add. type lakePixel struct { point image.Point score float64 - index int // required for heap.Interface + index int } -// priorityQueue implements a max-heap for lakePixel structs. type priorityQueue []*lakePixel func (pq priorityQueue) Len() int { return len(pq) } -func (pq priorityQueue) Less(i, j int) bool { return pq[i].score > pq[j].score } // Max-heap +func (pq priorityQueue) Less(i, j int) bool { return pq[i].score > pq[j].score } func (pq priorityQueue) Swap(i, j int) { pq[i], pq[j] = pq[j], pq[i] pq[i].index = i @@ -46,10 +43,8 @@ func (pq *priorityQueue) Pop() any { return item } -// GenerateLakes creates lakes on the map using a growth algorithm. -// When lakeEdgeRoughness is 0, lakes grow in perfect circles. Higher values add noise-based irregularity. +// GenerateLakes creates lakes on the map using a priority queue growth algorithm func GenerateLakes(width, height, numLakes int, lakeSizeLower, lakeSizeUpper float64, seed int64, lakeEdgeRoughness float64) (image.Image, [][]image.Point) { - // Initialize a white canvas to draw the lakes on canvas := image.NewRGBA(image.Rect(0, 0, width, height)) draw.Draw(canvas, canvas.Bounds(), image.NewUniform(color.White), image.Point{}, draw.Src) @@ -60,7 +55,7 @@ func GenerateLakes(width, height, numLakes int, lakeSizeLower, lakeSizeUpper flo var allLakes [][]image.Point randSrc := rand.New(rand.NewSource(seed)) - // Step 1: Divide the image into a grid to distribute the lakes. + // Divide the image into a grid to distribute lakes evenly gridDim := int(math.Ceil(math.Sqrt(float64(numLakes)))) if gridDim == 0 { return canvas, nil @@ -71,7 +66,7 @@ func GenerateLakes(width, height, numLakes int, lakeSizeLower, lakeSizeUpper flo return canvas, nil } - // Step 2: Create a shuffled list of chunk indices to randomize lake placement. + // Shuffle chunk indices for random lake placement chunkIndices := make([]int, gridDim*gridDim) for i := range chunkIndices { chunkIndices[i] = i @@ -83,7 +78,7 @@ func GenerateLakes(width, height, numLakes int, lakeSizeLower, lakeSizeUpper flo totalArea := float64(width * height) noiseGen := opensimplex.New(seed) - // Step 3: Generate a lake in a subset of the chunks. + // Generate each lake for i := range numLakes { if i >= len(chunkIndices) { break @@ -91,7 +86,7 @@ func GenerateLakes(width, height, numLakes int, lakeSizeLower, lakeSizeUpper flo var currentLake []image.Point - // Each lake gets a random size within the defined range. + // Randomize lake size within specified range lakeSize := lakeSizeLower if lakeSizeUpper > lakeSizeLower { lakeSize = lakeSizeLower + randSrc.Float64()*(lakeSizeUpper-lakeSizeLower) @@ -112,12 +107,12 @@ func GenerateLakes(width, height, numLakes int, lakeSizeLower, lakeSizeUpper flo (chunkGridY+1)*chunkHeight, ) - // Use a priority queue-based growth algorithm within the chunk. + // Initialize priority queue growth algorithm pq := &priorityQueue{} heap.Init(pq) visited := make(map[image.Point]bool) - // Start the growth near the center of the chunk. + // Start growth at chunk center startPt := image.Point{ X: chunkRect.Min.X + chunkWidth/2, Y: chunkRect.Min.Y + chunkHeight/2, @@ -126,33 +121,31 @@ func GenerateLakes(width, height, numLakes int, lakeSizeLower, lakeSizeUpper flo continue } - // Use noise to create a more natural lake shape (only if roughness > 0). + // Setup noise generation for natural lake shapes seedX := randSrc.Float64() * 10000.0 seedY := randSrc.Float64() * 10000.0 radius := math.Sqrt(float64(targetPixelsPerLake) / math.Pi) noiseFreq := 0.01 + (0.2 / (radius + 1.0)) + // Score function determines which pixels to add to lake getScore := func(pt image.Point) float64 { dx, dy := pt.X-startPt.X, pt.Y-startPt.Y dist := math.Sqrt(float64(dx*dx + dy*dy)) distPenalty := math.Pow(dist/radius, 3.0) - // Only apply noise if edge roughness is requested if lakeEdgeRoughness > 0 { noise := noiseGen.Eval2(seedX+float64(dx)*noiseFreq, seedY+float64(dy)*noiseFreq) - // Scale noise contribution by roughness setting noiseContribution := noise * (lakeEdgeRoughness / 100.0) return noiseContribution - distPenalty } - // Pure circular growth when variability is 0 return -distPenalty } heap.Push(pq, &lakePixel{point: startPt, score: getScore(startPt)}) visited[startPt] = true - // Grow the lake until it reaches its target size. + // Grow lake to target size lakeCount := 0 for pq.Len() > 0 && lakeCount < targetPixelsPerLake { current := heap.Pop(pq).(*lakePixel) @@ -161,7 +154,7 @@ func GenerateLakes(width, height, numLakes int, lakeSizeLower, lakeSizeUpper flo currentLake = append(currentLake, current.point) lakeCount++ - // Add neighbors to the priority queue. + // Add neighboring pixels to growth queue for dy := -1; dy <= 1; dy++ { for dx := -1; dx <= 1; dx++ { if dx == 0 && dy == 0 { @@ -189,14 +182,14 @@ func GenerateLakes(width, height, numLakes int, lakeSizeLower, lakeSizeUpper flo return canvas, allLakes } -// River represents a river on the map. +// River represents a river on the map type River struct { Width float64 Start, End image.Point Points []image.Point } -// GenerateRivers creates rivers on the map. +// GenerateRivers creates rivers flowing across the map from edge to edge func GenerateRivers(width, height, numRivers int, minWidth, maxWidth, curvyness float64, inputImage image.Image, lakes [][]image.Point, seed int64, heightmap image.Image, riverWidthVariability, riverEdgeRoughness float64) (image.Image, []image.Point) { if numRivers == 0 { return inputImage, nil @@ -212,7 +205,7 @@ func GenerateRivers(width, height, numRivers int, minWidth, maxWidth, curvyness randSrc := rand.New(rand.NewSource(seed)) avgDim := float64(width+height) / 2.0 - // Create a map of water pixels for collision detection. + // Build water pixel lookup maps isWater := make(map[image.Point]bool) lakePixelMap := make(map[image.Point]int) for i, lake := range lakes { @@ -222,7 +215,7 @@ func GenerateRivers(width, height, numRivers int, minWidth, maxWidth, curvyness } } - // Create rivers with varying widths. + // Create rivers with progressively varying widths rivers := make([]River, numRivers) for i := range numRivers { widthPercent := float64(i) / float64(numRivers-1) @@ -232,36 +225,36 @@ func GenerateRivers(width, height, numRivers int, minWidth, maxWidth, curvyness rivers[i].Width = maxWidth - widthPercent*(maxWidth-minWidth) } - // Sort rivers by width in descending order. + // Sort rivers by width in descending order sort.Slice(rivers, func(i, j int) bool { return rivers[i].Width > rivers[j].Width }) numControlPoints := max(int(avgDim*0.03), 60) - // Generate each river. + // Generate each river for i := range rivers { r := &rivers[i] - // Determine the start and end edges of the river. + // Pick random start and end edges startEdge := randSrc.Intn(4) endEdge := (startEdge + randSrc.Intn(3) + 1) % 4 r.Start = getPointOnEdge(width, height, startEdge, randSrc) r.End = getPointOnEdge(width, height, endEdge, randSrc) - // Calculate the river's path. + // Calculate river path with curves path := calculateRiverPath(r.Start, r.End, curvyness/100.0, avgDim, randSrc, numControlPoints) - // Check for intersections with other water bodies. + // Check for intersections with existing water for _, p := range path { if isWater[p] { if lakeIndex, isLake := lakePixelMap[p]; isLake { - // If the river intersects with a lake, end the river at the lake's center. + // End river at lake center if it intersects lakeCenter := findCenter(lakes[lakeIndex]) r.End = lakeCenter } else { - // If the river intersects with another river, end it at the intersection point. + // End river at intersection with another river r.End = p } path = calculateRiverPath(r.Start, r.End, curvyness/100.0, avgDim, randSrc, numControlPoints) @@ -269,7 +262,7 @@ func GenerateRivers(width, height, numRivers int, minWidth, maxWidth, curvyness } } - // Draw the river on the canvas. + // Draw river on canvas riverWidthPx := (r.Width / 100.0) * avgDim radius := riverWidthPx / 2.0 @@ -282,7 +275,7 @@ func GenerateRivers(width, height, numRivers int, minWidth, maxWidth, curvyness return canvas, allRiverPixels } -// bresenhamRiver creates a path between control points using Bresenham's line algorithm. +// bresenhamRiver draws a line between control points using Bresenham's algorithm func bresenhamRiver(path []image.Point) []image.Point { if len(path) < 2 { return path @@ -322,7 +315,7 @@ func bresenhamRiver(path []image.Point) []image.Point { return fullPath } -// calculateRiverPath computes the path for a river, including curves. +// calculateRiverPath computes a curved path for a river using sine waves func calculateRiverPath(start, end image.Point, curvyness, avgDim float64, randSrc *rand.Rand, numControlPoints int) []image.Point { dx := end.X - start.X dy := end.Y - start.Y @@ -336,7 +329,7 @@ func calculateRiverPath(start, end image.Point, curvyness, avgDim float64, randS return bresenhamRiver([]image.Point{start, end}) } - // Use sine waves to create curves in the river. + // Use multiple sine waves at different frequencies for natural curves type wave struct { amplitude float64 numWaves float64 @@ -362,7 +355,7 @@ func calculateRiverPath(start, end image.Point, curvyness, avgDim float64, randS amp /= 3 } - // Generate control points for the curve. + // Generate control points along the path controlPoints := make([]image.Point, numControlPoints+1) for i := 0; i <= numControlPoints; i++ { t := float64(i) / float64(numControlPoints) @@ -381,11 +374,11 @@ func calculateRiverPath(start, end image.Point, curvyness, avgDim float64, randS controlPoints[i] = image.Point{X: int(math.Round(x)), Y: int(math.Round(y))} } - // Create the final path using Bresenham's algorithm between control points. + // Create final path using Bresenham between control points return bresenhamRiver(controlPoints) } -// findCenter finds the center of a slice of points. +// findCenter calculates the center point of a set of pixels func findCenter(pixels []image.Point) image.Point { if len(pixels) == 0 { return image.Point{} @@ -401,7 +394,7 @@ func findCenter(pixels []image.Point) image.Point { } } -// getPointOnEdge returns a random point on a specified edge of the map. +// getPointOnEdge returns a random point on the specified map edge func getPointOnEdge(width, height, edge int, randSrc *rand.Rand) image.Point { switch edge { case 0: // Top @@ -415,20 +408,11 @@ func getPointOnEdge(width, height, edge int, randSrc *rand.Rand) image.Point { } } -// drawCircle draws a circle on the image and adds its pixels to the given slice. -// The outer edges are roughened using dual sin waves for natural-looking banks. -// riverWidthVariability controls the amplitude of width changes (0-100%). -// riverEdgeRoughness controls the detail level of the edge roughness (0-100%). +// drawCircle draws a circular river cross-section with sine wave edge roughening func drawCircle(img *image.RGBA, center image.Point, radius float64, c color.Color, pixels *[]image.Point, isWater map[image.Point]bool, heightmap image.Image, riverWidthVariability, riverEdgeRoughness float64) { bounds := img.Bounds() - // Calculate dual sin wave amplitudes for outer edge roughening - // Large amplitude represents major variations in river width (controlled by riverWidthVariability) - // At 0%, no width variation; at 100%, amplitude is 50% of radius largeAmplitude := (radius * 0.5) * (riverWidthVariability / 100.0) - - // Small amplitude is controlled by riverEdgeRoughness - // At 0%, no detail; at 100%, detail amplitude equals large amplitude smallAmplitude := largeAmplitude * (riverEdgeRoughness / 100.0) for y := int(math.Floor(float64(center.Y) - radius)); y <= int(math.Ceil(float64(center.Y)+radius)); y++ { @@ -441,13 +425,12 @@ func drawCircle(img *image.RGBA, center image.Point, radius float64, c color.Col dx, dy := float64(x-center.X), float64(y-center.Y) dist := math.Sqrt(dx*dx + dy*dy) - // Apply dual sin wave offset to create rough edges + // Apply dual sine waves for edge roughness positionPhase := float64(x)*0.008 + float64(y)*0.012 largeWave := math.Sin(positionPhase) * largeAmplitude smallWave := math.Sin(positionPhase*3.5) * smallAmplitude waveOffset := largeWave + smallWave - // Effective radius varies based on sin wave effectiveRadius := radius + waveOffset if dist <= effectiveRadius {