tree generation (needs work) and added deterministic seed
This commit is contained in:
+7
-5
@@ -4,8 +4,6 @@ import (
|
|||||||
"image"
|
"image"
|
||||||
"image/color"
|
"image/color"
|
||||||
"image/draw"
|
"image/draw"
|
||||||
"math/rand"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/aquilax/go-perlin"
|
"github.com/aquilax/go-perlin"
|
||||||
)
|
)
|
||||||
@@ -16,10 +14,14 @@ const (
|
|||||||
n = 3
|
n = 3
|
||||||
)
|
)
|
||||||
|
|
||||||
func GenerateHeightmap(width, height, octaves int) image.Image {
|
func GenerateHeightmap(width, height, octaves int, scale float64, seed int64) image.Image {
|
||||||
p := perlin.NewPerlin(alpha, beta, n, rand.New(rand.NewSource(time.Now().UnixNano())).Int63())
|
p := perlin.NewPerlin(alpha, beta, n, seed)
|
||||||
img := image.NewGray(image.Rect(0, 0, width, height))
|
img := image.NewGray(image.Rect(0, 0, width, height))
|
||||||
|
|
||||||
|
if scale == 0 {
|
||||||
|
scale = 100.0
|
||||||
|
}
|
||||||
|
|
||||||
for x := 0; x < width; x++ {
|
for x := 0; x < width; x++ {
|
||||||
for y := 0; y < height; y++ {
|
for y := 0; y < height; y++ {
|
||||||
var noise float64
|
var noise float64
|
||||||
@@ -28,7 +30,7 @@ func GenerateHeightmap(width, height, octaves int) image.Image {
|
|||||||
maxAmplitude := 0.0
|
maxAmplitude := 0.0
|
||||||
|
|
||||||
for i := 0; i < octaves; i++ {
|
for i := 0; i < octaves; i++ {
|
||||||
noise += p.Noise2D(float64(x)*frequency/100, float64(y)*frequency/100) * amplitude
|
noise += p.Noise2D(float64(x)*frequency/scale, float64(y)*frequency/scale) * amplitude
|
||||||
maxAmplitude += amplitude
|
maxAmplitude += amplitude
|
||||||
amplitude /= 2.0
|
amplitude /= 2.0
|
||||||
frequency *= 2.0
|
frequency *= 2.0
|
||||||
|
|||||||
@@ -2,8 +2,15 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"image"
|
||||||
|
"image/color"
|
||||||
"log"
|
"log"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"image/png"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
"fyne.io/fyne/v2"
|
"fyne.io/fyne/v2"
|
||||||
"fyne.io/fyne/v2/app"
|
"fyne.io/fyne/v2/app"
|
||||||
@@ -24,12 +31,6 @@ func main() {
|
|||||||
settings = &Settings{Detail: 1, Roughness: 0, Width: 300, Height: 300, Lakes: 0, LakeSizeLower: 1, LakeSizeUpper: 5}
|
settings = &Settings{Detail: 1, Roughness: 0, Width: 300, Height: 300, Lakes: 0, LakeSizeLower: 1, LakeSizeUpper: 5}
|
||||||
}
|
}
|
||||||
|
|
||||||
w.SetOnClosed(func() {
|
|
||||||
if err := settings.Save(); err != nil {
|
|
||||||
log.Println("Error saving settings:", err)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
canvasImg := &canvas.Image{
|
canvasImg := &canvas.Image{
|
||||||
FillMode: canvas.ImageFillContain,
|
FillMode: canvas.ImageFillContain,
|
||||||
}
|
}
|
||||||
@@ -38,13 +39,74 @@ func main() {
|
|||||||
FillMode: canvas.ImageFillContain,
|
FillMode: canvas.ImageFillContain,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
configDir, err := os.UserConfigDir()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal("Failed to get user config dir:", err)
|
||||||
|
}
|
||||||
|
appConfigDir := filepath.Join(configDir, "rpgcitymakerreborn")
|
||||||
|
|
||||||
|
canvasPath := filepath.Join(appConfigDir, "canvas.png")
|
||||||
|
heightmapPath := filepath.Join(appConfigDir, "heightmap.png")
|
||||||
|
|
||||||
|
w.SetOnClosed(func() {
|
||||||
|
if err := settings.Save(); err != nil {
|
||||||
|
log.Println("Error saving settings:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if canvasImg.Image != nil {
|
||||||
|
canvasFile, err := os.Create(canvasPath)
|
||||||
|
if err != nil {
|
||||||
|
log.Println("Failed to create canvas file:", err)
|
||||||
|
} else {
|
||||||
|
defer canvasFile.Close()
|
||||||
|
if err := png.Encode(canvasFile, canvasImg.Image); err != nil {
|
||||||
|
log.Println("Failed to encode canvas image:", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if heightmapImg.Image != nil {
|
||||||
|
heightmapFile, err := os.Create(heightmapPath)
|
||||||
|
if err != nil {
|
||||||
|
log.Println("Failed to create heightmap file:", err)
|
||||||
|
} else {
|
||||||
|
defer heightmapFile.Close()
|
||||||
|
if err := png.Encode(heightmapFile, heightmapImg.Image); err != nil {
|
||||||
|
log.Println("Failed to encode heightmap image:", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
canvasFile, err := os.Open(canvasPath)
|
||||||
|
if err == nil {
|
||||||
|
defer canvasFile.Close()
|
||||||
|
img, err := png.Decode(canvasFile)
|
||||||
|
if err == nil {
|
||||||
|
canvasImg.Image = img
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
heightmapFile, err := os.Open(heightmapPath)
|
||||||
|
if err == nil {
|
||||||
|
defer heightmapFile.Close()
|
||||||
|
img, err := png.Decode(heightmapFile)
|
||||||
|
if err == nil {
|
||||||
|
heightmapImg.Image = img
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if canvasImg.Image == nil || heightmapImg.Image == nil {
|
||||||
// Initial image generation
|
// Initial image generation
|
||||||
noiseImg := GenerateHeightmap(settings.Width, settings.Height, int(settings.Detail))
|
noiseImg := GenerateHeightmap(settings.Width, settings.Height, int(settings.Detail), 100.0, settings.Seed)
|
||||||
canvasWithLakes, lakePixels := GenerateLakes(settings.Width, settings.Height, settings.Lakes, settings.LakeSizeLower, settings.LakeSizeUpper, noiseImg)
|
lakeImage, lakePixels := GenerateLakes(settings.Width, settings.Height, settings.Lakes, settings.LakeSizeLower, settings.LakeSizeUpper, noiseImg, settings.Seed)
|
||||||
|
finalImage := lakeImage.(*image.RGBA)
|
||||||
|
GenerateTrees(finalImage, lakePixels, settings.MinTreeSize, settings.MaxTreeSize, settings.TreeCoverage, settings.TreeClumpiness, settings.Seed)
|
||||||
darkenedHeightmap := DarkenLakeAreas(noiseImg, lakePixels)
|
darkenedHeightmap := DarkenLakeAreas(noiseImg, lakePixels)
|
||||||
compositeImg := ApplyRoughness(darkenedHeightmap, settings.Roughness)
|
compositeImg := ApplyRoughness(darkenedHeightmap, settings.Roughness)
|
||||||
heightmapImg.Image = compositeImg
|
heightmapImg.Image = compositeImg
|
||||||
canvasImg.Image = canvasWithLakes
|
canvasImg.Image = finalImage
|
||||||
|
}
|
||||||
|
|
||||||
detailLabel := widget.NewLabel(fmt.Sprintf("Detail: %.0f", settings.Detail))
|
detailLabel := widget.NewLabel(fmt.Sprintf("Detail: %.0f", settings.Detail))
|
||||||
detailSlider := widget.NewSlider(1, 16)
|
detailSlider := widget.NewSlider(1, 16)
|
||||||
@@ -95,35 +157,113 @@ func main() {
|
|||||||
}
|
}
|
||||||
lakeSizeUpperSlider.SetValue(settings.LakeSizeUpper)
|
lakeSizeUpperSlider.SetValue(settings.LakeSizeUpper)
|
||||||
|
|
||||||
widthEntry := widget.NewEntry()
|
minTreeSizeLabel := widget.NewLabel(fmt.Sprintf("Min Tree Size: %.0fpx", settings.MinTreeSize))
|
||||||
widthEntry.SetText(strconv.Itoa(settings.Width))
|
minTreeSizeSlider := widget.NewSlider(1, 150)
|
||||||
widthEntry.OnChanged = func(s string) {
|
maxTreeSizeLabel := widget.NewLabel(fmt.Sprintf("Max Tree Size: %.0fpx", settings.MaxTreeSize))
|
||||||
val, err := strconv.Atoi(s)
|
maxTreeSizeSlider := widget.NewSlider(1, 150)
|
||||||
if err == nil {
|
|
||||||
settings.Width = val
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
heightEntry := widget.NewEntry()
|
minTreeSizeSlider.OnChanged = func(val float64) {
|
||||||
heightEntry.SetText(strconv.Itoa(settings.Height))
|
settings.MinTreeSize = val
|
||||||
heightEntry.OnChanged = func(s string) {
|
if settings.MinTreeSize > settings.MaxTreeSize {
|
||||||
val, err := strconv.Atoi(s)
|
settings.MaxTreeSize = settings.MinTreeSize
|
||||||
if err == nil {
|
maxTreeSizeSlider.SetValue(settings.MaxTreeSize)
|
||||||
settings.Height = val
|
|
||||||
}
|
}
|
||||||
|
minTreeSizeLabel.SetText(fmt.Sprintf("Min Tree Size: %.0fpx", settings.MinTreeSize))
|
||||||
}
|
}
|
||||||
|
minTreeSizeSlider.SetValue(settings.MinTreeSize)
|
||||||
|
|
||||||
|
maxTreeSizeSlider.OnChanged = func(val float64) {
|
||||||
|
settings.MaxTreeSize = val
|
||||||
|
if settings.MaxTreeSize < settings.MinTreeSize {
|
||||||
|
settings.MinTreeSize = settings.MaxTreeSize
|
||||||
|
minTreeSizeSlider.SetValue(settings.MinTreeSize)
|
||||||
|
}
|
||||||
|
maxTreeSizeLabel.SetText(fmt.Sprintf("Max Tree Size: %.0fpx", settings.MaxTreeSize))
|
||||||
|
}
|
||||||
|
maxTreeSizeSlider.SetValue(settings.MaxTreeSize)
|
||||||
|
|
||||||
|
treeCoverageLabel := widget.NewLabel(fmt.Sprintf("Tree Coverage: %.0f%%", settings.TreeCoverage))
|
||||||
|
treeCoverageSlider := widget.NewSlider(1, 100)
|
||||||
|
treeCoverageSlider.OnChanged = func(val float64) {
|
||||||
|
settings.TreeCoverage = val
|
||||||
|
treeCoverageLabel.SetText(fmt.Sprintf("Tree Coverage: %.0f%%", settings.TreeCoverage))
|
||||||
|
}
|
||||||
|
treeCoverageSlider.SetValue(settings.TreeCoverage)
|
||||||
|
|
||||||
|
treeClumpinessLabel := widget.NewLabel(fmt.Sprintf("Tree Clumpiness: %.0f%%", settings.TreeClumpiness))
|
||||||
|
treeClumpinessSlider := widget.NewSlider(0, 100)
|
||||||
|
treeClumpinessSlider.OnChanged = func(val float64) {
|
||||||
|
settings.TreeClumpiness = val
|
||||||
|
treeClumpinessLabel.SetText(fmt.Sprintf("Tree Clumpiness: %.0f%%", settings.TreeClumpiness))
|
||||||
|
}
|
||||||
|
treeClumpinessSlider.SetValue(settings.TreeClumpiness)
|
||||||
|
|
||||||
|
errorLabel := canvas.NewText("", color.RGBA{R: 255, A: 255})
|
||||||
|
errorLabel.TextSize = 12
|
||||||
|
errorLabel.Hide()
|
||||||
|
|
||||||
generateBtn := widget.NewButton("Generate", func() {
|
generateBtn := widget.NewButton("Generate", func() {
|
||||||
noiseImg := GenerateHeightmap(settings.Width, settings.Height, int(settings.Detail))
|
noiseImg := GenerateHeightmap(settings.Width, settings.Height, int(settings.Detail), 100.0, settings.Seed)
|
||||||
canvasWithLakes, lakePixels := GenerateLakes(settings.Width, settings.Height, settings.Lakes, settings.LakeSizeLower, settings.LakeSizeUpper, noiseImg)
|
lakeImage, lakePixels := GenerateLakes(settings.Width, settings.Height, settings.Lakes, settings.LakeSizeLower, settings.LakeSizeUpper, noiseImg, settings.Seed)
|
||||||
|
finalImage := lakeImage.(*image.RGBA)
|
||||||
|
GenerateTrees(finalImage, lakePixels, settings.MinTreeSize, settings.MaxTreeSize, settings.TreeCoverage, settings.TreeClumpiness, settings.Seed)
|
||||||
darkenedHeightmap := DarkenLakeAreas(noiseImg, lakePixels)
|
darkenedHeightmap := DarkenLakeAreas(noiseImg, lakePixels)
|
||||||
compositeImg := ApplyRoughness(darkenedHeightmap, settings.Roughness)
|
compositeImg := ApplyRoughness(darkenedHeightmap, settings.Roughness)
|
||||||
heightmapImg.Image = compositeImg
|
heightmapImg.Image = compositeImg
|
||||||
heightmapImg.Refresh()
|
heightmapImg.Refresh()
|
||||||
canvasImg.Image = canvasWithLakes
|
canvasImg.Image = finalImage
|
||||||
canvasImg.Refresh()
|
canvasImg.Refresh()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
widthEntry := widget.NewEntry()
|
||||||
|
widthEntry.SetText(strconv.Itoa(settings.Width))
|
||||||
|
widthEntry.OnChanged = func(s string) {
|
||||||
|
val, err := strconv.Atoi(s)
|
||||||
|
if err != nil {
|
||||||
|
errorLabel.Text = "Width must be a number"
|
||||||
|
errorLabel.Show()
|
||||||
|
generateBtn.Disable()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
errorLabel.Hide()
|
||||||
|
generateBtn.Enable()
|
||||||
|
settings.Width = val
|
||||||
|
}
|
||||||
|
heightEntry := widget.NewEntry()
|
||||||
|
heightEntry.SetText(strconv.Itoa(settings.Height))
|
||||||
|
heightEntry.OnChanged = func(s string) {
|
||||||
|
val, err := strconv.Atoi(s)
|
||||||
|
if err != nil {
|
||||||
|
errorLabel.Text = "Height must be a number"
|
||||||
|
errorLabel.Show()
|
||||||
|
generateBtn.Disable()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
errorLabel.Hide()
|
||||||
|
generateBtn.Enable()
|
||||||
|
settings.Height = val
|
||||||
|
}
|
||||||
|
|
||||||
|
seedEntry := widget.NewEntry()
|
||||||
|
seedEntry.SetText(strconv.FormatInt(settings.Seed, 10))
|
||||||
|
seedEntry.OnChanged = func(s string) {
|
||||||
|
val, err := strconv.ParseInt(s, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
errorLabel.Text = "Seed must be a number"
|
||||||
|
errorLabel.Show()
|
||||||
|
generateBtn.Disable()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
errorLabel.Hide()
|
||||||
|
generateBtn.Enable()
|
||||||
|
settings.Seed = val
|
||||||
|
}
|
||||||
|
|
||||||
|
randomizeBtn := widget.NewButton("Randomize", func() {
|
||||||
|
settings.Seed = time.Now().UnixNano()
|
||||||
|
seedEntry.SetText(strconv.FormatInt(settings.Seed, 10))
|
||||||
|
})
|
||||||
|
|
||||||
terrainTab := container.NewTabItem("Terrain", container.NewVBox(
|
terrainTab := container.NewTabItem("Terrain", container.NewVBox(
|
||||||
detailLabel,
|
detailLabel,
|
||||||
detailSlider,
|
detailSlider,
|
||||||
@@ -135,6 +275,14 @@ func main() {
|
|||||||
lakeSizeLowerSlider,
|
lakeSizeLowerSlider,
|
||||||
lakeSizeUpperLabel,
|
lakeSizeUpperLabel,
|
||||||
lakeSizeUpperSlider,
|
lakeSizeUpperSlider,
|
||||||
|
minTreeSizeLabel,
|
||||||
|
minTreeSizeSlider,
|
||||||
|
maxTreeSizeLabel,
|
||||||
|
maxTreeSizeSlider,
|
||||||
|
treeCoverageLabel,
|
||||||
|
treeCoverageSlider,
|
||||||
|
treeClumpinessLabel,
|
||||||
|
treeClumpinessSlider,
|
||||||
))
|
))
|
||||||
|
|
||||||
imageTab := container.NewTabItem("Image", container.NewVBox(
|
imageTab := container.NewTabItem("Image", container.NewVBox(
|
||||||
@@ -142,7 +290,11 @@ func main() {
|
|||||||
widthEntry,
|
widthEntry,
|
||||||
widget.NewLabel("Height:"),
|
widget.NewLabel("Height:"),
|
||||||
heightEntry,
|
heightEntry,
|
||||||
|
widget.NewLabel("Seed:"),
|
||||||
|
seedEntry,
|
||||||
|
randomizeBtn,
|
||||||
generateBtn,
|
generateBtn,
|
||||||
|
errorLabel,
|
||||||
))
|
))
|
||||||
|
|
||||||
tabs := container.NewAppTabs(
|
tabs := container.NewAppTabs(
|
||||||
|
|||||||
+20
-1
@@ -4,6 +4,7 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Settings struct {
|
type Settings struct {
|
||||||
@@ -14,6 +15,11 @@ type Settings struct {
|
|||||||
Lakes int `json:"lakes"`
|
Lakes int `json:"lakes"`
|
||||||
LakeSizeLower float64 `json:"lake_size_lower"`
|
LakeSizeLower float64 `json:"lake_size_lower"`
|
||||||
LakeSizeUpper float64 `json:"lake_size_upper"`
|
LakeSizeUpper float64 `json:"lake_size_upper"`
|
||||||
|
MinTreeSize float64 `json:"min_tree_size"`
|
||||||
|
MaxTreeSize float64 `json:"max_tree_size"`
|
||||||
|
TreeCoverage float64 `json:"tree_coverage"`
|
||||||
|
TreeClumpiness float64 `json:"tree_clumpiness"`
|
||||||
|
Seed int64 `json:"seed"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Settings) Save() error {
|
func (s *Settings) Save() error {
|
||||||
@@ -48,7 +54,20 @@ func LoadSettings() (*Settings, error) {
|
|||||||
file, err := os.Open(configFile)
|
file, err := os.Open(configFile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if os.IsNotExist(err) {
|
if os.IsNotExist(err) {
|
||||||
return &Settings{Detail: 1, Roughness: 0, Width: 300, Height: 300, Lakes: 0, LakeSizeLower: 1, LakeSizeUpper: 5}, nil
|
return &Settings{
|
||||||
|
Detail: 1,
|
||||||
|
Roughness: 0,
|
||||||
|
Width: 300,
|
||||||
|
Height: 300,
|
||||||
|
Lakes: 0,
|
||||||
|
LakeSizeLower: 1,
|
||||||
|
LakeSizeUpper: 5,
|
||||||
|
MinTreeSize: 5,
|
||||||
|
MaxTreeSize: 20,
|
||||||
|
TreeCoverage: 20,
|
||||||
|
TreeClumpiness: 50,
|
||||||
|
Seed: time.Now().UnixNano(),
|
||||||
|
}, nil
|
||||||
}
|
}
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
+103
-3
@@ -7,7 +7,6 @@ import (
|
|||||||
"image/draw"
|
"image/draw"
|
||||||
"math"
|
"math"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/aquilax/go-perlin"
|
"github.com/aquilax/go-perlin"
|
||||||
)
|
)
|
||||||
@@ -45,7 +44,7 @@ func (pq *priorityQueue) Pop() interface{} {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GenerateLakes creates a specific number of lakes by dividing the image into chunks and placing one lake per chunk.
|
// GenerateLakes creates a specific number of lakes by dividing the image into chunks and placing one lake per chunk.
|
||||||
func GenerateLakes(width, height, numLakes int, lakeSizeLower, lakeSizeUpper float64, heightmap image.Image) (image.Image, []image.Point) {
|
func GenerateLakes(width, height, numLakes int, lakeSizeLower, lakeSizeUpper float64, heightmap image.Image, seed int64) (image.Image, []image.Point) {
|
||||||
canvas := image.NewRGBA(image.Rect(0, 0, width, height))
|
canvas := image.NewRGBA(image.Rect(0, 0, width, height))
|
||||||
draw.Draw(canvas, canvas.Bounds(), image.NewUniform(color.White), image.Point{}, draw.Src)
|
draw.Draw(canvas, canvas.Bounds(), image.NewUniform(color.White), image.Point{}, draw.Src)
|
||||||
|
|
||||||
@@ -54,7 +53,7 @@ func GenerateLakes(width, height, numLakes int, lakeSizeLower, lakeSizeUpper flo
|
|||||||
}
|
}
|
||||||
|
|
||||||
var allLakePixels []image.Point
|
var allLakePixels []image.Point
|
||||||
randSrc := rand.New(rand.NewSource(time.Now().UnixNano()))
|
randSrc := rand.New(rand.NewSource(seed))
|
||||||
|
|
||||||
// 1. Divide the image into a grid
|
// 1. Divide the image into a grid
|
||||||
gridDim := int(math.Ceil(math.Sqrt(float64(numLakes))))
|
gridDim := int(math.Ceil(math.Sqrt(float64(numLakes))))
|
||||||
@@ -202,3 +201,104 @@ func DarkenLakeAreas(heightmap image.Image, lakePixels []image.Point) image.Imag
|
|||||||
|
|
||||||
return composite
|
return composite
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func GenerateTrees(img *image.RGBA, lakePixels []image.Point, minTreeSize, maxTreeSize, treeCoverage, treeClumpiness float64, seed int64) {
|
||||||
|
width := img.Bounds().Dx()
|
||||||
|
height := img.Bounds().Dy()
|
||||||
|
|
||||||
|
// 1. Calculate number of trees to place from coverage %.
|
||||||
|
avgTreeSize := (minTreeSize + maxTreeSize) / 2
|
||||||
|
if avgTreeSize <= 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
avgRadius := avgTreeSize / 2
|
||||||
|
avgTreeArea := math.Pi * avgRadius * avgRadius
|
||||||
|
if avgTreeArea == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
totalArea := float64(width * height)
|
||||||
|
targetTreePixels := totalArea * (treeCoverage / 100.0)
|
||||||
|
numTreesToPlace := int(targetTreePixels / avgTreeArea)
|
||||||
|
|
||||||
|
// 2. Generate a new noise map for tree placement.
|
||||||
|
treeNoise := perlin.NewPerlin(2, 2, 3, seed)
|
||||||
|
treeNoiseMap := image.NewGray(image.Rect(0, 0, width, height))
|
||||||
|
treeNoiseZoom := 0.05
|
||||||
|
for y := 0; y < height; y++ {
|
||||||
|
for x := 0; x < width; x++ {
|
||||||
|
val := treeNoise.Noise2D(float64(x)*treeNoiseZoom, float64(y)*treeNoiseZoom)
|
||||||
|
val = (val + 1) / 2 // Normalize to 0-1
|
||||||
|
treeNoiseMap.SetGray(x, y, color.Gray{Y: uint8(val * 255)})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
threshold := uint8(255 * (treeClumpiness / 100.0))
|
||||||
|
|
||||||
|
isLake := make(map[image.Point]bool)
|
||||||
|
for _, p := range lakePixels {
|
||||||
|
isLake[p] = true
|
||||||
|
}
|
||||||
|
|
||||||
|
treeCenters := make(map[image.Point]float64)
|
||||||
|
randSrc := rand.New(rand.NewSource(seed))
|
||||||
|
|
||||||
|
isValidCenter := func(p image.Point, r float64) bool {
|
||||||
|
if p.X-int(r) < 0 || p.X+int(r) >= width || p.Y-int(r) < 0 || p.Y+int(r) >= height {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if isLake[p] {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for center, r2 := range treeCenters {
|
||||||
|
dist := math.Sqrt(math.Pow(float64(p.X-center.X), 2) + math.Pow(float64(p.Y-center.Y), 2))
|
||||||
|
if dist < (r2+r)*0.75 { // Allow overlap
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Place trees in valid locations.
|
||||||
|
maxConsecutiveFails := 10000
|
||||||
|
consecutiveFails := 0
|
||||||
|
for len(treeCenters) < numTreesToPlace && consecutiveFails < maxConsecutiveFails {
|
||||||
|
p := image.Point{X: randSrc.Intn(width), Y: randSrc.Intn(height)}
|
||||||
|
|
||||||
|
// Check against noise map threshold
|
||||||
|
if treeNoiseMap.GrayAt(p.X, p.Y).Y < threshold {
|
||||||
|
consecutiveFails++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
size := minTreeSize + randSrc.Float64()*(maxTreeSize-minTreeSize)
|
||||||
|
if size <= 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
radius := size / 2
|
||||||
|
|
||||||
|
if isValidCenter(p, radius) {
|
||||||
|
treeCenters[p] = radius
|
||||||
|
consecutiveFails = 0
|
||||||
|
} else {
|
||||||
|
consecutiveFails++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Draw the trees.
|
||||||
|
for p, r := range treeCenters {
|
||||||
|
// Use a simple pixel-by-pixel circle drawing method
|
||||||
|
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()) || isLake[pt] {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if (math.Pow(float64(x-p.X), 2) + math.Pow(float64(y-p.Y), 2)) <= r*r {
|
||||||
|
// Blend the tree color with the background
|
||||||
|
// For simplicity, we just set a solid color for now.
|
||||||
|
img.Set(x, y, color.RGBA{R: 0, G: 100, B: 0, A: 255})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user