feat(lighting): flicker tipus foc per antorxes

This commit is contained in:
JailDesigner committed 2026-05-12 22:07:25 +02:00
1 parent 3195746189
commit 35aaa6c519
8 files changed
+124 -16

No files matched your search

+14
View File
@@ -20,3 +20,17 @@ gradient:
inner_intensity: 1.0 inner_intensity: 1.0
mid_intensity: 0.9 mid_intensity: 0.9
outer_intensity: 0.8 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
+8 -3
View File
@@ -17,8 +17,13 @@ flags:
animations: animations:
idle: { frames: [0, 1, 2, 3], fps: 8, loop: true } 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 # Cercle de llum centrat a l'antorxa. El radi és en píxels del canvas;
# canvas; el gradient global (data/config/lighting.yaml) hi posa la # el gradient global (data/config/lighting.yaml) hi posa la forma (3
# forma (3 anells). Omet el bloc per fer una antorxa "apagada". # 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: light:
radius: 40 radius: 40
flicker: true
+49 -2
View File
@@ -1,8 +1,11 @@
#include "core/rendering/lighting.hpp" #include "core/rendering/lighting.hpp"
#include <SDL3/SDL.h>
#include <algorithm> #include <algorithm>
#include <cmath> #include <cmath>
#include <cstddef> #include <cstddef>
#include <numbers>
#include <vector> #include <vector>
#include "core/resources/resource_helper.hpp" #include "core/resources/resource_helper.hpp"
@@ -36,6 +39,32 @@ namespace Lighting {
return cfg_; 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<float>;
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<int>(std::lround(DELTA));
return std::max(1, R);
}
// Quantitza un factor 0..1 a 0..255. `clamp` per defensar-nos de // 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à. // YAMLs que entreguen valors fora de rang sense forçar la mà.
auto toByte(float v) -> std::uint8_t { auto toByte(float v) -> std::uint8_t {
@@ -154,6 +183,14 @@ namespace Lighting {
readField(g, "mid_intensity", cfg.mid_intensity); readField(g, "mid_intensity", cfg.mid_intensity);
readField(g, "outer_intensity", cfg.outer_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) { } catch (const std::exception& ex) {
Log::warn("Lighting", "error parsejant {}: {} — s'usaran els defaults compilats", yaml_path, ex.what()); 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); std::ranges::fill(lum, ROOM);
const auto& cfg = activeConfig(); const auto& cfg = activeConfig();
const auto& fc = activeFlicker();
const float T_S = static_cast<float>(SDL_GetTicks()) / 1000.0F;
for (const auto& e : emitters) { for (const auto& e : emitters) {
if (e.radius <= 0) { continue; } if (e.radius <= 0) { continue; }
const auto RINGS = computeRings(e.radius, cfg); const int R_EFF = e.flicker
rasterizeEmitter(e, RINGS, lum.data(), width, height); ? 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); modulateCanvas(canvas, lum.data(), width, height);
+17
View File
@@ -27,10 +27,15 @@ namespace Lighting {
// Posició al canvas (en píxels, mateixa unitat que la resta del // 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 // motor) i radi efectiu de l'emissor. El centre és el del sprite
// visible: és l'entitat qui sap on cau (veure `Entity::getLightCenter`). // 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 { struct Emitter {
float cx{0.0F}; float cx{0.0F};
float cy{0.0F}; float cy{0.0F};
int radius{0}; int radius{0};
bool flicker{false};
}; };
// Forma del gradient. Compartida per tots els emissors per evitar // Forma del gradient. Compartida per tots els emissors per evitar
@@ -43,6 +48,18 @@ namespace Lighting {
float outer_intensity{0.8F}; 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 // Llegeix `yaml_path` (data/config/lighting.yaml) i pobla la
// configuració global. Qualsevol clau absent cau als Defaults::Lighting. // 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 // No es crida més d'una vegada en un run normal; tornar-la a cridar
+8
View File
@@ -259,6 +259,13 @@ class Entity {
[[nodiscard]] auto getLightRadius() const -> int { return light_radius_; } [[nodiscard]] auto getLightRadius() const -> int { return light_radius_; }
void setLightRadius(int r) { light_radius_ = r; } 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 // 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). // `pixel_rect_` (que és la caixa de render: sprite top-left + mida).
// Virtual perquè entitats amb desplaçament visual sobre la posició // Virtual perquè entitats amb desplaçament visual sobre la posició
@@ -280,6 +287,7 @@ class Entity {
bool blocks_movement_{false}; bool blocks_movement_{false};
int height_{0}; int height_{0};
int light_radius_{0}; int light_radius_{0};
bool light_flicker_{false};
bool dead_{false}; bool dead_{false};
SDL_FlipMode flip_{SDL_FLIP_NONE}; SDL_FlipMode flip_{SDL_FLIP_NONE};
EventBus* bus_{nullptr}; EventBus* bus_{nullptr};
@@ -155,15 +155,23 @@ namespace {
return c["height"].get_value<int>(); return c["height"].get_value<int>();
} }
// Bloc opcional `light: { radius: N }` de la plantilla. Si manca, el // Bloc opcional `light: { radius: N, flicker: bool }` de la plantilla.
// radi és 0 i l'entitat no entra a la pasada d'il·luminació. La forma // Si manca, el radi és 0 i l'entitat no entra a la pasada d'il·luminació.
// del gradient (3 anells) viu a data/config/lighting.yaml — no cal // `flicker` activa la modulació "tipus foc" del radi (només antorxes
// declarar-la per entitat. // l'haurien d'usar). La forma del gradient (3 anells) i els paràmetres
auto readLightRadius(const fkyaml::node& tmpl) -> int { // del flicker viuen a data/config/lighting.yaml — no cal declarar-los
if (!tmpl.contains("light")) { return 0; } // 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"]; const auto& l = tmpl["light"];
if (!l.contains("radius")) { return 0; } LightTemplate out;
return l["radius"].get_value<int>(); if (l.contains("radius")) { out.radius = l["radius"].get_value<int>(); }
if (l.contains("flicker")) { out.flicker = l["flicker"].get_value<bool>(); }
return out;
} }
// Bloc `collision: { size: [w, h], base_size: [bw, bh]?, offset: [x, y]? }` // Bloc `collision: { size: [w, h], base_size: [bw, bh]?, offset: [x, y]? }`
@@ -288,7 +296,9 @@ namespace {
entity->setCollisionBox(*BOX); entity->setCollisionBox(*BOX);
} }
entity->setHeight(readHeight(tmpl)); 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); anchor_out = readAnchor(tmpl);
return entity; return entity;
} }
+2 -2
View File
@@ -1021,13 +1021,13 @@ void GameScene::applyLightingPass() {
for (const auto& p : players_) { for (const auto& p : players_) {
if (p->isOutOfPlay()) { continue; } if (p->isOutOfPlay()) { continue; }
const auto C = p->getLightCenter(); 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()) { for (const auto* e : entity_manager_.getEntities()) {
const int R = e->getLightRadius(); const int R = e->getLightRadius();
if (R <= 0) { continue; } if (R <= 0) { continue; }
const auto C = e->getLightCenter(); 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(), Lighting::apply(Screen::get().getCanvasArgb(),
Screen::get().getCanvasWidth(), Screen::get().getCanvasWidth(),
+7
View File
@@ -119,6 +119,13 @@ namespace Defaults {
// Nivell d'il·luminació de sala per defecte. Una sala sense la // Nivell d'il·luminació de sala per defecte. Una sala sense la
// clau `light_level` queda totalment il·luminada (com hui). // clau `light_level` queda totalment il·luminada (com hui).
constexpr float ROOM_LIGHT_DEFAULT = 1.0F; 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 } // namespace Lighting
// Fallbacks del tema d'UI: índexs de paleta, timings (ms) i fonts. // Fallbacks del tema d'UI: índexs de paleta, timings (ms) i fonts.