Table of content

Previous: 3.a Minimal code for the laser

# b. Building a better laser

A more promising solution would be to create more than one particle at each time frame, and to place these new particles along the axis of the beam to "plug the gaps". Thanks to vector algebra, only a few lines of code are necessary to do this. Here is the improved action of the laser beam:

    var n;
    var position;
    var velocity_dir;

    action a_improved_laser
    {
    	// define a direction/velocity for the laser
    	velocity.x = 50;
    	velocity.y = 0;
    	velocity.z = 0;

    	// loop which generates the photons
    	while(1)
    	{
    		n = ceiling(vec_length(velocity.x)*time/photon_size);

    		if (key_2)
    		{
    			my.skill1 = 1;		// counts particles in the strip
    			while(my.skill1<=n)
    			{
    				// compute the unitary vector parallel to the velocity
    				vec_set(velocity_dir.x,velocity.x);
    				vec_normalize(velocity_dir.x,1);

    				// places the n particles along a strip begining at my.x
    				// and ending at my.x + vel_direction*l*(n-1)
    				position.x = my.x +
    				        (my.skill1-1)*velocity_dir.x*photon_size
    					 - time*velocity.x;
    				position.y = my.y +
    				        (my.skill1-1)*velocity_dir.y*photon_size
    					 - time*velocity.y;;
    				position.z = my.z +
    				        (my.skill1-1)*velocity_dir.z*photon_size
    					 - time*velocity.z;

    				// create the particles
    				effect(improved_photon,1,position.x,velocity.x);

    				//increment the counter
    				my.skill1 += 1;
    			}
    		}
    		wait(1);
    	}
    }

which gives the beam in Figure 3.2.

Figure 3.2: Laser beam generated with the slightly improved code but still using no bitmap to represent the particles.

This is certainly an improvement! The gaps have vanished and the beam looks solid, though it reminds me a little of those old sci-fi movies. But what have we done here? Figure 3.3 illustrates it (I hope) clearly.

Figure 3.3: (top) Drawing of the 'beam' generated by the minimal script: particles (in blue) are created at intervals of time ticks, moving in a straight line from left to right (green dashed line) by jumps of velocity*time (large gray arrow) and with gaps between them of size |velocity|*time. (middle) The beam created by the improved script. At each frame we create a strip of particles: the red squares (representing particles) are created at time t+2*time, the green ones earlier at time t+time. The variable my.skill1 of action a_improved_laser counts the particles in the strip and goes from 1 to n. All the particles move along a straight line with equal velocity. Notice the slight overlap between consecutive strips. (bottom) Replacing the particles by a bitmap of light dot generates a dotted line because of the black borders on the sprites (left strip). Introducing the parameter photon_p = 0.2 in the equations produces 1/0.2 = 5 times more particles per strip and tightens their arrangement into a bright compact beam (right strip). small gray arrow = velocity_dir*photon_size.

Instead of emitting single particles at each time step, we want to create whole strips of them, all with equal velocity. The strips will be long enough to cover the gaps observed before and will also have a slight overlap with each other (see Figure 3.3-middle). This way, we can be sure that the beam will not have any holes in it (which would look very ugly!).

The first question is how many particles do we need to build these strips? We could use hundreds: that would look alright but it would be a waste of memory and frame rate. Instead, we use the following formula for that number, which we call n:

    n = ceiling(vec_length(velocity.x)*time/photon_size)	(3.2)

As indicated by equation (2.6), the width of the gap in the beam is equal to the length of the vector velocity*time, i.e. how much each particle travels between two consecutive frames. To see how many particles we could fit in there, we just divide this value by the width of the particles, photon_size. The result might not be an integer so we round it off using the ceiling() function which we define as follows:

    function ceiling(x)
    {
    	return(int(x)+(frc(x)>0));
    }							(3.3)

(see manual for the definition of frc() and int()). ceiling(x), unlike round() defined by equation (2.4), always returns the smallest integer larger than x. For instance, ceiling(0.9) = 1 and ceiling(1.25) = 2. This way, the strip is always at least as long as the gaps, filling any holes that might occur in the beam. We therefore get the number of particles n defined in equation (3.2).

All that remains to do is create the particles in the right locations in the strips. For this we define the vector position which points where the particles should be placed:

    position = my.x + (my.skill1-1)*velocity_dir*photon_size - time*velocity	(3.4)

where my.skill1 goes from 1 to n. velocity_dir is the vector of length 1 which points in the direction of velocity, and is obtained with a vec_normalize instruction (see equations (1.8) and (1.9)). Now, lets see how equation (3.4) works.

The last factor, - time*velocity, is present for the same reason as in lesson 2: to make sure that the beam starts at the tip of the laser, and not a distance time*velocity in front of it (see equation (2.7) - we will drop this term from the reasoning below for simplicity). What about the rest?

Let's give my.skill1 some values to see what vector position comes out of equation (3.4). If my.skill1 = 1, then position = my.x: position points to the tip of the laser canon; we put a first particle there. Next, the loop sets my.skill1 = 2. This time, position = my.x + velocity_dir*photon_size: it points to the tip of the canon followed by a step equal to the particle's width, photon_size, in the direction of the beam. By creating a particle there, we set it neatly next to the first along the direction of the beam (see Figure 3.3-middle). If my.skill1 = 3, position = my.x + 2*velocity_dir*photon_size and this puts the third particle in a straight line right next to the second photon. And so on. When the loop is completed, it has created a strip of n particles, side by side, positioned along the direction of the beam (see green strip on Figure 3.3-middle).

At the next frame, these n particles will have moved forward by the distance velocity*time along the x axis. The gap between this strip and the tip of the canon will be filled by a new strip, created exactly like we just described above (see red strip on Figure 3.3-middle). And so on as long as the user presses the "1" key. Voila! A nice compact laser beam.

The last thing we need to do is to get rid of those clumsy green squares and put our favorite light dot sprite instead (like the bitmap novaV1.pcx I included in the level). However, before doing this, we need to modify the script one last time. Indeed, as shown on the left strip of Figure 3.3-bottom, if we use these sprites for the particles, we will get a dotted line again because of the black area on the sprites around the light dot. To work with this bitmap, we need to tighten the arrangement of particles in the strips.

One way to do it is to replace photon_size in equations (3.2) and (3.4) by photon_size*photon_p, where photon_p is a parameter which we set by hand between 0 and 1. Setting photon_p = 1 changes nothing. But if we choose photon_p smaller than 1, this makes the algorithm 'think' that the particles are narrower than they really are. As a consequence, the number n of particles in the strip increases while the total length of the strip remains unchanged at velocity*time: particles are now squeezed tighter and overlap with their neighbors. By choosing photon_p small enough, (0.2 in the case of bitmap novaV1.pcx), the black borders in the bitmap can be eliminated altogether and we get a nice continuous bright- looking beam. The larger the portion of the bitmap is black, the smaller photon_p needs to be ( photon_p is roughly equal to the proportion of black area on the sprite).

Here is a look at the final beam effect:

Figure 3.4: Finished look of the laser beam once we use a bitmap for the particles and we adjust the p_photon parameter to 0.2.

Much better! :-) Well, that is all for the laser beam project. As I mentioned at the beginning of this lesson, this script is alright for stationary lasers but not for moving ones. This is mainly because we build the beam piece by piece (strip by strip, rather). If we move the canon while we shoot it, the piece-wise structure of the beam would start to show. This could however be fixed by generalizing the algorithm. The same goes for making a light saber: computations similar to those above would be useful to compute on the fly the right number of particles to be used to make up the "blade". This project is therefore not an end to itself but rather a collection of ideas to create more elaborate effects. We will now apply these very ideas to the creation of particle trails. See you in lesson 4!

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