507 lines
15 KiB
C++
507 lines
15 KiB
C++
#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
|