fixed up comments
This commit is contained in:
+33
-8
@@ -8,14 +8,18 @@ import (
|
||||
"sort"
|
||||
)
|
||||
|
||||
// GenerateBuildings creates and places buildings on the map.
|
||||
func GenerateBuildings(img *image.RGBA, width, height int, settings *Settings, roadPixels, allWaterPixels []image.Point, seed int64) []image.Point {
|
||||
// Early exit if no buildings are to be generated
|
||||
if settings.NumBuildings == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Initialize random number generator
|
||||
randSrc := rand.New(rand.NewSource(seed))
|
||||
buildingColor := color.RGBA{R: 128, G: 128, B: 128, A: 255} // Gray color for buildings
|
||||
|
||||
// Create lookup maps for water and road pixels for efficient collision detection
|
||||
isWater := make(map[image.Point]bool)
|
||||
for _, p := range allWaterPixels {
|
||||
isWater[p] = true
|
||||
@@ -26,9 +30,12 @@ func GenerateBuildings(img *image.RGBA, width, height int, settings *Settings, r
|
||||
isRoad[p] = true
|
||||
}
|
||||
|
||||
// Initialize building data structures
|
||||
isBuilding := make(map[image.Point]bool)
|
||||
var buildingPixels []image.Point
|
||||
var anchorPoints []image.Point
|
||||
|
||||
// Determine anchor points for building placement
|
||||
if len(roadPixels) > 0 {
|
||||
anchorPoints = roadPixels
|
||||
} else {
|
||||
@@ -42,16 +49,20 @@ func GenerateBuildings(img *image.RGBA, width, height int, settings *Settings, r
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Early exit if no anchor points are available
|
||||
if len(anchorPoints) == 0 {
|
||||
return nil
|
||||
}
|
||||
// Sort anchor points to have a deterministic order if needed, although we are selecting randomly
|
||||
// Sort anchor points for deterministic placement
|
||||
sort.Slice(anchorPoints, func(i, j int) bool {
|
||||
if anchorPoints[i].Y != anchorPoints[j].Y {
|
||||
return anchorPoints[i].Y < anchorPoints[j].Y
|
||||
}
|
||||
return anchorPoints[i].X < anchorPoints[j].X
|
||||
})
|
||||
|
||||
// Collect all land points for random placement
|
||||
landPoints := make([]image.Point, 0, width*height)
|
||||
for y := 0; y < height; y++ {
|
||||
for x := 0; x < width; x++ {
|
||||
@@ -61,25 +72,29 @@ func GenerateBuildings(img *image.RGBA, width, height int, settings *Settings, r
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Main loop for placing buildings
|
||||
buildingsPlaced := 0
|
||||
searchTries := 100 // Number of attempts to find a spot for a building
|
||||
searchTries := 100 // Number of attempts to find a spot for a building around an anchor
|
||||
maxPlacementAttempts := settings.NumBuildings * 5 // To prevent infinite loops
|
||||
|
||||
for buildingsPlaced < settings.NumBuildings && maxPlacementAttempts > 0 {
|
||||
maxPlacementAttempts--
|
||||
|
||||
// Select an anchor point for the new building
|
||||
var anchor image.Point
|
||||
if randSrc.Float64() > settings.BuildingDistribution/100.0 {
|
||||
// Place near roads
|
||||
// Place near roads or other existing features
|
||||
anchor = anchorPoints[randSrc.Intn(len(anchorPoints))]
|
||||
} else {
|
||||
// Place randomly on land
|
||||
// Place randomly on any available land
|
||||
if len(landPoints) == 0 {
|
||||
continue // No land to place buildings on
|
||||
}
|
||||
anchor = landPoints[randSrc.Intn(len(landPoints))]
|
||||
}
|
||||
|
||||
// Search for a valid building location around the anchor
|
||||
for i := 0; i < searchTries; i++ {
|
||||
searchRadius := float64(i) * 2.0 // Search in expanding circles
|
||||
angle := randSrc.Float64() * 2 * math.Pi
|
||||
@@ -88,6 +103,7 @@ func GenerateBuildings(img *image.RGBA, width, height int, settings *Settings, r
|
||||
X: anchor.X + int(dist*math.Cos(angle)),
|
||||
Y: anchor.Y + int(dist*math.Sin(angle)),
|
||||
}
|
||||
// For fully random distribution, pick any point on the map
|
||||
if settings.BuildingDistribution == 100 {
|
||||
center = image.Point{
|
||||
X: randSrc.Intn(width),
|
||||
@@ -95,36 +111,42 @@ func GenerateBuildings(img *image.RGBA, width, height int, settings *Settings, r
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure the center point is within the map boundaries
|
||||
if center.X < 0 || center.Y < 0 || center.X >= width || center.Y >= height {
|
||||
continue
|
||||
}
|
||||
|
||||
// Attempt to create a building at the selected center
|
||||
size := settings.MinBuildingSize + randSrc.Float64()*(settings.MaxBuildingSize-settings.MinBuildingSize)
|
||||
pixels, ok := getBuildingPixels(center, size, settings.BuildingShape, isWater, isRoad, isBuilding, width, height, randSrc)
|
||||
if ok {
|
||||
// If successful, draw the building and update data structures
|
||||
for _, p := range pixels {
|
||||
img.Set(p.X, p.Y, buildingColor)
|
||||
isBuilding[p] = true
|
||||
buildingPixels = append(buildingPixels, p)
|
||||
}
|
||||
buildingsPlaced++
|
||||
break // Found a spot, move to next building
|
||||
break // Move to the next building
|
||||
}
|
||||
}
|
||||
}
|
||||
return buildingPixels
|
||||
}
|
||||
|
||||
// getBuildingPixels determines the pixels for a single building based on its shape and checks for collisions.
|
||||
func getBuildingPixels(center image.Point, size float64, shape string, isWater, isRoad, isBuilding map[image.Point]bool, width, height int, randSrc *rand.Rand) ([]image.Point, bool) {
|
||||
var pixels []image.Point
|
||||
var halfSize = int(size / 2)
|
||||
|
||||
// Generate pixels based on the selected building shape
|
||||
switch shape {
|
||||
case "squares":
|
||||
for y := center.Y - halfSize; y <= center.Y+halfSize; y++ {
|
||||
for x := center.X - halfSize; x <= center.X+halfSize; x++ {
|
||||
p := image.Point{X: x, Y: y}
|
||||
if p.X < 0 || p.Y < 0 || p.X >= width || p.Y >= height || isWater[p] || isRoad[p] || isBuilding[p] {
|
||||
return nil, false
|
||||
return nil, false // Collision detected
|
||||
}
|
||||
pixels = append(pixels, p)
|
||||
}
|
||||
@@ -137,13 +159,14 @@ func getBuildingPixels(center image.Point, size float64, shape string, isWater,
|
||||
if dx*dx+dy*dy <= r2 {
|
||||
p := image.Point{X: x, Y: y}
|
||||
if p.X < 0 || p.Y < 0 || p.X >= width || p.Y >= height || isWater[p] || isRoad[p] || isBuilding[p] {
|
||||
return nil, false
|
||||
return nil, false // Collision detected
|
||||
}
|
||||
pixels = append(pixels, p)
|
||||
}
|
||||
}
|
||||
}
|
||||
case "rectangles":
|
||||
// Create rectangles with varied aspect ratios
|
||||
longSide := size
|
||||
shortSide := randSrc.Float64()*(size-float64(halfSize)) + float64(halfSize)
|
||||
var w, h int
|
||||
@@ -154,17 +177,19 @@ func getBuildingPixels(center image.Point, size float64, shape string, isWater,
|
||||
}
|
||||
halfW, halfH := w/2, h/2
|
||||
|
||||
// Check for collisions and gather pixels
|
||||
for y := center.Y - halfH; y <= center.Y+halfH; y++ {
|
||||
for x := center.X - halfW; x <= center.X+halfW; x++ {
|
||||
p := image.Point{X: x, Y: y}
|
||||
if p.X < 0 || p.Y < 0 || p.X >= width || p.Y >= height || isWater[p] || isRoad[p] || isBuilding[p] {
|
||||
return nil, false
|
||||
return nil, false // Collision detected
|
||||
}
|
||||
pixels = append(pixels, p)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Final check to ensure pixels were generated
|
||||
if len(pixels) == 0 {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
+10
-1
@@ -6,6 +6,7 @@ import (
|
||||
)
|
||||
|
||||
// TextOverlayProgressBar is a custom widget that displays a progress bar with text overlay.
|
||||
// This allows showing progress information (e.g., step description) directly on the progress bar.
|
||||
type TextOverlayProgressBar struct {
|
||||
widget.BaseWidget
|
||||
progressBar *widget.ProgressBar
|
||||
@@ -22,7 +23,7 @@ func NewTextOverlayProgressBar() *TextOverlayProgressBar {
|
||||
return p
|
||||
}
|
||||
|
||||
// SetValue sets the progress value.
|
||||
// SetValue sets the progress value of the underlying progress bar.
|
||||
func (p *TextOverlayProgressBar) SetValue(v float64) {
|
||||
p.progressBar.SetValue(v)
|
||||
}
|
||||
@@ -41,31 +42,38 @@ func (p *TextOverlayProgressBar) CreateRenderer() fyne.WidgetRenderer {
|
||||
}
|
||||
}
|
||||
|
||||
// textOverlayProgressBarRenderer is the renderer for the TextOverlayProgressBar.
|
||||
// It handles the layout and rendering of the progress bar and the overlay text.
|
||||
type textOverlayProgressBarRenderer struct {
|
||||
progressBar *widget.ProgressBar
|
||||
label *widget.Label
|
||||
objects []fyne.CanvasObject
|
||||
}
|
||||
|
||||
// Layout defines the size and position of the progress bar and the label.
|
||||
func (r *textOverlayProgressBarRenderer) Layout(size fyne.Size) {
|
||||
r.progressBar.Resize(size)
|
||||
r.label.Resize(size)
|
||||
r.label.Move(fyne.NewPos(0, 0))
|
||||
}
|
||||
|
||||
// MinSize returns the minimum size of the widget.
|
||||
func (r *textOverlayProgressBarRenderer) MinSize() fyne.Size {
|
||||
return r.progressBar.MinSize()
|
||||
}
|
||||
|
||||
// Refresh redraws the widget.
|
||||
func (r *textOverlayProgressBarRenderer) Refresh() {
|
||||
r.progressBar.Refresh()
|
||||
r.label.Refresh()
|
||||
}
|
||||
|
||||
// Objects returns the canvas objects that make up the widget.
|
||||
func (r *textOverlayProgressBarRenderer) Objects() []fyne.CanvasObject {
|
||||
return r.objects
|
||||
}
|
||||
|
||||
// Destroy is a no-op for this renderer.
|
||||
func (r *textOverlayProgressBarRenderer) Destroy() {}
|
||||
|
||||
// CustomTheme is a custom theme to make the progress bar thinner.
|
||||
@@ -79,6 +87,7 @@ func NewCustomTheme(theme fyne.Theme) *CustomTheme {
|
||||
}
|
||||
|
||||
// Size returns the size for a given themeable item.
|
||||
// It overrides the default progress bar height to make it thinner.
|
||||
func (t *CustomTheme) Size(name fyne.ThemeSizeName) float32 {
|
||||
if name == "progressBar.height" {
|
||||
return 10
|
||||
|
||||
@@ -28,11 +28,13 @@ import (
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Initialize the Fyne application and window
|
||||
a := app.NewWithID("com.example.rpgcitymaker")
|
||||
a.Settings().SetTheme(&CustomTheme{a.Settings().Theme()})
|
||||
w := a.NewWindow("RPG City Maker")
|
||||
w.Resize(fyne.NewSize(800, 600))
|
||||
|
||||
// Load settings from file, or use defaults if loading fails
|
||||
settings, err := LoadSettings()
|
||||
if err != nil {
|
||||
log.Println("Error loading settings:", err)
|
||||
@@ -40,14 +42,15 @@ func main() {
|
||||
settings = &Settings{Detail: 1, Roughness: 0, Width: 300, Height: 300, Lakes: 0, LakeSizeLower: 1, LakeSizeUpper: 5}
|
||||
}
|
||||
|
||||
// Create canvas objects for displaying the generated images
|
||||
canvasImg := &canvas.Image{
|
||||
FillMode: canvas.ImageFillContain,
|
||||
}
|
||||
|
||||
heightmapImg := &canvas.Image{
|
||||
FillMode: canvas.ImageFillContain,
|
||||
}
|
||||
|
||||
// Initialize slices to store generated map features
|
||||
var lakes [][]image.Point
|
||||
var riverPixels []image.Point
|
||||
var treePixels []image.Point
|
||||
@@ -55,6 +58,7 @@ func main() {
|
||||
var roadPixels []image.Point
|
||||
var bridgePixels []image.Point
|
||||
|
||||
// Set up application configuration directory
|
||||
configDir, err := os.UserConfigDir()
|
||||
if err != nil {
|
||||
log.Fatal("Failed to get user config dir:", err)
|
||||
@@ -62,6 +66,8 @@ func main() {
|
||||
appConfigDir := filepath.Join(configDir, "rpgcitymakerreborn")
|
||||
canvasPath := filepath.Join(appConfigDir, "canvas.png")
|
||||
heightmapPath := filepath.Join(appConfigDir, "heightmap.png")
|
||||
|
||||
// Save settings and images on window close
|
||||
w.SetOnClosed(func() {
|
||||
if err := settings.Save(); err != nil {
|
||||
log.Println("Error saving settings:", err)
|
||||
@@ -91,6 +97,8 @@ func main() {
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// Load previously saved images
|
||||
canvasFile, err := os.Open(canvasPath)
|
||||
if err == nil {
|
||||
defer canvasFile.Close()
|
||||
@@ -109,16 +117,19 @@ func main() {
|
||||
}
|
||||
}
|
||||
|
||||
// Generate initial images if none are loaded
|
||||
if canvasImg.Image == nil || heightmapImg.Image == nil {
|
||||
|
||||
// Initial image generation
|
||||
// Step 1: Generating Heightmap
|
||||
seedProvider := NewSeedProvider(settings.Seed)
|
||||
noiseImg := GenerateHeightmap(settings.Width, settings.Height, int(settings.Detail), 100.0, seedProvider.Next())
|
||||
|
||||
// Step 2: Generating Lakes
|
||||
var lakeImage image.Image
|
||||
|
||||
lakeImage, lakes = GenerateLakes(settings.Width, settings.Height, settings.Lakes, settings.LakeSizeLower, settings.LakeSizeUpper, noiseImg, seedProvider.Next())
|
||||
|
||||
// Step 3: Generating Rivers
|
||||
var riverImage image.Image
|
||||
|
||||
riverImage, riverPixels = GenerateRivers(settings.Width, settings.Height, settings.Rivers, settings.MinRiverWidth, settings.MaxRiverWidth, settings.RiverCurvyness, lakeImage, lakes, seedProvider.Next(), noiseImg)
|
||||
@@ -136,6 +147,7 @@ func main() {
|
||||
}
|
||||
|
||||
allWaterPixels := append(flatLakePixels, riverPixels...)
|
||||
// Step 4: Generating Roads
|
||||
roadPixels, bridgePixels, roadImage = GenerateRoads(settings.Width, settings.Height, settings, noiseImg, allWaterPixels, seedProvider.Next())
|
||||
for y := 0; y < roadImage.Bounds().Max.Y; y++ {
|
||||
for x := 0; x < roadImage.Bounds().Max.X; x++ {
|
||||
@@ -146,14 +158,19 @@ func main() {
|
||||
}
|
||||
}
|
||||
|
||||
// Step 5: Generating Buildings
|
||||
buildingPixels = GenerateBuildings(finalImage, settings.Width, settings.Height, settings, roadPixels, allWaterPixels, seedProvider.Next())
|
||||
|
||||
// Step 6: Generating Trees
|
||||
treePixels = GenerateTrees(finalImage, allWaterPixels, roadPixels, buildingPixels, settings.MinTreeSize, settings.MaxTreeSize, settings.TreeCoverage, settings.TreeClumpiness, seedProvider.Next())
|
||||
|
||||
// Step 7: Darkening Water Areas
|
||||
darkenedHeightmap := DarkenLakeAreas(noiseImg, allWaterPixels)
|
||||
|
||||
// Step 8: Flattening Road Areas
|
||||
flattenedHeightmap := FlattenRoadAreas(darkenedHeightmap, roadPixels)
|
||||
|
||||
// Step 9: Applying Roughness
|
||||
compositeImg := ApplyRoughness(flattenedHeightmap, settings.Roughness)
|
||||
|
||||
heightmapImg.Image = compositeImg
|
||||
@@ -161,7 +178,7 @@ func main() {
|
||||
canvasImg.Image = finalImage
|
||||
|
||||
}
|
||||
|
||||
// Create UI elements for controlling terrain generation settings
|
||||
detailLabel := widget.NewLabel(fmt.Sprintf("Detail: %.0f", settings.Detail))
|
||||
detailSlider := widget.NewSlider(1, 16)
|
||||
detailSlider.OnChanged = func(val float64) {
|
||||
@@ -177,7 +194,7 @@ func main() {
|
||||
roughnessLabel.SetText(fmt.Sprintf("Roughness: %.0f%%", settings.Roughness))
|
||||
}
|
||||
roughnessSlider.SetValue(settings.Roughness)
|
||||
|
||||
// Create UI elements for controlling lake generation settings
|
||||
lakesLabel := widget.NewLabel(fmt.Sprintf("Lakes: %d", settings.Lakes))
|
||||
lakesSlider := widget.NewSlider(0, 15)
|
||||
lakesSlider.OnChanged = func(val float64) {
|
||||
@@ -210,7 +227,7 @@ func main() {
|
||||
lakeSizeUpperLabel.SetText(fmt.Sprintf("Max Lake Size: %.0f%%", settings.LakeSizeUpper))
|
||||
}
|
||||
lakeSizeUpperSlider.SetValue(settings.LakeSizeUpper)
|
||||
|
||||
// Create UI elements for controlling river generation settings
|
||||
riversLabel := widget.NewLabel(fmt.Sprintf("Rivers: %d", settings.Rivers))
|
||||
riversSlider := widget.NewSlider(0, 5)
|
||||
riversSlider.OnChanged = func(val float64) {
|
||||
@@ -251,7 +268,7 @@ func main() {
|
||||
riverCurvynessLabel.SetText(fmt.Sprintf("River Curvyness: %.0f%%", settings.RiverCurvyness))
|
||||
}
|
||||
riverCurvynessSlider.SetValue(settings.RiverCurvyness)
|
||||
|
||||
// Create UI elements for controlling tree generation settings
|
||||
minTreeSizeLabel := widget.NewLabel(fmt.Sprintf("Min Tree Size: %.0fpx", settings.MinTreeSize))
|
||||
minTreeSizeSlider := widget.NewSlider(1, 150)
|
||||
maxTreeSizeLabel := widget.NewLabel(fmt.Sprintf("Max Tree Size: %.0fpx", settings.MaxTreeSize))
|
||||
@@ -292,7 +309,7 @@ func main() {
|
||||
treeClumpinessLabel.SetText(fmt.Sprintf("Tree Clumpiness: %.0f%%", settings.TreeClumpiness))
|
||||
}
|
||||
treeClumpinessSlider.SetValue(settings.TreeClumpiness)
|
||||
|
||||
// Create UI elements for controlling road generation settings
|
||||
numRoadsLabel := widget.NewLabel(fmt.Sprintf("Number of Roads: %d", settings.NumRoads))
|
||||
numRoadsSlider := widget.NewSlider(0, 2000)
|
||||
numRoadsSlider.OnChanged = func(val float64) {
|
||||
@@ -354,6 +371,7 @@ func main() {
|
||||
}
|
||||
roadDistributionSlider.SetValue(settings.RoadDistribution)
|
||||
|
||||
// Create UI elements for error display and action buttons
|
||||
errorLabel := canvas.NewText("", color.RGBA{R: 255, A: 255})
|
||||
errorLabel.TextSize = 12
|
||||
errorLabel.Hide()
|
||||
@@ -369,18 +387,20 @@ func main() {
|
||||
exportMasksBtn := widget.NewButton("Export Masks", func() {
|
||||
showMasksSaveDialog(w, canvasImg.Image, heightmapImg.Image, settings, lakes, riverPixels, treePixels, roadPixels, bridgePixels, buildingPixels)
|
||||
})
|
||||
|
||||
// Main generation button and logic
|
||||
generateBtn = widget.NewButton("Generate", func() {
|
||||
go func() {
|
||||
// Disable button during generation
|
||||
fyne.Do(func() {
|
||||
generateBtn.Disable()
|
||||
})
|
||||
defer func() {
|
||||
// Re-enable button after generation
|
||||
fyne.Do(func() {
|
||||
generateBtn.Enable()
|
||||
})
|
||||
}()
|
||||
|
||||
// Set up progress bar
|
||||
steps := 9
|
||||
currentStep := 0
|
||||
|
||||
@@ -483,7 +503,7 @@ func main() {
|
||||
})
|
||||
}()
|
||||
})
|
||||
|
||||
// Create UI elements for image dimensions and seed
|
||||
widthEntry := widget.NewEntry()
|
||||
widthEntry.SetText(strconv.Itoa(settings.Width))
|
||||
widthEntry.OnChanged = func(s string) {
|
||||
@@ -532,7 +552,7 @@ func main() {
|
||||
settings.Seed = time.Now().UnixNano()
|
||||
seedEntry.SetText(strconv.FormatInt(settings.Seed, 10))
|
||||
})
|
||||
|
||||
// Create tabs for organizing settings
|
||||
terrainTab := container.NewTabItem("Terrain", container.NewVBox(
|
||||
detailLabel,
|
||||
detailSlider,
|
||||
@@ -587,7 +607,7 @@ func main() {
|
||||
roadDistributionLabel,
|
||||
roadDistributionSlider,
|
||||
))
|
||||
|
||||
// Create UI elements for building generation settings
|
||||
numBuildingsLabel := widget.NewLabel(fmt.Sprintf("Number of Buildings: %d", settings.NumBuildings))
|
||||
numBuildingsSlider := widget.NewSlider(0, 10000)
|
||||
numBuildingsSlider.OnChanged = func(val float64) {
|
||||
@@ -663,7 +683,7 @@ func main() {
|
||||
exportHeightmapBtn,
|
||||
exportMasksBtn,
|
||||
))
|
||||
|
||||
// Create the main layout using a horizontal split
|
||||
tabs := container.NewAppTabs(
|
||||
imageTab,
|
||||
terrainTab,
|
||||
@@ -687,10 +707,12 @@ func main() {
|
||||
right,
|
||||
)
|
||||
split.SetOffset(0.3)
|
||||
|
||||
// Set the window content and start the application
|
||||
w.SetContent(split)
|
||||
w.ShowAndRun()
|
||||
}
|
||||
|
||||
// getImageData encodes an image to the specified format and returns the data as a byte buffer.
|
||||
func getImageData(img image.Image, format string) (*bytes.Buffer, error) {
|
||||
buf := new(bytes.Buffer)
|
||||
var err error
|
||||
@@ -705,7 +727,9 @@ func getImageData(img image.Image, format string) (*bytes.Buffer, error) {
|
||||
return buf, err
|
||||
}
|
||||
|
||||
// showMasksSaveDialog displays a dialog for saving the generated masks.
|
||||
func showMasksSaveDialog(win fyne.Window, canvasImg, heightmapImg image.Image, settings *Settings, lakes [][]image.Point, riverPixels, treePixels, roadPixels, bridgePixels, buildingPixels []image.Point) {
|
||||
// Create UI elements for the save dialog
|
||||
fileNameEntry := widget.NewEntry()
|
||||
fileNameEntry.SetPlaceHolder("masks_folder")
|
||||
|
||||
@@ -752,7 +776,7 @@ func showMasksSaveDialog(win fyne.Window, canvasImg, heightmapImg image.Image, s
|
||||
imgFormat := strings.ToLower(formatSelect.Selected)
|
||||
bounds := canvasImg.Bounds()
|
||||
|
||||
// Create mask images
|
||||
// Create mask images from the generated data
|
||||
lakeMask := image.NewGray(bounds)
|
||||
for _, lake := range lakes {
|
||||
for _, p := range lake {
|
||||
@@ -790,7 +814,7 @@ func showMasksSaveDialog(win fyne.Window, canvasImg, heightmapImg image.Image, s
|
||||
"bridges_mask." + imgFormat: bridgeMask,
|
||||
"buildings_mask." + imgFormat: buildingMask,
|
||||
}
|
||||
|
||||
// Save the images based on the selected packaging option
|
||||
switch packageSelect.Selected {
|
||||
case "Folder":
|
||||
exportPath := filepath.Join(pathLabel.Text, folderName)
|
||||
@@ -888,6 +912,7 @@ func showMasksSaveDialog(win fyne.Window, canvasImg, heightmapImg image.Image, s
|
||||
saveDialog.Show()
|
||||
}
|
||||
|
||||
// saveImage saves an image to the specified path.
|
||||
func saveImage(img image.Image, path string) {
|
||||
file, err := os.Create(path)
|
||||
if err != nil {
|
||||
@@ -910,7 +935,9 @@ func saveImage(img image.Image, path string) {
|
||||
}
|
||||
}
|
||||
|
||||
// showSaveDialog displays a dialog for saving an image.
|
||||
func showSaveDialog(win fyne.Window, img image.Image, settings *Settings) {
|
||||
// Create UI elements for the save dialog
|
||||
fileNameEntry := widget.NewEntry()
|
||||
fileNameEntry.SetPlaceHolder("image")
|
||||
|
||||
|
||||
@@ -11,17 +11,20 @@ import (
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
// PointOfInterest represents a location on the map where roads may start, end, or intersect.
|
||||
type PointOfInterest struct {
|
||||
X, Y int
|
||||
Connections int
|
||||
IsExit bool
|
||||
}
|
||||
|
||||
// PathPoint represents a single point in a road's path, with a flag to indicate if it's a bridge.
|
||||
type PathPoint struct {
|
||||
Point image.Point
|
||||
IsBridge bool
|
||||
}
|
||||
|
||||
// Road represents a connection between two Points of Interest.
|
||||
type Road struct {
|
||||
Start, End *PointOfInterest
|
||||
Width int
|
||||
@@ -29,27 +32,33 @@ type Road struct {
|
||||
Importance int
|
||||
}
|
||||
|
||||
// GenerateRoads is the main function for creating roads on the map.
|
||||
func GenerateRoads(width, height int, settings *Settings, noiseImg image.Image, allWaterPixels []image.Point, seed int64) ([]image.Point, []image.Point, *image.RGBA) {
|
||||
// Step 1: Initialize a transparent image for drawing roads
|
||||
img := image.NewRGBA(image.Rect(0, 0, width, height))
|
||||
// Transparent background
|
||||
for y := 0; y < height; y++ {
|
||||
for x := 0; x < width; x++ {
|
||||
img.Set(x, y, color.Transparent)
|
||||
}
|
||||
}
|
||||
|
||||
// Step 2: Set up random number generator and colors
|
||||
randSrc := rand.New(rand.NewSource(seed))
|
||||
roadColor := color.RGBA{R: 139, G: 69, B: 19, A: 255}
|
||||
bridgeColor := color.RGBA{R: 60, G: 42, B: 33, A: 255}
|
||||
|
||||
// Step 3: Generate Points of Interest (POIs)
|
||||
pois := generatePOIs(width, height, settings, allWaterPixels, randSrc)
|
||||
if len(pois) == 0 {
|
||||
return nil, nil, img
|
||||
}
|
||||
|
||||
// Step 4: Connect POIs to form roads
|
||||
roads := connectPOIs(pois, width, height, settings, randSrc, allWaterPixels)
|
||||
// Step 5: Assign widths to the roads based on their importance
|
||||
assignRoadWidths(roads, settings)
|
||||
|
||||
// Step 6: Draw the roads on the image
|
||||
var allRoadPixels []image.Point
|
||||
var allBridgePixels []image.Point
|
||||
for _, road := range roads {
|
||||
@@ -61,6 +70,7 @@ func GenerateRoads(width, height int, settings *Settings, noiseImg image.Image,
|
||||
return allRoadPixels, allBridgePixels, img
|
||||
}
|
||||
|
||||
// generatePOIs creates the initial set of points where roads will originate.
|
||||
func generatePOIs(width, height int, settings *Settings, allWaterPixels []image.Point, randSrc *rand.Rand) []*PointOfInterest {
|
||||
numPOIs := settings.NumRoads / 2
|
||||
if numPOIs == 0 {
|
||||
@@ -81,15 +91,16 @@ func generatePOIs(width, height int, settings *Settings, allWaterPixels []image.
|
||||
centerX := width / 2
|
||||
centerY := height / 2
|
||||
|
||||
// Distribution affects the radius
|
||||
// Distribution affects the radius of POI generation
|
||||
maxRadius := math.Min(float64(width)/2, float64(height)/2)
|
||||
radius := maxRadius * (settings.RoadDistribution / 100.0)
|
||||
|
||||
for i := 0; i < numPOIs; i++ {
|
||||
var x, y int
|
||||
found := false
|
||||
for j := 0; j < 100; j++ { // 100 retries to find a land spot
|
||||
for j := 0; j < 100; j++ { // Retries to find a land spot
|
||||
if i < numExits {
|
||||
// Create POIs at the map edges
|
||||
side := randSrc.Intn(4)
|
||||
switch side {
|
||||
case 0: // Top
|
||||
@@ -106,6 +117,7 @@ func generatePOIs(width, height int, settings *Settings, allWaterPixels []image.
|
||||
y = randSrc.Intn(height)
|
||||
}
|
||||
} else {
|
||||
// Create POIs within the map
|
||||
angle := randSrc.Float64() * 2 * math.Pi
|
||||
r := math.Sqrt(randSrc.Float64()) * radius
|
||||
x = int(float64(centerX) + r*math.Cos(angle))
|
||||
@@ -125,6 +137,8 @@ func generatePOIs(width, height int, settings *Settings, allWaterPixels []image.
|
||||
|
||||
return pois
|
||||
}
|
||||
|
||||
// connectPOIs creates roads by connecting the generated Points of Interest.
|
||||
func connectPOIs(pois []*PointOfInterest, width, height int, settings *Settings, randSrc *rand.Rand, allWaterPixels []image.Point) []*Road {
|
||||
if len(pois) < 2 {
|
||||
return nil
|
||||
@@ -137,7 +151,7 @@ func connectPOIs(pois []*PointOfInterest, width, height int, settings *Settings,
|
||||
visited := make(map[*PointOfInterest]bool)
|
||||
existingRoads := make(map[string]bool)
|
||||
|
||||
// Find the center-most POI
|
||||
// Find the center-most POI to start connecting from
|
||||
centerX := width / 2
|
||||
centerY := height / 2
|
||||
var startNode *PointOfInterest
|
||||
@@ -160,9 +174,11 @@ func connectPOIs(pois []*PointOfInterest, width, height int, settings *Settings,
|
||||
|
||||
visited[startNode] = true
|
||||
|
||||
// Use average dimension for controlling road path calculation
|
||||
avgDim := float64(width+height) / 2.0
|
||||
numControlPoints := max(int(avgDim*0.03), 60)
|
||||
|
||||
// Connect all POIs using a minimum spanning tree-like algorithm
|
||||
for len(visited) < len(pois) {
|
||||
var closest *PointOfInterest
|
||||
var fromNode *PointOfInterest
|
||||
@@ -176,7 +192,7 @@ func connectPOIs(pois []*PointOfInterest, width, height int, settings *Settings,
|
||||
if !visited[other] {
|
||||
dist := math.Sqrt(math.Pow(float64(poi.X-other.X), 2) + math.Pow(float64(poi.Y-other.Y), 2))
|
||||
|
||||
// Check if road exists
|
||||
// Check if a road already exists between these two POIs
|
||||
key := fmt.Sprintf("%p-%p", poi, other)
|
||||
if uintptr(unsafe.Pointer(poi)) > uintptr(unsafe.Pointer(other)) {
|
||||
key = fmt.Sprintf("%p-%p", other, poi)
|
||||
@@ -185,7 +201,7 @@ func connectPOIs(pois []*PointOfInterest, width, height int, settings *Settings,
|
||||
continue
|
||||
}
|
||||
|
||||
// Don't connect two exit points
|
||||
// Avoid connecting two exit points directly
|
||||
if poi.IsExit && other.IsExit {
|
||||
continue
|
||||
}
|
||||
@@ -204,7 +220,7 @@ func connectPOIs(pois []*PointOfInterest, width, height int, settings *Settings,
|
||||
fromNode.Connections++
|
||||
closest.Connections++
|
||||
|
||||
// Add road to existing roads map
|
||||
// Add road to existing roads map to prevent duplicates
|
||||
key := fmt.Sprintf("%p-%p", fromNode, closest)
|
||||
if uintptr(unsafe.Pointer(fromNode)) > uintptr(unsafe.Pointer(closest)) {
|
||||
key = fmt.Sprintf("%p-%p", closest, fromNode)
|
||||
@@ -222,7 +238,7 @@ func connectPOIs(pois []*PointOfInterest, width, height int, settings *Settings,
|
||||
}
|
||||
}(fromNode, closest)
|
||||
} else {
|
||||
// No more reachable POIs
|
||||
// No more reachable POIs, break the loop
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -236,6 +252,7 @@ func connectPOIs(pois []*PointOfInterest, width, height int, settings *Settings,
|
||||
roads = append(roads, road)
|
||||
}
|
||||
|
||||
// Calculate road importance based on the number of connections at its endpoints
|
||||
for _, road := range roads {
|
||||
road.Importance = road.Start.Connections + road.End.Connections
|
||||
}
|
||||
@@ -243,11 +260,13 @@ func connectPOIs(pois []*PointOfInterest, width, height int, settings *Settings,
|
||||
return roads
|
||||
}
|
||||
|
||||
// assignRoadWidths sets the width of each road based on its importance.
|
||||
func assignRoadWidths(roads []*Road, settings *Settings) {
|
||||
if len(roads) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
// Sort roads by importance in descending order
|
||||
sort.Slice(roads, func(i, j int) bool {
|
||||
return roads[i].Importance > roads[j].Importance
|
||||
})
|
||||
@@ -259,11 +278,13 @@ func assignRoadWidths(roads []*Road, settings *Settings) {
|
||||
widthStep = (maxWidth - minWidth) / float64(len(roads)-1)
|
||||
}
|
||||
|
||||
// Assign widths, with more important roads being wider
|
||||
for i, road := range roads {
|
||||
road.Width = int(maxWidth - float64(i)*widthStep)
|
||||
}
|
||||
}
|
||||
|
||||
// drawRoad draws a single road on the image, including bridges.
|
||||
func drawRoad(img *image.RGBA, points []PathPoint, roadColor, bridgeColor color.Color, width int) ([]image.Point, []image.Point) {
|
||||
var roadPixels []image.Point
|
||||
var bridgePixels []image.Point
|
||||
@@ -285,6 +306,7 @@ func drawRoad(img *image.RGBA, points []PathPoint, roadColor, bridgeColor color.
|
||||
return roadPixels, bridgePixels
|
||||
}
|
||||
|
||||
// bresenhamRoad uses Bresenham's line algorithm to create a path between control points.
|
||||
func bresenhamRoad(path []image.Point) []image.Point {
|
||||
if len(path) < 2 {
|
||||
return path
|
||||
@@ -324,6 +346,7 @@ func bresenhamRoad(path []image.Point) []image.Point {
|
||||
return fullPath
|
||||
}
|
||||
|
||||
// calculateRoadPath computes the path for a road, including curves and bridges.
|
||||
func calculateRoadPath(start, end *PointOfInterest, curvyness, avgDim float64, randSrc *rand.Rand, numControlPoints int, allWaterPixels []image.Point) []PathPoint {
|
||||
dx := end.X - start.X
|
||||
dy := end.Y - start.Y
|
||||
@@ -338,7 +361,7 @@ func calculateRoadPath(start, end *PointOfInterest, curvyness, avgDim float64, r
|
||||
return []PathPoint{{Point: image.Point{X: start.X, Y: start.Y}, IsBridge: waterMap[image.Point{X: start.X, Y: start.Y}]}}
|
||||
}
|
||||
|
||||
// Adjust curviness based on distance
|
||||
// Adjust curviness based on the distance between the POIs
|
||||
distanceFactor := math.Min(1.0, dist/(avgDim*0.5))
|
||||
adjustedCurvyness := curvyness * distanceFactor
|
||||
|
||||
@@ -351,6 +374,7 @@ func calculateRoadPath(start, end *PointOfInterest, curvyness, avgDim float64, r
|
||||
return pathPoints
|
||||
}
|
||||
|
||||
// Use sine waves to create curves in the road
|
||||
type wave struct {
|
||||
amplitude float64
|
||||
numWaves float64
|
||||
@@ -365,20 +389,21 @@ func calculateRoadPath(start, end *PointOfInterest, curvyness, avgDim float64, r
|
||||
}
|
||||
baseNumWaves := (dist / mainWavelength) * adjustedCurvyness
|
||||
|
||||
// Main wave
|
||||
// Main wave for overall curve
|
||||
waves[0] = wave{
|
||||
amplitude: amp,
|
||||
numWaves: baseNumWaves * (0.75 + randSrc.Float64()*0.5),
|
||||
phase: randSrc.Float64() * 2 * math.Pi,
|
||||
}
|
||||
|
||||
// Smaller wave for detours
|
||||
// Smaller wave for minor detours and a more natural look
|
||||
waves[1] = wave{
|
||||
amplitude: amp / 4,
|
||||
numWaves: baseNumWaves * 4 * (0.75 + randSrc.Float64()*0.5),
|
||||
phase: randSrc.Float64() * 2 * math.Pi,
|
||||
}
|
||||
|
||||
// Generate control points for the curve
|
||||
controlPoints := make([]image.Point, numControlPoints+1)
|
||||
for i := 0; i <= numControlPoints; i++ {
|
||||
t := float64(i) / float64(numControlPoints)
|
||||
@@ -401,6 +426,7 @@ func calculateRoadPath(start, end *PointOfInterest, curvyness, avgDim float64, r
|
||||
controlPoints[i] = image.Point{X: int(math.Round(x)), Y: int(math.Round(y))}
|
||||
}
|
||||
|
||||
// Create the final path using Bresenham's algorithm between control points
|
||||
points := bresenhamRoad(controlPoints)
|
||||
pathPoints := make([]PathPoint, len(points))
|
||||
for i, p := range points {
|
||||
@@ -409,7 +435,7 @@ func calculateRoadPath(start, end *PointOfInterest, curvyness, avgDim float64, r
|
||||
return pathPoints
|
||||
}
|
||||
|
||||
// Bresenham's line algorithm for drawing segments of the curve
|
||||
// drawLine draws a line with a specified width on the image.
|
||||
func drawLine(img *image.RGBA, x0, y0, x1, y1 int, col color.Color, width int) []image.Point {
|
||||
var points []image.Point
|
||||
dx := abs(x1 - x0)
|
||||
@@ -452,6 +478,7 @@ func drawLine(img *image.RGBA, x0, y0, x1, y1 int, col color.Color, width int) [
|
||||
return points
|
||||
}
|
||||
|
||||
// abs returns the absolute value of an integer.
|
||||
func abs(x int) int {
|
||||
if x < 0 {
|
||||
return -x
|
||||
|
||||
@@ -2,16 +2,21 @@ package main
|
||||
|
||||
import "math/rand"
|
||||
|
||||
// SeedProvider is a simple struct that provides a stream of random seeds
|
||||
// from a single initial seed. This ensures that the entire map generation
|
||||
// process is deterministic if the same initial seed is used.
|
||||
type SeedProvider struct {
|
||||
rand *rand.Rand
|
||||
}
|
||||
|
||||
// NewSeedProvider creates a new SeedProvider with the given initial seed.
|
||||
func NewSeedProvider(seed int64) *SeedProvider {
|
||||
return &SeedProvider{
|
||||
rand: rand.New(rand.NewSource(seed)),
|
||||
}
|
||||
}
|
||||
|
||||
// Next returns the next random seed in the sequence.
|
||||
func (sp *SeedProvider) Next() int64 {
|
||||
return sp.rand.Int63()
|
||||
}
|
||||
|
||||
+47
-23
@@ -7,48 +7,64 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// Settings holds all the user-configurable parameters for map generation.
|
||||
type Settings struct {
|
||||
Detail float64 `json:"detail"`
|
||||
Roughness float64 `json:"roughness"`
|
||||
Width int `json:"width"`
|
||||
Height int `json:"height"`
|
||||
Lakes int `json:"lakes"`
|
||||
LakeSizeLower float64 `json:"lake_size_lower"`
|
||||
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"`
|
||||
Rivers int `json:"rivers"`
|
||||
MinRiverWidth float64 `json:"min_river_width"`
|
||||
MaxRiverWidth float64 `json:"max_river_width"`
|
||||
RiverCurvyness float64 `json:"river_curvyness"`
|
||||
NumRoads int `json:"num_roads"`
|
||||
MinRoadWidth float64 `json:"min_road_width"`
|
||||
MaxRoadWidth float64 `json:"max_road_width"`
|
||||
RoadExits int `json:"road_exits"`
|
||||
RoadCurvyness float64 `json:"road_curvyness"`
|
||||
RoadDistribution float64 `json:"road_distribution"`
|
||||
// Terrain settings
|
||||
Detail float64 `json:"detail"`
|
||||
Roughness float64 `json:"roughness"`
|
||||
Width int `json:"width"`
|
||||
Height int `json:"height"`
|
||||
|
||||
// Water settings
|
||||
Lakes int `json:"lakes"`
|
||||
LakeSizeLower float64 `json:"lake_size_lower"`
|
||||
LakeSizeUpper float64 `json:"lake_size_upper"`
|
||||
Rivers int `json:"rivers"`
|
||||
MinRiverWidth float64 `json:"min_river_width"`
|
||||
MaxRiverWidth float64 `json:"max_river_width"`
|
||||
RiverCurvyness float64 `json:"river_curvyness"`
|
||||
|
||||
// Tree settings
|
||||
MinTreeSize float64 `json:"min_tree_size"`
|
||||
MaxTreeSize float64 `json:"max_tree_size"`
|
||||
TreeCoverage float64 `json:"tree_coverage"`
|
||||
TreeClumpiness float64 `json:"tree_clumpiness"`
|
||||
|
||||
// Road settings
|
||||
NumRoads int `json:"num_roads"`
|
||||
MinRoadWidth float64 `json:"min_road_width"`
|
||||
MaxRoadWidth float64 `json:"max_road_width"`
|
||||
RoadExits int `json:"road_exits"`
|
||||
RoadCurvyness float64 `json:"road_curvyness"`
|
||||
RoadDistribution float64 `json:"road_distribution"`
|
||||
|
||||
// Building settings
|
||||
NumBuildings int `json:"num_buildings"`
|
||||
MinBuildingSize float64 `json:"min_building_size"`
|
||||
MaxBuildingSize float64 `json:"max_building_size"`
|
||||
BuildingDistribution float64 `json:"building_distribution"`
|
||||
BuildingShape string `json:"building_shape"`
|
||||
LastExportPath string `json:"last_export_path"`
|
||||
|
||||
// General settings
|
||||
Seed int64 `json:"seed"`
|
||||
LastExportPath string `json:"last_export_path"`
|
||||
}
|
||||
|
||||
// Save saves the current settings to a JSON file in the user's config directory.
|
||||
func (s *Settings) Save() error {
|
||||
// Get the user's config directory
|
||||
configDir, err := os.UserConfigDir()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Create the application's config directory if it doesn't exist
|
||||
appConfigDir := filepath.Join(configDir, "rpgcitymakerreborn")
|
||||
if err := os.MkdirAll(appConfigDir, 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Create and open the settings file
|
||||
configFile := filepath.Join(appConfigDir, "settings.json")
|
||||
file, err := os.Create(configFile)
|
||||
if err != nil {
|
||||
@@ -56,19 +72,25 @@ func (s *Settings) Save() error {
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
// Encode the settings as JSON and write to the file
|
||||
encoder := json.NewEncoder(file)
|
||||
return encoder.Encode(s)
|
||||
}
|
||||
|
||||
// LoadSettings loads the settings from a JSON file in the user's config directory.
|
||||
// If the file doesn't exist, it returns a default set of settings.
|
||||
func LoadSettings() (*Settings, error) {
|
||||
// Get the user's config directory
|
||||
configDir, err := os.UserConfigDir()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Open the settings file
|
||||
configFile := filepath.Join(configDir, "rpgcitymakerreborn", "settings.json")
|
||||
file, err := os.Open(configFile)
|
||||
if err != nil {
|
||||
// If the file doesn't exist, return default settings
|
||||
if os.IsNotExist(err) {
|
||||
homeDir, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
@@ -109,12 +131,14 @@ func LoadSettings() (*Settings, error) {
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
// Decode the JSON data into a Settings struct
|
||||
var settings Settings
|
||||
decoder := json.NewDecoder(file)
|
||||
if err := decoder.Decode(&settings); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Ensure LastExportPath is set to a default value if it's empty
|
||||
if settings.LastExportPath == "" {
|
||||
homeDir, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
|
||||
+25
-12
@@ -14,13 +14,16 @@ import (
|
||||
"github.com/ojrac/opensimplex-go"
|
||||
)
|
||||
|
||||
// Constants for Perlin noise generation
|
||||
const (
|
||||
alpha = 2.
|
||||
beta = 2.
|
||||
n = 3
|
||||
)
|
||||
|
||||
// GenerateHeightmap creates a grayscale image representing the terrain's elevation using Perlin noise.
|
||||
func GenerateHeightmap(width, height, octaves int, scale float64, seed int64) image.Image {
|
||||
// Initialize Perlin noise generator
|
||||
p := perlin.NewPerlin(alpha, beta, n, seed)
|
||||
img := image.NewGray(image.Rect(0, 0, width, height))
|
||||
|
||||
@@ -28,6 +31,7 @@ func GenerateHeightmap(width, height, octaves int, scale float64, seed int64) im
|
||||
scale = 100.0
|
||||
}
|
||||
|
||||
// Use multiple goroutines to speed up noise generation
|
||||
numGoroutines := runtime.NumCPU()
|
||||
var wg sync.WaitGroup
|
||||
rowsPerGoroutine := height / numGoroutines
|
||||
@@ -43,6 +47,7 @@ func GenerateHeightmap(width, height, octaves int, scale float64, seed int64) im
|
||||
defer wg.Done()
|
||||
for y := startY; y < endY; y++ {
|
||||
for x := 0; x < width; x++ {
|
||||
// Combine multiple octaves of noise for more detail
|
||||
var noise float64
|
||||
frequency := 1.0
|
||||
amplitude := 1.0
|
||||
@@ -55,6 +60,7 @@ func GenerateHeightmap(width, height, octaves int, scale float64, seed int64) im
|
||||
frequency *= 2.0
|
||||
}
|
||||
|
||||
// Normalize the noise value and set the pixel color
|
||||
noise /= maxAmplitude
|
||||
grayColor := uint8((noise + 1) * 127.5)
|
||||
img.SetGray(x, y, color.Gray{Y: grayColor})
|
||||
@@ -67,11 +73,13 @@ func GenerateHeightmap(width, height, octaves int, scale float64, seed int64) im
|
||||
return img
|
||||
}
|
||||
|
||||
// ApplyRoughness adds a visual roughness effect to the heightmap.
|
||||
func ApplyRoughness(heightmap image.Image, roughness float64) image.Image {
|
||||
bounds := heightmap.Bounds()
|
||||
composite := image.NewRGBA(bounds)
|
||||
draw.Draw(composite, bounds, heightmap, image.Point{}, draw.Src)
|
||||
|
||||
// The alpha value of the overlay determines the roughness effect
|
||||
alphaValue := 255 - uint8(roughness*2.55)
|
||||
overlay := image.NewUniform(color.RGBA{R: 128, G: 128, B: 128, A: alphaValue})
|
||||
draw.Draw(composite, bounds, overlay, image.Point{}, draw.Over)
|
||||
@@ -91,11 +99,11 @@ func DarkenLakeAreas(heightmap image.Image, lakePixels []image.Point) image.Imag
|
||||
lakeMask.Set(p.X, p.Y, black)
|
||||
}
|
||||
|
||||
// Apply a Gaussian blur to the lake mask
|
||||
// Apply a Gaussian blur to the lake mask to create smooth edges
|
||||
blurRadius := float64(width) * 0.05
|
||||
blurredLakeMask := imaging.Blur(lakeMask, blurRadius)
|
||||
|
||||
// Composite the blurred lake mask onto the heightmap with 50% opacity
|
||||
// Composite the blurred lake mask onto the heightmap with some opacity
|
||||
composite := image.NewRGBA(bounds)
|
||||
draw.Draw(composite, bounds, heightmap, image.Point{}, draw.Src)
|
||||
draw.DrawMask(composite, bounds, blurredLakeMask, image.Point{}, image.NewUniform(color.Alpha{192}), image.Point{}, draw.Over)
|
||||
@@ -103,31 +111,33 @@ func DarkenLakeAreas(heightmap image.Image, lakePixels []image.Point) image.Imag
|
||||
return composite
|
||||
}
|
||||
|
||||
// FlattenRoadAreas smoothens the terrain under roads.
|
||||
func FlattenRoadAreas(heightmap image.Image, roadPixels []image.Point) image.Image {
|
||||
bounds := heightmap.Bounds()
|
||||
width := bounds.Dx()
|
||||
|
||||
// Create a new image with the road pixels drawn on it.
|
||||
// Create a mask with the road pixels
|
||||
roadMask := image.NewGray(bounds)
|
||||
for _, p := range roadPixels {
|
||||
roadMask.SetGray(p.X, p.Y, color.Gray{Y: 255})
|
||||
}
|
||||
|
||||
// Blur the road mask.
|
||||
// Blur the road mask to create a smooth transition
|
||||
blurRadius := float64(width) * 0.01
|
||||
blurredRoadMask := imaging.Blur(roadMask, blurRadius)
|
||||
|
||||
// Create a new image to store the blurred heightmap.
|
||||
// Blur the entire heightmap
|
||||
blurredHeightmap := imaging.Blur(heightmap, blurRadius)
|
||||
|
||||
// Create a new composite image.
|
||||
// Create a new composite image
|
||||
composite := image.NewRGBA(bounds)
|
||||
|
||||
// Interpolate between the original and blurred heightmap based on the road mask
|
||||
for y := bounds.Min.Y; y < bounds.Max.Y; y++ {
|
||||
for x := bounds.Min.X; x < bounds.Max.X; x++ {
|
||||
maskAlpha, _, _, _ := blurredRoadMask.At(x, y).RGBA()
|
||||
if maskAlpha > 0 {
|
||||
// Linearly interpolate between the original and blurred heightmap based on the mask alpha.
|
||||
// Linearly interpolate between the original and blurred heightmap
|
||||
originalColor := heightmap.At(x, y)
|
||||
blurredColor := blurredHeightmap.At(x, y)
|
||||
|
||||
@@ -151,11 +161,12 @@ func FlattenRoadAreas(heightmap image.Image, roadPixels []image.Point) image.Ima
|
||||
return composite
|
||||
}
|
||||
|
||||
// GenerateTrees places trees on the map.
|
||||
func GenerateTrees(img *image.RGBA, lakePixels, roadPixels, buildingPixels []image.Point, minTreeSize, maxTreeSize, treeCoverage, treeClumpiness float64, seed int64) []image.Point {
|
||||
width := img.Bounds().Dx()
|
||||
height := img.Bounds().Dy()
|
||||
|
||||
// 1. Calculate number of trees to place from coverage %.
|
||||
// Step 1: Calculate the number of trees to place based on coverage percentage.
|
||||
avgTreeSize := (minTreeSize + maxTreeSize) / 2
|
||||
if avgTreeSize <= 0 {
|
||||
return nil
|
||||
@@ -172,7 +183,7 @@ func GenerateTrees(img *image.RGBA, lakePixels, roadPixels, buildingPixels []ima
|
||||
return nil
|
||||
}
|
||||
|
||||
// 2. Generate a simplex noise map for tree placement.
|
||||
// Step 2: Generate a simplex noise map to guide tree placement.
|
||||
noise := opensimplex.New(seed)
|
||||
treeNoiseMap := image.NewGray(image.Rect(0, 0, width, height))
|
||||
treeNoiseZoom := 0.05
|
||||
@@ -185,6 +196,7 @@ func GenerateTrees(img *image.RGBA, lakePixels, roadPixels, buildingPixels []ima
|
||||
}
|
||||
threshold := uint8(255 * (1 - (treeCoverage / 100.0)))
|
||||
|
||||
// Create lookup maps for water, roads, and buildings for efficient collision detection
|
||||
isLake := make(map[image.Point]bool)
|
||||
for _, p := range lakePixels {
|
||||
isLake[p] = true
|
||||
@@ -202,7 +214,7 @@ func GenerateTrees(img *image.RGBA, lakePixels, roadPixels, buildingPixels []ima
|
||||
|
||||
randSrc := rand.New(rand.NewSource(seed))
|
||||
|
||||
// 3. Determine initial clump trees
|
||||
// Step 3: Determine initial points for clumps of trees.
|
||||
numClumpTrees := min(int(treeClumpiness), numTreesToPlace)
|
||||
|
||||
initialPoints := make([]image.Point, 0, numClumpTrees)
|
||||
@@ -216,14 +228,14 @@ func GenerateTrees(img *image.RGBA, lakePixels, roadPixels, buildingPixels []ima
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Place remaining trees using Bridson's Algorithm
|
||||
// Step 4: Place remaining trees using Poisson Disc Sampling for a natural distribution.
|
||||
minRadius := minTreeSize
|
||||
allPoints := poissonDiscSampling(width, height, minRadius, 30, initialPoints, func(p image.Point) bool {
|
||||
return treeNoiseMap.GrayAt(p.X, p.Y).Y >= threshold && !isLake[p] && !isRoad[p] && !isBuilding[p]
|
||||
}, seed)
|
||||
|
||||
var treePixels []image.Point
|
||||
// 5. Draw the trees.
|
||||
// Step 5: Draw the trees on the image.
|
||||
numGoroutines := runtime.NumCPU()
|
||||
if len(allPoints) < numGoroutines {
|
||||
numGoroutines = len(allPoints)
|
||||
@@ -282,6 +294,7 @@ func GenerateTrees(img *image.RGBA, lakePixels, roadPixels, buildingPixels []ima
|
||||
return treePixels
|
||||
}
|
||||
|
||||
// poissonDiscSampling generates points that are randomly distributed but no closer than a given minimum radius.
|
||||
func poissonDiscSampling(width, height int, minRadius float64, k int, initialPoints []image.Point, isValid func(image.Point) bool, seed int64) []image.Point {
|
||||
randSrc := rand.New(rand.NewSource(seed))
|
||||
points := initialPoints
|
||||
|
||||
@@ -12,13 +12,15 @@ import (
|
||||
"github.com/ojrac/opensimplex-go"
|
||||
)
|
||||
|
||||
// lakePixel represents a potential pixel to be added to a lake during growth
|
||||
// lakePixel represents a potential pixel to be added to a lake during growth.
|
||||
// It is used in a priority queue to determine the next pixel to add.
|
||||
type lakePixel struct {
|
||||
point image.Point
|
||||
score float64
|
||||
index int // required for heap.Interface
|
||||
}
|
||||
|
||||
// priorityQueue implements a max-heap for lakePixel structs.
|
||||
type priorityQueue []*lakePixel
|
||||
|
||||
func (pq priorityQueue) Len() int { return len(pq) }
|
||||
@@ -44,7 +46,9 @@ func (pq *priorityQueue) Pop() any {
|
||||
return item
|
||||
}
|
||||
|
||||
// GenerateLakes creates lakes on the map using a growth algorithm.
|
||||
func GenerateLakes(width, height, numLakes int, lakeSizeLower, lakeSizeUpper float64, heightmap image.Image, seed int64) (image.Image, [][]image.Point) {
|
||||
// Initialize a white canvas to draw the lakes on
|
||||
canvas := image.NewRGBA(image.Rect(0, 0, width, height))
|
||||
draw.Draw(canvas, canvas.Bounds(), image.NewUniform(color.White), image.Point{}, draw.Src)
|
||||
|
||||
@@ -55,7 +59,7 @@ func GenerateLakes(width, height, numLakes int, lakeSizeLower, lakeSizeUpper flo
|
||||
var allLakes [][]image.Point
|
||||
randSrc := rand.New(rand.NewSource(seed))
|
||||
|
||||
// 1. Divide the image into a grid
|
||||
// Step 1: Divide the image into a grid to distribute the lakes.
|
||||
gridDim := int(math.Ceil(math.Sqrt(float64(numLakes))))
|
||||
if gridDim == 0 {
|
||||
return canvas, nil
|
||||
@@ -66,7 +70,7 @@ func GenerateLakes(width, height, numLakes int, lakeSizeLower, lakeSizeUpper flo
|
||||
return canvas, nil
|
||||
}
|
||||
|
||||
// 2. Create a list of chunk indices and shuffle them to randomize lake placement
|
||||
// Step 2: Create a shuffled list of chunk indices to randomize lake placement.
|
||||
chunkIndices := make([]int, gridDim*gridDim)
|
||||
for i := range chunkIndices {
|
||||
chunkIndices[i] = i
|
||||
@@ -78,7 +82,7 @@ func GenerateLakes(width, height, numLakes int, lakeSizeLower, lakeSizeUpper flo
|
||||
totalArea := float64(width * height)
|
||||
noiseGen := opensimplex.New(seed)
|
||||
|
||||
// 3. Generate a lake in a subset of the chunks
|
||||
// Step 3: Generate a lake in a subset of the chunks.
|
||||
for i := range numLakes {
|
||||
if i >= len(chunkIndices) {
|
||||
break
|
||||
@@ -86,7 +90,7 @@ func GenerateLakes(width, height, numLakes int, lakeSizeLower, lakeSizeUpper flo
|
||||
|
||||
var currentLake []image.Point
|
||||
|
||||
// Each lake gets a random size within the defined range
|
||||
// Each lake gets a random size within the defined range.
|
||||
lakeSize := lakeSizeLower
|
||||
if lakeSizeUpper > lakeSizeLower {
|
||||
lakeSize = lakeSizeLower + randSrc.Float64()*(lakeSizeUpper-lakeSizeLower)
|
||||
@@ -107,21 +111,21 @@ func GenerateLakes(width, height, numLakes int, lakeSizeLower, lakeSizeUpper flo
|
||||
(chunkGridY+1)*chunkHeight,
|
||||
)
|
||||
|
||||
// Use the growth algorithm within the chunk
|
||||
// Use a priority queue-based growth algorithm within the chunk.
|
||||
pq := &priorityQueue{}
|
||||
heap.Init(pq)
|
||||
visited := make(map[image.Point]bool)
|
||||
|
||||
// Start near the center of the chunk
|
||||
// Start the growth near the center of the chunk.
|
||||
startPt := image.Point{
|
||||
X: chunkRect.Min.X + chunkWidth/2,
|
||||
Y: chunkRect.Min.Y + chunkHeight/2,
|
||||
}
|
||||
// just in case the center is out of bounds
|
||||
if !startPt.In(chunkRect) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Use noise to create a more natural lake shape.
|
||||
seedX := randSrc.Float64() * 10000.0
|
||||
seedY := randSrc.Float64() * 10000.0
|
||||
radius := math.Sqrt(float64(targetPixelsPerLake) / math.Pi)
|
||||
@@ -134,23 +138,23 @@ func GenerateLakes(width, height, numLakes int, lakeSizeLower, lakeSizeUpper flo
|
||||
distPenalty := math.Pow(dist/radius, 3.0)
|
||||
luma, _, _, _ := heightmap.At(pt.X, pt.Y).RGBA()
|
||||
heightmapVal := float64(luma) / 65535.0
|
||||
heightmapEffect := (0.5 - heightmapVal) * 1.5
|
||||
heightmapEffect := (0.5 - heightmapVal) * 1.5 // Encourage growth in lower areas
|
||||
return noise - distPenalty + heightmapEffect
|
||||
}
|
||||
|
||||
heap.Push(pq, &lakePixel{point: startPt, score: getScore(startPt)})
|
||||
visited[startPt] = true
|
||||
|
||||
// Grow the lake until it reaches its target size.
|
||||
lakeCount := 0
|
||||
for pq.Len() > 0 && lakeCount < targetPixelsPerLake {
|
||||
current := heap.Pop(pq).(*lakePixel)
|
||||
|
||||
// The pixel is valid, claim it.
|
||||
canvas.Set(current.point.X, current.point.Y, color.RGBA{R: 0, G: 0, B: 255, A: 255})
|
||||
currentLake = append(currentLake, current.point)
|
||||
lakeCount++
|
||||
|
||||
// Add neighbors, constrained to the chunk rectangle
|
||||
// Add neighbors to the priority queue.
|
||||
for dy := -1; dy <= 1; dy++ {
|
||||
for dx := -1; dx <= 1; dx++ {
|
||||
if dx == 0 && dy == 0 {
|
||||
@@ -178,12 +182,14 @@ func GenerateLakes(width, height, numLakes int, lakeSizeLower, lakeSizeUpper flo
|
||||
return canvas, allLakes
|
||||
}
|
||||
|
||||
// River represents a river on the map.
|
||||
type River struct {
|
||||
Width float64
|
||||
Start, End image.Point
|
||||
Points []image.Point
|
||||
}
|
||||
|
||||
// GenerateRivers creates rivers on the map.
|
||||
func GenerateRivers(width, height, numRivers int, minWidth, maxWidth, curvyness float64, inputImage image.Image, lakes [][]image.Point, seed int64, heightmap image.Image) (image.Image, []image.Point) {
|
||||
if numRivers == 0 {
|
||||
return inputImage, nil
|
||||
@@ -199,6 +205,7 @@ func GenerateRivers(width, height, numRivers int, minWidth, maxWidth, curvyness
|
||||
randSrc := rand.New(rand.NewSource(seed))
|
||||
avgDim := float64(width+height) / 2.0
|
||||
|
||||
// Create a map of water pixels for collision detection.
|
||||
isWater := make(map[image.Point]bool)
|
||||
lakePixelMap := make(map[image.Point]int)
|
||||
for i, lake := range lakes {
|
||||
@@ -208,6 +215,7 @@ func GenerateRivers(width, height, numRivers int, minWidth, maxWidth, curvyness
|
||||
}
|
||||
}
|
||||
|
||||
// Create rivers with varying widths.
|
||||
rivers := make([]River, numRivers)
|
||||
for i := range numRivers {
|
||||
widthPercent := float64(i) / float64(numRivers-1)
|
||||
@@ -217,31 +225,36 @@ func GenerateRivers(width, height, numRivers int, minWidth, maxWidth, curvyness
|
||||
rivers[i].Width = maxWidth - widthPercent*(maxWidth-minWidth)
|
||||
}
|
||||
|
||||
// Sort rivers by width in descending order.
|
||||
sort.Slice(rivers, func(i, j int) bool {
|
||||
return rivers[i].Width > rivers[j].Width
|
||||
})
|
||||
|
||||
numControlPoints := max(int(avgDim*0.03), 60)
|
||||
|
||||
// Generate each river.
|
||||
for i := range rivers {
|
||||
r := &rivers[i]
|
||||
|
||||
// Determine the start and end edges of the river.
|
||||
startEdge := randSrc.Intn(4)
|
||||
endEdge := (startEdge + randSrc.Intn(3) + 1) % 4
|
||||
|
||||
r.Start = getPointOnEdge(width, height, startEdge, randSrc)
|
||||
r.End = getPointOnEdge(width, height, endEdge, randSrc)
|
||||
|
||||
// Calculate the river's path.
|
||||
path := calculateRiverPath(r.Start, r.End, curvyness/100.0, avgDim, randSrc, numControlPoints)
|
||||
|
||||
// Check for intersections with other water bodies.
|
||||
for _, p := range path {
|
||||
if isWater[p] {
|
||||
if lakeIndex, isLake := lakePixelMap[p]; isLake {
|
||||
// Intersection is with a lake, find its center
|
||||
// If the river intersects with a lake, end the river at the lake's center.
|
||||
lakeCenter := findCenter(lakes[lakeIndex])
|
||||
r.End = lakeCenter
|
||||
} else {
|
||||
// Intersection is with another river
|
||||
// If the river intersects with another river, end it at the intersection point.
|
||||
r.End = p
|
||||
}
|
||||
path = calculateRiverPath(r.Start, r.End, curvyness/100.0, avgDim, randSrc, numControlPoints)
|
||||
@@ -249,11 +262,11 @@ func GenerateRivers(width, height, numRivers int, minWidth, maxWidth, curvyness
|
||||
}
|
||||
}
|
||||
|
||||
// Draw the river on the canvas.
|
||||
riverWidthPx := (r.Width / 100.0) * avgDim
|
||||
radius := riverWidthPx / 2.0
|
||||
|
||||
for _, p := range path {
|
||||
// When drawing river pixels, add them to isWater to detect river-river intersections
|
||||
drawCircle(canvas, p, radius, color.RGBA{R: 0, G: 0, B: 255, A: 255}, &allRiverPixels, isWater, heightmap)
|
||||
}
|
||||
r.Points = path
|
||||
@@ -262,6 +275,7 @@ func GenerateRivers(width, height, numRivers int, minWidth, maxWidth, curvyness
|
||||
return canvas, allRiverPixels
|
||||
}
|
||||
|
||||
// bresenhamRiver creates a path between control points using Bresenham's line algorithm.
|
||||
func bresenhamRiver(path []image.Point) []image.Point {
|
||||
if len(path) < 2 {
|
||||
return path
|
||||
@@ -301,6 +315,7 @@ func bresenhamRiver(path []image.Point) []image.Point {
|
||||
return fullPath
|
||||
}
|
||||
|
||||
// calculateRiverPath computes the path for a river, including curves.
|
||||
func calculateRiverPath(start, end image.Point, curvyness, avgDim float64, randSrc *rand.Rand, numControlPoints int) []image.Point {
|
||||
dx := end.X - start.X
|
||||
dy := end.Y - start.Y
|
||||
@@ -314,6 +329,7 @@ func calculateRiverPath(start, end image.Point, curvyness, avgDim float64, randS
|
||||
return bresenhamRiver([]image.Point{start, end})
|
||||
}
|
||||
|
||||
// Use sine waves to create curves in the river.
|
||||
type wave struct {
|
||||
amplitude float64
|
||||
numWaves float64
|
||||
@@ -339,6 +355,7 @@ func calculateRiverPath(start, end image.Point, curvyness, avgDim float64, randS
|
||||
amp /= 3
|
||||
}
|
||||
|
||||
// Generate control points for the curve.
|
||||
controlPoints := make([]image.Point, numControlPoints+1)
|
||||
for i := 0; i <= numControlPoints; i++ {
|
||||
t := float64(i) / float64(numControlPoints)
|
||||
@@ -357,9 +374,11 @@ func calculateRiverPath(start, end image.Point, curvyness, avgDim float64, randS
|
||||
controlPoints[i] = image.Point{X: int(math.Round(x)), Y: int(math.Round(y))}
|
||||
}
|
||||
|
||||
// Create the final path using Bresenham's algorithm between control points.
|
||||
return bresenhamRiver(controlPoints)
|
||||
}
|
||||
|
||||
// findCenter finds the center of a slice of points.
|
||||
func findCenter(pixels []image.Point) image.Point {
|
||||
if len(pixels) == 0 {
|
||||
return image.Point{}
|
||||
@@ -375,6 +394,7 @@ func findCenter(pixels []image.Point) image.Point {
|
||||
}
|
||||
}
|
||||
|
||||
// getPointOnEdge returns a random point on a specified edge of the map.
|
||||
func getPointOnEdge(width, height, edge int, randSrc *rand.Rand) image.Point {
|
||||
switch edge {
|
||||
case 0: // Top
|
||||
@@ -387,6 +407,8 @@ func getPointOnEdge(width, height, edge int, randSrc *rand.Rand) image.Point {
|
||||
return image.Point{X: 0, Y: randSrc.Intn(height)}
|
||||
}
|
||||
}
|
||||
|
||||
// drawCircle draws a circle on the image and adds its pixels to the given slice.
|
||||
func drawCircle(img *image.RGBA, center image.Point, radius float64, c color.Color, pixels *[]image.Point, isWater map[image.Point]bool, heightmap image.Image) {
|
||||
bounds := img.Bounds()
|
||||
r2 := radius * radius
|
||||
@@ -405,12 +427,10 @@ func drawCircle(img *image.RGBA, center image.Point, radius float64, c color.Col
|
||||
|
||||
if dist2 <= r2 {
|
||||
if !isWater[p] {
|
||||
// Roughen the outer 15% of the river
|
||||
// Roughen the outer 15% of the river based on the heightmap.
|
||||
if dist2 > innerR2 {
|
||||
luma, _, _, _ := heightmap.At(x, y).RGBA()
|
||||
// Normalize luma to 0-1 range
|
||||
heightmapVal := float64(luma) / 65535.0
|
||||
// Roughen the edges based on the heightmap
|
||||
if heightmapVal < 0.5 {
|
||||
continue
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user