556 lines
16 KiB
C++
556 lines
16 KiB
C++
#include "render/TextureManager.h"
|
||
#include "render/OpenGlExtensions.h"
|
||
#ifdef PNG_ENABLED
|
||
#include "png.h"
|
||
#endif
|
||
#include <iostream>
|
||
|
||
namespace ZL
|
||
{
|
||
#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();
|
||
}
|
||
|
||
} |