3.0: Giving Life to the View

There are two ways to animate the scene, NSTimer and CVDisplayLink.  NSTimer is the shotgun approach.  You set the timer to go off a 100 times each second and hope that one of them is going to hit the mark and be displayed.  CVDisplayLink can be used to request a frame according to the display's refresh rate (60Hz or 60 frames per second).  Once choice is not necessarily better than the other; however, a strict design choice is made when you choose one over the other.  NSTimer is innately single-threaded while CVDisplayLink is multi-threaded.  When working with OpenGL in a multi-threaded environment, you cannot make changes to the OpenGL state until you have "locked the focus" to the context you are affecting.  We're going to use the CVDisplayLink going forward, but for completeness, we're going to set up an NSTimer first.

First, add a timer property to the top of the class after the other class properties.

private var timer = NSTimer()

Second, tell the view's context how often to expect new frames to be drawn.  A value of 1 indicates that the view is to expect a new frame once for refresh of the screen (i.e. 60 frames per 1 second).  The NSOpenGLContextParamter, GLCPSwapInterval, may be passed using a shortened dot notation because the type is already known by the function.

self.openGLContext?.setValues([1], forParameter: .GLCPSwapInterval)

Third, in prepareOpenGL( ), at the bottom of the definition, redefine the timer.  When initializing the timer, we set the time interval to 1/1000 of a second (0.001), the target calls a method when the timer fires (self indicates on of SwiftOpenGLView's methods is called), selector is the name of the method to be called, userInfo is a way to pass additional information to the method being called (we don't have anything additional we need to send so we pass in nil), and repeats indicates if we want the timer to continue firing (of course, we do).

self.timer = NSTimer(timeInterval: 0.001, target: self, selector: "redraw", userInfo: nil, repeats: true)

Fourth, we add the timer to the current application loop with appropriate hints:  default and event tracking.  These mean that the timer fires when idle and when events (such as dragging the view or clicking in the view) occur.

NSRunLoop.currentRunLoop().addTimer(self.timer, forMode: NSDefaultRunLoopMode)
NSRunLoop.currentRunLoop().addTimer(self.timer, forMode: NSEventTrackingRunLoopMode)

Fifth, we define the redraw( ) function.  It will simply tell the view and any subviews, that it is time to call drawRect(_:).

func redraw() {
   
    self.display()
        

}

Note that this function cannot be a private function.  Finally, we'll stop the timer and remove it from the run loop by invalidating the timer in deinit.

self.timer.invalidate()

Now that we have a functioning timer, let's add a couple lines to our drawRect(_:) that will result in animation.  The key to animation is change over time.  We can achieve this with CACurrentMediaTime( ).  This function returns the amount of time that has passed since the app started.  Using the sin( ) function, we can turn this number into a value between 0.0 and 1.0.  Then we can use this value in the definition of our background color.  At the top of drawView( ), add the following lines.

let value = Float(sin(CACurrentMediaTime()))
glClearColor(value, value, value, 1.0)

Run the app and you see the background change from black to white to black again in a sin pattern.


The CVDisplayLink is a little more complicated to implement and work with (mainly because it is an API written in C), but you get back a bunch of computing power and memory.

The NSTimer driver app 
The CVDisplayLink driven app

Notice in the amount of CPU required by each build.  The NSTimer driver app requires 5 times the CPU power as the CVDisplayLink app.  I ran these apps for nearly the same amount of time and even thought the CVDisplayLink app ran for a little longer, the amount of memory used is still less than the NSTimer driven app.  Over time, the memory used reaches a critical mass and unneeded data gleaned and given back to the system.  CVDisplayLink maintains a lower, more stable profile for a longer period.  NSTimer's memory requirement may start out lower, but builds up fast.  Regardless, it is again, up to you, the programmer, to decide what you are willing to sacrifice and gain with design decisions.

Within a new target let's implement the CVDisplayLink.  We'll define a displayLink property at the top of the class after the other properties.  We'll make the property optional for two reasons, we may have an implementation where animation is not required on start, and we don't want to have to start working with CVDisplayLink code until the end of prepareOpenGL( ).

private var displayLink: CVDisplayLink?

Then we'll set the context's swap interval parameter at the end of init(_:) after we create the context.

self.openGLContext?.setValues([1], forParameter: .GLCPSwapInterval)

Now at the end of prepareOpenGL( ), we'll define a nested function that we will use as our callback function.  Callback functions are a C API's equivalent of the NSTimer target/selector parameters.  The name of the callback is CVDisplayLinkOutputCallback and it's signature is quite long.  We're only going to use two of it's six parameters.

func displayLinkOutputCallback(displayLink: CVDisplayLink, _ inNow: UnsafePointer<CVTimeStamp>,
      _ inOutputTime: UnsafePointer<CVTimeStamp>, _ flagsIn: CVOptionFlags, _ flagsOut: 
      UnsafeMutablePointer<CVOptionFlags>, _ displayLinkContext: UnsafeMutablePointer<Void>) ->
      CVReturn {

    unsafeBitCast(displayLinkContext, SwiftOpenGLView.self).drawView()
            
    return kCVReturnSuccess
}

Most of the parameters of this function aren't important right now, but we'll talk about the displayLinkContext.  This parameter's type, UnsafeMutablePointer<Void>, is how we pass in a reference to the view which contains the context we are drawing into.  It's type is Void initially so we need to cast it to the appropriate type before we can call methods from within that object.  I got some help with this process form a fellow developer on the Apple Developer Forums (a big thank you to @OOPer).  unsafeBitCast( ) takes two parameters, the first is the initial object, and second is the type we wish to cast to.  SwiftOpenGLView.self indicates we are casting to an instance of SwiftOpenGLView.  Finally, we assume everything went well and return the CVReturn success flag.

Notice that we are using the drawView( ) function directly.  We're putting lock, buffer swap, and unlock methods to the drawView( ) function soon so were safe and don't need to call display( ) on the view.

Now we create a link to use and set it to the view's displayLink property.  There are a few permutation of this function, but we are using one that looks for all of the available displays and creates a link to them.

CVDisplayLinkCreateWithActiveCGDisplays(&displayLink)

Now that we have a link, we set the callback function.  This function takes three parameters, the displayLink, the name (not a literal) of the callback function, and a pointer to object that contains the function you'll calling in the callback function.  In other words, we are telling CVDisplayLink to pass the instance of SwiftOpenGLView as the displayLinkContext argument in the callback function.  Notice that the type is the same, UnsafeMutablePointer<Void>.  The unsafeAddressOf(self) is how we pass the pointer, the address of the instance (something that you always try to avoid doing in Swift, but is completely necessary when working with C API's).

CVDisplayLinkSetOutputCallback(displayLink!, displayLinkOutputCallback
   UnsafeMutablePointer<Void>(unsafeAddressOf(self)))

Start the link.

CVDisplayLinkStart(displayLink!)

In draw view, grab the context.  We'll use a guard just in case we don't get one back, but we'll just use a bogus error for now.

guard let context = self.openGLContext else {
    Swift.print("oops")
    return

}

Tell the app which context to apply the following OpenGL calls to with makeCurrentContext( ).

context.makeCurrentContext()

Now lock the focus to this context.  This ensures the GPU, which is multi-threaded, doesn't access data that being used during the following calls.  The CPU has a one track mind, and can't deal with GPU's multitasking nature, so the CPU set's some temporary boundaries to say, "Right now, this is what we're going to work on.  Don't do anything else until we're done here".  The "stuff" is the context we want to work with turned into the C type CGLContextObj.

CGLLockContext(context.CGLContextObj)

Once we're done doing our drawing, we tell the app to swap the buffers and then unlock the focus and let the GPU run free again.

CGLFlushDrawable(context.CGLContextObj)
CGLUnlockContext(context.CGLContextObj)

The last thing to do is stop the display link when the view is destroyed.  We'll use deinit again.

CVDisplayLinkStop(displayLink!)

Run the app and you'll get the same triangle with changing background.  Below is the full implementation of the CVDisplayLink driven app.

import Cocoa
import OpenGL.GL3


final class SwiftOpenGLView: NSOpenGLView {
    
    private var programID: GLuint = 0
    private var vaoID: GLuint = 0
    private var vboID: GLuint = 0
    private var tboID: GLuint = 0
    
    //  The CVDisplayLink for animating.  Optional value initialized to nil.
    private var displayLink: CVDisplayLink?
    
    required init?(coder: NSCoder) {
        super.init(coder: coder)
        
        //  We'll use double buffering this time (one buffer is displayed while the other is
        //  calculated, then we swap them.
        let attrs: [NSOpenGLPixelFormatAttribute] = [
            UInt32(NSOpenGLPFAAccelerated),
            UInt32(NSOpenGLPFADoubleBuffer),
            UInt32(NSOpenGLPFAColorSize), UInt32(32),
            UInt32(NSOpenGLPFAOpenGLProfile), UInt32(NSOpenGLProfileVersion3_2Core),
            UInt32(0)
        ]
        guard let pixelFormat = NSOpenGLPixelFormat(attributes: attrs) else {
            Swift.print("pixelFormat could not be constructed")
            return
        }
        self.pixelFormat = pixelFormat
        guard let context = NSOpenGLContext(format: pixelFormat, shareContext: nil) else {
            Swift.print("context could not be constructed")
            return
        }
        self.openGLContext = context
        
        //  Set the context's swap interval parameter to 60Hz (i.e. 1 frame per swamp)
        self.openGLContext?.setValues([1], forParameter: .GLCPSwapInterval)
        
    }
    
    override func prepareOpenGL() {
        
        super.prepareOpenGL()
        
        glClearColor(0.0, 0.0, 0.0, 1.0)
        
        programID = glCreateProgram()
        
        //format:                x,    y,    r,   g,   b,    s,   t,    nx,   ny,   nz
        let data: [GLfloat] = [-1.0, -1.01.0, 0.0, 1.00.0, 2.0,  -1.0, -1.0, 0.0001,
                                0.01.00.0, 1.0, 0.01.0, 0.0,   0.01.0, 0.0001,
                                1.0, -1.00.0, 0.0, 1.02.0, 2.0,   1.0, -1.0, 0.0001]
        
        let fileURL = NSBundle.mainBundle().URLForResource("Texture", withExtension: "png")
        
        let dataProvider = CGDataProviderCreateWithURL(fileURL)
        let image = CGImageCreateWithPNGDataProvider(dataProvider, nil, false
                     .RenderingIntentDefault)
        
        let textureData = UnsafeMutablePointer<Void>(malloc(256 * 4 * 256))
        
        let context = CGBitmapContextCreate(textureData, 256, 256, 8, 4 * 256
                       CGColorSpaceCreateWithName(kCGColorSpaceGenericRGB),  
                       CGImageAlphaInfo.PremultipliedLast.rawValue)
        
        CGContextDrawImage(context, CGRectMake(0.0, 0.0, 256.0, 256.0), image)
        
        glGenTextures(1, &tboID)
        glBindTexture(GLenum(GL_TEXTURE_2D), tboID)
        
        glTexParameteri(GLenum(GL_TEXTURE_2D), GLenum(GL_TEXTURE_MIN_FILTER), GL_LINEAR)
        glTexParameteri(GLenum(GL_TEXTURE_2D), GLenum(GL_TEXTURE_MAG_FILTER), GL_LINEAR)
        glTexParameteri(GLenum(GL_TEXTURE_2D), GLenum(GL_TEXTURE_WRAP_S), GL_REPEAT)
        glTexParameteri(GLenum(GL_TEXTURE_2D), GLenum(GL_TEXTURE_WRAP_T), GL_REPEAT)
        
        glTexImage2D(GLenum(GL_TEXTURE_2D), 0, GL_RGBA, 256, 256, 0, GLenum(GL_RGBA), 
         GLenum(GL_UNSIGNED_BYTE), textureData)
        
        free(textureData)
        
        glGenBuffers(1, &vboID)
        glBindBuffer(GLenum(GL_ARRAY_BUFFER), vboID)
        glBufferData(GLenum(GL_ARRAY_BUFFER), data.count * sizeof(GLfloat), data
         GLenum(GL_STATIC_DRAW))
        
        glGenVertexArrays(1, &vaoID)
        glBindVertexArray(vaoID)
        
        glVertexAttribPointer(0, 2, GLenum(GL_FLOAT), GLboolean(GL_FALSE), 40
         UnsafePointer<GLuint>(bitPattern: 0))
        glEnableVertexAttribArray(0)
        
        glVertexAttribPointer(1, 3, GLenum(GL_FLOAT), GLboolean(GL_FALSE), 40
         UnsafePointer<GLuint>(bitPattern: 8))
        glEnableVertexAttribArray(1)
        
        glVertexAttribPointer(2, 2, GLenum(GL_FLOAT), GLboolean(GL_FALSE), 40
         UnsafePointer<GLuint>(bitPattern: 20))
        glEnableVertexAttribArray(2)
        
        glVertexAttribPointer(3, 3, GLenum(GL_FLOAT), GLboolean(GL_FALSE), 40
         UnsafePointer<GLuint>(bitPattern:28))
        glEnableVertexAttribArray(3)
        
        glBindVertexArray(0)
        
        let vs = glCreateShader(GLenum(GL_VERTEX_SHADER))
        var source = "#version 330 core                     \n" +
            "layout (location = 0) in vec2 position;        \n" +
            "layout (location = 1) in vec3 color;           \n" +
            "layout (location = 2) in vec2 texturePosition; \n" +
            "layout (location = 3) in vec3 normal;          \n" +
            "out vec3 passPosition;                         \n" +
            "out vec3 passColor;                            \n" +
            "out vec2 passTexturePosition;                  \n" +
            "out vec3 passNormal;                           \n" +
            "void main()                                    \n" +
            "{                                              \n" +
            "     gl_Position = vec4(position, 0.0, 1.0);   \n" +
            "     passPosition = vec3(position, 0.0);       \n" +
            "     passColor = color;                        \n" +
            "     passTexturePosition = texturePosition;    \n" +
            "     passNormal = normal;                      \n" +
        "}                                              \n"
        if let vss = source.cStringUsingEncoding(NSASCIIStringEncoding) {
            var vssptr = UnsafePointer<GLchar>(vss)
            glShaderSource(vs, 1, &vssptr, nil)
            glCompileShader(vs)
            var compiled: GLint = 0
            glGetShaderiv(vs, GLbitfield(GL_COMPILE_STATUS), &compiled)
            if compiled <= 0 {
                Swift.print("Could not compile vertex, getting log")
                var logLength: GLint = 0
                glGetShaderiv(vs, GLenum(GL_INFO_LOG_LENGTH), &logLength)
                Swift.print(" logLength = \(logLength)")
                if logLength > 0 {
                    let cLog = UnsafeMutablePointer<CChar>(malloc(Int(logLength)))
                    glGetShaderInfoLog(vs, GLsizei(logLength), &logLength, cLog)
                    if let log = String(CString: cLog, encoding: NSASCIIStringEncoding) {
                        Swift.print("log = \(log)")
                        free(cLog)
                    }
                }
            }
        }
        
        
        let fs = glCreateShader(GLenum(GL_FRAGMENT_SHADER))
        source = "#version 330 core                                                          \n" +
            "uniform sampler2D sample;                                                       \n" +
            "uniform struct Light {                                                          \n" +
            "    vec3 color;                                                                 \n" +
            "    vec3 position;                                                              \n" +
            "    float ambient;                                                              \n" +
            "    float specStrength;                                                         \n" +
            "    float specHardness;                                                         \n" +
            "} light;                                                                        \n" +
            "in vec3 passPosition;                                                           \n" +
            "in vec3 passColor;                                                              \n" +
            "in vec2 passTexturePosition;                                                    \n" +
            "in vec3 passNormal;                                                             \n" +
            "out vec4 outColor;                                                              \n" +
            "void main()                                                                     \n" +
            "{                                                                               \n" +
            "     vec3 normal = normalize(passNormal);                                       \n" +
            "     vec3 lightRay = normalize(light.position - passPosition);                  \n" +
            "     float intensity = dot(normal, lightRay);                                   \n" +
            "     intensity = clamp(intensity, 0, 1);                                        \n" +
            "     vec3 viewer = normalize(vec3(0.0, 0.0, 0.2) - passPosition);               \n" +
            "     vec3 reflection = reflect(lightRay, normal);                               \n" +
            "     float specular = pow(max(dot(viewer, reflection), 0.0),
                                     light.specHardness);                                   \n" +
            "     vec3 light = light.ambient + light.color * intensity +
                                 light.specStrength * specular * light.color;               \n" +
            "     vec3 surface = texture(sample, passTexturePosition).rgb * passColor;       \n" +
            "     vec3 rgb = surface * light;                                                \n" +
            "     outColor = vec4(rgb, 1.0);                                                 \n" +
            "}                                                                               \n"

        if let fss = source.cStringUsingEncoding(NSASCIIStringEncoding) {
            var fssptr = UnsafePointer<GLchar>(fss)
            glShaderSource(fs, 1, &fssptr, nil)
            glCompileShader(fs)
            var compiled: GLint = 0
            glGetShaderiv(fs, GLbitfield(GL_COMPILE_STATUS), &compiled)
            if compiled <= 0 {
                Swift.print("Could not compile fragment, getting log")
                var logLength: GLint = 0
                glGetShaderiv(fs, GLbitfield(GL_INFO_LOG_LENGTH), &logLength)
                Swift.print(" logLength = \(logLength)")
                if logLength > 0 {
                    let cLog = UnsafeMutablePointer<CChar>(malloc(Int(logLength)))
                    glGetShaderInfoLog(fs, GLsizei(logLength), &logLength, cLog)
                    if let log = String(CString: cLog, encoding: NSASCIIStringEncoding) {
                        Swift.print("log = \(log)")
                        free(cLog)
                    }
                }
            }
        }
        
        glAttachShader(programID, vs)
        glAttachShader(programID, fs)
        glLinkProgram(programID)
        var linked: GLint = 0
        glGetProgramiv(programID, UInt32(GL_LINK_STATUS), &linked)
        if linked <= 0 {
            Swift.print("Could not link, getting log")
            var logLength: GLint = 0
            glGetProgramiv(programID, UInt32(GL_INFO_LOG_LENGTH), &logLength)
            Swift.print(" logLength = \(logLength)")
            if logLength > 0 {
                let cLog = UnsafeMutablePointer<CChar>(malloc(Int(logLength)))
                glGetProgramInfoLog(programID, GLsizei(logLength), &logLength, cLog)
                if let log = String(CString: cLog, encoding: NSASCIIStringEncoding) {
                    Swift.print("log: \(log)")
                }
                free(cLog)
            }
        }
        
        glDeleteShader(vs)
        glDeleteShader(fs)
        
        let sampleLocation = glGetUniformLocation(programID, "sample")
        glUniform1i(sampleLocation, GL_TEXTURE0)
        
        glUseProgram(programID)
        
        //  Uniforms for the light struct.  Each component is accessed using dot notation.
        glUniform3fv(glGetUniformLocation(programID, "light.color"), 1, [1.0, 1.0, 1.0])
        glUniform3fv(glGetUniformLocation(programID, "light.position"), 1, [0.0, 1.0, 0.1])
        glUniform1f(glGetUniformLocation(programID, "light.ambient"), 0.25)
        glUniform1f(glGetUniformLocation(programID, "light.specStrength"), 1.0)
        glUniform1f(glGetUniformLocation(programID, "light.specHardness"), 32)
        
        //  Set up the CVDisplayLink not that the pipeline is defined.
        //
        //  The following code was developed in response to personal endeavor as well
        //    as an answer to a StackOverflow question that I initially answered before the
        //    CFunctionPointer worked.  We'll cover the Swift/Obj-C/C implementation I developed
        //    for the same answer next time.  I feel it is still worthwhile should you require
        //    bridging between Obj-C and Swift.
        //
        //  The callback function is called everytime CVDisplayLink says its time to get
        //    a new frame.
        func displayLinkOutputCallback(displayLink: CVDisplayLink, _ inNow:
              UnsafePointer<CVTimeStamp>, _ inOutputTime: UnsafePointer<CVTimeStamp>,
              _ flagsIn: CVOptionFlags, _ flagsOut: UnsafeMutablePointer<CVOptionFlags>,
              _ displayLinkContext: UnsafeMutablePointer<Void>) -> CVReturn {
            
            /*  The displayLinkContext is CVDisplayLink's parameter definition of the view in
                which we are working.  In order to access the methods of a given view we need to
                specify what kind of view it is as right now the UnsafeMutablePointer<Void> just
                means we have a pointer to "something".  To cast the pointer
                such that the compiler at runtime can access the methods associated with our
                SwiftOpenGLView, we use an unsafeBitCast.  The definition of which states,
                "Returns the the bits of x, interpreted as having type U."  We may then call any
                of that view's methods.  Here we call drawView() which we draw a frame for
                rendering.  */
            unsafeBitCast(displayLinkContext, SwiftOpenGLView.self).drawView()
            
            //  We are going to assume that everything went well for this mock up, and pass
            //    success as the CVReturn
            return kCVReturnSuccess
        }
        
        //  Grab the a link to the active displays, set the callback defined above, and
        //    start the link.

        /*  An alternative to a nested function is a global function or a closure passed
            as the argument--a local function (i.e. a function defined within the class)
            is NOT allowed. */

        //  The UnsafeMutablePointer<Void>(unsafeAddressOf(self)) passes a pointer to the
        //    instance of our class.
        CVDisplayLinkCreateWithActiveCGDisplays(&displayLink)
        CVDisplayLinkSetOutputCallback(displayLink!, displayLinkOutputCallback,
         UnsafeMutablePointer<Void>(unsafeAddressOf(self)))
        CVDisplayLinkStart(displayLink!)

        //  Test Render
        drawView()
        
    }
    
    override func drawRect(dirtyRect: NSRect) {
        super.drawRect(dirtyRect)
        
        // Drawing code here.
        
        drawView()
        
    }
    
    private func drawView() {
        
        //  Grab a context, make it the active context for drawing, and then lock the focus
        //  before making OpenGL calls that change state or data within objects.
        guard let context = self.openGLContext else {
            //  Just a filler error
            Swift.print("oops")
            return
        }
        
        context.makeCurrentContext()
        CGLLockContext(context.CGLContextObj)
        
        //  To make the animation visible, we'll change the background color over time.
        //  CACurrentMediaTime() returns the amount of time since the app started.
        //  sin() is applied to this value and then a float value is made from it.
        //  glClearColor() takes four floats to create an rgba color.  We have not activated
        //  blending, so no matter what value we pass here is ignored.
        let value = Float(sin(CACurrentMediaTime()))
        glClearColor(value, value, value, 1.0)
        
        glClear(GLbitfield(GL_COLOR_BUFFER_BIT))
        
        glUseProgram(programID)
        glBindVertexArray(vaoID)
        
        glDrawArrays(GLenum(GL_TRIANGLES), 03)
        
        glBindVertexArray(0)
        
        //  glFlush() is replaced with CGLFlushDrawable() and swaps the buffer being displayed
        CGLFlushDrawable(context.CGLContextObj)
        CGLUnlockContext(context.CGLContextObj)
    }
    
    deinit {
        //  Stop the display link.
        CVDisplayLinkStop(displayLink!)
        glDeleteVertexArrays(1, &vaoID)
        glDeleteBuffers(1, &vboID)
        glDeleteProgram(programID)
        glDeleteTextures(1, &tboID)
    }
    

}

There isn't a whole lot to play around with in this tutorial, but be sure you understand the concepts behind setting up the animation loop for both types discussed.  In particular, really think about and make sure you understand how working with pointers in Swift works in this case.  There is much that I don't understand about it either, mainly because it isn't written right up front in the documentation.  The reasoning being that we are not supposed to have to worry about pointers in Swift.  However, when Swift is mixed with ObjC and C, you must start worrying about it.  The concept of how to deal with self also has it's mysteries.

Next time, we'll tackle movement through our 3D space.  This is a huge step!  Hope you're excited.

Please remember to comment below.  I would really appreciate feedback as I try to write easy to understand tutorials.  If you can think of a way to make something easier to understand, or if I have made an error in the content, let me know.  Thank you following!

When you're ready click here to take the preliminary steps toward 3D movement.

You can find the project target for NSTimer at StartAnimating and the target for CVDisplayLink at AnimationNext on GitHub.

Comments

Popular posts from this blog

4.0.0: MVC OpenGL Part 1

Using Swift with OpenGL

0.0: Swift OpenGL Setup