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-25 03:39:45 -08:00
|
|
|
|
#include <unordered_map>
|
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;
|
2026-02-22 22:34:48 -08:00
|
|
|
|
float liquidBasicType; // 0=water, 1=ocean, 2=magma, 3=slime
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// Matches set 2 binding 3 in water.frag.glsl
|
|
|
|
|
|
struct ReflectionUBOData {
|
|
|
|
|
|
glm::mat4 reflViewProj;
|
2026-02-21 19:41:21 -08:00
|
|
|
|
};
|
|
|
|
|
|
|
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;
|
2026-02-25 13:26:08 -08:00
|
|
|
|
poolInfo.flags = VK_DESCRIPTOR_POOL_CREATE_FREE_DESCRIPTOR_SET_BIT;
|
2026-02-21 19:41:21 -08:00
|
|
|
|
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 22:34:48 -08:00
|
|
|
|
// --- Scene history + reflection descriptor set layout (set 2) ---
|
2026-02-22 09:34:27 -08:00
|
|
|
|
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;
|
|
|
|
|
|
|
2026-02-22 22:34:48 -08:00
|
|
|
|
VkDescriptorSetLayoutBinding reflColorBinding{};
|
|
|
|
|
|
reflColorBinding.binding = 2;
|
|
|
|
|
|
reflColorBinding.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
|
|
|
|
|
|
reflColorBinding.descriptorCount = 1;
|
|
|
|
|
|
reflColorBinding.stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT;
|
|
|
|
|
|
|
|
|
|
|
|
VkDescriptorSetLayoutBinding reflUBOBinding{};
|
|
|
|
|
|
reflUBOBinding.binding = 3;
|
|
|
|
|
|
reflUBOBinding.descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER;
|
|
|
|
|
|
reflUBOBinding.descriptorCount = 1;
|
|
|
|
|
|
reflUBOBinding.stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT;
|
|
|
|
|
|
|
|
|
|
|
|
sceneSetLayout = createDescriptorSetLayout(device,
|
|
|
|
|
|
{sceneColorBinding, sceneDepthBinding, reflColorBinding, reflUBOBinding});
|
2026-02-22 09:34:27 -08:00
|
|
|
|
if (!sceneSetLayout) {
|
|
|
|
|
|
LOG_ERROR("WaterRenderer: failed to create scene set layout");
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-06 19:15:34 -08:00
|
|
|
|
// Pool needs 3 combined image samplers + 1 uniform buffer per frame
|
2026-02-22 22:34:48 -08:00
|
|
|
|
std::array<VkDescriptorPoolSize, 2> scenePoolSizes{};
|
|
|
|
|
|
scenePoolSizes[0].type = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
|
2026-03-06 19:15:34 -08:00
|
|
|
|
scenePoolSizes[0].descriptorCount = 3 * SCENE_HISTORY_FRAMES;
|
2026-02-22 22:34:48 -08:00
|
|
|
|
scenePoolSizes[1].type = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER;
|
2026-03-06 19:15:34 -08:00
|
|
|
|
scenePoolSizes[1].descriptorCount = SCENE_HISTORY_FRAMES;
|
2026-02-22 09:34:27 -08:00
|
|
|
|
VkDescriptorPoolCreateInfo scenePoolInfo{};
|
|
|
|
|
|
scenePoolInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO;
|
2026-03-06 19:15:34 -08:00
|
|
|
|
scenePoolInfo.maxSets = SCENE_HISTORY_FRAMES;
|
2026-02-22 22:34:48 -08:00
|
|
|
|
scenePoolInfo.poolSizeCount = static_cast<uint32_t>(scenePoolSizes.size());
|
|
|
|
|
|
scenePoolInfo.pPoolSizes = scenePoolSizes.data();
|
2026-02-22 09:34:27 -08:00
|
|
|
|
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{};
|
2026-02-22 22:34:48 -08:00
|
|
|
|
pushRange.stageFlags = VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT;
|
2026-02-21 19:41:21 -08:00
|
|
|
|
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 22:34:48 -08:00
|
|
|
|
// Create reflection resources FIRST so reflectionUBO exists when
|
|
|
|
|
|
// createSceneHistoryResources writes descriptor binding 3
|
|
|
|
|
|
createReflectionResources();
|
|
|
|
|
|
|
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 22:34:48 -08:00
|
|
|
|
destroyReflectionResources();
|
|
|
|
|
|
createReflectionResources();
|
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-03-06 19:15:34 -08:00
|
|
|
|
void WaterRenderer::setRefractionEnabled(bool enabled) {
|
|
|
|
|
|
if (refractionEnabled == enabled) return;
|
|
|
|
|
|
refractionEnabled = enabled;
|
|
|
|
|
|
|
|
|
|
|
|
// When turning off, clear scene history images to black so the shader
|
|
|
|
|
|
// detects "no data" and uses the non-refraction path.
|
|
|
|
|
|
if (!enabled && vkCtx) {
|
|
|
|
|
|
vkCtx->immediateSubmit([&](VkCommandBuffer cmd) {
|
|
|
|
|
|
for (uint32_t f = 0; f < SCENE_HISTORY_FRAMES; f++) {
|
|
|
|
|
|
auto& sh = sceneHistory[f];
|
|
|
|
|
|
if (!sh.colorImage) continue;
|
|
|
|
|
|
|
|
|
|
|
|
VkImageMemoryBarrier toTransfer{};
|
|
|
|
|
|
toTransfer.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER;
|
|
|
|
|
|
toTransfer.oldLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
|
|
|
|
|
|
toTransfer.newLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL;
|
|
|
|
|
|
toTransfer.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
|
|
|
|
|
|
toTransfer.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
|
|
|
|
|
|
toTransfer.image = sh.colorImage;
|
|
|
|
|
|
toTransfer.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1};
|
|
|
|
|
|
toTransfer.srcAccessMask = VK_ACCESS_SHADER_READ_BIT;
|
|
|
|
|
|
toTransfer.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
|
|
|
|
|
|
vkCmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT,
|
|
|
|
|
|
0, 0, nullptr, 0, nullptr, 1, &toTransfer);
|
|
|
|
|
|
|
|
|
|
|
|
VkClearColorValue clearColor = {{0.0f, 0.0f, 0.0f, 0.0f}};
|
|
|
|
|
|
VkImageSubresourceRange range = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1};
|
|
|
|
|
|
vkCmdClearColorImage(cmd, sh.colorImage, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, &clearColor, 1, &range);
|
|
|
|
|
|
|
|
|
|
|
|
VkImageMemoryBarrier toRead = toTransfer;
|
|
|
|
|
|
toRead.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL;
|
|
|
|
|
|
toRead.newLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
|
|
|
|
|
|
toRead.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
|
|
|
|
|
|
toRead.dstAccessMask = VK_ACCESS_SHADER_READ_BIT;
|
|
|
|
|
|
vkCmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT,
|
|
|
|
|
|
0, 0, nullptr, 0, nullptr, 1, &toRead);
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
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 22:34:48 -08:00
|
|
|
|
destroyWater1xResources();
|
|
|
|
|
|
destroyReflectionResources();
|
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();
|
2026-03-06 19:15:34 -08:00
|
|
|
|
for (auto& sh : sceneHistory) {
|
|
|
|
|
|
if (sh.colorView) { vkDestroyImageView(device, sh.colorView, nullptr); sh.colorView = VK_NULL_HANDLE; }
|
|
|
|
|
|
if (sh.depthView) { vkDestroyImageView(device, sh.depthView, nullptr); sh.depthView = VK_NULL_HANDLE; }
|
|
|
|
|
|
if (sh.colorImage) { vmaDestroyImage(vkCtx->getAllocator(), sh.colorImage, sh.colorAlloc); sh.colorImage = VK_NULL_HANDLE; sh.colorAlloc = VK_NULL_HANDLE; }
|
|
|
|
|
|
if (sh.depthImage) { vmaDestroyImage(vkCtx->getAllocator(), sh.depthImage, sh.depthAlloc); sh.depthImage = VK_NULL_HANDLE; sh.depthAlloc = VK_NULL_HANDLE; }
|
|
|
|
|
|
sh.sceneSet = VK_NULL_HANDLE;
|
|
|
|
|
|
}
|
2026-02-22 09:34:27 -08:00
|
|
|
|
if (sceneColorSampler) { vkDestroySampler(device, sceneColorSampler, nullptr); sceneColorSampler = VK_NULL_HANDLE; }
|
|
|
|
|
|
if (sceneDepthSampler) { vkDestroySampler(device, sceneDepthSampler, nullptr); sceneDepthSampler = VK_NULL_HANDLE; }
|
|
|
|
|
|
sceneHistoryExtent = {0, 0};
|
|
|
|
|
|
sceneHistoryReady = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void WaterRenderer::createSceneHistoryResources(VkExtent2D extent, VkFormat colorFormat, VkFormat depthFormat) {
|
|
|
|
|
|
if (!vkCtx || extent.width == 0 || extent.height == 0 || !sceneSetLayout || !sceneDescPool) return;
|
|
|
|
|
|
VkDevice device = vkCtx->getDevice();
|
|
|
|
|
|
|
|
|
|
|
|
destroySceneHistoryResources();
|
|
|
|
|
|
vkResetDescriptorPool(device, sceneDescPool, 0);
|
|
|
|
|
|
sceneHistoryExtent = extent;
|
|
|
|
|
|
|
2026-03-06 19:15:34 -08:00
|
|
|
|
// Create shared samplers
|
|
|
|
|
|
VkSamplerCreateInfo sampCI{};
|
|
|
|
|
|
sampCI.sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO;
|
|
|
|
|
|
sampCI.magFilter = VK_FILTER_LINEAR;
|
|
|
|
|
|
sampCI.minFilter = VK_FILTER_LINEAR;
|
|
|
|
|
|
sampCI.addressModeU = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
|
|
|
|
|
|
sampCI.addressModeV = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
|
|
|
|
|
|
sampCI.addressModeW = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
|
|
|
|
|
|
if (vkCreateSampler(device, &sampCI, nullptr, &sceneColorSampler) != VK_SUCCESS) {
|
|
|
|
|
|
LOG_ERROR("WaterRenderer: failed to create scene color sampler");
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
sampCI.magFilter = VK_FILTER_NEAREST;
|
|
|
|
|
|
sampCI.minFilter = VK_FILTER_NEAREST;
|
|
|
|
|
|
if (vkCreateSampler(device, &sampCI, nullptr, &sceneDepthSampler) != VK_SUCCESS) {
|
|
|
|
|
|
LOG_ERROR("WaterRenderer: failed to create scene depth sampler");
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-22 09:34:27 -08:00
|
|
|
|
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;
|
|
|
|
|
|
|
2026-03-06 19:15:34 -08:00
|
|
|
|
VkImageCreateInfo depthImgInfo = colorImgInfo;
|
|
|
|
|
|
depthImgInfo.format = depthFormat;
|
|
|
|
|
|
|
2026-02-22 09:34:27 -08:00
|
|
|
|
VmaAllocationCreateInfo allocCI{};
|
|
|
|
|
|
allocCI.usage = VMA_MEMORY_USAGE_GPU_ONLY;
|
|
|
|
|
|
|
2026-03-06 19:15:34 -08:00
|
|
|
|
// Create per-frame images, views, and descriptor sets
|
|
|
|
|
|
for (uint32_t f = 0; f < SCENE_HISTORY_FRAMES; f++) {
|
|
|
|
|
|
auto& sh = sceneHistory[f];
|
2026-02-22 09:34:27 -08:00
|
|
|
|
|
2026-03-06 19:15:34 -08:00
|
|
|
|
if (vmaCreateImage(vkCtx->getAllocator(), &colorImgInfo, &allocCI, &sh.colorImage, &sh.colorAlloc, nullptr) != VK_SUCCESS) {
|
|
|
|
|
|
LOG_ERROR("WaterRenderer: failed to create scene color history image [", f, "]");
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (vmaCreateImage(vkCtx->getAllocator(), &depthImgInfo, &allocCI, &sh.depthImage, &sh.depthAlloc, nullptr) != VK_SUCCESS) {
|
|
|
|
|
|
LOG_ERROR("WaterRenderer: failed to create scene depth history image [", f, "]");
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
2026-02-22 09:34:27 -08:00
|
|
|
|
|
2026-03-06 19:15:34 -08:00
|
|
|
|
VkImageViewCreateInfo colorViewInfo{};
|
|
|
|
|
|
colorViewInfo.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO;
|
|
|
|
|
|
colorViewInfo.image = sh.colorImage;
|
|
|
|
|
|
colorViewInfo.viewType = VK_IMAGE_VIEW_TYPE_2D;
|
|
|
|
|
|
colorViewInfo.format = colorFormat;
|
|
|
|
|
|
colorViewInfo.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
|
|
|
|
|
|
colorViewInfo.subresourceRange.levelCount = 1;
|
|
|
|
|
|
colorViewInfo.subresourceRange.layerCount = 1;
|
|
|
|
|
|
if (vkCreateImageView(device, &colorViewInfo, nullptr, &sh.colorView) != VK_SUCCESS) {
|
|
|
|
|
|
LOG_ERROR("WaterRenderer: failed to create scene color history view [", f, "]");
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
2026-02-22 09:34:27 -08:00
|
|
|
|
|
2026-03-06 19:15:34 -08:00
|
|
|
|
VkImageViewCreateInfo depthViewInfo = colorViewInfo;
|
|
|
|
|
|
depthViewInfo.image = sh.depthImage;
|
|
|
|
|
|
depthViewInfo.format = depthFormat;
|
|
|
|
|
|
depthViewInfo.subresourceRange.aspectMask = VK_IMAGE_ASPECT_DEPTH_BIT;
|
|
|
|
|
|
if (vkCreateImageView(device, &depthViewInfo, nullptr, &sh.depthView) != VK_SUCCESS) {
|
|
|
|
|
|
LOG_ERROR("WaterRenderer: failed to create scene depth history view [", f, "]");
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
2026-02-22 09:34:27 -08:00
|
|
|
|
|
2026-03-06 19:15:34 -08:00
|
|
|
|
// Allocate descriptor set for this frame
|
|
|
|
|
|
VkDescriptorSetAllocateInfo ai{};
|
|
|
|
|
|
ai.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO;
|
|
|
|
|
|
ai.descriptorPool = sceneDescPool;
|
|
|
|
|
|
ai.descriptorSetCount = 1;
|
|
|
|
|
|
ai.pSetLayouts = &sceneSetLayout;
|
|
|
|
|
|
if (vkAllocateDescriptorSets(device, &ai, &sh.sceneSet) != VK_SUCCESS) {
|
|
|
|
|
|
LOG_ERROR("WaterRenderer: failed to allocate scene descriptor set [", f, "]");
|
|
|
|
|
|
sh.sceneSet = VK_NULL_HANDLE;
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
2026-02-22 09:34:27 -08:00
|
|
|
|
|
2026-03-06 19:15:34 -08:00
|
|
|
|
VkDescriptorImageInfo colorInfo{};
|
|
|
|
|
|
colorInfo.sampler = sceneColorSampler;
|
|
|
|
|
|
colorInfo.imageView = sh.colorView;
|
|
|
|
|
|
colorInfo.imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
|
|
|
|
|
|
|
|
|
|
|
|
VkDescriptorImageInfo depthInfo{};
|
|
|
|
|
|
depthInfo.sampler = sceneDepthSampler;
|
|
|
|
|
|
depthInfo.imageView = sh.depthView;
|
|
|
|
|
|
depthInfo.imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
|
|
|
|
|
|
|
|
|
|
|
|
VkDescriptorImageInfo reflColorInfo{};
|
|
|
|
|
|
reflColorInfo.sampler = sceneColorSampler;
|
|
|
|
|
|
reflColorInfo.imageView = reflectionColorView ? reflectionColorView : sh.colorView;
|
|
|
|
|
|
reflColorInfo.imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
|
|
|
|
|
|
|
|
|
|
|
|
VkDescriptorBufferInfo reflUBOInfo{};
|
|
|
|
|
|
reflUBOInfo.buffer = reflectionUBO;
|
|
|
|
|
|
reflUBOInfo.offset = 0;
|
|
|
|
|
|
reflUBOInfo.range = sizeof(ReflectionUBOData);
|
|
|
|
|
|
|
|
|
|
|
|
std::vector<VkWriteDescriptorSet> writes;
|
|
|
|
|
|
|
|
|
|
|
|
VkWriteDescriptorSet w0{};
|
|
|
|
|
|
w0.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
|
|
|
|
|
|
w0.dstSet = sh.sceneSet;
|
|
|
|
|
|
w0.dstBinding = 0;
|
|
|
|
|
|
w0.descriptorCount = 1;
|
|
|
|
|
|
w0.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
|
|
|
|
|
|
w0.pImageInfo = &colorInfo;
|
|
|
|
|
|
writes.push_back(w0);
|
|
|
|
|
|
|
|
|
|
|
|
VkWriteDescriptorSet w1{};
|
|
|
|
|
|
w1.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
|
|
|
|
|
|
w1.dstSet = sh.sceneSet;
|
|
|
|
|
|
w1.dstBinding = 1;
|
|
|
|
|
|
w1.descriptorCount = 1;
|
|
|
|
|
|
w1.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
|
|
|
|
|
|
w1.pImageInfo = &depthInfo;
|
|
|
|
|
|
writes.push_back(w1);
|
|
|
|
|
|
|
|
|
|
|
|
VkWriteDescriptorSet w2{};
|
|
|
|
|
|
w2.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
|
|
|
|
|
|
w2.dstSet = sh.sceneSet;
|
|
|
|
|
|
w2.dstBinding = 2;
|
|
|
|
|
|
w2.descriptorCount = 1;
|
|
|
|
|
|
w2.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
|
|
|
|
|
|
w2.pImageInfo = &reflColorInfo;
|
|
|
|
|
|
writes.push_back(w2);
|
|
|
|
|
|
|
|
|
|
|
|
if (reflectionUBO) {
|
|
|
|
|
|
VkWriteDescriptorSet w3{};
|
|
|
|
|
|
w3.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
|
|
|
|
|
|
w3.dstSet = sh.sceneSet;
|
|
|
|
|
|
w3.dstBinding = 3;
|
|
|
|
|
|
w3.descriptorCount = 1;
|
|
|
|
|
|
w3.descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER;
|
|
|
|
|
|
w3.pBufferInfo = &reflUBOInfo;
|
|
|
|
|
|
writes.push_back(w3);
|
|
|
|
|
|
}
|
2026-02-22 22:34:48 -08:00
|
|
|
|
|
2026-03-06 19:15:34 -08:00
|
|
|
|
vkUpdateDescriptorSets(device, static_cast<uint32_t>(writes.size()), writes.data(), 0, nullptr);
|
2026-02-22 22:34:48 -08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-06 19:15:34 -08:00
|
|
|
|
// Initialize all per-frame history images to shader-read layout
|
2026-02-22 09:34:27 -08:00
|
|
|
|
vkCtx->immediateSubmit([&](VkCommandBuffer cmd) {
|
2026-03-06 19:15:34 -08:00
|
|
|
|
std::vector<VkImageMemoryBarrier> barriers;
|
|
|
|
|
|
for (uint32_t f = 0; f < SCENE_HISTORY_FRAMES; f++) {
|
|
|
|
|
|
VkImageMemoryBarrier b{};
|
|
|
|
|
|
b.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER;
|
|
|
|
|
|
b.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED;
|
|
|
|
|
|
b.newLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
|
|
|
|
|
|
b.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
|
|
|
|
|
|
b.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
|
|
|
|
|
|
b.image = sceneHistory[f].colorImage;
|
|
|
|
|
|
b.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1};
|
|
|
|
|
|
b.dstAccessMask = VK_ACCESS_SHADER_READ_BIT;
|
|
|
|
|
|
barriers.push_back(b);
|
|
|
|
|
|
|
|
|
|
|
|
b.image = sceneHistory[f].depthImage;
|
|
|
|
|
|
b.subresourceRange.aspectMask = VK_IMAGE_ASPECT_DEPTH_BIT;
|
|
|
|
|
|
barriers.push_back(b);
|
|
|
|
|
|
}
|
2026-02-22 09:34:27 -08:00
|
|
|
|
vkCmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT,
|
2026-03-06 19:15:34 -08:00
|
|
|
|
0, 0, nullptr, 0, nullptr, static_cast<uint32_t>(barriers.size()), barriers.data());
|
2026-02-22 09:34:27 -08:00
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
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);
|
2026-03-07 00:48:04 -08:00
|
|
|
|
if (basicType == 2) {
|
|
|
|
|
|
// Magma — bright orange-red, opaque
|
|
|
|
|
|
color = glm::vec4(1.0f, 0.35f, 0.05f, 1.0f);
|
|
|
|
|
|
alpha = 0.95f;
|
|
|
|
|
|
} else if (basicType == 3) {
|
|
|
|
|
|
// Slime — green, semi-opaque
|
|
|
|
|
|
color = glm::vec4(0.2f, 0.6f, 0.1f, 1.0f);
|
|
|
|
|
|
alpha = 0.85f;
|
2026-02-21 19:41:21 -08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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-25 13:26:08 -08:00
|
|
|
|
} else {
|
|
|
|
|
|
LOG_WARNING("Water: failed to allocate material descriptor set (pool exhaustion?)");
|
2026-02-21 19:41:21 -08:00
|
|
|
|
}
|
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();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-25 03:39:45 -08:00
|
|
|
|
// ── Pass 1: collect layers into merge groups keyed by {liquidType, roundedHeight} ──
|
|
|
|
|
|
struct ChunkLayerInfo {
|
|
|
|
|
|
int chunkX, chunkY;
|
|
|
|
|
|
const pipeline::ADTTerrain::WaterLayer* layer;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
struct MergeKey {
|
|
|
|
|
|
uint16_t liquidType;
|
|
|
|
|
|
int32_t roundedHeight; // minHeight * 2, rounded to int
|
|
|
|
|
|
bool operator==(const MergeKey& o) const {
|
|
|
|
|
|
return liquidType == o.liquidType && roundedHeight == o.roundedHeight;
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
struct MergeKeyHash {
|
|
|
|
|
|
size_t operator()(const MergeKey& k) const {
|
|
|
|
|
|
return std::hash<uint64_t>()((uint64_t(k.liquidType) << 32) | uint32_t(k.roundedHeight));
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
std::unordered_map<MergeKey, std::vector<ChunkLayerInfo>, MergeKeyHash> mergeGroups;
|
2026-02-02 12:24:50 -08:00
|
|
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
|
|
|
|
|
|
|
for (const auto& layer : chunkWater.layers) {
|
2026-02-25 03:39:45 -08:00
|
|
|
|
MergeKey key;
|
|
|
|
|
|
key.liquidType = layer.liquidType;
|
|
|
|
|
|
key.roundedHeight = static_cast<int32_t>(std::round(layer.minHeight * 2.0f));
|
|
|
|
|
|
mergeGroups[key].push_back({chunkX, chunkY, &layer});
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Tile origin = NW corner = chunk(0,0) position
|
|
|
|
|
|
const auto& chunk00 = terrain.getChunk(0, 0);
|
|
|
|
|
|
|
|
|
|
|
|
// Stormwind water lowering check
|
|
|
|
|
|
bool isStormwindArea = (tileX >= 28 && tileX <= 50 && tileY >= 28 && tileY <= 52);
|
|
|
|
|
|
float tileWorldX = 0, tileWorldY = 0;
|
|
|
|
|
|
glm::vec2 moonwellPos2D(0.0f);
|
|
|
|
|
|
if (isStormwindArea) {
|
|
|
|
|
|
tileWorldX = (32.0f - tileX) * 533.33333f;
|
|
|
|
|
|
tileWorldY = (32.0f - tileY) * 533.33333f;
|
|
|
|
|
|
moonwellPos2D = glm::vec2(-8755.9f, 1108.9f);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
int totalSurfaces = 0;
|
|
|
|
|
|
|
|
|
|
|
|
// Merge threshold: groups with more than this many chunks get merged into
|
|
|
|
|
|
// one tile-wide surface. Small groups (shore, lakes) stay per-chunk so
|
|
|
|
|
|
// their original mask / height data is preserved exactly.
|
|
|
|
|
|
constexpr size_t MERGE_THRESHOLD = 4;
|
|
|
|
|
|
|
|
|
|
|
|
// ── Pass 2: create surfaces ──
|
|
|
|
|
|
for (auto& [key, chunkLayers] : mergeGroups) {
|
|
|
|
|
|
|
|
|
|
|
|
// ── Small group → per-chunk surfaces (original code path) ──
|
|
|
|
|
|
if (chunkLayers.size() <= MERGE_THRESHOLD) {
|
|
|
|
|
|
for (const auto& info : chunkLayers) {
|
|
|
|
|
|
const auto& layer = *info.layer;
|
|
|
|
|
|
const auto& terrainChunk = terrain.getChunk(info.chunkX, info.chunkY);
|
|
|
|
|
|
|
|
|
|
|
|
WaterSurface surface;
|
|
|
|
|
|
surface.position = glm::vec3(
|
|
|
|
|
|
terrainChunk.position[0],
|
|
|
|
|
|
terrainChunk.position[1],
|
|
|
|
|
|
layer.minHeight
|
|
|
|
|
|
);
|
|
|
|
|
|
surface.origin = glm::vec3(
|
2026-03-06 20:23:22 -08:00
|
|
|
|
surface.position.x - (static_cast<float>(layer.x) * TILE_SIZE),
|
|
|
|
|
|
surface.position.y - (static_cast<float>(layer.y) * TILE_SIZE),
|
2026-02-25 03:39:45 -08:00
|
|
|
|
layer.minHeight
|
|
|
|
|
|
);
|
2026-03-06 20:23:22 -08:00
|
|
|
|
surface.stepX = glm::vec3(-TILE_SIZE, 0.0f, 0.0f);
|
|
|
|
|
|
surface.stepY = glm::vec3(0.0f, -TILE_SIZE, 0.0f);
|
2026-02-25 03:39:45 -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;
|
|
|
|
|
|
|
|
|
|
|
|
size_t numVertices = (layer.width + 1) * (layer.height + 1);
|
|
|
|
|
|
bool useFlat = true;
|
|
|
|
|
|
if (layer.heights.size() == numVertices) {
|
|
|
|
|
|
bool sane = true;
|
|
|
|
|
|
for (float h : layer.heights) {
|
|
|
|
|
|
if (!std::isfinite(h) || std::abs(h) > 50000.0f) { sane = false; break; }
|
|
|
|
|
|
if (h < layer.minHeight - 8.0f || h > layer.maxHeight + 8.0f) { sane = false; break; }
|
|
|
|
|
|
}
|
|
|
|
|
|
if (sane) { useFlat = false; surface.heights = layer.heights; }
|
|
|
|
|
|
}
|
|
|
|
|
|
if (useFlat) surface.heights.resize(numVertices, layer.minHeight);
|
|
|
|
|
|
|
|
|
|
|
|
if (isStormwindArea && layer.minHeight > 94.0f) {
|
|
|
|
|
|
float distToMoonwell = glm::distance(glm::vec2(tileWorldX, tileWorldY), moonwellPos2D);
|
|
|
|
|
|
if (distToMoonwell > 300.0f) {
|
|
|
|
|
|
for (float& h : surface.heights) h -= 1.0f;
|
|
|
|
|
|
surface.minHeight -= 1.0f;
|
|
|
|
|
|
surface.maxHeight -= 1.0f;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
surface.mask = layer.mask;
|
|
|
|
|
|
surface.tileX = tileX;
|
|
|
|
|
|
surface.tileY = tileY;
|
|
|
|
|
|
|
|
|
|
|
|
createWaterMesh(surface);
|
|
|
|
|
|
if (surface.indexCount > 0 && vkCtx) {
|
|
|
|
|
|
updateMaterialUBO(surface);
|
|
|
|
|
|
}
|
|
|
|
|
|
surfaces.push_back(std::move(surface));
|
|
|
|
|
|
totalSurfaces++;
|
|
|
|
|
|
}
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ── Large group → merged tile-wide surface ──
|
|
|
|
|
|
WaterSurface surface;
|
|
|
|
|
|
|
|
|
|
|
|
float groupHeight = key.roundedHeight / 2.0f;
|
|
|
|
|
|
|
|
|
|
|
|
surface.width = 128;
|
|
|
|
|
|
surface.height = 128;
|
|
|
|
|
|
surface.xOffset = 0;
|
|
|
|
|
|
surface.yOffset = 0;
|
|
|
|
|
|
surface.liquidType = key.liquidType;
|
|
|
|
|
|
surface.tileX = tileX;
|
|
|
|
|
|
surface.tileY = tileY;
|
|
|
|
|
|
|
|
|
|
|
|
// Origin = chunk(0,0) position (NW corner of tile)
|
|
|
|
|
|
surface.origin = glm::vec3(chunk00.position[0], chunk00.position[1], groupHeight);
|
|
|
|
|
|
surface.position = surface.origin;
|
2026-03-06 20:23:22 -08:00
|
|
|
|
surface.stepX = glm::vec3(-TILE_SIZE, 0.0f, 0.0f);
|
|
|
|
|
|
surface.stepY = glm::vec3(0.0f, -TILE_SIZE, 0.0f);
|
2026-02-25 03:39:45 -08:00
|
|
|
|
|
|
|
|
|
|
surface.minHeight = groupHeight;
|
|
|
|
|
|
surface.maxHeight = groupHeight;
|
|
|
|
|
|
|
|
|
|
|
|
// Initialize height grid (129×129) with group height
|
|
|
|
|
|
constexpr int MERGED_W = 128;
|
|
|
|
|
|
const int gridW = MERGED_W + 1; // 129
|
|
|
|
|
|
const int gridH = MERGED_W + 1;
|
|
|
|
|
|
surface.heights.resize(gridW * gridH, groupHeight);
|
|
|
|
|
|
|
2026-02-26 02:28:26 -08:00
|
|
|
|
// Initialize mask (128×128 sub-tiles)
|
2026-02-25 03:39:45 -08:00
|
|
|
|
// Mask uses LSB bit order: tileIndex = row * 128 + col
|
|
|
|
|
|
const int maskBytes = (MERGED_W * MERGED_W + 7) / 8;
|
2026-02-26 02:28:26 -08:00
|
|
|
|
// For ocean water (basicType 1) at sea level, fill the entire tile.
|
|
|
|
|
|
// Depth testing against terrain handles land occlusion naturally.
|
|
|
|
|
|
uint8_t basicType = (key.liquidType == 0) ? 0 : ((key.liquidType - 1) % 4);
|
|
|
|
|
|
bool isOcean = (basicType == 1) && (std::abs(groupHeight) < 1.0f);
|
|
|
|
|
|
surface.mask.resize(maskBytes, isOcean ? 0xFF : 0x00);
|
2026-02-25 03:39:45 -08:00
|
|
|
|
|
|
|
|
|
|
// ── Fill from each contributing chunk ──
|
|
|
|
|
|
for (const auto& info : chunkLayers) {
|
|
|
|
|
|
const auto& layer = *info.layer;
|
|
|
|
|
|
|
|
|
|
|
|
// Merged grid offset for this chunk
|
|
|
|
|
|
// gx = chunkY*8 + layer.x + localX, gy = chunkX*8 + layer.y + localY
|
|
|
|
|
|
int baseGx = info.chunkY * 8;
|
|
|
|
|
|
int baseGy = info.chunkX * 8;
|
|
|
|
|
|
|
|
|
|
|
|
// Copy heights
|
|
|
|
|
|
int layerGridW = layer.width + 1;
|
|
|
|
|
|
size_t numVertices = static_cast<size_t>(layerGridW) * (layer.height + 1);
|
2026-02-03 20:40:59 -08:00
|
|
|
|
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-25 03:39:45 -08:00
|
|
|
|
if (sane) useFlat = false;
|
2026-02-03 20:40:59 -08:00
|
|
|
|
}
|
2026-02-25 03:39:45 -08:00
|
|
|
|
|
|
|
|
|
|
for (int ly = 0; ly <= layer.height; ly++) {
|
|
|
|
|
|
for (int lx = 0; lx <= layer.width; lx++) {
|
|
|
|
|
|
int mgx = baseGx + layer.x + lx;
|
|
|
|
|
|
int mgy = baseGy + layer.y + ly;
|
|
|
|
|
|
if (mgx >= gridW || mgy >= gridH) continue;
|
|
|
|
|
|
|
|
|
|
|
|
float h;
|
|
|
|
|
|
if (!useFlat) {
|
|
|
|
|
|
int layerIdx = ly * layerGridW + lx;
|
|
|
|
|
|
h = layer.heights[layerIdx];
|
|
|
|
|
|
} else {
|
|
|
|
|
|
h = layer.minHeight;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
surface.heights[mgy * gridW + mgx] = h;
|
|
|
|
|
|
if (h < surface.minHeight) surface.minHeight = h;
|
|
|
|
|
|
if (h > surface.maxHeight) surface.maxHeight = h;
|
2026-02-09 21:24:34 -08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-25 03:39:45 -08:00
|
|
|
|
// Copy mask — mark contributing sub-tiles as renderable
|
|
|
|
|
|
for (int ly = 0; ly < layer.height; ly++) {
|
|
|
|
|
|
for (int lx = 0; lx < layer.width; lx++) {
|
|
|
|
|
|
bool render = true;
|
|
|
|
|
|
if (!layer.mask.empty()) {
|
|
|
|
|
|
int cx = layer.x + lx;
|
|
|
|
|
|
int cy = layer.y + ly;
|
|
|
|
|
|
int origTileIdx = cy * 8 + cx;
|
|
|
|
|
|
int origByte = origTileIdx / 8;
|
|
|
|
|
|
int origBit = origTileIdx % 8;
|
|
|
|
|
|
if (origByte < static_cast<int>(layer.mask.size())) {
|
|
|
|
|
|
uint8_t mb = layer.mask[origByte];
|
|
|
|
|
|
render = (mb & (1 << origBit)) || (mb & (1 << (7 - origBit)));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (render) {
|
|
|
|
|
|
int mx = baseGx + layer.x + lx;
|
|
|
|
|
|
int my = baseGy + layer.y + ly;
|
|
|
|
|
|
if (mx >= MERGED_W || my >= MERGED_W) continue;
|
|
|
|
|
|
|
|
|
|
|
|
int mergedTileIdx = my * MERGED_W + mx;
|
|
|
|
|
|
int byteIdx = mergedTileIdx / 8;
|
|
|
|
|
|
int bitIdx = mergedTileIdx % 8;
|
|
|
|
|
|
surface.mask[byteIdx] |= static_cast<uint8_t>(1 << bitIdx);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-02-21 19:41:21 -08:00
|
|
|
|
|
2026-02-25 03:39:45 -08:00
|
|
|
|
// Stormwind water lowering
|
|
|
|
|
|
if (isStormwindArea && surface.minHeight > 94.0f) {
|
|
|
|
|
|
float distToMoonwell = glm::distance(glm::vec2(tileWorldX, tileWorldY), moonwellPos2D);
|
|
|
|
|
|
if (distToMoonwell > 300.0f) {
|
|
|
|
|
|
for (float& h : surface.heights) h -= 1.0f;
|
|
|
|
|
|
surface.minHeight -= 1.0f;
|
|
|
|
|
|
surface.maxHeight -= 1.0f;
|
2026-02-21 19:41:21 -08:00
|
|
|
|
}
|
2026-02-02 12:24:50 -08:00
|
|
|
|
}
|
2026-02-25 03:39:45 -08:00
|
|
|
|
|
|
|
|
|
|
createWaterMesh(surface);
|
|
|
|
|
|
if (surface.indexCount > 0 && vkCtx) {
|
|
|
|
|
|
updateMaterialUBO(surface);
|
|
|
|
|
|
}
|
|
|
|
|
|
surfaces.push_back(std::move(surface));
|
|
|
|
|
|
totalSurfaces++;
|
2026-02-02 12:24:50 -08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-25 13:26:08 -08:00
|
|
|
|
if (totalSurfaces > 0) {
|
|
|
|
|
|
LOG_INFO("Water: Loaded ", totalSurfaces, " surfaces from tile [", tileX, ",", tileY,
|
|
|
|
|
|
"] (", mergeGroups.size(), " groups), total surfaces: ", surfaces.size());
|
|
|
|
|
|
}
|
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) {
|
2026-02-25 03:39:45 -08:00
|
|
|
|
LOG_DEBUG("Water: Removed ", removed, " surfaces for tile [", tileX, ",", tileY, "], remaining: ", surfaces.size());
|
2026-02-02 12:24:50 -08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
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;
|
2026-02-23 00:18:32 -08:00
|
|
|
|
|
|
|
|
|
|
// WMO liquid base heights sit ~2 units above the visual waterline.
|
|
|
|
|
|
constexpr float WMO_WATER_Z_OFFSET = -1.0f;
|
|
|
|
|
|
float adjustedZ = surface.origin.z + WMO_WATER_Z_OFFSET;
|
|
|
|
|
|
surface.heights.assign(vertexCount, adjustedZ);
|
|
|
|
|
|
surface.minHeight = adjustedZ;
|
|
|
|
|
|
surface.maxHeight = adjustedZ;
|
|
|
|
|
|
surface.origin.z = adjustedZ;
|
|
|
|
|
|
surface.position.z = adjustedZ;
|
|
|
|
|
|
|
2026-03-07 00:48:04 -08:00
|
|
|
|
if (surface.origin.z > 2000.0f || surface.origin.z < -500.0f) return;
|
2026-02-09 20:29:47 -08:00
|
|
|
|
|
2026-03-09 16:09:44 -07:00
|
|
|
|
// Build tile mask from MLIQ flags
|
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;
|
2026-02-23 00:18:32 -08:00
|
|
|
|
surface.mask.assign(maskBytes, 0x00);
|
|
|
|
|
|
for (size_t t = 0; t < tileCount; t++) {
|
|
|
|
|
|
bool hasLiquid = true;
|
2026-03-05 21:13:55 -08:00
|
|
|
|
int tx = static_cast<int>(t) % surface.width;
|
|
|
|
|
|
int ty = static_cast<int>(t) / surface.width;
|
|
|
|
|
|
|
|
|
|
|
|
// Standard WoW check: low nibble 0x0F = "don't render"
|
2026-02-23 00:18:32 -08:00
|
|
|
|
if (t < liquid.flags.size()) {
|
|
|
|
|
|
if ((liquid.flags[t] & 0x0F) == 0x0F) {
|
|
|
|
|
|
hasLiquid = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-03-05 21:13:55 -08:00
|
|
|
|
// Suppress water tiles that extend into enclosed WMO areas
|
|
|
|
|
|
// (e.g. Stormwind barracks stairway where canal water pokes through)
|
|
|
|
|
|
// Render coords: x=wowY(west), y=wowX(north)
|
|
|
|
|
|
if (hasLiquid) {
|
|
|
|
|
|
glm::vec3 tileWorld = surface.origin +
|
|
|
|
|
|
surface.stepX * (static_cast<float>(tx) + 0.5f) +
|
|
|
|
|
|
surface.stepY * (static_cast<float>(ty) + 0.5f);
|
|
|
|
|
|
// Stormwind Barracks / Stockade stairway:
|
|
|
|
|
|
// Stockade entrance at approximately render (-8768, 848)
|
|
|
|
|
|
if (tileWorld.x > -8790.0f && tileWorld.x < -8735.0f &&
|
|
|
|
|
|
tileWorld.y > 828.0f && tileWorld.y < 878.0f) {
|
|
|
|
|
|
hasLiquid = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-02-23 00:18:32 -08:00
|
|
|
|
if (hasLiquid) {
|
|
|
|
|
|
size_t byteIdx = t / 8;
|
|
|
|
|
|
size_t bitIdx = t % 8;
|
|
|
|
|
|
surface.mask[byteIdx] |= (1 << bitIdx);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-02-03 20:40:59 -08:00
|
|
|
|
|
|
|
|
|
|
createWaterMesh(surface);
|
2026-03-05 21:13:55 -08:00
|
|
|
|
|
|
|
|
|
|
// Count how many tiles passed the flag check and compute bounds
|
|
|
|
|
|
size_t activeTiles = 0;
|
|
|
|
|
|
float minWX = 1e9f, maxWX = -1e9f, minWY = 1e9f, maxWY = -1e9f;
|
|
|
|
|
|
for (size_t t = 0; t < tileCount; t++) {
|
|
|
|
|
|
size_t byteIdx = t / 8;
|
|
|
|
|
|
size_t bitIdx = t % 8;
|
|
|
|
|
|
if (surface.mask[byteIdx] & (1 << bitIdx)) {
|
|
|
|
|
|
activeTiles++;
|
|
|
|
|
|
int atx = static_cast<int>(t) % surface.width;
|
|
|
|
|
|
int aty = static_cast<int>(t) / surface.width;
|
|
|
|
|
|
glm::vec3 tw = surface.origin +
|
|
|
|
|
|
surface.stepX * (static_cast<float>(atx) + 0.5f) +
|
|
|
|
|
|
surface.stepY * (static_cast<float>(aty) + 0.5f);
|
|
|
|
|
|
if (tw.x < minWX) minWX = tw.x;
|
|
|
|
|
|
if (tw.x > maxWX) maxWX = tw.x;
|
|
|
|
|
|
if (tw.y < minWY) minWY = tw.y;
|
|
|
|
|
|
if (tw.y > maxWY) maxWY = tw.y;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
LOG_DEBUG("WMO water: origin=(", surface.origin.x, ",", surface.origin.y, ",", surface.origin.z,
|
|
|
|
|
|
") tiles=", (int)surface.width, "x", (int)surface.height,
|
|
|
|
|
|
" active=", activeTiles, "/", tileCount,
|
|
|
|
|
|
" wmoId=", wmoId, " indexCount=", surface.indexCount,
|
|
|
|
|
|
" bounds x=[", minWX, "..", maxWX, "] y=[", minWY, "..", maxWY, "]");
|
|
|
|
|
|
|
2026-02-03 20:40:59 -08:00
|
|
|
|
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,
|
2026-03-06 19:15:34 -08:00
|
|
|
|
const Camera& /*camera*/, float /*time*/, bool use1x, uint32_t frameIndex) {
|
2026-02-22 22:34:48 -08:00
|
|
|
|
VkPipeline pipeline = (use1x && water1xPipeline) ? water1xPipeline : waterPipeline;
|
2026-02-25 13:26:08 -08:00
|
|
|
|
if (!renderingEnabled || surfaces.empty() || !pipeline) {
|
|
|
|
|
|
if (renderDiagCounter_++ % 300 == 0 && !surfaces.empty()) {
|
|
|
|
|
|
LOG_WARNING("Water: render skipped — enabled=", renderingEnabled,
|
|
|
|
|
|
" surfaces=", surfaces.size(),
|
|
|
|
|
|
" pipeline=", (pipeline ? "ok" : "null"),
|
|
|
|
|
|
" use1x=", use1x);
|
|
|
|
|
|
}
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
2026-03-06 19:15:34 -08:00
|
|
|
|
uint32_t fi = frameIndex % SCENE_HISTORY_FRAMES;
|
|
|
|
|
|
VkDescriptorSet activeSceneSet = sceneHistory[fi].sceneSet;
|
|
|
|
|
|
if (!activeSceneSet) {
|
2026-02-25 13:26:08 -08:00
|
|
|
|
if (renderDiagCounter_++ % 300 == 0) {
|
|
|
|
|
|
LOG_WARNING("Water: render skipped — sceneSet is null, surfaces=", surfaces.size());
|
|
|
|
|
|
}
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
2026-02-02 12:24:50 -08:00
|
|
|
|
|
2026-02-22 22:34:48 -08:00
|
|
|
|
vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline);
|
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,
|
2026-03-06 19:15:34 -08:00
|
|
|
|
2, 1, &activeSceneSet, 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-23 00:18:32 -08:00
|
|
|
|
bool isWmoWater = (surface.wmoId != 0);
|
|
|
|
|
|
bool canalProfile = isWmoWater || (surface.liquidType == 5);
|
2026-02-22 22:34:48 -08:00
|
|
|
|
uint8_t basicType = (surface.liquidType == 0) ? 0 : ((surface.liquidType - 1) % 4);
|
2026-02-23 00:18:32 -08:00
|
|
|
|
// WMO water gets no wave displacement — prevents visible slosh at
|
|
|
|
|
|
// geometry edges (bridges, docks) where water is far below the surface.
|
2026-02-23 06:58:46 -08:00
|
|
|
|
float waveAmp = isWmoWater ? 0.0f : (basicType == 1 ? 0.35f : 0.08f);
|
2026-02-22 22:34:48 -08:00
|
|
|
|
float waveFreq = canalProfile ? 0.35f : (basicType == 1 ? 0.20f : 0.30f);
|
|
|
|
|
|
float waveSpeed = canalProfile ? 1.00f : (basicType == 1 ? 1.20f : 1.40f);
|
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-22 22:34:48 -08:00
|
|
|
|
push.liquidBasicType = static_cast<float>(basicType);
|
2026-02-02 12:24:50 -08:00
|
|
|
|
|
2026-02-22 22:34:48 -08:00
|
|
|
|
vkCmdPushConstants(cmd, pipelineLayout,
|
|
|
|
|
|
VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT,
|
2026-02-21 19:41:21 -08:00
|
|
|
|
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,
|
2026-03-06 19:15:34 -08:00
|
|
|
|
bool srcDepthIsMsaa,
|
|
|
|
|
|
uint32_t frameIndex) {
|
|
|
|
|
|
uint32_t fi = frameIndex % SCENE_HISTORY_FRAMES;
|
|
|
|
|
|
auto& sh = sceneHistory[fi];
|
|
|
|
|
|
if (!vkCtx || !cmd || !sh.colorImage || !sh.depthImage || srcExtent.width == 0 || srcExtent.height == 0) {
|
2026-02-22 09:34:27 -08:00
|
|
|
|
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);
|
2026-03-06 19:15:34 -08:00
|
|
|
|
barrier2(sh.colorImage, VK_IMAGE_ASPECT_COLOR_BIT,
|
2026-02-22 09:34:27 -08:00
|
|
|
|
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,
|
2026-03-06 19:15:34 -08:00
|
|
|
|
sh.colorImage, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, &colorCopy);
|
2026-02-22 09:34:27 -08:00
|
|
|
|
|
2026-03-06 19:15:34 -08:00
|
|
|
|
barrier2(sh.colorImage, VK_IMAGE_ASPECT_COLOR_BIT,
|
2026-02-22 09:34:27 -08:00
|
|
|
|
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);
|
2026-03-06 19:15:34 -08:00
|
|
|
|
barrier2(sh.depthImage, VK_IMAGE_ASPECT_DEPTH_BIT,
|
2026-02-22 09:34:27 -08:00
|
|
|
|
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,
|
2026-03-06 19:15:34 -08:00
|
|
|
|
sh.depthImage, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, &depthCopy);
|
2026-02-22 09:34:27 -08:00
|
|
|
|
|
2026-03-06 19:15:34 -08:00
|
|
|
|
barrier2(sh.depthImage, VK_IMAGE_ASPECT_DEPTH_BIT,
|
2026-02-22 09:34:27 -08:00
|
|
|
|
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;
|
2026-02-25 03:39:45 -08:00
|
|
|
|
bool isMergedTerrain = (surface.wmoId == 0 && surface.width > 8);
|
|
|
|
|
|
if (surface.wmoId == 0 && surface.width <= 8 && surface.mask.size() >= 8) {
|
2026-02-03 21:11:10 -08:00
|
|
|
|
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];
|
2026-02-25 03:39:45 -08:00
|
|
|
|
if (isMergedTerrain) {
|
|
|
|
|
|
// Merged surfaces use LSB-only bit order
|
|
|
|
|
|
renderTile = (maskByte & (1 << bitIndex)) != 0;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
bool lsbOrder = (maskByte & (1 << bitIndex)) != 0;
|
|
|
|
|
|
bool msbOrder = (maskByte & (1 << (7 - bitIndex))) != 0;
|
|
|
|
|
|
renderTile = lsbOrder || msbOrder;
|
|
|
|
|
|
}
|
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;
|
2026-02-25 03:39:45 -08:00
|
|
|
|
if (surface.wmoId == 0 && surface.width <= 8 && 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];
|
2026-02-25 03:39:45 -08:00
|
|
|
|
if (isMergedTerrain) {
|
|
|
|
|
|
if (nMask & (1 << nBitIdx)) {
|
|
|
|
|
|
renderTile = true;
|
|
|
|
|
|
goto found_neighbor;
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
if ((nMask & (1 << nBitIdx)) || (nMask & (1 << (7 - nBitIdx)))) {
|
|
|
|
|
|
renderTile = true;
|
|
|
|
|
|
goto found_neighbor;
|
|
|
|
|
|
}
|
2026-02-08 22:27:11 -08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
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-25 13:26:08 -08:00
|
|
|
|
if (surface.materialSet && materialDescPool) {
|
|
|
|
|
|
vkFreeDescriptorSets(vkCtx->getDevice(), materialDescPool, 1, &surface.materialSet);
|
|
|
|
|
|
}
|
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;
|
2026-02-25 03:39:45 -08:00
|
|
|
|
if (surface.wmoId == 0 && surface.width <= 8 && 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-25 03:39:45 -08:00
|
|
|
|
bool renderTile;
|
|
|
|
|
|
if (surface.wmoId == 0 && surface.width > 8) {
|
|
|
|
|
|
renderTile = (maskByte & (1 << bitIndex)) != 0;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
renderTile = (maskByte & (1 << bitIndex)) || (maskByte & (1 << (7 - bitIndex)));
|
|
|
|
|
|
}
|
2026-02-21 19:41:21 -08:00
|
|
|
|
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-23 00:18:32 -08:00
|
|
|
|
std::optional<float> WaterRenderer::getNearestWaterHeightAt(float glX, float glY, float queryZ, float maxAbove) const {
|
|
|
|
|
|
std::optional<float> best;
|
|
|
|
|
|
float bestDist = 1e9f;
|
|
|
|
|
|
|
|
|
|
|
|
for (const auto& surface : surfaces) {
|
|
|
|
|
|
glm::vec2 rel(glX - surface.origin.x, glY - surface.origin.y);
|
|
|
|
|
|
glm::vec2 sX(surface.stepX.x, surface.stepX.y);
|
|
|
|
|
|
glm::vec2 sY(surface.stepY.x, surface.stepY.y);
|
|
|
|
|
|
float lenSqX = glm::dot(sX, sX);
|
|
|
|
|
|
float lenSqY = glm::dot(sY, sY);
|
|
|
|
|
|
if (lenSqX < 1e-6f || lenSqY < 1e-6f) continue;
|
|
|
|
|
|
float gx = glm::dot(rel, sX) / lenSqX;
|
|
|
|
|
|
float gy = glm::dot(rel, sY) / lenSqY;
|
|
|
|
|
|
|
|
|
|
|
|
if (gx < 0.0f || gx > static_cast<float>(surface.width) ||
|
|
|
|
|
|
gy < 0.0f || gy > static_cast<float>(surface.height)) continue;
|
|
|
|
|
|
|
|
|
|
|
|
int gridWidth = surface.width + 1;
|
|
|
|
|
|
int ix = static_cast<int>(gx);
|
|
|
|
|
|
int iy = static_cast<int>(gy);
|
|
|
|
|
|
float fx = gx - ix;
|
|
|
|
|
|
float fy = gy - iy;
|
|
|
|
|
|
|
|
|
|
|
|
if (ix >= surface.width) { ix = surface.width - 1; fx = 1.0f; }
|
|
|
|
|
|
if (iy >= surface.height) { iy = surface.height - 1; fy = 1.0f; }
|
|
|
|
|
|
if (ix < 0 || iy < 0) continue;
|
|
|
|
|
|
|
|
|
|
|
|
if (!surface.mask.empty()) {
|
|
|
|
|
|
int tileIndex;
|
2026-02-25 03:39:45 -08:00
|
|
|
|
if (surface.wmoId == 0 && surface.width <= 8 && surface.mask.size() >= 8) {
|
2026-02-23 00:18:32 -08:00
|
|
|
|
tileIndex = (static_cast<int>(surface.yOffset) + iy) * 8 +
|
|
|
|
|
|
(static_cast<int>(surface.xOffset) + ix);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
tileIndex = iy * surface.width + ix;
|
|
|
|
|
|
}
|
|
|
|
|
|
int byteIndex = tileIndex / 8;
|
|
|
|
|
|
int bitIndex = tileIndex % 8;
|
|
|
|
|
|
if (byteIndex < static_cast<int>(surface.mask.size())) {
|
|
|
|
|
|
uint8_t maskByte = surface.mask[byteIndex];
|
2026-02-25 03:39:45 -08:00
|
|
|
|
bool renderTile;
|
|
|
|
|
|
if (surface.wmoId == 0 && surface.width > 8) {
|
|
|
|
|
|
renderTile = (maskByte & (1 << bitIndex)) != 0;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
renderTile = (maskByte & (1 << bitIndex)) || (maskByte & (1 << (7 - bitIndex)));
|
|
|
|
|
|
}
|
2026-02-23 00:18:32 -08:00
|
|
|
|
if (!renderTile) continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
int idx00 = iy * gridWidth + ix;
|
|
|
|
|
|
int idx10 = idx00 + 1;
|
|
|
|
|
|
int idx01 = idx00 + gridWidth;
|
|
|
|
|
|
int idx11 = idx01 + 1;
|
|
|
|
|
|
|
|
|
|
|
|
int total = static_cast<int>(surface.heights.size());
|
|
|
|
|
|
if (idx11 >= total) continue;
|
|
|
|
|
|
|
|
|
|
|
|
float h00 = surface.heights[idx00], h10 = surface.heights[idx10];
|
|
|
|
|
|
float h01 = surface.heights[idx01], h11 = surface.heights[idx11];
|
|
|
|
|
|
float h = h00*(1-fx)*(1-fy) + h10*fx*(1-fy) + h01*(1-fx)*fy + h11*fx*fy;
|
|
|
|
|
|
|
|
|
|
|
|
// Only consider water that's above queryZ but not too far above
|
|
|
|
|
|
if (h < queryZ - 2.0f) continue; // water below camera, skip
|
|
|
|
|
|
if (h > queryZ + maxAbove) continue; // water way above camera, skip
|
|
|
|
|
|
|
|
|
|
|
|
float dist = std::abs(h - queryZ);
|
|
|
|
|
|
if (!best || dist < bestDist) {
|
|
|
|
|
|
best = h;
|
|
|
|
|
|
bestDist = dist;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return best;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
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;
|
2026-02-25 03:39:45 -08:00
|
|
|
|
if (surface.wmoId == 0 && surface.width <= 8 && 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-25 03:39:45 -08:00
|
|
|
|
bool renderTile;
|
|
|
|
|
|
if (surface.wmoId == 0 && surface.width > 8) {
|
|
|
|
|
|
renderTile = (maskByte & (1 << bitIndex)) != 0;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-23 00:18:32 -08:00
|
|
|
|
bool WaterRenderer::isWmoWaterAt(float glX, float glY) const {
|
|
|
|
|
|
for (const auto& surface : surfaces) {
|
|
|
|
|
|
if (surface.wmoId == 0) continue;
|
|
|
|
|
|
glm::vec2 rel(glX - surface.origin.x, glY - surface.origin.y);
|
|
|
|
|
|
glm::vec2 sX(surface.stepX.x, surface.stepX.y);
|
|
|
|
|
|
glm::vec2 sY(surface.stepY.x, surface.stepY.y);
|
|
|
|
|
|
float lenSqX = glm::dot(sX, sX);
|
|
|
|
|
|
float lenSqY = glm::dot(sY, sY);
|
|
|
|
|
|
if (lenSqX < 1e-6f || lenSqY < 1e-6f) continue;
|
|
|
|
|
|
float gx = glm::dot(rel, sX) / lenSqX;
|
|
|
|
|
|
float gy = glm::dot(rel, sY) / lenSqY;
|
|
|
|
|
|
if (gx >= 0.0f && gx <= static_cast<float>(surface.width) &&
|
|
|
|
|
|
gy >= 0.0f && gy <= static_cast<float>(surface.height))
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-03 20:40:59 -08:00
|
|
|
|
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-03-06 19:15:34 -08:00
|
|
|
|
case 0: return glm::vec4(0.10f, 0.28f, 0.55f, 1.0f); // inland: richer blue
|
|
|
|
|
|
case 1: return glm::vec4(0.04f, 0.16f, 0.38f, 1.0f); // ocean: deep blue
|
2026-02-22 22:34:48 -08:00
|
|
|
|
case 2: return glm::vec4(0.9f, 0.3f, 0.05f, 1.0f); // magma
|
|
|
|
|
|
case 3: return glm::vec4(0.2f, 0.6f, 0.1f, 1.0f); // slime
|
2026-03-06 19:15:34 -08:00
|
|
|
|
default: return glm::vec4(0.10f, 0.28f, 0.55f, 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-22 22:34:48 -08:00
|
|
|
|
case 1: return 0.72f; // ocean
|
|
|
|
|
|
case 2: return 0.75f; // magma
|
|
|
|
|
|
case 3: return 0.65f; // slime
|
|
|
|
|
|
default: return 0.48f; // inland water
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ==============================================================
|
|
|
|
|
|
// Planar reflection resources
|
|
|
|
|
|
// ==============================================================
|
|
|
|
|
|
|
|
|
|
|
|
void WaterRenderer::createReflectionResources() {
|
|
|
|
|
|
if (!vkCtx) return;
|
|
|
|
|
|
VkDevice device = vkCtx->getDevice();
|
|
|
|
|
|
VmaAllocator allocator = vkCtx->getAllocator();
|
|
|
|
|
|
|
|
|
|
|
|
// --- Reflection color image ---
|
|
|
|
|
|
VkImageCreateInfo colorImgCI{};
|
|
|
|
|
|
colorImgCI.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO;
|
|
|
|
|
|
colorImgCI.imageType = VK_IMAGE_TYPE_2D;
|
|
|
|
|
|
colorImgCI.format = vkCtx->getSwapchainFormat();
|
|
|
|
|
|
colorImgCI.extent = {REFLECTION_WIDTH, REFLECTION_HEIGHT, 1};
|
|
|
|
|
|
colorImgCI.mipLevels = 1;
|
|
|
|
|
|
colorImgCI.arrayLayers = 1;
|
|
|
|
|
|
colorImgCI.samples = VK_SAMPLE_COUNT_1_BIT;
|
|
|
|
|
|
colorImgCI.tiling = VK_IMAGE_TILING_OPTIMAL;
|
|
|
|
|
|
colorImgCI.usage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_SAMPLED_BIT;
|
|
|
|
|
|
colorImgCI.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
|
|
|
|
|
|
|
|
|
|
|
|
VmaAllocationCreateInfo allocCI{};
|
|
|
|
|
|
allocCI.usage = VMA_MEMORY_USAGE_GPU_ONLY;
|
|
|
|
|
|
|
|
|
|
|
|
if (vmaCreateImage(allocator, &colorImgCI, &allocCI,
|
|
|
|
|
|
&reflectionColorImage, &reflectionColorAlloc, nullptr) != VK_SUCCESS) {
|
|
|
|
|
|
LOG_ERROR("WaterRenderer: failed to create reflection color image");
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
VkImageViewCreateInfo colorViewCI{};
|
|
|
|
|
|
colorViewCI.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO;
|
|
|
|
|
|
colorViewCI.image = reflectionColorImage;
|
|
|
|
|
|
colorViewCI.viewType = VK_IMAGE_VIEW_TYPE_2D;
|
|
|
|
|
|
colorViewCI.format = vkCtx->getSwapchainFormat();
|
|
|
|
|
|
colorViewCI.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1};
|
|
|
|
|
|
if (vkCreateImageView(device, &colorViewCI, nullptr, &reflectionColorView) != VK_SUCCESS) {
|
|
|
|
|
|
LOG_ERROR("WaterRenderer: failed to create reflection color view");
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// --- Reflection depth image ---
|
|
|
|
|
|
VkImageCreateInfo depthImgCI{};
|
|
|
|
|
|
depthImgCI.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO;
|
|
|
|
|
|
depthImgCI.imageType = VK_IMAGE_TYPE_2D;
|
|
|
|
|
|
depthImgCI.format = vkCtx->getDepthFormat();
|
|
|
|
|
|
depthImgCI.extent = {REFLECTION_WIDTH, REFLECTION_HEIGHT, 1};
|
|
|
|
|
|
depthImgCI.mipLevels = 1;
|
|
|
|
|
|
depthImgCI.arrayLayers = 1;
|
|
|
|
|
|
depthImgCI.samples = VK_SAMPLE_COUNT_1_BIT;
|
|
|
|
|
|
depthImgCI.tiling = VK_IMAGE_TILING_OPTIMAL;
|
|
|
|
|
|
depthImgCI.usage = VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT;
|
|
|
|
|
|
depthImgCI.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
|
|
|
|
|
|
|
|
|
|
|
|
if (vmaCreateImage(allocator, &depthImgCI, &allocCI,
|
|
|
|
|
|
&reflectionDepthImage, &reflectionDepthAlloc, nullptr) != VK_SUCCESS) {
|
|
|
|
|
|
LOG_ERROR("WaterRenderer: failed to create reflection depth image");
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
VkImageViewCreateInfo depthViewCI{};
|
|
|
|
|
|
depthViewCI.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO;
|
|
|
|
|
|
depthViewCI.image = reflectionDepthImage;
|
|
|
|
|
|
depthViewCI.viewType = VK_IMAGE_VIEW_TYPE_2D;
|
|
|
|
|
|
depthViewCI.format = vkCtx->getDepthFormat();
|
|
|
|
|
|
depthViewCI.subresourceRange = {VK_IMAGE_ASPECT_DEPTH_BIT, 0, 1, 0, 1};
|
|
|
|
|
|
if (vkCreateImageView(device, &depthViewCI, nullptr, &reflectionDepthView) != VK_SUCCESS) {
|
|
|
|
|
|
LOG_ERROR("WaterRenderer: failed to create reflection depth view");
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// --- Reflection sampler ---
|
|
|
|
|
|
VkSamplerCreateInfo sampCI{};
|
|
|
|
|
|
sampCI.sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO;
|
|
|
|
|
|
sampCI.magFilter = VK_FILTER_LINEAR;
|
|
|
|
|
|
sampCI.minFilter = VK_FILTER_LINEAR;
|
|
|
|
|
|
sampCI.addressModeU = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
|
|
|
|
|
|
sampCI.addressModeV = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
|
|
|
|
|
|
sampCI.addressModeW = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
|
|
|
|
|
|
if (vkCreateSampler(device, &sampCI, nullptr, &reflectionSampler) != VK_SUCCESS) {
|
|
|
|
|
|
LOG_ERROR("WaterRenderer: failed to create reflection sampler");
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// --- Reflection render pass ---
|
|
|
|
|
|
VkAttachmentDescription colorAttach{};
|
|
|
|
|
|
colorAttach.format = vkCtx->getSwapchainFormat();
|
|
|
|
|
|
colorAttach.samples = VK_SAMPLE_COUNT_1_BIT;
|
|
|
|
|
|
colorAttach.loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR;
|
|
|
|
|
|
colorAttach.storeOp = VK_ATTACHMENT_STORE_OP_STORE;
|
|
|
|
|
|
colorAttach.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE;
|
|
|
|
|
|
colorAttach.stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE;
|
|
|
|
|
|
colorAttach.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
|
|
|
|
|
|
colorAttach.finalLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
|
|
|
|
|
|
|
|
|
|
|
|
VkAttachmentDescription depthAttach{};
|
|
|
|
|
|
depthAttach.format = vkCtx->getDepthFormat();
|
|
|
|
|
|
depthAttach.samples = VK_SAMPLE_COUNT_1_BIT;
|
|
|
|
|
|
depthAttach.loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR;
|
|
|
|
|
|
depthAttach.storeOp = VK_ATTACHMENT_STORE_OP_DONT_CARE;
|
|
|
|
|
|
depthAttach.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE;
|
|
|
|
|
|
depthAttach.stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE;
|
|
|
|
|
|
depthAttach.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
|
|
|
|
|
|
depthAttach.finalLayout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL;
|
|
|
|
|
|
|
|
|
|
|
|
VkAttachmentReference colorRef{0, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL};
|
|
|
|
|
|
VkAttachmentReference depthRef{1, VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL};
|
|
|
|
|
|
|
|
|
|
|
|
VkSubpassDescription subpass{};
|
|
|
|
|
|
subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS;
|
|
|
|
|
|
subpass.colorAttachmentCount = 1;
|
|
|
|
|
|
subpass.pColorAttachments = &colorRef;
|
|
|
|
|
|
subpass.pDepthStencilAttachment = &depthRef;
|
|
|
|
|
|
|
|
|
|
|
|
VkSubpassDependency dep{};
|
|
|
|
|
|
dep.srcSubpass = VK_SUBPASS_EXTERNAL;
|
|
|
|
|
|
dep.dstSubpass = 0;
|
|
|
|
|
|
dep.srcStageMask = VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT;
|
|
|
|
|
|
dep.dstStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT | VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT;
|
|
|
|
|
|
dep.srcAccessMask = 0;
|
|
|
|
|
|
dep.dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT;
|
|
|
|
|
|
|
|
|
|
|
|
std::array<VkAttachmentDescription, 2> attachments = {colorAttach, depthAttach};
|
|
|
|
|
|
VkRenderPassCreateInfo rpCI{};
|
|
|
|
|
|
rpCI.sType = VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO;
|
|
|
|
|
|
rpCI.attachmentCount = static_cast<uint32_t>(attachments.size());
|
|
|
|
|
|
rpCI.pAttachments = attachments.data();
|
|
|
|
|
|
rpCI.subpassCount = 1;
|
|
|
|
|
|
rpCI.pSubpasses = &subpass;
|
|
|
|
|
|
rpCI.dependencyCount = 1;
|
|
|
|
|
|
rpCI.pDependencies = &dep;
|
|
|
|
|
|
|
|
|
|
|
|
if (vkCreateRenderPass(device, &rpCI, nullptr, &reflectionRenderPass) != VK_SUCCESS) {
|
|
|
|
|
|
LOG_ERROR("WaterRenderer: failed to create reflection render pass");
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// --- Reflection framebuffer ---
|
|
|
|
|
|
std::array<VkImageView, 2> fbAttach = {reflectionColorView, reflectionDepthView};
|
|
|
|
|
|
VkFramebufferCreateInfo fbCI{};
|
|
|
|
|
|
fbCI.sType = VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO;
|
|
|
|
|
|
fbCI.renderPass = reflectionRenderPass;
|
|
|
|
|
|
fbCI.attachmentCount = static_cast<uint32_t>(fbAttach.size());
|
|
|
|
|
|
fbCI.pAttachments = fbAttach.data();
|
|
|
|
|
|
fbCI.width = REFLECTION_WIDTH;
|
|
|
|
|
|
fbCI.height = REFLECTION_HEIGHT;
|
|
|
|
|
|
fbCI.layers = 1;
|
|
|
|
|
|
|
|
|
|
|
|
if (vkCreateFramebuffer(device, &fbCI, nullptr, &reflectionFramebuffer) != VK_SUCCESS) {
|
|
|
|
|
|
LOG_ERROR("WaterRenderer: failed to create reflection framebuffer");
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// --- Reflection UBO ---
|
|
|
|
|
|
VkBufferCreateInfo bufCI{};
|
|
|
|
|
|
bufCI.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO;
|
|
|
|
|
|
bufCI.size = sizeof(ReflectionUBOData);
|
|
|
|
|
|
bufCI.usage = VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT;
|
|
|
|
|
|
|
|
|
|
|
|
VmaAllocationCreateInfo uboAllocCI{};
|
|
|
|
|
|
uboAllocCI.usage = VMA_MEMORY_USAGE_CPU_TO_GPU;
|
|
|
|
|
|
uboAllocCI.flags = VMA_ALLOCATION_CREATE_MAPPED_BIT;
|
|
|
|
|
|
|
|
|
|
|
|
VmaAllocationInfo mapInfo{};
|
|
|
|
|
|
if (vmaCreateBuffer(allocator, &bufCI, &uboAllocCI,
|
|
|
|
|
|
&reflectionUBO, &reflectionUBOAlloc, &mapInfo) != VK_SUCCESS) {
|
|
|
|
|
|
LOG_ERROR("WaterRenderer: failed to create reflection UBO");
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
reflectionUBOMapped = mapInfo.pMappedData;
|
|
|
|
|
|
|
|
|
|
|
|
// Initialize with identity
|
|
|
|
|
|
ReflectionUBOData initData{};
|
|
|
|
|
|
initData.reflViewProj = glm::mat4(1.0f);
|
|
|
|
|
|
if (reflectionUBOMapped) {
|
|
|
|
|
|
std::memcpy(reflectionUBOMapped, &initData, sizeof(initData));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Transition reflection color image to shader-read so first frame doesn't read undefined
|
|
|
|
|
|
vkCtx->immediateSubmit([&](VkCommandBuffer cmd) {
|
|
|
|
|
|
VkImageMemoryBarrier barrier{};
|
|
|
|
|
|
barrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER;
|
|
|
|
|
|
barrier.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED;
|
|
|
|
|
|
barrier.newLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
|
|
|
|
|
|
barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
|
|
|
|
|
|
barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
|
|
|
|
|
|
barrier.image = reflectionColorImage;
|
|
|
|
|
|
barrier.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1};
|
|
|
|
|
|
barrier.dstAccessMask = VK_ACCESS_SHADER_READ_BIT;
|
|
|
|
|
|
vkCmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT,
|
|
|
|
|
|
VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT,
|
|
|
|
|
|
0, 0, nullptr, 0, nullptr, 1, &barrier);
|
|
|
|
|
|
});
|
|
|
|
|
|
reflectionColorLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
|
|
|
|
|
|
|
|
|
|
|
|
LOG_INFO("Water reflection resources created (", REFLECTION_WIDTH, "x", REFLECTION_HEIGHT, ")");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void WaterRenderer::destroyReflectionResources() {
|
|
|
|
|
|
if (!vkCtx) return;
|
|
|
|
|
|
VkDevice device = vkCtx->getDevice();
|
|
|
|
|
|
VmaAllocator allocator = vkCtx->getAllocator();
|
|
|
|
|
|
|
|
|
|
|
|
if (reflectionFramebuffer) { vkDestroyFramebuffer(device, reflectionFramebuffer, nullptr); reflectionFramebuffer = VK_NULL_HANDLE; }
|
|
|
|
|
|
if (reflectionRenderPass) { vkDestroyRenderPass(device, reflectionRenderPass, nullptr); reflectionRenderPass = VK_NULL_HANDLE; }
|
|
|
|
|
|
if (reflectionColorView) { vkDestroyImageView(device, reflectionColorView, nullptr); reflectionColorView = VK_NULL_HANDLE; }
|
|
|
|
|
|
if (reflectionDepthView) { vkDestroyImageView(device, reflectionDepthView, nullptr); reflectionDepthView = VK_NULL_HANDLE; }
|
|
|
|
|
|
if (reflectionColorImage) { vmaDestroyImage(allocator, reflectionColorImage, reflectionColorAlloc); reflectionColorImage = VK_NULL_HANDLE; }
|
|
|
|
|
|
if (reflectionDepthImage) { vmaDestroyImage(allocator, reflectionDepthImage, reflectionDepthAlloc); reflectionDepthImage = VK_NULL_HANDLE; }
|
|
|
|
|
|
if (reflectionSampler) { vkDestroySampler(device, reflectionSampler, nullptr); reflectionSampler = VK_NULL_HANDLE; }
|
|
|
|
|
|
if (reflectionUBO) {
|
|
|
|
|
|
AllocatedBuffer ab{}; ab.buffer = reflectionUBO; ab.allocation = reflectionUBOAlloc;
|
|
|
|
|
|
destroyBuffer(allocator, ab);
|
|
|
|
|
|
reflectionUBO = VK_NULL_HANDLE;
|
|
|
|
|
|
reflectionUBOMapped = nullptr;
|
2026-02-02 12:24:50 -08:00
|
|
|
|
}
|
2026-02-22 22:34:48 -08:00
|
|
|
|
reflectionColorLayout = VK_IMAGE_LAYOUT_UNDEFINED;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ==============================================================
|
|
|
|
|
|
// Reflection pass begin/end
|
|
|
|
|
|
// ==============================================================
|
|
|
|
|
|
|
|
|
|
|
|
bool WaterRenderer::beginReflectionPass(VkCommandBuffer cmd) {
|
|
|
|
|
|
if (!reflectionRenderPass || !reflectionFramebuffer || !cmd) return false;
|
|
|
|
|
|
|
|
|
|
|
|
VkRenderPassBeginInfo rpInfo{};
|
|
|
|
|
|
rpInfo.sType = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO;
|
|
|
|
|
|
rpInfo.renderPass = reflectionRenderPass;
|
|
|
|
|
|
rpInfo.framebuffer = reflectionFramebuffer;
|
|
|
|
|
|
rpInfo.renderArea = {{0, 0}, {REFLECTION_WIDTH, REFLECTION_HEIGHT}};
|
|
|
|
|
|
|
|
|
|
|
|
VkClearValue clears[2]{};
|
|
|
|
|
|
clears[0].color = {{0.0f, 0.0f, 0.0f, 1.0f}};
|
|
|
|
|
|
clears[1].depthStencil = {1.0f, 0};
|
|
|
|
|
|
rpInfo.clearValueCount = 2;
|
|
|
|
|
|
rpInfo.pClearValues = clears;
|
|
|
|
|
|
|
|
|
|
|
|
vkCmdBeginRenderPass(cmd, &rpInfo, VK_SUBPASS_CONTENTS_INLINE);
|
|
|
|
|
|
|
|
|
|
|
|
VkViewport vp{0, 0, static_cast<float>(REFLECTION_WIDTH), static_cast<float>(REFLECTION_HEIGHT), 0.0f, 1.0f};
|
|
|
|
|
|
vkCmdSetViewport(cmd, 0, 1, &vp);
|
|
|
|
|
|
VkRect2D sc{{0, 0}, {REFLECTION_WIDTH, REFLECTION_HEIGHT}};
|
|
|
|
|
|
vkCmdSetScissor(cmd, 0, 1, &sc);
|
|
|
|
|
|
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void WaterRenderer::endReflectionPass(VkCommandBuffer cmd) {
|
|
|
|
|
|
if (!cmd) return;
|
|
|
|
|
|
vkCmdEndRenderPass(cmd);
|
|
|
|
|
|
reflectionColorLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
|
|
|
|
|
|
|
2026-03-06 19:15:34 -08:00
|
|
|
|
// Update all per-frame scene descriptor sets with the freshly rendered reflection texture
|
|
|
|
|
|
if (reflectionColorView && reflectionSampler) {
|
2026-02-22 22:34:48 -08:00
|
|
|
|
VkDescriptorImageInfo reflInfo{};
|
|
|
|
|
|
reflInfo.sampler = reflectionSampler;
|
|
|
|
|
|
reflInfo.imageView = reflectionColorView;
|
|
|
|
|
|
reflInfo.imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
|
|
|
|
|
|
|
2026-03-06 19:15:34 -08:00
|
|
|
|
std::vector<VkWriteDescriptorSet> writes;
|
|
|
|
|
|
for (uint32_t f = 0; f < SCENE_HISTORY_FRAMES; f++) {
|
|
|
|
|
|
if (!sceneHistory[f].sceneSet) continue;
|
|
|
|
|
|
VkWriteDescriptorSet write{};
|
|
|
|
|
|
write.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
|
|
|
|
|
|
write.dstSet = sceneHistory[f].sceneSet;
|
|
|
|
|
|
write.dstBinding = 2;
|
|
|
|
|
|
write.descriptorCount = 1;
|
|
|
|
|
|
write.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
|
|
|
|
|
|
write.pImageInfo = &reflInfo;
|
|
|
|
|
|
writes.push_back(write);
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!writes.empty()) {
|
|
|
|
|
|
vkUpdateDescriptorSets(vkCtx->getDevice(), static_cast<uint32_t>(writes.size()), writes.data(), 0, nullptr);
|
|
|
|
|
|
}
|
2026-02-22 22:34:48 -08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void WaterRenderer::updateReflectionUBO(const glm::mat4& reflViewProj) {
|
|
|
|
|
|
if (!reflectionUBOMapped) return;
|
|
|
|
|
|
ReflectionUBOData data{};
|
|
|
|
|
|
data.reflViewProj = reflViewProj;
|
|
|
|
|
|
std::memcpy(reflectionUBOMapped, &data, sizeof(data));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ==============================================================
|
|
|
|
|
|
// Mirror camera computations
|
|
|
|
|
|
// ==============================================================
|
|
|
|
|
|
|
|
|
|
|
|
std::optional<float> WaterRenderer::getDominantWaterHeight(const glm::vec3& cameraPos) const {
|
|
|
|
|
|
if (surfaces.empty()) return std::nullopt;
|
|
|
|
|
|
|
|
|
|
|
|
// Find the water surface closest to the camera (XY distance)
|
|
|
|
|
|
float bestDist = std::numeric_limits<float>::max();
|
|
|
|
|
|
float bestHeight = 0.0f;
|
|
|
|
|
|
bool found = false;
|
|
|
|
|
|
|
|
|
|
|
|
for (const auto& surface : surfaces) {
|
|
|
|
|
|
// Skip magma/slime — only reflect water/ocean
|
|
|
|
|
|
uint8_t basicType = (surface.liquidType == 0) ? 0 : ((surface.liquidType - 1) % 4);
|
|
|
|
|
|
if (basicType >= 2) continue;
|
|
|
|
|
|
|
|
|
|
|
|
// Compute center of surface in world space
|
|
|
|
|
|
glm::vec3 center = surface.origin +
|
|
|
|
|
|
surface.stepX * (static_cast<float>(surface.width) * 0.5f) +
|
|
|
|
|
|
surface.stepY * (static_cast<float>(surface.height) * 0.5f);
|
|
|
|
|
|
|
|
|
|
|
|
float dx = cameraPos.x - center.x;
|
|
|
|
|
|
float dy = cameraPos.y - center.y;
|
|
|
|
|
|
float dist = dx * dx + dy * dy;
|
|
|
|
|
|
if (dist < bestDist) {
|
|
|
|
|
|
bestDist = dist;
|
|
|
|
|
|
bestHeight = surface.minHeight;
|
|
|
|
|
|
found = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (!found) return std::nullopt;
|
|
|
|
|
|
return bestHeight;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
glm::mat4 WaterRenderer::computeReflectedView(const Camera& camera, float waterHeight) {
|
|
|
|
|
|
// In this engine, Z is up. Water height is stored in the Z component.
|
|
|
|
|
|
// Mirror camera position across Z = waterHeight plane.
|
|
|
|
|
|
|
|
|
|
|
|
glm::vec3 camPos = camera.getPosition();
|
|
|
|
|
|
glm::vec3 reflPos = camPos;
|
|
|
|
|
|
reflPos.z = 2.0f * waterHeight - camPos.z;
|
|
|
|
|
|
|
|
|
|
|
|
// Get camera forward and reflect the Z component
|
|
|
|
|
|
glm::vec3 forward = camera.getForward();
|
|
|
|
|
|
forward.z = -forward.z;
|
|
|
|
|
|
glm::vec3 reflTarget = reflPos + forward;
|
|
|
|
|
|
|
|
|
|
|
|
glm::vec3 up(0.0f, 0.0f, 1.0f);
|
|
|
|
|
|
return glm::lookAt(reflPos, reflTarget, up);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
glm::mat4 WaterRenderer::computeObliqueProjection(const glm::mat4& proj, const glm::mat4& view,
|
|
|
|
|
|
float waterHeight) {
|
|
|
|
|
|
// Clip plane: everything below waterHeight in world space
|
|
|
|
|
|
// Z is up, so the clip plane normal is (0, 0, 1)
|
|
|
|
|
|
glm::vec4 clipPlaneWorld(0.0f, 0.0f, 1.0f, -waterHeight);
|
|
|
|
|
|
glm::vec4 clipPlaneView = glm::transpose(glm::inverse(view)) * clipPlaneWorld;
|
|
|
|
|
|
|
|
|
|
|
|
// Lengyel's oblique near-plane projection matrix modification
|
|
|
|
|
|
glm::mat4 result = proj;
|
|
|
|
|
|
glm::vec4 q;
|
|
|
|
|
|
q.x = (glm::sign(clipPlaneView.x) + result[2][0]) / result[0][0];
|
|
|
|
|
|
q.y = (glm::sign(clipPlaneView.y) + result[2][1]) / result[1][1];
|
|
|
|
|
|
q.z = -1.0f;
|
|
|
|
|
|
q.w = (1.0f + result[2][2]) / result[3][2];
|
|
|
|
|
|
|
|
|
|
|
|
glm::vec4 c = clipPlaneView * (2.0f / glm::dot(clipPlaneView, q));
|
|
|
|
|
|
result[0][2] = c.x;
|
|
|
|
|
|
result[1][2] = c.y;
|
|
|
|
|
|
result[2][2] = c.z + 1.0f;
|
|
|
|
|
|
result[3][2] = c.w;
|
|
|
|
|
|
|
|
|
|
|
|
return result;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ==============================================================
|
|
|
|
|
|
// Separate 1x water pass (used when MSAA is active)
|
|
|
|
|
|
// ==============================================================
|
|
|
|
|
|
|
|
|
|
|
|
bool WaterRenderer::createWater1xPass(VkFormat colorFormat, VkFormat depthFormat) {
|
|
|
|
|
|
if (!vkCtx) return false;
|
|
|
|
|
|
VkDevice device = vkCtx->getDevice();
|
|
|
|
|
|
|
|
|
|
|
|
VkAttachmentDescription attachments[2]{};
|
|
|
|
|
|
// Color: load existing resolved content, store after water draw
|
|
|
|
|
|
attachments[0].format = colorFormat;
|
|
|
|
|
|
attachments[0].samples = VK_SAMPLE_COUNT_1_BIT;
|
|
|
|
|
|
attachments[0].loadOp = VK_ATTACHMENT_LOAD_OP_LOAD;
|
|
|
|
|
|
attachments[0].storeOp = VK_ATTACHMENT_STORE_OP_STORE;
|
|
|
|
|
|
attachments[0].stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE;
|
|
|
|
|
|
attachments[0].stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE;
|
|
|
|
|
|
attachments[0].initialLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR;
|
|
|
|
|
|
attachments[0].finalLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR;
|
|
|
|
|
|
|
|
|
|
|
|
// Depth: load resolved depth for depth testing
|
|
|
|
|
|
attachments[1].format = depthFormat;
|
|
|
|
|
|
attachments[1].samples = VK_SAMPLE_COUNT_1_BIT;
|
|
|
|
|
|
attachments[1].loadOp = VK_ATTACHMENT_LOAD_OP_LOAD;
|
|
|
|
|
|
attachments[1].storeOp = VK_ATTACHMENT_STORE_OP_DONT_CARE;
|
|
|
|
|
|
attachments[1].stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE;
|
|
|
|
|
|
attachments[1].stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE;
|
|
|
|
|
|
attachments[1].initialLayout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL;
|
|
|
|
|
|
attachments[1].finalLayout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL;
|
|
|
|
|
|
|
|
|
|
|
|
VkAttachmentReference colorRef{0, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL};
|
|
|
|
|
|
VkAttachmentReference depthRef{1, VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL};
|
|
|
|
|
|
|
|
|
|
|
|
VkSubpassDescription subpass{};
|
|
|
|
|
|
subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS;
|
|
|
|
|
|
subpass.colorAttachmentCount = 1;
|
|
|
|
|
|
subpass.pColorAttachments = &colorRef;
|
|
|
|
|
|
subpass.pDepthStencilAttachment = &depthRef;
|
|
|
|
|
|
|
|
|
|
|
|
VkSubpassDependency dep{};
|
|
|
|
|
|
dep.srcSubpass = VK_SUBPASS_EXTERNAL;
|
|
|
|
|
|
dep.dstSubpass = 0;
|
|
|
|
|
|
dep.srcStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT | VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT;
|
|
|
|
|
|
dep.dstStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT | VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT;
|
|
|
|
|
|
dep.srcAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT;
|
|
|
|
|
|
dep.dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT |
|
|
|
|
|
|
VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_READ_BIT;
|
|
|
|
|
|
|
|
|
|
|
|
VkRenderPassCreateInfo rpCI{};
|
|
|
|
|
|
rpCI.sType = VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO;
|
|
|
|
|
|
rpCI.attachmentCount = 2;
|
|
|
|
|
|
rpCI.pAttachments = attachments;
|
|
|
|
|
|
rpCI.subpassCount = 1;
|
|
|
|
|
|
rpCI.pSubpasses = &subpass;
|
|
|
|
|
|
rpCI.dependencyCount = 1;
|
|
|
|
|
|
rpCI.pDependencies = &dep;
|
|
|
|
|
|
|
|
|
|
|
|
if (vkCreateRenderPass(device, &rpCI, nullptr, &water1xRenderPass) != VK_SUCCESS) {
|
|
|
|
|
|
LOG_ERROR("WaterRenderer: failed to create 1x water render pass");
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Build 1x water pipeline against this render pass
|
|
|
|
|
|
VkShaderModule vertShader, fragShader;
|
|
|
|
|
|
if (!vertShader.loadFromFile(device, "assets/shaders/water.vert.spv") ||
|
|
|
|
|
|
!fragShader.loadFromFile(device, "assets/shaders/water.frag.spv")) {
|
|
|
|
|
|
LOG_ERROR("WaterRenderer: failed to load shaders for 1x pipeline");
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
VkVertexInputBindingDescription vertBinding{};
|
|
|
|
|
|
vertBinding.binding = 0;
|
|
|
|
|
|
vertBinding.stride = 8 * sizeof(float);
|
|
|
|
|
|
vertBinding.inputRate = VK_VERTEX_INPUT_RATE_VERTEX;
|
|
|
|
|
|
|
|
|
|
|
|
std::vector<VkVertexInputAttributeDescription> vertAttribs = {
|
|
|
|
|
|
{ 0, 0, VK_FORMAT_R32G32B32_SFLOAT, 0 },
|
|
|
|
|
|
{ 1, 0, VK_FORMAT_R32G32_SFLOAT, 6 * sizeof(float) },
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
water1xPipeline = PipelineBuilder()
|
|
|
|
|
|
.setShaders(vertShader.stageInfo(VK_SHADER_STAGE_VERTEX_BIT),
|
|
|
|
|
|
fragShader.stageInfo(VK_SHADER_STAGE_FRAGMENT_BIT))
|
|
|
|
|
|
.setVertexInput({ vertBinding }, vertAttribs)
|
|
|
|
|
|
.setTopology(VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST)
|
|
|
|
|
|
.setRasterization(VK_POLYGON_MODE_FILL, VK_CULL_MODE_NONE)
|
|
|
|
|
|
.setDepthTest(true, false, VK_COMPARE_OP_LESS_OR_EQUAL)
|
|
|
|
|
|
.setColorBlendAttachment(PipelineBuilder::blendAlpha())
|
|
|
|
|
|
.setMultisample(VK_SAMPLE_COUNT_1_BIT)
|
|
|
|
|
|
.setLayout(pipelineLayout)
|
|
|
|
|
|
.setRenderPass(water1xRenderPass)
|
|
|
|
|
|
.setDynamicStates({ VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR })
|
|
|
|
|
|
.build(device);
|
|
|
|
|
|
|
|
|
|
|
|
vertShader.destroy();
|
|
|
|
|
|
fragShader.destroy();
|
|
|
|
|
|
|
|
|
|
|
|
if (!water1xPipeline) {
|
|
|
|
|
|
LOG_ERROR("WaterRenderer: failed to create 1x water pipeline");
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
LOG_INFO("WaterRenderer: created 1x water pass and pipeline");
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void WaterRenderer::createWater1xFramebuffers(const std::vector<VkImageView>& swapViews,
|
|
|
|
|
|
VkImageView depthView, VkExtent2D extent) {
|
|
|
|
|
|
if (!vkCtx || !water1xRenderPass || !depthView) return;
|
|
|
|
|
|
VkDevice device = vkCtx->getDevice();
|
|
|
|
|
|
|
|
|
|
|
|
// Destroy old framebuffers
|
|
|
|
|
|
for (auto fb : water1xFramebuffers) {
|
|
|
|
|
|
if (fb) vkDestroyFramebuffer(device, fb, nullptr);
|
|
|
|
|
|
}
|
|
|
|
|
|
water1xFramebuffers.clear();
|
|
|
|
|
|
|
|
|
|
|
|
water1xFramebuffers.resize(swapViews.size());
|
|
|
|
|
|
for (size_t i = 0; i < swapViews.size(); i++) {
|
|
|
|
|
|
VkImageView views[2] = { swapViews[i], depthView };
|
|
|
|
|
|
VkFramebufferCreateInfo fbCI{};
|
|
|
|
|
|
fbCI.sType = VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO;
|
|
|
|
|
|
fbCI.renderPass = water1xRenderPass;
|
|
|
|
|
|
fbCI.attachmentCount = 2;
|
|
|
|
|
|
fbCI.pAttachments = views;
|
|
|
|
|
|
fbCI.width = extent.width;
|
|
|
|
|
|
fbCI.height = extent.height;
|
|
|
|
|
|
fbCI.layers = 1;
|
|
|
|
|
|
if (vkCreateFramebuffer(device, &fbCI, nullptr, &water1xFramebuffers[i]) != VK_SUCCESS) {
|
|
|
|
|
|
LOG_ERROR("WaterRenderer: failed to create 1x framebuffer ", i);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void WaterRenderer::destroyWater1xResources() {
|
|
|
|
|
|
if (!vkCtx) return;
|
|
|
|
|
|
VkDevice device = vkCtx->getDevice();
|
|
|
|
|
|
for (auto fb : water1xFramebuffers) {
|
|
|
|
|
|
if (fb) vkDestroyFramebuffer(device, fb, nullptr);
|
|
|
|
|
|
}
|
|
|
|
|
|
water1xFramebuffers.clear();
|
|
|
|
|
|
if (water1xPipeline) { vkDestroyPipeline(device, water1xPipeline, nullptr); water1xPipeline = VK_NULL_HANDLE; }
|
|
|
|
|
|
if (water1xRenderPass) { vkDestroyRenderPass(device, water1xRenderPass, nullptr); water1xRenderPass = VK_NULL_HANDLE; }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
bool WaterRenderer::beginWater1xPass(VkCommandBuffer cmd, uint32_t imageIndex, VkExtent2D extent) {
|
|
|
|
|
|
if (!water1xRenderPass || imageIndex >= water1xFramebuffers.size() || !water1xFramebuffers[imageIndex])
|
|
|
|
|
|
return false;
|
|
|
|
|
|
|
|
|
|
|
|
VkRenderPassBeginInfo rpBI{};
|
|
|
|
|
|
rpBI.sType = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO;
|
|
|
|
|
|
rpBI.renderPass = water1xRenderPass;
|
|
|
|
|
|
rpBI.framebuffer = water1xFramebuffers[imageIndex];
|
|
|
|
|
|
rpBI.renderArea = {{0, 0}, extent};
|
|
|
|
|
|
rpBI.clearValueCount = 0;
|
|
|
|
|
|
rpBI.pClearValues = nullptr;
|
|
|
|
|
|
vkCmdBeginRenderPass(cmd, &rpBI, VK_SUBPASS_CONTENTS_INLINE);
|
|
|
|
|
|
|
|
|
|
|
|
VkViewport vp{0, 0, static_cast<float>(extent.width), static_cast<float>(extent.height), 0.0f, 1.0f};
|
|
|
|
|
|
vkCmdSetViewport(cmd, 0, 1, &vp);
|
|
|
|
|
|
VkRect2D sc{{0, 0}, extent};
|
|
|
|
|
|
vkCmdSetScissor(cmd, 0, 1, &sc);
|
|
|
|
|
|
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void WaterRenderer::endWater1xPass(VkCommandBuffer cmd) {
|
|
|
|
|
|
vkCmdEndRenderPass(cmd);
|
2026-02-02 12:24:50 -08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
} // namespace rendering
|
|
|
|
|
|
} // namespace wowee
|