Easy breezy beautiful… Metaball?
Go ahead, admit it. You read the title of this post in the voice from the Ad. Didn’t you?
As of late, there really is no animation awesome-er to me than the Metaball effect — apart from Spinny, the result of my last animation-centric post. So, my curiosity decently piqued, I set about trying to create them on iOS.
Metaballs are, in computer graphics, organic-looking n-dimensional objects. In designer’s English, they are shapes that have been gaussian-blurred and masked with a sharp gradient map. In layman’s terms, they’re blurred shapes that have been sharpened somehow so they appear solid.
Basically, when the shapes move close together, they collectively begin to resemble that odd Cornstarch and Water goop that kids love to play with.
An effect like the above can be created in few steps with Adobe Photoshop CC™ — Thanks, Marc! — but on the other hand, such image effects are not so easy to implement on iOS as it doesn’t have the same CoreImage implementation. As such, its coveted filters are consequently unavailable on iOS’ CALayer. 😭
Never fear however, for SpriteKit is here!
With SpriteKit, you can have a whole bunch of shapes (nodes) on screen, and apparently filter the entire scene! I had an inkling that SpriteKit would be the thing to use for this effect and so I set about teaching myself how to get it working.
Ihave always dreamed about writing a game for iOS using one of the available game engines, so this was an exciting prospect for me. I’ve used and loved CoreAnimation since iPhone OS 2. I’ve also — after many creative cuss words—long-since mastered the presentationLayer problem that rears its head upon adding animations to a layer.
Naturally, my first attempt at recreating this effect was with CoreAnimation. I quickly drew a smattering of CAShapeLayer on screen, and tried to use the filters property before being promptly — and rather rudely — informed by Xcode that I couldn’t. It was then that I learned Apple haven’t improved CoreAnimation for iOS to the extent that it interoperates with CoreImage, even though the latter has been available on iOS since 5.0. This is not the case on macOS.
It was then that I commenced researching other technologies, eventually deciding that I could leverage SpriteKit to help me achieve my goal.
SpriteKit however was, until yesterday, a mysterious annal of iOS development that I had yet to explore. I’ll be the first to admit that I’m generally hesitant to stray off the beaten path. I’m of the firm belief that “tried and true” ways to do things are there for a reason, and I will only generally try something new when the moment calls for it. That said, when those moments inevitably roll around, I greatly enjoy attempting to learn whatever it is.
Yesterday was one of those times.
This was my first attempt at introducing my brain to the workings of SpriteKit. Most or all objects belonging to this framework are prefixed with SK. In case you didn’t already know, prefixing classes is a standard Apple practice that around 50% of developers hope to stamp out for good come the release of Swift 3.
Once this appeared to work — there was a black scene on top of a white view — I decided to attempt adding a shape. After a lengthy peruse of the documentation, I decided that SKShapeNode would suit my requirements to-the-tee. So, let’s go ahead and add an SKShapeNode to our “scene.”
Call that function at the end of viewDidLoad() however you like — in a loop or on its own, for example — but we’ll play with just one object for now, to keep things simple.
Important Note (aka Something I Learnt The Hard Way™): The origin point of these SKShapeNode objects is the centre. This is wildly in contrast to everything from UIView to CALayer which all typically have their origin at the top left. I also found that a certain object inside SpriteKit had an origin point at the bottom left. I don’t remember what object it was, but I think it was a bog-standard rect. Perhaps I did it wrong? 😭
In saying that, I realised that actual sprite animation for gaming would be far, far easier with this coordinate system. I even had to stop myself from imagining all the possibilities — after all I was only just learning how to use SpriteKit.
As mentioned above, the Metaball filter can be achieved with Photoshop in a few simple steps. You apply a gaussian blur to the shape layer/s and then use a gradient map to make the shape/s appear solid.
The problem at hand, was how to successfully replicate this with CIFilter. After a little bit of experimentation, I found that the code behind filters could get a bit messy. I didn’t want to use a third-party library for such an easily solvable problem, so I ended up writing a subclass to make applying the filter a 🍰. We’re going to be using a CIGaussianBlur, and the equivalent of a gradient map — CIColorMap.
Let’s write that now.
You’ll also need this image. Save it, name it as per the above, then drag it into your Xcode project.
If you want to reverse the effect, open your preferred image editor and flip the canvas horizontally. That'll ensure you have a black-on-white effect like the top image, as opposed to a white-on-black one. Also colour-up the white side to add some trippy effects if you’re so inclined.
In summary, that filter is first blurring the contents of the scene, and then applying the gradient map — note that it is not applying the filter/s directly to the object/s. This is exactly the same way that Photoshop handles it.
Now let’s apply the effects to the scene. If you feel a slightly more procedural evolution of your CIFilter-ing skills is in order, comment the lines to do with CIColorMap and only return the blurred scene, before you move on to adding in the gradient map.
Pop these two lines at the end of your viewDidLoad() for the sake of convenience.
scene.filter = MetaMetaball()
scene.shouldEnableEffects = true
This Is The Cool Part, I Promise
One really cool part about SpriteKit is that it supports physics. You can relatively easily add gravity and collision detection to objects for example, which are two things I hate attempting with straight-up UIKit.
Because we’re playing with Metaballs, we need to ensure that the balls “pass through” one another, and don’t go pinging off in every which direction when they interact. If you’re having trouble imagining this, just imagine the viral game phenomenon Agar.io. I apologise for the impending severe addiction to that game in advance. 👌
The default behaviour, or so I found when I added two objects to the scene a couple of hours later, is apparently collision with all the things. This is good for perhaps 1% of use cases, so I needed to figure out how to selectively disable collision detection for certain objects while retaining it for others.
Disclaimer: Sadly, a major shortcoming with SpriteKit and this effect, is that combined balls don’t seem to grow in size like they do in Photoshop. 😔
The beautiful part is that collision detection in SpriteKit was easy enough to understand, even for me. Simply put, it seems that a physics body with the same categoryBitMask as something it encounters won't register a collision. I wanted this to only affect the balls, because I planned on having quite a few of them on screen. Go ahead and declare your categories at the top of your view controller as follows:
let wall: UInt32 = 0x1
let ball: UInt32 = 0x1 << 1
It’s an unsigned 32-bit integer for reasons only known to SpriteKit. However before we go happily skipping — or flying, or warping, or everything — through the land of physics and gravity, we’ll need to add the aforementioned physics body to the SKShapeNode so it knows what to do. A quick experiment with adding 200 of these little buggers to the scene made me realise that they could go off-screen pretty easily, so we’ll add some walls too.
Add that to the addShapeNode() function. Now the ball has a physics body, and they won’t collide with each other later on when we try to make them merge! Play with the values, if you want super bouncy, liquidy sexiness then play with the restitution value.
Let’s add the containing walls to the scene. Trust me, losing the balls off screen is rather annoying.
Now we want to play with gravity, right? ’Cause why not?
Essentially, the balls should float freely around the screen, interacting with the walls and pinging off appropriate things to accurately simulate the desired liquidity. So let’s add a gravity rule to the ball itself. Your final addShapeNode() function should look like this:
Ignore the extension part, that’s just for formatting. But keep it if it tickles your fancy.
What fun is a such a thing if we can’t interact with it? They may be “floating freely in space” without a care in the iOS-ecosystem, but they should react when we touch the screen. Before we invoke some good-old bog-standard touchesBegan, touchesEnded and touchesMoved foo, we need to tell the “world” that we want to do this kind of thing.
After a short amount of research, I decided to see if the same radialGravityField could be applied to the scene itself. Another cool discovery was a “drag field” that allows you to simulate resistance through
“air” when you interact with objects. Declare the variables at the top of your view controller, and add the lines of code after them at the end of viewDidLoad().
let radialGravity = SKFieldNode.radialGravityField()
let dragField = SKFieldNode.dragField()
// 0.0, -9.8 correspond with Earth's gravity. Go ahead and test it. It's funny.
scene.physicsWorld.gravity = CGVector(dx: 0, dy: 0)
radialGravity.strength = 0.5
dragField.strength = 0.2
Now you should have a working solution. Add a loop surrounding your ball generation code to add about 20–80 to the scene. Provided you have set everything up exactly as prescribed, you should have a liquid-like substance on your device’s screen to play with! 🎉
The Final Step
Lastly, apply these touch methods to your view controller. Have some creative freedom and play with the values as you see fit. It’s fun to mess with physics!
If you create a whole bunch on the screen (approx. 20–50) it’ll start to behave like true “liquid.” You can have some fun on your own to create some smaller/larger ones and create a dynamic blur effect for them etcetera. But enjoy, and go wild! 👍