Add water refraction toggle with per-frame scene history

Fix VK_ERROR_DEVICE_LOST crash by allocating per-frame scene history
images (color + depth) instead of a single shared image that raced
between frames in flight. Water refraction can now be toggled via
Settings > Video > Water Refraction.

Without refraction: richer blue base colors, animated caustic shimmer,
and normal-based color shifts give the water visible life. With
refraction: clean screen-space refraction with Beer-Lambert absorption.
Disabling clears scene history to black for immediate fallback.
This commit is contained in:
Kelsi 2026-03-06 19:15:34 -08:00
parent 7630c7aec7
commit 5a227c0376
8 changed files with 323 additions and 191 deletions

View file

@ -226,11 +226,32 @@ void main() {
float depthFade = 1.0 - exp(-verticalDepth * 0.15); float depthFade = 1.0 - exp(-verticalDepth * 0.15);
vec3 waterBody = mix(shallowColor, deepColor, depthFade); vec3 waterBody = mix(shallowColor, deepColor, depthFade);
vec3 refractedColor = mix(foggedScene * absorbed, waterBody, depthFade * 0.7); // Detect if scene history is available (scene data captured for refraction)
float sceneBrightness = dot(sceneRefract, vec3(0.299, 0.587, 0.114));
bool hasSceneData = (sceneBrightness > 0.003);
if (verticalDepth < 0.01) { // Animated caustic shimmer — only without refraction (refraction already provides movement)
float opticalDepth = 1.0 - exp(-dist * 0.004); if (!hasSceneData) {
refractedColor = mix(foggedScene, waterBody, opticalDepth * 0.6); float caustic1 = noiseValue(FragPos.xy * 1.8 + time * vec2(0.3, 0.15));
float caustic2 = noiseValue(FragPos.xy * 3.2 - time * vec2(0.2, 0.35));
float causticPattern = caustic1 * 0.6 + caustic2 * 0.4;
vec3 causticTint = vec3(0.08, 0.18, 0.28) * smoothstep(0.35, 0.75, causticPattern);
waterBody += causticTint;
}
vec3 refractedColor;
if (hasSceneData) {
refractedColor = mix(foggedScene * absorbed, waterBody, depthFade * 0.7);
if (verticalDepth < 0.01) {
float opticalDepth = 1.0 - exp(-dist * 0.004);
refractedColor = mix(foggedScene, waterBody, opticalDepth * 0.6);
}
} else {
// No refraction data — use lit water body with animated variation
vec3 litWater = waterBody * (ambientColor.rgb * 0.8 + NdotL * lightColor.rgb * 0.6);
float normalShift = dot(detailNorm.xy, vec2(0.5, 0.5));
litWater += vec3(0.02, 0.06, 0.10) * normalShift;
refractedColor = litWater;
} }
vec3 litBase = waterBody * (ambientColor.rgb * 0.7 + NdotL * lightColor.rgb * 0.5); vec3 litBase = waterBody * (ambientColor.rgb * 0.7 + NdotL * lightColor.rgb * 0.5);

Binary file not shown.

View file

@ -256,6 +256,9 @@ public:
bool areShadowsEnabled() const { return shadowsEnabled; } bool areShadowsEnabled() const { return shadowsEnabled; }
void setMsaaSamples(VkSampleCountFlagBits samples); void setMsaaSamples(VkSampleCountFlagBits samples);
void setWaterRefractionEnabled(bool enabled);
bool isWaterRefractionEnabled() const;
private: private:
void applyMsaaChange(); void applyMsaaChange();
VkSampleCountFlagBits pendingMsaaSamples_ = VK_SAMPLE_COUNT_1_BIT; VkSampleCountFlagBits pendingMsaaSamples_ = VK_SAMPLE_COUNT_1_BIT;

View file

@ -93,12 +93,13 @@ public:
bool hasWater1xPass() const { return water1xRenderPass != VK_NULL_HANDLE; } bool hasWater1xPass() const { return water1xRenderPass != VK_NULL_HANDLE; }
VkRenderPass getWater1xRenderPass() const { return water1xRenderPass; } VkRenderPass getWater1xRenderPass() const { return water1xRenderPass; }
void render(VkCommandBuffer cmd, VkDescriptorSet perFrameSet, const Camera& camera, float time, bool use1x = false); void render(VkCommandBuffer cmd, VkDescriptorSet perFrameSet, const Camera& camera, float time, bool use1x = false, uint32_t frameIndex = 0);
void captureSceneHistory(VkCommandBuffer cmd, void captureSceneHistory(VkCommandBuffer cmd,
VkImage srcColorImage, VkImage srcColorImage,
VkImage srcDepthImage, VkImage srcDepthImage,
VkExtent2D srcExtent, VkExtent2D srcExtent,
bool srcDepthIsMsaa); bool srcDepthIsMsaa,
uint32_t frameIndex = 0);
// --- Planar reflection pass --- // --- Planar reflection pass ---
// Call sequence: beginReflectionPass → [render scene] → endReflectionPass // Call sequence: beginReflectionPass → [render scene] → endReflectionPass
@ -124,6 +125,9 @@ public:
void setEnabled(bool enabled) { renderingEnabled = enabled; } void setEnabled(bool enabled) { renderingEnabled = enabled; }
bool isEnabled() const { return renderingEnabled; } bool isEnabled() const { return renderingEnabled; }
void setRefractionEnabled(bool enabled);
bool isRefractionEnabled() const { return refractionEnabled; }
std::optional<float> getWaterHeightAt(float glX, float glY) const; std::optional<float> getWaterHeightAt(float glX, float glY) const;
/// Like getWaterHeightAt but only returns water surfaces whose height is /// Like getWaterHeightAt but only returns water surfaces whose height is
/// close to the query Z (within maxAbove units above). Avoids false /// close to the query Z (within maxAbove units above). Avoids false
@ -159,17 +163,22 @@ private:
VkDescriptorPool materialDescPool = VK_NULL_HANDLE; VkDescriptorPool materialDescPool = VK_NULL_HANDLE;
VkDescriptorSetLayout sceneSetLayout = VK_NULL_HANDLE; VkDescriptorSetLayout sceneSetLayout = VK_NULL_HANDLE;
VkDescriptorPool sceneDescPool = VK_NULL_HANDLE; VkDescriptorPool sceneDescPool = VK_NULL_HANDLE;
VkDescriptorSet sceneSet = VK_NULL_HANDLE;
static constexpr uint32_t MAX_WATER_SETS = 16384; static constexpr uint32_t MAX_WATER_SETS = 16384;
VkSampler sceneColorSampler = VK_NULL_HANDLE; VkSampler sceneColorSampler = VK_NULL_HANDLE;
VkSampler sceneDepthSampler = VK_NULL_HANDLE; VkSampler sceneDepthSampler = VK_NULL_HANDLE;
VkImage sceneColorImage = VK_NULL_HANDLE; // Per-frame scene history to avoid race between frames in flight
VmaAllocation sceneColorAlloc = VK_NULL_HANDLE; static constexpr uint32_t SCENE_HISTORY_FRAMES = 2;
VkImageView sceneColorView = VK_NULL_HANDLE; struct PerFrameSceneHistory {
VkImage sceneDepthImage = VK_NULL_HANDLE; VkImage colorImage = VK_NULL_HANDLE;
VmaAllocation sceneDepthAlloc = VK_NULL_HANDLE; VmaAllocation colorAlloc = VK_NULL_HANDLE;
VkImageView sceneDepthView = VK_NULL_HANDLE; VkImageView colorView = VK_NULL_HANDLE;
VkImage depthImage = VK_NULL_HANDLE;
VmaAllocation depthAlloc = VK_NULL_HANDLE;
VkImageView depthView = VK_NULL_HANDLE;
VkDescriptorSet sceneSet = VK_NULL_HANDLE;
};
PerFrameSceneHistory sceneHistory[SCENE_HISTORY_FRAMES];
VkExtent2D sceneHistoryExtent = {0, 0}; VkExtent2D sceneHistoryExtent = {0, 0};
bool sceneHistoryReady = false; bool sceneHistoryReady = false;
mutable uint32_t renderDiagCounter_ = 0; mutable uint32_t renderDiagCounter_ = 0;
@ -200,6 +209,7 @@ private:
std::vector<WaterSurface> surfaces; std::vector<WaterSurface> surfaces;
bool renderingEnabled = true; bool renderingEnabled = true;
bool refractionEnabled = false;
}; };
} // namespace rendering } // namespace rendering

View file

@ -87,6 +87,7 @@ private:
bool pendingVsync = false; bool pendingVsync = false;
int pendingResIndex = 0; int pendingResIndex = 0;
bool pendingShadows = true; bool pendingShadows = true;
bool pendingWaterRefraction = false;
int pendingMasterVolume = 100; int pendingMasterVolume = 100;
int pendingMusicVolume = 30; int pendingMusicVolume = 30;
int pendingAmbientVolume = 100; int pendingAmbientVolume = 100;
@ -123,6 +124,7 @@ private:
bool minimapSettingsApplied_ = false; bool minimapSettingsApplied_ = false;
bool volumeSettingsApplied_ = false; // True once saved volume settings applied to audio managers bool volumeSettingsApplied_ = false; // True once saved volume settings applied to audio managers
bool msaaSettingsApplied_ = false; // True once saved MSAA setting applied to renderer bool msaaSettingsApplied_ = false; // True once saved MSAA setting applied to renderer
bool waterRefractionApplied_ = false;
bool normalMapSettingsApplied_ = false; // True once saved normal map/POM settings applied bool normalMapSettingsApplied_ = false; // True once saved normal map/POM settings applied
// Mute state: mute bypasses master volume without touching slider values // Mute state: mute bypasses master volume without touching slider values

View file

@ -855,6 +855,14 @@ void Renderer::unregisterPreview(CharacterPreview* preview) {
} }
} }
void Renderer::setWaterRefractionEnabled(bool enabled) {
if (waterRenderer) waterRenderer->setRefractionEnabled(enabled);
}
bool Renderer::isWaterRefractionEnabled() const {
return waterRenderer && waterRenderer->isRefractionEnabled();
}
void Renderer::setMsaaSamples(VkSampleCountFlagBits samples) { void Renderer::setMsaaSamples(VkSampleCountFlagBits samples) {
if (!vkCtx) return; if (!vkCtx) return;
@ -1054,20 +1062,27 @@ void Renderer::endFrame() {
vkCmdEndRenderPass(currentCmd); vkCmdEndRenderPass(currentCmd);
// Scene-history capture is disabled: with MAX_FRAMES_IN_FLIGHT=2, the single uint32_t frame = vkCtx->getCurrentFrame();
// sceneColorImage can race between frame N-1's water shader read and frame N's
// transfer write, eventually causing VK_ERROR_DEVICE_LOST. Water renders
// without refraction until per-frame scene-history images are implemented.
// TODO: allocate per-frame sceneColor/Depth images to fix the race.
// Render water in separate 1x pass (without scene refraction for now) // Capture scene color/depth into per-frame history images for water refraction
if (waterRenderer && waterRenderer->isRefractionEnabled() && waterRenderer->hasSurfaces()
&& currentImageIndex < vkCtx->getSwapchainImages().size()) {
waterRenderer->captureSceneHistory(
currentCmd,
vkCtx->getSwapchainImages()[currentImageIndex],
vkCtx->getDepthCopySourceImage(),
vkCtx->getSwapchainExtent(),
vkCtx->isDepthCopySourceMsaa(),
frame);
}
// Render water in separate 1x pass after MSAA resolve + scene capture
bool waterDeferred = waterRenderer && waterRenderer->hasSurfaces() && waterRenderer->hasWater1xPass() bool waterDeferred = waterRenderer && waterRenderer->hasSurfaces() && waterRenderer->hasWater1xPass()
&& vkCtx->getMsaaSamples() != VK_SAMPLE_COUNT_1_BIT; && vkCtx->getMsaaSamples() != VK_SAMPLE_COUNT_1_BIT;
if (waterDeferred && camera) { if (waterDeferred && camera) {
VkExtent2D ext = vkCtx->getSwapchainExtent(); VkExtent2D ext = vkCtx->getSwapchainExtent();
uint32_t frame = vkCtx->getCurrentFrame();
if (waterRenderer->beginWater1xPass(currentCmd, currentImageIndex, ext)) { if (waterRenderer->beginWater1xPass(currentCmd, currentImageIndex, ext)) {
waterRenderer->render(currentCmd, perFrameDescSets[frame], *camera, globalTime, true); waterRenderer->render(currentCmd, perFrameDescSets[frame], *camera, globalTime, true, frame);
waterRenderer->endWater1xPass(currentCmd); waterRenderer->endWater1xPass(currentCmd);
} }
} }
@ -3268,7 +3283,7 @@ void Renderer::renderWorld(game::World* world, game::GameHandler* gameHandler) {
bool waterDeferred = waterRenderer && waterRenderer->hasWater1xPass() bool waterDeferred = waterRenderer && waterRenderer->hasWater1xPass()
&& vkCtx->getMsaaSamples() != VK_SAMPLE_COUNT_1_BIT; && vkCtx->getMsaaSamples() != VK_SAMPLE_COUNT_1_BIT;
if (waterRenderer && camera && !waterDeferred) { if (waterRenderer && camera && !waterDeferred) {
waterRenderer->render(currentCmd, perFrameSet, *camera, globalTime); waterRenderer->render(currentCmd, perFrameSet, *camera, globalTime, false, vkCtx->getCurrentFrame());
} }
// Weather particles // Weather particles

View file

@ -118,15 +118,15 @@ bool WaterRenderer::initialize(VkContext* ctx, VkDescriptorSetLayout perFrameLay
return false; return false;
} }
// Pool needs 3 combined image samplers + 1 uniform buffer // Pool needs 3 combined image samplers + 1 uniform buffer per frame
std::array<VkDescriptorPoolSize, 2> scenePoolSizes{}; std::array<VkDescriptorPoolSize, 2> scenePoolSizes{};
scenePoolSizes[0].type = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; scenePoolSizes[0].type = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
scenePoolSizes[0].descriptorCount = 3; scenePoolSizes[0].descriptorCount = 3 * SCENE_HISTORY_FRAMES;
scenePoolSizes[1].type = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; scenePoolSizes[1].type = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER;
scenePoolSizes[1].descriptorCount = 1; scenePoolSizes[1].descriptorCount = SCENE_HISTORY_FRAMES;
VkDescriptorPoolCreateInfo scenePoolInfo{}; VkDescriptorPoolCreateInfo scenePoolInfo{};
scenePoolInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO; scenePoolInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO;
scenePoolInfo.maxSets = 1; scenePoolInfo.maxSets = SCENE_HISTORY_FRAMES;
scenePoolInfo.poolSizeCount = static_cast<uint32_t>(scenePoolSizes.size()); scenePoolInfo.poolSizeCount = static_cast<uint32_t>(scenePoolSizes.size());
scenePoolInfo.pPoolSizes = scenePoolSizes.data(); scenePoolInfo.pPoolSizes = scenePoolSizes.data();
if (vkCreateDescriptorPool(device, &scenePoolInfo, nullptr, &sceneDescPool) != VK_SUCCESS) { if (vkCreateDescriptorPool(device, &scenePoolInfo, nullptr, &sceneDescPool) != VK_SUCCESS) {
@ -267,6 +267,47 @@ void WaterRenderer::recreatePipelines() {
} }
} }
void WaterRenderer::setRefractionEnabled(bool enabled) {
if (refractionEnabled == enabled) return;
refractionEnabled = enabled;
// When turning off, clear scene history images to black so the shader
// detects "no data" and uses the non-refraction path.
if (!enabled && vkCtx) {
vkCtx->immediateSubmit([&](VkCommandBuffer cmd) {
for (uint32_t f = 0; f < SCENE_HISTORY_FRAMES; f++) {
auto& sh = sceneHistory[f];
if (!sh.colorImage) continue;
VkImageMemoryBarrier toTransfer{};
toTransfer.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER;
toTransfer.oldLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
toTransfer.newLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL;
toTransfer.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
toTransfer.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
toTransfer.image = sh.colorImage;
toTransfer.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1};
toTransfer.srcAccessMask = VK_ACCESS_SHADER_READ_BIT;
toTransfer.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
vkCmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT,
0, 0, nullptr, 0, nullptr, 1, &toTransfer);
VkClearColorValue clearColor = {{0.0f, 0.0f, 0.0f, 0.0f}};
VkImageSubresourceRange range = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1};
vkCmdClearColorImage(cmd, sh.colorImage, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, &clearColor, 1, &range);
VkImageMemoryBarrier toRead = toTransfer;
toRead.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL;
toRead.newLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
toRead.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
toRead.dstAccessMask = VK_ACCESS_SHADER_READ_BIT;
vkCmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT,
0, 0, nullptr, 0, nullptr, 1, &toRead);
}
});
}
}
void WaterRenderer::shutdown() { void WaterRenderer::shutdown() {
clear(); clear();
@ -304,13 +345,15 @@ VkDescriptorSet WaterRenderer::allocateMaterialSet() {
void WaterRenderer::destroySceneHistoryResources() { void WaterRenderer::destroySceneHistoryResources() {
if (!vkCtx) return; if (!vkCtx) return;
VkDevice device = vkCtx->getDevice(); VkDevice device = vkCtx->getDevice();
if (sceneColorView) { vkDestroyImageView(device, sceneColorView, nullptr); sceneColorView = VK_NULL_HANDLE; } for (auto& sh : sceneHistory) {
if (sceneDepthView) { vkDestroyImageView(device, sceneDepthView, nullptr); sceneDepthView = VK_NULL_HANDLE; } if (sh.colorView) { vkDestroyImageView(device, sh.colorView, nullptr); sh.colorView = VK_NULL_HANDLE; }
if (sceneColorImage) { vmaDestroyImage(vkCtx->getAllocator(), sceneColorImage, sceneColorAlloc); sceneColorImage = VK_NULL_HANDLE; sceneColorAlloc = VK_NULL_HANDLE; } if (sh.depthView) { vkDestroyImageView(device, sh.depthView, nullptr); sh.depthView = VK_NULL_HANDLE; }
if (sceneDepthImage) { vmaDestroyImage(vkCtx->getAllocator(), sceneDepthImage, sceneDepthAlloc); sceneDepthImage = VK_NULL_HANDLE; sceneDepthAlloc = VK_NULL_HANDLE; } if (sh.colorImage) { vmaDestroyImage(vkCtx->getAllocator(), sh.colorImage, sh.colorAlloc); sh.colorImage = VK_NULL_HANDLE; sh.colorAlloc = VK_NULL_HANDLE; }
if (sh.depthImage) { vmaDestroyImage(vkCtx->getAllocator(), sh.depthImage, sh.depthAlloc); sh.depthImage = VK_NULL_HANDLE; sh.depthAlloc = VK_NULL_HANDLE; }
sh.sceneSet = VK_NULL_HANDLE;
}
if (sceneColorSampler) { vkDestroySampler(device, sceneColorSampler, nullptr); sceneColorSampler = VK_NULL_HANDLE; } if (sceneColorSampler) { vkDestroySampler(device, sceneColorSampler, nullptr); sceneColorSampler = VK_NULL_HANDLE; }
if (sceneDepthSampler) { vkDestroySampler(device, sceneDepthSampler, nullptr); sceneDepthSampler = VK_NULL_HANDLE; } if (sceneDepthSampler) { vkDestroySampler(device, sceneDepthSampler, nullptr); sceneDepthSampler = VK_NULL_HANDLE; }
sceneSet = VK_NULL_HANDLE;
sceneHistoryExtent = {0, 0}; sceneHistoryExtent = {0, 0};
sceneHistoryReady = false; sceneHistoryReady = false;
} }
@ -323,54 +366,7 @@ void WaterRenderer::createSceneHistoryResources(VkExtent2D extent, VkFormat colo
vkResetDescriptorPool(device, sceneDescPool, 0); vkResetDescriptorPool(device, sceneDescPool, 0);
sceneHistoryExtent = extent; sceneHistoryExtent = extent;
VkImageCreateInfo colorImgInfo{}; // Create shared samplers
colorImgInfo.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO;
colorImgInfo.imageType = VK_IMAGE_TYPE_2D;
colorImgInfo.format = colorFormat;
colorImgInfo.extent = {extent.width, extent.height, 1};
colorImgInfo.mipLevels = 1;
colorImgInfo.arrayLayers = 1;
colorImgInfo.samples = VK_SAMPLE_COUNT_1_BIT;
colorImgInfo.tiling = VK_IMAGE_TILING_OPTIMAL;
colorImgInfo.usage = VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT;
colorImgInfo.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
VmaAllocationCreateInfo allocCI{};
allocCI.usage = VMA_MEMORY_USAGE_GPU_ONLY;
if (vmaCreateImage(vkCtx->getAllocator(), &colorImgInfo, &allocCI, &sceneColorImage, &sceneColorAlloc, nullptr) != VK_SUCCESS) {
LOG_ERROR("WaterRenderer: failed to create scene color history image");
return;
}
VkImageCreateInfo depthImgInfo = colorImgInfo;
depthImgInfo.format = depthFormat;
if (vmaCreateImage(vkCtx->getAllocator(), &depthImgInfo, &allocCI, &sceneDepthImage, &sceneDepthAlloc, nullptr) != VK_SUCCESS) {
LOG_ERROR("WaterRenderer: failed to create scene depth history image");
return;
}
VkImageViewCreateInfo colorViewInfo{};
colorViewInfo.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO;
colorViewInfo.image = sceneColorImage;
colorViewInfo.viewType = VK_IMAGE_VIEW_TYPE_2D;
colorViewInfo.format = colorFormat;
colorViewInfo.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
colorViewInfo.subresourceRange.levelCount = 1;
colorViewInfo.subresourceRange.layerCount = 1;
if (vkCreateImageView(device, &colorViewInfo, nullptr, &sceneColorView) != VK_SUCCESS) {
LOG_ERROR("WaterRenderer: failed to create scene color history view");
return;
}
VkImageViewCreateInfo depthViewInfo = colorViewInfo;
depthViewInfo.image = sceneDepthImage;
depthViewInfo.format = depthFormat;
depthViewInfo.subresourceRange.aspectMask = VK_IMAGE_ASPECT_DEPTH_BIT;
if (vkCreateImageView(device, &depthViewInfo, nullptr, &sceneDepthView) != VK_SUCCESS) {
LOG_ERROR("WaterRenderer: failed to create scene depth history view");
return;
}
VkSamplerCreateInfo sampCI{}; VkSamplerCreateInfo sampCI{};
sampCI.sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO; sampCI.sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO;
sampCI.magFilter = VK_FILTER_LINEAR; sampCI.magFilter = VK_FILTER_LINEAR;
@ -389,99 +385,155 @@ void WaterRenderer::createSceneHistoryResources(VkExtent2D extent, VkFormat colo
return; return;
} }
VkDescriptorSetAllocateInfo ai{}; VkImageCreateInfo colorImgInfo{};
ai.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO; colorImgInfo.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO;
ai.descriptorPool = sceneDescPool; colorImgInfo.imageType = VK_IMAGE_TYPE_2D;
ai.descriptorSetCount = 1; colorImgInfo.format = colorFormat;
ai.pSetLayouts = &sceneSetLayout; colorImgInfo.extent = {extent.width, extent.height, 1};
if (vkAllocateDescriptorSets(device, &ai, &sceneSet) != VK_SUCCESS) { colorImgInfo.mipLevels = 1;
LOG_ERROR("WaterRenderer: failed to allocate scene descriptor set"); colorImgInfo.arrayLayers = 1;
sceneSet = VK_NULL_HANDLE; colorImgInfo.samples = VK_SAMPLE_COUNT_1_BIT;
return; colorImgInfo.tiling = VK_IMAGE_TILING_OPTIMAL;
colorImgInfo.usage = VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT;
colorImgInfo.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
VkImageCreateInfo depthImgInfo = colorImgInfo;
depthImgInfo.format = depthFormat;
VmaAllocationCreateInfo allocCI{};
allocCI.usage = VMA_MEMORY_USAGE_GPU_ONLY;
// Create per-frame images, views, and descriptor sets
for (uint32_t f = 0; f < SCENE_HISTORY_FRAMES; f++) {
auto& sh = sceneHistory[f];
if (vmaCreateImage(vkCtx->getAllocator(), &colorImgInfo, &allocCI, &sh.colorImage, &sh.colorAlloc, nullptr) != VK_SUCCESS) {
LOG_ERROR("WaterRenderer: failed to create scene color history image [", f, "]");
return;
}
if (vmaCreateImage(vkCtx->getAllocator(), &depthImgInfo, &allocCI, &sh.depthImage, &sh.depthAlloc, nullptr) != VK_SUCCESS) {
LOG_ERROR("WaterRenderer: failed to create scene depth history image [", f, "]");
return;
}
VkImageViewCreateInfo colorViewInfo{};
colorViewInfo.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO;
colorViewInfo.image = sh.colorImage;
colorViewInfo.viewType = VK_IMAGE_VIEW_TYPE_2D;
colorViewInfo.format = colorFormat;
colorViewInfo.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
colorViewInfo.subresourceRange.levelCount = 1;
colorViewInfo.subresourceRange.layerCount = 1;
if (vkCreateImageView(device, &colorViewInfo, nullptr, &sh.colorView) != VK_SUCCESS) {
LOG_ERROR("WaterRenderer: failed to create scene color history view [", f, "]");
return;
}
VkImageViewCreateInfo depthViewInfo = colorViewInfo;
depthViewInfo.image = sh.depthImage;
depthViewInfo.format = depthFormat;
depthViewInfo.subresourceRange.aspectMask = VK_IMAGE_ASPECT_DEPTH_BIT;
if (vkCreateImageView(device, &depthViewInfo, nullptr, &sh.depthView) != VK_SUCCESS) {
LOG_ERROR("WaterRenderer: failed to create scene depth history view [", f, "]");
return;
}
// Allocate descriptor set for this frame
VkDescriptorSetAllocateInfo ai{};
ai.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO;
ai.descriptorPool = sceneDescPool;
ai.descriptorSetCount = 1;
ai.pSetLayouts = &sceneSetLayout;
if (vkAllocateDescriptorSets(device, &ai, &sh.sceneSet) != VK_SUCCESS) {
LOG_ERROR("WaterRenderer: failed to allocate scene descriptor set [", f, "]");
sh.sceneSet = VK_NULL_HANDLE;
return;
}
VkDescriptorImageInfo colorInfo{};
colorInfo.sampler = sceneColorSampler;
colorInfo.imageView = sh.colorView;
colorInfo.imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
VkDescriptorImageInfo depthInfo{};
depthInfo.sampler = sceneDepthSampler;
depthInfo.imageView = sh.depthView;
depthInfo.imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
VkDescriptorImageInfo reflColorInfo{};
reflColorInfo.sampler = sceneColorSampler;
reflColorInfo.imageView = reflectionColorView ? reflectionColorView : sh.colorView;
reflColorInfo.imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
VkDescriptorBufferInfo reflUBOInfo{};
reflUBOInfo.buffer = reflectionUBO;
reflUBOInfo.offset = 0;
reflUBOInfo.range = sizeof(ReflectionUBOData);
std::vector<VkWriteDescriptorSet> writes;
VkWriteDescriptorSet w0{};
w0.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
w0.dstSet = sh.sceneSet;
w0.dstBinding = 0;
w0.descriptorCount = 1;
w0.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
w0.pImageInfo = &colorInfo;
writes.push_back(w0);
VkWriteDescriptorSet w1{};
w1.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
w1.dstSet = sh.sceneSet;
w1.dstBinding = 1;
w1.descriptorCount = 1;
w1.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
w1.pImageInfo = &depthInfo;
writes.push_back(w1);
VkWriteDescriptorSet w2{};
w2.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
w2.dstSet = sh.sceneSet;
w2.dstBinding = 2;
w2.descriptorCount = 1;
w2.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
w2.pImageInfo = &reflColorInfo;
writes.push_back(w2);
if (reflectionUBO) {
VkWriteDescriptorSet w3{};
w3.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
w3.dstSet = sh.sceneSet;
w3.dstBinding = 3;
w3.descriptorCount = 1;
w3.descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER;
w3.pBufferInfo = &reflUBOInfo;
writes.push_back(w3);
}
vkUpdateDescriptorSets(device, static_cast<uint32_t>(writes.size()), writes.data(), 0, nullptr);
} }
VkDescriptorImageInfo colorInfo{}; // Initialize all per-frame history images to shader-read layout
colorInfo.sampler = sceneColorSampler;
colorInfo.imageView = sceneColorView;
colorInfo.imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
VkDescriptorImageInfo depthInfo{};
depthInfo.sampler = sceneDepthSampler;
depthInfo.imageView = sceneDepthView;
depthInfo.imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
// Reflection color texture (binding 2) — use scene color as placeholder until reflection is created
VkDescriptorImageInfo reflColorInfo{};
reflColorInfo.sampler = sceneColorSampler;
reflColorInfo.imageView = reflectionColorView ? reflectionColorView : sceneColorView;
reflColorInfo.imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
// Reflection UBO (binding 3)
VkDescriptorBufferInfo reflUBOInfo{};
reflUBOInfo.buffer = reflectionUBO;
reflUBOInfo.offset = 0;
reflUBOInfo.range = sizeof(ReflectionUBOData);
// Write bindings 0,1 always; write 2,3 only if reflection resources exist
std::vector<VkWriteDescriptorSet> writes;
VkWriteDescriptorSet w0{};
w0.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
w0.dstSet = 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 = 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 = 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 = sceneSet;
w3.dstBinding = 3;
w3.descriptorCount = 1;
w3.descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER;
w3.pBufferInfo = &reflUBOInfo;
writes.push_back(w3);
}
vkUpdateDescriptorSets(device, static_cast<uint32_t>(writes.size()), writes.data(), 0, nullptr);
// Initialize history images to shader-read layout so first frame samples are defined.
vkCtx->immediateSubmit([&](VkCommandBuffer cmd) { vkCtx->immediateSubmit([&](VkCommandBuffer cmd) {
VkImageMemoryBarrier barriers[2]{}; std::vector<VkImageMemoryBarrier> barriers;
barriers[0].sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER; for (uint32_t f = 0; f < SCENE_HISTORY_FRAMES; f++) {
barriers[0].oldLayout = VK_IMAGE_LAYOUT_UNDEFINED; VkImageMemoryBarrier b{};
barriers[0].newLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; b.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER;
barriers[0].srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; b.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED;
barriers[0].dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; b.newLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
barriers[0].image = sceneColorImage; b.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
barriers[0].subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}; b.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
barriers[0].dstAccessMask = VK_ACCESS_SHADER_READ_BIT; b.image = sceneHistory[f].colorImage;
b.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1};
barriers[1] = barriers[0]; b.dstAccessMask = VK_ACCESS_SHADER_READ_BIT;
barriers[1].image = sceneDepthImage; barriers.push_back(b);
barriers[1].subresourceRange.aspectMask = VK_IMAGE_ASPECT_DEPTH_BIT;
b.image = sceneHistory[f].depthImage;
b.subresourceRange.aspectMask = VK_IMAGE_ASPECT_DEPTH_BIT;
barriers.push_back(b);
}
vkCmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, vkCmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT,
0, 0, nullptr, 0, nullptr, 2, barriers); 0, 0, nullptr, 0, nullptr, static_cast<uint32_t>(barriers.size()), barriers.data());
}); });
} }
@ -986,7 +1038,7 @@ void WaterRenderer::clear() {
// ============================================================== // ==============================================================
void WaterRenderer::render(VkCommandBuffer cmd, VkDescriptorSet perFrameSet, void WaterRenderer::render(VkCommandBuffer cmd, VkDescriptorSet perFrameSet,
const Camera& /*camera*/, float /*time*/, bool use1x) { const Camera& /*camera*/, float /*time*/, bool use1x, uint32_t frameIndex) {
VkPipeline pipeline = (use1x && water1xPipeline) ? water1xPipeline : waterPipeline; VkPipeline pipeline = (use1x && water1xPipeline) ? water1xPipeline : waterPipeline;
if (!renderingEnabled || surfaces.empty() || !pipeline) { if (!renderingEnabled || surfaces.empty() || !pipeline) {
if (renderDiagCounter_++ % 300 == 0 && !surfaces.empty()) { if (renderDiagCounter_++ % 300 == 0 && !surfaces.empty()) {
@ -997,7 +1049,9 @@ void WaterRenderer::render(VkCommandBuffer cmd, VkDescriptorSet perFrameSet,
} }
return; return;
} }
if (!sceneSet) { uint32_t fi = frameIndex % SCENE_HISTORY_FRAMES;
VkDescriptorSet activeSceneSet = sceneHistory[fi].sceneSet;
if (!activeSceneSet) {
if (renderDiagCounter_++ % 300 == 0) { if (renderDiagCounter_++ % 300 == 0) {
LOG_WARNING("Water: render skipped — sceneSet is null, surfaces=", surfaces.size()); LOG_WARNING("Water: render skipped — sceneSet is null, surfaces=", surfaces.size());
} }
@ -1009,7 +1063,7 @@ void WaterRenderer::render(VkCommandBuffer cmd, VkDescriptorSet perFrameSet,
vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, pipelineLayout, vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, pipelineLayout,
0, 1, &perFrameSet, 0, nullptr); 0, 1, &perFrameSet, 0, nullptr);
vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, pipelineLayout, vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, pipelineLayout,
2, 1, &sceneSet, 0, nullptr); 2, 1, &activeSceneSet, 0, nullptr);
for (const auto& surface : surfaces) { for (const auto& surface : surfaces) {
if (surface.vertexBuffer == VK_NULL_HANDLE || surface.indexCount == 0) continue; if (surface.vertexBuffer == VK_NULL_HANDLE || surface.indexCount == 0) continue;
@ -1050,8 +1104,11 @@ void WaterRenderer::captureSceneHistory(VkCommandBuffer cmd,
VkImage srcColorImage, VkImage srcColorImage,
VkImage srcDepthImage, VkImage srcDepthImage,
VkExtent2D srcExtent, VkExtent2D srcExtent,
bool srcDepthIsMsaa) { bool srcDepthIsMsaa,
if (!vkCtx || !cmd || !sceneColorImage || !sceneDepthImage || srcExtent.width == 0 || srcExtent.height == 0) { uint32_t frameIndex) {
uint32_t fi = frameIndex % SCENE_HISTORY_FRAMES;
auto& sh = sceneHistory[fi];
if (!vkCtx || !cmd || !sh.colorImage || !sh.depthImage || srcExtent.width == 0 || srcExtent.height == 0) {
return; return;
} }
@ -1091,7 +1148,7 @@ void WaterRenderer::captureSceneHistory(VkCommandBuffer cmd,
VK_IMAGE_LAYOUT_PRESENT_SRC_KHR, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, VK_IMAGE_LAYOUT_PRESENT_SRC_KHR, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
0, VK_ACCESS_TRANSFER_READ_BIT, 0, VK_ACCESS_TRANSFER_READ_BIT,
VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT); VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT);
barrier2(sceneColorImage, VK_IMAGE_ASPECT_COLOR_BIT, barrier2(sh.colorImage, VK_IMAGE_ASPECT_COLOR_BIT,
VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
VK_ACCESS_SHADER_READ_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT, VK_ACCESS_TRANSFER_WRITE_BIT,
VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT); VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT);
@ -1101,9 +1158,9 @@ void WaterRenderer::captureSceneHistory(VkCommandBuffer cmd,
colorCopy.dstSubresource = {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}; colorCopy.extent = {copyExtent.width, copyExtent.height, 1};
vkCmdCopyImage(cmd, srcColorImage, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, vkCmdCopyImage(cmd, srcColorImage, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
sceneColorImage, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, &colorCopy); sh.colorImage, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, &colorCopy);
barrier2(sceneColorImage, VK_IMAGE_ASPECT_COLOR_BIT, barrier2(sh.colorImage, VK_IMAGE_ASPECT_COLOR_BIT,
VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL,
VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT,
VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT); VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT);
@ -1118,7 +1175,7 @@ void WaterRenderer::captureSceneHistory(VkCommandBuffer cmd,
VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, 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_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT,
VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT); VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT);
barrier2(sceneDepthImage, VK_IMAGE_ASPECT_DEPTH_BIT, barrier2(sh.depthImage, VK_IMAGE_ASPECT_DEPTH_BIT,
VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
VK_ACCESS_SHADER_READ_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT, VK_ACCESS_TRANSFER_WRITE_BIT,
VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT); VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT);
@ -1128,9 +1185,9 @@ void WaterRenderer::captureSceneHistory(VkCommandBuffer cmd,
depthCopy.dstSubresource = {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}; depthCopy.extent = {copyExtent.width, copyExtent.height, 1};
vkCmdCopyImage(cmd, srcDepthImage, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, vkCmdCopyImage(cmd, srcDepthImage, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
sceneDepthImage, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, &depthCopy); sh.depthImage, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, &depthCopy);
barrier2(sceneDepthImage, VK_IMAGE_ASPECT_DEPTH_BIT, barrier2(sh.depthImage, VK_IMAGE_ASPECT_DEPTH_BIT,
VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL,
VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT,
VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT); VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT);
@ -1543,11 +1600,11 @@ bool WaterRenderer::isWmoWaterAt(float glX, float glY) const {
glm::vec4 WaterRenderer::getLiquidColor(uint16_t liquidType) const { glm::vec4 WaterRenderer::getLiquidColor(uint16_t liquidType) const {
uint8_t basicType = (liquidType == 0) ? 0 : ((liquidType - 1) % 4); uint8_t basicType = (liquidType == 0) ? 0 : ((liquidType - 1) % 4);
switch (basicType) { switch (basicType) {
case 0: return glm::vec4(0.12f, 0.32f, 0.48f, 1.0f); // inland: blue-green case 0: return glm::vec4(0.10f, 0.28f, 0.55f, 1.0f); // inland: richer blue
case 1: return glm::vec4(0.04f, 0.14f, 0.30f, 1.0f); // ocean: deep blue case 1: return glm::vec4(0.04f, 0.16f, 0.38f, 1.0f); // ocean: deep blue
case 2: return glm::vec4(0.9f, 0.3f, 0.05f, 1.0f); // magma case 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 case 3: return glm::vec4(0.2f, 0.6f, 0.1f, 1.0f); // slime
default: return glm::vec4(0.12f, 0.32f, 0.48f, 1.0f); default: return glm::vec4(0.10f, 0.28f, 0.55f, 1.0f);
} }
} }
@ -1815,21 +1872,28 @@ void WaterRenderer::endReflectionPass(VkCommandBuffer cmd) {
vkCmdEndRenderPass(cmd); vkCmdEndRenderPass(cmd);
reflectionColorLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; reflectionColorLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
// Update scene descriptor set with the freshly rendered reflection texture // Update all per-frame scene descriptor sets with the freshly rendered reflection texture
if (sceneSet && reflectionColorView && reflectionSampler) { if (reflectionColorView && reflectionSampler) {
VkDescriptorImageInfo reflInfo{}; VkDescriptorImageInfo reflInfo{};
reflInfo.sampler = reflectionSampler; reflInfo.sampler = reflectionSampler;
reflInfo.imageView = reflectionColorView; reflInfo.imageView = reflectionColorView;
reflInfo.imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; reflInfo.imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
VkWriteDescriptorSet write{}; std::vector<VkWriteDescriptorSet> writes;
write.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; for (uint32_t f = 0; f < SCENE_HISTORY_FRAMES; f++) {
write.dstSet = sceneSet; if (!sceneHistory[f].sceneSet) continue;
write.dstBinding = 2; VkWriteDescriptorSet write{};
write.descriptorCount = 1; write.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
write.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; write.dstSet = sceneHistory[f].sceneSet;
write.pImageInfo = &reflInfo; write.dstBinding = 2;
vkUpdateDescriptorSets(vkCtx->getDevice(), 1, &write, 0, nullptr); 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);
}
} }
} }

View file

@ -288,6 +288,15 @@ void GameScreen::render(game::GameHandler& gameHandler) {
msaaSettingsApplied_ = true; msaaSettingsApplied_ = true;
} }
// Apply saved water refraction setting once when renderer is available
if (!waterRefractionApplied_) {
auto* renderer = core::Application::getInstance().getRenderer();
if (renderer) {
renderer->setWaterRefractionEnabled(pendingWaterRefraction);
waterRefractionApplied_ = true;
}
}
// Apply saved normal mapping / POM settings once when WMO renderer is available // Apply saved normal mapping / POM settings once when WMO renderer is available
if (!normalMapSettingsApplied_) { if (!normalMapSettingsApplied_) {
auto* renderer = core::Application::getInstance().getRenderer(); auto* renderer = core::Application::getInstance().getRenderer();
@ -6237,6 +6246,10 @@ void GameScreen::renderSettingsWindow() {
if (renderer) renderer->setShadowsEnabled(pendingShadows); if (renderer) renderer->setShadowsEnabled(pendingShadows);
saveSettings(); saveSettings();
} }
if (ImGui::Checkbox("Water Refraction", &pendingWaterRefraction)) {
if (renderer) renderer->setWaterRefractionEnabled(pendingWaterRefraction);
saveSettings();
}
{ {
const char* aaLabels[] = { "Off", "2x MSAA", "4x MSAA", "8x MSAA" }; const char* aaLabels[] = { "Off", "2x MSAA", "4x MSAA", "8x MSAA" };
if (ImGui::Combo("Anti-Aliasing", &pendingAntiAliasing, aaLabels, 4)) { if (ImGui::Combo("Anti-Aliasing", &pendingAntiAliasing, aaLabels, 4)) {
@ -6336,7 +6349,9 @@ void GameScreen::renderSettingsWindow() {
window->setFullscreen(pendingFullscreen); window->setFullscreen(pendingFullscreen);
window->setVsync(pendingVsync); window->setVsync(pendingVsync);
window->applyResolution(kResolutions[pendingResIndex][0], kResolutions[pendingResIndex][1]); window->applyResolution(kResolutions[pendingResIndex][0], kResolutions[pendingResIndex][1]);
pendingWaterRefraction = false;
if (renderer) renderer->setShadowsEnabled(pendingShadows); if (renderer) renderer->setShadowsEnabled(pendingShadows);
if (renderer) renderer->setWaterRefractionEnabled(pendingWaterRefraction);
if (renderer) renderer->setMsaaSamples(VK_SAMPLE_COUNT_1_BIT); if (renderer) renderer->setMsaaSamples(VK_SAMPLE_COUNT_1_BIT);
if (renderer) { if (renderer) {
if (auto* tm = renderer->getTerrainManager()) { if (auto* tm = renderer->getTerrainManager()) {
@ -7349,6 +7364,7 @@ void GameScreen::saveSettings() {
out << "auto_loot=" << (pendingAutoLoot ? 1 : 0) << "\n"; out << "auto_loot=" << (pendingAutoLoot ? 1 : 0) << "\n";
out << "ground_clutter_density=" << pendingGroundClutterDensity << "\n"; out << "ground_clutter_density=" << pendingGroundClutterDensity << "\n";
out << "shadows=" << (pendingShadows ? 1 : 0) << "\n"; out << "shadows=" << (pendingShadows ? 1 : 0) << "\n";
out << "water_refraction=" << (pendingWaterRefraction ? 1 : 0) << "\n";
out << "antialiasing=" << pendingAntiAliasing << "\n"; out << "antialiasing=" << pendingAntiAliasing << "\n";
out << "normal_mapping=" << (pendingNormalMapping ? 1 : 0) << "\n"; out << "normal_mapping=" << (pendingNormalMapping ? 1 : 0) << "\n";
out << "normal_map_strength=" << pendingNormalMapStrength << "\n"; out << "normal_map_strength=" << pendingNormalMapStrength << "\n";
@ -7433,6 +7449,7 @@ void GameScreen::loadSettings() {
else if (key == "auto_loot") pendingAutoLoot = (std::stoi(val) != 0); else if (key == "auto_loot") pendingAutoLoot = (std::stoi(val) != 0);
else if (key == "ground_clutter_density") pendingGroundClutterDensity = std::clamp(std::stoi(val), 0, 150); else if (key == "ground_clutter_density") pendingGroundClutterDensity = std::clamp(std::stoi(val), 0, 150);
else if (key == "shadows") pendingShadows = (std::stoi(val) != 0); else if (key == "shadows") pendingShadows = (std::stoi(val) != 0);
else if (key == "water_refraction") pendingWaterRefraction = (std::stoi(val) != 0);
else if (key == "antialiasing") pendingAntiAliasing = std::clamp(std::stoi(val), 0, 3); else if (key == "antialiasing") pendingAntiAliasing = std::clamp(std::stoi(val), 0, 3);
else if (key == "normal_mapping") pendingNormalMapping = (std::stoi(val) != 0); else if (key == "normal_mapping") pendingNormalMapping = (std::stoi(val) != 0);
else if (key == "normal_map_strength") pendingNormalMapStrength = std::clamp(std::stof(val), 0.0f, 2.0f); else if (key == "normal_map_strength") pendingNormalMapStrength = std::clamp(std::stof(val), 0.0f, 2.0f);