From c45aacbc46069f649c4453ac5571f90720b203ab Mon Sep 17 00:00:00 2001 From: Grimsace Date: Thu, 26 Feb 2026 13:39:39 -0600 Subject: [PATCH] number of other small optimizations for speed and memory usage --- buildings.go | 75 ++++++++++++++++++++++++----------------- main.go | 94 ++++++++++++++++++++++++---------------------------- roads.go | 45 +++++++++++++++++-------- terrain.go | 29 ++++++++++------ 4 files changed, 139 insertions(+), 104 deletions(-) diff --git a/buildings.go b/buildings.go index 2977082..cd256ab 100644 --- a/buildings.go +++ b/buildings.go @@ -87,6 +87,31 @@ func capRequestedBuildingsToFit(settings *Settings, width, height int) int { return settings.NumBuildings } +func sampleRandomLandPoint(width, height int, waterMask, roadMask *PixelMask, randSrc *rand.Rand) (image.Point, bool) { + const randomTries = 128 + for i := 0; i < randomTries; i++ { + p := image.Point{X: randSrc.Intn(width), Y: randSrc.Intn(height)} + if !waterMask.GetPoint(p) && !roadMask.GetPoint(p) { + return p, true + } + } + if width <= 0 || height <= 0 { + return image.Point{}, false + } + start := randSrc.Intn(width * height) + total := width * height + for i := 0; i < total; i++ { + idx := (start + i) % total + x := idx % width + y := idx / width + p := image.Point{X: x, Y: y} + if !waterMask.GetPoint(p) && !roadMask.GetPoint(p) { + return p, true + } + } + return image.Point{}, false +} + // GenerateBuildings creates and places buildings on the map. func GenerateBuildings( img *image.RGBA, @@ -130,38 +155,21 @@ func GenerateBuildings( normalRoadAnchors = append(normalRoadAnchors, p) } } - } else { - // If no roads, use all land pixels as anchors - for y := 0; y < height; y++ { - for x := 0; x < width; x++ { - p := image.Point{X: x, Y: y} - if !waterMask.GetPoint(p) { - anchorPoints = append(anchorPoints, p) - } - } - } } - // Early exit if no anchor points are available + // Early exit if no anchors and no valid land. if len(anchorPoints) == 0 { - return nil, nil - } - // Sort anchor points for deterministic placement - sort.Slice(anchorPoints, func(i, j int) bool { - if anchorPoints[i].Y != anchorPoints[j].Y { - return anchorPoints[i].Y < anchorPoints[j].Y + if _, ok := sampleRandomLandPoint(width, height, waterMask, roadMask, randSrc); !ok { + return nil, nil } - return anchorPoints[i].X < anchorPoints[j].X - }) - - landPoints := make([]image.Point, 0, width*height) - for y := 0; y < height; y++ { - for x := 0; x < width; x++ { - p := image.Point{X: x, Y: y} - if !waterMask.GetPoint(p) && !roadMask.GetPoint(p) { - landPoints = append(landPoints, p) + } else { + // Sort anchor points for deterministic placement + sort.Slice(anchorPoints, func(i, j int) bool { + if anchorPoints[i].Y != anchorPoints[j].Y { + return anchorPoints[i].Y < anchorPoints[j].Y } - } + return anchorPoints[i].X < anchorPoints[j].X + }) } // Main loop for placing buildings @@ -182,14 +190,21 @@ func GenerateBuildings( anchor = exitRoadAnchors[randSrc.Intn(len(exitRoadAnchors))] } else if len(normalRoadAnchors) > 0 { anchor = normalRoadAnchors[randSrc.Intn(len(normalRoadAnchors))] - } else { + } else if len(anchorPoints) > 0 { anchor = anchorPoints[randSrc.Intn(len(anchorPoints))] + } else { + p, ok := sampleRandomLandPoint(width, height, waterMask, roadMask, randSrc) + if !ok { + continue + } + anchor = p } } else { - if len(landPoints) == 0 { + p, ok := sampleRandomLandPoint(width, height, waterMask, roadMask, randSrc) + if !ok { continue // No land to place buildings on } - anchor = landPoints[randSrc.Intn(len(landPoints))] + anchor = p } // Search for a valid building location around the anchor diff --git a/main.go b/main.go index 17dd092..023e4f3 100644 --- a/main.go +++ b/main.go @@ -11,6 +11,7 @@ import ( "image/draw" "image/jpeg" "image/png" + "io" "log" "os" "path/filepath" @@ -1261,19 +1262,16 @@ func main() { w.ShowAndRun() } -// getImageData encodes an image to the specified format and returns the data as a byte buffer. -func getImageData(img image.Image, format string) (*bytes.Buffer, error) { - buf := new(bytes.Buffer) - var err error +func encodeImageToWriter(w io.Writer, img image.Image, format string) error { switch format { case "PNG": - err = png.Encode(buf, img) + return png.Encode(w, img) case "JPG": - err = jpeg.Encode(buf, img, nil) + return jpeg.Encode(w, img, nil) case "WEBP": - err = webp.Encode(buf, img, &webp.Options{Lossless: true}) + return webp.Encode(w, img, &webp.Options{Lossless: true}) } - return buf, err + return fmt.Errorf("unsupported format: %s", format) } // showMasksSaveDialog displays a dialog for saving the generated masks. @@ -1324,7 +1322,7 @@ func showMasksSaveDialog(win fyne.Window, canvasImg, heightmapImg, bumpmapImg im } imgFormat := strings.ToLower(formatSelect.Selected) bounds := canvasImg.Bounds() - maskToGray := func(mask *PixelMask) *image.Gray { + maskToGray := func(mask *PixelMask) image.Image { out := image.NewGray(bounds) if mask == nil { return out @@ -1339,31 +1337,31 @@ func showMasksSaveDialog(win fyne.Window, canvasImg, heightmapImg, bumpmapImg im } return out } - - // Create mask images from the generated data - lakeMask := image.NewGray(bounds) - for _, lake := range lakes { - for _, p := range lake { - lakeMask.SetGray(p.X, p.Y, color.Gray{Y: 255}) + buildLakeMask := func() image.Image { + lakeMask := image.NewGray(bounds) + for _, lake := range lakes { + for _, p := range lake { + lakeMask.SetGray(p.X, p.Y, color.Gray{Y: 255}) + } } + return lakeMask + } + type exportItem struct { + name string + make func() image.Image + } + items := []exportItem{ + {name: "canvas." + imgFormat, make: func() image.Image { return canvasImg }}, + {name: "heightmap." + imgFormat, make: func() image.Image { return heightmapImg }}, + {name: "bump_map." + imgFormat, make: func() image.Image { return bumpmapImg }}, + {name: "lakes_mask." + imgFormat, make: buildLakeMask}, + {name: "rivers_mask." + imgFormat, make: func() image.Image { return maskToGray(riverMask) }}, + {name: "trees_mask." + imgFormat, make: func() image.Image { return maskToGray(treeMask) }}, + {name: "roads_mask." + imgFormat, make: func() image.Image { return maskToGray(roadMask) }}, + {name: "bridges_mask." + imgFormat, make: func() image.Image { return maskToGray(bridgeMask) }}, + {name: "buildings_mask." + imgFormat, make: func() image.Image { return maskToGray(buildingMask) }}, } - 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: 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 { case "Folder": @@ -1373,8 +1371,8 @@ func showMasksSaveDialog(win fyne.Window, canvasImg, heightmapImg, bumpmapImg im return } settings.LastExportPath = exportPath - for name, img := range imagesToSave { - saveImage(img, filepath.Join(exportPath, name)) + for _, item := range items { + saveImage(item.make(), filepath.Join(exportPath, item.name)) } case "tar.gz": filePath := filepath.Join(pathLabel.Text, folderName+".tar.gz") @@ -1390,24 +1388,24 @@ func showMasksSaveDialog(win fyne.Window, canvasImg, heightmapImg, bumpmapImg im defer gw.Close() tw := tar.NewWriter(gw) defer tw.Close() - - for name, img := range imagesToSave { - buf, err := getImageData(img, formatSelect.Selected) - if err != nil { - log.Printf("Error encoding image %s: %v\n", name, err) + var buf bytes.Buffer + for _, item := range items { + buf.Reset() + if err := encodeImageToWriter(&buf, item.make(), formatSelect.Selected); err != nil { + log.Printf("Error encoding image %s: %v\n", item.name, err) continue } hdr := &tar.Header{ - Name: name, + Name: item.name, Mode: 0644, Size: int64(buf.Len()), } if err := tw.WriteHeader(hdr); err != nil { - log.Printf("Error writing tar header for %s: %v\n", name, err) + log.Printf("Error writing tar header for %s: %v\n", item.name, err) continue } if _, err := tw.Write(buf.Bytes()); err != nil { - log.Printf("Error writing tar data for %s: %v\n", name, err) + log.Printf("Error writing tar data for %s: %v\n", item.name, err) } } case "zip": @@ -1423,20 +1421,16 @@ func showMasksSaveDialog(win fyne.Window, canvasImg, heightmapImg, bumpmapImg im zw := zip.NewWriter(file) defer zw.Close() - for name, img := range imagesToSave { - buf, err := getImageData(img, formatSelect.Selected) + for _, item := range items { + f, err := zw.Create(item.name) if err != nil { - log.Printf("Error encoding image %s: %v\n", name, err) + log.Printf("Error creating zip entry for %s: %v\n", item.name, err) continue } - f, err := zw.Create(name) - if err != nil { - log.Printf("Error creating zip entry for %s: %v\n", name, err) + if err := encodeImageToWriter(f, item.make(), formatSelect.Selected); err != nil { + log.Printf("Error writing zip data for %s: %v\n", item.name, err) continue } - if _, err := f.Write(buf.Bytes()); err != nil { - log.Printf("Error writing zip data for %s: %v\n", name, err) - } } } diff --git a/roads.go b/roads.go index 7d37da2..c5380e0 100644 --- a/roads.go +++ b/roads.go @@ -572,38 +572,55 @@ func assignRoadWidths(roads []*Road, settings *Settings, randSrc *rand.Rand, wid } } - widths := make(map[*Road]float64, len(roads)) - adj := make(map[*PointOfInterest][]*Road) - for _, r := range roads { + widths := make([]float64, len(roads)) + startNode := make([]int, len(roads)) + endNode := make([]int, len(roads)) + nodeIndex := make(map[*PointOfInterest]int, len(roads)*2) + adj := make([][]int, 0, len(roads)) + getNodeID := func(p *PointOfInterest) int { + if id, ok := nodeIndex[p]; ok { + return id + } + id := len(adj) + nodeIndex[p] = id + adj = append(adj, nil) + return id + } + + for i, r := range roads { n := float64(r.Importance) / float64(maxImportance) jitter := (randSrc.Float64() - 0.5) * 0.16 base := minWidth + (maxWidth-minWidth)*clamp01(n+jitter) - widths[r] = base - adj[r.Start] = append(adj[r.Start], r) - adj[r.End] = append(adj[r.End], r) + widths[i] = base + sid := getNodeID(r.Start) + eid := getNodeID(r.End) + startNode[i] = sid + endNode[i] = eid + adj[sid] = append(adj[sid], i) + adj[eid] = append(adj[eid], i) } for i := 0; i < 2; i++ { - next := make(map[*Road]float64, len(widths)) - for r, w := range widths { + next := make([]float64, len(widths)) + for ridx, w := range widths { total := w count := 1.0 - for _, n := range []*PointOfInterest{r.Start, r.End} { - for _, nbr := range adj[n] { - if nbr == r { + for _, nid := range []int{startNode[ridx], endNode[ridx]} { + for _, nbr := range adj[nid] { + if nbr == ridx { continue } total += widths[nbr] count += 1 } } - next[r] = w*0.55 + (total/count)*0.45 + next[ridx] = w*0.55 + (total/count)*0.45 } widths = next } - for _, r := range roads { - w := clamp(widths[r], minWidth, maxWidth) + for i, r := range roads { + w := clamp(widths[i], minWidth, maxWidth) r.Width = max(1, int(math.Round(w))) } } diff --git a/terrain.go b/terrain.go index cd7cd8a..d22b41b 100644 --- a/terrain.go +++ b/terrain.go @@ -288,24 +288,27 @@ func GenerateTrees(img *image.RGBA, waterMask, roadMask, buildingMask *PixelMask 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 - activeList := append([]image.Point(nil), initialPoints...) + activeList := make([]int, len(initialPoints)) + for i := range initialPoints { + activeList[i] = i + } cellSize := minRadius / math.Sqrt(2) gridWidth := int(math.Ceil(float64(width)/cellSize)) + 1 gridHeight := int(math.Ceil(float64(height)/cellSize)) + 1 - grid := make([][]image.Point, gridWidth) + grid := make([]int32, gridWidth*gridHeight) for i := range grid { - grid[i] = make([]image.Point, gridHeight) + grid[i] = -1 } - for _, p := range points { + for i, p := range points { gridX, gridY := int(float64(p.X)/cellSize), int(float64(p.Y)/cellSize) - grid[gridX][gridY] = p + grid[gridY*gridWidth+gridX] = int32(i) } for len(activeList) > 0 { listIndex := randSrc.Intn(len(activeList)) - p := activeList[listIndex] + p := points[activeList[listIndex]] found := false for range k { angle := randSrc.Float64() * 2 * math.Pi @@ -326,8 +329,13 @@ func poissonDiscSampling(width, height int, minRadius float64, k int, initialPoi for m := -1; m <= 1; m++ { for n := -1; n <= 1; n++ { checkX, checkY := gridX+m, gridY+n - if checkX >= 0 && checkX < gridWidth && checkY >= 0 && checkY < gridHeight && grid[checkX][checkY] != (image.Point{}) { - dist := math.Sqrt(math.Pow(float64(grid[checkX][checkY].X-newPoint.X), 2) + math.Pow(float64(grid[checkX][checkY].Y-newPoint.Y), 2)) + if checkX >= 0 && checkX < gridWidth && checkY >= 0 && checkY < gridHeight { + g := grid[checkY*gridWidth+checkX] + if g < 0 { + continue + } + existing := points[int(g)] + dist := math.Sqrt(math.Pow(float64(existing.X-newPoint.X), 2) + math.Pow(float64(existing.Y-newPoint.Y), 2)) if dist < minRadius { valid = false break @@ -341,8 +349,9 @@ func poissonDiscSampling(width, height int, minRadius float64, k int, initialPoi if valid { points = append(points, newPoint) - activeList = append(activeList, newPoint) - grid[gridX][gridY] = newPoint + newIdx := len(points) - 1 + activeList = append(activeList, newIdx) + grid[gridY*gridWidth+gridX] = int32(newIdx) found = true } }