如何在 3D 中对 QML 旋转变换进行动画处理和属性插值

Posted

技术标签:

【中文标题】如何在 3D 中对 QML 旋转变换进行动画处理和属性插值【英文标题】:How to animate and propertly intepolate a QML rotation transform in 3D 【发布时间】:2015-11-11 04:40:43 【问题描述】:

这里的代码示例:

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;
    

“几乎”有效。问题是,如果你尝试一下,你会发现在动画的前半部分旋转完全偏离了它想要的旋转轴,但在动画的后半部分会自行修复。起始轮换很好,目标轮换也很好,但是中间发生的任何事情都不够好。如果我使用较小的角度(例如 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 
        
    ]

给我这个:

这是不正确的。

【问题讨论】:

【参考方案1】:

我解决了自己的问题。我完全忘记了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中没有暴露:doc.qt.io/qt-5/qquaternion.html#slerp 他们真的应该添加一个 QuaternionAnimation 来为我们执行这个球形线性插值器,或者至少公开 slerp 函数 你为什么只通过函数来​​做这个?为什么不使用 QML 的声明性? 但是我已经在 C++ 中实现了我自己的转换实用程序模块,并且如果这就是你的意思的话,可以使用 QML。在 javascript 中制作原型并在堆栈溢出时显示答案更容易,因为人们可以简单地复制和粘贴解决方案,如果他们想看到结果,可以自己尝试【参考方案2】:

您正在以错误的方式尝试此操作。您可以组合转换并为其中之一设置动画。通过这种方式,您将完全满足您的需求。

我看到的另一个问题是你正在写关于度数的代码,我在代码中看到了弧度:)。

底线应该是这样的:

    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)) 不,我不能接受你的回答。您的代码并没有给出我上面图片的确切结果。但是你让我意识到我犯了一个错误。角度使用右手定则,这意味着我的角度应该是-45度和-60度。谢谢你:-) 我用 cmets 更新了我的 C++ 代码,了解度数到弧度并从弧度转换回度数。并添加了我正在寻找的标准,即输出必须与我提供的屏幕截图相同 我做了一些测试,结果发现有一些 QML 错误。围绕 X 旋转 89 度,然后围绕 Y 旋转 89 度显示一个像素大小的对象(旋转 90 度不显示任何内容)。看起来每个转换都应用于生成新平面对象的平面对象。 IMO 所有的转换都应该结合起来然后应用。提供单个转换可以解决这个问题,但会使动画更难。 是的,我得出的结论与您完全相同。谢谢@marek-r

以上是关于如何在 3D 中对 QML 旋转变换进行动画处理和属性插值的主要内容,如果未能解决你的问题,请参考以下文章

动画 SVG 组对象 - 使用样式组件进行变换旋转不会围绕圆原点旋转

有没有办法用关键点进行 3D 变换?

如何使用qml qt3d(qt)将对象旋转一个角度?

在 180º firefox 和 opera 中旋转 svg matrix3d 变换 是

围绕特定轴动画qt3d旋转

使用 jQuery 对 CSS 变换进行动画处理