summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorSean Harmer <sean.harmer@kdab.com>2017-09-29 16:16:57 +0100
committerSean Harmer <sean.harmer@kdab.com>2017-10-06 10:27:38 +0000
commit18e7d2ed8176697933c08e974821311aa26a0ffa (patch)
treed6304786c08bcafb7b29fc761afda9f75197142f
parentbff7d2aaa74cb6d576e56b44617949f29c36cead (diff)
Support loading animation data from glTF 2 files
So far just skeletal animations. Morph targets will come in Qt 5.11. Change-Id: Ib1f3efe894ad993b97f79e7f71d92cb4836e4e32 Reviewed-by: Paul Lemire <paul.lemire@kdab.com>
-rw-r--r--src/animation/backend/animationclip.cpp58
-rw-r--r--src/animation/backend/backend.pri6
-rw-r--r--src/animation/backend/gltfimporter.cpp852
-rw-r--r--src/animation/backend/gltfimporter_p.h232
4 files changed, 1125 insertions, 23 deletions
diff --git a/src/animation/backend/animationclip.cpp b/src/animation/backend/animationclip.cpp
index daee4008d..e8f8f4552 100644
--- a/src/animation/backend/animationclip.cpp
+++ b/src/animation/backend/animationclip.cpp
@@ -41,6 +41,7 @@
#include <Qt3DAnimation/private/qanimationcliploader_p.h>
#include <Qt3DAnimation/private/animationlogging_p.h>
#include <Qt3DAnimation/private/managers_p.h>
+#include <Qt3DAnimation/private/gltfimporter_p.h>
#include <Qt3DRender/private/qurlhelper_p.h>
#include <Qt3DCore/qpropertyupdatedchange.h>
@@ -209,28 +210,43 @@ void AnimationClip::loadAnimationFromUrl()
return;
}
- QByteArray animationData = file.readAll();
- QJsonDocument document = QJsonDocument::fromJson(animationData);
- QJsonObject rootObject = document.object();
-
- // TODO: Allow loading of a named animation from a file containing many
- QJsonArray animationsArray = rootObject[QLatin1String("animations")].toArray();
- qCDebug(Jobs) << "Found" << animationsArray.size() << "animations:";
- for (int i = 0; i < animationsArray.size(); ++i) {
- QJsonObject animation = animationsArray.at(i).toObject();
- qCDebug(Jobs) << "Animation Name:" << animation[QLatin1String("animationName")].toString();
- }
+ // TODO: Convert to plugins
+ // Load glTF or "native"
+ if (filePath.endsWith(QLatin1String("gltf"))) {
+ qCDebug(Jobs) << "Loading glTF animation from" << filePath;
+ GLTFImporter gltf;
+ gltf.load(&file);
+ // TODO: Allow loading of a named animation from a file containing many
+ m_name = gltf.animations().first().name;
+ m_channels = gltf.createAnimationData();
+ } else if (filePath.endsWith(QLatin1String("json"))) {
+ // Native format
+ QByteArray animationData = file.readAll();
+ QJsonDocument document = QJsonDocument::fromJson(animationData);
+ QJsonObject rootObject = document.object();
+
+ // TODO: Allow loading of a named animation from a file containing many
+ QJsonArray animationsArray = rootObject[QLatin1String("animations")].toArray();
+ qCDebug(Jobs) << "Found" << animationsArray.size() << "animations:";
+ for (int i = 0; i < animationsArray.size(); ++i) {
+ QJsonObject animation = animationsArray.at(i).toObject();
+ qCDebug(Jobs) << "Animation Name:" << animation[QLatin1String("animationName")].toString();
+ }
- // For now just load the first animation
- // TODO: Allow loading a named animation from within the file analogous to QMesh
- QJsonObject animation = animationsArray.at(0).toObject();
- m_name = animation[QLatin1String("animationName")].toString();
- QJsonArray channelsArray = animation[QLatin1String("channels")].toArray();
- const int channelCount = channelsArray.size();
- m_channels.resize(channelCount);
- for (int i = 0; i < channelCount; ++i) {
- const QJsonObject group = channelsArray.at(i).toObject();
- m_channels[i].read(group);
+ // For now just load the first animation
+ // TODO: Allow loading a named animation from within the file analogous to QMesh
+ QJsonObject animation = animationsArray.at(0).toObject();
+ m_name = animation[QLatin1String("animationName")].toString();
+ QJsonArray channelsArray = animation[QLatin1String("channels")].toArray();
+ const int channelCount = channelsArray.size();
+ m_channels.resize(channelCount);
+ for (int i = 0; i < channelCount; ++i) {
+ const QJsonObject group = channelsArray.at(i).toObject();
+ m_channels[i].read(group);
+ }
+ } else {
+ qWarning() << "Unknown animation clip type. Please use json or glTF 2.0";
+ setStatus(QAnimationClipLoader::Error);
}
}
diff --git a/src/animation/backend/backend.pri b/src/animation/backend/backend.pri
index ee36a98ea..cc1104102 100644
--- a/src/animation/backend/backend.pri
+++ b/src/animation/backend/backend.pri
@@ -28,7 +28,8 @@ HEADERS += \
$$PWD/clipblendvalue_p.h \
$$PWD/animationclip_p.h \
$$PWD/clock_p.h \
- $$PWD/skeleton_p.h
+ $$PWD/skeleton_p.h \
+ $$PWD/gltfimporter_p.h
SOURCES += \
$$PWD/handler.cpp \
@@ -54,4 +55,5 @@ SOURCES += \
$$PWD/clipblendvalue.cpp \
$$PWD/animationclip.cpp \
$$PWD/clock.cpp \
- $$PWD/skeleton.cpp
+ $$PWD/skeleton.cpp \
+ $$PWD/gltfimporter.cpp
diff --git a/src/animation/backend/gltfimporter.cpp b/src/animation/backend/gltfimporter.cpp
new file mode 100644
index 000000000..8d9b5bb60
--- /dev/null
+++ b/src/animation/backend/gltfimporter.cpp
@@ -0,0 +1,852 @@
+/****************************************************************************
+**
+** Copyright (C) 2017 Klaralvdalens Datakonsult AB (KDAB).
+** Contact: http://www.qt-project.org/legal
+**
+** This file is part of the Qt3D module of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:LGPL3$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see http://www.qt.io/terms-conditions. For further
+** information use the contact form at http://www.qt.io/contact-us.
+**
+** GNU Lesser General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU Lesser
+** General Public License version 3 as published by the Free Software
+** Foundation and appearing in the file LICENSE.LGPLv3 included in the
+** packaging of this file. Please review the following information to
+** ensure the GNU Lesser General Public License version 3 requirements
+** will be met: https://www.gnu.org/licenses/lgpl.html.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 2.0 or later as published by the Free
+** Software Foundation and appearing in the file LICENSE.GPL included in
+** the packaging of this file. Please review the following information to
+** ensure the GNU General Public License version 2.0 requirements will be
+** met: http://www.gnu.org/licenses/gpl-2.0.html.
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+#include "gltfimporter_p.h"
+#include <Qt3DAnimation/private/animationlogging_p.h>
+#include <Qt3DAnimation/private/fcurve_p.h>
+#include <Qt3DAnimation/private/keyframe_p.h>
+
+#include <QtGui/qopengl.h>
+#include <QtGui/qquaternion.h>
+#include <QtGui/qvector2d.h>
+#include <QtGui/qvector3d.h>
+#include <QtGui/qvector4d.h>
+#include <QtCore/qdir.h>
+#include <QtCore/qfile.h>
+#include <QtCore/qfileinfo.h>
+#include <QtCore/qiodevice.h>
+#include <QtCore/qversionnumber.h>
+
+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<float>(element.toDouble());
+
+ decomposeQMatrix4x4(m, sqt);
+}
+
+void jsonArrayToVector3D(const QJsonArray &jsonArray, QVector3D &v)
+{
+ Q_ASSERT(jsonArray.size() == 3);
+ v.setX(static_cast<float>(jsonArray.at(0).toDouble()));
+ v.setY(static_cast<float>(jsonArray.at(1).toDouble()));
+ v.setZ(static_cast<float>(jsonArray.at(2).toDouble()));
+}
+
+void jsonArrayToQuaternion(const QJsonArray &jsonArray, QQuaternion &q)
+{
+ Q_ASSERT(jsonArray.size() == 4);
+ q.setX(static_cast<float>(jsonArray.at(0).toDouble()));
+ q.setY(static_cast<float>(jsonArray.at(1).toDouble()));
+ q.setZ(static_cast<float>(jsonArray.at(2).toDouble()));
+ q.setScalar(static_cast<float>(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(Qt3DRender::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);
+
+ QVector3D s(1.0f, 1.0f, 1.0f);
+ if (!scaleValue.isUndefined())
+ jsonArrayToVector3D(scaleValue.toArray(), localTransform.scale);
+
+ QQuaternion r;
+ if (!rotationValue.isUndefined())
+ jsonArrayToQuaternion(json.value(KEY_ROTATION).toArray(), localTransform.rotation);
+
+ QVector3D t;
+ 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();
+}
+
+Qt3DRender::QAttribute::VertexBaseType GLTFImporter::accessorTypeFromJSON(int componentType)
+{
+ if (componentType == GL_BYTE)
+ return Qt3DRender::QAttribute::Byte;
+ else if (componentType == GL_UNSIGNED_BYTE)
+ return Qt3DRender::QAttribute::UnsignedByte;
+ else if (componentType == GL_SHORT)
+ return Qt3DRender::QAttribute::Short;
+ else if (componentType == GL_UNSIGNED_SHORT)
+ return Qt3DRender::QAttribute::UnsignedShort;
+ else if (componentType == GL_UNSIGNED_INT)
+ return Qt3DRender::QAttribute::UnsignedInt;
+ else if (componentType == GL_FLOAT)
+ return Qt3DRender::QAttribute::Float;
+
+ // There shouldn't be an invalid case here
+ qWarning("unsupported accessor type %d", componentType);
+ return Qt3DRender::QAttribute::Float;
+}
+
+uint GLTFImporter::accessorTypeSize(Qt3DRender::QAttribute::VertexBaseType componentType)
+{
+ switch (componentType) {
+ case Qt3DRender::QAttribute::Byte:
+ case Qt3DRender::QAttribute::UnsignedByte:
+ return 1;
+
+ case Qt3DRender::QAttribute::Short:
+ case Qt3DRender::QAttribute::UnsignedShort:
+ return 2;
+
+ case Qt3DRender::QAttribute::Int:
+ case Qt3DRender::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)
+{
+ QByteArray jsonData = ioDev->readAll();
+ QJsonDocument sceneDocument = QJsonDocument::fromBinaryData(jsonData);
+ if (sceneDocument.isNull())
+ sceneDocument = QJsonDocument::fromJson(jsonData);
+
+ if (Q_UNLIKELY(!setJSON(sceneDocument))) {
+ qWarning("not a JSON document");
+ return false;
+ }
+
+ auto file = qobject_cast<QFile*>(ioDev);
+ if (file) {
+ QFileInfo finfo(file->fileName());
+ setBasePath(finfo.dir().absolutePath());
+ }
+
+ return parse();
+}
+
+QHash<int, int> GLTFImporter::createNodeIndexToJointIndexMap(const Skin &skin) const
+{
+ const int jointCount = skin.jointNodeIndices.size();
+ QHash<int, int> nodeIndexToJointIndexMap;
+ nodeIndexToJointIndexMap.reserve(jointCount);
+ for (int i = 0; i < jointCount; ++i)
+ nodeIndexToJointIndexMap.insert(skin.jointNodeIndices[i], i);
+ return nodeIndexToJointIndexMap;
+}
+
+QVector<Qt3DAnimation::Animation::Channel> GLTFImporter::createAnimationData(const QString &animationName) const
+{
+ QVector<Qt3DAnimation::Animation::Channel> channels;
+ if (m_animations.isEmpty()) {
+ qCWarning(Jobs) << "File does not contain any animation data";
+ return channels;
+ }
+
+ int animationIndex = 0;
+ if (!animationName.isEmpty()) {
+ for (int i = 0; i < m_animations.size(); ++i) {
+ if (m_animations[i].name == animationName) {
+ animationIndex = i;
+ break;
+ }
+ }
+ }
+ const Animation &animation = m_animations[animationIndex];
+
+ // Create node index to joint index lookup tables for each skin
+ QVector<QHash<int, int>> nodeIndexToJointIndexMaps;
+ nodeIndexToJointIndexMaps.reserve(m_skins.size());
+ for (const auto &skin : m_skins)
+ nodeIndexToJointIndexMaps.push_back(createNodeIndexToJointIndexMap(skin));
+
+ int channelIndex = 0;
+ 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.
+ int 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" << endl;
+ continue;
+ }
+
+ const auto &inputAccessor = m_accessors[sampler.inputAccessorIndex];
+ const auto &outputAccessor = m_accessors[sampler.outputAccessorIndex];
+
+ if (inputAccessor.type != Qt3DRender::QAttribute::Float) {
+ qWarning() << "Input accessor has wrong data type. Skipping channel.";
+ continue;
+ }
+
+ if (outputAccessor.type != Qt3DRender::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<float> keyframeTimes(keyFrameCount);
+ for (int i = 0; i < keyFrameCount; ++i) {
+ const auto rawTimestamp = accessorData(sampler.inputAccessorIndex, i);
+ keyframeTimes[i] = *reinterpret_cast<const float*>(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
+ }
+
+ channels.push_back(outputChannel);
+ ++channelIndex;
+ }
+
+ return channels;
+}
+
+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 int 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)
+{
+ 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 int nodeCount = m_nodes.size();
+ for (int i = 0; i < nodeCount; ++i) {
+ const Node &node = m_nodes[i];
+ const QVector<int> &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
diff --git a/src/animation/backend/gltfimporter_p.h b/src/animation/backend/gltfimporter_p.h
new file mode 100644
index 000000000..ae7674fe7
--- /dev/null
+++ b/src/animation/backend/gltfimporter_p.h
@@ -0,0 +1,232 @@
+/****************************************************************************
+**
+** Copyright (C) 2017 Klaralvdalens Datakonsult AB (KDAB).
+** Contact: http://www.qt-project.org/legal
+**
+** This file is part of the Qt3D module of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:LGPL3$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see http://www.qt.io/terms-conditions. For further
+** information use the contact form at http://www.qt.io/contact-us.
+**
+** GNU Lesser General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU Lesser
+** General Public License version 3 as published by the Free Software
+** Foundation and appearing in the file LICENSE.LGPLv3 included in the
+** packaging of this file. Please review the following information to
+** ensure the GNU Lesser General Public License version 3 requirements
+** will be met: https://www.gnu.org/licenses/lgpl.html.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 2.0 or later as published by the Free
+** Software Foundation and appearing in the file LICENSE.GPL included in
+** the packaging of this file. Please review the following information to
+** ensure the GNU General Public License version 2.0 requirements will be
+** met: http://www.gnu.org/licenses/gpl-2.0.html.
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+#ifndef QT3DANIMATION_ANIMATION_GLTFIMPORTER_H
+#define QT3DANIMATION_ANIMATION_GLTFIMPORTER_H
+
+//
+// W A R N I N G
+// -------------
+//
+// This file is not part of the Qt API. It exists for the convenience
+// of other Qt classes. This header file may change from version to
+// version without notice, or even be removed.
+//
+// We mean it.
+//
+
+#include <QtGlobal>
+#include <Qt3DAnimation/private/fcurve_p.h>
+#include <Qt3DRender/qattribute.h>
+#include <Qt3DCore/private/sqt_p.h>
+#include <Qt3DCore/private/qmath3d_p.h>
+
+#include <QJsonDocument>
+#include <QJsonObject>
+#include <QJsonArray>
+#include <QJsonValue>
+#include <QVector>
+
+QT_BEGIN_NAMESPACE
+
+class QIODevice;
+
+namespace Qt3DAnimation {
+namespace Animation {
+
+class GLTFImporter
+{
+public:
+ class BufferData
+ {
+ public:
+ BufferData();
+ explicit BufferData(const QJsonObject &json);
+
+ quint64 byteLength;
+ QString path;
+ QByteArray data;
+ };
+
+ class BufferView
+ {
+ public:
+ BufferView();
+ explicit BufferView(const QJsonObject &json);
+
+ quint64 byteOffset;
+ quint64 byteLength;
+ int bufferIndex;
+ int target; // Only for per vertex attributes
+ };
+
+ class AccessorData
+ {
+ public:
+ AccessorData();
+ explicit AccessorData(const QJsonObject &json);
+
+ int bufferViewIndex;
+ Qt3DRender::QAttribute::VertexBaseType type;
+ uint dataSize;
+ int count;
+ int byteOffset;
+ int byteStride; // Only for per vertex attributes
+
+ // TODO: Extend to support sparse accessors
+ };
+
+ class Skin
+ {
+ public:
+ Skin();
+ explicit Skin(const QJsonObject &json);
+
+ QString name;
+ int inverseBindAccessorIndex;
+ QVector<int> jointNodeIndices;
+ };
+
+ class Channel
+ {
+ public:
+ Channel();
+ explicit Channel(const QJsonObject &json);
+
+ int samplerIndex;
+ int targetNodeIndex;
+ QString targetProperty;
+ };
+
+ class Sampler
+ {
+ public:
+ Sampler();
+ explicit Sampler(const QJsonObject &json);
+
+ enum InterpolationMode {
+ Linear,
+ Step,
+ CatmullRomSpline,
+ CubicSpline
+ };
+
+ QString interpolationModeString() const;
+
+ int inputAccessorIndex;
+ int outputAccessorIndex;
+ InterpolationMode interpolationMode;
+ };
+
+ class Animation
+ {
+ public:
+ Animation();
+ explicit Animation(const QJsonObject &json);
+
+ QString name;
+ QVector<Channel> channels;
+ QVector<Sampler> samplers;
+ };
+
+ class Node
+ {
+ public:
+ Node();
+ explicit Node(const QJsonObject &json);
+
+ Qt3DCore::Sqt localTransform;
+ QVector<int> childNodeIndices;
+ QString name;
+ int parentNodeIndex;
+ int cameraIndex;
+ int meshIndex;
+ int skinIndex;
+ };
+
+ GLTFImporter();
+
+ bool load(QIODevice *ioDev);
+ const QVector<Animation> animations() const { return m_animations; }
+
+ QVector<Qt3DAnimation::Animation::Channel> createAnimationData(const QString &animationName = QString()) const;
+
+private:
+ static Qt3DRender::QAttribute::VertexBaseType accessorTypeFromJSON(int componentType);
+ static uint accessorTypeSize(Qt3DRender::QAttribute::VertexBaseType componentType);
+ static uint accessorDataSizeFromJson(const QString &type);
+
+ struct RawData
+ {
+ const char *data;
+ quint64 byteLength;
+ };
+
+ void setBasePath(const QString &path);
+ bool setJSON(const QJsonDocument &json);
+
+ bool parse();
+ bool parseGLTF2();
+ void cleanup();
+ QHash<int, int> createNodeIndexToJointIndexMap(const Skin &skin) const;
+
+ bool processJSONBuffer(const QJsonObject &json);
+ bool processJSONBufferView(const QJsonObject &json);
+ bool processJSONAccessor(const QJsonObject &json);
+ bool processJSONSkin(const QJsonObject &json);
+ bool processJSONAnimation(const QJsonObject &json);
+ bool processJSONNode(const QJsonObject &json);
+ void setupNodeParentLinks();
+ QByteArray resolveLocalData(const QString &path) const;
+
+ RawData accessorData(int accessorIndex, int index) const;
+
+ QJsonDocument m_json;
+ QString m_basePath;
+ QVector<BufferData> m_bufferDatas;
+ QVector<BufferView> m_bufferViews;
+ QVector<AccessorData> m_accessors;
+ QVector<Skin> m_skins;
+ QVector<Animation> m_animations;
+ QVector<Node> m_nodes;
+};
+
+} // namespace Animation
+} // namespace Qt3DAnimation
+
+QT_END_NAMESPACE
+
+#endif // QT3DANIMATION_ANIMATION_GLTFIMPORTER_H