feat(lighting): flicker proporcional al radi amb overrides per emissor
This commit is contained in:
1 parent
3a42c7958a
commit
2272d3a37a
13 files changed
+298
-150
No files matched your search
@@ -57,9 +57,9 @@ namespace Lighting {
|
||||
|
||||
auto activeFlicker() -> FlickerConfig& {
|
||||
static FlickerConfig fc_{
|
||||
.wave_amplitude_px = Defaults::Lighting::FLICKER_WAVE_AMPLITUDE_PX,
|
||||
.wave_amplitude_ratio = Defaults::Lighting::FLICKER_WAVE_AMPLITUDE_RATIO,
|
||||
.wave_frequency_hz = Defaults::Lighting::FLICKER_WAVE_FREQUENCY_HZ,
|
||||
.noise_amplitude_px = Defaults::Lighting::FLICKER_NOISE_AMPLITUDE_PX,
|
||||
.noise_amplitude_ratio = Defaults::Lighting::FLICKER_NOISE_AMPLITUDE_RATIO,
|
||||
.noise_frequency_hz = Defaults::Lighting::FLICKER_NOISE_FREQUENCY_HZ,
|
||||
};
|
||||
return fc_;
|
||||
@@ -70,13 +70,16 @@ namespace Lighting {
|
||||
// identificador estable de l'emissor (no canvia amb la posició) —
|
||||
// així el patró no "deriva" quan l'emissor es mou. El factor
|
||||
// ~φ (golden ratio) dispersa les fases per a seeds consecutives.
|
||||
// Les amplituds són ràtios sobre el radi base, multiplicades aquí
|
||||
// → mateix percentatge d'oscil·lació per a qualsevol mida.
|
||||
auto computeFlickerRadius(int base_radius, int seed, float t_s, const FlickerConfig& fc) -> int {
|
||||
constexpr float TWO_PI = 2.0F * std::numbers::pi_v<float>;
|
||||
constexpr float GOLDEN_FRACTION = 0.6180339F;
|
||||
const float PHASE = static_cast<float>(seed) * GOLDEN_FRACTION;
|
||||
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 auto R_F = static_cast<float>(base_radius);
|
||||
const float DELTA = (fc.wave_amplitude_ratio * R_F * W1) + (fc.noise_amplitude_ratio * R_F * W2);
|
||||
const int R = base_radius + static_cast<int>(std::lround(DELTA));
|
||||
return std::max(1, R);
|
||||
}
|
||||
@@ -251,6 +254,10 @@ namespace Lighting {
|
||||
return activeSubtractiveConfig();
|
||||
}
|
||||
|
||||
auto flickerConfig() -> const FlickerConfig& {
|
||||
return activeFlicker();
|
||||
}
|
||||
|
||||
namespace {
|
||||
// Llegeix els 5 camps de `GradientConfig` d'un node YAML — qualsevol
|
||||
// camp absent conserva el valor compilat dels defaults. Compartit
|
||||
@@ -283,9 +290,9 @@ namespace Lighting {
|
||||
if (yaml.contains("flicker")) {
|
||||
const auto& f = yaml["flicker"];
|
||||
auto& fc = activeFlicker();
|
||||
readField(f, "wave_amplitude_px", fc.wave_amplitude_px);
|
||||
readField(f, "wave_amplitude_ratio", fc.wave_amplitude_ratio);
|
||||
readField(f, "wave_frequency_hz", fc.wave_frequency_hz);
|
||||
readField(f, "noise_amplitude_px", fc.noise_amplitude_px);
|
||||
readField(f, "noise_amplitude_ratio", fc.noise_amplitude_ratio);
|
||||
readField(f, "noise_frequency_hz", fc.noise_frequency_hz);
|
||||
}
|
||||
} catch (const std::exception& ex) {
|
||||
@@ -297,9 +304,17 @@ namespace Lighting {
|
||||
|
||||
// Helper: emissor "patxat" amb el radi efectiu (post-flicker) per
|
||||
// a aquest frame. Mantenim l'`Emitter` del caller intacte i la
|
||||
// resta de camps (color, seed, ...) propagats.
|
||||
// resta de camps (color, seed, flicker_params, ...) propagats.
|
||||
auto withEffectiveRadius(const Emitter& e, int r_eff) -> Emitter {
|
||||
return Emitter{.cx = e.cx, .cy = e.cy, .radius = r_eff, .seed = e.seed, .flicker = e.flicker, .color = e.color};
|
||||
return Emitter{
|
||||
.cx = e.cx,
|
||||
.cy = e.cy,
|
||||
.radius = r_eff,
|
||||
.seed = e.seed,
|
||||
.flicker = e.flicker,
|
||||
.color = e.color,
|
||||
.flicker_params = e.flicker_params,
|
||||
};
|
||||
}
|
||||
|
||||
// Camí del mode subtractive (històric): omple un buffer de
|
||||
@@ -312,11 +327,10 @@ namespace Lighting {
|
||||
std::ranges::fill(lum, room);
|
||||
|
||||
const auto& cfg = activeSubtractiveConfig();
|
||||
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 int R_EFF = e.flicker ? computeFlickerRadius(e.radius, e.seed, T_S, fc) : e.radius;
|
||||
const int R_EFF = e.flicker ? computeFlickerRadius(e.radius, e.seed, T_S, e.flicker_params) : e.radius;
|
||||
const auto RINGS = computeRings(R_EFF, cfg);
|
||||
const Emitter EFF = withEffectiveRadius(e, R_EFF);
|
||||
rasterizeEmitter(EFF, RINGS, lum.data(), width, height);
|
||||
@@ -330,11 +344,10 @@ namespace Lighting {
|
||||
void applyAdditive(std::uint32_t* canvas, int width, int height, std::uint8_t room, std::span<const Emitter> emitters) {
|
||||
darkenCanvas(canvas, width, height, room);
|
||||
const auto& cfg = activeAdditiveConfig();
|
||||
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 int R_EFF = e.flicker ? computeFlickerRadius(e.radius, e.seed, T_S, fc) : e.radius;
|
||||
const int R_EFF = e.flicker ? computeFlickerRadius(e.radius, e.seed, T_S, e.flicker_params) : e.radius;
|
||||
const auto RINGS = computeRings(R_EFF, cfg);
|
||||
const Emitter EFF = withEffectiveRadius(e, R_EFF);
|
||||
rasterizeEmitterAdditive(EFF, RINGS, canvas, width, height);
|
||||
|
||||
@@ -67,6 +67,32 @@ namespace Lighting {
|
||||
std::uint8_t b{255};
|
||||
};
|
||||
|
||||
// Forma del gradient. Compartida per tots els emissors per evitar
|
||||
// duplicar la corba a cada YAML d'entitat. Càrrega via loadConfig.
|
||||
struct GradientConfig {
|
||||
float inner_ratio{0.8F};
|
||||
float mid_ratio{0.9F};
|
||||
float inner_intensity{1.0F};
|
||||
float mid_intensity{0.9F};
|
||||
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 seed estable de cada emissor — sense estat per
|
||||
// emissor. Les amplituds són **ràtios** sobre el radi base (p.ex.
|
||||
// 0.05 = 5% del radi): així una torxa de radi 40 i una de radi 24
|
||||
// tenen el mateix percentatge d'oscil·lació visual, no els mateixos
|
||||
// píxels (que es notarien més en la petita).
|
||||
struct FlickerConfig {
|
||||
float wave_amplitude_ratio{0.05F};
|
||||
float wave_frequency_hz{3.0F};
|
||||
float noise_amplitude_ratio{0.025F};
|
||||
float noise_frequency_hz{7.0F};
|
||||
};
|
||||
|
||||
// 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`).
|
||||
@@ -82,28 +108,12 @@ namespace Lighting {
|
||||
int seed{0};
|
||||
bool flicker{false};
|
||||
Color color{};
|
||||
};
|
||||
|
||||
// Forma del gradient. Compartida per tots els emissors per evitar
|
||||
// duplicar la corba a cada YAML d'entitat. Càrrega via loadConfig.
|
||||
struct GradientConfig {
|
||||
float inner_ratio{0.8F};
|
||||
float mid_ratio{0.9F};
|
||||
float inner_intensity{1.0F};
|
||||
float mid_intensity{0.9F};
|
||||
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};
|
||||
// Paràmetres del flicker resolts per a aquest emissor (combinació
|
||||
// global + override del tipus/sala/spawn). Només té sentit si
|
||||
// `flicker == true`; en cas contrari s'ignoren. Per defecte
|
||||
// s'inicialitza amb els defaults compilats — el caller (game_scene)
|
||||
// hi posa el resolt correcte.
|
||||
FlickerConfig flicker_params{};
|
||||
};
|
||||
|
||||
// Llegeix `yaml_path` (data/config/lighting.yaml) i pobla la
|
||||
@@ -119,6 +129,12 @@ namespace Lighting {
|
||||
// Defaults::Lighting fins que loadConfig() es crida.
|
||||
[[nodiscard]] auto config() -> const GradientConfig&;
|
||||
|
||||
// Defaults globals del flicker (data/config/lighting.yaml, bloc
|
||||
// `flicker:`). Els callers del construct path d'entitats el llegeixen
|
||||
// per inicialitzar la `FlickerConfig` de cada emissor, i aplicar a
|
||||
// sobre els overrides per tipus/sala/spawn.
|
||||
[[nodiscard]] auto flickerConfig() -> const FlickerConfig&;
|
||||
|
||||
// Aplica la pasada d'il·luminació al canvas in-place. `mode`
|
||||
// selecciona la composició (vegeu el comentari de la capçalera).
|
||||
// `room_light` és l'atenuació ambient de la sala (0..1, 1 = sense
|
||||
|
||||
Reference in new issue
Block a user