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.