Table of content

Previous: 3.b Building a better laser

# Lesson 4: Leaving trails: creating a complex fireworks effect

This is where the real fun begins. In this lesson, we put together the tools of the previous lessons to create a classic pyrotechnic effect: we will program rockets which rise while leaving behind a nice green trail and a shower of white sparks. Though interesting in its own right (it's pretty!), the code can be straightforwardly modified for missiles, shooting stars or any luminous projectiles.

# a. The theory behind the effect

Figure 4.1 shows what we would like to create with 3DGS: a leader (a firework rocket, a missile, a shooting star, a comet, etc.) moves in the level along a trajectory while leaving behind one, or several, tails or trails (smoke, luminous gas, sparks, etc.).

Figure 4.1: (left) The 'leader' follows a trajectory while leaving behind a tail of particles. (right) We may think that an entity (blue circle) moves along a smooth trajectory (green line), but in 3DGS we only compute and display snapshots of this movement at instants separated by time ticks. Since we do not know this trajectory, we create the tail by placing particles along straight lines connecting the positions of the entity at instants t, t+time, t+2*time, etc (blue dashed lines).

The tail is going to be made of tightly arranged particles, much like we did in lesson 3. The main difference is that the particles will not be created by a stationary source, but instead by a moving one (the leader). This asks the question "where should we put the tail particles so as to give the impression they were left behind by the leader?".

You might think "Easy! Just put them behind the leader at each frame and that will look ok".

Figure 4.1(right) shows that it is not quite as simple as that. Indeed, in our minds, we program and think of entities as if they moved along smooth curvy trajectories, like the objects we encounter in our everyday life (cars, snowflakes, basketballs, etc.). However, remember that we are working with computers, where everything is a question of displaying discrete images or frames on a computer screen. All we really have to work with, all we compute, is the position of our entities at discrete instants separated by time ticks (see Figure 4.1). It is our eyes and brain which create back the illusion of continuous movement when we play video games. Of course, it is the exact same thing when watching movies on television or at cinemas, and it works quite well as long as the frame rate is large enough. However, if the latter drops too much, the movements start to look jerky, like in those movies in the first days of cinematography.

So, in a nutshell, we want to create a trail of particles which follows the smooth trajectory of an object, but we cannot compute that trajectory or we do not want to waste all our CPU power trying to compute it. We have to find some way of guessing, or approximating, what the entity is doing between the positions we actually computed [at times t, t+time, t+2*time, etc. (see Figure 4.1(right))].

I propose the simplest and most economical solution of making the assumption that the frame rate of the game is high enough (and that the entity's trajectory is smooth enough) so that we can approximate the movement of the leader between the positions already computed by straight lines (see Figure 4.1(right)). Obviously, as long as the green line does not wiggle around too much, the straight lines making up the tail will look smooth and close to what the leader is doing. However, even for a smooth trajectory, when the frame rate drops the value of time will rise, as will the length of the straight lines we use to create the tail. If the fps drops too much, the result will become very geometrical and ugly. Then a better approximation of the trajectory should be used.

However, in the example below, you can check that things still look alright for a frame rate as low as 30 fps, a value below which games rarely drop with today's hardware. So in most cases, the present algorithm should work fine.

Ok, enough theory: let's check out the code of the example. Lesson4.sed contains 1 action and 3 functions. The action, a_launcher, is virtually identical to the one used in lesson 1 for the particles fountain: it creates a leader (i.e. a rocket) every 4 ticks and sends it in the air with a random velocity. The only big difference is the presence of instructions to count the 4 ticks delay between launches using my.skill1 as counter:

    var initial_speed = 80;

    action a_launcher
    {

    	var theta;
    	var phi;
    	var init_velocity;

    	my.skill1 = 4;			// wait 4 ticks between launches

    	while(1)
    	{
    		if (my.skill1>0)
    		{
    			my.skill1 -= time;
    		}
    		else
    		{
    			my.skill1 = 4;
    			// pick an initial velocity in a cone
    			theta = random(30);
    			phi = random(360);
    			init_velocity.x = initial_speed*cos(phi)*sin(theta);
    			init_velocity.y = initial_speed*sin(phi)*sin(theta);
    			init_velocity.z = initial_speed*cos(theta);
    			// launch now
    			effect(leader,1,my.x,init_velocity.x);
    		}
    		wait(1);
    	}
    }

Function leader() implements the behavior of the rocket. The first lines of code, you can figure out by yourself: they simply define the aspect (it is red and shines) and dynamic behavior (it moves and is attracted to the ground) of the leader particle. Afterwards, the leader is made to sparkle simply by varying randomly its size between 225 and 300 quants.

    bmap leader_image = <novaR1.pcx>;
    var leader_life = 32;

    function leader()
    {
    	if (my.lifespan==80)
    	{
    		my.bmap = leader_image;
    		my.size = 300;
    		my.bright = on;
    		my.flare = on;
    		my.alpha = 100;
    		my.move  = on;
    		my.red = 255;
    		my.green = 150;
    		my.blue = 50;
    		my.gravity = 3;
    		my.lifespan = leader_life;
    	}

    	// leader size fluctuates a bit
    	my.size = 300*(0.75+0.25*random(1));
    }

Function leader() also creates green tail particles (function tail()) and, during the last half of its life, sprinkles about white sparks (function sparks()) [see instructions in the second half of the function leader() presented below]. These last two types of particles do not have anything particular. Here is the function for the tail particles:

    bmap tail_image = <novaw.pcx>;
    var tail_size = 40;
    var tail_life = 7;

    function tail()
    {
    	if (my.lifespan==80)
    	{
    		my.bmap = tail_image;
    		my.size = tail_size;
    		my.bright = on;
    		my.flare = on;
    		my.alpha = 100;
    		my.move  = on;
    		my.red = 50;
    		my.green = 255;
    		my.blue = 50;
    		my.gravity = 0;
    		my.lifespan = tail_life;
    	}

    	// erases the tail after a while
    	my.alpha = 100*(my.lifespan/tail_life);
    }

Tail particles are very ordinary: there are only two things to notice about them. First, they will not move after being created: no velocity and no gravity makes them stay in place after the leader has moved away. This way we get a nice compact trail instead of a diffuse cloud of particles. The latter would look great too. In fact, all sorts of neat effects can be obtained by playing around with the initial velocity of trail particles.

Second, we erase the particles after a while, giving the tail a rounded and somewhat softer look. This is done by the factor 100*(my.lifespan/tail_life) which decreases with time: 100*(my.lifespan/tail_life) = 100 when my.lifespan = tail_life and decreases to 0 when my.lifespan = 0. Playing also with the color of particles would allow an almost limitless number of different effects.

The particles we use for white sparks are also quite common:

    bmap spark_image = <novaw.pcx>;
    var spark_size = 50;
    var spark_max_life = 20;

    function spark()
    {
    	if (my.lifespan==80)
    	{
    		my.bmap = spark_image;
    		my.size = 0;
    		my.bright = on;
    		my.flare = on;
    		my.alpha = 100;
    		my.move  = on;
    		my.red = 255;
    		my.green = 255;
    		my.blue = 255;
    		my.gravity = 2;
    		my.lifespan = 5 + random(spark_max_life-5);
    	}

    	// makes the sparks "pop"
    	my.size = spark_size*sin(180*my.lifespan/spark_max_life);
    }

They behave very much like those used in the particle fountain of lesson 2: their lifespan is drawn randomly between 5 and 20 ticks, and their size evolves with time according to a sine function (see Figure 2.2) making them 'pop' in mid-air.

Next: 4.b: Putting the trail together