2.0: VBO's and VAO's

Now that we have a working renderer, we can go back and revaluate some of the concepts we touched upon in greater detail.  The first is going to be the vertex buffer object (VBO).  OpenGL objects are reserved spaces of memory that contain user specified data such as vertex locations, pointers, or inaccessible data that you can only refer to by handles (like shaders).

VBO's may be used for raw data associated with vertices--x, y, z, w positions; r, g, b, a colors; s, t, r, q texture coordinates; and vertex indices.  Our first program used a hard coded position so we didn't need a VBO; however, this method is only useful for drawing one vertex.  To draw multiple vertices, we will load the VBO with the vertex data, and use vertex array object (VAO) to tell the shader where it can find the data it needs for drawing.
A VAO then, is an OpenGL object that stores pointers to to each input attribute in a vertex shader.  There are two major ways to create pointers:  point to the first element of a VBO, or point to an element within a VBO.  What does that mean?


The x value of a vertex when learning OpenGL is commonly defined as a float:  4 bytes (32 bits) in Swift.  OpenGL is a low level API so you'll be working a lot with the concept of type sizes.  With that in mind, let's create a hypothetical vertex that has a location in 3D space and a color.  That means each vertex has an x, y, z, r, g, b, and a value.  The x, y, and z values are stored as Swift Float's and the r, g, b, and a values are stored as Int8's (one byte).  We'll use Int8 because this gives us 256 different values--otherwise known as 256 bit color.  This is the commonly used color size used in OpenGL tutorials.  Each vertex then, is 3values(4bytes) + 4values(1byte) = 16 bytes.  Three vertices will occupy 48 bytes of space.


Let's assume for the moment that we have put our data into data into a VBO.  How do we use a VAO to access the data?  The vertex shader will be run once per vertex, but you must specify how many bytes make up a vertex.  Or in other words, we have to tell OpenGL how many bytes to jump ahead to reach the next vertex--our vertex is 48 bytes.  In OpenGL this is the "stride".  We also have to tell OpenGL how many bytes from the start of a vertex's location in the buffer until the desired input attribute is reached.  Our vertex has two attributes:  location and color.  Location is at the beginning of each vertex's data--it has an offset of 0 from the beginning of that vertex's address.  Color is 12 bytes from the beginning of a vertex's address--it has an offset of 12.  With this information, OpenGL will automatically look up the next vertex and apply the appropriate information to each input every time the vertex shader runs.

The last thing we have to do is add the appropriate attributes to our shader code.  We're providing location and color in our VBO, so we add two "in" attributes: location and color.

We'll use this bit of theory and apply it to an example that draws one triangle to the screen.  Below is the code to set up a VBO, VAO, and shaders.  The code is annotated for clarity.  Remember to create a new target.  This time, call it FirstTriangle.  It'll be a little different this time though.  We don't need to create an entirely separate target, we can use a duplicate.  Click on SwiftOpenGL in the Project Navigator.  Then select the FirstVertex target.  Right click on it and click duplicate.


You'll get a FirstVertex Copy target at the bottom of the list.  If you want to move it, just click and drag it up and down in the list.  Like before, select the FirstVertex Copy target, and press return on the keyboard.  Rename the target FirstTriangle.  Click on Build Settings and type "First" into the search field.  Change the name of the info.plist and product name like before.  Now, in the project Navigator, find the FirstVertex Copy-Info.plist and change it's name to FirstTriangle-Info.plist.  Place it into a new group folder called FirstTriangle by right clicking on the file and selection New Group from Selection.  Optionally, click on the plist again and create another group folder called Supporting Files.  Move the group up so it is after the FirstVertex group.  In Finder, navigate to the info.plist.  Create a folder called FirstTriangle, and most the new plist into it (make sure it's name is FirstTriangle-Info.plist before you closed the folder).  Back in Xcode, select the info.plist again, and set it's location from the Utilities panel.


Click on SwiftOpenGL one last time in the Project Navigator and select the FirstTriangle target.  Click on the General tab.  If you see a button that says Choose Info.plist File..., click on it and select the appropriate plist in the page down menu.



Click choose when you're done.  Click on Run to test the app.  You'll see the white square vertex we made last time.  Back in Xcode, stop the program ("⌘"+ ".").  Now select the SwiftOpenGLView in FirstVertex.  In the Utilities panel, unselect FirstTriangle so the targets appear as below.


Create a new file (a Cocoa Class file).  Name it SwiftOpenGLView and make it a subclass of NSOpenGLView as before.  Save it to the FirstTriangle file.  Make sure the group is set to FirstTriangle, the targets are set to FirstTriangle alone, and the destination is set to the FirstTriangle file.  Click Create.



Make sure you really understand the code before moving on to the next post.  We'll start by passing in three vertex locations and then next post we'll add colors.

Note that ➥ indicates a continuation of the same line of code (the blog with isn't wide enough at times).

final class SwiftOpenGLView: NSOpenGLView {
    
    private var programID: GLuint = 0
    private var vaoID: GLuint = 0
    private var vboID: GLuint = 0    //  The VBO handle
    
    required init?(coder: NSCoder) {
        //  /////////////////////////////////////  //
        //  init code here, see previous tutorial  //
        //  /////////////////////////////////////  //
    }
    
    override func prepareOpenGL() {
        
        super.prepareOpenGL()
        
        //  Setup OpenGL
        
        glClearColor(0.0, 0.0, 0.0, 1.0)
        
        programID = glCreateProgram()

        //  A triangle is one of three primitives used for drawing in OpenGL:
        //      points, lines, triangles
        //  To draw a solid triangle to the screen, we'll need three points.  OpenGL defines
        //  drawing space as being Unit length (Normalized) in positive and negative directions.
        //  The top of the view is y 1.0, bottom -1.0, left -1.0, right 1.0, etc.
        //  This is different from the way other Mac API's define screen space:
        //      the origin x 0.0, y 0.0, is the middle of the view in OpenGL and the bottom left 
        //      of the view in Mac.
        //  We'll define a triangle that fits these coordinates in an array that we can send to
        //  a VBO.  We don't need to access it later, so we'll define it within the method such
        //  that upon method completion, the memory will be released.
        //  We are drawing a two dimensional object, so for right now, we only need to define an
        //  x and y coordinate.  The z and w coordinates will be added in the shader (see below).


        let data: [GLfloat] = [-1.0, -1.0, 0.0, 1.0, 1.0, -1.0//  Three vertices

                   //  The ampersand is used when passing a variable of type <Type> (i.e. a: <Type>)
        //  to C API's that are expecting a pointer but is provided a Swift<Type>.  You may 
        //  drop the ampersand if you define the variable as the type that is expected by the
        //  method.
        //      let a: UnsafeMutablePointer<Type>
        //  We choose to define our variables as Swift types because we'll be using this form
        //  more often than the unsafe counterpart.  It also makes the code look nicer.

        glGenBuffers(1, &vboID)                             //  Allocate a buffer with the handle
        glBindBuffer(GLenum(GL_ARRAY_BUFFER), vboID)    //  Initialize the buffer
        
        //  The next line fills the VBO with the data in our array "data".  The first argument is 
        //  the type of VBO--GL_ARRAY_BUFFER is used when we are drawing vertices one after
        //  another in an array.  The alternative is GL_ELEMENT_ARRAY_BUFFER which feeds the
        //  shader by indices--more on this later.  The second argument tells OpenGL how many
                    //  bytes are required (each element in data is a float and there are data.count number of
        //  elements or 6 elements(4 bytes) = 24 bytes. The third argument is supposed to be
                   //  a pointer to the data.  Swift does not directly expose pointers, but it does allow you
        //  to pass variable names to UnsafePointer<Type> parameters.  The fourth argument tells
                   //  tells OpenGL how to optimize memory for drawing.  GL_STATIC_DRAW states that the data
        //  will mostly read from and won't be changed often (it's just a hint to the GPU).
        glBufferData(GLenum(GL_ARRAY_BUFFER), data.count * sizeof(GLfloat), data,
            GLenum(GL_STATIC_DRAW))

        glGenVertexArrays(1, &vaoID)
        //  We have to bind the VAO before we can add a pointer to an attribute
        glBindVertexArray(vaoID)

        //  Vertex attribute pointers set up connections between VBO data and shader input
        //  attributes.  The first parameter is the index location of the attribute in the shader.
        //  We'll look more at this later, but the first defined attribute is 0, the second is 1,
        //  etc.  The second parameter tells OpenGL how many "pieces" of data are being supplied--
        //  we're passing a location with an x and why coordinate, or 2 values.  The third 
        //  parameter tells OpenGL what type the data is so it knows how many bytes are needed.  
        //  The fourth parameter tells OpenGL if the data needs to be normalized (converted to a 
        //  value between -1.0 and 1.0).  We are already doing so in our array so we pass in 
        //  false.  The fifth parameter is the stride and tells OpenGL how large a vertex is in
        //  bytes.  If you pass 0, OpenGL assumes the vertex size is the same as the number of 
        //  pieces of data times the data type (here that is 2 * 4bytes = 8).  The sixth parameter 
        //  tells OpenGL how many bytes from the vertex's address must be skipped over to reach 
        //  the data.  0 tells OpenGL the data is at the start of the vertex's address.
        glVertexAttribPointer(0, 2, GLenum(GL_FLOAT), GLboolean(GL_FALSE), 0
            UnsafePointer<GLuint>(bitPattern: 0))
        glEnableVertexAttribArray(0)    //  Enable the attribute (otherwise it won't accept data)

        glBindVertexArray(0)    //  Ensure no further changes are made to the VAO.

        //  Now we'll add input attributes to the shader.  Inputs are marked as in, uniform, or
        //  Sample2D, Sample3D, etc.  They are defined after the shader version, but before the
        //  main function.  Our in attribute is a 2 point vector (vec2).  In the main function
        //  we set the predefined variable gl_Position.  Remember that it expects a vec4.  We make
        //  a new vec4 by using our vec2 for the first two arguments and passing in 0.0 for the z,
                   //  and 1.0 for w arguments.
        let vs = glCreateShader(GLenum(GL_VERTEX_SHADER))
        var source = "#version 330 core                             \n" +
                     "in vec2 position;                             \n" +
                     "void main()                                   \n" +
                     "{                                             \n" +
                     "    gl_Position = vec4(position, 0.0, 1.0);   \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, 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" +
                 "out vec4 color;                       \n" +
                 "void main()                           \n" +
                 "{                                     \n" +
                 "    color = vec4(1.0, 1.0, 1.0, 1.0); \n" +
                 "}                                     \n"
        if let fss = source.cStringUsingEncoding(NSASCIIStringEncoding) {
            //  Note that we are going to use cast our pointer both times without the use of
            //  the pass-through function getPointer(_:)
            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, 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)
        
        drawView()
        
    }
    
    override func drawRect(dirtyRect: NSRect) {
        super.drawRect(dirtyRect)
        
        // Drawing code here.
        
        drawView()
        
    }
    
    private func drawView() {
        
        glClear(GLbitfield(GL_COLOR_BUFFER_BIT))
        
        glUseProgram(programID)
        glBindVertexArray(vaoID)     //  VBO's are used indirectly through VAO's
        
        //  To draw a solid triangle, we pass GL_TRIANGLES as the first argument and 3 as the last
        glDrawArrays(GLenum(GL_TRIANGLES), 0, 3)
        
        glBindVertexArray(0)
        
        glFlush()
    }
    
    deinit {
        glDeleteVertexArrays(1, &vaoID)
        glDeleteBuffers(1, &vboID)        //  All objects must be deleted manually
        glDeleteProgram(programID)
    }

}

With that, run your app to make sure everything works.


Please, don't forget to comment below and help me make these tutorials better!  When you're ready, click here to add an attribute for color input.

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

Comments

  1. Great set of articles, do you have a github repository so we can check against the real thing?

    I'm getting an issue use of unersolved identifier getPointer and I'd like to look at working source code to see why.

    ReplyDelete
    Replies
    1. Thank you! I have not yet put the project on GitHub, but I shall. The "unresolved identifier warning that you are getting means that you are using the function getPointer(_:) without first defining it. In the tutorial prior to this one, I explained that there were two ways to transform our data into the type the OpenGL function is expecting. The first being to construct the expected type in situ (i.e. let fssptr = UnsafePointer(fss)). The second is to use a method to wrap this process. It takes an extra line of code, but the style or appearance may be more appealing to some users. The end result if the same. The problem is during this tutorial, I took out the definition of the getPointer(_:) nested function declaration, but did not change the fssptr declaration accordingly in the blog post (i.e. fssptr was written as let fssptr = getPointer(fss) // Error: getPointer(_:) is not defined).

      I have corrected the blog post so that this error is no longer present. Thank you so much for pointing that out!!

      Delete

Post a Comment

Popular posts from this blog

4.0.0: MVC OpenGL Part 1

Using Swift with OpenGL

0.0: Swift OpenGL Setup