diff --git a/include/pipeline/m2_loader.hpp b/include/pipeline/m2_loader.hpp index f13db470..e7f7dfe9 100644 --- a/include/pipeline/m2_loader.hpp +++ b/include/pipeline/m2_loader.hpp @@ -109,6 +109,13 @@ struct M2Batch { uint16_t submeshLevel = 0; // Submesh level (0=base, 1+=LOD/alternate mesh) }; +// Texture transform (UV animation) data +struct M2TextureTransform { + M2AnimationTrack translation; // UV translation keyframes + M2AnimationTrack rotation; // UV rotation keyframes (quat) + M2AnimationTrack scale; // UV scale keyframes +}; + // Attachment point (bone-anchored position for weapons, effects, etc.) struct M2Attachment { uint32_t id; // 0=Head, 1=RightHand, 2=LeftHand, etc. @@ -139,6 +146,10 @@ struct M2Model { std::vector textures; std::vector textureLookup; // Batch texture index lookup + // Texture transforms (UV animation) + std::vector textureTransforms; + std::vector textureTransformLookup; + // Attachment points (for weapon/effect anchoring) std::vector attachments; std::vector attachmentLookup; // attachment ID → index diff --git a/include/rendering/m2_renderer.hpp b/include/rendering/m2_renderer.hpp index d93b2624..bf48e3c5 100644 --- a/include/rendering/m2_renderer.hpp +++ b/include/rendering/m2_renderer.hpp @@ -31,6 +31,7 @@ struct M2ModelGPU { uint32_t indexStart = 0; // offset in indices (not bytes) uint32_t indexCount = 0; bool hasAlpha = false; + uint16_t textureAnimIndex = 0xFFFF; // 0xFFFF = no texture animation }; GLuint vao = 0; @@ -61,6 +62,11 @@ struct M2ModelGPU { bool hasAnimation = false; // True if any bone has keyframes bool isSmoke = false; // True for smoke models (UV scroll animation) bool disableAnimation = false; // Keep foliage/tree doodads visually stable + bool hasTextureAnimation = false; // True if any batch has UV animation + + // Texture transform data for UV animation + std::vector textureTransforms; + std::vector textureTransformLookup; std::vector idleVariationIndices; // Sequence indices for idle variations (animId 0) bool isValid() const { return vao != 0 && indexCount > 0; } diff --git a/src/pipeline/m2_loader.cpp b/src/pipeline/m2_loader.cpp index 52b77c83..d33afb1c 100644 --- a/src/pipeline/m2_loader.cpp +++ b/src/pipeline/m2_loader.cpp @@ -210,6 +210,13 @@ struct CompressedQuat { int16_t x, y, z, w; }; +// M2 texture transform (on-disk, 3 × M2TrackDisk = 60 bytes) +struct M2TextureTransformDisk { + M2TrackDisk translation; // 20 + M2TrackDisk rotation; // 20 + M2TrackDisk scaling; // 20 +}; + // M2 attachment point (on-disk) struct M2AttachmentDisk { uint32_t id; @@ -511,6 +518,35 @@ M2Model M2Loader::load(const std::vector& m2Data) { model.textureLookup = readArray(m2Data, header.ofsTexLookup, header.nTexLookup); } + // Read texture transforms (UV animation data) + if (header.nUVAnimation > 0 && header.ofsUVAnimation > 0) { + // Build per-sequence flags for skipping external .anim data + std::vector seqFlags; + seqFlags.reserve(model.sequences.size()); + for (const auto& seq : model.sequences) { + seqFlags.push_back(seq.flags); + } + + model.textureTransforms.reserve(header.nUVAnimation); + for (uint32_t i = 0; i < header.nUVAnimation; i++) { + uint32_t ofs = header.ofsUVAnimation + i * sizeof(M2TextureTransformDisk); + if (ofs + sizeof(M2TextureTransformDisk) > m2Data.size()) break; + + M2TextureTransformDisk dt = readValue(m2Data, ofs); + M2TextureTransform tt; + parseAnimTrack(m2Data, dt.translation, tt.translation, TrackType::VEC3, seqFlags); + parseAnimTrack(m2Data, dt.rotation, tt.rotation, TrackType::QUAT_COMPRESSED, seqFlags); + parseAnimTrack(m2Data, dt.scaling, tt.scale, TrackType::VEC3, seqFlags); + model.textureTransforms.push_back(std::move(tt)); + } + core::Logger::getInstance().debug(" Texture transforms: ", model.textureTransforms.size()); + } + + // Read texture transform lookup (nTransLookup) + if (header.nTransLookup > 0 && header.ofsTransLookup > 0) { + model.textureTransformLookup = readArray(m2Data, header.ofsTransLookup, header.nTransLookup); + } + // Read attachment points if (header.nAttachments > 0 && header.ofsAttachments > 0) { auto diskAttachments = readArray(m2Data, header.ofsAttachments, header.nAttachments); diff --git a/src/rendering/m2_renderer.cpp b/src/rendering/m2_renderer.cpp index 0fd4d30d..f711ea6b 100644 --- a/src/rendering/m2_renderer.cpp +++ b/src/rendering/m2_renderer.cpp @@ -219,6 +219,7 @@ bool M2Renderer::initialize(pipeline::AssetManager* assets) { uniform mat4 uProjection; uniform bool uUseBones; uniform mat4 uBones[128]; + uniform vec2 uUVOffset; out vec3 FragPos; out vec3 Normal; out vec2 TexCoord; @@ -240,7 +241,7 @@ bool M2Renderer::initialize(pipeline::AssetManager* assets) { vec4 worldPos = uModel * vec4(pos, 1.0); FragPos = worldPos.xyz; Normal = mat3(uModel) * norm; - TexCoord = aTexCoord; + TexCoord = aTexCoord + uUVOffset; gl_Position = uProjection * uView * worldPos; } @@ -710,6 +711,11 @@ bool M2Renderer::loadModel(const pipeline::M2Model& model, uint32_t modelId) { } } + // Copy texture transform data for UV animation + gpuModel.textureTransforms = model.textureTransforms; + gpuModel.textureTransformLookup = model.textureTransformLookup; + gpuModel.hasTextureAnimation = false; + // Build per-batch GPU entries if (!model.batches.empty()) { for (const auto& batch : model.batches) { @@ -717,6 +723,12 @@ bool M2Renderer::loadModel(const pipeline::M2Model& model, uint32_t modelId) { bgpu.indexStart = batch.indexStart; bgpu.indexCount = batch.indexCount; + // Store texture animation index from batch + bgpu.textureAnimIndex = batch.textureAnimIndex; + if (bgpu.textureAnimIndex != 0xFFFF) { + gpuModel.hasTextureAnimation = true; + } + // Resolve texture: batch.textureIndex → textureLookup → allTextures GLuint tex = whiteTexture; if (batch.textureIndex < model.textureLookup.size()) { @@ -1224,6 +1236,23 @@ void M2Renderer::render(const Camera& camera, const glm::mat4& view, const glm:: for (const auto& batch : model.batches) { if (batch.indexCount == 0) continue; + // Compute UV offset for texture animation + glm::vec2 uvOffset(0.0f, 0.0f); + if (batch.textureAnimIndex != 0xFFFF && model.hasTextureAnimation) { + uint16_t lookupIdx = batch.textureAnimIndex; + if (lookupIdx < model.textureTransformLookup.size()) { + uint16_t transformIdx = model.textureTransformLookup[lookupIdx]; + if (transformIdx < model.textureTransforms.size()) { + const auto& tt = model.textureTransforms[transformIdx]; + glm::vec3 trans = interpVec3(tt.translation, + instance.currentSequenceIndex, instance.animTime, + glm::vec3(0.0f), model.globalSequenceDurations); + uvOffset = glm::vec2(trans.x, trans.y); + } + } + } + shader->setUniform("uUVOffset", uvOffset); + bool hasTexture = (batch.texture != 0); shader->setUniform("uHasTexture", hasTexture); shader->setUniform("uAlphaTest", batch.hasAlpha);