Apr 26, 2021, Mobile

OpenGL on Android – using shaders

Tomasz Kądziołka Flutter Developer

In this short series of articles I will try to present a basic Android application that uses OpenGL and shows simple pitch with an interactive soccer ball :). The first part covered the topic of Basics and gesture handling. The second one will expand information on painting and drawing with shaders, and the third one on working with textures.

The final result is simple enough for beginners to start and so complex that we will cover many useful OpenGL topics. Some preview of complete example is shown below:

I highly recommend you check official Android documentation about OpenGL, as it contains much more detailed information and can be a good addition to this article.

A source code for the complete application will be stored on our Github repository and shared in the 3rd part of our series. Stay tuned!

A few words about shaders

In general, shaders are small programs that are attached to the main program running to draw a frame. Indeed, the word “small” doesn’t mean that the shader’s code must be short or as concise as possible. It rather corresponds to the idea that every shader should run very fast and have processing that matches its role. Talking about roles, there are two distinctions:

  • Vertex shader – program that is responsible for conversion position of given vertex. Here you may want to change coordinates according to the shader’s function, e.g. rotate shape. It will be called once per vertex and mostly you will pass position without any transformation down to the fragment shader. 
  • Fragment shader – has the same structure as vertex shader but it is invoked for every fragment (pixel) that must be colored. Here you will apply colors, textures and what may not be too intuitive, draw shapes like circles. This part of code should be really efficient in order to not slow down drawing.

It is worth mentioning that shaders are written in GLSL (OpenGL Shading Language) which is the C language with extra keywords and methods that are useful in the OpenGL environment.

Representation of shader in code

I would like to show you my approach to the shaders in the architecture of an application. This contains some abstraction for actions with shaders that are mostly repeating. Working with shaders, I have found that I am duplicating every time more or less the same steps:

  1. Pass a vertex position to the vertex shader
  2. Apply the uniforms to vertex and fragment shader
  3. Pass some texture that will cover the background
  4. Add extra texture for elements in the foreground

Following that line, it would be very convenient to have a common interface for every shader.

That interface is “smart” enough that it only requires us to pass vertex and fragment shader code as a String. For every OpenGL code we also need to pass at least one coordinate of point so this method is obligatory.

As we may want to draw a simple triangle without textures and uniforms other methods have their default values which return information (null) that weren’t applied to this particular shader.

First vertex shader in GLSL language

In the previous article I have explained the architecture of an application that can receive user’s gestures and transfer them to the OpenGL program. 

Our goal is to create an imitation of a football game. To do this, we will draw a circle, move it and resize. For each of these actions it is needed to pass a changed value (e.g. new center position) to the shaders and draw a shape once again. 

Now, when we are focused on the shader’s code, let’s see some details. On the first shot, let’s take a simpler vertex shader. It is a good base to describe general shader’s logic.

As I have mentioned before, GLSL code must be represented as a String. It may be painful due to lack of lint or any C code verification.

I strongly recommend you to validate your GLSL code before running it on Android. Especially when you don’t have program compilation or validation done in your project.

The tools that I have found really useful during writing shaders are:

Now, let me shortly introduce our main keywords:

  • attribute – global variable that may change per vertex and is passed from Kotlin
  • vec4 – vector (one dimensional array) with four float values
  • main – function that runs once per vertex. Here you can calculate values and return them to OpenGL program or down to the fragment shader
  • gl_Position – variable that stores current vertex position in the OpenGL program

Yet, you’ll find a better explanation of what the differences between a uniform, an attribute and a varying are in this thread.

From the code we need to invoke a function with the same variable position:

This shader is truly modest. It will only pass each vertex position to the OpenGL program and  down to the fragment shader. When we know some basic keywords, let’s look at the place where magic happens.

Drawing a circle with the fragment shader

The fragment shader is already longer and more complicated, but don’t worry. I will describe each part of the code in detail.

First are the uniform values. These are the parameters of our shader that we want to calculate and give every time when change is needed (user’s click or gesture). As you can see, we can put here different types. From the Android side we are responsible for calling the right method. 

There are different functions: glUniform1i(), glUniform1f(), glUniform2fv(). So, available are single ints and floats and their vector equivalents. For example, for uniform vec2 you have to use glUniform2fv().

A function drawCircle() is not defined in OpenGL and I took and adjusted code from this example.

In short: Thanks to the length() function, we will find a line from (0, 0) to the coordinate position. Step function will return 1 when a given fragment (pixel) belongs to the circle’s field and 0 when its position is outside (larger than the area set via radius).

In the main body there are steps to properly invoke drawCircle() and return a colored fragment.

  1. Get the relative (0-1) coordinates via dividing absolute fragment’s position by resolution
  2. Invoke drawing with applied offset which comes from user’s input
  3. Create a color vector (RGB) with 1,1,1 or 0,0,0 which will represent circle or background
  4. Fragment color also needs vec4 so at the end simply add full alpha (1 – not transparent)

And it’s mostly everything. We are reaching the last steps to run a successful program.

Applying uniforms from Android code

Now, I can show you the complete draw() method. Now you know all steps and methods.

Most details of this method were discussed in the previous post.

After all, we are able to run our program which has movable and scalable circle:

In the last part we will focus on how to make this app more visually appealing for users via usage of textures. 

Stay tuned! And if you have any questions, don’t hesitate to contact our team.