feat: Wowee Open Model format (.wom) — novel M2 replacement

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
This commit is contained in:
Kelsi 2026-05-05 10:24:46 -07:00
parent 176115f279
commit b4cb833108
3 changed files with 210 additions and 0 deletions

View file

@ -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

View file

@ -0,0 +1,46 @@
#pragma once
#include <glm/glm.hpp>
#include <string>
#include <vector>
#include <cstdint>
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<Vertex> vertices;
std::vector<uint32_t> indices;
std::vector<std::string> 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

View file

@ -0,0 +1,162 @@
#include "pipeline/wowee_model.hpp"
#include "pipeline/asset_manager.hpp"
#include "pipeline/m2_loader.hpp"
#include "core/logger.hpp"
#include <fstream>
#include <filesystem>
#include <cstring>
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<char*>(&magic), 4);
if (magic != WOM_MAGIC) return model;
uint32_t vertCount, indexCount, texCount;
f.read(reinterpret_cast<char*>(&vertCount), 4);
f.read(reinterpret_cast<char*>(&indexCount), 4);
f.read(reinterpret_cast<char*>(&texCount), 4);
f.read(reinterpret_cast<char*>(&model.boundRadius), 4);
f.read(reinterpret_cast<char*>(&model.boundMin), 12);
f.read(reinterpret_cast<char*>(&model.boundMax), 12);
// Read name
uint16_t nameLen;
f.read(reinterpret_cast<char*>(&nameLen), 2);
model.name.resize(nameLen);
f.read(model.name.data(), nameLen);
// Read vertices
model.vertices.resize(vertCount);
f.read(reinterpret_cast<char*>(model.vertices.data()),
vertCount * sizeof(WoweeModel::Vertex));
// Read indices
model.indices.resize(indexCount);
f.read(reinterpret_cast<char*>(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<char*>(&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<const char*>(&WOM_MAGIC), 4);
uint32_t vertCount = static_cast<uint32_t>(model.vertices.size());
uint32_t indexCount = static_cast<uint32_t>(model.indices.size());
uint32_t texCount = static_cast<uint32_t>(model.texturePaths.size());
f.write(reinterpret_cast<const char*>(&vertCount), 4);
f.write(reinterpret_cast<const char*>(&indexCount), 4);
f.write(reinterpret_cast<const char*>(&texCount), 4);
f.write(reinterpret_cast<const char*>(&model.boundRadius), 4);
f.write(reinterpret_cast<const char*>(&model.boundMin), 12);
f.write(reinterpret_cast<const char*>(&model.boundMax), 12);
uint16_t nameLen = static_cast<uint16_t>(model.name.size());
f.write(reinterpret_cast<const char*>(&nameLen), 2);
f.write(model.name.data(), nameLen);
f.write(reinterpret_cast<const char*>(model.vertices.data()),
vertCount * sizeof(WoweeModel::Vertex));
f.write(reinterpret_cast<const char*>(model.indices.data()),
indexCount * sizeof(uint32_t));
for (const auto& path : model.texturePaths) {
uint16_t pathLen = static_cast<uint16_t>(path.size());
f.write(reinterpret_cast<const char*>(&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<uint32_t>(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