diff --git a/CMakeLists.txt b/CMakeLists.txt index e2eb88c6..54228b37 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -143,6 +143,7 @@ set(WOWEE_SOURCES src/rendering/minimap.cpp src/rendering/world_map.cpp src/rendering/swim_effects.cpp + src/rendering/mount_dust.cpp src/rendering/loading_screen.cpp src/rendering/video_player.cpp diff --git a/include/rendering/mount_dust.hpp b/include/rendering/mount_dust.hpp new file mode 100644 index 00000000..fa729fa9 --- /dev/null +++ b/include/rendering/mount_dust.hpp @@ -0,0 +1,50 @@ +#pragma once + +#include +#include +#include +#include + +namespace wowee { +namespace rendering { + +class Camera; +class Shader; + +class MountDust { +public: + MountDust(); + ~MountDust(); + + bool initialize(); + void shutdown(); + + // Spawn dust particles at mount feet when moving on ground + void spawnDust(const glm::vec3& position, const glm::vec3& velocity, bool isMoving); + + void update(float deltaTime); + void render(const Camera& camera); + +private: + struct Particle { + glm::vec3 position; + glm::vec3 velocity; + float lifetime; + float maxLifetime; + float size; + float alpha; + }; + + static constexpr int MAX_DUST_PARTICLES = 300; + std::vector particles; + + GLuint vao = 0; + GLuint vbo = 0; + std::unique_ptr shader; + std::vector vertexData; + + float spawnAccum = 0.0f; +}; + +} // namespace rendering +} // namespace wowee diff --git a/include/rendering/renderer.hpp b/include/rendering/renderer.hpp index a4222822..d4145ce3 100644 --- a/include/rendering/renderer.hpp +++ b/include/rendering/renderer.hpp @@ -27,6 +27,7 @@ class Clouds; class LensFlare; class Weather; class SwimEffects; +class MountDust; class CharacterRenderer; class WMORenderer; class M2Renderer; @@ -165,6 +166,7 @@ private: std::unique_ptr lensFlare; std::unique_ptr weather; std::unique_ptr swimEffects; + std::unique_ptr mountDust; std::unique_ptr characterRenderer; std::unique_ptr wmoRenderer; std::unique_ptr m2Renderer; diff --git a/src/rendering/mount_dust.cpp b/src/rendering/mount_dust.cpp new file mode 100644 index 00000000..08a4ba6f --- /dev/null +++ b/src/rendering/mount_dust.cpp @@ -0,0 +1,210 @@ +#include "rendering/mount_dust.hpp" +#include "rendering/camera.hpp" +#include "rendering/shader.hpp" +#include "core/logger.hpp" +#include +#include +#include + +namespace wowee { +namespace rendering { + +static std::mt19937& rng() { + static std::random_device rd; + static std::mt19937 gen(rd()); + return gen; +} + +static float randFloat(float lo, float hi) { + std::uniform_real_distribution dist(lo, hi); + return dist(rng()); +} + +MountDust::MountDust() = default; +MountDust::~MountDust() { shutdown(); } + +bool MountDust::initialize() { + LOG_INFO("Initializing mount dust effects"); + + // Dust particle shader (brownish/tan dust clouds) + shader = std::make_unique(); + + const char* dustVS = R"( + #version 330 core + layout (location = 0) in vec3 aPos; + layout (location = 1) in float aSize; + layout (location = 2) in float aAlpha; + + uniform mat4 uView; + uniform mat4 uProjection; + + out float vAlpha; + + void main() { + gl_Position = uProjection * uView * vec4(aPos, 1.0); + gl_PointSize = aSize; + vAlpha = aAlpha; + } + )"; + + const char* dustFS = R"( + #version 330 core + in float vAlpha; + out vec4 FragColor; + + void main() { + vec2 coord = gl_PointCoord - vec2(0.5); + float dist = length(coord); + if (dist > 0.5) discard; + // Soft dust cloud with brownish/tan color + float alpha = smoothstep(0.5, 0.0, dist) * vAlpha; + vec3 dustColor = vec3(0.7, 0.65, 0.55); // Tan/brown dust + FragColor = vec4(dustColor, alpha * 0.4); // Semi-transparent + } + )"; + + if (!shader->loadFromSource(dustVS, dustFS)) { + LOG_ERROR("Failed to create mount dust shader"); + return false; + } + + // Create VAO/VBO + glGenVertexArrays(1, &vao); + glGenBuffers(1, &vbo); + + glBindVertexArray(vao); + glBindBuffer(GL_ARRAY_BUFFER, vbo); + + // Position (vec3) + Size (float) + Alpha (float) = 5 floats per vertex + glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)0); + glEnableVertexAttribArray(0); + + glVertexAttribPointer(1, 1, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)(3 * sizeof(float))); + glEnableVertexAttribArray(1); + + glVertexAttribPointer(2, 1, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)(4 * sizeof(float))); + glEnableVertexAttribArray(2); + + glBindVertexArray(0); + + particles.reserve(MAX_DUST_PARTICLES); + vertexData.reserve(MAX_DUST_PARTICLES * 5); + + LOG_INFO("Mount dust effects initialized"); + return true; +} + +void MountDust::shutdown() { + if (vao) glDeleteVertexArrays(1, &vao); + if (vbo) glDeleteBuffers(1, &vbo); + vao = 0; + vbo = 0; + particles.clear(); + shader.reset(); +} + +void MountDust::spawnDust(const glm::vec3& position, const glm::vec3& velocity, bool isMoving) { + if (!isMoving) { + spawnAccum = 0.0f; + return; + } + + // Spawn rate based on speed + float speed = glm::length(velocity); + if (speed < 0.1f) return; + + // Spawn dust particles at a rate proportional to speed + float spawnRate = speed * 8.0f; // More dust at higher speeds + spawnAccum += spawnRate * 0.016f; // Assume ~60 FPS + + while (spawnAccum >= 1.0f && particles.size() < MAX_DUST_PARTICLES) { + spawnAccum -= 1.0f; + + Particle p; + // Spawn slightly behind and to the sides of the mount + p.position = position + glm::vec3( + randFloat(-0.3f, 0.3f), + randFloat(-0.3f, 0.3f), + randFloat(-0.1f, 0.1f) + ); + + // Dust rises up and drifts backward slightly + p.velocity = glm::vec3( + randFloat(-0.2f, 0.2f), + randFloat(-0.2f, 0.2f), + randFloat(0.5f, 1.2f) // Rise upward + ) - velocity * 0.2f; // Drift backward relative to movement + + p.lifetime = 0.0f; + p.maxLifetime = randFloat(0.4f, 0.8f); + p.size = randFloat(8.0f, 16.0f); + p.alpha = 1.0f; + + particles.push_back(p); + } +} + +void MountDust::update(float deltaTime) { + // Update existing particles + for (auto it = particles.begin(); it != particles.end(); ) { + it->lifetime += deltaTime; + + if (it->lifetime >= it->maxLifetime) { + it = particles.erase(it); + continue; + } + + // Update position + it->position += it->velocity * deltaTime; + + // Slow down velocity (friction) + it->velocity *= 0.96f; + + // Fade out + float t = it->lifetime / it->maxLifetime; + it->alpha = 1.0f - t; + + // Grow slightly as they fade + it->size += deltaTime * 12.0f; + + ++it; + } +} + +void MountDust::render(const Camera& camera) { + if (particles.empty() || !shader) return; + + // Build vertex data + vertexData.clear(); + for (const auto& p : particles) { + vertexData.push_back(p.position.x); + vertexData.push_back(p.position.y); + vertexData.push_back(p.position.z); + vertexData.push_back(p.size); + vertexData.push_back(p.alpha); + } + + // Upload to GPU + glBindBuffer(GL_ARRAY_BUFFER, vbo); + glBufferData(GL_ARRAY_BUFFER, vertexData.size() * sizeof(float), vertexData.data(), GL_DYNAMIC_DRAW); + + // Render + shader->use(); + shader->setUniform("uView", camera.getViewMatrix()); + shader->setUniform("uProjection", camera.getProjectionMatrix()); + + glEnable(GL_BLEND); + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + glDepthMask(GL_FALSE); // Don't write to depth buffer + glEnable(GL_PROGRAM_POINT_SIZE); + + glBindVertexArray(vao); + glDrawArrays(GL_POINTS, 0, static_cast(particles.size())); + glBindVertexArray(0); + + glDepthMask(GL_TRUE); + glDisable(GL_PROGRAM_POINT_SIZE); +} + +} // namespace rendering +} // namespace wowee diff --git a/src/rendering/renderer.cpp b/src/rendering/renderer.cpp index 9e80c518..77eccbe9 100644 --- a/src/rendering/renderer.cpp +++ b/src/rendering/renderer.cpp @@ -13,6 +13,7 @@ #include "rendering/lens_flare.hpp" #include "rendering/weather.hpp" #include "rendering/swim_effects.hpp" +#include "rendering/mount_dust.hpp" #include "rendering/character_renderer.hpp" #include "rendering/wmo_renderer.hpp" #include "rendering/m2_renderer.hpp" @@ -295,6 +296,13 @@ bool Renderer::initialize(core::Window* win) { swimEffects.reset(); } + // Create mount dust effects + mountDust = std::make_unique(); + if (!mountDust->initialize()) { + LOG_WARNING("Failed to initialize mount dust effects"); + mountDust.reset(); + } + // Create character renderer characterRenderer = std::make_unique(); if (!characterRenderer->initialize()) { @@ -1251,6 +1259,29 @@ void Renderer::update(float deltaTime) { swimEffects->update(*camera, *cameraController, *waterRenderer, deltaTime); } + // Update mount dust effects + if (mountDust) { + mountDust->update(deltaTime); + + // Spawn dust when mounted and moving on ground + if (isMounted() && cameraController && !taxiFlight_) { + bool isMoving = cameraController->isMoving(); + bool onGround = cameraController->isGrounded(); + + if (isMoving && onGround) { + // Calculate velocity from camera direction and speed + glm::vec3 forward = camera->getForward(); + float speed = cameraController->getMovementSpeed(); + glm::vec3 velocity = forward * speed; + velocity.z = 0.0f; // Ignore vertical component + + // Spawn dust at mount's feet (slightly below character position) + glm::vec3 dustPos = characterPosition - glm::vec3(0.0f, 0.0f, mountHeightOffset_ * 0.8f); + mountDust->spawnDust(dustPos, velocity, isMoving); + } + } + } + // Update character animations if (characterRenderer) { characterRenderer->update(deltaTime); @@ -1650,6 +1681,11 @@ void Renderer::renderWorld(game::World* world) { swimEffects->render(*camera); } + // Render mount dust effects + if (mountDust && camera) { + mountDust->render(*camera); + } + // Compute view/projection once for all sub-renderers const glm::mat4& view = camera ? camera->getViewMatrix() : glm::mat4(1.0f); const glm::mat4& projection = camera ? camera->getProjectionMatrix() : glm::mat4(1.0f);