Apple has developed a framework called GLKit to help developers create apps that leverage OpenGL and to abstract boilerplate code.
It also allows developers to focus on drawing, not on getting the project set up.
GLKit provides functionality in four areas:
- Views and View Controllers: These abstract much of the boilerplate code that GLKit uses to set up a basic OpenGL ES (Embedded Systems) project.
- Effects: These implement common shading behaviors and are a handy way of setting up basic lighting, shading, reflection mapping and skybox effects.
- Math: Provides helpers and functions for common math routines like vector and matrix manipulation.
- Texture Loading: Makes it much easier to load images as textures to be used in OpenGL.
Your OpenGL context has a buffer that it uses to store the colors that will be displayed to the screen. You can use the Color Format property to set the color format for each pixel in the buffer.
ViewController: GLKViewController
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
private var context: EAGLContext?
private func setupGL() {
// 1
context = EAGLContext(api: .openGLES3)
// 2
EAGLContext.setCurrent(context)
if let view = self.view as? GLKView, let context = context {
// 3
view.context = context
// 4
delegate = self
}
}
1
2
3
4
5
6
override func glkView(_ view: GLKView, drawIn rect: CGRect) {
// 1
glClearColor(0.85, 0.85, 0.85, 1.0) //specify the RGB and alpha (transparency) values to use when clearing the screen
// 2
glClear(GLbitfield(GL_COLOR_BUFFER_BIT)) // perform the clearing
}
creating vertex data for a simple square
Only triangle geometry can be rendered using OpenGL.
One of the nice things about OpenGL ES is that you can keep your vertex data organized however you like.
1
2
3
4
5
6
7
8
9
struct Vertex {
var x: GLfloat
var y: GLfloat
var z: GLfloat
var r: GLfloat
var g: GLfloat
var b: GLfloat
var a: GLfloat
}
1
2
3
4
5
6
7
8
9
10
11
12
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] = [// specifies the order in which to draw each of the three vertices that make up a triangle, the first three integers (0, 1, 2) indicate to draw the first triangle by using the 0th, the 1st and, finally, the 2nd verex.
0, 1, 2,
2, 3, 0
]
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 theVertices
array. - Element Buffer Object (EBO): Keeps track of the
indices
that define triangles, like the indices you have stored in theIndices
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.
1
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.”
1
2
3
private var ebo = GLuint()
private var vbo = GLuint()
private var vao = GLuint()
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.
1
2
3
4
5
6
7
8
9
10
// 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)
Creating VAO Buffers
1
2
3
4
5
// 1: asks OpenGL to generate, or create, a new VAO
glGenVertexArraysOES(1, &vao)
// 2: 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.
glBindVertexArrayOES(vao)
Creating VBO Buffers
1
2
3
4
5
6
7
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
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.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
glEnableVertexAttribArray(vertexAttribPosition) // 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(vertexAttribPosition, // 1: Specifies the attribute name to set. You use the constants that you set up earlier in the method.
3, // 2: Specifies how many values are present for each vertex
GLenum(GL_FLOAT), // 3: the type of each value, which is float for both position and color
GLboolean(UInt8(GL_FALSE)), // 4 : Specifies if you want the data to be normalized, This is almost always set to false
GLsizei(vertexSize), // 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.
nil) // 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
glEnableVertexAttribArray(vertexAttribColor)
glVertexAttribPointer(vertexAttribColor,
4,
GLenum(GL_FLOAT),
GLboolean(UInt8(GL_FALSE)),
GLsizei(vertexSize),
colorOffsetPointer)
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:
1
2
3
4
5
6
glGenBuffers(1, &ebo)
glBindBuffer(GLenum(GL_ELEMENT_ARRAY_BUFFER), ebo)
glBufferData(GLenum(GL_ELEMENT_ARRAY_BUFFER),
Indices.size(),
Indices,
GLenum(GL_STATIC_DRAW))
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.
1
2
3
glBindVertexArrayOES(0)
glBindBuffer(GLenum(GL_ARRAY_BUFFER), 0)
glBindBuffer(GLenum(GL_ELEMENT_ARRAY_BUFFER), 0)
Tidying up
1
2
3
4
5
6
7
8
9
10
11
private func tearDownGL() {
EAGLContext.setCurrent(context)
glDeleteBuffers(1, &vao)
glDeleteBuffers(1, &vbo)
glDeleteBuffers(1, &ebo)
EAGLContext.setCurrent(nil)
context = nil
}