Getting Started with OpenGL ES 3+ Programming by Hans de Ruiter - HTML preview

PLEASE NOTE: This is an HTML preview only and some elements such as links or page numbers may be incorrect.
Download the book in PDF, ePub, Kindle for a complete version.

Tutorial 2: Hello Triangle

This tutorial you’ll finally draw something! Okay, it’ll only be one triangle, but it’s a big step forward. There’s a lot for you to learn: namely how to use shaders and Vertex Buffer Objects (VBOs).

Most tutorial series will teach how to draw a triangle using old methods, but I don’t like that idea. Those old methods are also slow and shouldn’t be used in production code. There’s no need to learn multiple ways to draw a triangle, so let’s go straight for the method you should be using.

  1. The Theory

Modern GPUs take large blocks of data (stored in VBOs, textures and other buffers), and process them via programs called shaders. We need two different shaders to draw our triangle. The first is a vertex shader, which reads vertices from the VBO and transforms them if necessary. Second, is a fragment shader which calculates the colour for each pixel in the triangle.

If you have the Modern Graphics Programming Primer, then I highly recommend reading the following sections first:

  • Overview of the Modern GPU
  • Shaders
  • Vertex Buffers

This will give you a good understanding of how the GPU works, and what it is you’re doing. If you don’t have the primer, then you can get it at: https://keasigmadelta.com/graphics-primer

  1. Getting Started

Create a new project called GLTutorial2 following the procedure learned in “Tutorial 1: Getting Started.” Next, copy and paste Tutorial 1’s code into Main.cpp. We’ll be using this as a starting point.

  1. The Shaders
  1. The Vertex Shader

Create a new source file called “Simple2D.vert” (NOTE: in Visual C++, use the C++ source file template, but make sure the file ends in “.vert”). Enter the following code:

#version 300 es

 

in vec2 vertPos;

 

out vec4 colour;

 

const vec4 white = vec4(1.0);

 

void main() {

    colour = white;

    gl_Position = vec4(vertPos, 0.0, 1.0);

}

 

Let's go through this one section at a time. The "#version 300 es" statement at the top indicates that this shader is written in GLSL version 3.0.0 for OpenGL ES.

The next two lines declare input and output variables:

in vec2 vertPos;

 

out vec4 colour;

 

So, the vertex shader takes a 2D vertex position as input, and outputs a 4 channel colour parameter. The colour output isn't really needed right now, but it demonstrates passing parameters from the vertex shader through to the fragment shader.

The next line creates a constant for the colour white:

const vec4 white = vec4(1.0);

 

The main() function is called for every vertex. The code simply sets the output colour to white, and copies the 2D position into gl_Position (which is a predefined output in GLSL):

void main() {

    colour = white;

    gl_Position = vec4(vertPos, 0.0, 1.0);

}

 

Vec4(vertPos, 0.0, 1.0) is the equivalent of vec4(vertPos.x, vertPos.y, 0.0, 1.0). This is necessary because our input vector is 2D, whereas gl_Position is a 4D vector.

  1. The Fragment Shader

Now create a file called “Simple2D.frag,” and enter the following:

#version 300 es

 

#ifdef GL_ES

precision highp float;

#endif

 

in vec4 colour;

 

out vec4 fragColour;

 

void main() {

    fragColour = colour;

}

 

This simply takes the colour received from the vertex shader (via the rasterizer), and writes it to the output pixel (fragColor).

  1. Compiling and Linking the Shaders

The shaders need to be compiled to the GPU’s machine code, and then linked to form a shader program. An OpenGL shader program contains all shaders needed to go from vertices to rendered pixels (so both the vertex and fragment shader).

Let’s put the shader handling code into its own separate source file. Organising a program into logical blocks is key to building large complicated systems, and we might as well start now. Plus, we’ll be needing this code again and again so creating a separate module will make reusing it easier (just copy the files to the next project).

So, create two new source files: “Shader.cpp” and “Shader.h.” For each file, right-click on “GLTutorial2” in the Solution Explorer (left column), and select Add => New Item...

NOTE: While the file suffix may be “.cpp,” we will be using plain C (with C++’s stricter syntax requirements). I don’t want to waste time explaining C++ classes because it would distract from your real goal, which is learning OpenGL.

The header file (Shader.h) provides an interface for other parts of the program to use. It declares two functions: shaderProgLoad() and shaderProgDestroy(). Other source files can use them by including this header, and calling those functions. Here’s Shader.h’s full listing:

// Shader.h

 

#ifndef __SHADER_H__

#define __SHADER_H__

 

#include <GLES3/gl3.h>

 

/** Loads a vertex and fragment shader from disk and compiles (& links) them

* into a shader program.

*

* This will print any errors to the console.

*

* @param vertFilename filename for the vertex shader

* @param fragFilename the fragment shader's filename.

*

* @return GLuint the shader program's ID, or 0 if failed.

*/

GLuint shaderProgLoad(const char *vertFilename, const char *fragFilename);

 

/** Destroys a shader program.

*/

void shaderProgDestroy(GLuint shaderProg);

 

#endif

 

The actual code for these functions (a.k.a., the function definitions) are in Shader.cpp, along with any support code they need. Let’s go through this file step-by-step.

First, we include Shader.h and other headers. Including Shader.h ensures that the function declarations and definitions match. The compiler will warn us if the two files ever get out of sync. Here’s the code:

// Shader.cpp

//

// See header file for details

 

#include "Shader.h"

 

#include <cstdio>

#include <cstdlib>

#include <SDL.h>

#include <SDL_opengles2.h>

 

#ifdef _MSC_VER

#pragma warning(disable:4996) // Allows us to use the portable fopen() function without warnings

#endif

 

The line starting with #pragma is to disable warnings when using standard C file handling functions. Visual Studio warns that they’re “unsafe,” and provides Microsoft Windows specific alternatives. However, we want to stick to standard C so that the code can be used unchanged on other systems. So, the warning is disabled.

HINT: It’s good practise to ensure that your code compiles with no warnings. That way you’ll easily see genuine warnings instead of losing them in a sea of false ones.

  1. Compiling a Single Shader

Next, we need a function to load a shader from disk. In turn, this function needs to be able to get the length of a file in bytes. So, we define another function called fileGetLength():

/** Gets the file's length.

 *

 * @param file the file

 *

 * @return size_t the file's length in bytes

 */

static size_t fileGetLength(FILE *file) {

    size_t length;

 

    size_t currPos = ftell(file);

 

    fseek(file, 0, SEEK_END);

    length = ftell(file);

 

    // Return the file to its previous position

    fseek(file, currPos, SEEK_SET);

 

    return length;

}

 

This function scans through a file with fseek() and uses ftell() to get its size. These are standard C library functions. Look up the documentation for fseek() and ftell() to find out how they’re used. You can find documentation for the entire standard C/C++ library here: http://www.cplusplus.com/reference/

Now we can write the function to load and compile a shader (shaderLoad()):

/** Loads and compiles a shader from a file.

 *

 * This will print any errors to the console.

 *

 * @param filename the shader's filename

 * @param shaderType the shader type (e.g., GL_VERTEX_SHADER)

 *

 * @return GLuint the shader's ID, or 0 if failed

 */

static GLuint shaderLoad(const char *filename, GLenum shaderType) {

    FILE *file = fopen(filename, "r");

    if (!file) {

        SDL_Log("Can't open file: %s\n", filename);

 

        return 0;

    }

 

    size_t length = fileGetLength(file);

 

    // Alloc space for the file (plus '\0' termination)

    GLchar *shaderSrc = (GLchar*)calloc(length + 1, 1);

    if (!shaderSrc) {

        SDL_Log("Out of memory when reading file: %s\n", filename);

        fclose(file);

        file = NULL;

 

        return 0;

    }

 

    fread(shaderSrc, 1, length, file);

 

    // Done with the file

    fclose(file);

    file = NULL;

 

    // Create the shader

    GLuint shader = glCreateShader(shaderType);

    glShaderSource(shader, 1, (const GLchar**)&shaderSrc, NULL);

    free(shaderSrc);

    shaderSrc = NULL;

 

    // Compile it

    glCompileShader(shader);

    GLint compileSucceeded = GL_FALSE;

    glGetShaderiv(shader, GL_COMPILE_STATUS, &compileSucceeded);

    if (!compileSucceeded) {

        // Compilation failed. Print error info

        SDL_Log("Compilation of shader %s failed:\n", filename);

        GLint logLength = 0;

        glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &logLength);

        GLchar *errLog = (GLchar*)malloc(logLength);

        if (errLog) {

            glGetShaderInfoLog(shader, logLength, &logLength, errLog);

            SDL_Log("%s\n", errLog);

            free(errLog);

        }

        else {

            SDL_Log("Couldn't get shader log; out of memory\n");

        }

 

        glDeleteShader(shader);

        shader = 0;

    }

 

    return shader;

}

 

This is a fairly long function, so here’s an overview. The first section (up to fclose()) loads the shader file into a buffer. Next, an empty shader is created (glCreateShader()), the source-code is added (glShaderSource()), and is compiled (glCompileShader()). After that, we check whether compilation succeeded (glGetShaderiv() with GL_COMPILE_STATUS), and get the compilation log if it failed (glGetShaderInfoLog()). SDL provides a convenient platform-independent method to print the log called SDL_Log().

Accompanying shaderLoad() is shaderDestroy(), which deletes a shader once it’s no longer needed:

/** Destroys a shader.

 */

static void shaderDestroy(GLuint shaderID) {