如何在 3D 中对 QML 旋转变换进行动画处理和属性插值
How to animate and propertly intepolate a QML rotation transform in 3D
这里的代码示例:
import QtQuick 2.0
Item {
width: 200; height: 200
Rectangle {
width: 100; height: 100
anchors.centerIn: parent
color: "#00FF00"
Rectangle {
color: "#FF0000"
width: 10; height: 10
anchors.top: parent.top
anchors.right: parent.right
}
}
}
将产生此输出:
现在我想从这个绿色矩形的中心应用 3D 旋转。首先,我想在 X 轴上旋转 -45 度(向下弯腰),然后在 Y 轴上旋转 -60 度(向左转)。
我在旁边使用了以下使用 GLM 截取的 C++ 代码来帮助我计算轴和角度:
// generate rotation matrix from euler in X-Y-Z order
// please note that GLM uses radians, not degrees
glm::mat4 rotationMatrix = glm::eulerAngleXY(glm::radians(-45.0f), glm::radians(-60.0f));
// convert the rotation matrix into a quaternion
glm::quat quaternion = glm::toQuat(rotationMatrix);
// extract the rotation axis from the quaternion
glm::vec3 axis = glm::axis(quaternion);
// extract the rotation angle from the quaternion
// and also convert it back to degrees for QML
double angle = glm::degrees(glm::angle(quaternion));
这个 C++ 小程序的输出给了我 {-0.552483, -0.770076, 0.318976}
的轴和 73.7201
的角度。所以我将示例代码更新为:
import QtQuick 2.0
Item {
width: 200; height: 200
Rectangle {
width: 100; height: 100
anchors.centerIn: parent
color: "#00FF00"
Rectangle {
color: "#FF0000"
width: 10; height: 10
anchors.top: parent.top
anchors.right: parent.right
}
transform: Rotation {
id: rot
origin.x: 50; origin.y: 50
axis: Qt.vector3d(-0.552483, -0.770076, 0.318976)
angle: 73.7201
}
}
}
这正是我想看到的:
到目前为止一切顺利。现在是困难的部分。我如何制作动画?例如,如果我想从 {45.0, 60.0, 0} 变为 {45.0, 60.0, 90.0}。换句话说,我想从这里开始制作动画
到这里
我在此处插入了目标旋转
// generate rotation matrix from euler in X-Y-Z order
// please note that GLM uses radians, not degrees
glm::mat4 rotationMatrix = glm::eulerAngleXYZ(glm::radians(-45.0f), glm::radians(-60.0f), glm::radians(90.0f);
// convert the rotation matrix into a quaternion
glm::quat quaternion = glm::toQuat(rotationMatrix);
// extract the rotation axis from the quaternion
glm::vec3 axis = glm::axis(quaternion);
// extract the rotation angle from the quaternion
// and also convert it back to degrees for QML
double angle = glm::degrees(glm::angle(quaternion));
这给了我 {-0.621515, -0.102255, 0.7767}
的轴和 129.007
的角度
所以我将这个动画添加到我的示例中
ParallelAnimation {
running: true
Vector3dAnimation {
target: rot
property: "axis"
from: Qt.vector3d(-0.552483, -0.770076, 0.318976)
to: Qt.vector3d(-0.621515, -0.102255, 0.7767)
duration: 4000
}
NumberAnimation {
target: rot;
property: "angle";
from: 73.7201; to: 129.007;
duration: 4000;
}
}
哪个 'almost' 有效。问题是,如果您尝试一下,您会发现在动画的前半部分,旋转完全偏离了其所需的旋转轴,但在动画的后半部分会自行修复。开始轮换很好,目标轮换也不错,但无论发生什么,都不够好。如果我使用较小的角度(例如 45 度而不是 90 度)会更好,而如果我使用较大的角度(例如 180 度而不是 45 度)则情况会更糟,它只会随机旋转直到达到最终目标。
如何让这个动画看起来正好在开始旋转和目标旋转之间?
--------------------编辑-----------------
我再添加一个条件:我正在寻找的答案必须提供与我上面提供的屏幕截图完全相同的输出。
例如,将 3 个旋转轴拆分为 3 个单独的旋转变换不会给我正确的结果
transform: [
Rotation {
id: zRot
origin.x: 50; origin.y: 50;
angle: 0
},
Rotation {
id: xRot
origin.x: 50; origin.y: 50;
angle: -45
axis { x: 1; y: 0; z: 0 }
},
Rotation {
id: yRot
origin.x: 50; origin.y: 50;
angle: -60
axis { x: 0; y: 1; z: 0 }
}
]
会给我这个:
这是不正确的。
您正在以错误的方式尝试此操作。您可以组合变换并对其中之一进行动画处理。这样您将完全达到您的需要。
我看到的另一个问题是你写的是度数,在代码中我看到弧度:)。
底线应如下所示:
Rectangle {
width: 100; height: 100
anchors.centerIn: parent
color: "#00FF00"
Rectangle {
color: "#FF0000"
width: 10; height: 10
anchors.top: parent.top
anchors.right: parent.right
}
transform: [
Rotation {
id: zRot
origin.x: 50; origin.y: 50;
angle: 0
},
Rotation {
id: xRot
origin.x: 50; origin.y: 50;
angle: 45
axis { x: 1; y: 0; z: 0 }
},
Rotation {
id: yRot
origin.x: 50; origin.y: 50;
angle: 60
axis { x: 0; y: 1; z: 0 }
}
]
NumberAnimation {
running: true
loops: 100
target: zRot;
property: "angle";
from: 0; to: 360;
duration: 4000;
}
}
结果与你图片上的这个不同,但这是你弄乱了度数和弧度的结果。我使用了文本中描述的转换,而不是您的代码。
我解决了我自己的问题。我完全忘记了 Qt 不做球面线性插值!!!一旦我做了我自己的 slerp 函数,它就完美地工作了。
这是我为寻求答案的人编写的代码:
import QtQuick 2.0
Item {
function angleAxisToQuat(angle, axis) {
var a = angle * Math.PI / 180.0;
var s = Math.sin(a * 0.5);
var c = Math.cos(a * 0.5);
return Qt.quaternion(c, axis.x * s, axis.y * s, axis.z * s);
}
function multiplyQuaternion(q1, q2) {
return Qt.quaternion(q1.scalar * q2.scalar - q1.x * q2.x - q1.y * q2.y - q1.z * q2.z,
q1.scalar * q2.x + q1.x * q2.scalar + q1.y * q2.z - q1.z * q2.y,
q1.scalar * q2.y + q1.y * q2.scalar + q1.z * q2.x - q1.x * q2.z,
q1.scalar * q2.z + q1.z * q2.scalar + q1.x * q2.y - q1.y * q2.x);
}
function eulerToQuaternionXYZ(x, y, z) {
var quatX = angleAxisToQuat(x, Qt.vector3d(1, 0, 0));
var quatY = angleAxisToQuat(y, Qt.vector3d(0, 1, 0));
var quatZ = angleAxisToQuat(z, Qt.vector3d(0, 0, 1));
return multiplyQuaternion(multiplyQuaternion(quatX, quatY), quatZ)
}
function slerp(start, end, t) {
var halfCosTheta = ((start.x * end.x) + (start.y * end.y)) + ((start.z * end.z) + (start.scalar * end.scalar));
if (halfCosTheta < 0.0)
{
end.scalar = -end.scalar
end.x = -end.x
end.y = -end.y
end.z = -end.z
halfCosTheta = -halfCosTheta;
}
if (Math.abs(halfCosTheta) > 0.999999)
{
return Qt.quaternion(start.scalar + (t * (end.scalar - start.scalar)),
start.x + (t * (end.x - start.x )),
start.y + (t * (end.y - start.y )),
start.z + (t * (end.z - start.z )));
}
var halfTheta = Math.acos(halfCosTheta);
var s1 = Math.sin((1.0 - t) * halfTheta);
var s2 = Math.sin(t * halfTheta);
var s3 = 1.0 / Math.sin(halfTheta);
return Qt.quaternion((s1 * start.scalar + s2 * end.scalar) * s3,
(s1 * start.x + s2 * end.x ) * s3,
(s1 * start.y + s2 * end.y ) * s3,
(s1 * start.z + s2 * end.z ) * s3);
}
function getAxis(quat) {
var tmp1 = 1.0 - quat.scalar * quat.scalar;
if (tmp1 <= 0) return Qt.vector3d(0.0, 0.0, 1.0);
var tmp2 = 1 / Math.sqrt(tmp1);
return Qt.vector3d(quat.x * tmp2, quat.y * tmp2, quat.z * tmp2);
}
function getAngle(quat) {
return Math.acos(quat.scalar) * 2.0 * 180.0 / Math.PI;
}
width: 200; height: 200
Rectangle {
width: 100; height: 100
anchors.centerIn: parent
color: "#00FF00"
Rectangle {
color: "#FF0000"
width: 10; height: 10
anchors.top: parent.top
anchors.right: parent.right
}
transform: Rotation {
id: rot
origin.x: 50; origin.y: 50
axis: getAxis(animator.result)
angle: getAngle(animator.result)
}
}
NumberAnimation
{
property quaternion start: eulerToQuaternionXYZ(-45, -60, 0)
property quaternion end: eulerToQuaternionXYZ(-45, -60, 180)
property quaternion result: slerp(start, end, progress)
property real progress: 0
id: animator
target: animator
property: "progress"
from: 0.0
to: 1.0
duration: 4000
running: true
}
}
这里的代码示例:
import QtQuick 2.0
Item {
width: 200; height: 200
Rectangle {
width: 100; height: 100
anchors.centerIn: parent
color: "#00FF00"
Rectangle {
color: "#FF0000"
width: 10; height: 10
anchors.top: parent.top
anchors.right: parent.right
}
}
}
将产生此输出:
现在我想从这个绿色矩形的中心应用 3D 旋转。首先,我想在 X 轴上旋转 -45 度(向下弯腰),然后在 Y 轴上旋转 -60 度(向左转)。
我在旁边使用了以下使用 GLM 截取的 C++ 代码来帮助我计算轴和角度:
// generate rotation matrix from euler in X-Y-Z order
// please note that GLM uses radians, not degrees
glm::mat4 rotationMatrix = glm::eulerAngleXY(glm::radians(-45.0f), glm::radians(-60.0f));
// convert the rotation matrix into a quaternion
glm::quat quaternion = glm::toQuat(rotationMatrix);
// extract the rotation axis from the quaternion
glm::vec3 axis = glm::axis(quaternion);
// extract the rotation angle from the quaternion
// and also convert it back to degrees for QML
double angle = glm::degrees(glm::angle(quaternion));
这个 C++ 小程序的输出给了我 {-0.552483, -0.770076, 0.318976}
的轴和 73.7201
的角度。所以我将示例代码更新为:
import QtQuick 2.0
Item {
width: 200; height: 200
Rectangle {
width: 100; height: 100
anchors.centerIn: parent
color: "#00FF00"
Rectangle {
color: "#FF0000"
width: 10; height: 10
anchors.top: parent.top
anchors.right: parent.right
}
transform: Rotation {
id: rot
origin.x: 50; origin.y: 50
axis: Qt.vector3d(-0.552483, -0.770076, 0.318976)
angle: 73.7201
}
}
}
这正是我想看到的:
到目前为止一切顺利。现在是困难的部分。我如何制作动画?例如,如果我想从 {45.0, 60.0, 0} 变为 {45.0, 60.0, 90.0}。换句话说,我想从这里开始制作动画
到这里
我在此处插入了目标旋转
// generate rotation matrix from euler in X-Y-Z order
// please note that GLM uses radians, not degrees
glm::mat4 rotationMatrix = glm::eulerAngleXYZ(glm::radians(-45.0f), glm::radians(-60.0f), glm::radians(90.0f);
// convert the rotation matrix into a quaternion
glm::quat quaternion = glm::toQuat(rotationMatrix);
// extract the rotation axis from the quaternion
glm::vec3 axis = glm::axis(quaternion);
// extract the rotation angle from the quaternion
// and also convert it back to degrees for QML
double angle = glm::degrees(glm::angle(quaternion));
这给了我 {-0.621515, -0.102255, 0.7767}
的轴和 129.007
所以我将这个动画添加到我的示例中
ParallelAnimation {
running: true
Vector3dAnimation {
target: rot
property: "axis"
from: Qt.vector3d(-0.552483, -0.770076, 0.318976)
to: Qt.vector3d(-0.621515, -0.102255, 0.7767)
duration: 4000
}
NumberAnimation {
target: rot;
property: "angle";
from: 73.7201; to: 129.007;
duration: 4000;
}
}
哪个 'almost' 有效。问题是,如果您尝试一下,您会发现在动画的前半部分,旋转完全偏离了其所需的旋转轴,但在动画的后半部分会自行修复。开始轮换很好,目标轮换也不错,但无论发生什么,都不够好。如果我使用较小的角度(例如 45 度而不是 90 度)会更好,而如果我使用较大的角度(例如 180 度而不是 45 度)则情况会更糟,它只会随机旋转直到达到最终目标。
如何让这个动画看起来正好在开始旋转和目标旋转之间?
--------------------编辑-----------------
我再添加一个条件:我正在寻找的答案必须提供与我上面提供的屏幕截图完全相同的输出。
例如,将 3 个旋转轴拆分为 3 个单独的旋转变换不会给我正确的结果
transform: [
Rotation {
id: zRot
origin.x: 50; origin.y: 50;
angle: 0
},
Rotation {
id: xRot
origin.x: 50; origin.y: 50;
angle: -45
axis { x: 1; y: 0; z: 0 }
},
Rotation {
id: yRot
origin.x: 50; origin.y: 50;
angle: -60
axis { x: 0; y: 1; z: 0 }
}
]
会给我这个:
这是不正确的。
您正在以错误的方式尝试此操作。您可以组合变换并对其中之一进行动画处理。这样您将完全达到您的需要。
我看到的另一个问题是你写的是度数,在代码中我看到弧度:)。
底线应如下所示:
Rectangle {
width: 100; height: 100
anchors.centerIn: parent
color: "#00FF00"
Rectangle {
color: "#FF0000"
width: 10; height: 10
anchors.top: parent.top
anchors.right: parent.right
}
transform: [
Rotation {
id: zRot
origin.x: 50; origin.y: 50;
angle: 0
},
Rotation {
id: xRot
origin.x: 50; origin.y: 50;
angle: 45
axis { x: 1; y: 0; z: 0 }
},
Rotation {
id: yRot
origin.x: 50; origin.y: 50;
angle: 60
axis { x: 0; y: 1; z: 0 }
}
]
NumberAnimation {
running: true
loops: 100
target: zRot;
property: "angle";
from: 0; to: 360;
duration: 4000;
}
}
结果与你图片上的这个不同,但这是你弄乱了度数和弧度的结果。我使用了文本中描述的转换,而不是您的代码。
我解决了我自己的问题。我完全忘记了 Qt 不做球面线性插值!!!一旦我做了我自己的 slerp 函数,它就完美地工作了。
这是我为寻求答案的人编写的代码:
import QtQuick 2.0
Item {
function angleAxisToQuat(angle, axis) {
var a = angle * Math.PI / 180.0;
var s = Math.sin(a * 0.5);
var c = Math.cos(a * 0.5);
return Qt.quaternion(c, axis.x * s, axis.y * s, axis.z * s);
}
function multiplyQuaternion(q1, q2) {
return Qt.quaternion(q1.scalar * q2.scalar - q1.x * q2.x - q1.y * q2.y - q1.z * q2.z,
q1.scalar * q2.x + q1.x * q2.scalar + q1.y * q2.z - q1.z * q2.y,
q1.scalar * q2.y + q1.y * q2.scalar + q1.z * q2.x - q1.x * q2.z,
q1.scalar * q2.z + q1.z * q2.scalar + q1.x * q2.y - q1.y * q2.x);
}
function eulerToQuaternionXYZ(x, y, z) {
var quatX = angleAxisToQuat(x, Qt.vector3d(1, 0, 0));
var quatY = angleAxisToQuat(y, Qt.vector3d(0, 1, 0));
var quatZ = angleAxisToQuat(z, Qt.vector3d(0, 0, 1));
return multiplyQuaternion(multiplyQuaternion(quatX, quatY), quatZ)
}
function slerp(start, end, t) {
var halfCosTheta = ((start.x * end.x) + (start.y * end.y)) + ((start.z * end.z) + (start.scalar * end.scalar));
if (halfCosTheta < 0.0)
{
end.scalar = -end.scalar
end.x = -end.x
end.y = -end.y
end.z = -end.z
halfCosTheta = -halfCosTheta;
}
if (Math.abs(halfCosTheta) > 0.999999)
{
return Qt.quaternion(start.scalar + (t * (end.scalar - start.scalar)),
start.x + (t * (end.x - start.x )),
start.y + (t * (end.y - start.y )),
start.z + (t * (end.z - start.z )));
}
var halfTheta = Math.acos(halfCosTheta);
var s1 = Math.sin((1.0 - t) * halfTheta);
var s2 = Math.sin(t * halfTheta);
var s3 = 1.0 / Math.sin(halfTheta);
return Qt.quaternion((s1 * start.scalar + s2 * end.scalar) * s3,
(s1 * start.x + s2 * end.x ) * s3,
(s1 * start.y + s2 * end.y ) * s3,
(s1 * start.z + s2 * end.z ) * s3);
}
function getAxis(quat) {
var tmp1 = 1.0 - quat.scalar * quat.scalar;
if (tmp1 <= 0) return Qt.vector3d(0.0, 0.0, 1.0);
var tmp2 = 1 / Math.sqrt(tmp1);
return Qt.vector3d(quat.x * tmp2, quat.y * tmp2, quat.z * tmp2);
}
function getAngle(quat) {
return Math.acos(quat.scalar) * 2.0 * 180.0 / Math.PI;
}
width: 200; height: 200
Rectangle {
width: 100; height: 100
anchors.centerIn: parent
color: "#00FF00"
Rectangle {
color: "#FF0000"
width: 10; height: 10
anchors.top: parent.top
anchors.right: parent.right
}
transform: Rotation {
id: rot
origin.x: 50; origin.y: 50
axis: getAxis(animator.result)
angle: getAngle(animator.result)
}
}
NumberAnimation
{
property quaternion start: eulerToQuaternionXYZ(-45, -60, 0)
property quaternion end: eulerToQuaternionXYZ(-45, -60, 180)
property quaternion result: slerp(start, end, progress)
property real progress: 0
id: animator
target: animator
property: "progress"
from: 0.0
to: 1.0
duration: 4000
running: true
}
}