#include "core/rendering/lighting.hpp" #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_; } // 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); } } 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(); for (const auto& e : emitters) { if (e.radius <= 0) { continue; } const auto RINGS = computeRings(e.radius, cfg); rasterizeEmitter(e, RINGS, lum.data(), width, height); } modulateCanvas(canvas, lum.data(), width, height); } } // namespace Lighting