GLKit Tutorial for iOS: Getting started with OpenGL ES

Learn how to use OpenGL ES in iOS in this GLKit tutorial. You’ll go from fresh project to spinning cube rendered using OpenGL and learn all the theory along the way! By Felipe Laso-Marsetti.

Leave a rating/review
Download materials
Save for later
Share
You are currently viewing page 2 of 4 of this article. Click here to view the first page.

Creating Vertex Data for a Simple Square

It’s time to begin the process of drawing a square on the screen! Firstly, you need to create the vertices that define the square. Vertices (plural of vertex) are simply points that define the outline of the shape that you want to draw.

You will set up the vertices as follows:

Only triangle geometry can be rendered using OpenGL. You can, however, create a square with two triangles as you can see in the picture above: One triangle with vertices (0, 1, 2) and one triangle with vertices (2, 3, 0).

One of the nice things about OpenGL ES is that you can keep your vertex data organized however you like. For this project, you will use a Swift structure to store the vertex position and color information, and then an array of vertices for each one that you’ll use to draw.

Right click the OpenGLKit folder in the Project navigator and select New File… Go to iOS\Swift File and click Next. Name the file Vertex and click Create. Replace the contents of the file with the following:

import GLKit

struct Vertex {
  var x: GLfloat
  var y: GLfloat
  var z: GLfloat
  var r: GLfloat
  var g: GLfloat
  var b: GLfloat
  var a: GLfloat
}

This is a pretty straightforward Swift structure for a vertex that has variables for position (x, y, z) and color (r, g, b, a). GLFloat is a type alias for a Swift Float, but it’s the recommended way to declare floats when working with OpenGL. You may see similar patterns wherein you use OpenGL types for other variables that you create.

Return to ViewController.swift. Add the following code inside your controller:

var Vertices = [
  Vertex(x:  1, y: -1, z: 0, r: 1, g: 0, b: 0, a: 1),
  Vertex(x:  1, y:  1, z: 0, r: 0, g: 1, b: 0, a: 1),
  Vertex(x: -1, y:  1, z: 0, r: 0, g: 0, b: 1, a: 1),
  Vertex(x: -1, y: -1, z: 0, r: 0, g: 0, b: 0, a: 1),
]

var Indices: [GLubyte] = [
  0, 1, 2,
  2, 3, 0
]

Here, you are using the Vertex structure to create an array of vertices for drawing. Then, you create an array of GLubyte values. GLubyte is just a type alias for good old UInt8, and this array specifies the order in which to draw each of the three vertices that make up a triangle. That is, the first three integers (0, 1, 2) indicate to draw the first triangle by using the 0th, the 1st and, finally, the 2nd verex. The second three integers (2, 3, 0) indicate to draw the second triangle by using the 2nd, the 3rd and then the 0th vertex.

Because triangles share vertices, this saves resources: You create just one array with all of the four vertices, and then you use a separate array to define triangles by referring to those vertices. Because an array index that points to a vertex takes less memory than the vertex itself, this saves memory.

With this complete, you have all the information you need to pass to OpenGL to draw your square.

Creating Vertex Buffer Objects and a Vertex Array Object

The best way to send data to OpenGL is through something called Vertex Buffer Objects. These are OpenGL objects that store buffers of vertex data for you.

There are three types of objects to be aware of, here:

  • Vertex Buffer Object (VBO): Keeps track of the per-vertex data itself, like the data you have in the Vertices array.
  • Element Buffer Object (EBO): Keeps track of the indices that define triangles, like the indices you have stored in the Indices array.
  • Vertex Array Object (VAO): This object can be bound like the vertex buffer object. Any future vertex attribute calls you make — after binding a vertex array object — will be stored inside it. What this means is that you only have to make calls to configure vertex attribute pointers once and then — whenever you want to draw an object — you bind the corresponding VAO. This facilitates and speeds up drawing different vertex data with different configurations.

At the top of ViewController.swift, add the following Array extension to help getting the size, in bytes, of the Vertices and Indices arrays:

extension Array {
  func size() -> Int {
    return MemoryLayout<Element>.stride * self.count
  }
}

An important subtlety here is that, in order to determine the memory occupied by an array, we need to add up the stride, not the size, of its constituent elements. An element’s stride is, by definition, the amount of memory the element occupies when it is in an array. This can be larger than the element’s size because of padding, which is basically a technical term for “extra memory that we use up to keep the CPU happy.”

Next, add the following variables inside ViewController:

private var ebo = GLuint()
private var vbo = GLuint()
private var vao = GLuint()

These are variables for the element buffer object, the vertex buffer object and the vertex array object. All are of type GLuint, a type alias for UInt32.

Setting Up the Buffers

Now, you want to start generating and binding buffers, passing data to them so that OpenGL knows how to draw your square on screen. Start by adding the following helper variables at the bottom of the setupGL() method:

// 1
let vertexAttribColor = GLuint(GLKVertexAttrib.color.rawValue)
// 2
let vertexAttribPosition = GLuint(GLKVertexAttrib.position.rawValue)
// 3    
let vertexSize = MemoryLayout<Vertex>.stride
// 4
let colorOffset = MemoryLayout<GLfloat>.stride * 3
// 5
let colorOffsetPointer = UnsafeRawPointer(bitPattern: colorOffset)

Here’s what that does:

  1. When you generate your buffers, you will need to specify information about how to read colors and positions from your data structures. OpenGL expects a GLuint for the color vertex attribute. Here, you use the GLKVertexAttrib enum to get the color attribute as a raw GLint. You then cast it to GLuint — what the OpenGL method calls expect — and store it for use in this method.
  2. As with the color vertex attribute, you want to avoid having to write that long code to store and read the position attribute as a GLuint.
  3. Here, you take advantage of the MemoryLayout enum to get the stride, which is the size, in bytes, of an item of type Vertex when in an array.
  4. To get the memory offset of the variables corresponding to a vertex color, you use the MemoryLayout enum once again except, this time, you specify that you want the stride of a GLfloat multiplied by three. This corresponds to the x, y and z variables in the Vertex structure.
  5. Finally, you need to convert the offset into the required type: UnsafeRawPointer.

With some helper constants ready, it’s time for you to create your buffers and set them up via a VAO for drawing.

Creating VAO Buffers

Add the following code right after the constants that you added inside setupGL():

// 1
glGenVertexArraysOES(1, &vao)
// 2
glBindVertexArrayOES(vao)

The first line asks OpenGL to generate, or create, a new VAO. The method expects two parameters: The first one is the number of VAOs to generate — in this case one — while the second expects a pointer to a GLuint wherein it will store the ID of the generated object.

In the second line, you are telling OpenGL to bind the VAO you that created and stored in the vao variable and that any upcoming calls to configure vertex attribute pointers should be stored in this VAO. OpenGL will use your VAO until you unbind it or bind a different one before making draw calls.

Using VAOs adds a little bit more code, but it will save you tons of time by not having to write lines of code to everything needed to draw even the simplest geometry.

Having created and bound the VAO, it’s time to create and set up the VBO.

Creating VBO Buffers

Continue by adding this code at the end of setupGL():

glGenBuffers(1, &vbo)
glBindBuffer(GLenum(GL_ARRAY_BUFFER), vbo)
glBufferData(GLenum(GL_ARRAY_BUFFER), // 1
             Vertices.size(),         // 2
             Vertices,                // 3
             GLenum(GL_STATIC_DRAW))  // 4

Like the VAO, glGenBuffers tells OpenGL you want to generate one VBO and store its identifier in the vbo variable.

Having created the VBO, you now bind it as the current one in the call to glBindBuffer. The method to bind a buffer expects the buffer type and buffer identifier. GL_ARRAY_BUFFER is used to specify that you are binding a vertex buffer and, because it expects a value of type GLenum, you cast it to one.

The call to glBufferData is where you’re passing all your vertex information to OpenGL. There are four parameters that this method expects:

  1. Indicates to what buffer you are passing data.
  2. Specifies the size, in bytes, of the data. In this case, you use the size() helper method on Array that you wrote earlier.
  3. The actual data you are going to use.
  4. Tells OpenGL how you want the GPU to manage the data. In this case, you use GL_STATIC_DRAW because the data you are passing to the graphics card will rarely change, if at all. This allows OpenGL to further optimize for a given scenario.

By now, you may have noticed that working with OpenGL in Swift has a pattern of having to cast certain variables or parameters to OpenGL-specific types. These are type aliases and nothing for you to be worried about. It makes your code a bit longer or trickier to read at first, but it’s not difficult to understand once you get into the flow of things.

You have now passed the color and position data for all your vertices to the GPU. But you still need to tell OpenGL how to interpret that data when you ask it to draw it all on screen. To do that, add this code at the end of setupGL():

glEnableVertexAttribArray(vertexAttribPosition)
glVertexAttribPointer(vertexAttribPosition,       // 1 
                      3,                          // 2
                      GLenum(GL_FLOAT),           // 3
                      GLboolean(UInt8(GL_FALSE)), // 4 
                      GLsizei(vertexSize),        // 5
                      nil)                        // 6
    
glEnableVertexAttribArray(vertexAttribColor)
glVertexAttribPointer(vertexAttribColor, 
                      4, 
                      GLenum(GL_FLOAT), 
                      GLboolean(UInt8(GL_FALSE)), 
                      GLsizei(vertexSize), 
                      colorOffsetPointer)

You see another set of very similar method calls. Here’s what each does, along with the parameters they take. Before you can tell OpenGL to interpret your data, you need to tell it what it’s even interpreting in the first place.

The call to glEnableVertexAttribArray enables the vertex attribute for position so that, in the next line of code, OpenGL knows that this data is for the position of your geometry.

glVertexAttribPointer takes six parameters so that OpenGL understands your data. This is what each parameter does:

  1. Specifies the attribute name to set. You use the constants that you set up earlier in the method.
  2. Specifies how many values are present for each vertex. If you look back up at the Vertex struct, you’ll see that, for the position, there are three GLfloat (x, y, z) and, for the color, there are four GLfloat (r, g, b, a).
  3. Specifies the type of each value, which is float for both position and color.
  4. Specifies if you want the data to be normalized. This is almost always set to false.
  5. The size of the stride, which is a fancy way of saying “the size of the data structure containing the per-vertex data, when it’s in an array.” You pass vertexSize, here.
  6. The offset of the position data. The position data is at the very start of the Vertices array, which is why this value is nil.

The second set of calls to glEnableVertexttribArray and glVertexAttribPointer are identical except that you specify that there are four components for color (r, g, b, a), and you pass a pointer for the offset of the color memory of each vertex in the Vertices array.

With your VBO and its data ready, it’s time to tell OpenGL about your indices by using the EBO. This will tell OpenGL what vertices to draw and in what order.

Creating EBO Buffers

Add the following code at the bottom of setupGL():

glGenBuffers(1, &ebo)
glBindBuffer(GLenum(GL_ELEMENT_ARRAY_BUFFER), ebo)
glBufferData(GLenum(GL_ELEMENT_ARRAY_BUFFER), 
             Indices.size(), 
             Indices, 
             GLenum(GL_STATIC_DRAW))

This code should look familiar to you. It’s identical to what you used for the VBO. You first generate a buffer and store its identifier in the ebo variable, then you bind this buffer to the GL_ELEMENT_ARRAY_BUFFER, and, finally, you pass the Indices array data to the buffer.

The last bit of code to add to this method is the following lines:

glBindVertexArrayOES(0)
glBindBuffer(GLenum(GL_ARRAY_BUFFER), 0)
glBindBuffer(GLenum(GL_ELEMENT_ARRAY_BUFFER), 0)

First, you unbind (detach) the VAO so that any further calls to set up buffers, attribute pointers, or something else, is not done on this VAO. The same is done for the vertex and element buffer objects. While not necessary, unbinding is a good practice and can help you avoid logic bugs in the future by not associating setup and configuration to the wrong object.

Build your project to make sure it compiles, then press ahead.