changed how building distribution works
This commit is contained in:
@@ -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` |
|
||||||
|
|||||||
+80
-21
@@ -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 {
|
||||||
p, ok := sampleRandomLandPoint(width, height, waterMask, roadMask, randSrc)
|
if a, ok := pickLeastUsedAnchor(normalRoadAnchors); ok {
|
||||||
if !ok {
|
anchor = a
|
||||||
continue
|
usedRoadAnchor = true
|
||||||
|
} else if a, ok := pickLeastUsedAnchor(anchorPoints); ok {
|
||||||
|
anchor = a
|
||||||
|
usedRoadAnchor = true
|
||||||
|
} else {
|
||||||
|
p, ok := sampleRandomLandPoint(width, height, waterMask, roadMask, randSrc)
|
||||||
|
if !ok {
|
||||||
|
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 {
|
||||||
continue // No land to place buildings on
|
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
|
||||||
|
}
|
||||||
|
} 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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user