Working with OpenGL ES to make 3D figures

Alankrit Bhardwaj
11 min readMay 18, 2020

--

Konichiwa Developers!!!

Since you’re here it seems you have decided to start learning OpenGL. Today, I am going to relieve you of the pain of searching for the answers on how to get started with OpenGl ES?

Let’s get started!!!!

The Basics

Before heading to the coding part we first need to get our basics strong by learning what OpenGl actually works and what are the concepts behind the working with OpenGl Api.

Android supports several versions of the OpenGL API-

  • OpenGL ES 1.0 and 1.1 — This API specification is supported by Android 1.0 and higher.
  • OpenGL ES 2.0 — This API specification is supported by Android 2.2 (API level 8) and higher.
  • OpenGL ES 3.0 — This API specification is supported by Android 4.3 (API level 18) and higher.
  • OpenGL ES 3.1 — This API specification is supported by Android 5.0 (API level 21) and higher.

There are two fundamental classes which help us to create and control graphics in OpenGL

ES-

a)GLSurfaceView-This class is used for drawing and manipulating the object drawn on the android. It’s also used to respond to the touch screen event.

b)GlSurfaceView.renderer-This interface provides us with the methods to draw graphics on the screen. Some methods need to be implemented with this interface to set up our graphics.

The methods are as follows-

a)onSurfaceCreated()-This method is called only once when the system tries to create the GLSurfaceView. This method is only called when the entire life cycle is created and gets destroyed when it ends.

b)onSurfaceChanged()- This method is called whenever the GLSurfaceView changes its geometry. This is the best place to define the textures for your objects depending upon the orientation of the android device(portrait or landscape).

c)onDrawFrame()-This method is called whenever the graphic object is created on the GLSurfaceView.

Getting Started

Create a new project in android studio, after the build is finished open the Manifest.XML file add the following-

a)For using OpenGl 2.0-

<uses-feature android:glEsVersion=”0x00020000" android:required=”true” />

b)For using OpenGl 3.0-

<uses-feature android:glEsVersion=”0x00030000" android:required=”true” />

c)For using OpenGl 3.1-

<uses-feature android:glEsVersion=”0x00030001" android:required=”true” />

Create a Java class and implement the interface GLSurfaceView.renderer after implementing the methods for the following interface the new created class will look like this-

public class MyGLRenderer implements GLSurfaceView.Renderer {public void onSurfaceCreated(GL10 unused, EGLConfig config) {// Set the background frame colorGLES20.glClearColor(0.0f, 0.0f, 0.0f, 1.0f);}public void onDrawFrame(GL10 unused) {// Redraw background colorGLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);}public void onSurfaceChanged(GL10 unused, int width, int height) {GLES20.glViewport(0, 0, width, height);}}

Create another class named MyGlSurfaceView extend it to use the GlSurfaceView class. This class does not do much, we will use this class to set the renderer for our graphic object.

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 contextsetEGLContextClientVersion(2);renderer = new MyGLRenderer();// Set the Renderer for drawing on the GLSurfaceViewsetRenderer(renderer);}}

Now, we will try to change the color of the surface where we are going to work with OpenGl.

Update your MyGlRenderer -

public void onSurfaceCreated(GL10 gl10, EGLConfig config) {//this method take in input asGLES20.glClearColor(0.0f, 0.0f, 0.0f, 1.0f);}public void onDrawFrame(GL10 gl10) {GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);//whenever the frame is made it applies the color specified in method onSurfaceCreated()}public void onSurfaceChanged(GL10 gl10, int i, int i1) {GLES20.glViewport(0, 0,i,i1);//this method applies the entire frame to cover the entire screen whenever the orientation is changed}

Understanding the functions used-

a)GLES20.glClearColor()-This function sets the frame colour for our screen. It takes input as RGBA(RED GREEN BLUE ALPHA) the color used for this example is BLACK.

b) GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT)-Whenever the frame is made it applies the color specified in method onSurfaceCreated()

c) GLES20.glViewport(0, 0,i,i1)-

this method applies the entire frame to cover the entire screen whenever the orientation is changed

Now, for the last step to get started we will change the MainActivity as-

public class OpenGLES20Activity extends Activity {private GLSurfaceView gLView;@Overridepublic 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);}}

And, we’re done with the basics. Now let’s move forward and learn about how to make a cube and add texture to it.

Making a Cube In OpenGL

To reach our calling we need to make a class TextureCube.java, where we will describe the attributes of our cube and try to display it on our already created screen.

Before,heading forward with the code let’s first understand how we’re going to draw each face of the cube.

For drawing the face of a cube i.e. square we need to draw 2 triangles to form the face of the cube(Since, it’s easy to draw a triangle in OpenGL ES).

Now, to draw this face with the given ordinates we need to make sure that we enter the ordinates in a counter clockwise manner for both triangles that represent this shape.

The drawing order is important because it defines which side is the front face of the shape, which you typically want to have drawn, and the back face, which you can choose to not draw using the OpenGL ES cull face feature.

So, the order to draw this face and make sure whether it’s the front face is 4,3,1,1,3,2.

Defining Shape for our Cube

In the created class TextureCube.java add the following code.

private FloatBuffer vertexBuffer;private ShortBuffer indexBuffer;private float[][] colors = {{1.0f, 0.5f, 0.0f, 1.0f},  // 0. orange{1.0f, 0.0f, 1.0f, 1.0f},  // 1. violet{0.0f, 1.0f, 0.0f, 1.0f},  // 2. green{0.0f, 0.0f, 1.0f, 1.0f},  // 3. blue{1.0f, 0.0f, 0.0f, 1.0f},  // 4. red{1.0f, 1.0f, 0.0f, 1.0f}};static final int COORDS_PER_VERTEX =3;static float vertices[]={-0.2f, -0.2f,  0.2f,  // 0. left-bottom-front0.2f, -0.2f,  0.2f,  // 1. right-bottom-front-0.2f,  0.2f,  0.2f,  // 2. left-top-front0.2f,  0.2f,  0.2f,  // 3. right-top-front// BACK0.2f, -0.2f, -0.2f,  // 6. right-bottom-back-0.2f, -0.2f, -0.2f,  // 4. left-bottom-back0.2f,  0.2f, -0.2f,  // 7. right-top-back-0.2f,  0.2f, -0.2f,  // 5. left-top-back// LEFT-0.2f, -0.2f, -0.2f,  // 4. left-bottom-back-0.2f, -0.2f,  0.2f,  // 0. left-bottom-front-0.2f,  0.2f, -0.2f,  // 5. left-top-back-0.2f,  0.2f,  0.2f,  // 2. left-top-front// RIGHT0.2f, -0.2f,  0.2f,  // 1. right-bottom-front0.2f, -0.2f, -0.2f,  // 6. right-bottom-back0.2f,  0.2f,  0.2f,  // 3. right-top-front0.2f,  0.2f, -0.2f,  // 7. right-top-back// TOP-0.2f,  0.2f,  0.2f,  // 2. left-top-front0.2f,  0.2f,  0.2f,  // 3. right-top-front-0.2f,  0.2f, -0.2f,  // 5. left-top-back0.2f,  0.2f, -0.2f,  // 7. right-top-back// BOTTOM-0.2f, -0.2f, -0.2f,  // 4. left-bottom-back0.2f, -0.2f, -0.2f,  // 6. right-bottom-back-0.2f, -0.2f,  0.2f,  // 0. left-bottom-front0.2f, -0.2f,  0.2f   // 1. right-bottom-front};short[] indices={0,1,2,2,1,3,5,4,7,7,4,6,8,9,10,10,9,11,12,13,14,14,13,15,16,17,18,18,17,19,22,23,20,20,23,21};

Dissecting the following code we will notice that the colors[] array holds the color which we are going to use for each face of the cube in rgba format. The vertices[] array stores the coordinate for every vertex we’re going to draw on the screen and the indices array holds the order in which we are going to connect the vertices so that it follows a counter clockwise direction. After describing the variable we’re going to use further we will describe a constructor for the class TextureCube.java in which we describe the shape of our cube.

// initialize vertex byte buffer for shape coordinatesPublic TextureCube(){ByteBuffer vbb = ByteBuffer.allocateDirect(vertices.length * 4);vbb.order(ByteOrder.nativeOrder()); // Use native byte ordervertexBuffer = vbb.asFloatBuffer(); // Convert from byte to floatvertexBuffer.put(vertices);         // Copy data into buffervertexBuffer.position(0);           // Rewind// initialize byte buffer for the draw listindexBuffer = ByteBuffer.allocateDirect(indeces.length * 2).order(ByteOrder.nativeOrder()).asShortBuffer();indexBuffer.put(indeces).position(0);}

Drawing the Cube

Initialize the shape which we describe in the last part in MyGlRenderer class-

public class MyGLRenderer implements GLSurfaceView.Renderer {Private TextureCube textureCube;public void onSurfaceCreated(GL10 unused, EGLConfig config) {textureCube=new TextureCube();}...}

To draw a cube we need to write quite a bit of code to the graphics rendering pipeline.

Most importantly we need to define the following-

  1. Vertex Shader- Vertex Shader is used to pass graphic code to the rendering pipeline which defines the attributes of the vertex created by us on the screen.
  2. Fragment Shader-The fragment Shader defines the attributes of the area formed by the vertices passed in the vertex shader.
  3. Program-An OpenGL ES object that contains the shaders you want to use for drawing one or more shapes.

Now, we will define the shaders in our TextureCube.java-

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;" +"}";

Shaders contain OpenGL Shading Language (GLSL) code that must be compiled prior to using it in the OpenGL ES environment. To compile this code, create a utility method in your renderer class:

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 itGLES20.glShaderSource(shader, shaderCode);GLES20.glCompileShader(shader);return shader;}

In order to draw your shape, you must compile the shader code, add them to a OpenGL ES program object and then link the program. Do this in your drawn object’s constructor, so it is only done once.

public class TextureCube() {...private final int program;public TextureCube{...int vertexShader = MyGLRenderer.loadShader(GLES20.GL_VERTEX_SHADER,vertexShaderCode);int fragmentShader = MyGLRenderer.loadShader(GLES20.GL_FRAGMENT_SHADER,fragmentShaderCode);// create empty OpenGL ES Programprogram = GLES20.glCreateProgram();// add the vertex shader to programGLES20.glAttachShader(program, vertexShader);// add the fragment shader to programGLES20.glAttachShader(program, fragmentShader);// creates OpenGL ES program executablesGLES20.glLinkProgram(program);}}

At this point when we have decided all the attributes of our cubes we are ready to draw our shape on the screen. Create a draw method in TextureCube.java to draw the cube.

This code sets the position and color values to the shape’s vertex shader and fragment shader, and then executes the drawing function.

private int positionHandle;private int colorHandle;private final int vertexCount = triangleCoords.length / COORDS_PER_VERTEX;private final int vertexStride = COORDS_PER_VERTEX * 4;public void draw() {// Adding program to OpenGL ES environmentGLES20.glUseProgram(program);// get handle to vertex shader's vPosition memberpositionHandle = GLES20.glGetAttribLocation(program, "vPosition");// Enable a handle to the triangle verticesGLES20.glEnableVertexAttribArray(positionHandle);// Prepare the triangle coordinate dataGLES20.glVertexAttribPointer(positionHandle, COORDS_PER_VERTEX,GLES20.GL_FLOAT, false,vertexStride, vertexBuffer);colorHandle = GLES20.glGetUniformLocation(program, "vColor");for (int face = 0; face < numFaces; face++) {// Set the color for each of the facesGLES20.glUniform4fv(colorHandle, 1, colors[face], 0);indexBuffer.position(face * 6);GLES20.glDrawElements(GLES20.GL_TRIANGLES, 6, GLES20.GL_UNSIGNED_SHORT, indexBuffer);}// Disable vertex arrayGLES20.glDisableVertexAttribArray(positionHandle);}

Once we get all this code in place we just have to call the draw method in our MyGLRenderer class-

public void onDrawFrame(GL10 unused) {...triangle.draw();}

This will give us the result which we all have been waiting for. Our very own cube.

You’ll notice that we are only able to see the front face of the cube due to which it seems we’re looking at a square to see the complete cube and all the faces we will try to rotate our cube.

Implementing Projection and Camera View

In the OpenGL ES environment, projection and camera views allow you to display drawn objects in a way that more closely resembles how you see physical objects with your eyes.

  • Projection — This transformation adjusts the coordinates of drawn objects based on the width and height of the GLSurfaceView where they are displayed. Without this calculation, objects drawn by OpenGL ES are skewed by the unequal proportions of the view window. A projection transformation typically only has to be calculated when the proportions of the OpenGL view are established or changed in the onSurfaceChanged() method of your renderer.
  • Camera View — This transformation adjusts the coordinates of drawn objects based on a virtual camera position. It’s important to note that OpenGL ES does not define an actual camera object, but instead provides utility methods that simulate a camera by transforming the display of drawn objects. A camera view transformation might be calculated only once when you establish your GLSurfaceView, or might change dynamically based on user actions or your application’s function.

The data for a projection is populated in onSurfacceChanged() method in GlSurfaceView.Renderer class. We will populate our matrix using Matrix.frustumM() method.

private final float[] vPMatrix = new float[16];private final float[] projectionMatrix = new float[16];private final float[] viewMatrix = new float[16];@Overridepublic void onSurfaceChanged(GL10 gl, int width, int height) {GLES20.glViewport(0, 0, width, height);float ratio = (float) width / height;// this projection matrix is applied to object coordinates// in the onDrawFrame() methodMatrix.frustumM(projectionMatrix, 0, -ratio, ratio, -1, 1, 3, 7);}

For adding camera view transformation in our project add the following code in onDrawFrame() method of GLSurfaceView.Renderer class. Here we will calculate the camera view transformation matrix by the help of method Matrix.setLookAtM() and multiply it by the projection matrix and pass it to the draw method.

@Overridepublic void onDrawFrame(GL10 unused) {...// Set the camera position (View matrix)Matrix.setLookAtM(viewMatrix, 0, 0, 0, -3, 0f, 0f, 0f, 0f, 1.0f, 0.0f);// Calculate the projection and view transformationMatrix.multiplyMM(vPMatrix, 0, projectionMatrix, 0, viewMatrix, 0);// Draw shapetriangle.draw(vPMatrix);}

To fix the position of the camera view we need to pass it to the vertex shader. Therefore, update your vertex shader as follows-

Public class TextureCube{private final String vertexShaderCode =// the coordinates of the objects that use this vertex shader"uniform mat4 uMVPMatrix;" +"attribute vec4 vPosition;" +"void main() {" +"  gl_Position = uMVPMatrix * vPosition;" +"}";private int vPMatrixHandle;}

To position the camera update the draw method as follows-

public void draw(float[] mvpMatrix) { // pass in the calculated transformation matrix...// get handle to shape's transformation matrixvPMatrixHandle = GLES20.glGetUniformLocation(program, "uMVPMatrix");// Pass the projection and view transformation to the shaderGLES20.glUniformMatrix4fv(vPMatrixHandle, 1, false, mvpMatrix, 0);// Draw the triangleGLES20.glDrawArrays(GLES20.GL_TRIANGLES, 0, vertexCount);// Disable vertex arrayGLES20.glDisableVertexAttribArray(positionHandle);}

We’re almost done here. To see our cube rotating we just need to add motion to our project so that we can see all the faces of the cube. For obtaining the following objective we need to pass the rotation matrix to get the required motion on our cube. To achieve it add the following code inside the OpenGl.Renderer class

private float[] rotationMatrix = new float[16];Overridepublic void onDrawFrame(GL10 gl) {float[] scratch = new float[16];...// Create a rotation transformation for the trianglelong time = SystemClock.uptimeMillis() % 4000L;float angle = 0.090f * ((int) time);Matrix.setRotateM(rotationMatrix, 0, angle, 0, 0, -1.0f);// Combine the rotation matrix with the projection and camera view// Note that the vPMatrix factor *must be first* in order// for the matrix multiplication product to be correct.Matrix.multiplyMM(scratch, 0, vPMatrix, 0, rotationMatrix, 0);// Draw cubetextureCube.draw(scratch);}

And, finally we will get what we’re waiting for: a cube made in OpenGl ES 2.0.

You can find the entire source code for this project here.

Since now you have your basic clear you can go and make a more complex structure.If you do please share it with us in the comment section and you’re most welcome to ask doubts too.

This is it from my side on this topic, if this helps you in any form don’t feel shy to press that 👏 button.

Over and out!!!

Happy Coding😊!!!!

--

--