CS 476: Computer Graphics Lecture Notes

lecture #1 began here

Graphics is not just rendering

Computer graphics is a broad area of computer science, incorporating elements of:

History

Graphics Hardware

Graphics hardware has been developing at a very rapid pace, perhaps faster than Moore's Law would suggest. The driving application domain: computer games.

Two of the significant changes in the past 10 years:

  1. "True Color" is now ubiquitous
  2. 3D graphics acceleration is now ubiquitous

Frame Buffers

Bitmapped raster graphics is founded on the notion of some big hunk of physical memory being mapped directly to pixels. It is common to use 1,4,8,15,16,24, or 32 bits per pixel. Writing directly to memory is fast. Coding tends to be a mess of bitwise operations.

Generally the operating system and/or window system limit access to the frame buffer. There is conflict between abstraction and performance. 3D graphics "accelerator" hardware capabilities are increasingly "higher level", to where writing to the frame buffer yourself makes less and less sense.

Graphics API's

Vendor specific API's have tended to be complex (400-800 functions, 50-100 complex structure types). Portable graphics API's were slow and not widely accepted, more or less until the OpenGL API was developed. Reputable sources have claimed 3-6 months' training are required to achieve reasonable mastery of a major API.

lecture #2 began here

Implementing DrawPixel(w,x,y)

In this course we consider not just the application programmer's point of view, but the graphics package implementor's point of view. Many of the fundamental algorithms implemented within existing graphics packages are presented to enhance your understanding and use of these packages.

Homework #0 postulated the use of a primitive for setting an individual dot (pixel, picture element) on the screen, and the previous lecture described the frame buffer used to store the contents of the screen in memory. So, how do we implement DrawPixel() ? The following pseudocode fragments approximate it for a display WIDTH x HEIGHT whose frame buffer is an array of bytes for which we have a pointer named fb. Implementation for real hardware would vary depending e.g. on whether the processor was "big-endian" or "little-endian", whether a 1-bit means black or means white, etc.

Monochrome (1-bit per pixel) case
    void DrawPixel(int x, int y, int value)
    {
	int index = y * HEIGHT + x / 8;	
	int bit = y * HEIGHT + x % 8;	
	if (value) { /* set the bit on */
	   fb[index] |= 1 << bit;
	   }
        else { /* set the bit off */
	   fb[index] &= ~(1 << bit);
           }
    }
    
True Color (packed 24-bits per pixel) case
   void DrawPixel(int x, int y, int value)
   {
      int index = (y * HEIGHT + x) * 3;
      fb[index++] = value & 255;
      fb[index++] = (value>>8) & 255;
      fb[index]   = (value>>16) & 255;
   }
   
Note that memory is cheap and writing 32-bits at a time has compelling advantages, so many frame buffers are organized as 32-bits per pixel instead of 24, even though they only offer 24 bits of color. Some use the extra 8 bits, others ignore them.

Line Drawing

OK, so we can draw pixels. How do we get from there to more complex graphic objects, such as lines, rectangles, etc.? We need algorithms. 15 years ago graphics packages implemented these algorithms in software the same way you and I would, but nowadays most graphics hardware implements many graphics algorithms directly. Pseudocode here switches to Icon as a way of passively introducing the notation. It is inconvenient to postulate a window, and we are pretending we are writing to the framebuffer, so let us say that the window is a global (default) variable.
Horizontal line from (x1, y) to (x2, y)
    procedure DrawHoriz(x1,x2,y) # while loop version
        x := x1
        while x <= x2 do {
           DrawPoint(x,y)
           x := x + 1
           }
    end
or
    procedure DrawHoriz(x1,x2,y) # "for"-loop version
        every x := x1 to x2 do {
           DrawPoint(x,y)
           }
    end
   
Brute force (incremental) line algorithm (based on: y = mx + B)
   # note: use real numbers and round off, to avoid numeric errors
   procedure DrawLine0(x1,y1,x2,y2)
      m := real(y2 - y1) / real(x2 - x1)
      y := real(y1)
      every x := x1 to x2 do {
         DrawPoint(x, integer(y + 0.5)) # round off
	 y +:= m
         }
   end
Midpoint algorithm (Bresenham, Pitteway, Van Aken)

Another important line algorithm was developed in the 60's and subsequently refined. Starting from the form F(x,y) = ax + by There is some cleverness here, to avoid any need for floating point numbers, which speeds things up greatly, especially on machines that do not have floating point instructions! See Figure 3.7 and read that section (3.2.2) carefully!
   procedure DrawLine1(x1,y1,x2,y2)
      dx := x2 - x1
      dy := y2 - y1
      d  := 2 * dy - dx
      incrE := 2 * dy
      incrNE := 2 * (dy - dx)
      x := x1
      y := y1
      DrawPoint(x,y)
      while x < x2 do {
         if d <= 0 then {
            d +:= incrE
            x +:= 1
            }
         else {
            d +:= incrNE
            x +:= 1
            y +:= 1
            }
         DrawPoint(x,y)
         }
    end

API parameters and graphic contexts

From skimming CG chapter 2 you should notice that a host of attributes (line width and style, color, etc) are applied in any given graphic output command. Some API's use separate parameters for each such "dimension", leading to very large parameter lists. In the design of the X Window System, this was avoided in order to reduce network traffic. Instead, resources for drawing attributes are preallocated on the display server in a large data structure called a graphics context. The X graphics context is not entirely complete (e.g. it doesn't include fonts) and other API's subdivide the graphics context further into specific abstractions such as "pen" and "brush".

lecture #3 began here

Midpoint Line Algorithm Example

The best way to get a feel for Bresenham/Midpoint in action is to see it. Here are the incrE and incrNE, and the value of x, y, and d for the line segment from (50,50) to (60,63):
incrE 6 incrNE -14
x 50 y 50 d -4
x 51 y 50 d 2
x 52 y 51 d -12
x 53 y 51 d -6
x 54 y 51 d 0
x 55 y 51 d 6
x 56 y 52 d -8
x 57 y 52 d -2
x 58 y 52 d 4
x 59 y 53 d -10
x 60 y 53 d -4

Color Indices and Colormaps

So far we have mainly considered monochrome and "true color" frame buffers. 15- and 16-bits per pixel color displays are available that closely approximate "true color" in behavior. You should learn of at least one very different method of organizing a frame buffer, commonly used for 4- and 8-bits per pixel color displays. Instead of literally representing intensity of color, the frame buffer bits on these displays are often used as subscripts into an array (called a colormap) whose elements are color intensities. The "array" may be hardwired to a specific set of colors, or modifiable. If it is modifiable, the colormap provides a level of indirection that enables clever (and non-portable) tricks to be played, where modifying a single array element instantaneously changes the color for many (possibly hundreds of thousands) pixels.

Circle Drawing

Circles obey the an equation such as x2 + y2 = R2. If you can draw a good quarter circle, you can draw the rest of the circle by symmetry. If you try to draw a quarter circle by advancing x each step and solving the equation for y (y == +- sqrt(R2 - x2) from the earlier equation. But besides being slow, for part of the circle pixels will not touch because the slope is too steep, similar to the problem we saw drawing very "steep" lines looping on the x coordinate.

To avoid the discontinuity you can write a loop that steps through small angular increments and uses sin() and cos() to compute x and y.

Brute force circle algorithm (based on: y == +- sqrt(R^2 - x^2))
How small an angular increment is needed? our distance is 2 * pi * R pixels worth of drawing; we need angular increments R small enough to make these adjacent. 1/R radians gives a nice solid circle; angular steps twice as large still look like a connected, thinner circle.
   procedure DrawCircle0(x,y,R)
      every angle := 0.0 to 2 * &pi by 1.0 / R do {
	 px := x + sin(angle) * R
	 py := y + cos(angle) * R
	 DrawPoint(px,py)
         }	
   end
8 Way Symmetry
Symmetry can speed up and simplify computations. We can do 1/8 as many calls to sin(), cos(), etc. This code also relies on "translation" of coordinates from (0,0) to an origin of (x0,y0). Translation is the first of many coordinate transformations you will see.
   procedure CirclePoints(x0, y0, x, y)
      DrawPoint(x0 + x, y0 + y)
      DrawPoint(x0 + y, y0 + x)
      DrawPoint(x0 + y, y0 - x)
      DrawPoint(x0 + x, y0 - y)
      DrawPoint(x0 - x, y0 - y)
      DrawPoint(x0 - y, y0 - x)
      DrawPoint(x0 - y, y0 + x)
      DrawPoint(x0 - x, y0 + y)
   end
Mid-point (or Bresenham) circle algorithm
Like the mid-point line algorithm, it is possible to do a much more efficient incremental job of drawing a circle. See Fig 3.14. The procedure is very similar to the Mid Point line algorithm.
   procedure DrawCircle1(x0,y0,R)
      x := 0
      y := R
      d := 5.0 / 4.0 - R
      CirclePoints(x0, y0, x, y)
      while (y > x) do {
         if d < 0 then {
            d +:= 2.0 * x + 3.0
            }
         else {
	    d +:= 2.0 * (x - y) + 5.0
            y -:= 1
            }
         x +:= 1
	 CirclePoints(x0, y0, x, y)
         }
   end
lecture #4 began here

Raster Operations

Our hardware lesson for today is yet another nuance of frame buffer manipulation. Consider the frame buffer as an array of bits in memory. When you are "drawing" on the frame buffer, you are writing some new bits out to that memory. You have the option of just replacing and discarding what used to be there, but there are other possibilities that involve combining the new value with the old. Each bit of the current contents could be combined using AND, OR, and NOT with corresponding bits being output. These different boolean combinations of current frame buffer with new output pixel values are called raster operations.

The most interesting raster operation is probably XOR. It has the property that XOR'ing a pixel value a second time restores the pixel to its former contents, giving you an easy "undo" for simple graphics. On monochrome displays XOR works like a champion, but on color displays it is less useful, because the act of XOR'ing in a pixel value may produce seemingly random or indistinct changes, especially on color index displays where the bits being changed are not color intensities. If a color display uses mostly a foreground color being drawn on a background color, drawing the XOR of the foreground and background colors in XOR mode works pretty much the way regular XOR does on monochrome displays.

An Overview of Icon's Graphics Facilities

We discuss selected aspects of Icon language syntax and the graphics facilities from Chapters 3 and 4 of the Icon Graphics Book. Here are some of my favorite functions you should start with.
WOpen(), WClose()
Bg(), Fg(), WAttrib()
DrawPoint(), DrawLine()
{Draw,Fill}{Arc,Circle,Polygon,Rectangle}
DrawCurve()
Event()
Pixel()

Region Filling

Suppose you want to fill in a circle or rectangle you've just drawn using one of the last few algorithms we've discussed? Starting from a point inside the object to be filled, one can examine pixels, and set all of them to a new color until one hits the border color of the object. (See the CG book Section 19.5 for more discussion of this material.)

The border that defines a region may be 4-connected or 8-connected. 4-connected regions do not consider diagonals to be adjacent; 8-connected regions do. The following algorithms demonstrate brute force filling. The foreground color is presumed to be set to "new".

# for interior-defined regions. old must not == new
procedure floodfill_4(x,y,old,new)
   if Pixel(x,y,1,1) === old then {
      DrawPoint(x,y)
      floodfill_4(x,y-1,old,new)
      floodfill_4(x,y+1,old,new)
      floodfill_4(x-1,y,old,new)
      floodfill_4(x+1,y,old,new)
      }
end
For the example we started with (filling in one of our circles) only slightly more is needed:
# for boundary-defined regions.  the new color may be == to the boundary
procedure boundaryfill_4(x, y, boundary, new)
   p := Pixel(x,y,1,1) # read pixel
   if p ~== boundary & p ~== new then {
      DrawPoint(x,y)
      boundaryfill_4(x,y-1,boundary,new)
      boundaryfill_4(x,y+1,boundary,new)
      boundaryfill_4(x-1,y,boundary,new)
      boundaryfill_4(x+1,y,boundary,new)
      }
end
The main limitation of these brute-force approaches is that the recursion is deep, slow, and heavily redundant. One way to do better is to fill all adjacent pixels along one dimension with a loop, and recurse only on the other dimension.

lecture #5 began here

More reading the frame buffer with Pixel()

If you try one of the fill algorithms above, they do genuinely recurse crazily enough to get stack overflows. Another reason they are sooo slow is because of the X11 window system architecture, which does not support reading the frame buffer as efficiently as writing to it; in particular, to ask for the contents of a window in general you must send a network message and wait for a reply.

Pixel(x,y,w,h) asks for all the pixels in a rectangular region with a single network transaction, which will be must faster than reading each pixel individually. In general, client-side "image" structures are not interchangeable with server-side "pixmap" and "window" structures, but this is an unfortunate limitation of their design.

How, how do you store a local (client-side) copy of a window in order to work with it efficiently? There are many ways you can represent an image, but we will start with a couple of brute force representations using lists of lists and tables of tables.

Faster Fills

To speedup the fill algorithm, one can
  1. Eliminate recursion, replace it with list operations and loops
  2. Reduce the number of redundant checks on each pixel
  3. Think in terms of line segments instead of individual pixels when possible.
# nonrecursive boundary fill.  the new color may be == to the boundary
procedure nr_boundaryfill_4(x, y, boundary, new)
   L := [x, y]
   while *L > 0 do {
      x := pop(L)
      y := pop(L)
      p := Pixel(x,y,1,1) # read pixel
      if p ~== boundary & p ~== new then {
         DrawPoint(x,y)
	 put(L, x, y-1)
	 put(L, x, y+1)
	 put(L, x-1, y)
	 put(L, x+1, y)
         }
      }
end
lecture #6 began here

Backing Store and Expose Events

Using Icon we operate under the illusion that each on-screen canvas is a chunk of the frame buffer that we control as long as our program requires it. In reality, window systems are sharing and coordinating use of the frame buffer much as operating systems schedule the CPU. In particular, moving, resizing, overlapping, minimizing and maximizing windows all point out the fact that an application doesn't have exclusive or continuous access to the frame buffer.

Under these various circumstances, window systems may discard part or all of an application's screen contents, and ask the application to redraw the needed portion of the screen later, when needed. A window system tells an application to redraw its screen by sending it an "expose" or "paint" event. Some applications just maintain a data structure containing the screen contents and redraw the screen by traversing this data structure; this is the raster graphics analog of the old "display list" systems used in early vector graphics hardware. But clients are complicated unnecessarily if they have to remember what they have already done. The obvious thing to do, and the thing that was done almost immediately, was to add to the window system the ability to store screen contents in an off-screen bitmap called a backing store, and let it do its own redrawing. Unfortunately, evil software monopolists at AT&T Bell Labs such as Rob Pike patented this technique, despite not having invented it, and despite its being obvious. This leaves us in the position of violating someone's patent whenever we use, for example, the X Window System.

Smooth Curves

DrawCurve(x1,y1,...,xN,yN) draws a smooth curve. If first and last points are the same, the curve is closed and is smooth around this boundary point. For the algorithm used, we turn to [Barry/Goldman 88] in the SIGGRAPH 88 conference proceedings.

Contexts and Cloning

With certain limits, you can have multiple contexts associated with a canvas, or can use a context on multiple canvases. Clone(w) creates an additional context on window w. Attributes in a context include: In contrast, canvas attributes:

Clipping

Clip(x,y,w,h) clips output to a specified rectangle in the window.

Fonts

"xlsfonts" reports 5315 fonts on the Linux system in my office. Windows systems vary even more than X11 systems, since applications often install their own fonts.

Fonts have: height, width, ascent, descent, baseline. Font portability is one of the major remaining "issues" in Icon's graphics facilities. There are four "portable font names": mono, typewriter, sans, serif, but unless/until these become bit-for-bit identical, programs that use text require care and/or adjustment in order to achieve portability. You can "fish for a font" with something like:

   Font(("Frutiger"|"Univers"|"Helvetica"|"sans") || ",14")

Proportional width fonts may be substantially more useful, but require substantially more computation to use well than do fixed width fonts. TextWidth(s) tells how many pixels wide string s is in a window's current font. Here is an example that performs text justification.

procedure justify(allwords, x, y, w)
   sofar := 0
   thisline := []
   while word := pop(allwords) do {
      if sofar + TextWidth(word) + (*thisline+1) * TextWidth(" ") > w then {
         setline(x, y, thisline, (w - sofar) / real(*thisline))
         thisline := []
         sofar := 0
	 y +:= WAttrib("fheight")
         }
      }
end
procedure setline(x,y,L,spacing)
   while word := pop(L) do {
      DrawString(x,y,word)
      x +:= TextWidth(s) + spacing
      }
end
lecture #7 began here

Curve Drawing

How do you draw smooth curves? You can approximate them with "polylines" and if you make the segments small enough, you will have the desired smoothness property. But how do you calculate the polylines for a smooth curve? See Section 11.2

brute force
You can just represent the curve with every point on it, or with very tiny polylines. This method is memory intensive and makes manipulating the curve untenably tedious. The remaining options represent the curve with mathematical function(s) that approximate the curve.
explicit functions y=f(x)
does not handle curves which "double back"
implicit equations f(x,y)=0
hard to write equations for parts of curves, hard to sew multiple curves together smoothly
parametric equations x=x(t) y=y(t)
avoids problems with slopes; complex curves are sewn together from simpler ones; usually cubic polynomials are sufficient for 3D. See equations 11.5-11.7.

Sewing together curves is a big issue: the "slope" (endpoint tangent vectors are used to track these) of both curves must be equal at the join point.

Splines

In the real world, a spline is a metal strip that can be bent into various shapes by pulling specific points on the strip in some direction with some amount of force. The mathematical equivalent is a continuous cubic polynomial that passes through a set of control points. There are lots of different splines with interesting mathematics behind them. Catmull-Rom splines are one popular family of splines, with the property that the slopes as the curve passes through each point will be parallel to the surrounding points (Figure 11.32). Icon's DrawCurve() function uses the Catmull-Rom splines, specifically the algorithm cited in CG as [BARR88b]: Barry, Phillip J., and Goldman, Ronald N. (1988). A Recursive Evaluation Algorithm for a class of Catmull-Rom Splines. SIGGRAPH'88 conference, published in Computer Graphics 22(4), 199-204.

Smooth Curves - an algorithm

record Point(x,y)
procedure gencurve(p)

#   float  ax, ay, bx, b_y, stepsize, stepsize2, stepsize3;
#   float  x, dx, d2x, d3x, y, dy, d2y, d3y;


   every i := 4 to *p do {
      #
      # build the coefficients ax, ay, bx and b_y, using:
      #                             _              _   _    _
      #   i                 i    1 | -1   3  -3   1 | | Pi-3 |
      #  Q (t) = T * M   * G   = - |  2  -5   4  -1 | | Pi-2 |
      #               CR    Bs   2 | -1   0   1   0 | | Pi-1 |
      #                            |_ 0   2   0   0_| |_Pi  _|
      #

      ax := p[i].x - 3 * p[i-1].x + 3 * p[i-2].x - p[i-3].x
      ay := p[i].y - 3 * p[i-1].y + 3 * p[i-2].y - p[i-3].y
      bx := 2 * p[i-3].x - 5 * p[i-2].x + 4 * p[i-1].x - p[i].x
      b_y := 2 * p[i-3].y - 5 * p[i-2].y + 4 * p[i-1].y - p[i].y

      #
      # calculate the forward differences for the function using
      # intervals of size 0.1
      #
      steps := max(abs(p[i-1].x - p[i-2].x), abs(p[i-1].y - p[i-2].y)) + 10

      thepoints := [ ]

      stepsize := 1.0 / steps
      stepsize2 := stepsize * stepsize
      stepsize3 := stepsize * stepsize2

      x := p[i-2].x
      y := p[i-2].y
      put(thepoints, x, y)
      dx := (stepsize3*0.5)*ax + (stepsize2*0.5)*bx +
	   (stepsize*0.5)*(p[i-1].x-p[i-3].x)
      dy := (stepsize3*0.5)*ay + (stepsize2*0.5)*b_y +
	   (stepsize*0.5)*(p[i-1].y-p[i-3].y)
      d2x := (stepsize3*3) * ax + stepsize2 * bx
      d2y := (stepsize3*3) * ay + stepsize2 * b_y
      d3x := (stepsize3*3) * ax
      d3y := (stepsize3*3) * ay

      # calculate the points for drawing (this piece of) the curve

      every 1 to steps do {
	 x   +:= dx
	 y   +:= dy
	 dx  +:= d2x
	 dy  +:= d2y
	 d2x +:= d3x
	 d2y +:= d3y
	 put(thepoints, x, y)
         }
      DrawLine ! thepoints
      }
end
lecture #8 began here

Smooth Curves - a slight embarassment

The following figures depict the behavior of the smooth curves algorithm on the curve through points. In each figure the top picture is the algorithm as given, and the bottom is the built-in drawcurve, which is (should be) doing the same algorithm in C.

After my first pass, it was obvious some pixels seem to be missing, not just from the Icon version, but from the "official" built-in version! Some missing pixels were filled in by adding another line segment to the end of each step:

To fill in more pixels, I traced the actual execution behavior, and saw some pixels in the generated output not showing up! Pixels in green are generated by the algorithm but not shown in the drawn curve:

Images

Image manipulation involves three areas File formats are numerous; these are things like GIF, JPG, BMP, PNG. They vary according to compression size, lossiness, and portability. Icon does GIF on all platforms, plus whatever formats are built-in to the Window system (e.g. BMP and XBM). GIF is not very suitable due to patent encumbrances, and Icon really needs to add PNG and JPG support.

Window system native manipulation starts with off-screen invisible windows you can draw on, and copy to visible windows from. A window opened with "canvas=hidden" in Icon can be used for this purpose; CopyArea(w1,w2,x,y,wd,ht,x2,y2) or WAttrib("canvas=normal") are examples of ways to get hidden graphics onto the screen.

lecture #9 began here

Client Side Image Manipulation in Icon

Besides the brute force methods ("list of list of string color names") Icon supports a client side image format that consists of a string encoding of an image. DrawImage(x, y, spec) sends such an image to the default window. The spec comes in two alternate forms:
"width,palette,data"
a color or gray scale image, each data char represents one pixel.
"width,#data"
a bilevel image, bits from data are interpreted as fg and bg colors.
Palettes are c1-c6 or g2-g256 and define the color interpretations of the data chars.

Gamma Correction

The eye is more sensitive to ratios of intensity levels, rather than absolute values of intensity. Intensity levels should be spaced logarithmically rather than linearly, to achieve equal levels of brightness. CG Section 13.1.1 gives an overview of gamma correction; without gamma correction your range of color values will not be spread out smoothly. Some hardware has gamma correction built-in, and some operating systems (e.g. MacOS) implement gamma correction uniformly at the system level. For the rest, you may have reasonable default behavior but it may be to your advantage to allow the user to modify the default gamma correction value for their nonstandard CRT's.

2D Geometrical Transformations

Please absorb Chapter 5 in detail, the 2D part for starters. Don't you just love those homogeneous coordinates!

lecture #10 (virtual lecture) began here

Window-to-Viewport (from CG section 5.4)

"World coordinates" are coordinates defined using application domain units. Graphics application writers usually find it far easier to write their programs using world-coordinates. Within the "world", the graphics application presents one or more rectangle views, denoted "world-coordinate windows", that thanks to translation, scaling, and rotation, might show the entire world, or might focus on a very tiny area in great detail.

A second set of transformations is applied to the graphics primitives to get to the "physical coordinates", or "viewport" of whatever hardware is rendering the graphics. The viewport's physical coordinates might refer to screen coordinates, printer device coordinates, or the window system's window coordinates.

If the world coordinates and the physical coordinates do not have the same height-width aspect ratio, the window-to-viewport transformation distorts the image. Avoiding distortion requires either that the "world-coordinate window" rectangles match the viewport rectangles' shapes, or that part of the viewport pixels go unused and the image is smaller.

Matrix Representations of 3D Coordinate Transformations

3D transformations can be represented by 4x4 matrices using homogeneous coordinates (x, y, z, W), or (x/W, y/W, z/W, 1) for W != 0.

Translation is extended from the 2D case:
T(dx,dy,dz) =
100dx
0 10dy
0 0 1dz
0001

Scaling is similarly extended:
S(dx,dy,dz) =
Sx000
0Sy00
00Sz0
0001

Rotation is a little more interesting; the rotation around the origin that we did before now becomes rotation around the z-axis with the following matrix:
Rz(θ) =
cos θ-sin θ00
sin θcos θ00
0010
0001

But there are two more axes one might want to rotate about:

Rx(θ) =
1000
0cos θ-sin θ0
0sin θcos θ0
0001

Ry(θ) =
cos θ0sin θ0
0100
-sin θ0cos θ0
0001

Composition of 3D Transformations (CG section 5.7)

As was the case for 2D, 3D translation, scaling, and rotation can be composed as much as you want, and reduced via matrix multiplication to a single matrix to apply to all points that need transforming. Typical will be a translation to the origin, a scaling, and as many rotations as are needed to orient the object in the right direction.

Change in Coordinate Systems

One way to view transformations is as a "coordinate system conversion" similar to converting temperature from Fahrenheit to Celsius. To render a scene in world coordinates you are changing world coordinates into "window" (logical) coordinates, and then changing "window" coordinates into "viewport" (physical) coordinates. This same approach, and the application of transformations to accomplish conversions, may be extended into the application domain, allowing objects to express their coordinate systems relative to whatever near neighbors are most logical, instead of expressing them as absolute positions relative to some world origin which is arbitrary.

lecture #11 (virtual lecture) began here

All About Colors

This talk is based on material in CG Chapter 13 and GB Chapter 7.

We have some basic introduction to color earlier, namely the RGB color coordinate system commonly used on computer monitors. Computer hardware commonly uses 24-bits to express color information for each pixel, while software may use another coordinate system, such as X11's 48-bit system.

It may surprise you to hear that RGB color coordinates are a relatively new invention, constructed solely as a by-product of the hardware used in color monitors. RGB is not used in traditional color disciplines such as photography or print shops. Interestingly, RGB is not even capable of expressing many colors that humans recognize. We will see aspects of that and other issues in this talk.

Intensity

Color is the perception of light, and the first coordinate that applies to all perception of light is how much light is seen: this is called intensity, luminance, lightness or brightness. Intensity may range from 0 (black) through the grays up to infinity (white). Intensity beyond a certain amount will cause blindness, so almost any upper bound of intensity that is whiter than everything else might be considered white for a given view. Aside from common binary systems (light vs. no light, a.k.a. monochrome), the question is: how many different levels of brightness does the hardware support, and how many levels of intensity would be required for us to see it as a smooth continuum. One answer, from Figures 13.1-13.6, is that 32 levels might be sufficient for practical purposes; Table 13.1 suggests that in real life the upper bound for most media are in the 400-500 intensities range. If resolution is lower, more intensities may help, while higher resolution can compensate somewhat for having fewer intensities. You can trade resolution for more intensities using so-called half-toning and dithering techniques, especially if you have more resolution than you need, or are willing to stand viewers far away from what they are looking at.

The truth is that humans vary a fair amount in their perception, ranging from those who are blind to those who can see details far more precisely than average. As was mentioned in the discussion of gamma correction, human perception of brightness is on a log scale; we perceive ratios of intensity, not absolute values. Having appropriate gamma correction might affect how many intensities are needed in a given application.

Hue

For those viewers who are not color blind, the thing that determines perception of color is the "dominant wavelength", ranging through the visible spectrum from the (ultra)violet through the (infra)red. The term "hue" is commonly used for this aspect of color. The main thing for me to mention regarding hue is that although the spectrum occupies a linear progression through wavelengths, humans do not perceive this linearity, if they recognize a progression from violet to red it is learned artificially.

Saturation

It is quite rare for you to perceive "pure" light, almost all colors you see are mixtures. Mixtures of specific colors may be averaged out to an intermediate color as a "dominant wavelength", but for any color you see we can generically ask how much white random light is mixed in with whatever dominant colors are to be seen. If the answer is: 0% white light, we would say the color is 100% saturated. If the answer is: 100% white light, we have fully unsaturated light which will be seen as a gray (or white, or black) based on intensity. Saturated colors are very vivid, while the more unsaturated would be considered "washed out" or "pale".

Additive and Subtractive Color Models

The traditional coordinates for colors in some disciplines are HSV for Hue, Saturation, and Value (=Intensity). But the "Hue" coordinate is not very easy to work, so other color models are common. For hardware where colors are formed by adding light together, the dominant coordinates RGB, and the "color cube" makes sense.

For colors that are formed by subtracting (filtering) out of white light from what will be reflected, the colors cyan, magenta, and yellow are subtractive primary colors; they are complements of red (cyan="no red"), green (magenta="no green"), and blue (yellow="no blue"). CMY coordinates are commonly used on color printers. Adding a fourth color (pure black) typically improves the quality and reduces ink costs by giving blacker blacks than the dull gray you get from using all three CMY inks to produce "black" by regular subtraction.

There are other significant color models in wide use; for example color TV signals use a model (YIQ) that emphasizes intensity but also tacks on hue and saturation using a lower amount of bandwidth. YIQ uses twice as much bandwidth for intensity as for all other color information, and is mapped onto both monochrome and color (RGB) TV sets.

lecture #12 began here

Introducing OpenGL

OpenGL is public, standard C language 3D graphics API based on an earlier, proprietary standard developed by SGI. OpenGL is a state machine of sorts, that takes in requests for graphics primitives, processes them through a rendering pipeline, and produces 2D pixel images as output.

OpenGL actually consists of two libraries (GL and GLU), and to use it you must either write window system code yourself (X11 or Win32) or use a third party library such as "glut" to handle window creation and user input. Here is a sample Makefile for compiling an OpenGL program (simple.c) on our local linux environment:

LDLIBS = -L/local/Mesa4.01/lib -lglut -lGLU -lGL -L/usr/X11R6/lib -lXmu -lX11 -lm
INCL = -I/local/Mesa4.01/include -I/usr/X11R6/include
simple: simple.c
	cc -o simple $(INCL) simple.c $(LDLIBS)

In addition to this makefile, it turns out that you will need a LD_LIBRARY_PATH environment variable in order for the system to find your glut shared library. This is set in your .cshrc file with something like:

setenv LD_LIBRARY_PATH /local/Mesa4.01/lib
Note that you may already have an LD_LIBRARY_PATH, and you should just add the Mesa directory to your existing path if you have one.

Compared with the earlier Icon 2D interface we have seen, OpenGL has many more features, and more complexity, for 3D programming. The glut library which interfaces OpenGL to the host window system is simple and easy to use, but limited and restrictive in its capabilities.

Callbacks

Most graphics API's are "event-driven", meaning that an application is written as a set of functions which are called by the window system in response to user input or when other services are required. glut follows this model in a strong sense; callback functions are "registered" with glut, after which glut's main loop owns the control flow of your program. You do not draw your graphics output in your main() procedure, you draw it in a "display callback" function that is invoked whenever the window needs to be redrawn.

Composing graphics primitives

Graphics primitives are generally composed using a series of functions expressed by the pattern:
glBegin glVertex+ glEnd
The glBegin(type_of_object) function specifies what primitive is being depicted, and the glVertex family of functions allow any number of (x,y,z) coordinates in a variety of types and dimensions. lecture #13 (virtual lecture) began here

This lecture is presenting selected material from the OpenGL Primer, chapter 2.

A Bevy of OpenGL and GLUT Functions

In addition to the core graphics functions, there are a lot of helper functions in the OpenGL API; we will present a few more details of these helper functions that you may find useful.
int glutCreateWindow(char *title)
Regarding window creation with glutCreateWindow(), I need to emphasize the point that you can create multiple windows, each call will return a separate integer "identifier". The example simple.c is slightly misleading since it uses old-style K&R C, implying glutCreateWindow() returns void. Since the OpenGL functions don't take a window argument, you can expect to find another helper function down the road which sets which window subsequent calls are directed to, stored in some hidden global variable.
void glutDisplayFunc(void (*func)(void))
As the book emphasizes, your callback takes no parameters, so expect to use a lot of global variables in your opengl programs.
glVertex*
There are 3 X 4 X 2 = 24 versions of this function! It may be moderately inefficient to call this function 100 times in order to specify a single, 100-vertex polygon. You should be looking for whether there is any mechanism to streamline this.
glFlush()
Most graphics systems buffer output even heavily, just as standard file I/O systems do. In particular X11 buffers output to reduce the number of network packets it uses.
glutInitWindowPosition(x,y), glutInitWindowSize(width, height)
Most applications will use these typical convenience functions. They store values in global variables for later use. Call them before glutCreateWindow().

Color in OpenGL

OpenGL supports "RGB" (true color) and "color index" (color map) modes; writing an application that will run on either is a bit awkward, and color index display hardware is vanishing, so it is reasonable to consider only the RGB color mode. Colors are specified in real numbers between 0.0 and 1.0. Some displays (notably, current generation Macs) support degrees of transparency for superimposing multiple images on screen, and use a fourth color component (alpha) where 1.0 means the color is solid and 0.0 means the color is invisible.

Consider now the task of setting the foreground color with which objects are drawn. Although colors are real numbers between 0 and 1, you can use almost any numeric type to supply RGB values; integer values are converted into fractions of the maximum value, so for example many programmers who are used to working with 8 bits each of R, G, and B, can call a function that takes values like (255, 127, 0) and internally this is converted to the equivalent (1.0, 0.5, 0).

So, like the glVertex family, there are many (28) functions in the families that set the foreground color, glColor*. An example call would be: glColor3f(1.0, 0.5, 0.0). Apparently they didn't bother to make 28 functions for setting the background color with glClearColor(), because that operation is far less common.

This discussion of color is well and good, but tragically it all becomes meaningless as you transition from "toy" programs to more realistic ones, because once you introduce lighting into your application, glColor*() has no effect! When lighting is introduced, the color of objects becomes a function of the color of light and the reflective properties of the objects, specified by "materials". We will see lighting in detail a little later.

Cameras, take one

OpenGL allows you good control over what "window on the world coordinates" will be visible in your scene. In the general case, you will be able to specify viewing from an arbitrary (x,y,z) location, in an arbitary (x,y,z) direction, as well as how wide and high your field of view is. Section 2.8 mentions the 2D special case of this, gluOrtho2D(x1,x2,y1,y2); if you ever use it, beware the surprising parameter order, and note that OpenGL's world coordinates are based on classic cartesian coordinates, not the typical physical coordinates in which y grows going down.

OpenGL Transformation Matrices

The translation, scaling, and rotation used in converting objects' world coordinates to their on-screen pixel locations is done automatically by OpenGL. Two transformation matrices are maintained by OpenGL (in global variables) and combined to perform rendering: the model-view matrix and the projection matrix. The same set of functions are used for manipulating both matrices, so you start by specifying which matrix you are working on, by calling glMatrixMode(GL_PROJECTION) (or GL_MODELVIEW).

When you use gluOrtho2D() you are manipulating the projection matrix. Functions like gluOtho2D modify (i.e. to a matrix multiply into) whatever is in the matrix already, and if you want to start from a known position, you need to reset the matrix to the identity matrix with glLoadIdentity(), so the complete code to specify a 2D window on the world in OpenGL looks like

glMatrixMode(GL_PROJECTION);
glLoadIdentity();
gluOrtho2D(x1,x2,y1,y2);
Later on, we will see that you frequently compose these matrices, especially the model view matrix, while drawing a complex scene. From one transformation cooresponding to a given "parent" coordinate system, if you have several "child" coordinate systems whose coordinates are relative to the parent, each child will need to save the parent transformation, make changes and then draw their objects, and then restore the parent. When object hierarchies are multiple levels deep, a special type of stack is natural. OpenGL provides a built-in stack mechanism, in which the "push" operation copies the top stack element, to which the child may then apply transformations for their coordinate system relative to the parent coordinates. glPushMatrix() and glPopMatrix() are used in this way, and they operate on whichever current matrix you have specified using glMatrixMode().

More OpenGL Graphic Primitives

Points
GL_POINTS are not usually very interesting in 3D graphics, but opengl supports them and even has a "point size" attribute (set by function glPointSize()) that says how large (in pixels) plotted points should appear. Are points round, or square? Are they really spheres in a 3D scene? The fact that their "size" is given in pixels is at odds with the world coordinates used to specify position, so you can be suspicious that points are a "special case" hack in OpenGL.
Lines
There are three opengl primitives for depicting lines, similar to the two primitives used in Icon's 2D graphics. GL_LINES draws disconnected segments between pairs of points, similar to Icon's DrawSegment(), while GL_LINE_STRIP draws lines between every adjacent pair of points, forming a single connected object. GL_LINE_LOOP also the first and last points specified, for people too lazy to repeat the first point at the end. Lines have a color, a thickness (glLineWidth()), and a so-called stipple pattern that let's you specify dashes or dots in the lines you draw. But, you can only use these line styles if you call glEnable(GL_LINE_STIPPLE).
Triangles, Triangle Strips, Triangle Fans
Triangles get a lot of attention in 3D graphics because they are the smallest number of points necessary to specify a portion of a solid object's surface. 3D graphics hardware supports the rendering of triangles directly, while more complex surfaces are usually composed of many many (possibly millions) triangles. In addition to GL_TRIANGLES, are are cleverly optimized ways to specify sequences of physically connected triangles without having to specify the vertices they have in common multiple times. The net effect is only having to specify one new vertex per triangle. GL_TRIANGLE_STRIP connects each vertex with the previous two, while GL_TRIANGLE_FAN connects each vertex with the previous one, and the first vertex.
Quads and Quad Strips
Four vertex objects are also common, and may be rendered in hardware or broken up into triangles for rendering. GL_QUAD_STRIP defines each new quad using the last two vertices and two new vertices, costing new vertices per quad. As an afterthought, OpenGL also supports conventional 2D rectangles with the glRect* family.
Polygons
GL_POLYGON is the most general of these primitives, and is typically broken down into triangles during rendering. It is analogous to FillPolygon() in Icon's graphics. It is worth mentioning a few extra items relating to polygons. If their vertices cross, OpenGL's semantics are not guaranteed, implementations can handle them however they please. And convex polygons are MUCH easier and faster to render. If you use multiple colors in the middle of a glBegin/glEnd pair, opengl will use interpolation to gradually change the colors. You can specify glShadeModel(GL_FLAT) if you want each primitive to be a single color instead of an interpolation of the varying colors of its vertices.

Viewports

Within your window, you can select subregions and use them for different purposes; for example, to render simultaneous views looking forward and backward to provide "eyeballs in the back of your head".

glViewport(x, y, w, h) maps the current projection onto a specific region within the selected window. lecture #14 began here.

Reshape and Idle callbacks

One typical way to do animation is to modify some global variables inside the idle callback function, and then call glutPostRedisplay() to cause your display callback to be called. The book's example rotates a square by two degrees each time the idle callback runs. If you want rotation to achieve a constant velocity independent of hardware capabilities, say 30 degrees per second, you'd need to make the angle increment a variable, and run some timings to see how many frames per second your computer is achieving. From our mass media, we know 24 (movies) or 30 (tv) frames per second appears "smooth" to most viewers.

Keyboard and Mouse

Many separate callbacks are used to handle keyboard and mouse input. Both callbacks supply (x,y) mouse location at the time of the input. Keyboard callback has a single integer to say what key is pressed, and causes to you use auxiliary functions to try and detect special key combinations such as the ALT key. The main mouse callback has a separate integer codes to indicate which button and whether it was a press or release. Separate callbacks are used to request mouse motion when a button is pressed (a "drag") and when no button is pressed ("passive motion"). lecture #15 began here

Display Lists

A display list is a collection of off-screen invisible OpenGL objects, stored on the server in a format that can be rapidly (re)displayed. They are created and accessed by integer handles. Display lists may be called from within other display lists.
glNewList(myhandle, GL_COMPILE);
glPushAttrib(CL_CURRENT_BIT);
glColor3f(1.0,0.0,0.0);
glRectf(-1.0,0.0,0.0);
glPopAttrib();
glEndList();
...
glCallList(myhandle);

Selection Mode

To select an object in a 3D scene, you rerender the scene in "selection mode", not writing to the frame buffer but labeling each object being rendered with an integer code, with the clipping set closely around the mouse click so that only objects on/near the mouse are rendered. Most of this work is done in the mouse callback function.

void mouse(int button, int state, int x, int y)
{
...
glInitNames();
glPushName(0);
glSelectBuffer(SIZE, nameBuffer);
glGetIntegerv(GL_VIEWPORT, viewport);
glMatrixMode(GL_PROJECTION);
glPushMatrix();
glLoadIdentity();
gluPickMatrix((GLdouble)x,(GLdouble)(viewport[3]-y),N,N,viewport);
gluOrtho2D(xmin,xmax,ymin,ymax);

draw_objects(GL_SELECT);
glMatrixMode(GL_PROJECTION);
glPopMatrix();
glFlush();

hits = glRenderMode(GL_RENDER);
processHits(hits, nameBuffer);
glutPostRedisplay();
}
The display callback function becomes trivial, because we have moved code to a helper function called to display or to check for hits:
   glClear(GL_COLOR_BUFFER_BIT);
   draw_objects(GL_RENDER);
   glFlush();
The draw_objects function does the graphics functions, but "labels" each selectable object with an integer code:
void draw_objects(GLenum mode)
{
   if (mode == GL_SELECT) glLoadName(1);
   glColor3f(1.0,0.0,0.0);
   glRectf(-0.5,-0.5,1.0,1.0);
   if (mode == GL_SELECT) glLoadName(2);
   glColor3f(0.0,0.0,1.0);
   glRectf(-1.0,-1.0,0.5,0.5);
}
The processHits function considers all objects within N pixels (N was specified in gluPickMatrix) of the user click.
void processHits(GLint hits, GLuint buffer[])
{
   unsigned int i, j;
   GLuint names, *ptr;
   printf("hits = %d\n", hits);
   ptr = (GLuint *) buffer;

   for (i = 0; i < hits; i++) {
      names = *ptr;
      ptr += 3; /* skip over number of names and depths */
      for (j = 0; j < names; j++) {
         if (*ptr == 1) printf("red rectangle\n");
         else printf("blue rectangle\n");
         ptr ++;
      }
   }
}

Cameras

OpenGL's synthetic camera model emulates real world imaging. 3D images within a particular view volume are projected onto a 2D surface, as viewed from a particular center of projection. 6 degrees of freedom specify the camera position and where it is looking. Three angles specify what is up. Six additional values specify the viewing volume.
orthographic projection
camera is at infinity, projection plane is near the objects
glOrtho(left,right,bottom,top,near,far)
3D orthographic projection defines a viewable box
gluLookAt(ex,ey,ez,ax,ay,az,ux,uy,uz)
Specify eye position and orientation.
gluPerspective(yangle, aspectratio, near, far)
3D perspective projection, defines a viewable box
Notice that glOrtho, and gluPerspective, use "camera coordinates" not world coordinates. "near" and "far" are distances from the camera. lecture #16 began here

Building objects

Graphics objects may be composed using either code or data.
void cube()
{
  glColor3f(1.0,0.0,0.0);

  glBegin(GL_POLYGON);
  glVertex3f(-1.0,-1.0,-1.0);
  glVertex3f(-1.0,1.0,-1.0);
  glVertex3f(-1.0,1.0,1.0);
  glVertex3f(-1.0,-1.0,1.0);
  glEnd();
  /* other 6 faces similar) */
}
Generally it will be preferable to store the graphics in a data structure (array, list, tree) and write code that walks the structure.

Hidden Surface Removal

There are several approaches to drawing only the things that should be visible in a 3D scene. The "painter's algorithm" says to sort the objects by distance from the camera, and draw the farther things first, and the nearer ones on top ("painting over") the farther ones. This approach may be too inefficient (all that "wasted paint" corresponds to wasted CPU cycles and/or awkward visual effects), and is a poor match for the graphics rendering pipeline. We need an approach that removes the hidden parts mathematically before submitting primitives for rendering.

OpenGL uses z-buffers, or depth buffers, which are extra memory buffers, to track different objects' relative depths. This is built-in, but you have to turn it on to get its benefits. Also, if any of your objects are see-through, you will need to read more details on z-buffering in the OpenGL references.

glutInitDisplayMode(GLUT_RGB|GLUT_DOUBLE|GLUT_DEPTH);
glEnable(GL_DEPTH_TEST);
...
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

GLU and GLUT objects

Among the available graphics primitives, OpenGL offered us lines, triangles, quads, and polygons. These are sufficient for any scene, but not very convenient for certain common shapes. GLU adds to these: spheres, cylinders, and disks. GLUT adds cones, toruses, tetrahedrons, octahedrons, dodecahedrons, icosahedrons, and a teapot. We may look at some of these more complex primitives later after introducing lighting and textures, since the structures used to manipulate these primitives in some cases use lighting and texture information.

OpenGL Transformations

The early sections of OpenGL Primer Chapter 5 review 3D transformations that we've seen earlier. Transformations are built-in to OpenGL, greatly simplifying the programmer's job. Here are some highlights.

Hierarchical Model Example

Consider Figure 5.9, it is representative of many many complex 3D objects composed from simpler pieces. A robot arm is an aggregate of: a base, a lower arm piece, and an upper arm piece. We could render all three pieces independently, but we'd be losing something: they are attached and have some common points and relative positions. We can infact have a mixture of local per-object transformations and relative/shared/accumulative transformations.
void base()
{
   glPushMatrix(); /* make our local copy */
   glRotatef(-90.0, 1.0, 0.0, 0.0);
   gluCylinder(p, BASE_RADIUS, BASE_RADIUS, BASE_HEIGHT, 5, 5);
   glPopMatrix();
}
void lower_arm()
{
   glPushMatrix(); /* make our local copy */
   glTranslate(0.0,0.5*LOWER_ARM_HEIGHT,0.0); /* translate to our center */
   glScalef(LOWER_ARM_WIDTH, LOWER_ARM_HEIGHT, LOWER_ARM_WIDTH);
   glutWireCube(1.0);
   glPopMatrix();
}
void upper_arm()
{
   glPushMatrix(); /* make our local copy */
   glTranslate(0.0,0.5*UPPER_ARM_HEIGHT,0.0); /* translate to our center */
   glScalef(UPPER_ARM_WIDTH, UPPER_ARM_HEIGHT, UPPER_ARM_WIDTH);
   glutWireCube(1.0);
   glPopMatrix();
}
void display()
{
   glClear(GL_COLOR_BUFFER_BIT);
   glMatrixMode(GL_MODELVIEW);
   glLoadIdentity();
   glColor3f(1.0,0.0,0.0); /* isn't this poor way to say "red" ? */  
   glRotatef(theta[0], 0.0, 1.0, 0.0);
   base();
   glTranslatef(0.0,BASE_HEIGHT,0.0);
   glRotate(theta[1], 0.0, 0.0, 1.0);
   lower_arm();
   glTranslatef(0.0,LOWER_ARM_HEIGHT,0.0);
   glRotatef(theta[2], 0.0, 0.0, 1.0);
   upper_arm();
   glutSwapBuffers();
}
What about more complex multi-piece objects such as the running man in Figure 5.10? With the right combination of pushes and pops, code similar to the above example would work... but its much cooler to do it as a tree traversal:
typedef struct treenode {
  GLfloat m[16];
  void (*f)();
  struct treenode *sibling, *child;
} treenode;

void traverse(treenode *root)
{
   if (root == NULL) return;
   glPushMatrix();
   glMultMatrix(root->m);
   root->f();
   traverse(root->child);
   glPopMatrix();
   traverse(root->sibling);
}

void display()
{
   glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
   glMatrixMode(GL_MODELVIEW);
   glLoadIdentity();
   traverse(torso_root);
   glutSwapBuffers();
}

where

treenode *torso_root = malloc(sizeof(treenode));
torso_root->f = torso;
glLoadIdentity();
glRotatef(theta[0], 0.0, 1.0, 0.0);
glGetFloatv(GL_MODELVIEW_MATRIX, torso_root->m);
torso_root->sibling = NULL;
torso_root->child = head_node;
... etc.

lecture #17 began here

Lighting

OpenGL incorporates a sophisticated lighting model, with different kinds of light, and different types of reflective textured and colored materials. To get reasonable realism in a scene, we have to calculate all the lights and all the reflections in the scene for every single pixel, this is why computer generated movies may use hundreds or thousands of processors to compute each frame.

In OpenGL, light calculations are done on a polygon by polygon basis. To get more realistic shading, break your objects into more polygons.

Phong Model

Diffuse reflection
These are dull surfaces that spread their light fairly evenly. They absorb some of the light (different percents for R, G, and B)
Specular reflection
Shiny surfaces reflect strongly along the angle of reflection; how much light reaches the viewer depends on whether they are on or near the angle of reflection or not.
Ambient reflection
Ambient light is light from so many sources and so many reflective surfaces that it doesn't appear to have any particular direction to it.
Emission
Light sources themselves may be visible in a scene, and they may reflect light in addition to whatever light they themselves emit.

OpenGL Light Sources

OpenGL has point sources, spotlights, and ambient sources, and has a limited number of them (like, at most 8). In any given scene you are approximating the lighting, possibly rather crudely, not modeling it as precisely as you model the world objects. Each of the (8) light sources can have a combination of diffuse, specular, and ambient light parameters that it applies to those objects that will be visible to the viewer. Each object will have a "material" that includes corresponding parameters for its reflective properties.
glEnable(GL_LIGHTING);
glEnable(GL_LIGHT0);
Once you enable lighting, all the glColor*() calls you've been doing will be ignored...what you see will depend on materials and light sources. The lighting model also uses a normal vector, which is not autocalculated by OpenGL, but rather, you set by calling glNormal3*(dx,dy,dz)

To specify a light source, you call glLight*(light, param, value) where light is an integer code (which light) param is what to set, and value is the value for that param. For example, the (x,y,z) location of light 0 would be

GLfloat a[] = {1.0, 2.0, 3.0, 1.0};
glLightfv(0, GL_POSITION, a);
To setup a light, you may see a lot of calls to glLight*(), besides position you can setup GL_DIFFUSE, GL_SPECULAR, GL_AMBIENT properties, etc. There are defaults for these values so that it is easy to setup a simple lighting (single source, white, bland) model.

Materials

Specifying materials properties is a lot like specifying light source properties. There is only one "materials" attribute internal to OpenGL, which is used for subsequent objects until you change materials properties to something else.

typedef struct material {
  GLfloat ambient[4];
  GLfloat diffuse[4];
  GLfloat specular[4];
  GLfloat shininess;
} material;

material redPlastic = {
  {0.3, 0.0, 0.0, 1.0},
  {0.6, 0.0, 0.0, 1.0},
  {0.8, 0.6, 0.6, 1.0},
  32.0
} rp;
...
glMaterialfv(GL_FRONT_AND_BACK, GL_AMBIENT, rp.ambient);
glMaterialfv(GL_FRONT_AND_BACK, GL_DIFFUSE, rp.diffuse);
glMaterialfv(GL_FRONT_AND_BACK, GL_SPECULAR, rp.specular);
glMaterialf (GL_FRONT_AND_BACK, GL_SHININESS, rp.shininess);
glNormal3f(nx, ny, nz); /* unit normal appropriate for this object */
glBegin(...);
...red plastic object
glEnd();
lecture #18 began here

Images (chapter 7)

Main reason we need to manipulate 2D images in 3D applications: textures described in Chapter 8. Other primary use: drawing text.

There is a "raster position", or pixel cursor, set using glRasterPos(), and a function

 glBitmap(height, width, x, y, x2, y2, bits)
which draws a bitmap. The main use of this facility is to draw text. The "raster position" is transformed by model-view and projection matrices to yield a screen coordinate. glBitmap uses the current "raster color", as set by glColor*(), which sets both raster color and drawing color.

Reading and Writing Pixels

glDrawPixels(height, width, GL_RGB, GL_UNSIGNED_BYTE, imagebits)
draws rectangles at the current raster position. There are several binary formats available besides GL_RGB, and several types besides GL_UNSIGNED_BYTE.

glReadPixels(0,0,rows,columns,GL_RGB,GL_UNSIGNED_BYTE, imagebits)
performs a semi-inverse operation. Note that if you write an image, and read it back in, you will not always get back identical bits, that depends on the actual display hardware and how much is "lost in translation".

glCopyPixels(x,y,w,h, GL_COLOR)
performs a "bit blit" to the current raster position of the rectangle given by (x,y,w,h). Blitting to other buffers besides the frame buffer (GL_COLOR) may be possible. Depending on the hardware, there may be a depth buffer, front and back buffers (a la glSwapBuffers) and on higher powered machines, stereo images for 3D eyegoggles, etc.

Intro to Texture Mapping

glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, 64, 64, 0, GL_RGB, GL_UNSIGNED_BYTE, imagebits);

Textures are applied to geometric primitives by defining the mapping between texel coordinates and vertices:

glBegin(GL_QUADS);
glTexCoord2f(0.0, 0.0);
glVertex3f(vertex[0]);
glTexCoord2f(0.0, 1.0);
glVertex3f(vertex[1]);
glTexCoord2f(1.0, 1.0);
glVertex3f(vertex[2]);
glTexCoord2f(1.0, 0.0);
glVertex3f(vertex[3]);
glEnd();
Some parameters are required in order to define the semantics of textures fully.
glTexParameter*(target, name, value)
is used to set texture mapping parameters.
glTextParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
glTextParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
sets things up so the texture "wraps" (repeats) lecture #19 began here

Texture Examples

We spent this lecture on the texture mapping example in section 8.5, and modifying it to (a) distort the mapping and (b) modify the texture image to include a "face" on it. We noted the significant amount of color blending that interacted with the colors in the texture image. And we concluded that we should look at another example in future, in which the image comes from a file.

lecture #20 began here

If we set the texture to replace, rather than mix, with the vertex colors, we will get an image that is closer to what we expected:

 glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_REPLACE); 

Width and height of a texture must be powers of 2! You should set your source (.jpg, .gif, whatever) to be a power of 2 in the first place, but there is a glScaleImage() function that you can use to changes its dimensions if it is not.

Mipmaps

A high-resolution texture will be wasted on a far away object. We can create several different versions of the texture by averaging blocks of pixels (say, 4 at a time) to form a smaller image. We can install each of these textures simultaneously using different "levels". We can then enable mipmapping to automate the process of selecting which resolution of texture to use. To enable mipmaps:
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST_MIPMAP_NEAREST);
to build and install a set of texture images based on your original source image, instead of a single-level texture, call gluBuild2DMipmaps() instead of glTexImage2D().

Texture Coordinate Generation

The hardest part of using textures us mapping texture coordinates to vertices. Distortions are a normal part of the process, but some distortions look unacceptably funny. For example, using the same texture on different polygons may scale the texture differently based on the size of the polygon. For those objects defined by the GLUquadricObj struct, a reasonable default behavior can be obtained by:
 gluQuadricTexture(GLUquadricObj *obj, GLboolean); 
There is a "lower level" automatic texture coordinate generation mechanism that will work for arbitrary objects. You can define an equation for computing the texture coordinate from the xyz space coordinates:
GLfloat plane_s = {0.5, 0.0, 0.0, 0.5};
GLfloat plane_t = {0.0, 0.5, 0.0, 0.5};
...
glEnable(GL_TEXTURE_GEN_S);
glEnable(GL_TEXTURE_GEN_T);
...
glTexGeni(GL_S, GL_TEXTURE_GEN_MODE, GL_OBJECT_LINEAR);
glTexGeni(GL_T, GL_TEXTURE_GEN_MODE, GL_OBJECT_LINEAR);
...
glTexGenfv(GL_S, GL_OBJECT_LINEAR, plane_s);
glTexGenfv(GL_T, GL_OBJECT_LINEAR, plane_t);

Texture Objects

You can within limitations store your textures on the server, avoiding reloading them each time you have to redraw your scene. Textures are identified by integer handles. On current hardware, you may have to carefully consider which textures are used so frequently that they should be placed into its texture memory in this fashion.

To get a "handle" in which you can place a texture.

glGenTextures(1, &i);

To assign the current texture to handle i:

glBindTexture(target, i);
If i has not been defined yet, subsequent texture calls define it. If i has been previously defined, opengl switches to it and uses it for subsequently drawn objects.

lecture #21 began here

Curves and Surfaces

As was the case for some of the 2D curves seen earlier, curves in OpenGL are based on parametric equations. Parametric equations are also used to define curving surfaces. You may wish to review Foley/Van Dam/et al for the mathematics of curves, it is presented in more detail there than in the OpenGL Primer.

Evaluators

OpenGL uses evaluators to implement a particular category of curves and surfaces, namely Bezier curves and surfaces. Evaluators are used to interpolate points along Bezier curves/surfaces at any desired resolution, and generate OpenGL primitives from them.

For curves, this is done with glMap1f(entity, u0, u1, stride, order, data). A subsequent call to glEvalcoord1*() replaces calls to glVertex*(). Example:

glBegin(GL_LINE_STRIP);
   for(i=0;i<20;i++) glEvalCoord1f(u0 + i * (u1-u0)/ 20.0);
glEnd();
This approximates the curve with 21 points between u0 and u1. Note instead of (x,y,z) coordinates, glEvalCoord1f() just takes a value of u and passes it to all enabled evaluators, which call e.g. glVertex.

Evaluators can be used for curves, surfaces, normals, colors, and textures. For any of these to work, you have to enable them e.g. glEnable(GL_MAP1_VERTEX_33)

For equally spaced values of u (such as the 21 values above) there is special support via:

glMapGrid1f(21,u0,u1);
glEvalMesh1(GL_LINE, 0, 20);

2D Evaluators

glMap2*(entity,u0,u1,ustride,uorder,v0,v1,vstride,vorder,data) is the 2D analog to glMap1 for surfaces. There is a cooresponding glEvalCoord2*(u,v) for calling the evaluator, and glMapGrid2*() and glEvalMesh2() for regularly-spaced intervals over u and v.

Interactive Curve Example

Let's look at curve.c

Other Curves

Actually, we can play with the polynomials and draw different kinds of curves. lecture #22-2X began here

There were a few class periods devoted to examples, in particular, there were texture and lighting examples. We grabbed a chainmail texture off a random image in a random website, converted it into a PPM and read the PPM and used it in an OpenGL program. And we looked at the "lighting lab" example, which also demonstrated several materials. We looked at a student example which moved the camera point (eye position) and played with orthographic versus perspective projection. lecture #2X+1 began here

Program Visualization

PV is the use of graphics to depict aspects of programs, especially behavior. PV tools are used for debugging, understanding, maintaining and improving an existing system, and for educational purposes. PV tools may be geared towards a single algorithm or aspect of a program, a whole program, or a large software system. They may visualize static information about the program, or dynamic execution-based information. The best tools combine static and dynamic information.

The big problem which PV addresses, compared with textual techniques, is the Volume Problem. You can capture most any aspect of program behavior textually, but the resulting log files easily grow to megabytes and beyond, for all but the smallest toy programs.

In addition to using graphics to deal with volume, PV tools must also solve two other hard problems: intrusion (observing some behavior modifies it), and access to program behavior.

Kinds of Program Visualization

There are several different kinds of program visualization tools, including

Biggest needs

What does program visualization build on?

Maps (5000+ years)
Statistical graphs (350 years)
The early work in this area relies on analogies to the physical world.
Data graphics (200 years)
More abstract, relational views of data (scatterplot, bar chart, pie chart, etc.)
Visualization (20 years)
Scientific visualization, modern computer graphics. Low resolution and small screenspace. Animation, color, and sound.

Principles of Graphic Excellence

Graphic designs should tell the truth about a complex situation using multiple variables. Give viewer the greatest # of ideas, in the shortest time, using the least "ink", in the smallest space possible.