CS409 / CS 809 Tutorial 3: Particles

Setting up an OpenGL Project

1.      Start Microsoft Visual Studio 2013.  Select File / New / Project.  Under “Project Type”, choose Templates / Visual C++. Then choose a Win32 Console Application.  Change the name to Tutorial3 or some suitable name and change the location to a hard drive located on the computer (typically C or D).  You will need to remember this location for steps 3 and 5.  Make sure the “Create folder for project” checkbox is not checked and that the “Add to source control” is not checked.  Click OK.

·         If the Chose Location window keeps popping up again, you aren’t allowed to save files at the location you chose.  Select a different location, such as the desktop.

·         Do not put your program on one of the external drives like H.  The connection is slow, and your program may not compile correctly.

2.      Click Next> or Application Settings (they go to the same place).  Check the “Empty Project” box.  Make sure the “Security development lifecycle check” is not checked.  Click Finish.

3.      Go to http://www2.cs.uregina.ca/~anima/409/Terms/201810/Tutorials/index.html and download the Tutorial3.zip and ObjLibrary.zip files to your project directory.

4.      Unzip the 2 zip files.  You will have a folder called ObjLibrary and 10 other files.

5.      In Visual C++, go to Project/Add Existing Item… and select main3.cpp (and holding control, continue to select), Sleep.h, Sleep.cpp, and GetGlut.h.

6.      Ensure that Solution Explorer is visible (View / Solution Explorer). In the Solution Explorer, select Tutorial3 (the project name) and right click it.  Choose Add / New Filter.  A new folder icon should appear in the Solution Explorer.  Name it ObjLibrary.  (This creates an imaginary folder in the project, but it would not create a Windows folder on the computer).

7.      In the Solution Explorer, right click on the ObjLibrary folder icon and select Add / Existing item.  Go into the ObjLibrary folder on Windows and select all files.  The files should appear in the Solution Explorer inside the ObjLibrary folder.  You should get 26 files, although you will not be using most of them for this tutorial.

8.      Compile and run the original program.  You should see 2 windows: a text console and a graphical window labeled “Particles”.  There is a white square in the middle of the screen.  Exit the windows.  Closing the text window will close the graphical window, and sometimes vice versa.

9.      For the remainder of the instructions, it is assumed that you compile and run regularly so that you can see what each change does.

Moving the Square

10.  In main3.cpp, add an #include for "ObjLibrary/Vector2.h"
#include "ObjLibrary/Vector2.h"
A Vector2 is a class containing 2 doubles and a lot of functions for manipulating them.

11.  Add a using directive for ObjLibrary after the using directive for std:
using namespace ObjLibrary;

12.  Declare a global variable (before the main function) of the Vector2 type:
Vector2 square_pos;
The default constructor will initialize the position to be at the origin.

13.  Change the display function in main3.cpp to draw the square at a position determined by square_pos:
  glTranslated(square_pos.x, square_pos.y, 0.0f);
Don’t forget to use Push/Pop matrix commands.  Try running the program with a few different positions to make sure it works correctly.  You can do this using the constructor:
Vector2 square_pos(100.0, -50.0);
or afterwards with:
  square_pos.x = 100.0;
  square_pos.y = -50.0;
or:
  square_pos.set(100.0, -50.0);

14.  Declare another global of the Vector2 type:
Vector2 square_vel(5.0, 1.0);

15.  Add a line to the update function to move the square according to its current velocity.
  square_pos += square_vel;
Compile and run the program.  The square should move off the right of screen and never come back.

16.  Declare a global variable of the double type:
  double square_age = 0.0;

17.  Add a line to the update function to increment this value.  If the age of the square is more than 60, move it to the origin and set its age to zero:
  square_age++;
  if (square_age > 60)
  {
       // restart square movement
       square_age = 0.0;
       square_pos.set(0.0, 0.0);
  }

Change the initial position of the square to (0, 0) instead of (100.0, -50.0).

18.  Compile and run the program.  The square should repeatedly towards the right of screen and then reappear at the origin.

Converting Square to a Class

19.  Move the part of your update function that changes the value of square_age and square_pos into a function called square_update.  Your update function should call square_update.

20.  Move the part of your display function that draws the square into a function called square_draw.  Include the push and pop matrix commands and everything between them.  Your display function should call square_draw.

21.  Move the square_pos, square_vel, and square_age variables and the square_draw and square_update functions to a separate file named Square.cpp.  You will have to #include "ObjLibrary/Vector2.h" at the top of Square.h and add the line:
using namespace ObjLibrary;

22.  Move the prototypes for the square_draw and square_update functions to a file named Square.h that #includes "ObjLibrary/Vector2.h".  You will have to either replace each instance of Vector2 with ObjLibrary::Vector2 or add the line
using namespace ObjLibrary;
near the top of this file too.

23.  Add #include "Square.h" at the top of main.cpp and attempt to recompile and run the program.  If all goes well, you have successfully isolated the code for the square.

24.  Add a class named Square to the Square.h file:
class Square
{
public:
  // declare your constructor here

  // declare your public member functions here

private:
  // declare your private member functions here

  // declare your member variables here
};

25.  Move the square_update and square_draw functions inside the Square class and rename them update and draw.  Remember to also update the implementations in the Square.cpp file.

26.  Change the square_pos, square_vel, and square_age global variables into member variables of the square class named pos, vel, and age.

27.  Add an init function to the Square class that sets the position to the origin, the velocity to (5, 1), and the age to 0  It should have a prototype in Square.h:
  void init ();
and an implementation in Square.cpp
void Square :: init ()
{
  pos.set(0.0, 0.0);
  vel.set(5.0, 1.0);
  age = 0;
}

28.  Add an explicit default constructor to the Square class that calls the init function.  It will need a prototype and an implementation.

29.  Add a global variable of type Square to main.cpp:
Square square;
It should be your only global variable.  Update the Square in update and display it in display.  The white square should behave the same as before it was a class.

Squares as Particles

30.  Add a new global constant to main.cpp named SQUARE_COUNT:
const unsigned int SQUARE_COUNT = 100;

31.  Replace the square variable in main with an array of Squares:
Square squares[SQUARE_COUNT];
Change the places where the squares are updated and drawn to use a
for loop to update and draw every square in the array.

32.  Compile and run the program.  All the squares should be in the same place doing the same thing, so it should look like there is still only one square.

33.  Add a function named isAlive to the Square class that returns a bool.  It will need a prototype and an implementation:
bool Square :: isAlive ()
{
  if (age <= 60)
       return true;
  else
       return false;
}

34.  Change the draw function to only display the square if it is alive:
void Square :: draw ()
{
  if (isAlive())
  {
       // draw square here
  }
}

35.  Remove the if check and its contents from the update function in Square.  The square will not move itself back to its starting position.

36.  Compile and run the program.  It should look like one square moves off towards the right and then vanishes.  The square will never come back.

37.  Add a global variable to the main3.cpp file named next_square:
unsigned int next_square = 0;

38.  Add commands to the update function in main.cpp to increment the next_square variable each time the function is called.  If the next_square variable is ever larger than or equal to SQUARE_COUNT, it should be set back to 0:
  next_square++;
  if (next_square >= SQUARE_COUNT)
       next_square = 0;
or
  next_square = (next_square + 1) % SQUARE_COUNT;
After this, call the init function for the Square indicated by next_square:
  squares[next_square].init();

39.  Compile and run the program.  There should be a steady stream of white squares moving to the right and vanishing.  It will look like a single elongated white shape.

40.  Add a global variable to the main.cpp file named emitter_on:
bool emitter_on = true;
Add a check to the update function to only call init for the square if emitter_on is true:
  if (emitter_on)
       squares[next_square].init();
Add a case to the keyboard function in main.cpp for the space bar (' ').  It should toggle the value of the emitter_on variable:
  case ' ': // on [SPACEBAR]
       emitter_on = !emitter_on;
       break;

41.  Compile and run the program.  You should be able to turn the stream of particles on and off using the space bar.

Fancier Particles

We want to produce a variety of particles, each with a randomly chosen direction of travel and a random chosen colour.

 

42.  In the init function for the Square class, set the particle to move in a random direction at speed 5:
  vel = Vector2::getRandomUnitVector() * 5.0;
The particles should now move in random directions instead of all the same way.  However, there will be a big burst of particles at the start.  This is caused by all the particles that are not initialized yet.
Note: a unit vector is a vector of length 1 (technically described as a norm of 1); such vectors are commonly used to represent directions.

43.  Add a line to the Square constructor to set the age to a large number.
  age = 999999;
This has to be after the call to
init, or the age will just be reset to 0.  You should no longer see a big burst of particles at the beginning of the program.

44.  Add a private member function to the Square class named random0to1 that returns a random float between 0.0 (inclusive) and 1.0 (exclusive).  It will need a prototype and an implementation:
float Square :: random0to1 ()
{
  return (float)(rand()) / ((float)RAND_MAX + 1.0f);
}
You will have to #include <cstdlib> in Square.cpp.

45.  Add three new member variables to the Square class named red, green, and blue:
  float red;
  float green;
  float blue;

46.  In the init function, initialize these value to form a random colour:
  red   = random0to1();
  green = random0to1();
  blue  = random0to1();

47.  In the draw function, display the square using these three variables as the display color instead of white:
            glColor3f(red, green, blue);

48.  Compile and run the program.  The squares should now be a mix of all colours and the middle will appear to flicker.  You may notice that the new squares normally appear in front of the old ones, but, once every 100 frames, they switch to appearing at the back.

49.  Make the squares partially transparent.  Add with:
            glEnable(GL_BLEND);
            glBlendFunc(GL_SRC_ALPHA,
GL_ONE_MINUS_SRC_ALPHA);
immediately before the
glColor3f line to enable transparency.  Also add:
            glDisable(GL_BLEND);

immediately after
glEnd() to disable transparency. Compile and run your program.  The squares should appear the same as before.  This is because we have not set how transparent they should be and the default is 100% opaque.

50.  Change the glColor3f line to:
            glColor4f(
red, green, blue, 0.4f);
Compile and run your program.  The squares should be only 40% opaque, allowing you to see the other squares behind them.  This type of transparency is named alpha transparency because it uses a fourth color component – commonly called alpha – to determine how solid the polygon is.  This is the type of transparency people normally think of.  It has the disadvantage that it makes it easy to see what order things were drawn in and figuring out the right order to draw them can be difficult.

51.  Add a line immediately after glEnable to change the transparency type:
            glBlendFunc(GL_SRC_ALPHA, GL_ONE);
The colours of the squares should now be combined differently, becoming brighter where there are more squares.  This type of transparency is named additive transparency because the corresponding colour components of several squares are added together; i.e., the red values of all overlapping squares are added together to determine the total redness at the point of overlap.  The colour of the square is still being multiplied by
0.4 before this happens, so it they are not as bright as they could be.  Additive transparency is used for glowing things like fire and only works well on dark backgrounds.  It does have the advantage that the transparent things can be drawn in any order and the end result is the same.

Exercises:

52.  Add a size field to the Square class representing the number of pixels from the middle of the square to its edge.  Assign it a random value in the range 20 to 60 in the init function.

53.  Change the transparency for the squares to begin at 1 (maximum brightness) and smoothly decrease to 0 at the time their age equals 60.  The squares should appear to fade out instead of disappearing suddenly.

54.  Make the squares spin and have a random starting rotation.  You will have to add a rotation field to the Square class.  Note: OpenGL uses angles in degrees, not radians.

55.  Add keyboard commands so that the [W], [A], [S], and [D] keys move the particle emitter (the place where the particles appear) around the screen.  This should only affect particles when they are being initialized/re-initialized (the call to init from update in main3.cpp).  Note: You will need to change your init function to allow the emitter position (and thus the initial position of the particle) to be passed in as a parameter.  Pass in (0, 0) from the default constructor.

56.  Slow down the speed of the particles to 3.0 instead of 5.0.  You should be able to leave a rough trail of particles behind your moving emitter. 

57.  Change all the squares to display with red 1.0, green 0.3, and blue 0.1.  Change the background colour to dark blue (0.0, 0.0, 0.5).  What effect does this have?

58.  Change the shape of the particle to an octagon (8 sides). Hint: add more vertices to the GL_POLYGON in Square.cpp. The vertices must appear in the order that the shape can be drawn while proceeding in a counterclockwise fashion.