changed how building distribution works

This commit is contained in:
grimsace
2026-04-06 09:50:05 -05:00
parent fdac5ef45a
commit 4888e10f73
3 changed files with 89 additions and 22 deletions
+1 -1
View File
@@ -71,7 +71,7 @@ All settings below are currently implemented in the UI.
| **Number of Buildings** | Requested building count before fit-cap. | `0` to `10000` | | **Number of Buildings** | Requested building count before fit-cap. | `0` to `10000` |
| **Min Building Size** | Minimum building size as % of average image dimension `((width+height)/2)`. | `0.5%` to `25%` in `0.5%` steps | | **Min Building Size** | Minimum building size as % of average image dimension `((width+height)/2)`. | `0.5%` to `25%` in `0.5%` steps |
| **Max Building Size** | Maximum building size as % of average image dimension `((width+height)/2)`. | `0.5%` to `25%` in `0.5%` steps | | **Max Building Size** | Maximum building size as % of average image dimension `((width+height)/2)`. | `0.5%` to `25%` in `0.5%` steps |
| **Building Distribution** | Placement preference from near roads toward random map-wide placement. | `0%` to `100%` | | **Building Distribution** | How far buildings may spread away from roads: `0%` stays road-adjacent, `100%` becomes fully random map-wide placement. | `0%` to `100%` |
| **Building Shape** | Primary building generation mode. | `squares`, `circles`, `rectangles`, `mixed`, `procedural` | | **Building Shape** | Primary building generation mode. | `squares`, `circles`, `rectangles`, `mixed`, `procedural` |
| **Squares / Circles / Rectangles** | Shape ratio sliders used by `mixed` and `procedural` building modes. | `0%` to `100%` each (normalized together) | | **Squares / Circles / Rectangles** | Shape ratio sliders used by `mixed` and `procedural` building modes. | `0%` to `100%` each (normalized together) |
| **Min Building Complexity** | Minimum number of shape components in procedural buildings. | `1` to `6` | | **Min Building Complexity** | Minimum number of shape components in procedural buildings. | `1` to `6` |
+74 -15
View File
@@ -170,18 +170,28 @@ func GenerateBuildings(
// Main loop for placing buildings // Main loop for placing buildings
buildingsPlaced := 0 buildingsPlaced := 0
searchTries := 100 // Number of attempts to find a spot for a building around an anchor maxPlacementAttempts := settings.NumBuildings * 3 // To prevent infinite loops
maxPlacementAttempts := settings.NumBuildings * 5 // To prevent infinite loops
minBuildingSizePx, maxBuildingSizePx := getBuildingSizeRangePixels(settings, width, height) minBuildingSizePx, maxBuildingSizePx := getBuildingSizeRangePixels(settings, width, height)
avgBuildingSizePx := (minBuildingSizePx + maxBuildingSizePx) * 0.5
anchorUsage := make(map[image.Point]int, len(anchorPoints)) anchorUsage := make(map[image.Point]int, len(anchorPoints))
normalAnchorCap := 0 normalAnchorCap := 0
exitAnchorCap := 0 exitAnchorCap := 0
distributionFactor := clamp01(settings.BuildingDistribution / 100.0)
roadPlacementBias := 1.0 - distributionFactor
roadAnchorMinOffset := clamp(avgBuildingSizePx*0.55, 2, 16)
roadAnchorMaxOffset := roadAnchorMinOffset + math.Pow(distributionFactor, 1.05)*clamp(avgBuildingSizePx*13.0, 16, 180)
roadSearchTries := 42
randomSearchTries := 18
if len(normalRoadAnchors) > 0 { if len(normalRoadAnchors) > 0 {
normalAnchorCap = max(2, int(math.Ceil((float64(settings.NumBuildings)/float64(len(normalRoadAnchors)))*1.15))) normalAnchorCap = max(2, int(math.Ceil((float64(settings.NumBuildings)/float64(len(normalRoadAnchors)))*1.15)))
} }
if len(exitRoadAnchors) > 0 { if len(exitRoadAnchors) > 0 {
exitAnchorCap = max(1, int(math.Ceil((float64(settings.NumBuildings)/float64(len(exitRoadAnchors)))*0.20))) exitAnchorCap = max(1, int(math.Ceil((float64(settings.NumBuildings)/float64(len(exitRoadAnchors)))*0.20)))
} }
if settings.NumBuildings <= 80 {
normalAnchorCap = max(normalAnchorCap, 6)
exitAnchorCap = max(exitAnchorCap, 2)
}
pickAnchorWithCapacity := func(candidates []image.Point, capLimit int) (image.Point, bool) { pickAnchorWithCapacity := func(candidates []image.Point, capLimit int) (image.Point, bool) {
if len(candidates) == 0 { if len(candidates) == 0 {
@@ -209,13 +219,28 @@ func GenerateBuildings(
return image.Point{}, false return image.Point{}, false
} }
pickLeastUsedAnchor := func(candidates []image.Point) (image.Point, bool) {
if len(candidates) == 0 {
return image.Point{}, false
}
best := candidates[randSrc.Intn(len(candidates))]
bestCount := anchorUsage[best]
for _, candidate := range candidates {
if count := anchorUsage[candidate]; count < bestCount {
best = candidate
bestCount = count
}
}
return best, true
}
for buildingsPlaced < settings.NumBuildings && maxPlacementAttempts > 0 { for buildingsPlaced < settings.NumBuildings && maxPlacementAttempts > 0 {
maxPlacementAttempts-- maxPlacementAttempts--
// Select an anchor point for the new building // Select an anchor point for the new building
var anchor image.Point var anchor image.Point
usedRoadAnchor := false usedRoadAnchor := false
if randSrc.Float64() > settings.BuildingDistribution/100.0 { if len(anchorPoints) > 0 && randSrc.Float64() < roadPlacementBias {
// Buildings should only rarely use exit-road anchors. // Buildings should only rarely use exit-road anchors.
useExitAnchor := len(exitRoadAnchors) > 0 && randSrc.Float64() < 0.02 useExitAnchor := len(exitRoadAnchors) > 0 && randSrc.Float64() < 0.02
if useExitAnchor { if useExitAnchor {
@@ -235,31 +260,72 @@ func GenerateBuildings(
} }
} }
if !usedRoadAnchor { if !usedRoadAnchor {
if a, ok := pickLeastUsedAnchor(normalRoadAnchors); ok {
anchor = a
usedRoadAnchor = true
} else if a, ok := pickLeastUsedAnchor(anchorPoints); ok {
anchor = a
usedRoadAnchor = true
} else {
p, ok := sampleRandomLandPoint(width, height, waterMask, roadMask, randSrc) p, ok := sampleRandomLandPoint(width, height, waterMask, roadMask, randSrc)
if !ok { if !ok {
continue continue
} }
anchor = p anchor = p
} }
} else { }
}
if !usedRoadAnchor {
p, ok := sampleRandomLandPoint(width, height, waterMask, roadMask, randSrc) p, ok := sampleRandomLandPoint(width, height, waterMask, roadMask, randSrc)
if !ok { if !ok {
if a, ok := pickLeastUsedAnchor(normalRoadAnchors); ok {
anchor = a
usedRoadAnchor = true
} else if a, ok := pickLeastUsedAnchor(anchorPoints); ok {
anchor = a
usedRoadAnchor = true
} else {
continue // No land to place buildings on continue // No land to place buildings on
} }
} else {
anchor = p anchor = p
} }
}
searchTries := randomSearchTries
if usedRoadAnchor {
searchTries = roadSearchTries
}
// Search for a valid building location around the anchor // Search for a valid building location around the anchor
for i := 0; i < searchTries; i++ { for i := 0; i < searchTries; i++ {
searchRadius := float64(i) * 2.0 // Search in expanding circles // Sample building shape/size first so road-adjacent placement can leave enough setback.
size := minBuildingSizePx + randSrc.Float64()*(maxBuildingSizePx-minBuildingSizePx)
shape := settings.BuildingShape
if shape == "mixed" {
shape = chooseShape(randSrc, settings.BuildingShapeRatios)
}
angle := randSrc.Float64() * 2 * math.Pi angle := randSrc.Float64() * 2 * math.Pi
dist := searchRadius * randSrc.Float64() dist := 0.0
if usedRoadAnchor {
minSetback := max(roadAnchorMinOffset, size*0.85+2.0)
progress := float64(i) / float64(max(searchTries-1, 1))
searchRadius := minSetback + progress*(roadAnchorMaxOffset-minSetback)
if searchRadius < minSetback {
searchRadius = minSetback
}
dist = minSetback + randSrc.Float64()*(searchRadius-minSetback)
} else {
searchRadius := clamp(float64(i+1)*avgBuildingSizePx*0.45, avgBuildingSizePx, avgBuildingSizePx*5.5)
dist = randSrc.Float64() * searchRadius
}
center := image.Point{ center := image.Point{
X: anchor.X + int(dist*math.Cos(angle)), X: anchor.X + int(dist*math.Cos(angle)),
Y: anchor.Y + int(dist*math.Sin(angle)), Y: anchor.Y + int(dist*math.Sin(angle)),
} }
// For fully random distribution, pick any point on the map // Non-road placement is fully random over the map.
if settings.BuildingDistribution == 100 { if !usedRoadAnchor {
center = image.Point{ center = image.Point{
X: randSrc.Intn(width), X: randSrc.Intn(width),
Y: randSrc.Intn(height), Y: randSrc.Intn(height),
@@ -271,13 +337,6 @@ func GenerateBuildings(
continue continue
} }
// Attempt to create a building at the selected center
size := minBuildingSizePx + randSrc.Float64()*(maxBuildingSizePx-minBuildingSizePx)
shape := settings.BuildingShape
if shape == "mixed" {
shape = chooseShape(randSrc, settings.BuildingShapeRatios)
}
var pixels []image.Point var pixels []image.Point
var ok bool var ok bool
+8
View File
@@ -1995,6 +1995,14 @@ func collectRoadAnchors(roads []*Road, settings *Settings, waterMask *PixelMask,
minBuildingSizePx, maxBuildingSizePx := getBuildingSizeRangePixels(settings, width, height) minBuildingSizePx, maxBuildingSizePx := getBuildingSizeRangePixels(settings, width, height)
spacing := int(math.Round(clamp((minBuildingSizePx+maxBuildingSizePx)*0.5, 8, 28))) spacing := int(math.Round(clamp((minBuildingSizePx+maxBuildingSizePx)*0.5, 8, 28)))
switch {
case settings.NumBuildings >= 2500:
spacing = int(math.Round(float64(spacing) * 0.72))
case settings.NumBuildings >= 1000:
spacing = int(math.Round(float64(spacing) * 0.80))
case settings.NumBuildings >= 300:
spacing = int(math.Round(float64(spacing) * 0.90))
}
if spacing < 6 { if spacing < 6 {
spacing = 6 spacing = 6
} }