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
|
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,38 +155,21 @@ 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 {
|
||||||
return nil, nil
|
if _, ok := sampleRandomLandPoint(width, height, waterMask, roadMask, randSrc); !ok {
|
||||||
}
|
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
|
|
||||||
}
|
}
|
||||||
return anchorPoints[i].X < anchorPoints[j].X
|
} else {
|
||||||
})
|
// Sort anchor points for deterministic placement
|
||||||
|
sort.Slice(anchorPoints, func(i, j int) bool {
|
||||||
landPoints := make([]image.Point, 0, width*height)
|
if anchorPoints[i].Y != anchorPoints[j].Y {
|
||||||
for y := 0; y < height; y++ {
|
return anchorPoints[i].Y < anchorPoints[j].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)
|
|
||||||
}
|
}
|
||||||
}
|
return anchorPoints[i].X < anchorPoints[j].X
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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
|
||||||
|
|||||||
@@ -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})
|
}
|
||||||
}
|
}
|
||||||
|
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
|
// 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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user