Kelsidavis-WoWee/src/rendering/water_renderer.cpp
Kelsi 55082a0925 Remove unused baseZ/hasHeights variables in WaterRenderer::loadFromWMO
These were declared to handle per-vertex WMO liquid height variation but
never actually used below — the surface is built with a flat adjustedZ
height throughout. Remove to eliminate -Wunused-variable warnings.
2026-03-09 16:09:44 -07:00

2156 lines
90 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#include "rendering/water_renderer.hpp"
#include "rendering/vk_context.hpp"
#include "rendering/vk_pipeline.hpp"
#include "rendering/vk_shader.hpp"
#include "rendering/vk_utils.hpp"
#include "rendering/vk_frame_data.hpp"
#include "rendering/camera.hpp"
#include "pipeline/adt_loader.hpp"
#include "pipeline/wmo_loader.hpp"
#include "core/logger.hpp"
#include <glm/gtc/matrix_transform.hpp>
#include <algorithm>
#include <cmath>
#include <cstring>
#include <limits>
#include <array>
#include <unordered_map>
namespace wowee {
namespace rendering {
// Matches set 1 binding 0 in water.frag.glsl
struct WaterMaterialUBO {
glm::vec4 waterColor;
float waterAlpha;
float shimmerStrength;
float alphaScale;
float _pad;
};
// Push constants matching water.vert.glsl
struct WaterPushConstants {
glm::mat4 model;
float waveAmp;
float waveFreq;
float waveSpeed;
float liquidBasicType; // 0=water, 1=ocean, 2=magma, 3=slime
};
// Matches set 2 binding 3 in water.frag.glsl
struct ReflectionUBOData {
glm::mat4 reflViewProj;
};
WaterRenderer::WaterRenderer() = default;
WaterRenderer::~WaterRenderer() {
shutdown();
}
bool WaterRenderer::initialize(VkContext* ctx, VkDescriptorSetLayout perFrameLayout) {
vkCtx = ctx;
if (!vkCtx) return false;
LOG_INFO("Initializing water renderer (Vulkan)");
VkDevice device = vkCtx->getDevice();
// --- Material descriptor set layout (set 1) ---
// binding 0: WaterMaterial UBO
VkDescriptorSetLayoutBinding matBinding{};
matBinding.binding = 0;
matBinding.descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER;
matBinding.descriptorCount = 1;
matBinding.stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT;
materialSetLayout = createDescriptorSetLayout(device, { matBinding });
if (!materialSetLayout) {
LOG_ERROR("WaterRenderer: failed to create material set layout");
return false;
}
// --- Descriptor pool ---
VkDescriptorPoolSize poolSize{};
poolSize.type = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER;
poolSize.descriptorCount = MAX_WATER_SETS;
VkDescriptorPoolCreateInfo poolInfo{};
poolInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO;
poolInfo.flags = VK_DESCRIPTOR_POOL_CREATE_FREE_DESCRIPTOR_SET_BIT;
poolInfo.maxSets = MAX_WATER_SETS;
poolInfo.poolSizeCount = 1;
poolInfo.pPoolSizes = &poolSize;
if (vkCreateDescriptorPool(device, &poolInfo, nullptr, &materialDescPool) != VK_SUCCESS) {
LOG_ERROR("WaterRenderer: failed to create descriptor pool");
return false;
}
// --- Scene history + reflection descriptor set layout (set 2) ---
VkDescriptorSetLayoutBinding sceneColorBinding{};
sceneColorBinding.binding = 0;
sceneColorBinding.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
sceneColorBinding.descriptorCount = 1;
sceneColorBinding.stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT;
VkDescriptorSetLayoutBinding sceneDepthBinding{};
sceneDepthBinding.binding = 1;
sceneDepthBinding.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
sceneDepthBinding.descriptorCount = 1;
sceneDepthBinding.stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT;
VkDescriptorSetLayoutBinding reflColorBinding{};
reflColorBinding.binding = 2;
reflColorBinding.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
reflColorBinding.descriptorCount = 1;
reflColorBinding.stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT;
VkDescriptorSetLayoutBinding reflUBOBinding{};
reflUBOBinding.binding = 3;
reflUBOBinding.descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER;
reflUBOBinding.descriptorCount = 1;
reflUBOBinding.stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT;
sceneSetLayout = createDescriptorSetLayout(device,
{sceneColorBinding, sceneDepthBinding, reflColorBinding, reflUBOBinding});
if (!sceneSetLayout) {
LOG_ERROR("WaterRenderer: failed to create scene set layout");
return false;
}
// Pool needs 3 combined image samplers + 1 uniform buffer per frame
std::array<VkDescriptorPoolSize, 2> scenePoolSizes{};
scenePoolSizes[0].type = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
scenePoolSizes[0].descriptorCount = 3 * SCENE_HISTORY_FRAMES;
scenePoolSizes[1].type = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER;
scenePoolSizes[1].descriptorCount = SCENE_HISTORY_FRAMES;
VkDescriptorPoolCreateInfo scenePoolInfo{};
scenePoolInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO;
scenePoolInfo.maxSets = SCENE_HISTORY_FRAMES;
scenePoolInfo.poolSizeCount = static_cast<uint32_t>(scenePoolSizes.size());
scenePoolInfo.pPoolSizes = scenePoolSizes.data();
if (vkCreateDescriptorPool(device, &scenePoolInfo, nullptr, &sceneDescPool) != VK_SUCCESS) {
LOG_ERROR("WaterRenderer: failed to create scene descriptor pool");
return false;
}
// --- Pipeline layout ---
VkPushConstantRange pushRange{};
pushRange.stageFlags = VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT;
pushRange.offset = 0;
pushRange.size = sizeof(WaterPushConstants);
std::vector<VkDescriptorSetLayout> setLayouts = { perFrameLayout, materialSetLayout, sceneSetLayout };
pipelineLayout = createPipelineLayout(device, setLayouts, { pushRange });
if (!pipelineLayout) {
LOG_ERROR("WaterRenderer: failed to create pipeline layout");
return false;
}
// Create reflection resources FIRST so reflectionUBO exists when
// createSceneHistoryResources writes descriptor binding 3
createReflectionResources();
createSceneHistoryResources(vkCtx->getSwapchainExtent(),
vkCtx->getSwapchainFormat(),
vkCtx->getDepthFormat());
// --- Shaders ---
VkShaderModule vertShader, fragShader;
if (!vertShader.loadFromFile(device, "assets/shaders/water.vert.spv")) {
LOG_ERROR("WaterRenderer: failed to load vertex shader");
return false;
}
if (!fragShader.loadFromFile(device, "assets/shaders/water.frag.spv")) {
LOG_ERROR("WaterRenderer: failed to load fragment shader");
return false;
}
// --- Vertex input (interleaved: pos3 + normal3 + uv2 = 8 floats = 32 bytes) ---
VkVertexInputBindingDescription vertBinding{};
vertBinding.binding = 0;
vertBinding.stride = 8 * sizeof(float);
vertBinding.inputRate = VK_VERTEX_INPUT_RATE_VERTEX;
// Water vertex shader only takes aPos(vec3) at loc 0 and aTexCoord(vec2) at loc 1
// (normal is computed in shader from wave derivatives)
std::vector<VkVertexInputAttributeDescription> vertAttribs = {
{ 0, 0, VK_FORMAT_R32G32B32_SFLOAT, 0 }, // aPos
{ 1, 0, VK_FORMAT_R32G32_SFLOAT, 6 * sizeof(float) }, // aTexCoord (skip normal)
};
VkRenderPass mainPass = vkCtx->getImGuiRenderPass();
waterPipeline = PipelineBuilder()
.setShaders(vertShader.stageInfo(VK_SHADER_STAGE_VERTEX_BIT),
fragShader.stageInfo(VK_SHADER_STAGE_FRAGMENT_BIT))
.setVertexInput({ vertBinding }, vertAttribs)
.setTopology(VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST)
.setRasterization(VK_POLYGON_MODE_FILL, VK_CULL_MODE_NONE)
.setDepthTest(true, false, VK_COMPARE_OP_LESS_OR_EQUAL) // depth test yes, write no
.setColorBlendAttachment(PipelineBuilder::blendAlpha())
.setMultisample(vkCtx->getMsaaSamples())
.setLayout(pipelineLayout)
.setRenderPass(mainPass)
.setDynamicStates({ VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR })
.build(device);
vertShader.destroy();
fragShader.destroy();
if (!waterPipeline) {
LOG_ERROR("WaterRenderer: failed to create pipeline");
return false;
}
LOG_INFO("Water renderer initialized (Vulkan)");
return true;
}
void WaterRenderer::recreatePipelines() {
if (!vkCtx) return;
VkDevice device = vkCtx->getDevice();
destroyReflectionResources();
createReflectionResources();
createSceneHistoryResources(vkCtx->getSwapchainExtent(),
vkCtx->getSwapchainFormat(),
vkCtx->getDepthFormat());
// Destroy old pipeline (keep layout)
if (waterPipeline) { vkDestroyPipeline(device, waterPipeline, nullptr); waterPipeline = VK_NULL_HANDLE; }
// Load shaders
VkShaderModule vertShader, fragShader;
if (!vertShader.loadFromFile(device, "assets/shaders/water.vert.spv")) {
LOG_ERROR("WaterRenderer::recreatePipelines: failed to load vertex shader");
return;
}
if (!fragShader.loadFromFile(device, "assets/shaders/water.frag.spv")) {
LOG_ERROR("WaterRenderer::recreatePipelines: failed to load fragment shader");
vertShader.destroy();
return;
}
// Vertex input (same as initialize)
VkVertexInputBindingDescription vertBinding{};
vertBinding.binding = 0;
vertBinding.stride = 8 * sizeof(float);
vertBinding.inputRate = VK_VERTEX_INPUT_RATE_VERTEX;
std::vector<VkVertexInputAttributeDescription> vertAttribs = {
{ 0, 0, VK_FORMAT_R32G32B32_SFLOAT, 0 },
{ 1, 0, VK_FORMAT_R32G32_SFLOAT, 6 * sizeof(float) },
};
VkRenderPass mainPass = vkCtx->getImGuiRenderPass();
waterPipeline = PipelineBuilder()
.setShaders(vertShader.stageInfo(VK_SHADER_STAGE_VERTEX_BIT),
fragShader.stageInfo(VK_SHADER_STAGE_FRAGMENT_BIT))
.setVertexInput({ vertBinding }, vertAttribs)
.setTopology(VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST)
.setRasterization(VK_POLYGON_MODE_FILL, VK_CULL_MODE_NONE)
.setDepthTest(true, false, VK_COMPARE_OP_LESS_OR_EQUAL)
.setColorBlendAttachment(PipelineBuilder::blendAlpha())
.setMultisample(vkCtx->getMsaaSamples())
.setLayout(pipelineLayout)
.setRenderPass(mainPass)
.setDynamicStates({ VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR })
.build(device);
vertShader.destroy();
fragShader.destroy();
if (!waterPipeline) {
LOG_ERROR("WaterRenderer::recreatePipelines: failed to create pipeline");
}
}
void WaterRenderer::setRefractionEnabled(bool enabled) {
if (refractionEnabled == enabled) return;
refractionEnabled = enabled;
// When turning off, clear scene history images to black so the shader
// detects "no data" and uses the non-refraction path.
if (!enabled && vkCtx) {
vkCtx->immediateSubmit([&](VkCommandBuffer cmd) {
for (uint32_t f = 0; f < SCENE_HISTORY_FRAMES; f++) {
auto& sh = sceneHistory[f];
if (!sh.colorImage) continue;
VkImageMemoryBarrier toTransfer{};
toTransfer.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER;
toTransfer.oldLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
toTransfer.newLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL;
toTransfer.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
toTransfer.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
toTransfer.image = sh.colorImage;
toTransfer.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1};
toTransfer.srcAccessMask = VK_ACCESS_SHADER_READ_BIT;
toTransfer.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
vkCmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT,
0, 0, nullptr, 0, nullptr, 1, &toTransfer);
VkClearColorValue clearColor = {{0.0f, 0.0f, 0.0f, 0.0f}};
VkImageSubresourceRange range = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1};
vkCmdClearColorImage(cmd, sh.colorImage, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, &clearColor, 1, &range);
VkImageMemoryBarrier toRead = toTransfer;
toRead.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL;
toRead.newLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
toRead.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
toRead.dstAccessMask = VK_ACCESS_SHADER_READ_BIT;
vkCmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT,
0, 0, nullptr, 0, nullptr, 1, &toRead);
}
});
}
}
void WaterRenderer::shutdown() {
clear();
if (!vkCtx) return;
VkDevice device = vkCtx->getDevice();
vkDeviceWaitIdle(device);
destroyWater1xResources();
destroyReflectionResources();
destroySceneHistoryResources();
if (waterPipeline) { vkDestroyPipeline(device, waterPipeline, nullptr); waterPipeline = VK_NULL_HANDLE; }
if (pipelineLayout) { vkDestroyPipelineLayout(device, pipelineLayout, nullptr); pipelineLayout = VK_NULL_HANDLE; }
if (sceneDescPool) { vkDestroyDescriptorPool(device, sceneDescPool, nullptr); sceneDescPool = VK_NULL_HANDLE; }
if (sceneSetLayout) { vkDestroyDescriptorSetLayout(device, sceneSetLayout, nullptr); sceneSetLayout = VK_NULL_HANDLE; }
if (materialDescPool) { vkDestroyDescriptorPool(device, materialDescPool, nullptr); materialDescPool = VK_NULL_HANDLE; }
if (materialSetLayout) { vkDestroyDescriptorSetLayout(device, materialSetLayout, nullptr); materialSetLayout = VK_NULL_HANDLE; }
vkCtx = nullptr;
}
VkDescriptorSet WaterRenderer::allocateMaterialSet() {
VkDescriptorSetAllocateInfo allocInfo{};
allocInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO;
allocInfo.descriptorPool = materialDescPool;
allocInfo.descriptorSetCount = 1;
allocInfo.pSetLayouts = &materialSetLayout;
VkDescriptorSet set = VK_NULL_HANDLE;
if (vkAllocateDescriptorSets(vkCtx->getDevice(), &allocInfo, &set) != VK_SUCCESS) {
return VK_NULL_HANDLE;
}
return set;
}
void WaterRenderer::destroySceneHistoryResources() {
if (!vkCtx) return;
VkDevice device = vkCtx->getDevice();
for (auto& sh : sceneHistory) {
if (sh.colorView) { vkDestroyImageView(device, sh.colorView, nullptr); sh.colorView = VK_NULL_HANDLE; }
if (sh.depthView) { vkDestroyImageView(device, sh.depthView, nullptr); sh.depthView = VK_NULL_HANDLE; }
if (sh.colorImage) { vmaDestroyImage(vkCtx->getAllocator(), sh.colorImage, sh.colorAlloc); sh.colorImage = VK_NULL_HANDLE; sh.colorAlloc = VK_NULL_HANDLE; }
if (sh.depthImage) { vmaDestroyImage(vkCtx->getAllocator(), sh.depthImage, sh.depthAlloc); sh.depthImage = VK_NULL_HANDLE; sh.depthAlloc = VK_NULL_HANDLE; }
sh.sceneSet = VK_NULL_HANDLE;
}
if (sceneColorSampler) { vkDestroySampler(device, sceneColorSampler, nullptr); sceneColorSampler = VK_NULL_HANDLE; }
if (sceneDepthSampler) { vkDestroySampler(device, sceneDepthSampler, nullptr); sceneDepthSampler = VK_NULL_HANDLE; }
sceneHistoryExtent = {0, 0};
sceneHistoryReady = false;
}
void WaterRenderer::createSceneHistoryResources(VkExtent2D extent, VkFormat colorFormat, VkFormat depthFormat) {
if (!vkCtx || extent.width == 0 || extent.height == 0 || !sceneSetLayout || !sceneDescPool) return;
VkDevice device = vkCtx->getDevice();
destroySceneHistoryResources();
vkResetDescriptorPool(device, sceneDescPool, 0);
sceneHistoryExtent = extent;
// Create shared samplers
VkSamplerCreateInfo sampCI{};
sampCI.sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO;
sampCI.magFilter = VK_FILTER_LINEAR;
sampCI.minFilter = VK_FILTER_LINEAR;
sampCI.addressModeU = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
sampCI.addressModeV = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
sampCI.addressModeW = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
if (vkCreateSampler(device, &sampCI, nullptr, &sceneColorSampler) != VK_SUCCESS) {
LOG_ERROR("WaterRenderer: failed to create scene color sampler");
return;
}
sampCI.magFilter = VK_FILTER_NEAREST;
sampCI.minFilter = VK_FILTER_NEAREST;
if (vkCreateSampler(device, &sampCI, nullptr, &sceneDepthSampler) != VK_SUCCESS) {
LOG_ERROR("WaterRenderer: failed to create scene depth sampler");
return;
}
VkImageCreateInfo colorImgInfo{};
colorImgInfo.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO;
colorImgInfo.imageType = VK_IMAGE_TYPE_2D;
colorImgInfo.format = colorFormat;
colorImgInfo.extent = {extent.width, extent.height, 1};
colorImgInfo.mipLevels = 1;
colorImgInfo.arrayLayers = 1;
colorImgInfo.samples = VK_SAMPLE_COUNT_1_BIT;
colorImgInfo.tiling = VK_IMAGE_TILING_OPTIMAL;
colorImgInfo.usage = VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT;
colorImgInfo.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
VkImageCreateInfo depthImgInfo = colorImgInfo;
depthImgInfo.format = depthFormat;
VmaAllocationCreateInfo allocCI{};
allocCI.usage = VMA_MEMORY_USAGE_GPU_ONLY;
// Create per-frame images, views, and descriptor sets
for (uint32_t f = 0; f < SCENE_HISTORY_FRAMES; f++) {
auto& sh = sceneHistory[f];
if (vmaCreateImage(vkCtx->getAllocator(), &colorImgInfo, &allocCI, &sh.colorImage, &sh.colorAlloc, nullptr) != VK_SUCCESS) {
LOG_ERROR("WaterRenderer: failed to create scene color history image [", f, "]");
return;
}
if (vmaCreateImage(vkCtx->getAllocator(), &depthImgInfo, &allocCI, &sh.depthImage, &sh.depthAlloc, nullptr) != VK_SUCCESS) {
LOG_ERROR("WaterRenderer: failed to create scene depth history image [", f, "]");
return;
}
VkImageViewCreateInfo colorViewInfo{};
colorViewInfo.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO;
colorViewInfo.image = sh.colorImage;
colorViewInfo.viewType = VK_IMAGE_VIEW_TYPE_2D;
colorViewInfo.format = colorFormat;
colorViewInfo.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
colorViewInfo.subresourceRange.levelCount = 1;
colorViewInfo.subresourceRange.layerCount = 1;
if (vkCreateImageView(device, &colorViewInfo, nullptr, &sh.colorView) != VK_SUCCESS) {
LOG_ERROR("WaterRenderer: failed to create scene color history view [", f, "]");
return;
}
VkImageViewCreateInfo depthViewInfo = colorViewInfo;
depthViewInfo.image = sh.depthImage;
depthViewInfo.format = depthFormat;
depthViewInfo.subresourceRange.aspectMask = VK_IMAGE_ASPECT_DEPTH_BIT;
if (vkCreateImageView(device, &depthViewInfo, nullptr, &sh.depthView) != VK_SUCCESS) {
LOG_ERROR("WaterRenderer: failed to create scene depth history view [", f, "]");
return;
}
// Allocate descriptor set for this frame
VkDescriptorSetAllocateInfo ai{};
ai.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO;
ai.descriptorPool = sceneDescPool;
ai.descriptorSetCount = 1;
ai.pSetLayouts = &sceneSetLayout;
if (vkAllocateDescriptorSets(device, &ai, &sh.sceneSet) != VK_SUCCESS) {
LOG_ERROR("WaterRenderer: failed to allocate scene descriptor set [", f, "]");
sh.sceneSet = VK_NULL_HANDLE;
return;
}
VkDescriptorImageInfo colorInfo{};
colorInfo.sampler = sceneColorSampler;
colorInfo.imageView = sh.colorView;
colorInfo.imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
VkDescriptorImageInfo depthInfo{};
depthInfo.sampler = sceneDepthSampler;
depthInfo.imageView = sh.depthView;
depthInfo.imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
VkDescriptorImageInfo reflColorInfo{};
reflColorInfo.sampler = sceneColorSampler;
reflColorInfo.imageView = reflectionColorView ? reflectionColorView : sh.colorView;
reflColorInfo.imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
VkDescriptorBufferInfo reflUBOInfo{};
reflUBOInfo.buffer = reflectionUBO;
reflUBOInfo.offset = 0;
reflUBOInfo.range = sizeof(ReflectionUBOData);
std::vector<VkWriteDescriptorSet> writes;
VkWriteDescriptorSet w0{};
w0.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
w0.dstSet = sh.sceneSet;
w0.dstBinding = 0;
w0.descriptorCount = 1;
w0.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
w0.pImageInfo = &colorInfo;
writes.push_back(w0);
VkWriteDescriptorSet w1{};
w1.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
w1.dstSet = sh.sceneSet;
w1.dstBinding = 1;
w1.descriptorCount = 1;
w1.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
w1.pImageInfo = &depthInfo;
writes.push_back(w1);
VkWriteDescriptorSet w2{};
w2.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
w2.dstSet = sh.sceneSet;
w2.dstBinding = 2;
w2.descriptorCount = 1;
w2.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
w2.pImageInfo = &reflColorInfo;
writes.push_back(w2);
if (reflectionUBO) {
VkWriteDescriptorSet w3{};
w3.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
w3.dstSet = sh.sceneSet;
w3.dstBinding = 3;
w3.descriptorCount = 1;
w3.descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER;
w3.pBufferInfo = &reflUBOInfo;
writes.push_back(w3);
}
vkUpdateDescriptorSets(device, static_cast<uint32_t>(writes.size()), writes.data(), 0, nullptr);
}
// Initialize all per-frame history images to shader-read layout
vkCtx->immediateSubmit([&](VkCommandBuffer cmd) {
std::vector<VkImageMemoryBarrier> barriers;
for (uint32_t f = 0; f < SCENE_HISTORY_FRAMES; f++) {
VkImageMemoryBarrier b{};
b.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER;
b.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED;
b.newLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
b.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
b.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
b.image = sceneHistory[f].colorImage;
b.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1};
b.dstAccessMask = VK_ACCESS_SHADER_READ_BIT;
barriers.push_back(b);
b.image = sceneHistory[f].depthImage;
b.subresourceRange.aspectMask = VK_IMAGE_ASPECT_DEPTH_BIT;
barriers.push_back(b);
}
vkCmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT,
0, 0, nullptr, 0, nullptr, static_cast<uint32_t>(barriers.size()), barriers.data());
});
}
void WaterRenderer::updateMaterialUBO(WaterSurface& surface) {
glm::vec4 color = getLiquidColor(surface.liquidType);
float alpha = getLiquidAlpha(surface.liquidType);
// WMO liquid material override
if (surface.wmoId != 0) {
const uint8_t basicType = (surface.liquidType == 0) ? 0 : ((surface.liquidType - 1) % 4);
if (basicType == 2) {
// Magma — bright orange-red, opaque
color = glm::vec4(1.0f, 0.35f, 0.05f, 1.0f);
alpha = 0.95f;
} else if (basicType == 3) {
// Slime — green, semi-opaque
color = glm::vec4(0.2f, 0.6f, 0.1f, 1.0f);
alpha = 0.85f;
}
}
bool canalProfile = (surface.wmoId != 0) || (surface.liquidType == 5);
float shimmerStrength = canalProfile ? 0.95f : 0.50f;
float alphaScale = canalProfile ? 0.90f : 1.00f;
WaterMaterialUBO mat{};
mat.waterColor = color;
mat.waterAlpha = alpha;
mat.shimmerStrength = shimmerStrength;
mat.alphaScale = alphaScale;
// Create UBO
VkBufferCreateInfo bufCI{};
bufCI.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO;
bufCI.size = sizeof(WaterMaterialUBO);
bufCI.usage = VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT;
VmaAllocationCreateInfo allocCI{};
allocCI.usage = VMA_MEMORY_USAGE_CPU_TO_GPU;
allocCI.flags = VMA_ALLOCATION_CREATE_MAPPED_BIT;
VmaAllocationInfo mapInfo{};
vmaCreateBuffer(vkCtx->getAllocator(), &bufCI, &allocCI,
&surface.materialUBO, &surface.materialAlloc, &mapInfo);
if (mapInfo.pMappedData) {
std::memcpy(mapInfo.pMappedData, &mat, sizeof(mat));
}
// Allocate and write descriptor set
surface.materialSet = allocateMaterialSet();
if (surface.materialSet) {
VkDescriptorBufferInfo bufInfo{};
bufInfo.buffer = surface.materialUBO;
bufInfo.offset = 0;
bufInfo.range = sizeof(WaterMaterialUBO);
VkWriteDescriptorSet write{};
write.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
write.dstSet = surface.materialSet;
write.dstBinding = 0;
write.descriptorCount = 1;
write.descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER;
write.pBufferInfo = &bufInfo;
vkUpdateDescriptorSets(vkCtx->getDevice(), 1, &write, 0, nullptr);
} else {
LOG_WARNING("Water: failed to allocate material descriptor set (pool exhaustion?)");
}
}
// ==============================================================
// Data loading (preserved from GL version — no GL calls)
// ==============================================================
void WaterRenderer::loadFromTerrain(const pipeline::ADTTerrain& terrain, bool append,
int tileX, int tileY) {
constexpr float TILE_SIZE = 33.33333f / 8.0f;
if (!append) {
clear();
}
// ── Pass 1: collect layers into merge groups keyed by {liquidType, roundedHeight} ──
struct ChunkLayerInfo {
int chunkX, chunkY;
const pipeline::ADTTerrain::WaterLayer* layer;
};
struct MergeKey {
uint16_t liquidType;
int32_t roundedHeight; // minHeight * 2, rounded to int
bool operator==(const MergeKey& o) const {
return liquidType == o.liquidType && roundedHeight == o.roundedHeight;
}
};
struct MergeKeyHash {
size_t operator()(const MergeKey& k) const {
return std::hash<uint64_t>()((uint64_t(k.liquidType) << 32) | uint32_t(k.roundedHeight));
}
};
std::unordered_map<MergeKey, std::vector<ChunkLayerInfo>, MergeKeyHash> mergeGroups;
for (int chunkIdx = 0; chunkIdx < 256; chunkIdx++) {
const auto& chunkWater = terrain.waterData[chunkIdx];
if (!chunkWater.hasWater()) continue;
int chunkX = chunkIdx % 16;
int chunkY = chunkIdx / 16;
for (const auto& layer : chunkWater.layers) {
MergeKey key;
key.liquidType = layer.liquidType;
key.roundedHeight = static_cast<int32_t>(std::round(layer.minHeight * 2.0f));
mergeGroups[key].push_back({chunkX, chunkY, &layer});
}
}
// Tile origin = NW corner = chunk(0,0) position
const auto& chunk00 = terrain.getChunk(0, 0);
// Stormwind water lowering check
bool isStormwindArea = (tileX >= 28 && tileX <= 50 && tileY >= 28 && tileY <= 52);
float tileWorldX = 0, tileWorldY = 0;
glm::vec2 moonwellPos2D(0.0f);
if (isStormwindArea) {
tileWorldX = (32.0f - tileX) * 533.33333f;
tileWorldY = (32.0f - tileY) * 533.33333f;
moonwellPos2D = glm::vec2(-8755.9f, 1108.9f);
}
int totalSurfaces = 0;
// Merge threshold: groups with more than this many chunks get merged into
// one tile-wide surface. Small groups (shore, lakes) stay per-chunk so
// their original mask / height data is preserved exactly.
constexpr size_t MERGE_THRESHOLD = 4;
// ── Pass 2: create surfaces ──
for (auto& [key, chunkLayers] : mergeGroups) {
// ── Small group → per-chunk surfaces (original code path) ──
if (chunkLayers.size() <= MERGE_THRESHOLD) {
for (const auto& info : chunkLayers) {
const auto& layer = *info.layer;
const auto& terrainChunk = terrain.getChunk(info.chunkX, info.chunkY);
WaterSurface surface;
surface.position = glm::vec3(
terrainChunk.position[0],
terrainChunk.position[1],
layer.minHeight
);
surface.origin = glm::vec3(
surface.position.x - (static_cast<float>(layer.x) * TILE_SIZE),
surface.position.y - (static_cast<float>(layer.y) * TILE_SIZE),
layer.minHeight
);
surface.stepX = glm::vec3(-TILE_SIZE, 0.0f, 0.0f);
surface.stepY = glm::vec3(0.0f, -TILE_SIZE, 0.0f);
surface.minHeight = layer.minHeight;
surface.maxHeight = layer.maxHeight;
surface.liquidType = layer.liquidType;
surface.xOffset = layer.x;
surface.yOffset = layer.y;
surface.width = layer.width;
surface.height = layer.height;
size_t numVertices = (layer.width + 1) * (layer.height + 1);
bool useFlat = true;
if (layer.heights.size() == numVertices) {
bool sane = true;
for (float h : layer.heights) {
if (!std::isfinite(h) || std::abs(h) > 50000.0f) { sane = false; break; }
if (h < layer.minHeight - 8.0f || h > layer.maxHeight + 8.0f) { sane = false; break; }
}
if (sane) { useFlat = false; surface.heights = layer.heights; }
}
if (useFlat) surface.heights.resize(numVertices, layer.minHeight);
if (isStormwindArea && layer.minHeight > 94.0f) {
float distToMoonwell = glm::distance(glm::vec2(tileWorldX, tileWorldY), moonwellPos2D);
if (distToMoonwell > 300.0f) {
for (float& h : surface.heights) h -= 1.0f;
surface.minHeight -= 1.0f;
surface.maxHeight -= 1.0f;
}
}
surface.mask = layer.mask;
surface.tileX = tileX;
surface.tileY = tileY;
createWaterMesh(surface);
if (surface.indexCount > 0 && vkCtx) {
updateMaterialUBO(surface);
}
surfaces.push_back(std::move(surface));
totalSurfaces++;
}
continue;
}
// ── Large group → merged tile-wide surface ──
WaterSurface surface;
float groupHeight = key.roundedHeight / 2.0f;
surface.width = 128;
surface.height = 128;
surface.xOffset = 0;
surface.yOffset = 0;
surface.liquidType = key.liquidType;
surface.tileX = tileX;
surface.tileY = tileY;
// Origin = chunk(0,0) position (NW corner of tile)
surface.origin = glm::vec3(chunk00.position[0], chunk00.position[1], groupHeight);
surface.position = surface.origin;
surface.stepX = glm::vec3(-TILE_SIZE, 0.0f, 0.0f);
surface.stepY = glm::vec3(0.0f, -TILE_SIZE, 0.0f);
surface.minHeight = groupHeight;
surface.maxHeight = groupHeight;
// Initialize height grid (129×129) with group height
constexpr int MERGED_W = 128;
const int gridW = MERGED_W + 1; // 129
const int gridH = MERGED_W + 1;
surface.heights.resize(gridW * gridH, groupHeight);
// Initialize mask (128×128 sub-tiles)
// Mask uses LSB bit order: tileIndex = row * 128 + col
const int maskBytes = (MERGED_W * MERGED_W + 7) / 8;
// For ocean water (basicType 1) at sea level, fill the entire tile.
// Depth testing against terrain handles land occlusion naturally.
uint8_t basicType = (key.liquidType == 0) ? 0 : ((key.liquidType - 1) % 4);
bool isOcean = (basicType == 1) && (std::abs(groupHeight) < 1.0f);
surface.mask.resize(maskBytes, isOcean ? 0xFF : 0x00);
// ── Fill from each contributing chunk ──
for (const auto& info : chunkLayers) {
const auto& layer = *info.layer;
// Merged grid offset for this chunk
// gx = chunkY*8 + layer.x + localX, gy = chunkX*8 + layer.y + localY
int baseGx = info.chunkY * 8;
int baseGy = info.chunkX * 8;
// Copy heights
int layerGridW = layer.width + 1;
size_t numVertices = static_cast<size_t>(layerGridW) * (layer.height + 1);
bool useFlat = true;
if (layer.heights.size() == numVertices) {
bool sane = true;
for (float h : layer.heights) {
if (!std::isfinite(h) || std::abs(h) > 50000.0f) { sane = false; break; }
if (h < layer.minHeight - 8.0f || h > layer.maxHeight + 8.0f) { sane = false; break; }
}
if (sane) useFlat = false;
}
for (int ly = 0; ly <= layer.height; ly++) {
for (int lx = 0; lx <= layer.width; lx++) {
int mgx = baseGx + layer.x + lx;
int mgy = baseGy + layer.y + ly;
if (mgx >= gridW || mgy >= gridH) continue;
float h;
if (!useFlat) {
int layerIdx = ly * layerGridW + lx;
h = layer.heights[layerIdx];
} else {
h = layer.minHeight;
}
surface.heights[mgy * gridW + mgx] = h;
if (h < surface.minHeight) surface.minHeight = h;
if (h > surface.maxHeight) surface.maxHeight = h;
}
}
// Copy mask — mark contributing sub-tiles as renderable
for (int ly = 0; ly < layer.height; ly++) {
for (int lx = 0; lx < layer.width; lx++) {
bool render = true;
if (!layer.mask.empty()) {
int cx = layer.x + lx;
int cy = layer.y + ly;
int origTileIdx = cy * 8 + cx;
int origByte = origTileIdx / 8;
int origBit = origTileIdx % 8;
if (origByte < static_cast<int>(layer.mask.size())) {
uint8_t mb = layer.mask[origByte];
render = (mb & (1 << origBit)) || (mb & (1 << (7 - origBit)));
}
}
if (render) {
int mx = baseGx + layer.x + lx;
int my = baseGy + layer.y + ly;
if (mx >= MERGED_W || my >= MERGED_W) continue;
int mergedTileIdx = my * MERGED_W + mx;
int byteIdx = mergedTileIdx / 8;
int bitIdx = mergedTileIdx % 8;
surface.mask[byteIdx] |= static_cast<uint8_t>(1 << bitIdx);
}
}
}
}
// Stormwind water lowering
if (isStormwindArea && surface.minHeight > 94.0f) {
float distToMoonwell = glm::distance(glm::vec2(tileWorldX, tileWorldY), moonwellPos2D);
if (distToMoonwell > 300.0f) {
for (float& h : surface.heights) h -= 1.0f;
surface.minHeight -= 1.0f;
surface.maxHeight -= 1.0f;
}
}
createWaterMesh(surface);
if (surface.indexCount > 0 && vkCtx) {
updateMaterialUBO(surface);
}
surfaces.push_back(std::move(surface));
totalSurfaces++;
}
if (totalSurfaces > 0) {
LOG_INFO("Water: Loaded ", totalSurfaces, " surfaces from tile [", tileX, ",", tileY,
"] (", mergeGroups.size(), " groups), total surfaces: ", surfaces.size());
}
}
void WaterRenderer::removeTile(int tileX, int tileY) {
int removed = 0;
auto it = surfaces.begin();
while (it != surfaces.end()) {
if (it->tileX == tileX && it->tileY == tileY) {
destroyWaterMesh(*it);
it = surfaces.erase(it);
removed++;
} else {
++it;
}
}
if (removed > 0) {
LOG_DEBUG("Water: Removed ", removed, " surfaces for tile [", tileX, ",", tileY, "], remaining: ", surfaces.size());
}
}
void WaterRenderer::loadFromWMO([[maybe_unused]] const pipeline::WMOLiquid& liquid,
[[maybe_unused]] const glm::mat4& modelMatrix,
[[maybe_unused]] uint32_t wmoId) {
if (!liquid.hasLiquid() || liquid.xTiles == 0 || liquid.yTiles == 0) return;
if (liquid.xVerts < 2 || liquid.yVerts < 2) return;
if (liquid.xTiles != liquid.xVerts - 1 || liquid.yTiles != liquid.yVerts - 1) return;
if (liquid.xTiles > 64 || liquid.yTiles > 64) return;
WaterSurface surface;
surface.tileX = -1;
surface.tileY = -1;
surface.wmoId = wmoId;
surface.liquidType = liquid.materialId;
surface.xOffset = 0;
surface.yOffset = 0;
surface.width = static_cast<uint8_t>(std::min<uint32_t>(255, liquid.xTiles));
surface.height = static_cast<uint8_t>(std::min<uint32_t>(255, liquid.yTiles));
constexpr float WMO_LIQUID_TILE_SIZE = 4.1666625f;
const glm::vec3 localBase(liquid.basePosition.x, liquid.basePosition.y, liquid.basePosition.z);
const glm::vec3 localStepX(WMO_LIQUID_TILE_SIZE, 0.0f, 0.0f);
const glm::vec3 localStepY(0.0f, WMO_LIQUID_TILE_SIZE, 0.0f);
surface.origin = glm::vec3(modelMatrix * glm::vec4(localBase, 1.0f));
surface.stepX = glm::vec3(modelMatrix * glm::vec4(localStepX, 0.0f));
surface.stepY = glm::vec3(modelMatrix * glm::vec4(localStepY, 0.0f));
surface.position = surface.origin;
float stepXLen = glm::length(surface.stepX);
float stepYLen = glm::length(surface.stepY);
glm::vec3 planeN = glm::cross(surface.stepX, surface.stepY);
float nz = (glm::length(planeN) > 1e-4f) ? std::abs(glm::normalize(planeN).z) : 0.0f;
float spanX = stepXLen * static_cast<float>(surface.width);
float spanY = stepYLen * static_cast<float>(surface.height);
if (stepXLen < 0.2f || stepXLen > 12.0f ||
stepYLen < 0.2f || stepYLen > 12.0f ||
nz < 0.60f || spanX > 450.0f || spanY > 450.0f) return;
const int gridWidth = static_cast<int>(surface.width) + 1;
const int gridHeight = static_cast<int>(surface.height) + 1;
const int vertexCount = gridWidth * gridHeight;
// WMO liquid base heights sit ~2 units above the visual waterline.
constexpr float WMO_WATER_Z_OFFSET = -1.0f;
float adjustedZ = surface.origin.z + WMO_WATER_Z_OFFSET;
surface.heights.assign(vertexCount, adjustedZ);
surface.minHeight = adjustedZ;
surface.maxHeight = adjustedZ;
surface.origin.z = adjustedZ;
surface.position.z = adjustedZ;
if (surface.origin.z > 2000.0f || surface.origin.z < -500.0f) return;
// Build tile mask from MLIQ flags
size_t tileCount = static_cast<size_t>(surface.width) * static_cast<size_t>(surface.height);
size_t maskBytes = (tileCount + 7) / 8;
surface.mask.assign(maskBytes, 0x00);
for (size_t t = 0; t < tileCount; t++) {
bool hasLiquid = true;
int tx = static_cast<int>(t) % surface.width;
int ty = static_cast<int>(t) / surface.width;
// Standard WoW check: low nibble 0x0F = "don't render"
if (t < liquid.flags.size()) {
if ((liquid.flags[t] & 0x0F) == 0x0F) {
hasLiquid = false;
}
}
// Suppress water tiles that extend into enclosed WMO areas
// (e.g. Stormwind barracks stairway where canal water pokes through)
// Render coords: x=wowY(west), y=wowX(north)
if (hasLiquid) {
glm::vec3 tileWorld = surface.origin +
surface.stepX * (static_cast<float>(tx) + 0.5f) +
surface.stepY * (static_cast<float>(ty) + 0.5f);
// Stormwind Barracks / Stockade stairway:
// Stockade entrance at approximately render (-8768, 848)
if (tileWorld.x > -8790.0f && tileWorld.x < -8735.0f &&
tileWorld.y > 828.0f && tileWorld.y < 878.0f) {
hasLiquid = false;
}
}
if (hasLiquid) {
size_t byteIdx = t / 8;
size_t bitIdx = t % 8;
surface.mask[byteIdx] |= (1 << bitIdx);
}
}
createWaterMesh(surface);
// Count how many tiles passed the flag check and compute bounds
size_t activeTiles = 0;
float minWX = 1e9f, maxWX = -1e9f, minWY = 1e9f, maxWY = -1e9f;
for (size_t t = 0; t < tileCount; t++) {
size_t byteIdx = t / 8;
size_t bitIdx = t % 8;
if (surface.mask[byteIdx] & (1 << bitIdx)) {
activeTiles++;
int atx = static_cast<int>(t) % surface.width;
int aty = static_cast<int>(t) / surface.width;
glm::vec3 tw = surface.origin +
surface.stepX * (static_cast<float>(atx) + 0.5f) +
surface.stepY * (static_cast<float>(aty) + 0.5f);
if (tw.x < minWX) minWX = tw.x;
if (tw.x > maxWX) maxWX = tw.x;
if (tw.y < minWY) minWY = tw.y;
if (tw.y > maxWY) maxWY = tw.y;
}
}
LOG_DEBUG("WMO water: origin=(", surface.origin.x, ",", surface.origin.y, ",", surface.origin.z,
") tiles=", (int)surface.width, "x", (int)surface.height,
" active=", activeTiles, "/", tileCount,
" wmoId=", wmoId, " indexCount=", surface.indexCount,
" bounds x=[", minWX, "..", maxWX, "] y=[", minWY, "..", maxWY, "]");
if (surface.indexCount > 0) {
if (vkCtx) updateMaterialUBO(surface);
surfaces.push_back(std::move(surface));
}
}
void WaterRenderer::removeWMO(uint32_t wmoId) {
if (wmoId == 0) return;
auto it = surfaces.begin();
while (it != surfaces.end()) {
if (it->wmoId == wmoId) {
destroyWaterMesh(*it);
it = surfaces.erase(it);
} else {
++it;
}
}
}
void WaterRenderer::clear() {
for (auto& surface : surfaces) {
destroyWaterMesh(surface);
}
surfaces.clear();
if (vkCtx && materialDescPool) {
vkResetDescriptorPool(vkCtx->getDevice(), materialDescPool, 0);
}
}
// ==============================================================
// Rendering
// ==============================================================
void WaterRenderer::render(VkCommandBuffer cmd, VkDescriptorSet perFrameSet,
const Camera& /*camera*/, float /*time*/, bool use1x, uint32_t frameIndex) {
VkPipeline pipeline = (use1x && water1xPipeline) ? water1xPipeline : waterPipeline;
if (!renderingEnabled || surfaces.empty() || !pipeline) {
if (renderDiagCounter_++ % 300 == 0 && !surfaces.empty()) {
LOG_WARNING("Water: render skipped — enabled=", renderingEnabled,
" surfaces=", surfaces.size(),
" pipeline=", (pipeline ? "ok" : "null"),
" use1x=", use1x);
}
return;
}
uint32_t fi = frameIndex % SCENE_HISTORY_FRAMES;
VkDescriptorSet activeSceneSet = sceneHistory[fi].sceneSet;
if (!activeSceneSet) {
if (renderDiagCounter_++ % 300 == 0) {
LOG_WARNING("Water: render skipped — sceneSet is null, surfaces=", surfaces.size());
}
return;
}
vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline);
vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, pipelineLayout,
0, 1, &perFrameSet, 0, nullptr);
vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, pipelineLayout,
2, 1, &activeSceneSet, 0, nullptr);
for (const auto& surface : surfaces) {
if (surface.vertexBuffer == VK_NULL_HANDLE || surface.indexCount == 0) continue;
if (!surface.materialSet) continue;
bool isWmoWater = (surface.wmoId != 0);
bool canalProfile = isWmoWater || (surface.liquidType == 5);
uint8_t basicType = (surface.liquidType == 0) ? 0 : ((surface.liquidType - 1) % 4);
// WMO water gets no wave displacement — prevents visible slosh at
// geometry edges (bridges, docks) where water is far below the surface.
float waveAmp = isWmoWater ? 0.0f : (basicType == 1 ? 0.35f : 0.08f);
float waveFreq = canalProfile ? 0.35f : (basicType == 1 ? 0.20f : 0.30f);
float waveSpeed = canalProfile ? 1.00f : (basicType == 1 ? 1.20f : 1.40f);
WaterPushConstants push{};
push.model = glm::mat4(1.0f);
push.waveAmp = waveAmp;
push.waveFreq = waveFreq;
push.waveSpeed = waveSpeed;
push.liquidBasicType = static_cast<float>(basicType);
vkCmdPushConstants(cmd, pipelineLayout,
VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT,
0, sizeof(WaterPushConstants), &push);
vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, pipelineLayout,
1, 1, &surface.materialSet, 0, nullptr);
VkDeviceSize offset = 0;
vkCmdBindVertexBuffers(cmd, 0, 1, &surface.vertexBuffer, &offset);
vkCmdBindIndexBuffer(cmd, surface.indexBuffer, 0, VK_INDEX_TYPE_UINT32);
vkCmdDrawIndexed(cmd, static_cast<uint32_t>(surface.indexCount), 1, 0, 0, 0);
}
}
void WaterRenderer::captureSceneHistory(VkCommandBuffer cmd,
VkImage srcColorImage,
VkImage srcDepthImage,
VkExtent2D srcExtent,
bool srcDepthIsMsaa,
uint32_t frameIndex) {
uint32_t fi = frameIndex % SCENE_HISTORY_FRAMES;
auto& sh = sceneHistory[fi];
if (!vkCtx || !cmd || !sh.colorImage || !sh.depthImage || srcExtent.width == 0 || srcExtent.height == 0) {
return;
}
VkExtent2D copyExtent{
std::min(srcExtent.width, sceneHistoryExtent.width),
std::min(srcExtent.height, sceneHistoryExtent.height)
};
if (copyExtent.width == 0 || copyExtent.height == 0) return;
auto barrier2 = [&](VkImage image,
VkImageAspectFlags aspect,
VkImageLayout oldLayout,
VkImageLayout newLayout,
VkAccessFlags srcAccess,
VkAccessFlags dstAccess,
VkPipelineStageFlags srcStage,
VkPipelineStageFlags dstStage) {
VkImageMemoryBarrier b{};
b.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER;
b.oldLayout = oldLayout;
b.newLayout = newLayout;
b.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
b.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
b.image = image;
b.subresourceRange.aspectMask = aspect;
b.subresourceRange.baseMipLevel = 0;
b.subresourceRange.levelCount = 1;
b.subresourceRange.baseArrayLayer = 0;
b.subresourceRange.layerCount = 1;
b.srcAccessMask = srcAccess;
b.dstAccessMask = dstAccess;
vkCmdPipelineBarrier(cmd, srcStage, dstStage, 0, 0, nullptr, 0, nullptr, 1, &b);
};
// Color source: final render pass layout is PRESENT_SRC.
barrier2(srcColorImage, VK_IMAGE_ASPECT_COLOR_BIT,
VK_IMAGE_LAYOUT_PRESENT_SRC_KHR, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
0, VK_ACCESS_TRANSFER_READ_BIT,
VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT);
barrier2(sh.colorImage, VK_IMAGE_ASPECT_COLOR_BIT,
VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
VK_ACCESS_SHADER_READ_BIT, VK_ACCESS_TRANSFER_WRITE_BIT,
VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT);
VkImageCopy colorCopy{};
colorCopy.srcSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1};
colorCopy.dstSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1};
colorCopy.extent = {copyExtent.width, copyExtent.height, 1};
vkCmdCopyImage(cmd, srcColorImage, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
sh.colorImage, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, &colorCopy);
barrier2(sh.colorImage, VK_IMAGE_ASPECT_COLOR_BIT,
VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL,
VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT,
VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT);
barrier2(srcColorImage, VK_IMAGE_ASPECT_COLOR_BIT,
VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, VK_IMAGE_LAYOUT_PRESENT_SRC_KHR,
VK_ACCESS_TRANSFER_READ_BIT, 0,
VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT);
// Depth source: only copy when source is single-sampled.
if (!srcDepthIsMsaa && srcDepthImage != VK_NULL_HANDLE) {
barrier2(srcDepthImage, VK_IMAGE_ASPECT_DEPTH_BIT,
VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT,
VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT);
barrier2(sh.depthImage, VK_IMAGE_ASPECT_DEPTH_BIT,
VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
VK_ACCESS_SHADER_READ_BIT, VK_ACCESS_TRANSFER_WRITE_BIT,
VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT);
VkImageCopy depthCopy{};
depthCopy.srcSubresource = {VK_IMAGE_ASPECT_DEPTH_BIT, 0, 0, 1};
depthCopy.dstSubresource = {VK_IMAGE_ASPECT_DEPTH_BIT, 0, 0, 1};
depthCopy.extent = {copyExtent.width, copyExtent.height, 1};
vkCmdCopyImage(cmd, srcDepthImage, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
sh.depthImage, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, &depthCopy);
barrier2(sh.depthImage, VK_IMAGE_ASPECT_DEPTH_BIT,
VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL,
VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT,
VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT);
barrier2(srcDepthImage, VK_IMAGE_ASPECT_DEPTH_BIT,
VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL,
VK_ACCESS_TRANSFER_READ_BIT, VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT,
VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT);
}
sceneHistoryReady = true;
}
// ==============================================================
// Mesh creation (Vulkan upload instead of GL)
// ==============================================================
void WaterRenderer::createWaterMesh(WaterSurface& surface) {
const int gridWidth = surface.width + 1;
const int gridHeight = surface.height + 1;
constexpr float VISUAL_WATER_Z_BIAS = 0.02f;
std::vector<float> vertices;
std::vector<uint32_t> indices;
for (int y = 0; y < gridHeight; y++) {
for (int x = 0; x < gridWidth; x++) {
int index = y * gridWidth + x;
float height = (index < static_cast<int>(surface.heights.size()))
? surface.heights[index] : surface.minHeight;
glm::vec3 pos = surface.origin +
surface.stepX * static_cast<float>(x) +
surface.stepY * static_cast<float>(y);
pos.z = height + VISUAL_WATER_Z_BIAS;
// pos (3 floats)
vertices.push_back(pos.x);
vertices.push_back(pos.y);
vertices.push_back(pos.z);
// normal (3 floats) - up
vertices.push_back(0.0f);
vertices.push_back(0.0f);
vertices.push_back(1.0f);
// texcoord (2 floats)
vertices.push_back(static_cast<float>(x) / std::max(1, gridWidth - 1));
vertices.push_back(static_cast<float>(y) / std::max(1, gridHeight - 1));
}
}
// Generate indices respecting render mask (same logic as GL version)
for (int y = 0; y < gridHeight - 1; y++) {
for (int x = 0; x < gridWidth - 1; x++) {
bool renderTile = true;
if (!surface.mask.empty()) {
int tileIndex;
bool isMergedTerrain = (surface.wmoId == 0 && surface.width > 8);
if (surface.wmoId == 0 && surface.width <= 8 && surface.mask.size() >= 8) {
int cx = static_cast<int>(surface.xOffset) + x;
int cy = static_cast<int>(surface.yOffset) + y;
tileIndex = cy * 8 + cx;
} else {
tileIndex = y * surface.width + x;
}
int byteIndex = tileIndex / 8;
int bitIndex = tileIndex % 8;
if (byteIndex < static_cast<int>(surface.mask.size())) {
uint8_t maskByte = surface.mask[byteIndex];
if (isMergedTerrain) {
// Merged surfaces use LSB-only bit order
renderTile = (maskByte & (1 << bitIndex)) != 0;
} else {
bool lsbOrder = (maskByte & (1 << bitIndex)) != 0;
bool msbOrder = (maskByte & (1 << (7 - bitIndex))) != 0;
renderTile = lsbOrder || msbOrder;
}
if (!renderTile) {
for (int dy = -1; dy <= 1; dy++) {
for (int dx = -1; dx <= 1; dx++) {
if (dx == 0 && dy == 0) continue;
int nx = x + dx, ny = y + dy;
if (nx < 0 || ny < 0 || nx >= gridWidth-1 || ny >= gridHeight-1) continue;
int neighborIdx;
if (surface.wmoId == 0 && surface.width <= 8 && surface.mask.size() >= 8) {
neighborIdx = (static_cast<int>(surface.yOffset) + ny) * 8 +
(static_cast<int>(surface.xOffset) + nx);
} else {
neighborIdx = ny * surface.width + nx;
}
int nByteIdx = neighborIdx / 8;
int nBitIdx = neighborIdx % 8;
if (nByteIdx < static_cast<int>(surface.mask.size())) {
uint8_t nMask = surface.mask[nByteIdx];
if (isMergedTerrain) {
if (nMask & (1 << nBitIdx)) {
renderTile = true;
goto found_neighbor;
}
} else {
if ((nMask & (1 << nBitIdx)) || (nMask & (1 << (7 - nBitIdx)))) {
renderTile = true;
goto found_neighbor;
}
}
}
}
}
found_neighbor:;
}
}
}
if (!renderTile) continue;
int topLeft = y * gridWidth + x;
int topRight = topLeft + 1;
int bottomLeft = (y + 1) * gridWidth + x;
int bottomRight = bottomLeft + 1;
indices.push_back(topLeft);
indices.push_back(bottomLeft);
indices.push_back(topRight);
indices.push_back(topRight);
indices.push_back(bottomLeft);
indices.push_back(bottomRight);
}
}
// Fallback: if terrain MH2O mask produced no tiles, render full rect
if (indices.empty() && surface.wmoId == 0) {
for (int y = 0; y < gridHeight - 1; y++) {
for (int x = 0; x < gridWidth - 1; x++) {
int topLeft = y * gridWidth + x;
int topRight = topLeft + 1;
int bottomLeft = (y + 1) * gridWidth + x;
int bottomRight = bottomLeft + 1;
indices.push_back(topLeft);
indices.push_back(bottomLeft);
indices.push_back(topRight);
indices.push_back(topRight);
indices.push_back(bottomLeft);
indices.push_back(bottomRight);
}
}
}
if (indices.empty()) return;
surface.indexCount = static_cast<int>(indices.size());
if (!vkCtx) return;
// Upload vertex buffer
VkDeviceSize vbSize = vertices.size() * sizeof(float);
AllocatedBuffer vb = uploadBuffer(*vkCtx, vertices.data(), vbSize,
VK_BUFFER_USAGE_VERTEX_BUFFER_BIT);
surface.vertexBuffer = vb.buffer;
surface.vertexAlloc = vb.allocation;
// Upload index buffer
VkDeviceSize ibSize = indices.size() * sizeof(uint32_t);
AllocatedBuffer ib = uploadBuffer(*vkCtx, indices.data(), ibSize,
VK_BUFFER_USAGE_INDEX_BUFFER_BIT);
surface.indexBuffer = ib.buffer;
surface.indexAlloc = ib.allocation;
}
void WaterRenderer::destroyWaterMesh(WaterSurface& surface) {
if (!vkCtx) return;
VmaAllocator allocator = vkCtx->getAllocator();
if (surface.vertexBuffer) {
AllocatedBuffer ab{}; ab.buffer = surface.vertexBuffer; ab.allocation = surface.vertexAlloc;
destroyBuffer(allocator, ab);
surface.vertexBuffer = VK_NULL_HANDLE;
}
if (surface.indexBuffer) {
AllocatedBuffer ab{}; ab.buffer = surface.indexBuffer; ab.allocation = surface.indexAlloc;
destroyBuffer(allocator, ab);
surface.indexBuffer = VK_NULL_HANDLE;
}
if (surface.materialUBO) {
AllocatedBuffer ab{}; ab.buffer = surface.materialUBO; ab.allocation = surface.materialAlloc;
destroyBuffer(allocator, ab);
surface.materialUBO = VK_NULL_HANDLE;
}
if (surface.materialSet && materialDescPool) {
vkFreeDescriptorSets(vkCtx->getDevice(), materialDescPool, 1, &surface.materialSet);
}
surface.materialSet = VK_NULL_HANDLE;
}
// ==============================================================
// Query functions (data-only, no GL)
// ==============================================================
std::optional<float> WaterRenderer::getWaterHeightAt(float glX, float glY) const {
std::optional<float> best;
for (const auto& surface : surfaces) {
glm::vec2 rel(glX - surface.origin.x, glY - surface.origin.y);
glm::vec2 sX(surface.stepX.x, surface.stepX.y);
glm::vec2 sY(surface.stepY.x, surface.stepY.y);
float lenSqX = glm::dot(sX, sX);
float lenSqY = glm::dot(sY, sY);
if (lenSqX < 1e-6f || lenSqY < 1e-6f) continue;
float gx = glm::dot(rel, sX) / lenSqX;
float gy = glm::dot(rel, sY) / lenSqY;
if (gx < 0.0f || gx > static_cast<float>(surface.width) ||
gy < 0.0f || gy > static_cast<float>(surface.height)) continue;
int gridWidth = surface.width + 1;
int ix = static_cast<int>(gx);
int iy = static_cast<int>(gy);
float fx = gx - ix;
float fy = gy - iy;
if (ix >= surface.width) { ix = surface.width - 1; fx = 1.0f; }
if (iy >= surface.height) { iy = surface.height - 1; fy = 1.0f; }
if (ix < 0 || iy < 0) continue;
if (!surface.mask.empty()) {
int tileIndex;
if (surface.wmoId == 0 && surface.width <= 8 && surface.mask.size() >= 8) {
tileIndex = (static_cast<int>(surface.yOffset) + iy) * 8 +
(static_cast<int>(surface.xOffset) + ix);
} else {
tileIndex = iy * surface.width + ix;
}
int byteIndex = tileIndex / 8;
int bitIndex = tileIndex % 8;
if (byteIndex < static_cast<int>(surface.mask.size())) {
uint8_t maskByte = surface.mask[byteIndex];
bool renderTile;
if (surface.wmoId == 0 && surface.width > 8) {
renderTile = (maskByte & (1 << bitIndex)) != 0;
} else {
renderTile = (maskByte & (1 << bitIndex)) || (maskByte & (1 << (7 - bitIndex)));
}
if (!renderTile) continue;
}
}
int idx00 = iy * gridWidth + ix;
int idx10 = idx00 + 1;
int idx01 = idx00 + gridWidth;
int idx11 = idx01 + 1;
int total = static_cast<int>(surface.heights.size());
if (idx11 >= total) continue;
float h00 = surface.heights[idx00], h10 = surface.heights[idx10];
float h01 = surface.heights[idx01], h11 = surface.heights[idx11];
float h = h00*(1-fx)*(1-fy) + h10*fx*(1-fy) + h01*(1-fx)*fy + h11*fx*fy;
if (!best || h > *best) best = h;
}
return best;
}
std::optional<float> WaterRenderer::getNearestWaterHeightAt(float glX, float glY, float queryZ, float maxAbove) const {
std::optional<float> best;
float bestDist = 1e9f;
for (const auto& surface : surfaces) {
glm::vec2 rel(glX - surface.origin.x, glY - surface.origin.y);
glm::vec2 sX(surface.stepX.x, surface.stepX.y);
glm::vec2 sY(surface.stepY.x, surface.stepY.y);
float lenSqX = glm::dot(sX, sX);
float lenSqY = glm::dot(sY, sY);
if (lenSqX < 1e-6f || lenSqY < 1e-6f) continue;
float gx = glm::dot(rel, sX) / lenSqX;
float gy = glm::dot(rel, sY) / lenSqY;
if (gx < 0.0f || gx > static_cast<float>(surface.width) ||
gy < 0.0f || gy > static_cast<float>(surface.height)) continue;
int gridWidth = surface.width + 1;
int ix = static_cast<int>(gx);
int iy = static_cast<int>(gy);
float fx = gx - ix;
float fy = gy - iy;
if (ix >= surface.width) { ix = surface.width - 1; fx = 1.0f; }
if (iy >= surface.height) { iy = surface.height - 1; fy = 1.0f; }
if (ix < 0 || iy < 0) continue;
if (!surface.mask.empty()) {
int tileIndex;
if (surface.wmoId == 0 && surface.width <= 8 && surface.mask.size() >= 8) {
tileIndex = (static_cast<int>(surface.yOffset) + iy) * 8 +
(static_cast<int>(surface.xOffset) + ix);
} else {
tileIndex = iy * surface.width + ix;
}
int byteIndex = tileIndex / 8;
int bitIndex = tileIndex % 8;
if (byteIndex < static_cast<int>(surface.mask.size())) {
uint8_t maskByte = surface.mask[byteIndex];
bool renderTile;
if (surface.wmoId == 0 && surface.width > 8) {
renderTile = (maskByte & (1 << bitIndex)) != 0;
} else {
renderTile = (maskByte & (1 << bitIndex)) || (maskByte & (1 << (7 - bitIndex)));
}
if (!renderTile) continue;
}
}
int idx00 = iy * gridWidth + ix;
int idx10 = idx00 + 1;
int idx01 = idx00 + gridWidth;
int idx11 = idx01 + 1;
int total = static_cast<int>(surface.heights.size());
if (idx11 >= total) continue;
float h00 = surface.heights[idx00], h10 = surface.heights[idx10];
float h01 = surface.heights[idx01], h11 = surface.heights[idx11];
float h = h00*(1-fx)*(1-fy) + h10*fx*(1-fy) + h01*(1-fx)*fy + h11*fx*fy;
// Only consider water that's above queryZ but not too far above
if (h < queryZ - 2.0f) continue; // water below camera, skip
if (h > queryZ + maxAbove) continue; // water way above camera, skip
float dist = std::abs(h - queryZ);
if (!best || dist < bestDist) {
best = h;
bestDist = dist;
}
}
return best;
}
std::optional<uint16_t> WaterRenderer::getWaterTypeAt(float glX, float glY) const {
std::optional<float> bestHeight;
std::optional<uint16_t> bestType;
for (const auto& surface : surfaces) {
glm::vec2 rel(glX - surface.origin.x, glY - surface.origin.y);
glm::vec2 sX(surface.stepX.x, surface.stepX.y);
glm::vec2 sY(surface.stepY.x, surface.stepY.y);
float lenSqX = glm::dot(sX, sX);
float lenSqY = glm::dot(sY, sY);
if (lenSqX < 1e-6f || lenSqY < 1e-6f) continue;
float gx = glm::dot(rel, sX) / lenSqX;
float gy = glm::dot(rel, sY) / lenSqY;
if (gx < 0.0f || gx > static_cast<float>(surface.width) ||
gy < 0.0f || gy > static_cast<float>(surface.height)) continue;
int ix = static_cast<int>(gx);
int iy = static_cast<int>(gy);
if (ix >= surface.width) ix = surface.width - 1;
if (iy >= surface.height) iy = surface.height - 1;
if (ix < 0 || iy < 0) continue;
if (!surface.mask.empty()) {
int tileIndex;
if (surface.wmoId == 0 && surface.width <= 8 && surface.mask.size() >= 8) {
tileIndex = (static_cast<int>(surface.yOffset) + iy) * 8 +
(static_cast<int>(surface.xOffset) + ix);
} else {
tileIndex = iy * surface.width + ix;
}
int byteIndex = tileIndex / 8;
int bitIndex = tileIndex % 8;
if (byteIndex < static_cast<int>(surface.mask.size())) {
uint8_t maskByte = surface.mask[byteIndex];
bool renderTile;
if (surface.wmoId == 0 && surface.width > 8) {
renderTile = (maskByte & (1 << bitIndex)) != 0;
} else {
renderTile = (maskByte & (1 << bitIndex)) || (maskByte & (1 << (7 - bitIndex)));
}
if (!renderTile) continue;
}
}
float h = surface.minHeight;
if (!bestHeight || h > *bestHeight) {
bestHeight = h;
bestType = surface.liquidType;
}
}
return bestType;
}
bool WaterRenderer::isWmoWaterAt(float glX, float glY) const {
for (const auto& surface : surfaces) {
if (surface.wmoId == 0) continue;
glm::vec2 rel(glX - surface.origin.x, glY - surface.origin.y);
glm::vec2 sX(surface.stepX.x, surface.stepX.y);
glm::vec2 sY(surface.stepY.x, surface.stepY.y);
float lenSqX = glm::dot(sX, sX);
float lenSqY = glm::dot(sY, sY);
if (lenSqX < 1e-6f || lenSqY < 1e-6f) continue;
float gx = glm::dot(rel, sX) / lenSqX;
float gy = glm::dot(rel, sY) / lenSqY;
if (gx >= 0.0f && gx <= static_cast<float>(surface.width) &&
gy >= 0.0f && gy <= static_cast<float>(surface.height))
return true;
}
return false;
}
glm::vec4 WaterRenderer::getLiquidColor(uint16_t liquidType) const {
uint8_t basicType = (liquidType == 0) ? 0 : ((liquidType - 1) % 4);
switch (basicType) {
case 0: return glm::vec4(0.10f, 0.28f, 0.55f, 1.0f); // inland: richer blue
case 1: return glm::vec4(0.04f, 0.16f, 0.38f, 1.0f); // ocean: deep blue
case 2: return glm::vec4(0.9f, 0.3f, 0.05f, 1.0f); // magma
case 3: return glm::vec4(0.2f, 0.6f, 0.1f, 1.0f); // slime
default: return glm::vec4(0.10f, 0.28f, 0.55f, 1.0f);
}
}
float WaterRenderer::getLiquidAlpha(uint16_t liquidType) const {
uint8_t basicType = (liquidType == 0) ? 0 : ((liquidType - 1) % 4);
switch (basicType) {
case 1: return 0.72f; // ocean
case 2: return 0.75f; // magma
case 3: return 0.65f; // slime
default: return 0.48f; // inland water
}
}
// ==============================================================
// Planar reflection resources
// ==============================================================
void WaterRenderer::createReflectionResources() {
if (!vkCtx) return;
VkDevice device = vkCtx->getDevice();
VmaAllocator allocator = vkCtx->getAllocator();
// --- Reflection color image ---
VkImageCreateInfo colorImgCI{};
colorImgCI.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO;
colorImgCI.imageType = VK_IMAGE_TYPE_2D;
colorImgCI.format = vkCtx->getSwapchainFormat();
colorImgCI.extent = {REFLECTION_WIDTH, REFLECTION_HEIGHT, 1};
colorImgCI.mipLevels = 1;
colorImgCI.arrayLayers = 1;
colorImgCI.samples = VK_SAMPLE_COUNT_1_BIT;
colorImgCI.tiling = VK_IMAGE_TILING_OPTIMAL;
colorImgCI.usage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_SAMPLED_BIT;
colorImgCI.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
VmaAllocationCreateInfo allocCI{};
allocCI.usage = VMA_MEMORY_USAGE_GPU_ONLY;
if (vmaCreateImage(allocator, &colorImgCI, &allocCI,
&reflectionColorImage, &reflectionColorAlloc, nullptr) != VK_SUCCESS) {
LOG_ERROR("WaterRenderer: failed to create reflection color image");
return;
}
VkImageViewCreateInfo colorViewCI{};
colorViewCI.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO;
colorViewCI.image = reflectionColorImage;
colorViewCI.viewType = VK_IMAGE_VIEW_TYPE_2D;
colorViewCI.format = vkCtx->getSwapchainFormat();
colorViewCI.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1};
if (vkCreateImageView(device, &colorViewCI, nullptr, &reflectionColorView) != VK_SUCCESS) {
LOG_ERROR("WaterRenderer: failed to create reflection color view");
return;
}
// --- Reflection depth image ---
VkImageCreateInfo depthImgCI{};
depthImgCI.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO;
depthImgCI.imageType = VK_IMAGE_TYPE_2D;
depthImgCI.format = vkCtx->getDepthFormat();
depthImgCI.extent = {REFLECTION_WIDTH, REFLECTION_HEIGHT, 1};
depthImgCI.mipLevels = 1;
depthImgCI.arrayLayers = 1;
depthImgCI.samples = VK_SAMPLE_COUNT_1_BIT;
depthImgCI.tiling = VK_IMAGE_TILING_OPTIMAL;
depthImgCI.usage = VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT;
depthImgCI.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
if (vmaCreateImage(allocator, &depthImgCI, &allocCI,
&reflectionDepthImage, &reflectionDepthAlloc, nullptr) != VK_SUCCESS) {
LOG_ERROR("WaterRenderer: failed to create reflection depth image");
return;
}
VkImageViewCreateInfo depthViewCI{};
depthViewCI.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO;
depthViewCI.image = reflectionDepthImage;
depthViewCI.viewType = VK_IMAGE_VIEW_TYPE_2D;
depthViewCI.format = vkCtx->getDepthFormat();
depthViewCI.subresourceRange = {VK_IMAGE_ASPECT_DEPTH_BIT, 0, 1, 0, 1};
if (vkCreateImageView(device, &depthViewCI, nullptr, &reflectionDepthView) != VK_SUCCESS) {
LOG_ERROR("WaterRenderer: failed to create reflection depth view");
return;
}
// --- Reflection sampler ---
VkSamplerCreateInfo sampCI{};
sampCI.sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO;
sampCI.magFilter = VK_FILTER_LINEAR;
sampCI.minFilter = VK_FILTER_LINEAR;
sampCI.addressModeU = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
sampCI.addressModeV = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
sampCI.addressModeW = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
if (vkCreateSampler(device, &sampCI, nullptr, &reflectionSampler) != VK_SUCCESS) {
LOG_ERROR("WaterRenderer: failed to create reflection sampler");
return;
}
// --- Reflection render pass ---
VkAttachmentDescription colorAttach{};
colorAttach.format = vkCtx->getSwapchainFormat();
colorAttach.samples = VK_SAMPLE_COUNT_1_BIT;
colorAttach.loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR;
colorAttach.storeOp = VK_ATTACHMENT_STORE_OP_STORE;
colorAttach.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE;
colorAttach.stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE;
colorAttach.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
colorAttach.finalLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
VkAttachmentDescription depthAttach{};
depthAttach.format = vkCtx->getDepthFormat();
depthAttach.samples = VK_SAMPLE_COUNT_1_BIT;
depthAttach.loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR;
depthAttach.storeOp = VK_ATTACHMENT_STORE_OP_DONT_CARE;
depthAttach.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE;
depthAttach.stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE;
depthAttach.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
depthAttach.finalLayout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL;
VkAttachmentReference colorRef{0, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL};
VkAttachmentReference depthRef{1, VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL};
VkSubpassDescription subpass{};
subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS;
subpass.colorAttachmentCount = 1;
subpass.pColorAttachments = &colorRef;
subpass.pDepthStencilAttachment = &depthRef;
VkSubpassDependency dep{};
dep.srcSubpass = VK_SUBPASS_EXTERNAL;
dep.dstSubpass = 0;
dep.srcStageMask = VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT;
dep.dstStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT | VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT;
dep.srcAccessMask = 0;
dep.dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT;
std::array<VkAttachmentDescription, 2> attachments = {colorAttach, depthAttach};
VkRenderPassCreateInfo rpCI{};
rpCI.sType = VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO;
rpCI.attachmentCount = static_cast<uint32_t>(attachments.size());
rpCI.pAttachments = attachments.data();
rpCI.subpassCount = 1;
rpCI.pSubpasses = &subpass;
rpCI.dependencyCount = 1;
rpCI.pDependencies = &dep;
if (vkCreateRenderPass(device, &rpCI, nullptr, &reflectionRenderPass) != VK_SUCCESS) {
LOG_ERROR("WaterRenderer: failed to create reflection render pass");
return;
}
// --- Reflection framebuffer ---
std::array<VkImageView, 2> fbAttach = {reflectionColorView, reflectionDepthView};
VkFramebufferCreateInfo fbCI{};
fbCI.sType = VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO;
fbCI.renderPass = reflectionRenderPass;
fbCI.attachmentCount = static_cast<uint32_t>(fbAttach.size());
fbCI.pAttachments = fbAttach.data();
fbCI.width = REFLECTION_WIDTH;
fbCI.height = REFLECTION_HEIGHT;
fbCI.layers = 1;
if (vkCreateFramebuffer(device, &fbCI, nullptr, &reflectionFramebuffer) != VK_SUCCESS) {
LOG_ERROR("WaterRenderer: failed to create reflection framebuffer");
return;
}
// --- Reflection UBO ---
VkBufferCreateInfo bufCI{};
bufCI.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO;
bufCI.size = sizeof(ReflectionUBOData);
bufCI.usage = VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT;
VmaAllocationCreateInfo uboAllocCI{};
uboAllocCI.usage = VMA_MEMORY_USAGE_CPU_TO_GPU;
uboAllocCI.flags = VMA_ALLOCATION_CREATE_MAPPED_BIT;
VmaAllocationInfo mapInfo{};
if (vmaCreateBuffer(allocator, &bufCI, &uboAllocCI,
&reflectionUBO, &reflectionUBOAlloc, &mapInfo) != VK_SUCCESS) {
LOG_ERROR("WaterRenderer: failed to create reflection UBO");
return;
}
reflectionUBOMapped = mapInfo.pMappedData;
// Initialize with identity
ReflectionUBOData initData{};
initData.reflViewProj = glm::mat4(1.0f);
if (reflectionUBOMapped) {
std::memcpy(reflectionUBOMapped, &initData, sizeof(initData));
}
// Transition reflection color image to shader-read so first frame doesn't read undefined
vkCtx->immediateSubmit([&](VkCommandBuffer cmd) {
VkImageMemoryBarrier barrier{};
barrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER;
barrier.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED;
barrier.newLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
barrier.image = reflectionColorImage;
barrier.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1};
barrier.dstAccessMask = VK_ACCESS_SHADER_READ_BIT;
vkCmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT,
VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT,
0, 0, nullptr, 0, nullptr, 1, &barrier);
});
reflectionColorLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
LOG_INFO("Water reflection resources created (", REFLECTION_WIDTH, "x", REFLECTION_HEIGHT, ")");
}
void WaterRenderer::destroyReflectionResources() {
if (!vkCtx) return;
VkDevice device = vkCtx->getDevice();
VmaAllocator allocator = vkCtx->getAllocator();
if (reflectionFramebuffer) { vkDestroyFramebuffer(device, reflectionFramebuffer, nullptr); reflectionFramebuffer = VK_NULL_HANDLE; }
if (reflectionRenderPass) { vkDestroyRenderPass(device, reflectionRenderPass, nullptr); reflectionRenderPass = VK_NULL_HANDLE; }
if (reflectionColorView) { vkDestroyImageView(device, reflectionColorView, nullptr); reflectionColorView = VK_NULL_HANDLE; }
if (reflectionDepthView) { vkDestroyImageView(device, reflectionDepthView, nullptr); reflectionDepthView = VK_NULL_HANDLE; }
if (reflectionColorImage) { vmaDestroyImage(allocator, reflectionColorImage, reflectionColorAlloc); reflectionColorImage = VK_NULL_HANDLE; }
if (reflectionDepthImage) { vmaDestroyImage(allocator, reflectionDepthImage, reflectionDepthAlloc); reflectionDepthImage = VK_NULL_HANDLE; }
if (reflectionSampler) { vkDestroySampler(device, reflectionSampler, nullptr); reflectionSampler = VK_NULL_HANDLE; }
if (reflectionUBO) {
AllocatedBuffer ab{}; ab.buffer = reflectionUBO; ab.allocation = reflectionUBOAlloc;
destroyBuffer(allocator, ab);
reflectionUBO = VK_NULL_HANDLE;
reflectionUBOMapped = nullptr;
}
reflectionColorLayout = VK_IMAGE_LAYOUT_UNDEFINED;
}
// ==============================================================
// Reflection pass begin/end
// ==============================================================
bool WaterRenderer::beginReflectionPass(VkCommandBuffer cmd) {
if (!reflectionRenderPass || !reflectionFramebuffer || !cmd) return false;
VkRenderPassBeginInfo rpInfo{};
rpInfo.sType = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO;
rpInfo.renderPass = reflectionRenderPass;
rpInfo.framebuffer = reflectionFramebuffer;
rpInfo.renderArea = {{0, 0}, {REFLECTION_WIDTH, REFLECTION_HEIGHT}};
VkClearValue clears[2]{};
clears[0].color = {{0.0f, 0.0f, 0.0f, 1.0f}};
clears[1].depthStencil = {1.0f, 0};
rpInfo.clearValueCount = 2;
rpInfo.pClearValues = clears;
vkCmdBeginRenderPass(cmd, &rpInfo, VK_SUBPASS_CONTENTS_INLINE);
VkViewport vp{0, 0, static_cast<float>(REFLECTION_WIDTH), static_cast<float>(REFLECTION_HEIGHT), 0.0f, 1.0f};
vkCmdSetViewport(cmd, 0, 1, &vp);
VkRect2D sc{{0, 0}, {REFLECTION_WIDTH, REFLECTION_HEIGHT}};
vkCmdSetScissor(cmd, 0, 1, &sc);
return true;
}
void WaterRenderer::endReflectionPass(VkCommandBuffer cmd) {
if (!cmd) return;
vkCmdEndRenderPass(cmd);
reflectionColorLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
// Update all per-frame scene descriptor sets with the freshly rendered reflection texture
if (reflectionColorView && reflectionSampler) {
VkDescriptorImageInfo reflInfo{};
reflInfo.sampler = reflectionSampler;
reflInfo.imageView = reflectionColorView;
reflInfo.imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
std::vector<VkWriteDescriptorSet> writes;
for (uint32_t f = 0; f < SCENE_HISTORY_FRAMES; f++) {
if (!sceneHistory[f].sceneSet) continue;
VkWriteDescriptorSet write{};
write.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
write.dstSet = sceneHistory[f].sceneSet;
write.dstBinding = 2;
write.descriptorCount = 1;
write.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
write.pImageInfo = &reflInfo;
writes.push_back(write);
}
if (!writes.empty()) {
vkUpdateDescriptorSets(vkCtx->getDevice(), static_cast<uint32_t>(writes.size()), writes.data(), 0, nullptr);
}
}
}
void WaterRenderer::updateReflectionUBO(const glm::mat4& reflViewProj) {
if (!reflectionUBOMapped) return;
ReflectionUBOData data{};
data.reflViewProj = reflViewProj;
std::memcpy(reflectionUBOMapped, &data, sizeof(data));
}
// ==============================================================
// Mirror camera computations
// ==============================================================
std::optional<float> WaterRenderer::getDominantWaterHeight(const glm::vec3& cameraPos) const {
if (surfaces.empty()) return std::nullopt;
// Find the water surface closest to the camera (XY distance)
float bestDist = std::numeric_limits<float>::max();
float bestHeight = 0.0f;
bool found = false;
for (const auto& surface : surfaces) {
// Skip magma/slime — only reflect water/ocean
uint8_t basicType = (surface.liquidType == 0) ? 0 : ((surface.liquidType - 1) % 4);
if (basicType >= 2) continue;
// Compute center of surface in world space
glm::vec3 center = surface.origin +
surface.stepX * (static_cast<float>(surface.width) * 0.5f) +
surface.stepY * (static_cast<float>(surface.height) * 0.5f);
float dx = cameraPos.x - center.x;
float dy = cameraPos.y - center.y;
float dist = dx * dx + dy * dy;
if (dist < bestDist) {
bestDist = dist;
bestHeight = surface.minHeight;
found = true;
}
}
if (!found) return std::nullopt;
return bestHeight;
}
glm::mat4 WaterRenderer::computeReflectedView(const Camera& camera, float waterHeight) {
// In this engine, Z is up. Water height is stored in the Z component.
// Mirror camera position across Z = waterHeight plane.
glm::vec3 camPos = camera.getPosition();
glm::vec3 reflPos = camPos;
reflPos.z = 2.0f * waterHeight - camPos.z;
// Get camera forward and reflect the Z component
glm::vec3 forward = camera.getForward();
forward.z = -forward.z;
glm::vec3 reflTarget = reflPos + forward;
glm::vec3 up(0.0f, 0.0f, 1.0f);
return glm::lookAt(reflPos, reflTarget, up);
}
glm::mat4 WaterRenderer::computeObliqueProjection(const glm::mat4& proj, const glm::mat4& view,
float waterHeight) {
// Clip plane: everything below waterHeight in world space
// Z is up, so the clip plane normal is (0, 0, 1)
glm::vec4 clipPlaneWorld(0.0f, 0.0f, 1.0f, -waterHeight);
glm::vec4 clipPlaneView = glm::transpose(glm::inverse(view)) * clipPlaneWorld;
// Lengyel's oblique near-plane projection matrix modification
glm::mat4 result = proj;
glm::vec4 q;
q.x = (glm::sign(clipPlaneView.x) + result[2][0]) / result[0][0];
q.y = (glm::sign(clipPlaneView.y) + result[2][1]) / result[1][1];
q.z = -1.0f;
q.w = (1.0f + result[2][2]) / result[3][2];
glm::vec4 c = clipPlaneView * (2.0f / glm::dot(clipPlaneView, q));
result[0][2] = c.x;
result[1][2] = c.y;
result[2][2] = c.z + 1.0f;
result[3][2] = c.w;
return result;
}
// ==============================================================
// Separate 1x water pass (used when MSAA is active)
// ==============================================================
bool WaterRenderer::createWater1xPass(VkFormat colorFormat, VkFormat depthFormat) {
if (!vkCtx) return false;
VkDevice device = vkCtx->getDevice();
VkAttachmentDescription attachments[2]{};
// Color: load existing resolved content, store after water draw
attachments[0].format = colorFormat;
attachments[0].samples = VK_SAMPLE_COUNT_1_BIT;
attachments[0].loadOp = VK_ATTACHMENT_LOAD_OP_LOAD;
attachments[0].storeOp = VK_ATTACHMENT_STORE_OP_STORE;
attachments[0].stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE;
attachments[0].stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE;
attachments[0].initialLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR;
attachments[0].finalLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR;
// Depth: load resolved depth for depth testing
attachments[1].format = depthFormat;
attachments[1].samples = VK_SAMPLE_COUNT_1_BIT;
attachments[1].loadOp = VK_ATTACHMENT_LOAD_OP_LOAD;
attachments[1].storeOp = VK_ATTACHMENT_STORE_OP_DONT_CARE;
attachments[1].stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE;
attachments[1].stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE;
attachments[1].initialLayout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL;
attachments[1].finalLayout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL;
VkAttachmentReference colorRef{0, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL};
VkAttachmentReference depthRef{1, VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL};
VkSubpassDescription subpass{};
subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS;
subpass.colorAttachmentCount = 1;
subpass.pColorAttachments = &colorRef;
subpass.pDepthStencilAttachment = &depthRef;
VkSubpassDependency dep{};
dep.srcSubpass = VK_SUBPASS_EXTERNAL;
dep.dstSubpass = 0;
dep.srcStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT | VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT;
dep.dstStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT | VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT;
dep.srcAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT;
dep.dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT |
VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_READ_BIT;
VkRenderPassCreateInfo rpCI{};
rpCI.sType = VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO;
rpCI.attachmentCount = 2;
rpCI.pAttachments = attachments;
rpCI.subpassCount = 1;
rpCI.pSubpasses = &subpass;
rpCI.dependencyCount = 1;
rpCI.pDependencies = &dep;
if (vkCreateRenderPass(device, &rpCI, nullptr, &water1xRenderPass) != VK_SUCCESS) {
LOG_ERROR("WaterRenderer: failed to create 1x water render pass");
return false;
}
// Build 1x water pipeline against this render pass
VkShaderModule vertShader, fragShader;
if (!vertShader.loadFromFile(device, "assets/shaders/water.vert.spv") ||
!fragShader.loadFromFile(device, "assets/shaders/water.frag.spv")) {
LOG_ERROR("WaterRenderer: failed to load shaders for 1x pipeline");
return false;
}
VkVertexInputBindingDescription vertBinding{};
vertBinding.binding = 0;
vertBinding.stride = 8 * sizeof(float);
vertBinding.inputRate = VK_VERTEX_INPUT_RATE_VERTEX;
std::vector<VkVertexInputAttributeDescription> vertAttribs = {
{ 0, 0, VK_FORMAT_R32G32B32_SFLOAT, 0 },
{ 1, 0, VK_FORMAT_R32G32_SFLOAT, 6 * sizeof(float) },
};
water1xPipeline = PipelineBuilder()
.setShaders(vertShader.stageInfo(VK_SHADER_STAGE_VERTEX_BIT),
fragShader.stageInfo(VK_SHADER_STAGE_FRAGMENT_BIT))
.setVertexInput({ vertBinding }, vertAttribs)
.setTopology(VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST)
.setRasterization(VK_POLYGON_MODE_FILL, VK_CULL_MODE_NONE)
.setDepthTest(true, false, VK_COMPARE_OP_LESS_OR_EQUAL)
.setColorBlendAttachment(PipelineBuilder::blendAlpha())
.setMultisample(VK_SAMPLE_COUNT_1_BIT)
.setLayout(pipelineLayout)
.setRenderPass(water1xRenderPass)
.setDynamicStates({ VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR })
.build(device);
vertShader.destroy();
fragShader.destroy();
if (!water1xPipeline) {
LOG_ERROR("WaterRenderer: failed to create 1x water pipeline");
return false;
}
LOG_INFO("WaterRenderer: created 1x water pass and pipeline");
return true;
}
void WaterRenderer::createWater1xFramebuffers(const std::vector<VkImageView>& swapViews,
VkImageView depthView, VkExtent2D extent) {
if (!vkCtx || !water1xRenderPass || !depthView) return;
VkDevice device = vkCtx->getDevice();
// Destroy old framebuffers
for (auto fb : water1xFramebuffers) {
if (fb) vkDestroyFramebuffer(device, fb, nullptr);
}
water1xFramebuffers.clear();
water1xFramebuffers.resize(swapViews.size());
for (size_t i = 0; i < swapViews.size(); i++) {
VkImageView views[2] = { swapViews[i], depthView };
VkFramebufferCreateInfo fbCI{};
fbCI.sType = VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO;
fbCI.renderPass = water1xRenderPass;
fbCI.attachmentCount = 2;
fbCI.pAttachments = views;
fbCI.width = extent.width;
fbCI.height = extent.height;
fbCI.layers = 1;
if (vkCreateFramebuffer(device, &fbCI, nullptr, &water1xFramebuffers[i]) != VK_SUCCESS) {
LOG_ERROR("WaterRenderer: failed to create 1x framebuffer ", i);
}
}
}
void WaterRenderer::destroyWater1xResources() {
if (!vkCtx) return;
VkDevice device = vkCtx->getDevice();
for (auto fb : water1xFramebuffers) {
if (fb) vkDestroyFramebuffer(device, fb, nullptr);
}
water1xFramebuffers.clear();
if (water1xPipeline) { vkDestroyPipeline(device, water1xPipeline, nullptr); water1xPipeline = VK_NULL_HANDLE; }
if (water1xRenderPass) { vkDestroyRenderPass(device, water1xRenderPass, nullptr); water1xRenderPass = VK_NULL_HANDLE; }
}
bool WaterRenderer::beginWater1xPass(VkCommandBuffer cmd, uint32_t imageIndex, VkExtent2D extent) {
if (!water1xRenderPass || imageIndex >= water1xFramebuffers.size() || !water1xFramebuffers[imageIndex])
return false;
VkRenderPassBeginInfo rpBI{};
rpBI.sType = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO;
rpBI.renderPass = water1xRenderPass;
rpBI.framebuffer = water1xFramebuffers[imageIndex];
rpBI.renderArea = {{0, 0}, extent};
rpBI.clearValueCount = 0;
rpBI.pClearValues = nullptr;
vkCmdBeginRenderPass(cmd, &rpBI, VK_SUBPASS_CONTENTS_INLINE);
VkViewport vp{0, 0, static_cast<float>(extent.width), static_cast<float>(extent.height), 0.0f, 1.0f};
vkCmdSetViewport(cmd, 0, 1, &vp);
VkRect2D sc{{0, 0}, extent};
vkCmdSetScissor(cmd, 0, 1, &sc);
return true;
}
void WaterRenderer::endWater1xPass(VkCommandBuffer cmd) {
vkCmdEndRenderPass(cmd);
}
} // namespace rendering
} // namespace wowee