From e16f0cf5ddb5745ea4370ab6e4d5eb210f331a3e Mon Sep 17 00:00:00 2001 From: Kelsi Date: Thu, 23 Jul 2026 01:23:31 -0700 Subject: [PATCH] fix: hold flame fixtures at a density that reads as fire MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CHANDELIER01 authors one particle per second over a six second life, which we reproduced faithfully and which sustains a single speck per candle — a bare glow, no flame. Fixtures that look right sustain 20 to 60 particles; authored rates vary wildly for the same intent. Since steady-state population is rate times lifespan, floor the rate against the lifespan so every flame fixture holds a comparable population. Removes the tracing that established emission and draw were both working. --- src/rendering/m2_renderer_particles.cpp | 50 ++++++------------------- 1 file changed, 12 insertions(+), 38 deletions(-) diff --git a/src/rendering/m2_renderer_particles.cpp b/src/rendering/m2_renderer_particles.cpp index 512934a0..e6341c84 100644 --- a/src/rendering/m2_renderer_particles.cpp +++ b/src/rendering/m2_renderer_particles.cpp @@ -101,6 +101,18 @@ void M2Renderer::emitParticles(M2Instance& inst, const M2ModelGPU& gpu, float dt inst.currentSequenceIndex, gpu.globalSequenceDurations); float life = interpFloat(em.lifespan, inst.animTime, inst.globalSequenceTime, inst.currentSequenceIndex, gpu.globalSequenceDurations); + // A flame reads as a flame only when enough particles are alive at once. + // Authored rates vary wildly for the same visual intent — a candle asks + // for 40/s over half a second, CHANDELIER01 for 1/s over six seconds, + // which sustains a single speck per candle and looks like a bare glow. + // Steady-state population is rate x lifespan, so floor the rate against + // the lifespan to hold every fixture at a comparable density. + if (rate > 0.0f && life > 0.0f && + (gpu.isLanternLike || gpu.isTorch || gpu.isBrazierOrFire || gpu.isKoboldFlame)) { + constexpr float kMinLiveParticles = 15.0f; + rate = std::max(rate, kMinLiveParticles / std::max(life, 0.1f)); + } + if (rate <= 0.0f || life <= 0.0f) { // Diagnostic: a lamp or flame whose emitter never fires produces a // fixture that glows but shows no flame. Report each model once so a @@ -196,20 +208,6 @@ void M2Renderer::emitParticles(M2Instance& inst, const M2ModelGPU& gpu, float dt inst.particles.push_back(p); - // Diagnostic: flame fixtures report their first birth too, so a - // model that emits but never draws can be told apart from one that - // never emits at all. - if (!gpu.isSpellEffect && inst.particles.size() == 1 && - (gpu.isLanternLike || gpu.isTorch || gpu.isBrazierOrFire)) { - static std::unordered_set bornReported; - if (bornReported.insert(gpu.name).second) { - LOG_WARNING("Flame born: '", gpu.name, "' emitter=", ei, - " rate=", rate, " life=", life, - " tex=", (ei < gpu.particleTextures.size() && - gpu.particleTextures[ei]) ? "ok" : "NULL", - " blend=", gpu.particleEmitters[ei].blendingType); - } - } // Diagnostic: log first particle birth per spell effect instance if (gpu.isSpellEffect && inst.particles.size() == 1) { LOG_INFO("SpellEffect: first particle for '", gpu.name, @@ -556,19 +554,6 @@ void M2Renderer::renderM2Particles(VkCommandBuffer cmd, VkDescriptorSet perFrame const auto& gpu = *inst.cachedModel; if (gpu.isInstancePortal) continue; - // Diagnostic counterpart to "Flame born": how many live particles a - // flame fixture presents to the draw pass, reported once per model. - const bool flameFixture = !gpu.isSpellEffect && - (gpu.isLanternLike || gpu.isTorch || gpu.isBrazierOrFire); - bool reportThisModel = false; - if (flameFixture) { - static std::unordered_set drawReported; - if (drawReported.insert(gpu.name).second) { - reportThisModel = true; - LOG_WARNING("Flame draw: '", gpu.name, "' liveParticles=", - inst.particles.size()); - } - } // Cache the last emitter's per-emitter state so adjacent particles // sharing an emitter (the common case — particles from one source @@ -635,17 +620,6 @@ void M2Renderer::renderM2Particles(VkCommandBuffer cmd, VkDescriptorSet perFrame if (rawScale > 2.0f) alpha *= 0.02f; if (cachedBlendType == 3 || cachedBlendType == 4) alpha *= 0.05f; } - if (reportThisModel) { - reportThisModel = false; - LOG_WARNING("Flame particle: '", gpu.name, - "' lifeRatio=", lifeRatio, - " rawScale=", rawScale, - " scaleKeys=", em.particleScale.floatValues.size(), - " alpha=", alpha, - " alphaKeys=", em.particleAlpha.floatValues.size(), - " colorKeys=", em.particleColor.vec3Values.size(), - " blend=", static_cast(cachedBlendType)); - } float scale = rawScale; if (gpu.isSpellEffect) { scale = std::max(rawScale * 1.5f, 0.15f);