From 5cc8e349a55cb4d347010df73ff9563e58969a0c Mon Sep 17 00:00:00 2001 From: Sergio Valor Date: Tue, 12 May 2026 22:37:27 +0200 Subject: [PATCH] feat(lighting): seed estable per emissor (no deriva pel moviment) --- source/core/rendering/lighting.cpp | 18 +++++++++--------- source/core/rendering/lighting.hpp | 9 +++++---- source/game/entities/entity.hpp | 14 ++++++++++++++ source/game/entities/player.hpp | 5 +++++ source/game/scenes/game_scene.cpp | 4 ++-- 5 files changed, 35 insertions(+), 15 deletions(-) diff --git a/source/core/rendering/lighting.cpp b/source/core/rendering/lighting.cpp index 65b9802..6265c51 100644 --- a/source/core/rendering/lighting.cpp +++ b/source/core/rendering/lighting.cpp @@ -50,14 +50,14 @@ namespace Lighting { } // 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 { + // (`wave` + `noise`) i una fase derivada de `seed`. La seed és un + // identificador estable de l'emissor (no canvia amb la posició) — + // així el patró no "deriva" quan l'emissor es mou. El factor + // ~φ (golden ratio) dispersa les fases per a seeds consecutives. + auto computeFlickerRadius(int base_radius, int seed, float t_s, const FlickerConfig& fc) -> int { constexpr float TWO_PI = 2.0F * std::numbers::pi_v; - const float PHASE = (cx * 0.0731F) + (cy * 0.1373F); + constexpr float GOLDEN_FRACTION = 0.6180339F; + const float PHASE = static_cast(seed) * GOLDEN_FRACTION; const float W1 = std::sin((TWO_PI * fc.wave_frequency_hz * t_s) + PHASE); const float W2 = std::sin((TWO_PI * fc.noise_frequency_hz * t_s) + (PHASE * 2.7F)); const float DELTA = (fc.wave_amplitude_px * W1) + (fc.noise_amplitude_px * W2); @@ -217,14 +217,14 @@ namespace Lighting { for (const auto& e : emitters) { if (e.radius <= 0) { continue; } const int R_EFF = e.flicker - ? computeFlickerRadius(e.radius, e.cx, e.cy, T_S, fc) + ? 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, .flicker = e.flicker}; + 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); } diff --git a/source/core/rendering/lighting.hpp b/source/core/rendering/lighting.hpp index 35ee5ca..25b6e62 100644 --- a/source/core/rendering/lighting.hpp +++ b/source/core/rendering/lighting.hpp @@ -27,14 +27,15 @@ namespace Lighting { // Posició al canvas (en píxels, mateixa unitat que la resta del // motor) i radi efectiu de l'emissor. El centre és el del sprite // visible: és l'entitat qui sap on cau (veure `Entity::getLightCenter`). - // `flicker` activa la modulació del radi per frame (només antorxes - // avui): el radi base es queda al YAML, l'oscil·lació en viu surt - // dels paràmetres globals i de la posició per generar fases - // distintes a cada emissor. + // `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. struct Emitter { float cx{0.0F}; float cy{0.0F}; int radius{0}; + int seed{0}; bool flicker{false}; }; diff --git a/source/game/entities/entity.hpp b/source/game/entities/entity.hpp index 0477628..9faca8c 100644 --- a/source/game/entities/entity.hpp +++ b/source/game/entities/entity.hpp @@ -266,6 +266,14 @@ class Entity { [[nodiscard]] auto getLightFlicker() const -> bool { return light_flicker_; } void setLightFlicker(bool f) { light_flicker_ = f; } + // Identificador estable per al flicker. Cada Entity rep un valor + // ú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 + // mou. Si l'entitat respawneja, la nova instància tindrà una seed + // nova; com que el cicle visual ja queda trencat per la mort, no és + // un problema. + [[nodiscard]] auto getLightSeed() const -> int { return light_seed_; } + // Centre de la llum en píxels del canvas. Per defecte és el centre del // `pixel_rect_` (que és la caixa de render: sprite top-left + mida). // Virtual perquè entitats amb desplaçament visual sobre la posició @@ -287,6 +295,12 @@ class Entity { bool blocks_movement_{false}; int height_{0}; int light_radius_{0}; + // Comptador global d'instàncies; cada Entity nova s'emporta el + // valor incrementat com a seed propi. Estàtic perquè no calgui + // estat extern (sense risc de threads: les entitats es construeixen + // sempre des del fil del joc). + static inline int s_next_light_seed{0}; + int light_seed_{++s_next_light_seed}; bool light_flicker_{false}; bool dead_{false}; SDL_FlipMode flip_{SDL_FLIP_NONE}; diff --git a/source/game/entities/player.hpp b/source/game/entities/player.hpp index 776f8d1..d7b535e 100644 --- a/source/game/entities/player.hpp +++ b/source/game/entities/player.hpp @@ -146,6 +146,11 @@ class Player : public AnimatedSprite { // 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 getLightFlicker() -> bool { return light_flicker; } + // Seed estable per al flicker: reusem `id_` (0 o 1 en multijugador + // 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 + // pròpies que no s'encavalquen amb cap entitat. + [[nodiscard]] auto getLightSeed() const -> int { return id_ + 1000; } [[nodiscard]] auto getLightCenter() const -> SDL_FPoint; Player(const std::string& anim_key, int id, float x, float y, const Controls& controls, const Map::Info& map_info, int paint_tile_index = -1); diff --git a/source/game/scenes/game_scene.cpp b/source/game/scenes/game_scene.cpp index bcb433b..137d851 100644 --- a/source/game/scenes/game_scene.cpp +++ b/source/game/scenes/game_scene.cpp @@ -1022,13 +1022,13 @@ void GameScene::applyLightingPass() { for (const auto& p : players_) { if (p->isOutOfPlay()) { continue; } const auto C = p->getLightCenter(); - emitters.push_back({.cx = C.x, .cy = C.y, .radius = PLAYER_R, .flicker = PLAYER_FLICKER}); + emitters.push_back({.cx = C.x, .cy = C.y, .radius = PLAYER_R, .seed = p->getLightSeed(), .flicker = PLAYER_FLICKER}); } for (const auto* e : entity_manager_.getEntities()) { const int R = e->getLightRadius(); if (R <= 0) { continue; } const auto C = e->getLightCenter(); - emitters.push_back({.cx = C.x, .cy = C.y, .radius = R, .flicker = e->getLightFlicker()}); + emitters.push_back({.cx = C.x, .cy = C.y, .radius = R, .seed = e->getLightSeed(), .flicker = e->getLightFlicker()}); } Lighting::apply(Screen::get().getCanvasArgb(), Screen::get().getCanvasWidth(),