This is an optional step assignment. If you choose to complete it, it will be
due April 28, 2008 at 11:49pm. If you choose to complete this step
assignment, you can use this assignment to replace the lowest score in your step
assignments.
There are two programming sections in this
step. The first will consist of the creation of a Bezier surface
patch. The second will consist of the creation of a NURBS surface.
For the Bezier patch, I have created a simple program that you will work
with. This program creates the 16 points for a Bezier surface patch and
lets you play with them. You'll have to add some code to make these
display a Bezier surface patch. Then, you are going to create a second
patch that exhibits G1 continuity with the first patch.
But, back to the programming task, for this step part A you'll be working
with some code I've created. You'll find this code in step9a.zip. This code displays a flat surface.
There's a "patch" menu. If you select the new option, 16 blocks
appear. These are the control points for a Bezier curve surface
patch. You can use the mouse to spin the scene around. Then, you can
right-click on any control point to select it. If you hold down the
right mouse button, you can drag the point. Be sure you are familiar with
this operation before proceeding.
|
You may notice that the mouse moves the block in the X,Y directions relative to the screen, no matter which way you view the scene from. If you are curious as to how this feat is accomplished, take a look at the mouse move handler function. There you will find an interesting function called gluUnProject. This seemingly miraculous function converts a 2D point on the screen back to a 3D point in space. Of course, for any 2D point on the screen there are in infinite number of 3D points. We resolve the ambiguity by providing a depth to the function. We can then move the blocks anywhere we want, provided the depth of the block remains the same. If you have questions about how this functionality works, feel free to ask. There are a few other fun things in this problem you may be interested in. We use GL_QUAD_STRIP. It's worth looking up what it does. |
I've described a patch this way in ChildView.h:
struct Patch
{
GLfloat p[4][4][3];
};
This struct is private to CChildView. There 3 dimensional array is: the u dimension, the v dimension, and 3 values for x,y, and z. So, when you see the first 16 control points, the front row are defined in p[0][0], p[1][0], p[2][0], and p[3][0] from left to right. The u dimension goes to the right in my implementation and the v dimension goes to the back (-Z direction). The array m_patches contains any patches we have defined this way. It's initially empty. CChildView::OnPatchNew() defines code for creating a new patch. My initial patch is very simple.
In CChildView::OnGLDraw() there is a loop that loops over the patches we have defined. Here's the code to create a new patch:
Patch p;
for(int u=0; u<4; u++)
{
for(int v=0; v<4; v++)
{
p.p[u][v][0] = GLfloat(u * 8. / 3.);
p.p[u][v][1] = 1;
p.p[u][v][2] = GLfloat((3-v) * 8. / 3.);
}
}
m_patches.push_back(p);
The mechanisms for creating curves such as Bezier curves in OpenGl are called evaluators. They work by providing a set of points we will use to evaluate, defining a range for the u,v values (often 0 to 1), defining how the curve will be broken into quadrilaterals, and drawing the curve faces. Note that OpenGL does not draw curves at all. It just provides a way for you to translate a curve to quadrilaterals (or triangles).
If you look in the function CChildView::ActualDraw(), you'll find a comment indicating where you will put the code to draw your surface:
//
//
// INSERT PATCH RENDERING HERE
//
//
Right before this that I've provided some lines that will make your surface transparent. I do this so you can still see the control points even when they are below the surface. If you've been wondering how to make something transparent, here's a simple example.
The first line we'll add is this:
glEnable(GL_AUTO_NORMAL);
This tells OpenGL that we want it to compute the surface normals for our curve for us. Because we are using lighting, we have to have normals. But, the normals are dependent on the underlying curve functions. This just tells OpenGL to do this work for us.
Now, add the following code:
glMap2f(GL_MAP2_VERTEX_3, // 2 variable surface, 3d points
0.0f, 1.0f, // u is 0 to 1
12, // Number of values between u points
4, // u axis dimension
0.0f, 1.0f, // v is 0 to 1
3, // Number of values between v points
4, // v axis dimension
&m_patches[i].p[0][0][0]); // The control point data
This is setting up the connection to the control points for OpenGL. The first parameter says we are using 3D vertices over a surface with two control variables, u and v. The next three lines define characteristics of the u dimension. We are indicating that u will be over the range 0.0 to 1.0. Then, we tell how far apart new u values are in the data. Our array is p[4][4][3], which the highest dimension being u, so there are 4 * 3 floating point values between u data points. Hence, the 12. 4 is simply how many points there are.
We do the same thing for v. Then, we pass a pointer to the control point data. Note that OpenGL expects that data to be contiguous. So, you can't use an array of CGrPoint objects here.
Now, enable the evaluator:
glEnable(GL_MAP2_VERTEX_3); // Enable the evaluator
Next, we are going to draw the surface using quadrilaterals. OpenGL has a feature called GL_QUAD_STRIP. The idea is that you can specify two initial points, then every new two points, in combination with the last two points, defines a quadrilateral. Because a vertex for a corner that is shared by two quads is only defined once, this is faster and more efficient.
We have to decide how many strips we'll use and how many quadrilaterals we'll put in a strip. I'll break this into a 30 by 30 surface:
int divide = 30;
// Now, we loop for each strip and for each element in each strip:
for (int j = 0; j < divide; j += 1)
{
glBegin(GL_QUAD_STRIP);
for (int i = 0; i <= divide; i += 1)
{
glEvalCoord2d(i / double(divide), (j+1) / double(divide));
glEvalCoord2d(i / double(divide), j / double(divide));
}
glEnd( );
}
glDisable(GL_BLEND);
glDisable(GL_TEXTURE_2D);
}
Normally we would have had glVertex and glNormal calls between glBegin and glEnd. What glEvalCoord3d does is to evaluate the curved surface function as we provided it and compute a vertex and normal. It then generates the appropriate glVertex and glNormal calls for us. This why they call it an evaluator. It evaluates the function and calls other functions with the results.
Last line to add:
glDisable(GL_AUTO_NORMAL);
Automatic normals are inefficient if we don't need them (and we generally don't).
Run this. You should get a curved surface. Please take some time and move the control points around to get an idea of what is happening. Try to create a surface that loops back on itself. Create curves and then turn off transparency and the control points. Look at the curves from different sides. Be sure you understand what is happening.
Just for fun, I've put in a texture map that you can apply to the surface. I've done everything to enable the texture map. All you will need to add are the lines to set the texture coordinates. This is actually quite easy. Just add before each of the glEvalCoord2d functions a glTexCoord2d function with the same parameters. The loop should look like this:
for (int i = 0; i <= divide; i += 1)
{
glTexCoord2d(i / double(divide), (j+1) / double(divide));
glEvalCoord2d(i / double(divide), (j+1) / double(divide));
glTexCoord2d(i / double(divide), j / double(divide));
glEvalCoord2d(i / double(divide), j / double(divide));
}
The texture map makes the curved surface operation much more obvious.
I provided code to create the first surface only. I want you to add to OnPatchNew the capability of adding a new patch that has at least G1 continuity with the previously created patch. It should work no matter how you have dragged around the control points for the first surface. You should be able to add any number of other surfaces, each with at least G1 continuity with the previously created surface.
The second half of this step will guide you through the creation of a simple NURBS surface. NURBS is Non-Uniform Rational B-Splines. It is the single most powerful curve creation tool in all Graphicsdom. You can create a curve or surface with many, many control points and you have a great deal of control over what happens at these points.
I have created a simple program that you will work with. This program creates an 8 (u) by 11 (v) set of control points that you can manipulate. You'll have to add some code to make these display a NURBS surface. Then, you are going to texture-map that surface.
Before you begin work on this lab, I suggest you refer to Lecture 16, the Curves III lecture for definitions of knots and other issues related to Spline curves. You may also want to experiment with the curves demonstration program. I've made this program available in curves.zip. If you select Spline Curves, you can insert additional control points, manipulate control points, and manipulate the knot values for a 2D curve.
For this step you'll be working with some code I've created. You'll find this code in step9b.zip. This code displays a flat surface and the fixed control points, much like step8a did. You can use the mouse to spin the scene around. Then, you can right-click on any control point to select it. If you hold down the right mouse button, you can drag the point. When selected, the up and down arrow keys move the point in the Y direction. Be sure you are familiar with this operation before proceeding.
I've described the control points this way:
// The points array
GLfloat m_p[UPOINTS][VPOINTS][3];
You'll probably notice below this line another array for texture coordinates. This array contains the texture coordinates that correspond to these points. More on that later. Be sure you understand how I'm initially setting the values of these arrays in the CChildView constructor.
The only code you should examine is OnGLDraw and ActualDraw. I've put comments in ActualDraw() to indicate where you will add the code to draw the NURBS surface.
We have to allocate a NURBS renderer for our program to use. You can allocate one every time you draw, then delete it when you are done. But, that's not very efficient. Instead, we're going to allocate one renderer that we'll use over and over again.
You need to allocate the renderer during the execution of OnGLDraw, because you can do OpenGL calls there. You can't do OpenGL calls in the CChildView constructor, because OpenGL has not yet been initialized. The best thing to do is to create a function called OnFirstDraw() that is called the first time OnGLDraw is called. You'll need a member variable something like m_firstdraw to make that work (which may already be there). Be sure that function gets called once and only once. It's a common mistake to have this function get called over and over again. That will make you code slow and chew up memory as it runs. Just be sure you are setting m_firstdraw to false in your OnFirstDraw function.
Create a member variable in CChildView of type GLUnurbsObj * named m_nurbs. Then add the following code to OnFirstDraw:
m_nurbs = gluNewNurbsRenderer();
gluNurbsProperty(m_nurbs, GLU_SAMPLING_TOLERANCE, 25.0);
gluNurbsProperty(m_nurbs, GLU_DISPLAY_MODE, GLU_FILL);
The first line of code creates the renderer. The second sets the "sampling tolerance". NURBS is much more automatic about rendering than the Bezier evaluators you used in the last lab. Read the documentation about this option to see what it does. You may want to play with this value once the program is working.
The last line indicates that we want to draw the image as a filled surface. Other options including drawing the outlines of the underlying polygons and just the outline of the entire surface.
Now, go into ActualDraw where the comment says "INSERT HERE...". We're going to render here. First, you need to create knot arrays. A NURBS surface of degree 3 (cubics) requires 4 more knots than control points in each dimension. So, add the following lines:
// The U direction knots
GLfloat uknots[UPOINTS + 4];
// The V direction knots
GLfloat vknots[VPOINTS + 4];
Now, initialize these arrays to 0, 1, 2, etc. uknots should contain the values 0, 1, 2, ..., UPOINTS+2, UPOINTS+3. vknots should contain the values 0, 1, 2, ..., VPOINTS+2, VPOINTS+3. This is a uniform spacing between the knots of 1.0. Now add this code to render the surface:
glEnable(GL_AUTO_NORMAL);
gluBeginSurface(m_nurbs);
gluNurbsSurface(m_nurbs,
UPOINTS + 4, uknots,
VPOINTS + 4, vknots,
VPOINTS * 3,
3,
&m_p[0][0][0],
4, 4,
GL_MAP2_VERTEX_3);
gluEndSurface(m_nurbs);
glDisable(GL_AUTO_NORMAL);
This is really all there is to it. When you run this, you
should see a NURBS surface. Be sure to spend some time playing with the
points so you can see what this does. Don't be surprised that the surface does
not extend to the outside control points.
| You may have noticed that I don't draw the surface if p_select is set. This is because NURBS don't seem to work right when in select mode. I'm not sure why, but they hang up. Besides, not rendering something in select mode that we can't select, anyway, just makes it faster. Select mode is the mode used to determine what the mouse has clicked on. |
There is a global variable m_interpolateends. You probably noticed that the NURBS surface does not extend to the outer control points. You can make it do this by changing the knots array. If we make the first four values of each array 0, 0, 0, 0, then follow with 1, 2, etc. the program will interpolate the start points and left and bottom edges. Note that the only actual point it will interpolate is the lower-left corner. Change you program to do this when m_interpolateends is set and play with the menu option to turn it on and off.
To make the other end interpolate, we have to have 4 duplicate values at the end of the knots array. As an example, if we have 7 control points, a knots array of {0, 0, 0, 0, 1, 2, 3, 4, 4, 4, 4} (7 + 4 = 11 values) would interpolate both end values. Change your program to set up the knot array this way and see what happens. When you turn on m_interpolateends, you should see the patch extend to the end points. Play with the end points to see what happens. Try dragging an end point over the top of the surface.
I also left in an m_texturemap member variable that is set true when the texture map option is selected. There's also an m_texture variable of type CTexture already loaded with a starting texture (feel free to change the texture image if you want).
Now, before the call to gluBeginSurface(), we need to enable texture mapping if this option is selected. This code should look familiar:
if(m_texturemap)
{
glEnable(GL_TEXTURE_2D);
glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE);
glBindTexture(GL_TEXTURE_2D, m_texture.MipTexName());
}
Note that I'm using MIPMAPPING here, so this texture should look pretty nice.
After the call to gluEndSurface(), please disable texture mapping:
glDisable(GL_TEXTURE_2D);
Now, to map a texture, you have to provide s,t values for every point on the surface. NURBS surfaces determine points in space for the surface by using u,v values. These points need to have associated texture points defined by the same u,v values. So, you create another NURBS surface to interpolate between the texture points. This surface has to be the same size. Hence, the m_ptex member variable. This variable maps the control points to points on the texture. We treat this as a NURBS surface. Add the following code immediately following the existing gluNurbsSurface call:
if(m_texturemap)
{
gluNurbsSurface(m_nurbs,
UPOINTS + 4, uknots,
VPOINTS + 4, vknots,
VPOINTS * 2,
2,
&m_ptex[0][0][0],
4, 4,
GL_MAP2_TEXTURE_COORD_2);
}
When you do gluNurbsSurface with GL_MAP2_VERTEX_2, you are creating glVertex calls when the surface is rendered. This function creates glTexCoord calls. These are all saved up and spit out by the gluEndSurface() call.
Run this and turn on texture mapping. Try the various options. You should have fun with this program...
You have two different projects to turn in. Please turn them both in. The TA will send out instructions on how to turn them in.