text: //========================================================================
  // A simple particle engine with threaded physics
  // Copyright (c) Marcus Geelnard
  // Copyright (c) Camilla Löwy <elmindreda@glfw.org>
  //
  // This software is provided 'as-is', without any express or implied
  // warranty. In no event will the authors be held liable for any damages
  // arising from the use of this software.
  //
  // Permission is granted to anyone to use this software for any purpose,
  // including commercial applications, and to alter it and redistribute it
  // freely, subject to the following restrictions:
  //
  // 1. The origin of this software must not be misrepresented; you must not
  //    claim that you wrote the original software. If you use this software
  //    in a product, an acknowledgment in the product documentation would
  //    be appreciated but is not required.
  //
  // 2. Altered source versions must be plainly marked as such, and must not
  //    be misrepresented as being the original software.
  //
  // 3. This notice may not be removed or altered from any source
  //    distribution.
  //
  //========================================================================
  
  #if defined(_MSC_VER)
   // Make MS math.h define M_PI
   #define _USE_MATH_DEFINES
  #endif
  
  #include <stdlib.h>
  #include <stdio.h>
  #include <string.h>
  #include <math.h>
  #include <time.h>
  
  #include <tinycthread.h>
  #include <getopt.h>
  #include <linmath.h>
  
  #include <glad/glad.h>
  #include <GLFW/glfw3.h>
  
  // Define tokens for GL_EXT_separate_specular_color if not already defined
  #ifndef GL_EXT_separate_specular_color
  #define GL_LIGHT_MODEL_COLOR_CONTROL_EXT  0x81F8
  #define GL_SINGLE_COLOR_EXT               0x81F9
  #define GL_SEPARATE_SPECULAR_COLOR_EXT    0x81FA
  #endif // GL_EXT_separate_specular_color
  
  
  //========================================================================
  // Type definitions
  //========================================================================
  
  typedef struct
  {
      float x, y, z;
  } Vec3;
  
  // This structure is used for interleaved vertex arrays (see the
  // draw_particles function)
  //
  // NOTE: This structure SHOULD be packed on most systems. It uses 32-bit fields
  // on 32-bit boundaries, and is a multiple of 64 bits in total (6x32=3x64). If
  // it does not work, try using pragmas or whatever to force the structure to be
  // packed.
  typedef struct
  {
      GLfloat s, t;         // Texture coordinates
      GLuint  rgba;         // Color (four ubytes packed into an uint)
      GLfloat x, y, z;      // Vertex coordinates
  } Vertex;
  
  
  //========================================================================
  // Program control global variables
  //========================================================================
  
  // Window dimensions
  float aspect_ratio;
  
  // "wireframe" flag (true if we use wireframe view)
  int wireframe;
  
  // Thread synchronization
  struct {
      double    t;         // Time (s)
      float     dt;        // Time since last frame (s)
      int       p_frame;   // Particle physics frame number
      int       d_frame;   // Particle draw frame number
      cnd_t     p_done;    // Condition: particle physics done
      cnd_t     d_done;    // Condition: particle draw done
      mtx_t     particles_lock; // Particles data sharing mutex
  } thread_sync;
  
  
  //========================================================================
  // Texture declarations (we hard-code them into the source code, since
  // they are so simple)
  //========================================================================
  
  #define P_TEX_WIDTH  8    // Particle texture dimensions
  #define P_TEX_HEIGHT 8
  #define F_TEX_WIDTH  16   // Floor texture dimensions
  #define F_TEX_HEIGHT 16
  
  // Texture object IDs
  GLuint particle_tex_id, floor_tex_id;
  
  // Particle texture (a simple spot)
  const unsigned char particle_texture[ P_TEX_WIDTH * P_TEX_HEIGHT ] = {
      0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
      0x00, 0x00, 0x11, 0x22, 0x22, 0x11, 0x00, 0x00,
      0x00, 0x11, 0x33, 0x88, 0x77, 0x33, 0x11, 0x00,
      0x00, 0x22, 0x88, 0xff, 0xee, 0x77, 0x22, 0x00,
      0x00, 0x22, 0x77, 0xee, 0xff, 0x88, 0x22, 0x00,
      0x00, 0x11, 0x33, 0x77, 0x88, 0x33, 0x11, 0x00,
      0x00, 0x00, 0x11, 0x33, 0x22, 0x11, 0x00, 0x00,
      0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
  };
  
  // Floor texture (your basic checkered floor)
  const unsigned char floor_texture[ F_TEX_WIDTH * F_TEX_HEIGHT ] = {
      0xf0, 0xf0, 0xf0, 0xf0, 0xf0, 0xf0, 0xf0, 0xf0, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30,
      0xff, 0xf0, 0xcc, 0xf0, 0xf0, 0xf0, 0xff, 0xf0, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30,
      0xf0, 0xcc, 0xee, 0xff, 0xf0, 0xf0, 0xf0, 0xf0, 0x30, 0x66, 0x30, 0x30, 0x30, 0x20, 0x30, 0x30,
      0xf0, 0xf0, 0xf0, 0xf0, 0xf0, 0xee, 0xf0, 0xf0, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30,
      0xf0, 0xf0, 0xf0, 0xf0, 0xcc, 0xf0, 0xf0, 0xf0, 0x30, 0x30, 0x55, 0x30, 0x30, 0x44, 0x30, 0x30,
      0xf0, 0xdd, 0xf0, 0xf0, 0xf0, 0xf0, 0xf0, 0xf0, 0x33, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30,
      0xf0, 0xf0, 0xf0, 0xf0, 0xf0, 0xff, 0xf0, 0xf0, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x60, 0x30,
      0xf0, 0xf0, 0xf0, 0xf0, 0xf0, 0xf0, 0xf0, 0xf0, 0x33, 0x33, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30,
      0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x33, 0x30, 0xf0, 0xf0, 0xf0, 0xf0, 0xf0, 0xf0, 0xf0, 0xf0,
      0x30, 0x30, 0x30, 0x30, 0x30, 0x20, 0x30, 0x30, 0xf0, 0xff, 0xf0, 0xf0, 0xdd, 0xf0, 0xf0, 0xff,
      0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x55, 0x33, 0xf0, 0xf0, 0xf0, 0xf0, 0xf0, 0xff, 0xf0, 0xf0,
      0x30, 0x44, 0x66, 0x30, 0x30, 0x30, 0x30, 0x30, 0xf0, 0xf0, 0xf0, 0xf0, 0xf0, 0xf0, 0xf0, 0xf0,
      0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0xf0, 0xf0, 0xf0, 0xaa, 0xf0, 0xf0, 0xcc, 0xf0,
      0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0xff, 0xf0, 0xf0, 0xf0, 0xff, 0xf0, 0xdd, 0xf0,
      0x30, 0x30, 0x30, 0x77, 0x30, 0x30, 0x30, 0x30, 0xf0, 0xf0, 0xf0, 0xf0, 0xf0, 0xf0, 0xf0, 0xf0,
      0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0xf0, 0xf0, 0xf0, 0xf0, 0xf0, 0xf0, 0xf0, 0xf0,
  };
  
  
  //========================================================================
  // These are fixed constants that control the particle engine. In a
  // modular world, these values should be variables...
  //========================================================================
  
  // Maximum number of particles
  #define MAX_PARTICLES   3000
  
  // Life span of a particle (in seconds)
  #define LIFE_SPAN       8.f
  
  // A new particle is born every [BIRTH_INTERVAL] second
  #define BIRTH_INTERVAL (LIFE_SPAN/(float)MAX_PARTICLES)
  
  // Particle size (meters)
  #define PARTICLE_SIZE   0.7f
  
  // Gravitational constant (m/s^2)
  #define GRAVITY         9.8f
  
  // Base initial velocity (m/s)
  #define VELOCITY        8.f
  
  // Bounce friction (1.0 = no friction, 0.0 = maximum friction)
  #define FRICTION        0.75f
  
  // "Fountain" height (m)
  #define FOUNTAIN_HEIGHT 3.f
  
  // Fountain radius (m)
  #define FOUNTAIN_RADIUS 1.6f
  
  // Minimum delta-time for particle phisics (s)
  #define MIN_DELTA_T     (BIRTH_INTERVAL * 0.5f)
  
  
  //========================================================================
  // Particle system global variables
  //========================================================================
  
  // This structure holds all state for a single particle
  typedef struct {
      float x,y,z;     // Position in space
      float vx,vy,vz;  // Velocity vector
      float r,g,b;     // Color of particle
      float life;      // Life of particle (1.0 = newborn, < 0.0 = dead)
      int   active;    // Tells if this particle is active
  } PARTICLE;
  
  // Global vectors holding all particles. We use two vectors for double
  // buffering.
  static PARTICLE particles[MAX_PARTICLES];
  
  // Global variable holding the age of the youngest particle
  static float min_age;
  
  // Color of latest born particle (used for fountain lighting)
  static float glow_color[4];
  
  // Position of latest born particle (used for fountain lighting)
  static float glow_pos[4];
  
  
  //========================================================================
  // Object material and fog configuration constants
  //========================================================================
  
  const GLfloat fountain_diffuse[4]  = { 0.7f, 1.f,  1.f,  1.f };
  const GLfloat fountain_specular[4] = {  1.f, 1.f,  1.f,  1.f };
  const GLfloat fountain_shininess   = 12.f;
  const GLfloat floor_diffuse[4]     = { 1.f,  0.6f, 0.6f, 1.f };
  const GLfloat floor_specular[4]    = { 0.6f, 0.6f, 0.6f, 1.f };
  const GLfloat floor_shininess      = 18.f;
  const GLfloat fog_color[4]         = { 0.1f, 0.1f, 0.1f, 1.f };
  
  
  //========================================================================
  // Print usage information
  //========================================================================
  
  static void usage(void)
  {
      printf("Usage: particles [-bfhs]\n");
      printf("Options:\n");
      printf(" -f   Run in full screen\n");
      printf(" -h   Display this help\n");
      printf(" -s   Run program as single thread (default is to use two threads)\n");
      printf("\n");
      printf("Program runtime controls:\n");
      printf(" W    Toggle wireframe mode\n");
      printf(" Esc  Exit program\n");
  }
  
  
  //========================================================================
  // Initialize a new particle
  //========================================================================
  
  static void init_particle(PARTICLE *p, double t)
  {
      float xy_angle, velocity;
  
      // Start position of particle is at the fountain blow-out
      p->x = 0.f;
      p->y = 0.f;
      p->z = FOUNTAIN_HEIGHT;
  
      // Start velocity is up (Z)...
      p->vz = 0.7f + (0.3f / 4096.f) * (float) (rand() & 4095);
  
      // ...and a randomly chosen X/Y direction
      xy_angle = (2.f * (float) M_PI / 4096.f) * (float) (rand() & 4095);
      p->vx = 0.4f * (float) cos(xy_angle);
      p->vy = 0.4f * (float) sin(xy_angle);
  
      // Scale velocity vector according to a time-varying velocity
      velocity = VELOCITY * (0.8f + 0.1f * (float) (sin(0.5 * t) + sin(1.31 * t)));
      p->vx *= velocity;
      p->vy *= velocity;
      p->vz *= velocity;
  
      // Color is time-varying
      p->r = 0.7f + 0.3f * (float) sin(0.34 * t + 0.1);
      p->g = 0.6f + 0.4f * (float) sin(0.63 * t + 1.1);
      p->b = 0.6f + 0.4f * (float) sin(0.91 * t + 2.1);
  
      // Store settings for fountain glow lighting
      glow_pos[0] = 0.4f * (float) sin(1.34 * t);
      glow_pos[1] = 0.4f * (float) sin(3.11 * t);
      glow_pos[2] = FOUNTAIN_HEIGHT + 1.f;
      glow_pos[3] = 1.f;
      glow_color[0] = p->r;
      glow_color[1] = p->g;
      glow_color[2] = p->b;
      glow_color[3] = 1.f;
  
      // The particle is new-born and active
      p->life = 1.f;
      p->active = 1;
  }
  
  
  //========================================================================
  // Update a particle
  //========================================================================
  
  #define FOUNTAIN_R2 (FOUNTAIN_RADIUS+PARTICLE_SIZE/2)*(FOUNTAIN_RADIUS+PARTICLE_SIZE/2)
  
  static void update_particle(PARTICLE *p, float dt)
  {
      // If the particle is not active, we need not do anything
      if (!p->active)
          return;
  
      // The particle is getting older...
      p->life -= dt * (1.f / LIFE_SPAN);
  
      // Did the particle die?
      if (p->life <= 0.f)
      {
          p->active = 0;
          return;
      }
  
      // Apply gravity
      p->vz = p->vz - GRAVITY * dt;
  
      // Update particle position
      p->x = p->x + p->vx * dt;
      p->y = p->y + p->vy * dt;
      p->z = p->z + p->vz * dt;
  
      // Simple collision detection + response
      if (p->vz < 0.f)
      {
          // Particles should bounce on the fountain (with friction)
          if ((p->x * p->x + p->y * p->y) < FOUNTAIN_R2 &&
              p->z < (FOUNTAIN_HEIGHT + PARTICLE_SIZE / 2))
          {
              p->vz = -FRICTION * p->vz;
              p->z  = FOUNTAIN_HEIGHT + PARTICLE_SIZE / 2 +
                      FRICTION * (FOUNTAIN_HEIGHT +
                      PARTICLE_SIZE / 2 - p->z);
          }
  
          // Particles should bounce on the floor (with friction)
          else if (p->z < PARTICLE_SIZE / 2)
          {
              p->vz = -FRICTION * p->vz;
              p->z  = PARTICLE_SIZE / 2 +
                      FRICTION * (PARTICLE_SIZE / 2 - p->z);
          }
      }
  }
  
  
  //========================================================================
  // The main frame for the particle engine. Called once per frame.
  //========================================================================
  
  static void particle_engine(double t, float dt)
  {
      int i;
      float dt2;
  
      // Update particles (iterated several times per frame if dt is too large)
      while (dt > 0.f)
      {
          // Calculate delta time for this iteration
          dt2 = dt < MIN_DELTA_T ? dt : MIN_DELTA_T;
  
          for (i = 0;  i < MAX_PARTICLES;  i++)
              update_particle(&particles[i], dt2);
  
          min_age += dt2;
  
          // Should we create any new particle(s)?
          while (min_age >= BIRTH_INTERVAL)
          {
              min_age -= BIRTH_INTERVAL;
  
              // Find a dead particle to replace with a new one
              for (i = 0;  i < MAX_PARTICLES;  i++)
              {
                  if (!particles[i].active)
                  {
                      init_particle(&particles[i], t + min_age);
                      update_particle(&particles[i], min_age);
                      break;
                  }
              }
          }
  
          dt -= dt2;
      }
  }
  
  
  //========================================================================
  // Draw all active particles. We use OpenGL 1.1 vertex
  // arrays for this in order to accelerate the drawing.
  //========================================================================
  
  #define BATCH_PARTICLES 70  // Number of particles to draw in each batch
                              // (70 corresponds to 7.5 KB = will not blow
                              // the L1 data cache on most CPUs)
  #define PARTICLE_VERTS  4   // Number of vertices per particle
  
  static void draw_particles(GLFWwindow* window, double t, float dt)
  {
      int i, particle_count;
      Vertex vertex_array[BATCH_PARTICLES * PARTICLE_VERTS];
      Vertex* vptr;
      float alpha;
      GLuint rgba;
      Vec3 quad_lower_left, quad_lower_right;
      GLfloat mat[16];
      PARTICLE* pptr;
  
      // Here comes the real trick with flat single primitive objects (s.c.
      // "billboards"): We must rotate the textured primitive so that it
      // always faces the viewer (is coplanar with the view-plane).
      // We:
      //   1) Create the primitive around origo (0,0,0)
      //   2) Rotate it so that it is coplanar with the view plane
      //   3) Translate it according to the particle position
      // Note that 1) and 2) is the same for all particles (done only once).
  
      // Get modelview matrix. We will only use the upper left 3x3 part of
      // the matrix, which represents the rotation.
      glGetFloatv(GL_MODELVIEW_MATRIX, mat);
  
      // 1) & 2) We do it in one swift step:
      // Although not obvious, the following six lines represent two matrix/
      // vector multiplications. The matrix is the inverse 3x3 rotation
      // matrix (i.e. the transpose of the same matrix), and the two vectors
      // represent the lower left corner of the quad, PARTICLE_SIZE/2 *
      // (-1,-1,0), and the lower right corner, PARTICLE_SIZE/2 * (1,-1,0).
      // The upper left/right corners of the quad is always the negative of
      // the opposite corners (regardless of rotation).
      quad_lower_left.x = (-PARTICLE_SIZE / 2) * (mat[0] + mat[1]);
      quad_lower_left.y = (-PARTICLE_SIZE / 2) * (mat[4] + mat[5]);
      quad_lower_left.z = (-PARTICLE_SIZE / 2) * (mat[8] + mat[9]);
      quad_lower_right.x = (PARTICLE_SIZE / 2) * (mat[0] - mat[1]);
      quad_lower_right.y = (PARTICLE_SIZE / 2) * (mat[4] - mat[5]);
      quad_lower_right.z = (PARTICLE_SIZE / 2) * (mat[8] - mat[9]);
  
      // Don't update z-buffer, since all particles are transparent!
      glDepthMask(GL_FALSE);
  
      glEnable(GL_BLEND);
      glBlendFunc(GL_SRC_ALPHA, GL_ONE);
  
      // Select particle texture
      if (!wireframe)
      {
          glEnable(GL_TEXTURE_2D);
          glBindTexture(GL_TEXTURE_2D, particle_tex_id);
      }
  
      // Set up vertex arrays. We use interleaved arrays, which is easier to
      // handle (in most situations) and it gives a linear memeory access
      // access pattern (which may give better performance in some
      // situations). GL_T2F_C4UB_V3F means: 2 floats for texture coords,
      // 4 ubytes for color and 3 floats for vertex coord (in that order).
      // Most OpenGL cards / drivers are optimized for this format.
      glInterleavedArrays(GL_T2F_C4UB_V3F, 0, vertex_array);
  
      // Wait for particle physics thread to be done
      mtx_lock(&thread_sync.particles_lock);
      while (!glfwWindowShouldClose(window) &&
              thread_sync.p_frame <= thread_sync.d_frame)
      {
          struct timespec ts;
          clock_gettime(CLOCK_REALTIME, &ts);
          ts.tv_nsec += 100 * 1000 * 1000;
          ts.tv_sec += ts.tv_nsec / (1000 * 1000 * 1000);
          ts.tv_nsec %= 1000 * 1000 * 1000;
          cnd_timedwait(&thread_sync.p_done, &thread_sync.particles_lock, &ts);
      }
  
      // Store the frame time and delta time for the physics thread
      thread_sync.t = t;
      thread_sync.dt = dt;
  
      // Update frame counter
      thread_sync.d_frame++;
  
      // Loop through all particles and build vertex arrays.
      particle_count = 0;
      vptr = vertex_array;
      pptr = particles;
  
      for (i = 0;  i < MAX_PARTICLES;  i++)
      {
          if (pptr->active)
          {
              // Calculate particle intensity (we set it to max during 75%
              // of its life, then it fades out)
              alpha =  4.f * pptr->life;
              if (alpha > 1.f)
                  alpha = 1.f;
  
              // Convert color from float to 8-bit (store it in a 32-bit
              // integer using endian independent type casting)
              ((GLubyte*) &rgba)[0] = (GLubyte)(pptr->r * 255.f);
              ((GLubyte*) &rgba)[1] = (GLubyte)(pptr->g * 255.f);
              ((GLubyte*) &rgba)[2] = (GLubyte)(pptr->b * 255.f);
              ((GLubyte*) &rgba)[3] = (GLubyte)(alpha * 255.f);
  
              // 3) Translate the quad to the correct position in modelview
              // space and store its parameters in vertex arrays (we also
              // store texture coord and color information for each vertex).
  
              // Lower left corner
              vptr->s    = 0.f;
              vptr->t    = 0.f;
              vptr->rgba = rgba;
              vptr->x    = pptr->x + quad_lower_left.x;
              vptr->y    = pptr->y + quad_lower_left.y;
              vptr->z    = pptr->z + quad_lower_left.z;
              vptr ++;
  
              // Lower right corner
              vptr->s    = 1.f;
              vptr->t    = 0.f;
              vptr->rgba = rgba;
              vptr->x    = pptr->x + quad_lower_right.x;
              vptr->y    = pptr->y + quad_lower_right.y;
              vptr->z    = pptr->z + quad_lower_right.z;
              vptr ++;
  
              // Upper right corner
              vptr->s    = 1.f;
              vptr->t    = 1.f;
              vptr->rgba = rgba;
              vptr->x    = pptr->x - quad_lower_left.x;
              vptr->y    = pptr->y - quad_lower_left.y;
              vptr->z    = pptr->z - quad_lower_left.z;
              vptr ++;
  
              // Upper left corner
              vptr->s    = 0.f;
              vptr->t    = 1.f;
              vptr->rgba = rgba;
              vptr->x    = pptr->x - quad_lower_right.x;
              vptr->y    = pptr->y - quad_lower_right.y;
              vptr->z    = pptr->z - quad_lower_right.z;
              vptr ++;
  
              // Increase count of drawable particles
              particle_count ++;
          }
  
          // If we have filled up one batch of particles, draw it as a set
          // of quads using glDrawArrays.
          if (particle_count >= BATCH_PARTICLES)
          {
              // The first argument tells which primitive type we use (QUAD)
              // The second argument tells the index of the first vertex (0)
              // The last argument is the vertex count
              glDrawArrays(GL_QUADS, 0, PARTICLE_VERTS * particle_count);
              particle_count = 0;
              vptr = vertex_array;
          }
  
          // Next particle
          pptr++;
      }
  
      // We are done with the particle data
      mtx_unlock(&thread_sync.particles_lock);
      cnd_signal(&thread_sync.d_done);
  
      // Draw final batch of particles (if any)
      glDrawArrays(GL_QUADS, 0, PARTICLE_VERTS * particle_count);
  
      // Disable vertex arrays (Note: glInterleavedArrays implicitly called
      // glEnableClientState for vertex, texture coord and color arrays)
      glDisableClientState(GL_VERTEX_ARRAY);
      glDisableClientState(GL_TEXTURE_COORD_ARRAY);
      glDisableClientState(GL_COLOR_ARRAY);
  
      glDisable(GL_TEXTURE_2D);
      glDisable(GL_BLEND);
  
      glDepthMask(GL_TRUE);
  }
  
  
  //========================================================================
  // Fountain geometry specification
  //========================================================================
  
  #define FOUNTAIN_SIDE_POINTS 14
  #define FOUNTAIN_SWEEP_STEPS 32
  
  static const float fountain_side[FOUNTAIN_SIDE_POINTS * 2] =
  {
      1.2f, 0.f,  1.f, 0.2f,  0.41f, 0.3f, 0.4f, 0.35f,
      0.4f, 1.95f, 0.41f, 2.f, 0.8f, 2.2f,  1.2f, 2.4f,
      1.5f, 2.7f,  1.55f,2.95f, 1.6f, 3.f,  1.f, 3.f,
      0.5f, 3.f,  0.f, 3.f
  };
  
  static const float fountain_normal[FOUNTAIN_SIDE_POINTS * 2] =
  {
      1.0000f, 0.0000f,  0.6428f, 0.7660f,  0.3420f, 0.9397f,  1.0000f, 0.0000f,
      1.0000f, 0.0000f,  0.3420f,-0.9397f,  0.4226f,-0.9063f,  0.5000f,-0.8660f,
      0.7660f,-0.6428f,  0.9063f,-0.4226f,  0.0000f,1.00000f,  0.0000f,1.00000f,
      0.0000f,1.00000f,  0.0000f,1.00000f
  };
  
  
  //========================================================================
  // Draw a fountain
  //========================================================================
  
  static void draw_fountain(void)
  {
      static GLuint fountain_list = 0;
      double angle;
      float  x, y;
      int m, n;
  
      // The first time, we build the fountain display list
      if (!fountain_list)
      {
          fountain_list = glGenLists(1);
          glNewList(fountain_list, GL_COMPILE_AND_EXECUTE);
  
          glMaterialfv(GL_FRONT, GL_DIFFUSE, fountain_diffuse);
          glMaterialfv(GL_FRONT, GL_SPECULAR, fountain_specular);
          glMaterialf(GL_FRONT, GL_SHININESS, fountain_shininess);
  
          // Build fountain using triangle strips
          for (n = 0;  n < FOUNTAIN_SIDE_POINTS - 1;  n++)
          {
              glBegin(GL_TRIANGLE_STRIP);
              for (m = 0;  m <= FOUNTAIN_SWEEP_STEPS;  m++)
              {
                  angle = (double) m * (2.0 * M_PI / (double) FOUNTAIN_SWEEP_STEPS);
                  x = (float) cos(angle);
                  y = (float) sin(angle);
  
                  // Draw triangle strip
                  glNormal3f(x * fountain_normal[n * 2 + 2],
                             y * fountain_normal[n * 2 + 2],
                             fountain_normal[n * 2 + 3]);
                  glVertex3f(x * fountain_side[n * 2 + 2],
                             y * fountain_side[n * 2 + 2],
                             fountain_side[n * 2 +3 ]);
                  glNormal3f(x * fountain_normal[n * 2],
                             y * fountain_normal[n * 2],
                             fountain_normal[n * 2 + 1]);
                  glVertex3f(x * fountain_side[n * 2],
                             y * fountain_side[n * 2],
                             fountain_side[n * 2 + 1]);
              }
  
              glEnd();
          }
  
          glEndList();
      }
      else
          glCallList(fountain_list);
  }
  
  
  //========================================================================
  // Recursive function for building variable tesselated floor
  //========================================================================
  
  static void tessellate_floor(float x1, float y1, float x2, float y2, int depth)
  {
      float delta, x, y;
  
      // Last recursion?
      if (depth >= 5)
          delta = 999999.f;
      else
      {
          x = (float) (fabs(x1) < fabs(x2) ? fabs(x1) : fabs(x2));
          y = (float) (fabs(y1) < fabs(y2) ? fabs(y1) : fabs(y2));
          delta = x*x + y*y;
      }
  
      // Recurse further?
      if (delta < 0.1f)
      {
          x = (x1 + x2) * 0.5f;
          y = (y1 + y2) * 0.5f;
          tessellate_floor(x1, y1,  x,  y, depth + 1);
          tessellate_floor(x, y1, x2,  y, depth + 1);
          tessellate_floor(x1,  y,  x, y2, depth + 1);
          tessellate_floor(x,  y, x2, y2, depth + 1);
      }
      else
      {
          glTexCoord2f(x1 * 30.f, y1 * 30.f);
          glVertex3f(  x1 * 80.f, y1 * 80.f, 0.f);
          glTexCoord2f(x2 * 30.f, y1 * 30.f);
          glVertex3f(  x2 * 80.f, y1 * 80.f, 0.f);
          glTexCoord2f(x2 * 30.f, y2 * 30.f);
          glVertex3f(  x2 * 80.f, y2 * 80.f, 0.f);
          glTexCoord2f(x1 * 30.f, y2 * 30.f);
          glVertex3f(  x1 * 80.f, y2 * 80.f, 0.f);
      }
  }
  
  
  //========================================================================
  // Draw floor. We build the floor recursively and let the tessellation in the
  // center (near x,y=0,0) be high, while the tessellation around the edges be
  // low.
  //========================================================================
  
  static void draw_floor(void)
  {
      static GLuint floor_list = 0;
  
      if (!wireframe)
      {
          glEnable(GL_TEXTURE_2D);
          glBindTexture(GL_TEXTURE_2D, floor_tex_id);
      }
  
      // The first time, we build the floor display list
      if (!floor_list)
      {
          floor_list = glGenLists(1);
          glNewList(floor_list, GL_COMPILE_AND_EXECUTE);
  
          glMaterialfv(GL_FRONT, GL_DIFFUSE, floor_diffuse);
          glMaterialfv(GL_FRONT, GL_SPECULAR, floor_specular);
          glMaterialf(GL_FRONT, GL_SHININESS, floor_shininess);
  
          // Draw floor as a bunch of triangle strips (high tesselation
          // improves lighting)
          glNormal3f(0.f, 0.f, 1.f);
          glBegin(GL_QUADS);
          tessellate_floor(-1.f, -1.f, 0.f, 0.f, 0);
          tessellate_floor( 0.f, -1.f, 1.f, 0.f, 0);
          tessellate_floor( 0.f,  0.f, 1.f, 1.f, 0);
          tessellate_floor(-1.f,  0.f, 0.f, 1.f, 0);
          glEnd();
  
          glEndList();
      }
      else
          glCallList(floor_list);
  
      glDisable(GL_TEXTURE_2D);
  
  }
  
  
  //========================================================================
  // Position and configure light sources
  //========================================================================
  
  static void setup_lights(void)
  {
      float l1pos[4], l1amb[4], l1dif[4], l1spec[4];
      float l2pos[4], l2amb[4], l2dif[4], l2spec[4];
  
      // Set light source 1 parameters
      l1pos[0] =  0.f;  l1pos[1] = -9.f; l1pos[2] =   8.f;  l1pos[3] = 1.f;
      l1amb[0] = 0.2f;  l1amb[1] = 0.2f;  l1amb[2] = 0.2f;  l1amb[3] = 1.f;
      l1dif[0] = 0.8f;  l1dif[1] = 0.4f;  l1dif[2] = 0.2f;  l1dif[3] = 1.f;
      l1spec[0] = 1.f; l1spec[1] = 0.6f; l1spec[2] = 0.2f; l1spec[3] = 0.f;
  
      // Set light source 2 parameters
      l2pos[0] =  -15.f; l2pos[1] =  12.f; l2pos[2] = 1.5f; l2pos[3] =  1.f;
      l2amb[0] =    0.f; l2amb[1] =   0.f; l2amb[2] =  0.f; l2amb[3] =  1.f;
      l2dif[0] =   0.2f; l2dif[1] =  0.4f; l2dif[2] = 0.8f; l2dif[3] =  1.f;
      l2spec[0] =  0.2f; l2spec[1] = 0.6f; l2spec[2] = 1.f; l2spec[3] = 0.f;
  
      glLightfv(GL_LIGHT1, GL_POSITION, l1pos);
      glLightfv(GL_LIGHT1, GL_AMBIENT, l1amb);
      glLightfv(GL_LIGHT1, GL_DIFFUSE, l1dif);
      glLightfv(GL_LIGHT1, GL_SPECULAR, l1spec);
      glLightfv(GL_LIGHT2, GL_POSITION, l2pos);
      glLightfv(GL_LIGHT2, GL_AMBIENT, l2amb);
      glLightfv(GL_LIGHT2, GL_DIFFUSE, l2dif);
      glLightfv(GL_LIGHT2, GL_SPECULAR, l2spec);
      glLightfv(GL_LIGHT3, GL_POSITION, glow_pos);
      glLightfv(GL_LIGHT3, GL_DIFFUSE, glow_color);
      glLightfv(GL_LIGHT3, GL_SPECULAR, glow_color);
  
      glEnable(GL_LIGHT1);
      glEnable(GL_LIGHT2);
      glEnable(GL_LIGHT3);
  }
  
  
  //========================================================================
  // Main rendering function
  //========================================================================
  
  static void draw_scene(GLFWwindow* window, double t)
  {
      double xpos, ypos, zpos, angle_x, angle_y, angle_z;
      static double t_old = 0.0;
      float dt;
      mat4x4 projection;
  
      // Calculate frame-to-frame delta time
      dt = (float) (t - t_old);
      t_old = t;
  
      mat4x4_perspective(projection,
                         65.f * (float) M_PI / 180.f,
                         aspect_ratio,
                         1.0, 60.0);
  
      glClearColor(0.1f, 0.1f, 0.1f, 1.f);
      glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
  
      glMatrixMode(GL_PROJECTION);
      glLoadMatrixf((const GLfloat*) projection);
  
      // Setup camera
      glMatrixMode(GL_MODELVIEW);
      glLoadIdentity();
  
      // Rotate camera
      angle_x = 90.0 - 10.0;
      angle_y = 10.0 * sin(0.3 * t);
      angle_z = 10.0 * t;
      glRotated(-angle_x, 1.0, 0.0, 0.0);
      glRotated(-angle_y, 0.0, 1.0, 0.0);
      glRotated(-angle_z, 0.0, 0.0, 1.0);
  
      // Translate camera
      xpos =  15.0 * sin((M_PI / 180.0) * angle_z) +
               2.0 * sin((M_PI / 180.0) * 3.1 * t);
      ypos = -15.0 * cos((M_PI / 180.0) * angle_z) +
               2.0 * cos((M_PI / 180.0) * 2.9 * t);
      zpos = 4.0 + 2.0 * cos((M_PI / 180.0) * 4.9 * t);
      glTranslated(-xpos, -ypos, -zpos);
  
      glFrontFace(GL_CCW);
      glCullFace(GL_BACK);
      glEnable(GL_CULL_FACE);
  
      setup_lights();
      glEnable(GL_LIGHTING);
  
      glEnable(GL_FOG);
      glFogi(GL_FOG_MODE, GL_EXP);
      glFogf(GL_FOG_DENSITY, 0.05f);
      glFogfv(GL_FOG_COLOR, fog_color);
  
      draw_floor();
  
      glEnable(GL_DEPTH_TEST);
      glDepthFunc(GL_LEQUAL);
      glDepthMask(GL_TRUE);
  
      draw_fountain();
  
      glDisable(GL_LIGHTING);
      glDisable(GL_FOG);
  
      // Particles must be drawn after all solid objects have been drawn
      draw_particles(window, t, dt);
  
      // Z-buffer not needed anymore
      glDisable(GL_DEPTH_TEST);
  }
  
  
  //========================================================================
  // Window resize callback function
  //========================================================================
  
  static void resize_callback(GLFWwindow* window, int width, int height)
  {
      glViewport(0, 0, width, height);
      aspect_ratio = height ? width / (float) height : 1.f;
  }
  
  
  //========================================================================
  // Key callback functions
  //========================================================================
  
  static bool key_callback(GLFWwindow* window, int key, int scancode, int action, int mods)
  {
      if (action == GLFW_PRESS)
      {
          switch (key)
          {
              case GLFW_KEY_ESCAPE:
                  glfwSetWindowShouldClose(window, GLFW_TRUE);
                  return true;
              case GLFW_KEY_W:
                  wireframe = !wireframe;
                  glPolygonMode(GL_FRONT_AND_BACK,
                                wireframe ? GL_LINE : GL_FILL);
                  return true;
              default:
                  break;
          }
      }
      return false;
  }
  
  
  //========================================================================
  // Thread for updating particle physics
  //========================================================================
  
  static int physics_thread_main(void* arg)
  {
      GLFWwindow* window = arg;
  
      for (;;)
      {
          mtx_lock(&thread_sync.particles_lock);
  
          // Wait for particle drawing to be done
          while (!glfwWindowShouldClose(window) &&
                 thread_sync.p_frame > thread_sync.d_frame)
          {
              struct timespec ts;
              clock_gettime(CLOCK_REALTIME, &ts);
              ts.tv_nsec += 100 * 1000 * 1000;
              ts.tv_sec += ts.tv_nsec / (1000 * 1000 * 1000);
              ts.tv_nsec %= 1000 * 1000 * 1000;
              cnd_timedwait(&thread_sync.d_done, &thread_sync.particles_lock, &ts);
          }
  
          if (glfwWindowShouldClose(window))
              break;
  
          // Update particles
          particle_engine(thread_sync.t, thread_sync.dt);
  
          // Update frame counter
          thread_sync.p_frame++;
  
          // Unlock mutex and signal drawing thread
          mtx_unlock(&thread_sync.particles_lock);
          cnd_signal(&thread_sync.p_done);
      }
  
      return 0;
  }
  
  
  //========================================================================
  // main
  //========================================================================
  
  int main(int argc, char** argv)
  {
      int ch, width, height;
      thrd_t physics_thread = 0;
      GLFWwindow* window;
      GLFWmonitor* monitor = NULL;
  
      if (!glfwInit())
      {
          fprintf(stderr, "Failed to initialize GLFW\n");
          exit(EXIT_FAILURE);
      }
  
      while ((ch = getopt(argc, argv, "fh")) != -1)
      {
          switch (ch)
          {
              case 'f':
                  monitor = glfwGetPrimaryMonitor();
                  break;
              case 'h':
                  usage();
                  exit(EXIT_SUCCESS);
          }
      }
  
      if (monitor)
      {
          const GLFWvidmode* mode = glfwGetVideoMode(monitor);
  
          glfwWindowHint(GLFW_RED_BITS, mode->redBits);
          glfwWindowHint(GLFW_GREEN_BITS, mode->greenBits);
          glfwWindowHint(GLFW_BLUE_BITS, mode->blueBits);
          glfwWindowHint(GLFW_REFRESH_RATE, mode->refreshRate);
  
          width  = mode->width;
          height = mode->height;
      }
      else
      {
          width  = 640;
          height = 480;
      }
  
      window = glfwCreateWindow(width, height, "Particle Engine", monitor, NULL);
      if (!window)
      {
          fprintf(stderr, "Failed to create GLFW window\n");
          glfwTerminate();
          exit(EXIT_FAILURE);
      }
  
      if (monitor)
          glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_DISABLED);
  
      glfwMakeContextCurrent(window);
      gladLoadGLLoader((GLADloadproc) glfwGetProcAddress);
      glfwSwapInterval(1);
  
      glfwSetFramebufferSizeCallback(window, resize_callback);
      glfwSetKeyCallback(window, key_callback);
  
      // Set initial aspect ratio
      glfwGetFramebufferSize(window, &width, &height);
      resize_callback(window, width, height);
  
      // Upload particle texture
      glGenTextures(1, &particle_tex_id);
      glBindTexture(GL_TEXTURE_2D, particle_tex_id);
      glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
      glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP);
      glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP);
      glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
      glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
      glTexImage2D(GL_TEXTURE_2D, 0, GL_LUMINANCE, P_TEX_WIDTH, P_TEX_HEIGHT,
                   0, GL_LUMINANCE, GL_UNSIGNED_BYTE, particle_texture);
  
      // Upload floor texture
      glGenTextures(1, &floor_tex_id);
      glBindTexture(GL_TEXTURE_2D, floor_tex_id);
      glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
      glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
      glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
      glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
      glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
      glTexImage2D(GL_TEXTURE_2D, 0, GL_LUMINANCE, F_TEX_WIDTH, F_TEX_HEIGHT,
                   0, GL_LUMINANCE, GL_UNSIGNED_BYTE, floor_texture);
  
      if (glfwExtensionSupported("GL_EXT_separate_specular_color"))
      {
          glLightModeli(GL_LIGHT_MODEL_COLOR_CONTROL_EXT,
                        GL_SEPARATE_SPECULAR_COLOR_EXT);
      }
  
      // Set filled polygon mode as default (not wireframe)
      glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);
      wireframe = 0;
  
      // Set initial times
      thread_sync.t  = 0.0;
      thread_sync.dt = 0.001f;
      thread_sync.p_frame = 0;
      thread_sync.d_frame = 0;
  
      mtx_init(&thread_sync.particles_lock, mtx_timed);
      cnd_init(&thread_sync.p_done);
      cnd_init(&thread_sync.d_done);
  
      if (thrd_create(&physics_thread, physics_thread_main, window) != thrd_success)
      {
          glfwTerminate();
          exit(EXIT_FAILURE);
      }
  
      glfwSetTime(0.0);
  
      while (!glfwWindowShouldClose(window))
      {
          draw_scene(window, glfwGetTime());
  
          glfwSwapBuffers(window);
          glfwPollEvents();
      }
  
      thrd_join(physics_thread, NULL);
  
      glfwDestroyWindow(window);
      glfwTerminate();
  
      exit(EXIT_SUCCESS);
  }
  
  