changed how masks are stored for massive performance improvment and way less ram usage

This commit is contained in:
Grimsace
2026-02-26 13:03:48 -06:00
parent 1dd9bf2c13
commit d01cb7a247
6 changed files with 277 additions and 210 deletions
+33 -31
View File
@@ -62,7 +62,17 @@ func getBuildingSizeRangePixels(settings *Settings, width, height int) (float64,
} }
// GenerateBuildings creates and places buildings on the map. // GenerateBuildings creates and places buildings on the map.
func GenerateBuildings(img *image.RGBA, width, height int, settings *Settings, roadPixels, allWaterPixels []image.Point, seed int64) ([][]image.Point, []image.Point) { func GenerateBuildings(
img *image.RGBA,
width,
height int,
settings *Settings,
roadAnchors []image.Point,
waterMask,
roadMask,
exitRoadMask *PixelMask,
seed int64,
) ([][]image.Point, *PixelMask) {
// Early exit if no buildings are to be generated // Early exit if no buildings are to be generated
if settings.NumBuildings == 0 { if settings.NumBuildings == 0 {
return nil, nil return nil, nil
@@ -70,32 +80,25 @@ func GenerateBuildings(img *image.RGBA, width, height int, settings *Settings, r
randSrc := rand.New(rand.NewSource(seed)) randSrc := rand.New(rand.NewSource(seed))
buildingColor := color.RGBA{R: 128, G: 128, B: 128, A: 255} // Gray color for buildings buildingColor := color.RGBA{R: 128, G: 128, B: 128, A: 255} // Gray color for buildings
if waterMask == nil {
isWater := make(map[image.Point]bool) waterMask = NewPixelMask(width, height)
for _, p := range allWaterPixels {
isWater[p] = true
} }
if roadMask == nil {
isRoad := make(map[image.Point]bool) roadMask = NewPixelMask(width, height)
for _, p := range roadPixels {
isRoad[p] = true
} }
isExitRoad := make(map[image.Point]bool) if exitRoadMask == nil {
for _, p := range getExitRoadPixels() { exitRoadMask = NewPixelMask(width, height)
isExitRoad[p] = true
} }
buildingMask := NewPixelMask(width, height)
isBuilding := make(map[image.Point]bool)
var buildings [][]image.Point var buildings [][]image.Point
var allBuildingPixels []image.Point
var anchorPoints []image.Point var anchorPoints []image.Point
var normalRoadAnchors []image.Point var normalRoadAnchors []image.Point
var exitRoadAnchors []image.Point var exitRoadAnchors []image.Point
if len(roadPixels) > 0 { if len(roadAnchors) > 0 {
anchorPoints = roadPixels anchorPoints = roadAnchors
for _, p := range anchorPoints { for _, p := range anchorPoints {
if isExitRoad[p] { if exitRoadMask.GetPoint(p) {
exitRoadAnchors = append(exitRoadAnchors, p) exitRoadAnchors = append(exitRoadAnchors, p)
} else { } else {
normalRoadAnchors = append(normalRoadAnchors, p) normalRoadAnchors = append(normalRoadAnchors, p)
@@ -106,7 +109,7 @@ func GenerateBuildings(img *image.RGBA, width, height int, settings *Settings, r
for y := 0; y < height; y++ { for y := 0; y < height; y++ {
for x := 0; x < width; x++ { for x := 0; x < width; x++ {
p := image.Point{X: x, Y: y} p := image.Point{X: x, Y: y}
if !isWater[p] { if !waterMask.GetPoint(p) {
anchorPoints = append(anchorPoints, p) anchorPoints = append(anchorPoints, p)
} }
} }
@@ -129,7 +132,7 @@ func GenerateBuildings(img *image.RGBA, width, height int, settings *Settings, r
for y := 0; y < height; y++ { for y := 0; y < height; y++ {
for x := 0; x < width; x++ { for x := 0; x < width; x++ {
p := image.Point{X: x, Y: y} p := image.Point{X: x, Y: y}
if !isWater[p] && !isRoad[p] { if !waterMask.GetPoint(p) && !roadMask.GetPoint(p) {
landPoints = append(landPoints, p) landPoints = append(landPoints, p)
} }
} }
@@ -196,17 +199,16 @@ func GenerateBuildings(img *image.RGBA, width, height int, settings *Settings, r
var ok bool var ok bool
if shape == "procedural" { if shape == "procedural" {
pixels, ok = getProceduralBuildingPixels(center, size, settings, isWater, isRoad, isBuilding, width, height, randSrc) pixels, ok = getProceduralBuildingPixels(center, size, settings, waterMask, roadMask, buildingMask, width, height, randSrc)
} else { } else {
pixels, ok = getBuildingPixels(center, size, shape, isWater, isRoad, isBuilding, width, height, randSrc) pixels, ok = getBuildingPixels(center, size, shape, waterMask, roadMask, buildingMask, width, height, randSrc)
} }
if ok { if ok {
// If successful, draw the building and update data structures // If successful, draw the building and update data structures
for _, p := range pixels { for _, p := range pixels {
img.Set(p.X, p.Y, buildingColor) img.Set(p.X, p.Y, buildingColor)
isBuilding[p] = true buildingMask.SetPoint(p)
allBuildingPixels = append(allBuildingPixels, p)
} }
buildings = append(buildings, pixels) buildings = append(buildings, pixels)
buildingsPlaced++ buildingsPlaced++
@@ -214,11 +216,11 @@ func GenerateBuildings(img *image.RGBA, width, height int, settings *Settings, r
} }
} }
} }
return buildings, allBuildingPixels return buildings, buildingMask
} }
// getProceduralBuildingPixels generates a complex building by connecting multiple shapes. // getProceduralBuildingPixels generates a complex building by connecting multiple shapes.
func getProceduralBuildingPixels(center image.Point, size float64, settings *Settings, isWater, isRoad, isBuilding map[image.Point]bool, width, height int, randSrc *rand.Rand) ([]image.Point, bool) { func getProceduralBuildingPixels(center image.Point, size float64, settings *Settings, waterMask, roadMask, buildingMask *PixelMask, width, height int, randSrc *rand.Rand) ([]image.Point, bool) {
complexity := settings.MinBuildingComplexity complexity := settings.MinBuildingComplexity
if settings.BuildingComplexityRatio > randSrc.Float64()*100 { if settings.BuildingComplexityRatio > randSrc.Float64()*100 {
complexity = settings.MinBuildingComplexity + randSrc.Intn(settings.MaxBuildingComplexity-settings.MinBuildingComplexity+1) complexity = settings.MinBuildingComplexity + randSrc.Intn(settings.MaxBuildingComplexity-settings.MinBuildingComplexity+1)
@@ -293,7 +295,7 @@ func getProceduralBuildingPixels(center image.Point, size float64, settings *Set
continue continue
} }
for _, p := range pixels { for _, p := range pixels {
if p.X < 0 || p.Y < 0 || p.X >= width || p.Y >= height || isWater[p] || isRoad[p] || isBuilding[p] { if p.X < 0 || p.Y < 0 || p.X >= width || p.Y >= height || waterMask.GetPoint(p) || roadMask.GetPoint(p) || buildingMask.GetPoint(p) {
return nil, false return nil, false
} }
if !pixelMap[p] { if !pixelMap[p] {
@@ -436,7 +438,7 @@ func chooseShape(randSrc *rand.Rand, ratios map[string]float64) string {
} }
// getBuildingPixels determines the pixels for a single building based on its shape and checks for collisions. // getBuildingPixels determines the pixels for a single building based on its shape and checks for collisions.
func getBuildingPixels(center image.Point, size float64, shape string, isWater, isRoad, isBuilding map[image.Point]bool, width, height int, randSrc *rand.Rand) ([]image.Point, bool) { func getBuildingPixels(center image.Point, size float64, shape string, waterMask, roadMask, buildingMask *PixelMask, width, height int, randSrc *rand.Rand) ([]image.Point, bool) {
var pixels []image.Point var pixels []image.Point
var halfSize = int(size / 2) var halfSize = int(size / 2)
@@ -446,7 +448,7 @@ func getBuildingPixels(center image.Point, size float64, shape string, isWater,
for y := center.Y - halfSize; y <= center.Y+halfSize; y++ { for y := center.Y - halfSize; y <= center.Y+halfSize; y++ {
for x := center.X - halfSize; x <= center.X+halfSize; x++ { for x := center.X - halfSize; x <= center.X+halfSize; x++ {
p := image.Point{X: x, Y: y} p := image.Point{X: x, Y: y}
if p.X < 0 || p.Y < 0 || p.X >= width || p.Y >= height || isWater[p] || isRoad[p] || isBuilding[p] { if p.X < 0 || p.Y < 0 || p.X >= width || p.Y >= height || waterMask.GetPoint(p) || roadMask.GetPoint(p) || buildingMask.GetPoint(p) {
return nil, false // Collision detected return nil, false // Collision detected
} }
pixels = append(pixels, p) pixels = append(pixels, p)
@@ -459,7 +461,7 @@ func getBuildingPixels(center image.Point, size float64, shape string, isWater,
dx, dy := float64(x-center.X), float64(y-center.Y) dx, dy := float64(x-center.X), float64(y-center.Y)
if dx*dx+dy*dy <= r2 { if dx*dx+dy*dy <= r2 {
p := image.Point{X: x, Y: y} p := image.Point{X: x, Y: y}
if p.X < 0 || p.Y < 0 || p.X >= width || p.Y >= height || isWater[p] || isRoad[p] || isBuilding[p] { if p.X < 0 || p.Y < 0 || p.X >= width || p.Y >= height || waterMask.GetPoint(p) || roadMask.GetPoint(p) || buildingMask.GetPoint(p) {
return nil, false // Collision detected return nil, false // Collision detected
} }
pixels = append(pixels, p) pixels = append(pixels, p)
@@ -480,7 +482,7 @@ func getBuildingPixels(center image.Point, size float64, shape string, isWater,
for y := center.Y - halfH; y <= center.Y+halfH; y++ { for y := center.Y - halfH; y <= center.Y+halfH; y++ {
for x := center.X - halfW; x <= center.X+halfW; x++ { for x := center.X - halfW; x <= center.X+halfW; x++ {
p := image.Point{X: x, Y: y} p := image.Point{X: x, Y: y}
if p.X < 0 || p.Y < 0 || p.X >= width || p.Y >= height || isWater[p] || isRoad[p] || isBuilding[p] { if p.X < 0 || p.Y < 0 || p.X >= width || p.Y >= height || waterMask.GetPoint(p) || roadMask.GetPoint(p) || buildingMask.GetPoint(p) {
return nil, false // Collision detected return nil, false // Collision detected
} }
pixels = append(pixels, p) pixels = append(pixels, p)
+54 -80
View File
@@ -55,12 +55,14 @@ func main() {
// Initialize slices to store generated map features // Initialize slices to store generated map features
var lakes [][]image.Point var lakes [][]image.Point
var riverPixels []image.Point
var treePixels []image.Point
var buildingPixels []image.Point
var buildings [][]image.Point var buildings [][]image.Point
var roadPixels []image.Point var waterMask *PixelMask
var bridgePixels []image.Point var riverMask *PixelMask
var treeMask *PixelMask
var buildingMask *PixelMask
var roadMask *PixelMask
var bridgeMask *PixelMask
var exitRoadMask *PixelMask
// Set up application configuration directory // Set up application configuration directory
configDir, err := os.UserConfigDir() configDir, err := os.UserConfigDir()
@@ -157,47 +159,31 @@ func main() {
// Step 3: Generating Rivers // Step 3: Generating Rivers
var riverImage image.Image var riverImage image.Image
riverImage, riverMask = GenerateRivers(settings.Width, settings.Height, settings.Rivers, settings.MinRiverWidth, settings.MaxRiverWidth, settings.RiverCurvyness, lakeImage, lakes, seedProvider.Next(), noiseImg, settings.RiverWidthVariability, settings.RiverEdgeRoughness)
riverImage, riverPixels = GenerateRivers(settings.Width, settings.Height, settings.Rivers, settings.MinRiverWidth, settings.MaxRiverWidth, settings.RiverCurvyness, lakeImage, lakes, seedProvider.Next(), noiseImg, settings.RiverWidthVariability, settings.RiverEdgeRoughness)
finalImage := riverImage.(*image.RGBA) finalImage := riverImage.(*image.RGBA)
waterMask = BuildMaskFromLakes(settings.Width, settings.Height, lakes)
var roadImage *image.RGBA if riverMask != nil {
waterMask.Merge(riverMask)
var flatLakePixels []image.Point
for _, lake := range lakes {
flatLakePixels = append(flatLakePixels, lake...)
} }
allWaterPixels := append(flatLakePixels, riverPixels...)
// Step 4: Generating Roads // Step 4: Generating Roads
roadPixels, bridgePixels, roadImage = GenerateRoads(settings.Width, settings.Height, settings, noiseImg, allWaterPixels, seedProvider.Next()) var roadAnchors []image.Point
for y := 0; y < roadImage.Bounds().Max.Y; y++ { roadMask, bridgeMask, exitRoadMask, roadAnchors = GenerateRoads(finalImage, settings.Width, settings.Height, settings, waterMask, seedProvider.Next())
for x := 0; x < roadImage.Bounds().Max.X; x++ {
r, g, b, a := roadImage.At(x, y).RGBA()
if a > 0 {
finalImage.Set(x, y, color.RGBA{R: uint8(r >> 8), G: uint8(g >> 8), B: uint8(b >> 8), A: uint8(a >> 8)})
}
}
}
// Step 5: Generating Buildings // Step 5: Generating Buildings
buildings, buildingPixels = GenerateBuildings(finalImage, settings.Width, settings.Height, settings, roadPixels, allWaterPixels, seedProvider.Next()) buildings, buildingMask = GenerateBuildings(finalImage, settings.Width, settings.Height, settings, roadAnchors, waterMask, roadMask, exitRoadMask, seedProvider.Next())
// Step 6: Generating Trees // Step 6: Generating Trees
treePixels = GenerateTrees(finalImage, allWaterPixels, roadPixels, buildingPixels, settings.MinTreeSize, settings.MaxTreeSize, settings.TreeCoverage, settings.TreeClumpiness, seedProvider.Next()) treeMask = GenerateTrees(finalImage, waterMask, roadMask, buildingMask, settings.MinTreeSize, settings.MaxTreeSize, settings.TreeCoverage, settings.TreeClumpiness, seedProvider.Next())
// Step 7: Darkening Water Areas // Step 7: Darkening Water Areas
darkenedHeightmap := DarkenLakeAreas(noiseImg, allWaterPixels) darkenedHeightmap := DarkenLakeAreas(noiseImg, waterMask)
// Step 8: Flattening Building Areas // Step 8: Flattening Building Areas
flattenedBuildingHeightmap := FlattenBuildingAreas(darkenedHeightmap.(*image.RGBA), buildings, settings.Width, settings.Height) flattenedBuildingHeightmap := FlattenBuildingAreas(darkenedHeightmap.(*image.RGBA), buildings, settings.Width, settings.Height)
// Step 9: Flattening Road Areas // Step 9: Flattening Road Areas
flattenedHeightmap := FlattenRoadAreas(flattenedBuildingHeightmap, roadPixels) flattenedHeightmap := FlattenRoadAreas(flattenedBuildingHeightmap, roadMask)
// Step 10: Applying Roughness // Step 10: Applying Roughness
compositeImg := ApplyRoughness(flattenedHeightmap, settings.Roughness) compositeImg := ApplyRoughness(flattenedHeightmap, settings.Roughness)
@@ -519,7 +505,7 @@ func main() {
showSaveDialog(w, bumpmapImg.Image, settings) showSaveDialog(w, bumpmapImg.Image, settings)
}) })
exportMasksBtn := widget.NewButton("Export Masks", func() { exportMasksBtn := widget.NewButton("Export Masks", func() {
showMasksSaveDialog(w, canvasImg.Image, heightmapImg.Image, bumpmapImg.Image, settings, lakes, riverPixels, treePixels, roadPixels, bridgePixels, buildingPixels, buildings) showMasksSaveDialog(w, canvasImg.Image, heightmapImg.Image, bumpmapImg.Image, settings, lakes, riverMask, treeMask, roadMask, bridgeMask, buildingMask)
}) })
// Main generation button and logic // Main generation button and logic
generateBtn = widget.NewButton("Generate", func() { generateBtn = widget.NewButton("Generate", func() {
@@ -560,15 +546,13 @@ func main() {
progressBar.SetValue(3.0 / 11.0) progressBar.SetValue(3.0 / 11.0)
}) })
var riverImage image.Image var riverImage image.Image
riverImage, riverPixels = GenerateRivers(settings.Width, settings.Height, settings.Rivers, settings.MinRiverWidth, settings.MaxRiverWidth, settings.RiverCurvyness, lakeImage, lakes, seedProvider.Next(), noiseImg, settings.RiverWidthVariability, settings.RiverEdgeRoughness) riverImage, riverMask = GenerateRivers(settings.Width, settings.Height, settings.Rivers, settings.MinRiverWidth, settings.MaxRiverWidth, settings.RiverCurvyness, lakeImage, lakes, seedProvider.Next(), noiseImg, settings.RiverWidthVariability, settings.RiverEdgeRoughness)
finalImage := riverImage.(*image.RGBA) finalImage := riverImage.(*image.RGBA)
waterMask = BuildMaskFromLakes(settings.Width, settings.Height, lakes)
var flatLakePixels []image.Point if riverMask != nil {
for _, lake := range lakes { waterMask.Merge(riverMask)
flatLakePixels = append(flatLakePixels, lake...)
} }
allWaterPixels := append(flatLakePixels, riverPixels...)
// Step 4: Generating Roads // Step 4: Generating Roads
@@ -576,16 +560,8 @@ func main() {
progressLabel.SetText("Step 4 of 11: Generating Roads") progressLabel.SetText("Step 4 of 11: Generating Roads")
progressBar.SetValue(4.0 / 11.0) progressBar.SetValue(4.0 / 11.0)
}) })
var roadImage *image.RGBA var roadAnchors []image.Point
roadPixels, bridgePixels, roadImage = GenerateRoads(settings.Width, settings.Height, settings, noiseImg, allWaterPixels, seedProvider.Next()) // Draw roadImage over finalImage roadMask, bridgeMask, exitRoadMask, roadAnchors = GenerateRoads(finalImage, settings.Width, settings.Height, settings, waterMask, seedProvider.Next())
for y := 0; y < roadImage.Bounds().Max.Y; y++ {
for x := 0; x < roadImage.Bounds().Max.X; x++ {
r, g, b, a := roadImage.At(x, y).RGBA()
if a > 0 {
finalImage.Set(x, y, color.RGBA{R: uint8(r >> 8), G: uint8(g >> 8), B: uint8(b >> 8), A: uint8(a >> 8)})
}
}
}
// Step 5: Generating Buildings // Step 5: Generating Buildings
@@ -593,7 +569,7 @@ func main() {
progressLabel.SetText("Step 5 of 11: Generating Buildings") progressLabel.SetText("Step 5 of 11: Generating Buildings")
progressBar.SetValue(5.0 / 11.0) progressBar.SetValue(5.0 / 11.0)
}) })
buildings, buildingPixels = GenerateBuildings(finalImage, settings.Width, settings.Height, settings, roadPixels, allWaterPixels, seedProvider.Next()) buildings, buildingMask = GenerateBuildings(finalImage, settings.Width, settings.Height, settings, roadAnchors, waterMask, roadMask, exitRoadMask, seedProvider.Next())
// Step 6: Darkening Water Areas // Step 6: Darkening Water Areas
@@ -601,7 +577,7 @@ func main() {
progressLabel.SetText("Step 6 of 11: Darkening Water Areas") progressLabel.SetText("Step 6 of 11: Darkening Water Areas")
progressBar.SetValue(6.0 / 11.0) progressBar.SetValue(6.0 / 11.0)
}) })
darkenedHeightmap := DarkenLakeAreas(noiseImg, allWaterPixels) darkenedHeightmap := DarkenLakeAreas(noiseImg, waterMask)
// Step 7: Flattening Building Areas // Step 7: Flattening Building Areas
@@ -615,7 +591,7 @@ func main() {
progressLabel.SetText("Step 8 of 11: Flattening Road Areas") progressLabel.SetText("Step 8 of 11: Flattening Road Areas")
progressBar.SetValue(8.0 / 11.0) progressBar.SetValue(8.0 / 11.0)
}) })
flattenedHeightmap := FlattenRoadAreas(flattenedBuildingHeightmap, roadPixels) flattenedHeightmap := FlattenRoadAreas(flattenedBuildingHeightmap, roadMask)
// Step 8: Applying Roughness // Step 8: Applying Roughness
@@ -631,7 +607,7 @@ func main() {
progressLabel.SetText("Step 10 of 11: Generating Trees") progressLabel.SetText("Step 10 of 11: Generating Trees")
progressBar.SetValue(10.0 / 11.0) progressBar.SetValue(10.0 / 11.0)
}) })
treePixels = GenerateTrees(finalImage, allWaterPixels, roadPixels, buildingPixels, settings.MinTreeSize, settings.MaxTreeSize, settings.TreeCoverage, settings.TreeClumpiness, seedProvider.Next()) treeMask = GenerateTrees(finalImage, waterMask, roadMask, buildingMask, settings.MinTreeSize, settings.MaxTreeSize, settings.TreeCoverage, settings.TreeClumpiness, seedProvider.Next())
// Step 10: Generating Bump Map // Step 10: Generating Bump Map
@@ -1105,7 +1081,7 @@ func getImageData(img image.Image, format string) (*bytes.Buffer, error) {
} }
// showMasksSaveDialog displays a dialog for saving the generated masks. // showMasksSaveDialog displays a dialog for saving the generated masks.
func showMasksSaveDialog(win fyne.Window, canvasImg, heightmapImg, bumpmapImg image.Image, settings *Settings, lakes [][]image.Point, riverPixels, treePixels, roadPixels, bridgePixels, buildingPixels []image.Point, buildings [][]image.Point) { func showMasksSaveDialog(win fyne.Window, canvasImg, heightmapImg, bumpmapImg image.Image, settings *Settings, lakes [][]image.Point, riverMask, treeMask, roadMask, bridgeMask, buildingMask *PixelMask) {
// Create UI elements for the save dialog // Create UI elements for the save dialog
fileNameEntry := widget.NewEntry() fileNameEntry := widget.NewEntry()
fileNameEntry.SetPlaceHolder("masks_folder") fileNameEntry.SetPlaceHolder("masks_folder")
@@ -1152,6 +1128,21 @@ 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 {
out := image.NewGray(bounds)
if mask == nil {
return out
}
for y := 0; y < mask.Height; y++ {
row := y * mask.Width
for x := 0; x < mask.Width; x++ {
if mask.Data[row+x] != 0 {
out.SetGray(x, y, color.Gray{Y: 255})
}
}
}
return out
}
// Create mask images from the generated data // Create mask images from the generated data
lakeMask := image.NewGray(bounds) lakeMask := image.NewGray(bounds)
@@ -1160,39 +1151,22 @@ func showMasksSaveDialog(win fyne.Window, canvasImg, heightmapImg, bumpmapImg im
lakeMask.SetGray(p.X, p.Y, color.Gray{Y: 255}) lakeMask.SetGray(p.X, p.Y, color.Gray{Y: 255})
} }
} }
riverMask := image.NewGray(bounds) riverMaskImg := maskToGray(riverMask)
for _, p := range riverPixels { treeMaskImg := maskToGray(treeMask)
riverMask.SetGray(p.X, p.Y, color.Gray{Y: 255}) roadMaskImg := maskToGray(roadMask)
} bridgeMaskImg := maskToGray(bridgeMask)
treeMask := image.NewGray(bounds) buildingMaskImg := maskToGray(buildingMask)
for _, p := range treePixels {
treeMask.SetGray(p.X, p.Y, color.Gray{Y: 255})
}
roadMask := image.NewGray(bounds)
for _, p := range roadPixels {
roadMask.SetGray(p.X, p.Y, color.Gray{Y: 255})
}
bridgeMask := image.NewGray(bounds)
for _, p := range bridgePixels {
bridgeMask.SetGray(p.X, p.Y, color.Gray{Y: 255})
}
buildingMask := image.NewGray(bounds)
for _, building := range buildings {
for _, p := range building {
buildingMask.SetGray(p.X, p.Y, color.Gray{Y: 255})
}
}
imagesToSave := map[string]image.Image{ imagesToSave := map[string]image.Image{
"canvas." + imgFormat: canvasImg, "canvas." + imgFormat: canvasImg,
"heightmap." + imgFormat: heightmapImg, "heightmap." + imgFormat: heightmapImg,
"bump_map." + imgFormat: bumpmapImg, "bump_map." + imgFormat: bumpmapImg,
"lakes_mask." + imgFormat: lakeMask, "lakes_mask." + imgFormat: lakeMask,
"rivers_mask." + imgFormat: riverMask, "rivers_mask." + imgFormat: riverMaskImg,
"trees_mask." + imgFormat: treeMask, "trees_mask." + imgFormat: treeMaskImg,
"roads_mask." + imgFormat: roadMask, "roads_mask." + imgFormat: roadMaskImg,
"bridges_mask." + imgFormat: bridgeMask, "bridges_mask." + imgFormat: bridgeMaskImg,
"buildings_mask." + imgFormat: buildingMask, "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 {
+91
View File
@@ -0,0 +1,91 @@
package main
import "image"
// PixelMask stores per-pixel occupancy using one byte per pixel.
type PixelMask struct {
Width int
Height int
Data []uint8
}
func NewPixelMask(width, height int) *PixelMask {
if width <= 0 || height <= 0 {
return &PixelMask{}
}
return &PixelMask{
Width: width,
Height: height,
Data: make([]uint8, width*height),
}
}
func (m *PixelMask) index(x, y int) int {
return y*m.Width + x
}
func (m *PixelMask) InBounds(x, y int) bool {
return m != nil && x >= 0 && y >= 0 && x < m.Width && y < m.Height
}
func (m *PixelMask) GetXY(x, y int) bool {
if !m.InBounds(x, y) {
return false
}
return m.Data[m.index(x, y)] != 0
}
func (m *PixelMask) SetXY(x, y int) {
if m.InBounds(x, y) {
m.Data[m.index(x, y)] = 1
}
}
func (m *PixelMask) GetPoint(p image.Point) bool {
return m.GetXY(p.X, p.Y)
}
func (m *PixelMask) SetPoint(p image.Point) {
m.SetXY(p.X, p.Y)
}
func (m *PixelMask) Merge(other *PixelMask) {
if m == nil || other == nil || m.Width != other.Width || m.Height != other.Height {
return
}
for i := range m.Data {
if other.Data[i] != 0 {
m.Data[i] = 1
}
}
}
func (m *PixelMask) AddPoints(points []image.Point) {
for _, p := range points {
m.SetPoint(p)
}
}
func (m *PixelMask) ToPoints() []image.Point {
if m == nil {
return nil
}
pts := make([]image.Point, 0)
for y := 0; y < m.Height; y++ {
row := y * m.Width
for x := 0; x < m.Width; x++ {
if m.Data[row+x] != 0 {
pts = append(pts, image.Point{X: x, Y: y})
}
}
}
return pts
}
func BuildMaskFromLakes(width, height int, lakes [][]image.Point) *PixelMask {
mask := NewPixelMask(width, height)
for _, lake := range lakes {
mask.AddPoints(lake)
}
return mask
}
+49 -59
View File
@@ -31,8 +31,6 @@ type Road struct {
Importance int Importance int
} }
var lastExitRoadPixels []image.Point
const ( const (
minRoadWidthPercent = 0.1 minRoadWidthPercent = 0.1
maxRoadWidthPercent = 5.0 maxRoadWidthPercent = 5.0
@@ -81,54 +79,50 @@ func getRoadWidthRangePixels(settings *Settings, width, height int) (float64, fl
return minPx, maxPx return minPx, maxPx
} }
func getExitRoadPixels() []image.Point {
out := make([]image.Point, len(lastExitRoadPixels))
copy(out, lastExitRoadPixels)
return out
}
// GenerateRoads creates roads on the map. // GenerateRoads creates roads on the map.
func GenerateRoads(width, height int, settings *Settings, _ image.Image, allWaterPixels []image.Point, seed int64) ([]image.Point, []image.Point, *image.RGBA) { func GenerateRoads(
img := image.NewRGBA(image.Rect(0, 0, width, height)) img *image.RGBA,
width,
height int,
settings *Settings,
waterMask *PixelMask,
seed int64,
) (*PixelMask, *PixelMask, *PixelMask, []image.Point) {
if img == nil {
img = image.NewRGBA(image.Rect(0, 0, width, height))
}
randSrc := rand.New(rand.NewSource(seed)) randSrc := rand.New(rand.NewSource(seed))
roadColor := color.RGBA{R: 139, G: 69, B: 19, A: 255} roadColor := color.RGBA{R: 139, G: 69, B: 19, A: 255}
bridgeColor := color.RGBA{R: 60, G: 42, B: 33, A: 255} bridgeColor := color.RGBA{R: 60, G: 42, B: 33, A: 255}
waterMap := make(map[image.Point]bool, len(allWaterPixels))
for _, p := range allWaterPixels {
waterMap[p] = true
}
roadTarget := estimateRoadTarget(settings, randSrc) roadTarget := estimateRoadTarget(settings, randSrc)
pois := generatePOIs(width, height, settings, waterMap, randSrc, roadTarget) pois := generatePOIs(width, height, settings, waterMask, randSrc, roadTarget)
if len(pois) < 2 { if len(pois) < 2 {
return nil, nil, img return NewPixelMask(width, height), NewPixelMask(width, height), NewPixelMask(width, height), nil
} }
roads := connectPOIs(pois, width, height, settings, randSrc, waterMap, roadTarget) roads := connectPOIs(pois, width, height, settings, randSrc, waterMask, roadTarget)
roads = appendExitRoads(roads, pois, width, height, settings, randSrc, waterMap) roads = appendExitRoads(roads, pois, width, height, settings, randSrc, waterMask)
if len(roads) == 0 { if len(roads) == 0 {
return nil, nil, img return NewPixelMask(width, height), NewPixelMask(width, height), NewPixelMask(width, height), nil
} }
assignRoadWidths(roads, settings, randSrc, width, height) assignRoadWidths(roads, settings, randSrc, width, height)
allRoadPixels := make([]image.Point, 0, len(roads)*64) roadMask := NewPixelMask(width, height)
allBridgePixels := make([]image.Point, 0, len(roads)*16) bridgeMask := NewPixelMask(width, height)
exitRoadPixels := make([]image.Point, 0, len(roads)*16) exitRoadMask := NewPixelMask(width, height)
for _, road := range roads { for _, road := range roads {
roadPixels, bridgePixels := drawRoad(img, road.Points, roadColor, bridgeColor, road.Width) drawRoadToMasks(img, road.Points, roadColor, bridgeColor, road.Width, roadMask, bridgeMask)
allRoadPixels = append(allRoadPixels, roadPixels...)
allBridgePixels = append(allBridgePixels, bridgePixels...)
if road.Start.IsExit || road.End.IsExit { if road.Start.IsExit || road.End.IsExit {
exitRoadPixels = append(exitRoadPixels, roadPixels...) drawRoadToMasks(img, road.Points, roadColor, bridgeColor, road.Width, exitRoadMask, exitRoadMask)
exitRoadPixels = append(exitRoadPixels, bridgePixels...)
} }
} }
lastExitRoadPixels = exitRoadPixels
return allRoadPixels, allBridgePixels, img roadAnchors := roadMask.ToPoints()
return roadMask, bridgeMask, exitRoadMask, roadAnchors
} }
func generatePOIs(width, height int, settings *Settings, waterMap map[image.Point]bool, randSrc *rand.Rand, roadTarget int) []*PointOfInterest { func generatePOIs(width, height int, settings *Settings, waterMask *PixelMask, randSrc *rand.Rand, roadTarget int) []*PointOfInterest {
distribution := clamp01(settings.RoadDistribution / 100.0) distribution := clamp01(settings.RoadDistribution / 100.0)
minBuildingSizePx, maxBuildingSizePx := getBuildingSizeRangePixels(settings, width, height) minBuildingSizePx, maxBuildingSizePx := getBuildingSizeRangePixels(settings, width, height)
avgBuildingSize := (minBuildingSizePx + maxBuildingSizePx) / 2.0 avgBuildingSize := (minBuildingSizePx + maxBuildingSizePx) / 2.0
@@ -160,7 +154,7 @@ func generatePOIs(width, height int, settings *Settings, waterMap map[image.Poin
} }
p := image.Point{X: x, Y: y} p := image.Point{X: x, Y: y}
// Keep larger spacing between intersections so buildings have room. // Keep larger spacing between intersections so buildings have room.
if waterMap[p] || isTooCloseToExisting(pois, x, y, avgBuildingSize*1.1) { if waterMask.GetPoint(p) || isTooCloseToExisting(pois, x, y, avgBuildingSize*1.1) {
continue continue
} }
pois = append(pois, &PointOfInterest{X: x, Y: y, TargetDegree: sampleTargetDegree(randSrc)}) pois = append(pois, &PointOfInterest{X: x, Y: y, TargetDegree: sampleTargetDegree(randSrc)})
@@ -254,7 +248,7 @@ func sampleTargetDegree(randSrc *rand.Rand) int {
} }
} }
func connectPOIs(pois []*PointOfInterest, width, height int, settings *Settings, randSrc *rand.Rand, waterMap map[image.Point]bool, roadTarget int) []*Road { func connectPOIs(pois []*PointOfInterest, width, height int, settings *Settings, randSrc *rand.Rand, waterMask *PixelMask, roadTarget int) []*Road {
minAngle := settings.MinRoadAngle * math.Pi / 180.0 minAngle := settings.MinRoadAngle * math.Pi / 180.0
if minAngle < 0 { if minAngle < 0 {
minAngle = 0 minAngle = 0
@@ -399,7 +393,7 @@ func connectPOIs(pois []*PointOfInterest, width, height int, settings *Settings,
for _, e := range selectedEdges { for _, e := range selectedEdges {
a := pois[e.a] a := pois[e.a]
b := pois[e.b] b := pois[e.b]
path := calculateRoadPath(a, b, settings.RoadCurvyness/100.0, avgDim, randSrc, waterMap) path := calculateRoadPath(a, b, settings.RoadCurvyness/100.0, avgDim, randSrc, waterMask)
imp := a.Connections + b.Connections + int(math.Round((a.ArterialWeight+b.ArterialWeight)*4)) imp := a.Connections + b.Connections + int(math.Round((a.ArterialWeight+b.ArterialWeight)*4))
roads = append(roads, &Road{Start: a, End: b, Points: path, Importance: imp}) roads = append(roads, &Road{Start: a, End: b, Points: path, Importance: imp})
} }
@@ -407,7 +401,7 @@ func connectPOIs(pois []*PointOfInterest, width, height int, settings *Settings,
return roads return roads
} }
func appendExitRoads(roads []*Road, pois []*PointOfInterest, width, height int, settings *Settings, randSrc *rand.Rand, waterMap map[image.Point]bool) []*Road { func appendExitRoads(roads []*Road, pois []*PointOfInterest, width, height int, settings *Settings, randSrc *rand.Rand, waterMask *PixelMask) []*Road {
if settings.RoadExits <= 0 || len(pois) == 0 { if settings.RoadExits <= 0 || len(pois) == 0 {
return roads return roads
} }
@@ -417,7 +411,7 @@ func appendExitRoads(roads []*Road, pois []*PointOfInterest, width, height int,
usedEdgePoints := make([]image.Point, 0, settings.RoadExits) usedEdgePoints := make([]image.Point, 0, settings.RoadExits)
for i := 0; i < settings.RoadExits; i++ { for i := 0; i < settings.RoadExits; i++ {
edgeNode, ok := sampleNonWaterEdgePOI(width, height, randSrc, waterMap, usedEdgePoints) edgeNode, ok := sampleNonWaterEdgePOI(width, height, randSrc, waterMask, usedEdgePoints)
if !ok { if !ok {
continue continue
} }
@@ -432,7 +426,7 @@ func appendExitRoads(roads []*Road, pois []*PointOfInterest, width, height int,
edgeNode.TargetDegree = 1 edgeNode.TargetDegree = 1
edgeNode.Connections = 1 edgeNode.Connections = 1
path := calculateRoadPath(anchor, edgeNode, settings.RoadCurvyness/100.0, avgDim, randSrc, waterMap) path := calculateRoadPath(anchor, edgeNode, settings.RoadCurvyness/100.0, avgDim, randSrc, waterMask)
importance := anchor.Connections + edgeNode.Connections + int(math.Round(anchor.ArterialWeight*3)) importance := anchor.Connections + edgeNode.Connections + int(math.Round(anchor.ArterialWeight*3))
roads = append(roads, &Road{ roads = append(roads, &Road{
Start: anchor, Start: anchor,
@@ -448,14 +442,14 @@ func appendExitRoads(roads []*Road, pois []*PointOfInterest, width, height int,
return roads return roads
} }
func sampleNonWaterEdgePOI(width, height int, randSrc *rand.Rand, waterMap map[image.Point]bool, used []image.Point) (*PointOfInterest, bool) { func sampleNonWaterEdgePOI(width, height int, randSrc *rand.Rand, waterMask *PixelMask, used []image.Point) (*PointOfInterest, bool) {
minSpacing := math.Min(float64(width), float64(height)) * 0.08 minSpacing := math.Min(float64(width), float64(height)) * 0.08
minSpacing2 := minSpacing * minSpacing minSpacing2 := minSpacing * minSpacing
for tries := 0; tries < 120; tries++ { for tries := 0; tries < 120; tries++ {
p := sampleEdgePOI(width, height, randSrc) p := sampleEdgePOI(width, height, randSrc)
pt := image.Point{X: p.X, Y: p.Y} pt := image.Point{X: p.X, Y: p.Y}
if waterMap[pt] { if waterMask.GetPoint(pt) {
continue continue
} }
tooClose := false tooClose := false
@@ -614,10 +608,8 @@ func assignRoadWidths(roads []*Road, settings *Settings, randSrc *rand.Rand, wid
} }
} }
// drawRoad draws a single road on the image including bridges. // drawRoadToMasks draws a single road on the image including bridges.
func drawRoad(img *image.RGBA, points []PathPoint, roadColor, bridgeColor color.Color, width int) ([]image.Point, []image.Point) { func drawRoadToMasks(img *image.RGBA, points []PathPoint, roadColor, bridgeColor color.Color, width int, roadMask, bridgeMask *PixelMask) {
var roadPixels []image.Point
var bridgePixels []image.Point
bridgeWidth := int(math.Ceil(float64(width) * 1.15)) bridgeWidth := int(math.Ceil(float64(width) * 1.15))
if bridgeWidth < 1 { if bridgeWidth < 1 {
bridgeWidth = 1 bridgeWidth = 1
@@ -628,8 +620,7 @@ func drawRoad(img *image.RGBA, points []PathPoint, roadColor, bridgeColor color.
p2 := points[i+1] p2 := points[i+1]
isBridge := p1.IsBridge && p2.IsBridge isBridge := p1.IsBridge && p2.IsBridge
if !isBridge { if !isBridge {
linePoints := drawLine(img, p1.Point.X, p1.Point.Y, p2.Point.X, p2.Point.Y, roadColor, width) drawLineMasked(img, p1.Point.X, p1.Point.Y, p2.Point.X, p2.Point.Y, roadColor, width, roadMask)
roadPixels = append(roadPixels, linePoints...)
i++ i++
continue continue
} }
@@ -640,17 +631,16 @@ func drawRoad(img *image.RGBA, points []PathPoint, roadColor, bridgeColor color.
for end < len(points)-1 && points[end].IsBridge && points[end+1].IsBridge { for end < len(points)-1 && points[end].IsBridge && points[end+1].IsBridge {
end++ end++
} }
linePoints := drawLine( drawLineMasked(
img, img,
points[start].Point.X, points[start].Point.Y, points[start].Point.X, points[start].Point.Y,
points[end].Point.X, points[end].Point.Y, points[end].Point.X, points[end].Point.Y,
bridgeColor, bridgeColor,
bridgeWidth, bridgeWidth,
bridgeMask,
) )
bridgePixels = append(bridgePixels, linePoints...)
i = end i = end
} }
return roadPixels, bridgePixels
} }
func bresenhamRoad(path []image.Point) []image.Point { func bresenhamRoad(path []image.Point) []image.Point {
@@ -693,27 +683,27 @@ func bresenhamRoad(path []image.Point) []image.Point {
} }
// calculateRoadPath computes the path for a road including curves and bridges. // calculateRoadPath computes the path for a road including curves and bridges.
func calculateRoadPath(start, end *PointOfInterest, curvyness, avgDim float64, randSrc *rand.Rand, waterMap map[image.Point]bool) []PathPoint { func calculateRoadPath(start, end *PointOfInterest, curvyness, avgDim float64, randSrc *rand.Rand, waterMask *PixelMask) []PathPoint {
dx := end.X - start.X dx := end.X - start.X
dy := end.Y - start.Y dy := end.Y - start.Y
dist := math.Hypot(float64(dx), float64(dy)) dist := math.Hypot(float64(dx), float64(dy))
if dist == 0 { if dist == 0 {
p := image.Point{X: start.X, Y: start.Y} p := image.Point{X: start.X, Y: start.Y}
return []PathPoint{{Point: p, IsBridge: waterMap[p]}} return []PathPoint{{Point: p, IsBridge: waterMask.GetPoint(p)}}
} }
curve := clamp(curvyness, 0, 1) curve := clamp(curvyness, 0, 1)
if curve <= 0 { if curve <= 0 {
points := bresenhamRoad([]image.Point{{X: start.X, Y: start.Y}, {X: end.X, Y: end.Y}}) points := bresenhamRoad([]image.Point{{X: start.X, Y: start.Y}, {X: end.X, Y: end.Y}})
return toPathPoints(points, waterMap) return toPathPoints(points, waterMask)
} }
// Non-linear scaling: low values stay fairly straight, high values become very winding. // Non-linear scaling: low values stay fairly straight, high values become very winding.
strength := math.Pow(curve, 1.35) strength := math.Pow(curve, 1.35)
if strength < 0.001 { if strength < 0.001 {
points := bresenhamRoad([]image.Point{{X: start.X, Y: start.Y}, {X: end.X, Y: end.Y}}) points := bresenhamRoad([]image.Point{{X: start.X, Y: start.Y}, {X: end.X, Y: end.Y}})
return toPathPoints(points, waterMap) return toPathPoints(points, waterMask)
} }
baseControls := int(math.Max(12, dist/(22.0-14.0*strength))) baseControls := int(math.Max(12, dist/(22.0-14.0*strength)))
@@ -774,20 +764,19 @@ func calculateRoadPath(start, end *PointOfInterest, curvyness, avgDim float64, r
} }
points := bresenhamRoad(controlPoints) points := bresenhamRoad(controlPoints)
return toPathPoints(points, waterMap) return toPathPoints(points, waterMask)
} }
func toPathPoints(points []image.Point, waterMap map[image.Point]bool) []PathPoint { func toPathPoints(points []image.Point, waterMask *PixelMask) []PathPoint {
pathPoints := make([]PathPoint, len(points)) pathPoints := make([]PathPoint, len(points))
for i, p := range points { for i, p := range points {
pathPoints[i] = PathPoint{Point: p, IsBridge: waterMap[p]} pathPoints[i] = PathPoint{Point: p, IsBridge: waterMask.GetPoint(p)}
} }
return pathPoints return pathPoints
} }
// drawLine draws a line with specified width on the image. // drawLineMasked draws a line with specified width on the image and mask.
func drawLine(img *image.RGBA, x0, y0, x1, y1 int, col color.Color, width int) []image.Point { func drawLineMasked(img *image.RGBA, x0, y0, x1, y1 int, col color.Color, width int, mask *PixelMask) {
var points []image.Point
dx := abs(x1 - x0) dx := abs(x1 - x0)
dy := -abs(y1 - y0) dy := -abs(y1 - y0)
sx := -1 sx := -1
@@ -807,7 +796,9 @@ func drawLine(img *image.RGBA, x0, y0, x1, y1 int, col color.Color, width int) [
py := y0 + j py := y0 + j
if img.Bounds().Min.X <= px && px < img.Bounds().Max.X && img.Bounds().Min.Y <= py && py < img.Bounds().Max.Y { if img.Bounds().Min.X <= px && px < img.Bounds().Max.X && img.Bounds().Min.Y <= py && py < img.Bounds().Max.Y {
img.Set(px, py, col) img.Set(px, py, col)
points = append(points, image.Point{X: px, Y: py}) if mask != nil {
mask.SetXY(px, py)
}
} }
} }
} }
@@ -825,7 +816,6 @@ func drawLine(img *image.RGBA, x0, y0, x1, y1 int, col color.Color, width int) [
y0 += sy y0 += sy
} }
} }
return points
} }
func clamp(v, lo, hi float64) float64 { func clamp(v, lo, hi float64) float64 {
+40 -29
View File
@@ -81,15 +81,22 @@ func ApplyRoughness(heightmap image.Image, roughness float64) image.Image {
return composite return composite
} }
// DarkenLakeAreas darkens the heightmap where lakes exist // DarkenLakeAreas darkens the heightmap where water exists.
func DarkenLakeAreas(heightmap image.Image, lakePixels []image.Point) image.Image { func DarkenLakeAreas(heightmap image.Image, waterMask *PixelMask) image.Image {
bounds := heightmap.Bounds() bounds := heightmap.Bounds()
width := bounds.Dx() width := bounds.Dx()
lakeMask := image.NewRGBA(bounds) lakeMask := image.NewRGBA(bounds)
black := color.RGBA{0, 0, 0, 255} black := color.RGBA{0, 0, 0, 255}
for _, p := range lakePixels { if waterMask != nil {
lakeMask.Set(p.X, p.Y, black) for y := 0; y < waterMask.Height; y++ {
row := y * waterMask.Width
for x := 0; x < waterMask.Width; x++ {
if waterMask.Data[row+x] != 0 {
lakeMask.Set(x, y, black)
}
}
}
} }
blurRadius := float64(width) * 0.05 blurRadius := float64(width) * 0.05
@@ -102,18 +109,25 @@ func DarkenLakeAreas(heightmap image.Image, lakePixels []image.Point) image.Imag
return composite return composite
} }
// FlattenRoadAreas smooths terrain under roads // FlattenRoadAreas smooths terrain under roads.
func FlattenRoadAreas(heightmap image.Image, roadPixels []image.Point) image.Image { func FlattenRoadAreas(heightmap image.Image, roadMask *PixelMask) image.Image {
bounds := heightmap.Bounds() bounds := heightmap.Bounds()
width := bounds.Dx() width := bounds.Dx()
roadMask := image.NewGray(bounds) roadGrayMask := image.NewGray(bounds)
for _, p := range roadPixels { if roadMask != nil {
roadMask.SetGray(p.X, p.Y, color.Gray{Y: 255}) for y := 0; y < roadMask.Height; y++ {
row := y * roadMask.Width
for x := 0; x < roadMask.Width; x++ {
if roadMask.Data[row+x] != 0 {
roadGrayMask.SetGray(x, y, color.Gray{Y: 255})
}
}
}
} }
blurRadius := float64(width) * 0.01 blurRadius := float64(width) * 0.01
blurredRoadMask := imaging.Blur(roadMask, blurRadius) blurredRoadMask := imaging.Blur(roadGrayMask, blurRadius)
blurredHeightmap := imaging.Blur(heightmap, blurRadius) blurredHeightmap := imaging.Blur(heightmap, blurRadius)
@@ -146,8 +160,8 @@ func FlattenRoadAreas(heightmap image.Image, roadPixels []image.Point) image.Ima
return composite return composite
} }
// GenerateTrees places trees on the map based on coverage and noise // GenerateTrees places trees on the map based on coverage and noise.
func GenerateTrees(img *image.RGBA, lakePixels, roadPixels, buildingPixels []image.Point, minTreeSize, maxTreeSize, treeCoverage, treeClumpiness float64, seed int64) []image.Point { func GenerateTrees(img *image.RGBA, waterMask, roadMask, buildingMask *PixelMask, minTreeSize, maxTreeSize, treeCoverage, treeClumpiness float64, seed int64) *PixelMask {
width := img.Bounds().Dx() width := img.Bounds().Dx()
height := img.Bounds().Dy() height := img.Bounds().Dy()
@@ -179,19 +193,14 @@ func GenerateTrees(img *image.RGBA, lakePixels, roadPixels, buildingPixels []ima
} }
threshold := uint8(255 * (1 - (treeCoverage / 100.0))) threshold := uint8(255 * (1 - (treeCoverage / 100.0)))
isLake := make(map[image.Point]bool) if waterMask == nil {
for _, p := range lakePixels { waterMask = NewPixelMask(width, height)
isLake[p] = true
} }
if roadMask == nil {
isRoad := make(map[image.Point]bool) roadMask = NewPixelMask(width, height)
for _, p := range roadPixels {
isRoad[p] = true
} }
if buildingMask == nil {
isBuilding := make(map[image.Point]bool) buildingMask = NewPixelMask(width, height)
for _, p := range buildingPixels {
isBuilding[p] = true
} }
randSrc := rand.New(rand.NewSource(seed)) randSrc := rand.New(rand.NewSource(seed))
@@ -202,7 +211,7 @@ func GenerateTrees(img *image.RGBA, lakePixels, roadPixels, buildingPixels []ima
for range numClumpTrees { for range numClumpTrees {
for range 100 { for range 100 {
p := image.Point{X: randSrc.Intn(width), Y: randSrc.Intn(height)} p := image.Point{X: randSrc.Intn(width), Y: randSrc.Intn(height)}
if treeNoiseMap.GrayAt(p.X, p.Y).Y >= threshold && !isLake[p] && !isRoad[p] && !isBuilding[p] { if treeNoiseMap.GrayAt(p.X, p.Y).Y >= threshold && !waterMask.GetPoint(p) && !roadMask.GetPoint(p) && !buildingMask.GetPoint(p) {
initialPoints = append(initialPoints, p) initialPoints = append(initialPoints, p)
break break
} }
@@ -211,10 +220,9 @@ func GenerateTrees(img *image.RGBA, lakePixels, roadPixels, buildingPixels []ima
minRadius := minTreeSize minRadius := minTreeSize
allPoints := poissonDiscSampling(width, height, minRadius, 30, initialPoints, func(p image.Point) bool { allPoints := poissonDiscSampling(width, height, minRadius, 30, initialPoints, func(p image.Point) bool {
return treeNoiseMap.GrayAt(p.X, p.Y).Y >= threshold && !isLake[p] && !isRoad[p] && !isBuilding[p] return treeNoiseMap.GrayAt(p.X, p.Y).Y >= threshold && !waterMask.GetPoint(p) && !roadMask.GetPoint(p) && !buildingMask.GetPoint(p)
}, seed) }, seed)
var treePixels []image.Point
numGoroutines := runtime.NumCPU() numGoroutines := runtime.NumCPU()
if len(allPoints) < numGoroutines { if len(allPoints) < numGoroutines {
numGoroutines = len(allPoints) numGoroutines = len(allPoints)
@@ -251,7 +259,7 @@ func GenerateTrees(img *image.RGBA, lakePixels, roadPixels, buildingPixels []ima
for y := p.Y - int(r); y <= p.Y+int(r); y++ { for y := p.Y - int(r); y <= p.Y+int(r); y++ {
for x := p.X - int(r); x <= p.X+int(r); x++ { for x := p.X - int(r); x <= p.X+int(r); x++ {
pt := image.Point{X: x, Y: y} pt := image.Point{X: x, Y: y}
if !pt.In(img.Bounds()) || isLake[pt] || isRoad[pt] || isBuilding[pt] { if !pt.In(img.Bounds()) || waterMask.GetPoint(pt) || roadMask.GetPoint(pt) || buildingMask.GetPoint(pt) {
continue continue
} }
@@ -269,11 +277,14 @@ func GenerateTrees(img *image.RGBA, lakePixels, roadPixels, buildingPixels []ima
wg.Wait() wg.Wait()
close(results) close(results)
treeMask := NewPixelMask(width, height)
for res := range results { for res := range results {
treePixels = append(treePixels, res...) for _, p := range res {
treeMask.SetPoint(p)
}
} }
return treePixels return treeMask
} }
// poissonDiscSampling generates randomly distributed points with minimum radius separation // poissonDiscSampling generates randomly distributed points with minimum radius separation
+10 -11
View File
@@ -899,7 +899,7 @@ func clamp01(v float64) float64 {
} }
// GenerateRivers creates rivers flowing across the map from edge to edge // GenerateRivers creates rivers flowing across the map from edge to edge
func GenerateRivers(width, height, numRivers int, minWidth, maxWidth, curvyness float64, inputImage image.Image, lakes [][]image.Point, seed int64, heightmap image.Image, riverWidthVariability, riverEdgeRoughness float64) (image.Image, []image.Point) { func GenerateRivers(width, height, numRivers int, minWidth, maxWidth, curvyness float64, inputImage image.Image, lakes [][]image.Point, seed int64, heightmap image.Image, riverWidthVariability, riverEdgeRoughness float64) (image.Image, *PixelMask) {
if numRivers == 0 { if numRivers == 0 {
return inputImage, nil return inputImage, nil
} }
@@ -910,16 +910,15 @@ func GenerateRivers(width, height, numRivers int, minWidth, maxWidth, curvyness
draw.Draw(canvas, canvas.Bounds(), inputImage, image.Point{}, draw.Src) draw.Draw(canvas, canvas.Bounds(), inputImage, image.Point{}, draw.Src)
} }
var allRiverPixels []image.Point riverMask := NewPixelMask(width, height)
randSrc := rand.New(rand.NewSource(seed)) randSrc := rand.New(rand.NewSource(seed))
avgDim := float64(width+height) / 2.0 avgDim := float64(width+height) / 2.0
// Build water pixel lookup maps // Build water pixel lookup maps
isWater := make(map[image.Point]bool) isWater := BuildMaskFromLakes(width, height, lakes)
lakePixelMap := make(map[image.Point]int) lakePixelMap := make(map[image.Point]int)
for i, lake := range lakes { for i, lake := range lakes {
for _, p := range lake { for _, p := range lake {
isWater[p] = true
lakePixelMap[p] = i lakePixelMap[p] = i
} }
} }
@@ -957,7 +956,7 @@ func GenerateRivers(width, height, numRivers int, minWidth, maxWidth, curvyness
// Check for intersections with existing water // Check for intersections with existing water
for _, p := range path { for _, p := range path {
if isWater[p] { if isWater.GetPoint(p) {
if lakeIndex, isLake := lakePixelMap[p]; isLake { if lakeIndex, isLake := lakePixelMap[p]; isLake {
// End river at lake center if it intersects // End river at lake center if it intersects
lakeCenter := findCenter(lakes[lakeIndex]) lakeCenter := findCenter(lakes[lakeIndex])
@@ -976,12 +975,12 @@ func GenerateRivers(width, height, numRivers int, minWidth, maxWidth, curvyness
radius := riverWidthPx / 2.0 radius := riverWidthPx / 2.0
for _, p := range path { for _, p := range path {
drawCircle(canvas, p, radius, color.RGBA{R: 0, G: 0, B: 255, A: 255}, &allRiverPixels, isWater, heightmap, riverWidthVariability, riverEdgeRoughness) drawCircle(canvas, p, radius, color.RGBA{R: 0, G: 0, B: 255, A: 255}, riverMask, isWater, heightmap, riverWidthVariability, riverEdgeRoughness)
} }
r.Points = path r.Points = path
} }
return canvas, allRiverPixels return canvas, riverMask
} }
// bresenhamRiver draws a line between control points using Bresenham's algorithm // bresenhamRiver draws a line between control points using Bresenham's algorithm
@@ -1118,7 +1117,7 @@ func getPointOnEdge(width, height, edge int, randSrc *rand.Rand) image.Point {
} }
// drawCircle draws a circular river cross-section with sine wave edge roughening // drawCircle draws a circular river cross-section with sine wave edge roughening
func drawCircle(img *image.RGBA, center image.Point, radius float64, c color.Color, pixels *[]image.Point, isWater map[image.Point]bool, heightmap image.Image, riverWidthVariability, riverEdgeRoughness float64) { func drawCircle(img *image.RGBA, center image.Point, radius float64, c color.Color, riverMask, isWater *PixelMask, heightmap image.Image, riverWidthVariability, riverEdgeRoughness float64) {
bounds := img.Bounds() bounds := img.Bounds()
largeAmplitude := (radius * 0.5) * (riverWidthVariability / 100.0) largeAmplitude := (radius * 0.5) * (riverWidthVariability / 100.0)
@@ -1143,10 +1142,10 @@ func drawCircle(img *image.RGBA, center image.Point, radius float64, c color.Col
effectiveRadius := radius + waveOffset effectiveRadius := radius + waveOffset
if dist <= effectiveRadius { if dist <= effectiveRadius {
if !isWater[p] { if !isWater.GetPoint(p) {
img.Set(x, y, c) img.Set(x, y, c)
*pixels = append(*pixels, p) riverMask.SetPoint(p)
isWater[p] = true isWater.SetPoint(p)
} }
} }
} }