Files
shadow-over-bishkek001/src/render/TextureManager.cpp
T

577 lines
16 KiB
C++

#include "render/TextureManager.h"
#include "render/OpenGlExtensions.h"
#ifdef PNG_ENABLED
#include "png.h"
#endif
#include <iostream>
namespace FRG
{
#ifdef EMSCRIPTEN
using std::min;
using std::max;
#endif
#ifdef __linux__
using std::min;
using std::max;
#endif
bool IsPowerOf2(size_t n) {
return n > 0 && (n & (n - 1)) == 0;
}
size_t NextPowerOf2(size_t n) {
if (n == 0) return 1;
--n;
n |= n >> 1; n |= n >> 2; n |= n >> 4; n |= n >> 8; n |= n >> 16;
#if SIZE_MAX > 0xFFFFFFFF
n |= n >> 32;
#endif
return n + 1;
}
TextureDataStruct ResizeTextureDataToPOT(const TextureDataStruct& src) {
size_t newW = IsPowerOf2(src.width) ? src.width : NextPowerOf2(src.width);
size_t newH = IsPowerOf2(src.height) ? src.height : NextPowerOf2(src.height);
if (newW == src.width && newH == src.height) return src;
int channels = (src.format == TextureDataStruct::R8) ? 1
: (src.format == TextureDataStruct::RGB) ? 3 : 4;
TextureDataStruct dst = src;
dst.width = newW;
dst.height = newH;
dst.data.resize(newW * newH * channels);
for (size_t y = 0; y < newH; ++y) {
for (size_t x = 0; x < newW; ++x) {
float fx = (newW > 1) ? (float)x * (float)(src.width - 1) / (float)(newW - 1) : 0.f;
float fy = (newH > 1) ? (float)y * (float)(src.height - 1) / (float)(newH - 1) : 0.f;
size_t x0 = (size_t)fx, y0 = (size_t)fy;
size_t x1 = min(x0 + 1, src.width - 1);
size_t y1 = min(y0 + 1, src.height - 1);
float tx = fx - (float)x0, ty = fy - (float)y0;
for (int c = 0; c < channels; ++c) {
float v00 = (float)(unsigned char)src.data[(y0 * src.width + x0) * channels + c];
float v10 = (float)(unsigned char)src.data[(y0 * src.width + x1) * channels + c];
float v01 = (float)(unsigned char)src.data[(y1 * src.width + x0) * channels + c];
float v11 = (float)(unsigned char)src.data[(y1 * src.width + x1) * channels + c];
float val = v00 * (1.f - tx) * (1.f - ty) + v10 * tx * (1.f - ty)
+ v01 * (1.f - tx) * ty + v11 * tx * ty;
dst.data[(y * newW + x) * channels + c] = (char)(unsigned char)(val + 0.5f);
}
}
}
logger() << "TextureManager: resized texture from "
<< src.width << "x" << src.height
<< " to " << newW << "x" << newH << " (POT)" << std::endl;
return dst;
}
Texture::Texture(const TextureDataStruct& texData) {
width = texData.width;
height = texData.height;
if (!IsPowerOf2(width) || !IsPowerOf2(height)) {
std::cerr << "TextureManager WARNING: non-POT texture "
<< width << "x" << height
<< " — upload may fail on some platforms." << std::endl;
}
glGenTextures(1, &texID);
glBindTexture(GL_TEXTURE_2D, texID);
GLint internalFormat;
GLenum externalFormat;
switch (texData.format) {
case TextureDataStruct::R8:
// Для WebGL 1.0 лучше использовать GL_LUMINANCE вместо GL_RED
// Если используешь WebGL 2.0, можно оставить GL_RED
#if defined(EMSCRIPTEN) || defined(__ANDROID__)
internalFormat = GL_LUMINANCE;
externalFormat = GL_LUMINANCE;
#else
internalFormat = GL_RED;
externalFormat = GL_RED;
#endif
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
// Исправлено: используем GL_TEXTURE_2D
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
break;
case TextureDataStruct::RGB:
internalFormat = GL_RGB;
externalFormat = GL_RGB;
glPixelStorei(GL_UNPACK_ALIGNMENT, 1); // RGB может быть не выровнен по 4 байта
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
break;
default: // RGBA
internalFormat = GL_RGBA;
externalFormat = GL_RGBA;
glPixelStorei(GL_UNPACK_ALIGNMENT, 4);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
break;
}
glTexImage2D(GL_TEXTURE_2D, 0, internalFormat,
static_cast<GLsizei>(width), static_cast<GLsizei>(height),
0, externalFormat, GL_UNSIGNED_BYTE, texData.data.data());
CheckGlError(__FILE__, __LINE__);
// 3. Фильтрация и Мип-мапы
// ВНИМАНИЕ: Для шрифтов (NPOT) в WebGL glGenerateMipmap работать НЕ будет!
if (texData.mipmap == TextureDataStruct::GENERATE && texData.format != TextureDataStruct::R8) {
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
glGenerateMipmap(GL_TEXTURE_2D);
}
else {
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
}
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
CheckGlError(__FILE__, __LINE__);
glPixelStorei(GL_UNPACK_ALIGNMENT, 4);
}
Texture::Texture(const std::array<TextureDataStruct, 6>& texDataArray)
{
width = texDataArray[0].width;
height = texDataArray[0].height;
for (size_t i = 1; i < 6; ++i) {
if (texDataArray[i].width != width || texDataArray[i].height != height) {
throw std::runtime_error("Cubemap faces must have the same dimensions");
}
}
glGenTextures(1, &texID);
if (texID == 0)
{
throw std::runtime_error("glGenTextures did not work for cubemap");
}
glBindTexture(GL_TEXTURE_CUBE_MAP, texID);
CheckGlError(__FILE__, __LINE__);
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
#ifndef EMSCRIPTEN
#ifndef __ANDROID__
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE);
#endif
#endif
CheckGlError(__FILE__, __LINE__);
for (int i = 0; i < 6; ++i)
{
GLint internalFormat;
GLenum format;
if (texDataArray[i].format == TextureDataStruct::RGB)
{
internalFormat = GL_RGB; // internalFormat
format = GL_RGB; // format
}
else // BS_32BIT
{
internalFormat = GL_RGBA; // internalFormat
format = GL_RGBA; // format
}
glTexImage2D(
GL_TEXTURE_CUBE_MAP_POSITIVE_X + i,
0,
internalFormat,
static_cast<GLsizei>(width),
static_cast<GLsizei>(height),
0,
format,
GL_UNSIGNED_BYTE,
texDataArray[i].data.data()
);
CheckGlError(__FILE__, __LINE__);
}
glBindTexture(GL_TEXTURE_CUBE_MAP, 0);
}
Texture::~Texture()
{
glDeleteTextures(1, &texID);
texID = 0;
}
GLuint Texture::getTexID()
{
return texID;
}
size_t Texture::getWidth()
{
return width;
}
size_t Texture::getHeight()
{
return height;
}
TextureDataStruct CreateTextureDataFromBmp24(const std::string& fullFileName, const std::string& ZIPFileName)
{
TextureDataStruct texData;
std::vector<char> fileArr;
fileArr = !ZIPFileName.empty() ? readFileFromZIP(fullFileName, ZIPFileName) : readFile(fullFileName);
size_t fileSize = fileArr.size();
if (fileSize < 22)
{
throw std::runtime_error("File is too short or not correct!");
}
//This refers to BITMAPV5HEADER
texData.width = *reinterpret_cast<uint32_t*>(&fileArr[18]);
texData.height = *reinterpret_cast<uint32_t*>(&fileArr[22]);
texData.format = TextureDataStruct::RGB;
size_t dataSize = texData.width * texData.height * 3;
texData.data.resize(dataSize);
size_t pos = *reinterpret_cast<uint32_t*>(&fileArr[10]);
size_t x = 0;
for (size_t i = 0; i < texData.width; i++)
for (size_t j = 0; j < texData.height; j++)
{
if (pos + 3 > fileSize)
{
throw std::runtime_error("File is too short!");
}
x = (i * texData.height + j) + (i * texData.height + j) + (i * texData.height + j);
texData.data[x + 2] = fileArr[pos++];
texData.data[x + 1] = fileArr[pos++];
texData.data[x + 0] = fileArr[pos++];
}
return texData;
}
TextureDataStruct CreateTextureDataFromBmp32(const std::string& fullFileName, const std::string& ZIPFileName)
{
TextureDataStruct texData;
std::vector<char> fileArr;
fileArr = !ZIPFileName.empty() ? readFileFromZIP(fullFileName, ZIPFileName) : readFile(fullFileName);
size_t fileSize = fileArr.size();
if (fileSize < 22)
{
throw std::runtime_error("File is too short or not correct!");
}
//This refers to BITMAPV5HEADER
texData.width = *reinterpret_cast<uint32_t*>(&fileArr[18]);
texData.height = *reinterpret_cast<uint32_t*>(&fileArr[22]);
texData.format = TextureDataStruct::RGBA;
size_t dataSize = texData.width * texData.height * 4;
texData.data.resize(dataSize);
size_t pos = *reinterpret_cast<uint32_t*>(&fileArr[10]);
size_t x = 0;
for (size_t i = 0; i < texData.width; i++)
for (size_t j = 0; j < texData.height; j++)
{
if (pos + 4 > fileSize)
{
throw std::runtime_error("File is too short!");
}
x = (i * texData.height + j) + (i * texData.height + j) + (i * texData.height + j) + (i * texData.height + j);
texData.data[x + 2] = fileArr[pos++];
texData.data[x + 1] = fileArr[pos++];
texData.data[x + 0] = fileArr[pos++];
texData.data[x + 3] = fileArr[pos++];
}
return texData;
}
#ifdef PNG_ENABLED
struct png_data_t {
const char* data;
size_t size;
size_t offset;
};
void user_read_data(png_structp png_ptr, png_bytep out_ptr, png_size_t bytes_to_read) {
png_data_t* data = (png_data_t*)png_get_io_ptr(png_ptr);
if (data->offset + bytes_to_read > data->size) {
png_error(png_ptr, "PNG Read Error: Attempted to read past end of data buffer.");
bytes_to_read = data->size - data->offset;
}
std::memcpy(out_ptr, data->data + data->offset, bytes_to_read);
data->offset += bytes_to_read;
}
void user_warning_fn(png_structp png_ptr, png_const_charp warning_msg) {
logger() << "PNG Warning: " << warning_msg << std::endl;
}
void user_error_fn(png_structp png_ptr, png_const_charp error_msg) {
logger() << "PNG Error: " << error_msg << std::endl;
longjmp(png_jmpbuf(png_ptr), 1);
}
TextureDataStruct CreateTextureDataFromPng(const std::vector<char>& fileArr)
{
TextureDataStruct texData;
png_data_t png_data = { fileArr.data(), fileArr.size(), 0 };
png_structp png = png_create_read_struct(PNG_LIBPNG_VER_STRING, nullptr, nullptr, nullptr);
if (!png) {
throw std::runtime_error("Could not create PNG read structure");
}
png_infop info = png_create_info_struct(png);
if (!info) {
png_destroy_read_struct(&png, nullptr, nullptr);
throw std::runtime_error("Could not create PNG info structure");
}
if (setjmp(png_jmpbuf(png))) {
png_destroy_read_struct(&png, &info, nullptr);
throw std::runtime_error("Error during PNG read (longjmp was executed)");
}
png_set_error_fn(png, nullptr, user_error_fn, user_warning_fn);
png_set_read_fn(png, &png_data, user_read_data);
png_read_info(png, info);
texData.width = png_get_image_width(png, info);
texData.height = png_get_image_height(png, info);
png_byte color_type = png_get_color_type(png, info);
png_byte bit_depth = png_get_bit_depth(png, info);
if (bit_depth == 16)
png_set_strip_16(png);
if (color_type == PNG_COLOR_TYPE_PALETTE)
png_set_palette_to_rgb(png);
if (color_type == PNG_COLOR_TYPE_GRAY && bit_depth < 8)
png_set_expand_gray_1_2_4_to_8(png);
if (png_get_valid(png, info, PNG_INFO_tRNS))
png_set_tRNS_to_alpha(png);
if (color_type == PNG_COLOR_TYPE_RGB ||
color_type == PNG_COLOR_TYPE_GRAY ||
color_type == PNG_COLOR_TYPE_PALETTE)
png_set_filler(png, 0xFF, PNG_FILLER_AFTER);
if (color_type == PNG_COLOR_TYPE_GRAY ||
color_type == PNG_COLOR_TYPE_GRAY_ALPHA)
png_set_gray_to_rgb(png);
png_read_update_info(png, info);
png_bytep* row_pointers = (png_bytep*)malloc(sizeof(png_bytep) * texData.height);
for (int y = 0; y < texData.height; y++) {
row_pointers[y] = (png_byte*)malloc(png_get_rowbytes(png, info));
}
png_read_image(png, row_pointers);
bool has_alpha = (color_type & PNG_COLOR_MASK_ALPHA) || (png_get_valid(png, info, PNG_INFO_tRNS));
size_t dataSize;
if (has_alpha)
{
texData.format = TextureDataStruct::RGBA;
}
else
{
texData.format = TextureDataStruct::RGB;
}
int channels = has_alpha ? 4 : 3;
dataSize = texData.width * texData.height * channels;
texData.data.resize(dataSize);
for (int y = texData.height - 1; y >= 0; y--) {
png_bytep row = row_pointers[texData.height - 1 - y];
for (int x = 0; x < texData.width; x++) {
png_bytep px = &(row[x * 4]);
texData.data[(y * texData.width + x) * channels + 0] = px[0]; // R
texData.data[(y * texData.width + x) * channels + 1] = px[1]; // G
texData.data[(y * texData.width + x) * channels + 2] = px[2]; // B
if (has_alpha) {
texData.data[(y * texData.width + x) * channels + 3] = px[3]; // A
}
}
free(row_pointers[texData.height - 1 - y]);
}
free(row_pointers);
// ==================================================
png_destroy_read_struct(&png, &info, nullptr);
return texData;
}
TextureDataStruct CreateTextureDataFromPng(const std::string& fullFileName, const std::string& ZIPFileName)
{
std::vector<char> fileArr;
fileArr = !ZIPFileName.empty() ? readFileFromZIP(fullFileName, ZIPFileName) : readFile(fullFileName);
if (fileArr.empty()) {
logger() << "Could not read file data into memory: " << fullFileName << " zip file name: " << ZIPFileName << std::endl;
throw std::runtime_error("Could not read file data into memory");
}
return CreateTextureDataFromPng(fileArr);
}
#endif
std::string TextureManager::MakeKey(const std::string& fileName, const std::string& zipFile)
{
return zipFile.empty() ? fileName : fileName + "|" + zipFile;
}
std::shared_ptr<Texture> TextureManager::LoadFromBmp24(const std::string& fileName, const std::string& zipFile, bool resizeToPOT)
{
std::string key = MakeKey(fileName, zipFile);
auto it = textureMap.find(key);
if (it != textureMap.end())
return it->second;
auto data = CreateTextureDataFromBmp24(fileName, zipFile);
if (resizeToPOT) data = ResizeTextureDataToPOT(data);
auto tex = std::make_shared<Texture>(data);
textureMap[key] = tex;
return tex;
}
std::shared_ptr<Texture> TextureManager::LoadFromBmp32(const std::string& fileName, const std::string& zipFile, bool resizeToPOT)
{
std::string key = MakeKey(fileName, zipFile);
auto it = textureMap.find(key);
if (it != textureMap.end())
return it->second;
auto data = CreateTextureDataFromBmp32(fileName, zipFile);
if (resizeToPOT) data = ResizeTextureDataToPOT(data);
auto tex = std::make_shared<Texture>(data);
textureMap[key] = tex;
return tex;
}
#ifdef PNG_ENABLED
std::shared_ptr<Texture> TextureManager::LoadFromPng(const std::string& fileName, const std::string& zipFile, bool resizeToPOT)
{
std::string key = MakeKey(fileName, zipFile);
auto it = textureMap.find(key);
if (it != textureMap.end())
return it->second;
auto data = CreateTextureDataFromPng(fileName, zipFile);
if (resizeToPOT) data = ResizeTextureDataToPOT(data);
auto tex = std::make_shared<Texture>(data);
textureMap[key] = tex;
return tex;
}
#endif
void TextureManager::Unload(const std::string& fileName, const std::string& zipFile)
{
textureMap.erase(MakeKey(fileName, zipFile));
}
void TextureManager::UnloadByPrefix(const std::string& prefix)
{
for (auto it = textureMap.begin(); it != textureMap.end(); ) {
if (it->first.substr(0, prefix.size()) == prefix) {
logger() << "Unloading texture with prefix: " << it->first << std::endl;
it = textureMap.erase(it);
}
else {
++it;
}
}
}
void TextureManager::UnloadAll()
{
textureMap.clear();
}
void TextureManager::MarkAllResident()
{
for (const auto& [key, tex] : textureMap) residentKeys.insert(key);
}
void TextureManager::UnloadUnused()
{
size_t unloaded = 0;
for (auto it = textureMap.begin(); it != textureMap.end(); ) {
const bool keep = residentKeys.count(it->first) || it->second.use_count() > 1;
if (keep) {
++it;
}
else {
it = textureMap.erase(it);
++unloaded;
}
}
logger() << "Unloaded " << unloaded << " unused textures" << std::endl;
}
}