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:
Passing time
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.
Even though Ticker
s are fairly commonly used in Flutter, you’re probably most familiar with using TickerProvider
s instead of Ticker
s 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!
Into the 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 initState()
method.
Now, the build()
method:
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!
Performance anxiety
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.
Conclusion
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. Check it out here: Practical Fragment Shaders in Flutter | Guide – Shading Widgets
Also, feel free to take a look at the code in the GitHub repo and reach out to me with any questions or comments!
More lessons on Practical Fragment Shaders in Flutter | Guide:
About the authors
Hire experienced Flutter developers to create your next app!
Our product development process ensures your goals will be reached in a fast and predictable way