#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 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 _pad; }; 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.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 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; sceneSetLayout = createDescriptorSetLayout(device, {sceneColorBinding, sceneDepthBinding}); if (!sceneSetLayout) { LOG_ERROR("WaterRenderer: failed to create scene set layout"); return false; } VkDescriptorPoolSize scenePoolSize{}; scenePoolSize.type = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; scenePoolSize.descriptorCount = 2; VkDescriptorPoolCreateInfo scenePoolInfo{}; scenePoolInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO; scenePoolInfo.maxSets = 1; scenePoolInfo.poolSizeCount = 1; scenePoolInfo.pPoolSizes = &scenePoolSize; 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; 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; } 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(); 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::shutdown() { clear(); if (!vkCtx) return; VkDevice device = vkCtx->getDevice(); vkDeviceWaitIdle(device); 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(); if (sceneColorView) { vkDestroyImageView(device, sceneColorView, nullptr); sceneColorView = VK_NULL_HANDLE; } if (sceneDepthView) { vkDestroyImageView(device, sceneDepthView, nullptr); sceneDepthView = VK_NULL_HANDLE; } if (sceneColorImage) { vmaDestroyImage(vkCtx->getAllocator(), sceneColorImage, sceneColorAlloc); sceneColorImage = VK_NULL_HANDLE; sceneColorAlloc = VK_NULL_HANDLE; } if (sceneDepthImage) { vmaDestroyImage(vkCtx->getAllocator(), sceneDepthImage, sceneDepthAlloc); sceneDepthImage = VK_NULL_HANDLE; sceneDepthAlloc = VK_NULL_HANDLE; } if (sceneColorSampler) { vkDestroySampler(device, sceneColorSampler, nullptr); sceneColorSampler = VK_NULL_HANDLE; } if (sceneDepthSampler) { vkDestroySampler(device, sceneDepthSampler, nullptr); sceneDepthSampler = VK_NULL_HANDLE; } sceneSet = 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; 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; VmaAllocationCreateInfo allocCI{}; allocCI.usage = VMA_MEMORY_USAGE_GPU_ONLY; if (vmaCreateImage(vkCtx->getAllocator(), &colorImgInfo, &allocCI, &sceneColorImage, &sceneColorAlloc, nullptr) != VK_SUCCESS) { LOG_ERROR("WaterRenderer: failed to create scene color history image"); return; } VkImageCreateInfo depthImgInfo = colorImgInfo; depthImgInfo.format = depthFormat; if (vmaCreateImage(vkCtx->getAllocator(), &depthImgInfo, &allocCI, &sceneDepthImage, &sceneDepthAlloc, nullptr) != VK_SUCCESS) { LOG_ERROR("WaterRenderer: failed to create scene depth history image"); return; } VkImageViewCreateInfo colorViewInfo{}; colorViewInfo.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO; colorViewInfo.image = sceneColorImage; 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, &sceneColorView) != VK_SUCCESS) { LOG_ERROR("WaterRenderer: failed to create scene color history view"); return; } VkImageViewCreateInfo depthViewInfo = colorViewInfo; depthViewInfo.image = sceneDepthImage; depthViewInfo.format = depthFormat; depthViewInfo.subresourceRange.aspectMask = VK_IMAGE_ASPECT_DEPTH_BIT; if (vkCreateImageView(device, &depthViewInfo, nullptr, &sceneDepthView) != VK_SUCCESS) { LOG_ERROR("WaterRenderer: failed to create scene depth history view"); return; } 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; } VkDescriptorSetAllocateInfo ai{}; ai.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO; ai.descriptorPool = sceneDescPool; ai.descriptorSetCount = 1; ai.pSetLayouts = &sceneSetLayout; if (vkAllocateDescriptorSets(device, &ai, &sceneSet) != VK_SUCCESS) { LOG_ERROR("WaterRenderer: failed to allocate scene descriptor set"); sceneSet = VK_NULL_HANDLE; return; } VkDescriptorImageInfo colorInfo{}; colorInfo.sampler = sceneColorSampler; colorInfo.imageView = sceneColorView; colorInfo.imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; VkDescriptorImageInfo depthInfo{}; depthInfo.sampler = sceneDepthSampler; depthInfo.imageView = sceneDepthView; depthInfo.imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; std::array writes{}; writes[0].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; writes[0].dstSet = sceneSet; writes[0].dstBinding = 0; writes[0].descriptorCount = 1; writes[0].descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; writes[0].pImageInfo = &colorInfo; writes[1].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; writes[1].dstSet = sceneSet; writes[1].dstBinding = 1; writes[1].descriptorCount = 1; writes[1].descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; writes[1].pImageInfo = &depthInfo; vkUpdateDescriptorSets(device, static_cast(writes.size()), writes.data(), 0, nullptr); // Initialize history images to shader-read layout so first frame samples are defined. vkCtx->immediateSubmit([&](VkCommandBuffer cmd) { VkImageMemoryBarrier barriers[2]{}; barriers[0].sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER; barriers[0].oldLayout = VK_IMAGE_LAYOUT_UNDEFINED; barriers[0].newLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; barriers[0].srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; barriers[0].dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; barriers[0].image = sceneColorImage; barriers[0].subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}; barriers[0].dstAccessMask = VK_ACCESS_SHADER_READ_BIT; barriers[1] = barriers[0]; barriers[1].image = sceneDepthImage; barriers[1].subresourceRange.aspectMask = VK_IMAGE_ASPECT_DEPTH_BIT; vkCmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, 0, 0, nullptr, 0, nullptr, 2, barriers); }); } 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 || basicType == 3) { color = glm::vec4(0.2f, 0.4f, 0.6f, 1.0f); alpha = 0.45f; } } 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); } } // ============================================================== // 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(); } int totalLayers = 0; 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; const auto& terrainChunk = terrain.getChunk(chunkX, chunkY); for (const auto& layer : chunkWater.layers) { WaterSurface surface; surface.position = glm::vec3( terrainChunk.position[0], terrainChunk.position[1], layer.minHeight ); surface.origin = glm::vec3( surface.position.x - (static_cast(layer.y) * TILE_SIZE), surface.position.y - (static_cast(layer.x) * TILE_SIZE), layer.minHeight ); surface.stepX = glm::vec3(0.0f, -TILE_SIZE, 0.0f); surface.stepY = glm::vec3(-TILE_SIZE, 0.0f, 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); // Stormwind water lowering bool isStormwindArea = (tileX >= 28 && tileX <= 50 && tileY >= 28 && tileY <= 52); if (isStormwindArea && layer.minHeight > 94.0f) { float tileWorldX = (32.0f - tileX) * 533.33333f; float tileWorldY = (32.0f - tileY) * 533.33333f; glm::vec3 moonwellPos(-8755.9f, 1108.9f, 96.1f); float distToMoonwell = glm::distance(glm::vec2(tileWorldX, tileWorldY), glm::vec2(moonwellPos.x, moonwellPos.y)); 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)); totalLayers++; } } LOG_DEBUG("Loaded ", totalLayers, " water layers from MH2O data"); } 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("Removed ", removed, " water surfaces for tile [", tileX, ",", tileY, "]"); } } 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; surface.heights.assign(vertexCount, surface.origin.z); surface.minHeight = surface.origin.z; surface.maxHeight = surface.origin.z; // Stormwind WMO water lowering int tilePosX = static_cast(std::floor((32.0f - surface.origin.x / 533.33333f))); int tilePosY = static_cast(std::floor((32.0f - surface.origin.y / 533.33333f))); bool isStormwindArea = (tilePosX >= 28 && tilePosX <= 50 && tilePosY >= 28 && tilePosY <= 52); if (isStormwindArea && surface.origin.z > 94.0f) { glm::vec3 moonwellPos(-8755.9f, 1108.9f, 96.1f); float distToMoonwell = glm::distance(glm::vec2(surface.origin.x, surface.origin.y), glm::vec2(moonwellPos.x, moonwellPos.y)); if (distToMoonwell > 20.0f) { for (float& h : surface.heights) h -= 1.0f; surface.minHeight -= 1.0f; surface.maxHeight -= 1.0f; } } if (surface.origin.z > 300.0f || surface.origin.z < -100.0f) return; size_t tileCount = static_cast(surface.width) * static_cast(surface.height); size_t maskBytes = (tileCount + 7) / 8; surface.mask.assign(maskBytes, 0xFF); createWaterMesh(surface); 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*/) { if (!renderingEnabled || surfaces.empty() || !waterPipeline) return; if (!sceneSet) return; vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, waterPipeline); vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, pipelineLayout, 0, 1, &perFrameSet, 0, nullptr); vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, pipelineLayout, 2, 1, &sceneSet, 0, nullptr); for (const auto& surface : surfaces) { if (surface.vertexBuffer == VK_NULL_HANDLE || surface.indexCount == 0) continue; if (!surface.materialSet) continue; bool canalProfile = (surface.wmoId != 0) || (surface.liquidType == 5); float waveAmp = canalProfile ? 0.04f : 0.06f; float waveFreq = canalProfile ? 0.30f : 0.22f; float waveSpeed = canalProfile ? 1.20f : 2.00f; WaterPushConstants push{}; push.model = glm::mat4(1.0f); push.waveAmp = waveAmp; push.waveFreq = waveFreq; push.waveSpeed = waveSpeed; vkCmdPushConstants(cmd, pipelineLayout, VK_SHADER_STAGE_VERTEX_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) { if (!vkCtx || !cmd || !sceneColorImage || !sceneDepthImage || 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(sceneColorImage, 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, sceneColorImage, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, &colorCopy); barrier2(sceneColorImage, 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(sceneDepthImage, 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, sceneDepthImage, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, &depthCopy); barrier2(sceneDepthImage, 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; if (surface.wmoId == 0 && 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]; 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.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 ((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; } 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.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 = (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::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.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 = (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; } 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.2f, 0.4f, 0.6f, 1.0f); case 1: return glm::vec4(0.06f, 0.18f, 0.34f, 1.0f); case 2: return glm::vec4(0.9f, 0.3f, 0.05f, 1.0f); case 3: return glm::vec4(0.2f, 0.6f, 0.1f, 1.0f); default: return glm::vec4(0.2f, 0.4f, 0.6f, 1.0f); } } float WaterRenderer::getLiquidAlpha(uint16_t liquidType) const { uint8_t basicType = (liquidType == 0) ? 0 : ((liquidType - 1) % 4); switch (basicType) { case 1: return 0.68f; case 2: return 0.72f; case 3: return 0.62f; default: return 0.38f; } } } // namespace rendering } // namespace wowee