2.4: Adding in Ambient and Specular Components

Now that we have a light that produces shadows, let's add the ambient component back in.  Ambient in the Phong model, is essentially an even level of light that is cast on all points of the model's surface.  We'll add an additional uniform to achieve pass in this value.

glUniform1f(glGetUniformLocation(programID, "lightAmbient"), 0.25)

Then we'll add this value to the light color and multiply it by the intensity to get the effect of the ambient/diffuse light on a fragment.  Within the fragment shader, add the lightAmbient uniform and add this value to the lightColor uniform.  Note that lightAmbient and lightColor are not added together before being multiplied by the intensity.  If they were, this would negate the effect of the ambient light component which gives some level of light to every fragment of the surface.

"uniform float lightAmbient;                            \n"

"vec3 light = lightAmbient + lightColor * intensity;    \n"

Run the program again and you'll notice that the lower corners are now slightly lit.


The final touch to a Phong light is the specular component.  Specular, like diffuse can be thought of as a value between 0.0 and 1.0.  When no light hits the surface, there is no diffuse component, but there is also no specular component.  When a light ray is incident upon a surface and the reflection of that light ray shines directly at the view, then there is full specularity (1.0).  When the reflection is not directly pointing at the viewer, then the value of specularity lies somewhere between 0.0 and 1.0.  Thus the idea of specularity is very much the same as diffuse with one key difference, we're not thinking about the light ray approaching the surface, but the light ray that is leaving the surface and it's direction compared to the direction of the viewer.


We'll add two more uniforms to our light definition:  lightSpecStrength and lightSpecHardness.  The strength uniform adjusts how much specular is drawn while the hardness uniform adjusts to sharp the reflection is.  Really shiny metallic objects are more likely to have maximum strength and very high hardness, but rough objects usually have low strength and very soft specular highlights.

glUniform1f(glGetUniformLocation(programID, "lightSpecStrength"), 1.0)
glUniform1f(glGetUniformLocation(programID, "lightSpecHardness"), 32)

We want to see our specular highlight, so we'll make it's strength 1.0.  A hardness of 32 may seem somewhat arbitrary, but it will make more sense when we look at the calculation.  Before that, we'll add these uniforms to the fragment shader.

"uniform float lightSpecStrength;    \n"
"uniform float lightSpecHardness;    \n"

Now let's look at the calculations.  From the figure above, you can see the incident light ray, the normal, the reflected ray, and the ray cast to the viewer.  We already have the first two.  To the calculate the light cast to the viewer, we subtract the position of the fragment from the position of the viewer.  So, where is the viewer?  The view we have been drawing is a two dimensional representation of a 2x2x2 cube.  We have been drawing the triangle on the z 0.0 plane.  During testing, the best viewer position for this example was 0.0, 0.0, 0.2.  We'll subtract the fragment position from this vector.

"vec3 viewer = normalize(vec3(0.0, 0.0, 0.2) - passPosition);    \n"

Then we'll get the reflected ray.  There is a GLSL function that will calculated the reflected ray for called reflect().  It has two parameters:  incident ray, and normal.

"vec3 reflection = reflect(lightRay, normal);    \n"

Of note, in researching the reflect() function, the common implementation passes in the negative value of the incident vector (-lightRay).  The reasoning behind this is that if incident ray is a vector from the light source to the fragment, we need to change it's direction to from the fragment to the light source so that the reflect ray will point from the fragment to the viewer.  Look at the figure above, all vectors are shown as from the fragment to some location (light source, viewer and so on).  However, let's look at our vector math and see why we are not going to pass in a -lightRay.


In the figure above, the top representation of A and B shows what happens when you have two vectors that point in the same direction.  By negating B, we simply shorten A, but it still points in the same direction.  In the second representation of A and B, we see what we are doing with the light position and fragment position.


A more practice example above shows the viewer at 0.0, 0.2, the light at 1.0, 0.2, and the fragment at 1.0, 0.0.  Below the graph we see a coordinate agnostic representation of the vectors where litP is the vector pointing to the light position and FragP is the negative vector.  LitP-FragP is the difference vector.  Note that it is positive.  In our code then, the vec3 lightRay variable is a positive vector in this instance.  If we pass this in as a negative, the the vector points into the screen and away from the viewer.  If we use this vector, we won't see any specularity.  All that to say that in this example, we are passing in the lightRay directly because it is already pointing away from the surface.

Now that we have these two vectors, we can calculate the specular component.  The dot() function we have seen, but this time, we pass in the viewer and reflection vectors.  The max() function takes two parameters and returns whichever is higher.  The dot() function may return a negative value (vectors that are at an angle 90-180° to one another) so the max() function ensures that we only get values greater than or equal to 0.0.  The pow() function takes two parameters as well.  The first is the value to be increased, and the second is the exponent by which to increase.  This is where the hardness uniform is useful.  We are going to raise the returned value from max() to the power of 32.  Higher numbers, like 256, result in a harder specular edge.

"float specular = pow(max(dot(viewer, reflection), 0.0), lightSpecHardness);    \n"

Then we combine the specular with the rest of the lighting components.  The specular provides a color like the diffuse component which is why we must be multiplied by the lightColor uniform also.

"vec3 light = lightAmbient + lightColor * intensity + 
                 lightSpecStrength * specular * lightColor;    \n"

Run the program and you'll see the specular component of light.


The last thing we'll do before we move on is group the light uniforms together into a struct.  This makes the code more readable since we know which components are attached to our light.  Creating a struct in GLSL is just like creating one in C and Obj-C, except that we are making a struct for GLSL uniforms; therefore, we use the uniform keyword before our struct.  Since the whole struct is a uniform, we don't have to declare each attribute as a uniform individually.

"uniform struct Light {     \n"
"    vec3 color;            \n"
"    vec3 position;         \n"
"    float ambient;         \n"
"    float specStrength;    \n"
"    float specHardness;    \n"

"} light;                   \n"

When we use these attributes, we can now say

"light.color"

Note that we removed the "light" part of each uniform name because that is now the struct name.  When we set these uniforms, we now set them using structName.structComponent.

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)

Be sure to change each of the attribute names to the new naming conventions within the fragment shader main().  Once you're done do a test run to make sure it still works.  The full code is below.

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
    
    
    required init?(coder: NSCoder) {
        super.init(coder: coder)
        
        let attrs: [NSOpenGLPixelFormatAttribute] = [
            UInt32(NSOpenGLPFAAccelerated),
            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
    }
    
    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
                     CGColorRenderingIntent.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" +
            //  The Light uniform Struct allows us to more convenietly access the light attributes
            "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" +
            //  viewer is the vector pointing from the fragment to the viewer
            "     vec3 viewer = normalize(vec3(0.0, 0.0, 0.2) - passPosition);               \n" +
            //  reflect() calculates the reflection vector
            //      first parameter is the incident ray
            //      second parameter is the normal
            //  We do not negate the lightRay because it is already pointing from the surface
            //  to the viewer.  Negating the vector would cause the reflection vector to point
            //  away from the viewer and no highlight would seen.
            "     vec3 reflection = reflect(lightRay, normal);                               \n" +
            //  specular is calculated by taking the dot product of the viewer and reflection 
            //  vectors, ensuring those vectors are >=0.0 with max(), and then raising that value
            //  by the value of hardness to adjust the hardness of the edge of the highlight.
            "     float specular = pow(max(dot(viewer, reflection), 0.0),
                                    light.specHardness);                                   \n" +
            //  The specular component casts light so it must also be multiplied by the
            //  .color component.
            "     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 fragement, 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)
        
        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)
        
        glDrawArrays(GLenum(GL_TRIANGLES), 03)
        
        glBindVertexArray(0)
        
        glFlush()
    }
    
    deinit {
        glDeleteVertexArrays(1, &vaoID)
        glDeleteBuffers(1, &vboID)
        glDeleteProgram(programID)
        glDeleteTextures(1, &tboID)
    }
    

}

There is a lot more to do with lighting.  It's a huge topic, but for now, we'll move on to other topics and come back later.  There are more pressing things to discuss like animation and movement in 3D space.

Next time, we'll delve into animation.  Until then, play around the uniform values and the calculations so get a feel for what part each one plays in the calculation of the specular highlight.

Please comment below with your ideas on how to make these tutorials better.  If something is unclear, redundant, to complex and needs to be simpler... help identify those tutorial glitches.  Thanks for reading!

When you're ready, let's animate!

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

Comments

Popular posts from this blog

4.0.0: MVC OpenGL Part 1

Using Swift with OpenGL

0.0: Swift OpenGL Setup