#include "rendering/swim_effects.hpp" #include "rendering/camera.hpp" #include "rendering/camera_controller.hpp" #include "rendering/water_renderer.hpp" #include "rendering/vk_context.hpp" #include "rendering/vk_shader.hpp" #include "rendering/vk_pipeline.hpp" #include "rendering/vk_frame_data.hpp" #include "rendering/vk_utils.hpp" #include "core/logger.hpp" #include #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(VkContext* ctx, VkDescriptorSetLayout perFrameLayout) { LOG_INFO("Initializing swim effects"); vkCtx = ctx; VkDevice device = vkCtx->getDevice(); // ---- Vertex input: pos(vec3) + size(float) + alpha(float) = 5 floats, stride = 20 bytes ---- VkVertexInputBindingDescription binding{}; binding.binding = 0; binding.stride = 5 * sizeof(float); binding.inputRate = VK_VERTEX_INPUT_RATE_VERTEX; std::vector attrs(3); // location 0: vec3 position attrs[0].location = 0; attrs[0].binding = 0; attrs[0].format = VK_FORMAT_R32G32B32_SFLOAT; attrs[0].offset = 0; // location 1: float size attrs[1].location = 1; attrs[1].binding = 0; attrs[1].format = VK_FORMAT_R32_SFLOAT; attrs[1].offset = 3 * sizeof(float); // location 2: float alpha attrs[2].location = 2; attrs[2].binding = 0; attrs[2].format = VK_FORMAT_R32_SFLOAT; attrs[2].offset = 4 * sizeof(float); std::vector dynamicStates = { VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR }; // ---- Ripple pipeline ---- { VkShaderModule vertModule; if (!vertModule.loadFromFile(device, "assets/shaders/swim_ripple.vert.spv")) { LOG_ERROR("Failed to load swim_ripple vertex shader"); return false; } VkShaderModule fragModule; if (!fragModule.loadFromFile(device, "assets/shaders/swim_ripple.frag.spv")) { LOG_ERROR("Failed to load swim_ripple fragment shader"); return false; } VkPipelineShaderStageCreateInfo vertStage = vertModule.stageInfo(VK_SHADER_STAGE_VERTEX_BIT); VkPipelineShaderStageCreateInfo fragStage = fragModule.stageInfo(VK_SHADER_STAGE_FRAGMENT_BIT); ripplePipelineLayout = createPipelineLayout(device, {perFrameLayout}, {}); if (ripplePipelineLayout == VK_NULL_HANDLE) { LOG_ERROR("Failed to create ripple pipeline layout"); return false; } ripplePipeline = PipelineBuilder() .setShaders(vertStage, fragStage) .setVertexInput({binding}, attrs) .setTopology(VK_PRIMITIVE_TOPOLOGY_POINT_LIST) .setRasterization(VK_POLYGON_MODE_FILL, VK_CULL_MODE_NONE) .setDepthTest(true, false, VK_COMPARE_OP_LESS) .setColorBlendAttachment(PipelineBuilder::blendAlpha()) .setMultisample(vkCtx->getMsaaSamples()) .setLayout(ripplePipelineLayout) .setRenderPass(vkCtx->getImGuiRenderPass()) .setDynamicStates(dynamicStates) .build(device); vertModule.destroy(); fragModule.destroy(); if (ripplePipeline == VK_NULL_HANDLE) { LOG_ERROR("Failed to create ripple pipeline"); return false; } } // ---- Bubble pipeline ---- { VkShaderModule vertModule; if (!vertModule.loadFromFile(device, "assets/shaders/swim_bubble.vert.spv")) { LOG_ERROR("Failed to load swim_bubble vertex shader"); return false; } VkShaderModule fragModule; if (!fragModule.loadFromFile(device, "assets/shaders/swim_bubble.frag.spv")) { LOG_ERROR("Failed to load swim_bubble fragment shader"); return false; } VkPipelineShaderStageCreateInfo vertStage = vertModule.stageInfo(VK_SHADER_STAGE_VERTEX_BIT); VkPipelineShaderStageCreateInfo fragStage = fragModule.stageInfo(VK_SHADER_STAGE_FRAGMENT_BIT); bubblePipelineLayout = createPipelineLayout(device, {perFrameLayout}, {}); if (bubblePipelineLayout == VK_NULL_HANDLE) { LOG_ERROR("Failed to create bubble pipeline layout"); return false; } bubblePipeline = PipelineBuilder() .setShaders(vertStage, fragStage) .setVertexInput({binding}, attrs) .setTopology(VK_PRIMITIVE_TOPOLOGY_POINT_LIST) .setRasterization(VK_POLYGON_MODE_FILL, VK_CULL_MODE_NONE) .setDepthTest(true, false, VK_COMPARE_OP_LESS) .setColorBlendAttachment(PipelineBuilder::blendAlpha()) .setMultisample(vkCtx->getMsaaSamples()) .setLayout(bubblePipelineLayout) .setRenderPass(vkCtx->getImGuiRenderPass()) .setDynamicStates(dynamicStates) .build(device); vertModule.destroy(); fragModule.destroy(); if (bubblePipeline == VK_NULL_HANDLE) { LOG_ERROR("Failed to create bubble pipeline"); return false; } } // ---- Create dynamic mapped vertex buffers ---- rippleDynamicVBSize = MAX_RIPPLE_PARTICLES * 5 * sizeof(float); { AllocatedBuffer buf = createBuffer(vkCtx->getAllocator(), rippleDynamicVBSize, VK_BUFFER_USAGE_VERTEX_BUFFER_BIT, VMA_MEMORY_USAGE_CPU_TO_GPU); rippleDynamicVB = buf.buffer; rippleDynamicVBAlloc = buf.allocation; rippleDynamicVBAllocInfo = buf.info; if (rippleDynamicVB == VK_NULL_HANDLE) { LOG_ERROR("Failed to create ripple dynamic vertex buffer"); return false; } } bubbleDynamicVBSize = MAX_BUBBLE_PARTICLES * 5 * sizeof(float); { AllocatedBuffer buf = createBuffer(vkCtx->getAllocator(), bubbleDynamicVBSize, VK_BUFFER_USAGE_VERTEX_BUFFER_BIT, VMA_MEMORY_USAGE_CPU_TO_GPU); bubbleDynamicVB = buf.buffer; bubbleDynamicVBAlloc = buf.allocation; bubbleDynamicVBAllocInfo = buf.info; if (bubbleDynamicVB == VK_NULL_HANDLE) { LOG_ERROR("Failed to create bubble dynamic vertex buffer"); return false; } } 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 (vkCtx) { VkDevice device = vkCtx->getDevice(); VmaAllocator allocator = vkCtx->getAllocator(); if (ripplePipeline != VK_NULL_HANDLE) { vkDestroyPipeline(device, ripplePipeline, nullptr); ripplePipeline = VK_NULL_HANDLE; } if (ripplePipelineLayout != VK_NULL_HANDLE) { vkDestroyPipelineLayout(device, ripplePipelineLayout, nullptr); ripplePipelineLayout = VK_NULL_HANDLE; } if (rippleDynamicVB != VK_NULL_HANDLE) { vmaDestroyBuffer(allocator, rippleDynamicVB, rippleDynamicVBAlloc); rippleDynamicVB = VK_NULL_HANDLE; rippleDynamicVBAlloc = VK_NULL_HANDLE; } if (bubblePipeline != VK_NULL_HANDLE) { vkDestroyPipeline(device, bubblePipeline, nullptr); bubblePipeline = VK_NULL_HANDLE; } if (bubblePipelineLayout != VK_NULL_HANDLE) { vkDestroyPipelineLayout(device, bubblePipelineLayout, nullptr); bubblePipelineLayout = VK_NULL_HANDLE; } if (bubbleDynamicVB != VK_NULL_HANDLE) { vmaDestroyBuffer(allocator, bubbleDynamicVB, bubbleDynamicVBAlloc); bubbleDynamicVB = VK_NULL_HANDLE; bubbleDynamicVBAlloc = VK_NULL_HANDLE; } } vkCtx = nullptr; ripples.clear(); bubbles.clear(); } void SwimEffects::recreatePipelines() { if (!vkCtx) return; VkDevice device = vkCtx->getDevice(); // Destroy old pipelines (NOT layouts) if (ripplePipeline != VK_NULL_HANDLE) { vkDestroyPipeline(device, ripplePipeline, nullptr); ripplePipeline = VK_NULL_HANDLE; } if (bubblePipeline != VK_NULL_HANDLE) { vkDestroyPipeline(device, bubblePipeline, nullptr); bubblePipeline = VK_NULL_HANDLE; } // Shared vertex input: pos(vec3) + size(float) + alpha(float) = 5 floats VkVertexInputBindingDescription binding{}; binding.binding = 0; binding.stride = 5 * sizeof(float); binding.inputRate = VK_VERTEX_INPUT_RATE_VERTEX; std::vector attrs(3); attrs[0].location = 0; attrs[0].binding = 0; attrs[0].format = VK_FORMAT_R32G32B32_SFLOAT; attrs[0].offset = 0; attrs[1].location = 1; attrs[1].binding = 0; attrs[1].format = VK_FORMAT_R32_SFLOAT; attrs[1].offset = 3 * sizeof(float); attrs[2].location = 2; attrs[2].binding = 0; attrs[2].format = VK_FORMAT_R32_SFLOAT; attrs[2].offset = 4 * sizeof(float); std::vector dynamicStates = { VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR }; // ---- Rebuild ripple pipeline ---- { VkShaderModule vertModule; vertModule.loadFromFile(device, "assets/shaders/swim_ripple.vert.spv"); VkShaderModule fragModule; fragModule.loadFromFile(device, "assets/shaders/swim_ripple.frag.spv"); VkPipelineShaderStageCreateInfo vertStage = vertModule.stageInfo(VK_SHADER_STAGE_VERTEX_BIT); VkPipelineShaderStageCreateInfo fragStage = fragModule.stageInfo(VK_SHADER_STAGE_FRAGMENT_BIT); ripplePipeline = PipelineBuilder() .setShaders(vertStage, fragStage) .setVertexInput({binding}, attrs) .setTopology(VK_PRIMITIVE_TOPOLOGY_POINT_LIST) .setRasterization(VK_POLYGON_MODE_FILL, VK_CULL_MODE_NONE) .setDepthTest(true, false, VK_COMPARE_OP_LESS) .setColorBlendAttachment(PipelineBuilder::blendAlpha()) .setMultisample(vkCtx->getMsaaSamples()) .setLayout(ripplePipelineLayout) .setRenderPass(vkCtx->getImGuiRenderPass()) .setDynamicStates(dynamicStates) .build(device); vertModule.destroy(); fragModule.destroy(); } // ---- Rebuild bubble pipeline ---- { VkShaderModule vertModule; vertModule.loadFromFile(device, "assets/shaders/swim_bubble.vert.spv"); VkShaderModule fragModule; fragModule.loadFromFile(device, "assets/shaders/swim_bubble.frag.spv"); VkPipelineShaderStageCreateInfo vertStage = vertModule.stageInfo(VK_SHADER_STAGE_VERTEX_BIT); VkPipelineShaderStageCreateInfo fragStage = fragModule.stageInfo(VK_SHADER_STAGE_FRAGMENT_BIT); bubblePipeline = PipelineBuilder() .setShaders(vertStage, fragStage) .setVertexInput({binding}, attrs) .setTopology(VK_PRIMITIVE_TOPOLOGY_POINT_LIST) .setRasterization(VK_POLYGON_MODE_FILL, VK_CULL_MODE_NONE) .setDepthTest(true, false, VK_COMPARE_OP_LESS) .setColorBlendAttachment(PipelineBuilder::blendAlpha()) .setMultisample(vkCtx->getMsaaSamples()) .setLayout(bubblePipelineLayout) .setRenderPass(vkCtx->getImGuiRenderPass()) .setDynamicStates(dynamicStates) .build(device); vertModule.destroy(); fragModule.destroy(); } } 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(VkCommandBuffer cmd, VkDescriptorSet perFrameSet) { if (rippleVertexData.empty() && bubbleVertexData.empty()) return; VkDeviceSize offset = 0; // --- Render ripples (splash droplets above water surface) --- if (!rippleVertexData.empty() && ripplePipeline != VK_NULL_HANDLE) { VkDeviceSize uploadSize = rippleVertexData.size() * sizeof(float); if (rippleDynamicVBAllocInfo.pMappedData) { std::memcpy(rippleDynamicVBAllocInfo.pMappedData, rippleVertexData.data(), uploadSize); } vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, ripplePipeline); vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, ripplePipelineLayout, 0, 1, &perFrameSet, 0, nullptr); vkCmdBindVertexBuffers(cmd, 0, 1, &rippleDynamicVB, &offset); vkCmdDraw(cmd, static_cast(rippleVertexData.size() / 5), 1, 0, 0); } // --- Render bubbles --- if (!bubbleVertexData.empty() && bubblePipeline != VK_NULL_HANDLE) { VkDeviceSize uploadSize = bubbleVertexData.size() * sizeof(float); if (bubbleDynamicVBAllocInfo.pMappedData) { std::memcpy(bubbleDynamicVBAllocInfo.pMappedData, bubbleVertexData.data(), uploadSize); } vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, bubblePipeline); vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, bubblePipelineLayout, 0, 1, &perFrameSet, 0, nullptr); vkCmdBindVertexBuffers(cmd, 0, 1, &bubbleDynamicVB, &offset); vkCmdDraw(cmd, static_cast(bubbleVertexData.size() / 5), 1, 0, 0); } } } // namespace rendering } // namespace wowee