Merge branch 'feature/lighting-additive'
This commit is contained in:
commit
15a4016407
15 files changed
+794
-210
No files matched your search
@@ -12,8 +12,13 @@
|
|||||||
# mid_ratio..1.0 → outer_intensity (anell exterior)
|
# mid_ratio..1.0 → outer_intensity (anell exterior)
|
||||||
# fora del radi → light_level de la sala
|
# fora del radi → light_level de la sala
|
||||||
#
|
#
|
||||||
# Les intensitats són factors 0..1 que es multipliquen sobre el píxel
|
# Hi ha dues corbes paral·leles, una per mode, perquè la mateixa escala
|
||||||
# final del canvas. inner=1.0 vol dir "píxel sense alterar".
|
# 0..1 vol dir coses molt diferents:
|
||||||
|
# `gradient` → subtractive: 1.0 = "no fosqueix" el píxel
|
||||||
|
# (l'emissor "trenca" una sala fosca).
|
||||||
|
# `gradient_additive` → additive: 1.0 = "afegeix el color sencer"
|
||||||
|
# (satura). Valors molt més baixos donen un
|
||||||
|
# halo cromàtic subtil sobre una sala normal.
|
||||||
gradient:
|
gradient:
|
||||||
inner_ratio: 0.5
|
inner_ratio: 0.5
|
||||||
mid_ratio: 0.85
|
mid_ratio: 0.85
|
||||||
@@ -21,16 +26,31 @@ gradient:
|
|||||||
mid_intensity: 0.8
|
mid_intensity: 0.8
|
||||||
outer_intensity: 0.6
|
outer_intensity: 0.6
|
||||||
|
|
||||||
|
# Corba del mode additive. Valors petits a propòsit: només volem un
|
||||||
|
# toc de tint cromàtic prop de l'emissor — fora del nucli, la sala
|
||||||
|
# original ha de quedar pràcticament intacta. Si pugeu massa l'inner,
|
||||||
|
# els colors de l'emissor "inunden" la sala (additiu satura ràpid).
|
||||||
|
gradient_additive:
|
||||||
|
inner_ratio: 0.5
|
||||||
|
mid_ratio: 0.85
|
||||||
|
inner_intensity: 0.15
|
||||||
|
mid_intensity: 0.10
|
||||||
|
outer_intensity: 0.05
|
||||||
|
|
||||||
# Modulació del radi quan una entitat declara `flicker: true` al seu
|
# Modulació del radi quan una entitat declara `flicker: true` al seu
|
||||||
# bloc `light:`. El radi efectiu per frame és:
|
# bloc `light:`. El radi efectiu per frame és:
|
||||||
# r_eff = r_base + A1·sin(2π·f1·t + φ1) + A2·sin(2π·f2·t + φ2)
|
# r_eff = r_base + (r_amp1·r_base)·sin(2π·f1·t + φ1) + (r_amp2·r_base)·sin(2π·f2·t + φ2)
|
||||||
# on (φ1, φ2) es deriven de la posició de l'emissor per a que cada
|
# on (φ1, φ2) es deriven de la seed de l'emissor per a que cada
|
||||||
# antorxa balancegi en una fase diferent (sense estat per emissor).
|
# antorxa balancegi en una fase diferent (sense estat per emissor).
|
||||||
# Combinant dues sinusoides amb freqüències incommensurables s'obté
|
# Les amplituds són **ràtios** sobre el radi base: així el feel del
|
||||||
# un patró "fluid" que el ull llegeix com a foc; pujar `noise_*`
|
# flicker és independent del radi (una torxa de 24 px oscil·la el
|
||||||
# aporta més inquietud, baixar-ho fa la flama més serena.
|
# mateix % que una de 40 px). 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. Aquests són els defaults globals; cada tipus d'entitat
|
||||||
|
# (o cada spawn / sala) pot sobreescriure'ls al seu `light.flicker_params:`.
|
||||||
flicker:
|
flicker:
|
||||||
wave_amplitude_px: 2
|
wave_amplitude_ratio: 0.05 # 5% del radi (per a radi 40 → ±2 px, feel actual)
|
||||||
wave_frequency_hz: 3.0
|
wave_frequency_hz: 3.0
|
||||||
noise_amplitude_px: 1
|
noise_amplitude_ratio: 0.025 # 2.5% (per a radi 40 → ±1 px, feel actual)
|
||||||
noise_frequency_hz: 7.0
|
noise_frequency_hz: 7.0
|
||||||
@@ -22,8 +22,12 @@ animations:
|
|||||||
# anells). Amb `flicker: true` el radi efectiu oscil·la per frame
|
# anells). Amb `flicker: true` el radi efectiu oscil·la per frame
|
||||||
# (suma de dues sinusoides amb fase derivada de la posició) per a
|
# (suma de dues sinusoides amb fase derivada de la posició) per a
|
||||||
# que el cercle "respiri" com una flama; els paràmetres del flicker
|
# que el cercle "respiri" com una flama; els paràmetres del flicker
|
||||||
# també viuen al config global. Omet el bloc per fer una antorxa
|
# també viuen al config global. `offset: [x, y]` desplaça el centre
|
||||||
# "apagada".
|
# del cercle: la flama del sprite no està al centre del bounding box
|
||||||
|
# sinó cap a dalt, així que pujem la llum 4 px (Y creix cap avall)
|
||||||
|
# per a que el halo "neixi" de la flama, no del peu. Omet el bloc
|
||||||
|
# per fer una antorxa "apagada".
|
||||||
light:
|
light:
|
||||||
radius: 40
|
radius: 40
|
||||||
flicker: true
|
flicker: true
|
||||||
|
offset: [0, -4]
|
||||||
+32
-58
@@ -10,35 +10,35 @@
|
|||||||
# la cel·la buida (transparent), útil per caselles que ocuparà un actor.
|
# la cel·la buida (transparent), útil per caselles que ocuparà un actor.
|
||||||
|
|
||||||
pyramid: 1
|
pyramid: 1
|
||||||
room: 1
|
room: 2
|
||||||
tileset: data/tilesets/pyramid_1.gif
|
tileset: data/tilesets/pyramid_1.gif
|
||||||
tile_size: 16
|
tile_size: 16
|
||||||
origin: [-8, 0]
|
origin: [-8, 0]
|
||||||
|
|
||||||
# Il·luminació de la sala. Bloc opcional; si manca, la sala queda
|
# Banc de proves del mode `additive` d'il·luminació. La sala es veu
|
||||||
# totalment il·luminada (1.0) i tots els tipus d'entitat emeten amb els
|
# "normal" (level 1.0, el GIF intacte fora del cercle de les torxes)
|
||||||
# valors declarats al seu YAML de tipus (comportament històric).
|
# i només les antorxes hi sumen un halo càlid suau. Player i mummies
|
||||||
#
|
# desactivats explícitament perquè aquí no volem cap halo al voltant
|
||||||
# level → atenuació base de la sala (0..1). 1.0 = res
|
# d'actors — el toc cromàtic és només l'antorxa. La corba additiva
|
||||||
# d'efecte, 0.0 = negre fora del radi dels emissors.
|
# (intensitats baixes a propòsit perquè additive satura ràpid) viu a
|
||||||
# El gradient dels emissors viu a
|
# `gradient_additive:` de data/config/lighting.yaml.
|
||||||
# data/config/lighting.yaml.
|
|
||||||
# emitters.<tipus> → override per tipus d'entitat. Tres formes:
|
|
||||||
# false → apaga el tipus
|
|
||||||
# true → defaults del tipus
|
|
||||||
# { radius: N, flicker: B } → camps presents
|
|
||||||
# sobreescriuen el
|
|
||||||
# default; els
|
|
||||||
# absents cauen al
|
|
||||||
# default del tipus.
|
|
||||||
# Plural per llegir-se natural ("totes les momies de la sala"). Per a
|
|
||||||
# casos particulars, un spawn pot dur el seu propi `light:` (mateixes
|
|
||||||
# formes) que té prioritat sobre l'override de sala. La forma antiga
|
|
||||||
# (`light_level:` plà a l'arrel) també segueix sent vàlida per
|
|
||||||
# retrocompatibilitat.
|
|
||||||
lighting:
|
lighting:
|
||||||
level: 0.75
|
mode: additive
|
||||||
|
level: 1.0
|
||||||
emitters:
|
emitters:
|
||||||
|
torches:
|
||||||
|
radius: 20
|
||||||
|
color: [255, 170, 60]
|
||||||
|
# Valors default del flicker explicitats per anar-los ajustant
|
||||||
|
# in-situ. Coincideixen amb els del global a data/config/lighting.yaml;
|
||||||
|
# baixar wave_frequency_hz fa el "palpitar" més lent, baixar les
|
||||||
|
# ratios fa l'oscil·lació més subtil. Qualsevol camp es pot
|
||||||
|
# eliminar i caurà al valor del global.
|
||||||
|
flicker_params:
|
||||||
|
wave_amplitude_ratio: 0.04
|
||||||
|
wave_frequency_hz: 3.0
|
||||||
|
noise_amplitude_ratio: 0.025
|
||||||
|
noise_frequency_hz: 3.0
|
||||||
players: false
|
players: false
|
||||||
mummies: false
|
mummies: false
|
||||||
|
|
||||||
@@ -109,12 +109,16 @@ data:
|
|||||||
# d'obertura (262/263). La momia al corredor central. L'antorxa al costat
|
# d'obertura (262/263). La momia al corredor central. L'antorxa al costat
|
||||||
# superior esquerre per verificar render d'una entitat decorativa.
|
# superior esquerre per verificar render d'una entitat decorativa.
|
||||||
entities:
|
entities:
|
||||||
|
- type: torch
|
||||||
|
position: [4, 2]
|
||||||
- type: torch
|
- type: torch
|
||||||
position: [8, 2]
|
position: [8, 2]
|
||||||
- type: torch
|
- type: torch
|
||||||
position: [12, 2]
|
position: [12, 2]
|
||||||
- type: torch
|
- type: torch
|
||||||
position: [16, 2]
|
position: [16, 2]
|
||||||
|
- type: sarcophagus
|
||||||
|
sarc_position: [0, 0]
|
||||||
- type: sarcophagus
|
- type: sarcophagus
|
||||||
sarc_position: [1, 0]
|
sarc_position: [1, 0]
|
||||||
- type: sarcophagus
|
- type: sarcophagus
|
||||||
@@ -141,6 +145,8 @@ entities:
|
|||||||
sarc_position: [0, 3]
|
sarc_position: [0, 3]
|
||||||
- type: sarcophagus
|
- type: sarcophagus
|
||||||
sarc_position: [1, 3]
|
sarc_position: [1, 3]
|
||||||
|
- type: sarcophagus
|
||||||
|
sarc_position: [2, 3]
|
||||||
- type: sarcophagus
|
- type: sarcophagus
|
||||||
sarc_position: [3, 3]
|
sarc_position: [3, 3]
|
||||||
- type: sarcophagus
|
- type: sarcophagus
|
||||||
@@ -151,40 +157,8 @@ entities:
|
|||||||
sarc_position: [2, 4]
|
sarc_position: [2, 4]
|
||||||
- type: sarcophagus
|
- type: sarcophagus
|
||||||
sarc_position: [3, 4]
|
sarc_position: [3, 4]
|
||||||
- type: torch
|
|
||||||
position: [4, 2]
|
|
||||||
- type: door
|
- type: door
|
||||||
- type: column
|
|
||||||
position: [3, 4]
|
|
||||||
frame: 2
|
|
||||||
- type: stone
|
|
||||||
position: [5, 4]
|
|
||||||
frame: 16
|
|
||||||
- type: stone
|
|
||||||
position: [4, 4]
|
|
||||||
frame: 19
|
|
||||||
- type: column
|
|
||||||
position: [13, 10]
|
|
||||||
frame: 4
|
|
||||||
- type: stone
|
|
||||||
position: [12, 10]
|
|
||||||
frame: 13
|
|
||||||
- type: stone
|
|
||||||
position: [11, 10]
|
|
||||||
frame: 23
|
|
||||||
- type: hole
|
|
||||||
position: [5, 9]
|
|
||||||
- type: hole
|
|
||||||
position: [9, 11]
|
|
||||||
- type: stone
|
|
||||||
position: [10, 10]
|
|
||||||
frame: 18
|
|
||||||
- type: mummy
|
- type: mummy
|
||||||
position: [10, 13]
|
position: [10, 7]
|
||||||
# Override per-instància: la sala apaga les momies (emitters.mummies:
|
- type: mummy
|
||||||
# false), però aquesta concreta sí emet, amb un radi més curt que el
|
position: [7, 9]
|
||||||
# default del seu tipus (24 → 16). Bona prova de la tercera capa de
|
|
||||||
# resolució: tipus → sala → spawn.
|
|
||||||
light:
|
|
||||||
radius: 16
|
|
||||||
flicker: true
|
|
||||||
+56
-10
@@ -10,11 +10,38 @@
|
|||||||
# la cel·la buida (transparent), útil per caselles que ocuparà un actor.
|
# la cel·la buida (transparent), útil per caselles que ocuparà un actor.
|
||||||
|
|
||||||
pyramid: 1
|
pyramid: 1
|
||||||
room: 2
|
room: 1
|
||||||
tileset: data/tilesets/pyramid_1.gif
|
tileset: data/tilesets/pyramid_1.gif
|
||||||
tile_size: 16
|
tile_size: 16
|
||||||
origin: [-8, 0]
|
origin: [-8, 0]
|
||||||
|
|
||||||
|
# Il·luminació de la sala. Bloc opcional; si manca, la sala queda
|
||||||
|
# totalment il·luminada (1.0) i tots els tipus d'entitat emeten amb els
|
||||||
|
# valors declarats al seu YAML de tipus (comportament històric).
|
||||||
|
#
|
||||||
|
# level → atenuació base de la sala (0..1). 1.0 = res
|
||||||
|
# d'efecte, 0.0 = negre fora del radi dels emissors.
|
||||||
|
# El gradient dels emissors viu a
|
||||||
|
# data/config/lighting.yaml.
|
||||||
|
# emitters.<tipus> → override per tipus d'entitat. Tres formes:
|
||||||
|
# false → apaga el tipus
|
||||||
|
# true → defaults del tipus
|
||||||
|
# { radius: N, flicker: B } → camps presents
|
||||||
|
# sobreescriuen el
|
||||||
|
# default; els
|
||||||
|
# absents cauen al
|
||||||
|
# default del tipus.
|
||||||
|
# Plural per llegir-se natural ("totes les momies de la sala"). Per a
|
||||||
|
# casos particulars, un spawn pot dur el seu propi `light:` (mateixes
|
||||||
|
# formes) que té prioritat sobre l'override de sala. La forma antiga
|
||||||
|
# (`light_level:` plà a l'arrel) també segueix sent vàlida per
|
||||||
|
# retrocompatibilitat.
|
||||||
|
lighting:
|
||||||
|
level: 0.15
|
||||||
|
emitters:
|
||||||
|
players: true
|
||||||
|
mummies: false
|
||||||
|
|
||||||
# Índex de tile amb què cada jugador deixa rastre en trepitjar un sòl
|
# Índex de tile amb què cada jugador deixa rastre en trepitjar un sòl
|
||||||
# pintable. Un índex per jugador perquè a multijugador local el recorregut
|
# pintable. Un índex per jugador perquè a multijugador local el recorregut
|
||||||
# es distingeix a simple vista. Provisional fins que l'art del pintat
|
# es distingeix a simple vista. Provisional fins que l'art del pintat
|
||||||
@@ -82,16 +109,12 @@ data:
|
|||||||
# d'obertura (262/263). La momia al corredor central. L'antorxa al costat
|
# d'obertura (262/263). La momia al corredor central. L'antorxa al costat
|
||||||
# superior esquerre per verificar render d'una entitat decorativa.
|
# superior esquerre per verificar render d'una entitat decorativa.
|
||||||
entities:
|
entities:
|
||||||
- type: torch
|
|
||||||
position: [4, 2]
|
|
||||||
- type: torch
|
- type: torch
|
||||||
position: [8, 2]
|
position: [8, 2]
|
||||||
- type: torch
|
- type: torch
|
||||||
position: [12, 2]
|
position: [12, 2]
|
||||||
- type: torch
|
- type: torch
|
||||||
position: [16, 2]
|
position: [16, 2]
|
||||||
- type: sarcophagus
|
|
||||||
sarc_position: [0, 0]
|
|
||||||
- type: sarcophagus
|
- type: sarcophagus
|
||||||
sarc_position: [1, 0]
|
sarc_position: [1, 0]
|
||||||
- type: sarcophagus
|
- type: sarcophagus
|
||||||
@@ -118,8 +141,6 @@ entities:
|
|||||||
sarc_position: [0, 3]
|
sarc_position: [0, 3]
|
||||||
- type: sarcophagus
|
- type: sarcophagus
|
||||||
sarc_position: [1, 3]
|
sarc_position: [1, 3]
|
||||||
- type: sarcophagus
|
|
||||||
sarc_position: [2, 3]
|
|
||||||
- type: sarcophagus
|
- type: sarcophagus
|
||||||
sarc_position: [3, 3]
|
sarc_position: [3, 3]
|
||||||
- type: sarcophagus
|
- type: sarcophagus
|
||||||
@@ -130,8 +151,33 @@ entities:
|
|||||||
sarc_position: [2, 4]
|
sarc_position: [2, 4]
|
||||||
- type: sarcophagus
|
- type: sarcophagus
|
||||||
sarc_position: [3, 4]
|
sarc_position: [3, 4]
|
||||||
|
- type: torch
|
||||||
|
position: [4, 2]
|
||||||
- type: door
|
- type: door
|
||||||
|
- type: column
|
||||||
|
position: [3, 4]
|
||||||
|
frame: 2
|
||||||
|
- type: stone
|
||||||
|
position: [5, 4]
|
||||||
|
frame: 16
|
||||||
|
- type: stone
|
||||||
|
position: [4, 4]
|
||||||
|
frame: 19
|
||||||
|
- type: column
|
||||||
|
position: [13, 10]
|
||||||
|
frame: 4
|
||||||
|
- type: stone
|
||||||
|
position: [12, 10]
|
||||||
|
frame: 13
|
||||||
|
- type: stone
|
||||||
|
position: [11, 10]
|
||||||
|
frame: 23
|
||||||
|
- type: hole
|
||||||
|
position: [5, 9]
|
||||||
|
- type: hole
|
||||||
|
position: [9, 11]
|
||||||
|
- type: stone
|
||||||
|
position: [10, 10]
|
||||||
|
frame: 18
|
||||||
- type: mummy
|
- type: mummy
|
||||||
position: [10, 7]
|
position: [10, 13]
|
||||||
- type: mummy
|
|
||||||
position: [7, 9]
|
|
||||||
@@ -26,9 +26,9 @@ namespace Lighting {
|
|||||||
return buf_;
|
return buf_;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Configuració global, inicialitzada amb els defaults compilats.
|
// Corba activa del mode subtractive. loadConfig() la reemplaça
|
||||||
// loadConfig() la reemplaça si troba el fitxer.
|
// si troba el bloc `gradient:` al YAML.
|
||||||
auto activeConfig() -> GradientConfig& {
|
auto activeSubtractiveConfig() -> GradientConfig& {
|
||||||
static GradientConfig cfg_{
|
static GradientConfig cfg_{
|
||||||
.inner_ratio = Defaults::Lighting::INNER_RATIO,
|
.inner_ratio = Defaults::Lighting::INNER_RATIO,
|
||||||
.mid_ratio = Defaults::Lighting::MID_RATIO,
|
.mid_ratio = Defaults::Lighting::MID_RATIO,
|
||||||
@@ -39,11 +39,27 @@ namespace Lighting {
|
|||||||
return cfg_;
|
return cfg_;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Corba activa del mode additive. Defaults baixos a propòsit:
|
||||||
|
// en additive la intensitat és l'alfa del color afegit, així
|
||||||
|
// que valors petits (0.05..0.15) donen el "halo subtil" que
|
||||||
|
// sol voler-se. loadConfig() la reemplaça si troba el bloc
|
||||||
|
// `gradient_additive:` al YAML.
|
||||||
|
auto activeAdditiveConfig() -> GradientConfig& {
|
||||||
|
static GradientConfig cfg_{
|
||||||
|
.inner_ratio = Defaults::Lighting::INNER_RATIO_ADDITIVE,
|
||||||
|
.mid_ratio = Defaults::Lighting::MID_RATIO_ADDITIVE,
|
||||||
|
.inner_intensity = Defaults::Lighting::INNER_INTENSITY_ADDITIVE,
|
||||||
|
.mid_intensity = Defaults::Lighting::MID_INTENSITY_ADDITIVE,
|
||||||
|
.outer_intensity = Defaults::Lighting::OUTER_INTENSITY_ADDITIVE,
|
||||||
|
};
|
||||||
|
return cfg_;
|
||||||
|
}
|
||||||
|
|
||||||
auto activeFlicker() -> FlickerConfig& {
|
auto activeFlicker() -> FlickerConfig& {
|
||||||
static FlickerConfig fc_{
|
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,
|
.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,
|
.noise_frequency_hz = Defaults::Lighting::FLICKER_NOISE_FREQUENCY_HZ,
|
||||||
};
|
};
|
||||||
return fc_;
|
return fc_;
|
||||||
@@ -54,13 +70,16 @@ namespace Lighting {
|
|||||||
// identificador estable de l'emissor (no canvia amb la posició) —
|
// identificador estable de l'emissor (no canvia amb la posició) —
|
||||||
// així el patró no "deriva" quan l'emissor es mou. El factor
|
// així el patró no "deriva" quan l'emissor es mou. El factor
|
||||||
// ~φ (golden ratio) dispersa les fases per a seeds consecutives.
|
// ~φ (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 {
|
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 TWO_PI = 2.0F * std::numbers::pi_v<float>;
|
||||||
constexpr float GOLDEN_FRACTION = 0.6180339F;
|
constexpr float GOLDEN_FRACTION = 0.6180339F;
|
||||||
const float PHASE = static_cast<float>(seed) * GOLDEN_FRACTION;
|
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 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 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));
|
const int R = base_radius + static_cast<int>(std::lround(DELTA));
|
||||||
return std::max(1, R);
|
return std::max(1, R);
|
||||||
}
|
}
|
||||||
@@ -133,9 +152,9 @@ namespace Lighting {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pasada final: pixel = pixel * lum / 255 per canal. El canal alpha
|
// Pasada final del mode subtractive: pixel = pixel * lum / 255
|
||||||
// es preserva (la resta del pipeline assumeix opacitat total al
|
// per canal. El canal alpha es preserva (la resta del pipeline
|
||||||
// canvas; veure Rendering::OPAQUE_ALPHA).
|
// assumeix opacitat total al canvas; veure Rendering::OPAQUE_ALPHA).
|
||||||
void modulateCanvas(std::uint32_t* canvas, const std::uint8_t* lum, int width, int height) {
|
void modulateCanvas(std::uint32_t* canvas, const std::uint8_t* lum, int width, int height) {
|
||||||
for (int y = 0; y < height; ++y) {
|
for (int y = 0; y < height; ++y) {
|
||||||
std::uint32_t* px = canvas + (static_cast<std::ptrdiff_t>(y) * width);
|
std::uint32_t* px = canvas + (static_cast<std::ptrdiff_t>(y) * width);
|
||||||
@@ -158,12 +177,100 @@ namespace Lighting {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Atenuació ambient del mode additive: aplica `pixel × level` per
|
||||||
|
// canvas abans del bucle d'emissors. Si `level == 255` (sala a
|
||||||
|
// 1.0) salta com a no-op — el cas comú quan només volem el tint
|
||||||
|
// dels emissors sense oscurir la sala.
|
||||||
|
void darkenCanvas(std::uint32_t* canvas, int width, int height, std::uint8_t level) {
|
||||||
|
if (level == 255) { return; }
|
||||||
|
for (int y = 0; y < height; ++y) {
|
||||||
|
std::uint32_t* px = canvas + (static_cast<std::ptrdiff_t>(y) * width);
|
||||||
|
for (int x = 0; x < width; ++x) {
|
||||||
|
const std::uint32_t SRC = px[x];
|
||||||
|
const std::uint8_t SR = (SRC >> 16) & 0xFF;
|
||||||
|
const std::uint8_t SG = (SRC >> 8) & 0xFF;
|
||||||
|
const std::uint8_t SB = SRC & 0xFF;
|
||||||
|
const auto R = static_cast<std::uint8_t>((SR * level) / 255);
|
||||||
|
const auto G = static_cast<std::uint8_t>((SG * level) / 255);
|
||||||
|
const auto B = static_cast<std::uint8_t>((SB * level) / 255);
|
||||||
|
px[x] = (SRC & 0xFF000000U) |
|
||||||
|
(static_cast<std::uint32_t>(R) << 16) |
|
||||||
|
(static_cast<std::uint32_t>(G) << 8) |
|
||||||
|
static_cast<std::uint32_t>(B);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pasada del mode additive per a un emissor: suma `color × intensity`
|
||||||
|
// a cada píxel cobert pel cercle, saturant a 255 per canal. A
|
||||||
|
// diferència del subtractive, NO usa buffer intermig (no cal max())
|
||||||
|
// perquè la suma és commutativa; emissors solapats acumulen
|
||||||
|
// contribucions de manera natural. Si dues llums de colors diferents
|
||||||
|
// es solapen, el resultat tendeix al blanc en la zona de coincidència
|
||||||
|
// (suma física): el solape s'aclareix més que un sol emissor.
|
||||||
|
void rasterizeEmitterAdditive(const Emitter& e, const EmitterRings& rings, std::uint32_t* canvas, int width, int height) {
|
||||||
|
const int R = e.radius;
|
||||||
|
const int CX = static_cast<int>(e.cx);
|
||||||
|
const int CY = static_cast<int>(e.cy);
|
||||||
|
const int X0 = std::max(0, CX - R);
|
||||||
|
const int Y0 = std::max(0, CY - R);
|
||||||
|
const int X1 = std::min(width - 1, CX + R);
|
||||||
|
const int Y1 = std::min(height - 1, CY + R);
|
||||||
|
for (int y = Y0; y <= Y1; ++y) {
|
||||||
|
const int DY = y - CY;
|
||||||
|
const int DY_SQ = DY * DY;
|
||||||
|
if (DY_SQ > rings.radius_sq) { continue; }
|
||||||
|
std::uint32_t* row = canvas + (static_cast<std::ptrdiff_t>(y) * width);
|
||||||
|
for (int x = X0; x <= X1; ++x) {
|
||||||
|
const int DX = x - CX;
|
||||||
|
const int DIST_SQ = (DX * DX) + DY_SQ;
|
||||||
|
if (DIST_SQ > rings.radius_sq) { continue; }
|
||||||
|
std::uint8_t intensity = rings.outer;
|
||||||
|
if (DIST_SQ <= rings.inner_sq) {
|
||||||
|
intensity = rings.inner;
|
||||||
|
} else if (DIST_SQ <= rings.mid_sq) {
|
||||||
|
intensity = rings.mid;
|
||||||
|
}
|
||||||
|
// contrib_channel = emitter_color_channel × intensity / 255
|
||||||
|
// (intensity ja és 0..255 quantitzat a `toByte`).
|
||||||
|
const int CR = (e.color.r * intensity) / 255;
|
||||||
|
const int CG = (e.color.g * intensity) / 255;
|
||||||
|
const int CB = (e.color.b * intensity) / 255;
|
||||||
|
const std::uint32_t SRC = row[x];
|
||||||
|
const int SR = static_cast<int>((SRC >> 16) & 0xFF);
|
||||||
|
const int SG = static_cast<int>((SRC >> 8) & 0xFF);
|
||||||
|
const int SB = static_cast<int>(SRC & 0xFF);
|
||||||
|
const auto NR = static_cast<std::uint32_t>(std::min(255, SR + CR));
|
||||||
|
const auto NG = static_cast<std::uint32_t>(std::min(255, SG + CG));
|
||||||
|
const auto NB = static_cast<std::uint32_t>(std::min(255, SB + CB));
|
||||||
|
row[x] = (SRC & 0xFF000000U) | (NR << 16) | (NG << 8) | NB;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
auto config() -> const GradientConfig& {
|
auto config() -> const GradientConfig& {
|
||||||
return activeConfig();
|
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
|
||||||
|
// entre els blocs `gradient:` i `gradient_additive:`.
|
||||||
|
void readGradient(const fkyaml::node& g, GradientConfig& cfg) {
|
||||||
|
readField(g, "inner_ratio", cfg.inner_ratio);
|
||||||
|
readField(g, "mid_ratio", cfg.mid_ratio);
|
||||||
|
readField(g, "inner_intensity", cfg.inner_intensity);
|
||||||
|
readField(g, "mid_intensity", cfg.mid_intensity);
|
||||||
|
readField(g, "outer_intensity", cfg.outer_intensity);
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
void loadConfig(const std::string& yaml_path) {
|
void loadConfig(const std::string& yaml_path) {
|
||||||
auto bytes = Resource::Helper::loadFile(yaml_path);
|
auto bytes = Resource::Helper::loadFile(yaml_path);
|
||||||
if (bytes.empty()) {
|
if (bytes.empty()) {
|
||||||
@@ -175,20 +282,17 @@ namespace Lighting {
|
|||||||
const auto* end = begin + bytes.size();
|
const auto* end = begin + bytes.size();
|
||||||
auto yaml = fkyaml::node::deserialize(begin, end);
|
auto yaml = fkyaml::node::deserialize(begin, end);
|
||||||
if (yaml.contains("gradient")) {
|
if (yaml.contains("gradient")) {
|
||||||
const auto& g = yaml["gradient"];
|
readGradient(yaml["gradient"], activeSubtractiveConfig());
|
||||||
auto& cfg = activeConfig();
|
}
|
||||||
readField(g, "inner_ratio", cfg.inner_ratio);
|
if (yaml.contains("gradient_additive")) {
|
||||||
readField(g, "mid_ratio", cfg.mid_ratio);
|
readGradient(yaml["gradient_additive"], activeAdditiveConfig());
|
||||||
readField(g, "inner_intensity", cfg.inner_intensity);
|
|
||||||
readField(g, "mid_intensity", cfg.mid_intensity);
|
|
||||||
readField(g, "outer_intensity", cfg.outer_intensity);
|
|
||||||
}
|
}
|
||||||
if (yaml.contains("flicker")) {
|
if (yaml.contains("flicker")) {
|
||||||
const auto& f = yaml["flicker"];
|
const auto& f = yaml["flicker"];
|
||||||
auto& fc = activeFlicker();
|
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, "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);
|
readField(f, "noise_frequency_hz", fc.noise_frequency_hz);
|
||||||
}
|
}
|
||||||
} catch (const std::exception& ex) {
|
} catch (const std::exception& ex) {
|
||||||
@@ -196,39 +300,77 @@ namespace Lighting {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void apply(std::uint32_t* canvas, int width, int height, float room_light, std::span<const Emitter> emitters) {
|
namespace {
|
||||||
|
|
||||||
|
// 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, 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,
|
||||||
|
.flicker_params = e.flicker_params,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Camí del mode subtractive (històric): omple un buffer de
|
||||||
|
// luminància amb `max(room, emitter_intensity)` i finalment
|
||||||
|
// multiplica el canvas per ell. Color de l'emissor ignorat.
|
||||||
|
void applySubtractive(std::uint32_t* canvas, int width, int height, std::uint8_t room, std::span<const Emitter> emitters) {
|
||||||
|
auto& lum = luminanceBuffer();
|
||||||
|
const std::size_t TOTAL = static_cast<std::size_t>(width) * static_cast<std::size_t>(height);
|
||||||
|
if (lum.size() != TOTAL) { lum.resize(TOTAL); }
|
||||||
|
std::ranges::fill(lum, room);
|
||||||
|
|
||||||
|
const auto& cfg = activeSubtractiveConfig();
|
||||||
|
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, 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);
|
||||||
|
}
|
||||||
|
modulateCanvas(canvas, lum.data(), width, height);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Camí del mode additive: atenua el canvas per `room` (si <255)
|
||||||
|
// i després suma `color × intensity` per emissor directament al
|
||||||
|
// canvas, sense buffer intermig.
|
||||||
|
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 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, 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
void apply(std::uint32_t* canvas, int width, int height, Mode mode, float room_light, std::span<const Emitter> emitters) {
|
||||||
if (canvas == nullptr || width <= 0 || height <= 0) { return; }
|
if (canvas == nullptr || width <= 0 || height <= 0) { return; }
|
||||||
|
|
||||||
// Atajo: si la sala està totalment il·luminada i no hi ha
|
// Atajo per a tots dos modes: sala totalment "neutra" + cap
|
||||||
// emissors, la pasada seria un `pixel * 1.0` global — no cal
|
// emissor → la pasada no canviaria res al canvas.
|
||||||
// tocar res. És el camí curt habitual mentre no posem
|
// subtractive: room=1.0 i sense emissors → pixel * 1.0 global.
|
||||||
// `light_level` a les sales.
|
// additive: room=1.0 i sense emissors → cap aportació.
|
||||||
const std::uint8_t ROOM = toByte(room_light);
|
const std::uint8_t ROOM = toByte(room_light);
|
||||||
if (ROOM == 255 && emitters.empty()) { return; }
|
if (ROOM == 255 && emitters.empty()) { return; }
|
||||||
|
|
||||||
auto& lum = luminanceBuffer();
|
if (mode == Mode::ADDITIVE) {
|
||||||
const std::size_t TOTAL = static_cast<std::size_t>(width) * static_cast<std::size_t>(height);
|
applyAdditive(canvas, width, height, ROOM, emitters);
|
||||||
if (lum.size() != TOTAL) { lum.resize(TOTAL); }
|
} else {
|
||||||
std::ranges::fill(lum, ROOM);
|
applySubtractive(canvas, width, height, ROOM, emitters);
|
||||||
|
|
||||||
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 int R_EFF = e.flicker
|
|
||||||
? computeFlickerRadius(e.radius, e.seed, 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, .seed = e.seed, .flicker = e.flicker};
|
|
||||||
rasterizeEmitter(EFF, RINGS, lum.data(), width, height);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
modulateCanvas(canvas, lum.data(), width, height);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace Lighting
|
} // namespace Lighting
|
||||||
@@ -7,36 +7,64 @@
|
|||||||
// Sistema d'il·luminació CPU. Treballa sobre el mateix canvas ARGB del
|
// Sistema d'il·luminació CPU. Treballa sobre el mateix canvas ARGB del
|
||||||
// joc (320×240) que tot el pipeline de render: després que GameScene
|
// joc (320×240) que tot el pipeline de render: després que GameScene
|
||||||
// haja pintat tilemap, paint layer, sombres i actors, una pasada final
|
// haja pintat tilemap, paint layer, sombres i actors, una pasada final
|
||||||
// multiplica cada píxel del canvas per un factor de luminància 0..1
|
// modifica cada píxel del canvas en funció de la "llum de la sala" i
|
||||||
// derivat de la combinació de "llum de la sala" + cercles emissors al
|
// dels cercles emissors al voltant de certes entitats.
|
||||||
// voltant de certes entitats.
|
|
||||||
//
|
//
|
||||||
// Forma de cada emissor (radial, 3 anells configurables al fitxer
|
// Dos modes seleccionables per la sala (`lighting.mode` al YAML):
|
||||||
// data/config/lighting.yaml; veure `GradientConfig`):
|
//
|
||||||
|
// SUBTRACTIVE (mode històric, default si no es declara mode):
|
||||||
|
// out = src × max(room_light, max_emitter_intensity_at_pixel)
|
||||||
|
// Els emissors "trenquen" una capa fosca multiplicativa. Tot el
|
||||||
|
// trip cromàtic dels emissors s'ignora — només la intensitat
|
||||||
|
// compta. Pensat per a sales fosques (cripta, pirámide).
|
||||||
|
//
|
||||||
|
// ADDITIVE (mode nou):
|
||||||
|
// out = saturate( (src × room_light) + Σ (emitter_color × intensity) )
|
||||||
|
// El canvas s'enfosqueix opcionalment per `room_light` (atenuació
|
||||||
|
// ambient) i els emissors hi sumen color cap a sobre. Les intensitats
|
||||||
|
// dels anells es llegeixen com a "alfa" del color afegit (0..1).
|
||||||
|
// Pensat per a sales clares on les antorxes aporten halo cromàtic
|
||||||
|
// sense restar contrast — o per a sales lleument fosques (0.85, p.ex.)
|
||||||
|
// on el halo de l'antorxa es veu càlid en lloc de "blanquinós".
|
||||||
|
//
|
||||||
|
// Forma del cercle de cada emissor (radial, 3 anells configurables al
|
||||||
|
// fitxer data/config/lighting.yaml; veure `GradientConfig`):
|
||||||
// 0..inner_ratio del radi → inner_intensity
|
// 0..inner_ratio del radi → inner_intensity
|
||||||
// inner_ratio..mid_ratio → mid_intensity
|
// inner_ratio..mid_ratio → mid_intensity
|
||||||
// mid_ratio..1.0 → outer_intensity
|
// mid_ratio..1.0 → outer_intensity
|
||||||
// fora del radi → light_level de la sala
|
|
||||||
//
|
//
|
||||||
// Combinació entre emissors: per a cada píxel es pren el màxim del
|
// La corba viu en dos blocs separats al YAML perquè la mateixa escala
|
||||||
// llum de sala i de la intensitat aportada per cada emissor que el
|
// 0..1 té significats molt diferents per mode:
|
||||||
// solapa — no se "sumen" intensitats. Coherent amb la idea de "centre
|
// `gradient:` → corba del mode subtractive (1.0 = "no fosqueix")
|
||||||
// 1.0": el píxel que cau dins de qualsevol nucli no s'enfosqueix.
|
// `gradient_additive:` → corba del mode additive (1.0 = "afegeix
|
||||||
|
// el color sencer i satura"). Es vol molt més
|
||||||
|
// baixa per a no inundar la sala de color.
|
||||||
|
// Cada mode llegeix la seva pròpia corba — no es comparteixen valors.
|
||||||
|
//
|
||||||
|
// Combinació entre emissors:
|
||||||
|
// SUBTRACTIVE → per a cada píxel es pren el màxim de la intensitat
|
||||||
|
// aportada per cada emissor que el solapa (no se sumen intensitats).
|
||||||
|
// ADDITIVE → per a cada píxel se sumen totes les contribucions
|
||||||
|
// (`color × intensity`), saturant a 255 per canal. Dues llums de
|
||||||
|
// colors diferents que solapen produeixen la suma natural (orange +
|
||||||
|
// blue → near-white) — comportament físic.
|
||||||
namespace Lighting {
|
namespace Lighting {
|
||||||
|
|
||||||
// Posició al canvas (en píxels, mateixa unitat que la resta del
|
// Mode de composició. La sala el selecciona via `lighting.mode:
|
||||||
// motor) i radi efectiu de l'emissor. El centre és el del sprite
|
// additive | subtractive` (default subtractive si manca, retrocompat).
|
||||||
// visible: és l'entitat qui sap on cau (veure `Entity::getLightCenter`).
|
enum class Mode : std::uint8_t {
|
||||||
// `flicker` activa la modulació del radi per frame; `seed` és un
|
SUBTRACTIVE,
|
||||||
// identificador estable per emissor (no canvia amb la posició) del
|
ADDITIVE,
|
||||||
// qual es deriva la fase del flicker — així els emissors mòbils
|
};
|
||||||
// mantenen un patró propi que no "deriva" pel moviment.
|
|
||||||
struct Emitter {
|
// Color emès. Default blanc (sense modificació quan s'usa en mode
|
||||||
float cx{0.0F};
|
// additive). En mode subtractive s'ignora — la pasada multiplicativa
|
||||||
float cy{0.0F};
|
// només té un canal (intensitat). El YAML accepta `color: [r, g, b]`
|
||||||
int radius{0};
|
// a qualsevol nivell de la cadena de resolució (tipus, sala, spawn).
|
||||||
int seed{0};
|
struct Color {
|
||||||
bool flicker{false};
|
std::uint8_t r{255};
|
||||||
|
std::uint8_t g{255};
|
||||||
|
std::uint8_t b{255};
|
||||||
};
|
};
|
||||||
|
|
||||||
// Forma del gradient. Compartida per tots els emissors per evitar
|
// Forma del gradient. Compartida per tots els emissors per evitar
|
||||||
@@ -53,30 +81,70 @@ namespace Lighting {
|
|||||||
// forma final del radi és la suma d'una sinusoide "respiració"
|
// forma final del radi és la suma d'una sinusoide "respiració"
|
||||||
// (wave_*) i una sinusoide secundària de freqüència més alta
|
// (wave_*) i una sinusoide secundària de freqüència més alta
|
||||||
// (noise_*) que aporta la sensació d'inquietud. Les fases es
|
// (noise_*) que aporta la sensació d'inquietud. Les fases es
|
||||||
// deriven de la posició de cada emissor — sense estat per emissor.
|
// 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 {
|
struct FlickerConfig {
|
||||||
float wave_amplitude_px{2.0F};
|
float wave_amplitude_ratio{0.05F};
|
||||||
float wave_frequency_hz{3.0F};
|
float wave_frequency_hz{3.0F};
|
||||||
float noise_amplitude_px{1.0F};
|
float noise_amplitude_ratio{0.025F};
|
||||||
float noise_frequency_hz{7.0F};
|
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`).
|
||||||
|
// `flicker` activa la modulació del radi per frame; `seed` és un
|
||||||
|
// identificador estable per emissor (no canvia amb la posició) del
|
||||||
|
// qual es deriva la fase del flicker — així els emissors mòbils
|
||||||
|
// mantenen un patró propi que no "deriva" pel moviment. `color`
|
||||||
|
// només té efecte en mode additive.
|
||||||
|
struct Emitter {
|
||||||
|
float cx{0.0F};
|
||||||
|
float cy{0.0F};
|
||||||
|
int radius{0};
|
||||||
|
int seed{0};
|
||||||
|
bool flicker{false};
|
||||||
|
Color color{};
|
||||||
|
// 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
|
// 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
|
||||||
// simplement reemplaça la configuració activa.
|
// simplement reemplaça la configuració activa.
|
||||||
void loadConfig(const std::string& yaml_path);
|
void loadConfig(const std::string& yaml_path);
|
||||||
|
|
||||||
// Configuració activa (per a debug/inspectors). Inicialitzada amb els
|
// Configuració activa (per a debug/inspectors). Retorna sempre la
|
||||||
|
// corba subtractive — la corba additive viu en paral·lel al cpp i,
|
||||||
|
// si algun dia es necessita inspeccionar des de fora, caldrà afegir
|
||||||
|
// una variant `configAdditive()`. Inicialitzada amb els
|
||||||
// Defaults::Lighting fins que loadConfig() es crida.
|
// Defaults::Lighting fins que loadConfig() es crida.
|
||||||
[[nodiscard]] auto config() -> const GradientConfig&;
|
[[nodiscard]] auto config() -> const GradientConfig&;
|
||||||
|
|
||||||
// Aplica la pasada d'il·luminació al canvas in-place. `room_light`
|
// Defaults globals del flicker (data/config/lighting.yaml, bloc
|
||||||
// és l'atenuació base de la sala (0..1, 1 = sense atenuació). El
|
// `flicker:`). Els callers del construct path d'entitats el llegeixen
|
||||||
// span d'emissors pot ser buit — en aquest cas tota la sala queda
|
// per inicialitzar la `FlickerConfig` de cada emissor, i aplicar a
|
||||||
// multiplicada per `room_light`. Aquesta funció no fa cap allocació
|
// sobre els overrides per tipus/sala/spawn.
|
||||||
// si es crida amb la mateixa mida de canvas múltiples cops: manté
|
[[nodiscard]] auto flickerConfig() -> const FlickerConfig&;
|
||||||
// un buffer intern reutilitzable.
|
|
||||||
void apply(std::uint32_t* canvas, int width, int height, float room_light, std::span<const Emitter> emitters);
|
// 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
|
||||||
|
// atenuació): en subtractive és el "terra" de la intensitat fora
|
||||||
|
// dels emissors; en additive és la multiplicació prèvia del canvas
|
||||||
|
// abans de sumar els emissors (es pot deixar a 1.0 per "només tint",
|
||||||
|
// o baixar per combinar oscuriment ambient + halo cromàtic). El
|
||||||
|
// span d'emissors pot ser buit. La funció no fa cap allocació si
|
||||||
|
// es crida amb la mateixa mida de canvas múltiples cops: manté un
|
||||||
|
// buffer intern reutilitzable (només l'usa el mode subtractive).
|
||||||
|
void apply(std::uint32_t* canvas, int width, int height, Mode mode, float room_light, std::span<const Emitter> emitters);
|
||||||
|
|
||||||
} // namespace Lighting
|
} // namespace Lighting
|
||||||
@@ -64,12 +64,13 @@ auto Entity::getCollisionRect() const -> SDL_FRect {
|
|||||||
}
|
}
|
||||||
|
|
||||||
auto Entity::getLightCenter() const -> SDL_FPoint {
|
auto Entity::getLightCenter() const -> SDL_FPoint {
|
||||||
// Centre del rect de render. Per a entitats que es desplacen
|
// Centre del rect de render + offset configurat al YAML. Per a
|
||||||
// visualment sobre la seva posició lògica (p.ex. el Player saltant),
|
// entitats que es desplacen visualment sobre la seva posició lògica
|
||||||
// la subclasse override aquest mètode per restar el desplaçament.
|
// (p.ex. el Player saltant), la subclasse override aquest mètode
|
||||||
|
// per restar el desplaçament — i ha d'incloure l'offset igualment.
|
||||||
return {
|
return {
|
||||||
.x = pixel_rect_.x + (pixel_rect_.w * 0.5F),
|
.x = pixel_rect_.x + (pixel_rect_.w * 0.5F) + static_cast<float>(light_offset_x_),
|
||||||
.y = pixel_rect_.y + (pixel_rect_.h * 0.5F),
|
.y = pixel_rect_.y + (pixel_rect_.h * 0.5F) + static_cast<float>(light_offset_y_),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,10 +2,12 @@
|
|||||||
|
|
||||||
#include <SDL3/SDL.h>
|
#include <SDL3/SDL.h>
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <optional>
|
#include <optional>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
|
#include "core/rendering/lighting.hpp"
|
||||||
#include "game/entities/animation_component.hpp"
|
#include "game/entities/animation_component.hpp"
|
||||||
|
|
||||||
// Forward-declaracions: mantenim aquest header lleuger. `Player` viu a
|
// Forward-declaracions: mantenim aquest header lleuger. `Player` viu a
|
||||||
@@ -266,6 +268,44 @@ class Entity {
|
|||||||
[[nodiscard]] auto getLightFlicker() const -> bool { return light_flicker_; }
|
[[nodiscard]] auto getLightFlicker() const -> bool { return light_flicker_; }
|
||||||
void setLightFlicker(bool f) { light_flicker_ = f; }
|
void setLightFlicker(bool f) { light_flicker_ = f; }
|
||||||
|
|
||||||
|
// Color emès en mode additive (RGB 0..255 per canal). Default blanc:
|
||||||
|
// en mode subtractive aquest valor s'ignora completament; en mode
|
||||||
|
// additive és el tint que l'emissor afegeix al canvas. La cadena de
|
||||||
|
// resolució (tipus → sala → spawn) el pobla via `setLightColor`.
|
||||||
|
[[nodiscard]] auto getLightColorR() const -> std::uint8_t { return light_color_r_; }
|
||||||
|
[[nodiscard]] auto getLightColorG() const -> std::uint8_t { return light_color_g_; }
|
||||||
|
[[nodiscard]] auto getLightColorB() const -> std::uint8_t { return light_color_b_; }
|
||||||
|
void setLightColor(std::uint8_t r, std::uint8_t g, std::uint8_t b) {
|
||||||
|
light_color_r_ = r;
|
||||||
|
light_color_g_ = g;
|
||||||
|
light_color_b_ = b;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Desplaçament en píxels del centre del cercle de llum respecte el
|
||||||
|
// centre visual del sprite (que és el que retornaria per defecte
|
||||||
|
// `getLightCenter`). Útil quan el "punt de llum" iconogràfic no
|
||||||
|
// coincideix amb el centre del bounding box — per exemple, la flama
|
||||||
|
// d'una antorxa està a la part de dalt del sprite i interessa pujar
|
||||||
|
// l'emissor 3-4 px per a que el cercle de llum surti de la flama,
|
||||||
|
// no del peu. Y creix cap avall al canvas, així que `y: -3` puja
|
||||||
|
// la llum 3 píxels. Cadena de resolució idèntica al color: tipus →
|
||||||
|
// sala → spawn.
|
||||||
|
[[nodiscard]] auto getLightOffsetX() const -> int { return light_offset_x_; }
|
||||||
|
[[nodiscard]] auto getLightOffsetY() const -> int { return light_offset_y_; }
|
||||||
|
void setLightOffset(int x, int y) {
|
||||||
|
light_offset_x_ = x;
|
||||||
|
light_offset_y_ = y;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Paràmetres del flicker resolts per a aquest emissor (combinació
|
||||||
|
// del global de `data/config/lighting.yaml` + overrides del tipus,
|
||||||
|
// sala i spawn). Només té efecte si `light_flicker_` és true. El
|
||||||
|
// construct path li passa una còpia ja resolta — el raster (lighting.cpp)
|
||||||
|
// només la llegeix. Es publica per camps perquè el bloc YAML
|
||||||
|
// `flicker_params:` permet sobreescriure qualsevol subconjunt.
|
||||||
|
[[nodiscard]] auto getFlickerParams() const -> const Lighting::FlickerConfig& { return flicker_params_; }
|
||||||
|
void setFlickerParams(const Lighting::FlickerConfig& fp) { flicker_params_ = fp; }
|
||||||
|
|
||||||
// Identificador estable per al flicker. Cada Entity rep un valor
|
// Identificador estable per al flicker. Cada Entity rep un valor
|
||||||
// únic a la construcció (contador estàtic) i el conserva fins a la
|
// únic a la construcció (contador estàtic) i el conserva fins a la
|
||||||
// mort — així el patró del flicker no "deriva" quan l'entitat es
|
// mort — així el patró del flicker no "deriva" quan l'entitat es
|
||||||
@@ -274,11 +314,13 @@ class Entity {
|
|||||||
// un problema.
|
// un problema.
|
||||||
[[nodiscard]] auto getLightSeed() const -> int { return light_seed_; }
|
[[nodiscard]] auto getLightSeed() const -> int { return light_seed_; }
|
||||||
|
|
||||||
// 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
|
||||||
// `pixel_rect_` (que és la caixa de render: sprite top-left + mida).
|
// del `pixel_rect_` (caixa de render) més l'offset configurat al
|
||||||
// Virtual perquè entitats amb desplaçament visual sobre la posició
|
// YAML (`light.offset: [x, y]`). Virtual perquè entitats amb
|
||||||
// lògica (p.ex. el Player en l'aire, que rebaixa visualment `pos_.y`
|
// desplaçament visual sobre la posició lògica (p.ex. el Player en
|
||||||
// amb `jump_z_`) puguin retornar el centre que realment es veu.
|
// l'aire, que rebaixa visualment `pos_.y` amb `jump_z_`) puguin
|
||||||
|
// retornar el centre que realment es veu — l'override n'és
|
||||||
|
// responsable d'incloure l'offset (com fa `Player::getLightCenter`).
|
||||||
[[nodiscard]] virtual auto getLightCenter() const -> SDL_FPoint;
|
[[nodiscard]] virtual auto getLightCenter() const -> SDL_FPoint;
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
@@ -302,6 +344,12 @@ class Entity {
|
|||||||
static inline int s_next_light_seed{0};
|
static inline int s_next_light_seed{0};
|
||||||
int light_seed_{++s_next_light_seed};
|
int light_seed_{++s_next_light_seed};
|
||||||
bool light_flicker_{false};
|
bool light_flicker_{false};
|
||||||
|
std::uint8_t light_color_r_{255};
|
||||||
|
std::uint8_t light_color_g_{255};
|
||||||
|
std::uint8_t light_color_b_{255};
|
||||||
|
int light_offset_x_{0};
|
||||||
|
int light_offset_y_{0};
|
||||||
|
Lighting::FlickerConfig flicker_params_{};
|
||||||
bool dead_{false};
|
bool dead_{false};
|
||||||
SDL_FlipMode flip_{SDL_FLIP_NONE};
|
SDL_FlipMode flip_{SDL_FLIP_NONE};
|
||||||
EventBus* bus_{nullptr};
|
EventBus* bus_{nullptr};
|
||||||
|
|||||||
@@ -4,6 +4,8 @@
|
|||||||
// consumidors que només volen la base del manager (p.ex. el smoke test
|
// consumidors que només volen la base del manager (p.ex. el smoke test
|
||||||
// stand-alone) enllacen `entity_manager.cpp` sense arrossegar aquest fitxer.
|
// stand-alone) enllacen `entity_manager.cpp` sense arrossegar aquest fitxer.
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cstdint>
|
||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <string>
|
#include <string>
|
||||||
@@ -155,22 +157,70 @@ namespace {
|
|||||||
return c["height"].get_value<int>();
|
return c["height"].get_value<int>();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Bloc opcional `light: { radius: N, flicker: bool }` de la plantilla.
|
// Bloc opcional `light: { radius: N, flicker: bool, color: [r,g,b], offset: [x,y] }`
|
||||||
// Si manca, el radi és 0 i l'entitat no entra a la pasada d'il·luminació.
|
// de la plantilla. Si manca, el radi és 0 i l'entitat no entra a la
|
||||||
// `flicker` activa la modulació "tipus foc" del radi (només antorxes
|
// pasada d'il·luminació. `flicker` activa la modulació "tipus foc"
|
||||||
// l'haurien d'usar). La forma del gradient (3 anells) i els paràmetres
|
// del radi (només antorxes l'haurien d'usar). `color` només té
|
||||||
// del flicker viuen a data/config/lighting.yaml — no cal declarar-los
|
// efecte en mode additive — defecte blanc (255,255,255). `offset`
|
||||||
// per entitat.
|
// desplaça el centre del cercle de llum respecte el centre del
|
||||||
|
// sprite (default (0, 0); Y creix cap avall). 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 {
|
struct LightTemplate {
|
||||||
int radius{0};
|
int radius{0};
|
||||||
bool flicker{false};
|
bool flicker{false};
|
||||||
|
std::uint8_t color_r{255};
|
||||||
|
std::uint8_t color_g{255};
|
||||||
|
std::uint8_t color_b{255};
|
||||||
|
int offset_x{0};
|
||||||
|
int offset_y{0};
|
||||||
|
// Inicialitzat al global a `readLight`; overrides parcials del
|
||||||
|
// tipus/sala/spawn modifiquen camps concrets sense tocar la resta.
|
||||||
|
Lighting::FlickerConfig flicker_params{};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Helper compartit entre les tres capes (tipus, sala via Map, spawn)
|
||||||
|
// per llegir `color: [r, g, b]` amb clamp defensiu a 0..255.
|
||||||
|
auto readColorTriplet(const fkyaml::node& seq, std::uint8_t& r, std::uint8_t& g, std::uint8_t& b) -> bool {
|
||||||
|
if (!seq.is_sequence() || seq.size() != 3) { return false; }
|
||||||
|
r = static_cast<std::uint8_t>(std::clamp(seq[0].get_value<int>(), 0, 255));
|
||||||
|
g = static_cast<std::uint8_t>(std::clamp(seq[1].get_value<int>(), 0, 255));
|
||||||
|
b = static_cast<std::uint8_t>(std::clamp(seq[2].get_value<int>(), 0, 255));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper paral·lel a `readColorTriplet` per a `offset: [x, y]` —
|
||||||
|
// píxels enters, sense clamp (offsets negatius són legítims; Y
|
||||||
|
// creix cap avall).
|
||||||
|
auto readOffsetPair(const fkyaml::node& seq, int& x, int& y) -> bool {
|
||||||
|
if (!seq.is_sequence() || seq.size() != 2) { return false; }
|
||||||
|
x = seq[0].get_value<int>();
|
||||||
|
y = seq[1].get_value<int>();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Aplica els camps presents al bloc `flicker_params: { ... }` sobre
|
||||||
|
// una `FlickerConfig` ja inicialitzada (típicament amb el global o
|
||||||
|
// amb el resultat d'una capa anterior). Camps absents es respecten
|
||||||
|
// — per a poder sobreescriure'n només un.
|
||||||
|
void applyFlickerOverrides(const fkyaml::node& fp, Lighting::FlickerConfig& out) {
|
||||||
|
if (fp.contains("wave_amplitude_ratio")) { out.wave_amplitude_ratio = fp["wave_amplitude_ratio"].get_value<float>(); }
|
||||||
|
if (fp.contains("wave_frequency_hz")) { out.wave_frequency_hz = fp["wave_frequency_hz"].get_value<float>(); }
|
||||||
|
if (fp.contains("noise_amplitude_ratio")) { out.noise_amplitude_ratio = fp["noise_amplitude_ratio"].get_value<float>(); }
|
||||||
|
if (fp.contains("noise_frequency_hz")) { out.noise_frequency_hz = fp["noise_frequency_hz"].get_value<float>(); }
|
||||||
|
}
|
||||||
|
|
||||||
auto readLight(const fkyaml::node& tmpl) -> LightTemplate {
|
auto readLight(const fkyaml::node& tmpl) -> LightTemplate {
|
||||||
if (!tmpl.contains("light")) { return {}; }
|
|
||||||
const auto& l = tmpl["light"];
|
|
||||||
LightTemplate out;
|
LightTemplate out;
|
||||||
|
// Punt de partida del flicker: el global de lighting.yaml.
|
||||||
|
out.flicker_params = Lighting::flickerConfig();
|
||||||
|
if (!tmpl.contains("light")) { return out; }
|
||||||
|
const auto& l = tmpl["light"];
|
||||||
if (l.contains("radius")) { out.radius = 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>(); }
|
if (l.contains("flicker")) { out.flicker = l["flicker"].get_value<bool>(); }
|
||||||
|
if (l.contains("color")) { readColorTriplet(l["color"], out.color_r, out.color_g, out.color_b); }
|
||||||
|
if (l.contains("offset")) { readOffsetPair(l["offset"], out.offset_x, out.offset_y); }
|
||||||
|
if (l.contains("flicker_params")) { applyFlickerOverrides(l["flicker_params"], out.flicker_params); }
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -182,26 +232,41 @@ namespace {
|
|||||||
// Forma 3: la sala declara `<tipus>: true` o `<tipus>: { ... }` → els
|
// Forma 3: la sala declara `<tipus>: true` o `<tipus>: { ... }` → els
|
||||||
// camps presents a l'override sobreescriuen el default; els absents
|
// camps presents a l'override sobreescriuen el default; els absents
|
||||||
// (`std::nullopt`) cauen al default del tipus.
|
// (`std::nullopt`) cauen al default del tipus.
|
||||||
auto resolveLightWithRoom(const std::string& type, LightTemplate type_default) -> LightTemplate {
|
auto resolveLightWithRoom(const std::string& type, const LightTemplate& type_default) -> LightTemplate {
|
||||||
const auto* room = Map::getRoomEmitterOverride(type);
|
const auto* room = Map::getRoomEmitterOverride(type);
|
||||||
if (room == nullptr) { return type_default; }
|
if (room == nullptr) { return type_default; }
|
||||||
if (!room->enabled) { return {.radius = 0, .flicker = false}; }
|
if (!room->enabled) {
|
||||||
|
LightTemplate empty;
|
||||||
|
empty.flicker_params = Lighting::flickerConfig();
|
||||||
|
return empty;
|
||||||
|
}
|
||||||
LightTemplate out = type_default;
|
LightTemplate out = type_default;
|
||||||
if (room->radius.has_value()) { out.radius = *room->radius; }
|
if (room->radius.has_value()) { out.radius = *room->radius; }
|
||||||
if (room->flicker.has_value()) { out.flicker = *room->flicker; }
|
if (room->flicker.has_value()) { out.flicker = *room->flicker; }
|
||||||
|
if (room->color.has_value()) {
|
||||||
|
out.color_r = (*room->color)[0];
|
||||||
|
out.color_g = (*room->color)[1];
|
||||||
|
out.color_b = (*room->color)[2];
|
||||||
|
}
|
||||||
|
if (room->offset.has_value()) {
|
||||||
|
out.offset_x = (*room->offset)[0];
|
||||||
|
out.offset_y = (*room->offset)[1];
|
||||||
|
}
|
||||||
|
if (room->flicker_wave_amplitude_ratio.has_value()) { out.flicker_params.wave_amplitude_ratio = *room->flicker_wave_amplitude_ratio; }
|
||||||
|
if (room->flicker_wave_frequency_hz.has_value()) { out.flicker_params.wave_frequency_hz = *room->flicker_wave_frequency_hz; }
|
||||||
|
if (room->flicker_noise_amplitude_ratio.has_value()) { out.flicker_params.noise_amplitude_ratio = *room->flicker_noise_amplitude_ratio; }
|
||||||
|
if (room->flicker_noise_frequency_hz.has_value()) { out.flicker_params.noise_frequency_hz = *room->flicker_noise_frequency_hz; }
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Aplica l'override per-spawn (si existeix) per damunt del que ja
|
// Aplica l'override per-spawn (si existeix) per damunt del que ja
|
||||||
// viu a l'entitat (defaults del tipus + override de sala). Tres formes
|
// viu a l'entitat (defaults del tipus + override de sala). Tres formes
|
||||||
// a l'entry del YAML de nivell:
|
// a l'entry del YAML de nivell:
|
||||||
// `light: false` → apaga aquesta instància
|
// `light: false` → apaga aquesta instància
|
||||||
// `light: true` → no canvia res; la línia és
|
// `light: true` → informatiu (cau a capes inferiors)
|
||||||
// informativa (la llum ja la donen
|
// `light: { radius, flicker, color: [r,g,b], offset: [x,y] }` → camps presents sobreescriuen;
|
||||||
// les capes inferiors)
|
// els absents respecten el que
|
||||||
// `light: { radius, flicker }` → els camps presents sobreescriuen;
|
// ja duia l'entitat
|
||||||
// els absents respecten el que ja
|
|
||||||
// duia l'entitat
|
|
||||||
void applySpawnLightOverride(Entity& entity, const fkyaml::node& spawn) {
|
void applySpawnLightOverride(Entity& entity, const fkyaml::node& spawn) {
|
||||||
if (!spawn.contains("light")) { return; }
|
if (!spawn.contains("light")) { return; }
|
||||||
const auto& l = spawn["light"];
|
const auto& l = spawn["light"];
|
||||||
@@ -215,6 +280,22 @@ namespace {
|
|||||||
if (!l.is_mapping()) { return; }
|
if (!l.is_mapping()) { return; }
|
||||||
if (l.contains("radius")) { entity.setLightRadius(l["radius"].get_value<int>()); }
|
if (l.contains("radius")) { entity.setLightRadius(l["radius"].get_value<int>()); }
|
||||||
if (l.contains("flicker")) { entity.setLightFlicker(l["flicker"].get_value<bool>()); }
|
if (l.contains("flicker")) { entity.setLightFlicker(l["flicker"].get_value<bool>()); }
|
||||||
|
if (l.contains("color")) {
|
||||||
|
std::uint8_t r{};
|
||||||
|
std::uint8_t g{};
|
||||||
|
std::uint8_t b{};
|
||||||
|
if (readColorTriplet(l["color"], r, g, b)) { entity.setLightColor(r, g, b); }
|
||||||
|
}
|
||||||
|
if (l.contains("offset")) {
|
||||||
|
int x{};
|
||||||
|
int y{};
|
||||||
|
if (readOffsetPair(l["offset"], x, y)) { entity.setLightOffset(x, y); }
|
||||||
|
}
|
||||||
|
if (l.contains("flicker_params")) {
|
||||||
|
Lighting::FlickerConfig fp = entity.getFlickerParams();
|
||||||
|
applyFlickerOverrides(l["flicker_params"], fp);
|
||||||
|
entity.setFlickerParams(fp);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Bloc `collision: { size: [w, h], base_size: [bw, bh]?, offset: [x, y]? }`
|
// Bloc `collision: { size: [w, h], base_size: [bw, bh]?, offset: [x, y]? }`
|
||||||
@@ -346,6 +427,9 @@ namespace {
|
|||||||
const auto LIGHT = resolveLightWithRoom(type, readLight(tmpl));
|
const auto LIGHT = resolveLightWithRoom(type, readLight(tmpl));
|
||||||
entity->setLightRadius(LIGHT.radius);
|
entity->setLightRadius(LIGHT.radius);
|
||||||
entity->setLightFlicker(LIGHT.flicker);
|
entity->setLightFlicker(LIGHT.flicker);
|
||||||
|
entity->setLightColor(LIGHT.color_r, LIGHT.color_g, LIGHT.color_b);
|
||||||
|
entity->setLightOffset(LIGHT.offset_x, LIGHT.offset_y);
|
||||||
|
entity->setFlickerParams(LIGHT.flicker_params);
|
||||||
anchor_out = readAnchor(tmpl);
|
anchor_out = readAnchor(tmpl);
|
||||||
return entity;
|
return entity;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -150,10 +150,46 @@ void Player::loadFrom(const std::string& yaml_path) {
|
|||||||
void Player::readLightBlock(const fkyaml::node& yaml) {
|
void Player::readLightBlock(const fkyaml::node& yaml) {
|
||||||
light_radius = 0;
|
light_radius = 0;
|
||||||
light_flicker = false;
|
light_flicker = false;
|
||||||
|
light_color_r = 255;
|
||||||
|
light_color_g = 255;
|
||||||
|
light_color_b = 255;
|
||||||
|
light_offset_x = 0;
|
||||||
|
light_offset_y = 0;
|
||||||
|
// Punt de partida del flicker: el global de lighting.yaml. El bloc
|
||||||
|
// opcional `flicker_params:` del personatge sobreescriu camps concrets.
|
||||||
|
flicker_params = Lighting::flickerConfig();
|
||||||
if (!yaml.contains("light")) { return; }
|
if (!yaml.contains("light")) { return; }
|
||||||
const auto& l = yaml["light"];
|
const auto& l = yaml["light"];
|
||||||
if (l.contains("radius")) { light_radius = l["radius"].get_value<int>(); }
|
if (l.contains("radius")) { light_radius = l["radius"].get_value<int>(); }
|
||||||
if (l.contains("flicker")) { light_flicker = l["flicker"].get_value<bool>(); }
|
if (l.contains("flicker")) { light_flicker = l["flicker"].get_value<bool>(); }
|
||||||
|
// `color: [r, g, b]` opcional, bytes 0..255. Si manca, blanc (sense
|
||||||
|
// tint) — comportament idèntic al mode subtractive de hui.
|
||||||
|
if (l.contains("color")) {
|
||||||
|
const auto& c = l["color"];
|
||||||
|
if (c.is_sequence() && c.size() == 3) {
|
||||||
|
light_color_r = static_cast<std::uint8_t>(std::clamp(c[0].get_value<int>(), 0, 255));
|
||||||
|
light_color_g = static_cast<std::uint8_t>(std::clamp(c[1].get_value<int>(), 0, 255));
|
||||||
|
light_color_b = static_cast<std::uint8_t>(std::clamp(c[2].get_value<int>(), 0, 255));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// `offset: [x, y]` opcional, píxels enters. Default (0, 0) → centre
|
||||||
|
// del sprite. Y creix cap avall, així que `y: -3` puja la llum 3 px.
|
||||||
|
if (l.contains("offset")) {
|
||||||
|
const auto& o = l["offset"];
|
||||||
|
if (o.is_sequence() && o.size() == 2) {
|
||||||
|
light_offset_x = o[0].get_value<int>();
|
||||||
|
light_offset_y = o[1].get_value<int>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// `flicker_params: { ... }` opcional. Qualsevol dels 4 camps absent
|
||||||
|
// conserva el valor heretat del global (vegeu inicialització a sobre).
|
||||||
|
if (l.contains("flicker_params")) {
|
||||||
|
const auto& fp = l["flicker_params"];
|
||||||
|
if (fp.contains("wave_amplitude_ratio")) { flicker_params.wave_amplitude_ratio = fp["wave_amplitude_ratio"].get_value<float>(); }
|
||||||
|
if (fp.contains("wave_frequency_hz")) { flicker_params.wave_frequency_hz = fp["wave_frequency_hz"].get_value<float>(); }
|
||||||
|
if (fp.contains("noise_amplitude_ratio")) { flicker_params.noise_amplitude_ratio = fp["noise_amplitude_ratio"].get_value<float>(); }
|
||||||
|
if (fp.contains("noise_frequency_hz")) { flicker_params.noise_frequency_hz = fp["noise_frequency_hz"].get_value<float>(); }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void Player::readCollisionBlock(const fkyaml::node& yaml, const std::string& yaml_path) {
|
void Player::readCollisionBlock(const fkyaml::node& yaml, const std::string& yaml_path) {
|
||||||
@@ -1479,9 +1515,10 @@ auto Player::getLightCenter() const -> SDL_FPoint {
|
|||||||
// Sprite top-left = (pos_.x, pos_.y). El render desplaça verticalment
|
// Sprite top-left = (pos_.x, pos_.y). El render desplaça verticalment
|
||||||
// `jump_z_` píxels durant el salt; aplicar el mateix offset aquí fa
|
// `jump_z_` píxels durant el salt; aplicar el mateix offset aquí fa
|
||||||
// que la llum "salti" amb el jugador en lloc de quedar-se al sòl.
|
// que la llum "salti" amb el jugador en lloc de quedar-se al sòl.
|
||||||
|
// Sumem també l'offset estàtic configurat al YAML del personatge.
|
||||||
return {
|
return {
|
||||||
.x = getPosX() + (static_cast<float>(sprite_w) * 0.5F),
|
.x = getPosX() + (static_cast<float>(sprite_w) * 0.5F) + static_cast<float>(light_offset_x),
|
||||||
.y = getPosY() - jump_z_ + (static_cast<float>(sprite_h) * 0.5F),
|
.y = getPosY() - jump_z_ + (static_cast<float>(sprite_h) * 0.5F) + static_cast<float>(light_offset_y),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
#include <utility>
|
#include <utility>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
|
#include "core/rendering/lighting.hpp"
|
||||||
#include "core/rendering/sprite/animated_sprite.hpp"
|
#include "core/rendering/sprite/animated_sprite.hpp"
|
||||||
#include "core/rendering/surface.hpp"
|
#include "core/rendering/surface.hpp"
|
||||||
#include "external/fkyaml_node.hpp"
|
#include "external/fkyaml_node.hpp"
|
||||||
@@ -125,12 +126,25 @@ class Player : public AnimatedSprite {
|
|||||||
inline static int body_height = 0;
|
inline static int body_height = 0;
|
||||||
|
|
||||||
// Radi de la llum emesa pel jugador (en píxels del canvas), llegit
|
// Radi de la llum emesa pel jugador (en píxels del canvas), llegit
|
||||||
// del bloc opcional `light: { radius: N, flicker: bool }` del YAML.
|
// del bloc opcional `light: { radius: N, flicker: bool, color: [r,g,b], offset: [x,y] }`
|
||||||
// 0 = no emet. Es declara estàtic com la resta de configs del jugador
|
// del YAML. 0 = no emet. Es declara estàtic com la resta de configs
|
||||||
// (immunity, jump, ...): tots els jugadors del mateix YAML comparteixen
|
// del jugador (immunity, jump, ...): tots els jugadors del mateix YAML
|
||||||
// el valor. `light_flicker` activa la modulació "tipus foc" del radi.
|
// comparteixen el valor. `light_flicker` activa la modulació "tipus
|
||||||
|
// foc" del radi; `light_color_*` són el tint quan la sala usa mode
|
||||||
|
// additive (en subtractive s'ignora); `light_offset_*` desplaça el
|
||||||
|
// centre del cercle respecte el centre del sprite (Y creix avall:
|
||||||
|
// `y: -3` puja la llum 3 px).
|
||||||
inline static int light_radius = 0;
|
inline static int light_radius = 0;
|
||||||
inline static bool light_flicker = false;
|
inline static bool light_flicker = false;
|
||||||
|
inline static std::uint8_t light_color_r = 255;
|
||||||
|
inline static std::uint8_t light_color_g = 255;
|
||||||
|
inline static std::uint8_t light_color_b = 255;
|
||||||
|
inline static int light_offset_x = 0;
|
||||||
|
inline static int light_offset_y = 0;
|
||||||
|
// Paràmetres del flicker resolts per al jugador (combinació del
|
||||||
|
// global + override del YAML del personatge + override de sala).
|
||||||
|
// Només té efecte si `light_flicker` és true.
|
||||||
|
inline static Lighting::FlickerConfig flicker_params{};
|
||||||
|
|
||||||
// Ha de cridar-se abans d'instanciar Player; GameScene ho fa al ctor.
|
// Ha de cridar-se abans d'instanciar Player; GameScene ho fa al ctor.
|
||||||
static void loadFrom(const std::string& yaml_path);
|
static void loadFrom(const std::string& yaml_path);
|
||||||
@@ -146,6 +160,12 @@ class Player : public AnimatedSprite {
|
|||||||
// per a que el cercle "salti" amb el jugador, no es quedi al sòl.
|
// per a que el cercle "salti" amb el jugador, no es quedi al sòl.
|
||||||
[[nodiscard]] static auto getLightRadius() -> int { return light_radius; }
|
[[nodiscard]] static auto getLightRadius() -> int { return light_radius; }
|
||||||
[[nodiscard]] static auto getLightFlicker() -> bool { return light_flicker; }
|
[[nodiscard]] static auto getLightFlicker() -> bool { return light_flicker; }
|
||||||
|
[[nodiscard]] static auto getLightColorR() -> std::uint8_t { return light_color_r; }
|
||||||
|
[[nodiscard]] static auto getLightColorG() -> std::uint8_t { return light_color_g; }
|
||||||
|
[[nodiscard]] static auto getLightColorB() -> std::uint8_t { return light_color_b; }
|
||||||
|
[[nodiscard]] static auto getLightOffsetX() -> int { return light_offset_x; }
|
||||||
|
[[nodiscard]] static auto getLightOffsetY() -> int { return light_offset_y; }
|
||||||
|
[[nodiscard]] static auto getFlickerParams() -> const Lighting::FlickerConfig& { return flicker_params; }
|
||||||
// Seed estable per al flicker: reusem `id_` (0 o 1 en multijugador
|
// Seed estable per al flicker: reusem `id_` (0 o 1 en multijugador
|
||||||
// local) amb un offset gran per evitar col·lisions amb el contador
|
// local) amb un offset gran per evitar col·lisions amb el contador
|
||||||
// d'Entity (que arrenca des de 1). Així P1 i P2 oscil·len en fases
|
// d'Entity (que arrenca des de 1). Així P1 i P2 oscil·len en fases
|
||||||
|
|||||||
+81
-13
@@ -1,5 +1,6 @@
|
|||||||
#include "game/map/map.hpp"
|
#include "game/map/map.hpp"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
#include <stdexcept>
|
#include <stdexcept>
|
||||||
#include <unordered_map>
|
#include <unordered_map>
|
||||||
@@ -49,32 +50,99 @@ namespace Map {
|
|||||||
return yaml_key;
|
return yaml_key;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Parser comú d'un color YAML 3-tupla a bytes. Retorna nullopt
|
||||||
|
// si el node no és una seqüència de 3 elements. Usat pels
|
||||||
|
// overrides de sala i, en `entity_manager_loadlevel.cpp`, també
|
||||||
|
// pels defaults de tipus i overrides de spawn (passa pel mateix
|
||||||
|
// helper per coherència). `clamp` defensiu per a YAMLs que
|
||||||
|
// entreguen valors fora de 0..255.
|
||||||
|
auto parseColorTriplet(const fkyaml::node& node) -> std::optional<std::array<std::uint8_t, 3>> {
|
||||||
|
if (!node.is_sequence() || node.size() != 3) { return std::nullopt; }
|
||||||
|
auto clamp = [](int v) { return static_cast<std::uint8_t>(std::clamp(v, 0, 255)); };
|
||||||
|
return std::array<std::uint8_t, 3>{
|
||||||
|
clamp(node[0].get_value<int>()),
|
||||||
|
clamp(node[1].get_value<int>()),
|
||||||
|
clamp(node[2].get_value<int>()),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parser comú d'una 2-tupla d'enters (offset de llum). Retorna
|
||||||
|
// nullopt si el node no és una seqüència de 2 elements. Sense
|
||||||
|
// clamp: els offsets negatius són legítims (Y creix cap avall,
|
||||||
|
// així que `y: -3` puja la llum 3 px).
|
||||||
|
auto parseIntPair(const fkyaml::node& node) -> std::optional<std::array<int, 2>> {
|
||||||
|
if (!node.is_sequence() || node.size() != 2) { return std::nullopt; }
|
||||||
|
return std::array<int, 2>{
|
||||||
|
node[0].get_value<int>(),
|
||||||
|
node[1].get_value<int>(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Llegeix el sub-bloc opcional `flicker_params:` d'un emissor de
|
||||||
|
// sala. Camps individuals — qualsevol absent conserva el valor
|
||||||
|
// del global (`Lighting::flickerConfig()`) o de la capa que ho
|
||||||
|
// precedeixi (tipus / sala).
|
||||||
|
void readFlickerOverrides(const fkyaml::node& fp, RoomEmitterOverride& ov) {
|
||||||
|
if (fp.contains("wave_amplitude_ratio")) { ov.flicker_wave_amplitude_ratio = fp["wave_amplitude_ratio"].get_value<float>(); }
|
||||||
|
if (fp.contains("wave_frequency_hz")) { ov.flicker_wave_frequency_hz = fp["wave_frequency_hz"].get_value<float>(); }
|
||||||
|
if (fp.contains("noise_amplitude_ratio")) { ov.flicker_noise_amplitude_ratio = fp["noise_amplitude_ratio"].get_value<float>(); }
|
||||||
|
if (fp.contains("noise_frequency_hz")) { ov.flicker_noise_frequency_hz = fp["noise_frequency_hz"].get_value<float>(); }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Llegeix una entrada del mapping `lighting.emitters.<tipus>` en
|
||||||
|
// qualsevol de les seves 3 formes (bool curt o mapping detallat).
|
||||||
|
// Extret per mantenir `loadLightingBlock` sota el llindar de
|
||||||
|
// complexitat cognitiva de clang-tidy.
|
||||||
|
auto readEmitterEntry(const fkyaml::node& val) -> RoomEmitterOverride {
|
||||||
|
RoomEmitterOverride ov;
|
||||||
|
if (val.is_boolean()) {
|
||||||
|
ov.enabled = val.get_value<bool>();
|
||||||
|
return ov;
|
||||||
|
}
|
||||||
|
if (!val.is_mapping()) { return ov; }
|
||||||
|
ov.enabled = true;
|
||||||
|
if (val.contains("radius")) { ov.radius = val["radius"].get_value<int>(); }
|
||||||
|
if (val.contains("flicker")) { ov.flicker = val["flicker"].get_value<bool>(); }
|
||||||
|
if (val.contains("color")) { ov.color = parseColorTriplet(val["color"]); }
|
||||||
|
if (val.contains("offset")) { ov.offset = parseIntPair(val["offset"]); }
|
||||||
|
if (val.contains("flicker_params")) { readFlickerOverrides(val["flicker_params"], ov); }
|
||||||
|
return ov;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Llegeix la subclau `mode:` del bloc `lighting:` i actualitza
|
||||||
|
// `lighting_mode`. Avisa via Log si el valor no és reconegut.
|
||||||
|
// Extret de `loadLightingBlock` per la mateixa raó.
|
||||||
|
void readLightingMode(const fkyaml::node& lighting) {
|
||||||
|
if (!lighting.contains("mode")) { return; }
|
||||||
|
const auto MODE_STR = lighting["mode"].get_value<std::string>();
|
||||||
|
if (MODE_STR == "additive") {
|
||||||
|
lighting_mode = Lighting::Mode::ADDITIVE;
|
||||||
|
} else if (MODE_STR == "subtractive") {
|
||||||
|
lighting_mode = Lighting::Mode::SUBTRACTIVE;
|
||||||
|
} else {
|
||||||
|
Log::warn("Map", "lighting.mode '{}' desconegut — s'usa subtractive", MODE_STR);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Llegeix el bloc opcional `lighting:` del YAML d'una sala i
|
// Llegeix el bloc opcional `lighting:` del YAML d'una sala i
|
||||||
// actualitza `light_level` (si declara `level`) i el mapping
|
// actualitza `light_level`, `lighting_mode` i el mapping
|
||||||
// `g_room_emitters`. Idempotent: cada `Map::loadFrom` neteja el
|
// `g_room_emitters`. Idempotent: cada `Map::loadFrom` reseteja
|
||||||
// mapping abans de poblar-lo, així no s'arrosseguen overrides
|
// tot l'estat abans de poblar-lo, així no s'arrosseguen valors
|
||||||
// d'una sala anterior.
|
// d'una sala anterior.
|
||||||
void loadLightingBlock(const fkyaml::node& yaml) {
|
void loadLightingBlock(const fkyaml::node& yaml) {
|
||||||
g_room_emitters.clear();
|
g_room_emitters.clear();
|
||||||
|
lighting_mode = Lighting::Mode::SUBTRACTIVE;
|
||||||
if (!yaml.contains("lighting")) { return; }
|
if (!yaml.contains("lighting")) { return; }
|
||||||
const auto& lighting = yaml["lighting"];
|
const auto& lighting = yaml["lighting"];
|
||||||
if (lighting.contains("level")) {
|
if (lighting.contains("level")) {
|
||||||
light_level = lighting["level"].get_value<float>();
|
light_level = lighting["level"].get_value<float>();
|
||||||
}
|
}
|
||||||
|
readLightingMode(lighting);
|
||||||
if (!lighting.contains("emitters") || !lighting["emitters"].is_mapping()) { return; }
|
if (!lighting.contains("emitters") || !lighting["emitters"].is_mapping()) { return; }
|
||||||
const auto& em = lighting["emitters"];
|
const auto& em = lighting["emitters"];
|
||||||
for (auto it = em.begin(); it != em.end(); ++it) {
|
for (auto it = em.begin(); it != em.end(); ++it) {
|
||||||
const auto KEY = normalizeEmitterKey(it.key().get_value<std::string>());
|
const auto KEY = normalizeEmitterKey(it.key().get_value<std::string>());
|
||||||
const auto& val = it.value();
|
g_room_emitters[KEY] = readEmitterEntry(it.value());
|
||||||
RoomEmitterOverride ov;
|
|
||||||
if (val.is_boolean()) {
|
|
||||||
ov.enabled = val.get_value<bool>();
|
|
||||||
} else if (val.is_mapping()) {
|
|
||||||
ov.enabled = true;
|
|
||||||
if (val.contains("radius")) { ov.radius = val["radius"].get_value<int>(); }
|
|
||||||
if (val.contains("flicker")) { ov.flicker = val["flicker"].get_value<bool>(); }
|
|
||||||
}
|
|
||||||
g_room_emitters[KEY] = ov;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|||||||
@@ -2,11 +2,15 @@
|
|||||||
|
|
||||||
#include <SDL3/SDL.h>
|
#include <SDL3/SDL.h>
|
||||||
|
|
||||||
|
#include <array>
|
||||||
|
#include <cstdint>
|
||||||
#include <functional>
|
#include <functional>
|
||||||
#include <optional>
|
#include <optional>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <utility>
|
#include <utility>
|
||||||
|
|
||||||
|
#include "core/rendering/lighting.hpp"
|
||||||
|
|
||||||
// Mapa d'una piràmide: sala rectangular amb marcador superior, muros pels
|
// Mapa d'una piràmide: sala rectangular amb marcador superior, muros pels
|
||||||
// quatre costats i graella de sarcòfags envoltada per un passadís a cada
|
// quatre costats i graella de sarcòfags envoltada per un passadís a cada
|
||||||
// cantó. La geometria (graella, pitch, amplada de murs) es carrega des del
|
// cantó. La geometria (graella, pitch, amplada de murs) es carrega des del
|
||||||
@@ -60,6 +64,13 @@ namespace Map {
|
|||||||
// al root es manté per compatibilitat enrere.
|
// al root es manté per compatibilitat enrere.
|
||||||
inline float light_level = 1.0F;
|
inline float light_level = 1.0F;
|
||||||
|
|
||||||
|
// Mode de composició de la pasada d'il·luminació (subtractive per
|
||||||
|
// defecte; additive si la sala declara `lighting.mode: additive`).
|
||||||
|
// Veure el comentari de `core/rendering/lighting.hpp` per a la
|
||||||
|
// semàntica de cada mode. En subtractive, els colors dels emissors
|
||||||
|
// s'ignoren — el camí és el de sempre.
|
||||||
|
inline Lighting::Mode lighting_mode = Lighting::Mode::SUBTRACTIVE;
|
||||||
|
|
||||||
// Override d'emissor declarat al bloc opcional `lighting.emitters.<tipus>`
|
// Override d'emissor declarat al bloc opcional `lighting.emitters.<tipus>`
|
||||||
// de la sala. Tres formes acceptades al YAML:
|
// de la sala. Tres formes acceptades al YAML:
|
||||||
// <tipus>: false → desactiva la llum per a tot el tipus
|
// <tipus>: false → desactiva la llum per a tot el tipus
|
||||||
@@ -80,6 +91,22 @@ namespace Map {
|
|||||||
bool enabled{true};
|
bool enabled{true};
|
||||||
std::optional<int> radius;
|
std::optional<int> radius;
|
||||||
std::optional<bool> flicker;
|
std::optional<bool> flicker;
|
||||||
|
// Color emès en mode additive. `nullopt` → conservar el color
|
||||||
|
// del tipus (default blanc 255,255,255 si el tipus no en declara).
|
||||||
|
// La forma YAML és `color: [r, g, b]` (bytes 0..255).
|
||||||
|
std::optional<std::array<std::uint8_t, 3>> color;
|
||||||
|
// Offset del centre del cercle de llum respecte el centre del
|
||||||
|
// sprite (píxels enters; Y creix avall). `nullopt` → conservar
|
||||||
|
// l'offset del tipus (default [0, 0]). Forma YAML: `offset: [x, y]`.
|
||||||
|
std::optional<std::array<int, 2>> offset;
|
||||||
|
// Overrides parcials per cada camp del `FlickerConfig` (ràtio
|
||||||
|
// d'amplitud i freqüència del wave + noise). `nullopt` → no
|
||||||
|
// toca aquell camp (conserva el del tipus, que al seu torn pot
|
||||||
|
// ser el global). Forma YAML: `flicker_params: { wave_frequency_hz: 1.0, ... }`.
|
||||||
|
std::optional<float> flicker_wave_amplitude_ratio;
|
||||||
|
std::optional<float> flicker_wave_frequency_hz;
|
||||||
|
std::optional<float> flicker_noise_amplitude_ratio;
|
||||||
|
std::optional<float> flicker_noise_frequency_hz;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Consulta l'override de sala per a un tipus d'entitat (clau singular,
|
// Consulta l'override de sala per a un tipus d'entitat (clau singular,
|
||||||
|
|||||||
@@ -250,6 +250,19 @@ namespace {
|
|||||||
}
|
}
|
||||||
if (room->radius.has_value()) { Player::light_radius = *room->radius; }
|
if (room->radius.has_value()) { Player::light_radius = *room->radius; }
|
||||||
if (room->flicker.has_value()) { Player::light_flicker = *room->flicker; }
|
if (room->flicker.has_value()) { Player::light_flicker = *room->flicker; }
|
||||||
|
if (room->color.has_value()) {
|
||||||
|
Player::light_color_r = (*room->color)[0];
|
||||||
|
Player::light_color_g = (*room->color)[1];
|
||||||
|
Player::light_color_b = (*room->color)[2];
|
||||||
|
}
|
||||||
|
if (room->offset.has_value()) {
|
||||||
|
Player::light_offset_x = (*room->offset)[0];
|
||||||
|
Player::light_offset_y = (*room->offset)[1];
|
||||||
|
}
|
||||||
|
if (room->flicker_wave_amplitude_ratio.has_value()) { Player::flicker_params.wave_amplitude_ratio = *room->flicker_wave_amplitude_ratio; }
|
||||||
|
if (room->flicker_wave_frequency_hz.has_value()) { Player::flicker_params.wave_frequency_hz = *room->flicker_wave_frequency_hz; }
|
||||||
|
if (room->flicker_noise_amplitude_ratio.has_value()) { Player::flicker_params.noise_amplitude_ratio = *room->flicker_noise_amplitude_ratio; }
|
||||||
|
if (room->flicker_noise_frequency_hz.has_value()) { Player::flicker_params.noise_frequency_hz = *room->flicker_noise_frequency_hz; }
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
@@ -581,6 +594,14 @@ void GameScene::loadRoom(const std::string& yaml_path) {
|
|||||||
// CLOSED amb visited buit, momies i altres actors a la posició inicial.
|
// CLOSED amb visited buit, momies i altres actors a la posició inicial.
|
||||||
config_.map_yaml = yaml_path;
|
config_.map_yaml = yaml_path;
|
||||||
Map::loadFrom(yaml_path);
|
Map::loadFrom(yaml_path);
|
||||||
|
// Refresquem els statics del Player a partir del YAML i tornem a
|
||||||
|
// aplicar l'override d'aquesta sala. Sense això, una sala amb
|
||||||
|
// `players: false` deixa `light_radius=0` i, en transicionar a una
|
||||||
|
// altra sala que no toqui el camp (o digui `players: true`), els
|
||||||
|
// jugadors no tornen a emetre — l'override actual és "additiu"
|
||||||
|
// sobre els statics existents, no un reset.
|
||||||
|
Player::loadFrom(config_.player_yaml);
|
||||||
|
applyRoomLightOverrideToPlayer();
|
||||||
// Tilemap pre-blita els tiles a un buffer ARGB intern al ctor — sense
|
// Tilemap pre-blita els tiles a un buffer ARGB intern al ctor — sense
|
||||||
// reconstruir-lo, la imatge de la sala anterior es queda fixada per a
|
// reconstruir-lo, la imatge de la sala anterior es queda fixada per a
|
||||||
// sempre. PaintLayer captura `tileset_path_`/`tileset_cols_` per valor
|
// sempre. PaintLayer captura `tileset_path_`/`tileset_cols_` per valor
|
||||||
@@ -1040,20 +1061,28 @@ void GameScene::applyLightingPass() {
|
|||||||
// gating extern aquí (els jugadors són només 1-2 actors i el cost
|
// gating extern aquí (els jugadors són només 1-2 actors i el cost
|
||||||
// d'afegir-los al vector és menyspreable).
|
// d'afegir-los al vector és menyspreable).
|
||||||
const bool PLAYER_FLICKER = Player::getLightFlicker();
|
const bool PLAYER_FLICKER = Player::getLightFlicker();
|
||||||
|
const Lighting::Color PLAYER_COLOR{
|
||||||
|
.r = Player::getLightColorR(),
|
||||||
|
.g = Player::getLightColorG(),
|
||||||
|
.b = Player::getLightColorB(),
|
||||||
|
};
|
||||||
|
const Lighting::FlickerConfig PLAYER_FP = Player::getFlickerParams();
|
||||||
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, .seed = p->getLightSeed(), .flicker = PLAYER_FLICKER});
|
emitters.push_back({.cx = C.x, .cy = C.y, .radius = PLAYER_R, .seed = p->getLightSeed(), .flicker = PLAYER_FLICKER, .color = PLAYER_COLOR, .flicker_params = PLAYER_FP});
|
||||||
}
|
}
|
||||||
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, .seed = e->getLightSeed(), .flicker = e->getLightFlicker()});
|
const Lighting::Color COL{.r = e->getLightColorR(), .g = e->getLightColorG(), .b = e->getLightColorB()};
|
||||||
|
emitters.push_back({.cx = C.x, .cy = C.y, .radius = R, .seed = e->getLightSeed(), .flicker = e->getLightFlicker(), .color = COL, .flicker_params = e->getFlickerParams()});
|
||||||
}
|
}
|
||||||
Lighting::apply(Screen::get().getCanvasArgb(),
|
Lighting::apply(Screen::get().getCanvasArgb(),
|
||||||
Screen::get().getCanvasWidth(),
|
Screen::get().getCanvasWidth(),
|
||||||
Screen::get().getCanvasHeight(),
|
Screen::get().getCanvasHeight(),
|
||||||
|
Map::lighting_mode,
|
||||||
Map::light_level,
|
Map::light_level,
|
||||||
emitters);
|
emitters);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -111,20 +111,36 @@ namespace Defaults {
|
|||||||
// fitxer falta o li manca alguna clau. La forma final (3 anells) es
|
// fitxer falta o li manca alguna clau. La forma final (3 anells) es
|
||||||
// descriu al YAML; veure `Lighting::GradientConfig` per a la semàntica.
|
// descriu al YAML; veure `Lighting::GradientConfig` per a la semàntica.
|
||||||
namespace Lighting {
|
namespace Lighting {
|
||||||
|
// Defaults del gradient subtractive (mode històric). Les
|
||||||
|
// intensitats són multiplicadors 0..1 que es multipliquen sobre
|
||||||
|
// el píxel: 1.0 = no toca, 0.0 = negre.
|
||||||
constexpr float INNER_RATIO = 0.8F;
|
constexpr float INNER_RATIO = 0.8F;
|
||||||
constexpr float MID_RATIO = 0.9F;
|
constexpr float MID_RATIO = 0.9F;
|
||||||
constexpr float INNER_INTENSITY = 1.0F;
|
constexpr float INNER_INTENSITY = 1.0F;
|
||||||
constexpr float MID_INTENSITY = 0.9F;
|
constexpr float MID_INTENSITY = 0.9F;
|
||||||
constexpr float OUTER_INTENSITY = 0.8F;
|
constexpr float OUTER_INTENSITY = 0.8F;
|
||||||
|
// Defaults del gradient additive. Les intensitats són "alfa"
|
||||||
|
// del color afegit (0..1): 1.0 = afegeix el color sencer i
|
||||||
|
// satura, 0.0 = no afegeix res. Valors baixos a propòsit perquè
|
||||||
|
// l'additive es nota fort molt fàcilment — només un toc de tint
|
||||||
|
// cromàtic al voltant de l'emissor.
|
||||||
|
constexpr float INNER_RATIO_ADDITIVE = 0.5F;
|
||||||
|
constexpr float MID_RATIO_ADDITIVE = 0.85F;
|
||||||
|
constexpr float INNER_INTENSITY_ADDITIVE = 0.15F;
|
||||||
|
constexpr float MID_INTENSITY_ADDITIVE = 0.10F;
|
||||||
|
constexpr float OUTER_INTENSITY_ADDITIVE = 0.05F;
|
||||||
// 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
|
// Defaults del flicker. Els reemplaça el bloc `flicker:` de
|
||||||
// data/config/lighting.yaml. Veure `Lighting::FlickerConfig`
|
// data/config/lighting.yaml. Veure `Lighting::FlickerConfig`
|
||||||
// per a la semàntica de cada camp.
|
// per a la semàntica de cada camp. Les amplituds són **ràtios**
|
||||||
constexpr float FLICKER_WAVE_AMPLITUDE_PX = 2.0F;
|
// sobre el radi base de l'emissor: així el feel del flicker és
|
||||||
|
// independent del radi. Els valors per defecte conserven el feel
|
||||||
|
// anterior per a una torxa de radi 40 (2/40 = 0.05, 1/40 = 0.025).
|
||||||
|
constexpr float FLICKER_WAVE_AMPLITUDE_RATIO = 0.05F;
|
||||||
constexpr float FLICKER_WAVE_FREQUENCY_HZ = 3.0F;
|
constexpr float FLICKER_WAVE_FREQUENCY_HZ = 3.0F;
|
||||||
constexpr float FLICKER_NOISE_AMPLITUDE_PX = 1.0F;
|
constexpr float FLICKER_NOISE_AMPLITUDE_RATIO = 0.025F;
|
||||||
constexpr float FLICKER_NOISE_FREQUENCY_HZ = 7.0F;
|
constexpr float FLICKER_NOISE_FREQUENCY_HZ = 7.0F;
|
||||||
} // namespace Lighting
|
} // namespace Lighting
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user