1.1 A Point in the Right Direction

Drawing a Point On Screen

Coding a Shader

Shader's (shader programs) are collections of complied code that run on the GPU.  They accept and process data which is used to calculate the individual pixel data (red, green, and blue values) for displaying/printing to the screen, memory, printer or other device.  A shader program is made of vertex, fragment, geometry, and you're mac is new enough to code with OpenGL 4.1, tessellation shaders.  I find it unfortunate that OpenGL chose to call these individual shader components shaders. I find it makes things confusing, but that's the naming convention.

The simplest shader you can make requires a program and a vertex shader.  This will calculate the position of vertices in space; however, to draw something to the screen we also have to have a fragment shader.  OpenGL objects are passed around in code using "reserved names" as it says in the Red Book (online resources call them object ID's and handle's).  We won't need to keep track of the vertex or fragment shader ID's for now, but we do need the program's ID.  Add a private var to the top of the SwiftOpenGLView class.

final class SwiftOpenGLView: NSOpenGLView {
  
    //  The handle to our shader
    private var programID: GLuint = 0
    //  Essentially a handle to a model
    private var vaoID: GLuint = 0

    ...

}

OpenGL object ID's are always GLuint's.  This is just a typedef in the OpenGL API for an unsigned int, but using GLuint instead of UInt theoretically allows the implementation to be cross platform compatible by ensuring that a UInt is the same number of bytes no matter which system you're running on.

We want our shader to start drawing the moment it's opened.  So we need to create a shader toward the beginning of app initialization.  In Swift, initialization of a class has two phases:  phase one initializes variables and constants and phase two performs additional setup operations.  You cannot call any functions or methods that utilize "self" until phase two of initialization.  Unfortunately, you also cannot use OpenGL functions until phase two either.  This creates a problem if we want to keep our program handle as a constant.  If we were to create a program handle variable after initialization of the class, struct or enum, this wouldn't be a problem.  Since we are going to need this handle before initialization is finished, we have to make it a var.

So what are we using this handle for?  Why don't we need handles for the vertex and and fragment shader?  Well, we need to use out program handle in more than one method, so we have to store it in a local variable.  However, for right now, we do not need our vertex and fragment shaders for more than one method so we don't need to store their handles.  You can make shaders by using different vertex, fragment shaders, etc.  So say you create vertex shader Va and Vb, and fragment shader Fa and Fb, you can make 4 different shaders (Va + Fa, Va + Fb, Vb + Fa, Vb + Fb).  Let's say all four of these shaders were different materials in your program and you wanted them to be available, but not created initially all at once.  You might create shader Va + Fa to start, but then later you need shader Va + Fb.  You may not want to have to recreate Va, so can hold onto Va's handle to access it when you create shader Va + Fb.

The other handle we need is a VAO (Vertex Array Object).  A VAO is used to hold pointers to the inputs of a shader and associates them with VBO's (Vertex Buffer Object).  We'll talk more about this pretty soon, but for right now, just know that you have to have a VAO to draw (even if it doesn't have any pointers in it).

This all probably seems like nonsense right now.  Let's make a shader and demystify these concepts.  Our handle is a local variable.  Local variables require initialization at class initialization.  We want the shader created close to initialization, but it doesn't have to occur during that process.  This is where that prepareOpenGL( ) method is useful once again.  Again, I've notated the code, so I'm going to let it talk for itself.

override func prepareOpenGL() {
    
    super.prepareOpenGL()
    
    //  Setup OpenGL
    
    glClearColor(0.0, 0.0, 0.0, 1.0)
    
    //  Create a shader program and set programID to this new program's handle
    programID = glCreateProgram()
    
    //  Required by OpenGL to draw, we'll use it later to hold the input locations 
    //  (the indexes) for our shader (i.e. vertex position, color, texture position, 
    //  etc.)  This will be more clear later.
    glGenVertexArrays(1, &vaoID)
    
    //  Here we see an example of converting a variable into a pointer to a pointer
    //  This function (from sbennett912 on GitHub) takes an UnsafePointer of a certain 
    //  type and returns an UnsafePointer of the same type to the compiler as an
    //  UnsafePointer<UnsafePointer<type>>
    //  As an alternative, initialize the variable in place as an UnsafePointer<type>
    //  i.e. UnsafePointer<CChar>(variable). Doing so does not create a memory leak because
    //  we have not used alloc, malloc, or calloc.
    func getPointer <T> (pointer: UnsafePointer<T>)->UnsafePointer<T> { return pointer }
    
    //  Create a vertex shader, the argument is an Enum that lets OpenGL know what type of
    //  shader to create
    let vs = glCreateShader(GLenum(GL_VERTEX_SHADER))

    //  Shader's a code compiled on the GPU, they consist of a main function and potentially
    //  a number of subroutines. You can't code for the GPU the same way you code for the CPU
    //  meaning that Xcode doesn't do the compiling, the GPU itself does. It does so be
    //  accepting shader code as a string.
    //  Every shader has to start the the GLSL (Graphic Language Shading Language) version
    //  The Shader must contain a void main() {} function, even it is empty.
    //  gl_Position is a predefined variable that outputs the location of a calculated point
    //  in space.
    var source = "#version 330 core                             \n" +
                 "void main()                                   \n" +
                 "{                                             \n" +
                 "    gl_Position = vec4(0.0, 0.0, 0.0, 1.0);   \n" +
                 "}                                             \n"

    //  OpenGL is a C based language so we have to take our Swift string, and make it into a
    //  an ASCII with the String Encoding method
    if let vss = source.cStringUsingEncoding(NSASCIIStringEncoding) {
        //  Here we cast instead of using the getPointer() function
        var vssptr = UnsafePointer<GLchar>(vss)
        //  This takes our string an puts it into the GPU's shader source code
        glShaderSource(vs, 1, &vssptr, nil)
        //  This function compiles the code in the shader
        glCompileShader(vs)
        //  Now we check to make sure no errors occurred during compilation
        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 {
                //  In order to print the log to the screen, we have to retrieve it first.
                //  We do this by reserving space in memory equal to the number of characters
                //  the log contains.
                let cLog = UnsafeMutablePointer<CChar>(malloc(Int(logLength)))
                //  Now we put the log into that reserved memory so we can print it
                glGetShaderInfoLog(vs, GLsizei(logLength), &logLength, cLog)
                if let log = String(CString: cLog, encoding: NSASCIIStringEncoding) {
                    Swift.print("log = \(log)")
                    //  We used malloc for this UnsafeMutablePointer so we are responsible
                    //  for freeing the memory.
                    free(cLog)
                }
            }
        }
    }
    
    //  We repeat the process for the fragment shader, but this time we ask for a fragment
    //  shader. There is no predefined output for fragment shaders.  Here we make our own
    //  called color.  We are hard coding the color white into the output.  Otherwise, it
    //  the same process.  You'll also see that we use the same process to check for errors.
    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) {
        //  This time we are using the getPointer(_:) function (future tutorials will just
        //    create a new UnsafePointer.
        var fssptr = getPointer(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)
                }
            }
        }
    }
    
    //  Attaching shaders to a shader program marks them as a unit, each shader is attached
    //  individually
    glAttachShader(programID, vs)
    glAttachShader(programID, fs)
    //  Linking creates the connections between the shaders so that in's match with out's
    //  from stage to stage of the shading process
    glLinkProgram(programID)
    //  Linking may also result in errors, so we check using the same process as with shaders
    //  however, we use glGetProgram*() functions
    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)
        }
    }
    
    //  These shaders are currently being used by the Shader Program, so they are just
    //  flagged for deletion.  They shall be automatically detached and deleted when 
    //  when glDeleteShader() is called on the associated shader program.
    glDeleteShader(vs)
    glDeleteShader(fs)
    
    //  Run a test render
    
    drawView()
    

}

Now we have a Shader, and we have a VAO.  We'll talk about the VAO later, but know that you can't draw without it in the OpenGL core profile.  Now we're going to set up the draw functions.  You may have noticed the drawView( ) method at the end of prepareOpenGL( ).  This method allows us to call our OpenGL drawing code outside of drawRect(dirtyRect:); thus, we can run a test render.  Theoretically, this allows us to take an opportunity to work out problems if some arise, or to provide a method to a callback for animation (a topic we'll be covering later).  Drawing requires three steps, calculate and set any uniform information (information that is the same for every vertex you are about the draw), select the program and the model (the VAO represents a particular "view" of a model) you want to draw with, and finally call draw.  When you call draw, you sent the data to the shader which leads the calculation of pixel color data for an images that X bytes wide by X bytes high.  These bytes of data are written into the part of the view's context that hold the individual colors for each pixel of the view when you call glFlush( ).  Take a look at the code below.

override func drawRect(dirtyRect: NSRect) {
    super.drawRect(dirtyRect)
    
    // Drawing code here.
    
    drawView()
    
}

private func drawView() {
    
    //  Make sure the bytes in the context are cleared to a specified uniform color
    //  They will later be overwritten as needed by successive calls to glDraw*()
    //  glClear() uses bitfields that are combined with '|' to clear color, depth, and stencil
    glClear(GLbitfield(GL_COLOR_BUFFER_BIT))
    
    // ==================
    //  Perform pre draw operations like matrix math and Uniform setting here
    //  More on this later
    // ==================

    //  Choose the shader to draw with
    glUseProgram(programID)
    //  Choose the model to draw with
    glBindVertexArray(vaoID)
    
    //  Make the vertex nice and big so we know it is there (40 pixels big)
    glPointSize(40)
    //  draw arrays is the simplest of the draw commands, we tell OpenGL the type of 
    //  primitive we are using with is points (other types include triangles, lines
    //  and variations on these two)
    //  We're only drawing one vertex and it is the first in the "array", so index 0
    //  and count 1, respectively
    glDrawArrays(GLenum(GL_POINTS), 0, 1)
    
    //  It's always good practice to unbind the active VAO so no unwanted changes are made
    //  to it that you weren't expecting.  This goes back to OpenGL being a state machine
    //  If you leave the VAO active, any calls you make that may have an effect on it's state
    //  such as changes to a VBO, affect the VAO.  This may result in very unexpected results

    glBindVertexArray(0)
    
    //  Send the fragments to to the context and the context to the screen
    glFlush()

}

The last thing we have to do is take care of some GPU memory management.  We have already marked the vertex and fragment shaders for deletion, but the shader program and VAO are still out there.  Use the class deinit method to do some last minute cleanup before the class is completely deallocated.

deinit {

    //  The ampersand is used because the function asks for an UnsafePointer<GLuint>
    glDeleteVertexArrays(1, &vaoID)
    glDeleteProgram(programID)

}

Alright, the real test.  Click on Run or ⌘R to see our beautiful creation:  the white square on black background.  Very minimalist I think, good composition, sure to be a show stopper... I promise, we'll draw more interesting stuff soon enough.


Next time we'll be doing some refactoring to simplify our class and make it more understandable.  For now, enjoy you're creation.  Take some time to experiment.

Don't forget to comment!  Help me make these tutorials better!  When you ready, click here to move on to the next tutorial.


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

Comments

Popular posts from this blog

4.0.0: MVC OpenGL Part 1

Using Swift with OpenGL

0.0: Swift OpenGL Setup