mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-05-06 00:53:52 +00:00
feat: WOT doodad/WMO placements, WOB materials, deduplicate loader
Architecture fixes for open format data fidelity: - WOT now serializes full doodad/WMO placement arrays (positions, rotations, scale, flags, doodad sets) — was only storing counts, causing all placed objects to be lost on WOT round-trip - WOT loader parses placements back into ADTTerrain for client rendering - WOB Material struct added: preserves WMO material flags, shader type, and blend mode during WMO→WOB conversion (was geometry-only) - WOB doodad rotation: quaternion→euler conversion instead of hardcoded zero (placed doodads inside buildings now retain their orientation) - importOpen() deduplicated: delegates to pipeline::WoweeTerrainLoader instead of duplicating 100 lines of parsing code
This commit is contained in:
parent
d00ddd1c73
commit
ca15da5e9b
4 changed files with 100 additions and 110 deletions
|
|
@ -18,11 +18,19 @@ struct WoweeBuilding {
|
|||
glm::vec4 color; // vertex color/lighting
|
||||
};
|
||||
|
||||
struct Material {
|
||||
std::string texturePath;
|
||||
uint32_t flags = 0;
|
||||
uint32_t shader = 0;
|
||||
uint32_t blendMode = 0;
|
||||
};
|
||||
|
||||
struct Group {
|
||||
std::string name;
|
||||
std::vector<Vertex> vertices;
|
||||
std::vector<uint32_t> indices;
|
||||
std::vector<std::string> texturePaths;
|
||||
std::vector<Material> materials;
|
||||
glm::vec3 boundMin{0}, boundMax{0};
|
||||
bool isOutdoor = false;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
#include "pipeline/wowee_building.hpp"
|
||||
#include "pipeline/wmo_loader.hpp"
|
||||
#include "core/logger.hpp"
|
||||
#include <glm/gtc/quaternion.hpp>
|
||||
#include <fstream>
|
||||
#include <filesystem>
|
||||
#include <cstring>
|
||||
|
|
@ -226,13 +227,19 @@ WoweeBuilding WoweeBuildingLoader::fromWMO(const WMOModel& wmo, const std::strin
|
|||
wobGroup.indices.push_back(static_cast<uint32_t>(idx));
|
||||
|
||||
for (const auto& mat : wmo.materials) {
|
||||
WoweeBuilding::Material wobMat;
|
||||
wobMat.flags = mat.flags;
|
||||
wobMat.shader = mat.shader;
|
||||
wobMat.blendMode = mat.blendMode;
|
||||
if (mat.texture1 < wmo.textures.size()) {
|
||||
std::string texPath = wmo.textures[mat.texture1];
|
||||
auto dot = texPath.rfind('.');
|
||||
if (dot != std::string::npos)
|
||||
texPath = texPath.substr(0, dot) + ".png";
|
||||
wobMat.texturePath = texPath;
|
||||
wobGroup.texturePaths.push_back(texPath);
|
||||
}
|
||||
wobGroup.materials.push_back(wobMat);
|
||||
}
|
||||
|
||||
bld.groups.push_back(std::move(wobGroup));
|
||||
|
|
@ -250,7 +257,10 @@ WoweeBuilding WoweeBuildingLoader::fromWMO(const WMOModel& wmo, const std::strin
|
|||
if (dot != std::string::npos)
|
||||
dp.modelPath = dp.modelPath.substr(0, dot) + ".wom";
|
||||
dp.position = doodad.position;
|
||||
dp.rotation = glm::vec3(0.0f);
|
||||
// Convert quaternion rotation to euler angles
|
||||
glm::quat q(doodad.rotation.w, doodad.rotation.x,
|
||||
doodad.rotation.y, doodad.rotation.z);
|
||||
dp.rotation = glm::degrees(glm::eulerAngles(q));
|
||||
dp.scale = doodad.scale;
|
||||
bld.doodads.push_back(dp);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -133,8 +133,54 @@ bool WoweeTerrainLoader::loadMetadata(const std::string& wotPath, ADTTerrain& te
|
|||
}
|
||||
}
|
||||
|
||||
// Parse doodad placements
|
||||
if (j.contains("doodadNames") && j["doodadNames"].is_array()) {
|
||||
for (const auto& n : j["doodadNames"])
|
||||
terrain.doodadNames.push_back(n.get<std::string>());
|
||||
}
|
||||
if (j.contains("doodads") && j["doodads"].is_array()) {
|
||||
for (const auto& jd : j["doodads"]) {
|
||||
ADTTerrain::DoodadPlacement dp{};
|
||||
dp.nameId = jd.value("nameId", 0u);
|
||||
dp.uniqueId = jd.value("uniqueId", 0u);
|
||||
if (jd.contains("pos") && jd["pos"].size() >= 3) {
|
||||
dp.position[0] = jd["pos"][0]; dp.position[1] = jd["pos"][1]; dp.position[2] = jd["pos"][2];
|
||||
}
|
||||
if (jd.contains("rot") && jd["rot"].size() >= 3) {
|
||||
dp.rotation[0] = jd["rot"][0]; dp.rotation[1] = jd["rot"][1]; dp.rotation[2] = jd["rot"][2];
|
||||
}
|
||||
dp.scale = jd.value("scale", 1024);
|
||||
dp.flags = jd.value("flags", 0);
|
||||
terrain.doodadPlacements.push_back(dp);
|
||||
}
|
||||
}
|
||||
|
||||
// Parse WMO placements
|
||||
if (j.contains("wmoNames") && j["wmoNames"].is_array()) {
|
||||
for (const auto& n : j["wmoNames"])
|
||||
terrain.wmoNames.push_back(n.get<std::string>());
|
||||
}
|
||||
if (j.contains("wmos") && j["wmos"].is_array()) {
|
||||
for (const auto& jw : j["wmos"]) {
|
||||
ADTTerrain::WMOPlacement wp{};
|
||||
wp.nameId = jw.value("nameId", 0u);
|
||||
wp.uniqueId = jw.value("uniqueId", 0u);
|
||||
if (jw.contains("pos") && jw["pos"].size() >= 3) {
|
||||
wp.position[0] = jw["pos"][0]; wp.position[1] = jw["pos"][1]; wp.position[2] = jw["pos"][2];
|
||||
}
|
||||
if (jw.contains("rot") && jw["rot"].size() >= 3) {
|
||||
wp.rotation[0] = jw["rot"][0]; wp.rotation[1] = jw["rot"][1]; wp.rotation[2] = jw["rot"][2];
|
||||
}
|
||||
wp.flags = jw.value("flags", 0);
|
||||
wp.doodadSet = jw.value("doodadSet", 0);
|
||||
terrain.wmoPlacements.push_back(wp);
|
||||
}
|
||||
}
|
||||
|
||||
LOG_INFO("WOT loaded: ", wotPath, " (tile [", terrain.coord.x, ",", terrain.coord.y,
|
||||
"], ", terrain.textures.size(), " textures)");
|
||||
"], ", terrain.textures.size(), " textures, ",
|
||||
terrain.doodadPlacements.size(), " doodads, ",
|
||||
terrain.wmoPlacements.size(), " WMOs)");
|
||||
return true;
|
||||
} catch (const std::exception& e) {
|
||||
LOG_ERROR("Failed to parse WOT: ", e.what());
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
#include "wowee_terrain.hpp"
|
||||
#include "pipeline/wowee_terrain_loader.hpp"
|
||||
#include "core/logger.hpp"
|
||||
#include "stb_image_write.h"
|
||||
#include <nlohmann/json.hpp>
|
||||
|
|
@ -86,8 +87,38 @@ bool WoweeTerrain::exportOpen(const pipeline::ADTTerrain& terrain,
|
|||
}
|
||||
}
|
||||
j["water"] = waterArr;
|
||||
j["doodadCount"] = terrain.doodadPlacements.size();
|
||||
j["wmoCount"] = terrain.wmoPlacements.size();
|
||||
|
||||
// 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;
|
||||
|
||||
std::ofstream f(jsonPath);
|
||||
if (!f) return false;
|
||||
|
|
@ -217,112 +248,7 @@ int WoweeTerrain::exportAlphaMaps(const pipeline::ADTTerrain& terrain,
|
|||
}
|
||||
|
||||
bool WoweeTerrain::importOpen(const std::string& basePath, pipeline::ADTTerrain& terrain) {
|
||||
// Load binary heightmap (.whm)
|
||||
std::string hmPath = basePath + ".whm";
|
||||
std::ifstream f(hmPath, std::ios::binary);
|
||||
if (!f) return false;
|
||||
|
||||
uint32_t magic, chunks, verts;
|
||||
f.read(reinterpret_cast<char*>(&magic), 4);
|
||||
if (magic != 0x314D4857) return false;
|
||||
f.read(reinterpret_cast<char*>(&chunks), 4);
|
||||
f.read(reinterpret_cast<char*>(&verts), 4);
|
||||
if (chunks != 256 || verts != 145) return false;
|
||||
|
||||
terrain.loaded = true;
|
||||
terrain.version = 18;
|
||||
for (int ci = 0; ci < 256; ci++) {
|
||||
auto& chunk = terrain.chunks[ci];
|
||||
chunk.heightMap.loaded = true;
|
||||
chunk.indexX = ci % 16;
|
||||
chunk.indexY = ci / 16;
|
||||
float base;
|
||||
f.read(reinterpret_cast<char*>(&base), 4);
|
||||
chunk.position[2] = base;
|
||||
f.read(reinterpret_cast<char*>(chunk.heightMap.heights.data()), 145 * 4);
|
||||
|
||||
uint32_t alphaSize = 0;
|
||||
if (f.read(reinterpret_cast<char*>(&alphaSize), 4) && alphaSize > 0 && alphaSize <= 65536) {
|
||||
chunk.alphaMap.resize(alphaSize);
|
||||
f.read(reinterpret_cast<char*>(chunk.alphaMap.data()), alphaSize);
|
||||
}
|
||||
|
||||
for (int i = 0; i < 145; i++) {
|
||||
chunk.normals[i * 3 + 0] = 0;
|
||||
chunk.normals[i * 3 + 1] = 0;
|
||||
chunk.normals[i * 3 + 2] = 127;
|
||||
}
|
||||
}
|
||||
|
||||
// Load JSON metadata (.wot)
|
||||
std::string wotPath = basePath + ".wot";
|
||||
std::ifstream wf(wotPath);
|
||||
if (wf) {
|
||||
try {
|
||||
auto j = nlohmann::json::parse(wf);
|
||||
|
||||
terrain.coord.x = j.value("tileX", 0);
|
||||
terrain.coord.y = j.value("tileY", 0);
|
||||
|
||||
float tileSize = 533.33333f;
|
||||
float chunkSize = tileSize / 16.0f;
|
||||
for (int cy = 0; cy < 16; cy++) {
|
||||
for (int cx = 0; cx < 16; cx++) {
|
||||
auto& chunk = terrain.chunks[cy * 16 + cx];
|
||||
chunk.position[0] = (32.0f - terrain.coord.x) * tileSize - cx * chunkSize;
|
||||
chunk.position[1] = (32.0f - terrain.coord.y) * tileSize - cy * chunkSize;
|
||||
}
|
||||
}
|
||||
|
||||
if (j.contains("textures") && j["textures"].is_array()) {
|
||||
for (const auto& tex : j["textures"]) {
|
||||
if (tex.is_string() && !tex.get<std::string>().empty())
|
||||
terrain.textures.push_back(tex.get<std::string>());
|
||||
}
|
||||
}
|
||||
|
||||
if (j.contains("chunkLayers") && j["chunkLayers"].is_array()) {
|
||||
const auto& layers = j["chunkLayers"];
|
||||
for (int ci = 0; ci < std::min(256, static_cast<int>(layers.size())); ci++) {
|
||||
const auto& cl = layers[ci];
|
||||
if (cl.contains("layers") && cl["layers"].is_array()) {
|
||||
for (const auto& texId : cl["layers"]) {
|
||||
pipeline::TextureLayer layer{};
|
||||
layer.textureId = texId.get<uint32_t>();
|
||||
layer.flags = terrain.chunks[ci].layers.empty() ? 0 : 0x100;
|
||||
terrain.chunks[ci].layers.push_back(layer);
|
||||
}
|
||||
}
|
||||
if (cl.contains("holes"))
|
||||
terrain.chunks[ci].holes = cl["holes"].get<uint16_t>();
|
||||
}
|
||||
}
|
||||
|
||||
if (j.contains("water") && j["water"].is_array()) {
|
||||
for (const auto& w : j["water"]) {
|
||||
if (w.is_null()) continue;
|
||||
int wci = w.value("chunk", -1);
|
||||
if (wci < 0 || wci >= 256) continue;
|
||||
pipeline::ADTTerrain::WaterLayer wl;
|
||||
wl.liquidType = w.value("type", 0u);
|
||||
wl.maxHeight = w.value("height", 0.0f);
|
||||
wl.minHeight = wl.maxHeight;
|
||||
wl.x = 0; wl.y = 0; wl.width = 9; wl.height = 9;
|
||||
wl.heights.assign(81, wl.maxHeight);
|
||||
wl.mask.assign(8, 0xFF);
|
||||
terrain.waterData[wci].layers.push_back(wl);
|
||||
}
|
||||
}
|
||||
|
||||
LOG_INFO("WOT metadata loaded: tile [", terrain.coord.x, ",", terrain.coord.y,
|
||||
"], ", terrain.textures.size(), " textures");
|
||||
} catch (const std::exception& e) {
|
||||
LOG_WARNING("Could not parse WOT metadata: ", e.what());
|
||||
}
|
||||
}
|
||||
|
||||
LOG_INFO("Open terrain imported: ", basePath);
|
||||
return true;
|
||||
return pipeline::WoweeTerrainLoader::load(basePath, terrain);
|
||||
}
|
||||
|
||||
} // namespace editor
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue