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` |
|
||||
| **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 |
|
||||
| **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` |
|
||||
| **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` |
|
||||
|
||||
+80
-21
@@ -170,18 +170,28 @@ func GenerateBuildings(
|
||||
|
||||
// Main loop for placing buildings
|
||||
buildingsPlaced := 0
|
||||
searchTries := 100 // Number of attempts to find a spot for a building around an anchor
|
||||
maxPlacementAttempts := settings.NumBuildings * 5 // To prevent infinite loops
|
||||
maxPlacementAttempts := settings.NumBuildings * 3 // To prevent infinite loops
|
||||
minBuildingSizePx, maxBuildingSizePx := getBuildingSizeRangePixels(settings, width, height)
|
||||
avgBuildingSizePx := (minBuildingSizePx + maxBuildingSizePx) * 0.5
|
||||
anchorUsage := make(map[image.Point]int, len(anchorPoints))
|
||||
normalAnchorCap := 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 {
|
||||
normalAnchorCap = max(2, int(math.Ceil((float64(settings.NumBuildings)/float64(len(normalRoadAnchors)))*1.15)))
|
||||
}
|
||||
if len(exitRoadAnchors) > 0 {
|
||||
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) {
|
||||
if len(candidates) == 0 {
|
||||
@@ -209,13 +219,28 @@ func GenerateBuildings(
|
||||
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 {
|
||||
maxPlacementAttempts--
|
||||
|
||||
// Select an anchor point for the new building
|
||||
var anchor image.Point
|
||||
usedRoadAnchor := false
|
||||
if randSrc.Float64() > settings.BuildingDistribution/100.0 {
|
||||
if len(anchorPoints) > 0 && randSrc.Float64() < roadPlacementBias {
|
||||
// Buildings should only rarely use exit-road anchors.
|
||||
useExitAnchor := len(exitRoadAnchors) > 0 && randSrc.Float64() < 0.02
|
||||
if useExitAnchor {
|
||||
@@ -235,31 +260,72 @@ func GenerateBuildings(
|
||||
}
|
||||
}
|
||||
if !usedRoadAnchor {
|
||||
p, ok := sampleRandomLandPoint(width, height, waterMask, roadMask, randSrc)
|
||||
if !ok {
|
||||
continue
|
||||
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)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
anchor = p
|
||||
}
|
||||
anchor = p
|
||||
}
|
||||
} else {
|
||||
}
|
||||
if !usedRoadAnchor {
|
||||
p, ok := sampleRandomLandPoint(width, height, waterMask, roadMask, randSrc)
|
||||
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
|
||||
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
|
||||
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{
|
||||
X: anchor.X + int(dist*math.Cos(angle)),
|
||||
Y: anchor.Y + int(dist*math.Sin(angle)),
|
||||
}
|
||||
// For fully random distribution, pick any point on the map
|
||||
if settings.BuildingDistribution == 100 {
|
||||
// Non-road placement is fully random over the map.
|
||||
if !usedRoadAnchor {
|
||||
center = image.Point{
|
||||
X: randSrc.Intn(width),
|
||||
Y: randSrc.Intn(height),
|
||||
@@ -271,13 +337,6 @@ func GenerateBuildings(
|
||||
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 ok bool
|
||||
|
||||
|
||||
@@ -1995,6 +1995,14 @@ func collectRoadAnchors(roads []*Road, settings *Settings, waterMask *PixelMask,
|
||||
|
||||
minBuildingSizePx, maxBuildingSizePx := getBuildingSizeRangePixels(settings, width, height)
|
||||
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 {
|
||||
spacing = 6
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user