As you guys probably notices, it was a while since last post here. I was quite busy with lot of projects, so did not had time for blogging.
If you ever wanted to do something with OpenGL, you can find lot of tutorials. But most of them are for ObjectiveC, there are some for Swift2 and almost nothing for Swift3. Well, there are some, but they seems to be very complex for beginners. What I want to show here, is very basic example of drawing red rectangle. Something everyone can start with. Later we will add textures and transformations, but lets start with this.
This tutorial and attached code was designed to work with XCode 8.3 and Swift 3.1 at the moment of writing this article.
1. Create new XCode project – Single Window, and call it whatever you want.
2. Create your new class “OpenGLView” as subclass of UIView
3. Now add some properties:
var _eaglLayer: CAEAGLLayer? var _context: EAGLContext? var _depthRenderBuffer = GLuint() var _colorRenderBuffer = GLuint() var rectangleAttr = GLuint() var vertexArray: GLuint = 0 var vertexBuffer: GLuint = 0
What they means? For holding OpenGL context (virtual space for operations) we set _context. Next, we need two buffers – color and depth, even if our app will display only flat rectangle, there is still Z-axis which we should take care of. For those we have _depthRenderBuffer and _colorRenderBuffer. Then we need pointer to draw our rectangle – that will be rectangleAttr. Finally vertexArray and vertexBuffer are just another pointers to drawing our shape. Be carefull – by pointer I mean just simple value (1, 0, and so on) which will inform OpenGL what we are doing, they are not pointers to memory.
4. Next important thing is:
override class var layerClass: AnyClass { get { return CAEAGLLayer.self } }
This will inform system that our layer is OpenGL.
5. Now we need to do lot of setup, lets start with layer itself:
func setupLayer() -> Int { _eaglLayer = self.layer as? CAEAGLLayer if (_eaglLayer == nil) { NSLog("setupLayer: _eaglLayer is nil") return -1 } _eaglLayer!.isOpaque = true return 0 }
Here we are assigning CAEAGLLayer to property and set its opaque param.
6. Next, we need to setup context:
func setupContext() -> Int { let api : EAGLRenderingAPI = EAGLRenderingAPI.openGLES2 _context = EAGLContext(api: api) if (_context == nil) { NSLog("Failed to initialize OpenGLES 2.0 context") return -1 } if (!EAGLContext.setCurrent(_context)) { NSLog("Failed to set current OpenGL context") return -1 } return 0 }
We are creating our context as instance of EAGLContext, and we’re setting OpenGL version to 2.0. Also we are informing rendering engine that our created context is current one.
7. Then we need our depth and frame buffers:
func setupDepthBuffer() -> Int { glGenRenderbuffers(1, &_depthRenderBuffer); glBindRenderbuffer(GLenum(GL_RENDERBUFFER), _depthRenderBuffer); glRenderbufferStorage(GLenum(GL_RENDERBUFFER), GLenum(GL_DEPTH_COMPONENT16), GLsizei(self.frame.size.width), GLsizei(self.frame.size.height)) return 0 } func setupFrameBuffer() -> Int { var framebuffer: GLuint = 0 glGenFramebuffers(1, &framebuffer) glBindFramebuffer(GLenum(GL_FRAMEBUFFER), framebuffer) glFramebufferRenderbuffer(GLenum(GL_FRAMEBUFFER), GLenum(GL_COLOR_ATTACHMENT0), GLenum(GL_RENDERBUFFER), _colorRenderBuffer) glFramebufferRenderbuffer(GLenum(GL_FRAMEBUFFER), GLenum(GL_DEPTH_ATTACHMENT), GLenum(GL_RENDERBUFFER), _depthRenderBuffer); return 0 }
Basically we need to generate buffer (using glGenRenderbuffers/glGenFramebuffers), then bind and set some parameters.
8. Also we need render buffer itself:
func setupRenderBuffer() -> Int { glGenRenderbuffers(1, &_colorRenderBuffer) glBindRenderbuffer(GLenum(GL_RENDERBUFFER), _colorRenderBuffer) if (_context == nil) { NSLog("setupRenderBuffer(): _context is nil") return -1 } if (_eaglLayer == nil) { NSLog("setupRenderBuffer(): _eagLayer is nil") return -1 } if (_context!.renderbufferStorage(Int(GL_RENDERBUFFER), from: _eaglLayer!) == false) { NSLog("setupRenderBuffer(): renderbufferStorage() failed") return -1 } return 0 }
9. Now something more interesting, we need to tell OpenGL where vertex data comes from, so first we need to declare our vertices:
let vertices: Array<GLfloat> = [ -1.0, -1.0, 0.0, -1.0, 1.0, 0.0, 1.0, 1.0, 0.0, 1.0, 1.0, 0.0, 1.0, -1.0, 0.0, -1.0, -1.0, 0.0 ]
Then we need to bind them to renderer:
func setupVBOs() -> Int { glGenVertexArrays(1, &vertexArray) glBindVertexArray(vertexArray) glGenBuffers(1, &vertexBuffer) glBindBuffer(GLenum(GL_ARRAY_BUFFER), vertexBuffer) glBufferData(GLenum(GL_ARRAY_BUFFER), vertices.count * MemoryLayout<GLfloat>.size, vertices, GLenum(GL_STATIC_DRAW)) return 0 }
What is happening here? Rectangle has only 4 vertices, but we have 6? This is required, because OpenGL renders everything as triangle, so if you want rectangle, you need to display 2 triangles. I will not describe screen coordinates here, it will be in another post. You need to trust me – they are good.
In our setupVBOs function we are generating vertex array pointer, vertex buffer, and assigning vertices data (which are floats) to array buffer.
10. To display anything on screen, we need display link created like this:
func setupDisplayLink() -> Int { let displayLink : CADisplayLink = CADisplayLink(target: self, selector: #selector(OpenGLView.render(displayLink:))) displayLink.add(to: RunLoop.current, forMode: RunLoopMode(rawValue: RunLoopMode.defaultRunLoopMode.rawValue)) return 0 }
11. Now we are getting closer, next and very important step, is shaders compilation, and we have compileShader and compileShaders functions to achieve this. I won’t paste them here, because they are too long, just look at github. Function called compileShaders takes 2 small files, SimpleVertex.glsl and SimpleFragment.glsl which contains very basic shader definition, and compiles them into OpenGL engine. What are shaders? Basically they are small programs written in shader language, which looks like C. They are responsabile for generating each pixel color and location. And they are very fast. I will describe them in next article. All you need to know for now, is that vertex shader is going to render our vertices on screen, and fragment shader is going to make our rectangle red.
12. And finally we can draw:
func render(displayLink: CADisplayLink) -> Int { glClearColor(0.0/255.0, 0.0/255.0, 120.0/255.0, 1.0) glClear(GLbitfield(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)) glEnable(GLenum(GL_DEPTH_TEST)) glViewport(0, 0, GLsizei(self.frame.size.width), GLsizei(self.frame.size.height)) glEnableVertexAttribArray(rectangleAttr) glVertexAttribPointer(rectangleAttr, 3, GLenum(GL_FLOAT), GLboolean(GL_FALSE), 0, UnsafePointer<GLfloat>(bitPattern:0)) glDrawArrays(GLenum(GL_TRIANGLES), 0, GLsizei(vertices.count/3)) glDisableVertexAttribArray(rectangleAttr) _context!.presentRenderbuffer(Int(GL_RENDERBUFFER)) return 0 }
So first we need to clear our screen and set fill color (dark blue here) with glClearColor. Then we are clearing buffers in glClear and enable depth testing in glEnable (remember we have Z axis even in 2d?). Now very important thing – viewport, which is just size of our screen. Then we need to enable our vertex array, set pointer and finally draw 2 triengles in glDrawArrays. Next we can disable array and finally display our image via presentRenderbuffer.
Ready to run project is located at https://github.com/blastar/BasicOpenGLRectangle. After running it, you should get something like this: