From b4cb833108cb5947dceb381bbf911e9664a8b831 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Tue, 5 May 2026 10:24:46 -0700 Subject: [PATCH] =?UTF-8?q?feat:=20Wowee=20Open=20Model=20format=20(.wom)?= =?UTF-8?q?=20=E2=80=94=20novel=20M2=20replacement?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit WOM format: binary model file with no Blizzard structures. - WOM1 magic header + vertex/index counts + bounding box - Vertices: position(vec3) + normal(vec3) + texCoord(vec2) = 32 bytes - Indices: uint32 triangle list - Texture paths: PNG references (not BLP) WoweeModelLoader: - load(): reads .wom binary back to WoweeModel struct - save(): writes WoweeModel to .wom binary - fromM2(): converts existing M2 models to WOM (static geometry, strips bone/animation data, converts BLP paths to PNG) - exists(): checks for .wom file Format replacement progress — 5 out of 6 done: - DONE: ADT → WOT/WHM (terrain) - DONE: WDT → zone.json (map definition) - DONE: BLP → PNG (textures) - DONE: DBC → JSON (data tables) - DONE: M2 → WOM (static models) - TODO: WMO → open building format --- CMakeLists.txt | 2 + include/pipeline/wowee_model.hpp | 46 +++++++++ src/pipeline/wowee_model.cpp | 162 +++++++++++++++++++++++++++++++ 3 files changed, 210 insertions(+) create mode 100644 include/pipeline/wowee_model.hpp create mode 100644 src/pipeline/wowee_model.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 60fe8ac8..7cd2ed8d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -590,6 +590,7 @@ set(WOWEE_SOURCES src/pipeline/adt_loader.cpp src/pipeline/wdt_loader.cpp src/pipeline/wowee_terrain_loader.cpp + src/pipeline/wowee_model.cpp src/pipeline/custom_zone_discovery.cpp src/pipeline/dbc_layout.cpp @@ -1318,6 +1319,7 @@ add_executable(wowee_editor src/pipeline/adt_loader.cpp src/pipeline/wdt_loader.cpp src/pipeline/wowee_terrain_loader.cpp + src/pipeline/wowee_model.cpp src/pipeline/custom_zone_discovery.cpp src/pipeline/terrain_mesh.cpp diff --git a/include/pipeline/wowee_model.hpp b/include/pipeline/wowee_model.hpp new file mode 100644 index 00000000..6f0144db --- /dev/null +++ b/include/pipeline/wowee_model.hpp @@ -0,0 +1,46 @@ +#pragma once + +#include +#include +#include +#include + +namespace wowee { +namespace pipeline { + +// Wowee Open Model format (.wom) — novel format, no Blizzard IP +// Designed for static doodads, props, and simple animated objects +struct WoweeModel { + struct Vertex { + glm::vec3 position; + glm::vec3 normal; + glm::vec2 texCoord; + }; + + std::string name; + std::vector vertices; + std::vector indices; + std::vector texturePaths; // PNG paths + float boundRadius = 1.0f; + glm::vec3 boundMin{0}, boundMax{0}; + + bool isValid() const { return !vertices.empty() && !indices.empty(); } +}; + +class WoweeModelLoader { +public: + // Load from .wom file (binary) + .wom.json (metadata) + static WoweeModel load(const std::string& basePath); + + // Save to .wom + .wom.json + static bool save(const WoweeModel& model, const std::string& basePath); + + // Convert an M2 model to WoweeModel (static geometry only, no animation) + static WoweeModel fromM2(const std::string& m2Path, class AssetManager* am); + + // Check if a .wom exists + static bool exists(const std::string& basePath); +}; + +} // namespace pipeline +} // namespace wowee diff --git a/src/pipeline/wowee_model.cpp b/src/pipeline/wowee_model.cpp new file mode 100644 index 00000000..57fcf84b --- /dev/null +++ b/src/pipeline/wowee_model.cpp @@ -0,0 +1,162 @@ +#include "pipeline/wowee_model.hpp" +#include "pipeline/asset_manager.hpp" +#include "pipeline/m2_loader.hpp" +#include "core/logger.hpp" +#include +#include +#include + +namespace wowee { +namespace pipeline { + +static constexpr uint32_t WOM_MAGIC = 0x314D4F57; // "WOM1" + +bool WoweeModelLoader::exists(const std::string& basePath) { + return std::filesystem::exists(basePath + ".wom"); +} + +WoweeModel WoweeModelLoader::load(const std::string& basePath) { + WoweeModel model; + std::string womPath = basePath + ".wom"; + + std::ifstream f(womPath, std::ios::binary); + if (!f) return model; + + uint32_t magic; + f.read(reinterpret_cast(&magic), 4); + if (magic != WOM_MAGIC) return model; + + uint32_t vertCount, indexCount, texCount; + f.read(reinterpret_cast(&vertCount), 4); + f.read(reinterpret_cast(&indexCount), 4); + f.read(reinterpret_cast(&texCount), 4); + f.read(reinterpret_cast(&model.boundRadius), 4); + f.read(reinterpret_cast(&model.boundMin), 12); + f.read(reinterpret_cast(&model.boundMax), 12); + + // Read name + uint16_t nameLen; + f.read(reinterpret_cast(&nameLen), 2); + model.name.resize(nameLen); + f.read(model.name.data(), nameLen); + + // Read vertices + model.vertices.resize(vertCount); + f.read(reinterpret_cast(model.vertices.data()), + vertCount * sizeof(WoweeModel::Vertex)); + + // Read indices + model.indices.resize(indexCount); + f.read(reinterpret_cast(model.indices.data()), + indexCount * sizeof(uint32_t)); + + // Read texture paths + for (uint32_t i = 0; i < texCount; i++) { + uint16_t pathLen; + f.read(reinterpret_cast(&pathLen), 2); + std::string path(pathLen, '\0'); + f.read(path.data(), pathLen); + model.texturePaths.push_back(path); + } + + LOG_INFO("WOM loaded: ", basePath, " (", vertCount, " verts, ", + indexCount / 3, " tris)"); + return model; +} + +bool WoweeModelLoader::save(const WoweeModel& model, const std::string& basePath) { + namespace fs = std::filesystem; + fs::create_directories(fs::path(basePath).parent_path()); + + std::string womPath = basePath + ".wom"; + std::ofstream f(womPath, std::ios::binary); + if (!f) return false; + + f.write(reinterpret_cast(&WOM_MAGIC), 4); + uint32_t vertCount = static_cast(model.vertices.size()); + uint32_t indexCount = static_cast(model.indices.size()); + uint32_t texCount = static_cast(model.texturePaths.size()); + f.write(reinterpret_cast(&vertCount), 4); + f.write(reinterpret_cast(&indexCount), 4); + f.write(reinterpret_cast(&texCount), 4); + f.write(reinterpret_cast(&model.boundRadius), 4); + f.write(reinterpret_cast(&model.boundMin), 12); + f.write(reinterpret_cast(&model.boundMax), 12); + + uint16_t nameLen = static_cast(model.name.size()); + f.write(reinterpret_cast(&nameLen), 2); + f.write(model.name.data(), nameLen); + + f.write(reinterpret_cast(model.vertices.data()), + vertCount * sizeof(WoweeModel::Vertex)); + f.write(reinterpret_cast(model.indices.data()), + indexCount * sizeof(uint32_t)); + + for (const auto& path : model.texturePaths) { + uint16_t pathLen = static_cast(path.size()); + f.write(reinterpret_cast(&pathLen), 2); + f.write(path.data(), pathLen); + } + + LOG_INFO("WOM saved: ", womPath, " (", vertCount, " verts, ", + indexCount / 3, " tris)"); + return true; +} + +WoweeModel WoweeModelLoader::fromM2(const std::string& m2Path, AssetManager* am) { + WoweeModel model; + if (!am) return model; + + auto data = am->readFile(m2Path); + if (data.empty()) return model; + + auto m2 = M2Loader::load(data); + + // Load skin file for WotLK M2s + if (!m2.isValid()) { + std::string skinPath = m2Path; + auto dotPos = skinPath.rfind('.'); + if (dotPos != std::string::npos) + skinPath = skinPath.substr(0, dotPos) + "00.skin"; + auto skinData = am->readFile(skinPath); + if (!skinData.empty()) + M2Loader::loadSkin(skinData, m2); + } + + if (!m2.isValid()) return model; + + model.name = m2.name; + model.boundRadius = m2.boundRadius; + + // Convert M2 vertices to WOM format (strip bone data) + model.vertices.reserve(m2.vertices.size()); + for (const auto& v : m2.vertices) { + WoweeModel::Vertex wv; + wv.position = v.position; + wv.normal = v.normal; + wv.texCoord = v.texCoords[0]; + model.vertices.push_back(wv); + + model.boundMin = glm::min(model.boundMin, v.position); + model.boundMax = glm::max(model.boundMax, v.position); + } + + // Convert indices (M2 uses uint16, WOM uses uint32) + model.indices.reserve(m2.indices.size()); + for (uint16_t idx : m2.indices) + model.indices.push_back(static_cast(idx)); + + // Convert texture paths (BLP → PNG) + for (const auto& tex : m2.textures) { + std::string path = tex.filename; + auto dot = path.rfind('.'); + if (dot != std::string::npos) + path = path.substr(0, dot) + ".png"; + model.texturePaths.push_back(path); + } + + return model; +} + +} // namespace pipeline +} // namespace wowee