将S3TC / DXTn数据转换为QImage

时间:2018-01-07 07:10:03

标签: c++ qt opengl

我加入了一个简化传统图形代码的项目,并对此数据转换问题的建议表示感谢。

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

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

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

编辑:澄清,基于Scheff的优秀答案。我知道可以手动解压缩纹理并从结果中加载QImage。我希望尽可能避免这一步并使用库函数,以便最简单。 QOpenGLTexture有一个可能使用的成员函数setCompressedData。

感谢您的任何建议。

2 个答案:

答案 0 :(得分:3)

阅读这个问题,我变得好奇并了解S3 Texture Compression。有趣的是,尽管过去我对压缩纹理很感兴趣,但我总是认为它会像LZW AlgorithmJPEG Compression那样复杂,而且从不深入挖掘。但是,今天我意识到我完全错了。

S3 Texture Compression实际上要简单得多,虽然它可以达到相当令人印象深刻的压缩率。

谷歌很容易找到好的介绍。这个问题已经提到了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字节标题和131072字节压缩图像数据)。 与此相反,文件test-dxt1.dds包含174904个字节。因此,文件中还有其他数据,但我(还)不知道它有什么用处。

答案 1 :(得分:2)

在我收到反馈后,我与my first answer中的提问者的期望不符,我修改了我的来源,将DXT1原始数据绘制成OpenGL纹理。

所以,这个答案特别针对问题的这一部分:

  

但是,我不了解如何从原始数据中构造可用的纹理对象并将其绘制到帧缓冲区。

这些修改强烈地受到启发&#34;通过Qt docs Cube OpenGL ES 2.0 example

关键部分是如何使用DXT1原始数据构建QOpenGLTexture

      _pQGLTex = new QOpenGLTexture(QOpenGLTexture::Target2D);
      _pQGLTex->setFormat(QOpenGLTexture::RGB_DXT1);
      _pQGLTex->setSize(_img.w, _img.h);
      _pQGLTex->allocateStorage(QOpenGLTexture::RGBA, QOpenGLTexture::UInt8);
      _pQGLTex->setCompressedData((int)_img.data.size(), _img.data.data());

      _pQGLTex->setMinificationFilter(QOpenGLTexture::Nearest);
      _pQGLTex->setMagnificationFilter(QOpenGLTexture::Linear);
      _pQGLTex->setWrapMode(QOpenGLTexture::ClampToEdge);

而且,这是完整的示例代码DXT1-QTexture-QImage.cc

#include <cstdint>
#include <fstream>

#include <QtWidgets>
#include <QOpenGLFunctions_4_0_Core>

#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;
};

struct Image {
  int w, h;
  std::vector<std::uint8_t> data;

  explicit Image(int w = 0, int h = 0):
    w(w), h(h), data(((w + 3) / 4) * ((h + 3) / 4) * 8)
  { }
  ~Image() = default;
  Image(const Image&) = delete;
  Image& operator=(const Image&) = delete;
  Image(Image &&img): w(img.w), h(img.h), data(move(img.data)) { }
};

Image 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 Image(); // ERROR: read failed
  }
  if (strncmp(magic, "DDS ", sizeMagic) != 0) {
    return Image(); // ERROR: wrong format (wrong magic code)
  }
  // read header
  DDS_HEADER header;
  if (!fIn.read((char*)&header, sizeof header)) {
    return Image(); // ERROR: read failed
  }
  qDebug() << "header size:" << sizeof header;
  // get raw data (size computation unclear)
  Image img(header.dwWidth, header.dwHeight);
  qDebug() << "data size:" << img.data.size();
  if (!fIn.read((char*)img.data.data(), img.data.size())) {
    return Image(); // ERROR: read failed
  }
  qDebug() << "processed image size:" << fIn.tellg();
  // done 
  return img;
}

const char *vertexShader =
  "#ifdef GL_ES\n"
  "// Set default precision to medium\n"
  "precision mediump int;\n"
  "precision mediump float;\n"
  "#endif\n"
  "\n"
  "uniform mat4 mvp_matrix;\n"
  "\n"
  "attribute vec4 a_position;\n"
  "attribute vec2 a_texcoord;\n"
  "\n"
  "varying vec2 v_texcoord;\n"
  "\n"
  "void main()\n"
  "{\n"
  "  // Calculate vertex position in screen space\n"
  "  gl_Position = mvp_matrix * a_position;\n"
  "\n"
  "  // Pass texture coordinate to fragment shader\n"
  "  // Value will be automatically interpolated to fragments inside polygon faces\n"
  "  v_texcoord = a_texcoord;\n"
  "}\n";

const char *fragmentShader =
  "#ifdef GL_ES\n"
  "// Set default precision to medium\n"
  "precision mediump int;\n"
  "precision mediump float;\n"
  "#endif\n"
  "\n"
  "uniform sampler2D texture;\n"
  "\n"
  "varying vec2 v_texcoord;\n"
  "\n"
  "void main()\n"
  "{\n"
  "  // Set fragment color from texture\n"
  "#if 0 // test check tex coords\n"
  "  gl_FragColor = vec4(1, v_texcoord.x, v_texcoord.y, 1);\n"
  "#else // (not) 0;\n"
  "  gl_FragColor = texture2D(texture, v_texcoord);\n"
  "#endif // 0\n"
  "}\n";

 struct Vertex {
   QVector3D coord;
   QVector2D texCoord;
   Vertex(qreal x, qreal y, qreal z, qreal s, qreal t):
     coord(x, y, z), texCoord(s, t)
   { }
 };

class OpenGLWidget: public QOpenGLWidget, public QOpenGLFunctions_4_0_Core {
  private:
    const Image &_img;
    QOpenGLShaderProgram _qGLSProg;
    QOpenGLBuffer _qGLBufArray;
    QOpenGLBuffer _qGLBufIndex;
    QOpenGLTexture *_pQGLTex;

  public:
    explicit OpenGLWidget(const Image &img):
      QOpenGLWidget(nullptr),
      _img(img),
      _qGLBufArray(QOpenGLBuffer::VertexBuffer),
      _qGLBufIndex(QOpenGLBuffer::IndexBuffer),
      _pQGLTex(nullptr)
    { }
    virtual ~OpenGLWidget()
    {
      makeCurrent();
      delete _pQGLTex;
      _qGLBufArray.destroy();
      _qGLBufIndex.destroy();
      doneCurrent();
    }
    // disabled: (to prevent accidental usage)
    OpenGLWidget(const OpenGLWidget&) = delete;
    OpenGLWidget& operator=(const OpenGLWidget&) = delete;

  protected:
    virtual void initializeGL() override
    {
      initializeOpenGLFunctions();
      glClearColor(0, 0, 0, 1);
      initShaders();
      initGeometry();
      initTexture();
    }
    virtual void paintGL() override
    {
      glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
      _pQGLTex->bind();
      QMatrix4x4 mat; mat.setToIdentity();
      _qGLSProg.setUniformValue("mvp_matrix", mat);
      _qGLSProg.setUniformValue("texture", 0);
      // draw geometry
      _qGLBufArray.bind();
      _qGLBufIndex.bind();
      quintptr offset = 0;
      int coordLocation = _qGLSProg.attributeLocation("a_position");
      _qGLSProg.enableAttributeArray(coordLocation);
      _qGLSProg.setAttributeBuffer(coordLocation, GL_FLOAT, offset, 3, sizeof(Vertex));
      offset += sizeof(QVector3D);
      int texCoordLocation = _qGLSProg.attributeLocation("a_texcoord");
      _qGLSProg.enableAttributeArray(texCoordLocation);
      _qGLSProg.setAttributeBuffer(texCoordLocation, GL_FLOAT, offset, 2, sizeof(Vertex));
      glDrawElements(GL_TRIANGLE_STRIP, 4, GL_UNSIGNED_SHORT, 0);
      //glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_SHORT, 0);
    }

  private:
    void initShaders()
    {
      if (!_qGLSProg.addShaderFromSourceCode(QOpenGLShader::Vertex,
        QString::fromLatin1(vertexShader))) close();
      if (!_qGLSProg.addShaderFromSourceCode(QOpenGLShader::Fragment,
        QString::fromLatin1(fragmentShader))) close();
      if (!_qGLSProg.link()) close();
      if (!_qGLSProg.bind()) close();
    }
    void initGeometry()
    {
      Vertex vertices[] = {
        //  x      y      z     s     t
        { -1.0f, -1.0f, 0.0f, 0.0f, 0.0f },
        { +1.0f, -1.0f, 0.0f, 1.0f, 0.0f },
        { +1.0f, +1.0f, 0.0f, 1.0f, 1.0f },
        { -1.0f, +1.0f, 0.0f, 0.0f, 1.0f }
      };
      enum { nVtcs = sizeof vertices / sizeof *vertices };
      // OpenGL ES doesn't have QUAD. A TRIANGLE_STRIP does as well.
      GLushort indices[] = { 3, 0, 2, 1 };
      //GLushort indices[] = { 0, 1, 2, 0, 2, 3 };
      enum { nIdcs = sizeof indices / sizeof *indices };
      _qGLBufArray.create();
      _qGLBufArray.bind();
      _qGLBufArray.allocate(vertices, nVtcs * sizeof (Vertex));
      _qGLBufIndex.create();
      _qGLBufIndex.bind();
      _qGLBufIndex.allocate(indices, nIdcs * sizeof (GLushort));
    }
    void initTexture()
    {
#if 0 // test whether texturing works at all
      //_pQGLTex = new QOpenGLTexture(QImage("test.png").mirrored());
      _pQGLTex = new QOpenGLTexture(QImage("test-dxt1.dds").mirrored());
#else // (not) 0
      _pQGLTex = new QOpenGLTexture(QOpenGLTexture::Target2D);
      _pQGLTex->setFormat(QOpenGLTexture::RGB_DXT1);
      _pQGLTex->setSize(_img.w, _img.h);
      _pQGLTex->allocateStorage(QOpenGLTexture::RGBA, QOpenGLTexture::UInt8);
      _pQGLTex->setCompressedData((int)_img.data.size(), _img.data.data());
#endif // 0
      _pQGLTex->setMinificationFilter(QOpenGLTexture::Nearest);
      _pQGLTex->setMagnificationFilter(QOpenGLTexture::Nearest);
      _pQGLTex->setWrapMode(QOpenGLTexture::ClampToEdge);
    }
};

int main(int argc, char **argv)
{
  qDebug() << "Qt Version:" << QT_VERSION_STR;
  QApplication app(argc, argv);
  // load a DDS image to get DTX1 raw data
  Image img = loadDXT1("test-dxt1.dds");
  // setup GUI
  QMainWindow qWin;
  OpenGLWidget qGLView(img);
  /* I apply brute-force to get main window to sufficient size
   * -> not appropriate for a productive solution...
   */
  qGLView.setMinimumSize(img.w, img.h);
  qWin.setCentralWidget(&qGLView);
  qWin.show();
  // exec. application
  return app.exec();
}

对于测试,我(再次)使用了样本文件test-dxt1.dds

这就是它的样子(使用VS2013和Qt 5.9.2编译的样本):

Snapshot of DXT1-QTexture-QImage

备注:

  1. 纹理显示为颠倒。请考虑原始样本以及QImage的纹理加载的我的(排除的)代码应用QImage::mirror()。似乎QImage存储了从上到下的数据,其中OpenGL纹理期望相反 - 从下到上。我想最简单的方法就是在纹理转换回QImage之后解决这个问题。

  2. 我的初衷是实现将纹理读回QImage的部分(如问题中描述/概述)。一般来说,我已经在OpenGL中做了类似的事情(但没有Qt)。 (我最近发布了另一个答案OpenGL offscreen render。我不得不承认,由于“超时”问题,我不得不取消此计划。这是由我需要的一些问题引起的很长时间才能解决它们。我将在下面分享这些经验,因为我认为这对其他人有帮助。

  3. 要查找使用DXT1数据初始化QOpenGLTexture的示例代码,我做了一个长期的谷歌研究 - 没有成功。因此,我仔细检查了Qt doc。 QOpenGLTexture的方法看起来有希望/必要的方法来使它工作。 (我必须承认我已经成功地完成了OpenGL纹理,但是在纯OpenGL中。)最后,我得到了实际的必要函数集。它编译并开始,但我得到的只是一个黑色的窗口。 (每次,我用OpenGL开始新的东西,它首先在黑色或蓝色的窗口中结束 - 取决于我使用resp的清晰颜色......)所以,我看了woboq.org上的qopengltexture.cpp(特别是在QOpenGLTexture::QOpenGLTexture(QImage&, ...))的实施中。这并没有多大帮助 - 他们的表现与我尝试过的非常相似。

  4. 最重要的问题是,我可以与一位提出最终提示的同事讨论此计划:我尝试使用QOpenGLFunctions来运行此程序。最后的步骤(朝着最后的解决方案)正在尝试用这个 _pQGLTex = new QOpenGLTexture(QImage("test.png").mirrored());
    (工作)和
    _pQGLTex = new QOpenGLTexture(QImage("test-dxt1.dds").mirrored());
    (没用) 这带来了QOpenGLFunctions(声称与OpenGL ES 2.0兼容)似乎没有启用S3纹理加载的想法。因此,我们将QOpenGLFunctions替换为QOpenGLFunctions_4_0_Core,突然之间,它发挥了作用。

  5. 我没有重载QOpenGLWidget::resizeGL()方法,因为我使用单位矩阵进行OpenGL渲染的模型 - 视图 - 投影变换。这旨在使模型空间和剪辑空间相同。相反,我构建了一个矩形(-1,-1,0) - (+ 1,+ 1,0),它应该完全填充剪辑空间xy平面(可见部分)。(

  6. 可以通过在着色器中启用左侧调试代码来直观地检查 gl_FragColor = vec4(1, v_texcoord.x, v_texcoord.y, 1);
    它使用纹理坐标本身作为绿色和蓝色组件。这是一个漂亮的彩色矩形,左下角为红色,左上角为品红色(红色和蓝色),右下角为黄色(红色和绿色),白色(红色,绿色和蓝色)为右上角。它表明矩形非常适合。

  7. 当我将OpenGLWidget的最小尺寸强制为纹理图像的精确尺寸时,纹理到像素的映射应为1:1。我查看了如果将放大倍数设置为Nearest会发生什么 - 没有视觉差异。

  8. 我必须承认,呈现为纹理的DXT1数据看起来比my other answer中公开的解压缩效果要好得多。考虑到这些是完全相同的数据(由我几乎相同的加载器读取)这让我认为我自己的解压缩算法还没有考虑到某些东西(换句话说:它似乎仍然是错误的)。嗯...(似乎需要额外修复。)