2026-02-02 12:24:50 -08:00
|
|
|
#include "rendering/swim_effects.hpp"
|
|
|
|
|
#include "rendering/camera.hpp"
|
|
|
|
|
#include "rendering/camera_controller.hpp"
|
|
|
|
|
#include "rendering/water_renderer.hpp"
|
2026-02-21 19:41:21 -08:00
|
|
|
#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"
|
2026-02-02 12:24:50 -08:00
|
|
|
#include "core/logger.hpp"
|
|
|
|
|
#include <glm/gtc/matrix_transform.hpp>
|
|
|
|
|
#include <random>
|
|
|
|
|
#include <cmath>
|
2026-02-21 19:41:21 -08:00
|
|
|
#include <cstring>
|
2026-02-02 12:24:50 -08:00
|
|
|
|
|
|
|
|
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<float> dist(lo, hi);
|
|
|
|
|
return dist(rng());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
SwimEffects::SwimEffects() = default;
|
|
|
|
|
SwimEffects::~SwimEffects() { shutdown(); }
|
|
|
|
|
|
2026-02-21 19:41:21 -08:00
|
|
|
bool SwimEffects::initialize(VkContext* ctx, VkDescriptorSetLayout perFrameLayout) {
|
2026-02-02 12:24:50 -08:00
|
|
|
LOG_INFO("Initializing swim effects");
|
|
|
|
|
|
2026-02-21 19:41:21 -08:00
|
|
|
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<VkVertexInputAttributeDescription> 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<VkDynamicState> 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;
|
2026-02-02 12:24:50 -08:00
|
|
|
}
|
2026-02-21 19:41:21 -08:00
|
|
|
VkShaderModule fragModule;
|
|
|
|
|
if (!fragModule.loadFromFile(device, "assets/shaders/swim_ripple.frag.spv")) {
|
|
|
|
|
LOG_ERROR("Failed to load swim_ripple fragment shader");
|
|
|
|
|
return false;
|
2026-02-02 12:24:50 -08:00
|
|
|
}
|
|
|
|
|
|
2026-02-21 19:41:21 -08:00
|
|
|
VkPipelineShaderStageCreateInfo vertStage = vertModule.stageInfo(VK_SHADER_STAGE_VERTEX_BIT);
|
|
|
|
|
VkPipelineShaderStageCreateInfo fragStage = fragModule.stageInfo(VK_SHADER_STAGE_FRAGMENT_BIT);
|
2026-02-02 12:24:50 -08:00
|
|
|
|
2026-02-21 19:41:21 -08:00
|
|
|
ripplePipelineLayout = createPipelineLayout(device, {perFrameLayout}, {});
|
|
|
|
|
if (ripplePipelineLayout == VK_NULL_HANDLE) {
|
|
|
|
|
LOG_ERROR("Failed to create ripple pipeline layout");
|
|
|
|
|
return false;
|
|
|
|
|
}
|
2026-02-02 12:24:50 -08:00
|
|
|
|
2026-02-21 19:41:21 -08:00
|
|
|
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())
|
2026-02-22 02:59:24 -08:00
|
|
|
.setMultisample(vkCtx->getMsaaSamples())
|
2026-02-21 19:41:21 -08:00
|
|
|
.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;
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-02-02 12:24:50 -08:00
|
|
|
|
2026-02-21 19:41:21 -08:00
|
|
|
// ---- 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;
|
|
|
|
|
}
|
2026-02-02 12:24:50 -08:00
|
|
|
|
2026-02-21 19:41:21 -08:00
|
|
|
VkPipelineShaderStageCreateInfo vertStage = vertModule.stageInfo(VK_SHADER_STAGE_VERTEX_BIT);
|
|
|
|
|
VkPipelineShaderStageCreateInfo fragStage = fragModule.stageInfo(VK_SHADER_STAGE_FRAGMENT_BIT);
|
2026-02-02 12:24:50 -08:00
|
|
|
|
2026-02-21 19:41:21 -08:00
|
|
|
bubblePipelineLayout = createPipelineLayout(device, {perFrameLayout}, {});
|
|
|
|
|
if (bubblePipelineLayout == VK_NULL_HANDLE) {
|
|
|
|
|
LOG_ERROR("Failed to create bubble pipeline layout");
|
|
|
|
|
return false;
|
2026-02-02 12:24:50 -08:00
|
|
|
}
|
2026-02-21 19:41:21 -08:00
|
|
|
|
|
|
|
|
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())
|
2026-02-22 02:59:24 -08:00
|
|
|
.setMultisample(vkCtx->getMsaaSamples())
|
2026-02-21 19:41:21 -08:00
|
|
|
.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;
|
2026-02-02 12:24:50 -08:00
|
|
|
}
|
2026-02-21 19:41:21 -08:00
|
|
|
}
|
2026-02-02 12:24:50 -08:00
|
|
|
|
2026-02-21 19:41:21 -08:00
|
|
|
// ---- 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;
|
|
|
|
|
}
|
2026-02-02 12:24:50 -08:00
|
|
|
}
|
|
|
|
|
|
2026-02-21 19:41:21 -08:00
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-02-02 12:24:50 -08:00
|
|
|
|
|
|
|
|
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() {
|
2026-02-21 19:41:21 -08:00
|
|
|
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;
|
2026-02-02 12:24:50 -08:00
|
|
|
ripples.clear();
|
|
|
|
|
bubbles.clear();
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-22 02:59:24 -08:00
|
|
|
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<VkVertexInputAttributeDescription> 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<VkDynamicState> 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();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-02 12:24:50 -08:00
|
|
|
void SwimEffects::spawnRipple(const glm::vec3& pos, const glm::vec3& moveDir, float waterH) {
|
|
|
|
|
if (static_cast<int>(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<int>(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<int>(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<int>(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);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-21 19:41:21 -08:00
|
|
|
void SwimEffects::render(VkCommandBuffer cmd, VkDescriptorSet perFrameSet) {
|
2026-02-02 12:24:50 -08:00
|
|
|
if (rippleVertexData.empty() && bubbleVertexData.empty()) return;
|
|
|
|
|
|
2026-02-21 19:41:21 -08:00
|
|
|
VkDeviceSize offset = 0;
|
2026-02-02 12:24:50 -08:00
|
|
|
|
|
|
|
|
// --- Render ripples (splash droplets above water surface) ---
|
2026-02-21 19:41:21 -08:00
|
|
|
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<uint32_t>(rippleVertexData.size() / 5), 1, 0, 0);
|
2026-02-02 12:24:50 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// --- Render bubbles ---
|
2026-02-21 19:41:21 -08:00
|
|
|
if (!bubbleVertexData.empty() && bubblePipeline != VK_NULL_HANDLE) {
|
|
|
|
|
VkDeviceSize uploadSize = bubbleVertexData.size() * sizeof(float);
|
|
|
|
|
if (bubbleDynamicVBAllocInfo.pMappedData) {
|
|
|
|
|
std::memcpy(bubbleDynamicVBAllocInfo.pMappedData, bubbleVertexData.data(), uploadSize);
|
|
|
|
|
}
|
2026-02-02 12:24:50 -08:00
|
|
|
|
2026-02-21 19:41:21 -08:00
|
|
|
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<uint32_t>(bubbleVertexData.size() / 5), 1, 0, 0);
|
|
|
|
|
}
|
2026-02-02 12:24:50 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
} // namespace rendering
|
|
|
|
|
} // namespace wowee
|