Render a simple Triangle using OpenGL ES on Android Studio
Overview
OpenGL ES is a wrapper for OpenGL used in embedded systems such as smart phones and tablets. OpenGL renders high performance 2D and 3D graphics. This is used for video games, 3D modeling, augmented reality, plotting data, and anything else that needs graphics to be rendered. For this tutorial we will be creating an Android application in Android Studio which will use OpenGL ES to display a triangle.
Getting started
In this tutorial we will be using Android Studio 3.5.* to develop Java classes for our mobile application. Since we will be using OpenGL ES 2.0, we will need to add the proper dependencies to the project. We will cover this step first in the section below. Below is a link to download the IDE we will be using, Android Studio.
- Android Studio : https://developer.android.com/studio
Step-by-step
1. First create a new project in Android Studio. Choose the empty activity template, next we will add the required dependencies for OpenGL ES.
2. Here we are going to need to add the OpenGL ES dependency to our androidManifest.xml, by inserting the highlighted code in the location shown below.
<?xml version=”1.0" encoding=”utf-8"?>
<manifest xmlns:android=”http://schemas.android.com/apk/res/android"
package=”com.example.openglmusicvisualizer”>
<uses-feature android:glEsVersion=”0x00020000" android:required=”true” />
</manifest>
3. Looking at our main activity we must first create a new class that will hold the view. This represents the area on the screen that you wish to be visualized. For this tutorial we will utilize the entire screen. To do this we will set the the view to a class that we will create to hold this state.
public class MainActivity extends Activity {
private GLSurfaceView gLView;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Create a GLSurfaceView instance and set it
// as the ContentView for this Activity.
gLView = new MyGLSurfaceView(this);
setContentView(gLView);
}
}
4. Creating a GLSurfaceView class
Create a java class called MyGLSurfaceView and insert the following code into it. This will extend the GLSurfaceView class and implement some needed functionality for the MyGLSurfaceView’s initialization. Inside this we will instantiate a new MyGLRenderer, this will handle the processing for any coordinates we want to visualize. The MainActivity holds the view which holds the renderer.
import android.content.Context;
import android.opengl.GLSurfaceView;
class MyGLSurfaceView extends GLSurfaceView {
private final MyGLRenderer renderer;
public MyGLSurfaceView(Context context){
super(context);
// Create an OpenGL ES 2.0 context
setEGLContextClientVersion(2);
renderer = new MyGLRenderer();
// Set the Renderer for drawing on the GLSurfaceView
setRenderer(renderer);
}
}
5. Create a renderer
Create a new java file called MyGLRenderer and add the following code to it. The renderer controls what is drawn on the GLSurfaceView, ours in this project being the MyGLSurfaceView. There are three lifecycle methods here that are called while the renderer is running. onSurfaceCreated() is called once when the renderer is created, and then onDrawFrame() is called when the view needs to be redrawn, and onSurfaceChanged() is called when the orientation on the device changes.
import javax.microedition.khronos.egl.EGLConfig;
import javax.microedition.khronos.opengles.GL10;
import android.opengl.GLES20;
import android.opengl.GLSurfaceView;
public class MyGLRenderer implements GLSurfaceView.Renderer {
public void onSurfaceCreated(GL10 unused, EGLConfig config) {
// Set the background frame color
GLES20.glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
}
public void onDrawFrame(GL10 unused) {
// Redraw background color
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);
}
public void onSurfaceChanged(GL10 unused, int width, int height) {
// Set the viewport to the size of the view.
GLES20.glViewport(0, 0, width, height);
}
}
6. Next create a new Java class that will be our object we wish to render, in this case it will be a triangle. This holds OpenGL C++ code that does the processing as well as coordinates, transforms, and translations.
public class Triangle {
private FloatBuffer vertexBuffer;
// number of coordinates per vertex in this array
static final int COORDS_PER_VERTEX = 3;
static float triangleCoords[] = { // in counterclockwise order:
0.0f, 0.622008459f, 0.0f, // top
-0.5f, -0.311004243f, 0.0f, // bottom left
0.5f, -0.311004243f, 0.0f // bottom right
};
// Set color with red, green, blue and alpha (opacity) values
float color[] = { 0.63671875f, 0.76953125f, 0.22265625f, 1.0f };
public Triangle() {
// initialize vertex byte buffer for shape coordinates
ByteBuffer bb = ByteBuffer.allocateDirect(
// (number of coordinate values * 4 bytes per float)
triangleCoords.length * 4);
// use the device hardware’s native byte order
bb.order(ByteOrder.nativeOrder());
// create a floating point buffer from the ByteBuffer
vertexBuffer = bb.asFloatBuffer();
// add the coordinates to the FloatBuffer
vertexBuffer.put(triangleCoords);
// set the buffer to read the first coordinate
vertexBuffer.position(0);
}
}
7. Create a new triangle in the MyGLRenderer class and initialize it in onSurfaceCreated()
public class MyGLRenderer implements GLSurfaceView.Renderer {
…
private Triangle mTriangle;
public void onSurfaceCreated(GL10 unused, EGLConfig config) {
…
// initialize a triangle
mTriangle = new Triangle();
}
…
}
8. OpenGL ES uses C++ shaders to handle the graphics rendering. We’re going to be setting that C++ code as strings into our object to pass them to the renderer. Add the shaders into the Triangle class at the top of the class before the line where FloatBuffer is created.
public class Triangle {
private final String vertexShaderCode =
“attribute vec4 vPosition;” +
“void main() {“ +
“ gl_Position = vPosition;” +
“}”;
private final String fragmentShaderCode =
“precision mediump float;” +
“uniform vec4 vColor;” +
“void main() {“ +
“ gl_FragColor = vColor;” +
“}”;
private FloatBuffer vertexBuffer;
…
}
9. To handle these shaders in our renderer we will need to compile our shaders. We’re going to wrap that process in a function to pass in the type and content of the shader.
public static int loadShader(int type, String shaderCode){
// create a vertex shader type (GLES20.GL_VERTEX_SHADER)
// or a fragment shader type (GLES20.GL_FRAGMENT_SHADER)
int shader = GLES20.glCreateShader(type);
// add the source code to the shader and compile it
GLES20.glShaderSource(shader, shaderCode);
GLES20.glCompileShader(shader);
return shader;
}
10. Next we are going to need to use these shaders in the Triangle’s constructor and then we are going to link the program which we will also add in this step.
public class Triangle() {
…
private final int mProgram;
public Triangle() {
…
int vertexShader = MyGLRenderer.loadShader(GLES20.GL_VERTEX_SHADER,
vertexShaderCode);
int fragmentShader = MyGLRenderer.loadShader(GLES20.GL_FRAGMENT_SHADER,
fragmentShaderCode);
// create empty OpenGL ES Program
mProgram = GLES20.glCreateProgram();
// add the vertex shader to program
GLES20.glAttachShader(mProgram, vertexShader);
// add the fragment shader to program
GLES20.glAttachShader(mProgram, fragmentShader);
// creates OpenGL ES program executables
GLES20.glLinkProgram(mProgram);
}
}
11. In order to start the rendering and set our base configuration we will implement a draw method in our Triangle. This will also be where our program binds to the C++ shader we added earlier.
private int positionHandle;
private int colorHandle;
private final int vertexCount = triangleCoords.length / COORDS_PER_VERTEX;
private final int vertexStride = COORDS_PER_VERTEX * 4; // 4 bytes per vertex
..
public void draw() {
// Add program to OpenGL ES environment
GLES20.glUseProgram(mProgram);
// get handle to vertex shader’s vPosition member
positionHandle = GLES20.glGetAttribLocation(mProgram, “vPosition”);
// Enable a handle to the triangle vertices
GLES20.glEnableVertexAttribArray(positionHandle);
// Prepare the triangle coordinate data
GLES20.glVertexAttribPointer(positionHandle, COORDS_PER_VERTEX,
GLES20.GL_FLOAT, false,
vertexStride, vertexBuffer);
// get handle to fragment shader’s vColor member
colorHandle = GLES20.glGetUniformLocation(mProgram, “vColor”);
// Set color for drawing the triangle
GLES20.glUniform4fv(colorHandle, 1, color, 0);
// Draw the triangle
GLES20.glDrawArrays(GLES20.GL_TRIANGLES, 0, vertexCount);
// Disable vertex array
GLES20.glDisableVertexAttribArray(positionHandle);
}
12. In order to draw our object we will need to call the draw that we created earlier. Implement this at the end of the onDrawFrame method in the renderer.
public void onDrawFrame(GL10 unused) {
…
triangle.draw();
}
If drawn successfully your object should look like this:
Further discussion/conclusions
OpenGL ES is a powerful tool capable of rendering complex graphics. We first started with an empty main activity where we then created a view to set our renderer and tie into the layout. We then made this renderer which loads the compiled C++ graphic. Last we created our triangle object where the C++ shaders are loaded and drawn. If you’re looking for additional functionality then head over to the Android Developer Docs where you can add rotation and touch event. Many mobile games, maps, and augmented reality applications utilize OpenGL ES. Thank you for taking the time to work through this together and good luck rendering your own custom graphics.
If you would like to view the source code for this tutorial please visit: