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.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
GLKit Tutorial for iOS: Getting started with OpenGL ES
35 mins
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:
- 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 theGLKVertexAttrib
enum to get thecolor
attribute as a rawGLint
. You then cast it toGLuint
— what the OpenGL method calls expect — and store it for use in this method. - 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
. - Here, you take advantage of the
MemoryLayout
enum to get the stride, which is the size, in bytes, of an item of typeVertex
when in an array. - 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 aGLfloat
multiplied by three. This corresponds to thex
,y
andz
variables in theVertex
structure. - 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:
- Indicates to what buffer you are passing data.
- Specifies the size, in bytes, of the data. In this case, you use the
size()
helper method onArray
that you wrote earlier. - The actual data you are going to use.
- 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:
- Specifies the attribute name to set. You use the constants that you set up earlier in the method.
- 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 threeGLfloat
(x, y, z) and, for the color, there are fourGLfloat
(r, g, b, a). - Specifies the type of each value, which is float for both position and color.
- Specifies if you want the data to be normalized. This is almost always set to false.
- 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. - The offset of the position data. The position data is at the very start of the
Vertices
array, which is why this value isnil
.
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.