..
#gamedev

How to Use OpenGL ES for Android

This article I'm gonna share with you how to use OpenGL ES 1.0 in Android.

In fact, the OpenGL has a newer version, with more features and more easy way to implement with shaders. But, I want to share the older style, because some projects and 3D stuff still used it.

I'm assuming you already know the Android basics and how to handle some UI on the screen.

The Android SDK has a special sub-view that create and manage the Render Thread. So, the only things that we need to do is to implement some subclasses and interfaces.

Let's start.

Create a new class that inheritances the GLSurfaceView.

This class provides a method called setRenderer. This interface Renderer makes the 'magic' happen.

class ExampleView(context: Context) : GLSurfaceView(context) {
    init {
        val renderer = Renderer()
        setRenderer(renderer)
    }
}

As you can see, inside the init block, we must provide the renderer.

Open a new file and implements the renderer.

class Renderer : GLSurfaceView.Renderer {

    override fun onSurfaceCreated(gl: GL10, config: EGLConfig) {
    }

    override fun onSurfaceChanged(gl: GL10, width: Int, height: Int) {
    }

    override fun onDrawFrame(gl: GL10) {
    }

}

This interface requires the class implements three methods.

  • Create surface
  • Change surface
  • Draw frame

The onSurfaceCreated and onSurfaceChanged are called when the View starts, but, the changed method is called when the user rotate the smarpthone or change the View size.

The onDrawFrame is called every frame and we use it to draw stuff and models.

Before writing the GL functions, you should know the OpenGL is a specification, it's a API that works with state machine.

Let's starts with create the surface.

override fun onSurfaceCreated(gl: GL10, config: EGLConfig) {
    gl.glDisable(GL10.GL_DITHER)
    gl.glHint(GL10.GL_PERSPECTIVE_CORRECTION_HINT, GL10.GL_FASTEST)

    if (translucentBackground) {
        gl.glClearColor(0f,0f,0f,0f)
    } else {
        gl.glClearColor(1f,1f,1f,1f)
    }

    gl.glEnable(GL10.GL_CULL_FACE)
    gl.glShadeModel(GL10.GL_SMOOTH)
    gl.glEnable(GL10.GL_DEPTH_TEST)
}

let's dive into this calls:

The glClearColor defines the color that will be used to clear the color buffer. Which means, when we call glClear for each frame, the user will see that color in buffer.

The gl.glDisable(GL10.GL_DITHER) disable the dithering feature.

Dithering is the technique used for simulating aditional colors and visual effects for limited display.

It works mixing pixels of different colors to see more details, since the some devices has a limited palette.

In this example, we don't need this detailing, so I disabled.

The gl.glHint(GL10.GL_PERSPECTIVE_CORRECTION_HINT, GL10.GL_FASTEST) tells to the OpenGL to do what it think is best, accepting certain tradeoffs like speed or quality.

In this case, I'm telling to OpenGL to prioritize speed over quality when correcting perspective.

It's useful for cases where there are much 3D objects to render at same time.

The gl.glEnable(GL10.GL_CULL_FACE) is a technique for culling the faces and improve the rendering quality by removing objects face that will not visible.

The OpenGL starts discarding objects face that face away from the camera.

The gl.glShadeModel(GL10.GL_SMOOTH) enable the smooth shading for the colors at surface.

The gl.glEnable(GL10.GL_DEPTH_TEST) ensure that the rendering of objects in the screen is done correctly in relation to its depth in the scene.

OpenGL compares the depth (distance from camera) of each pixel being rendered with the depth stored in the depth buffer. If the current pixel is closer to the camera than the pixel already drawn, it will be drawn on the screen. If it's further away, it will discared.

In the surface changed, you must define dimensions to project a volume (3D space) into a 2D screen. Let's see.

override fun onSurfaceChanged(gl: GL10, width: Int, height: Int) {
    gl.glViewport(0, 0, width, height) // left-bottom 0x0

    gl.glMatrixMode(GL10.GL_PROJECTION)
    gl.glLoadIdentity()

    val ratio = width.toFloat() / height
    gl.glFrustumf(-ratio, ratio, -1f, 1f, 1f, 10.0f)
}

The gl.glViewport defines the viewport dimension (top-left) for rendering the 3D world.

The gl.glMatrixMode(GL10.GL_PROJECTION) uses the projection matriz. It's used to transform the coordinates of three-dimensional objects to the 2D screen space. It's required to be called before glFrustum or glOrtho.

The gl.glLoadIdentity() is the identity matrix. It is used as a starting point for geometric transformations. You are basically resetting the current matrix to the identity matrix. In the other words, begins with a clear transformation, specially before models.

For example, if you want to position an object in the world without any previous transformations affecting it, you can call glLoadIdentity() to make sure it starts at your original position without rotation or scaling.

The gl.glFrustumf(-ratio, ratio, -1f, 1f, 1f, 10.0f) is used for perspective projection to determine which objects are inside of camera view and must be rendered on the screen.

Now, in the onDrawFrame, we have:

override fun onDrawFrame(gl: GL10) {
    gl.glClear(GL10.GL_COLOR_BUFFER_BIT.or(GL10.GL_DEPTH_BUFFER_BIT))

    gl.glMatrixMode(GL10.GL_MODELVIEW)
    gl.glLoadIdentity()

    gl.glTranslatef(0.0f, sin(transY), -3.0f)

    gl.glEnableClientState(GL10.GL_VERTEX_ARRAY)
    gl.glEnableClientState(GL10.GL_COLOR_ARRAY)

    square.draw(gl)

    transY += 0.03f
}

The gl.glClear(GL10.GL_COLOR_BUFFER_BIT.or(GL10.GL_DEPTH_BUFFER_BIT)) clear two buffer data.

  • DEPTHBUFFER: The depth buffer is used to determine the sort of rendering objects based on camera. Clear this buffer ensure that the next objects will be rendering correctly.
  • COLORBUFFER: The buffer stores the colors of pixels. So, let's clear it to the next frame.

The gl.glMatrixMode(GL10.GL_MODELVIEW) set the actual matrix to be model view. I mean, the next operations affects the models (objects).

Call the glLoadIdentity again.

The gl.glTranslatef(0.0f, sin(transY), -3.0f) multiple the actual matrix with a new transaformation matrix. This cause a translate (movement) for actual matrix.

The gl.glEnableClientState(GL10.GL_VERTEX_ARRAY) and gl.glEnableClientState(GL10.GL_COLOR_ARRAY) indicates to OpenGL that we will use vertex arrays and colors arrays.

This step will handle by our entity.

Now, let's create a new file Square and define the vertex buffer array, index buffer array and color buffer array.

class Square {

    private val vertices = floatArrayOf(
        -1.0f, -1.0f,
        1.0f, -1.0f,
        -1.0f, 1.0f,
        1.0f, 1.0f
    )

    private val maxColor = 255.toByte()

    private val colors = byteArrayOf(
        // r, g, b, a
        maxColor, maxColor, 0, maxColor,        // for vertice 1
        0, maxColor, maxColor, maxColor,        // for vertice 2
        maxColor, maxColor, maxColor, maxColor, // for vertice 3
        maxColor, 0, maxColor, maxColor,        // for vertice 4
    )

    private val indices = byteArrayOf(
        0, 3, 1, // triangle 1
        0, 2, 3  // triangle 2
    )

    private val vertexBuffer = ByteBuffer.allocateDirect(vertices.size * 4).apply {
        order(ByteOrder.nativeOrder())
        asFloatBuffer().apply {
            put(vertices)
            position(0)
        }
    }

    private val colorBuffer = ByteBuffer.allocateDirect(colors.size).apply {
        put(colors)
        position(0)
    }

    private val indexBuffer = ByteBuffer.allocateDirect(indices.size).apply {
        put(indices)
        position(0)
    }

    fun draw(gl: GL10) {
        gl.glFrontFace(GL11.GL_CW) // front face of tiangle are clockwise

        gl.glVertexPointer(2, GL11.GL_FLOAT, 0, vertexBuffer)
        gl.glColorPointer(4, GL11.GL_UNSIGNED_BYTE, 0, colorBuffer)

        gl.glDrawElements(GL11.GL_TRIANGLES, 6, GL11.GL_UNSIGNED_BYTE, indexBuffer)

        // reset front face of triangle to the default counterclockwise
        gl.glFrontFace(GL11.GL_CCW)
    }

}

The three buffer was allocate using java.nio.ByteBuffer and reset to position zero at the beginning.

The draw method is responsible for render the vertex with some colors.

The gl.glFrontFace(GL11.GL_CW) setting is important because some rendering operations, like face culling (removing faces that are not visible), depend this definition to determine which faces should be drawn. If the/ face orientation is incorrect, this can lead to unexpected results/ in rendering.

The gl.glDrawElements(GL11.GL_TRIANGLES, 6, GL11.GL_UNSIGNED_BYTE, indexBuffer) draws our square.

1734628399.png