summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorMichael Brasser <mbrasser@ford.com>2019-03-27 15:10:36 -0500
committerMichael Brasser <mbrasser@ford.com>2019-07-09 07:56:35 -0500
commit17a627288452ae8fb9359839571630eb009bd667 (patch)
tree3e3d4316f98c53e541ef21e4af3bf161212b8014
parent1eded2ea43beff4ea10d178da2e054ef81d2d69c (diff)
Basic KTX support for Qt 3D
Adapt the KTX loading code from Qt 3D Studio. Currently only supports a limited subset of texture types. [ChangeLog] Add basic support for KTX container format. Change-Id: Iccad368ed5db571f1f896fb77889db16b8d4b794 Reviewed-by: Paul Lemire <paul.lemire@kdab.com>
-rw-r--r--src/render/texture/qtexture.cpp143
-rw-r--r--src/render/texture/qtextureimagedata.cpp27
-rw-r--r--src/render/texture/qtextureimagedata_p.h13
3 files changed, 173 insertions, 10 deletions
diff --git a/src/render/texture/qtexture.cpp b/src/render/texture/qtexture.cpp
index 5fd2f4169..fb9b542ca 100644
--- a/src/render/texture/qtexture.cpp
+++ b/src/render/texture/qtexture.cpp
@@ -40,6 +40,7 @@
#include "qtextureimage.h"
#include "qabstracttextureimage.h"
#include "qtextureimage_p.h"
+#include "qtextureimagedata_p.h"
#include "qtexturedata.h"
#include "qtexture.h"
#include "qtexture_p.h"
@@ -451,7 +452,8 @@ enum ImageFormat {
GenericImageFormat = 0,
DDS,
PKM,
- HDR
+ HDR,
+ KTX
};
ImageFormat imageFormatFromSuffix(const QString &suffix)
@@ -462,9 +464,144 @@ ImageFormat imageFormatFromSuffix(const QString &suffix)
return DDS;
if (suffix == QStringLiteral("hdr"))
return HDR;
+ if (suffix == QStringLiteral("ktx"))
+ return KTX;
return GenericImageFormat;
}
+// NOTE: the ktx loading code is a near-duplication of the code in qt3d-runtime, and changes
+// should be kept up to date in both locations.
+quint32 blockSizeForTextureFormat(QOpenGLTexture::TextureFormat format)
+{
+ switch (format) {
+ case QOpenGLTexture::RGB8_ETC1:
+ case QOpenGLTexture::RGB8_ETC2:
+ case QOpenGLTexture::SRGB8_ETC2:
+ case QOpenGLTexture::RGB8_PunchThrough_Alpha1_ETC2:
+ case QOpenGLTexture::SRGB8_PunchThrough_Alpha1_ETC2:
+ case QOpenGLTexture::R11_EAC_UNorm:
+ case QOpenGLTexture::R11_EAC_SNorm:
+ case QOpenGLTexture::RGB_DXT1:
+ return 8;
+
+ default:
+ return 16;
+ }
+}
+
+QTextureImageDataPtr setKtxFile(QIODevice *source)
+{
+ static const int KTX_IDENTIFIER_LENGTH = 12;
+ static const char ktxIdentifier[KTX_IDENTIFIER_LENGTH] = { '\xAB', 'K', 'T', 'X', ' ', '1', '1', '\xBB', '\r', '\n', '\x1A', '\n' };
+ static const quint32 platformEndianIdentifier = 0x04030201;
+ static const quint32 inversePlatformEndianIdentifier = 0x01020304;
+
+ struct KTXHeader {
+ quint8 identifier[KTX_IDENTIFIER_LENGTH];
+ quint32 endianness;
+ quint32 glType;
+ quint32 glTypeSize;
+ quint32 glFormat;
+ quint32 glInternalFormat;
+ quint32 glBaseInternalFormat;
+ quint32 pixelWidth;
+ quint32 pixelHeight;
+ quint32 pixelDepth;
+ quint32 numberOfArrayElements;
+ quint32 numberOfFaces;
+ quint32 numberOfMipmapLevels;
+ quint32 bytesOfKeyValueData;
+ };
+
+ KTXHeader header;
+ QTextureImageDataPtr imageData;
+ if (source->read(reinterpret_cast<char *>(&header), sizeof(header)) != sizeof(header)
+ || qstrncmp(reinterpret_cast<char *>(header.identifier), ktxIdentifier, KTX_IDENTIFIER_LENGTH) != 0
+ || (header.endianness != platformEndianIdentifier && header.endianness != inversePlatformEndianIdentifier))
+ {
+ return imageData;
+ }
+
+ const bool isInverseEndian = (header.endianness == inversePlatformEndianIdentifier);
+ auto decode = [isInverseEndian](quint32 val) {
+ return isInverseEndian ? qbswap<quint32>(val) : val;
+ };
+
+ const bool isCompressed = decode(header.glType) == 0 && decode(header.glFormat) == 0 && decode(header.glTypeSize) == 1;
+ if (!isCompressed) {
+ qWarning("Uncompressed ktx texture data is not supported");
+ return imageData;
+ }
+
+ if (decode(header.numberOfArrayElements) != 0) {
+ qWarning("Array ktx textures not supported");
+ return imageData;
+ }
+
+ if (decode(header.pixelDepth) != 0) {
+ qWarning("Only 2D and cube ktx textures are supported");
+ return imageData;
+ }
+
+ const int bytesToSkip = decode(header.bytesOfKeyValueData);
+ if (source->read(bytesToSkip).count() != bytesToSkip) {
+ qWarning("Unexpected end of ktx data");
+ return imageData;
+ }
+
+ const int level0Width = decode(header.pixelWidth);
+ const int level0Height = decode(header.pixelHeight);
+ const int faceCount = decode(header.numberOfFaces);
+ const int mipMapLevels = decode(header.numberOfMipmapLevels);
+ const QOpenGLTexture::TextureFormat format = QOpenGLTexture::TextureFormat(decode(header.glInternalFormat));
+ const int blockSize = blockSizeForTextureFormat(format);
+
+ // now for each mipmap level we have (arrays and 3d textures not supported here)
+ // uint32 imageSize
+ // for each array element
+ // for each face
+ // for each z slice
+ // compressed data
+ // padding so that each face data starts at an offset that is a multiple of 4
+ // padding so that each imageSize starts at an offset that is a multiple of 4
+
+ // assumes no depth or uncompressed textures (per above)
+ auto computeMipMapLevelSize = [&] (int level) {
+ const int w = qMax(level0Width >> level, 1);
+ const int h = qMax(level0Height >> level, 1);
+ return ((w + 3) / 4) * ((h + 3) / 4) * blockSize;
+ };
+
+ int dataSize = 0;
+ for (auto i = 0; i < mipMapLevels; ++i)
+ dataSize += computeMipMapLevelSize(i) * faceCount + 4; // assumes a single layer (per above)
+
+ const QByteArray rawData = source->read(dataSize);
+ if (rawData.size() < dataSize) {
+ qWarning() << "Unexpected end of data in" << source;
+ return imageData;
+ }
+
+ if (!source->atEnd())
+ qWarning() << "Unrecognized data in" << source;
+
+ imageData = QTextureImageDataPtr::create();
+ imageData->setTarget(faceCount == 6 ? QOpenGLTexture::TargetCubeMap : QOpenGLTexture::Target2D);
+ imageData->setFormat(format);
+ imageData->setWidth(level0Width);
+ imageData->setHeight(level0Height);
+ imageData->setLayers(1);
+ imageData->setDepth(1);
+ imageData->setFaces(faceCount);
+ imageData->setMipLevels(mipMapLevels);
+ imageData->setPixelFormat(QOpenGLTexture::NoSourceFormat);
+ imageData->setPixelType(QOpenGLTexture::NoPixelType);
+ imageData->setData(rawData, blockSize, true);
+ QTextureImageDataPrivate::get(imageData.data())->m_isKtx = true; // see note in QTextureImageDataPrivate
+
+ return imageData;
+}
+
QTextureImageDataPtr setPkmFile(QIODevice *source)
{
QTextureImageDataPtr imageData;
@@ -889,6 +1026,10 @@ QTextureImageDataPtr TextureLoadingHelper::loadTextureData(QIODevice *data, cons
case HDR:
textureData = setHdrFile(data);
break;
+ case KTX: {
+ textureData = setKtxFile(data);
+ break;
+ }
default: {
QImage img;
if (img.load(data, suffix.toLatin1())) {
diff --git a/src/render/texture/qtextureimagedata.cpp b/src/render/texture/qtextureimagedata.cpp
index bf43a6e16..28d296feb 100644
--- a/src/render/texture/qtextureimagedata.cpp
+++ b/src/render/texture/qtextureimagedata.cpp
@@ -59,9 +59,25 @@ QTextureImageDataPrivate::QTextureImageDataPrivate()
, m_pixelFormat(QOpenGLTexture::RGBA)
, m_pixelType(QOpenGLTexture::UInt8)
, m_isCompressed(false)
+ , m_isKtx(false)
{
}
+QByteArray QTextureImageDataPrivate::ktxData(int layer, int face, int mipmapLevel) const
+{
+ Q_ASSERT(layer >= 0 && layer < m_layers &&
+ face >= 0 && face < m_faces &&
+ mipmapLevel >= 0 && mipmapLevel < m_mipLevels);
+
+ int offset = 0;
+ for (int i = 0; i < mipmapLevel; i++)
+ offset += (mipmapLevelSize(i) * m_faces * m_layers) + 4;
+ const int selectedMipmapLevelSize = mipmapLevelSize(mipmapLevel);
+ offset += (selectedMipmapLevelSize * m_faces * layer) + (selectedMipmapLevelSize * face) + 4;
+
+ return QByteArray::fromRawData(m_data.constData() + offset, selectedMipmapLevelSize);
+}
+
QByteArray QTextureImageDataPrivate::data(int layer, int face, int mipmapLevel) const
{
if (layer < 0 || layer >= m_layers ||
@@ -71,7 +87,10 @@ QByteArray QTextureImageDataPrivate::data(int layer, int face, int mipmapLevel)
return QByteArray();
}
- int offset = layer * layerSize() + face * faceSize();
+ if (m_isKtx)
+ return ktxData(layer, face, mipmapLevel);
+
+ int offset = layer * ddsLayerSize() + face * ddsFaceSize();
for (int i = 0; i < mipmapLevel; i++)
offset += mipmapLevelSize(i);
@@ -93,12 +112,12 @@ void QTextureImageDataPrivate::setData(const QByteArray &data,
m_blockSize = blockSize;
}
-int QTextureImageDataPrivate::layerSize() const
+int QTextureImageDataPrivate::ddsLayerSize() const
{
- return m_faces * faceSize();
+ return m_faces * ddsFaceSize();
}
-int QTextureImageDataPrivate::faceSize() const
+int QTextureImageDataPrivate::ddsFaceSize() const
{
int size = 0;
diff --git a/src/render/texture/qtextureimagedata_p.h b/src/render/texture/qtextureimagedata_p.h
index 8bb836d5b..d9a0952de 100644
--- a/src/render/texture/qtextureimagedata_p.h
+++ b/src/render/texture/qtextureimagedata_p.h
@@ -82,17 +82,20 @@ public:
QOpenGLTexture::PixelType m_pixelType;
bool m_isCompressed;
+ // ### Qt 6
+ // QTextureImageData was originally written with assumptions around the internal data format
+ // matching dds layout. This is an ugly, but easy, way to add basic ktx support without any
+ // public API changes. Consider https://codereview.qt-project.org/#/c/178474/ for Qt 6.
+ bool m_isKtx;
QByteArray m_data;
static QTextureImageDataPrivate *get(QTextureImageData *imageData);
private:
- int layerSize() const;
- int faceSize() const;
+ int ddsLayerSize() const;
+ int ddsFaceSize() const;
int mipmapLevelSize(int level) const;
-
- bool setPkmFile(const QString &source);
- bool setDdsFile(const QString &source);
+ QByteArray ktxData(int layer, int face, int mipmapLevel) const;
};
} // namespace Qt3DRender