OpenGL ES 2.0 Graphics Programming for Android JAVA - samrg123/JniTeapot GitHub Wiki

OpenGL ES 2.0 Graphics Programming for Android - JAVA


Table of contents:

  1. Overview of OpenGL ES on Android

  2. Setting up an OpenGL ES App
    2.1 Setting up the Manifest files
    2.2 Setting up an Activity
    2.3 Setting up a GLSurfaceView
    2.4 Setting up a GLSurfaceView.Renderer

  3. Drawing To The Screen
    3.1 Setting up A Vertex Buffer
    3.2 Setting up A Vertex Shader
    3.3 Setting up A Fragment Shader
    3.4 Creating an OpenGL Render Program
    3.5 Drawing the Vertex Buffer




Section 1: Overview of how OpenGL ES 2.0 works on Android

Android uses the following three classes to wrap the openGL ES (gles) native backend into JAVA


This the class any application inherits from. It isn't part of gles, but is still essential for creating a gles app. It's used to instantiate your app and handles event callbacks from the OS such as like touch events.


gles only specifies how to render graphics, it doesn't specify any information about how the underlying windowing service should display graphics on the screen or handle graphics context switching across threads. This is instead handled by a separate OS specific graphics API (WGL on windows, GLX on Linux, CGL on OSX). Similarly Android uses the EGL API to create gles contexts and draw them to the screen. GLSurfaceView acts as a wrapper for this class and like its name implies, extends the View class used to display windows on the screen.


This is the class used to update and draw to the gles context. It's important to know that Android creates a separate thread for for the gles context and that the context can only be updated from within that thread. This is achieved by overriding three abstract methods in GLSurfaceView.Render:

  1. onSurfaceCreated: Called when GLSurface creates a new EGL surface for drawing the gles context
  2. onSurfaceChanged: Called when GLSurface changes the size of an existing EGL surface.
    Note: this is also called when the surface is created with its initial dimensions
  3. onDrawFrame: Called before the current frame is drawn to screen

WARNING: If you try to update the gles context outside of these three threads your the updates will be ignored!




Section 2: Setting up and OpenGL ES 2.0 App

Here we will explain how to setup a the framework for an gles app on Android. There are four sections describing how to this, one for stetting up the Android manifest file and one for each class talked about in the Overview. Because each of these classes work hand and hand with each other each section must be implemented before writing gles render code.


2.1 Setting up the Manifest:

Because our app uses gles 2.0 we want to make sure that the app will only be installed on devices that support it. We enforce this requirement by adding the following line to the AndroidManifest.xml file:

<!-- Tell the system this app requires OpenGL ES 2.0. -->
<uses-feature android:glEsVersion="0x00020000" android:required="true" />

Note: If you want to use any OpenGL texture compression methods make sure to also add a . See Example


While we're here we also want to make sure that android knows what our default activity is by adding the following inside the application tag:

<activity android:name=".MyActivity">
    <intent-filter>

        <!-- Make this the default activity for the app -->
        <action android:name="android.intent.action.MAIN" />

        <!-- Show the activity in the android launcher drawer -->
        <category android:name="android.intent.category.LAUNCHER" />
    </intent-filter>
</activity>

The Final AndroidManifest.xml file should look something like:

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.eecs487.myapplication">

    <!-- Tell the system this app requires OpenGL ES 2.0. -->
    <uses-feature android:glEsVersion="0x00020000" android:required="true" />

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/AppTheme"
    >
        <activity android:name=".MyActivity">
            <intent-filter>

                <!-- Make this the default activity for the app -->
                <action android:name="android.intent.action.MAIN" />

                <!-- Show the activity in the android launcher drawer -->
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>
</manifest>

2.2 Setting up the Activity:

Below is a code snippet of how to prepare an Activity for gles:

public class MyActivity extends Activity {

    private MyGLSurfaceView glView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        // Instantiate an EGL Surface View
        glView = new MyGLSurfaceView(this);

        // display the surface
        setContentView(glView);
    }
}

Note: The savedInstanceState variable holds past information we saved about our activity in Activity::onSaveInstanceState. This gets called right before our app is purged from memory (Activity::onDestroy is not guaranteed to be called). Because we don't save any information to the bundle, savedInstanceState is null and we can safely ignore it


2.3 Setting up a GLSurfaceView:

Below is a code snippet of how to prepare a GLSurfaceView for gles:

public class MyGLSurfaceView extends GLSurfaceView {

    private Context activity;
    private MyRenderer renderer;

    MyGLSurfaceView(Context activity_) {

        // prepare the GLSurfaceView by calling its 
        // constructor with a handle to our activity
        super(activity_);
        activity = activity_;

        // Tell the GLSurfaceView eglFactory to create a egl 
        // context that supports openGLES 2.0
        setEGLContextClientVersion(2);

        // Instantiate our renderer and tell GLSurface to use it
        renderer = new MyRenderer(this);
        setRenderer(renderer);
    }
}

Note: As it stands each time our app is paused GLSurfaceView will destroy our egl render context and recreate one when the app resumes. This saves resources when our app is no longer running in the foreground, but can lead to performance issues if our app is frequently paused. To resolve this call setPreserveEGLContextOnPause(true) to inform GLSurface not to destroy the egl context.

Note: By default GLSurfaceView will render an new frame at the phones refresh rate. You can change this behavior by calling: setRenderMode(RENDERMODE_WHEN_DIRTY) to only render a frame when a surface is created and when requestRender() is called. This can help improve battery life if you are only updating the screen intermittently.


2.4 Setting up a OpenGL Renderer:

Below is a code snippet of how to prepare a GLSurfaceView.Renderer for gles:

public class MyRenderer implements GLSurfaceView.Renderer {

    private GLSurfaceView view;
    private int width, height;

    public MyRenderer(GLSurfaceView v) {

        // Cache an reference to the GLSurfaceView we're attached to.
        // This will allow us to query the height and with of the surface 
        // as soon as our surface is created, but before Android tells us 
        // its dimensions in `onSurfaceChanged` is called.
        view = v;
    }

    @Override
    public void onSurfaceCreated(GL10 gl10, EGLConfig eglConfig) {

        // Cache the initial Surface width and height (We'll use this later)
        width = view.getWidth();
        height = view.getHeight();
        
        // Initialize gles context by setting the back buffer clear color to white
        // Note: Color is in 'RGBA'
        GLES20.glClearColor(1.f, 1.f, 1.f, 1.f);

        Log("Created Surface: [width: "+width+", height: "+height+"]");
    }

    @Override
    public void onSurfaceChanged(GL10 gl10, int newWidth, int newHeight) {
        Log("Surface Changed [newWidht: "+newWidth+", newHeight: "+newHeight+"]");

        // Update the gles viewport to reflect the new surface width and height.
        // Note: The glViewport controls the area of the surface that GLES20
        //       draws to.
        GLES20.glViewport(0, 0, newWidth, newHeight);

        width = newWidth;
        height = newHeight;
    }

    @Override
    public void onDrawFrame(GL10 gl10) {
        
        // Draw over the previous rendered back buffer with 'glClearColor'
        GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);
    }

    private static void Log(String msg) { 
        Log.d("Lab0Renderer - MSG", msg); 
    }
}

Note: For backwards compatibility Android passes eglConfig and an OpenGL ES 1.0 context to each overridden function via gl10. Because we are using OpenGL ES 2.0 we can safely ignore these variables and just use the static global GLES20 class to render instead.




Section 3: Drawing to the Screen With OpenGL ES 2.0

Drawing to the screen requires three main components. An array of vertices to draw, a program that tells the GPU how to position those vertices on the screen (Vertex Shader), and a program that tells the GPU how to color each pixel it draws to on the screen (Fragment Shader).

Note: The Fragment shader technically tell the GPU how to color each fragment, not pixel. If antialiasing is used each pixel may be made up of multiple pixel samples or fragments. These fragments are then averaged together to produce the color of the underlying pixel.


3.1 Setting up A Vertex Buffer:

Because gles is accelerated by the GPU any information needed to draw an object to the screen must exist in the GPU's memory. Gles achieves this by working with buffers. These buffers represent blocks of memory that exist on the GPU and are referenced via an int handle. It's important to note that memory on the GPU does not exist in RAM and cannot be referenced like a traditional array. Instead data has to be uploaded or download to and from the device in chunks which is considerably slower.

NOTE: Gles has a lot of optimizations under the hood. One of these optimizations is caching and queuing GPU buffers in RAM and only uploading them to the GPU when they are need. Because of this the integer handles gles works with technically represent buffers in RAM which are associated with memory on the GPU. If the buffer in RAM is modified and a draw call is issued that depends on it, gles will re-upload the buffer the GPU memory.


To get started we add the following class to MyRenderer that represents a set of vertices backing a GPU buffer:

static private class VertexBuffer {
    public int glBufferHandle;       // handle to the GPU buffer
    public FloatBuffer vertexBuffer; // native buffer in RAM
    public int vertexDimension;      // number of dimensions each vertex is (1,2,3, or 4)
}    

NOTE: Gles works in 4 dimensional vertex coordinates, but allows us to upload 1, 2, or 3 dimensional coordinates to the GPU. Coordinates that have less than 4 dimensions get expanded and stored on the GPU as 4 dimensional vectors by filling in missing x, y, and z components with 0 the missing w component with 1. By expanding coordinates gles reduces RAM usage and saves on GPU memory bandwidth. Later on we'll use vertexDimension to tell gles how many components our vertices have. You can read more about expanding coordinates in Transferring Array Elements.


Even though VertexBuffer represents vertices on the GPU, gles can't draw them to screen just yet. This is because gles has no idea what these vertices represent. Do they represent points on a line or corners of a triangle? Do all the vertices belong to a single object or multiple ones? To answer these questions we add the following to VertexBuffer:

public BufferObject bufferObjects[]; // array of objects stored in the vertexBuffer

static public class BufferObject {
    public int bufferType;       // gles render type - GL_TRIANGLES, GL_LINES, etc.
    public int numberOfVertices; // number of vertices representing the object 

    BufferObject(int bufferType_, int numberOfVertices_) {
        bufferType       = bufferType_;
        numberOfVertices = numberOfVertices_;
    }
};

Finally we add the following error checking methods to MyRenderer to make gles succeeds in updating the GPU buffer:

private static  void Error(String msg) { 
    Log.e("Lab0Renderer - ERROR", msg); 
}

private static void Panic(String msg) {
    Error(msg);
    System.exit(1);
}

private static void AssertNoGLError() {
    int error = GLES20.glGetError();
    if(error != GLES20.GL_NO_ERROR) {
        Panic("AssertNoGLError Failed! - GL Error: "+error);
    }
}

And add a constructor to VertexBuffer which copies our vertices to the GPU buffer:

VertexBuffer(int glBufferHandle_, int vertexDimension_,
             FloatBuffer vertexBuffer_, BufferObject[] bufferObjects_) {

    glBufferHandle  = glBufferHandle_;
    vertexDimension = vertexDimension_;
    vertexBuffer    = vertexBuffer_;
    bufferObjects   = bufferObjects_;

    // tell gles to attach our gpu buffer
    GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, // Type of buffer
                        glBufferHandle          // buffer handle to bind
                        );
    AssertNoGLError();

    // upload vertices to the attached GPU buffer
    GLES20.glBufferData(GLES20.GL_ARRAY_BUFFER,     // Type of buffer
                        4*vertexBuffer.capacity(),  // size of buffer in bytes (4 bytes in float)
                        vertexBuffer,               // initialization data
                        GLES20.GL_STATIC_DRAW       // buffer optimization hint (read only)
                        );
    AssertNoGLError();
}

The finished VertexBuffer should look like:

static private class VertexBuffer {

    public int glBufferHandle;           // handle to the GPU buffer
    public FloatBuffer vertexBuffer;     // native buffer in RAM
    public int vertexDimension;          // number of dimensions each vertex is (1,2,3, or 4)
    public BufferObject bufferObjects[]; // array of objects stored in the vertexBuffer

    static public class BufferObject {
        public int bufferType;       // gles render type - GL_TRIANGLES, GL_LINES, etc.
        public int numberOfVertices; // number of vertices representing the object

        BufferObject(int bufferType_, int numberOfVertices_) {
            bufferType       = bufferType_;
            numberOfVertices = numberOfVertices_;
        }
    };

    VertexBuffer(int glBufferHandle_, int vertexDimension_,
                 FloatBuffer vertexBuffer_, BufferObject[] bufferObjects_) {

        glBufferHandle  = glBufferHandle_;
        vertexDimension = vertexDimension_;
        vertexBuffer    = vertexBuffer_;
        bufferObjects   = bufferObjects_;

        // tell gles to attach our gpu buffer
        GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, // Type of buffer
                            glBufferHandle          // buffer handle to bind
                            );
        AssertNoGLError();

        // upload vertices to the attached GPU buffer
        GLES20.glBufferData(GLES20.GL_ARRAY_BUFFER,     // Type of buffer
                            4*vertexBuffer.capacity(),  // size of buffer in bytes (4 bytes in float)
                            vertexBuffer,               // initialization data
                            GLES20.GL_STATIC_DRAW       // buffer usage (read only)
                            );
        AssertNoGLError();
    }
};

Note: The 'buffer usage' is only a hint, GL_STATIC_DRAW doesn't prevent you from writing to the buffer, but doing so will incur a significant performance hit as the GPU memory will have to be remapped to RAM. You can learn more about gles render hints here.


Now that we have a generalized way of representing vertices on the GPU lets create an object that can be draw on screen.

Because gles uses a native backed, the vertices that we give it must must be allocated in the native address space instead of the Java address space. We achieve this by adding the following method to MyRenderer which translates Java float arrays to native float arrays:

private static FloatBuffer AllocateFloatBuffer(float[] initVals) {

    // create native buffer that is contiguous in native memory 
    // (Java arrays can be out of order)
    // Note: 4 bytes per float
    FloatBuffer buffer = ByteBuffer.allocateDirect(4*initVals.length)
                                   .order(ByteOrder.nativeOrder()).asFloatBuffer();

    buffer.put(initVals); // fill buffer with initVals
    buffer.rewind(); // set buffer pointer to start of buffer

    return buffer;
}

WARNING: Reading and writing to native arrays in Java is a lot slower than than reading and writing to normal Java arrays. Because of this you should avoid reading and writing to native arrays in Java whenever possible.


Next we extend VertexBuffer with a new class in MyRenderer representing a triangle:

static private class Triangle extends VertexBuffer {

    public Triangle(int glBufferHandle) {

        super(  glBufferHandle,            
                2, // 2-dimensional vertices (x, y)

                // create a native array of vertices for our triangle
                AllocateFloatBuffer(new float[] {
                    -.25f, -.25f,   // Vertex 1 (x,y)
                     .25f, -.25f,   // Vertex 2 (x,y)
                      0f,   .25f    // Vertex 3 (x,y)
                }),

                // Store some meta information about the triangle
                new BufferObject[] {
                        new BufferObject(GLES20.GL_TRIANGLES, // vertex primitive type
                                         3                    // number of vertices
                        )
                }
        );
    }
};

Note: You can learn about other gles primitives here.
Note: Because gles 2.0 doesn't support a geometry or tessellation shader Adjacent and patch primitives are not supported

Note: These triangle coordinates are in Normalized Device Coordinates or NDC. NDC maps the bottom left corner of the screen to (-1, -1) and the upper right of the screen to (1, 1). You can learn more about gles coordinate systems and how they work here.


Last we create some MyRenderer member variables to store handles to gles GPU buffers and VertexBuffers:

private int glBufferHandles[];
private VertexBuffer vertexBuffers[];

Add a method to MyRenderer that allocates gles GPU buffers:

private int[] CreateGlBuffers(int numBuffers) {

    int buffers[] = new int[numBuffers];
    GLES20.glGenBuffers(numBuffers, // number of buffers to create
                        buffers,    // location to store buffer handles
                        0           // start offset into `buffers` to place handles
                        );

    AssertNoGLError();

    return buffers;
}

And Instantiate a Triangle in MyRenderer::onSurfaceCreated with:

glBufferHandles = CreateGlBuffers(1);
vertexBuffers = new VertexBuffer[] {
        new Triangle(glBufferHandles[0])
};

The Final MyRenderer::onSurfaceCreated should look like:

@Override
public void onSurfaceCreated(GL10 gl10, EGLConfig eglConfig) {

    // Cache the initial Surface width and height (We'll use this later)
    width = view.getWidth();
    height = view.getHeight();

    // Initialize gles vertex buffers
    glBufferHandles = CreateGlBuffers(1);
    vertexBuffers = new VertexBuffer[] {
            new Triangle(glBufferHandles[0])
    };
    
    // Initialize gles context by setting the back buffer clear color to white
    GLES20.glClearColor(1.f, 1.f, 1.f, 1.f);

    Log("Created Surface: [width: "+width+", height: "+height+"]");
}



3.2 Setting up A Vertex Shader:

In favor of efficiency, flexibility, and ease of hardware implementation gles 2.0 abandons the idea of a fixed function render pipeline. Instead gles requires the user to program shaders which get executed on the GPU and control how information gets rendered to the screen. Shaders are written in the 'OpenGL ES Shading Language' (GLESSL) which gets compiled at runtime and uploaded to GPU. Because of this shaders are written as source code strings. Here we will program a vertex shader which will tell the GPU where to display each vertex on the screen.


We add the following vertex shader member to MyRenderer:

private final String kVertexShaderSource =  "attribute vec4 v4Position;"    +
                                            "void main() {"                 +
                                            "   gl_Position = v4Position;"  +
                                            "}";

Note: GLESSL looks very similar to C. In fact you can program loops, if statements, and even non-recursive functions - All which get executed on the GPU. However there is a very important difference between how GPUs and CPUs execute code. GPUs follow a 'Single Instruction Multiple Data' (SIMD) model. This means that the GPU executes each instruction across a block of parallel threads (NVidia refers to these as blocks warps which contain 32 threads on modern GPUs). The SIMD model allows GPUs to process large chunks of data in parallel with very little hardware overhead assuming each thread follows the same control flow. If this is not the case and a SIMD block contains threads that diverge, the GPU serializes each branch and disables inactive threads. This can lead to a very large performance hit.

For example given:

if(condition) {
    //slow code 
} else {
    //fast code
}

If there is any divergence in a SIMD block, the GPU will have to execute both 'slow code' and 'fast code' for the whole block!

Note: GLESSL provides us with built in types such as vec4 and global variables such as gl_Position. vec4 is a four dimensional floating point vector. You can learn more about built in types Here in section 4.1.

GLESSL uses gl_Position as an output from the vertex shader. The final value assigned to gl_Position represents the position of the input vertex in clip-space. To determine where on the screen a vertex lies, gles translates gl_Position from clip-space into normalized device coordinates or NDC. NDC coordinates represent a vertices x,y,z screen coordinates in the range [-1, 1] (The screen z-coordinate is used for z-buffing). Gles does this by first clipping vertices that lie out side of the range [-gl_Position.w, gl_Position.w] and then accounting for perspective by dividing each component of gl_Position by gl_Position.w. Because we are programming the vertex shader, we have to make sure that the value we assign to gl_Position correctly maps our 3D coordinates into clip-space. In our example all of our coordinates are in NDC, so we just set gl_Position to v4Position.

Note: GLESSL uses storage qualifiers to control how user variables get passed through the render pipeline. The two main qualifiers are attribute and uniform. attribute variables are variables that get set once per vertex while uniform variables are variables that get set once per primitive. Because we want to set gl_Position on a per vertex basis we mark v4Posistion as an attribute. You can read more about storage qualifiers here.




3.3 Setting up a Fragment Shader:

Similar to vertex shaders, fragment shaders are written in GLESSL and as strings and compiled at runtime. Here we will program a fragment shader which will tell the GPU what color each pixel on the screen is.


We add the following fragment shader member to MyRenderer:

private final String kFragmentShaderSource = "precision mediump float;"   +
                                             "uniform vec4 v4Color;"      +
                                             "void main() {"              +
                                             "   gl_FragColor = v4Color;" +
                                             "}";

Note: Similar to the vertex shader GLESSL provides us with global variables in the fragment shader. On of these variables is a vec4 called gl_FragColor used to determine the RGBA color of a given pixel fragment. In our example each pixel belonging to a single primitive is the same color so we assign gl_FragColor to v4Color directly and mark v4Color as uniform. You can read more about global shader variables Here in section 7

Note: Because graphics cards can support different floating point resolutions GLESSL requires programmers to specify the floating point resolution they are working in. we do this with the precision keyword. A precision statement takes in a precision-qualifier and type where the precision-qualifier can be highp (minium 16-bit precision), mediump (minimum 10-bit precision), or lowp (minimum 8-bit precision) and the type can be either float, int, or a texture sampling type. Later on we will calculate the color of each pixel in the fragment. If these calculations are performed in 8-bit resolution, large rounding errors might be introduced and because most screens have an 8-bit color depth, these errors will be displayed as color artifacts. On the flip side, using 16-bit math to compute the color of each pixel is overkill and won't produce perceivable differences on a 8-bit pixel depth screen. So we settle for something in the middle and use the 10-bit floating point resolution of mediump as it might save on the number of calculations the GPU needs to perform without introduction artifacts.

Note: We didn't need to specify a floating point precision for the vertex shader because GLESSL defaults the vertex shader to precision highp float.




3.4 Creating an OpenGL Render Program:

Now that we've written the source code for a vertex and fragment shader we need to compile the code into machine code and upload it to the GPU. Similar to vertex buffers gles uses integer handles to represent regions of memory that contain executable code.


To start off we'll add the following member variables to MyRenderer:

private int glProgram;               // handle to our GPU executable 
private int glStatus[] = new int[1]; // used to check for gles compile errors;

Next we add a function to MyRenderer to compile shader code:

private int CompileShader(int type, String source) {
    
    // Create a new shader
    // Note: type can be GL_VERTEX_SHADER or GL_FRAGMENT_SHADER    
    int shader = GLES20.glCreateShader(type); 
    GLES20.glShaderSource(shader, source);      // Set shader source code
    GLES20.glCompileShader(shader);             // Compile shader into gpu code

    // Get shader compile status
    GLES20.glGetShaderiv(shader,                    // variable shader handle  
                         GLES20.GL_COMPILE_STATUS,  // variable to get
                         glStatus,                  // array to dump variable to 
                         0                          // offset into array
                        );
    
    // check for shader compile errors 
    if(glStatus[0] == GLES20.GL_FALSE) {
        Panic("Failed to compile shader with {\n"+
              "\tSOURCE: [\n" +
              "\t\t"+source.trim().replace("\n", "\n\t\t")+"\n" +
              "\t]\n\n" +
              "\tINFO: [\n" +
              "\t\t"+GLES20.glGetShaderInfoLog(shader).replace("\n", "\n\t\t")+"\n" +
              "\t]\n" +
              "}");
    }

    // return handle to compiled shader
    return shader;
}

Then we add a function to MyRenderer that can create a gpu executable:

private int CreateGlProgram(String vertexShaderSource, String fragmentShaderSource) {
    // compile shader source code to gpu machine code
    int vertexShader    = CompileShader(GLES20.GL_VERTEX_SHADER, vertexShaderSource),
        fragmentShader  = CompileShader(GLES20.GL_FRAGMENT_SHADER, fragmentShaderSource);

    // create a gpu render program and attach the vertex shader and fragment shader
    int program = GLES20.glCreateProgram();
    GLES20.glAttachShader(program, vertexShader);
    GLES20.glAttachShader(program, fragmentShader);

    // link the gpu program into an executable
    GLES20.glLinkProgram(program);

    // check for link program errors
    GLES20.glGetProgramiv(program, GLES20.GL_LINK_STATUS, glStatus, 0);
    if(glStatus[0] == GLES20.GL_FALSE) {
        Panic("Failed to link glProgram | INFO: [ " + GLES20.glGetProgramInfoLog(program) + " ]");
    }

    // Note: After we link `program` into a GPU executable we no longer
    //       need to keep the compiled shader object files in RAM taking up
    //       valuable resources. So we free them from memory below

    // remove references to our shaders
    // Note: gles wont delete shader objects until we remove all references to them
    GLES20.glDetachShader(program, vertexShader);
    GLES20.glDetachShader(program, fragmentShader);

    // delete object files for our shaders
    GLES20.glDeleteShader(vertexShader);
    GLES20.glDeleteShader(fragmentShader);
    
    // return a handle to the compiled gpu program 
    return program;
}

Finally we add the following method to MyRenderer to initialize our shader variables:

private void InitGlProgram() {

    // Get handle to memory location for `v4Position`
    v4PositionAttribute = GLES20.glGetAttribLocation(glProgram, "v4Position");
    AssertNoGLError();

    // Get handle to memory location for `v4Color`
    v4ColorUniform = GLES20.glGetUniformLocation(glProgram, "v4Color");
    AssertNoGLError();

    // tell gles to use `glProgram` in the render pipeline
    GLES20.glUseProgram(glProgram);

    // tell gles to treat `v4PositionAttribute` as a per-vertex array instead as a register
    GLES20.glEnableVertexAttribArray(v4PositionAttribute);

    // set v4Color to green (R=0, G=1, B=0, A=1)
    GLES20.glUniform4f(v4ColorUniform, 0, 1, 0, 1);
}

Note: By default gles treats attribute variables as register variables. As a register v4Position would be the same value for each vertex in our vertex shader. Instead we want v4Position to be assigned a different value for each vertex so we use glEnableVertexAttribArray to tell gles to treat v4Position as the base address of array and use its elements as sequential values v4Position in the vertex shader.

WARNING: Gles preserves program variable state across programs. This means if you attach a new glProgram gles will still treat the GPU register associated with v4Position as an array which could lead to an illegal GPU memory access and undefined behavior. Use glDisableVertexAttribArray(v4PositionAttribute) to switch back to treating v4Position as a register before switching gl programs to prevent this.

WARNING: glUniform4f and its variants require a program to be set with glUseProgram prior to calling them to take effect. You can read more about glUniform here.


And setup a gpu program in MyRender::onSurfaceCreated with:

glProgram = CreateGlProgram(kVertexShaderSource, kFragmentShaderSource);
InitGlProgram();

The finished MyRenderer::onSurfaceCreated should look like the following:

@Override
public void onSurfaceCreated(GL10 gl10, EGLConfig eglConfig) {

    // Cache the initial Surface width and height (We'll use this later)
    width = view.getWidth();
    height = view.getHeight();

    // Initialize gles vertex buffers
    glBufferHandles = CreateGlBuffers(1);
    vertexBuffers = new VertexBuffer[] {
            new Triangle(glBufferHandles[0])
    };
    
    // Initialize gles context by setting the back buffer clear color to white
    GLES20.glClearColor(1.f, 1.f, 1.f, 1.f);

    // Setup a gpu program for the gles render pipeline
    glProgram = CreateGlProgram(kVertexShaderSource, kFragmentShaderSource);
    InitGlProgram();

    Log("Created Surface: [width: "+width+", height: "+height+"]");
}



3.5 Drawing the Vertex Buffer:

Now That we've established a vertex buffer and compiled a gles render program it's time to draw to the screen. Fortunately most of the work needed for drawing render primitives has already been done in MyRenderer::VertexBufferso this task is fairly straight forward.


The only thing we need to do to draw primitives to the screen is modify MyRenderer::onDraw to include two nested loops; An outer loop that iterates over each of the GPU vertex buffers and an inner loop which draws each render primitive stored in it. The finished MyRenderer::onDraw should look like the following:

@Override
public void onDrawFrame(GL10 gl10) {

    // Draw over the previous rendered back buffer with 'glClearColor'
    GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);

    for(int i = 0; i < vertexBuffers.length; ++i) {
        VertexBuffer vBuffer = vertexBuffers[i];

        // tell gles to use the `vBuffer` GPU vertex buffer
        GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, vBuffer.glBufferHandle);

        // tell gles how to treat each vertex in the bound buffer vertex buffer
        GLES20.glVertexAttribPointer(v4PositionAttribute,     // handle to attribute to use for buffer values
                                     vBuffer.vertexDimension, // number of components per vertex in array
                                     GLES20.GL_FLOAT,         // type of each component
                                     false,                   // normalize integers (using floating point so it's ignored)
                                     0,                       // stride between vertices in buffer (0 means packed data)
                                     0                        // start offset into buffer in bytes
                                    );
        AssertNoGLError();

        // draw each primitive in `vBuffer`
        int vertexOffset = 0;
        for(int j = 0; j < vBuffer.bufferObjects.length; ++j) {
            VertexBuffer.BufferObject vbo = vBuffer.bufferObjects[j];

            GLES20.glDrawArrays(vbo.bufferType,         // Type of primitive (GL_TRIANGLES, etc).  
                                vertexOffset,           // vertex offset into bound buffer to start drawing 
                                vbo.numberOfVertices    // number of vertices to draw
                                );
            AssertNoGLError();

            vertexOffset+= vbo.numberOfVertices;
        }
    }
}



The Completed MyRenderer should look like:

import android.opengl.GLES20;
import android.opengl.GLSurfaceView;
import android.util.Log;

import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.FloatBuffer;

import javax.microedition.khronos.egl.EGLConfig;
import javax.microedition.khronos.opengles.GL10;

public class MyRenderer implements GLSurfaceView.Renderer {

    private final String kVertexShaderSource =  "attribute vec4 v4Position;"    +
                                                "void main() {"                 +
                                                "   gl_Position = v4Position;"  +
                                                "}";
    private int v4PositionAttribute;


    private final String kFragmentShaderSource = "precision mediump float;"   +
                                                 "uniform vec4 v4Color;"      +
                                                 "void main() {"              +
                                                 "   gl_FragColor = v4Color;" +
                                                 "}";
    private int v4ColorUniform;

    private int glProgram;               // handle to our GPU executable
    private int glStatus[] = new int[1]; // used to check for gles compile errors;

    private GLSurfaceView view;
    private int width, height;

    static private class VertexBuffer {

        public int glBufferHandle;           // handle to the GPU buffer
        public FloatBuffer vertexBuffer;     // native buffer in RAM
        public int vertexDimension;          // number of dimensions each vertex is (1,2,3, or 4)
        public BufferObject bufferObjects[]; // array of objects stored in the vertexBuffer

        static public class BufferObject {
            public int bufferType;          // gles render type - GL_TRIANGLES, GL_LINES, etc.
            public int numberOfVertices;    // number of vertices representing the object

            BufferObject(int bufferType_, int numberOfVertices_) {
                bufferType       = bufferType_;
                numberOfVertices = numberOfVertices_;
            }
        };

        VertexBuffer(int glBufferHandle_, int vertexDimension_,
                     FloatBuffer vertexBuffer_, BufferObject[] bufferObjects_) {

            glBufferHandle  = glBufferHandle_;
            vertexDimension = vertexDimension_;
            vertexBuffer    = vertexBuffer_;
            bufferObjects   = bufferObjects_;

            // tell gles to attach our gpu buffer
            GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, // Type of buffer
                                glBufferHandle          // buffer handle to bind
                               );
            AssertNoGLError();

            // upload vertices to the attached GPU buffer
            GLES20.glBufferData(GLES20.GL_ARRAY_BUFFER,     // Type of buffer
                                4*vertexBuffer.capacity(),  // size of buffer in bytes (4 bytes in float)
                                vertexBuffer,               // initialization data
                                GLES20.GL_STATIC_DRAW       // buffer optimization hint (read only)
                               );
            AssertNoGLError();
        }
    };

    static private class Triangle extends VertexBuffer {
        public Triangle(int glBufferHandle) {
            super(glBufferHandle,

                  2, // 2-dimensional vertices (x, y)

                  // create a native array of vertices for our triangle
                  AllocateFloatBuffer(new float[] {
                          -.25f, -.25f, // Vertex 1 (x,y)
                           .25f, -.25f, // Vertex 2 (x,y)
                           0f,    .25f  // Vertex 3 (x,y)
                  }),

                  // Store some meta information about the triangle
                  new BufferObject[] {
                          new BufferObject(GLES20.GL_TRIANGLES, // vertex primitive type
                                           3                    // number of vertices
                          )
                  }
             );
        }
    };

    private int glBufferHandles[];
    private VertexBuffer vertexBuffers[];

    public MyRenderer(GLSurfaceView v) {

        // Cache an reference to the GLSurfaceView we're attached to.
        // This will allow us to query the height and with of the surface
        // as soon as our surface is created, but before Android tells us
        // its dimensions in `onSurfaceChanged` is called.
        view = v;
    }

    @Override
    public void onSurfaceCreated(GL10 gl10, EGLConfig eglConfig) {

        // Cache the initial Surface width and height (We'll use this later)
        width = view.getWidth();
        height = view.getHeight();

        // Initialize gles vertex buffers
        glBufferHandles = CreateGlBuffers(1);
        vertexBuffers = new VertexBuffer[] {
                new Triangle(glBufferHandles[0])
        };

        // Initialize gles context by setting the back buffer clear color to white
        GLES20.glClearColor(1.f, 1.f, 1.f, 1.f);

        // Setup a gpu program for the gles render pipeline
        glProgram = CreateGlProgram(kVertexShaderSource, kFragmentShaderSource);
        InitGlProgram();

        Log("Created Surface: [width: "+width+", height: "+height+"]");
    }

    @Override
    public void onSurfaceChanged(GL10 gl10, int newWidth, int newHeight) {
        Log("Surface Changed [newWidht: "+newWidth+", newHeight: "+newHeight+"]");

        // Update the gles viewport to reflect the new surface width and height.
        // Note: The glViewport controls the area of the surface that GLES20
        //       draws to.
        GLES20.glViewport(0, 0, newWidth, newHeight);

        width = newWidth;
        height = newHeight;
    }

    @Override
    public void onDrawFrame(GL10 gl10) {

        // Draw over the previous rendered back buffer with 'glClearColor'
        GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);

        for(int i = 0; i < vertexBuffers.length; ++i) {
            VertexBuffer vBuffer = vertexBuffers[i];

            // tell gles to use the `vBuffer` GPU vertex buffer
            GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, vBuffer.glBufferHandle);

            // tell gles how to treat each vertex in the bound buffer vertex buffer
            GLES20.glVertexAttribPointer(v4PositionAttribute,     // handle to attribute to use for buffer values
                                         vBuffer.vertexDimension, // number of components per vertex in array
                                         GLES20.GL_FLOAT,         // type of each component
                                         false,                   // normalize integers (using floating point so it's ignored)
                                         0,                       // stride between vertices in buffer (0 means packed data)
                                         0                        // start offset into buffer in bytes
                                        );
            AssertNoGLError();

            // draw each primitive in `vBuffer`
            int vertexOffset = 0;
            for(int j = 0; j < vBuffer.bufferObjects.length; ++j) {
                VertexBuffer.BufferObject vbo = vBuffer.bufferObjects[j];

                GLES20.glDrawArrays(vbo.bufferType,         // Type of primitive (GL_TRIANGLES, etc).
                                    vertexOffset,           // vertex offset into bound buffer to start drawing
                                    vbo.numberOfVertices    // number of vertices to draw
                                   );
                AssertNoGLError();

                vertexOffset+= vbo.numberOfVertices;
            }
        }
    }

    private int[] CreateGlBuffers(int numBuffers) {

        int buffers[] = new int[numBuffers];
        GLES20.glGenBuffers(numBuffers, // number of buffers to create
                            buffers,    // location to store buffer handles
                            0           // start offset into `buffers` to place handles
                           );

        AssertNoGLError();

        return buffers;
    }


    private int CreateGlProgram(String vertexShaderSource, String fragmentShaderSource) {
        // compile shader source code to gpu machine code
        int vertexShader    = CompileShader(GLES20.GL_VERTEX_SHADER, vertexShaderSource),
            fragmentShader  = CompileShader(GLES20.GL_FRAGMENT_SHADER, fragmentShaderSource);

        // create a gpu render program and attach the vertex shader and fragment shader
        int program = GLES20.glCreateProgram();
        GLES20.glAttachShader(program, vertexShader);
        GLES20.glAttachShader(program, fragmentShader);

        // link the gpu program into an executable
        GLES20.glLinkProgram(program);

        // check for link program errors
        GLES20.glGetProgramiv(program, GLES20.GL_LINK_STATUS, glStatus, 0);
        if(glStatus[0] == GLES20.GL_FALSE) {
            Panic("Failed to link glProgram | INFO: [ " + GLES20.glGetProgramInfoLog(program) + " ]");
        }

        // Note: After we link `program` into a GPU executable we no longer
        //       need to keep the compiled shader object files in RAM taking up
        //       valuable resources. So we free them from memory below

        // remove references to our shaders
        // Note: gles wont delete shader objects until we remove all references to them
        GLES20.glDetachShader(program, vertexShader);
        GLES20.glDetachShader(program, fragmentShader);

        // delete object files for our shaders
        GLES20.glDeleteShader(vertexShader);
        GLES20.glDeleteShader(fragmentShader);

        // return a handle to the compiled gpu program
        return program;
    }

    private void InitGlProgram() {

        // Get handle to memory location for `v4Position`
        v4PositionAttribute = GLES20.glGetAttribLocation(glProgram, "v4Position");
        AssertNoGLError();

        // Get handle to memory location for `v4Color`
        v4ColorUniform = GLES20.glGetUniformLocation(glProgram, "v4Color");
        AssertNoGLError();

        // tell gles to use `glProgram` in the render pipeline
        GLES20.glUseProgram(glProgram);

        // tell gles to treat `v4PositionAttribute` as a per-vertex array instead as a register
        GLES20.glEnableVertexAttribArray(v4PositionAttribute);

        // set v4Color to green (R=0, G=1, B=0, A=1)
        GLES20.glUniform4f(v4ColorUniform, 0, 1, 0, 1);
    }

    private void DisableGlProgram() {

        // prevent GPU from accessing invalid memory when our GPU program is not in use
        GLES20.glDisableVertexAttribArray(v4PositionAttribute);
    }

    private int CompileShader(int type, String source) {

        // Create a new shader
        // Note: type can be GL_VERTEX_SHADER or GL_FRAGMENT_SHADER
        int shader = GLES20.glCreateShader(type);
        GLES20.glShaderSource(shader, source);      // Set shader source code
        GLES20.glCompileShader(shader);             // Compile shader into gpu code

        // Get shader compile status
        GLES20.glGetShaderiv(shader,                    // variable shader handle
                             GLES20.GL_COMPILE_STATUS,  // variable to get
                             glStatus,                  // array to dump variable to
                             0                          // offset into array
                            );

        // check for shader compile errors
        if(glStatus[0] == GLES20.GL_FALSE) {
            Panic("Failed to compile shader with {\n"+
                  "\tSOURCE: [\n" +
                  "\t\t"+source.trim().replace("\n", "\n\t\t")+"\n" +
                  "\t]\n\n" +
                  "\tINFO: [\n" +
                  "\t\t"+GLES20.glGetShaderInfoLog(shader).replace("\n", "\n\t\t")+"\n" +
                  "\t]\n" +
                  "}");
        }

        // return handle to compiled shader
        return shader;
    }

    private static FloatBuffer AllocateFloatBuffer(float[] initVals) {

        // create native buffer that is contiguous in native memory (Java buffers can be out of order)
        // Note: 4 bytes per float
        FloatBuffer buffer = ByteBuffer.allocateDirect(initVals.length * 4).order(ByteOrder.nativeOrder()).asFloatBuffer();

        buffer.put(initVals); // fill buffer with initVals
        buffer.rewind();      // set buffer pointer to start of buffer

        return buffer;
    }


    private static void Log(String msg) { Log.d("Lab0Renderer - MSG", msg); }
    private static  void Error(String msg) { Log.e("Lab0Renderer - ERROR", msg); }

    private static void Panic(String msg) {
        Error(msg);
        System.exit(1);
    }

    private static void AssertNoGLError() {
        int error = GLES20.glGetError();
        if(error != GLES20.GL_NO_ERROR) {
            Panic("AssertNoGLError Failed! - GL Error: "+error);
        }
    }
}
⚠️ **GitHub.com Fallback** ⚠️