c++openglgouraud

Can't get Gouraud Shading in OpenGL to work


I'm trying to get a shape to have some shading due to a light source but I'd like the shape to all be one colour.

My problem is that no matter how hard I try I cannot seem to get any shading on a singular colour model. I've simplified my model to a single triangle to make this example clearer:

#include <GL/glut.h>
#include <math.h>
#include <iostream>

#include<map>
#include<vector>

using namespace std;

/* Verticies for simplified demo */
float vertices[][3] = {
            {0.1, 0.1, 0.1},
            {0.2, 0.8, 0.3},
            {0.3, 0.5, 0.5},
            {0.8, 0.2, 0.1},
           };
const int VERTICES_SIZE = 4;
/* Polygons for simplified demo */
int polygon[][3] = {
                {0, 1, 3},
                {0, 2, 1},
                {0, 3, 2},
                {1, 2, 3},
            };
const int POLYGON_SIZE = 4;
/* Average point for looking at */
float av_point[3];

/*
 * Holds the normal for each vertex calculated by averaging the
 * planar normals that each vertex is connected to.
 * It holds {index_of_vertex_in_vertices : normal}
 */
map<int, float*> vertex_normals;

/*
 * Calculates average point in list of vertices
 * Stores in result
 */
void averagePoint(float vertices[][3], int length, float result[3]) {
  for(int i = 0; i < length; i++) {
    result[0] += vertices[i][0];
    result[1] += vertices[i][1];
    result[2] += vertices[i][2];
  }

  result[0] /= length;
  result[1] /= length;
  result[2] /= length;
}

/*
 * Performs inplace normalisation of vector v
 */
void normalise(float v[3]) {
  GLfloat length = sqrt(v[0] * v[0] + v[1] * v[1] + v[2] * v[2]);
  v[0] /= length;
  v[1] /= length;
  v[2] /= length;
}

/*
 * Performs cross product of vectors u and v and stores
 * result in result
 * Normalises result.
 */
void crossProduct(float u[], float v[], float result[]) {
  result[0] = u[1] * v[2] - u[2] * v[1];
  result[1] = u[2] * v[0] - u[0] * v[2];
  result[2] = u[0] * v[1] - u[1] * v[0];
}

/*
 * Calculates normal for plane
 */
void calculate_normal(int polygon[3], float vertices[][3], float normal[3]) {
  GLfloat u[3], v[3];
  for (int i = 0; i < 3; i++) {
    u[i] = vertices[polygon[0]][i] - vertices[polygon[1]][i];
    v[i] = vertices[polygon[2]][i] - vertices[polygon[1]][i];
  }

  crossProduct(u, v, normal);
  normalise(normal);
}

/*
 * Populates vertex_normal with it's averaged face normal
 */
void calculate_vertex_normals (map<int, float*> &vertex_normal){
  map<int, vector<int> > vertex_to_faces;
  map<int, float*> faces_to_normal;
  // Loop over faces
  for (int i = 0; i < POLYGON_SIZE; i++) {
    float* normal = new float[3];
    calculate_normal(polygon[i], vertices, normal);
    for (int j = 0; j < 3; j++) {
     vertex_to_faces[polygon[i][j]].push_back(i);
    }
    faces_to_normal[i] = normal;
  }


  vertex_normal.clear();
  // Loop over vertices
  for (int v = 0; v < VERTICES_SIZE; v++) {
    vector<int> faces = vertex_to_faces[v];
    int faces_count = 0;
    float* normal = new float[3];
    for (vector<int>::iterator it = faces.begin(); it != faces.end(); ++it){
      normal[0] += faces_to_normal[*it][0];
      normal[1] += faces_to_normal[*it][1];
      normal[2] += faces_to_normal[*it][2];
      faces_count++;
    }
    normal[0] /= faces_count;
    normal[1] /= faces_count;
    normal[2] /= faces_count;
    vertex_normal[v] = normal;
  }

  // Delete normal declared in first loop
  for (int i = 0; i < POLYGON_SIZE; i++) {
    delete faces_to_normal[i];
  }
}

/*
 * Draws polygons in polygon array.
 */
void draw_polygon() {
  for(int i = 0; i < POLYGON_SIZE; i++) {
    glBegin(GL_POLYGON);
    for(int j = 0; j < 3; j++) {
      glNormal3fv(vertex_normals[polygon[i][j]]);
      glVertex3fv(vertices[polygon[i][j]]);
    }
    glEnd();
  }
}


/*
 * Sets up lighting and material properties
 */
void init()
{
  // Calculate average point for looking at
  averagePoint(vertices, VERTICES_SIZE, av_point);

  // Calculate vertices average normals
  calculate_vertex_normals(vertex_normals);

  glClearColor (0.0, 0.0, 0.0, 0.0);
  cout << "init" << endl;

  // Intialise and set lighting parameters
  GLfloat light_pos[] = {1.0, 1.0, 1.0, 0.0};
  GLfloat light_ka[] = {0.2, 0.2, 0.2, 1.0};
  GLfloat light_kd[] = {1.0, 1.0, 1.0, 1.0};
  GLfloat light_ks[] = {1.0, 1.0, 1.0, 1.0};

  glLightfv(GL_LIGHT0, GL_POSITION, light_pos);
  glLightfv(GL_LIGHT0, GL_AMBIENT,  light_ka);
  glLightfv(GL_LIGHT0, GL_DIFFUSE,  light_kd);
  glLightfv(GL_LIGHT0, GL_SPECULAR, light_ks);

  // Initialise and set material parameters
  GLfloat material_ka[] = {1.0, 1.0, 1.0, 1.0};
  GLfloat material_kd[] = {0.43, 0.47, 0.54, 1.0};
  GLfloat material_ks[] = {0.33, 0.33, 0.52, 1.0};
  GLfloat material_ke[] = {0.0, 0.0, 0.0, 0.0};
  GLfloat material_se[] = {10.0};

  glMaterialfv(GL_FRONT_AND_BACK, GL_AMBIENT,  material_ka);
  glMaterialfv(GL_FRONT_AND_BACK, GL_DIFFUSE,  material_kd);
  glMaterialfv(GL_FRONT_AND_BACK, GL_SPECULAR,  material_ks);
  glMaterialfv(GL_FRONT_AND_BACK, GL_EMISSION,  material_ke);
  glMaterialfv(GL_FRONT_AND_BACK, GL_SHININESS, material_se);

  // Smooth shading
  glShadeModel(GL_SMOOTH);

  // Enable lighting
  glEnable (GL_LIGHTING);
  glEnable (GL_LIGHT0);

  // Enable Z-buffering
  glEnable(GL_DEPTH_TEST);
}

/*
 * Free's resources
 */
void destroy() {
  for (int i = 0; i < VERTICES_SIZE; i++) {
    delete vertex_normals[i];
  }
}

/*
 * Display simple polygon
 */
void display (){
  glClear  (GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
  draw_polygon();
  glutSwapBuffers();
}

/*
 * Sets up camera perspective and view point
 * Looks at average point in model.
 */
void reshape (int w, int h)
{
  glMatrixMode(GL_PROJECTION);
  glLoadIdentity();
  gluPerspective(70, 1.0, 0.1, 1000);
  glMatrixMode(GL_MODELVIEW);
  glLoadIdentity();
  gluLookAt(0, 0, 1, av_point[0], av_point[1], av_point[2], 0, 0.5, 0);
}

int main (int argc, char **argv)
{

  // Initialize graphics window
  glutInit(&argc, argv);
  glutInitWindowSize(256, 256);
  glutInitDisplayMode(GLUT_DEPTH | GLUT_DOUBLE);

  // Initialize OpenGL
  init();

  glutCreateWindow("Rendering");
  glutDisplayFunc(display);
  glutReshapeFunc(reshape);

  glutMainLoop   ();

  destroy();

  return 1;
}

I'm really new to OpenGL so I'm hoping that it's something simple. Since I've remembered to set my normals so I'm not sure what else is going wrong.

The end aim is to render a face with Gouraud shading (and then textures) for my coursework however we've almost been left to figure out OpenGL (1.4 - course requirement) for ourselves, and we aren't allowed to use shaders. I'm trying to create something similar to this picture (taken from Google): enter image description here

with my triangle.


Solution

  • shading due to a light source but I'd like the shape to all be one colour.

    Aren't those two requirements mutually exclusive? What exactly is your desired outcome. Can you draw a picture what you're imagining? When it comes to implementing, using shaders is a lot easier than juggling with a gazillion of OpenGL state machine switches.

    Update

    Anyway here's my revised version of OPs code that draws a single triangle subject to Gourad illumination. This code compiles and draw a single triangle with a hint of a specular reflex.

    Let's go through what I did. First there's your original setup of the triangle. Nothing special here and nothing changed either (except a few includes) (EDIT) on second look I did a change. The use of a std::map was totally unaccounted for. We know the number of vertices and can just preallocate the normals' memory.

    #include <GL/glut.h>
    #include <math.h>
    
    // for memcpy
    #include <string.h>
    
    #include <map>
    #include <vector>
    #include <iostream>
    
    using namespace::std;
    
    /* Verticies for simplified demo */
    const int VERTICES_SIZE = 4;
    float vertices[VERTICES_SIZE][3] = {
                {0.1, 0.1, 0.1},
                {0.2, 0.8, 0.3},
                {0.3, 0.5, 0.5},
                {0.8, 0.2, 0.1},
               };
    
    // this is now a plain array
    float vertex_normals[VERTICES_SIZE][3];
    
    /* Polygons for simplified demo */
    const int POLYGON_SIZE = 4;
    int polygon[POLYGON_SIZE][3] = {
                    {0, 1, 3},
                    {0, 2, 1},
                    {0, 3, 2},
                    {1, 2, 3},
    };
    
    /* Average point for looking at */
    float av_point[3];
        
    /*
     * Calculates average point in list of vertices
     * Stores in result
     */
    void averagePoint(float vertices[][3], int length, float result[3]) {
      for(int i = 0; i < length; i++) {
        result[0] += vertices[i][0];
        result[1] += vertices[i][1];
        result[2] += vertices[i][2];
      }
    
      result[0] /= length;
      result[1] /= length;
      result[2] /= length;
    }
    
    /*
     * Performs inplace normalisation of vector v
     */
    void normalise(float v[3]) {
      GLfloat length = sqrtf(v[0] * v[0] + v[1] * v[1] + v[2] * v[2]);
      v[0] /= length;
      v[1] /= length;
      v[2] /= length;
    }
    
    /*
     * Performs cross product of vectors u and v and stores
     * result in result
     * Normalises result.
     */
    void crossProduct(float u[], float v[], float result[]) {
      result[0] = u[1] * v[2] - u[2] * v[1];
      result[1] = u[2] * v[0] - u[0] * v[2];
      result[2] = u[0] * v[1] - u[1] * v[0];
    }
    
    /*
     * Calculates normal for plane
     */
    void calculate_normal(int polygon[3], float vertices[][3], float normal[3]) {
      GLfloat u[3], v[3];
      for (int i = 0; i < 3; i++) {
        u[i] = vertices[polygon[0]][i] - vertices[polygon[1]][i];
        v[i] = vertices[polygon[2]][i] - vertices[polygon[1]][i];
      }
    
      crossProduct(u, v, normal);
      normalise(normal);
    }
    

    EDIT: My next change was here. See the comment

    /*
     * Populates normals with it's averaged face normal
     *
     * Passing the normal output buffer as a parameter was a bit
     * pointless, as this procedure accesses global variables anyway.
     * Either pass everything as parameters or noting at all,
     * be consequent. And doing it mixed is pure evil.
     */
    void calculate_vertex_normals()
    {
      // We love RAII, no need for new and delete!
      vector< vector<int> > vertex_to_faces(POLYGON_SIZE);
      vector< vector<float> > faces_to_normal(POLYGON_SIZE);
    
      // Loop over faces
      for (int i = 0; i < POLYGON_SIZE; i++) {
        vector<float> normal(3);
        calculate_normal(polygon[i], vertices, &normal[0]);
        for (int j = 0; j < 3; j++) {
         vertex_to_faces[polygon[i][j]].push_back(i);
        }
        faces_to_normal[i] = normal;
      }
    
      // Loop over vertices
      for (int v = 0; v < VERTICES_SIZE; v++) {
        // avoid a copy here by using a reference
        vector<int> &faces = vertex_to_faces[v];
        int faces_count = 0;
        float normal[3];
        for (vector<int>::iterator it = faces.begin(); it != faces.end(); ++it){
          normal[0] += faces_to_normal[*it][0];
          normal[1] += faces_to_normal[*it][1];
          normal[2] += faces_to_normal[*it][2];
          faces_count++;
        }
        // dividing a vector obtained by a number of unit length vectors
        // summed by the number of unit vectors summed does not normalize
        // it. You need to normalize it properly!
        normalise(normal);
    
        // memcpy is really be best choice here
        memcpy(vertex_normals[v], normal, sizeof(normal));
      }
    }
    

    draw_polygon is a rather unhappy name for this function. It draws a triangulated mesh. *EDIT: Also it can be written much nicer by employing vertex arrays (available since 1994 with OpenGL-1.1).

    /* 
     * Draws polygons in polygon array.
     */
    void draw_polygon() {
      glEnableClientState(GL_VERTEX_ARRAY);
      glEnableClientState(GL_NORMAL_ARRAY);
    
      glVertexPointer(3, GL_FLOAT, 0, &vertices[0][0]);
      glNormalPointer(GL_FLOAT, 0, &vertex_normals[0][0]);
    
      glDrawElements(GL_TRIANGLES, POLYGON_SIZE*3, GL_UNSIGNED_INT, polygon);
    }
    

    Here it's getting interesting. A common misconception is, that people think OpenGL is "initialized". That's not the case. What you initialize is data. In your case your geometry data

    /*
     * Sets up lighting and material properties
     */
    void init_geometry()
    {
      // Calculate average point for looking at
      averagePoint(vertices, VERTICES_SIZE, av_point);
    
      // Calculate vertices average normals
      calculate_vertex_normals(vertex_normals);
    }
    

    Here comes the tricky part: OpenGL fixed function illumination is a state as everything else. When you call glLightfv it will set internal parameters based on state when being called. The position is transformed by the modelview when calling this. But without a proper modelview being set up, you can't setup the illumination. Hence I put it into its own function, which we call right after setting up modelview in the drawing function.

    void setup_illumination()
    {
      // Intialise and set lighting parameters
      GLfloat light_pos[] = {1.0, 1.0, 1.0, 0.0};
      GLfloat light_ka[] = {0.2, 0.2, 0.2, 1.0};
      GLfloat light_kd[] = {1.0, 1.0, 1.0, 1.0};
      GLfloat light_ks[] = {1.0, 1.0, 1.0, 1.0};
    
      glLightfv(GL_LIGHT0, GL_POSITION, light_pos);
      glLightfv(GL_LIGHT0, GL_AMBIENT,  light_ka);
      glLightfv(GL_LIGHT0, GL_DIFFUSE,  light_kd);
      glLightfv(GL_LIGHT0, GL_SPECULAR, light_ks);
    
      // Initialise and set material parameters
      GLfloat material_ka[] = {1.0, 1.0, 1.0, 1.0};
      GLfloat material_kd[] = {0.43, 0.47, 0.54, 1.0};
      GLfloat material_ks[] = {0.33, 0.33, 0.52, 1.0};
      GLfloat material_ke[] = {0.0, 0.0, 0.0, 0.0};
      GLfloat material_se[] = {10.0};
    
      glMaterialfv(GL_FRONT_AND_BACK, GL_AMBIENT,  material_ka);
      glMaterialfv(GL_FRONT_AND_BACK, GL_DIFFUSE,  material_kd);
      glMaterialfv(GL_FRONT_AND_BACK, GL_SPECULAR,  material_ks);
      glMaterialfv(GL_FRONT_AND_BACK, GL_EMISSION,  material_ke);
      glMaterialfv(GL_FRONT_AND_BACK, GL_SHININESS, material_se);
    
      // Smooth shading
      glShadeModel(GL_SMOOTH);
    
      // Enable lighting
      glEnable (GL_LIGHTING);
      glEnable (GL_LIGHT0);
    }
    

    For the drawing function a few things were changed. See the comments in the code

    /*
     * Display simple polygon
     */
    void display (void)
    {
      // float window sizes are usefull for view volume calculations
      //
      // requesting the window dimensions for each drawing iteration
      // is just two function calls. Compare this to the number of function
      // calls a typical application will do for the actual rendering
      // Trying to optimize away those two calls is a fruitless microoptimization
      float const window_width  = glutGet(GLUT_WINDOW_WIDTH);
      float const window_height = glutGet(GLUT_WINDOW_HEIGHT);
      float const window_aspect = window_width / window_height;
    
      // glViewport operates independent of the projection --
      // another reason to put it into the drawing code
      glViewport(0, 0, window_width, window_height);
    
      glClearDepth(1.);
      glClearColor (0.0, 0.0, 0.0, 0.0);
      glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
    
      // It's a often made mistake to setup projection in the window resize
      // handler. Projection is a drawing state, hence should be set in
      // the drawing code. Also in most programs you will have multiple
      // projections mixed throughout rendering a single frame so there you
      // actually **must** set projection in drawing code, otherwise it
      // wouldn't work.
      glMatrixMode(GL_PROJECTION);
      glLoadIdentity();
      gluPerspective(70, window_aspect, 1, 100);
    
      glMatrixMode(GL_MODELVIEW);
      glLoadIdentity();
      gluLookAt(0, 0, -3, av_point[0], av_point[1], av_point[2], 0, 1, 0);
     
      // Fixed function pipeline light position setup calls operate on the current
      // modelview matrix, so we must setup the illumination parameters with the
      // modelview matrix at least after the view transformation (look-at) applied.
      setup_illumination();
     
      // Enable depth testing (z buffering would be enabled/disabled with glDepthMask)
      glEnable(GL_DEPTH_TEST);
    
      draw_polygon();
    
      glutSwapBuffers();
    }
       
    int main (int argc, char **argv)
    {    
      // Initialize graphics window
      glutInit(&argc, argv);
      glutInitWindowSize(256, 256);
      glutInitDisplayMode    (GLUT_DEPTH | GLUT_DOUBLE);
    
      // we actually have to create a window
      glutCreateWindow("illuination");
    
      // Initialize geometry
      init_geometry();
    
      glutDisplayFunc(display);
    
      glutMainLoop();
    
      return 0;
    }