3-D Silhouette-Finding in OpenGL

If we want to find silhouettes as we draw objects, rather than as a post-process on the z-buffer, then we should check all of the edges to see if either (a) one adjacent face is front-facing and the other is back-facing, or (b) the edge has only one adjacent face. To test if a face is front-facing, we need a vector from a point on that face to the camera and a normal vector. The face is front-facing iff the dot product of those two vectors is positive. The above algorithm sounds easy, but it's made more complicated by the need to transform between coordinate systems.

The formulas we'll develop here are also useful for doing our own lighting calculations, or for calculating the screen locations and sizes of things.

A Review of OpenGL Coordinate Systems

OpenGL's glViewport call controls the transform from view space to screen space. OpenGL's projection matrix is the transform from world space to view space. Sometimes this matrix is fixed for all time, but in some programs (if camera moving) it changes every frame. It is an affine matrix (last row (0,0,0,1)) for parallel projection, but not for perspective projection. OpenGL's modelview matrix is the transform from object space to world space ("modelview" is really a misnomer; typically this matrix has nothing to do with viewing). The modelview matrix typically changes from object to object, and also from frame to frame, if objects are moving. It is almost always affine.

Pick a Common Coordinate System

We need to transform everything to a common coordinate system for these silhouette tests. The choice is arbitrary, but let's use world space. So we'll need to transform polygon points and normals from object space to world space, and transform the camera from screen space to world space. To compute these transformations, we could maintain our own copies of the transformation matrices in parallel with OpenGL, but that would be redundant and error-prone. Instead, we can use glGet to query OpenGL's state to read these matrices.

Transform the Camera Position

The camera is at (0,0,-infinity) in screen space, by definition. That's equivalent to the homogeneous point (0,0,-1,0) or (0,0,1,0). (In projective geometry, -infinity=+infinity). If we let Then to find the camera position in world space, we take the position of the camera in screen space, (0,0,1,0), and transform it to world space. Using the SVL library
Vec4 cw4 = Msw*Vec4(0,0,1,0);	// matrix times column vector
Vec3 cw = proj(cw4);		// homogeneous divide
or simply
Vec3 cw = proj(Msw*Vec4(0,0,1,0)); // camera position in world space
For corroboration of this formula, see Trace::trace(x,y) in a3/raytrace/trace.cc . Note that this formula won't work if Msw is affine (uses parallel projection, not perspective). If we used it in that situation, we'd end up dividing by zero. I'll let you work out the correct formula for that case.

Recall that in SVL, if M is a matrix and v is a vector, M*v means matrix times column-vector, and v*M (which we'll use later) means row-vector times matrix. SVL regards the vector as either a row or column depending on context.

Recovering the Screen-to-World Transform

But where do we get Msw? In OpenGL, the world-to-view transform is the transpose of the projection matrix:
Mat4 Mwvt;
glGetDoublev(GL_PROJECTION_MATRIX, Mwvt.Ref());
// need the .Ref() because Mat4 is a high level 4x4 matrix class and
// glGetDoublev in this context wants a ptr to 16 consecutive doubles
Mat4 Mwv = trans(Mwvt);		// world-to-view transform
Recall that OpenGL stores all of its matrices transposed relative to standard form (an unfortunate anachronism).

We can compute the view-to-screen transform easily ourself:

Mat4 scale, tran;
scale.MakeHScale(Vec3(width/2., height/2., 1.));
tran.MakeHTrans(Vec3(width/2., height/2., 0.));
Mat4 Mvs = tran*scale;		// view-to-screen transform
width and height are the width and height of the window, in pixels. (In the subsurf code from Assignment 2 they can be found in the View class). If our code calls glViewport, our view-to-screen transform will be consistent with the above. If not, try glGetIntegerv(GL_VIEWPORT...) to recover the viewport parameters.

The world-to-screen transform is then simply Mvs*Mwv. As a shortcut, the screen-to-world transform can be calculated directly, without Mwv or Mvs:

(get Mwvt, calc. scale & tran as above)
Mat4 Msw = inv(tran*scale*trans(Mwvt)); // screen-to-world transform

Transform a Point

Next, we need to compute a vector from a point on the polygon to the camera. To do that, we'll first transform an object point from object space to world space, then take the difference of the camera position (in world space) and the object point position (in world space).

We'll need two more matrices:

The first of these is simply the transpose of OpenGL's modelview matrix:
Mat4 Mowt;
glGetDoublev(GL_MODELVIEW_MATRIX, Mowt.Ref());
Mat4 Mow = trans(Mowt);
And the latter, Mwo, is the inverse of Mow.

Transforming the object space point po to world space is easy and familiar:

Vec3 po;				// point in object space
Vec3 pw = proj(Mow*Vec4(po,1));	// point in world space
Since po is a point (not a direction) we add homogeneous coordinate 1, transform by the 4x4 object-to-world matrix. Don't forget to do the homogeneous division after transformation. If Mow is affine (as it is, typically), we'll end up dividing by 1, so simply extracting the first three coordinates suffices in that case.

The direction of the camera from the point, in world space, is then cw-pw.

Transform a Normal

We also need to transform face normals for backface testing. Given a face normal "no" in object space, should we transform to world space it like this: proj(Mow*Vec4(no,1))? NO, way off! How about like this (changing the 1 to a 0): proj(Mow*Vec4(no,0))? AGAIN, NO! The latter formula only works in the unlikely event that Mow is a rotation only.

It turns out the correct formula to transform a normal to world space is:

Vec3 no;				// normal in object space
Vec3 nw = first3(Vec4(no,0)*Mwo);	// normal in world space
Above we're multiplying a row vector times a matrix. Where does this formula come from? Recall that transforming a normal (or a plane) requires multiplication of the coefficients of the plane (a,b,c,d) or the normal (a,b,c,0) on the right by the inverse of the point transformation matrix, or equivalently, multiplication on the left by the inverse-transpose. In the 15-462 notes on transformations, see the slides on transforming normals. This is important stuff!

The hypothetical subroutine first3() used above simply extracts the first three elements of a Vec4 to form a Vec3. The fourth coordinate, the plane offset coefficient d, is irrelevant to no and nw in the (typical) case that Mwo is an affine transformation, since parallel planes transform to parallel planes through an affine transformation. Note that it would be inappropriate to do a homogeneous division here, since that could change the sign of the normal coefficients and flip the normal's direction. For the purposes of backface testing, the lengths of no and nw are irrelevant; we need not normalize those vectors.

Putting it All Together

Given all of the above (whew!) we simply take the dot product of cw-pw and nw to determine if the face is front-facing or not. Finding silhouettes is easy once we have the above details worked out.

The above can be optimized by calculating matrices only when they change and caching transformed points and normals.

As an aside: once we've got Mow and Mws, we can simulate gluProject(), which transforms points from object space to screen space. Do that with Vec3 ps = proj(Mws*Mow*Vec4(po,1));

15-463, Computer Graphics 2
Paul Heckbert, 26 Apr 2001