// shaded.cc
// Draw a shaded sphere in OpenGL
//
// Copyright (C) 2004-2005, 2009 Frank C. Langbein, http://www.langbein.org/
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program.  If not, see <http://www.gnu.org/licenses/>.
//
// Compile with
//
//   c++ -o shaded shaded.cc -L/usr/X11R6/lib -lm -lX11 -lXext -lXt \
//      -lXmu -lXi -lGL -lGLU -lglut


// Standard headers
#include <stdlib.h>
#include <unistd.h>
#include <math.h>

// OpenGL header (includes everything necessary for OpenGL)
#include <GL/glut.h>


//////////////////////////////////////////////////////////////////////////////
// Constants, global variables, etc.

// Conversion factors for degrees to radians
const double deg_to_rad (M_PI / 180.0);
const double rad_to_deg (180.0 / M_PI);

// Id of display list containing the scene describtion
GLuint scene_dl (0U);

// Track mouse position
int mouse_x (0);
int mouse_y (0);
bool mouse_button1 (false);
bool mouse_button2 (false);
bool mouse_button3 (false);

// Camera position
double camera_x (3.0);
double camera_y (0.0);
double camera_z (0.0);
double camera_yaw (0.0);
double camera_pitch (0.0);

// Camera movement sensitivity
const double angle_sensitivity (0.005);
const double dist_sensitivity (0.01);

// Scene rotation
double scene_yaw (0.0);
double scene_pitch (0.0);


//////////////////////////////////////////////////////////////////////////////
// Functions to draw the models in the scene

// Draw a sphere
void sphere () {

  // Angle steps for facet size
  static double da (1.0);

  // Sphere material
  static GLfloat diffuse[] = {0.7, 0.0, 0.0, 1.0};
  static GLfloat ambient[] = {0.5, 0.0, 0.0, 1.0};
  static GLfloat specular[] = {1.0, 1.0, 1.0, 1.0};
  static GLfloat shine (127.0);

  // Set material
  glMaterialfv (GL_FRONT, GL_AMBIENT, ambient);
  glMaterialfv (GL_FRONT, GL_DIFFUSE, diffuse);
  glMaterialfv (GL_FRONT, GL_SPECULAR, specular);
  glMaterialfv (GL_FRONT, GL_SHININESS, &shine);

  // For shading we need surface normals in addition to vertex
  // positions; for the unit sphere the normal is the same than the
  // surface position (note that this is a very special case!).
  //
  // We first set the normal of the vertex and then the vertex itself.
  
  // Quadrilateral strips
  for (double phi = -90.0 + da; phi < 90.0; phi += da) {
    glBegin (GL_QUAD_STRIP);
    for (double thet = -180.0; thet <= 180.0; thet += da) {
      glNormal3d (sin (deg_to_rad * thet) * cos (deg_to_rad * phi),
		  cos (deg_to_rad * thet) * cos (deg_to_rad * phi),
		  sin (deg_to_rad * phi)); 
      glVertex3d (sin (deg_to_rad * thet) * cos (deg_to_rad * phi),
		  cos (deg_to_rad * thet) * cos (deg_to_rad * phi),
		  sin (deg_to_rad * phi)); 
      glNormal3d (sin (deg_to_rad * thet) * cos (deg_to_rad * (phi + da)),
		  cos (deg_to_rad * thet) * cos (deg_to_rad * (phi + da)),
		  sin (deg_to_rad * (phi + da)));  
      glVertex3d (sin (deg_to_rad * thet) * cos (deg_to_rad * (phi + da)),
		  cos (deg_to_rad * thet) * cos (deg_to_rad * (phi + da)),
		  sin (deg_to_rad * (phi + da)));  
    }
    glEnd();
  }
  
  // North pole
  glBegin (GL_TRIANGLE_FAN);
  glNormal3d (0.0, 0.0, 1.0);
  glVertex3d (0.0, 0.0, 1.0);
  for (double thet = -180.0; thet <= 180.0; thet += 10.0) {
    glNormal3d (sin (deg_to_rad * thet) * cos (deg_to_rad * (90.0 - da)),
		cos (deg_to_rad * thet) * cos (deg_to_rad * (90.0 - da)),
		sin (deg_to_rad * 80.0));
    glVertex3d (sin (deg_to_rad * thet) * cos (deg_to_rad * (90.0 - da)),
		cos (deg_to_rad * thet) * cos (deg_to_rad * (90.0 - da)),
		sin (deg_to_rad * 80.0));
  }
  glEnd();

  // South pole
  glBegin (GL_TRIANGLE_FAN);
  glNormal3d (0.0, 0.0, -1.0);
  glVertex3d (0.0, 0.0, -1.0);
  for (double thet = -180.0; thet <= 180.0; thet += 10.0) {
    glNormal3d (sin (deg_to_rad * thet) * cos (deg_to_rad * (-90.0 + da)),
		cos (deg_to_rad * thet) * cos (deg_to_rad * (-90.0 + da)),
		sin (deg_to_rad * (-90.0 + da)));
    glVertex3d (sin (deg_to_rad * thet) * cos (deg_to_rad * (-90.0 + da)),
		cos (deg_to_rad * thet) * cos (deg_to_rad * (-90.0 + da)),
		sin (deg_to_rad * (-90.0 + da)));
  }
  glEnd();

  return;
}

// Draw a teapot
void teapot () {

  // Teapot material
  static GLfloat diffuse[] = {0.0, 0.7, 0.0, 1.0};
  static GLfloat ambient[] = {0.0, 0.5, 0.0, 1.0};
  static GLfloat specular[] = {1.0, 1.0, 1.0, 1.0};
  static GLfloat shine (127.0);

  // Set material
  glMaterialfv (GL_FRONT, GL_AMBIENT, ambient);
  glMaterialfv (GL_FRONT, GL_DIFFUSE, diffuse);
  glMaterialfv (GL_FRONT, GL_SPECULAR, specular);
  glMaterialfv (GL_FRONT, GL_SHININESS, &shine);

  // Push current modelview matrix on a matrix stack to save current
  // transformation.
  glPushMatrix ();

  // Multiply modelview matrix with additional transformation to move
  // and rotate the teapot to a different position.
  glTranslated (0.0, 3.0, 0.0);
  glRotated (90.0, 1.0, 0.0, 0.0);
  
  // Glut function for drawing a solid teapot
  glutSolidTeapot (1.0);

  // Get original matrix back from stack (undo above transformation
  // for objects drawn after this one)
  glPopMatrix ();
    
  return;
}

// Main function called to setup the scene contents
void scene () {

  // Draw teapot
  teapot ();
  
  // Draw sphere
  sphere ();
  
  return;
}


//////////////////////////////////////////////////////////////////////////////
// Lights

// Initialise lighting
void init_lights () {

  // Specify light emitted by light source 0
  static GLfloat diffuse[] = {1.0, 1.0, 1.0, 1.0};
  static GLfloat ambient[] = {0.3, 0.3, 0.3, 1.0};
  static GLfloat specular[] = {1.0, 1.0, 1.0, 1.0};
  static GLfloat attenuation = 1;

  // Enable lighting
  glEnable (GL_LIGHTING);

  // Enable first light source
  glEnable (GL_LIGHT0);
  // Set light emitted by light source 0
  glLightfv (GL_LIGHT0, GL_AMBIENT, ambient);
  glLightfv (GL_LIGHT0, GL_DIFFUSE, diffuse);
  glLightfv (GL_LIGHT0, GL_SPECULAR, specular);
  glLightfv (GL_LIGHT0, GL_CONSTANT_ATTENUATION, &attenuation);

  return;
}

// Render (set position of) light source
void lights () {

  // Light source 0 position
  static GLfloat light0_pos[] = {10, 15, 20, 1.0};

  // Set light source 0 position
  glLightfv (GL_LIGHT0, GL_POSITION, light0_pos);

  return;
}


//////////////////////////////////////////////////////////////////////////////
// Callback functions for user interaction, etc.

// Display callback to render scene
void display_callback () {

  // Clear frame buffer
  glClear (GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

  // Setup modelview matrix (model and viewing transformation)
  glMatrixMode (GL_MODELVIEW);
  glLoadIdentity ();

  // Camera
  gluLookAt (camera_x + cos (camera_yaw) * cos (camera_pitch),
	     camera_y + sin (camera_yaw) * cos (camera_pitch),
	     camera_z + sin (camera_pitch),
	     camera_x, camera_y, camera_z,
	     0.0, 0.0, 1.0);

  // Scene
  glRotated (scene_yaw * rad_to_deg, 0.0, 0.0, 1.0);
  glRotated (scene_pitch * rad_to_deg, 0.0, 1.0, 0.0);

  // Set light positions: fixed position with respect to objects; if
  // fixed position with respect to viewer (e.g. viewer has a flash
  // light) draw after setting modelview matrix to identity above.
  lights ();
  
  // Render scene by calling display list
  // (without display lists call world () here)
  glCallList (scene_dl);

  // Flush OpenGL pipeline
  glFlush ();

  // Swap buffers (double buffering for smooth animation)
  glutSwapBuffers ();

  return;
}

// Window reshape callback
void reshape_callback (int w, int h) {

  // Setup viewport size
  glViewport (0, 0, w, h);
  
  // Setup projection matrix
  glMatrixMode (GL_PROJECTION);
  glLoadIdentity ();

  // Perspective projection
  gluPerspective (60.0, 
		  static_cast<double>(w) / static_cast<double> (h),
		  0.1, 1000.0);

  return;
}

// Keyboard callback called when key pressed
void keyboard_callback (unsigned char key, int x, int y) {

  // ESC for exit
  if (key == 27) {
    exit (0);
  }

  return;
}

// Mouse callback called when mouse button is pressed
void mouse_callback (int btn, int state, int x, int y) {

  switch (btn) {
  case GLUT_LEFT_BUTTON:
    mouse_button1 = (state == GLUT_DOWN);
    break;
  case GLUT_MIDDLE_BUTTON:
    mouse_button2 = (state == GLUT_DOWN);
    break;
  case GLUT_RIGHT_BUTTON:
    mouse_button3 = (state == GLUT_DOWN);
    break;
  }

  // Update mouse position
  mouse_x = x;
  mouse_y = y;

  // Mark window for redrawing.
  glutPostRedisplay ();
  
  return;
}

// Motion callback to capture mouse movements while buttons pressed
void motion_callback (int x, int y) {

  // Button 1 pressed: change viewing direction
  if (mouse_button1) {
    camera_yaw += angle_sensitivity * static_cast<double> (mouse_x - x);
    if (camera_yaw > M_PI) {
      camera_yaw -= 2.0 * M_PI;
    } else if (camera_yaw < -M_PI) {
      camera_yaw += 2.0 * M_PI;
    }
    camera_pitch += angle_sensitivity * static_cast<double> (y - mouse_y);
    if (camera_pitch > M_PI_2) {
      camera_pitch = M_PI_2;
    } else if (camera_pitch < -M_PI_2) {
      camera_pitch = -M_PI_2;
    }
  }

  // Button 2 pressed: change position
  if (mouse_button2) {
    camera_x += dist_sensitivity * static_cast<double> (y - mouse_y)
      * cos (camera_yaw) * cos (camera_pitch);
    camera_y += dist_sensitivity * static_cast<double> (y - mouse_y)
      * sin (camera_yaw) * cos (camera_pitch);
    camera_z += dist_sensitivity * static_cast<double> (y - mouse_y)
      * sin (camera_pitch);
  }

  // Button 3 pressed: rotate scene
  if (mouse_button3) {
    scene_yaw += angle_sensitivity * static_cast<double> (x - mouse_x);
    if (scene_yaw > M_PI) {
      scene_yaw -= 2.0 * M_PI;
    } else if (scene_yaw < -M_PI) {
      scene_yaw += 2.0 * M_PI;
    }
    scene_pitch += angle_sensitivity * static_cast<double> (y - mouse_y);
    if (scene_pitch > M_PI) {
      scene_pitch -= 2.0 * M_PI;
    } else if (scene_pitch < -M_PI) {
      scene_pitch += 2.0 * M_PI;
    }
  }

  // Update mouse position
  mouse_x = x;
  mouse_y = y;
    
  // Mark window for redrawing.
  glutPostRedisplay ();
  
  return;
}

// Passive motion callback to capture mouse movements while buttons
// are not pressed
void passive_motion_callback (int x, int y) {

  // Update mouse position
  mouse_x = x;
  mouse_y = y;

  return;
}


//////////////////////////////////////////////////////////////////////////////
// Main Function

int main(int argc, char **argv) {

  // Initialise GLUT
  glutInit (&argc, argv);
  // Use double buffering, RGB colours and depth buffer (for visible
  // surface detection)
  glutInitDisplayMode (GLUT_DOUBLE | GLUT_RGB | GLUT_DEPTH);

  // Create display window
  glutInitWindowSize (500, 500);
  glutCreateWindow ("sphere");

  // Setup callback functions for rendering the scene, user interaction, ...
  glutDisplayFunc (display_callback);
  glutReshapeFunc (reshape_callback);
  glutKeyboardFunc (keyboard_callback);
  glutMouseFunc (mouse_callback);
  glutMotionFunc (motion_callback);
  glutPassiveMotionFunc (passive_motion_callback);

  // Enable visible surface detection via depth tests
  glDepthFunc (GL_LEQUAL);
  glClearDepth (1.0);
  glEnable (GL_DEPTH_TEST);

  // Set clear color to black
  glClearColor (0.0, 0.0, 0.0, 1.0);

  // Fill polygons for shading
  glPolygonMode(GL_FRONT, GL_FILL);

  // Treat polygon front and back side equal (no two-sided polygons)
  glLightModeli (GL_LIGHT_MODEL_TWO_SIDE, GL_FALSE);

  // Enable local viewer model for specular light
  glLightModeli (GL_LIGHT_MODEL_LOCAL_VIEWER, GL_TRUE);

  // Enable smoooth shading (multi-coloured polygons)
  glShadeModel (GL_SMOOTH);

  // Initialise lights
  init_lights ();
  
  // Store scene in a display list: 
  //   first create one display list
  scene_dl = glGenLists (1);
  //   and then call the rendering functions to store the primitives
  //   in the list
  glNewList (scene_dl, GL_COMPILE);
  scene ();
  glEndList ();
  
  // Start GLUT event loop
  // (display callback will render the scene)
  glutMainLoop();

  return 0;
}

