Tutorial Stream: Intro to Shaders
Today at 4:30pm PST, I’ll be streaming a tutorial about how to use shaders in Castle.
What is a shader?
A shader is a small program that runs on your GPU. It’s more limited than a normal computer program but the advantage is that the GPU can run it extremely quickly. When you draw an image on the screen the shader program has two jobs. First, the vertex shader decides where on the screen the image should be drawn. Second, the pixel shader decides the color and opacity for each pixel in the region that the vertex shader selected. We’ll focus on the pixel shader for this tutorial since it can produce more interesting effects.
Trying shaders in Castle
I built a small game in Castle for learning about shaders. Open this game in Castle to try: https://castle.games/+mol8o0/@jesse/shader-tutorial
The source is at https://github.com/jesseruder/castle-shader-tutorial
Once you’re in the game you’ll see a text input. This is the source code for the shader and will get recompiled when you click “Update”. The default shader simply renders the image as you’d normally expect.
As you can see the source code contains one function name effect
. This is the pixel shader that we talked about above, and the return value is used for the color and opacity of a single pixel. Let’s try making the image completely red. Copy this into the text field and click “Update”:
vec4 effect(vec4 color, Image tex, vec2 texture_coords, vec2 screen_coords)
{
return vec4(1.0, 0.0, 0.0, 1.0);
}
This new shader ignores the image texture and just returns a constant color for every pixel. The first three numbers the vec4
represent the RGB color and then last number represents the opacity. If we change the last number to 0
we’ll see that the square disappears and we just see the black background instead.
Here’s an example of rotating the image using just the pixel shader:
vec4 effect(vec4 color, Image tex, vec2 texture_coords, vec2 screen_coords)
{
vec2 newTexCoords = vec2(texture_coords.y, texture_coords.x);
vec4 texturecolor = Texel(tex, newTexCoords);
return texturecolor * color;
}
Sending variables to the shader
There are a few different ways of getting data into shaders. In the basic shader we get the image texture as an argument to the effect
function. But what if we want to send in more data?
Shaders have a type of variable called a “uniform”. Uniform in this case means that the variable has a uniform value over every invocation of the shader function during a single frame. I added a uniform variable named time
to the example that can be enabled by toggling the checkbox. Try selecting the checkbox and then using this shader:
uniform highp float time;vec4 effect(vec4 color, Image tex, vec2 texture_coords, vec2 screen_coords)
{
vec2 newTexCoords = vec2(texture_coords.x + sin(time), texture_coords.y);
vec4 texturecolor = Texel(tex, newTexCoords);
return texturecolor * color;
}
The time
variable contains the seconds elapsed since clicking the “Update” button. In this shader we pass that into the sin
function, which means that the x coordinate of the texture selected for each pixel changes from [-1 + original x coordinate, 1 + original x coordinate] over time.
Next steps
There are a lot of different effects you can build with shaders. You can add blur, add CRT scan lines, distort the shape of the screen, or anything else you can imagine! I’ll work through some more examples of different effects on the stream.