part
Introduction

In this example we will spawn and simulate particles on the cpu which we, on each frame, will upload to the gpu. You will start by rendering simple fixed points from a fixed position. After that you will add velocity to those points, set the source at the exhaust of the ship, and finally use point sprites to render the particles as billboards with a texture.

The system

In the files ParticleSystem.cpp and ParticleSystem.h we have provided a skeleton where it's up to you to fill in the blanks.

void ParticleSystem::kill(int id)
void ParticleSystem::spawn(Particle particle)
void ParticleSystem::process_particles(float dt)

Before we start with the implementation of these functions, let's first take a look at the data structure for a particle.

//The full particle state
struct Particle {
	float lifetime;
	float life_length;
	glm::vec3 velocity;
	glm::vec3 pos;
};

Here lifetime is for how long the particle has been alive. lifelength is the lifetime of the particle. When lifetime > lifelength we consider the particle to be dead, and remove it (kill). The particle also has a position (the center of the particle) and a velocity. We keep a std::vector of the full states, and a std::vector with the reduced states (which will be uploaded to the gpu) on the CPU.

General procedure

The Particle struct contains all data we need for the simulation and you will also introduce a more compact struct which contains all we need for the rendering. For each frame we will do the following:

  1. Eventually spawn particle(s) by calling spawn()
  2. Process particles by calling process_particles()
    1. For each particle, remove if dead by calling kill()
    2. For each particle, update the state (see below)
  3. Update the vector of reduced states, from the full states
  4. Upload the data to the gpu
  5. Render the particles as point sprites

Gpu data

In order to render the particles you will need to create a vertex array object, to get the inputs to your vertex shader (see Tutorial 1). The input we need are the position and lifetime of the particle. Later we will render transparent particles, which we then need to sort depending on their view space depth, in order to get proper blending. Because of this we will do the view space transformation on the cpu and upload the already transformed positions. We will modify the lifetime at the same time, so that 0 corresponds to just spawned, and 1 corresponds to just about to die.

You don't need to upload anything here, just allocate enough memory to hold your data, i.e. call glBufferData with nullptr as the data pointer. Later you may want to make it so the buffer can hold as many particles as your particle system, but for now go ahead and allocate space for 100000 elements.

Hint: It is probably easiest to store the data as a vec4, where xyz is the view space position and w is the lifetime. If you want to experiment you could try to use stride and offset to access the buffer as a vec3 and a float in the shader, instead of a vec4.

Extract the data we wish to upload

In order to extract the particle data we want to upload we will first copy the modified particle data from the system, here named particle_system. Assuming we have the GL (cpu) data in a variable std::vector<glm::vec4> data; the last step (the sorting) is done like so:

unsigned int active_particles = particle_system.particles.size();
/* Code for extracting data goes here */
// sort particles with sort from c++ standard library
std::sort(data.begin(), std::next(data.begin(), active_particles), 
	[](const vec4 &lhs, const vec4 &rhs){ return lhs.z < rhs.z; });

    If you are not familiar with std::vector, here is some basic functionality:
  1. vector.push_back() - append an element to the end of the vector
  2. vector.pop_back() - remove an element at the end of the vector
  3. vector.size() - returns the number of elements in the vector
  4. vector.back() - returns the last element of the vector
  5. vector.resize(int n) - Allocate space for n elements. OBS: size will be n and back will refer to the element at index n
  6. Elements can be accessed as with arrays foo[i]
Note: The vector will grow dynamically if you use push_back.

Uploading data to the gpu

Uploading the can be done by binding your buffer and then using glBufferSubData. This lets you upload a subset of the data, e.g. if your buffer holds 64 bytes, you can choose to upload only 8 if you wish.

    Note:
  1. The size of data we want to upload is the size of your GL data (in this case vec4) times the number of alive particles (active_particles).
  2. The pointer to the data is acessed by data.data() (or equivalently &data[0]).

Milestone

To see that we have got it working so far we can fill the GL (cpu) data with some values, upload it and render. To generate the positions we sample the surface of a sphere (http://mathworld.wolfram.com/SpherePointPicking.html), using the utility function labhelper::uniform_randf(float from, float to) which gernerates random floating point numbers in the specified interval.

const float theta = labhelper::uniform_randf(0.f, 2.f * M_PI);
const float u     = labhelper::uniform_randf(-1.f, 1.f);
glm::vec3 pos     = glm::vec3(sqrt(1.f - u * u) * cosf(theta), u, sqrt(1.f - u * u) * sinf(theta));

Now, just scale the position by 10 to make the sphere larger, do the extraction and upload.
Bind a basic shader program where the vertex shader just do the projective transformation, and the fragment shader outputs the color green. Finally bind your vao and draw using glDrawArrays(GL_POINTS, 0, active_particles); where active_particles is the number of particles you just generated. You should have something like this: part

Kill and spawn particles

Spawning a particle is fairly easy. Check if the number of elements is less than max_size and if so, push the element to the vector.

Killing a particle is quite straight forward as well. Simply copy or swap the last element to/with the element you want to be killed, and then pop the last element. This way we are sure to only have alive particles in our vector. So go ahead and implement the kill and spawn functions.

Do the simulation!

So far we have only implemented the memory management of the system, so let's move on to actually do some simulation.

void ParticleSystem::process_particles(float dt) {
	for (unsigned i = 0; i < particles.size(); ++i) {
		// Kill dead particles!
	}
	for (unsigned i = 0; i < particles.size(); ++i) {
		// Update alive particles!
	}
}

The reason we have two loops here is that it's easier to kill dead particles first, and then update the alive ones. An added benefit is that the updating part is also much easier to be done in parallel.

Updating a particle is done by calculating the new position from the velocity and delta time (dt) as well as the lifetime (also using dt). If you desire you could also update the velocity to simulate e.g. gravity, or air resistance.

Use the system

Now, try to use your particle system instead of generating the GL particles directly. Start by creating a large particle system in global scope.

ParticleSystem particle_system(100000);

Every frame, create 64 particles where the inital position i zero, the velocity as your previous sphere sample, lifetime as zero and life_length to five. Do a simulation step, extract and upload.
part

To create a fountain, we can modify the sphere sample a bit by changing u:

//const float u = labhelper::uniform_randf(-1.f, 1.f);
const float u = labhelper::uniform_randf(0.95f, 1.f);

part

To change the axis of the fountain:

//vec3(sqrt(1.f - u * u) * cosf(theta), u, sqrt(1.f - u * u) * sinf(theta))
vec3(u, sqrt(1.f - u * u) * cosf(theta), sqrt(1.f - u * u) * sinf(theta))

part

If we multiply the positions with the fighters modelmatrix they will be "attached" to the ship. Further try to offset the position (before the multiplication) so that they align to the exhaust.

Hint: ImGui::DragFloat3 may be of use here.

part

Finally we can apply the rotation of the ship to the direction so that the particles always go out from the exhaust. Add keyboard controls for the ship, similarily to how you did in lab3 when you controlled the car (although, make use of glm's rotate() and translate()). Move the particle spawning code to the event handling as well, and use the rotation matrix applied to the direction to have a particle trail for the ship!

Note: The ship in this scene has the world x-axis as its forward vector, so you need to do some small modifications from the code in lab3.

Rendering pretty particles

We can use something called point sprites to render billboards with a texture. Now is a good time to use the provided shaders particle.vert and particle.frag. Also be sure to enable required functionality before the draw call:

// Enable shader program point size modulation.
glEnable(GL_PROGRAM_POINT_SIZE);
// Enable blending.
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);

    This will allow us to:
  1. Modify the point size from within the shader.
  2. Give uv-coordinates for the point.
  3. Enable blending

Load explosion.png and bind it to unit 0. Lastly, add these lines of code to allow us to properly scale the points.

labhelper::setUniformSlow(particleProgram, "screen_x", float(windowWidth));
labhelper::setUniformSlow(particleProgram, "screen_y", float(windowHeight));

part

Now, drive around with your blazingly fast space ship!

When done, show your result to one of the assistants. Have the finished program running and be prepared to explain what you have done.