Skip to content

Commit 9d76a4d

Browse files
mkrusjalbamon
authored andcommitted
Support GLB files
Loads binary gltf files
1 parent 36e1e67 commit 9d76a4d

File tree

12 files changed

+263
-17
lines changed

12 files changed

+263
-17
lines changed

src/core/gltf2importer/bufferparser.cpp

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,10 +97,13 @@ bool BufferParser::parse(const QJsonArray &buffersArray, GLTF2Context *context)
9797
return false;
9898
}
9999
}
100+
} else if (!context->bufferChunk().isEmpty() && bufferId == 0) {
101+
readSuccess = true;
102+
context->addBuffer(context->bufferChunk());
100103
}
101104

102105
if (!readSuccess) {
103-
qCWarning(kuesa) << "Failed to read buffer" << bufferName;
106+
qCWarning(kuesa) << "Failed to read buffer" << bufferName << " (" << bufferId << ")";
104107
return false;
105108
}
106109
}

src/core/gltf2importer/gltf2context.cpp

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -370,6 +370,16 @@ void GLTF2Context::addLocalFile(const QString &file)
370370
m_localFiles.push_back(file);
371371
}
372372

373+
QByteArray GLTF2Context::bufferChunk() const
374+
{
375+
return m_bufferChunk;
376+
}
377+
378+
void GLTF2Context::setBufferChunk(const QByteArray &bufferChunk)
379+
{
380+
m_bufferChunk = bufferChunk;
381+
}
382+
373383
template<>
374384
int GLTF2Context::count<Mesh>() const
375385
{

src/core/gltf2importer/gltf2context_p.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,9 @@ class KUESA_PRIVATE_EXPORT GLTF2Context
153153
const QStringList &localFiles() const;
154154
void addLocalFile(const QString &file);
155155

156+
QByteArray bufferChunk() const;
157+
void setBufferChunk(const QByteArray &bufferChunk);
158+
156159
private:
157160
QVector<Accessor> m_accessors;
158161
QVector<QByteArray> m_buffers;
@@ -173,6 +176,7 @@ class KUESA_PRIVATE_EXPORT GLTF2Context
173176
QString m_filename;
174177
QJsonDocument m_json;
175178
QStringList m_localFiles;
179+
QByteArray m_bufferChunk;
176180
};
177181

178182
template<>

src/core/gltf2importer/gltf2parser.cpp

Lines changed: 115 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,17 @@ using namespace Kuesa;
7474
using namespace GLTF2Import;
7575

7676
namespace {
77+
78+
const quint32 GLTF_BINARY_MAGIC = 0x46546C67;
79+
const quint32 GLTF_CHUNCK_JSON = 0x4E4F534A;
80+
const quint32 GLTF_CHUNCK_BIN = 0x004E4942;
81+
82+
struct GLBHeader {
83+
quint32 magic;
84+
quint32 version;
85+
quint32 length;
86+
};
87+
7788
template<class CollectionType>
7889
void addToCollectionWithUniqueName(CollectionType *collection, const QString &basename, typename CollectionType::ContentType *asset)
7990
{
@@ -149,8 +160,42 @@ Qt3DCore::QEntity *GLTF2Parser::parse(const QString &filePath)
149160
}
150161

151162
QFileInfo finfo(filePath);
152-
const QByteArray jsonData = f.readAll();
153-
return parse(jsonData, finfo.absolutePath(), finfo.fileName());
163+
QByteArray data = f.readAll();
164+
return parse(data, finfo.absolutePath(), finfo.fileName());
165+
}
166+
167+
Qt3DCore::QEntity *GLTF2Parser::parse(const QByteArray &data, const QString &basePath, const QString &filename)
168+
{
169+
bool isValid = false;
170+
171+
if (isBinaryGLTF(data, isValid)) {
172+
return isValid ? parseBinary(data, basePath, filename) : nullptr;
173+
} else {
174+
return isValid ? parseJSON(data, basePath, filename) : nullptr;
175+
}
176+
}
177+
178+
bool GLTF2Parser::isBinaryGLTF(const QByteArray &data, bool &isValid)
179+
{
180+
bool isBinary = true;
181+
isValid = true;
182+
183+
if (data.size() < sizeof(GLBHeader))
184+
return false;
185+
const GLBHeader *glbHeader = reinterpret_cast<const GLBHeader *>(data.constData());
186+
if (glbHeader->magic != GLTF_BINARY_MAGIC) {
187+
isBinary = false;
188+
} else {
189+
if (glbHeader->version != 2) {
190+
qCWarning(kuesa()) << "Unsupported glb version" << glbHeader->version;
191+
isValid = false;
192+
} else if (glbHeader->length != data.size()) {
193+
qCWarning(kuesa()) << "Unexpected glb file size" << data.size() << ". Was expecting" << glbHeader->length;
194+
isValid = false;
195+
}
196+
}
197+
198+
return isBinary;
154199
}
155200

156201
template<class T>
@@ -283,7 +328,7 @@ QVector<KeyParserFuncPair> GLTF2Parser::prepareParsers()
283328
};
284329
}
285330

286-
Qt3DCore::QEntity *GLTF2Parser::parse(const QByteArray &jsonData, const QString &basePath, const QString &filename)
331+
Qt3DCore::QEntity *GLTF2Parser::parseJSON(const QByteArray &jsonData, const QString &basePath, const QString &filename)
287332
{
288333
QJsonDocument jsonDocument = QJsonDocument::fromJson(jsonData);
289334
if (jsonDocument.isNull() || !jsonDocument.isObject()) {
@@ -294,13 +339,13 @@ Qt3DCore::QEntity *GLTF2Parser::parse(const QByteArray &jsonData, const QString
294339
Q_ASSERT(m_context);
295340
m_context->setJson(jsonDocument);
296341
m_context->setFilename(filename);
342+
m_basePath = basePath;
297343

298344
m_animators.clear();
299345
m_treeNodes.clear();
300346
m_skeletons.clear();
301347
m_gltfJointIdxToSkeletonJointIdxPerSkeleton.clear();
302348

303-
m_basePath = basePath;
304349
const QJsonObject rootObject = jsonDocument.object();
305350

306351
if (rootObject.contains(KEY_EXTENSIONS_USED) && rootObject.value(KEY_EXTENSIONS_USED).isArray()) {
@@ -476,6 +521,72 @@ Qt3DCore::QEntity *GLTF2Parser::parse(const QByteArray &jsonData, const QString
476521
return gltfSceneEntity;
477522
}
478523

524+
Qt3DCore::QEntity *GLTF2Parser::parseBinary(const QByteArray &data, const QString &basePath, const QString &filename)
525+
{
526+
QByteArray jsonData;
527+
const char *current = data.constData();
528+
current += sizeof(GLBHeader);
529+
int currentOffset = sizeof(GLBHeader);
530+
const int endOffset = data.size();
531+
532+
while (currentOffset < endOffset) {
533+
struct ChunkHeader {
534+
quint32 chunkLength;
535+
quint32 chunkType;
536+
};
537+
538+
if ((endOffset - currentOffset) < sizeof(ChunkHeader)) {
539+
qCWarning(kuesa()) << "Malformed chunck in glb file";
540+
return nullptr;
541+
}
542+
543+
const ChunkHeader *chunkHeader = reinterpret_cast<const ChunkHeader *>(current);
544+
current += sizeof(ChunkHeader);
545+
currentOffset += sizeof(ChunkHeader);
546+
547+
if ((endOffset - currentOffset) < chunkHeader->chunkLength) {
548+
qCWarning(kuesa()) << "Malformed chunck in glb file";
549+
return nullptr;
550+
}
551+
552+
if (chunkHeader->chunkType == GLTF_CHUNCK_JSON) {
553+
// JSON chunck
554+
if (jsonData.isEmpty()) {
555+
jsonData = QByteArray::fromRawData(current, chunkHeader->chunkLength);
556+
} else {
557+
qCWarning(kuesa()) << "Multiple JSON chunks in glb file";
558+
return nullptr;
559+
}
560+
} else if (chunkHeader->chunkType == GLTF_CHUNCK_BIN) {
561+
// BIN chunck
562+
if (m_context->bufferChunk().isEmpty()) {
563+
m_context->setBufferChunk(QByteArray::fromRawData(current, chunkHeader->chunkLength));
564+
} else {
565+
qCWarning(kuesa()) << "Multiple BIN chunks in glb file";
566+
return nullptr;
567+
}
568+
} else {
569+
// skip chunck
570+
qCWarning(kuesa()) << "Ignoring unhandled chunck type in glb file:" << chunkHeader->chunkType;
571+
}
572+
573+
current += chunkHeader->chunkLength;
574+
currentOffset += chunkHeader->chunkLength;
575+
}
576+
577+
if (jsonData.isEmpty()) {
578+
qCWarning(kuesa()) << "Missing JSON chunk in glb file";
579+
return nullptr;
580+
}
581+
582+
auto res = parseJSON(jsonData, basePath, filename);
583+
584+
// make sure chunk doesn't outlive the original data
585+
m_context->setBufferChunk({});
586+
587+
return res;
588+
}
589+
479590
void GLTF2Parser::setContext(GLTF2Context *ctx)
480591
{
481592
m_context = ctx;

src/core/gltf2importer/gltf2parser_p.h

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,12 +82,15 @@ class KUESA_PRIVATE_EXPORT GLTF2Parser
8282

8383
virtual QVector<KeyParserFuncPair> prepareParsers();
8484
Qt3DCore::QEntity *parse(const QString &filePath);
85-
Qt3DCore::QEntity *parse(const QByteArray &jsonData, const QString &basePath, const QString &filename = {});
85+
Qt3DCore::QEntity *parse(const QByteArray &data, const QString &basePath, const QString &filename = {});
86+
Qt3DCore::QEntity *parseJSON(const QByteArray &jsonData, const QString &basePath, const QString &filename = {});
87+
Qt3DCore::QEntity *parseBinary(const QByteArray &data, const QString &basePath, const QString &filename = {});
8688

8789
void setContext(GLTF2Context *);
8890
const GLTF2Context *context() const;
8991

9092
private:
93+
bool isBinaryGLTF(const QByteArray &data, bool &isValid);
9194
void buildEntitiesAndJointsGraph();
9295
void buildJointHierarchy(const HierarchyNode *node, int &jointAccessor, const Skin &skin, int skinIdx, Qt3DCore::QJoint *parentJoint = nullptr);
9396
void generateTreeNodeContent();

src/core/gltf2importer/imageparser.cpp

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ bool ImageParser::parse(const QJsonArray &imageArray, GLTF2Context *context) con
8282
break;
8383
}
8484
}
85+
image.key = uriString;
8586
} else {
8687
const BufferView bufferData = context->bufferView(bufferViewValue.toInt());
8788
image.data = bufferData.bufferData;
@@ -92,9 +93,11 @@ bool ImageParser::parse(const QJsonArray &imageArray, GLTF2Context *context) con
9293
return false;
9394
}
9495
image.mimeType = mimeTypeValue.toString();
96+
image.key = bufferViewValue.toString();
9597
}
9698

9799
image.name = imageObject[KEY_NAME].toString();
100+
98101
context->addImage(image);
99102
}
100103

src/core/gltf2importer/imageparser_p.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ struct Image {
5858
QString name;
5959
QByteArray data;
6060
QString mimeType;
61+
QString key;
6162
};
6263

6364
class Q_AUTOTEST_EXPORT ImageParser

src/core/gltf2importer/textureparser.cpp

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ class EmbeddedTextureImage : public Qt3DRender::QAbstractTextureImage
9494

9595
bool TextureParser::parse(const QJsonArray &texturesArray, GLTF2Context *context) const
9696
{
97-
QHash<QUrl, Qt3DRender::QAbstractTextureImage *> sharedImages;
97+
QHash<QString, Qt3DRender::QAbstractTextureImage *> sharedImages;
9898

9999
for (const auto &textureValue : texturesArray) {
100100
const auto &textureObject = textureValue.toObject();
@@ -121,8 +121,10 @@ bool TextureParser::parse(const QJsonArray &texturesArray, GLTF2Context *context
121121
}
122122

123123
const auto image = context->image(sourceValue.toInt());
124-
if (image.url.isEmpty() && image.data.isEmpty())
124+
if (image.url.isEmpty() && image.data.isEmpty()) {
125+
qCWarning(kuesa) << "Invalid image source index for texture:" << sourceValue.toInt();
125126
return false; // Not a valid image
127+
}
126128

127129
auto texture2d = std::unique_ptr<Qt3DRender::QAbstractTexture>(nullptr);
128130
if (isDDSTexture) {
@@ -137,21 +139,24 @@ bool TextureParser::parse(const QJsonArray &texturesArray, GLTF2Context *context
137139
}
138140
} else {
139141
texture2d.reset(new Qt3DRender::QTexture2D);
140-
auto *textureImage = sharedImages.value(image.url);
142+
auto *textureImage = image.key.isEmpty() ? nullptr : sharedImages.value(image.key);
141143

142144
if (textureImage == nullptr) {
143145
if (image.data.isEmpty()) {
144146
auto ti = new Qt3DRender::QTextureImage();
145147
ti->setSource(image.url);
146148
ti->setMirrored(false);
147149
textureImage = ti;
148-
sharedImages.insert(image.url, textureImage);
149150
} else {
150151
QImage qimage;
151-
qimage.loadFromData(image.data);
152+
if (!qimage.loadFromData(image.data)) {
153+
qCWarning(kuesa) << "Failed to decode image " << sourceValue.toInt() << "from buffer";
154+
return false;
155+
}
152156
textureImage = new EmbeddedTextureImage(qimage);
153-
sharedImages.insert(image.url, textureImage);
154157
}
158+
if (!image.key.isEmpty())
159+
sharedImages.insert(image.key, textureImage);
155160
}
156161

157162
// Add Image to Texture if compatible

tests/auto/assets/Box.glb

1.63 KB
Binary file not shown.

tests/auto/gltfexporter/tst_gltfexporter.cpp

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -307,7 +307,7 @@ private Q_SLOTS:
307307
QVERIFY(exported.success());
308308

309309
ctx = GLTF2Context{};
310-
auto res = parser.parse(QJsonDocument{ exported.json() }.toJson(), tmp.absolutePath(), QStringLiteral("test.gltf"));
310+
auto res = parser.parseJSON(QJsonDocument{ exported.json() }.toJson(), tmp.absolutePath(), QStringLiteral("test.gltf"));
311311
QVERIFY(res != nullptr);
312312

313313
QVERIFY(tmp.exists(exported.compressedBufferFilename()));
@@ -383,7 +383,7 @@ private Q_SLOTS:
383383

384384
// Check that we can reload the mesh properly
385385
ctx = GLTF2Context{};
386-
auto res = parser.parse(QJsonDocument{ exported.json() }.toJson(), tmp.absolutePath(), QStringLiteral("test.gltf"));
386+
auto res = parser.parseJSON(QJsonDocument{ exported.json() }.toJson(), tmp.absolutePath(), QStringLiteral("test.gltf"));
387387
QVERIFY(res != nullptr);
388388
}
389389
}
@@ -438,7 +438,7 @@ private Q_SLOTS:
438438

439439
// Check that we can reload the mesh properly
440440
ctx = GLTF2Context{};
441-
auto res = parser.parse(QJsonDocument{ exported.json() }.toJson(), tmp.absolutePath(), QStringLiteral("test.gltf"));
441+
auto res = parser.parseJSON(QJsonDocument{ exported.json() }.toJson(), tmp.absolutePath(), QStringLiteral("test.gltf"));
442442
QVERIFY(res != nullptr);
443443
}
444444
}
@@ -548,7 +548,7 @@ private Q_SLOTS:
548548
QCOMPARE(sub.count(), 1U);
549549

550550
ctx = GLTF2Context{};
551-
auto res = parser.parse(QJsonDocument{ exported.json() }.toJson(), sub.absolutePath(), QStringLiteral("test.gltf"));
551+
auto res = parser.parseJSON(QJsonDocument{ exported.json() }.toJson(), sub.absolutePath(), QStringLiteral("test.gltf"));
552552
QVERIFY(res != nullptr);
553553
}
554554
}

0 commit comments

Comments
 (0)