refactor(wom): extract WOM->M2 conversion to shared helper

Adds WoweeModelLoader::toM2() and tryLoadByGamePath() to deduplicate the
identical conversion code that lived in editor_viewport for both objects
and NPCs. Cuts ~70 lines of duplicated logic and makes WOM->M2 reusable
across the codebase.
This commit is contained in:
Kelsi 2026-05-06 01:02:56 -07:00
parent eb8f5a09b1
commit 03a863abe1
3 changed files with 80 additions and 81 deletions

View file

@ -9,6 +9,8 @@
namespace wowee {
namespace pipeline {
struct M2Model;
// Wowee Open Model format (.wom) — novel format, no Blizzard IP
// WOM1: static geometry | WOM2: + bones + animations
struct WoweeModel {
@ -68,6 +70,14 @@ public:
// Check if a .wom exists
static bool exists(const std::string& basePath);
// Convert WoweeModel to an in-memory M2Model so the M2Renderer can consume it.
// Single batch, single material — sufficient for static and simple animated meshes.
static M2Model toM2(const WoweeModel& wom);
// Convenience: try loading <path-without-ext>.wom from the standard editor
// search paths (custom_zones/models/, output/models/). Returns valid model on hit.
static WoweeModel tryLoadByGamePath(const std::string& gamePath);
};
} // namespace pipeline

View file

@ -324,5 +324,65 @@ WoweeModel WoweeModelLoader::fromM2(const std::string& m2Path, AssetManager* am)
return model;
}
M2Model WoweeModelLoader::toM2(const WoweeModel& wom) {
M2Model m;
if (!wom.isValid()) return m;
m.name = wom.name;
m.boundRadius = wom.boundRadius;
m.vertices.reserve(wom.vertices.size());
for (const auto& v : wom.vertices) {
M2Vertex mv;
mv.position = v.position;
mv.normal = v.normal;
mv.texCoords[0] = v.texCoord;
std::memcpy(mv.boneWeights, v.boneWeights, 4);
std::memcpy(mv.boneIndices, v.boneIndices, 4);
m.vertices.push_back(mv);
}
m.indices.reserve(wom.indices.size());
for (uint32_t idx : wom.indices)
m.indices.push_back(static_cast<uint16_t>(idx));
for (const auto& tp : wom.texturePaths) {
M2Texture tex;
tex.type = 0;
tex.flags = 0;
tex.filename = tp;
m.textures.push_back(tex);
}
m.textureLookup = {0};
M2Batch batch{};
batch.textureCount = std::min(1u, static_cast<uint32_t>(wom.texturePaths.size()));
batch.indexCount = static_cast<uint32_t>(m.indices.size());
batch.vertexCount = static_cast<uint32_t>(m.vertices.size());
m.batches.push_back(batch);
M2Material mat;
mat.flags = 0;
mat.blendMode = 0;
m.materials.push_back(mat);
return m;
}
WoweeModel WoweeModelLoader::tryLoadByGamePath(const std::string& gamePath) {
std::string base = gamePath;
auto dot = base.rfind('.');
if (dot != std::string::npos) base = base.substr(0, dot);
std::replace(base.begin(), base.end(), '\\', '/');
for (const char* prefix : {"custom_zones/models/", "output/models/"}) {
std::string full = std::string(prefix) + base;
if (exists(full)) {
auto wom = load(full);
if (wom.isValid()) return wom;
}
}
return {};
}
} // namespace pipeline
} // namespace wowee

View file

@ -134,46 +134,11 @@ void EditorViewport::rebuildObjects(const std::vector<PlacedObject>& objects,
pipeline::M2Model model;
bool loaded = false;
// Try WOM open format first
{
std::string womBase = obj.path;
auto womDot = womBase.rfind('.');
if (womDot != std::string::npos) womBase = womBase.substr(0, womDot);
std::replace(womBase.begin(), womBase.end(), '\\', '/');
for (const char* prefix : {"custom_zones/models/", "output/models/"}) {
if (pipeline::WoweeModelLoader::exists(std::string(prefix) + womBase)) {
auto wom = pipeline::WoweeModelLoader::load(std::string(prefix) + womBase);
if (wom.isValid()) {
model.name = wom.name;
model.boundRadius = wom.boundRadius;
for (const auto& v : wom.vertices) {
pipeline::M2Vertex mv;
mv.position = v.position;
mv.normal = v.normal;
mv.texCoords[0] = v.texCoord;
std::memcpy(mv.boneWeights, v.boneWeights, 4);
std::memcpy(mv.boneIndices, v.boneIndices, 4);
model.vertices.push_back(mv);
}
for (uint32_t idx : wom.indices)
model.indices.push_back(static_cast<uint16_t>(idx));
for (const auto& tp : wom.texturePaths) {
pipeline::M2Texture tex; tex.type = 0; tex.flags = 0; tex.filename = tp;
model.textures.push_back(tex);
}
model.textureLookup = {0};
pipeline::M2Batch batch{};
batch.textureCount = std::min(1u, static_cast<uint32_t>(wom.texturePaths.size()));
batch.indexCount = static_cast<uint32_t>(model.indices.size());
batch.vertexCount = static_cast<uint32_t>(model.vertices.size());
model.batches.push_back(batch);
pipeline::M2Material mat; mat.flags = 0; mat.blendMode = 0;
model.materials.push_back(mat);
loaded = true;
break;
}
}
}
// Try WOM open format first (replaces proprietary M2 when available)
if (auto wom = pipeline::WoweeModelLoader::tryLoadByGamePath(obj.path);
wom.isValid()) {
model = pipeline::WoweeModelLoader::toM2(wom);
loaded = true;
}
// Fall back to M2 from game data
@ -277,49 +242,13 @@ void EditorViewport::rebuildObjects(const std::vector<PlacedObject>& objects,
if (it != m2ModelIds.end()) {
modelId = it->second;
} else {
// Try WOM open format first
// Try WOM open format first (replaces proprietary M2 when available)
pipeline::M2Model model;
bool loaded = false;
{
std::string womBase = npc.modelPath;
auto womDot = womBase.rfind('.');
if (womDot != std::string::npos) womBase = womBase.substr(0, womDot);
std::replace(womBase.begin(), womBase.end(), '\\', '/');
for (const char* prefix : {"custom_zones/models/", "output/models/"}) {
if (pipeline::WoweeModelLoader::exists(std::string(prefix) + womBase)) {
auto wom = pipeline::WoweeModelLoader::load(std::string(prefix) + womBase);
if (wom.isValid()) {
model.name = wom.name;
model.boundRadius = wom.boundRadius;
for (const auto& v : wom.vertices) {
pipeline::M2Vertex mv;
mv.position = v.position;
mv.normal = v.normal;
mv.texCoords[0] = v.texCoord;
std::memcpy(mv.boneWeights, v.boneWeights, 4);
std::memcpy(mv.boneIndices, v.boneIndices, 4);
model.vertices.push_back(mv);
}
for (uint32_t idx : wom.indices)
model.indices.push_back(static_cast<uint16_t>(idx));
for (const auto& tp : wom.texturePaths) {
pipeline::M2Texture tex; tex.type = 0; tex.flags = 0; tex.filename = tp;
model.textures.push_back(tex);
}
model.textureLookup = {0};
pipeline::M2Batch batch{};
batch.textureCount = std::min(1u, static_cast<uint32_t>(wom.texturePaths.size()));
batch.indexCount = static_cast<uint32_t>(model.indices.size());
batch.vertexCount = static_cast<uint32_t>(model.vertices.size());
model.batches.push_back(batch);
pipeline::M2Material mat; mat.flags = 0; mat.blendMode = 0;
model.materials.push_back(mat);
loaded = true;
LOG_DEBUG("NPC loaded from WOM: ", prefix, womBase);
break;
}
}
}
if (auto wom = pipeline::WoweeModelLoader::tryLoadByGamePath(npc.modelPath);
wom.isValid()) {
model = pipeline::WoweeModelLoader::toM2(wom);
loaded = true;
}
// Fall back to M2 from game data