cleaned up comments
This commit is contained in:
@@ -16,11 +16,9 @@ func GenerateBuildings(img *image.RGBA, width, height int, settings *Settings, r
|
|||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize random number generator
|
|
||||||
randSrc := rand.New(rand.NewSource(seed))
|
randSrc := rand.New(rand.NewSource(seed))
|
||||||
buildingColor := color.RGBA{R: 128, G: 128, B: 128, A: 255} // Gray color for buildings
|
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)
|
isWater := make(map[image.Point]bool)
|
||||||
for _, p := range allWaterPixels {
|
for _, p := range allWaterPixels {
|
||||||
isWater[p] = true
|
isWater[p] = true
|
||||||
@@ -31,13 +29,11 @@ func GenerateBuildings(img *image.RGBA, width, height int, settings *Settings, r
|
|||||||
isRoad[p] = true
|
isRoad[p] = true
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize building data structures
|
|
||||||
isBuilding := make(map[image.Point]bool)
|
isBuilding := make(map[image.Point]bool)
|
||||||
var buildings [][]image.Point
|
var buildings [][]image.Point
|
||||||
var allBuildingPixels []image.Point
|
var allBuildingPixels []image.Point
|
||||||
var anchorPoints []image.Point
|
var anchorPoints []image.Point
|
||||||
|
|
||||||
// Determine anchor points for building placement
|
|
||||||
if len(roadPixels) > 0 {
|
if len(roadPixels) > 0 {
|
||||||
anchorPoints = roadPixels
|
anchorPoints = roadPixels
|
||||||
} else {
|
} else {
|
||||||
@@ -64,7 +60,6 @@ func GenerateBuildings(img *image.RGBA, width, height int, settings *Settings, r
|
|||||||
return anchorPoints[i].X < anchorPoints[j].X
|
return anchorPoints[i].X < anchorPoints[j].X
|
||||||
})
|
})
|
||||||
|
|
||||||
// Collect all land points for random placement
|
|
||||||
landPoints := make([]image.Point, 0, width*height)
|
landPoints := make([]image.Point, 0, width*height)
|
||||||
for y := 0; y < height; y++ {
|
for y := 0; y < height; y++ {
|
||||||
for x := 0; x < width; x++ {
|
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
|
// Select an anchor point for the new building
|
||||||
var anchor image.Point
|
var anchor image.Point
|
||||||
if randSrc.Float64() > settings.BuildingDistribution/100.0 {
|
if randSrc.Float64() > settings.BuildingDistribution/100.0 {
|
||||||
// Place near roads or other existing features
|
|
||||||
anchor = anchorPoints[randSrc.Intn(len(anchorPoints))]
|
anchor = anchorPoints[randSrc.Intn(len(anchorPoints))]
|
||||||
} else {
|
} else {
|
||||||
// Place randomly on any available land
|
|
||||||
if len(landPoints) == 0 {
|
if len(landPoints) == 0 {
|
||||||
continue // No land to place buildings on
|
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.
|
// 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) {
|
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
|
complexity := settings.MinBuildingComplexity
|
||||||
if settings.BuildingComplexityRatio > randSrc.Float64()*100 {
|
if settings.BuildingComplexityRatio > randSrc.Float64()*100 {
|
||||||
complexity = settings.MinBuildingComplexity + randSrc.Intn(settings.MaxBuildingComplexity-settings.MinBuildingComplexity+1)
|
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
|
newCenter = center
|
||||||
buildingCenter = center
|
buildingCenter = center
|
||||||
} else {
|
} else {
|
||||||
// Place subsequent components near existing ones
|
|
||||||
prevShape := shapeDescriptions[randSrc.Intn(len(shapeDescriptions))]
|
prevShape := shapeDescriptions[randSrc.Intn(len(shapeDescriptions))]
|
||||||
angle := randSrc.Float64() * 2 * math.Pi
|
angle := randSrc.Float64() * 2 * math.Pi
|
||||||
dist := componentSize * (0.25 + randSrc.Float64()*0.5) // Overlap between 25% and 75%
|
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})
|
shapeDescriptions = append(shapeDescriptions, shapeDescription{shape, newCenter, componentSize})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find the bounding box of the unscaled building
|
|
||||||
var minX, minY, maxX, maxY int
|
var minX, minY, maxX, maxY int
|
||||||
for i, sd := range shapeDescriptions {
|
for i, sd := range shapeDescriptions {
|
||||||
halfSize := int(sd.size / 2)
|
halfSize := int(sd.size / 2)
|
||||||
@@ -252,7 +242,6 @@ func scalePixels(pixels []image.Point, finalSize float64) []image.Point {
|
|||||||
return pixels
|
return pixels
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find the bounding box of the pixels
|
|
||||||
minX, minY := pixels[0].X, pixels[0].Y
|
minX, minY := pixels[0].X, pixels[0].Y
|
||||||
maxX, maxY := pixels[0].X, pixels[0].Y
|
maxX, maxY := pixels[0].X, pixels[0].Y
|
||||||
for _, p := range pixels {
|
for _, p := range pixels {
|
||||||
@@ -274,7 +263,6 @@ func scalePixels(pixels []image.Point, finalSize float64) []image.Point {
|
|||||||
currentWidth := float64(maxX - minX)
|
currentWidth := float64(maxX - minX)
|
||||||
currentHeight := float64(maxY - minY)
|
currentHeight := float64(maxY - minY)
|
||||||
|
|
||||||
// Determine the scaling factor
|
|
||||||
scale := finalSize / math.Max(currentWidth, currentHeight)
|
scale := finalSize / math.Max(currentWidth, currentHeight)
|
||||||
|
|
||||||
// Calculate the center of the bounding box
|
// 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.
|
// chooseShape selects a building shape based on the provided ratios.
|
||||||
func chooseShape(randSrc *rand.Rand, ratios map[string]float64) string {
|
func chooseShape(randSrc *rand.Rand, ratios map[string]float64) string {
|
||||||
// Create a slice of shapes and their cumulative weights
|
|
||||||
var shapes []string
|
var shapes []string
|
||||||
var weights []float64
|
var weights []float64
|
||||||
var cumulativeWeight 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
|
// Generate a random number between 0 and the total weight
|
||||||
randNum := randSrc.Float64() * cumulativeWeight
|
randNum := randSrc.Float64() * cumulativeWeight
|
||||||
|
|
||||||
// Find the shape corresponding to the random number
|
|
||||||
for i, weight := range weights {
|
for i, weight := range weights {
|
||||||
if randNum < weight {
|
if randNum < weight {
|
||||||
return shapes[i]
|
return shapes[i]
|
||||||
@@ -407,7 +393,6 @@ func getBuildingPixels(center image.Point, size float64, shape string, isWater,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
case "rectangles":
|
case "rectangles":
|
||||||
// Create rectangles with varied aspect ratios
|
|
||||||
longSide := size
|
longSide := size
|
||||||
shortSide := randSrc.Float64()*(size-float64(halfSize)) + float64(halfSize)
|
shortSide := randSrc.Float64()*(size-float64(halfSize)) + float64(halfSize)
|
||||||
var w, h int
|
var w, h int
|
||||||
@@ -418,7 +403,6 @@ func getBuildingPixels(center image.Point, size float64, shape string, isWater,
|
|||||||
}
|
}
|
||||||
halfW, halfH := w/2, h/2
|
halfW, halfH := w/2, h/2
|
||||||
|
|
||||||
// Check for collisions and gather pixels
|
|
||||||
for y := center.Y - halfH; y <= center.Y+halfH; y++ {
|
for y := center.Y - halfH; y <= center.Y+halfH; y++ {
|
||||||
for x := center.X - halfW; x <= center.X+halfW; x++ {
|
for x := center.X - halfW; x <= center.X+halfW; x++ {
|
||||||
p := image.Point{X: x, Y: y}
|
p := image.Point{X: x, Y: y}
|
||||||
@@ -453,7 +437,6 @@ func FlattenBuildingAreas(heightMap *image.RGBA, buildings [][]image.Point, widt
|
|||||||
return heightMap
|
return heightMap
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a copy of the heightmap to avoid modifying the original during processing.
|
|
||||||
newHeightMap := image.NewRGBA(heightMap.Bounds())
|
newHeightMap := image.NewRGBA(heightMap.Bounds())
|
||||||
copy(newHeightMap.Pix, heightMap.Pix)
|
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)
|
newHeightMap.Set(p.X, p.Y, avgColor)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a buffer around the building.
|
|
||||||
buffer := make([]image.Point, 0)
|
buffer := make([]image.Point, 0)
|
||||||
for _, p := range building {
|
for _, p := range building {
|
||||||
for y := p.Y - 5; y <= p.Y+5; y++ {
|
for y := p.Y - 5; y <= p.Y+5; y++ {
|
||||||
|
|||||||
+4
-4
@@ -12,7 +12,7 @@ import (
|
|||||||
"fyne.io/fyne/v2/widget"
|
"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 {
|
type numericInputSlider struct {
|
||||||
widget.BaseWidget
|
widget.BaseWidget
|
||||||
value binding.Float
|
value binding.Float
|
||||||
@@ -52,7 +52,7 @@ func (r *numericInputSliderRenderer) Refresh() {
|
|||||||
|
|
||||||
func (r *numericInputSliderRenderer) Destroy() {}
|
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 {
|
func newNumericInputSlider(min, max float64, initialValue float64, format string, labelText string) *numericInputSlider {
|
||||||
s := &numericInputSlider{
|
s := &numericInputSlider{
|
||||||
min: min,
|
min: min,
|
||||||
@@ -80,7 +80,7 @@ func newNumericInputSlider(min, max float64, initialValue float64, format string
|
|||||||
return s
|
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)) {
|
func (s *numericInputSlider) validate(text string, onError func(bool)) {
|
||||||
text = strings.TrimSuffix(text, "px")
|
text = strings.TrimSuffix(text, "px")
|
||||||
text = strings.TrimSuffix(text, "%")
|
text = strings.TrimSuffix(text, "%")
|
||||||
@@ -104,7 +104,7 @@ func (s *numericInputSlider) validate(text string, onError func(bool)) {
|
|||||||
s.value.Set(val)
|
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 {
|
func (s *numericInputSlider) CreateRenderer() fyne.WidgetRenderer {
|
||||||
r := &numericInputSliderRenderer{
|
r := &numericInputSliderRenderer{
|
||||||
slider: s,
|
slider: s,
|
||||||
|
|||||||
@@ -11,81 +11,35 @@ import (
|
|||||||
"github.com/ojrac/opensimplex-go"
|
"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 {
|
type riverParams struct {
|
||||||
EdgeBandRatio float64 // fraction of river radius used for roughening band (e.g. 0.6)
|
EdgeBandRatio float64
|
||||||
RoughnessStrength float64 // 0..1 how aggressive clipping is at the edge
|
RoughnessStrength float64
|
||||||
IslandAttemptProb float64 // chance per-river to attempt islands
|
IslandAttemptProb float64
|
||||||
IslandSeedChance float64 // chance per-water-pixel to become an island seed candidate
|
IslandSeedChance float64
|
||||||
MinIslandSize int // minimum island pixel count
|
MinIslandSize int
|
||||||
MaxIslandSize int // maximum island pixel count
|
MaxIslandSize int
|
||||||
WaterLevelBias float64 // baseline water level in normalized height units [0..1]; small bias subtracted to favor water
|
WaterLevelBias float64
|
||||||
NoiseFrequency float64 // frequency for simplex noise
|
NoiseFrequency float64
|
||||||
KeepInnerFraction float64 // fraction of inner radius always kept as channel (0..1)
|
KeepInnerFraction float64
|
||||||
MaxWorkers int // concurrency limit (0 means runtime.NumCPU())
|
MaxWorkers int
|
||||||
MinWidthPx float64 // minimum river width in pixels (for sin wave amplitude calculation)
|
MinWidthPx float64
|
||||||
MaxWidthPx float64 // maximum river width in pixels (for sin wave amplitude calculation)
|
MaxWidthPx float64
|
||||||
}
|
}
|
||||||
|
|
||||||
// computeSinWaveEdgeOffset computes the radial offset for river edge roughening
|
// computeSinWaveEdgeOffset computes dual sine wave edge roughening for realistic river banks
|
||||||
// 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.
|
|
||||||
func computeSinWaveEdgeOffset(absX, absY int, largeAmplitude, smallAmplitude float64) float64 {
|
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
|
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
|
largeWave := math.Sin(positionPhase) * largeAmplitude
|
||||||
|
|
||||||
// Small wave: faster frequency for subtle and natural bank details
|
|
||||||
smallWave := math.Sin(positionPhase*3.5) * smallAmplitude
|
smallWave := math.Sin(positionPhase*3.5) * smallAmplitude
|
||||||
|
|
||||||
// Return combined offset
|
|
||||||
return largeWave + smallWave
|
return largeWave + smallWave
|
||||||
}
|
}
|
||||||
|
|
||||||
// RasterizeAndRoughenRiver rasterizes a river path, roughens edges using the heightmap and dual sin waves,
|
// RasterizeAndRoughenRiver rasterizes a river path with natural edge roughening and optional islands
|
||||||
// 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)
|
|
||||||
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 {
|
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 {
|
if canvas == nil || len(path) == 0 || riverWidthPx <= 0 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Default parameters - tweak as needed
|
|
||||||
params := riverParams{
|
params := riverParams{
|
||||||
EdgeBandRatio: 0.6,
|
EdgeBandRatio: 0.6,
|
||||||
RoughnessStrength: 0.65,
|
RoughnessStrength: 0.65,
|
||||||
@@ -95,7 +49,7 @@ func RasterizeAndRoughenRiver(canvas *image.RGBA, path []image.Point, riverWidth
|
|||||||
MaxIslandSize: 800,
|
MaxIslandSize: 800,
|
||||||
WaterLevelBias: 0.02,
|
WaterLevelBias: 0.02,
|
||||||
NoiseFrequency: 0.02,
|
NoiseFrequency: 0.02,
|
||||||
KeepInnerFraction: 0.85, // keep central 85% of radius
|
KeepInnerFraction: 0.85,
|
||||||
MaxWorkers: 0,
|
MaxWorkers: 0,
|
||||||
MinWidthPx: minWidthPx,
|
MinWidthPx: minWidthPx,
|
||||||
MaxWidthPx: maxWidthPx,
|
MaxWidthPx: maxWidthPx,
|
||||||
@@ -104,10 +58,8 @@ func RasterizeAndRoughenRiver(canvas *image.RGBA, path []image.Point, riverWidth
|
|||||||
bounds := canvas.Bounds()
|
bounds := canvas.Bounds()
|
||||||
imgW, imgH := bounds.Dx(), bounds.Dy()
|
imgW, imgH := bounds.Dx(), bounds.Dy()
|
||||||
|
|
||||||
// Precompute normalized height grid for faster sampling.
|
|
||||||
heightGrid := precomputeHeightGrid(heightmap, imgW, imgH)
|
heightGrid := precomputeHeightGrid(heightmap, imgW, imgH)
|
||||||
|
|
||||||
// Compute bounding box for path expanded by radius + edge band
|
|
||||||
radius := riverWidthPx / 2.0
|
radius := riverWidthPx / 2.0
|
||||||
edgeBand := radius * params.EdgeBandRatio
|
edgeBand := radius * params.EdgeBandRatio
|
||||||
expand := int(math.Ceil(radius + edgeBand + 2))
|
expand := int(math.Ceil(radius + edgeBand + 2))
|
||||||
@@ -151,11 +103,8 @@ func RasterizeAndRoughenRiver(canvas *image.RGBA, path []image.Point, riverWidth
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create base raster mask inside bounding box.
|
|
||||||
// baseMask[i] == 1 means inside nominal river radius (before roughening).
|
|
||||||
baseMask := make([]uint8, bw*bh)
|
baseMask := make([]uint8, bw*bh)
|
||||||
|
|
||||||
// Rasterize simple circular stamping for each center point into baseMask
|
|
||||||
radiusSq := radius * radius
|
radiusSq := radius * radius
|
||||||
for _, c := range path {
|
for _, c := range path {
|
||||||
cx := c.X - minX
|
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)
|
dist := chamferDistanceField(baseMask, bw, bh)
|
||||||
|
|
||||||
// Prepare noise generator
|
|
||||||
noise := opensimplex.New(seed)
|
noise := opensimplex.New(seed)
|
||||||
noiseFreq := params.NoiseFrequency
|
noiseFreq := params.NoiseFrequency
|
||||||
|
|
||||||
// Determine inner keep radius (always keep central channel)
|
|
||||||
innerKeepRadius := radius * params.KeepInnerFraction
|
innerKeepRadius := radius * params.KeepInnerFraction
|
||||||
|
|
||||||
// Prepare final mask
|
|
||||||
finalMask := make([]uint8, bw*bh)
|
finalMask := make([]uint8, bw*bh)
|
||||||
|
|
||||||
// Concurrency setup
|
|
||||||
workers := params.MaxWorkers
|
workers := params.MaxWorkers
|
||||||
if workers <= 0 {
|
if workers <= 0 {
|
||||||
workers = runtime.NumCPU()
|
workers = runtime.NumCPU()
|
||||||
@@ -200,18 +144,13 @@ func RasterizeAndRoughenRiver(canvas *image.RGBA, path []image.Point, riverWidth
|
|||||||
rowsPerWorker := (bh + workers - 1) / workers
|
rowsPerWorker := (bh + workers - 1) / workers
|
||||||
randBase := rand.New(rand.NewSource(seed))
|
randBase := rand.New(rand.NewSource(seed))
|
||||||
|
|
||||||
// Precompute some weights for the decision formula
|
|
||||||
heightWeight := 2.0 * params.RoughnessStrength
|
heightWeight := 2.0 * params.RoughnessStrength
|
||||||
distWeight := params.RoughnessStrength
|
distWeight := params.RoughnessStrength
|
||||||
noiseWeight := 0.5 * 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
|
largeAmplitude := params.MaxWidthPx - params.MinWidthPx
|
||||||
smallAmplitude := largeAmplitude / 4.0
|
smallAmplitude := largeAmplitude / 4.0
|
||||||
|
|
||||||
// Evaluate per-pixel decision in parallel
|
|
||||||
for wi := 0; wi < workers; wi++ {
|
for wi := 0; wi < workers; wi++ {
|
||||||
startY := wi * rowsPerWorker
|
startY := wi * rowsPerWorker
|
||||||
endY := startY + rowsPerWorker
|
endY := startY + rowsPerWorker
|
||||||
@@ -228,11 +167,8 @@ func RasterizeAndRoughenRiver(canvas *image.RGBA, path []image.Point, riverWidth
|
|||||||
for y := startY; y < endY; y++ {
|
for y := startY; y < endY; y++ {
|
||||||
for x := 0; x < bw; x++ {
|
for x := 0; x < bw; x++ {
|
||||||
idx := y*bw + x
|
idx := y*bw + x
|
||||||
// If already inside base mask, candidate for water
|
|
||||||
if baseMask[idx] == 1 {
|
if baseMask[idx] == 1 {
|
||||||
// If within inner keep radius: keep always
|
|
||||||
d := dist[idx]
|
d := dist[idx]
|
||||||
// dist is approximate pixels; we compare to innerKeepRadius
|
|
||||||
absX := x + minX
|
absX := x + minX
|
||||||
absY := y + minY
|
absY := y + minY
|
||||||
if d <= float32(innerKeepRadius) {
|
if d <= float32(innerKeepRadius) {
|
||||||
@@ -240,12 +176,9 @@ func RasterizeAndRoughenRiver(canvas *image.RGBA, path []image.Point, riverWidth
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply sin wave offset for realistic edge roughening
|
|
||||||
sinWaveOffset := computeSinWaveEdgeOffset(absX, absY, largeAmplitude, smallAmplitude)
|
sinWaveOffset := computeSinWaveEdgeOffset(absX, absY, largeAmplitude, smallAmplitude)
|
||||||
effectiveInnerRadius := innerKeepRadius + sinWaveOffset
|
effectiveInnerRadius := innerKeepRadius + sinWaveOffset
|
||||||
|
|
||||||
// Compute influences
|
|
||||||
// normalizedDist: 0 at effectiveInnerRadius, 1 at effectiveInnerRadius + edgeBand
|
|
||||||
normDist := float64((float32(d) - float32(effectiveInnerRadius)) / float32(edgeBand))
|
normDist := float64((float32(d) - float32(effectiveInnerRadius)) / float32(edgeBand))
|
||||||
if normDist < 0 {
|
if normDist < 0 {
|
||||||
normDist = 0
|
normDist = 0
|
||||||
@@ -254,18 +187,15 @@ func RasterizeAndRoughenRiver(canvas *image.RGBA, path []image.Point, riverWidth
|
|||||||
normDist = 1
|
normDist = 1
|
||||||
}
|
}
|
||||||
|
|
||||||
heightVal := sampleHeightGrid(heightGrid, imgW, imgH, absX, absY) // 0..1
|
heightVal := sampleHeightGrid(heightGrid, imgW, imgH, absX, absY)
|
||||||
// Apply bias so slightly lower areas favor water
|
|
||||||
heightAdj := float64(heightVal) - params.WaterLevelBias
|
heightAdj := float64(heightVal) - params.WaterLevelBias
|
||||||
|
|
||||||
noiseVal := noise.Eval2(float64(absX)*noiseFreq, float64(absY)*noiseFreq) // -1 .. 1
|
noiseVal := noise.Eval2(float64(absX)*noiseFreq, float64(absY)*noiseFreq)
|
||||||
noiseNorm := (noiseVal + 1.0) / 2.0 // 0..1
|
noiseNorm := (noiseVal + 1.0) / 2.0
|
||||||
|
|
||||||
score := distWeight*normDist + heightWeight*heightAdj + noiseWeight*(noiseNorm-0.5)
|
score := distWeight*normDist + heightWeight*heightAdj + noiseWeight*(noiseNorm-0.5)
|
||||||
|
|
||||||
// Decision threshold: higher score means more likely land.
|
|
||||||
threshold := 0.35 + 0.5*params.RoughnessStrength
|
threshold := 0.35 + 0.5*params.RoughnessStrength
|
||||||
// Small stochastic factor to add natural variance
|
|
||||||
if localRand.Float64() < 0.0005 {
|
if localRand.Float64() < 0.0005 {
|
||||||
score += (localRand.Float64() - 0.5) * 0.2
|
score += (localRand.Float64() - 0.5) * 0.2
|
||||||
}
|
}
|
||||||
@@ -282,14 +212,12 @@ func RasterizeAndRoughenRiver(canvas *image.RGBA, path []image.Point, riverWidth
|
|||||||
}
|
}
|
||||||
wg.Wait()
|
wg.Wait()
|
||||||
|
|
||||||
// Optionally attempt islands with small probability
|
|
||||||
randForIsland := rand.New(rand.NewSource(seed + 1234567))
|
randForIsland := rand.New(rand.NewSource(seed + 1234567))
|
||||||
tryIslands := randForIsland.Float64() < params.IslandAttemptProb
|
tryIslands := randForIsland.Float64() < params.IslandAttemptProb
|
||||||
if tryIslands {
|
if tryIslands {
|
||||||
generateIslandsInMask(finalMask, bw, bh, minX, minY, heightGrid, imgW, imgH, ¶ms, seed+4242)
|
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
|
var added []image.Point
|
||||||
for y := 0; y < bh; y++ {
|
for y := 0; y < bh; y++ {
|
||||||
absY := y + minY
|
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)
|
removeSpeckles(&finalMask, bw, bh, 2)
|
||||||
|
|
||||||
return added
|
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 {
|
func precomputeHeightGrid(hmap image.Image, width, height int) []float32 {
|
||||||
out := make([]float32, width*height)
|
out := make([]float32, width*height)
|
||||||
if hmap == nil {
|
if hmap == nil {
|
||||||
// default flat
|
|
||||||
for i := range out {
|
for i := range out {
|
||||||
out[i] = 0.5
|
out[i] = 0.5
|
||||||
}
|
}
|
||||||
@@ -337,7 +263,7 @@ func precomputeHeightGrid(hmap image.Image, width, height int) []float32 {
|
|||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
// sampleHeightGrid safe accessor
|
// sampleHeightGrid safely samples height at coordinates
|
||||||
func sampleHeightGrid(grid []float32, width, height, x, y int) float32 {
|
func sampleHeightGrid(grid []float32, width, height, x, y int) float32 {
|
||||||
if x < 0 || x >= width || y < 0 || y >= height {
|
if x < 0 || x >= width || y < 0 || y >= height {
|
||||||
return 0.5
|
return 0.5
|
||||||
@@ -345,14 +271,11 @@ func sampleHeightGrid(grid []float32, width, height, x, y int) float32 {
|
|||||||
return grid[y*width+x]
|
return grid[y*width+x]
|
||||||
}
|
}
|
||||||
|
|
||||||
// chamferDistanceField computes a fast approximate distance (in pixels) from any pixel to the nearest
|
// chamferDistanceField computes fast approximate distance from any pixel to centerline
|
||||||
// baseMask==1 pixel. Distance is zero for pixels inside baseMask.
|
|
||||||
// This is a two-pass chamfer approximation (float), cheap and parallel friendly.
|
|
||||||
func chamferDistanceField(baseMask []uint8, w, h int) []float32 {
|
func chamferDistanceField(baseMask []uint8, w, h int) []float32 {
|
||||||
const maxF = 1e6
|
const maxF = 1e6
|
||||||
dist := make([]float32, w*h)
|
dist := make([]float32, w*h)
|
||||||
|
|
||||||
// Initialize
|
|
||||||
for i := 0; i < w*h; i++ {
|
for i := 0; i < w*h; i++ {
|
||||||
if baseMask[i] == 1 {
|
if baseMask[i] == 1 {
|
||||||
dist[i] = 0
|
dist[i] = 0
|
||||||
@@ -368,28 +291,24 @@ func chamferDistanceField(baseMask []uint8, w, h int) []float32 {
|
|||||||
if dist[i] == 0 {
|
if dist[i] == 0 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
// check left
|
|
||||||
if x > 0 {
|
if x > 0 {
|
||||||
v := dist[i-1] + 1.0
|
v := dist[i-1] + 1.0
|
||||||
if v < dist[i] {
|
if v < dist[i] {
|
||||||
dist[i] = v
|
dist[i] = v
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// check top
|
|
||||||
if y > 0 {
|
if y > 0 {
|
||||||
v := dist[i-w] + 1.0
|
v := dist[i-w] + 1.0
|
||||||
if v < dist[i] {
|
if v < dist[i] {
|
||||||
dist[i] = v
|
dist[i] = v
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// check top-left
|
|
||||||
if x > 0 && y > 0 {
|
if x > 0 && y > 0 {
|
||||||
v := dist[i-w-1] + 1.41421356
|
v := dist[i-w-1] + 1.41421356
|
||||||
if v < dist[i] {
|
if v < dist[i] {
|
||||||
dist[i] = v
|
dist[i] = v
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// check top-right
|
|
||||||
if x < w-1 && y > 0 {
|
if x < w-1 && y > 0 {
|
||||||
v := dist[i-w+1] + 1.41421356
|
v := dist[i-w+1] + 1.41421356
|
||||||
if v < dist[i] {
|
if v < dist[i] {
|
||||||
@@ -403,28 +322,24 @@ func chamferDistanceField(baseMask []uint8, w, h int) []float32 {
|
|||||||
for y := h - 1; y >= 0; y-- {
|
for y := h - 1; y >= 0; y-- {
|
||||||
for x := w - 1; x >= 0; x-- {
|
for x := w - 1; x >= 0; x-- {
|
||||||
i := y*w + x
|
i := y*w + x
|
||||||
// check right
|
|
||||||
if x < w-1 {
|
if x < w-1 {
|
||||||
v := dist[i+1] + 1.0
|
v := dist[i+1] + 1.0
|
||||||
if v < dist[i] {
|
if v < dist[i] {
|
||||||
dist[i] = v
|
dist[i] = v
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// check bottom
|
|
||||||
if y < h-1 {
|
if y < h-1 {
|
||||||
v := dist[i+w] + 1.0
|
v := dist[i+w] + 1.0
|
||||||
if v < dist[i] {
|
if v < dist[i] {
|
||||||
dist[i] = v
|
dist[i] = v
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// check bottom-right
|
|
||||||
if x < w-1 && y < h-1 {
|
if x < w-1 && y < h-1 {
|
||||||
v := dist[i+w+1] + 1.41421356
|
v := dist[i+w+1] + 1.41421356
|
||||||
if v < dist[i] {
|
if v < dist[i] {
|
||||||
dist[i] = v
|
dist[i] = v
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// check bottom-left
|
|
||||||
if x > 0 && y < h-1 {
|
if x > 0 && y < h-1 {
|
||||||
v := dist[i+w-1] + 1.41421356
|
v := dist[i+w-1] + 1.41421356
|
||||||
if v < dist[i] {
|
if v < dist[i] {
|
||||||
@@ -437,15 +352,9 @@ func chamferDistanceField(baseMask []uint8, w, h int) []float32 {
|
|||||||
return dist
|
return dist
|
||||||
}
|
}
|
||||||
|
|
||||||
// generateIslandsInMask will attempt to create small islands inside contiguous water areas.
|
// generateIslandsInMask creates small islands inside 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
|
|
||||||
func generateIslandsInMask(mask []uint8, bw, bh, minX, minY int, heightGrid []float32, fullW, fullH int, params *riverParams, seed int64) {
|
func generateIslandsInMask(mask []uint8, bw, bh, minX, minY int, heightGrid []float32, fullW, fullH int, params *riverParams, seed int64) {
|
||||||
r := rand.New(rand.NewSource(seed))
|
r := rand.New(rand.NewSource(seed))
|
||||||
// Collect candidates
|
|
||||||
type pt struct{ x, y int }
|
type pt struct{ x, y int }
|
||||||
candidates := make([]pt, 0)
|
candidates := make([]pt, 0)
|
||||||
for y := 0; y < bh; y++ {
|
for y := 0; y < bh; y++ {
|
||||||
@@ -457,7 +366,6 @@ func generateIslandsInMask(mask []uint8, bw, bh, minX, minY int, heightGrid []fl
|
|||||||
absX := x + minX
|
absX := x + minX
|
||||||
absY := y + minY
|
absY := y + minY
|
||||||
hv := sampleHeightGrid(heightGrid, fullW, fullH, absX, absY)
|
hv := sampleHeightGrid(heightGrid, fullW, fullH, absX, absY)
|
||||||
// candidate if slightly higher than local water bias
|
|
||||||
if float64(hv) > params.WaterLevelBias+0.03 {
|
if float64(hv) > params.WaterLevelBias+0.03 {
|
||||||
if r.Float64() < params.IslandSeedChance {
|
if r.Float64() < params.IslandSeedChance {
|
||||||
candidates = append(candidates, pt{x, y})
|
candidates = append(candidates, pt{x, y})
|
||||||
@@ -469,7 +377,6 @@ func generateIslandsInMask(mask []uint8, bw, bh, minX, minY int, heightGrid []fl
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Shuffle candidates to randomize island placement
|
|
||||||
r.Shuffle(len(candidates), func(i, j int) { candidates[i], candidates[j] = candidates[j], candidates[i] })
|
r.Shuffle(len(candidates), func(i, j int) { candidates[i], candidates[j] = candidates[j], candidates[i] })
|
||||||
|
|
||||||
visited := make([]uint8, bw*bh)
|
visited := make([]uint8, bw*bh)
|
||||||
@@ -479,10 +386,8 @@ func generateIslandsInMask(mask []uint8, bw, bh, minX, minY int, heightGrid []fl
|
|||||||
if visited[ci] != 0 {
|
if visited[ci] != 0 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
// BFS grow island
|
|
||||||
maxSize := params.MaxIslandSize
|
maxSize := params.MaxIslandSize
|
||||||
minSize := params.MinIslandSize
|
minSize := params.MinIslandSize
|
||||||
// randomize size a bit
|
|
||||||
targetSize := minSize + r.Intn(maxSize-minSize+1)
|
targetSize := minSize + r.Intn(maxSize-minSize+1)
|
||||||
|
|
||||||
queue := []pt{{c.x, c.y}}
|
queue := []pt{{c.x, c.y}}
|
||||||
@@ -494,13 +399,11 @@ func generateIslandsInMask(mask []uint8, bw, bh, minX, minY int, heightGrid []fl
|
|||||||
p := queue[qi]
|
p := queue[qi]
|
||||||
absX := p.x + minX
|
absX := p.x + minX
|
||||||
absY := p.y + minY
|
absY := p.y + minY
|
||||||
// Height constraint: island must be above a modest threshold
|
|
||||||
hv := sampleHeightGrid(heightGrid, fullW, fullH, absX, absY)
|
hv := sampleHeightGrid(heightGrid, fullW, fullH, absX, absY)
|
||||||
if float64(hv) < params.WaterLevelBias+0.01 {
|
if float64(hv) < params.WaterLevelBias+0.01 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
island = append(island, p)
|
island = append(island, p)
|
||||||
// Expand
|
|
||||||
for dy := -1; dy <= 1; dy++ {
|
for dy := -1; dy <= 1; dy++ {
|
||||||
for dx := -1; dx <= 1; dx++ {
|
for dx := -1; dx <= 1; dx++ {
|
||||||
nx, ny := p.x+dx, p.y+dy
|
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 {
|
if visited[nidx] != 0 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
// Only grow into water pixels
|
|
||||||
if mask[nidx] != 1 {
|
if mask[nidx] != 1 {
|
||||||
continue
|
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 {
|
if touchesEdge {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// size check
|
|
||||||
if len(island) < minSize {
|
if len(island) < minSize {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// Carve the island: set mask pixels to 0 (land)
|
|
||||||
for _, p := range island {
|
for _, p := range island {
|
||||||
mask[p.y*bw+p.x] = 0
|
mask[p.y*bw+p.x] = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
// Optionally stop after creating a few islands to keep them rare
|
|
||||||
if r.Float64() < 0.7 {
|
if r.Float64() < 0.7 {
|
||||||
// keep creating more sometimes, break otherwise
|
|
||||||
if r.Intn(3) == 0 {
|
if r.Intn(3) == 0 {
|
||||||
break
|
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).
|
// removeSpeckles removes tiny isolated water pixels
|
||||||
// This is a simple pass that clears pixels that have fewer than minNeighbors water neighbors.
|
|
||||||
func removeSpeckles(mask *[]uint8, bw, bh, minNeighbors int) {
|
func removeSpeckles(mask *[]uint8, bw, bh, minNeighbors int) {
|
||||||
arr := *mask
|
arr := *mask
|
||||||
out := make([]uint8, len(arr))
|
out := make([]uint8, len(arr))
|
||||||
@@ -584,7 +480,7 @@ func removeSpeckles(mask *[]uint8, bw, bh, minNeighbors int) {
|
|||||||
*mask = arr
|
*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 {
|
func maskToPoints(mask []uint8, bw, bh, minX, minY int) []image.Point {
|
||||||
var pts []image.Point
|
var pts []image.Point
|
||||||
for y := 0; y < bh; y++ {
|
for y := 0; y < bh; y++ {
|
||||||
@@ -597,7 +493,7 @@ func maskToPoints(mask []uint8, bw, bh, minX, minY int) []image.Point {
|
|||||||
return pts
|
return pts
|
||||||
}
|
}
|
||||||
|
|
||||||
// small clamp helpers
|
// clamp01 clamps value to 0..1 range
|
||||||
func clamp01(v float64) float64 {
|
func clamp01(v float64) float64 {
|
||||||
if v < 0 {
|
if v < 0 {
|
||||||
return 0
|
return 0
|
||||||
|
|||||||
@@ -11,20 +11,20 @@ import (
|
|||||||
"unsafe"
|
"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 {
|
type PointOfInterest struct {
|
||||||
X, Y int
|
X, Y int
|
||||||
Connections int
|
Connections int
|
||||||
IsExit bool
|
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 {
|
type PathPoint struct {
|
||||||
Point image.Point
|
Point image.Point
|
||||||
IsBridge bool
|
IsBridge bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// Road represents a connection between two Points of Interest.
|
// Road represents a connection between two Points of Interest
|
||||||
type Road struct {
|
type Road struct {
|
||||||
Start, End *PointOfInterest
|
Start, End *PointOfInterest
|
||||||
Width int
|
Width int
|
||||||
@@ -32,9 +32,8 @@ type Road struct {
|
|||||||
Importance int
|
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) {
|
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))
|
img := image.NewRGBA(image.Rect(0, 0, width, height))
|
||||||
for y := 0; y < height; y++ {
|
for y := 0; y < height; y++ {
|
||||||
for x := 0; x < width; x++ {
|
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))
|
randSrc := rand.New(rand.NewSource(seed))
|
||||||
roadColor := color.RGBA{R: 139, G: 69, B: 19, A: 255}
|
roadColor := color.RGBA{R: 139, G: 69, B: 19, A: 255}
|
||||||
bridgeColor := color.RGBA{R: 60, G: 42, B: 33, 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)
|
pois := generatePOIs(width, height, settings, allWaterPixels, randSrc)
|
||||||
if len(pois) == 0 {
|
if len(pois) == 0 {
|
||||||
return nil, nil, img
|
return nil, nil, img
|
||||||
}
|
}
|
||||||
|
|
||||||
// Step 4: Connect POIs to form roads
|
|
||||||
roads := connectPOIs(pois, width, height, settings, randSrc, allWaterPixels)
|
roads := connectPOIs(pois, width, height, settings, randSrc, allWaterPixels)
|
||||||
// Step 5: Assign widths to the roads based on their importance
|
|
||||||
assignRoadWidths(roads, settings)
|
assignRoadWidths(roads, settings)
|
||||||
|
|
||||||
// Step 6: Draw the roads on the image
|
|
||||||
var allRoadPixels []image.Point
|
var allRoadPixels []image.Point
|
||||||
var allBridgePixels []image.Point
|
var allBridgePixels []image.Point
|
||||||
for _, road := range roads {
|
for _, road := range roads {
|
||||||
@@ -70,7 +64,7 @@ func GenerateRoads(width, height int, settings *Settings, noiseImg image.Image,
|
|||||||
return allRoadPixels, allBridgePixels, img
|
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 {
|
func generatePOIs(width, height int, settings *Settings, allWaterPixels []image.Point, randSrc *rand.Rand) []*PointOfInterest {
|
||||||
numPOIs := settings.NumRoads / 2
|
numPOIs := settings.NumRoads / 2
|
||||||
if numPOIs == 0 {
|
if numPOIs == 0 {
|
||||||
@@ -91,33 +85,30 @@ func generatePOIs(width, height int, settings *Settings, allWaterPixels []image.
|
|||||||
centerX := width / 2
|
centerX := width / 2
|
||||||
centerY := height / 2
|
centerY := height / 2
|
||||||
|
|
||||||
// Distribution affects the radius of POI generation
|
|
||||||
maxRadius := math.Min(float64(width)/2, float64(height)/2)
|
maxRadius := math.Min(float64(width)/2, float64(height)/2)
|
||||||
radius := maxRadius * (settings.RoadDistribution / 100.0)
|
radius := maxRadius * (settings.RoadDistribution / 100.0)
|
||||||
|
|
||||||
for i := 0; i < numPOIs; i++ {
|
for i := 0; i < numPOIs; i++ {
|
||||||
var x, y int
|
var x, y int
|
||||||
found := false
|
found := false
|
||||||
for j := 0; j < 100; j++ { // Retries to find a land spot
|
for j := 0; j < 100; j++ {
|
||||||
if i < numExits {
|
if i < numExits {
|
||||||
// Create POIs at the map edges
|
|
||||||
side := randSrc.Intn(4)
|
side := randSrc.Intn(4)
|
||||||
switch side {
|
switch side {
|
||||||
case 0: // Top
|
case 0:
|
||||||
x = randSrc.Intn(width)
|
x = randSrc.Intn(width)
|
||||||
y = 0
|
y = 0
|
||||||
case 1: // Bottom
|
case 1:
|
||||||
x = randSrc.Intn(width)
|
x = randSrc.Intn(width)
|
||||||
y = height - 1
|
y = height - 1
|
||||||
case 2: // Left
|
case 2:
|
||||||
x = 0
|
x = 0
|
||||||
y = randSrc.Intn(height)
|
y = randSrc.Intn(height)
|
||||||
case 3: // Right
|
case 3:
|
||||||
x = width - 1
|
x = width - 1
|
||||||
y = randSrc.Intn(height)
|
y = randSrc.Intn(height)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Create POIs within the map
|
|
||||||
angle := randSrc.Float64() * 2 * math.Pi
|
angle := randSrc.Float64() * 2 * math.Pi
|
||||||
r := math.Sqrt(randSrc.Float64()) * radius
|
r := math.Sqrt(randSrc.Float64()) * radius
|
||||||
x = int(float64(centerX) + r*math.Cos(angle))
|
x = int(float64(centerX) + r*math.Cos(angle))
|
||||||
@@ -138,7 +129,7 @@ func generatePOIs(width, height int, settings *Settings, allWaterPixels []image.
|
|||||||
return pois
|
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 {
|
func connectPOIs(pois []*PointOfInterest, width, height int, settings *Settings, randSrc *rand.Rand, allWaterPixels []image.Point) []*Road {
|
||||||
if len(pois) < 2 {
|
if len(pois) < 2 {
|
||||||
return nil
|
return nil
|
||||||
@@ -151,7 +142,6 @@ func connectPOIs(pois []*PointOfInterest, width, height int, settings *Settings,
|
|||||||
visited := make(map[*PointOfInterest]bool)
|
visited := make(map[*PointOfInterest]bool)
|
||||||
existingRoads := make(map[string]bool)
|
existingRoads := make(map[string]bool)
|
||||||
|
|
||||||
// Find the center-most POI to start connecting from
|
|
||||||
centerX := width / 2
|
centerX := width / 2
|
||||||
centerY := height / 2
|
centerY := height / 2
|
||||||
var startNode *PointOfInterest
|
var startNode *PointOfInterest
|
||||||
@@ -174,11 +164,9 @@ func connectPOIs(pois []*PointOfInterest, width, height int, settings *Settings,
|
|||||||
|
|
||||||
visited[startNode] = true
|
visited[startNode] = true
|
||||||
|
|
||||||
// Use average dimension for controlling road path calculation
|
|
||||||
avgDim := float64(width+height) / 2.0
|
avgDim := float64(width+height) / 2.0
|
||||||
numControlPoints := max(int(avgDim*0.03), 60)
|
numControlPoints := max(int(avgDim*0.03), 60)
|
||||||
|
|
||||||
// Connect all POIs using a minimum spanning tree-like algorithm
|
|
||||||
for len(visited) < len(pois) {
|
for len(visited) < len(pois) {
|
||||||
var closest *PointOfInterest
|
var closest *PointOfInterest
|
||||||
var fromNode *PointOfInterest
|
var fromNode *PointOfInterest
|
||||||
@@ -192,7 +180,6 @@ func connectPOIs(pois []*PointOfInterest, width, height int, settings *Settings,
|
|||||||
if !visited[other] {
|
if !visited[other] {
|
||||||
dist := math.Sqrt(math.Pow(float64(poi.X-other.X), 2) + math.Pow(float64(poi.Y-other.Y), 2))
|
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)
|
key := fmt.Sprintf("%p-%p", poi, other)
|
||||||
if uintptr(unsafe.Pointer(poi)) > uintptr(unsafe.Pointer(other)) {
|
if uintptr(unsafe.Pointer(poi)) > uintptr(unsafe.Pointer(other)) {
|
||||||
key = fmt.Sprintf("%p-%p", other, poi)
|
key = fmt.Sprintf("%p-%p", other, poi)
|
||||||
@@ -201,7 +188,6 @@ func connectPOIs(pois []*PointOfInterest, width, height int, settings *Settings,
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// Avoid connecting two exit points directly
|
|
||||||
if poi.IsExit && other.IsExit {
|
if poi.IsExit && other.IsExit {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -220,7 +206,6 @@ func connectPOIs(pois []*PointOfInterest, width, height int, settings *Settings,
|
|||||||
fromNode.Connections++
|
fromNode.Connections++
|
||||||
closest.Connections++
|
closest.Connections++
|
||||||
|
|
||||||
// Add road to existing roads map to prevent duplicates
|
|
||||||
key := fmt.Sprintf("%p-%p", fromNode, closest)
|
key := fmt.Sprintf("%p-%p", fromNode, closest)
|
||||||
if uintptr(unsafe.Pointer(fromNode)) > uintptr(unsafe.Pointer(closest)) {
|
if uintptr(unsafe.Pointer(fromNode)) > uintptr(unsafe.Pointer(closest)) {
|
||||||
key = fmt.Sprintf("%p-%p", closest, fromNode)
|
key = fmt.Sprintf("%p-%p", closest, fromNode)
|
||||||
@@ -238,7 +223,6 @@ func connectPOIs(pois []*PointOfInterest, width, height int, settings *Settings,
|
|||||||
}
|
}
|
||||||
}(fromNode, closest)
|
}(fromNode, closest)
|
||||||
} else {
|
} else {
|
||||||
// No more reachable POIs, break the loop
|
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -252,7 +236,6 @@ func connectPOIs(pois []*PointOfInterest, width, height int, settings *Settings,
|
|||||||
roads = append(roads, road)
|
roads = append(roads, road)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate road importance based on the number of connections at its endpoints
|
|
||||||
for _, road := range roads {
|
for _, road := range roads {
|
||||||
road.Importance = road.Start.Connections + road.End.Connections
|
road.Importance = road.Start.Connections + road.End.Connections
|
||||||
}
|
}
|
||||||
@@ -260,13 +243,12 @@ func connectPOIs(pois []*PointOfInterest, width, height int, settings *Settings,
|
|||||||
return roads
|
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) {
|
func assignRoadWidths(roads []*Road, settings *Settings) {
|
||||||
if len(roads) == 0 {
|
if len(roads) == 0 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sort roads by importance in descending order
|
|
||||||
sort.Slice(roads, func(i, j int) bool {
|
sort.Slice(roads, func(i, j int) bool {
|
||||||
return roads[i].Importance > roads[j].Importance
|
return roads[i].Importance > roads[j].Importance
|
||||||
})
|
})
|
||||||
@@ -278,13 +260,12 @@ func assignRoadWidths(roads []*Road, settings *Settings) {
|
|||||||
widthStep = (maxWidth - minWidth) / float64(len(roads)-1)
|
widthStep = (maxWidth - minWidth) / float64(len(roads)-1)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Assign widths, with more important roads being wider
|
|
||||||
for i, road := range roads {
|
for i, road := range roads {
|
||||||
road.Width = int(maxWidth - float64(i)*widthStep)
|
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) {
|
func drawRoad(img *image.RGBA, points []PathPoint, roadColor, bridgeColor color.Color, width int) ([]image.Point, []image.Point) {
|
||||||
var roadPixels []image.Point
|
var roadPixels []image.Point
|
||||||
var bridgePixels []image.Point
|
var bridgePixels []image.Point
|
||||||
@@ -306,7 +287,7 @@ func drawRoad(img *image.RGBA, points []PathPoint, roadColor, bridgeColor color.
|
|||||||
return roadPixels, bridgePixels
|
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 {
|
func bresenhamRoad(path []image.Point) []image.Point {
|
||||||
if len(path) < 2 {
|
if len(path) < 2 {
|
||||||
return path
|
return path
|
||||||
@@ -346,7 +327,7 @@ func bresenhamRoad(path []image.Point) []image.Point {
|
|||||||
return fullPath
|
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 {
|
func calculateRoadPath(start, end *PointOfInterest, curvyness, avgDim float64, randSrc *rand.Rand, numControlPoints int, allWaterPixels []image.Point) []PathPoint {
|
||||||
dx := end.X - start.X
|
dx := end.X - start.X
|
||||||
dy := end.Y - start.Y
|
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}]}}
|
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))
|
distanceFactor := math.Min(1.0, dist/(avgDim*0.5))
|
||||||
adjustedCurvyness := curvyness * distanceFactor
|
adjustedCurvyness := curvyness * distanceFactor
|
||||||
|
|
||||||
@@ -374,7 +354,6 @@ func calculateRoadPath(start, end *PointOfInterest, curvyness, avgDim float64, r
|
|||||||
return pathPoints
|
return pathPoints
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use sine waves to create curves in the road
|
|
||||||
type wave struct {
|
type wave struct {
|
||||||
amplitude float64
|
amplitude float64
|
||||||
numWaves float64
|
numWaves float64
|
||||||
@@ -389,21 +368,18 @@ func calculateRoadPath(start, end *PointOfInterest, curvyness, avgDim float64, r
|
|||||||
}
|
}
|
||||||
baseNumWaves := (dist / mainWavelength) * adjustedCurvyness
|
baseNumWaves := (dist / mainWavelength) * adjustedCurvyness
|
||||||
|
|
||||||
// Main wave for overall curve
|
|
||||||
waves[0] = wave{
|
waves[0] = wave{
|
||||||
amplitude: amp,
|
amplitude: amp,
|
||||||
numWaves: baseNumWaves * (0.75 + randSrc.Float64()*0.5),
|
numWaves: baseNumWaves * (0.75 + randSrc.Float64()*0.5),
|
||||||
phase: randSrc.Float64() * 2 * math.Pi,
|
phase: randSrc.Float64() * 2 * math.Pi,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Smaller wave for minor detours and a more natural look
|
|
||||||
waves[1] = wave{
|
waves[1] = wave{
|
||||||
amplitude: amp / 4,
|
amplitude: amp / 4,
|
||||||
numWaves: baseNumWaves * 4 * (0.75 + randSrc.Float64()*0.5),
|
numWaves: baseNumWaves * 4 * (0.75 + randSrc.Float64()*0.5),
|
||||||
phase: randSrc.Float64() * 2 * math.Pi,
|
phase: randSrc.Float64() * 2 * math.Pi,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate control points for the curve
|
|
||||||
controlPoints := make([]image.Point, numControlPoints+1)
|
controlPoints := make([]image.Point, numControlPoints+1)
|
||||||
for i := 0; i <= numControlPoints; i++ {
|
for i := 0; i <= numControlPoints; i++ {
|
||||||
t := float64(i) / float64(numControlPoints)
|
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))}
|
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)
|
points := bresenhamRoad(controlPoints)
|
||||||
pathPoints := make([]PathPoint, len(points))
|
pathPoints := make([]PathPoint, len(points))
|
||||||
for i, p := range points {
|
for i, p := range points {
|
||||||
@@ -435,7 +410,7 @@ func calculateRoadPath(start, end *PointOfInterest, curvyness, avgDim float64, r
|
|||||||
return pathPoints
|
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 {
|
func drawLine(img *image.RGBA, x0, y0, x1, y1 int, col color.Color, width int) []image.Point {
|
||||||
var points []image.Point
|
var points []image.Point
|
||||||
dx := abs(x1 - x0)
|
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
|
return points
|
||||||
}
|
}
|
||||||
|
|
||||||
// abs returns the absolute value of an integer.
|
// abs returns the absolute value of an integer
|
||||||
func abs(x int) int {
|
func abs(x int) int {
|
||||||
if x < 0 {
|
if x < 0 {
|
||||||
return -x
|
return -x
|
||||||
|
|||||||
@@ -2,21 +2,19 @@ package main
|
|||||||
|
|
||||||
import "math/rand"
|
import "math/rand"
|
||||||
|
|
||||||
// SeedProvider is a simple struct that provides a stream of random seeds
|
// SeedProvider provides a stream of random seeds from a single initial seed
|
||||||
// from a single initial seed. This ensures that the entire map generation
|
|
||||||
// process is deterministic if the same initial seed is used.
|
|
||||||
type SeedProvider struct {
|
type SeedProvider struct {
|
||||||
rand *rand.Rand
|
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 {
|
func NewSeedProvider(seed int64) *SeedProvider {
|
||||||
return &SeedProvider{
|
return &SeedProvider{
|
||||||
rand: rand.New(rand.NewSource(seed)),
|
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 {
|
func (sp *SeedProvider) Next() int64 {
|
||||||
return sp.rand.Int63()
|
return sp.rand.Int63()
|
||||||
}
|
}
|
||||||
|
|||||||
+8
-29
@@ -14,16 +14,14 @@ import (
|
|||||||
"github.com/ojrac/opensimplex-go"
|
"github.com/ojrac/opensimplex-go"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Constants for Perlin noise generation
|
|
||||||
const (
|
const (
|
||||||
alpha = 2.
|
alpha = 2.
|
||||||
beta = 2.
|
beta = 2.
|
||||||
n = 3
|
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 {
|
func GenerateHeightmap(width, height, octaves int, scale float64, seed int64) image.Image {
|
||||||
// Initialize Perlin noise generator
|
|
||||||
p := perlin.NewPerlin(alpha, beta, n, seed)
|
p := perlin.NewPerlin(alpha, beta, n, seed)
|
||||||
img := image.NewGray(image.Rect(0, 0, width, height))
|
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
|
scale = 100.0
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use multiple goroutines to speed up noise generation
|
|
||||||
numGoroutines := runtime.NumCPU()
|
numGoroutines := runtime.NumCPU()
|
||||||
var wg sync.WaitGroup
|
var wg sync.WaitGroup
|
||||||
rowsPerGoroutine := height / numGoroutines
|
rowsPerGoroutine := height / numGoroutines
|
||||||
@@ -47,7 +44,6 @@ func GenerateHeightmap(width, height, octaves int, scale float64, seed int64) im
|
|||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
for y := startY; y < endY; y++ {
|
for y := startY; y < endY; y++ {
|
||||||
for x := 0; x < width; x++ {
|
for x := 0; x < width; x++ {
|
||||||
// Combine multiple octaves of noise for more detail
|
|
||||||
var noise float64
|
var noise float64
|
||||||
frequency := 1.0
|
frequency := 1.0
|
||||||
amplitude := 1.0
|
amplitude := 1.0
|
||||||
@@ -60,7 +56,6 @@ func GenerateHeightmap(width, height, octaves int, scale float64, seed int64) im
|
|||||||
frequency *= 2.0
|
frequency *= 2.0
|
||||||
}
|
}
|
||||||
|
|
||||||
// Normalize the noise value and set the pixel color
|
|
||||||
noise /= maxAmplitude
|
noise /= maxAmplitude
|
||||||
grayColor := uint8((noise + 1) * 127.5)
|
grayColor := uint8((noise + 1) * 127.5)
|
||||||
img.SetGray(x, y, color.Gray{Y: grayColor})
|
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
|
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 {
|
func ApplyRoughness(heightmap image.Image, roughness float64) image.Image {
|
||||||
bounds := heightmap.Bounds()
|
bounds := heightmap.Bounds()
|
||||||
composite := image.NewRGBA(bounds)
|
composite := image.NewRGBA(bounds)
|
||||||
draw.Draw(composite, bounds, heightmap, image.Point{}, draw.Src)
|
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)
|
alphaValue := 255 - uint8(roughness*2.55)
|
||||||
overlay := image.NewUniform(color.RGBA{R: 128, G: 128, B: 128, A: alphaValue})
|
overlay := image.NewUniform(color.RGBA{R: 128, G: 128, B: 128, A: alphaValue})
|
||||||
draw.Draw(composite, bounds, overlay, image.Point{}, draw.Over)
|
draw.Draw(composite, bounds, overlay, image.Point{}, draw.Over)
|
||||||
@@ -87,23 +81,20 @@ func ApplyRoughness(heightmap image.Image, roughness float64) image.Image {
|
|||||||
return composite
|
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 {
|
func DarkenLakeAreas(heightmap image.Image, lakePixels []image.Point) image.Image {
|
||||||
bounds := heightmap.Bounds()
|
bounds := heightmap.Bounds()
|
||||||
width := bounds.Dx()
|
width := bounds.Dx()
|
||||||
|
|
||||||
// Create a new black image to draw the lakes on
|
|
||||||
lakeMask := image.NewRGBA(bounds)
|
lakeMask := image.NewRGBA(bounds)
|
||||||
black := color.RGBA{0, 0, 0, 255}
|
black := color.RGBA{0, 0, 0, 255}
|
||||||
for _, p := range lakePixels {
|
for _, p := range lakePixels {
|
||||||
lakeMask.Set(p.X, p.Y, black)
|
lakeMask.Set(p.X, p.Y, black)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply a Gaussian blur to the lake mask to create smooth edges
|
|
||||||
blurRadius := float64(width) * 0.05
|
blurRadius := float64(width) * 0.05
|
||||||
blurredLakeMask := imaging.Blur(lakeMask, blurRadius)
|
blurredLakeMask := imaging.Blur(lakeMask, blurRadius)
|
||||||
|
|
||||||
// Composite the blurred lake mask onto the heightmap with some opacity
|
|
||||||
composite := image.NewRGBA(bounds)
|
composite := image.NewRGBA(bounds)
|
||||||
draw.Draw(composite, bounds, heightmap, image.Point{}, draw.Src)
|
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)
|
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
|
return composite
|
||||||
}
|
}
|
||||||
|
|
||||||
// FlattenRoadAreas smoothens the terrain under roads.
|
// FlattenRoadAreas smooths terrain under roads
|
||||||
func FlattenRoadAreas(heightmap image.Image, roadPixels []image.Point) image.Image {
|
func FlattenRoadAreas(heightmap image.Image, roadPixels []image.Point) image.Image {
|
||||||
bounds := heightmap.Bounds()
|
bounds := heightmap.Bounds()
|
||||||
width := bounds.Dx()
|
width := bounds.Dx()
|
||||||
|
|
||||||
// Create a mask with the road pixels
|
|
||||||
roadMask := image.NewGray(bounds)
|
roadMask := image.NewGray(bounds)
|
||||||
for _, p := range roadPixels {
|
for _, p := range roadPixels {
|
||||||
roadMask.SetGray(p.X, p.Y, color.Gray{Y: 255})
|
roadMask.SetGray(p.X, p.Y, color.Gray{Y: 255})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Blur the road mask to create a smooth transition
|
|
||||||
blurRadius := float64(width) * 0.01
|
blurRadius := float64(width) * 0.01
|
||||||
blurredRoadMask := imaging.Blur(roadMask, blurRadius)
|
blurredRoadMask := imaging.Blur(roadMask, blurRadius)
|
||||||
|
|
||||||
// Blur the entire heightmap
|
|
||||||
blurredHeightmap := imaging.Blur(heightmap, blurRadius)
|
blurredHeightmap := imaging.Blur(heightmap, blurRadius)
|
||||||
|
|
||||||
// Create a new composite image
|
|
||||||
composite := image.NewRGBA(bounds)
|
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 y := bounds.Min.Y; y < bounds.Max.Y; y++ {
|
||||||
for x := bounds.Min.X; x < bounds.Max.X; x++ {
|
for x := bounds.Min.X; x < bounds.Max.X; x++ {
|
||||||
maskAlpha, _, _, _ := blurredRoadMask.At(x, y).RGBA()
|
maskAlpha, _, _, _ := blurredRoadMask.At(x, y).RGBA()
|
||||||
if maskAlpha > 0 {
|
if maskAlpha > 0 {
|
||||||
// Linearly interpolate between the original and blurred heightmap
|
|
||||||
originalColor := heightmap.At(x, y)
|
originalColor := heightmap.At(x, y)
|
||||||
blurredColor := blurredHeightmap.At(x, y)
|
blurredColor := blurredHeightmap.At(x, y)
|
||||||
|
|
||||||
@@ -161,12 +146,11 @@ func FlattenRoadAreas(heightmap image.Image, roadPixels []image.Point) image.Ima
|
|||||||
return composite
|
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 {
|
func GenerateTrees(img *image.RGBA, lakePixels, roadPixels, buildingPixels []image.Point, minTreeSize, maxTreeSize, treeCoverage, treeClumpiness float64, seed int64) []image.Point {
|
||||||
width := img.Bounds().Dx()
|
width := img.Bounds().Dx()
|
||||||
height := img.Bounds().Dy()
|
height := img.Bounds().Dy()
|
||||||
|
|
||||||
// Step 1: Calculate the number of trees to place based on coverage percentage.
|
|
||||||
avgTreeSize := (minTreeSize + maxTreeSize) / 2
|
avgTreeSize := (minTreeSize + maxTreeSize) / 2
|
||||||
if avgTreeSize <= 0 {
|
if avgTreeSize <= 0 {
|
||||||
return nil
|
return nil
|
||||||
@@ -183,20 +167,18 @@ func GenerateTrees(img *image.RGBA, lakePixels, roadPixels, buildingPixels []ima
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Step 2: Generate a simplex noise map to guide tree placement.
|
|
||||||
noise := opensimplex.New(seed)
|
noise := opensimplex.New(seed)
|
||||||
treeNoiseMap := image.NewGray(image.Rect(0, 0, width, height))
|
treeNoiseMap := image.NewGray(image.Rect(0, 0, width, height))
|
||||||
treeNoiseZoom := 0.05
|
treeNoiseZoom := 0.05
|
||||||
for y := 0; y < height; y++ {
|
for y := 0; y < height; y++ {
|
||||||
for x := 0; x < width; x++ {
|
for x := 0; x < width; x++ {
|
||||||
val := noise.Eval2(float64(x)*treeNoiseZoom, float64(y)*treeNoiseZoom)
|
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)})
|
treeNoiseMap.SetGray(x, y, color.Gray{Y: uint8(val * 255)})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
threshold := uint8(255 * (1 - (treeCoverage / 100.0)))
|
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)
|
isLake := make(map[image.Point]bool)
|
||||||
for _, p := range lakePixels {
|
for _, p := range lakePixels {
|
||||||
isLake[p] = true
|
isLake[p] = true
|
||||||
@@ -214,12 +196,11 @@ func GenerateTrees(img *image.RGBA, lakePixels, roadPixels, buildingPixels []ima
|
|||||||
|
|
||||||
randSrc := rand.New(rand.NewSource(seed))
|
randSrc := rand.New(rand.NewSource(seed))
|
||||||
|
|
||||||
// Step 3: Determine initial points for clumps of trees.
|
|
||||||
numClumpTrees := min(int(treeClumpiness), numTreesToPlace)
|
numClumpTrees := min(int(treeClumpiness), numTreesToPlace)
|
||||||
|
|
||||||
initialPoints := make([]image.Point, 0, numClumpTrees)
|
initialPoints := make([]image.Point, 0, numClumpTrees)
|
||||||
for range 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)}
|
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] {
|
if treeNoiseMap.GrayAt(p.X, p.Y).Y >= threshold && !isLake[p] && !isRoad[p] && !isBuilding[p] {
|
||||||
initialPoints = append(initialPoints, 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
|
minRadius := minTreeSize
|
||||||
allPoints := poissonDiscSampling(width, height, minRadius, 30, initialPoints, func(p image.Point) bool {
|
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]
|
return treeNoiseMap.GrayAt(p.X, p.Y).Y >= threshold && !isLake[p] && !isRoad[p] && !isBuilding[p]
|
||||||
}, seed)
|
}, seed)
|
||||||
|
|
||||||
var treePixels []image.Point
|
var treePixels []image.Point
|
||||||
// Step 5: Draw the trees on the image.
|
|
||||||
numGoroutines := runtime.NumCPU()
|
numGoroutines := runtime.NumCPU()
|
||||||
if len(allPoints) < numGoroutines {
|
if len(allPoints) < numGoroutines {
|
||||||
numGoroutines = len(allPoints)
|
numGoroutines = len(allPoints)
|
||||||
@@ -297,7 +276,7 @@ func GenerateTrees(img *image.RGBA, lakePixels, roadPixels, buildingPixels []ima
|
|||||||
return treePixels
|
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 {
|
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))
|
randSrc := rand.New(rand.NewSource(seed))
|
||||||
points := initialPoints
|
points := initialPoints
|
||||||
|
|||||||
@@ -12,19 +12,16 @@ import (
|
|||||||
"github.com/ojrac/opensimplex-go"
|
"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 {
|
type lakePixel struct {
|
||||||
point image.Point
|
point image.Point
|
||||||
score float64
|
score float64
|
||||||
index int // required for heap.Interface
|
index int
|
||||||
}
|
}
|
||||||
|
|
||||||
// priorityQueue implements a max-heap for lakePixel structs.
|
|
||||||
type priorityQueue []*lakePixel
|
type priorityQueue []*lakePixel
|
||||||
|
|
||||||
func (pq priorityQueue) Len() int { return len(pq) }
|
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) {
|
func (pq priorityQueue) Swap(i, j int) {
|
||||||
pq[i], pq[j] = pq[j], pq[i]
|
pq[i], pq[j] = pq[j], pq[i]
|
||||||
pq[i].index = i
|
pq[i].index = i
|
||||||
@@ -46,10 +43,8 @@ func (pq *priorityQueue) Pop() any {
|
|||||||
return item
|
return item
|
||||||
}
|
}
|
||||||
|
|
||||||
// GenerateLakes creates lakes on the map using a growth algorithm.
|
// GenerateLakes creates lakes on the map using a priority queue growth algorithm
|
||||||
// When lakeEdgeRoughness is 0, lakes grow in perfect circles. Higher values add noise-based irregularity.
|
|
||||||
func GenerateLakes(width, height, numLakes int, lakeSizeLower, lakeSizeUpper float64, seed int64, lakeEdgeRoughness float64) (image.Image, [][]image.Point) {
|
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))
|
canvas := image.NewRGBA(image.Rect(0, 0, width, height))
|
||||||
draw.Draw(canvas, canvas.Bounds(), image.NewUniform(color.White), image.Point{}, draw.Src)
|
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
|
var allLakes [][]image.Point
|
||||||
randSrc := rand.New(rand.NewSource(seed))
|
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))))
|
gridDim := int(math.Ceil(math.Sqrt(float64(numLakes))))
|
||||||
if gridDim == 0 {
|
if gridDim == 0 {
|
||||||
return canvas, nil
|
return canvas, nil
|
||||||
@@ -71,7 +66,7 @@ func GenerateLakes(width, height, numLakes int, lakeSizeLower, lakeSizeUpper flo
|
|||||||
return canvas, nil
|
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)
|
chunkIndices := make([]int, gridDim*gridDim)
|
||||||
for i := range chunkIndices {
|
for i := range chunkIndices {
|
||||||
chunkIndices[i] = i
|
chunkIndices[i] = i
|
||||||
@@ -83,7 +78,7 @@ func GenerateLakes(width, height, numLakes int, lakeSizeLower, lakeSizeUpper flo
|
|||||||
totalArea := float64(width * height)
|
totalArea := float64(width * height)
|
||||||
noiseGen := opensimplex.New(seed)
|
noiseGen := opensimplex.New(seed)
|
||||||
|
|
||||||
// Step 3: Generate a lake in a subset of the chunks.
|
// Generate each lake
|
||||||
for i := range numLakes {
|
for i := range numLakes {
|
||||||
if i >= len(chunkIndices) {
|
if i >= len(chunkIndices) {
|
||||||
break
|
break
|
||||||
@@ -91,7 +86,7 @@ func GenerateLakes(width, height, numLakes int, lakeSizeLower, lakeSizeUpper flo
|
|||||||
|
|
||||||
var currentLake []image.Point
|
var currentLake []image.Point
|
||||||
|
|
||||||
// Each lake gets a random size within the defined range.
|
// Randomize lake size within specified range
|
||||||
lakeSize := lakeSizeLower
|
lakeSize := lakeSizeLower
|
||||||
if lakeSizeUpper > lakeSizeLower {
|
if lakeSizeUpper > lakeSizeLower {
|
||||||
lakeSize = lakeSizeLower + randSrc.Float64()*(lakeSizeUpper-lakeSizeLower)
|
lakeSize = lakeSizeLower + randSrc.Float64()*(lakeSizeUpper-lakeSizeLower)
|
||||||
@@ -112,12 +107,12 @@ func GenerateLakes(width, height, numLakes int, lakeSizeLower, lakeSizeUpper flo
|
|||||||
(chunkGridY+1)*chunkHeight,
|
(chunkGridY+1)*chunkHeight,
|
||||||
)
|
)
|
||||||
|
|
||||||
// Use a priority queue-based growth algorithm within the chunk.
|
// Initialize priority queue growth algorithm
|
||||||
pq := &priorityQueue{}
|
pq := &priorityQueue{}
|
||||||
heap.Init(pq)
|
heap.Init(pq)
|
||||||
visited := make(map[image.Point]bool)
|
visited := make(map[image.Point]bool)
|
||||||
|
|
||||||
// Start the growth near the center of the chunk.
|
// Start growth at chunk center
|
||||||
startPt := image.Point{
|
startPt := image.Point{
|
||||||
X: chunkRect.Min.X + chunkWidth/2,
|
X: chunkRect.Min.X + chunkWidth/2,
|
||||||
Y: chunkRect.Min.Y + chunkHeight/2,
|
Y: chunkRect.Min.Y + chunkHeight/2,
|
||||||
@@ -126,33 +121,31 @@ func GenerateLakes(width, height, numLakes int, lakeSizeLower, lakeSizeUpper flo
|
|||||||
continue
|
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
|
seedX := randSrc.Float64() * 10000.0
|
||||||
seedY := randSrc.Float64() * 10000.0
|
seedY := randSrc.Float64() * 10000.0
|
||||||
radius := math.Sqrt(float64(targetPixelsPerLake) / math.Pi)
|
radius := math.Sqrt(float64(targetPixelsPerLake) / math.Pi)
|
||||||
noiseFreq := 0.01 + (0.2 / (radius + 1.0))
|
noiseFreq := 0.01 + (0.2 / (radius + 1.0))
|
||||||
|
|
||||||
|
// Score function determines which pixels to add to lake
|
||||||
getScore := func(pt image.Point) float64 {
|
getScore := func(pt image.Point) float64 {
|
||||||
dx, dy := pt.X-startPt.X, pt.Y-startPt.Y
|
dx, dy := pt.X-startPt.X, pt.Y-startPt.Y
|
||||||
dist := math.Sqrt(float64(dx*dx + dy*dy))
|
dist := math.Sqrt(float64(dx*dx + dy*dy))
|
||||||
distPenalty := math.Pow(dist/radius, 3.0)
|
distPenalty := math.Pow(dist/radius, 3.0)
|
||||||
|
|
||||||
// Only apply noise if edge roughness is requested
|
|
||||||
if lakeEdgeRoughness > 0 {
|
if lakeEdgeRoughness > 0 {
|
||||||
noise := noiseGen.Eval2(seedX+float64(dx)*noiseFreq, seedY+float64(dy)*noiseFreq)
|
noise := noiseGen.Eval2(seedX+float64(dx)*noiseFreq, seedY+float64(dy)*noiseFreq)
|
||||||
// Scale noise contribution by roughness setting
|
|
||||||
noiseContribution := noise * (lakeEdgeRoughness / 100.0)
|
noiseContribution := noise * (lakeEdgeRoughness / 100.0)
|
||||||
return noiseContribution - distPenalty
|
return noiseContribution - distPenalty
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pure circular growth when variability is 0
|
|
||||||
return -distPenalty
|
return -distPenalty
|
||||||
}
|
}
|
||||||
|
|
||||||
heap.Push(pq, &lakePixel{point: startPt, score: getScore(startPt)})
|
heap.Push(pq, &lakePixel{point: startPt, score: getScore(startPt)})
|
||||||
visited[startPt] = true
|
visited[startPt] = true
|
||||||
|
|
||||||
// Grow the lake until it reaches its target size.
|
// Grow lake to target size
|
||||||
lakeCount := 0
|
lakeCount := 0
|
||||||
for pq.Len() > 0 && lakeCount < targetPixelsPerLake {
|
for pq.Len() > 0 && lakeCount < targetPixelsPerLake {
|
||||||
current := heap.Pop(pq).(*lakePixel)
|
current := heap.Pop(pq).(*lakePixel)
|
||||||
@@ -161,7 +154,7 @@ func GenerateLakes(width, height, numLakes int, lakeSizeLower, lakeSizeUpper flo
|
|||||||
currentLake = append(currentLake, current.point)
|
currentLake = append(currentLake, current.point)
|
||||||
lakeCount++
|
lakeCount++
|
||||||
|
|
||||||
// Add neighbors to the priority queue.
|
// 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 {
|
||||||
@@ -189,14 +182,14 @@ func GenerateLakes(width, height, numLakes int, lakeSizeLower, lakeSizeUpper flo
|
|||||||
return canvas, allLakes
|
return canvas, allLakes
|
||||||
}
|
}
|
||||||
|
|
||||||
// River represents a river on the map.
|
// River represents a river on the map
|
||||||
type River struct {
|
type River struct {
|
||||||
Width float64
|
Width float64
|
||||||
Start, End image.Point
|
Start, End image.Point
|
||||||
Points []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) {
|
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 {
|
if numRivers == 0 {
|
||||||
return inputImage, nil
|
return inputImage, nil
|
||||||
@@ -212,7 +205,7 @@ func GenerateRivers(width, height, numRivers int, minWidth, maxWidth, curvyness
|
|||||||
randSrc := rand.New(rand.NewSource(seed))
|
randSrc := rand.New(rand.NewSource(seed))
|
||||||
avgDim := float64(width+height) / 2.0
|
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)
|
isWater := make(map[image.Point]bool)
|
||||||
lakePixelMap := make(map[image.Point]int)
|
lakePixelMap := make(map[image.Point]int)
|
||||||
for i, lake := range lakes {
|
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)
|
rivers := make([]River, numRivers)
|
||||||
for i := range numRivers {
|
for i := range numRivers {
|
||||||
widthPercent := float64(i) / float64(numRivers-1)
|
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)
|
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 {
|
sort.Slice(rivers, func(i, j int) bool {
|
||||||
return rivers[i].Width > rivers[j].Width
|
return rivers[i].Width > rivers[j].Width
|
||||||
})
|
})
|
||||||
|
|
||||||
numControlPoints := max(int(avgDim*0.03), 60)
|
numControlPoints := max(int(avgDim*0.03), 60)
|
||||||
|
|
||||||
// Generate each river.
|
// Generate each river
|
||||||
for i := range rivers {
|
for i := range rivers {
|
||||||
r := &rivers[i]
|
r := &rivers[i]
|
||||||
|
|
||||||
// Determine the start and end edges of the river.
|
// Pick random start and end edges
|
||||||
startEdge := randSrc.Intn(4)
|
startEdge := randSrc.Intn(4)
|
||||||
endEdge := (startEdge + randSrc.Intn(3) + 1) % 4
|
endEdge := (startEdge + randSrc.Intn(3) + 1) % 4
|
||||||
|
|
||||||
r.Start = getPointOnEdge(width, height, startEdge, randSrc)
|
r.Start = getPointOnEdge(width, height, startEdge, randSrc)
|
||||||
r.End = getPointOnEdge(width, height, endEdge, 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)
|
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 {
|
for _, p := range path {
|
||||||
if isWater[p] {
|
if isWater[p] {
|
||||||
if lakeIndex, isLake := lakePixelMap[p]; isLake {
|
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])
|
lakeCenter := findCenter(lakes[lakeIndex])
|
||||||
r.End = lakeCenter
|
r.End = lakeCenter
|
||||||
} else {
|
} else {
|
||||||
// If the river intersects with another river, end it at the intersection point.
|
// End river at intersection with another river
|
||||||
r.End = p
|
r.End = p
|
||||||
}
|
}
|
||||||
path = calculateRiverPath(r.Start, r.End, curvyness/100.0, avgDim, randSrc, numControlPoints)
|
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
|
riverWidthPx := (r.Width / 100.0) * avgDim
|
||||||
radius := riverWidthPx / 2.0
|
radius := riverWidthPx / 2.0
|
||||||
|
|
||||||
@@ -282,7 +275,7 @@ func GenerateRivers(width, height, numRivers int, minWidth, maxWidth, curvyness
|
|||||||
return canvas, allRiverPixels
|
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 {
|
func bresenhamRiver(path []image.Point) []image.Point {
|
||||||
if len(path) < 2 {
|
if len(path) < 2 {
|
||||||
return path
|
return path
|
||||||
@@ -322,7 +315,7 @@ func bresenhamRiver(path []image.Point) []image.Point {
|
|||||||
return fullPath
|
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 {
|
func calculateRiverPath(start, end image.Point, curvyness, avgDim float64, randSrc *rand.Rand, numControlPoints int) []image.Point {
|
||||||
dx := end.X - start.X
|
dx := end.X - start.X
|
||||||
dy := end.Y - start.Y
|
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})
|
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 {
|
type wave struct {
|
||||||
amplitude float64
|
amplitude float64
|
||||||
numWaves float64
|
numWaves float64
|
||||||
@@ -362,7 +355,7 @@ func calculateRiverPath(start, end image.Point, curvyness, avgDim float64, randS
|
|||||||
amp /= 3
|
amp /= 3
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate control points for the curve.
|
// Generate control points along the path
|
||||||
controlPoints := make([]image.Point, numControlPoints+1)
|
controlPoints := make([]image.Point, numControlPoints+1)
|
||||||
for i := 0; i <= numControlPoints; i++ {
|
for i := 0; i <= numControlPoints; i++ {
|
||||||
t := float64(i) / float64(numControlPoints)
|
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))}
|
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)
|
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 {
|
func findCenter(pixels []image.Point) image.Point {
|
||||||
if len(pixels) == 0 {
|
if len(pixels) == 0 {
|
||||||
return image.Point{}
|
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 {
|
func getPointOnEdge(width, height, edge int, randSrc *rand.Rand) image.Point {
|
||||||
switch edge {
|
switch edge {
|
||||||
case 0: // Top
|
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.
|
// drawCircle draws a circular river cross-section with sine wave edge roughening
|
||||||
// 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%).
|
|
||||||
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) {
|
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()
|
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)
|
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)
|
smallAmplitude := largeAmplitude * (riverEdgeRoughness / 100.0)
|
||||||
|
|
||||||
for y := int(math.Floor(float64(center.Y) - radius)); y <= int(math.Ceil(float64(center.Y)+radius)); y++ {
|
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)
|
dx, dy := float64(x-center.X), float64(y-center.Y)
|
||||||
dist := math.Sqrt(dx*dx + dy*dy)
|
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
|
positionPhase := float64(x)*0.008 + float64(y)*0.012
|
||||||
largeWave := math.Sin(positionPhase) * largeAmplitude
|
largeWave := math.Sin(positionPhase) * largeAmplitude
|
||||||
smallWave := math.Sin(positionPhase*3.5) * smallAmplitude
|
smallWave := math.Sin(positionPhase*3.5) * smallAmplitude
|
||||||
waveOffset := largeWave + smallWave
|
waveOffset := largeWave + smallWave
|
||||||
|
|
||||||
// Effective radius varies based on sin wave
|
|
||||||
effectiveRadius := radius + waveOffset
|
effectiveRadius := radius + waveOffset
|
||||||
|
|
||||||
if dist <= effectiveRadius {
|
if dist <= effectiveRadius {
|
||||||
|
|||||||
Reference in New Issue
Block a user