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
}
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,22 +155,14 @@ 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 {
if _, ok := sampleRandomLandPoint(width, height, waterMask, roadMask, randSrc); !ok {
return nil, nil
}
} else {
// Sort anchor points for deterministic placement
sort.Slice(anchorPoints, func(i, j int) bool {
if anchorPoints[i].Y != anchorPoints[j].Y {
@@ -153,15 +170,6 @@ func GenerateBuildings(
}
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
@@ -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
+40 -46
View File
@@ -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
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})
}
}
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,
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) }},
}
// 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)
}
}
}
+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))
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)))
}
}
+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 {
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
}
}