/* -------------------------------------------------------------------------------

   Copyright (C) 1999-2007 id Software, Inc. and contributors.
   For a list of contributors, see the accompanying CONTRIBUTORS file.

   This file is part of GtkRadiant.

   GtkRadiant is free software; you can redistribute it and/or modify
   it under the terms of the GNU General Public License as published by
   the Free Software Foundation; either version 2 of the License, or
   (at your option) any later version.

   GtkRadiant is distributed in the hope that it will be useful,
   but WITHOUT ANY WARRANTY; without even the implied warranty of
   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
   GNU General Public License for more details.

   You should have received a copy of the GNU General Public License
   along with GtkRadiant; if not, write to the Free Software
   Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA

   ----------------------------------------------------------------------------------

   This code has been altered significantly from its original form, to support
   several games based on the Quake III Arena engine, in the form of "Q3Map2."

   ------------------------------------------------------------------------------- */



/* dependencies */
#include "q3map2.h"
#include "bspfile_rbsp.h"
#include "surface_extra.h"
#include "timer.h"




/* -------------------------------------------------------------------------------

   this file contains code that doe lightmap allocation and projection that
   runs in the -light phase.

   this is handled here rather than in the bsp phase for a few reasons--
   surfaces are no longer necessarily convex polygons, patches may or may not be
   planar or have lightmaps projected directly onto control points.

   also, this allows lightmaps to be calculated before being allocated and stored
   in the bsp. lightmaps that have little high-frequency information are candidates
   for having their resolutions scaled down.

   ------------------------------------------------------------------------------- */

/*
   WriteTGA24()
   based on WriteTGA() from qimagelib.cpp
 */

static void WriteTGA24( char *filename, byte *data, int width, int height, bool flip ){
	int i;
	const int headSz = 18;
	const int sz = width * height * 3 + headSz;
	byte    *buffer, *pix, *in;
	FILE    *file;

	/* allocate a buffer and set it up */
	buffer = safe_malloc( sz );
	pix = buffer + headSz;
	memset( buffer, 0, headSz );
	buffer[ 2 ] = 2;		// uncompressed type
	buffer[ 12 ] = width & 255;
	buffer[ 13 ] = width >> 8;
	buffer[ 14 ] = height & 255;
	buffer[ 15 ] = height >> 8;
	buffer[ 16 ] = 24;	// pixel size

	/* swap rgb to bgr */
	for ( i = 0; i < sz - headSz; i += 3 )
	{
		pix[ i + 0 ] = data[ i + 2 ];   /* blue */
		pix[ i + 1 ] = data[ i + 1 ];   /* green */
		pix[ i + 2 ] = data[ i + 0 ];   /* red */
	}

	/* write it and free the buffer */
	file = SafeOpenWrite( filename );

	/* flip vertically? */
	if ( flip ) {
		fwrite( buffer, 1, headSz, file );
		for ( in = pix + ( ( height - 1 ) * width * 3 ); in >= pix; in -= ( width * 3 ) )
			fwrite( in, 1, ( width * 3 ), file );
	}
	else{
		fwrite( buffer, 1, sz, file );
	}

	/* close the file */
	fclose( file );
	free( buffer );
}



/*
   ExportLightmaps()
   exports the lightmaps as a list of numbered tga images
 */

void ExportLightmaps(){
	int i;
	char dirname[ 1024 ], filename[ 1024 ];
	byte        *lightmap;


	/* note it */
	Sys_FPrintf( SYS_VRB, "--- ExportLightmaps ---\n" );

	/* do some path mangling */
	strcpy( dirname, source );
	StripExtension( dirname );

	/* sanity check */
	if ( bspLightBytes.empty() ) {
		Sys_Warning( "No BSP lightmap data\n" );
		return;
	}

	/* make a directory for the lightmaps */
	Q_mkdir( dirname );

	/* iterate through the lightmaps */
	for ( i = 0, lightmap = bspLightBytes.data(); lightmap < ( bspLightBytes.data() + bspLightBytes.size() ); ++i, lightmap += ( g_game->lightmapSize * g_game->lightmapSize * 3 ) )
	{
		/* write a tga image out */
		sprintf( filename, "%s/lightmap_%04d.tga", dirname, i );
		Sys_Printf( "Writing %s\n", filename );
		WriteTGA24( filename, lightmap, g_game->lightmapSize, g_game->lightmapSize, false );
	}
}



/*
   ExportLightmapsMain()
   exports the lightmaps as a list of numbered tga images
 */

int ExportLightmapsMain( Args& args ){
	/* arg checking */
	if ( args.empty() ) {
		Sys_Printf( "Usage: q3map2 -export [-v] <mapname>\n" );
		return 0;
	}

	/* do some path mangling */
	strcpy( source, ExpandArg( args.takeBack() ) );
	path_set_extension( source, ".bsp" );

	/* load the bsp */
	Sys_Printf( "Loading %s\n", source );
	LoadBSPFile( source );

	/* export the lightmaps */
	ExportLightmaps();

	/* return to sender */
	return 0;
}



/*
   ImportLightmapsMain()
   imports the lightmaps from a list of numbered tga images
 */

int ImportLightmapsMain( Args& args ){
	int i, x, y, width, height;
	char dirname[ 1024 ], filename[ 1024 ];
	byte        *lightmap, *pixels, *in, *out;


	/* arg checking */
	if ( args.empty() ) {
		Sys_Printf( "Usage: q3map2 -import [-v] <mapname>\n" );
		return 0;
	}

	/* do some path mangling */
	strcpy( source, ExpandArg( args.takeBack() ) );
	path_set_extension( source, ".bsp" );

	/* load the bsp */
	Sys_Printf( "Loading %s\n", source );
	LoadBSPFile( source );

	/* note it */
	Sys_FPrintf( SYS_VRB, "--- ImportLightmaps ---\n" );

	/* do some path mangling */
	strcpy( dirname, source );
	StripExtension( dirname );

	/* sanity check */
	if ( bspLightBytes.empty() ) {
		Error( "No lightmap data" );
	}

	/* make a directory for the lightmaps */
	Q_mkdir( dirname );

	/* iterate through the lightmaps */
	for ( i = 0, lightmap = bspLightBytes.data(); lightmap < ( bspLightBytes.data() + bspLightBytes.size() ); ++i, lightmap += ( g_game->lightmapSize * g_game->lightmapSize * 3 ) )
	{
		/* read a tga image */
		sprintf( filename, "%s/lightmap_%04d.tga", dirname, i );
		Sys_Printf( "Loading %s\n", filename );
		MemBuffer buffer = vfsLoadFile( filename, -1 );
		if ( !buffer ) {
			Sys_Warning( "Unable to load image %s\n", filename );
			continue;
		}

		/* parse file into an image */
		pixels = nullptr;
		LoadTGABuffer( buffer.data(), buffer.size(), &pixels, &width, &height );

		/* sanity check it */
		if ( pixels == nullptr ) {
			Sys_Warning( "Unable to load image %s\n", filename );
			continue;
		}
		if ( width != g_game->lightmapSize || height != g_game->lightmapSize ) {
			Sys_Warning( "Image %s is not the right size (%d, %d) != (%d, %d)\n",
			             filename, width, height, g_game->lightmapSize, g_game->lightmapSize );
		}

		/* copy the pixels */
		in = pixels;
		for ( y = 1; y <= g_game->lightmapSize; ++y )
		{
			out = lightmap + ( ( g_game->lightmapSize - y ) * g_game->lightmapSize * 3 );
			for ( x = 0; x < g_game->lightmapSize; ++x, in += 4, out += 3 )
				std::copy_n( in, 3, out );
		}

		/* free the image */
		free( pixels );
	}

	/* write the bsp */
	WriteBSPFile( source );

	/* return to sender */
	return 0;
}



/* -------------------------------------------------------------------------------

   this section deals with projecting a lightmap onto a raw drawsurface

   ------------------------------------------------------------------------------- */

namespace
{
int numSurfsVertexLit;
int numSurfsVertexForced;
int numSurfsVertexApproximated;
int numSurfsLightmapped;
int numPlanarsLightmapped;
int numNonPlanarsLightmapped;
int numPatchesLightmapped;
int numPlanarPatchesLightmapped;
}

/*
   FinishRawLightmap()
   allocates a raw lightmap's necessary buffers
 */

static void FinishRawLightmap( rawLightmap_t& lm ){
	int i, j, c, size, *sc;
	float is;
	surfaceInfo_t       *info;


	/* sort light surfaces by shader name */
	std::ranges::sort( Span( &lightSurfaces[ lm.firstLightSurface ], lm.numLightSurfaces ), []( const int a, const int b ){
		/* get shaders */
		const shaderInfo_t *asi = surfaceInfos[ a ].si;
		const shaderInfo_t *bsi = surfaceInfos[ b ].si;

		/* dummy check */
		if ( asi == nullptr ) {
			return true;
		}
		if ( bsi == nullptr ) {
			return false;
		}

		/* compare shader names */
		return strcmp( asi->shader, bsi->shader ) < 0;
	} );

	/* count clusters */
	lm.numLightClusters = 0;
	for ( i = 0; i < lm.numLightSurfaces; ++i )
	{
		/* get surface info */
		info = &surfaceInfos[ lightSurfaces[ lm.firstLightSurface + i ] ];

		/* add surface clusters */
		lm.numLightClusters += info->numSurfaceClusters;
	}

	/* allocate buffer for clusters and copy */
	lm.lightClusters = safe_malloc( lm.numLightClusters * sizeof( *lm.lightClusters ) );
	c = 0;
	for ( i = 0; i < lm.numLightSurfaces; ++i )
	{
		/* get surface info */
		info = &surfaceInfos[ lightSurfaces[ lm.firstLightSurface + i ] ];

		/* add surface clusters */
		for ( j = 0; j < info->numSurfaceClusters; ++j )
			lm.lightClusters[ c++ ] = surfaceClusters[ info->firstSurfaceCluster + j ];
	}

	/* set styles */
	lm.styles = { LS_NORMAL, LS_NONE, LS_NONE, LS_NONE };

	/* set supersampling size */
	lm.sw = lm.w * superSample;
	lm.sh = lm.h * superSample;

	/* manipulate origin/vecs for supersampling */
	if ( superSample > 1 && lm.vecs != nullptr ) {
		/* calc inverse supersample */
		is = 1.0f / superSample;

		/* scale the vectors and shift the origin */
		#if 1
		/* new code that works for arbitrary supersampling values */
		lm.origin -= vector3_mid( lm.vecs[ 0 ], lm.vecs[ 1 ] );
		lm.vecs[ 0 ] *= is;
		lm.vecs[ 1 ] *= is;
		lm.origin += ( lm.vecs[ 0 ] + lm.vecs[ 1 ] ) * is;
		#else
		/* old code that only worked with a value of 2 */
		lm.vecs[ 0 ] *= is;
		lm.vecs[ 1 ] *= is;
		lm.origin -=  ( lm.vecs[ 0 ] + lm.vecs[ 1 ] ) * is;
		#endif
	}

	/* allocate bsp lightmap storage */
	size = lm.w * lm.h * sizeof( *( lm.bspLuxels[ 0 ] ) );
	if ( lm.bspLuxels[ 0 ] == nullptr ) {
		lm.bspLuxels[ 0 ] = safe_malloc( size );
	}
	memset( lm.bspLuxels[ 0 ], 0, size );

	/* allocate radiosity lightmap storage */
	if ( bounce ) {
		size = lm.w * lm.h * sizeof( *lm.radLuxels[ 0 ] );
		if ( lm.radLuxels[ 0 ] == nullptr ) {
			lm.radLuxels[ 0 ] = safe_malloc( size );
		}
		memset( lm.radLuxels[ 0 ], 0, size );
	}

	/* allocate sampling lightmap storage */
	size = lm.sw * lm.sh * sizeof( *lm.superLuxels[ 0 ] );
	if ( lm.superLuxels[ 0 ] == nullptr ) {
		lm.superLuxels[ 0 ] = safe_malloc( size );
	}
	memset( lm.superLuxels[ 0 ], 0, size );

	/* allocate origin map storage */
	size = lm.sw * lm.sh * sizeof( *lm.superOrigins );
	if ( lm.superOrigins == nullptr ) {
		lm.superOrigins = safe_malloc( size );
	}
	memset( lm.superOrigins, 0, size );

	/* allocate normal map storage */
	size = lm.sw * lm.sh * sizeof( *lm.superNormals );
	if ( lm.superNormals == nullptr ) {
		lm.superNormals = safe_malloc( size );
	}
	memset( lm.superNormals, 0, size );

	/* allocate dirt map storage */
	size = lm.sw * lm.sh * sizeof( *lm.superDirt );
	if ( lm.superDirt == nullptr ) {
		lm.superDirt = safe_malloc( size );
	}
	memset( lm.superDirt, 0, size );

	/* allocate floodlight map storage */
	size = lm.sw * lm.sh * sizeof( *lm.superFloodLight );
	if ( lm.superFloodLight == nullptr ) {
		lm.superFloodLight = safe_malloc( size );
	}
	memset( lm.superFloodLight, 0, size );

	/* allocate cluster map storage */
	size = lm.sw * lm.sh * sizeof( *lm.superClusters );
	if ( lm.superClusters == nullptr ) {
		lm.superClusters = safe_malloc( size );
	}
	size = lm.sw * lm.sh;
	sc = lm.superClusters;
	for ( i = 0; i < size; ++i )
		( *sc++ ) = CLUSTER_UNMAPPED;

	/* deluxemap allocation */
	if ( deluxemap ) {
		/* allocate sampling deluxel storage */
		size = lm.sw * lm.sh * sizeof( *lm.superDeluxels );
		if ( lm.superDeluxels == nullptr ) {
			lm.superDeluxels = safe_malloc( size );
		}
		memset( lm.superDeluxels, 0, size );

		/* allocate bsp deluxel storage */
		size = lm.w * lm.h * sizeof( *lm.bspDeluxels );
		if ( lm.bspDeluxels == nullptr ) {
			lm.bspDeluxels = safe_malloc( size );
		}
		memset( lm.bspDeluxels, 0, size );
	}

	/* add to count */
	numLuxels += ( lm.sw * lm.sh );
}



/*
   AddPatchToRawLightmap()
   projects a lightmap for a patch surface
   since lightmap calculation for surfaces is now handled in a general way (light_ydnar.c),
   it is no longer necessary for patch verts to fall exactly on a lightmap sample
   based on AllocateLightmapForPatch()
 */

static bool AddPatchToRawLightmap( int num, rawLightmap_t& lm ){
	float sBasis, tBasis;
	float length, widthTable[ MAX_EXPANDED_AXIS ] = {0}, heightTable[ MAX_EXPANDED_AXIS ] = {0};


	/* patches finish a raw lightmap */
	lm.finished = true;

	/* get surface and info  */
	const bspDrawSurface_t& ds = bspDrawSurfaces[ num ];
	const surfaceInfo_t& info = surfaceInfos[ num ];

	/* make a temporary mesh from the drawsurf */
	const mesh_t mesh = TessellatedMesh( mesh_view_t( ds.patchWidth, ds.patchHeight, &yDrawVerts[ ds.firstVert ] ), info.patchIterations );

	/* find the longest distance on each row/column */
	for ( int y = 0; y < mesh.height; ++y )
	{
		for ( int x = 0; x < mesh.width; ++x )
		{
			/* get width */
			if ( x + 1 < mesh.width ) {
				const bspDrawVert_t& a = mesh[ y ][ x ];
				const bspDrawVert_t& b = mesh[ y ][ x + 1 ];
				value_maximize( widthTable[ x ], (float)vector3_length( a.xyz - b.xyz ) );
			}

			/* get height */
			if ( y + 1 < mesh.height ) {
				const bspDrawVert_t& a = mesh[ y ][ x ];
				const bspDrawVert_t& b = mesh[ y + 1 ][ x ];
				value_maximize( heightTable[ y ], (float)vector3_length( a.xyz - b.xyz ) );
			}
		}
	}

	/* determine lightmap width */
	length = 0;
	for ( int x = 0; x < ( mesh.width - 1 ); ++x )
		length += widthTable[ x ];
	lm.w = lm.sampleSize != 0 ? ceil( length / lm.sampleSize ) + 1 : 0;
	value_maximize( lm.w, ds.patchWidth );
	value_minimize( lm.w, lm.customWidth );
	sBasis = (float) ( lm.w - 1 ) / ( ds.patchWidth - 1 );

	/* determine lightmap height */
	length = 0;
	for ( int y = 0; y < ( mesh.height - 1 ); ++y )
		length += heightTable[ y ];
	lm.h = lm.sampleSize != 0 ? ceil( length / lm.sampleSize ) + 1 : 0;
	value_maximize( lm.h, ds.patchHeight );
	value_minimize( lm.h, lm.customHeight );
	tBasis = (float) ( lm.h - 1 ) / ( ds.patchHeight - 1 );

	/* set the lightmap texture coordinates in yDrawVerts */
	lm.wrap[ 0 ] = true;
	lm.wrap[ 1 ] = true;
	bspDrawVert_t *verts = &yDrawVerts[ ds.firstVert ];
	for ( int y = 0; y < ds.patchHeight; ++y )
	{
		const float t = ( tBasis * y ) + 0.5f;
		for ( int x = 0; x < ds.patchWidth; ++x )
		{
			const float s = ( sBasis * x ) + 0.5f;
			verts[ ( y * ds.patchWidth ) + x ].lightmap[ 0 ][ 0 ] = s * superSample;
			verts[ ( y * ds.patchWidth ) + x ].lightmap[ 0 ][ 1 ] = t * superSample;

			if ( y == 0 && !VectorCompare( verts[ x ].xyz, verts[ ( ( ds.patchHeight - 1 ) * ds.patchWidth ) + x ].xyz ) ) {
				lm.wrap[ 1 ] = false;
			}
		}

		if ( !VectorCompare( verts[ ( y * ds.patchWidth ) ].xyz, verts[ ( y * ds.patchWidth ) + ( ds.patchWidth - 1 ) ].xyz ) ) {
			lm.wrap[ 0 ] = false;
		}
	}

	/* debug code: */
	//%	Sys_Printf( "wrap S: %d wrap T: %d\n", lm.wrap[ 0 ], lm.wrap[ 1 ] );
	//% if( lm.w > ( ds.lightmapWidth & 0xFF ) || lm.h > ( ds.lightmapHeight & 0xFF ) )
	//%		Sys_Printf( "Patch lightmap: (%3d %3d) > (%3d, %3d)\n", lm.w, lm.h, ds.lightmapWidth & 0xFF, ds.lightmapHeight & 0xFF );
	//% ds.lightmapWidth = lm.w | ( ds.lightmapWidth & 0xFFFF0000 );
	//% ds.lightmapHeight = lm.h | ( ds.lightmapHeight & 0xFFFF0000 );

	/* add to counts */
	numPatchesLightmapped++;

	/* return */
	return true;
}



/*
   AddSurfaceToRawLightmap()
   projects a lightmap for a surface
   based on AllocateLightmapForSurface()
 */

static bool AddSurfaceToRawLightmap( int num, rawLightmap_t& lm ){
	int axisNum;
	float sampleSize;
	Vector3 mins, maxs, origin, faxis, size, vecs[ 2 ]{ Vector3( 0 ), Vector3( 0 ) };
	Plane3f plane;
	bspDrawVert_t       *verts;


	/* get surface and info  */
	bspDrawSurface_t& ds = bspDrawSurfaces[ num ];
	const surfaceInfo_t& info = surfaceInfos[ num ];

	/* add the surface to the raw lightmap */
	lightSurfaces[ numLightSurfaces++ ] = num;
	lm.numLightSurfaces++;

	/* does this raw lightmap already have any surfaces? */
	if ( lm.numLightSurfaces > 1 ) {
		/* surface and raw lightmap must have the same lightmap projection axis */
		if ( !VectorCompare( info.axis, lm.axis ) ) {
			return false;
		}

		/* match identical attributes */
		if ( info.sampleSize         != lm.sampleSize ||
		     info.ambientColor       != lm.ambientColor || // may be different inside one entityNum for attached misc_models
		     info.entityNum          != lm.entityNum ||
		     info.recvShadows        != lm.recvShadows ||
		     info.si->lmCustomWidth  != lm.customWidth ||
		     info.si->lmCustomHeight != lm.customHeight ||
		     info.si->lmBrightness   != lm.brightness ||
		     info.si->lmFilterRadius != lm.filterRadius ||
		     info.si->splotchFix     != lm.splotchFix ) {
			return false;
		}

		/* surface bounds must intersect with raw lightmap bounds */
		if( !info.minmax.test( lm.minmax ) ){
			return false;
		}

		/* plane check (fixme: allow merging of nonplanars) */
		if ( !info.si->lmMergable ) {
			if ( info.plane == nullptr || lm.plane == nullptr ) {
				return false;
			}

			/* compare planes */
			if( !vector3_equal_epsilon( info.plane->normal(), lm.plane->normal(), EQUAL_EPSILON )
			 || !float_equal_epsilon( info.plane->dist(), lm.plane->dist(), EQUAL_EPSILON ) ){
				return false;
			}
		}

		/* debug code hacking */
		//%	if( lm.numLightSurfaces > 1 )
		//%		return false;
	}

	/* set plane */
	if ( info.plane == nullptr ) {
		lm.plane = nullptr;
	}

	/* add surface to lightmap bounds */
	lm.minmax.extend( info.minmax );

	/* check to see if this is a non-planar patch */
	if ( ds.surfaceType == MST_PATCH &&
	     lm.axis == g_vector3_identity ) {
		return AddPatchToRawLightmap( num, lm );
	}

	/* start with initially requested sample size */
	sampleSize = lm.sampleSize;

	/* round to the lightmap resolution */
	for ( int i = 0; i < 3; ++i )
	{
		mins[ i ] = sampleSize * floor( lm.minmax.mins[ i ] / sampleSize );
		maxs[ i ] = sampleSize * ceil( lm.minmax.maxs[ i ] / sampleSize );
		size[ i ] = ( maxs[ i ] - mins[ i ] ) / sampleSize + 1;

		/* hack (god this sucks) */
		if ( size[ i ] > lm.customWidth || size[ i ] > lm.customHeight || ( lmLimitSize && size[i] > lmLimitSize ) ) {
			i = -1;
			sampleSize += 1;
		}
	}

	if ( sampleSize != lm.sampleSize && lmLimitSize == 0 ){
		if ( debugSampleSize == 1 || lm.customWidth > 128 ){
			Sys_FPrintf( SYS_WRN | SYS_VRBflag, "WARNING: surface at (%6.0f %6.0f %6.0f) (%6.0f %6.0f %6.0f) too large for desired samplesize/lightmapsize/lightmapscale combination, increased samplesize from %d to %d\n",
			             info.minmax.mins[0],
			             info.minmax.mins[1],
			             info.minmax.mins[2],
			             info.minmax.maxs[0],
			             info.minmax.maxs[1],
			             info.minmax.maxs[2],
			             lm.sampleSize,
			             (int) sampleSize );
		}
		else if ( debugSampleSize == 0 ){
			Sys_FPrintf( SYS_WRN | SYS_VRBflag, "WARNING: surface at (%6.0f %6.0f %6.0f) (%6.0f %6.0f %6.0f) too large for desired samplesize/lightmapsize/lightmapscale combination, increased samplesize from %d to %d\n",
			             info.minmax.mins[0],
			             info.minmax.mins[1],
			             info.minmax.mins[2],
			             info.minmax.maxs[0],
			             info.minmax.maxs[1],
			             info.minmax.maxs[2],
			             lm.sampleSize,
			             (int) sampleSize );
			debugSampleSize--;
		}
		else{
			debugSampleSize--;
		}
	}

	/* set actual sample size */
	lm.actualSampleSize = sampleSize;

	/* fixme: copy rounded mins/maxes to lightmap record? */
	if ( lm.plane == nullptr ) {
		lm.minmax = { mins, maxs };
	}

	/* set lightmap origin */
	origin = lm.minmax.mins;

	/* make absolute axis */
	faxis[ 0 ] = std::fabs( lm.axis[ 0 ] );
	faxis[ 1 ] = std::fabs( lm.axis[ 1 ] );
	faxis[ 2 ] = std::fabs( lm.axis[ 2 ] );

	/* classify the plane (x y or z major) (ydnar: biased to z axis projection) */
	if ( faxis[ 2 ] >= faxis[ 0 ] && faxis[ 2 ] >= faxis[ 1 ] ) {
		axisNum = 2;
		lm.w = size[ 0 ];
		lm.h = size[ 1 ];
		vecs[ 0 ][ 0 ] = 1.0f / sampleSize;
		vecs[ 1 ][ 1 ] = 1.0f / sampleSize;
	}
	else if ( faxis[ 0 ] >= faxis[ 1 ] && faxis[ 0 ] >= faxis[ 2 ] ) {
		axisNum = 0;
		lm.w = size[ 1 ];
		lm.h = size[ 2 ];
		vecs[ 0 ][ 1 ] = 1.0f / sampleSize;
		vecs[ 1 ][ 2 ] = 1.0f / sampleSize;
	}
	else
	{
		axisNum = 1;
		lm.w = size[ 0 ];
		lm.h = size[ 2 ];
		vecs[ 0 ][ 0 ] = 1.0f / sampleSize;
		vecs[ 1 ][ 2 ] = 1.0f / sampleSize;
	}

	/* check for bogus axis */
	if ( faxis[ axisNum ] == 0 ) {
		Sys_Warning( "ProjectSurfaceLightmap: Chose a 0 valued axis\n" );
		lm.w = lm.h = 0;
		return false;
	}

	/* store the axis number in the lightmap */
	lm.axisNum = axisNum;

	/* walk the list of surfaces on this raw lightmap */
	for ( int n = 0; n < lm.numLightSurfaces; ++n )
	{
		/* get surface */
		const int num2 = lightSurfaces[ lm.firstLightSurface + n ];
		const bspDrawSurface_t& ds2 = bspDrawSurfaces[ num2 ];
		verts = &yDrawVerts[ ds2.firstVert ];

		/* set the lightmap texture coordinates in yDrawVerts in [0, superSample * lm.customWidth] space */
		for ( int i = 0; i < ds2.numVerts; ++i )
		{
			const Vector3 delta = verts[ i ].xyz - origin;
			const float s = vector3_dot( delta, vecs[ 0 ] ) + 0.5f;
			const float t = vector3_dot( delta, vecs[ 1 ] ) + 0.5f;
			verts[ i ].lightmap[ 0 ][ 0 ] = s * superSample;
			verts[ i ].lightmap[ 0 ][ 1 ] = t * superSample;

			if ( s > lm.w || t > lm.h ) {
				Sys_FPrintf( SYS_WRN | SYS_VRBflag, "WARNING: Lightmap texture coords out of range: S %1.4f > %3d || T %1.4f > %3d\n",
				             s, lm.w, t, lm.h );
			}
		}
	}

	/* get first drawsurface */
	const int num2 = lightSurfaces[ lm.firstLightSurface ];
	const bspDrawSurface_t& ds2 = bspDrawSurfaces[ num2 ];
	verts = &yDrawVerts[ ds2.firstVert ];

	/* calculate lightmap origin */
	if ( vector3_length( ds2.lightmapVecs[ 2 ] ) ) {
		plane.normal() = ds2.lightmapVecs[ 2 ];
	}
	else{
		plane.normal() = lm.axis;
	}
	plane.dist() = vector3_dot( verts[ 0 ].xyz, plane.normal() );

	lm.origin = origin;
	lm.origin[ axisNum ] -= plane3_distance_to_point( plane, lm.origin ) / plane.normal()[ axisNum ];

	/* legacy support */
	ds.lightmapOrigin = lm.origin;

	/* for planar surfaces, create lightmap vectors for st->xyz conversion */
	if ( vector3_length( ds.lightmapVecs[ 2 ] ) || 1 ) {  /* ydnar: can't remember what exactly i was thinking here... */
		/* allocate space for the vectors */
		lm.vecs = safe_calloc( 3 * sizeof( *lm.vecs ) );
		lm.vecs[ 2 ] = ds.lightmapVecs[ 2 ];

		/* project stepped lightmap blocks and subtract to get planevecs */
		for ( int i = 0; i < 2; ++i )
		{
			Vector3 normalized = vecs[ i ];
			const float len = VectorNormalize( normalized );
			lm.vecs[ i ] = normalized * ( 1.0 / len );
			lm.vecs[ i ][ axisNum ] -= vector3_dot( lm.vecs[ i ], plane.normal() ) / plane.normal()[ axisNum ];
		}
	}
	else
	{
		/* lightmap vectors are useless on a non-planar surface */
		lm.vecs = nullptr;
	}

	/* add to counts */
	if ( ds.surfaceType == MST_PATCH ) {
		numPatchesLightmapped++;
		if ( lm.plane != nullptr ) {
			numPlanarPatchesLightmapped++;
		}
	}
	else
	{
		if ( lm.plane != nullptr ) {
			numPlanarsLightmapped++;
		}
		else{
			numNonPlanarsLightmapped++;
		}
	}

	/* return */
	return true;
}



/*
   CompareSurfaceInfo()
   compare functor for std::sort()
 */

struct CompareSurfaceInfo
{
	bool operator()( const int a, const int b ) const {
		/* get surface info */
		const surfaceInfo_t& aInfo = surfaceInfos[ a ];
		const surfaceInfo_t& bInfo = surfaceInfos[ b ];

		/* model first */
		if ( aInfo.modelindex < bInfo.modelindex ) {
			return false;
		}
		else if ( aInfo.modelindex > bInfo.modelindex ) {
			return true;
		}

		/* then lightmap status */
		if ( aInfo.hasLightmap < bInfo.hasLightmap ) {
			return false;
		}
		else if ( aInfo.hasLightmap > bInfo.hasLightmap ) {
			return true;
		}

		/* 27: then shader! */
		if ( aInfo.si < bInfo.si ) {
			return false;
		}
		else if ( aInfo.si > bInfo.si ) {
			return true;
		}


		/* then lightmap sample size */
		if ( aInfo.sampleSize < bInfo.sampleSize ) {
			return false;
		}
		else if ( aInfo.sampleSize > bInfo.sampleSize ) {
			return true;
		}

		/* then lightmap axis */
		for ( int i = 0; i < 3; ++i )
		{
			if ( aInfo.axis[ i ] < bInfo.axis[ i ] ) {
				return false;
			}
			else if ( aInfo.axis[ i ] > bInfo.axis[ i ] ) {
				return true;
			}
		}

		/* then plane */
		if ( aInfo.plane == nullptr && bInfo.plane != nullptr ) {
			return false;
		}
		else if ( aInfo.plane != nullptr && bInfo.plane == nullptr ) {
			return true;
		}
		else if ( aInfo.plane != nullptr && bInfo.plane != nullptr ) {
			for ( int i = 0; i < 3; ++i )
			{
				if ( aInfo.plane->normal()[ i ] < bInfo.plane->normal()[ i ] ) {
					return false;
				}
				else if ( aInfo.plane->normal()[ i ] > bInfo.plane->normal()[ i ] ) {
					return true;
				}
			}
			if ( aInfo.plane->dist() < bInfo.plane->dist() ) {
				return false;
			}
			else if ( aInfo.plane->dist() > bInfo.plane->dist() ) {
				return true;
			}
		}

		/* then position in world */
		for ( int i = 0; i < 3; ++i )
		{
			if ( aInfo.minmax.mins[ i ] < bInfo.minmax.mins[ i ] ) {
				return false;
			}
			else if ( aInfo.minmax.mins[ i ] > bInfo.minmax.mins[ i ] ) {
				return true;
			}
		}

		/* these are functionally identical (this should almost never happen) */
		return false;
	}
};



/*
   SetupSurfaceLightmaps()
   allocates lightmaps for every surface in the bsp that needs one
   this depends on yDrawVerts being allocated
 */

void SetupSurfaceLightmaps(){
	int i, j, k;
	const int numBSPDrawSurfaces = bspDrawSurfaces.size();


	/* note it */
	Sys_FPrintf( SYS_VRB, "--- SetupSurfaceLightmaps ---\n" );

	/* determine supersample amount */
	value_maximize( superSample, 1 );
	if ( superSample > 8 ) {
		Sys_Warning( "Insane supersampling amount (%d) detected.\n", superSample );
		superSample = 8;
	}

	/* clear map bounds */
	g_mapMinmax.clear();

	/* allocate a list of surface clusters */
	numSurfaceClusters = 0;
	maxSurfaceClusters = bspLeafSurfaces.size();
	surfaceClusters = safe_calloc( maxSurfaceClusters * sizeof( *surfaceClusters ) );

	/* allocate a list for per-surface info */
	surfaceInfos = safe_calloc( numBSPDrawSurfaces * sizeof( *surfaceInfos ) );
	for ( i = 0; i < numBSPDrawSurfaces; ++i )
		surfaceInfos[ i ].childSurfaceNum = -1;

	/* allocate a list of surface indexes to be sorted */
	int *sortSurfaces = safe_calloc( numBSPDrawSurfaces * sizeof( int ) );

	/* walk each model in the bsp */
	for ( const bspModel_t& model : bspModels )
	{
		/* walk the list of surfaces in this model and fill out the info structs */
		for ( j = 0; j < model.numBSPSurfaces; ++j )
		{
			/* make surface index */
			const int num = model.firstBSPSurface + j;

			/* copy index to sort list */
			sortSurfaces[ num ] = num;

			/* get surface and info */
			const bspDrawSurface_t& ds = bspDrawSurfaces[ num ];
			surfaceInfo_t& info = surfaceInfos[ num ];

			/* basic setup */
			info.modelindex = i;
			info.lm = nullptr;
			info.plane = nullptr;
			info.firstSurfaceCluster = numSurfaceClusters;

			{ /* get extra data */
				const surfaceExtra_t& se = GetSurfaceExtra( num );
				info.si = se.si;
				if ( info.si == nullptr ) {
					info.si = &ShaderInfoForShader( bspShaders[ ds.shaderNum ].shader );
				}
				info.parentSurfaceNum = se.parentSurfaceNum;
				info.entityNum        = se.entityNum;
				info.castShadows      = se.castShadows;
				info.recvShadows      = se.recvShadows;
				info.sampleSize       = se.sampleSize;
				info.ambientColor     = se.ambientColor;
				info.longestCurve     = se.longestCurve;
				info.patchIterations = IterationsForCurve( info.longestCurve, patchSubdivisions );
				info.axis             = se.lightmapAxis;
			}

			/* mark parent */
			if ( info.parentSurfaceNum >= 0 ) {
				surfaceInfos[ info.parentSurfaceNum ].childSurfaceNum = j;
			}

			/* determine surface bounds */
			info.minmax.clear();
			for ( const bspDrawVert_t& dv : Span( &yDrawVerts[ ds.firstVert ], ds.numVerts ) )
			{
				g_mapMinmax.extend( dv.xyz );
				info.minmax.extend( dv.xyz );
			}

			/* find all the bsp clusters the surface falls into */
			for ( const bspLeaf_t& leaf : bspLeafs )
			{
				/* test bbox */
				if( !leaf.minmax.test( info.minmax ) ) {
					continue;
				}

				/* test leaf surfaces */
				for ( const int s : Span( &bspLeafSurfaces[ leaf.firstBSPLeafSurface ], leaf.numBSPLeafSurfaces ) )
				{
					if ( s == num ) {
						if ( numSurfaceClusters >= maxSurfaceClusters ) {
							Error( "maxSurfaceClusters exceeded" );
						}
						surfaceClusters[ numSurfaceClusters ] = leaf.cluster;
						numSurfaceClusters++;
						info.numSurfaceClusters++;
					}
				}
			}

			/* determine if surface is planar */
			if ( vector3_length( ds.lightmapVecs[ 2 ] ) != 0 ) {
				/* make a plane */
				info.plane = safe_malloc( sizeof( *( info.plane ) ) );
				info.plane->normal() = ds.lightmapVecs[ 2 ];
				info.plane->dist() = vector3_dot( yDrawVerts[ ds.firstVert ].xyz, info.plane->normal() );
			}

			/* determine if surface requires a lightmap */
			if ( ds.surfaceType == MST_TRIANGLE_SOUP ||
			     ds.surfaceType == MST_FOLIAGE ||
			     ( info.si->compileFlags & C_VERTEXLIT ) ||
			     noLightmaps ) {
				numSurfsVertexLit++;
			}
			else
			{
				numSurfsLightmapped++;
				info.hasLightmap = true;
			}
		}
	}

	/* sort the surfaces info list */
	std::ranges::sort( Span( sortSurfaces, numBSPDrawSurfaces ), CompareSurfaceInfo() );

	/* allocate a list of surfaces that would go into raw lightmaps */
	numLightSurfaces = 0;
	lightSurfaces = safe_calloc( numSurfsLightmapped * sizeof( int ) );

	/* allocate a list of raw lightmaps */
	numRawLightmaps = 0;
	rawLightmaps = safe_calloc( numSurfsLightmapped * sizeof( *rawLightmaps ) );

	/* walk the list of sorted surfaces */
	for ( i = 0; i < numBSPDrawSurfaces; ++i )
	{
		/* get info and attempt early out */
		const int num = sortSurfaces[ i ];
		surfaceInfo_t& info = surfaceInfos[ num ];
		if ( !info.hasLightmap || info.lm != nullptr || info.parentSurfaceNum >= 0 ) {
			continue;
		}

		/* allocate a new raw lightmap */
		rawLightmap_t& lm = rawLightmaps[ numRawLightmaps ];
		numRawLightmaps++;

		/* set it up */
		lm.splotchFix = info.si->splotchFix;
		lm.firstLightSurface = numLightSurfaces;
		lm.numLightSurfaces = 0;
		/* vortex: multiply lightmap sample size by -samplescale */
		if ( sampleScale > 0 ) {
			lm.sampleSize = info.sampleSize * sampleScale;
		}
		else{
			lm.sampleSize = info.sampleSize;
		}
		lm.actualSampleSize = lm.sampleSize;
		lm.entityNum                = info.entityNum;
		lm.recvShadows              = info.recvShadows;
		lm.brightness               = info.si->lmBrightness;
		lm.filterRadius             = info.si->lmFilterRadius;
		lm.ambientColor             = info.ambientColor;
		lm.floodlightRGB            = info.si->floodlightRGB;
		lm.floodlightDistance       = info.si->floodlightDistance;
		lm.floodlightIntensity      = info.si->floodlightIntensity;
		lm.floodlightDirectionScale = info.si->floodlightDirectionScale;
		lm.axis                     = info.axis;
		lm.plane                    = info.plane;
		lm.minmax                   = info.minmax;

		lm.customWidth              = info.si->lmCustomWidth;
		lm.customHeight             = info.si->lmCustomHeight;

		/* add the surface to the raw lightmap */
		AddSurfaceToRawLightmap( num, lm );
		info.lm = &lm;

		/* do an exhaustive merge */
		for ( bool added = true; added; )
		{
			/* walk the list of surfaces again */
			added = false;
			for ( j = i + 1; j < numBSPDrawSurfaces && !lm.finished; ++j )
			{
				/* get info and attempt early out */
				const int num2 = sortSurfaces[ j ];
				surfaceInfo_t& info2 = surfaceInfos[ num2 ];
				if ( !info2.hasLightmap || info2.lm != nullptr ) {
					continue;
				}

				/* add the surface to the raw lightmap */
				if ( AddSurfaceToRawLightmap( num2, lm ) ) {
					info2.lm = &lm;
					added = true;
				}
				else
				{
					/* back up one */
					lm.numLightSurfaces--;
					numLightSurfaces--;
				}
			}
		}

		/* finish the lightmap and allocate the various buffers */
		FinishRawLightmap( lm );
	}

	if ( debugSampleSize < -1 ){
		Sys_FPrintf( SYS_WRN | SYS_VRBflag, "+%d similar occurrences;\t-debugsamplesize to show ones\n", -debugSampleSize - 1 );
	}

	/* allocate vertex luxel storage */
	for ( k = 0; k < MAX_LIGHTMAPS; ++k )
	{
		vertexLuxels[ k ] = safe_calloc( bspDrawVerts.size() * sizeof( *vertexLuxels[ 0 ] ) );
		radVertexLuxels[ k ] = safe_calloc( bspDrawVerts.size() * sizeof( *radVertexLuxels[ 0 ] ) );
	}

	/* emit some stats */
	Sys_FPrintf( SYS_VRB, "%9d surfaces\n", numBSPDrawSurfaces );
	Sys_FPrintf( SYS_VRB, "%9d raw lightmaps\n", numRawLightmaps );
	Sys_FPrintf( SYS_VRB, "%9d surfaces vertex lit\n", numSurfsVertexLit );
	Sys_FPrintf( SYS_VRB, "%9d surfaces lightmapped\n", numSurfsLightmapped );
	Sys_FPrintf( SYS_VRB, "%9d planar surfaces lightmapped\n", numPlanarsLightmapped );
	Sys_FPrintf( SYS_VRB, "%9d non-planar surfaces lightmapped\n", numNonPlanarsLightmapped );
	Sys_FPrintf( SYS_VRB, "%9d patches lightmapped\n", numPatchesLightmapped );
	Sys_FPrintf( SYS_VRB, "%9d planar patches lightmapped\n", numPlanarPatchesLightmapped );
}



/*
   StitchSurfaceLightmaps()
   stitches lightmap edges
   2002-11-20 update: use this func only for stitching nonplanar patch lightmap seams
 */

#define MAX_STITCH_CANDIDATES   32
#define MAX_STITCH_LUXELS       64

void StitchSurfaceLightmaps(){
	int i, j, x, y, x2, y2,
	    numStitched, numCandidates, numLuxels;
	rawLightmap_t   *lm, *a, *b, *c[ MAX_STITCH_CANDIDATES ];
	float           sampleSize, totalColor;


	/* disabled for now */
	return;

	/* note it */
	Sys_Printf( "--- StitchSurfaceLightmaps ---\n" );

	/* init pacifier */
	Pacifier pacifier( numRawLightmaps );
	Timer timer;

	/* walk the list of raw lightmaps */
	numStitched = 0;
	for ( i = 0; i < numRawLightmaps; ++i )
	{
		/* print pacifier */
		++pacifier;

		/* get lightmap a */
		a = &rawLightmaps[ i ];

		/* walk rest of lightmaps */
		numCandidates = 0;
		for ( j = i + 1; j < numRawLightmaps && numCandidates < MAX_STITCH_CANDIDATES; ++j )
		{
			/* get lightmap b */
			b = &rawLightmaps[ j ];

			/* test bounding box */
			if ( !a->minmax.test( b->minmax ) ) {
				continue;
			}

			/* add candidate */
			c[ numCandidates++ ] = b;
		}

		/* walk luxels */
		for ( y = 0; y < a->sh; ++y )
		{
			for ( x = 0; x < a->sw; ++x )
			{
				/* ignore unmapped/unlit luxels */
				lm = a;
				if ( lm->getSuperCluster( x, y ) == CLUSTER_UNMAPPED ) {
					continue;
				}
				SuperLuxel& luxel = lm->getSuperLuxel( 0, x, y );
				if ( luxel.count <= 0 ) {
					continue;
				}

				/* get particulars */
				const Vector3& origin = lm->getSuperOrigin( x, y );
				const Vector3& normal = lm->getSuperNormal( x, y );

				/* walk candidate list */
				for ( j = 0; j < numCandidates; ++j )
				{
					/* get candidate */
					b = c[ j ];
					lm = b;

					/* set samplesize to the smaller of the pair */
					sampleSize = 0.5f * std::min( a->actualSampleSize, b->actualSampleSize );

					/* test bounding box */
					if ( !b->minmax.test( origin, sampleSize ) ) {
						continue;
					}

					/* walk candidate luxels */
					Vector3 average( 0 );
					numLuxels = 0;
					totalColor = 0;
					for ( y2 = 0; y2 < b->sh && numLuxels < MAX_STITCH_LUXELS; ++y2 )
					{
						for ( x2 = 0; x2 < b->sw && numLuxels < MAX_STITCH_LUXELS; ++x2 )
						{
							/* ignore same luxels */
							if ( a == b && abs( x - x2 ) <= 1 && abs( y - y2 ) <= 1 ) {
								continue;
							}

							/* ignore unmapped/unlit luxels */
							if ( lm->getSuperCluster( x2, y2 ) == CLUSTER_UNMAPPED ) {
								continue;
							}
							const SuperLuxel& luxel2 = lm->getSuperLuxel( 0, x2, y2 );
							if ( luxel2.count <= 0 ) {
								continue;
							}

							/* get particulars */
							const Vector3& origin2 = lm->getSuperOrigin( x2, y2 );
							const Vector3& normal2 = lm->getSuperNormal( x2, y2 );

							/* test normal */
							if ( vector3_dot( normal, normal2 ) < 0.5f ) {
								continue;
							}

							/* test bounds */
							if ( !vector3_equal_epsilon( origin, origin2, sampleSize ) ) {
								continue;
							}

							/* add luxel */
							//%	luxel2.value = { 255, 0, 255 };
							numLuxels++;
							average += luxel2.value;
							totalColor += luxel2.count;
						}
					}

					/* early out */
					if ( numLuxels == 0 ) {
						continue;
					}

					/* scale average */
					luxel.value = average * ( 1.0f / totalColor );
					luxel.count = 1;
					numStitched++;
				}
			}
		}
	}

	/* emit statistics */
	Sys_Printf( " (%i)\n", int( timer.elapsed_sec() ) );
	Sys_FPrintf( SYS_VRB, "%9d luxels stitched\n", numStitched );
}



/*
   CompareBSPLuxels()
   compares two surface lightmaps' bsp luxels, ignoring occluded luxels
 */

#define SOLID_EPSILON       0.0625f
#define LUXEL_TOLERANCE     0.0025
#define LUXEL_COLOR_FRAC    0.001302083 /* 1 / 3 / 256 */

static bool CompareBSPLuxels( rawLightmap_t *a, int aNum, rawLightmap_t *b, int bNum ){
	/* styled lightmaps will never be collapsed to non-styled lightmaps when there is _minlight */
	if ( minLight != g_vector3_identity &&
	     ( aNum == 0 ) != ( bNum == 0 ) ) {
		return false;
	}

	/* basic tests */
	if ( a->customWidth != b->customWidth || a->customHeight != b->customHeight ||
	     a->brightness != b->brightness ||
	     a->solid[ aNum ] != b->solid[ bNum ] ||
	     a->bspLuxels[ aNum ] == nullptr || b->bspLuxels[ bNum ] == nullptr ) {
		return false;
	}

	/* compare solid color lightmaps */
	if ( a->solid[ aNum ] && b->solid[ bNum ] ) {
		/* compare color */
		if ( !vector3_equal_epsilon( a->solidColor[ aNum ], b->solidColor[ bNum ], SOLID_EPSILON ) ) {
			return false;
		}

		/* okay */
		return true;
	}

	/* compare nonsolid lightmaps */
	if ( a->w != b->w || a->h != b->h ) {
		return false;
	}

	/* compare luxels */
	double delta = 0;
	double total = 0;
	for ( int y = 0; y < a->h; ++y )
	{
		for ( int x = 0; x < a->w; ++x )
		{
			/* increment total */
			total += 1.0;

			/* get luxels */
			const Vector3& aLuxel = a->getBspLuxel( aNum, x, y );
			const Vector3& bLuxel = b->getBspLuxel( bNum, x, y );

			/* ignore unused luxels */
			if ( aLuxel[ 0 ] < 0 || bLuxel[ 0 ] < 0 ) {
				continue;
			}

			/* 2003-09-27: compare individual luxels */
			if ( !vector3_equal_epsilon( aLuxel, bLuxel, 3.f ) ) {
				return false;
			}

			/* compare (fixme: take into account perceptual differences) */
			Vector3 diff = aLuxel - bLuxel;
			for( int i = 0; i < 3; ++i )
				diff[i] = std::fabs( diff[i] );
			delta += vector3_dot( diff, Vector3( LUXEL_COLOR_FRAC ) );

			/* is the change too high? */
			if ( total > 0 && ( ( delta / total ) > LUXEL_TOLERANCE ) ) {
				return false;
			}
		}
	}

	/* made it this far, they must be identical (or close enough) */
	return true;
}



/*
   MergeBSPLuxels()
   merges two surface lightmaps' bsp luxels, overwriting occluded luxels
 */

static bool MergeBSPLuxels( rawLightmap_t *a, int aNum, rawLightmap_t *b, int bNum ){
	int x, y;
	Vector3 luxel;


	/* basic tests */
	if ( a->customWidth != b->customWidth || a->customHeight != b->customHeight ||
	     a->brightness != b->brightness ||
	     a->solid[ aNum ] != b->solid[ bNum ] ||
	     a->bspLuxels[ aNum ] == nullptr || b->bspLuxels[ bNum ] == nullptr ) {
		return false;
	}

	/* compare solid lightmaps */
	if ( a->solid[ aNum ] && b->solid[ bNum ] ) {
		/* average */
		luxel = vector3_mid( a->solidColor[ aNum ], b->solidColor[ bNum ] );

		/* copy to both */
		a->solidColor[ aNum ] = luxel;
		b->solidColor[ bNum ] = luxel;

		/* return to sender */
		return true;
	}

	/* compare nonsolid lightmaps */
	if ( a->w != b->w || a->h != b->h ) {
		return false;
	}

	/* merge luxels */
	for ( y = 0; y < a->h; ++y )
	{
		for ( x = 0; x < a->w; ++x )
		{
			/* get luxels */
			Vector3& aLuxel = a->getBspLuxel( aNum, x, y );
			Vector3& bLuxel = b->getBspLuxel( bNum, x, y );

			/* handle occlusion mismatch */
			if ( aLuxel[ 0 ] < 0 ) {
				aLuxel = bLuxel;
			}
			else if ( bLuxel[ 0 ] < 0 ) {
				bLuxel = aLuxel;
			}
			else
			{
				/* average */
				luxel = vector3_mid( aLuxel, bLuxel );

				/* debugging code */
				//%	luxel[ 2 ] += 64.0f;

				/* copy to both */
				aLuxel = luxel;
				bLuxel = luxel;
			}
		}
	}

	/* done */
	return true;
}



/*
   ApproximateLuxel()
   determines if a single luxel is can be approximated with the interpolated vertex rgba
 */

static bool ApproximateLuxel( rawLightmap_t *lm, const bspDrawVert_t& dv ){
	/* find luxel xy coords */
	const int x = std::clamp( int( dv.lightmap[ 0 ][ 0 ] / superSample ), 0, lm->w - 1 );
	const int y = std::clamp( int( dv.lightmap[ 0 ][ 1 ] / superSample ), 0, lm->h - 1 );

	/* walk list */
	for ( int lightmapNum = 0; lightmapNum < MAX_LIGHTMAPS; ++lightmapNum )
	{
		/* early out */
		if ( lm->styles[ lightmapNum ] == LS_NONE ) {
			continue;
		}

		/* get luxel */
		const Vector3& luxel = lm->getBspLuxel( lightmapNum, x, y );

		/* ignore occluded luxels */
		if ( luxel[ 0 ] < 0 || luxel[ 1 ] < 0 || luxel[ 2 ] < 0 ) {
			return true;
		}

		/* copy, set min color and compare */
		Vector3 color = luxel;
		Vector3 vertexColor = dv.color[ 0 ].rgb();

		/* styles are not affected by minlight */
		if ( lightmapNum == 0 ) {
			for ( int i = 0; i < 3; ++i )
			{
				/* set min color */
				value_maximize( color[ i ], minLight[ i ] );
				value_maximize( vertexColor[ i ], minLight[ i ] ); /* note NOT minVertexLight */
			}
		}

		/* set to bytes */
		Color4b cb( ColorToBytes( color ), 0 );
		Color4b vcb( ColorToBytes( vertexColor ), 0 );

		/* compare */
		for ( int i = 0; i < 3; ++i )
		{
			if ( abs( cb[ i ] - vcb[ i ] ) > approximateTolerance ) {
				return false;
			}
		}
	}

	/* close enough for the girls i date */
	return true;
}



/*
   ApproximateTriangle()
   determines if a single triangle can be approximated with vertex rgba
 */

static bool ApproximateTriangle_r( rawLightmap_t *lm, const TriRef& tri ){
	/* approximate the vertexes */
	if ( !ApproximateLuxel( lm, *tri[ 0 ] )
	  || !ApproximateLuxel( lm, *tri[ 1 ] )
	  || !ApproximateLuxel( lm, *tri[ 2 ] ) )
		return false;

	/* subdivide calc */
	int max = -1;
	{
		/* find the longest edge and split it */
		float maxDist = 0;
		for ( int i = 0; i < 3; ++i )
		{
			const float dist = vector2_length( tri[ i ]->lightmap[ 0 ] - tri[ ( i + 1 ) % 3 ]->lightmap[ 0 ] );
			if ( dist > maxDist ) {
				maxDist = dist;
				max = i;
			}
		}

		/* try to early out */
		if ( max < 0 || maxDist < subdivideThreshold ) {
			return true;
		}
	}

	/* split the longest edge and map it */
	const bspDrawVert_t mid = LerpDrawVert( *tri[ max ], *tri[ ( max + 1 ) % 3 ] );
	if ( !ApproximateLuxel( lm, mid ) ) {
		return false;
	}

	/* recurse to first triangle */
	TriRef tri2 = tri;
	tri2[ max ] = &mid;
	if ( !ApproximateTriangle_r( lm, tri2 ) ) {
		return false;
	}

	/* recurse to second triangle */
	tri2 = tri;
	tri2[ ( max + 1 ) % 3 ] = &mid;
	return ApproximateTriangle_r( lm, tri2 );
}



/*
   ApproximateLightmap()
   determines if a raw lightmap can be approximated sufficiently with vertex colors
 */

static bool ApproximateLightmap( rawLightmap_t *lm ){
	/* approximating? */
	if ( approximateTolerance <= 0 ) {
		return false;
	}

	/* test for jmonroe */
	#if 0
	/* don't approx lightmaps with styled twins */
	if ( lm->numStyledTwins > 0 ) {
		return false;
	}

	/* don't approx lightmaps with styles */
	for ( int i = 1; i < MAX_LIGHTMAPS; ++i )
	{
		if ( lm->styles[ i ] != LS_NONE ) {
			return false;
		}
	}
	#endif

	/* assume reduced until shadow detail is found */
	bool approximated = true;

	/* walk the list of surfaces on this raw lightmap */
	for ( int n = 0; n < lm->numLightSurfaces; ++n )
	{
		/* get surface */
		const int num = lightSurfaces[ lm->firstLightSurface + n ];
		const bspDrawSurface_t& ds = bspDrawSurfaces[ num ];
		surfaceInfo_t& info = surfaceInfos[ num ];

		/* assume not-reduced initially */
		info.approximated = false;

		/* bail if lightmap doesn't match up */
		if ( info.lm != lm ) {
			continue;
		}

		/* bail if not vertex lit */
		if ( info.si->noVertexLight ) {
			continue;
		}

		/* assume that surfaces whose bounding boxes is smaller than 2x samplesize will be forced to vertex */
		if ( ( info.minmax.maxs[ 0 ] - info.minmax.mins[ 0 ] ) <= ( 2.0f * info.sampleSize ) &&
		     ( info.minmax.maxs[ 1 ] - info.minmax.mins[ 1 ] ) <= ( 2.0f * info.sampleSize ) &&
		     ( info.minmax.maxs[ 2 ] - info.minmax.mins[ 2 ] ) <= ( 2.0f * info.sampleSize ) ) {
			info.approximated = true;
			numSurfsVertexForced++;
			continue;
		}

		/* handle the triangles */
		switch ( ds.surfaceType )
		{
		case MST_PLANAR:
		{
			/* get verts */
			const bspDrawVert_t *verts = &yDrawVerts[ ds.firstVert ];

			/* map the triangles */
			info.approximated = true;
			for ( int i = 0; i < ds.numIndexes && info.approximated; i += 3 )
			{
				info.approximated = ApproximateTriangle_r( lm, TriRef{
					&verts[ bspDrawIndexes[ ds.firstIndex + i + 0 ] ],
					&verts[ bspDrawIndexes[ ds.firstIndex + i + 1 ] ],
					&verts[ bspDrawIndexes[ ds.firstIndex + i + 2 ] ] } );
			}
			break;
		}
		case MST_PATCH:
		{
			/* make a mesh from the drawsurf */
			const mesh_t mesh = TessellatedMesh( mesh_view_t( ds.patchWidth, ds.patchHeight, &yDrawVerts[ ds.firstVert ] ), info.patchIterations );

			/* map the mesh quads */
			info.approximated = true;
			for( MeshQuadIterator it( mesh ); it && info.approximated; ++it ){
				for( const TriRef& tri : it.tris() )
				{
					if ( info.approximated ) {
						info.approximated = ApproximateTriangle_r( lm, tri );
					}
				}
			}
			break;
		}
		default:
			break;
		}

		/* reduced? */
		if ( !info.approximated ) {
			approximated = false;
		}
		else{
			numSurfsVertexApproximated++;
		}
	}

	/* return */
	return approximated;
}



/*
   TestOutLightmapStamp()
   tests a stamp on a given lightmap for validity
 */

static bool TestOutLightmapStamp( rawLightmap_t *lm, int lightmapNum, outLightmap_t *olm, int x, int y ){
	/* bounds check */
	if ( x < 0 || y < 0 || ( x + lm->w ) > olm->customWidth || ( y + lm->h ) > olm->customHeight ) {
		return false;
	}

	/* solid lightmaps test a 1x1 stamp */
	if ( lm->solid[ lightmapNum ] ) {
		if ( bit_is_enabled( olm->lightBits, ( y * olm->customWidth ) + x ) ) {
			return false;
		}
		return true;
	}

	/* test the stamp */
	for ( int sy = 0; sy < lm->h; ++sy )
	{
		for ( int sx = 0; sx < lm->w; ++sx )
		{
			/* get luxel */
			if ( lm->getBspLuxel( lightmapNum, sx, sy )[ 0 ] < 0 ) {
				continue;
			}

			/* get bsp lightmap coords and test */
			const int ox = x + sx;
			const int oy = y + sy;
			if ( bit_is_enabled( olm->lightBits, ( oy * olm->customWidth ) + ox ) ) {
				return false;
			}
		}
	}

	/* stamp is empty */
	return true;
}



/*
   SetupOutLightmap()
   sets up an output lightmap
 */

static void SetupOutLightmap( rawLightmap_t *lm, outLightmap_t *olm ){
	/* dummy check */
	if ( lm == nullptr || olm == nullptr ) {
		return;
	}

	/* is this a "normal" bsp-stored lightmap? */
	if ( ( lm->customWidth == g_game->lightmapSize && lm->customHeight == g_game->lightmapSize ) || externalLightmaps ) {
		olm->lightmapNum = numBSPLightmaps;
		numBSPLightmaps++;

		/* lightmaps are interleaved with light direction maps */
		if ( deluxemap ) {
			numBSPLightmaps++;
		}
	}
	else{
		olm->lightmapNum = LIGHTMAP_BY_VERTEX;
	}

	/* set external lightmap number */
	olm->extLightmapNum = -1;

	/* set it up */
	olm->numLightmaps = 0;
	olm->customWidth = lm->customWidth;
	olm->customHeight = lm->customHeight;
	olm->freeLuxels = olm->customWidth * olm->customHeight;
	olm->numShaders = 0;

	/* allocate buffers */
	olm->lightBits = safe_calloc( ( olm->customWidth * olm->customHeight / 8 ) + 8 );
	olm->bspLightBytes = safe_calloc( olm->customWidth * olm->customHeight * sizeof( *olm->bspLightBytes ) );
	if ( deluxemap ) {
		olm->bspDirBytes = safe_calloc( olm->customWidth * olm->customHeight * sizeof( *olm->bspDirBytes ) );
	}
}



/*
   FindOutLightmaps()
   for a given surface lightmap, find output lightmap pages and positions for it
 */

#define LIGHTMAP_RESERVE_COUNT 1
static void FindOutLightmaps( rawLightmap_t *lm, bool fastAllocate ){
	int i, j, k, lightmapNum, xMax, yMax, x = -1, y = -1, sx, sy, ox, oy;
	outLightmap_t       *olm;
	surfaceInfo_t       *info;
	bool ok;
	int xIncrement, yIncrement;


	/* set default lightmap number */
	lm->outLightmapNums.fill( LIGHTMAP_BY_VERTEX );

	/* can this lightmap be approximated with vertex color? */
	if ( ApproximateLightmap( lm ) ) {
		return;
	}

	/* walk list */
	for ( lightmapNum = 0; lightmapNum < MAX_LIGHTMAPS; ++lightmapNum )
	{
		/* early out */
		if ( lm->styles[ lightmapNum ] == LS_NONE ) {
			continue;
		}

		/* don't store twinned lightmaps */
		if ( lm->twins[ lightmapNum ] != nullptr ) {
			continue;
		}

		/* if this is a styled lightmap, try some normalized locations first */
		ok = false;
		if ( lightmapNum > 0 && outLightmaps != nullptr ) {
			/* loop twice */
			for ( j = 0; j < 2; ++j )
			{
				/* try identical position */
				for ( i = 0; i < numOutLightmaps; ++i )
				{
					/* get the output lightmap */
					olm = &outLightmaps[ i ];

					/* simple early out test */
					if ( olm->freeLuxels < lm->used ) {
						continue;
					}

					/* don't store non-custom raw lightmaps on custom bsp lightmaps */
					if ( olm->customWidth != lm->customWidth ||
					     olm->customHeight != lm->customHeight ) {
						continue;
					}

					/* try identical */
					if ( j == 0 ) {
						x = lm->lightmapX[ 0 ];
						y = lm->lightmapY[ 0 ];
						ok = TestOutLightmapStamp( lm, lightmapNum, olm, x, y );
					}

					/* try shifting */
					else
					{
						for ( sy = -1; sy <= 1; ++sy )
						{
							for ( sx = -1; sx <= 1; ++sx )
							{
								x = lm->lightmapX[ 0 ] + sx * ( olm->customWidth >> 1 );  //%	lm->w;
								y = lm->lightmapY[ 0 ] + sy * ( olm->customHeight >> 1 ); //%	lm->h;
								ok = TestOutLightmapStamp( lm, lightmapNum, olm, x, y );

								if ( ok ) {
									break;
								}
							}

							if ( ok ) {
								break;
							}
						}
					}

					if ( ok ) {
						break;
					}
				}

				if ( ok ) {
					break;
				}
			}
		}

		/* try normal placement algorithm */
		if ( !ok ) {
			/* reset origin */
			x = 0;
			y = 0;

			/* walk the list of lightmap pages */
			if ( lightmapSearchBlockSize <= 0 || numOutLightmaps < LIGHTMAP_RESERVE_COUNT ) {
				i = 0;
			}
			else{
				i = ( ( numOutLightmaps - LIGHTMAP_RESERVE_COUNT ) / lightmapSearchBlockSize ) * lightmapSearchBlockSize;
			}
			for ( ; i < numOutLightmaps; ++i )
			{
				/* get the output lightmap */
				olm = &outLightmaps[ i ];

				/* simple early out test */
				if ( olm->freeLuxels < lm->used ) {
					continue;
				}

				/* if fast allocation, skip lightmap files that are more than 90% complete */
				if ( fastAllocate ) {
					if ( olm->freeLuxels < ( olm->customWidth * olm->customHeight ) / 10 ) {
						continue;
					}
				}

				/* don't store non-custom raw lightmaps on custom bsp lightmaps */
				if ( olm->customWidth != lm->customWidth ||
				     olm->customHeight != lm->customHeight ) {
					continue;
				}

				/* set maxs */
				if ( lm->solid[ lightmapNum ] ) {
					xMax = olm->customWidth;
					yMax = olm->customHeight;
				}
				else
				{
					xMax = ( olm->customWidth - lm->w ) + 1;
					yMax = ( olm->customHeight - lm->h ) + 1;
				}

				/* if fast allocation, do not test allocation on every pixels, especially for large lightmaps */
				if ( fastAllocate ) {
					xIncrement = std::max( 1, lm->w / 15 );
					yIncrement = std::max( 1, lm->h / 15 );
				}
				else {
					xIncrement = 1;
					yIncrement = 1;
				}

				/* walk the origin around the lightmap */
				for ( y = 0; y < yMax; y += yIncrement )
				{
					for ( x = 0; x < xMax; x += xIncrement )
					{
						/* find a fine tract of lauhnd */
						ok = TestOutLightmapStamp( lm, lightmapNum, olm, x, y );

						if ( ok ) {
							break;
						}
					}

					if ( ok ) {
						break;
					}
				}

				if ( ok ) {
					break;
				}

				/* reset x and y */
				x = 0;
				y = 0;
			}
		}

		/* no match? */
		if ( !ok ) {
			/* allocate LIGHTMAP_RESERVE_COUNT new output lightmaps */
			numOutLightmaps += LIGHTMAP_RESERVE_COUNT;
			olm = safe_malloc( numOutLightmaps * sizeof( outLightmap_t ) );

			if ( outLightmaps != nullptr && numOutLightmaps > LIGHTMAP_RESERVE_COUNT ) {
				memcpy( olm, outLightmaps, ( numOutLightmaps - LIGHTMAP_RESERVE_COUNT ) * sizeof( outLightmap_t ) );
				free( outLightmaps );
			}
			outLightmaps = olm;

			/* initialize both out lightmaps */
			for ( k = numOutLightmaps - LIGHTMAP_RESERVE_COUNT; k < numOutLightmaps; ++k )
				SetupOutLightmap( lm, &outLightmaps[ k ] );

			/* set out lightmap */
			i = numOutLightmaps - LIGHTMAP_RESERVE_COUNT;
			olm = &outLightmaps[ i ];

			/* set stamp xy origin to the first surface lightmap */
			if ( lightmapNum > 0 ) {
				x = lm->lightmapX[ 0 ];
				y = lm->lightmapY[ 0 ];
			}
		}

		/* if this is a style-using lightmap, it must be exported */
		if ( lightmapNum > 0 && g_game->load != LoadRBSPFile ) {
			olm->extLightmapNum = 0;
		}

		/* add the surface lightmap to the bsp lightmap */
		lm->outLightmapNums[ lightmapNum ] = i;
		lm->lightmapX[ lightmapNum ] = x;
		lm->lightmapY[ lightmapNum ] = y;
		olm->numLightmaps++;

		/* add shaders */
		for ( i = 0; i < lm->numLightSurfaces; ++i )
		{
			/* get surface info */
			info = &surfaceInfos[ lightSurfaces[ lm->firstLightSurface + i ] ];

			/* test for shader */
			for ( j = 0; j < olm->numShaders; ++j )
			{
				if ( olm->shaders[ j ] == info->si ) {
					break;
				}
			}

			/* if it doesn't exist, add it */
			if ( j >= olm->numShaders && olm->numShaders < MAX_LIGHTMAP_SHADERS ) {
				olm->shaders[ olm->numShaders ] = info->si;
				olm->numShaders++;
				numLightmapShaders++;
			}
		}

		/* set maxs */
		if ( lm->solid[ lightmapNum ] ) {
			xMax = 1;
			yMax = 1;
		}
		else
		{
			xMax = lm->w;
			yMax = lm->h;
		}

		/* mark the bits used */
		for ( y = 0; y < yMax; ++y )
		{
			for ( x = 0; x < xMax; ++x )
			{
				/* get luxel */
				const Vector3& luxel = lm->getBspLuxel( lightmapNum, x, y );
				if ( luxel[ 0 ] < 0 && !lm->solid[ lightmapNum ] ) {
					continue;
				}
				Vector3 color;
				/* set minimum light */
				if ( lm->solid[ lightmapNum ] ) {
					if ( debug ) {
						color = { 255.0f, 0.0f, 0.0f };
					}
					else{
						color = lm->solidColor[ lightmapNum ];
					}
				}
				else{

					color = luxel;
				}

				/* styles are not affected by minlight */
				if ( lightmapNum == 0 ) {
					for ( i = 0; i < 3; ++i )
					{
						value_maximize( color[ i ], minLight[ i ] );
					}
				}

				/* get bsp lightmap coords  */
				ox = x + lm->lightmapX[ lightmapNum ];
				oy = y + lm->lightmapY[ lightmapNum ];

				/* flag pixel as used */
				bit_enable( olm->lightBits, ( oy * olm->customWidth ) + ox );
				olm->freeLuxels--;

				/* store color */
				olm->bspLightBytes[ oy * olm->customWidth + ox] = ColorToBytes( color, 1, lm->brightness );

				/* store direction */
				if ( deluxemap ) {
					/* normalize average light direction */
					const Vector3 direction = VectorNormalized( lm->getBspDeluxel( x, y ) * 1000.0f ) * 127.5f;
					olm->bspDirBytes[ oy * olm->customWidth + ox ] = direction + Vector3( 127.5f );
				}
			}
		}
	}
}



/*
   CompareRawLightmap
   compare functor for std::sort()
 */

struct CompareRawLightmap
{
	bool operator()( const int a, const int b ) const {
		int diff;

		/* get lightmaps */
		const rawLightmap_t& alm = rawLightmaps[ a ];
		const rawLightmap_t& blm = rawLightmaps[ b ];

		/* get min number of surfaces */
		const int min = std::min( alm.numLightSurfaces, blm.numLightSurfaces );

		/* iterate */
		for ( int i = 0; i < min; ++i )
		{
			/* get surface info */
			const surfaceInfo_t& aInfo = surfaceInfos[ lightSurfaces[ alm.firstLightSurface + i ] ];
			const surfaceInfo_t& bInfo = surfaceInfos[ lightSurfaces[ blm.firstLightSurface + i ] ];

			/* compare shader names */
			diff = strcmp( aInfo.si->shader, bInfo.si->shader );
			if ( diff != 0 ) {
				return diff < 0;
			}
		}

		/* test style count */
		diff = 0;
		for ( int i = 0; i < MAX_LIGHTMAPS; ++i )
			diff += blm.styles[ i ] - alm.styles[ i ];
		if ( diff != 0 ) {
			return diff < 0;
		}

		/* compare size */
		diff = ( blm.w * blm.h ) - ( alm.w * alm.h );
		if ( diff != 0 ) {
			return diff < 0;
		}
		/* must be equivalent */
		return false;
	}
};

static void FillOutLightmap( outLightmap_t *olm ){
	int x, y;
	int ofs;
	int cnt, filled;
	byte *lightBitsNew = nullptr;
	Vector3b *lightBytesNew = nullptr;
	Vector3b *dirBytesNew = nullptr;
	const size_t size = olm->customWidth * olm->customHeight * sizeof( Vector3b );

	lightBitsNew = safe_malloc( ( olm->customWidth * olm->customHeight + 8 ) / 8 );
	lightBytesNew = safe_malloc( size );
	if ( deluxemap ) {
		dirBytesNew = safe_malloc( size );
	}

	/*
	   memset( olm->lightBits, 0, ( olm->customWidth * olm->customHeight + 8 ) / 8 );
	    olm->lightBits[0] |= 1;
	    olm->lightBits[( 10 * olm->customWidth + 30 ) >> 3] |= 1 << ( ( 10 * olm->customWidth + 30 ) & 7 );
	   memset( olm->bspLightBytes, 0, olm->customWidth * olm->customHeight * 3 );
	    olm->bspLightBytes[0] = 255;
	    olm->bspLightBytes[( 10 * olm->customWidth + 30 ) * 3 + 2] = 255;
	 */

	memcpy( lightBitsNew, olm->lightBits, ( olm->customWidth * olm->customHeight + 8 ) / 8 );
	memcpy( lightBytesNew, olm->bspLightBytes, size );
	if ( deluxemap ) {
		memcpy( dirBytesNew, olm->bspDirBytes, size );
	}

	for (;; )
	{
		filled = 0;
		for ( y = 0; y < olm->customHeight; ++y )
		{
			for ( x = 0; x < olm->customWidth; ++x )
			{
				ofs = y * olm->customWidth + x;
				if ( bit_is_enabled( olm->lightBits, ofs ) ) { /* already filled */
					continue;
				}
				cnt = 0;
				Vector3 dir_sum( 0 ), light_sum( 0 );

				/* try all four neighbors */
				ofs = ( ( y + olm->customHeight - 1 ) % olm->customHeight ) * olm->customWidth + x;
				if ( bit_is_enabled( olm->lightBits, ofs ) ) { /* already filled */
					++cnt;
					light_sum += olm->bspLightBytes[ofs];
					if ( deluxemap ) {
						dir_sum += olm->bspDirBytes[ofs];
					}
				}

				ofs = ( ( y + 1 ) % olm->customHeight ) * olm->customWidth + x;
				if ( bit_is_enabled( olm->lightBits, ofs ) ) { /* already filled */
					++cnt;
					light_sum += olm->bspLightBytes[ofs];
					if ( deluxemap ) {
						dir_sum += olm->bspDirBytes[ofs];
					}
				}

				ofs = y * olm->customWidth + ( x + olm->customWidth - 1 ) % olm->customWidth;
				if ( bit_is_enabled( olm->lightBits, ofs ) ) { /* already filled */
					++cnt;
					light_sum += olm->bspLightBytes[ofs];
					if ( deluxemap ) {
						dir_sum += olm->bspDirBytes[ofs];
					}
				}

				ofs = y * olm->customWidth + ( x + 1 ) % olm->customWidth;
				if ( bit_is_enabled( olm->lightBits, ofs ) ) { /* already filled */
					++cnt;
					light_sum += olm->bspLightBytes[ofs];
					if ( deluxemap ) {
						dir_sum += olm->bspDirBytes[ofs];
					}
				}

				if ( cnt ) {
					++filled;
					ofs = y * olm->customWidth + x;
					bit_enable( lightBitsNew, ofs );
					lightBytesNew[ofs] = light_sum * ( 1.0 / cnt );
					if ( deluxemap ) {
						dirBytesNew[ofs] = dir_sum * ( 1.0 / cnt );
					}
				}
			}
		}

		if ( !filled ) {
			break;
		}

		memcpy( olm->lightBits, lightBitsNew, ( olm->customWidth * olm->customHeight + 8 ) / 8 );
		memcpy( olm->bspLightBytes, lightBytesNew, size );
		if ( deluxemap ) {
			memcpy( olm->bspDirBytes, dirBytesNew, size );
		}
	}

	free( lightBitsNew );
	free( lightBytesNew );
	if ( deluxemap ) {
		free( dirBytesNew );
	}
}

/*
   StoreSurfaceLightmaps()
   stores the surface lightmaps into the bsp as byte rgb triplets
 */

void StoreSurfaceLightmaps( bool fastAllocate, bool storeForReal ){
	int i, j, k, x, y, lx, ly, sx, sy, mappedSamples;
	int lightmapNum, lightmapNum2;
	float               samples, occludedSamples;
	Vector3 sample, occludedSample, dirSample;
	byte                *lb;
	int numUsed, numTwins, numTwinLuxels, numStored;
	float lmx, lmy, efficiency;
	rawLightmap_t       *lm, *lm2;
	outLightmap_t       *olm;
	bspDrawVert_t       *dv, *ydv, *dvParent;
	char dirname[ 1024 ], filename[ 1024 ];
	char lightmapName[ 128 ];
	const char              *rgbGenValues[ 256 ] = {0};
	const char              *alphaGenValues[ 256 ] = {0};


	/* note it */
	Sys_Printf( "--- StoreSurfaceLightmaps ---\n" );

	/* setup */
	if ( lmCustomDir ) {
		strcpy( dirname, lmCustomDir );
	}
	else
	{
		strcpy( dirname, source );
		StripExtension( dirname );
	}

	/* -----------------------------------------------------------------
	   average the sampled luxels into the bsp luxels
	   ----------------------------------------------------------------- */

	/* note it */
	Sys_Printf( "Subsampling..." );

	Timer timer;

	/* walk the list of raw lightmaps */
	numUsed = 0;
	numTwins = 0;
	numTwinLuxels = 0;
	numSolidLightmaps = 0;
	for ( i = 0; i < numRawLightmaps; ++i )
	{
		/* get lightmap */
		lm = &rawLightmaps[ i ];

		/* walk individual lightmaps */
		for ( lightmapNum = 0; lightmapNum < MAX_LIGHTMAPS; ++lightmapNum )
		{
			/* early outs */
			if ( lm->superLuxels[ lightmapNum ] == nullptr ) {
				continue;
			}

			/* allocate bsp luxel storage */
			if ( lm->bspLuxels[ lightmapNum ] == nullptr ) {
				const size_t size = lm->w * lm->h * sizeof( *( lm->bspLuxels[ 0 ] ) );
				lm->bspLuxels[ lightmapNum ] = safe_calloc( size );
			}

			/* allocate radiosity lightmap storage */
			if ( bounce ) {
				const size_t size = lm->w * lm->h * sizeof( *lm->radLuxels[ 0 ] );
				if ( lm->radLuxels[ lightmapNum ] == nullptr ) {
					lm->radLuxels[ lightmapNum ] = safe_malloc( size );
				}
				memset( lm->radLuxels[ lightmapNum ], 0, size );
			}

			/* average supersampled luxels */
			for ( y = 0; y < lm->h; ++y )
			{
				for ( x = 0; x < lm->w; ++x )
				{
					/* subsample */
					samples = 0;
					occludedSamples = 0;
					mappedSamples = 0;
					sample.set( 0 );
					occludedSample.set( 0 );
					dirSample.set( 0 );
					for ( ly = 0; ly < superSample; ++ly )
					{
						for ( lx = 0; lx < superSample; ++lx )
						{
							/* sample luxel */
							sx = x * superSample + lx;
							sy = y * superSample + ly;
							SuperLuxel& luxel = lm->getSuperLuxel( lightmapNum, sx, sy );
							int& cluster = lm->getSuperCluster( sx, sy );

							/* sample deluxemap */
							if ( deluxemap && lightmapNum == 0 ) {
								dirSample += lm->getSuperDeluxel( sx, sy );
							}

							/* keep track of used/occluded samples */
							if ( cluster != CLUSTER_UNMAPPED ) {
								mappedSamples++;
							}

							/* handle lightmap border? */
							if ( lightmapBorder && ( sx == 0 || sx == ( lm->sw - 1 ) || sy == 0 || sy == ( lm->sh - 1 ) ) && luxel.count > 0 ) {
								sample = { 255, 0, 0 };
								samples += 1;
							}

							/* handle debug */
							else if ( debug && cluster < CLUSTER_NORMAL ) {
								if ( cluster == CLUSTER_UNMAPPED ) {
									luxel.value = { 255, 204, 0 };
								}
								else if ( cluster == CLUSTER_OCCLUDED ) {
									luxel.value = { 255, 0, 255 };
								}
								else if ( cluster == CLUSTER_FLOODED ) {
									luxel.value = { 0, 32, 255 };
								}
								occludedSample += luxel.value;
								occludedSamples += 1;
							}

							/* normal luxel handling */
							else if ( luxel.count > 0 ) {
								/* handle lit or flooded luxels */
								if ( cluster > CLUSTER_NORMAL || cluster == CLUSTER_FLOODED ) {
									sample += luxel.value;
									samples += luxel.count;
								}

								/* handle occluded or unmapped luxels */
								else
								{
									occludedSample += luxel.value;
									occludedSamples += luxel.count;
								}

								/* handle style debugging */
								if ( debug && lightmapNum > 0 && x < 2 && y < 2 ) {
									sample = debugColors[ 0 ];
									samples = 1;
								}
							}
						}
					}

					/* only use occluded samples if necessary */
					if ( samples <= 0 ) {
						sample = occludedSample;
						samples = occludedSamples;
					}

					/* get luxels */
					SuperLuxel& luxel = lm->getSuperLuxel( lightmapNum, x, y );

					/* store light direction */
					if ( deluxemap && lightmapNum == 0 ) {
						lm->getSuperDeluxel( x, y ) = dirSample;
					}

					/* store the sample back in super luxels */
					if ( samples > 0.01f ) {
						luxel.value = sample * ( 1.0f / samples );
						luxel.count = 1;
					}

					/* if any samples were mapped in any way, store ambient color */
					else if ( mappedSamples > 0 ) {
						if ( lightmapNum == 0 ) {
							luxel.value = lm->ambientColor;
						}
						else{
							luxel.value.set( 0 );
						}
						luxel.count = 1;
					}

					/* store a bogus value to be fixed later */
					else
					{
						luxel.value.set( 0 );
						luxel.count = -1;
					}
				}
			}

			/* setup */
			lm->used = 0;
			MinMax colorMinmax;

			/* clean up and store into bsp luxels */
			for ( y = 0; y < lm->h; ++y )
			{
				for ( x = 0; x < lm->w; ++x )
				{
					/* get luxels */
					const SuperLuxel& luxel = lm->getSuperLuxel( lightmapNum, x, y );

					/* copy light direction */
					if ( deluxemap && lightmapNum == 0 ) {
						dirSample = lm->getSuperDeluxel( x, y );
					}

					/* is this a valid sample? */
					if ( luxel.count > 0 ) {
						sample = luxel.value;
						samples = luxel.count;
						numUsed++;
						lm->used++;

						/* fix negative samples */
						for ( j = 0; j < 3; ++j )
						{
							value_maximize( sample[ j ], 0.0f );
						}
					}
					else
					{
						/* nick an average value from the neighbors */
						sample.set( 0 );
						dirSample.set( 0 );
						samples = 0;

						/* fixme: why is this disabled?? */
						for ( sy = ( y - 1 ); sy <= ( y + 1 ); ++sy )
						{
							if ( sy < 0 || sy >= lm->h ) {
								continue;
							}

							for ( sx = ( x - 1 ); sx <= ( x + 1 ); ++sx )
							{
								if ( sx < 0 || sx >= lm->w || ( sx == x && sy == y ) ) {
									continue;
								}

								/* get neighbor's particulars */
								const SuperLuxel& luxel = lm->getSuperLuxel( lightmapNum, sx, sy );
								if ( luxel.count < 0 ) {
									continue;
								}
								sample += luxel.value;
								samples += luxel.count;
							}
						}

						/* no samples? */
						if ( samples == 0 ) {
							sample.set( -1 );
							samples = 1;
						}
						else
						{
							numUsed++;
							lm->used++;

							/* fix negative samples */
							for ( j = 0; j < 3; ++j )
							{
								value_maximize( sample[ j ], 0.0f );
							}
						}
					}

					/* scale the sample */
					sample *= ( 1.0f / samples );

					/* store the sample in the radiosity luxels */
					if ( bounce > 0 ) {
						lm->getRadLuxel( lightmapNum, x, y ) = sample;

						/* if only storing bounced light, early out here */
						if ( bounceOnly && !bouncing ) {
							continue;
						}
					}

					/* store the sample in the bsp luxels */
					Vector3& bspLuxel = lm->getBspLuxel( lightmapNum, x, y );

					bspLuxel += sample;
					if ( deluxemap && lightmapNum == 0 ) {
						lm->getBspDeluxel( x, y ) += dirSample;
					}

					/* add color to bounds for solid checking */
					if ( samples > 0 ) {
						colorMinmax.extend( bspLuxel );
					}
				}
			}

			/* set solid color */
			lm->solid[ lightmapNum ] = false;
			lm->solidColor[ lightmapNum ] = colorMinmax.origin();

			/* nocollapse prevents solid lightmaps */
			if ( !noCollapse ) {
				/* check solid color */
				sample = colorMinmax.maxs - colorMinmax.mins;
				if ( ( sample[ 0 ] <= SOLID_EPSILON && sample[ 1 ] <= SOLID_EPSILON && sample[ 2 ] <= SOLID_EPSILON ) ||
				     ( lm->w <= 2 && lm->h <= 2 ) ) { /* small lightmaps get forced to solid color */
					/* set to solid */
					lm->solidColor[ lightmapNum ] = colorMinmax.mins;
					lm->solid[ lightmapNum ] = true;
					numSolidLightmaps++;
				}

				/* if all lightmaps aren't solid, then none of them are solid */
				if ( lm->solid[ lightmapNum ] != lm->solid[ 0 ] ) {
					for ( y = 0; y < MAX_LIGHTMAPS; ++y )
					{
						if ( lm->solid[ y ] ) {
							numSolidLightmaps--;
						}
						lm->solid[ y ] = false;
					}
				}
			}

			/* wrap bsp luxels if necessary */
			if ( lm->wrap[ 0 ] ) {
				for ( y = 0; y < lm->h; ++y )
				{
					Vector3& bspLuxel = lm->getBspLuxel( lightmapNum, 0, y );
					Vector3& bspLuxel2 = lm->getBspLuxel( lightmapNum, lm->w - 1, y );
					bspLuxel = bspLuxel2 = vector3_mid( bspLuxel, bspLuxel2 );
					if ( deluxemap && lightmapNum == 0 ) {
						Vector3& bspDeluxel = lm->getBspDeluxel( 0, y );
						Vector3& bspDeluxel2 = lm->getBspDeluxel( lm->w - 1, y );
						bspDeluxel = bspDeluxel2 = vector3_mid( bspDeluxel, bspDeluxel2 );
					}
				}
			}
			if ( lm->wrap[ 1 ] ) {
				for ( x = 0; x < lm->w; ++x )
				{
					Vector3& bspLuxel = lm->getBspLuxel( lightmapNum, x, 0 );
					Vector3& bspLuxel2 = lm->getBspLuxel( lightmapNum, x, lm->h - 1 );
					bspLuxel = vector3_mid( bspLuxel, bspLuxel2 );
					bspLuxel2 = bspLuxel;
					if ( deluxemap && lightmapNum == 0 ) {
						Vector3& bspDeluxel = lm->getBspDeluxel( x, 0 );
						Vector3& bspDeluxel2 = lm->getBspDeluxel( x, lm->h - 1 );
						bspDeluxel = bspDeluxel2 = vector3_mid( bspDeluxel, bspDeluxel2 );
					}
				}
			}
		}
	}

	Sys_Printf( "%d.", int( timer.elapsed_sec() ) );

	/* -----------------------------------------------------------------
	   convert modelspace deluxemaps to tangentspace
	   ----------------------------------------------------------------- */
	/* note it */
	if ( !bouncing ) {
		if ( deluxemap && deluxemode == 1 ) {
			timer.start();

			Sys_Printf( "converting..." );

			for ( i = 0; i < numRawLightmaps; ++i )
			{
				/* get lightmap */
				lm = &rawLightmaps[ i ];

				/* walk lightmap samples */
				for ( y = 0; y < lm->sh; ++y )
				{
					for ( x = 0; x < lm->sw; ++x )
					{
						/* get normal and deluxel */
						Vector3& bspDeluxel = lm->getBspDeluxel( x, y );

						/* get normal */
						const Vector3 myNormal = lm->getSuperNormal( x, y );

						/* get tangent vectors */
						Vector3 myTangent, myBinormal;
						if ( myNormal[ 0 ] == 0 && myNormal[ 1 ] == 0 ) {
							if ( myNormal.z() == 1 ) {
								myTangent = g_vector3_axis_x;
								myBinormal = g_vector3_axis_y;
							}
							else if ( myNormal.z() == -1 ) {
								myTangent = -g_vector3_axis_x;
								myBinormal = g_vector3_axis_y;
							}
						}
						else
						{
							myTangent = VectorNormalized( vector3_cross( myNormal, g_vector3_axis_z ) );
							myBinormal = VectorNormalized( vector3_cross( myTangent, myNormal ) );
						}

						/* project onto plane */
						myTangent -= myNormal * vector3_dot( myTangent, myNormal );
						myBinormal -= myNormal * vector3_dot( myBinormal, myNormal );

						/* renormalize */
						VectorNormalize( myTangent );
						VectorNormalize( myBinormal );

						/* convert modelspace deluxel to tangentspace */
						dirSample = VectorNormalized( bspDeluxel );

						/* fix tangents to world matrix */
						if ( myNormal.x() > 0 || myNormal.y() < 0 || myNormal.z() < 0 ) {
							vector3_negate( myTangent );
						}

						/* build tangentspace vectors */
						bspDeluxel[0] = vector3_dot( dirSample, myTangent );
						bspDeluxel[1] = vector3_dot( dirSample, myBinormal );
						bspDeluxel[2] = vector3_dot( dirSample, myNormal );
					}
				}
			}

			Sys_Printf( "%d.", int( timer.elapsed_sec() ) );
		}
	}

	/* -----------------------------------------------------------------
	   blend lightmaps
	   ----------------------------------------------------------------- */

#ifdef sdfsdfwq312323
	/* note it */
	Sys_Printf( "blending..." );

	for ( i = 0; i < numRawLightmaps; ++i )
	{
		Vector3 myColor;
		float myBrightness;

		/* get lightmap */
		lm = &rawLightmaps[ i ];

		/* walk individual lightmaps */
		for ( lightmapNum = 0; lightmapNum < MAX_LIGHTMAPS; ++lightmapNum )
		{
			/* early outs */
			if ( lm->superLuxels[ lightmapNum ] == nullptr ) {
				continue;
			}

			/* walk lightmap samples */
			for ( y = 0; y < lm->sh; ++y )
			{
				for ( x = 0; x < lm->sw; ++x )
				{
					/* get luxel */
					Vector3& bspLuxel = lm->getBspLuxel( lightmapNum, x, y );

					/* get color */
					myColor = VectorNormalized( bspLuxel );
					myBrightness = vector3_length( bspLuxel );
					myBrightness *= ( 1 / 127.0f );
					myBrightness = myBrightness * myBrightness;
					myBrightness *= 127.0f;
					bspLuxel = myColor * myBrightness;
				}
			}
		}
	}
#endif

	/* -----------------------------------------------------------------
	   collapse non-unique lightmaps
	   ----------------------------------------------------------------- */

	if ( storeForReal && !noCollapse && !deluxemap ) {
		/* note it */
		Sys_Printf( "collapsing..." );

		timer.start();

		/* set all twin refs to null */
		for ( i = 0; i < numRawLightmaps; ++i )
		{
			rawLightmaps[ i ].twins.fill( nullptr );
			rawLightmaps[ i ].twinNums.fill( -1 );
			rawLightmaps[ i ].numStyledTwins = 0;
		}

		/* walk the list of raw lightmaps */
		for ( i = 0; i < numRawLightmaps; ++i )
		{
			/* get lightmap */
			lm = &rawLightmaps[ i ];

			/* walk lightmaps */
			for ( lightmapNum = 0; lightmapNum < MAX_LIGHTMAPS; ++lightmapNum )
			{
				/* early outs */
				if ( lm->bspLuxels[ lightmapNum ] == nullptr ||
				     lm->twins[ lightmapNum ] != nullptr ) {
					continue;
				}

				/* find all lightmaps that are virtually identical to this one */
				for ( j = i + 1; j < numRawLightmaps; ++j )
				{
					/* get lightmap */
					lm2 = &rawLightmaps[ j ];

					/* walk lightmaps */
					for ( lightmapNum2 = 0; lightmapNum2 < MAX_LIGHTMAPS; ++lightmapNum2 )
					{
						/* early outs */
						if ( lm2->bspLuxels[ lightmapNum2 ] == nullptr ||
						     lm2->twins[ lightmapNum2 ] != nullptr ) {
							continue;
						}

						/* compare them */
						if ( CompareBSPLuxels( lm, lightmapNum, lm2, lightmapNum2 ) ) {
							/* merge and set twin */
							if ( MergeBSPLuxels( lm, lightmapNum, lm2, lightmapNum2 ) ) {
								lm2->twins[ lightmapNum2 ] = lm;
								lm2->twinNums[ lightmapNum2 ] = lightmapNum;
								numTwins++;
								numTwinLuxels += ( lm->w * lm->h );

								/* count styled twins */
								if ( lightmapNum > 0 ) {
									lm->numStyledTwins++;
								}
							}
						}
					}
				}
			}
		}

		Sys_Printf( "%d.", int( timer.elapsed_sec() ) );
	}

	/* -----------------------------------------------------------------
	   sort raw lightmaps by shader
	   ----------------------------------------------------------------- */

	if ( storeForReal ) {
		/* note it */
		Sys_Printf( "sorting..." );

		timer.start();

		/* allocate a new sorted list */
		if ( sortLightmaps == nullptr ) {
			sortLightmaps = safe_malloc( numRawLightmaps * sizeof( int ) );
		}

		/* fill it out and sort it */
		for ( i = 0; i < numRawLightmaps; ++i )
			sortLightmaps[ i ] = i;
		std::ranges::sort( Span( sortLightmaps, numRawLightmaps ), CompareRawLightmap() );

		Sys_Printf( "%d.", int( timer.elapsed_sec() ) );
	}

	/* -----------------------------------------------------------------
	   allocate output lightmaps
	   ----------------------------------------------------------------- */

	if ( storeForReal ) {
		/* note it */
		Sys_Printf( "allocating..." );

		timer.start();

		/* kill all existing output lightmaps */
		if ( outLightmaps != nullptr ) {
			for ( i = 0; i < numOutLightmaps; ++i )
			{
				free( outLightmaps[ i ].lightBits );
				free( outLightmaps[ i ].bspLightBytes );
			}
			free( outLightmaps );
			outLightmaps = nullptr;
		}

		numLightmapShaders = 0;
		numOutLightmaps = 0;
		numBSPLightmaps = 0;
		numExtLightmaps = 0;

		/* find output lightmap */
		for ( i = 0; i < numRawLightmaps; ++i )
		{
			lm = &rawLightmaps[ sortLightmaps[ i ] ];
			FindOutLightmaps( lm, fastAllocate );
		}

		/* set output numbers in twinned lightmaps */
		for ( i = 0; i < numRawLightmaps; ++i )
		{
			/* get lightmap */
			lm = &rawLightmaps[ sortLightmaps[ i ] ];

			/* walk lightmaps */
			for ( lightmapNum = 0; lightmapNum < MAX_LIGHTMAPS; ++lightmapNum )
			{
				/* get twin */
				lm2 = lm->twins[ lightmapNum ];
				if ( lm2 == nullptr ) {
					continue;
				}
				lightmapNum2 = lm->twinNums[ lightmapNum ];

				/* find output lightmap from twin */
				lm->outLightmapNums[ lightmapNum ] = lm2->outLightmapNums[ lightmapNum2 ];
				lm->lightmapX[ lightmapNum ] = lm2->lightmapX[ lightmapNum2 ];
				lm->lightmapY[ lightmapNum ] = lm2->lightmapY[ lightmapNum2 ];
			}
		}

		Sys_Printf( "%d.", int( timer.elapsed_sec() ) );
	}

	/* -----------------------------------------------------------------
	   store output lightmaps
	   ----------------------------------------------------------------- */

	if ( storeForReal ) {
		/* note it */
		Sys_Printf( "storing..." );

		timer.start();

		/* count the bsp lightmaps and allocate space */
		const size_t gameLmSize = g_game->lightmapSize * g_game->lightmapSize * sizeof( Vector3b );
		if ( numBSPLightmaps == 0 || externalLightmaps ) {
			bspLightBytes.clear();
		}
		else
		{
			bspLightBytes = decltype( bspLightBytes )( numBSPLightmaps * gameLmSize, 0 );
		}

		/* walk the list of output lightmaps */
		for ( i = 0; i < numOutLightmaps; ++i )
		{
			/* get output lightmap */
			olm = &outLightmaps[ i ];

			/* fill output lightmap */
			if ( lightmapFill ) {
				FillOutLightmap( olm );
			}
			else if( lightmapPink ){
				for ( x = 0; x < olm->customHeight * olm->customWidth; ++x ){
					if ( !bit_is_enabled( olm->lightBits, x ) ) { /* not filled */
						olm->bspLightBytes[x] = { 255, 0, 255 };
					}
				}
			}

			/* is this a valid bsp lightmap? */
			if ( olm->lightmapNum >= 0 && !externalLightmaps ) {
				/* copy lighting data */
				lb = bspLightBytes.data() + ( olm->lightmapNum * gameLmSize );
				memcpy( lb, olm->bspLightBytes, gameLmSize );

				/* copy direction data */
				if ( deluxemap ) {
					lb = bspLightBytes.data() + ( ( olm->lightmapNum + 1 ) * gameLmSize );
					memcpy( lb, olm->bspDirBytes, gameLmSize );
				}
			}

			/* external lightmap? */
			if ( olm->lightmapNum < 0 || olm->extLightmapNum >= 0 || externalLightmaps ) {
				/* make a directory for the lightmaps */
				Q_mkdir( dirname );

				/* set external lightmap number */
				olm->extLightmapNum = numExtLightmaps;

				/* write lightmap */
				sprintf( filename, "%s/" EXTERNAL_LIGHTMAP, dirname, numExtLightmaps );
				Sys_FPrintf( SYS_VRB, "\nwriting %s", filename );
				WriteTGA24( filename, olm->bspLightBytes->data(), olm->customWidth, olm->customHeight, true );
				numExtLightmaps++;

				/* write deluxemap */
				if ( deluxemap ) {
					sprintf( filename, "%s/" EXTERNAL_LIGHTMAP, dirname, numExtLightmaps );
					Sys_FPrintf( SYS_VRB, "\nwriting %s", filename );
					WriteTGA24( filename, olm->bspDirBytes->data(), olm->customWidth, olm->customHeight, true );
					numExtLightmaps++;

					if ( debugDeluxemap ) {
						olm->extLightmapNum++;
					}
				}
			}
		}

		if ( numExtLightmaps > 0 ) {
			Sys_Printf( "\n" );
		}

		/* delete unused external lightmaps */
		for ( i = numExtLightmaps; i; ++i )
		{
			/* determine if file exists */
			sprintf( filename, "%s/" EXTERNAL_LIGHTMAP, dirname, i );
			if ( !FileExists( filename ) ) {
				break;
			}

			/* delete it */
			remove( filename );
		}

		Sys_Printf( "%d.", int( timer.elapsed_sec() ) );
	}

	/* -----------------------------------------------------------------
	   project the lightmaps onto the bsp surfaces
	   ----------------------------------------------------------------- */

	if ( storeForReal ) {
		/* note it */
		Sys_Printf( "projecting..." );

		timer.start();

		/* walk the list of surfaces */
		for ( size_t i = 0; i < bspDrawSurfaces.size(); ++i )
		{
			/* get the surface and info */
			bspDrawSurface_t& ds = bspDrawSurfaces[ i ];
			const surfaceInfo_t& info = surfaceInfos[ i ];
			lm = info.lm;
			olm = nullptr;

			/* handle surfaces with identical parent */
			if ( info.parentSurfaceNum >= 0 ) {
				/* preserve original data and get parent */
				const bspDrawSurface_t dsTemp( ds );
				const bspDrawSurface_t& parent = bspDrawSurfaces[ info.parentSurfaceNum ];
				/* overwrite child with parent data */
				ds = parent;

				/* restore key parts */
				ds.fogNum = dsTemp.fogNum;
				ds.firstVert = dsTemp.firstVert;
				ds.firstIndex = dsTemp.firstIndex;
				ds.lightmapVecs = dsTemp.lightmapVecs;

				/* set vertex data */
				dv = &bspDrawVerts[ ds.firstVert ];
				dvParent = &bspDrawVerts[ parent.firstVert ];
				for ( j = 0; j < ds.numVerts; ++j )
				{
					dv[ j ].lightmap = dvParent[ j ].lightmap;
					dv[ j ].color = dvParent[ j ].color;
				}

				/* skip the rest */
				continue;
			}

			/* handle vertex lit or approximated surfaces */
			else if ( lm == nullptr || lm->outLightmapNums[ 0 ] < 0 ) {
				ds.lightmapStyles = ds.vertexStyles;
				ds.lightmapNum.fill( LIGHTMAP_BY_VERTEX );
			}

			/* handle lightmapped surfaces */
			else
			{
				/* walk lightmaps */
				for ( lightmapNum = 0; lightmapNum < MAX_LIGHTMAPS; ++lightmapNum )
				{
					/* set style */
					ds.lightmapStyles[ lightmapNum ] = lm->styles[ lightmapNum ];

					/* handle unused style */
					if ( lm->styles[ lightmapNum ] == LS_NONE || lm->outLightmapNums[ lightmapNum ] < 0 ) {
						ds.lightmapNum[ lightmapNum ] = LIGHTMAP_BY_VERTEX;
						continue;
					}

					/* get output lightmap */
					olm = &outLightmaps[ lm->outLightmapNums[ lightmapNum ] ];

					/* set bsp lightmap number */
					ds.lightmapNum[ lightmapNum ] = olm->lightmapNum;

					/* deluxemap debugging makes the deluxemap visible */
					if ( deluxemap && debugDeluxemap && lightmapNum == 0 ) {
						ds.lightmapNum[ lightmapNum ]++;
					}

					/* calc lightmap origin in texture space */
					lmx = (float) lm->lightmapX[ lightmapNum ] / olm->customWidth;
					lmy = (float) lm->lightmapY[ lightmapNum ] / olm->customHeight;

					/* calc lightmap st coords */
					dv = &bspDrawVerts[ ds.firstVert ];
					ydv = &yDrawVerts[ ds.firstVert ];
					for ( j = 0; j < ds.numVerts; ++j )
					{
						if ( lm->solid[ lightmapNum ] ) {
							dv[ j ].lightmap[ lightmapNum ][ 0 ] = lmx + ( 0.5f / olm->customWidth );
							dv[ j ].lightmap[ lightmapNum ][ 1 ] = lmy + ( 0.5f / olm->customWidth );
						}
						else
						{
							dv[ j ].lightmap[ lightmapNum ][ 0 ] = lmx + ( ydv[ j ].lightmap[ 0 ][ 0 ] / ( superSample * olm->customWidth ) );
							dv[ j ].lightmap[ lightmapNum ][ 1 ] = lmy + ( ydv[ j ].lightmap[ 0 ][ 1 ] / ( superSample * olm->customHeight ) );
						}
					}
				}
			}

			/* store vertex colors */
			dv = &bspDrawVerts[ ds.firstVert ];
			for ( j = 0; j < ds.numVerts; ++j )
			{
				/* walk lightmaps */
				for ( lightmapNum = 0; lightmapNum < MAX_LIGHTMAPS; ++lightmapNum )
				{
					Vector3 color;
					/* handle unused style */
					if ( ds.vertexStyles[ lightmapNum ] == LS_NONE ) {
						color.set( 0 );
					}
					else
					{
						/* get vertex color */
						color = getVertexLuxel( lightmapNum, ds.firstVert + j );

						/* set minimum light */
						if ( lightmapNum == 0 ) {
							for ( k = 0; k < 3; ++k )
								value_maximize( color[ k ], minVertexLight[ k ] );
						}
					}

					/* store to bytes */
					if ( !info.si->noVertexLight ) {
						dv[ j ].color[ lightmapNum ].rgb() = ColorToBytes( color, info.si->vertexScale );
					}
				}
			}

			/* surfaces with styled lightmaps and a style marker get a custom generated shader (fixme: make this work with external lightmaps) */
			if ( olm != nullptr && lm != nullptr && lm->styles[ 1 ] != LS_NONE && g_game->load != LoadRBSPFile ) { //%	info.si->styleMarker > 0 )
				char key[ 32 ], styleStage[ 512 ], styleStages[ 4096 ], rgbGen[ 128 ], alphaGen[ 128 ];


				/* setup */
				sprintf( styleStages, "\n\t// Q3Map2 custom lightstyle stage(s)\n" );
				dv = &bspDrawVerts[ ds.firstVert ];

				/* depthFunc equal? */
				const bool dfEqual = ( info.si->styleMarker == 2 || info.si->implicitMap == EImplicitMap::Masked );

				/* generate stages for styled lightmaps */
				for ( lightmapNum = 1; lightmapNum < MAX_LIGHTMAPS; ++lightmapNum )
				{
					/* early out */
					const int style = lm->styles[ lightmapNum ];
					if ( style == LS_NONE || lm->outLightmapNums[ lightmapNum ] < 0 ) {
						continue;
					}

					/* get output lightmap */
					olm = &outLightmaps[ lm->outLightmapNums[ lightmapNum ] ];

					/* lightmap name */
					if ( lm->outLightmapNums[ lightmapNum ] == lm->outLightmapNums[ 0 ] ) {
						strcpy( lightmapName, "$lightmap" );
					}
					else{
						sprintf( lightmapName, "maps/%s/" EXTERNAL_LIGHTMAP, mapName.c_str(), olm->extLightmapNum );
					}

					/* get rgbgen string */
					if ( rgbGenValues[ style ] == nullptr ) {
						sprintf( key, "_style%drgbgen", style );
						rgbGenValues[ style ] = entities[ 0 ].valueForKey( key );
						if ( strEmpty( rgbGenValues[ style ] ) ) {
							rgbGenValues[ style ] = "wave noise 0.5 1 0 5.37";
						}
					}
					strClear( rgbGen );
					if ( !strEmpty( rgbGenValues[ style ] ) ) {
						sprintf( rgbGen, "\t\trgbGen %s // style %d\n", rgbGenValues[ style ], style );
					}
					else{
						strClear( rgbGen );
					}

					/* get alphagen string */
					if ( alphaGenValues[ style ] == nullptr ) {
						sprintf( key, "_style%dalphagen", style );
						alphaGenValues[ style ] = entities[ 0 ].valueForKey( key );
					}
					if ( !strEmpty( alphaGenValues[ style ] ) ) {
						sprintf( alphaGen, "\t\talphaGen %s // style %d\n", alphaGenValues[ style ], style );
					}
					else{
						strClear( alphaGen );
					}

					/* calculate st offset */
					const Vector2 lmxy = dv[ 0 ].lightmap[ lightmapNum ] - dv[ 0 ].lightmap[ 0 ];

					/* create additional stage */
					if ( lmxy.x() == 0 && lmxy.y() == 0 ) {
						sprintf( styleStage, "\t{\n"
						                     "\t\tmap %s\n"                                      /* lightmap */
						                     "\t\tblendFunc GL_SRC_ALPHA GL_ONE\n"
						                     "%s"                                                /* depthFunc equal */
						                     "%s"                                                /* rgbGen */
						                     "%s"                                                /* alphaGen */
						                     "\t\ttcGen lightmap\n"
						                     "\t}\n",
						         lightmapName,
						         ( dfEqual ? "\t\tdepthFunc equal\n" : "" ),
						         rgbGen,
						         alphaGen );
					}
					else
					{
						sprintf( styleStage, "\t{\n"
						                     "\t\tmap %s\n"                                      /* lightmap */
						                     "\t\tblendFunc GL_SRC_ALPHA GL_ONE\n"
						                     "%s"                                                /* depthFunc equal */
						                     "%s"                                                /* rgbGen */
						                     "%s"                                                /* alphaGen */
						                     "\t\ttcGen lightmap\n"
						                     "\t\ttcMod transform 1 0 0 1 %1.5f %1.5f\n"         /* st offset */
						                     "\t}\n",
						         lightmapName,
						         ( dfEqual ? "\t\tdepthFunc equal\n" : "" ),
						         rgbGen,
						         alphaGen,
						         lmxy.x(), lmxy.y() );
					}

					/* concatenate */
					strcat( styleStages, styleStage );
				}

				/* create custom shader */
				const shaderInfo_t& csi = CustomShader( info.si, info.si->styleMarker == 2? "q3map_styleMarker2" : "q3map_styleMarker", styleStages );

				/* emit remap command */
				//%	EmitVertexRemapShader( csi.shader, info.si->shader );

				/* store it */
				//%	Sys_Printf( "Emitting: %s (%d", csi.shader, strlen( csi.shader ) );
				const int cont = bspShaders[ ds.shaderNum ].contentFlags;
				const int surf = bspShaders[ ds.shaderNum ].surfaceFlags;
				ds.shaderNum = EmitShader( csi.shader, &cont, &surf );
				//%	Sys_Printf( ")\n" );
			}

			/* devise a custom shader for this surface (fixme: make this work with light styles) */
			else if ( olm != nullptr && lm != nullptr && !externalLightmaps &&
			          ( olm->customWidth != g_game->lightmapSize || olm->customHeight != g_game->lightmapSize ) ) {
				/* get output lightmap */
				olm = &outLightmaps[ lm->outLightmapNums[ 0 ] ];

				/* do some name mangling */
				sprintf( lightmapName, "maps/%s/" EXTERNAL_LIGHTMAP "\n\t\ttcgen lightmap", mapName.c_str(), olm->extLightmapNum );

				/* create custom shader */
				const shaderInfo_t& csi = CustomShader( info.si, "$lightmap", lightmapName );

				/* store it */
				//%	Sys_Printf( "Emitting: %s (%d", csi.shader, strlen( csi.shader ) );
				const int cont = bspShaders[ ds.shaderNum ].contentFlags;
				const int surf = bspShaders[ ds.shaderNum ].surfaceFlags;
				ds.shaderNum = EmitShader( csi.shader, &cont, &surf );
				//%	Sys_Printf( ")\n" );
			}

			/* use the normal plain-jane shader */
			else{
				const int cont = bspShaders[ ds.shaderNum ].contentFlags;
				const int surf = bspShaders[ ds.shaderNum ].surfaceFlags;
				ds.shaderNum = EmitShader( info.si->shader, &cont, &surf );
			}
		}

		Sys_Printf( "%d.", int( timer.elapsed_sec() ) );
	}

	/* finish */
	Sys_Printf( "done.\n" );

	if ( storeForReal ) {
		/* calc num stored */
		numStored = bspLightBytes.size() / 3;
		efficiency = ( numStored <= 0 )
	                 ? 0
	                 : (float) numUsed / numStored;

		/* print stats */
		Sys_Printf( "%9d luxels used\n", numUsed );
		Sys_Printf( "%9d luxels stored (%3.2f percent efficiency)\n", numStored, efficiency * 100.0f );
		Sys_Printf( "%9d solid surface lightmaps\n", numSolidLightmaps );
		Sys_Printf( "%9d identical surface lightmaps, using %d luxels\n", numTwins, numTwinLuxels );
		Sys_Printf( "%9d vertex forced surfaces\n", numSurfsVertexForced );
		Sys_Printf( "%9d vertex approximated surfaces\n", numSurfsVertexApproximated );
		Sys_Printf( "%9d BSP lightmaps\n", numBSPLightmaps );
		Sys_Printf( "%9d total lightmaps\n", numOutLightmaps );
		Sys_Printf( "%9d unique lightmap/shader combinations\n", numLightmapShaders );

		/* write map shader file */
		WriteMapShaderFile();
	}
}
