fix: flicker the light lamps cast, not just their glow sprite

Modulating the additive sprite alone was invisible: it already saturates,
so a small alpha change shows only in its faint edges. The guttering now
also drives each lamp's local light intensity, which is what actually
lights the ground and walls nearby and reads as firelight. Sprite and
light share one position-hashed phase, so a lamp and its pool of light
breathe together while neighbouring lamps stay out of step. Lava keeps a
steady burn.
This commit is contained in:
Kelsi
2026-07-23 00:36:43 -07:00
parent 06438d11b3
commit 5896bdb75b
3 changed files with 45 additions and 11 deletions

View File

@@ -117,6 +117,7 @@ uint32_t M2Renderer::gatherLocalLights(const glm::vec3& cameraPos,
float distSq;
glm::vec4 posRadius;
glm::vec4 colorIntensity;
bool flame = false; // lamps/torches/braziers gutter; lava burns steady
};
std::vector<Candidate> candidates;
@@ -133,7 +134,8 @@ uint32_t M2Renderer::gatherLocalLights(const glm::vec3& cameraPos,
const float radius = std::clamp(instance.cachedVisualRadius * 0.8f,
10.0f, 35.0f);
candidates.push_back({distSq, glm::vec4(worldPos, radius),
glm::vec4(1.0f, 0.28f, 0.035f, 1.75f)});
glm::vec4(1.0f, 0.28f, 0.035f, 1.75f),
/*flame=*/false});
}
}
@@ -169,7 +171,7 @@ uint32_t M2Renderer::gatherLocalLights(const glm::vec3& cameraPos,
if (batch.glowTint == 1) color = glm::vec3(0.42f, 0.68f, 1.0f);
else if (batch.glowTint == 2) color = glm::vec3(1.0f, 0.24f, 0.14f);
candidates.push_back({distSq, glm::vec4(worldPos, radius),
glm::vec4(color, 1.35f)});
glm::vec4(color, 1.35f), /*flame=*/true});
hasBatchLight = true;
}
@@ -201,7 +203,8 @@ uint32_t M2Renderer::gatherLocalLights(const glm::vec3& cameraPos,
candidates.push_back({distSq,
glm::vec4(worldPos, chandelier ? 10.0f : 5.0f),
glm::vec4(1.0f, 0.58f, 0.22f,
chandelier ? 1.25f : 0.85f)});
chandelier ? 1.25f : 0.85f),
/*flame=*/true});
}
}
}
@@ -212,9 +215,18 @@ uint32_t M2Renderer::gatherLocalLights(const glm::vec3& cameraPos,
if (count == 0) return 0;
std::partial_sort(candidates.begin(), candidates.begin() + count, candidates.end(),
[](const Candidate& a, const Candidate& b) { return a.distSq < b.distSq; });
// Guttering is applied to the light itself, not just the glow sprite: the
// pool of light a lamp throws on the ground and nearby walls is what the eye
// actually reads as firelight, so modulating the sprite alone was too subtle
// to notice.
const float flickerSeconds = lampFlickerClockSeconds();
for (uint32_t i = 0; i < count; ++i) {
outPosRadius[i] = candidates[i].posRadius;
outColorIntensity[i] = candidates[i].colorIntensity;
if (candidates[i].flame) {
outColorIntensity[i].w *= lampFlicker(glm::vec3(candidates[i].posRadius),
flickerSeconds, 0.86f, 0.11f, 0.05f);
}
}
return count;
}

View File

@@ -330,6 +330,30 @@ inline void computeBoneMatrices(const M2ModelGPU& model, M2Instance& instance) {
instance.bonesDirty[0] = instance.bonesDirty[1] = true;
}
// Firelight guttering for lamps, torches and braziers.
//
// The phase is hashed from the fixture's world position so no two lights ever
// pulse together — a synchronised row of street lamps reads as a rendering
// artifact rather than firelight — and it stays stable frame to frame because
// it derives from a fixed position rather than a counter.
//
// Two detuned sines, slow enough to read as a flame breathing rather than a
// strobe, with periods that share no common multiple over any watchable span.
inline float lampFlicker(const glm::vec3& worldPos, float seconds,
float base, float slowAmp, float fastAmp) {
float h = std::sin(worldPos.x * 12.9898f + worldPos.y * 78.233f +
worldPos.z * 37.719f) * 43758.5453f;
const float phase = (h - std::floor(h)) * 6.2831853f;
return base + slowAmp * std::sin(seconds * 1.3f + phase)
+ fastAmp * std::sin(seconds * 2.9f + phase * 1.7f);
}
/// Seconds since process start, shared by the flicker animations.
inline float lampFlickerClockSeconds() {
static const auto start = std::chrono::steady_clock::now();
return std::chrono::duration<float>(std::chrono::steady_clock::now() - start).count();
}
} // namespace m2_internal
// Pull all symbols into the rendering namespace so existing code compiles unchanged

View File

@@ -1309,15 +1309,13 @@ void M2Renderer::render(VkCommandBuffer cmd, VkDescriptorSet perFrameSet, const
// visibly. Alpha and size move together, since a
// brighter flame also looks slightly larger.
{
float h = std::sin(worldPos.x * 12.9898f +
worldPos.y * 78.233f +
worldPos.z * 37.719f) * 43758.5453f;
const float phase = (h - std::floor(h)) * 6.2831853f;
const float flicker =
0.90f + 0.07f * std::sin(lavaAnimSeconds * 1.3f + phase)
+ 0.03f * std::sin(lavaAnimSeconds * 2.9f + phase * 1.7f);
// Matches the phase used for this lamp's local
// light, so the sprite and the pool of light it
// casts breathe together.
const float flicker = lampFlicker(
worldPos, lavaAnimSeconds, 0.86f, 0.11f, 0.05f);
gs.color.a *= flicker;
gs.size *= 0.97f + 0.03f * flicker;
gs.size *= 0.94f + 0.06f * flicker;
}
glowSprites_.push_back(gs);
GlowSprite halo = gs;