Category: Flutter, Blog, Development

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.

Practical Fragment Shaders in Flutter Guide

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!

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

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!

fragment shaders loop scaledTime

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!

flutter fragment shaders GLSL shader code to Dart

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:

Flutter DevTools Custom Painter and Shader Performance

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:

flutter wrap shader

And here are the performance results:

Performance results of custom painter and shader

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

Tymoteusz Buczkowski

Tymoteusz Buczkowski

Flutter Team Tech Officer

A Flutter Developer with 6 years of software development experience. DroidsOnRoids’ Flutter Group Tech Officer.

He’s been with Flutter pretty much since Flutter’s 1.0 release and ever since is on a mission to deliver the best Flutter apps in the world.

Before Flutter, he worked as a full-stack software engineer, creating mobile apps, web apps and back-end systems in the insurance and banking industry.

A passionate gamer and game developer, he will never pass an opportunity to take part in a gamejam. Interested in everything related to new technologies and widely understood engineering. Loves to go to a racetrack and race in his Mazda Miata after hours.