Bezier Patches / Fullscreen Fix
Bezier Patches
Written by: David Nikdel ( ogapo@ithink.net )
This tutorial is intended to introduce you to Bezier Surfaces in the hopes that someone more artistic than myself will do something really cool with them and show all of us. This is not intended as a complete Bezier patch library, but more as proof of concept code to get you familiar with how these curved surfaces actually work. Also, as this is a very informal piece, I may have occasional lapses in correct terminology in favor of comprehensability; I hope this sits well with everyone. Finally, to those of you already familiar with Beziers who are just reading this to see if I screw up, shame on you ;-), but if you find anything wrong by all means let me or NeHe know, after all no one's perfect, eh? Oh, and one more thing, none of this code is optimized beyond my normal programming technique, this is by design. I want everyone to be able to see exactly what is going on. Well, I guess that's enough of an intro. On with the show!
The Math - ::evil music:: (warning, kinda long section)
Ok, it will be very hard to understand Beziers without at least a basic understanding of the math behind it, however, if you just don't feel like reading this section or already know the math, you can skip it. First I will start out by describing the Bezier curve itself then move on to how to create a Bezier Patch.
Odds are, if you've ever used a graphics program you are already familiar with Bezier curves, perhaps not by that name though. They are the primary method of drawing curved lines and are commonly represented as a series of points each with 2 points representing the tangent at that point from the left and right. Here's what one looks like:

This is the most basic Bezier curve possible (longer ones are made by attaching many of these together (many times without the user realizing it)). This curve is actually defined by only 4 points, those would be the 2 ending control points and the 2 middle control points. To the computer, all the points are the same, but to aid in design we often connect the first and the last two, respectively, because those lines will always be tangent to the endpoint. The curve is a parametric curve and is drawn by finding any number of points evenly spaced along the curve and connecting them with straight lines. In this way you can control the resolution of the patch (and the amount of computation). The most common way to use this is to tesselate it less at a farther distance and more at a closer distance so that, to the viewer, it always appears to be a perfectly curved surface with the lowest possible speed hit.
Bezier curves are based on a basis function from which more complicated versions are derived. Here's the function:
t + (1 - t) = 1
Sounds simple enough huh? Well it really is, this is the Bezier most basic Bezier curve, a 1st degree curve. As you may have guessed from the terminology, the Bezier curves are polynomials, and as we remember from algebra, a 1st degree polynomial is just a straight line; not very interesting. Well, since the basis function is true for all numbers t, we can square, cube, whatever, each side and it will still be true right? Well, lets try cubing it.
(t + (1-t))^3 = 1^3
t^3 + 3*t^2*(1-t) + 3*t*(1-t)^2 + (1-t)^3 = 1
This is the equation we use to calculate the most common Bezier, the 3rd degree Bezier curve. This is most common for two reasons, a) it's the lowest degree polynomial that need not necesarily lie in a plane (there are 4 control points) and b) the tangent lines on the sides are not dependant on one another (with a 2nd degree there would be only 3 control points). So do you see the Bezier curve yet? Hehe, me neither, that's because I still need to add one thing.
Ok, since the entire left side is equal to 1, it's safe to assume that if you add all the components they should still equal one. Does this sound like it could be used to decide how much of each control point to use in calculating a point on the curve? (hint: just say yes ;-) ) Well you're right! When we want to calculate the value of a point some percent along the curve we simply multiply each part by a control point (as a vector) and find the sum. Generally, we'll work with 0
P1*t^3 + P2*3*t^2*(1-t) + P3*3*t*(1-t)^2 + P4*(1-t)^3 = Pnew
Because polynomials are always continuous, this makes for a good way to morp between the 4 points. The only points it actually reaches though are P1 and P4, when t = 1 and 0 respectively.
Now, that's all well and good, but how can I use these in 3D you ask? Well it's actually quite simple, in order to form a Bezier patch, you need 16 control points (4*4), and 2 variables t and v. What you do from there is calculate a point at v along 4 of the parallel curves then use those 4 points to make a new curve and calculate t along that curve. By calculating enough of these points, we can draw triangle strips to connect them, thus drawing the Bezier patch.

Well, I suppose that's enough math for now, on to the code!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | #include <windows.h> // Header File For Windows
#include <math.h> // Header File For Math Library Routines
#include <stdio.h> // Header File For Standard I/O Routines
#include <stdlib.h> // Header File For Standard Library
#include <gl\gl.h> // Header File For The OpenGL32 Library
#include <gl\glu.h> // Header File For The GLu32 Library
#include <gl\glaux.h> // Header File For The Glaux Library
typedef struct point_3d {
double x, y, z;
} POINT_3D;
typedef struct bpatch {
POINT_3D anchors[4][4];
GLuint dlBPatch;
GLuint texture;
} BEZIER_PATCH;
HDC hDC=NULL;
HGLRC hRC=NULL;
HWND hWnd=NULL;
HINSTANCE hInstance;
DEVMODE DMsaved;
bool keys[256];
bool active=TRUE;
bool fullscreen=TRUE;
GLfloat rotz = 0.0f;
BEZIER_PATCH mybezier;
BOOL showCPoints=TRUE;
int divs = 7;
LRESULT CALLBACK WndProc( HWND , UINT , WPARAM , LPARAM );
|
The following are just a few quick functions for some simple vector math. If you're a fan of C++ you might consider using a point class (just make sure it's 3d).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | POINT_3D pointAdd(POINT_3D p, POINT_3D q) {
p.x += q.x; p.y += q.y; p.z += q.z;
return p;
}
POINT_3D pointTimes( double c, POINT_3D p) {
p.x *= c; p.y *= c; p.z *= c;
return p;
}
POINT_3D makePoint( double a, double b, double c) {
POINT_3D p;
p.x = a; p.y = b; p.z = c;
return p;
}
|
This is basically just the 3rd degree basis function written in C, it takes a variable u and an array of 4 points and computes a point on the curve. By stepping u in equal increments between 0 and 1, we'll get a nice approximation of the curve.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | POINT_3D Bernstein( float u, POINT_3D *p) {
POINT_3D a, b, c, d, r;
a = pointTimes( pow (u,3), p[0]);
b = pointTimes(3* pow (u,2)*(1-u), p[1]);
c = pointTimes(3*u* pow ((1-u),2), p[2]);
d = pointTimes( pow ((1-u),3), p[3]);
r = pointAdd(pointAdd(a, b), pointAdd(c, d));
return r;
}
|
This function does the lion's share of the work by generating all the triangle strips and storing them in a display list. We do this so that we don't have to recalculate the patch each frame, only when it changes. By the way, a cool effect you might want to try might be to use the morphing tutorial to morph the patch's control points. This would yield a very cool smooth, organic, morphing effect for relatively little overhead (you only morph 16 points, but you have to recalculate). The "last" array is used to keep the previous line of points (since a triangle strip needs both rows). Also, texture coordinates are calculated by using the u and v values as the percentages (planar mapping).
One thing we don't do is calculate the normals for lighting. When it comes to this, you basically have two options. The first is to find the center of each triangle, then use a bit of calculus and calculate the tangent on both the x and y axes, then do the cross product to get a vector perpendicular to both, THEN normalize the vector and use that as the normal. OR (yes, there is a faster way) you can cheat and just use the normal of the triangle (calculated your favorite way) to get a pretty good approximation. I prefer the latter; the speed hit, in my opinion, isn't worth the extra little bit of realism.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 | GLuint genBezier(BEZIER_PATCH patch, int divs) {
int u = 0, v;
float py, px, pyold;
GLuint drawlist = glGenLists(1);
POINT_3D temp[4];
POINT_3D *last = (POINT_3D*) malloc ( sizeof (POINT_3D)*(divs+1));
if (patch.dlBPatch != NULL)
glDeleteLists(patch.dlBPatch, 1);
temp[0] = patch.anchors[0][3];
temp[1] = patch.anchors[1][3];
temp[2] = patch.anchors[2][3];
temp[3] = patch.anchors[3][3];
for (v=0;v<=divs;v++) {
px = (( float )v)/(( float )divs);
last[v] = Bernstein(px, temp);
}
glNewList(drawlist, GL_COMPILE);
glBindTexture(GL_TEXTURE_2D, patch.texture);
for (u=1;u<=divs;u++) {
py = (( float )u)/(( float )divs);
pyold = (( float )u-1.0f)/(( float )divs);
temp[0] = Bernstein(py, patch.anchors[0]);
temp[1] = Bernstein(py, patch.anchors[1]);
temp[2] = Bernstein(py, patch.anchors[2]);
temp[3] = Bernstein(py, patch.anchors[3]);
glBegin(GL_TRIANGLE_STRIP);
for (v=0;v<=divs;v++) {
px = (( float )v)/(( float )divs);
glTexCoord2f(pyold, px);
glVertex3d(last[v].x, last[v].y, last[v].z);
last[v] = Bernstein(px, temp);
glTexCoord2f(py, px);
glVertex3d(last[v].x, last[v].y, last[v].z);
}
glEnd();
}
glEndList();
free (last);
return drawlist;
}
|
Here we're just loading the matrix with some values I've picked that I think look cool. Feel free to screw around with these and see what it looks like. :-)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | void initBezier( void ) {
mybezier.anchors[0][0] = makePoint(-0.75, -0.75, -0.50);
mybezier.anchors[0][1] = makePoint(-0.25, -0.75, 0.00);
mybezier.anchors[0][2] = makePoint( 0.25, -0.75, 0.00);
mybezier.anchors[0][3] = makePoint( 0.75, -0.75, -0.50);
mybezier.anchors[1][0] = makePoint(-0.75, -0.25, -0.75);
mybezier.anchors[1][1] = makePoint(-0.25, -0.25, 0.50);
mybezier.anchors[1][2] = makePoint( 0.25, -0.25, 0.50);
mybezier.anchors[1][3] = makePoint( 0.75, -0.25, -0.75);
mybezier.anchors[2][0] = makePoint(-0.75, 0.25, 0.00);
mybezier.anchors[2][1] = makePoint(-0.25, 0.25, -0.50);
mybezier.anchors[2][2] = makePoint( 0.25, 0.25, -0.50);
mybezier.anchors[2][3] = makePoint( 0.75, 0.25, 0.00);
mybezier.anchors[3][0] = makePoint(-0.75, 0.75, -0.50);
mybezier.anchors[3][1] = makePoint(-0.25, 0.75, -1.00);
mybezier.anchors[3][2] = makePoint( 0.25, 0.75, -1.00);
mybezier.anchors[3][3] = makePoint( 0.75, 0.75, -0.50);
mybezier.dlBPatch = NULL;
}
|
This is basically just an optimized routine to load a single bitmap. It can easily be used to load an array of em just by putting it in a simple loop.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | BOOL LoadGLTexture(GLuint *texPntr, char * name)
{
BOOL success = FALSE;
AUX_RGBImageRec *TextureImage = NULL;
glGenTextures(1, texPntr);
FILE * test=NULL;
TextureImage = NULL;
test = fopen (name, "r" );
if (test != NULL) {
fclose (test);
TextureImage = auxDIBImageLoad(name);
}
if (TextureImage != NULL) {
success = TRUE;
glBindTexture(GL_TEXTURE_2D, *texPntr);
glTexImage2D(GL_TEXTURE_2D, 0, 3, TextureImage->sizeX, TextureImage->sizeY, 0, GL_RGB, GL_UNSIGNED_BYTE, TextureImage->data);
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_LINEAR);
}
if (TextureImage->data)
free (TextureImage->data);
return success;
}
|
Just adding the patch initialization here. You would do this whenever you create a patch. Again, this might be a cool place to use C++ (bezier class?).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | int InitGL(GLvoid)
{
glEnable(GL_TEXTURE_2D);
glShadeModel(GL_SMOOTH);
glClearColor(0.05f, 0.05f, 0.05f, 0.5f);
glClearDepth(1.0f);
glEnable(GL_DEPTH_TEST);
glDepthFunc(GL_LEQUAL);
glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST);
initBezier();
LoadGLTexture(&(mybezier.texture), "./Data/NeHe.bmp" );
mybezier.dlBPatch = genBezier(mybezier, divs);
return TRUE;
}
|
First call the bezier's display list. Then (if the outlines are on) draw the lines connecting the control points. You can toggle these by pressing SPACE.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | int DrawGLScene(GLvoid) {
int i, j;
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
glLoadIdentity();
glTranslatef(0.0f,0.0f,-4.0f);
glRotatef(-75.0f,1.0f,0.0f,0.0f);
glRotatef(rotz,0.0f,0.0f,1.0f);
glCallList(mybezier.dlBPatch);
if (showCPoints) {
glDisable(GL_TEXTURE_2D);
glColor3f(1.0f,0.0f,0.0f);
for (i=0;i<4;i++) {
glBegin(GL_LINE_STRIP);
for (j=0;j<4;j++)
glVertex3d(mybezier.anchors[i][j].x, mybezier.anchors[i][j].y, mybezier.anchors[i][j].z);
glEnd();
}
for (i=0;i<4;i++) {
glBegin(GL_LINE_STRIP);
for (j=0;j<4;j++)
glVertex3d(mybezier.anchors[j][i].x, mybezier.anchors[j][i].y, mybezier.anchors[j][i].z);
glEnd();
}
glColor3f(1.0f,1.0f,1.0f);
glEnable(GL_TEXTURE_2D);
}
return TRUE;
}
|
This function contains some modified code to make your projects more compatable. It doesn't have anything to do with Bezier curves, but it does fix a problem with switching back the resolution after fullscreen mode with some video cards (including mine, a crappy old ATI Rage PRO, and a few others). I hope, you'll use this from now on so me and others with similar cards can view your cool examples GL code properly. To make these modifications make the changes in KillGLWindow(), make sure and define DMsaved, and make the one line change in CreateGLWindow() (it's marked).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 | GLvoid KillGLWindow(GLvoid)
{
if (fullscreen)
{
if (!ChangeDisplaySettings(NULL,CDS_TEST)) {
ChangeDisplaySettings(NULL,CDS_RESET);
ChangeDisplaySettings(&DMsaved,CDS_RESET);
} else {
ChangeDisplaySettings(NULL,CDS_RESET);
}
ShowCursor(TRUE);
}
if (hRC)
{
if (!wglMakeCurrent(NULL,NULL))
{
MessageBox(NULL, "Release Of DC And RC Failed." , "SHUTDOWN ERROR" ,MB_OK | MB_ICONINFORMATION);
}
if (!wglDeleteContext(hRC))
{
MessageBox(NULL, "Release Rendering Context Failed." , "SHUTDOWN ERROR" ,MB_OK | MB_ICONINFORMATION);
}
hRC=NULL;
}
if (hDC && !ReleaseDC(hWnd,hDC))
{
MessageBox(NULL, "Release Device Context Failed." , "SHUTDOWN ERROR" ,MB_OK | MB_ICONINFORMATION);
hDC=NULL;
}
if (hWnd && !DestroyWindow(hWnd))
{
MessageBox(NULL, "Could Not Release hWnd." , "SHUTDOWN ERROR" ,MB_OK | MB_ICONINFORMATION);
hWnd=NULL;
}
if (!UnregisterClass( "OpenGL" ,hInstance))
{
MessageBox(NULL, "Could Not Unregister Class." , "SHUTDOWN ERROR" ,MB_OK | MB_ICONINFORMATION);
hInstance=NULL;
}
}
|
Just added the EnumDisplaySettings() command here to save the old display settings. (part of the old graphics card fix).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 |
BOOL CreateGLWindow( char * title, int width, int height, int bits, bool fullscreenflag)
{
GLuint PixelFormat;
WNDCLASS wc;
DWORD dwExStyle;
DWORD dwStyle;
RECT WindowRect;
WindowRect.left=( long )0;
WindowRect.right=( long )width;
WindowRect.top=( long )0;
WindowRect.bottom=( long )height;
fullscreen=fullscreenflag;
hInstance = GetModuleHandle(NULL);
wc.style = CS_HREDRAW | CS_VREDRAW | CS_OWNDC;
wc.lpfnWndProc = (WNDPROC) WndProc;
wc.cbClsExtra = 0;
wc.cbWndExtra = 0;
wc.hInstance = hInstance;
wc.hIcon = LoadIcon(NULL, IDI_WINLOGO);
wc.hCursor = LoadCursor(NULL, IDC_ARROW);
wc.hbrBackground = NULL;
wc.lpszMenuName = NULL;
wc.lpszClassName = "OpenGL" ;
EnumDisplaySettings(NULL, ENUM_CURRENT_SETTINGS, &DMsaved);
if (fullscreen)
{
DEVMODE dmScreenSettings;
memset (&dmScreenSettings,0, sizeof (dmScreenSettings));
dmScreenSettings.dmSize= sizeof (dmScreenSettings);
dmScreenSettings.dmPelsWidth = width;
dmScreenSettings.dmPelsHeight = height;
dmScreenSettings.dmBitsPerPel = bits;
dmScreenSettings.dmFields=DM_BITSPERPEL|DM_PELSWIDTH|DM_PELSHEIGHT;
... Code Cut To Save Space (No Further Changes To This Function) ...
return TRUE;
}
|
All I did here was add commands to rotate the patch, raise/lower the resolution, and toggle the control lines.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 | int WINAPI WinMain( HINSTANCE hInstance,
HINSTANCE hPrevInstance,
LPSTR lpCmdLine,
int nCmdShow)
{
MSG msg;
BOOL done=FALSE;
if (MessageBox(NULL, "Would You Like To Run In Fullscreen Mode?" , "Start FullScreen?" ,MB_YESNO|MB_ICONQUESTION)==IDNO)
{
fullscreen=FALSE;
}
if (!CreateGLWindow( "NeHe's Solid Object Tutorial" ,640,480,16,fullscreen))
{
return 0;
}
while (!done)
{
if (PeekMessage(&msg,NULL,0,0,PM_REMOVE))
{
if (msg.message==WM_QUIT)
{
done=TRUE;
}
else
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
}
else
{
if ((active && !DrawGLScene()) || keys[VK_ESCAPE])
{
done=TRUE;
}
else
{
SwapBuffers(hDC);
}
if (keys[VK_LEFT]) rotz -= 0.8f;
if (keys[VK_RIGHT]) rotz += 0.8f;
if (keys[VK_UP]) {
divs++;
mybezier.dlBPatch = genBezier(mybezier, divs);
keys[VK_UP] = FALSE;
}
if (keys[VK_DOWN] && divs > 1) {
divs--;
mybezier.dlBPatch = genBezier(mybezier, divs);
keys[VK_DOWN] = FALSE;
}
if (keys[VK_SPACE]) {
showCPoints = !showCPoints;
keys[VK_SPACE] = FALSE;
}
if (keys[VK_F1])
{
keys[VK_F1]=FALSE;
KillGLWindow();
fullscreen=!fullscreen;
if (!CreateGLWindow( "NeHe's Solid Object Tutorial" ,640,480,16,fullscreen))
{
return 0;
}
}
}
}
KillGLWindow();
return (msg.wParam);
}
|
Well, I hope this tutorial has been enlightening and you all now love Bezier curves as much as I do ;-). If you like this tutorial I may write another one on NURBS curves if anyone's interested. Please e-mail me and let me know what you thought of this tutorial.
About The Author: David Nikdel is currently 18 and a senior at Bartow Senior High School. His current projects include a research paper on curved surfaces in 3D graphics, an OpenGL based game called Blazing Sands and being lazy. His hobbies include programming, football, and paintballing. He will (hopefully) be a freshman at Georgia Tech next year.
David Nikdel
Jeff Molofee (NeHe)
* DOWNLOAD Visual C++ Code For This Lesson.
* DOWNLOAD Borland C++ Builder 6 Code For This Lesson. ( Conversion by Christian Kindahl ) * DOWNLOAD Code Warrior 5.3 Code For This Lesson. ( Conversion by Scott Lupton ) * DOWNLOAD Delphi Code For This Lesson. ( Conversion by Michal Tucek ) * DOWNLOAD Dev C++ Code For This Lesson. ( Conversion by Dan ) * DOWNLOAD Euphoria Code For This Lesson. ( Conversion by Evan Marshall ) * DOWNLOAD LCC Win32 Code For This Lesson. ( Conversion by Robert Wishlaw ) * DOWNLOAD Linux/GLX Code For This Lesson. ( Conversion by Rodolphe Suescun ) * DOWNLOAD LWJGL Code For This Lesson. ( Conversion by Mark Bernard ) * DOWNLOAD Mac OS X/Cocoa Code For This Lesson. ( Conversion by Bryan Blackburn ) * DOWNLOAD Visual Studio .NET Code For This Lesson. ( Conversion by Grant James )
< Lesson 27Lesson 29 >
|
|