mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-03-22 23:30:14 +00:00
Add ambient insect particles near water vegetation, fix firefly particles, and improve water foam
- Spawn dark point-sprite insects buzzing around cattails/reeds/kelp/seaweed - Fix firefly M2 particles: exempt from alpha dampening and forced gravity - Make water shoreline/crest foam more irregular with UV warping and bluer tint
This commit is contained in:
parent
c35b40391f
commit
4db97e37b7
9 changed files with 332 additions and 20 deletions
14
assets/shaders/swim_insect.frag.glsl
Normal file
14
assets/shaders/swim_insect.frag.glsl
Normal file
|
|
@ -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);
|
||||||
|
}
|
||||||
BIN
assets/shaders/swim_insect.frag.spv
Normal file
BIN
assets/shaders/swim_insect.frag.spv
Normal file
Binary file not shown.
|
|
@ -130,20 +130,26 @@ float fbmNoise(vec2 p, float time) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Voronoi-like cellular noise for foam particles
|
// 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 i = floor(p);
|
||||||
vec2 f = fract(p);
|
vec2 f = fract(p);
|
||||||
float minDist = 1.0;
|
float minDist = 1.0;
|
||||||
for (int y = -1; y <= 1; y++) {
|
for (int y = -1; y <= 1; y++) {
|
||||||
for (int x = -1; x <= 1; x++) {
|
for (int x = -1; x <= 1; x++) {
|
||||||
vec2 neighbor = vec2(float(x), float(y));
|
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);
|
float d = length(neighbor + point - f);
|
||||||
minDist = min(minDist, d);
|
minDist = min(minDist, d);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return minDist;
|
return minDist;
|
||||||
}
|
}
|
||||||
|
float cellularFoam(vec2 p) { return cellularFoam(p, 1.0); }
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
float time = fogParams.z;
|
float time = fogParams.z;
|
||||||
|
|
@ -299,24 +305,32 @@ void main() {
|
||||||
if (basicType < 1.5 && verticalDepth > 0.01 && push.waveAmp > 0.0) {
|
if (basicType < 1.5 && verticalDepth > 0.01 && push.waveAmp > 0.0) {
|
||||||
float foamDepthMask = 1.0 - smoothstep(0.0, 1.8, verticalDepth);
|
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
|
// Fine scattered particles
|
||||||
float cells1 = cellularFoam(FragPos.xy * 14.0 + time * vec2(0.15, 0.08));
|
float cells1 = cellularFoam(foamUV * 14.0 + time * vec2(0.15, 0.08));
|
||||||
float foam1 = (1.0 - smoothstep(0.0, 0.10, cells1)) * 0.5;
|
float foam1 = (1.0 - smoothstep(0.0, 0.12, cells1)) * 0.45;
|
||||||
|
|
||||||
// Tiny spray dots
|
// Tiny spray dots
|
||||||
float cells2 = cellularFoam(FragPos.xy * 30.0 + time * vec2(-0.12, 0.22));
|
float cells2 = cellularFoam(foamUV * 28.0 + time * vec2(-0.12, 0.22));
|
||||||
float foam2 = (1.0 - smoothstep(0.0, 0.06, cells2)) * 0.35;
|
float foam2 = (1.0 - smoothstep(0.0, 0.07, cells2)) * 0.3;
|
||||||
|
|
||||||
// Micro specks
|
// Micro specks
|
||||||
float cells3 = cellularFoam(FragPos.xy * 55.0 + time * vec2(0.25, -0.1));
|
float cells3 = cellularFoam(foamUV * 50.0 + time * vec2(0.25, -0.1));
|
||||||
float foam3 = (1.0 - smoothstep(0.0, 0.04, cells3)) * 0.2;
|
float foam3 = (1.0 - smoothstep(0.0, 0.05, cells3)) * 0.18;
|
||||||
|
|
||||||
// Noise breakup for clumping
|
// Noise breakup for clumping
|
||||||
float noiseMask = noiseValue(FragPos.xy * 3.0 + time * 0.15);
|
float noiseMask = noiseValue(FragPos.xy * 3.0 + time * 0.15);
|
||||||
float foam = (foam1 + foam2 + foam3) * foamDepthMask * smoothstep(0.3, 0.6, noiseMask);
|
float foam = (foam1 + foam2 + foam3) * foamDepthMask * smoothstep(0.3, 0.6, noiseMask);
|
||||||
|
|
||||||
foam *= smoothstep(0.0, 0.1, verticalDepth);
|
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) {
|
if (basicType > 0.5 && basicType < 1.5 && push.waveAmp > 0.0) {
|
||||||
float crestMask = smoothstep(0.5, 1.0, WaveOffset);
|
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 crestFoam = (1.0 - smoothstep(0.0, 0.18, crestCells)) * crestMask;
|
||||||
float crestNoise = noiseValue(FragPos.xy * 3.0 - time * 0.3);
|
float crestNoise = noiseValue(FragPos.xy * 3.0 - time * 0.3);
|
||||||
crestFoam *= smoothstep(0.3, 0.6, crestNoise);
|
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
|
||||||
Binary file not shown.
|
|
@ -76,6 +76,8 @@ struct M2ModelGPU {
|
||||||
bool isSmallFoliage = false; // Small foliage (bushes, grass, plants) - skip during taxi
|
bool isSmallFoliage = false; // Small foliage (bushes, grass, plants) - skip during taxi
|
||||||
bool isInvisibleTrap = false; // Invisible trap objects (don't render, no collision)
|
bool isInvisibleTrap = false; // Invisible trap objects (don't render, no collision)
|
||||||
bool isGroundDetail = false; // Ground clutter/detail doodads (special fallback render path)
|
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)
|
// Collision mesh with spatial grid (from M2 bounding geometry)
|
||||||
struct CollisionMesh {
|
struct CollisionMesh {
|
||||||
|
|
@ -307,6 +309,8 @@ public:
|
||||||
void setInsideInterior(bool inside) { insideInterior = inside; }
|
void setInsideInterior(bool inside) { insideInterior = inside; }
|
||||||
void setOnTaxi(bool onTaxi) { onTaxi_ = onTaxi; }
|
void setOnTaxi(bool onTaxi) { onTaxi_ = onTaxi; }
|
||||||
|
|
||||||
|
std::vector<glm::vec3> getWaterVegetationPositions(const glm::vec3& camPos, float maxDist) const;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
bool initialized_ = false;
|
bool initialized_ = false;
|
||||||
bool insideInterior = false;
|
bool insideInterior = false;
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ namespace rendering {
|
||||||
class Camera;
|
class Camera;
|
||||||
class CameraController;
|
class CameraController;
|
||||||
class WaterRenderer;
|
class WaterRenderer;
|
||||||
|
class M2Renderer;
|
||||||
class VkContext;
|
class VkContext;
|
||||||
|
|
||||||
class SwimEffects {
|
class SwimEffects {
|
||||||
|
|
@ -25,6 +26,7 @@ public:
|
||||||
const WaterRenderer& water, float deltaTime);
|
const WaterRenderer& water, float deltaTime);
|
||||||
void render(VkCommandBuffer cmd, VkDescriptorSet perFrameSet);
|
void render(VkCommandBuffer cmd, VkDescriptorSet perFrameSet);
|
||||||
void spawnFootSplash(const glm::vec3& footPos, float waterH);
|
void spawnFootSplash(const glm::vec3& footPos, float waterH);
|
||||||
|
void setM2Renderer(M2Renderer* renderer) { m2Renderer = renderer; }
|
||||||
|
|
||||||
private:
|
private:
|
||||||
struct Particle {
|
struct Particle {
|
||||||
|
|
@ -36,14 +38,30 @@ private:
|
||||||
float alpha;
|
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_RIPPLE_PARTICLES = 200;
|
||||||
static constexpr int MAX_BUBBLE_PARTICLES = 150;
|
static constexpr int MAX_BUBBLE_PARTICLES = 150;
|
||||||
|
static constexpr int MAX_INSECT_PARTICLES = 50;
|
||||||
|
|
||||||
std::vector<Particle> ripples;
|
std::vector<Particle> ripples;
|
||||||
std::vector<Particle> bubbles;
|
std::vector<Particle> bubbles;
|
||||||
|
std::vector<InsectParticle> insects;
|
||||||
|
|
||||||
// Vulkan objects
|
// Vulkan objects
|
||||||
VkContext* vkCtx = nullptr;
|
VkContext* vkCtx = nullptr;
|
||||||
|
M2Renderer* m2Renderer = nullptr;
|
||||||
|
|
||||||
// Ripple pipeline + dynamic buffer
|
// Ripple pipeline + dynamic buffer
|
||||||
VkPipeline ripplePipeline = VK_NULL_HANDLE;
|
VkPipeline ripplePipeline = VK_NULL_HANDLE;
|
||||||
|
|
@ -61,14 +79,25 @@ private:
|
||||||
VmaAllocationInfo bubbleDynamicVBAllocInfo{};
|
VmaAllocationInfo bubbleDynamicVBAllocInfo{};
|
||||||
VkDeviceSize bubbleDynamicVBSize = 0;
|
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> rippleVertexData;
|
||||||
std::vector<float> bubbleVertexData;
|
std::vector<float> bubbleVertexData;
|
||||||
|
std::vector<float> insectVertexData;
|
||||||
|
|
||||||
float rippleSpawnAccum = 0.0f;
|
float rippleSpawnAccum = 0.0f;
|
||||||
float bubbleSpawnAccum = 0.0f;
|
float bubbleSpawnAccum = 0.0f;
|
||||||
|
float insectSpawnAccum = 0.0f;
|
||||||
|
|
||||||
void spawnRipple(const glm::vec3& pos, const glm::vec3& moveDir, float waterH);
|
void spawnRipple(const glm::vec3& pos, const glm::vec3& moveDir, float waterH);
|
||||||
void spawnBubble(const glm::vec3& pos, float waterH);
|
void spawnBubble(const glm::vec3& pos, float waterH);
|
||||||
|
void spawnInsect(const glm::vec3& vegPos);
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace rendering
|
} // namespace rendering
|
||||||
|
|
|
||||||
|
|
@ -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)
|
// Spell effect models: particle-dominated with minimal geometry (e.g. LevelUp.m2)
|
||||||
gpuModel.isSpellEffect = hasParticles && model.vertices.size() <= 200 &&
|
gpuModel.isSpellEffect = hasParticles && model.vertices.size() <= 200 &&
|
||||||
model.particleEmitters.size() >= 3;
|
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
|
// Build collision mesh + spatial grid from M2 bounding geometry
|
||||||
gpuModel.collision.vertices = model.collisionVertices;
|
gpuModel.collision.vertices = model.collisionVertices;
|
||||||
|
|
@ -2803,6 +2816,20 @@ glm::vec3 M2Renderer::interpFBlockVec3(const pipeline::M2FBlock& fb, float lifeR
|
||||||
return fb.vec3Values.back();
|
return fb.vec3Values.back();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::vector<glm::vec3> M2Renderer::getWaterVegetationPositions(const glm::vec3& camPos, float maxDist) const {
|
||||||
|
std::vector<glm::vec3> 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) {
|
void M2Renderer::emitParticles(M2Instance& inst, const M2ModelGPU& gpu, float dt) {
|
||||||
if (inst.emitterAccumulators.size() != gpu.particleEmitters.size()) {
|
if (inst.emitterAccumulators.size() != gpu.particleEmitters.size()) {
|
||||||
inst.emitterAccumulators.resize(gpu.particleEmitters.size(), 0.0f);
|
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
|
// particles pile up at the same position. Give them a drift so they
|
||||||
// spread outward like a mist/spray effect instead of clustering.
|
// spread outward like a mist/spray effect instead of clustering.
|
||||||
if (std::abs(speed) < 0.01f) {
|
if (std::abs(speed) < 0.01f) {
|
||||||
p.velocity = rotMat * glm::vec3(
|
if (gpu.isFireflyEffect) {
|
||||||
distN(particleRng_) * 1.0f,
|
// Fireflies: gentle random drift in all directions
|
||||||
distN(particleRng_) * 1.0f,
|
p.velocity = rotMat * glm::vec3(
|
||||||
-dist01(particleRng_) * 0.5f
|
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<uint16_t>(em.textureCols, 1);
|
const uint32_t tilesX = std::max<uint16_t>(em.textureCols, 1);
|
||||||
|
|
@ -2917,7 +2953,8 @@ void M2Renderer::updateParticles(M2Instance& inst, float dt) {
|
||||||
gpu.sequences, gpu.globalSequenceDurations);
|
gpu.sequences, gpu.globalSequenceDurations);
|
||||||
// When M2 gravity is 0, apply default gravity so particles arc downward.
|
// 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.
|
// 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,
|
float emSpeed = interpFloat(pem.emissionSpeed,
|
||||||
inst.animTime, inst.currentSequenceIndex,
|
inst.animTime, inst.currentSequenceIndex,
|
||||||
gpu.sequences, gpu.globalSequenceDurations);
|
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 alpha = std::min(interpFBlockFloat(em.particleAlpha, lifeRatio), 1.0f);
|
||||||
float rawScale = interpFBlockFloat(em.particleScale, lifeRatio);
|
float rawScale = interpFBlockFloat(em.particleScale, lifeRatio);
|
||||||
|
|
||||||
if (!gpu.isSpellEffect) {
|
if (!gpu.isSpellEffect && !gpu.isFireflyEffect) {
|
||||||
color = glm::mix(color, glm::vec3(1.0f), 0.7f);
|
color = glm::mix(color, glm::vec3(1.0f), 0.7f);
|
||||||
if (rawScale > 2.0f) alpha *= 0.02f;
|
if (rawScale > 2.0f) alpha *= 0.02f;
|
||||||
if (em.blendingType == 3 || em.blendingType == 4) alpha *= 0.05f;
|
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();
|
VkTexture* tex = whiteTexture_.get();
|
||||||
if (p.emitterIndex < static_cast<int>(gpu.particleTextures.size())) {
|
if (p.emitterIndex < static_cast<int>(gpu.particleTextures.size())) {
|
||||||
|
|
|
||||||
|
|
@ -3359,6 +3359,9 @@ bool Renderer::loadTestTerrain(pipeline::AssetManager* assetManager, const std::
|
||||||
if (!m2Renderer) {
|
if (!m2Renderer) {
|
||||||
m2Renderer = std::make_unique<M2Renderer>();
|
m2Renderer = std::make_unique<M2Renderer>();
|
||||||
m2Renderer->initialize(vkCtx, perFrameSetLayout, assetManager);
|
m2Renderer->initialize(vkCtx, perFrameSetLayout, assetManager);
|
||||||
|
if (swimEffects) {
|
||||||
|
swimEffects->setM2Renderer(m2Renderer.get());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (!wmoRenderer) {
|
if (!wmoRenderer) {
|
||||||
wmoRenderer = std::make_unique<WMORenderer>();
|
wmoRenderer = std::make_unique<WMORenderer>();
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
#include "rendering/camera.hpp"
|
#include "rendering/camera.hpp"
|
||||||
#include "rendering/camera_controller.hpp"
|
#include "rendering/camera_controller.hpp"
|
||||||
#include "rendering/water_renderer.hpp"
|
#include "rendering/water_renderer.hpp"
|
||||||
|
#include "rendering/m2_renderer.hpp"
|
||||||
#include "rendering/vk_context.hpp"
|
#include "rendering/vk_context.hpp"
|
||||||
#include "rendering/vk_shader.hpp"
|
#include "rendering/vk_shader.hpp"
|
||||||
#include "rendering/vk_pipeline.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 ----
|
// ---- Create dynamic mapped vertex buffers ----
|
||||||
rippleDynamicVBSize = MAX_RIPPLE_PARTICLES * 5 * sizeof(float);
|
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);
|
ripples.reserve(MAX_RIPPLE_PARTICLES);
|
||||||
bubbles.reserve(MAX_BUBBLE_PARTICLES);
|
bubbles.reserve(MAX_BUBBLE_PARTICLES);
|
||||||
|
insects.reserve(MAX_INSECT_PARTICLES);
|
||||||
rippleVertexData.reserve(MAX_RIPPLE_PARTICLES * 5);
|
rippleVertexData.reserve(MAX_RIPPLE_PARTICLES * 5);
|
||||||
bubbleVertexData.reserve(MAX_BUBBLE_PARTICLES * 5);
|
bubbleVertexData.reserve(MAX_BUBBLE_PARTICLES * 5);
|
||||||
|
insectVertexData.reserve(MAX_INSECT_PARTICLES * 5);
|
||||||
|
|
||||||
LOG_INFO("Swim effects initialized");
|
LOG_INFO("Swim effects initialized");
|
||||||
return true;
|
return true;
|
||||||
|
|
@ -220,11 +280,26 @@ void SwimEffects::shutdown() {
|
||||||
bubbleDynamicVB = VK_NULL_HANDLE;
|
bubbleDynamicVB = VK_NULL_HANDLE;
|
||||||
bubbleDynamicVBAlloc = 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;
|
vkCtx = nullptr;
|
||||||
ripples.clear();
|
ripples.clear();
|
||||||
bubbles.clear();
|
bubbles.clear();
|
||||||
|
insects.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
void SwimEffects::recreatePipelines() {
|
void SwimEffects::recreatePipelines() {
|
||||||
|
|
@ -240,6 +315,10 @@ void SwimEffects::recreatePipelines() {
|
||||||
vkDestroyPipeline(device, bubblePipeline, nullptr);
|
vkDestroyPipeline(device, bubblePipeline, nullptr);
|
||||||
bubblePipeline = VK_NULL_HANDLE;
|
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
|
// Shared vertex input: pos(vec3) + size(float) + alpha(float) = 5 floats
|
||||||
VkVertexInputBindingDescription binding{};
|
VkVertexInputBindingDescription binding{};
|
||||||
|
|
@ -319,6 +398,33 @@ void SwimEffects::recreatePipelines() {
|
||||||
vertModule.destroy();
|
vertModule.destroy();
|
||||||
fragModule.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) {
|
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);
|
bubbles.push_back(p);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void SwimEffects::spawnInsect(const glm::vec3& vegPos) {
|
||||||
|
if (static_cast<int>(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,
|
void SwimEffects::update(const Camera& camera, const CameraController& cc,
|
||||||
const WaterRenderer& water, float deltaTime) {
|
const WaterRenderer& water, float deltaTime) {
|
||||||
glm::vec3 camPos = camera.getPosition();
|
glm::vec3 camPos = camera.getPosition();
|
||||||
|
|
@ -438,6 +569,23 @@ void SwimEffects::update(const Camera& camera, const CameraController& cc,
|
||||||
bubbles.clear();
|
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<float>(vegPositions.size()) * 4.0f, 20.0f);
|
||||||
|
insectSpawnAccum += spawnRate * deltaTime;
|
||||||
|
while (insectSpawnAccum >= 1.0f && static_cast<int>(insects.size()) < MAX_INSECT_PARTICLES) {
|
||||||
|
// Pick a random vegetation position to spawn near
|
||||||
|
int idx = static_cast<int>(randFloat(0.0f, static_cast<float>(vegPositions.size()) - 0.01f));
|
||||||
|
spawnInsect(vegPositions[idx]);
|
||||||
|
insectSpawnAccum -= 1.0f;
|
||||||
|
}
|
||||||
|
if (insectSpawnAccum > 2.0f) insectSpawnAccum = 0.0f;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// --- Update ripples (splash droplets with gravity) ---
|
// --- Update ripples (splash droplets with gravity) ---
|
||||||
for (int i = static_cast<int>(ripples.size()) - 1; i >= 0; --i) {
|
for (int i = static_cast<int>(ripples.size()) - 1; i >= 0; --i) {
|
||||||
auto& p = ripples[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<int>(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 ---
|
// --- Build vertex data ---
|
||||||
rippleVertexData.clear();
|
rippleVertexData.clear();
|
||||||
for (const auto& p : ripples) {
|
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.size);
|
||||||
bubbleVertexData.push_back(p.alpha);
|
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) {
|
void SwimEffects::render(VkCommandBuffer cmd, VkDescriptorSet perFrameSet) {
|
||||||
if (rippleVertexData.empty() && bubbleVertexData.empty()) return;
|
if (rippleVertexData.empty() && bubbleVertexData.empty() && insectVertexData.empty()) return;
|
||||||
|
|
||||||
VkDeviceSize offset = 0;
|
VkDeviceSize offset = 0;
|
||||||
|
|
||||||
|
|
@ -539,6 +732,20 @@ void SwimEffects::render(VkCommandBuffer cmd, VkDescriptorSet perFrameSet) {
|
||||||
vkCmdBindVertexBuffers(cmd, 0, 1, &bubbleDynamicVB, &offset);
|
vkCmdBindVertexBuffers(cmd, 0, 1, &bubbleDynamicVB, &offset);
|
||||||
vkCmdDraw(cmd, static_cast<uint32_t>(bubbleVertexData.size() / 5), 1, 0, 0);
|
vkCmdDraw(cmd, static_cast<uint32_t>(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<uint32_t>(insectVertexData.size() / 5), 1, 0, 0);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace rendering
|
} // namespace rendering
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue