#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 la posició. Per a dos // antorxes diferents (cx, cy) les fases queden incommensurables // entre si, així que mai oscil·len al unison — patró típic de // foc. Multipliquem cx i cy per constants irracionals petites // per evitar fases "rodones" quan la posició és múltiple de 16. auto computeFlickerRadius(int base_radius, float cx, float cy, float t_s, const FlickerConfig& fc) -> int { constexpr float TWO_PI = 2.0F * std::numbers::pi_v; const float PHASE = (cx * 0.0731F) + (cy * 0.1373F); 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: 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); } } } } // 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()); } } void apply(std::uint32_t* canvas, int width, int height, float room_light, std::span 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. const std::uint8_t ROOM = toByte(room_light); if (ROOM == 255 && emitters.empty()) { return; } 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.cx, e.cy, 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, .flicker = e.flicker}; rasterizeEmitter(EFF, RINGS, lum.data(), width, height); } modulateCanvas(canvas, lum.data(), width, height); } } // namespace Lighting