If you’re new to game development, you’ve probably heard of shaders but don’t quite understand them. If you’re new to SpriteKit, you’ve probably hit a few speed bumps working with shaders. Since I’m still learning more about the two, I figured it would be nice to put up a concrete example that covers cropping and effects using shaders. Specifically, we’ll cover two concepts:
- Using alpha, black, and white pixel values to determine which pixels to display.
- Porting shaders found on Shadertoy into SpriteKit.
Here is what it’ll look like when we’re done.
If you have never worked with a shader in SpriteKit before, you should probably check out this quick introductory tutorial. And if you’re interested in shaders, but not SpriteKit, you may still find a thing or two that grabs you’re attention.
The first thing to do is find an image that you would like to mask or modify in some way. For this example, I’ll be using a circular health bar that wraps around a shield graphic. Note the image is a .png, and that in between the black lines along with the outside is fully transparent. We’ll try to satisfy three requirements.
- Only show health in between the two black lines outlining the image.
- Allow for the health section to be partially full.
- Add movement to the health bar.
A Basic Shader
To apply a basic shader to our image, we’ll need to tell SpriteKit to attach a shader to the SKSpriteNode, and we’ll also need to create an empty text file ending in .fsh that contains our shader code. First, the shader code:
Next, attach the shader in SpriteKit:
As you can see to the right, the shader turns every pixel green. This is because
main() is the entry point, and by default
gl_FragColor is the return value. So, for each pixel of the image, this snippet of code runs. And when the code runs, it is telling each pixel that the color should be green (note the r,g,b,a value of the
Detecting Alpha Values
Now that we can change the color of pixels, the next step is to only change the color of fully transparent pixels. We’ll change our shader code to the following:
Now, our image fully displays, and all transparent pixels have been replaced with green pixels. Specifically, we create
val which contains the pixel in the actual texture. The alpha value (
val.a) is transparent if it equals 0.0. So, we either replace the pixel with a green one, or set the return value to the pixel from the actual texture.
Masking & Gradients
The obvious problem with the previous step is that we have to get rid of everything outside of the circle. We only want to fill the inside. In addition, we want the inside to fill only a percentage equal to the health of the player. We’re going to add a separate image to act as both an image mask, and a gradient fill.
First, let’s talk about the mask. This circle is the exact same size as the shield above. So, if a pixel falls inside of both the circle and the shield, we know we want to draw it. If it falls outside, we can get rid of it. That’s how we’ll achieve the health bar look.
Second, let’s talk about the gradient. Notice that a radial gradient has been applied with pure black meeting pure white at the top. The general idea here is that we can check pixel colors. Imagine a player with health of 75%. We can check all pixels that are at least 75% black, and turn them on. Anything else gets turned off.
To make these two concepts, we’ll have to attach SKUniform’s to the shader. Think of this as passing variables through to the shader. In this case, we’ll need two variables. One for health, and one for the image above. Our Swift code should now look like:
We’re basically supplying a float and SKTexture to the shader. Then, we can modify the shader to produce our desired result.
Here is the result:
A few notes on the inline code comments:
 At the beginning of this tutorial, we checked
val.a == 0.0 and now we’re checking
val.a less than 0.1. This is because there are partially transparent pixels around the edges of the circle that we’re being excluded. A limit of 0.1 set the best result.
 You can test the black value of a pixel against any of the r, g, b values of the vec4. I just used
.r because that is what I’ve seen the most.
 Similar to , I could have check the mask for pixels that are fully visible where
grad.a == 1.0, but a tolerance was needed to produce the best results.
Porting From Shadertoy
When browsing Shadertoy, you’ll notice certain variables that aren’t available to you, or that break your script. Here are the most common ones I’ve seen, and how to use them in SpriteKit.
|iChannelX||SKUniform with name of “iChannelX” containing SKTexture|
Using Existing Animation Examples
Now that we know how to port from Shadertoy, let’s try it with a few examples.
Example 1: Bullseye
Example 2: Wobble Spiral
Example 3: Glowing Thing
Further Reading & Next Steps
- Making a Pixel Shader for iOS8 with SpriteKit
- Hub.ae’s shader intro and bloom/blur tutorial.
- SKShader documentation.
- WWDC14 PDF
- OpenGL ES 2.0 API Quick Reference Card
- Alternative to Shadertoy: GLSLSandbox
Beyond this, I’m still interested in the performance of shaders, and confidently knowing when to use them. For example, the 6th shader in the original gif above starts to lag a bit if you leave it running too long. Is it because of poor code, or is it too intensive? Similarly, would it be better to use 20 static images to represent health chunks of the circle, and just manage the health bar in code? I’ve often heard that using shaders is the “right way”, but I’d like to know with certainty when that is true. Perhaps another part to this tutorial is in order.