#include "rendering/swim_effects.hpp" #include "rendering/camera.hpp" #include "rendering/camera_controller.hpp" #include "rendering/water_renderer.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()); } SwimEffects::SwimEffects() = default; SwimEffects::~SwimEffects() { shutdown(); } bool SwimEffects::initialize() { LOG_INFO("Initializing swim effects"); // --- Ripple/splash shader (small white spray droplets) --- rippleShader = std::make_unique(); const char* rippleVS = 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* rippleFS = 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 circular splash droplet float alpha = smoothstep(0.5, 0.2, dist) * vAlpha; FragColor = vec4(0.85, 0.92, 1.0, alpha); } )"; if (!rippleShader->loadFromSource(rippleVS, rippleFS)) { LOG_ERROR("Failed to create ripple shader"); return false; } // --- Bubble shader --- bubbleShader = std::make_unique(); const char* bubbleVS = 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* bubbleFS = 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; // Bubble with highlight float edge = smoothstep(0.5, 0.35, dist); float hollow = smoothstep(0.25, 0.35, dist); float bubble = edge * hollow; // Specular highlight near top-left float highlight = smoothstep(0.3, 0.0, length(coord - vec2(-0.12, -0.12))); float alpha = (bubble * 0.6 + highlight * 0.4) * vAlpha; vec3 color = vec3(0.7, 0.85, 1.0); FragColor = vec4(color, alpha); } )"; if (!bubbleShader->loadFromSource(bubbleVS, bubbleFS)) { LOG_ERROR("Failed to create bubble shader"); return false; } // --- Ripple VAO/VBO --- glGenVertexArrays(1, &rippleVAO); glGenBuffers(1, &rippleVBO); glBindVertexArray(rippleVAO); glBindBuffer(GL_ARRAY_BUFFER, rippleVBO); // layout: vec3 pos, float size, float alpha (stride = 5 floats) 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); // --- Bubble VAO/VBO --- glGenVertexArrays(1, &bubbleVAO); glGenBuffers(1, &bubbleVBO); glBindVertexArray(bubbleVAO); glBindBuffer(GL_ARRAY_BUFFER, bubbleVBO); 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); ripples.reserve(MAX_RIPPLE_PARTICLES); bubbles.reserve(MAX_BUBBLE_PARTICLES); rippleVertexData.reserve(MAX_RIPPLE_PARTICLES * 5); bubbleVertexData.reserve(MAX_BUBBLE_PARTICLES * 5); LOG_INFO("Swim effects initialized"); return true; } void SwimEffects::shutdown() { if (rippleVAO) { glDeleteVertexArrays(1, &rippleVAO); rippleVAO = 0; } if (rippleVBO) { glDeleteBuffers(1, &rippleVBO); rippleVBO = 0; } if (bubbleVAO) { glDeleteVertexArrays(1, &bubbleVAO); bubbleVAO = 0; } if (bubbleVBO) { glDeleteBuffers(1, &bubbleVBO); bubbleVBO = 0; } rippleShader.reset(); bubbleShader.reset(); ripples.clear(); bubbles.clear(); } void SwimEffects::spawnRipple(const glm::vec3& pos, const glm::vec3& moveDir, float waterH) { if (static_cast(ripples.size()) >= MAX_RIPPLE_PARTICLES) return; Particle p; // Scatter splash droplets around the character at the water surface float ox = randFloat(-1.5f, 1.5f); float oy = randFloat(-1.5f, 1.5f); p.position = glm::vec3(pos.x + ox, pos.y + oy, waterH + 0.3f); // Spray outward + upward from movement direction float spread = randFloat(-1.0f, 1.0f); glm::vec3 perp(-moveDir.y, moveDir.x, 0.0f); glm::vec3 outDir = -moveDir + perp * spread; float speed = randFloat(1.5f, 4.0f); p.velocity = glm::vec3(outDir.x * speed, outDir.y * speed, randFloat(1.0f, 3.0f)); p.lifetime = 0.0f; p.maxLifetime = randFloat(0.5f, 1.0f); p.size = randFloat(3.0f, 7.0f); p.alpha = randFloat(0.5f, 0.8f); ripples.push_back(p); } void SwimEffects::spawnBubble(const glm::vec3& pos, float /*waterH*/) { if (static_cast(bubbles.size()) >= MAX_BUBBLE_PARTICLES) return; Particle p; float ox = randFloat(-3.0f, 3.0f); float oy = randFloat(-3.0f, 3.0f); float oz = randFloat(-2.0f, 0.0f); p.position = glm::vec3(pos.x + ox, pos.y + oy, pos.z + oz); p.velocity = glm::vec3(randFloat(-0.3f, 0.3f), randFloat(-0.3f, 0.3f), randFloat(4.0f, 8.0f)); p.lifetime = 0.0f; p.maxLifetime = randFloat(2.0f, 3.5f); p.size = randFloat(6.0f, 12.0f); p.alpha = 0.6f; bubbles.push_back(p); } void SwimEffects::update(const Camera& camera, const CameraController& cc, const WaterRenderer& water, float deltaTime) { glm::vec3 camPos = camera.getPosition(); // Use character position for ripples in third-person mode glm::vec3 charPos = camPos; const glm::vec3* followTarget = cc.getFollowTarget(); if (cc.isThirdPerson() && followTarget) { charPos = *followTarget; } // Check water at character position (for ripples) and camera position (for bubbles) auto charWaterH = water.getWaterHeightAt(charPos.x, charPos.y); auto camWaterH = water.getWaterHeightAt(camPos.x, camPos.y); bool swimming = cc.isSwimming(); bool moving = cc.isMoving(); // --- Ripple/splash spawning --- if (swimming && charWaterH) { float wh = *charWaterH; float spawnRate = moving ? 40.0f : 8.0f; rippleSpawnAccum += spawnRate * deltaTime; // Compute movement direction from camera yaw float yawRad = glm::radians(cc.getYaw()); glm::vec3 moveDir(std::cos(yawRad), std::sin(yawRad), 0.0f); if (glm::length(glm::vec2(moveDir)) > 0.001f) { moveDir = glm::normalize(moveDir); } while (rippleSpawnAccum >= 1.0f) { spawnRipple(charPos, moveDir, wh); rippleSpawnAccum -= 1.0f; } } else { rippleSpawnAccum = 0.0f; ripples.clear(); } // --- Bubble spawning --- bool underwater = camWaterH && camPos.z < *camWaterH; if (underwater) { float bubbleRate = 20.0f; bubbleSpawnAccum += bubbleRate * deltaTime; while (bubbleSpawnAccum >= 1.0f) { spawnBubble(camPos, *camWaterH); bubbleSpawnAccum -= 1.0f; } } else { bubbleSpawnAccum = 0.0f; bubbles.clear(); } // --- Update ripples (splash droplets with gravity) --- for (int i = static_cast(ripples.size()) - 1; i >= 0; --i) { auto& p = ripples[i]; p.lifetime += deltaTime; if (p.lifetime >= p.maxLifetime) { ripples[i] = ripples.back(); ripples.pop_back(); continue; } // Apply gravity to splash droplets p.velocity.z -= 9.8f * deltaTime; p.position += p.velocity * deltaTime; // Kill if fallen back below water float surfaceZ = charWaterH ? *charWaterH : 0.0f; if (p.position.z < surfaceZ && p.lifetime > 0.1f) { ripples[i] = ripples.back(); ripples.pop_back(); continue; } float t = p.lifetime / p.maxLifetime; p.alpha = glm::mix(0.7f, 0.0f, t); p.size = glm::mix(5.0f, 2.0f, t); } // --- Update bubbles --- float bubbleCeilH = camWaterH ? *camWaterH : 0.0f; for (int i = static_cast(bubbles.size()) - 1; i >= 0; --i) { auto& p = bubbles[i]; p.lifetime += deltaTime; if (p.lifetime >= p.maxLifetime || p.position.z >= bubbleCeilH) { bubbles[i] = bubbles.back(); bubbles.pop_back(); continue; } // Wobble float wobbleX = std::sin(p.lifetime * 3.0f) * 0.5f; float wobbleY = std::cos(p.lifetime * 2.5f) * 0.5f; p.position += (p.velocity + glm::vec3(wobbleX, wobbleY, 0.0f)) * deltaTime; float t = p.lifetime / p.maxLifetime; if (t > 0.8f) { p.alpha = 0.6f * (1.0f - (t - 0.8f) / 0.2f); } else { p.alpha = 0.6f; } } // --- Build vertex data --- rippleVertexData.clear(); for (const auto& p : ripples) { rippleVertexData.push_back(p.position.x); rippleVertexData.push_back(p.position.y); rippleVertexData.push_back(p.position.z); rippleVertexData.push_back(p.size); rippleVertexData.push_back(p.alpha); } bubbleVertexData.clear(); for (const auto& p : bubbles) { bubbleVertexData.push_back(p.position.x); bubbleVertexData.push_back(p.position.y); bubbleVertexData.push_back(p.position.z); bubbleVertexData.push_back(p.size); bubbleVertexData.push_back(p.alpha); } } void SwimEffects::render(const Camera& camera) { if (rippleVertexData.empty() && bubbleVertexData.empty()) return; glEnable(GL_BLEND); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); glDepthMask(GL_FALSE); glEnable(GL_PROGRAM_POINT_SIZE); glm::mat4 view = camera.getViewMatrix(); glm::mat4 projection = camera.getProjectionMatrix(); // --- Render ripples (splash droplets above water surface) --- if (!rippleVertexData.empty() && rippleShader) { rippleShader->use(); rippleShader->setUniform("uView", view); rippleShader->setUniform("uProjection", projection); glBindVertexArray(rippleVAO); glBindBuffer(GL_ARRAY_BUFFER, rippleVBO); glBufferData(GL_ARRAY_BUFFER, rippleVertexData.size() * sizeof(float), rippleVertexData.data(), GL_DYNAMIC_DRAW); glDrawArrays(GL_POINTS, 0, static_cast(rippleVertexData.size() / 5)); glBindVertexArray(0); } // --- Render bubbles --- if (!bubbleVertexData.empty() && bubbleShader) { bubbleShader->use(); bubbleShader->setUniform("uView", view); bubbleShader->setUniform("uProjection", projection); glBindVertexArray(bubbleVAO); glBindBuffer(GL_ARRAY_BUFFER, bubbleVBO); glBufferData(GL_ARRAY_BUFFER, bubbleVertexData.size() * sizeof(float), bubbleVertexData.data(), GL_DYNAMIC_DRAW); glDrawArrays(GL_POINTS, 0, static_cast(bubbleVertexData.size() / 5)); glBindVertexArray(0); } glDisable(GL_BLEND); glDepthMask(GL_TRUE); glDisable(GL_PROGRAM_POINT_SIZE); } } // namespace rendering } // namespace wowee