Files
WoWee/include/rendering/swim_effects.hpp
Kelsi db7dcb6c24 feat(water): froth and wake at the surface; draw spray on top of the water
Three separate faults were keeping the water's edge dry-looking.

The spray was being painted over. Water moved into a pass of its own today
so the refraction copy could be taken before it, but the swim effects still
recorded into the scene pass, which now runs first — so the droplets ended
up under the sheet. Shallow shore water hid them only partly, because its
alpha sits near the 0.15 floor; the deeper water you swim in hid them
completely. The spray now draws in the continuation pass right after the
water, with its pipelines built for that pass (single-sampled in both
arrangements) and a flag so a mode change cannot record them into a pass
they do not match.

The wading spray never spawned at all. It shared rippleSpawnAccum with the
swimming spray, whose else-branch zeroes that accumulator on every frame it
is not swimming — so a 30/s rate could only ever reach 0.5 in a frame and
never crossed the threshold. It has its own accumulator now. Its travel
direction was also rotated 90 degrees: forward from yaw is (cos, sin), as
the camera controller derives it, not (sin, -cos).

Neither of those puts froth on the water, though, which is what churned
water actually looks like. Wake points are laid down along the path and
handed to the water shader, where they read as aerated white water broken
up by the same cellular octaves the shoreline foam uses. Wading drops one
churned patch per stride; swimming emits a pair off the shoulders that
drift apart as they age, which is the V. Points age out, spread as they go,
and carry a bounding circle so every other water pixel on screen rejects
the trail in one test.
2026-07-31 12:23:47 -07:00

118 lines
3.7 KiB
C++

#pragma once
#include <vulkan/vulkan.h>
#include <vk_mem_alloc.h>
#include <glm/glm.hpp>
#include <vector>
namespace wowee {
namespace rendering {
class Camera;
class CameraController;
class WaterRenderer;
class M2Renderer;
class VkContext;
class SwimEffects {
public:
SwimEffects();
~SwimEffects();
[[nodiscard]] bool initialize(VkContext* ctx, VkDescriptorSetLayout perFrameLayout);
void shutdown();
void recreatePipelines();
/// Select the render pass these particles will be recorded into. Water now
/// draws after them in a pass of its own, so the spray has to move into that
/// pass too or the water sheet is painted straight over it. Call before
/// initialize() or recreatePipelines().
void setTargetPass(VkRenderPass pass, VkSampleCountFlagBits samples) {
targetPass_ = pass;
targetSamples_ = samples;
}
void update(const Camera& camera, const CameraController& cc,
const WaterRenderer& water, float deltaTime);
void render(VkCommandBuffer cmd, VkDescriptorSet perFrameSet);
void spawnFootSplash(const glm::vec3& footPos, float waterH);
void setM2Renderer(M2Renderer* renderer) { m2Renderer = renderer; }
private:
struct Particle {
glm::vec3 position;
glm::vec3 velocity;
float lifetime;
float maxLifetime;
float size;
float alpha;
};
struct InsectParticle {
glm::vec3 position;
glm::vec3 orbitCenter; // vegetation position to orbit around
float lifetime;
float maxLifetime;
float size;
float alpha;
float phase; // random phase offset for erratic motion
float orbitRadius;
float orbitSpeed;
float heightOffset; // height above plant
};
static constexpr int MAX_RIPPLE_PARTICLES = 200;
static constexpr int MAX_BUBBLE_PARTICLES = 150;
static constexpr int MAX_INSECT_PARTICLES = 50;
std::vector<Particle> ripples;
std::vector<Particle> bubbles;
std::vector<InsectParticle> insects;
// Vulkan objects
VkContext* vkCtx = nullptr;
M2Renderer* m2Renderer = nullptr;
// Ripple pipeline + dynamic buffer
VkPipeline ripplePipeline = VK_NULL_HANDLE;
VkPipelineLayout ripplePipelineLayout = VK_NULL_HANDLE;
::VkBuffer rippleDynamicVB = VK_NULL_HANDLE;
VmaAllocation rippleDynamicVBAlloc = VK_NULL_HANDLE;
VmaAllocationInfo rippleDynamicVBAllocInfo{};
VkDeviceSize rippleDynamicVBSize = 0;
// Bubble pipeline + dynamic buffer
VkPipeline bubblePipeline = VK_NULL_HANDLE;
VkPipelineLayout bubblePipelineLayout = VK_NULL_HANDLE;
::VkBuffer bubbleDynamicVB = VK_NULL_HANDLE;
VmaAllocation bubbleDynamicVBAlloc = VK_NULL_HANDLE;
VmaAllocationInfo bubbleDynamicVBAllocInfo{};
VkDeviceSize bubbleDynamicVBSize = 0;
// Insect pipeline + dynamic buffer
VkPipeline insectPipeline = VK_NULL_HANDLE;
VkPipelineLayout insectPipelineLayout = VK_NULL_HANDLE;
::VkBuffer insectDynamicVB = VK_NULL_HANDLE;
VmaAllocation insectDynamicVBAlloc = VK_NULL_HANDLE;
VmaAllocationInfo insectDynamicVBAllocInfo{};
VkDeviceSize insectDynamicVBSize = 0;
std::vector<float> rippleVertexData;
std::vector<float> bubbleVertexData;
std::vector<float> insectVertexData;
VkRenderPass targetPass_ = VK_NULL_HANDLE;
VkSampleCountFlagBits targetSamples_ = VK_SAMPLE_COUNT_1_BIT;
float wadeSpawnAccum = 0.0f;
float rippleSpawnAccum = 0.0f;
float bubbleSpawnAccum = 0.0f;
float insectSpawnAccum = 0.0f;
void spawnRipple(const glm::vec3& pos, const glm::vec3& moveDir, float waterH);
void spawnBubble(const glm::vec3& pos, float waterH);
void spawnInsect(const glm::vec3& vegPos);
};
} // namespace rendering
} // namespace wowee