2026-05-05 09:32:13 -07:00
|
|
|
|
#include "wowee_terrain.hpp"
|
2026-05-05 14:44:46 -07:00
|
|
|
|
#include "pipeline/wowee_terrain_loader.hpp"
|
2026-05-05 09:32:13 -07:00
|
|
|
|
#include "core/logger.hpp"
|
2026-05-05 11:01:37 -07:00
|
|
|
|
#include "stb_image_write.h"
|
2026-05-05 13:04:51 -07:00
|
|
|
|
#include <nlohmann/json.hpp>
|
2026-05-05 09:32:13 -07:00
|
|
|
|
#include <fstream>
|
|
|
|
|
|
#include <filesystem>
|
|
|
|
|
|
#include <cstring>
|
2026-05-06 06:32:24 -07:00
|
|
|
|
#include <cmath>
|
2026-05-05 09:32:13 -07:00
|
|
|
|
|
|
|
|
|
|
namespace wowee {
|
|
|
|
|
|
namespace editor {
|
|
|
|
|
|
|
|
|
|
|
|
bool WoweeTerrain::exportOpen(const pipeline::ADTTerrain& terrain,
|
|
|
|
|
|
const std::string& basePath, int tileX, int tileY) {
|
|
|
|
|
|
namespace fs = std::filesystem;
|
|
|
|
|
|
fs::create_directories(fs::path(basePath).parent_path());
|
|
|
|
|
|
|
|
|
|
|
|
// Export binary heightmap (.whm = Wowee HeightMap)
|
|
|
|
|
|
// Format: 256 chunks × 145 floats = 37120 floats (148480 bytes)
|
|
|
|
|
|
std::string hmPath = basePath + ".whm";
|
|
|
|
|
|
{
|
|
|
|
|
|
std::ofstream f(hmPath, std::ios::binary);
|
|
|
|
|
|
if (!f) return false;
|
|
|
|
|
|
// Header: "WHM1" + chunkCount(4) + vertsPerChunk(4)
|
|
|
|
|
|
uint32_t magic = 0x314D4857; // "WHM1"
|
|
|
|
|
|
uint32_t chunks = 256, verts = 145;
|
|
|
|
|
|
f.write(reinterpret_cast<const char*>(&magic), 4);
|
|
|
|
|
|
f.write(reinterpret_cast<const char*>(&chunks), 4);
|
|
|
|
|
|
f.write(reinterpret_cast<const char*>(&verts), 4);
|
2026-05-05 13:04:51 -07:00
|
|
|
|
// Per-chunk: baseHeight(4) + heights[145](580) + alphaSize(4) + alphaData(N)
|
2026-05-05 09:32:13 -07:00
|
|
|
|
for (int ci = 0; ci < 256; ci++) {
|
|
|
|
|
|
const auto& chunk = terrain.chunks[ci];
|
2026-05-06 06:32:24 -07:00
|
|
|
|
// Sanitize base + heights on save so a corrupt in-memory terrain
|
|
|
|
|
|
// (e.g. mid-edit NaN spike) doesn't get persisted into the WHM
|
|
|
|
|
|
// and require the load-time guard to clean up forever after.
|
2026-05-05 09:32:13 -07:00
|
|
|
|
float base = chunk.position[2];
|
2026-05-06 06:32:24 -07:00
|
|
|
|
if (!std::isfinite(base)) base = 0.0f;
|
2026-05-05 09:32:13 -07:00
|
|
|
|
f.write(reinterpret_cast<const char*>(&base), 4);
|
2026-05-06 06:32:24 -07:00
|
|
|
|
float clean[145];
|
|
|
|
|
|
for (int i = 0; i < 145; i++) {
|
|
|
|
|
|
clean[i] = chunk.heightMap.heights[i];
|
|
|
|
|
|
if (!std::isfinite(clean[i])) clean[i] = 0.0f;
|
|
|
|
|
|
}
|
|
|
|
|
|
f.write(reinterpret_cast<const char*>(clean), 145 * 4);
|
|
|
|
|
|
// Cap alpha size at 64KB (matches loader cap) — alphaMap is
|
|
|
|
|
|
// bounded in practice but defensive truncation prevents a
|
|
|
|
|
|
// stale memory state from producing an unloadable WHM.
|
|
|
|
|
|
uint32_t alphaSize = std::min<uint32_t>(
|
|
|
|
|
|
static_cast<uint32_t>(chunk.alphaMap.size()), 65536);
|
2026-05-05 13:04:51 -07:00
|
|
|
|
f.write(reinterpret_cast<const char*>(&alphaSize), 4);
|
|
|
|
|
|
if (alphaSize > 0) {
|
|
|
|
|
|
f.write(reinterpret_cast<const char*>(chunk.alphaMap.data()), alphaSize);
|
|
|
|
|
|
}
|
2026-05-05 09:32:13 -07:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Export JSON metadata (.wot = Wowee Open Terrain)
|
|
|
|
|
|
std::string jsonPath = basePath + ".wot";
|
|
|
|
|
|
{
|
2026-05-05 13:04:51 -07:00
|
|
|
|
nlohmann::json j;
|
|
|
|
|
|
j["format"] = "wot-1.0";
|
|
|
|
|
|
j["editor"] = "wowee-editor-1.0.0";
|
|
|
|
|
|
j["tileX"] = tileX;
|
|
|
|
|
|
j["tileY"] = tileY;
|
|
|
|
|
|
j["chunkGrid"] = {16, 16};
|
|
|
|
|
|
j["vertsPerChunk"] = 145;
|
|
|
|
|
|
j["heightmapFile"] = fs::path(hmPath).filename().string();
|
|
|
|
|
|
j["tileSize"] = 533.33333f;
|
|
|
|
|
|
j["chunkSize"] = 33.33333f;
|
|
|
|
|
|
|
|
|
|
|
|
nlohmann::json texArr = nlohmann::json::array();
|
|
|
|
|
|
for (const auto& tex : terrain.textures) texArr.push_back(tex);
|
|
|
|
|
|
j["textures"] = texArr;
|
|
|
|
|
|
|
|
|
|
|
|
nlohmann::json chunkArr = nlohmann::json::array();
|
2026-05-05 09:32:13 -07:00
|
|
|
|
for (int ci = 0; ci < 256; ci++) {
|
|
|
|
|
|
const auto& chunk = terrain.chunks[ci];
|
2026-05-05 13:04:51 -07:00
|
|
|
|
nlohmann::json cl;
|
|
|
|
|
|
nlohmann::json layerIds = nlohmann::json::array();
|
|
|
|
|
|
for (const auto& layer : chunk.layers) layerIds.push_back(layer.textureId);
|
|
|
|
|
|
cl["layers"] = layerIds;
|
|
|
|
|
|
cl["holes"] = chunk.holes;
|
2026-05-05 11:12:36 -07:00
|
|
|
|
bool hasAlpha = false;
|
|
|
|
|
|
for (size_t li = 1; li < chunk.layers.size(); li++)
|
|
|
|
|
|
if (chunk.layers[li].useAlpha()) { hasAlpha = true; break; }
|
2026-05-05 13:04:51 -07:00
|
|
|
|
cl["hasAlpha"] = hasAlpha;
|
|
|
|
|
|
chunkArr.push_back(cl);
|
2026-05-05 09:32:13 -07:00
|
|
|
|
}
|
2026-05-05 13:04:51 -07:00
|
|
|
|
j["chunkLayers"] = chunkArr;
|
|
|
|
|
|
|
|
|
|
|
|
nlohmann::json waterArr = nlohmann::json::array();
|
2026-05-05 09:32:13 -07:00
|
|
|
|
for (int ci = 0; ci < 256; ci++) {
|
|
|
|
|
|
const auto& water = terrain.waterData[ci];
|
|
|
|
|
|
if (water.hasWater()) {
|
2026-05-05 13:04:51 -07:00
|
|
|
|
waterArr.push_back({{"chunk", ci},
|
|
|
|
|
|
{"type", water.layers[0].liquidType},
|
|
|
|
|
|
{"height", water.layers[0].maxHeight}});
|
2026-05-05 09:32:13 -07:00
|
|
|
|
} else {
|
2026-05-05 13:04:51 -07:00
|
|
|
|
waterArr.push_back(nullptr);
|
2026-05-05 09:32:13 -07:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-05-05 13:04:51 -07:00
|
|
|
|
j["water"] = waterArr;
|
2026-05-05 14:44:46 -07:00
|
|
|
|
|
|
|
|
|
|
// Doodad placements (M2 models on terrain)
|
|
|
|
|
|
nlohmann::json doodadNames = nlohmann::json::array();
|
|
|
|
|
|
for (const auto& n : terrain.doodadNames) doodadNames.push_back(n);
|
|
|
|
|
|
j["doodadNames"] = doodadNames;
|
|
|
|
|
|
|
|
|
|
|
|
nlohmann::json doodads = nlohmann::json::array();
|
|
|
|
|
|
for (const auto& dp : terrain.doodadPlacements) {
|
|
|
|
|
|
doodads.push_back({
|
|
|
|
|
|
{"nameId", dp.nameId}, {"uniqueId", dp.uniqueId},
|
|
|
|
|
|
{"pos", {dp.position[0], dp.position[1], dp.position[2]}},
|
|
|
|
|
|
{"rot", {dp.rotation[0], dp.rotation[1], dp.rotation[2]}},
|
|
|
|
|
|
{"scale", dp.scale}, {"flags", dp.flags}
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
j["doodads"] = doodads;
|
|
|
|
|
|
|
|
|
|
|
|
// WMO placements (buildings on terrain)
|
|
|
|
|
|
nlohmann::json wmoNames = nlohmann::json::array();
|
|
|
|
|
|
for (const auto& n : terrain.wmoNames) wmoNames.push_back(n);
|
|
|
|
|
|
j["wmoNames"] = wmoNames;
|
|
|
|
|
|
|
|
|
|
|
|
nlohmann::json wmos = nlohmann::json::array();
|
|
|
|
|
|
for (const auto& wp : terrain.wmoPlacements) {
|
|
|
|
|
|
wmos.push_back({
|
|
|
|
|
|
{"nameId", wp.nameId}, {"uniqueId", wp.uniqueId},
|
|
|
|
|
|
{"pos", {wp.position[0], wp.position[1], wp.position[2]}},
|
|
|
|
|
|
{"rot", {wp.rotation[0], wp.rotation[1], wp.rotation[2]}},
|
|
|
|
|
|
{"flags", wp.flags}, {"doodadSet", wp.doodadSet}
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
j["wmos"] = wmos;
|
2026-05-05 13:04:51 -07:00
|
|
|
|
|
|
|
|
|
|
std::ofstream f(jsonPath);
|
|
|
|
|
|
if (!f) return false;
|
|
|
|
|
|
f << j.dump(2) << "\n";
|
2026-05-05 09:32:13 -07:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
LOG_INFO("Open terrain exported: ", basePath, " (.wot + .whm)");
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-05 11:01:37 -07:00
|
|
|
|
bool WoweeTerrain::exportNormalMap(const pipeline::ADTTerrain& terrain,
|
|
|
|
|
|
const std::string& path) {
|
|
|
|
|
|
// Export 129x129 normal map as RGB PNG
|
|
|
|
|
|
constexpr int res = 129;
|
|
|
|
|
|
std::vector<uint8_t> pixels(res * res * 3);
|
|
|
|
|
|
|
|
|
|
|
|
for (int cy = 0; cy < 16; cy++) {
|
|
|
|
|
|
for (int cx = 0; cx < 16; cx++) {
|
|
|
|
|
|
const 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;
|
|
|
|
|
|
if (col > 8) continue;
|
|
|
|
|
|
int px = cx * 8 + col, py = cy * 8 + row;
|
|
|
|
|
|
if (px >= res || py >= res) continue;
|
|
|
|
|
|
|
|
|
|
|
|
int ni = v * 3;
|
|
|
|
|
|
float nx = static_cast<float>(chunk.normals[ni]) / 127.0f;
|
|
|
|
|
|
float ny = static_cast<float>(chunk.normals[ni + 1]) / 127.0f;
|
|
|
|
|
|
float nz = static_cast<float>(chunk.normals[ni + 2]) / 127.0f;
|
|
|
|
|
|
|
|
|
|
|
|
int idx = (py * res + px) * 3;
|
|
|
|
|
|
pixels[idx] = static_cast<uint8_t>((nx * 0.5f + 0.5f) * 255);
|
|
|
|
|
|
pixels[idx + 1] = static_cast<uint8_t>((ny * 0.5f + 0.5f) * 255);
|
|
|
|
|
|
pixels[idx + 2] = static_cast<uint8_t>((nz * 0.5f + 0.5f) * 255);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
stbi_write_png(path.c_str(), res, res, 3, pixels.data(), res * 3);
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-05 11:14:58 -07:00
|
|
|
|
bool WoweeTerrain::exportHeightmapPreview(const pipeline::ADTTerrain& terrain,
|
|
|
|
|
|
const std::string& path) {
|
|
|
|
|
|
constexpr int res = 129;
|
|
|
|
|
|
std::vector<uint8_t> pixels(res * res);
|
|
|
|
|
|
|
|
|
|
|
|
float minH = 1e30f, maxH = -1e30f;
|
|
|
|
|
|
for (int ci = 0; ci < 256; ci++) {
|
|
|
|
|
|
const auto& chunk = terrain.chunks[ci];
|
|
|
|
|
|
if (!chunk.hasHeightMap()) continue;
|
|
|
|
|
|
for (int v = 0; v < 145; v++) {
|
|
|
|
|
|
float h = chunk.position[2] + chunk.heightMap.heights[v];
|
|
|
|
|
|
minH = std::min(minH, h);
|
|
|
|
|
|
maxH = std::max(maxH, h);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
float range = std::max(1.0f, maxH - minH);
|
|
|
|
|
|
|
|
|
|
|
|
for (int cy = 0; cy < 16; cy++) {
|
|
|
|
|
|
for (int cx = 0; cx < 16; cx++) {
|
|
|
|
|
|
const 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;
|
|
|
|
|
|
if (col > 8) continue;
|
|
|
|
|
|
int px = cx * 8 + col, py = cy * 8 + row;
|
|
|
|
|
|
if (px >= res || py >= res) continue;
|
|
|
|
|
|
float h = chunk.position[2] + chunk.heightMap.heights[v];
|
|
|
|
|
|
float t = (h - minH) / range;
|
|
|
|
|
|
pixels[py * res + px] = static_cast<uint8_t>(t * 255.0f);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
std::filesystem::create_directories(std::filesystem::path(path).parent_path());
|
|
|
|
|
|
stbi_write_png(path.c_str(), res, res, 1, pixels.data(), res);
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-05 11:36:04 -07:00
|
|
|
|
bool WoweeTerrain::exportWaterMask(const pipeline::ADTTerrain& terrain,
|
|
|
|
|
|
const std::string& path) {
|
|
|
|
|
|
constexpr int res = 16; // One pixel per chunk
|
|
|
|
|
|
std::vector<uint8_t> pixels(res * res);
|
|
|
|
|
|
for (int ci = 0; ci < 256; ci++)
|
|
|
|
|
|
pixels[ci] = terrain.waterData[ci].hasWater() ? 255 : 0;
|
|
|
|
|
|
|
|
|
|
|
|
std::filesystem::create_directories(std::filesystem::path(path).parent_path());
|
|
|
|
|
|
stbi_write_png(path.c_str(), res, res, 1, pixels.data(), res);
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-05 11:43:23 -07:00
|
|
|
|
bool WoweeTerrain::exportHoleMask(const pipeline::ADTTerrain& terrain,
|
|
|
|
|
|
const std::string& path) {
|
|
|
|
|
|
constexpr int res = 16;
|
|
|
|
|
|
std::vector<uint8_t> pixels(res * res);
|
|
|
|
|
|
for (int ci = 0; ci < 256; ci++)
|
|
|
|
|
|
pixels[ci] = (terrain.chunks[ci].holes != 0) ? 255 : 0;
|
|
|
|
|
|
|
|
|
|
|
|
std::filesystem::create_directories(std::filesystem::path(path).parent_path());
|
|
|
|
|
|
stbi_write_png(path.c_str(), res, res, 1, pixels.data(), res);
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-05 11:08:40 -07:00
|
|
|
|
int WoweeTerrain::exportAlphaMaps(const pipeline::ADTTerrain& terrain,
|
|
|
|
|
|
const std::string& outputDir) {
|
|
|
|
|
|
namespace fs = std::filesystem;
|
|
|
|
|
|
fs::create_directories(outputDir);
|
|
|
|
|
|
int exported = 0;
|
|
|
|
|
|
|
|
|
|
|
|
for (int ci = 0; ci < 256; ci++) {
|
|
|
|
|
|
const auto& chunk = terrain.chunks[ci];
|
|
|
|
|
|
for (size_t li = 1; li < chunk.layers.size(); li++) {
|
|
|
|
|
|
if (!chunk.layers[li].useAlpha()) continue;
|
|
|
|
|
|
size_t off = chunk.layers[li].offsetMCAL;
|
|
|
|
|
|
if (off + 4096 > chunk.alphaMap.size()) continue;
|
|
|
|
|
|
|
|
|
|
|
|
std::string path = outputDir + "/chunk_" + std::to_string(ci) +
|
|
|
|
|
|
"_layer_" + std::to_string(li) + ".png";
|
|
|
|
|
|
stbi_write_png(path.c_str(), 64, 64, 1,
|
|
|
|
|
|
chunk.alphaMap.data() + off, 64);
|
|
|
|
|
|
exported++;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return exported;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-05 16:01:29 -07:00
|
|
|
|
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<uint8_t> 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<float>(px) / resolution;
|
|
|
|
|
|
float v = static_cast<float>(py) / resolution;
|
|
|
|
|
|
|
|
|
|
|
|
int cx = static_cast<int>(u * 16); cx = std::clamp(cx, 0, 15);
|
|
|
|
|
|
int cy = static_cast<int>(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<int>(localU), 0, 7);
|
|
|
|
|
|
int gy = std::clamp(static_cast<int>(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<uint8_t>(std::clamp(r, 0.0f, 1.0f) * 255);
|
|
|
|
|
|
pixels[idx+1] = static_cast<uint8_t>(std::clamp(g, 0.0f, 1.0f) * 255);
|
|
|
|
|
|
pixels[idx+2] = static_cast<uint8_t>(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<int>(vv * resolution);
|
|
|
|
|
|
int py = static_cast<int>(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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-05 09:32:13 -07:00
|
|
|
|
bool WoweeTerrain::importOpen(const std::string& basePath, pipeline::ADTTerrain& terrain) {
|
2026-05-05 14:44:46 -07:00
|
|
|
|
return pipeline::WoweeTerrainLoader::load(basePath, terrain);
|
2026-05-05 09:32:13 -07:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
} // namespace editor
|
|
|
|
|
|
} // namespace wowee
|