Files
WoWee/include/rendering/water_renderer.hpp
Kelsi a3a6685d8b feat(water): draw water single-sampled under MSAA, out of the scene pass
Water is a large alpha-blended surface whose interior MSAA does nothing
for — it only pays for the samples. It also had to stay inside the
multisampled scene pass, which meant that under MSAA it was still inside
its own refraction copy: the ghost trains and brightness pumping fixed
for everyone else came back for anyone with anti-aliasing on.

Revive the 1x water pass that was retired when water moved into the main
pass. Under MSAA the frame is now scene (multisampled) → resolve → copy →
water at one sample into the resolved image → UI, matching what the
non-MSAA path already does through the scene continuation pass.

setupWater1xPass had no callers at all; it is now built on MSAA change
and rebuilt on swapchain resize, since its framebuffers reference the
swapchain views. It needs a resolved depth buffer to depth test against,
and the pass targets the swapchain directly, so water stays in the scene
pass when depth resolve is unavailable or post-processing renders the
scene off-screen.
2026-07-31 07:44:05 -07:00

228 lines
8.6 KiB
C++

#pragma once
#include <vector>
#include <memory>
#include <optional>
#include <cstdint>
#include <functional>
#include <vulkan/vulkan.h>
#include <vk_mem_alloc.h>
#include <glm/glm.hpp>
namespace wowee {
namespace pipeline {
struct ADTTerrain;
struct LiquidData;
struct WMOLiquid;
}
namespace rendering {
class Camera;
class VkContext;
/**
* Water surface for a single map chunk
*/
struct WaterSurface {
glm::vec3 position;
glm::vec3 origin;
glm::vec3 stepX;
glm::vec3 stepY;
float minHeight;
float maxHeight;
uint16_t liquidType;
int tileX = -1, tileY = -1;
uint32_t wmoId = 0;
uint8_t xOffset = 0;
uint8_t yOffset = 0;
uint8_t width = 8;
uint8_t height = 8;
std::vector<float> heights;
std::vector<uint8_t> mask;
// Vulkan render data
::VkBuffer vertexBuffer = VK_NULL_HANDLE;
VmaAllocation vertexAlloc = VK_NULL_HANDLE;
::VkBuffer indexBuffer = VK_NULL_HANDLE;
VmaAllocation indexAlloc = VK_NULL_HANDLE;
int indexCount = 0;
// Per-surface material UBO
::VkBuffer materialUBO = VK_NULL_HANDLE;
VmaAllocation materialAlloc = VK_NULL_HANDLE;
// Material descriptor set (set 1)
VkDescriptorSet materialSet = VK_NULL_HANDLE;
bool hasHeightData() const { return !heights.empty(); }
};
/**
* Water renderer (Vulkan) with planar reflections, Gerstner waves,
* GGX specular, shoreline foam, and subsurface scattering.
*/
class WaterRenderer {
public:
WaterRenderer();
~WaterRenderer();
bool initialize(VkContext* ctx, VkDescriptorSetLayout perFrameLayout);
void shutdown();
void loadFromTerrain(const pipeline::ADTTerrain& terrain, bool append = false,
int tileX = -1, int tileY = -1);
void loadFromWMO(const pipeline::WMOLiquid& liquid, const glm::mat4& modelMatrix, uint32_t wmoId);
void removeWMO(uint32_t wmoId);
void removeTile(int tileX, int tileY);
void clear();
void recreatePipelines();
// Separate 1x pass for MSAA mode — water rendered after MSAA resolve
bool createWater1xPass(VkFormat colorFormat, VkFormat depthFormat);
void createWater1xFramebuffers(const std::vector<VkImageView>& swapViews,
VkImageView depthView, VkExtent2D extent);
void destroyWater1xResources();
bool beginWater1xPass(VkCommandBuffer cmd, uint32_t imageIndex, VkExtent2D extent);
void endWater1xPass(VkCommandBuffer cmd);
bool hasWater1xPass() const { return water1xRenderPass != VK_NULL_HANDLE; }
VkRenderPass getWater1xRenderPass() const { return water1xRenderPass; }
VkFramebuffer getWater1xFramebuffer(uint32_t index) const {
return index < water1xFramebuffers.size() ? water1xFramebuffers[index] : VK_NULL_HANDLE;
}
void render(VkCommandBuffer cmd, VkDescriptorSet perFrameSet, const Camera& camera, float time, bool use1x = false, uint32_t frameIndex = 0);
void captureSceneHistory(VkCommandBuffer cmd,
VkImage srcColorImage,
VkImage srcDepthImage,
VkExtent2D srcExtent,
bool srcDepthIsMsaa,
uint32_t frameIndex = 0);
// --- Planar reflection pass ---
// Call sequence: beginReflectionPass → [render scene] → endReflectionPass
bool beginReflectionPass(VkCommandBuffer cmd);
void endReflectionPass(VkCommandBuffer cmd);
// Get the dominant water height near a position (for reflection plane)
std::optional<float> getDominantWaterHeight(const glm::vec3& cameraPos) const;
// Compute reflected view matrix for a given water height
static glm::mat4 computeReflectedView(const Camera& camera, float waterHeight);
// Compute oblique clip projection to clip below-water geometry in reflection
static glm::mat4 computeObliqueProjection(const glm::mat4& proj, const glm::mat4& view, float waterHeight);
// Update the reflection UBO with reflected viewProj matrix
void updateReflectionUBO(const glm::mat4& reflViewProj);
VkRenderPass getReflectionRenderPass() const { return reflectionRenderPass; }
VkExtent2D getReflectionExtent() const { return {REFLECTION_WIDTH, REFLECTION_HEIGHT}; }
bool hasReflectionPass() const { return reflectionRenderPass != VK_NULL_HANDLE; }
bool hasSurfaces() const { return !surfaces.empty(); }
void setEnabled(bool enabled) { renderingEnabled = enabled; }
bool isEnabled() const { return renderingEnabled; }
void setRefractionEnabled(bool enabled);
bool isRefractionEnabled() const { return refractionEnabled; }
// Display brightness (1.0 = neutral). The scene-history capture used for
// refraction bakes this in, so the shader divides it back out.
// Size of the target the water is drawn into. Screen-space lookups derive
// their UVs from this rather than from the refraction texture's own size,
// which is deliberately smaller than the frame.
void setRenderExtent(VkExtent2D e) { renderExtent_ = e; }
std::optional<float> getWaterHeightAt(float glX, float glY) const;
/// Like getWaterHeightAt but only returns water surfaces whose height is
/// close to the query Z (within maxAbove units above). Avoids false
/// underwater detection from elevated WMO water far above the camera.
std::optional<float> getNearestWaterHeightAt(float glX, float glY, float queryZ, float maxAbove = 15.0f) const;
std::optional<uint16_t> getWaterTypeAt(float glX, float glY) const;
bool isWmoWaterAt(float glX, float glY) const;
int getSurfaceCount() const { return static_cast<int>(surfaces.size()); }
private:
void createWaterMesh(WaterSurface& surface);
void destroyWaterMesh(WaterSurface& surface);
glm::vec4 getLiquidColor(uint16_t liquidType) const;
float getLiquidAlpha(uint16_t liquidType) const;
void updateMaterialUBO(WaterSurface& surface);
VkDescriptorSet allocateMaterialSet();
VkExtent2D refractionCaptureExtent() const;
void createSceneHistoryResources(VkExtent2D extent, VkFormat colorFormat, VkFormat depthFormat);
void destroySceneHistoryResources();
// Reflection pass resources
void createReflectionResources();
void destroyReflectionResources();
VkContext* vkCtx = nullptr;
// Pipeline
VkPipeline waterPipeline = VK_NULL_HANDLE;
VkPipelineLayout pipelineLayout = VK_NULL_HANDLE;
VkDescriptorSetLayout materialSetLayout = VK_NULL_HANDLE;
VkDescriptorPool materialDescPool = VK_NULL_HANDLE;
VkDescriptorSetLayout sceneSetLayout = VK_NULL_HANDLE;
VkDescriptorPool sceneDescPool = VK_NULL_HANDLE;
static constexpr uint32_t MAX_WATER_SETS = 16384;
VkSampler sceneColorSampler = VK_NULL_HANDLE;
VkSampler sceneDepthSampler = VK_NULL_HANDLE;
// Per-frame scene history to avoid race between frames in flight
static constexpr uint32_t SCENE_HISTORY_FRAMES = 2;
struct PerFrameSceneHistory {
VkImage colorImage = VK_NULL_HANDLE;
VmaAllocation colorAlloc = 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};
bool sceneHistoryReady = false;
mutable uint32_t renderDiagCounter_ = 0;
// Planar reflection resources
static constexpr uint32_t REFLECTION_WIDTH = 512;
static constexpr uint32_t REFLECTION_HEIGHT = 512;
VkRenderPass reflectionRenderPass = VK_NULL_HANDLE;
VkFramebuffer reflectionFramebuffer = VK_NULL_HANDLE;
VkImage reflectionColorImage = VK_NULL_HANDLE;
VmaAllocation reflectionColorAlloc = VK_NULL_HANDLE;
VkImageView reflectionColorView = VK_NULL_HANDLE;
VkImage reflectionDepthImage = VK_NULL_HANDLE;
VmaAllocation reflectionDepthAlloc = VK_NULL_HANDLE;
VkImageView reflectionDepthView = VK_NULL_HANDLE;
VkSampler reflectionSampler = VK_NULL_HANDLE;
VkImageLayout reflectionColorLayout = VK_IMAGE_LAYOUT_UNDEFINED;
// Reflection UBO (mat4 reflViewProj)
::VkBuffer reflectionUBO = VK_NULL_HANDLE;
VmaAllocation reflectionUBOAlloc = VK_NULL_HANDLE;
void* reflectionUBOMapped = nullptr;
// Separate 1x water pass (used when MSAA is active)
VkRenderPass water1xRenderPass = VK_NULL_HANDLE;
VkPipeline water1xPipeline = VK_NULL_HANDLE;
std::vector<VkFramebuffer> water1xFramebuffers;
std::vector<WaterSurface> surfaces;
bool renderingEnabled = true;
bool refractionEnabled = false;
VkExtent2D renderExtent_{0, 0};
};
} // namespace rendering
} // namespace wowee