mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-05-06 09:03:52 +00:00
feat(wom): add WOM3 multi-batch format for material-aware models
WOM1/WOM2 had a single mesh with one texture, which lost the multi-submesh structure of complex M2 models (body+hair+eyes+armor each need different textures and blend modes). WOM3 adds a Batch array: each batch has indexStart/indexCount + a textureIndex into texturePaths + blendMode + flags. Loader is fully backward compatible: WOM1/WOM2 files still load, and WOM3 with no batches block falls back to a single full-mesh batch. fromM2 now extracts batches with materials, and toM2 emits matching M2 batches so the renderer can draw them correctly.
This commit is contained in:
parent
00c078a9af
commit
b736c6b2e1
3 changed files with 124 additions and 30 deletions
|
|
@ -12,7 +12,7 @@ namespace pipeline {
|
|||
struct M2Model;
|
||||
|
||||
// Wowee Open Model format (.wom) — novel format, no Blizzard IP
|
||||
// WOM1: static geometry | WOM2: + bones + animations
|
||||
// WOM1: static geometry | WOM2: + bones + animations | WOM3: + multi-batch materials
|
||||
struct WoweeModel {
|
||||
struct Vertex {
|
||||
glm::vec3 position;
|
||||
|
|
@ -43,18 +43,31 @@ struct WoweeModel {
|
|||
std::vector<std::vector<AnimKeyframe>> boneKeyframes; // [boneIdx][keyframe]
|
||||
};
|
||||
|
||||
// WOM3: a contiguous slice of indices that draws with one material/texture.
|
||||
// Most M2 models have multiple submeshes (body, hair, eyes, etc.) — each
|
||||
// becomes one Batch in WOM3.
|
||||
struct Batch {
|
||||
uint32_t indexStart = 0; // first index in the global index buffer
|
||||
uint32_t indexCount = 0; // number of indices to draw
|
||||
uint32_t textureIndex = 0; // index into texturePaths
|
||||
uint16_t blendMode = 0; // 0=opaque, 1=alpha-test, 2=alpha, 3=add
|
||||
uint16_t flags = 0; // bit 0 = unlit, bit 1 = two-sided, bit 2 = no z-write
|
||||
};
|
||||
|
||||
std::string name;
|
||||
std::vector<Vertex> vertices;
|
||||
std::vector<uint32_t> indices;
|
||||
std::vector<std::string> texturePaths;
|
||||
std::vector<Bone> bones;
|
||||
std::vector<Animation> animations;
|
||||
std::vector<Batch> batches; // empty in WOM1/WOM2, populated in WOM3
|
||||
float boundRadius = 1.0f;
|
||||
glm::vec3 boundMin{0}, boundMax{0};
|
||||
uint32_t version = 1; // 1=WOM1(static), 2=WOM2(animated)
|
||||
uint32_t version = 1; // 1=WOM1(static), 2=WOM2(animated), 3=WOM3(multi-batch)
|
||||
|
||||
bool isValid() const { return !vertices.empty() && !indices.empty(); }
|
||||
bool hasAnimation() const { return !bones.empty() && !animations.empty(); }
|
||||
bool hasBatches() const { return !batches.empty(); }
|
||||
};
|
||||
|
||||
class WoweeModelLoader {
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ namespace pipeline {
|
|||
|
||||
static constexpr uint32_t WOM_MAGIC = 0x314D4F57; // "WOM1"
|
||||
static constexpr uint32_t WOM2_MAGIC = 0x324D4F57; // "WOM2"
|
||||
static constexpr uint32_t WOM3_MAGIC = 0x334D4F57; // "WOM3"
|
||||
|
||||
bool WoweeModelLoader::exists(const std::string& basePath) {
|
||||
return std::filesystem::exists(basePath + ".wom");
|
||||
|
|
@ -25,9 +26,10 @@ WoweeModel WoweeModelLoader::load(const std::string& basePath) {
|
|||
|
||||
uint32_t magic;
|
||||
f.read(reinterpret_cast<char*>(&magic), 4);
|
||||
bool isV2 = (magic == WOM2_MAGIC);
|
||||
if (magic != WOM_MAGIC && magic != WOM2_MAGIC) return model;
|
||||
model.version = isV2 ? 2 : 1;
|
||||
bool isV2 = (magic == WOM2_MAGIC || magic == WOM3_MAGIC);
|
||||
bool isV3 = (magic == WOM3_MAGIC);
|
||||
if (magic != WOM_MAGIC && magic != WOM2_MAGIC && magic != WOM3_MAGIC) return model;
|
||||
model.version = isV3 ? 3 : (isV2 ? 2 : 1);
|
||||
|
||||
uint32_t vertCount, indexCount, texCount;
|
||||
f.read(reinterpret_cast<char*>(&vertCount), 4);
|
||||
|
|
@ -110,8 +112,25 @@ WoweeModel WoweeModelLoader::load(const std::string& basePath) {
|
|||
}
|
||||
}
|
||||
|
||||
LOG_INFO("WOM", (isV2 ? "2" : "1"), " loaded: ", basePath, " (", vertCount, " verts, ",
|
||||
model.bones.size(), " bones, ", model.animations.size(), " anims)");
|
||||
// WOM3: read batches (multi-material support)
|
||||
if (isV3) {
|
||||
uint32_t batchCount = 0;
|
||||
if (f.read(reinterpret_cast<char*>(&batchCount), 4) && batchCount > 0 && batchCount <= 4096) {
|
||||
model.batches.resize(batchCount);
|
||||
for (uint32_t i = 0; i < batchCount; i++) {
|
||||
auto& b = model.batches[i];
|
||||
f.read(reinterpret_cast<char*>(&b.indexStart), 4);
|
||||
f.read(reinterpret_cast<char*>(&b.indexCount), 4);
|
||||
f.read(reinterpret_cast<char*>(&b.textureIndex), 4);
|
||||
f.read(reinterpret_cast<char*>(&b.blendMode), 2);
|
||||
f.read(reinterpret_cast<char*>(&b.flags), 2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LOG_INFO("WOM", (isV3 ? "3" : (isV2 ? "2" : "1")), " loaded: ", basePath, " (",
|
||||
vertCount, " verts, ", model.bones.size(), " bones, ",
|
||||
model.animations.size(), " anims, ", model.batches.size(), " batches)");
|
||||
return model;
|
||||
}
|
||||
|
||||
|
|
@ -124,7 +143,13 @@ bool WoweeModelLoader::save(const WoweeModel& model, const std::string& basePath
|
|||
if (!f) return false;
|
||||
|
||||
bool hasAnim = model.hasAnimation();
|
||||
uint32_t magic = hasAnim ? WOM2_MAGIC : WOM_MAGIC;
|
||||
bool hasBatches = model.hasBatches();
|
||||
// WOM3 implies WOM2 layout (vertex format with bones), so we only emit
|
||||
// WOM3 if the model also has animation data — pure-batch static meshes
|
||||
// still go to WOM1/WOM2 with batches written via the WOM3 trailing block
|
||||
// when present alongside animation. For static-only with batches, write
|
||||
// as WOM3 anyway (decoder handles missing bones).
|
||||
uint32_t magic = hasBatches ? WOM3_MAGIC : (hasAnim ? WOM2_MAGIC : WOM_MAGIC);
|
||||
f.write(reinterpret_cast<const char*>(&magic), 4);
|
||||
|
||||
uint32_t vertCount = static_cast<uint32_t>(model.vertices.size());
|
||||
|
|
@ -141,8 +166,8 @@ bool WoweeModelLoader::save(const WoweeModel& model, const std::string& basePath
|
|||
f.write(reinterpret_cast<const char*>(&nameLen), 2);
|
||||
f.write(model.name.data(), nameLen);
|
||||
|
||||
// WOM2 writes full vertex with bone data; WOM1 writes 32-byte vertex
|
||||
if (hasAnim) {
|
||||
// WOM2/WOM3 write full vertex with bone data; WOM1 writes 32-byte vertex
|
||||
if (hasAnim || hasBatches) {
|
||||
f.write(reinterpret_cast<const char*>(model.vertices.data()),
|
||||
vertCount * sizeof(WoweeModel::Vertex));
|
||||
} else {
|
||||
|
|
@ -161,8 +186,8 @@ bool WoweeModelLoader::save(const WoweeModel& model, const std::string& basePath
|
|||
f.write(path.data(), pathLen);
|
||||
}
|
||||
|
||||
// WOM2: write bones and animations
|
||||
if (hasAnim) {
|
||||
// WOM2/WOM3: write bones and animations (always, even if empty for WOM3)
|
||||
if (hasAnim || hasBatches) {
|
||||
uint32_t boneCount = static_cast<uint32_t>(model.bones.size());
|
||||
f.write(reinterpret_cast<const char*>(&boneCount), 4);
|
||||
for (const auto& bone : model.bones) {
|
||||
|
|
@ -194,8 +219,22 @@ bool WoweeModelLoader::save(const WoweeModel& model, const std::string& basePath
|
|||
}
|
||||
}
|
||||
|
||||
LOG_INFO("WOM", (hasAnim ? "2" : "1"), " saved: ", womPath, " (", vertCount, " verts, ",
|
||||
model.bones.size(), " bones, ", model.animations.size(), " anims)");
|
||||
// WOM3: write batches
|
||||
if (hasBatches) {
|
||||
uint32_t batchCount = static_cast<uint32_t>(model.batches.size());
|
||||
f.write(reinterpret_cast<const char*>(&batchCount), 4);
|
||||
for (const auto& b : model.batches) {
|
||||
f.write(reinterpret_cast<const char*>(&b.indexStart), 4);
|
||||
f.write(reinterpret_cast<const char*>(&b.indexCount), 4);
|
||||
f.write(reinterpret_cast<const char*>(&b.textureIndex), 4);
|
||||
f.write(reinterpret_cast<const char*>(&b.blendMode), 2);
|
||||
f.write(reinterpret_cast<const char*>(&b.flags), 2);
|
||||
}
|
||||
}
|
||||
|
||||
LOG_INFO("WOM", (hasBatches ? "3" : (hasAnim ? "2" : "1")), " saved: ", womPath,
|
||||
" (", vertCount, " verts, ", model.bones.size(), " bones, ",
|
||||
model.animations.size(), " anims, ", model.batches.size(), " batches)");
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -260,6 +299,25 @@ WoweeModel WoweeModelLoader::fromM2(const std::string& m2Path, AssetManager* am)
|
|||
model.bones.push_back(wb);
|
||||
}
|
||||
|
||||
// Convert batches with material/blend mode info (WOM3 feature).
|
||||
// Each M2 batch maps to a WOM batch — preserves multi-submesh material structure.
|
||||
for (const auto& mb : m2.batches) {
|
||||
WoweeModel::Batch wb;
|
||||
wb.indexStart = mb.indexStart;
|
||||
wb.indexCount = mb.indexCount;
|
||||
// Resolve textureLookup -> texture index
|
||||
uint32_t lookupIdx = mb.textureIndex;
|
||||
wb.textureIndex = (lookupIdx < m2.textureLookup.size())
|
||||
? static_cast<uint32_t>(std::max<int16_t>(0, m2.textureLookup[lookupIdx]))
|
||||
: 0;
|
||||
if (mb.materialIndex < m2.materials.size()) {
|
||||
const auto& mat = m2.materials[mb.materialIndex];
|
||||
wb.blendMode = mat.blendMode;
|
||||
wb.flags = mat.flags;
|
||||
}
|
||||
model.batches.push_back(wb);
|
||||
}
|
||||
|
||||
// Convert animations (first keyframe per bone per sequence)
|
||||
for (const auto& seq : m2.sequences) {
|
||||
WoweeModel::Animation anim;
|
||||
|
|
@ -353,18 +411,38 @@ M2Model WoweeModelLoader::toM2(const WoweeModel& wom) {
|
|||
tex.filename = tp;
|
||||
m.textures.push_back(tex);
|
||||
}
|
||||
m.textureLookup = {0};
|
||||
m.textureLookup.clear();
|
||||
for (uint32_t i = 0; i < wom.texturePaths.size(); i++)
|
||||
m.textureLookup.push_back(static_cast<int16_t>(i));
|
||||
if (m.textureLookup.empty()) m.textureLookup.push_back(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);
|
||||
if (wom.hasBatches()) {
|
||||
for (const auto& wb : wom.batches) {
|
||||
M2Batch batch{};
|
||||
batch.indexStart = wb.indexStart;
|
||||
batch.indexCount = wb.indexCount;
|
||||
batch.vertexCount = static_cast<uint32_t>(m.vertices.size());
|
||||
batch.textureCount = 1;
|
||||
batch.textureIndex = static_cast<uint16_t>(
|
||||
std::min<uint32_t>(wb.textureIndex, m.textureLookup.size() - 1));
|
||||
batch.materialIndex = static_cast<uint16_t>(m.materials.size());
|
||||
m.batches.push_back(batch);
|
||||
M2Material mat;
|
||||
mat.flags = wb.flags;
|
||||
mat.blendMode = wb.blendMode;
|
||||
m.materials.push_back(mat);
|
||||
}
|
||||
} else {
|
||||
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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,14 +20,17 @@ Novel file formats for custom WoW zone content. No Blizzard IP.
|
|||
|
||||
## WOM — Wowee Open Model (binary)
|
||||
- Extension: `.wom`
|
||||
- Magic: `WOM1` (0x314D4F57) for static, `WOM2` (0x324D4F57) for animated
|
||||
- Magic: `WOM1` (0x314D4F57) static, `WOM2` (0x324D4F57) animated, `WOM3` (0x334D4F57) multi-batch
|
||||
- Layout: magic(4) + vertCount(4) + indexCount(4) + texCount(4) + bounds(28) + name + vertices + indices + texPaths
|
||||
- WOM1 Vertex: position(vec3) + normal(vec3) + texCoord(vec2) = 32 bytes
|
||||
- WOM2 Vertex: + boneWeights(4) + boneIndices(4) = 40 bytes
|
||||
- WOM2 Bones: boneCount(4) + [keyBoneId(4) + parentBone(2) + pivot(12) + flags(4)] × N
|
||||
- WOM2 Animations: animCount(4) + [id(4) + duration(4) + speed(4) + per-bone keyframes] × N
|
||||
- WOM2/WOM3 Vertex: + boneWeights(4) + boneIndices(4) = 40 bytes
|
||||
- WOM2/WOM3 Bones: boneCount(4) + [keyBoneId(4) + parentBone(2) + pivot(12) + flags(4)] × N
|
||||
- WOM2/WOM3 Animations: animCount(4) + [id(4) + duration(4) + speed(4) + per-bone keyframes] × N
|
||||
- Keyframe: timeMs(4) + translation(12) + rotation(16) + scale(12) = 44 bytes
|
||||
- Backward compatible: WOM1 files load without bone/animation data
|
||||
- WOM3 Batches: batchCount(4) + [indexStart(4) + indexCount(4) + textureIndex(4) + blendMode(2) + flags(2)] × N
|
||||
- WOM3 blendMode: 0=opaque, 1=alpha-test, 2=alpha, 3=add, 4=mod, 5=mod2x, 6=blendAdd, 7=screen
|
||||
- WOM3 flags: bit 0 = unlit, bit 1 = two-sided, bit 2 = no z-write
|
||||
- Backward compatible: WOM1 files load without bone/animation data; WOM3 falls back to single-batch when batches block missing
|
||||
|
||||
## WOB — Wowee Open Building (binary)
|
||||
- Extension: `.wob`
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue