Revision control

Copy as Markdown

// Copyright (C) 2026 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
// Qt-Security score:critical reason:data-parser
#include "qssggltfparser_p.h"
#include "qssggltfresourceresolver_p.h"
#include <QtCore/qendian.h>
#include <QtCore/qfile.h>
#include <QtCore/qfileinfo.h>
#include <QtCore/qhash.h>
#include <QtCore/qjsonarray.h>
#include <QtCore/qjsondocument.h>
#include <QtCore/qset.h>
#include <QtCore/qvarlengtharray.h>
#include "meshoptimizer.h"
QT_BEGIN_NAMESPACE
Q_LOGGING_CATEGORY(lcQuick3DGltf, "qt.quick3d.gltf")
using namespace QSSGGltf;
namespace {
// GLB container constants
constexpr quint32 GLB_MAGIC = 0x46546C67; // 'glTF'
constexpr quint32 GLB_CHUNK_JSON = 0x4E4F534A; // 'JSON'
constexpr quint32 GLB_CHUNK_BIN = 0x004E4942; // 'BIN\0'
QVector3D toVector3D(const QJsonArray &array, const QVector3D &defaultValue = {})
{
if (array.size() < 3)
return defaultValue;
return QVector3D(float(array.at(0).toDouble()), float(array.at(1).toDouble()), float(array.at(2).toDouble()));
}
QVector4D toVector4D(const QJsonArray &array, const QVector4D &defaultValue = {})
{
if (array.size() < 4)
return defaultValue;
return QVector4D(float(array.at(0).toDouble()), float(array.at(1).toDouble()),
float(array.at(2).toDouble()), float(array.at(3).toDouble()));
}
TextureInfo parseTextureInfo(const QJsonObject &object, const char *scaleOrStrengthKey = nullptr)
{
TextureInfo info;
if (object.isEmpty())
return info;
info.index = object.value(QLatin1String("index")).toInt(-1);
info.texCoord = object.value(QLatin1String("texCoord")).toInt(0);
if (scaleOrStrengthKey)
info.scaleOrStrength = float(object.value(QLatin1String(scaleOrStrengthKey)).toDouble(1.0));
const QJsonObject extensions = object.value(QLatin1String("extensions")).toObject();
const QJsonValue transformValue = extensions.value(QLatin1String("KHR_texture_transform"));
if (transformValue.isObject()) {
const QJsonObject t = transformValue.toObject();
TextureTransform transform;
const QJsonArray offset = t.value(QLatin1String("offset")).toArray();
if (offset.size() >= 2)
transform.offset = QVector2D(float(offset.at(0).toDouble()), float(offset.at(1).toDouble()));
const QJsonArray scale = t.value(QLatin1String("scale")).toArray();
if (scale.size() >= 2)
transform.scale = QVector2D(float(scale.at(0).toDouble(1.0)), float(scale.at(1).toDouble(1.0)));
transform.rotation = float(t.value(QLatin1String("rotation")).toDouble(0.0));
transform.texCoord = t.value(QLatin1String("texCoord")).toInt(-1);
info.transform = transform;
}
return info;
}
Accessor::Type accessorTypeFromString(const QString &type, bool *ok)
{
*ok = true;
if (type == QLatin1String("SCALAR"))
return Accessor::Type::Scalar;
if (type == QLatin1String("VEC2"))
return Accessor::Type::Vec2;
if (type == QLatin1String("VEC3"))
return Accessor::Type::Vec3;
if (type == QLatin1String("VEC4"))
return Accessor::Type::Vec4;
if (type == QLatin1String("MAT2"))
return Accessor::Type::Mat2;
if (type == QLatin1String("MAT3"))
return Accessor::Type::Mat3;
if (type == QLatin1String("MAT4"))
return Accessor::Type::Mat4;
*ok = false;
return Accessor::Type::Scalar;
}
bool isValidComponentType(int value)
{
switch (Accessor::ComponentType(value)) {
case Accessor::ComponentType::Byte:
case Accessor::ComponentType::UnsignedByte:
case Accessor::ComponentType::Short:
case Accessor::ComponentType::UnsignedShort:
case Accessor::ComponentType::UnsignedInt:
case Accessor::ComponentType::Float:
return true;
}
return false;
}
// Views can alias the same compressed bytes, so the budget is per document
struct MeshoptDecodeState
{
qint64 remainingBytes = qint64(1024) * 1024 * 1024;
QHash<QString, int> decodedBuffers; // decode parameters -> synthetic buffer index
};
// Returns -1 for anything but a non-negative integer below 2^53
qint64 toByteCount(const QJsonValue &value, double defaultValue)
{
const double d = value.toDouble(defaultValue);
return d >= 0.0 && d <= 9007199254740992.0 ? qint64(d) : -1;
}
// Decodes into a synthetic buffer and points the view at it
bool decodeMeshoptBufferView(const QJsonObject &ext, QSSGGltfDocument &document, BufferView &view,
MeshoptDecodeState &state, QString *errorMessage)
{
const auto fail = [errorMessage](const QString &message) {
*errorMessage = message;
return false;
};
const int sourceBuffer = ext.value(QLatin1String("buffer")).toInt(-1);
const qint64 byteOffset = toByteCount(ext.value(QLatin1String("byteOffset")), 0.0);
const qint64 byteLength = toByteCount(ext.value(QLatin1String("byteLength")), -1.0);
const qint64 byteStride = toByteCount(ext.value(QLatin1String("byteStride")), -1.0);
// Checked as double, since the qint64 product can wrap
constexpr qint64 maxDecodedBytes = qint64(1024) * 1024 * 1024;
const double rawCount = ext.value(QLatin1String("count")).toDouble();
if (!(byteStride > 0 && rawCount > 0.0 && rawCount * double(byteStride) <= double(maxDecodedBytes)))
return fail(QStringLiteral("unreasonable decoded data size"));
const qint64 count = qint64(rawCount);
const QString mode = ext.value(QLatin1String("mode")).toString();
const QString filter = ext.value(QLatin1String("filter")).toString(QStringLiteral("NONE"));
if (sourceBuffer < 0 || sourceBuffer >= document.buffers.size()
|| document.buffers.at(sourceBuffer).meshoptFallback) {
return fail(QStringLiteral("compressed data references invalid buffer %1").arg(sourceBuffer));
}
const Buffer &compressed = document.buffers.at(sourceBuffer);
// Not summed, since offset + length can wrap
if (byteOffset < 0 || byteLength <= 0 || byteLength > compressed.byteLength
|| byteOffset > compressed.byteLength - byteLength) {
return fail(QStringLiteral("compressed data is outside buffer %1").arg(sourceBuffer));
}
enum class Mode { Attributes, Triangles, Indices };
Mode decodeMode;
if (mode == QLatin1String("ATTRIBUTES"))
decodeMode = Mode::Attributes;
else if (mode == QLatin1String("TRIANGLES"))
decodeMode = Mode::Triangles;
else if (mode == QLatin1String("INDICES"))
decodeMode = Mode::Indices;
else
return fail(QStringLiteral("unsupported compression mode '%1'").arg(mode));
// Stride limits required by the decoders
if (decodeMode == Mode::Attributes) {
if (byteStride < 4 || byteStride > 256 || byteStride % 4)
return fail(QStringLiteral("invalid byte stride %1 for ATTRIBUTES mode").arg(byteStride));
} else {
if (byteStride != 2 && byteStride != 4)
return fail(QStringLiteral("invalid byte stride %1 for index data").arg(byteStride));
if (decodeMode == Mode::Triangles && count % 3)
return fail(QStringLiteral("TRIANGLES element count %1 is not divisible by 3").arg(count));
if (filter != QLatin1String("NONE"))
return fail(QStringLiteral("filters do not apply to index data"));
}
if (decodeMode == Mode::Attributes && filter != QLatin1String("NONE") && filter != QLatin1String("OCTAHEDRAL")
&& filter != QLatin1String("QUATERNION") && filter != QLatin1String("EXPONENTIAL")) {
return fail(QStringLiteral("unsupported filter '%1'").arg(filter));
}
if ((filter == QLatin1String("OCTAHEDRAL") && byteStride != 4 && byteStride != 8)
|| (filter == QLatin1String("QUATERNION") && byteStride != 8)) {
return fail(QStringLiteral("invalid byte stride %1 for filter %2").arg(byteStride).arg(filter));
}
const QString key = QStringLiteral("%1:%2:%3:%4:%5:%6:%7")
.arg(sourceBuffer).arg(byteOffset).arg(byteLength).arg(count)
.arg(byteStride).arg(mode, filter);
const auto rewriteView = [&view, decodeMode, byteStride](int buffer, qint64 length) {
view.buffer = buffer;
view.byteOffset = 0;
view.byteLength = length;
view.byteStride = decodeMode == Mode::Attributes ? int(byteStride) : 0;
};
if (const auto it = state.decodedBuffers.constFind(key); it != state.decodedBuffers.constEnd()) {
rewriteView(*it, document.buffers.at(*it).byteLength);
return true;
}
if (count * byteStride > state.remainingBytes)
return fail(QStringLiteral("decoded data exceeds the limit for the document"));
QByteArray decoded(count * byteStride, Qt::Uninitialized);
const auto *source = reinterpret_cast<const unsigned char *>(compressed.data.constData()) + byteOffset;
int result = -1;
switch (decodeMode) {
case Mode::Attributes:
result = meshopt_decodeVertexBuffer(decoded.data(), size_t(count), size_t(byteStride), source,
size_t(byteLength));
break;
case Mode::Triangles:
result = meshopt_decodeIndexBuffer(decoded.data(), size_t(count), size_t(byteStride), source,
size_t(byteLength));
break;
case Mode::Indices:
result = meshopt_decodeIndexSequence(decoded.data(), size_t(count), size_t(byteStride), source,
size_t(byteLength));
break;
}
if (result != 0)
return fail(QStringLiteral("corrupt compressed data"));
if (filter == QLatin1String("OCTAHEDRAL"))
meshopt_decodeFilterOct(decoded.data(), size_t(count), size_t(byteStride));
else if (filter == QLatin1String("QUATERNION"))
meshopt_decodeFilterQuat(decoded.data(), size_t(count), size_t(byteStride));
else if (filter == QLatin1String("EXPONENTIAL"))
meshopt_decodeFilterExp(decoded.data(), size_t(count), size_t(byteStride));
state.remainingBytes -= decoded.size();
Buffer decodedBuffer;
decodedBuffer.byteLength = decoded.size();
decodedBuffer.data = std::move(decoded);
document.buffers.append(decodedBuffer);
const int bufferIndex = int(document.buffers.size() - 1);
state.decodedBuffers.insert(key, bufferIndex);
rewriteView(bufferIndex, decodedBuffer.byteLength);
return true;
}
} // namespace
/*!
\class QSSGGltfParser
\internal
Parses glTF 2.0 content (.gltf JSON or binary .glb container) into a
QSSGGltfDocument. Buffers (external files, data: URIs, and the GLB BIN
chunk) are resolved eagerly; image contents are left unresolved.
Validation is two-tier: structural violations (out-of-range indices,
accessors extending past their buffer view, truncated GLB, unsupported
required extensions) fail the parse with an error message; tolerable
deviations from the specification are logged as warnings and clamped,
repaired, or ignored, matching the leniency of other glTF consumers. A
node with more than one parent and a scene root that is not unique both
fall in the second group: the offending edge is dropped, because the node
hierarchy has to be a forest for consumers to recurse over it safely, but
the rest of the asset is still worth loading.
*/
/*!
\internal
Returns the extensions the parser understands. Assets that list anything
else in extensionsRequired fail to parse.
Being on this list means the extension is represented in QSSGGltfDocument,
not that any particular consumer acts on it: a consumer that ignores one
of these still gets a document it can load.
*/
QStringList QSSGGltfParser::supportedExtensions()
{
// Order does not matter; keep alphabetical for readability. Extensions
// needing a decoder we do not ship (KHR_draco_mesh_compression,
// KHR_texture_basisu) are intentionally absent and get a clear error
// when required by an asset.
// KHR_xmp_json_ld is accepted, but left in the raw extension objects.
return {
QStringLiteral("EXT_mesh_gpu_instancing"),
QStringLiteral("EXT_meshopt_compression"),
QStringLiteral("EXT_texture_webp"),
QStringLiteral("KHR_lights_punctual"),
QStringLiteral("KHR_materials_anisotropy"),
QStringLiteral("KHR_materials_clearcoat"),
QStringLiteral("KHR_materials_dispersion"),
QStringLiteral("KHR_materials_emissive_strength"),
QStringLiteral("KHR_materials_ior"),
QStringLiteral("KHR_materials_iridescence"),
QStringLiteral("KHR_materials_pbrSpecularGlossiness"),
QStringLiteral("KHR_materials_sheen"),
QStringLiteral("KHR_materials_specular"),
QStringLiteral("KHR_materials_transmission"),
QStringLiteral("KHR_materials_unlit"),
QStringLiteral("KHR_materials_variants"),
QStringLiteral("KHR_materials_volume"),
QStringLiteral("KHR_mesh_quantization"),
QStringLiteral("KHR_node_visibility"),
QStringLiteral("KHR_texture_transform"),
QStringLiteral("KHR_xmp_json_ld"),
};
}
bool QSSGGltfParser::setError(const QString &message)
{
m_errorMessage = message;
qCWarning(lcQuick3DGltf) << message;
return false;
}
/*!
\internal
Convenience overload of parse() reading \a filePath, a local file or qrc
path, into \a document.
*/
bool QSSGGltfParser::parseFile(const QString &filePath, QSSGGltfDocument *document)
{
QFile file(filePath);
if (!file.open(QIODevice::ReadOnly))
return setError(QStringLiteral("Failed to open '%1': %2").arg(filePath, file.errorString()));
return parse(file.readAll(), QFileInfo(filePath).path(), document);
}
/*!
\internal
Parses .gltf JSON or a .glb container in \a data into \a document.
\a baseDir is the local or qrc directory used to resolve relative
buffer and image URIs.
*/
bool QSSGGltfParser::parse(const QByteArray &data, const QString &baseDir, QSSGGltfDocument *document)
{
Q_ASSERT(document);
m_errorMessage.clear();
*document = QSSGGltfDocument();
document->baseDir = baseDir;
QByteArray json = data;
QByteArray binChunk;
// Binary container? (12-byte header: magic, version, length)
if (data.size() >= 12 && qFromLittleEndian<quint32>(data.constData()) == GLB_MAGIC) {
const quint32 version = qFromLittleEndian<quint32>(data.constData() + 4);
if (version != 2)
return setError(QStringLiteral("Unsupported GLB container version %1").arg(version));
const quint32 length = qFromLittleEndian<quint32>(data.constData() + 8);
if (qint64(length) > data.size())
return setError(QStringLiteral("Truncated GLB file: header declares %1 bytes, got %2")
.arg(length).arg(data.size()));
json.clear();
qint64 offset = 12;
while (offset + 8 <= qint64(length)) {
QT_WARNING_PUSH QT_WARNING_DISABLE_GCC("-Warray-bounds")
// gcc 15.2.0 warns because of static const char _empty in QByteArray
const quint32 chunkLength = qFromLittleEndian<quint32>(data.constData() + offset);
const quint32 chunkType = qFromLittleEndian<quint32>(data.constData() + offset + 4);
QT_WARNING_POP
offset += 8;
if (offset + qint64(chunkLength) > qint64(length))
return setError(QStringLiteral("Truncated GLB chunk at offset %1").arg(offset - 8));
if (chunkType == GLB_CHUNK_JSON && json.isEmpty())
json = data.mid(offset, chunkLength);
else if (chunkType == GLB_CHUNK_BIN && binChunk.isEmpty())
binChunk = data.mid(offset, chunkLength);
else
qCWarning(lcQuick3DGltf) << "Skipping unknown GLB chunk type" << Qt::hex << chunkType;
// Chunks are 4-byte aligned
offset += chunkLength;
if (offset % 4)
offset += 4 - (offset % 4);
}
if (json.isEmpty())
return setError(QStringLiteral("GLB container has no JSON chunk"));
}
QJsonParseError jsonError;
const QJsonDocument jsonDocument = QJsonDocument::fromJson(json, &jsonError);
if (jsonDocument.isNull())
return setError(QStringLiteral("Invalid glTF JSON: %1").arg(jsonError.errorString()));
if (!jsonDocument.isObject())
return setError(QStringLiteral("Invalid glTF JSON: root is not an object"));
const QJsonObject root = jsonDocument.object();
// asset (required)
{
const QJsonValue assetValue = root.value(QLatin1String("asset"));
if (!assetValue.isObject())
return setError(QStringLiteral("Not a glTF document: no asset object"));
const QJsonObject asset = assetValue.toObject();
document->asset.version = asset.value(QLatin1String("version")).toString();
document->asset.minVersion = asset.value(QLatin1String("minVersion")).toString();
document->asset.generator = asset.value(QLatin1String("generator")).toString();
document->asset.copyright = asset.value(QLatin1String("copyright")).toString();
const int major = document->asset.version.section(QLatin1Char('.'), 0, 0).toInt();
if (major != 2)
return setError(QStringLiteral("Unsupported glTF version '%1'").arg(document->asset.version));
// The specification requires a client to fail when minVersion asks for
// more than it implements. Only 2.0 exists so far, so this is about not
// silently loading a future asset that says it needs more.
if (!document->asset.minVersion.isEmpty()) {
const int minMajor = document->asset.minVersion.section(QLatin1Char('.'), 0, 0).toInt();
const int minMinor = document->asset.minVersion.section(QLatin1Char('.'), 1, 1).toInt();
if (minMajor != 2 || minMinor > 0)
return setError(QStringLiteral("Asset requires glTF version '%1' or higher")
.arg(document->asset.minVersion));
}
}
// extensionsUsed / extensionsRequired
const QJsonArray extensionsUsed = root.value(QLatin1String("extensionsUsed")).toArray();
for (const auto &value : extensionsUsed)
document->extensionsUsed.append(value.toString());
const QJsonArray extensionsRequired = root.value(QLatin1String("extensionsRequired")).toArray();
for (const auto &value : extensionsRequired)
document->extensionsRequired.append(value.toString());
document->rootExtensions = root.value(QLatin1String("extensions")).toObject();
// KHR_materials_variants (document level): the variant names
{
const QJsonValue variantsExt = document->rootExtensions.value(QLatin1String("KHR_materials_variants"));
if (variantsExt.isObject()) {
const QJsonArray variants = variantsExt.toObject().value(QLatin1String("variants")).toArray();
for (const auto &value : variants)
document->materialVariants.append(value.toObject().value(QLatin1String("name")).toString());
}
}
const QStringList supported = supportedExtensions();
for (const QString &required : std::as_const(document->extensionsRequired)) {
if (!supported.contains(required))
return setError(QStringLiteral("Asset requires unsupported glTF extension '%1'").arg(required));
}
for (const QString &used : std::as_const(document->extensionsUsed)) {
if (!supported.contains(used))
qCWarning(lcQuick3DGltf) << "Ignoring unsupported glTF extension" << used;
}
// buffers
const QJsonArray buffers = root.value(QLatin1String("buffers")).toArray();
for (const auto &value : buffers) {
const QJsonObject object = value.toObject();
Buffer buffer;
buffer.uri = object.value(QLatin1String("uri")).toString();
buffer.byteLength = qint64(object.value(QLatin1String("byteLength")).toDouble());
if (buffer.byteLength < 0)
return setError(QStringLiteral("Buffer %1 has negative byte length").arg(document->buffers.size()));
buffer.meshoptFallback = object.value(QLatin1String("extensions")).toObject()
.value(QLatin1String("EXT_meshopt_compression")).toObject()
.value(QLatin1String("fallback")).toBool(false);
if (buffer.meshoptFallback) {
document->buffers.append(buffer);
continue;
}
if (buffer.uri.isEmpty()) {
// GLB BIN chunk; only valid for the first buffer
if (document->buffers.isEmpty() && !binChunk.isEmpty())
buffer.data = binChunk;
else if (binChunk.isEmpty())
return setError(
QStringLiteral("Buffer %1 has no URI and there is no GLB BIN chunk")
.arg(document->buffers.size()));
else
return setError(QStringLiteral("Only the first buffer may refer to the GLB BIN chunk"));
} else {
QString resolveError;
buffer.data = QSSGGltfResourceResolver::loadUri(buffer.uri, baseDir, &resolveError);
// Loading can legitimately produce nothing, for an empty file or an
// empty data URI payload, so the error string is what says whether
// it failed. The length check below rejects a short buffer anyway.
if (!resolveError.isEmpty())
return setError(QStringLiteral("Failed to load buffer %1: %2")
.arg(document->buffers.size()).arg(resolveError));
}
if (buffer.data.size() < buffer.byteLength)
return setError(QStringLiteral("Buffer %1 is %2 bytes, expected at least %3")
.arg(document->buffers.size()).arg(buffer.data.size()).arg(buffer.byteLength));
document->buffers.append(buffer);
}
// bufferViews
MeshoptDecodeState meshoptState;
const QJsonArray bufferViews = root.value(QLatin1String("bufferViews")).toArray();
for (const auto &value : bufferViews) {
const QJsonObject object = value.toObject();
BufferView view;
view.buffer = object.value(QLatin1String("buffer")).toInt(-1);
view.byteOffset = qint64(object.value(QLatin1String("byteOffset")).toDouble(0));
view.byteLength = qint64(object.value(QLatin1String("byteLength")).toDouble());
view.byteStride = object.value(QLatin1String("byteStride")).toInt(0);
view.target = object.value(QLatin1String("target")).toInt(0);
view.name = object.value(QLatin1String("name")).toString();
const QJsonValue meshoptValue = object.value(QLatin1String("extensions")).toObject()
.value(QLatin1String("EXT_meshopt_compression"));
if (meshoptValue.isObject()) {
QString decodeError;
if (!decodeMeshoptBufferView(meshoptValue.toObject(), *document, view, meshoptState, &decodeError)) {
return setError(QStringLiteral("Buffer view %1: EXT_meshopt_compression: %2")
.arg(document->bufferViews.size()).arg(decodeError));
}
document->bufferViews.append(view);
continue;
}
if (view.buffer < 0 || view.buffer >= document->buffers.size())
return setError(
QStringLiteral("Buffer view %1 references invalid buffer %2")
.arg(document->bufferViews.size()).arg(view.buffer));
if (document->buffers.at(view.buffer).meshoptFallback)
return setError(QStringLiteral("Buffer view %1 references unloaded fallback buffer %2")
.arg(document->bufferViews.size()).arg(view.buffer));
// The stride bound is from the specification; together with
// non-negative offsets and lengths it also keeps all later offset
// arithmetic far away from overflowing 64 bits.
if (view.byteOffset < 0 || view.byteLength < 0 || view.byteStride < 0 || view.byteStride > 252)
return setError(QStringLiteral("Buffer view %1 has an invalid byte offset, length, or stride")
.arg(document->bufferViews.size()));
// Compared without adding the two together: both are individually
// positive by the check above, but their sum can overflow to a negative
// value and slip under the buffer length.
const qint64 bufferLength = document->buffers.at(view.buffer).byteLength;
if (view.byteLength > bufferLength || view.byteOffset > bufferLength - view.byteLength)
return setError(
QStringLiteral("Buffer view %1 extends past the end of buffer %2")
.arg(document->bufferViews.size()).arg(view.buffer));
document->bufferViews.append(view);
}
// accessors
const QJsonArray accessors = root.value(QLatin1String("accessors")).toArray();
for (const auto &value : accessors) {
const QJsonObject object = value.toObject();
Accessor accessor;
accessor.bufferView = object.value(QLatin1String("bufferView")).toInt(-1);
accessor.byteOffset = qint64(object.value(QLatin1String("byteOffset")).toDouble(0));
const int componentType = object.value(QLatin1String("componentType")).toInt();
if (!isValidComponentType(componentType))
return setError(QStringLiteral("Accessor %1 has invalid component type %2")
.arg(document->accessors.size()).arg(componentType));
accessor.componentType = Accessor::ComponentType(componentType);
bool typeOk = false;
accessor.type = accessorTypeFromString(object.value(QLatin1String("type")).toString(), &typeOk);
if (!typeOk)
return setError(QStringLiteral("Accessor %1 has invalid type '%2'")
.arg(document->accessors.size())
.arg(object.value(QLatin1String("type")).toString()));
// The specification puts no upper bound on count, but the offset
// arithmetic below needs one. A count near the top of the qint64 range
// makes those products overflow, which is undefined behavior, and in
// practice wraps the range test at the end of this loop into an
// acceptance: a ten-line document then yields an accessor claiming
// more elements than could ever be allocated. One element occupies at
// least one byte, so no real asset comes anywhere near this cap, and
// capping in elements keeps every product here well inside 64 bits.
constexpr qint64 maxAccessorElements = qint64(256) * 1024 * 1024;
const double rawCount = object.value(QLatin1String("count")).toDouble();
if (!(rawCount >= 0.0 && rawCount <= double(maxAccessorElements)))
return setError(QStringLiteral("Accessor %1 has a negative or unreasonably large count")
.arg(document->accessors.size()));
accessor.count = qint64(rawCount);
if (accessor.byteOffset < 0)
return setError(QStringLiteral("Accessor %1 has a negative byte offset")
.arg(document->accessors.size()));
accessor.normalized = object.value(QLatin1String("normalized")).toBool(false);
accessor.name = object.value(QLatin1String("name")).toString();
const QJsonArray minValues = object.value(QLatin1String("min")).toArray();
for (const auto &m : minValues)
accessor.min.append(m.toDouble());
const QJsonArray maxValues = object.value(QLatin1String("max")).toArray();
for (const auto &m : maxValues)
accessor.max.append(m.toDouble());
const QJsonValue sparseValue = object.value(QLatin1String("sparse"));
if (sparseValue.isObject()) {
const QJsonObject sparseObject = sparseValue.toObject();
Accessor::Sparse sparse;
// Bounded the same way as accessor.count, and for the same reason:
// it is multiplied by a component size a few lines down
const double rawSparseCount = sparseObject.value(QLatin1String("count")).toDouble();
if (!(rawSparseCount >= 0.0 && rawSparseCount <= double(maxAccessorElements)))
return setError(QStringLiteral("Accessor %1 has a negative or unreasonably large sparse count")
.arg(document->accessors.size()));
sparse.count = qint64(rawSparseCount);
const QJsonObject indices = sparseObject.value(QLatin1String("indices")).toObject();
sparse.indicesBufferView = indices.value(QLatin1String("bufferView")).toInt(-1);
sparse.indicesByteOffset = qint64(indices.value(QLatin1String("byteOffset")).toDouble(0));
const int indicesComponentType = indices.value(QLatin1String("componentType")).toInt();
if (!isValidComponentType(indicesComponentType))
return setError(
QStringLiteral("Accessor %1 sparse indices have invalid component type")
.arg(document->accessors.size()));
sparse.indicesComponentType = Accessor::ComponentType(indicesComponentType);
const QJsonObject values = sparseObject.value(QLatin1String("values")).toObject();
sparse.valuesBufferView = values.value(QLatin1String("bufferView")).toInt(-1);
sparse.valuesByteOffset = qint64(values.value(QLatin1String("byteOffset")).toDouble(0));
if (sparse.indicesBufferView < 0 || sparse.indicesBufferView >= document->bufferViews.size()
|| sparse.valuesBufferView < 0 || sparse.valuesBufferView >= document->bufferViews.size()) {
return setError(QStringLiteral("Accessor %1 sparse data references an invalid buffer view")
.arg(document->accessors.size()));
}
if (sparse.count > accessor.count
|| sparse.indicesByteOffset < 0 || sparse.valuesByteOffset < 0) {
return setError(QStringLiteral("Accessor %1 has an invalid sparse count or byte offset")
.arg(document->accessors.size()));
}
// Offsets kept out of the sums, like the buffer view check above
const BufferView &indicesView = document->bufferViews.at(sparse.indicesBufferView);
const BufferView &valuesView = document->bufferViews.at(sparse.valuesBufferView);
const qint64 indicesSpan =
sparse.count * Accessor::componentByteSize(sparse.indicesComponentType);
const qint64 valuesSpan = sparse.count * accessor.elementByteSize();
if (indicesSpan > indicesView.byteLength
|| sparse.indicesByteOffset > indicesView.byteLength - indicesSpan
|| valuesSpan > valuesView.byteLength
|| sparse.valuesByteOffset > valuesView.byteLength - valuesSpan) {
return setError(
QStringLiteral("Accessor %1 sparse data extends past the end of its buffer view")
.arg(document->accessors.size()));
}
accessor.sparse = sparse;
}
if (accessor.bufferView >= document->bufferViews.size())
return setError(
QStringLiteral("Accessor %1 references invalid buffer view %2")
.arg(document->accessors.size()).arg(accessor.bufferView));
if (accessor.bufferView >= 0) {
const BufferView &view = document->bufferViews.at(accessor.bufferView);
const qint64 elementSize = accessor.elementByteSize();
const qint64 stride = view.byteStride > 0 ? view.byteStride : elementSize;
// Offset kept out of the sum, like the buffer view check above
const qint64 span = (accessor.count - 1) * stride + elementSize;
if (accessor.count > 0
&& (span > view.byteLength || accessor.byteOffset > view.byteLength - span))
return setError(QStringLiteral("Accessor %1 extends past the end of buffer view %2")
.arg(document->accessors.size())
.arg(accessor.bufferView));
} else {
// An accessor without a buffer view is read as all zeros, so no
// buffer bounds its size. The element cap above keeps this product
// from overflowing, but 256M elements of MAT4 would still be a
// 16 GB allocation, so bound the byte count as well.
constexpr qint64 maxSyntheticAccessorBytes = qint64(256) * 1024 * 1024;
if (accessor.count * accessor.elementByteSize() > maxSyntheticAccessorBytes)
return setError(QStringLiteral("Accessor %1 has no buffer view and an unreasonably large count")
.arg(document->accessors.size()));
}
document->accessors.append(accessor);
}
// images
const QJsonArray images = root.value(QLatin1String("images")).toArray();
for (const auto &value : images) {
const QJsonObject object = value.toObject();
Image image;
image.uri = object.value(QLatin1String("uri")).toString();
image.bufferView = object.value(QLatin1String("bufferView")).toInt(-1);
image.mimeType = object.value(QLatin1String("mimeType")).toString();
image.name = object.value(QLatin1String("name")).toString();
if (image.bufferView >= document->bufferViews.size())
return setError(QStringLiteral("Image %1 references invalid buffer view %2")
.arg(document->images.size()).arg(image.bufferView));
document->images.append(image);
}
// samplers
const QJsonArray samplers = root.value(QLatin1String("samplers")).toArray();
for (const auto &value : samplers) {
const QJsonObject object = value.toObject();
Sampler sampler;
sampler.magFilter = object.value(QLatin1String("magFilter")).toInt(0);
sampler.minFilter = object.value(QLatin1String("minFilter")).toInt(0);
sampler.wrapS = object.value(QLatin1String("wrapS")).toInt(Sampler::Repeat);
sampler.wrapT = object.value(QLatin1String("wrapT")).toInt(Sampler::Repeat);
sampler.name = object.value(QLatin1String("name")).toString();
document->samplers.append(sampler);
}
// textures
const QJsonArray textures = root.value(QLatin1String("textures")).toArray();
for (const auto &value : textures) {
const QJsonObject object = value.toObject();
Texture texture;
texture.sampler = object.value(QLatin1String("sampler")).toInt(-1);
texture.source = object.value(QLatin1String("source")).toInt(-1);
texture.name = object.value(QLatin1String("name")).toString();
texture.extensions = object.value(QLatin1String("extensions")).toObject();
const QJsonValue webpExt = texture.extensions.value(QLatin1String("EXT_texture_webp"));
if (webpExt.isObject())
texture.webpSource = webpExt.toObject().value(QLatin1String("source")).toInt(-1);
if (texture.sampler >= document->samplers.size())
return setError(QStringLiteral("Texture %1 references invalid sampler %2")
.arg(document->textures.size()).arg(texture.sampler));
if (texture.source >= document->images.size() || texture.webpSource >= document->images.size())
return setError(QStringLiteral("Texture %1 references invalid image %2")
.arg(document->textures.size())
.arg(qMax(texture.source, texture.webpSource)));
document->textures.append(texture);
}
// materials
//
// Every texture reference goes through parseTexture() rather than
// parseTextureInfo() directly, so that the index is range checked in one
// place and a newly supported extension cannot forget to do it. The
// textures array is already parsed at this point.
int badTextureIndex = -1;
const auto parseTexture = [&](const QJsonObject &object,
const char *scaleOrStrengthKey = nullptr) {
const TextureInfo info = parseTextureInfo(object, scaleOrStrengthKey);
if (info.index >= document->textures.size())
badTextureIndex = info.index;
return info;
};
const QJsonArray materials = root.value(QLatin1String("materials")).toArray();
for (const auto &value : materials) {
const QJsonObject object = value.toObject();
Material material;
material.name = object.value(QLatin1String("name")).toString();
const QJsonValue pbrValue = object.value(QLatin1String("pbrMetallicRoughness"));
if (pbrValue.isObject()) {
const QJsonObject pbr = pbrValue.toObject();
material.baseColorFactor =
toVector4D(pbr.value(QLatin1String("baseColorFactor")).toArray(), material.baseColorFactor);
material.baseColorTexture = parseTexture(pbr.value(QLatin1String("baseColorTexture")).toObject());
material.metallicFactor = float(pbr.value(QLatin1String("metallicFactor")).toDouble(1.0));
material.roughnessFactor = float(pbr.value(QLatin1String("roughnessFactor")).toDouble(1.0));
material.metallicRoughnessTexture =
parseTexture(pbr.value(QLatin1String("metallicRoughnessTexture")).toObject());
}
material.normalTexture = parseTexture(object.value(QLatin1String("normalTexture")).toObject(), "scale");
material.occlusionTexture =
parseTexture(object.value(QLatin1String("occlusionTexture")).toObject(), "strength");
material.emissiveTexture = parseTexture(object.value(QLatin1String("emissiveTexture")).toObject());
material.emissiveFactor =
toVector3D(object.value(QLatin1String("emissiveFactor")).toArray(), material.emissiveFactor);
const QString alphaMode = object.value(QLatin1String("alphaMode")).toString();
if (alphaMode == QLatin1String("MASK"))
material.alphaMode = Material::AlphaMode::Mask;
else if (alphaMode == QLatin1String("BLEND"))
material.alphaMode = Material::AlphaMode::Blend;
material.alphaCutoff = float(object.value(QLatin1String("alphaCutoff")).toDouble(0.5));
material.doubleSided = object.value(QLatin1String("doubleSided")).toBool(false);
material.extensions = object.value(QLatin1String("extensions")).toObject();
const QJsonObject &ext = material.extensions;
material.unlit = ext.contains(QLatin1String("KHR_materials_unlit"));
const QJsonValue sgValue = ext.value(QLatin1String("KHR_materials_pbrSpecularGlossiness"));
if (sgValue.isObject()) {
const QJsonObject sg = sgValue.toObject();
Material::SpecularGlossiness specularGlossiness;
specularGlossiness.diffuseFactor = toVector4D(sg.value(QLatin1String("diffuseFactor")).toArray(),
specularGlossiness.diffuseFactor);
specularGlossiness.diffuseTexture = parseTexture(sg.value(QLatin1String("diffuseTexture")).toObject());
specularGlossiness.specularFactor = toVector3D(sg.value(QLatin1String("specularFactor")).toArray(),
specularGlossiness.specularFactor);
specularGlossiness.glossinessFactor = float(sg.value(QLatin1String("glossinessFactor")).toDouble(1.0));
specularGlossiness.specularGlossinessTexture = parseTexture(
sg.value(QLatin1String("specularGlossinessTexture")).toObject());
material.specularGlossiness = specularGlossiness;
}
const QJsonValue ccValue = ext.value(QLatin1String("KHR_materials_clearcoat"));
if (ccValue.isObject()) {
const QJsonObject cc = ccValue.toObject();
Material::Clearcoat clearcoat;
clearcoat.clearcoatFactor = float(cc.value(QLatin1String("clearcoatFactor")).toDouble(0.0));
clearcoat.clearcoatTexture = parseTexture(cc.value(QLatin1String("clearcoatTexture")).toObject());
clearcoat.clearcoatRoughnessFactor =
float(cc.value(QLatin1String("clearcoatRoughnessFactor")).toDouble(0.0));
clearcoat.clearcoatRoughnessTexture =
parseTexture(cc.value(QLatin1String("clearcoatRoughnessTexture")).toObject());
clearcoat.clearcoatNormalTexture =
parseTexture(cc.value(QLatin1String("clearcoatNormalTexture")).toObject(), "scale");
material.clearcoat = clearcoat;
}
const QJsonValue trValue = ext.value(QLatin1String("KHR_materials_transmission"));
if (trValue.isObject()) {
const QJsonObject tr = trValue.toObject();
Material::Transmission transmission;
transmission.transmissionFactor = float(tr.value(QLatin1String("transmissionFactor")).toDouble(0.0));
transmission.transmissionTexture =
parseTexture(tr.value(QLatin1String("transmissionTexture")).toObject());
material.transmission = transmission;
}
const QJsonValue volValue = ext.value(QLatin1String("KHR_materials_volume"));
if (volValue.isObject()) {
const QJsonObject vol = volValue.toObject();
Material::Volume volume;
volume.thicknessFactor = float(vol.value(QLatin1String("thicknessFactor")).toDouble(0.0));
volume.thicknessTexture = parseTexture(vol.value(QLatin1String("thicknessTexture")).toObject());
volume.attenuationDistance = float(vol.value(QLatin1String("attenuationDistance")).toDouble(0.0));
volume.attenuationColor =
toVector3D(vol.value(QLatin1String("attenuationColor")).toArray(), volume.attenuationColor);
material.volume = volume;
}
const QJsonValue iorValue = ext.value(QLatin1String("KHR_materials_ior"));
if (iorValue.isObject())
material.ior = float(iorValue.toObject().value(QLatin1String("ior")).toDouble(1.5));
const QJsonValue esValue = ext.value(QLatin1String("KHR_materials_emissive_strength"));
if (esValue.isObject())
material.emissiveStrength =
float(esValue.toObject().value(QLatin1String("emissiveStrength")).toDouble(1.0));
const QJsonValue spValue = ext.value(QLatin1String("KHR_materials_specular"));
if (spValue.isObject()) {
const QJsonObject sp = spValue.toObject();
Material::Specular specular;
specular.specularFactor = float(sp.value(QLatin1String("specularFactor")).toDouble(1.0));
specular.specularTexture = parseTexture(sp.value(QLatin1String("specularTexture")).toObject());
specular.specularColorFactor = toVector3D(sp.value(QLatin1String("specularColorFactor")).toArray(),
specular.specularColorFactor);
specular.specularColorTexture =
parseTexture(sp.value(QLatin1String("specularColorTexture")).toObject());
material.specular = specular;
}
const QJsonValue shValue = ext.value(QLatin1String("KHR_materials_sheen"));
if (shValue.isObject()) {
const QJsonObject sh = shValue.toObject();
Material::Sheen sheen;
sheen.sheenColorFactor = toVector3D(sh.value(QLatin1String("sheenColorFactor")).toArray(), sheen.sheenColorFactor);
sheen.sheenColorTexture = parseTexture(sh.value(QLatin1String("sheenColorTexture")).toObject());
sheen.sheenRoughnessFactor = float(sh.value(QLatin1String("sheenRoughnessFactor")).toDouble(0.0));
sheen.sheenRoughnessTexture = parseTexture(sh.value(QLatin1String("sheenRoughnessTexture")).toObject());
material.sheen = sheen;
}
const QJsonValue anValue = ext.value(QLatin1String("KHR_materials_anisotropy"));
if (anValue.isObject()) {
const QJsonObject an = anValue.toObject();
Material::Anisotropy anisotropy;
anisotropy.anisotropyStrength = float(an.value(QLatin1String("anisotropyStrength")).toDouble(0.0));
anisotropy.anisotropyRotation = float(an.value(QLatin1String("anisotropyRotation")).toDouble(0.0));
anisotropy.anisotropyTexture = parseTexture(an.value(QLatin1String("anisotropyTexture")).toObject());
material.anisotropy = anisotropy;
}
const QJsonValue irValue = ext.value(QLatin1String("KHR_materials_iridescence"));
if (irValue.isObject()) {
const QJsonObject ir = irValue.toObject();
Material::Iridescence iridescence;
iridescence.iridescenceFactor = float(ir.value(QLatin1String("iridescenceFactor")).toDouble(0.0));
iridescence.iridescenceTexture = parseTexture(ir.value(QLatin1String("iridescenceTexture")).toObject());
iridescence.iridescenceIor = float(ir.value(QLatin1String("iridescenceIor")).toDouble(1.3));
iridescence.iridescenceThicknessMinimum = float(ir.value(QLatin1String("iridescenceThicknessMinimum")).toDouble(100.0));
iridescence.iridescenceThicknessMaximum = float(ir.value(QLatin1String("iridescenceThicknessMaximum")).toDouble(400.0));
iridescence.iridescenceThicknessTexture = parseTexture(
ir.value(QLatin1String("iridescenceThicknessTexture")).toObject());
material.iridescence = iridescence;
}
const QJsonValue dispValue = ext.value(QLatin1String("KHR_materials_dispersion"));
if (dispValue.isObject())
material.dispersion = float(dispValue.toObject().value(QLatin1String("dispersion")).toDouble(0.0));
document->materials.append(material);
}
if (badTextureIndex >= 0)
return setError(QStringLiteral("Material references invalid texture %1").arg(badTextureIndex));
// meshes
const QJsonArray meshes = root.value(QLatin1String("meshes")).toArray();
for (const auto &value : meshes) {
const QJsonObject object = value.toObject();
Mesh mesh;
mesh.name = object.value(QLatin1String("name")).toString();
const QJsonArray weights = object.value(QLatin1String("weights")).toArray();
for (const auto &w : weights)
mesh.weights.append(float(w.toDouble()));
const QJsonArray primitives = object.value(QLatin1String("primitives")).toArray();
for (const auto &primitiveValue : primitives) {
const QJsonObject primitiveObject = primitiveValue.toObject();
MeshPrimitive primitive;
const QJsonObject attributes = primitiveObject.value(QLatin1String("attributes")).toObject();
for (auto it = attributes.constBegin(); it != attributes.constEnd(); ++it)
primitive.attributes.insert(it.key().toUtf8(), it.value().toInt(-1));
primitive.indices = primitiveObject.value(QLatin1String("indices")).toInt(-1);
primitive.material = primitiveObject.value(QLatin1String("material")).toInt(-1);
primitive.mode = primitiveObject.value(QLatin1String("mode")).toInt(MeshPrimitive::Triangles);
const QJsonArray targets = primitiveObject.value(QLatin1String("targets")).toArray();
for (const auto &targetValue : targets) {
const QJsonObject targetObject = targetValue.toObject();
QHash<QByteArray, int> target;
for (auto it = targetObject.constBegin(); it != targetObject.constEnd(); ++it)
target.insert(it.key().toUtf8(), it.value().toInt(-1));
primitive.targets.append(target);
}
primitive.extensions = primitiveObject.value(QLatin1String("extensions")).toObject();
const QJsonValue variantsExt = primitive.extensions.value(QLatin1String("KHR_materials_variants"));
if (variantsExt.isObject()) {
const QJsonArray mappings = variantsExt.toObject().value(QLatin1String("mappings")).toArray();
for (const auto &mappingValue : mappings) {
const QJsonObject mappingObject = mappingValue.toObject();
MeshPrimitive::VariantMapping mapping;
mapping.material = mappingObject.value(QLatin1String("material")).toInt(-1);
if (mapping.material < 0 || mapping.material >= document->materials.size()) {
return setError(
QStringLiteral("Mesh %1 primitive variant mapping references invalid material %2")
.arg(document->meshes.size())
.arg(mapping.material));
}
const QJsonArray mappingVariants = mappingObject.value(QLatin1String("variants")).toArray();
for (const auto &variantValue : mappingVariants) {
const int variant = variantValue.toInt(-1);
if (variant < 0 || variant >= document->materialVariants.size()) {
return setError(
QStringLiteral("Mesh %1 primitive variant mapping references invalid variant %2")
.arg(document->meshes.size())
.arg(variant));
}
mapping.variants.append(variant);
}
primitive.variantMappings.append(mapping);
}
}
// Structural validation of accessor references. As everywhere in
// this parser, any negative index means "unset": the glTF defaults
// are -1, the readers return nothing for a negative index, and so
// only the upper bound needs checking here.
const auto checkAccessor = [&](int accessor, const char *what) {
if (accessor >= document->accessors.size())
return setError(QStringLiteral("Mesh %1 primitive references invalid %2 accessor %3")
.arg(document->meshes.size()).arg(QLatin1String(what)).arg(accessor));
return true;
};
for (auto it = primitive.attributes.constBegin(); it != primitive.attributes.constEnd(); ++it) {
if (!checkAccessor(it.value(), it.key().constData()))
return false;
}
if (primitive.indices >= 0 && !checkAccessor(primitive.indices, "index"))
return false;
if (primitive.material >= document->materials.size())
return setError(QStringLiteral("Mesh %1 primitive references invalid material %2")
.arg(document->meshes.size())
.arg(primitive.material));
mesh.primitives.append(primitive);
}
if (mesh.primitives.isEmpty())
qCWarning(lcQuick3DGltf) << "Mesh" << document->meshes.size() << "has no primitives";
document->meshes.append(mesh);
}
// cameras
const QJsonArray cameras = root.value(QLatin1String("cameras")).toArray();
for (const auto &value : cameras) {
const QJsonObject object = value.toObject();
Camera camera;
camera.name = object.value(QLatin1String("name")).toString();
const QString type = object.value(QLatin1String("type")).toString();
if (type == QLatin1String("orthographic")) {
camera.type = Camera::Type::Orthographic;
const QJsonObject ortho = object.value(QLatin1String("orthographic")).toObject();
camera.xmag = float(ortho.value(QLatin1String("xmag")).toDouble());
camera.ymag = float(ortho.value(QLatin1String("ymag")).toDouble());
camera.znear = float(ortho.value(QLatin1String("znear")).toDouble());
camera.zfar = float(ortho.value(QLatin1String("zfar")).toDouble());
} else {
camera.type = Camera::Type::Perspective;
const QJsonObject perspective = object.value(QLatin1String("perspective")).toObject();
camera.aspectRatio = float(perspective.value(QLatin1String("aspectRatio")).toDouble(0.0));
camera.yfov = float(perspective.value(QLatin1String("yfov")).toDouble());
camera.znear = float(perspective.value(QLatin1String("znear")).toDouble());
camera.zfar = float(perspective.value(QLatin1String("zfar")).toDouble(0.0));
}
document->cameras.append(camera);
}
// KHR_lights_punctual (document level)
{
const QJsonValue lightsExt = document->rootExtensions.value(QLatin1String("KHR_lights_punctual"));
if (lightsExt.isObject()) {
const QJsonArray lights = lightsExt.toObject().value(QLatin1String("lights")).toArray();
for (const auto &value : lights) {
const QJsonObject object = value.toObject();
Light light;
light.name = object.value(QLatin1String("name")).toString();
const QString type = object.value(QLatin1String("type")).toString();
if (type == QLatin1String("point"))
light.type = Light::Type::Point;
else if (type == QLatin1String("spot"))
light.type = Light::Type::Spot;
else
light.type = Light::Type::Directional;
light.color = toVector3D(object.value(QLatin1String("color")).toArray(), light.color);
light.intensity = float(object.value(QLatin1String("intensity")).toDouble(1.0));
light.range = float(object.value(QLatin1String("range")).toDouble(0.0));
if (light.type == Light::Type::Spot) {
const QJsonObject spot = object.value(QLatin1String("spot")).toObject();
light.innerConeAngle = float(spot.value(QLatin1String("innerConeAngle")).toDouble(0.0));
light.outerConeAngle = float(spot.value(QLatin1String("outerConeAngle")).toDouble(M_PI_4));
}
document->lights.append(light);
}
}
}
// nodes
const QJsonArray nodes = root.value(QLatin1String("nodes")).toArray();
for (const auto &value : nodes) {
const QJsonObject object = value.toObject();
Node node;
node.name = object.value(QLatin1String("name")).toString();
const QJsonArray children = object.value(QLatin1String("children")).toArray();
for (const auto &child : children)
node.children.append(child.toInt(-1));
node.mesh = object.value(QLatin1String("mesh")).toInt(-1);
node.skin = object.value(QLatin1String("skin")).toInt(-1);
node.camera = object.value(QLatin1String("camera")).toInt(-1);
const QJsonValue matrixValue = object.value(QLatin1String("matrix"));
if (matrixValue.isArray()) {
const QJsonArray m = matrixValue.toArray();
if (m.size() == 16) {
float values[16];
for (int i = 0; i < 16; ++i)
values[i] = float(m.at(i).toDouble());
// glTF matrices are column-major; QMatrix4x4(float*) is row-major
node.matrix = QMatrix4x4(values).transposed();
node.hasMatrix = true;
}
}
node.translation = toVector3D(object.value(QLatin1String("translation")).toArray(), node.translation);
const QJsonArray rotation = object.value(QLatin1String("rotation")).toArray();
if (rotation.size() == 4) {
// glTF quaternions are (x, y, z, w)
node.rotation = QQuaternion(float(rotation.at(3).toDouble()), float(rotation.at(0).toDouble()),
float(rotation.at(1).toDouble()), float(rotation.at(2).toDouble()));
}
node.scale = toVector3D(object.value(QLatin1String("scale")).toArray(), node.scale);
const QJsonArray weights = object.value(QLatin1String("weights")).toArray();
for (const auto &w : weights)
node.weights.append(float(w.toDouble()));
node.extensions = object.value(QLatin1String("extensions")).toObject();
const QJsonValue lightExt = node.extensions.value(QLatin1String("KHR_lights_punctual"));
if (lightExt.isObject())
node.light = lightExt.toObject().value(QLatin1String("light")).toInt(-1);
const QJsonValue visibilityExt = node.extensions.value(QLatin1String("KHR_node_visibility"));
if (visibilityExt.isObject())
node.visible = visibilityExt.toObject().value(QLatin1String("visible")).toBool(true);
const QJsonValue instancingExt = node.extensions.value(QLatin1String("EXT_mesh_gpu_instancing"));
if (instancingExt.isObject()) {
const QJsonObject instanceAttributes =
instancingExt.toObject().value(QLatin1String("attributes")).toObject();
for (auto it = instanceAttributes.constBegin(); it != instanceAttributes.constEnd(); ++it) {
const int accessor = it.value().toInt(-1);
if (accessor < 0 || accessor >= document->accessors.size()) {
return setError(QStringLiteral("Node %1 instance attribute %2 references invalid accessor %3")
.arg(document->nodes.size()).arg(it.key()).arg(accessor));
}
if (it.key() == QLatin1String("TRANSLATION"))
node.instanceTranslation = accessor;
else if (it.key() == QLatin1String("ROTATION"))
node.instanceRotation = accessor;
else if (it.key() == QLatin1String("SCALE"))
node.instanceScale = accessor;
else
qCWarning(lcQuick3DGltf) << "Ignoring unsupported instance attribute" << it.key();
}
node.hasInstancing = node.instanceTranslation >= 0 || node.instanceRotation >= 0
|| node.instanceScale >= 0;
}
if (node.mesh >= document->meshes.size())
return setError(QStringLiteral("Node %1 references invalid mesh %2")
.arg(document->nodes.size()).arg(node.mesh));
if (node.camera >= document->cameras.size())
return setError(QStringLiteral("Node %1 references invalid camera %2")
.arg(document->nodes.size()).arg(node.camera));
if (node.light >= document->lights.size())
return setError(QStringLiteral("Node %1 references invalid light %2")
.arg(document->nodes.size()).arg(node.light));
document->nodes.append(node);
}
// Validate node graph references (children and skins can be forward
// references). The specification requires the hierarchy to be a forest,
// and consumers that recurse over the graph rely on that to be safe from
// cycles and exponential blowup, so the shape is repaired here rather than
// left for them to cope with.
const int nodeCount = document->nodes.size();
QVarLengthArray<int, 64> parentCount(nodeCount);
std::fill(parentCount.begin(), parentCount.end(), 0);
for (int i = 0; i < nodeCount; ++i) {
Node &node = document->nodes[i];
for (qsizetype c = 0; c < node.children.size(); ++c) {
const int child = node.children.at(c);
if (child < 0 || child >= nodeCount)
return setError(QStringLiteral("Node %1 references invalid child node %2").arg(i).arg(child));
if (++parentCount[child] > 1) {
// A second parent cannot be represented at all: a node
// occupies exactly one place in a scene graph. Keep the first
// edge and drop this one, so the rest of the asset still
// loads.
qCWarning(lcQuick3DGltf) << "Node" << child
<< "has more than one parent; ignoring the edge from node" << i;
--parentCount[child];
node.children.removeAt(c--);
}
}
}
// With single parents the only remaining hazards are parentless cycles
// and excessive depth; both are caught by walking up each node's parent
// chain, memoizing the depths so every edge is visited only once.
{
constexpr int maxNodeDepth = 4096;
QVarLengthArray<int, 64> parent(nodeCount);
std::fill(parent.begin(), parent.end(), -1);
for (int i = 0; i < nodeCount; ++i) {
for (int child : document->nodes.at(i).children)
parent[child] = i;
}
QVarLengthArray<int, 64> depth(nodeCount);
std::fill(depth.begin(), depth.end(), -1);
QVarLengthArray<int, 64> chain;
for (int i = 0; i < nodeCount; ++i) {
chain.clear();
int n = i;
while (n >= 0 && depth[n] < 0 && chain.size() <= nodeCount) {
chain.append(n);
n = parent[n];
}
if (chain.size() > nodeCount)
return setError(QStringLiteral("Node hierarchy contains a cycle"));
int d = n >= 0 ? depth[n] : 0;
for (auto it = chain.rbegin(); it != chain.rend(); ++it)
depth[*it] = ++d;
if (d > maxNodeDepth)
return setError(QStringLiteral("Node hierarchy is deeper than %1").arg(maxNodeDepth));
}
}
// skins
const QJsonArray skins = root.value(QLatin1String("skins")).toArray();
for (const auto &value : skins) {
const QJsonObject object = value.toObject();
Skin skin;
skin.name = object.value(QLatin1String("name")).toString();
skin.inverseBindMatrices = object.value(QLatin1String("inverseBindMatrices")).toInt(-1);
skin.skeleton = object.value(QLatin1String("skeleton")).toInt(-1);
const QJsonArray joints = object.value(QLatin1String("joints")).toArray();
for (const auto &joint : joints)
skin.joints.append(joint.toInt(-1));
if (skin.inverseBindMatrices >= document->accessors.size())
return setError(QStringLiteral("Skin %1 references invalid accessor %2")
.arg(document->skins.size()).arg(skin.inverseBindMatrices));
for (int joint : std::as_const(skin.joints)) {
if (joint < 0 || joint >= nodeCount)
return setError(QStringLiteral("Skin %1 references invalid joint node %2")
.arg(document->skins.size()).arg(joint));
}
document->skins.append(skin);
}
for (int i = 0; i < nodeCount; ++i) {
if (document->nodes.at(i).skin >= document->skins.size())
return setError(QStringLiteral("Node %1 references invalid skin %2")
.arg(i).arg(document->nodes.at(i).skin));
}
// animations
const QJsonArray animations = root.value(QLatin1String("animations")).toArray();
for (const auto &value : animations) {
const QJsonObject object = value.toObject();
Animation animation;
animation.name = object.value(QLatin1String("name")).toString();
const QJsonArray animationSamplers = object.value(QLatin1String("samplers")).toArray();
for (const auto &samplerValue : animationSamplers) {
const QJsonObject samplerObject = samplerValue.toObject();
AnimationSampler sampler;
sampler.input = samplerObject.value(QLatin1String("input")).toInt(-1);
sampler.output = samplerObject.value(QLatin1String("output")).toInt(-1);
const QString interpolation = samplerObject.value(QLatin1String("interpolation")).toString();
if (interpolation == QLatin1String("STEP"))
sampler.interpolation = AnimationSampler::Interpolation::Step;
else if (interpolation == QLatin1String("CUBICSPLINE"))
sampler.interpolation = AnimationSampler::Interpolation::CubicSpline;
if (sampler.input < 0 || sampler.input >= document->accessors.size()
|| sampler.output < 0 || sampler.output >= document->accessors.size()) {
return setError(
QStringLiteral("Animation %1 sampler references an invalid accessor")
.arg(document->animations.size()));
}
animation.samplers.append(sampler);
}
const QJsonArray channels = object.value(QLatin1String("channels")).toArray();
for (const auto &channelValue : channels) {
const QJsonObject channelObject = channelValue.toObject();
AnimationChannel channel;
channel.sampler = channelObject.value(QLatin1String("sampler")).toInt(-1);
const QJsonObject target = channelObject.value(QLatin1String("target")).toObject();
channel.targetNode = target.value(QLatin1String("node")).toInt(-1);
const QString path = target.value(QLatin1String("path")).toString();
if (path == QLatin1String("rotation"))
channel.path = AnimationChannel::Path::Rotation;
else if (path == QLatin1String("scale"))
channel.path = AnimationChannel::Path::Scale;
else if (path == QLatin1String("weights"))
channel.path = AnimationChannel::Path::Weights;
else if (path != QLatin1String("translation")) {
qCWarning(lcQuick3DGltf) << "Ignoring animation channel with unsupported path" << path;
continue;
}
if (channel.sampler < 0 || channel.sampler >= animation.samplers.size())
return setError(QStringLiteral("Animation %1 channel references invalid sampler %2")
.arg(document->animations.size())
.arg(channel.sampler));
if (channel.targetNode >= nodeCount)
return setError(QStringLiteral("Animation %1 channel references invalid node %2")
.arg(document->animations.size())
.arg(channel.targetNode));
animation.channels.append(channel);
}
document->animations.append(animation);
}
// scenes
const QJsonArray scenes = root.value(QLatin1String("scenes")).toArray();
for (const auto &value : scenes) {
const QJsonObject object = value.toObject();
Scene scene;
scene.name = object.value(QLatin1String("name")).toString();
QSet<int> seenRoots;
const QJsonArray sceneNodes = object.value(QLatin1String("nodes")).toArray();
for (const auto &nodeIndex : sceneNodes) {
const int index = nodeIndex.toInt(-1);
if (index < 0 || index >= nodeCount)
return setError(QStringLiteral("Scene %1 references invalid node %2")
.arg(document->scenes.size()).arg(index));
// Scene nodes must be distinct root nodes; a repeated node, or one
// that is also somebody's child, would have its subtree emitted
// more than once. Skip the offending entry rather than failing, so
// that the scene still contains everything it should, once.
if (parentCount[index] != 0 || seenRoots.contains(index)) {
qCWarning(lcQuick3DGltf) << "Scene" << document->scenes.size() << "references node" << index
<< "which is not a unique root node; ignoring it";
continue;
}
seenRoots.insert(index);
scene.nodes.append(index);
}
document->scenes.append(scene);
}
document->scene = root.value(QLatin1String("scene")).toInt(-1);
if (document->scene >= document->scenes.size())
return setError(QStringLiteral("Document references invalid default scene %1").arg(document->scene));
return true;
}
QT_END_NAMESPACE