QT Rotating object problems

Viewed 155

I am currently working on a project that has a spinning object. The object's rotation speed is changed and its direction of rotation is changed (forwards/backward). After doing some research I came across RotationAnimator on the QT documentation, it seems to have everything I need and I can easily reverse direction. The only problem is that is in QML. My program is entirely in Qt's C++.

I tried an attempt at using QVariantAnimation (documentation here)but every way I have tried there is always some problem. Changing speed is relatively easy, just fooAnimation->setLoop(-1) (loop indefinitely) and change the duration of the animation. The problem I run into is looping forwards AND backward. Or in other terms, changing direction. I have attempted to change the direction of the animation using fooAnimation->setDirection(<Direction goes here>), but if I run the animation in reverse long enough it will stop the animation entirely. It will only loop forwards. That's the key part of the problem.

So I tried alternative ways to change the direction. One idea was changing the endValue from 360 degrees to -360 using fooAnimation->setEndValue(<value goes here>) when I want to reverse, but this has an adverse effect. If I were to swap the direction, the current rotation: let's say it's 110, will be inverted. So now it suddenly jumps to -110 and starts from there. This leads to very jittery rotation as every time a direction swap happens it teleports the rotation of the object.

This seems like a pretty simple thing to implement, rotating an object that can have its rotation speed and direction changed, but I just can't wrap my head around how I could implement it with animations.

A side note here is that all this is being done in Qt 3D so I can grab rotation and other properties from the rotating object (or in this case its QTransform)

1 Answers

There are multiple ways to achieve this effect. On this Qt/QML demo, I used an approach based on NumberAnimation to rotate the cube.

These are the key ideas behind it:

  • The cube has a Transform component that defines the 4x4 matrix that Qt3D uses to place the cube in the world using predefined rotation and scale values. The NumberAnimation simply changes the rotation angle used on this transform to make the cube rotate on the screen;
  • The rotation speed is defined by the duration property of that NumberAnimation: a smaller value makes the cube rotate faster. The cube takes 5 seconds to rotate from 0 to 360 degrees;
  • Changing the rotation speed means recalculating the duration of the animation based on how far away from 0 degrees the current rotation angle is. The animation needs to be restarted whenever we change ANY NumberAnimation properties;
  • When the direction of the rotation is changed, we store the current rotation angle in cubeTransform.startAngle and update the to and from properties of NumberAnimation along with duration so the cube can be rotated in a new direction starting from the current angle.

I added a few buttons to the UI to change the direction and speed in runtime:

main.qml:

import QtQuick 2.12 as QQ2
import QtQuick.Controls 2.12
import QtQuick.Scene3D 2.12

import Qt3D.Core 2.0
import Qt3D.Render 2.0
import Qt3D.Input 2.0
import Qt3D.Extras 2.0
import Qt3D.Logic 2.0

QQ2.Item {
    id: windowId
    width: 800
    height: 800

    property real cubeScale: 100.0
    property int rotationSpeed: 5000
    property real speedFactor: 1.0
    property bool clockwiseRotation: true

    Scene3D {
        id: scene3D
        anchors.fill: parent
        aspects: ["input", "logic"]
        focus: true
        cameraAspectRatioMode: Scene3D.AutomaticAspectRatio

        Entity {
            id: rootEntity

            Camera {
                id: camera
                projectionType: CameraLens.PerspectiveProjection
                fieldOfView: 45
                nearPlane : 0.1
                farPlane : 100000.0
                position: Qt.vector3d(0.0, 0.0, 500.0)
                viewCenter: Qt.vector3d(0.0, 0.0, 0.0)
                upVector: Qt.vector3d( 0.0, 1.0, 0.0 )
            }

            components: [
                RenderSettings {
                    activeFrameGraph: ForwardRenderer {
                        camera: camera
                        clearColor: "silver"
                    }
                },
                InputSettings { }
            ]

            Entity {
                id: lightEntity
                enabled: true

                DirectionalLight {
                    id: infiniteLight
                    color: "white"
                    intensity: 1.0
                    worldDirection: Qt.vector3d(0, 0, 0)
                }

                Transform {
                    id: infiniteLightTransform
                    translation: Qt.vector3d(0.0, 0.0, 500.0)
                }

                components: [ infiniteLight, infiniteLightTransform ]
            }

            Entity {
                id: cubeEntity

                CuboidMesh {
                    id: cubeMesh
                }

                PhongMaterial {
                    id: cubeMaterial
                    ambient: "red"
                    diffuse: Qt.rgba(1.0, 1.0, 1.0, 1.0)
                }

                Transform {
                    id: cubeTransform

                    property real angle: 0.0
                    property real startAngle: 0.0

                    matrix: {
                        var m = Qt.matrix4x4();
                        m.rotate(cubeTransform.angle, Qt.vector3d(0, 1, 0));
                        m.translate(Qt.vector3d(0, 0, 0));
                        m.scale(windowId.cubeScale, windowId.cubeScale, windowId.cubeScale);
                        return m;
                    }
                }

                components: [ cubeMesh, cubeMaterial, cubeTransform ]
            }

            QQ2.NumberAnimation {
                id: cubeAnimation

                property int startPos: {
                    if (cubeTransform.startAngle === 0)
                    {
                        if (windowId.clockwiseRotation === true)
                            return 0;

                        return 360;
                    }

                    return cubeTransform.startAngle;
                }

                property int endPos: (windowId.clockwiseRotation === true) ? 360 : 0

                target: cubeTransform
                property: "angle"
                duration: windowId.rotationSpeed / windowId.speedFactor
                from: cubeAnimation.startPos
                to: cubeAnimation.endPos
                loops: 1
                running: true

                onStarted: {
                    console.log("onStarted");
                }

                onFinished: {
                    console.log("onFinished");
                    // reset the starting angle
                    cubeTransform.startAngle = 0;

                    // reset the duration of the animation
                    cubeAnimation.duration = windowId.rotationSpeed / windowId.speedFactor;

                    // the animation is currently stopped, run it again
                    cubeAnimation.running = true;
                }
            }
        }
    }

    QQ2.Row {
        anchors.fill: parent

        Button {
            text: "Change Direction"

            onClicked: {
                // pause the animation
                cubeAnimation.pause();

                // invert rotation
                windowId.clockwiseRotation = !windowId.clockwiseRotation;

                // store current angle as the starting angle for the rotation
                cubeTransform.startAngle = cubeTransform.angle;

                // update the remaining time to avoid changing the rotation speed
                let progressPercentage = cubeTransform.startAngle / 360.0;
                if (windowId.clockwiseRotation)
                    progressPercentage = 1.0 - progressPercentage;

                cubeAnimation.duration = (windowId.rotationSpeed / windowId.speedFactor) * progressPercentage;

                // restart the animation from this angle using a new direction
                cubeAnimation.restart();
            }
        }

        Button {
            text: "1x"
            highlighted: (windowId.speedFactor === 1.0)

            onClicked: changeSpeed(1.0)
        }

        Button {
            text: "2x"
            highlighted: (windowId.speedFactor === 2.0)

            onClicked: changeSpeed(2.0)
        }
    }

    function changeSpeed(newSpeed) {
        windowId.speedFactor = newSpeed;

        // pause the animation
        cubeAnimation.pause();

        // store current angle as the starting angle for the rotation
        cubeTransform.startAngle = cubeTransform.angle;

        // update the remaining time to avoid changing the rotation speed
        let progressPercentage = cubeTransform.startAngle / 360.0;
        if (windowId.clockwiseRotation)
            progressPercentage = 1.0 - progressPercentage;

        cubeAnimation.duration = (windowId.rotationSpeed / windowId.speedFactor) * progressPercentage;

        // restart the animation from this angle using a new direction
        cubeAnimation.restart();
    }
}

Related