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