Particle fade timing fix
Posted: Sun Oct 02, 2016 4:54 pm
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:
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:
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:
But first let's take care of the remaining p->die initialization problems.
Now, all that's left is to fix are the ramp checks in R_DrawParticles:
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:
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)
;
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;
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;
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;
}
}
}
}
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;
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;