2.1: Custom Color

Last time, we talked about defining a vertex with a position and color, but we only created three vertices using a simple array of location data.  This time we'll create a struct that has position and color and make an array with three of those structs.  We'll send this to the VBO and then create two vertex attribute pointers--this time utilizing the offset parameter in glVertexAttribPointer( ) to specify the location of the color data.

We'll have to update the shader, too.  The vertex shader will need to have a second input for the color, but it will also need an output.  When an output from the vertex shader and the input of fragment shader have the same name, they automatically connect to one another.  Why would we want to do that though?  It is the fragment shader that adds color to the geometry that we draw; however, data has to be passed through the vertex shader to get to the fragment shader.  Once there, we already have an output for each outgoing fragments.

Before we start, let's create another target--I'll include the steps once more for creating a duplicate.  We'll make a duplicate of FirstTriangle and call it ColoredTriangle.  Click on SwiftOpenGL in the Project Navigator and select FirstTriangle.  Right click and select Duplicate.



Rename FirstTriangle copy to ColorTriangle.  With ColorTriangle selected, click on the Build Settings tab and type "first" into the search field.  Change the plist file name to ColorTriangle-Info.plist and the product name to ColorTriangle.  Then select the plist in the Project Navigator, place it into a new group and rename it ColoredTriangle-Info.plist.


Navigate to the plist's location in the Finder by click on the full path icon in the File Inspector.


Place the ColorTriangle plist into a new folder called ColoredTriangle.  Back in Xcode, tell Xcode where to find the file.


Select the SwiftOpenGL project in the Project Navigator, then select the ColoredTriangle target.  Click on the General tab and then the Choose Info.plist file... button.


Select the ColorTriangle-Info.plist from the page down menu and click Choose.


Now manage the schemes.


Change the name of the FirstTriangle copy scheme to ColoredTriangle and click close.


Finally, create a new Cocoa Class file that is a subclass of NSOpenGLView and save it to the ColoredTriangle file.




Select the previous version of SwiftOpenGLView and in the File Inspector, change the target membership to be just FirstTriangle.  Then back in the new SwiftOpenGLView, transcribe the code from the old SwiftOpenGLView.


When you're done, run a test render to verify everything is working.  If you get the white triangle and no errors, you're ready to move on.  This is the last time we'll talk about creating new targets.

Now let's modify the SwiftOpenGLView code so that we can change the color from outside the shader.  Again, I'll post the class code with annotation for clarity, but I've placed comments where some of the functions were so as to save some space and help emphasize what has really changed the code.

final class SwiftOpenGLView: NSOpenGLView {

    //  //////  //
    //  init()  //
    //  //////  //
    
    override func prepareOpenGL() {
        
        super.prepareOpenGL()
        
        glClearColor(0.0, 0.0, 0.0, 1.0)
        
        programID = glCreateProgram()
        
        //  We'll add color information to the position information such that the position is 
        //  listed first with two components (x and y) and the color is listed second with 
        //  three components (r, g, and b).  Note that you can add white space (spaces and
        //  line breaks to the array for clarity.
        
        let data: [GLfloat] = [-1.0, -1.0,  1.0, 0.0, 0.0,
                                0.0,  1.0,  0.0, 1.0, 0.0,
                                1.0, -1.0,  0.00.0, 1.0]
        
        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)
        
        //  The first pointer is to the position data -- note the last two arguments:  stride 
        //  and offset.
        //  Stride - number of bytes from the start of one vertex to the start of the next
        //      In other words, each vertex is made of 5 elements from the array (x, y, r, g, 
        //      and b).  Each element is a GLfloat with is 4 bytes of data; therefore, 
        //      5(4) = 20 bytes
        //  Offset - number of bytes from the vertex's start address that must be passed by
        //  before reaching the appropriate data
        //      The position data starts at the start address; therefore, offset = 0
        //      The color data starts 2 floats (2(4) = 8 bytes) from the vertex address; 
        //      therefore, offset = 8
        glVertexAttribPointer(0, 2, GLenum(GL_FLOAT), GLboolean(GL_FALSE), 20
          UnsafePointer<GLuint>(bitPattern: 0))
        glEnableVertexAttribArray(0)
        //  The second pointer is to the color data.
        glVertexAttribPointer(1, 3, GLenum(GL_FLOAT), GLboolean(GL_FALSE), 20
          UnsafePointer<GLuint>(bitPattern: 8))
        glEnableVertexAttribArray(1)
        
        //  Unbind the VAO so no further changes are made.
        glBindVertexArray(0)

        //  Adjust the shader attributes.  There are two ways to do this, but the simplest is 
        //  the use the layout keyword.  This allows us to use simple indicies when defining 
        //  our vertex attributes as you saw above (i.e. 0 and 1 for position and color, 
        //  respectively).  The alternative is to use glGetAttribLocation().  It's certainly 
        //  more declarative to use this method so we have a named variable instead of a "magic 
        //  number" like 0 or 1, but it's a more common convention to just use the indices.  
        //  The other caveat is that the function assumes the shader has already been 
        //  compiled--in our case it has not.  layout allows us to specifically tell OpenGL 
        //  what index we want for a particular attribute.
        //      layout (location = 0) means the attribute declared thereafter will be located 
        //      at position 0.  It is a little weird that you are forced to use the layout 
        //      keyword to force the convention considering that if you use the functionand 
        //      then print() the result, you'll find that 0 and 1 are assigned as we have them 
        //      below.  If you don't use the layout keyword though, you'll get unexpected 
        //      results (perhaps a black screen, at the worst).  Don't fight the system, just
        //      use layout (location = x)!
        //
        //  Notice the addition of two attributes:  color and passColor.  Color is what is 
        //  passed in from the VBO while passColor is the color that is given to the fragment 
        //  shader.  Note that they are both vec3's and not vec2's because they have three 
        //  values, r, g, and b.
        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" +
            "out vec3 passColor;                           \n" +
            "void main()                                   \n" +
            "{                                             \n" +
            "    gl_Position = vec4(position, 0.0, 1.0);   \n" +
            "    passColor = color;                        \n" +
            "}                                             \n"
        //  /////////////////////////  //
        //  Check compilation success  //
        //  /////////////////////////  //
        
        //  It is really important to name the in attribute the same as the out attribute from
        //  the vertex shader--otherwise the connection won't be between them.  The name of the
        //  out attribute does not matter.  You just have to have an out and it has to be a 
        //  vec4:  we have to account for the alpha component of a color.  We can use the same 
        //  syntax for creating the vec4 color as we did the position in the vertex shader 
        //  (i.e. passing passColor for the frist three vertices and 1.0 for the fourth to make 
        //  a complete vec4
        let fs = glCreateShader(GLenum(GL_FRAGMENT_SHADER))
        source = "#version 330 core                 \n" +
            "in vec3 passColor;                     \n" +
            "out vec4 outColor;                     \n" +
            "void main()                            \n" +
            "{                                      \n" +
            "    outColor = vec4(passColor, 1.0);   \n" +
            "}                                      \n"

        //  /////////////////////////  //
        //  Check compilation success  //
        //  /////////////////////////  //
        
        //  /////////////////////  //
        //  Check linking success  //
        //  /////////////////////  //
        
        //  ///////////////////////////////////////////  //
        //  Mark shaders for deletion / run test render  //
        //  ///////////////////////////////////////////  //
        
    }
    
    //  //////////////////////////  //
    //  drawRect(), drawView(), deinit  //
    //  //////////////////////////  //
    
}

Now, run the app and enjoy the multicolored triangle!


Next time, we'll take on texturing.  We'll be using Core Graphics (one of Apple's C based API's) to create our bitmaps.  It's fun to work with, but sometimes it get's a little weird in that the documentation can be a bit wanting.  Until then, experiment with your new colored triangle and don't forget to comment!  Help me make these tutorials better by commenting below!  When you're ready, click to to implement textures.

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

Comments

Popular posts from this blog

4.0.0: MVC OpenGL Part 1

Using Swift with OpenGL

0.0: Swift OpenGL Setup