Particle fade timing fix

Post tutorials on how to do certain tasks within game or engine code here.
Post Reply
mankrip
Posts: 924
Joined: Fri Jul 04, 2008 3:02 am

Particle fade timing fix

Post by mankrip »

Particle timing is a lot more complicated than it seems. When I started implementing translucent particles long ago, I've run into several timing glitches and ended up tweaking the translucency code in a way which was awful, but tolerable. Now I've finally spent some time studying the vanilla particle timing code to figure out what's wrong with it, and here are the solutions.

The die field in struct particle_t stores the time when the particle should disappear... but not always. Usually, it is set as in p->die = cl.time + 5, but on rocket trails, grenade trails and explosions, p->die is modified by p->type and p->ramp.

First, let's take a look at the ramp tables:

Code: Select all

int
	ramp1[8] = {0x6f, 0x6d, 0x6b, 0x69, 0x67, 0x65, 0x63, 0x61} // pt_explode
,	ramp2[8] = {0x6f, 0x6e, 0x6d, 0x6c, 0x6b, 0x6a, 0x68, 0x66} // pt_explode2
,	ramp3[6] = {0x6d, 0x6b,    6,    5,    4,    3,           } // pt_fire (rocket trail and smoke trail only) (smoke offset by 2)
	;
ramp1 and ramp2 are only used by explosions. ramp3 is used by both rocket trails and grenade trails. The first two color indexes in ramp3 are the orange tones used by the rocket trail; the grenade trail skips those and uses only the last four indexes.

The code above is already fixed by me. Originally, ramp3 had a size [8], with only the first 6 numbers defined; the last two undefined values weren't supposed to be used... but the vanilla code could actually access it randomly due to poor bounds checking.

See this code:

Code: Select all

void R_RocketTrail (vec3_t start, vec3_t end, int type)
{

[...]

	while (len > 0)
	{

[...]

		p->die = cl.time + 2;
		p->start_time = cl.time; // mankrip

		switch (type) // trail type, not particle type
		{
			case 0:	// rocket trail
				p->ramp = rand()&3; // 8 colors, from zero to 7
				// mankrip -- fix ramp range - begin
				if (p->ramp >= 6) // there's only 6 colors in this ramp (clamped by R_DrawParticles)
					p->ramp -= 6;
				// mankrip -- fix ramp range - end
				p->die = cl.time + (6 - p->ramp) / 5.0f; // for frametime * 5.0f // mankrip - p->die timing fix
				p->color = ramp3[ (int)p->ramp];//109 107 6 5
				p->type = pt_fire;
				for (j=0 ; j<3 ; j++)
					p->org[j] = start[j] + ((rand()%6)-3);
				break;

			case 1:	// smoke trail
				p->ramp = 2 + (rand () & 2); // mankrip -- fix ramp range -- skip fire colors, get only grayscale -- there's only 4 grayscale colors in ramp
				p->die = cl.time + (6 - p->ramp) / 5.0f; // for frametime * 5.0f // mankrip - p->die timing fix
				p->color = ramp3[ (int)p->ramp];
				p->type = pt_fire;
				for (j=0 ; j<3 ; j++)
					p->org[j] = start[j] + ((rand()%6)-3);
				break;
As you can see, rocket trails could actually start with an index beyond the indexes of the first six numbers of ramp3, due to the bitmasking allowing eight values, all the way up to 7. So, I've added an extra check to wrap the value properly in this case.

The equivalent fix for grenade trails was simpler, since it always skips the first two indexes of ramp3 and only uses the four indexes of gray tones. Here, the vanilla code was even worse, allowing values all the way up to 9 (zero to 7, plus the offset 2). All I had to do was adjust the bitmask to 2, to get the correct range of zero to 4 (plus the offset 2).

And the last fix in the R_RocketTrail function above was to recalculate p->die according to the resulting p->ramp values. It is defined as cl.time plus the remaining countdown of p->ramp divided by the default framerate of each trail. These default framerate values are defined by the vanilla code in R_DrawParticles:

Code: Select all

	time3 = frametime * 15.0f;
	time2 = frametime * 10.0f; // 15;
	time1 = frametime * 5.0f;
But first let's take care of the remaining p->die initialization problems.

Code: Select all

void R_ParticleExplosion (vec3_t org)
{
	int			i, j;
	particle_t	*p;

	for (i=0 ; i<1024 ; i++)
	{
		if (!free_particles)
			return;
		p = free_particles;
		free_particles = p->next;
		p->next = active_particles;
		active_particles = p;

		p->start_time = cl.time; // mankrip
		p->color = ramp1[0];
		p->ramp = rand()&3; // 8 colors, from zero to 7
		// calculate the amount of ramp steps, and multiply by the time of each ramp
		if (i & 1)
		{
			p->type = pt_explode;
			p->die = cl.time + (8 - p->ramp) / 10.0f; // for frametime * 10.0f // mankrip - p->die timing fix
		}
		else
		{
			p->type = pt_explode2;
			p->die = cl.time + (8 - p->ramp) / 15.0f; // for frametime * 15.0f // mankrip - p->die timing fix
		}
		for (j=0 ; j<3 ; j++)
		{
			p->org[j] = org[j] + ((rand()%32)-16);
			p->vel[j] = (rand()%512)-256;
		}
	}
}


void R_RunParticleEffect (vec3_t org, vec3_t dir, int color, int count)
{
	int			i, j;
	particle_t	*p;

	for (i=0 ; i<count ; i++)
	{
		if (!free_particles)
			return;
		p = free_particles;
		free_particles = p->next;
		p->next = active_particles;
		active_particles = p;

		if (count == 1024)
		{	// rocket explosion
			p->start_time = cl.time;
			p->color = ramp1[0];
			p->ramp = rand()&3;
			// calculate the amount of ramp steps, and multiply by the time of each ramp
			if (i & 1)
			{
				p->type = pt_explode;
				p->die = cl.time + (8 - p->ramp) / 10.0f; // for frametime * 10.0f // mankrip - p->die timing fix
			}
			else
			{
				p->type = pt_explode2;
				p->die = cl.time + (8 - p->ramp) / 15.0f; // for frametime * 15.0f // mankrip - p->die timing fix
			}
			for (j=0 ; j<3 ; j++)
			{
				p->org[j] = org[j] + ((rand()%32)-16);
				p->vel[j] = (rand()%512)-256;
			}
		}
		else
		{
			p->start_time = cl.time; // mankrip
			p->die = cl.time + 0.1*(rand()%5);
			p->color = (color&~7) + (rand()&7);
			p->type = pt_slowgrav;
			for (j=0 ; j<3 ; j++)
			{
				p->org[j] = org[j] + ((rand()&15)-8);
				p->vel[j] = dir[j]*15;// + (rand()%300)-150;
			}
		}
	}
}
Now, all that's left is to fix are the ramp checks in R_DrawParticles:

Code: Select all

void R_DrawParticles (void)
{

[...]

	time3 = frametime * 15.0f;
	time2 = frametime * 10.0f; // 15;
	time1 = frametime * 5.0f;

[...]

	for (p=active_particles ; p ; p=p->next)
	{

[...]

		switch (p->type)
		{
		case pt_static:
			break;
		case pt_fire:
			p->ramp += time1;
			if (p->ramp >= 6) // set particle to be removed on the next frame
				p->die = cl.time - 0.01f; // mankrip - was -1, screwing up particle alpha
			else
				p->color = ramp3[(int)p->ramp];
			p->vel[2] += grav;
			break;

		case pt_explode:
			p->ramp += time2;
			if (p->ramp >= 8) // set particle to be removed on the next frame
				p->die = cl.time - 0.01f; // mankrip - was -1, screwing up particle alpha
			else
				p->color = ramp1[(int)p->ramp];
			// mankrip - unroll - begin
			p->vel[0] += p->vel[0]*dvel;
			p->vel[1] += p->vel[1]*dvel;
			p->vel[2] += p->vel[2]*dvel - grav;
			// mankrip - unroll - end
			break;

		case pt_explode2:
			p->ramp += time3;
			if (p->ramp >= 8) // set particle to be removed on the next frame
				p->die = cl.time - 0.01f; // mankrip - was -1, screwing up particle alpha
			else
				p->color = ramp2[(int)p->ramp];
			// mankrip - unroll - begin
			p->vel[0] -= p->vel[0]*frametime;
			p->vel[1] -= p->vel[1]*frametime;
			p->vel[2] = p->vel[2] - p->vel[2]*frametime - grav;
			// mankrip - unroll - end
			break;
What should be noted about R_DrawParticles is that setting p->die in the past does not eliminate the particle immediately. The particle is still drawn in the current frame, and it will only be eliminated in the next frame. Since I don't want to change this vanilla behavior, and setting p->die to a negative time makes it impossible to measure the elapsed time against the initial p->die, what I've done here is to set p->die to a past time which is still very close to the current time -- and to the initial p->die defined in the previous fixes above.

Now, with all these fixes in place, we can finally use the particle timing for other effects, such as translucency, with accurate results on all kinds of particles:

Code: Select all

	if (r_particle_blend_mode.value)
		pparticle->alpha = 1.0f - ( (float)cl.time - pparticle->start_time) / (pparticle->die - pparticle->start_time);
	else
		pparticle->alpha = 1.0f;
Ph'nglui mglw'nafh mankrip Hell's end wgah'nagl fhtagn.
==-=-=-=-=-=-=-=-=-=-==
Dev blog / Twitter / YouTube
Baker
Posts: 3666
Joined: Tue Mar 14, 2006 5:15 am

Re: Particle fade timing fix

Post by Baker »

Interesting stuff.

I've mostly avoided the particle system except for Nehahra compatibility and giving WinQuake some of the FitzQuake adjustments for particles (separating rendering from physics mostly).

And I guess I needed to play with particle sizing in Mark V's software renderer because the particles would get too big at higher resolutions.
The night is young. How else can I annoy the world before sunsrise? 8) Inquisitive minds want to know ! And if they don't -- well like that ever has stopped me before ..
mankrip
Posts: 924
Joined: Fri Jul 04, 2008 3:02 am

Re: Particle fade timing fix

Post by mankrip »

My code in this tutorial isn't good enough either, but I've decided to post it as soon as possible, to avoid the risk of forgetting how to explain these quirks of the vanilla renderer.

Afterwards I've refined and rewritten all these changes in a more proper way, but I can't post it right now because my main laptop's PSU is busted.

Anyway, vore trails' particles should also be taken care of, and there's no need to subtract 0.01f from cl.time when setting the particle to be removed on the next frame; p->die = cl.time; is enough.
Ph'nglui mglw'nafh mankrip Hell's end wgah'nagl fhtagn.
==-=-=-=-=-=-=-=-=-=-==
Dev blog / Twitter / YouTube
Post Reply