feat(lighting): flicker tipus foc per antorxes
This commit is contained in:
1 parent
3195746189
commit
35aaa6c519
8 files changed
+124
-16
No files matched your search
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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);
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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(),
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
Reference in new issue
Block a user