diff options
author | Michael Brasser <mbrasser@ford.com> | 2019-03-27 15:10:36 -0500 |
---|---|---|
committer | Michael Brasser <mbrasser@ford.com> | 2019-07-09 07:56:35 -0500 |
commit | 17a627288452ae8fb9359839571630eb009bd667 (patch) | |
tree | 3e3d4316f98c53e541ef21e4af3bf161212b8014 | |
parent | 1eded2ea43beff4ea10d178da2e054ef81d2d69c (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.cpp | 143 | ||||
-rw-r--r-- | src/render/texture/qtextureimagedata.cpp | 27 | ||||
-rw-r--r-- | src/render/texture/qtextureimagedata_p.h | 13 |
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 |