diff --git a/include/rendering/m2_renderer.hpp b/include/rendering/m2_renderer.hpp index a4cdf104..73ae3ecd 100644 --- a/include/rendering/m2_renderer.hpp +++ b/include/rendering/m2_renderer.hpp @@ -52,6 +52,11 @@ struct M2ModelGPU { std::string name; + // Skeletal animation data (kept from M2Model for bone computation) + std::vector bones; + std::vector sequences; + bool hasAnimation = false; // True if any bone has keyframes + bool isValid() const { return vao != 0 && indexCount > 0; } }; @@ -70,9 +75,12 @@ struct M2Instance { glm::vec3 worldBoundsMax; // Animation state - float animTime = 0.0f; // Current animation time + float animTime = 0.0f; // Current animation time (ms) float animSpeed = 1.0f; // Animation playback speed uint32_t animId = 0; // Current animation sequence + int currentSequenceIndex = 0;// Index into sequences array + float animDuration = 0.0f; // Duration of current animation (ms) + std::vector boneMatrices; void updateModelMatrix(); }; diff --git a/src/rendering/m2_renderer.cpp b/src/rendering/m2_renderer.cpp index 02445da5..cd09c507 100644 --- a/src/rendering/m2_renderer.cpp +++ b/src/rendering/m2_renderer.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include #include #include @@ -192,18 +193,20 @@ bool M2Renderer::initialize(pipeline::AssetManager* assets) { LOG_INFO("Initializing M2 renderer..."); - // Create M2 shader with simple animation support + // Create M2 shader with skeletal animation support const char* vertexSrc = R"( #version 330 core layout (location = 0) in vec3 aPos; layout (location = 1) in vec3 aNormal; layout (location = 2) in vec2 aTexCoord; + layout (location = 3) in vec4 aBoneWeights; + layout (location = 4) in vec4 aBoneIndicesF; uniform mat4 uModel; uniform mat4 uView; uniform mat4 uProjection; - uniform float uTime; - uniform float uAnimScale; // 0 = no animation, 1 = full animation + uniform bool uUseBones; + uniform mat4 uBones[128]; out vec3 FragPos; out vec3 Normal; @@ -211,19 +214,21 @@ bool M2Renderer::initialize(pipeline::AssetManager* assets) { void main() { vec3 pos = aPos; + vec3 norm = aNormal; - // Simple swaying animation for vegetation/doodads - // Only animate vertices above ground level (positive Y in model space) - if (uAnimScale > 0.0 && pos.z > 0.5) { - float sway = sin(uTime * 2.0 + pos.x * 0.5 + pos.y * 0.3) * 0.1; - float heightFactor = clamp((pos.z - 0.5) / 3.0, 0.0, 1.0); - pos.x += sway * heightFactor * uAnimScale; - pos.y += sway * 0.5 * heightFactor * uAnimScale; + if (uUseBones) { + ivec4 bi = ivec4(aBoneIndicesF); + mat4 boneTransform = uBones[bi.x] * aBoneWeights.x + + uBones[bi.y] * aBoneWeights.y + + uBones[bi.z] * aBoneWeights.z + + uBones[bi.w] * aBoneWeights.w; + pos = vec3(boneTransform * vec4(aPos, 1.0)); + norm = mat3(boneTransform) * aNormal; } vec4 worldPos = uModel * vec4(pos, 1.0); FragPos = worldPos.xyz; - Normal = mat3(uModel) * aNormal; + Normal = mat3(uModel) * norm; TexCoord = aTexCoord; gl_Position = uProjection * uView * worldPos; @@ -446,10 +451,22 @@ bool M2Renderer::loadModel(const pipeline::M2Model& model, uint32_t modelId) { glGenVertexArrays(1, &gpuModel.vao); glBindVertexArray(gpuModel.vao); + // Store bone/sequence data for animation + gpuModel.bones = model.bones; + gpuModel.sequences = model.sequences; + gpuModel.hasAnimation = false; + for (const auto& bone : model.bones) { + if (bone.translation.hasData() || bone.rotation.hasData() || bone.scale.hasData()) { + gpuModel.hasAnimation = true; + break; + } + } + // Create VBO with interleaved vertex data - // Format: position (3), normal (3), texcoord (2) + // Format: position (3), normal (3), texcoord (2), boneWeights (4), boneIndices (4 as float) + const size_t floatsPerVertex = 16; std::vector vertexData; - vertexData.reserve(model.vertices.size() * 8); + vertexData.reserve(model.vertices.size() * floatsPerVertex); for (const auto& v : model.vertices) { vertexData.push_back(v.position.x); @@ -460,6 +477,20 @@ bool M2Renderer::loadModel(const pipeline::M2Model& model, uint32_t modelId) { vertexData.push_back(v.normal.z); vertexData.push_back(v.texCoords[0].x); vertexData.push_back(v.texCoords[0].y); + // Bone weights (normalized 0-1) + float w0 = v.boneWeights[0] / 255.0f; + float w1 = v.boneWeights[1] / 255.0f; + float w2 = v.boneWeights[2] / 255.0f; + float w3 = v.boneWeights[3] / 255.0f; + vertexData.push_back(w0); + vertexData.push_back(w1); + vertexData.push_back(w2); + vertexData.push_back(w3); + // Bone indices (clamped to max 127 for uniform array) + vertexData.push_back(static_cast(std::min(v.boneIndices[0], uint8_t(127)))); + vertexData.push_back(static_cast(std::min(v.boneIndices[1], uint8_t(127)))); + vertexData.push_back(static_cast(std::min(v.boneIndices[2], uint8_t(127)))); + vertexData.push_back(static_cast(std::min(v.boneIndices[3], uint8_t(127)))); } glGenBuffers(1, &gpuModel.vbo); @@ -474,7 +505,7 @@ bool M2Renderer::loadModel(const pipeline::M2Model& model, uint32_t modelId) { model.indices.data(), GL_STATIC_DRAW); // Set up vertex attributes - const size_t stride = 8 * sizeof(float); + const size_t stride = floatsPerVertex * sizeof(float); // Position glEnableVertexAttribArray(0); @@ -488,6 +519,14 @@ bool M2Renderer::loadModel(const pipeline::M2Model& model, uint32_t modelId) { glEnableVertexAttribArray(2); glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, stride, (void*)(6 * sizeof(float))); + // Bone Weights + glEnableVertexAttribArray(3); + glVertexAttribPointer(3, 4, GL_FLOAT, GL_FALSE, stride, (void*)(8 * sizeof(float))); + + // Bone Indices (as integer attribute) + glEnableVertexAttribArray(4); + glVertexAttribPointer(4, 4, GL_FLOAT, GL_FALSE, stride, (void*)(12 * sizeof(float))); + glBindVertexArray(0); // Load ALL textures from the model into a local vector @@ -559,6 +598,14 @@ uint32_t M2Renderer::createInstance(uint32_t modelId, const glm::vec3& position, getTightCollisionBounds(models[modelId], localMin, localMax); transformAABB(instance.modelMatrix, localMin, localMax, instance.worldBoundsMin, instance.worldBoundsMax); + // Initialize animation: play first sequence (usually Stand/Idle) + const auto& mdl = models[modelId]; + if (mdl.hasAnimation && !mdl.sequences.empty()) { + instance.currentSequenceIndex = 0; + instance.animDuration = static_cast(mdl.sequences[0].duration); + instance.animTime = static_cast(rand() % std::max(1u, mdl.sequences[0].duration)); + } + instances.push_back(instance); size_t idx = instances.size() - 1; instanceIndexById[instance.id] = idx; @@ -593,7 +640,15 @@ uint32_t M2Renderer::createInstanceWithMatrix(uint32_t modelId, const glm::mat4& glm::vec3 localMin, localMax; getTightCollisionBounds(models[modelId], localMin, localMax); transformAABB(instance.modelMatrix, localMin, localMax, instance.worldBoundsMin, instance.worldBoundsMax); - instance.animTime = static_cast(rand()) / RAND_MAX * 10.0f; // Random start time + // Initialize animation: play first sequence (usually Stand/Idle) + const auto& mdl2 = models[modelId]; + if (mdl2.hasAnimation && !mdl2.sequences.empty()) { + instance.currentSequenceIndex = 0; + instance.animDuration = static_cast(mdl2.sequences[0].duration); + instance.animTime = static_cast(rand() % std::max(1u, mdl2.sequences[0].duration)); + } else { + instance.animTime = static_cast(rand()) / RAND_MAX * 10.0f; + } instances.push_back(instance); size_t idx = instances.size() - 1; @@ -611,10 +666,109 @@ uint32_t M2Renderer::createInstanceWithMatrix(uint32_t modelId, const glm::mat4& return instance.id; } +// --- Bone animation helpers (same logic as CharacterRenderer) --- + +static int findKeyframeIndex(const std::vector& timestamps, float time) { + if (timestamps.empty()) return -1; + if (timestamps.size() == 1) return 0; + for (size_t i = 0; i < timestamps.size() - 1; i++) { + if (time < static_cast(timestamps[i + 1])) { + return static_cast(i); + } + } + return static_cast(timestamps.size() - 2); +} + +static glm::vec3 interpVec3(const pipeline::M2AnimationTrack& track, + int seqIdx, float time, const glm::vec3& def) { + if (!track.hasData()) return def; + if (seqIdx < 0 || seqIdx >= static_cast(track.sequences.size())) return def; + const auto& keys = track.sequences[seqIdx]; + if (keys.timestamps.empty() || keys.vec3Values.empty()) return def; + auto safe = [&](const glm::vec3& v) -> glm::vec3 { + if (std::isnan(v.x) || std::isnan(v.y) || std::isnan(v.z)) return def; + return v; + }; + if (keys.vec3Values.size() == 1) return safe(keys.vec3Values[0]); + int idx = findKeyframeIndex(keys.timestamps, time); + if (idx < 0) return def; + size_t i0 = static_cast(idx); + size_t i1 = std::min(i0 + 1, keys.vec3Values.size() - 1); + if (i0 == i1) return safe(keys.vec3Values[i0]); + float t0 = static_cast(keys.timestamps[i0]); + float t1 = static_cast(keys.timestamps[i1]); + float dur = t1 - t0; + float t = (dur > 0.0f) ? glm::clamp((time - t0) / dur, 0.0f, 1.0f) : 0.0f; + return safe(glm::mix(keys.vec3Values[i0], keys.vec3Values[i1], t)); +} + +static glm::quat interpQuat(const pipeline::M2AnimationTrack& track, + int seqIdx, float time) { + glm::quat identity(1.0f, 0.0f, 0.0f, 0.0f); + if (!track.hasData()) return identity; + if (seqIdx < 0 || seqIdx >= static_cast(track.sequences.size())) return identity; + const auto& keys = track.sequences[seqIdx]; + if (keys.timestamps.empty() || keys.quatValues.empty()) return identity; + auto safe = [&](const glm::quat& q) -> glm::quat { + float len = glm::length(q); + if (len < 0.001f || std::isnan(len)) return identity; + return q; + }; + if (keys.quatValues.size() == 1) return safe(keys.quatValues[0]); + int idx = findKeyframeIndex(keys.timestamps, time); + if (idx < 0) return identity; + size_t i0 = static_cast(idx); + size_t i1 = std::min(i0 + 1, keys.quatValues.size() - 1); + if (i0 == i1) return safe(keys.quatValues[i0]); + float t0 = static_cast(keys.timestamps[i0]); + float t1 = static_cast(keys.timestamps[i1]); + float dur = t1 - t0; + float t = (dur > 0.0f) ? glm::clamp((time - t0) / dur, 0.0f, 1.0f) : 0.0f; + return glm::slerp(safe(keys.quatValues[i0]), safe(keys.quatValues[i1]), t); +} + +static void computeBoneMatrices(const M2ModelGPU& model, M2Instance& instance) { + size_t numBones = model.bones.size(); + if (numBones == 0) return; + instance.boneMatrices.resize(numBones); + + for (size_t i = 0; i < numBones; i++) { + const auto& bone = model.bones[i]; + glm::vec3 trans = interpVec3(bone.translation, instance.currentSequenceIndex, instance.animTime, glm::vec3(0.0f)); + glm::quat rot = interpQuat(bone.rotation, instance.currentSequenceIndex, instance.animTime); + glm::vec3 scl = interpVec3(bone.scale, instance.currentSequenceIndex, instance.animTime, glm::vec3(1.0f)); + + glm::mat4 local = glm::translate(glm::mat4(1.0f), bone.pivot); + local = glm::translate(local, trans); + local *= glm::toMat4(rot); + local = glm::scale(local, scl); + local = glm::translate(local, -bone.pivot); + + if (bone.parentBone >= 0 && static_cast(bone.parentBone) < numBones) { + instance.boneMatrices[i] = instance.boneMatrices[bone.parentBone] * local; + } else { + instance.boneMatrices[i] = local; + } + } +} + void M2Renderer::update(float deltaTime) { - // Advance animation time for all instances + float dtMs = deltaTime * 1000.0f; // Convert to milliseconds for keyframe lookup for (auto& instance : instances) { - instance.animTime += deltaTime * instance.animSpeed; + instance.animTime += dtMs * instance.animSpeed; + + auto it = models.find(instance.modelId); + if (it == models.end()) continue; + const M2ModelGPU& model = it->second; + + if (!model.hasAnimation) continue; + + // Loop animation + if (instance.animDuration > 0.0f && instance.animTime >= instance.animDuration) { + instance.animTime = std::fmod(instance.animTime, instance.animDuration); + } + + computeBoneMatrices(model, instance); } } @@ -695,10 +849,16 @@ void M2Renderer::render(const Camera& camera, const glm::mat4& view, const glm:: } shader->setUniform("uModel", instance.modelMatrix); - shader->setUniform("uTime", instance.animTime); - shader->setUniform("uAnimScale", 0.0f); // Disabled - proper M2 animation needs bone/particle systems shader->setUniform("uFadeAlpha", fadeAlpha); + // Upload bone matrices if model has skeletal animation + bool useBones = model.hasAnimation && !instance.boneMatrices.empty(); + shader->setUniform("uUseBones", useBones); + if (useBones) { + int numBones = std::min(static_cast(instance.boneMatrices.size()), 128); + shader->setUniformMatrixArray("uBones[0]", instance.boneMatrices.data(), numBones); + } + // Disable depth writes for fading objects to avoid z-fighting if (fadeAlpha < 1.0f) { glDepthMask(GL_FALSE); diff --git a/src/rendering/terrain_renderer.cpp b/src/rendering/terrain_renderer.cpp index be7ffa2c..c6e656f6 100644 --- a/src/rendering/terrain_renderer.cpp +++ b/src/rendering/terrain_renderer.cpp @@ -301,6 +301,15 @@ void TerrainRenderer::render(const Camera& camera) { // Use shader shader->use(); + // Bind sampler uniforms to texture units (constant, only needs to be set once per use) + shader->setUniform("uBaseTexture", 0); + shader->setUniform("uLayer1Texture", 1); + shader->setUniform("uLayer2Texture", 2); + shader->setUniform("uLayer3Texture", 3); + shader->setUniform("uLayer1Alpha", 4); + shader->setUniform("uLayer2Alpha", 5); + shader->setUniform("uLayer3Alpha", 6); + // Set view/projection matrices glm::mat4 view = camera.getViewMatrix(); glm::mat4 projection = camera.getProjectionMatrix();