/// LSU EE 7700-2 (Sp 08), Graphics Processors // /// Homework 5 -- SOLUTION // Search for SOLUTION. // See Problem 1 below for diff. // $Id:$ /// References: // OpenGL: // http://www.ece.lsu.edu/koppel/gp/refs/glspec21.pdf // OpenGL Shading Language: // http://www.ece.lsu.edu/koppel/gp/refs/GLSLangSpec.Full.1.20.8.pdf // This code uses a pShader class for initializing and using // programmable shaders. This class can be used for your own shaders, // or you can use the OpenGL calls directly. See shader.h for the // pShader class (to figure out how to use it) and for examples look // at the code in cube4.cc, search for vs_bump. (Cube 4 is in // https://svn.ece.lsu.edu/svn/gp/play/) /// Problem 1 // The code in this file displays an undulating tube, similar to the // one used in Homework 3. For each frame the code needs to compute // the position of each vertex, a time-consuming task. Re-write the // code so that it uses the GPU (running a vertex shader) to compute // vertex positions. The goal is to reduce both the computation load // on the CPU and the per-frame data transfer between the CPU and GPU. // The code is already set up with a user-interface to switch between CPU // and GPU computation (by pressing "a"), make sure it continues to work. // The code is already written so that the CPU code can switch vertex // buffering methods: sending data one vertex at a time or by using a // client array. The user interface also supports switching the GPU // code vertex buffering methods between vertex-at-a-time, a client // array, or a buffer object. Write your code so this works as // expected. // The code already uses a programmable shader for lighting, that shader // code is in hw5_shader.cc. Add your shader code to that file. Note that // your added code should just be used for the tube. The lonely triangle // will continue to use "vs_lighting" and the light location marker (the // tetrahedron) will continue to use fixed functionality. // Some User Interface Commands // // v: Vertex Buffering // Change the way vertices are passed to OpenGL. // See uses of opt_v_buffering_cpu, opt_v_buffering_gpu // Note: The "Buffer Object" (2) setting won't work on some GPUs. // // a: GPU/CPU toggle. // Toggles the use of the GPU for computing vertex positions. // Initially turning this on will make the tube disappear, // but in the completed assignment it should work correctly. /// Solution Checklist // [ ] Use GPU code to compute vertex locations. // [ ] Honor the UI settings for vertex buffering (opt_v_buffering_gpu). // [ ] CPU time and CPU/GPU bandwidth should be moderately reduced when // not using buffer objects. // [ ] CPU time and CPU/GPU bandwidth should be greatly reduced when using // buffer objects. // [ ] Do not be concerned about minimizing GPU computation (for this problem). /// Solution Hints // The code in the inner loop is simpler than it looks. The same // shader routine should handle the three different vertices that // the inner loop body writes without any if statements. // Think about what data should be passed to the shader routine per // vertex, and what data should be passed per frame. If possible, // make the per-vertex data identical from frame to frame so that // it can be kept in a buffer object. // To ease in slowly, make a copy of vs_main_lighting and try // passing vs_ff_vertex or vs_lighting slightly changed parameters. // This will verify that the code is indeed running on the GPU. /// Changes Made // A diff of the solution with the original files appears below. // For the shader code the one new routine, vs_main_tube, and two // uniforms were added. #if 0 diff -u -p /home/faculty/koppel/teach/gpcom/gp/hw/2008/hw5/hw5.cc /home/faculty/koppel/teach/gpcom/gp/hw/2008/hw5/hw5sol.cc @@ -244,7 +215,16 @@ private: int num_coor_alloc; MTrig tarray; + // SOLUTION STARTS bool opt_gpu; + float* data_buffer; + bool data_buffer_updated; + GLuint gpu_data_buffer; + int data_buffer_size; + pShader *vs_tube; + GLint uni_wave; + GLint uni_tube_geometry; + }; void @@ -287,6 +267,14 @@ Tube::init() vs_lighting = new pShader("hw5_shader.cc","vs_main_lighting();"); opt_gpu = false; + + // SOLUTION STARTS + data_buffer = NULL; + data_buffer_updated = false; + glGenBuffers(1,&gpu_data_buffer); + vs_tube = new pShader("hw5_shader.cc","vs_main_tube();"); + uni_wave = vs_tube->uniform_location("uni_wave"); + uni_tube_geometry = vs_tube->uniform_location("uni_tube_geometry"); } @@ -452,14 +440,21 @@ Tube::render() tarray.init( vertices_per_ring * 4 ); num_coor_alloc = num_coor; + + // SOLUTION STARTS + if ( data_buffer ) delete data_buffer; + data_buffer_size = num_coor * 2; + data_buffer = new float[ data_buffer_size ]; + data_buffer_updated = false; } // Outer Loop: z axis (down axis of tube). // - if ( !opt_gpu ) + if ( !opt_gpu || !data_buffer_updated ) // SOLUTION { pCoor* cptr = coor_buffer; pVect* nptr = norm_buffer; + float* dptr = data_buffer; // SOLUTION const float delta_theta = M_PI / pattern_width; @@ -502,6 +497,8 @@ Tube::render() *cptr++ = pCoor(x_shift + r * cos_theta, r * sin_theta, z); *nptr++ = pVect(-cos_theta,-sin_theta,cos_z); + *dptr++ = theta; *dptr++ = z; // SOLUTION + theta += delta_theta; cos_theta = tarray.cos(theta); sin_theta = tarray.sin(theta); @@ -511,6 +508,8 @@ Tube::render() *cptr++ = pCoor(x_shift + rz1 * cos_theta, rz1 * sin_theta, z1); *nptr++ = pVect(-cos_theta,-sin_theta,cos_z1); + *dptr++ = theta; *dptr++ = z1; // SOLUTION + theta += delta_theta; cos_theta = tarray.cos(theta); sin_theta = tarray.sin(theta); @@ -520,14 +519,42 @@ Tube::render() *cptr++ = pCoor(x_shift + r * cos_theta, r * sin_theta, z); *nptr++ = pVect(-cos_theta,-sin_theta,cos_z); + *dptr++ = theta; *dptr++ = z; // SOLUTION + } z = next_z; } + + // SOLUTION STARTS + // + // Write data to buffer object, if necessary. + // + if ( !data_buffer_updated ) + { + glBindBuffer(GL_ARRAY_BUFFER, gpu_data_buffer); + glBufferData + (GL_ARRAY_BUFFER, + data_buffer_size * sizeof(data_buffer[0]), + data_buffer, GL_STATIC_DRAW); + glBindBuffer(GL_ARRAY_BUFFER, 0); + num_bytes += data_buffer_size * sizeof(data_buffer[0]); + data_buffer_updated = true; + } + } if ( opt_gpu ) { + // SOLUTION STARTS + + pError_Check(); + vs_tube->use(); + pError_Check(); + ptr_glUniform2f(uni_tube_geometry,x_shift,r0); + ptr_glUniform3f(uni_wave,phase,ampl,radians_per_z); + num_bytes += 5 * 4; + pError_Check(); // // Specify Vertices to GL in one of Three Ways @@ -539,7 +566,12 @@ Tube::render() // Individual Vertex Specification (Buffering) // { - + float* const stop = &data_buffer[data_buffer_size]; + glBegin(GL_TRIANGLES); + for( float *dptr = data_buffer; dptr < stop; dptr+=2 ) + glVertex2fv(dptr); + glEnd(); + num_bytes += data_buffer_size * sizeof(data_buffer[0]); } break; @@ -549,7 +581,13 @@ Tube::render() // Vertices given to OpenGL as a single array each // time this code reached. { + glVertexPointer(2,GL_FLOAT,0,data_buffer); + glEnableClientState(GL_VERTEX_ARRAY); + glDrawArrays(GL_TRIANGLES,0,num_coor); + + glDisableClientState(GL_VERTEX_ARRAY); + num_bytes += data_buffer_size * sizeof(data_buffer[0]); } break; @@ -561,11 +599,21 @@ Tube::render() // glBufferData above). Hopefully the vertices are now on the // GPU. Code below merely refers to that data. // + glBindBuffer(GL_ARRAY_BUFFER,gpu_data_buffer); + glVertexPointer(2,GL_FLOAT,0,NULL); + glBindBuffer(GL_ARRAY_BUFFER,0); + glEnableClientState(GL_VERTEX_ARRAY); + + glDrawArrays(GL_TRIANGLES,0,num_coor); + glDisableClientState(GL_VERTEX_ARRAY); } break; } + pError_Check(); + vs_fixed->use(); + } else { #endif #include <stdio.h> #include <strings.h> #include <stdlib.h> #include <deque> #define GL_GLEXT_PROTOTYPES #define GLX_GLXEXT_PROTOTYPES #include <GL/gl.h> #include <GL/glext.h> #include <GL/glx.h> #include <GL/glxext.h> #include <GL/glu.h> #include <GL/freeglut.h> #include "util.h" #include "coord.h" #include "shader.h" // Display a tetrahedron, used to indicate light position. // void insert_tetrahedron(pCoor& loc, float size) { pCoor v0(loc.x,loc.y,loc.z); pCoor v1(loc.x,loc.y-size,loc.z+size); pCoor v2(loc.x-.866*size,loc.y-size,loc.z-0.5*size); pCoor v3(loc.x+.866*size,loc.y-size,loc.z-0.5*size); static pColor c1(0xffffff); static pColor c2(0xff00); glDisable(GL_LIGHTING); #define TRI(va,vb,vc) \ { \ pVect n = cross(va,vb,vc); \ glNormal3fv(n); \ glColor3fv(c1); glVertex3fv(va); \ glColor3fv(c2); glVertex3fv(vb); \ glVertex3fv(vc); \ } glBegin(GL_TRIANGLES); TRI(v0,v1,v2); TRI(v0,v2,v3); TRI(v0,v3,v1); glEnd(); # undef TRI glEnable(GL_LIGHTING); } // Class for re-using sine and cosine values. // class MTrig { public: MTrig():size(0),storage(NULL){} void init(int sizep){ size = sizep; if ( storage ) delete storage; storage = new float[size]; idx = 0; full = false; } float sin(float theta){ return trig(theta,::sin); } float cos(float theta){ return trig(theta,::cos); } private: float trig(float theta,double (*func)(double)) { if ( !full ) { storage[idx] = func(theta); full = idx == size - 1; } if ( idx == size ) idx = 0; return storage[idx++]; } int size; float* storage; int idx; bool full; }; // /// Tube Object // class Tube { public: Tube(pOpenGL_Helper &fb):ogl_helper(fb){ init(); } static void render_w(void *moi){ ((Tube*)moi)->render(); } void init(); void render(); private: pOpenGL_Helper &ogl_helper; pVariable_Control variable_control; pFrame_Timer frame_timer; pShader *vs_fixed; // Fixed functionality. pShader *vs_lighting; // Lighting tweak. pVect to_eye_vector; float r0; float x_shift; float pattern_pitch_z; float opt_pattern_levels; float opt_pattern_width; float opt_light_intensity; int opt_v_buffering_cpu; int opt_v_buffering_gpu; pCoor opt_light_location; double time_app_start; pCoor* coor_buffer; pVect* norm_buffer; int num_coor_alloc; MTrig tarray; // SOLUTION STARTS bool opt_gpu; float* data_buffer; bool data_buffer_updated; GLuint gpu_data_buffer; int data_buffer_size; pShader *vs_tube; GLint uni_wave; GLint uni_tube_geometry; }; void Tube::init() { time_app_start = time_wall_fp(); // Tell frame timer that work unit is "MB/s" and how should be scaled. // frame_timer.work_unit_set("MB/s",1e-6); r0 = 2; // Tube radius. x_shift = 0.4; // Tube x offset. pattern_pitch_z = 0.25; // Triangle size (z axis). opt_pattern_levels = 50; // Tube depth (z direction.) opt_pattern_width = 200; // Number of triangles along circumference. opt_light_intensity = 2; opt_v_buffering_cpu = 0; opt_v_buffering_gpu = 0; opt_light_location.set(( r0 - 0.1 ), 0, -3 ); to_eye_vector.set(-1,-0.5,-3); // Arrange that variables below can be modified from the keyboard. // variable_control.insert(opt_light_intensity,"Light Intensity"); variable_control.insert(opt_pattern_levels,"Pattern Levels"); variable_control.insert(to_eye_vector.x,"Viewer X"); coor_buffer = NULL; norm_buffer = NULL; num_coor_alloc = 0; glEnable(GL_NORMALIZE); // Declared like a programmable shader, but used for fixed-functionality. vs_fixed = new pShader(); vs_lighting = new pShader("hw5_shader.cc","vs_main_lighting();"); opt_gpu = false; // SOLUTION STARTS data_buffer = NULL; data_buffer_updated = false; glGenBuffers(1,&gpu_data_buffer); vs_tube = new pShader("hw5_shader.cc","vs_main_tube();"); uni_wave = vs_tube->uniform_location("uni_wave"); uni_tube_geometry = vs_tube->uniform_location("uni_tube_geometry"); } void Tube::render() { frame_timer.frame_start(); /// /// Reset Frame and Z Buffers /// glClearColor(0,0,0.0,0.5); glClearDepth(1.0); glClear( GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT ); // Have frame timer provide timing information for top of image. // ogl_helper.fbprintf("%s\n",frame_timer.frame_rate_text_get()); /// /// Transformation Matrices Setup /// glMatrixMode(GL_MODELVIEW); glLoadIdentity(); glTranslatef(to_eye_vector.x,to_eye_vector.y,to_eye_vector.z); const int win_width = ogl_helper.get_width(); const int win_height = ogl_helper.get_height(); const float aspect = float(win_width) / win_height; glMatrixMode(GL_PROJECTION); glLoadIdentity(); glFrustum(-0.8,+0.8,-0.8/aspect,0.8/aspect,1,5000); glViewport(0, 0, win_width, win_height); pError_Check(); /// /// Light Location and Lighting Options /// // Adjust options based on user input. // switch ( ogl_helper.keyboard_key ) { case FB_KEY_LEFT: opt_light_location.x -= 0.1; break; case FB_KEY_RIGHT: opt_light_location.x += 0.1; break; case FB_KEY_UP: opt_light_location.y += 0.1; break; case FB_KEY_DOWN: opt_light_location.y -= 0.1; break; case FB_KEY_PAGE_DOWN: opt_light_location.z += 0.2; break; case FB_KEY_PAGE_UP: opt_light_location.z -= 0.2; break; case 'a': case 'A': opt_gpu = !opt_gpu; break; case 9: variable_control.switch_var_right(); break; case '-':case '_': variable_control.adjust_lower(); break; case '+':case '=': variable_control.adjust_higher(); break; case 'v': case 'V': if ( opt_gpu ) { opt_v_buffering_gpu++; if ( opt_v_buffering_gpu == 3 ) opt_v_buffering_gpu = 0; } else { opt_v_buffering_cpu++; if ( opt_v_buffering_cpu == 2 ) opt_v_buffering_cpu = 0; } break; default: break; } glLightfv(GL_LIGHT0, GL_POSITION, opt_light_location); const float light_intensity[4] = {opt_light_intensity, opt_light_intensity, opt_light_intensity, 1.0}; const float light_off[4] = {0,0,0,0}; const float light_dim[4] = {0.1,0.1,0.1,1}; glLightModelfv(GL_LIGHT_MODEL_AMBIENT, &light_dim[0]); glLightfv(GL_LIGHT0, GL_DIFFUSE, &light_intensity[0]); glLightfv(GL_LIGHT0, GL_AMBIENT, &light_off[0]); glLightf(GL_LIGHT0, GL_CONSTANT_ATTENUATION, 0); glLightf(GL_LIGHT0, GL_LINEAR_ATTENUATION, 1); glLightf(GL_LIGHT0, GL_QUADRATIC_ATTENUATION, 0.25); glEnable(GL_LIGHT0); glEnable(GL_LIGHTING); glColorMaterial(GL_FRONT_AND_BACK,GL_AMBIENT_AND_DIFFUSE); glEnable(GL_COLOR_MATERIAL); const char* const v_buffering_str[] = { "individual", "client array", "buffer object" }; ogl_helper.fbprintf ("VERTEX BUFFERING: CPU %s GPU %s (v TO CHANGE)\n", v_buffering_str[opt_v_buffering_cpu], v_buffering_str[opt_v_buffering_gpu]); ogl_helper.fbprintf ("GPU TUBE COMPUTATION: %s (A TO CHANGE)\n", opt_gpu ? "gpu" : "cpu"); ogl_helper.fbprintf ("PROGRAMMABLE GPU API: %s GPU CODE: %s\n", ptr_glCreateShader ? "yes" : "no", vs_lighting->pobject ? "okay" : "problem"); pVariable_Control_Elt* const cvar = variable_control.current; ogl_helper.fbprintf("VAR %s = %.3f\n",cvar->name,cvar->var[0]); glEnable(GL_DEPTH_TEST); glDepthFunc(GL_LESS); // Insert marker (green tetrahedron) to show light location. // vs_fixed->use(); insert_tetrahedron(opt_light_location,0.05); /// /// Insert a tessellated tube in the vertex array. /// float z = -1; pColor color_purple(0x580da6); // LSU Spirit Purple pColor color_gold(0xf9b237); // LSU Spirit Gold // // Compute Tube Specifications // const int pattern_width = 3 * int( opt_pattern_width * 0.33333333 ); const int pattern_levels = int( opt_pattern_levels + 0.5 ); // Number of vertices passed to OpenGL. const int vertices_per_ring = 3 * 2 * pattern_width; const int num_coor = pattern_levels * vertices_per_ring; const double cycles_per_second = 0.2; const double phase_n = ( time_wall_fp() - time_app_start ) * cycles_per_second; const double phase = phase_n * 2.0 * M_PI; const float wavelength_z = 2.8; const float radians_per_z = 2.0 * M_PI / wavelength_z; int num_bytes = 0; const float ampl = 0.4; glColor3fv( color_gold ); // If number of vertices has changed re-allocate our storage // (coor_buffer, norm_buffer) and MTrig object and also remember // that gpu's buffer needs to be updated. if ( num_coor_alloc != num_coor ) { if ( coor_buffer ) { delete coor_buffer; delete norm_buffer; } coor_buffer = new pCoor[num_coor]; norm_buffer = new pVect[num_coor]; tarray.init( vertices_per_ring * 4 ); num_coor_alloc = num_coor; // SOLUTION STARTS if ( data_buffer ) delete data_buffer; data_buffer_size = num_coor * 2; data_buffer = new float[ data_buffer_size ]; data_buffer_updated = false; } // Outer Loop: z axis (down axis of tube). // if ( !opt_gpu || !data_buffer_updated ) // SOLUTION { pCoor* cptr = coor_buffer; pVect* nptr = norm_buffer; float* dptr = data_buffer; // SOLUTION const float delta_theta = M_PI / pattern_width; for ( int i = 0; i < pattern_levels; i++ ) { const float next_z = z - pattern_pitch_z; const float last_z = z + pattern_pitch_z; float theta = i & 1 ? delta_theta : 0; const float angle_z = phase + radians_per_z * z; const float angle_nz = phase + radians_per_z * next_z; const float angle_lz = phase + radians_per_z * last_z; const float r = r0 * ( 1 + ampl * sin( angle_z ) ); const float rnz = r0 * ( 1 + ampl * sin( angle_nz ) ); const float rlz = r0 * ( 1 + ampl * sin( angle_lz ) ); const float cos_z = cos(angle_z); const float cos_lz = cos(angle_lz); const float cos_nz = cos(angle_nz); pCoor* const cptr_stop = cptr + vertices_per_ring; // Inner Loop: around circumference of tube. // while ( cptr < cptr_stop ) { // This loop iterates for two trips around the ring, // on the first trip first_round is true and triangles point // away from the viewer, on the second round they point towards // the viewer. const bool first_round = theta < 2 * M_PI; const float z1 = first_round ? next_z : last_z; const float rz1 = first_round ? rnz : rlz; const float cos_z1 = first_round ? cos_nz : cos_lz; float cos_theta = tarray.cos(theta); // Reassigned float sin_theta = tarray.sin(theta); // Reassigned // First vertex of triangle. // *cptr++ = pCoor(x_shift + r * cos_theta, r * sin_theta, z); *nptr++ = pVect(-cos_theta,-sin_theta,cos_z); *dptr++ = theta; *dptr++ = z; // SOLUTION theta += delta_theta; cos_theta = tarray.cos(theta); sin_theta = tarray.sin(theta); // Second vertex of triangle. // *cptr++ = pCoor(x_shift + rz1 * cos_theta, rz1 * sin_theta, z1); *nptr++ = pVect(-cos_theta,-sin_theta,cos_z1); *dptr++ = theta; *dptr++ = z1; // SOLUTION theta += delta_theta; cos_theta = tarray.cos(theta); sin_theta = tarray.sin(theta); // Third vertex of triangle. // *cptr++ = pCoor(x_shift + r * cos_theta, r * sin_theta, z); *nptr++ = pVect(-cos_theta,-sin_theta,cos_z); *dptr++ = theta; *dptr++ = z; // SOLUTION } z = next_z; } // SOLUTION STARTS // // Write data to buffer object, if necessary. // if ( !data_buffer_updated ) { glBindBuffer(GL_ARRAY_BUFFER, gpu_data_buffer); glBufferData (GL_ARRAY_BUFFER, data_buffer_size * sizeof(data_buffer[0]), data_buffer, GL_STATIC_DRAW); glBindBuffer(GL_ARRAY_BUFFER, 0); num_bytes += data_buffer_size * sizeof(data_buffer[0]); data_buffer_updated = true; } } if ( opt_gpu ) { // SOLUTION STARTS pError_Check(); vs_tube->use(); pError_Check(); ptr_glUniform2f(uni_tube_geometry,x_shift,r0); ptr_glUniform3f(uni_wave,phase,ampl,radians_per_z); num_bytes += 5 * 4; pError_Check(); // // Specify Vertices to GL in one of Three Ways // switch ( opt_v_buffering_gpu ) { case 0: // Individual Vertex Specification (Buffering) // { float* const stop = &data_buffer[data_buffer_size]; glBegin(GL_TRIANGLES); for( float *dptr = data_buffer; dptr < stop; dptr+=2 ) glVertex2fv(dptr); glEnd(); num_bytes += data_buffer_size * sizeof(data_buffer[0]); } break; case 1: // Client Array Vertex Buffering // // Vertices given to OpenGL as a single array each // time this code reached. { glVertexPointer(2,GL_FLOAT,0,data_buffer); glEnableClientState(GL_VERTEX_ARRAY); glDrawArrays(GL_TRIANGLES,0,num_coor); glDisableClientState(GL_VERTEX_ARRAY); num_bytes += data_buffer_size * sizeof(data_buffer[0]); } break; case 2: { // Buffer Object Vertex Buffering // // Vertices given to OpenGL earlier (using a buffer object, see // glBufferData above). Hopefully the vertices are now on the // GPU. Code below merely refers to that data. // glBindBuffer(GL_ARRAY_BUFFER,gpu_data_buffer); glVertexPointer(2,GL_FLOAT,0,NULL); glBindBuffer(GL_ARRAY_BUFFER,0); glEnableClientState(GL_VERTEX_ARRAY); glDrawArrays(GL_TRIANGLES,0,num_coor); glDisableClientState(GL_VERTEX_ARRAY); } break; } pError_Check(); vs_fixed->use(); } else { // // Send CPU-Computed Vertices to GPU // vs_lighting->use(); switch ( opt_v_buffering_cpu ) { case 0: // Individual Vertex Specification (Buffering) // { glBegin(GL_TRIANGLES); for ( int i=0; i<num_coor; i++ ) { glNormal3fv(norm_buffer[i]); glVertex3fv(coor_buffer[i]); num_bytes += sizeof(float) * 6; } glEnd(); } break; case 1: // Client Array Vertex Buffering // // Vertices given to OpenGL as a single array each // time this code reached. { glNormalPointer(GL_FLOAT,0,norm_buffer); glVertexPointer(3,GL_FLOAT,sizeof(pCoor),coor_buffer); glEnableClientState(GL_NORMAL_ARRAY); glEnableClientState(GL_VERTEX_ARRAY); glDrawArrays(GL_TRIANGLES,0,num_coor); glDisableClientState(GL_NORMAL_ARRAY); glDisableClientState(GL_VERTEX_ARRAY); num_bytes += sizeof(float) * 6 * num_coor; } break; case 2: // It should be impossible to select this option. // Buffer Object Vertex Buffering // // Vertices given to OpenGL earlier (using a buffer object, see // glBufferData above). Hopefully the vertices are now on the // GPU. // This option not implemented because vertex positions change // from frame to frame and so there would be little advantage // in using a buffer object since the buffer object data // would have to be sent over every frame. break; } } pError_Check(); frame_timer.work_amt_set(num_bytes); // Insert additional triangle. // { pCoor v0( 1.5, 0, -3.2 ); pCoor v1( 0, 5, -5 ); pCoor v2( 9, 6, -9 ); pVect normal(cross(v0,v1,v2)); glColor3fv( color_purple ); glBegin(GL_TRIANGLES); glNormal3fv(normal); glVertex3fv(v0); glNormal3fv(normal); glVertex3fv(v1); glNormal3fv(normal); glVertex3fv(v2); glEnd(); } glColor3f(0,1,0); // This sets the text color. Don't know why. pError_Check(); frame_timer.frame_end(); glutSwapBuffers(); } int main(int argc, char **argv) { pOpenGL_Helper popengl_helper(argc,argv); Tube tube(popengl_helper); popengl_helper.rate_set(30); popengl_helper.display_cb_set(tube.render_w,&tube); return 0; }