segon commit
This commit is contained in:
295
source/core/rendering/gif.cpp
Normal file
295
source/core/rendering/gif.cpp
Normal file
@@ -0,0 +1,295 @@
|
||||
#include "core/rendering/gif.hpp"
|
||||
|
||||
#include <cstring> // Para memcpy, size_t
|
||||
#include <iostream> // Para std::cout
|
||||
#include <stdexcept> // Para runtime_error
|
||||
#include <string> // Para allocator, char_traits, operator==, basic_string
|
||||
|
||||
namespace GIF {
|
||||
|
||||
// Función inline para reemplazar el macro READ.
|
||||
// Actualiza el puntero 'buffer' tras copiar 'size' bytes a 'dst'.
|
||||
inline void readBytes(const uint8_t*& buffer, void* dst, size_t size) {
|
||||
std::memcpy(dst, buffer, size);
|
||||
buffer += size;
|
||||
}
|
||||
|
||||
// Inicializa el diccionario LZW con los valores iniciales
|
||||
inline void initializeDictionary(std::vector<DictionaryEntry>& dictionary, int code_length, int& dictionary_ind) { // NOLINT(readability-identifier-naming)
|
||||
int size = 1 << code_length;
|
||||
dictionary.resize(1 << (code_length + 1));
|
||||
for (dictionary_ind = 0; dictionary_ind < size; dictionary_ind++) {
|
||||
dictionary[dictionary_ind].byte = static_cast<uint8_t>(dictionary_ind);
|
||||
dictionary[dictionary_ind].prev = -1;
|
||||
dictionary[dictionary_ind].len = 1;
|
||||
}
|
||||
dictionary_ind += 2; // Reservamos espacio para clear y stop codes
|
||||
}
|
||||
|
||||
// Lee los próximos bits del stream de entrada para formar un código
|
||||
inline auto readNextCode(const uint8_t*& input, int& input_length, unsigned int& mask, int code_length) -> int {
|
||||
int code = 0;
|
||||
for (int i = 0; i < (code_length + 1); i++) {
|
||||
if (input_length <= 0) {
|
||||
throw std::runtime_error("Unexpected end of input in decompress");
|
||||
}
|
||||
int bit = ((*input & mask) != 0) ? 1 : 0;
|
||||
mask <<= 1;
|
||||
if (mask == 0x100) {
|
||||
mask = 0x01;
|
||||
input++;
|
||||
input_length--;
|
||||
}
|
||||
code |= (bit << i);
|
||||
}
|
||||
return code;
|
||||
}
|
||||
|
||||
// Encuentra el primer byte de una cadena del diccionario
|
||||
inline auto findFirstByte(const std::vector<DictionaryEntry>& dictionary, int code) -> uint8_t {
|
||||
int ptr = code;
|
||||
while (dictionary[ptr].prev != -1) {
|
||||
ptr = dictionary[ptr].prev;
|
||||
}
|
||||
return dictionary[ptr].byte;
|
||||
}
|
||||
|
||||
// Agrega una nueva entrada al diccionario
|
||||
inline void addDictionaryEntry(std::vector<DictionaryEntry>& dictionary, int& dictionary_ind, int& code_length, int prev, int code) { // NOLINT(readability-identifier-naming)
|
||||
uint8_t first_byte;
|
||||
if (code == dictionary_ind) {
|
||||
first_byte = findFirstByte(dictionary, prev);
|
||||
} else {
|
||||
first_byte = findFirstByte(dictionary, code);
|
||||
}
|
||||
|
||||
dictionary[dictionary_ind].byte = first_byte;
|
||||
dictionary[dictionary_ind].prev = prev;
|
||||
dictionary[dictionary_ind].len = dictionary[prev].len + 1;
|
||||
dictionary_ind++;
|
||||
|
||||
if ((dictionary_ind == (1 << (code_length + 1))) && (code_length < 11)) {
|
||||
code_length++;
|
||||
dictionary.resize(1 << (code_length + 1));
|
||||
}
|
||||
}
|
||||
|
||||
// Escribe la cadena decodificada al buffer de salida
|
||||
inline auto writeDecodedString(const std::vector<DictionaryEntry>& dictionary, int code, uint8_t*& out) -> int {
|
||||
int cur_code = code;
|
||||
int match_len = dictionary[cur_code].len;
|
||||
while (cur_code != -1) {
|
||||
out[dictionary[cur_code].len - 1] = dictionary[cur_code].byte;
|
||||
if (dictionary[cur_code].prev == cur_code) {
|
||||
std::cerr << "Internal error; self-reference detected." << '\n';
|
||||
throw std::runtime_error("Internal error in decompress: self-reference");
|
||||
}
|
||||
cur_code = dictionary[cur_code].prev;
|
||||
}
|
||||
out += match_len;
|
||||
return match_len;
|
||||
}
|
||||
|
||||
void Gif::decompress(int code_length, const uint8_t* input, int input_length, uint8_t* out) { // NOLINT(readability-convert-member-functions-to-static)
|
||||
// Verifica que el code_length tenga un rango razonable.
|
||||
if (code_length < 2 || code_length > 12) {
|
||||
throw std::runtime_error("Invalid LZW code length");
|
||||
}
|
||||
|
||||
int prev = -1;
|
||||
std::vector<DictionaryEntry> dictionary;
|
||||
int dictionary_ind;
|
||||
unsigned int mask = 0x01;
|
||||
int reset_code_length = code_length;
|
||||
int clear_code = 1 << code_length;
|
||||
int stop_code = clear_code + 1;
|
||||
|
||||
// Inicializamos el diccionario con el tamaño correspondiente.
|
||||
initializeDictionary(dictionary, code_length, dictionary_ind);
|
||||
|
||||
// Bucle principal: procesar el stream comprimido.
|
||||
while (input_length > 0) {
|
||||
int code = readNextCode(input, input_length, mask, code_length);
|
||||
|
||||
if (code == clear_code) {
|
||||
// Reinicia el diccionario.
|
||||
code_length = reset_code_length;
|
||||
initializeDictionary(dictionary, code_length, dictionary_ind);
|
||||
prev = -1;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (code == stop_code) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (prev > -1 && code_length < 12) {
|
||||
if (code > dictionary_ind) {
|
||||
std::cerr << "code = " << std::hex << code
|
||||
<< ", but dictionary_ind = " << dictionary_ind << '\n';
|
||||
throw std::runtime_error("LZW error: code exceeds dictionary_ind.");
|
||||
}
|
||||
|
||||
addDictionaryEntry(dictionary, dictionary_ind, code_length, prev, code);
|
||||
}
|
||||
|
||||
prev = code;
|
||||
|
||||
// Verifica que 'code' sea un índice válido antes de usarlo.
|
||||
if (code < 0 || static_cast<size_t>(code) >= dictionary.size()) {
|
||||
std::cerr << "Invalid LZW code " << code
|
||||
<< ", dictionary size " << dictionary.size() << '\n';
|
||||
throw std::runtime_error("LZW error: invalid code encountered");
|
||||
}
|
||||
|
||||
writeDecodedString(dictionary, code, out);
|
||||
}
|
||||
}
|
||||
|
||||
auto Gif::readSubBlocks(const uint8_t*& buffer) -> std::vector<uint8_t> { // NOLINT(readability-convert-member-functions-to-static)
|
||||
std::vector<uint8_t> data;
|
||||
uint8_t block_size = *buffer;
|
||||
buffer++;
|
||||
while (block_size != 0) {
|
||||
data.insert(data.end(), buffer, buffer + block_size);
|
||||
buffer += block_size;
|
||||
block_size = *buffer;
|
||||
buffer++;
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
auto Gif::processImageDescriptor(const uint8_t*& buffer, const std::vector<RGB>& gct, int resolution_bits) -> std::vector<uint8_t> { // NOLINT(readability-convert-member-functions-to-static)
|
||||
ImageDescriptor image_descriptor;
|
||||
// Lee 9 bytes para el image descriptor.
|
||||
readBytes(buffer, &image_descriptor, sizeof(ImageDescriptor));
|
||||
|
||||
uint8_t lzw_code_size;
|
||||
readBytes(buffer, &lzw_code_size, sizeof(uint8_t));
|
||||
|
||||
std::vector<uint8_t> compressed_data = readSubBlocks(buffer);
|
||||
int uncompressed_data_length = image_descriptor.image_width * image_descriptor.image_height;
|
||||
std::vector<uint8_t> uncompressed_data(uncompressed_data_length);
|
||||
|
||||
decompress(lzw_code_size, compressed_data.data(), static_cast<int>(compressed_data.size()), uncompressed_data.data());
|
||||
return uncompressed_data;
|
||||
}
|
||||
|
||||
auto Gif::loadPalette(const uint8_t* buffer) -> std::vector<uint32_t> { // NOLINT(readability-convert-member-functions-to-static)
|
||||
uint8_t header[6];
|
||||
std::memcpy(header, buffer, 6);
|
||||
buffer += 6;
|
||||
|
||||
ScreenDescriptor screen_descriptor;
|
||||
std::memcpy(&screen_descriptor, buffer, sizeof(ScreenDescriptor));
|
||||
buffer += sizeof(ScreenDescriptor);
|
||||
|
||||
std::vector<uint32_t> global_color_table;
|
||||
if ((screen_descriptor.fields & 0x80) != 0) {
|
||||
int global_color_table_size = 1 << ((screen_descriptor.fields & 0x07) + 1);
|
||||
global_color_table.resize(global_color_table_size);
|
||||
for (int i = 0; i < global_color_table_size; ++i) {
|
||||
uint8_t r = buffer[0];
|
||||
uint8_t g = buffer[1];
|
||||
uint8_t b = buffer[2];
|
||||
global_color_table[i] = (r << 16) | (g << 8) | b;
|
||||
buffer += 3;
|
||||
}
|
||||
}
|
||||
return global_color_table;
|
||||
}
|
||||
|
||||
auto Gif::processGifStream(const uint8_t* buffer, uint16_t& w, uint16_t& h) -> std::vector<uint8_t> { // NOLINT(readability-convert-member-functions-to-static)
|
||||
// Leer la cabecera de 6 bytes ("GIF87a" o "GIF89a")
|
||||
uint8_t header[6];
|
||||
std::memcpy(header, buffer, 6);
|
||||
buffer += 6;
|
||||
|
||||
// Opcional: Validar header
|
||||
std::string header_str(reinterpret_cast<char*>(header), 6);
|
||||
if (header_str != "GIF87a" && header_str != "GIF89a") {
|
||||
throw std::runtime_error("Formato de archivo GIF inválido.");
|
||||
}
|
||||
|
||||
// Leer el Screen Descriptor (7 bytes, empaquetado sin padding)
|
||||
ScreenDescriptor screen_descriptor;
|
||||
readBytes(buffer, &screen_descriptor, sizeof(ScreenDescriptor));
|
||||
|
||||
// Asigna ancho y alto
|
||||
w = screen_descriptor.width;
|
||||
h = screen_descriptor.height;
|
||||
|
||||
int color_resolution_bits = ((screen_descriptor.fields & 0x70) >> 4) + 1;
|
||||
std::vector<RGB> global_color_table;
|
||||
if ((screen_descriptor.fields & 0x80) != 0) {
|
||||
int global_color_table_size = 1 << ((screen_descriptor.fields & 0x07) + 1);
|
||||
global_color_table.resize(global_color_table_size);
|
||||
std::memcpy(global_color_table.data(), buffer, 3 * global_color_table_size);
|
||||
buffer += 3 * global_color_table_size;
|
||||
}
|
||||
|
||||
// Supongamos que 'buffer' es el puntero actual y TRAILER es 0x3B
|
||||
uint8_t block_type = *buffer++;
|
||||
while (block_type != TRAILER) {
|
||||
if (block_type == EXTENSION_INTRODUCER) // 0x21
|
||||
{
|
||||
// Se lee la etiqueta de extensión, la cual indica el tipo de extensión.
|
||||
uint8_t extension_label = *buffer++;
|
||||
switch (extension_label) {
|
||||
case GRAPHIC_CONTROL: // 0xF9
|
||||
{
|
||||
// Procesar Graphic Control Extension:
|
||||
uint8_t block_size = *buffer++; // Normalmente, blockSize == 4
|
||||
buffer += block_size; // Saltamos los 4 bytes del bloque fijo
|
||||
// Saltar los sub-bloques
|
||||
uint8_t sub_block_size = *buffer++;
|
||||
while (sub_block_size != 0) {
|
||||
buffer += sub_block_size;
|
||||
sub_block_size = *buffer++;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case APPLICATION_EXTENSION: // 0xFF
|
||||
case COMMENT_EXTENSION: // 0xFE
|
||||
case PLAINTEXT_EXTENSION: // 0x01
|
||||
{
|
||||
// Para estas extensiones, saltamos el bloque fijo y los sub-bloques.
|
||||
uint8_t block_size = *buffer++;
|
||||
buffer += block_size;
|
||||
uint8_t sub_block_size = *buffer++;
|
||||
while (sub_block_size != 0) {
|
||||
buffer += sub_block_size;
|
||||
sub_block_size = *buffer++;
|
||||
}
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
// Si la etiqueta de extensión es desconocida, saltarla también:
|
||||
uint8_t block_size = *buffer++;
|
||||
buffer += block_size;
|
||||
uint8_t sub_block_size = *buffer++;
|
||||
while (sub_block_size != 0) {
|
||||
buffer += sub_block_size;
|
||||
sub_block_size = *buffer++;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else if (block_type == IMAGE_DESCRIPTOR) {
|
||||
// Procesar el Image Descriptor y retornar los datos de imagen
|
||||
return processImageDescriptor(buffer, global_color_table, color_resolution_bits);
|
||||
} else {
|
||||
std::cerr << "Unrecognized block type " << std::hex << static_cast<int>(block_type) << '\n';
|
||||
return std::vector<uint8_t>{};
|
||||
}
|
||||
block_type = *buffer++;
|
||||
}
|
||||
|
||||
return std::vector<uint8_t>{};
|
||||
}
|
||||
|
||||
auto Gif::loadGif(const uint8_t* buffer, uint16_t& w, uint16_t& h) -> std::vector<uint8_t> {
|
||||
return processGifStream(buffer, w, h);
|
||||
}
|
||||
|
||||
} // namespace GIF
|
||||
92
source/core/rendering/gif.hpp
Normal file
92
source/core/rendering/gif.hpp
Normal file
@@ -0,0 +1,92 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint> // Para uint8_t, uint16_t, uint32_t
|
||||
#include <vector> // Para vector
|
||||
|
||||
namespace GIF {
|
||||
|
||||
// Constantes definidas con constexpr, en lugar de macros
|
||||
constexpr uint8_t EXTENSION_INTRODUCER = 0x21;
|
||||
constexpr uint8_t IMAGE_DESCRIPTOR = 0x2C;
|
||||
constexpr uint8_t TRAILER = 0x3B;
|
||||
constexpr uint8_t GRAPHIC_CONTROL = 0xF9;
|
||||
constexpr uint8_t APPLICATION_EXTENSION = 0xFF;
|
||||
constexpr uint8_t COMMENT_EXTENSION = 0xFE;
|
||||
constexpr uint8_t PLAINTEXT_EXTENSION = 0x01;
|
||||
|
||||
#pragma pack(push, 1)
|
||||
struct ScreenDescriptor {
|
||||
uint16_t width;
|
||||
uint16_t height;
|
||||
uint8_t fields;
|
||||
uint8_t background_color_index;
|
||||
uint8_t pixel_aspect_ratio;
|
||||
};
|
||||
|
||||
struct RGB {
|
||||
uint8_t r, g, b;
|
||||
};
|
||||
|
||||
struct ImageDescriptor {
|
||||
uint16_t image_left_position;
|
||||
uint16_t image_top_position;
|
||||
uint16_t image_width;
|
||||
uint16_t image_height;
|
||||
uint8_t fields;
|
||||
};
|
||||
#pragma pack(pop)
|
||||
|
||||
struct DictionaryEntry {
|
||||
uint8_t byte;
|
||||
int prev;
|
||||
int len;
|
||||
};
|
||||
|
||||
struct Extension {
|
||||
uint8_t extension_code;
|
||||
uint8_t block_size;
|
||||
};
|
||||
|
||||
struct GraphicControlExtension {
|
||||
uint8_t fields;
|
||||
uint16_t delay_time;
|
||||
uint8_t transparent_color_index;
|
||||
};
|
||||
|
||||
struct ApplicationExtension {
|
||||
uint8_t application_id[8];
|
||||
uint8_t version[3];
|
||||
};
|
||||
|
||||
struct PlaintextExtension {
|
||||
uint16_t left, top, width, height;
|
||||
uint8_t cell_width, cell_height;
|
||||
uint8_t foreground_color, background_color;
|
||||
};
|
||||
|
||||
class Gif {
|
||||
public:
|
||||
// Descompone (uncompress) el bloque comprimido usando LZW.
|
||||
// Este método puede lanzar std::runtime_error en caso de error.
|
||||
static void decompress(int code_length, const uint8_t* input, int input_length, uint8_t* out);
|
||||
|
||||
// Carga la paleta (global color table) a partir de un buffer,
|
||||
// retornándola en un vector de uint32_t (cada color se compone de R, G, B).
|
||||
static auto loadPalette(const uint8_t* buffer) -> std::vector<uint32_t>;
|
||||
|
||||
// Carga el stream GIF; devuelve un vector con los datos de imagen sin comprimir y
|
||||
// asigna el ancho y alto mediante referencias.
|
||||
static auto loadGif(const uint8_t* buffer, uint16_t& w, uint16_t& h) -> std::vector<uint8_t>;
|
||||
|
||||
private:
|
||||
// Lee los sub-bloques de datos y los acumula en un std::vector<uint8_t>.
|
||||
static auto readSubBlocks(const uint8_t*& buffer) -> std::vector<uint8_t>;
|
||||
|
||||
// Procesa el Image Descriptor y retorna el vector de datos sin comprimir.
|
||||
static auto processImageDescriptor(const uint8_t*& buffer, const std::vector<RGB>& gct, int resolution_bits) -> std::vector<uint8_t>;
|
||||
|
||||
// Procesa el stream completo del GIF y devuelve los datos sin comprimir.
|
||||
static auto processGifStream(const uint8_t* buffer, uint16_t& w, uint16_t& h) -> std::vector<uint8_t>;
|
||||
};
|
||||
|
||||
} // namespace GIF
|
||||
276
source/core/rendering/palette_manager.cpp
Normal file
276
source/core/rendering/palette_manager.cpp
Normal file
@@ -0,0 +1,276 @@
|
||||
#include "core/rendering/palette_manager.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <cmath>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "core/rendering/surface.hpp"
|
||||
#include "core/resources/resource_cache.hpp"
|
||||
#include "game/defaults.hpp"
|
||||
#include "game/options.hpp"
|
||||
#include "utils/utils.hpp"
|
||||
|
||||
// ── Conversión string ↔ PaletteSortMode ──────────────────────────────────────
|
||||
|
||||
auto sortModeFromString(const std::string& str) -> PaletteSortMode {
|
||||
const std::string LOWER = toLower(str);
|
||||
if (LOWER == "luminance") { return PaletteSortMode::LUMINANCE; }
|
||||
if (LOWER == "spectrum") { return PaletteSortMode::SPECTRUM; }
|
||||
return PaletteSortMode::ORIGINAL;
|
||||
}
|
||||
|
||||
auto sortModeToString(PaletteSortMode mode) -> std::string {
|
||||
switch (mode) {
|
||||
case PaletteSortMode::LUMINANCE:
|
||||
return "luminance";
|
||||
case PaletteSortMode::SPECTRUM:
|
||||
return "spectrum";
|
||||
default:
|
||||
return "original";
|
||||
}
|
||||
}
|
||||
|
||||
// ── Paleta de referencia ZX Spectrum (16 colores ARGB) ───────────────────────
|
||||
|
||||
namespace {
|
||||
// Helpers para extraer componentes RGB de un color ARGB (0xAARRGGBB)
|
||||
constexpr auto redOf(Uint32 c) -> int { return static_cast<int>((c >> 16) & 0xFF); }
|
||||
constexpr auto greenOf(Uint32 c) -> int { return static_cast<int>((c >> 8) & 0xFF); }
|
||||
constexpr auto blueOf(Uint32 c) -> int { return static_cast<int>(c & 0xFF); }
|
||||
|
||||
constexpr auto makeARGB(int r, int g, int b) -> Uint32 {
|
||||
return (0xFFU << 24) | (static_cast<Uint32>(r) << 16) | (static_cast<Uint32>(g) << 8) | static_cast<Uint32>(b);
|
||||
}
|
||||
|
||||
// Paleta ZX Spectrum de referencia (misma que en tools/sort_palette/sort_palette.py)
|
||||
constexpr std::array<Uint32, 16> SPECTRUM_REFERENCE = {
|
||||
makeARGB(0, 0, 0),
|
||||
makeARGB(0, 0, 0),
|
||||
makeARGB(0, 0, 216),
|
||||
makeARGB(0, 0, 255),
|
||||
makeARGB(216, 0, 0),
|
||||
makeARGB(255, 0, 0),
|
||||
makeARGB(216, 0, 216),
|
||||
makeARGB(255, 0, 255),
|
||||
makeARGB(0, 216, 0),
|
||||
makeARGB(0, 255, 0),
|
||||
makeARGB(0, 216, 216),
|
||||
makeARGB(0, 255, 255),
|
||||
makeARGB(216, 216, 0),
|
||||
makeARGB(255, 255, 0),
|
||||
makeARGB(216, 216, 216),
|
||||
makeARGB(255, 255, 255),
|
||||
};
|
||||
|
||||
// Luminancia percibida (ITU-R BT.709)
|
||||
auto luminance(Uint32 color) -> double {
|
||||
return (0.2126 * redOf(color)) + (0.7152 * greenOf(color)) + (0.0722 * blueOf(color));
|
||||
}
|
||||
|
||||
// Distancia euclídea al cuadrado en espacio RGB (no necesita sqrt para comparar)
|
||||
auto rgbDistanceSq(Uint32 a, Uint32 b) -> int {
|
||||
const int DR = redOf(a) - redOf(b);
|
||||
const int DG = greenOf(a) - greenOf(b);
|
||||
const int DB = blueOf(a) - blueOf(b);
|
||||
return (DR * DR) + (DG * DG) + (DB * DB);
|
||||
}
|
||||
|
||||
// Cuenta los colores activos en la paleta (los que tienen alpha != 0)
|
||||
auto countActiveColors(const Palette& palette) -> size_t {
|
||||
size_t count = 0;
|
||||
for (const auto& c : palette) {
|
||||
if (c == 0) { break; }
|
||||
++count;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
// Ordenar por luminancia
|
||||
auto sortByLuminance(const Palette& palette) -> Palette {
|
||||
const size_t N = countActiveColors(palette);
|
||||
std::vector<Uint32> colors(palette.begin(), palette.begin() + static_cast<ptrdiff_t>(N));
|
||||
std::ranges::sort(colors, [](Uint32 a, Uint32 b) {
|
||||
return luminance(a) < luminance(b);
|
||||
});
|
||||
|
||||
Palette result{};
|
||||
result.fill(0);
|
||||
std::ranges::copy(colors, result.begin());
|
||||
return result;
|
||||
}
|
||||
|
||||
// Ordenar por similitud con la paleta ZX Spectrum (greedy matching)
|
||||
auto sortBySpectrum(const Palette& palette) -> Palette {
|
||||
const size_t N = countActiveColors(palette);
|
||||
std::vector<Uint32> available(palette.begin(), palette.begin() + static_cast<ptrdiff_t>(N));
|
||||
std::vector<Uint32> result;
|
||||
result.reserve(N);
|
||||
|
||||
// Para cada color de referencia del Spectrum, buscar el más cercano disponible
|
||||
const size_t REFS = std::min(N, SPECTRUM_REFERENCE.size());
|
||||
for (size_t i = 0; i < REFS && !available.empty(); ++i) {
|
||||
const Uint32 REF = SPECTRUM_REFERENCE[i];
|
||||
auto best = std::ranges::min_element(available, [REF](Uint32 a, Uint32 b) {
|
||||
return rgbDistanceSq(a, REF) < rgbDistanceSq(b, REF);
|
||||
});
|
||||
result.push_back(*best);
|
||||
available.erase(best);
|
||||
}
|
||||
|
||||
// Si quedan colores sin asignar, añadirlos al final
|
||||
for (const auto& c : available) {
|
||||
result.push_back(c);
|
||||
}
|
||||
|
||||
Palette out{};
|
||||
out.fill(0);
|
||||
std::ranges::copy(result, out.begin());
|
||||
return out;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
// ── PaletteManager ───────────────────────────────────────────────────────────
|
||||
|
||||
PaletteManager::PaletteManager(
|
||||
std::vector<std::string> raw_paths,
|
||||
const std::string& initial_name,
|
||||
PaletteSortMode initial_sort_mode,
|
||||
std::shared_ptr<Surface> game_surface,
|
||||
std::shared_ptr<Surface> border_surface,
|
||||
OnChangeCallback on_change)
|
||||
: palettes_(std::move(raw_paths)),
|
||||
sort_mode_(initial_sort_mode),
|
||||
game_surface_(std::move(game_surface)),
|
||||
border_surface_(std::move(border_surface)),
|
||||
on_change_(std::move(on_change)) {
|
||||
current_ = findIndex(initial_name);
|
||||
|
||||
// Leer y aplicar paleta inicial directamente desde el archivo
|
||||
// (Resource::Cache aún no está disponible en este punto del ciclo de vida)
|
||||
const auto INITIAL_PALETTE = sortPalette(readPalFile(palettes_.at(current_)), sort_mode_);
|
||||
game_surface_->setPalette(INITIAL_PALETTE);
|
||||
border_surface_->setPalette(INITIAL_PALETTE);
|
||||
|
||||
// Procesar la lista: conservar solo los nombres de archivo (sin ruta)
|
||||
processPathList();
|
||||
}
|
||||
|
||||
void PaletteManager::next() {
|
||||
if (++current_ == palettes_.size()) {
|
||||
current_ = 0;
|
||||
}
|
||||
apply();
|
||||
}
|
||||
|
||||
void PaletteManager::previous() {
|
||||
current_ = (current_ > 0) ? current_ - 1 : palettes_.size() - 1;
|
||||
apply();
|
||||
}
|
||||
|
||||
auto PaletteManager::setByName(const std::string& name) -> bool {
|
||||
const std::string LOWER_NAME = toLower(name + ".pal");
|
||||
for (size_t i = 0; i < palettes_.size(); ++i) {
|
||||
if (toLower(palettes_[i]) == LOWER_NAME) {
|
||||
current_ = i;
|
||||
apply();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
auto PaletteManager::getNames() const -> std::vector<std::string> {
|
||||
std::vector<std::string> names;
|
||||
names.reserve(palettes_.size());
|
||||
for (const auto& p : palettes_) {
|
||||
std::string name = p;
|
||||
const size_t POS = name.find(".pal");
|
||||
if (POS != std::string::npos) { name.erase(POS, 4); }
|
||||
std::ranges::transform(name, name.begin(), ::tolower);
|
||||
names.push_back(std::move(name));
|
||||
}
|
||||
return names;
|
||||
}
|
||||
|
||||
auto PaletteManager::getCurrentName() const -> std::string {
|
||||
std::string name = palettes_.at(current_);
|
||||
const size_t POS = name.find(".pal");
|
||||
if (POS != std::string::npos) { name.erase(POS, 4); }
|
||||
std::ranges::transform(name, name.begin(), ::tolower);
|
||||
return name;
|
||||
}
|
||||
|
||||
auto PaletteManager::getPrettyName() const -> std::string {
|
||||
std::string name = getCurrentName();
|
||||
std::ranges::replace(name, '-', ' ');
|
||||
return name;
|
||||
}
|
||||
|
||||
void PaletteManager::nextSortMode() {
|
||||
sort_mode_ = static_cast<PaletteSortMode>((static_cast<int>(sort_mode_) + 1) % static_cast<int>(PaletteSortMode::COUNT));
|
||||
Options::video.palette_sort = sortModeToString(sort_mode_);
|
||||
apply();
|
||||
}
|
||||
|
||||
void PaletteManager::setSortMode(PaletteSortMode mode) {
|
||||
sort_mode_ = mode;
|
||||
Options::video.palette_sort = sortModeToString(sort_mode_);
|
||||
apply();
|
||||
}
|
||||
|
||||
auto PaletteManager::getSortMode() const -> PaletteSortMode {
|
||||
return sort_mode_;
|
||||
}
|
||||
|
||||
auto PaletteManager::getSortModeName() const -> std::string {
|
||||
return sortModeToString(sort_mode_);
|
||||
}
|
||||
|
||||
void PaletteManager::apply() {
|
||||
Palette raw = Resource::Cache::get()->getPalette(palettes_.at(current_));
|
||||
Palette sorted = sortPalette(raw, sort_mode_);
|
||||
game_surface_->loadPalette(sorted);
|
||||
border_surface_->loadPalette(sorted);
|
||||
|
||||
Options::video.palette = getCurrentName();
|
||||
|
||||
if (on_change_) {
|
||||
on_change_();
|
||||
}
|
||||
}
|
||||
|
||||
auto PaletteManager::findIndex(const std::string& name) const -> size_t {
|
||||
const std::string LOWER_NAME = toLower(name + ".pal");
|
||||
for (size_t i = 0; i < palettes_.size(); ++i) {
|
||||
if (toLower(getFileName(palettes_[i])) == LOWER_NAME) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
// Fallback: buscar la paleta por defecto
|
||||
const std::string DEFAULT_NAME = toLower(std::string(Defaults::Video::PALETTE_NAME) + ".pal");
|
||||
for (size_t i = 0; i < palettes_.size(); ++i) {
|
||||
if (toLower(getFileName(palettes_[i])) == DEFAULT_NAME) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
void PaletteManager::processPathList() {
|
||||
for (auto& palette : palettes_) {
|
||||
palette = getFileName(palette);
|
||||
}
|
||||
}
|
||||
|
||||
auto PaletteManager::sortPalette(const Palette& palette, PaletteSortMode mode) -> Palette {
|
||||
switch (mode) {
|
||||
case PaletteSortMode::LUMINANCE:
|
||||
return sortByLuminance(palette);
|
||||
case PaletteSortMode::SPECTRUM:
|
||||
return sortBySpectrum(palette);
|
||||
default:
|
||||
return palette;
|
||||
}
|
||||
}
|
||||
64
source/core/rendering/palette_manager.hpp
Normal file
64
source/core/rendering/palette_manager.hpp
Normal file
@@ -0,0 +1,64 @@
|
||||
#pragma once
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include <array>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
// Alias de paleta (igual que en surface.hpp; evita incluir todo el header)
|
||||
using Palette = std::array<Uint32, 256>;
|
||||
|
||||
class Surface;
|
||||
|
||||
// Modo de ordenación de paletas
|
||||
enum class PaletteSortMode : int {
|
||||
ORIGINAL = 0, // Paleta tal cual viene del fichero
|
||||
LUMINANCE = 1, // Ordenada por luminancia percibida
|
||||
SPECTRUM = 2, // Reordenada para imitar la paleta ZX Spectrum
|
||||
COUNT = 3
|
||||
};
|
||||
|
||||
// Conversión string ↔ PaletteSortMode
|
||||
auto sortModeFromString(const std::string& str) -> PaletteSortMode;
|
||||
auto sortModeToString(PaletteSortMode mode) -> std::string;
|
||||
|
||||
class PaletteManager {
|
||||
public:
|
||||
using OnChangeCallback = std::function<void()>;
|
||||
|
||||
PaletteManager(
|
||||
std::vector<std::string> raw_paths,
|
||||
const std::string& initial_name,
|
||||
PaletteSortMode initial_sort_mode,
|
||||
std::shared_ptr<Surface> game_surface,
|
||||
std::shared_ptr<Surface> border_surface,
|
||||
OnChangeCallback on_change = nullptr);
|
||||
|
||||
void next(); // Avanza a la siguiente paleta
|
||||
void previous(); // Retrocede a la paleta anterior
|
||||
auto setByName(const std::string& name) -> bool; // Cambia a paleta por nombre; false si no existe
|
||||
[[nodiscard]] auto getNames() const -> std::vector<std::string>; // Nombres disponibles (minúsculas, sin .pal)
|
||||
[[nodiscard]] auto getCurrentName() const -> std::string; // Nombre de la paleta actual (minúsculas, sin .pal)
|
||||
[[nodiscard]] auto getPrettyName() const -> std::string; // Nombre actual con guiones sustituidos por espacios
|
||||
|
||||
void nextSortMode(); // Cicla al siguiente modo de ordenación
|
||||
void setSortMode(PaletteSortMode mode); // Establece un modo de ordenación concreto
|
||||
[[nodiscard]] auto getSortMode() const -> PaletteSortMode; // Devuelve el modo de ordenación actual
|
||||
[[nodiscard]] auto getSortModeName() const -> std::string; // Nombre del modo actual ("ORIGINAL", etc.)
|
||||
|
||||
private:
|
||||
void apply(); // Aplica la paleta actual a ambas surfaces
|
||||
[[nodiscard]] auto findIndex(const std::string& name) const -> size_t; // Localiza paleta por nombre en el vector
|
||||
void processPathList(); // Extrae nombres de archivo de las rutas completas
|
||||
static auto sortPalette(const Palette& palette, PaletteSortMode mode) -> Palette; // Reordena una paleta según el modo
|
||||
|
||||
std::vector<std::string> palettes_;
|
||||
size_t current_{0};
|
||||
PaletteSortMode sort_mode_{PaletteSortMode::ORIGINAL};
|
||||
std::shared_ptr<Surface> game_surface_;
|
||||
std::shared_ptr<Surface> border_surface_;
|
||||
OnChangeCallback on_change_;
|
||||
};
|
||||
107
source/core/rendering/pixel_reveal.cpp
Normal file
107
source/core/rendering/pixel_reveal.cpp
Normal file
@@ -0,0 +1,107 @@
|
||||
#include "core/rendering/pixel_reveal.hpp"
|
||||
|
||||
#include <algorithm> // Para min, ranges::all_of
|
||||
#include <numeric> // Para iota
|
||||
#include <queue> // Para queue (BFS en modo ORDERED)
|
||||
#include <random> // Para mt19937, shuffle
|
||||
|
||||
#include "core/rendering/surface.hpp" // Para Surface
|
||||
#include "utils/utils.hpp" // Para PaletteColor
|
||||
|
||||
// Constructor
|
||||
PixelReveal::PixelReveal(int width, int height, float pixels_per_second, float step_duration, int num_steps, bool reverse, RevealMode mode)
|
||||
: cover_surface_(std::make_shared<Surface>(width, height)),
|
||||
reveal_order_(height),
|
||||
row_step_(height, 0),
|
||||
width_(width),
|
||||
height_(height),
|
||||
pixels_per_second_(pixels_per_second),
|
||||
step_duration_(step_duration),
|
||||
num_steps_(num_steps),
|
||||
reverse_(reverse),
|
||||
mode_(mode) {
|
||||
// En modo normal: empieza negro sólido (se irá revelando a transparente)
|
||||
// En modo inverso: empieza transparente (se irá cubriendo de negro)
|
||||
const auto INITIAL_COLOR = reverse_ ? static_cast<Uint8>(PaletteColor::TRANSPARENT) : static_cast<Uint8>(PaletteColor::BLACK);
|
||||
cover_surface_->clear(INITIAL_COLOR);
|
||||
|
||||
if (mode_ == RevealMode::ORDERED) {
|
||||
// Calcula offsets por bisección BFS: 0, N/2, N/4, 3N/4, ...
|
||||
std::vector<int> offsets;
|
||||
offsets.push_back(0);
|
||||
std::queue<std::pair<int, int>> bq;
|
||||
bq.emplace(0, num_steps_);
|
||||
while (static_cast<int>(offsets.size()) < num_steps_) {
|
||||
auto [lo, hi] = bq.front();
|
||||
bq.pop();
|
||||
if (hi - lo <= 1) {
|
||||
continue;
|
||||
}
|
||||
const int MID = (lo + hi) / 2;
|
||||
offsets.push_back(MID);
|
||||
bq.emplace(lo, MID);
|
||||
bq.emplace(MID, hi);
|
||||
}
|
||||
// Genera el orden: para cada offset, todas las columnas col = offset, offset+N, offset+2N, ...
|
||||
std::vector<int> ordered_cols;
|
||||
ordered_cols.reserve(width_);
|
||||
for (const int OFF : offsets) {
|
||||
for (int col = OFF; col < width_; col += num_steps_) {
|
||||
ordered_cols.push_back(col);
|
||||
}
|
||||
}
|
||||
// Todas las filas usan el mismo orden (sin aleatoriedad)
|
||||
for (int r = 0; r < height_; r++) {
|
||||
reveal_order_[r] = ordered_cols;
|
||||
}
|
||||
} else {
|
||||
// Modo RANDOM: orden aleatorio por fila usando la fila como semilla (reproducible)
|
||||
for (int r = 0; r < height_; r++) {
|
||||
reveal_order_[r].resize(width_);
|
||||
std::iota(reveal_order_[r].begin(), reveal_order_[r].end(), 0);
|
||||
std::mt19937 rng(static_cast<unsigned int>(r));
|
||||
std::shuffle(reveal_order_[r].begin(), reveal_order_[r].end(), rng);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Actualiza el estado del revelado
|
||||
void PixelReveal::update(float time_active) { // NOLINT(readability-make-member-function-const)
|
||||
// En modo normal revela (pone transparente); en modo inverso cubre (pone negro)
|
||||
const auto PIXEL_COLOR = reverse_ ? static_cast<Uint8>(PaletteColor::BLACK) : static_cast<Uint8>(PaletteColor::TRANSPARENT);
|
||||
|
||||
for (int r = 0; r < height_; r++) {
|
||||
const float T_START = static_cast<float>(r) / pixels_per_second_;
|
||||
const float TIME_IN_ROW = time_active - T_START;
|
||||
|
||||
if (TIME_IN_ROW < 0.0F) {
|
||||
continue; // Esta fila aún no ha empezado
|
||||
}
|
||||
|
||||
const int STEPS = std::min(num_steps_, static_cast<int>(TIME_IN_ROW / step_duration_));
|
||||
|
||||
if (STEPS > row_step_[r]) {
|
||||
// Procesa los píxeles de los pasos pendientes
|
||||
for (int step = row_step_[r]; step < STEPS; step++) {
|
||||
const int START_IDX = step * width_ / num_steps_;
|
||||
const int END_IDX = (step == num_steps_ - 1) ? width_ : (step + 1) * width_ / num_steps_;
|
||||
|
||||
for (int idx = START_IDX; idx < END_IDX; idx++) {
|
||||
const int COL = reveal_order_[r][idx];
|
||||
cover_surface_->putPixel(COL, r, PIXEL_COLOR);
|
||||
}
|
||||
}
|
||||
row_step_[r] = STEPS;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Dibuja la máscara en la posición indicada
|
||||
void PixelReveal::render(int dst_x, int dst_y) const {
|
||||
cover_surface_->render(dst_x, dst_y);
|
||||
}
|
||||
|
||||
// Indica si el revelado ha completado todas las filas
|
||||
auto PixelReveal::isComplete() const -> bool {
|
||||
return std::ranges::all_of(row_step_, [this](int s) -> bool { return s >= num_steps_; });
|
||||
}
|
||||
41
source/core/rendering/pixel_reveal.hpp
Normal file
41
source/core/rendering/pixel_reveal.hpp
Normal file
@@ -0,0 +1,41 @@
|
||||
#pragma once
|
||||
|
||||
#include <memory> // Para shared_ptr
|
||||
#include <vector> // Para vector
|
||||
|
||||
class Surface;
|
||||
|
||||
// Efecto de revelado pixel a pixel por filas, de arriba a abajo.
|
||||
// Cada fila se revela en num_steps pasos, con píxeles en orden aleatorio u ordenado (bisección).
|
||||
class PixelReveal {
|
||||
public:
|
||||
// Modo de revelado: aleatorio por fila o en orden de bisección (dithering ordenado 1D)
|
||||
enum class RevealMode { RANDOM,
|
||||
ORDERED };
|
||||
|
||||
// Constructor
|
||||
PixelReveal(int width, int height, float pixels_per_second, float step_duration, int num_steps = 4, bool reverse = false, RevealMode mode = RevealMode::RANDOM);
|
||||
|
||||
~PixelReveal() = default;
|
||||
|
||||
// Actualiza el estado del revelado según el tiempo transcurrido
|
||||
void update(float time_active);
|
||||
|
||||
// Dibuja la máscara de revelado en la posición indicada
|
||||
void render(int dst_x, int dst_y) const;
|
||||
|
||||
// Indica si el revelado ha completado todas las filas
|
||||
[[nodiscard]] auto isComplete() const -> bool;
|
||||
|
||||
private:
|
||||
std::shared_ptr<Surface> cover_surface_; // Máscara negra que se va haciendo transparente
|
||||
std::vector<std::vector<int>> reveal_order_; // Orden de columnas por fila (aleatorio u ordenado por bisección)
|
||||
std::vector<int> row_step_; // Paso actual de revelado por fila (0..num_steps_)
|
||||
int width_;
|
||||
int height_;
|
||||
float pixels_per_second_; // Filas reveladas por segundo
|
||||
float step_duration_; // Segundos por paso dentro de una fila
|
||||
int num_steps_; // Número de pasos de revelado por fila
|
||||
bool reverse_; // Si true: transparente → negro (ocultar); si false: negro → transparente (revelar)
|
||||
RevealMode mode_; // Modo de revelado: aleatorio u ordenado por bisección
|
||||
};
|
||||
159
source/core/rendering/render_info.cpp
Normal file
159
source/core/rendering/render_info.cpp
Normal file
@@ -0,0 +1,159 @@
|
||||
#include "core/rendering/render_info.hpp"
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include <algorithm> // Para transform
|
||||
#include <cmath> // Para round, floor
|
||||
#include <iomanip> // Para setprecision
|
||||
#include <sstream> // Para ostringstream
|
||||
#include <string> // Para string
|
||||
|
||||
#include "core/rendering/screen.hpp" // Para Screen
|
||||
#include "core/rendering/surface.hpp" // Para Surface
|
||||
#include "core/rendering/text.hpp" // Para Text
|
||||
#include "game/options.hpp" // Para Options
|
||||
#include "game/ui/console.hpp" // Para Console
|
||||
#include "game/ui/notifier.hpp" // Para Notifier
|
||||
#include "utils/utils.hpp" // Para prettyName
|
||||
|
||||
// [SINGLETON]
|
||||
RenderInfo* RenderInfo::render_info = nullptr;
|
||||
|
||||
// [SINGLETON] Crearemos el objeto con esta función estática
|
||||
void RenderInfo::init() {
|
||||
RenderInfo::render_info = new RenderInfo();
|
||||
}
|
||||
|
||||
// [SINGLETON] Destruiremos el objeto con esta función estática
|
||||
void RenderInfo::destroy() {
|
||||
delete RenderInfo::render_info;
|
||||
RenderInfo::render_info = nullptr;
|
||||
}
|
||||
|
||||
// [SINGLETON] Con este método obtenemos el objeto y podemos trabajar con él
|
||||
auto RenderInfo::get() -> RenderInfo* {
|
||||
return RenderInfo::render_info;
|
||||
}
|
||||
|
||||
// Constructor: en DEBUG se activa inmediatamente (notifica a Notifier del offset)
|
||||
RenderInfo::RenderInfo() {
|
||||
#ifdef _DEBUG
|
||||
toggle();
|
||||
#endif
|
||||
}
|
||||
|
||||
// Actualiza la animación de entrada/salida del overlay
|
||||
void RenderInfo::update(float delta_time) {
|
||||
switch (status_) {
|
||||
case Status::RISING:
|
||||
y_ += SLIDE_SPEED * delta_time;
|
||||
if (y_ >= 0.0F) {
|
||||
y_ = 0.0F;
|
||||
status_ = Status::ACTIVE;
|
||||
}
|
||||
break;
|
||||
case Status::VANISHING:
|
||||
y_ -= SLIDE_SPEED * delta_time;
|
||||
if (y_ <= static_cast<float>(-HEIGHT)) {
|
||||
y_ = static_cast<float>(-HEIGHT);
|
||||
status_ = Status::HIDDEN;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Renderiza el overlay de información por pantalla
|
||||
void RenderInfo::render() const {
|
||||
if (status_ == Status::HIDDEN) { return; }
|
||||
|
||||
// FPS
|
||||
std::string line = std::to_string(Screen::get()->getLastFPS()) + " fps";
|
||||
|
||||
// Driver GPU
|
||||
const auto& driver = Screen::get()->getGPUDriver();
|
||||
line += " | " + (driver.empty() ? std::string("sdl") : driver);
|
||||
|
||||
// Zoom calculado (alto físico / alto lógico), con coma decimal y sin ceros innecesarios
|
||||
const float ROUNDED = std::round(Screen::get()->getZoomFactor() * 100.0F) / 100.0F;
|
||||
std::string zoom_str;
|
||||
if (ROUNDED == std::floor(ROUNDED)) {
|
||||
zoom_str = std::to_string(static_cast<int>(ROUNDED));
|
||||
} else {
|
||||
std::ostringstream oss;
|
||||
oss << std::fixed << std::setprecision(2) << ROUNDED;
|
||||
zoom_str = oss.str();
|
||||
if (zoom_str.back() == '0') { zoom_str.pop_back(); }
|
||||
std::ranges::replace(zoom_str, '.', ',');
|
||||
}
|
||||
line += " | " + zoom_str + "x";
|
||||
|
||||
// PostFX: muestra shader + preset y supersampling, o nada si está desactivado
|
||||
if (Options::video.shader.enabled) {
|
||||
const bool IS_CRTPI = (Options::video.shader.current_shader == Rendering::ShaderType::CRTPI);
|
||||
const std::string SHADER_NAME = IS_CRTPI ? "crtpi" : "postfx";
|
||||
std::string preset_name = "-";
|
||||
if (IS_CRTPI) {
|
||||
if (!Options::crtpi_presets.empty()) {
|
||||
preset_name = prettyName(Options::crtpi_presets[static_cast<size_t>(Options::video.shader.current_crtpi_preset)].name);
|
||||
}
|
||||
} else {
|
||||
if (!Options::postfx_presets.empty()) {
|
||||
preset_name = prettyName(Options::postfx_presets[static_cast<size_t>(Options::video.shader.current_postfx_preset)].name);
|
||||
}
|
||||
}
|
||||
const bool SHOW_SS = Options::video.supersampling.enabled && !IS_CRTPI;
|
||||
line += " | " + SHADER_NAME + " " + preset_name + (SHOW_SS ? " (ss)" : "");
|
||||
}
|
||||
|
||||
// Todo en lowercase
|
||||
std::ranges::transform(line, line.begin(), [](unsigned char c) { return std::tolower(c); });
|
||||
|
||||
// Constantes visuales (igual que Console)
|
||||
static constexpr Uint8 BG_COLOR = 0; // PaletteColor::BLACK
|
||||
static constexpr Uint8 MSG_COLOR = 9; // PaletteColor::BRIGHT_GREEN
|
||||
static constexpr int TEXT_SIZE = 6;
|
||||
static constexpr int PADDING_V = (TEXT_SIZE / 2) - 1;
|
||||
|
||||
// Fuente: preferir la de la consola si está disponible
|
||||
auto text_obj = (Console::get() != nullptr) ? Console::get()->getText() : Screen::get()->getText();
|
||||
|
||||
// Posición Y: debajo de la consola + offset animado propio
|
||||
const int CONSOLE_Y = (Console::get() != nullptr) ? Console::get()->getVisibleHeight() : 0;
|
||||
const int Y = CONSOLE_Y + static_cast<int>(y_);
|
||||
|
||||
// Rectángulo de fondo: ancho completo, alto ajustado al texto
|
||||
const SDL_FRect RECT = {
|
||||
.x = 0.0F,
|
||||
.y = static_cast<float>(Y),
|
||||
.w = Options::game.width,
|
||||
.h = static_cast<float>(TEXT_SIZE + (PADDING_V * 2))};
|
||||
|
||||
auto game_surface = Screen::get()->getGameSurface();
|
||||
game_surface->fillRect(&RECT, BG_COLOR);
|
||||
// game_surface->drawRectBorder(&RECT, BORDER_COLOR);
|
||||
text_obj->writeDX(Text::CENTER_FLAG | Text::COLOR_FLAG,
|
||||
static_cast<int>(Options::game.width / 2),
|
||||
Y + PADDING_V,
|
||||
line,
|
||||
1,
|
||||
MSG_COLOR);
|
||||
}
|
||||
|
||||
// Activa o desactiva el overlay y notifica a Notifier del cambio de offset
|
||||
void RenderInfo::toggle() {
|
||||
switch (status_) {
|
||||
case Status::HIDDEN:
|
||||
status_ = Status::RISING;
|
||||
Screen::get()->updateZoomFactor();
|
||||
if (Notifier::get() != nullptr) { Notifier::get()->addYOffset(HEIGHT); }
|
||||
break;
|
||||
case Status::ACTIVE:
|
||||
status_ = Status::VANISHING;
|
||||
if (Notifier::get() != nullptr) { Notifier::get()->removeYOffset(HEIGHT); }
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
37
source/core/rendering/render_info.hpp
Normal file
37
source/core/rendering/render_info.hpp
Normal file
@@ -0,0 +1,37 @@
|
||||
#pragma once
|
||||
|
||||
class RenderInfo {
|
||||
public:
|
||||
// Singleton
|
||||
static void init();
|
||||
static void destroy();
|
||||
static auto get() -> RenderInfo*;
|
||||
|
||||
// Métodos principales
|
||||
void update(float delta_time);
|
||||
void render() const;
|
||||
void toggle();
|
||||
|
||||
// Consultas
|
||||
[[nodiscard]] auto isActive() const -> bool { return status_ != Status::HIDDEN; }
|
||||
|
||||
// Altura fija del overlay (TEXT_SIZE(6) + PADDING_V(2) * 2)
|
||||
static constexpr int HEIGHT = 10;
|
||||
static constexpr float SLIDE_SPEED = 120.0F;
|
||||
|
||||
private:
|
||||
enum class Status { HIDDEN,
|
||||
RISING,
|
||||
ACTIVE,
|
||||
VANISHING };
|
||||
|
||||
// Singleton
|
||||
static RenderInfo* render_info;
|
||||
|
||||
// Constructor y destructor privados [SINGLETON]
|
||||
RenderInfo();
|
||||
~RenderInfo() = default;
|
||||
|
||||
Status status_{Status::HIDDEN};
|
||||
float y_{static_cast<float>(-HEIGHT)};
|
||||
};
|
||||
750
source/core/rendering/screen.cpp
Normal file
750
source/core/rendering/screen.cpp
Normal file
@@ -0,0 +1,750 @@
|
||||
#include "core/rendering/screen.hpp"
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include <algorithm> // Para max, min, transform
|
||||
#include <cctype> // Para toupper
|
||||
#include <cmath> // Para round, floor
|
||||
#include <cstring> // Para memcpy
|
||||
#include <fstream> // Para basic_ostream, operator<<, endl, basic_...
|
||||
#include <iostream> // Para cerr
|
||||
#include <iterator> // Para istreambuf_iterator, operator==
|
||||
#include <string> // Para char_traits, string, operator+, operator==
|
||||
|
||||
#include "core/input/mouse.hpp" // Para updateCursorVisibility
|
||||
#include "core/rendering/render_info.hpp" // Para RenderInfo
|
||||
#include "core/rendering/sdl3gpu/sdl3gpu_shader.hpp" // Para SDL3GPUShader
|
||||
#include "core/rendering/surface.hpp" // Para Surface, readPalFile
|
||||
#include "core/rendering/text.hpp" // Para Text
|
||||
#include "core/resources/resource_cache.hpp" // Para Resource
|
||||
#include "core/resources/resource_helper.hpp" // Para ResourceHelper
|
||||
#include "core/resources/resource_list.hpp" // Para Asset, AssetType
|
||||
#include "game/options.hpp" // Para Options, options, OptionsVideo, Border
|
||||
#include "game/ui/console.hpp" // Para Console
|
||||
#include "game/ui/notifier.hpp" // Para Notifier
|
||||
|
||||
// [SINGLETON]
|
||||
Screen* Screen::screen = nullptr;
|
||||
|
||||
// [SINGLETON] Crearemos el objeto con esta función estática
|
||||
void Screen::init() {
|
||||
Screen::screen = new Screen();
|
||||
}
|
||||
|
||||
// [SINGLETON] Destruiremos el objeto con esta función estática
|
||||
void Screen::destroy() {
|
||||
delete Screen::screen;
|
||||
}
|
||||
|
||||
// [SINGLETON] Con este método obtenemos el objeto y podemos trabajar con él
|
||||
auto Screen::get() -> Screen* {
|
||||
return Screen::screen;
|
||||
}
|
||||
|
||||
// Constructor
|
||||
Screen::Screen() {
|
||||
// Arranca SDL VIDEO, crea la ventana y el renderizador
|
||||
initSDLVideo();
|
||||
if (Options::video.fullscreen) { SDL_HideCursor(); }
|
||||
|
||||
// Calcular tamaños y hacer .resize() de los buffers de píxeles
|
||||
adjustWindowSize();
|
||||
adjustRenderLogicalSize();
|
||||
updateZoomFactor();
|
||||
|
||||
// Ajusta los tamaños
|
||||
game_surface_dstrect_ = {.x = Options::video.border.width, .y = Options::video.border.height, .w = Options::game.width, .h = Options::game.height};
|
||||
|
||||
// Define el color del borde para el modo de pantalla completa
|
||||
border_color_ = static_cast<Uint8>(PaletteColor::BLACK);
|
||||
|
||||
// Crea la textura donde se dibujan los graficos del juego
|
||||
game_texture_ = SDL_CreateTexture(renderer_, SDL_PIXELFORMAT_ARGB8888, SDL_TEXTUREACCESS_STREAMING, Options::game.width, Options::game.height);
|
||||
if (game_texture_ == nullptr) {
|
||||
// Registrar el error si está habilitado
|
||||
std::cerr << "Error: game_texture_ could not be created!\nSDL Error: " << SDL_GetError() << '\n';
|
||||
}
|
||||
SDL_SetTextureScaleMode(game_texture_, SDL_SCALEMODE_NEAREST);
|
||||
|
||||
// Crea la textura donde se dibuja el borde que rodea el area de juego
|
||||
border_texture_ = SDL_CreateTexture(renderer_, SDL_PIXELFORMAT_ARGB8888, SDL_TEXTUREACCESS_STREAMING, Options::game.width + (Options::video.border.width * 2), Options::game.height + (Options::video.border.height * 2));
|
||||
if (border_texture_ == nullptr) {
|
||||
// Registrar el error si está habilitado
|
||||
std::cerr << "Error: border_texture_ could not be created!\nSDL Error: " << SDL_GetError() << '\n';
|
||||
}
|
||||
SDL_SetTextureScaleMode(border_texture_, SDL_SCALEMODE_NEAREST);
|
||||
|
||||
// Crea las surfaces (PaletteManager aplicará la paleta inicial en su constructor)
|
||||
game_surface_ = std::make_shared<Surface>(Options::game.width, Options::game.height);
|
||||
game_surface_->clear(static_cast<Uint8>(PaletteColor::BLACK));
|
||||
|
||||
border_surface_ = std::make_shared<Surface>(Options::game.width + (Options::video.border.width * 2), Options::game.height + (Options::video.border.height * 2));
|
||||
border_surface_->clear(border_color_);
|
||||
|
||||
// Crea el gestor de paletas; aplica la paleta inicial a ambas surfaces
|
||||
palette_manager_ = std::make_unique<PaletteManager>(
|
||||
Resource::List::get()->getListByType(Resource::List::Type::PALETTE),
|
||||
Options::video.palette,
|
||||
sortModeFromString(Options::video.palette_sort),
|
||||
game_surface_,
|
||||
border_surface_,
|
||||
[this]() {
|
||||
// Actualizar caché ARGB del borde cuando cambia la paleta
|
||||
if (border_is_solid_) {
|
||||
border_surface_->toARGBBuffer(border_pixel_buffer_.data());
|
||||
border_argb_color_ = border_pixel_buffer_[0];
|
||||
}
|
||||
});
|
||||
|
||||
// Cachear el color ARGB inicial del borde (borde sólido por defecto)
|
||||
border_surface_->toARGBBuffer(border_pixel_buffer_.data());
|
||||
border_argb_color_ = border_pixel_buffer_[0];
|
||||
|
||||
// Establece la surface que actuará como renderer para recibir las llamadas a render()
|
||||
renderer_surface_ = std::make_shared<std::shared_ptr<Surface>>(game_surface_);
|
||||
|
||||
// Crea el objeto de texto para la pantalla de carga
|
||||
createText();
|
||||
|
||||
// Renderizar una vez la textura vacía para que tenga contenido válido
|
||||
// antes de inicializar los shaders (evita pantalla negra)
|
||||
SDL_RenderTexture(renderer_, game_texture_, nullptr, nullptr);
|
||||
SDL_RenderTexture(renderer_, border_texture_, nullptr, nullptr);
|
||||
|
||||
// Ahora sí inicializar los shaders
|
||||
initShaders();
|
||||
}
|
||||
|
||||
// Destructor
|
||||
Screen::~Screen() {
|
||||
SDL_DestroyTexture(game_texture_);
|
||||
SDL_DestroyTexture(border_texture_);
|
||||
}
|
||||
|
||||
// Limpia el renderer
|
||||
void Screen::clearRenderer(Rgb color) {
|
||||
SDL_SetRenderDrawColor(renderer_, color.r, color.g, color.b, 0xFF);
|
||||
SDL_RenderClear(renderer_);
|
||||
}
|
||||
|
||||
// Prepara para empezar a dibujar en la textura de juego
|
||||
void Screen::start() { setRendererSurface(nullptr); }
|
||||
|
||||
// Vuelca el contenido del renderizador en pantalla
|
||||
void Screen::render() {
|
||||
fps_.increment();
|
||||
|
||||
// Renderiza todos los overlays (escribe en game_surface_ CPU-side)
|
||||
renderOverlays();
|
||||
|
||||
// En el path SDL3GPU, los píxeles se suben directamente desde la Surface.
|
||||
// En el path SDL_Renderer, primero copiamos la surface a la SDL_Texture.
|
||||
if (!(shader_backend_ && shader_backend_->isHardwareAccelerated())) {
|
||||
surfaceToTexture();
|
||||
}
|
||||
|
||||
// Copia la textura al renderizador (o hace el present GPU)
|
||||
textureToRenderer();
|
||||
}
|
||||
|
||||
// Establece el modo de video
|
||||
void Screen::setVideoMode(bool mode) {
|
||||
// Actualiza las opciones
|
||||
Options::video.fullscreen = mode;
|
||||
|
||||
// Configura el modo de pantalla y ajusta la ventana
|
||||
SDL_SetWindowFullscreen(window_, Options::video.fullscreen);
|
||||
SDL_SyncWindow(window_);
|
||||
adjustWindowSize();
|
||||
adjustRenderLogicalSize();
|
||||
updateZoomFactor();
|
||||
}
|
||||
|
||||
// Camibia entre pantalla completa y ventana
|
||||
void Screen::toggleVideoMode() {
|
||||
Options::video.fullscreen = !Options::video.fullscreen;
|
||||
setVideoMode(Options::video.fullscreen);
|
||||
}
|
||||
|
||||
// Reduce el tamaño de la ventana
|
||||
auto Screen::decWindowZoom() -> bool {
|
||||
if (static_cast<int>(Options::video.fullscreen) == 0) {
|
||||
const int PREVIOUS_ZOOM = Options::window.zoom;
|
||||
--Options::window.zoom;
|
||||
Options::window.zoom = std::max(Options::window.zoom, 1);
|
||||
|
||||
if (Options::window.zoom != PREVIOUS_ZOOM) {
|
||||
setVideoMode(Options::video.fullscreen);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Aumenta el tamaño de la ventana
|
||||
auto Screen::incWindowZoom() -> bool {
|
||||
if (static_cast<int>(Options::video.fullscreen) == 0) {
|
||||
const int PREVIOUS_ZOOM = Options::window.zoom;
|
||||
++Options::window.zoom;
|
||||
Options::window.zoom = std::min(Options::window.zoom, Options::window.max_zoom);
|
||||
|
||||
if (Options::window.zoom != PREVIOUS_ZOOM) {
|
||||
setVideoMode(Options::video.fullscreen);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Establece el zoom directamente; false si fuera del rango [1, max_zoom] o en pantalla completa
|
||||
auto Screen::setWindowZoom(int zoom) -> bool {
|
||||
if (Options::video.fullscreen) { return false; }
|
||||
if (zoom < 1 || zoom > Options::window.max_zoom) { return false; }
|
||||
if (zoom == Options::window.zoom) { return false; }
|
||||
Options::window.zoom = zoom;
|
||||
setVideoMode(Options::video.fullscreen);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Devuelve el zoom máximo permitido según la pantalla actual
|
||||
auto Screen::getMaxZoom() -> int {
|
||||
return Options::window.max_zoom;
|
||||
}
|
||||
|
||||
// Cambia el color del borde
|
||||
void Screen::setBorderColor(Uint8 color) {
|
||||
border_color_ = color;
|
||||
border_surface_->clear(border_color_);
|
||||
|
||||
// Actualizar caché ARGB del borde sólido (ocurre una vez por habitación, no cada frame)
|
||||
border_surface_->toARGBBuffer(border_pixel_buffer_.data());
|
||||
border_argb_color_ = border_pixel_buffer_[0];
|
||||
border_is_solid_ = true;
|
||||
}
|
||||
|
||||
// Cambia entre borde visible y no visible
|
||||
void Screen::toggleBorder() {
|
||||
Options::video.border.enabled = !Options::video.border.enabled;
|
||||
setVideoMode(Options::video.fullscreen);
|
||||
initShaders();
|
||||
}
|
||||
|
||||
// Dibuja las notificaciones
|
||||
void Screen::renderNotifications() const {
|
||||
if (notifications_enabled_) {
|
||||
Notifier::get()->render();
|
||||
}
|
||||
}
|
||||
|
||||
// Activa/desactiva todos los shaders respetando el shader actualmente seleccionado
|
||||
void Screen::toggleShaders() {
|
||||
Options::video.shader.enabled = !Options::video.shader.enabled;
|
||||
if (shader_backend_ && shader_backend_->isHardwareAccelerated()) {
|
||||
if (Options::video.shader.enabled) {
|
||||
// Activar: usar el shader actualmente seleccionado
|
||||
if (Options::video.shader.current_shader == Rendering::ShaderType::CRTPI) {
|
||||
shader_backend_->setActiveShader(Rendering::ShaderType::CRTPI);
|
||||
applyCurrentCrtPiPreset();
|
||||
} else {
|
||||
applyCurrentPostFXPreset();
|
||||
}
|
||||
} else {
|
||||
// Desactivar: pass-through con POSTFX (pipeline sin efecto)
|
||||
shader_backend_->setActiveShader(Rendering::ShaderType::POSTFX);
|
||||
shader_backend_->setPostFXParams(Rendering::PostFXParams{});
|
||||
}
|
||||
} else {
|
||||
// Backend no inicializado aún — inicializarlo ahora
|
||||
initShaders();
|
||||
}
|
||||
}
|
||||
|
||||
// Recarga el shader del preset actual sin toggle
|
||||
void Screen::reloadPostFX() {
|
||||
if (Options::video.shader.enabled && shader_backend_ && shader_backend_->isHardwareAccelerated()) {
|
||||
// El backend ya está activo: solo actualizar uniforms, sin recrear el pipeline
|
||||
applyCurrentPostFXPreset();
|
||||
} else if (Options::video.shader.enabled) {
|
||||
initShaders();
|
||||
}
|
||||
}
|
||||
|
||||
// Recarga el shader CrtPi del preset actual sin toggle
|
||||
void Screen::reloadCrtPi() {
|
||||
if (!shader_backend_) { return; }
|
||||
applyCurrentCrtPiPreset();
|
||||
}
|
||||
|
||||
// Actualiza la lógica de la clase (versión nueva con delta_time para escenas migradas)
|
||||
void Screen::update(float delta_time) {
|
||||
fps_.calculate(SDL_GetTicks());
|
||||
Notifier::get()->update(delta_time);
|
||||
if (Console::get() != nullptr) {
|
||||
Console::get()->update(delta_time);
|
||||
}
|
||||
if (RenderInfo::get() != nullptr) { RenderInfo::get()->update(delta_time); }
|
||||
Mouse::updateCursorVisibility();
|
||||
}
|
||||
|
||||
// Calcula el tamaño de la ventana
|
||||
void Screen::adjustWindowSize() {
|
||||
window_width_ = Options::game.width + (Options::video.border.enabled ? Options::video.border.width * 2 : 0);
|
||||
window_height_ = Options::game.height + (Options::video.border.enabled ? Options::video.border.height * 2 : 0);
|
||||
|
||||
// Reservamos memoria una sola vez.
|
||||
// Si el buffer es más pequeño que la superficie, crash asegurado.
|
||||
border_pixel_buffer_.resize(static_cast<size_t>(window_width_ * window_height_));
|
||||
game_pixel_buffer_.resize(static_cast<size_t>(Options::game.width * Options::game.height));
|
||||
|
||||
// border_pixel_buffer_ es el buffer que se sube a la GPU (tamaño total ventana).
|
||||
if (Options::video.border.enabled) {
|
||||
border_pixel_buffer_.resize(static_cast<size_t>(window_width_ * window_height_));
|
||||
}
|
||||
|
||||
// Lógica de centrado y redimensionado de ventana SDL
|
||||
if (static_cast<int>(Options::video.fullscreen) == 0) {
|
||||
int old_w;
|
||||
int old_h;
|
||||
SDL_GetWindowSize(window_, &old_w, &old_h);
|
||||
int old_x;
|
||||
int old_y;
|
||||
SDL_GetWindowPosition(window_, &old_x, &old_y);
|
||||
|
||||
const int NEW_W = window_width_ * Options::window.zoom;
|
||||
const int NEW_H = window_height_ * Options::window.zoom;
|
||||
const int NEW_X = old_x + ((old_w - NEW_W) / 2);
|
||||
const int NEW_Y = old_y + ((old_h - NEW_H) / 2);
|
||||
|
||||
SDL_SetWindowSize(window_, NEW_W, NEW_H);
|
||||
|
||||
// En Wayland, SDL_SetWindowPosition es ignorado por el compositor (limitación de
|
||||
// protocolo: el compositor controla la posición de ventanas toplevel). Solo se
|
||||
// aplica en X11/Windows/macOS donde el posicionado funciona correctamente.
|
||||
// SDL_SyncWindow garantiza que el resize esté completado antes de reposicionar
|
||||
// (evita el race condition en X11).
|
||||
SDL_SyncWindow(window_);
|
||||
const char* driver = SDL_GetCurrentVideoDriver();
|
||||
const bool IS_WAYLAND = (driver != nullptr && SDL_strcmp(driver, "wayland") == 0);
|
||||
if (!IS_WAYLAND) {
|
||||
SDL_SetWindowPosition(window_, std::max(NEW_X, WINDOWS_DECORATIONS), std::max(NEW_Y, 0));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Ajusta el tamaño lógico del renderizador
|
||||
void Screen::adjustRenderLogicalSize() {
|
||||
SDL_SetRenderLogicalPresentation(renderer_, window_width_, window_height_, Options::video.integer_scale ? SDL_LOGICAL_PRESENTATION_INTEGER_SCALE : SDL_LOGICAL_PRESENTATION_LETTERBOX);
|
||||
}
|
||||
|
||||
// Recalcula y almacena el factor de zoom. Llamar solo cuando SDL ya ha estabilizado el estado de la ventana.
|
||||
// En ventana: Options::window.zoom (siempre entero).
|
||||
// En fullscreen: mínimo de las escalas en ambos ejes; floor si integer scale está activo.
|
||||
void Screen::updateZoomFactor() {
|
||||
if (!Options::video.fullscreen) {
|
||||
zoom_factor_ = static_cast<float>(Options::window.zoom);
|
||||
return;
|
||||
}
|
||||
if (window_width_ == 0 || window_height_ == 0) {
|
||||
zoom_factor_ = 1.0F;
|
||||
return;
|
||||
}
|
||||
int pw{0};
|
||||
int ph{0};
|
||||
SDL_GetRenderOutputSize(renderer_, &pw, &ph);
|
||||
const float SCALE = std::min(static_cast<float>(pw) / static_cast<float>(window_width_),
|
||||
static_cast<float>(ph) / static_cast<float>(window_height_));
|
||||
zoom_factor_ = Options::video.integer_scale ? std::floor(SCALE) : SCALE;
|
||||
}
|
||||
|
||||
// Establece el renderizador para las surfaces
|
||||
void Screen::setRendererSurface(const std::shared_ptr<Surface>& surface) {
|
||||
(surface) ? renderer_surface_ = std::make_shared<std::shared_ptr<Surface>>(surface) : renderer_surface_ = std::make_shared<std::shared_ptr<Surface>>(game_surface_);
|
||||
}
|
||||
|
||||
// Cambia la paleta
|
||||
void Screen::nextPalette() { palette_manager_->next(); }
|
||||
|
||||
// Cambia la paleta
|
||||
void Screen::previousPalette() { palette_manager_->previous(); }
|
||||
|
||||
// Copia la surface a la textura
|
||||
void Screen::surfaceToTexture() { // NOLINT(readability-convert-member-functions-to-static)
|
||||
if (Options::video.border.enabled) {
|
||||
border_surface_->copyToTexture(renderer_, border_texture_);
|
||||
game_surface_->copyToTexture(renderer_, border_texture_, nullptr, &game_surface_dstrect_);
|
||||
} else {
|
||||
game_surface_->copyToTexture(renderer_, game_texture_);
|
||||
}
|
||||
}
|
||||
|
||||
// Copia la textura al renderizador (o hace el present GPU)
|
||||
void Screen::textureToRenderer() {
|
||||
if (shader_backend_ && shader_backend_->isHardwareAccelerated()) {
|
||||
const int GAME_W = Options::game.width;
|
||||
const int GAME_H = Options::game.height;
|
||||
|
||||
if (Options::video.border.enabled) {
|
||||
const int BORDER_W = window_width_;
|
||||
const int BORDER_H = window_height_;
|
||||
const int OFF_X = static_cast<int>(game_surface_dstrect_.x);
|
||||
const int OFF_Y = static_cast<int>(game_surface_dstrect_.y);
|
||||
|
||||
if (border_is_solid_) {
|
||||
// Path A: borde sólido (gameplay normal)
|
||||
// Rellena solo el marco con el color cacheado — sin lookups de paleta.
|
||||
// El área central (juego) se deja sin tocar; el overlay la sobreescribe igualmente.
|
||||
|
||||
// Franjas superior e inferior (ancho completo)
|
||||
std::fill_n(border_pixel_buffer_.data(), OFF_Y * BORDER_W, border_argb_color_);
|
||||
std::fill_n(&border_pixel_buffer_[(OFF_Y + GAME_H) * BORDER_W],
|
||||
(BORDER_H - OFF_Y - GAME_H) * BORDER_W,
|
||||
border_argb_color_);
|
||||
// Columnas laterales en las filas del área de juego
|
||||
for (int y = OFF_Y; y < OFF_Y + GAME_H; ++y) {
|
||||
std::fill_n(&border_pixel_buffer_[y * BORDER_W], OFF_X, border_argb_color_);
|
||||
std::fill_n(&border_pixel_buffer_[(y * BORDER_W) + OFF_X + GAME_W],
|
||||
BORDER_W - OFF_X - GAME_W,
|
||||
border_argb_color_);
|
||||
}
|
||||
} else {
|
||||
// Path B: borde dinámico (escena de carga — bandas de colores animadas)
|
||||
// Conversión completa: la escena modifica border_surface_ cada frame
|
||||
border_surface_->toARGBBuffer(border_pixel_buffer_.data());
|
||||
}
|
||||
|
||||
// Overlay del juego sobre el centro del buffer (ambos paths)
|
||||
game_surface_->toARGBBuffer(game_pixel_buffer_.data());
|
||||
for (int y = 0; y < GAME_H; ++y) {
|
||||
const Uint32* src = &game_pixel_buffer_[y * GAME_W];
|
||||
Uint32* dst = &border_pixel_buffer_[((OFF_Y + y) * BORDER_W) + OFF_X];
|
||||
std::memcpy(dst, src, GAME_W * sizeof(Uint32));
|
||||
}
|
||||
|
||||
shader_backend_->uploadPixels(border_pixel_buffer_.data(), BORDER_W, BORDER_H);
|
||||
} else {
|
||||
// Caso sin borde: subida directa simplificada
|
||||
game_surface_->toARGBBuffer(game_pixel_buffer_.data());
|
||||
shader_backend_->uploadPixels(game_pixel_buffer_.data(), GAME_W, GAME_H);
|
||||
}
|
||||
|
||||
shader_backend_->render();
|
||||
} else {
|
||||
// Fallback SDL_Renderer (mantiene tu lógica de texturas SDL)
|
||||
SDL_Texture* tex = Options::video.border.enabled ? border_texture_ : game_texture_;
|
||||
SDL_SetRenderTarget(renderer_, nullptr);
|
||||
SDL_RenderClear(renderer_);
|
||||
SDL_RenderTexture(renderer_, tex, nullptr, nullptr);
|
||||
SDL_RenderPresent(renderer_);
|
||||
}
|
||||
}
|
||||
|
||||
// Renderiza todos los overlays (orden: último dibujado queda encima)
|
||||
void Screen::renderOverlays() {
|
||||
renderNotifications(); // Notifier (abajo)
|
||||
if (RenderInfo::get() != nullptr) { RenderInfo::get()->render(); } // RenderInfo (medio)
|
||||
if (Console::get() != nullptr) { Console::get()->render(); } // Console (encima)
|
||||
}
|
||||
|
||||
// Cambia a una paleta por nombre (case-insensitive); devuelve false si no existe
|
||||
auto Screen::setPaletteByName(const std::string& name) -> bool { return palette_manager_->setByName(name); }
|
||||
|
||||
// Devuelve los nombres de paletas disponibles (minúsculas, sin extensión .pal)
|
||||
auto Screen::getPaletteNames() const -> std::vector<std::string> { return palette_manager_->getNames(); }
|
||||
auto Screen::getPalettePrettyName() const -> std::string { return palette_manager_->getPrettyName(); }
|
||||
void Screen::nextPaletteSortMode() { palette_manager_->nextSortMode(); }
|
||||
void Screen::setPaletteSortMode(PaletteSortMode mode) { palette_manager_->setSortMode(mode); }
|
||||
auto Screen::getPaletteSortModeName() const -> std::string { return palette_manager_->getSortModeName(); }
|
||||
|
||||
// Limpia la game_surface_
|
||||
void Screen::clearSurface(Uint8 index) { game_surface_->clear(index); }
|
||||
|
||||
// Establece el tamaño del borde
|
||||
void Screen::setBorderWidth(int width) { Options::video.border.width = width; }
|
||||
|
||||
// Establece el tamaño del borde
|
||||
void Screen::setBorderHeight(int height) { Options::video.border.height = height; }
|
||||
|
||||
// Establece si se ha de ver el borde en el modo ventana
|
||||
void Screen::setBorderEnabled(bool value) { Options::video.border.enabled = value; }
|
||||
|
||||
// Muestra la ventana
|
||||
void Screen::show() { SDL_ShowWindow(window_); }
|
||||
|
||||
// Oculta la ventana
|
||||
void Screen::hide() { SDL_HideWindow(window_); }
|
||||
|
||||
// Establece la visibilidad de las notificaciones
|
||||
void Screen::setNotificationsEnabled(bool value) { notifications_enabled_ = value; }
|
||||
|
||||
// Alterna entre activar y desactivar el escalado entero
|
||||
void Screen::toggleIntegerScale() {
|
||||
Options::video.integer_scale = !Options::video.integer_scale;
|
||||
SDL_SetRenderLogicalPresentation(renderer_, Options::game.width, Options::game.height, Options::video.integer_scale ? SDL_LOGICAL_PRESENTATION_INTEGER_SCALE : SDL_LOGICAL_PRESENTATION_LETTERBOX);
|
||||
if (shader_backend_) {
|
||||
shader_backend_->setScaleMode(Options::video.integer_scale);
|
||||
}
|
||||
updateZoomFactor();
|
||||
}
|
||||
|
||||
// Alterna entre activar y desactivar el V-Sync
|
||||
void Screen::toggleVSync() {
|
||||
Options::video.vertical_sync = !Options::video.vertical_sync;
|
||||
SDL_SetRenderVSync(renderer_, Options::video.vertical_sync ? 1 : SDL_RENDERER_VSYNC_DISABLED);
|
||||
if (shader_backend_) {
|
||||
shader_backend_->setVSync(Options::video.vertical_sync);
|
||||
}
|
||||
}
|
||||
|
||||
// Getters
|
||||
auto Screen::getRenderer() -> SDL_Renderer* { return renderer_; }
|
||||
auto Screen::getRendererSurface() -> std::shared_ptr<Surface> { return (*renderer_surface_); }
|
||||
auto Screen::getGameSurface() -> std::shared_ptr<Surface> { return game_surface_; }
|
||||
auto Screen::getBorderSurface() -> std::shared_ptr<Surface> {
|
||||
border_is_solid_ = false; // Modificación externa → modo borde dinámico
|
||||
return border_surface_;
|
||||
}
|
||||
|
||||
auto loadData(const std::string& filepath) -> std::vector<uint8_t> {
|
||||
// Load using ResourceHelper (supports both filesystem and pack)
|
||||
return Resource::Helper::loadFile(filepath);
|
||||
}
|
||||
|
||||
void Screen::setLinearUpscale(bool linear) {
|
||||
Options::video.supersampling.linear_upscale = linear;
|
||||
if (shader_backend_ && shader_backend_->isHardwareAccelerated()) {
|
||||
shader_backend_->setLinearUpscale(linear);
|
||||
}
|
||||
}
|
||||
|
||||
void Screen::setDownscaleAlgo(int algo) {
|
||||
Options::video.supersampling.downscale_algo = algo;
|
||||
if (shader_backend_ && shader_backend_->isHardwareAccelerated()) {
|
||||
shader_backend_->setDownscaleAlgo(algo);
|
||||
}
|
||||
}
|
||||
|
||||
auto Screen::getSsTextureSize() const -> std::pair<int, int> {
|
||||
if (!shader_backend_) { return {0, 0}; }
|
||||
return shader_backend_->getSsTextureSize();
|
||||
}
|
||||
|
||||
// Activa/desactiva el supersampling global (Ctrl+F4)
|
||||
void Screen::toggleSupersampling() {
|
||||
Options::video.supersampling.enabled = !Options::video.supersampling.enabled;
|
||||
if (Options::video.shader.enabled && shader_backend_ && shader_backend_->isHardwareAccelerated()) {
|
||||
applyCurrentPostFXPreset();
|
||||
}
|
||||
}
|
||||
|
||||
// Aplica los parámetros del preset actual al backend de shaders
|
||||
void Screen::applyCurrentPostFXPreset() { // NOLINT(readability-convert-member-functions-to-static)
|
||||
if (shader_backend_ && !Options::postfx_presets.empty()) {
|
||||
const auto& p = Options::postfx_presets[static_cast<size_t>(Options::video.shader.current_postfx_preset)];
|
||||
// Supersampling es un toggle global (Options::video.supersampling.enabled), no por preset.
|
||||
// setOversample primero: puede recrear texturas antes de que setPostFXParams
|
||||
// decida si hornear scanlines en CPU o aplicarlas en GPU.
|
||||
shader_backend_->setOversample(Options::video.supersampling.enabled ? 3 : 1);
|
||||
Rendering::PostFXParams params{.vignette = p.vignette, .scanlines = p.scanlines, .chroma = p.chroma, .mask = p.mask, .gamma = p.gamma, .curvature = p.curvature, .bleeding = p.bleeding, .flicker = p.flicker};
|
||||
shader_backend_->setPostFXParams(params);
|
||||
}
|
||||
}
|
||||
|
||||
// Aplica los parámetros del preset CrtPi actual al backend de shaders
|
||||
void Screen::applyCurrentCrtPiPreset() { // NOLINT(readability-convert-member-functions-to-static)
|
||||
if (shader_backend_ && !Options::crtpi_presets.empty()) {
|
||||
const auto& p = Options::crtpi_presets[static_cast<size_t>(Options::video.shader.current_crtpi_preset)];
|
||||
Rendering::CrtPiParams params{
|
||||
.scanline_weight = p.scanline_weight,
|
||||
.scanline_gap_brightness = p.scanline_gap_brightness,
|
||||
.bloom_factor = p.bloom_factor,
|
||||
.input_gamma = p.input_gamma,
|
||||
.output_gamma = p.output_gamma,
|
||||
.mask_brightness = p.mask_brightness,
|
||||
.curvature_x = p.curvature_x,
|
||||
.curvature_y = p.curvature_y,
|
||||
.mask_type = p.mask_type,
|
||||
.enable_scanlines = p.enable_scanlines,
|
||||
.enable_multisample = p.enable_multisample,
|
||||
.enable_gamma = p.enable_gamma,
|
||||
.enable_curvature = p.enable_curvature,
|
||||
.enable_sharper = p.enable_sharper,
|
||||
};
|
||||
shader_backend_->setCrtPiParams(params);
|
||||
}
|
||||
}
|
||||
|
||||
// Cambia el shader de post-procesado activo y aplica el preset correspondiente
|
||||
void Screen::setActiveShader(Rendering::ShaderType type) {
|
||||
Options::video.shader.current_shader = type;
|
||||
if (!shader_backend_) { return; }
|
||||
if (!Options::video.shader.enabled) {
|
||||
// Shaders desactivados: guardar preferencia pero mantener pass-through
|
||||
shader_backend_->setActiveShader(Rendering::ShaderType::POSTFX);
|
||||
shader_backend_->setPostFXParams(Rendering::PostFXParams{});
|
||||
return;
|
||||
}
|
||||
shader_backend_->setActiveShader(type);
|
||||
if (type == Rendering::ShaderType::CRTPI) {
|
||||
applyCurrentCrtPiPreset();
|
||||
} else {
|
||||
applyCurrentPostFXPreset();
|
||||
}
|
||||
}
|
||||
|
||||
// Cicla al siguiente shader disponible (preparado para futura UI)
|
||||
void Screen::nextShader() {
|
||||
const Rendering::ShaderType NEXT = (Options::video.shader.current_shader == Rendering::ShaderType::POSTFX)
|
||||
? Rendering::ShaderType::CRTPI
|
||||
: Rendering::ShaderType::POSTFX;
|
||||
setActiveShader(NEXT);
|
||||
}
|
||||
|
||||
// Inicializa los shaders
|
||||
// El device GPU se crea siempre (independientemente de postfx) para evitar
|
||||
// conflictos SDL_Renderer/SDL_GPU al hacer toggle F4 en Windows/Vulkan.
|
||||
void Screen::initShaders() {
|
||||
SDL_Texture* tex = Options::video.border.enabled ? border_texture_ : game_texture_;
|
||||
|
||||
if (!shader_backend_) {
|
||||
shader_backend_ = std::make_unique<Rendering::SDL3GPUShader>();
|
||||
const std::string FALLBACK_DRIVER = "none";
|
||||
shader_backend_->setPreferredDriver(Options::video.gpu.acceleration ? Options::video.gpu.preferred_driver : FALLBACK_DRIVER);
|
||||
}
|
||||
shader_backend_->init(window_, tex, "", "");
|
||||
gpu_driver_ = shader_backend_->getDriverName();
|
||||
|
||||
// Propagar flags de vsync, integer scale, upscale y downscale al backend GPU
|
||||
shader_backend_->setVSync(Options::video.vertical_sync);
|
||||
shader_backend_->setScaleMode(Options::video.integer_scale);
|
||||
shader_backend_->setLinearUpscale(Options::video.supersampling.linear_upscale);
|
||||
shader_backend_->setDownscaleAlgo(Options::video.supersampling.downscale_algo);
|
||||
|
||||
if (Options::video.shader.enabled) {
|
||||
applyCurrentPostFXPreset();
|
||||
} else {
|
||||
// Pass-through: todos los efectos a 0, el shader solo copia la textura
|
||||
shader_backend_->setPostFXParams(Rendering::PostFXParams{});
|
||||
}
|
||||
|
||||
// Restaurar el shader activo guardado en config (y sus parámetros CrtPi si aplica)
|
||||
shader_backend_->setActiveShader(Options::video.shader.current_shader);
|
||||
if (Options::video.shader.current_shader == Rendering::ShaderType::CRTPI) {
|
||||
applyCurrentCrtPiPreset();
|
||||
}
|
||||
}
|
||||
|
||||
// Obtiene información sobre la pantalla
|
||||
void Screen::getDisplayInfo() { // NOLINT(readability-convert-member-functions-to-static)
|
||||
std::cout << "\n** VIDEO SYSTEM **\n";
|
||||
|
||||
int num_displays = 0;
|
||||
SDL_DisplayID* displays = SDL_GetDisplays(&num_displays);
|
||||
if (displays != nullptr) {
|
||||
for (int i = 0; i < num_displays; ++i) {
|
||||
SDL_DisplayID instance_id = displays[i];
|
||||
const char* name = SDL_GetDisplayName(instance_id);
|
||||
|
||||
std::cout << "Display " << instance_id << ": " << ((name != nullptr) ? name : "Unknown") << '\n';
|
||||
}
|
||||
|
||||
const auto* dm = SDL_GetCurrentDisplayMode(displays[0]);
|
||||
|
||||
// Guarda información del monitor en display_monitor_
|
||||
const char* first_display_name = SDL_GetDisplayName(displays[0]);
|
||||
display_monitor_.name = (first_display_name != nullptr) ? first_display_name : "Unknown";
|
||||
display_monitor_.width = static_cast<int>(dm->w);
|
||||
display_monitor_.height = static_cast<int>(dm->h);
|
||||
display_monitor_.refresh_rate = static_cast<int>(dm->refresh_rate);
|
||||
|
||||
// Calcula el máximo factor de zoom que se puede aplicar a la pantalla
|
||||
Options::window.max_zoom = std::min(dm->w / Options::game.width, dm->h / Options::game.height);
|
||||
Options::window.zoom = std::min(Options::window.zoom, Options::window.max_zoom);
|
||||
|
||||
// Muestra información sobre el tamaño de la pantalla y de la ventana de juego
|
||||
std::cout << "Current display mode: " << static_cast<int>(dm->w) << "x" << static_cast<int>(dm->h) << " @ " << static_cast<int>(dm->refresh_rate) << "Hz\n";
|
||||
|
||||
std::cout << "Window resolution: " << static_cast<int>(Options::game.width) << "x" << static_cast<int>(Options::game.height) << " x" << Options::window.zoom << '\n';
|
||||
|
||||
Options::video.info = std::to_string(static_cast<int>(dm->w)) + "x" +
|
||||
std::to_string(static_cast<int>(dm->h)) + " @ " +
|
||||
std::to_string(static_cast<int>(dm->refresh_rate)) + " Hz";
|
||||
|
||||
// Calcula el máximo factor de zoom que se puede aplicar a la pantalla
|
||||
const int MAX_ZOOM = std::min(dm->w / Options::game.width, (dm->h - WINDOWS_DECORATIONS) / Options::game.height);
|
||||
|
||||
// Normaliza los valores de zoom
|
||||
Options::window.zoom = std::min(Options::window.zoom, MAX_ZOOM);
|
||||
|
||||
SDL_free(displays);
|
||||
}
|
||||
}
|
||||
|
||||
// Arranca SDL VIDEO y crea la ventana
|
||||
auto Screen::initSDLVideo() -> bool {
|
||||
// Inicializar SDL
|
||||
if (!SDL_Init(SDL_INIT_VIDEO)) {
|
||||
std::cerr << "FATAL: Failed to initialize SDL_VIDEO! SDL Error: " << SDL_GetError() << '\n';
|
||||
return false;
|
||||
}
|
||||
|
||||
// Obtener información de la pantalla
|
||||
getDisplayInfo();
|
||||
|
||||
// Configurar hint para renderizado
|
||||
#ifdef __APPLE__
|
||||
if (!SDL_SetHint(SDL_HINT_RENDER_DRIVER, "metal")) {
|
||||
std::cout << "WARNING: Failed to set Metal hint!\n";
|
||||
}
|
||||
#endif
|
||||
|
||||
// Crear ventana
|
||||
const auto WINDOW_WIDTH = Options::video.border.enabled ? Options::game.width + (Options::video.border.width * 2) : Options::game.width;
|
||||
const auto WINDOW_HEIGHT = Options::video.border.enabled ? Options::game.height + (Options::video.border.height * 2) : Options::game.height;
|
||||
SDL_WindowFlags window_flags = 0;
|
||||
if (Options::video.fullscreen) {
|
||||
window_flags |= SDL_WINDOW_FULLSCREEN;
|
||||
}
|
||||
window_ = SDL_CreateWindow(Options::window.caption.c_str(), WINDOW_WIDTH * Options::window.zoom, WINDOW_HEIGHT * Options::window.zoom, window_flags);
|
||||
|
||||
if (window_ == nullptr) {
|
||||
std::cerr << "FATAL: Failed to create window! SDL Error: " << SDL_GetError() << '\n';
|
||||
SDL_Quit();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Crear renderer
|
||||
renderer_ = SDL_CreateRenderer(window_, nullptr);
|
||||
if (renderer_ == nullptr) {
|
||||
std::cerr << "FATAL: Failed to create renderer! SDL Error: " << SDL_GetError() << '\n';
|
||||
SDL_DestroyWindow(window_);
|
||||
window_ = nullptr;
|
||||
SDL_Quit();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Configurar renderer
|
||||
const int EXTRA_WIDTH = Options::video.border.enabled ? Options::video.border.width * 2 : 0;
|
||||
const int EXTRA_HEIGHT = Options::video.border.enabled ? Options::video.border.height * 2 : 0;
|
||||
SDL_SetRenderLogicalPresentation(
|
||||
renderer_,
|
||||
Options::game.width + EXTRA_WIDTH,
|
||||
Options::game.height + EXTRA_HEIGHT,
|
||||
Options::video.integer_scale ? SDL_LOGICAL_PRESENTATION_INTEGER_SCALE : SDL_LOGICAL_PRESENTATION_LETTERBOX);
|
||||
SDL_SetRenderDrawColor(renderer_, 0x00, 0x00, 0x00, 0xFF);
|
||||
SDL_SetRenderDrawBlendMode(renderer_, SDL_BLENDMODE_BLEND);
|
||||
SDL_SetRenderVSync(renderer_, Options::video.vertical_sync ? 1 : SDL_RENDERER_VSYNC_DISABLED);
|
||||
|
||||
std::cout << "Video system initialized successfully\n";
|
||||
return true;
|
||||
}
|
||||
|
||||
// Crea el objeto de texto
|
||||
void Screen::createText() { // NOLINT(readability-convert-member-functions-to-static)
|
||||
// Carga la surface de la fuente directamente del archivo
|
||||
auto surface = std::make_shared<Surface>(Resource::List::get()->get("aseprite.gif"));
|
||||
|
||||
// Crea el objeto de texto (el constructor de Text carga el archivo text_file internamente)
|
||||
text_ = std::make_shared<Text>(surface, Resource::List::get()->get("aseprite.fnt"));
|
||||
}
|
||||
184
source/core/rendering/screen.hpp
Normal file
184
source/core/rendering/screen.hpp
Normal file
@@ -0,0 +1,184 @@
|
||||
#pragma once
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
#include <SDL3/SDL_pixels.h> // Para Uint32
|
||||
|
||||
#include <cstddef> // Para size_t
|
||||
#include <memory> // Para shared_ptr, __shared_ptr_access
|
||||
#include <string> // Para string
|
||||
#include <utility> // Para std::pair
|
||||
#include <vector> // Para vector
|
||||
|
||||
#include "core/rendering/palette_manager.hpp" // Para PaletteManager
|
||||
#include "core/rendering/shader_backend.hpp" // Para Rendering::ShaderType, ShaderBackend
|
||||
#include "utils/utils.hpp" // Para Color
|
||||
class Surface;
|
||||
class Text;
|
||||
|
||||
class Screen {
|
||||
public:
|
||||
// Tipos de filtro
|
||||
enum class Filter : Uint32 {
|
||||
NEAREST = 0,
|
||||
LINEAR = 1,
|
||||
};
|
||||
|
||||
// Singleton
|
||||
static void init(); // Crea el singleton
|
||||
static void destroy(); // Destruye el singleton
|
||||
static auto get() -> Screen*; // Obtiene el singleton
|
||||
|
||||
// Renderizado
|
||||
void clearRenderer(Rgb color = {0x00, 0x00, 0x00}); // Limpia el renderer
|
||||
void clearSurface(Uint8 index); // Limpia la game_surface_
|
||||
void start(); // Prepara para empezar a dibujar en la textura de juego
|
||||
void render(); // Vuelca el contenido del renderizador en pantalla
|
||||
void update(float delta_time); // Actualiza la lógica de la clase
|
||||
|
||||
// Video y ventana
|
||||
void setVideoMode(bool mode); // Establece el modo de video
|
||||
void toggleVideoMode(); // Cambia entre pantalla completa y ventana
|
||||
void toggleIntegerScale(); // Alterna entre activar y desactivar el escalado entero
|
||||
void toggleVSync(); // Alterna entre activar y desactivar el V-Sync
|
||||
auto decWindowZoom() -> bool; // Reduce el tamaño de la ventana
|
||||
auto incWindowZoom() -> bool; // Aumenta el tamaño de la ventana
|
||||
auto setWindowZoom(int zoom) -> bool; // Establece zoom directo; false si fuera de [1, max_zoom]
|
||||
void show(); // Muestra la ventana
|
||||
void hide(); // Oculta la ventana
|
||||
|
||||
// Borde
|
||||
void setBorderColor(Uint8 color); // Cambia el color del borde
|
||||
static void setBorderWidth(int width); // Establece el ancho del borde
|
||||
static void setBorderHeight(int height); // Establece el alto del borde
|
||||
static void setBorderEnabled(bool value); // Establece si se ha de ver el borde
|
||||
void toggleBorder(); // Cambia entre borde visible y no visible
|
||||
|
||||
// Paletas y PostFX
|
||||
void nextPalette(); // Cambia a la siguiente paleta
|
||||
void previousPalette(); // Cambia a la paleta anterior
|
||||
auto setPaletteByName(const std::string& name) -> bool; // Cambia a paleta por nombre; false si no existe
|
||||
[[nodiscard]] auto getPaletteNames() const -> std::vector<std::string>; // Nombres disponibles (minúsculas, sin .pal)
|
||||
[[nodiscard]] auto getPalettePrettyName() const -> std::string; // Nombre actual con guiones sustituidos por espacios
|
||||
void nextPaletteSortMode(); // Cicla al siguiente modo de ordenación de paleta
|
||||
void setPaletteSortMode(PaletteSortMode mode); // Establece modo de ordenación concreto
|
||||
[[nodiscard]] auto getPaletteSortModeName() const -> std::string; // Nombre del modo de ordenación actual
|
||||
void toggleShaders(); // Activa/desactiva todos los shaders respetando current_shader
|
||||
void toggleSupersampling(); // Activa/desactiva el supersampling global
|
||||
void reloadPostFX(); // Recarga el shader del preset actual sin toggle
|
||||
void reloadCrtPi(); // Recarga el shader CrtPi del preset actual sin toggle
|
||||
void setLinearUpscale(bool linear); // Upscale NEAREST (false) o LINEAR (true) en el paso SS
|
||||
void setDownscaleAlgo(int algo); // 0=bilinear legacy, 1=Lanczos2, 2=Lanczos3
|
||||
void setActiveShader(Rendering::ShaderType type); // Cambia el shader de post-procesado activo
|
||||
void nextShader(); // Cicla al siguiente shader disponible (para futura UI)
|
||||
|
||||
// Surfaces y notificaciones
|
||||
void setRendererSurface(const std::shared_ptr<Surface>& surface = nullptr); // Establece el renderizador para las surfaces
|
||||
void setNotificationsEnabled(bool value); // Establece la visibilidad de las notificaciones
|
||||
void updateZoomFactor(); // Recalcula y almacena el factor de zoom real
|
||||
|
||||
// Getters
|
||||
auto getRenderer() -> SDL_Renderer*;
|
||||
auto getRendererSurface() -> std::shared_ptr<Surface>;
|
||||
auto getBorderSurface() -> std::shared_ptr<Surface>;
|
||||
auto getGameSurface() -> std::shared_ptr<Surface>;
|
||||
[[nodiscard]] auto getText() const -> std::shared_ptr<Text> { return text_; }
|
||||
[[nodiscard]] auto getGameSurfaceDstRect() const -> SDL_FRect { return game_surface_dstrect_; }
|
||||
[[nodiscard]] auto getGPUDriver() const -> const std::string& { return gpu_driver_; }
|
||||
[[nodiscard]] auto isHardwareAccelerated() const -> bool { return shader_backend_ && shader_backend_->isHardwareAccelerated(); }
|
||||
[[nodiscard]] auto getLastFPS() const -> int { return fps_.last_value; }
|
||||
[[nodiscard]] auto getZoomFactor() const -> float { return zoom_factor_; }
|
||||
[[nodiscard]] static auto getMaxZoom() -> int;
|
||||
[[nodiscard]] auto getSsTextureSize() const -> std::pair<int, int>;
|
||||
|
||||
private:
|
||||
// Estructuras
|
||||
struct DisplayMonitor {
|
||||
std::string name;
|
||||
int width{0};
|
||||
int height{0};
|
||||
int refresh_rate{0};
|
||||
};
|
||||
|
||||
struct FPS {
|
||||
Uint32 ticks{0}; // Tiempo en milisegundos desde que se comenzó a contar
|
||||
int frame_count{0}; // Número acumulado de frames en el intervalo
|
||||
int last_value{0}; // Número de frames calculado en el último segundo
|
||||
|
||||
void increment() {
|
||||
frame_count++;
|
||||
}
|
||||
|
||||
auto calculate(Uint32 current_ticks) -> int {
|
||||
if (current_ticks - ticks >= 1000) {
|
||||
last_value = frame_count;
|
||||
frame_count = 0;
|
||||
ticks = current_ticks;
|
||||
}
|
||||
return last_value;
|
||||
}
|
||||
};
|
||||
|
||||
// Constantes
|
||||
static constexpr int WINDOWS_DECORATIONS = 35; // Decoraciones de la ventana
|
||||
|
||||
// Singleton
|
||||
static Screen* screen;
|
||||
|
||||
// Métodos privados
|
||||
void renderNotifications() const; // Dibuja las notificaciones
|
||||
void adjustWindowSize(); // Calcula el tamaño de la ventana
|
||||
void adjustRenderLogicalSize(); // Ajusta el tamaño lógico del renderizador
|
||||
void surfaceToTexture(); // Copia la surface a la textura
|
||||
void textureToRenderer(); // Copia la textura al renderizador
|
||||
void renderOverlays(); // Renderiza todos los overlays
|
||||
void initShaders(); // Inicializa los shaders
|
||||
void applyCurrentPostFXPreset(); // Aplica los parámetros del preset PostFX actual al backend
|
||||
void applyCurrentCrtPiPreset(); // Aplica los parámetros del preset CrtPi actual al backend
|
||||
void getDisplayInfo(); // Obtiene información sobre la pantalla
|
||||
auto initSDLVideo() -> bool; // Arranca SDL VIDEO y crea la ventana
|
||||
void createText(); // Crea el objeto de texto
|
||||
|
||||
// Constructor y destructor
|
||||
Screen();
|
||||
~Screen();
|
||||
|
||||
// Objetos SDL
|
||||
SDL_Window* window_{nullptr}; // Ventana de la aplicación
|
||||
SDL_Renderer* renderer_{nullptr}; // Renderizador de la ventana
|
||||
SDL_Texture* game_texture_{nullptr}; // Textura donde se dibuja el juego
|
||||
SDL_Texture* border_texture_{nullptr}; // Textura donde se dibuja el borde del juego
|
||||
|
||||
// Surfaces y renderizado
|
||||
std::shared_ptr<Surface> game_surface_; // Surface principal del juego
|
||||
std::shared_ptr<Surface> border_surface_; // Surface para el borde de la pantalla
|
||||
std::shared_ptr<std::shared_ptr<Surface>> renderer_surface_; // Puntero a la Surface activa
|
||||
std::unique_ptr<Rendering::ShaderBackend> shader_backend_; // Backend de shaders (OpenGL/Metal/Vulkan)
|
||||
std::shared_ptr<Text> text_; // Objeto para escribir texto
|
||||
|
||||
// Buffers persistentes para evitar .resize() cada frame
|
||||
std::vector<Uint32> game_pixel_buffer_; // Textura de juego
|
||||
std::vector<Uint32> border_pixel_buffer_; // Textura de borde (composición final borde+juego)
|
||||
|
||||
// Caché del borde sólido (gameplay normal)
|
||||
bool border_is_solid_{true}; // true = borde de color sólido; false = borde dinámico (carga)
|
||||
Uint32 border_argb_color_{0}; // Color ARGB pre-convertido del borde sólido
|
||||
|
||||
// Configuración de ventana y pantalla
|
||||
int window_width_{0}; // Ancho de la pantalla o ventana
|
||||
int window_height_{0}; // Alto de la pantalla o ventana
|
||||
float zoom_factor_{1.0F}; // Factor de zoom calculado (alto físico / alto lógico)
|
||||
SDL_FRect game_surface_dstrect_; // Coordenadas donde se dibuja la textura del juego
|
||||
|
||||
// Paletas y colores
|
||||
Uint8 border_color_{0}; // Color del borde
|
||||
std::unique_ptr<PaletteManager> palette_manager_; // Gestor de paletas de color
|
||||
|
||||
// Estado y configuración
|
||||
bool notifications_enabled_{false}; // Indica si se muestran las notificaciones
|
||||
FPS fps_; // Gestor de frames por segundo
|
||||
DisplayMonitor display_monitor_; // Información de la pantalla
|
||||
|
||||
// Shaders
|
||||
std::string info_resolution_; // Texto con la información de la pantalla
|
||||
std::string gpu_driver_; // Nombre del driver GPU (SDL3GPU), capturado en initShaders()
|
||||
};
|
||||
10362
source/core/rendering/sdl3gpu/crtpi_frag_spv.h
Normal file
10362
source/core/rendering/sdl3gpu/crtpi_frag_spv.h
Normal file
File diff suppressed because it is too large
Load Diff
4253
source/core/rendering/sdl3gpu/downscale_frag_spv.h
Normal file
4253
source/core/rendering/sdl3gpu/downscale_frag_spv.h
Normal file
File diff suppressed because it is too large
Load Diff
11717
source/core/rendering/sdl3gpu/postfx_frag_spv.h
Normal file
11717
source/core/rendering/sdl3gpu/postfx_frag_spv.h
Normal file
File diff suppressed because it is too large
Load Diff
1449
source/core/rendering/sdl3gpu/postfx_vert_spv.h
Normal file
1449
source/core/rendering/sdl3gpu/postfx_vert_spv.h
Normal file
File diff suppressed because it is too large
Load Diff
1329
source/core/rendering/sdl3gpu/sdl3gpu_shader.cpp
Normal file
1329
source/core/rendering/sdl3gpu/sdl3gpu_shader.cpp
Normal file
File diff suppressed because it is too large
Load Diff
178
source/core/rendering/sdl3gpu/sdl3gpu_shader.hpp
Normal file
178
source/core/rendering/sdl3gpu/sdl3gpu_shader.hpp
Normal file
@@ -0,0 +1,178 @@
|
||||
#pragma once
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
#include <SDL3/SDL_gpu.h>
|
||||
|
||||
#include "core/rendering/shader_backend.hpp"
|
||||
|
||||
// PostFX uniforms pushed to fragment stage each frame.
|
||||
// Must match the MSL struct and GLSL uniform block layout.
|
||||
// 12 floats = 48 bytes — meets Metal/Vulkan 16-byte alignment requirement.
|
||||
struct PostFXUniforms {
|
||||
float vignette_strength; // 0 = none, ~0.8 = subtle
|
||||
float chroma_strength; // 0 = off, ~0.2 = subtle chromatic aberration
|
||||
float scanline_strength; // 0 = off, 1 = full
|
||||
float screen_height; // logical height in pixels (used by bleeding effect)
|
||||
float mask_strength; // 0 = off, 1 = full phosphor dot mask
|
||||
float gamma_strength; // 0 = off, 1 = full gamma 2.4/2.2 correction
|
||||
float curvature; // 0 = flat, 1 = max barrel distortion
|
||||
float bleeding; // 0 = off, 1 = max NTSC chrominance bleeding
|
||||
float pixel_scale; // physical pixels per logical pixel (vh / tex_height_)
|
||||
float time; // seconds since SDL init (SDL_GetTicks() / 1000.0f)
|
||||
float oversample; // supersampling factor (1.0 = off, 3.0 = 3×SS)
|
||||
float flicker; // 0 = off, 1 = phosphor flicker ~50 Hz — keep struct at 48 bytes (3 × 16)
|
||||
};
|
||||
|
||||
// CrtPi uniforms pushed to fragment stage each frame.
|
||||
// Must match the MSL struct and GLSL uniform block layout.
|
||||
// 14 fields (8 floats + 6 ints) + 2 floats (texture size) = 16 fields = 64 bytes — 4 × 16-byte alignment.
|
||||
struct CrtPiUniforms {
|
||||
// vec4 #0
|
||||
float scanline_weight; // Ajuste gaussiano (default 6.0)
|
||||
float scanline_gap_brightness; // Brillo mínimo entre scanlines (default 0.12)
|
||||
float bloom_factor; // Factor brillo zonas iluminadas (default 3.5)
|
||||
float input_gamma; // Gamma de entrada (default 2.4)
|
||||
// vec4 #1
|
||||
float output_gamma; // Gamma de salida (default 2.2)
|
||||
float mask_brightness; // Brillo sub-píxeles máscara (default 0.80)
|
||||
float curvature_x; // Distorsión barrel X (default 0.05)
|
||||
float curvature_y; // Distorsión barrel Y (default 0.10)
|
||||
// vec4 #2
|
||||
int mask_type; // 0=ninguna, 1=verde/magenta, 2=RGB fósforo
|
||||
int enable_scanlines; // 0 = off, 1 = on
|
||||
int enable_multisample; // 0 = off, 1 = on (antialiasing analítico)
|
||||
int enable_gamma; // 0 = off, 1 = on
|
||||
// vec4 #3
|
||||
int enable_curvature; // 0 = off, 1 = on
|
||||
int enable_sharper; // 0 = off, 1 = on
|
||||
float texture_width; // Ancho del canvas en píxeles (inyectado en render)
|
||||
float texture_height; // Alto del canvas en píxeles (inyectado en render)
|
||||
};
|
||||
|
||||
// Downscale uniforms pushed to the Lanczos downscale fragment stage.
|
||||
// 1 int + 3 floats = 16 bytes — meets Metal/Vulkan alignment.
|
||||
struct DownscaleUniforms {
|
||||
int algorithm; // 0 = Lanczos2 (ventana 2), 1 = Lanczos3 (ventana 3)
|
||||
float pad0;
|
||||
float pad1;
|
||||
float pad2;
|
||||
};
|
||||
|
||||
namespace Rendering {
|
||||
|
||||
/**
|
||||
* @brief Backend de shaders usando SDL3 GPU API (Metal en macOS, Vulkan/SPIR-V en Win/Linux)
|
||||
*
|
||||
* Reemplaza el backend OpenGL para que los shaders PostFX funcionen en macOS.
|
||||
* Pipeline: Surface pixels (CPU) → SDL_GPUTransferBuffer → SDL_GPUTexture (scene)
|
||||
* → PostFX render pass → swapchain → present
|
||||
*/
|
||||
class SDL3GPUShader : public ShaderBackend {
|
||||
public:
|
||||
SDL3GPUShader() = default;
|
||||
~SDL3GPUShader() override;
|
||||
|
||||
auto init(SDL_Window* window,
|
||||
SDL_Texture* texture,
|
||||
const std::string& vertex_source,
|
||||
const std::string& fragment_source) -> bool override;
|
||||
|
||||
void render() override;
|
||||
void setTextureSize(float width, float height) override {}
|
||||
void cleanup() final; // Libera pipeline/texturas pero mantiene el device vivo
|
||||
void destroy(); // Limpieza completa (device + swapchain); llamar solo al cerrar
|
||||
[[nodiscard]] auto isHardwareAccelerated() const -> bool override { return is_initialized_; }
|
||||
[[nodiscard]] auto getDriverName() const -> std::string override { return driver_name_; }
|
||||
|
||||
// Establece el driver GPU preferido (vacío = auto). Debe llamarse antes de init().
|
||||
void setPreferredDriver(const std::string& driver) override { preferred_driver_ = driver; }
|
||||
|
||||
// Sube píxeles ARGB8888 desde CPU; llamado antes de render()
|
||||
void uploadPixels(const Uint32* pixels, int width, int height) override;
|
||||
|
||||
// Actualiza los parámetros de intensidad de los efectos PostFX
|
||||
void setPostFXParams(const PostFXParams& p) override;
|
||||
|
||||
// Activa/desactiva VSync en el swapchain
|
||||
void setVSync(bool vsync) override;
|
||||
|
||||
// Activa/desactiva escalado entero (integer scale)
|
||||
void setScaleMode(bool integer_scale) override;
|
||||
|
||||
// Establece factor de supersampling (1 = off, 3 = 3×SS)
|
||||
void setOversample(int factor) override;
|
||||
|
||||
// Activa/desactiva interpolación LINEAR en el upscale (false = NEAREST)
|
||||
void setLinearUpscale(bool linear) override;
|
||||
|
||||
// Selecciona algoritmo de downscale: 0=bilinear legacy, 1=Lanczos2, 2=Lanczos3
|
||||
void setDownscaleAlgo(int algo) override;
|
||||
|
||||
// Devuelve las dimensiones de la textura de supersampling (0,0 si SS desactivado)
|
||||
[[nodiscard]] auto getSsTextureSize() const -> std::pair<int, int> override;
|
||||
|
||||
// Selecciona el shader de post-procesado activo (POSTFX o CRTPI)
|
||||
void setActiveShader(ShaderType type) override;
|
||||
|
||||
// Actualiza los parámetros del shader CRT-Pi
|
||||
void setCrtPiParams(const CrtPiParams& p) override;
|
||||
|
||||
// Devuelve el shader activo
|
||||
[[nodiscard]] auto getActiveShader() const -> ShaderType override { return active_shader_; }
|
||||
|
||||
private:
|
||||
static auto createShaderMSL(SDL_GPUDevice* device,
|
||||
const char* msl_source,
|
||||
const char* entrypoint,
|
||||
SDL_GPUShaderStage stage,
|
||||
Uint32 num_samplers,
|
||||
Uint32 num_uniform_buffers) -> SDL_GPUShader*;
|
||||
|
||||
static auto createShaderSPIRV(SDL_GPUDevice* device,
|
||||
const uint8_t* spv_code,
|
||||
size_t spv_size,
|
||||
const char* entrypoint,
|
||||
SDL_GPUShaderStage stage,
|
||||
Uint32 num_samplers,
|
||||
Uint32 num_uniform_buffers) -> SDL_GPUShader*;
|
||||
|
||||
auto createPipeline() -> bool;
|
||||
auto createCrtPiPipeline() -> bool; // Pipeline dedicado para el shader CrtPi
|
||||
auto reinitTexturesAndBuffer() -> bool; // Recrea scene_texture_ y upload_buffer_
|
||||
auto recreateScaledTexture(int factor) -> bool; // Recrea scaled_texture_ para factor dado
|
||||
static auto calcSsFactor(float zoom) -> int; // Primer múltiplo de 3 >= zoom (mín 3)
|
||||
// Devuelve el mejor present mode disponible: IMMEDIATE > MAILBOX > VSYNC
|
||||
[[nodiscard]] auto bestPresentMode(bool vsync) const -> SDL_GPUPresentMode;
|
||||
|
||||
SDL_Window* window_ = nullptr;
|
||||
SDL_GPUDevice* device_ = nullptr;
|
||||
SDL_GPUGraphicsPipeline* pipeline_ = nullptr; // PostFX pass (→ swapchain o → postfx_texture_)
|
||||
SDL_GPUGraphicsPipeline* crtpi_pipeline_ = nullptr; // CrtPi pass (→ swapchain directo, sin SS)
|
||||
SDL_GPUGraphicsPipeline* postfx_offscreen_pipeline_ = nullptr; // PostFX → postfx_texture_ (B8G8R8A8, solo con Lanczos)
|
||||
SDL_GPUGraphicsPipeline* upscale_pipeline_ = nullptr; // Upscale pass (solo con SS)
|
||||
SDL_GPUGraphicsPipeline* downscale_pipeline_ = nullptr; // Lanczos downscale (solo con SS + algo > 0)
|
||||
SDL_GPUTexture* scene_texture_ = nullptr; // Canvas del juego (game_width_ × game_height_)
|
||||
SDL_GPUTexture* scaled_texture_ = nullptr; // Upscale target (game×factor), solo con SS
|
||||
SDL_GPUTexture* postfx_texture_ = nullptr; // PostFX output a resolución escalada, solo con Lanczos
|
||||
SDL_GPUTransferBuffer* upload_buffer_ = nullptr;
|
||||
SDL_GPUSampler* sampler_ = nullptr; // NEAREST
|
||||
SDL_GPUSampler* linear_sampler_ = nullptr; // LINEAR
|
||||
|
||||
PostFXUniforms uniforms_{.vignette_strength = 0.6F, .chroma_strength = 0.15F, .scanline_strength = 0.7F, .screen_height = 192.0F, .pixel_scale = 1.0F, .oversample = 1.0F};
|
||||
CrtPiUniforms crtpi_uniforms_{.scanline_weight = 6.0F, .scanline_gap_brightness = 0.12F, .bloom_factor = 3.5F, .input_gamma = 2.4F, .output_gamma = 2.2F, .mask_brightness = 0.80F, .curvature_x = 0.05F, .curvature_y = 0.10F, .mask_type = 2, .enable_scanlines = 1, .enable_multisample = 1, .enable_gamma = 1};
|
||||
ShaderType active_shader_ = ShaderType::POSTFX; // Shader de post-procesado activo
|
||||
|
||||
int game_width_ = 0; // Dimensiones originales del canvas
|
||||
int game_height_ = 0;
|
||||
int ss_factor_ = 0; // Factor SS activo (3, 6, 9...) o 0 si SS desactivado
|
||||
int oversample_ = 1; // SS on/off (1 = off, >1 = on)
|
||||
int downscale_algo_ = 1; // 0 = bilinear legacy, 1 = Lanczos2, 2 = Lanczos3
|
||||
std::string driver_name_;
|
||||
std::string preferred_driver_; // Driver preferido; vacío = auto (SDL elige)
|
||||
bool is_initialized_ = false;
|
||||
bool vsync_ = true;
|
||||
bool integer_scale_ = false;
|
||||
bool linear_upscale_ = false; // Upscale NEAREST (false) o LINEAR (true)
|
||||
};
|
||||
|
||||
} // namespace Rendering
|
||||
633
source/core/rendering/sdl3gpu/upscale_frag_spv.h
Normal file
633
source/core/rendering/sdl3gpu/upscale_frag_spv.h
Normal file
@@ -0,0 +1,633 @@
|
||||
#pragma once
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
static const uint8_t kupscale_frag_spv[] = {
|
||||
0x03,
|
||||
0x02,
|
||||
0x23,
|
||||
0x07,
|
||||
0x00,
|
||||
0x00,
|
||||
0x01,
|
||||
0x00,
|
||||
0x0b,
|
||||
0x00,
|
||||
0x0d,
|
||||
0x00,
|
||||
0x14,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x11,
|
||||
0x00,
|
||||
0x02,
|
||||
0x00,
|
||||
0x01,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x0b,
|
||||
0x00,
|
||||
0x06,
|
||||
0x00,
|
||||
0x01,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x47,
|
||||
0x4c,
|
||||
0x53,
|
||||
0x4c,
|
||||
0x2e,
|
||||
0x73,
|
||||
0x74,
|
||||
0x64,
|
||||
0x2e,
|
||||
0x34,
|
||||
0x35,
|
||||
0x30,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x0e,
|
||||
0x00,
|
||||
0x03,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x01,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x0f,
|
||||
0x00,
|
||||
0x07,
|
||||
0x00,
|
||||
0x04,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x04,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x6d,
|
||||
0x61,
|
||||
0x69,
|
||||
0x6e,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x09,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x11,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x10,
|
||||
0x00,
|
||||
0x03,
|
||||
0x00,
|
||||
0x04,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x07,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x03,
|
||||
0x00,
|
||||
0x03,
|
||||
0x00,
|
||||
0x02,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0xc2,
|
||||
0x01,
|
||||
0x00,
|
||||
0x00,
|
||||
0x04,
|
||||
0x00,
|
||||
0x0a,
|
||||
0x00,
|
||||
0x47,
|
||||
0x4c,
|
||||
0x5f,
|
||||
0x47,
|
||||
0x4f,
|
||||
0x4f,
|
||||
0x47,
|
||||
0x4c,
|
||||
0x45,
|
||||
0x5f,
|
||||
0x63,
|
||||
0x70,
|
||||
0x70,
|
||||
0x5f,
|
||||
0x73,
|
||||
0x74,
|
||||
0x79,
|
||||
0x6c,
|
||||
0x65,
|
||||
0x5f,
|
||||
0x6c,
|
||||
0x69,
|
||||
0x6e,
|
||||
0x65,
|
||||
0x5f,
|
||||
0x64,
|
||||
0x69,
|
||||
0x72,
|
||||
0x65,
|
||||
0x63,
|
||||
0x74,
|
||||
0x69,
|
||||
0x76,
|
||||
0x65,
|
||||
0x00,
|
||||
0x00,
|
||||
0x04,
|
||||
0x00,
|
||||
0x08,
|
||||
0x00,
|
||||
0x47,
|
||||
0x4c,
|
||||
0x5f,
|
||||
0x47,
|
||||
0x4f,
|
||||
0x4f,
|
||||
0x47,
|
||||
0x4c,
|
||||
0x45,
|
||||
0x5f,
|
||||
0x69,
|
||||
0x6e,
|
||||
0x63,
|
||||
0x6c,
|
||||
0x75,
|
||||
0x64,
|
||||
0x65,
|
||||
0x5f,
|
||||
0x64,
|
||||
0x69,
|
||||
0x72,
|
||||
0x65,
|
||||
0x63,
|
||||
0x74,
|
||||
0x69,
|
||||
0x76,
|
||||
0x65,
|
||||
0x00,
|
||||
0x05,
|
||||
0x00,
|
||||
0x04,
|
||||
0x00,
|
||||
0x04,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x6d,
|
||||
0x61,
|
||||
0x69,
|
||||
0x6e,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x05,
|
||||
0x00,
|
||||
0x05,
|
||||
0x00,
|
||||
0x09,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x6f,
|
||||
0x75,
|
||||
0x74,
|
||||
0x5f,
|
||||
0x63,
|
||||
0x6f,
|
||||
0x6c,
|
||||
0x6f,
|
||||
0x72,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x05,
|
||||
0x00,
|
||||
0x04,
|
||||
0x00,
|
||||
0x0d,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x73,
|
||||
0x63,
|
||||
0x65,
|
||||
0x6e,
|
||||
0x65,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x05,
|
||||
0x00,
|
||||
0x04,
|
||||
0x00,
|
||||
0x11,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x76,
|
||||
0x5f,
|
||||
0x75,
|
||||
0x76,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x47,
|
||||
0x00,
|
||||
0x04,
|
||||
0x00,
|
||||
0x09,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x1e,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x47,
|
||||
0x00,
|
||||
0x04,
|
||||
0x00,
|
||||
0x0d,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x21,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x47,
|
||||
0x00,
|
||||
0x04,
|
||||
0x00,
|
||||
0x0d,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x22,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x02,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x47,
|
||||
0x00,
|
||||
0x04,
|
||||
0x00,
|
||||
0x11,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x1e,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x13,
|
||||
0x00,
|
||||
0x02,
|
||||
0x00,
|
||||
0x02,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x21,
|
||||
0x00,
|
||||
0x03,
|
||||
0x00,
|
||||
0x03,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x02,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x16,
|
||||
0x00,
|
||||
0x03,
|
||||
0x00,
|
||||
0x06,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x20,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x17,
|
||||
0x00,
|
||||
0x04,
|
||||
0x00,
|
||||
0x07,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x06,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x04,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x20,
|
||||
0x00,
|
||||
0x04,
|
||||
0x00,
|
||||
0x08,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x03,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x07,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x3b,
|
||||
0x00,
|
||||
0x04,
|
||||
0x00,
|
||||
0x08,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x09,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x03,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x19,
|
||||
0x00,
|
||||
0x09,
|
||||
0x00,
|
||||
0x0a,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x06,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x01,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x01,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x1b,
|
||||
0x00,
|
||||
0x03,
|
||||
0x00,
|
||||
0x0b,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x0a,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x20,
|
||||
0x00,
|
||||
0x04,
|
||||
0x00,
|
||||
0x0c,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x0b,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x3b,
|
||||
0x00,
|
||||
0x04,
|
||||
0x00,
|
||||
0x0c,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x0d,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x17,
|
||||
0x00,
|
||||
0x04,
|
||||
0x00,
|
||||
0x0f,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x06,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x02,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x20,
|
||||
0x00,
|
||||
0x04,
|
||||
0x00,
|
||||
0x10,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x01,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x0f,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x3b,
|
||||
0x00,
|
||||
0x04,
|
||||
0x00,
|
||||
0x10,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x11,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x01,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x36,
|
||||
0x00,
|
||||
0x05,
|
||||
0x00,
|
||||
0x02,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x04,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x03,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0xf8,
|
||||
0x00,
|
||||
0x02,
|
||||
0x00,
|
||||
0x05,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x3d,
|
||||
0x00,
|
||||
0x04,
|
||||
0x00,
|
||||
0x0b,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x0e,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x0d,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x3d,
|
||||
0x00,
|
||||
0x04,
|
||||
0x00,
|
||||
0x0f,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x12,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x11,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x57,
|
||||
0x00,
|
||||
0x05,
|
||||
0x00,
|
||||
0x07,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x13,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x0e,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x12,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x3e,
|
||||
0x00,
|
||||
0x03,
|
||||
0x00,
|
||||
0x09,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x13,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0xfd,
|
||||
0x00,
|
||||
0x01,
|
||||
0x00,
|
||||
0x38,
|
||||
0x00,
|
||||
0x01,
|
||||
0x00};
|
||||
static const size_t kupscale_frag_spv_size = 628;
|
||||
175
source/core/rendering/shader_backend.hpp
Normal file
175
source/core/rendering/shader_backend.hpp
Normal file
@@ -0,0 +1,175 @@
|
||||
#pragma once
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
namespace Rendering {
|
||||
|
||||
/** @brief Identificador del shader de post-procesado activo */
|
||||
enum class ShaderType { POSTFX,
|
||||
CRTPI };
|
||||
|
||||
/**
|
||||
* @brief Parámetros de intensidad de los efectos PostFX
|
||||
* Definido a nivel de namespace para facilitar el uso desde subclases y screen.cpp
|
||||
*/
|
||||
struct PostFXParams {
|
||||
float vignette = 0.0F; // Intensidad de la viñeta
|
||||
float scanlines = 0.0F; // Intensidad de las scanlines
|
||||
float chroma = 0.0F; // Aberración cromática
|
||||
float mask = 0.0F; // Máscara de fósforo RGB
|
||||
float gamma = 0.0F; // Corrección gamma (blend 0=off, 1=full)
|
||||
float curvature = 0.0F; // Curvatura barrel CRT
|
||||
float bleeding = 0.0F; // Sangrado de color NTSC
|
||||
float flicker = 0.0F; // Parpadeo de fósforo CRT ~50 Hz
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Parámetros del shader CRT-Pi (algoritmo de scanlines continuas)
|
||||
* Diferente al PostFX: usa pesos gaussianos por distancia subpixel y bloom.
|
||||
*/
|
||||
struct CrtPiParams {
|
||||
float scanline_weight{6.0F}; // Ajuste gaussiano (mayor = scanlines más estrechas)
|
||||
float scanline_gap_brightness{0.12F}; // Brillo mínimo en las ranuras entre scanlines
|
||||
float bloom_factor{3.5F}; // Factor de brillo para zonas iluminadas
|
||||
float input_gamma{2.4F}; // Gamma de entrada (linealización)
|
||||
float output_gamma{2.2F}; // Gamma de salida (codificación)
|
||||
float mask_brightness{0.80F}; // Sub-píxeles tenues en la máscara de fósforo
|
||||
float curvature_x{0.05F}; // Distorsión barrel eje X
|
||||
float curvature_y{0.10F}; // Distorsión barrel eje Y
|
||||
int mask_type{2}; // 0=ninguna, 1=verde/magenta, 2=RGB fósforo
|
||||
bool enable_scanlines{true}; // Activar efecto de scanlines
|
||||
bool enable_multisample{true}; // Antialiasing analítico de scanlines
|
||||
bool enable_gamma{true}; // Corrección gamma
|
||||
bool enable_curvature{false}; // Distorsión barrel CRT
|
||||
bool enable_sharper{false}; // Submuestreo más nítido (modo SHARPER)
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Interfaz abstracta para backends de renderizado con shaders
|
||||
*
|
||||
* Esta interfaz define el contrato que todos los backends de shaders
|
||||
* deben cumplir (OpenGL, Metal, Vulkan, etc.)
|
||||
*/
|
||||
class ShaderBackend {
|
||||
public:
|
||||
virtual ~ShaderBackend() = default;
|
||||
|
||||
/**
|
||||
* @brief Inicializa el backend de shaders
|
||||
* @param window Ventana SDL
|
||||
* @param texture Textura de backbuffer a la que aplicar shaders
|
||||
* @param vertex_source Código fuente del vertex shader
|
||||
* @param fragment_source Código fuente del fragment shader
|
||||
* @return true si la inicialización fue exitosa
|
||||
*/
|
||||
virtual auto init(SDL_Window* window,
|
||||
SDL_Texture* texture,
|
||||
const std::string& vertex_source,
|
||||
const std::string& fragment_source) -> bool = 0;
|
||||
|
||||
/**
|
||||
* @brief Renderiza la textura con los shaders aplicados
|
||||
*/
|
||||
virtual void render() = 0;
|
||||
|
||||
/**
|
||||
* @brief Establece el tamaño de la textura como parámetro del shader
|
||||
* @param width Ancho de la textura
|
||||
* @param height Alto de la textura
|
||||
*/
|
||||
virtual void setTextureSize(float width, float height) = 0;
|
||||
|
||||
/**
|
||||
* @brief Limpia y libera recursos del backend
|
||||
*/
|
||||
virtual void cleanup() = 0;
|
||||
|
||||
/**
|
||||
* @brief Sube píxeles ARGB8888 desde la CPU al backend de shaders
|
||||
* Usado por SDL3GPUShader para evitar pasar por SDL_Texture
|
||||
*/
|
||||
virtual void uploadPixels(const Uint32* /*pixels*/, int /*width*/, int /*height*/) {}
|
||||
|
||||
/**
|
||||
* @brief Establece los parámetros de intensidad de los efectos PostFX
|
||||
* @param p Struct con todos los parámetros PostFX
|
||||
*/
|
||||
virtual void setPostFXParams(const PostFXParams& /*p*/) {}
|
||||
|
||||
/**
|
||||
* @brief Activa o desactiva VSync en el swapchain del GPU device
|
||||
*/
|
||||
virtual void setVSync(bool /*vsync*/) {}
|
||||
|
||||
/**
|
||||
* @brief Activa o desactiva el escalado entero (integer scale)
|
||||
*/
|
||||
virtual void setScaleMode(bool /*integer_scale*/) {}
|
||||
|
||||
/**
|
||||
* @brief Establece el factor de supersampling (1 = off, 3 = 3× SS)
|
||||
* Con factor > 1, la textura GPU se crea a game×factor resolución y
|
||||
* las scanlines se hornean en CPU (uploadPixels). El sampler usa LINEAR.
|
||||
*/
|
||||
virtual void setOversample(int /*factor*/) {}
|
||||
|
||||
/**
|
||||
* @brief Activa/desactiva interpolación LINEAR en el paso de upscale (SS).
|
||||
* Por defecto NEAREST (false). Solo tiene efecto con supersampling activo.
|
||||
*/
|
||||
virtual void setLinearUpscale(bool /*linear*/) {}
|
||||
[[nodiscard]] virtual auto isLinearUpscale() const -> bool { return false; }
|
||||
|
||||
/**
|
||||
* @brief Selecciona el algoritmo de downscale tras el PostFX (SS activo).
|
||||
* 0 = bilinear legacy (comportamiento actual, sin textura intermedia),
|
||||
* 1 = Lanczos2 (ventana 2, ~25 muestras), 2 = Lanczos3 (ventana 3, ~49 muestras).
|
||||
*/
|
||||
virtual void setDownscaleAlgo(int /*algo*/) {}
|
||||
[[nodiscard]] virtual auto getDownscaleAlgo() const -> int { return 0; }
|
||||
|
||||
/**
|
||||
* @brief Devuelve las dimensiones de la textura de supersampling.
|
||||
* @return Par (ancho, alto) en píxeles; (0, 0) si SS está desactivado.
|
||||
*/
|
||||
[[nodiscard]] virtual auto getSsTextureSize() const -> std::pair<int, int> { return {0, 0}; }
|
||||
|
||||
/**
|
||||
* @brief Verifica si el backend está usando aceleración por hardware
|
||||
* @return true si usa aceleración (OpenGL/Metal/Vulkan)
|
||||
*/
|
||||
[[nodiscard]] virtual auto isHardwareAccelerated() const -> bool = 0;
|
||||
|
||||
/**
|
||||
* @brief Nombre del driver GPU activo (p.ej. "vulkan", "metal", "direct3d12")
|
||||
* @return Cadena vacía si no disponible
|
||||
*/
|
||||
[[nodiscard]] virtual auto getDriverName() const -> std::string { return {}; }
|
||||
|
||||
/**
|
||||
* @brief Establece el driver GPU preferido antes de init().
|
||||
* Vacío = selección automática de SDL. Implementado en SDL3GPUShader.
|
||||
*/
|
||||
virtual void setPreferredDriver(const std::string& /*driver*/) {}
|
||||
|
||||
/**
|
||||
* @brief Selecciona el shader de post-procesado activo (POSTFX o CRTPI).
|
||||
* Debe llamarse antes de render(). No recrea pipelines.
|
||||
*/
|
||||
virtual void setActiveShader(ShaderType /*type*/) {}
|
||||
|
||||
/**
|
||||
* @brief Establece los parámetros del shader CRT-Pi.
|
||||
*/
|
||||
virtual void setCrtPiParams(const CrtPiParams& /*p*/) {}
|
||||
|
||||
/**
|
||||
* @brief Devuelve el shader de post-procesado activo.
|
||||
*/
|
||||
[[nodiscard]] virtual auto getActiveShader() const -> ShaderType { return ShaderType::POSTFX; }
|
||||
};
|
||||
|
||||
} // namespace Rendering
|
||||
344
source/core/rendering/sprite/animated_sprite.cpp
Normal file
344
source/core/rendering/sprite/animated_sprite.cpp
Normal file
@@ -0,0 +1,344 @@
|
||||
#include "core/rendering/sprite/animated_sprite.hpp"
|
||||
|
||||
#include <cstddef> // Para size_t
|
||||
#include <fstream> // Para basic_ostream, basic_istream, operator<<, basic...
|
||||
#include <iostream> // Para cout, cerr
|
||||
#include <sstream> // Para basic_stringstream
|
||||
#include <stdexcept> // Para runtime_error
|
||||
#include <utility>
|
||||
|
||||
#include "core/rendering/surface.hpp" // Para Surface
|
||||
#include "core/resources/resource_cache.hpp" // Para Resource
|
||||
#include "core/resources/resource_helper.hpp" // Para ResourceHelper
|
||||
#include "external/fkyaml_node.hpp" // Para fkyaml::node
|
||||
#include "utils/utils.hpp" // Para printWithDots
|
||||
|
||||
// Helper: Convierte un nodo YAML de frames (array) a vector de SDL_FRect
|
||||
auto convertYAMLFramesToRects(const fkyaml::node& frames_node, float frame_width, float frame_height, int frames_per_row, int max_tiles) -> std::vector<SDL_FRect> {
|
||||
std::vector<SDL_FRect> frames;
|
||||
SDL_FRect rect = {.x = 0.0F, .y = 0.0F, .w = frame_width, .h = frame_height};
|
||||
|
||||
for (const auto& frame_index_node : frames_node) {
|
||||
const int NUM_TILE = frame_index_node.get_value<int>();
|
||||
if (NUM_TILE <= max_tiles) {
|
||||
rect.x = (NUM_TILE % frames_per_row) * frame_width;
|
||||
rect.y = (NUM_TILE / frames_per_row) * frame_height;
|
||||
frames.emplace_back(rect);
|
||||
}
|
||||
}
|
||||
|
||||
return frames;
|
||||
}
|
||||
|
||||
// Carga las animaciones desde un fichero YAML
|
||||
auto AnimatedSprite::loadAnimationsFromYAML(const std::string& file_path, std::shared_ptr<Surface>& surface, float& frame_width, float& frame_height) -> std::vector<AnimationData> { // NOLINT(readability-convert-member-functions-to-static)
|
||||
std::vector<AnimationData> animations;
|
||||
|
||||
// Extract filename for logging
|
||||
const std::string FILE_NAME = file_path.substr(file_path.find_last_of("\\/") + 1);
|
||||
|
||||
try {
|
||||
// Load YAML file using ResourceHelper (supports both filesystem and pack)
|
||||
auto file_data = Resource::Helper::loadFile(file_path);
|
||||
|
||||
if (file_data.empty()) {
|
||||
std::cerr << "Error: Unable to load animation file " << FILE_NAME << '\n';
|
||||
throw std::runtime_error("Animation file not found: " + file_path);
|
||||
}
|
||||
|
||||
printWithDots("Animation : ", FILE_NAME, "[ LOADED ]");
|
||||
|
||||
// Parse YAML from string
|
||||
std::string yaml_content(file_data.begin(), file_data.end());
|
||||
auto yaml = fkyaml::node::deserialize(yaml_content);
|
||||
|
||||
// --- Parse global configuration ---
|
||||
if (yaml.contains("tileSetFile")) {
|
||||
auto tile_set_file = yaml["tileSetFile"].get_value<std::string>();
|
||||
surface = Resource::Cache::get()->getSurface(tile_set_file);
|
||||
}
|
||||
|
||||
if (yaml.contains("frameWidth")) {
|
||||
frame_width = static_cast<float>(yaml["frameWidth"].get_value<int>());
|
||||
}
|
||||
|
||||
if (yaml.contains("frameHeight")) {
|
||||
frame_height = static_cast<float>(yaml["frameHeight"].get_value<int>());
|
||||
}
|
||||
|
||||
// Calculate sprite sheet parameters
|
||||
int frames_per_row = 1;
|
||||
int max_tiles = 1;
|
||||
if (surface) {
|
||||
frames_per_row = surface->getWidth() / static_cast<int>(frame_width);
|
||||
const int W = surface->getWidth() / static_cast<int>(frame_width);
|
||||
const int H = surface->getHeight() / static_cast<int>(frame_height);
|
||||
max_tiles = W * H;
|
||||
}
|
||||
|
||||
// --- Parse animations array ---
|
||||
if (yaml.contains("animations") && yaml["animations"].is_sequence()) {
|
||||
const auto& animations_node = yaml["animations"];
|
||||
|
||||
for (const auto& anim_node : animations_node) {
|
||||
AnimationData animation;
|
||||
|
||||
// Parse animation name
|
||||
if (anim_node.contains("name")) {
|
||||
animation.name = anim_node["name"].get_value<std::string>();
|
||||
}
|
||||
|
||||
// Parse speed (seconds per frame)
|
||||
if (anim_node.contains("speed")) {
|
||||
animation.speed = anim_node["speed"].get_value<float>();
|
||||
}
|
||||
|
||||
// Parse loop frame index
|
||||
if (anim_node.contains("loop")) {
|
||||
animation.loop = anim_node["loop"].get_value<int>();
|
||||
}
|
||||
|
||||
// Parse frames array
|
||||
if (anim_node.contains("frames") && anim_node["frames"].is_sequence()) {
|
||||
animation.frames = convertYAMLFramesToRects(
|
||||
anim_node["frames"],
|
||||
frame_width,
|
||||
frame_height,
|
||||
frames_per_row,
|
||||
max_tiles);
|
||||
}
|
||||
|
||||
animations.push_back(animation);
|
||||
}
|
||||
}
|
||||
|
||||
} catch (const fkyaml::exception& e) {
|
||||
std::cerr << "YAML parsing error in " << FILE_NAME << ": " << e.what() << '\n';
|
||||
throw;
|
||||
} catch (const std::exception& e) {
|
||||
std::cerr << "Error loading animation " << FILE_NAME << ": " << e.what() << '\n';
|
||||
throw;
|
||||
}
|
||||
|
||||
return animations;
|
||||
}
|
||||
|
||||
// Constructor con bytes YAML del cache (parsing lazy)
|
||||
AnimatedSprite::AnimatedSprite(const AnimationResource& cached_data) {
|
||||
// Parsear YAML desde los bytes cargados en cache
|
||||
std::string yaml_content(cached_data.yaml_data.begin(), cached_data.yaml_data.end());
|
||||
|
||||
try {
|
||||
auto yaml = fkyaml::node::deserialize(yaml_content);
|
||||
|
||||
// Variables para almacenar configuración global
|
||||
float frame_width = 0.0F;
|
||||
float frame_height = 0.0F;
|
||||
|
||||
// --- Parse global configuration ---
|
||||
if (yaml.contains("tileSetFile")) {
|
||||
auto tile_set_file = yaml["tileSetFile"].get_value<std::string>();
|
||||
// Ahora SÍ podemos acceder al cache (ya está completamente cargado)
|
||||
surface_ = Resource::Cache::get()->getSurface(tile_set_file);
|
||||
}
|
||||
|
||||
if (yaml.contains("frameWidth")) {
|
||||
frame_width = static_cast<float>(yaml["frameWidth"].get_value<int>());
|
||||
}
|
||||
|
||||
if (yaml.contains("frameHeight")) {
|
||||
frame_height = static_cast<float>(yaml["frameHeight"].get_value<int>());
|
||||
}
|
||||
|
||||
// Calculate sprite sheet parameters
|
||||
int frames_per_row = 1;
|
||||
int max_tiles = 1;
|
||||
if (surface_) {
|
||||
frames_per_row = surface_->getWidth() / static_cast<int>(frame_width);
|
||||
const int W = surface_->getWidth() / static_cast<int>(frame_width);
|
||||
const int H = surface_->getHeight() / static_cast<int>(frame_height);
|
||||
max_tiles = W * H;
|
||||
}
|
||||
|
||||
// --- Parse animations array ---
|
||||
if (yaml.contains("animations") && yaml["animations"].is_sequence()) {
|
||||
const auto& animations_node = yaml["animations"];
|
||||
|
||||
for (const auto& anim_node : animations_node) {
|
||||
AnimationData animation;
|
||||
|
||||
// Parse animation name
|
||||
if (anim_node.contains("name")) {
|
||||
animation.name = anim_node["name"].get_value<std::string>();
|
||||
}
|
||||
|
||||
// Parse speed (seconds per frame)
|
||||
if (anim_node.contains("speed")) {
|
||||
animation.speed = anim_node["speed"].get_value<float>();
|
||||
}
|
||||
|
||||
// Parse loop frame index
|
||||
if (anim_node.contains("loop")) {
|
||||
animation.loop = anim_node["loop"].get_value<int>();
|
||||
}
|
||||
|
||||
// Parse frames array
|
||||
if (anim_node.contains("frames") && anim_node["frames"].is_sequence()) {
|
||||
animation.frames = convertYAMLFramesToRects(
|
||||
anim_node["frames"],
|
||||
frame_width,
|
||||
frame_height,
|
||||
frames_per_row,
|
||||
max_tiles);
|
||||
}
|
||||
|
||||
animations_.push_back(animation);
|
||||
}
|
||||
}
|
||||
|
||||
// Set dimensions
|
||||
setWidth(frame_width);
|
||||
setHeight(frame_height);
|
||||
|
||||
// Inicializar con la primera animación si existe
|
||||
if (!animations_.empty() && !animations_[0].frames.empty()) {
|
||||
setClip(animations_[0].frames[0]);
|
||||
}
|
||||
|
||||
} catch (const fkyaml::exception& e) {
|
||||
std::cerr << "YAML parsing error in animation " << cached_data.name << ": " << e.what() << '\n';
|
||||
throw;
|
||||
} catch (const std::exception& e) {
|
||||
std::cerr << "Error loading animation " << cached_data.name << ": " << e.what() << '\n';
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
// Constructor per a subclasses amb surface directa (sense YAML)
|
||||
AnimatedSprite::AnimatedSprite(std::shared_ptr<Surface> surface, SDL_FRect pos)
|
||||
: MovingSprite(std::move(surface), pos) {
|
||||
// animations_ queda buit (protegit per el guard de animate())
|
||||
if (surface_) {
|
||||
clip_ = {.x = 0, .y = 0, .w = surface_->getWidth(), .h = surface_->getHeight()};
|
||||
}
|
||||
}
|
||||
|
||||
// Obtiene el indice de la animación a partir del nombre
|
||||
auto AnimatedSprite::getIndex(const std::string& name) -> int { // NOLINT(readability-convert-member-functions-to-static)
|
||||
auto index = -1;
|
||||
|
||||
for (const auto& a : animations_) {
|
||||
index++;
|
||||
if (a.name == name) {
|
||||
return index;
|
||||
}
|
||||
}
|
||||
std::cout << "** Warning: could not find \"" << name.c_str() << "\" animation" << '\n';
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Calcula el frame correspondiente a la animación (time-based)
|
||||
void AnimatedSprite::animate(float delta_time) { // NOLINT(readability-convert-member-functions-to-static)
|
||||
if (animations_.empty()) { return; }
|
||||
if (animations_[current_animation_].speed <= 0.0F) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Acumula el tiempo transcurrido
|
||||
animations_[current_animation_].accumulated_time += delta_time;
|
||||
|
||||
// Calcula el frame actual a partir del tiempo acumulado
|
||||
const int TARGET_FRAME = static_cast<int>(
|
||||
animations_[current_animation_].accumulated_time /
|
||||
animations_[current_animation_].speed);
|
||||
|
||||
// Si alcanza el final de la animación, maneja el loop
|
||||
if (TARGET_FRAME >= static_cast<int>(animations_[current_animation_].frames.size())) {
|
||||
if (animations_[current_animation_].loop == -1) {
|
||||
// Si no hay loop, congela en el último frame
|
||||
animations_[current_animation_].current_frame =
|
||||
static_cast<int>(animations_[current_animation_].frames.size()) - 1;
|
||||
animations_[current_animation_].completed = true;
|
||||
|
||||
// Establece el clip del último frame
|
||||
if (animations_[current_animation_].current_frame >= 0) {
|
||||
setClip(animations_[current_animation_].frames[animations_[current_animation_].current_frame]);
|
||||
}
|
||||
} else {
|
||||
// Si hay loop, vuelve al frame indicado
|
||||
animations_[current_animation_].accumulated_time =
|
||||
static_cast<float>(animations_[current_animation_].loop) *
|
||||
animations_[current_animation_].speed;
|
||||
animations_[current_animation_].current_frame = animations_[current_animation_].loop;
|
||||
|
||||
// Establece el clip del frame de loop
|
||||
setClip(animations_[current_animation_].frames[animations_[current_animation_].current_frame]);
|
||||
}
|
||||
} else {
|
||||
// Actualiza el frame actual
|
||||
animations_[current_animation_].current_frame = TARGET_FRAME;
|
||||
|
||||
// Establece el clip del frame actual
|
||||
if (animations_[current_animation_].current_frame >= 0 &&
|
||||
animations_[current_animation_].current_frame <
|
||||
static_cast<int>(animations_[current_animation_].frames.size())) {
|
||||
setClip(animations_[current_animation_].frames[animations_[current_animation_].current_frame]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Comprueba si ha terminado la animación
|
||||
auto AnimatedSprite::animationIsCompleted() -> bool {
|
||||
return animations_[current_animation_].completed;
|
||||
}
|
||||
|
||||
// Establece la animacion actual
|
||||
void AnimatedSprite::setCurrentAnimation(const std::string& name) {
|
||||
const auto NEW_ANIMATION = getIndex(name);
|
||||
if (current_animation_ != NEW_ANIMATION) {
|
||||
current_animation_ = NEW_ANIMATION;
|
||||
animations_[current_animation_].current_frame = 0;
|
||||
animations_[current_animation_].accumulated_time = 0.0F;
|
||||
animations_[current_animation_].completed = false;
|
||||
setClip(animations_[current_animation_].frames[animations_[current_animation_].current_frame]);
|
||||
}
|
||||
}
|
||||
|
||||
// Establece la animacion actual
|
||||
void AnimatedSprite::setCurrentAnimation(int index) {
|
||||
const auto NEW_ANIMATION = index;
|
||||
if (current_animation_ != NEW_ANIMATION) {
|
||||
current_animation_ = NEW_ANIMATION;
|
||||
animations_[current_animation_].current_frame = 0;
|
||||
animations_[current_animation_].accumulated_time = 0.0F;
|
||||
animations_[current_animation_].completed = false;
|
||||
setClip(animations_[current_animation_].frames[animations_[current_animation_].current_frame]);
|
||||
}
|
||||
}
|
||||
|
||||
// Actualiza las variables del objeto (time-based)
|
||||
void AnimatedSprite::update(float delta_time) {
|
||||
animate(delta_time);
|
||||
MovingSprite::update(delta_time);
|
||||
}
|
||||
|
||||
// Reinicia la animación
|
||||
void AnimatedSprite::resetAnimation() {
|
||||
animations_[current_animation_].current_frame = 0;
|
||||
animations_[current_animation_].accumulated_time = 0.0F;
|
||||
animations_[current_animation_].completed = false;
|
||||
}
|
||||
|
||||
// Establece el frame actual de la animación
|
||||
void AnimatedSprite::setCurrentAnimationFrame(int num) {
|
||||
// Descarta valores fuera de rango
|
||||
if (num < 0 || num >= static_cast<int>(animations_[current_animation_].frames.size())) {
|
||||
num = 0;
|
||||
}
|
||||
|
||||
// Cambia el valor de la variable
|
||||
animations_[current_animation_].current_frame = num;
|
||||
|
||||
// Escoge el frame correspondiente de la animación
|
||||
setClip(animations_[current_animation_].frames[animations_[current_animation_].current_frame]);
|
||||
}
|
||||
60
source/core/rendering/sprite/animated_sprite.hpp
Normal file
60
source/core/rendering/sprite/animated_sprite.hpp
Normal file
@@ -0,0 +1,60 @@
|
||||
#pragma once
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include <memory> // Para shared_ptr
|
||||
#include <string> // Para string
|
||||
#include <utility>
|
||||
#include <vector> // Para vector
|
||||
|
||||
#include "core/rendering/sprite/moving_sprite.hpp" // Para SMovingSprite
|
||||
#include "core/resources/resource_types.hpp" // Para AnimationResource
|
||||
|
||||
class Surface;
|
||||
|
||||
class AnimatedSprite : public MovingSprite {
|
||||
public:
|
||||
using Animations = std::vector<std::string>; // Tipo para lista de animaciones
|
||||
|
||||
// Estructura pública de datos de animación
|
||||
struct AnimationData {
|
||||
std::string name; // Nombre de la animacion
|
||||
std::vector<SDL_FRect> frames; // Cada uno de los frames que componen la animación
|
||||
float speed{0.083F}; // Velocidad de la animación (segundos por frame)
|
||||
int loop{0}; // Indica a que frame vuelve la animación al terminar. -1 para que no vuelva
|
||||
bool completed{false}; // Indica si ha finalizado la animación
|
||||
int current_frame{0}; // Frame actual
|
||||
float accumulated_time{0.0F}; // Tiempo acumulado para las animaciones (time-based)
|
||||
};
|
||||
|
||||
// Métodos estáticos
|
||||
static auto loadAnimationsFromYAML(const std::string& file_path, std::shared_ptr<Surface>& surface, float& frame_width, float& frame_height) -> std::vector<AnimationData>; // Carga las animaciones desde fichero YAML
|
||||
|
||||
// Constructores
|
||||
explicit AnimatedSprite(const AnimationResource& cached_data); // Constructor con datos pre-cargados del cache
|
||||
|
||||
~AnimatedSprite() override = default; // Destructor
|
||||
|
||||
void update(float delta_time) override; // Actualiza las variables del objeto (time-based)
|
||||
|
||||
// Consultas de estado
|
||||
auto animationIsCompleted() -> bool; // Comprueba si ha terminado la animación
|
||||
auto getIndex(const std::string& name) -> int; // Obtiene el índice de la animación por nombre
|
||||
auto getCurrentAnimationSize() -> int { return static_cast<int>(animations_[current_animation_].frames.size()); } // Número de frames de la animación actual
|
||||
|
||||
// Modificadores de animación
|
||||
void setCurrentAnimation(const std::string& name = "default"); // Establece la animación actual por nombre
|
||||
void setCurrentAnimation(int index = 0); // Establece la animación actual por índice
|
||||
void resetAnimation(); // Reinicia la animación
|
||||
void setCurrentAnimationFrame(int num); // Establece el frame actual de la animación
|
||||
void animate(float delta_time); // Calcula el frame correspondiente a la animación actual (time-based)
|
||||
|
||||
protected:
|
||||
// Constructor per a ús de subclasses que gestionen la surface directament (sense YAML)
|
||||
AnimatedSprite(std::shared_ptr<Surface> surface, SDL_FRect pos);
|
||||
|
||||
private:
|
||||
// Variables miembro
|
||||
std::vector<AnimationData> animations_; // Vector con las diferentes animaciones
|
||||
int current_animation_{0}; // Animación activa
|
||||
};
|
||||
188
source/core/rendering/sprite/dissolve_sprite.cpp
Normal file
188
source/core/rendering/sprite/dissolve_sprite.cpp
Normal file
@@ -0,0 +1,188 @@
|
||||
#include "core/rendering/sprite/dissolve_sprite.hpp"
|
||||
|
||||
#include <algorithm> // Para min
|
||||
#include <cstdint> // Para uint32_t
|
||||
|
||||
#include "core/rendering/surface.hpp" // Para Surface
|
||||
|
||||
// Hash 2D estable per a dithering (rank aleatori per posició de píxel)
|
||||
static auto pixelRank(int col, int row) -> float {
|
||||
auto h = (static_cast<uint32_t>(col) * 2246822519U) ^ (static_cast<uint32_t>(row) * 2654435761U);
|
||||
h ^= (h >> 13);
|
||||
h *= 1274126177U;
|
||||
h ^= (h >> 16);
|
||||
return static_cast<float>(h & 0xFFFFU) / 65536.0F;
|
||||
}
|
||||
|
||||
// Rang per a un píxel tenint en compte direcció (70% direccional + 30% aleatori)
|
||||
auto DissolveSprite::computePixelRank(int col, int row, int frame_h, DissolveDirection dir) -> float {
|
||||
const float RANDOM = pixelRank(col, row);
|
||||
if (dir == DissolveDirection::NONE || frame_h <= 0) {
|
||||
return RANDOM;
|
||||
}
|
||||
|
||||
float y_factor = 0.0F;
|
||||
if (dir == DissolveDirection::DOWN) {
|
||||
y_factor = static_cast<float>(row) / static_cast<float>(frame_h);
|
||||
} else {
|
||||
y_factor = static_cast<float>(frame_h - 1 - row) / static_cast<float>(frame_h);
|
||||
}
|
||||
|
||||
return (y_factor * 0.7F) + (RANDOM * 0.3F);
|
||||
}
|
||||
|
||||
// Constructor per a surface directa (sense AnimationResource)
|
||||
DissolveSprite::DissolveSprite(std::shared_ptr<Surface> surface, SDL_FRect pos)
|
||||
: AnimatedSprite(std::move(surface), pos) {
|
||||
if (surface_) {
|
||||
const int W = static_cast<int>(surface_->getWidth());
|
||||
const int H = static_cast<int>(surface_->getHeight());
|
||||
surface_display_ = std::make_shared<Surface>(W, H);
|
||||
surface_display_->setTransparentColor(surface_->getTransparentColor());
|
||||
surface_display_->clear(surface_->getTransparentColor());
|
||||
}
|
||||
}
|
||||
|
||||
// Constructor
|
||||
DissolveSprite::DissolveSprite(const AnimationResource& data)
|
||||
: AnimatedSprite(data) {
|
||||
if (surface_) {
|
||||
const int W = static_cast<int>(surface_->getWidth());
|
||||
const int H = static_cast<int>(surface_->getHeight());
|
||||
surface_display_ = std::make_shared<Surface>(W, H);
|
||||
surface_display_->setTransparentColor(surface_->getTransparentColor());
|
||||
// Inicialitza tots els píxels com a transparents
|
||||
surface_display_->clear(surface_->getTransparentColor());
|
||||
}
|
||||
}
|
||||
|
||||
// Reconstrueix la surface_display_ filtrant píxels per progress_
|
||||
void DissolveSprite::rebuildDisplaySurface() {
|
||||
if (!surface_ || !surface_display_) {
|
||||
return;
|
||||
}
|
||||
|
||||
const SDL_FRect CLIP = clip_;
|
||||
const int SX = static_cast<int>(CLIP.x);
|
||||
const int SY = static_cast<int>(CLIP.y);
|
||||
const int SW = static_cast<int>(CLIP.w);
|
||||
const int SH = static_cast<int>(CLIP.h);
|
||||
|
||||
if (SW <= 0 || SH <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto src_data = surface_->getSurfaceData();
|
||||
auto dst_data = surface_display_->getSurfaceData();
|
||||
|
||||
const int SRC_W = static_cast<int>(src_data->width);
|
||||
const int DST_W = static_cast<int>(dst_data->width);
|
||||
const Uint8 TRANSPARENT = surface_->getTransparentColor();
|
||||
|
||||
// Esborra frame anterior si ha canviat
|
||||
if (prev_clip_.w > 0 && prev_clip_.h > 0 &&
|
||||
(prev_clip_.x != CLIP.x || prev_clip_.y != CLIP.y ||
|
||||
prev_clip_.w != CLIP.w || prev_clip_.h != CLIP.h)) {
|
||||
surface_display_->fillRect(&prev_clip_, TRANSPARENT);
|
||||
}
|
||||
|
||||
// Esborra la zona del frame actual (reconstrucció neta)
|
||||
surface_display_->fillRect(&CLIP, TRANSPARENT);
|
||||
|
||||
// Copia píxels filtrats per progress_
|
||||
for (int row = 0; row < SH; ++row) {
|
||||
for (int col = 0; col < SW; ++col) {
|
||||
const Uint8 COLOR = src_data->data[((SY + row) * SRC_W) + (SX + col)];
|
||||
if (COLOR == TRANSPARENT) {
|
||||
continue;
|
||||
}
|
||||
const float RANK = computePixelRank(col, row, SH, direction_);
|
||||
if (RANK >= progress_) {
|
||||
const Uint8 OUT = (COLOR == source_color_) ? target_color_ : COLOR;
|
||||
dst_data->data[((SY + row) * DST_W) + (SX + col)] = OUT;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
prev_clip_ = CLIP;
|
||||
needs_rebuild_ = false;
|
||||
}
|
||||
|
||||
// Actualitza animació, moviment i transició temporal
|
||||
void DissolveSprite::update(float delta_time) {
|
||||
const SDL_FRect OLD_CLIP = clip_;
|
||||
AnimatedSprite::update(delta_time);
|
||||
|
||||
// Detecta canvi de frame d'animació
|
||||
if (clip_.x != OLD_CLIP.x || clip_.y != OLD_CLIP.y ||
|
||||
clip_.w != OLD_CLIP.w || clip_.h != OLD_CLIP.h) {
|
||||
needs_rebuild_ = true;
|
||||
}
|
||||
|
||||
// Actualitza transició temporal si activa
|
||||
if (transition_mode_ != TransitionMode::NONE) {
|
||||
transition_elapsed_ += delta_time * 1000.0F;
|
||||
const float T = std::min(transition_elapsed_ / transition_duration_, 1.0F);
|
||||
progress_ = (transition_mode_ == TransitionMode::DISSOLVING) ? T : (1.0F - T);
|
||||
needs_rebuild_ = true;
|
||||
if (T >= 1.0F) {
|
||||
transition_mode_ = TransitionMode::NONE;
|
||||
}
|
||||
}
|
||||
|
||||
if (needs_rebuild_) {
|
||||
rebuildDisplaySurface();
|
||||
}
|
||||
}
|
||||
|
||||
// Renderitza: usa surface_display_ (amb color replace) si disponible
|
||||
void DissolveSprite::render() {
|
||||
if (!surface_display_) {
|
||||
AnimatedSprite::render();
|
||||
return;
|
||||
}
|
||||
surface_display_->render(static_cast<int>(pos_.x), static_cast<int>(pos_.y), &clip_, flip_);
|
||||
}
|
||||
|
||||
// Estableix el progrés manualment
|
||||
void DissolveSprite::setProgress(float progress) {
|
||||
progress_ = std::min(std::max(progress, 0.0F), 1.0F);
|
||||
needs_rebuild_ = true;
|
||||
}
|
||||
|
||||
// Inicia dissolució temporal (visible → invisible)
|
||||
void DissolveSprite::startDissolve(float duration_ms, DissolveDirection dir) {
|
||||
direction_ = dir;
|
||||
transition_mode_ = TransitionMode::DISSOLVING;
|
||||
transition_duration_ = duration_ms;
|
||||
transition_elapsed_ = 0.0F;
|
||||
progress_ = 0.0F;
|
||||
needs_rebuild_ = true;
|
||||
}
|
||||
|
||||
// Inicia generació temporal (invisible → visible)
|
||||
void DissolveSprite::startGenerate(float duration_ms, DissolveDirection dir) {
|
||||
direction_ = dir;
|
||||
transition_mode_ = TransitionMode::GENERATING;
|
||||
transition_duration_ = duration_ms;
|
||||
transition_elapsed_ = 0.0F;
|
||||
progress_ = 1.0F;
|
||||
needs_rebuild_ = true;
|
||||
}
|
||||
|
||||
// Atura la transició temporal
|
||||
void DissolveSprite::stopTransition() {
|
||||
transition_mode_ = TransitionMode::NONE;
|
||||
}
|
||||
|
||||
// Retorna si la transició ha acabat
|
||||
auto DissolveSprite::isTransitionDone() const -> bool {
|
||||
return transition_mode_ == TransitionMode::NONE;
|
||||
}
|
||||
|
||||
// Configura substitució de color per a la reconstrucció
|
||||
void DissolveSprite::setColorReplace(Uint8 source, Uint8 target) {
|
||||
source_color_ = source;
|
||||
target_color_ = target;
|
||||
needs_rebuild_ = true;
|
||||
}
|
||||
62
source/core/rendering/sprite/dissolve_sprite.hpp
Normal file
62
source/core/rendering/sprite/dissolve_sprite.hpp
Normal file
@@ -0,0 +1,62 @@
|
||||
#pragma once
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include <memory> // Para shared_ptr
|
||||
|
||||
#include "core/rendering/sprite/animated_sprite.hpp" // Para SurfaceAnimatedSprite
|
||||
|
||||
class Surface;
|
||||
|
||||
// Direcció de la dissolució
|
||||
enum class DissolveDirection { NONE,
|
||||
DOWN,
|
||||
UP };
|
||||
|
||||
// Sprite que pot dissoldre's o generar-se de forma aleatòria en X mil·lisegons.
|
||||
// progress_ va de 0.0 (totalment visible) a 1.0 (totalment invisible).
|
||||
class DissolveSprite : public AnimatedSprite {
|
||||
public:
|
||||
explicit DissolveSprite(const AnimationResource& data);
|
||||
DissolveSprite(std::shared_ptr<Surface> surface, SDL_FRect pos);
|
||||
~DissolveSprite() override = default;
|
||||
|
||||
void update(float delta_time) override;
|
||||
void render() override;
|
||||
|
||||
// Progrés manual [0.0 = totalment visible, 1.0 = totalment invisible]
|
||||
void setProgress(float progress);
|
||||
[[nodiscard]] auto getProgress() const -> float { return progress_; }
|
||||
|
||||
// Inicia una dissolució temporal (visible → invisible en duration_ms)
|
||||
void startDissolve(float duration_ms, DissolveDirection dir = DissolveDirection::NONE);
|
||||
|
||||
// Inicia una generació temporal (invisible → visible en duration_ms)
|
||||
void startGenerate(float duration_ms, DissolveDirection dir = DissolveDirection::NONE);
|
||||
|
||||
void stopTransition();
|
||||
[[nodiscard]] auto isTransitionDone() const -> bool;
|
||||
|
||||
// Substitució de color: en reconstruir, substitueix source per target
|
||||
void setColorReplace(Uint8 source, Uint8 target);
|
||||
|
||||
private:
|
||||
enum class TransitionMode { NONE,
|
||||
DISSOLVING,
|
||||
GENERATING };
|
||||
|
||||
std::shared_ptr<Surface> surface_display_; // Superfície amb els píxels filtrats
|
||||
|
||||
float progress_{0.0F}; // [0=visible, 1=invisible]
|
||||
DissolveDirection direction_{DissolveDirection::NONE};
|
||||
TransitionMode transition_mode_{TransitionMode::NONE};
|
||||
float transition_duration_{0.0F};
|
||||
float transition_elapsed_{0.0F};
|
||||
SDL_FRect prev_clip_{.x = 0, .y = 0, .w = 0, .h = 0};
|
||||
bool needs_rebuild_{false};
|
||||
Uint8 source_color_{255}; // 255 = transparent = sense replace per defecte
|
||||
Uint8 target_color_{0};
|
||||
|
||||
void rebuildDisplaySurface();
|
||||
[[nodiscard]] static auto computePixelRank(int col, int row, int frame_h, DissolveDirection dir) -> float;
|
||||
};
|
||||
103
source/core/rendering/sprite/moving_sprite.cpp
Normal file
103
source/core/rendering/sprite/moving_sprite.cpp
Normal file
@@ -0,0 +1,103 @@
|
||||
#include "core/rendering/sprite/moving_sprite.hpp"
|
||||
|
||||
#include <utility>
|
||||
|
||||
#include "core/rendering/surface.hpp" // Para Surface
|
||||
|
||||
// Constructor
|
||||
MovingSprite::MovingSprite(std::shared_ptr<Surface> surface, SDL_FRect pos, SDL_FlipMode flip)
|
||||
: Sprite(std::move(surface), pos),
|
||||
x_(pos.x),
|
||||
y_(pos.y),
|
||||
flip_(flip) { Sprite::pos_ = pos; }
|
||||
|
||||
MovingSprite::MovingSprite(std::shared_ptr<Surface> surface, SDL_FRect pos)
|
||||
: Sprite(std::move(surface), pos),
|
||||
x_(pos.x),
|
||||
y_(pos.y) { Sprite::pos_ = pos; }
|
||||
|
||||
MovingSprite::MovingSprite() { Sprite::clear(); }
|
||||
|
||||
MovingSprite::MovingSprite(std::shared_ptr<Surface> surface)
|
||||
: Sprite(std::move(surface)) { Sprite::clear(); }
|
||||
|
||||
// Reinicia todas las variables
|
||||
void MovingSprite::clear() {
|
||||
// Resetea posición
|
||||
x_ = 0.0F;
|
||||
y_ = 0.0F;
|
||||
|
||||
// Resetea velocidad
|
||||
vx_ = 0.0F;
|
||||
vy_ = 0.0F;
|
||||
|
||||
// Resetea aceleración
|
||||
ax_ = 0.0F;
|
||||
ay_ = 0.0F;
|
||||
|
||||
// Resetea flip
|
||||
flip_ = SDL_FLIP_NONE;
|
||||
|
||||
Sprite::clear();
|
||||
}
|
||||
|
||||
// Mueve el sprite (time-based)
|
||||
// Nota: vx_, vy_ ahora se interpretan como pixels/segundo
|
||||
// Nota: ax_, ay_ ahora se interpretan como pixels/segundo²
|
||||
void MovingSprite::move(float delta_time) {
|
||||
// Aplica aceleración a velocidad (time-based)
|
||||
vx_ += ax_ * delta_time;
|
||||
vy_ += ay_ * delta_time;
|
||||
|
||||
// Aplica velocidad a posición (time-based)
|
||||
x_ += vx_ * delta_time;
|
||||
y_ += vy_ * delta_time;
|
||||
|
||||
// Actualiza posición entera para renderizado
|
||||
pos_.x = static_cast<int>(x_);
|
||||
pos_.y = static_cast<int>(y_);
|
||||
}
|
||||
|
||||
// Actualiza las variables internas del objeto (time-based)
|
||||
void MovingSprite::update(float delta_time) {
|
||||
move(delta_time);
|
||||
}
|
||||
|
||||
// Muestra el sprite por pantalla
|
||||
void MovingSprite::render() {
|
||||
surface_->render(pos_.x, pos_.y, &clip_, flip_);
|
||||
}
|
||||
|
||||
// Muestra el sprite por pantalla
|
||||
void MovingSprite::render(Uint8 source_color, Uint8 target_color) {
|
||||
surface_->renderWithColorReplace(pos_.x, pos_.y, source_color, target_color, &clip_, flip_);
|
||||
}
|
||||
|
||||
// Establece la posición y_ el tamaño del objeto
|
||||
void MovingSprite::setPos(SDL_FRect rect) {
|
||||
x_ = rect.x;
|
||||
y_ = rect.y;
|
||||
|
||||
pos_ = rect;
|
||||
}
|
||||
|
||||
// Establece el valor de las variables
|
||||
void MovingSprite::setPos(float x, float y) {
|
||||
x_ = x;
|
||||
y_ = y;
|
||||
|
||||
pos_.x = static_cast<int>(x_);
|
||||
pos_.y = static_cast<int>(y_);
|
||||
}
|
||||
|
||||
// Establece el valor de la variable
|
||||
void MovingSprite::setPosX(float value) {
|
||||
x_ = value;
|
||||
pos_.x = static_cast<int>(x_);
|
||||
}
|
||||
|
||||
// Establece el valor de la variable
|
||||
void MovingSprite::setPosY(float value) {
|
||||
y_ = value;
|
||||
pos_.y = static_cast<int>(y_);
|
||||
}
|
||||
77
source/core/rendering/sprite/moving_sprite.hpp
Normal file
77
source/core/rendering/sprite/moving_sprite.hpp
Normal file
@@ -0,0 +1,77 @@
|
||||
#pragma once
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include <memory> // Para shared_ptr
|
||||
|
||||
#include "core/rendering/sprite/sprite.hpp" // Para SSprite
|
||||
class Surface; // lines 8-8
|
||||
|
||||
// Clase SMovingSprite. Añade movimiento y flip al sprite
|
||||
class MovingSprite : public Sprite {
|
||||
public:
|
||||
// Constructores
|
||||
MovingSprite(std::shared_ptr<Surface> surface, SDL_FRect pos, SDL_FlipMode flip);
|
||||
MovingSprite(std::shared_ptr<Surface> surface, SDL_FRect pos);
|
||||
explicit MovingSprite();
|
||||
explicit MovingSprite(std::shared_ptr<Surface> surface);
|
||||
~MovingSprite() override = default;
|
||||
|
||||
// Actualización y renderizado
|
||||
void update(float delta_time) override; // Actualiza variables internas (time-based)
|
||||
void render() override; // Muestra el sprite por pantalla
|
||||
void render(Uint8 source_color, Uint8 target_color) override; // Renderiza con reemplazo de color
|
||||
|
||||
// Gestión de estado
|
||||
void clear() override; // Reinicia todas las variables a cero
|
||||
|
||||
// Getters de posición
|
||||
[[nodiscard]] auto getPosX() const -> float { return x_; }
|
||||
[[nodiscard]] auto getPosY() const -> float { return y_; }
|
||||
|
||||
// Getters de velocidad
|
||||
[[nodiscard]] auto getVelX() const -> float { return vx_; }
|
||||
[[nodiscard]] auto getVelY() const -> float { return vy_; }
|
||||
|
||||
// Getters de aceleración
|
||||
[[nodiscard]] auto getAccelX() const -> float { return ax_; }
|
||||
[[nodiscard]] auto getAccelY() const -> float { return ay_; }
|
||||
|
||||
// Setters de posición
|
||||
void setPos(SDL_FRect rect); // Establece posición y tamaño del objeto
|
||||
void setPos(float x, float y); // Establece posición x, y
|
||||
void setPosX(float value); // Establece posición X
|
||||
void setPosY(float value); // Establece posición Y
|
||||
|
||||
// Setters de velocidad
|
||||
void setVelX(float value) { vx_ = value; }
|
||||
void setVelY(float value) { vy_ = value; }
|
||||
|
||||
// Setters de aceleración
|
||||
void setAccelX(float value) { ax_ = value; }
|
||||
void setAccelY(float value) { ay_ = value; }
|
||||
|
||||
// Gestión de flip (volteo horizontal)
|
||||
void setFlip(SDL_FlipMode flip) { flip_ = flip; } // Establece modo de flip
|
||||
auto getFlip() -> SDL_FlipMode { return flip_; } // Obtiene modo de flip
|
||||
void flip() { flip_ = (flip_ == SDL_FLIP_HORIZONTAL) ? SDL_FLIP_NONE : SDL_FLIP_HORIZONTAL; } // Alterna flip horizontal
|
||||
|
||||
protected:
|
||||
// Métodos protegidos
|
||||
void move(float delta_time); // Mueve el sprite (time-based)
|
||||
|
||||
// Variables miembro - Posición
|
||||
float x_{0.0F}; // Posición en el eje X
|
||||
float y_{0.0F}; // Posición en el eje Y
|
||||
|
||||
// Variables miembro - Velocidad (pixels/segundo)
|
||||
float vx_{0.0F}; // Velocidad en el eje X
|
||||
float vy_{0.0F}; // Velocidad en el eje Y
|
||||
|
||||
// Variables miembro - Aceleración (pixels/segundo²)
|
||||
float ax_{0.0F}; // Aceleración en el eje X
|
||||
float ay_{0.0F}; // Aceleración en el eje Y
|
||||
|
||||
// Variables miembro - Renderizado
|
||||
SDL_FlipMode flip_{SDL_FLIP_NONE}; // Modo de volteo del sprite
|
||||
};
|
||||
76
source/core/rendering/sprite/sprite.cpp
Normal file
76
source/core/rendering/sprite/sprite.cpp
Normal file
@@ -0,0 +1,76 @@
|
||||
#include "core/rendering/sprite/sprite.hpp"
|
||||
|
||||
#include <utility>
|
||||
|
||||
#include "core/rendering/surface.hpp" // Para Surface
|
||||
|
||||
// Constructor
|
||||
Sprite::Sprite(std::shared_ptr<Surface> surface, float x, float y, float w, float h)
|
||||
: surface_(std::move(surface)),
|
||||
pos_{.x = x, .y = y, .w = w, .h = h},
|
||||
clip_{.x = 0.0F, .y = 0.0F, .w = pos_.w, .h = pos_.h} {}
|
||||
|
||||
Sprite::Sprite(std::shared_ptr<Surface> surface, SDL_FRect rect)
|
||||
: surface_(std::move(surface)),
|
||||
pos_(rect),
|
||||
clip_{.x = 0.0F, .y = 0.0F, .w = pos_.w, .h = pos_.h} {}
|
||||
|
||||
Sprite::Sprite() = default;
|
||||
|
||||
Sprite::Sprite(std::shared_ptr<Surface> surface)
|
||||
: surface_(std::move(surface)),
|
||||
pos_{0.0F, 0.0F, surface_->getWidth(), surface_->getHeight()},
|
||||
clip_(pos_) {}
|
||||
|
||||
// Muestra el sprite por pantalla
|
||||
void Sprite::render() {
|
||||
surface_->render(pos_.x, pos_.y, &clip_);
|
||||
}
|
||||
|
||||
void Sprite::render(Uint8 source_color, Uint8 target_color) {
|
||||
surface_->renderWithColorReplace(pos_.x, pos_.y, source_color, target_color, &clip_);
|
||||
}
|
||||
|
||||
void Sprite::renderWithVerticalFade(int fade_h, int canvas_height) {
|
||||
surface_->renderWithVerticalFade(
|
||||
static_cast<int>(pos_.x),
|
||||
static_cast<int>(pos_.y),
|
||||
fade_h,
|
||||
canvas_height,
|
||||
&clip_);
|
||||
}
|
||||
|
||||
void Sprite::renderWithVerticalFade(int fade_h, int canvas_height, Uint8 source_color, Uint8 target_color) {
|
||||
surface_->renderWithVerticalFade(
|
||||
static_cast<int>(pos_.x),
|
||||
static_cast<int>(pos_.y),
|
||||
fade_h,
|
||||
canvas_height,
|
||||
source_color,
|
||||
target_color,
|
||||
&clip_);
|
||||
}
|
||||
|
||||
// Establece la posición del objeto
|
||||
void Sprite::setPosition(float x, float y) {
|
||||
pos_.x = x;
|
||||
pos_.y = y;
|
||||
}
|
||||
|
||||
// Establece la posición del objeto
|
||||
void Sprite::setPosition(SDL_FPoint p) {
|
||||
pos_.x = p.x;
|
||||
pos_.y = p.y;
|
||||
}
|
||||
|
||||
// Reinicia las variables a cero
|
||||
void Sprite::clear() {
|
||||
pos_ = {.x = 0.0F, .y = 0.0F, .w = 0.0F, .h = 0.0F};
|
||||
clip_ = {.x = 0.0F, .y = 0.0F, .w = 0.0F, .h = 0.0F};
|
||||
}
|
||||
|
||||
// Actualiza el estado del sprite (time-based)
|
||||
void Sprite::update(float delta_time) {
|
||||
// Base implementation does nothing (static sprites)
|
||||
(void)delta_time; // Evita warning de parámetro no usado
|
||||
}
|
||||
62
source/core/rendering/sprite/sprite.hpp
Normal file
62
source/core/rendering/sprite/sprite.hpp
Normal file
@@ -0,0 +1,62 @@
|
||||
#pragma once
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include <memory> // Para shared_ptr
|
||||
#include <utility>
|
||||
class Surface; // lines 5-5
|
||||
|
||||
// Clase SurfaceSprite
|
||||
class Sprite {
|
||||
public:
|
||||
// Constructores
|
||||
Sprite(std::shared_ptr<Surface>, float x, float y, float w, float h);
|
||||
Sprite(std::shared_ptr<Surface>, SDL_FRect rect);
|
||||
Sprite();
|
||||
explicit Sprite(std::shared_ptr<Surface>);
|
||||
|
||||
// Destructor
|
||||
virtual ~Sprite() = default;
|
||||
|
||||
// Actualización y renderizado
|
||||
virtual void update(float delta_time); // Actualiza el estado del sprite (time-based)
|
||||
virtual void render(); // Muestra el sprite por pantalla
|
||||
virtual void render(Uint8 source_color, Uint8 target_color); // Renderiza con reemplazo de color
|
||||
virtual void renderWithVerticalFade(int fade_h, int canvas_height); // Renderiza amb dissolució vertical (hash 2D, sense parpelleig)
|
||||
virtual void renderWithVerticalFade(int fade_h, int canvas_height, Uint8 source_color, Uint8 target_color); // Idem amb reemplaç de color
|
||||
|
||||
// Gestión de estado
|
||||
virtual void clear(); // Reinicia las variables a cero
|
||||
|
||||
// Obtención de propiedades
|
||||
[[nodiscard]] auto getX() const -> float { return pos_.x; }
|
||||
[[nodiscard]] auto getY() const -> float { return pos_.y; }
|
||||
[[nodiscard]] auto getWidth() const -> float { return pos_.w; }
|
||||
[[nodiscard]] auto getHeight() const -> float { return pos_.h; }
|
||||
[[nodiscard]] auto getPosition() const -> SDL_FRect { return pos_; }
|
||||
[[nodiscard]] auto getClip() const -> SDL_FRect { return clip_; }
|
||||
[[nodiscard]] auto getSurface() const -> std::shared_ptr<Surface> { return surface_; }
|
||||
auto getRect() -> SDL_FRect& { return pos_; }
|
||||
|
||||
// Modificación de posición y tamaño
|
||||
void setX(float x) { pos_.x = x; }
|
||||
void setY(float y) { pos_.y = y; }
|
||||
void setWidth(float w) { pos_.w = w; }
|
||||
void setHeight(float h) { pos_.h = h; }
|
||||
void setPosition(float x, float y);
|
||||
void setPosition(SDL_FPoint p);
|
||||
void setPosition(SDL_FRect r) { pos_ = r; }
|
||||
void incX(float value) { pos_.x += value; }
|
||||
void incY(float value) { pos_.y += value; }
|
||||
|
||||
// Modificación de clip y surface
|
||||
void setClip(SDL_FRect rect) { clip_ = rect; }
|
||||
void setClip(float x, float y, float w, float h) { clip_ = SDL_FRect{.x = x, .y = y, .w = w, .h = h}; }
|
||||
void setSurface(std::shared_ptr<Surface> surface) { surface_ = std::move(surface); }
|
||||
|
||||
protected:
|
||||
// Variables miembro
|
||||
std::shared_ptr<Surface> surface_{nullptr}; // Surface donde estan todos los dibujos del sprite
|
||||
SDL_FRect pos_{.x = 0.0F, .y = 0.0F, .w = 0.0F, .h = 0.0F}; // Posición y tamaño donde dibujar el sprite
|
||||
SDL_FRect clip_{.x = 0.0F, .y = 0.0F, .w = 0.0F, .h = 0.0F}; // Rectangulo de origen de la surface que se dibujará en pantalla
|
||||
};
|
||||
695
source/core/rendering/surface.cpp
Normal file
695
source/core/rendering/surface.cpp
Normal file
@@ -0,0 +1,695 @@
|
||||
// IWYU pragma: no_include <bits/std_abs.h>
|
||||
#include "core/rendering/surface.hpp"
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include <algorithm> // Para min, max, copy_n, fill
|
||||
#include <cmath> // Para abs
|
||||
#include <cstdint> // Para uint32_t
|
||||
#include <cstring> // Para memcpy, size_t
|
||||
#include <fstream> // Para basic_ifstream, basic_ostream, basic_ist...
|
||||
#include <iostream> // Para cerr
|
||||
#include <memory> // Para shared_ptr, __shared_ptr_access, default...
|
||||
#include <sstream> // Para basic_istringstream
|
||||
#include <stdexcept> // Para runtime_error
|
||||
#include <vector> // Para vector
|
||||
|
||||
#include "core/rendering/gif.hpp" // Para Gif
|
||||
#include "core/rendering/screen.hpp" // Para Screen
|
||||
#include "core/resources/resource_helper.hpp" // Para ResourceHelper
|
||||
|
||||
// Carga una paleta desde un archivo .gif
|
||||
auto loadPalette(const std::string& file_path) -> Palette {
|
||||
// Load file using ResourceHelper (supports both filesystem and pack)
|
||||
auto buffer = Resource::Helper::loadFile(file_path);
|
||||
if (buffer.empty()) {
|
||||
throw std::runtime_error("Error opening file: " + file_path);
|
||||
}
|
||||
|
||||
// Cargar la paleta usando los datos del buffer
|
||||
std::vector<uint32_t> pal = GIF::Gif::loadPalette(buffer.data());
|
||||
if (pal.empty()) {
|
||||
throw std::runtime_error("No palette found in GIF file: " + file_path);
|
||||
}
|
||||
|
||||
// Crear la paleta y copiar los datos desde 'pal'
|
||||
Palette palette = {}; // Inicializa la paleta con ceros
|
||||
std::copy_n(pal.begin(), std::min(pal.size(), palette.size()), palette.begin());
|
||||
|
||||
// Mensaje de depuración
|
||||
printWithDots("Palette : ", file_path.substr(file_path.find_last_of("\\/") + 1), "[ LOADED ]");
|
||||
|
||||
return palette;
|
||||
}
|
||||
|
||||
// Carga una paleta desde un archivo .pal
|
||||
auto readPalFile(const std::string& file_path) -> Palette {
|
||||
Palette palette{};
|
||||
palette.fill(0); // Inicializar todo con 0 (transparente por defecto)
|
||||
|
||||
// Load file using ResourceHelper (supports both filesystem and pack)
|
||||
auto file_data = Resource::Helper::loadFile(file_path);
|
||||
if (file_data.empty()) {
|
||||
throw std::runtime_error("No se pudo abrir el archivo .pal: " + file_path);
|
||||
}
|
||||
|
||||
// Convert bytes to string for parsing
|
||||
std::string content(file_data.begin(), file_data.end());
|
||||
std::istringstream stream(content);
|
||||
|
||||
std::string line;
|
||||
int line_number = 0;
|
||||
int color_index = 0;
|
||||
|
||||
while (std::getline(stream, line)) {
|
||||
++line_number;
|
||||
|
||||
// Ignorar las tres primeras líneas del archivo
|
||||
if (line_number <= 3) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Procesar las líneas restantes con valores RGB
|
||||
std::istringstream ss(line);
|
||||
int r;
|
||||
int g;
|
||||
int b;
|
||||
if (ss >> r >> g >> b) {
|
||||
// Construir el color ARGB (A = 255 por defecto)
|
||||
Uint32 color = (255 << 24) | (r << 16) | (g << 8) | b;
|
||||
palette[color_index++] = color;
|
||||
|
||||
// Limitar a un máximo de 256 colores (opcional)
|
||||
if (color_index >= 256) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
printWithDots("Palette : ", file_path.substr(file_path.find_last_of("\\/") + 1), "[ LOADED ]");
|
||||
return palette;
|
||||
}
|
||||
|
||||
// Constructor
|
||||
Surface::Surface(int w, int h)
|
||||
: surface_data_(std::make_shared<SurfaceData>(w, h)),
|
||||
transparent_color_(static_cast<Uint8>(PaletteColor::TRANSPARENT)) { initializeSubPalette(sub_palette_); }
|
||||
|
||||
Surface::Surface(const std::string& file_path)
|
||||
: transparent_color_(static_cast<Uint8>(PaletteColor::TRANSPARENT)) {
|
||||
SurfaceData loaded_data = loadSurface(file_path);
|
||||
surface_data_ = std::make_shared<SurfaceData>(std::move(loaded_data));
|
||||
|
||||
initializeSubPalette(sub_palette_);
|
||||
}
|
||||
|
||||
// Carga una superficie desde un archivo
|
||||
auto Surface::loadSurface(const std::string& file_path) -> SurfaceData { // NOLINT(readability-convert-member-functions-to-static)
|
||||
// Load file using ResourceHelper (supports both filesystem and pack)
|
||||
std::vector<Uint8> buffer = Resource::Helper::loadFile(file_path);
|
||||
if (buffer.empty()) {
|
||||
std::cerr << "Error opening file: " << file_path << '\n';
|
||||
throw std::runtime_error("Error opening file");
|
||||
}
|
||||
|
||||
// Crear un objeto Gif y llamar a la función loadGif
|
||||
Uint16 w = 0;
|
||||
Uint16 h = 0;
|
||||
std::vector<Uint8> raw_pixels = GIF::Gif::loadGif(buffer.data(), w, h);
|
||||
if (raw_pixels.empty()) {
|
||||
std::cerr << "Error loading GIF from file: " << file_path << '\n';
|
||||
throw std::runtime_error("Error loading GIF");
|
||||
}
|
||||
|
||||
// Si el constructor de Surface espera un std::shared_ptr<Uint8[]>,
|
||||
// reservamos un bloque dinámico y copiamos los datos del vector.
|
||||
size_t pixel_count = raw_pixels.size();
|
||||
auto pixels = std::shared_ptr<Uint8[]>(new Uint8[pixel_count], std::default_delete<Uint8[]>());
|
||||
std::memcpy(pixels.get(), raw_pixels.data(), pixel_count);
|
||||
|
||||
// Crear y devolver directamente el objeto SurfaceData
|
||||
printWithDots("Surface : ", file_path.substr(file_path.find_last_of("\\/") + 1), "[ LOADED ]");
|
||||
return {static_cast<float>(w), static_cast<float>(h), pixels};
|
||||
}
|
||||
|
||||
// Carga una paleta desde un archivo
|
||||
void Surface::loadPalette(const std::string& file_path) {
|
||||
palette_ = ::loadPalette(file_path);
|
||||
}
|
||||
|
||||
// Carga una paleta desde otra paleta
|
||||
void Surface::loadPalette(const Palette& palette) {
|
||||
palette_ = palette;
|
||||
}
|
||||
|
||||
// Establece un color en la paleta
|
||||
void Surface::setColor(int index, Uint32 color) {
|
||||
palette_.at(index) = color;
|
||||
}
|
||||
|
||||
// Rellena la superficie con un color
|
||||
void Surface::clear(Uint8 color) { // NOLINT(readability-convert-member-functions-to-static)
|
||||
const size_t TOTAL_PIXELS = surface_data_->width * surface_data_->height;
|
||||
Uint8* data_ptr = surface_data_->data.get();
|
||||
std::fill(data_ptr, data_ptr + TOTAL_PIXELS, color);
|
||||
}
|
||||
|
||||
// Pone un pixel en la SurfaceData
|
||||
void Surface::putPixel(int x, int y, Uint8 color) { // NOLINT(readability-convert-member-functions-to-static)
|
||||
if (x < 0 || y < 0 || x >= surface_data_->width || y >= surface_data_->height) {
|
||||
return; // Coordenadas fuera de rango
|
||||
}
|
||||
|
||||
const int INDEX = x + (y * surface_data_->width);
|
||||
surface_data_->data.get()[INDEX] = color;
|
||||
}
|
||||
|
||||
// Obtiene el color de un pixel de la surface_data
|
||||
auto Surface::getPixel(int x, int y) -> Uint8 { return surface_data_->data.get()[x + (y * static_cast<int>(surface_data_->width))]; }
|
||||
|
||||
// Dibuja un rectangulo relleno
|
||||
void Surface::fillRect(const SDL_FRect* rect, Uint8 color) { // NOLINT(readability-convert-member-functions-to-static)
|
||||
// Limitar los valores del rectángulo al tamaño de la superficie
|
||||
float x_start = std::max(0.0F, rect->x);
|
||||
float y_start = std::max(0.0F, rect->y);
|
||||
float x_end = std::min(rect->x + rect->w, surface_data_->width);
|
||||
float y_end = std::min(rect->y + rect->h, surface_data_->height);
|
||||
|
||||
// Rellenar fila a fila con memset (memoria contigua por fila)
|
||||
Uint8* data_ptr = surface_data_->data.get();
|
||||
const int SURF_WIDTH = static_cast<int>(surface_data_->width);
|
||||
const int ROW_WIDTH = static_cast<int>(x_end) - static_cast<int>(x_start);
|
||||
for (int y = static_cast<int>(y_start); y < static_cast<int>(y_end); ++y) {
|
||||
std::memset(data_ptr + (y * SURF_WIDTH) + static_cast<int>(x_start), color, ROW_WIDTH);
|
||||
}
|
||||
}
|
||||
|
||||
// Dibuja el borde de un rectangulo
|
||||
void Surface::drawRectBorder(const SDL_FRect* rect, Uint8 color) { // NOLINT(readability-convert-member-functions-to-static)
|
||||
// Limitar los valores del rectángulo al tamaño de la superficie
|
||||
float x_start = std::max(0.0F, rect->x);
|
||||
float y_start = std::max(0.0F, rect->y);
|
||||
float x_end = std::min(rect->x + rect->w, surface_data_->width);
|
||||
float y_end = std::min(rect->y + rect->h, surface_data_->height);
|
||||
|
||||
// Dibujar bordes horizontales con memset (líneas contiguas en memoria)
|
||||
Uint8* data_ptr = surface_data_->data.get();
|
||||
const int SURF_WIDTH = static_cast<int>(surface_data_->width);
|
||||
const int ROW_WIDTH = static_cast<int>(x_end) - static_cast<int>(x_start);
|
||||
std::memset(data_ptr + (static_cast<int>(y_start) * SURF_WIDTH) + static_cast<int>(x_start), color, ROW_WIDTH);
|
||||
std::memset(data_ptr + ((static_cast<int>(y_end) - 1) * SURF_WIDTH) + static_cast<int>(x_start), color, ROW_WIDTH);
|
||||
|
||||
// Dibujar bordes verticales
|
||||
for (int y = y_start; y < y_end; ++y) {
|
||||
// Borde izquierdo
|
||||
const int LEFT_INDEX = x_start + (y * surface_data_->width);
|
||||
surface_data_->data.get()[LEFT_INDEX] = color;
|
||||
|
||||
// Borde derecho
|
||||
const int RIGHT_INDEX = (x_end - 1) + (y * surface_data_->width);
|
||||
surface_data_->data.get()[RIGHT_INDEX] = color;
|
||||
}
|
||||
}
|
||||
|
||||
// Dibuja una linea
|
||||
void Surface::drawLine(float x1, float y1, float x2, float y2, Uint8 color) { // NOLINT(readability-convert-member-functions-to-static)
|
||||
// Calcula las diferencias
|
||||
float dx = std::abs(x2 - x1);
|
||||
float dy = std::abs(y2 - y1);
|
||||
|
||||
// Determina la dirección del incremento
|
||||
float sx = (x1 < x2) ? 1 : -1;
|
||||
float sy = (y1 < y2) ? 1 : -1;
|
||||
|
||||
float err = dx - dy;
|
||||
|
||||
while (true) {
|
||||
// Asegúrate de no dibujar fuera de los límites de la superficie
|
||||
if (x1 >= 0 && x1 < surface_data_->width && y1 >= 0 && y1 < surface_data_->height) {
|
||||
surface_data_->data.get()[static_cast<size_t>(x1 + (y1 * surface_data_->width))] = color;
|
||||
}
|
||||
|
||||
// Si alcanzamos el punto final, salimos
|
||||
if (x1 == x2 && y1 == y2) {
|
||||
break;
|
||||
}
|
||||
|
||||
int e2 = 2 * err;
|
||||
if (e2 > -dy) {
|
||||
err -= dy;
|
||||
x1 += sx;
|
||||
}
|
||||
if (e2 < dx) {
|
||||
err += dx;
|
||||
y1 += sy;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Surface::render(float dx, float dy, float sx, float sy, float w, float h) { // NOLINT(readability-make-member-function-const)
|
||||
auto surface_data = Screen::get()->getRendererSurface()->getSurfaceData();
|
||||
|
||||
// Limitar la región para evitar accesos fuera de rango en origen
|
||||
w = std::min(w, surface_data_->width - sx);
|
||||
h = std::min(h, surface_data_->height - sy);
|
||||
|
||||
// Limitar la región para evitar accesos fuera de rango en destino
|
||||
w = std::min(w, surface_data->width - dx);
|
||||
h = std::min(h, surface_data->height - dy);
|
||||
|
||||
const Uint8* src_ptr = surface_data_->data.get();
|
||||
Uint8* dst_ptr = surface_data->data.get();
|
||||
for (int iy = 0; iy < h; ++iy) {
|
||||
for (int ix = 0; ix < w; ++ix) {
|
||||
// Verificar que las coordenadas de destino están dentro de los límites
|
||||
if (int dest_x = dx + ix; dest_x >= 0 && dest_x < surface_data->width) {
|
||||
if (int dest_y = dy + iy; dest_y >= 0 && dest_y < surface_data->height) {
|
||||
int src_x = sx + ix;
|
||||
int src_y = sy + iy;
|
||||
|
||||
Uint8 color = src_ptr[static_cast<size_t>(src_x + (src_y * surface_data_->width))];
|
||||
if (color != static_cast<Uint8>(transparent_color_)) {
|
||||
dst_ptr[static_cast<size_t>(dest_x + (dest_y * surface_data->width))] = sub_palette_[color];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Surface::render(int x, int y, SDL_FRect* src_rect, SDL_FlipMode flip) { // NOLINT(readability-make-member-function-const)
|
||||
auto surface_data_dest = Screen::get()->getRendererSurface()->getSurfaceData();
|
||||
|
||||
// Determina la región de origen (clip) a renderizar
|
||||
float sx = (src_rect != nullptr) ? src_rect->x : 0;
|
||||
float sy = (src_rect != nullptr) ? src_rect->y : 0;
|
||||
float w = (src_rect != nullptr) ? src_rect->w : surface_data_->width;
|
||||
float h = (src_rect != nullptr) ? src_rect->h : surface_data_->height;
|
||||
|
||||
// Limitar la región para evitar accesos fuera de rango en origen
|
||||
w = std::min(w, surface_data_->width - sx);
|
||||
h = std::min(h, surface_data_->height - sy);
|
||||
w = std::min(w, surface_data_dest->width - x);
|
||||
h = std::min(h, surface_data_dest->height - y);
|
||||
|
||||
// Limitar la región para evitar accesos fuera de rango en destino
|
||||
w = std::min(w, surface_data_dest->width - x);
|
||||
h = std::min(h, surface_data_dest->height - y);
|
||||
|
||||
// Renderiza píxel por píxel aplicando el flip si es necesario
|
||||
const Uint8* src_ptr = surface_data_->data.get();
|
||||
Uint8* dst_ptr = surface_data_dest->data.get();
|
||||
for (int iy = 0; iy < h; ++iy) {
|
||||
for (int ix = 0; ix < w; ++ix) {
|
||||
// Coordenadas de origen
|
||||
int src_x = (flip == SDL_FLIP_HORIZONTAL) ? (sx + w - 1 - ix) : (sx + ix);
|
||||
int src_y = (flip == SDL_FLIP_VERTICAL) ? (sy + h - 1 - iy) : (sy + iy);
|
||||
|
||||
// Coordenadas de destino
|
||||
int dest_x = x + ix;
|
||||
int dest_y = y + iy;
|
||||
|
||||
// Verificar que las coordenadas de destino están dentro de los límites
|
||||
if (dest_x >= 0 && dest_x < surface_data_dest->width && dest_y >= 0 && dest_y < surface_data_dest->height) {
|
||||
// Copia el píxel si no es transparente
|
||||
Uint8 color = src_ptr[static_cast<size_t>(src_x + (src_y * surface_data_->width))];
|
||||
if (color != static_cast<Uint8>(transparent_color_)) {
|
||||
dst_ptr[static_cast<size_t>(dest_x + (dest_y * surface_data_dest->width))] = sub_palette_[color];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Helper para calcular coordenadas con flip
|
||||
void Surface::calculateFlippedCoords(int ix, int iy, float sx, float sy, float w, float h, SDL_FlipMode flip, int& src_x, int& src_y) {
|
||||
src_x = (flip == SDL_FLIP_HORIZONTAL) ? (sx + w - 1 - ix) : (sx + ix);
|
||||
src_y = (flip == SDL_FLIP_VERTICAL) ? (sy + h - 1 - iy) : (sy + iy);
|
||||
}
|
||||
|
||||
// Helper para copiar un pixel si no es transparente
|
||||
void Surface::copyPixelIfNotTransparent(Uint8* dest_data, int dest_x, int dest_y, int dest_width, int src_x, int src_y) const {
|
||||
if (dest_x < 0 || dest_y < 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
Uint8 color = surface_data_->data.get()[static_cast<size_t>(src_x + (src_y * surface_data_->width))];
|
||||
if (color != static_cast<Uint8>(transparent_color_)) {
|
||||
dest_data[dest_x + (dest_y * dest_width)] = sub_palette_[color];
|
||||
}
|
||||
}
|
||||
|
||||
// Copia una región de la superficie de origen a la de destino
|
||||
void Surface::render(SDL_FRect* src_rect, SDL_FRect* dst_rect, SDL_FlipMode flip) {
|
||||
auto surface_data = Screen::get()->getRendererSurface()->getSurfaceData();
|
||||
|
||||
// Si srcRect es nullptr, tomar toda la superficie fuente
|
||||
float sx = (src_rect != nullptr) ? src_rect->x : 0;
|
||||
float sy = (src_rect != nullptr) ? src_rect->y : 0;
|
||||
float sw = (src_rect != nullptr) ? src_rect->w : surface_data_->width;
|
||||
float sh = (src_rect != nullptr) ? src_rect->h : surface_data_->height;
|
||||
|
||||
// Si dstRect es nullptr, asignar las mismas dimensiones que srcRect
|
||||
float dx = (dst_rect != nullptr) ? dst_rect->x : 0;
|
||||
float dy = (dst_rect != nullptr) ? dst_rect->y : 0;
|
||||
float dw = (dst_rect != nullptr) ? dst_rect->w : sw;
|
||||
float dh = (dst_rect != nullptr) ? dst_rect->h : sh;
|
||||
|
||||
// Asegurarse de que srcRect y dstRect tienen las mismas dimensiones
|
||||
if (sw != dw || sh != dh) {
|
||||
dw = sw; // Respetar las dimensiones de srcRect
|
||||
dh = sh;
|
||||
}
|
||||
|
||||
// Limitar la región para evitar accesos fuera de rango en src y dst
|
||||
sw = std::min(sw, surface_data_->width - sx);
|
||||
sh = std::min(sh, surface_data_->height - sy);
|
||||
dw = std::min(dw, surface_data->width - dx);
|
||||
dh = std::min(dh, surface_data->height - dy);
|
||||
|
||||
int final_width = std::min(sw, dw);
|
||||
int final_height = std::min(sh, dh);
|
||||
|
||||
// Renderiza píxel por píxel aplicando el flip si es necesario
|
||||
for (int iy = 0; iy < final_height; ++iy) {
|
||||
for (int ix = 0; ix < final_width; ++ix) {
|
||||
int src_x = 0;
|
||||
int src_y = 0;
|
||||
calculateFlippedCoords(ix, iy, sx, sy, final_width, final_height, flip, src_x, src_y);
|
||||
|
||||
int dest_x = dx + ix;
|
||||
int dest_y = dy + iy;
|
||||
|
||||
// Verificar límites de destino antes de copiar
|
||||
if (dest_x >= 0 && dest_x < surface_data->width && dest_y >= 0 && dest_y < surface_data->height) {
|
||||
copyPixelIfNotTransparent(surface_data->data.get(), dest_x, dest_y, surface_data->width, src_x, src_y);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Copia una región de la SurfaceData de origen a la SurfaceData de destino reemplazando un color por otro
|
||||
void Surface::renderWithColorReplace(int x, int y, Uint8 source_color, Uint8 target_color, SDL_FRect* src_rect, SDL_FlipMode flip) const {
|
||||
auto surface_data = Screen::get()->getRendererSurface()->getSurfaceData();
|
||||
|
||||
// Determina la región de origen (clip) a renderizar
|
||||
float sx = (src_rect != nullptr) ? src_rect->x : 0;
|
||||
float sy = (src_rect != nullptr) ? src_rect->y : 0;
|
||||
float w = (src_rect != nullptr) ? src_rect->w : surface_data_->width;
|
||||
float h = (src_rect != nullptr) ? src_rect->h : surface_data_->height;
|
||||
|
||||
// Limitar la región para evitar accesos fuera de rango
|
||||
w = std::min(w, surface_data_->width - sx);
|
||||
h = std::min(h, surface_data_->height - sy);
|
||||
|
||||
// Renderiza píxel por píxel aplicando el flip si es necesario
|
||||
for (int iy = 0; iy < h; ++iy) {
|
||||
for (int ix = 0; ix < w; ++ix) {
|
||||
// Coordenadas de origen
|
||||
int src_x = (flip == SDL_FLIP_HORIZONTAL) ? (sx + w - 1 - ix) : (sx + ix);
|
||||
int src_y = (flip == SDL_FLIP_VERTICAL) ? (sy + h - 1 - iy) : (sy + iy);
|
||||
|
||||
// Coordenadas de destino
|
||||
int dest_x = x + ix;
|
||||
int dest_y = y + iy;
|
||||
|
||||
// Verifica que las coordenadas de destino estén dentro de los límites
|
||||
if (dest_x < 0 || dest_y < 0 || dest_x >= surface_data->width || dest_y >= surface_data->height) {
|
||||
continue; // Saltar píxeles fuera del rango del destino
|
||||
}
|
||||
|
||||
// Copia el píxel si no es transparente
|
||||
Uint8 color = surface_data_->data.get()[static_cast<size_t>(src_x + (src_y * surface_data_->width))];
|
||||
if (color != static_cast<Uint8>(transparent_color_)) {
|
||||
surface_data->data[dest_x + (dest_y * surface_data->width)] =
|
||||
(color == source_color) ? target_color : color;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Hash 2D estable per a dithering sense flickering
|
||||
static auto pixelThreshold(int col, int row) -> float {
|
||||
auto h = (static_cast<uint32_t>(col) * 2246822519U) ^ (static_cast<uint32_t>(row) * 2654435761U);
|
||||
h ^= (h >> 13);
|
||||
h *= 1274126177U;
|
||||
h ^= (h >> 16);
|
||||
return static_cast<float>(h & 0xFFFFU) / 65536.0F;
|
||||
}
|
||||
|
||||
// Calcula la densidad de fade para un pixel en posición screen_y
|
||||
static auto computeFadeDensity(int screen_y, int fade_h, int canvas_height) -> float {
|
||||
if (screen_y < fade_h) {
|
||||
return static_cast<float>(fade_h - screen_y) / static_cast<float>(fade_h);
|
||||
}
|
||||
if (screen_y >= canvas_height - fade_h) {
|
||||
return static_cast<float>(screen_y - (canvas_height - fade_h)) / static_cast<float>(fade_h);
|
||||
}
|
||||
return 0.0F;
|
||||
}
|
||||
|
||||
// Render amb dissolució als cantons superior/inferior (hash 2D, sense parpelleig)
|
||||
void Surface::renderWithVerticalFade(int x, int y, int fade_h, int canvas_height, SDL_FRect* src_rect) const {
|
||||
const int SX = (src_rect != nullptr) ? static_cast<int>(src_rect->x) : 0;
|
||||
const int SY = (src_rect != nullptr) ? static_cast<int>(src_rect->y) : 0;
|
||||
const int SW = (src_rect != nullptr) ? static_cast<int>(src_rect->w) : static_cast<int>(surface_data_->width);
|
||||
const int SH = (src_rect != nullptr) ? static_cast<int>(src_rect->h) : static_cast<int>(surface_data_->height);
|
||||
|
||||
auto surface_data_dest = Screen::get()->getRendererSurface()->getSurfaceData();
|
||||
|
||||
for (int row = 0; row < SH; row++) {
|
||||
const int SCREEN_Y = y + row;
|
||||
if (SCREEN_Y < 0 || SCREEN_Y >= static_cast<int>(surface_data_dest->height)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const float DENSITY = computeFadeDensity(SCREEN_Y, fade_h, canvas_height);
|
||||
|
||||
for (int col = 0; col < SW; col++) {
|
||||
const int SCREEN_X = x + col;
|
||||
if (SCREEN_X < 0 || SCREEN_X >= static_cast<int>(surface_data_dest->width)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const Uint8 COLOR = surface_data_->data[((SY + row) * static_cast<int>(surface_data_->width)) + (SX + col)];
|
||||
if (COLOR == static_cast<Uint8>(transparent_color_)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (pixelThreshold(col, row) < DENSITY) {
|
||||
continue; // Pixel tapat per la zona de fade
|
||||
}
|
||||
|
||||
surface_data_dest->data[SCREEN_X + (SCREEN_Y * static_cast<int>(surface_data_dest->width))] = sub_palette_[COLOR];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Idem però reemplaçant un color índex
|
||||
void Surface::renderWithVerticalFade(int x, int y, int fade_h, int canvas_height, Uint8 source_color, Uint8 target_color, SDL_FRect* src_rect) const {
|
||||
const int SX = (src_rect != nullptr) ? static_cast<int>(src_rect->x) : 0;
|
||||
const int SY = (src_rect != nullptr) ? static_cast<int>(src_rect->y) : 0;
|
||||
const int SW = (src_rect != nullptr) ? static_cast<int>(src_rect->w) : static_cast<int>(surface_data_->width);
|
||||
const int SH = (src_rect != nullptr) ? static_cast<int>(src_rect->h) : static_cast<int>(surface_data_->height);
|
||||
|
||||
auto surface_data_dest = Screen::get()->getRendererSurface()->getSurfaceData();
|
||||
|
||||
for (int row = 0; row < SH; row++) {
|
||||
const int SCREEN_Y = y + row;
|
||||
if (SCREEN_Y < 0 || SCREEN_Y >= static_cast<int>(surface_data_dest->height)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const float DENSITY = computeFadeDensity(SCREEN_Y, fade_h, canvas_height);
|
||||
|
||||
for (int col = 0; col < SW; col++) {
|
||||
const int SCREEN_X = x + col;
|
||||
if (SCREEN_X < 0 || SCREEN_X >= static_cast<int>(surface_data_dest->width)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const Uint8 COLOR = surface_data_->data[((SY + row) * static_cast<int>(surface_data_->width)) + (SX + col)];
|
||||
if (COLOR == static_cast<Uint8>(transparent_color_)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (pixelThreshold(col, row) < DENSITY) {
|
||||
continue; // Pixel tapat per la zona de fade
|
||||
}
|
||||
|
||||
const Uint8 OUT_COLOR = (COLOR == source_color) ? target_color : sub_palette_[COLOR];
|
||||
surface_data_dest->data[SCREEN_X + (SCREEN_Y * static_cast<int>(surface_data_dest->width))] = OUT_COLOR;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Vuelca los píxeles como ARGB8888 a un buffer externo (sin SDL_Texture ni SDL_Renderer)
|
||||
void Surface::toARGBBuffer(Uint32* buffer) const {
|
||||
if (!surface_data_ || !surface_data_->data || (buffer == nullptr)) { return; }
|
||||
|
||||
const int WIDTH = static_cast<int>(surface_data_->width);
|
||||
const int HEIGHT = static_cast<int>(surface_data_->height);
|
||||
const Uint8* src = surface_data_->data.get();
|
||||
|
||||
// Obtenemos el tamaño de la paleta para evitar accesos fuera de rango
|
||||
const size_t PAL_SIZE = palette_.size();
|
||||
|
||||
for (int i = 0; i < WIDTH * HEIGHT; ++i) {
|
||||
Uint8 color_index = src[i];
|
||||
|
||||
// Verificación de seguridad: ¿El índice existe en la paleta?
|
||||
if (color_index < PAL_SIZE) {
|
||||
buffer[i] = palette_[color_index];
|
||||
} else {
|
||||
buffer[i] = 0xFF000000; // Negro opaco si el índice es erróneo
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Vuelca la superficie a una textura
|
||||
void Surface::copyToTexture(SDL_Renderer* renderer, SDL_Texture* texture) { // NOLINT(readability-convert-member-functions-to-static)
|
||||
if ((renderer == nullptr) || (texture == nullptr) || !surface_data_) {
|
||||
throw std::runtime_error("Renderer or texture is null.");
|
||||
}
|
||||
|
||||
if (surface_data_->width <= 0 || surface_data_->height <= 0 || (surface_data_->data == nullptr)) {
|
||||
throw std::runtime_error("Invalid surface dimensions or data.");
|
||||
}
|
||||
|
||||
Uint32* pixels = nullptr;
|
||||
int pitch = 0;
|
||||
|
||||
// Bloquea la textura para modificar los píxeles directamente
|
||||
if (!SDL_LockTexture(texture, nullptr, reinterpret_cast<void**>(&pixels), &pitch)) {
|
||||
throw std::runtime_error("Failed to lock texture: " + std::string(SDL_GetError()));
|
||||
}
|
||||
|
||||
// Convertir `pitch` de bytes a Uint32 (asegurando alineación correcta en hardware)
|
||||
int row_stride = pitch / sizeof(Uint32);
|
||||
|
||||
// Cachear punteros fuera del bucle para permitir autovectorización SIMD
|
||||
const Uint8* src = surface_data_->data.get();
|
||||
const Uint32* pal = palette_.data();
|
||||
const int WIDTH = surface_data_->width;
|
||||
const int HEIGHT = surface_data_->height;
|
||||
for (int y = 0; y < HEIGHT; ++y) {
|
||||
const Uint8* src_row = src + (y * WIDTH);
|
||||
Uint32* dst_row = pixels + (y * row_stride);
|
||||
for (int x = 0; x < WIDTH; ++x) {
|
||||
dst_row[x] = pal[src_row[x]];
|
||||
}
|
||||
}
|
||||
|
||||
SDL_UnlockTexture(texture); // Desbloquea la textura
|
||||
|
||||
// Renderiza la textura en la pantalla completa
|
||||
if (!SDL_RenderTexture(renderer, texture, nullptr, nullptr)) {
|
||||
throw std::runtime_error("Failed to copy texture to renderer: " + std::string(SDL_GetError()));
|
||||
}
|
||||
}
|
||||
|
||||
// Vuelca la superficie a una textura
|
||||
void Surface::copyToTexture(SDL_Renderer* renderer, SDL_Texture* texture, SDL_FRect* src_rect, SDL_FRect* dest_rect) { // NOLINT(readability-convert-member-functions-to-static)
|
||||
if ((renderer == nullptr) || (texture == nullptr) || !surface_data_) {
|
||||
throw std::runtime_error("Renderer or texture is null.");
|
||||
}
|
||||
|
||||
if (surface_data_->width <= 0 || surface_data_->height <= 0 || (surface_data_->data == nullptr)) {
|
||||
throw std::runtime_error("Invalid surface dimensions or data.");
|
||||
}
|
||||
|
||||
Uint32* pixels = nullptr;
|
||||
int pitch = 0;
|
||||
|
||||
SDL_Rect lock_rect;
|
||||
if (dest_rect != nullptr) {
|
||||
lock_rect.x = static_cast<int>(dest_rect->x);
|
||||
lock_rect.y = static_cast<int>(dest_rect->y);
|
||||
lock_rect.w = static_cast<int>(dest_rect->w);
|
||||
lock_rect.h = static_cast<int>(dest_rect->h);
|
||||
}
|
||||
|
||||
// Usa lockRect solo si destRect no es nulo
|
||||
if (!SDL_LockTexture(texture, (dest_rect != nullptr) ? &lock_rect : nullptr, reinterpret_cast<void**>(&pixels), &pitch)) {
|
||||
throw std::runtime_error("Failed to lock texture: " + std::string(SDL_GetError()));
|
||||
}
|
||||
|
||||
int row_stride = pitch / sizeof(Uint32);
|
||||
|
||||
// Cachear punteros fuera del bucle para permitir autovectorización SIMD
|
||||
const Uint8* src = surface_data_->data.get();
|
||||
const Uint32* pal = palette_.data();
|
||||
const int WIDTH = surface_data_->width;
|
||||
const int HEIGHT = surface_data_->height;
|
||||
for (int y = 0; y < HEIGHT; ++y) {
|
||||
const Uint8* src_row = src + (y * WIDTH);
|
||||
Uint32* dst_row = pixels + (y * row_stride);
|
||||
for (int x = 0; x < WIDTH; ++x) {
|
||||
dst_row[x] = pal[src_row[x]];
|
||||
}
|
||||
}
|
||||
|
||||
SDL_UnlockTexture(texture);
|
||||
|
||||
// Renderiza la textura con los rectángulos especificados
|
||||
if (!SDL_RenderTexture(renderer, texture, src_rect, dest_rect)) {
|
||||
throw std::runtime_error("Failed to copy texture to renderer: " + std::string(SDL_GetError()));
|
||||
}
|
||||
}
|
||||
|
||||
// Realiza un efecto de fundido en la paleta principal
|
||||
auto Surface::fadePalette() -> bool { // NOLINT(readability-convert-member-functions-to-static)
|
||||
// Verificar que el tamaño mínimo de palette_ sea adecuado
|
||||
static constexpr int PALETTE_SIZE = 19;
|
||||
if (sizeof(palette_) / sizeof(palette_[0]) < PALETTE_SIZE) {
|
||||
throw std::runtime_error("Palette size is insufficient for fadePalette operation.");
|
||||
}
|
||||
|
||||
// Desplazar colores (pares e impares)
|
||||
for (int i = 18; i > 1; --i) {
|
||||
palette_[i] = palette_[i - 2];
|
||||
}
|
||||
|
||||
// Ajustar el primer color
|
||||
palette_[1] = palette_[0];
|
||||
|
||||
// Devolver si el índice 15 coincide con el índice 0
|
||||
return palette_[15] == palette_[0];
|
||||
}
|
||||
|
||||
// Realiza un efecto de fundido en la paleta secundaria
|
||||
auto Surface::fadeSubPalette(Uint32 delay) -> bool { // NOLINT(readability-convert-member-functions-to-static)
|
||||
// Variable estática para almacenar el último tick
|
||||
static Uint32 last_tick_ = 0;
|
||||
|
||||
// Obtener el tiempo actual
|
||||
Uint32 current_tick = SDL_GetTicks();
|
||||
|
||||
// Verificar si ha pasado el tiempo de retardo
|
||||
if (current_tick - last_tick_ < delay) {
|
||||
return false; // No se realiza el fade
|
||||
}
|
||||
|
||||
// Actualizar el último tick
|
||||
last_tick_ = current_tick;
|
||||
|
||||
// Verificar que el tamaño mínimo de sub_palette_ sea adecuado
|
||||
static constexpr int SUB_PALETTE_SIZE = 19;
|
||||
if (sizeof(sub_palette_) / sizeof(sub_palette_[0]) < SUB_PALETTE_SIZE) {
|
||||
throw std::runtime_error("Palette size is insufficient for fadePalette operation.");
|
||||
}
|
||||
|
||||
// Desplazar colores (pares e impares)
|
||||
for (int i = 18; i > 1; --i) {
|
||||
sub_palette_[i] = sub_palette_[i - 2];
|
||||
}
|
||||
|
||||
// Ajustar el primer color
|
||||
sub_palette_[1] = sub_palette_[0];
|
||||
|
||||
// Devolver si el índice 15 coincide con el índice 0
|
||||
return sub_palette_[15] == sub_palette_[0];
|
||||
}
|
||||
|
||||
// Restaura la sub paleta a su estado original
|
||||
void Surface::resetSubPalette() { initializeSubPalette(sub_palette_); } // NOLINT(readability-convert-member-functions-to-static)
|
||||
153
source/core/rendering/surface.hpp
Normal file
153
source/core/rendering/surface.hpp
Normal file
@@ -0,0 +1,153 @@
|
||||
#pragma once
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include <array> // Para array
|
||||
#include <memory> // Para default_delete, shared_ptr, __shared_pt...
|
||||
#include <numeric> // Para iota
|
||||
#include <string> // Para string
|
||||
#include <utility> // Para move
|
||||
|
||||
#include "utils/utils.hpp" // Para PaletteColor
|
||||
|
||||
// Alias
|
||||
using Palette = std::array<Uint32, 256>;
|
||||
using SubPalette = std::array<Uint8, 256>;
|
||||
|
||||
// Carga una paleta desde un archivo .gif
|
||||
auto loadPalette(const std::string& file_path) -> Palette;
|
||||
|
||||
// Carga una paleta desde un archivo .pal
|
||||
auto readPalFile(const std::string& file_path) -> Palette;
|
||||
|
||||
struct SurfaceData {
|
||||
std::shared_ptr<Uint8[]> data; // Usa std::shared_ptr para gestión automática
|
||||
float width; // Ancho de la imagen
|
||||
float height; // Alto de la imagen
|
||||
|
||||
// Constructor por defecto
|
||||
SurfaceData()
|
||||
: data(nullptr),
|
||||
width(0),
|
||||
height(0) {}
|
||||
|
||||
// Constructor que inicializa dimensiones y asigna memoria
|
||||
SurfaceData(float w, float h)
|
||||
: data(std::shared_ptr<Uint8[]>(new Uint8[static_cast<size_t>(w * h)](), std::default_delete<Uint8[]>())),
|
||||
width(w),
|
||||
height(h) {}
|
||||
|
||||
// Constructor para inicializar directamente con datos
|
||||
SurfaceData(float w, float h, std::shared_ptr<Uint8[]> pixels)
|
||||
: data(std::move(pixels)),
|
||||
width(w),
|
||||
height(h) {}
|
||||
|
||||
// Constructor de movimiento
|
||||
SurfaceData(SurfaceData&& other) noexcept = default;
|
||||
|
||||
// Operador de movimiento
|
||||
auto operator=(SurfaceData&& other) noexcept -> SurfaceData& = default;
|
||||
|
||||
// Evita copias accidentales
|
||||
SurfaceData(const SurfaceData&) = delete;
|
||||
auto operator=(const SurfaceData&) -> SurfaceData& = delete;
|
||||
};
|
||||
|
||||
class Surface {
|
||||
private:
|
||||
std::shared_ptr<SurfaceData> surface_data_; // Datos a dibujar
|
||||
Palette palette_; // Paleta para volcar la SurfaceData a una Textura
|
||||
SubPalette sub_palette_; // Paleta para reindexar colores
|
||||
int transparent_color_; // Indice de la paleta que se omite en la copia de datos
|
||||
|
||||
public:
|
||||
// Constructor
|
||||
Surface(int w, int h);
|
||||
explicit Surface(const std::string& file_path);
|
||||
|
||||
// Destructor
|
||||
~Surface() = default;
|
||||
|
||||
// Carga una SurfaceData desde un archivo
|
||||
static auto loadSurface(const std::string& file_path) -> SurfaceData;
|
||||
|
||||
// Carga una paleta desde un archivo
|
||||
void loadPalette(const std::string& file_path);
|
||||
void loadPalette(const Palette& palette);
|
||||
|
||||
// Copia una región de la SurfaceData de origen a la SurfaceData de destino
|
||||
void render(float dx, float dy, float sx, float sy, float w, float h);
|
||||
void render(int x, int y, SDL_FRect* src_rect = nullptr, SDL_FlipMode flip = SDL_FLIP_NONE);
|
||||
void render(SDL_FRect* src_rect = nullptr, SDL_FRect* dst_rect = nullptr, SDL_FlipMode flip = SDL_FLIP_NONE);
|
||||
|
||||
// Copia una región de la SurfaceData de origen a la SurfaceData de destino reemplazando un color por otro
|
||||
void renderWithColorReplace(int x, int y, Uint8 source_color = 0, Uint8 target_color = 0, SDL_FRect* src_rect = nullptr, SDL_FlipMode flip = SDL_FLIP_NONE) const;
|
||||
|
||||
// Render amb dissolució als cantons superior/inferior (hash 2D, sense parpelleig)
|
||||
void renderWithVerticalFade(int x, int y, int fade_h, int canvas_height, SDL_FRect* src_rect = nullptr) const;
|
||||
|
||||
// Idem però reemplaçant un color índex (per a sprites sobre fons del mateix color)
|
||||
void renderWithVerticalFade(int x, int y, int fade_h, int canvas_height, Uint8 source_color, Uint8 target_color, SDL_FRect* src_rect = nullptr) const;
|
||||
|
||||
// Establece un color en la paleta
|
||||
void setColor(int index, Uint32 color);
|
||||
|
||||
// Rellena la SurfaceData con un color
|
||||
void clear(Uint8 color);
|
||||
|
||||
// Vuelca la SurfaceData a una textura
|
||||
void copyToTexture(SDL_Renderer* renderer, SDL_Texture* texture);
|
||||
void copyToTexture(SDL_Renderer* renderer, SDL_Texture* texture, SDL_FRect* src_rect, SDL_FRect* dest_rect);
|
||||
|
||||
// Realiza un efecto de fundido en las paletas
|
||||
auto fadePalette() -> bool;
|
||||
auto fadeSubPalette(Uint32 delay = 0) -> bool;
|
||||
|
||||
// Restaura la sub paleta a su estado original
|
||||
void resetSubPalette();
|
||||
|
||||
// Vuelca los píxeles como ARGB8888 a un buffer externo (sin SDL_Texture)
|
||||
void toARGBBuffer(Uint32* buffer) const;
|
||||
|
||||
// Pone un pixel en la SurfaceData
|
||||
void putPixel(int x, int y, Uint8 color);
|
||||
|
||||
// Obtiene el color de un pixel de la surface_data
|
||||
auto getPixel(int x, int y) -> Uint8;
|
||||
|
||||
// Dibuja un rectangulo relleno
|
||||
void fillRect(const SDL_FRect* rect, Uint8 color);
|
||||
|
||||
// Dibuja el borde de un rectangulo
|
||||
void drawRectBorder(const SDL_FRect* rect, Uint8 color);
|
||||
|
||||
// Dibuja una linea
|
||||
void drawLine(float x1, float y1, float x2, float y2, Uint8 color);
|
||||
|
||||
// Metodos para gestionar surface_data_
|
||||
[[nodiscard]] auto getSurfaceData() const -> std::shared_ptr<SurfaceData> { return surface_data_; }
|
||||
void setSurfaceData(std::shared_ptr<SurfaceData> new_data) { surface_data_ = std::move(new_data); }
|
||||
|
||||
// Obtien ancho y alto
|
||||
[[nodiscard]] auto getWidth() const -> float { return surface_data_->width; }
|
||||
[[nodiscard]] auto getHeight() const -> float { return surface_data_->height; }
|
||||
|
||||
// Color transparente
|
||||
[[nodiscard]] auto getTransparentColor() const -> Uint8 { return transparent_color_; }
|
||||
void setTransparentColor(Uint8 color = 255) { transparent_color_ = color; }
|
||||
|
||||
// Paleta
|
||||
void setPalette(const std::array<Uint32, 256>& palette) { palette_ = palette; }
|
||||
[[nodiscard]] auto getPaletteColor(Uint8 index) const -> Uint32 { return palette_[index]; }
|
||||
|
||||
// Inicializa la sub paleta
|
||||
static void initializeSubPalette(SubPalette& palette) { std::iota(palette.begin(), palette.end(), 0); }
|
||||
|
||||
private:
|
||||
// Helper para calcular coordenadas con flip
|
||||
static void calculateFlippedCoords(int ix, int iy, float sx, float sy, float w, float h, SDL_FlipMode flip, int& src_x, int& src_y);
|
||||
|
||||
// Helper para copiar un pixel si no es transparente
|
||||
void copyPixelIfNotTransparent(Uint8* dest_data, int dest_x, int dest_y, int dest_width, int src_x, int src_y) const;
|
||||
};
|
||||
311
source/core/rendering/text.cpp
Normal file
311
source/core/rendering/text.cpp
Normal file
@@ -0,0 +1,311 @@
|
||||
#include "core/rendering/text.hpp"
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include <cstddef> // Para size_t
|
||||
#include <iostream> // Para cerr
|
||||
#include <sstream> // Para istringstream
|
||||
#include <stdexcept> // Para runtime_error
|
||||
|
||||
#include "core/rendering/screen.hpp" // Para Screen
|
||||
#include "core/rendering/sprite/sprite.hpp" // Para SSprite
|
||||
#include "core/rendering/surface.hpp" // Para Surface
|
||||
#include "core/resources/resource_helper.hpp" // Para ResourceHelper
|
||||
#include "utils/utils.hpp" // Para getFileName, stringToColor, printWithDots
|
||||
|
||||
// Extrae el siguiente codepoint UTF-8 de la cadena, avanzando 'pos' al byte siguiente
|
||||
auto Text::nextCodepoint(const std::string& s, size_t& pos) -> uint32_t { // NOLINT(readability-convert-member-functions-to-static)
|
||||
auto c = static_cast<unsigned char>(s[pos]);
|
||||
uint32_t cp = 0;
|
||||
size_t extra = 0;
|
||||
|
||||
if (c < 0x80) {
|
||||
cp = c;
|
||||
extra = 0;
|
||||
} else if (c < 0xC0) {
|
||||
pos++;
|
||||
return 0xFFFD;
|
||||
} // byte de continuación suelto
|
||||
else if (c < 0xE0) {
|
||||
cp = c & 0x1F;
|
||||
extra = 1;
|
||||
} else if (c < 0xF0) {
|
||||
cp = c & 0x0F;
|
||||
extra = 2;
|
||||
} else if (c < 0xF8) {
|
||||
cp = c & 0x07;
|
||||
extra = 3;
|
||||
} else {
|
||||
pos++;
|
||||
return 0xFFFD;
|
||||
}
|
||||
|
||||
pos++;
|
||||
for (size_t i = 0; i < extra && pos < s.size(); ++i, ++pos) {
|
||||
auto cb = static_cast<unsigned char>(s[pos]);
|
||||
if ((cb & 0xC0) != 0x80) { return 0xFFFD; }
|
||||
cp = (cp << 6) | (cb & 0x3F);
|
||||
}
|
||||
return cp;
|
||||
}
|
||||
|
||||
// Convierte un codepoint Unicode a una cadena UTF-8
|
||||
auto Text::codepointToUtf8(uint32_t cp) -> std::string { // NOLINT(readability-convert-member-functions-to-static)
|
||||
std::string result;
|
||||
if (cp < 0x80) {
|
||||
result += static_cast<char>(cp);
|
||||
} else if (cp < 0x800) {
|
||||
result += static_cast<char>(0xC0 | (cp >> 6));
|
||||
result += static_cast<char>(0x80 | (cp & 0x3F));
|
||||
} else if (cp < 0x10000) {
|
||||
result += static_cast<char>(0xE0 | (cp >> 12));
|
||||
result += static_cast<char>(0x80 | ((cp >> 6) & 0x3F));
|
||||
result += static_cast<char>(0x80 | (cp & 0x3F));
|
||||
} else {
|
||||
result += static_cast<char>(0xF0 | (cp >> 18));
|
||||
result += static_cast<char>(0x80 | ((cp >> 12) & 0x3F));
|
||||
result += static_cast<char>(0x80 | ((cp >> 6) & 0x3F));
|
||||
result += static_cast<char>(0x80 | (cp & 0x3F));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// Carga un fichero de definición de fuente .fnt
|
||||
// Formato: líneas "clave valor", comentarios con #, gliphos como "codepoint ancho"
|
||||
auto Text::loadTextFile(const std::string& file_path) -> std::shared_ptr<File> { // NOLINT(readability-convert-member-functions-to-static)
|
||||
auto tf = std::make_shared<File>();
|
||||
|
||||
auto file_data = Resource::Helper::loadFile(file_path);
|
||||
if (file_data.empty()) {
|
||||
std::cerr << "Error: Fichero no encontrado " << getFileName(file_path) << '\n';
|
||||
throw std::runtime_error("Fichero no encontrado: " + getFileName(file_path));
|
||||
}
|
||||
|
||||
std::string content(file_data.begin(), file_data.end());
|
||||
std::istringstream stream(content);
|
||||
std::string line;
|
||||
int glyph_index = 0;
|
||||
|
||||
while (std::getline(stream, line)) {
|
||||
if (!line.empty() && line.back() == '\r') { line.pop_back(); }
|
||||
if (line.empty() || line[0] == '#') { continue; }
|
||||
|
||||
std::istringstream ls(line);
|
||||
std::string key;
|
||||
ls >> key;
|
||||
|
||||
if (key == "box_width") {
|
||||
ls >> tf->box_width;
|
||||
} else if (key == "box_height") {
|
||||
ls >> tf->box_height;
|
||||
} else if (key == "columns") {
|
||||
ls >> tf->columns;
|
||||
} else if (key == "cell_spacing") {
|
||||
ls >> tf->cell_spacing;
|
||||
} else if (key == "row_spacing") {
|
||||
ls >> tf->row_spacing;
|
||||
} else {
|
||||
// Línea de glifo: codepoint_decimal ancho_visual
|
||||
uint32_t codepoint = 0;
|
||||
int width = 0;
|
||||
try {
|
||||
codepoint = static_cast<uint32_t>(std::stoul(key));
|
||||
ls >> width;
|
||||
} catch (...) {
|
||||
continue; // línea mal formateada, ignorar
|
||||
}
|
||||
Offset off{};
|
||||
const int ROW_SP = tf->row_spacing > 0 ? tf->row_spacing : tf->cell_spacing;
|
||||
off.x = ((glyph_index % tf->columns) * (tf->box_width + tf->cell_spacing)) + tf->cell_spacing;
|
||||
off.y = ((glyph_index / tf->columns) * (tf->box_height + ROW_SP)) + tf->cell_spacing;
|
||||
off.w = width;
|
||||
tf->offset[codepoint] = off;
|
||||
++glyph_index;
|
||||
}
|
||||
}
|
||||
|
||||
printWithDots("Text File : ", getFileName(file_path), "[ LOADED ]");
|
||||
return tf;
|
||||
}
|
||||
|
||||
// Constructor desde fichero
|
||||
Text::Text(const std::shared_ptr<Surface>& surface, const std::string& text_file) {
|
||||
auto tf = loadTextFile(text_file);
|
||||
|
||||
box_height_ = tf->box_height;
|
||||
box_width_ = tf->box_width;
|
||||
offset_ = tf->offset;
|
||||
|
||||
sprite_ = std::make_unique<Sprite>(surface, SDL_FRect{.x = 0.0F, .y = 0.0F, .w = static_cast<float>(box_width_), .h = static_cast<float>(box_height_)});
|
||||
}
|
||||
|
||||
// Constructor desde estructura precargada
|
||||
Text::Text(const std::shared_ptr<Surface>& surface, const std::shared_ptr<File>& text_file)
|
||||
: sprite_(std::make_unique<Sprite>(surface, SDL_FRect{.x = 0.0F, .y = 0.0F, .w = static_cast<float>(text_file->box_width), .h = static_cast<float>(text_file->box_height)})),
|
||||
box_width_(text_file->box_width),
|
||||
box_height_(text_file->box_height),
|
||||
offset_(text_file->offset) {
|
||||
}
|
||||
|
||||
// Escribe texto en pantalla
|
||||
void Text::write(int x, int y, const std::string& text, int kerning, int lenght) { // NOLINT(readability-convert-member-functions-to-static)
|
||||
int shift = 0;
|
||||
int glyphs_done = 0;
|
||||
size_t pos = 0;
|
||||
|
||||
sprite_->setY(y);
|
||||
while (pos < text.size()) {
|
||||
if (lenght != -1 && glyphs_done >= lenght) { break; }
|
||||
uint32_t cp = nextCodepoint(text, pos);
|
||||
auto it = offset_.find(cp);
|
||||
if (it == offset_.end()) { it = offset_.find('?'); }
|
||||
if (it != offset_.end()) {
|
||||
sprite_->setClip(it->second.x, it->second.y, box_width_, box_height_);
|
||||
sprite_->setX(x + shift);
|
||||
sprite_->render(1, 15);
|
||||
shift += it->second.w + kerning;
|
||||
}
|
||||
++glyphs_done;
|
||||
}
|
||||
}
|
||||
|
||||
// Escribe el texto en una surface
|
||||
auto Text::writeToSurface(const std::string& text, int zoom, int kerning) -> std::shared_ptr<Surface> { // NOLINT(readability-make-member-function-const)
|
||||
auto width = length(text, kerning) * zoom;
|
||||
auto height = box_height_ * zoom;
|
||||
auto surface = std::make_shared<Surface>(width, height);
|
||||
auto previuos_renderer = Screen::get()->getRendererSurface();
|
||||
Screen::get()->setRendererSurface(surface);
|
||||
surface->clear(stringToColor("transparent"));
|
||||
write(0, 0, text, kerning);
|
||||
Screen::get()->setRendererSurface(previuos_renderer);
|
||||
|
||||
return surface;
|
||||
}
|
||||
|
||||
// Escribe el texto con extras en una surface
|
||||
auto Text::writeDXToSurface(Uint8 flags, const std::string& text, int kerning, Uint8 text_color, Uint8 shadow_distance, Uint8 shadow_color, int lenght) -> std::shared_ptr<Surface> { // NOLINT(readability-make-member-function-const)
|
||||
auto width = Text::length(text, kerning) + shadow_distance;
|
||||
auto height = box_height_ + shadow_distance;
|
||||
auto surface = std::make_shared<Surface>(width, height);
|
||||
auto previuos_renderer = Screen::get()->getRendererSurface();
|
||||
Screen::get()->setRendererSurface(surface);
|
||||
surface->clear(stringToColor("transparent"));
|
||||
writeDX(flags, 0, 0, text, kerning, text_color, shadow_distance, shadow_color, lenght);
|
||||
Screen::get()->setRendererSurface(previuos_renderer);
|
||||
|
||||
return surface;
|
||||
}
|
||||
|
||||
// Escribe el texto con colores
|
||||
void Text::writeColored(int x, int y, const std::string& text, Uint8 color, int kerning, int lenght) { // NOLINT(readability-convert-member-functions-to-static)
|
||||
int shift = 0;
|
||||
int glyphs_done = 0;
|
||||
size_t pos = 0;
|
||||
|
||||
sprite_->setY(y);
|
||||
while (pos < text.size()) {
|
||||
if (lenght != -1 && glyphs_done >= lenght) { break; }
|
||||
uint32_t cp = nextCodepoint(text, pos);
|
||||
auto it = offset_.find(cp);
|
||||
if (it == offset_.end()) { it = offset_.find('?'); }
|
||||
if (it != offset_.end()) {
|
||||
sprite_->setClip(it->second.x, it->second.y, box_width_, box_height_);
|
||||
sprite_->setX(x + shift);
|
||||
sprite_->render(1, color);
|
||||
shift += it->second.w + kerning;
|
||||
}
|
||||
++glyphs_done;
|
||||
}
|
||||
}
|
||||
|
||||
// Escribe el texto con sombra
|
||||
void Text::writeShadowed(int x, int y, const std::string& text, Uint8 color, Uint8 shadow_distance, int kerning, int lenght) {
|
||||
writeColored(x + shadow_distance, y + shadow_distance, text, color, kerning, lenght);
|
||||
write(x, y, text, kerning, lenght);
|
||||
}
|
||||
|
||||
// Escribe el texto centrado en un punto x
|
||||
void Text::writeCentered(int x, int y, const std::string& text, int kerning, int lenght) {
|
||||
x -= (Text::length(text, kerning) / 2);
|
||||
write(x, y, text, kerning, lenght);
|
||||
}
|
||||
|
||||
// Escribe texto con extras
|
||||
void Text::writeDX(Uint8 flags, int x, int y, const std::string& text, int kerning, Uint8 text_color, Uint8 shadow_distance, Uint8 shadow_color, int lenght) { // NOLINT(readability-convert-member-functions-to-static)
|
||||
const auto CENTERED = ((flags & CENTER_FLAG) == CENTER_FLAG);
|
||||
const auto SHADOWED = ((flags & SHADOW_FLAG) == SHADOW_FLAG);
|
||||
const auto COLORED = ((flags & COLOR_FLAG) == COLOR_FLAG);
|
||||
const auto STROKED = ((flags & STROKE_FLAG) == STROKE_FLAG);
|
||||
|
||||
if (CENTERED) {
|
||||
x -= (Text::length(text, kerning) / 2);
|
||||
}
|
||||
|
||||
if (SHADOWED) {
|
||||
writeColored(x + shadow_distance, y + shadow_distance, text, shadow_color, kerning, lenght);
|
||||
}
|
||||
|
||||
if (STROKED) {
|
||||
const int MAX_DIST = static_cast<int>(shadow_distance);
|
||||
for (int dist = 1; dist <= MAX_DIST; ++dist) {
|
||||
for (int dy = -dist; dy <= dist; ++dy) {
|
||||
for (int dx = -dist; dx <= dist; ++dx) {
|
||||
writeColored(x + dx, y + dy, text, shadow_color, kerning, lenght);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (COLORED) {
|
||||
writeColored(x, y, text, text_color, kerning, lenght);
|
||||
} else {
|
||||
writeColored(x, y, text, text_color, kerning, lenght);
|
||||
}
|
||||
}
|
||||
|
||||
// Obtiene la longitud en pixels de una cadena UTF-8
|
||||
auto Text::length(const std::string& text, int kerning) const -> int { // NOLINT(readability-convert-member-functions-to-static)
|
||||
int shift = 0;
|
||||
size_t pos = 0;
|
||||
|
||||
while (pos < text.size()) {
|
||||
uint32_t cp = nextCodepoint(text, pos);
|
||||
auto it = offset_.find(cp);
|
||||
if (it == offset_.end()) { it = offset_.find('?'); }
|
||||
if (it != offset_.end()) {
|
||||
shift += it->second.w + kerning;
|
||||
}
|
||||
}
|
||||
|
||||
return shift > 0 ? shift - kerning : 0;
|
||||
}
|
||||
|
||||
// Devuelve el ancho en pixels de un glifo dado su codepoint Unicode
|
||||
auto Text::glyphWidth(uint32_t codepoint, int kerning) const -> int { // NOLINT(readability-convert-member-functions-to-static)
|
||||
auto it = offset_.find(codepoint);
|
||||
if (it == offset_.end()) { it = offset_.find('?'); }
|
||||
if (it != offset_.end()) { return it->second.w + kerning; }
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Devuelve el clip rect (región en el bitmap) de un glifo dado su codepoint
|
||||
auto Text::getGlyphClip(uint32_t codepoint) const -> SDL_FRect {
|
||||
auto it = offset_.find(codepoint);
|
||||
if (it == offset_.end()) { it = offset_.find('?'); }
|
||||
if (it == offset_.end()) { return {.x = 0.0F, .y = 0.0F, .w = 0.0F, .h = 0.0F}; }
|
||||
return {.x = static_cast<float>(it->second.x),
|
||||
.y = static_cast<float>(it->second.y),
|
||||
.w = static_cast<float>(box_width_),
|
||||
.h = static_cast<float>(box_height_)};
|
||||
}
|
||||
|
||||
// Devuelve el tamaño de la caja de cada caracter
|
||||
auto Text::getCharacterSize() const -> int {
|
||||
return box_width_;
|
||||
}
|
||||
|
||||
// Establece si se usa un tamaño fijo de letra
|
||||
void Text::setFixedWidth(bool value) {
|
||||
fixed_width_ = value;
|
||||
}
|
||||
72
source/core/rendering/text.hpp
Normal file
72
source/core/rendering/text.hpp
Normal file
@@ -0,0 +1,72 @@
|
||||
#pragma once
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include <memory> // Para shared_ptr, unique_ptr
|
||||
#include <string> // Para string
|
||||
#include <unordered_map> // Para unordered_map
|
||||
|
||||
#include "core/rendering/sprite/sprite.hpp" // Para SSprite
|
||||
class Surface; // Forward declaration
|
||||
|
||||
// Clase texto. Pinta texto en pantalla a partir de un bitmap con soporte UTF-8
|
||||
class Text {
|
||||
public:
|
||||
// Tipos anidados públicos
|
||||
struct Offset {
|
||||
int x{0}, y{0}, w{0};
|
||||
};
|
||||
|
||||
struct File {
|
||||
int box_width{0}; // Anchura de la caja de cada caracter en el png
|
||||
int box_height{0}; // Altura de la caja de cada caracter en el png
|
||||
int columns{16}; // Número de columnas en el bitmap
|
||||
int cell_spacing{0}; // Píxeles de separación entre columnas (y borde izquierdo/superior)
|
||||
int row_spacing{0}; // Píxeles de separación entre filas (si difiere de cell_spacing)
|
||||
std::unordered_map<uint32_t, Offset> offset; // Posición y ancho de cada glifo (clave: codepoint Unicode)
|
||||
};
|
||||
|
||||
// Constructor
|
||||
Text(const std::shared_ptr<Surface>& surface, const std::string& text_file);
|
||||
Text(const std::shared_ptr<Surface>& surface, const std::shared_ptr<File>& text_file);
|
||||
|
||||
// Destructor
|
||||
~Text() = default;
|
||||
|
||||
// Constantes de flags para writeDX
|
||||
static constexpr int COLOR_FLAG = 1;
|
||||
static constexpr int SHADOW_FLAG = 2;
|
||||
static constexpr int CENTER_FLAG = 4;
|
||||
static constexpr int STROKE_FLAG = 8;
|
||||
|
||||
void write(int x, int y, const std::string& text, int kerning = 1, int lenght = -1); // Escribe el texto en pantalla
|
||||
void writeColored(int x, int y, const std::string& text, Uint8 color, int kerning = 1, int lenght = -1); // Escribe el texto con colores
|
||||
void writeShadowed(int x, int y, const std::string& text, Uint8 color, Uint8 shadow_distance = 1, int kerning = 1, int lenght = -1); // Escribe el texto con sombra
|
||||
void writeCentered(int x, int y, const std::string& text, int kerning = 1, int lenght = -1); // Escribe el texto centrado en un punto x
|
||||
void writeDX(Uint8 flags, int x, int y, const std::string& text, int kerning = 1, Uint8 text_color = Uint8(), Uint8 shadow_distance = 1, Uint8 shadow_color = Uint8(), int lenght = -1); // Escribe texto con extras
|
||||
|
||||
auto writeToSurface(const std::string& text, int zoom = 1, int kerning = 1) -> std::shared_ptr<Surface>; // Escribe el texto en una textura
|
||||
auto writeDXToSurface(Uint8 flags, const std::string& text, int kerning = 1, Uint8 text_color = Uint8(), Uint8 shadow_distance = 1, Uint8 shadow_color = Uint8(), int lenght = -1) -> std::shared_ptr<Surface>; // Escribe el texto con extras en una textura
|
||||
|
||||
[[nodiscard]] auto length(const std::string& text, int kerning = 1) const -> int; // Obtiene la longitud en pixels de una cadena
|
||||
[[nodiscard]] auto getCharacterSize() const -> int; // Devuelve el tamaño del caracter
|
||||
[[nodiscard]] auto glyphWidth(uint32_t codepoint, int kerning = 0) const -> int; // Devuelve el ancho en pixels de un glifo
|
||||
[[nodiscard]] auto getGlyphClip(uint32_t codepoint) const -> SDL_FRect; // Devuelve el clip rect del glifo
|
||||
[[nodiscard]] auto getSprite() const -> Sprite* { return sprite_.get(); } // Acceso al sprite interno
|
||||
|
||||
void setFixedWidth(bool value); // Establece si se usa un tamaño fijo de letra
|
||||
|
||||
static auto loadTextFile(const std::string& file_path) -> std::shared_ptr<File>; // Carga un fichero de definición de fuente .fnt
|
||||
static auto codepointToUtf8(uint32_t cp) -> std::string; // Convierte un codepoint Unicode a string UTF-8
|
||||
static auto nextCodepoint(const std::string& s, size_t& pos) -> uint32_t; // Extrae el siguiente codepoint UTF-8
|
||||
|
||||
private:
|
||||
// Objetos y punteros
|
||||
std::unique_ptr<Sprite> sprite_ = nullptr; // Objeto con los graficos para el texto
|
||||
|
||||
// Variables
|
||||
int box_width_ = 0; // Anchura de la caja de cada caracter en el png
|
||||
int box_height_ = 0; // Altura de la caja de cada caracter en el png
|
||||
bool fixed_width_ = false; // Indica si el texto se ha de escribir con longitud fija
|
||||
std::unordered_map<uint32_t, Offset> offset_; // Posición y ancho de cada glifo (clave: codepoint Unicode)
|
||||
};
|
||||
Reference in New Issue
Block a user