CS 381 Fall 2012 > Lecture Notes for Tuesday, October 23, 2012 |
When a polygon is rendered, the user only sees one side of it, in any single animation frame. Thus each rendered polygon is either front-facing or back-facing. Which of these two categories a polygon lies in is determined by OpenGL close to the end of Vertex Processing, after clipping.
For a primitive containing separate polygons
(GL_TRIANGLES
,
GL_QUADS
,
GL_POLYGON
),
a front-facing polygon is one in which the vertices appear
in the viewport in counterclockwise order.
The vertices of a back-facing polygon appear in clockwise order.
For a primitive describing a ribbon of triangles
(GL_TRIANGLE_STRIP
,
GL_TRIANGLE_FAN
),
the above technique is used to determine
whether the first polygon in the ribbon is front-facing.
Then the rest are handled in a consistent manner,
so that one side of the ribbon is the front,
and the other is the back.
Lastly, GL_QUAD_STRIP
is handled just as if it were
GL_TRIANGLE_STRIP
.
An important point:
You can reverse the vertex order used for front-facing determination.
Call glFrontFace
with parameter GL_CW
to make clockwise vertex order indicate a front-facing polygon,
and counter-clockwise order a back-facing polygon.
[C++]
glFrontFace(GL_CW);
Pass GL_CCW
to restore the default behavior.
A GLSL fragment shader can determine whether the current fragment
comes from a front- or back-facing polygon
by reading gl_FrontFacing
,
a predefined bool
.
Many objects are drawn so that the user never sees the back side of any polygons. Thus, if we avoid drawing them, we may save time. Throwing out polygons is called culling; when we throw out back-facing polygons, it is back-face culling. In the pipeline, culling is done at the end of Vertex Processing, after clipping.
In OpenGL, turn on culling with
[C++]
glEnable(GL_CULL_FACE);
and turn it off with the corresponding glDisable
call.
OpenGL can cull back-facing polygons, front-facing polygons,
or (oddly) both.
Determine which is done using glCullFace
,
passing
GL_FRONT
,
GL_BACK
,
or GL_FRONT_AND_BACK
.
For example, to do back-face culling:
[C++]
glCullFace(GL_BACK); // This is the default setting
We can produce the same effect in a fragment shader—although it is less efficient, since it requires the polygon to be rasterized:
[GLSL fragment shader]
if (!gl_FrontFacing) discard;
Note that back-face culling can be used as a simple HSR method. It works when the only thing drawn is a convex object (sphere, cube, etc.), with all polygons facing outward. More generally, it also works when many such objects are drawn, in back-to-front order.
Another application of culling is to render the two sides of a surface using different shaders. Render using one shader with back-face culling enabled, and render using another shader with front-face culling enabled.
We may wish a shader to color the two sides of a polygon differently.
GLSL supports automatic choosing of different colors for front-facing and back-facing polygons. To enable distinct front & back colors, do (in your application):
[C++]
glEnable(GL_VERTEX_PROGRAM_TWO_SIDE);
Apparently some implementations also require the following in the vertex shader (and it does not seem to hurt):
[GLSL vertex shader]
#extension GL_VERTEX_PROGRAM_TWO_SIDE : enable
If you do the above, then you need to set both
gl_FrontColor
and gl_BackColor
in the vertex shader.
In the fragment shader,
gl_Color
is set to the interpolated color value, as usual.
However, for back-facing polygons,
the value of gl_BackColor
is used.
For front-facing polygons,
the value of gl_FrontColor
is used, as before.
I am not fond of the above method, since it requires shader
functionality to be enabled in the application;
it seems to me that this is putting information in the wrong place.
I prefer to send the color for back-facing polygons in my own
varying
variable.
[GLSL vertex shader]
varying vec4 mybackcolor; ... gl_FrontColor = ...; // Color for front-facing polygon mybackcolor = ...; // Color for back-facing polygon[GLSL fragment shader]
varying vec4 mybackcolor; ... vec4 mycolor = gl_FrontFacing ? gl_Color : mybackcolor; // Select color based on which side the user sees
Alternatively, just hard-code the back-facing color into the fragment shader.
[GLSL fragment shader]
vec4 mycolor = gl_FrontFacing ? gl_Color : vec4(0.2, 0.7, 0.2, 1.0); // Select color based on which side the user sees
In order to light both sides of a polygon correctly, reverse the normal when the polygon is back-facing.
[GLSL fragment shader]
if (!gl_FrontFacing) mynorm = -mynorm;
For this to work properly, the normals specified for the surface should point toward the front side of the polygons.
See
twoside_shaders.zip
for examples of shaders that do two-sided coloring/lighting.
These shaders are intended for use with
useshaders.cpp
.
Note: The teapot drawn
by glutSolidTeapot
has its normals facing outward,
as we would expect.
However, when we try two-sided lighting with the teapot,
we find that, tragically,
it has the back side of its polygons facing outward.
Thus, the normals point toward the back side of the polygons.
This can be fixed using glFrontFace
as described above.
I suggest doing this fix just before calling glutSolidTeapot
and then restoring the default behavior just afterward.
[C++]
glFrontFace(GL_CW); glutSolidTeapot(1.); glFrontFace(GL_CCW);
Application
useshaders.cpp
has been modified so that the teapot is handled as above.
Attenuation is the reduction of light intensity as distance from the light source grows.
For a point light source with a position in space, the physically correct attenuation is quadratic attenuation, in which the intensity of the light source is inversely proportional to the square of the distance from it. This is easily computed in GLSL:
[GLSL]
float dist = distance(vertpos, lightpos); // Compute light-source distance ... lightcolor / (dist * dist) ... // Use it
A problem with attenuation stems from the fact that
computer monitors have much less dynamic range than the real world.
In the real world, light sources are often too bright too look at
(think of the sun);
on a computer, they cannot be brighter than (1.0, 1.0, 1.0)
.
Thus, a light source that can be displayed, will often fail to
light up a scene adequately, particularly if quadratic attentuation is used.
One solution is to use linear attenuation,
in which intensity is inversely proportional to the distance.
Thus,
“/ (dist * dist)
”,
above, becomes
“/ dist
”.
Linear attenuation is also physically correct for a light source
shaped like an infinitely long line.
Thus it is not a bad approximation for things like long
fluorescent bulbs, if the lit object is not too far from the bulb,
or a long line of closely spaced lights.
See attenmult_shaders.zip
for shaders that
do quadratic attenuation.
These shaders are intended to be used with
useshaders.cpp
.
A spotlight is a light source that lights primarily in a particular direction.
To make a spotlight, we need to know, in addition to the usual information about a light source:
As with a light’s position, we want the spot direction
to be modified appropriately by the model/view transformation
at the time we are specifying the light source.
So again, we use glLight*
.
Pass GL_SPOT_DIRECTION
as the second parameter.
The third parameter should be a pointer to an array of three
GLfloat
values.
[C++]GLfloat spotdir[] = { 0., 0., -1. }; glLightfv(GL_LIGHT0, GL_SPOT_DIRECTION, spotdir);
In GLSL, get the spot direction using the spotDirection
member (vec3
)
of an item in the gl_LightSource
array.
[GLSL]
vec3 spotdir = normalize(gl_LightSource[0].spotDirection);
We might have multiple cutoff angles, since we might want the spotlight to fade out slowly as we reach the edge of its beam. Thus, inside the inner cutoff angle, we have the full effect of the spotlight, outside the outer cutoff angle, we have no effect, and between the two we interpolate.
We need to compute the vector from the light-source position to the vertex position (the opposite of the light-source direction vector). Then we find the angle between this vector and the spot direction (arccosine of the dot-product, assuming everything is normalized). This angle can be compared with the various cutoff angles to determine the effect of the spotlight on the vertex.
It is a bit simpler to work entirely with cosines. Store the cosines of the cutoff angles, and compare the dot product to these directly; no arccosine is needed.
See spot_shaders.zip
for shaders that
include a spotlight with two cutoff angles.
These shaders are intended to be used with
useshaders.cpp
,
which has been modified to provide a spot direction for light source 0.
An alternate method is to replace the cutoff angle(s) with a spot exponent. Take the above dot product, raise it to a power, and multiply the result by the light color.
To have multiple light sources:
glLight*
to pass light-source information,
then vary the first parameter
(GL_LIGHT0
, GL_LIGHT1
, etc.)
to indicate which light source you are specifying.
Now, once we have the surface color resulting from each light source, we need to combine them somehow into a single color. The obvious—and essentially correct—thing to do is to add them. However, this may result in color components that are too high (that dynamic-range problem again).
The standard solution is to clamp color values to the proper range.
We can use the GLSL function clamp
to do this.
[GLSL]
vec4 clampedcolor = clamp(color0 + color1 + color2, 0., 1.); // Clamp to range [0., 1.]
Note that, if you have multiple light sources, then you may wish to make individual light sources dimmer.
See attenmult_shaders.zip
for shaders that
have multiple light sources.
These shaders are intended to be used with
useshaders.cpp
,
which has been modified to provide positions for two light sources.
This subtopic will be covered in class on Thursday, October 25.
The lighting methods we have discussed so far
have moved smoothly between colors,
so that adjacent pixels on the same polygon receive nearly
the same color.
An alternative is cel shading,
in which we step through a small set of colors.
The result is a cartoon-like appearance.
A simple way to do this when using Lambertian illumination is to replace the value of the Lambert cosine by the nearest fraction with some small denominator—say \(5\).
Consider the following GLSL functions.
[GLSL]
// nearwhole // Returns float version of nearest integer to v. float nearwhole(float v) { return float(int(v + (v>=0. ? 0.5 : -0.5))); } // nearfrac // Returns float version of nearest fraction to v with denominator d. float nearfrac(int d, float v) { float df = float(d); return nearwhole(df*v)/df; }
Now, in our color computation,
we can replace “lambcos
”
by something like “nearfrac(5, lambcos)
”.
Similar ideas apply to more complicated models.
In the Blinn-Phong model, we could apply nearfrac
to the specular coefficient before multiplying it by the light color.
See cel_shaders.zip
for shaders that
do cel shading.
These shaders are intended to be used with
useshaders.cpp
.
This subtopic was covered in class on Thursday, October 11.
So far, our light sources have had positions in space; they are positional light sources. Sometimes we want a light source that is extremely far away (“at infinity”). For the purpose of computations, such a light source has no position, only a direction; it is a directional light source. How do we handle such a light source?
Recall that, when we convert a position to homogeneous form, we can multiply by any positive number we want. The usual number is \(1\); thus, the position \((-1, 2, 5)\) usually becomes \((-1, 2, 5,\ 1)\) in homogeneous form, and the position \((-10, 20, 50)\) usually becomes \((-10, 20, 50,\ 1)\) in homogeneous form. But we are not required to use \(1\). Using \(0.1\) in the second conversion gives an alternate homogenous form of \((-1, 2, 5,\ 0.1)\).
Now use this same idea for a position that is progressively farther away.
Position | Homogeneous Form | Alternate Homogeneous Form |
---|---|---|
\((-1,2,5)\) | \((-1,2,5,\ 1)\) | \((-1,2,5,\ 1)\) |
\((-10,20,50)\) | \((-10,20,50,\ 1)\) | \((-1,2,5,\ 0.1)\) |
\((-100,200,500)\) | \((-100,200,500,\ 1)\) | \((-1,2,5,\ 0.01)\) |
\((-1000,2000,5000)\) | \((-1000,2000,5000,\ 1)\) | \((-1,2,5,\ 0.001)\) |
\((-1000000,2000000,5000000)\) | \((-1000000,2000000,5000000,\ 1)\) | \((-1,2,5,\ 0.000001)\) |
\((-\infty,2\infty,5\infty)\) ??? | \((-\infty,2\infty,5\infty,\ 1)\) ??? | \((-1,2,5,\ 0)\) |
It is not clear that the last row of the above table makes any sense. On the other hand, the right-hand column seems to make sense. In any case, the representation on the lower right does actually work.
That is, we can specify the direction of a directional light source,
while allowing for modification by the model/view transformation,
by giving zero as the fourth coordinate of our vec4
.
[C++]
GLfloat lsdir[] = { -1., 2., 5., 0. }; glLightfv(GL_LIGHT0, GL_POSITION, lsdir);
Note that we still use GL_POSITION
above.
The shader must be aware of the possibility of a directional light source; otherwise, it will divide by zero when converting from homogenous form.
[GLSL]
// Compute normalized light-source direction in camera coords vec3 lightdir; vec4 lightpos4 = gl_LightSource[0].position; if (lightpos4.w != 0.0) { // Positional light source vec3 lightpos = lightpos4.xyz / lightpos4.w; lightdir = normalize(lightpos - vertpos); } else { // Directional light source lightdir = normalize(lightpos4.xyz); }
Directional light sources do not work well with some of the other lighting ideas we have covered. In particular, a directional light source cannot be attenuated; nor can it be a spotlight.
I will not be posting a full implementation of a directional light source.
ggchappell@alaska.edu