tree coverage percentage fixed

This commit is contained in:
Grimsace
2026-02-26 14:52:19 -06:00
parent 505d9ebbdd
commit 0c56ce9b81
4 changed files with 122 additions and 72 deletions
+2 -2
View File
@@ -22,8 +22,8 @@ A remake in go of a program that generates maps of rpg like towns. Inspied by Ro
| **Lakes** | The number of lakes to generate on the map. | `0` to `100` | | **Lakes** | The number of lakes to generate on the map. | `0` to `100` |
| **Lake Size Lower** | The minimum size of a generated lake, as a percentage of the smaller of the map's width or height. | `1%` to `100%` | | **Lake Size Lower** | The minimum size of a generated lake, as a percentage of the smaller of the map's width or height. | `1%` to `100%` |
| **Lake Size Upper** | The maximum size of a generated lake, as a percentage of the smaller of the map's width or height. | `1%` to `100%` | | **Lake Size Upper** | The maximum size of a generated lake, as a percentage of the smaller of the map's width or height. | `1%` to `100%` |
| **Min Tree Size** | The minimum size of a generated tree in pixels. | `1` to `100` | | **Min Tree Size** | The minimum size of a generated tree as a percentage of the average image dimension (`(width + height) / 2`). | `0.2%` to `15%` in `0.2%` steps |
| **Max Tree Size** | The maximum size of a generated tree in pixels. | `1` to `100` | | **Max Tree Size** | The maximum size of a generated tree as a percentage of the average image dimension (`(width + height) / 2`). | `0.2%` to `15%` in `0.2%` steps |
| **Tree Coverage** | The density of trees on the map, as a percentage of the total land area. At 100%, the land will be completely covered in trees, forming a dense forest. At 0%, there will be no trees. | `0%` (no trees) to `100%` (dense forest) | | **Tree Coverage** | The density of trees on the map, as a percentage of the total land area. At 100%, the land will be completely covered in trees, forming a dense forest. At 0%, there will be no trees. | `0%` (no trees) to `100%` (dense forest) |
| **Tree Clumpiness** | Controls how much trees are clumped together. At 100%, trees will be tightly clustered in a few areas. At 0%, they will be distributed evenly across the map. | `0%` (evenly distributed) to `100%` (highly clumped) | | **Tree Clumpiness** | Controls how much trees are clumped together. At 100%, trees will be tightly clustered in a few areas. At 0%, they will be distributed evenly across the map. | `0%` (evenly distributed) to `100%` (highly clumped) |
| **Seed** | The random seed used for generation. Using the same seed will produce the exact same map every time. This is useful for sharing and reproducing maps. | Any integer | | **Seed** | The random seed used for generation. Using the same seed will produce the exact same map every time. This is useful for sharing and reproducing maps. | Any integer |
+2 -2
View File
@@ -482,7 +482,7 @@ func main() {
settings.RiverEdgeRoughness = val settings.RiverEdgeRoughness = val
})) }))
minTreeSizeSlider := newNumericInputSlider(1, 150, settings.MinTreeSize, "%.0fpx", "Min Tree Size") minTreeSizeSlider := newNumericInputSliderWithStep(minTreeSizePercent, maxTreeSizePercent, settings.MinTreeSize, treeSizePercentStep, "%.1f%%", "Min Tree Size")
minTreeSizeSlider.entry.OnChanged = func(s string) { minTreeSizeSlider.entry.OnChanged = func(s string) {
minTreeSizeSlider.validate(s, func(hasError bool) { minTreeSizeSlider.validate(s, func(hasError bool) {
errorStates["minTreeSize"] = hasError errorStates["minTreeSize"] = hasError
@@ -494,7 +494,7 @@ func main() {
settings.MinTreeSize = val settings.MinTreeSize = val
})) }))
maxTreeSizeSlider := newNumericInputSlider(1, 150, settings.MaxTreeSize, "%.0fpx", "Max Tree Size") maxTreeSizeSlider := newNumericInputSliderWithStep(minTreeSizePercent, maxTreeSizePercent, settings.MaxTreeSize, treeSizePercentStep, "%.1f%%", "Max Tree Size")
maxTreeSizeSlider.entry.OnChanged = func(s string) { maxTreeSizeSlider.entry.OnChanged = func(s string) {
maxTreeSizeSlider.validate(s, func(hasError bool) { maxTreeSizeSlider.validate(s, func(hasError bool) {
errorStates["maxTreeSize"] = hasError errorStates["maxTreeSize"] = hasError
+14 -2
View File
@@ -120,8 +120,8 @@ func LoadSettings() (*Settings, error) {
LakeSizeUpper: 5, LakeSizeUpper: 5,
LakeEdgeRoughness: 50, LakeEdgeRoughness: 50,
LakeShape: "circle", LakeShape: "circle",
MinTreeSize: 5, MinTreeSize: 1.6,
MaxTreeSize: 20, MaxTreeSize: 6.6,
TreeCoverage: 20, TreeCoverage: 20,
TreeClumpiness: 50, TreeClumpiness: 50,
Seed: time.Now().UnixNano(), Seed: time.Now().UnixNano(),
@@ -199,6 +199,18 @@ func LoadSettings() (*Settings, error) {
settings.MinRoadAngle = 18 settings.MinRoadAngle = 18
} }
// Tree sizes are percentages of average image dimension.
// Migrate older pixel-based values when they exceed the valid percentage range.
if settings.MinTreeSize > maxTreeSizePercent || settings.MaxTreeSize > maxTreeSizePercent {
avgDim := averageImageDimension(settings.Width, settings.Height)
if avgDim < 1 {
avgDim = 1
}
settings.MinTreeSize = (settings.MinTreeSize / avgDim) * 100.0
settings.MaxTreeSize = (settings.MaxTreeSize / avgDim) * 100.0
}
settings.MinTreeSize, settings.MaxTreeSize = normalizeTreeSizePercentRange(settings.MinTreeSize, settings.MaxTreeSize)
// Road widths are percentages of average image dimension. // Road widths 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.
if settings.MinRoadWidth > maxRoadWidthPercent || settings.MaxRoadWidth > maxRoadWidthPercent { if settings.MinRoadWidth > maxRoadWidthPercent || settings.MaxRoadWidth > maxRoadWidthPercent {
+104 -66
View File
@@ -18,8 +18,54 @@ const (
alpha = 2. alpha = 2.
beta = 2. beta = 2.
n = 3 n = 3
minTreeSizePercent = 0.2
maxTreeSizePercent = 15.0
treeSizePercentStep = 0.2
) )
func clampTreeSizePercent(v float64) float64 {
if v < minTreeSizePercent {
return minTreeSizePercent
}
if v > maxTreeSizePercent {
return maxTreeSizePercent
}
return v
}
func snapTreeSizePercent(v float64) float64 {
v = clampTreeSizePercent(v)
steps := math.Round((v - minTreeSizePercent) / treeSizePercentStep)
return clampTreeSizePercent(minTreeSizePercent + steps*treeSizePercentStep)
}
func normalizeTreeSizePercentRange(minPercent, maxPercent float64) (float64, float64) {
minPercent = snapTreeSizePercent(minPercent)
maxPercent = snapTreeSizePercent(maxPercent)
if minPercent > maxPercent {
minPercent, maxPercent = maxPercent, minPercent
}
return minPercent, maxPercent
}
func getTreeSizeRangePixels(minPercent, maxPercent float64, width, height int) (float64, float64) {
minPercent, maxPercent = normalizeTreeSizePercentRange(minPercent, maxPercent)
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
}
// GenerateHeightmap creates terrain elevation using Perlin noise // GenerateHeightmap creates terrain elevation using Perlin noise
func GenerateHeightmap(width, height, octaves int, scale float64, seed int64) image.Image { func GenerateHeightmap(width, height, octaves int, scale float64, seed int64) image.Image {
p := perlin.NewPerlin(alpha, beta, n, seed) p := perlin.NewPerlin(alpha, beta, n, seed)
@@ -161,21 +207,18 @@ func FlattenRoadAreas(heightmap image.Image, roadMask *PixelMask) image.Image {
func GenerateTrees(img *image.RGBA, waterMask, roadMask, buildingMask *PixelMask, minTreeSize, maxTreeSize, treeCoverage, treeClumpiness float64, seed int64) *PixelMask { 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()
minTreeSizePx, maxTreeSizePx := getTreeSizeRangePixels(minTreeSize, maxTreeSize, width, height)
avgTreeSize := (minTreeSize + maxTreeSize) / 2 totalPixels := width * height
if avgTreeSize <= 0 { if totalPixels <= 0 || treeCoverage <= 0 {
return nil return NewPixelMask(width, height)
} }
avgRadius := avgTreeSize / 2 targetTreePixels := int((float64(totalPixels) * treeCoverage) / 100.0)
avgTreeArea := math.Pi * avgRadius * avgRadius if treeCoverage >= 100 {
if avgTreeArea == 0 { targetTreePixels = totalPixels
return nil
} }
totalArea := float64(width * height) if targetTreePixels < 1 {
targetTreePixels := totalArea * (treeCoverage / 100.0) targetTreePixels = 1
numTreesToPlace := int(targetTreePixels / avgTreeArea)
if numTreesToPlace == 0 {
return nil
} }
noise := opensimplex.New(seed) noise := opensimplex.New(seed)
@@ -201,8 +244,7 @@ func GenerateTrees(img *image.RGBA, waterMask, roadMask, buildingMask *PixelMask
} }
randSrc := rand.New(rand.NewSource(seed)) randSrc := rand.New(rand.NewSource(seed))
numClumpTrees := max(1, int(treeClumpiness))
numClumpTrees := min(int(treeClumpiness), numTreesToPlace)
initialPoints := make([]image.Point, 0, numClumpTrees) initialPoints := make([]image.Point, 0, numClumpTrees)
for range numClumpTrees { for range numClumpTrees {
@@ -214,71 +256,67 @@ func GenerateTrees(img *image.RGBA, waterMask, roadMask, buildingMask *PixelMask
} }
} }
} }
if len(initialPoints) == 0 {
for i := 0; i < 256; i++ {
p := image.Point{X: randSrc.Intn(width), Y: randSrc.Intn(height)}
if treeNoiseMap.GrayAt(p.X, p.Y).Y >= threshold && !waterMask.GetPoint(p) && !roadMask.GetPoint(p) && !buildingMask.GetPoint(p) {
initialPoints = append(initialPoints, p)
break
}
}
if len(initialPoints) == 0 {
return NewPixelMask(width, height)
}
}
minRadius := minTreeSize minRadius := minTreeSizePx
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 && !waterMask.GetPoint(p) && !roadMask.GetPoint(p) && !buildingMask.GetPoint(p) return treeNoiseMap.GrayAt(p.X, p.Y).Y >= threshold && !waterMask.GetPoint(p) && !roadMask.GetPoint(p) && !buildingMask.GetPoint(p)
}, seed) }, seed)
treeMask := NewPixelMask(width, height)
numGoroutines := runtime.NumCPU() if len(allPoints) == 0 {
if len(allPoints) < numGoroutines { return treeMask
numGoroutines = len(allPoints)
} }
if numGoroutines == 0 { treePixelsPlaced := 0
return nil sizeRand := rand.New(rand.NewSource(seed + 17))
} done := false
for _, p := range allPoints {
var wg sync.WaitGroup size := minTreeSizePx + sizeRand.Float64()*(maxTreeSizePx-minTreeSizePx)
results := make(chan []image.Point, numGoroutines) if size <= 0 {
pointsPerGoroutine := (len(allPoints) + numGoroutines - 1) / numGoroutines continue
for i := 0; i < numGoroutines; i++ {
start := i * pointsPerGoroutine
if start >= len(allPoints) {
break
} }
end := start + pointsPerGoroutine r := size / 2
if end > len(allPoints) { r2 := r * r
end = len(allPoints) for y := p.Y - int(r); y <= p.Y+int(r); y++ {
} for x := p.X - int(r); x <= p.X+int(r); x++ {
pt := image.Point{X: x, Y: y}
wg.Add(1) if !pt.In(img.Bounds()) || waterMask.GetPoint(pt) || roadMask.GetPoint(pt) || buildingMask.GetPoint(pt) {
go func(points []image.Point, seed int64) {
defer wg.Done()
localRand := rand.New(rand.NewSource(seed))
localTreePixels := make([]image.Point, 0)
for _, p := range points {
size := minTreeSize + localRand.Float64()*(maxTreeSize-minTreeSize)
if size <= 0 {
continue continue
} }
r := size / 2 dx := float64(x - p.X)
for y := p.Y - int(r); y <= p.Y+int(r); y++ { dy := float64(y - p.Y)
for x := p.X - int(r); x <= p.X+int(r); x++ { if dx*dx+dy*dy > r2 {
pt := image.Point{X: x, Y: y} continue
if !pt.In(img.Bounds()) || waterMask.GetPoint(pt) || roadMask.GetPoint(pt) || buildingMask.GetPoint(pt) { }
continue idx := y*width + x
} if treeMask.Data[idx] == 0 {
treeMask.Data[idx] = 1
if (math.Pow(float64(x-p.X), 2) + math.Pow(float64(y-p.Y), 2)) <= r*r { treePixelsPlaced++
localTreePixels = append(localTreePixels, pt) if treePixelsPlaced >= targetTreePixels {
} done = true
break
} }
} }
} }
results <- localTreePixels if done {
}(allPoints[start:end], seed+int64(i)) break
} }
}
wg.Wait() if done {
close(results) break
treeMask := NewPixelMask(width, height)
for res := range results {
for _, p := range res {
treeMask.SetPoint(p)
} }
} }
for y := 0; y < treeMask.Height; y++ { for y := 0; y < treeMask.Height; y++ {
row := y * treeMask.Width row := y * treeMask.Width
for x := 0; x < treeMask.Width; x++ { for x := 0; x < treeMask.Width; x++ {