BGE: scrolling textures with GLSL

Often in games we’ll want to give the appearance of a complex animation by animating 2D images. Such as smoke, explosions, muzzle flashes and tracks/conveyor belts. In the BGE is this typically done by manipulating an object’s UV co-ordinates. In this tutorial we’ll take a look at another way of doing this by writing custom GLSL shaders to manipulate textures. I’ll cover how we can create endless scrolling textures, rotate textures and scroll through sprite sheets. I’ll also touch on using try/except blocks to optimise code. Check out the video to see it all in action:

Before we start

I think it’s important to clear one thing up. We’re not actually UV scrolling. We’re not manipulating the UVs of a mesh in any way. Instead we’re changing how a texture is mapped to a face by the graphics card.

It’s also worth noting that GLSL code is not python, nor is it C or C++. It’s its own domain specific language. While it may have a similar syntax to C/C++ it can only be run and complied by a graphics card. A GPU cannot run C/C++ any more than a CPU can run GLSL code.

Simple endless scroll

Set up

Let’s start by adding a plane and giving it a new material. In the material properties disable backface culling and enable transparency and set the alpha to 0.0. Give this material a new image texture and open up the tracks image. In the influence panel tick alpha to allow the texture to influence the materials alpha values. Make sure the mapping is set to UV.

Now, with the plane selected, enter edit mode and unwrap the plane (u->unwrap) and edit the UVs until you’ve got something you like.

tracksUVSetup

If you can’t see what you’re doing or the transparency is not displaying properly, make sure you’re in textured mode in the view port and shading is set to GLSL in the render properties.

Logic

Lets add 2 game properties to our plane. The first one is called ‘timer’ is is set to a timer. The second one is called ‘rate’, set it to a float and give it the value of 1.0 for now. This will control how fast our texture scrolls and in what direction (-/+). If you find that you’ve set everything up correctly but nothing moves check that rate is not set to zero (this caught me out a few times!).

uvscrollLogicSetup

Then give the plane an always sensor set to pulse mode and attach it to the following python script:

VertexShader = """
    void main()
    {
        gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;
        gl_TexCoord[0] = gl_MultiTexCoord0;
    }
"""
 
FragmentShader = """
uniform float timer;
uniform float rate;
uniform sampler2D texture;

    void main()
    {   
        vec4 colour = texture2D(texture, vec2(gl_TexCoord[0].x, gl_TexCoord[0].y + (timer * rate)));
        gl_FragColor = colour;
    }
"""


import bge 
own = bge.logic.getCurrentController().owner

try:
    own['shader'].setUniform1f('timer', own['timer'])
except KeyError: 
    mesh = own.meshes[0]
    for mat in mesh.materials:
        shader = mat.getShader()
        own['shader'] = shader
        if shader != None:
            if not shader.isValid():
                shader.setSource(VertexShader, FragmentShader, 1)
                shader.setSampler('texture', 0) 
                shader.setUniform1f('rate', own['rate'])

Let’s break down what’s going on here. I’ll start with the GLSL code. The first thing you might notice is that the GLSL is wrapped in quotes. Because GLSL is a complied language we need a way to pass the source code to the compiler. We do this by wrapping it in a string and passing that string to the the shader (line 34). We’re using tripple quotes to indicate its a multi-line string. This also means that if we use quotation marks in our GLSL code they won’t break the string encapsulation.

The first piece of GLSL code is a simple vertex shader so there’s not a lot going on here. Every shader has a main() function that returns nothing (void). In our vertex shader main we multiply vertex position by the view projection matrix. This ensures that our plane’s vertices are displayed correctly. The vertex positions and view matrix are predefined values provided to use by GLSL (denoted by the gl_ prefix). The next line gets the current texture co-ordinates and stores them for passing to the fragment shader for us to use.

The fragment shader controls each pixel is displayed. Before we enter our main() function we define 3 variables. The first 2 (timer and rate) are our game object properties we are going to pass to the shader. The third is our texture. All 3 use the ‘uniform’ qualifier. This tells the graphics card they don’t change throughout the course of the shader’s iteration, ie: they’re read-only.

Next comes where the shader scrolls our texture. We create a vector with 4 components (red, green, blue alpha) and get these values from our texture by passing it look-up co-ordinates (texture2D(texture, vec2(x,y))). Ordinarily these are based on the pixels current position. However, we add the value of our timer variable multiplied by the rate variable to the Y co-ordinate. This allows the texture to scroll along and let us control the rate in which it does. You could try changing the x co-ordinate to scroll on the other axis, or both for diagonal scrolling. You could even go as far as to have different rates for x and y.

Now for the python. In a nut shell, we get the plane’s mesh, grab the attached material and the associated shader. We then assign our new vertex and fragment shader. We are able to pass our game properties by using shader.setUniform1f(‘uniformName’, value). We only need to set rate once, but the timer  uniform will need updating each frame.

Normally I’m not one for over-optimising things, but when it comes to scripts that are going to function as part of a particle or special effects system I like to throttle them so they cost as little as possible to use. This is where the try/except block comes into play. We don’t want to have to keep grabbing the shader each time we want to update the timer uniform, so we store the shader in the game object.

Now we could just do this with an if statement:

if 'shader' in own:
    own['shader'].setUniform1f('timer'), own['timer'])
else:
    # grab the shader and set it

But if statements always have a cost, that’s usually the cost of the look up or check its got to perform. In the above example we’ve got to look if ‘shader’ is stored in the game object, and if it is we’ve got to look it up again to modify it. Since ‘shader’ will always exist after the first frame, that’s an extra unnecessary look up each frame.

Try, on the other hand, has a much smaller cost because we don’t have to check anything first. However, if try fails and we enter the except block it is slower than if/else. So if we know that exceptions are exceptional (ie they don’t occur often) then try/except will be faster. However, if we’re in a situation where either block is just as like then if/else is quicker. In our texture scroller the except block will only ever been entered into once.

You don’t often see try/except blocks in BGE scripts, but they are part of good control flow practice. The python source is littered with them. For example, when iterating through lists, instead of checking if the next value exists python just tries it and keeps going until it hits an error, at which point the exception is handled and the list iteration ends. Again, this is an exceptional circumstance, the list will only end once.

rotating textures

Our set up here is much like before, but instead of a track texture we’re using a simple hub cap texture

hubCap2

The only other difference is that our object has 2 materials, one for the front (the hub cap, UV mapped) and one for the sides (a grey material). So let’s jump straight into the code:

VertexShader = """
    void main()
    {
        gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;
        gl_TexCoord[0] = gl_MultiTexCoord0;
    }
"""
 
FragmentShader = """

    uniform float timer;
    uniform float rate;
    uniform sampler2D texture;

    void main( void )
    {
        vec2 texCoords = vec2(gl_TexCoord[0].x-0.5, gl_TexCoord[0].y-0.5);

        float sinX = sin(timer*rate);
        float cosX = cos(timer*rate);

        mat2 rotMat = mat2( cosX, -sinX, sinX, cosX);
                
        vec2 newCoords=(texCoords*rotMat);
        newCoords += 0.5;
        gl_FragColor = texture2D(texture, newCoords );
    }
"""


import bge

own = bge.logic.getCurrentController().owner

try:
    own['shader'].setUniform1f('timer', own['timer'])
except KeyError: 
    mesh = own.meshes[0]
    for mat in mesh.materials:        
        if mesh.getMaterialName(mat.material_index) == 'MAhub':
            shader = mat.getShader()
            own['shader'] = shader
            if shader != None:
                if not shader.isValid():
                    shader.setSource(VertexShader, FragmentShader, 1)
                    shader.setSampler('texture', 0) 
                    shader.setUniform1f('rate', own['rate'])

Our vertex shader remains the same as the previous example, all we’re changing here is how the fragment is manipulated. The first thing we do with our texture co-ordinates is minus 0.5 from both the x and y. This is because texture co-ordinates are normalised (0.0 to 1.0), with [0.0, 0.0] being the bottom top left, and we need to rotate our texture around the centre. Try removing that transformation or giving new values to change the texture pivot.

Rotation in 2D is defined as x’ = x*cos(theta) – y*sin(theta) and y’ = x*sin(theta) + y*cos(theta). We could do this ourselves, or make life easier and construct a rotation matrix and multiply our texture co-ordinates by that (lines 19-24). We use the timer multiplied by the rate as theta or the angle. Finally, we add 0.5 back to the texture co-ordinates to put them back where they should be so they can be applied to gl_FragColor.

The only difference in the python is the extra if statement on line 42. This makes sure that we’re applying the shader to the right material. Since we don’t know what index has been assigned to each material we must look up the material name based on the material’s index and compare strings. Remember that Blender adds the prefix ‘MA’ to all material names internally. We can use this to apply a custom shader to one material on an object that has several.

scrolling through sprite sheets

Ok, now it’s time for the tough one. A little bit more is involved in the set up here. Let’s start like we have before: create a plane. Apply a material with 0.0 alpha, and add our sprite sheet texture and set it to control the colour and alpha. Notice that the animation starts in the bottom left corner and reads left to right, ending in the top right corner.

explode1

Now, we need to unwrap the plane carefully. We can work out the size our UV map needs to be by dividing our image size by the number of rows and columns. The above image is 256×256 and we’ve got 4×4 images, which means our UV map needs to be 64X64 pixels. The UV map then needs to be placed in the top-left corner to work. You can use the transform properties (press n in the UV image editor) to help you get the positions right.

spriteSetUp

Logic

This time around we’ve got some different properties. Add 3 integers called ‘frame’, ‘cols’ and ‘rows’. Leave frame at 0, but set cols and rows to the number of columns and rows respectively. Add an always sensor and set it to pulse mode. Set the frequency to somewhere between 0-3, this will control how fast the animation plays in the game engine. Now attach the following script:

VertexShader = """
uniform float rate;
    void main()
    {
        gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;
        gl_TexCoord[0] = gl_MultiTexCoord0;

    }
"""
 
FragmentShader = """
uniform int frame;
uniform int cols;
uniform int rows;
uniform sampler2D texture;

   void main()
   {   
        float stepx = 1.0/cols;
        float stepy = 1.0/rows;
        vec2 texCoords = vec2(gl_TexCoord[0].x, gl_TexCoord[0].y);
        
        texCoords.y += stepy * 1;
        texCoords.x += stepx * frame;
        
        texCoords.y += stepy * ((frame - (frame % cols)) / cols);

        gl_FragColor = texture2D(texture, texCoords );        
   }
"""


import bge

own = bge.logic.getCurrentController().owner

try:
    own['shader'].setUniform1i('frame', own['frame'])
    own['frame'] += 1    
except KeyError: 
    mesh = own.meshes[0]
    for mat in mesh.materials:
        shader = mat.getShader()
        own['shader'] = shader
        if shader != None:
            if not shader.isValid():
                shader.setSource(VertexShader, FragmentShader, 1)
                shader.setSampler('texture', 0) 
                own['shader'].setUniform1i('frame', own['frame'])
                own['shader'].setUniform1i('cols', own['cols'])
                own['shader'].setUniform1i('rows', own['rows'])

There’s still no change to the vertex shader, and the python should be familiar by now. So We’ll look more closely at the fragment shader. Just like the simple scroll we’re moving the texture co-ordinates, but this time in steps. The step size if determined by 1/cols and 1/rows. Remember, texture co-ordinates are normalised, so by dividing 1 by the number of steps we need to make we get the normalised step size. Scrolling in the x direction is simple, we just multiply our frame count by our x axis step size. We don’t need to keep track of our x position since the wrap around automatically (1.25 because 0.25). Because of this we don’t need to keep track of our frame value either. So long as it keeps getting incremented all will be fine. The y direction takes a bit more thinking. Basically, we want it to increase in multiples of the amount of columns there are.

The only change in the python side of things is the use of .setUniform1i() instead of .setUniform1f(). This allows us to pass a integer to the shader instead of a float. Check out the API to see all the possible variables you can send to a shader.

Because we have to keep incrementing our frame value. We could also check it to end the object once the animation’s finished should we wish:

try:
    own['shader'].setUniform1i('frame', own['frame'])
    own['frame'] += 1
    if own['frame'] > own['cols'] * own['rows'] - 1:
        own.endObject()     

or perform some other action at the end.

Debugging

Debugging GLSL shaders can be a bit of a pain since we cannot print anything to the console. However, there’s one handy way to debug our sprite sheets. Open up the sprite sheet in your preferred image editor and add a new layer with transparency. Then just write in the frame numbers on top. Update your texture in Blender, then set the always actuator frequency to something like 20 or higher. This way you can see each step taken and where you are in the sprite sheet.

Drawbacks

As with using python to scroll UVs we encounter similar problems with multiple instances of the same object. When python is scrolling UV’s because instances share UV’s you get stuttering. When using GLSL we’re not altering a mesh’s UV co-ordinates, so that problem doesn’t exist. However, because instances share materials they are using the same shader. Consequently, they’ll all instances will run at the same time and texture position.

The solution is the same for both python UV scrolling and GLSL texture co-ordinate scrolling. For each instance you want to use you need a unique copy of the material/object. If you’ve got lots of explosions/smoke/whatever this means lots of copies. In my BGMC entry Planet Smasher I worked around this difficulty by having 14 unique explosions (7 for the player and 7 for the enemies) and would pick one at random when needed. The chance of the same explosion object being picked twice at the same time was low enough that you’d probably not encounter it. A more reliable solution would be to use buckets and check objects in and out, which I’ll cover in my next tutorial on GLSL texture manipulation.

It’s worth noting that with the demo file we don’t see this problem occurring. That’s because all the tank’s scrolling parts are seamless, so there’s no start-end point and therefore wont see strange jumps. However, you might notice that they are oddly synchronised.

Using GLSL texture manipulation has another drawback that python UV scrolling doesn’t: By overriding the default shader our scrolling parts become shadeless (insensitive to lights in the scene). This happens because when we apply a custom shader Blender’s default shader gets overriden. Which means no shadows either.

We can get around this by adding to our texture scrolling shader to include lighting calculations. But at this point it becomes more hassle than just using python to scroll UV’s. For the demo video I put a short light shader into the scrolling parts on the tank, just because it looks better. But a full discussion on how to do that is beyond the scope of this tutorial. This is an excellent resource for those that want to.

So why use this method?

For most applications, there’s probably little point. However, it does provide a nice starting point for doing explosions, flashes and smoke. These sorts of effects can get away with being shadeless. The dirt flowing from the tank tracks is a good example of this.

There are also situations where you would want to actually write your own shader rather than Blender’s default shader, such as when you’re doing water. If that’s the case and you need to scroll UVs, you might as well just use one of these methods.

It’s also more efficient than UV scrolling in python, being compiled rather than interpreted. Well, correction, it still costs performance-wise, but the cost is elsewhere. GLSL runs on the GPU whereas python runs on the CPU. By taking advantage of this we can distribute the load across the two. So if you’ve got a logic heavy game then the graphics card can pick up some of the slack by simulating UV scrolling.

Final remarks

Phew, that was a lot! Despite the length of this tutorial it actually only takes a minute or two to step up each effect and paste in the code. While not a massively useful technique it does have worthwhile applications in explosion, flash and smoke sprite animations where you want to take some strain off the CPU.

Next time we’ll be looking at developing these techniques further and taking advantage of the control GLSL offers us over colour and alpha values to create smoke. Let me know how you’ve applied GLSL texture manipulation to your projects in the comments below. And, as always, feel free to leave your feedback and suggestions.

Resources

Simple scrolling texture .blend

Simple rotating texture .blend

Scrolling through a sprite sheet .blend

Scrolling tank demo .blend

Explosion sprite sheet generator

Advertisements

~ by Jay on March 9, 2015.

One Response to “BGE: scrolling textures with GLSL”

  1. […] [1] Scrolling Textures with GLSL. https://whatjaysaid.wordpress.com/2015/03/09/bge-scrolling-textures-with-glsl […]

    Like

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

 
%d bloggers like this: