diff options
Diffstat (limited to 'mp/src/utils/vrad/incremental.cpp')
| -rw-r--r-- | mp/src/utils/vrad/incremental.cpp | 766 |
1 files changed, 766 insertions, 0 deletions
diff --git a/mp/src/utils/vrad/incremental.cpp b/mp/src/utils/vrad/incremental.cpp new file mode 100644 index 00000000..9dd877e0 --- /dev/null +++ b/mp/src/utils/vrad/incremental.cpp @@ -0,0 +1,766 @@ +//========= Copyright Valve Corporation, All rights reserved. ============//
+//
+// Purpose:
+//
+// $NoKeywords: $
+//
+//=============================================================================//
+#include "incremental.h"
+#include "lightmap.h"
+
+
+
+static bool g_bFileError = false;
+
+
+// -------------------------------------------------------------------------------- //
+// Static helpers.
+// -------------------------------------------------------------------------------- //
+
+static bool CompareLights( dworldlight_t *a, dworldlight_t *b )
+{
+ static float flEpsilon = 1e-7;
+
+ bool a1 = VectorsAreEqual( a->origin, b->origin, flEpsilon );
+ bool a2 = VectorsAreEqual( a->intensity, b->intensity, 1.1f ); // intensities are huge numbers
+ bool a3 = VectorsAreEqual( a->normal, b->normal, flEpsilon );
+ bool a4 = fabs( a->constant_attn - b->constant_attn ) < flEpsilon;
+ bool a5 = fabs( a->linear_attn - b->linear_attn ) < flEpsilon;
+ bool a6 = fabs( a->quadratic_attn - b->quadratic_attn ) < flEpsilon;
+ bool a7 = fabs( float( a->flags - b->flags ) ) < flEpsilon;
+ bool a8 = fabs( a->stopdot - b->stopdot ) < flEpsilon;
+ bool a9 = fabs( a->stopdot2 - b->stopdot2 ) < flEpsilon;
+ bool a10 = fabs( a->exponent - b->exponent ) < flEpsilon;
+ bool a11 = fabs( a->radius - b->radius ) < flEpsilon;
+
+ return a1 && a2 && a3 && a4 && a5 && a6 && a7 && a8 && a9 && a10 && a11;
+}
+
+
+long FileOpen( char const *pFilename, bool bRead )
+{
+ g_bFileError = false;
+ return (long)g_pFileSystem->Open( pFilename, bRead ? "rb" : "wb" );
+}
+
+
+void FileClose( long fp )
+{
+ if( fp )
+ g_pFileSystem->Close( (FILE*)fp );
+}
+
+
+// Returns true if there was an error reading from the file.
+bool FileError()
+{
+ return g_bFileError;
+}
+
+static inline void FileRead( long fp, void *pOut, int size )
+{
+ if( g_bFileError || g_pFileSystem->Read( pOut, size, (FileHandle_t)fp ) != size )
+ {
+ g_bFileError = true;
+ memset( pOut, 0, size );
+ }
+}
+
+
+template<class T>
+static inline void FileRead( long fp, T &out )
+{
+ FileRead( fp, &out, sizeof(out) );
+}
+
+
+static inline void FileWrite( long fp, void const *pData, int size )
+{
+ if( g_bFileError || g_pFileSystem->Write( pData, size, (FileHandle_t)fp ) != size )
+ {
+ g_bFileError = true;
+ }
+}
+
+
+template<class T>
+static inline void FileWrite( long fp, T out )
+{
+ FileWrite( fp, &out, sizeof(out) );
+}
+
+
+IIncremental* GetIncremental()
+{
+ static CIncremental inc;
+ return &inc;
+}
+
+
+// -------------------------------------------------------------------------------- //
+// CIncremental.
+// -------------------------------------------------------------------------------- //
+
+CIncremental::CIncremental()
+{
+ m_TotalMemory = 0;
+ m_pIncrementalFilename = NULL;
+ m_pBSPFilename = NULL;
+ m_bSuccessfulRun = false;
+}
+
+
+CIncremental::~CIncremental()
+{
+}
+
+
+bool CIncremental::Init( char const *pBSPFilename, char const *pIncrementalFilename )
+{
+ m_pBSPFilename = pBSPFilename;
+ m_pIncrementalFilename = pIncrementalFilename;
+ return true;
+}
+
+
+bool CIncremental::PrepareForLighting()
+{
+ if( !m_pBSPFilename )
+ return false;
+
+ // Clear the touched faces list.
+ m_FacesTouched.SetSize( numfaces );
+ memset( m_FacesTouched.Base(), 0, numfaces );
+
+ // If we haven't done a complete successful run yet, then we either haven't
+ // loaded the lights, or a run was aborted and our lights are half-done so we
+ // should reload them.
+ if( !m_bSuccessfulRun )
+ LoadIncrementalFile();
+
+ // unmatched = a list of the lights we have
+ CUtlLinkedList<int,int> unmatched;
+ for( int i=m_Lights.Head(); i != m_Lights.InvalidIndex(); i = m_Lights.Next(i) )
+ unmatched.AddToTail( i );
+
+ // Match the light lists and get rid of lights that we already have all the data for.
+ directlight_t *pNext;
+ directlight_t **pPrev = &activelights;
+ for( directlight_t *dl=activelights; dl != NULL; dl = pNext )
+ {
+ pNext = dl->next;
+
+ //float flClosest = 3000000000;
+ //CIncLight *pClosest = 0;
+
+ // Look for this light in our light list.
+ int iNextUnmatched, iUnmatched;
+ for( iUnmatched=unmatched.Head(); iUnmatched != unmatched.InvalidIndex(); iUnmatched = iNextUnmatched )
+ {
+ iNextUnmatched = unmatched.Next( iUnmatched );
+
+ CIncLight *pLight = m_Lights[ unmatched[iUnmatched] ];
+
+ //float flTest = (pLight->m_Light.origin - dl->light.origin).Length();
+ //if( flTest < flClosest )
+ //{
+ // flClosest = flTest;
+ // pClosest = pLight;
+ //}
+
+ if( CompareLights( &dl->light, &pLight->m_Light ) )
+ {
+ unmatched.Remove( iUnmatched );
+
+ // Ok, we have this light's data already, yay!
+ // Get rid of it from the active light list.
+ *pPrev = dl->next;
+ free( dl );
+ dl = 0;
+ break;
+ }
+ }
+
+ //bool bTest=false;
+ //if(bTest)
+ // CompareLights( &dl->light, &pClosest->m_Light );
+
+ if( iUnmatched == unmatched.InvalidIndex() )
+ pPrev = &dl->next;
+ }
+
+ // Remove any of our lights that were unmatched.
+ for( int iUnmatched=unmatched.Head(); iUnmatched != unmatched.InvalidIndex(); iUnmatched = unmatched.Next( iUnmatched ) )
+ {
+ CIncLight *pLight = m_Lights[ unmatched[iUnmatched] ];
+
+ // First tag faces that it touched so they get recomposited.
+ for( unsigned short iFace=pLight->m_LightFaces.Head(); iFace != pLight->m_LightFaces.InvalidIndex(); iFace = pLight->m_LightFaces.Next( iFace ) )
+ {
+ m_FacesTouched[ pLight->m_LightFaces[iFace]->m_FaceIndex ] = 1;
+ }
+
+ delete pLight;
+ m_Lights.Remove( unmatched[iUnmatched] );
+ }
+
+ // Now add a light structure for each new light.
+ AddLightsForActiveLights();
+
+ return true;
+}
+
+
+bool CIncremental::ReadIncrementalHeader( long fp, CIncrementalHeader *pHeader )
+{
+ int version;
+ FileRead( fp, version );
+ if( version != INCREMENTALFILE_VERSION )
+ return false;
+
+ int nFaces;
+ FileRead( fp, nFaces );
+
+ pHeader->m_FaceLightmapSizes.SetSize( nFaces );
+ FileRead( fp, pHeader->m_FaceLightmapSizes.Base(), sizeof(CIncrementalHeader::CLMSize) * nFaces );
+
+ return !FileError();
+}
+
+
+bool CIncremental::WriteIncrementalHeader( long fp )
+{
+ int version = INCREMENTALFILE_VERSION;
+ FileWrite( fp, version );
+
+ int nFaces = numfaces;
+ FileWrite( fp, nFaces );
+
+ CIncrementalHeader hdr;
+ hdr.m_FaceLightmapSizes.SetSize( nFaces );
+
+ for( int i=0; i < nFaces; i++ )
+ {
+ hdr.m_FaceLightmapSizes[i].m_Width = g_pFaces[i].m_LightmapTextureSizeInLuxels[0];
+ hdr.m_FaceLightmapSizes[i].m_Height = g_pFaces[i].m_LightmapTextureSizeInLuxels[1];
+ }
+
+ FileWrite( fp, hdr.m_FaceLightmapSizes.Base(), sizeof(CIncrementalHeader::CLMSize) * nFaces );
+
+ return !FileError();
+}
+
+
+bool CIncremental::IsIncrementalFileValid()
+{
+ long fp = FileOpen( m_pIncrementalFilename, true );
+ if( !fp )
+ return false;
+
+ bool bValid = false;
+ CIncrementalHeader hdr;
+ if( ReadIncrementalHeader( fp, &hdr ) )
+ {
+ // If the number of faces is the same and their lightmap sizes are the same,
+ // then this file is considered a legitimate incremental file.
+ if( hdr.m_FaceLightmapSizes.Count() == numfaces )
+ {
+ int i;
+ for( i=0; i < numfaces; i++ )
+ {
+ if( hdr.m_FaceLightmapSizes[i].m_Width != g_pFaces[i].m_LightmapTextureSizeInLuxels[0] ||
+ hdr.m_FaceLightmapSizes[i].m_Height != g_pFaces[i].m_LightmapTextureSizeInLuxels[1] )
+ {
+ break;
+ }
+ }
+
+ // Were all faces valid?
+ if( i == numfaces )
+ bValid = true;
+ }
+ }
+
+ FileClose( fp );
+ return bValid && !FileError();
+}
+
+
+void CIncremental::AddLightToFace(
+ IncrementalLightID lightID,
+ int iFace,
+ int iSample,
+ int lmSize,
+ float dot,
+ int iThread )
+{
+ // If we're not being used, don't do anything.
+ if( !m_pIncrementalFilename )
+ return;
+
+ CIncLight *pLight = m_Lights[lightID];
+
+ // Check for the 99.99% case in which the face already exists.
+ CLightFace *pFace;
+ if( pLight->m_pCachedFaces[iThread] &&
+ pLight->m_pCachedFaces[iThread]->m_FaceIndex == iFace )
+ {
+ pFace = pLight->m_pCachedFaces[iThread];
+ }
+ else
+ {
+ bool bNew;
+
+ EnterCriticalSection( &pLight->m_CS );
+ pFace = pLight->FindOrCreateLightFace( iFace, lmSize, &bNew );
+ LeaveCriticalSection( &pLight->m_CS );
+
+ pLight->m_pCachedFaces[iThread] = pFace;
+
+ if( bNew )
+ m_TotalMemory += pFace->m_LightValues.Count() * sizeof( pFace->m_LightValues[0] );
+ }
+
+ // Add this into the light's data.
+ pFace->m_LightValues[iSample].m_Dot = dot;
+}
+
+
+unsigned short DecodeCharOrShort( CUtlBuffer *pIn )
+{
+ unsigned short val = pIn->GetUnsignedChar();
+ if( val & 0x80 )
+ {
+ val = ((val & 0x7F) << 8) | pIn->GetUnsignedChar();
+ }
+
+ return val;
+}
+
+
+void EncodeCharOrShort( CUtlBuffer *pBuf, unsigned short val )
+{
+ if( (val & 0xFF80) == 0 )
+ {
+ pBuf->PutUnsignedChar( (unsigned char)val );
+ }
+ else
+ {
+ if( val > 32767 )
+ val = 32767;
+
+ pBuf->PutUnsignedChar( (val >> 8) | 0x80 );
+ pBuf->PutUnsignedChar( val & 0xFF );
+ }
+}
+
+
+void DecompressLightData( CUtlBuffer *pIn, CUtlVector<CLightValue> *pOut )
+{
+ int iOut = 0;
+ while( pIn->TellGet() < pIn->TellPut() )
+ {
+ unsigned char runLength = pIn->GetUnsignedChar();
+ unsigned short usVal = DecodeCharOrShort( pIn );
+
+ while( runLength > 0 )
+ {
+ --runLength;
+
+ pOut->Element(iOut).m_Dot = usVal;
+ ++iOut;
+ }
+ }
+}
+
+#ifdef _WIN32
+#pragma warning (disable:4701)
+#endif
+
+void CompressLightData(
+ CLightValue const *pValues,
+ int nValues,
+ CUtlBuffer *pBuf )
+{
+ unsigned char runLength=0;
+ unsigned short flLastValue;
+
+ for( int i=0; i < nValues; i++ )
+ {
+ unsigned short flCurValue = (unsigned short)pValues[i].m_Dot;
+
+ if( i == 0 )
+ {
+ flLastValue = flCurValue;
+ runLength = 1;
+ }
+ else if( flCurValue == flLastValue && runLength < 255 )
+ {
+ ++runLength;
+ }
+ else
+ {
+ pBuf->PutUnsignedChar( runLength );
+ EncodeCharOrShort( pBuf, flLastValue );
+
+ flLastValue = flCurValue;
+ runLength = 1;
+ }
+ }
+
+ // Write the end..
+ if( runLength )
+ {
+ pBuf->PutUnsignedChar( runLength );
+ EncodeCharOrShort( pBuf, flLastValue );
+ }
+}
+
+#ifdef _WIN32
+#pragma warning (default:4701)
+#endif
+
+void MultiplyValues( CUtlVector<CLightValue> &values, float scale )
+{
+ for( int i=0; i < values.Count(); i++ )
+ values[i].m_Dot *= scale;
+}
+
+
+void CIncremental::FinishFace(
+ IncrementalLightID lightID,
+ int iFace,
+ int iThread )
+{
+ CIncLight *pLight = m_Lights[lightID];
+
+ // Check for the 99.99% case in which the face already exists.
+ CLightFace *pFace;
+ if( pLight->m_pCachedFaces[iThread] && pLight->m_pCachedFaces[iThread]->m_FaceIndex == iFace )
+ {
+ pFace = pLight->m_pCachedFaces[iThread];
+
+ // Compress the data.
+ MultiplyValues( pFace->m_LightValues, pLight->m_flMaxIntensity );
+
+ pFace->m_CompressedData.SeekPut( CUtlBuffer::SEEK_HEAD, 0 );
+ CompressLightData(
+ pFace->m_LightValues.Base(),
+ pFace->m_LightValues.Count(),
+ &pFace->m_CompressedData );
+
+#if 0
+ // test decompression
+ CUtlVector<CLightValue> test;
+ test.SetSize( 2048 );
+ pFace->m_CompressedData.SeekGet( CUtlBuffer::SEEK_HEAD, 0 );
+ DecompressLightData( &pFace->m_CompressedData, &test );
+#endif
+
+ if( pFace->m_CompressedData.TellPut() == 0 )
+ {
+ // No contribution.. delete this face from the light.
+ EnterCriticalSection( &pLight->m_CS );
+ pLight->m_LightFaces.Remove( pFace->m_LightFacesIndex );
+ delete pFace;
+ LeaveCriticalSection( &pLight->m_CS );
+ }
+ else
+ {
+ // Discard the uncompressed data.
+ pFace->m_LightValues.Purge();
+ m_FacesTouched[ pFace->m_FaceIndex ] = 1;
+ }
+ }
+}
+
+
+bool CIncremental::Finalize()
+{
+ // If we're not being used, don't do anything.
+ if( !m_pIncrementalFilename || !m_pBSPFilename )
+ return false;
+
+ CUtlVector<CFaceLightList> faceLights;
+ LinkLightsToFaces( faceLights );
+
+ Vector faceLight[(MAX_LIGHTMAP_DIM_WITHOUT_BORDER+2) * (MAX_LIGHTMAP_DIM_WITHOUT_BORDER+2)];
+ CUtlVector<CLightValue> faceLightValues;
+ faceLightValues.SetSize( (MAX_LIGHTMAP_DIM_WITHOUT_BORDER+2) * (MAX_LIGHTMAP_DIM_WITHOUT_BORDER+2) );
+
+ // Only update the faces we've touched.
+ for( int facenum = 0; facenum < numfaces; facenum++ )
+ {
+ if( !m_FacesTouched[facenum] || !faceLights[facenum].Count() )
+ continue;
+
+ int w = g_pFaces[facenum].m_LightmapTextureSizeInLuxels[0]+1;
+ int h = g_pFaces[facenum].m_LightmapTextureSizeInLuxels[1]+1;
+ int nLuxels = w * h;
+ assert( nLuxels <= sizeof(faceLight) / sizeof(faceLight[0]) );
+
+ // Clear the lighting for this face.
+ memset( faceLight, 0, nLuxels * sizeof(Vector) );
+
+ // Composite all the light contributions.
+ for( int iFace=0; iFace < faceLights[facenum].Count(); iFace++ )
+ {
+ CLightFace *pFace = faceLights[facenum][iFace];
+
+ pFace->m_CompressedData.SeekGet( CUtlBuffer::SEEK_HEAD, 0 );
+ DecompressLightData( &pFace->m_CompressedData, &faceLightValues );
+
+ for( int iSample=0; iSample < nLuxels; iSample++ )
+ {
+ float flDot = faceLightValues[iSample].m_Dot;
+ if( flDot )
+ {
+ VectorMA(
+ faceLight[iSample],
+ flDot / pFace->m_pLight->m_flMaxIntensity,
+ pFace->m_pLight->m_Light.intensity,
+ faceLight[iSample] );
+ }
+ }
+ }
+
+ // Convert to the floating-point representation in the BSP file.
+ Vector *pSrc = faceLight;
+ unsigned char *pDest = &(*pdlightdata)[ g_pFaces[facenum].lightofs ];
+
+ for( int iSample=0; iSample < nLuxels; iSample++ )
+ {
+ VectorToColorRGBExp32( *pSrc, *( ColorRGBExp32 *)pDest );
+ pDest += 4;
+ pSrc++;
+ }
+ }
+
+ m_bSuccessfulRun = true;
+ return true;
+}
+
+
+void CIncremental::GetFacesTouched( CUtlVector<unsigned char> &touched )
+{
+ touched.CopyArray( m_FacesTouched.Base(), m_FacesTouched.Count() );
+}
+
+
+bool CIncremental::Serialize()
+{
+ if( !SaveIncrementalFile() )
+ return false;
+
+ WriteBSPFile( (char*)m_pBSPFilename );
+ return true;
+}
+
+
+void CIncremental::Term()
+{
+ m_Lights.PurgeAndDeleteElements();
+ m_TotalMemory = 0;
+}
+
+
+void CIncremental::AddLightsForActiveLights()
+{
+ // Create our lights.
+ for( directlight_t *dl=activelights; dl != NULL; dl = dl->next )
+ {
+ CIncLight *pLight = new CIncLight;
+ dl->m_IncrementalID = m_Lights.AddToTail( pLight );
+
+ // Copy the light information.
+ pLight->m_Light = dl->light;
+ pLight->m_flMaxIntensity = max( dl->light.intensity[0], max( dl->light.intensity[1], dl->light.intensity[2] ) );
+ }
+}
+
+
+bool CIncremental::LoadIncrementalFile()
+{
+ Term();
+
+ if( !IsIncrementalFileValid() )
+ return false;
+
+ long fp = FileOpen( m_pIncrementalFilename, true );
+ if( !fp )
+ return false;
+
+ // Read the header.
+ CIncrementalHeader hdr;
+ if( !ReadIncrementalHeader( fp, &hdr ) )
+ {
+ FileClose( fp );
+ return false;
+ }
+
+
+ // Read the lights.
+ int nLights;
+ FileRead( fp, nLights );
+ for( int iLight=0; iLight < nLights; iLight++ )
+ {
+ CIncLight *pLight = new CIncLight;
+ m_Lights.AddToTail( pLight );
+
+ FileRead( fp, pLight->m_Light );
+ pLight->m_flMaxIntensity =
+ max( pLight->m_Light.intensity.x,
+ max( pLight->m_Light.intensity.y, pLight->m_Light.intensity.z ) );
+
+ int nFaces;
+ FileRead( fp, nFaces );
+ assert( nFaces < 70000 );
+
+ for( int iFace=0; iFace < nFaces; iFace++ )
+ {
+ CLightFace *pFace = new CLightFace;
+ pLight->m_LightFaces.AddToTail( pFace );
+
+ pFace->m_pLight = pLight;
+ FileRead( fp, pFace->m_FaceIndex );
+
+ int dataSize;
+ FileRead( fp, dataSize );
+
+ pFace->m_CompressedData.SeekPut( CUtlBuffer::SEEK_HEAD, 0 );
+ while( dataSize )
+ {
+ --dataSize;
+
+ unsigned char ucData;
+ FileRead( fp, ucData );
+
+ pFace->m_CompressedData.PutUnsignedChar( ucData );
+ }
+ }
+ }
+
+
+ FileClose( fp );
+ return !FileError();
+}
+
+
+bool CIncremental::SaveIncrementalFile()
+{
+ long fp = FileOpen( m_pIncrementalFilename, false );
+ if( !fp )
+ return false;
+
+ if( !WriteIncrementalHeader( fp ) )
+ {
+ FileClose( fp );
+ return false;
+ }
+
+ // Write the lights.
+ int nLights = m_Lights.Count();
+ FileWrite( fp, nLights );
+ for( int iLight=m_Lights.Head(); iLight != m_Lights.InvalidIndex(); iLight = m_Lights.Next( iLight ) )
+ {
+ CIncLight *pLight = m_Lights[iLight];
+
+ FileWrite( fp, pLight->m_Light );
+
+ int nFaces = pLight->m_LightFaces.Count();
+ FileWrite( fp, nFaces );
+ for( int iFace=pLight->m_LightFaces.Head(); iFace != pLight->m_LightFaces.InvalidIndex(); iFace = pLight->m_LightFaces.Next( iFace ) )
+ {
+ CLightFace *pFace = pLight->m_LightFaces[iFace];
+
+ FileWrite( fp, pFace->m_FaceIndex );
+
+ int dataSize = pFace->m_CompressedData.TellPut();
+ FileWrite( fp, dataSize );
+
+ pFace->m_CompressedData.SeekGet( CUtlBuffer::SEEK_HEAD, 0 );
+ while( dataSize )
+ {
+ --dataSize;
+ FileWrite( fp, pFace->m_CompressedData.GetUnsignedChar() );
+ }
+ }
+ }
+
+
+ FileClose( fp );
+ return !FileError();
+}
+
+
+void CIncremental::LinkLightsToFaces( CUtlVector<CFaceLightList> &faceLights )
+{
+ faceLights.SetSize( numfaces );
+
+ for( int iLight=m_Lights.Head(); iLight != m_Lights.InvalidIndex(); iLight = m_Lights.Next( iLight ) )
+ {
+ CIncLight *pLight = m_Lights[iLight];
+
+ for( int iFace=pLight->m_LightFaces.Head(); iFace != pLight->m_LightFaces.InvalidIndex(); iFace = pLight->m_LightFaces.Next( iFace ) )
+ {
+ CLightFace *pFace = pLight->m_LightFaces[iFace];
+
+ if( m_FacesTouched[pFace->m_FaceIndex] )
+ faceLights[ pFace->m_FaceIndex ].AddToTail( pFace );
+ }
+ }
+}
+
+
+// ------------------------------------------------------------------ //
+// CIncLight
+// ------------------------------------------------------------------ //
+
+CIncLight::CIncLight()
+{
+ memset( m_pCachedFaces, 0, sizeof(m_pCachedFaces) );
+ InitializeCriticalSection( &m_CS );
+}
+
+
+CIncLight::~CIncLight()
+{
+ m_LightFaces.PurgeAndDeleteElements();
+ DeleteCriticalSection( &m_CS );
+}
+
+
+CLightFace* CIncLight::FindOrCreateLightFace( int iFace, int lmSize, bool *bNew )
+{
+ if( bNew )
+ *bNew = false;
+
+
+ // Look for it.
+ for( int i=m_LightFaces.Head(); i != m_LightFaces.InvalidIndex(); i=m_LightFaces.Next(i) )
+ {
+ CLightFace *pFace = m_LightFaces[i];
+
+ if( pFace->m_FaceIndex == iFace )
+ {
+ assert( pFace->m_LightValues.Count() == lmSize );
+ return pFace;
+ }
+ }
+
+ // Ok, create one.
+ CLightFace *pFace = new CLightFace;
+ pFace->m_LightFacesIndex = m_LightFaces.AddToTail( pFace );
+ pFace->m_pLight = this;
+
+ pFace->m_FaceIndex = iFace;
+ pFace->m_LightValues.SetSize( lmSize );
+ memset( pFace->m_LightValues.Base(), 0, sizeof( CLightValue ) * lmSize );
+
+ if( bNew )
+ *bNew = true;
+
+ return pFace;
+}
+
+
|