Shaders Overview - Part 2

Posted by Mike Dailly on 17 June 2013

In the first part of this overview, we looked at what shaders were, and how they have been integrated into Studio, so in this part, we're going to create a simple shader and see how we'll actually use them.

First, lets take a quick look at the basic default variables Studio sets up, and inputs it uses. If you remember from part one, I talked about vertex data and what this was, so in this first section we'll look at how this data gets into a shader.

attribute vec3 in_Position;
attribute vec4 in_Colour;
attribute vec2 in_TextureCoord;
attribute vec3 in_Normal;

Adding these lines to the top of your vertex shader declares the "inputs" to the shader. Note a vec4 contains 4 floating point values in a single variable (in_Colour.argb for example), while a vec3 contains 3 floats (in_Position.xyz), and vec2, 2 floats (in_TextureCoord.xy). vec4 is the largest size (aside from arrays), but you can have just a single float if you need to. 

These are the values your shader has to work with, and the must be called these names in order for Studio to map certain inputs correctly across platform. It also has the added advantage that when sharing shaders between members in th community, there is some common ground for inputs.

Now, as long as these are at the top, Studio will match up position, colour, texture coordinates and normals as you'd expect, but if you don't use one (like the normal), you don't need to include it.

Now that we've declared the inputs vertex shader, we need to declare the outputs as well - these are the values passed onto the fragment shader.

varying vec2 v_vTexcoord;
varying vec4 v_vColour;

In this case, our vertex shader is going to send a texture coorinate and a colour to the fragment shader, these two lines will also be at the top of our fragment shader.

Each shader must have a main() function, this is the main entry point for each vertex and fragment shaders. We can define an empty function like this....

void main()
{
}

This will of course, not do very much, and may well in fact throw an error, because inside a vertex shader you must store the output into the GLSL built-in variable gl_Position. (see http://www.khronos.org/opengles/sdk/docs/manglsl/ for more details on built in variables). So lets take a look at what a very simple  "pass through" vertex shader would look like.

//
// Simple passthrough vertex shader
//
attribute vec3 in_Position;
attribute vec4 in_Colour;
attribute vec2 in_TextureCoord;

varying vec2 v_vTexcoord;
varying vec4 v_vColour;

void main()
{
vec4 pos= vec4( in_Position, 1.0);
gl_Position = gm_Matrices[MATRIX_WORLD_VIEW_PROJECTION] * pos;

v_vColour = in_Colour;
v_vTexcoord = in_TextureCoord;
}

As you can see, we first copy the positional information into a new variable pos. The reason for the copy, is so that we can expand it to fill a full vec4. Once we have the position into a usable format, we then multiply it by the world*view*projection matrix, and this gives us the final clip space position (the position the hardware actually wants). Studio provides a whole heap of ready to use matrices, taking the pain out of having to set them up yourself, or provide them to every shader you do. Each of of these can be used for different effects, but if you're simply transforming them to go to the screen, you'll probably use the line above.

The last 2 lines are simply where we copy the colour and texture coordinates from the input vertex data, to the output, ready to be passed to the fragment shader.

Now lets take a look at the fragment shader, since it's a pass through shader, this is increadibly straight forward.

//
// Simple passthrough fragment shader
//
varying vec2 v_vTexcoord;
varying vec4 v_vColour;

void main()
{
gl_FragColor = v_vColour * texture2D( gm_BaseTexture, v_vTexcoord );
}

At the top, we have our two inputs from the pixel shader, and this is used along side another builin constant: gm_BaseTexture. All sprites, backgrounds, surfaces etc. all reside on textures, so whenever you draw anything, you specify a texture (in some shape or form), and when you use a shader, Studio sets the variable to be the texture your drawing with. This again lifts the strain of setting up shaders, and helps you hit the ground running.

So, what does this line shader actually do? Well, the function texture2D() is a GLSL ES shader function, and it looks up a pixel in a texture using the UVs provided. So, it looks up the texture (your sprite/background/stuface etc) using the UVs provided fom the vertex, and returns the ARGB pixel from there. It then multiplys this with the vertex colour - allowing you to tint the pixel with a colour. This is then passed to the GLSL built in variable gl_FragColor for the hardware to render.

And that's it as far as shaders go! So how would you use this? Well, it couldn't be easier really....

shader_set(PassThroughShader);
draw_self();
shader_reset();

If we call our shader PassThroughShader, then simply setting the shader, drawing the sprite, and resetting it, will send everything through it, although it'll look the same - for now!

passthrough shader

Now comes the fun part. Since we're now going through a shader, lets see what happens if we simply store a value instead.

void main()
{
gl_FragColor = vec4( 1,1,1,0.5 );
}

If you now run this, you should simply get a slightly transparent (the 0.5 value), solid white square (as shown below)

Solid white passthrough shader

So, lets mix things up a little... Vectors are cool little things, stuctures with x,y,z and w (or r,g,b and a) components, and you can read/write from/to each however you like. Lets clear the blue and green channels, and see what happens. Add this line at the bottom of the original fragment shader.

gl_FragColor.bg = vec2(0,0);

This sets the blue and green channels to zero, leaving only the red and alpha channels active.

Red passthrough

As you can see, we're left with a red version of the sprite. if we also set the ALPHA channel to 0, we'd get the same image as this, but a black box around the sprite.

gl_FragColor.a = 0.0;

no alpha passthrough 

 In the next part, we'll take a closer look at how the hardware actually handles shaders, and how they get so fast!