number of other small optimizations for speed and memory usage
This commit is contained in:
+45
-30
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user