/*==LICENSE==*

CyanWorlds.com Engine - MMOG client, server and tools
Copyright (C) 2011  Cyan Worlds, Inc.

This program 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.

This program 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 this program.  If not, see <http://www.gnu.org/licenses/>.

You can contact Cyan Worlds, Inc. by email legal@cyan.com
 or by snail mail at:
      Cyan Worlds, Inc.
      14617 N Newport Hwy
      Mead, WA   99021

*==LICENSE==*/
//////////////////////////////////////////////////////////////////////////////
//																			//
//	plMeshConverter Class Functions											//
//																			//
//// Version History /////////////////////////////////////////////////////////
//																			//
//	Created 4.18.2001 mcn													//
//																			//
//////////////////////////////////////////////////////////////////////////////

#include "hsTypes.h"
#include "Max.h"
#include "iparamb2.h"
#include "modstack.h"
#include "ISkin.h"
#include "meshdlib.h" 


#include "HeadSpin.h"
#include "../CoreLib/hsBitVector.h"
#include "plMeshConverter.h"
#include "hsResMgr.h"
#include "../MaxMain/plMaxNode.h"
#include "../MaxExport/plErrorMsg.h"
#include "../plSurface/hsGMaterial.h"
#include "../plSurface/plLayerInterface.h"
#include "../plDrawable/plGeometrySpan.h"
#include "hsConverterUtils.h"
#include "hsMaterialConverter.h"
#include "hsControlConverter.h"
#include "hsExceptionStack.h"
#include "../MaxPlasmaMtls/Materials/plCompositeMtl.h"
#include "../MaxPlasmaMtls/Materials/plPassMtl.h"
#include "../MaxPlasmaMtls/Materials/plCompositeMtlPB.h"
#include "../MaxPlasmaMtls/Materials/plPassMtlBasicPB.h"
#include "../plPipeline/plGBufferGroup.h"
#include "../plParticleSystem/plConvexVolume.h"
#include "../plDrawable/plGeoSpanDice.h"

#include "../plDrawable/plAccessGeometry.h"
#include "../plDrawable/plAccessSpan.h"
#include "../plDrawable/plAccessTriSpan.h"
#include "../plDrawable/plAccessVtxSpan.h"

#include "../plStatusLog/plStatusLog.h"

#include "plTweak.h"

//// Static Members //////////////////////////////////////////////////////////

hsBool	plMeshConverter::fWarnBadNormals = true;
char	plMeshConverter::fWarnBadNormalsMsg[] = "Bad normal autogeneration - please deliver Max file to QA";

hsBool	plMeshConverter::fWarnBadUVs = true;
char	plMeshConverter::fWarnBadUVsMsg[] = "The object \"%s\" does not have enough valid UVW mapping channels \
for the material(s) applied to it. This might produce unwanted rendering artifacts at runtime";

hsBool	plMeshConverter::fWarnSuspiciousUVs = true;
char	plMeshConverter::fWarnSuspiciousUVsMsg[] = "The object \"%s\" has suspicious UVW coordinates on it. \
You should apply an Unwrap UVW modifier to it.";

char	plMeshConverter::fTooManyVertsMsg[] = "The mesh \"%s\" has too many vertices to fit into a single buffer. \
Please break up the mesh into pieces with no more than %u vertices each\
or apply optimize terrain.";

char	plMeshConverter::fTooManyFacesMsg[] = "The mesh \"%s\" has too many faces to fit into a single buffer. \
Please break up the mesh into pieces with no more than %u faces each\
or apply optimize terrain.";


//// Local Helper Class Definitions //////////////////////////////////////////

class TempWeightInfo
{
	public:
		float	fWeights[ 4 ];
		UInt32	fIndices;
};


class plMAXVertexAccNode
{
	public:
		hsPoint3	fPoint;	// Inefficient space-wise, I know, but it makes this a lot simpler...
		hsVector3	fNormal;
		hsColorRGBA	fColor, fIllum;
		UInt32		fIndex;
		hsPoint3	fUVs[ plGeometrySpan::kMaxNumUVChannels ];
		UInt32		fNumChannels;

		plMAXVertexAccNode	*fNext;

		plMAXVertexAccNode( const hsPoint3 *point, const hsVector3 *normal, const hsColorRGBA& color, const hsColorRGBA& illum, int numChannels, const hsPoint3 *uvs, UInt32 index );

		hsBool		IsEqual( const hsVector3 *normal, const hsColorRGBA& color, const hsColorRGBA& illum, const hsPoint3 *uvs );
};

typedef plMAXVertexAccNode		*plMAXVertexAccNodePtr;

class plMAXVertexAccumulator
{
	protected:
		
		int					fNumPoints, fNumChannels, fNumVertices;
		plMAXVertexAccNode	**fPointList;

		hsTArray<UInt32>	fIndices;
		hsTArray<UInt32>	fInverseVertTable;

		void	IFindSkinWeights( ISkinContextData *skinData, int vertex, float *weights, UInt32 *indices );
		void	IFindUserSkinWeights( plMaxNode* node, Mesh* mesh, int vertex, float *weights, UInt32 *indices );
		void	IFindAllUserSkinWeights( plMaxNode* node, Mesh* mesh, TempWeightInfo weights[]);
	public:

		plMAXVertexAccumulator( int numOrigPoints, int numChannels );
		~plMAXVertexAccumulator();

		void		AddVertex( int index, hsPoint3 *point, hsVector3 *normal, const hsColorRGBA& color, const hsColorRGBA& illum, hsPoint3 uvs[ plGeometrySpan::kMaxNumUVChannels ] );

		void		StuffMyData( plMaxNode* node, plGeometrySpan *span, Mesh* mesh, ISkinContextData* skinData );

		int			GetVertexCount();
		UInt32		GetIndexCount( void ) { return fIndices.GetCount(); }
};

class plMAXVertNormal
{
	public:
		Point3	fNormal;
		DWORD	fSmGroup;
		bool	fInited;

		plMAXVertNormal	*fNext;

		plMAXVertNormal() { fSmGroup = 0; fNext = nil; fInited = false; fNormal = Point3( 0, 0, 0 ); }
		plMAXVertNormal( Point3 &n, DWORD s ) { fNext = nil; fInited = true; fNormal = n; fSmGroup = s; }
		~plMAXVertNormal() { /*delete fNext; */}
		void	DestroyChain( void ) { if( fNext != nil ) fNext->DestroyChain(); delete fNext; fNext = nil; }

		// Adding normalization of input n. Input is usually just crossproduct of face edges. Non-normalized,
		// large faces will overwhelm small faces on summation, which is the opposite of what we want, since
		// curvature is most accurately captured in the small faces. mf.
		void	AddNormal( Point3 &n, DWORD s )
		{
			if( !( s & fSmGroup ) && fInited )
			{
				if( fNext )
					fNext->AddNormal( n, s );
				else
					fNext = TRACKED_NEW plMAXVertNormal( ::Normalize(n), s );
			}
			else
			{
				fNormal += ::Normalize(n);
				fSmGroup |= s;
				fInited = true;
			}
		}

		Point3	&GetNormal( DWORD s )
		{
			if( ( fSmGroup & s ) || ( fNext == nil ) )
				return fNormal;
			else
				return fNext->GetNormal( s );
		}

		/// Relies on static variable, values will be destroyed between calls
		hsVector3	&GetPlNormal( DWORD s )
		{
			Point3	n = GetNormal( s );
			static hsVector3	pt;
			pt.Set( n.x, n.y, n.z );
			return pt;
		}

		void	Normalize( void )
		{
			plMAXVertNormal	*ptr = fNext, *prev = this;


			while( ptr != nil )
			{
				if( ptr->fSmGroup & fSmGroup )
				{
					fNormal += ptr->fNormal;
					prev->fNext = ptr->fNext;
					delete ptr;
					ptr = prev->fNext;
				}
				else
				{
					prev = ptr;
					ptr = ptr->fNext;
				}
			}

			fNormal = ::Normalize( fNormal );
			if( fNext )
				fNext->Normalize();
		}
};

//// Instance and Constructor/Destructor /////////////////////////////////////

plMeshConverter& plMeshConverter::Instance()
{
	static plMeshConverter the_instance;
	return the_instance;
}

plMeshConverter::plMeshConverter() :
					fInterface(nil),
					fConverterUtils(hsConverterUtils::Instance()),
					fIsInitialized(false)
{
	hsGuardBegin("plMeshConverter::plMeshConverter");
	hsGuardEnd;
}

plMeshConverter::~plMeshConverter()
{
	hsGuardBegin("plMeshConverter::~plMeshConverter");
	hsGuardEnd;
}

//// Init and DeInit /////////////////////////////////////////////////////////

void	plMeshConverter::Init( hsBool save, plErrorMsg *msg )
{
	hsGuardBegin( "plMeshConverter::Init" );

	if( fIsInitialized )
		DeInit( false );

	fIsInitialized = true;
    fInterface = GetCOREInterface();
    fErrorMsg = msg;
	fWarnBadUVs = true;
	fWarnSuspiciousUVs = true;
	fWarnBadNormals = true;

	hsGuardEnd;
}

void	plMeshConverter::DeInit( hsBool deInitLongRecur )
{
	hsGuardBegin( "plMeshConverter::DeInit" );

	fIsInitialized = false;

	hsGuardEnd;
}

void plMeshConverter::StuffPositionsAndNormals(plMaxNode *node, hsTArray<hsPoint3> *pos, hsTArray<hsVector3> *normals)
{
	hsGuardBegin( "plMeshConverter::BuildNormalsArray" );

	const char* dbgNodeName = node->GetName();
	Mesh			*mesh;
	Int32			numVerts;
	hsMatrix44		l2wMatrix, vert2LMatrix, vertInvTransMatrix, tempMatrix;

	/// Get da mesh
	mesh = IGetNodeMesh( node );
	if( mesh == nil )
		return ;

	numVerts = mesh->getNumVerts();

	/// Get transforms
	l2wMatrix = node->GetLocalToWorld44();
	vert2LMatrix = node->GetVertToLocal44();			// vert2LMatrix is the transform we apply 
														// now to the verts, to get into *our* object-local-space
	vert2LMatrix.GetInverse( &tempMatrix );
	tempMatrix.GetTranspose( &vertInvTransMatrix );	// Inverse-transpose of the vert2Local matrix, 
													// for xforming the normals
	mesh->buildNormals();

	normals->SetCount(numVerts);
	pos->SetCount(numVerts);
	int i;
	for (i = 0; i < numVerts; i++)
	{
		// positions
		hsPoint3 currPos;
		currPos.Set(mesh->verts[i].x, mesh->verts[i].y, mesh->verts[i].z);
		pos->Set(i, vert2LMatrix * currPos);

		// normals
		RVertex &rv = mesh->getRVert(i);
		Point3& norm = rv.rn.getNormal();
		hsVector3 currNorm(norm.x, norm.y, norm.z);
		currNorm.Normalize();
		normals->Set(i, vertInvTransMatrix * currNorm);
	}

	IDeleteTempGeometry();
	hsGuardEnd; 
}

plConvexVolume *plMeshConverter::CreateConvexVolume(plMaxNode *node)
{
	hsGuardBegin( "plMeshConverter::CreateConvexVolume" );

	const char* dbgNodeName = node->GetName();
	Mesh			*mesh;
	Int32			numFaces, i, j, numVerts;
	hsMatrix44		l2wMatrix, vert2LMatrix, vertInvTransMatrix, tempMatrix;
	hsBool			flipOrder, checkForOverflow = false;

	/// Get da mesh
	mesh = IGetNodeMesh( node );
	if( mesh == nil )
		return nil;

	numFaces = mesh->getNumFaces();
	numVerts = mesh->getNumVerts();

	plConvexVolume *bounds = TRACKED_NEW plConvexVolume();

	/// Get transforms
	l2wMatrix = node->GetLocalToWorld44();
	vert2LMatrix = node->GetVertToLocal44();
	flipOrder = vert2LMatrix.GetParity();				// vert2LMatrix is the transform we apply 
														// now to the verts, to get into *our* object-local-space
	vert2LMatrix.GetInverse( &tempMatrix );
	tempMatrix.GetTranspose( &vertInvTransMatrix );	// Inverse-transpose of the vert2Local matrix, 
													// for xforming the normals


	//mesh->buildNormals();

	for( i = 0; i < numFaces; i++ )
	{
		Face		*maxFace = &mesh->faces[ i ];
		TVFace		*maxColorFace = ( mesh->vcFace != nil ) ? &mesh->vcFace[ i ] : nil;
		hsPoint3	pos[ 3 ];
		hsPoint3	testPt1, testPt2, testPt3;
		hsVector3	normal;
		UInt32		vertIdx[ 3 ];

		/// Add the 3 vertices to the correct vertex accumulator object

		// Get positions
		if( flipOrder )
		{
			for( j = 0; j < 3; j++ )
			{
				vertIdx[ j ] = maxFace->getVert( 2 - j );
				pos[ j ].fX = mesh->verts[ vertIdx[ j ] ].x;
				pos[ j ].fY = mesh->verts[ vertIdx[ j ] ].y;
				pos[ j ].fZ = mesh->verts[ vertIdx[ j ] ].z;
			}
		}
		else
		{
			for( j = 0; j < 3; j++ )
			{
				vertIdx[ j ] = maxFace->getVert( j );
				pos[ j ].fX = mesh->verts[ vertIdx[ j ] ].x;
				pos[ j ].fY = mesh->verts[ vertIdx[ j ] ].y;
				pos[ j ].fZ = mesh->verts[ vertIdx[ j ] ].z;
			}
		}

		// Look for degenerate triangles (why MAX even gives us these I have no clue...)
		testPt1 = pos[ 1 ] - pos[ 0 ];
		testPt2 = pos[ 2 ] - pos[ 0 ];
		testPt3 = pos[ 2 ] - pos[ 1 ];
		if( ( testPt1.fX == 0.0f && testPt1.fY == 0.0f && testPt1.fZ == 0.0f ) ||
			( testPt2.fX == 0.0f && testPt2.fY == 0.0f && testPt2.fZ == 0.0f ) ||
			( testPt3.fX == 0.0f && testPt3.fY == 0.0f && testPt3.fZ == 0.0f ) )
		{
			continue;
		}

		// Translate to local space
		for( j = 0; j < 3; j++ )
			pos[ j ] = vert2LMatrix * pos[ j ];

		// Calculate normal for face
		hsVector3	v1, v2;
		v1.Set( &pos[ 1 ], &pos[ 0 ] );
		v2.Set( &pos[ 2 ], &pos[ 0 ] );
		normal = (v1 % v2);
		normal.Normalize();

		hsPlane3 plane(&normal, normal.InnerProduct(pos[0]));
		bounds->AddPlane(plane); // auto-checks for redundant planes.
	}

	IDeleteTempGeometry();

	return bounds;
	hsGuardEnd; 
}

//
// Sometimes objects can have faces without UV coordinates.  These faces will
// export random UV values each time we export, changing the data and forcing
// patches when they aren't necessary.  To detect these, we put a Unwrap UVW mod
// on the object and see if it changes the UV values.
//
bool plMeshConverter::IValidateUVs(plMaxNode* node)
{
	if (node->GetObjectRef()->SuperClassID() != GEN_DERIVOB_CLASS_ID)
		return true;

	Mesh* mesh = IGetNodeMesh(node);
	if (!mesh)
		return true;

	if (mesh->getNumMaps() < 2)
		return true;

	// Cache the original UV verts
	int numVerts = mesh->getNumMapVerts(1);
	int vertBufSize = sizeof(UVVert)*numVerts;
	UVVert* origVerts = TRACKED_NEW UVVert[vertBufSize];
	memcpy(origVerts, mesh->mapVerts(1), vertBufSize);

	IDeleteTempGeometry();

	// Add an Unwrap UVW mod onto the stack
	IDerivedObject* derivedObject = (IDerivedObject*)node->GetObjectRef();
	#define UNWRAP_UVW_CID Class_ID(0x02df2e3a, 0x72ba4e1f)
	Modifier* mod = (Modifier*)GetCOREInterface()->CreateInstance(OSM_CLASS_ID, UNWRAP_UVW_CID);
	derivedObject->AddModifier(mod);

	mesh = IGetNodeMesh(node);

	bool uvsAreBad = false;

	UVVert* newVerts = mesh->mapVerts(1);
	for (int i = 0; i < numVerts; i++)
	{
		UVVert uvDiff = newVerts[i] - origVerts[i];
		float diff = uvDiff.Length();
		if (diff > 0.01)
		{
			uvsAreBad = true;
			break;
		}
	}

	delete [] origVerts;
	IDeleteTempGeometry();

	derivedObject->DeleteModifier();

	if (uvsAreBad)
	{
		TSTR logfile = "UV_";
		logfile += GetCOREInterface()->GetCurFileName();
		logfile += ".log";
		plStatusLog::AddLineS(logfile, "%s has suspicious UVs", node->GetName());
		

		if (fWarnSuspiciousUVs)
		{
			/// We're missing some UV channels on our object. We'll handle it later; warn the user here
			if (fErrorMsg->Set(true, "UVW Warning", fWarnSuspiciousUVsMsg, node->GetName()).CheckAskOrCancel())
				fWarnSuspiciousUVs = false;
			fErrorMsg->Set(false);
		}
	}

	return uvsAreBad;
}

//// CreateSpans /////////////////////////////////////////////////////////////
//	Main function. Takes a maxNode's object and creates geometrySpans from it
//	suitable for drawing as ice.

hsBool	plMeshConverter::CreateSpans( plMaxNode *node, hsTArray<plGeometrySpan *> &spanArray, bool doPreshading )
{
	hsGuardBegin( "plMeshConverter::CreateSpans" );

	const char* dbgNodeName = node->GetName();
	Mesh			*mesh;
	Int32			numFaces, i, j, k, numVerts, maxNumBones, maxUVWSrc;
	Int32			numMaterials = 1, numSubMaterials = 1;
	hsMatrix44		l2wMatrix, vert2LMatrix, vertInvTransMatrix, tempMatrix;
	Mtl				*maxMaterial = nil;
	hsBool			isComposite, isMultiMat, flipOrder, checkForOverflow = false, includesComp;
	UInt8			ourFormat, numChannels, maxBlendChannels;
	hsColorRGBA		*colorArray = nil;
	hsColorRGBA		*illumArray = nil;
	UInt32			sharedSpanProps = 0;
	hsBitVector		usedSubMtls;
	hsBool			makeAlphaLayer = node->VtxAlphaNotAvailable();

	ISkinContextData	*skinData;

	hsTArray<hsTArray<plExportMaterialData> *>		ourMaterials;
	hsTArray<hsTArray<plMAXVertexAccumulator *> *>	ourAccumulators;

	hsTArray<plMAXVertNormal>		vertNormalCache;
	hsTArray<plMAXVertNormal>*		vertDPosDuCache = nil;
	hsTArray<plMAXVertNormal>*		vertDPosDvCache = nil;

	//// Setup ///////////////////////////////////////////////////////////////
	plLocation nodeLoc = node->GetLocation(); 
	
	TimeValue timeVal = fConverterUtils.GetTime(fInterface);
	Class_ID cid = node->EvalWorldState(timeVal).obj->ClassID();
	if( node->EvalWorldState(timeVal).obj->ClassID() == BONE_OBJ_CLASSID )
		return false;

	IValidateUVs(node);

	/// Get da mesh
	mesh = IGetNodeMesh( node );
	if( mesh == nil )
		return false;
	numFaces = mesh->getNumFaces();
	numVerts = mesh->getNumVerts();

	/// Get the material
	maxMaterial = hsMaterialConverter::Instance().GetBaseMtl( node );
	isMultiMat = hsMaterialConverter::Instance().IsMultiMat( maxMaterial );

	const hsBool smoothAll = node->GetSmoothAll();

	includesComp = false;
	if (isMultiMat)
	{
		for (i = 0; i < numFaces; i++)
		{
			int index = mesh->faces[i].getMatID();
			if (index >= maxMaterial->NumSubMtls())
				index = 0;

			usedSubMtls.SetBit(index);
			if (hsMaterialConverter::Instance().IsCompositeMat(maxMaterial->GetSubMtl(index)))
				includesComp = true;
		}
	}
	else
		includesComp = hsMaterialConverter::Instance().IsCompositeMat(maxMaterial);
	
	try
	{

		/// Check vert count
		if( numVerts >= plGBufferGroup::kMaxNumVertsPerBuffer || numFaces * 3 >= plGBufferGroup::kMaxNumIndicesPerBuffer )
		{
			/// Possible overflow, but not sure. Only check for overflow if this is set
			checkForOverflow = true;
		}

		/// Get transforms
		l2wMatrix = node->GetLocalToWorld44();
		vert2LMatrix = node->GetVertToLocal44();
		flipOrder = vert2LMatrix.GetParity();				// vert2LMatrix is the transform we apply 
															// now to the verts, to get into *our* object-local-space
		vert2LMatrix.GetInverse( &tempMatrix );
		tempMatrix.GetTranspose( &vertInvTransMatrix );	// Inverse-transpose of the vert2Local matrix, 
														// for xforming the normals

		// OTM used in generating normals.
		Matrix3 otm = node->GetOTM();
		Matrix3 invOtm = Inverse(otm);
		invOtm.SetTrans(Point3(0,0,0));
		invOtm.ValidateFlags();

		// If we use a composite on this object, we don't want to export the illumination channel.
		UVVert *illumMap = mesh->mapVerts(MAP_SHADING);
		int numIllumVerts = mesh->getNumMapVerts(MAP_SHADING);

		UVVert *alphaMap = mesh->mapVerts(MAP_ALPHA);	
		int numAlphaVerts = mesh->getNumMapVerts(MAP_ALPHA);

		if( node->GetRunTimeLight() )
		{
			sharedSpanProps |= plGeometrySpan::kPropRunTimeLight;
		}
		if( node->GetNoPreShade() )
		{
			sharedSpanProps |= plGeometrySpan::kPropNoPreShade;
		}
		hsScalar waterHeight = 0;
		if( node->GetHasWaterHeight() )
		{
			sharedSpanProps |= plGeometrySpan::kWaterHeight;
			waterHeight = node->GetWaterHeight();
		}
		/// Which lighting equation?
		if( node->NonVtxPreshaded() )
		{
			/// OK, we can go with kLiteVtxNonPreshaded, so we get vertex alpha. Yipee!!!
			sharedSpanProps |= plGeometrySpan::kLiteVtxNonPreshaded;
		}
		
		//// Vertex Colors / Illumination ////////////////////////////////////////

		/// If there are colors, pre-convert them 
		hsColorRGBA white, black;
		white.Set(1.f, 1.f, 1.f, 1.f);
		black.Set(0, 0, 0, 1.f);
		hsBool allWhite = true, allBlack = true;

		if( mesh->numCVerts > 0)
		{
			if (mesh->vertCol != nil)
			{
				colorArray = TRACKED_NEW hsColorRGBA[ mesh->numCVerts ];
				for( i = 0; i < mesh->numCVerts; i++ )	
				{	
					colorArray[i].Set(mesh->vertCol[ i ].x, mesh->vertCol[ i ].y, mesh->vertCol[ i ].z, 1.f);
					if (colorArray[ i ] != black)
						allBlack = false;
				}

				// XXX Sometimes 3DS reports that all colors have been set black (when they haven't been touched).
				// We set them white here, so that they don't affect the shader when multiplied in.
				// (Sometimes it reports them as all white too, but hey, that's the value we'd use anyway...)
				if (allBlack)
					for( i = 0; i < mesh->numCVerts; i++ )
						colorArray[ i ] = white;
			}
		}

		if (illumMap != nil)
		{
			// MF_HORSE CARNAGE
			illumArray = TRACKED_NEW hsColorRGBA[numIllumVerts];
			for( i = 0; i < numIllumVerts; i++ )
			{
				illumArray[i].Set(illumMap[ i ].x, illumMap[ i ].y, illumMap[ i ].z, 1.f);
				if (illumArray[ i ] != white)
					allWhite = false;
			}

			// XXX Same hack as with colorArray above, except illumination values are added in, so we set them black
			// in order to not affect the shader.
			if (allWhite)
				for( i = 0; i < numIllumVerts; i++ )
					illumArray[ i ] = black;
			// MF_HORSE CARNAGE
		}
		
		//// Materials / Mapping Channels Setup //////////////////////////////////

		numChannels = node->NumUVWChannels();

		maxBlendChannels = 0;
		
		if( isMultiMat )
		{
			numMaterials = maxMaterial->NumSubMtls();

			ourMaterials.SetCountAndZero( numMaterials );
			for( i = 0; i < numMaterials; i++ )
			{
				if (usedSubMtls.IsBitSet(i)) // Only export the sub materials actually used
					ourMaterials[i] = hsMaterialConverter::Instance().CreateMaterialArray( maxMaterial->GetSubMtl(i), node, i);
				else
					ourMaterials[i] = nil;
			}
		}
		else // plPassMtl, plDecalMat, plMultiPassMtl
		{
			numMaterials = 1;
			
			ourMaterials.Reset();
			ourMaterials.Append(hsMaterialConverter::Instance().CreateMaterialArray( maxMaterial, node, 0 ));
		}

		/// UV check on the layers
		for( i = 0, maxUVWSrc = -1; i < numMaterials; i++ )
		{
			hsTArray<plExportMaterialData> *subMats = ourMaterials[i];
			if (subMats == nil)
				continue;
			for( j = 0; j < subMats->GetCount(); j++ )
			{				
				plExportMaterialData currData = subMats->Get(j);
				if (currData.fMaterial == nil)
					continue;
				
				for( k = 0; k < currData.fMaterial->GetNumLayers(); k++ )
				{
					plLayerInterface	*layer = currData.fMaterial->GetLayer( k );

					int	uvwSrc = layer->GetUVWSrc() & plLayerInterface::kUVWIdxMask;

					if( maxUVWSrc < uvwSrc && layer->GetTexture() != nil )
						maxUVWSrc = uvwSrc;
					if( maxBlendChannels < currData.fNumBlendChannels)
						maxBlendChannels = currData.fNumBlendChannels;
				}
			}
		}
		// If this node is a water decal set to environment map, then there's only 1 layer, but
		// we'll need an extra 2 uvw channels for the tangent space basis vectors.
		if( node->GetWaterDecEnv() )
			maxUVWSrc = 2;

		if( numChannels + maxBlendChannels < ( maxUVWSrc + 1 ) && fWarnBadUVs )
		{
			/// We're missing some UV channels on our object. We'll handle it later; warn the user here
			if( fErrorMsg->Set( true, "UVW Channel Warning", fWarnBadUVsMsg, node->GetName() ).CheckAskOrCancel() )
				fWarnBadUVs = false;
			fErrorMsg->Set( false );
		}
		else if( numChannels > ( maxUVWSrc + 1 ) )
		{
			// Make sure we allocate enough for all the channel data, even if the materials don't use them (yet...)
			// (trick is, make sure those extra channels are valid first)
			for( i = maxUVWSrc + 1; i < numChannels; i++ )
			{
				if( mesh->mapFaces( i + 1 ) == nil )
				{
					numChannels = i;
					break;
				}
			}

			maxUVWSrc = numChannels - 1;	
		}
		
		//maxUVWSrc += maxBlendChannels;
		if (maxUVWSrc > plGeometrySpan::kMaxNumUVChannels - 1) maxUVWSrc = plGeometrySpan::kMaxNumUVChannels - 1;

		/// Our buffer format...
/*		ourFormat = ( maxUVWSrc == -1 ) ? plGeometrySpan::kNoUVChannels :
					( maxUVWSrc == 0 ) ? plGeometrySpan::k1UVChannel :
					( maxUVWSrc == 1 ) ? plGeometrySpan::k2UVChannels :
					( maxUVWSrc == 2 ) ? plGeometrySpan::k3UVChannels :
					plGeometrySpan::k4UVChannels;
*/
		ourFormat = plGeometrySpan::UVCountToFormat( maxUVWSrc + 1 );
		/// NOW allocate our accumulators, since maxUVWSrc was just calculated...
		ourAccumulators.SetCount( numMaterials );
		for( i = 0; i < numMaterials; i++ )
		{			
			if (ourMaterials[i] == nil)
			{
				ourAccumulators[i] = nil;
				continue;
			}

			hsTArray<plMAXVertexAccumulator *> *currAccum = TRACKED_NEW hsTArray<plMAXVertexAccumulator *>;
			int currNumSubMtls = ourMaterials[i]->GetCount();
			currAccum->Reset();
			ourAccumulators[i] = currAccum;
			for (j = 0; j < currNumSubMtls; j++)
			{
				currAccum->Append(new plMAXVertexAccumulator( mesh->getNumVerts(), maxUVWSrc + 1 ));
			}
		}
		

		//// Skinning ////////////////////////////////////////////////////////////

		/// Check for skinning
		ISkin* skin = node->FindSkinModifier();
		if( skin )
		{
			skinData = skin->GetContextInterface(node);
			int skinNumPoints = skinData->GetNumPoints();
			if(skinNumPoints != numVerts)
			{
				fErrorMsg->Set(true, "Skinning Error", "Invalid point count on ISkin data on node %s", dbgNodeName ).Show();
				fErrorMsg->Set();
				throw (hsBool)false;
				//hsAssert( skinData->GetNumPoints() == numVerts, "Invalid point count on ISkin data" );

			}

			
			/// Loop through the skin verts and find the max # of bones
			for( i = 0, maxNumBones = 0; i < numVerts; i++ )
			{
				if( skinData->GetNumAssignedBones( i ) > maxNumBones )
					maxNumBones = skinData->GetNumAssignedBones( i );
			}
			maxNumBones++;
			if( maxNumBones > 4 )
				maxNumBones = 4;
			//hsAssert( maxNumBones >= 2, "Invalid skin (not enough bones)" );
			if( maxNumBones < 2)
			{
				fErrorMsg->Set(true, "Skinning Error", "Invalid skin (no bones) on node %s", dbgNodeName ).Show();
				fErrorMsg->Set();
				throw (hsBool)false;
			}


			if (node->GetBoneMap() && maxNumBones == 2)
				maxNumBones++;

			/// Change format to match
			ourFormat |= ( maxNumBones == 2 ) ? plGeometrySpan::kSkin1Weight : 
						 ( maxNumBones == 3 ) ? plGeometrySpan::kSkin2Weights : plGeometrySpan::kSkin3Weights;

			if( skin->GetNumBones() > 1 || node->GetBoneMap())
				ourFormat |= plGeometrySpan::kSkinIndices;
		}
		else
		{
			skinData = nil;

			if( node->NumBones() )
			{
				maxNumBones = 2;
				ourFormat |= plGeometrySpan::kSkin1Weight;
			}
		}


		//// Build Vertex Normal Cache ///////////////////////////////////////////

		vertNormalCache.SetCount( mesh->getNumVerts() );
		for( i = 0; i < mesh->getNumFaces(); i++ )
		{
			Face		*maxFace = &mesh->faces[ i ];
			Point3		v0, v1, v2, norm;

			UInt32 smGroup = smoothAll ? 1 : maxFace->getSmGroup();

			v0 = mesh->verts[ maxFace->v[ 0 ] ];
			v1 = mesh->verts[ maxFace->v[ 1 ] ];
			v2 = mesh->verts[ maxFace->v[ 2 ] ];

			norm = ( v1 - v0 ) ^ ( v2 - v1 );
			for( j = 0; j < 3; j++ )
				vertNormalCache[ maxFace->v[ j ] ].AddNormal( norm, maxFace->smGroup );
		}
		for( i = 0; i < vertNormalCache.GetCount(); i++ )
			vertNormalCache[ i ].Normalize();

		vertDPosDuCache = TRACKED_NEW hsTArray<plMAXVertNormal>[numMaterials];
		vertDPosDvCache = TRACKED_NEW hsTArray<plMAXVertNormal>[numMaterials];

		hsTArray<Int16>					bumpLayIdx;
		hsTArray<Int16>					bumpLayChan;
		hsTArray<Int16>					bumpDuChan;
		hsTArray<Int16>					bumpDvChan;
		ISetBumpUvSrcs(ourMaterials, bumpLayIdx, bumpLayChan, bumpDuChan, bumpDvChan);
		if( node->GetWaterDecEnv() )
			ISetWaterDecEnvUvSrcs(ourMaterials, bumpLayIdx, bumpLayChan, bumpDuChan, bumpDvChan);

		ISmoothUVGradients(node, mesh, ourMaterials, bumpLayIdx, bumpLayChan, vertDPosDuCache, vertDPosDvCache);
		
		//// Main Conversion Loop ////////////////////////////////////////////////

		// Loop through the faces and stuff them into spans
		spanArray.Reset();

		mesh->buildNormals();

		for( i = 0; i < numFaces; i++ )
		{
			Face		*maxFace = &mesh->faces[ i ];
			TVFace		*maxColorFace = ( mesh->vcFace != nil ) ? &mesh->vcFace[ i ] : nil;
			hsPoint3	pos[ 3 ];
			hsPoint3	testPt1, testPt2, testPt3;
			hsVector3	normals[ 3 ];
			hsColorRGBA	colors[ 3 ], illums[ 3 ];
			UInt32		smGroup, vertIdx[ 3 ];
			hsPoint3	uvs1[ plGeometrySpan::kMaxNumUVChannels + 1];
			hsPoint3    uvs2[ plGeometrySpan::kMaxNumUVChannels + 1];
			hsPoint3	uvs3[ plGeometrySpan::kMaxNumUVChannels + 1];
			hsPoint3	temp;
			Mtl			*currMaxMtl;

			// The main index is how a multi-material keeps track of which sub material is involved. The sub material
			// may actually create multiple materials (like composites), hence the second index, but it most cases it
			// will be zero as well.

			int mainMatIndex = 0; 
			int subMatIndex = 0; 

			// Get span index
			if( isMultiMat )
			{
				mainMatIndex = maxFace->getMatID();
				if( mainMatIndex >= numMaterials )
					mainMatIndex = 0;
				currMaxMtl = maxMaterial->GetSubMtl(mainMatIndex);
			}	
			else
				currMaxMtl = maxMaterial;

			int numBlendChannels = 0;
			hsTArray<plExportMaterialData> *subMtls = ourMaterials[mainMatIndex];
			hsAssert(subMtls != nil, "Face is assigned a material that we think is unused.");

			for (j = 0; j < subMtls->GetCount(); j++)
			{
				int currBlend = subMtls->Get(j).fNumBlendChannels;
				if (numBlendChannels < currBlend)
					numBlendChannels = currBlend;
			}
			isComposite = hsMaterialConverter::Instance().IsCompositeMat( currMaxMtl );

			/// Add the 3 vertices to the correct vertex accumulator object

			// Get positions
			if( flipOrder )
			{
				for( j = 0; j < 3; j++ )
				{
					vertIdx[ j ] = maxFace->getVert( 2 - j );
					pos[ j ].fX = mesh->verts[ vertIdx[ j ] ].x;
					pos[ j ].fY = mesh->verts[ vertIdx[ j ] ].y;
					pos[ j ].fZ = mesh->verts[ vertIdx[ j ] ].z;
				}
			}
			else
			{
				for( j = 0; j < 3; j++ )
				{
					vertIdx[ j ] = maxFace->getVert( j );
					pos[ j ].fX = mesh->verts[ vertIdx[ j ] ].x;
					pos[ j ].fY = mesh->verts[ vertIdx[ j ] ].y;
					pos[ j ].fZ = mesh->verts[ vertIdx[ j ] ].z;
				}
			}

			// Look for degenerate triangles (why MAX even gives us these I have no clue...)
			testPt1 = pos[ 1 ] - pos[ 0 ];
			testPt2 = pos[ 2 ] - pos[ 0 ];
			testPt3 = pos[ 2 ] - pos[ 1 ];
			if( ( testPt1.fX == 0.0f && testPt1.fY == 0.0f && testPt1.fZ == 0.0f ) ||
				( testPt2.fX == 0.0f && testPt2.fY == 0.0f && testPt2.fZ == 0.0f ) ||
				( testPt3.fX == 0.0f && testPt3.fY == 0.0f && testPt3.fZ == 0.0f ) )
			{
				continue;
			}

			// If we're expanding the UVW channel list, fill out the rest with zeros
			for( j = numChannels; j < maxUVWSrc; j++ )
			{
				uvs1[ j ].Set( 0, 0, 0 );
				uvs2[ j ].Set( 0, 0, 0 );
				uvs3[ j ].Set( 0, 0, 0 );
			}

			// Now for each vertex, get the UVs, calc color, and add
			if( numChannels > 0 )
			{
				// Just go ahead and always generate the opacity into the uvs, because we're
				// going to look for it there on composites whether they actually use the
				// alpha hack texture or not.
				IGenerateUVs( node, currMaxMtl, mesh, i, numChannels, 
							  1, uvs1, uvs2, uvs3 );
				if( flipOrder )
				{
					for( j = 0; j < 3; j++ )
					{
						temp = uvs1[ j ];
						uvs1[ j ] = uvs3[ j ];
						uvs3[ j ] = temp;
					}
				}
			}

			// Handle colors
			if( maxColorFace == nil )
			{
				colors[2] = colors[1] = colors[0] = white;
			}
			else
			{
				colors[ 0 ] = colorArray[ maxColorFace->t[ flipOrder ? 2 : 0 ] ];
				colors[ 1 ] = colorArray[ maxColorFace->t[ flipOrder ? 1 : 1 ] ];
				colors[ 2 ] = colorArray[ maxColorFace->t[ flipOrder ? 0 : 2 ] ];
			}
			
			// Don't want to write illum values to the vertex for composite materials
			if (illumArray == nil || includesComp)
			{
				illums[ 0 ] = illums[ 1 ] = illums[ 2 ] = black;
			}
			else
			{
				// MF_HORSE CARNAGE
				TVFace* tvFace = &mesh->mapFaces(MAP_SHADING)[i];
				for( j = 0; j < 3; j++ )
					illums[j] = illumArray[ tvFace->getTVert(flipOrder ? 2 - j : j) ];
				// MF_HORSE CARNAGE
			}
			
			if (alphaMap != nil) // if it IS nil, then alpha values are all at the default 1.0
			{
				// MF_HORSE CARNAGE
				TVFace* tvFace = &mesh->mapFaces(MAP_ALPHA)[i];
				for (j = 0; j < 3; j++)
					colors[j].a = alphaMap[ tvFace->getTVert(flipOrder ? 2 - j : j) ].x;
				// MF_HORSE CARNAGE
			}

			if (isComposite && !makeAlphaLayer) 
			{
				int index = ((plCompositeMtl *)currMaxMtl)->CanWriteAlpha();
				int j;
				TVFace* tvFaces = mesh->mapFaces(MAP_SHADING);
				for (j = 0; j < 3; j++)
				{
					switch(index)
					{
					case plCompositeMtl::kCompBlendVertexAlpha:
						break;
					case plCompositeMtl::kCompBlendVertexIllumRed:
						colors[j].a = (tvFaces != nil ? illumMap[tvFaces[i].getTVert(flipOrder ? 2 - j : j)].x : 1.0f);
						break;
					case plCompositeMtl::kCompBlendVertexIllumGreen:
						colors[j].a = (tvFaces != nil ? illumMap[tvFaces[i].getTVert(flipOrder ? 2 - j : j)].y : 1.0f);
						break;
					case plCompositeMtl::kCompBlendVertexIllumBlue:
						colors[j].a = (tvFaces != nil ? illumMap[tvFaces[i].getTVert(flipOrder ? 2 - j : j)].z : 1.0f);
						break;
					default: // Different channels, thus we flush the alpha to 100 and do alpha through a 2nd layer.
						colors[j].a = 1.0f;
						break;
					}
				}
			}
			// Calculate normal for face
			if( node->HasNormalChan() )
			{
				// Someone has stuffed a requested normal into a map channel.
				// Ignore common sense and use it as is.
				int normChan = node->GetNormalChan();
				TVFace* mapFaces = mesh->mapFaces(normChan);
				if( mapFaces )
				{
					TVFace* normFace = mapFaces + i;
					int ii;
					for( ii = 0; ii < 3; ii++ )
					{
						Point3 norm = mesh->mapVerts(normChan)[normFace->getTVert(ii)];
						normals[ii].Set(norm.x, norm.y, norm.z);
					}
				}
				else
				{
					if( fErrorMsg->Set(fWarnBadNormals, node->GetName(), fWarnBadNormalsMsg).CheckAskOrCancel() )
						fWarnBadNormals = false;
					fErrorMsg->Set( false );
					normals[0].Set(0,0,1.f);
					normals[1] = normals[2] = normals[0];
				}

			}
			else if( node->GetRadiateNorms() )
			{
				int ii;
				for( ii = 0; ii < 3; ii++ )
				{
					Point3 pos = mesh->getVert(vertIdx[ii]) * otm;
					pos = pos * invOtm;

					normals[ii].Set(pos.x, pos.y, pos.z);
				}
			}
			else
			{
				smGroup = smoothAll ? 1 : maxFace->getSmGroup();
				if( smGroup == 0 )
				{
					hsVector3	v1, v2;
					v1.Set( &pos[ 1 ], &pos[ 0 ] );
					v2.Set( &pos[ 2 ], &pos[ 1 ] );	// Hey, MAX does it...see normalCache building above
					// Note: if flipOrder is set, we have to reverse the order of the cross product, since
					// we already flipped the order of the points, to match what MAX would get
					normals[ 0 ] = normals[ 1 ] = normals[ 2 ] = flipOrder ? ( v2 % v1 ) : ( v1 % v2 );
				}
				else
				{
					normals[ 0 ] = vertNormalCache[ vertIdx[ 0 ] ].GetPlNormal( smGroup );
					normals[ 1 ] = vertNormalCache[ vertIdx[ 1 ] ].GetPlNormal( smGroup );
					normals[ 2 ] = vertNormalCache[ vertIdx[ 2 ] ].GetPlNormal( smGroup );
				}
			}
			normals[ 0 ] = vertInvTransMatrix * normals[ 0 ];
			normals[ 1 ] = vertInvTransMatrix * normals[ 1 ];
			normals[ 2 ] = vertInvTransMatrix * normals[ 2 ];

			// Adding normalization here, because we're going to compare them when searching for
			// this vertex to share. mf.
			normals[0].Normalize();
			normals[1].Normalize();
			normals[2].Normalize();

			// The above section of code has just set any bump uv channels incorrectly,
			// but at least they are there. Now we just need to correct the values.
			if( bumpLayIdx[mainMatIndex] >= 0 )
			{
				TVFace* tvFace = mesh->mapFaces(bumpLayChan[mainMatIndex]+1) + i;
				ISetBumpUvs(bumpDuChan[mainMatIndex], vertDPosDuCache[mainMatIndex], tvFace, smGroup, uvs1, uvs2, uvs3);
				ISetBumpUvs(bumpDvChan[mainMatIndex], vertDPosDvCache[mainMatIndex], tvFace, smGroup, uvs1, uvs2, uvs3);
			}

			// Do this here, cause if we do it before we calculate the normals on smoothing group #0, 
			// the normals will be wrong
			for( j = 0; j < 3; j++ )
				pos[ j ] = vert2LMatrix * pos[ j ];

/* We already compute the index, this looks like redundant code - 7/26/01 Bob
			// Get span index
			if( isMultiMat )
			{
				mainMatIndex = maxFace->getMatID();
				if( mainMatIndex >= numMaterials )
					mainMatIndex = 0;
			}
*/
			if (isComposite)
			{
				// I don't care about flipOrder here... it doesn't affect the index
				float opac[][2] = {{0.0, 0.0}, {0.0, 0.0}, {0.0, 0.0}};

				opac[0][0] = uvs1[numChannels].fX;
				opac[1][0] = uvs2[numChannels].fX;
				opac[2][0] = uvs3[numChannels].fX;
				opac[0][1] = uvs1[numChannels].fY;
				opac[1][1] = uvs2[numChannels].fY;
				opac[2][1] = uvs3[numChannels].fY;
				
				subMatIndex = ((plCompositeMtl *)currMaxMtl)->ComputeMaterialIndex(opac, 3) - 1;
			}
			// Add!
			hsAssert(ourAccumulators[mainMatIndex] != nil, "Trying to add a face with an unused sub-material.");
			ourAccumulators[ mainMatIndex ]->Get(subMatIndex)->AddVertex( vertIdx[ 0 ], &pos[ 0 ], &normals[ 0 ], colors[ 0 ], illums[ 0 ], uvs1 );
			ourAccumulators[ mainMatIndex ]->Get(subMatIndex)->AddVertex( vertIdx[ 1 ], &pos[ 1 ], &normals[ 1 ], colors[ 1 ], illums[ 1 ], uvs2 );
			ourAccumulators[ mainMatIndex ]->Get(subMatIndex)->AddVertex( vertIdx[ 2 ], &pos[ 2 ], &normals[ 2 ], colors[ 2 ], illums[ 2 ], uvs3 );

		}


		/// Now go through each accumulator, take any data created and stuff it into a new span
		for( i = 0; i < numMaterials; i++ )
		{
			hsTArray<plExportMaterialData> *subMats = ourMaterials[i];
			// A sub material of a MultiMat that never gets used will have a nil value, signifying no spans to export
			if (subMats == nil)
				continue;
			for( j = 0; j < subMats->GetCount(); j++)
			{
				plMAXVertexAccumulator *accum = ourAccumulators[i]->Get(j);

				// With composite materials, not every accumulator will have faces. Only create spans for the ones that do.
				if (accum->GetVertexCount() == 0)
					continue;

				plGeometrySpan		*span = TRACKED_NEW plGeometrySpan;

				span->BeginCreate( subMats->Get(j).fMaterial, l2wMatrix, ourFormat );
				span->fLocalToOBB = node->GetLocalToOBB44();
				span->fOBBToLocal = node->GetOBBToLocal44();

				accum->StuffMyData( node, span, mesh, skinData );
				span->fProps |= sharedSpanProps;

				span->fWaterHeight = waterHeight;

				if( (bumpDuChan[i] >= 0) && (bumpDvChan[i] > 0) )
					span->fLocalUVWChans = (bumpDuChan[i] << 8) | bumpDvChan[i];

				if( (span->fMaterial != nil)
					&& (span->fMaterial->GetNumLayers() > 0) 
					&& (span->fMaterial->GetLayer( 0 )->GetState().fBlendFlags & hsGMatState::kBlendMask) )	
				{
					span->fProps |= plGeometrySpan::kRequiresBlending;
				}
				if( node->GetForceSortable() )
					span->fProps |= plGeometrySpan::kRequiresBlending;

				span->EndCreate();

				hsScalar minDist, maxDist;
				if( hsMaterialConverter::HasVisDists(node, i, minDist, maxDist) )
				{
					span->fMinDist = (minDist);
					span->fMaxDist = (maxDist);
				}

				// If we're not doing preshading later, make sure everything is illuminated so you can see
				if (doPreshading)
				{
					hsColorRGBA gray;
					gray.Set(0.5, 0.5, 0.5, 0.0);
					for( int iVert = 0; iVert < span->fNumVerts; iVert++ )
						span->StuffVertex( iVert, &white, &gray );
				}

				if( span->fNumVerts > 0 )
					spanArray.Append( span );
				else
					delete span;
			}
		}

		void SetWaterColor(hsTArray<plGeometrySpan*>& spans);

		// A bit of test hack here. Remind me to nuke it.
		if( node->GetCalcEdgeLens() || node->UserPropExists("XXXWaterColor") )
			SetWaterColor(spanArray);


		// Now that we have our nice spans, see if they need to be diced up a bit
		int maxFaces, minFaces;
		float maxSize;
		if( node->GetGeoDice(maxFaces, maxSize, minFaces) )
		{
			plGeoSpanDice dice;
			dice.SetMaxFaces(maxFaces);
			dice.SetMaxSize(hsPoint3(maxSize, maxSize, maxSize));
			dice.SetMinFaces(minFaces);
			dice.Dice(spanArray);
		}

		/// Check for overflow (only do it if our initial tests showed there could be overflow; this
		/// is so these tests don't slow down the loop unless absolutely needed).
		// We're going to go ahead and quietly break up the mesh if it needs breaking,
		// because we can probably do a better job of it than production anyway.
		if( checkForOverflow )
		{
			hsBool needMoreDicing = false;
			int i;
			for( i = 0; i < spanArray.GetCount(); i++ )
			{
				plAccessGeometry accGeom;
				plAccessSpan accSpan;
				accGeom.AccessSpanFromGeometrySpan(accSpan, spanArray[i]);
				hsBool destroySpan = false;
				if( accSpan.HasAccessVtx() )
				{
					if( accSpan.AccessVtx().VertCount() >= plGBufferGroup::kMaxNumVertsPerBuffer )
					{
						needMoreDicing = true;
					}
				}
				if( accSpan.HasAccessTri() )
				{
					if( accSpan.AccessTri().TriCount() * 3 >= plGBufferGroup::kMaxNumIndicesPerBuffer )
					{
						needMoreDicing = true;
					}
				}
				accGeom.Close(accSpan);
			}
			if( needMoreDicing )
			{
				// Could just dice the ones that need it, but whatever. mf.
				plConst(int) kAutoMaxFaces(5000);
				plConst(float) kAutoMaxSize(10000.f);
				plConst(int) kAutoMinFaces(1000);
				plGeoSpanDice dice;
				dice.SetMaxFaces(kAutoMaxFaces);
				dice.SetMaxSize(hsPoint3(kAutoMaxSize,kAutoMaxSize,kAutoMaxSize));
				dice.SetMinFaces(kAutoMinFaces);
				dice.Dice(spanArray);
			}
		}
		throw (hsBool)true;
	}
	catch( hsBool retVal )
	{
		/// Cleanup!
		for( i = 0; i < vertNormalCache.GetCount(); i++ )
			vertNormalCache[ i ].DestroyChain();

		for( i = 0; i < numMaterials; i++ )
		{
			if( vertDPosDuCache != nil )
			{
				for( j = 0; j < vertDPosDuCache[i].GetCount(); j++ )
					vertDPosDuCache[i][j].DestroyChain();
			}
			if( vertDPosDvCache != nil )
			{
				for( j = 0; j < vertDPosDvCache[i].GetCount(); j++ )
					vertDPosDvCache[i][j].DestroyChain();
			}

			if (ourAccumulators[i] == nil)
				continue;
			for( j = 0; j < ourAccumulators[ i ]->GetCount(); j++ )
			{
				delete ourAccumulators[ i ]->Get(j);
			}
			delete ourMaterials[ i ];
			delete ourAccumulators[ i ];
		}
		delete [] vertDPosDuCache;
		delete [] vertDPosDvCache;
		delete [] colorArray;
		delete [] illumArray;
	
		IDeleteTempGeometry();

		return retVal;
	}

	return true;
	hsGuardEnd; 
}

//// ICreateHexColor /////////////////////////////////////////////////////////

UInt32	plMeshConverter::ICreateHexColor( float r, float g, float b )
{
	UInt32		ru, gu, bu, au;


	au = 0xff000000;
	ru = r * 255.0f;
	gu = g * 255.0f;
	bu = b * 255.0f;
	return au | ( ru << 16 ) | ( gu << 8 ) | ( bu );
}

UInt32	plMeshConverter::ISetHexAlpha( UInt32 color, float alpha)
{
	UInt32 alphaBits = alpha * 255;
	alphaBits <<= 24;
	return color & 0x00ffffff | alphaBits;
}

// Delete temp geo stuff allocated (either directly or indirectly)
// via IGetNodeMesh().
void plMeshConverter::IDeleteTempGeometry()
{
	if( fTriObjToDelete != nil )
	{
		fTriObjToDelete->DeleteMe();
		fTriObjToDelete = nil;
	}
	if( fMeshToDelete )
	{
		delete fMeshToDelete;
		fMeshToDelete = nil;
	}
}

//// IGetNodeMesh ////////////////////////////////////////////////////////////
//	Get the Mesh object attached to a node. Returns nil if the node
//	is not a triMesh object

Mesh	*plMeshConverter::IGetNodeMesh( plMaxNode *node )
{
	hsGuardBegin( "plMeshConverter::IGetNodeMesh" );

	const char* dbgNodeName = node->GetName();

	fTriObjToDelete = nil;
	fMeshToDelete = nil;

	// Get da object
	Object *obj = node->EvalWorldState( fConverterUtils.GetTime( fInterface ) ).obj;
	if( obj == nil )
		return nil;

	if( !obj->CanConvertToType( triObjectClassID ) )
		return nil;

	// Convert to triMesh object
	TriObject	*meshObj = (TriObject *)obj->ConvertToType( fConverterUtils.GetTime( fInterface ), triObjectClassID );
	if( meshObj == nil )
		return nil;

	if( meshObj != obj )
		fTriObjToDelete = meshObj;

	// Get the mesh
	Mesh	*mesh = &(meshObj->mesh);
	if( mesh->getNumFaces() == 0 )
		return nil;

	if( node->GetDup2Sided() )
	{
		mesh = IDuplicate2Sided(node, mesh);
		
		IDeleteTempGeometry();

		fMeshToDelete = mesh;
	}

	return mesh;
	hsGuardEnd; 
}

Mesh* plMeshConverter::IDuplicate2Sided(plMaxNode* node, Mesh* mesh)
{
	mesh = TRACKED_NEW Mesh(*mesh);

	Mtl* mtl = node->GetMtl();

	BitArray faces(mesh->getNumFaces());

	int num2Sided = 0;

	int origNumFaces = mesh->getNumFaces();

	int i;
	for( i = 0; i < mesh->getNumFaces(); i++ )
	{
		if( hsMaterialConverter::IsTwoSided(mtl, mesh->faces[i].getMatID()) )
		{
			num2Sided++;
			faces.Set(i);
		}
	}

	if( !num2Sided )
		return mesh;

	MeshDelta meshDelta(*mesh);
	meshDelta.CloneFaces(*mesh, faces);
	meshDelta.Apply(*mesh);

	BitArray verts(mesh->getNumVerts());
	verts.SetAll();
	const float kWeldThresh = 0.1f;
	meshDelta.WeldByThreshold(*mesh, verts, kWeldThresh);
	meshDelta.Apply(*mesh);

	hsAssert(origNumFaces + num2Sided == mesh->getNumFaces(), "Whoa, lost or gained, unexpected");

	for( i = origNumFaces; i < mesh->getNumFaces(); i++ )
	{
		meshDelta.FlipNormal(*mesh, i);
	}
	meshDelta.Apply(*mesh);

	return mesh;
}

//// IGenerateUVs ////////////////////////////////////////////////////////////
//	Generates the UV coordinates for the three vertices of a given face. 
//	Returns the number of UV channels. Ripped off of SetUVs() from the old
//	hsMeshConverter.

int		plMeshConverter::IGenerateUVs( plMaxNode *node, Mtl *maxMtl, Mesh *mesh, int faceIdx, int numChan, int numBlend,
									   hsPoint3 *uvs1, hsPoint3 *uvs2, hsPoint3 *uvs3 )
{
	hsGuardBegin( "plMeshConverter::IGenerateUVs" );

	if( !( maxMtl && ( hsMaterialConverter::Instance().IsMultiMat( maxMtl ) || ( maxMtl->Requirements(-1) & MTLREQ_UV ) ) ) )
	{
		return 0;
	}


	// To avoid transforming the shared UVs while rendering, we will
	// sometimes pretransform them and not share.

	int		j, k;
	Face	*face = &mesh->faces[ faceIdx ];



	if( hsMaterialConverter::Instance().IsMultiMat( maxMtl ) )
	{
		int faceMtlIndex = face->getMatID();
		if( faceMtlIndex >= maxMtl->NumSubMtls() ) // we'll warn in createtrimeshrecur
			faceMtlIndex = 0;
	}

	hsBool firstWarn = true;
	hsPoint3	pt;

	/// Loop through the vertices
	for( j = 0; j < 3; j++ )
	{
		int chan;
		for( chan = 0; chan < numChan; chan++ )
		{
			if( mesh->mapFaces( chan + 1 ) )
			{
				TVFace* tvFace = &mesh->mapFaces( chan + 1 )[ faceIdx ];
				UVVert* tVerts = mesh->mapVerts( chan + 1 );

				if( firstWarn && fErrorMsg->Set( !tvFace, node->GetName(), "Check mapping on textured objects" ).CheckAndAsk() )
				{
					firstWarn = false;
				}
				fErrorMsg->Set( false );
				if( !tvFace )
				{
					continue;
				}

				int tvIdx = tvFace->getTVert( j );

				if( tvIdx >= mesh->getNumMapVerts( chan + 1 ) )
				{
					static int muteWarn = false;
					if( !muteWarn )
					{
						muteWarn = fErrorMsg->Set( true, node->GetName(), "Check mapping on channel %d!!!", chan + 1 ).CheckAskOrCancel();
						fErrorMsg->Set( false );
					}
					tvIdx = 0;
				}

				UVVert		uv = tVerts[tvIdx];

				// The artists set the 3rd coordinate to help create the mapping,
				// but we never need it at runtime, so let's set it to zero on
				// export and then the vert coder can detect that it doesn't 
				// even need to write it.
				pt.Set( uv.x, 1.0f - uv.y, 0.f );

				if( _isnan( (double)pt.fX ) || _isnan( (double)pt.fY ) || _isnan( (double)pt.fZ ) )
					pt.Set( 0, 0, 0 );

				switch( j )
				{
					case 0: uvs1[ chan ] = pt; break;
					case 1: uvs2[ chan ] = pt; break;
					case 2: uvs3[ chan ] = pt; break;
				}			
			}
			
		}

		// That takes care of the UVs MAX gives us. Do we need some leftover channels to store our blending info?
		for (k = numChan; k < numChan + numBlend; k++)
		{
			UVVert *alphas = mesh->mapVerts(MAP_ALPHA);
			UVVert *illums = mesh->mapVerts(MAP_SHADING);
			UVVert pt;
			pt.z = 0.0f;

			TVFace* alphaFace = mesh->mapFaces(MAP_ALPHA) ? &mesh->mapFaces(MAP_ALPHA)[faceIdx] : nil;
			TVFace* illumFace = mesh->mapFaces(MAP_SHADING) ? &mesh->mapFaces(MAP_SHADING)[faceIdx] : nil;

			if (hsMaterialConverter::Instance().IsCompositeMat( maxMtl ))
			{
				plCompositeMtl *compMtl = (plCompositeMtl *)maxMtl;
				IParamBlock2 *pb = maxMtl->GetParamBlockByID(kCompPasses);

				// MF_HORSE CARNAGE


				compMtl->SetOpacityVal(&pt.x, 
						(alphas == nil 
							? nil 
							: &alphas[ alphaFace->getTVert(j) ]),
						(illums == nil 
							? nil 
							: &illums[ illumFace->getTVert(j) ]),
						pb->GetInt(kCompBlend, 0, 0));
				
				compMtl->SetOpacityVal(&pt.y, 
					(alphas == nil 
						? nil 
						: &alphas[ alphaFace->getTVert(j) ]),
					(illums == nil 
						? nil 
						: &illums[ illumFace->getTVert(j) ]),
					pb->GetInt(kCompBlend, 0, 1));
				// MF_HORSE CARNAGE
			}
			else // all other materials
			{		
				pt.y = 0.0;
				pt.z = 1.0;
				if (alphas == nil || alphaFace == nil)
					pt.x = 1.0;
				else
					pt.x = alphas[alphaFace->getTVert(j)].x;
			}

			switch( j )
			{
				case 0: uvs1[ k ].fX = pt.x; uvs1[ k ].fY = pt.y; uvs1[ k ].fZ = pt.z; break;
				case 1: uvs2[ k ].fX = pt.x; uvs2[ k ].fY = pt.y; uvs2[ k ].fZ = pt.z; break;
				case 2: uvs3[ k ].fX = pt.x; uvs3[ k ].fY = pt.y; uvs3[ k ].fZ = pt.z; break;
			}			
		}
	}

	return numChan;
	hsGuardEnd;
}

void plMeshConverter::ISetWaterDecEnvUvSrcs(hsTArray<hsTArray<plExportMaterialData> *>& ourMaterials, 
									 hsTArray<Int16>& bumpLayIdx, 
									 hsTArray<Int16>& bumpLayChan,
									 hsTArray<Int16>& bumpDuChan, 
									 hsTArray<Int16>& bumpDvChan)
{
	bumpLayIdx.SetCount(ourMaterials.GetCount());
	bumpLayChan.SetCount(ourMaterials.GetCount());
	bumpDuChan.SetCount(ourMaterials.GetCount());
	bumpDvChan.SetCount(ourMaterials.GetCount());

	int i;
	for( i = 0; i < ourMaterials.GetCount(); i++ )
	{
		bumpLayIdx[i] = i;
		bumpLayChan[i] = 0;
		bumpDuChan[i] = 1;
		bumpDvChan[i] = 2;
	}
}

void plMeshConverter::ISetBumpUvSrcs(hsTArray<hsTArray<plExportMaterialData> *>& ourMaterials, 
									 hsTArray<Int16>& bumpLayIdx, 
									 hsTArray<Int16>& bumpLayChan,
									 hsTArray<Int16>& bumpDuChan, 
									 hsTArray<Int16>& bumpDvChan)
{
	bumpLayIdx.SetCount(ourMaterials.GetCount());
	bumpLayChan.SetCount(ourMaterials.GetCount());
	bumpDuChan.SetCount(ourMaterials.GetCount());
	bumpDvChan.SetCount(ourMaterials.GetCount());

	int i;
	for( i = 0; i < ourMaterials.GetCount(); i++ )
	{
		bumpLayIdx[i] = -1;
		bumpLayChan[i] = -1;
		bumpDuChan[i] = -1;
		bumpDvChan[i] = -1;

		// The following two lines pretty much rule out composites with bump maps.
		if( !ourMaterials[i] )
			continue;

		if( ourMaterials[i]->GetCount() != 1 )
			continue;


		hsGMaterial* ourMat = ourMaterials[i]->Get(0).fMaterial;
		int j;
		for( j = 0; j < ourMat->GetNumLayers(); j++ )
		{
			if( ourMat->GetLayer(j)->GetMiscFlags() & hsGMatState::kMiscBumpLayer )
			{
				bumpLayIdx[i] = j;
				bumpLayChan[i] = ourMat->GetLayer(j)->GetUVWSrc();
			}

			if( ourMat->GetLayer(j)->GetMiscFlags() & hsGMatState::kMiscBumpDu )
				bumpDuChan[i] = ourMat->GetLayer(j)->GetUVWSrc();

			if( ourMat->GetLayer(j)->GetMiscFlags() & hsGMatState::kMiscBumpDv )
				bumpDvChan[i] = ourMat->GetLayer(j)->GetUVWSrc();
		}
	}
}

void plMeshConverter::ISetBumpUvs(Int16 uvChan, hsTArray<plMAXVertNormal>& vertDPosDuvCache, TVFace* tvFace, UInt32 smGroup, 
								  hsPoint3* uvs1, hsPoint3* uvs2, hsPoint3* uvs3)
{
	if( uvChan < 0 )
		return;

	uvs1[uvChan] = *(hsPoint3*)&vertDPosDuvCache[tvFace->getTVert(0)].GetPlNormal(smGroup);
	uvs2[uvChan] = *(hsPoint3*)&vertDPosDuvCache[tvFace->getTVert(1)].GetPlNormal(smGroup);
	uvs3[uvChan] = *(hsPoint3*)&vertDPosDuvCache[tvFace->getTVert(2)].GetPlNormal(smGroup);
}

// Determine if we're going to need a uv gradient channel.
// If we do need one, determine which uvw channel needs the gradient.
// Finally, make the gradients, smoothing according to smooth groups (just like vertex normals).
//
// If we decided we needed them, they are in the output arrays, otherwise the output arrays are made empty.
void plMeshConverter::ISmoothUVGradients(plMaxNode* node, Mesh* mesh, 
										 hsTArray<hsTArray<plExportMaterialData> *>& ourMaterials, 
										 hsTArray<Int16>& bumpLayIdx, hsTArray<Int16>& bumpLayChan,
										 hsTArray<plMAXVertNormal>* vertDPosDuCache, hsTArray<plMAXVertNormal>* vertDPosDvCache)
{
	const char* dbgNodeName = node->GetName();

	Mtl* mainMtl = hsMaterialConverter::Instance().GetBaseMtl( node );

	hsBool needsGradientUvs = hsMaterialConverter::Instance().HasBumpLayer(node, mainMtl) || node->GetWaterDecEnv();

	if( needsGradientUvs )
	{
		int matIdx;
		for( matIdx = 0; matIdx < ourMaterials.GetCount(); matIdx++ )
		{
			if( bumpLayIdx[matIdx] >= 0 )
			{
				UInt32 uvwSrc = bumpLayChan[matIdx];
				if( mesh->getNumMapVerts(uvwSrc+1) && mesh->mapVerts(uvwSrc+1) )
				{
					vertDPosDuCache[matIdx].SetCount(mesh->getNumMapVerts(uvwSrc+1));
					vertDPosDvCache[matIdx].SetCount(mesh->getNumMapVerts(uvwSrc+1));
				}
				else
				{
					// Ooops. This is probably an error somewhere.
					hsAssert(false, "Thought we had a valid bump map, but we don't.");
					bumpLayIdx[matIdx] = -1;
				}
			}
		}

		hsBool isMultiMat = hsMaterialConverter::Instance().IsMultiMat(mainMtl);
		int i;
		for( i = 0; i < mesh->getNumFaces(); i++ )
		{
			const plLayerInterface* layer = nil;
			if( isMultiMat )
			{
				int index = mesh->faces[i].getMatID();
				if (index >= mainMtl->NumSubMtls())
					index = 0;

				matIdx = index;
				if( bumpLayIdx[index] >= 0 )
					layer = ourMaterials[index]->Get(0).fMaterial->GetLayer(bumpLayIdx[index]);
			}
			else
			{
				matIdx = 0;
				if( bumpLayIdx[0] >= 0 )
					layer = ourMaterials[0]->Get(0).fMaterial->GetLayer(bumpLayIdx[0]);
			}
			if( layer )
			{
				Point3 dPosDu = IGetUvGradient(node, layer->GetTransform(), layer->GetUVWSrc(),
								mesh, i, 
								0);
				Point3 dPosDv = IGetUvGradient(node, layer->GetTransform(), layer->GetUVWSrc(),
								mesh, i, 
								1);

// #define MF_BUMP_CHECK_DUXDV
#ifdef MF_BUMP_CHECK_DUXDV
				Point3 duXdv = ::Normalize(dPosDu) ^ ::Normalize(dPosDv);

				Point3 v0 = mesh->verts[ mesh->faces[i].v[ 0 ] ];
				Point3 v1 = mesh->verts[ mesh->faces[i].v[ 1 ] ];
				Point3 v2 = mesh->verts[ mesh->faces[i].v[ 2 ] ];

				Point3 norm = ::Normalize(( v1 - v0 ) ^ ( v2 - v1 ));
				
				Point3 diff = duXdv - norm;

				static int doAgain = false;
				if( doAgain )
				{
					dPosDu = IGetUvGradient(node, layer->GetTransform(), layer->GetUVWSrc(),
									mesh, i, 
									0);
					dPosDv = IGetUvGradient(node, layer->GetTransform(), layer->GetUVWSrc(),
									mesh, i, 
									1);
				}
#endif // MF_BUMP_CHECK_DUXDV

				if( node->GetWaterDecEnv() )
				{
					dPosDu.z = dPosDv.z = 0.f;
				}

				// Flip the direction of dPosDv, because we flip textures about V for histerical reasons.
				dPosDv = -dPosDv;

				TVFace* tvFace = &mesh->mapFaces(layer->GetUVWSrc() + 1)[i];
				int j;
				for( j = 0; j < 3; j++ )
				{
					vertDPosDuCache[matIdx][tvFace->getTVert(j)].AddNormal(dPosDu, mesh->faces[i].smGroup);
					vertDPosDvCache[matIdx][tvFace->getTVert(j)].AddNormal(dPosDv, mesh->faces[i].smGroup);
				}
			}
		}
		for( matIdx = 0; matIdx < ourMaterials.GetCount(); matIdx++ )
		{
			for( i = 0; i < vertDPosDuCache[matIdx].GetCount(); i++ )
			{
				vertDPosDuCache[matIdx][i].Normalize();
				vertDPosDvCache[matIdx][i].Normalize();
			}
		}
	}
}

// Get dPos/du into uvws[0], and dPos/dv into uvws[1]. dPos should be in the object's local space.
Point3 plMeshConverter::IGetUvGradient(plMaxNode* node, 
										const hsMatrix44& uvXform44, Int16 bmpUvwSrc, // Transform and uvwSrc of layer to gradient
										Mesh *mesh, int faceIdx, 
										int iUV) // 0 = uvw.x, 1 = uv2.y
{
	Point3 uvwOut(0,0,0);
	if( bmpUvwSrc < 0 )
		return uvwOut; // Not Error.

	if( bmpUvwSrc >= mesh->getNumMaps() )
		return uvwOut; // Error?

	TVFace* tvFace = &mesh->mapFaces(bmpUvwSrc + 1)[faceIdx];
	UVVert* tVerts = mesh->mapVerts(bmpUvwSrc + 1);
	if( !tvFace )
		return uvwOut; // Error?
	if( !tVerts )
		return uvwOut; // Error?

	Matrix3 v2l = node->GetVertToLocal();

	hsBool flipOrder = v2l.Parity();
	int vtxIdx = 0;
	int vtxNext = flipOrder ? 2 : 1;
	int vtxLast = flipOrder ? 1 : 2;

	// Get the three verts, v0-v2, where v0 is the corner in question.
	Face* face = &mesh->faces[faceIdx];				
	Point3 v0 = v2l * mesh->verts[face->getVert(vtxIdx)];
	Point3 v1 = v2l * mesh->verts[face->getVert(vtxNext)];
	Point3 v2 = v2l * mesh->verts[face->getVert(vtxLast)];

	// Get the three uvs, uv0-uv2, matching above verts.
	if( tvFace->getTVert(vtxIdx) >= mesh->getNumMapVerts(bmpUvwSrc + 1) )
		return uvwOut; // Error?
	if( tvFace->getTVert(vtxNext) >= mesh->getNumMapVerts(bmpUvwSrc + 1) )
		return uvwOut; // Error?
	if( tvFace->getTVert(vtxLast) >= mesh->getNumMapVerts(bmpUvwSrc + 1) )
		return uvwOut; // Error?

	Matrix3 uvwXform = plMaxNodeBase::Matrix44ToMatrix3(uvXform44);

	Point3 uv0 = uvwXform * tVerts[tvFace->getTVert(vtxIdx)];
	Point3 uv1 = uvwXform * tVerts[tvFace->getTVert(vtxNext)];
	Point3 uv2 = uvwXform * tVerts[tvFace->getTVert(vtxLast)];


	const float kRealSmall = 1.e-6f;
	// First, look for degenerate cases. 
	// If (uvn - uvm)[!iUV] == 0
	//		then (vn - vm) is tangent in  iUV dimension
	// Just be careful about direction, since (vn-vm) may be opposite direction from
	// increasing iUV.
	int iNotUV = !iUV;
	float del = uv0[iNotUV] - uv1[iNotUV];
	if( fabs(del) < kRealSmall )
	{
		if( uv0[iUV] - uv1[iUV] < 0 )
			uvwOut = v1 - v0;
		else
			uvwOut = v0 - v1;
		return uvwOut;
	}
	del = uv2[iNotUV] - uv1[iNotUV];
	if( fabs(del) < kRealSmall )
	{
		if( uv2[iUV] - uv1[iUV] < 0 )
			uvwOut = v1 - v2;
		else
			uvwOut = v2 - v1;
		return uvwOut;
	}
	del = uv2[iNotUV] - uv0[iNotUV];
	if( fabs(del) < kRealSmall )
	{
		if( uv2[iUV] - uv0[iUV] < 0 )
			uvwOut = v0 - v2;
		else
			uvwOut = v2 - v0;
		return uvwOut;
	}

	// Okay, none of the edges are along the dU gradient. That's good, because
	// it means we don't have to worry about divides by zero in what we're about
	// to do.
	del = uv0[iNotUV] - uv1[iNotUV];
	del = 1.f / del;
	Point3 v0Mv1 = v0 - v1;
	v0Mv1 *= del;
	float v0uv = (uv0[iUV] - uv1[iUV]) * del;

	del = uv2[iNotUV] - uv1[iNotUV];
	del = 1.f / del;
	Point3 v2Mv1 = v2 - v1;
	v2Mv1 *= del;
	float v2uv = (uv2[iUV] - uv1[iUV]) * del;

	if( v0uv > v2uv )
		uvwOut = v0Mv1 - v2Mv1;
	else
		uvwOut = v2Mv1 - v0Mv1;

	return uvwOut;
}

//// IGetUVTransform /////////////////////////////////////////////////////////
//	Gets the UV transform matrix for the given channel.

void	plMeshConverter::IGetUVTransform( plMaxNode *node, Mtl *mtl, Matrix3 *uvTransform, int which )
{
	hsGuardBegin( "plMeshConverter::IGetUVTransform" );

	uvTransform->IdentityMatrix();

    if( !mtl )
		return;

	Texmap* texMap = hsMaterialConverter::Instance().GetUVChannelBase(node, mtl, which);

	if( !texMap )
		return;

	BitmapTex *bitmapTex = (BitmapTex *)texMap;

#ifndef NDEBUG
	CStr className;
	texMap->GetClassName(className);
	if( strcmp(className,"Bitmap") && strcmp(className,"Plasma Layer") && strcmp(className,"Plasma Layer Dbg."))
		return;

	char txtFileName[256];
	strcpy(txtFileName, bitmapTex->GetMapName());
#endif // NDEBUG

	StdUVGen *uvGen = bitmapTex->GetUVGen();

	uvGen->GetUVTransform(*uvTransform);

	// We're going to munge the internals of the matrix here, but we won't need to worry about
	// the flags (ident etc.) because we're not changing the character of the matrix (unless
	// it's a pure translation by an integer amount, which is a no-op).
	// We also don't have to worry about preserving unit offsets for animation, because the
	// output of this function is used as a static matrix (for uv generation). We could check
	// and not do this on animated transforms or something. mf
	// Note that we can only do this if the texture wraps (not clamps)
	if( !hsMaterialConverter::Instance().PreserveUVOffset(mtl) )
	{
		MRow* data = uvTransform->GetAddr();
		int i;
		for( i = 0; i < 2; i++ )
		{
			if( fabsf(data[3][i]) >= 1.f )
			{
				data[3][i] -= float(int(data[3][i]));
			}
		}
	}
	hsGuardEnd;
}

//////////////////////////////////////////////////////////////////////////////
//// Helper Class Functions //////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////

//// plMAXVertexAccNode Constructor //////////////////////////////////////////

plMAXVertexAccNode::plMAXVertexAccNode( const hsPoint3 *point, const hsVector3 *normal, const hsColorRGBA& color, const hsColorRGBA& illum, int numChannels, const hsPoint3 *uvs, UInt32 index )
{
	int		i;


	fPoint = *point;
	fNormal = *normal;
	fColor = color;
	fIllum = illum;
	for( i = 0; i < numChannels; i++ )
		fUVs[ i ] = uvs[ i ];
	fNumChannels = numChannels;
	fIndex = index;

	fNext = nil;
}

//// IsEqual /////////////////////////////////////////////////////////////////
//	Determines whether the node matches the values given.

hsBool		plMAXVertexAccNode::IsEqual( const hsVector3 *normal, const hsColorRGBA& color, const hsColorRGBA& illum, const hsPoint3 *uvs )
{
	int		i;


	if( color != fColor || !( *normal == fNormal ) || illum != fIllum )
		return false;

	for( i = 0; i < fNumChannels; i++ )
	{
		if( !( uvs[ i ] == fUVs[ i ] ) )
			return false;
	}

	return true;
}

//// plMAXVertexAccumulator Constructor & Destructor /////////////////////////

plMAXVertexAccumulator::plMAXVertexAccumulator( int numOrigPoints, int numChannels )
{
	fNumPoints = numOrigPoints;
	fNumChannels = numChannels;

	fPointList = TRACKED_NEW plMAXVertexAccNodePtr[ fNumPoints ];
	memset( fPointList, 0, sizeof( plMAXVertexAccNode * ) * fNumPoints );

	fIndices.Reset();
	fInverseVertTable.Reset();
	fNumVertices = 0;
}

plMAXVertexAccumulator::~plMAXVertexAccumulator()
{
	int					i;
	plMAXVertexAccNode	*node;

	
	for( i = 0; i < fNumPoints; i++ )
	{
		while( fPointList[ i ] != nil )
		{
			node = fPointList[ i ]->fNext;
			delete fPointList[ i ];
			fPointList[ i ] = node;
		}
	}

	delete [] fPointList;
}

//// AddVertex ///////////////////////////////////////////////////////////////
//	Adds a vertex to this accumulator. If it already exists, adds the normals
//	together and stores the old index; if not, creates a new node and adds
//	an index for it. In the end, fIndices will be our final index buffer,
//	while fInverseVertTable is basically an array telling us where to get
//	our vertices when building the vertex buffer.

void	plMAXVertexAccumulator::AddVertex( int index, hsPoint3 *point, hsVector3 *normal, const hsColorRGBA& color, const hsColorRGBA& illum, hsPoint3 *uvs )
{
	plMAXVertexAccNode		*node;


	// See if one exists in this list
	for( node = fPointList[ index ]; node != nil; node = node->fNext )
	{
		if( node->IsEqual( normal, color, illum, uvs ) )
		{
			// Found match! Accumulate
			fIndices.Append( node->fIndex );
			return;
		}
	}


	/// Adding new
	node = TRACKED_NEW plMAXVertexAccNode( point, normal, color, illum, fNumChannels, uvs, fNumVertices );
	fInverseVertTable.Append( index );
	fIndices.Append( fNumVertices++ );

	node->fNext = fPointList[ index ];
	fPointList[ index ] = node;
}

//// StuffMyData /////////////////////////////////////////////////////////////
//	Stuffs the data into the given span. Assumes the span has already begun 
//	creation.

void	plMAXVertexAccumulator::StuffMyData( plMaxNode* maxNode, plGeometrySpan *span, Mesh* mesh, ISkinContextData* skinData )
{
	int						i, j, origIdx;
	plMAXVertexAccNode		*node;
	hsPoint3				*uvs[ plGeometrySpan::kMaxNumUVChannels ];	

	const char* dbgNodeName = maxNode->GetName();
	TempWeightInfo			*weights = nil;

	/// Precalculate the weights if necessary
	weights = TRACKED_NEW TempWeightInfo[ fNumPoints ];
	if( skinData != nil )
	{
		for( i = 0; i < fNumPoints; i++ )
		{
			IFindSkinWeights( skinData, i, weights[ i ].fWeights, &weights[ i ].fIndices );

			/// Debug checking stuff, for testing only
			// Wrong. Geometry span depends on this
			switch( span->fFormat & plGeometrySpan::kSkinWeightMask )
			{
			case plGeometrySpan::kSkin1Weight:
				weights[ i ].fWeights[1] = -1.f;
				break;
			case plGeometrySpan::kSkin2Weights:
				weights[ i ].fWeights[2] = -1.f;
				break;
			case plGeometrySpan::kSkin3Weights:
				weights[ i ].fWeights[3] = -1.f;
				break;
			default:
				hsAssert(false, "Shouldn't have gotten here");
				
				break;
			}
		}
	}
	else if( maxNode->NumBones() )
	{
		IFindAllUserSkinWeights(maxNode, mesh, weights);
	}
	else
	{
		for( i = 0; i < fNumPoints; i++ )
		{
			weights[ i ].fWeights[ 0 ] = weights[ i ].fWeights[ 1 ] = 
										weights[ i ].fWeights[ 2 ] = weights[ i ].fWeights[ 3 ] = -1.0f;
			weights[ i ].fIndices = 0;
		}
	}
	
	plMaxBoneMap *boneMap = maxNode->GetBoneMap();
	if (boneMap)
	{
		for (i = 0; i < fNumPoints; i++)
		{
			UInt8 indices[4];
			indices[0] = (weights[i].fIndices) & 0xff;
			indices[1] = (weights[i].fIndices >> 8) & 0xff;
			indices[2] = (weights[i].fIndices >> 16) & 0xff;
			indices[3] = (weights[i].fIndices >> 24) & 0xff;
			
			for (j = 0; j < 4; j++)
			{
				//if (weights[i].fWeights[j] >= 0)
				//{
					if (indices[j] != 0)
					{
						plMaxNodeBase *bone = maxNode->GetBone(indices[j] - 1);
						char *dbgBoneName = bone->GetName();
						indices[j] = boneMap->GetIndex(bone) + 1;
					}
				//}
			}
			weights[i].fIndices = (indices[0]) |
								  (indices[1] << 8) |
								  (indices[2] << 16) |
								  (indices[3] << 24);
		}
	}
	
	hsScalar maxWgt = 0;
	hsScalar penWgt = 0;
	Int16 maxIdx = -1;
	Int16 penIdx = -1;
	// Find the highest two weighted bones. We'll use just these two to calculate our bounds.
	for( i = 0; i < fNumPoints; i++ )
	{
		if( weights[i].fIndices )
		{
			for( j = 0; j < 4; j++ )
			{
				if( weights[i].fWeights[j] < 0 )
					break;
				if( weights[i].fWeights[j] > maxWgt )
				{
					penWgt = maxWgt;
					penIdx = maxIdx;

					maxWgt = weights[i].fWeights[j];
					maxIdx = (weights[i].fIndices >> (j*8)) & 0xff;
				}
				else
				if( weights[i].fWeights[j] > penWgt )
				{
					penWgt = weights[i].fWeights[j];
					penIdx = (weights[i].fIndices >> (j*8)) & 0xff;
				}
			}
		}
	}
	if( maxIdx < 0 )
		maxIdx = 0;
	if( penIdx < 0 )
		penIdx = maxIdx;
	span->fMaxBoneIdx = maxIdx;
	span->fPenBoneIdx = penIdx;

	/// Stuff the verts
	for( i = 0; i < plGeometrySpan::kMaxNumUVChannels; i++)
		uvs[ i ] = nil;

	for( i = 0; i < fNumVertices; i++ )
	{
		origIdx = fInverseVertTable[ i ];

		// origIdx gets us the list, but we need to know which node in the list
		for( node = fPointList[ origIdx ]; node != nil; node = node->fNext )
		{
			if( node->fIndex == i )
			{
				// Found it! output this one
				hsPoint3		normal;
			
				node->fNormal.Normalize();
				normal.Set( node->fNormal.fX, node->fNormal.fY, node->fNormal.fZ );
				for( j = 0; j < fNumChannels; j++ )
					uvs[ j ] = &node->fUVs[ j ];
	
				/// Add!
				span->AddVertex( &node->fPoint, &normal, node->fColor, node->fIllum, uvs,
								   weights[ origIdx ].fWeights[ 0 ], weights[ origIdx ].fWeights[ 1 ], 
								   weights[ origIdx ].fWeights[ 2 ], weights[ origIdx ].fIndices );
				break;
			}
		}
		hsAssert( node != nil, "Invalid accumulator table when stuffing buffers!" );
	}

	/// Now stuff the indices
	for( i = 0; i < fIndices.GetCount(); i++ )
		span->AddIndex( fIndices[ i ] );

	if( weights != nil )
		delete [] weights;
}

// IFindAllUserSkinWeights
// Like IFindSkinWeights, but doesn't use Max's native skinning (e.g. ISkinContextData).
// Rather, someone has put a bone (currently only support one) on this plMaxNode,
// and told us what vertex channel to find the weight for that bone in.
void plMAXVertexAccumulator::IFindAllUserSkinWeights( plMaxNode* node, Mesh* mesh, TempWeightInfo weights[])
{
	const char* dbgNodeName = node->GetName();

	int iMap = MAP_ALPHA; // FISH HACK, till we stuff the src channel into the max node.
	iMap = 66;

	int iChan = 1; // FISH HACK, get this one stuffed too. Could probably or them into the same
	//thing or something, but who cares. Gotta stop with the ethers.

	UVVert *wgtMap = mesh->mapVerts(iMap);	
	int numWgtVerts = mesh->getNumMapVerts(iMap);

	TVFace* mapFaces = mesh->mapFaces(iMap);

	if( wgtMap && mapFaces )
	{
		Face* faces = mesh->faces;

		int i;
		for( i = 0; i < mesh->getNumFaces(); i++ )
		{
			int j;
			for( j = 0; j < 3; j++ )
			{
				int iVtx = faces[i].getVert(j);
				int iTvtx = mapFaces[i].getTVert(j);

				weights[iVtx].fWeights[2] = weights[iVtx].fWeights[3] = 0;
				weights[iVtx].fWeights[0] = 1.f - wgtMap[iTvtx][iChan];
				if( weights[iVtx].fWeights[0] > 1.f )
					weights[iVtx].fWeights[0] = 1.f;
				else if( weights[iVtx].fWeights[0] < 0 )
					weights[iVtx].fWeights[0] = 0;
				weights[iVtx].fWeights[1] = 1.f - weights[iVtx].fWeights[0];
				weights[iVtx].fIndices = 1 << 8;
			}
		}
	}
	else
	{
		int i;
		for( i = 0; i < mesh->getNumVerts(); i++ )
		{
			weights[i].fWeights[1] = weights[i].fWeights[2] = weights[i].fWeights[3] = 0;
			weights[i].fWeights[0] = 1.f;
			weights[i].fIndices = 1 << 8;
		}
	}
}

//// IFindSkinWeights ////////////////////////////////////////////////////////
//	Finds the biggest weights (up to 4) for the given vertex index and returns
//	them, along with a dword specifying the indices for each.

void	plMAXVertexAccumulator::IFindSkinWeights( ISkinContextData *skinData, 
												  int vertex, 
												  float *weights, UInt32 *indices )
{
	float	tempWs[ 4 ], tempW, t;
	UInt32	idxs[ 4 ], tempIdx, tI;
	int		i, j;

	tempWs[ 0 ] = tempWs[ 1 ] = tempWs[ 2 ] = tempWs[ 3 ] = 0;
	idxs[ 0 ] = idxs[ 1 ] = idxs[ 2 ] = idxs[ 3 ] = 0;

	int boneCount = skinData->GetNumAssignedBones( vertex);

	if( boneCount )
	{
		hsScalar defWgt = 1.f;
		for( i = 0; i < boneCount; i++ )
		{
			/// Grab the weight and index for this bone
			tempW = skinData->GetBoneWeight( vertex, i );
			defWgt -= tempW;

			// GetAssignedBone will assert unpredictably if the weight is 0.0f (exactly)
			// It will usually then return 0 for the bone index, but sometimes 1
			// In any case, the bone index should not matter at that point.
			// Without walking through all the downstream code, seems to work ok.
			if(tempW > 0.0f)
				tempIdx = skinData->GetAssignedBone( vertex, i ) + 1;
			else
				tempIdx = 0;

	//	float hi = skinData->GetBoneWeight( vertex, tempIdx );

			/// Slide it in to our list
			for( j = 0; j < 4; j++ )
			{
				if( tempWs[ j ] < tempW )
				{
					t = tempWs[ j ];
					tempWs[ j ] = tempW;
					tempW = t;

					tI = idxs[ j ];
					idxs[ j ] = tempIdx;
					tempIdx = tI;
				}
			}
		}

		// This isn't really what we want. If the weights add up to less than
		// 1.f, the remainder is the un-skinned, un-boned Transform.
		// If the weights add up to more than 1.f, someone probably screwed up,
		// but we'll deal with it gracefully by normalizing.
		if( defWgt > 0 )
		{
			tempW = defWgt;
			tempIdx = 0;

			/// Slide it in to our list
			for( j = 0; j < 4; j++ )
			{
				if( tempWs[ j ] < tempW )
				{
					t = tempWs[ j ];
					tempWs[ j ] = tempW;
					tempW = t;

					tI = idxs[ j ];
					idxs[ j ] = tempIdx;
					tempIdx = tI;
				}
			}
		}

		t = tempWs[ 0 ] + tempWs[ 1 ] + tempWs[ 2 ] + tempWs[ 3 ];
		t = 1.0f / t;

		weights[ 0 ] = tempWs[ 0 ] * t;
		weights[ 1 ] = tempWs[ 1 ] * t;
		weights[ 2 ] = tempWs[ 2 ] * t;
		weights[ 3 ] = tempWs[ 3 ] * t;
	}
	else
	{
		weights[ 0 ] = 1.f;
		idxs[ 0 ] = 0;

		weights[1] = weights[2] = weights[3] = 0;
		idxs[1] = idxs[2] = idxs[3] = 0;
	}

	if( skinData->GetNumAssignedBones( vertex ) < 2 )
	{
		if( idxs[0] )
		{
			float tWgt = weights[0];
			int tIdx = idxs[0];

			weights[0] = weights[1];
			idxs[0] = idxs[1];

			weights[1] = tWgt;
			idxs[1] = tIdx;
		}
	}
	
	*indices = ( idxs[ 0 ] & 0xff ) | ( ( idxs[ 1 ] & 0xff ) << 8 ) | 
			   ( ( idxs[ 2 ] & 0xff ) << 16 ) | ( ( idxs[ 3 ] & 0xff ) << 24 );
}

int plMAXVertexAccumulator::GetVertexCount()
{
//	return fIndices.GetCount();
	return fNumVertices;
}

void SetWaterColor(plGeometrySpan* span)
{
	plAccessGeometry accGeom;
	// First, set up our access, iterators and that mess.
	plAccessSpan acc;
	accGeom.AccessSpanFromGeometrySpan(acc, span);
	if( !acc.HasAccessTri() )
	{
		plAccessGeometry::Instance()->Close(acc);
		return;
	}
	plAccessTriSpan& tri = acc.AccessTri();
	plAccTriIterator triIter(&tri);

	const int nVerts = tri.VertCount();
	// Now, set up our accumulators
	hsTArray<hsScalar> lens;
	lens.SetCount(nVerts);
	memset(lens.AcquireArray(), 0, nVerts * sizeof(hsScalar));
	hsTArray<hsScalar> wgts;
	wgts.SetCount(nVerts);
	memset(wgts.AcquireArray(), 0, nVerts * sizeof(hsScalar));

	// For each triangle
	for( triIter.Begin(); triIter.More(); triIter.Advance() )
	{
		// This area thing seems like a good robust idea, but it doesn't really
		// take into account the fact that the sampling frequency is really determined
		// by the weakest link, or in this case the longest link. Experimenting
		// with alternatives.
		// Actually, I just realized that the area way kind of sucks, because, 
		// as a parallelogram gets less and less rectangular, the area goes down
		// even as the longest edge (the diagonal) gets longer.
		hsScalar lenSq20 = hsVector3(&triIter.Position(2), &triIter.Position(0)).MagnitudeSquared();
		hsScalar lenSq10 = hsVector3(&triIter.Position(1), &triIter.Position(0)).MagnitudeSquared();
		hsScalar lenSq21 = hsVector3(&triIter.Position(2), &triIter.Position(1)).MagnitudeSquared();
		hsScalar len = lenSq20;
		if( len < lenSq10 )
			len = lenSq10;
		if( len < lenSq21 )
			len = lenSq21;
		len = hsSquareRoot(len);

		lens[triIter.RawIndex(0)] += len;
		wgts[triIter.RawIndex(0)] += 1.f;

		lens[triIter.RawIndex(1)] += len;
		wgts[triIter.RawIndex(1)] += 1.f;

		lens[triIter.RawIndex(2)] += len;
		wgts[triIter.RawIndex(2)] += 1.f;

	}
	// For each vert
	int iVert;
	for( iVert = 0; iVert < nVerts; iVert++ )
	{
		if( wgts[iVert] > 0.f )
			lens[iVert] /= wgts[iVert];
		
		wgts[iVert] = 0.f; // We'll use them again on smoothing.
	}
	// Now we might want to smooth this out some
	// This can be repeated for any degree of smoothing
	hsTArray<hsScalar> smLens;
	smLens.SetCount(nVerts);
	memset(smLens.AcquireArray(), 0, nVerts * sizeof(hsScalar));
	// For each triangle
	for( triIter.Begin(); triIter.More(); triIter.Advance() )
	{
		int i;
		// For each edge
		for( i = 0; i < 3; i++ )
		{
			int iVert = triIter.RawIndex(i);
			int iVertNext = triIter.RawIndex(i < 2 ? i+1 : 0);
			int iVertLast = triIter.RawIndex(i ? i-1 : 2);
			smLens[iVert] += lens[iVert];
			wgts[iVert] += 1.f;

			const hsScalar kSmooth(8.f);
			smLens[iVertNext] += lens[iVert] * kSmooth;
			wgts[iVertNext] += kSmooth;

			smLens[iVertLast] += lens[iVert] * kSmooth;
			wgts[iVertLast] += kSmooth;
		}
	}
	lens.Swap(smLens);
	// For each vert
	for( iVert = 0; iVert < nVerts; iVert++ )
	{
		if( wgts[iVert] > 0.f )
			lens[iVert] /= wgts[iVert];
		
		wgts[iVert] = 0.f; // We'll use them again on smoothing.
	}

	plConst(hsScalar) kNumLens(4.f);
	// Okay, we have smoothed lengths. We just need to 
	// iterate over the vertices and stuff 1/len into the alpha channel
	// For each vert
	plAccDiffuseIterator colIter(&tri);
	for( iVert = 0, colIter.Begin(); colIter.More(); iVert++, colIter.Advance() )
	{
		hsColorRGBA multCol;
		hsColorRGBA addCol;
		span->ExtractInitColor(iVert, &multCol, &addCol);

		// Get the vert color
		hsColorRGBA col = colIter.DiffuseRGBA();
		col = multCol;
		
		col.a = lens[iVert] > 0.f ? 1.f / (kNumLens * lens[iVert]) : 1.f;

		// Stuff color back in.
		*colIter.Diffuse32() = col.ToARGB32();
	}

	// Close up the access span.
	accGeom.Close(acc);
}

void SetWaterColor(hsTArray<plGeometrySpan*>& spans)
{
	int i;
	for( i = 0; i < spans.GetCount(); i++ )
		SetWaterColor(spans[i]);
}