Working with shaders in LibGDX

Let's now turn our attention to the topic of shaders. This is a feature that is available in OpenGL (ES) 2.0 and above as it makes use of the so-called Programmable Pipeline. Shaders are usually small programs, which allow us to take over control of certain stages in the rendering process to define the way a scene should be rendered by the graphics processor. In consequence, shaders are an important building block in today's computer graphics and are also an extremely powerful tool to create all sorts of (special) effects that would be very hard to realize otherwise. For the sake of simplicity, we will only discuss vertex and fragment shaders here.

Note

Fragment shaders are also called pixel shaders. Unfortunately, this is a bit misleading as this type of shader actually operates on fragments instead of pixels.

Consider the following list of reasons as to why shaders are generally useful and highly recommended to be in the toolkit of every (graphics) programmer:

  • Programmability of the GPU rendering pipeline via shaders to create arbitrary complex effects. This means a high degree of flexibility for all sorts of special effects expressible through mathematical formulas.
  • Shaders are run on the GPU, which saves the CPU time that can be spent on other tasks, such as doing physics and general game logic.
  • Heavy mathematical computations are usually done faster on GPUs than on CPUs.
  • GPUs are able to parallelize the processing of vertices and fragments.

Vertex shaders operate on each vertex given to the GPU. A vertex is a point in 3D space with attributes, such as a position, a color, and texture coordinates. These values can then be manipulated through the shader to achieve effects, for example, the deformation of an object. The output for each vertex computed by the vertex shader is then passed along in the rendering pipeline as the input for the next rendering stage.

Fragment shaders compute the color of a pixel for each fragment. For this, many factors may be taken into account to simulate different kinds of materials. Some of them are values for lighting, translucency, shadows, and so on.

The combination of a vertex and a fragment shader is called a shader program. Shaders are usually written in an API-specific, high-level language, such as OpenGL Shading Language (GLSL) for OpenGL, which uses C-like code syntax. Take a look at the last two pages of the OpenGL ES 2.0 Reference Card at http://www.khronos.org/opengles/sdk/docs/reference_cards/OpenGL-ES-2_0-Reference-card.pdf.

This should give a quick overview of the available feature set in GLSL. For more details about the specifications of OpenGL (ES) 2.0, GLSL, as well as the Programmable Pipeline, check out the official website of the Khronos Group at http://www.khronos.org/opengles/2_X/.

Also, the following list contains some links to the GLSL tutorials and websites with collections of shader examples ranging from beginners to experts:

Creating a monochrome filter shader program

We now want to create a new pair of vertex and fragment shaders to form a shader program. Its purpose will be to act as a color filter that renders everything it has applied to in a beautiful grayscale.

Let's start with the vertex shader. Create a new subdirectory in CanyonBunny-android/assets named shaders. Then, create a new file monochrome.vs in the shaders directory, and add the following code:

attribute vec4 a_position;
attribute vec4 a_color;
attribute vec2 a_texCoord0;
varying vec4 v_color;
varying vec2 v_texCoords;
uniform mat4 u_projTrans;

void main() {
v_color = a_color;
v_texCoords = a_texCoord0;
gl_Position = u_projTrans * a_position;
}

The first six lines in the vertex shader declare the variables of different data types using the so-called storage qualifiers in terms of GLSL. The data typesvec2 and vec4 stand for the float vectors with two and four components, respectively, while mat4 stands for a square matrix of order four of floats. The attribute qualifier is only available in the vertex shader and denotes the inputs that are passed from the vertex arrays sent by the application. As mentioned before, these inputs are the position, color, and texture coordinates of one vertex at a time. The varying qualifier denotes the variables that are readable and writeable in the context of the vertex shader but read only to the fragment shader. Thus, the variables using this qualifier allow the transfer of additional information from the vertex shader to the fragment shader. Last but not least, the uniform qualifier denotes the variables that are known to be constant during multiple executions of the shader in the same draw call. The variable u_projTrans is automatically set to the combined projection-model view matrix by the LibGDX's Spritebatch class.

The next step is the declaration of the main() function. It is the entry point of the shader where the execution begins. In this vertex shader, we pass the input values of the color and texture coordinates via varying variables to the fragment shader for later use. The variable gl_Position is a predefined GLSL output variable (refer to GLES2 Reference Card) that holds the projected position vector and is computed by the multiplication of the combined projection-model view matrix (u_projTrans) and the position vector (a_position) of the current vertex.

Next, create another new file monochrome.fs in the shaders directory and add the following code:

#ifdef GL_ES
precision mediump float;
#endif
varying vec4 v_color;
varying vec2 v_texCoords;
uniform sampler2D u_texture;
uniform float u_amount;

void main() {
vec4 color = v_color * texture2D(u_texture, v_texCoords);
float grayscale = dot(color.rgb, vec3(0.222, 0.707, 0.071));
color.rgb = mix(color.rgb, vec3(grayscale), u_amount);
gl_FragColor = color;
}

In the first three lines, we use a GLSL macro to set precision of the float values to medium for devices that use OpenGL ES. In the next lines, you may have already recognized our two varying variables that we pass from within our vertex shader to this one.

Note

Be sure to always match the name and data type of each varying variable passed from a vertex shader to the corresponding fragment shader.

The data type sampler2D stands for a two-dimensional texture. The u_amount variable is meant to be set by the application code to control the amount of grayscale that should be applied. The first line inside the main() function of the fragment shader computes a combined color value between the original vertex color and the color value of the texture referenced in u_texture using the coordinates passed in v_texCoords. Now, to find an appropriate grayscale value, we compute the dot product of our color vector and another vector containing varying color weights that match best with the sensitivity of a typical human eyesight as suggested in Chapter 22.3.1, Grayscale Conversion of the book GPU Gems, Randima (Randy) Fernando, Addison Wesley.

This book is publicly available on NVDIA's developer zone at http://http.developer.nvidia.com/GPUGems/gpugems_ch22.html.

Then, we use the mix() function, which applies a linear interpolation between the original color vector and the grayscale vector using the u_amount variable. The gl_FragColor variable is another predefined output variable of GLSL that determines the final pixel color of the fragment that is currently being processed.

Using the monochrome filter shader program in Canyon Bunny

Now that we have created our monochrome filter shader program, it is time to put it to use in Canyon Bunny. We want to use the shader's effect in the game screen, and apply it to the game world. This means that the GUI will remain colored like before. In addition to this, the shader program can be switched on and off with a new checkbox added to the debug section of the menu screen's Options dialog.

First, add these new constants to the Constants class that point to the files of our new shader program:

// Shader
public static final String shaderMonochromeVertex = "shaders/monochrome.vs";
public static final String shaderMonochromeFragment = "shaders/monochrome.fs";

After this, add the following line to the GamePreferences class:

public boolean useMonochromeShader;

Next, make the following changes to the same class:

public void load () {
showFpsCounter = prefs.getBoolean("showFpsCounter", false);
useMonochromeShader = prefs.getBoolean("useMonochromeShader", false);
}

public void save () {
prefs.putBoolean("showFpsCounter", showFpsCounter);
prefs.putBoolean("useMonochromeShader", useMonochromeShader);
prefs.flush();
}

Then, add the following line to the MenuScreen class:

private CheckBox chkUseMonoChromeShader;

After this, make the following changes to the same class:

private Table buildOptWinDebug () {
  Table tbl = new Table();
  // + Title: "Debug"

  // + Checkbox, "Show FPS Counter" label
  // + Checkbox, "Use Monochrome Shader" label
chkUseMonochromeShader = new CheckBox("", skinLibgdx);
tbl.add(new Label("Use Monochrome Shader", skinLibgdx));
tbl.add(chkUseMonochromeShader);
tbl.row();
return tbl;  
}

private void loadSettings () {
chkShowFpsCounter.setChecked(prefs.showFpsCounter);
chkUseMonochromeShader.setChecked(prefs.useMonochromeShader);
}

private void saveSettings () {
prefs.showFpsCounter = chkShowFpsCounter.isChecked();
prefs.useMonochromeShader = chkUseMonochromeShader.isChecked();
prefs.save();
}

The last modifications to GamePreferences and MenuScreen just extended the settings handling by a new Boolean flag, which can now be toggled via the new checkbox added to the debug section of the Options dialog in the menu screen.

Next, add the following import lines to the WorldRenderer class:

import com.badlogic.gdx.graphics.glutils.ShaderProgram;
import com.badlogic.gdx.utils.GdxRuntimeException;

After this, add the following line to the same class:

private ShaderProgram shaderMonochrome;

Also make the following changes to the same class:

private void init () {
batch = new SpriteBatch();
camera = new OrthographicCamera(Constants.VIEWPORT_WIDTH, Constants.VIEWPORT_HEIGHT);
camera.position.set(0, 0, 0);
camera.update();
cameraGUI = new OrthographicCamera(Constants.VIEWPORT_GUI_WIDTH, Constants.VIEWPORT_GUI_HEIGHT);
cameraGUI.position.set(0, 0, 0);
cameraGUI.setToOrtho(true); // flip y-axis
cameraGUI.update();
  b2debugRenderer = new Box2DDebugRenderer();
shaderMonochrome = new ShaderProgram(Gdx.files.internal(Constants.shaderMonochromeVertex), Gdx.files.internal(Constants.shaderMonochromeFragment));
if (!shaderMonochrome.isCompiled()) {
    String msg = "Could not compile shader program: "+ shaderMonochrome.getLog();
throw new GdxRuntimeException(msg);
    }
  }

private void renderWorld (SpriteBatch batch) {
worldController.cameraHelper.applyTo(camera);
batch.setProjectionMatrix(camera.combined);
batch.begin();
if (GamePreferences.instance.useMonochromeShader) {
batch.setShader(shaderMonochrome);
shaderMonochrome.setUniformf("u_amount", 1.0f);
    }
worldController.level.render(batch);
batch.setShader(null);
batch.end();
if (DEBUG_DRAW_BOX2D_WORLD) {
b2debugRenderer.render(worldController.b2world, camera.combined);
    }
  }

@Override
public void dispose () {
batch.dispose();
shaderMonochrome.dispose();
}

To load and initialize our shader program in the init() method, we pass both the shader files to the constructor of a new instance of ShaderProgram, which are then stored in the shaderMonochrome variable for later reference. It is good practice to ask whether a new shader program instance could be successfully compiled by calling its isCompiled() method. If this is not the case, the corresponding log message of the compile error can be retrieved by calling the getLog() method.

In the renderWorld() method, we wrapped the call that renders the actual game world with two setShader() calls. The first call activates our monochrome filter shader program while the next call passes null for the shader, which makes SpriteBatch switch back to LibGDX's default shader. Calling the setUniformf() method of an instance of a shader program allows us to set a float value for a uniform variable by a name. There are many more of these setter methods for different combinations of storage qualifiers and data types. We set the value of the uniform float variable named u_amount to the value 1.0f. According to our code in the fragment shader, the linear interpolation will apply the grayscale effect in full to its target pixels.

Finally, we take care that the shader program's dispose() method is called to free any allocated memory when it is no longer needed.

Now, run the game, go to the Options dialog, and tick the checkbox to activate the monochrome filter shader program. The game screen should show up in a beautiful grayscale tone as soon as the game is started. A screenshot of the game with the enabled monochrome filter shader program is as follows:

Using the monochrome filter shader program in Canyon Bunny

Note

The blue background color, which is actually created using OpenGL's clear color, is not affected by our shader program. The reason behind this is that the shader's effect is only applied during the rendering of the game world where it is temporarily set in the sprite batch. Moreover, the game world is rendered on top of the (blue) clear color, which back then appeared to be a good idea just because it was an easy and cheap way of making the clear color a part of the scene and get a sky for free.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.145.87.161