number of other small optimizations for speed and memory usage

This commit is contained in:
Grimsace
2026-02-26 13:39:39 -06:00
parent 25321e4aa0
commit c45aacbc46
4 changed files with 139 additions and 104 deletions
+38 -23
View File
@@ -87,6 +87,31 @@ func capRequestedBuildingsToFit(settings *Settings, width, height int) int {
return settings.NumBuildings 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. // GenerateBuildings creates and places buildings on the map.
func GenerateBuildings( func GenerateBuildings(
img *image.RGBA, img *image.RGBA,
@@ -130,22 +155,14 @@ func GenerateBuildings(
normalRoadAnchors = append(normalRoadAnchors, p) 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 { if len(anchorPoints) == 0 {
if _, ok := sampleRandomLandPoint(width, height, waterMask, roadMask, randSrc); !ok {
return nil, nil return nil, nil
} }
} else {
// Sort anchor points for deterministic placement // Sort anchor points for deterministic placement
sort.Slice(anchorPoints, func(i, j int) bool { sort.Slice(anchorPoints, func(i, j int) bool {
if anchorPoints[i].Y != anchorPoints[j].Y { if anchorPoints[i].Y != anchorPoints[j].Y {
@@ -153,15 +170,6 @@ func GenerateBuildings(
} }
return anchorPoints[i].X < anchorPoints[j].X 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)
}
}
} }
// Main loop for placing buildings // Main loop for placing buildings
@@ -182,14 +190,21 @@ func GenerateBuildings(
anchor = exitRoadAnchors[randSrc.Intn(len(exitRoadAnchors))] anchor = exitRoadAnchors[randSrc.Intn(len(exitRoadAnchors))]
} else if len(normalRoadAnchors) > 0 { } else if len(normalRoadAnchors) > 0 {
anchor = normalRoadAnchors[randSrc.Intn(len(normalRoadAnchors))] anchor = normalRoadAnchors[randSrc.Intn(len(normalRoadAnchors))]
} else { } else if len(anchorPoints) > 0 {
anchor = anchorPoints[randSrc.Intn(len(anchorPoints))] anchor = anchorPoints[randSrc.Intn(len(anchorPoints))]
} else {
p, ok := sampleRandomLandPoint(width, height, waterMask, roadMask, randSrc)
if !ok {
continue
}
anchor = p
} }
} else { } else {
if len(landPoints) == 0 { p, ok := sampleRandomLandPoint(width, height, waterMask, roadMask, randSrc)
if !ok {
continue // No land to place buildings on continue // No land to place buildings on
} }
anchor = landPoints[randSrc.Intn(len(landPoints))] anchor = p
} }
// Search for a valid building location around the anchor // Search for a valid building location around the anchor
+40 -46
View File
@@ -11,6 +11,7 @@ import (
"image/draw" "image/draw"
"image/jpeg" "image/jpeg"
"image/png" "image/png"
"io"
"log" "log"
"os" "os"
"path/filepath" "path/filepath"
@@ -1261,19 +1262,16 @@ func main() {
w.ShowAndRun() w.ShowAndRun()
} }
// getImageData encodes an image to the specified format and returns the data as a byte buffer. func encodeImageToWriter(w io.Writer, img image.Image, format string) error {
func getImageData(img image.Image, format string) (*bytes.Buffer, error) {
buf := new(bytes.Buffer)
var err error
switch format { switch format {
case "PNG": case "PNG":
err = png.Encode(buf, img) return png.Encode(w, img)
case "JPG": case "JPG":
err = jpeg.Encode(buf, img, nil) return jpeg.Encode(w, img, nil)
case "WEBP": 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. // 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) imgFormat := strings.ToLower(formatSelect.Selected)
bounds := canvasImg.Bounds() bounds := canvasImg.Bounds()
maskToGray := func(mask *PixelMask) *image.Gray { maskToGray := func(mask *PixelMask) image.Image {
out := image.NewGray(bounds) out := image.NewGray(bounds)
if mask == nil { if mask == nil {
return out return out
@@ -1339,31 +1337,31 @@ func showMasksSaveDialog(win fyne.Window, canvasImg, heightmapImg, bumpmapImg im
} }
return out return out
} }
buildLakeMask := func() image.Image {
// Create mask images from the generated data
lakeMask := image.NewGray(bounds) lakeMask := image.NewGray(bounds)
for _, lake := range lakes { for _, lake := range lakes {
for _, p := range lake { for _, p := range lake {
lakeMask.SetGray(p.X, p.Y, color.Gray{Y: 255}) lakeMask.SetGray(p.X, p.Y, color.Gray{Y: 255})
} }
} }
riverMaskImg := maskToGray(riverMask) return lakeMask
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,
} }
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) }},
}
// Save the images based on the selected packaging option // Save the images based on the selected packaging option
switch packageSelect.Selected { switch packageSelect.Selected {
case "Folder": case "Folder":
@@ -1373,8 +1371,8 @@ func showMasksSaveDialog(win fyne.Window, canvasImg, heightmapImg, bumpmapImg im
return return
} }
settings.LastExportPath = exportPath settings.LastExportPath = exportPath
for name, img := range imagesToSave { for _, item := range items {
saveImage(img, filepath.Join(exportPath, name)) saveImage(item.make(), filepath.Join(exportPath, item.name))
} }
case "tar.gz": case "tar.gz":
filePath := filepath.Join(pathLabel.Text, folderName+".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() defer gw.Close()
tw := tar.NewWriter(gw) tw := tar.NewWriter(gw)
defer tw.Close() defer tw.Close()
var buf bytes.Buffer
for name, img := range imagesToSave { for _, item := range items {
buf, err := getImageData(img, formatSelect.Selected) buf.Reset()
if err != nil { if err := encodeImageToWriter(&buf, item.make(), formatSelect.Selected); err != nil {
log.Printf("Error encoding image %s: %v\n", name, err) log.Printf("Error encoding image %s: %v\n", item.name, err)
continue continue
} }
hdr := &tar.Header{ hdr := &tar.Header{
Name: name, Name: item.name,
Mode: 0644, Mode: 0644,
Size: int64(buf.Len()), Size: int64(buf.Len()),
} }
if err := tw.WriteHeader(hdr); err != nil { 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 continue
} }
if _, err := tw.Write(buf.Bytes()); err != nil { 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": case "zip":
@@ -1423,20 +1421,16 @@ func showMasksSaveDialog(win fyne.Window, canvasImg, heightmapImg, bumpmapImg im
zw := zip.NewWriter(file) zw := zip.NewWriter(file)
defer zw.Close() defer zw.Close()
for name, img := range imagesToSave { for _, item := range items {
buf, err := getImageData(img, formatSelect.Selected) f, err := zw.Create(item.name)
if err != nil { 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 continue
} }
f, err := zw.Create(name) if err := encodeImageToWriter(f, item.make(), formatSelect.Selected); err != nil {
if err != nil { log.Printf("Error writing zip data for %s: %v\n", item.name, err)
log.Printf("Error creating zip entry for %s: %v\n", name, err)
continue continue
} }
if _, err := f.Write(buf.Bytes()); err != nil {
log.Printf("Error writing zip data for %s: %v\n", name, err)
}
} }
} }
+31 -14
View File
@@ -572,38 +572,55 @@ func assignRoadWidths(roads []*Road, settings *Settings, randSrc *rand.Rand, wid
} }
} }
widths := make(map[*Road]float64, len(roads)) widths := make([]float64, len(roads))
adj := make(map[*PointOfInterest][]*Road) startNode := make([]int, len(roads))
for _, r := range 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) n := float64(r.Importance) / float64(maxImportance)
jitter := (randSrc.Float64() - 0.5) * 0.16 jitter := (randSrc.Float64() - 0.5) * 0.16
base := minWidth + (maxWidth-minWidth)*clamp01(n+jitter) base := minWidth + (maxWidth-minWidth)*clamp01(n+jitter)
widths[r] = base widths[i] = base
adj[r.Start] = append(adj[r.Start], r) sid := getNodeID(r.Start)
adj[r.End] = append(adj[r.End], r) 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++ { for i := 0; i < 2; i++ {
next := make(map[*Road]float64, len(widths)) next := make([]float64, len(widths))
for r, w := range widths { for ridx, w := range widths {
total := w total := w
count := 1.0 count := 1.0
for _, n := range []*PointOfInterest{r.Start, r.End} { for _, nid := range []int{startNode[ridx], endNode[ridx]} {
for _, nbr := range adj[n] { for _, nbr := range adj[nid] {
if nbr == r { if nbr == ridx {
continue continue
} }
total += widths[nbr] total += widths[nbr]
count += 1 count += 1
} }
} }
next[r] = w*0.55 + (total/count)*0.45 next[ridx] = w*0.55 + (total/count)*0.45
} }
widths = next widths = next
} }
for _, r := range roads { for i, r := range roads {
w := clamp(widths[r], minWidth, maxWidth) w := clamp(widths[i], minWidth, maxWidth)
r.Width = max(1, int(math.Round(w))) r.Width = max(1, int(math.Round(w)))
} }
} }
+19 -10
View File
@@ -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 { 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)) randSrc := rand.New(rand.NewSource(seed))
points := initialPoints 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) cellSize := minRadius / math.Sqrt(2)
gridWidth := int(math.Ceil(float64(width)/cellSize)) + 1 gridWidth := int(math.Ceil(float64(width)/cellSize)) + 1
gridHeight := int(math.Ceil(float64(height)/cellSize)) + 1 gridHeight := int(math.Ceil(float64(height)/cellSize)) + 1
grid := make([][]image.Point, gridWidth) grid := make([]int32, gridWidth*gridHeight)
for i := range grid { 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) 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 { for len(activeList) > 0 {
listIndex := randSrc.Intn(len(activeList)) listIndex := randSrc.Intn(len(activeList))
p := activeList[listIndex] p := points[activeList[listIndex]]
found := false found := false
for range k { for range k {
angle := randSrc.Float64() * 2 * math.Pi 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 m := -1; m <= 1; m++ {
for n := -1; n <= 1; n++ { for n := -1; n <= 1; n++ {
checkX, checkY := gridX+m, gridY+n checkX, checkY := gridX+m, gridY+n
if checkX >= 0 && checkX < gridWidth && checkY >= 0 && checkY < gridHeight && grid[checkX][checkY] != (image.Point{}) { if checkX >= 0 && checkX < gridWidth && checkY >= 0 && checkY < gridHeight {
dist := math.Sqrt(math.Pow(float64(grid[checkX][checkY].X-newPoint.X), 2) + math.Pow(float64(grid[checkX][checkY].Y-newPoint.Y), 2)) 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 { if dist < minRadius {
valid = false valid = false
break break
@@ -341,8 +349,9 @@ func poissonDiscSampling(width, height int, minRadius float64, k int, initialPoi
if valid { if valid {
points = append(points, newPoint) points = append(points, newPoint)
activeList = append(activeList, newPoint) newIdx := len(points) - 1
grid[gridX][gridY] = newPoint activeList = append(activeList, newIdx)
grid[gridY*gridWidth+gridX] = int32(newIdx)
found = true found = true
} }
} }