From b9173dd1f3478099a773ab959cc7be0b6f0f3efb Mon Sep 17 00:00:00 2001 From: Grimsace Date: Wed, 4 Feb 2026 14:19:18 -0600 Subject: [PATCH] basic road generation added --- main.go | 164 +++++++++++++++++++++++++++--- roads.go | 280 ++++++++++++++++++++++++++++++++++++++++++++++++++++ settings.go | 80 ++++++++------- 3 files changed, 478 insertions(+), 46 deletions(-) create mode 100644 roads.go diff --git a/main.go b/main.go index 28b8bcf..c4a8c71 100644 --- a/main.go +++ b/main.go @@ -51,6 +51,7 @@ func main() { var lakes [][]image.Point var riverPixels []image.Point var treePixels []image.Point + var roadPixels []image.Point configDir, err := os.UserConfigDir() if err != nil { @@ -107,25 +108,63 @@ func main() { } if canvasImg.Image == nil || heightmapImg.Image == nil { + // Initial image generation + noiseImg := GenerateHeightmap(settings.Width, settings.Height, int(settings.Detail), 100.0, settings.Seed) + var lakeImage image.Image + lakeImage, lakes = GenerateLakes(settings.Width, settings.Height, settings.Lakes, settings.LakeSizeLower, settings.LakeSizeUpper, noiseImg, settings.Seed) + var riverImage image.Image + riverImage, riverPixels = GenerateRivers(settings.Width, settings.Height, settings.Rivers, settings.MinRiverWidth, settings.MaxRiverWidth, settings.RiverCurvyness, lakeImage, lakes, settings.Seed, noiseImg) + finalImage := riverImage.(*image.RGBA) - var flatLakePixels []image.Point - for _, lake := range lakes { - flatLakePixels = append(flatLakePixels, lake...) + var roadImage *image.RGBA + + roadPixels, roadImage = GenerateRoads(settings.Width, settings.Height, settings, noiseImg) + + // Draw roadImage over finalImage + + for y := 0; y < roadImage.Bounds().Max.Y; y++ { + + for x := 0; x < roadImage.Bounds().Max.X; x++ { + + r, g, b, a := roadImage.At(x, y).RGBA() + + if a > 0 { + + finalImage.Set(x, y, color.RGBA{R: uint8(r >> 8), G: uint8(g >> 8), B: uint8(b >> 8), A: uint8(a >> 8)}) + + } + + } + } + + var flatLakePixels []image.Point + + for _, lake := range lakes { + + flatLakePixels = append(flatLakePixels, lake...) + + } + allWaterPixels := append(flatLakePixels, riverPixels...) treePixels = 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 + } detailLabel := widget.NewLabel(fmt.Sprintf("Detail: %.0f", settings.Detail)) @@ -259,6 +298,67 @@ func main() { } treeClumpinessSlider.SetValue(settings.TreeClumpiness) + numRoadsLabel := widget.NewLabel(fmt.Sprintf("Number of Roads: %d", settings.NumRoads)) + numRoadsSlider := widget.NewSlider(1, 1000) + numRoadsSlider.OnChanged = func(val float64) { + settings.NumRoads = int(val) + numRoadsLabel.SetText(fmt.Sprintf("Number of Roads: %d", settings.NumRoads)) + } + numRoadsSlider.SetValue(float64(settings.NumRoads)) + + minRoadWidthLabel := widget.NewLabel(fmt.Sprintf("Min Road Width: %.0fpx", settings.MinRoadWidth)) + minRoadWidthSlider := widget.NewSlider(1, 150) + maxRoadWidthLabel := widget.NewLabel(fmt.Sprintf("Max Road Width: %.0fpx", settings.MaxRoadWidth)) + maxRoadWidthSlider := widget.NewSlider(1, 150) + + minRoadWidthSlider.OnChanged = func(val float64) { + settings.MinRoadWidth = val + if settings.MinRoadWidth > settings.MaxRoadWidth { + settings.MaxRoadWidth = settings.MinRoadWidth + maxRoadWidthSlider.SetValue(settings.MaxRoadWidth) + } + minRoadWidthLabel.SetText(fmt.Sprintf("Min Road Width: %.0fpx", settings.MinRoadWidth)) + } + minRoadWidthSlider.SetValue(settings.MinRoadWidth) + + maxRoadWidthSlider.OnChanged = func(val float64) { + settings.MaxRoadWidth = val + if settings.MaxRoadWidth < settings.MinRoadWidth { + settings.MinRoadWidth = settings.MaxRoadWidth + minRoadWidthSlider.SetValue(settings.MinRoadWidth) + } + maxRoadWidthLabel.SetText(fmt.Sprintf("Max Road Width: %.0fpx", settings.MaxRoadWidth)) + } + maxRoadWidthSlider.SetValue(settings.MaxRoadWidth) + + roadExitsLabel := widget.NewLabel(fmt.Sprintf("Road Exits: %d", settings.RoadExits)) + roadExitsSlider := widget.NewSlider(1, 100) + roadExitsSlider.OnChanged = func(val float64) { + if val > float64(settings.NumRoads) { + val = float64(settings.NumRoads) + roadExitsSlider.SetValue(val) + } + settings.RoadExits = int(val) + roadExitsLabel.SetText(fmt.Sprintf("Road Exits: %d", settings.RoadExits)) + } + roadExitsSlider.SetValue(float64(settings.RoadExits)) + + roadCurvynessLabel := widget.NewLabel(fmt.Sprintf("Road Curvyness: %.0f%%", settings.RoadCurvyness)) + roadCurvynessSlider := widget.NewSlider(0, 100) + roadCurvynessSlider.OnChanged = func(val float64) { + settings.RoadCurvyness = val + roadCurvynessLabel.SetText(fmt.Sprintf("Road Curvyness: %.0f%%", settings.RoadCurvyness)) + } + roadCurvynessSlider.SetValue(settings.RoadCurvyness) + + roadDistributionLabel := widget.NewLabel(fmt.Sprintf("Distribution: %.0f%%", settings.RoadDistribution)) + roadDistributionSlider := widget.NewSlider(0, 100) + roadDistributionSlider.OnChanged = func(val float64) { + settings.RoadDistribution = val + roadDistributionLabel.SetText(fmt.Sprintf("Distribution: %.0f%%", settings.RoadDistribution)) + } + roadDistributionSlider.SetValue(settings.RoadDistribution) + errorLabel := canvas.NewText("", color.RGBA{R: 255, A: 255}) errorLabel.TextSize = 12 errorLabel.Hide() @@ -272,7 +372,7 @@ func main() { showSaveDialog(w, heightmapImg.Image, settings) }) exportMasksBtn := widget.NewButton("Export Masks", func() { - showMasksSaveDialog(w, canvasImg.Image, heightmapImg.Image, settings, lakes, riverPixels, treePixels) + showMasksSaveDialog(w, canvasImg.Image, heightmapImg.Image, settings, lakes, riverPixels, treePixels, roadPixels) }) generateBtn = widget.NewButton("Generate", func() { @@ -286,7 +386,7 @@ func main() { }) }() - steps := 7 + steps := 8 currentStep := 0 // Step 1: Generating Heightmap @@ -316,13 +416,31 @@ func main() { riverImage, riverPixels = GenerateRivers(settings.Width, settings.Height, settings.Rivers, settings.MinRiverWidth, settings.MaxRiverWidth, settings.RiverCurvyness, lakeImage, lakes, settings.Seed, noiseImg) finalImage := riverImage.(*image.RGBA) + + // Step 4: Generating Roads + currentStep++ + fyne.Do(func() { + progressBar.SetText(fmt.Sprintf("Step %d/%d: Generating Roads", currentStep, steps)) + progressBar.SetValue(float64(currentStep) / float64(steps)) + }) + var roadImage *image.RGBA + roadPixels, roadImage = GenerateRoads(settings.Width, settings.Height, settings, noiseImg) // Draw roadImage over finalImage + for y := 0; y < roadImage.Bounds().Max.Y; y++ { + for x := 0; x < roadImage.Bounds().Max.X; x++ { + r, g, b, a := roadImage.At(x, y).RGBA() + if a > 0 { + finalImage.Set(x, y, color.RGBA{R: uint8(r >> 8), G: uint8(g >> 8), B: uint8(b >> 8), A: uint8(a >> 8)}) + } + } + } + var flatLakePixels []image.Point for _, lake := range lakes { flatLakePixels = append(flatLakePixels, lake...) } allWaterPixels := append(flatLakePixels, riverPixels...) - // Step 4: Darkening Water Areas + // Step 5: Darkening Water Areas currentStep++ fyne.Do(func() { progressBar.SetText(fmt.Sprintf("Step %d/%d: Darkening Water Areas", currentStep, steps)) @@ -330,7 +448,7 @@ func main() { }) darkenedHeightmap := DarkenLakeAreas(noiseImg, allWaterPixels) - // Step 5: Applying Roughness + // Step 6: Applying Roughness currentStep++ fyne.Do(func() { progressBar.SetText(fmt.Sprintf("Step %d/%d: Applying Roughness", currentStep, steps)) @@ -338,7 +456,7 @@ func main() { }) compositeImg := ApplyRoughness(darkenedHeightmap, settings.Roughness) - // Step 6: Generating Trees + // Step 7: Generating Trees currentStep++ fyne.Do(func() { progressBar.SetText(fmt.Sprintf("Step %d/%d: Generating Trees", currentStep, steps)) @@ -346,7 +464,7 @@ func main() { }) treePixels = GenerateTrees(finalImage, allWaterPixels, settings.MinTreeSize, settings.MaxTreeSize, settings.TreeCoverage, settings.TreeClumpiness, settings.Seed) - // Step 7: Finalizing Images + // Step 8: Finalizing Images currentStep++ fyne.Do(func() { progressBar.SetText(fmt.Sprintf("Step %d/%d: Finalizing Images", currentStep, steps)) @@ -448,10 +566,26 @@ func main() { riverCurvynessSlider, )) + roadDistributionSlider.SetValue(settings.RoadDistribution) + + roadsTab := container.NewTabItem("Roads", container.NewVBox( + numRoadsLabel, + numRoadsSlider, + minRoadWidthLabel, + minRoadWidthSlider, + maxRoadWidthLabel, + maxRoadWidthSlider, + roadExitsLabel, + roadExitsSlider, + roadCurvynessLabel, + roadCurvynessSlider, + roadDistributionLabel, + roadDistributionSlider, + )) + imageTab := container.NewTabItem("Image", container.NewVBox( widget.NewLabel("Width:"), - widthEntry, - widget.NewLabel("Height:"), + widthEntry, widget.NewLabel("Height:"), heightEntry, widget.NewLabel("Seed:"), seedEntry, @@ -469,6 +603,7 @@ func main() { imageTab, terrainTab, waterTab, + roadsTab, ) left := container.NewVBox( @@ -504,7 +639,7 @@ func getImageData(img image.Image, format string) (*bytes.Buffer, error) { return buf, err } -func showMasksSaveDialog(win fyne.Window, canvasImg, heightmapImg image.Image, settings *Settings, lakes [][]image.Point, riverPixels, treePixels []image.Point) { +func showMasksSaveDialog(win fyne.Window, canvasImg, heightmapImg image.Image, settings *Settings, lakes [][]image.Point, riverPixels, treePixels, roadPixels []image.Point) { fileNameEntry := widget.NewEntry() fileNameEntry.SetPlaceHolder("masks_folder") @@ -566,6 +701,10 @@ func showMasksSaveDialog(win fyne.Window, canvasImg, heightmapImg image.Image, s for _, p := range treePixels { treeMask.SetGray(p.X, p.Y, color.Gray{Y: 255}) } + roadMask := image.NewGray(bounds) + for _, p := range roadPixels { + roadMask.SetGray(p.X, p.Y, color.Gray{Y: 255}) + } imagesToSave := map[string]image.Image{ "canvas." + imgFormat: canvasImg, @@ -573,6 +712,7 @@ func showMasksSaveDialog(win fyne.Window, canvasImg, heightmapImg image.Image, s "lakes_mask." + imgFormat: lakeMask, "rivers_mask." + imgFormat: riverMask, "trees_mask." + imgFormat: treeMask, + "roads_mask." + imgFormat: roadMask, } switch packageSelect.Selected { diff --git a/roads.go b/roads.go new file mode 100644 index 0000000..cb52657 --- /dev/null +++ b/roads.go @@ -0,0 +1,280 @@ +package main + +import ( + "fmt" + "image" + "image/color" + "math" + "math/rand" + "sort" + "unsafe" +) + +type PointOfInterest struct { + X, Y int + Connections int +} + +type Road struct { + Start, End, Control *PointOfInterest + Width int + Points []image.Point + Importance int +} + +func GenerateRoads(width, height int, settings *Settings, noiseImg image.Image) ([]image.Point, *image.RGBA) { + img := image.NewRGBA(image.Rect(0, 0, width, height)) + // Transparent background + for y := 0; y < height; y++ { + for x := 0; x < width; x++ { + img.Set(x, y, color.Transparent) + } + } + + rand.Seed(settings.Seed) + roadColor := color.RGBA{R: 139, G: 69, B: 19, A: 255} + + pois := generatePOIs(width, height, settings) + if len(pois) == 0 { + return nil, img + } + + roads := connectPOIs(pois, width, height, settings) + assignRoadWidths(roads, settings) + + var allRoadPixels []image.Point + for _, road := range roads { + roadPixels := drawCurve(img, road.Start, road.End, road.Control, roadColor, road.Width) + allRoadPixels = append(allRoadPixels, roadPixels...) + } + + return allRoadPixels, img +} + +func generatePOIs(width, height int, settings *Settings) []*PointOfInterest { + numPOIs := settings.NumRoads / 2 + if numPOIs == 0 { + return nil + } + + numExits := settings.RoadExits + if numExits > settings.NumRoads { + numExits = settings.NumRoads + } + + pois := make([]*PointOfInterest, numPOIs) + centerX := width / 2 + centerY := height / 2 + + // Distribution affects the radius + maxRadius := math.Min(float64(width)/2, float64(height)/2) + radius := maxRadius * (settings.RoadDistribution / 100.0) + + for i := 0; i < numPOIs; i++ { + // Handle exits + if i < numExits { + side := rand.Intn(4) + var x, y int + switch side { + case 0: // Top + x = rand.Intn(width) + y = 0 + case 1: // Bottom + x = rand.Intn(width) + y = height - 1 + case 2: // Left + x = 0 + y = rand.Intn(height) + case 3: // Right + x = width - 1 + y = rand.Intn(height) + } + pois[i] = &PointOfInterest{X: x, Y: y} + continue + } + + angle := rand.Float64() * 2 * math.Pi + r := rand.Float64() * radius + x := int(float64(centerX) + r*math.Cos(angle)) + y := int(float64(centerY) + r*math.Sin(angle)) + pois[i] = &PointOfInterest{X: x, Y: y} + } + + return pois +} + +func connectPOIs(pois []*PointOfInterest, width, height int, settings *Settings) []*Road { + if len(pois) < 2 { + return nil + } + + var roads []*Road + visited := make(map[*PointOfInterest]bool) + existingRoads := make(map[string]bool) + + // Find the center-most POI + centerX := width / 2 + centerY := height / 2 + var startNode *PointOfInterest + minDist := -1.0 + + for _, poi := range pois { + dist := math.Sqrt(math.Pow(float64(poi.X-centerX), 2) + math.Pow(float64(poi.Y-centerY), 2)) + if startNode == nil || dist < minDist { + minDist = dist + startNode = poi + } + } + + visited[startNode] = true + + for len(visited) < len(pois) { + var closest *PointOfInterest + var fromNode *PointOfInterest + minDist := -1.0 + + for poi := range visited { + for _, other := range pois { + if !visited[other] { + dist := math.Sqrt(math.Pow(float64(poi.X-other.X), 2) + math.Pow(float64(poi.Y-other.Y), 2)) + + // Check if road exists + key := fmt.Sprintf("%p-%p", poi, other) + if uintptr(unsafe.Pointer(poi)) > uintptr(unsafe.Pointer(other)) { + key = fmt.Sprintf("%p-%p", other, poi) + } + if existingRoads[key] { + continue + } + + if closest == nil || dist < minDist { + minDist = dist + closest = other + fromNode = poi + } + } + } + } + + if closest != nil { + visited[closest] = true + fromNode.Connections++ + closest.Connections++ + + // Add road to existing roads map + key := fmt.Sprintf("%p-%p", fromNode, closest) + if uintptr(unsafe.Pointer(fromNode)) > uintptr(unsafe.Pointer(closest)) { + key = fmt.Sprintf("%p-%p", closest, fromNode) + } + existingRoads[key] = true + + // Create control point for curve + midX := (fromNode.X + closest.X) / 2 + midY := (fromNode.Y + closest.Y) / 2 + dist := math.Sqrt(math.Pow(float64(fromNode.X-closest.X), 2) + math.Pow(float64(fromNode.Y-closest.Y), 2)) + offset := dist * (settings.RoadCurvyness / 100.0) * (rand.Float64() - 0.5) + + controlX := int(float64(midX) + offset) + controlY := int(float64(midY) + offset) + + roads = append(roads, &Road{ + Start: fromNode, + End: closest, + Control: &PointOfInterest{X: controlX, Y: controlY}, + }) + } + } + + for _, road := range roads { + road.Importance = road.Start.Connections + road.End.Connections + } + + return roads +} + +func assignRoadWidths(roads []*Road, settings *Settings) { + if len(roads) == 0 { + return + } + + sort.Slice(roads, func(i, j int) bool { + return roads[i].Importance > roads[j].Importance + }) + + minWidth := settings.MinRoadWidth + maxWidth := settings.MaxRoadWidth + widthStep := 0.0 + if len(roads) > 1 { + widthStep = (maxWidth - minWidth) / float64(len(roads)-1) + } + + for i, road := range roads { + road.Width = int(maxWidth - float64(i)*widthStep) + } +} + +func drawCurve(img *image.RGBA, p0, p1, p2 *PointOfInterest, col color.Color, width int) []image.Point { + var points []image.Point + var lastX, lastY int = -1, -1 + for t := 0.0; t <= 1.0; t += 0.01 { + x := (1-t)*(1-t)*float64(p0.X) + 2*(1-t)*t*float64(p2.X) + t*t*float64(p1.X) + y := (1-t)*(1-t)*float64(p0.Y) + 2*(1-t)*t*float64(p2.Y) + t*t*float64(p1.Y) + + if lastX != -1 { + linePoints := drawLine(img, lastX, lastY, int(x), int(y), col, width) + points = append(points, linePoints...) + } + lastX, lastY = int(x), int(y) + } + return points +} + +// Bresenham's line algorithm for drawing segments of the curve +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) + dy := -abs(y1 - y0) + sx := -1 + if x0 < x1 { + sx = 1 + } + sy := -1 + if y0 < y1 { + sy = 1 + } + err := dx + dy + + for { + for i := -width / 2; i <= width/2; i++ { + for j := -width / 2; j <= width/2; j++ { + px := x0 + i + py := y0 + j + if img.Bounds().Min.X <= px && px < img.Bounds().Max.X && img.Bounds().Min.Y <= py && py < img.Bounds().Max.Y { + img.Set(px, py, col) + points = append(points, image.Point{X: px, Y: py}) + } + } + } + + if x0 == x1 && y0 == y1 { + break + } + e2 := 2 * err + if e2 >= dy { + err += dy + x0 += sx + } + if e2 <= dx { + err += dx + y0 += sy + } + } + return points +} + +func abs(x int) int { + if x < 0 { + return -x + } + return x +} diff --git a/settings.go b/settings.go index fda342c..f894e72 100644 --- a/settings.go +++ b/settings.go @@ -8,23 +8,29 @@ import ( ) type Settings struct { - Detail float64 `json:"detail"` - Roughness float64 `json:"roughness"` - Width int `json:"width"` - Height int `json:"height"` - Lakes int `json:"lakes"` - LakeSizeLower float64 `json:"lake_size_lower"` - LakeSizeUpper float64 `json:"lake_size_upper"` - MinTreeSize float64 `json:"min_tree_size"` - MaxTreeSize float64 `json:"max_tree_size"` - 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"` - LastExportPath string `json:"last_export_path"` + Detail float64 `json:"detail"` + Roughness float64 `json:"roughness"` + Width int `json:"width"` + Height int `json:"height"` + Lakes int `json:"lakes"` + LakeSizeLower float64 `json:"lake_size_lower"` + LakeSizeUpper float64 `json:"lake_size_upper"` + MinTreeSize float64 `json:"min_tree_size"` + MaxTreeSize float64 `json:"max_tree_size"` + 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"` + NumRoads int `json:"num_roads"` + MinRoadWidth float64 `json:"min_road_width"` + MaxRoadWidth float64 `json:"max_road_width"` + RoadExits int `json:"road_exits"` + RoadCurvyness float64 `json:"road_curvyness"` + RoadDistribution float64 `json:"road_distribution"` + LastExportPath string `json:"last_export_path"` } func (s *Settings) Save() error { @@ -64,23 +70,29 @@ func LoadSettings() (*Settings, error) { homeDir = "." } return &Settings{ - Detail: 1, - Roughness: 0, - Width: 300, - Height: 300, - Lakes: 0, - LakeSizeLower: 1, - LakeSizeUpper: 5, - MinTreeSize: 5, - MaxTreeSize: 20, - TreeCoverage: 20, - TreeClumpiness: 50, - Seed: time.Now().UnixNano(), - Rivers: 0, - MinRiverWidth: 1, - MaxRiverWidth: 5, - RiverCurvyness: 50, - LastExportPath: homeDir, + Detail: 1, + Roughness: 0, + Width: 300, + Height: 300, + Lakes: 0, + LakeSizeLower: 1, + LakeSizeUpper: 5, + MinTreeSize: 5, + MaxTreeSize: 20, + TreeCoverage: 20, + TreeClumpiness: 50, + Seed: time.Now().UnixNano(), + Rivers: 0, + MinRiverWidth: 1, + MaxRiverWidth: 5, + RiverCurvyness: 50, + NumRoads: 100, + MinRoadWidth: 2, + MaxRoadWidth: 8, + RoadExits: 5, + RoadCurvyness: 50, + RoadDistribution: 50, + LastExportPath: homeDir, }, nil } return nil, err