#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 #include #include #include #include #include #include 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 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(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 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 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 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 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(writes.size()), writes.data(), 0, nullptr); } // Initialize all per-frame history images to shader-read layout vkCtx->immediateSubmit([&](VkCommandBuffer cmd) { std::vector 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(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(k.liquidType) << 32) | uint32_t(k.roundedHeight)); } }; std::unordered_map, 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(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(layer.x) * TILE_SIZE), surface.position.y - (static_cast(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(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(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(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(std::min(255, liquid.xTiles)); surface.height = static_cast(std::min(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(surface.width); float spanY = stepYLen * static_cast(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(surface.width) + 1; const int gridHeight = static_cast(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(surface.width) * static_cast(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(t) % surface.width; int ty = static_cast(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(tx) + 0.5f) + surface.stepY * (static_cast(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(t) % surface.width; int aty = static_cast(t) / surface.width; glm::vec3 tw = surface.origin + surface.stepX * (static_cast(atx) + 0.5f) + surface.stepY * (static_cast(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(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(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 vertices; std::vector 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(surface.heights.size())) ? surface.heights[index] : surface.minHeight; glm::vec3 pos = surface.origin + surface.stepX * static_cast(x) + surface.stepY * static_cast(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(x) / std::max(1, gridWidth - 1)); vertices.push_back(static_cast(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(surface.xOffset) + x; int cy = static_cast(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(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(surface.yOffset) + ny) * 8 + (static_cast(surface.xOffset) + nx); } else { neighborIdx = ny * surface.width + nx; } int nByteIdx = neighborIdx / 8; int nBitIdx = neighborIdx % 8; if (nByteIdx < static_cast(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(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 WaterRenderer::getWaterHeightAt(float glX, float glY) const { std::optional 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(surface.width) || gy < 0.0f || gy > static_cast(surface.height)) continue; int gridWidth = surface.width + 1; int ix = static_cast(gx); int iy = static_cast(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(surface.yOffset) + iy) * 8 + (static_cast(surface.xOffset) + ix); } else { tileIndex = iy * surface.width + ix; } int byteIndex = tileIndex / 8; int bitIndex = tileIndex % 8; if (byteIndex < static_cast(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(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 WaterRenderer::getNearestWaterHeightAt(float glX, float glY, float queryZ, float maxAbove) const { std::optional 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(surface.width) || gy < 0.0f || gy > static_cast(surface.height)) continue; int gridWidth = surface.width + 1; int ix = static_cast(gx); int iy = static_cast(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(surface.yOffset) + iy) * 8 + (static_cast(surface.xOffset) + ix); } else { tileIndex = iy * surface.width + ix; } int byteIndex = tileIndex / 8; int bitIndex = tileIndex % 8; if (byteIndex < static_cast(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(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 WaterRenderer::getWaterTypeAt(float glX, float glY) const { std::optional bestHeight; std::optional 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(surface.width) || gy < 0.0f || gy > static_cast(surface.height)) continue; int ix = static_cast(gx); int iy = static_cast(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(surface.yOffset) + iy) * 8 + (static_cast(surface.xOffset) + ix); } else { tileIndex = iy * surface.width + ix; } int byteIndex = tileIndex / 8; int bitIndex = tileIndex % 8; if (byteIndex < static_cast(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(surface.width) && gy >= 0.0f && gy <= static_cast(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 attachments = {colorAttach, depthAttach}; VkRenderPassCreateInfo rpCI{}; rpCI.sType = VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO; rpCI.attachmentCount = static_cast(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 fbAttach = {reflectionColorView, reflectionDepthView}; VkFramebufferCreateInfo fbCI{}; fbCI.sType = VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO; fbCI.renderPass = reflectionRenderPass; fbCI.attachmentCount = static_cast(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(REFLECTION_WIDTH), static_cast(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 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(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 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::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(surface.width) * 0.5f) + surface.stepY * (static_cast(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 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& 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(extent.width), static_cast(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