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