diff --git a/main.go b/main.go index 15aedd4..1ccabed 100644 --- a/main.go +++ b/main.go @@ -101,9 +101,11 @@ func main() { // Initial image generation noiseImg := GenerateHeightmap(settings.Width, settings.Height, int(settings.Detail), 100.0, settings.Seed) lakeImage, lakePixels := GenerateLakes(settings.Width, settings.Height, settings.Lakes, settings.LakeSizeLower, settings.LakeSizeUpper, noiseImg, settings.Seed) - finalImage := lakeImage.(*image.RGBA) - GenerateTrees(finalImage, lakePixels, settings.MinTreeSize, settings.MaxTreeSize, settings.TreeCoverage, settings.TreeClumpiness, settings.Seed) - darkenedHeightmap := DarkenLakeAreas(noiseImg, lakePixels) + riverImage, riverPixels := GenerateRivers(settings.Width, settings.Height, settings.Rivers, settings.MinRiverWidth, settings.MaxRiverWidth, settings.RiverCurvyness, lakeImage, lakePixels, settings.Seed) + finalImage := riverImage.(*image.RGBA) + allWaterPixels := append(lakePixels, riverPixels...) + GenerateTrees(finalImage, allWaterPixels, settings.MinTreeSize, settings.MaxTreeSize, settings.TreeCoverage, settings.TreeClumpiness, settings.Seed) + darkenedHeightmap := DarkenLakeAreas(noiseImg, allWaterPixels) compositeImg := ApplyRoughness(darkenedHeightmap, settings.Roughness) heightmapImg.Image = compositeImg canvasImg.Image = finalImage @@ -158,6 +160,47 @@ func main() { } lakeSizeUpperSlider.SetValue(settings.LakeSizeUpper) + riversLabel := widget.NewLabel(fmt.Sprintf("Rivers: %d", settings.Rivers)) + riversSlider := widget.NewSlider(0, 5) + riversSlider.OnChanged = func(val float64) { + settings.Rivers = int(val) + riversLabel.SetText(fmt.Sprintf("Rivers: %d", settings.Rivers)) + } + riversSlider.SetValue(float64(settings.Rivers)) + + minRiverWidthLabel := widget.NewLabel(fmt.Sprintf("Min River Width: %.0f%%", settings.MinRiverWidth)) + minRiverWidthSlider := widget.NewSlider(1, 100) + maxRiverWidthLabel := widget.NewLabel(fmt.Sprintf("Max River Width: %.0f%%", settings.MaxRiverWidth)) + maxRiverWidthSlider := widget.NewSlider(1, 100) + + minRiverWidthSlider.OnChanged = func(val float64) { + settings.MinRiverWidth = val + if settings.MinRiverWidth > settings.MaxRiverWidth { + settings.MaxRiverWidth = settings.MinRiverWidth + maxRiverWidthSlider.SetValue(settings.MaxRiverWidth) + } + minRiverWidthLabel.SetText(fmt.Sprintf("Min River Width: %.0f%%", settings.MinRiverWidth)) + } + minRiverWidthSlider.SetValue(settings.MinRiverWidth) + + maxRiverWidthSlider.OnChanged = func(val float64) { + settings.MaxRiverWidth = val + if settings.MaxRiverWidth < settings.MinRiverWidth { + settings.MinRiverWidth = settings.MaxRiverWidth + minRiverWidthSlider.SetValue(settings.MinRiverWidth) + } + maxRiverWidthLabel.SetText(fmt.Sprintf("Max River Width: %.0f%%", settings.MaxRiverWidth)) + } + maxRiverWidthSlider.SetValue(settings.MaxRiverWidth) + + riverCurvynessLabel := widget.NewLabel(fmt.Sprintf("River Curvyness: %.0f%%", settings.RiverCurvyness)) + riverCurvynessSlider := widget.NewSlider(0, 100) + riverCurvynessSlider.OnChanged = func(val float64) { + settings.RiverCurvyness = val + riverCurvynessLabel.SetText(fmt.Sprintf("River Curvyness: %.0f%%", settings.RiverCurvyness)) + } + riverCurvynessSlider.SetValue(settings.RiverCurvyness) + minTreeSizeLabel := widget.NewLabel(fmt.Sprintf("Min Tree Size: %.0fpx", settings.MinTreeSize)) minTreeSizeSlider := widget.NewSlider(1, 150) maxTreeSizeLabel := widget.NewLabel(fmt.Sprintf("Max Tree Size: %.0fpx", settings.MaxTreeSize)) @@ -211,7 +254,7 @@ func main() { generateBtn.Disable() defer generateBtn.Enable() - steps := 6 + steps := 7 currentStep := 0 // Step 1: Generating Heightmap @@ -225,27 +268,35 @@ func main() { progressBar.SetText(fmt.Sprintf("Step %d/%d: Generating Lakes", currentStep, steps)) progressBar.SetValue(float64(currentStep) / float64(steps)) lakeImage, lakePixels := GenerateLakes(settings.Width, settings.Height, settings.Lakes, settings.LakeSizeLower, settings.LakeSizeUpper, noiseImg, settings.Seed) - finalImage := lakeImage.(*image.RGBA) - // Step 3: Darkening Lake Areas + // Step 3: Generating Rivers currentStep++ - progressBar.SetText(fmt.Sprintf("Step %d/%d: Darkening Lake Areas", currentStep, steps)) + progressBar.SetText(fmt.Sprintf("Step %d/%d: Generating Rivers", currentStep, steps)) progressBar.SetValue(float64(currentStep) / float64(steps)) - darkenedHeightmap := DarkenLakeAreas(noiseImg, lakePixels) + riverImage, riverPixels := GenerateRivers(settings.Width, settings.Height, settings.Rivers, settings.MinRiverWidth, settings.MaxRiverWidth, settings.RiverCurvyness, lakeImage, lakePixels, settings.Seed) - // Step 4: Applying Roughness + finalImage := riverImage.(*image.RGBA) + allWaterPixels := append(lakePixels, riverPixels...) + + // Step 4: Darkening Water Areas + currentStep++ + progressBar.SetText(fmt.Sprintf("Step %d/%d: Darkening Water Areas", currentStep, steps)) + progressBar.SetValue(float64(currentStep) / float64(steps)) + darkenedHeightmap := DarkenLakeAreas(noiseImg, allWaterPixels) + + // Step 5: Applying Roughness currentStep++ progressBar.SetText(fmt.Sprintf("Step %d/%d: Applying Roughness", currentStep, steps)) progressBar.SetValue(float64(currentStep) / float64(steps)) compositeImg := ApplyRoughness(darkenedHeightmap, settings.Roughness) - // Step 5: Generating Trees + // Step 6: Generating Trees currentStep++ progressBar.SetText(fmt.Sprintf("Step %d/%d: Generating Trees", currentStep, steps)) progressBar.SetValue(float64(currentStep) / float64(steps)) - GenerateTrees(finalImage, lakePixels, settings.MinTreeSize, settings.MaxTreeSize, settings.TreeCoverage, settings.TreeClumpiness, settings.Seed) + GenerateTrees(finalImage, allWaterPixels, settings.MinTreeSize, settings.MaxTreeSize, settings.TreeCoverage, settings.TreeClumpiness, settings.Seed) - // Step 6: Finalizing Images + // Step 7: Finalizing Images currentStep++ progressBar.SetText(fmt.Sprintf("Step %d/%d: Finalizing Images", currentStep, steps)) progressBar.SetValue(float64(currentStep) / float64(steps)) @@ -318,6 +369,20 @@ func main() { lakeSizeLowerSlider, lakeSizeUpperLabel, lakeSizeUpperSlider, + + widget.NewLabel(""), // Spacer + + riversLabel, + riversSlider, + minRiverWidthLabel, + minRiverWidthSlider, + maxRiverWidthLabel, + maxRiverWidthSlider, + riverCurvynessLabel, + riverCurvynessSlider, + + widget.NewLabel(""), // Spacer + minTreeSizeLabel, minTreeSizeSlider, maxTreeSizeLabel, diff --git a/settings.go b/settings.go index a5a212e..f030355 100644 --- a/settings.go +++ b/settings.go @@ -20,6 +20,10 @@ type Settings struct { TreeCoverage float64 `json:"tree_coverage"` TreeClumpiness float64 `json:"tree_clumpiness"` Seed int64 `json:"seed"` + Rivers int `json:"rivers"` + MinRiverWidth float64 `json:"min_river_width"` + MaxRiverWidth float64 `json:"max_river_width"` + RiverCurvyness float64 `json:"river_curvyness"` } func (s *Settings) Save() error { @@ -67,6 +71,10 @@ func LoadSettings() (*Settings, error) { TreeCoverage: 20, TreeClumpiness: 50, Seed: time.Now().UnixNano(), + Rivers: 0, + MinRiverWidth: 1, + MaxRiverWidth: 5, + RiverCurvyness: 50, }, nil } return nil, err diff --git a/terrain.go b/terrain.go index cfed11b..aabeed9 100644 --- a/terrain.go +++ b/terrain.go @@ -7,6 +7,7 @@ import ( "image/draw" "math" "math/rand" + "sort" "github.com/disintegration/imaging" "github.com/ojrac/opensimplex-go" @@ -174,6 +175,252 @@ func GenerateLakes(width, height, numLakes int, lakeSizeLower, lakeSizeUpper flo return canvas, allLakePixels } +type River struct { + Width float64 + Start, End image.Point + Points []image.Point +} + +func GenerateRivers(width, height, numRivers int, minWidth, maxWidth, curvyness float64, inputImage image.Image, lakePixels []image.Point, seed int64) (image.Image, []image.Point) { + if numRivers == 0 { + return inputImage, nil + } + + canvas, ok := inputImage.(*image.RGBA) + if !ok { + canvas = image.NewRGBA(inputImage.Bounds()) + draw.Draw(canvas, canvas.Bounds(), inputImage, image.Point{}, draw.Src) + } + + var allRiverPixels []image.Point + randSrc := rand.New(rand.NewSource(seed)) + avgDim := float64(width+height) / 2.0 + + isWater := make(map[image.Point]bool) + for _, p := range lakePixels { + isWater[p] = true + } + + rivers := make([]River, numRivers) + for i := 0; i < numRivers; i++ { + widthPercent := float64(i) / float64(numRivers-1) + if numRivers == 1 { + widthPercent = 0.5 + } + rivers[i].Width = maxWidth - widthPercent*(maxWidth-minWidth) + } + + sort.Slice(rivers, func(i, j int) bool { + return rivers[i].Width > rivers[j].Width + }) + + numControlPoints := int(avgDim * 0.03) + if numControlPoints < 60 { + numControlPoints = 60 + } + + for i := range rivers { + r := &rivers[i] + + 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) + + path := calculatePath(r.Start, r.End, curvyness/100.0, avgDim, randSrc, numControlPoints) + + for _, p := range path { + if isWater[p] { + r.End = p + path = calculatePath(r.Start, r.End, curvyness/100.0, avgDim, randSrc, numControlPoints) + break + } + } + + riverWidthPx := (r.Width / 100.0) * avgDim + radius := riverWidthPx / 2.0 + + for _, p := range path { + drawCircle(canvas, p, radius, color.RGBA{R: 0, G: 0, B: 255, A: 255}, &allRiverPixels, isWater) + } + r.Points = path + } + + return canvas, allRiverPixels +} + +func getPointOnEdge(width, height, edge int, randSrc *rand.Rand) image.Point { + switch edge { + case 0: // Top + return image.Point{X: randSrc.Intn(width), Y: 0} + case 1: // Right + return image.Point{X: width - 1, Y: randSrc.Intn(height)} + case 2: // Bottom + return image.Point{X: randSrc.Intn(width), Y: height - 1} + default: // Left + return image.Point{X: 0, Y: randSrc.Intn(height)} + } +} + +func calculatePath(start, end image.Point, curvyness, avgDim float64, randSrc *rand.Rand, numControlPoints int) []image.Point { + + dx := end.X - start.X + + dy := end.Y - start.Y + + dist := math.Sqrt(float64(dx*dx + dy*dy)) + + if dist == 0 { + + return []image.Point{start} + + } + + if curvyness == 0 { + + return bresenham([]image.Point{start, end}) + + } + + type wave struct { + amplitude float64 + + numWaves float64 + + phase float64 + } + + waves := make([]wave, 3) + + amp := (avgDim / 10.0) * curvyness + + mainWavelength := avgDim / 4.0 + + if mainWavelength < 1 { + + mainWavelength = 1 + + } + + baseNumWaves := (dist / mainWavelength) * curvyness + + for i := 0; i < 3; i++ { + + freqMultiplier := 1.0 + float64(i) + + randomizedNumWaves := baseNumWaves * freqMultiplier * (0.75 + randSrc.Float64()*0.5) + + waves[i] = wave{ + + amplitude: amp, + + numWaves: randomizedNumWaves, + + phase: randSrc.Float64() * 2 * math.Pi, + } + + amp /= 3 + + } + + controlPoints := make([]image.Point, numControlPoints+1) + + for i := 0; i <= numControlPoints; i++ { + + t := float64(i) / float64(numControlPoints) + + x := float64(start.X) + t*float64(dx) + + y := float64(start.Y) + t*float64(dy) + + perpX, perpY := -float64(dy)/dist, float64(dx)/dist + + totalOffset := 0.0 + + for _, w := range waves { + + totalOffset += math.Sin(t*w.numWaves*2*math.Pi+w.phase) * w.amplitude + + } + + // Apply an envelope to ensure start/end points are anchored + + totalOffset *= math.Sin(t * math.Pi) + + x += totalOffset * perpX + + y += totalOffset * perpY + + controlPoints[i] = image.Point{X: int(math.Round(x)), Y: int(math.Round(y))} + + } + + return bresenham(controlPoints) + +} + +func bresenham(path []image.Point) []image.Point { + if len(path) < 2 { + return path + } + + var fullPath []image.Point + for i := 0; i < len(path)-1; i++ { + p1, p2 := path[i], path[i+1] + dx, dy := p2.X-p1.X, p2.Y-p1.Y + absDx, absDy := int(math.Abs(float64(dx))), int(math.Abs(float64(dy))) + sx, sy := 1, 1 + if dx < 0 { + sx = -1 + } + if dy < 0 { + sy = -1 + } + err := absDx - absDy + + x, y := p1.X, p1.Y + for { + fullPath = append(fullPath, image.Point{X: x, Y: y}) + if x == p2.X && y == p2.Y { + break + } + e2 := 2 * err + if e2 > -absDy { + err -= absDy + x += sx + } + if e2 < absDx { + err += absDx + y += sy + } + } + } + return fullPath +} + +func drawCircle(img *image.RGBA, center image.Point, radius float64, c color.Color, pixels *[]image.Point, isWater map[image.Point]bool) { + bounds := img.Bounds() + r2 := radius * radius + for y := int(math.Floor(float64(center.Y) - radius)); y <= int(math.Ceil(float64(center.Y)+radius)); y++ { + for x := int(math.Floor(float64(center.X) - radius)); x <= int(math.Ceil(float64(center.X)+radius)); x++ { + p := image.Point{X: x, Y: y} + if !p.In(bounds) { + continue + } + + dx, dy := float64(x-center.X), float64(y-center.Y) + if dx*dx+dy*dy <= r2 { + if !isWater[p] { + img.Set(x, y, c) + *pixels = append(*pixels, p) + isWater[p] = true + } + } + } + } +} + // DarkenLakeAreas applies a visual darkening effect to the heightmap where lakes exist. func DarkenLakeAreas(heightmap image.Image, lakePixels []image.Point) image.Image { bounds := heightmap.Bounds()