tree coverage percentage fixed
This commit is contained in:
@@ -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` |
|
||||
| **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%` |
|
||||
| **Min Tree Size** | The minimum size of a generated tree in pixels. | `1` to `100` |
|
||||
| **Max Tree Size** | The maximum 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 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 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 |
|
||||
|
||||
@@ -482,7 +482,7 @@ func main() {
|
||||
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.validate(s, func(hasError bool) {
|
||||
errorStates["minTreeSize"] = hasError
|
||||
@@ -494,7 +494,7 @@ func main() {
|
||||
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.validate(s, func(hasError bool) {
|
||||
errorStates["maxTreeSize"] = hasError
|
||||
|
||||
+14
-2
@@ -120,8 +120,8 @@ func LoadSettings() (*Settings, error) {
|
||||
LakeSizeUpper: 5,
|
||||
LakeEdgeRoughness: 50,
|
||||
LakeShape: "circle",
|
||||
MinTreeSize: 5,
|
||||
MaxTreeSize: 20,
|
||||
MinTreeSize: 1.6,
|
||||
MaxTreeSize: 6.6,
|
||||
TreeCoverage: 20,
|
||||
TreeClumpiness: 50,
|
||||
Seed: time.Now().UnixNano(),
|
||||
@@ -199,6 +199,18 @@ func LoadSettings() (*Settings, error) {
|
||||
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.
|
||||
// Migrate older pixel-based values when they exceed the valid percentage range.
|
||||
if settings.MinRoadWidth > maxRoadWidthPercent || settings.MaxRoadWidth > maxRoadWidthPercent {
|
||||
|
||||
+104
-66
@@ -18,8 +18,54 @@ const (
|
||||
alpha = 2.
|
||||
beta = 2.
|
||||
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
|
||||
func GenerateHeightmap(width, height, octaves int, scale float64, seed int64) image.Image {
|
||||
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 {
|
||||
width := img.Bounds().Dx()
|
||||
height := img.Bounds().Dy()
|
||||
minTreeSizePx, maxTreeSizePx := getTreeSizeRangePixels(minTreeSize, maxTreeSize, width, height)
|
||||
|
||||
avgTreeSize := (minTreeSize + maxTreeSize) / 2
|
||||
if avgTreeSize <= 0 {
|
||||
return nil
|
||||
totalPixels := width * height
|
||||
if totalPixels <= 0 || treeCoverage <= 0 {
|
||||
return NewPixelMask(width, height)
|
||||
}
|
||||
avgRadius := avgTreeSize / 2
|
||||
avgTreeArea := math.Pi * avgRadius * avgRadius
|
||||
if avgTreeArea == 0 {
|
||||
return nil
|
||||
targetTreePixels := int((float64(totalPixels) * treeCoverage) / 100.0)
|
||||
if treeCoverage >= 100 {
|
||||
targetTreePixels = totalPixels
|
||||
}
|
||||
totalArea := float64(width * height)
|
||||
targetTreePixels := totalArea * (treeCoverage / 100.0)
|
||||
numTreesToPlace := int(targetTreePixels / avgTreeArea)
|
||||
if numTreesToPlace == 0 {
|
||||
return nil
|
||||
if targetTreePixels < 1 {
|
||||
targetTreePixels = 1
|
||||
}
|
||||
|
||||
noise := opensimplex.New(seed)
|
||||
@@ -201,8 +244,7 @@ func GenerateTrees(img *image.RGBA, waterMask, roadMask, buildingMask *PixelMask
|
||||
}
|
||||
|
||||
randSrc := rand.New(rand.NewSource(seed))
|
||||
|
||||
numClumpTrees := min(int(treeClumpiness), numTreesToPlace)
|
||||
numClumpTrees := max(1, int(treeClumpiness))
|
||||
|
||||
initialPoints := make([]image.Point, 0, 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 {
|
||||
return treeNoiseMap.GrayAt(p.X, p.Y).Y >= threshold && !waterMask.GetPoint(p) && !roadMask.GetPoint(p) && !buildingMask.GetPoint(p)
|
||||
}, seed)
|
||||
|
||||
numGoroutines := runtime.NumCPU()
|
||||
if len(allPoints) < numGoroutines {
|
||||
numGoroutines = len(allPoints)
|
||||
treeMask := NewPixelMask(width, height)
|
||||
if len(allPoints) == 0 {
|
||||
return treeMask
|
||||
}
|
||||
if numGoroutines == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
var wg sync.WaitGroup
|
||||
results := make(chan []image.Point, numGoroutines)
|
||||
pointsPerGoroutine := (len(allPoints) + numGoroutines - 1) / numGoroutines
|
||||
|
||||
for i := 0; i < numGoroutines; i++ {
|
||||
start := i * pointsPerGoroutine
|
||||
if start >= len(allPoints) {
|
||||
break
|
||||
treePixelsPlaced := 0
|
||||
sizeRand := rand.New(rand.NewSource(seed + 17))
|
||||
done := false
|
||||
for _, p := range allPoints {
|
||||
size := minTreeSizePx + sizeRand.Float64()*(maxTreeSizePx-minTreeSizePx)
|
||||
if size <= 0 {
|
||||
continue
|
||||
}
|
||||
end := start + pointsPerGoroutine
|
||||
if end > len(allPoints) {
|
||||
end = len(allPoints)
|
||||
}
|
||||
|
||||
wg.Add(1)
|
||||
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 {
|
||||
r := size / 2
|
||||
r2 := r * r
|
||||
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}
|
||||
if !pt.In(img.Bounds()) || waterMask.GetPoint(pt) || roadMask.GetPoint(pt) || buildingMask.GetPoint(pt) {
|
||||
continue
|
||||
}
|
||||
r := size / 2
|
||||
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}
|
||||
if !pt.In(img.Bounds()) || waterMask.GetPoint(pt) || roadMask.GetPoint(pt) || buildingMask.GetPoint(pt) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (math.Pow(float64(x-p.X), 2) + math.Pow(float64(y-p.Y), 2)) <= r*r {
|
||||
localTreePixels = append(localTreePixels, pt)
|
||||
}
|
||||
dx := float64(x - p.X)
|
||||
dy := float64(y - p.Y)
|
||||
if dx*dx+dy*dy > r2 {
|
||||
continue
|
||||
}
|
||||
idx := y*width + x
|
||||
if treeMask.Data[idx] == 0 {
|
||||
treeMask.Data[idx] = 1
|
||||
treePixelsPlaced++
|
||||
if treePixelsPlaced >= targetTreePixels {
|
||||
done = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
results <- localTreePixels
|
||||
}(allPoints[start:end], seed+int64(i))
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
close(results)
|
||||
|
||||
treeMask := NewPixelMask(width, height)
|
||||
for res := range results {
|
||||
for _, p := range res {
|
||||
treeMask.SetPoint(p)
|
||||
if done {
|
||||
break
|
||||
}
|
||||
}
|
||||
if done {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
for y := 0; y < treeMask.Height; y++ {
|
||||
row := y * treeMask.Width
|
||||
for x := 0; x < treeMask.Width; x++ {
|
||||
|
||||
Reference in New Issue
Block a user