From 8615223eebc78ea602217064474de51584f01a30 Mon Sep 17 00:00:00 2001 From: Jeremy Hu Date: Sun, 28 Oct 2018 13:22:10 +0800 Subject: [PATCH] Export motions to FBX Now motions can export to AutoDesk FBX file. Tested in Unity 2017.4.1f1 --- ACKNOWLEDGEMENTS.html | 5 + src/documentwindow.cpp | 10 +- src/fbxfile.cpp | 516 ++++++++++++++++++++++++++++++++++++++++- src/fbxfile.h | 13 +- src/tetrapodposer.cpp | 3 +- src/util.cpp | 16 +- src/util.h | 3 + 7 files changed, 554 insertions(+), 12 deletions(-) diff --git a/ACKNOWLEDGEMENTS.html b/ACKNOWLEDGEMENTS.html index b7b70eef..947272f6 100644 --- a/ACKNOWLEDGEMENTS.html +++ b/ACKNOWLEDGEMENTS.html @@ -1127,4 +1127,9 @@ https://www.reddit.com/r/gamedev/comments/5iuf3h/i_am_writting_a_3d_monster_mode * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ + + +

Birdy

+
+    http://bediyap.com/programming/convert-quaternion-to-euler-rotations/
 
\ No newline at end of file diff --git a/src/documentwindow.cpp b/src/documentwindow.cpp index 2e6927a6..ab9799cc 100644 --- a/src/documentwindow.cpp +++ b/src/documentwindow.cpp @@ -1257,7 +1257,15 @@ void SkeletonDocumentWindow::exportFbxResult() } QApplication::setOverrideCursor(Qt::WaitCursor); Outcome skeletonResult = m_document->currentPostProcessedOutcome(); - FbxFileWriter fbxFileWriter(skeletonResult, m_document->resultRigBones(), m_document->resultRigWeights(), filename); + std::vector>>> exportMotions; + for (const auto &motionId: m_document->motionIdList) { + const Motion *motion = m_document->findMotion(motionId); + if (nullptr == motion) + continue; + exportMotions.push_back({motion->name, motion->jointNodeTrees}); + } + FbxFileWriter fbxFileWriter(skeletonResult, m_document->resultRigBones(), m_document->resultRigWeights(), filename, + exportMotions.empty() ? nullptr : &exportMotions); fbxFileWriter.save(); QApplication::restoreOverrideCursor(); } diff --git a/src/fbxfile.cpp b/src/fbxfile.cpp index f50dc10b..36194d71 100644 --- a/src/fbxfile.cpp +++ b/src/fbxfile.cpp @@ -1,9 +1,11 @@ #include #include #include +#include #include "fbxfile.h" #include "version.h" #include "jointnodetree.h" +#include "util.h" using namespace fbx; @@ -409,7 +411,12 @@ void FbxFileWriter::createReferences() m_fbxDocument.nodes.push_back(references); } -void FbxFileWriter::createDefinitions(size_t deformerCount) +void FbxFileWriter::createDefinitions(size_t deformerCount, + bool hasAnimtion, + size_t animationStackCount, + size_t animationLayerCount, + size_t animationCurveNodeCount, + size_t animationCurveCount) { FBXNode definitions("Definitions"); definitions.addPropertyNode("Version", (int32_t)100); @@ -1466,6 +1473,197 @@ void FbxFileWriter::createDefinitions(size_t deformerCount) objectType.addChild(FBXNode()); definitions.addChild(objectType); } + if (hasAnimtion) { + FBXNode objectType("ObjectType"); + objectType.addProperty("AnimationStack"); + objectType.addPropertyNode("Count", (int32_t)animationStackCount); + FBXNode propertyTemplate("PropertyTemplate"); + propertyTemplate.addProperty("FbxAnimStack"); + { + FBXNode properties("Properties70"); + { + FBXNode p("P"); + p.addProperty("Description"); + p.addProperty("KString"); + p.addProperty(""); + p.addProperty(""); + p.addProperty(""); + properties.addChild(p); + } + { + FBXNode p("P"); + p.addProperty("LocalStart"); + p.addProperty("KTime"); + p.addProperty("Time"); + p.addProperty(""); + p.addProperty((int64_t)0); + properties.addChild(p); + } + { + FBXNode p("P"); + p.addProperty("LocalStop"); + p.addProperty("KTime"); + p.addProperty("Time"); + p.addProperty(""); + p.addProperty((int64_t)0); + properties.addChild(p); + } + { + FBXNode p("P"); + p.addProperty("ReferenceStart"); + p.addProperty("KTime"); + p.addProperty("Time"); + p.addProperty(""); + p.addProperty((int64_t)0); + properties.addChild(p); + } + { + FBXNode p("P"); + p.addProperty("ReferenceStop"); + p.addProperty("KTime"); + p.addProperty("Time"); + p.addProperty(""); + p.addProperty((int64_t)0); + properties.addChild(p); + } + properties.addChild(FBXNode()); + propertyTemplate.addChild(properties); + } + propertyTemplate.addChild(FBXNode()); + objectType.addChild(propertyTemplate); + objectType.addChild(FBXNode()); + definitions.addChild(objectType); + } + if (hasAnimtion) { + FBXNode objectType("ObjectType"); + objectType.addProperty("AnimationLayer"); + objectType.addPropertyNode("Count", (int32_t)animationLayerCount); + FBXNode propertyTemplate("PropertyTemplate"); + propertyTemplate.addProperty("FbxAnimLayer"); + { + FBXNode properties("Properties70"); + { + FBXNode p("P"); + p.addProperty("Weight"); + p.addProperty("Number"); + p.addProperty(""); + p.addProperty("A"); + p.addProperty((double)100.000000); + properties.addChild(p); + } + { + FBXNode p("P"); + p.addProperty("Mute"); + p.addProperty("bool"); + p.addProperty(""); + p.addProperty(""); + p.addProperty((int32_t)0); + properties.addChild(p); + } + { + FBXNode p("P"); + p.addProperty("Solo"); + p.addProperty("bool"); + p.addProperty(""); + p.addProperty(""); + p.addProperty((int32_t)0); + properties.addChild(p); + } + { + FBXNode p("P"); + p.addProperty("Lock"); + p.addProperty("bool"); + p.addProperty(""); + p.addProperty(""); + p.addProperty((int32_t)0); + properties.addChild(p); + } + { + FBXNode p("P"); + p.addProperty("Color"); + p.addProperty("ColorRGB"); + p.addProperty("Color"); + p.addProperty(""); + p.addProperty((double)0.800000); + p.addProperty((double)0.800000); + p.addProperty((double)0.800000); + properties.addChild(p); + } + { + FBXNode p("P"); + p.addProperty("BlendMode"); + p.addProperty("enum"); + p.addProperty(""); + p.addProperty(""); + p.addProperty((int32_t)0); + properties.addChild(p); + } + { + FBXNode p("P"); + p.addProperty("RotationAccumulationMode"); + p.addProperty("enum"); + p.addProperty(""); + p.addProperty(""); + p.addProperty((int32_t)0); + properties.addChild(p); + } + { + FBXNode p("P"); + p.addProperty("ScaleAccumulationMode"); + p.addProperty("enum"); + p.addProperty(""); + p.addProperty(""); + p.addProperty((int32_t)0); + properties.addChild(p); + } + { + FBXNode p("P"); + p.addProperty("BlendModeBypass"); + p.addProperty("ULongLong"); + p.addProperty(""); + p.addProperty(""); + p.addProperty((int64_t)0); + properties.addChild(p); + } + properties.addChild(FBXNode()); + propertyTemplate.addChild(properties); + } + propertyTemplate.addChild(FBXNode()); + objectType.addChild(propertyTemplate); + objectType.addChild(FBXNode()); + definitions.addChild(objectType); + } + if (hasAnimtion) { + FBXNode objectType("ObjectType"); + objectType.addProperty("AnimationCurveNode"); + objectType.addPropertyNode("Count", (int32_t)animationCurveNodeCount); + FBXNode propertyTemplate("PropertyTemplate"); + propertyTemplate.addProperty("FbxAnimCurveNode"); + { + FBXNode properties("Properties70"); + { + FBXNode p("P"); + p.addProperty("d"); + p.addProperty("Compound"); + p.addProperty(""); + p.addProperty(""); + properties.addChild(p); + } + properties.addChild(FBXNode()); + propertyTemplate.addChild(properties); + } + propertyTemplate.addChild(FBXNode()); + objectType.addChild(propertyTemplate); + objectType.addChild(FBXNode()); + definitions.addChild(objectType); + } + if (hasAnimtion) { + FBXNode objectType("ObjectType"); + objectType.addProperty("AnimationCurve"); + objectType.addPropertyNode("Count", (int32_t)animationCurveCount); + objectType.addChild(FBXNode()); + definitions.addChild(objectType); + } definitions.addChild(FBXNode()); m_fbxDocument.nodes.push_back(definitions); } @@ -1473,7 +1671,8 @@ void FbxFileWriter::createDefinitions(size_t deformerCount) FbxFileWriter::FbxFileWriter(Outcome &outcome, const std::vector *resultRigBones, const std::map *resultRigWeights, - const QString &filename) : + const QString &filename, + const std::vector>>> *motions) : m_filename(filename) { createFbxHeader(); @@ -1484,10 +1683,14 @@ FbxFileWriter::FbxFileWriter(Outcome &outcome, createDocuments(); createReferences(); + FBXNode connections("Connections"); + size_t deformerCount = 0; if (resultRigBones && !resultRigBones->empty()) deformerCount = 1 + resultRigBones->size(); // 1 for the root Skin deformer - createDefinitions(deformerCount); + + JointNodeTree jointNodeTree(resultRigBones); + const auto &boneNodes = jointNodeTree.nodes(); FBXNode geometry("Geometry"); int64_t geometryId = m_next64Id++; @@ -1629,8 +1832,6 @@ FbxFileWriter::FbxFileWriter(Outcome &outcome, std::vector nodeAttributeIds; int64_t skinId = 0; int64_t armatureId = 0; - JointNodeTree jointNodeTree(resultRigBones); - const auto &boneNodes = jointNodeTree.nodes(); if (resultRigBones && !resultRigBones->empty()) { std::vector, std::vector>> bindPerBone(resultRigBones->size()); if (resultRigWeights && !resultRigWeights->empty()) { @@ -2136,6 +2337,267 @@ FbxFileWriter::FbxFileWriter(Outcome &outcome, } material.addChild(FBXNode()); + bool hasAnimation = nullptr != motions && !motions->empty(); + size_t animationStackCount = 0; + size_t animationLayerCount = 0; + size_t animationCurveNodeCount = 0; + size_t animationCurveCount = 0; + + std::vector animationStacks; + std::vector animationLayers; + std::vector animationCurveNodes; + std::vector animationCurves; + + if (hasAnimation) { + std::set rotatedJoints; + std::set translatedJoints; + + for (int animationIndex = 0; animationIndex < (int)motions->size(); ++animationIndex) { + const auto &motion = (*motions)[animationIndex]; + + FBXNode animationStack("AnimationStack"); + int64_t animationStackId = m_next64Id++; + animationStack.addProperty(animationStackId); + { + std::vector name; + auto stackName = motion.first.toUtf8(); + for (const auto &c: stackName) { + name.push_back((uint8_t)c); + } + name.push_back(0); + name.push_back(1); + name.push_back('A'); + name.push_back('n'); + name.push_back('i'); + name.push_back('m'); + name.push_back('S'); + name.push_back('t'); + name.push_back('a'); + name.push_back('c'); + name.push_back('k'); + animationStack.addProperty(name, 'S'); + } + animationStack.addProperty(""); + { + FBXNode properties("Properties70"); + { + FBXNode p("P"); + p.addProperty("LocalStop"); + p.addProperty("KTime"); + p.addProperty("Time"); + p.addProperty(""); + p.addProperty((int64_t)0); + properties.addChild(p); + } + { + FBXNode p("P"); + p.addProperty("ReferenceStop"); + p.addProperty("KTime"); + p.addProperty("Time"); + p.addProperty(""); + p.addProperty((int64_t)0); + properties.addChild(p); + } + properties.addChild(FBXNode()); + animationStack.addChild(properties); + } + animationStack.addChild(FBXNode()); + animationStacks.push_back(animationStack); + + FBXNode animationLayer("AnimationLayer"); + int64_t animationLayerId = m_next64Id++; + animationLayer.addProperty((int64_t)animationLayerId); + { + std::vector name; + auto layerName = motion.first.toUtf8(); + for (const auto &c: layerName) { + name.push_back((uint8_t)c); + } + name.push_back(0); + name.push_back(1); + name.push_back('A'); + name.push_back('n'); + name.push_back('i'); + name.push_back('m'); + name.push_back('L'); + name.push_back('a'); + name.push_back('y'); + name.push_back('e'); + name.push_back('r'); + animationLayer.addProperty(name, 'S'); + } + animationLayer.addProperty(""); + animationLayer.addChild(FBXNode()); + animationLayers.push_back(animationLayer); + + { + FBXNode p("C"); + p.addProperty("OO"); + p.addProperty(animationLayerId); + p.addProperty(animationStackId); + connections.addChild(p); + } + + for (const auto &keyframe: motion.second) { + for (int i = 0; i < (int)keyframe.second.nodes().size() && i < (int)boneNodes.size(); ++i) { + const auto &src = boneNodes[i]; + const auto &dest = keyframe.second.nodes()[i]; + if (!qFuzzyCompare(src.rotation, dest.rotation)) + rotatedJoints.insert(i); + if (!qFuzzyCompare(src.translation, dest.translation)) + translatedJoints.insert(i); + } + } + + for (const auto &jointIndex: rotatedJoints) { + int64_t animationCurveIds[3]; + std::vector ktimes; + std::vector values[3]; + for (int curveIndex = 0; curveIndex < 3; ++curveIndex) { + animationCurveIds[curveIndex] = m_next64Id++; + } + double timePoint = 0; + for (int frame = 0; frame < (int)motion.second.size(); frame++) { + const auto &keyframe = motion.second[frame]; + const auto &rotation = keyframe.second.nodes()[jointIndex].rotation; + double pitch = 0; + double yaw = 0; + double roll = 0; + quaternionToFbxEulerAngles(rotation, &pitch, &yaw, &roll); + + qDebug() << "curve:" << boneNodes[jointIndex].name << "frame:" << frame << "pitch:" << pitch << "yaw:" << yaw << "roll:" << roll; + + { + double qpitch = 0; + double qyaw = 0; + double qroll = 0; + quaternionToEulerAngles(rotation, &qpitch, &qyaw, &qroll); + if (!qFuzzyCompare(pitch, qpitch)) { + qDebug() << "pitch qt:" << qpitch << "this:" << pitch; + } + if (!qFuzzyCompare(yaw, qyaw)) { + qDebug() << "yaw qt:" << qyaw << "this:" << yaw; + } + if (!qFuzzyCompare(roll, qroll)) { + qDebug() << "roll qt:" << qroll << "this:" << roll; + } + } + + values[0].push_back(pitch); + values[1].push_back(yaw); + values[2].push_back(roll); + ktimes.push_back(secondsToKtime(timePoint)); + timePoint += keyframe.first; + + FBXNode animationCurveNode("AnimationCurveNode"); + int64_t animationCurveNodeId = m_next64Id++; + animationCurveNode.addProperty(animationCurveNodeId); + animationCurveNode.addProperty(std::vector({'R',0,1,'A','n','i','m','C','u','r','v','e','N','o','d','e'}), 'S'); + animationCurveNode.addProperty(""); + { + FBXNode properties("Properties70"); + { + FBXNode p("P"); + p.addProperty("d|X"); + p.addProperty("Number"); + p.addProperty(""); + p.addProperty("A"); + p.addProperty((double)pitch); + properties.addChild(p); + } + { + FBXNode p("P"); + p.addProperty("d|Y"); + p.addProperty("Number"); + p.addProperty(""); + p.addProperty("A"); + p.addProperty((double)yaw); + properties.addChild(p); + } + { + FBXNode p("P"); + p.addProperty("d|Z"); + p.addProperty("Number"); + p.addProperty(""); + p.addProperty("A"); + p.addProperty((double)roll); + properties.addChild(p); + } + properties.addChild(FBXNode()); + animationCurveNode.addChild(properties); + } + animationCurveNode.addChild(FBXNode()); + animationCurveNodes.push_back(animationCurveNode); + + { + FBXNode p("C"); + p.addProperty("OO"); + p.addProperty(animationCurveNodeId); + p.addProperty(animationLayerId); + connections.addChild(p); + } + { + FBXNode p("C"); + p.addProperty("OP"); + p.addProperty(animationCurveNodeId); + p.addProperty(limbNodeIds[1 + jointIndex]); + p.addProperty("Lcl Rotation"); + connections.addChild(p); + } + { + FBXNode p("C"); + p.addProperty("OP"); + p.addProperty(animationCurveIds[0]); + p.addProperty(animationCurveNodeId); + p.addProperty("d|X"); + connections.addChild(p); + } + { + FBXNode p("C"); + p.addProperty("OP"); + p.addProperty(animationCurveIds[1]); + p.addProperty(animationCurveNodeId); + p.addProperty("d|Y"); + connections.addChild(p); + } + { + FBXNode p("C"); + p.addProperty("OP"); + p.addProperty(animationCurveIds[2]); + p.addProperty(animationCurveNodeId); + p.addProperty("d|Z"); + connections.addChild(p); + } + } + for (int curveIndex = 0; curveIndex < 3; ++curveIndex) + { + FBXNode animationCurve("AnimationCurve"); + animationCurve.addProperty(animationCurveIds[curveIndex]); + animationCurve.addProperty(std::vector({'C','u','r','v','e',(uint8_t)('1'+curveIndex),0,1,'A','n','i','m','C','u','r','v','e'}), 'S'); + animationCurve.addProperty(""); + animationCurve.addPropertyNode("Default", (double)0.000000); + animationCurve.addPropertyNode("KeyVer", (int32_t)4008); + animationCurve.addPropertyNode("KeyTime", ktimes); + animationCurve.addPropertyNode("KeyValueFloat", values[curveIndex]); + animationCurve.addPropertyNode("KeyAttrFlags", std::vector(1, 24836)); + animationCurve.addPropertyNode("KeyAttrDataFloat", std::vector(4, 0.000000)); + animationCurve.addPropertyNode("KeyAttrRefCount", std::vector(1, ktimes.size())); + animationCurve.addChild(FBXNode()); + animationCurves.push_back(animationCurve); + } + } + } + + animationStackCount = animationStacks.size(); + animationLayerCount = animationLayers.size(); + animationCurveNodeCount = animationCurveNodes.size(); + animationCurveCount = animationCurves.size(); + } + + createDefinitions(deformerCount, + hasAnimation, + animationStackCount, animationLayerCount, animationCurveNodeCount, animationCurveCount); + FBXNode objects("Objects"); objects.addChild(geometry); objects.addChild(model); @@ -2151,10 +2613,23 @@ FbxFileWriter::FbxFileWriter(Outcome &outcome, for (const auto &nodeAttribute: nodeAttributes) { objects.addChild(nodeAttribute); } + if (hasAnimation) { + for (const auto &animationStack: animationStacks) { + objects.addChild(animationStack); + } + for (const auto &animationLayer: animationLayers) { + objects.addChild(animationLayer); + } + for (const auto &animationCurveNode: animationCurveNodes) { + objects.addChild(animationCurveNode); + } + for (const auto &animationCurve: animationCurves) { + objects.addChild(animationCurve); + } + } objects.addChild(FBXNode()); m_fbxDocument.nodes.push_back(objects); - FBXNode connections("Connections"); { FBXNode p("C"); p.addProperty("OO"); @@ -2252,7 +2727,7 @@ int64_t FbxFileWriter::to64Id(const QUuid &uuid) bool FbxFileWriter::save() { - m_fbxDocument.print(); + //m_fbxDocument.print(); m_fbxDocument.write(m_filename.toStdString()); return true; } @@ -2269,3 +2744,30 @@ std::vector FbxFileWriter::matrixToVector(const QMatrix4x4 &matrix) } return vec; } + +int64_t FbxFileWriter::secondsToKtime(double seconds) +{ + return (int64_t)(seconds * 46186158000); +} + +// http://bediyap.com/programming/convert-quaternion-to-euler-rotations/ +static void threeaxisrot(double r11, double r12, double r21, double r31, double r32, double res[]) +{ + res[0] = atan2(r31, r32); + res[1] = asin(r21); + res[2] = atan2(r11, r12); +} + +void FbxFileWriter::quaternionToFbxEulerAngles(const QQuaternion &q, double *pitch, double *yaw, double *roll) +{ + double radians[3] = {0, 0, 0}; + threeaxisrot(2*(q.x()*q.y() + q.scalar()*q.z()), + q.scalar()*q.scalar() + q.x()*q.x() - q.y()*q.y() - q.z()*q.z(), + -2*(q.x()*q.z() - q.scalar()*q.y()), + 2*(q.y()*q.z() + q.scalar()*q.x()), + q.scalar()*q.scalar() - q.x()*q.x() - q.y()*q.y() + q.z()*q.z(), + radians); + *pitch = qRadiansToDegrees(radians[0]); + *yaw = qRadiansToDegrees(radians[1]); + *roll = qRadiansToDegrees(radians[2]); +} diff --git a/src/fbxfile.h b/src/fbxfile.h index 0e97988f..5a722836 100644 --- a/src/fbxfile.h +++ b/src/fbxfile.h @@ -4,6 +4,7 @@ #include #include #include +#include #include "outcome.h" #include "document.h" @@ -14,7 +15,8 @@ public: FbxFileWriter(Outcome &outcome, const std::vector *resultRigBones, const std::map *resultRigWeights, - const QString &filename); + const QString &filename, + const std::vector>>> *motions=nullptr); bool save(); private: @@ -25,9 +27,16 @@ private: void createGlobalSettings(); void createDocuments(); void createReferences(); - void createDefinitions(size_t deformerCount); + void createDefinitions(size_t deformerCount, + bool hasAnimtion=false, + size_t animationStackCount=0, + size_t animationLayerCount=0, + size_t animationCurveNodeCount=0, + size_t animationCurveCount=0); void createTakes(); std::vector matrixToVector(const QMatrix4x4 &matrix); + void quaternionToFbxEulerAngles(const QQuaternion &q, double *pitch, double *yaw, double *roll); + int64_t secondsToKtime(double seconds); int64_t to64Id(const QUuid &uuid); int64_t m_next64Id = 612150000; diff --git a/src/tetrapodposer.cpp b/src/tetrapodposer.cpp index 5ad9a55f..bed80f9a 100644 --- a/src/tetrapodposer.cpp +++ b/src/tetrapodposer.cpp @@ -1,4 +1,5 @@ #include "tetrapodposer.h" +#include "util.h" TetrapodPoser::TetrapodPoser(const std::vector &bones) : Poser(bones) @@ -22,7 +23,7 @@ void TetrapodPoser::commit() if (item.first.startsWith("Left")) { yawAngle = -yawAngle; } - QQuaternion rotation = QQuaternion::fromEulerAngles(valueOfKeyInMapOrEmpty(item.second, "pitch").toFloat(), + QQuaternion rotation = eulerAnglesToQuaternion(valueOfKeyInMapOrEmpty(item.second, "pitch").toFloat(), yawAngle, valueOfKeyInMapOrEmpty(item.second, "roll").toFloat()); m_jointNodeTree.updateRotation(boneIndex, rotation); diff --git a/src/util.cpp b/src/util.cpp index 59759dac..581ff160 100644 --- a/src/util.cpp +++ b/src/util.cpp @@ -1,4 +1,5 @@ #include +#include #include "util.h" #include "version.h" @@ -89,4 +90,17 @@ float areaOfTriangle(const QVector3D &a, const QVector3D &b, const QVector3D &c) auto ab = b - a; auto ac = c - a; return 0.5 * QVector3D::crossProduct(ab, ac).length(); -}; +} + +QQuaternion eulerAnglesToQuaternion(double pitch, double yaw, double roll) +{ + return QQuaternion::fromEulerAngles(pitch, yaw, roll); +} + +void quaternionToEulerAngles(const QQuaternion &q, double *pitch, double *yaw, double *roll) +{ + auto eulerAngles = q.toEulerAngles(); + *pitch = eulerAngles.x(); + *yaw = eulerAngles.y(); + *roll = eulerAngles.z(); +} diff --git a/src/util.h b/src/util.h index 6c2a1436..871caa1c 100644 --- a/src/util.h +++ b/src/util.h @@ -22,5 +22,8 @@ QQuaternion quaternionOvershootSlerp(const QQuaternion &q0, const QQuaternion &q float radianBetweenVectors(const QVector3D &first, const QVector3D &second); float angleBetweenVectors(const QVector3D &first, const QVector3D &second); float areaOfTriangle(const QVector3D &a, const QVector3D &b, const QVector3D &c); +QQuaternion eulerAnglesToQuaternion(double pitch, double yaw, double roll); +void quaternionToEulerAngles(const QQuaternion &q, double *pitch, double *yaw, double *roll); +void quaternionToEulerAnglesXYZ(const QQuaternion &q, double *pitch, double *yaw, double *roll); #endif