You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

768 lines
22 KiB

/*==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==*/
//////////////////////////////////////////////////////////////////////////////
// //
// plDrawableSpans Class Export-only Functions //
// //
//// Version History /////////////////////////////////////////////////////////
// //
// Created 4.3.2001 mcn //
// //
//////////////////////////////////////////////////////////////////////////////
#include "hsTypes.h"
#include "plDrawableSpans.h"
#include "hsStream.h"
#include "hsResMgr.h"
#include "plPipeline.h"
#include "plGeometrySpan.h"
#include "plSpaceTree.h"
#include "plSpaceTreeMaker.h" // This is fun and amusing and wonderful to have here.
// Keep it here forever.
#include "plSurface/hsGMaterial.h"
#include "plPipeline/plFogEnvironment.h"
#include "pnMessage/plRefMsg.h"
#include "pnMessage/plNodeRefMsg.h" // for NodeRefMsg
#include "plMessage/plDeviceRecreateMsg.h"
#include "plPipeline/plGBufferGroup.h"
#include "plSurface/hsGMaterial.h"
#include "plSurface/plLayerInterface.h"
#include "plGImage/plBitmap.h"
#include "plGLight/plLightInfo.h"
#include "plgDispatch.h"
#include "plStatusLog/plStatusLog.h"
//#define VERT_LOG
//// Write ///////////////////////////////////////////////////////////////////
void plDrawableSpans::Write( hsStream* s, hsResMgr* mgr )
{
UInt32 i, j, count;
// Make sure we're optimized before we write (should be tho)
// Optimize();
// Make sure all the garbage is cleaned up
if( fNeedCleanup )
IRemoveGarbage();
// Parent write
plDrawable::Write(s, mgr);
s->WriteSwap32( fProps );
s->WriteSwap32( fCriteria );
s->WriteSwap32( fRenderLevel.fLevel );
/// Write out the material keys
s->WriteSwap32( fMaterials.GetCount() );
for( i = 0; i < fMaterials.GetCount(); i++ )
mgr->WriteKey( s, fMaterials[ i ] );
/// Write out the icicles
s->WriteSwap32( fIcicles.GetCount() );
for( i = 0; i < fIcicles.GetCount(); i++ )
fIcicles[ i ].Write( s );
/// Write out the patches
// FIXME MAJOR VERSION
// no more patches, remove this line
s->WriteSwap32(0);
/// Write out the index table based on the pointer array
count = fSpans.GetCount();
s->WriteSwap32( count );
for( i = 0; i < count; i++ )
{
UInt8 *icicle = (UInt8 *)fSpans[ i ], *base = (UInt8 *)fIcicles.AcquireArray();
j = (UInt32)( icicle - base ) / sizeof( plIcicle );
s->WriteSwap32( j );
}
/// Write out the common keys
for( i = 0; i < count; i++ )
{
// The fog environ key
mgr->WriteKey( s, fSpans[ i ]->fFogEnvironment );
}
/// Write out the bounds and stuff
if( count > 0 )
{
fLocalBounds.Write(s);
fWorldBounds.Write(s);
fMaxWorldBounds.Write(s);
}
for( i = 0; i < count; i++ )
{
if( fSpans[i]->fProps & plSpan::kPropHasPermaLights )
{
UInt32 lcnt = fSpans[i]->fPermaLights.GetCount();
s->WriteSwap32(lcnt);
int j;
for( j = 0; j < lcnt; j++ )
mgr->WriteKey( s, fSpans[i]->fPermaLights[j]);
}
if( fSpans[i]->fProps & plSpan::kPropHasPermaProjs )
{
UInt32 lcnt = fSpans[i]->fPermaProjs.GetCount();
s->WriteSwap32(lcnt);
int j;
for( j = 0; j < lcnt; j++ )
mgr->WriteKey( s, fSpans[i]->fPermaProjs[j]);
}
}
/// Write out the source spans if necessary
s->WriteSwap32( fSourceSpans.GetCount() );
if( fSourceSpans.GetCount() > 0 )
{
for( i = 0; i < fSourceSpans.GetCount(); i++ )
fSourceSpans[ i ]->Write( s );
}
count = fLocalToWorlds.GetCount();
s->WriteSwap32(count);
for( i = 0; i < count; i++ )
{
fLocalToWorlds[i].Write(s);
fWorldToLocals[i].Write(s);
fLocalToBones[i].Write(s);
fBoneToLocals[i].Write(s);
}
// Write out the drawInterface index arrays
s->WriteSwap32( fDIIndices.GetCount() );
for( i = 0; i < fDIIndices.GetCount(); i++ )
{
plDISpanIndex *array = fDIIndices[ i ];
s->WriteSwap32( array->fFlags );
s->WriteSwap32( array->GetCount() );
for( j = 0; j < array->GetCount(); j++ )
s->WriteSwap32( (*array)[ j ] );
}
// Write the groups out
count = fGroups.GetCount();
s->WriteSwap( count );
for( i = 0; i < count; i++ )
{
#ifdef VERT_LOG
hsUNIXStream log;
log.Open("log\\GBuf.log", "ab");
char buf[256];
sprintf(buf, "Drawable Span: %s, GroupNum: %u\r\n", GetKeyName(), i);
log.WriteString(buf);
log.Close();
#endif
fGroups[ i ]->Write( s );
}
/// Other stuff
mgr->WriteCreatable(s, fSpaceTree);
mgr->WriteKey(s, fSceneNode);
/// All done!
}
//// AddDISpans //////////////////////////////////////////////////////////////
// Adds a drawInterface's geometry spans to the list to be collapsed into
// buffers.
UInt32 plDrawableSpans::AddDISpans( hsTArray<plGeometrySpan *> &spans, UInt32 index )
{
int i;
UInt32 spanIdx;
plSpan *span;
hsBounds3Ext bounds;
/// Do garbage cleanup first
if( fNeedCleanup )
IRemoveGarbage();
if (index == (UInt32)-1) // need a new one
{
/// Create a lookup entry
index = fDIIndices.GetCount();
fDIIndices.Append( TRACKED_NEW plDISpanIndex );
fDIIndices[ index ]->fFlags = plDISpanIndex::kNone;
}
plDISpanIndex *spanLookup = fDIIndices[ index ];
/// Add the geometry spans to our list. Also add our internal span
/// copies
for( i = 0; i < spans.GetCount(); i++ )
{
spanLookup->Append( fSourceSpans.GetCount() );
spans[ i ]->fSpanRefIndex = fSourceSpans.GetCount();
fSourceSpans.Append( spans[ i ] );
spanIdx = fIcicles.GetCount();
fIcicles.Append( plIcicle() );
plIcicle *icicle = &fIcicles[ spanIdx ];
span = (plSpan *)icicle;
/// Set common stuff
IAssignMatIdxToSpan( span, spans[ i ]->fMaterial );
span->fLocalToWorld = spans[ i ]->fLocalToWorld;
span->fWorldToLocal = spans[ i ]->fWorldToLocal;
span->fProps |= ( spans[ i ]->fProps & plGeometrySpan::kPropRunTimeLight ) ? plSpan::kPropRunTimeLight : 0;
if( spans[i]->fProps & plGeometrySpan::kPropNoShadowCast )
span->fProps |= plSpan::kPropNoShadowCast;
if( spans[i]->fProps & plGeometrySpan::kPropNoShadow )
span->fProps |= plSpan::kPropNoShadow;
if( spans[i]->fProps & plGeometrySpan::kPropForceShadow )
span->fProps |= plSpan::kPropForceShadow;
if( spans[i]->fProps & plGeometrySpan::kPropReverseSort )
span->fProps |= plSpan::kPropReverseSort;
if( spans[i]->fProps & plGeometrySpan::kPartialSort )
span->fProps |= plSpan::kPartialSort;
if( spans[i]->fProps & plGeometrySpan::kVisLOS )
{
span->fProps |= plSpan::kVisLOS;
fProps |= plDrawable::kPropHasVisLOS;
}
span->fNumMatrices = spans[ i ]->fNumMatrices;
span->fBaseMatrix = spans[ i ]->fBaseMatrix;
span->fLocalUVWChans = spans[i]->fLocalUVWChans;
span->fMaxBoneIdx = spans[i]->fMaxBoneIdx;
span->fPenBoneIdx = (UInt16)(spans[i]->fPenBoneIdx);
bounds = spans[ i ]->fLocalBounds;
span->fLocalBounds = bounds;
bounds.Transform( &span->fLocalToWorld );
span->fWorldBounds = bounds;
span->fFogEnvironment = spans[ i ]->fFogEnviron;
/// Add to our source indices
fSpans.Append( span );
fSpanSourceIndices.Append( spanIdx );
}
/// Rebuild the pointer array
IRebuildSpanArray();
SetSpaceTree(nil);
fOptimized = false;
return index;
}
//// Optimize ////////////////////////////////////////////////////////////////
void plDrawableSpans::Optimize( void )
{
int i;
if( fOptimized )
return;
/// Sort all the source spans
if( !(fProps & kPropNoReSort) )
ISortSourceSpans();
/// Pack the source spans into spans and indices
IPackSourceSpans();
/// Now that we're done with the source spans, get rid of them! (BLEAH!)
for( i = 0; i < fSourceSpans.GetCount(); i++ )
delete fSourceSpans[ i ];
fSourceSpans.Reset();
if( fCriteria & kCritSortSpans )
{
if( !(fProps & kPropNoReSort) )
fProps |= kPropSortSpans;
}
if( fCriteria & kCritSortFaces )
{
fProps |= kPropSortFaces;
}
/// Now we do a pass at the buffer groups, asking them to tidy up
for( i = 0; i < fGroups.GetCount(); i++ )
fGroups[ i ]->TidyUp();
// Look to see if we have any materials we aren't using anymore.
for( i = 0; i < fMaterials.GetCount(); i++ )
{
ICheckToRemoveMaterial(i);
}
fReadyToRender = false;
// Make the space tree (hierarchical bounds).
plSpaceTreeMaker maker;
maker.Reset();
for( i = 0; i < GetNumSpans(); i++ )
{
maker.AddLeaf( fSpans[ i ]->fWorldBounds, fSpans[ i ]->fProps & plSpan::kPropNoDraw );
}
plSpaceTree* tree = maker.MakeTree();
SetSpaceTree(tree);
/// All done!
fOptimized = true;
}
static plStatusLog* IStartLog(const char* name, int numSpans)
{
static char buff[256];
sprintf(buff, "x%s.log", name);
plStatusLog* statusLog = plStatusLogMgr::GetInstance().CreateStatusLog(
plStatusLogMgr::kDefaultNumLines,
buff,
plStatusLog::kFilledBackground | plStatusLog::kDeleteForMe );
return statusLog;
}
static plStatusLog* IEndLog(plStatusLog* statusLog)
{
delete statusLog;
return nil;
}
static void ILogSpan(plStatusLog* statusLog, plGeometrySpan* geo, plVertexSpan* span, plGBufferGroup* group)
{
if( span->fTypeMask & plSpan::kIcicleSpan )
{
plIcicle* ice = (plIcicle*)span;
if( geo->fProps & plGeometrySpan::kFirstInstance )
{
plGBufferCell* cell = group->GetCell(span->fVBufferIdx, span->fCellIdx);
UInt32 stride = group->GetVertexSize();
UInt32 ptr = cell->fVtxStart + span->fCellOffset * stride;
statusLog->AddLineF("From obj <%s> mat <%s> size %d bytes grp=%d (%d offset)",
geo->fMaxOwner ? geo->fMaxOwner : "<unknown>",
geo->fMaterial ? geo->fMaterial->GetKey()->GetName() : "<unknown>",
geo->GetVertexSize(geo->fFormat) * geo->fNumVerts + sizeof(UInt16) * geo->fNumIndices,
span->fGroupIdx,
ptr
);
// span->fVBufferIdx,
// span->fCellIdx,
// span->fCellOffset,
// span->fVStartIdx,
// span->fVLength,
// ice->fIBufferIdx,
// ice->fIStartIdx,
// ice->fILength
}
else
{
statusLog->AddLineF("Instanced obj <%s> mat <%s> grp=%d (%d/%d/%d/%d/%d/%d/%d/%d)",
geo->fMaxOwner ? geo->fMaxOwner : "<unknown>",
geo->fMaterial ? geo->fMaterial->GetKey()->GetName() : "<unknown>",
span->fGroupIdx,
span->fVBufferIdx,
span->fCellIdx,
span->fCellOffset,
span->fVStartIdx,
span->fVLength,
ice->fIBufferIdx,
ice->fIStartIdx,
ice->fILength
);
}
}
else
{
if( geo->fProps & plGeometrySpan::kFirstInstance )
{
statusLog->AddLineF("From obj <%s> mat <%s> size %d bytes grp=%d (%d/%d/%d/%d/%d)",
geo->fMaxOwner ? geo->fMaxOwner : "<unknown>",
geo->fMaterial ? geo->fMaterial->GetKey()->GetName() : "<unknown>",
geo->GetVertexSize(geo->fFormat) * geo->fNumVerts + sizeof(UInt16) * geo->fNumIndices,
span->fGroupIdx,
span->fVBufferIdx,
span->fCellIdx,
span->fCellOffset,
span->fVStartIdx,
span->fVLength
);
}
else
{
statusLog->AddLineF("Instanced obj <%s> mat <%s> grp=%d (%d/%d/%d/%d/%d)",
geo->fMaxOwner ? geo->fMaxOwner : "<unknown>",
geo->fMaterial ? geo->fMaterial->GetKey()->GetName() : "<unknown>",
span->fGroupIdx,
span->fVBufferIdx,
span->fCellIdx,
span->fCellOffset,
span->fVStartIdx,
span->fVLength
);
}
}
}
//// IPackSourceSpans ////////////////////////////////////////////////////////
// Takes the array of source spans and converts them to our internal icicle
// spans, vertex buffers and index buffers.
void plDrawableSpans::IPackSourceSpans( void )
{
int i, j;
hsBounds3Ext bounds;
hsBitVector doneSpans;
/// Calc bounds
fLocalBounds.MakeEmpty();
fWorldBounds.MakeEmpty();
for( i = 0; i < fSourceSpans.GetCount(); i++ )
{
hsBounds3Ext bnd = fSourceSpans[ i ]->fLocalBounds;
bnd.Transform( &fSourceSpans[ i ]->fLocalToWorld );
fWorldBounds.Union( &bnd );
}
fLocalBounds = fWorldBounds;
fLocalBounds.Transform( &fWorldToLocal );
fMaxWorldBounds = fWorldBounds;
// It could be that instance refs in spans that we (the drawable) own
// are actually spans in some other drawable. That's not currently handled.
// (Making that case handled would involve rewriting the instancing implementation
// to not suck ass).
// So for each instance set, we make two lists of instance refs, the ones also
// in this drawable (refsHere), and ones not (refsThere). If there are refsThere,
// we split out the refsHere into a new instance group, removing them from the
// refsThere list. If refsThere still contains spans from separate drawables,
// that will be dealt with in those drawables' Optimize calls.
doneSpans.Clear();
for( i = 0; i < fSourceSpans.GetCount(); i++ )
{
if( !doneSpans.IsBitSet(i) )
{
plGeometrySpan* span = fSourceSpans[i];
if( span->fInstanceRefs )
{
hsTArray<plGeometrySpan*>& refs = *(span->fInstanceRefs);
hsTArray<plGeometrySpan*> refsHere;
hsTArray<plGeometrySpan*> refsThere;
int k;
for( k = 0; k < refs.GetCount(); k++ )
{
plGeometrySpan* other = refs[k];
if( other != span )
{
int idx = fSourceSpans.Find(other);
if( fSourceSpans.kMissingIndex == idx )
{
refsThere.Append(other);
}
else
{
refsHere.Append(other);
}
}
}
if( refsThere.GetCount() )
{
if( refsHere.GetCount() )
{
span->BreakInstance();
// Okay, got to form a new instance group out of refsHere.
for( k = 0; k < refsHere.GetCount(); k++ )
{
plGeometrySpan* other = refsHere[k];
other->ChangeInstance(span);
doneSpans.SetBit(other->fSpanRefIndex);
}
}
else
{
span->UnInstance();
}
if( refsThere.GetCount() == 1 )
{
refsThere[0]->UnInstance();
}
}
}
doneSpans.SetBit(i);
}
}
/// Now pack the spans
doneSpans.Clear();
for( i = 0; i < fSourceSpans.GetCount(); i++ )
{
// Now we fill the rest of the data in for our span
if( !doneSpans.IsBitSet( i ) )
{
if( fSourceSpans[ i ]->fProps & plGeometrySpan::kInstanced )
{
// Instanced spans--convert the first as normal, then convert the rest
// using the first as reference
doneSpans.SetBit( i, true );
plIcicle *baseIcicle = (plIcicle *)fSpans[ i ];
IConvertGeoSpanToIcicle( fSourceSpans[ i ], baseIcicle, 0, baseIcicle );
// Loop through the rest
for( j = 0; j < fSourceSpans[ i ]->fInstanceRefs->GetCount(); j++ )
{
plGeometrySpan *other = (*fSourceSpans[ i ]->fInstanceRefs)[ j ];
if( other == fSourceSpans[ i ] )
continue;
#if 0 // What exactly is this supposed to be doing? My guess is, NADA.
if( IConvertGeoSpanToIcicle( other, (plIcicle *)fSpans[ other->fSpanRefIndex ], 0, baseIcicle ) )
baseIcicle = (plIcicle *)fSpans[ other->fSpanRefIndex ];
#else // What exactly is this supposed to be doing? My guess is, NADA.
IConvertGeoSpanToIcicle( other, (plIcicle *)fSpans[ other->fSpanRefIndex ], 0, baseIcicle );
#endif // What exactly is this supposed to be doing? My guess is, NADA.
doneSpans.SetBit( other->fSpanRefIndex, true );
}
}
else
{
// Do normal, uninstanced conversion
IConvertGeoSpanToIcicle( fSourceSpans[ i ], (plIcicle *)fSpans[ i ], 0 );
doneSpans.SetBit( i, true );
}
}
}
#ifdef VERT_LOG
hsTArray<plGeometrySpan*> order;
order.SetCount(fSourceSpans.GetCount());
for( i = 0; i < fSourceSpans.GetCount(); i++ )
{
order[fSourceSpans[i]->fSpanRefIndex] = fSourceSpans[i];
}
plStatusLog* statusLog = IStartLog(GetKey()->GetName(), fSourceSpans.GetCount());
for( i = 0; i < order.GetCount(); i++ )
{
plVertexSpan* vSpan = (plVertexSpan*)fSpans[i];
ILogSpan(statusLog, order[i], vSpan, fGroups[vSpan->fGroupIdx]);
}
statusLog = IEndLog(statusLog);
#endif
}
//// ISortSourceSpans ////////////////////////////////////////////////////////
// Does our actual optimization path by resorting all the spans into the
// most efficient order possible. Also has to re-order the span lookup
// table.
void plDrawableSpans::ISortSourceSpans( void )
{
hsTArray<UInt32> spanReorderTable, spanInverseTable;
int i, j, idx;
plGeometrySpan *tmpSpan;
UInt32 tmpIdx;
plSpan *tmpSpanPtr;
// Init the reorder table
for( i = 0; i < fSourceSpans.GetCount(); i++ )
spanReorderTable.Append( i );
// Do a nice, if naiive, sort by material (hehe by the pointers, no less)
for( i = 0; i < fSourceSpans.GetCount() - 1; i++ )
{
for( j = i + 1, idx = i; j < fSourceSpans.GetCount(); j++ )
{
if( ICompareSpans( fSourceSpans[ j ], fSourceSpans[ idx ] ) < 0 )
idx = j;
}
// Swap idx with i, so we get the smallest pointer on top
if( i != idx )
{
// Swap both the source span and our internal span
tmpSpan = fSourceSpans[ i ];
fSourceSpans[ i ] = fSourceSpans[ idx ];
fSourceSpans[ idx ] = tmpSpan;
fSourceSpans[ i ]->fSpanRefIndex = i;
fSourceSpans[ idx ]->fSpanRefIndex = idx;
tmpSpanPtr = fSpans[ i ];
fSpans[ i ] = fSpans[ idx ];
fSpans[ idx ] = tmpSpanPtr;
// Also swap the entries in the reorder table
tmpIdx = spanReorderTable[ i ];
spanReorderTable[ i ] = spanReorderTable[ idx ];
spanReorderTable[ idx ] = tmpIdx;
}
// Next!
}
/// Problem: our reorder table is inversed(y->x instead of x->y). Either we search for numbers,
/// or we just flip it first...
spanInverseTable.SetCountAndZero( spanReorderTable.GetCount() );
for( i = 0; i < spanReorderTable.GetCount(); i++ )
spanInverseTable[ spanReorderTable[ i ] ] = i;
/// Now update our span xlate table
for( i = 0; i < fDIIndices.GetCount(); i++ )
{
if( !fDIIndices[ i ]->IsMatrixOnly() )
{
for( j = 0; j < fDIIndices[ i ]->GetCount(); j++ )
{
idx = (*fDIIndices[ i ])[ j ];
(*fDIIndices[ i ])[ j ] = spanInverseTable[ idx ];
}
}
}
/// Use our pointer array to rebuild the icicle array (UUUUGLY)
hsTArray<plIcicle> tempIcicles;
plIcicle *newIcicle;
tempIcicles.SetCount( fIcicles.GetCount() );
for( i = 0, newIcicle = tempIcicles.AcquireArray();
i < fSpans.GetCount(); i++ )
{
*newIcicle = *( (plIcicle *)fSpans[ i ] );
fSpans[ i ] = newIcicle;
newIcicle++;
}
/// Swap the two arrays out. This will basically swap the actual memory blocks, so be careful...
fIcicles.Swap( tempIcicles );
tempIcicles.Reset();
}
//// ICompareSpans ///////////////////////////////////////////////////////////
// Sorting function for ISortSpans(). Kinda like strcmp(): returns -1 if
// span1 < span2, 1 if span1 > span2, 0 if "equal".
short plDrawableSpans::ICompareSpans( plGeometrySpan *span1, plGeometrySpan *span2 )
{
hsBool b1, b2;
int i, j, numLayers;
plBitmap *t1, *t2;
/// Quick check--identical materials are easy to compare :)
if( span1->fMaterial == span2->fMaterial )
return 0;
/// Compare features from most to least important...
// Any decal span should come after a non-decal
if( span1->fDecalLevel < span2->fDecalLevel )
return -1;
if( span1->fDecalLevel > span2->fDecalLevel )
return 1;
// Ok, they're equal decal-wise, so find something else to judge on
// Most important: is one of the materials an alpha blend? (if so, gotta
// put at end, so it's "bigger")
if( span1->fMaterial->GetNumLayers() > 0 &&
( span1->fMaterial->GetLayer( 0 )->GetState().fBlendFlags & hsGMatState::kBlendMask ) != 0 )
b1 = true;
else
b1 = false;
if( span2->fMaterial->GetNumLayers() > 0 &&
( span2->fMaterial->GetLayer( 0 )->GetState().fBlendFlags & hsGMatState::kBlendMask ) != 0 )
b2 = true;
else
b2 = false;
if( b1 != b2 )
return( b1 ? 1 : -1 );
// Next is texture (name). We do this kinda like strings: compare the first layer's
// textures and go upwards, so that we group materials together starting with the
// base layer's texture and going upwards
numLayers = span1->fMaterial->GetNumLayers();
if( span2->fMaterial->GetNumLayers() < numLayers )
numLayers = span2->fMaterial->GetNumLayers();
for( i = 0; i < numLayers; i++ )
{
t1 = span1->fMaterial->GetLayer( i )->GetTexture();
t2 = span2->fMaterial->GetLayer( i )->GetTexture();
if( t1 != nil && t2 == nil )
return 1;
else if( t1 == nil && t2 != nil )
return -1;
else if( t1 == nil && t2 == nil )
break; // Textures equal up to here--keep going with rest of tests
if( t1->GetKeyName() != nil && t2->GetKeyName() != nil )
{
j = stricmp( t1->GetKeyName(), t2->GetKeyName() );
if( j != 0 )
return (short)j;
}
}
// Finally, by material itself.
if( span1->fMaterial->GetKeyName() != nil && span2->fMaterial->GetKeyName() != nil )
{
j = stricmp( span1->fMaterial->GetKeyName(), span2->fMaterial->GetKeyName() );
if( j != 0 )
return (short)j;
}
if( span1->fLocalToWorld.fFlags != span2->fLocalToWorld.fFlags )
{
if( span1->fLocalToWorld.fFlags )
return -1;
else
return 1;
}
/// Equal in our book...
return 0;
}