2026-02-02 12:24:50 -08:00
|
|
|
#include "rendering/water_renderer.hpp"
|
2026-02-21 19:41:21 -08:00
|
|
|
#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"
|
2026-02-02 12:24:50 -08:00
|
|
|
#include "rendering/camera.hpp"
|
|
|
|
|
#include "pipeline/adt_loader.hpp"
|
2026-02-03 13:33:31 -08:00
|
|
|
#include "pipeline/wmo_loader.hpp"
|
2026-02-02 12:24:50 -08:00
|
|
|
#include "core/logger.hpp"
|
|
|
|
|
#include <glm/gtc/matrix_transform.hpp>
|
2026-02-03 20:40:59 -08:00
|
|
|
#include <algorithm>
|
2026-02-02 12:24:50 -08:00
|
|
|
#include <cmath>
|
2026-02-21 19:41:21 -08:00
|
|
|
#include <cstring>
|
2026-02-03 20:40:59 -08:00
|
|
|
#include <limits>
|
2026-02-22 09:34:27 -08:00
|
|
|
#include <array>
|
2026-02-02 12:24:50 -08:00
|
|
|
|
|
|
|
|
namespace wowee {
|
|
|
|
|
namespace rendering {
|
|
|
|
|
|
2026-02-21 19:41:21 -08:00
|
|
|
// 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;
|
|
|
|
|
};
|
|
|
|
|
|
2026-02-02 12:24:50 -08:00
|
|
|
WaterRenderer::WaterRenderer() = default;
|
|
|
|
|
|
|
|
|
|
WaterRenderer::~WaterRenderer() {
|
|
|
|
|
shutdown();
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-21 19:41:21 -08:00
|
|
|
bool WaterRenderer::initialize(VkContext* ctx, VkDescriptorSetLayout perFrameLayout) {
|
|
|
|
|
vkCtx = ctx;
|
|
|
|
|
if (!vkCtx) return false;
|
2026-02-02 12:24:50 -08:00
|
|
|
|
2026-02-21 19:41:21 -08:00
|
|
|
LOG_INFO("Initializing water renderer (Vulkan)");
|
|
|
|
|
VkDevice device = vkCtx->getDevice();
|
2026-02-02 12:24:50 -08:00
|
|
|
|
2026-02-21 19:41:21 -08:00
|
|
|
// --- 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;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-22 09:34:27 -08:00
|
|
|
// --- 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;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-21 19:41:21 -08:00
|
|
|
// --- Pipeline layout ---
|
|
|
|
|
VkPushConstantRange pushRange{};
|
|
|
|
|
pushRange.stageFlags = VK_SHADER_STAGE_VERTEX_BIT;
|
|
|
|
|
pushRange.offset = 0;
|
|
|
|
|
pushRange.size = sizeof(WaterPushConstants);
|
|
|
|
|
|
2026-02-22 09:34:27 -08:00
|
|
|
std::vector<VkDescriptorSetLayout> setLayouts = { perFrameLayout, materialSetLayout, sceneSetLayout };
|
2026-02-21 19:41:21 -08:00
|
|
|
pipelineLayout = createPipelineLayout(device, setLayouts, { pushRange });
|
|
|
|
|
if (!pipelineLayout) {
|
|
|
|
|
LOG_ERROR("WaterRenderer: failed to create pipeline layout");
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-22 09:34:27 -08:00
|
|
|
createSceneHistoryResources(vkCtx->getSwapchainExtent(),
|
|
|
|
|
vkCtx->getSwapchainFormat(),
|
|
|
|
|
vkCtx->getDepthFormat());
|
|
|
|
|
|
2026-02-21 19:41:21 -08:00
|
|
|
// --- 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;
|
|
|
|
|
}
|
2026-02-02 12:24:50 -08:00
|
|
|
|
2026-02-21 19:41:21 -08:00
|
|
|
// --- Vertex input (interleaved: pos3 + normal3 + uv2 = 8 floats = 32 bytes) ---
|
|
|
|
|
VkVertexInputBindingDescription vertBinding{};
|
|
|
|
|
vertBinding.binding = 0;
|
|
|
|
|
vertBinding.stride = 8 * sizeof(float);
|
|
|
|
|
vertBinding.inputRate = VK_VERTEX_INPUT_RATE_VERTEX;
|
|
|
|
|
|
|
|
|
|
// Water vertex shader only takes aPos(vec3) at loc 0 and aTexCoord(vec2) at loc 1
|
|
|
|
|
// (normal is computed in shader from wave derivatives)
|
|
|
|
|
std::vector<VkVertexInputAttributeDescription> vertAttribs = {
|
|
|
|
|
{ 0, 0, VK_FORMAT_R32G32B32_SFLOAT, 0 }, // aPos
|
|
|
|
|
{ 1, 0, VK_FORMAT_R32G32_SFLOAT, 6 * sizeof(float) }, // aTexCoord (skip normal)
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
VkRenderPass mainPass = vkCtx->getImGuiRenderPass();
|
|
|
|
|
|
|
|
|
|
waterPipeline = PipelineBuilder()
|
|
|
|
|
.setShaders(vertShader.stageInfo(VK_SHADER_STAGE_VERTEX_BIT),
|
|
|
|
|
fragShader.stageInfo(VK_SHADER_STAGE_FRAGMENT_BIT))
|
|
|
|
|
.setVertexInput({ vertBinding }, vertAttribs)
|
|
|
|
|
.setTopology(VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST)
|
|
|
|
|
.setRasterization(VK_POLYGON_MODE_FILL, VK_CULL_MODE_NONE)
|
|
|
|
|
.setDepthTest(true, false, VK_COMPARE_OP_LESS_OR_EQUAL) // depth test yes, write no
|
|
|
|
|
.setColorBlendAttachment(PipelineBuilder::blendAlpha())
|
2026-02-22 02:59:24 -08:00
|
|
|
.setMultisample(vkCtx->getMsaaSamples())
|
2026-02-21 19:41:21 -08:00
|
|
|
.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");
|
2026-02-02 12:24:50 -08:00
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-21 19:41:21 -08:00
|
|
|
LOG_INFO("Water renderer initialized (Vulkan)");
|
2026-02-02 12:24:50 -08:00
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-22 02:59:24 -08:00
|
|
|
void WaterRenderer::recreatePipelines() {
|
|
|
|
|
if (!vkCtx) return;
|
|
|
|
|
VkDevice device = vkCtx->getDevice();
|
|
|
|
|
|
2026-02-22 09:34:27 -08:00
|
|
|
createSceneHistoryResources(vkCtx->getSwapchainExtent(),
|
|
|
|
|
vkCtx->getSwapchainFormat(),
|
|
|
|
|
vkCtx->getDepthFormat());
|
|
|
|
|
|
2026-02-22 02:59:24 -08:00
|
|
|
// Destroy old pipeline (keep layout)
|
|
|
|
|
if (waterPipeline) { vkDestroyPipeline(device, waterPipeline, nullptr); waterPipeline = VK_NULL_HANDLE; }
|
|
|
|
|
|
|
|
|
|
// Load shaders
|
|
|
|
|
VkShaderModule vertShader, fragShader;
|
|
|
|
|
if (!vertShader.loadFromFile(device, "assets/shaders/water.vert.spv")) {
|
|
|
|
|
LOG_ERROR("WaterRenderer::recreatePipelines: failed to load vertex shader");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (!fragShader.loadFromFile(device, "assets/shaders/water.frag.spv")) {
|
|
|
|
|
LOG_ERROR("WaterRenderer::recreatePipelines: failed to load fragment shader");
|
|
|
|
|
vertShader.destroy();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Vertex input (same as initialize)
|
|
|
|
|
VkVertexInputBindingDescription vertBinding{};
|
|
|
|
|
vertBinding.binding = 0;
|
|
|
|
|
vertBinding.stride = 8 * sizeof(float);
|
|
|
|
|
vertBinding.inputRate = VK_VERTEX_INPUT_RATE_VERTEX;
|
|
|
|
|
|
|
|
|
|
std::vector<VkVertexInputAttributeDescription> vertAttribs = {
|
|
|
|
|
{ 0, 0, VK_FORMAT_R32G32B32_SFLOAT, 0 },
|
|
|
|
|
{ 1, 0, VK_FORMAT_R32G32_SFLOAT, 6 * sizeof(float) },
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
VkRenderPass mainPass = vkCtx->getImGuiRenderPass();
|
|
|
|
|
|
|
|
|
|
waterPipeline = PipelineBuilder()
|
|
|
|
|
.setShaders(vertShader.stageInfo(VK_SHADER_STAGE_VERTEX_BIT),
|
|
|
|
|
fragShader.stageInfo(VK_SHADER_STAGE_FRAGMENT_BIT))
|
|
|
|
|
.setVertexInput({ vertBinding }, vertAttribs)
|
|
|
|
|
.setTopology(VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST)
|
|
|
|
|
.setRasterization(VK_POLYGON_MODE_FILL, VK_CULL_MODE_NONE)
|
|
|
|
|
.setDepthTest(true, false, VK_COMPARE_OP_LESS_OR_EQUAL)
|
|
|
|
|
.setColorBlendAttachment(PipelineBuilder::blendAlpha())
|
|
|
|
|
.setMultisample(vkCtx->getMsaaSamples())
|
|
|
|
|
.setLayout(pipelineLayout)
|
|
|
|
|
.setRenderPass(mainPass)
|
|
|
|
|
.setDynamicStates({ VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR })
|
|
|
|
|
.build(device);
|
|
|
|
|
|
|
|
|
|
vertShader.destroy();
|
|
|
|
|
fragShader.destroy();
|
|
|
|
|
|
|
|
|
|
if (!waterPipeline) {
|
|
|
|
|
LOG_ERROR("WaterRenderer::recreatePipelines: failed to create pipeline");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-02 12:24:50 -08:00
|
|
|
void WaterRenderer::shutdown() {
|
|
|
|
|
clear();
|
2026-02-21 19:41:21 -08:00
|
|
|
|
|
|
|
|
if (!vkCtx) return;
|
|
|
|
|
VkDevice device = vkCtx->getDevice();
|
|
|
|
|
vkDeviceWaitIdle(device);
|
|
|
|
|
|
2026-02-22 09:34:27 -08:00
|
|
|
destroySceneHistoryResources();
|
2026-02-21 19:41:21 -08:00
|
|
|
if (waterPipeline) { vkDestroyPipeline(device, waterPipeline, nullptr); waterPipeline = VK_NULL_HANDLE; }
|
|
|
|
|
if (pipelineLayout) { vkDestroyPipelineLayout(device, pipelineLayout, nullptr); pipelineLayout = VK_NULL_HANDLE; }
|
2026-02-22 09:34:27 -08:00
|
|
|
if (sceneDescPool) { vkDestroyDescriptorPool(device, sceneDescPool, nullptr); sceneDescPool = VK_NULL_HANDLE; }
|
|
|
|
|
if (sceneSetLayout) { vkDestroyDescriptorSetLayout(device, sceneSetLayout, nullptr); sceneSetLayout = VK_NULL_HANDLE; }
|
2026-02-21 19:41:21 -08:00
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-22 09:34:27 -08:00
|
|
|
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<VkWriteDescriptorSet, 2> 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<uint32_t>(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);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-21 19:41:21 -08:00
|
|
|
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);
|
|
|
|
|
}
|
2026-02-02 12:24:50 -08:00
|
|
|
}
|
|
|
|
|
|
2026-02-21 19:41:21 -08:00
|
|
|
// ==============================================================
|
|
|
|
|
// Data loading (preserved from GL version — no GL calls)
|
|
|
|
|
// ==============================================================
|
|
|
|
|
|
2026-02-02 12:24:50 -08:00
|
|
|
void WaterRenderer::loadFromTerrain(const pipeline::ADTTerrain& terrain, bool append,
|
|
|
|
|
int tileX, int tileY) {
|
2026-02-03 20:40:59 -08:00
|
|
|
constexpr float TILE_SIZE = 33.33333f / 8.0f;
|
|
|
|
|
|
2026-02-02 12:24:50 -08:00
|
|
|
if (!append) {
|
|
|
|
|
clear();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
int totalLayers = 0;
|
|
|
|
|
|
|
|
|
|
for (int chunkIdx = 0; chunkIdx < 256; chunkIdx++) {
|
|
|
|
|
const auto& chunkWater = terrain.waterData[chunkIdx];
|
2026-02-21 19:41:21 -08:00
|
|
|
if (!chunkWater.hasWater()) continue;
|
2026-02-02 12:24:50 -08:00
|
|
|
|
|
|
|
|
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
|
|
|
|
|
);
|
2026-02-03 20:40:59 -08:00
|
|
|
surface.origin = glm::vec3(
|
|
|
|
|
surface.position.x - (static_cast<float>(layer.y) * TILE_SIZE),
|
|
|
|
|
surface.position.y - (static_cast<float>(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);
|
2026-02-02 12:24:50 -08:00
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
|
2026-02-03 20:40:59 -08:00
|
|
|
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) {
|
2026-02-21 19:41:21 -08:00
|
|
|
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; }
|
2026-02-03 20:40:59 -08:00
|
|
|
}
|
2026-02-21 19:41:21 -08:00
|
|
|
if (sane) { useFlat = false; surface.heights = layer.heights; }
|
2026-02-03 20:40:59 -08:00
|
|
|
}
|
2026-02-21 19:41:21 -08:00
|
|
|
if (useFlat) surface.heights.resize(numVertices, layer.minHeight);
|
2026-02-02 12:24:50 -08:00
|
|
|
|
2026-02-21 19:41:21 -08:00
|
|
|
// Stormwind water lowering
|
2026-02-09 21:24:34 -08:00
|
|
|
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));
|
2026-02-21 19:41:21 -08:00
|
|
|
if (distToMoonwell > 300.0f) {
|
|
|
|
|
for (float& h : surface.heights) h -= 1.0f;
|
2026-02-09 21:24:34 -08:00
|
|
|
surface.minHeight -= 1.0f;
|
|
|
|
|
surface.maxHeight -= 1.0f;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-02 12:24:50 -08:00
|
|
|
surface.mask = layer.mask;
|
|
|
|
|
surface.tileX = tileX;
|
|
|
|
|
surface.tileY = tileY;
|
2026-02-21 19:41:21 -08:00
|
|
|
|
2026-02-02 12:24:50 -08:00
|
|
|
createWaterMesh(surface);
|
2026-02-21 19:41:21 -08:00
|
|
|
if (surface.indexCount > 0 && vkCtx) {
|
|
|
|
|
updateMaterialUBO(surface);
|
|
|
|
|
}
|
|
|
|
|
surfaces.push_back(std::move(surface));
|
2026-02-02 12:24:50 -08:00
|
|
|
totalLayers++;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-11 22:27:02 -08:00
|
|
|
LOG_DEBUG("Loaded ", totalLayers, " water layers from MH2O data");
|
2026-02-02 12:24:50 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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, "]");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-03 13:33:31 -08:00
|
|
|
void WaterRenderer::loadFromWMO([[maybe_unused]] const pipeline::WMOLiquid& liquid,
|
|
|
|
|
[[maybe_unused]] const glm::mat4& modelMatrix,
|
|
|
|
|
[[maybe_unused]] uint32_t wmoId) {
|
2026-02-21 19:41:21 -08:00
|
|
|
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;
|
2026-02-03 20:40:59 -08:00
|
|
|
|
|
|
|
|
WaterSurface surface;
|
|
|
|
|
surface.tileX = -1;
|
|
|
|
|
surface.tileY = -1;
|
|
|
|
|
surface.wmoId = wmoId;
|
|
|
|
|
surface.liquidType = liquid.materialId;
|
|
|
|
|
surface.xOffset = 0;
|
|
|
|
|
surface.yOffset = 0;
|
|
|
|
|
surface.width = static_cast<uint8_t>(std::min<uint32_t>(255, liquid.xTiles));
|
|
|
|
|
surface.height = static_cast<uint8_t>(std::min<uint32_t>(255, liquid.yTiles));
|
|
|
|
|
|
|
|
|
|
constexpr float WMO_LIQUID_TILE_SIZE = 4.1666625f;
|
|
|
|
|
const glm::vec3 localBase(liquid.basePosition.x, liquid.basePosition.y, liquid.basePosition.z);
|
|
|
|
|
const glm::vec3 localStepX(WMO_LIQUID_TILE_SIZE, 0.0f, 0.0f);
|
|
|
|
|
const glm::vec3 localStepY(0.0f, WMO_LIQUID_TILE_SIZE, 0.0f);
|
|
|
|
|
|
|
|
|
|
surface.origin = glm::vec3(modelMatrix * glm::vec4(localBase, 1.0f));
|
|
|
|
|
surface.stepX = glm::vec3(modelMatrix * glm::vec4(localStepX, 0.0f));
|
|
|
|
|
surface.stepY = glm::vec3(modelMatrix * glm::vec4(localStepY, 0.0f));
|
|
|
|
|
surface.position = surface.origin;
|
2026-02-21 19:41:21 -08:00
|
|
|
|
2026-02-03 21:30:59 -08:00
|
|
|
float stepXLen = glm::length(surface.stepX);
|
|
|
|
|
float stepYLen = glm::length(surface.stepY);
|
|
|
|
|
glm::vec3 planeN = glm::cross(surface.stepX, surface.stepY);
|
|
|
|
|
float nz = (glm::length(planeN) > 1e-4f) ? std::abs(glm::normalize(planeN).z) : 0.0f;
|
|
|
|
|
float spanX = stepXLen * static_cast<float>(surface.width);
|
|
|
|
|
float spanY = stepYLen * static_cast<float>(surface.height);
|
|
|
|
|
if (stepXLen < 0.2f || stepXLen > 12.0f ||
|
|
|
|
|
stepYLen < 0.2f || stepYLen > 12.0f ||
|
2026-02-21 19:41:21 -08:00
|
|
|
nz < 0.60f || spanX > 450.0f || spanY > 450.0f) return;
|
2026-02-03 20:40:59 -08:00
|
|
|
|
|
|
|
|
const int gridWidth = static_cast<int>(surface.width) + 1;
|
|
|
|
|
const int gridHeight = static_cast<int>(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;
|
|
|
|
|
|
2026-02-21 19:41:21 -08:00
|
|
|
// Stormwind WMO water lowering
|
|
|
|
|
int tilePosX = static_cast<int>(std::floor((32.0f - surface.origin.x / 533.33333f)));
|
|
|
|
|
int tilePosY = static_cast<int>(std::floor((32.0f - surface.origin.y / 533.33333f)));
|
|
|
|
|
bool isStormwindArea = (tilePosX >= 28 && tilePosX <= 50 && tilePosY >= 28 && tilePosY <= 52);
|
2026-02-09 21:24:34 -08:00
|
|
|
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) {
|
2026-02-21 19:41:21 -08:00
|
|
|
for (float& h : surface.heights) h -= 1.0f;
|
2026-02-09 21:24:34 -08:00
|
|
|
surface.minHeight -= 1.0f;
|
|
|
|
|
surface.maxHeight -= 1.0f;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-21 19:41:21 -08:00
|
|
|
if (surface.origin.z > 300.0f || surface.origin.z < -100.0f) return;
|
2026-02-09 20:29:47 -08:00
|
|
|
|
2026-02-03 20:40:59 -08:00
|
|
|
size_t tileCount = static_cast<size_t>(surface.width) * static_cast<size_t>(surface.height);
|
|
|
|
|
size_t maskBytes = (tileCount + 7) / 8;
|
|
|
|
|
surface.mask.assign(maskBytes, 0xFF);
|
|
|
|
|
|
|
|
|
|
createWaterMesh(surface);
|
|
|
|
|
if (surface.indexCount > 0) {
|
2026-02-21 19:41:21 -08:00
|
|
|
if (vkCtx) updateMaterialUBO(surface);
|
|
|
|
|
surfaces.push_back(std::move(surface));
|
2026-02-03 20:40:59 -08:00
|
|
|
}
|
2026-02-03 13:33:31 -08:00
|
|
|
}
|
|
|
|
|
|
2026-02-03 20:40:59 -08:00
|
|
|
void WaterRenderer::removeWMO(uint32_t wmoId) {
|
2026-02-21 19:41:21 -08:00
|
|
|
if (wmoId == 0) return;
|
2026-02-03 20:40:59 -08:00
|
|
|
auto it = surfaces.begin();
|
|
|
|
|
while (it != surfaces.end()) {
|
|
|
|
|
if (it->wmoId == wmoId) {
|
|
|
|
|
destroyWaterMesh(*it);
|
|
|
|
|
it = surfaces.erase(it);
|
|
|
|
|
} else {
|
|
|
|
|
++it;
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-02-03 13:33:31 -08:00
|
|
|
}
|
|
|
|
|
|
2026-02-02 12:24:50 -08:00
|
|
|
void WaterRenderer::clear() {
|
|
|
|
|
for (auto& surface : surfaces) {
|
|
|
|
|
destroyWaterMesh(surface);
|
|
|
|
|
}
|
|
|
|
|
surfaces.clear();
|
|
|
|
|
|
2026-02-21 19:41:21 -08:00
|
|
|
if (vkCtx && materialDescPool) {
|
|
|
|
|
vkResetDescriptorPool(vkCtx->getDevice(), materialDescPool, 0);
|
2026-02-02 12:24:50 -08:00
|
|
|
}
|
2026-02-21 19:41:21 -08:00
|
|
|
}
|
2026-02-02 12:24:50 -08:00
|
|
|
|
2026-02-21 19:41:21 -08:00
|
|
|
// ==============================================================
|
|
|
|
|
// Rendering
|
|
|
|
|
// ==============================================================
|
2026-02-03 20:40:59 -08:00
|
|
|
|
2026-02-21 19:41:21 -08:00
|
|
|
void WaterRenderer::render(VkCommandBuffer cmd, VkDescriptorSet perFrameSet,
|
|
|
|
|
const Camera& /*camera*/, float /*time*/) {
|
|
|
|
|
if (!renderingEnabled || surfaces.empty() || !waterPipeline) return;
|
2026-02-22 09:34:27 -08:00
|
|
|
if (!sceneSet) return;
|
2026-02-02 12:24:50 -08:00
|
|
|
|
2026-02-21 19:41:21 -08:00
|
|
|
vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, waterPipeline);
|
2026-02-02 12:24:50 -08:00
|
|
|
|
2026-02-21 19:41:21 -08:00
|
|
|
vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, pipelineLayout,
|
|
|
|
|
0, 1, &perFrameSet, 0, nullptr);
|
2026-02-22 09:34:27 -08:00
|
|
|
vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, pipelineLayout,
|
|
|
|
|
2, 1, &sceneSet, 0, nullptr);
|
2026-02-02 12:24:50 -08:00
|
|
|
|
2026-02-21 19:41:21 -08:00
|
|
|
for (const auto& surface : surfaces) {
|
|
|
|
|
if (surface.vertexBuffer == VK_NULL_HANDLE || surface.indexCount == 0) continue;
|
|
|
|
|
if (!surface.materialSet) continue;
|
2026-02-02 12:24:50 -08:00
|
|
|
|
2026-02-21 19:41:21 -08:00
|
|
|
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;
|
2026-02-02 12:24:50 -08:00
|
|
|
|
2026-02-21 19:41:21 -08:00
|
|
|
WaterPushConstants push{};
|
|
|
|
|
push.model = glm::mat4(1.0f);
|
|
|
|
|
push.waveAmp = waveAmp;
|
|
|
|
|
push.waveFreq = waveFreq;
|
|
|
|
|
push.waveSpeed = waveSpeed;
|
2026-02-02 12:24:50 -08:00
|
|
|
|
2026-02-21 19:41:21 -08:00
|
|
|
vkCmdPushConstants(cmd, pipelineLayout, VK_SHADER_STAGE_VERTEX_BIT,
|
|
|
|
|
0, sizeof(WaterPushConstants), &push);
|
2026-02-12 00:45:24 -08:00
|
|
|
|
2026-02-21 19:41:21 -08:00
|
|
|
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);
|
2026-02-02 12:24:50 -08:00
|
|
|
|
2026-02-21 19:41:21 -08:00
|
|
|
vkCmdDrawIndexed(cmd, static_cast<uint32_t>(surface.indexCount), 1, 0, 0, 0);
|
|
|
|
|
}
|
2026-02-02 12:24:50 -08:00
|
|
|
}
|
|
|
|
|
|
2026-02-22 09:34:27 -08:00
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-21 19:41:21 -08:00
|
|
|
// ==============================================================
|
|
|
|
|
// Mesh creation (Vulkan upload instead of GL)
|
|
|
|
|
// ==============================================================
|
|
|
|
|
|
2026-02-02 12:24:50 -08:00
|
|
|
void WaterRenderer::createWaterMesh(WaterSurface& surface) {
|
2026-02-21 19:41:21 -08:00
|
|
|
const int gridWidth = surface.width + 1;
|
2026-02-02 12:24:50 -08:00
|
|
|
const int gridHeight = surface.height + 1;
|
2026-02-21 19:41:21 -08:00
|
|
|
constexpr float VISUAL_WATER_Z_BIAS = 0.02f;
|
2026-02-02 12:24:50 -08:00
|
|
|
|
|
|
|
|
std::vector<float> vertices;
|
|
|
|
|
std::vector<uint32_t> indices;
|
|
|
|
|
|
|
|
|
|
for (int y = 0; y < gridHeight; y++) {
|
|
|
|
|
for (int x = 0; x < gridWidth; x++) {
|
|
|
|
|
int index = y * gridWidth + x;
|
2026-02-21 19:41:21 -08:00
|
|
|
float height = (index < static_cast<int>(surface.heights.size()))
|
|
|
|
|
? surface.heights[index] : surface.minHeight;
|
2026-02-02 12:24:50 -08:00
|
|
|
|
2026-02-03 20:40:59 -08:00
|
|
|
glm::vec3 pos = surface.origin +
|
|
|
|
|
surface.stepX * static_cast<float>(x) +
|
|
|
|
|
surface.stepY * static_cast<float>(y);
|
|
|
|
|
pos.z = height + VISUAL_WATER_Z_BIAS;
|
2026-02-02 12:24:50 -08:00
|
|
|
|
2026-02-21 19:41:21 -08:00
|
|
|
// pos (3 floats)
|
2026-02-03 20:40:59 -08:00
|
|
|
vertices.push_back(pos.x);
|
|
|
|
|
vertices.push_back(pos.y);
|
|
|
|
|
vertices.push_back(pos.z);
|
2026-02-21 19:41:21 -08:00
|
|
|
// normal (3 floats) - up
|
2026-02-02 12:24:50 -08:00
|
|
|
vertices.push_back(0.0f);
|
|
|
|
|
vertices.push_back(0.0f);
|
|
|
|
|
vertices.push_back(1.0f);
|
2026-02-21 19:41:21 -08:00
|
|
|
// texcoord (2 floats)
|
2026-02-02 12:24:50 -08:00
|
|
|
vertices.push_back(static_cast<float>(x) / std::max(1, gridWidth - 1));
|
|
|
|
|
vertices.push_back(static_cast<float>(y) / std::max(1, gridHeight - 1));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-21 19:41:21 -08:00
|
|
|
// Generate indices respecting render mask (same logic as GL version)
|
2026-02-02 12:24:50 -08:00
|
|
|
for (int y = 0; y < gridHeight - 1; y++) {
|
|
|
|
|
for (int x = 0; x < gridWidth - 1; x++) {
|
|
|
|
|
bool renderTile = true;
|
|
|
|
|
if (!surface.mask.empty()) {
|
2026-02-03 21:11:10 -08:00
|
|
|
int tileIndex;
|
|
|
|
|
if (surface.wmoId == 0 && surface.mask.size() >= 8) {
|
|
|
|
|
int cx = static_cast<int>(surface.xOffset) + x;
|
|
|
|
|
int cy = static_cast<int>(surface.yOffset) + y;
|
|
|
|
|
tileIndex = cy * 8 + cx;
|
|
|
|
|
} else {
|
|
|
|
|
tileIndex = y * surface.width + x;
|
|
|
|
|
}
|
2026-02-02 12:24:50 -08:00
|
|
|
int byteIndex = tileIndex / 8;
|
|
|
|
|
int bitIndex = tileIndex % 8;
|
|
|
|
|
if (byteIndex < static_cast<int>(surface.mask.size())) {
|
2026-02-03 21:11:10 -08:00
|
|
|
uint8_t maskByte = surface.mask[byteIndex];
|
|
|
|
|
bool lsbOrder = (maskByte & (1 << bitIndex)) != 0;
|
|
|
|
|
bool msbOrder = (maskByte & (1 << (7 - bitIndex))) != 0;
|
|
|
|
|
renderTile = lsbOrder || msbOrder;
|
2026-02-08 22:27:11 -08:00
|
|
|
|
2026-02-11 00:54:38 -08:00
|
|
|
if (!renderTile) {
|
2026-02-08 22:27:11 -08:00
|
|
|
for (int dy = -1; dy <= 1; dy++) {
|
|
|
|
|
for (int dx = -1; dx <= 1; dx++) {
|
|
|
|
|
if (dx == 0 && dy == 0) continue;
|
2026-02-21 19:41:21 -08:00
|
|
|
int nx = x + dx, ny = y + dy;
|
2026-02-11 00:54:38 -08:00
|
|
|
if (nx < 0 || ny < 0 || nx >= gridWidth-1 || ny >= gridHeight-1) continue;
|
|
|
|
|
int neighborIdx;
|
|
|
|
|
if (surface.wmoId == 0 && surface.mask.size() >= 8) {
|
2026-02-21 19:41:21 -08:00
|
|
|
neighborIdx = (static_cast<int>(surface.yOffset) + ny) * 8 +
|
|
|
|
|
(static_cast<int>(surface.xOffset) + nx);
|
2026-02-11 00:54:38 -08:00
|
|
|
} else {
|
|
|
|
|
neighborIdx = ny * surface.width + nx;
|
|
|
|
|
}
|
2026-02-08 22:27:11 -08:00
|
|
|
int nByteIdx = neighborIdx / 8;
|
|
|
|
|
int nBitIdx = neighborIdx % 8;
|
|
|
|
|
if (nByteIdx < static_cast<int>(surface.mask.size())) {
|
|
|
|
|
uint8_t nMask = surface.mask[nByteIdx];
|
|
|
|
|
if ((nMask & (1 << nBitIdx)) || (nMask & (1 << (7 - nBitIdx)))) {
|
|
|
|
|
renderTile = true;
|
|
|
|
|
goto found_neighbor;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
found_neighbor:;
|
|
|
|
|
}
|
2026-02-02 12:24:50 -08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-21 19:41:21 -08:00
|
|
|
if (!renderTile) continue;
|
2026-02-02 12:24:50 -08:00
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-21 19:41:21 -08:00
|
|
|
// Fallback: if terrain MH2O mask produced no tiles, render full rect
|
2026-02-03 21:30:59 -08:00
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-02-02 12:24:50 -08:00
|
|
|
}
|
|
|
|
|
|
2026-02-03 21:30:59 -08:00
|
|
|
if (indices.empty()) return;
|
2026-02-02 12:24:50 -08:00
|
|
|
surface.indexCount = static_cast<int>(indices.size());
|
|
|
|
|
|
2026-02-21 19:41:21 -08:00
|
|
|
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;
|
2026-02-02 12:24:50 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void WaterRenderer::destroyWaterMesh(WaterSurface& surface) {
|
2026-02-21 19:41:21 -08:00
|
|
|
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;
|
2026-02-02 12:24:50 -08:00
|
|
|
}
|
2026-02-21 19:41:21 -08:00
|
|
|
if (surface.indexBuffer) {
|
|
|
|
|
AllocatedBuffer ab{}; ab.buffer = surface.indexBuffer; ab.allocation = surface.indexAlloc;
|
|
|
|
|
destroyBuffer(allocator, ab);
|
|
|
|
|
surface.indexBuffer = VK_NULL_HANDLE;
|
2026-02-02 12:24:50 -08:00
|
|
|
}
|
2026-02-21 19:41:21 -08:00
|
|
|
if (surface.materialUBO) {
|
|
|
|
|
AllocatedBuffer ab{}; ab.buffer = surface.materialUBO; ab.allocation = surface.materialAlloc;
|
|
|
|
|
destroyBuffer(allocator, ab);
|
|
|
|
|
surface.materialUBO = VK_NULL_HANDLE;
|
2026-02-02 12:24:50 -08:00
|
|
|
}
|
2026-02-21 19:41:21 -08:00
|
|
|
surface.materialSet = VK_NULL_HANDLE;
|
2026-02-02 12:24:50 -08:00
|
|
|
}
|
|
|
|
|
|
2026-02-21 19:41:21 -08:00
|
|
|
// ==============================================================
|
|
|
|
|
// Query functions (data-only, no GL)
|
|
|
|
|
// ==============================================================
|
|
|
|
|
|
2026-02-02 12:24:50 -08:00
|
|
|
std::optional<float> WaterRenderer::getWaterHeightAt(float glX, float glY) const {
|
|
|
|
|
std::optional<float> best;
|
|
|
|
|
|
2026-02-21 19:41:21 -08:00
|
|
|
for (const auto& surface : surfaces) {
|
2026-02-03 20:40:59 -08:00
|
|
|
glm::vec2 rel(glX - surface.origin.x, glY - surface.origin.y);
|
2026-02-21 19:41:21 -08:00
|
|
|
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;
|
2026-02-02 12:24:50 -08:00
|
|
|
|
|
|
|
|
if (gx < 0.0f || gx > static_cast<float>(surface.width) ||
|
2026-02-21 19:41:21 -08:00
|
|
|
gy < 0.0f || gy > static_cast<float>(surface.height)) continue;
|
2026-02-02 12:24:50 -08:00
|
|
|
|
|
|
|
|
int gridWidth = surface.width + 1;
|
|
|
|
|
int ix = static_cast<int>(gx);
|
|
|
|
|
int iy = static_cast<int>(gy);
|
|
|
|
|
float fx = gx - ix;
|
|
|
|
|
float fy = gy - iy;
|
|
|
|
|
|
|
|
|
|
if (ix >= surface.width) { ix = surface.width - 1; fx = 1.0f; }
|
|
|
|
|
if (iy >= surface.height) { iy = surface.height - 1; fy = 1.0f; }
|
2026-02-21 19:41:21 -08:00
|
|
|
if (ix < 0 || iy < 0) continue;
|
2026-02-03 20:40:59 -08:00
|
|
|
|
|
|
|
|
if (!surface.mask.empty()) {
|
2026-02-03 21:11:10 -08:00
|
|
|
int tileIndex;
|
|
|
|
|
if (surface.wmoId == 0 && surface.mask.size() >= 8) {
|
2026-02-21 19:41:21 -08:00
|
|
|
tileIndex = (static_cast<int>(surface.yOffset) + iy) * 8 +
|
|
|
|
|
(static_cast<int>(surface.xOffset) + ix);
|
2026-02-03 21:11:10 -08:00
|
|
|
} else {
|
|
|
|
|
tileIndex = iy * surface.width + ix;
|
|
|
|
|
}
|
2026-02-03 20:40:59 -08:00
|
|
|
int byteIndex = tileIndex / 8;
|
|
|
|
|
int bitIndex = tileIndex % 8;
|
|
|
|
|
if (byteIndex < static_cast<int>(surface.mask.size())) {
|
2026-02-03 21:11:10 -08:00
|
|
|
uint8_t maskByte = surface.mask[byteIndex];
|
2026-02-21 19:41:21 -08:00
|
|
|
bool renderTile = (maskByte & (1 << bitIndex)) || (maskByte & (1 << (7 - bitIndex)));
|
|
|
|
|
if (!renderTile) continue;
|
2026-02-03 20:40:59 -08:00
|
|
|
}
|
|
|
|
|
}
|
2026-02-02 12:24:50 -08:00
|
|
|
|
|
|
|
|
int idx00 = iy * gridWidth + ix;
|
|
|
|
|
int idx10 = idx00 + 1;
|
|
|
|
|
int idx01 = idx00 + gridWidth;
|
|
|
|
|
int idx11 = idx01 + 1;
|
|
|
|
|
|
|
|
|
|
int total = static_cast<int>(surface.heights.size());
|
|
|
|
|
if (idx11 >= total) continue;
|
|
|
|
|
|
2026-02-21 19:41:21 -08:00
|
|
|
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;
|
2026-02-02 12:24:50 -08:00
|
|
|
|
2026-02-21 19:41:21 -08:00
|
|
|
if (!best || h > *best) best = h;
|
2026-02-02 12:24:50 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return best;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-03 20:40:59 -08:00
|
|
|
std::optional<uint16_t> WaterRenderer::getWaterTypeAt(float glX, float glY) const {
|
|
|
|
|
std::optional<float> bestHeight;
|
|
|
|
|
std::optional<uint16_t> bestType;
|
|
|
|
|
|
|
|
|
|
for (const auto& surface : surfaces) {
|
|
|
|
|
glm::vec2 rel(glX - surface.origin.x, glY - surface.origin.y);
|
2026-02-21 19:41:21 -08:00
|
|
|
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;
|
2026-02-03 20:40:59 -08:00
|
|
|
if (gx < 0.0f || gx > static_cast<float>(surface.width) ||
|
2026-02-21 19:41:21 -08:00
|
|
|
gy < 0.0f || gy > static_cast<float>(surface.height)) continue;
|
2026-02-03 20:40:59 -08:00
|
|
|
|
|
|
|
|
int ix = static_cast<int>(gx);
|
|
|
|
|
int iy = static_cast<int>(gy);
|
|
|
|
|
if (ix >= surface.width) ix = surface.width - 1;
|
|
|
|
|
if (iy >= surface.height) iy = surface.height - 1;
|
|
|
|
|
if (ix < 0 || iy < 0) continue;
|
|
|
|
|
|
|
|
|
|
if (!surface.mask.empty()) {
|
2026-02-03 21:30:59 -08:00
|
|
|
int tileIndex;
|
|
|
|
|
if (surface.wmoId == 0 && surface.mask.size() >= 8) {
|
2026-02-21 19:41:21 -08:00
|
|
|
tileIndex = (static_cast<int>(surface.yOffset) + iy) * 8 +
|
|
|
|
|
(static_cast<int>(surface.xOffset) + ix);
|
2026-02-03 21:30:59 -08:00
|
|
|
} else {
|
|
|
|
|
tileIndex = iy * surface.width + ix;
|
|
|
|
|
}
|
2026-02-03 20:40:59 -08:00
|
|
|
int byteIndex = tileIndex / 8;
|
|
|
|
|
int bitIndex = tileIndex % 8;
|
|
|
|
|
if (byteIndex < static_cast<int>(surface.mask.size())) {
|
2026-02-03 21:30:59 -08:00
|
|
|
uint8_t maskByte = surface.mask[byteIndex];
|
2026-02-21 19:41:21 -08:00
|
|
|
bool renderTile = (maskByte & (1 << bitIndex)) || (maskByte & (1 << (7 - bitIndex)));
|
2026-02-03 20:40:59 -08:00
|
|
|
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 {
|
2026-02-21 19:41:21 -08:00
|
|
|
uint8_t basicType = (liquidType == 0) ? 0 : ((liquidType - 1) % 4);
|
2026-02-02 12:24:50 -08:00
|
|
|
switch (basicType) {
|
2026-02-21 19:41:21 -08:00
|
|
|
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);
|
2026-02-02 12:24:50 -08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-03 20:40:59 -08:00
|
|
|
float WaterRenderer::getLiquidAlpha(uint16_t liquidType) const {
|
2026-02-02 12:24:50 -08:00
|
|
|
uint8_t basicType = (liquidType == 0) ? 0 : ((liquidType - 1) % 4);
|
|
|
|
|
switch (basicType) {
|
2026-02-21 19:41:21 -08:00
|
|
|
case 1: return 0.68f;
|
|
|
|
|
case 2: return 0.72f;
|
|
|
|
|
case 3: return 0.62f;
|
|
|
|
|
default: return 0.38f;
|
2026-02-02 12:24:50 -08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
} // namespace rendering
|
|
|
|
|
} // namespace wowee
|