Skip to content
Stanislav Georgiev edited this page Oct 15, 2020 · 3 revisions

Welcome to the OpenGL wiki!

OpenGL is simple library for creating OpenGL shapes using OpenGL ES2.0, and drawing them on a View that can have transformations applied to it using regular graphic 3x3 Matrix. The matrix is then converted in to a 4x4 OpenGL matrix represented as single FloatArray, that holds the values for the matrix. In order to understand the code below and to have overall understanding about OpenGL for Android I recommend to check the official google page here.

OpenGLMatrixGestureDetector

The class OpenGLMatrixGestureDetector is used to apply finger gesture transformations to regular graphic 3x3 Matrix, that is used to create the corresponding OpenGL matrix that is represented as FloatArray. All shapes need gesture detector that is used to convert points from graphic coordinates system to a OpenGL coordinate system. The conversion is needed since its easier to use the graphic coordinate system and that way shapes can be draw the same way as the shapes draw in regular View using canvas.



Image

Before using the methods for conversion in the gesture detector, you must specify the device size. This is done using the static method OpenGLStatic.setComponents() where you set the device width and height using the first and second argument. And the method sets static properties for the device size, the ratio of the with and height. This call to the static method is usually done in the onSurfaceChanged method, in the OpenGLRenderer class. Below are the properties that are set using the static method and are used by the gesture detector during the conversion.

  • DEVICE_WIDTH - device width
  • DEVICE_HEIGHT - device height
  • DEVICE_HALF_WIDTH - half of the device width
  • DEVICE_HALF_HEIGHT - half of the device height
  • RATIO - device width to height (width/height) ratio

Below is example on how to convert coordinates from graphic to OpenGL coordinate system:

// create the gesture detector
val mainGestureDetector: OpenGLMatrixGestureDetector = OpenGLMatrixGestureDetector()

// get the corresponding OpenGL coordinates from point in graphic coordinates system, with coordinates (100f,200f)
val pointOpenGL = PointF()
mainGestureDetector.normalizeCoordinate(100f, 200f, pointOpenGL)

// get the corresponding OpenGL coordinates from multiple points in graphic coordinates system, passed as FloatArray
val coordinates = floatArrayOf(
   100f, 200f, // first point coordinates
   214f, 441f, // second point coordinates
   21f, 122f   // third point coordinates
)
val coordinatesOpenGL = FloatArray(coordinates.size)
mainGestureDetector.normalizeCoordinates(coordinates, coordinatesOpenGL)

// get the corresponding OpenGL width and height, from width and height given in graphic coordinates system
val widthOpenGL = mainGestureDetector.normalizeWidth(100f)
val heightOpenGL = mainGestureDetector.normalizeHeight(100f)

Program

Drawing a defined shape using OpenGL ES 2.0 requires a significant amount of code, because you must provide a lot of details to the graphics rendering pipeline. Preload programs are integer value that represents the vertex and fragment shader that are generated by string usually from the raw *glsl files and using the GLES20 methods the index is generated. Specifically, you must define the following:

  • Vertex Shader - OpenGL ES graphics code for rendering the vertices of a shape.
  • Fragment Shader - OpenGL ES code for rendering the face of a shape with colors or textures.
  • Program - An OpenGL ES object that contains the shaders you want to use for drawing one or more shapes.

You must create the program from any of the methods in the GLSurfaceView.Renderer interface, those are onSurfaceCreated(), onDrawFrame() and onSurfaceChanged(), otherwise a error will be returned that the OpenGL context is not available. And since all shapes need program in order to be defined, the program must be initialized before the shapes. That is why it is good idea to create the program when the surface is created and then shapes can be created at any given moment. You can read more about the program here.

You can define your own program using your string for the shader and the vertex, or use any of the premade *glsl files from the raw folder. There are 3 premade programs you can load:

  • Single Color Program - program used when you want to specify only one color for the each shape (it is also used for single shapes)
  • Multiple Color Program - program used when multiple shapes are created, where you can specify different color for each shape
  • Texture Program - program used when generating texture for Images, Image objects
// create program for shapes that use the same colors
val singleColorsProgram = OpenGLStatic.setSingleColorsProgram()

// create program for shapes that use the different colors
val multipleColorsProgram = OpenGLStatic.setMultipleColorsProgram()

// create program when you need Images or Image objects
val textureProgram = OpenGLStatic.setTextureProgram()

Shapes

Drawing shapes in OpenGL is not all that hard, but to create even the simplest shapes in OpenGL such as Triangle or Rectangle you would have to write your own implementation in order to do so. You can read how to define your own shape here. There are many included shapes that easy to define and have simple methods for drawing or changing there properties. The library has two packages multiple and single for defining single or multiple shapes of the same type. This is done to save resources when defining multiple shapes and it is way faster. The classes for single shapes extend those for multiple shapes and wraps the overall implementation of the classes with some extra properties that are handy to use. The shapes have property style the determining whether the shape is filled or stroked with given color. When creating shape a gesture detector should be set, which is used to convert the position and the size of the shapes from graphic coordinate system to OpenGL coordinate system.

Image

Included single shapes are:

  • Circle
  • Ellipse
  • Image
  • Line
  • PassByCurve
  • Rectangle
  • RegularPolygon
  • Triangle

Included multiple shapes are:

  • Circles
  • Ellipses
  • Images
  • Lines
  • Rectangles
  • RegularPolygons
  • Triangles

Rectangle/Rectangles

To create single rectangle use the class Rectangle, where you specify the top-left corner of the rectangle using the x,y coordinates and the size of the rectangle using the width and height properties. Use the property color to specify the color of the rectangle, which is a 32-bit integer representation. You can set the style to STYLE_FILL or STYLE_STROKE to fill or stroke the rectangle, you can also specify the stroke width as well. To create multiple rectangles each with different color use the class Rectangles, where you specify the position and size as FloatArray.

First define your preloaded programs and gesture detector:

// gesture detector used for applying transformations to all OpenGL objects: line, images, triangles..
val mainGestureDetector: OpenGLMatrixGestureDetector = OpenGLMatrixGestureDetector()

// create program for shapes that use the same colors
val singleColorsProgram = OpenGLStatic.setSingleColorsProgram()

// create program for shapes that use the different colors
val multipleColorsProgram = OpenGLStatic.setMultipleColorsProgram()

// create program for shapes that use texture: Image, Images
val textureProgram = OpenGLStatic.setTextureProgram()

To create single rectangle use the code below:

// create single rectangle
val rectangle = Rectangle(
     x = 300f,
     y = 200f,
     width = 300f,
     height = 150f,
     color = Color.BLACK,
     strokeWidth = 5f,
     gestureDetector = mainGestureDetector,
     preloadProgram = singleColorsProgram,
     style = Shapes.STYLE_FILL
)

To create multiple rectangles with different colors use the code below:

// coordinates info in format: [x1,y1,width1,height1, x2,y2,width2,height2,...]
val coordinatesInfo = floatArrayOf(
    0f, 0f, 200f, 200f,       // first rectangle info
    400f, 400f, 500f, 100f,   // second rectangle info
    200f, 600f, 200f, 100f    // third rectangle info
)

val colors = intArrayOf(
    Color.RED,    // first rectangle color
    Color.GREEN,  // second rectangle color
    Color.GRAY    // third rectangle color
)

// create multiple rectangles
val rectangles = Rectangles(
    coordinatesInfo = coordinatesInfo,  
    colors = colors,
    style = STYLE_FILL,
    strokeWidth = 5f,
    preloadProgram = multipleColorsProgram,
    gestureDetector = mainGestureDetector
)

Triangle/Triangles

To create single triangle use the class Triangle, where you specify the 3 points for the triangle using x,y coordinates. Use the property color to specify the color of the triangle , which is a 32-bit integer representation. You can set the style to STYLE_FILL or STYLE_STROKE to fill or stroke the triangle, you can also specify the stroke width as well. To create multiple triangles each with different color use the class Triangles, where you specify the coordinates for the triangles as FloatArray.

First define your preloaded programs and gesture detector, just as with the rectangle

To create single triangle use the code below:

// create single triangle 
val triangle = Triangle(
    x1 = 40f, y1 = 550f,   // first point coordinates
    x2 = 200f, y2 = 350f,  // second point coordinates
    x3 = 310f, y3 = 650f,  // third point coordinates
    color = Color.BLACK,
    strokeWidth = 5f,
    gestureDetector = mainGestureDetector,
    preloadProgram = singleColorsProgram,
    style = style
)

To create multiple triangle with different colors use the code below:

// coordinates in format: [x1,y1,x2,y2,x3,y3,  x4,y4,x5,y5,x6,y6...]
val coordinates = floatArrayOf(
    490f, 380f, 100f, 540f, 500f, 600f,  // first triangle coordinates
    800f, 200f, 750f, 600f, 400f, 900f,  // second triangle coordinates
    900f, 600f, 950f, 700f, 400f, 900f   // third triangle coordinates
)

val colors = intArrayOf(
    Color.RED,   // first triangle color
    Color.GREEN, // second triangle color
    Color.YELLOW // third triangle color
)

// create multiple triangle 
val triangles = Triangles(
    coordinates = coordinates,
    colors = colors, 
    style = STYLE_FILL,
    strokeWidth = 5f,
    preloadProgram = multipleColorsProgram,
    gestureDetector = mainGestureDetector
)

Line/Lines

To create single line use the class Line, where you specify the 2 points for the line using x,y coordinates. Use the property color to specify the color of the line, which is a 32-bit integer representation. You can specify the stroke width using the property strokeWidth. To create multiple lines each with different color use the class Lines, where you specify the coordinates for the lines as FloatArray.

First define your preloaded programs and gesture detector, just as with the rectangle

To create single line use the code below:

// create single line
val line = Line(
    x1 = 100f, y1 = 800f,   // first point coordinates
    x2 = 300f, y2 = 800f,   // second point coordinates
    color = Color.BLACK,
    strokeWidth = 5f,
    gestureDetector = mainGestureDetector,
    preloadProgram = singleColorsProgram
)

To create multiple lines with different colors use the code below:

// coordinates in format: [x1,y1,x2,y2, x3,y3,x4,y4,...]
val coordinates = floatArrayOf(
     0f, 0f, 200f, 200f,
     200f, 200f, 400f, 200f,
     400f, 200f, 400f, 400f
)

val colors = intArrayOf(
    Color.RED,   // first line color
    Color.GREEN, // second line color
    Color.YELLOW // third line color
)

// create multiple lines
val lines = Lines(
    coordinates = coordinates,
    colors = colors, 
    strokeWidth = 5f,
    preloadProgram = multipleColorsProgram,
    gestureDetector = mainGestureDetector
)

RegularPolygon/RegularPolygons

To create single regular polygon use the class RegularPolygon, where you specify the center of the polygon using x,y coordinates. Use the property color to specify the color of the polygon, which is a 32-bit integer representation. You can set the style to STYLE_FILL or STYLE_STROKE to fill or stroke the polygon, you can also specify the stroke width as well. You can specify the number of vertices that will determine the number of segments the polygon is made of using the propery numberVertices. To create multiple polygons each with different color use the class RegularPolygons, where you specify the position and radius for each polygon as FloatArray.

First define your preloaded programs and gesture detector, just as with the rectangle

To create single polygon use the code below:

// create single regular polygon
val regularPolygon = RegularPolygon(
    x = 500f,
    y = 500f,
    radius = 100f,
    angle = 0f,
    numberVertices = 6,
    color = Color.BLACK,
    strokeWidth = 5f,
    gestureDetector = mainGestureDetector,
    preloadProgram = singleColorsProgram,
    style = style
)

To create multiple polygons with different colors use the code below:

// coordinates in format: [cx1,cy1,radius1,angle1,  cx2,cy2,radius2,angle2,...]
val coordinatesInfo = floatArrayOf(
    600f, 200f, 100f, 90f,  // first polygon info
    550f, 700f, 60f, 45f,   // second polygon info
    200f, 400f, 180f, 0f    // third polygon info
)

val colors = intArrayOf(
    Color.RED,   // first polygon color
    Color.GREEN, // second polygon color
    Color.YELLOW // third polygon color
)

// create multiple regular polygons
val regularPolygons = RegularPolygons(
    coordinatesInfo = coordinatesInfo,
    colors = colors,
    style = STYLE_FILL,
    numberOfVertices = 6,
    strokeWidth = 5f,
    preloadProgram = multipleColorsProgram,
    gestureDetector = mainGestureDetector
)

Circle/Circles

To create single circle use the class Circle, where you specify the center of the circle using x,y coordinates. Use the property color to specify the color of the circle, which is a 32-bit integer representation. You can set the style to STYLE_FILL or STYLE_STROKE to fill or stroke the circle, you can also specify the stroke width as well. To create multiple circles each with different color use the class Circles, where you specify the position and radius for each circle as FloatArray.

First define your preloaded programs and gesture detector, just as with the rectangle

To create single circle use the code below:

// create single circle
val circle = Circle(
    x = 700f, 
    y = 700f, 
    radius = 110f,
    color = Color.BLACK,
    strokeWidth = 5f,
    gestureDetector = mainGestureDetector,
    preloadProgram = singleColorsProgram,
    style = style
)

To create multiple circles with different colors use the code below:

// coordinates in format: [cx1,cy1,radius1,angle1,  cx2,cy2,radius2,angle2,...]
val coordinatesInfo = floatArrayOf(
    130f, 130f, 100f, 0f,  // first circle coordinate info
    400f, 400f, 50f, 0f,   // second circle coordinate info
    700f, 200f, 150f, 0f   // third circle coordinate info
)

val colors = intArrayOf(
    Color.RED,   // first circle color
    Color.GREEN, // second circle color
    Color.YELLOW // third circle color
)

// create multiple circles
val circles = Circles(
    coordinatesInfo = coordinatesInfo,
    colors = colors,
    style = STYLE_FILL, 
    strokeWidth = 5f,
    preloadProgram = multipleColorsProgram,
    gestureDetector = mainGestureDetector
)

Image/Image

To create single image use the class Image, where you specify the position of the image using as the top-left corner of the image use the x,y coordinates. Before creating a image a texture have to be generated from a bitmap or image file from the drawable folder. You have to set the bitmap size using the bitmapWidth and bitmapHeight properties, since it is used to calculate the OpenGL image size when the width or height are set as WRAP_CONTENT, MATCH_DEVICE or AUTO_SIZE. The size of the image is set using the width and height properties. If you want to keep the size of the image even when the sceen is scaled by the gesture detector, use the property keepSize. To use the position of the image to determine the center of the image instead of the top-left corner use the property usePositionAsCenter. To create multiple image each with its own positon use the class Images, where you specify the position for each image as FloatArray. When creating multiple images the same texture will be use, that means the images will be the same but with different positions.

First define your preloaded programs and gesture detector, just as with the rectangle

To create single circle use the code below:

// generate texture handle by image file from the drawable folder
val textureHandler = OpenGLStatic.loadTexture(context, R.drawable.earth)

// create single image
val image = Image(
    bitmapWidth = 302f,
    bitmapHeight = 303f,
    x = 50f,
    y = 50f,
    width = 100f,
    height = 100f,
    textureHandle = textureHandler,
    preloadProgram = textureProgram,
    keepSize = true,
    usePositionAsCenter = true,
    gestureDetector = mainGestureDetector
) 

To create multiple images with different positions and same texture use the code below:

// positions in format: [x1,y1, x2,y2, x3,y3,...]
val positions = floatArrayOf(
    130f, 130f,  // first image position 
    400f, 400f,  // second image position 
    700f, 200f   // third image position 
) 

// create multiple images with same texture
val images = Images( 
    positions = positions,
    preloadProgram = program,
    textureHandle = textureHandler,
    gestureDetector = mainGestureDetector,
    bitmapWidth = 302f,
    bitmapHeight = 303f,
    width = 25f,
    height = 25f
)

How to create you first OpenGL program

Fist we need to create helper class, where the shapes will be created and method for the drawing of the shape will be created. Create a class named OpenGLHelper.kt, and put the code below:

class OpenGLHelper : View.OnTouchListener {

    // gesture detector used for applying transformations to all OpenGL objects: line, images, triangles..
    var mainGestureDetector: OpenGLMatrixGestureDetector =
                             OpenGLMatrixGestureDetector()

    lateinit var line: Line                          // OpenGL line object
    lateinit var requestRenderListener: (() -> Unit) // listener for requesting new rendering(redrawing of the scene)

    fun createShapes() {

        // preload program in case a shape need to be created when the OpenGL context is not available 
        val singleColorsProgram = OpenGLStatic.setSingleColorsProgram()

        // create shapes
        line = Line(
            x1 = 100f, y1 = 800f,
            x2 = 300f, y2 = 800f,
            color = Color.BLACK,
            strokeWidth = 10f,
            gestureDetector = mainGestureDetector,
            preloadProgram = singleColorsProgram
        )
    }

    override fun onTouch(v: View, event: MotionEvent): Boolean {

        // apply finger gesture to the matrix with transformations
        mainGestureDetector.onTouchEvent(event)
        requestRenderListener.invoke()
        return true
    }

    fun draw(transformedMatrixOpenGL: FloatArray) {

        // draw shapes
        line.draw(transformedMatrixOpenGL) 
    }
}



Now we need to define a renderer class, that implements the GLSurfaceView.Renderer interface and all its methods that are used when the surface is created and changed. In this class we make initial call to the helper class for creating the shapes, and also initializing the gesture detector and device size and ratio.

class OpenGLRenderer(val context: Context, var openGLHelper: OpenGLHelper, requestRenderListener: (() -> Unit)) : GLSurfaceView.Renderer {

    private var MVPMatrix: FloatArray                       // model view projection matrix
    private var projectionMatrix: FloatArray                // matrix with applied projection
    private var viewMatrix: FloatArray                      // view matrix
    private val transformedMatrixOpenGL: FloatArray         // matrix with transformation applied by finger gestures as OpenGL values
    private val totalScaleMatrix: android.graphics.Matrix   // the graphic matrix, that has the total scale of both scale and transformation matrices

    init {

        OpenGLStatic.DEVICE_HALF_WIDTH = 0f
        OpenGLStatic.DEVICE_HALF_HEIGHT = 0f

        // set default OpenGL matrix
        MVPMatrix = FloatArray(16)
        viewMatrix = FloatArray(16)
        projectionMatrix = FloatArray(16)
        transformedMatrixOpenGL = FloatArray(16)
        totalScaleMatrix = android.graphics.Matrix()
        OpenGLStatic.setShaderStrings(context)
        openGLHelper.requestRenderListener = requestRenderListener
    }


    override fun onSurfaceCreated(unused: GL10, config: EGLConfig) {
        // set the background frame color
        GLES20.glClearColor(1f, 1f, 1f, 1f)
    }

    override fun onDrawFrame(unused: GL10) {

        // clear the scene
        GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT)

        // set MVPMatrix 
        Matrix.setLookAtM(viewMatrix, 0, 0f, 0f, -3f, 0f, 0f, 0f, 0f, 1.0f, 0.0f)
        Matrix.multiplyMM(MVPMatrix, 0, projectionMatrix, 0, viewMatrix, 0)

        // get OpenGL matrix with the applied transformations, from finger gestures
        openGLHelper.mainGestureDetector.transform(MVPMatrix, transformedMatrixOpenGL)

        // draw all elements
        openGLHelper.draw(transformedMatrixOpenGL)
    }
 
    override fun onSurfaceChanged(unused: GL10, width: Int, height: Int) {

        // if alpha is enabled
        if (OpenGLStatic.ENABLE_ALPHA) {
            GLES20.glEnable(GLES20.GL_BLEND)
            GLES20.glBlendFunc(GLES20.GL_SRC_ALPHA, GLES20.GL_ONE_MINUS_SRC_ALPHA)
        }

        // set device size and ratio
        OpenGLStatic.setComponents(width.toFloat(), height.toFloat())

        // adjust the viewport based on geometry changes, such as screen rotation
        GLES20.glViewport(0, 0, width, height)

        // this projection matrix is applied to object coordinates
        Matrix.frustumM(projectionMatrix, 0, -OpenGLStatic.RATIO, OpenGLStatic.RATIO, -1f, 1f, 3f / OpenGLStatic.NEAR, 7f)

        // translate to center, make the openGL point(0,0) the center of the device
        openGLHelper.mainGestureDetector.matrix = android.graphics.Matrix()
        openGLHelper.mainGestureDetector.matrix.postTranslate(OpenGLStatic.DEVICE_HALF_WIDTH, OpenGLStatic.DEVICE_HALF_HEIGHT)

        // create the shapes
        openGLHelper.createShapes()
    }
}



Finally we need to create view class that extends the GLSurfaceView, this will be the view that will display the OpenGL scene will all the shapes that are drawn on it. Below is a example code of creating the view:

class OpenGLSurfaceView : GLSurfaceView {

    constructor(context: Context) : super(context)
    constructor(context: Context, attrs: AttributeSet) : super(context, attrs)

    val openGLRenderer: OpenGLRenderer
    var openGLHelper: OpenGLHelper

    init {

        // whether to enable alpha
        if (ENABLE_ALPHA) {
            holder.setFormat(PixelFormat.TRANSLUCENT)
        }

        // create an OpenGL ES 2.0 context
        setEGLContextClientVersion(2)

        // fix for error No Config chosen
        if (ENABLE_ANTIALIASING) {
            setEGLConfigChooser(OpenGLConfigChooser())
        } else {
            this.setEGLConfigChooser(8, 8, 8, 8, 16, 0)
        }

        // create the helper object
        openGLHelper = OpenGLHelper()
        this.setOnTouchListener(openGLHelper)

        // set the Renderer for drawing on the GLSurfaceView
        openGLRenderer = OpenGLRenderer(context, openGLHelper) {
            requestRender()
        }
        setRenderer(openGLRenderer)

        // render the view only when there is a change in the drawing data
        renderMode = RENDERMODE_WHEN_DIRTY
    }
}