From 36dc9ddef70673ee9800aa05eb51a38b3de769fc Mon Sep 17 00:00:00 2001 From: Kelsi Date: Tue, 5 May 2026 15:42:35 -0700 Subject: [PATCH] feat(editor): PNG/JPG/BMP heightmap image import, undo for all imports - importHeightmapImage(): loads any resolution PNG/JPG/BMP/TGA via stb_image, supports both 8-bit and 16-bit precision, maps to terrain vertices with bilinear coordinate mapping - Both image import and RAW import now wrapped with undo (recordGeneratorUndo/commitGeneratorUndo) - UI: File > Import Heightmap now offers "Import Image" (any format) and "Import RAW" (binary) as separate options - Enables professional terrain workflows: paint in Photoshop/GIMP, generate in World Machine/Gaea, import directly as terrain --- tools/editor/editor_ui.cpp | 19 ++++++---- tools/editor/terrain_editor.cpp | 64 ++++++++++++++++++++++++++++++++- tools/editor/terrain_editor.hpp | 1 + 3 files changed, 77 insertions(+), 7 deletions(-) diff --git a/tools/editor/editor_ui.cpp b/tools/editor/editor_ui.cpp index d3c45363..55b8292c 100644 --- a/tools/editor/editor_ui.cpp +++ b/tools/editor/editor_ui.cpp @@ -327,17 +327,24 @@ void EditorUI::renderMenuBar(EditorApp& app) { ImGui::EndMenu(); } if (ImGui::BeginMenu("Import Heightmap", app.hasTerrainLoaded())) { - static char hmPath[256] = "heightmap.raw"; + static char hmPath[256] = "heightmap.png"; static float hmScale = 200.0f; ImGui::InputText("File##hm", hmPath, sizeof(hmPath)); ImGui::SliderFloat("Height Scale", &hmScale, 10.0f, 1000.0f); - ImGui::TextColored(ImVec4(0.6f,0.6f,0.6f,1), "RAW 16-bit or 8-bit (129x129 or 257x257)"); - if (ImGui::MenuItem("Import")) { - if (app.getTerrainEditor().importHeightmap(hmPath, hmScale)) - app.showToast("Heightmap imported"); + if (ImGui::MenuItem("Import Image (PNG/JPG/BMP/TGA)")) { + if (app.getTerrainEditor().importHeightmapImage(hmPath, hmScale)) + app.showToast("Heightmap image imported (undoable)"); else - app.showToast("Failed to import heightmap"); + app.showToast("Failed — check image path and format"); } + if (ImGui::MenuItem("Import RAW (16/8-bit binary)")) { + if (app.getTerrainEditor().importHeightmap(hmPath, hmScale)) + app.showToast("RAW heightmap imported (undoable)"); + else + app.showToast("Failed — need 129x129 or 257x257 RAW"); + } + ImGui::TextColored(ImVec4(0.5f,0.5f,0.5f,1), + "Supports any resolution PNG/JPG/BMP/TGA or RAW"); ImGui::EndMenu(); } if (ImGui::MenuItem("Generate Complete Zone", nullptr, false, app.hasTerrainLoaded())) diff --git a/tools/editor/terrain_editor.cpp b/tools/editor/terrain_editor.cpp index 2ba3b609..246be826 100644 --- a/tools/editor/terrain_editor.cpp +++ b/tools/editor/terrain_editor.cpp @@ -1,5 +1,6 @@ #include "terrain_editor.hpp" #include "core/logger.hpp" +#include "stb_image.h" #include #include #include @@ -1650,7 +1651,7 @@ void TerrainEditor::applyNoise(float frequency, float amplitude, int octaves, ui bool TerrainEditor::importHeightmap(const std::string& path, float heightScale) { if (!terrain_) return false; - + recordGeneratorUndo(); std::ifstream f(path, std::ios::binary | std::ios::ate); if (!f) { return false; } auto fileSize = f.tellg(); @@ -1704,6 +1705,67 @@ bool TerrainEditor::importHeightmap(const std::string& path, float heightScale) } } dirty_ = true; + commitGeneratorUndo(); + return true; +} + +bool TerrainEditor::importHeightmapImage(const std::string& path, float heightScale) { + if (!terrain_) return false; + recordGeneratorUndo(); + + int w = 0, h = 0, channels = 0; + bool is16 = false; + std::vector heightData; + + // Try 16-bit first for precision + unsigned short* data16 = stbi_load_16(path.c_str(), &w, &h, &channels, 1); + if (data16) { + is16 = true; + heightData.resize(w * h); + for (int i = 0; i < w * h; i++) + heightData[i] = static_cast(data16[i]) / 65535.0f; + stbi_image_free(data16); + } else { + unsigned char* data8 = stbi_load(path.c_str(), &w, &h, &channels, 1); + if (!data8) { + LOG_ERROR("Failed to load heightmap image: ", path); + commitGeneratorUndo(); + return false; + } + heightData.resize(w * h); + for (int i = 0; i < w * h; i++) + heightData[i] = static_cast(data8[i]) / 255.0f; + stbi_image_free(data8); + } + + LOG_INFO("Heightmap image loaded: ", path, " (", w, "x", h, + is16 ? " 16-bit" : " 8-bit", ")"); + + for (int cy = 0; cy < 16; cy++) { + for (int cx = 0; cx < 16; cx++) { + auto& chunk = terrain_->chunks[cy * 16 + cx]; + if (!chunk.hasHeightMap()) continue; + + for (int v = 0; v < 145; v++) { + int row = v / 17, col = v % 17; + float offX = static_cast(col); + float offY = static_cast(row); + if (col > 8) { offY += 0.5f; offX -= 8.5f; } + + float u = (cx * 8.0f + offX) / 128.0f; + float vv = (cy * 8.0f + offY) / 128.0f; + int px = std::clamp(static_cast(u * (w - 1)), 0, w - 1); + int py = std::clamp(static_cast(vv * (h - 1)), 0, h - 1); + + chunk.heightMap.heights[v] = heightData[py * w + px] * heightScale; + } + stitchEdges(cy * 16 + cx); + dirtyChunks_.push_back(cy * 16 + cx); + } + } + dirty_ = true; + commitGeneratorUndo(); + LOG_INFO("Heightmap applied: scale=", heightScale); return true; } diff --git a/tools/editor/terrain_editor.hpp b/tools/editor/terrain_editor.hpp index 90476f56..8a7d949d 100644 --- a/tools/editor/terrain_editor.hpp +++ b/tools/editor/terrain_editor.hpp @@ -144,6 +144,7 @@ public: // Import/export heightmap (raw 16-bit grayscale, 129x129) bool importHeightmap(const std::string& path, float heightScale); + bool importHeightmapImage(const std::string& path, float heightScale); bool exportHeightmap(const std::string& path, float heightScale); // Water editing