August 19, 2022 by Kaj Grönholm | Comments
2022年8月19日由Kaj Grönholm |评论
In this blog post we try to solve the classical "Mouse chasing Mouse" -problem. Don't know it? No problem, nobody does. But if you are interested in Qt Quick, smooth animations and what's new in Qt 6.4 (Beta3 was just released!), please continue reading and you'll find out!
在这篇博文中,我们试图解决经典的“老鼠追老鼠”问题。不知道吗?没问题,没人会。但如果您对Qt Quick、平滑的动画以及Qt 6.4(Beta3刚刚发布!)中的新功能感兴趣,请继续阅读,你会发现的!
When animating a Qt Quick property from A to B with speed X, you usually use some Animation element like PropertyAnimation, NumberAnimation or ColorAnimation. And for combining multiple animations, you can use ParallelAnimation or SequentialAnimation. These are declarative and work great in most cases. Sometimes in the middle of an animation the target changes to C instead (or back to A) and standard animations can handle also this case smoothly. But if C is a moving target, changing frequently or speed X should be adjustable during the animation (and not just some pre-defined easing curve), then these standard Qt Quick animations are not ideal anymore.
当以速度X将Qt Quick属性从A设置为B时,通常使用一些动画元素,如PropertyAnimation、NumberAnimation或ColorAnimation。对于组合多个动画,可以使用ParallelAnimation或SequentialAnimation。这些是声明性的,在大多数情况下都非常有效。有时在动画中间,目标会改为C(或返回到A),标准动画也可以顺利处理这种情况。但是,如果C是一个移动目标,在动画期间频繁变化或速度X应该是可调整的(而不仅仅是一些预定义的缓和曲线),那么这些标准Qt Quick动画不再理想。
Now let's demonstrate this issue with a mouse chasing mouse tester application and present different (more or less optional) ways to solve it.
现在,让我们用一个鼠标追踪鼠标测试仪应用程序来演示这个问题,并给出不同的(或多或少是可选的)解决方法。
When I have property changes and need to animate them, first option that comes to my mind usually is adding a Behavior for those properties. So let's try that first. Attach the mouse position to mouseX and mouseY properties and whenever they change, instead of changing directly to the new values, use Behavior to animate the changes like this:
当我有属性更改并需要设置动画时,我想到的第一个选项通常是为这些属性添加行为。让我们先试试。将鼠标位置附加到mouseX和mouseY属性,每当它们更改时,使用行为来动画化更改,而不是直接更改为新值:
property real mouseX: 0
property real mouseY: 0Behavior on mouseX {NumberAnimation {duration: 1000}
}
Behavior on mouseY {NumberAnimation {duration: 1000}
}
Here is what this solution looks like:
以下是此解决方案的外观:
https://youtu.be/3GdIUYVMwN8
There are a few issues with this approach: As the animations are duration-based, mouse moves faster when the distance is longer and slower when it is shorter. Another issue is that animations are recalculated & restarted whenever the mouse pointer moves, which causes extra CPU usage. This is especially notable when we artificially generate high CPU load → the animations become jumpy. Instead of NumberAnimation it would be possible to use SmoothedAnimation or SpringAnimation but the easings of those animation types don't really suit for this use case and they don't really fix the issues.
这种方法存在一些问题:由于动画是基于持续时间的,所以当距离较长时,鼠标移动得更快,而当距离较短时,鼠标则移动得更慢。另一个问题是,每当鼠标指针移动时,动画都会重新计算和重新启动,这会导致额外的CPU使用。当我们人为地产生高CPU负载时,这一点尤为明显→ 动画变得跳跃。代替NumberAnimation,可以使用SmoothedAnimation或SpringAnimation,但这些动画类型的效果并不适合这个用例,也不能真正解决问题。
As the property animations seem a bit too restrictive for this use case, someone (not me) could consider using Timer with 16ms interval for 60fps animation action. Code for that would look something like this:
由于属性动画对于这个用例来说似乎有点过于严格,所以有人(不是我)可以考虑使用16毫秒间隔的定时器来执行60帧的动画动作。代码如下所示:
Timer {running: truerepeat: trueinterval: 16onTriggered: {var xDelta = mouseArea.mouseX - mouseX;var yDelta = mouseArea.mouseY - mouseY;var length = Math.sqrt(xDelta * xDelta + yDelta * yDelta);var speed = 3.0;if (length > speed) {var xNormalized = xDelta / length;var yNormalized = yDelta / length;mouseX += xNormalized * speed;mouseY += yNormalized * speed;}}
}
And the outcome:
结果是:
https://youtu.be/kDCSs7WQfWY
Initially this Timer approach seems to work pretty well. But it also comes with its own flaws: As we set the interval to 16ms, the timer doesn't match well to non-60Hz animation refresh rates. Also, QML Timers are not really meant for animations and internally they have an extra Qt event loop roundtrip, meaning that they are not as tightly integrated with the animation loop. This is clearly visible when loading the event system e.g. by moving the window → the animation becomes jumpy. Lastly, if the target doesn't reach the intended fps for any reason, the mouse starts to move slower as the speed doesn't have any multiplier taking the animation frame rate into account. We could manually calculate some multiplier e.g. with Javascript Date & getMilliseconds() but that is an extra work we rather avoid if possible.
最初,这种计时器方法似乎工作得很好。但它也有自己的缺陷:当我们将时间间隔设置为16ms时,定时器与非60Hz动画刷新率不匹配。此外,QML定时器实际上并不适用于动画,在内部它们有一个额外的Qt事件循环往返,这意味着它们与动画循环没有紧密集成。这在加载事件系统时(例如通过移动窗口)清晰可见→ 动画变得跳跃。最后,如果目标由于任何原因没有达到预期的帧速率,则鼠标开始移动得较慢,因为速度没有考虑动画帧速率的任何乘数。我们可以手动计算一些乘数,例如使用Javascript Date&getmillizes(),但如果可能的话,我们宁愿避免额外的工作。
Next we will switch to the main topic of this blog post, the new FrameAnimation element. FrameAnimation can be considered as a "custom animation" where you control what happens each time it is triggered. Compared to Timer, FrameAnimation doesn't have repeat or interval properties, since the intervals are always synchronized with the animations and triggered once per Qt Quick animation frame. Source code of the FrameAnimation version is very similar to previous Timer code, with an addition to also rotate the mouse image:
接下来,我们将切换到这篇博客文章的主要主题,新的FrameAnimation元素。FrameAnimation可以被视为“自定义动画”,您可以控制每次触发时发生的情况。与计时器相比,FrameAnimation不具有重复或间隔属性,因为间隔始终与动画同步,并且每Qt Quick animation帧触发一次。FrameAnimation版本的源代码与以前的定时器代码非常相似,除了旋转鼠标图像之外:
FrameAnimation {running: trueonTriggered: {var xDelta = mouseArea.mouseX - mouseX;var yDelta = mouseArea.mouseY - mouseY;var length = Math.sqrt(xDelta * xDelta + yDelta * yDelta);var speed = 3.0 * 60 * frameTime;if (length > speed) {var xNormalized = xDelta / length;var yNormalized = yDelta / length;mouseX += xNormalized * speed;mouseY += yNormalized * speed;var rot = Math.atan2(yDelta, xDelta) - (Math.PI / 2);mouseImage.rotation = rot * (180 / Math.PI);}}
}
And the outcome:
结果是:
https://youtu.be/paClowU51qw
This version is the one to use because FrameAnimation is better synchronized with the animation loop, getting triggered for every animation frame. This way it will adjust to different target screen refresh rates when using the threaded render loop with vsync-based throttling. You can also use frameTime (or smoothFrameTime) property as a multiplier and your animation speed will then adjust to different target fps, taking into account possible missed frames. Actually, FrameAnimation with smoothFrameTime property is used in this example also for the simple frame time & rate information, which is good enough for our needs to show when the animation speed drops:
使用此版本是因为FrameAnimation与动画循环更好地同步,每个动画帧都会触发。这样,当使用基于vsync的线程渲染循环时,它将调整到不同的目标屏幕刷新率。您还可以使用frameTime(或smoothFrameTime)属性作为乘数,然后将动画速度调整到不同的目标fps,同时考虑可能丢失的帧。实际上,本示例中还使用了具有smoothFrameTime属性的FrameAnimation,用于简单的帧时间和速率信息,这足以满足我们在动画速度下降时显示的需要:
Text {text: fpsHelper.ft + " ms, " + fpsHelper.fps + " fps"FrameAnimation {id: fpsHelperreadonly property int fps: smoothFrameTime > 0 ? Math.round(1.0 / smoothFrameTime) : 0readonly property string ft: (1000 * smoothFrameTime).toFixed(1)running: true}
}
Now you might think "OK, this FrameAnimation can do stuff but shouldn't we avoid QML Javascript code for optimal performance?". This was true in the past but not as much anymore. With Qt 6, a lot of work has been done to optimize the QML engine and the new Qt Quick Compiler even compiles QML Javascript to native C++ code. For more details, see the Qt Quick Compiler performance blog post and the related optimization series posts. So while you should still keep the frontend UI layer (QML) and the backend logic (C++) separate and have most of the imperative code in C++ side, don't be afraid of using QML scripts a bit more for UI related things.
现在您可能会想“好吧,这个框架动画可以做一些事情,但我们不应该避免QML Javascript代码以获得最佳性能吗?”。这在过去是真实的,但现在不再如此。在Qt6中,已经做了大量工作来优化QML引擎,新的QtQuick编译器甚至将QML Javascript编译为本地C++代码。有关详细信息,请参阅Qt Quick编译器性能博客和相关优化系列文章。因此,虽然您仍然应该将前端UI层(QML)和后端逻辑(C++)分开,并将大部分命令代码放在C++端,但不要害怕将QML脚本更多地用于与UI相关的事情。
The example used in this blog post was just a quick tester app, but FrameAnimation can of course also be used to do much nicer things (which I will blog about soonish!). But in conclusion for this post: If you are using Qt 6.4 (or newer) and have a need for fully custom Qt Quick animations, consider using the new FrameAnimation element.
这篇博文中使用的示例只是一个快速测试应用程序,但FrameAnimation当然也可以用来做更好的事情(我将在soonish的博客上讨论)。但在这篇文章的最后:如果您使用的是Qt 6.4(或更高版本),并且需要完全自定义的Qt Quick动画,请考虑使用新的FrameAnimation元素。