Practical Fragment Shaders in Flutter | Guide – Generative Art Part 2
Welcome to part 3 of our Guide! It’s time to add some animation to our art and discover how fragment shaders can boost performance.
Table of contents
Now that you have learned how using math is essential for writing awesome GLSL shader effects in Flutter, let’s learn the performance benefits fragment shaders give us over the CPU-based rendering methods. This time, it’s all about making our art animated!
Without further ado, let’s get coding!
Unless, of course, you want to go back and look at the previous lessons:
So, how do we go about animating something? Well, an animation is just a bunch of images glued together and displayed over some period of time. We know how to generate the images already, so all we need to do now is add some time into the equation. And if you’ve ever animated any widget in Flutter, you know just the right tool to use for that!
I’m talking about the Ticker class, of course.
All this handy little API does is, every time there’s a new frame being drawn by Flutter, call a function we provide to it. And that function of ours will measure the elapsed time which we’ll then use in the shader.
Tickers are fairly commonly used in Flutter, you’re probably most familiar with using
TickerProviders instead of
Tickers directly – for example adding
SingleTickerProviderStateMixin to your widget’s state and then passing
vsync: this into some animation controller. This time it’s a little different – we’re going to be handling our ticker manually.
So let’s start with writing a widget that will get us our ticker:
As you can see, we’re still using
SingleTickerProviderStateMixin – that’s because it gives us a convenient
createTicker() method, which makes things somewhat less complicated. We use that method in
initState, where we assign the constructed ticker to a variable, so that we can later call the
dispose() method when it’s not needed anymore. Always remember to dispose of all objects that require that, or else your code will be prone to memory leaks!
createTicker() method, we need to pass a function that the ticker will call every time it ticks. Every call we’ll get a
Duration object that will contain a value representing time elapsed since starting the ticker. Save that time to a variable – we’ll use it in a second – and call
setState() in order to force the widget to rebuild, causing our shader to redraw.
Finally, don’t forget to start the ticker at the end of
And a painter that will draw whatever the shader outputs:
Here, we take the
_currentTime containing our ticker’s output and pass it to the painter. The painter then converts it from
Duration to a number, representing milliseconds, and passes it as the third float variable into the shader.
And inside the shader:
The new additions are the
uniform float time variable, and the
TIME_SCALE constant, which is needed because we’re using milliseconds to measure the time, so the numbers will get quite big quite fast.
Next, we take both of those components and combine them into a new variable called
scaledTime that is then used as an additional factor modifying the inputs of our math functions.
And that’s it! Let’s see what we get!
Now is a great time to finally talk about why we even bother with writing shaders instead of just sticking with good old custom painters. While custom painters are great for every-day stuff like custom small-scale animations, or even displaying SVG pictures in a performant way, they’re not really designed to be used for drawing pixel-by-pixel generated rasters, which is what we’re doing here. The Canvas API that is used inside custom painters doesn’t even have a method for drawing a single pixel! The best we can do is use the
drawCircle method with a radius of 1.
So what would happen if we tried to pull off the same things we did in our shader, only using a custom painter? Let’s see!
The code above is pretty much a direct port of our GLSL shader code to Dart. Let’s run it and see how it works!
And in case you’re wondering – there’s nothing wrong with your internet connection – the framerate actually is that bad when we’re using a custom painter.
Just to be absolutely sure, let’s look at the performance tab inside Flutter DevTools:
As you can see, while using a custom shader, we’re easily rocking 60 frames per second, while the custom painter version is struggling to keep up with 7 frames per second. Of course, this is not a very scientific test and the results might vary depending on your hardware, but I think it’s still a great example of the scale of the performance gains shaders give us.
And that’s just for drawing a couple of sin waves! Imagine what would happen with more sophisticated shaders!
Actually, you don’t have to imagine. Let’s try and check ourselves! Let’s borrow this WARP shader from shadertoy.com. This time I won’t bore you with the details of rewriting it to work with Flutter APIs – you can check it out on my GitHub repo. Here’s what the shader runs in a Flutter app:
And here are the performance results:
Pretty consistent with our previous results, right?
And this consistency also tells us something new – while the intuitive thing would be to assume the cause for performance loss is that we’re computing each pixel’s value individually on the UI thread of the app, that might not actually be entirely true. It’s likely the sheer amount of individual draw calls we’re making is overwhelming Flutter and causing frame drops.
That being said, even if that wasn’t the case, shaders would still outperform UI-thread drawing for the simple reason that they’re processed in parallel on multiple cores of the GPU, instead of a single CPU thread.
It took us 3 parts of the series, but we finally got to a point where we can prove why custom shaders in Flutter are useful and worth exploring. You now possess another tool in your arsenal that will let you create beautiful and fluid animations and graphical effects in your Flutter projects.
But we’re not done just yet! In the next part we’ll think outside the… smartphone, and learn how shaders can help us when we want to utilize the device’s camera.
Until then, as always, feel free to take a look at the code in the GitHub repo and reach out to me with any questions or comments!