feat(lighting): mode additive amb color per emissor
This commit is contained in:
1 parent
24609d6c5c
commit
771ff1f6cd
10 files changed
+352
-71
No files matched your search
@@ -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
|
||||
@@ -7,36 +7,73 @@
|
||||
// Sistema d'il·luminació CPU. Treballa sobre el mateix canvas ARGB del
|
||||
// joc (320×240) que tot el pipeline de render: després que GameScene
|
||||
// haja pintat tilemap, paint layer, sombres i actors, una pasada final
|
||||
// multiplica cada píxel del canvas per un factor de luminància 0..1
|
||||
// derivat de la combinació de "llum de la sala" + cercles emissors al
|
||||
// voltant de certes entitats.
|
||||
// modifica cada píxel del canvas en funció de la "llum de la sala" i
|
||||
// dels cercles emissors al voltant de certes entitats.
|
||||
//
|
||||
// Forma de cada emissor (radial, 3 anells configurables al fitxer
|
||||
// data/config/lighting.yaml; veure `GradientConfig`):
|
||||
// Dos modes seleccionables per la sala (`lighting.mode` al YAML):
|
||||
//
|
||||
// SUBTRACTIVE (mode històric, default si no es declara mode):
|
||||
// out = src × max(room_light, max_emitter_intensity_at_pixel)
|
||||
// Els emissors "trenquen" una capa fosca multiplicativa. Tot el
|
||||
// trip cromàtic dels emissors s'ignora — només la intensitat
|
||||
// compta. Pensat per a sales fosques (cripta, pirámide).
|
||||
//
|
||||
// ADDITIVE (mode nou):
|
||||
// out = saturate( (src × room_light) + Σ (emitter_color × intensity) )
|
||||
// El canvas s'enfosqueix opcionalment per `room_light` (atenuació
|
||||
// ambient) i els emissors hi sumen color cap a sobre. Les intensitats
|
||||
// dels anells es llegeixen com a "alfa" del color afegit (0..1).
|
||||
// Pensat per a sales clares on les antorxes aporten halo cromàtic
|
||||
// sense restar contrast — o per a sales lleument fosques (0.85, p.ex.)
|
||||
// on el halo de l'antorxa es veu càlid en lloc de "blanquinós".
|
||||
//
|
||||
// Forma del cercle de cada emissor (radial, 3 anells configurables al
|
||||
// fitxer data/config/lighting.yaml; veure `GradientConfig`):
|
||||
// 0..inner_ratio del radi → inner_intensity
|
||||
// inner_ratio..mid_ratio → mid_intensity
|
||||
// mid_ratio..1.0 → outer_intensity
|
||||
// fora del radi → light_level de la sala
|
||||
//
|
||||
// Combinació entre emissors: per a cada píxel es pren el màxim del
|
||||
// llum de sala i de la intensitat aportada per cada emissor que el
|
||||
// solapa — no se "sumen" intensitats. Coherent amb la idea de "centre
|
||||
// 1.0": el píxel que cau dins de qualsevol nucli no s'enfosqueix.
|
||||
// Combinació entre emissors:
|
||||
// SUBTRACTIVE → per a cada píxel es pren el màxim de la intensitat
|
||||
// aportada per cada emissor que el solapa (no se sumen intensitats).
|
||||
// ADDITIVE → per a cada píxel se sumen totes les contribucions
|
||||
// (`color × intensity`), saturant a 255 per canal. Dues llums de
|
||||
// colors diferents que solapen produeixen la suma natural (orange +
|
||||
// blue → near-white) — comportament físic.
|
||||
namespace Lighting {
|
||||
|
||||
// Mode de composició. La sala el selecciona via `lighting.mode:
|
||||
// additive | subtractive` (default subtractive si manca, retrocompat).
|
||||
enum class Mode : std::uint8_t {
|
||||
Subtractive,
|
||||
Additive,
|
||||
};
|
||||
|
||||
// Color emès. Default blanc (sense modificació quan s'usa en mode
|
||||
// additive). En mode subtractive s'ignora — la pasada multiplicativa
|
||||
// només té un canal (intensitat). El YAML accepta `color: [r, g, b]`
|
||||
// a qualsevol nivell de la cadena de resolució (tipus, sala, spawn).
|
||||
struct Color {
|
||||
std::uint8_t r{255};
|
||||
std::uint8_t g{255};
|
||||
std::uint8_t b{255};
|
||||
};
|
||||
|
||||
// Posició al canvas (en píxels, mateixa unitat que la resta del
|
||||
// motor) i radi efectiu de l'emissor. El centre és el del sprite
|
||||
// visible: és l'entitat qui sap on cau (veure `Entity::getLightCenter`).
|
||||
// `flicker` activa la modulació del radi per frame; `seed` és un
|
||||
// identificador estable per emissor (no canvia amb la posició) del
|
||||
// qual es deriva la fase del flicker — així els emissors mòbils
|
||||
// mantenen un patró propi que no "deriva" pel moviment.
|
||||
// mantenen un patró propi que no "deriva" pel moviment. `color`
|
||||
// només té efecte en mode additive.
|
||||
struct Emitter {
|
||||
float cx{0.0F};
|
||||
float cy{0.0F};
|
||||
int radius{0};
|
||||
int seed{0};
|
||||
bool flicker{false};
|
||||
Color color{};
|
||||
};
|
||||
|
||||
// Forma del gradient. Compartida per tots els emissors per evitar
|
||||
@@ -71,12 +108,16 @@ namespace Lighting {
|
||||
// Defaults::Lighting fins que loadConfig() es crida.
|
||||
[[nodiscard]] auto config() -> const GradientConfig&;
|
||||
|
||||
// Aplica la pasada d'il·luminació al canvas in-place. `room_light`
|
||||
// és l'atenuació base de la sala (0..1, 1 = sense atenuació). El
|
||||
// span d'emissors pot ser buit — en aquest cas tota la sala queda
|
||||
// multiplicada per `room_light`. Aquesta funció no fa cap allocació
|
||||
// si es crida amb la mateixa mida de canvas múltiples cops: manté
|
||||
// un buffer intern reutilitzable.
|
||||
void apply(std::uint32_t* canvas, int width, int height, float room_light, std::span<const Emitter> emitters);
|
||||
// Aplica la pasada d'il·luminació al canvas in-place. `mode`
|
||||
// selecciona la composició (vegeu el comentari de la capçalera).
|
||||
// `room_light` és l'atenuació ambient de la sala (0..1, 1 = sense
|
||||
// atenuació): en subtractive és el "terra" de la intensitat fora
|
||||
// dels emissors; en additive és la multiplicació prèvia del canvas
|
||||
// abans de sumar els emissors (es pot deixar a 1.0 per "només tint",
|
||||
// o baixar per combinar oscuriment ambient + halo cromàtic). El
|
||||
// span d'emissors pot ser buit. La funció no fa cap allocació si
|
||||
// es crida amb la mateixa mida de canvas múltiples cops: manté un
|
||||
// buffer intern reutilitzable (només l'usa el mode subtractive).
|
||||
void apply(std::uint32_t* canvas, int width, int height, Mode mode, float room_light, std::span<const Emitter> emitters);
|
||||
|
||||
} // namespace Lighting
|
||||
Reference in new issue
Block a user