feat(lighting): mode additive amb color per emissor

This commit is contained in:
JailDesigner committed 2026-05-13 14:11:51 +02:00
1 parent 24609d6c5c
commit 771ff1f6cd
10 files changed
+352 -71

No files matched your search

+133 -30
View File
@@ -133,9 +133,9 @@ namespace Lighting {
}
}
// Pasada final: pixel = pixel * lum / 255 per canal. El canal alpha
// es preserva (la resta del pipeline assumeix opacitat total al
// canvas; veure Rendering::OPAQUE_ALPHA).
// Pasada final del mode subtractive: pixel = pixel * lum / 255
// per canal. El canal alpha es preserva (la resta del pipeline
// assumeix opacitat total al canvas; veure Rendering::OPAQUE_ALPHA).
void modulateCanvas(std::uint32_t* canvas, const std::uint8_t* lum, int width, int height) {
for (int y = 0; y < height; ++y) {
std::uint32_t* px = canvas + (static_cast<std::ptrdiff_t>(y) * width);
@@ -158,6 +158,77 @@ namespace Lighting {
}
}
// Atenuació ambient del mode additive: aplica `pixel × level` per
// canvas abans del bucle d'emissors. Si `level == 255` (sala a
// 1.0) salta com a no-op — el cas comú quan només volem el tint
// dels emissors sense oscurir la sala.
void darkenCanvas(std::uint32_t* canvas, int width, int height, std::uint8_t level) {
if (level == 255) { return; }
for (int y = 0; y < height; ++y) {
std::uint32_t* px = canvas + (static_cast<std::ptrdiff_t>(y) * width);
for (int x = 0; x < width; ++x) {
const std::uint32_t SRC = px[x];
const std::uint8_t SR = (SRC >> 16) & 0xFF;
const std::uint8_t SG = (SRC >> 8) & 0xFF;
const std::uint8_t SB = SRC & 0xFF;
const auto R = static_cast<std::uint8_t>((SR * level) / 255);
const auto G = static_cast<std::uint8_t>((SG * level) / 255);
const auto B = static_cast<std::uint8_t>((SB * level) / 255);
px[x] = (SRC & 0xFF000000U) |
(static_cast<std::uint32_t>(R) << 16) |
(static_cast<std::uint32_t>(G) << 8) |
static_cast<std::uint32_t>(B);
}
}
}
// Pasada del mode additive per a un emissor: suma `color × intensity`
// a cada píxel cobert pel cercle, saturant a 255 per canal. A
// diferència del subtractive, NO usa buffer intermig (no cal max())
// perquè la suma és commutativa; emissors solapats acumulen
// contribucions de manera natural. Si dues llums de colors diferents
// es solapen, el resultat tendeix al blanc en la zona de coincidència
// (suma física): el solape s'aclareix més que un sol emissor.
void rasterizeEmitterAdditive(const Emitter& e, const EmitterRings& rings, std::uint32_t* canvas, int width, int height) {
const int R = e.radius;
const int CX = static_cast<int>(e.cx);
const int CY = static_cast<int>(e.cy);
const int X0 = std::max(0, CX - R);
const int Y0 = std::max(0, CY - R);
const int X1 = std::min(width - 1, CX + R);
const int Y1 = std::min(height - 1, CY + R);
for (int y = Y0; y <= Y1; ++y) {
const int DY = y - CY;
const int DY_SQ = DY * DY;
if (DY_SQ > rings.radius_sq) { continue; }
std::uint32_t* row = canvas + (static_cast<std::ptrdiff_t>(y) * width);
for (int x = X0; x <= X1; ++x) {
const int DX = x - CX;
const int DIST_SQ = (DX * DX) + DY_SQ;
if (DIST_SQ > rings.radius_sq) { continue; }
std::uint8_t intensity = rings.outer;
if (DIST_SQ <= rings.inner_sq) {
intensity = rings.inner;
} else if (DIST_SQ <= rings.mid_sq) {
intensity = rings.mid;
}
// contrib_channel = emitter_color_channel × intensity / 255
// (intensity ja és 0..255 quantitzat a `toByte`).
const int CR = (e.color.r * intensity) / 255;
const int CG = (e.color.g * intensity) / 255;
const int CB = (e.color.b * intensity) / 255;
const std::uint32_t SRC = row[x];
const int SR = static_cast<int>((SRC >> 16) & 0xFF);
const int SG = static_cast<int>((SRC >> 8) & 0xFF);
const int SB = static_cast<int>(SRC & 0xFF);
const auto NR = static_cast<std::uint32_t>(std::min(255, SR + CR));
const auto NG = static_cast<std::uint32_t>(std::min(255, SG + CG));
const auto NB = static_cast<std::uint32_t>(std::min(255, SB + CB));
row[x] = (SRC & 0xFF000000U) | (NR << 16) | (NG << 8) | NB;
}
}
}
} // namespace
auto config() -> const GradientConfig& {
@@ -196,39 +267,71 @@ namespace Lighting {
}
}
void apply(std::uint32_t* canvas, int width, int height, float room_light, std::span<const Emitter> emitters) {
namespace {
// Helper: emissor "patxat" amb el radi efectiu (post-flicker) per
// a aquest frame. Mantenim l'`Emitter` del caller intacte i la
// resta de camps (color, seed, ...) propagats.
auto withEffectiveRadius(const Emitter& e, int r_eff) -> Emitter {
return Emitter{.cx = e.cx, .cy = e.cy, .radius = r_eff, .seed = e.seed, .flicker = e.flicker, .color = e.color};
}
// Camí del mode subtractive (històric): omple un buffer de
// luminància amb `max(room, emitter_intensity)` i finalment
// multiplica el canvas per ell. Color de l'emissor ignorat.
void applySubtractive(std::uint32_t* canvas, int width, int height, std::uint8_t room, std::span<const Emitter> emitters) {
auto& lum = luminanceBuffer();
const std::size_t TOTAL = static_cast<std::size_t>(width) * static_cast<std::size_t>(height);
if (lum.size() != TOTAL) { lum.resize(TOTAL); }
std::ranges::fill(lum, room);
const auto& cfg = activeConfig();
const auto& fc = activeFlicker();
const float T_S = static_cast<float>(SDL_GetTicks()) / 1000.0F;
for (const auto& e : emitters) {
if (e.radius <= 0) { continue; }
const int R_EFF = e.flicker ? computeFlickerRadius(e.radius, e.seed, T_S, fc) : e.radius;
const auto RINGS = computeRings(R_EFF, cfg);
const Emitter EFF = withEffectiveRadius(e, R_EFF);
rasterizeEmitter(EFF, RINGS, lum.data(), width, height);
}
modulateCanvas(canvas, lum.data(), width, height);
}
// Camí del mode additive: atenua el canvas per `room` (si <255)
// i després suma `color × intensity` per emissor directament al
// canvas, sense buffer intermig.
void applyAdditive(std::uint32_t* canvas, int width, int height, std::uint8_t room, std::span<const Emitter> emitters) {
darkenCanvas(canvas, width, height, room);
const auto& cfg = activeConfig();
const auto& fc = activeFlicker();
const float T_S = static_cast<float>(SDL_GetTicks()) / 1000.0F;
for (const auto& e : emitters) {
if (e.radius <= 0) { continue; }
const int R_EFF = e.flicker ? computeFlickerRadius(e.radius, e.seed, T_S, fc) : e.radius;
const auto RINGS = computeRings(R_EFF, cfg);
const Emitter EFF = withEffectiveRadius(e, R_EFF);
rasterizeEmitterAdditive(EFF, RINGS, canvas, width, height);
}
}
} // namespace
void apply(std::uint32_t* canvas, int width, int height, Mode mode, float room_light, std::span<const Emitter> emitters) {
if (canvas == nullptr || width <= 0 || height <= 0) { return; }
// Atajo: si la sala està totalment il·luminada i no hi ha
// emissors, la pasada seria un `pixel * 1.0` global — no cal
// tocar res. És el camí curt habitual mentre no posem
// `light_level` a les sales.
// Atajo per a tots dos modes: sala totalment "neutra" + cap
// emissor → la pasada no canviaria res al canvas.
// subtractive: room=1.0 i sense emissors → pixel * 1.0 global.
// additive: room=1.0 i sense emissors → cap aportació.
const std::uint8_t ROOM = toByte(room_light);
if (ROOM == 255 && emitters.empty()) { return; }
auto& lum = luminanceBuffer();
const std::size_t TOTAL = static_cast<std::size_t>(width) * static_cast<std::size_t>(height);
if (lum.size() != TOTAL) { lum.resize(TOTAL); }
std::ranges::fill(lum, ROOM);
const auto& cfg = activeConfig();
const auto& fc = activeFlicker();
const float T_S = static_cast<float>(SDL_GetTicks()) / 1000.0F;
for (const auto& e : emitters) {
if (e.radius <= 0) { continue; }
const int R_EFF = e.flicker
? computeFlickerRadius(e.radius, e.seed, T_S, fc)
: e.radius;
const auto RINGS = computeRings(R_EFF, cfg);
// El rasteritzador llegeix el radi de l'`Emitter` per al
// càlcul de la bounding box, així que li passem un emissor
// "patxat" amb el radi efectiu d'aquest frame. Mantenim el
// valor original al `Emitter` del caller intacte.
const Emitter EFF{.cx = e.cx, .cy = e.cy, .radius = R_EFF, .seed = e.seed, .flicker = e.flicker};
rasterizeEmitter(EFF, RINGS, lum.data(), width, height);
if (mode == Mode::Additive) {
applyAdditive(canvas, width, height, ROOM, emitters);
} else {
applySubtractive(canvas, width, height, ROOM, emitters);
}
modulateCanvas(canvas, lum.data(), width, height);
}
} // namespace Lighting