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` |
|
| **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 |
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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++ {
|
||||||
|
|||||||
Reference in New Issue
Block a user