// Copyright (C) 2017 Klaralvdalens Datakonsult AB (KDAB). // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only #include "gltfimporter_p.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include QT_BEGIN_NAMESPACE namespace Qt3DAnimation { namespace Animation { namespace { QString gltfTargetPropertyToChannelName(const QString &propertyName) { if (propertyName == QLatin1String("rotation")) return QLatin1String("Rotation"); else if (propertyName == QLatin1String("translation")) return QLatin1String("Location"); else if (propertyName == QLatin1String("scale")) return QLatin1String("Scale"); qWarning() << "Unknown target property name"; return QString(); } QKeyFrame::InterpolationType gltfToQKeyFrameInterpolation(GLTFImporter::Sampler::InterpolationMode mode) { switch (mode) { case GLTFImporter::Sampler::Linear: return QKeyFrame::LinearInterpolation; case GLTFImporter::Sampler::Step: return QKeyFrame::ConstantInterpolation; case GLTFImporter::Sampler::CubicSpline: return QKeyFrame::BezierInterpolation; case GLTFImporter::Sampler::CatmullRomSpline: // TODO: Implement this interpolation type qWarning() << "Unhandled interpolation type"; return QKeyFrame::LinearInterpolation; } return QKeyFrame::LinearInterpolation; } void jsonArrayToSqt(const QJsonArray &jsonArray, Qt3DCore::Sqt &sqt) { Q_ASSERT(jsonArray.size() == 16); QMatrix4x4 m; float *data = m.data(); int i = 0; for (const auto element : jsonArray) *(data + i++) = static_cast(element.toDouble()); decomposeQMatrix4x4(m, sqt); } void jsonArrayToVector3D(const QJsonArray &jsonArray, QVector3D &v) { Q_ASSERT(jsonArray.size() == 3); v.setX(static_cast(jsonArray.at(0).toDouble())); v.setY(static_cast(jsonArray.at(1).toDouble())); v.setZ(static_cast(jsonArray.at(2).toDouble())); } void jsonArrayToQuaternion(const QJsonArray &jsonArray, QQuaternion &q) { Q_ASSERT(jsonArray.size() == 4); q.setX(static_cast(jsonArray.at(0).toDouble())); q.setY(static_cast(jsonArray.at(1).toDouble())); q.setZ(static_cast(jsonArray.at(2).toDouble())); q.setScalar(static_cast(jsonArray.at(3).toDouble())); } } #define KEY_ACCESSORS QLatin1String("accessors") #define KEY_ANIMATIONS QLatin1String("animations") #define KEY_ASSET QLatin1String("asset") #define KEY_BUFFER QLatin1String("buffer") #define KEY_BUFFERS QLatin1String("buffers") #define KEY_BUFFER_VIEW QLatin1String("bufferView") #define KEY_BUFFER_VIEWS QLatin1String("bufferViews") #define KEY_BYTE_LENGTH QLatin1String("byteLength") #define KEY_BYTE_OFFSET QLatin1String("byteOffset") #define KEY_BYTE_STRIDE QLatin1String("byteStride") #define KEY_CAMERA QLatin1String("camera") #define KEY_CHANNELS QLatin1String("channels") #define KEY_CHILDREN QLatin1String("children") #define KEY_COMPONENT_TYPE QLatin1String("componentType") #define KEY_COUNT QLatin1String("count") #define KEY_JOINTS QLatin1String("joints") #define KEY_INPUT QLatin1String("input") #define KEY_INTERPOLATION QLatin1String("interpolation") #define KEY_INVERSE_BIND_MATRICES QLatin1String("inverseBindMatrices") #define KEY_MATRIX QLatin1String("matrix") #define KEY_MESH QLatin1String("mesh") #define KEY_NAME QLatin1String("name") #define KEY_NODE QLatin1String("node") #define KEY_NODES QLatin1String("nodes") #define KEY_OUTPUT QLatin1String("output") #define KEY_PATH QLatin1String("path") #define KEY_ROTATION QLatin1String("rotation") #define KEY_SAMPLER QLatin1String("sampler") #define KEY_SAMPLERS QLatin1String("samplers") #define KEY_SCALE QLatin1String("scale") #define KEY_SKIN QLatin1String("skin") #define KEY_SKINS QLatin1String("skins") #define KEY_TARGET QLatin1String("target") #define KEY_TRANSLATION QLatin1String("translation") #define KEY_TYPE QLatin1String("type") #define KEY_URI QLatin1String("uri") #define KEY_VERSION QLatin1String("version") GLTFImporter::BufferData::BufferData() : byteLength(0) , data() { } GLTFImporter::BufferData::BufferData(const QJsonObject &json) : byteLength(json.value(KEY_BYTE_LENGTH).toInt()) , path(json.value(KEY_URI).toString()) , data() { } GLTFImporter::BufferView::BufferView() : byteOffset(0) , byteLength(0) , bufferIndex(-1) , target(0) { } GLTFImporter::BufferView::BufferView(const QJsonObject &json) : byteOffset(json.value(KEY_BYTE_OFFSET).toInt()) , byteLength(json.value(KEY_BYTE_LENGTH).toInt()) , bufferIndex(json.value(KEY_BUFFER).toInt()) , target(0) { const auto targetValue = json.value(KEY_TARGET); if (!targetValue.isUndefined()) target = targetValue.toInt(); } GLTFImporter::AccessorData::AccessorData() : type(Qt3DCore::QAttribute::Float) , dataSize(0) , count(0) , byteOffset(0) , byteStride(0) { } GLTFImporter::AccessorData::AccessorData(const QJsonObject &json) : bufferViewIndex(json.value(KEY_BUFFER_VIEW).toInt(-1)) , type(accessorTypeFromJSON(json.value(KEY_COMPONENT_TYPE).toInt())) , dataSize(accessorDataSizeFromJson(json.value(KEY_TYPE).toString())) , count(json.value(KEY_COUNT).toInt()) , byteOffset(0) , byteStride(0) { const auto byteOffsetValue = json.value(KEY_BYTE_OFFSET); if (!byteOffsetValue.isUndefined()) byteOffset = byteOffsetValue.toInt(); const auto byteStrideValue = json.value(KEY_BYTE_STRIDE); if (!byteStrideValue.isUndefined()) byteStride = byteStrideValue.toInt(); } GLTFImporter::Skin::Skin() : inverseBindAccessorIndex(-1) , jointNodeIndices() { } GLTFImporter::Skin::Skin(const QJsonObject &json) : name(json.value(KEY_NAME).toString()) , inverseBindAccessorIndex(json.value(KEY_INVERSE_BIND_MATRICES).toInt()) { QJsonArray jointNodes = json.value(KEY_JOINTS).toArray(); jointNodeIndices.reserve(jointNodes.size()); for (const auto jointNodeValue : jointNodes) jointNodeIndices.push_back(jointNodeValue.toInt()); } GLTFImporter::Channel::Channel() : samplerIndex(-1) , targetNodeIndex(-1) , targetProperty() { } GLTFImporter::Channel::Channel(const QJsonObject &json) : samplerIndex(json.value(KEY_SAMPLER).toInt()) , targetNodeIndex(-1) , targetProperty() { const auto targetJson = json.value(KEY_TARGET).toObject(); targetNodeIndex = targetJson.value(KEY_NODE).toInt(); targetProperty = targetJson.value(KEY_PATH).toString(); } GLTFImporter::Sampler::Sampler() : inputAccessorIndex(-1) , outputAccessorIndex(-1) , interpolationMode(Linear) { } GLTFImporter::Sampler::Sampler(const QJsonObject &json) : inputAccessorIndex(json.value(KEY_INPUT).toInt()) , outputAccessorIndex(json.value(KEY_OUTPUT).toInt()) , interpolationMode(Linear) { const auto interpolation = json.value(KEY_INTERPOLATION).toString(); if (interpolation == QLatin1String("LINEAR")) interpolationMode = Linear; else if (interpolation == QLatin1String("STEP")) interpolationMode = Step; else if (interpolation == QLatin1String("CATMULLROMSPLINE")) interpolationMode = CatmullRomSpline; else if (interpolation == QLatin1String("CUBICSPLINE")) interpolationMode = CubicSpline; } QString GLTFImporter::Sampler::interpolationModeString() const { switch (interpolationMode) { case Linear: return QLatin1String("LINEAR"); case Step: return QLatin1String("STEP"); case CatmullRomSpline: return QLatin1String("CATMULLROMSPLINE"); case CubicSpline: return QLatin1String("CUBICSPLINE"); } return QLatin1String("Unknown"); } GLTFImporter::Animation::Animation() : name() , channels() , samplers() { } GLTFImporter::Animation::Animation(const QJsonObject &json) : name(json.value(KEY_NAME).toString()) { QJsonArray channelsArray = json.value(KEY_CHANNELS).toArray(); channels.reserve(channelsArray.size()); for (const auto channelValue : channelsArray) { Channel channel(channelValue.toObject()); channels.push_back(channel); } QJsonArray samplersArray = json.value(KEY_SAMPLERS).toArray(); samplers.reserve(samplersArray.size()); for (const auto samplerValue : samplersArray) { Sampler sampler(samplerValue.toObject()); samplers.push_back(sampler); } } GLTFImporter::Node::Node() : localTransform() , childNodeIndices() , name() , parentNodeIndex(-1) , cameraIndex(-1) , meshIndex(-1) , skinIndex(-1) { } GLTFImporter::Node::Node(const QJsonObject &json) : localTransform() , childNodeIndices() , name(json.value(KEY_NAME).toString()) , parentNodeIndex(-1) , cameraIndex(-1) , meshIndex(-1) , skinIndex(-1) { // Child nodes - we setup the parent links in a later pass QJsonArray childNodes = json.value(KEY_CHILDREN).toArray(); childNodeIndices.reserve(childNodes.size()); for (const auto childNodeValue : childNodes) childNodeIndices.push_back(childNodeValue.toInt()); // Local transform - matrix or scale, rotation, translation const auto matrixValue = json.value(KEY_MATRIX); if (!matrixValue.isUndefined()) { jsonArrayToSqt(matrixValue.toArray(), localTransform); } else { const auto scaleValue = json.value(KEY_SCALE); const auto rotationValue = json.value(KEY_ROTATION); const auto translationValue = json.value(KEY_TRANSLATION); if (!scaleValue.isUndefined()) jsonArrayToVector3D(scaleValue.toArray(), localTransform.scale); if (!rotationValue.isUndefined()) jsonArrayToQuaternion(json.value(KEY_ROTATION).toArray(), localTransform.rotation); if (!translationValue.isUndefined()) jsonArrayToVector3D(json.value(KEY_TRANSLATION).toArray(), localTransform.translation); } // Referenced objects const auto cameraValue = json.value(KEY_CAMERA); if (!cameraValue.isUndefined()) cameraIndex = cameraValue.toInt(); const auto meshValue = json.value(KEY_MESH); if (!meshValue.isUndefined()) meshIndex = meshValue.toInt(); const auto skinValue = json.value(KEY_SKIN); if (!skinValue.isUndefined()) skinIndex = skinValue.toInt(); } Qt3DCore::QAttribute::VertexBaseType GLTFImporter::accessorTypeFromJSON(int componentType) { if (componentType == GL_BYTE) return Qt3DCore::QAttribute::Byte; else if (componentType == GL_UNSIGNED_BYTE) return Qt3DCore::QAttribute::UnsignedByte; else if (componentType == GL_SHORT) return Qt3DCore::QAttribute::Short; else if (componentType == GL_UNSIGNED_SHORT) return Qt3DCore::QAttribute::UnsignedShort; else if (componentType == GL_UNSIGNED_INT) return Qt3DCore::QAttribute::UnsignedInt; else if (componentType == GL_FLOAT) return Qt3DCore::QAttribute::Float; // There shouldn't be an invalid case here qWarning("unsupported accessor type %d", componentType); return Qt3DCore::QAttribute::Float; } uint GLTFImporter::accessorTypeSize(Qt3DCore::QAttribute::VertexBaseType componentType) { switch (componentType) { case Qt3DCore::QAttribute::Byte: case Qt3DCore::QAttribute::UnsignedByte: return 1; case Qt3DCore::QAttribute::Short: case Qt3DCore::QAttribute::UnsignedShort: return 2; case Qt3DCore::QAttribute::Int: case Qt3DCore::QAttribute::Float: return 4; default: qWarning("Unhandled accessor data type %d", componentType); return 0; } } uint GLTFImporter::accessorDataSizeFromJson(const QString &type) { QString typeName = type.toUpper(); if (typeName == QLatin1String("SCALAR")) return 1; if (typeName == QLatin1String("VEC2")) return 2; if (typeName == QLatin1String("VEC3")) return 3; if (typeName == QLatin1String("VEC4")) return 4; if (typeName == QLatin1String("MAT2")) return 4; if (typeName == QLatin1String("MAT3")) return 9; if (typeName == QLatin1String("MAT4")) return 16; return 0; } GLTFImporter::GLTFImporter() { } bool GLTFImporter::load(QIODevice *ioDev) { if (Q_UNLIKELY(!setJSON(qLoadGLTF(ioDev->readAll())))) { qWarning("not a JSON document"); return false; } auto file = qobject_cast(ioDev); if (file) { QFileInfo finfo(file->fileName()); setBasePath(finfo.dir().absolutePath()); } return parse(); } QHash GLTFImporter::createNodeIndexToJointIndexMap(const Skin &skin) const { const qsizetype jointCount = skin.jointNodeIndices.size(); QHash nodeIndexToJointIndexMap; nodeIndexToJointIndexMap.reserve(jointCount); for (qsizetype i = 0; i < jointCount; ++i) nodeIndexToJointIndexMap.insert(skin.jointNodeIndices[i], i); return nodeIndexToJointIndexMap; } GLTFImporter::AnimationNameAndChannels GLTFImporter::createAnimationData(qsizetype animationIndex, const QString &animationName) const { AnimationNameAndChannels nameAndChannels; if (m_animations.isEmpty()) { qCWarning(Jobs) << "File does not contain any animation data"; return nameAndChannels; } if (m_animations.size() == 1) { animationIndex = 0; } else if (animationIndex < 0 && !animationName.isEmpty()) { for (qsizetype i = 0; i < m_animations.size(); ++i) { if (m_animations[i].name == animationName) { animationIndex = i; break; } } } if (animationIndex < 0 || animationIndex >= m_animations.size()) { qCWarning(Jobs) << "Invalid animation index. Skipping."; return nameAndChannels; } const Animation &animation = m_animations[animationIndex]; nameAndChannels.name = animation.name; // Create node index to joint index lookup tables for each skin QList> nodeIndexToJointIndexMaps; nodeIndexToJointIndexMaps.reserve(m_skins.size()); for (const auto &skin : m_skins) nodeIndexToJointIndexMaps.push_back(createNodeIndexToJointIndexMap(skin)); for (const auto &channel : animation.channels) { Qt3DAnimation::Animation::Channel outputChannel; outputChannel.name = gltfTargetPropertyToChannelName(channel.targetProperty); // Find the node index to joint index map that contains the target node and // look up the joint index from it. If no such map is found, the target joint // is not part of a skeleton and so we can just set the jointIndex to -1. qsizetype jointIndex = -1; for (const auto &map : nodeIndexToJointIndexMaps) { const auto result = map.find(channel.targetNodeIndex); if (result != map.cend()) { jointIndex = result.value(); break; } } outputChannel.jointIndex = jointIndex; const auto &sampler = animation.samplers[channel.samplerIndex]; const auto interpolationType = gltfToQKeyFrameInterpolation(sampler.interpolationMode); if (sampler.inputAccessorIndex == -1 || sampler.outputAccessorIndex == -1) { qWarning() << "Skipping channel due to invalid accessor indices in the sampler" << Qt::endl; continue; } const auto &inputAccessor = m_accessors[sampler.inputAccessorIndex]; const auto &outputAccessor = m_accessors[sampler.outputAccessorIndex]; if (inputAccessor.type != Qt3DCore::QAttribute::Float) { qWarning() << "Input accessor has wrong data type. Skipping channel."; continue; } if (outputAccessor.type != Qt3DCore::QAttribute::Float) { qWarning() << "Output accessor has wrong data type. Skipping channel."; continue; } if (inputAccessor.count != outputAccessor.count) { qWarning() << "Warning!!! Input accessor has" << inputAccessor.count << "entries and output accessor has" << outputAccessor.count << "entries. They should match. Please check your data."; continue; } // TODO: Allow Qt 3D animation data to share timestamps between multiple // channel components. I.e. allow key frame values of composite types. // Doesn't give as much freedom but more efficient at runtime. // Get the key frame times first as these are common to all components of the // key frame values. const int keyFrameCount = inputAccessor.count; QVector keyframeTimes(keyFrameCount); for (int i = 0; i < keyFrameCount; ++i) { const auto rawTimestamp = accessorData(sampler.inputAccessorIndex, i); keyframeTimes[i] = *reinterpret_cast(rawTimestamp.data); } // Create a ChannelComponent for each component of the output sampler and // populate it with data. switch (outputAccessor.dataSize) { // TODO: Handle other types as needed case 3: { // vec3 const int componentCount = 3; // Construct the channel component names and add component to the channel const QStringList suffixes = (QStringList() << QLatin1String("X") << QLatin1String("Y") << QLatin1String("Z")); outputChannel.channelComponents.resize(componentCount); for (int componentIndex = 0; componentIndex < componentCount; ++componentIndex) { outputChannel.channelComponents[componentIndex].name = QString(QLatin1String("%1 %2")).arg(outputChannel.name, suffixes[componentIndex]); } // Populate the fcurves in the channel components for (int i = 0; i < keyFrameCount; ++i) { const auto rawKeyframeValue = accessorData(sampler.outputAccessorIndex, i); QVector3D v; memcpy(&v, rawKeyframeValue.data, rawKeyframeValue.byteLength); for (int componentIndex = 0; componentIndex < componentCount; ++componentIndex) { Keyframe keyFrame; keyFrame.interpolation = interpolationType; keyFrame.value = v[componentIndex]; outputChannel.channelComponents[componentIndex].fcurve.appendKeyframe(keyframeTimes[i], keyFrame); } } break; } // case 3 case 4: { // vec4 or quaternion const int componentCount = 4; // Construct the channel component names and add component to the channel const QStringList rotationSuffixes = (QStringList() << QLatin1String("X") << QLatin1String("Y") << QLatin1String("Z") << QLatin1String("W")); const QStringList standardSuffixes = (QStringList() << QLatin1String("X") << QLatin1String("Y") << QLatin1String("Z")); const QStringList suffixes = (channel.targetProperty == QLatin1String("rotation")) ? rotationSuffixes : standardSuffixes; outputChannel.channelComponents.resize(componentCount); for (int componentIndex = 0; componentIndex < componentCount; ++componentIndex) { outputChannel.channelComponents[componentIndex].name = QString(QLatin1String("%1 %2")).arg(outputChannel.name, suffixes[componentIndex]); } // Populate the fcurves in the channel components for (int i = 0; i < keyFrameCount; ++i) { const auto rawKeyframeValue = accessorData(sampler.outputAccessorIndex, i); QVector4D v; memcpy(&v, rawKeyframeValue.data, rawKeyframeValue.byteLength); for (int componentIndex = 0; componentIndex < componentCount; ++componentIndex) { Keyframe keyFrame; keyFrame.interpolation = interpolationType; keyFrame.value = v[componentIndex]; outputChannel.channelComponents[componentIndex].fcurve.appendKeyframe(keyframeTimes[i], keyFrame); } } break; } // case 4 } nameAndChannels.channels.push_back(outputChannel); } return nameAndChannels; } GLTFImporter::RawData GLTFImporter::accessorData(int accessorIndex, int index) const { const AccessorData &accessor = m_accessors[accessorIndex]; const BufferView &bufferView = m_bufferViews[accessor.bufferViewIndex]; const BufferData &bufferData = m_bufferDatas[bufferView.bufferIndex]; const QByteArray &ba = bufferData.data; const char *rawData = ba.constData() + bufferView.byteOffset + accessor.byteOffset; const uint typeSize = accessorTypeSize(accessor.type); const qsizetype stride = (accessor.byteStride == 0) ? accessor.dataSize * typeSize : accessor.byteStride; const char* data = rawData + index * stride; if (data - rawData > ba.size()) { qWarning("Attempting to access data beyond end of buffer"); return RawData{ nullptr, 0 }; } const quint64 byteLength = accessor.dataSize * typeSize; RawData rd{ data, byteLength }; return rd; } void GLTFImporter::setBasePath(const QString &path) { m_basePath = path; } bool GLTFImporter::setJSON(const QJsonDocument &json) { if (!json.isObject()) return false; m_json = json; cleanup(); return true; } bool GLTFImporter::parse() { // Find the glTF version const QJsonObject asset = m_json.object().value(KEY_ASSET).toObject(); const QString versionString = asset.value(KEY_VERSION).toString(); const auto version = QVersionNumber::fromString(versionString); switch (version.majorVersion()) { case 2: return parseGLTF2(); default: qWarning() << "Unsupported version of glTF" << versionString; return false; } } bool GLTFImporter::parseGLTF2() { bool success = true; const QJsonArray buffers = m_json.object().value(KEY_BUFFERS).toArray(); for (const auto bufferValue : buffers) success &= processJSONBuffer(bufferValue.toObject()); const QJsonArray bufferViews = m_json.object().value(KEY_BUFFER_VIEWS).toArray(); for (const auto bufferViewValue : bufferViews) success &= processJSONBufferView(bufferViewValue.toObject()); const QJsonArray accessors = m_json.object().value(KEY_ACCESSORS).toArray(); for (const auto accessorValue : accessors) success &= processJSONAccessor(accessorValue.toObject()); const QJsonArray skins = m_json.object().value(KEY_SKINS).toArray(); for (const auto skinValue : skins) success &= processJSONSkin(skinValue.toObject()); const QJsonArray animations = m_json.object().value(KEY_ANIMATIONS).toArray(); for (const auto animationValue : animations) success &= processJSONAnimation(animationValue.toObject()); const QJsonArray nodes = m_json.object().value(KEY_NODES).toArray(); for (const auto nodeValue : nodes) success &= processJSONNode(nodeValue.toObject()); setupNodeParentLinks(); // TODO: Make a complete GLTF 2 parser by extending to other top level elements: // scenes, animations, meshes etc. return success; } void GLTFImporter::cleanup() { m_accessors.clear(); m_bufferViews.clear(); m_bufferDatas.clear(); } bool GLTFImporter::processJSONBuffer(const QJsonObject &json) { // Store buffer details and load data into memory BufferData buffer(json); buffer.data = resolveLocalData(buffer.path); if (buffer.data.isEmpty()) return false; m_bufferDatas.push_back(buffer); return true; } bool GLTFImporter::processJSONBufferView(const QJsonObject &json) { BufferView bufferView(json); // Perform sanity checks const auto bufferIndex = bufferView.bufferIndex; if (Q_UNLIKELY(bufferIndex) >= m_bufferDatas.size()) { qWarning("Unknown buffer %d when processing buffer view", bufferIndex); return false; } const auto &bufferData = m_bufferDatas[bufferIndex]; if (bufferView.byteOffset > bufferData.byteLength) { qWarning("Bufferview has offset greater than buffer %d length", bufferIndex); return false; } if (Q_UNLIKELY(bufferView.byteOffset + bufferView.byteLength > bufferData.byteLength)) { qWarning("BufferView extends beyond end of buffer %d", bufferIndex); return false; } m_bufferViews.push_back(bufferView); return true; } bool GLTFImporter::processJSONAccessor(const QJsonObject &json) { AccessorData accessor(json); // TODO: Perform sanity checks m_accessors.push_back(accessor); return true; } bool GLTFImporter::processJSONSkin(const QJsonObject &json) { Skin skin(json); // TODO: Perform sanity checks m_skins.push_back(skin); return true; } bool GLTFImporter::processJSONAnimation(const QJsonObject &json) { const Animation animation(json); for (const auto &channel : animation.channels) { if (channel.samplerIndex == -1) qWarning() << "Invalid sampler index in animation" << animation.name << "for channel targeting node" << channel.targetNodeIndex << " and property" << channel.targetProperty; } for (const auto &sampler : animation.samplers) { if (sampler.inputAccessorIndex == -1) { qWarning() << "Sampler for animaton" << animation.name << "references has an invalid input accessor index"; } if (sampler.outputAccessorIndex == -1) { qWarning() << "Sampler for animaton" << animation.name << "references has an invalid output accessor index"; } } m_animations.push_back(animation); return true; } bool GLTFImporter::processJSONNode(const QJsonObject &json) { Node node(json); // TODO: Perform sanity checks m_nodes.push_back(node); return true; } void GLTFImporter::setupNodeParentLinks() { const qsizetype nodeCount = m_nodes.size(); for (qsizetype i = 0; i < nodeCount; ++i) { const Node &node = m_nodes[i]; const QList &childNodeIndices = node.childNodeIndices; for (const auto childNodeIndex : childNodeIndices) { Q_ASSERT(childNodeIndex < m_nodes.size()); Node &childNode = m_nodes[childNodeIndex]; Q_ASSERT(childNode.parentNodeIndex == -1); childNode.parentNodeIndex = i; } } } QByteArray GLTFImporter::resolveLocalData(const QString &path) const { QDir d(m_basePath); Q_ASSERT(d.exists()); QString absPath = d.absoluteFilePath(path); QFile f(absPath); f.open(QIODevice::ReadOnly); return f.readAll(); } } // namespace Animation } // namespace Qt3DAnimation QT_END_NAMESPACE