diff --git a/bumpmap.go b/bumpmap.go new file mode 100644 index 0000000..8f99d22 --- /dev/null +++ b/bumpmap.go @@ -0,0 +1,57 @@ +package main + +import ( + "image" + "image/color" + "math" +) + +// GenerateBumpMap creates a bump map from a heightmap. +func GenerateBumpMap(heightmap *image.RGBA, width, height int, depth float64) *image.RGBA { + bumpMap := image.NewRGBA(image.Rect(0, 0, width, height)) + + sobelX := [][]int{ + {-1, 0, 1}, + {-2, 0, 2}, + {-1, 0, 1}, + } + + sobelY := [][]int{ + {-1, -2, -1}, + {0, 0, 0}, + {1, 2, 1}, + } + + for y := 1; y < height-1; y++ { + for x := 1; x < width-1; x++ { + var gx, gy float64 + + for i := 0; i < 3; i++ { + for j := 0; j < 3; j++ { + gray, _, _, _ := heightmap.At(x+i-1, y+j-1).RGBA() + intensity := float64(gray >> 8) + gx += intensity * float64(sobelX[j][i]) + gy += intensity * float64(sobelY[j][i]) + } + } + + nx := gx + ny := gy + nz := 1.0 / depth + + length := math.Sqrt(nx*nx + ny*ny + nz*nz) + if length > 0 { + nx /= length + ny /= length + nz /= length + } + + r := uint8((nx + 1.0) * 127.5) + g := uint8((ny + 1.0) * 127.5) + b := uint8(nz * 255.0) + + bumpMap.Set(x, y, color.RGBA{R: r, G: g, B: b, A: 255}) + } + } + return bumpMap +} diff --git a/main.go b/main.go index 071982c..e92afb1 100644 --- a/main.go +++ b/main.go @@ -49,6 +49,9 @@ func main() { heightmapImg := &canvas.Image{ FillMode: canvas.ImageFillContain, } + bumpmapImg := &canvas.Image{ + FillMode: canvas.ImageFillContain, + } // Initialize slices to store generated map features var lakes [][]image.Point @@ -67,6 +70,7 @@ func main() { appConfigDir := filepath.Join(configDir, "rpgcitymakerreborn") canvasPath := filepath.Join(appConfigDir, "canvas.png") heightmapPath := filepath.Join(appConfigDir, "heightmap.png") + bumpmapPath := filepath.Join(appConfigDir, "bumpmap.png") // Save settings and images on window close w.SetOnClosed(func() { @@ -97,6 +101,18 @@ func main() { } } } + + if bumpmapImg.Image != nil { + bumpmapFile, err := os.Create(bumpmapPath) + if err != nil { + log.Println("Failed to create bumpmap file:", err) + } else { + defer bumpmapFile.Close() + if err := png.Encode(bumpmapFile, bumpmapImg.Image); err != nil { + log.Println("Failed to encode bumpmap image:", err) + } + } + } }) // Load previously saved images @@ -118,6 +134,15 @@ func main() { } } + bumpmapFile, err := os.Open(bumpmapPath) + if err == nil { + defer bumpmapFile.Close() + img, err := png.Decode(bumpmapFile) + if err == nil { + bumpmapImg.Image = img + } + } + // Generate initial images if none are loaded if canvasImg.Image == nil || heightmapImg.Image == nil { @@ -177,7 +202,11 @@ func main() { // Step 10: Applying Roughness compositeImg := ApplyRoughness(flattenedHeightmap, settings.Roughness) + // Step 11: Generating Bump Map + bumpMap := GenerateBumpMap(compositeImg.(*image.RGBA), settings.Width, settings.Height, 0.10) + heightmapImg.Image = compositeImg + bumpmapImg.Image = bumpMap canvasImg.Image = finalImage @@ -388,6 +417,9 @@ func main() { exportHeightmapBtn := widget.NewButton("Export Heightmap", func() { showSaveDialog(w, heightmapImg.Image, settings) }) + exportBumpmapBtn := widget.NewButton("Export Bump Map", func() { + showSaveDialog(w, bumpmapImg.Image, settings) + }) exportMasksBtn := widget.NewButton("Export Masks", func() { showMasksSaveDialog(w, canvasImg.Image, heightmapImg.Image, settings, lakes, riverPixels, treePixels, roadPixels, bridgePixels, buildingPixels, buildings) }) @@ -502,13 +534,23 @@ func main() { }) treePixels = GenerateTrees(finalImage, allWaterPixels, roadPixels, buildingPixels, settings.MinTreeSize, settings.MaxTreeSize, settings.TreeCoverage, settings.TreeClumpiness, seedProvider.Next()) - // Step 10: Finalizing Images + // Step 10: Generating Bump Map + currentStep++ + fyne.Do(func() { + progressBar.SetText(fmt.Sprintf("Step %d/%d: Generating Bump Map", currentStep, steps)) + progressBar.SetValue(float64(currentStep) / float64(steps)) + }) + bumpMap := GenerateBumpMap(compositeImg.(*image.RGBA), settings.Width, settings.Height, 0.10) + + // Step 11: Finalizing Images currentStep++ fyne.Do(func() { progressBar.SetText(fmt.Sprintf("Step %d/%d: Finalizing Images", currentStep, steps)) progressBar.SetValue(float64(currentStep) / float64(steps)) heightmapImg.Image = compositeImg heightmapImg.Refresh() + bumpmapImg.Image = bumpMap + bumpmapImg.Refresh() canvasImg.Image = finalImage canvasImg.Refresh() @@ -884,6 +926,7 @@ func main() { widget.NewSeparator(), exportCanvasBtn, exportHeightmapBtn, + exportBumpmapBtn, exportMasksBtn, )) // Create the main layout using a horizontal split @@ -900,9 +943,10 @@ func main() { tabs, ) - right := container.NewGridWithRows(2, + right := container.NewGridWithRows(3, canvasImg, heightmapImg, + bumpmapImg, ) split := container.NewHSplit(