feat: WOM2 animated model format with bones and keyframe animation

Upgrades WOM from geometry-only (WOM1) to fully animated (WOM2):

- WOM2 magic (0x324D4F57) for animated models, WOM1 for static
- Vertex extended: +boneWeights[4] +boneIndices[4] (40 bytes vs 32)
- Bone data: keyBoneId, parentBone, pivot, flags per bone
- Animation data: per-sequence per-bone keyframes with translation,
  rotation (quaternion), scale at millisecond timestamps
- fromM2() now preserves all skeletal data: bone hierarchy, weights,
  and per-sequence keyframes from M2 animation tracks
- Backward compatible: WOM1 files load without bone data (32-byte
  vertices read and padded with default bone weights)
- FORMAT_SPEC.md updated with WOM2 binary layout
This commit is contained in:
Kelsi 2026-05-05 16:16:07 -07:00
parent 109b288573
commit f6dfc295ab
3 changed files with 225 additions and 27 deletions

View file

@ -1,6 +1,7 @@
#pragma once
#include <glm/glm.hpp>
#include <glm/gtc/quaternion.hpp>
#include <string>
#include <vector>
#include <cstdint>
@ -9,22 +10,49 @@ namespace wowee {
namespace pipeline {
// Wowee Open Model format (.wom) — novel format, no Blizzard IP
// Designed for static doodads, props, and simple animated objects
// WOM1: static geometry | WOM2: + bones + animations
struct WoweeModel {
struct Vertex {
glm::vec3 position;
glm::vec3 normal;
glm::vec2 texCoord;
uint8_t boneWeights[4] = {255, 0, 0, 0};
uint8_t boneIndices[4] = {0, 0, 0, 0};
};
struct Bone {
int32_t keyBoneId = -1;
int16_t parentBone = -1;
glm::vec3 pivot{0};
uint32_t flags = 0;
};
struct AnimKeyframe {
uint32_t timeMs;
glm::vec3 translation;
glm::quat rotation;
glm::vec3 scale;
};
struct Animation {
uint32_t id = 0;
uint32_t durationMs = 0;
float movingSpeed = 0;
std::vector<std::vector<AnimKeyframe>> boneKeyframes; // [boneIdx][keyframe]
};
std::string name;
std::vector<Vertex> vertices;
std::vector<uint32_t> indices;
std::vector<std::string> texturePaths; // PNG paths
std::vector<std::string> texturePaths;
std::vector<Bone> bones;
std::vector<Animation> animations;
float boundRadius = 1.0f;
glm::vec3 boundMin{0}, boundMax{0};
uint32_t version = 1; // 1=WOM1(static), 2=WOM2(animated)
bool isValid() const { return !vertices.empty() && !indices.empty(); }
bool hasAnimation() const { return !bones.empty() && !animations.empty(); }
};
class WoweeModelLoader {