![]() |
are you ready for a mindfuck? | ||||||||||||||||
|
....::::Menu::::.... ---------------------------...::About::... ...::Articles::... ...::Contact::... ...::Home & News::... ...::Links & Credits::... --------------------------- --------------------------- |
Rendering the Stanford bunnyby Elie De BrauwerGoal of this fileThis file covers the creation of a 3D object using Vertex Arrays and Lighting in three steps. History of this file
Step 0: AboutHistoryFirst a word of gratitude towards the people of the Stanford 3D Scanning Repository who scanned the orignal terra cotta in 1993-1994 and provided the internet/graphics community with this well known test model. The history of the bunny is available here. bunny.hFor the purpose of this article I have created a bunny.h file. This file contains the data to create the bunny the data originates from the original archives from Stanford and are based on the bunny.ply file from this site. I just took the needed information vertex coordinates and triangle definition from that file. I used Greg Turk's ply tools to calculate the vertices' normal vectors. Basicly the structure of bunny.h can be summarized to:
#define NUM_POINTS 35947
#define NUM_TRIANGLES 69451
GLfloat bunny[]={
-0.0378297,0.12794,0.00447467,
// vertex1x, vertex1y, vertex1z,
// vertex2x, vertex2y, vertex2z,
// add a whole lot of coordinates here
};
GLint triangles [] = {
20399,21215,21216,
// triangle1vertex1, triangle1vertex2, triangle1vertex3
// triangle2vertex1, triangle2vertex2, triangle2vertex3
// add a whole lot of triangles here
};
GLfloat normals [] = {
0.196151,0.972595,-0.124833,
// vertex1normalx, vertex1normaly, vertex1normalz
// vertex2normalx, vertex2normaly, vertex2normalz
// add a whole lot of coordinates here
};
The bunny consists of 35947 points that create together 69451 triangles. The bunny.h file is 3433645 bytes large which is about 3.3 megabyte. Which is only 400 kilbyte larger than the original bunny.ply file which did not contain the normal vector information. You can include this file in your C/C++ program, your program will get a couple megabytes large but no file IO will be required when running the program and the three arrays are immediatly at your disposal. Step 1: Wireframe bunnyThe codeIn the first step we'll connect the dots of the bunny. This is rather basic so jumping to the source right away isn't a bad idea: #include <GL/glut.h> #include "../inc/bunny.h" // Function prototypes void disp(); void keyb(unsigned char key, int x, int y); // Is the bunny rotating ? bool rotate=0; Nothing complicated happens here, we include the GLUT header and the bunny header containing the bunny data. We define two function prototypes, one for the display callback and one for the keyboard callback. And all that follows is a global bool that we use to animate our model.
int main(int argc, char **argv){
glutInit(&argc, argv);
glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGBA | GLUT_DEPTH);
glutCreateWindow("Stanford Bunny");
glClearColor(0.0,0.0,0.0,0.0);
glEnable(GL_DEPTH_TEST);
glutDisplayFunc(disp);
glutKeyboardFunc(keyb);
glutMainLoop();
return 0;
}
We create a new window that has depth testing enabled since this is a 3D model.
void disp(void){
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
if(rotate==1){
glRotatef(1,0.0,1.0,0.0);
}
glPolygonMode(GL_FRONT_AND_BACK,GL_LINE);
glBegin(GL_TRIANGLES);
for(int i=0;i<NUM_TRIANGLES*3;i++){
glVertex3f(10*bunny[3*triangles[i]]+0.2,10*bunny[3*triangles[i]+1]-1,10*bunny[3*triangles[i]+2]);
}
glEnd();
glutSwapBuffers();
glutPostRedisplay();
}
Like usual the magic happens in the display callback. But if you compare the amount of lines to the result you see the power of a well structured programming language. First we clear the color buffer and the depth buffer using glClear after that we rotate the bunny one degree around the Y-axis if the rotate flag is set. Next we use glPolygonMode to define we want to see a wireframe model (filled polygons are showed by default) but since we don't have any lighting we would only see a solid white 2D bunny. The next step will draw all the triangles. Drawing the triangles is a glBegin(GL_TRIANGLES) which contains a for loop this foor loop uses the data in the triangles array in bunny.h. It draws vertices specified in the order they appear in in the triangles array. The coordinates are multiplied by 10, 0.2 is added to the x coordinate and 1 is subtracted from the Y value in order to center the bunny on the screen.
void keyb(unsigned char key, int x, int y){
if(key=='l'){
rotate=1;
}else{
rotate=0;
}
}
If the key pressed is 'l' then we rotate in the other case we don't. The resultBelow you can see two (resized) screenshots of the first step result, click them in order to enlarge them.
Step 2: Vertex ArraysIntroduction to Vertex Arrays
To be honest the first step wasn't such a big deal. The only piece of code that might look complicated
is the for loop where we were adding some 'mistery' constants. Now OpenGL provides the programmer
with something handy called Vertex Arrays. This comes in handy in cases where you have an
array of vertex coordinates, normal coordinates, color information, texture coordinates, ... and you
want to access these on a transparant way.
void glVertexPointer( GLint size,
GLenum type,
GLsizei stride,
const GLvoid *ptr )
void glColorPointer( GLint size,
GLenum type,
GLsizei stride,
const GLvoid *ptr )
void glIndexPointer( GLenum type,
GLsizei stride,
const GLvoid *ptr )
void glNormalPointer( GLenum type,
GLsizei stride,
const GLvoid *ptr )
void glTexCoordPointer( GLint size,
GLenum type,
GLsizei stride,
const GLvoid *ptr )
void glEdgeFlagPointer( GLsizei stride,
const GLvoid *ptr )
-> size : how many elements are needed to create on (e.g. 3 elements to create one vertex
when x,y,z coordinates are defined, 2 when only x and y are defined).
-> type : which is the type of the data e.g. GL_INT for integers GL_FLOAT for floats
-> stride : the offset between consecutive groups of elements (see interleaved arrays)
-> ptr : the pointer to the data
At this point we can use void glArrayElement( GLint i ) between a glBegin() and a glEnd() for example to get all the information of the ith element out of all the currently enabled arrays. So when we have a color array and a vertex array enabled glArrayElement(1) would be equale to a call to glVertex3f() and a call to glColor3f(). At this point we could alter the body of the for loop to:
for(int i=0;i<NUM_TRIANGLES*3;i++){
glArrayElement(triangles[i]);
}
This already looks much better and much more readable but we still have a for loop. OpenGL also provides a method to call all the vertices when you have an array of indices available (which is our triangles array !).Observer the following prototype: void glDrawElements( GLenum mode,GLsizei count,GLenum type, const GLvoid *indices ). Here is the mode each parameter that can be passed to glBegin() in this case it will be GL_TRIANGLES, count is the amount of indices in the array, type is the datatype contained in the array (e.g GL_UNSIGNED_SHORT or GL_UNSIGNED_INT and finally indices is the array. So the for loop becomes: glDrawElements(GL_TRIANGLES,3*NUM_TRIANGLES,GL_UNSIGNED_INT,triangles); Now I don't think it can get any shorter. OpenGL provides some other functions like glDrawRangeElements to draw a range of elements or glDrawArrays() to draw a sequence of geometric primitives. For more information look at the man pages. Another subject I'd briefly like to mention is the subject of Interleaved Arrays, this is an array where we pack all the arrays together in one large interleaved array. (So bunny.h could exist out of a single array). In this case the stride value of the call to gl*Pointer should be set to a value representing the data between two consecutive elements and a call to void glInterleavedArrays( GLenum format,GLsizei stride,const GLvoid *pointer ) should be made to define the structure of the array. Again for more information see the manpages. The CodeBelow you can find the code of the second step. The big differences are that we make use of vertex arrays and that we defined a perspective projection to get a different view of the bunny. #include <GL/glut.h> #include "../inc/bunny.h" void disp(); void keyb(unsigned char key, int x, int y); void reshape(int x, int y); bool rotate=0; We added a reshape callback
int main(int argc, char **argv){
glutInit(&argc, argv);
glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGBA | GLUT_DEPTH);
glutCreateWindow("Stanford Bunny");
glClearColor(0.0,0.0,0.0,0.0);
glEnable(GL_DEPTH_TEST);
glEnableClientState(GL_VERTEX_ARRAY);
glVertexPointer(3,GL_FLOAT,0,bunny);
glutDisplayFunc(disp);
glutKeyboardFunc(keyb);
glutReshapeFunc(reshape);
glutMainLoop();
return 0;
}
A call to glEnableClientState(GL_VERTEX_ARRAY); is made to enable vertex arrays and glVertexPointer(3,GL_FLOAT,0,bunny); defines which array contains the vertex data. The 3 says we have bot x,y and z coordinates, the GL_FLOAT says the array consits of GLfloat variables.
void disp(void){
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
if(rotate==1){
glRotatef(1,0.0,1.0,0.0);
}
glPolygonMode(GL_FRONT_AND_BACK,GL_LINE);
glDrawElements(GL_TRIANGLES,3*NUM_TRIANGLES,GL_UNSIGNED_INT,triangles);
glutSwapBuffers();
glutPostRedisplay();
}
We are still using the wireframe model to show the bunny (one step at a time). But the for loop has been changed to a single call to glDrawElements. If you read the explanation above this speaks for itself.
void keyb(unsigned char key, int x, int y){
if(key=='l'){
rotate=1;
}else{
rotate=0;
}
}
void reshape(int x, int y){
glViewport(0,0,x,y);
glMatrixMode(GL_PROJECTION);
glLoadIdentity();
gluPerspective(50,x/(y*1.0),0.1,2.0);
glMatrixMode(GL_MODELVIEW);
glLoadIdentity();
gluLookAt(0.25,0.11,0,0,0.11,0,0,1,0);
}
The keyboard callback hasn't changed and it won't change anymore. The reshape function lets the application use the available viewport performs a perspective projection using gluPerspective and a call to gluLookAt is used to position the eye in front of the bunny. The ResultWhen you look at the results below it gives you a more 3D feeling, you can see the bottom and the holes in the bottom. Click on the image for a larger version.
Step 3: LightingIntroductionLighting is a very complex topic covering many aspects. The more realism you want to add to your scene the more complex the lighting will become. Most books like the red book (OpenGL Programming Guide) or a general computer graphics book will start to explain the entire lighting model using images, vectors, etcetera. So if you want to know this go get a good book. I will explain the practical part of lighting assuming that you know what a normal vector is and how light reflects. I will explain the basics of using lighting by a little example where we will create a scene of glut primitives and we'll enlighten it. In the next step we'll implement that lighting process on the bunny. Turning the lights on1: Initial sceneConsider the following easy to understand program:
#include <GL/glut.h>
void disp();
void init();
int main(int argc, char **argv){
glutInit(&argc, argv);
glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGBA | GLUT_DEPTH);
glutCreateWindow("Turn the light on");
init();
glutDisplayFunc(disp);
glutMainLoop();
return 0;
}
void init(){
glClearColor(0.0,0.0,0.0,0.0);
glEnable(GL_DEPTH_TEST);
}
void disp(){
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
glLoadIdentity();
glPushMatrix();
glTranslatef(-0.5,0,0);
glColor3f(1.0,0.0,0.0);
glutSolidSphere(0.2,50,50);
glTranslatef(1,0,0);
glScalef(0.4,0.4,0.4);
glColor3f(0.0,1.0,0.0);
glutSolidIcosahedron();
glPopMatrix();
glTranslatef(0,0.5,0);
glColor3f(0.0,0.0,1.0);
glutSolidTorus(0.1,0.2,10,100);
glTranslatef(0,-1.0,0);
glColor3f(0.0,1.0,1.0);
glutSolidTeapot(0.2);
glutSwapBuffers();
}
This programs displays a scene containing of four glut primitives. A sphere, an icosahedron, a torus and a
teapot. Each painted in a different color. We have put all the initialisation in a separate init()
function.
2: Enabling lightingIn this step we enable lights by adding glEnable(GL_LIGHTING) in the init() function. So the init() function now looks like:
void init(){
glClearColor(0.0,0.0,0.0,0.0);
glEnable(GL_DEPTH_TEST);
glEnable(GL_LIGHTING);
}
The output of the program looks like this:
3: Enabling lightsThe previous snapshot looks rather dark. Why's that ? Simple, lighting is enabled but no lights are turned on. You can enable light by calls glEnable(GL_LIGHTi) where i is a number ≥ 0 and ≤ GL_MAX_LIGHTS which depends on the implementation. Now we enable the first two lights by calling glEnable(GL_LIGHT0); and glEnable(GL_LIGHT1);, we also add this in the init() function. The default position of a light is {0.0,0.0,1.0,0.0} which means the light incoming light comes from the
back keeping everything dark. So we define two light coordinates one in the upper left and one in the lower right
corner with a z coordinate of -1. Next we set this position with void glLightf( GLenum light,GLenum pname,GLfloat param )
where light is the light (e.g. GL_LIGHT0), pname is the light parameter we want to change (e.g.
GL_POSITION) and param is the new value for the parameter. If param is an array call glLightfv also
integer variants exist. For more information about the available parameters please refer to the glLight manpage.
void init(){
static GLfloat l0_pos[] = { 1.0, 1.0,-1.0,1.0};
static GLfloat l1_pos[] = {-1.0,-1.0,-1.0,1.0};
glClearColor(0.0,0.0,0.0,0.0);
glShadeModel(GL_SMOOTH);
glEnable(GL_DEPTH_TEST);
glEnable(GL_LIGHTING);
glEnable(GL_LIGHT0);
glEnable(GL_LIGHT1);
glLightfv(GL_LIGHT0, GL_POSITION, l0_pos);
glLightfv(GL_LIGHT1, GL_POSITION, l1_pos);
}
This results in the following output :
4: Configuring the lights Ok now we can see something we're on our way. But we enabled two lights and only one appears to be shining.
void init(){
static GLfloat l0_pos[] = { 1.0, 1.0,-1.0,1.0};
static GLfloat l1_pos[] = {-1.0,-1.0,-1.0,1.0};
static GLfloat l0_col[] = { 1.0, 1.0, 1.0,1.0};
static GLfloat l1_col[] = { 1.0, 0.0, 0.0,1.0};
glClearColor(0.0,0.0,0.0,0.0);
glShadeModel(GL_SMOOTH);
glEnable(GL_DEPTH_TEST);
glEnable(GL_LIGHTING);
glEnable(GL_LIGHT0);
glEnable(GL_LIGHT1);
glLightfv(GL_LIGHT0, GL_POSITION, l0_pos);
glLightfv(GL_LIGHT1, GL_POSITION, l1_pos);
glLightfv(GL_LIGHT0, GL_DIFFUSE , l0_col);
glLightfv(GL_LIGHT1, GL_DIFFUSE , l1_col);
}
With the red light enabled we get:
Other light parameters can be set this way: GL_SPOT_EXPONENT,GL_SPOT_CUTOFF,GL_CONSTANT_ATTENUATION, GL_LINEAR_ATTENUATION and GL_QUADRATIC_ATTENUATION can be set using glLightf() or glLighti() and GL_AMBIENT, GL_DIFFUSE, GL_SPECULAR,GL_POSITION, GL_SPOT_CUTOFF, GL_SPOT_DIRECTION, GL_SPOT_EXPONENT,GL_CONSTANT_ATTENUATION, GL_LINEAR_ATTENUATION and GL_QUADRATIC_ATTENUATION can be set using glLightfv() or glLightiv(). For more information concerning the meaning of this parameters and the default values please consult the manual pages. 5: Material properties
But in the orignal scene each object has it's own color defined using glColor3f() but when lighting is enabled it looks like
OpenGL makes each object white. True, when lighting is enabled there's a difference between a red car, a red bal and a red brick. Each
are red but reflect the light on their own different way. In order to obtain a certain degree of realism each object should have a defined material
in OpenGL.
void disp(){
// Color definitions
static GLfloat sphere_col[] = {1.0,0.0,0.0,1.0};
static GLfloat icos_col[] = {0.0,1.0,0.0,1.0};
static GLfloat torus_col[] = {0.0,0.0,1.0,1.0};
static GLfloat tea_col[] = {0.0,1.0,1.0,1.0};
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
glLoadIdentity();
glPushMatrix();
glTranslatef(-0.5,0,0);
glMaterialfv(GL_FRONT_AND_BACK, GL_AMBIENT_AND_DIFFUSE, sphere_col);
glutSolidSphere(0.2,50,50);
glTranslatef(1,0,0);
glScalef(0.4,0.4,0.4);
glMaterialfv(GL_FRONT_AND_BACK, GL_AMBIENT_AND_DIFFUSE, icos_col);
glutSolidIcosahedron();
glPopMatrix();
glTranslatef(0,0.5,0);
glMaterialfv(GL_FRONT_AND_BACK, GL_AMBIENT_AND_DIFFUSE, torus_col);
glutSolidTorus(0.1,0.2,10,100);
glTranslatef(0,-1.0,0);
glMaterialfv(GL_FRONT_AND_BACK, GL_AMBIENT_AND_DIFFUSE, tea_col);
glutSolidTeapot(0.2);
glutSwapBuffers();
}
Now with the material properties enabled we get the following result:
Which doesn't look that bad. End of this scene
Beware, this is only the tip of the iceberg. I've used some glut primitives here who al have their normals defined internally.
So when adapting this to our bunny each single call to glVertex*() should have a call to glNormal* to define the coordinates
of the normal vector ! Much more options and properties are available but at this point you should be able to get started and understand the
rest of this article. The Code#include <GL/glut.h> #include "../inc/bunny.h" void disp(); void keyb(unsigned char key, int x, int y); void reshape(int x, int y); void init(); bool rotate=0; The prototype for an init() function has been added.
int main(int argc, char **argv){
glutInit(&argc, argv);
glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGBA | GLUT_DEPTH);
glutCreateWindow("Stanford Bunny");
init();
glutDisplayFunc(disp);
glutKeyboardFunc(keyb);
glutReshapeFunc(reshape);
glutMainLoop();
return 0;
}
All the initialisation has been moved to an init function called init
void init(){
glClearColor(0.0,0.0,0.0,0.0);
glShadeModel(GL_SMOOTH);
glEnable(GL_LIGHTING);
glEnable(GL_LIGHT0);
glEnable(GL_DEPTH_TEST);
glEnableClientState(GL_VERTEX_ARRAY);
glEnableClientState(GL_NORMAL_ARRAY);
glVertexPointer(3,GL_FLOAT,0,bunny);
glNormalPointer(GL_FLOAT,0,normals);
}
This is the initilisation function. We initialise the Vertex Arrays (we enable the client state for an arary of vertices and an array of normal vectors). We also enable lighting and enable the light GL_LIGHT0 the first light. And we tell OpenGL we want smooth shading (or else each triangle would be in a single color, with this model it won't be such a big problem since we have enough and small enough triangles).
void disp(void){
static int spin=0;
static GLfloat position [] = {0.0,0.15,0.1,1.0};
static GLfloat color [] = {0.5,0.25,0.0,1.0};
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
// Draw the light
glPushMatrix();
glRotatef(spin,0.0,1.0,0.0);
if(rotate==1){
spin=(spin+1)%360;
}
glLightfv(GL_LIGHT0,GL_POSITION,position);
// Draw a blue wireframe cube wherle the light is
glPushMatrix();
glDisable(GL_LIGHTING);
glTranslatef(position[0],position[1],position[2]);
glColor3f(0.0,0.0,1.0);
glutWireCube(0.01);
glEnable(GL_LIGHTING);
glPopMatrix();
glPopMatrix();
// Draw the bunny
glPolygonMode(GL_FRONT_AND_BACK,GL_FILL);
glMaterialfv(GL_FRONT_AND_BACK,GL_AMBIENT_AND_DIFFUSE,color);
glDrawElements(GL_TRIANGLES,3*NUM_TRIANGLES,GL_UNSIGNED_INT,triangles);
// Swap the buffers && redraw
glutSwapBuffers();
glutPostRedisplay();
}
Now more magic. First we define three static variables the first one is the number of
degrees the light has rotated around the Y-axis. Next are 2 arrays (which should be declared const)
the first one defines the position of the light and the second one defines the abient and diffuse
color of the bunny. After that the buffers are cleared.
void keyb(unsigned char key, int x, int y){
if(key=='l'){
rotate=1;
}else{
rotate=0;
}
}
Still unchanged only the rotate flag nog lets the light rotate and not the bunny.
void reshape(int x, int y){
glViewport(0,0,x,y);
glMatrixMode(GL_PROJECTION);
glLoadIdentity();
gluPerspective(50,x/(y*1.0),0.1,2.0);
glMatrixMode(GL_MODELVIEW);
glLoadIdentity();
gluLookAt(-0.2,0.11,0.25,0,0.11,0,0,1,0);
}
The reshape function hasn't changed from step 2. The ResultBelow you can see four screenshots of the program with the light on different positions and the bunny colored brown. The position of the light is shown by the blue wireframe cube. Click on the images for a larger version.
The CodeThe bunny sourcecode for each of the three steps and the bunny header file is available
here. Due to the amount of points needed to build the bunny
this archive has a size of approximately 3.3 megabytes. |