Hello Triangle: An OpenGL ES 3.0 Example - OpenGL ES 3.0: Programming Guide, Second Edition (2014)

OpenGL ES 3.0: Programming Guide, Second Edition (2014)

Chapter 2. Hello Triangle: An OpenGL ES 3.0 Example

To introduce the basic concepts of OpenGL ES 3.0, we begin with a simple example. This chapter shows what is required to create an OpenGL ES 3.0 program that draws a single triangle. The program we will write is just about the most basic example of an OpenGL ES 3.0 application that draws geometry. This chapter covers the following concepts:

• Creating an on-screen render surface with EGL

• Loading vertex and fragment shaders

• Creating a program object, attaching vertex and fragment shaders, and linking a program object

• Setting the viewport

• Clearing the color buffer

• Rendering a simple primitive

• Making the contents of the color buffer visible in the EGL window surface

As it turns out, a significant number of steps are required before we can start drawing a triangle with OpenGL ES 3.0. This chapter goes over the basics of each of these steps. Later in this book, we fill in the details on each of these steps and further document the API. Our purpose here is to get you up and running with your first simple example so that you get an idea of what goes into creating an application with OpenGL ES 3.0.

Code Framework

Throughout this book, we build a library of utility functions that form a framework of useful functions for writing OpenGL ES 3.0 programs. In developing example programs for the book, we had several goals for this code framework:

1. It should be simple, small, and easy to understand. We wanted to focus our examples on the relevant OpenGL ES 3.0 calls, rather than on a large code framework that we invented. Thus we focused our framework on simplicity and sought to make the example programs easy to read and understand. The goal of the framework is to allow you to focus your attention on the important OpenGL ES 3.0 API concepts in each example.

2. It should be portable. To the extent possible, we wanted the sample code to be available on all platforms where OpenGL ES 3.0 is present.

As we go through the examples in the book, we will formally introduce any new code framework functions that we use. In addition, you can find full documentation for the code framework in Appendix C. Any functions called in the example code that have names beginning with es (e.g.,esCreateWindow()) are part of the code framework we wrote for the sample programs in this book.

Where to Download the Examples

You can find links to download the examples from the book website at opengles-book.com.

As of this writing, the source code is available for Windows, Linux, Android 4.3+ NDK, Android 4.3+ SDK (Java), and iOS7. On Windows, the code is compatible with the Qualcomm OpenGL ES 3.0 Emulator, ARM OpenGL ES 3.0 Emulator, and PowerVR OpenGL ES 3.0 Emulator. On Linux, the currently available emulators are the Qualcomm OpenGL ES 3.0 Emulator and the PowerVR OpenGL ES 3.0 Emulator. The code should be compatible with any Windows- or Linux-based OpenGL ES 3.0 implementations in addition to those mentioned here. The choice of development tool is up to the reader. We have used cmake, a cross-platform build generation tool, on Windows and Linux, which allows you to use IDEs including Microsoft Visual Studio, Eclipse, Code::Blocks, and Xcode.

On Android and iOS, we provide projects compatible with those platforms (Eclipse ADT and Xcode). As of this writing, many devices support OpenGL ES 3.0, including iPhone 5s, Google Nexus 4 and 7, Nexus 10, HTC One, LG G2, Samsung Galaxy S4 (Snapdragon), and Samsung Galaxy Note 3. On iOS7, you can run the OpenGL ES 3.0 examples on your Mac using the iOS Simulator. On Android, you will need a device compatible with OpenGL ES 3.0 to run the samples. Details on building the sample code for each platform are provided in Chapter 16, “OpenGL ES Platforms.”

Hello Triangle Example

Let’s look at the full source code for our Hello Triangle example program, which is listed in Example 2-1. Those readers who are familiar with fixed-function desktop OpenGL will probably think this is a lot of code just to draw a simple triangle. Those of you who are not familiar with desktop OpenGL will also probably think this is a lot of code just to draw a triangle! Remember, OpenGL ES 3.0 is fully shader based, which means you cannot draw any geometry without having the appropriate shaders loaded and bound. This means that more setup code is required to render than in desktop OpenGL using fixed-function processing.

Example 2-1 Hello_Triangle.c Example


#include "esUtil.h"

typedef struct
{
// Handle to a program object
GLuint programObject;

} UserData;

///
// Create a shader object, load the shader source, and
// compile the shader
//
GLuint LoadShader ( GLenum type, const char *shaderSrc )
{
GLuint shader;
GLint compiled;

// Create the shader object
shader = glCreateShader ( type );
if ( shader == 0 )
return 0;

// Load the shader source
glShaderSource ( shader, 1, &shaderSrc, NULL );

// Compile the shader
glCompileShader ( shader );

// Check the compile status
glGetShaderiv ( shader, GL_COMPILE_STATUS, &compiled );

if ( !compiled )
{
GLint infoLen = 0;

glGetShaderiv ( shader, GL_INFO_LOG_LENGTH, &infoLen );

if ( infoLen > 1 )
{
char* infoLog = malloc (sizeof(char) * infoLen );

glGetShaderInfoLog( shader, infoLen, NULL, infoLog );
esLogMessage ( "Error compiling shader:\n%s\n", infoLog );

free ( infoLog );

}

glDeleteShader ( shader );
return 0;
}

return shader;

}

///
// Initialize the shader and program object
//
int Init ( ESContext *esContext )
{
UserData *userData = esContext->userData;
char vShaderStr[] =
"#version 300 es \n"
"layout(location = 0) in vec4 vPosition; \n"
"void main() \n"
"{ \n"
" gl_Position = vPosition; \n"
"} \n";

char fShaderStr[] =
"#version 300 es \n"
"precision mediump float; \n"
"out vec4 fragColor; \n"
"void main() \n"
"{ \n"
" fragColor = vec4 ( 1.0, 0.0, 0.0, 1.0 ); \n"
"} \n";

GLuint vertexShader;
GLuint fragmentShader;
GLuint programObject;
GLint linked;

// Load the vertex/fragment shaders
vertexShader = LoadShader ( GL_VERTEX_SHADER, vShaderStr );
fragmentShader = LoadShader ( GL_FRAGMENT_SHADER, fShaderStr );

// Create the program object
programObject = glCreateProgram ( );

if ( programObject == 0 )
return 0;

glAttachShader ( programObject, vertexShader );
glAttachShader ( programObject, fragmentShader );

// Link the program
glLinkProgram ( programObject );

// Check the link status
glGetProgramiv ( programObject, GL_LINK_STATUS, &linked );

if ( !linked )
{
GLint infoLen = 0;

glGetProgramiv ( programObject, GL_INFO_LOG_LENGTH, &infoLen );

if ( infoLen > 1 )
{
char* infoLog = malloc (sizeof(char) * infoLen );

glGetProgramInfoLog ( programObject, infoLen, NULL, infoLog );

esLogMessage ( "Error linking program:\n%s\n", infoLog );

free ( infoLog );
}

glDeleteProgram ( programObject );
return FALSE;

}

// Store the program object
userData->programObject = programObject;

glClearColor ( 0.0f, 0.0f, 0.0f, 0.0f );
return TRUE;
}

///
// Draw a triangle using the shader pair created in Init()
//
void Draw ( ESContext *esContext )
{
UserData *userData = esContext->userData;
GLfloat vVertices[] = { 0.0f, 0.5f, 0.0f,
-0.5f, -0.5f, 0.0f,
0.5f, -0.5f, 0.0f };

// Set the viewport
glViewport ( 0, 0, esContext->width, esContext->height );

// Clear the color buffer
glClear ( GL_COLOR_BUFFER_BIT );

// Use the program object
glUseProgram ( userData->programObject );

// Load the vertex data
glVertexAttribPointer ( 0, 3, GL_FLOAT, GL_FALSE, 0, vVertices );
glEnableVertexAttribArray ( 0 );

glDrawArrays ( GL_TRIANGLES, 0, 3 );
}

void Shutdown ( ESContext *esContext )
{

UserData *userData = esContext->userData;

glDeleteProgram( userData->programObject );
}

int esMain( ESContext *esContext )
{
esContext->userData = malloc ( sizeof( UserData ) );

esCreateWindow ( esContext, "Hello Triangle", 320, 240,
ES_WINDOW_RGB );

if ( !Init ( esContext ) )
return GL_FALSE;

esRegisterShutdownFunc( esContext, Shutdown );
esRegisterDrawFunc ( esContext, Draw );

return GL_TRUE;
}


The remainder of this chapter describes the code in this example. If you run the Hello Triangle example, you should see the window shown in Figure 2-1. Instructions on how to build and run the sample code for Windows, Linux, Android 4.3+, and iOS are provided in Chapter 16, “OpenGL ES Platforms.” Please refer to the instructions in that chapter for your platform to get up and running with the sample code.

Image

Figure 2-1 Hello Triangle Example

The standard GL3 (GLES3/gl3.h) and EGL (EGL/egl.h) header files provided by Khronos are used as an interface to OpenGL ES 3.0 and EGL. The OpenGL ES 3.0 examples are organized in the following directories:

• Common/—Contains the OpenGL ES 3.0 Framework project, code, and the emulator.

• chapter_x/—Contains the example programs for each chapter.

Using the OpenGL ES 3.0 Framework

Each application that uses our code framework declares a main entry point named esMain. In the main function in Hello Triangle, you will see calls to several ES utility functions. The esMain function takes an ESContext as an argument.

int esMain( ESContext *esContext )

The ESContext has a member variable named userData that is a void*. Each of the sample programs will store any of the data that are needed for the application in userData. The other elements in the ESContext structure are described in the header file and are intended only to be read by the user application. Other data in the ESContext structure include information such as the window width and height, EGL context, and callback function pointers.

The esMain function is responsible for allocating the userData, creating the window, and initializing the draw callback function:

esContext->userData = malloc ( sizeof( UserData ) );

esCreateWindow( esContext, "Hello Triangle", 320, 240,
ES_WINDOW_RGB );

if ( !Init( esContext ) )
return GL_FALSE;

esRegisterDrawFunc(esContext, Draw);

The call to esCreateWindow creates a window of the specified width and height (in this case, 320 × 240). The “Hello Triangle” parameter is used to name the window; on platforms supporting it (Windows and Linux), this name will be displayed in the top band of the window. The last parameter is a bit field that specifies options for the window creation. In this case, we request an RGB framebuffer. Chapter 3, “An Introduction to EGL,” discusses what esCreateWindow does in more detail. This function uses EGL to create an on-screen render surface that is attached to a window. EGL is a platform-independent API for creating rendering surfaces and contexts. For now, we will simply say that this function creates a rendering surface and leave the details on how it works for the next chapter.

After calling esCreateWindow, the main function next calls Init to initialize everything needed to run the program. Finally, it registers a callback function, Draw, that will be called to render the frame. After exiting esMain, the framework enters into the main loop, which will call the registered callback functions (Draw, Update) until the window is closed.

Creating a Simple Vertex and Fragment Shader

In OpenGL ES 3.0, no geometry can be drawn unless a valid vertex and fragment shader have been loaded. In Chapter 1, “Introduction to OpenGL ES 3.0,” we covered the basics of the OpenGL ES 3.0 programmable pipeline. There, you learned about the concepts of vertex and fragment shaders. These two shader programs describe the transformation of vertices and drawing of fragments. To do any rendering at all, an OpenGL ES 3.0 program must have at least one vertex shader and one fragment shader.

The biggest task that the Init function in Hello Triangle accomplishes is the loading of a vertex shader and a fragment shader. The vertex shader that is given in the program is very simple:

char vShaderStr[] =
"#version 300 es \n"
"layout(location = 0) in vec4 vPosition; \n"
"void main() \n"
"{ \n"
" gl_Position = vPosition; \n"
"} \n";

The first line of the vertex shader declares the shader version that is being used (#version 300 es indicates OpenGL ES Shading Language v3.00). The vertex shader declares one input attribute array—a four-component vector named vPosition. Later on, the Draw function in Hello Triangle will send in positions for each vertex that will be placed in this variable. The layout(location = 0) qualifier signifies that the location of this variable is vertex attribute 0. The shader declares a main function that marks the beginning of execution of the shader. The body of the shader is very simple; it copies the vPosition input attribute into a special output variable named gl_Position. Every vertex shader must output a position into the gl_Position variable. This variable defines the position that is passed through to the next stage in the pipeline. The topic of writing shaders is a large part of what we cover in this book, but for now we just want to give you a flavor of what a vertex shader looks like. In Chapter 5, “OpenGL ES Shading Language,” we cover the OpenGL ES shading language; in Chapter 8, “Vertex Shaders,” we specifically cover how to write vertex shaders.

The fragment shader in the example is simple:

char fShaderStr[] =
"#version 300 es \n"
"precision mediump float; \n"
"out vec4 fragColor; \n"
"void main() \n"
"{ \n"
" fragColor = vec4 ( 1.0, 0.0, 0.0, 1.0 ); \n"
"} \n";

Just as in the vertex shader, the first line of the fragment shader declares the shader version. The next statement in the fragment shader declares the default precision for float variables in the shader. For more details on this topic, please see the section on precision qualifiers in Chapter 5, “OpenGL ES Shading Language.” The fragment shader declares a single output variable fragColor, which is a vector of four components. The value written to this variable is what will be written out into the color buffer. In this case, the shader outputs a red color (1.0, 0.0, 0.0, 1.0) for all fragments. The details of developing fragment shaders are covered in Chapter 9, “Texturing,” and Chapter 10, “Fragment Shaders.” Again, here we are just showing you what a fragment shader looks like.

Typically, a game or application would not place shader source strings inline in the way we have done in this example. In most real-world applications, the shader is loaded from some sort of text or data file and then loaded to the API. However, for simplicity and to make the example program self-contained, we provide the shader source strings directly in the program code.

Compiling and Loading the Shaders

Now that we have the shader source code defined, we can go about loading the shaders to OpenGL ES. The LoadShader function in the Hello Triangle example is responsible for loading the shader source code, compiling it, and checking it for errors. It returns a shader object, which is an OpenGL ES 3.0 object that can later be used for attachment to a program object (these two objects are detailed in Chapter 4, “Shaders and Programs”).

Let’s look at how the LoadShader function works. First, glCreateShader creates a new shader object of the type specified.

GLuint LoadShader(GLenum type, const char *shaderSrc)
{
GLuint shader;
GLint compiled;

// Create the shader object
shader = glCreateShader(type);

if(shader == 0)
return 0;

The shader source code itself is loaded to the shader object using glShaderSource. The shader is then compiled using the glCompileShader function.

// Load the shader source
glShaderSource(shader, 1, &shaderSrc, NULL);

// Compile the shader
glCompileShader(shader);

After compiling the shader, the status of the compile is determined and any errors that were generated are printed out.

// Check the compile status
glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled);

if(!compiled)
{
GLint infoLen = 0;

glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &infoLen);

if(infoLen > 1)
{
char* infoLog = malloc(sizeof(char) * infoLen);

glGetShaderInfoLog(shader, infoLen, NULL, infoLog);
esLogMessage("Error compiling shader:\n%s\n", infoLog);

free(infoLog);

}

glDeleteShader(shader);
return 0;
}

return shader;

}

If the shader compiles successfully, a new shader object is returned that will be attached to the program later. The details of these shader object functions are covered in the first sections of Chapter 4, “Shaders and Programs.”

Creating a Program Object and Linking the Shaders

Once the application has created a shader object for the vertex and fragment shaders, it needs to create a program object. Conceptually, the program object can be thought of as the final linked program. Once the various shaders are compiled into a shader object, they must be attached to a program object and linked together before drawing.

The process of creating program objects and linking is fully described in Chapter 4, “Shaders and Programs.” For now, we provide a brief overview of the process. The first step is to create the program object and attach the vertex shader and fragment shader to it.

// Create the program object
programObject = glCreateProgram();

if(programObject == 0)
return 0;

glAttachShader(programObject, vertexShader);
glAttachShader(programObject, fragmentShader);

Finally, we are ready to link the program and check for errors:

// Link the program
glLinkProgram(programObject);

// Check the link status
glGetProgramiv(programObject, GL_LINK_STATUS, &1inked);

if(!linked)
{
GLint infoLen = 0;

glGetProgramiv(programObject, GL_INFO_LOG_LENGTH,&infoLen);

if(infoLen > 1)
{
char* infoLog = malloc(sizeof(char) * infoLen);

glGetProgramInfoLog(programObject, infoLen, NULL,infoLog);
esLogMessage("Error linking program:\n%s\n", infoLog);

free(infoLog) ;
}

glDeleteProgram(programObject) ;
return FALSE;
}

// Store the program object
userData->programObject = programObject;

After all of these steps, we have finally compiled the shaders, checked for compile errors, created the program object, attached the shaders, linked the program, and checked for link errors. After successful linking of the program object, we can now finally use the program object for rendering! To use the program object for rendering, we bind it using glUseProgram.

// Use the program object
glUseProgram(userData->programObject);

After calling glUseProgram with the program object handle, all subsequent rendering will occur using the vertex and fragment shaders attached to the program object.

Setting the Viewport and Clearing the Color Buffer

Now that we have created a rendering surface with EGL and initialized and loaded shaders, we are ready to actually draw something. The Draw callback function draws the frame. The first command that we execute in Draw is glViewport, which informs OpenGL ES of the origin, width, and height of the 2D rendering surface that will be drawn to. In OpenGL ES, the viewport defines the 2D rectangle in which all OpenGL ES rendering operations will ultimately be displayed.

// Set the viewport
glviewport(0, 0, esContext->width, esContext->height);

The viewport is defined by an origin (x, y) and a width and height. We cover glViewport in more detail in Chapter 7, “Primitive Assembly and Rasterization,” when we discuss coordinate systems and clipping.

After setting the viewport, the next step is to clear the screen. In OpenGL ES, multiple types of buffers are involved in drawing: color, depth, and stencil. We cover these buffers in more detail in Chapter 11, “Fragment Operations.” In the Hello Triangle example, only the color buffer is drawn to. At the beginning of each frame, we clear the color buffer using the glClear function.

// Clear the color buffer
glClear(GL_COLOR_BUFFER_BIT);

The buffer will be cleared to the color specified with glClearColor. In the example program at the end of Init, the clear color was set to (1.0, 1.0, 1.0, 1.0), so the screen is cleared to white. The clear color should be set by the application prior to calling glClear on the color buffer.

Loading the Geometry and Drawing a Primitive

Now that we have the color buffer cleared, viewport set, and program object loaded, we need to specify the geometry for the triangle. The vertices for the triangle are specified with three (x, y, z) coordinates in the vVertices array.

GLfloat vVertices[] = { O.Of, 0.5f, O.Of,
-0.5f, -0.5f, O.Of,
0.5f, -0.5f, O.Of};
...
// Load the vertex data
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, vVertices);
glEnableVertexAttribArray(O) ;
glDrawArrays(GL_TRIANGLES, 0, 3);

The vertex positions need to be loaded to the GL and connected to the vPosition attribute declared in the vertex shader. As you will remember, earlier we bound the vPosition variable to the input attribute location 0. Each attribute in the vertex shader has a location that is uniquely identified by an unsigned integer value. To load the data into vertex attribute 0, we call the glVertexAttribPointer function. In Chapter 6, “Vertex Attributes, Vertex Arrays, and Buffer Objects,” we cover how to load vertex attributes and use vertex arrays in full.

The final step in drawing the triangle is to actually tell OpenGL ES to draw the primitive. In this example, we use the function glDrawArrays for this purpose. This function draws a primitive such as a triangle, line, or strip. We get into primitives in much more detail in Chapter 7, “Primitive Assembly and Rasterization.”

Displaying the Back Buffer

We have finally gotten to the point where our triangle has been drawn into the framebuffer. Now there is one final detail we must address: how to actually display the framebuffer on the screen. Before we get into that, let’s back up a little bit and discuss the concept of double buffering.

The framebuffer that is visible on the screen is represented by a two-dimensional array of pixel data. One possible way we could think about displaying images on the screen is to simply update the pixel data in the visible framebuffer as we draw. However, there is a significant issue with updating pixels directly on the displayable buffer—that is, in a typical display system, the physical screen is updated from framebuffer memory at a fixed rate. If we were to draw directly into the framebuffer, the user could see artifacts as partial updates to the framebuffer where it is displayed.

To address this problem, we use a system known as double buffering. In this scheme, there are two buffers: a front buffer and a back buffer. All rendering occurs to the back buffer, which is located in an area of memory that is not visible to the screen. When all rendering is complete, this buffer is “swapped” with the front buffer (or visible buffer). The front buffer then becomes the back buffer for the next frame.

Using this technique, we do not display a visible surface until all rendering is complete for a frame. This activity is controlled in an OpenGL ES application through EGL, by using an EGL function called eglSwapBuffers (this function is called by our framework after calling the Drawcallback function):

eglSwapBuffers(esContext->eglDisplay, esContext->eglSurface);

This function informs EGL to swap the front buffer and back buffers. The parameters sent to eglSwapBuffers are the EGL display and surface. These two parameters represent the physical display and the rendering surface, respectively. In the next chapter, we explaineglSwapBuffers in more detail and further clarify the concepts of surface, context, and buffer management. For now, suffice it to say that after swapping buffers we finally have our triangle on screen!

Summary

In this chapter, we introduced a simple OpenGL ES 3.0 program that draws a single triangle to the screen. The purpose of this introduction was to familiarize you with several of the key components that make up an OpenGL ES 3.0 application: creating an on-screen render surface with EGL, working with shaders and their associated objects, setting the viewport, clearing the color buffer, and rendering a primitive. Now that you understand the basics of what makes up an OpenGL ES 3.0 application, we will dive into these topics in more detail, starting in the next chapter with more information on EGL.