/*****************************************************************************
The Dark Mod GPL Source Code

This file is part of the The Dark Mod Source Code, originally based
on the Doom 3 GPL Source Code as published in 2011.

The Dark Mod Source Code 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 3 of the License,
or (at your option) any later version. For details, see LICENSE.TXT.

Project: The Dark Mod (http://www.thedarkmod.com/)

******************************************************************************/

#include "precompiled.h"
#pragma hdrstop



#include "renderer/tr_local.h"
#include "renderer/resources/Model_local.h"
#include "renderer/resources/Model_ase.h"
#include "renderer/resources/Model_lwo.h"
#include "renderer/resources/Model_obj.h"
#include "renderer/resources/Model_ma.h"
#include "math/PoissonSampling.h"

idCVar idRenderModelStatic::r_mergeModelSurfaces( "r_mergeModelSurfaces", "1", CVAR_BOOL|CVAR_RENDERER, "combine model surfaces with the same material" );
idCVar idRenderModelStatic::r_slopVertex( "r_slopVertex", "0.01", CVAR_RENDERER, "merge xyz coordinates this far apart" );
idCVar idRenderModelStatic::r_slopTexCoord( "r_slopTexCoord", "0.001", CVAR_RENDERER, "merge texture coordinates this far apart" );
idCVar idRenderModelStatic::r_slopNormal( "r_slopNormal", "0.02", CVAR_RENDERER, "merge normals that dot less than this" );

/*
================
idRenderModelStatic::idRenderModelStatic
================
*/
idRenderModelStatic::idRenderModelStatic() {
	name = "<undefined>";
	bounds.Clear();
	lastModifiedFrame = 0;
	lastArchivedFrame = 0;
	overlaysAdded = 0;
	shadowHull = NULL;
	isStaticWorldModel = false;
	defaulted = false;
	purged = false;
	fastLoad = false;
	reloadable = true;
	levelLoadReferenced = false;
	timeStamp = 0;
	loadVersion = 0;
}

/*
================
idRenderModelStatic::~idRenderModelStatic
================
*/
idRenderModelStatic::~idRenderModelStatic() {
	PurgeModel();
}

/*
==============
idRenderModelStatic::Print
==============
*/
void idRenderModelStatic::Print() const {
	common->Printf( "%s\n", name.c_str() );
	common->Printf( "Static model.\n" );
	common->Printf( "bounds: (%f %f %f) to (%f %f %f)\n", 
		bounds[0][0], bounds[0][1], bounds[0][2], 
		bounds[1][0], bounds[1][1], bounds[1][2] );

	common->Printf( "    verts  tris material\n" );
	for ( int i = 0 ; i < NumSurfaces() ; i++ ) {
		const modelSurface_t *surf = Surface( i );
		const srfTriangles_t *tri = surf->geometry;
		const idMaterial *material = surf->material;
		
		if ( !tri ) {
			common->Printf( "%2i: %s, NULL surface geometry\n", i, material->GetName() );
			continue;
		}

		common->Printf( "%2i: %5i %5i %s", i, tri->numVerts, tri->numIndexes / 3, material->GetName() );
		if ( tri->generateNormals ) {
			common->Printf( " (smoothed)\n" );
		} else {
			common->Printf( "\n" );
		}
	}	
}

/*
==============
idRenderModelStatic::Memory
==============
*/
int idRenderModelStatic::Memory() const {
	size_t	totalBytes = 0;

	totalBytes += sizeof( *this );
	totalBytes += name.DynamicMemoryUsed();
	totalBytes += surfaces.MemoryUsed();

	if ( shadowHull ) {
		totalBytes += R_TriSurfMemory( shadowHull );
	}

	for ( int j = 0 ; j < NumSurfaces() ; j++ ) {
		const modelSurface_t	*surf = Surface( j );
		if ( !surf->geometry ) {
			continue;
		}
		totalBytes += R_TriSurfMemory( surf->geometry );
	}

    return static_cast<int>(totalBytes);
}

/*
==============
idRenderModelStatic::List
==============
*/
void idRenderModelStatic::List() const {
	int	totalTris = 0;
	int	totalVerts = 0;
	int	totalBytes = 0;

	totalBytes = Memory();

	char closed = 'C';
	for ( int j = 0 ; j < NumSurfaces() ; j++ ) {
		const modelSurface_t *surf = Surface( j );
		if ( !surf->geometry ) {
			continue;
		}
		if ( !surf->geometry->perfectHull ) {
			closed = ' ';
		}
		totalTris += surf->geometry->numIndexes / 3;
		totalVerts += surf->geometry->numVerts;
	}
	common->Printf( "%c%4ik %3i %4i %4i %s", closed, totalBytes/1024, NumSurfaces(), totalVerts, totalTris, Name() );

	if ( IsDynamicModel() == DM_CACHED ) {
		common->Printf( " (DM_CACHED)" );
	} else if ( IsDynamicModel() == DM_CONTINUOUS ) {
		common->Printf( " (DM_CONTINUOUS)" );
	}
	if ( defaulted ) {
		common->Printf( " (DEFAULTED)" );
	}
	if ( bounds[0][0] >= bounds[1][0] ) {
		common->Printf( " (EMPTY BOUNDS)" );
	} else if ( bounds[1][0] - bounds[0][0] > 100000 ) {
		common->Printf( " (HUGE BOUNDS)" );
	}

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

/*
================
idRenderModelStatic::IsDefaultModel
================
*/
bool idRenderModelStatic::IsDefaultModel() const {
	return defaulted;
}

/*
================
AddCubeFace
================
*/
static void AddCubeFace( srfTriangles_t *tri, idVec3 v1, idVec3 v2, idVec3 v3, idVec3 v4 ) {
	tri->verts[tri->numVerts+0].Clear();
	tri->verts[tri->numVerts+0].xyz = v1 * 8;
	tri->verts[tri->numVerts+0].st[0] = 0;
	tri->verts[tri->numVerts+0].st[1] = 0;

	tri->verts[tri->numVerts+1].Clear();
	tri->verts[tri->numVerts+1].xyz = v2 * 8;
	tri->verts[tri->numVerts+1].st[0] = 1;
	tri->verts[tri->numVerts+1].st[1] = 0;

	tri->verts[tri->numVerts+2].Clear();
	tri->verts[tri->numVerts+2].xyz = v3 * 8;
	tri->verts[tri->numVerts+2].st[0] = 1;
	tri->verts[tri->numVerts+2].st[1] = 1;

	tri->verts[tri->numVerts+3].Clear();
	tri->verts[tri->numVerts+3].xyz = v4 * 8;
	tri->verts[tri->numVerts+3].st[0] = 0;
	tri->verts[tri->numVerts+3].st[1] = 1;

	tri->indexes[tri->numIndexes+0] = tri->numVerts + 0;
	tri->indexes[tri->numIndexes+1] = tri->numVerts + 1;
	tri->indexes[tri->numIndexes+2] = tri->numVerts + 2;
	tri->indexes[tri->numIndexes+3] = tri->numVerts + 0;
	tri->indexes[tri->numIndexes+4] = tri->numVerts + 2;
	tri->indexes[tri->numIndexes+5] = tri->numVerts + 3;

	tri->numVerts += 4;
	tri->numIndexes += 6;
}

/*
================
idRenderModelStatic::MakeDefaultModel
================
*/
void idRenderModelStatic::MakeDefaultModel() {

	defaulted = true;

	// throw out any surfaces we already have
	PurgeModel();

	// create one new surface
	modelSurface_t	surf;

	srfTriangles_t *tri = R_AllocStaticTriSurf();

	surf.material = tr.defaultMaterial;
	surf.geometry = tri;

	R_AllocStaticTriSurfVerts( tri, 24 );
	R_AllocStaticTriSurfIndexes( tri, 36 );

	AddCubeFace( tri, idVec3(-1, 1, 1), idVec3(1, 1, 1), idVec3(1, -1, 1), idVec3(-1, -1, 1) );
	AddCubeFace( tri, idVec3(-1, 1, -1), idVec3(-1, -1, -1), idVec3(1, -1, -1), idVec3(1, 1, -1) );

	AddCubeFace( tri, idVec3(1, -1, 1), idVec3(1, 1, 1), idVec3(1, 1, -1), idVec3(1, -1, -1) );
	AddCubeFace( tri, idVec3(-1, -1, 1), idVec3(-1, -1, -1), idVec3(-1, 1, -1), idVec3(-1, 1, 1) );

	AddCubeFace( tri, idVec3(-1, -1, 1), idVec3(1, -1, 1), idVec3(1, -1, -1), idVec3(-1, -1, -1) );
	AddCubeFace( tri, idVec3(-1, 1, 1), idVec3(-1, 1, -1), idVec3(1, 1, -1), idVec3(1, 1, 1) );

	tri->generateNormals = true;

	AddSurface( surf );
	FinishSurfaces();
}

/*
================
idRenderModelStatic::PartialInitFromFile
================
*/
void idRenderModelStatic::PartialInitFromFile( const char *fileName ) {
	fastLoad = true;
	InitFromFile( fileName );
}

/*
================
idRenderModelStatic::InitFromFile
================
*/
void idRenderModelStatic::InitFromFile( const char *fileName ) {
	bool loaded;
	idStr fallback = "";
	idStr extension;

	// set name = fileName
	InitEmpty( fileName );

	// FIXME: load new .proc map format
	name.ExtractFileExtension( extension );

	if ( extension.Icmp( "ase" ) == 0 ) {
		loaded		= LoadASE( name );
		// Tels: #3111 try to load LWO as a fallback
		if (!loaded) {
			name.Replace(".ase",".lwo");
			fallback 	= "LWO";
			loaded		= LoadLWO( name );
		}
		reloadable	= true;
	} else if ( extension.Icmp( "lwo" ) == 0 ) {
		loaded		= LoadLWO( name );
		// Tels: #3111 try to load ASE as a fallback
		if (!loaded) {
			name.Replace(".lwo",".ase");
			fallback 	= "ASE";
			loaded		= LoadASE( name );
		}
		reloadable	= true;
	} else if ( extension.Icmp( "obj" ) == 0 ) {
		loaded		= LoadOBJ( name );
		reloadable	= true;
	} else if ( extension.Icmp( "flt" ) == 0 ) {
		loaded		= LoadFLT( name );
		reloadable	= true;
	} else if ( extension.Icmp( "ma" ) == 0 ) {
		loaded		= LoadMA( name );
		reloadable	= true;
	} else if ( extension.Icmp( "proxy" ) == 0 ) {
		//stgatilov #4970: proxy models substitute rotation hack
		loaded		= LoadProxy( name );
		reloadable  = true;
	} else {
		common->Warning( "idRenderModelStatic::InitFromFile: unknown type for model: \'%s\'", name.c_str() );
		loaded		= false;
	}

	if ( !loaded ) {
		if (fallback.IsEmpty()) {
			common->Warning( "Couldn't load model: '%s'", name.c_str() );
		} else {
			common->Warning( "Couldn't load model: '%s' (nor the fallback to %s)", fileName, fallback.c_str() );
		}
		MakeDefaultModel();
		return;
	}

	// it is now available for use
	purged = false;

	// create the bounds for culling and dynamic surface creation
	FinishSurfaces();
}

/*
================
idRenderModelStatic::LoadModel
================
*/
void idRenderModelStatic::LoadModel() {
	PurgeModel();
	InitFromFile( name );
	loadVersion++;
}

/*
================
idRenderModelStatic::InitEmpty
================
*/
void idRenderModelStatic::InitEmpty( const char *fileName ) {
	// model names of the form _area* are static parts of the
	// world, and have already been considered for optimized shadows
	// other model names are inline entity models, and need to be
	// shadowed normally
	isStaticWorldModel = false;
	if ( !idStr::Cmpn( fileName, "_area", 5 ) ) {
		isStaticWorldModel = true;
	}

	name = fileName;
	reloadable = false;	// if it didn't come from a file, we can't reload it
	PurgeModel();
	purged = false;
	bounds.Zero();
}

/*
================
idRenderModelStatic::AddSurface
================
*/
void idRenderModelStatic::AddSurface( modelSurface_t surface ) {
	surfaces.Append( surface );
	if ( surface.geometry ) {
		bounds += surface.geometry->bounds;
	}
}

/*
================
idRenderModelStatic::Name
================
*/
const char *idRenderModelStatic::Name() const {
	return name;
}

/*
================
idRenderModelStatic::Timestamp
================
*/
ID_TIME_T idRenderModelStatic::Timestamp() const {
	return timeStamp;
}

/*
================
idRenderModelStatic::GetLoadVersion
================
*/
int idRenderModelStatic::GetLoadVersion() const {
	return loadVersion;
}

/*
================
idRenderModelStatic::NumSurfaces
================
*/
int idRenderModelStatic::NumSurfaces() const {
	return surfaces.Num();
}

/*
================
idRenderModelStatic::NumBaseSurfaces
================
*/
int idRenderModelStatic::NumBaseSurfaces() const {
	return surfaces.Num() - overlaysAdded;
}

/*
================
idRenderModelStatic::Surface
================
*/
const modelSurface_t *idRenderModelStatic::Surface( int surfaceNum ) const {
	return &surfaces[surfaceNum];
}

/*
================
idRenderModelStatic::AllocSurfaceTriangles
================
*/
srfTriangles_t *idRenderModelStatic::AllocSurfaceTriangles( int numVerts, int numIndexes ) const {
	srfTriangles_t *tri = R_AllocStaticTriSurf();
	R_AllocStaticTriSurfVerts( tri, numVerts );
	R_AllocStaticTriSurfIndexes( tri, numIndexes );
	return tri;
}

/*
================
idRenderModelStatic::FreeSurfaceTriangles
================
*/
void idRenderModelStatic::FreeSurfaceTriangles( srfTriangles_t *tris ) const {
	R_FreeStaticTriSurf( tris );
}

/*
================
idRenderModelStatic::ShadowHull
================
*/
srfTriangles_t *idRenderModelStatic::ShadowHull() const {
	return shadowHull;
}

/*
================
idRenderModelStatic::IsStaticWorldModel
================
*/
bool idRenderModelStatic::IsStaticWorldModel() const {
	return isStaticWorldModel;
}

/*
================
idRenderModelStatic::IsDynamicModel
================
*/
dynamicModel_t idRenderModelStatic::IsDynamicModel() const {
	// dynamic subclasses will override this
	return DM_STATIC;
}

/*
================
idRenderModelStatic::IsReloadable
================
*/
bool idRenderModelStatic::IsReloadable() const {
	return reloadable;
}

/*
================
idRenderModelStatic::Bounds
================
*/
idBounds idRenderModelStatic::Bounds( const struct renderEntity_s *mdef ) const {
	return bounds;
}

/*
================
idRenderModelStatic::JointBounds
================
*/
const idBounds *idRenderModelStatic::JointBounds() const {
	return nullptr;
}

/*
================
idRenderModelStatic::DepthHack
================
*/
float idRenderModelStatic::DepthHack() const {
	return 0.0f;
}

/*
================
idRenderModelStatic::InstantiateDynamicModel
================
*/
idRenderModel *idRenderModelStatic::InstantiateDynamicModel( const struct renderEntity_s *ent, const struct viewDef_s *view, idRenderModel *cachedModel ) {
	if ( cachedModel ) {
		delete cachedModel;
		cachedModel = NULL;
	}
	common->Error( "InstantiateDynamicModel called on static model '%s'", name.c_str() );
	return NULL;
}

/*
================
idRenderModelStatic::NumJoints
================
*/
int idRenderModelStatic::NumJoints( void ) const {
	return 0;
}

/*
================
idRenderModelStatic::GetJoints
================
*/
const idMD5Joint *idRenderModelStatic::GetJoints( void ) const {
	return NULL;
}

/*
================
idRenderModelStatic::GetJointHandle
================
*/
jointHandle_t idRenderModelStatic::GetJointHandle( const char *name ) const {
	return INVALID_JOINT;
}

/*
================
idRenderModelStatic::GetJointName
================
*/
const char * idRenderModelStatic::GetJointName( jointHandle_t handle ) const {
	return "";
}

/*
================
idRenderModelStatic::GetDefaultPose
================
*/
const idJointQuat *idRenderModelStatic::GetDefaultPose( void ) const {
	return NULL;
}

/*
================
idRenderModelStatic::NearestJoint
================
*/
int idRenderModelStatic::NearestJoint( int surfaceNum, int a, int b, int c ) const {
	return INVALID_JOINT;
}


//=====================================================================


/*
================
idRenderModelStatic::FinishSurfaces

The mergeShadows option allows surfaces with different textures to share
silhouette edges for shadow calculation, instead of leaving shared edges
hanging.

If any of the original shaders have the noSelfShadow flag set, the surfaces
can't be merged, because they will need to be drawn in different order.

If there is only one surface, a separate merged surface won't be generated.

A model with multiple surfaces can't later have a skinned shader change the
state of the noSelfShadow flag.

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

Creates mirrored copies of two sided surfaces with normal maps, which would
otherwise light funny.

Extends the bounds of deformed surfaces so they don't cull incorrectly at screen edges.

================
*/
void idRenderModelStatic::FinishSurfaces() {
	TRACE_CPU_SCOPE("FinishSurfaces");

	int			i;
	int			totalVerts, totalIndexes;

	purged = false;

	// make sure we don't have a huge bounds even if we don't finish everything
	bounds.Zero();

	if ( surfaces.Num() == 0 ) {
		return;
	}

	// renderBump doesn't care about most of this
	if ( fastLoad ) {
		bounds.Zero();
		for ( i = 0 ; i < surfaces.Num() ; i++ ) {
			const modelSurface_t	*surf = &surfaces[i];

			R_BoundTriSurf( surf->geometry );
			bounds.AddBounds( surf->geometry->bounds );
		}

		return;
	}

	// cleanup all the final surfaces, but don't create sil edges
	totalVerts = 0;
	totalIndexes = 0;

	// decide if we are going to merge all the surfaces into one shadower
	int	numOriginalSurfaces = surfaces.Num();

	// make sure there aren't any NULL shaders or geometry
	for ( i = 0 ; i < numOriginalSurfaces ; i++ ) {
		const modelSurface_t	*surf = &surfaces[i];

		if ( surf->geometry == NULL || surf->material == NULL ) {
			MakeDefaultModel();
			common->Error( "Model %s, surface %i had NULL geometry", name.c_str(), i );
		}
		if ( surf->material == NULL ) {
			MakeDefaultModel();
			common->Error( "Model %s, surface %i had NULL shader", name.c_str(), i );
		}
	}

	// duplicate and reverse triangles for two sided bump mapped surfaces
	// note that this won't catch surfaces that have their shaders dynamically
	// changed, and won't work with animated models.
	// It is better to create completely separate surfaces, rather than
	// add vertexes and indexes to the existing surface, because the
	// tangent generation wouldn't like the acute shared edges
	for ( i = 0 ; i < numOriginalSurfaces ; i++ ) {
		const modelSurface_t	*surf = &surfaces[i];

		if ( surf->material->ShouldCreateBackSides() ) {
			srfTriangles_t *newTri;

			newTri = R_CopyStaticTriSurf( surf->geometry );
			R_ReverseTriangles( newTri );

			modelSurface_t	newSurf;

			newSurf.material = surf->material;
			newSurf.geometry = newTri;
			// stgatilov: otherwise it is left uninitialized
			// which is sometimes negative and removed by RemoveOverlaySurfacesFromModel
			newSurf.id = 1000000 + surf->id;

			AddSurface( newSurf );
		}
	}

	// clean the surfaces
	const char *modelname = this->name.c_str();
	for ( i = 0 ; i < surfaces.Num() ; i++ ) {
		const modelSurface_t	*surf = &surfaces[i];

		R_CleanupTriangles( surf->geometry, surf->geometry->generateNormals, true, surf->material->UseUnsmoothedTangents() );
		if ( surf->material->SurfaceCastsShadow() ) {
			totalVerts += surf->geometry->numVerts;
			totalIndexes += surf->geometry->numIndexes;
		}
	}

	// add up the total surface area for development information
	for ( i = 0 ; i < surfaces.Num() ; i++ ) {
		const modelSurface_t	*surf = &surfaces[i];
		srfTriangles_t	*tri = surf->geometry;

		for ( int j = 0 ; j < tri->numIndexes ; j += 3 ) {
			float	area = idWinding::TriangleArea( tri->verts[tri->indexes[j]].xyz,
				 tri->verts[tri->indexes[j+1]].xyz,  tri->verts[tri->indexes[j+2]].xyz );
			const_cast<idMaterial *>(surf->material)->AddToSurfaceArea( area );
		}
	}

	// calculate the bounds
	if ( surfaces.Num() == 0 )
	{
		bounds.Zero();
	}
	else
	{
		bounds.Clear();
		for ( i = 0 ; i < surfaces.Num() ; i++ )
		{
			modelSurface_t *surf = &surfaces[i];

			// grayman #3278 - solution provided by Zbyl
			// if the surface has a deformation, increase the bounds
			switch ( surf->material->Deform() )
			{
			case DFRM_NONE:
			case DFRM_TURB:	// does not modify geometry
				break;
			case DFRM_PARTICLE:
			case DFRM_PARTICLE2:
			{
				// stgatilov: this is some interval math which computes provably correct bounds
				srfTriangles_t *tri = surf->geometry;
				const idDeclParticle *particleSystem = (idDeclParticle *)surf->material->GetDeformDecl();

				idBounds csysBounds[4];
				idParticle_AnalyzeSurfaceEmitter(tri, csysBounds);

				idBounds fullBounds;
				fullBounds.Clear();
				//the rest is done on per-stage basis
				for (int i = 0; i < particleSystem->stages.Num(); i++) {
					idParticleStage &stg = *particleSystem->stages[i];
					idBounds bounds = idParticle_GetStageBoundsDeform(stg, stg.stdBounds, csysBounds);
					assert(bounds.ContainsPoint(tri->bounds[0]) && bounds.ContainsPoint(tri->bounds[1]));
					//unify bounding boxes of all stages
					fullBounds.AddBounds(bounds);
				}

				tri->bounds = fullBounds;
				break;
			}
			default:
				{
				// the amount here is somewhat arbitrary, designed to handle
				// autosprites and flares, but could be done better with exact
				// deformation information.
				// Note that this doesn't handle deformations that are skinned in
				// at run time...
				srfTriangles_t *tri = surf->geometry;
				idVec3 mid = ( tri->bounds[1] + tri->bounds[0] ) * 0.5f;
				float  radius = ( tri->bounds[0] - mid ).Length();
				radius += 20.0f;

				tri->bounds[0][0] = mid[0] - radius;
				tri->bounds[0][1] = mid[1] - radius;
				tri->bounds[0][2] = mid[2] - radius;

				tri->bounds[1][0] = mid[0] + radius;
				tri->bounds[1][1] = mid[1] + radius;
				tri->bounds[1][2] = mid[2] + radius;
				}
				break;
			}
			// add to the model bounds
			bounds.AddBounds( surf->geometry->bounds );
		}
	}
}

/*
=================
idRenderModelStatic::ConvertASEToModelSurfaces
=================
*/
typedef struct matchVert_s {
	struct matchVert_s	*next;
	int		v, tv;
	byte	color[4];
	idVec3	normal;
} matchVert_t;

bool idRenderModelStatic::ConvertASEToModelSurfaces( const struct aseModel_s *ase ) {
	aseObject_t *	object;
	aseMesh_t *		mesh;
	aseMaterial_t *	material;
	const idMaterial *im1, *im2;
	srfTriangles_t *tri;
	int				objectNum;
	int				i, j, k;
	int				v, tv;
	int *			vRemap;
	int *			tvRemap;
	matchVert_t *	mvTable;	// all of the match verts
	matchVert_t **	mvHash;		// points inside mvTable for each xyz index
	matchVert_t *	lastmv;
	matchVert_t *	mv;
	idVec3			normal;
	float			uOffset, vOffset, textureSin, textureCos;
	float			uTiling, vTiling;
	int *			mergeTo;
	byte *			color;
	static byte	identityColor[4] = { 255, 255, 255, 255 };
	modelSurface_t	surf, *modelSurf;

	if ( !ase ) {
		return false;
	}
	if ( ase->objects.Num() < 1 ) {
		return false;
	}

	timeStamp = ase->timeStamp;

	// the modeling programs can save out multiple surfaces with a common
	// material, but we would like to mege them together where possible
	// meaning that this->NumSurfaces() <= ase->objects.currentElements
	mergeTo = (int *)_alloca( ase->objects.Num() * sizeof( *mergeTo ) ); 
	surf.geometry = NULL;
	if ( ase->materials.Num() == 0 ) {
		// if we don't have any materials, dump everything into a single surface
		surf.material = tr.defaultMaterial;
		surf.id = 0;
		this->AddSurface( surf );
		for ( i = 0 ; i < ase->objects.Num() ; i++ ) { 
			mergeTo[i] = 0;
		}
	} else if ( !r_mergeModelSurfaces.GetBool() ) {
		// don't merge any
		for ( i = 0 ; i < ase->objects.Num() ; i++ ) { 
			mergeTo[i] = i;
			object = ase->objects[i];
			material = ase->materials[object->materialRef];
			surf.material = declManager->FindMaterial( material->name );
			surf.id = this->NumSurfaces();
			this->AddSurface( surf );
		}
	} else {
		// search for material matches
		for ( i = 0 ; i < ase->objects.Num() ; i++ ) { 
			object = ase->objects[i];
			material = ase->materials[object->materialRef];
			im1 = declManager->FindMaterial( material->name );
			if ( im1->IsDiscrete() ) {
				// flares, autosprites, etc
				j = this->NumSurfaces();
			} else {
				for ( j = 0 ; j < this->NumSurfaces() ; j++ ) {
					modelSurf = &this->surfaces[j];
					im2 = modelSurf->material;
					if ( im1 == im2 ) {
						// merge this
						mergeTo[i] = j;
						break;
					}
				}
			}
			if ( j == this->NumSurfaces() ) {
				// didn't merge
				mergeTo[i] = j;
				surf.material = im1;
				surf.id = this->NumSurfaces();
				this->AddSurface( surf );
			}
		}
	}

	idVectorSubset<idVec3, 3> vertexSubset;
	idVectorSubset<idVec2, 2> texCoordSubset;

	// build the surfaces
	for ( objectNum = 0 ; objectNum < ase->objects.Num() ; objectNum++ ) {
		object = ase->objects[objectNum];
		mesh = &object->mesh;
		material = ase->materials[object->materialRef];
		im1 = declManager->FindMaterial( material->name );

		bool normalsParsed = mesh->normalsParsed;

		// completely ignore any explict normals on surfaces with a renderbump command
		// which will guarantee the best contours and least vertexes.
		const char *rb = im1->GetRenderBump();
		if ( rb && rb[0] ) {
			normalsParsed = false;
		}

		// It seems like the tools our artists are using often generate
		// verts and texcoords slightly separated that should be merged
		// FIXME : note that we really should combine the surfaces with common materials
		// before doing this operation, because we can miss a slop combination
		// if they are in different surfaces

		vRemap = (int *)R_StaticAlloc( mesh->numVertexes * sizeof( vRemap[0] ) );

		if ( fastLoad ) {
			// renderbump doesn't care about vertex count
			for ( j = 0; j < mesh->numVertexes; j++ ) {
				vRemap[j] = j;
			}
		} else {
			float vertexEpsilon = r_slopVertex.GetFloat();
			float expand = 2 * 32 * vertexEpsilon;
			idVec3 mins, maxs;

			SIMDProcessor->MinMax( mins, maxs, mesh->vertexes, mesh->numVertexes );
			mins -= idVec3( expand, expand, expand );
			maxs += idVec3( expand, expand, expand );
			vertexSubset.Init( mins, maxs, 32, 1024 );
			for ( j = 0; j < mesh->numVertexes; j++ ) {
				vRemap[j] = vertexSubset.FindVector( mesh->vertexes, j, vertexEpsilon );
			}
		}

		tvRemap = (int *)R_StaticAlloc( mesh->numTVertexes * sizeof( tvRemap[0] ) );

		if ( fastLoad ) {
			// renderbump doesn't care about vertex count
			for ( j = 0; j < mesh->numTVertexes; j++ ) {
				tvRemap[j] = j;
			}
		} else {
			float texCoordEpsilon = r_slopTexCoord.GetFloat();
			float expand = 2 * 32 * texCoordEpsilon;
			idVec2 mins, maxs;

			SIMDProcessor->MinMax( mins, maxs, mesh->tvertexes, mesh->numTVertexes );
			mins -= idVec2( expand, expand );
			maxs += idVec2( expand, expand );
			texCoordSubset.Init( mins, maxs, 32, 1024 );
			for ( j = 0; j < mesh->numTVertexes; j++ ) {
				tvRemap[j] = texCoordSubset.FindVector( mesh->tvertexes, j, texCoordEpsilon );
			}
		}

		// we need to find out how many unique vertex / texcoord combinations
		// there are, because ASE tracks them separately but we need them unified

		// the maximum possible number of combined vertexes is the number of indexes
		mvTable = (matchVert_t *)R_ClearedStaticAlloc( mesh->numFaces * 3 * sizeof( mvTable[0] ) );

		// we will have a hash chain based on the xyz values
		mvHash = (matchVert_t **)R_ClearedStaticAlloc( mesh->numVertexes * sizeof( mvHash[0] ) );

		// allocate triangle surface
		tri = R_AllocStaticTriSurf();
		tri->numVerts = 0;
		tri->numIndexes = 0;
		R_AllocStaticTriSurfIndexes( tri, mesh->numFaces * 3 );
		tri->generateNormals = !normalsParsed;

		// init default normal, color and tex coord index
		normal.Zero();
		color = identityColor;
		tv = 0;

		// find all the unique combinations
		float normalEpsilon = 1.0f - r_slopNormal.GetFloat();
		for ( j = 0; j < mesh->numFaces; j++ ) {
			for ( k = 0; k < 3; k++ ) {
				v = mesh->faces[j].vertexNum[k];

				if ( v < 0 || v >= mesh->numVertexes ) {
					common->Error( "ConvertASEToModelSurfaces: bad vertex index in ASE file %s", name.c_str() );
				}

				// collapse the position if it was slightly offset 
				v = vRemap[v];

				// we may or may not have texcoords to compare
				if ( mesh->numTVFaces == mesh->numFaces && mesh->numTVertexes != 0 ) {
					tv = mesh->faces[j].tVertexNum[k];
					if ( tv < 0 || tv >= mesh->numTVertexes ) {
						common->Error( "ConvertASEToModelSurfaces: bad tex coord index in ASE file %s", name.c_str() );
					}
					// collapse the tex coord if it was slightly offset
					tv = tvRemap[tv];
				}

				// we may or may not have normals to compare
				if ( normalsParsed ) {
					normal = mesh->faces[j].vertexNormals[k];
				}

				// we may or may not have colors to compare
				if ( mesh->colorsParsed ) {
					color = mesh->faces[j].vertexColors[k];
				}

				// find a matching vert
				for ( lastmv = NULL, mv = mvHash[v]; mv != NULL; lastmv = mv, mv = mv->next ) {
					if ( mv->tv != tv ) {
						continue;
					}
					if ( *(unsigned *)mv->color != *(unsigned *)color ) {
						continue;
					}
					if ( !normalsParsed ) {
						// if we are going to create the normals, just
						// matching texcoords is enough
						break;
					}
					if ( mv->normal * normal > normalEpsilon ) {
						break;		// we already have this one
					}
				}
				if ( !mv ) {
					// allocate a new match vert and link to hash chain
					mv = &mvTable[ tri->numVerts ];
					mv->v = v;
					mv->tv = tv;
					mv->normal = normal;
					*(unsigned *)mv->color = *(unsigned *)color;
					mv->next = NULL;
					if ( lastmv ) {
						lastmv->next = mv;
					} else {
						mvHash[v] = mv;
					}
					tri->numVerts++;
				}

				tri->indexes[tri->numIndexes] = mv - mvTable;
				tri->numIndexes++;
			}
		}

		// allocate space for the indexes and copy them
		if ( tri->numIndexes > mesh->numFaces * 3 ) {
			common->FatalError( "ConvertASEToModelSurfaces: index miscount in ASE file %s", name.c_str() );
		}
		if ( tri->numVerts > mesh->numFaces * 3 ) {
			common->FatalError( "ConvertASEToModelSurfaces: vertex miscount in ASE file %s", name.c_str() );
		}

		// an ASE allows the texture coordinates to be scaled, translated, and rotated
		if ( ase->materials.Num() == 0 ) {
			uOffset = vOffset = 0.0f;
			uTiling = vTiling = 1.0f;
			textureSin = 0.0f;
			textureCos = 1.0f;
		} else {
			material = ase->materials[object->materialRef];
			uOffset = -material->uOffset;
			vOffset = material->vOffset;
			uTiling = material->uTiling;
			vTiling = material->vTiling;
			textureSin = idMath::Sin( material->angle );
			textureCos = idMath::Cos( material->angle );
		}

		// now allocate and generate the combined vertexes
		R_AllocStaticTriSurfVerts( tri, tri->numVerts );
		for ( j = 0; j < tri->numVerts; j++ ) {
			mv = &mvTable[j];
			tri->verts[ j ].Clear();
			tri->verts[ j ].xyz = mesh->vertexes[ mv->v ];
			tri->verts[ j ].normal = mv->normal;
			*(unsigned *)tri->verts[j].color = *(unsigned *)mv->color;
			if ( mesh->numTVFaces == mesh->numFaces && mesh->numTVertexes != 0 ) {
				const idVec2 &tv = mesh->tvertexes[ mv->tv ];
				float u = tv.x * uTiling + uOffset;
				float v = tv.y * vTiling + vOffset;
				tri->verts[ j ].st[0] = u * textureCos + v * textureSin;
				tri->verts[ j ].st[1] = u * -textureSin + v * textureCos;
			}
		}

		R_StaticFree( mvTable );
		R_StaticFree( mvHash );
		R_StaticFree( tvRemap );
		R_StaticFree( vRemap );

		// see if we need to merge with a previous surface of the same material
		modelSurf = &this->surfaces[mergeTo[ objectNum ]];
		srfTriangles_t	*mergeTri = modelSurf->geometry;
		if ( !mergeTri ) {
			modelSurf->geometry = tri;
		} else {
			modelSurf->geometry = R_MergeTriangles( mergeTri, tri );
			R_FreeStaticTriSurf( tri );
			R_FreeStaticTriSurf( mergeTri );
		}
	}

	return true;
}

/*
=================
idRenderModelStatic::ConvertLWOToModelSurfaces
=================
*/
bool idRenderModelStatic::ConvertLWOToModelSurfaces( const struct st_lwObject *lwo ) {
	const idMaterial *im1, *im2;
	srfTriangles_t	*tri;
	lwSurface *		lwoSurf;
	int				numTVertexes;
	int				i, j, k;
	int				v, tv;
	idVec3 *		vList;
	int *			vRemap;
	idVec2 *		tvList;
	int *			tvRemap;
	matchVert_t *	mvTable;	// all of the match verts
	matchVert_t **	mvHash;		// points inside mvTable for each xyz index
	matchVert_t *	lastmv;
	matchVert_t *	mv;
	idVec3			normal;
	int *			mergeTo;
	byte			color[4];
	modelSurface_t	surf, *modelSurf;

	if ( !lwo ) {
		return false;
	}
	if ( lwo->surf == NULL ) {
		return false;
	}

	timeStamp = lwo->timeStamp;

	// count the number of surfaces
	i = 0;
	for ( lwoSurf = lwo->surf; lwoSurf; lwoSurf = lwoSurf->next ) {
		i++;
	}

	// the modeling programs can save out multiple surfaces with a common
	// material, but we would like to merge them together where possible
	mergeTo = (int *)_alloca( i * sizeof( mergeTo[0] ) ); 
	memset( &surf, 0, sizeof( surf ) );

	if ( !r_mergeModelSurfaces.GetBool() ) {
		// don't merge any
		for ( lwoSurf = lwo->surf, i = 0; lwoSurf; lwoSurf = lwoSurf->next, i++ ) {
			mergeTo[i] = i;
			surf.material = declManager->FindMaterial( lwoSurf->name );
			surf.id = this->NumSurfaces();
			this->AddSurface( surf );
		}
	} else {
		// search for material matches
		for ( lwoSurf = lwo->surf, i = 0; lwoSurf; lwoSurf = lwoSurf->next, i++ ) {
			im1 = declManager->FindMaterial( lwoSurf->name );
			if ( im1->IsDiscrete() ) {
				// flares, autosprites, etc
				j = this->NumSurfaces();
			} else {
				for ( j = 0 ; j < this->NumSurfaces() ; j++ ) {
					modelSurf = &this->surfaces[j];
					im2 = modelSurf->material;
					if ( im1 == im2 ) {
						// merge this
						mergeTo[i] = j;
						break;
					}
				}
			}
			if ( j == this->NumSurfaces() ) {
				// didn't merge
				mergeTo[i] = j;
				surf.material = im1;
				surf.id = this->NumSurfaces();
				this->AddSurface( surf );
			}
		}
	}

	idVectorSubset<idVec3, 3> vertexSubset;
	idVectorSubset<idVec2, 2> texCoordSubset;

	// we only ever use the first layer
	lwLayer *layer = lwo->layer;

	// vertex positions
	if ( layer->point.count <= 0 ) {
		common->Warning( "ConvertLWOToModelSurfaces: model \'%s\' has bad or missing vertex data", name.c_str() );
		return false;
	}

	vList = (idVec3 *)R_StaticAlloc( layer->point.count * sizeof( vList[0] ) );
	for ( j = 0; j < layer->point.count; j++ ) {
		vList[j].x = layer->point.pt[j].pos[0];
		vList[j].y = layer->point.pt[j].pos[2];
		vList[j].z = layer->point.pt[j].pos[1];
	}

	// vertex texture coords
	numTVertexes = 0;

	if ( layer->nvmaps ) {
		for( lwVMap *vm = layer->vmap; vm; vm = vm->next ) {
			if ( vm->type == LWID_('T','X','U','V') ) {
				numTVertexes += vm->nverts;
			}
		}
	}

	if ( numTVertexes ) {
		tvList = (idVec2 *)Mem_Alloc( numTVertexes * sizeof( tvList[0] ) );
		int offset = 0;
		for( lwVMap *vm = layer->vmap; vm; vm = vm->next ) {
			if ( vm->type == LWID_('T','X','U','V') ) {
	  			vm->offset = offset;
		  		for ( k = 0; k < vm->nverts; k++ ) {
		  		   	tvList[k + offset].x = vm->val[k][0];
					tvList[k + offset].y = 1.0f - vm->val[k][1];	// invert the t
		  		}
			  	offset += vm->nverts;
			}
		}
	} else {
		common->Warning( "ConvertLWOToModelSurfaces: model \'%s\' has bad or missing uv data", name.c_str() );
	  	numTVertexes = 1;
		tvList = (idVec2 *)Mem_ClearedAlloc( numTVertexes * sizeof( tvList[0] ) );
	}

	// It seems like the tools our artists are using often generate
	// verts and texcoords slightly separated that should be merged
	// note that we really should combine the surfaces with common materials
	// before doing this operation, because we can miss a slop combination
	// if they are in different surfaces

	vRemap = (int *)R_StaticAlloc( layer->point.count * sizeof( vRemap[0] ) );

	if ( fastLoad ) {
		// renderbump doesn't care about vertex count
		for ( j = 0; j < layer->point.count; j++ ) {
			vRemap[j] = j;
		}
	} else {
		float vertexEpsilon = r_slopVertex.GetFloat();
		float expand = 2 * 32 * vertexEpsilon;
		idVec3 mins, maxs;

		SIMDProcessor->MinMax( mins, maxs, vList, layer->point.count );
		mins -= idVec3( expand, expand, expand );
		maxs += idVec3( expand, expand, expand );
		vertexSubset.Init( mins, maxs, 32, 1024 );
		for ( j = 0; j < layer->point.count; j++ ) {
			vRemap[j] = vertexSubset.FindVector( vList, j, vertexEpsilon );
		}
	}

	tvRemap = (int *)R_StaticAlloc( numTVertexes * sizeof( tvRemap[0] ) );

	if ( fastLoad ) {
		// renderbump doesn't care about vertex count
		for ( j = 0; j < numTVertexes; j++ ) {
			tvRemap[j] = j;
		}
	} else {
		float texCoordEpsilon = r_slopTexCoord.GetFloat();
		float expand = 2 * 32 * texCoordEpsilon;
		idVec2 mins, maxs;

		SIMDProcessor->MinMax( mins, maxs, tvList, numTVertexes );
		mins -= idVec2( expand, expand );
		maxs += idVec2( expand, expand );
		texCoordSubset.Init( mins, maxs, 32, 1024 );
		for ( j = 0; j < numTVertexes; j++ ) {
			tvRemap[j] = texCoordSubset.FindVector( tvList, j, texCoordEpsilon );
		}
	}

	int totalPolysCount = 0;
	int nontriPolysCount = 0;

	// build the surfaces
	for ( lwoSurf = lwo->surf, i = 0; lwoSurf; lwoSurf = lwoSurf->next, i++ ) {
		im1 = declManager->FindMaterial( lwoSurf->name );

		bool normalsParsed = true;

		// completely ignore any explict normals on surfaces with a renderbump command
		// which will guarantee the best contours and least vertexes.
		const char *rb = im1->GetRenderBump();
		if ( rb && rb[0] ) {
			normalsParsed = false;
		}

		// we need to find out how many unique vertex / texcoord combinations there are

		// the maximum possible number of combined vertexes is the number of indexes
		mvTable = (matchVert_t *)R_ClearedStaticAlloc( layer->polygon.count * 3 * sizeof( mvTable[0] ) );

		// we will have a hash chain based on the xyz values
		mvHash = (matchVert_t **)R_ClearedStaticAlloc( layer->point.count * sizeof( mvHash[0] ) );

		// allocate triangle surface
		tri = R_AllocStaticTriSurf();
		tri->numVerts = 0;
		tri->numIndexes = 0;
		R_AllocStaticTriSurfIndexes( tri, layer->polygon.count * 3 );
		tri->generateNormals = !normalsParsed;

		// find all the unique combinations
		float	normalEpsilon;
		if ( fastLoad ) {
			normalEpsilon = 1.0f;	// don't merge unless completely exact
		} else {
			normalEpsilon = 1.0f - r_slopNormal.GetFloat();
		}
		for ( j = 0; j < layer->polygon.count; j++ ) {
			lwPolygon *poly = &layer->polygon.pol[j];

			if ( poly->surf != lwoSurf ) {
				continue;
			}

			totalPolysCount++;
			if ( poly->nverts != 3 ) {
				nontriPolysCount++;
				continue;
			}

			for ( k = 0; k < 3; k++ ) {

				v = vRemap[poly->v[k].index];

				normal.x = poly->v[k].norm[0];
				normal.y = poly->v[k].norm[2];
				normal.z = poly->v[k].norm[1];

				// LWO models aren't all that pretty when it comes down to the floating point values they store
				normal.FixDegenerateNormal();

				tv = 0;

				color[0] = lwoSurf->color.rgb[0] * 255;
				color[1] = lwoSurf->color.rgb[1] * 255;
				color[2] = lwoSurf->color.rgb[2] * 255;
				color[3] = 255;

				// first set attributes from the vertex
				lwPoint	*pt = &layer->point.pt[poly->v[k].index];
				int nvm;
				for ( nvm = 0; nvm < pt->nvmaps; nvm++ ) {
					lwVMapPt *vm = &pt->vm[nvm];

					if ( vm->vmap->type == LWID_('T','X','U','V') ) {
						tv = tvRemap[vm->index + vm->vmap->offset];
					}
					if ( vm->vmap->type == LWID_('R','G','B','A') ) {
						for ( int chan = 0; chan < 4; chan++ ) {
							color[chan] = 255 * vm->vmap->val[vm->index][chan];
						}
					}
				}

				// then override with polygon attributes
				for ( nvm = 0; nvm < poly->v[k].nvmaps; nvm++ ) {
					lwVMapPt *vm = &poly->v[k].vm[nvm];

					if ( vm->vmap->type == LWID_('T','X','U','V') ) {
						tv = tvRemap[vm->index + vm->vmap->offset];
					}
					if ( vm->vmap->type == LWID_('R','G','B','A') ) {
						for ( int chan = 0; chan < 4; chan++ ) {
							color[chan] = 255 * vm->vmap->val[vm->index][chan];
						}
					}
				}

				// find a matching vert
				for ( lastmv = NULL, mv = mvHash[v]; mv != NULL; lastmv = mv, mv = mv->next ) {
					if ( mv->tv != tv ) {
						continue;
					}
					if ( *(unsigned *)mv->color != *(unsigned *)color ) {
						continue;
					}
					if ( !normalsParsed ) {
						// if we are going to create the normals, just
						// matching texcoords is enough
						break;
					}
					if ( mv->normal * normal > normalEpsilon ) {
						break;		// we already have this one
					}
				}
				if ( !mv ) {
					// allocate a new match vert and link to hash chain
					mv = &mvTable[ tri->numVerts ];
					mv->v = v;
					mv->tv = tv;
					mv->normal = normal;
					*(unsigned *)mv->color = *(unsigned *)color;
					mv->next = NULL;
					if ( lastmv ) {
						lastmv->next = mv;
					} else {
						mvHash[v] = mv;
					}
					tri->numVerts++;
				}

				tri->indexes[tri->numIndexes] = mv - mvTable;
				tri->numIndexes++;
			}
		}

		// allocate space for the indexes and copy them
		if ( tri->numIndexes > layer->polygon.count * 3 ) {
			common->FatalError( "ConvertLWOToModelSurfaces: index miscount in LWO file %s", name.c_str() );
		}
		if ( tri->numVerts > layer->polygon.count * 3 ) {
			common->FatalError( "ConvertLWOToModelSurfaces: vertex miscount in LWO file %s", name.c_str() );
		}

		// now allocate and generate the combined vertexes
		R_AllocStaticTriSurfVerts( tri, tri->numVerts );
		for ( j = 0; j < tri->numVerts; j++ ) {
			mv = &mvTable[j];
			tri->verts[ j ].Clear();
			tri->verts[ j ].xyz = vList[ mv->v ];
			tri->verts[ j ].st = tvList[ mv->tv ];
			tri->verts[ j ].normal = mv->normal;
			*(unsigned *)tri->verts[j].color = *(unsigned *)mv->color;
		}

		R_StaticFree( mvTable );
		R_StaticFree( mvHash );

		// see if we need to merge with a previous surface of the same material
		modelSurf = &this->surfaces[mergeTo[ i ]];
		srfTriangles_t	*mergeTri = modelSurf->geometry;
		if ( !mergeTri ) {
			modelSurf->geometry = tri;
		} else {
			modelSurf->geometry = R_MergeTriangles( mergeTri, tri );
			R_FreeStaticTriSurf( tri );
			R_FreeStaticTriSurf( mergeTri );
		}
	}

	if (nontriPolysCount > 0) {
		common->Warning(
			"ConvertLWOToModelSurfaces: model \'%s\' has %d/%d nontriangular polygons. Make sure you triplet it down",
			name.c_str(), nontriPolysCount, totalPolysCount
		);
	}

	R_StaticFree( tvRemap );
	R_StaticFree( vRemap );
	R_StaticFree( tvList );
	R_StaticFree( vList );

	return true;
}

/*
=================
idRenderModelStatic::ConvertLWOToASE
=================
*/
struct aseModel_s *idRenderModelStatic::ConvertLWOToASE( const struct st_lwObject *obj, const char *fileName ) {
	int j, k;
	aseModel_t *ase;

	if ( !obj ) {
		return NULL;
	}

	// NOTE: using new operator because aseModel_t contains idList class objects
	ase = new aseModel_t;
	ase->timeStamp = obj->timeStamp;
	ase->objects.Resize( obj->nlayers, obj->nlayers );

	int materialRef = 0;

	int totalPolysCount = 0;
	int nontriPolysCount = 0;

	for ( lwSurface *surf = obj->surf; surf; surf = surf->next ) {

		aseMaterial_t *mat = (aseMaterial_t *)Mem_ClearedAlloc( sizeof( *mat ) );
		strcpy( mat->name, surf->name );
		mat->uTiling = mat->vTiling = 1;
		mat->angle = mat->uOffset = mat->vOffset = 0;
		ase->materials.Append( mat );

		lwLayer *layer = obj->layer;

		aseObject_t *object = (aseObject_t *)Mem_ClearedAlloc( sizeof( *object ) );
		object->materialRef = materialRef++;

		aseMesh_t *mesh = &object->mesh;
		ase->objects.Append( object );

		mesh->numFaces = layer->polygon.count;
		mesh->numTVFaces = mesh->numFaces;
		mesh->faces = (aseFace_t *)Mem_Alloc( mesh->numFaces  * sizeof( mesh->faces[0] ) );

		mesh->numVertexes = layer->point.count;
		mesh->vertexes = (idVec3 *)Mem_Alloc( mesh->numVertexes * sizeof( mesh->vertexes[0] ) );

		// vertex positions
		if ( layer->point.count <= 0 ) {
			common->Warning( "ConvertLWOToASE: model \'%s\' has bad or missing vertex data", name.c_str() );
		}

		for ( j = 0; j < layer->point.count; j++ ) {
			mesh->vertexes[j].x = layer->point.pt[j].pos[0];
			mesh->vertexes[j].y = layer->point.pt[j].pos[2];
			mesh->vertexes[j].z = layer->point.pt[j].pos[1];
		}

		// vertex texture coords
		mesh->numTVertexes = 0;

		if ( layer->nvmaps ) {
		  	for( lwVMap *vm = layer->vmap; vm; vm = vm->next ) {
				if ( vm->type == LWID_('T','X','U','V') ) {
					mesh->numTVertexes += vm->nverts;
				}
			}
		}

		if ( mesh->numTVertexes ) {
		  	mesh->tvertexes = (idVec2 *)Mem_Alloc( mesh->numTVertexes * sizeof( mesh->tvertexes[0] ) );
		  	int offset = 0;
		  	for( lwVMap *vm = layer->vmap; vm; vm = vm->next ) {
				if ( vm->type == LWID_('T','X','U','V') ) {
	  				vm->offset = offset;
		  			for ( k = 0; k < vm->nverts; k++ ) {
		  		   		mesh->tvertexes[k + offset].x = vm->val[k][0];
						mesh->tvertexes[k + offset].y = 1.0f - vm->val[k][1];	// invert the t
		  		   	}
			  		offset += vm->nverts;
				}
		  	}
	  	} else {
			common->Warning( "ConvertLWOToASE: model \'%s\' has bad or missing uv data", fileName );
	  		mesh->numTVertexes = 1;
	  		mesh->tvertexes = (idVec2 *)Mem_ClearedAlloc( mesh->numTVertexes * sizeof( mesh->tvertexes[0] ) );
	  	}

		mesh->normalsParsed = true;
		mesh->colorsParsed = true;	// because we are falling back to the surface color

		// triangles
		int faceIndex = 0;
		for ( j = 0; j < layer->polygon.count; j++ ) {
			lwPolygon *poly = &layer->polygon.pol[j];

			if ( poly->surf != surf ) {
				continue;
			}

			totalPolysCount++;
			if ( poly->nverts != 3 ) {
				nontriPolysCount++;
				continue;
			}
	
			mesh->faces[faceIndex].faceNormal.x = poly->norm[0];
			mesh->faces[faceIndex].faceNormal.y = poly->norm[2];
			mesh->faces[faceIndex].faceNormal.z = poly->norm[1];

			for ( k = 0; k < 3; k++ ) {

				mesh->faces[faceIndex].vertexNum[k] = poly->v[k].index;

				mesh->faces[faceIndex].vertexNormals[k].x = poly->v[k].norm[0];
				mesh->faces[faceIndex].vertexNormals[k].y = poly->v[k].norm[2];
				mesh->faces[faceIndex].vertexNormals[k].z = poly->v[k].norm[1];

				// complete fallbacks
				mesh->faces[faceIndex].tVertexNum[k] = 0;

				mesh->faces[faceIndex].vertexColors[k][0] = surf->color.rgb[0] * 255;
				mesh->faces[faceIndex].vertexColors[k][1] = surf->color.rgb[1] * 255;
				mesh->faces[faceIndex].vertexColors[k][2] = surf->color.rgb[2] * 255;
				mesh->faces[faceIndex].vertexColors[k][3] = 255;

				// first set attributes from the vertex
				lwPoint	*pt = &layer->point.pt[poly->v[k].index];
				int nvm;
				for ( nvm = 0; nvm < pt->nvmaps; nvm++ ) {
					lwVMapPt *vm = &pt->vm[nvm];

					if ( vm->vmap->type == LWID_('T','X','U','V') ) {
						mesh->faces[faceIndex].tVertexNum[k] = vm->index + vm->vmap->offset;
					}
					if ( vm->vmap->type == LWID_('R','G','B','A') ) {
						for ( int chan = 0; chan < 4; chan++ ) {
							mesh->faces[faceIndex].vertexColors[k][chan] = 255 * vm->vmap->val[vm->index][chan];
						}
					}
				}

				// then override with polygon attributes
				for ( nvm = 0; nvm < poly->v[k].nvmaps; nvm++ ) {
					lwVMapPt *vm = &poly->v[k].vm[nvm];

					if ( vm->vmap->type == LWID_('T','X','U','V') ) {
						mesh->faces[faceIndex].tVertexNum[k] = vm->index + vm->vmap->offset;
					}
					if ( vm->vmap->type == LWID_('R','G','B','A') ) {
						for ( int chan = 0; chan < 4; chan++ ) {
							mesh->faces[faceIndex].vertexColors[k][chan] = 255 * vm->vmap->val[vm->index][chan];
						}
					}
				}
			}

			faceIndex++;
		}

		mesh->numFaces = faceIndex;
		mesh->numTVFaces = faceIndex;

		aseFace_t *newFaces = ( aseFace_t* )Mem_Alloc( mesh->numFaces * sizeof ( mesh->faces[0] ) );
		memcpy( newFaces, mesh->faces, sizeof( mesh->faces[0] ) * mesh->numFaces );
		Mem_Free( mesh->faces );
		mesh->faces = newFaces;
	}

	if (nontriPolysCount > 0) {
		common->Warning(
			"ConvertLWOToASE: model \'%s\' has %d/%d nontriangular polygons. Make sure you triplet it down",
			name.c_str(), nontriPolysCount, totalPolysCount
		);
	}

	return ase;
}

/*
=================
idRenderModelStatic::ConvertMAToModelSurfaces
=================
*/
bool idRenderModelStatic::ConvertMAToModelSurfaces (const struct maModel_s *ma ) {

	maObject_t *	object;
	maMesh_t *		mesh;
	maMaterial_t *	material;
	
	const idMaterial *im1, *im2;
	srfTriangles_t *tri;
	int				objectNum;
	int				i, j, k;
	int				v, tv;
	int *			vRemap;
	int *			tvRemap;
	matchVert_t *	mvTable;	// all of the match verts
	matchVert_t **	mvHash;		// points inside mvTable for each xyz index
	matchVert_t *	lastmv;
	matchVert_t *	mv;
	idVec3			normal;
	float			uOffset, vOffset, textureSin, textureCos;
	float			uTiling, vTiling;
	int *			mergeTo;
	byte *			color;
	static byte	identityColor[4] = { 255, 255, 255, 255 };
	modelSurface_t	surf, *modelSurf;

	if ( !ma ) {
		return false;
	}
	if ( ma->objects.Num() < 1 ) {
		return false;
	}

	timeStamp = ma->timeStamp;

	// the modeling programs can save out multiple surfaces with a common
	// material, but we would like to mege them together where possible
	// meaning that this->NumSurfaces() <= ma->objects.currentElements
	mergeTo = (int *)_alloca( ma->objects.Num() * sizeof( *mergeTo ) ); 

	surf.geometry = NULL;
	if ( ma->materials.Num() == 0 ) {
		// if we don't have any materials, dump everything into a single surface
		surf.material = tr.defaultMaterial;
		surf.id = 0;
		this->AddSurface( surf );
		for ( i = 0 ; i < ma->objects.Num() ; i++ ) { 
			mergeTo[i] = 0;
		}
	} else if ( !r_mergeModelSurfaces.GetBool() ) {
		// don't merge any
		for ( i = 0 ; i < ma->objects.Num() ; i++ ) { 
			mergeTo[i] = i;
			object = ma->objects[i];
			if(object->materialRef >= 0) {
				material = ma->materials[object->materialRef];
				surf.material = declManager->FindMaterial( material->name );
			} else {
				surf.material = tr.defaultMaterial;
			}
			surf.id = this->NumSurfaces();
			this->AddSurface( surf );
		}
	} else {
		// search for material matches
		for ( i = 0 ; i < ma->objects.Num() ; i++ ) { 
			object = ma->objects[i];
			if(object->materialRef >= 0) {
				material = ma->materials[object->materialRef];
				im1 = declManager->FindMaterial( material->name );
			} else {
				im1 = tr.defaultMaterial;
			}
			if ( im1->IsDiscrete() ) {
				// flares, autosprites, etc
				j = this->NumSurfaces();
			} else {
				for ( j = 0 ; j < this->NumSurfaces() ; j++ ) {
					modelSurf = &this->surfaces[j];
					im2 = modelSurf->material;
					if ( im1 == im2 ) {
						// merge this
						mergeTo[i] = j;
						break;
					}
				}
			}
			if ( j == this->NumSurfaces() ) {
				// didn't merge
				mergeTo[i] = j;
				surf.material = im1;
				surf.id = this->NumSurfaces();
				this->AddSurface( surf );
			}
		}
	}

	idVectorSubset<idVec3, 3> vertexSubset;
	idVectorSubset<idVec2, 2> texCoordSubset;

	// build the surfaces
	for ( objectNum = 0 ; objectNum < ma->objects.Num() ; objectNum++ ) {
		object = ma->objects[objectNum];
		mesh = &object->mesh;
		if(object->materialRef >= 0) {
			material = ma->materials[object->materialRef];
			im1 = declManager->FindMaterial( material->name );
		} else {
			im1 = tr.defaultMaterial;
		}

		bool normalsParsed = mesh->normalsParsed;
		
		// completely ignore any explict normals on surfaces with a renderbump command
		// which will guarantee the best contours and least vertexes.
		const char *rb = im1->GetRenderBump();
		if ( rb && rb[0] ) {
			normalsParsed = false;
		}

		// It seems like the tools our artists are using often generate
		// verts and texcoords slightly separated that should be merged
		// note that we really should combine the surfaces with common materials
		// before doing this operation, because we can miss a slop combination
		// if they are in different surfaces

		vRemap = (int *)R_StaticAlloc( mesh->numVertexes * sizeof( vRemap[0] ) );

		if ( fastLoad ) {
			// renderbump doesn't care about vertex count
			for ( j = 0; j < mesh->numVertexes; j++ ) {
				vRemap[j] = j;
			}
		} else {
			float vertexEpsilon = r_slopVertex.GetFloat();
			float expand = 2 * 32 * vertexEpsilon;
			idVec3 mins, maxs;

			SIMDProcessor->MinMax( mins, maxs, mesh->vertexes, mesh->numVertexes );
			mins -= idVec3( expand, expand, expand );
			maxs += idVec3( expand, expand, expand );
			vertexSubset.Init( mins, maxs, 32, 1024 );
			for ( j = 0; j < mesh->numVertexes; j++ ) {
				vRemap[j] = vertexSubset.FindVector( mesh->vertexes, j, vertexEpsilon );
			}
		}

		tvRemap = (int *)R_StaticAlloc( mesh->numTVertexes * sizeof( tvRemap[0] ) );

		if ( fastLoad ) {
			// renderbump doesn't care about vertex count
			for ( j = 0; j < mesh->numTVertexes; j++ ) {
				tvRemap[j] = j;
			}
		} else {
			float texCoordEpsilon = r_slopTexCoord.GetFloat();
			float expand = 2 * 32 * texCoordEpsilon;
			idVec2 mins, maxs;

			SIMDProcessor->MinMax( mins, maxs, mesh->tvertexes, mesh->numTVertexes );
			mins -= idVec2( expand, expand );
			maxs += idVec2( expand, expand );
			texCoordSubset.Init( mins, maxs, 32, 1024 );
			for ( j = 0; j < mesh->numTVertexes; j++ ) {
				tvRemap[j] = texCoordSubset.FindVector( mesh->tvertexes, j, texCoordEpsilon );
			}
		}

		// we need to find out how many unique vertex / texcoord / color combinations
		// there are, because MA tracks them separately but we need them unified

		// the maximum possible number of combined vertexes is the number of indexes
		mvTable = (matchVert_t *)R_ClearedStaticAlloc( mesh->numFaces * 3 * sizeof( mvTable[0] ) );

		// we will have a hash chain based on the xyz values
		mvHash = (matchVert_t **)R_ClearedStaticAlloc( mesh->numVertexes * sizeof( mvHash[0] ) );

		// allocate triangle surface
		tri = R_AllocStaticTriSurf();
		tri->numVerts = 0;
		tri->numIndexes = 0;
		R_AllocStaticTriSurfIndexes( tri, mesh->numFaces * 3 );
		tri->generateNormals = !normalsParsed;

		// init default normal, color and tex coord index
		normal.Zero();
		color = identityColor;
		tv = 0;

		// find all the unique combinations
		float normalEpsilon = 1.0f - r_slopNormal.GetFloat();
		for ( j = 0; j < mesh->numFaces; j++ ) {
			for ( k = 0; k < 3; k++ ) {
				v = mesh->faces[j].vertexNum[k];

				if ( v < 0 || v >= mesh->numVertexes ) {
					common->Error( "ConvertMAToModelSurfaces: bad vertex index in MA file %s", name.c_str() );
				}

				// collapse the position if it was slightly offset 
				v = vRemap[v];

				// we may or may not have texcoords to compare
				if ( mesh->numTVertexes != 0 ) {
					tv = mesh->faces[j].tVertexNum[k];
					if ( tv < 0 || tv >= mesh->numTVertexes ) {
						common->Error( "ConvertMAToModelSurfaces: bad tex coord index in MA file %s", name.c_str() );
					}
					// collapse the tex coord if it was slightly offset
					tv = tvRemap[tv];
				}

				// we may or may not have normals to compare
				if ( normalsParsed ) {
					normal = mesh->faces[j].vertexNormals[k];
				}

				//BSM: Todo: Fix the vertex colors
				// we may or may not have colors to compare
				if ( mesh->faces[j].vertexColors[k] != -1 && mesh->faces[j].vertexColors[k] != -999 ) {

					color = &mesh->colors[mesh->faces[j].vertexColors[k]*4];
				}

				// find a matching vert
				for ( lastmv = NULL, mv = mvHash[v]; mv != NULL; lastmv = mv, mv = mv->next ) {
					if ( mv->tv != tv ) {
						continue;
					}
					if ( *(unsigned *)mv->color != *(unsigned *)color ) {
						continue;
					}
					if ( !normalsParsed ) {
						// if we are going to create the normals, just
						// matching texcoords is enough
						break;
					}
					if ( mv->normal * normal > normalEpsilon ) {
						break;		// we already have this one
					}
				}
				if ( !mv ) {
					// allocate a new match vert and link to hash chain
					mv = &mvTable[ tri->numVerts ];
					mv->v = v;
					mv->tv = tv;
					mv->normal = normal;
					*(unsigned *)mv->color = *(unsigned *)color;
					mv->next = NULL;
					if ( lastmv ) {
						lastmv->next = mv;
					} else {
						mvHash[v] = mv;
					}
					tri->numVerts++;
				}

				tri->indexes[tri->numIndexes] = mv - mvTable;
				tri->numIndexes++;
			}
		}

		// allocate space for the indexes and copy them
		if ( tri->numIndexes > mesh->numFaces * 3 ) {
			common->FatalError( "ConvertMAToModelSurfaces: index miscount in MA file %s", name.c_str() );
		}
		if ( tri->numVerts > mesh->numFaces * 3 ) {
			common->FatalError( "ConvertMAToModelSurfaces: vertex miscount in MA file %s", name.c_str() );
		}

		// an MA allows the texture coordinates to be scaled, translated, and rotated
		//BSM: Todo: Does Maya support this and if so how
		//if ( ase->materials.Num() == 0 ) {
			uOffset = vOffset = 0.0f;
			uTiling = vTiling = 1.0f;
			textureSin = 0.0f;
			textureCos = 1.0f;
		//} else {
		//	material = ase->materials[object->materialRef];
		//	uOffset = -material->uOffset;
		//	vOffset = material->vOffset;
		//	uTiling = material->uTiling;
		//	vTiling = material->vTiling;
		//	textureSin = idMath::Sin( material->angle );
		//	textureCos = idMath::Cos( material->angle );
		//}

		// now allocate and generate the combined vertexes
		R_AllocStaticTriSurfVerts( tri, tri->numVerts );
		for ( j = 0; j < tri->numVerts; j++ ) {
			mv = &mvTable[j];
			tri->verts[ j ].Clear();
			tri->verts[ j ].xyz = mesh->vertexes[ mv->v ];
			tri->verts[ j ].normal = mv->normal;
			*(unsigned *)tri->verts[j].color = *(unsigned *)mv->color;
			if ( mesh->numTVertexes != 0 ) {
				const idVec2 &tv = mesh->tvertexes[ mv->tv ];
				float u = tv.x * uTiling + uOffset;
				float v = tv.y * vTiling + vOffset;
				tri->verts[ j ].st[0] = u * textureCos + v * textureSin;
				tri->verts[ j ].st[1] = u * -textureSin + v * textureCos;
			}
		}

		R_StaticFree( mvTable );
		R_StaticFree( mvHash );
		R_StaticFree( tvRemap );
		R_StaticFree( vRemap );

		// see if we need to merge with a previous surface of the same material
		modelSurf = &this->surfaces[mergeTo[ objectNum ]];
		srfTriangles_t	*mergeTri = modelSurf->geometry;
		if ( !mergeTri ) {
			modelSurf->geometry = tri;
		} else {
			modelSurf->geometry = R_MergeTriangles( mergeTri, tri );
			R_FreeStaticTriSurf( tri );
			R_FreeStaticTriSurf( mergeTri );
		}
	}

	return true;
}

/*
=================
idRenderModelStatic::LoadOBJ
=================
*/
bool idRenderModelStatic::LoadOBJ( const char *fileName ) {
	obj_file_t *obj;
	{
		TRACE_CPU_SCOPE("Obj_Load");
		obj = OBJ_Load(fileName);
	}
	if (!obj)
		return false;

	TRACE_CPU_SCOPE("LoadOBJ postprocess");
	timeStamp = obj->timestamp;

	auto AreSame = [](const obj_index_t &a, const obj_index_t &b) -> bool {
		return (
			a.vertex == b.vertex &&
			a.normal == b.normal && 
			a.texcoord == b.texcoord
		);
	};

	struct FullVertex {
		obj_index_t data;
		int finalIdx;
		int next;
	};
	// for each geometric vertex, store a linked list of all vertex-uses with it
	idList<FullVertex> vertexListItems;
	idList<int> vertexListFirst;

	// create surface independently for every unique material used
	for (int materialIdx = (obj->usesUnknownMaterial ? -1 : 0); materialIdx < obj->materials.Num(); materialIdx++) {
		// select material
		const idMaterial *material = nullptr;
		if (materialIdx < 0)
			material = declManager->FindMaterial("_default");
		else
			material = obj->materials[materialIdx].material;

		// clear per-vertex structure
		vertexListFirst.SetNum(obj->vertices.Num());
		memset(vertexListFirst.Ptr(), -1, vertexListFirst.Allocated());
		vertexListItems.Clear();

		// index in vertexListItems for each triangle corner with current material
		idList<int> itemOfTriVert;
		assert(obj->indices.Num() == 3 * obj->materialIds.Num());

		// collect all distinct vertex-uses
		for (int f = 0; f < obj->materialIds.Num(); f++) {
			if (obj->materialIds[f] != materialIdx)
				continue;

			for (int c = 0; c < 3; c++) {
				const obj_index_t &vertUse = obj->indices[3 * f + c];
				// find item in linked list of its vertex
				int *list = &vertexListFirst[vertUse.vertex];
				int item;
				for (item = *list; item >= 0; item = vertexListItems[item].next)
					if (AreSame(vertexListItems[item].data, vertUse))
						break;
				if (item < 0) {
					// not found: add new item to the list
					item = vertexListItems.AddGrow({vertUse, -666, *list});
					*list = item;
				}
				itemOfTriVert.AddGrow(item);
			}
		}

		assert(itemOfTriVert.Num() % 3 == 0);
		int numTotalTris = itemOfTriVert.Num() / 3;
		int numTotalVerts = vertexListItems.Num();

		// allocate triangle surface
		srfTriangles_t *tri = R_AllocStaticTriSurf();
		tri->numVerts = numTotalVerts;
		tri->numIndexes = numTotalTris * 3;
		R_AllocStaticTriSurfVerts(tri, tri->numVerts);
		R_AllocStaticTriSurfIndexes(tri, tri->numIndexes);

		// assign final index for every vertex-use
		// make sure vertex-uses are ordered by their geometric vertex first!
		int vertexPos = 0;
		for (int v = 0; v < vertexListFirst.Num(); v++)
			for (int item = vertexListFirst[v]; item >= 0; item = vertexListItems[item].next) {
				int q = vertexPos++;
				vertexListItems[item].finalIdx = q;

				// also fill vertex in the output surface
				const obj_index_t &vertUse = vertexListItems[item].data;
				idDrawVert &outVertex = tri->verts[q];
				outVertex.Clear();
				memcpy(outVertex.xyz.ToFloatPtr(), &obj->vertices[vertUse.vertex], sizeof(idVec3));
				memcpy(outVertex.normal.ToFloatPtr(), &obj->normals[vertUse.normal], sizeof(idVec3));
				memcpy(outVertex.st.ToFloatPtr(), &obj->texcoords[vertUse.texcoord], sizeof(idVec2));
				idVec3 rgb = obj->colors[vertUse.vertex];
				outVertex.color[0] = idMath::ClampByte(0, 255, byte(255.0f * rgb[0]));
				outVertex.color[1] = idMath::ClampByte(0, 255, byte(255.0f * rgb[1]));
				outVertex.color[2] = idMath::ClampByte(0, 255, byte(255.0f * rgb[2]));
				outVertex.color[3] = 255;
			}
		assert(vertexPos == numTotalVerts);

		// fill the surface indices
		for (int q = 0; q < numTotalTris * 3; q++)
			tri->indexes[q] = vertexListItems[itemOfTriVert[q]].finalIdx;

		// add surface to model
		modelSurface_t surf;
		surf.geometry = tri;
		surf.id = surfaces.Num();
		surf.material = material;
		AddSurface(surf);
	}

	delete obj;

	return true;
}

/*
=================
idRenderModelStatic::LoadASE
=================
*/
bool idRenderModelStatic::LoadASE( const char *fileName ) {
	aseModel_t *ase;

	{
		TRACE_CPU_SCOPE("ASE_Load");
		ase = ASE_Load( fileName );
	}
	if ( ase == NULL ) {
		return false;
	}

	TRACE_CPU_SCOPE("ConvertASEToModelSurfaces");
	ConvertASEToModelSurfaces( ase );

	ASE_Free( ase );

	return true;
}

/*
=================
idRenderModelStatic::LoadLWO
=================
*/
bool idRenderModelStatic::LoadLWO( const char *fileName ) {
	unsigned int failID;
	int failPos;
	lwObject *lwo;

	{
		TRACE_CPU_SCOPE("lwGetObject");
		lwo = lwGetObject( fileName, &failID, &failPos );
	}
	if ( lwo == NULL ) {
		return false;
	}

	TRACE_CPU_SCOPE("ConvertLWOToModelSurfaces");
	ConvertLWOToModelSurfaces( lwo );

	lwFreeObject( lwo );

	return true;
}

/*
=================
idRenderModelStatic::LoadMA
=================
*/
bool idRenderModelStatic::LoadMA( const char *fileName ) {
	maModel_t *ma;

	ma = MA_Load( fileName );
	if ( ma == NULL ) {
		return false;
	}

	ConvertMAToModelSurfaces( ma );

	MA_Free( ma );

	return true;
}

/*
=================
idRenderModelStatic::LoadFLT

USGS height map data for megaTexture experiments
=================
*/
bool idRenderModelStatic::LoadFLT( const char *fileName ) {
	float	*data;
	int		len;

	len = fileSystem->ReadFile( fileName, (void **)&data );
	if ( len <= 0 ) {
		return false;
	}
	int	size = sqrt( len / 4.0f );

	// bound the altitudes
	float min = 9999999;
	float max = -9999999;
	for ( int i = 0 ; i < len/4 ; i++ ) {
	data[i] = BigFloat( data[i] );
	if ( data[i] == -9999 ) {
		data[i] = 0;		// unscanned areas
	}

		if ( data[i] < min ) {
			min = data[i];
		}
		if ( data[i] > max ) {
			max = data[i];
		}
	}
#if 1
	// write out a gray scale height map
	byte	*image = (byte *)R_StaticAlloc( len );
	byte	*image_p = image;
	for ( int i = 0 ; i < len/4 ; i++ ) {
		float v = ( data[i] - min ) / ( max - min );
		image_p[0] =
		image_p[1] =
		image_p[2] = v * 255;
		image_p[3] = 255;
		image_p += 4;
	}
	idStr	tgaName = fileName;
	tgaName.StripFileExtension();
	tgaName += ".tga";
	R_WriteTGA( tgaName.c_str(), image, size, size, false );
	R_StaticFree( image );
//return false;
#endif

	// find the island above sea level
	int	minX, maxX, minY, maxY;
	{
		int	i;	
		for ( minX = 0 ; minX < size ; minX++ ) {
			for ( i = 0 ; i < size ; i++ ) {
				if ( data[i*size + minX] > 1.0 ) {
					break;
				}
			}
			if ( i != size ) {
				break;
			}
		}

		for ( maxX = size-1 ; maxX > 0 ; maxX-- ) {
			for ( i = 0 ; i < size ; i++ ) {
				if ( data[i*size + maxX] > 1.0 ) {
					break;
				}
			}
			if ( i != size ) {
				break;
			}
		}

		for ( minY = 0 ; minY < size ; minY++ ) {
			for ( i = 0 ; i < size ; i++ ) {
				if ( data[minY*size + i] > 1.0 ) {
					break;
				}
			}
			if ( i != size ) {
				break;
			}
		}

		for ( maxY = size-1 ; maxY < size ; maxY-- ) {
			for ( i = 0 ; i < size ; i++ ) {
				if ( data[maxY*size + i] > 1.0 ) {
					break;
				}
			}
			if ( i != size ) {
				break;
			}
		}
	}

	int	width = maxX - minX + 1;
	int height = maxY - minY + 1;

//width /= 2;
	// allocate triangle surface
	srfTriangles_t *tri = R_AllocStaticTriSurf();
	tri->numVerts = width * height;
	tri->numIndexes = (width-1) * (height-1) * 6;

	fastLoad = true;		// don't do all the sil processing

	R_AllocStaticTriSurfIndexes( tri, tri->numIndexes );
	R_AllocStaticTriSurfVerts( tri, tri->numVerts );

	for ( int i = 0 ; i < height ; i++ ) {
		for ( int j = 0; j < width ; j++ ) {
			int		v = i * width + j;
			tri->verts[ v ].Clear();
			tri->verts[ v ].xyz[0] = j * 10;	// each sample is 10 meters
			tri->verts[ v ].xyz[1] = -i * 10;
			tri->verts[ v ].xyz[2] = data[(minY+i)*size+minX+j];	// height is in meters
			tri->verts[ v ].st[0] = (float) j / (width-1);
			tri->verts[ v ].st[1] = 1.0 - ( (float) i / (height-1) );
		}
	}

	for ( int i = 0 ; i < height-1 ; i++ ) {
		for ( int j = 0; j < width-1 ; j++ ) {
			int	v = ( i * (width-1) + j ) * 6;
#if 0
			tri->indexes[ v + 0 ] = i * width + j;
			tri->indexes[ v + 1 ] = (i+1) * width + j;
			tri->indexes[ v + 2 ] = (i+1) * width + j + 1;
			tri->indexes[ v + 3 ] = i * width + j;
			tri->indexes[ v + 4 ] = (i+1) * width + j + 1;
			tri->indexes[ v + 5 ] = i * width + j + 1;
#else
			tri->indexes[ v + 0 ] = i * width + j;
			tri->indexes[ v + 1 ] = i * width + j + 1;
			tri->indexes[ v + 2 ] = (i+1) * width + j + 1;
			tri->indexes[ v + 3 ] = i * width + j;
			tri->indexes[ v + 4 ] = (i+1) * width + j + 1;
			tri->indexes[ v + 5 ] = (i+1) * width + j;
#endif
		}
	}

	fileSystem->FreeFile( data );

	modelSurface_t	surface;

	surface.geometry = tri;
	surface.id = 0;
	surface.material = tr.defaultMaterial; // declManager->FindMaterial( "shaderDemos/megaTexture" );

	this->AddSurface( surface );

	return true;
}

/*
=================
idRenderModelStatic::LoadProxy
=================
*/
bool idRenderModelStatic::LoadProxy( const char *fileName ) {
	idParser parser( LEXFL_ALLOWPATHNAMES | LEXFL_NOSTRINGESCAPECHARS );
	if ( !parser.LoadFile( fileName ) ) {
		return false;
	}

	idRenderModel *sourceModel = nullptr;
	idMat3 rotation = mat3_identity;

	idToken token;
	while( parser.ReadToken( &token ) ) {
		if ( !token.Icmp( "model" ) ) {
			if ( parser.ReadToken( &token ) ) {
				sourceModel = renderModelManager->CheckModel( token );
				proxySourceName = token;
			}
		} else if ( !token.Icmp( "rotation" ) ) {
			if ( parser.ReadToken( &token ) ) {
				if (sscanf( token, "%f %f %f %f %f %f %f %f %f",
					&rotation[0].x, &rotation[0].y, &rotation[0].z,
					&rotation[1].x, &rotation[1].y, &rotation[1].z,
					&rotation[2].x, &rotation[2].y, &rotation[2].z
				) != 9) {
					rotation.Identity();
				}
			}
		} else {
			parser.Warning( "Unknown parameter '%s'", token.c_str() );
			return false;
		}
	}
	if ( !sourceModel ) {
		parser.Warning( "Source model not specified" );
		return false;
	}

	auto sourceModelStatic = dynamic_cast<idRenderModelStatic*>( sourceModel );
	if ( !sourceModelStatic ) {
		common->Warning( "Proxy model implemented only for static models, cannot handle '%s'", sourceModel->Name() );
		return false;
	}

	TransformModel( sourceModelStatic, rotation );
	return true;
}


//=============================================================================

/*
================
idRenderModelStatic::PurgeModel
================
*/
void idRenderModelStatic::PurgeModel() {
	modelSurface_t	*surf;

	for ( int i = 0 ; i < surfaces.Num() ; i++ ) {
		surf = &surfaces[i];

		if ( surf->geometry ) {
			R_FreeStaticTriSurf( surf->geometry );
		}
	}
	surfaces.ClearFree();

	purged = true;
}

/*
==============
idRenderModelStatic::FreeVertexCache

We are about to restart the vertex cache, so dump everything
==============
*/
void idRenderModelStatic::FreeVertexCache( void ) {
	for ( int j = 0 ; j < surfaces.Num() ; j++ ) {
		srfTriangles_t *tri = surfaces[j].geometry;
		if ( !tri ) {
			continue;
		}
		if ( tri->ambientCache.IsValid() ) {
			// we don't support freeing static data until next level load
			tri->ambientCache = NO_CACHE;
		}
		// static shadows may be present
		if ( tri->shadowCache.IsValid() ) {
			// we don't support freeing static data until next level load
			tri->shadowCache = NO_CACHE;
		}
	}
}

/*
================
idRenderModelStatic::ReadFromDemoFile
================
*/
void idRenderModelStatic::ReadFromDemoFile( class idDemoFile *f ) {
	PurgeModel();

	InitEmpty( f->ReadHashString() );

	int numSurfaces;
	f->ReadInt( numSurfaces );
	
	for ( int i = 0 ; i < numSurfaces ; i++ ) {
		modelSurface_t	surf;
		
		surf.material = declManager->FindMaterial( f->ReadHashString() );
		
		srfTriangles_t	*tri = R_AllocStaticTriSurf();
		
		f->ReadInt( tri->numIndexes );
		R_AllocStaticTriSurfIndexes( tri, tri->numIndexes );
		for ( int j = 0; j < tri->numIndexes; ++j )
			f->ReadInt( (int&)tri->indexes[j] );
		
		f->ReadInt( tri->numVerts );
		R_AllocStaticTriSurfVerts( tri, tri->numVerts );
		for ( int j = 0; j < tri->numVerts; ++j ) {
			f->ReadVec3( tri->verts[j].xyz );
			f->ReadVec2( tri->verts[j].st );
			f->ReadVec3( tri->verts[j].normal );
			f->ReadVec3( tri->verts[j].tangents[0] );
			f->ReadVec3( tri->verts[j].tangents[1] );
			f->ReadUnsignedChar( tri->verts[j].color[0] );
			f->ReadUnsignedChar( tri->verts[j].color[1] );
			f->ReadUnsignedChar( tri->verts[j].color[2] );
			f->ReadUnsignedChar( tri->verts[j].color[3] );
		}
		
		surf.geometry = tri;
		
		this->AddSurface( surf );
	}
	this->FinishSurfaces();
}

/*
================
idRenderModelStatic::WriteToDemoFile
================
*/
void idRenderModelStatic::WriteToDemoFile( class idDemoFile *f ) {
	int	data[1];

	// note that it has been updated
	lastArchivedFrame = tr.frameCount;

	data[0] = DC_DEFINE_MODEL;
	f->WriteInt( data[0] );
	f->WriteHashString( this->Name() );

	int iData = surfaces.Num();
	f->WriteInt( iData );

	for ( int i = 0 ; i < surfaces.Num() ; i++ ) {
		const modelSurface_t	*surf = &surfaces[i];
		
		f->WriteHashString( surf->material->GetName() );
		
		srfTriangles_t *tri = surf->geometry;
		f->WriteInt( tri->numIndexes );
		for ( int j = 0; j < tri->numIndexes; ++j )
			f->WriteInt( (int&)tri->indexes[j] );
		f->WriteInt( tri->numVerts );
		for ( int j = 0; j < tri->numVerts; ++j ) {
			f->WriteVec3( tri->verts[j].xyz );
			f->WriteVec2( tri->verts[j].st );
			f->WriteVec3( tri->verts[j].normal );
			f->WriteVec3( tri->verts[j].tangents[0] );
			f->WriteVec3( tri->verts[j].tangents[1] );
			f->WriteUnsignedChar( tri->verts[j].color[0] );
			f->WriteUnsignedChar( tri->verts[j].color[1] );
			f->WriteUnsignedChar( tri->verts[j].color[2] );
			f->WriteUnsignedChar( tri->verts[j].color[3] );
		}
	}
}

/*
================
idRenderModelStatic::IsLoaded
================
*/
bool idRenderModelStatic::IsLoaded( void ) const {
	return !purged;
}

/*
================
idRenderModelStatic::SetLevelLoadReferenced
================
*/
void idRenderModelStatic::SetLevelLoadReferenced( bool referenced ) {
	levelLoadReferenced = referenced;
}

/*
================
idRenderModelStatic::IsLevelLoadReferenced
================
*/
bool idRenderModelStatic::IsLevelLoadReferenced( void ) {
	return levelLoadReferenced;
}

/*
=================
idRenderModelStatic::TouchData
=================
*/
void idRenderModelStatic::TouchData( void ) {
	for ( int i = 0 ; i < surfaces.Num() ; i++ ) {
		const modelSurface_t	*surf = &surfaces[i];

		// re-find the material to make sure it gets added to the
		// level keep list
		declManager->FindMaterial( surf->material->GetName() );
	}
}

/*
=================
idRenderModelStatic::DeleteSurfaceWithId
=================
*/
bool idRenderModelStatic::DeleteSurfaceWithId( int id ) {
	for ( int i = 0; i < surfaces.Num(); i++ ) {
		if ( surfaces[i].id == id ) {
			R_FreeStaticTriSurf( surfaces[i].geometry );
			surfaces.RemoveIndex( i );
			return true;
		}
	}
	return false;
}

/*
=================
idRenderModelStatic::DeleteSurfacesWithNegativeId
=================
*/
void idRenderModelStatic::DeleteSurfacesWithNegativeId( void ) {
	for ( int i = 0; i < surfaces.Num(); i++ ) {
		if ( surfaces[i].id < 0 ) {
			R_FreeStaticTriSurf( surfaces[i].geometry );
			surfaces.RemoveIndex( i );
			i--;
		}
	}
}

/*
=================
idRenderModelStatic::FindSurfaceWithId
=================
*/
bool idRenderModelStatic::FindSurfaceWithId( int id, int &surfaceNum ) {
	for ( int i = 0; i < surfaces.Num(); i++ ) {
		if ( surfaces[i].id == id ) {
			surfaceNum = i;
			return true;
		}
	}
	return false;
}


void idRenderModelStatic::TransformModel( const idRenderModelStatic *sourceModel, const idMat3 &rotation ) {
	idMat3 normalRotation = rotation.Inverse().Transpose();

	for (int s = 0; s < sourceModel->surfaces.Num(); s++) {
		const modelSurface_t &surf = sourceModel->surfaces[s];
		//clone data
		modelSurface_t newSurf;
		newSurf.id = surf.id;
		newSurf.material = surf.material;
		srfTriangles_t *newTri = R_CopyStaticTriSurf(surf.geometry);
		newSurf.geometry = newTri;

		for (int v = 0; v < newTri->numVerts; v++) {
			idDrawVert &vert = newTri->verts[v];
			vert.xyz = rotation * vert.xyz;
			vert.normal = normalRotation * vert.normal;
			vert.normal.Normalize();
			vert.tangents[0] = rotation * vert.tangents[0];
			vert.tangents[1] = rotation * vert.tangents[1];
		}

		AddSurface(newSurf);
	}
}

void idRenderModelStatic::GenerateSamples( idList<samplePointOnModel_t> &samples, const modelSamplingParameters_t &params, idRandom &rnd ) const {

	int numSurfs = NumBaseSurfaces();

	// count triangles across surfaces
	idList<int> surfaceStarts;
	surfaceStarts.SetNum( numSurfs + 1 );
	for ( int s = 0; s < numSurfs; s++ ) {
		int numTris = surfaces[s].geometry->numIndexes / 3;
		surfaceStarts[s + 1] = numTris;
	}
	// prepare prefix sums of tri counts
	surfaceStarts[0] = 0;
	for ( int s = 0; s < numSurfs; s++ )
		surfaceStarts[s + 1] += surfaceStarts[s];
	int totalTris = surfaceStarts[numSurfs];

	// compute areas of triangles
	idList<float> triAreas;
	triAreas.SetNum( totalTris + 1 );
	int pos = 0;
	for ( int s = 0; s < numSurfs; s++ ) {
		const srfTriangles_t *tri = surfaces[s].geometry;
		int cnt = surfaceStarts[s + 1] - surfaceStarts[s];
		for ( int i = 0; i < cnt; i++ ) {
			int va = tri->indexes[3 * i + 0];
			int vb = tri->indexes[3 * i + 1];
			int vc = tri->indexes[3 * i + 2];
			const idVec3 &pa = tri->verts[va].xyz;
			const idVec3 &pb = tri->verts[vb].xyz;
			const idVec3 &pc = tri->verts[vc].xyz;
			triAreas[1 + (pos++)] = 0.5f * (pb - pa).Cross(pc - pa).Length();
		}
	}
	assert( pos == totalTris );
	// prepare prefix sums of triangles areas
	triAreas[0] = 0.0f;
	for ( int i = 0; i < totalTris; i++ )
		triAreas[i + 1] += triAreas[i];

	// compute desired number of samples according to settings
	float totalArea = triAreas.Last();
	float totalSamplesFloat = totalArea / idMath::Fmax( params.areaPerSample, 1.0f );
	int totalSamples = (int) idMath::Fmin( totalSamplesFloat, params.maxSamplesTotal );

	// distribute sample budget across surfaces
	idList<int> numSamplesOnSurf;
	numSamplesOnSurf.SetNum( numSurfs );
	for ( int s = 0; s < numSurfs; s++ ) {
		float ratio = ( triAreas[surfaceStarts[s + 1]] - triAreas[surfaceStarts[s]] ) / totalArea;
		numSamplesOnSurf[s] = idMath::FtoiRound( ratio * totalSamples );
		// note: we absolutely want at least 1 sample on each surface
		// because all the other surfaces can get skin-mapped to nodraw at any moment!
		numSamplesOnSurf[s] = idMath::Imax( numSamplesOnSurf[s], params.minSamplesPerSurface );
		if (surfaceStarts[s + 1] == surfaceStarts[s])
			numSamplesOnSurf[s] = 0;	// empty surface
	}
	totalSamples = 0;
	for ( int s = 0; s < numSurfs; s++ )
		totalSamples += numSamplesOnSurf[s];

	samples.Clear();
	for ( int s = 0; s < numSurfs; s++ ) {
		const srfTriangles_t *tri = surfaces[s].geometry;
		int cntBeg = surfaceStarts[s + 0];
		int cntEnd = surfaceStarts[s + 1];
		float areaBeg = triAreas[cntBeg];
		float areaEnd = triAreas[cntEnd];
		if ( numSamplesOnSurf[s] == 0 )
			continue;

		auto RandomSampleOnSurface = [&]() -> samplePointOnModel_t {
			// use binary search to see which triangle we have picked
			float param = areaBeg + rnd.RandomFloat() * ( areaEnd - areaBeg );
			int i = idBinSearch_Less( triAreas.Ptr() + cntBeg, cntEnd - cntBeg, param );

			// fetch triangle
			int va = tri->indexes[3 * i + 0];
			int vb = tri->indexes[3 * i + 1];
			int vc = tri->indexes[3 * i + 2];
			const idVec3 &pa = tri->verts[va].xyz;
			const idVec3 &pb = tri->verts[vb].xyz;
			const idVec3 &pc = tri->verts[vc].xyz;
			// generate random barycentric coordinates
			float u = rnd.RandomFloat();
			float v = rnd.RandomFloat();
			if ( u + v > 1.0f ) {
				u = 1.0f - u;
				v = 1.0f - v;
			}
			float w = 1.0f - u - v;
			idVec3 pos = u * pa + v * pb + w * pc;

			// fill sample
			samplePointOnModel_t smp;
			smp.surfaceIndex = s;
			smp.triangleIndex = i;
			smp.baryCoords = idVec3(u, v, w);
			smp.staticPosition = pos;
			return smp;
		};

		idList<samplePointOnModel_t> samplesOnSurf;
		samplesOnSurf.SetNum( numSamplesOnSurf[s], false );
		bool generated = false;
		if ( params.poisson ) {
			// poisson-like sampling (quite slow but covers model better)

			exponentialSearchParams_t params;
			params.initValue = idMath::Sqrt( ( areaEnd - areaBeg ) / samplesOnSurf.Num() );
			params.minValue = 1.0f;
			params.maxValue = 1e+5f;
			params.absolutePrecision = params.relativePrecision = 0.3f;

			if ( GeneratePoissonSamples(
				samplesOnSurf, 10, params,
				RandomSampleOnSurface,
				[] ( const samplePointOnModel_t &a, const samplePointOnModel_t &b, float thres ) {
					float distSq = ( a.staticPosition - b.staticPosition ).LengthSqr();
					return distSq <= thres * thres;
				}
			) ) {
				generated = true;
			}
		}

		if ( !generated ) {
			// compute all samples the simple way
			for ( int q = 0; q < samplesOnSurf.Num(); q++ ) {
				samplesOnSurf[q] = RandomSampleOnSurface();
			}
		}

		// add these samples to output
		samples.Append( samplesOnSurf );
	}

	// reorder samples randomly
	for ( int i = 0; i < samples.Num(); i++ ) {
		int j = rnd.RandomInt() % ( i + 1 );
		idSwap( samples[i], samples[j] );
	}
}

idVec3 idRenderModelStatic::GetSamplePosition( const struct renderEntity_s *ent, const samplePointOnModel_t &sample ) const {
	return sample.staticPosition;
}

const idMaterial *idRenderModelStatic::GetSampleMaterial( const struct renderEntity_s *ent, const samplePointOnModel_t &sample ) const {
	const idMaterial *material = surfaces[sample.surfaceIndex].material;
	material = R_RemapShaderBySkin( material, ent->customSkin, ent->customShader );
	return material;
}
