From 35aaa6c519bace4f221ca7c13895e6023705269f Mon Sep 17 00:00:00 2001 From: Sergio Valor Date: Tue, 12 May 2026 22:07:25 +0200 Subject: [PATCH] feat(lighting): flicker tipus foc per antorxes --- data/config/lighting.yaml | 14 +++++ data/entities/torch/torch.yaml | 11 ++-- source/core/rendering/lighting.cpp | 51 ++++++++++++++++++- source/core/rendering/lighting.hpp | 17 +++++++ source/game/entities/entity.hpp | 8 +++ .../entities/entity_manager_loadlevel.cpp | 28 ++++++---- source/game/scenes/game_scene.cpp | 4 +- source/utils/defaults.hpp | 7 +++ 8 files changed, 124 insertions(+), 16 deletions(-) diff --git a/data/config/lighting.yaml b/data/config/lighting.yaml index 68c8a2e..e4000b7 100644 --- a/data/config/lighting.yaml +++ b/data/config/lighting.yaml @@ -20,3 +20,17 @@ gradient: inner_intensity: 1.0 mid_intensity: 0.9 outer_intensity: 0.8 + +# Modulació del radi quan una entitat declara `flicker: true` al seu +# bloc `light:`. El radi efectiu per frame és: +# r_eff = r_base + A1·sin(2π·f1·t + φ1) + A2·sin(2π·f2·t + φ2) +# on (φ1, φ2) es deriven de la posició de l'emissor per a que cada +# antorxa balancegi en una fase diferent (sense estat per emissor). +# Combinant dues sinusoides amb freqüències incommensurables s'obté +# un patró "fluid" que el ull llegeix com a foc; pujar `noise_*` +# aporta més inquietud, baixar-ho fa la flama més serena. +flicker: + wave_amplitude_px: 2 + wave_frequency_hz: 3.0 + noise_amplitude_px: 1 + noise_frequency_hz: 7.0 diff --git a/data/entities/torch/torch.yaml b/data/entities/torch/torch.yaml index ed78bc2..488cdb9 100644 --- a/data/entities/torch/torch.yaml +++ b/data/entities/torch/torch.yaml @@ -17,8 +17,13 @@ flags: animations: idle: { frames: [0, 1, 2, 3], fps: 8, loop: true } -# Cercle de llum estàtic centrat a l'antorxa. El radi és en píxels del -# canvas; el gradient global (data/config/lighting.yaml) hi posa la -# forma (3 anells). Omet el bloc per fer una antorxa "apagada". +# Cercle de llum centrat a l'antorxa. El radi és en píxels del canvas; +# el gradient global (data/config/lighting.yaml) hi posa la forma (3 +# anells). Amb `flicker: true` el radi efectiu oscil·la per frame +# (suma de dues sinusoides amb fase derivada de la posició) per a +# que el cercle "respiri" com una flama; els paràmetres del flicker +# també viuen al config global. Omet el bloc per fer una antorxa +# "apagada". light: radius: 40 + flicker: true diff --git a/source/core/rendering/lighting.cpp b/source/core/rendering/lighting.cpp index ed9076a..65b9802 100644 --- a/source/core/rendering/lighting.cpp +++ b/source/core/rendering/lighting.cpp @@ -1,8 +1,11 @@ #include "core/rendering/lighting.hpp" +#include + #include #include #include +#include #include #include "core/resources/resource_helper.hpp" @@ -36,6 +39,32 @@ namespace Lighting { 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 { @@ -154,6 +183,14 @@ namespace Lighting { 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()); } @@ -175,10 +212,20 @@ namespace Lighting { 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 auto RINGS = computeRings(e.radius, cfg); - rasterizeEmitter(e, RINGS, lum.data(), width, height); + 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); diff --git a/source/core/rendering/lighting.hpp b/source/core/rendering/lighting.hpp index 54781d5..35ee5ca 100644 --- a/source/core/rendering/lighting.hpp +++ b/source/core/rendering/lighting.hpp @@ -27,10 +27,15 @@ namespace Lighting { // 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 (només antorxes + // avui): el radi base es queda al YAML, l'oscil·lació en viu surt + // dels paràmetres globals i de la posició per generar fases + // distintes a cada emissor. struct Emitter { float cx{0.0F}; float cy{0.0F}; int radius{0}; + bool flicker{false}; }; // Forma del gradient. Compartida per tots els emissors per evitar @@ -43,6 +48,18 @@ namespace Lighting { float outer_intensity{0.8F}; }; + // Paràmetres del flicker per a emissors amb `flicker=true`. La + // forma final del radi és la suma d'una sinusoide "respiració" + // (wave_*) i una sinusoide secundària de freqüència més alta + // (noise_*) que aporta la sensació d'inquietud. Les fases es + // deriven de la posició de cada emissor — sense estat per emissor. + struct FlickerConfig { + float wave_amplitude_px{2.0F}; + float wave_frequency_hz{3.0F}; + float noise_amplitude_px{1.0F}; + float noise_frequency_hz{7.0F}; + }; + // Llegeix `yaml_path` (data/config/lighting.yaml) i pobla la // configuració global. Qualsevol clau absent cau als Defaults::Lighting. // No es crida més d'una vegada en un run normal; tornar-la a cridar diff --git a/source/game/entities/entity.hpp b/source/game/entities/entity.hpp index 0aa76f5..0477628 100644 --- a/source/game/entities/entity.hpp +++ b/source/game/entities/entity.hpp @@ -259,6 +259,13 @@ class Entity { [[nodiscard]] auto getLightRadius() const -> int { return light_radius_; } void setLightRadius(int r) { light_radius_ = r; } + // Activa la modulació "tipus foc" del radi a la pasada d'il·luminació + // (per a antorxes, fogueres, etc.). El bloc opcional + // `light: { flicker: true }` del YAML el pobla. Per defecte false: + // la llum queda perfectament estable (cas player, mòmia). + [[nodiscard]] auto getLightFlicker() const -> bool { return light_flicker_; } + void setLightFlicker(bool f) { light_flicker_ = f; } + // Centre de la llum en píxels del canvas. Per defecte és el centre del // `pixel_rect_` (que és la caixa de render: sprite top-left + mida). // Virtual perquè entitats amb desplaçament visual sobre la posició @@ -280,6 +287,7 @@ class Entity { bool blocks_movement_{false}; int height_{0}; int light_radius_{0}; + bool light_flicker_{false}; bool dead_{false}; SDL_FlipMode flip_{SDL_FLIP_NONE}; EventBus* bus_{nullptr}; diff --git a/source/game/entities/entity_manager_loadlevel.cpp b/source/game/entities/entity_manager_loadlevel.cpp index 6a1e9ab..cde7a88 100644 --- a/source/game/entities/entity_manager_loadlevel.cpp +++ b/source/game/entities/entity_manager_loadlevel.cpp @@ -155,15 +155,23 @@ namespace { return c["height"].get_value(); } - // Bloc opcional `light: { radius: N }` de la plantilla. Si manca, el - // radi és 0 i l'entitat no entra a la pasada d'il·luminació. La forma - // del gradient (3 anells) viu a data/config/lighting.yaml — no cal - // declarar-la per entitat. - auto readLightRadius(const fkyaml::node& tmpl) -> int { - if (!tmpl.contains("light")) { return 0; } + // Bloc opcional `light: { radius: N, flicker: bool }` de la plantilla. + // Si manca, el radi és 0 i l'entitat no entra a la pasada d'il·luminació. + // `flicker` activa la modulació "tipus foc" del radi (només antorxes + // l'haurien d'usar). La forma del gradient (3 anells) i els paràmetres + // del flicker viuen a data/config/lighting.yaml — no cal declarar-los + // per entitat. + struct LightTemplate { + int radius{0}; + bool flicker{false}; + }; + auto readLight(const fkyaml::node& tmpl) -> LightTemplate { + if (!tmpl.contains("light")) { return {}; } const auto& l = tmpl["light"]; - if (!l.contains("radius")) { return 0; } - return l["radius"].get_value(); + LightTemplate out; + if (l.contains("radius")) { out.radius = l["radius"].get_value(); } + if (l.contains("flicker")) { out.flicker = l["flicker"].get_value(); } + return out; } // Bloc `collision: { size: [w, h], base_size: [bw, bh]?, offset: [x, y]? }` @@ -288,7 +296,9 @@ namespace { entity->setCollisionBox(*BOX); } entity->setHeight(readHeight(tmpl)); - entity->setLightRadius(readLightRadius(tmpl)); + const auto LIGHT = readLight(tmpl); + entity->setLightRadius(LIGHT.radius); + entity->setLightFlicker(LIGHT.flicker); anchor_out = readAnchor(tmpl); return entity; } diff --git a/source/game/scenes/game_scene.cpp b/source/game/scenes/game_scene.cpp index 2d807bb..2ffda5c 100644 --- a/source/game/scenes/game_scene.cpp +++ b/source/game/scenes/game_scene.cpp @@ -1021,13 +1021,13 @@ void GameScene::applyLightingPass() { for (const auto& p : players_) { if (p->isOutOfPlay()) { continue; } const auto C = p->getLightCenter(); - emitters.push_back({.cx = C.x, .cy = C.y, .radius = PLAYER_R}); + emitters.push_back({.cx = C.x, .cy = C.y, .radius = PLAYER_R, .flicker = false}); } for (const auto* e : entity_manager_.getEntities()) { const int R = e->getLightRadius(); if (R <= 0) { continue; } const auto C = e->getLightCenter(); - emitters.push_back({.cx = C.x, .cy = C.y, .radius = R}); + emitters.push_back({.cx = C.x, .cy = C.y, .radius = R, .flicker = e->getLightFlicker()}); } Lighting::apply(Screen::get().getCanvasArgb(), Screen::get().getCanvasWidth(), diff --git a/source/utils/defaults.hpp b/source/utils/defaults.hpp index 9eb0a70..7f11e7f 100644 --- a/source/utils/defaults.hpp +++ b/source/utils/defaults.hpp @@ -119,6 +119,13 @@ namespace Defaults { // Nivell d'il·luminació de sala per defecte. Una sala sense la // clau `light_level` queda totalment il·luminada (com hui). constexpr float ROOM_LIGHT_DEFAULT = 1.0F; + // Defaults del flicker. Els reemplaça el bloc `flicker:` de + // data/config/lighting.yaml. Veure `Lighting::FlickerConfig` + // per a la semàntica de cada camp. + constexpr float FLICKER_WAVE_AMPLITUDE_PX = 2.0F; + constexpr float FLICKER_WAVE_FREQUENCY_HZ = 3.0F; + constexpr float FLICKER_NOISE_AMPLITUDE_PX = 1.0F; + constexpr float FLICKER_NOISE_FREQUENCY_HZ = 7.0F; } // namespace Lighting // Fallbacks del tema d'UI: índexs de paleta, timings (ms) i fonts.