OpenGL introduction

OpenGL 1.1 specification

OpenGL is a cross-platform graphics API for specifying a standard software interface for 3D graphics processing hardware. OpenGL ES is a form of the OpenGL specification for embedded devices. Android supports multiple versions of the OpenGL ES API (it is recommended to use OpenGL ES 2.0 API version on the latest Android devices) :

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

1.2 Basic Classes of OpenGL framework

In the Android framework, GLSurfaceView is a view container for graphics drawn using OpenGL, and GlSurfaceView.renderer can control the graphics drawn in this view.

1.2.1 GLSurfaceView

This class is a View. For full-screen or near-full-screen graphics views, GLSurfaceView makes sense. In addition, if you want to integrate OpenGL ES graphics into a small part of their layout, you can also consider using TextureView. SurfaceView can also be used as an OpenGL view container, but it requires a lot of code and is not recommended.

1.2.2 GLSurfaceView. The Renderer

This interface defines the methods needed to draw graphics in the GLSurfaceView. Will this interface implementation class by GLSurfaceView. SetRenderer () associated with GLSurfaceView instance. The glsurfaceView.renderer interface requires the following methods:

  • onSurfaceCreated(): The system is creatingGLSurfaceViewCall this method once when. Typically used to set up operations that occur only once, such as setting OpenGL environment parameters or initializing OpenGL graphics objects.
  • onDrawFrame(): The system will be redrawn each timeGLSurfaceViewIs called. Is the main method of drawing images to GLSurfaceView.
  • onSurfaceChanged(): The system will be inGLSurfaceViewThe geometry changes (includingGLSurfaceViewThis method is called when the size changes or the device screen orientation changes. For example, the system calls this method when the device screen orientation changes from portrait to landscape.

1.3 OpenGL Standardizes device coordinates

OpenGL has Normalized Device Coordinates (NDC), which are up on the Y-axis, instead of assuming a uniform square coordinate system for the screen. The (0, 0) coordinate is the center of the picture, not the top left corner. Standardized device coordinates are a coordinate system with x, y, and Z values between -1.0 and 1.0. Any coordinates that fall outside the range are discarded/clipped and will not be drawn on the phone screen. For example, three points of a triangle are represented in the OpenGL standard coordinate system as follows (z axis is all 0, ignore z axis for the moment) :

2. Basic Usage

Let’s get started with OpenGL with a simple example of drawing triangles.

2.1 Building OpenGL ES Environment

2.1.1 Declaration List

OpenGL ES 2.0 glEsVersion OpenGL ES 2.0 glEsVersion OpenGL ES 2.0


      
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools" package="com.bc.example">
    <uses-feature android:glEsVersion="0x00020000" android:required="true" />
</manifest>
Copy the code

2.1.2 Creating GLSurfaceView and Render

GLSurfaceView is a view container for graphics drawn using OpenGL, and GLSurfaceView.renderer controls the graphics drawn in that view.

<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android">
    <com.bc.example.opengl.MyGLSurfaceView
        android:id="@+id/my_gl_surface_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>
</FrameLayout>
Copy the code

Associate GLSurfaceView with Render in a custom MyGLSurfaceView:

class MyGLSurfaceView(context: Context? , attrs: AttributeSet?) : GLSurfaceView(context, attrs) {private val renderer: MyGLRenderer

    init {
        Create an OpenGL ES 2.0 context
        setEGLContextClientVersion(2)
        renderer = MyGLRenderer()
        // GLSurfaceView associated with Render
        setRenderer(renderer)
    }
}
Copy the code

Glsurfaceview. Renderer does the actual rendering, and only sets the background to black:

class MyGLRenderer : GLSurfaceView.Renderer {
    override fun onSurfaceCreated(gl: GL10? , config:EGLConfig?). {
        // Set the background color to black
        GLES20.glClearColor(0.0 f.0.0 f.0.0 f.1.0 f)}override fun onSurfaceChanged(gl: GL10? , width:Int, height: Int) {
        // OpenGL uses standardized device coordinates;
        GLES20.glViewport(0.0, width, height)
    }

    override fun onDrawFrame(gl: GL10?). {
        GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT)
    }
}
Copy the code

At this point, when you open your APP, you’ll see a GLSurfaceView with a black background.

2.2 Extended drawing methods

If other content needs to be drawn in OpenGL, it can be expanded in the onDrawFrame() method. In large OpenGL projects, the template design mode similar to the View system of the Android system is generally adopted (that is, the ViewGroup calls the draw() method of the sub-view, layer by layer). The main work of drawing a Triangle is done in our custom Triangle class. We just call Triangle in onDrawFrame() to draw the Triangle:

class MyGLRenderer : GLSurfaceView.Renderer {

    private lateinit var triangle: Triangle

    override fun onSurfaceCreated(gl: GL10? , config:EGLConfig?). {
        GLES20.glClearColor(0.0 f.0.0 f.0.0 f.1.0 f)
        // In our example, we manipulate GLES20 in the Triangle constructor, so be sure to create the Triangle object onSurfaceCreated
        triangle = Triangle()
    }

    override fun onSurfaceChanged(gl: GL10? , width:Int, height: Int) {
        GLES20.glViewport(0.0, width, height)
    }

    override fun onDrawFrame(gl: GL10?). {
        GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT)
        // Draw a triangle
        triangle.draw()
    }
}
Copy the code

2.3 Defining Shapes

Continuing with Triangle’s main steps, OpenGL uses FloatBuffer to manage vertex data to improve efficiency. A triangle needs to be represented by three vertices. These vertices are handed to OpenGL in FloatBuffer format. Here’s how they are defined:

class Triangle {
    // The coordinate values of the three points of the triangle (counterclockwise, in 3D coordinates, the direction determines which side is facing)
    private var triangleCoords = floatArrayOf(
        0.0 f.0.622008459 f.0.0 f.// top
        -0.5 f, -0.311004243 f.0.0 f.// bottom left
        0.5 f, -0.311004243 f.0.0 f      // bottom right
    )

    // Set the colors (red, green, blue and alpha)
    private val color = floatArrayOf(0.63671875 f.0.76953125 f.0.22265625 f.1.0 f)

    private var vertexBuffer: FloatBuffer =
        // Number of coordinate points * float in bytes
        ByteBuffer.allocateDirect(triangleCoords.size * 4)
            .order(ByteOrder.nativeOrder()).asFloatBuffer().apply {
                // Add the coordinates to the FloatBuffer
                put(triangleCoords)
                // Set the position of buffer to 0
                position(0)}}Copy the code

2.3.1 Vertex buffer object

Three terms are introduced here:

  • Vertex Array Object (VAO) Vertex Array Object (VAO)
  • Vertex Buffer Object (VBO) Vertex Buffer Object (vertexBuffer)
  • Index Buffer Object: Element Buffer Object, EBO or Index Buffer Object, IBO, which represents an array of Index vertices, described in Section 3.2, used to describe the order in which vertices are reused.

OpenGL stores a large number of vertices in GPU memory, which is managed using Vertex Buffer Objects (VBO). The advantage of using the buffer object VBO is that we can send a large number of data to the graphics card at once, rather than each vertex once. Sending data from the CPU to the graphics card is relatively slow, so whenever possible we try to send as much data as possible at once. Vertex shaders can access vertices almost immediately after data is sent to GPU memory, which is a very fast process.

The vertex array object and the vertex buffer object are as follows:

2.4 Drawing a Triangle

Against 2.4.1 GLSL

To render graphics, OpenGL requires us to set up at least one vertex shader and one fragment shader. The first thing we need to do is write the vertex shader in GLSL(OpenGL Shading Language) and then compile the shader so we can use it in our shader program.

2.4.2 Vertex shaders

Vertex shaders are OpenGL ES graphic code for rendering shapes of vertices. The source code for a GLSL vertex shader is shown below:

Private val vertexShaderCode = "Attribute vec4 vPosition;" + "void main() {" + " gl_Position = vPosition;" + "}"Copy the code
  • Attribute is a GLSL keyword that declares an attribute;
  • Vec4 is the GLSL data type keyword and contains the default vector of four float components
  • VPosition is the developer’s custom variable name;
  • Gl_Position is the vertex shader object of GLSL. Here we assign the value of our custom vPosition to the past. Later we will take out the data from the vertex shader in the shader program.

2.4.3 Fragment shader

Fragment shaders are OpenGL ES code for rendering shapes with colors or textures. The main job is to calculate the final color output of pixels. The source code for a fragment shader is as follows:

/** * fragment shader code */
private val fragmentShaderCode =
    "precision mediump float;" +
            "uniform vec4 vColor;" +
            "void main() {" +
            " gl_FragColor = vColor;" +
            "}"
Copy the code
  • Uniform is the GLSL keyword, which is a way to send data from applications in the CPU to shaders in the GPU; Unlike ordinary attributes, UNIFORM variables are Global, meaning that they are unique within each shader program object and can be accessed at any stage by any shader of a shader program. Second, Whatever you set uniform values to, Uniform will keep their data until they are reset or updated.
  • VColor is the developer’s custom variable name;
  • Gl_FragColor is a fragment shader object of GLSL. Here we assign our custom vColor to the object. Later we will retrieve it from the shader program and operate on it.

2.4.4 Compiling shader code

To enable OpenGL to use the shader code described above, it first needs to dynamically compile its source code at run time. Compilation is performed only once and is typically done in the constructor of the drawn object.

/** * compile shader *@paramType Indicates the shader type: gles20.gl_vertex_shader gles20.gl_fragment_shader *@paramShaderCode shader source code; The aforementioned hard-coded GLSL code **/
private fun loadShader(type: Int, shaderCode: String): Int {
    The glCreateShader function creates a vertex shader or fragment shader and returns a reference to the ID of the newly created shader
    val shader = GLES20.glCreateShader(type)
    // Associate the shader with the code, then compile the shader
    GLES20.glShaderSource(shader, shaderCode)
    GLES20.glCompileShader(shader)
    return shader
}
Copy the code

2.4.5 Shader program

To use the shaders we just compiled we must Link them to a shader program object and then activate the shader program when rendering the object. The shader of the activated shader program will be used when we send the render call.

class Triangle {
    init {
        // Compile vertex shaders and fragment shaders
        val vertexShader: Int = loadShader(GLES20.GL_VERTEX_SHADER, vertexShaderCode)
        val fragmentShader: Int = loadShader(GLES20.GL_FRAGMENT_SHADER, fragmentShaderCode)
        The glCreateProgram function creates a shader program and returns an ID reference to the newly created program object
        mProgram = GLES20.glCreateProgram().also {
            // Add a vertex shader to the program object
            GLES20.glAttachShader(it, vertexShader)
            // Add fragment shaders to program objects
            GLES20.glAttachShader(it, fragmentShader)
            // Connect and create an executable OpenGL ES program object
            GLES20.glLinkProgram(it)
        }
    }
}
Copy the code

2.4.6 summary

Let’s concatenate the above processes, activate the shader program in the method draw() that is executed during the actual drawing, and then manipulate the vertex shader and fragment shader. Here is the complete flow code for drawing a triangle:

class Triangle {
    // The coordinate values of the three points of the triangle (counterclockwise, in 3D coordinates, the direction determines which side is facing)
    private var triangleCoords = floatArrayOf(
        0.0 f.0.622008459 f.0.0 f.// top
        -0.5 f, -0.311004243 f.0.0 f.// bottom left
        0.5 f, -0.311004243 f.0.0 f      // bottom right
    )
    // Number of coordinates for each vertex
    const val COORDS_PER_VERTEX = 3

    // Set the colors (red, green, blue and alpha)
    private val color = floatArrayOf(0.63671875 f.0.76953125 f.0.22265625 f.1.0 f)

    private var vertexBuffer: FloatBuffer =
        // Number of coordinate points * float in bytes
        ByteBuffer.allocateDirect(triangleCoords.size * 4)
            .order(ByteOrder.nativeOrder()).asFloatBuffer().apply {
                // Add the coordinates to the FloatBuffer
                put(triangleCoords)
                // Set the position of buffer to 0
                position(0)}/** * Vertex shader code; * /
    private val vertexShaderCode =
        "attribute vec4 vPosition;" +
                "void main() {" +
                " gl_Position = vPosition;" +
                "}"

    /** * fragment shader code */
    private val fragmentShaderCode =
        "precision mediump float;" +
                "uniform vec4 vColor;" +
                "void main() {" +
                " gl_FragColor = vColor;" +
                "}"

    /** * shader program ID reference */
    private var mProgram: Int

    init {
        // Compile vertex shaders and fragment shaders
        val vertexShader: Int = loadShader(GLES20.GL_VERTEX_SHADER, vertexShaderCode)
        val fragmentShader: Int = loadShader(GLES20.GL_FRAGMENT_SHADER, fragmentShaderCode)
        The glCreateProgram function creates a shader program and returns an ID reference to the newly created program object
        mProgram = GLES20.glCreateProgram().also {
            // Add a vertex shader to the program object
            GLES20.glAttachShader(it, vertexShader)
            // Add fragment shaders to program objects
            GLES20.glAttachShader(it, fragmentShader)
            // Connect and create an executable OpenGL ES program object
            GLES20.glLinkProgram(it)
        }
    }

    private fun loadShader(type: Int, shaderCode: String): Int {
        The glCreateShader function creates a vertex shader or fragment shader and returns a reference to the ID of the newly created shader
        val shader = GLES20.glCreateShader(type)
        // Associate the shader with the code, then compile the shader
        GLES20.glShaderSource(shader, shaderCode)
        GLES20.glCompileShader(shader)
        return shader
    }

    private val vertexCount: Int = triangleCoords.size / COORDS_PER_VERTEX
    private val vertexStride: Int = COORDS_PER_VERTEX * 4 // 4 bytes per vertex

    /** * the method to execute when actually drawing **/
    fun draw(a) {
        // Activate the shader program and add it to the OpenGL ES environment
        GLES20.glUseProgram(mProgram)
        // Get the vPosition variable in the vertex shader (it can be obtained from the shader program because the shader code has been compiled previously); It's represented by a unique ID
        val position = GLES20.glGetAttribLocation(mProgram, "vPosition")
        // Allows manipulation of the vertex object position
        GLES20.glEnableVertexAttribArray(position)
        // Pass vertex data to the vPosition variable pointed to by position; Associates a vertex property with a vertex buffer object
        GLES20.glVertexAttribPointer(
            position, COORDS_PER_VERTEX, GLES20.GL_FLOAT,
            false, vertexStride, vertexBuffer)
        // Get the vColor variable in the fragment shader
        val colorHandle = GLES20.glGetUniformLocation(mProgram, "vColor")
        // Use colorHandle to set the color value to draw
        GLES20.glUniform4fv(colorHandle, 1, color, 0)
        // Draw the vertex array;
        GLES20.glDrawArrays(GLES20.GL_TRIANGLES, 0, vertexCount)
        // After the operation, the vertex object position is disallowed
        GLES20.glDisableVertexAttribArray(position)
    }
}
Copy the code

The drawing result is as follows:

Three, draw the quadrilateral

OpenGL only supports drawing points, lines, and triangles. For drawing a quadrilateral, the typical way in OpenGL ES is to use two triangles drawn together:

3.1 Methods of drawing geometric figures

OpenGL ES provides two classes of methods for drawing a spatial geometry:

  • Public abstract void glDrawArrays(int mode, int first, int count) draw using VetexBuffer. The order of vertexBuffer specifies the vertexBuffer.
  • Public abstract void glDrawElements(int mode, int count, int Type, Buffer indices), can redefine the order of vertices specified by the indices Buffer.

3.2 Index buffer objects

Both methods can be used to draw quadrangles, but glDrawElements is more flexible by representing the drawing order between vertices through another indexed array. The index array tells the OpenGL ES graphics pipeline in what order to draw these vertices. Likewise, indexed arrays need to be passed to OpenGL in FloatBuffer:

Private val drawOrder = shortArrayOf(0, 1, 2, 0, 2, 3) private val drawOrder = shortArrayOf(0, 1, 2, 2, 3) ShortBuffer = ByteBuffer.allocateDirect(drawOrder.size * 2).order(ByteOrder.nativeOrder()) .asShortBuffer().apply { put(drawOrder) position(0) }Copy the code

3.3 Drawing a Quadrilateral

The code for drawing a quadrilateral is as follows. The overall logic is similar to that of drawing a triangle, except that glDrawElements method is used for drawing:

class Square {
    // Number of coordinates for each vertex
    private val COORDS_PER_VERTEX = 3
    private var squareCoords = floatArrayOf(
        -0.5 f.0.5 f.0.0 f.// top left
        -0.5 f, -0.5 f.0.0 f.// bottom left
        0.5 f, -0.5 f.0.0 f.// bottom right
        0.5 f.0.5 f.0.0 f       // top right
    )

    // A buffer array of four vertices
    private val vertexBuffer: FloatBuffer =
        ByteBuffer.allocateDirect(squareCoords.size * 4).order(ByteOrder.nativeOrder())
            .asFloatBuffer().apply {
                put(squareCoords)
                position(0)}// A sequential array of four vertices
    private val drawOrder = shortArrayOf(0.1.2.0.2.3)

    // A buffer array of four vertices to draw a sequential array
    private val drawListBuffer: ShortBuffer =
        ByteBuffer.allocateDirect(drawOrder.size * 2).order(ByteOrder.nativeOrder())
            .asShortBuffer().apply {
                put(drawOrder)
                position(0)}/** * Vertex shader code; * Temporarily hardcode the source code for the vertex shader in a C-style string */
    private val vertexShaderCode =
        "attribute vec4 vPosition;" +
                "void main() {" +
                " gl_Position = vPosition;" +
                "}"

    /** * fragment shader code */
    private val fragmentShaderCode =
        "precision mediump float;" +
                "uniform vec4 vColor;" +
                "void main() {" +
                " gl_FragColor = vColor;" +
                "}"

    // Set the colors (red, green, blue and alpha)
    private val color = floatArrayOf(0.63671875 f.0.76953125 f.0.22265625 f.1.0 f)

    /** * shader program ID reference */
    private var mProgram: Int

    init {
        // Compile vertex shaders and fragment shaders
        val vertexShader: Int = loadShader(GLES20.GL_VERTEX_SHADER, vertexShaderCode)
        val fragmentShader: Int = loadShader(GLES20.GL_FRAGMENT_SHADER, fragmentShaderCode)
        The glCreateProgram function creates a shader program and returns an ID reference to the newly created program object
        mProgram = GLES20.glCreateProgram().also {
            // Add a vertex shader to the program object
            GLES20.glAttachShader(it, vertexShader)
            // Add fragment shaders to program objects
            GLES20.glAttachShader(it, fragmentShader)
            // Connect and create an executable OpenGL ES program object
            GLES20.glLinkProgram(it)
        }
    }

    private fun loadShader(type: Int, shaderCode: String): Int {
        The glCreateShader function creates a vertex shader or fragment shader and returns a reference to the ID of the newly created shader
        val shader = GLES20.glCreateShader(type)
        // Associate the shader with the code, then compile the shader
        GLES20.glShaderSource(shader, shaderCode)
        GLES20.glCompileShader(shader)
        return shader
    }

    private val vertexStride: Int = COORDS_PER_VERTEX * 4

    fun draw(a) {
        Add program to OpenGL ES environment
        GLES20.glUseProgram(mProgram)
        // Get the vPosition variable in the vertex shader (it can be obtained from the shader program because the shader code has been compiled previously); It's represented by a unique ID
        val position = GLES20.glGetAttribLocation(mProgram, "vPosition")
        // Allows manipulation of the vertex object position
        GLES20.glEnableVertexAttribArray(position)
        // Pass vertex data to the vPosition variable pointed to by position
        GLES20.glVertexAttribPointer(
            position, COORDS_PER_VERTEX, GLES20.GL_FLOAT,
            false, vertexStride, vertexBuffer
        )
        // Get the vColor variable in the fragment shader
        val colorHandle = GLES20.glGetUniformLocation(mProgram, "vColor")
        // Use colorHandle to set the color value to draw
        GLES20.glUniform4fv(colorHandle, 1, color, 0)
        // Draw the quadrilateral in the order specified in drawListBuffer
        GLES20.glDrawElements(
            GLES20.GL_TRIANGLES, drawOrder.size,
            GLES20.GL_UNSIGNED_SHORT, drawListBuffer
        )
        // After the operation, the vertex object position is disallowed
        GLES20.glDisableVertexAttribArray(position)
    }
}
Copy the code

The drawing result is as follows:

3.4 Other Methods

Gles20.gl_triangle_strip: Gles20.gl_triangle_strip: gles20.gl_triangle_strip: gles20.gl_triangle_strip:

class Square {
    / / number of vertices
    private val vertexCount: Int = squareCoords.size / COORDS_PER_VERTEX

    fun draw(a) {
        / / to omit...
        // Use GL_TRIANGLE_FAN to connect four vertices
        GLES20.glDrawArrays(GLES20.GL_TRIANGLE_FAN, 0, vertexCount)
        / / to omit...}}Copy the code

The End

Please follow me to unlock more skills: BC’s home page ~ ~ 💐💐💐

Android OpenGL developer documentation: developer.android.com/guide/topic… Opengl learnopengl-cn.github. IO /