#include "core/rendering/lighting.hpp" #include #include #include #include #include #include #include "core/resources/resource_helper.hpp" #include "external/fkyaml_node.hpp" #include "utils/defaults.hpp" #include "utils/log.hpp" namespace Lighting { namespace { // Buffer de luminància compartit entre frames. Es redimensiona // només si canvia la mida del canvas (mai en un run normal: el // canvas del joc és 320×240 fix). El reusem perquè una `apply` // allotjant cada frame seria un cost evitable. auto luminanceBuffer() -> std::vector& { static std::vector buf_; return buf_; } // Configuració global, inicialitzada amb els defaults compilats. // loadConfig() la reemplaça si troba el fitxer. auto activeConfig() -> GradientConfig& { static GradientConfig cfg_{ .inner_ratio = Defaults::Lighting::INNER_RATIO, .mid_ratio = Defaults::Lighting::MID_RATIO, .inner_intensity = Defaults::Lighting::INNER_INTENSITY, .mid_intensity = Defaults::Lighting::MID_INTENSITY, .outer_intensity = Defaults::Lighting::OUTER_INTENSITY, }; return cfg_; } auto activeFlicker() -> FlickerConfig& { static FlickerConfig fc_{ .wave_amplitude_px = Defaults::Lighting::FLICKER_WAVE_AMPLITUDE_PX, .wave_frequency_hz = Defaults::Lighting::FLICKER_WAVE_FREQUENCY_HZ, .noise_amplitude_px = Defaults::Lighting::FLICKER_NOISE_AMPLITUDE_PX, .noise_frequency_hz = Defaults::Lighting::FLICKER_NOISE_FREQUENCY_HZ, }; return fc_; } // Modula el radi base d'un emissor amb la suma de dues sinusoides // (`wave` + `noise`) i una fase derivada de `seed`. La seed és un // identificador estable de l'emissor (no canvia amb la posició) — // així el patró no "deriva" quan l'emissor es mou. El factor // ~φ (golden ratio) dispersa les fases per a seeds consecutives. auto computeFlickerRadius(int base_radius, int seed, float t_s, const FlickerConfig& fc) -> int { constexpr float TWO_PI = 2.0F * std::numbers::pi_v; constexpr float GOLDEN_FRACTION = 0.6180339F; const float PHASE = static_cast(seed) * GOLDEN_FRACTION; const float W1 = std::sin((TWO_PI * fc.wave_frequency_hz * t_s) + PHASE); const float W2 = std::sin((TWO_PI * fc.noise_frequency_hz * t_s) + (PHASE * 2.7F)); const float DELTA = (fc.wave_amplitude_px * W1) + (fc.noise_amplitude_px * W2); const int R = base_radius + static_cast(std::lround(DELTA)); return std::max(1, R); } // Quantitza un factor 0..1 a 0..255. `clamp` per defensar-nos de // YAMLs que entreguen valors fora de rang sense forçar la mà. auto toByte(float v) -> std::uint8_t { const float CLAMPED = std::clamp(v, 0.0F, 1.0F); return static_cast(std::lround(CLAMPED * 255.0F)); } void readField(const fkyaml::node& node, const char* key, float& dst) { if (node.contains(key)) { dst = node[key].get_value(); } } // Pre-calcula els 3 quadrats de radi i les intensitats que farem // servir dins del bucle per píxel. Es resol una sola vegada per // emissor i s'evita arrastrar 6 paràmetres a `rasterizeEmitter`. struct EmitterRings { int radius_sq{0}; int mid_sq{0}; int inner_sq{0}; std::uint8_t inner{0}; std::uint8_t mid{0}; std::uint8_t outer{0}; }; auto computeRings(int radius, const GradientConfig& cfg) -> EmitterRings { const auto R_F = static_cast(radius); const float R_INNER = R_F * cfg.inner_ratio; const float R_MID = R_F * cfg.mid_ratio; return EmitterRings{ .radius_sq = radius * radius, .mid_sq = static_cast(R_MID * R_MID), .inner_sq = static_cast(R_INNER * R_INNER), .inner = toByte(cfg.inner_intensity), .mid = toByte(cfg.mid_intensity), .outer = toByte(cfg.outer_intensity), }; } // Itera la bounding box de l'emissor clipada al canvas i actualitza // la fila de luminància corresponent amb `max(actual, ring)`. Es // crida un cop per emissor, així extreure-la deixa `apply` simple. void rasterizeEmitter(const Emitter& e, const EmitterRings& rings, std::uint8_t* lum, int width, int height) { const int R = e.radius; const int CX = static_cast(e.cx); const int CY = static_cast(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::uint8_t* row = lum + (static_cast(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; } row[x] = std::max(intensity, row[x]); } } } // 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(y) * width); const std::uint8_t* lrow = lum + (static_cast(y) * width); for (int x = 0; x < width; ++x) { const std::uint8_t L = lrow[x]; if (L == 255) { continue; } // píxel intacte. 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((SR * L) / 255); const auto G = static_cast((SG * L) / 255); const auto B = static_cast((SB * L) / 255); px[x] = (SRC & 0xFF000000U) | (static_cast(R) << 16) | (static_cast(G) << 8) | static_cast(B); } } } // 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(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((SR * level) / 255); const auto G = static_cast((SG * level) / 255); const auto B = static_cast((SB * level) / 255); px[x] = (SRC & 0xFF000000U) | (static_cast(R) << 16) | (static_cast(G) << 8) | static_cast(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(e.cx); const int CY = static_cast(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(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((SRC >> 16) & 0xFF); const int SG = static_cast((SRC >> 8) & 0xFF); const int SB = static_cast(SRC & 0xFF); const auto NR = static_cast(std::min(255, SR + CR)); const auto NG = static_cast(std::min(255, SG + CG)); const auto NB = static_cast(std::min(255, SB + CB)); row[x] = (SRC & 0xFF000000U) | (NR << 16) | (NG << 8) | NB; } } } } // namespace auto config() -> const GradientConfig& { return activeConfig(); } void loadConfig(const std::string& yaml_path) { auto bytes = Resource::Helper::loadFile(yaml_path); if (bytes.empty()) { Log::warn("Lighting", "no s'ha pogut obrir {} — s'usaran els defaults compilats", yaml_path); return; } try { const auto* begin = reinterpret_cast(bytes.data()); const auto* end = begin + bytes.size(); auto yaml = fkyaml::node::deserialize(begin, end); if (yaml.contains("gradient")) { const auto& g = yaml["gradient"]; auto& cfg = activeConfig(); readField(g, "inner_ratio", cfg.inner_ratio); readField(g, "mid_ratio", cfg.mid_ratio); readField(g, "inner_intensity", cfg.inner_intensity); readField(g, "mid_intensity", cfg.mid_intensity); readField(g, "outer_intensity", cfg.outer_intensity); } if (yaml.contains("flicker")) { const auto& f = yaml["flicker"]; auto& fc = activeFlicker(); readField(f, "wave_amplitude_px", fc.wave_amplitude_px); readField(f, "wave_frequency_hz", fc.wave_frequency_hz); readField(f, "noise_amplitude_px", fc.noise_amplitude_px); readField(f, "noise_frequency_hz", fc.noise_frequency_hz); } } catch (const std::exception& ex) { Log::warn("Lighting", "error parsejant {}: {} — s'usaran els defaults compilats", yaml_path, ex.what()); } } 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 emitters) { auto& lum = luminanceBuffer(); const std::size_t TOTAL = static_cast(width) * static_cast(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(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 emitters) { darkenCanvas(canvas, width, height, room); const auto& cfg = activeConfig(); const auto& fc = activeFlicker(); const float T_S = static_cast(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 emitters) { if (canvas == nullptr || width <= 0 || height <= 0) { return; } // 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; } if (mode == Mode::Additive) { applyAdditive(canvas, width, height, ROOM, emitters); } else { applySubtractive(canvas, width, height, ROOM, emitters); } } } // namespace Lighting