Kelsidavis-WoWee/src/rendering/swim_effects.cpp
Kelsi e12141a673 Add configurable MSAA anti-aliasing, update auth screen and terrain shader
- MSAA: conditional 2-att (off) vs 3-att (on) render pass with auto-resolve
- MSAA: multisampled color+depth images, query max supported sample count
- MSAA: .setMultisample() on all 25+ main-pass pipelines across 17 renderers
- MSAA: recreatePipelines() on every sub-renderer for runtime MSAA changes
- MSAA: Renderer::setMsaaSamples() orchestrates swapchain+pipeline+ImGui rebuild
- MSAA: Anti-Aliasing combo (Off/2x/4x/8x) in Video settings, persisted
- Update auth screen assets and terrain fragment shader
2026-02-22 02:59:24 -08:00

524 lines
19 KiB
C++

#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 <glm/gtc/matrix_transform.hpp>
#include <random>
#include <cmath>
#include <cstring>
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(); }
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<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;
}
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<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();
}
}
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);
}
}
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<uint32_t>(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<uint32_t>(bubbleVertexData.size() / 5), 1, 0, 0);
}
}
} // namespace rendering
} // namespace wowee