You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and dots ('.'), can be up to 35 characters long. Letters must be lowercase.
1072 lines
35 KiB
1072 lines
35 KiB
//======================================================================== |
|
// 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 void 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); |
|
break; |
|
case GLFW_KEY_W: |
|
wireframe = !wireframe; |
|
glPolygonMode(GL_FRONT_AND_BACK, |
|
wireframe ? GL_LINE : GL_FILL); |
|
break; |
|
default: |
|
break; |
|
} |
|
} |
|
} |
|
|
|
|
|
//======================================================================== |
|
// 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); |
|
} |
|
|
|
|