Пример #1
0
/*
==============
CaptureRenderToFile

==============
*/
void idRenderSystemLocal::CaptureRenderToFile( const char *fileName, bool fixAlpha ) {
	if ( !glConfig.isInitialized ) {
		return;
	}

	renderCrop_t *rc = &renderCrops[currentRenderCrop];

	guiModel->EmitFullScreen();
	guiModel->Clear();
	R_IssueRenderCommands();

	qglReadBuffer( GL_BACK );

	// include extra space for OpenGL padding to word boundaries
	int	c = ( rc->width + 3 ) * rc->height;
	byte *data = (byte *)R_StaticAlloc( c * 3 );
	
	qglReadPixels( rc->x, rc->y, rc->width, rc->height, GL_RGB, GL_UNSIGNED_BYTE, data ); 

	byte *data2 = (byte *)R_StaticAlloc( c * 4 );

	for ( int i = 0 ; i < c ; i++ ) {
		data2[ i * 4 ] = data[ i * 3 ];
		data2[ i * 4 + 1 ] = data[ i * 3 + 1 ];
		data2[ i * 4 + 2 ] = data[ i * 3 + 2 ];
		data2[ i * 4 + 3 ] = 0xff;
	}

	R_WriteTGA( fileName, data2, rc->width, rc->height, true );

	R_StaticFree( data );
	R_StaticFree( data2 );
}
Пример #2
0
/*
=================
R_ClearedStaticAlloc
=================
*/
void *R_ClearedStaticAlloc( int bytes ) {
	void	*buf;

	buf = R_StaticAlloc( bytes );
	SIMDProcessor->Memset( buf, 0, bytes );
	return buf;
}
Пример #3
0
/*
=================
R_HeightmapToNormalMap

it is not possible to convert a heightmap into a normal map
properly without knowing the texture coordinate stretching.
We can assume constant and equal ST vectors for walls, but not for characters.
=================
*/
static void R_HeightmapToNormalMap( byte *data, int width, int height, float scale )
{
	int		i, j;
	byte	*depth;
	
	scale = scale / 256;
	
	// copy and convert to grey scale
	j = width * height;
	depth = ( byte * ) R_StaticAlloc( j );
	
	for( i = 0; i < j; i++ )
	{
		depth[i] = ( data[i * 4] + data[i * 4 + 1] + data[i * 4 + 2] ) / 3;
	}
	
	idVec3	dir, dir2;
	
	for( i = 0; i < height; i++ )
	{
		for( j = 0; j < width; j++ )
		{
			int		d1, d2, d3, d4;
			int		a1, a2, a3, a4;
			
			// FIXME: look at five points?
			// look at three points to estimate the gradient
			a1 = d1 = depth[( i * width + j )];
			a2 = d2 = depth[( i * width + ( ( j + 1 ) & ( width - 1 ) ) )];
			a3 = d3 = depth[( ( ( i + 1 ) & ( height - 1 ) ) * width + j )];
			a4 = d4 = depth[( ( ( i + 1 ) & ( height - 1 ) ) * width + ( ( j + 1 ) & ( width - 1 ) ) )];
			
			d2 -= d1;
			d3 -= d1;
			
			dir[0] = -d2 * scale;
			dir[1] = -d3 * scale;
			dir[2] = 1;
			dir.NormalizeFast();
			
			a1 -= a3;
			a4 -= a3;
			
			dir2[0] = -a4 * scale;
			dir2[1] = a1 * scale;
			dir2[2] = 1;
			dir2.NormalizeFast();
			
			dir += dir2;
			dir.NormalizeFast();
			
			a1 = ( i * width + j ) * 4;
			data[a1 + 0] = ( byte )( dir[0] * 127 + 128 );
			data[a1 + 1] = ( byte )( dir[1] * 127 + 128 );
			data[a1 + 2] = ( byte )( dir[2] * 127 + 128 );
			data[a1 + 3] = 255;
		}
	}
	R_StaticFree( depth );
}
Пример #4
0
/*
================
R_CalcInteractionFacing

Determines which triangles of the surface are facing towards the light origin.

The facing array should be allocated with one extra index than
the number of surface triangles, which will be used to handle dangling
edge silhouettes.
================
*/
void R_CalcInteractionFacing( const idRenderEntityLocal *ent, const srfTriangles_t *tri, const idRenderLightLocal *light, srfCullInfo_t &cullInfo ) {
	SCOPED_PROFILE_EVENT( "R_CalcInteractionFacing" );

	if ( cullInfo.facing != NULL ) {
		return;
	}

	idVec3 localLightOrigin;
	R_GlobalPointToLocal( ent->modelMatrix, light->globalLightOrigin, localLightOrigin );

	const int numFaces = tri->numIndexes / 3;
	cullInfo.facing = (byte *) R_StaticAlloc( ( numFaces + 1 ) * sizeof( cullInfo.facing[0] ), TAG_RENDER_INTERACTION );

	// exact geometric cull against face
	for ( int i = 0, face = 0; i < tri->numIndexes; i += 3, face++ ) {
		const idDrawVert & v0 = tri->verts[tri->indexes[i + 0]];
		const idDrawVert & v1 = tri->verts[tri->indexes[i + 1]];
		const idDrawVert & v2 = tri->verts[tri->indexes[i + 2]];

		const idPlane plane( v0.xyz, v1.xyz, v2.xyz );
		const float d = plane.Distance( localLightOrigin );

		cullInfo.facing[face] = ( d >= 0.0f );
	}
	cullInfo.facing[numFaces] = 1;	// for dangling edges to reference
}
Пример #5
0
/*
================
R_CalcInteractionFacing

Determines which triangles of the surface are facing towards the light origin.

The facing array should be allocated with one extra index than
the number of surface triangles, which will be used to handle dangling
edge silhouettes.
================
*/
void R_CalcInteractionFacing( const idRenderEntityLocal *ent, const srfTriangles_t *tri, const idRenderLightLocal *light, srfCullInfo_t &cullInfo ) {
	idVec3 localLightOrigin;

	if ( cullInfo.facing != NULL ) {
		return;
	}

	R_GlobalPointToLocal( ent->modelMatrix, light->globalLightOrigin, localLightOrigin );

	int numFaces = tri->numIndexes / 3;

	if ( !tri->facePlanes || !tri->facePlanesCalculated ) {
		R_DeriveFacePlanes( const_cast<srfTriangles_t *>(tri) );
	}

	cullInfo.facing = (byte *) R_StaticAlloc( ( numFaces + 1 ) * sizeof( cullInfo.facing[0] ) );

	// calculate back face culling
	float *planeSide = (float *) _alloca16( numFaces * sizeof( float ) );

	// exact geometric cull against face
	SIMDProcessor->Dot( planeSide, localLightOrigin, tri->facePlanes, numFaces );
	SIMDProcessor->CmpGE( cullInfo.facing, planeSide, 0.0f, numFaces );

	cullInfo.facing[ numFaces ] = 1;	// for dangling edges to reference
}
Пример #6
0
/*
====================
GenerateMegaPreview

Make a 2k x 2k preview image for a mega texture that can be used in modeling programs
====================
*/
void	idMegaTexture::GenerateMegaPreview( const char *fileName ) {
	idFile	*fileHandle = fileSystem->OpenFileRead( fileName );
	if ( !fileHandle ) {
		common->Printf( "idMegaTexture: failed to open %s\n", fileName );
		return;
	}

	idStr	outName = fileName;
	outName.StripFileExtension();
	outName += "_preview.tga";

	common->Printf( "Creating %s.\n", outName.c_str() );

	megaTextureHeader_t header;

	fileHandle->Read( &header, sizeof( header ) );
	if ( header.tileSize < 64 || header.tilesWide < 1 || header.tilesHigh < 1 ) {
		common->Printf( "idMegaTexture: bad header on %s\n", fileName );
		return;
	}

	int	tileSize = header.tileSize;
	int	width = header.tilesWide;
	int	height = header.tilesHigh;
	int	tileOffset = 1;
	int	tileBytes = tileSize * tileSize * 4;
	// find the level that fits
	while ( width * tileSize > 2048 || height * tileSize > 2048 ) {
		tileOffset += width * height;
		width >>= 1;
		if ( width < 1 ) {
			width = 1;
		}
		height >>= 1;
		if ( height < 1 ) {
			height = 1;
		}
	}

	byte *pic = (byte *)R_StaticAlloc( width * height * tileBytes );
	byte	*oldBlock = (byte *)_alloca( tileBytes );
	for ( int y = 0 ; y < height ; y++ ) {
		for ( int x = 0 ; x < width ; x++ ) {
			int tileNum = tileOffset + y * width + x;
			fileHandle->Seek( tileNum * tileBytes, FS_SEEK_SET );
			fileHandle->Read( oldBlock, tileBytes );

			for ( int yy = 0 ; yy < tileSize ; yy++ ) {
				memcpy( pic + ( ( y * tileSize + yy ) * width * tileSize + x * tileSize  ) * 4,
					oldBlock + yy * tileSize * 4, tileSize * 4 );
			}
		}
	}

	R_WriteTGA( outName.c_str(), pic, width * tileSize, height * tileSize, false );

	R_StaticFree( pic );

	delete fileHandle;
}
Пример #7
0
/*
================
R_SmoothNormalMap
================
*/
static void R_SmoothNormalMap( byte* data, int width, int height )
{
	byte*	orig;
	int		i, j, k, l;
	idVec3	normal;
	byte*	out;
	static float	factors[3][3] =
	{
		{ 1, 1, 1 },
		{ 1, 1, 1 },
		{ 1, 1, 1 }
	};
	
	orig = ( byte* )R_StaticAlloc( width * height * 4, TAG_IMAGE );
	memcpy( orig, data, width * height * 4 );
	
	for( i = 0 ; i < width ; i++ )
	{
		for( j = 0 ; j < height ; j++ )
		{
			normal = vec3_origin;
			for( k = -1 ; k < 2 ; k++ )
			{
				for( l = -1 ; l < 2 ; l++ )
				{
					byte*	in;
					
					in = orig + ( ( ( j + l ) & ( height - 1 ) ) * width + ( ( i + k ) & ( width - 1 ) ) ) * 4;
					
					// ignore 000 and -1 -1 -1
					if( in[0] == 0 && in[1] == 0 && in[2] == 0 )
					{
						continue;
					}
					if( in[0] == 128 && in[1] == 128 && in[2] == 128 )
					{
						continue;
					}
					
					normal[0] += factors[k + 1][l + 1] * ( in[0] - 128 );
					normal[1] += factors[k + 1][l + 1] * ( in[1] - 128 );
					normal[2] += factors[k + 1][l + 1] * ( in[2] - 128 );
				}
			}
			normal.Normalize();
			out = data + ( j * width + i ) * 4;
			out[0] = ( byte )( 128 + 127 * normal[0] );
			out[1] = ( byte )( 128 + 127 * normal[1] );
			out[2] = ( byte )( 128 + 127 * normal[2] );
		}
	}
	
	R_StaticFree( orig );
}
Пример #8
0
/*
=====================
R_CalcInteractionCullBits

We want to cull a little on the sloppy side, because the pre-clipping
of geometry to the lights in dmap will give many cases that are right
at the border. We throw things out on the border, because if any one
vertex is clearly inside, the entire triangle will be accepted.
=====================
*/
void R_CalcInteractionCullBits( const idRenderEntityLocal* ent, const srfTriangles_t* tri, const idRenderLightLocal* light, srfCullInfo_t& cullInfo )
{
	SCOPED_PROFILE_EVENT( "R_CalcInteractionCullBits" );
	
	if( cullInfo.cullBits != NULL )
	{
		return;
	}
	
	idPlane frustumPlanes[6];
	idRenderMatrix::GetFrustumPlanes( frustumPlanes, light->baseLightProject, true, true );
	
	int frontBits = 0;
	
	// cull the triangle surface bounding box
	for( int i = 0; i < 6; i++ )
	{
		R_GlobalPlaneToLocal( ent->modelMatrix, frustumPlanes[i], cullInfo.localClipPlanes[i] );
		
		// get front bits for the whole surface
		if( tri->bounds.PlaneDistance( cullInfo.localClipPlanes[i] ) >= LIGHT_CLIP_EPSILON )
		{
			frontBits |= 1 << i;
		}
	}
	
	// if the surface is completely inside the light frustum
	if( frontBits == ( ( 1 << 6 ) - 1 ) )
	{
		cullInfo.cullBits = LIGHT_CULL_ALL_FRONT;
		return;
	}
	
	cullInfo.cullBits = ( byte* ) R_StaticAlloc( tri->numVerts * sizeof( cullInfo.cullBits[0] ), TAG_RENDER_INTERACTION );
	memset( cullInfo.cullBits, 0, tri->numVerts * sizeof( cullInfo.cullBits[0] ) );
	
	for( int i = 0; i < 6; i++ )
	{
		// if completely infront of this clipping plane
		if( frontBits & ( 1 << i ) )
		{
			continue;
		}
		for( int j = 0; j < tri->numVerts; j++ )
		{
			float d = cullInfo.localClipPlanes[i].Distance( tri->verts[j].xyz );
			cullInfo.cullBits[j] |= ( d < LIGHT_CLIP_EPSILON ) << i;
		}
	}
}
Пример #9
0
/*
==============
idRenderSystemLocal::CaptureRenderToFile
==============
*/
void idRenderSystemLocal::CaptureRenderToFile( const char *fileName, bool fixAlpha ) {
	if ( !R_IsInitialized() ) {
		return;
	}

	idScreenRect & rc = renderCrops[currentRenderCrop];

	guiModel->EmitFullScreen();
	guiModel->Clear();
	RenderCommandBuffers( frameData->cmdHead );


	if ( !vr->useFBO ) // koz
	{
		glReadBuffer( GL_BACK );
	}

	// include extra space for OpenGL padding to word boundaries
	int	c = ( rc.GetWidth() + 3 ) * rc.GetHeight();
	byte *data = (byte *)R_StaticAlloc( c * 3 );
	
	qglReadPixels( rc.x1, rc.y1, rc.GetWidth(), rc.GetHeight(), GL_RGB, GL_UNSIGNED_BYTE, data ); 

	byte *data2 = (byte *)R_StaticAlloc( c * 4 );

	for ( int i = 0 ; i < c ; i++ ) {
		data2[ i * 4 ] = data[ i * 3 ];
		data2[ i * 4 + 1 ] = data[ i * 3 + 1 ];
		data2[ i * 4 + 2 ] = data[ i * 3 + 2 ];
		data2[ i * 4 + 3 ] = 0xff;
	}

	R_WriteTGA( fileName, data2, rc.GetWidth(), rc.GetHeight(), true );

	R_StaticFree( data );
	R_StaticFree( data2 );
}
Пример #10
0
/*
===================
RB_ShowIntensity

Debugging tool to see how much dynamic range a scene is using.
The greatest of the rgb values at each pixel will be used, with
the resulting color shading from red at 0 to green at 128 to blue at 255
===================
*/
static void RB_ShowIntensity() {
	byte	*colorReadback;
	int		i, j, c;

	if ( !r_showIntensity.GetBool() ) {
		return;
	}

	colorReadback = (byte *)R_StaticAlloc( renderSystem->GetWidth() * renderSystem->GetHeight() * 4, TAG_RENDER_TOOLS );
	qglReadPixels( 0, 0, renderSystem->GetWidth(), renderSystem->GetHeight(), GL_RGBA, GL_UNSIGNED_BYTE, colorReadback );

	c = renderSystem->GetWidth() * renderSystem->GetHeight() * 4;
	for ( i = 0; i < c; i+=4 ) {
		j = colorReadback[i];
		if ( colorReadback[i+1] > j ) {
			j = colorReadback[i+1];
		}
		if ( colorReadback[i+2] > j ) {
			j = colorReadback[i+2];
		}
		if ( j < 128 ) {
			colorReadback[i+0] = 2*(128-j);
			colorReadback[i+1] = 2*j;
			colorReadback[i+2] = 0;
		} else {
			colorReadback[i+0] = 0;
			colorReadback[i+1] = 2*(255-j);
			colorReadback[i+2] = 2*(j-128);
		}
	}

	// draw it back to the screen
	qglLoadIdentity();
	qglMatrixMode( GL_PROJECTION );
	GL_State( GLS_DEPTHFUNC_ALWAYS );
	qglPushMatrix();
	qglLoadIdentity(); 
    qglOrtho( 0, 1, 0, 1, -1, 1 );
	qglRasterPos2f( 0, 0 );
	qglPopMatrix();
	GL_Color( 1, 1, 1 );
	globalImages->BindNull();
	qglMatrixMode( GL_MODELVIEW );

	qglDrawPixels( renderSystem->GetWidth(), renderSystem->GetHeight(), GL_RGBA , GL_UNSIGNED_BYTE, colorReadback );

	R_StaticFree( colorReadback );
}
Пример #11
0
/*
===================
RB_CountStencilBuffer

Print an overdraw count based on stencil index values
===================
*/
static void RB_CountStencilBuffer() {
	int		count;
	int		i;
	byte	*stencilReadback;


	stencilReadback = (byte *)R_StaticAlloc( renderSystem->GetWidth() * renderSystem->GetHeight(), TAG_RENDER_TOOLS );
	qglReadPixels( 0, 0, renderSystem->GetWidth(), renderSystem->GetHeight(), GL_STENCIL_INDEX, GL_UNSIGNED_BYTE, stencilReadback );

	count = 0;
	for ( i = 0; i < renderSystem->GetWidth() * renderSystem->GetHeight(); i++ ) {
		count += stencilReadback[i];
	}

	R_StaticFree( stencilReadback );

	// print some stats (not supposed to do from back end in SMP...)
	common->Printf( "overdraw: %5.1f\n", (float)count/(renderSystem->GetWidth() * renderSystem->GetHeight())  );
}
Пример #12
0
/*
=====================
R_CalcInteractionCullBits

We want to cull a little on the sloppy side, because the pre-clipping
of geometry to the lights in dmap will give many cases that are right
at the border we throw things out on the border, because if any one
vertex is clearly inside, the entire triangle will be accepted.
=====================
*/
void R_CalcInteractionCullBits( const idRenderEntityLocal *ent, const srfTriangles_t *tri, const idRenderLightLocal *light, srfCullInfo_t &cullInfo ) {
	int i, frontBits;

	if ( cullInfo.cullBits != NULL ) {
		return;
	}

	frontBits = 0;

	// cull the triangle surface bounding box
	for ( i = 0; i < 6; i++ ) {

		R_GlobalPlaneToLocal( ent->modelMatrix, -light->frustum[i], cullInfo.localClipPlanes[i] );

		// get front bits for the whole surface
		if ( tri->bounds.PlaneDistance( cullInfo.localClipPlanes[i] ) >= LIGHT_CLIP_EPSILON ) {
			frontBits |= 1<<i;
		}
	}

	// if the surface is completely inside the light frustum
	if ( frontBits == ( ( 1 << 6 ) - 1 ) ) {
		cullInfo.cullBits = LIGHT_CULL_ALL_FRONT;
		return;
	}

	cullInfo.cullBits = (byte *) R_StaticAlloc( tri->numVerts * sizeof( cullInfo.cullBits[0] ) );
	SIMDProcessor->Memset( cullInfo.cullBits, 0, tri->numVerts * sizeof( cullInfo.cullBits[0] ) );

	float *planeSide = (float *) _alloca16( tri->numVerts * sizeof( float ) );

	for ( i = 0; i < 6; i++ ) {
		// if completely infront of this clipping plane
		if ( frontBits & ( 1 << i ) ) {
			continue;
		}
		SIMDProcessor->Dot( planeSide, cullInfo.localClipPlanes[i], tri->verts, tri->numVerts );
		SIMDProcessor->CmpLT( cullInfo.cullBits, i, planeSide, LIGHT_CLIP_EPSILON, tri->numVerts );
	}
}
Пример #13
0
/*
===================
RB_ShowDepthBuffer

Draw the depth buffer as colors
===================
*/
static void RB_ShowDepthBuffer() {
	void	*depthReadback;

	if ( !r_showDepth.GetBool() ) {
		return;
	}

	qglPushMatrix();
	qglLoadIdentity();
	qglMatrixMode( GL_PROJECTION );
	qglPushMatrix();
	qglLoadIdentity(); 
    qglOrtho( 0, 1, 0, 1, -1, 1 );
	qglRasterPos2f( 0, 0 );
	qglPopMatrix();
	qglMatrixMode( GL_MODELVIEW );
	qglPopMatrix();

	GL_State( GLS_DEPTHFUNC_ALWAYS );
	GL_Color( 1, 1, 1 );
	globalImages->BindNull();

	depthReadback = R_StaticAlloc( renderSystem->GetWidth() * renderSystem->GetHeight()*4, TAG_RENDER_TOOLS );
	memset( depthReadback, 0, renderSystem->GetWidth() * renderSystem->GetHeight()*4 );

	qglReadPixels( 0, 0, renderSystem->GetWidth(), renderSystem->GetHeight(), GL_DEPTH_COMPONENT , GL_FLOAT, depthReadback );

#if 0
	for ( i = 0; i < renderSystem->GetWidth() * renderSystem->GetHeight(); i++ ) {
		((byte *)depthReadback)[i*4] = 
		((byte *)depthReadback)[i*4+1] = 
		((byte *)depthReadback)[i*4+2] = 255 * ((float *)depthReadback)[i];
		((byte *)depthReadback)[i*4+3] = 1;
	}
#endif

	qglDrawPixels( renderSystem->GetWidth(), renderSystem->GetHeight(), GL_RGBA , GL_UNSIGNED_BYTE, depthReadback );
	R_StaticFree( depthReadback );
}
Пример #14
0
/*
===================
RB_ScanStencilBuffer

Debugging tool to see what values are in the stencil buffer
===================
*/
void RB_ScanStencilBuffer() {
	int		counts[256];
	int		i;
	byte	*stencilReadback;

	memset( counts, 0, sizeof( counts ) );

	stencilReadback = (byte *)R_StaticAlloc( renderSystem->GetWidth() * renderSystem->GetHeight(), TAG_RENDER_TOOLS );
	qglReadPixels( 0, 0, renderSystem->GetWidth(), renderSystem->GetHeight(), GL_STENCIL_INDEX, GL_UNSIGNED_BYTE, stencilReadback );

	for ( i = 0; i < renderSystem->GetWidth() * renderSystem->GetHeight(); i++ ) {
		counts[ stencilReadback[i] ]++;
	}

	R_StaticFree( stencilReadback );

	// print some stats (not supposed to do from back end in SMP...)
	common->Printf( "stencil values:\n" );
	for ( i = 0; i < 255; i++ ) {
		if ( counts[i] ) {
			common->Printf( "%i: %i\n", i, counts[i] );
		}
	}
}
Пример #15
0
/*
=================
R_ClearedStaticAlloc
=================
*/
void *R_ClearedStaticAlloc( int bytes ) {
	void * buf = R_StaticAlloc( bytes );
	memset( buf, 0, bytes );
	return buf;
}
Пример #16
0
void fhBaseRenderList::Init() {
	assert(!renderlistMemory);
	renderlistMemory = R_StaticAlloc(renderlistMaxSize);
}
Пример #17
0
/*
==============
RenderBump_f

==============
*/
void RenderBump_f( const idCmdArgs &args ) {
	idRenderModel	*lowPoly;
	idStr	source;
	int		i, j;
	const char	*cmdLine;
	int		numRenderBumps;
	renderBump_t	*renderBumps, *rb = NULL;
	renderBump_t	opt;
	int		startTime, endTime;

	// update the screen as we print
	common->SetRefreshOnPrint( true );

	// there should be a single parameter, the filename for a game loadable low-poly model
	if ( args.Argc() != 2 ) {
		common->Error( "Usage: renderbump <lowPolyModel>" );
	}

	common->Printf( "----- Renderbump %s -----\n", args.Argv( 1 ) );

	startTime = Sys_Milliseconds();

	// get the lowPoly model
	source = args.Argv( 1 );
	lowPoly = renderModelManager->CheckModel( source );
	if ( !lowPoly ) {
		common->Error( "Can't load model %s", source.c_str() );
	}

	renderBumps = (renderBump_t *)R_StaticAlloc( lowPoly->NumSurfaces() * sizeof( *renderBumps ) );
	numRenderBumps = 0;
	for ( i = 0 ; i < lowPoly->NumSurfaces() ; i++ ) {
		const modelSurface_t	*ms = lowPoly->Surface( i );

		// default options
		memset( &opt, 0, sizeof( opt ) );
		opt.width = 512;
		opt.height = 512;
		opt.antiAlias = 1;
		opt.outline = 8;
		opt.traceFrac = 0.05f;

		// parse the renderbump parameters for this surface
		cmdLine = ms->shader->GetRenderBump();

		common->Printf( "surface %i, shader %s\nrenderBump = %s ", i,
			ms->shader->GetName(), cmdLine );

		if ( !ms->geometry ) {
			common->Printf( "(no geometry)\n" );
			continue;
		}

		idCmdArgs localArgs;
		localArgs.TokenizeString( cmdLine, false );

		if ( localArgs.Argc() < 2 ) {
			common->Printf( "(no action)\n" );
			continue;
		}

		common->Printf( "(rendering)\n" );

		for ( j = 0 ; j < localArgs.Argc() - 2; j++ ) {
			const char *s;

			s = localArgs.Argv( j );
			if ( s[0] == '-' ) {
				j++;
				s = localArgs.Argv( j );
				if ( s[0] == '\0' ) {
					continue;
				}
			}

			if ( !idStr::Icmp( s, "size" ) ) {
				if ( j + 2 >= localArgs.Argc() ) {
					j = localArgs.Argc();
					break;
				}
				opt.width = atoi( localArgs.Argv( j + 1 ) );
				opt.height = atoi( localArgs.Argv( j + 2 ) );
				j += 2;
			} else if ( !idStr::Icmp( s, "trace" ) ) {
				opt.traceFrac = atof( localArgs.Argv( j + 1 ) );
				j += 1;
			} else if ( !idStr::Icmp( s, "globalMap" ) ) {
				opt.saveGlobalMap = true;
			} else if ( !idStr::Icmp( s, "colorMap" ) ) {
				opt.saveColorMap = true;
			} else if ( !idStr::Icmp( s, "outline" ) ) {
				opt.outline = atoi( localArgs.Argv( j + 1 ) );
				j += 1;
			} else if ( !idStr::Icmp( s, "aa" ) ) {
				opt.antiAlias = atoi( localArgs.Argv( j + 1 ) );
				j += 1;
			} else {
				common->Printf( "WARNING: Unknown option \"%s\"\n", s );
				break;
			}
		}

		if ( j != ( localArgs.Argc() - 2 ) ) {
			common->Error( "usage: renderBump [-size width height] [-aa <1-2>] [globalMap] [colorMap] [-trace <0.01 - 1.0>] normalMapImageFile highPolyAseFile" );
		}
		idStr::Copynz( opt.outputName, localArgs.Argv( j ), sizeof( opt.outputName ) );
		idStr::Copynz( opt.highName, localArgs.Argv( j + 1 ), sizeof( opt.highName ) );

		// adjust size for anti-aliasing
		opt.width <<= opt.antiAlias;
		opt.height <<= opt.antiAlias;

		// see if we already have a renderbump going for another surface that this should use
		for ( j = 0 ; j < numRenderBumps ; j++ ) {
			rb = &renderBumps[j];

			if ( idStr::Icmp( rb->outputName, opt.outputName ) ) {
				continue;
			}
			// all the other parameters must match, or it is an error
			if ( idStr::Icmp( rb->highName, opt.highName) || rb->width != opt.width ||
				rb->height != opt.height || rb->antiAlias != opt.antiAlias ||
				rb->traceFrac != opt.traceFrac ) {
				common->Error( "mismatched renderbump parameters on image %s", rb->outputName );
				continue;
			}

			// saveGlobalMap will be a sticky option
			rb->saveGlobalMap = rb->saveGlobalMap | opt.saveGlobalMap;
			break;
		}

		// create a new renderbump if needed
		if ( j == numRenderBumps ) {
			numRenderBumps++;
			rb = &renderBumps[j];
			*rb = opt;

			InitRenderBump( rb );
		}

		// render the triangles for this surface
		RenderBumpTriangles( ms->geometry, rb );
	}

	//
	// anti-alias and write out all renderbumps that we have completed
	//
	for ( i = 0 ; i < numRenderBumps ; i++ ) {
		WriteRenderBump( &renderBumps[i], opt.outline << opt.antiAlias );
	}

	R_StaticFree( renderBumps );

	endTime = Sys_Milliseconds();
	common->Printf( "%5.2f seconds for renderBump\n", ( endTime - startTime ) / 1000.0 );
	common->Printf( "---------- RenderBump Completed ----------\n" );

	// stop updating the screen as we print
	common->SetRefreshOnPrint( false );
}