From 84a431880ee7ac45d5f4a44b4d1a161764dd8283 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Tue, 5 May 2026 16:01:29 -0700 Subject: [PATCH] feat(editor): zone map image export (colored top-down PNG) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - exportZoneMap(): renders terrain as colored top-down image with height-based coloring (blue lowlands → green plains → brown hills → white peaks), water overlay, hole visualization, doodad markers - Configurable resolution (128-2048px, default 512) - Auto-exported as zone_map.png alongside other assets on save - File > Export Zone Map menu with resolution slider - Useful for documentation, server admin tools, custom map websites --- tools/editor/editor_app.cpp | 1 + tools/editor/editor_ui.cpp | 17 ++++++ tools/editor/wowee_terrain.cpp | 99 ++++++++++++++++++++++++++++++++++ tools/editor/wowee_terrain.hpp | 4 ++ 4 files changed, 121 insertions(+) diff --git a/tools/editor/editor_app.cpp b/tools/editor/editor_app.cpp index 71b7c89b..56dc8c8c 100644 --- a/tools/editor/editor_app.cpp +++ b/tools/editor/editor_app.cpp @@ -968,6 +968,7 @@ void EditorApp::exportZone(const std::string& outputDir) { WoweeTerrain::exportHeightmapPreview(terrain_, openBase + "_heightmap.png"); // Also save heightmap as zone thumbnail for content pack browsing WoweeTerrain::exportHeightmapPreview(terrain_, base + "/thumbnail.png"); + WoweeTerrain::exportZoneMap(terrain_, base + "/zone_map.png", 512); // Write zone info README { diff --git a/tools/editor/editor_ui.cpp b/tools/editor/editor_ui.cpp index e87b24e0..f68098e7 100644 --- a/tools/editor/editor_ui.cpp +++ b/tools/editor/editor_ui.cpp @@ -8,6 +8,7 @@ #include "quest_editor.hpp" #include "pipeline/custom_zone_discovery.hpp" #include "content_pack.hpp" +#include "wowee_terrain.hpp" #include "pipeline/wowee_terrain_loader.hpp" #include #include "asset_browser.hpp" @@ -426,6 +427,22 @@ void EditorUI::renderMenuBar(EditorApp& app) { } ImGui::EndMenu(); } + if (ImGui::BeginMenu("Export Zone Map", app.hasTerrainLoaded())) { + static char mapPath[256] = "output/zone_map.png"; + static int mapRes = 512; + ImGui::InputText("File##zonemap", mapPath, sizeof(mapPath)); + ImGui::SliderInt("Resolution", &mapRes, 128, 2048); + if (ImGui::MenuItem("Export PNG")) { + if (editor::WoweeTerrain::exportZoneMap( + *app.getTerrainEditor().getTerrain(), mapPath, mapRes)) + app.showToast("Zone map exported: " + std::string(mapPath)); + else + app.showToast("Export failed"); + } + ImGui::TextColored(ImVec4(0.5f,0.5f,0.5f,1), + "Top-down colored map with terrain, water, objects"); + ImGui::EndMenu(); + } ImGui::Separator(); if (ImGui::MenuItem("Quit", "Alt+F4")) app.requestQuit(); ImGui::EndMenu(); diff --git a/tools/editor/wowee_terrain.cpp b/tools/editor/wowee_terrain.cpp index fcd344fd..df38efc4 100644 --- a/tools/editor/wowee_terrain.cpp +++ b/tools/editor/wowee_terrain.cpp @@ -247,6 +247,105 @@ int WoweeTerrain::exportAlphaMaps(const pipeline::ADTTerrain& terrain, return exported; } +bool WoweeTerrain::exportZoneMap(const pipeline::ADTTerrain& terrain, + const std::string& path, int resolution) { + namespace fs = std::filesystem; + fs::create_directories(fs::path(path).parent_path()); + + std::vector pixels(resolution * resolution * 3, 0); + + // Find height range + float minH = 1e30f, maxH = -1e30f; + for (int ci = 0; ci < 256; ci++) { + const auto& c = terrain.chunks[ci]; + if (!c.hasHeightMap()) continue; + for (int v = 0; v < 145; v++) { + float h = c.position[2] + c.heightMap.heights[v]; + minH = std::min(minH, h); maxH = std::max(maxH, h); + } + } + float range = std::max(maxH - minH, 1.0f); + + // Render terrain colors + for (int py = 0; py < resolution; py++) { + for (int px = 0; px < resolution; px++) { + float u = static_cast(px) / resolution; + float v = static_cast(py) / resolution; + + int cx = static_cast(u * 16); cx = std::clamp(cx, 0, 15); + int cy = static_cast(v * 16); cy = std::clamp(cy, 0, 15); + int ci = cy * 16 + cx; + + const auto& chunk = terrain.chunks[ci]; + if (!chunk.hasHeightMap()) continue; + + float localU = (u * 16 - cx) * 8; + float localV = (v * 16 - cy) * 8; + int gx = std::clamp(static_cast(localU), 0, 7); + int gy = std::clamp(static_cast(localV), 0, 7); + float h = chunk.position[2] + chunk.heightMap.heights[gy * 17 + gx]; + float t = (h - minH) / range; + + // Terrain coloring: blue(low) -> green(mid) -> brown(high) -> white(peak) + float r, g, b; + if (t < 0.15f) { + r = 0.2f; g = 0.3f; b = 0.6f; + } else if (t < 0.4f) { + float tt = (t - 0.15f) / 0.25f; + r = 0.2f * (1-tt) + 0.3f * tt; + g = 0.3f * (1-tt) + 0.6f * tt; + b = 0.6f * (1-tt) + 0.2f * tt; + } else if (t < 0.7f) { + float tt = (t - 0.4f) / 0.3f; + r = 0.3f + tt * 0.4f; g = 0.6f - tt * 0.1f; b = 0.2f - tt * 0.1f; + } else { + float tt = (t - 0.7f) / 0.3f; + r = 0.7f + tt * 0.2f; g = 0.5f + tt * 0.3f; b = 0.1f + tt * 0.6f; + } + + // Water overlay + if (terrain.waterData[ci].hasWater()) { + float wh = terrain.waterData[ci].layers[0].maxHeight; + if (h < wh) { r = 0.15f; g = 0.3f; b = 0.7f; } + } + + // Hole overlay + if (chunk.holes) { + int hx = gx / 2, hy = gy / 2; + if (chunk.holes & (1 << (hy * 4 + hx))) { + r = 0.1f; g = 0.1f; b = 0.1f; + } + } + + int idx = (py * resolution + px) * 3; + pixels[idx] = static_cast(std::clamp(r, 0.0f, 1.0f) * 255); + pixels[idx+1] = static_cast(std::clamp(g, 0.0f, 1.0f) * 255); + pixels[idx+2] = static_cast(std::clamp(b, 0.0f, 1.0f) * 255); + } + } + + // Draw doodad positions as yellow dots + float tileNW_X = (32.0f - terrain.coord.y) * 533.33333f; + float tileNW_Y = (32.0f - terrain.coord.x) * 533.33333f; + for (const auto& dp : terrain.doodadPlacements) { + float u = (tileNW_X - dp.position[1]) / 533.33333f; + float vv = (tileNW_Y - dp.position[0]) / 533.33333f; + int px = static_cast(vv * resolution); + int py = static_cast(u * resolution); + if (px >= 0 && px < resolution && py >= 0 && py < resolution) { + int idx = (py * resolution + px) * 3; + pixels[idx] = 255; pixels[idx+1] = 220; pixels[idx+2] = 50; + } + } + + if (!stbi_write_png(path.c_str(), resolution, resolution, 3, pixels.data(), resolution * 3)) { + LOG_ERROR("Failed to write zone map: ", path); + return false; + } + LOG_INFO("Zone map exported: ", path, " (", resolution, "x", resolution, ")"); + return true; +} + bool WoweeTerrain::importOpen(const std::string& basePath, pipeline::ADTTerrain& terrain) { return pipeline::WoweeTerrainLoader::load(basePath, terrain); } diff --git a/tools/editor/wowee_terrain.hpp b/tools/editor/wowee_terrain.hpp index c9b0e84d..d7ffd092 100644 --- a/tools/editor/wowee_terrain.hpp +++ b/tools/editor/wowee_terrain.hpp @@ -34,6 +34,10 @@ public: static bool exportHoleMask(const pipeline::ADTTerrain& terrain, const std::string& path); + // Export zone overview map as colored PNG (terrain + water + objects) + static bool exportZoneMap(const pipeline::ADTTerrain& terrain, + const std::string& path, int resolution = 512); + // Import terrain from open format back to ADTTerrain static bool importOpen(const std::string& basePath, pipeline::ADTTerrain& terrain); };