diff --git a/assets/shaders/swim_insect.frag.glsl b/assets/shaders/swim_insect.frag.glsl new file mode 100644 index 00000000..06ab430a --- /dev/null +++ b/assets/shaders/swim_insect.frag.glsl @@ -0,0 +1,14 @@ +#version 450 + +layout(location = 0) in float vAlpha; + +layout(location = 0) out vec4 outColor; + +void main() { + vec2 p = gl_PointCoord - vec2(0.5); + float dist = length(p); + if (dist > 0.5) discard; + float alpha = smoothstep(0.5, 0.05, dist) * vAlpha; + // Dark brown/black insect color + outColor = vec4(0.12, 0.08, 0.05, alpha); +} diff --git a/assets/shaders/swim_insect.frag.spv b/assets/shaders/swim_insect.frag.spv new file mode 100644 index 00000000..6e849c37 Binary files /dev/null and b/assets/shaders/swim_insect.frag.spv differ diff --git a/assets/shaders/water.frag.glsl b/assets/shaders/water.frag.glsl index 73ddf5fc..ecd7ee1d 100644 --- a/assets/shaders/water.frag.glsl +++ b/assets/shaders/water.frag.glsl @@ -130,20 +130,26 @@ float fbmNoise(vec2 p, float time) { } // Voronoi-like cellular noise for foam particles -float cellularFoam(vec2 p) { +// jitter parameter controls how much cell points deviate from grid centers +// (0.0 = regular grid, 1.0 = fully random within cell) +float cellularFoam(vec2 p, float jitter) { vec2 i = floor(p); vec2 f = fract(p); float minDist = 1.0; for (int y = -1; y <= 1; y++) { for (int x = -1; x <= 1; x++) { vec2 neighbor = vec2(float(x), float(y)); - vec2 point = vec2(hash21(i + neighbor), hash22x(i + neighbor)); + vec2 cellId = i + neighbor; + // Jittered cell point — higher jitter = more irregular placement + vec2 point = vec2(hash21(cellId), hash22x(cellId)) * jitter + + vec2(0.5) * (1.0 - jitter); float d = length(neighbor + point - f); minDist = min(minDist, d); } } return minDist; } +float cellularFoam(vec2 p) { return cellularFoam(p, 1.0); } void main() { float time = fogParams.z; @@ -299,24 +305,32 @@ void main() { if (basicType < 1.5 && verticalDepth > 0.01 && push.waveAmp > 0.0) { float foamDepthMask = 1.0 - smoothstep(0.0, 1.8, verticalDepth); + // Warp UV coords with noise to break up cellular regularity + vec2 warpOffset = vec2( + noiseValue(FragPos.xy * 2.5 + time * 0.08) - 0.5, + noiseValue(FragPos.xy * 2.5 + vec2(37.0) + time * 0.06) - 0.5 + ) * 1.6; + vec2 foamUV = FragPos.xy + warpOffset; + // Fine scattered particles - float cells1 = cellularFoam(FragPos.xy * 14.0 + time * vec2(0.15, 0.08)); - float foam1 = (1.0 - smoothstep(0.0, 0.10, cells1)) * 0.5; + float cells1 = cellularFoam(foamUV * 14.0 + time * vec2(0.15, 0.08)); + float foam1 = (1.0 - smoothstep(0.0, 0.12, cells1)) * 0.45; // Tiny spray dots - float cells2 = cellularFoam(FragPos.xy * 30.0 + time * vec2(-0.12, 0.22)); - float foam2 = (1.0 - smoothstep(0.0, 0.06, cells2)) * 0.35; + float cells2 = cellularFoam(foamUV * 28.0 + time * vec2(-0.12, 0.22)); + float foam2 = (1.0 - smoothstep(0.0, 0.07, cells2)) * 0.3; // Micro specks - float cells3 = cellularFoam(FragPos.xy * 55.0 + time * vec2(0.25, -0.1)); - float foam3 = (1.0 - smoothstep(0.0, 0.04, cells3)) * 0.2; + float cells3 = cellularFoam(foamUV * 50.0 + time * vec2(0.25, -0.1)); + float foam3 = (1.0 - smoothstep(0.0, 0.05, cells3)) * 0.18; // Noise breakup for clumping float noiseMask = noiseValue(FragPos.xy * 3.0 + time * 0.15); float foam = (foam1 + foam2 + foam3) * foamDepthMask * smoothstep(0.3, 0.6, noiseMask); foam *= smoothstep(0.0, 0.1, verticalDepth); - color = mix(color, vec3(0.92, 0.95, 0.98), clamp(foam, 0.0, 0.45)); + // Bluer foam tint instead of near-white + color = mix(color, vec3(0.68, 0.78, 0.88), clamp(foam, 0.0, 0.40)); } // ============================================================ @@ -324,11 +338,15 @@ void main() { // ============================================================ if (basicType > 0.5 && basicType < 1.5 && push.waveAmp > 0.0) { float crestMask = smoothstep(0.5, 1.0, WaveOffset); - float crestCells = cellularFoam(FragPos.xy * 6.0 + time * vec2(0.12, 0.08)); + vec2 crestWarp = vec2( + noiseValue(FragPos.xy * 1.8 + time * 0.1) - 0.5, + noiseValue(FragPos.xy * 1.8 + vec2(53.0) + time * 0.07) - 0.5 + ) * 2.0; + float crestCells = cellularFoam((FragPos.xy + crestWarp) * 6.0 + time * vec2(0.12, 0.08)); float crestFoam = (1.0 - smoothstep(0.0, 0.18, crestCells)) * crestMask; float crestNoise = noiseValue(FragPos.xy * 3.0 - time * 0.3); crestFoam *= smoothstep(0.3, 0.6, crestNoise); - color = mix(color, vec3(0.92, 0.95, 0.98), crestFoam * 0.35); + color = mix(color, vec3(0.68, 0.78, 0.88), crestFoam * 0.30); } // ============================================================ diff --git a/assets/shaders/water.frag.spv b/assets/shaders/water.frag.spv index c437d847..5f1d56b1 100644 Binary files a/assets/shaders/water.frag.spv and b/assets/shaders/water.frag.spv differ diff --git a/include/rendering/m2_renderer.hpp b/include/rendering/m2_renderer.hpp index 784f357e..c2a663b5 100644 --- a/include/rendering/m2_renderer.hpp +++ b/include/rendering/m2_renderer.hpp @@ -76,6 +76,8 @@ struct M2ModelGPU { bool isSmallFoliage = false; // Small foliage (bushes, grass, plants) - skip during taxi bool isInvisibleTrap = false; // Invisible trap objects (don't render, no collision) bool isGroundDetail = false; // Ground clutter/detail doodads (special fallback render path) + bool isWaterVegetation = false; // Cattails, reeds, kelp etc. near water (insect spawning) + bool isFireflyEffect = false; // Firefly/fireflies M2 (exempt from particle dampeners) // Collision mesh with spatial grid (from M2 bounding geometry) struct CollisionMesh { @@ -307,6 +309,8 @@ public: void setInsideInterior(bool inside) { insideInterior = inside; } void setOnTaxi(bool onTaxi) { onTaxi_ = onTaxi; } + std::vector getWaterVegetationPositions(const glm::vec3& camPos, float maxDist) const; + private: bool initialized_ = false; bool insideInterior = false; diff --git a/include/rendering/swim_effects.hpp b/include/rendering/swim_effects.hpp index 15858258..20d63176 100644 --- a/include/rendering/swim_effects.hpp +++ b/include/rendering/swim_effects.hpp @@ -11,6 +11,7 @@ namespace rendering { class Camera; class CameraController; class WaterRenderer; +class M2Renderer; class VkContext; class SwimEffects { @@ -25,6 +26,7 @@ public: 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 { @@ -36,14 +38,30 @@ private: 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 ripples; std::vector bubbles; + std::vector insects; // Vulkan objects VkContext* vkCtx = nullptr; + M2Renderer* m2Renderer = nullptr; // Ripple pipeline + dynamic buffer VkPipeline ripplePipeline = VK_NULL_HANDLE; @@ -61,14 +79,25 @@ private: 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 rippleVertexData; std::vector bubbleVertexData; + std::vector insectVertexData; 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 diff --git a/src/rendering/m2_renderer.cpp b/src/rendering/m2_renderer.cpp index f41390f3..ab53532e 100644 --- a/src/rendering/m2_renderer.cpp +++ b/src/rendering/m2_renderer.cpp @@ -1105,6 +1105,19 @@ bool M2Renderer::loadModel(const pipeline::M2Model& model, uint32_t modelId) { // Spell effect models: particle-dominated with minimal geometry (e.g. LevelUp.m2) gpuModel.isSpellEffect = hasParticles && model.vertices.size() <= 200 && model.particleEmitters.size() >= 3; + // Water vegetation: cattails, reeds, bulrushes, kelp, seaweed, lilypad near water + gpuModel.isWaterVegetation = + (lowerName.find("cattail") != std::string::npos) || + (lowerName.find("reed") != std::string::npos) || + (lowerName.find("bulrush") != std::string::npos) || + (lowerName.find("seaweed") != std::string::npos) || + (lowerName.find("kelp") != std::string::npos) || + (lowerName.find("lilypad") != std::string::npos); + // Firefly effect models: particle-based ambient glow (exempt from dampeners) + gpuModel.isFireflyEffect = + (lowerName.find("firefly") != std::string::npos) || + (lowerName.find("fireflies") != std::string::npos) || + (lowerName.find("fireflys") != std::string::npos); // Build collision mesh + spatial grid from M2 bounding geometry gpuModel.collision.vertices = model.collisionVertices; @@ -2803,6 +2816,20 @@ glm::vec3 M2Renderer::interpFBlockVec3(const pipeline::M2FBlock& fb, float lifeR return fb.vec3Values.back(); } +std::vector M2Renderer::getWaterVegetationPositions(const glm::vec3& camPos, float maxDist) const { + std::vector result; + float maxDistSq = maxDist * maxDist; + for (const auto& inst : instances) { + auto it = models.find(inst.modelId); + if (it == models.end() || !it->second.isWaterVegetation) continue; + glm::vec3 diff = inst.position - camPos; + if (glm::dot(diff, diff) <= maxDistSq) { + result.push_back(inst.position); + } + } + return result; +} + void M2Renderer::emitParticles(M2Instance& inst, const M2ModelGPU& gpu, float dt) { if (inst.emitterAccumulators.size() != gpu.particleEmitters.size()) { inst.emitterAccumulators.resize(gpu.particleEmitters.size(), 0.0f); @@ -2867,11 +2894,20 @@ void M2Renderer::emitParticles(M2Instance& inst, const M2ModelGPU& gpu, float dt // particles pile up at the same position. Give them a drift so they // spread outward like a mist/spray effect instead of clustering. if (std::abs(speed) < 0.01f) { - p.velocity = rotMat * glm::vec3( - distN(particleRng_) * 1.0f, - distN(particleRng_) * 1.0f, - -dist01(particleRng_) * 0.5f - ); + if (gpu.isFireflyEffect) { + // Fireflies: gentle random drift in all directions + p.velocity = rotMat * glm::vec3( + distN(particleRng_) * 0.6f, + distN(particleRng_) * 0.6f, + distN(particleRng_) * 0.3f + ); + } else { + p.velocity = rotMat * glm::vec3( + distN(particleRng_) * 1.0f, + distN(particleRng_) * 1.0f, + -dist01(particleRng_) * 0.5f + ); + } } const uint32_t tilesX = std::max(em.textureCols, 1); @@ -2917,7 +2953,8 @@ void M2Renderer::updateParticles(M2Instance& inst, float dt) { gpu.sequences, gpu.globalSequenceDurations); // When M2 gravity is 0, apply default gravity so particles arc downward. // Many fountain M2s rely on bone animation (.anim files) we don't load yet. - if (grav == 0.0f) { + // Firefly/ambient glow particles intentionally have zero gravity — skip fallback. + if (grav == 0.0f && !gpu.isFireflyEffect) { float emSpeed = interpFloat(pem.emissionSpeed, inst.animTime, inst.currentSequenceIndex, gpu.sequences, gpu.globalSequenceDurations); @@ -2985,12 +3022,12 @@ void M2Renderer::renderM2Particles(VkCommandBuffer cmd, VkDescriptorSet perFrame float alpha = std::min(interpFBlockFloat(em.particleAlpha, lifeRatio), 1.0f); float rawScale = interpFBlockFloat(em.particleScale, lifeRatio); - if (!gpu.isSpellEffect) { + if (!gpu.isSpellEffect && !gpu.isFireflyEffect) { color = glm::mix(color, glm::vec3(1.0f), 0.7f); if (rawScale > 2.0f) alpha *= 0.02f; if (em.blendingType == 3 || em.blendingType == 4) alpha *= 0.05f; } - float scale = gpu.isSpellEffect ? rawScale : std::min(rawScale, 1.5f); + float scale = (gpu.isSpellEffect || gpu.isFireflyEffect) ? rawScale : std::min(rawScale, 1.5f); VkTexture* tex = whiteTexture_.get(); if (p.emitterIndex < static_cast(gpu.particleTextures.size())) { diff --git a/src/rendering/renderer.cpp b/src/rendering/renderer.cpp index ba26f5bb..bd66a99a 100644 --- a/src/rendering/renderer.cpp +++ b/src/rendering/renderer.cpp @@ -3359,6 +3359,9 @@ bool Renderer::loadTestTerrain(pipeline::AssetManager* assetManager, const std:: if (!m2Renderer) { m2Renderer = std::make_unique(); m2Renderer->initialize(vkCtx, perFrameSetLayout, assetManager); + if (swimEffects) { + swimEffects->setM2Renderer(m2Renderer.get()); + } } if (!wmoRenderer) { wmoRenderer = std::make_unique(); diff --git a/src/rendering/swim_effects.cpp b/src/rendering/swim_effects.cpp index b2837167..804917c3 100644 --- a/src/rendering/swim_effects.cpp +++ b/src/rendering/swim_effects.cpp @@ -2,6 +2,7 @@ #include "rendering/camera.hpp" #include "rendering/camera_controller.hpp" #include "rendering/water_renderer.hpp" +#include "rendering/m2_renderer.hpp" #include "rendering/vk_context.hpp" #include "rendering/vk_shader.hpp" #include "rendering/vk_pipeline.hpp" @@ -152,6 +153,50 @@ bool SwimEffects::initialize(VkContext* ctx, VkDescriptorSetLayout perFrameLayou } } + // ---- Insect pipeline (dark point sprites) ---- + { + VkShaderModule vertModule; + if (!vertModule.loadFromFile(device, "assets/shaders/swim_ripple.vert.spv")) { + LOG_ERROR("Failed to load insect vertex shader"); + return false; + } + VkShaderModule fragModule; + if (!fragModule.loadFromFile(device, "assets/shaders/swim_insect.frag.spv")) { + LOG_ERROR("Failed to load insect fragment shader"); + return false; + } + + VkPipelineShaderStageCreateInfo vertStage = vertModule.stageInfo(VK_SHADER_STAGE_VERTEX_BIT); + VkPipelineShaderStageCreateInfo fragStage = fragModule.stageInfo(VK_SHADER_STAGE_FRAGMENT_BIT); + + insectPipelineLayout = createPipelineLayout(device, {perFrameLayout}, {}); + if (insectPipelineLayout == VK_NULL_HANDLE) { + LOG_ERROR("Failed to create insect pipeline layout"); + return false; + } + + insectPipeline = PipelineBuilder() + .setShaders(vertStage, fragStage) + .setVertexInput({binding}, attrs) + .setTopology(VK_PRIMITIVE_TOPOLOGY_POINT_LIST) + .setRasterization(VK_POLYGON_MODE_FILL, VK_CULL_MODE_NONE) + .setDepthTest(true, false, VK_COMPARE_OP_LESS) + .setColorBlendAttachment(PipelineBuilder::blendAlpha()) + .setMultisample(vkCtx->getMsaaSamples()) + .setLayout(insectPipelineLayout) + .setRenderPass(vkCtx->getImGuiRenderPass()) + .setDynamicStates(dynamicStates) + .build(device); + + vertModule.destroy(); + fragModule.destroy(); + + if (insectPipeline == VK_NULL_HANDLE) { + LOG_ERROR("Failed to create insect pipeline"); + return false; + } + } + // ---- Create dynamic mapped vertex buffers ---- rippleDynamicVBSize = MAX_RIPPLE_PARTICLES * 5 * sizeof(float); { @@ -179,10 +224,25 @@ bool SwimEffects::initialize(VkContext* ctx, VkDescriptorSetLayout perFrameLayou } } + insectDynamicVBSize = MAX_INSECT_PARTICLES * 5 * sizeof(float); + { + AllocatedBuffer buf = createBuffer(vkCtx->getAllocator(), insectDynamicVBSize, + VK_BUFFER_USAGE_VERTEX_BUFFER_BIT, VMA_MEMORY_USAGE_CPU_TO_GPU); + insectDynamicVB = buf.buffer; + insectDynamicVBAlloc = buf.allocation; + insectDynamicVBAllocInfo = buf.info; + if (insectDynamicVB == VK_NULL_HANDLE) { + LOG_ERROR("Failed to create insect dynamic vertex buffer"); + return false; + } + } + ripples.reserve(MAX_RIPPLE_PARTICLES); bubbles.reserve(MAX_BUBBLE_PARTICLES); + insects.reserve(MAX_INSECT_PARTICLES); rippleVertexData.reserve(MAX_RIPPLE_PARTICLES * 5); bubbleVertexData.reserve(MAX_BUBBLE_PARTICLES * 5); + insectVertexData.reserve(MAX_INSECT_PARTICLES * 5); LOG_INFO("Swim effects initialized"); return true; @@ -220,11 +280,26 @@ void SwimEffects::shutdown() { bubbleDynamicVB = VK_NULL_HANDLE; bubbleDynamicVBAlloc = VK_NULL_HANDLE; } + + if (insectPipeline != VK_NULL_HANDLE) { + vkDestroyPipeline(device, insectPipeline, nullptr); + insectPipeline = VK_NULL_HANDLE; + } + if (insectPipelineLayout != VK_NULL_HANDLE) { + vkDestroyPipelineLayout(device, insectPipelineLayout, nullptr); + insectPipelineLayout = VK_NULL_HANDLE; + } + if (insectDynamicVB != VK_NULL_HANDLE) { + vmaDestroyBuffer(allocator, insectDynamicVB, insectDynamicVBAlloc); + insectDynamicVB = VK_NULL_HANDLE; + insectDynamicVBAlloc = VK_NULL_HANDLE; + } } vkCtx = nullptr; ripples.clear(); bubbles.clear(); + insects.clear(); } void SwimEffects::recreatePipelines() { @@ -240,6 +315,10 @@ void SwimEffects::recreatePipelines() { vkDestroyPipeline(device, bubblePipeline, nullptr); bubblePipeline = VK_NULL_HANDLE; } + if (insectPipeline != VK_NULL_HANDLE) { + vkDestroyPipeline(device, insectPipeline, nullptr); + insectPipeline = VK_NULL_HANDLE; + } // Shared vertex input: pos(vec3) + size(float) + alpha(float) = 5 floats VkVertexInputBindingDescription binding{}; @@ -319,6 +398,33 @@ void SwimEffects::recreatePipelines() { vertModule.destroy(); fragModule.destroy(); } + + // ---- Rebuild insect pipeline ---- + { + VkShaderModule vertModule; + vertModule.loadFromFile(device, "assets/shaders/swim_ripple.vert.spv"); + VkShaderModule fragModule; + fragModule.loadFromFile(device, "assets/shaders/swim_insect.frag.spv"); + + VkPipelineShaderStageCreateInfo vertStage = vertModule.stageInfo(VK_SHADER_STAGE_VERTEX_BIT); + VkPipelineShaderStageCreateInfo fragStage = fragModule.stageInfo(VK_SHADER_STAGE_FRAGMENT_BIT); + + insectPipeline = PipelineBuilder() + .setShaders(vertStage, fragStage) + .setVertexInput({binding}, attrs) + .setTopology(VK_PRIMITIVE_TOPOLOGY_POINT_LIST) + .setRasterization(VK_POLYGON_MODE_FILL, VK_CULL_MODE_NONE) + .setDepthTest(true, false, VK_COMPARE_OP_LESS) + .setColorBlendAttachment(PipelineBuilder::blendAlpha()) + .setMultisample(vkCtx->getMsaaSamples()) + .setLayout(insectPipelineLayout) + .setRenderPass(vkCtx->getImGuiRenderPass()) + .setDynamicStates(dynamicStates) + .build(device); + + vertModule.destroy(); + fragModule.destroy(); + } } void SwimEffects::spawnRipple(const glm::vec3& pos, const glm::vec3& moveDir, float waterH) { @@ -384,6 +490,31 @@ void SwimEffects::spawnBubble(const glm::vec3& pos, float /*waterH*/) { bubbles.push_back(p); } +void SwimEffects::spawnInsect(const glm::vec3& vegPos) { + if (static_cast(insects.size()) >= MAX_INSECT_PARTICLES) return; + + InsectParticle p; + p.orbitCenter = vegPos; + p.phase = randFloat(0.0f, 6.2832f); + p.orbitRadius = randFloat(0.5f, 2.0f); + p.orbitSpeed = randFloat(1.5f, 4.0f); + p.heightOffset = randFloat(0.5f, 3.0f); + p.lifetime = 0.0f; + p.maxLifetime = randFloat(3.0f, 8.0f); + p.size = randFloat(2.0f, 3.0f); + p.alpha = randFloat(0.6f, 0.9f); + + // Start at orbit position + float angle = p.phase; + p.position = vegPos + glm::vec3( + std::cos(angle) * p.orbitRadius, + std::sin(angle) * p.orbitRadius, + p.heightOffset + ); + + insects.push_back(p); +} + void SwimEffects::update(const Camera& camera, const CameraController& cc, const WaterRenderer& water, float deltaTime) { glm::vec3 camPos = camera.getPosition(); @@ -438,6 +569,23 @@ void SwimEffects::update(const Camera& camera, const CameraController& cc, bubbles.clear(); } + // --- Insect spawning near water vegetation --- + if (m2Renderer) { + auto vegPositions = m2Renderer->getWaterVegetationPositions(camPos, 60.0f); + if (!vegPositions.empty()) { + // Spawn rate: ~4/sec per nearby vegetation cluster (capped by MAX_INSECT_PARTICLES) + float spawnRate = std::min(static_cast(vegPositions.size()) * 4.0f, 20.0f); + insectSpawnAccum += spawnRate * deltaTime; + while (insectSpawnAccum >= 1.0f && static_cast(insects.size()) < MAX_INSECT_PARTICLES) { + // Pick a random vegetation position to spawn near + int idx = static_cast(randFloat(0.0f, static_cast(vegPositions.size()) - 0.01f)); + spawnInsect(vegPositions[idx]); + insectSpawnAccum -= 1.0f; + } + if (insectSpawnAccum > 2.0f) insectSpawnAccum = 0.0f; + } + } + // --- Update ripples (splash droplets with gravity) --- for (int i = static_cast(ripples.size()) - 1; i >= 0; --i) { auto& p = ripples[i]; @@ -487,6 +635,42 @@ void SwimEffects::update(const Camera& camera, const CameraController& cc, } } + // --- Update insects (erratic orbiting flight) --- + for (int i = static_cast(insects.size()) - 1; i >= 0; --i) { + auto& p = insects[i]; + p.lifetime += deltaTime; + if (p.lifetime >= p.maxLifetime) { + insects[i] = insects.back(); + insects.pop_back(); + continue; + } + + float t = p.lifetime / p.maxLifetime; + float time = p.lifetime * p.orbitSpeed + p.phase; + + // Erratic looping: primary orbit + secondary wobble + float primaryAngle = time; + float wobbleAngle = std::sin(time * 2.3f) * 0.8f; + float radius = p.orbitRadius + std::sin(time * 1.7f) * 0.3f; + + float heightWobble = std::sin(time * 1.1f + p.phase * 0.5f) * 0.5f; + + p.position = p.orbitCenter + glm::vec3( + std::cos(primaryAngle + wobbleAngle) * radius, + std::sin(primaryAngle + wobbleAngle) * radius, + p.heightOffset + heightWobble + ); + + // Fade in/out + if (t < 0.1f) { + p.alpha = glm::mix(0.0f, 0.8f, t / 0.1f); + } else if (t > 0.85f) { + p.alpha = glm::mix(0.8f, 0.0f, (t - 0.85f) / 0.15f); + } else { + p.alpha = 0.8f; + } + } + // --- Build vertex data --- rippleVertexData.clear(); for (const auto& p : ripples) { @@ -505,10 +689,19 @@ void SwimEffects::update(const Camera& camera, const CameraController& cc, bubbleVertexData.push_back(p.size); bubbleVertexData.push_back(p.alpha); } + + insectVertexData.clear(); + for (const auto& p : insects) { + insectVertexData.push_back(p.position.x); + insectVertexData.push_back(p.position.y); + insectVertexData.push_back(p.position.z); + insectVertexData.push_back(p.size); + insectVertexData.push_back(p.alpha); + } } void SwimEffects::render(VkCommandBuffer cmd, VkDescriptorSet perFrameSet) { - if (rippleVertexData.empty() && bubbleVertexData.empty()) return; + if (rippleVertexData.empty() && bubbleVertexData.empty() && insectVertexData.empty()) return; VkDeviceSize offset = 0; @@ -539,6 +732,20 @@ void SwimEffects::render(VkCommandBuffer cmd, VkDescriptorSet perFrameSet) { vkCmdBindVertexBuffers(cmd, 0, 1, &bubbleDynamicVB, &offset); vkCmdDraw(cmd, static_cast(bubbleVertexData.size() / 5), 1, 0, 0); } + + // --- Render insects --- + if (!insectVertexData.empty() && insectPipeline != VK_NULL_HANDLE) { + VkDeviceSize uploadSize = insectVertexData.size() * sizeof(float); + if (insectDynamicVBAllocInfo.pMappedData) { + std::memcpy(insectDynamicVBAllocInfo.pMappedData, insectVertexData.data(), uploadSize); + } + + vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, insectPipeline); + vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, insectPipelineLayout, + 0, 1, &perFrameSet, 0, nullptr); + vkCmdBindVertexBuffers(cmd, 0, 1, &insectDynamicVB, &offset); + vkCmdDraw(cmd, static_cast(insectVertexData.size() / 5), 1, 0, 0); + } } } // namespace rendering