Qt
Internal/Contributor docs for the Qt SDK. Note: These are NOT official API docs; those are found at https://doc.qt.io/
Loading...
Searching...
No Matches
qssggltfparser.cpp
Go to the documentation of this file.
1// Copyright (C) 2026 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
3// Qt-Security score:critical reason:data-parser
4
7
8#include <QtCore/qendian.h>
9#include <QtCore/qfile.h>
10#include <QtCore/qfileinfo.h>
11#include <QtCore/qhash.h>
12#include <QtCore/qjsonarray.h>
13#include <QtCore/qjsondocument.h>
14#include <QtCore/qset.h>
15#include <QtCore/qvarlengtharray.h>
16
17#include "meshoptimizer.h"
18
20
21Q_LOGGING_CATEGORY(lcQuick3DGltf, "qt.quick3d.gltf")
22
23using namespace QSSGGltf;
24
25namespace {
26
27// GLB container constants
28constexpr quint32 GLB_MAGIC = 0x46546C67; // 'glTF'
29constexpr quint32 GLB_CHUNK_JSON = 0x4E4F534A; // 'JSON'
30constexpr quint32 GLB_CHUNK_BIN = 0x004E4942; // 'BIN\0'
31
32QVector3D toVector3D(const QJsonArray &array, const QVector3D &defaultValue = {})
33{
34 if (array.size() < 3)
35 return defaultValue;
36 return QVector3D(float(array.at(0).toDouble()), float(array.at(1).toDouble()), float(array.at(2).toDouble()));
37}
38
39QVector4D toVector4D(const QJsonArray &array, const QVector4D &defaultValue = {})
40{
41 if (array.size() < 4)
42 return defaultValue;
43 return QVector4D(float(array.at(0).toDouble()), float(array.at(1).toDouble()),
44 float(array.at(2).toDouble()), float(array.at(3).toDouble()));
45}
46
47TextureInfo parseTextureInfo(const QJsonObject &object, const char *scaleOrStrengthKey = nullptr)
48{
49 TextureInfo info;
50 if (object.isEmpty())
51 return info;
52 info.index = object.value(QLatin1String("index")).toInt(-1);
53 info.texCoord = object.value(QLatin1String("texCoord")).toInt(0);
54 if (scaleOrStrengthKey)
55 info.scaleOrStrength = float(object.value(QLatin1String(scaleOrStrengthKey)).toDouble(1.0));
56
57 const QJsonObject extensions = object.value(QLatin1String("extensions")).toObject();
58 const QJsonValue transformValue = extensions.value(QLatin1String("KHR_texture_transform"));
59 if (transformValue.isObject()) {
60 const QJsonObject t = transformValue.toObject();
61 TextureTransform transform;
62 const QJsonArray offset = t.value(QLatin1String("offset")).toArray();
63 if (offset.size() >= 2)
64 transform.offset = QVector2D(float(offset.at(0).toDouble()), float(offset.at(1).toDouble()));
65 const QJsonArray scale = t.value(QLatin1String("scale")).toArray();
66 if (scale.size() >= 2)
67 transform.scale = QVector2D(float(scale.at(0).toDouble(1.0)), float(scale.at(1).toDouble(1.0)));
68 transform.rotation = float(t.value(QLatin1String("rotation")).toDouble(0.0));
69 transform.texCoord = t.value(QLatin1String("texCoord")).toInt(-1);
70 info.transform = transform;
71 }
72 return info;
73}
74
75Accessor::Type accessorTypeFromString(const QString &type, bool *ok)
76{
77 *ok = true;
78 if (type == QLatin1String("SCALAR"))
79 return Accessor::Type::Scalar;
80 if (type == QLatin1String("VEC2"))
81 return Accessor::Type::Vec2;
82 if (type == QLatin1String("VEC3"))
83 return Accessor::Type::Vec3;
84 if (type == QLatin1String("VEC4"))
85 return Accessor::Type::Vec4;
86 if (type == QLatin1String("MAT2"))
87 return Accessor::Type::Mat2;
88 if (type == QLatin1String("MAT3"))
89 return Accessor::Type::Mat3;
90 if (type == QLatin1String("MAT4"))
91 return Accessor::Type::Mat4;
92 *ok = false;
93 return Accessor::Type::Scalar;
94}
95
96bool isValidComponentType(int value)
97{
98 switch (Accessor::ComponentType(value)) {
99 case Accessor::ComponentType::Byte:
100 case Accessor::ComponentType::UnsignedByte:
101 case Accessor::ComponentType::Short:
102 case Accessor::ComponentType::UnsignedShort:
103 case Accessor::ComponentType::UnsignedInt:
104 case Accessor::ComponentType::Float:
105 return true;
106 }
107 return false;
108}
109
110// Views can alias the same compressed bytes, so the budget is per document
111struct MeshoptDecodeState
112{
113 qint64 remainingBytes = qint64(1024) * 1024 * 1024;
114 QHash<QString, int> decodedBuffers; // decode parameters -> synthetic buffer index
115};
116
117// Returns -1 for anything but a non-negative integer below 2^53
118qint64 toByteCount(const QJsonValue &value, double defaultValue)
119{
120 const double d = value.toDouble(defaultValue);
121 return d >= 0.0 && d <= 9007199254740992.0 ? qint64(d) : -1;
122}
123
124// Decodes into a synthetic buffer and points the view at it
125bool decodeMeshoptBufferView(const QJsonObject &ext, QSSGGltfDocument &document, BufferView &view,
126 MeshoptDecodeState &state, QString *errorMessage)
127{
128 const auto fail = [errorMessage](const QString &message) {
129 *errorMessage = message;
130 return false;
131 };
132
133 const int sourceBuffer = ext.value(QLatin1String("buffer")).toInt(-1);
134 const qint64 byteOffset = toByteCount(ext.value(QLatin1String("byteOffset")), 0.0);
135 const qint64 byteLength = toByteCount(ext.value(QLatin1String("byteLength")), -1.0);
136 const qint64 byteStride = toByteCount(ext.value(QLatin1String("byteStride")), -1.0);
137
138 // Checked as double, since the qint64 product can wrap
139 constexpr qint64 maxDecodedBytes = qint64(1024) * 1024 * 1024;
140 const double rawCount = ext.value(QLatin1String("count")).toDouble();
141 if (!(byteStride > 0 && rawCount > 0.0 && rawCount * double(byteStride) <= double(maxDecodedBytes)))
142 return fail(QStringLiteral("unreasonable decoded data size"));
143 const qint64 count = qint64(rawCount);
144
145 const QString mode = ext.value(QLatin1String("mode")).toString();
146 const QString filter = ext.value(QLatin1String("filter")).toString(QStringLiteral("NONE"));
147
148 if (sourceBuffer < 0 || sourceBuffer >= document.buffers.size()
149 || document.buffers.at(sourceBuffer).meshoptFallback) {
150 return fail(QStringLiteral("compressed data references invalid buffer %1").arg(sourceBuffer));
151 }
152 const Buffer &compressed = document.buffers.at(sourceBuffer);
153 // Not summed, since offset + length can wrap
154 if (byteOffset < 0 || byteLength <= 0 || byteLength > compressed.byteLength
155 || byteOffset > compressed.byteLength - byteLength) {
156 return fail(QStringLiteral("compressed data is outside buffer %1").arg(sourceBuffer));
157 }
158
159 enum class Mode { Attributes, Triangles, Indices };
160 Mode decodeMode;
161 if (mode == QLatin1String("ATTRIBUTES"))
162 decodeMode = Mode::Attributes;
163 else if (mode == QLatin1String("TRIANGLES"))
164 decodeMode = Mode::Triangles;
165 else if (mode == QLatin1String("INDICES"))
166 decodeMode = Mode::Indices;
167 else
168 return fail(QStringLiteral("unsupported compression mode '%1'").arg(mode));
169
170 // Stride limits required by the decoders
171 if (decodeMode == Mode::Attributes) {
172 if (byteStride < 4 || byteStride > 256 || byteStride % 4)
173 return fail(QStringLiteral("invalid byte stride %1 for ATTRIBUTES mode").arg(byteStride));
174 } else {
175 if (byteStride != 2 && byteStride != 4)
176 return fail(QStringLiteral("invalid byte stride %1 for index data").arg(byteStride));
177 if (decodeMode == Mode::Triangles && count % 3)
178 return fail(QStringLiteral("TRIANGLES element count %1 is not divisible by 3").arg(count));
179 if (filter != QLatin1String("NONE"))
180 return fail(QStringLiteral("filters do not apply to index data"));
181 }
182
183 if (decodeMode == Mode::Attributes && filter != QLatin1String("NONE") && filter != QLatin1String("OCTAHEDRAL")
184 && filter != QLatin1String("QUATERNION") && filter != QLatin1String("EXPONENTIAL")) {
185 return fail(QStringLiteral("unsupported filter '%1'").arg(filter));
186 }
187 if ((filter == QLatin1String("OCTAHEDRAL") && byteStride != 4 && byteStride != 8)
188 || (filter == QLatin1String("QUATERNION") && byteStride != 8)) {
189 return fail(QStringLiteral("invalid byte stride %1 for filter %2").arg(byteStride).arg(filter));
190 }
191
192 const QString key = QStringLiteral("%1:%2:%3:%4:%5:%6:%7")
193 .arg(sourceBuffer).arg(byteOffset).arg(byteLength).arg(count)
194 .arg(byteStride).arg(mode, filter);
195 const auto rewriteView = [&view, decodeMode, byteStride](int buffer, qint64 length) {
196 view.buffer = buffer;
197 view.byteOffset = 0;
198 view.byteLength = length;
199 view.byteStride = decodeMode == Mode::Attributes ? int(byteStride) : 0;
200 };
201 if (const auto it = state.decodedBuffers.constFind(key); it != state.decodedBuffers.constEnd()) {
202 rewriteView(*it, document.buffers.at(*it).byteLength);
203 return true;
204 }
205 if (count * byteStride > state.remainingBytes)
206 return fail(QStringLiteral("decoded data exceeds the limit for the document"));
207
208 QByteArray decoded(count * byteStride, Qt::Uninitialized);
209 const auto *source = reinterpret_cast<const unsigned char *>(compressed.data.constData()) + byteOffset;
210 int result = -1;
211 switch (decodeMode) {
212 case Mode::Attributes:
213 result = meshopt_decodeVertexBuffer(decoded.data(), size_t(count), size_t(byteStride), source,
214 size_t(byteLength));
215 break;
216 case Mode::Triangles:
217 result = meshopt_decodeIndexBuffer(decoded.data(), size_t(count), size_t(byteStride), source,
218 size_t(byteLength));
219 break;
220 case Mode::Indices:
221 result = meshopt_decodeIndexSequence(decoded.data(), size_t(count), size_t(byteStride), source,
222 size_t(byteLength));
223 break;
224 }
225 if (result != 0)
226 return fail(QStringLiteral("corrupt compressed data"));
227
228 if (filter == QLatin1String("OCTAHEDRAL"))
229 meshopt_decodeFilterOct(decoded.data(), size_t(count), size_t(byteStride));
230 else if (filter == QLatin1String("QUATERNION"))
231 meshopt_decodeFilterQuat(decoded.data(), size_t(count), size_t(byteStride));
232 else if (filter == QLatin1String("EXPONENTIAL"))
233 meshopt_decodeFilterExp(decoded.data(), size_t(count), size_t(byteStride));
234
235 state.remainingBytes -= decoded.size();
236 Buffer decodedBuffer;
237 decodedBuffer.byteLength = decoded.size();
238 decodedBuffer.data = std::move(decoded);
239 document.buffers.append(decodedBuffer);
240 const int bufferIndex = int(document.buffers.size() - 1);
241 state.decodedBuffers.insert(key, bufferIndex);
242 rewriteView(bufferIndex, decodedBuffer.byteLength);
243 return true;
244}
245
246} // namespace
247
248/*!
249 \class QSSGGltfParser
250 \internal
251
252 Parses glTF 2.0 content (.gltf JSON or binary .glb container) into a
253 QSSGGltfDocument. Buffers (external files, data: URIs, and the GLB BIN
254 chunk) are resolved eagerly; image contents are left unresolved.
255
256 Validation is two-tier: structural violations (out-of-range indices,
257 accessors extending past their buffer view, truncated GLB, unsupported
258 required extensions) fail the parse with an error message; tolerable
259 deviations from the specification are logged as warnings and clamped,
260 repaired, or ignored, matching the leniency of other glTF consumers. A
261 node with more than one parent and a scene root that is not unique both
262 fall in the second group: the offending edge is dropped, because the node
263 hierarchy has to be a forest for consumers to recurse over it safely, but
264 the rest of the asset is still worth loading.
265*/
266
267/*!
268 \internal
269
270 Returns the extensions the parser understands. Assets that list anything
271 else in extensionsRequired fail to parse.
272
273 Being on this list means the extension is represented in QSSGGltfDocument,
274 not that any particular consumer acts on it: a consumer that ignores one
275 of these still gets a document it can load.
276*/
277QStringList QSSGGltfParser::supportedExtensions()
278{
279 // Order does not matter; keep alphabetical for readability. Extensions
280 // needing a decoder we do not ship (KHR_draco_mesh_compression,
281 // KHR_texture_basisu) are intentionally absent and get a clear error
282 // when required by an asset.
283 // KHR_xmp_json_ld is accepted, but left in the raw extension objects.
284 return {
285 QStringLiteral("EXT_mesh_gpu_instancing"),
286 QStringLiteral("EXT_meshopt_compression"),
287 QStringLiteral("EXT_texture_webp"),
288 QStringLiteral("KHR_lights_punctual"),
289 QStringLiteral("KHR_materials_anisotropy"),
290 QStringLiteral("KHR_materials_clearcoat"),
291 QStringLiteral("KHR_materials_dispersion"),
292 QStringLiteral("KHR_materials_emissive_strength"),
293 QStringLiteral("KHR_materials_ior"),
294 QStringLiteral("KHR_materials_iridescence"),
295 QStringLiteral("KHR_materials_pbrSpecularGlossiness"),
296 QStringLiteral("KHR_materials_sheen"),
297 QStringLiteral("KHR_materials_specular"),
298 QStringLiteral("KHR_materials_transmission"),
299 QStringLiteral("KHR_materials_unlit"),
300 QStringLiteral("KHR_materials_variants"),
301 QStringLiteral("KHR_materials_volume"),
302 QStringLiteral("KHR_mesh_quantization"),
303 QStringLiteral("KHR_node_visibility"),
304 QStringLiteral("KHR_texture_transform"),
305 QStringLiteral("KHR_xmp_json_ld"),
306 };
307}
308
309bool QSSGGltfParser::setError(const QString &message)
310{
311 m_errorMessage = message;
312 qCWarning(lcQuick3DGltf) << message;
313 return false;
314}
315
316/*!
317 \internal
318
319 Convenience overload of parse() reading \a filePath, a local file or qrc
320 path, into \a document.
321*/
322bool QSSGGltfParser::parseFile(const QString &filePath, QSSGGltfDocument *document)
323{
324 QFile file(filePath);
325 if (!file.open(QIODevice::ReadOnly))
326 return setError(QStringLiteral("Failed to open '%1': %2").arg(filePath, file.errorString()));
327 return parse(file.readAll(), QFileInfo(filePath).path(), document);
328}
329
330/*!
331 \internal
332
333 Parses .gltf JSON or a .glb container in \a data into \a document.
334 \a baseDir is the local or qrc directory used to resolve relative
335 buffer and image URIs.
336*/
337bool QSSGGltfParser::parse(const QByteArray &data, const QString &baseDir, QSSGGltfDocument *document)
338{
339 Q_ASSERT(document);
340 m_errorMessage.clear();
341 *document = QSSGGltfDocument();
342 document->baseDir = baseDir;
343
344 QByteArray json = data;
345 QByteArray binChunk;
346
347 // Binary container? (12-byte header: magic, version, length)
348 if (data.size() >= 12 && qFromLittleEndian<quint32>(data.constData()) == GLB_MAGIC) {
349 const quint32 version = qFromLittleEndian<quint32>(data.constData() + 4);
350 if (version != 2)
351 return setError(QStringLiteral("Unsupported GLB container version %1").arg(version));
352 const quint32 length = qFromLittleEndian<quint32>(data.constData() + 8);
353 if (qint64(length) > data.size())
354 return setError(QStringLiteral("Truncated GLB file: header declares %1 bytes, got %2")
355 .arg(length).arg(data.size()));
356
357 json.clear();
358 qint64 offset = 12;
359 while (offset + 8 <= qint64(length)) {
360 const quint32 chunkLength = qFromLittleEndian<quint32>(data.constData() + offset);
361 const quint32 chunkType = qFromLittleEndian<quint32>(data.constData() + offset + 4);
362 offset += 8;
363 if (offset + qint64(chunkLength) > qint64(length))
364 return setError(QStringLiteral("Truncated GLB chunk at offset %1").arg(offset - 8));
365
366 if (chunkType == GLB_CHUNK_JSON && json.isEmpty())
367 json = data.mid(offset, chunkLength);
368 else if (chunkType == GLB_CHUNK_BIN && binChunk.isEmpty())
369 binChunk = data.mid(offset, chunkLength);
370 else
371 qCWarning(lcQuick3DGltf) << "Skipping unknown GLB chunk type" << Qt::hex << chunkType;
372
373 // Chunks are 4-byte aligned
374 offset += chunkLength;
375 if (offset % 4)
376 offset += 4 - (offset % 4);
377 }
378 if (json.isEmpty())
379 return setError(QStringLiteral("GLB container has no JSON chunk"));
380 }
381
382 QJsonParseError jsonError;
383 const QJsonDocument jsonDocument = QJsonDocument::fromJson(json, &jsonError);
384 if (jsonDocument.isNull())
385 return setError(QStringLiteral("Invalid glTF JSON: %1").arg(jsonError.errorString()));
386 if (!jsonDocument.isObject())
387 return setError(QStringLiteral("Invalid glTF JSON: root is not an object"));
388
389 const QJsonObject root = jsonDocument.object();
390
391 // asset (required)
392 {
393 const QJsonValue assetValue = root.value(QLatin1String("asset"));
394 if (!assetValue.isObject())
395 return setError(QStringLiteral("Not a glTF document: no asset object"));
396 const QJsonObject asset = assetValue.toObject();
397 document->asset.version = asset.value(QLatin1String("version")).toString();
398 document->asset.minVersion = asset.value(QLatin1String("minVersion")).toString();
399 document->asset.generator = asset.value(QLatin1String("generator")).toString();
400 document->asset.copyright = asset.value(QLatin1String("copyright")).toString();
401
402 const int major = document->asset.version.section(QLatin1Char('.'), 0, 0).toInt();
403 if (major != 2)
404 return setError(QStringLiteral("Unsupported glTF version '%1'").arg(document->asset.version));
405
406 // The specification requires a client to fail when minVersion asks for
407 // more than it implements. Only 2.0 exists so far, so this is about not
408 // silently loading a future asset that says it needs more.
409 if (!document->asset.minVersion.isEmpty()) {
410 const int minMajor = document->asset.minVersion.section(QLatin1Char('.'), 0, 0).toInt();
411 const int minMinor = document->asset.minVersion.section(QLatin1Char('.'), 1, 1).toInt();
412 if (minMajor != 2 || minMinor > 0)
413 return setError(QStringLiteral("Asset requires glTF version '%1' or higher")
414 .arg(document->asset.minVersion));
415 }
416 }
417
418 // extensionsUsed / extensionsRequired
419 const QJsonArray extensionsUsed = root.value(QLatin1String("extensionsUsed")).toArray();
420 for (const auto &value : extensionsUsed)
421 document->extensionsUsed.append(value.toString());
422 const QJsonArray extensionsRequired = root.value(QLatin1String("extensionsRequired")).toArray();
423 for (const auto &value : extensionsRequired)
424 document->extensionsRequired.append(value.toString());
425 document->rootExtensions = root.value(QLatin1String("extensions")).toObject();
426
427 // KHR_materials_variants (document level): the variant names
428 {
429 const QJsonValue variantsExt = document->rootExtensions.value(QLatin1String("KHR_materials_variants"));
430 if (variantsExt.isObject()) {
431 const QJsonArray variants = variantsExt.toObject().value(QLatin1String("variants")).toArray();
432 for (const auto &value : variants)
433 document->materialVariants.append(value.toObject().value(QLatin1String("name")).toString());
434 }
435 }
436
437 const QStringList supported = supportedExtensions();
438 for (const QString &required : std::as_const(document->extensionsRequired)) {
439 if (!supported.contains(required))
440 return setError(QStringLiteral("Asset requires unsupported glTF extension '%1'").arg(required));
441 }
442 for (const QString &used : std::as_const(document->extensionsUsed)) {
443 if (!supported.contains(used))
444 qCWarning(lcQuick3DGltf) << "Ignoring unsupported glTF extension" << used;
445 }
446
447 // buffers
448 const QJsonArray buffers = root.value(QLatin1String("buffers")).toArray();
449 for (const auto &value : buffers) {
450 const QJsonObject object = value.toObject();
451 Buffer buffer;
452 buffer.uri = object.value(QLatin1String("uri")).toString();
453 buffer.byteLength = qint64(object.value(QLatin1String("byteLength")).toDouble());
454 if (buffer.byteLength < 0)
455 return setError(QStringLiteral("Buffer %1 has negative byte length").arg(document->buffers.size()));
456 buffer.meshoptFallback = object.value(QLatin1String("extensions")).toObject()
457 .value(QLatin1String("EXT_meshopt_compression")).toObject()
458 .value(QLatin1String("fallback")).toBool(false);
459 if (buffer.meshoptFallback) {
460 document->buffers.append(buffer);
461 continue;
462 }
463 if (buffer.uri.isEmpty()) {
464 // GLB BIN chunk; only valid for the first buffer
465 if (document->buffers.isEmpty() && !binChunk.isEmpty())
466 buffer.data = binChunk;
467 else if (binChunk.isEmpty())
468 return setError(
469 QStringLiteral("Buffer %1 has no URI and there is no GLB BIN chunk")
470 .arg(document->buffers.size()));
471 else
472 return setError(QStringLiteral("Only the first buffer may refer to the GLB BIN chunk"));
473 } else {
474 QString resolveError;
475 buffer.data = QSSGGltfResourceResolver::loadUri(buffer.uri, baseDir, &resolveError);
476 // Loading can legitimately produce nothing, for an empty file or an
477 // empty data URI payload, so the error string is what says whether
478 // it failed. The length check below rejects a short buffer anyway.
479 if (!resolveError.isEmpty())
480 return setError(QStringLiteral("Failed to load buffer %1: %2")
481 .arg(document->buffers.size()).arg(resolveError));
482 }
483 if (buffer.data.size() < buffer.byteLength)
484 return setError(QStringLiteral("Buffer %1 is %2 bytes, expected at least %3")
485 .arg(document->buffers.size()).arg(buffer.data.size()).arg(buffer.byteLength));
486 document->buffers.append(buffer);
487 }
488
489 // bufferViews
490 MeshoptDecodeState meshoptState;
491 const QJsonArray bufferViews = root.value(QLatin1String("bufferViews")).toArray();
492 for (const auto &value : bufferViews) {
493 const QJsonObject object = value.toObject();
494 BufferView view;
495 view.buffer = object.value(QLatin1String("buffer")).toInt(-1);
496 view.byteOffset = qint64(object.value(QLatin1String("byteOffset")).toDouble(0));
497 view.byteLength = qint64(object.value(QLatin1String("byteLength")).toDouble());
498 view.byteStride = object.value(QLatin1String("byteStride")).toInt(0);
499 view.target = object.value(QLatin1String("target")).toInt(0);
500 view.name = object.value(QLatin1String("name")).toString();
501
502 const QJsonValue meshoptValue = object.value(QLatin1String("extensions")).toObject()
503 .value(QLatin1String("EXT_meshopt_compression"));
504 if (meshoptValue.isObject()) {
505 QString decodeError;
506 if (!decodeMeshoptBufferView(meshoptValue.toObject(), *document, view, meshoptState, &decodeError)) {
507 return setError(QStringLiteral("Buffer view %1: EXT_meshopt_compression: %2")
508 .arg(document->bufferViews.size()).arg(decodeError));
509 }
510 document->bufferViews.append(view);
511 continue;
512 }
513
514 if (view.buffer < 0 || view.buffer >= document->buffers.size())
515 return setError(
516 QStringLiteral("Buffer view %1 references invalid buffer %2")
517 .arg(document->bufferViews.size()).arg(view.buffer));
518 if (document->buffers.at(view.buffer).meshoptFallback)
519 return setError(QStringLiteral("Buffer view %1 references unloaded fallback buffer %2")
520 .arg(document->bufferViews.size()).arg(view.buffer));
521 // The stride bound is from the specification; together with
522 // non-negative offsets and lengths it also keeps all later offset
523 // arithmetic far away from overflowing 64 bits.
524 if (view.byteOffset < 0 || view.byteLength < 0 || view.byteStride < 0 || view.byteStride > 252)
525 return setError(QStringLiteral("Buffer view %1 has an invalid byte offset, length, or stride")
526 .arg(document->bufferViews.size()));
527 // Compared without adding the two together: both are individually
528 // positive by the check above, but their sum can overflow to a negative
529 // value and slip under the buffer length.
530 const qint64 bufferLength = document->buffers.at(view.buffer).byteLength;
531 if (view.byteLength > bufferLength || view.byteOffset > bufferLength - view.byteLength)
532 return setError(
533 QStringLiteral("Buffer view %1 extends past the end of buffer %2")
534 .arg(document->bufferViews.size()).arg(view.buffer));
535 document->bufferViews.append(view);
536 }
537
538 // accessors
539 const QJsonArray accessors = root.value(QLatin1String("accessors")).toArray();
540 for (const auto &value : accessors) {
541 const QJsonObject object = value.toObject();
542 Accessor accessor;
543 accessor.bufferView = object.value(QLatin1String("bufferView")).toInt(-1);
544 accessor.byteOffset = qint64(object.value(QLatin1String("byteOffset")).toDouble(0));
545 const int componentType = object.value(QLatin1String("componentType")).toInt();
546 if (!isValidComponentType(componentType))
547 return setError(QStringLiteral("Accessor %1 has invalid component type %2")
548 .arg(document->accessors.size()).arg(componentType));
549 accessor.componentType = Accessor::ComponentType(componentType);
550 bool typeOk = false;
551 accessor.type = accessorTypeFromString(object.value(QLatin1String("type")).toString(), &typeOk);
552 if (!typeOk)
553 return setError(QStringLiteral("Accessor %1 has invalid type '%2'")
554 .arg(document->accessors.size())
555 .arg(object.value(QLatin1String("type")).toString()));
556 // The specification puts no upper bound on count, but the offset
557 // arithmetic below needs one. A count near the top of the qint64 range
558 // makes those products overflow, which is undefined behavior, and in
559 // practice wraps the range test at the end of this loop into an
560 // acceptance: a ten-line document then yields an accessor claiming
561 // more elements than could ever be allocated. One element occupies at
562 // least one byte, so no real asset comes anywhere near this cap, and
563 // capping in elements keeps every product here well inside 64 bits.
564 constexpr qint64 maxAccessorElements = qint64(256) * 1024 * 1024;
565 const double rawCount = object.value(QLatin1String("count")).toDouble();
566 if (!(rawCount >= 0.0 && rawCount <= double(maxAccessorElements)))
567 return setError(QStringLiteral("Accessor %1 has a negative or unreasonably large count")
568 .arg(document->accessors.size()));
569 accessor.count = qint64(rawCount);
570 if (accessor.byteOffset < 0)
571 return setError(QStringLiteral("Accessor %1 has a negative byte offset")
572 .arg(document->accessors.size()));
573 accessor.normalized = object.value(QLatin1String("normalized")).toBool(false);
574 accessor.name = object.value(QLatin1String("name")).toString();
575 const QJsonArray minValues = object.value(QLatin1String("min")).toArray();
576 for (const auto &m : minValues)
577 accessor.min.append(m.toDouble());
578 const QJsonArray maxValues = object.value(QLatin1String("max")).toArray();
579 for (const auto &m : maxValues)
580 accessor.max.append(m.toDouble());
581
582 const QJsonValue sparseValue = object.value(QLatin1String("sparse"));
583 if (sparseValue.isObject()) {
584 const QJsonObject sparseObject = sparseValue.toObject();
585 Accessor::Sparse sparse;
586 // Bounded the same way as accessor.count, and for the same reason:
587 // it is multiplied by a component size a few lines down
588 const double rawSparseCount = sparseObject.value(QLatin1String("count")).toDouble();
589 if (!(rawSparseCount >= 0.0 && rawSparseCount <= double(maxAccessorElements)))
590 return setError(QStringLiteral("Accessor %1 has a negative or unreasonably large sparse count")
591 .arg(document->accessors.size()));
592 sparse.count = qint64(rawSparseCount);
593 const QJsonObject indices = sparseObject.value(QLatin1String("indices")).toObject();
594 sparse.indicesBufferView = indices.value(QLatin1String("bufferView")).toInt(-1);
595 sparse.indicesByteOffset = qint64(indices.value(QLatin1String("byteOffset")).toDouble(0));
596 const int indicesComponentType = indices.value(QLatin1String("componentType")).toInt();
597 if (!isValidComponentType(indicesComponentType))
598 return setError(
599 QStringLiteral("Accessor %1 sparse indices have invalid component type")
600 .arg(document->accessors.size()));
601 sparse.indicesComponentType = Accessor::ComponentType(indicesComponentType);
602 const QJsonObject values = sparseObject.value(QLatin1String("values")).toObject();
603 sparse.valuesBufferView = values.value(QLatin1String("bufferView")).toInt(-1);
604 sparse.valuesByteOffset = qint64(values.value(QLatin1String("byteOffset")).toDouble(0));
605
606 if (sparse.indicesBufferView < 0 || sparse.indicesBufferView >= document->bufferViews.size()
607 || sparse.valuesBufferView < 0 || sparse.valuesBufferView >= document->bufferViews.size()) {
608 return setError(QStringLiteral("Accessor %1 sparse data references an invalid buffer view")
609 .arg(document->accessors.size()));
610 }
611 if (sparse.count > accessor.count
612 || sparse.indicesByteOffset < 0 || sparse.valuesByteOffset < 0) {
613 return setError(QStringLiteral("Accessor %1 has an invalid sparse count or byte offset")
614 .arg(document->accessors.size()));
615 }
616 // Offsets kept out of the sums, like the buffer view check above
617 const BufferView &indicesView = document->bufferViews.at(sparse.indicesBufferView);
618 const BufferView &valuesView = document->bufferViews.at(sparse.valuesBufferView);
619 const qint64 indicesSpan =
620 sparse.count * Accessor::componentByteSize(sparse.indicesComponentType);
621 const qint64 valuesSpan = sparse.count * accessor.elementByteSize();
622 if (indicesSpan > indicesView.byteLength
623 || sparse.indicesByteOffset > indicesView.byteLength - indicesSpan
624 || valuesSpan > valuesView.byteLength
625 || sparse.valuesByteOffset > valuesView.byteLength - valuesSpan) {
626 return setError(
627 QStringLiteral("Accessor %1 sparse data extends past the end of its buffer view")
628 .arg(document->accessors.size()));
629 }
630 accessor.sparse = sparse;
631 }
632
633 if (accessor.bufferView >= document->bufferViews.size())
634 return setError(
635 QStringLiteral("Accessor %1 references invalid buffer view %2")
636 .arg(document->accessors.size()).arg(accessor.bufferView));
637 if (accessor.bufferView >= 0) {
638 const BufferView &view = document->bufferViews.at(accessor.bufferView);
639 const qint64 elementSize = accessor.elementByteSize();
640 const qint64 stride = view.byteStride > 0 ? view.byteStride : elementSize;
641 // Offset kept out of the sum, like the buffer view check above
642 const qint64 span = (accessor.count - 1) * stride + elementSize;
643 if (accessor.count > 0
644 && (span > view.byteLength || accessor.byteOffset > view.byteLength - span))
645 return setError(QStringLiteral("Accessor %1 extends past the end of buffer view %2")
646 .arg(document->accessors.size())
647 .arg(accessor.bufferView));
648 } else {
649 // An accessor without a buffer view is read as all zeros, so no
650 // buffer bounds its size. The element cap above keeps this product
651 // from overflowing, but 256M elements of MAT4 would still be a
652 // 16 GB allocation, so bound the byte count as well.
653 constexpr qint64 maxSyntheticAccessorBytes = qint64(256) * 1024 * 1024;
654 if (accessor.count * accessor.elementByteSize() > maxSyntheticAccessorBytes)
655 return setError(QStringLiteral("Accessor %1 has no buffer view and an unreasonably large count")
656 .arg(document->accessors.size()));
657 }
658 document->accessors.append(accessor);
659 }
660
661 // images
662 const QJsonArray images = root.value(QLatin1String("images")).toArray();
663 for (const auto &value : images) {
664 const QJsonObject object = value.toObject();
665 Image image;
666 image.uri = object.value(QLatin1String("uri")).toString();
667 image.bufferView = object.value(QLatin1String("bufferView")).toInt(-1);
668 image.mimeType = object.value(QLatin1String("mimeType")).toString();
669 image.name = object.value(QLatin1String("name")).toString();
670 if (image.bufferView >= document->bufferViews.size())
671 return setError(QStringLiteral("Image %1 references invalid buffer view %2")
672 .arg(document->images.size()).arg(image.bufferView));
673 document->images.append(image);
674 }
675
676 // samplers
677 const QJsonArray samplers = root.value(QLatin1String("samplers")).toArray();
678 for (const auto &value : samplers) {
679 const QJsonObject object = value.toObject();
680 Sampler sampler;
681 sampler.magFilter = object.value(QLatin1String("magFilter")).toInt(0);
682 sampler.minFilter = object.value(QLatin1String("minFilter")).toInt(0);
683 sampler.wrapS = object.value(QLatin1String("wrapS")).toInt(Sampler::Repeat);
684 sampler.wrapT = object.value(QLatin1String("wrapT")).toInt(Sampler::Repeat);
685 sampler.name = object.value(QLatin1String("name")).toString();
686 document->samplers.append(sampler);
687 }
688
689 // textures
690 const QJsonArray textures = root.value(QLatin1String("textures")).toArray();
691 for (const auto &value : textures) {
692 const QJsonObject object = value.toObject();
693 Texture texture;
694 texture.sampler = object.value(QLatin1String("sampler")).toInt(-1);
695 texture.source = object.value(QLatin1String("source")).toInt(-1);
696 texture.name = object.value(QLatin1String("name")).toString();
697 texture.extensions = object.value(QLatin1String("extensions")).toObject();
698
699 const QJsonValue webpExt = texture.extensions.value(QLatin1String("EXT_texture_webp"));
700 if (webpExt.isObject())
701 texture.webpSource = webpExt.toObject().value(QLatin1String("source")).toInt(-1);
702
703 if (texture.sampler >= document->samplers.size())
704 return setError(QStringLiteral("Texture %1 references invalid sampler %2")
705 .arg(document->textures.size()).arg(texture.sampler));
706 if (texture.source >= document->images.size() || texture.webpSource >= document->images.size())
707 return setError(QStringLiteral("Texture %1 references invalid image %2")
708 .arg(document->textures.size())
709 .arg(qMax(texture.source, texture.webpSource)));
710 document->textures.append(texture);
711 }
712
713 // materials
714 //
715 // Every texture reference goes through parseTexture() rather than
716 // parseTextureInfo() directly, so that the index is range checked in one
717 // place and a newly supported extension cannot forget to do it. The
718 // textures array is already parsed at this point.
719 int badTextureIndex = -1;
720 const auto parseTexture = [&](const QJsonObject &object,
721 const char *scaleOrStrengthKey = nullptr) {
722 const TextureInfo info = parseTextureInfo(object, scaleOrStrengthKey);
723 if (info.index >= document->textures.size())
724 badTextureIndex = info.index;
725 return info;
726 };
727
728 const QJsonArray materials = root.value(QLatin1String("materials")).toArray();
729 for (const auto &value : materials) {
730 const QJsonObject object = value.toObject();
731 Material material;
732 material.name = object.value(QLatin1String("name")).toString();
733
734 const QJsonValue pbrValue = object.value(QLatin1String("pbrMetallicRoughness"));
735 if (pbrValue.isObject()) {
736 const QJsonObject pbr = pbrValue.toObject();
737 material.baseColorFactor =
738 toVector4D(pbr.value(QLatin1String("baseColorFactor")).toArray(), material.baseColorFactor);
739 material.baseColorTexture = parseTexture(pbr.value(QLatin1String("baseColorTexture")).toObject());
740 material.metallicFactor = float(pbr.value(QLatin1String("metallicFactor")).toDouble(1.0));
741 material.roughnessFactor = float(pbr.value(QLatin1String("roughnessFactor")).toDouble(1.0));
742 material.metallicRoughnessTexture =
743 parseTexture(pbr.value(QLatin1String("metallicRoughnessTexture")).toObject());
744 }
745
746 material.normalTexture = parseTexture(object.value(QLatin1String("normalTexture")).toObject(), "scale");
747 material.occlusionTexture =
748 parseTexture(object.value(QLatin1String("occlusionTexture")).toObject(), "strength");
749 material.emissiveTexture = parseTexture(object.value(QLatin1String("emissiveTexture")).toObject());
750 material.emissiveFactor =
751 toVector3D(object.value(QLatin1String("emissiveFactor")).toArray(), material.emissiveFactor);
752
753 const QString alphaMode = object.value(QLatin1String("alphaMode")).toString();
754 if (alphaMode == QLatin1String("MASK"))
755 material.alphaMode = Material::AlphaMode::Mask;
756 else if (alphaMode == QLatin1String("BLEND"))
757 material.alphaMode = Material::AlphaMode::Blend;
758 material.alphaCutoff = float(object.value(QLatin1String("alphaCutoff")).toDouble(0.5));
759 material.doubleSided = object.value(QLatin1String("doubleSided")).toBool(false);
760
761 material.extensions = object.value(QLatin1String("extensions")).toObject();
762 const QJsonObject &ext = material.extensions;
763
764 material.unlit = ext.contains(QLatin1String("KHR_materials_unlit"));
765
766 const QJsonValue sgValue = ext.value(QLatin1String("KHR_materials_pbrSpecularGlossiness"));
767 if (sgValue.isObject()) {
768 const QJsonObject sg = sgValue.toObject();
769 Material::SpecularGlossiness specularGlossiness;
770 specularGlossiness.diffuseFactor = toVector4D(sg.value(QLatin1String("diffuseFactor")).toArray(),
771 specularGlossiness.diffuseFactor);
772 specularGlossiness.diffuseTexture = parseTexture(sg.value(QLatin1String("diffuseTexture")).toObject());
773 specularGlossiness.specularFactor = toVector3D(sg.value(QLatin1String("specularFactor")).toArray(),
774 specularGlossiness.specularFactor);
775 specularGlossiness.glossinessFactor = float(sg.value(QLatin1String("glossinessFactor")).toDouble(1.0));
776 specularGlossiness.specularGlossinessTexture = parseTexture(
777 sg.value(QLatin1String("specularGlossinessTexture")).toObject());
778 material.specularGlossiness = specularGlossiness;
779 }
780
781 const QJsonValue ccValue = ext.value(QLatin1String("KHR_materials_clearcoat"));
782 if (ccValue.isObject()) {
783 const QJsonObject cc = ccValue.toObject();
784 Material::Clearcoat clearcoat;
785 clearcoat.clearcoatFactor = float(cc.value(QLatin1String("clearcoatFactor")).toDouble(0.0));
786 clearcoat.clearcoatTexture = parseTexture(cc.value(QLatin1String("clearcoatTexture")).toObject());
787 clearcoat.clearcoatRoughnessFactor =
788 float(cc.value(QLatin1String("clearcoatRoughnessFactor")).toDouble(0.0));
789 clearcoat.clearcoatRoughnessTexture =
790 parseTexture(cc.value(QLatin1String("clearcoatRoughnessTexture")).toObject());
791 clearcoat.clearcoatNormalTexture =
792 parseTexture(cc.value(QLatin1String("clearcoatNormalTexture")).toObject(), "scale");
793 material.clearcoat = clearcoat;
794 }
795
796 const QJsonValue trValue = ext.value(QLatin1String("KHR_materials_transmission"));
797 if (trValue.isObject()) {
798 const QJsonObject tr = trValue.toObject();
799 Material::Transmission transmission;
800 transmission.transmissionFactor = float(tr.value(QLatin1String("transmissionFactor")).toDouble(0.0));
801 transmission.transmissionTexture =
802 parseTexture(tr.value(QLatin1String("transmissionTexture")).toObject());
803 material.transmission = transmission;
804 }
805
806 const QJsonValue volValue = ext.value(QLatin1String("KHR_materials_volume"));
807 if (volValue.isObject()) {
808 const QJsonObject vol = volValue.toObject();
809 Material::Volume volume;
810 volume.thicknessFactor = float(vol.value(QLatin1String("thicknessFactor")).toDouble(0.0));
811 volume.thicknessTexture = parseTexture(vol.value(QLatin1String("thicknessTexture")).toObject());
812 volume.attenuationDistance = float(vol.value(QLatin1String("attenuationDistance")).toDouble(0.0));
813 volume.attenuationColor =
814 toVector3D(vol.value(QLatin1String("attenuationColor")).toArray(), volume.attenuationColor);
815 material.volume = volume;
816 }
817
818 const QJsonValue iorValue = ext.value(QLatin1String("KHR_materials_ior"));
819 if (iorValue.isObject())
820 material.ior = float(iorValue.toObject().value(QLatin1String("ior")).toDouble(1.5));
821
822 const QJsonValue esValue = ext.value(QLatin1String("KHR_materials_emissive_strength"));
823 if (esValue.isObject())
824 material.emissiveStrength =
825 float(esValue.toObject().value(QLatin1String("emissiveStrength")).toDouble(1.0));
826
827 const QJsonValue spValue = ext.value(QLatin1String("KHR_materials_specular"));
828 if (spValue.isObject()) {
829 const QJsonObject sp = spValue.toObject();
830 Material::Specular specular;
831 specular.specularFactor = float(sp.value(QLatin1String("specularFactor")).toDouble(1.0));
832 specular.specularTexture = parseTexture(sp.value(QLatin1String("specularTexture")).toObject());
833 specular.specularColorFactor = toVector3D(sp.value(QLatin1String("specularColorFactor")).toArray(),
834 specular.specularColorFactor);
835 specular.specularColorTexture =
836 parseTexture(sp.value(QLatin1String("specularColorTexture")).toObject());
837 material.specular = specular;
838 }
839
840 const QJsonValue shValue = ext.value(QLatin1String("KHR_materials_sheen"));
841 if (shValue.isObject()) {
842 const QJsonObject sh = shValue.toObject();
843 Material::Sheen sheen;
844 sheen.sheenColorFactor = toVector3D(sh.value(QLatin1String("sheenColorFactor")).toArray(), sheen.sheenColorFactor);
845 sheen.sheenColorTexture = parseTexture(sh.value(QLatin1String("sheenColorTexture")).toObject());
846 sheen.sheenRoughnessFactor = float(sh.value(QLatin1String("sheenRoughnessFactor")).toDouble(0.0));
847 sheen.sheenRoughnessTexture = parseTexture(sh.value(QLatin1String("sheenRoughnessTexture")).toObject());
848 material.sheen = sheen;
849 }
850
851 const QJsonValue anValue = ext.value(QLatin1String("KHR_materials_anisotropy"));
852 if (anValue.isObject()) {
853 const QJsonObject an = anValue.toObject();
854 Material::Anisotropy anisotropy;
855 anisotropy.anisotropyStrength = float(an.value(QLatin1String("anisotropyStrength")).toDouble(0.0));
856 anisotropy.anisotropyRotation = float(an.value(QLatin1String("anisotropyRotation")).toDouble(0.0));
857 anisotropy.anisotropyTexture = parseTexture(an.value(QLatin1String("anisotropyTexture")).toObject());
858 material.anisotropy = anisotropy;
859 }
860
861 const QJsonValue irValue = ext.value(QLatin1String("KHR_materials_iridescence"));
862 if (irValue.isObject()) {
863 const QJsonObject ir = irValue.toObject();
864 Material::Iridescence iridescence;
865 iridescence.iridescenceFactor = float(ir.value(QLatin1String("iridescenceFactor")).toDouble(0.0));
866 iridescence.iridescenceTexture = parseTexture(ir.value(QLatin1String("iridescenceTexture")).toObject());
867 iridescence.iridescenceIor = float(ir.value(QLatin1String("iridescenceIor")).toDouble(1.3));
868 iridescence.iridescenceThicknessMinimum = float(ir.value(QLatin1String("iridescenceThicknessMinimum")).toDouble(100.0));
869 iridescence.iridescenceThicknessMaximum = float(ir.value(QLatin1String("iridescenceThicknessMaximum")).toDouble(400.0));
870 iridescence.iridescenceThicknessTexture = parseTexture(
871 ir.value(QLatin1String("iridescenceThicknessTexture")).toObject());
872 material.iridescence = iridescence;
873 }
874
875 const QJsonValue dispValue = ext.value(QLatin1String("KHR_materials_dispersion"));
876 if (dispValue.isObject())
877 material.dispersion = float(dispValue.toObject().value(QLatin1String("dispersion")).toDouble(0.0));
878
879 document->materials.append(material);
880 }
881
882 if (badTextureIndex >= 0)
883 return setError(QStringLiteral("Material references invalid texture %1").arg(badTextureIndex));
884
885 // meshes
886 const QJsonArray meshes = root.value(QLatin1String("meshes")).toArray();
887 for (const auto &value : meshes) {
888 const QJsonObject object = value.toObject();
889 Mesh mesh;
890 mesh.name = object.value(QLatin1String("name")).toString();
891 const QJsonArray weights = object.value(QLatin1String("weights")).toArray();
892 for (const auto &w : weights)
893 mesh.weights.append(float(w.toDouble()));
894
895 const QJsonArray primitives = object.value(QLatin1String("primitives")).toArray();
896 for (const auto &primitiveValue : primitives) {
897 const QJsonObject primitiveObject = primitiveValue.toObject();
898 MeshPrimitive primitive;
899 const QJsonObject attributes = primitiveObject.value(QLatin1String("attributes")).toObject();
900 for (auto it = attributes.constBegin(); it != attributes.constEnd(); ++it)
901 primitive.attributes.insert(it.key().toUtf8(), it.value().toInt(-1));
902 primitive.indices = primitiveObject.value(QLatin1String("indices")).toInt(-1);
903 primitive.material = primitiveObject.value(QLatin1String("material")).toInt(-1);
904 primitive.mode = primitiveObject.value(QLatin1String("mode")).toInt(MeshPrimitive::Triangles);
905 const QJsonArray targets = primitiveObject.value(QLatin1String("targets")).toArray();
906 for (const auto &targetValue : targets) {
907 const QJsonObject targetObject = targetValue.toObject();
908 QHash<QByteArray, int> target;
909 for (auto it = targetObject.constBegin(); it != targetObject.constEnd(); ++it)
910 target.insert(it.key().toUtf8(), it.value().toInt(-1));
911 primitive.targets.append(target);
912 }
913 primitive.extensions = primitiveObject.value(QLatin1String("extensions")).toObject();
914
915 const QJsonValue variantsExt = primitive.extensions.value(QLatin1String("KHR_materials_variants"));
916 if (variantsExt.isObject()) {
917 const QJsonArray mappings = variantsExt.toObject().value(QLatin1String("mappings")).toArray();
918 for (const auto &mappingValue : mappings) {
919 const QJsonObject mappingObject = mappingValue.toObject();
920 MeshPrimitive::VariantMapping mapping;
921 mapping.material = mappingObject.value(QLatin1String("material")).toInt(-1);
922 if (mapping.material < 0 || mapping.material >= document->materials.size()) {
923 return setError(
924 QStringLiteral("Mesh %1 primitive variant mapping references invalid material %2")
925 .arg(document->meshes.size())
926 .arg(mapping.material));
927 }
928 const QJsonArray mappingVariants = mappingObject.value(QLatin1String("variants")).toArray();
929 for (const auto &variantValue : mappingVariants) {
930 const int variant = variantValue.toInt(-1);
931 if (variant < 0 || variant >= document->materialVariants.size()) {
932 return setError(
933 QStringLiteral("Mesh %1 primitive variant mapping references invalid variant %2")
934 .arg(document->meshes.size())
935 .arg(variant));
936 }
937 mapping.variants.append(variant);
938 }
939 primitive.variantMappings.append(mapping);
940 }
941 }
942
943 // Structural validation of accessor references. As everywhere in
944 // this parser, any negative index means "unset": the glTF defaults
945 // are -1, the readers return nothing for a negative index, and so
946 // only the upper bound needs checking here.
947 const auto checkAccessor = [&](int accessor, const char *what) {
948 if (accessor >= document->accessors.size())
949 return setError(QStringLiteral("Mesh %1 primitive references invalid %2 accessor %3")
950 .arg(document->meshes.size()).arg(QLatin1String(what)).arg(accessor));
951 return true;
952 };
953 for (auto it = primitive.attributes.constBegin(); it != primitive.attributes.constEnd(); ++it) {
954 if (!checkAccessor(it.value(), it.key().constData()))
955 return false;
956 }
957 if (primitive.indices >= 0 && !checkAccessor(primitive.indices, "index"))
958 return false;
959 if (primitive.material >= document->materials.size())
960 return setError(QStringLiteral("Mesh %1 primitive references invalid material %2")
961 .arg(document->meshes.size())
962 .arg(primitive.material));
963
964 mesh.primitives.append(primitive);
965 }
966 if (mesh.primitives.isEmpty())
967 qCWarning(lcQuick3DGltf) << "Mesh" << document->meshes.size() << "has no primitives";
968 document->meshes.append(mesh);
969 }
970
971 // cameras
972 const QJsonArray cameras = root.value(QLatin1String("cameras")).toArray();
973 for (const auto &value : cameras) {
974 const QJsonObject object = value.toObject();
975 Camera camera;
976 camera.name = object.value(QLatin1String("name")).toString();
977 const QString type = object.value(QLatin1String("type")).toString();
978 if (type == QLatin1String("orthographic")) {
979 camera.type = Camera::Type::Orthographic;
980 const QJsonObject ortho = object.value(QLatin1String("orthographic")).toObject();
981 camera.xmag = float(ortho.value(QLatin1String("xmag")).toDouble());
982 camera.ymag = float(ortho.value(QLatin1String("ymag")).toDouble());
983 camera.znear = float(ortho.value(QLatin1String("znear")).toDouble());
984 camera.zfar = float(ortho.value(QLatin1String("zfar")).toDouble());
985 } else {
986 camera.type = Camera::Type::Perspective;
987 const QJsonObject perspective = object.value(QLatin1String("perspective")).toObject();
988 camera.aspectRatio = float(perspective.value(QLatin1String("aspectRatio")).toDouble(0.0));
989 camera.yfov = float(perspective.value(QLatin1String("yfov")).toDouble());
990 camera.znear = float(perspective.value(QLatin1String("znear")).toDouble());
991 camera.zfar = float(perspective.value(QLatin1String("zfar")).toDouble(0.0));
992 }
993 document->cameras.append(camera);
994 }
995
996 // KHR_lights_punctual (document level)
997 {
998 const QJsonValue lightsExt = document->rootExtensions.value(QLatin1String("KHR_lights_punctual"));
999 if (lightsExt.isObject()) {
1000 const QJsonArray lights = lightsExt.toObject().value(QLatin1String("lights")).toArray();
1001 for (const auto &value : lights) {
1002 const QJsonObject object = value.toObject();
1003 Light light;
1004 light.name = object.value(QLatin1String("name")).toString();
1005 const QString type = object.value(QLatin1String("type")).toString();
1006 if (type == QLatin1String("point"))
1007 light.type = Light::Type::Point;
1008 else if (type == QLatin1String("spot"))
1009 light.type = Light::Type::Spot;
1010 else
1011 light.type = Light::Type::Directional;
1012 light.color = toVector3D(object.value(QLatin1String("color")).toArray(), light.color);
1013 light.intensity = float(object.value(QLatin1String("intensity")).toDouble(1.0));
1014 light.range = float(object.value(QLatin1String("range")).toDouble(0.0));
1015 if (light.type == Light::Type::Spot) {
1016 const QJsonObject spot = object.value(QLatin1String("spot")).toObject();
1017 light.innerConeAngle = float(spot.value(QLatin1String("innerConeAngle")).toDouble(0.0));
1018 light.outerConeAngle = float(spot.value(QLatin1String("outerConeAngle")).toDouble(M_PI_4));
1019 }
1020 document->lights.append(light);
1021 }
1022 }
1023 }
1024
1025 // nodes
1026 const QJsonArray nodes = root.value(QLatin1String("nodes")).toArray();
1027 for (const auto &value : nodes) {
1028 const QJsonObject object = value.toObject();
1029 Node node;
1030 node.name = object.value(QLatin1String("name")).toString();
1031 const QJsonArray children = object.value(QLatin1String("children")).toArray();
1032 for (const auto &child : children)
1033 node.children.append(child.toInt(-1));
1034 node.mesh = object.value(QLatin1String("mesh")).toInt(-1);
1035 node.skin = object.value(QLatin1String("skin")).toInt(-1);
1036 node.camera = object.value(QLatin1String("camera")).toInt(-1);
1037
1038 const QJsonValue matrixValue = object.value(QLatin1String("matrix"));
1039 if (matrixValue.isArray()) {
1040 const QJsonArray m = matrixValue.toArray();
1041 if (m.size() == 16) {
1042 float values[16];
1043 for (int i = 0; i < 16; ++i)
1044 values[i] = float(m.at(i).toDouble());
1045 // glTF matrices are column-major; QMatrix4x4(float*) is row-major
1046 node.matrix = QMatrix4x4(values).transposed();
1047 node.hasMatrix = true;
1048 }
1049 }
1050 node.translation = toVector3D(object.value(QLatin1String("translation")).toArray(), node.translation);
1051 const QJsonArray rotation = object.value(QLatin1String("rotation")).toArray();
1052 if (rotation.size() == 4) {
1053 // glTF quaternions are (x, y, z, w)
1054 node.rotation = QQuaternion(float(rotation.at(3).toDouble()), float(rotation.at(0).toDouble()),
1055 float(rotation.at(1).toDouble()), float(rotation.at(2).toDouble()));
1056 }
1057 node.scale = toVector3D(object.value(QLatin1String("scale")).toArray(), node.scale);
1058 const QJsonArray weights = object.value(QLatin1String("weights")).toArray();
1059 for (const auto &w : weights)
1060 node.weights.append(float(w.toDouble()));
1061 node.extensions = object.value(QLatin1String("extensions")).toObject();
1062
1063 const QJsonValue lightExt = node.extensions.value(QLatin1String("KHR_lights_punctual"));
1064 if (lightExt.isObject())
1065 node.light = lightExt.toObject().value(QLatin1String("light")).toInt(-1);
1066
1067 const QJsonValue visibilityExt = node.extensions.value(QLatin1String("KHR_node_visibility"));
1068 if (visibilityExt.isObject())
1069 node.visible = visibilityExt.toObject().value(QLatin1String("visible")).toBool(true);
1070
1071 const QJsonValue instancingExt = node.extensions.value(QLatin1String("EXT_mesh_gpu_instancing"));
1072 if (instancingExt.isObject()) {
1073 const QJsonObject instanceAttributes =
1074 instancingExt.toObject().value(QLatin1String("attributes")).toObject();
1075 for (auto it = instanceAttributes.constBegin(); it != instanceAttributes.constEnd(); ++it) {
1076 const int accessor = it.value().toInt(-1);
1077 if (accessor < 0 || accessor >= document->accessors.size()) {
1078 return setError(QStringLiteral("Node %1 instance attribute %2 references invalid accessor %3")
1079 .arg(document->nodes.size()).arg(it.key()).arg(accessor));
1080 }
1081 if (it.key() == QLatin1String("TRANSLATION"))
1082 node.instanceTranslation = accessor;
1083 else if (it.key() == QLatin1String("ROTATION"))
1084 node.instanceRotation = accessor;
1085 else if (it.key() == QLatin1String("SCALE"))
1086 node.instanceScale = accessor;
1087 else
1088 qCWarning(lcQuick3DGltf) << "Ignoring unsupported instance attribute" << it.key();
1089 }
1090 node.hasInstancing = node.instanceTranslation >= 0 || node.instanceRotation >= 0
1091 || node.instanceScale >= 0;
1092 }
1093
1094 if (node.mesh >= document->meshes.size())
1095 return setError(QStringLiteral("Node %1 references invalid mesh %2")
1096 .arg(document->nodes.size()).arg(node.mesh));
1097 if (node.camera >= document->cameras.size())
1098 return setError(QStringLiteral("Node %1 references invalid camera %2")
1099 .arg(document->nodes.size()).arg(node.camera));
1100 if (node.light >= document->lights.size())
1101 return setError(QStringLiteral("Node %1 references invalid light %2")
1102 .arg(document->nodes.size()).arg(node.light));
1103 document->nodes.append(node);
1104 }
1105
1106 // Validate node graph references (children and skins can be forward
1107 // references). The specification requires the hierarchy to be a forest,
1108 // and consumers that recurse over the graph rely on that to be safe from
1109 // cycles and exponential blowup, so the shape is repaired here rather than
1110 // left for them to cope with.
1111 const int nodeCount = document->nodes.size();
1112 QVarLengthArray<int, 64> parentCount(nodeCount);
1113 std::fill(parentCount.begin(), parentCount.end(), 0);
1114 for (int i = 0; i < nodeCount; ++i) {
1115 Node &node = document->nodes[i];
1116 for (qsizetype c = 0; c < node.children.size(); ++c) {
1117 const int child = node.children.at(c);
1118 if (child < 0 || child >= nodeCount)
1119 return setError(QStringLiteral("Node %1 references invalid child node %2").arg(i).arg(child));
1120 if (++parentCount[child] > 1) {
1121 // A second parent cannot be represented at all: a node
1122 // occupies exactly one place in a scene graph. Keep the first
1123 // edge and drop this one, so the rest of the asset still
1124 // loads.
1125 qCWarning(lcQuick3DGltf) << "Node" << child
1126 << "has more than one parent; ignoring the edge from node" << i;
1127 --parentCount[child];
1128 node.children.removeAt(c--);
1129 }
1130 }
1131 }
1132 // With single parents the only remaining hazards are parentless cycles
1133 // and excessive depth; both are caught by walking up each node's parent
1134 // chain, memoizing the depths so every edge is visited only once.
1135 {
1136 constexpr int maxNodeDepth = 4096;
1137 QVarLengthArray<int, 64> parent(nodeCount);
1138 std::fill(parent.begin(), parent.end(), -1);
1139 for (int i = 0; i < nodeCount; ++i) {
1140 for (int child : document->nodes.at(i).children)
1141 parent[child] = i;
1142 }
1143 QVarLengthArray<int, 64> depth(nodeCount);
1144 std::fill(depth.begin(), depth.end(), -1);
1145 QVarLengthArray<int, 64> chain;
1146 for (int i = 0; i < nodeCount; ++i) {
1147 chain.clear();
1148 int n = i;
1149 while (n >= 0 && depth[n] < 0 && chain.size() <= nodeCount) {
1150 chain.append(n);
1151 n = parent[n];
1152 }
1153 if (chain.size() > nodeCount)
1154 return setError(QStringLiteral("Node hierarchy contains a cycle"));
1155 int d = n >= 0 ? depth[n] : 0;
1156 for (auto it = chain.rbegin(); it != chain.rend(); ++it)
1157 depth[*it] = ++d;
1158 if (d > maxNodeDepth)
1159 return setError(QStringLiteral("Node hierarchy is deeper than %1").arg(maxNodeDepth));
1160 }
1161 }
1162
1163 // skins
1164 const QJsonArray skins = root.value(QLatin1String("skins")).toArray();
1165 for (const auto &value : skins) {
1166 const QJsonObject object = value.toObject();
1167 Skin skin;
1168 skin.name = object.value(QLatin1String("name")).toString();
1169 skin.inverseBindMatrices = object.value(QLatin1String("inverseBindMatrices")).toInt(-1);
1170 skin.skeleton = object.value(QLatin1String("skeleton")).toInt(-1);
1171 const QJsonArray joints = object.value(QLatin1String("joints")).toArray();
1172 for (const auto &joint : joints)
1173 skin.joints.append(joint.toInt(-1));
1174
1175 if (skin.inverseBindMatrices >= document->accessors.size())
1176 return setError(QStringLiteral("Skin %1 references invalid accessor %2")
1177 .arg(document->skins.size()).arg(skin.inverseBindMatrices));
1178 for (int joint : std::as_const(skin.joints)) {
1179 if (joint < 0 || joint >= nodeCount)
1180 return setError(QStringLiteral("Skin %1 references invalid joint node %2")
1181 .arg(document->skins.size()).arg(joint));
1182 }
1183 document->skins.append(skin);
1184 }
1185 for (int i = 0; i < nodeCount; ++i) {
1186 if (document->nodes.at(i).skin >= document->skins.size())
1187 return setError(QStringLiteral("Node %1 references invalid skin %2")
1188 .arg(i).arg(document->nodes.at(i).skin));
1189 }
1190
1191 // animations
1192 const QJsonArray animations = root.value(QLatin1String("animations")).toArray();
1193 for (const auto &value : animations) {
1194 const QJsonObject object = value.toObject();
1195 Animation animation;
1196 animation.name = object.value(QLatin1String("name")).toString();
1197
1198 const QJsonArray animationSamplers = object.value(QLatin1String("samplers")).toArray();
1199 for (const auto &samplerValue : animationSamplers) {
1200 const QJsonObject samplerObject = samplerValue.toObject();
1201 AnimationSampler sampler;
1202 sampler.input = samplerObject.value(QLatin1String("input")).toInt(-1);
1203 sampler.output = samplerObject.value(QLatin1String("output")).toInt(-1);
1204 const QString interpolation = samplerObject.value(QLatin1String("interpolation")).toString();
1205 if (interpolation == QLatin1String("STEP"))
1206 sampler.interpolation = AnimationSampler::Interpolation::Step;
1207 else if (interpolation == QLatin1String("CUBICSPLINE"))
1208 sampler.interpolation = AnimationSampler::Interpolation::CubicSpline;
1209 if (sampler.input < 0 || sampler.input >= document->accessors.size()
1210 || sampler.output < 0 || sampler.output >= document->accessors.size()) {
1211 return setError(
1212 QStringLiteral("Animation %1 sampler references an invalid accessor")
1213 .arg(document->animations.size()));
1214 }
1215 animation.samplers.append(sampler);
1216 }
1217
1218 const QJsonArray channels = object.value(QLatin1String("channels")).toArray();
1219 for (const auto &channelValue : channels) {
1220 const QJsonObject channelObject = channelValue.toObject();
1221 AnimationChannel channel;
1222 channel.sampler = channelObject.value(QLatin1String("sampler")).toInt(-1);
1223 const QJsonObject target = channelObject.value(QLatin1String("target")).toObject();
1224 channel.targetNode = target.value(QLatin1String("node")).toInt(-1);
1225 const QString path = target.value(QLatin1String("path")).toString();
1226 if (path == QLatin1String("rotation"))
1227 channel.path = AnimationChannel::Path::Rotation;
1228 else if (path == QLatin1String("scale"))
1229 channel.path = AnimationChannel::Path::Scale;
1230 else if (path == QLatin1String("weights"))
1231 channel.path = AnimationChannel::Path::Weights;
1232 else if (path != QLatin1String("translation")) {
1233 qCWarning(lcQuick3DGltf) << "Ignoring animation channel with unsupported path" << path;
1234 continue;
1235 }
1236 if (channel.sampler < 0 || channel.sampler >= animation.samplers.size())
1237 return setError(QStringLiteral("Animation %1 channel references invalid sampler %2")
1238 .arg(document->animations.size())
1239 .arg(channel.sampler));
1240 if (channel.targetNode >= nodeCount)
1241 return setError(QStringLiteral("Animation %1 channel references invalid node %2")
1242 .arg(document->animations.size())
1243 .arg(channel.targetNode));
1244 animation.channels.append(channel);
1245 }
1246
1247 document->animations.append(animation);
1248 }
1249
1250 // scenes
1251 const QJsonArray scenes = root.value(QLatin1String("scenes")).toArray();
1252 for (const auto &value : scenes) {
1253 const QJsonObject object = value.toObject();
1254 Scene scene;
1255 scene.name = object.value(QLatin1String("name")).toString();
1256 QSet<int> seenRoots;
1257 const QJsonArray sceneNodes = object.value(QLatin1String("nodes")).toArray();
1258 for (const auto &nodeIndex : sceneNodes) {
1259 const int index = nodeIndex.toInt(-1);
1260 if (index < 0 || index >= nodeCount)
1261 return setError(QStringLiteral("Scene %1 references invalid node %2")
1262 .arg(document->scenes.size()).arg(index));
1263 // Scene nodes must be distinct root nodes; a repeated node, or one
1264 // that is also somebody's child, would have its subtree emitted
1265 // more than once. Skip the offending entry rather than failing, so
1266 // that the scene still contains everything it should, once.
1267 if (parentCount[index] != 0 || seenRoots.contains(index)) {
1268 qCWarning(lcQuick3DGltf) << "Scene" << document->scenes.size() << "references node" << index
1269 << "which is not a unique root node; ignoring it";
1270 continue;
1271 }
1272 seenRoots.insert(index);
1273 scene.nodes.append(index);
1274 }
1275 document->scenes.append(scene);
1276 }
1277 document->scene = root.value(QLatin1String("scene")).toInt(-1);
1278 if (document->scene >= document->scenes.size())
1279 return setError(QStringLiteral("Document references invalid default scene %1").arg(document->scene));
1280
1281 return true;
1282}
1283
1284QT_END_NAMESPACE
Combined button and popup list for selecting options.