// Written in the D Programming Language /** * The 19th lesson in the NeHe tutorial series. * Originally written by Jeff Molofee. * * Authors: Jeff Molofee * Olli Aalto */ module lesson19; import derelict.opengl.gl; import derelict.opengl.glu; import derelict.sdl.sdl; import tango.stdc.stringz; import tango.math.random.Kiss; import tango.io.Stdout; import tango.text.convert.Sprint; /// The window title const char[] WINDOW_TITLE = "NeHe's Particle Tutorial (D version)"; /// Max number of particles const MAX_PARTICLES = 1000; Kiss kiss; /// The main loop flag bool running; /// Toggle rainbow effect bool rainbow = true; /// Slow Down Particles float slowdown = 2.0f; /// Base X Speed (To Allow Keyboard Direction Of Tail) float xspeed = 0.0f; /// Base Y Speed (To Allow Keyboard Direction Of Tail) float yspeed = 0.0f; /// Used To Zoom Out float zoom = -40.0f; /// Current Color Selection GLuint color; /// Storage For Our Particle Texture GLuint texture; /// Create our particle structure struct Particle { // Active (Yes/No) bool active; // Particle Life float life = 0.0f; // Fade Speed float fade = 0.0f; // Red Value float r = 0.0f; // Green Value float g = 0.0f; // Blue Value float b = 0.0f; // X Position float x = 0.0f; // Y Position float y = 0.0f; // Z Position float z = 0.0f; // X Direction float xi = 0.0f; // Y Direction float yi = 0.0f; // Z Direction float zi = 0.0f; // X Gravity float xg = 0.0f; // Y Gravity float yg = 0.0f; // Z Gravity float zg = 0.0f; } // Rainbow of colors static GLfloat[][] colors = [[1.0f, 0.5f, 0.5f], [1.0f, 0.75f, 0.5f], [1.0f, 1.0f, 0.5f], [0.75f, 1.0f, 0.5f], [0.5f, 1.0f, 0.5f], [0.5f, 1.0f, 0.75f], [0.5f, 1.0f, 1.0f], [0.5f, 0.75f, 1.0f], [0.5f, 0.5f, 1.0f], [0.75f, 0.5f, 1.0f], [1.0f, 0.5f, 1.0f], [1.0f, 0.5f, 0.75f]]; /// Our beloved array of particles Particle[MAX_PARTICLES] particles; /** * Module constructor. Here we load the GL, GLU and SDL shared libraries, * and the initialize SDL. */ static this() { DerelictGL.load(); DerelictGLU.load(); DerelictSDL.load(); if (SDL_Init(SDL_INIT_VIDEO) < 0) { throw new Exception("Failed to initialize SDL: " ~ getSDLError()); } if (SDL_EnableKeyRepeat(100, SDL_DEFAULT_REPEAT_INTERVAL)) { throw new Exception("Failed to set key repeat: " ~ getSDLError()); } } /** * Module destructor. SDL_Quit must be called somewhere, and as we initialized * it in the module constructor so the module destructor should be a suitable * place. */ static ~this() { SDL_Quit(); } /** * The main function. This is where the fun begins. The first order of business * is the check the command line arguments if the user wanted to start in * fullscreen mode. Then the window is created and OpenGL is initialized with * basic settings. Finally the the function starts the main loop which will live * for the duration of the application. * * Params: * args = the command line arguments */ void main(char[][] args) { bool fullScreen = false; if (args.length > 1) { fullScreen = args[1] == "-fullscreen"; } kiss = Kiss(); createGLWindow(WINDOW_TITLE, 640, 480, 16, fullScreen); initGL(); running = true; while (running) { processEvents(); drawGLScene(); SDL_GL_SwapBuffers(); SDL_Delay(10); // If rainbow coloring is turned on, cycle the colors if (rainbow) { color = (++color) % 12; } } } /** * Process all the pending events. */ void processEvents() { SDL_Event event; while (SDL_PollEvent(&event)) { switch (event.type) { case SDL_KEYUP: keyReleased(event.key.keysym.sym); break; case SDL_KEYDOWN: keyPressed(event.key.keysym.sym); break; case SDL_QUIT: running = false; break; default: break; } } } /** * Process key pressed events. * * Params: * key = the key that have been pressed */ void keyPressed(int key) { switch (key) { case SDLK_KP_PLUS: // '+' key was pressed. This speeds up the particles. if (slowdown > 1.0f) { slowdown -= 0.01f; } break; case SDLK_KP_MINUS: // '-' key was pressed. This slows down the particles. if (slowdown < 4.0f) { slowdown += 0.01f; } case SDLK_PAGEUP: // PageUp key was pressed. This zooms into the scene. zoom += 0.01f; break; case SDLK_PAGEDOWN: // PageDown key was pressed. This zooms out of the scene. zoom -= 0.01f; break; case SDLK_UP: // Up arrow key was pressed. This increases the particles' y movement. if (yspeed < 200.0f) { yspeed++; } break; case SDLK_DOWN: // Down arrow key was pressed. This decreases the particles' y movement. if (yspeed > -200.0f) { yspeed--; } break; case SDLK_RIGHT: // Right arrow key was pressed. This increases the particles' x movement. if (xspeed < 200.0f) { xspeed++; } break; case SDLK_LEFT: // Left arrow key was pressed. This decreases the particles' x movement. if (xspeed > -200.0f) { xspeed--; } break; case SDLK_KP8: // NumPad 8 key was pressed. Increase particles' y gravity. foreach (inout particle; particles) { if (particle.yg < 1.5f) { particle.yg += 0.01f; } } break; case SDLK_KP2: // NumPad 2 key was pressed. Decrease particles' y gravity. foreach (inout particle; particles) { if (particle.yg > -1.5f) { particle.yg -= 0.01f; } } break; case SDLK_KP6: // NumPad 6 key was pressed. This increases the particles' x gravity. foreach (inout particle; particles) { if (particle.xg < 1.5f) { particle.xg += 0.01f; } } break; case SDLK_KP4: // NumPad 4 key was pressed. This decreases the particles' y gravity. foreach (inout particle; particles) { if (particle.xg > -1.5f) { particle.xg -= 0.01f; } } break; default: break; } } /** * Process a key released event. * * Params: * key = the key that have been released */ void keyReleased(int key) { switch (key) { case SDLK_ESCAPE: running = false; break; case SDLK_TAB: // Tab key was pressed. This resets the particles and makes them re-explode. resetParticles(); break; case SDLK_RETURN: // Return key was pressed. This toggles the rainbow color effect. rainbow = !rainbow; break; case SDLK_SPACE: // Spacebar was pressed. This turns off rainbow-ing and manually cycles through colors. rainbow = false; color = (++color) % 12; break; default: break; } } /** * Function to load in bitmap as a GL texture. */ void loadGLTextures() { SDL_Surface* textureImage; // Load The Bitmap, Check For Errors, If Bitmap's Not Found Quit if ((textureImage = SDL_LoadBMP("data/particle.bmp")) !is null) { // Free the surface when we exit the scope scope(exit) SDL_FreeSurface(textureImage); // Create The Textures glGenTextures(1, &texture); // Create Linear Filtered Texture glBindTexture(GL_TEXTURE_2D, texture); 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, 3, textureImage.w, textureImage.h, 0, GL_BGR, GL_UNSIGNED_BYTE, textureImage.pixels); return; } throw new Exception("Failed to load texture."); } /** * Reset all the particles. */ void resetParticles() { for (int loop = 0; loop < MAX_PARTICLES; loop++) { int col = (loop + 1) / (MAX_PARTICLES / 11); float xi = ((kiss.toInt() % 50) - 26.0f) * 10.0f; float yi = ((kiss.toInt() % 50) - 25.0f) * 10.0f; resetParticle(loop, col, xi, yi, yi); } } /** * Function to reset one particle to initial state. * * NOTE: I added this function to replace doing the same thing in several * places and to also make it easy to move the pressing of numpad keys * 2, 4, 6, and 8 into handleKeyPress function. */ void resetParticle(int num, int col, float xDir, float yDir, float zDir) { // Make the particels active particles[num].active = true; // Give the particles life particles[num].life = 1.0f; // Random Fade Speed particles[num].fade = (kiss.toInt() % 100) / 1000.0f + 0.003f; // Select Red Rainbow Color particles[num].r = colors[col][0]; // Select Green Rainbow Color particles[num].g = colors[col][1]; // Select Blue Rainbow Color particles[num].b = colors[col][2]; // Set the position on the X axis particles[num].x = 0.0f; // Set the position on the Y axis particles[num].y = 0.0f; // Set the position on the Z axis particles[num].z = 0.0f; // Random Speed On X Axis particles[num].xi = xDir; // Random Speed On Y Axi particles[num].yi = yDir; // Random Speed On Z Axis particles[num].zi = zDir; // Set Horizontal Pull To Zero particles[num].xg = 0.0f; // Set Vertical Pull Downward particles[num].yg = -0.8f; // Set Pull On Z Axis To Zero particles[num].zg = 0.0f; } /** * Resize and initialize the OpenGL window. */ void resizeGLScene(GLsizei width, GLsizei height) { if (height == 0) { height = 1; } // Reset The Current Viewport glViewport(0, 0, width, height); // Select The Projection Matrix glMatrixMode(GL_PROJECTION); // Reset The Projection Matrix glLoadIdentity(); // Calculate The Aspect Ratio Of The Window gluPerspective(45.0f, cast(GLfloat) width / cast(GLfloat) height, 0.1f, 100.0f); // Select The Modelview Matrix glMatrixMode(GL_MODELVIEW); // Reset The Modelview Matrix glLoadIdentity(); } /** * Initialize OpenGL. */ void initGL() { loadGLTextures(); // Enables Smooth Shading glShadeModel(GL_SMOOTH); // Black Background glClearColor(0.0f, 0.0f, 0.0f, 0.0f); // Depth Buffer Setup glClearDepth(1.0f); // Disables Depth Testing glDisable(GL_DEPTH_TEST); // Really Nice Perspective Calculations glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST); // Really Nice Point Smoothing glHint(GL_POINT_SMOOTH_HINT, GL_NICEST); // Enable Blending glEnable(GL_BLEND); // Type Of Blending To Perform glBlendFunc(GL_SRC_ALPHA, GL_ONE); /* Enable Texture Mapping */ glEnable(GL_TEXTURE_2D); /* Select Our Texture */ glBindTexture(GL_TEXTURE_2D, texture); // Change to texture matrix and flip and rotate the texture glMatrixMode(GL_TEXTURE); glRotatef(180.0f, 0.0f, 0.0f, 1.0f); glScalef(-1.0f, 1.0f, 1.0f); // Back to normal glMatrixMode(GL_MODELVIEW); // Reset all the particles resetParticles(); } /** * The drawing function. Now we only clear the color and depht buffers, so that * the window stays black. */ void drawGLScene() { // Clear The Screen And The Depth Buffer glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); glLoadIdentity(); // Modify each of the particles foreach (loop, inout particle; particles) { if (particle.active) { // Grab Our Particle X Position float x = particle.x; // Grab Our Particle Y Position float y = particle.y; // Particle Z Position + Zoom float z = particle.z + zoom; /* Draw The Particle Using Our RGB Values, * Fade The Particle Based On It's Life */ glColor4f(particle.r, particle.g, particle.b, particle.life); // Build Quad From A Triangle Strip glBegin(GL_TRIANGLE_STRIP); // Top Right glTexCoord2d(1, 1); glVertex3f(x + 0.5f, y + 0.5f, z); // Top Left glTexCoord2d(0, 1); glVertex3f(x - 0.5f, y + 0.5f, z); // Bottom Right glTexCoord2d(1, 0); glVertex3f(x + 0.5f, y - 0.5f, z); // Bottom Left glTexCoord2d(0, 0); glVertex3f(x - 0.5f, y - 0.5f, z); glEnd(); // Move On The X Axis By X Speed particle.x += particle.xi / (slowdown * 1000); // Move On The Y Axis By Y Speed particle.y += particle.yi / (slowdown * 1000); // Move On The Z Axis By Z Speed particle.z += particle.zi / (slowdown * 1000); // Take Pull On X Axis Into Account particle.xi += particle.xg; // Take Pull On Y Axis Into Account particle.yi += particle.yg; // Take Pull On Z Axis Into Account particle.zi += particle.zg; // Reduce Particles Life By 'Fade' particle.life -= particle.fade; // If the particle dies, revive it if (particle.life < 0.0f) { float xi = xspeed + (kiss.toInt() % 60) - 32.0f; float yi = yspeed + (kiss.toInt() % 60) - 30.0f; float zi = (kiss.toInt() % 60) - 30.0f; resetParticle(loop, color, xi, yi, zi); } } } calculateFps(); } /** * Calculates the FPS and outputs it to the cosole. */ void calculateFps() { // These are to calculate our fps static GLint T0; static GLint frames; /* Gather our frames per second */ frames++; GLint time = SDL_GetTicks(); if ((time - T0) >= 5000) { GLfloat seconds = (time - T0) / 1000.0; GLfloat fps = frames / seconds; // Create a Sprint instance auto sprint = new Sprint!(char); // Write formatted text to the console Stdout(sprint("{0} frames in {1} seconds = {2} FPS", frames, seconds, fps)).newline; T0 = time; frames = 0; } } /** * Initializes and opens the SDL window. */ void createGLWindow(char[] title, int width, int height, int bits, bool fullScreen) { // Set the OpenGL attributes SDL_GL_SetAttribute(SDL_GL_RED_SIZE, 5); SDL_GL_SetAttribute(SDL_GL_GREEN_SIZE, 6); SDL_GL_SetAttribute(SDL_GL_BLUE_SIZE, 5); SDL_GL_SetAttribute(SDL_GL_DEPTH_SIZE, 16); SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1); // Set the window title SDL_WM_SetCaption(toStringz(title), null); // Note the SDL_DOUBLEBUF flag is not required to enable double // buffering when setting an OpenGL video mode. // Double buffering is enabled or disabled using the // SDL_GL_DOUBLEBUFFER attribute. (See above.) int mode = SDL_OPENGL; if (fullScreen) { mode |= SDL_FULLSCREEN; } // Now open a SDL OpenGL window with the given parameters if (SDL_SetVideoMode(width, height, bits, mode) is null) { throw new Exception("Failed to open OpenGL window: " ~ getSDLError()); } resizeGLScene(width, height); } /** * Get the SDL error as a D string. * * Returns: A D string containing the current SDL error. */ char[] getSDLError() { return fromStringz(SDL_GetError()); }