added basic wall generation
This commit is contained in:
@@ -0,0 +1,347 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"image"
|
||||||
|
"image/color"
|
||||||
|
"math"
|
||||||
|
"math/rand"
|
||||||
|
"sort"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
minWallWidthPercent = minBuildingSizePercent
|
||||||
|
maxWallWidthPercent = maxBuildingSizePercent
|
||||||
|
wallWidthPercentStep = buildingSizePercentStep
|
||||||
|
)
|
||||||
|
|
||||||
|
func clampWallWidthPercent(v float64) float64 {
|
||||||
|
if v < minWallWidthPercent {
|
||||||
|
return minWallWidthPercent
|
||||||
|
}
|
||||||
|
if v > maxWallWidthPercent {
|
||||||
|
return maxWallWidthPercent
|
||||||
|
}
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
func snapWallWidthPercent(v float64) float64 {
|
||||||
|
v = clampWallWidthPercent(v)
|
||||||
|
steps := math.Round((v - minWallWidthPercent) / wallWidthPercentStep)
|
||||||
|
return clampWallWidthPercent(minWallWidthPercent + steps*wallWidthPercentStep)
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizeWallWidthPercentRange(minPercent, maxPercent float64) (float64, float64) {
|
||||||
|
minPercent = snapWallWidthPercent(minPercent)
|
||||||
|
maxPercent = snapWallWidthPercent(maxPercent)
|
||||||
|
if minPercent > maxPercent {
|
||||||
|
minPercent, maxPercent = maxPercent, minPercent
|
||||||
|
}
|
||||||
|
return minPercent, maxPercent
|
||||||
|
}
|
||||||
|
|
||||||
|
func getWallWidthRangePixels(settings *Settings, width, height int) (float64, float64) {
|
||||||
|
minPercent, maxPercent := normalizeWallWidthPercentRange(settings.MinWallWidth, settings.MaxWallWidth)
|
||||||
|
avgDim := averageImageDimension(width, height)
|
||||||
|
if avgDim < 1 {
|
||||||
|
avgDim = 1
|
||||||
|
}
|
||||||
|
minPx := (minPercent / 100.0) * avgDim
|
||||||
|
maxPx := (maxPercent / 100.0) * avgDim
|
||||||
|
if minPx < 1 {
|
||||||
|
minPx = 1
|
||||||
|
}
|
||||||
|
if maxPx < 1 {
|
||||||
|
maxPx = 1
|
||||||
|
}
|
||||||
|
return minPx, maxPx
|
||||||
|
}
|
||||||
|
|
||||||
|
type FortificationLayout struct {
|
||||||
|
Mask *PixelMask
|
||||||
|
WallIDByPixel []int
|
||||||
|
Coverages []float64
|
||||||
|
}
|
||||||
|
|
||||||
|
func GenerateFortifications(
|
||||||
|
img *image.RGBA,
|
||||||
|
width, height int,
|
||||||
|
settings *Settings,
|
||||||
|
waterMask *PixelMask,
|
||||||
|
roadNodes []*PointOfInterest,
|
||||||
|
seed int64,
|
||||||
|
) (*FortificationLayout, [][]image.Point) {
|
||||||
|
layout := &FortificationLayout{
|
||||||
|
Mask: NewPixelMask(width, height),
|
||||||
|
WallIDByPixel: make([]int, width*height),
|
||||||
|
}
|
||||||
|
if settings.NumWalls <= 0 || settings.CityCoverage <= 0 {
|
||||||
|
return layout, nil
|
||||||
|
}
|
||||||
|
if waterMask == nil {
|
||||||
|
waterMask = NewPixelMask(width, height)
|
||||||
|
}
|
||||||
|
if img == nil {
|
||||||
|
img = image.NewRGBA(image.Rect(0, 0, width, height))
|
||||||
|
}
|
||||||
|
|
||||||
|
randSrc := rand.New(rand.NewSource(seed))
|
||||||
|
minWidthPx, maxWidthPx := getWallWidthRangePixels(settings, width, height)
|
||||||
|
wallColor := color.RGBA{R: 0, G: 0, B: 0, A: 255}
|
||||||
|
|
||||||
|
walls := make([][]image.Point, 0, settings.NumWalls)
|
||||||
|
outerCoverage := clamp(settings.CityCoverage, 1, 100)
|
||||||
|
totalWalls := max(1, settings.NumWalls)
|
||||||
|
prevCoverage := 101.0
|
||||||
|
layout.Coverages = make([]float64, 0, totalWalls)
|
||||||
|
|
||||||
|
for i := 0; i < totalWalls; i++ {
|
||||||
|
baseCoverage := outerCoverage * float64(totalWalls-i) / float64(totalWalls)
|
||||||
|
coverage := baseCoverage
|
||||||
|
if i > 0 {
|
||||||
|
coverage += randSrc.Float64()*10.0 - 5.0
|
||||||
|
}
|
||||||
|
coverage = clamp(coverage, 1, 100)
|
||||||
|
if coverage >= prevCoverage {
|
||||||
|
coverage = prevCoverage - 1
|
||||||
|
if coverage < 1 {
|
||||||
|
coverage = 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
prevCoverage = coverage
|
||||||
|
layout.Coverages = append(layout.Coverages, coverage)
|
||||||
|
|
||||||
|
nodes := estimateWallNodeCount(coverage)
|
||||||
|
wallPath := generateWallLoop(width, height, coverage, settings.WallCurvyness, nodes, randSrc, roadNodes)
|
||||||
|
if len(wallPath) < 3 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
wallWidthPx := minWidthPx
|
||||||
|
if maxWidthPx > minWidthPx {
|
||||||
|
wallWidthPx = minWidthPx + randSrc.Float64()*(maxWidthPx-minWidthPx)
|
||||||
|
}
|
||||||
|
wallWidth := int(math.Round(wallWidthPx))
|
||||||
|
if wallWidth < 1 {
|
||||||
|
wallWidth = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
pixels := drawWallLoopWithWaterGaps(img, wallPath, wallColor, wallWidth, layout.Mask, waterMask, layout.WallIDByPixel, i+1)
|
||||||
|
if len(pixels) > 0 {
|
||||||
|
walls = append(walls, pixels)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return layout, walls
|
||||||
|
}
|
||||||
|
|
||||||
|
func estimateWallNodeCount(coverage float64) int {
|
||||||
|
n := int(math.Round(20 + coverage*0.7))
|
||||||
|
if n < 20 {
|
||||||
|
n = 20
|
||||||
|
}
|
||||||
|
if n > 96 {
|
||||||
|
n = 96
|
||||||
|
}
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
|
||||||
|
func generateWallLoop(width, height int, coverage, curvyness float64, nodes int, randSrc *rand.Rand, roadNodes []*PointOfInterest) []image.Point {
|
||||||
|
if width <= 0 || height <= 0 || nodes < 3 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
centerX, centerY, baseRadiusX, baseRadiusY := wallEllipseFromRoadNodes(width, height, coverage, roadNodes)
|
||||||
|
curveScale := clamp(curvyness, 0, 100) / 100.0
|
||||||
|
warpAmp := 0.20 * curveScale
|
||||||
|
phaseA := randSrc.Float64() * 2 * math.Pi
|
||||||
|
phaseB := randSrc.Float64() * 2 * math.Pi
|
||||||
|
|
||||||
|
out := make([]image.Point, 0, nodes+1)
|
||||||
|
for i := 0; i < nodes; i++ {
|
||||||
|
t := (2 * math.Pi * float64(i)) / float64(nodes)
|
||||||
|
warp := 1.0 + warpAmp*(0.6*math.Sin(3*t+phaseA)+0.4*math.Sin(5*t+phaseB))
|
||||||
|
if warp < 0.7 {
|
||||||
|
warp = 0.7
|
||||||
|
}
|
||||||
|
rx := baseRadiusX * warp
|
||||||
|
ry := baseRadiusY * warp
|
||||||
|
x := int(math.Round(centerX + rx*math.Cos(t)))
|
||||||
|
y := int(math.Round(centerY + ry*math.Sin(t)))
|
||||||
|
if x < 0 {
|
||||||
|
x = 0
|
||||||
|
}
|
||||||
|
if x >= width {
|
||||||
|
x = width - 1
|
||||||
|
}
|
||||||
|
if y < 0 {
|
||||||
|
y = 0
|
||||||
|
}
|
||||||
|
if y >= height {
|
||||||
|
y = height - 1
|
||||||
|
}
|
||||||
|
out = append(out, image.Point{X: x, Y: y})
|
||||||
|
}
|
||||||
|
if len(out) > 0 {
|
||||||
|
out = append(out, out[0])
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func wallEllipseFromRoadNodes(width, height int, coverage float64, roadNodes []*PointOfInterest) (centerX, centerY, radiusX, radiusY float64) {
|
||||||
|
centerX = float64(width-1) * 0.5
|
||||||
|
centerY = float64(height-1) * 0.5
|
||||||
|
coverageRadius := math.Sqrt(clamp(coverage, 1, 100) / 100.0)
|
||||||
|
radiusX = centerX * coverageRadius
|
||||||
|
radiusY = centerY * coverageRadius
|
||||||
|
|
||||||
|
if len(roadNodes) == 0 {
|
||||||
|
return centerX, centerY, radiusX, radiusY
|
||||||
|
}
|
||||||
|
|
||||||
|
sumX, sumY := 0.0, 0.0
|
||||||
|
for _, n := range roadNodes {
|
||||||
|
sumX += float64(n.X)
|
||||||
|
sumY += float64(n.Y)
|
||||||
|
}
|
||||||
|
centerX = sumX / float64(len(roadNodes))
|
||||||
|
centerY = sumY / float64(len(roadNodes))
|
||||||
|
|
||||||
|
dists := make([]float64, 0, len(roadNodes))
|
||||||
|
var sx, sy float64
|
||||||
|
for _, n := range roadNodes {
|
||||||
|
dx := float64(n.X) - centerX
|
||||||
|
dy := float64(n.Y) - centerY
|
||||||
|
dists = append(dists, math.Hypot(dx, dy))
|
||||||
|
sx += dx * dx
|
||||||
|
sy += dy * dy
|
||||||
|
}
|
||||||
|
sort.Float64s(dists)
|
||||||
|
q := clamp(coverage, 1, 100) / 100.0
|
||||||
|
idx := int(math.Ceil(q*float64(len(dists)))) - 1
|
||||||
|
if idx < 0 {
|
||||||
|
idx = 0
|
||||||
|
}
|
||||||
|
if idx >= len(dists) {
|
||||||
|
idx = len(dists) - 1
|
||||||
|
}
|
||||||
|
baseRadius := dists[idx]
|
||||||
|
if baseRadius < 10 {
|
||||||
|
baseRadius = 10
|
||||||
|
}
|
||||||
|
|
||||||
|
stdX := math.Sqrt(sx / float64(len(roadNodes)))
|
||||||
|
stdY := math.Sqrt(sy / float64(len(roadNodes)))
|
||||||
|
aspect := 1.0
|
||||||
|
if stdY > 0.001 {
|
||||||
|
aspect = stdX / stdY
|
||||||
|
}
|
||||||
|
aspect = clamp(aspect, 0.65, 1.55)
|
||||||
|
radiusX = baseRadius * aspect
|
||||||
|
radiusY = baseRadius / aspect
|
||||||
|
|
||||||
|
maxRadiusX := math.Max(5, math.Min(centerX, float64(width-1)-centerX))
|
||||||
|
maxRadiusY := math.Max(5, math.Min(centerY, float64(height-1)-centerY))
|
||||||
|
radiusX = clamp(radiusX, 5, maxRadiusX)
|
||||||
|
radiusY = clamp(radiusY, 5, maxRadiusY)
|
||||||
|
|
||||||
|
return centerX, centerY, radiusX, radiusY
|
||||||
|
}
|
||||||
|
|
||||||
|
func drawWallLoopWithWaterGaps(
|
||||||
|
img *image.RGBA,
|
||||||
|
loop []image.Point,
|
||||||
|
col color.RGBA,
|
||||||
|
width int,
|
||||||
|
wallMask *PixelMask,
|
||||||
|
waterMask *PixelMask,
|
||||||
|
wallIDByPixel []int,
|
||||||
|
wallID int,
|
||||||
|
) []image.Point {
|
||||||
|
if len(loop) < 2 || wallMask == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
seen := make(map[int]bool)
|
||||||
|
pixels := make([]image.Point, 0, len(loop)*8)
|
||||||
|
radius := max(1, width/2)
|
||||||
|
|
||||||
|
for i := 0; i < len(loop)-1; i++ {
|
||||||
|
a := loop[i]
|
||||||
|
b := loop[i+1]
|
||||||
|
drawSegmentSelective(a.X, a.Y, b.X, b.Y, func(x, y int) {
|
||||||
|
if !wallMask.InBounds(x, y) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if waterMask != nil && waterMask.GetXY(x, y) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for dy := -radius; dy <= radius; dy++ {
|
||||||
|
yy := y + dy
|
||||||
|
if yy < 0 || yy >= wallMask.Height {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for dx := -radius; dx <= radius; dx++ {
|
||||||
|
if dx*dx+dy*dy > radius*radius {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
xx := x + dx
|
||||||
|
if xx < 0 || xx >= wallMask.Width {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if waterMask != nil && waterMask.GetXY(xx, yy) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
wallMask.SetXY(xx, yy)
|
||||||
|
if len(wallIDByPixel) == wallMask.Width*wallMask.Height {
|
||||||
|
wallIDByPixel[yy*wallMask.Width+xx] = wallID
|
||||||
|
}
|
||||||
|
img.Set(xx, yy, col)
|
||||||
|
idx := yy*wallMask.Width + xx
|
||||||
|
if !seen[idx] {
|
||||||
|
seen[idx] = true
|
||||||
|
pixels = append(pixels, image.Point{X: xx, Y: yy})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return pixels
|
||||||
|
}
|
||||||
|
|
||||||
|
func drawSegmentSelective(x0, y0, x1, y1 int, plot func(x, y int)) {
|
||||||
|
dx := abs(x1 - x0)
|
||||||
|
dy := abs(y1 - y0)
|
||||||
|
sx := -1
|
||||||
|
if x0 < x1 {
|
||||||
|
sx = 1
|
||||||
|
}
|
||||||
|
sy := -1
|
||||||
|
if y0 < y1 {
|
||||||
|
sy = 1
|
||||||
|
}
|
||||||
|
err := dx - dy
|
||||||
|
for {
|
||||||
|
plot(x0, y0)
|
||||||
|
if x0 == x1 && y0 == y1 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
e2 := 2 * err
|
||||||
|
if e2 > -dy {
|
||||||
|
err -= dy
|
||||||
|
x0 += sx
|
||||||
|
}
|
||||||
|
if e2 < dx {
|
||||||
|
err += dx
|
||||||
|
y0 += sy
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func cloneMask(src *PixelMask) *PixelMask {
|
||||||
|
if src == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
dst := NewPixelMask(src.Width, src.Height)
|
||||||
|
copy(dst.Data, src.Data)
|
||||||
|
return dst
|
||||||
|
}
|
||||||
@@ -95,6 +95,7 @@ func main() {
|
|||||||
var roadMask *PixelMask
|
var roadMask *PixelMask
|
||||||
var bridgeMask *PixelMask
|
var bridgeMask *PixelMask
|
||||||
var exitRoadMask *PixelMask
|
var exitRoadMask *PixelMask
|
||||||
|
var wallMask *PixelMask
|
||||||
|
|
||||||
// Set up application configuration directory
|
// Set up application configuration directory
|
||||||
configDir, err := os.UserConfigDir()
|
configDir, err := os.UserConfigDir()
|
||||||
@@ -231,7 +232,55 @@ func main() {
|
|||||||
if riverMask != nil {
|
if riverMask != nil {
|
||||||
waterMask.Merge(riverMask)
|
waterMask.Merge(riverMask)
|
||||||
}
|
}
|
||||||
// Step 4: Generating Roads
|
// Step 4: Preparing Road Nodes
|
||||||
|
var roadNodes []*PointOfInterest
|
||||||
|
var roadTarget int
|
||||||
|
var edgeToEdgeOnly bool
|
||||||
|
if out, ok := runWithTimeout(generationStepTimeout, func() struct {
|
||||||
|
pois []*PointOfInterest
|
||||||
|
target int
|
||||||
|
edgeToEdge bool
|
||||||
|
} {
|
||||||
|
pois, target, edgeToEdge := PrepareRoadNodes(settings.Width, settings.Height, settings, waterMask, seedProvider.Next())
|
||||||
|
return struct {
|
||||||
|
pois []*PointOfInterest
|
||||||
|
target int
|
||||||
|
edgeToEdge bool
|
||||||
|
}{pois: pois, target: target, edgeToEdge: edgeToEdge}
|
||||||
|
}); ok {
|
||||||
|
roadNodes, roadTarget, edgeToEdgeOnly = out.pois, out.target, out.edgeToEdge
|
||||||
|
} else {
|
||||||
|
log.Println("PrepareRoadNodes timed out after 1 minute; continuing.")
|
||||||
|
roadNodes = nil
|
||||||
|
roadTarget = 0
|
||||||
|
edgeToEdgeOnly = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 5: Generating Fortifications
|
||||||
|
var wallLayout *FortificationLayout
|
||||||
|
fortBase := cloneToRGBA(finalImage, settings.Width, settings.Height)
|
||||||
|
if out, ok := runWithTimeout(generationStepTimeout, func() struct {
|
||||||
|
layout *FortificationLayout
|
||||||
|
} {
|
||||||
|
layout, _ := GenerateFortifications(fortBase, settings.Width, settings.Height, settings, waterMask, roadNodes, seedProvider.Next())
|
||||||
|
return struct {
|
||||||
|
layout *FortificationLayout
|
||||||
|
}{layout: layout}
|
||||||
|
}); ok {
|
||||||
|
wallLayout = out.layout
|
||||||
|
if wallLayout != nil {
|
||||||
|
wallMask = wallLayout.Mask
|
||||||
|
} else {
|
||||||
|
wallMask = NewPixelMask(settings.Width, settings.Height)
|
||||||
|
}
|
||||||
|
finalImage = fortBase
|
||||||
|
} else {
|
||||||
|
log.Println("GenerateFortifications timed out after 1 minute; continuing.")
|
||||||
|
wallLayout = &FortificationLayout{Mask: NewPixelMask(settings.Width, settings.Height)}
|
||||||
|
wallMask = wallLayout.Mask
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 6: Generating Roads
|
||||||
var roadAnchors []image.Point
|
var roadAnchors []image.Point
|
||||||
roadBase := cloneToRGBA(finalImage, settings.Width, settings.Height)
|
roadBase := cloneToRGBA(finalImage, settings.Width, settings.Height)
|
||||||
if out, ok := runWithTimeout(generationStepTimeout, func() struct {
|
if out, ok := runWithTimeout(generationStepTimeout, func() struct {
|
||||||
@@ -240,7 +289,7 @@ func main() {
|
|||||||
ex *PixelMask
|
ex *PixelMask
|
||||||
anc []image.Point
|
anc []image.Point
|
||||||
} {
|
} {
|
||||||
rd, br, ex, anc := GenerateRoads(roadBase, settings.Width, settings.Height, settings, waterMask, seedProvider.Next())
|
rd, br, ex, anc := GenerateRoadsWithPOIs(roadBase, settings.Width, settings.Height, settings, waterMask, wallLayout, roadNodes, roadTarget, edgeToEdgeOnly, seedProvider.Next())
|
||||||
return struct {
|
return struct {
|
||||||
rd *PixelMask
|
rd *PixelMask
|
||||||
br *PixelMask
|
br *PixelMask
|
||||||
@@ -258,13 +307,21 @@ func main() {
|
|||||||
roadAnchors = nil
|
roadAnchors = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Step 5: Generating Buildings
|
placementMask := cloneMask(roadMask)
|
||||||
|
if placementMask == nil {
|
||||||
|
placementMask = NewPixelMask(settings.Width, settings.Height)
|
||||||
|
}
|
||||||
|
if wallMask != nil {
|
||||||
|
placementMask.Merge(wallMask)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 7: Generating Buildings
|
||||||
buildingBase := cloneToRGBA(finalImage, settings.Width, settings.Height)
|
buildingBase := cloneToRGBA(finalImage, settings.Width, settings.Height)
|
||||||
if out, ok := runWithTimeout(generationStepTimeout, func() struct {
|
if out, ok := runWithTimeout(generationStepTimeout, func() struct {
|
||||||
blds [][]image.Point
|
blds [][]image.Point
|
||||||
bmsk *PixelMask
|
bmsk *PixelMask
|
||||||
} {
|
} {
|
||||||
blds, bmsk := GenerateBuildings(buildingBase, settings.Width, settings.Height, settings, roadAnchors, waterMask, roadMask, exitRoadMask, seedProvider.Next())
|
blds, bmsk := GenerateBuildings(buildingBase, settings.Width, settings.Height, settings, roadAnchors, waterMask, placementMask, exitRoadMask, seedProvider.Next())
|
||||||
return struct {
|
return struct {
|
||||||
blds [][]image.Point
|
blds [][]image.Point
|
||||||
bmsk *PixelMask
|
bmsk *PixelMask
|
||||||
@@ -278,10 +335,10 @@ func main() {
|
|||||||
buildingMask = NewPixelMask(settings.Width, settings.Height)
|
buildingMask = NewPixelMask(settings.Width, settings.Height)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Step 6: Generating Trees
|
// Step 8: Generating Trees
|
||||||
treeBase := cloneToRGBA(finalImage, settings.Width, settings.Height)
|
treeBase := cloneToRGBA(finalImage, settings.Width, settings.Height)
|
||||||
if out, ok := runWithTimeout(generationStepTimeout, func() *PixelMask {
|
if out, ok := runWithTimeout(generationStepTimeout, func() *PixelMask {
|
||||||
return GenerateTrees(treeBase, waterMask, roadMask, buildingMask, settings.MinTreeSize, settings.MaxTreeSize, settings.TreeCoverage, settings.TreeClumpiness, seedProvider.Next())
|
return GenerateTrees(treeBase, waterMask, placementMask, buildingMask, settings.MinTreeSize, settings.MaxTreeSize, settings.TreeCoverage, settings.TreeClumpiness, seedProvider.Next())
|
||||||
}); ok {
|
}); ok {
|
||||||
treeMask = out
|
treeMask = out
|
||||||
finalImage = treeBase
|
finalImage = treeBase
|
||||||
@@ -296,8 +353,8 @@ func main() {
|
|||||||
// 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 and Wall Areas
|
||||||
flattenedHeightmap := FlattenRoadAreas(flattenedBuildingHeightmap, roadMask)
|
flattenedHeightmap := FlattenRoadAreas(flattenedBuildingHeightmap, placementMask)
|
||||||
|
|
||||||
// Step 10: Applying Roughness
|
// Step 10: Applying Roughness
|
||||||
compositeImg := ApplyRoughness(flattenedHeightmap, settings.Roughness)
|
compositeImg := ApplyRoughness(flattenedHeightmap, settings.Roughness)
|
||||||
@@ -613,6 +670,66 @@ func main() {
|
|||||||
settings.MinRoadAngle = val
|
settings.MinRoadAngle = val
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
minWallWidthSlider := newNumericInputSliderWithStep(minWallWidthPercent, maxWallWidthPercent, settings.MinWallWidth, wallWidthPercentStep, "%.1f%%", "Min Wall Width")
|
||||||
|
minWallWidthSlider.entry.OnChanged = func(s string) {
|
||||||
|
minWallWidthSlider.validate(s, func(hasError bool) {
|
||||||
|
errorStates["minWallWidth"] = hasError
|
||||||
|
updateGenerateBtnState()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
minWallWidthSlider.value.AddListener(binding.NewDataListener(func() {
|
||||||
|
val, _ := minWallWidthSlider.value.Get()
|
||||||
|
settings.MinWallWidth = val
|
||||||
|
}))
|
||||||
|
|
||||||
|
maxWallWidthSlider := newNumericInputSliderWithStep(minWallWidthPercent, maxWallWidthPercent, settings.MaxWallWidth, wallWidthPercentStep, "%.1f%%", "Max Wall Width")
|
||||||
|
maxWallWidthSlider.entry.OnChanged = func(s string) {
|
||||||
|
maxWallWidthSlider.validate(s, func(hasError bool) {
|
||||||
|
errorStates["maxWallWidth"] = hasError
|
||||||
|
updateGenerateBtnState()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
maxWallWidthSlider.value.AddListener(binding.NewDataListener(func() {
|
||||||
|
val, _ := maxWallWidthSlider.value.Get()
|
||||||
|
settings.MaxWallWidth = val
|
||||||
|
}))
|
||||||
|
|
||||||
|
numWallsSlider := newNumericInputSlider(1, 5, float64(settings.NumWalls), "%.0f", "Number of Walls")
|
||||||
|
numWallsSlider.entry.OnChanged = func(s string) {
|
||||||
|
numWallsSlider.validate(s, func(hasError bool) {
|
||||||
|
errorStates["numWalls"] = hasError
|
||||||
|
updateGenerateBtnState()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
numWallsSlider.value.AddListener(binding.NewDataListener(func() {
|
||||||
|
val, _ := numWallsSlider.value.Get()
|
||||||
|
settings.NumWalls = int(val)
|
||||||
|
}))
|
||||||
|
|
||||||
|
cityCoverageSlider := newNumericInputSlider(1, 100, settings.CityCoverage, "%.0f%%", "City Coverage")
|
||||||
|
cityCoverageSlider.entry.OnChanged = func(s string) {
|
||||||
|
cityCoverageSlider.validate(s, func(hasError bool) {
|
||||||
|
errorStates["cityCoverage"] = hasError
|
||||||
|
updateGenerateBtnState()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
cityCoverageSlider.value.AddListener(binding.NewDataListener(func() {
|
||||||
|
val, _ := cityCoverageSlider.value.Get()
|
||||||
|
settings.CityCoverage = val
|
||||||
|
}))
|
||||||
|
|
||||||
|
wallCurvynessSlider := newNumericInputSlider(0, 100, settings.WallCurvyness, "%.0f%%", "Wall Curvyness")
|
||||||
|
wallCurvynessSlider.entry.OnChanged = func(s string) {
|
||||||
|
wallCurvynessSlider.validate(s, func(hasError bool) {
|
||||||
|
errorStates["wallCurvyness"] = hasError
|
||||||
|
updateGenerateBtnState()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
wallCurvynessSlider.value.AddListener(binding.NewDataListener(func() {
|
||||||
|
val, _ := wallCurvynessSlider.value.Get()
|
||||||
|
settings.WallCurvyness = val
|
||||||
|
}))
|
||||||
|
|
||||||
// Create UI elements for error display and action buttons
|
// Create UI elements for error display and action buttons
|
||||||
errorLabel := widget.NewLabel("")
|
errorLabel := widget.NewLabel("")
|
||||||
errorLabel.Wrapping = fyne.TextWrapWord
|
errorLabel.Wrapping = fyne.TextWrapWord
|
||||||
@@ -631,7 +748,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, riverMask, treeMask, roadMask, bridgeMask, buildingMask)
|
showMasksSaveDialog(w, canvasImg.Image, heightmapImg.Image, bumpmapImg.Image, settings, lakes, riverMask, treeMask, roadMask, bridgeMask, wallMask, buildingMask)
|
||||||
})
|
})
|
||||||
// Main generation button and logic
|
// Main generation button and logic
|
||||||
generateBtn = widget.NewButton("Generate", func() {
|
generateBtn = widget.NewButton("Generate", func() {
|
||||||
@@ -660,16 +777,16 @@ func main() {
|
|||||||
}
|
}
|
||||||
// Step 1: Generating Heightmap
|
// Step 1: Generating Heightmap
|
||||||
fyne.Do(func() {
|
fyne.Do(func() {
|
||||||
progressLabel.SetText("Step 1 of 11: Generating Heightmap")
|
progressLabel.SetText("Step 1 of 13: Generating Heightmap")
|
||||||
progressBar.SetValue(1.0 / 11.0)
|
progressBar.SetValue(1.0 / 13.0)
|
||||||
})
|
})
|
||||||
noiseImg := GenerateHeightmap(settings.Width, settings.Height, int(settings.Detail), 100.0, seedProvider.Next())
|
noiseImg := GenerateHeightmap(settings.Width, settings.Height, int(settings.Detail), 100.0, seedProvider.Next())
|
||||||
|
|
||||||
// Step 2: Generating Lakes
|
// Step 2: Generating Lakes
|
||||||
|
|
||||||
fyne.Do(func() {
|
fyne.Do(func() {
|
||||||
progressLabel.SetText("Step 2 of 11: Generating Lakes")
|
progressLabel.SetText("Step 2 of 13: Generating Lakes")
|
||||||
progressBar.SetValue(2.0 / 11.0)
|
progressBar.SetValue(2.0 / 13.0)
|
||||||
})
|
})
|
||||||
var lakeImage image.Image = image.NewRGBA(image.Rect(0, 0, settings.Width, settings.Height))
|
var lakeImage image.Image = image.NewRGBA(image.Rect(0, 0, settings.Width, settings.Height))
|
||||||
if out, ok := runWithTimeout(generationStepTimeout, func() struct {
|
if out, ok := runWithTimeout(generationStepTimeout, func() struct {
|
||||||
@@ -693,8 +810,8 @@ func main() {
|
|||||||
// Step 3: Generating Rivers
|
// Step 3: Generating Rivers
|
||||||
|
|
||||||
fyne.Do(func() {
|
fyne.Do(func() {
|
||||||
progressLabel.SetText("Step 3 of 11: Generating Rivers")
|
progressLabel.SetText("Step 3 of 13: Generating Rivers")
|
||||||
progressBar.SetValue(3.0 / 11.0)
|
progressBar.SetValue(3.0 / 13.0)
|
||||||
})
|
})
|
||||||
riverBase := cloneToRGBA(lakeImage, settings.Width, settings.Height)
|
riverBase := cloneToRGBA(lakeImage, settings.Width, settings.Height)
|
||||||
finalImage := riverBase
|
finalImage := riverBase
|
||||||
@@ -720,11 +837,71 @@ func main() {
|
|||||||
waterMask.Merge(riverMask)
|
waterMask.Merge(riverMask)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Step 4: Generating Roads
|
// Step 4: Preparing Road Nodes
|
||||||
|
|
||||||
fyne.Do(func() {
|
fyne.Do(func() {
|
||||||
progressLabel.SetText("Step 4 of 11: Generating Roads")
|
progressLabel.SetText("Step 4 of 13: Preparing Road Nodes")
|
||||||
progressBar.SetValue(4.0 / 11.0)
|
progressBar.SetValue(4.0 / 13.0)
|
||||||
|
})
|
||||||
|
var roadNodes []*PointOfInterest
|
||||||
|
var roadTarget int
|
||||||
|
var edgeToEdgeOnly bool
|
||||||
|
if out, ok := runWithTimeout(generationStepTimeout, func() struct {
|
||||||
|
pois []*PointOfInterest
|
||||||
|
target int
|
||||||
|
edgeToEdge bool
|
||||||
|
} {
|
||||||
|
pois, target, edgeToEdge := PrepareRoadNodes(settings.Width, settings.Height, settings, waterMask, seedProvider.Next())
|
||||||
|
return struct {
|
||||||
|
pois []*PointOfInterest
|
||||||
|
target int
|
||||||
|
edgeToEdge bool
|
||||||
|
}{pois: pois, target: target, edgeToEdge: edgeToEdge}
|
||||||
|
}); ok {
|
||||||
|
roadNodes, roadTarget, edgeToEdgeOnly = out.pois, out.target, out.edgeToEdge
|
||||||
|
} else {
|
||||||
|
log.Println("PrepareRoadNodes timed out after 1 minute; continuing.")
|
||||||
|
addTimeout("Road Nodes")
|
||||||
|
roadNodes = nil
|
||||||
|
roadTarget = 0
|
||||||
|
edgeToEdgeOnly = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 5: Generating Fortifications
|
||||||
|
|
||||||
|
fyne.Do(func() {
|
||||||
|
progressLabel.SetText("Step 5 of 13: Generating Fortifications")
|
||||||
|
progressBar.SetValue(5.0 / 13.0)
|
||||||
|
})
|
||||||
|
var wallLayout *FortificationLayout
|
||||||
|
fortBase := cloneToRGBA(finalImage, settings.Width, settings.Height)
|
||||||
|
if out, ok := runWithTimeout(generationStepTimeout, func() struct {
|
||||||
|
layout *FortificationLayout
|
||||||
|
} {
|
||||||
|
layout, _ := GenerateFortifications(fortBase, settings.Width, settings.Height, settings, waterMask, roadNodes, seedProvider.Next())
|
||||||
|
return struct {
|
||||||
|
layout *FortificationLayout
|
||||||
|
}{layout: layout}
|
||||||
|
}); ok {
|
||||||
|
wallLayout = out.layout
|
||||||
|
if wallLayout != nil {
|
||||||
|
wallMask = wallLayout.Mask
|
||||||
|
} else {
|
||||||
|
wallMask = NewPixelMask(settings.Width, settings.Height)
|
||||||
|
}
|
||||||
|
finalImage = fortBase
|
||||||
|
} else {
|
||||||
|
log.Println("GenerateFortifications timed out after 1 minute; continuing.")
|
||||||
|
addTimeout("Fortifications")
|
||||||
|
wallLayout = &FortificationLayout{Mask: NewPixelMask(settings.Width, settings.Height)}
|
||||||
|
wallMask = wallLayout.Mask
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 6: Generating Roads
|
||||||
|
|
||||||
|
fyne.Do(func() {
|
||||||
|
progressLabel.SetText("Step 6 of 13: Generating Roads")
|
||||||
|
progressBar.SetValue(6.0 / 13.0)
|
||||||
})
|
})
|
||||||
var roadAnchors []image.Point
|
var roadAnchors []image.Point
|
||||||
roadBase := cloneToRGBA(finalImage, settings.Width, settings.Height)
|
roadBase := cloneToRGBA(finalImage, settings.Width, settings.Height)
|
||||||
@@ -734,7 +911,7 @@ func main() {
|
|||||||
ex *PixelMask
|
ex *PixelMask
|
||||||
anc []image.Point
|
anc []image.Point
|
||||||
} {
|
} {
|
||||||
rd, br, ex, anc := GenerateRoads(roadBase, settings.Width, settings.Height, settings, waterMask, seedProvider.Next())
|
rd, br, ex, anc := GenerateRoadsWithPOIs(roadBase, settings.Width, settings.Height, settings, waterMask, wallLayout, roadNodes, roadTarget, edgeToEdgeOnly, seedProvider.Next())
|
||||||
return struct {
|
return struct {
|
||||||
rd *PixelMask
|
rd *PixelMask
|
||||||
br *PixelMask
|
br *PixelMask
|
||||||
@@ -752,19 +929,26 @@ func main() {
|
|||||||
exitRoadMask = NewPixelMask(settings.Width, settings.Height)
|
exitRoadMask = NewPixelMask(settings.Width, settings.Height)
|
||||||
roadAnchors = nil
|
roadAnchors = nil
|
||||||
}
|
}
|
||||||
|
placementMask := cloneMask(roadMask)
|
||||||
|
if placementMask == nil {
|
||||||
|
placementMask = NewPixelMask(settings.Width, settings.Height)
|
||||||
|
}
|
||||||
|
if wallMask != nil {
|
||||||
|
placementMask.Merge(wallMask)
|
||||||
|
}
|
||||||
|
|
||||||
// Step 5: Generating Buildings
|
// Step 7: Generating Buildings
|
||||||
|
|
||||||
fyne.Do(func() {
|
fyne.Do(func() {
|
||||||
progressLabel.SetText("Step 5 of 11: Generating Buildings")
|
progressLabel.SetText("Step 7 of 13: Generating Buildings")
|
||||||
progressBar.SetValue(5.0 / 11.0)
|
progressBar.SetValue(7.0 / 13.0)
|
||||||
})
|
})
|
||||||
buildingBase := cloneToRGBA(finalImage, settings.Width, settings.Height)
|
buildingBase := cloneToRGBA(finalImage, settings.Width, settings.Height)
|
||||||
if out, ok := runWithTimeout(generationStepTimeout, func() struct {
|
if out, ok := runWithTimeout(generationStepTimeout, func() struct {
|
||||||
blds [][]image.Point
|
blds [][]image.Point
|
||||||
bmsk *PixelMask
|
bmsk *PixelMask
|
||||||
} {
|
} {
|
||||||
blds, bmsk := GenerateBuildings(buildingBase, settings.Width, settings.Height, settings, roadAnchors, waterMask, roadMask, exitRoadMask, seedProvider.Next())
|
blds, bmsk := GenerateBuildings(buildingBase, settings.Width, settings.Height, settings, roadAnchors, waterMask, placementMask, exitRoadMask, seedProvider.Next())
|
||||||
return struct {
|
return struct {
|
||||||
blds [][]image.Point
|
blds [][]image.Point
|
||||||
bmsk *PixelMask
|
bmsk *PixelMask
|
||||||
@@ -779,45 +963,45 @@ func main() {
|
|||||||
buildingMask = NewPixelMask(settings.Width, settings.Height)
|
buildingMask = NewPixelMask(settings.Width, settings.Height)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Step 6: Darkening Water Areas
|
// Step 8: Darkening Water Areas
|
||||||
|
|
||||||
fyne.Do(func() {
|
fyne.Do(func() {
|
||||||
progressLabel.SetText("Step 6 of 11: Darkening Water Areas")
|
progressLabel.SetText("Step 8 of 13: Darkening Water Areas")
|
||||||
progressBar.SetValue(6.0 / 11.0)
|
progressBar.SetValue(8.0 / 13.0)
|
||||||
})
|
})
|
||||||
darkenedHeightmap := DarkenLakeAreas(noiseImg, waterMask)
|
darkenedHeightmap := DarkenLakeAreas(noiseImg, waterMask)
|
||||||
|
|
||||||
// Step 7: Flattening Building Areas
|
// Step 9: Flattening Building Areas
|
||||||
|
|
||||||
fyne.Do(func() {
|
fyne.Do(func() {
|
||||||
progressLabel.SetText("Step 7 of 11: Flattening Building Areas")
|
progressLabel.SetText("Step 9 of 13: Flattening Building Areas")
|
||||||
progressBar.SetValue(7.0 / 11.0)
|
progressBar.SetValue(9.0 / 13.0)
|
||||||
})
|
})
|
||||||
flattenedBuildingHeightmap := FlattenBuildingAreas(darkenedHeightmap.(*image.RGBA), buildings, settings.Width, settings.Height)
|
flattenedBuildingHeightmap := FlattenBuildingAreas(darkenedHeightmap.(*image.RGBA), buildings, settings.Width, settings.Height)
|
||||||
|
|
||||||
fyne.Do(func() {
|
fyne.Do(func() {
|
||||||
progressLabel.SetText("Step 8 of 11: Flattening Road Areas")
|
progressLabel.SetText("Step 10 of 13: Flattening Road and Wall Areas")
|
||||||
progressBar.SetValue(8.0 / 11.0)
|
progressBar.SetValue(10.0 / 13.0)
|
||||||
})
|
})
|
||||||
flattenedHeightmap := FlattenRoadAreas(flattenedBuildingHeightmap, roadMask)
|
flattenedHeightmap := FlattenRoadAreas(flattenedBuildingHeightmap, placementMask)
|
||||||
|
|
||||||
// Step 8: Applying Roughness
|
// Step 11: Applying Roughness
|
||||||
|
|
||||||
fyne.Do(func() {
|
fyne.Do(func() {
|
||||||
progressLabel.SetText("Step 9 of 11: Applying Roughness")
|
progressLabel.SetText("Step 11 of 13: Applying Roughness")
|
||||||
progressBar.SetValue(9.0 / 11.0)
|
progressBar.SetValue(11.0 / 13.0)
|
||||||
})
|
})
|
||||||
compositeImg := ApplyRoughness(flattenedHeightmap, settings.Roughness)
|
compositeImg := ApplyRoughness(flattenedHeightmap, settings.Roughness)
|
||||||
|
|
||||||
// Step 9: Generating Trees
|
// Step 12: Generating Trees
|
||||||
|
|
||||||
fyne.Do(func() {
|
fyne.Do(func() {
|
||||||
progressLabel.SetText("Step 10 of 11: Generating Trees")
|
progressLabel.SetText("Step 12 of 13: Generating Trees")
|
||||||
progressBar.SetValue(10.0 / 11.0)
|
progressBar.SetValue(12.0 / 13.0)
|
||||||
})
|
})
|
||||||
treeBase := cloneToRGBA(finalImage, settings.Width, settings.Height)
|
treeBase := cloneToRGBA(finalImage, settings.Width, settings.Height)
|
||||||
if out, ok := runWithTimeout(generationStepTimeout, func() *PixelMask {
|
if out, ok := runWithTimeout(generationStepTimeout, func() *PixelMask {
|
||||||
return GenerateTrees(treeBase, waterMask, roadMask, buildingMask, settings.MinTreeSize, settings.MaxTreeSize, settings.TreeCoverage, settings.TreeClumpiness, seedProvider.Next())
|
return GenerateTrees(treeBase, waterMask, placementMask, buildingMask, settings.MinTreeSize, settings.MaxTreeSize, settings.TreeCoverage, settings.TreeClumpiness, seedProvider.Next())
|
||||||
}); ok {
|
}); ok {
|
||||||
treeMask = out
|
treeMask = out
|
||||||
finalImage = treeBase
|
finalImage = treeBase
|
||||||
@@ -827,10 +1011,10 @@ func main() {
|
|||||||
treeMask = NewPixelMask(settings.Width, settings.Height)
|
treeMask = NewPixelMask(settings.Width, settings.Height)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Step 10: Generating Bump Map
|
// Step 13: Generating Bump Map
|
||||||
|
|
||||||
fyne.Do(func() {
|
fyne.Do(func() {
|
||||||
progressLabel.SetText("Step 11 of 11: Generating Bump Map")
|
progressLabel.SetText("Step 13 of 13: Generating Bump Map")
|
||||||
progressBar.SetValue(1.0)
|
progressBar.SetValue(1.0)
|
||||||
})
|
})
|
||||||
bumpMap := GenerateBumpMap(compositeImg.(*image.RGBA), settings.Width, settings.Height, 0.10)
|
bumpMap := GenerateBumpMap(compositeImg.(*image.RGBA), settings.Width, settings.Height, 0.10)
|
||||||
@@ -942,6 +1126,14 @@ func main() {
|
|||||||
roadCurvynessSlider,
|
roadCurvynessSlider,
|
||||||
roadDistributionSlider,
|
roadDistributionSlider,
|
||||||
))
|
))
|
||||||
|
|
||||||
|
fortificationsTab := container.NewTabItem("Fortifications", container.NewVBox(
|
||||||
|
minWallWidthSlider,
|
||||||
|
maxWallWidthSlider,
|
||||||
|
numWallsSlider,
|
||||||
|
cityCoverageSlider,
|
||||||
|
wallCurvynessSlider,
|
||||||
|
))
|
||||||
numBuildingsSlider := newNumericInputSlider(0, 10000, float64(settings.NumBuildings), "%.0f", "Number of Buildings")
|
numBuildingsSlider := newNumericInputSlider(0, 10000, float64(settings.NumBuildings), "%.0f", "Number of Buildings")
|
||||||
numBuildingsSlider.entry.OnChanged = func(s string) {
|
numBuildingsSlider.entry.OnChanged = func(s string) {
|
||||||
numBuildingsSlider.validate(s, func(hasError bool) {
|
numBuildingsSlider.validate(s, func(hasError bool) {
|
||||||
@@ -1219,6 +1411,7 @@ func main() {
|
|||||||
terrainTab,
|
terrainTab,
|
||||||
waterTab,
|
waterTab,
|
||||||
roadsTab,
|
roadsTab,
|
||||||
|
fortificationsTab,
|
||||||
buildingsTab,
|
buildingsTab,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -1303,7 +1496,7 @@ func encodeImageToWriter(w io.Writer, img image.Image, format string) 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, riverMask, treeMask, roadMask, bridgeMask, buildingMask *PixelMask) {
|
func showMasksSaveDialog(win fyne.Window, canvasImg, heightmapImg, bumpmapImg image.Image, settings *Settings, lakes [][]image.Point, riverMask, treeMask, roadMask, bridgeMask, wallMask, 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")
|
||||||
@@ -1387,6 +1580,7 @@ func showMasksSaveDialog(win fyne.Window, canvasImg, heightmapImg, bumpmapImg im
|
|||||||
{name: "trees_mask." + imgFormat, make: func() image.Image { return maskToGray(treeMask) }},
|
{name: "trees_mask." + imgFormat, make: func() image.Image { return maskToGray(treeMask) }},
|
||||||
{name: "roads_mask." + imgFormat, make: func() image.Image { return maskToGray(roadMask) }},
|
{name: "roads_mask." + imgFormat, make: func() image.Image { return maskToGray(roadMask) }},
|
||||||
{name: "bridges_mask." + imgFormat, make: func() image.Image { return maskToGray(bridgeMask) }},
|
{name: "bridges_mask." + imgFormat, make: func() image.Image { return maskToGray(bridgeMask) }},
|
||||||
|
{name: "walls_mask." + imgFormat, make: func() image.Image { return maskToGray(wallMask) }},
|
||||||
{name: "buildings_mask." + imgFormat, make: func() image.Image { return maskToGray(buildingMask) }},
|
{name: "buildings_mask." + imgFormat, make: func() image.Image { return maskToGray(buildingMask) }},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -87,6 +87,41 @@ func GenerateRoads(
|
|||||||
settings *Settings,
|
settings *Settings,
|
||||||
waterMask *PixelMask,
|
waterMask *PixelMask,
|
||||||
seed int64,
|
seed int64,
|
||||||
|
) (*PixelMask, *PixelMask, *PixelMask, []image.Point) {
|
||||||
|
return GenerateRoadsWithPOIs(img, width, height, settings, waterMask, nil, nil, 0, false, seed)
|
||||||
|
}
|
||||||
|
|
||||||
|
func PrepareRoadNodes(width, height int, settings *Settings, waterMask *PixelMask, seed int64) ([]*PointOfInterest, int, bool) {
|
||||||
|
randSrc := rand.New(rand.NewSource(seed))
|
||||||
|
|
||||||
|
if settings.NumBuildings == 0 {
|
||||||
|
internalRoads := int(math.Round(clamp(settings.RoadDistribution, 0, 100)))
|
||||||
|
exitRoads := max(0, settings.RoadExits)
|
||||||
|
if internalRoads == 0 && exitRoads > 0 && settings.RoadDistribution <= 0 {
|
||||||
|
return nil, 0, true
|
||||||
|
}
|
||||||
|
if internalRoads > 0 {
|
||||||
|
roadTarget := internalRoads
|
||||||
|
return generatePOIs(width, height, settings, waterMask, randSrc, roadTarget), roadTarget, false
|
||||||
|
}
|
||||||
|
return nil, 0, false
|
||||||
|
}
|
||||||
|
|
||||||
|
roadTarget := estimateRoadTarget(settings)
|
||||||
|
return generatePOIs(width, height, settings, waterMask, randSrc, roadTarget), roadTarget, false
|
||||||
|
}
|
||||||
|
|
||||||
|
func GenerateRoadsWithPOIs(
|
||||||
|
img *image.RGBA,
|
||||||
|
width,
|
||||||
|
height int,
|
||||||
|
settings *Settings,
|
||||||
|
waterMask *PixelMask,
|
||||||
|
wallLayout *FortificationLayout,
|
||||||
|
pois []*PointOfInterest,
|
||||||
|
roadTarget int,
|
||||||
|
edgeToEdgeOnly bool,
|
||||||
|
seed int64,
|
||||||
) (*PixelMask, *PixelMask, *PixelMask, []image.Point) {
|
) (*PixelMask, *PixelMask, *PixelMask, []image.Point) {
|
||||||
if img == nil {
|
if img == nil {
|
||||||
img = image.NewRGBA(image.Rect(0, 0, width, height))
|
img = image.NewRGBA(image.Rect(0, 0, width, height))
|
||||||
@@ -96,56 +131,40 @@ func GenerateRoads(
|
|||||||
bridgeColor := color.RGBA{R: 60, G: 42, B: 33, A: 255}
|
bridgeColor := color.RGBA{R: 60, G: 42, B: 33, A: 255}
|
||||||
|
|
||||||
// Edge-case mode: no buildings.
|
// Edge-case mode: no buildings.
|
||||||
if settings.NumBuildings == 0 {
|
if settings.NumBuildings == 0 && roadTarget == 0 && !edgeToEdgeOnly {
|
||||||
internalRoads := int(math.Round(clamp(settings.RoadDistribution, 0, 100)))
|
internalRoads := int(math.Round(clamp(settings.RoadDistribution, 0, 100)))
|
||||||
exitRoads := max(0, settings.RoadExits)
|
exitRoads := max(0, settings.RoadExits)
|
||||||
if internalRoads == 0 && exitRoads == 0 {
|
if internalRoads == 0 && exitRoads == 0 {
|
||||||
return NewPixelMask(width, height), NewPixelMask(width, height), NewPixelMask(width, height), nil
|
return NewPixelMask(width, height), NewPixelMask(width, height), NewPixelMask(width, height), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var roads []*Road
|
|
||||||
if internalRoads > 0 {
|
if internalRoads > 0 {
|
||||||
roadTarget := internalRoads
|
roadTarget = internalRoads
|
||||||
pois := generatePOIs(width, height, settings, waterMask, randSrc, roadTarget)
|
|
||||||
if len(pois) >= 2 {
|
|
||||||
roads = connectPOIs(pois, width, height, settings, randSrc, waterMask, roadTarget)
|
|
||||||
// Use existing exit-road logic when internal roads are present.
|
|
||||||
roads = appendExitRoads(roads, pois, width, height, settings, randSrc, waterMask)
|
|
||||||
}
|
|
||||||
} else if settings.RoadDistribution <= 0 && exitRoads > 0 {
|
} else if settings.RoadDistribution <= 0 && exitRoads > 0 {
|
||||||
// Only in 0% distribution mode: exit roads are edge-to-edge.
|
edgeToEdgeOnly = true
|
||||||
roads = generateEdgeToEdgeExitRoads(exitRoads, width, height, settings, randSrc, waterMask)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(roads) == 0 {
|
|
||||||
return NewPixelMask(width, height), NewPixelMask(width, height), NewPixelMask(width, height), nil
|
|
||||||
}
|
|
||||||
roads = reduceRepeatedBridges(roads, waterMask, width, height, randSrc)
|
|
||||||
if len(roads) == 0 {
|
|
||||||
return NewPixelMask(width, height), NewPixelMask(width, height), NewPixelMask(width, height), nil
|
|
||||||
}
|
|
||||||
assignRoadWidths(roads, settings, randSrc, width, height)
|
|
||||||
|
|
||||||
roadMask := NewPixelMask(width, height)
|
|
||||||
bridgeMask := NewPixelMask(width, height)
|
|
||||||
exitRoadMask := NewPixelMask(width, height)
|
|
||||||
for _, road := range roads {
|
|
||||||
drawRoadToMasks(img, road.Points, roadColor, bridgeColor, road.Width, roadMask, bridgeMask)
|
|
||||||
if road.Start.IsExit || road.End.IsExit {
|
|
||||||
drawRoadToMasks(img, road.Points, roadColor, bridgeColor, road.Width, exitRoadMask, exitRoadMask)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return roadMask, bridgeMask, exitRoadMask, roadMask.ToPoints()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
roadTarget := estimateRoadTarget(settings)
|
var roads []*Road
|
||||||
pois := generatePOIs(width, height, settings, waterMask, randSrc, roadTarget)
|
if edgeToEdgeOnly {
|
||||||
if len(pois) < 2 {
|
roads = generateEdgeToEdgeExitRoads(max(0, settings.RoadExits), width, height, settings, randSrc, waterMask, wallLayout)
|
||||||
|
} else {
|
||||||
|
if roadTarget <= 0 {
|
||||||
|
roadTarget = estimateRoadTarget(settings)
|
||||||
|
}
|
||||||
|
if pois == nil {
|
||||||
|
pois = generatePOIs(width, height, settings, waterMask, randSrc, roadTarget)
|
||||||
|
}
|
||||||
|
if len(pois) < 2 {
|
||||||
|
return NewPixelMask(width, height), NewPixelMask(width, height), NewPixelMask(width, height), nil
|
||||||
|
}
|
||||||
|
roads = connectPOIs(pois, width, height, settings, randSrc, waterMask, wallLayout, roadTarget)
|
||||||
|
roads = appendExitRoads(roads, pois, width, height, settings, randSrc, waterMask, wallLayout)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(roads) == 0 {
|
||||||
return NewPixelMask(width, height), NewPixelMask(width, height), NewPixelMask(width, height), nil
|
return NewPixelMask(width, height), NewPixelMask(width, height), NewPixelMask(width, height), nil
|
||||||
}
|
}
|
||||||
|
roads = applyWallCrossingRules(roads, wallLayout, waterMask, randSrc)
|
||||||
roads := connectPOIs(pois, width, height, settings, randSrc, waterMask, roadTarget)
|
|
||||||
roads = appendExitRoads(roads, pois, width, height, settings, randSrc, waterMask)
|
|
||||||
if len(roads) == 0 {
|
if len(roads) == 0 {
|
||||||
return NewPixelMask(width, height), NewPixelMask(width, height), NewPixelMask(width, height), nil
|
return NewPixelMask(width, height), NewPixelMask(width, height), NewPixelMask(width, height), nil
|
||||||
}
|
}
|
||||||
@@ -169,7 +188,7 @@ func GenerateRoads(
|
|||||||
return roadMask, bridgeMask, exitRoadMask, roadAnchors
|
return roadMask, bridgeMask, exitRoadMask, roadAnchors
|
||||||
}
|
}
|
||||||
|
|
||||||
func generateEdgeToEdgeExitRoads(exitRoads, width, height int, settings *Settings, randSrc *rand.Rand, waterMask *PixelMask) []*Road {
|
func generateEdgeToEdgeExitRoads(exitRoads, width, height int, settings *Settings, randSrc *rand.Rand, waterMask *PixelMask, wallLayout *FortificationLayout) []*Road {
|
||||||
if exitRoads <= 0 {
|
if exitRoads <= 0 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -179,7 +198,7 @@ func generateEdgeToEdgeExitRoads(exitRoads, width, height int, settings *Setting
|
|||||||
start, end := sampleDifferentEdgePair(width, height, randSrc)
|
start, end := sampleDifferentEdgePair(width, height, randSrc)
|
||||||
start.IsExit = true
|
start.IsExit = true
|
||||||
end.IsExit = true
|
end.IsExit = true
|
||||||
path := calculateRoadPath(start, end, settings.RoadCurvyness/100.0, avgDim, randSrc, waterMask)
|
path := calculateRoadPath(start, end, settings.RoadCurvyness/100.0, avgDim, randSrc, waterMask, wallLayout)
|
||||||
roads = append(roads, &Road{
|
roads = append(roads, &Road{
|
||||||
Start: start,
|
Start: start,
|
||||||
End: end,
|
End: end,
|
||||||
@@ -367,7 +386,7 @@ func sampleTargetDegree(randSrc *rand.Rand) int {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func connectPOIs(pois []*PointOfInterest, width, height int, settings *Settings, randSrc *rand.Rand, waterMask *PixelMask, roadTarget int) []*Road {
|
func connectPOIs(pois []*PointOfInterest, width, height int, settings *Settings, randSrc *rand.Rand, waterMask *PixelMask, wallLayout *FortificationLayout, 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
|
||||||
@@ -512,7 +531,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, waterMask)
|
path := calculateRoadPath(a, b, settings.RoadCurvyness/100.0, avgDim, randSrc, waterMask, wallLayout)
|
||||||
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})
|
||||||
}
|
}
|
||||||
@@ -520,7 +539,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, waterMask *PixelMask) []*Road {
|
func appendExitRoads(roads []*Road, pois []*PointOfInterest, width, height int, settings *Settings, randSrc *rand.Rand, waterMask *PixelMask, wallLayout *FortificationLayout) []*Road {
|
||||||
if settings.RoadExits <= 0 || len(pois) == 0 {
|
if settings.RoadExits <= 0 || len(pois) == 0 {
|
||||||
return roads
|
return roads
|
||||||
}
|
}
|
||||||
@@ -540,12 +559,36 @@ func appendExitRoads(roads []*Road, pois []*PointOfInterest, width, height int,
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
path := calculateRoadPath(anchor, edgeNode, settings.RoadCurvyness/100.0, avgDim, randSrc, waterMask, wallLayout)
|
||||||
|
if wallLayout != nil && wallLayout.Mask != nil && len(crossedWallIDs(path, wallLayout)) == 0 {
|
||||||
|
bestScore := -1.0
|
||||||
|
bestAnchor := anchor
|
||||||
|
bestPath := path
|
||||||
|
for _, cand := range pois {
|
||||||
|
testPath := calculateRoadPath(cand, edgeNode, settings.RoadCurvyness/100.0, avgDim, randSrc, waterMask, wallLayout)
|
||||||
|
if len(crossedWallIDs(testPath, wallLayout)) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
d := math.Hypot(float64(cand.X-edgeNode.X), float64(cand.Y-edgeNode.Y))
|
||||||
|
score := cand.ArterialWeight*2.0 + clamp(1.0-d/2000.0, 0, 1)
|
||||||
|
if score > bestScore {
|
||||||
|
bestScore = score
|
||||||
|
bestAnchor = cand
|
||||||
|
bestPath = testPath
|
||||||
|
}
|
||||||
|
}
|
||||||
|
anchor = bestAnchor
|
||||||
|
path = bestPath
|
||||||
|
}
|
||||||
|
if wallLayout != nil && wallLayout.Mask != nil && len(wallLayout.Coverages) > 0 && len(crossedWallIDs(path, wallLayout)) == 0 {
|
||||||
|
// Exit roads should pass through walls when walls exist.
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
anchor.Connections++
|
anchor.Connections++
|
||||||
edgeNode.IsExit = true
|
edgeNode.IsExit = true
|
||||||
edgeNode.TargetDegree = 1
|
edgeNode.TargetDegree = 1
|
||||||
edgeNode.Connections = 1
|
edgeNode.Connections = 1
|
||||||
|
|
||||||
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,
|
||||||
@@ -821,7 +864,7 @@ 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, waterMask *PixelMask) []PathPoint {
|
func calculateRoadPath(start, end *PointOfInterest, curvyness, avgDim float64, randSrc *rand.Rand, waterMask *PixelMask, wallLayout *FortificationLayout) []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))
|
||||||
@@ -834,14 +877,14 @@ func calculateRoadPath(start, end *PointOfInterest, curvyness, avgDim float64, r
|
|||||||
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, waterMask)
|
return straightenPathAcrossWalls(toPathPoints(points, waterMask), wallLayout, 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, waterMask)
|
return straightenPathAcrossWalls(toPathPoints(points, waterMask), wallLayout, waterMask)
|
||||||
}
|
}
|
||||||
|
|
||||||
baseControls := int(math.Max(12, dist/(22.0-14.0*strength)))
|
baseControls := int(math.Max(12, dist/(22.0-14.0*strength)))
|
||||||
@@ -902,17 +945,194 @@ func calculateRoadPath(start, end *PointOfInterest, curvyness, avgDim float64, r
|
|||||||
}
|
}
|
||||||
|
|
||||||
points := bresenhamRoad(controlPoints)
|
points := bresenhamRoad(controlPoints)
|
||||||
return toPathPoints(points, waterMask)
|
return straightenPathAcrossWalls(toPathPoints(points, waterMask), wallLayout, waterMask)
|
||||||
}
|
}
|
||||||
|
|
||||||
func toPathPoints(points []image.Point, waterMask *PixelMask) []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: waterMask.GetPoint(p)}
|
isBridge := false
|
||||||
|
if waterMask != nil {
|
||||||
|
isBridge = waterMask.GetPoint(p)
|
||||||
|
}
|
||||||
|
pathPoints[i] = PathPoint{Point: p, IsBridge: isBridge}
|
||||||
}
|
}
|
||||||
return pathPoints
|
return pathPoints
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func wallIDAtPoint(p image.Point, wallLayout *FortificationLayout) int {
|
||||||
|
if wallLayout == nil || wallLayout.Mask == nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
if !wallLayout.Mask.InBounds(p.X, p.Y) {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
if len(wallLayout.WallIDByPixel) != wallLayout.Mask.Width*wallLayout.Mask.Height {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return wallLayout.WallIDByPixel[p.Y*wallLayout.Mask.Width+p.X]
|
||||||
|
}
|
||||||
|
|
||||||
|
func straightenPathAcrossWalls(points []PathPoint, wallLayout *FortificationLayout, waterMask *PixelMask) []PathPoint {
|
||||||
|
if wallLayout == nil || wallLayout.Mask == nil || len(points) < 2 {
|
||||||
|
return points
|
||||||
|
}
|
||||||
|
|
||||||
|
straight := make([]image.Point, 0, len(points))
|
||||||
|
i := 0
|
||||||
|
for i < len(points) {
|
||||||
|
curr := points[i].Point
|
||||||
|
currWallID := wallIDAtPoint(curr, wallLayout)
|
||||||
|
if currWallID == 0 {
|
||||||
|
straight = append(straight, curr)
|
||||||
|
i++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
start := i
|
||||||
|
if start > 0 {
|
||||||
|
start--
|
||||||
|
}
|
||||||
|
j := i
|
||||||
|
for j < len(points) && wallIDAtPoint(points[j].Point, wallLayout) != 0 {
|
||||||
|
j++
|
||||||
|
}
|
||||||
|
end := j
|
||||||
|
if end >= len(points) {
|
||||||
|
end = len(points) - 1
|
||||||
|
}
|
||||||
|
line := bresenhamRoad([]image.Point{points[start].Point, points[end].Point})
|
||||||
|
for k, p := range line {
|
||||||
|
if len(straight) > 0 && k == 0 && straight[len(straight)-1] == p {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
straight = append(straight, p)
|
||||||
|
}
|
||||||
|
i = j
|
||||||
|
}
|
||||||
|
|
||||||
|
return toPathPoints(straight, waterMask)
|
||||||
|
}
|
||||||
|
|
||||||
|
func crossedWallIDs(points []PathPoint, wallLayout *FortificationLayout) []int {
|
||||||
|
if wallLayout == nil || wallLayout.Mask == nil || len(points) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
seen := make(map[int]bool)
|
||||||
|
out := make([]int, 0, 2)
|
||||||
|
prevID := wallIDAtPoint(points[0].Point, wallLayout)
|
||||||
|
for i := 1; i < len(points); i++ {
|
||||||
|
currID := wallIDAtPoint(points[i].Point, wallLayout)
|
||||||
|
if (prevID == 0 && currID > 0) || (prevID > 0 && currID == 0) {
|
||||||
|
wid := currID
|
||||||
|
if wid == 0 {
|
||||||
|
wid = prevID
|
||||||
|
}
|
||||||
|
if wid > 0 && !seen[wid] {
|
||||||
|
seen[wid] = true
|
||||||
|
out = append(out, wid)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
prevID = currID
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func containsWallID(ids []int, wallID int) bool {
|
||||||
|
for _, id := range ids {
|
||||||
|
if id == wallID {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func applyWallCrossingRules(roads []*Road, wallLayout *FortificationLayout, waterMask *PixelMask, randSrc *rand.Rand) []*Road {
|
||||||
|
if len(roads) == 0 || wallLayout == nil || wallLayout.Mask == nil || len(wallLayout.Coverages) == 0 {
|
||||||
|
return roads
|
||||||
|
}
|
||||||
|
|
||||||
|
// Straighten each wall crossing segment first.
|
||||||
|
for _, road := range roads {
|
||||||
|
road.Points = straightenPathAcrossWalls(road.Points, wallLayout, waterMask)
|
||||||
|
}
|
||||||
|
|
||||||
|
type roadInfo struct {
|
||||||
|
road *Road
|
||||||
|
ids []int
|
||||||
|
}
|
||||||
|
infos := make([]roadInfo, 0, len(roads))
|
||||||
|
for _, road := range roads {
|
||||||
|
infos = append(infos, roadInfo{road: road, ids: crossedWallIDs(road.Points, wallLayout)})
|
||||||
|
}
|
||||||
|
|
||||||
|
const repeatWallFactor = 0.55
|
||||||
|
wallCrossCount := make(map[int]int)
|
||||||
|
requiredWalls := make(map[int]bool)
|
||||||
|
for i, cov := range wallLayout.Coverages {
|
||||||
|
if cov < 95 {
|
||||||
|
requiredWalls[i+1] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
keep := make([]bool, len(infos))
|
||||||
|
for i, info := range infos {
|
||||||
|
if len(info.ids) == 0 {
|
||||||
|
keep[i] = true
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if info.road.Start.IsExit || info.road.End.IsExit {
|
||||||
|
keep[i] = true
|
||||||
|
for _, wid := range info.ids {
|
||||||
|
wallCrossCount[wid]++
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
keepProb := 1.0
|
||||||
|
for _, wid := range info.ids {
|
||||||
|
c := wallCrossCount[wid]
|
||||||
|
if c > 0 {
|
||||||
|
keepProb *= math.Pow(repeatWallFactor, float64(c))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if randSrc.Float64() <= keepProb {
|
||||||
|
keep[i] = true
|
||||||
|
for _, wid := range info.ids {
|
||||||
|
wallCrossCount[wid]++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure at least one crossing on each wall unless its configured coverage is >= 95%.
|
||||||
|
for wallID := range requiredWalls {
|
||||||
|
if wallCrossCount[wallID] > 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for i, info := range infos {
|
||||||
|
if keep[i] {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if !containsWallID(info.ids, wallID) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
keep[i] = true
|
||||||
|
for _, wid := range info.ids {
|
||||||
|
wallCrossCount[wid]++
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
filtered := make([]*Road, 0, len(roads))
|
||||||
|
for i, info := range infos {
|
||||||
|
if keep[i] {
|
||||||
|
filtered = append(filtered, info.road)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return filtered
|
||||||
|
}
|
||||||
|
|
||||||
// drawLineMasked draws a line with specified width on the image and mask.
|
// drawLineMasked draws a line with specified width on the image and mask.
|
||||||
func drawLineMasked(img *image.RGBA, x0, y0, x1, y1 int, col color.Color, width int, mask *PixelMask) {
|
func drawLineMasked(img *image.RGBA, x0, y0, x1, y1 int, col color.Color, width int, mask *PixelMask) {
|
||||||
dx := abs(x1 - x0)
|
dx := abs(x1 - x0)
|
||||||
|
|||||||
+47
@@ -44,6 +44,13 @@ type Settings struct {
|
|||||||
RoadDistribution float64 `json:"road_distribution"`
|
RoadDistribution float64 `json:"road_distribution"`
|
||||||
MinRoadAngle float64 `json:"min_road_angle"`
|
MinRoadAngle float64 `json:"min_road_angle"`
|
||||||
|
|
||||||
|
// Fortification settings
|
||||||
|
MinWallWidth float64 `json:"min_wall_width"`
|
||||||
|
MaxWallWidth float64 `json:"max_wall_width"`
|
||||||
|
NumWalls int `json:"num_walls"`
|
||||||
|
CityCoverage float64 `json:"city_coverage"`
|
||||||
|
WallCurvyness float64 `json:"wall_curvyness"`
|
||||||
|
|
||||||
// Building settings
|
// Building settings
|
||||||
NumBuildings int `json:"num_buildings"`
|
NumBuildings int `json:"num_buildings"`
|
||||||
MinBuildingSize float64 `json:"min_building_size"`
|
MinBuildingSize float64 `json:"min_building_size"`
|
||||||
@@ -139,6 +146,11 @@ func LoadSettings() (*Settings, error) {
|
|||||||
RoadCurvyness: 50,
|
RoadCurvyness: 50,
|
||||||
RoadDistribution: 50,
|
RoadDistribution: 50,
|
||||||
MinRoadAngle: 18,
|
MinRoadAngle: 18,
|
||||||
|
MinWallWidth: 1.5,
|
||||||
|
MaxWallWidth: 4.0,
|
||||||
|
NumWalls: 1,
|
||||||
|
CityCoverage: 70,
|
||||||
|
WallCurvyness: 35,
|
||||||
NumBuildings: 200,
|
NumBuildings: 200,
|
||||||
MinBuildingSize: 3.5,
|
MinBuildingSize: 3.5,
|
||||||
MaxBuildingSize: 10.0,
|
MaxBuildingSize: 10.0,
|
||||||
@@ -200,9 +212,44 @@ func LoadSettings() (*Settings, error) {
|
|||||||
if settings.BuildingsPerRoad == 0 {
|
if settings.BuildingsPerRoad == 0 {
|
||||||
settings.BuildingsPerRoad = 6
|
settings.BuildingsPerRoad = 6
|
||||||
}
|
}
|
||||||
|
if _, ok := rawKeys["min_wall_width"]; !ok {
|
||||||
|
settings.MinWallWidth = 1.5
|
||||||
|
}
|
||||||
|
if _, ok := rawKeys["max_wall_width"]; !ok {
|
||||||
|
settings.MaxWallWidth = 4.0
|
||||||
|
}
|
||||||
|
if settings.NumWalls == 0 {
|
||||||
|
settings.NumWalls = 1
|
||||||
|
}
|
||||||
|
if settings.CityCoverage == 0 {
|
||||||
|
settings.CityCoverage = 70
|
||||||
|
}
|
||||||
if _, ok := rawKeys["min_road_angle"]; !ok {
|
if _, ok := rawKeys["min_road_angle"]; !ok {
|
||||||
settings.MinRoadAngle = 18
|
settings.MinRoadAngle = 18
|
||||||
}
|
}
|
||||||
|
if _, ok := rawKeys["wall_curvyness"]; !ok {
|
||||||
|
settings.WallCurvyness = 35
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wall widths are percentages of average image dimension.
|
||||||
|
// Migrate older pixel-based values when they exceed the valid percentage range.
|
||||||
|
if settings.MinWallWidth > maxWallWidthPercent || settings.MaxWallWidth > maxWallWidthPercent {
|
||||||
|
avgDim := averageImageDimension(settings.Width, settings.Height)
|
||||||
|
if avgDim < 1 {
|
||||||
|
avgDim = 1
|
||||||
|
}
|
||||||
|
settings.MinWallWidth = (settings.MinWallWidth / avgDim) * 100.0
|
||||||
|
settings.MaxWallWidth = (settings.MaxWallWidth / avgDim) * 100.0
|
||||||
|
}
|
||||||
|
settings.MinWallWidth, settings.MaxWallWidth = normalizeWallWidthPercentRange(settings.MinWallWidth, settings.MaxWallWidth)
|
||||||
|
if settings.NumWalls < 1 {
|
||||||
|
settings.NumWalls = 1
|
||||||
|
}
|
||||||
|
if settings.NumWalls > 5 {
|
||||||
|
settings.NumWalls = 5
|
||||||
|
}
|
||||||
|
settings.CityCoverage = clamp(settings.CityCoverage, 1, 100)
|
||||||
|
settings.WallCurvyness = clamp(settings.WallCurvyness, 0, 100)
|
||||||
|
|
||||||
// Tree sizes are percentages of average image dimension.
|
// Tree sizes are percentages of average image dimension.
|
||||||
// Migrate older pixel-based values when they exceed the valid percentage range.
|
// Migrate older pixel-based values when they exceed the valid percentage range.
|
||||||
|
|||||||
Reference in New Issue
Block a user