EDIT . I thought you were experimenting, but I see that the code came from an OpenGL tutorial . I looked at her and understood your point of view now. It's hard to learn the basics from there.
NURBS Background
The best way to access NURBS is to play online with it. Then you will get an intuition about the points defining the boundaries (along the edges), defining the shape (each other), the tangent connection between them and continuity. NURBS can be made from patches sewn along the edges where continuity is controlled, namely, you can ask for G3 for the main body of the car or C1 for a cheap game model. It is very difficult to get a concept from any description. If you want to do this, I would highly recommend the trial version of the Rhino Nurbs Modeller . I used it many years ago and now it seems abandoned, but still it is software with one of the best NURBS support (Autodesk 3d Studio MAX and MAYA worse). It may be a little time, but for beginners I would recommend playing with something simpler; grab the applet from the Simple Bezier Curve Editor for rotation.
To understand NURBS, it is also good to consult a Wikipedia article on Bezier curves . Once you understand the connection between the point and the final shape of the curve, you can easily generalize it to the surface. I find this animation very intuitive:

You can imagine the surface from your example as a set of four of these curves with fabric stretched over them. Using the applet that I linked earlier, you can play with the position and receive instant feedback on the form received. Pay attention to the parameter t - this is the coordinate along the curve and has the range [0, 1]. On the NURBS surface, there are two of these coordinates, tentatively called u and v (which is important for the drawing function).
So, the ctrlpoints structure from the code contains all the point coordinates. A simplification to explain is the four cubic Bezier curves (those from the animation). For each curve, you have four points in three dimensions. If you ignore the Y axis, then they all lie on a grid with X and Z: -1.5, -1.0, 1.0, 1.5. This explains a total of 32 values ββ(4x4 for X plus 4x4 for Z).
The rest is height, Y-values. In your case, this is the second value of each point in ctrlpoints . To get the expected result, you can make all the Y values ββequal at the edges (external) and slightly raised in the middle (4 internal). You'll get:

Points used to render above the image:
GLfloat ctrlpoints[4][4][3] = { {{-1.5, 1.0, -1.5}, {-0.5, 1.0,-1.5 }, {0.5, 1.0, -1.5 }, {1.5, 1.0,-1.5}}, {{-1.5, 1.0, -0.5}, {-0.5, 2.0,-0.5 }, {0.5, 2.0, -0.5 }, {1.5, 1.0,-0.5}}, {{-1.5, 1.0, 0.5}, {-0.5, 2.0, 0.5 }, {0.5, 2.0, 0.5 }, {1.5, 1.0, 0.5}}, {{-1.5, 1.0, 1.5}, {-0.5, 1.0, 1.5 }, {0.5, 1.0, 1.5 }, {1.5, 1.0, 1.5}} }; // ^ ^ ^ ^ // | | | | // | | | | // \_________ Those are most relevant - Y-coord, height ______/
NURBS in OpenGL with GLUT Pass - API
I see the OpenGL API hiding very relevant details. A NURBS surface is drawn using the Evaluator and defined using the Map function.
You must define breakpoints in the init(void) function, for example:
glMap2f(GL_MAP2_VERTEX_3, 0, 1, 3, 4, 0, 1, 12, 4, &ctrlpoints[0][0][0]);
A good explanation of the function can be found on the MSDN website for glMap2f . We pass control points, their type and details, such as array pitch and order.
You can draw it using the Evaluator function. It takes two coordinates as arguments and returns a point in three-dimensional space. These input coordinates exactly match the u and v mentioned earlier under the animation. In our example:
glBegin(GL_LINE_STRIP); // we'll draw a line // take 31 samples of a cross-section of the surface for (i = 0; i <= 30; i++) // for each sample, evaluate a 3d point glEvalCoord2f((GLfloat)i/30.0, (GLfloat)j/8.0); // notice j is constant in the loop here, but // is being changed by the outer loop. // // j is iterated in 9 steps, so we'll end up // with 9 lines glEnd();
I intentionally omitted the outer loop, which is described here:
// we want 9 lines for (j = 0; j <= 8; j++) { // OpenGL state machine will be used to draw lines glBegin(GL_LINE_STRIP); // inner loop for j-th line along X glBegin(GL_LINE_STRIP); // inner loop for j-th line along Z glEnd(); // done with the lines }
Working example
#include <stdlib.h> #include <GL/glut.h> GLfloat ctrlpoints[4][4][3] = { {{-1.5, 1.0, -1.5}, {-0.5, 1.0,-1.5 }, {0.5, 1.0, -1.5 }, {1.5, 1.0,-1.5}}, {{-1.5, 1.0, -0.5}, {-0.5, 2.0,-0.5 }, {0.5, 2.0, -0.5 }, {1.5, 1.0,-0.5}}, {{-1.5, 1.0, 0.5}, {-0.5, 2.0, 0.5 }, {0.5, 2.0, 0.5 }, {1.5, 1.0, 0.5}}, {{-1.5, 1.0, 1.5}, {-0.5, 1.0, 1.5 }, {0.5, 1.0, 1.5 }, {1.5, 1.0, 1.5}} }; void display(void) { int i, j; glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); glColor3f(1.0, 1.0, 1.0); glPushMatrix(); glRotatef(25.0, 1.0, 1.0, 1.0); for (j = 0; j <= 8; j++) { glBegin(GL_LINE_STRIP); for (i = 0; i <= 30; i++) glEvalCoord2f((GLfloat)i/30.0, (GLfloat)j/8.0); glEnd(); glBegin(GL_LINE_STRIP); for (i = 0; i <= 30; i++) glEvalCoord2f((GLfloat)j/8.0, (GLfloat)i/30.0); glEnd(); } glPopMatrix(); glFlush(); } void init(void) { glClearColor(0.0, 0.0, 0.0, 0.0); glMap2f(GL_MAP2_VERTEX_3, 0, 1, 3, 4, 0, 1, 12, 4, &ctrlpoints[0][0][0]); glEnable(GL_MAP2_VERTEX_3); glMapGrid2f(20, 0.0, 1.0, 20, 0.0, 1.0); glEnable(GL_DEPTH_TEST); glShadeModel(GL_FLAT); } void reshape(int w, int h) { glViewport(0, 0, (GLsizei) w, (GLsizei) h); glMatrixMode(GL_PROJECTION); glLoadIdentity(); if (w <= h) glOrtho(-5.0, 5.0, -5.0*(GLfloat)h/(GLfloat)w, 5.0*(GLfloat)h/(GLfloat)w, -5.0, 5.0); else glOrtho(-5.0*(GLfloat)w/(GLfloat)h, 5.0*(GLfloat)w/(GLfloat)h, -5.0, 5.0, -5.0, 5.0); glMatrixMode(GL_MODELVIEW); glLoadIdentity(); } int main(int argc, char** argv) { glutInit(&argc, argv); glutInitDisplayMode(GLUT_SINGLE | GLUT_RGB); glutInitWindowSize(500, 500); glutInitWindowPosition(100, 100); glutCreateWindow(argv[0]); init(); glutDisplayFunc(display); glutReshapeFunc(reshape); glutMainLoop(); return 0; }