Source: Atmospheric Effects

Date: 05-05-2001
Authors: Golliwog and Arnout 'RR2DO2' van Meer

This is the code that is used in Q3F to create the atmospheric effects in the maps, such as rain and snow. Please keep in mind that this is not a tutorial, only a direct source dump. Thus there is a lack of comments and clarity at some points. (And unless someone can give me a really good reason to change that, it isn't likely to change ;)

First a new file is needed in the cgame module, cg_atmospheric.c:

/*
**  	Copyright (C) 2000, 2001 by the Q3F Development team
**  	All rights reserved.
**
**  	cg_atmospheric.c
**
**  	Add atmospheric effects to view.
**
**  	Current supported effects are rain and snow.
*/

#include "cg_local.h"

#define MAX_ATMOSPHERIC_PARTICLES  	  	1000  	// maximum # of particles
#define MAX_ATMOSPHERIC_DISTANCE  	  	1000  	// maximum distance from refdef origin that particles are visible
#define MAX_ATMOSPHERIC_HEIGHT  	  	  	4096  	// maximum world height (FIXME: since 1.27 this should be 65536)
#define MIN_ATMOSPHERIC_HEIGHT  	  	  	-4096  	// minimum world height (FIXME: since 1.27 this should be -65536)
#define MAX_ATMOSPHERIC_EFFECTSHADERS  	6  	  	// maximum different effectshaders for an atmospheric effect
#define ATMOSPHERIC_DROPDELAY  	  	  	1000
#define ATMOSPHERIC_CUTHEIGHT  	  	  	800


#define ATMOSPHERIC_RAIN_SPEED  	  	1.1f * DEFAULT_GRAVITY
#define ATMOSPHERIC_RAIN_HEIGHT  	  	150

#define ATMOSPHERIC_SNOW_SPEED  	  	0.1f * DEFAULT_GRAVITY
#define ATMOSPHERIC_SNOW_HEIGHT  	  	10

typedef struct cg_atmosphericParticle_s {
  	vec3_t pos, delta, deltaNormalized, colour, surfacenormal;
  	float height, minz, weight;
  	qboolean active;
  	int contents, surface, nextDropTime;
  	qhandle_t *effectshader;
} cg_atmosphericParticle_t;

typedef struct cg_atmosphericEffect_s {
  	cg_atmosphericParticle_t particles[MAX_ATMOSPHERIC_PARTICLES];
  	qhandle_t effectshaders[MAX_ATMOSPHERIC_EFFECTSHADERS];
  	qhandle_t effectwatershader, effectlandshader;
  	int lastRainTime, numDrops;
  	int gustStartTime, gustEndTime;
  	int baseStartTime, baseEndTime;
  	int gustMinTime, gustMaxTime;
  	int changeMinTime, changeMaxTime;
  	int baseMinTime, baseMaxTime;
  	float baseWeight, gustWeight;
  	int baseDrops, gustDrops;
  	int numEffectShaders;
  	qboolean waterSplash, landSplash;
  	vec3_t baseVec, gustVec;

  	qboolean (*ParticleCheckVisible)( cg_atmosphericParticle_t *particle );
  	qboolean (*ParticleGenerate)( cg_atmosphericParticle_t *particle, vec3_t currvec, float currweight );
  	void (*ParticleRender)( cg_atmosphericParticle_t *particle );
} cg_atmosphericEffect_t;

static cg_atmosphericEffect_t cg_atmFx;

/*
**  Render utility functions
*/

void CG_EffectMark(  	qhandle_t markShader, const vec3_t origin, const vec3_t dir, float alpha, float radius ) {
  	// 'quick' version of the CG_ImpactMark function

  	vec3_t  	  	  	axis[3];
  	float  	  	  	texCoordScale;
  	vec3_t  	  	  	originalPoints[4];
  	byte  	  	  	colors[4];
  	int  	  	  	  	i;
  	polyVert_t  	  	*v;
  	polyVert_t  	  	verts[4];

  	if ( !cg_addMarks.integer ) {
  	  	return;
  	}

  	if ( radius <= 0 ) {
  	  	CG_Error( "CG_EffectMark called with <= 0 radius" );
  	}

  	// create the texture axis
  	VectorNormalize2( dir, axis[0] );
  	PerpendicularVector( axis[1], axis[0] );
  	VectorSet( axis[2], 1, 0, 0 );  	  	  	// This is _wrong_, but the function is for water anyway (i.e. usually flat)
  	CrossProduct( axis[0], axis[2], axis[1] );

  	texCoordScale = 0.5 * 1.0 / radius;

  	// create the full polygon
  	for ( i = 0 ; i < 3 ; i++ ) {
  	  	originalPoints[0][i] = origin[i] - radius * axis[1][i] - radius * axis[2][i];
  	  	originalPoints[1][i] = origin[i] + radius * axis[1][i] - radius * axis[2][i];
  	  	originalPoints[2][i] = origin[i] + radius * axis[1][i] + radius * axis[2][i];
  	  	originalPoints[3][i] = origin[i] - radius * axis[1][i] + radius * axis[2][i];
  	}

  	colors[0] = 127;
  	colors[1] = 127;
  	colors[2] = 127;
  	colors[3] = alpha * 255;

  	for ( i = 0, v = verts ; i < 4 ; i++, v++ ) {
  	  	vec3_t  	  	delta;

  	  	VectorCopy( originalPoints[i], v->xyz );

  	  	VectorSubtract( v->xyz, origin, delta );
  	  	v->st[0] = 0.5 + DotProduct( delta, axis[1] ) * texCoordScale;
  	  	v->st[1] = 0.5 + DotProduct( delta, axis[2] ) * texCoordScale;
  	  	*(int *)v->modulate = *(int *)colors;
  	}

  	trap_R_AddPolyToScene( markShader, 4, verts );
}

/*
**  	Raindrop management functions
*/

static qboolean CG_RainParticleCheckVisible( cg_atmosphericParticle_t *particle )
{
  	// Check the raindrop is visible and still going, wrapping if necessary.

  	float moved;
  	vec3_t distance;

  	if( !particle || !particle->active )
  	  	return( qfalse );

  	moved = (cg.time - cg_atmFx.lastRainTime) * 0.001;  	// Units moved since last frame
  	VectorMA( particle->pos, moved, particle->delta, particle->pos );
  	if( particle->pos[2] + ATMOSPHERIC_CUTHEIGHT < particle->minz )
  	  	return( particle->active = qfalse );

  	VectorSubtract( cg.refdef.vieworg, particle->pos, distance );
  	if( sqrt( distance[0] * distance[0] + distance[1] * distance[1] ) > MAX_ATMOSPHERIC_DISTANCE )
  	  	return( particle->active = qfalse );

  	return( qtrue );
}

static qboolean CG_RainParticleGenerate( cg_atmosphericParticle_t *particle, vec3_t currvec, float currweight )
{
  	// Attempt to 'spot' a raindrop somewhere below a sky texture.

  	float angle, distance, origz;
  	vec3_t testpoint, testend;
  	trace_t tr;

  	angle = random() * 2*M_PI;
  	distance = 20 + MAX_ATMOSPHERIC_DISTANCE * random();

  	testpoint[0] = testend[0] = cg.refdef.vieworg[0] + sin(angle) * distance;
  	testpoint[1] = testend[1] = cg.refdef.vieworg[1] + cos(angle) * distance;
  	testpoint[2] = origz = cg.refdef.vieworg[2];
  	testend[2] = testpoint[2] + MAX_ATMOSPHERIC_HEIGHT;

  	while( 1 )
  	{
  	  	if( testpoint[2] >= MAX_ATMOSPHERIC_HEIGHT )
  	  	  	return( qfalse );
  	  	if( testend[2] >= MAX_ATMOSPHERIC_HEIGHT )
  	  	  	testend[2] = MAX_ATMOSPHERIC_HEIGHT - 1;
  	  	CG_Trace( &tr, testpoint, NULL, NULL, testend, ENTITYNUM_NONE, MASK_SOLID|MASK_WATER );
  	  	if( tr.startsolid )  	  	  	// Stuck in something, skip over it.
  	  	{
  	  	  	testpoint[2] += 64;
  	  	  	testend[2] = testpoint[2] + MAX_ATMOSPHERIC_HEIGHT;
  	  	}
  	  	else if( tr.fraction == 1 )  	  	// Didn't hit anything, we're (probably) outside the world
  	  	  	return( qfalse );
  	  	else if( tr.surfaceFlags & SURF_SKY )  	// Hit sky, this is where we start.
  	  	  	break;
  	  	else return( qfalse );
  	}

  	particle->active = qtrue;
  	particle->colour[0] = 0.6 + 0.2 * random();
  	particle->colour[1] = 0.6 + 0.2 * random();
  	particle->colour[2] = 0.6 + 0.2 * random();
  	VectorCopy( tr.endpos, particle->pos );
  	VectorCopy( currvec, particle->delta );
  	particle->delta[2] += crandom() * 100;
  	VectorNormalize2( particle->delta, particle->deltaNormalized );
  	particle->height = ATMOSPHERIC_RAIN_HEIGHT + crandom() * 100;
  	particle->weight = currweight;
  	particle->effectshader = &cg_atmFx.effectshaders[0];

  	distance =  	((float)(tr.endpos[2] - MIN_ATMOSPHERIC_HEIGHT)) / -particle->delta[2];
  	VectorMA( tr.endpos, distance, particle->delta, testend );

  	CG_Trace( &tr, particle->pos, NULL, NULL, testend, ENTITYNUM_NONE, MASK_SOLID|MASK_WATER );
  	particle->minz = tr.endpos[2];
  	tr.endpos[2]--;
  	VectorCopy( tr.plane.normal, particle->surfacenormal );
  	particle->surface = tr.surfaceFlags;
  	particle->contents = CG_PointContents( tr.endpos, ENTITYNUM_NONE );

  	return( qtrue );
}

static void CG_RainParticleRender( cg_atmosphericParticle_t *particle )
{
  	// Draw a raindrop

  	vec3_t  	  	forward, right;
  	polyVert_t  	verts[4];
  	vec2_t  	  	line;
  	float  	  	len, frac;
  	vec3_t  	  	start, finish;

  	if( !particle->active )
  	  	return;

  	VectorCopy( particle->pos, start );
  	len = particle->height;
  	if( start[2] <= particle->minz )
  	{
  	  	// Stop rain going through surfaces.
  	  	len = particle->height - particle->minz + start[2];
  	  	frac = start[2];
  	  	VectorMA( start, len - particle->height, particle->deltaNormalized, start );

  	  	if( !cg_lowEffects.integer )
  	  	{
  	  	  	frac = (ATMOSPHERIC_CUTHEIGHT - particle->minz + frac) / (float) ATMOSPHERIC_CUTHEIGHT;
  	  	  	// Splash effects on different surfaces
  	  	  	if( particle->contents & (CONTENTS_WATER|CONTENTS_SLIME) )
  	  	  	{
  	  	  	  	// Water splash
  	  	  	  	if( cg_atmFx.effectwatershader && frac > 0 && frac < 1 )
  	  	  	  	  	CG_EffectMark( cg_atmFx.effectwatershader, start, particle->surfacenormal, frac * 0.5, 8 - frac * 8 );
  	  	  	}
  	  	  	else if( !(particle->contents & CONTENTS_LAVA) && !(particle->surface & (SURF_NODAMAGE|SURF_NOIMPACT|SURF_NOMARKS|SURF_SKY)) )
  	  	  	{
  	  	  	  	// Solid splash
  	  	  	  	if( cg_atmFx.effectlandshader && frac > 0 && frac < 1  )
  	  	  	  	  	CG_ImpactMark( cg_atmFx.effectlandshader, start, particle->surfacenormal, 0, 1, 1, 1, frac * 0.5, qfalse, 3 - frac * 2, qtrue );
  	  	  	}
  	  	}
  	}
  	if( len <= 0 )
  	  	return;

  	VectorCopy( particle->deltaNormalized, forward );
  	VectorMA( start, -len, forward, finish );

  	line[0] = DotProduct( forward, cg.refdef.viewaxis[1] );
  	line[1] = DotProduct( forward, cg.refdef.viewaxis[2] );

  	VectorScale( cg.refdef.viewaxis[1], line[1], right );
  	VectorMA( right, -line[0], cg.refdef.viewaxis[2], right );
  	VectorNormalize( right );

  	VectorMA( finish, particle->weight, right, verts[0].xyz );
  	verts[0].st[0] = 1;
  	verts[0].st[1] = 0;
  	verts[0].modulate[0] = 255;
  	verts[0].modulate[1] = 255;
  	verts[0].modulate[2] = 255;
  	verts[0].modulate[3] = 0;

  	VectorMA( finish, -particle->weight, right, verts[1].xyz );
  	verts[1].st[0] = 0;
  	verts[1].st[1] = 0;
  	verts[1].modulate[0] = 255;
  	verts[1].modulate[1] = 255;
  	verts[1].modulate[2] = 255;
  	verts[1].modulate[3] = 0;

  	VectorMA( start, -particle->weight, right, verts[2].xyz );
  	verts[2].st[0] = 0;
  	verts[2].st[1] = 1;
  	verts[2].modulate[0] = 255;
  	verts[2].modulate[1] = 255;
  	verts[2].modulate[2] = 255;
  	verts[2].modulate[3] = 127;

  	VectorMA( start, particle->weight, right, verts[3].xyz );
  	verts[3].st[0] = 1;
  	verts[3].st[1] = 1;
  	verts[3].modulate[0] = 255;
  	verts[3].modulate[1] = 255;
  	verts[3].modulate[2] = 255;
  	verts[3].modulate[3] = 127;

  	trap_R_AddPolyToScene( *particle->effectshader, 4, verts );
}

/*
**  	Snow management functions
*/

static qboolean CG_SnowParticleGenerate( cg_atmosphericParticle_t *particle, vec3_t currvec, float currweight )
{
  	// Attempt to 'spot' a raindrop somewhere below a sky texture.

  	float angle, distance, origz;
  	vec3_t testpoint, testend;
  	trace_t tr;

  	angle = random() * 2*M_PI;
  	distance = 20 + MAX_ATMOSPHERIC_DISTANCE * random();

  	testpoint[0] = testend[0] = cg.refdef.vieworg[0] + sin(angle) * distance;
  	testpoint[1] = testend[1] = cg.refdef.vieworg[1] + cos(angle) * distance;
  	testpoint[2] = origz = cg.refdef.vieworg[2];
  	testend[2] = testpoint[2] + MAX_ATMOSPHERIC_HEIGHT;

  	while( 1 )
  	{
  	  	if( testpoint[2] >= MAX_ATMOSPHERIC_HEIGHT )
  	  	  	return( qfalse );
  	  	if( testend[2] >= MAX_ATMOSPHERIC_HEIGHT )
  	  	  	testend[2] = MAX_ATMOSPHERIC_HEIGHT - 1;
  	  	CG_Trace( &tr, testpoint, NULL, NULL, testend, ENTITYNUM_NONE, MASK_SOLID|MASK_WATER );
  	  	if( tr.startsolid )  	  	  	// Stuck in something, skip over it.
  	  	{
  	  	  	testpoint[2] += 64;
  	  	  	testend[2] = testpoint[2] + MAX_ATMOSPHERIC_HEIGHT;
  	  	}
  	  	else if( tr.fraction == 1 )  	  	// Didn't hit anything, we're (probably) outside the world
  	  	  	return( qfalse );
  	  	else if( tr.surfaceFlags & SURF_SKY )  	// Hit sky, this is where we start.
  	  	  	break;
  	  	else return( qfalse );
  	}

  	particle->active = qtrue;
  	particle->colour[0] = 0.6 + 0.2 * random();
  	particle->colour[1] = 0.6 + 0.2 * random();
  	particle->colour[2] = 0.6 + 0.2 * random();
  	VectorCopy( tr.endpos, particle->pos );
  	VectorCopy( currvec, particle->delta );
  	particle->delta[2] += crandom() * 25;
  	VectorNormalize2( particle->delta, particle->deltaNormalized );
  	particle->height = ATMOSPHERIC_SNOW_HEIGHT + crandom() * 8;
  	particle->weight = particle->height * 0.5f;
  	particle->effectshader = &cg_atmFx.effectshaders[ (int) (random() * ( cg_atmFx.numEffectShaders - 1 )) ];

  	distance =  	((float)(tr.endpos[2] - MIN_ATMOSPHERIC_HEIGHT)) / -particle->delta[2];
  	VectorMA( tr.endpos, distance, particle->delta, testend );
  	CG_Trace( &tr, particle->pos, NULL, NULL, testend, ENTITYNUM_NONE, MASK_SOLID|MASK_WATER );
  	particle->minz = tr.endpos[2];
  	tr.endpos[2]--;
  	VectorCopy( tr.plane.normal, particle->surfacenormal );
  	particle->surface = tr.surfaceFlags;
  	particle->contents = CG_PointContents( tr.endpos, ENTITYNUM_NONE );

  	return( qtrue );
}

static void CG_SnowParticleRender( cg_atmosphericParticle_t *particle )
{
  	// Draw a snowflake

  	vec3_t  	  	forward, right;
  	polyVert_t  	verts[4];
  	vec2_t  	  	line;
  	float  	  	len, frac, sinTumbling, cosTumbling, particleWidth;
  	vec3_t  	  	start, finish;

  	if( !particle->active )
  	  	return;

  	VectorCopy( particle->pos, start );

  	sinTumbling = sin( particle->pos[2] * 0.03125f );
  	cosTumbling = cos( ( particle->pos[2] + particle->pos[1] )  * 0.03125f );

  	start[0] += 24 * ( 1 - particle->deltaNormalized[2] ) * sinTumbling;
  	start[1] += 24 * ( 1 - particle->deltaNormalized[2] ) * cosTumbling;

  	len = particle->height;
  	if( start[2] <= particle->minz )
  	{
  	  	// Stop snow going through surfaces.
  	  	len = particle->height - particle->minz + start[2];
  	  	frac = start[2];
  	  	VectorMA( start, len - particle->height, particle->deltaNormalized, start );
  	}
  	if( len <= 0 )
  	  	return;

  	VectorCopy( particle->deltaNormalized, forward );
  	VectorMA( start, -( len * sinTumbling ), forward, finish );

  	line[0] = DotProduct( forward, cg.refdef.viewaxis[1] );
  	line[1] = DotProduct( forward, cg.refdef.viewaxis[2] );

  	VectorScale( cg.refdef.viewaxis[1], line[1], right );
  	VectorMA( right, -line[0], cg.refdef.viewaxis[2], right );
  	VectorNormalize( right );

  	particleWidth = cosTumbling * particle->weight;

  	VectorMA( finish, particleWidth, right, verts[0].xyz );
  	verts[0].st[0] = 1;
  	verts[0].st[1] = 0;
  	verts[0].modulate[0] = 255;
  	verts[0].modulate[1] = 255;
  	verts[0].modulate[2] = 255;
  	verts[0].modulate[3] = 255;

  	VectorMA( finish, -particleWidth, right, verts[1].xyz );
  	verts[1].st[0] = 0;
  	verts[1].st[1] = 0;
  	verts[1].modulate[0] = 255;
  	verts[1].modulate[1] = 255;
  	verts[1].modulate[2] = 255;
  	verts[1].modulate[3] = 255;

  	VectorMA( start, -particleWidth, right, verts[2].xyz );
  	verts[2].st[0] = 0;
  	verts[2].st[1] = 1;
  	verts[2].modulate[0] = 255;
  	verts[2].modulate[1] = 255;
  	verts[2].modulate[2] = 255;
  	verts[2].modulate[3] = 255;

  	VectorMA( start, particleWidth, right, verts[3].xyz );
  	verts[3].st[0] = 1;
  	verts[3].st[1] = 1;
  	verts[3].modulate[0] = 255;
  	verts[3].modulate[1] = 255;
  	verts[3].modulate[2] = 255;
  	verts[3].modulate[3] = 255;

  	trap_R_AddPolyToScene( *particle->effectshader, 4, verts );
}

/*
**  	Set up gust parameters.
*/

static void CG_EffectGust()
{
  	// Generate random values for the next gust

  	int diff;

  	cg_atmFx.baseEndTime  	  	= cg.time  	  	  	  	  	+ cg_atmFx.baseMinTime  	  	+ (rand() % (cg_atmFx.baseMaxTime - cg_atmFx.baseMinTime));
  	diff  	  	  	  	  	  	= cg_atmFx.changeMaxTime  	- cg_atmFx.changeMinTime;
  	cg_atmFx.gustStartTime  	  	= cg_atmFx.baseEndTime  	  	+ cg_atmFx.changeMinTime  	+ (diff ? (rand() % diff) : 0);
  	diff  	  	  	  	  	  	= cg_atmFx.gustMaxTime  	  	- cg_atmFx.gustMinTime;
  	cg_atmFx.gustEndTime  	  	= cg_atmFx.gustStartTime  	+ cg_atmFx.gustMinTime  	  	+ (diff ? (rand() % diff) : 0);
  	diff  	  	  	  	  	  	= cg_atmFx.changeMaxTime  	- cg_atmFx.changeMinTime;
  	cg_atmFx.baseStartTime  	  	= cg_atmFx.gustEndTime  	  	+ cg_atmFx.changeMinTime  	+ (diff ? (rand() % diff) : 0);
}

static qboolean CG_EffectGustCurrent( vec3_t curr, float *weight, int *num )
{
  	// Calculate direction for new drops.

  	vec3_t temp;
  	float frac;

  	if( cg.time < cg_atmFx.baseEndTime )
  	{
  	  	VectorCopy( cg_atmFx.baseVec, curr );
  	  	*weight = cg_atmFx.baseWeight;
  	  	*num = cg_atmFx.baseDrops;
  	}
  	else {
  	  	VectorSubtract( cg_atmFx.gustVec, cg_atmFx.baseVec, temp );
  	  	if( cg.time < cg_atmFx.gustStartTime )
  	  	{
  	  	  	frac = ((float)(cg.time - cg_atmFx.baseEndTime))/((float)(cg_atmFx.gustStartTime - cg_atmFx.baseEndTime));
  	  	  	VectorMA( cg_atmFx.baseVec, frac, temp, curr );
  	  	  	*weight = cg_atmFx.baseWeight + (cg_atmFx.gustWeight - cg_atmFx.baseWeight) * frac;
  	  	  	*num = cg_atmFx.baseDrops + ((float)(cg_atmFx.gustDrops - cg_atmFx.baseDrops)) * frac;
  	  	}
  	  	else if( cg.time < cg_atmFx.gustEndTime )
  	  	{
  	  	  	VectorCopy( cg_atmFx.gustVec, curr );
  	  	  	*weight = cg_atmFx.gustWeight;
  	  	  	*num = cg_atmFx.gustDrops;
  	  	}
  	  	else
  	  	{
  	  	  	frac = 1.0 - ((float)(cg.time - cg_atmFx.gustEndTime))/((float)(cg_atmFx.baseStartTime - cg_atmFx.gustEndTime));
  	  	  	VectorMA( cg_atmFx.baseVec, frac, temp, curr );
  	  	  	*weight = cg_atmFx.baseWeight + (cg_atmFx.gustWeight - cg_atmFx.baseWeight) * frac;
  	  	  	*num = cg_atmFx.baseDrops + ((float)(cg_atmFx.gustDrops - cg_atmFx.baseDrops)) * frac;
  	  	  	if( cg.time >= cg_atmFx.baseStartTime )
  	  	  	  	return( qtrue );
  	  	}
  	}
  	return( qfalse );
}

static void CG_EP_ParseFloats( char *floatstr, float *f1, float *f2 )
{
  	// Parse the float or floats

  	char *middleptr;
  	char buff[64];

  	Q_strncpyz( buff, floatstr, sizeof(buff) );
  	for( middleptr = buff; *middleptr && *middleptr != ' '; middleptr++ );
  	if( *middleptr )
  	{
  	  	*middleptr++ = 0;
  	  	*f1 = atof( floatstr );
  	  	*f2 = atof( middleptr );
  	}
  	else {
  	  	*f1 = *f2 = atof( floatstr );
  	}
}
void CG_EffectParse( const char *effectstr )
{
  	// Split the string into it's component parts.

  	float bmin, bmax, cmin, cmax, gmin, gmax, bdrop, gdrop, wsplash, lsplash;
  	int count;
  	char *startptr, *eqptr, *endptr, *type;
  	char workbuff[128];

  	if( CG_AtmosphericKludge() )
  	  	return;

  	  	// Set up some default values
  	cg_atmFx.baseVec[0] = cg_atmFx.baseVec[1] = 0;
  	cg_atmFx.gustVec[0] = cg_atmFx.gustVec[1] = 100;
  	bmin = 5;
  	bmax = 10;
  	cmin = 1;
  	cmax = 1;
  	gmin = 0;
  	gmax = 2;
  	bdrop = gdrop = 300;
  	cg_atmFx.baseWeight = 0.7f;
  	cg_atmFx.gustWeight = 1.5f;
  	wsplash = 1;
  	lsplash = 1;
  	type = NULL;

  	  	// Parse the parameter string
  	Q_strncpyz( workbuff, effectstr, sizeof(workbuff) );
  	for( startptr = workbuff; *startptr; )
  	{
  	  	for( eqptr = startptr; *eqptr && *eqptr != '=' && *eqptr != ','; eqptr++ );
  	  	if( !*eqptr )
  	  	  	break;  	  	  	// No more string
  	  	if( *eqptr == ',' )
  	  	{
  	  	  	startptr = eqptr + 1;  	// Bad argument, continue
  	  	  	continue;
  	  	}
  	  	*eqptr++ = 0;
  	  	for( endptr = eqptr; *endptr && *endptr != ','; endptr++ );
  	  	if( *endptr )
  	  	  	*endptr++ = 0;

  	  	if( !type )
  	  	{
  	  	  	if( Q_stricmp( startptr, "T" ) ) {
  	  	  	  	cg_atmFx.numDrops = 0;
  	  	  	  	CG_Printf( "Atmospheric effect must start with a type.\n" );
  	  	  	  	return;
  	  	  	}
  	  	  	if( !Q_stricmp( eqptr, "RAIN" ) ) {
  	  	  	  	type = "rain";
  	  	  	  	cg_atmFx.ParticleCheckVisible = &CG_RainParticleCheckVisible;
  	  	  	  	cg_atmFx.ParticleGenerate = &CG_RainParticleGenerate;
  	  	  	  	cg_atmFx.ParticleRender = &CG_RainParticleRender;

  	  	  	  	cg_atmFx.baseVec[2] = cg_atmFx.gustVec[2] = - ATMOSPHERIC_RAIN_SPEED;
  	  	  	} else if( !Q_stricmp( eqptr, "SNOW" ) ) {
  	  	  	  	type = "snow";
  	  	  	  	cg_atmFx.ParticleCheckVisible = &CG_RainParticleCheckVisible;
  	  	  	  	cg_atmFx.ParticleGenerate = &CG_SnowParticleGenerate;
  	  	  	  	cg_atmFx.ParticleRender = &CG_SnowParticleRender;

  	  	  	  	cg_atmFx.baseVec[2] = cg_atmFx.gustVec[2] = - ATMOSPHERIC_SNOW_SPEED;
  	  	  	} else {
  	  	  	  	cg_atmFx.numDrops = 0;
  	  	  	  	CG_Printf( "Only effect type 'rain' and 'snow' are supported.\n" );
  	  	  	  	return;
  	  	  	}
  	  	}
  	  	else {
  	  	  	if( !Q_stricmp( startptr, "B" ) )
  	  	  	  	CG_EP_ParseFloats( eqptr, &bmin, &bmax );
  	  	  	else if( !Q_stricmp( startptr, "C" ) )
  	  	  	  	CG_EP_ParseFloats( eqptr, &cmin, &cmax );
  	  	  	else if( !Q_stricmp( startptr, "G" ) )
  	  	  	  	CG_EP_ParseFloats( eqptr, &gmin, &gmax );
  	  	  	else if( !Q_stricmp( startptr, "BV" ) )
  	  	  	  	CG_EP_ParseFloats( eqptr, &cg_atmFx.baseVec[0], &cg_atmFx.baseVec[1] );
  	  	  	else if( !Q_stricmp( startptr, "GV" ) )
  	  	  	  	CG_EP_ParseFloats( eqptr, &cg_atmFx.gustVec[0], &cg_atmFx.gustVec[1] );
  	  	  	else if( !Q_stricmp( startptr, "W" ) )
  	  	  	  	CG_EP_ParseFloats( eqptr, &cg_atmFx.baseWeight, &cg_atmFx.gustWeight );
  	  	  	else if( !Q_stricmp( startptr, "S" ) )
  	  	  	  	CG_EP_ParseFloats( eqptr, &wsplash, &lsplash );
  	  	  	else if( !Q_stricmp( startptr, "D" ) )
  	  	  	  	CG_EP_ParseFloats( eqptr, &bdrop, &gdrop );
  	  	  	else CG_Printf( "Unknown effect key '%s'.\n", startptr );
  	  	}
  	  	startptr = endptr;
  	}

  	if( !type )
  	{
  	  	// No effects

  	  	cg_atmFx.numDrops = -1;
  	  	return;
  	}
  	  	
  	cg_atmFx.baseMinTime = 1000 * bmin;
  	cg_atmFx.baseMaxTime = 1000 * bmax;
  	cg_atmFx.changeMinTime = 1000 * cmin;
  	cg_atmFx.changeMaxTime = 1000 * cmax;
  	cg_atmFx.gustMinTime = 1000 * gmin;
  	cg_atmFx.gustMaxTime = 1000 * gmax;
  	cg_atmFx.baseDrops = bdrop;
  	cg_atmFx.gustDrops = gdrop;
  	cg_atmFx.waterSplash = wsplash;
  	cg_atmFx.landSplash = lsplash;

  	cg_atmFx.numDrops = (cg_atmFx.baseDrops > cg_atmFx.gustDrops) ? cg_atmFx.baseDrops : cg_atmFx.gustDrops;
  	if( cg_atmFx.numDrops > MAX_ATMOSPHERIC_PARTICLES )
  	  	cg_atmFx.numDrops = MAX_ATMOSPHERIC_PARTICLES;

  	  	// Load graphics

  	// Rain
  	if( type == "rain" ) {
  	  	cg_atmFx.numEffectShaders = 1;
  	  	if( !(cg_atmFx.effectshaders[0] = trap_R_RegisterShader( "gfx/atmosphere/raindrop" )) )
  	  	  	cg_atmFx.effectshaders[0] = -1;
  	  	if( cg_atmFx.waterSplash )
  	  	  	cg_atmFx.effectwatershader = trap_R_RegisterShader( "gfx/atmosphere/raindropwater" );
  	  	if( cg_atmFx.landSplash )
  	  	  	cg_atmFx.effectlandshader = trap_R_RegisterShader( "gfx/atmosphere/raindropsolid" );

  	// Snow
  	} else if( type == "snow" ) {
  	  	for( cg_atmFx.numEffectShaders = 0; cg_atmFx.numEffectShaders < 6; cg_atmFx.numEffectShaders++ ) {
  	  	  	if( !( cg_atmFx.effectshaders[cg_atmFx.numEffectShaders] = trap_R_RegisterShader( va("gfx/atmosphere/snowflake0%i", cg_atmFx.numEffectShaders ) ) ) )
  	  	  	  	cg_atmFx.effectshaders[cg_atmFx.numEffectShaders] = -1;  	// we had some kind of a problem
  	  	}
  	  	cg_atmFx.waterSplash = 0;
  	  	cg_atmFx.landSplash = 0;

  	// This really should never happen
  	} else
  	  	cg_atmFx.numEffectShaders = 0;

  	  	// Initialise atmospheric effect to prevent all particles falling at the start
  	for( count = 0; count < cg_atmFx.numDrops; count++ )
  	  	cg_atmFx.particles[count].nextDropTime = ATMOSPHERIC_DROPDELAY + (rand() % ATMOSPHERIC_DROPDELAY);

  	CG_EffectGust();
}

/*
** Main render loop
*/

void CG_AddAtmosphericEffects()
{
  	// Add atmospheric effects (e.g. rain, snow etc.) to view

  	int curr, max, currnum;
  	cg_atmosphericParticle_t *particle;
  	vec3_t currvec;
  	float currweight;

  	if( cg_atmFx.numDrops <= 0 || cg_atmFx.numEffectShaders == 0 )
  	  	return;

  	max = cg_lowEffects.integer ? (cg_atmFx.numDrops >> 1) : cg_atmFx.numDrops;
  	if( CG_EffectGustCurrent( currvec, &currweight, &currnum ) )
  	  	CG_EffectGust();  	  	  	// Recalculate gust parameters
  	for( curr = 0; curr < max; curr++ )
  	{
  	  	particle = &cg_atmFx.particles[curr];
  	  	if( !cg_atmFx.ParticleCheckVisible( particle ) )
  	  	{
  	  	  	// Effect has terminated / fallen from screen view

  	  	  	if( !particle->nextDropTime )
  	  	  	{
  	  	  	  	// Stop rain being synchronized 
  	  	  	  	particle->nextDropTime = rand() % ATMOSPHERIC_DROPDELAY;
  	  	  	}
  	  	  	else if( currnum < curr || particle->nextDropTime > cg.time )
  	  	  	  	continue;
  	  	  	if( !cg_atmFx.ParticleGenerate( particle, currvec, currweight ) )
  	  	  	{
  	  	  	  	// Ensure it doesn't attempt to generate every frame, to prevent
  	  	  	  	// 'clumping' when there's only a small sky area available.
  	  	  	  	particle->nextDropTime = cg.time + ATMOSPHERIC_DROPDELAY;
  	  	  	  	continue;
  	  	  	}
  	  	}

  	  	cg_atmFx.ParticleRender( particle );
  	}

  	cg_atmFx.lastRainTime = cg.time;
}


/*
**  	G_AtmosphericKludge
*/

static qboolean kludgeChecked, kludgeResult;
qboolean CG_AtmosphericKludge()
{
  	// Activate effects for specified kludge maps that don't
  	// have it specified for them.

  	if( kludgeChecked )
  	  	return( kludgeResult );
  	kludgeChecked = qtrue;
  	kludgeResult = qfalse;

  	/*if( !Q_stricmp( cgs.mapname, "maps/2night3.bsp" ) )
  	{
  	  	CG_EffectParse( "T=RAIN" );
  	  	return( kludgeResult = qtrue );
  	}*/

  	return( kludgeResult = qfalse );
}

After that, add to cg_main.c:

vmCvar_t	cg_obeliskRespawnDelay;
#endif

vmCvar_t	cg_atmosphericEffects;
vmCvar_t	cg_lowEffects;

And a bit furtheron:

	{ &cg_bigFont, "ui_bigFont", "0.4", CVAR_ARCHIVE},
	{ &cg_atmosphericEffects, "cg_atmosphericEffects", "1", CVAR_ARCHIVE },
	{ &cg_lowEffects, "cg_lowEffects", "0", CVAR_ARCHIVE },

Next in cg_local.h add:

  	extern  vmCvar_t  	  	cg_recordSPDemoName;
  	extern  	vmCvar_t  	  	cg_obeliskRespawnDelay;
  	#endif
  	extern  	vmCvar_t  	  	cg_atmosphericEffects;
  	extern  	vmCvar_t  	  	cg_lowEffects;

And in the same file:

  	//
  	// cg_main.c
  	//
  	void CG_Respawn( void );
  	void CG_TransitionPlayerState( playerState_t *ps, playerState_t *ops );
  	void CG_CheckChangedPredictableEvents( playerState_t *ps );

  	//
  	// cg_atmospheric.c
  	//
  	void CG_EffectParse( const char *effectstr );
  	void CG_AddAtmosphericEffects();
  	qboolean CG_AtmosphericKludge();

The next file is cg_servercmds.c. Add the next bits of code:

  	}
  	#endif
   	cg.warmup = atoi( CG_ConfigString( CS_WARMUP ) );
  	if ( cg_atmosphericEffects.integer )
  	  	CG_EffectParse( CG_ConfigString( CS_ATMOSEFFECT ) );
  	} 	

And a bit furtheron:

   	}
   	else if ( num == CS_SHADERSTATE ) {
   	  	CG_ShaderStateChanged();
  	} else if( num == CS_ATMOSEFFECT ) {
  	  	CG_EffectParse( str );
   	}

}  	

Finally add to cg_view.c:

   	  	CG_AddPacketEntities();  	  	  	// alter calcViewValues, so predicted player state is correct
   	  	CG_AddMarks();
   	  	CG_AddLocalEntities();
  	  	CG_AddAtmosphericEffects();  	  	// Add rain/snow etc.
   	}
   	CG_AddViewWeapon( &cg.predictedPlayerState );

The rest of the code is in the game module. Starting with bg_public.h:

#define  	CS_ITEMS  	  	  	  	27  	  	// string of 0's and 1's that tell which items are present
 
#define  	CS_ATMOSEFFECT  	  	  	28  	  	// Atmospheric effect, if any.

#define  	CS_MODELS  	  	  	  	32
#define  	CS_SOUNDS  	  	  	  	(CS_MODELS+MAX_MODELS)
#define  	CS_PLAYERS  	  	  	  	(CS_SOUNDS+MAX_SOUNDS)

And finally in g_spawn.c:

  	G_SpawnString( "enableBreath", "0", &s );
  	trap_Cvar_Set( "g_enableBreath", s );
 
  	G_SpawnString( "atmosphere", "", &s );
  	trap_SetConfigstring( CS_ATMOSEFFECT, s );  	  	  	// Atmospheric effect

  	g_entities[ENTITYNUM_WORLD].s.number = ENTITYNUM_WORLD;
  	g_entities[ENTITYNUM_WORLD].classname = "worldspawn";

That's all. All you further need is the little media package available from http://www.q3f.com/. It contains the needed shaders and images.