feat(rendering): lighting module CPU + gradient config YAML
This commit is contained in:
1 parent
5e5ff835a9
commit
5ed963b53d
6 files changed
+295
No files matched your search
@@ -0,0 +1,187 @@
|
||||
#include "core/rendering/lighting.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstddef>
|
||||
#include <vector>
|
||||
|
||||
#include "core/resources/resource_helper.hpp"
|
||||
#include "external/fkyaml_node.hpp"
|
||||
#include "utils/defaults.hpp"
|
||||
#include "utils/log.hpp"
|
||||
|
||||
namespace Lighting {
|
||||
|
||||
namespace {
|
||||
|
||||
// Buffer de luminància compartit entre frames. Es redimensiona
|
||||
// només si canvia la mida del canvas (mai en un run normal: el
|
||||
// canvas del joc és 320×240 fix). El reusem perquè una `apply`
|
||||
// allotjant cada frame seria un cost evitable.
|
||||
auto luminanceBuffer() -> std::vector<std::uint8_t>& {
|
||||
static std::vector<std::uint8_t> buf_;
|
||||
return buf_;
|
||||
}
|
||||
|
||||
// Configuració global, inicialitzada amb els defaults compilats.
|
||||
// loadConfig() la reemplaça si troba el fitxer.
|
||||
auto activeConfig() -> GradientConfig& {
|
||||
static GradientConfig cfg_{
|
||||
.inner_ratio = Defaults::Lighting::INNER_RATIO,
|
||||
.mid_ratio = Defaults::Lighting::MID_RATIO,
|
||||
.inner_intensity = Defaults::Lighting::INNER_INTENSITY,
|
||||
.mid_intensity = Defaults::Lighting::MID_INTENSITY,
|
||||
.outer_intensity = Defaults::Lighting::OUTER_INTENSITY,
|
||||
};
|
||||
return cfg_;
|
||||
}
|
||||
|
||||
// Quantitza un factor 0..1 a 0..255. `clamp` per defensar-nos de
|
||||
// YAMLs que entreguen valors fora de rang sense forçar la mà.
|
||||
auto toByte(float v) -> std::uint8_t {
|
||||
const float CLAMPED = std::clamp(v, 0.0F, 1.0F);
|
||||
return static_cast<std::uint8_t>(std::lround(CLAMPED * 255.0F));
|
||||
}
|
||||
|
||||
void readField(const fkyaml::node& node, const char* key, float& dst) {
|
||||
if (node.contains(key)) { dst = node[key].get_value<float>(); }
|
||||
}
|
||||
|
||||
// Pre-calcula els 3 quadrats de radi i les intensitats que farem
|
||||
// servir dins del bucle per píxel. Es resol una sola vegada per
|
||||
// emissor i s'evita arrastrar 6 paràmetres a `rasterizeEmitter`.
|
||||
struct EmitterRings {
|
||||
int radius_sq{0};
|
||||
int mid_sq{0};
|
||||
int inner_sq{0};
|
||||
std::uint8_t inner{0};
|
||||
std::uint8_t mid{0};
|
||||
std::uint8_t outer{0};
|
||||
};
|
||||
|
||||
auto computeRings(int radius, const GradientConfig& cfg) -> EmitterRings {
|
||||
const auto R_F = static_cast<float>(radius);
|
||||
const float R_INNER = R_F * cfg.inner_ratio;
|
||||
const float R_MID = R_F * cfg.mid_ratio;
|
||||
return EmitterRings{
|
||||
.radius_sq = radius * radius,
|
||||
.mid_sq = static_cast<int>(R_MID * R_MID),
|
||||
.inner_sq = static_cast<int>(R_INNER * R_INNER),
|
||||
.inner = toByte(cfg.inner_intensity),
|
||||
.mid = toByte(cfg.mid_intensity),
|
||||
.outer = toByte(cfg.outer_intensity),
|
||||
};
|
||||
}
|
||||
|
||||
// Itera la bounding box de l'emissor clipada al canvas i actualitza
|
||||
// la fila de luminància corresponent amb `max(actual, ring)`. Es
|
||||
// crida un cop per emissor, així extreure-la deixa `apply` simple.
|
||||
void rasterizeEmitter(const Emitter& e, const EmitterRings& rings, std::uint8_t* lum, 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::uint8_t* row = lum + (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;
|
||||
}
|
||||
row[x] = std::max(intensity, row[x]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Pasada final: pixel = pixel * lum / 255 per canal. El canal alpha
|
||||
// es preserva (la resta del pipeline assumeix opacitat total al
|
||||
// canvas; veure Rendering::OPAQUE_ALPHA).
|
||||
void modulateCanvas(std::uint32_t* canvas, const std::uint8_t* lum, int width, int height) {
|
||||
for (int y = 0; y < height; ++y) {
|
||||
std::uint32_t* px = canvas + (static_cast<std::ptrdiff_t>(y) * width);
|
||||
const std::uint8_t* lrow = lum + (static_cast<std::ptrdiff_t>(y) * width);
|
||||
for (int x = 0; x < width; ++x) {
|
||||
const std::uint8_t L = lrow[x];
|
||||
if (L == 255) { continue; } // píxel intacte.
|
||||
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 * L) / 255);
|
||||
const auto G = static_cast<std::uint8_t>((SG * L) / 255);
|
||||
const auto B = static_cast<std::uint8_t>((SB * L) / 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
auto config() -> const GradientConfig& {
|
||||
return activeConfig();
|
||||
}
|
||||
|
||||
void loadConfig(const std::string& yaml_path) {
|
||||
auto bytes = Resource::Helper::loadFile(yaml_path);
|
||||
if (bytes.empty()) {
|
||||
Log::warn("Lighting", "no s'ha pogut obrir {} — s'usaran els defaults compilats", yaml_path);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const auto* begin = reinterpret_cast<const char*>(bytes.data());
|
||||
const auto* end = begin + bytes.size();
|
||||
auto yaml = fkyaml::node::deserialize(begin, end);
|
||||
if (yaml.contains("gradient")) {
|
||||
const auto& g = yaml["gradient"];
|
||||
auto& cfg = activeConfig();
|
||||
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);
|
||||
}
|
||||
} catch (const std::exception& ex) {
|
||||
Log::warn("Lighting", "error parsejant {}: {} — s'usaran els defaults compilats", yaml_path, ex.what());
|
||||
}
|
||||
}
|
||||
|
||||
void apply(std::uint32_t* canvas, int width, int height, float room_light, std::span<const Emitter> emitters) {
|
||||
if (canvas == nullptr || width <= 0 || height <= 0) { return; }
|
||||
|
||||
// Atajo: si la sala està totalment il·luminada i no hi ha
|
||||
// emissors, la pasada seria un `pixel * 1.0` global — no cal
|
||||
// tocar res. És el camí curt habitual mentre no posem
|
||||
// `light_level` a les sales.
|
||||
const std::uint8_t ROOM = toByte(room_light);
|
||||
if (ROOM == 255 && emitters.empty()) { return; }
|
||||
|
||||
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 = activeConfig();
|
||||
for (const auto& e : emitters) {
|
||||
if (e.radius <= 0) { continue; }
|
||||
const auto RINGS = computeRings(e.radius, cfg);
|
||||
rasterizeEmitter(e, RINGS, lum.data(), width, height);
|
||||
}
|
||||
|
||||
modulateCanvas(canvas, lum.data(), width, height);
|
||||
}
|
||||
|
||||
} // namespace Lighting
|
||||
@@ -0,0 +1,64 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <span>
|
||||
#include <string>
|
||||
|
||||
// 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
|
||||
// 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
|
||||
// derivat de la combinació de "llum de la sala" + cercles emissors al
|
||||
// voltant de certes entitats.
|
||||
//
|
||||
// Forma de cada emissor (radial, 3 anells configurables al fitxer
|
||||
// data/config/lighting.yaml; veure `GradientConfig`):
|
||||
// 0..inner_ratio del radi → inner_intensity
|
||||
// inner_ratio..mid_ratio → mid_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
|
||||
// llum de sala i de la intensitat aportada per cada emissor que el
|
||||
// solapa — no se "sumen" intensitats. Coherent amb la idea de "centre
|
||||
// 1.0": el píxel que cau dins de qualsevol nucli no s'enfosqueix.
|
||||
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`).
|
||||
struct Emitter {
|
||||
float cx{0.0F};
|
||||
float cy{0.0F};
|
||||
int radius{0};
|
||||
};
|
||||
|
||||
// Forma del gradient. Compartida per tots els emissors per evitar
|
||||
// duplicar la corba a cada YAML d'entitat. Càrrega via loadConfig.
|
||||
struct GradientConfig {
|
||||
float inner_ratio{0.8F};
|
||||
float mid_ratio{0.9F};
|
||||
float inner_intensity{1.0F};
|
||||
float mid_intensity{0.9F};
|
||||
float outer_intensity{0.8F};
|
||||
};
|
||||
|
||||
// Llegeix `yaml_path` (data/config/lighting.yaml) i pobla la
|
||||
// configuració global. Qualsevol clau absent cau als Defaults::Lighting.
|
||||
// No es crida més d'una vegada en un run normal; tornar-la a cridar
|
||||
// simplement reemplaça la configuració activa.
|
||||
void loadConfig(const std::string& yaml_path);
|
||||
|
||||
// Configuració activa (per a debug/inspectors). Inicialitzada amb els
|
||||
// Defaults::Lighting fins que loadConfig() es crida.
|
||||
[[nodiscard]] auto config() -> const GradientConfig&;
|
||||
|
||||
// Aplica la pasada d'il·luminació al canvas in-place. `room_light`
|
||||
// és l'atenuació base de la sala (0..1, 1 = sense atenuació). El
|
||||
// span d'emissors pot ser buit — en aquest cas tota la sala queda
|
||||
// multiplicada per `room_light`. Aquesta funció no fa cap allocació
|
||||
// si es crida amb la mateixa mida de canvas múltiples cops: manté
|
||||
// un buffer intern reutilitzable.
|
||||
void apply(std::uint32_t* canvas, int width, int height, float room_light, std::span<const Emitter> emitters);
|
||||
|
||||
} // namespace Lighting
|
||||
Reference in new issue
Block a user