From d01cb7a24726375aa92313aeb985d25d31cb8c04 Mon Sep 17 00:00:00 2001 From: Grimsace Date: Thu, 26 Feb 2026 13:03:48 -0600 Subject: [PATCH] changed how masks are stored for massive performance improvment and way less ram usage --- buildings.go | 64 ++++++++++++------------ main.go | 134 +++++++++++++++++++++------------------------------ mask.go | 91 ++++++++++++++++++++++++++++++++++ roads.go | 108 +++++++++++++++++++---------------------- terrain.go | 69 +++++++++++++++----------- water.go | 21 ++++---- 6 files changed, 277 insertions(+), 210 deletions(-) create mode 100644 mask.go diff --git a/buildings.go b/buildings.go index cae90d6..aba9b1b 100644 --- a/buildings.go +++ b/buildings.go @@ -62,7 +62,17 @@ func getBuildingSizeRangePixels(settings *Settings, width, height int) (float64, } // GenerateBuildings creates and places buildings on the map. -func GenerateBuildings(img *image.RGBA, width, height int, settings *Settings, roadPixels, allWaterPixels []image.Point, seed int64) ([][]image.Point, []image.Point) { +func GenerateBuildings( + img *image.RGBA, + width, + height int, + settings *Settings, + roadAnchors []image.Point, + waterMask, + roadMask, + exitRoadMask *PixelMask, + seed int64, +) ([][]image.Point, *PixelMask) { // Early exit if no buildings are to be generated if settings.NumBuildings == 0 { return nil, nil @@ -70,32 +80,25 @@ func GenerateBuildings(img *image.RGBA, width, height int, settings *Settings, r randSrc := rand.New(rand.NewSource(seed)) buildingColor := color.RGBA{R: 128, G: 128, B: 128, A: 255} // Gray color for buildings - - isWater := make(map[image.Point]bool) - for _, p := range allWaterPixels { - isWater[p] = true + if waterMask == nil { + waterMask = NewPixelMask(width, height) } - - isRoad := make(map[image.Point]bool) - for _, p := range roadPixels { - isRoad[p] = true + if roadMask == nil { + roadMask = NewPixelMask(width, height) } - isExitRoad := make(map[image.Point]bool) - for _, p := range getExitRoadPixels() { - isExitRoad[p] = true + if exitRoadMask == nil { + exitRoadMask = NewPixelMask(width, height) } - - isBuilding := make(map[image.Point]bool) + buildingMask := NewPixelMask(width, height) var buildings [][]image.Point - var allBuildingPixels []image.Point var anchorPoints []image.Point var normalRoadAnchors []image.Point var exitRoadAnchors []image.Point - if len(roadPixels) > 0 { - anchorPoints = roadPixels + if len(roadAnchors) > 0 { + anchorPoints = roadAnchors for _, p := range anchorPoints { - if isExitRoad[p] { + if exitRoadMask.GetPoint(p) { exitRoadAnchors = append(exitRoadAnchors, p) } else { normalRoadAnchors = append(normalRoadAnchors, p) @@ -106,7 +109,7 @@ func GenerateBuildings(img *image.RGBA, width, height int, settings *Settings, r for y := 0; y < height; y++ { for x := 0; x < width; x++ { p := image.Point{X: x, Y: y} - if !isWater[p] { + if !waterMask.GetPoint(p) { anchorPoints = append(anchorPoints, p) } } @@ -129,7 +132,7 @@ func GenerateBuildings(img *image.RGBA, width, height int, settings *Settings, r for y := 0; y < height; y++ { for x := 0; x < width; x++ { p := image.Point{X: x, Y: y} - if !isWater[p] && !isRoad[p] { + if !waterMask.GetPoint(p) && !roadMask.GetPoint(p) { landPoints = append(landPoints, p) } } @@ -196,17 +199,16 @@ func GenerateBuildings(img *image.RGBA, width, height int, settings *Settings, r var ok bool if shape == "procedural" { - pixels, ok = getProceduralBuildingPixels(center, size, settings, isWater, isRoad, isBuilding, width, height, randSrc) + pixels, ok = getProceduralBuildingPixels(center, size, settings, waterMask, roadMask, buildingMask, width, height, randSrc) } else { - pixels, ok = getBuildingPixels(center, size, shape, isWater, isRoad, isBuilding, width, height, randSrc) + pixels, ok = getBuildingPixels(center, size, shape, waterMask, roadMask, buildingMask, width, height, randSrc) } if ok { // If successful, draw the building and update data structures for _, p := range pixels { img.Set(p.X, p.Y, buildingColor) - isBuilding[p] = true - allBuildingPixels = append(allBuildingPixels, p) + buildingMask.SetPoint(p) } buildings = append(buildings, pixels) buildingsPlaced++ @@ -214,11 +216,11 @@ func GenerateBuildings(img *image.RGBA, width, height int, settings *Settings, r } } } - return buildings, allBuildingPixels + return buildings, buildingMask } // getProceduralBuildingPixels generates a complex building by connecting multiple shapes. -func getProceduralBuildingPixels(center image.Point, size float64, settings *Settings, isWater, isRoad, isBuilding map[image.Point]bool, width, height int, randSrc *rand.Rand) ([]image.Point, bool) { +func getProceduralBuildingPixels(center image.Point, size float64, settings *Settings, waterMask, roadMask, buildingMask *PixelMask, width, height int, randSrc *rand.Rand) ([]image.Point, bool) { complexity := settings.MinBuildingComplexity if settings.BuildingComplexityRatio > randSrc.Float64()*100 { complexity = settings.MinBuildingComplexity + randSrc.Intn(settings.MaxBuildingComplexity-settings.MinBuildingComplexity+1) @@ -293,7 +295,7 @@ func getProceduralBuildingPixels(center image.Point, size float64, settings *Set continue } for _, p := range pixels { - if p.X < 0 || p.Y < 0 || p.X >= width || p.Y >= height || isWater[p] || isRoad[p] || isBuilding[p] { + if p.X < 0 || p.Y < 0 || p.X >= width || p.Y >= height || waterMask.GetPoint(p) || roadMask.GetPoint(p) || buildingMask.GetPoint(p) { return nil, false } if !pixelMap[p] { @@ -436,7 +438,7 @@ func chooseShape(randSrc *rand.Rand, ratios map[string]float64) string { } // getBuildingPixels determines the pixels for a single building based on its shape and checks for collisions. -func getBuildingPixels(center image.Point, size float64, shape string, isWater, isRoad, isBuilding map[image.Point]bool, width, height int, randSrc *rand.Rand) ([]image.Point, bool) { +func getBuildingPixels(center image.Point, size float64, shape string, waterMask, roadMask, buildingMask *PixelMask, width, height int, randSrc *rand.Rand) ([]image.Point, bool) { var pixels []image.Point var halfSize = int(size / 2) @@ -446,7 +448,7 @@ func getBuildingPixels(center image.Point, size float64, shape string, isWater, for y := center.Y - halfSize; y <= center.Y+halfSize; y++ { for x := center.X - halfSize; x <= center.X+halfSize; x++ { p := image.Point{X: x, Y: y} - if p.X < 0 || p.Y < 0 || p.X >= width || p.Y >= height || isWater[p] || isRoad[p] || isBuilding[p] { + if p.X < 0 || p.Y < 0 || p.X >= width || p.Y >= height || waterMask.GetPoint(p) || roadMask.GetPoint(p) || buildingMask.GetPoint(p) { return nil, false // Collision detected } pixels = append(pixels, p) @@ -459,7 +461,7 @@ func getBuildingPixels(center image.Point, size float64, shape string, isWater, dx, dy := float64(x-center.X), float64(y-center.Y) if dx*dx+dy*dy <= r2 { p := image.Point{X: x, Y: y} - if p.X < 0 || p.Y < 0 || p.X >= width || p.Y >= height || isWater[p] || isRoad[p] || isBuilding[p] { + if p.X < 0 || p.Y < 0 || p.X >= width || p.Y >= height || waterMask.GetPoint(p) || roadMask.GetPoint(p) || buildingMask.GetPoint(p) { return nil, false // Collision detected } pixels = append(pixels, p) @@ -480,7 +482,7 @@ func getBuildingPixels(center image.Point, size float64, shape string, isWater, 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} - if p.X < 0 || p.Y < 0 || p.X >= width || p.Y >= height || isWater[p] || isRoad[p] || isBuilding[p] { + if p.X < 0 || p.Y < 0 || p.X >= width || p.Y >= height || waterMask.GetPoint(p) || roadMask.GetPoint(p) || buildingMask.GetPoint(p) { return nil, false // Collision detected } pixels = append(pixels, p) diff --git a/main.go b/main.go index a3b2c0f..8062497 100644 --- a/main.go +++ b/main.go @@ -55,12 +55,14 @@ func main() { // Initialize slices to store generated map features var lakes [][]image.Point - var riverPixels []image.Point - var treePixels []image.Point - var buildingPixels []image.Point var buildings [][]image.Point - var roadPixels []image.Point - var bridgePixels []image.Point + var waterMask *PixelMask + var riverMask *PixelMask + var treeMask *PixelMask + var buildingMask *PixelMask + var roadMask *PixelMask + var bridgeMask *PixelMask + var exitRoadMask *PixelMask // Set up application configuration directory configDir, err := os.UserConfigDir() @@ -157,47 +159,31 @@ func main() { // Step 3: Generating Rivers var riverImage image.Image - - riverImage, riverPixels = GenerateRivers(settings.Width, settings.Height, settings.Rivers, settings.MinRiverWidth, settings.MaxRiverWidth, settings.RiverCurvyness, lakeImage, lakes, seedProvider.Next(), noiseImg, settings.RiverWidthVariability, settings.RiverEdgeRoughness) + riverImage, riverMask = GenerateRivers(settings.Width, settings.Height, settings.Rivers, settings.MinRiverWidth, settings.MaxRiverWidth, settings.RiverCurvyness, lakeImage, lakes, seedProvider.Next(), noiseImg, settings.RiverWidthVariability, settings.RiverEdgeRoughness) finalImage := riverImage.(*image.RGBA) - - var roadImage *image.RGBA - - var flatLakePixels []image.Point - - for _, lake := range lakes { - - flatLakePixels = append(flatLakePixels, lake...) - + waterMask = BuildMaskFromLakes(settings.Width, settings.Height, lakes) + if riverMask != nil { + waterMask.Merge(riverMask) } - - allWaterPixels := append(flatLakePixels, riverPixels...) // Step 4: Generating Roads - roadPixels, bridgePixels, roadImage = GenerateRoads(settings.Width, settings.Height, settings, noiseImg, allWaterPixels, seedProvider.Next()) - 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 roadAnchors []image.Point + roadMask, bridgeMask, exitRoadMask, roadAnchors = GenerateRoads(finalImage, settings.Width, settings.Height, settings, waterMask, seedProvider.Next()) // Step 5: Generating Buildings - buildings, buildingPixels = GenerateBuildings(finalImage, settings.Width, settings.Height, settings, roadPixels, allWaterPixels, seedProvider.Next()) + buildings, buildingMask = GenerateBuildings(finalImage, settings.Width, settings.Height, settings, roadAnchors, waterMask, roadMask, exitRoadMask, seedProvider.Next()) // Step 6: Generating Trees - treePixels = GenerateTrees(finalImage, allWaterPixels, roadPixels, buildingPixels, settings.MinTreeSize, settings.MaxTreeSize, settings.TreeCoverage, settings.TreeClumpiness, seedProvider.Next()) + treeMask = GenerateTrees(finalImage, waterMask, roadMask, buildingMask, settings.MinTreeSize, settings.MaxTreeSize, settings.TreeCoverage, settings.TreeClumpiness, seedProvider.Next()) // Step 7: Darkening Water Areas - darkenedHeightmap := DarkenLakeAreas(noiseImg, allWaterPixels) + darkenedHeightmap := DarkenLakeAreas(noiseImg, waterMask) // Step 8: Flattening Building Areas flattenedBuildingHeightmap := FlattenBuildingAreas(darkenedHeightmap.(*image.RGBA), buildings, settings.Width, settings.Height) // Step 9: Flattening Road Areas - flattenedHeightmap := FlattenRoadAreas(flattenedBuildingHeightmap, roadPixels) + flattenedHeightmap := FlattenRoadAreas(flattenedBuildingHeightmap, roadMask) // Step 10: Applying Roughness compositeImg := ApplyRoughness(flattenedHeightmap, settings.Roughness) @@ -519,7 +505,7 @@ func main() { showSaveDialog(w, bumpmapImg.Image, settings) }) exportMasksBtn := widget.NewButton("Export Masks", func() { - showMasksSaveDialog(w, canvasImg.Image, heightmapImg.Image, bumpmapImg.Image, settings, lakes, riverPixels, treePixels, roadPixels, bridgePixels, buildingPixels, buildings) + showMasksSaveDialog(w, canvasImg.Image, heightmapImg.Image, bumpmapImg.Image, settings, lakes, riverMask, treeMask, roadMask, bridgeMask, buildingMask) }) // Main generation button and logic generateBtn = widget.NewButton("Generate", func() { @@ -560,15 +546,13 @@ func main() { progressBar.SetValue(3.0 / 11.0) }) var riverImage image.Image - riverImage, riverPixels = GenerateRivers(settings.Width, settings.Height, settings.Rivers, settings.MinRiverWidth, settings.MaxRiverWidth, settings.RiverCurvyness, lakeImage, lakes, seedProvider.Next(), noiseImg, settings.RiverWidthVariability, settings.RiverEdgeRoughness) + riverImage, riverMask = GenerateRivers(settings.Width, settings.Height, settings.Rivers, settings.MinRiverWidth, settings.MaxRiverWidth, settings.RiverCurvyness, lakeImage, lakes, seedProvider.Next(), noiseImg, settings.RiverWidthVariability, settings.RiverEdgeRoughness) finalImage := riverImage.(*image.RGBA) - - var flatLakePixels []image.Point - for _, lake := range lakes { - flatLakePixels = append(flatLakePixels, lake...) + waterMask = BuildMaskFromLakes(settings.Width, settings.Height, lakes) + if riverMask != nil { + waterMask.Merge(riverMask) } - allWaterPixels := append(flatLakePixels, riverPixels...) // Step 4: Generating Roads @@ -576,16 +560,8 @@ func main() { progressLabel.SetText("Step 4 of 11: Generating Roads") progressBar.SetValue(4.0 / 11.0) }) - var roadImage *image.RGBA - roadPixels, bridgePixels, roadImage = GenerateRoads(settings.Width, settings.Height, settings, noiseImg, allWaterPixels, seedProvider.Next()) // 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 roadAnchors []image.Point + roadMask, bridgeMask, exitRoadMask, roadAnchors = GenerateRoads(finalImage, settings.Width, settings.Height, settings, waterMask, seedProvider.Next()) // Step 5: Generating Buildings @@ -593,7 +569,7 @@ func main() { progressLabel.SetText("Step 5 of 11: Generating Buildings") progressBar.SetValue(5.0 / 11.0) }) - buildings, buildingPixels = GenerateBuildings(finalImage, settings.Width, settings.Height, settings, roadPixels, allWaterPixels, seedProvider.Next()) + buildings, buildingMask = GenerateBuildings(finalImage, settings.Width, settings.Height, settings, roadAnchors, waterMask, roadMask, exitRoadMask, seedProvider.Next()) // Step 6: Darkening Water Areas @@ -601,7 +577,7 @@ func main() { progressLabel.SetText("Step 6 of 11: Darkening Water Areas") progressBar.SetValue(6.0 / 11.0) }) - darkenedHeightmap := DarkenLakeAreas(noiseImg, allWaterPixels) + darkenedHeightmap := DarkenLakeAreas(noiseImg, waterMask) // Step 7: Flattening Building Areas @@ -615,7 +591,7 @@ func main() { progressLabel.SetText("Step 8 of 11: Flattening Road Areas") progressBar.SetValue(8.0 / 11.0) }) - flattenedHeightmap := FlattenRoadAreas(flattenedBuildingHeightmap, roadPixels) + flattenedHeightmap := FlattenRoadAreas(flattenedBuildingHeightmap, roadMask) // Step 8: Applying Roughness @@ -631,7 +607,7 @@ func main() { progressLabel.SetText("Step 10 of 11: Generating Trees") progressBar.SetValue(10.0 / 11.0) }) - treePixels = GenerateTrees(finalImage, allWaterPixels, roadPixels, buildingPixels, settings.MinTreeSize, settings.MaxTreeSize, settings.TreeCoverage, settings.TreeClumpiness, seedProvider.Next()) + treeMask = GenerateTrees(finalImage, waterMask, roadMask, buildingMask, settings.MinTreeSize, settings.MaxTreeSize, settings.TreeCoverage, settings.TreeClumpiness, seedProvider.Next()) // Step 10: Generating Bump Map @@ -1105,7 +1081,7 @@ func getImageData(img image.Image, format string) (*bytes.Buffer, error) { } // showMasksSaveDialog displays a dialog for saving the generated masks. -func showMasksSaveDialog(win fyne.Window, canvasImg, heightmapImg, bumpmapImg image.Image, settings *Settings, lakes [][]image.Point, riverPixels, treePixels, roadPixels, bridgePixels, buildingPixels []image.Point, buildings [][]image.Point) { +func showMasksSaveDialog(win fyne.Window, canvasImg, heightmapImg, bumpmapImg image.Image, settings *Settings, lakes [][]image.Point, riverMask, treeMask, roadMask, bridgeMask, buildingMask *PixelMask) { // Create UI elements for the save dialog fileNameEntry := widget.NewEntry() fileNameEntry.SetPlaceHolder("masks_folder") @@ -1152,6 +1128,21 @@ func showMasksSaveDialog(win fyne.Window, canvasImg, heightmapImg, bumpmapImg im } imgFormat := strings.ToLower(formatSelect.Selected) bounds := canvasImg.Bounds() + maskToGray := func(mask *PixelMask) *image.Gray { + out := image.NewGray(bounds) + if mask == nil { + return out + } + for y := 0; y < mask.Height; y++ { + row := y * mask.Width + for x := 0; x < mask.Width; x++ { + if mask.Data[row+x] != 0 { + out.SetGray(x, y, color.Gray{Y: 255}) + } + } + } + return out + } // Create mask images from the generated data lakeMask := image.NewGray(bounds) @@ -1160,39 +1151,22 @@ func showMasksSaveDialog(win fyne.Window, canvasImg, heightmapImg, bumpmapImg im lakeMask.SetGray(p.X, p.Y, color.Gray{Y: 255}) } } - riverMask := image.NewGray(bounds) - for _, p := range riverPixels { - riverMask.SetGray(p.X, p.Y, color.Gray{Y: 255}) - } - treeMask := image.NewGray(bounds) - 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}) - } - bridgeMask := image.NewGray(bounds) - for _, p := range bridgePixels { - bridgeMask.SetGray(p.X, p.Y, color.Gray{Y: 255}) - } - buildingMask := image.NewGray(bounds) - for _, building := range buildings { - for _, p := range building { - buildingMask.SetGray(p.X, p.Y, color.Gray{Y: 255}) - } - } + riverMaskImg := maskToGray(riverMask) + treeMaskImg := maskToGray(treeMask) + roadMaskImg := maskToGray(roadMask) + bridgeMaskImg := maskToGray(bridgeMask) + buildingMaskImg := maskToGray(buildingMask) imagesToSave := map[string]image.Image{ "canvas." + imgFormat: canvasImg, "heightmap." + imgFormat: heightmapImg, "bump_map." + imgFormat: bumpmapImg, "lakes_mask." + imgFormat: lakeMask, - "rivers_mask." + imgFormat: riverMask, - "trees_mask." + imgFormat: treeMask, - "roads_mask." + imgFormat: roadMask, - "bridges_mask." + imgFormat: bridgeMask, - "buildings_mask." + imgFormat: buildingMask, + "rivers_mask." + imgFormat: riverMaskImg, + "trees_mask." + imgFormat: treeMaskImg, + "roads_mask." + imgFormat: roadMaskImg, + "bridges_mask." + imgFormat: bridgeMaskImg, + "buildings_mask." + imgFormat: buildingMaskImg, } // Save the images based on the selected packaging option switch packageSelect.Selected { diff --git a/mask.go b/mask.go new file mode 100644 index 0000000..151aed0 --- /dev/null +++ b/mask.go @@ -0,0 +1,91 @@ +package main + +import "image" + +// PixelMask stores per-pixel occupancy using one byte per pixel. +type PixelMask struct { + Width int + Height int + Data []uint8 +} + +func NewPixelMask(width, height int) *PixelMask { + if width <= 0 || height <= 0 { + return &PixelMask{} + } + return &PixelMask{ + Width: width, + Height: height, + Data: make([]uint8, width*height), + } +} + +func (m *PixelMask) index(x, y int) int { + return y*m.Width + x +} + +func (m *PixelMask) InBounds(x, y int) bool { + return m != nil && x >= 0 && y >= 0 && x < m.Width && y < m.Height +} + +func (m *PixelMask) GetXY(x, y int) bool { + if !m.InBounds(x, y) { + return false + } + return m.Data[m.index(x, y)] != 0 +} + +func (m *PixelMask) SetXY(x, y int) { + if m.InBounds(x, y) { + m.Data[m.index(x, y)] = 1 + } +} + +func (m *PixelMask) GetPoint(p image.Point) bool { + return m.GetXY(p.X, p.Y) +} + +func (m *PixelMask) SetPoint(p image.Point) { + m.SetXY(p.X, p.Y) +} + +func (m *PixelMask) Merge(other *PixelMask) { + if m == nil || other == nil || m.Width != other.Width || m.Height != other.Height { + return + } + for i := range m.Data { + if other.Data[i] != 0 { + m.Data[i] = 1 + } + } +} + +func (m *PixelMask) AddPoints(points []image.Point) { + for _, p := range points { + m.SetPoint(p) + } +} + +func (m *PixelMask) ToPoints() []image.Point { + if m == nil { + return nil + } + pts := make([]image.Point, 0) + for y := 0; y < m.Height; y++ { + row := y * m.Width + for x := 0; x < m.Width; x++ { + if m.Data[row+x] != 0 { + pts = append(pts, image.Point{X: x, Y: y}) + } + } + } + return pts +} + +func BuildMaskFromLakes(width, height int, lakes [][]image.Point) *PixelMask { + mask := NewPixelMask(width, height) + for _, lake := range lakes { + mask.AddPoints(lake) + } + return mask +} diff --git a/roads.go b/roads.go index 82c0e98..7d37da2 100644 --- a/roads.go +++ b/roads.go @@ -31,8 +31,6 @@ type Road struct { Importance int } -var lastExitRoadPixels []image.Point - const ( minRoadWidthPercent = 0.1 maxRoadWidthPercent = 5.0 @@ -81,54 +79,50 @@ func getRoadWidthRangePixels(settings *Settings, width, height int) (float64, fl return minPx, maxPx } -func getExitRoadPixels() []image.Point { - out := make([]image.Point, len(lastExitRoadPixels)) - copy(out, lastExitRoadPixels) - return out -} - // GenerateRoads creates roads on the map. -func GenerateRoads(width, height int, settings *Settings, _ image.Image, allWaterPixels []image.Point, seed int64) ([]image.Point, []image.Point, *image.RGBA) { - img := image.NewRGBA(image.Rect(0, 0, width, height)) +func GenerateRoads( + img *image.RGBA, + width, + height int, + settings *Settings, + waterMask *PixelMask, + seed int64, +) (*PixelMask, *PixelMask, *PixelMask, []image.Point) { + if img == nil { + img = image.NewRGBA(image.Rect(0, 0, width, height)) + } 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} - waterMap := make(map[image.Point]bool, len(allWaterPixels)) - for _, p := range allWaterPixels { - waterMap[p] = true - } roadTarget := estimateRoadTarget(settings, randSrc) - pois := generatePOIs(width, height, settings, waterMap, randSrc, roadTarget) + pois := generatePOIs(width, height, settings, waterMask, randSrc, roadTarget) if len(pois) < 2 { - return nil, nil, img + return NewPixelMask(width, height), NewPixelMask(width, height), NewPixelMask(width, height), nil } - roads := connectPOIs(pois, width, height, settings, randSrc, waterMap, roadTarget) - roads = appendExitRoads(roads, pois, width, height, settings, randSrc, waterMap) + roads := connectPOIs(pois, width, height, settings, randSrc, waterMask, roadTarget) + roads = appendExitRoads(roads, pois, width, height, settings, randSrc, waterMask) if len(roads) == 0 { - return nil, nil, img + return NewPixelMask(width, height), NewPixelMask(width, height), NewPixelMask(width, height), nil } assignRoadWidths(roads, settings, randSrc, width, height) - allRoadPixels := make([]image.Point, 0, len(roads)*64) - allBridgePixels := make([]image.Point, 0, len(roads)*16) - exitRoadPixels := make([]image.Point, 0, len(roads)*16) + roadMask := NewPixelMask(width, height) + bridgeMask := NewPixelMask(width, height) + exitRoadMask := NewPixelMask(width, height) for _, road := range roads { - roadPixels, bridgePixels := drawRoad(img, road.Points, roadColor, bridgeColor, road.Width) - allRoadPixels = append(allRoadPixels, roadPixels...) - allBridgePixels = append(allBridgePixels, bridgePixels...) + drawRoadToMasks(img, road.Points, roadColor, bridgeColor, road.Width, roadMask, bridgeMask) if road.Start.IsExit || road.End.IsExit { - exitRoadPixels = append(exitRoadPixels, roadPixels...) - exitRoadPixels = append(exitRoadPixels, bridgePixels...) + drawRoadToMasks(img, road.Points, roadColor, bridgeColor, road.Width, exitRoadMask, exitRoadMask) } } - lastExitRoadPixels = exitRoadPixels - return allRoadPixels, allBridgePixels, img + roadAnchors := roadMask.ToPoints() + return roadMask, bridgeMask, exitRoadMask, roadAnchors } -func generatePOIs(width, height int, settings *Settings, waterMap map[image.Point]bool, randSrc *rand.Rand, roadTarget int) []*PointOfInterest { +func generatePOIs(width, height int, settings *Settings, waterMask *PixelMask, randSrc *rand.Rand, roadTarget int) []*PointOfInterest { distribution := clamp01(settings.RoadDistribution / 100.0) minBuildingSizePx, maxBuildingSizePx := getBuildingSizeRangePixels(settings, width, height) avgBuildingSize := (minBuildingSizePx + maxBuildingSizePx) / 2.0 @@ -160,7 +154,7 @@ func generatePOIs(width, height int, settings *Settings, waterMap map[image.Poin } p := image.Point{X: x, Y: y} // Keep larger spacing between intersections so buildings have room. - if waterMap[p] || isTooCloseToExisting(pois, x, y, avgBuildingSize*1.1) { + if waterMask.GetPoint(p) || isTooCloseToExisting(pois, x, y, avgBuildingSize*1.1) { continue } pois = append(pois, &PointOfInterest{X: x, Y: y, TargetDegree: sampleTargetDegree(randSrc)}) @@ -254,7 +248,7 @@ func sampleTargetDegree(randSrc *rand.Rand) int { } } -func connectPOIs(pois []*PointOfInterest, width, height int, settings *Settings, randSrc *rand.Rand, waterMap map[image.Point]bool, roadTarget int) []*Road { +func connectPOIs(pois []*PointOfInterest, width, height int, settings *Settings, randSrc *rand.Rand, waterMask *PixelMask, roadTarget int) []*Road { minAngle := settings.MinRoadAngle * math.Pi / 180.0 if minAngle < 0 { minAngle = 0 @@ -399,7 +393,7 @@ func connectPOIs(pois []*PointOfInterest, width, height int, settings *Settings, for _, e := range selectedEdges { a := pois[e.a] b := pois[e.b] - path := calculateRoadPath(a, b, settings.RoadCurvyness/100.0, avgDim, randSrc, waterMap) + path := calculateRoadPath(a, b, settings.RoadCurvyness/100.0, avgDim, randSrc, waterMask) imp := a.Connections + b.Connections + int(math.Round((a.ArterialWeight+b.ArterialWeight)*4)) roads = append(roads, &Road{Start: a, End: b, Points: path, Importance: imp}) } @@ -407,7 +401,7 @@ func connectPOIs(pois []*PointOfInterest, width, height int, settings *Settings, return roads } -func appendExitRoads(roads []*Road, pois []*PointOfInterest, width, height int, settings *Settings, randSrc *rand.Rand, waterMap map[image.Point]bool) []*Road { +func appendExitRoads(roads []*Road, pois []*PointOfInterest, width, height int, settings *Settings, randSrc *rand.Rand, waterMask *PixelMask) []*Road { if settings.RoadExits <= 0 || len(pois) == 0 { return roads } @@ -417,7 +411,7 @@ func appendExitRoads(roads []*Road, pois []*PointOfInterest, width, height int, usedEdgePoints := make([]image.Point, 0, settings.RoadExits) for i := 0; i < settings.RoadExits; i++ { - edgeNode, ok := sampleNonWaterEdgePOI(width, height, randSrc, waterMap, usedEdgePoints) + edgeNode, ok := sampleNonWaterEdgePOI(width, height, randSrc, waterMask, usedEdgePoints) if !ok { continue } @@ -432,7 +426,7 @@ func appendExitRoads(roads []*Road, pois []*PointOfInterest, width, height int, edgeNode.TargetDegree = 1 edgeNode.Connections = 1 - path := calculateRoadPath(anchor, edgeNode, settings.RoadCurvyness/100.0, avgDim, randSrc, waterMap) + path := calculateRoadPath(anchor, edgeNode, settings.RoadCurvyness/100.0, avgDim, randSrc, waterMask) importance := anchor.Connections + edgeNode.Connections + int(math.Round(anchor.ArterialWeight*3)) roads = append(roads, &Road{ Start: anchor, @@ -448,14 +442,14 @@ func appendExitRoads(roads []*Road, pois []*PointOfInterest, width, height int, return roads } -func sampleNonWaterEdgePOI(width, height int, randSrc *rand.Rand, waterMap map[image.Point]bool, used []image.Point) (*PointOfInterest, bool) { +func sampleNonWaterEdgePOI(width, height int, randSrc *rand.Rand, waterMask *PixelMask, used []image.Point) (*PointOfInterest, bool) { minSpacing := math.Min(float64(width), float64(height)) * 0.08 minSpacing2 := minSpacing * minSpacing for tries := 0; tries < 120; tries++ { p := sampleEdgePOI(width, height, randSrc) pt := image.Point{X: p.X, Y: p.Y} - if waterMap[pt] { + if waterMask.GetPoint(pt) { continue } tooClose := false @@ -614,10 +608,8 @@ func assignRoadWidths(roads []*Road, settings *Settings, randSrc *rand.Rand, wid } } -// 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 +// drawRoadToMasks draws a single road on the image including bridges. +func drawRoadToMasks(img *image.RGBA, points []PathPoint, roadColor, bridgeColor color.Color, width int, roadMask, bridgeMask *PixelMask) { bridgeWidth := int(math.Ceil(float64(width) * 1.15)) if bridgeWidth < 1 { bridgeWidth = 1 @@ -628,8 +620,7 @@ func drawRoad(img *image.RGBA, points []PathPoint, roadColor, bridgeColor color. p2 := points[i+1] isBridge := p1.IsBridge && p2.IsBridge if !isBridge { - linePoints := drawLine(img, p1.Point.X, p1.Point.Y, p2.Point.X, p2.Point.Y, roadColor, width) - roadPixels = append(roadPixels, linePoints...) + drawLineMasked(img, p1.Point.X, p1.Point.Y, p2.Point.X, p2.Point.Y, roadColor, width, roadMask) i++ continue } @@ -640,17 +631,16 @@ func drawRoad(img *image.RGBA, points []PathPoint, roadColor, bridgeColor color. for end < len(points)-1 && points[end].IsBridge && points[end+1].IsBridge { end++ } - linePoints := drawLine( + drawLineMasked( img, points[start].Point.X, points[start].Point.Y, points[end].Point.X, points[end].Point.Y, bridgeColor, bridgeWidth, + bridgeMask, ) - bridgePixels = append(bridgePixels, linePoints...) i = end } - return roadPixels, bridgePixels } func bresenhamRoad(path []image.Point) []image.Point { @@ -693,27 +683,27 @@ func bresenhamRoad(path []image.Point) []image.Point { } // calculateRoadPath computes the path for a road including curves and bridges. -func calculateRoadPath(start, end *PointOfInterest, curvyness, avgDim float64, randSrc *rand.Rand, waterMap map[image.Point]bool) []PathPoint { +func calculateRoadPath(start, end *PointOfInterest, curvyness, avgDim float64, randSrc *rand.Rand, waterMask *PixelMask) []PathPoint { dx := end.X - start.X dy := end.Y - start.Y dist := math.Hypot(float64(dx), float64(dy)) if dist == 0 { p := image.Point{X: start.X, Y: start.Y} - return []PathPoint{{Point: p, IsBridge: waterMap[p]}} + return []PathPoint{{Point: p, IsBridge: waterMask.GetPoint(p)}} } curve := clamp(curvyness, 0, 1) if curve <= 0 { points := bresenhamRoad([]image.Point{{X: start.X, Y: start.Y}, {X: end.X, Y: end.Y}}) - return toPathPoints(points, waterMap) + return toPathPoints(points, waterMask) } // Non-linear scaling: low values stay fairly straight, high values become very winding. strength := math.Pow(curve, 1.35) if strength < 0.001 { points := bresenhamRoad([]image.Point{{X: start.X, Y: start.Y}, {X: end.X, Y: end.Y}}) - return toPathPoints(points, waterMap) + return toPathPoints(points, waterMask) } baseControls := int(math.Max(12, dist/(22.0-14.0*strength))) @@ -774,20 +764,19 @@ func calculateRoadPath(start, end *PointOfInterest, curvyness, avgDim float64, r } points := bresenhamRoad(controlPoints) - return toPathPoints(points, waterMap) + return toPathPoints(points, waterMask) } -func toPathPoints(points []image.Point, waterMap map[image.Point]bool) []PathPoint { +func toPathPoints(points []image.Point, waterMask *PixelMask) []PathPoint { pathPoints := make([]PathPoint, len(points)) for i, p := range points { - pathPoints[i] = PathPoint{Point: p, IsBridge: waterMap[p]} + pathPoints[i] = PathPoint{Point: p, IsBridge: waterMask.GetPoint(p)} } return pathPoints } -// 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 +// drawLineMasked draws a line with specified width on the image and mask. +func drawLineMasked(img *image.RGBA, x0, y0, x1, y1 int, col color.Color, width int, mask *PixelMask) { dx := abs(x1 - x0) dy := -abs(y1 - y0) sx := -1 @@ -807,7 +796,9 @@ func drawLine(img *image.RGBA, x0, y0, x1, y1 int, col color.Color, width int) [ 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 mask != nil { + mask.SetXY(px, py) + } } } } @@ -825,7 +816,6 @@ func drawLine(img *image.RGBA, x0, y0, x1, y1 int, col color.Color, width int) [ y0 += sy } } - return points } func clamp(v, lo, hi float64) float64 { diff --git a/terrain.go b/terrain.go index b278fbd..7f7439b 100644 --- a/terrain.go +++ b/terrain.go @@ -81,15 +81,22 @@ func ApplyRoughness(heightmap image.Image, roughness float64) image.Image { return composite } -// DarkenLakeAreas darkens the heightmap where lakes exist -func DarkenLakeAreas(heightmap image.Image, lakePixels []image.Point) image.Image { +// DarkenLakeAreas darkens the heightmap where water exists. +func DarkenLakeAreas(heightmap image.Image, waterMask *PixelMask) image.Image { bounds := heightmap.Bounds() width := bounds.Dx() lakeMask := image.NewRGBA(bounds) black := color.RGBA{0, 0, 0, 255} - for _, p := range lakePixels { - lakeMask.Set(p.X, p.Y, black) + if waterMask != nil { + for y := 0; y < waterMask.Height; y++ { + row := y * waterMask.Width + for x := 0; x < waterMask.Width; x++ { + if waterMask.Data[row+x] != 0 { + lakeMask.Set(x, y, black) + } + } + } } blurRadius := float64(width) * 0.05 @@ -102,18 +109,25 @@ func DarkenLakeAreas(heightmap image.Image, lakePixels []image.Point) image.Imag return composite } -// FlattenRoadAreas smooths terrain under roads -func FlattenRoadAreas(heightmap image.Image, roadPixels []image.Point) image.Image { +// FlattenRoadAreas smooths terrain under roads. +func FlattenRoadAreas(heightmap image.Image, roadMask *PixelMask) image.Image { bounds := heightmap.Bounds() width := bounds.Dx() - roadMask := image.NewGray(bounds) - for _, p := range roadPixels { - roadMask.SetGray(p.X, p.Y, color.Gray{Y: 255}) + roadGrayMask := image.NewGray(bounds) + if roadMask != nil { + for y := 0; y < roadMask.Height; y++ { + row := y * roadMask.Width + for x := 0; x < roadMask.Width; x++ { + if roadMask.Data[row+x] != 0 { + roadGrayMask.SetGray(x, y, color.Gray{Y: 255}) + } + } + } } blurRadius := float64(width) * 0.01 - blurredRoadMask := imaging.Blur(roadMask, blurRadius) + blurredRoadMask := imaging.Blur(roadGrayMask, blurRadius) blurredHeightmap := imaging.Blur(heightmap, blurRadius) @@ -146,8 +160,8 @@ func FlattenRoadAreas(heightmap image.Image, roadPixels []image.Point) image.Ima return composite } -// 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 { +// GenerateTrees places trees on the map based on coverage and noise. +func GenerateTrees(img *image.RGBA, waterMask, roadMask, buildingMask *PixelMask, minTreeSize, maxTreeSize, treeCoverage, treeClumpiness float64, seed int64) *PixelMask { width := img.Bounds().Dx() height := img.Bounds().Dy() @@ -179,19 +193,14 @@ func GenerateTrees(img *image.RGBA, lakePixels, roadPixels, buildingPixels []ima } threshold := uint8(255 * (1 - (treeCoverage / 100.0))) - isLake := make(map[image.Point]bool) - for _, p := range lakePixels { - isLake[p] = true + if waterMask == nil { + waterMask = NewPixelMask(width, height) } - - isRoad := make(map[image.Point]bool) - for _, p := range roadPixels { - isRoad[p] = true + if roadMask == nil { + roadMask = NewPixelMask(width, height) } - - isBuilding := make(map[image.Point]bool) - for _, p := range buildingPixels { - isBuilding[p] = true + if buildingMask == nil { + buildingMask = NewPixelMask(width, height) } randSrc := rand.New(rand.NewSource(seed)) @@ -202,7 +211,7 @@ func GenerateTrees(img *image.RGBA, lakePixels, roadPixels, buildingPixels []ima for range numClumpTrees { 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] { + if treeNoiseMap.GrayAt(p.X, p.Y).Y >= threshold && !waterMask.GetPoint(p) && !roadMask.GetPoint(p) && !buildingMask.GetPoint(p) { initialPoints = append(initialPoints, p) break } @@ -211,10 +220,9 @@ func GenerateTrees(img *image.RGBA, lakePixels, roadPixels, buildingPixels []ima 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] + return treeNoiseMap.GrayAt(p.X, p.Y).Y >= threshold && !waterMask.GetPoint(p) && !roadMask.GetPoint(p) && !buildingMask.GetPoint(p) }, seed) - var treePixels []image.Point numGoroutines := runtime.NumCPU() if len(allPoints) < numGoroutines { numGoroutines = len(allPoints) @@ -251,7 +259,7 @@ func GenerateTrees(img *image.RGBA, lakePixels, roadPixels, buildingPixels []ima for y := p.Y - int(r); y <= p.Y+int(r); y++ { for x := p.X - int(r); x <= p.X+int(r); x++ { pt := image.Point{X: x, Y: y} - if !pt.In(img.Bounds()) || isLake[pt] || isRoad[pt] || isBuilding[pt] { + if !pt.In(img.Bounds()) || waterMask.GetPoint(pt) || roadMask.GetPoint(pt) || buildingMask.GetPoint(pt) { continue } @@ -269,11 +277,14 @@ func GenerateTrees(img *image.RGBA, lakePixels, roadPixels, buildingPixels []ima wg.Wait() close(results) + treeMask := NewPixelMask(width, height) for res := range results { - treePixels = append(treePixels, res...) + for _, p := range res { + treeMask.SetPoint(p) + } } - return treePixels + return treeMask } // poissonDiscSampling generates randomly distributed points with minimum radius separation diff --git a/water.go b/water.go index 38e45be..0696c2c 100644 --- a/water.go +++ b/water.go @@ -899,7 +899,7 @@ func clamp01(v float64) float64 { } // GenerateRivers creates rivers flowing across the map from edge to edge -func GenerateRivers(width, height, numRivers int, minWidth, maxWidth, curvyness float64, inputImage image.Image, lakes [][]image.Point, seed int64, heightmap image.Image, riverWidthVariability, riverEdgeRoughness float64) (image.Image, []image.Point) { +func GenerateRivers(width, height, numRivers int, minWidth, maxWidth, curvyness float64, inputImage image.Image, lakes [][]image.Point, seed int64, heightmap image.Image, riverWidthVariability, riverEdgeRoughness float64) (image.Image, *PixelMask) { if numRivers == 0 { return inputImage, nil } @@ -910,16 +910,15 @@ func GenerateRivers(width, height, numRivers int, minWidth, maxWidth, curvyness draw.Draw(canvas, canvas.Bounds(), inputImage, image.Point{}, draw.Src) } - var allRiverPixels []image.Point + riverMask := NewPixelMask(width, height) randSrc := rand.New(rand.NewSource(seed)) avgDim := float64(width+height) / 2.0 // Build water pixel lookup maps - isWater := make(map[image.Point]bool) + isWater := BuildMaskFromLakes(width, height, lakes) lakePixelMap := make(map[image.Point]int) for i, lake := range lakes { for _, p := range lake { - isWater[p] = true lakePixelMap[p] = i } } @@ -957,7 +956,7 @@ func GenerateRivers(width, height, numRivers int, minWidth, maxWidth, curvyness // Check for intersections with existing water for _, p := range path { - if isWater[p] { + if isWater.GetPoint(p) { if lakeIndex, isLake := lakePixelMap[p]; isLake { // End river at lake center if it intersects lakeCenter := findCenter(lakes[lakeIndex]) @@ -976,12 +975,12 @@ func GenerateRivers(width, height, numRivers int, minWidth, maxWidth, curvyness radius := riverWidthPx / 2.0 for _, p := range path { - drawCircle(canvas, p, radius, color.RGBA{R: 0, G: 0, B: 255, A: 255}, &allRiverPixels, isWater, heightmap, riverWidthVariability, riverEdgeRoughness) + drawCircle(canvas, p, radius, color.RGBA{R: 0, G: 0, B: 255, A: 255}, riverMask, isWater, heightmap, riverWidthVariability, riverEdgeRoughness) } r.Points = path } - return canvas, allRiverPixels + return canvas, riverMask } // bresenhamRiver draws a line between control points using Bresenham's algorithm @@ -1118,7 +1117,7 @@ func getPointOnEdge(width, height, edge int, randSrc *rand.Rand) image.Point { } // 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) { +func drawCircle(img *image.RGBA, center image.Point, radius float64, c color.Color, riverMask, isWater *PixelMask, heightmap image.Image, riverWidthVariability, riverEdgeRoughness float64) { bounds := img.Bounds() largeAmplitude := (radius * 0.5) * (riverWidthVariability / 100.0) @@ -1143,10 +1142,10 @@ func drawCircle(img *image.RGBA, center image.Point, radius float64, c color.Col effectiveRadius := radius + waveOffset if dist <= effectiveRadius { - if !isWater[p] { + if !isWater.GetPoint(p) { img.Set(x, y, c) - *pixels = append(*pixels, p) - isWater[p] = true + riverMask.SetPoint(p) + isWater.SetPoint(p) } } }