- We are now going to look at the actual fragment shader.
This fragment shader corresponds
to the mytest3 program.
Your homework 2 will require a slight generalization
in order to extend it to multiple lights.
Let's look at the fragment shader setup again.
Notice that the version is GLSL 330,
that's what version 330 core says.
Notice that the inputs coming into the fragment shader
are the myvertex, which was from the vertex shader,
mynormal, and texture coordinate.
These values will be interpolated from the vertex shader
and provided to each pixel or fragment.
We will output the fragment color,
which is the key value you get out from the fragment shader,
what should be the color of the corresponding pixel.
And we have some uniform variables.
The texture, tex, corresponds to the texture,
istex says are we texturing or not,
islight, are we lighting or not, and uniform vec3, color.
We then have the fragment shader variables.
Notice that in the mytest sequence of programs
we always assume two lights. Light 0 is directional,
light 1 is a point light.
The actual light values are passed
in from the main OpenGL program.
So, we simply have light 0 direction,
light 0 color. The color is in RGBA,
which is why it's a vec4, whereas the direction is a vec3.
Light 1 position in homogenous coordinates is a vec4,
and light1color, which is in RGBA is a vec4.
In your homework 2, you will have to generalize this
and handle general numbers of lights.
We also define the material parameters.
In this case they are uniform for the object,
which is simply the ambient, diffuse,
specular, and the shininess.
Note that ambient diffuse, and specular are RGBA colors,
while shininess is a single value.
Here is the code to compute the lighting
in the fragment shader.
Notice the ComputeLight command.
It takes in the direction for the light source,
the light color, the normal, the half vector,
and the shading parameters for the materials,
which is the diffuse component,
the specular component and the shininess.
Thereafter, it computes the shading.
So in the first part of it is the Lambertian shading,
which is simply nDotL, which is the dot product
between the surface normal and the direction corresponding
to the direction to the light source.
This is is simply N.L and we can see
that if we have a surface and this is the normal,
this is the lighting direction,
then it simply corresponds to the cosine of this angle.
The Lambertian shading is then given
by the diffuse component times the light color,
times max(N.L, 0).
The max command just ensures
that if you are below the surface the value will be 0.
We now talk about the specular component
and here I'm computing N.H.
Again, if I have a surface and this is the normal,
this is the lighting direction,
this is the viewing direction,
which is the viewer or the eye direction,
then I will define the half vector in such a way
that the lighting direction and the viewing direction
have equal angle, and I will be interested
in the angle between the normal
and the half vector direction.
So this is nDotH and then we multiply
by the specular color of the surface, the light color,
and we take max(N.H,0)
and we raise it to the power of the shininess.
So this goes to the power of s,
where s is the value of myshininess.
Finally, the return value from the shader is the sum
of the Lambertian and Phong components and that is written.
Let's look at the transformations
applied in the fragment shader.
The first step is to check whether
texturing is greater than zero.
If texturing is greater than zero,
then the fragment color is simply given by the texture map.
We don't do the lighting calculations.
It would, of course, be possible
to also multiply by the lighting calculations
and modulate it with the texture.
In this case we call the texture function,
with tex being the texture map, and texture coordinate
being the appropriate coordinates for the texture map.
Otherwise, if islight is equal to zero,
then there is no lighting and the fragment color
is simply the value of the color variable,
which was input promoted to an RGBA.
Otherwise, we have to actually
perform the lighting calculation.
Note the OpenGL convention where the eye is always at
the origin (0,0,0), looking down the -Z axis.
Therefore we define the eye position
as the origin (0,0,0) vector.
Now we take my position to the position
of the vertex involved, which is simply
the myvertex xyz divided by w.
This is simply a dehomogenization
of the current vertex to define my position.
We now need to know what is the direction
to the viewer, or the direction to the eye.
This will be given simply by eye position,
where the eye is, minus mypos, where the vertex is.
And we normalize this value in order to get unit vector.
Finally, we need the normal, which is needed for shading,
and that is simply computed by a normalization
of the mynormal, which is passed in from the vertex shader.
So this will be the normalized normal
and the normalized viewing or eye direction.
We now handle light 0 and light 1.
Note that light 0 is a directional light source,
so the direction to the light
is simply given by light 0 direction,
which again we normalize to get a unit vector.
We are now computing the half vector,
which is simply given by the bisector
of the lighting and eye directions,
which can be obtained simply by taking the vector sum
of these directions and normalizing the result.
To get the color corresponding to light 0
or the shading corresponding to light 0,
we now call ComputeLight given
this direction 0 for the light source,
given the light color, the normal,
the half vector that we just computed and the diffuse,
specular, and shininess material properties.
For light 1, we have to first find the position
by dehomogenizing, so this takes light 1
position xyz and and divides by w.
The direction to the light source
is then the position of the light minus the vertex location.
Remember that when we were computing the viewing
direction, it was the eye position,
which was just the origin - mypos,
in this case it's the light position - mypos,
and again normalized to get a unit vector.
Notice that we are not computing attenuation
in this example but you can of course
do so in homework 2.
To compute the half vector we again normalize the direction
to the light plus the direction to the eye,
and then we determine a color by calling
the ComputeLight for function with the direction 1,
the light color, the normal, the half vector,
and diffuse, specular, and shininess.
Finally, the fragment color is simply a sum
of the ambient, the color from light 0
and the color from light 1.
We will now talk about the source code
in the display routine.
Let us see what the light set up in display looks like.
It involves a whole bunch of parameters,
which just correspond to the shading
or the lighting parameters that we will use.
So one is just white.
Its RGB is (1,1,1), with 1 for the alpha channel,
and we'll define these medium and small values.
We defined high for the shininess, and zero.
Light specular is now red, reddish, with some green tinge.
Light specular 1 you have green and bluish tinge.
For the light direction, for the direction of light,
it's in the x is equal to 0.5 direction.
For the positional light we define
the positional light coordinates.
And now we transform the light direction
and the light position to get light 0 and light 1.
This is a transformation by the current model view matrix,
so that you have the light's position
and direction properly transformed
before you pass it to the shader.
Why do we need to do this?
So let's take a brief aside to talk
about moving a light source.
The lights transform like other geometry.
Now since lighting is done in 3D
and not projected onto the image,
only the modelview matrix acts, not the projection matrix.
And in fact lighting is the only place
where you care about multiplying
by the modelview matrix separately
instead of a combination of projection times modelview.
There are many different types of light motion you can have.
So if the light is stationary, you just set the transforms
to the identity before specifying the light.
If you want to move only the light source you would
push the matrix, move the light, pop the matrix.
Sometimes you want something like a miner's hat
or a light source attached to the camera
where the light source is moved with the viewpoint,
and so you can simply set the light to the origin
and not have the modelview matrix act,
or set it to the identity. In this case, the light
is simply at the origin with respect to eye coordinates.
However, in the previous slide what we were simply doing
was moving the light into the correct location
in eye coordinates by transforming by the modelview matrix.
In fact this is the helper function
transformvec I'm using for that purpose.
It just takes as input four variables
xyzw and output four variables.
So it first defines a glm vector from the input variables
and then defines a glm output vector as modelview times
the input vector and then just sets
the output values to output vector.
So essentially, it's just multiplying by the modelview
matrix to transform the lights appropriately.
This is what we need to do to set
up the lighting for the teapot.
First, I set up a uniform vector,
which is three floating point numbers,
light 0 direction and I set it to the value light0.
The 1 here just means that I'm specifying only one vector.
So thereafter, I set up the light color,
which is now a 4-vector because of RGBA
and I set it to light_specular.
Light1 position is set to light1.
Light1 color is set to light_specular1.
light_specular1 is now our four-vector
that was specified earlier as just a constant.
Then I set the material parameters,
ambient, diffuse, specular, and shininess,
again to variables that were defined
as constants in my OpenGL program, small, medium, one, high.
So essentially the ambient is a small value.
Diffuse is a medium value.
For specular it's just set to one.
And the shininess is set to this high value of 100.
We'll wish to enable and disable lighting along the teapot,
so for the teapot you set the uniform variable islight
to one, so that lighting will be done for the teapot.
Normally in order to do lighting you'd also need
to define normals for the teapot and so on,
but the teapot object file,
we've already done that within it.
In the shader itself we need to do some mappings
in order to keep the bookkeeping straight.
So first you define the vertex shader by calling
initshaders with the file name for the vertex shader.
Similarly for the fragment shader,
you define a program and now you set
the shader parameter mappings, where islight
tells you the location in the shader of islight.
In this case I've just defined the variables,
same as the source strings in the shader
of the variable names in the shaders,
so it's easiest to keep track of,
but they could be different.
But essentially this sequence of commands is required
so that the variable specular in the OpenGL program
stores the appropriate location
for the specular variable in the shader,
and then it can be assigned with the suitable value.