0.1: The SwiftOpenGLView

Create the View

Now that you have a working Xcode project, we can set up the app for displaying some OpenGL content.  Cocoa apps are built using a number of windows and views.  A single window contains one base view--the content view.  It is the content view that holds subsequent views such as buttons, labels, text fields, etc.  Notice that every part of the user interface is at it's heart, a view.

To insert a view into the app that will display our OpenGL content, we need to do two things:  1) use interface builder to insert an NSOpenGLView object into the app's window's content view, and 2) create a subclass of NSOpenGLView.

Interface Builder


To get to Interface Builder, click on Main.storyboard in the Project Navigator (if you can't see it press  ⌘1 to show the Navigator view or ⇧⌘Space to search the project for Main.storyboard).  The Editor view in the center of the project, will display a white background or it might show some windows.  To see everything currently built into the UI, click on the Document Outline button at the bottom left of the Editor view.


This shows you a tree hierarchy of the objects being used to make the UI.  Click on View Controller Scene in the Document Outline.  You'll see two objects drop down in the list:  View Controller and First Responder.  Click on the toggle arrow next to View Controller.  This View Controller is used to control the actions and initiation of view updates according to user interaction.  The View Controller controls a view--the one you see drop down when you click on the toggle arrow.  This is the content view two which we will add our customized OpenGL View.

Open the Utilities Panel by clicking on the right most button at the top of the Xcode window (as shown below), or press ⌥⌘0 on the keyboard.


In the Utilities Panel, locate the Object Library in the bottom portion of the panel.  Find the NSOpenGLView Object by scrolling through the objects, or by searching for it in the search field at the bottom of the Object Library.


Drag and drop this into the are that says View Controller, or drop it directly into the View in the Document Outline.  Dropping directly in the Document Outline objects allows for more specific control of where you object is going.  As you develop your UI, this fine control becomes more important so you are not adding a child view to the wrong parent view.

Drop into the View Controller in the Editor

Drop into the View directly with the Document Outline

We want our view to fill the window and to resize with the window when we click on the window edges.  If you want to see what I mean, leave out this step until after we have finished our custom view subclass.  You'll see that there will be a black box that does not adjust with window resizing.  Adding constraints makes the view stay stuck to the boundaries you specify.  We want the view to fill the window, for now, so we will specify 0 in the top, bottom, left and right fields.  I suggest clicking in the top field and entering 0 then pressing Tab to go to the next field and simultaneous activating the constraint.  By activating, I mean clicking on the red bar that is between the input field and a white box such that it becomes a bold red bar.  When you use tab to go from field to field, these bars will automatically activate if you have placed in a new value.  Lately, click on the the drop down next to Update Frames and select Items of New Constraints to make the frame of the view stretch to accommodate the size of the window.  An alternate method is to just click Apply Constraints at the bottom without this step and then make sure the view you added is selected before clicking on the Resolve Autolayout Issues button next to the Pin button.  Then click on Update Frames under the Selected Views heading.  Either way, you'll see the view stretch out to the size of the parent view's frame.


Now we're going to do a little coding.  The NSOpenGLView that we put into our interface is an instance of NSOpenGLView.  There are a few customization options available in the Utilities Panel under the Attributes Inspector, but they don't offer the customization that we want for this project.

The Attributes Inspector Tab in the Utilities Panel
Instead, we are going to make this view a custom subclass of NSOpenGLView.  Create a new file by clicking File>New File at the top left of the screen, or ⌘N on the keyboard.  Under OS X, select Cocoa Class and click Next.


On the next screen, type SwiftOpenGLView into the Class field, set the Subclass of to NSOpenGLView, and set the Language to Swift.  Click Next.


Choose a location to save the file.  Make sure you are in the SwiftOpenGL project folder.  We're going to be adding a number of new Schemes to the project as we go on (and we'll talk about what those are later), so make sure the SwiftOpenGL folder is selected and then create a new folder by clicking on the New Folder button in the bottom left of the menu screen.  Name this folder Beginnings (I called it Begin in my project as an accident that I never fixed).  You can name it what you like, really, but it'll help you know what I am talking about if you use the same names for now.  Next, make sure the App target is selected (We're going to be deleting the Test Target as we don't need it right now).  When you're done, click Create.


You might have noticed I have some additional targets, files, and a Group called Begin shown here.  This is a project I have been working on for a little while so it has some stuff that we will be building together.  Think of it as a sneak peak.

SwiftOpenGLView.swift

You should see the new file opened in the Editor view, but if you don't, you may select it in the Project Navigator, or by searching for it with ⇧⌘O.  I have added some extra annotation to the class to make it more self explanatory.

//
//  SwiftOpenGLView.swift
//  SwiftOpenGL
//
//  This is the first iteration of the SwiftOpenGLView class.  It starts a CVDisplayLink
//  and draws a black background.
//


import Cocoa
import OpenGL.GL3


//  To optimize the binary created for this class, we'll set the class to final.  This tells the
//  compiler methods declared in this file are owned by the file and you don't have to worry about
//  the potential of a subclass's implementation.  In other words, this eliminates some behind the
//  scenes stack calls which speeds things up (this is per WWDC 15, so it is probably something 
//  realized in Swift 2 and not Swift 1.2).  It's good practice to mark class you do not intend to
//  subclass as final, and methods you do not accessible to other classes, structs, and enums as
//  private.
final class SwiftOpenGLView: NSOpenGLView {
    
    required init?(coder: NSCoder) {
        //  Allow the super class to initialize it's properties (phase 1 initialization)
        super.init(coder: coder)

        //  Some OpenGL setup
        //  NSOpenGLPixelFormatAttribute is a typealias for UInt32 in Swift, but the individual
        //  attributes are Int's.  We have initialize them as Int32's to fit them into an array
        //  of NSOpenGLPixelFormatAttributes
        
        let attrs: [NSOpenGLPixelFormatAttribute] = [
            UInt32(NSOpenGLPFAAccelerated),            //  Use accelerated renderers
            UInt32(NSOpenGLPFAColorSize), UInt32(32),  //  Use 32-bit color
            UInt32(NSOpenGLPFAOpenGLProfile),          //  Use version's >= 3.2 core
            UInt32( NSOpenGLProfileVersion3_2Core),
            UInt32(0)                                  //  C API's expect to end with 0
        ]

        //  Create a pixel format using our attributes
        guard let pixelFormat = NSOpenGLPixelFormat(attributes: attrs) else {
            Swift.print("pixelFormat could not be constructed")
            return
        }
        self.pixelFormat = pixelFormat

        //  Create a context with our pixel format (we have no other context, so nil)
        guard let context = NSOpenGLContext(format: pixelFormat, shareContext: nilelse {
            Swift.print("context could not be constructed")
            return
        }
        self.openGLContext = context
    }
    
    override func prepareOpenGL() {
        //  Allow the superclass to perform it's tasks
        super.prepareOpenGL()
        
        //  Setup OpenGL
        
        //  The buffer will clear each pixel to black upon starting the creation of a new frame
        glClearColor(0.0, 0.0, 0.0, 1.0)

        /*  other setup here, e.g. gelable() calls */
        
        //  Run a test render
        drawView()
    }
    
    override func drawRect(dirtyRect: NSRect) {
        super.drawRect(dirtyRect)
        
        // Drawing code here.

        //  Call our OpenGL rendering code.
        drawView()
    }
    
    private func drawView() {
        //  Clear out the context before rendering
        //  This uses the clear color we set earlier (0.0, 0.0, 0.0, 1.0 or black)
        //  We're only drawing with colors now, but if we were using a depth or stencil buffer
        //  we would also want to clear those here separated by "logical or" " | "
        glClear(GLbitfield(GL_COLOR_BUFFER_BIT))
        
        /*  Drawing code here  */
        
        //  Tell OpenGL that you're done rendering and it's time to send the context to the screen
        glFlush()
    }
}

When we subclass NSOpenGLView and use storyboards, we are required to implement init?(coder:). This is a perfect place to set up our pixel format and context.  The pixel format describes the information used to calculate the color of a pixel.  This includes color, depth, and stencil buffers (e.g. 2 bits per r, g, b, a or 8 bit color, 4 bits per r, g, b, a or 16 bit color, etc), various optimizations, and very important, the OpenGL version we wish to use.  Currently, NSOpenGLView uses the legacy profile.  However, to work with modern OpenGL we need to use the core profile.  If you have a new enough Mac, you can select version 4.1, but my Mac is not new enough so we shall specify NSOpenGLProfileVersion3_2Core which allows use of versions newer than 3.2 (on my Mac that is version 3.3 core).  We're starting out simply so we have specified that we want to use accelerated renderers, 32-bit color, and versions newer than 3.2 core.  Using these attributes we create a pixel format.  Then we create a context with our pixel format.  We set the NSOpenGLView's pixel format and context attributes to our newly created pixel format and context.

The next method to implement is prepareOpenGL.  This is setup method you may have seen in other OpenGL tutorials.  In this method, we initialize OpenGL state--we'll set the clear color here and submit a test render (thought we do not have to do so).  Later we would also use this opportunity to enable various OpenGL capabilities like alpha.

If you do not decide to do a test render, then you don't need declare the drawView() method.  You can just call glClear() and glFlush() in drawRect(_:_).  Otherwise, this method allows us to attempt a test render and render to the view with calls to drawRect(_:_).

Using a subclass in Interface Builder

To make our NSOpenGLView object in Interface Builder behave like a SwiftOpenGLView, we have to set the Class in the Identity Inspector.  Navigate to main.storyboard, select the NSOpenGLView object in the Document Outline, and in the Utilities Panel, click on the Identity Inspector tab.


When you set the class to SwiftOpenGLView, you are telling Interface Builder that the OpenGL view object is actually our SwiftOpenGL view object.  This means that instead of calling the init?(coder:) method in NSOpenGLView, it calls the init?(coder:) in our SwiftOpenGLView subclass.

The difference a view makes

As a last touch, click on the window controller object in the editor view.  Look at the Attribute Inspector in the Utility Panel for the Title field under Window.  Change this title to SwiftOpenGL.  Now click on Run and you should see your app, but this time with a black view.  Success!


When you're ready, click here.

You can find the project target, Beginning, on GitHub.

Comments

  1. Thanks! First place in the Internet with clear instructions!

    ReplyDelete
  2. Thank you! Clarity is certainly one the main goals of these tutorials. If ever this is something that you feel requires further clarification, don't be afraid to let me know.

    ReplyDelete

Post a Comment

Popular posts from this blog

4.0.0: MVC OpenGL Part 1

Using Swift with OpenGL

0.0: Swift OpenGL Setup