Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Mobile / Android

Basic Shader Programming on Android

5.00/5 (1 vote)
10 Jul 2015CPOL5 min read 35.3K   636  
Get started with OpenGL ES 2.0 shader programming on Android

Initialize OpenGL ES 2.0 in Android

Shader is a small program that gets executed in GPU. Shader is available since OpenGL ES 2.0 and Android added OpenGL ES 2.0 support since Android SDK API level 8 (Froyo). Android application can use OpenGL ES 2.0 with NDK or GLSurfaceView. The latter is easier. This article will use GLSurfaceView only. How to display OpenGL ES 2.0 graphics in Android is similar to OpenGL ES 1.0, i.e., using GLSurfaceView and implements GLSurfaceView.Renderer interface. What difference is value we use when calling setEGLContextClientVersion() method. You need to use 2 to indicate that you want to use OpenGL ES 2.0. The code below shows how to initialize GLSurfaceView to use OpenGL ES 2.0. Note GLRenderer class is our own class that implements GLSurfaceView.Renderer interface.

Java
renderer=new GLRenderer(this);
//load shader from resource
renderer.setVertexShader(
    ResourceHelper.readRawTextFile(this,
             R.raw._vertex_shader));
renderer.setFragmentShader(
    ResourceHelper.readRawTextFile(this,
            R.raw._fragment_shader));

//load texture from resource
renderer.setResTexId(R.drawable.kayla);

GLSurfaceView view=new GLSurfaceView(this);
//only OpenGL ES 2.0
view.setEGLContextClientVersion(2);
view.setRenderer(renderer);

To use OpenGL ES 2.0 functionalities, you use GLES20. For example to call glClear(), you use something like this:

Java
GLES20.glClear(GLES20.GL_DEPTH_BUFFER_BIT | GLES20.GL_COLOR_BUFFER_BIT);

Create Shader Resource

Before you can use shader, you must create shader resource, by calling glCreateShader(). It expects one integer parameter which is the type of shader you need to create. You can use GL_VERTEX_SHADER or GL_FRAGMENT_SHADER to create vertex shader or fragment shader, respectively.

Java
int vtxshaderHandle = GLES20.glCreateShader(GLES20.GL_VERTEX_SHADER);
int frgshaderHandle = GLES20.glCreateShader(GLES20.GL_FRAGMENT_SHADER);

glCreateShader() returns handle to shader resource that has been created. To delete shader resource, you need to call glDeleteShader() and pass this handle.

Shader Compilation

In OpenGL ES 2.0 shader programming, there are two concepts related to code that run in GPU, shader and program. These concepts are similar to compiling and linking process in C language. At compiling stage, shader source code is translated into intermediate code. Later in linking stage, it gets linked into program that is ready to be run in GPU.

To compile shader code, you need to provide shader code to compile (obviously) by calling glShaderSource(), which expects two parameters, handle to shader resource and shader source code to compile. To start compilation process, you call glCompileShader() and pass shader resource handle.

Java
GLES20.glShaderSource(shaderHandle, shaderSourceCode);
GLES20.glCompileShader(shaderHandle);

Human makes mistakes. Shader compilation may succeed or not. To find out, you call glGetShaderiv(), which expects 4 parameters, shader resource handle, type of information required (in case of compilation status, we use GL_COMPILE_STATUS), array of integer variables to be filled with status and offset.

Java
final int[] compileStatus = new int[1];
GLES20.glGetShaderiv(shaderHandle, GLES20.GL_COMPILE_STATUS, compileStatus, 0);

if (compileStatus[0] == GLES20.GL_FALSE)
{
   // failed.
} else {
   // succeed.
}

glGetShaderiv() returns status only. To find out more about what causes compilation to fail, you can call glGetShaderInfoLog(). It returns String which contains more descriptive error messages.

The following code snippet shows steps to compile shader source.

Java
public static int compileShader(final int shaderType,
        String shaderSource)
{
   int shaderHandle=GLES20.glCreateShader(shaderType);
   if (shaderHandle !=0)
   {
     GLES20.glShaderSource(shaderHandle, shaderSource);
     GLES20.glCompileShader(shaderHandle);
       
     final int[] compileStatus = new int[1];
     GLES20.glGetShaderiv(shaderHandle, GLES20.GL_COMPILE_STATUS, compileStatus, 0);
     if (compileStatus[0] == GLES20.GL_FALSE)
     {
       Log.e(TAG, "Error compiling shader: " +
              GLES20.glGetShaderInfoLog(shaderHandle));
       GLES20.glDeleteShader(shaderHandle);
       shaderHandle = 0;
     }
   }
   return shaderHandle;
}

That's it. Now shader is ready to be linked into program.

Create Program Resource

To start linking process, you need to create a program by calling glCreateProgram() which will return program handle. To delete it, pass this handle to glDeleteProgram().

Java
int programHandle=GLES20.glCreateProgram();
GLES20.glDeleteProgram(programHandle);

Attaching Shaders to Program

Each shader that needs to be linked must be attached to the program first. We call glAttachShader() to do this and pass program handle and shader resource handle. Each program must be attached to one vertex shader and one fragment shader.

Java
GLES20.glAttachShader(programHandle, vtxShader);
GLES20.glAttachShader(programHandle, pxlShader);

Attaching shader can be done at any time before linking program as long as both handles are valid. For example, you can attach shader even if it has not been compiled yet. To detach a shader from a program, call glDetachShader() and pass program handle and shader resource handle.

Java
GLES20.glDetachShader(programHandle, vtxShader);

Binding Attributes to Program

If your shader code contains attribute definition, you may specify it before linking with glBindAttribLocation(). For example, if your vertex shader code contains the following attribute declaration:

Java
attribute vec4 a_Position; // Per-vertex position information we will pass in.

To bind a_Position to index 0, we can call it like this:

Java
GLES20.glBindAttribLocation(programHandle, 0, "a_Position");         

Linking a Program

After shaders are attached to program, to start linking process, call glLinkProgram(). Linking process does several things, such as making sure that output program code does not violate any GPU requirements (each GPU places limit on number of attributes, uniform variables and instructions that are available to shader) and output machine code that can be run by GPU.

Java
GLES20.glLinkProgram(programHandle);

Linking status can be queried by calling glGetProgramiv() and pass GL_LINK_STATUS , similar to the way we called glGetShaderiv().

Java
final int[] linkStatus = new int[1];
GLES20.glGetProgramiv(programHandle, GLES20.GL_LINK_STATUS, linkStatus, 0);
if (linkStatus[0]==GLES20.GL_FALSE)
{
   //linking failed
} else {
   //linking succeed
}

To get more information about what cause linking to failed, call glGetProgramInfoLog(). The following code snippet shows steps to create program.

Java
public static int createProgram(final int vtxShader, final int pxlShader,
                   final String[] attributes)
{
   int programHandle=GLES20.glCreateProgram();
   if (programHandle!=0)
   {
      GLES20.glAttachShader(programHandle, vtxShader);
      GLES20.glAttachShader(programHandle, pxlShader);
      // Bind attributes
      if (attributes != null)
      {
        final int size = attributes.length;
        for (int i = 0; i < size; i++)
        {
           GLES20.glBindAttribLocation(
                   programHandle, i,
                   attributes[i]);
        }                        
      }          

      GLES20.glLinkProgram(programHandle);

     //get linking status
     final int[] linkStatus = new int[1];
     GLES20.glGetProgramiv(programHandle, GLES20.GL_LINK_STATUS, linkStatus, 0);
     if (linkStatus[0]==0)
     {
          Log.e(TAG, "Error linking program: " +
                GLES20.glGetProgramInfoLog(programHandle));
          GLES20.glDeleteProgram(programHandle);
          programHandle = 0;  
     }
  }
  return programHandle;
}

Copy Data into Program

Before you can copy data to program, make sure to activate the program by calling glUseProgram() and pass program handle that you want to make active. To copy data into uniform variables or attribute variables, you need to get handle to location of that uniform or attribute using glGetUniformLocation() than glGetAttribLocation() that previously bound to program. First parameter is program handle and second parameter is string name of variables as written in shader source code. Both methods return handle to variable location.

Java
GLES20.glUseProgram(g_hShader);

// get handle to variables
g_matMVPHandle = GLES20.glGetUniformLocation(g_hShader, "u_MVPMatrix");
g_texHandle = GLES20.glGetUniformLocation(g_hShader, "u_Texture");
g_timeHandle = GLES20.glGetUniformLocation(g_hShader, "elapsed_time");
g_posHandle = GLES20.glGetAttribLocation(g_hShader, "a_Position");
g_texCoordHandle = GLES20.glGetAttribLocation(g_hShader, "a_TexCoordinate");

This handle later can be used to pass data by calling glVertexAttribPointer() and glEnableVertexAttribArray(). The code below illustrates this. Instance vertexBuffer is FloatBuffer type.

Java
// Pass in the position information
vertexBuffer.position(0);
GLES20.glVertexAttribPointer(g_posHandle,
        3, //number of coordinate per vertex
        GLES20.GL_FLOAT,
        false,
        0, vertexBuffer);

GLES20.glEnableVertexAttribArray(g_posHandle);

Draw

After vertex buffer filled, texture is set and model, view, projection transformation, we need to draw quad using glDrawElements(). We use 2 triangles to draw a quad with each triangle that consists of 3 indices.

GLES20.glDrawElements(GLES20.GL_TRIANGLES, 6, GLES20.GL_UNSIGNED_SHORT, indexBuffer);

Running Shader on Android Virtual Device

OpenGL ES 2.0 support in Android Virtual Device is available with Android API level 15 release. To test shader code in Android emulator, you must create Android Virtual Device (AVD) with minimum target API Level 15 and select Use Host GPU to enable GPU emulation, otherwise your application may crash.

Sample Demo

Sample demo is Android application that does simple image processing using shader. It does it by loading an image and mapping it as texture on a rectangular plane (a quad). Vertex shader is used to display the quad and does simple model, view, projection transformation. Image processing is done on fragment shader. Whenever user changes active fragment shader, application recompiles and links it with program, then shows the output.

The code below is vertex shader:

Java
uniform mat4 u_MVPMatrix; //model/view/projection matrix.
                      
attribute vec4 a_Position; //Per-vertex position information we will pass in.
attribute vec2 a_TexCoordinate;// Per-vertex texture coordinate information we will pass in.
                                        
varying vec2 v_TexCoordinate; //This will be passed into the fragment shader.

void main()
{                                                         
    // Pass through the texture coordinate.
    v_TexCoordinate = a_TexCoordinate;
          
    // multiply the vertex by the matrix to get the normalized screen coordinates.
    gl_Position = u_MVPMatrix * a_Position;
} 

The original fragment shader does simple texture lookup and output final color.

Java
precision mediump float;
uniform sampler2D u_Texture;
varying vec2 v_TexCoordinate;

void main()
{
     gl_FragColor = (texture2D(u_Texture, v_TexCoordinate));
}

Image 1

Original Image

Blur fragment shader does several texture lookups on current texture coordinate and its neighbour. It calculates average then output final color.

Java
precision mediump float;
uniform sampler2D u_Texture;
varying vec2 v_TexCoordinate;

void main()
{
    vec3 irgb = texture2D(u_Texture, v_TexCoordinate).rgb;
    float ResS = 150.0;
    float ResT = 150.0;

    vec2 stp0 = vec2(1.0/ResS, 0.0);
    vec2 st0p = vec2(0.0, 1.0/ResT);
    vec2 stpp = vec2(1.0/ResS, 1.0/ResT);
    vec2 stpm = vec2(1.0/ResS, -1.0/ResT);

    vec3 i00 = texture2D(u_Texture, v_TexCoordinate).rgb;
    vec3 im1m1 = texture2D(u_Texture, v_TexCoordinate-stpp).rgb;
    vec3 ip1p1 = texture2D(u_Texture, v_TexCoordinate+stpp).rgb;
    vec3 im1p1 = texture2D(u_Texture, v_TexCoordinate-stpm).rgb;
    vec3 ip1m1 = texture2D(u_Texture, v_TexCoordinate+stpm).rgb;
    vec3 im10 = texture2D(u_Texture, v_TexCoordinate-stp0).rgb;
    vec3 ip10 = texture2D(u_Texture, v_TexCoordinate+stp0).rgb;
    vec3 i0m1 = texture2D(u_Texture, v_TexCoordinate-st0p).rgb;
    vec3 i0p1 = texture2D(u_Texture, v_TexCoordinate+st0p).rgb;

    vec3 target = vec3(0.0, 0.0, 0.0);
    target += 1.0*(im1m1+ip1m1+ip1p1+im1p1);
    target += 2.0*(im10+ip10+i0p1);
    target += 4.0*(i00);
    target /= 16.0;
    gl_FragColor = vec4(target, 1.0);
}

Image 2

Output image after blur shader applied.

Color inversion shader does texture lookup and invert its color.

Java
precision mediump float; // Set the default precision to medium. We don't need as high of a
                         // precision in the fragment shader.
uniform sampler2D u_Texture; // The input texture. 
varying vec2 v_TexCoordinate; // Interpolated texture coordinate per fragment

void main()
{    
    gl_FragColor = 1.0-texture2D(u_Texture, v_TexCoordinate);
}

Image 3

Output image after color inversion shader applied.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)