/*==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 .
Additional permissions under GNU GPL version 3 section 7
If you modify this Program, or any covered work, by linking or
combining it with any of RAD Game Tools Bink SDK, Autodesk 3ds Max SDK,
NVIDIA PhysX SDK, Microsoft DirectX SDK, OpenSSL library, Independent
JPEG Group JPEG library, Microsoft Windows Media SDK, or Apple QuickTime SDK
(or a modified version of those libraries),
containing parts covered by the terms of the Bink SDK EULA, 3ds Max EULA,
PhysX SDK EULA, DirectX SDK EULA, OpenSSL and SSLeay licenses, IJG
JPEG Library README, Windows Media SDK EULA, or QuickTime SDK EULA, the
licensors of this Program grant you additional
permission to convey the resulting work. Corresponding Source for a
non-source form of such a combination shall include the source code for
the parts of OpenSSL and IJG JPEG Library used as well as that of the covered
work.
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 &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 : "",
geo->fMaterial ? geo->fMaterial->GetKey()->GetName() : "",
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 : "",
geo->fMaterial ? geo->fMaterial->GetKey()->GetName() : "",
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 : "",
geo->fMaterial ? geo->fMaterial->GetKey()->GetName() : "",
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 : "",
geo->fMaterial ? geo->fMaterial->GetKey()->GetName() : "",
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& refs = *(span->fInstanceRefs);
hsTArray refsHere;
hsTArray 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 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 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 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;
}