Revision control
Copy as Markdown
// Copyright (C) 2026 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
#include <QtTest>
#include <QtNumeric>
#include <QtQuick3D/private/qquick3dquaternionutils_p.h>
class tst_qquick3dquaternionutils : public QObject
{
Q_OBJECT
private slots:
// fromAxisAndAngle
void fromAxisAndAngle_identity();
void fromAxisAndAngle_rotation();
void fromAxisAndAngle_overloads();
void fromAxisAndAngle_negativeAngle();
void fromAxisAndAngle_zeroAxis();
void fromAxisAndAngle_fullRotation();
// fromEulerAngles
void fromEulerAngles_identity();
void fromEulerAngles_overloads();
void fromEulerAngles_rotation();
void fromEulerAngles_largeAngles();
void fromEulerAngles_gimbalLock();
// fromAxesAndAngles
void fromAxesAndAngles_twoAxes();
void fromAxesAndAngles_threeAxes();
void fromAxesAndAngles_zeroAngles();
void fromAxesAndAngles_nonOrthogonal();
void fromAxesAndAngles_vectorCheck_twoAxes();
void fromAxesAndAngles_vectorCheck_threeAxes();
// lookAt
void lookAt_identity();
void lookAt_simple();
void lookAt_sourceEqualsTarget();
void lookAt_parallelUpForward();
void lookAt_behindSource();
void lookAt_vectorCheck_simple();
void lookAt_vectorCheck_behind();
void lookAt_vectorCheck_arbitrary();
void lookAt_vectorCheck_parallelUpForward();
void lookAt_QTBUG_143886();
};
// For quaternions a and b, they are equivalent if a == b OR a == -b
static bool fuzzyQuaternionCompare(const QQuaternion &a, const QQuaternion &b)
{
return qFuzzyCompare(a, b) || qFuzzyCompare(a, -b);
}
void tst_qquick3dquaternionutils::fromAxisAndAngle_identity()
{
const QQuaternion q = QQuick3DQuaternionUtils::fromAxisAndAngle(QVector3D(0, 1, 0), 0.0f);
QVERIFY(q.isIdentity());
}
void tst_qquick3dquaternionutils::fromAxisAndAngle_rotation()
{
const QQuaternion q = QQuick3DQuaternionUtils::fromAxisAndAngle(QVector3D(0, 1, 0), 90.0f);
const QVector3D rotated = q.rotatedVector(QVector3D(1, 0, 0));
QVERIFY(qFuzzyCompare(rotated, QVector3D(0.0f, 0.0f, -1.0f)));
}
void tst_qquick3dquaternionutils::fromAxisAndAngle_overloads()
{
const QQuaternion q1 = QQuick3DQuaternionUtils::fromAxisAndAngle(0, 0, 1, 45.0f);
const QQuaternion q2 = QQuick3DQuaternionUtils::fromAxisAndAngle(QVector3D(0, 0, 1), 45.0f);
QVERIFY(fuzzyQuaternionCompare(q1, q2));
}
void tst_qquick3dquaternionutils::fromAxisAndAngle_negativeAngle()
{
const QQuaternion q = QQuick3DQuaternionUtils::fromAxisAndAngle(QVector3D(0, 1, 0), -90.0f);
const QVector3D rotated = q.rotatedVector(QVector3D(1, 0, 0));
QVERIFY(qFuzzyCompare(rotated, QVector3D(0.0f, 0.0f, 1.0f)));
}
void tst_qquick3dquaternionutils::fromAxisAndAngle_zeroAxis()
{
const QQuaternion q = QQuick3DQuaternionUtils::fromAxisAndAngle(QVector3D(0, 0, 0), 90.0f);
QVERIFY(q.isIdentity());
}
void tst_qquick3dquaternionutils::fromAxisAndAngle_fullRotation()
{
const QQuaternion q = QQuick3DQuaternionUtils::fromAxisAndAngle(QVector3D(0, 1, 0), 360.0f);
QVERIFY(fuzzyQuaternionCompare(q, QQuaternion())); // Fuzzy check identity
}
void tst_qquick3dquaternionutils::fromEulerAngles_identity()
{
const QQuaternion q = QQuick3DQuaternionUtils::fromEulerAngles(0.0f, 0.0f, 0.0f);
QVERIFY(q.isIdentity());
}
void tst_qquick3dquaternionutils::fromEulerAngles_overloads()
{
const QQuaternion q1 = QQuick3DQuaternionUtils::fromEulerAngles(10, 20, 30);
const QQuaternion q2 = QQuick3DQuaternionUtils::fromEulerAngles(QVector3D(10, 20, 30));
QVERIFY(fuzzyQuaternionCompare(q1, q2));
}
void tst_qquick3dquaternionutils::fromEulerAngles_rotation()
{
const QQuaternion q = QQuick3DQuaternionUtils::fromEulerAngles(0, 90, 0);
const QVector3D rotated = q.rotatedVector(QVector3D(1, 0, 0));
QVERIFY(qFuzzyIsNull(rotated.x()));
QVERIFY(qFuzzyCompare(rotated.z(), -1.0f));
}
void tst_qquick3dquaternionutils::fromEulerAngles_largeAngles()
{
const QQuaternion q = QQuick3DQuaternionUtils::fromEulerAngles(0, 450, 0); // 360+90
const QVector3D rotated = q.rotatedVector(QVector3D(1, 0, 0));
QVERIFY(qFuzzyCompare(rotated.normalized(), QVector3D(0, 0, -1)));
}
void tst_qquick3dquaternionutils::fromEulerAngles_gimbalLock()
{
const QQuaternion q = QQuick3DQuaternionUtils::fromEulerAngles(0, 90, 90);
QVERIFY(!q.isIdentity());
QVERIFY(qFuzzyCompare(q.length(), 1.0f));
}
void tst_qquick3dquaternionutils::fromAxesAndAngles_twoAxes()
{
const QQuaternion q = QQuick3DQuaternionUtils::fromAxesAndAngles(QVector3D(1, 0, 0), 90, QVector3D(0, 1, 0), 90);
QVERIFY(!q.isIdentity());
QVERIFY(qFuzzyCompare(q.length(), 1.0f));
}
void tst_qquick3dquaternionutils::fromAxesAndAngles_threeAxes()
{
const QQuaternion q = QQuick3DQuaternionUtils::fromAxesAndAngles(QVector3D(1, 0, 0), 30, QVector3D(0, 1, 0), 45, QVector3D(0, 0, 1), 60);
QVERIFY(!q.isIdentity());
QVERIFY(qFuzzyCompare(q.length(), 1.0f));
}
void tst_qquick3dquaternionutils::fromAxesAndAngles_zeroAngles()
{
const QQuaternion q = QQuick3DQuaternionUtils::fromAxesAndAngles(QVector3D(1, 0, 0), 0, QVector3D(0, 1, 0), 0);
QVERIFY(q.isIdentity());
}
void tst_qquick3dquaternionutils::fromAxesAndAngles_nonOrthogonal()
{
const QQuaternion q = QQuick3DQuaternionUtils::fromAxesAndAngles(QVector3D(1, 1, 0).normalized(),
45,
QVector3D(0, 1, 1).normalized(),
30);
QVERIFY(!q.isIdentity());
QVERIFY(qFuzzyCompare(q.length(), 1.0f));
}
void tst_qquick3dquaternionutils::fromAxesAndAngles_vectorCheck_twoAxes()
{
const QQuaternion q = QQuick3DQuaternionUtils::fromAxesAndAngles(QVector3D(1, 0, 0), 90.0f, QVector3D(0, 1, 0), 90.0f);
// Rotate the basis vectors
const QVector3D xr = q.rotatedVector(QVector3D(1, 0, 0));
const QVector3D yr = q.rotatedVector(QVector3D(0, 1, 0));
const QVector3D zr = q.rotatedVector(QVector3D(0, 0, 1));
// Check that lengths remain 1 (normalization sanity)
QVERIFY(qFuzzyCompare(xr.length(), 1.0f));
QVERIFY(qFuzzyCompare(yr.length(), 1.0f));
QVERIFY(qFuzzyCompare(zr.length(), 1.0f));
// Check that axes are still orthogonal
QVERIFY(qFuzzyIsNull(QVector3D::dotProduct(xr, yr)));
QVERIFY(qFuzzyIsNull(QVector3D::dotProduct(xr, zr)));
QVERIFY(qFuzzyIsNull(QVector3D::dotProduct(yr, zr)));
// Optionally, check that the rotation produced a permutation of the original axes
// (e.g., xr might be along original Z, etc.)
QVERIFY(!qFuzzyCompare(xr, QVector3D(1, 0, 0)));
QVERIFY(!qFuzzyCompare(yr, QVector3D(0, 1, 0)));
QVERIFY(!qFuzzyCompare(zr, QVector3D(0, 0, 1)));
}
void tst_qquick3dquaternionutils::fromAxesAndAngles_vectorCheck_threeAxes()
{
const QQuaternion q = QQuick3DQuaternionUtils::fromAxesAndAngles(QVector3D(1, 0, 0), 30, QVector3D(0, 1, 0), 45, QVector3D(0, 0, 1), 60);
const QVector3D x(1, 0, 0);
const QVector3D y(0, 1, 0);
const QVector3D z(0, 0, 1);
QVERIFY(qFuzzyCompare(q.rotatedVector(x).length(), 1.0f));
QVERIFY(qFuzzyCompare(q.rotatedVector(y).length(), 1.0f));
QVERIFY(qFuzzyCompare(q.rotatedVector(z).length(), 1.0f));
QVERIFY(!qFuzzyCompare(q.rotatedVector(x), x));
QVERIFY(!qFuzzyCompare(q.rotatedVector(y), y));
QVERIFY(!qFuzzyCompare(q.rotatedVector(z), z));
}
void tst_qquick3dquaternionutils::lookAt_identity()
{
const QQuaternion q = QQuick3DQuaternionUtils::lookAt(QVector3D(0, 0, 0), QVector3D(0, 0, -1));
QVERIFY(q.isIdentity());
}
void tst_qquick3dquaternionutils::lookAt_simple()
{
const QQuaternion q = QQuick3DQuaternionUtils::lookAt(QVector3D(0, 0, 0), QVector3D(1, 0, 0));
const QVector3D rotated = q.rotatedVector(QVector3D(0, 0, -1));
QVERIFY(qFuzzyCompare(rotated.normalized(), QVector3D(1, 0, 0)));
}
void tst_qquick3dquaternionutils::lookAt_sourceEqualsTarget()
{
const QQuaternion q = QQuick3DQuaternionUtils::lookAt(QVector3D(1, 2, 3), QVector3D(1, 2, 3));
QEXPECT_FAIL("", "lookAt does not handle same start end position", Continue);
QVERIFY(q.isIdentity());
}
void tst_qquick3dquaternionutils::lookAt_parallelUpForward()
{
const QQuaternion q = QQuick3DQuaternionUtils::lookAt(QVector3D(0, 0, 0), QVector3D(0, 0, 1), QVector3D(0, 0, 1), QVector3D(0, 0, 1));
const QVector3D rotated = q.rotatedVector(QVector3D(0, 0, 1)).normalized();
QVERIFY(qFuzzyCompare(rotated, QVector3D(0, 0, 1)));
}
void tst_qquick3dquaternionutils::lookAt_behindSource()
{
const QQuaternion q = QQuick3DQuaternionUtils::lookAt(QVector3D(0, 0, 0), QVector3D(0, 0, 1));
const QVector3D rotated = q.rotatedVector(QVector3D(0, 0, -1)).normalized();
QVERIFY(qFuzzyCompare(rotated, QVector3D(0, 0, 1)));
}
void tst_qquick3dquaternionutils::lookAt_vectorCheck_simple()
{
QVector3D source(0, 0, 0);
QVector3D target(1, 0, 0);
const QQuaternion q = QQuick3DQuaternionUtils::lookAt(source, target);
QVector3D rotated = q.rotatedVector(QVector3D(0, 0, -1)).normalized();
QVERIFY(qFuzzyCompare(rotated, QVector3D(1, 0, 0)));
}
void tst_qquick3dquaternionutils::lookAt_vectorCheck_behind()
{
QVector3D source(0, 0, 0);
QVector3D target(0, 0, 1);
const QQuaternion q = QQuick3DQuaternionUtils::lookAt(source, target);
QVector3D rotated = q.rotatedVector(QVector3D(0, 0, -1)).normalized();
QVERIFY(qFuzzyCompare(rotated, QVector3D(0, 0, 1)));
}
void tst_qquick3dquaternionutils::lookAt_vectorCheck_arbitrary()
{
QVector3D source(1, 2, 3);
QVector3D target(4, 6, 3);
const QQuaternion q = QQuick3DQuaternionUtils::lookAt(source, target);
QVector3D rotated = q.rotatedVector(QVector3D(0, 0, -1)).normalized();
QVector3D expected = (target - source).normalized();
QVERIFY(qFuzzyCompare(rotated, expected));
}
void tst_qquick3dquaternionutils::lookAt_vectorCheck_parallelUpForward()
{
QVector3D source(0, 0, 0);
QVector3D target(0, 0, 1);
QVector3D forward(0, 0, 1);
QVector3D up(0, 0, 1);
const QQuaternion q = QQuick3DQuaternionUtils::lookAt(source, target, forward, up);
QVector3D rotated = q.rotatedVector(forward).normalized();
QVector3D expected = (target - source).normalized();
QVERIFY(qFuzzyCompare(rotated, expected));
}
void tst_qquick3dquaternionutils::lookAt_QTBUG_143886()
{
QVector3D source(0.47212299704551697, -0.05714830011129379, -0.03842129930853844);
QVector3D target(0.0174984410405159, -0.1452823281288147, -0.038421452045440674);
QVector3D forward(-0.9816991090774536, -0.19043900072574615, 0);
QVector3D up(0, 1, 0);
const QQuaternion q = QQuick3DQuaternionUtils::lookAt(source, target, forward, up);
QVERIFY(fuzzyQuaternionCompare(q, QQuaternion()));
}
QTEST_APPLESS_MAIN(tst_qquick3dquaternionutils)
#include "tst_qquick3dquaternionutils.moc"