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

+49 -2
View File
@@ -1,8 +1,11 @@
#include "core/rendering/lighting.hpp"
#include <SDL3/SDL.h>
#include <algorithm>
#include <cmath>
#include <cstddef>
#include <numbers>
#include <vector>
#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<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
// 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<float>(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);
+17
View File
@@ -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
+8
View File
@@ -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};
@@ -155,15 +155,23 @@ namespace {
return c["height"].get_value<int>();
}
// 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<int>();
LightTemplate out;
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]? }`
@@ -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;
}
+2 -2
View File
@@ -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(),
+7
View File
@@ -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.