Files
aee-arcade/source/core/rendering/lighting.cpp
T

188 lines
8.1 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#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