Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to animate and propertly intepolate a QML rotation transform in 3D

Tags:

c++

qt

3d

qml

glm-math

This code sample here:

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

Will produce this output:

step 1

Now I want to apply a 3D rotation from the center of this green rectangle. First, I want to rotate on X by -45 degrees (bowing down), then on Y by -60 degrees (turning left).

I used the following c++ code snipped using GLM on the side to help me calculate the axis and angle:

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

The output of this little C++ program gave me an axis of {-0.552483, -0.770076, 0.318976} and an angle of 73.7201. So I updated my sample code to this:

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

Which give me exactly what I wanted to see:

step 2

So far so good. Now comes the hard part. How do I animate this? For example, if I want to go from {45.0, 60.0, 0} to {45.0, 60.0, 90.0}. In other word, I want to animate from here

step 2

to here

step3

I plugged that target rotation here

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

which gave me an axis of {-0.621515, -0.102255, 0.7767} and an angle of 129.007

So I added this animation to my sample

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

Which 'almost' works. The problem is, if you try it, you will see that the rotation goes completely off its desired rotation axis for the first half of the animation, but fixes itself for the last half of the animation. The starting rotation is good, the target rotation is good, but whatever that happens in between is not good enough. It is better if I use smaller angles like 45 degrees instead of 90 degrees, and is going to be worst if I use larger angles like 180 degrees instead of 45 degrees, where it just spins in random directions until it reaches its final targets.

How do I get this animation to look right between the start rotation and the target rotation?

------------------- EDIT -------------------

I am adding one more criteria: The answer I am looking for must absolutely provide an identical output as the screenshots I provided above.

For example, splitting the 3 rotation axis in 3 separate rotation transforms doesn't give me the right results

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

Will give me this:

incorrect

Which is incorrect.

like image 390
mchiasson Avatar asked Mar 14 '23 05:03

mchiasson


2 Answers

I solved my own problem. I completely forgot that Qt doesn't do spherical linear interpolation!!! As soon as I did my own slerp function, it all worked perfectly.

Here's my code for those who are seeking the answer:

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
    }
}
like image 177
mchiasson Avatar answered Mar 31 '23 10:03

mchiasson


You are trying to this in wrong way. You can combine transformations and animate one of it. This way you will achieve exactly what you need.

Another problem I see is that you are writing about degrees and in code I see radians :).

Bottom line this should look like this:

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

Result is different from this one on your pictures, but this is result you've messed up degrees and radians. I used transformation described in text, not from your code.

like image 42
Marek R Avatar answered Mar 31 '23 11:03

Marek R