如何在QML中以3D方式实现旋转变换并适当地插值。

5
这里有一个代码示例:
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
        }
    }
}

将会生成如下输出:

step 1

我现在想要从这个绿色矩形的中心应用一个3D旋转。首先,我想通过X轴旋转-45度(向下弯曲),然后通过Y轴旋转-60度(向左转)。

我使用了以下C++代码片段,并结合GLM帮助我计算轴和角度:

// 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
        }
    }
}

这给了我想要看到的内容:

第2步

到目前为止一切都很好。现在来到了难点。如何实现动画呢?例如,如果我想从{45.0、60.0、0}到{45.0、60.0、90.0}。换句话说,我想从这里进行动画

第2步

到这里

第3步

我将目标旋转插入到这里

// 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;
    }
}

"几乎"可行。问题是,如果你尝试它,你会发现旋转在动画的前半段完全偏离了其预期的旋转轴,但在动画的后半段修正了自己。起始旋转很好,目标旋转也很好,但其中发生的任何事情都不够好。如果我使用较小的角度(如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 }
        }
    ]

我会收到这个:

不正确

这是不正确的。

2个回答

5
我解决了自己的问题。我完全忘记了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
    }
}

真令人困惑,因为他们在其 QQuaternion 类中提供了 slerp 函数,但在 QML 中没有公开: http://doc.qt.io/qt-5/qquaternion.html#slerp - mchiasson
1
他们真的应该添加一个四元数动画来执行这个球形线性插值器,或者至少公开slerp函数。 - mchiasson
你为什么只使用函数来实现这个?为什么不利用 QML 的声明性特点呢? - Marek R
但是我已经在C++中实现了自己的转换工具模块,并将其暴露给QML,如果这就是你的意思。只是在JavaScript中进行原型设计并在堆栈溢出上显示答案更容易,因为人们可以简单地复制和粘贴解决方案并尝试自己查看结果。 - mchiasson

3

您正在错误的方式尝试。您可以组合变换并将其中一个动画化。这样,您将达到所需的效果。

我看到的另一个问题是,您在写关于度数,但是代码中看到弧度 :)

底线是应该像这样:

    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;
        }
    }

结果与你的图片不同,但这是由于你混淆了度数和弧度。我使用的是文本中描述的转换方法,而不是你的代码。


GLM 仅支持弧度制,因此我将我的欧拉角转换为弧度(例如 glm::radians(45.0f)),最后再将其转换回度数(例如 glm::degrees(glm::angle(quaternion)))。 - mchiasson
不,我不能接受你的答案。你的代码没有给我与上面图片完全相同的结果。但是你让我意识到了我犯了一个错误。角度应该使用右手定则,这意味着我的角度应该是-45度和-60度。谢谢你 :-) - mchiasson
我在我的C++代码中更新了关于度数转弧度和弧度转度数的注释,并添加了输出必须与我提供的截图完全相同的标准。 - mchiasson
我进行了一些测试,结果发现存在一些QML的bug。绕X轴旋转89度,然后绕Y轴旋转89度会显示一个像素大小的对象(90度旋转则不显示)。看起来每个变换都被应用于平面对象,产生新的平面对象。在我看来,所有的变换应该合并,然后应用。提供单一的变换可以解决这个问题,但使动画更难实现。 - Marek R
是的,我得出了和你完全一样的结论。谢谢 @marek-r - mchiasson

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接