Soft Particles
Before we start
First you need to check your existing particles. Run up quake2 in gl mode and have a look at any particles. Railgun effects are usually good for checking out particles, or you can set cl_testparticles to 1.

If your particles look like nice round discs, then your hardware supports a gl extension for point parameters. By default Quake2 will try to render particles using points but it needs the extension to be supported so that it can specify a diameter for the points. You can turn off this rendering method by setting gl_ext_pointparameters to 0.

Now your particles should look like ugly squat crosses, this is what Quake2 does when point parameters are not supported. It creates a texture internally when the ref_gl initializes and then maps that onto little triangular polygons in order to render the particles. Have a look at this code from the top of gl_rmisc.c
original ugly particles
/*
==================
R_InitParticleTexture
==================
*/
byte	dottexture[8][8] =
{
	{0,0,0,0,0,0,0,0},
	{0,0,1,1,0,0,0,0},
	{0,1,1,1,1,0,0,0},
	{0,1,1,1,1,0,0,0},
	{0,0,1,1,0,0,0,0},
	{0,0,0,0,0,0,0,0},
	{0,0,0,0,0,0,0,0},
	{0,0,0,0,0,0,0,0},
};

void R_InitParticleTexture (void)
{
	int		x,y;
	byte	data[8][8][4];

	//
	// particle texture
	//
	for (x=0 ; x<8 ; x++)
	{
		for (y=0 ; y<8 ; y++)
		{
			data[y][x][0] = 255;
			data[y][x][1] = 255;
			data[y][x][2] = 255;
			data[y][x][3] = dottexture[x][y]*255;
		}
	}
	r_particletexture = GL_LoadPic ("***particle***", (byte *)data, 8, 8, it_sprite, 32);

	//
	// also use this for bad textures, but without alpha
	//
	for (x=0 ; x<8 ; x++)
	{
		for (y=0 ; y<8 ; y++)
		{
			data[y][x][0] = dottexture[x&3][y&3]*255;
			data[y][x][1] = 0; // dottexture[x&3][y&3]*255;
			data[y][x][2] = 0; //dottexture[x&3][y&3]*255;
			data[y][x][3] = 255;
		}
	}
	r_notexture = GL_LoadPic ("***r_notexture***", (byte *)data, 8, 8, it_wall, 32);
}
Here you can see that the function R_InitParticleTexture creates two textures based on the dottexture array. One of these it assigns to r_particletexture to use for particles. The other is assigned to r_notexture to use for brushes when the wal texture cannot be loaded.

The first thing we will do is try to improve this default particle texture. R_InitParticleTexture uses the array to set the transparency of the texture. Where there is a one, the texture is opaque, where it is zero, the texture is transparent. The color is set to white throughout. This texture will be modulated by the color and transparency of the particle when it comes to be rendered.
Softening the particles
Lets improve things a little. Change the dottexture array to read like so:
byte	dottexture[8][8] =
{
	{0  ,1  ,2  ,4  ,4  ,2  ,1  ,0  },
	{1  ,4  ,8  ,16 ,16 ,8  ,4  ,1  },
	{2  ,8  ,32 ,64 ,64 ,32 ,8  ,2  },
	{4  ,16 ,64 ,255,255,64 ,16 ,4  },
	{4  ,16 ,64 ,255,255,64 ,16 ,4  },
	{2  ,8  ,32 ,64 ,64 ,32 ,8  ,2  },
	{1  ,4  ,8  ,16 ,16 ,8  ,4  ,1  },
	{0  ,1  ,2  ,4  ,4  ,2  ,1  ,0  },
};
And change the particle texture creation to:
	//
	// particle texture
	//
	for (x=0 ; x<8 ; x++)
	{
		for (y=0 ; y<8 ; y++)
		{
			data[y][x][0] = 255;
			data[y][x][1] = 255;
			data[y][x][2] = 255;
			data[y][x][3] = dottexture[x][y]; // c14 Just this line changes
		}
	}
Now you can compile up and have a look at the effect. Your particles will have softer edges because there is now a range of transparencies in the texture, rather than just the on off that we had previously.

HOWEVER you may have noticed that your particles are missing one corner.
softer but clipped particles
Fixing the clipping issue
To see why, let's have a look at the function R_DrawParticles in the file gl_rmain.c
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];

    GL_Bind(r_particletexture->texnum);
	qglDepthMask( GL_FALSE );		// no z buffering
	qglEnable( GL_BLEND );
	GL_TexEnv( GL_MODULATE );
	qglBegin( GL_TRIANGLES );

	VectorScale (vup, 1.5, up);
	VectorScale (vright, 1.5, right);

	for ( p = particles, i=0 ; i < num_particles ; i++,p++)
	{
		// hack a scale up to keep particles from disapearing
		scale = ( p->origin[0] - r_origin[0] ) * vpn[0] +
			    ( p->origin[1] - r_origin[1] ) * vpn[1] +
			    ( p->origin[2] - r_origin[2] ) * vpn[2];

		if (scale < 20)
			scale = 1;
		else
			scale = 1 + scale * 0.004;

		*(int *)color = colortable[p->color];
		color[3] = p->alpha*255;

		qglColor4ubv( color );

		qglTexCoord2f( 0.0625, 0.0625 );
		qglVertex3fv( p->origin );

		qglTexCoord2f( 1.0625, 0.0625 );
		qglVertex3f( p->origin[0] + up[0]*scale,
			         p->origin[1] + up[1]*scale,
					 p->origin[2] + up[2]*scale);

		qglTexCoord2f( 0.0625, 1.0625 );
		qglVertex3f( p->origin[0] + right[0]*scale,
			         p->origin[1] + right[1]*scale,
					 p->origin[2] + right[2]*scale);
	}

	qglEnd ();
	qglDisable( GL_BLEND );
	qglColor4f( 1,1,1,1 );
	qglDepthMask( 1 );		// back to normal Z buffering
	GL_TexEnv( GL_REPLACE );
}
This function loops through all of the active particles and renders them. Outside of the loop we have some gl instructions. The texture environment is set to GL_MODULATE so that the rendered texture can be modulated by the required color and transparency. Then we start a set of render operations with the qglBegin( GL_TRIANGLES ) Instruction.

GL_TRIANGLES tells the renderer that it should regard the coordinates that it receives in triplets, each triplet corresponding to one triangle. Then inside the loop, we set the environment with the color and transparency for the particle, and give the vertex coordinates and the texture coordinates for the triangle which will contain the polygon. The triangle is always oriented so that it is flat on to the viewer.

It is because the particles are rendered as triangles that we are losing a corner of our new softer particles, it also explains why the original texture was not centred in the dottexture array. The texture is mapped onto the triangle, offset a little and neatly fits between the corners of it. One consequence is that particles are always off to the right and above where they really should be on the screen.

How original particles are rendered and texture mapped

How the original particles are rendered and texture mapped

To fix all this we will do the following:
Use quadrilaterals instead of triangles to render each particle and show our entire texture.
Place the centre new quadrilateral at the particle origin, instead of just one corner of it.
Change the size of the quadrilateral to compensate for that fact that our new particle textures covers a larger area than before.
Change the texture mapping coordinates to match our new texture to our new shape.

Change the code to look like this.
      GL_Bind(r_particletexture->texnum);
	qglDepthMask( GL_FALSE );		// no z buffering
	qglEnable( GL_BLEND );
	GL_TexEnv( GL_MODULATE );
	qglBegin( GL_QUADS);               // c14 This line changes

	VectorScale (vup, .5, up);         // c14 new size in here
	VectorScale (vright, .5, right);   // c14 new size in here

	for ( p = particles, i=0 ; i < num_particles ; i++,p++)
	{
		// hack a scale up to keep particles from disapearing
		scale = ( p->origin[0] - r_origin[0] ) * vpn[0] +
			    ( p->origin[1] - r_origin[1] ) * vpn[1] +
			    ( p->origin[2] - r_origin[2] ) * vpn[2];

		if (scale < 40)                // c14 this scale has changed
			scale = 1;
		else
			scale = 1 + scale * 0.004;

		*(int *)color = colortable[p->color];
		color[3] = p->alpha*255;

		qglColor4ubv( color );

		// c14 change from here ...

		qglTexCoord2f( 0, 0 );
		qglVertex3f( p->origin[0] - right[0]*scale - up[0]*scale,
			         p->origin[1] - right[1]*scale - up[1]*scale,
					 p->origin[2] - right[2]*scale - up[2]*scale);

		qglTexCoord2f( 0, 1 );
		qglVertex3f( p->origin[0] - right[0]*scale + up[0]*scale,
			         p->origin[1] - right[1]*scale + up[1]*scale,
					 p->origin[2] - right[2]*scale + up[2]*scale);

		qglTexCoord2f( 1, 1 );
		qglVertex3f( p->origin[0] + right[0]*scale + up[0]*scale,
			         p->origin[1] + right[1]*scale + up[1]*scale,
					 p->origin[2] + right[2]*scale + up[2]*scale);

		qglTexCoord2f( 1, 0 );
		qglVertex3f( p->origin[0] + right[0]*scale - up[0]*scale,
			         p->origin[1] + right[1]*scale - up[1]*scale,
					 p->origin[2] + right[2]*scale - up[2]*scale);

       // c14 ... to here, 4 vertices, 4 texture coordinates instead of three

	}

	qglEnd ();
	qglDisable( GL_BLEND );
	qglColor4f( 1,1,1,1 );
	qglDepthMask( 1 );		// back to normal Z buffering
	GL_TexEnv( GL_REPLACE );        
Now compile again and you should see softer particles with no clipping. clipping issue resolved
Adding external textures
This is now very simple. Go back to gl_rmisc.c, to the R_InitParticleTexture function and make these changes.
extern image_t *Draw_FindPic(char *name);  //c14 add this external declaration

void R_InitParticleTexture (void)
{
	int		x,y;
	byte	data[8][8][4];


    //
    // particle texture
    //
    for (x=0 ; x<8 ; x++)
	{
	   	for (y=0 ; y<8 ; y++)
		{
		  data[y][x][0] = 255;
		  data[y][x][1] = 255;
		  data[y][x][2] = 255;
		  data[y][x][3] = dottexture[x][y];
		}
	}
    r_particletexture = Draw_FindPic("particle");   //c14 add this line

    if (!r_particletexture) {                                 //c14 add this line
		r_particletexture = GL_LoadPic ("***particle***", (byte *)data, 8, 8, it_sprite, 32);
    }                                                         //c14 add this line 
All we've done here, is that we try to load r_particletexture with an external texture using the standard Draw_FindPic function. We have asked for 'particle' so it will try to load 'pics/particle.pcx'. If you have modified your quake2 to load tga files, then I guess it will try to load 'pics/particle.tga' instead.

If it can't find the texture that it is looking for, then it will load the internally created dottexture as it originally did.
external tga texture loaded
Other things to think about
You can make the default texture nicer if you want by making it bigger. Adjust the dimensions of the dottexture array, put more information in it and don't forget to adjust the code in the R_InitParticleTexture function to work with the new dimensions instead of the original 8.

If you haven't yet made quake2 load tga files in place of pcx files, then you should do so. I have a tutorial on that here.