c++ - 将 S3TC/DXTn 数据转换为 QImage

标签 c++ qt opengl

我加入了一个简化遗留图形代码的项目,非常感谢有关此数据转换问题的建议。

输入是 DXT1、DXT3、DXT5 格式的压缩纹理。数据在主内存中,而不是显卡内存中。输入没有标准 DDS_HEADER ,只有压缩像素数据。所需的输出是 QImages。

使用现有的元数据,我们可以构造一个 DDS_HEADER,将纹理写入临时文件,然后从该文件加载 QImage。然而,我们希望避免这种解决方案并直接使用原始数据,因为它有很多很多实例。

我的研究没有发现任何 Qt 函数可以直接执行此转换。到目前为止,听起来最有前途的方法是使用我们现有的 OpenGL 上下文将纹理绘制到 QOpenGLFrameBufferObject。这个类有一个 toImage() 成员函数。但是,我不明白如何从原始数据构造可用的纹理对象并将其绘制到帧缓冲区。

编辑:基于 Scheff 的出色回答的澄清。我知道可以手动解压缩纹理并从结果中加载 QImage。为了最简单起见,我宁愿避免此步骤并尽可能使用库函数。 QOpenGLTexture有一个成员函数setCompressedData可以用到。

感谢您的任何建议。

最佳答案

看到这个问题,我开始好奇,了解了S3 Texture Compression .有趣的是,虽然我过去很关心压缩纹理,但我一直认为它会像 LZW Algorithm 这样复杂的东西。或 JPEG Compression ,并且从未深入挖掘。但是,今天我意识到我完全错了。

S3 Texture Compression尽管它可以实现相当可观的压缩比,但实际上要简单得多。

好的介绍google一下就能轻松找到。这个问题已经提到了 MSDN。此外,我还找到了一些其他网站,这些网站在最短的时间内为我很好地介绍了这个主题:

关于GitHub项目,好像已经有人做了。我用眼睛扫描了一点代码,但最后,我不确定它们是否支持所有可能的功能。然而,我从 Brandon Jones 网站“借用”了测试图像,所以,提一下就足够了。

所以,这是我的实际答案:另一种方法可能是在 CPU 端将纹理完全解码到 QImage。

作为概念证明,我留下了今天早上我修改代码的结果——我尝试将链接描述转换为工作 C++ 代码——DXT1-QImage.cc:

#include <cstdint>
#include <fstream>

#include <QtWidgets>

#ifndef _WIN32
typedef quint32 DWORD;
#endif // _WIN32

/* borrowed from:
 * https://msdn.microsoft.com/en-us/library/windows/desktop/bb943984(v=vs.85).aspx
 */
struct DDS_PIXELFORMAT {
  DWORD dwSize;
  DWORD dwFlags;
  DWORD dwFourCC;
  DWORD dwRGBBitCount;
  DWORD dwRBitMask;
  DWORD dwGBitMask;
  DWORD dwBBitMask;
  DWORD dwABitMask;
};
/* borrowed from:
 * https://msdn.microsoft.com/en-us/library/windows/desktop/bb943982(v=vs.85).aspx
 */
struct DDS_HEADER {
  DWORD           dwSize;
  DWORD           dwFlags;
  DWORD           dwHeight;
  DWORD           dwWidth;
  DWORD           dwPitchOrLinearSize;
  DWORD           dwDepth;
  DWORD           dwMipMapCount;
  DWORD           dwReserved1[11];
  DDS_PIXELFORMAT ddspf;
  DWORD           dwCaps;
  DWORD           dwCaps2;
  DWORD           dwCaps3;
  DWORD           dwCaps4;
  DWORD           dwReserved2;
};

inline quint32 stretch(std::uint16_t color)
{
  return 0xff000000u
    | (quint32)(color & 0x001f) << 3 // >>  0 << 3 <<  0
    | (quint32)(color & 0x07e0) << 5 // >>  5 << 2 <<  8
    | (quint32)(color & 0xf800) << 8;// >> 11 << 3 << 16
}

void makeLUT(
  quint32 lut[4], std::uint16_t color0, std::uint16_t color1)
{
  const quint32 argb0 = stretch(color0);
  const quint32 argb1 = stretch(color1);
  lut[0] = argb0;
  lut[1] = argb1;
  if (color0 > color1) {
    lut[2] = 0xff000000u
      | ((((argb0 & 0xff0000) >> 15) + ((argb1 & 0xff0000) >> 16)) / 3) << 16
      | ((((argb0 & 0x00ff00) >>  7) + ((argb1 & 0x00ff00) >>  8)) / 3) <<  8
      | ((((argb0 & 0x0000ff) <<  1) + ((argb1 & 0x0000ff) >>  0)) / 3) <<  0;
    lut[3] = 0xff000000u
      | ((((argb1 & 0xff0000) >> 15) + ((argb0 & 0xff0000) >> 16)) / 3) << 16
      | ((((argb1 & 0x00ff00) >>  7) + ((argb0 & 0x00ff00) >>  8)) / 3) <<  8
      | ((((argb1 & 0x0000ff) <<  1) + ((argb0 & 0x0000ff) >>  0)) / 3) <<  0;
  } else {
    lut[2] = 0xff000000u
      | ((((argb0 & 0xff0000) >> 16) + ((argb1 & 0xff0000) >> 16)) / 2) << 16
      | ((((argb0 & 0x00ff00) >>  8) + ((argb1 & 0x00ff00) >>  8)) / 2) <<  8
      | ((((argb0 & 0x0000ff) >>  0) + ((argb1 & 0x0000ff) >>  0)) / 2) <<  0;
    lut[3] = 0xff000000u;
  }
}

const std::uint8_t* uncompress(
  const std::uint8_t *data, QImage &qImg, int x, int y)
{
  // get color 0 and color 1
  std::uint16_t color0 = data[0] | data[1] << 8;
  std::uint16_t color1 = data[2] | data[3] << 8;
  data += 4;
  quint32 lut[4]; makeLUT(lut, color0, color1);
  // decode 4 x 4 pixels
  for (int i = 0; i < 4; ++i) {
    qImg.setPixel(x + 0, y + i, lut[data[i] >> 0 & 3]);
    qImg.setPixel(x + 1, y + i, lut[data[i] >> 2 & 3]);
    qImg.setPixel(x + 2, y + i, lut[data[i] >> 4 & 3]);
    qImg.setPixel(x + 3, y + i, lut[data[i] >> 6 & 3]);
  }
  data += 4;
  // done
  return data;
}

QImage loadDXT1(const char *file)
{
  std::ifstream fIn(file, std::ios::in | std::ios::binary);
  // read magic code
  enum { sizeMagic = 4 }; char magic[sizeMagic];
  if (!fIn.read(magic, sizeMagic)) {
    return QImage(); // ERROR: read failed
  }
  if (strncmp(magic, "DDS ", sizeMagic) != 0) {
    return QImage(); // ERROR: wrong format (wrong magic code)
  }
  // read header
  DDS_HEADER header;
  if (!fIn.read((char*)&header, sizeof header)) {
    return QImage(); // ERROR: read failed
  }
  qDebug() << "header size:" << sizeof header;
  // get raw data (size computation unclear)
  const unsigned w = (header.dwWidth + 3) / 4;
  const unsigned h = (header.dwHeight + 3) / 4;
  std::vector<std::uint8_t> data(w * h * 8);
  qDebug() << "data size:" << data.size();
  if (!fIn.read((char*)data.data(), data.size())) {
    return QImage(); // ERROR: read failed
  }
  // decode raw data
  QImage qImg(header.dwWidth, header.dwHeight, QImage::Format_ARGB32);
  const std::uint8_t *pData = data.data();
  for (int y = 0; y < (int)header.dwHeight; y += 4) {
    for (int x = 0; x < (int)header.dwWidth; x += 4) {
      pData = uncompress(pData, qImg, x, y);
    }
  }
  qDebug() << "processed image size:" << fIn.tellg();
  // done 
  return qImg;
}

int main(int argc, char **argv)
{
  qDebug() << "Qt Version:" << QT_VERSION_STR;
  QApplication app(argc, argv);
  // build QImage
  QImage qImg = loadDXT1("test-dxt1.dds");
  // setup GUI
  QMainWindow qWin;
  QLabel qLblImg;
  qLblImg.setPixmap(QPixmap::fromImage(qImg));
  qWin.setCentralWidget(&qLblImg);
  qWin.show();
  // exec. application
  return app.exec();
}

我是在VS2013上开发调试的。为了检查它是否可以移植到 Linux,我能做的最好的事情就是在 cygwin 上编译和测试。

为此,我写了一个QMake文件DXT1-QImage.pro:

SOURCES = DXT1-QImage.cc

QT += widgets

bash 中编译和运行:

$ qmake-qt5 DXT1-QImage.pro 

$ make
g++ -c -fno-keep-inline-dllexport -D_GNU_SOURCE -pipe -O2 -Wall -W -D_REENTRANT -DQT_NO_DEBUG -DQT_WIDGETS_LIB -DQT_GUI_LIB -DQT_CORE_LIB -I. -isystem /usr/include/qt5 -isystem /usr/include/qt5/QtWidgets -isystem /usr/include/qt5/QtGui -isystem /usr/include/qt5/QtCore -I. -I/usr/lib/qt5/mkspecs/cygwin-g++ -o DXT1-QImage.o DXT1-QImage.cc
g++  -o DXT1-QImage.exe DXT1-QImage.o   -lQt5Widgets -lQt5Gui -lQt5Core -lGL -lpthread 

$ ./DXT1-QImage 
Qt Version: 5.9.2
QStandardPaths: XDG_RUNTIME_DIR not set, defaulting to '/tmp/runtime-ds32737'
header size: 124
data size: 131072
processed image size: 131200
QXcbShmImage: shmget() failed (88: Function not implemented) for size 1048576 (512x512)

为了测试,我使用了示例文件 test-dxt1.dds .

结果是这样的: Snapshot of DXT1-QImage

为了对比,原图:

the test image as PNG

注意事项:

尽管发问者明确提到他想转换已经在内存中的原始图像数据,但我实现了一个文件加载器。我不得不这样做,因为我没有看到任何其他方法可以将(有效的)DXT1 原始数据存入我这边的内存中(以便事后证明它是否有效)。

我的调试输出显示我的加载器读取了 131200 字节(即 4 字节魔术代码、124 字节 header 和 131072 字节压缩图像数据)。 与此相反,文件 test-dxt1.dds包含 174904 个字节。所以,文件中有额外的数据,但我(还)不知道它有什么用。

关于c++ - 将 S3TC/DXTn 数据转换为 QImage,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/48135093/

相关文章:

python - 使用 PyQt5 和 OpenGL 使用 VBO 绘制多个对象?

c++ - 2D openGL 引擎简单光源和帧缓冲区 (c++)

c++ - bind() 和 listen() 函数出错 (WinSock)

c++ - 在 C++11 中将 char* 转换为 bool 时强制类型安全

c++ - Qt 5.0.2 和 OpenGL 3+ 渲染问题

C++ 类格式 - Qt creator 5.2.1

c++ - 为已经转换的QGraphicsItem设置转换原点

c - 正确使用 gluLookAt()?

c++ - 继承的析构函数是否包含在虚拟表中?

c++ - 查询指针取消引用