OpenGL ES 2.0 Graphics Programming for Android - JAVA
Table of contents:
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.RendererDrawing 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
Activity
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.
GLSurfaceView
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.
GLSurfaceView.Renderer
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
:
-
onSurfaceCreated
: Called whenGLSurface
creates a new EGL surface for drawing the gles context -
onSurfaceChanged
: Called whenGLSurface
changes the size of an existing EGL surface.
Note: this is also called when the surface is created with its initial dimensions -
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 inActivity::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 informGLSurface
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 whenrequestRender()
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 viagl10
. Because we are using OpenGL ES 2.0 we can safely ignore these variables and just use the static globalGLES20
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 asgl_Position
.vec4
is a four dimensional floating point vector. You can learn more about built in types Here in section 4.1.
GLESSL usesgl_Position
as an output from the vertex shader. The final value assigned togl_Position
represents the position of the input vertex in clip-space. To determine where on the screen a vertex lies, gles translatesgl_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 ofgl_Position
bygl_Position.w
. Because we are programming the vertex shader, we have to make sure that the value we assign togl_Position
correctly maps our 3D coordinates into clip-space. In our example all of our coordinates are in NDC, so we just setgl_Position
tov4Position
.
Note: GLESSL uses storage qualifiers to control how user variables get passed through the render pipeline. The two main qualifiers are
attribute
anduniform
.attribute
variables are variables that get set once per vertex whileuniform
variables are variables that get set once per primitive. Because we want to setgl_Position
on a per vertex basis we markv4Posistion
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
calledgl_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 assigngl_FragColor
tov4Color
directly and markv4Color
asuniform
. 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. Aprecision
statement takes in a precision-qualifier and type where the precision-qualifier can behighp
(minium 16-bit precision),mediump
(minimum 10-bit precision), orlowp
(minimum 8-bit precision) and the type can be eitherfloat
,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 ofmediump
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 wantv4Position
to be assigned a different value for each vertex so we useglEnableVertexAttribArray
to tell gles to treatv4Position
as the base address of array and use its elements as sequential valuesv4Position
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. UseglDisableVertexAttribArray(v4PositionAttribute)
to switch back to treatingv4Position
as a register before switching gl programs to prevent this.
WARNING:
glUniform4f
and its variants require a program to be set withglUseProgram
prior to calling them to take effect. You can read more aboutglUniform
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::VertexBuffer
so 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);
}
}
}