Extra2D/Extra2D/src/graphics/backends/opengl/gl_texture.cpp

507 lines
15 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#include <extra2d/graphics/backends/opengl/gl_texture.h>
#include <extra2d/graphics/memory/gpu_context.h>
#include <extra2d/graphics/memory/vram_manager.h>
#define STB_IMAGE_IMPLEMENTATION
#include <cstring>
#include <extra2d/core/service_locator.h>
#include <extra2d/services/logger_service.h>
#include <fstream>
#include <stb/stb_image.h>
namespace extra2d {
// ============================================================================
// KTX 文件头结构
// ============================================================================
#pragma pack(push, 1)
struct KTXHeader {
uint8_t identifier[12];
uint32_t endianness;
uint32_t glType;
uint32_t glTypeSize;
uint32_t glFormat;
uint32_t glInternalFormat;
uint32_t glBaseInternalFormat;
uint32_t pixelWidth;
uint32_t pixelHeight;
uint32_t pixelDepth;
uint32_t numberOfArrayElements;
uint32_t numberOfFaces;
uint32_t numberOfMipmapLevels;
uint32_t bytesOfKeyValueData;
};
#pragma pack(pop)
// KTX 文件标识符
static const uint8_t KTX_IDENTIFIER[12] = {0xAB, 0x4B, 0x54, 0x58, 0x20, 0x31,
0x31, 0xBB, 0x0D, 0x0A, 0x1A, 0x0A};
// ============================================================================
// DDS 文件头结构
// ============================================================================
#pragma pack(push, 1)
struct DDSPixelFormat {
uint32_t size;
uint32_t flags;
uint32_t fourCC;
uint32_t rgbBitCount;
uint32_t rBitMask;
uint32_t gBitMask;
uint32_t bBitMask;
uint32_t aBitMask;
};
struct DDSHeader {
uint32_t magic;
uint32_t size;
uint32_t flags;
uint32_t height;
uint32_t width;
uint32_t pitchOrLinearSize;
uint32_t depth;
uint32_t mipMapCount;
uint32_t reserved1[11];
DDSPixelFormat pixelFormat;
uint32_t caps;
uint32_t caps2;
uint32_t caps3;
uint32_t caps4;
uint32_t reserved2;
};
struct DDSHeaderDXT10 {
uint32_t dxgiFormat;
uint32_t resourceDimension;
uint32_t miscFlag;
uint32_t arraySize;
uint32_t miscFlags2;
};
#pragma pack(pop)
static constexpr uint32_t DDS_MAGIC = 0x20534444; // "DDS "
static constexpr uint32_t DDPF_FOURCC = 0x04;
/**
* @brief 生成四字符代码FourCC
* @param a 第一个字符
* @param b 第二个字符
* @param c 第三个字符
* @param d 第四个字符
* @return 组合后的32位无符号整数
*/
static uint32_t makeFourCC(char a, char b, char c, char d) {
return static_cast<uint32_t>(a) | (static_cast<uint32_t>(b) << 8) |
(static_cast<uint32_t>(c) << 16) | (static_cast<uint32_t>(d) << 24);
}
// ============================================================================
// GLTexture 实现
// ============================================================================
/**
* @brief 从像素数据构造纹理对象
* @param width 纹理宽度(像素)
* @param height 纹理高度(像素)
* @param pixels 像素数据指针可为nullptr创建空纹理
* @param channels 颜色通道数1=R, 3=RGB, 4=RGBA
*/
GLTexture::GLTexture(int width, int height, const uint8_t *pixels, int channels)
: textureID_(0), width_(width), height_(height), channels_(channels),
format_(PixelFormat::RGBA8), dataSize_(0) {
// 保存像素数据用于生成遮罩
if (pixels) {
pixelData_.resize(width * height * channels);
std::memcpy(pixelData_.data(), pixels, pixelData_.size());
}
createTexture(pixels);
}
/**
* @brief 从文件路径构造纹理对象
* @param filepath 纹理文件路径支持普通图片格式和压缩格式KTX/DDS
*/
GLTexture::GLTexture(const std::string &filepath)
: textureID_(0), width_(0), height_(0), channels_(0),
format_(PixelFormat::RGBA8), dataSize_(0) {
// 检查是否为压缩纹理格式
std::string ext = filepath.substr(filepath.find_last_of('.') + 1);
if (ext == "ktx" || ext == "KTX") {
loadCompressed(filepath);
return;
}
if (ext == "dds" || ext == "DDS") {
loadCompressed(filepath);
return;
}
// 不翻转图片,保持原始方向
stbi_set_flip_vertically_on_load(false);
uint8_t *data = stbi_load(filepath.c_str(), &width_, &height_, &channels_, 0);
if (data) {
// 保存像素数据用于生成遮罩
pixelData_.resize(width_ * height_ * channels_);
std::memcpy(pixelData_.data(), data, pixelData_.size());
createTexture(data);
stbi_image_free(data);
} else {
E2D_LOG_ERROR("加载纹理失败: {}", filepath);
}
}
GLTexture::~GLTexture() {
if (textureID_ != 0) {
// 检查 GPU 上下文是否仍然有效
// 如果 OpenGL 上下文已销毁,则跳过 glDeleteTextures 调用
if (GPUContext::get().isValid()) {
glDeleteTextures(1, &textureID_);
}
// VRAM 跟踪: 释放纹理显存(无论上下文是否有效都需要更新统计)
if (dataSize_ > 0) {
VRAMMgr::get().freeTexture(dataSize_);
}
}
}
void GLTexture::setFilter(bool linear) {
bind();
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER,
linear ? GL_LINEAR : GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER,
linear ? GL_LINEAR : GL_NEAREST);
}
void GLTexture::setWrap(bool repeat) {
bind();
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S,
repeat ? GL_REPEAT : GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T,
repeat ? GL_REPEAT : GL_CLAMP_TO_EDGE);
}
void GLTexture::bind(unsigned int slot) const {
glActiveTexture(GL_TEXTURE0 + slot);
glBindTexture(GL_TEXTURE_2D, textureID_);
}
/**
* @brief 解绑当前纹理
*/
void GLTexture::unbind() const { glBindTexture(GL_TEXTURE_2D, 0); }
/**
* @brief 创建OpenGL纹理对象并上传像素数据
* @param pixels 像素数据指针
*/
void GLTexture::createTexture(const uint8_t *pixels) {
GLenum format = GL_RGBA;
GLenum internalFormat = GL_RGBA8;
int unpackAlignment = 4;
if (channels_ == 1) {
format = GL_RED;
internalFormat = GL_R8;
unpackAlignment = 1;
format_ = PixelFormat::R8;
} else if (channels_ == 3) {
format = GL_RGB;
internalFormat = GL_RGB8;
unpackAlignment = 1;
format_ = PixelFormat::RGB8;
} else if (channels_ == 4) {
format = GL_RGBA;
internalFormat = GL_RGBA8;
unpackAlignment = 4;
format_ = PixelFormat::RGBA8;
}
glGenTextures(1, &textureID_);
bind();
GLint prevUnpackAlignment = 4;
glGetIntegerv(GL_UNPACK_ALIGNMENT, &prevUnpackAlignment);
glPixelStorei(GL_UNPACK_ALIGNMENT, unpackAlignment);
glTexImage2D(GL_TEXTURE_2D, 0, internalFormat, width_, height_, 0, format,
GL_UNSIGNED_BYTE, pixels);
glPixelStorei(GL_UNPACK_ALIGNMENT, prevUnpackAlignment);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
// 使用 NEAREST 过滤器,更适合像素艺术风格的精灵
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glGenerateMipmap(GL_TEXTURE_2D);
// VRAM 跟踪
dataSize_ = static_cast<size_t>(width_ * height_ * channels_);
VRAMMgr::get().allocTexture(dataSize_);
}
// ============================================================================
// 压缩纹理加载
// ============================================================================
bool GLTexture::loadCompressed(const std::string &filepath) {
std::string ext = filepath.substr(filepath.find_last_of('.') + 1);
if (ext == "ktx" || ext == "KTX") {
return loadKTX(filepath);
}
if (ext == "dds" || ext == "DDS") {
return loadDDS(filepath);
}
E2D_LOG_ERROR("不支持的压缩纹理格式: {}", filepath);
return false;
}
/**
* @brief 加载KTX格式压缩纹理
* @param filepath KTX文件路径
* @return 加载成功返回true失败返回false
*/
bool GLTexture::loadKTX(const std::string &filepath) {
std::ifstream file(filepath, std::ios::binary);
if (!file.is_open()) {
E2D_LOG_ERROR("打开 KTX 文件失败: {}", filepath);
return false;
}
KTXHeader header;
file.read(reinterpret_cast<char *>(&header), sizeof(header));
if (!file) {
E2D_LOG_ERROR("读取 KTX 文件头失败: {}", filepath);
return false;
}
// 验证标识符
if (std::memcmp(header.identifier, KTX_IDENTIFIER, 12) != 0) {
E2D_LOG_ERROR("无效的 KTX 标识符: {}", filepath);
return false;
}
width_ = static_cast<int>(header.pixelWidth);
height_ = static_cast<int>(header.pixelHeight);
channels_ = 4; // 压缩纹理通常解压为 RGBA
// 确定压缩格式
GLenum glInternalFormat = header.glInternalFormat;
switch (glInternalFormat) {
case GL_COMPRESSED_RGB8_ETC2:
format_ = PixelFormat::ETC2_RGB8;
channels_ = 3;
break;
case GL_COMPRESSED_RGBA8_ETC2_EAC:
format_ = PixelFormat::ETC2_RGBA8;
break;
case GL_COMPRESSED_RGBA_ASTC_4x4:
format_ = PixelFormat::ASTC_4x4;
break;
case GL_COMPRESSED_RGBA_ASTC_6x6:
format_ = PixelFormat::ASTC_6x6;
break;
case GL_COMPRESSED_RGBA_ASTC_8x8:
format_ = PixelFormat::ASTC_8x8;
break;
default:
E2D_LOG_ERROR("不支持的 KTX 内部格式: {:#06x}", glInternalFormat);
return false;
}
// 跳过 key-value 数据
file.seekg(header.bytesOfKeyValueData, std::ios::cur);
// 读取第一个 mipmap level
uint32_t imageSize = 0;
file.read(reinterpret_cast<char *>(&imageSize), sizeof(imageSize));
if (!file || imageSize == 0) {
E2D_LOG_ERROR("读取 KTX 图像大小失败: {}", filepath);
return false;
}
std::vector<uint8_t> compressedData(imageSize);
file.read(reinterpret_cast<char *>(compressedData.data()), imageSize);
if (!file) {
E2D_LOG_ERROR("读取 KTX 图像数据失败: {}", filepath);
return false;
}
// 创建 GL 纹理
glGenTextures(1, &textureID_);
bind();
glCompressedTexImage2D(GL_TEXTURE_2D, 0, glInternalFormat, width_, height_, 0,
static_cast<GLsizei>(imageSize),
compressedData.data());
GLenum err = glGetError();
if (err != GL_NO_ERROR) {
E2D_LOG_ERROR("KTX 纹理上传失败: {:#06x}", err);
glDeleteTextures(1, &textureID_);
textureID_ = 0;
return false;
}
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
// VRAM 跟踪
dataSize_ = imageSize;
VRAMMgr::get().allocTexture(dataSize_);
E2D_LOG_INFO("已加载 KTX 压缩纹理: {} ({}x{}, 格式={:#06x})",
filepath, width_, height_, glInternalFormat);
return true;
}
/**
* @brief 加载DDS格式压缩纹理
* @param filepath DDS文件路径
* @return 加载成功返回true失败返回false
*/
bool GLTexture::loadDDS(const std::string &filepath) {
std::ifstream file(filepath, std::ios::binary);
if (!file.is_open()) {
E2D_LOG_ERROR("打开 DDS 文件失败: {}", filepath);
return false;
}
DDSHeader header;
file.read(reinterpret_cast<char *>(&header), sizeof(header));
if (!file) {
E2D_LOG_ERROR("读取 DDS 文件头失败: {}", filepath);
return false;
}
if (header.magic != DDS_MAGIC) {
E2D_LOG_ERROR("无效的 DDS 魔数: {}", filepath);
return false;
}
width_ = static_cast<int>(header.width);
height_ = static_cast<int>(header.height);
channels_ = 4;
GLenum glInternalFormat = 0;
// 检查 DX10 扩展头
if ((header.pixelFormat.flags & DDPF_FOURCC) &&
header.pixelFormat.fourCC == makeFourCC('D', 'X', '1', '0')) {
DDSHeaderDXT10 dx10Header;
file.read(reinterpret_cast<char *>(&dx10Header), sizeof(dx10Header));
if (!file) {
E2D_LOG_ERROR("读取 DDS DX10 文件头失败: {}", filepath);
return false;
}
// DXGI_FORMAT 映射到 GL 格式
switch (dx10Header.dxgiFormat) {
case 147: // DXGI_FORMAT_ETC2_RGB8
glInternalFormat = GL_COMPRESSED_RGB8_ETC2;
format_ = PixelFormat::ETC2_RGB8;
channels_ = 3;
break;
case 148: // DXGI_FORMAT_ETC2_RGBA8
glInternalFormat = GL_COMPRESSED_RGBA8_ETC2_EAC;
format_ = PixelFormat::ETC2_RGBA8;
break;
default:
E2D_LOG_ERROR("不支持的 DDS DX10 格式: {}", dx10Header.dxgiFormat);
return false;
}
} else {
E2D_LOG_ERROR("DDS 文件未使用 DX10 扩展,不支持: {}",
filepath);
return false;
}
// 计算压缩数据大小
size_t blockSize = (glInternalFormat == GL_COMPRESSED_RGB8_ETC2) ? 8 : 16;
size_t blocksWide = (width_ + 3) / 4;
size_t blocksHigh = (height_ + 3) / 4;
size_t imageSize = blocksWide * blocksHigh * blockSize;
std::vector<uint8_t> compressedData(imageSize);
file.read(reinterpret_cast<char *>(compressedData.data()), imageSize);
if (!file) {
E2D_LOG_ERROR("读取 DDS 图像数据失败: {}", filepath);
return false;
}
// 创建 GL 纹理
glGenTextures(1, &textureID_);
bind();
glCompressedTexImage2D(GL_TEXTURE_2D, 0, glInternalFormat, width_, height_, 0,
static_cast<GLsizei>(imageSize),
compressedData.data());
GLenum err = glGetError();
if (err != GL_NO_ERROR) {
E2D_LOG_ERROR("DDS 纹理上传失败: {:#06x}", err);
glDeleteTextures(1, &textureID_);
textureID_ = 0;
return false;
}
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
// VRAM 跟踪
dataSize_ = imageSize;
VRAMMgr::get().allocTexture(dataSize_);
E2D_LOG_INFO("已加载 DDS 压缩纹理: {} ({}x{})", filepath, width_,
height_);
return true;
}
void GLTexture::generateAlphaMask() {
if (pixelData_.empty() || width_ <= 0 || height_ <= 0) {
E2D_LOG_WARN("无法生成透明遮罩: 没有可用的像素数据");
return;
}
alphaMask_ = std::make_unique<AlphaMask>(AlphaMask::createFromPixels(
pixelData_.data(), width_, height_, channels_));
E2D_LOG_DEBUG("已为纹理生成透明遮罩: {}x{}", width_, height_);
}
PixelFormat GLTexture::getFormat() const { return format_; }
/**
* @brief 静态工厂方法,创建指定格式的空纹理
* @param width 纹理宽度
* @param height 纹理高度
* @param format 像素格式
* @return 创建的纹理智能指针
*/
Ptr<Texture> GLTexture::create(int width, int height, PixelFormat format) {
int channels = 4;
switch (format) {
case PixelFormat::R8:
channels = 1;
break;
case PixelFormat::RG8:
channels = 2;
break;
case PixelFormat::RGB8:
channels = 3;
break;
case PixelFormat::RGBA8:
channels = 4;
break;
default:
channels = 4;
break;
}
return makePtr<GLTexture>(width, height, nullptr, channels);
}
} // namespace extra2d