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.
renderer=new GLRenderer(this);
renderer.setVertexShader(
ResourceHelper.readRawTextFile(this,
R.raw._vertex_shader));
renderer.setFragmentShader(
ResourceHelper.readRawTextFile(this,
R.raw._fragment_shader));
renderer.setResTexId(R.drawable.kayla);
GLSurfaceView view=new GLSurfaceView(this);
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:
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.
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.
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.
final int[] compileStatus = new int[1];
GLES20.glGetShaderiv(shaderHandle, GLES20.GL_COMPILE_STATUS, compileStatus, 0);
if (compileStatus[0] == GLES20.GL_FALSE)
{
} else {
}
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.
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()
.
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.
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.
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:
attribute vec4 a_Position;
To bind a_Position
to index 0
, we can call it like this:
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.
GLES20.glLinkProgram(programHandle);
Linking status can be queried by calling glGetProgramiv()
and pass GL_LINK_STATUS
, similar to the way we called glGetShaderiv()
.
final int[] linkStatus = new int[1];
GLES20.glGetProgramiv(programHandle, GLES20.GL_LINK_STATUS, linkStatus, 0);
if (linkStatus[0]==GLES20.GL_FALSE)
{
} else {
}
To get more information about what cause linking to failed, call glGetProgramInfoLog()
. The following code snippet shows steps to create program.
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);
if (attributes != null)
{
final int size = attributes.length;
for (int i = 0; i < size; i++)
{
GLES20.glBindAttribLocation(
programHandle, i,
attributes[i]);
}
}
GLES20.glLinkProgram(programHandle);
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.
GLES20.glUseProgram(g_hShader);
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.
vertexBuffer.position(0);
GLES20.glVertexAttribPointer(g_posHandle,
3,
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:
uniform mat4 u_MVPMatrix;
attribute vec4 a_Position;
attribute vec2 a_TexCoordinate;
varying vec2 v_TexCoordinate;
void main()
{
v_TexCoordinate = a_TexCoordinate;
gl_Position = u_MVPMatrix * a_Position;
}
The original fragment shader does simple texture lookup and output final color.
precision mediump float;
uniform sampler2D u_Texture;
varying vec2 v_TexCoordinate;
void main()
{
gl_FragColor = (texture2D(u_Texture, v_TexCoordinate));
}
Original Image
Blur fragment shader does several texture lookups on current texture coordinate and its neighbour. It calculates average then output final color.
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);
}
Output image after blur shader applied.
Color inversion shader does texture lookup and invert its color.
precision mediump float;
uniform sampler2D u_Texture;
varying vec2 v_TexCoordinate;
void main()
{
gl_FragColor = 1.0-texture2D(u_Texture, v_TexCoordinate);
}
Output image after color inversion shader applied.