Multiple Particle Effects 2. Our First Effect Introduction We are going to modify the engine and the gl renderer to make use of our new particle type parameter, and define a new particle effect. We will create two new particle types to change the appearance of the railgun. Before you start, you should have already implemented the first part of this tutorial. Creating new particles In the file client\cl_fx.c, find the function CL_RailTrail ```/* =============== CL_RailTrail =============== */ void CL_RailTrail (vec3_t start, vec3_t end) { vec3_t move; vec3_t vec; float len; int j; cparticle_t *p; float dec; vec3_t right, up; int i; float d, c, s; vec3_t dir; byte clr = 0x74; VectorCopy (start, move); VectorSubtract (end, start, vec); len = VectorNormalize (vec); MakeNormalVectors (vec, right, up); for (i=0 ; inext; p->next = active_particles; active_particles = p; p->time = cl.time; VectorClear (p->accel); d = i * 0.1; c = cos(d); s = sin(d); VectorScale (right, c, dir); VectorMA (dir, s, up, dir); p->alpha = 1.0; p->alphavel = -1.0 / (1+frand()*0.2); p->color = clr + (rand()&7); for (j=0 ; j<3 ; j++) { p->org[j] = move[j] + dir[j]*3; p->vel[j] = dir[j]*6; } VectorAdd (move, vec, move); } dec = 0.75; VectorScale (vec, dec, vec); VectorCopy (start, move); while (len > 0) { len -= dec; if (!free_particles) return; p = free_particles; free_particles = p->next; p->next = active_particles; active_particles = p; p->time = cl.time; VectorClear (p->accel); p->alpha = 1.0; p->alphavel = -1.0 / (0.6+frand()*0.2); p->color = 0x0 + rand()&15; for (j=0 ; j<3 ; j++) { p->org[j] = move[j] + crand()*3; p->vel[j] = crand()*3; p->accel[j] = 0; } VectorAdd (move, vec, move); } } ``` This function creates a railtrail by creating a long line of grey particles, and a long spiral of blue particles. You should pay particular attention to the first half of the function, which is concerned with the spiral. It steps along the line of the railtrail winding the particles around it as it goes. Now replace that whole function with this; ```/* =============== CL_RailTrail =============== */ void CL_RailTrail (vec3_t start, vec3_t end) // c14 Particles { vec3_t vec; int j; cparticle_t *p; byte clr = 0x74; VectorSubtract (end, start, vec); if (!free_particles) return; p = free_particles; free_particles = p->next; p->next = active_particles; active_particles = p; p->time = cl.time; VectorClear (p->accel); p->alpha = 1.0; p->alphavel = -0.8; p->color = clr; p->type = PT_SPIRAL; // c14 particles for (j=0 ; j<3 ; j++) { p->org[j] = start[j]; p->length[j] = vec[j]; p->vel[j] = 0; p->accel[j] = 0; } if (!free_particles) return; p = free_particles; free_particles = p->next; p->next = active_particles; active_particles = p; p->time = cl.time; VectorClear (p->accel); p->alpha = 1.0; p->alphavel = -1; p->color = 0x0F; p->type = PT_BEAM; // c14 particles for (j=0 ; j<3 ; j++) { p->org[j] = start[j]; p->length[j] = vec[j]; p->vel[j] = 0; p->accel[j] = 0; } } ``` Now we have replaced all of the particles in the original trail, with just 2. One has a new type PT_BEAM, and the other the type PT_SPIRAL. We set the color of the particles as before, but now the origin becomes the start of the trail, and the length of the trail is stored in the new length parameter. If you compile and play now, you won't see anything at all for rail guns, because in the last tutorial, we told the renderer to ignore all particles which were not of type PT_DEFAULT. Rendering the central beam. The rendering is done in the file ref_gl\gl_rmain.c in the function GL_DrawParticles. Just above that function add the following: ```// c14 added this function to draw spirals void MakeNormalVectors (vec3_t forward, vec3_t right, vec3_t up) { float d; // this rotate and negat guarantees a vector // not colinear with the original right[1] = -forward[0]; right[2] = forward[1]; right[0] = forward[2]; d = DotProduct (right, forward); VectorMA (right, -d, forward, right); VectorNormalize (right); CrossProduct (right, forward, up); } ``` This is the same MakeNormalVectors function used in client\cl_fx.c to help calculate the spiral. cl_fx no longer calculates the spiral, we just took all of that out, we'll need it here later. At the top of GL_DrawParticles, you need to add three variables. (My thanks to Brett Thomas for spotting that this bit was missing from the tut.) ```void GL_DrawParticles( int num_particles, const particle_t particles[], const unsigned colortable[768] ) { const particle_t *p; int i; vec3_t up, right; float scale; byte color[4]; vec3_t point, width, depth; // c14 add this line ``` Now in the body of GL_DrawParticles, right before the closing brace (curly bracket) add this. ```// code for beams GL_Bind(r_particletexture->texnum); qglDepthMask( GL_FALSE ); // no z buffering qglBlendFunc(GL_SRC_ALPHA, GL_ONE); qglEnable( GL_BLEND ); qglDisable(GL_CULL_FACE); GL_TexEnv( GL_MODULATE ); qglBegin( GL_QUADS); VectorScale (vup, 2, up); VectorScale (vright, 2, right); for ( p = particles, i=0 ; i < num_particles ; i++,p++) { if (p->type != PT_BEAM) continue; *(int *)color = colortable[p->color]; color[3] = p->alpha*255; qglColor4ubv( color ); VectorSubtract(p->origin, r_origin, point); CrossProduct(point, p->length, width); if (VectorLength(width)) VectorNormalize(width); else VectorCopy(vup, width); CrossProduct(p->length, width, depth); if (VectorLength(depth)) VectorNormalize(depth); else VectorCopy(vright, depth); VectorScale(depth, 2, depth); VectorScale(width, 2, width); qglTexCoord2f( 0, 0 ); qglVertex3f( p->origin[0] + depth[0] + width[0], p->origin[1] + depth[1] + width[1], p->origin[2] + depth[2] + width[2]); qglTexCoord2f( 0, 1 ); qglVertex3f( p->origin[0] + depth[0] - width[0], p->origin[1] + depth[1] - width[1], p->origin[2] + depth[2] - width[2]); qglTexCoord2f( 0.5, 1 ); qglVertex3f( p->origin[0] - width[0], p->origin[1] - width[1], p->origin[2] - width[2]); qglTexCoord2f( 0.5, 0 ); qglVertex3f( p->origin[0] + width[0], p->origin[1] + width[1], p->origin[2] + width[2]); qglTexCoord2f( 0.5, 1 ); qglVertex3f( p->origin[0] + width[0], p->origin[1] + width[1], p->origin[2] + width[2]); qglTexCoord2f( 0.5, 0 ); qglVertex3f( p->origin[0] - width[0], p->origin[1] - width[1], p->origin[2] - width[2]); VectorSubtract(p->origin, r_origin, point); VectorAdd(point, p->length, point); CrossProduct(point, p->length, width); if (VectorLength(width)) VectorNormalize(width); else VectorCopy(vup, width); CrossProduct(p->length, width, depth); if (VectorLength(depth)) VectorNormalize(depth); else VectorCopy(vright, depth); VectorScale(depth, 2, depth); VectorScale(width, 2, width); qglTexCoord2f(0.5, 0); qglVertex3f( p->origin[0] + p->length[0] - width[0], p->origin[1] + p->length[1] - width[1], p->origin[2] + p->length[2] - width[2]); qglTexCoord2f(0.5, 1); qglVertex3f( p->origin[0] + p->length[0] + width[0], p->origin[1] + p->length[1] + width[1], p->origin[2] + p->length[2] + width[2]); qglTexCoord2f(0.5, 0); qglVertex3f( p->origin[0] + p->length[0] - width[0], p->origin[1] + p->length[1] - width[1], p->origin[2] + p->length[2] - width[2]); qglTexCoord2f(0.5, 1); qglVertex3f( p->origin[0] + p->length[0] + width[0], p->origin[1] + p->length[1] + width[1], p->origin[2] + p->length[2] + width[2]); qglTexCoord2f(1, 1); qglVertex3f( p->origin[0] + p->length[0] - depth[0] + width[0], p->origin[1] + p->length[1] - depth[1] + width[1], p->origin[2] + p->length[2] - depth[2] + width[2]); qglTexCoord2f(1, 0); qglVertex3f( p->origin[0] + p->length[0] - depth[0] - width[0], p->origin[1] + p->length[1] - depth[1] - width[1], p->origin[2] + p->length[2] - depth[2] - width[2]); } qglEnd (); qglEnable(GL_CULL_FACE); qglDisable( GL_BLEND ); qglColor4f( 1,1,1,1 ); qglDepthMask( 1 ); // back to normal Z buffering GL_TexEnv( GL_REPLACE ); ``` This is a complicated piece of work, but it simply aims to draw the beam from the start defined by the particle's origin, to an end measured by the particle's length. We could render a beam by drawing a long cylinder. This would by quite simple to picture mathematically, but it isn't very efficient when it comes to OpenGL. Instead we will render the beam as a long flat rectangle. As long as we can keep the full width of the rectangle to the viewer, so that they never see it edge on, it will look fine. It's very much like the original particles, which disks, always drawn flat on to the viewer, so they are indistinguishable from spheres. It's getting the rectangle flat to the viewer which requires the complicated vector maths, for each end of the beam we calculate a cross product of the view angle, and the beam direction and use this to set that end of the rectangle with its widest aspect to the viewer. Then we calculate another cross product, and put a half circle at right angles to the end of the beam, to make the end look rounded. If you compile and play this now, you'll be able to see single white beams instead of the string of particles we used to have. Also notice that the beams seem somehow brighter than they used to be. In the code that you pasted in, we have change the Blend Function which determines how OpenGL adds transparent objects, from qglBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); to qglBlendFunc(GL_SRC_ALPHA, GL_ONE); The brightness of default particles is partly determined by what is behind them, they are illuminated by the light that comes through them. But our beam provides its own light so the surrounding environment doesn't have the same effect. Now below that last little lot, add in some similar code for the spiral. ```// Code for spirals GL_Bind(r_particletexture->texnum); qglDepthMask( GL_FALSE ); // no z buffering qglBlendFunc(GL_SRC_ALPHA, GL_ONE); qglEnable( GL_BLEND ); qglDisable(GL_CULL_FACE); GL_TexEnv( GL_MODULATE ); qglBegin( GL_QUADS); for ( p = particles, i=0 ; i < num_particles ; i++,p++) { if (p->type != PT_SPIRAL) continue; { vec3_t move, vec, up, right, dir1, dir2, dir3, spdir; float c, d, s; int len; int loc; *(int *)color = colortable[p->color]; color[3] = p->alpha*255; qglColor4ubv( color ); VectorCopy(p->origin, move); VectorCopy(p->length, vec); len = VectorNormalize (vec); MakeNormalVectors (vec, right, up); for (loc = 0; loc < len; loc++) { d = loc * 0.1; c = cos(d); s = sin(d); VectorScale(right, c * 5, dir1); VectorMA (dir1, s * 5, up, dir1); d = (loc + 1) * 0.1; c = cos(d); s = sin(d); VectorScale(right, c * 5, dir2); VectorMA (dir2, s * 5, up, dir2); VectorAdd(dir2, vec, dir2); d = (loc + 2) * 0.1; c = cos(d); s = sin(d); VectorScale(right, c * 5, dir3); VectorMA (dir3, s * 5, up, dir3); VectorMA (dir3, 2, vec, dir3); VectorAdd (move, dir1, point); VectorSubtract(dir2, dir1, spdir); VectorSubtract(point, r_origin, point); CrossProduct(point, spdir, width); if (VectorLength(width)) VectorNormalize(width); else VectorCopy(vup, width); qglTexCoord2f( 0.5, 1 ); qglVertex3f(point[0] + width[0] + r_origin[0], point[1] + width[1] + r_origin[1], point[2] + width[2] + r_origin[2]); qglTexCoord2f( 0.5, 0 ); qglVertex3f(point[0] - width[0] + r_origin[0], point[1] - width[1] + r_origin[1], point[2] - width[2] + r_origin[2]); VectorAdd (move, dir2, point); VectorSubtract(dir3, dir2, spdir); VectorSubtract(point, r_origin, point); CrossProduct(point, spdir, width); if (VectorLength(width)) VectorNormalize(width); else VectorCopy(vup, width); qglTexCoord2f( 0.5, 0 ); qglVertex3f(point[0] - width[0] + r_origin[0], point[1] - width[1] + r_origin[1], point[2] - width[2] + r_origin[2]); qglTexCoord2f( 0.5, 1 ); qglVertex3f(point[0] + width[0] + r_origin[0], point[1] + width[1] + r_origin[1], point[2] + width[2] + r_origin[2]); VectorAdd (move, vec, move); } } } qglEnd (); qglEnable(GL_CULL_FACE); qglDisable( GL_BLEND ); qglColor4f( 1,1,1,1 ); qglDepthMask( 1 ); // back to normal Z buffering GL_TexEnv( GL_REPLACE ); qglBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); ``` This code does something similar to the spiralling code that we took out of client\cl_fx.c. It works along the length of the particle, and calculates points wound around the core. For each of these points, it draws a rectangle, very similar to the one that we draw for the PT_BEAM. We calculate a cross product to ensure that we never see the edges of the rectangles. Because each of these little rectangles joins up with the next, we don't need to draw the little half circles. Now compile up and play. You should see beautiful smooth railgun trails. Well not quite. If you look carefully you may see some little blocky bits, particularly in the parts of the spiral closest to you. Why? Well the answer is a little subtle. We calculate the cross product for each end of the section of spiral which we are drawing. Because each end is in a different place, and the direction of the spiral is slightly different at each point, the width vector which results from the cross product calculation is not the same for each end of the section. This is intentional, if they were going to be the same, we would only have to do the sums once, but what it represents in the 3d environment, is a twist in the rectangle which we are drawing. If this twist is slight, then there should be no problem, but some sections of the spiral, will have a high degree of twist, and cannot be drawn smoothly. Personally I think that a comprehensive solution is too complex for these tutorials, but if you want to experiment on your own I have a suggestion. The spiral is generally very smooth, it could be drawn effectively with half the number of sections. You could rewrite the looping code to work with fewer sections. But before you draw each section, measure the amount of twist in the section (hint use a dot product). If the twist is too high, break the section into two and try again. Note. If you do all this, and you're not getting the trail showing up, check that you have turned off gl_extpointparameters. You can't use textures for particles if quake2 is trying to use points. See my Soft Particles Tutorial for more information. In the next part of this tutorial, we will see how to load up different textures for different particle types. Go to part 3