//-----------------------------------------------------------------------------
// Torque Game Engine
// Copyright (C) GarageGames.com, Inc.
//-----------------------------------------------------------------------------

#include "sceneGraph/sceneLighting.h"
#include "interior/interiorInstance.h"
#include "interior/interiorRes.h"
#include "interior/interior.h"
#include "dgl/gBitmap.h"
#include "math/mPlane.h"
#include "sceneGraph/sceneGraph.h"
#include "core/fileStream.h"
#include "console/consoleTypes.h"
#include "gui/core/guiCanvas.h"
#include "core/zipSubStream.h"
#include "game/gameConnection.h"

namespace {

   static void findObjectsCallback(SceneObject* obj, void *val)
   {
      Vector<SceneObject*> * list = (Vector<SceneObject*>*)val;
      list->push_back(obj);
   }

   // color: comes in clamped
   // return U16: BGR1
   static U16 convertColor(ColorF color)
   {

#if defined(TORQUE_OS_MAC)
      return((U32(color.red * 31.f + 0.5f) << 11) |
             (U32(color.green * 31.f + 0.5f) << 6) |
             (U32(color.blue * 31.f + 0.5f) << 1) | 1);
#else
      return((U32(color.blue * 31.f + 0.5f) << 11) |
             (U32(color.green * 31.f + 0.5f) << 6) |
             (U32(color.red * 31.f + 0.5f) << 1) | 1);
#endif
   }

   static const Point3F BoxNormals[] =
   {
      Point3F( 1, 0, 0),
      Point3F(-1, 0, 0),
      Point3F( 0, 1, 0),
      Point3F( 0,-1, 0),
      Point3F( 0, 0, 1),
      Point3F( 0, 0,-1)
   };

   static U32 BoxVerts[][4] = {
      {7,6,4,5},     // +x
      {0,2,3,1},     // -x
      {7,3,2,6},     // +y
      {0,1,5,4},     // -y
      {7,5,1,3},     // +z
      {0,4,6,2}      // -z
   };

   static U32 BoxSharedEdgeMask[][6] = {
      {0, 0, 1, 4, 8, 2},
      {0, 0, 2, 8, 4, 1},
      {8, 2, 0, 0, 1, 4},
      {4, 1, 0, 0, 2, 8},
      {1, 4, 8, 2, 0, 0},
      {2, 8, 4, 1, 0, 0}
   };

   static U32 TerrainSquareIndices[][3] = {
      {2, 1, 0},  // 45
      {3, 2, 0},
      {3, 1, 0},  // 135
      {3, 2, 1}
   };

   static Point3F BoxPnts[] = {
      Point3F(0,0,0),
      Point3F(0,0,1),
      Point3F(0,1,0),
      Point3F(0,1,1),
      Point3F(1,0,0),
      Point3F(1,0,1),
      Point3F(1,1,0),
      Point3F(1,1,1)
   };

   SceneLighting *   gLighting = 0;
   F32               gPlaneNormThresh = 0.999;
   F32               gPlaneDistThresh = 0.001;
   F32               gParellelVectorThresh = 0.01;
   bool              gTerminateLighting = false;
   F32               gLightingProgress = 0.f;
   const char *      gCompleteCallback = 0;
   U32               gConnectionMissionCRC = 0xffffffff;
}


//------------------------------------------------------------------------------
class SceneLightingProcessEvent : public SimEvent
{
   private:
      U32      mLightIndex;
      S32      mObjectIndex;

   public:
      SceneLightingProcessEvent(U32 lightIndex, S32 objectIndex)
      {
         mLightIndex = lightIndex;        // size(): end of lighting
         mObjectIndex = objectIndex;      // -1: preLight, size(): next light
      }

      void process(SimObject * object)
      {
         AssertFatal(object, "SceneLightingProcessEvent:: null event object!");
         if(object)
            static_cast<SceneLighting*>(object)->processEvent(mLightIndex, mObjectIndex);
      };
};

//------------------------------------------------------------------------------
bool SceneLighting::smUseVertexLighting   = false;

SceneLighting::SceneLighting()
{
   mStartTime = 0;
   mFileName[0] = 0;
   smUseVertexLighting = Interior::smUseVertexLighting;

   static bool initialized = false;
   if(!initialized)
   {
      Con::addVariable("SceneLighting::terminateLighting", TypeBool, &gTerminateLighting);
      Con::addVariable("SceneLighting::lightingProgress", TypeF32, &gLightingProgress);
      initialized = true;
   }
}

SceneLighting::~SceneLighting()
{
   gLighting = 0;
   gLightingProgress = 0.f;

   ObjectProxy ** proxyItr;
   for(proxyItr = mSceneObjects.begin(); proxyItr != mSceneObjects.end(); proxyItr++)
      delete *proxyItr;
}

void SceneLighting::completed(bool success)
{
   // process the cached lighting files
   processCache();

   if(success)
   {
      AssertFatal(smUseVertexLighting == Interior::smUseVertexLighting, "SceneLighting::completed: vertex lighting state changed during scene light");

      // cannot do anything if vertex state has changed (since we only load in what is needed)
      if(smUseVertexLighting == Interior::smUseVertexLighting)
      {
         if(!smUseVertexLighting)
         {
            gInteriorLMManager.downloadGLTextures();
            gInteriorLMManager.destroyBitmaps();
         }
         else
            gInteriorLMManager.destroyTextures();
      }
   }

   if(gCompleteCallback && gCompleteCallback[0])
      Con::executef(1, gCompleteCallback);
}

//------------------------------------------------------------------------------
// Static access method: there can be only one SceneLighting object
bool SceneLighting::lightScene(const char * callback, BitSet32 flags)
{
   if(gLighting)
   {
      Con::errorf(ConsoleLogEntry::General, "SceneLighting:: forcing restart of lighting!");
      gLighting->deleteObject();
      gLighting = 0;
   }

   SceneLighting * lighting = new SceneLighting;

   // register the object
   if(!lighting->registerObject())
   {
      AssertFatal(0, "SceneLighting:: Unable to register SceneLighting object!");
      Con::errorf(ConsoleLogEntry::General, "SceneLighting:: Unable to register SceneLighting object!");
      delete lighting;
      return(false);
   }

   // could have interior resources but no instances (hey, got this far didnt we...)
   GameConnection * con = dynamic_cast<GameConnection*>(NetConnection::getConnectionToServer());
   if(!con)
   {
      Con::errorf(ConsoleLogEntry::General, "SceneLighting:: no GameConnection");
      return(false);
   }
   con->addObject(lighting);

   // set the globals
   gLighting = lighting;
   gTerminateLighting = false;
   gLightingProgress = 0.f;
   gCompleteCallback = callback;
   gConnectionMissionCRC = con->getMissionCRC();

   // dont want to delete the current textures because bitmaps may already be loaded
   gInteriorLMManager.purgeGLTextures();

   if(!lighting->light(flags))
   {
      lighting->completed(true);
      lighting->deleteObject();
      return(false);
   }
   return(true);
}

ConsoleFunction(lightScene, bool, 1, 3, "(script_function completeCallback=NULL, string mode=\"\")"
                "Relight the scene.\n\n"
                "If mode is \"forceAlways\", the lightmaps will be regenerated regardless of whether "
                "lighting cache files can be written to. If mode is \"forceWritable\", then the lightmaps "
                "will be regenerated only if the lighting cache files can be written.")
{
   const char * callback = StringTable->insert(argv[1]);
   BitSet32 flags = 0;

   if(argc>1)
   {
      if(!dStricmp(argv[2], "forceAlways"))
         flags.set(SceneLighting::ForceAlways);
      else if(!dStricmp(argv[2], "forceWritable"))
         flags.set(SceneLighting::ForceWritable);
   }

   return(SceneLighting::lightScene(callback, flags));
}

bool SceneLighting::isLighting()
{
   return(bool(gLighting));
}

//------------------------------------------------------------------------------
// Class SceneLighting::PersistInfo
//------------------------------------------------------------------------------
U32 SceneLighting::PersistInfo::smFileVersion   = 0x10;

SceneLighting::PersistInfo::~PersistInfo()
{
   for(U32 i = 0; i < mChunks.size(); i++)
      delete mChunks[i];
}

//------------------------------------------------------------------------------
bool SceneLighting::PersistInfo::read(Stream & stream)
{
   U32 version;
   if(!stream.read(&version) || version != smFileVersion)
      return(false);

   U32 numChunks;
   if(!stream.read(&numChunks))
      return(false);

   if(numChunks == 0)
      return(false);

   // read in all the chunks
   for(U32 i = 0; i < numChunks; i++)
   {
      U32 chunkType;
      if(!stream.read(&chunkType))
         return(false);

      // MissionChunk must be first chunk
      if(i == 0 && chunkType != PersistChunk::MissionChunkType)
         return(false);
      if(i != 0 && chunkType == PersistChunk::MissionChunkType)
         return(false);

      // create the chunk
      switch(chunkType)
      {
         case PersistChunk::MissionChunkType:
            mChunks.push_back(new SceneLighting::PersistInfo::MissionChunk);
            break;

         case PersistChunk::InteriorChunkType:
            mChunks.push_back(new SceneLighting::PersistInfo::InteriorChunk);
            break;

         case PersistChunk::TerrainChunkType:
            mChunks.push_back(new SceneLighting::PersistInfo::TerrainChunk);
            break;

         default:
            return(false);
            break;
      }

      // load the chunk info
      if(!mChunks[i]->read(stream))
         return(false);
   }

   return(true);
}

bool SceneLighting::PersistInfo::write(Stream & stream)
{
   if(!stream.write(smFileVersion))
      return(false);

   if(!stream.write((U32)mChunks.size()))
      return(false);

   for(U32 i = 0; i < mChunks.size(); i++)
   {
      if(!stream.write(mChunks[i]->mChunkType))
         return(false);
      if(!mChunks[i]->write(stream))
         return(false);
   }

   return(true);
}

//------------------------------------------------------------------------------
// Class SceneLighting::PersistInfo::PersistChunk
//------------------------------------------------------------------------------
bool SceneLighting::PersistInfo::PersistChunk::read(Stream & stream)
{
   if(!stream.read(&mChunkCRC))
      return(false);
   return(true);
}

bool SceneLighting::PersistInfo::PersistChunk::write(Stream & stream)
{
   if(!stream.write(mChunkCRC))
      return(false);
   return(true);
}

//------------------------------------------------------------------------------
// Class SceneLighting::PersistInfo::MissionChunk
//------------------------------------------------------------------------------
SceneLighting::PersistInfo::MissionChunk::MissionChunk()
{
   mChunkType = PersistChunk::MissionChunkType;
}

//------------------------------------------------------------------------------
// Class SceneLighting::PersistInfo::InteriorChunk
//------------------------------------------------------------------------------
SceneLighting::PersistInfo::InteriorChunk::InteriorChunk()
{
   mChunkType = PersistChunk::InteriorChunkType;
}

SceneLighting::PersistInfo::InteriorChunk::~InteriorChunk()
{
   for(U32 i = 0; i < mLightmaps.size(); i++)
      delete mLightmaps[i];
}

//------------------------------------------------------------------------------
// - always read in vertex lighting, lightmaps may not be needed
bool SceneLighting::PersistInfo::InteriorChunk::read(Stream & stream)
{
   if(!Parent::read(stream))
      return(false);

   U32 size;
   U32 i;

   // lightmaps->vertex-info
   if(!SceneLighting::smUseVertexLighting)
   {
      // size of this minichunk
      if(!stream.read(&size))
         return(false);

      // lightmaps
      stream.read(&size);
      mDetailLightmapCount.setSize(size);
      for(i = 0; i < size; i++)
         if(!stream.read(&mDetailLightmapCount[i]))
            return(false);

      stream.read(&size);
      mDetailLightmapIndices.setSize(size);
      for(i = 0; i < size; i++)
         if(!stream.read(&mDetailLightmapIndices[i]))
            return(false);

      if(!stream.read(&size))
         return(false);
      mLightmaps.setSize(size);

      for(i = 0; i < size; i++)
      {
         mLightmaps[i] = new GBitmap;
         if(!mLightmaps[i]->readPNG(stream))
            return(false);
      }
   }
   else
   {
      // step past the lightmaps
      if(!stream.read(&size))
         return(false);
      if(!stream.setPosition(stream.getPosition() + size))
         return(false);
   }

   // size of the vertex lighting: need to reset stream position after zipStream reading
   U32 zipStreamEnd;
   if(!stream.read(&zipStreamEnd))
      return(false);
   zipStreamEnd += stream.getPosition();

   // vertex lighting
   ZipSubRStream zipStream;
   if(!zipStream.attachStream(&stream))
      return(false);

   if(!zipStream.read(&size))
      return(false);
   mHasAlarmState = bool(size);

   if(!zipStream.read(&size))
      return(false);
   mDetailVertexCount.setSize(size);
   for(i = 0; i < size; i++)
      if(!zipStream.read(&mDetailVertexCount[i]))
         return(false);

   size = 0;
   for(i = 0; i < mDetailVertexCount.size(); i++)
      size += mDetailVertexCount[i];

   mVertexColorsNormal.setSize(size);

   if(mHasAlarmState)
      mVertexColorsAlarm.setSize(size);

   U32 curPos = 0;
   for(i = 0; i < mDetailVertexCount.size(); i++)
   {
      U32 count = mDetailVertexCount[i];
      for(U32 j = 0; j < count; j++)
         if(!zipStream.read(&mVertexColorsNormal[curPos + j]))
            return(false);

      // read in the alarm info
      if(mHasAlarmState)
      {
         // same?
         if(!zipStream.read(&size))
            return(false);
         if(bool(size))
            dMemcpy(&mVertexColorsAlarm[curPos], &mVertexColorsNormal[curPos], count * sizeof(ColorI));
         else
         {
            for(U32 j = 0; j < count; j++)
               if(!zipStream.read(&mVertexColorsAlarm[curPos + j]))
                  return(false);
         }
      }

      curPos += count;
   }

   zipStream.detachStream();

   // since there is no resizeFilterStream the zipStream happily reads
   // off the end of the compressed block... reset the position
   stream.setPosition(zipStreamEnd);

   return(true);
}

bool SceneLighting::PersistInfo::InteriorChunk::write(Stream & stream)
{
   if(!Parent::write(stream))
      return(false);

   // lightmaps
   U32 startPos = stream.getPosition();
   if(!stream.write(U32(0)))
      return(false);

   U32 i;
   if(!stream.write(U32(mDetailLightmapCount.size())))
      return(false);
   for(i = 0; i < mDetailLightmapCount.size(); i++)
      if(!stream.write(mDetailLightmapCount[i]))
         return(false);

   if(!stream.write(U32(mDetailLightmapIndices.size())))
      return(false);
   for(i = 0; i < mDetailLightmapIndices.size(); i++)
      if(!stream.write(mDetailLightmapIndices[i]))
         return(false);

   if(!stream.write(U32(mLightmaps.size())))
      return(false);
   for(i = 0; i < mLightmaps.size(); i++)
   {
      AssertFatal(mLightmaps[i], "SceneLighting::SceneLighting::PersistInfo::InteriorChunk::Write: Invalid bitmap!");
      if(!mLightmaps[i]->writePNG(stream))
         return(false);
   }

   // write out the lightmap portions size
   U32 endPos = stream.getPosition();
   if(!stream.setPosition(startPos))
      return(false);

   // don't include the offset in the size
   if(!stream.write(U32(endPos - startPos - sizeof(U32))))
      return(false);
   if(!stream.setPosition(endPos))
      return(false);

   // vertex lighting: needs the size of the vertex info because the
   // zip stream may read off the end of the chunk
   startPos = stream.getPosition();
   if(!stream.write(U32(0)))
      return(false);

   ZipSubWStream zipStream;
   if(!zipStream.attachStream(&stream))
      return(false);

   if(!zipStream.write(U32(mHasAlarmState)))
      return(false);
   if(!zipStream.write(U32(mDetailVertexCount.size())))
      return(false);

   for(U32 i = 0; i < mDetailVertexCount.size(); i++)
      if(!zipStream.write(mDetailVertexCount[i]))
         return(false);

   U32 curPos = 0;
   for(i = 0; i < mDetailVertexCount.size(); i++)
   {
      U32 count = mDetailVertexCount[i];
      for(U32 j = 0; j < count; j++)
         if(!zipStream.write(mVertexColorsNormal[curPos + j]))
            return(false);

      // do alarm.. check if the same
      if(mHasAlarmState)
      {
         if(!dMemcmp(&mVertexColorsNormal[curPos], &mVertexColorsAlarm[curPos], count * sizeof(ColorI)))
         {
            if(!zipStream.write(U32(1)))
               return(false);
         }
         else
         {
            if(!zipStream.write(U32(0)))
               return(false);
            for(U32 j = 0; j < count; j++)
               if(!zipStream.write(mVertexColorsAlarm[curPos + j]))
                  return(false);
         }
      }

      curPos += count;
   }
   zipStream.detachStream();

   // write out the vertex lighting portions size
   endPos = stream.getPosition();
   if(!stream.setPosition(startPos))
      return(false);

   // don't include the offset in the size
   if(!stream.write(U32(endPos - startPos - sizeof(U32))))
      return(false);
   if(!stream.setPosition(endPos))
      return(false);

   return(true);
}

//------------------------------------------------------------------------------
// Class SceneLighting::PersistInfo::TerrainChunk
//------------------------------------------------------------------------------
SceneLighting::PersistInfo::TerrainChunk::TerrainChunk()
{
   mChunkType = PersistChunk::TerrainChunkType;
   mLightmap = 0;
}

SceneLighting::PersistInfo::TerrainChunk::~TerrainChunk()
{
   delete [] mLightmap;
}

//------------------------------------------------------------------------------

bool SceneLighting::PersistInfo::TerrainChunk::read(Stream & stream)
{
   if(!Parent::read(stream))
      return(false);

   GBitmap bitmap;
   if(!bitmap.readPNG(stream))
      return(false);

   if((bitmap.getWidth() != TerrainBlock::LightmapSize) ||
      (bitmap.getHeight() != TerrainBlock::LightmapSize) ||
      (bitmap.getFormat() != GBitmap::RGB))
      return(false);

   mLightmap = new U16[TerrainBlock::LightmapSize * TerrainBlock::LightmapSize];
   dMemset(mLightmap, 0, sizeof(U16) * TerrainBlock::LightmapSize * TerrainBlock::LightmapSize);

   // copy the bitmap: bits should already be clamped
   U8 * bp = bitmap.getAddress(0,0);
   for(U32 i = 0; i < TerrainBlock::LightmapSize * TerrainBlock::LightmapSize; i++)
   {
      mLightmap[i] = (U16(bp[0]) << 11) | (U16(bp[1]) << 6) | (U16(bp[2]) << 1) | 1;
      bp += 3;
   }

   return(true);
}

bool SceneLighting::PersistInfo::TerrainChunk::write(Stream & stream)
{
   if(!Parent::write(stream))
      return(false);

   if(!mLightmap)
      return(false);

   // write out an RGB bitmap so it can be PNG compressed
   GBitmap bitmap(TerrainBlock::LightmapSize, TerrainBlock::LightmapSize, false, GBitmap::RGB);
   U8 * bp = bitmap.getAddress(0,0);

   for(U32 i = 0; i < TerrainBlock::LightmapSize * TerrainBlock::LightmapSize; i++)
   {
      bp[0] = mLightmap[i] >> 11;
      bp[1] = (mLightmap[i] >> 6) & 0x1f;
      bp[2] = (mLightmap[i] >> 1) & 0x1f;
      bp += 3;
   }

   if(!bitmap.writePNG(stream))
      return(false);

   return(true);
}

//------------------------------------------------------------------------------
bool SceneLighting::verifyMissionInfo(SceneLighting::PersistInfo::PersistChunk * chunk)
{
   SceneLighting::PersistInfo::MissionChunk * info = dynamic_cast<SceneLighting::PersistInfo::MissionChunk*>(chunk);
   if(!info)
      return(false);

   SceneLighting::PersistInfo::MissionChunk curInfo;
   if(!getMissionInfo(&curInfo))
      return(false);

   return(curInfo.mChunkCRC == info->mChunkCRC);
}

bool SceneLighting::getMissionInfo(SceneLighting::PersistInfo::PersistChunk * chunk)
{
   SceneLighting::PersistInfo::MissionChunk * info = dynamic_cast<SceneLighting::PersistInfo::MissionChunk*>(chunk);
   if(!info)
      return(false);

   info->mChunkCRC = gConnectionMissionCRC ^ PersistInfo::smFileVersion;
   return(true);
}

//------------------------------------------------------------------------------
bool SceneLighting::loadPersistInfo(const char * fileName)
{
   // open the file
   Stream * stream = 0;
   stream = ResourceManager->openStream(fileName);
   if(!stream)
      return(false);

   PersistInfo persistInfo;
   bool success = persistInfo.read(*stream);
   delete stream;
   if(!success)
      return(false);

   // verify the mission chunk
   if(!verifyMissionInfo(persistInfo.mChunks[0]))
      return(false);

   if(mSceneObjects.size() != (persistInfo.mChunks.size() - 1))
      return(false);

   Vector<SceneLighting::PersistInfo::PersistChunk*> chunks;

   // ensure that the scene objects are in the same order as the chunks
   //  - different instances will depend on this
   U32 i;
   for(i = 0; i < mSceneObjects.size(); i++)
   {
      // 0th chunk is the mission chunk
      U32 chunkIdx = i+1;
      if(chunkIdx >= persistInfo.mChunks.size())
         return(false);

      if(!mSceneObjects[i]->isValidChunk(persistInfo.mChunks[chunkIdx]))
         return(false);
      chunks.push_back(persistInfo.mChunks[chunkIdx]);
   }

   // get the objects to load in the persisted chunks
   for(i = 0; i < mSceneObjects.size(); i++)
      if(!mSceneObjects[i]->setPersistInfo(chunks[i]))
         return(false);

   return(true);
}

bool SceneLighting::savePersistInfo(const char * fileName)
{
   // open the file
   FileStream file;
   if(!ResourceManager->openFileForWrite(file, fileName))
      return(false);

   PersistInfo persistInfo;

   // add in the mission chunk
   persistInfo.mChunks.push_back(new SceneLighting::PersistInfo::MissionChunk);

   // get the mission info, will return false when there are 0 lights
   if(!getMissionInfo(persistInfo.mChunks[0]))
      return(false);

   // get all the persist chunks
   for(U32 i = 0; i < mSceneObjects.size(); i++)
   {
      if(isInterior(mSceneObjects[i]->mObj))
         persistInfo.mChunks.push_back(new SceneLighting::PersistInfo::InteriorChunk);
      else if(isTerrain(mSceneObjects[i]->mObj))
         persistInfo.mChunks.push_back(new SceneLighting::PersistInfo::TerrainChunk);
      else
         return(false);

      if(!mSceneObjects[i]->getPersistInfo(persistInfo.mChunks.last()))
         return(false);
   }

   if(!persistInfo.write(file))
      return(false);

   file.close();

   // open/close the stream to get the fileSize calculated on the resource object
   ResourceManager->closeStream(ResourceManager->openStream(fileName));
   return(true);
}

//------------------------------------------------------------------------------
// SceneLighting
//------------------------------------------------------------------------------

void SceneLighting::addInterior(ShadowVolumeBSP * shadowVolume, InteriorProxy & interior, LightInfo * light, S32 level)
{
   if(light->mType != LightInfo::Vector)
      return;

   bool shadowedTree = true;

   // check if just getting shadow detail
   if(level == SHADOW_DETAIL)
   {
      shadowedTree = false;
      level = interior->mInteriorRes->getNumDetailLevels() - 1;
   }

   Interior * detail = interior->mInteriorRes->getDetailLevel(level);
   bool hasAlarm = detail->hasAlarmState();

   // make sure surfaces do not get processed more than once
   BitVector surfaceProcessed;
   surfaceProcessed.setSize(detail->mSurfaces.size());
   surfaceProcessed.clear();

   // go through the zones of the interior and grab outside visible surfaces
   for(U32 i = 0; i < detail->getNumZones(); i++)
   {
      Interior::Zone & zone = detail->mZones[i];
      for(U32 j = 0; j < zone.surfaceCount; j++)
      {
         U32 surfaceIndex = detail->mZoneSurfaces[zone.surfaceStart + j];

         // dont reprocess a surface
         if(surfaceProcessed.test(surfaceIndex))
            continue;
         surfaceProcessed.set(surfaceIndex);

         Interior::Surface & surface = detail->mSurfaces[surfaceIndex];

         // outside visible?
         if(!(surface.surfaceFlags & Interior::SurfaceOutsideVisible))
            continue;

         // good surface?
         PlaneF plane = detail->getPlane(surface.planeIndex);
         if(Interior::planeIsFlipped(surface.planeIndex))
            plane.neg();

         // project the plane
         PlaneF projPlane;
         mTransformPlane(interior->getTransform(), interior->getScale(), plane, &projPlane);

         // fill with ambient? (need to do here, because surface will not be
         // added to the SVBSP tree)
         F32 dot = mDot(projPlane, light->mDirection);
         if(dot > -gParellelVectorThresh)
         {
            if(shadowedTree)
            {
               // alarm lighting
               TextureHandle * normHandle = gInteriorLMManager.duplicateBaseLightmap(detail->getLMHandle(), interior->getLMHandle(), detail->getNormalLMapIndex(surfaceIndex));
               TextureHandle * alarmHandle = 0;

               GBitmap * normLightmap = normHandle->getBitmap();
               GBitmap * alarmLightmap = 0;

              // check if they share the lightmap
               if(hasAlarm)
               {
                  if(detail->getNormalLMapIndex(surfaceIndex) != detail->getAlarmLMapIndex(surfaceIndex))
                  {
                     alarmHandle = gInteriorLMManager.duplicateBaseLightmap(detail->getLMHandle(), interior->getLMHandle(), detail->getAlarmLMapIndex(surfaceIndex));
                     alarmLightmap = alarmHandle->getBitmap();
                  }
               }


               //
               // Support for interior light map border sizes.
               //
               U32 xlen, ylen, xoff, yoff;
               U32 lmborder = detail->getLightMapBorderSize();
               xlen = surface.mapSizeX + (lmborder * 2);
               ylen = surface.mapSizeY + (lmborder * 2);
               xoff = surface.mapOffsetX - lmborder;
               yoff = surface.mapOffsetY - lmborder;


               // attempt to light normal and alarm lighting
               for(U32 c = 0; c < 2; c++)
               {
                  GBitmap * lightmap = (c == 0) ? normLightmap : alarmLightmap;
                  if(!lightmap)
                     continue;

                  // fill it
                  for(U32 y = 0; y < ylen; y++)
                  {
                     ColorI color = light->mAmbient;
                     U8 * pBits = lightmap->getAddress(xoff, yoff + y);
                     for(U32 x = 0; x < xlen; x++)
                     {
#ifdef SET_COLORS
                        *pBits++ = color.red;
                        *pBits++ = color.green;
                        *pBits++ = color.blue;
#else
                        // the previous *pBit++ = ... code is broken.
                        U32 _r = static_cast<U32>( color.red ) + static_cast<U32>( *pBits );
                        *pBits = ( _r <= 255 ) ? _r : 255;
                        pBits++;

                        U32 _g = static_cast<U32>( color.green ) + static_cast<U32>( *pBits );
                        *pBits = ( _g <= 255 ) ? _g : 255;
                        pBits++;

                        U32 _b = static_cast<U32>( color.blue ) + static_cast<U32>( *pBits );
                        *pBits = ( _b <= 255 ) ? _b : 255;
                        pBits++;
#endif
                     }
                  }
               }
            }
            continue;
         }

         ShadowVolumeBSP::SVPoly * poly = buildInteriorPoly(shadowVolume, interior, detail,
            surfaceIndex, light, shadowedTree);

         // insert it into the SVBSP tree
         shadowVolume->insertPoly(poly);
      }
   }
}

//------------------------------------------------------------------------------
ShadowVolumeBSP::SVPoly * SceneLighting::buildInteriorPoly(ShadowVolumeBSP * shadowVolumeBSP,
   InteriorProxy & interior, Interior * detail, U32 surfaceIndex, LightInfo * light,
   bool createSurfaceInfo)
{
   // transform and add the points...
   const MatrixF & transform = interior->getTransform();
   const VectorF & scale = interior->getScale();

   const Interior::Surface & surface = detail->mSurfaces[surfaceIndex];

   ShadowVolumeBSP::SVPoly * poly = shadowVolumeBSP->createPoly();

   poly->mWindingCount = surface.windingCount;

   // project these points
   for(U32 j = 0; j < poly->mWindingCount; j++)
   {
      Point3F iPnt = detail->mPoints[detail->mWindings[surface.windingStart + j]].point;
      Point3F tPnt;
      iPnt.convolve(scale);
      transform.mulP(iPnt, &tPnt);
      poly->mWinding[j] = tPnt;
   }

   // convert from fan
   U32 tmpIndices[ShadowVolumeBSP::SVPoly::MaxWinding];
   Point3F fanIndices[ShadowVolumeBSP::SVPoly::MaxWinding];

   tmpIndices[0] = 0;

   U32 idx = 1;
   U32 i;
   for(i = 1; i < poly->mWindingCount; i += 2)
      tmpIndices[idx++] = i;
   for(i = ((poly->mWindingCount - 1) & (~0x1)); i > 0; i -= 2)
      tmpIndices[idx++] = i;

   idx = 0;
   for(i = 0; i < poly->mWindingCount; i++)
      if(surface.fanMask & (1 << i))
         fanIndices[idx++] = poly->mWinding[tmpIndices[i]];

   // set the data
   poly->mWindingCount = idx;
   for(i = 0; i < poly->mWindingCount; i++)
      poly->mWinding[i] = fanIndices[i];

   // flip the plane - shadow volumes face inwards
   PlaneF plane = detail->getPlane(surface.planeIndex);
   if(!Interior::planeIsFlipped(surface.planeIndex))
      plane.neg();

   // transform the plane
   mTransformPlane(transform, scale, plane, &poly->mPlane);
   shadowVolumeBSP->buildPolyVolume(poly, light);

   // do surface info?
   if(createSurfaceInfo)
   {
      ShadowVolumeBSP::SurfaceInfo * surfaceInfo = new ShadowVolumeBSP::SurfaceInfo;
      shadowVolumeBSP->mSurfaces.push_back(surfaceInfo);

      // fill it
      surfaceInfo->mSurfaceIndex = surfaceIndex;
      surfaceInfo->mShadowVolume = shadowVolumeBSP->getShadowVolume(poly->mShadowVolume);

      // POLY and POLY node gets it too
      ShadowVolumeBSP::SVNode * traverse = shadowVolumeBSP->getShadowVolume(poly->mShadowVolume);
      while(traverse->mFront)
      {
         traverse->mSurfaceInfo = surfaceInfo;
         traverse = traverse->mFront;
      }

      // get some info from the poly node
      poly->mSurfaceInfo = traverse->mSurfaceInfo = surfaceInfo;
      surfaceInfo->mPlaneIndex = traverse->mPlaneIndex;
   }

   return(poly);
}

//--------------------------------------------------------------------------
static S32 QSORT_CALLBACK compareS32(const void * a, const void * b)
{
   return(*((S32 *)a) - *((S32 *)b));
}

U32 SceneLighting::calcMissionCRC()
{
   // all the objects + mission chunk
   Vector<U32> crc;

   // grab the object crcs
   for(U32 i = 0; i < mSceneObjects.size(); i++)
      crc.push_back(mSceneObjects[i]->mChunkCRC);

   // grab the missions crc
   SceneLighting::PersistInfo::MissionChunk curInfo;
   getMissionInfo(&curInfo);
   crc.push_back(curInfo.mChunkCRC);

   // sort them (order may not have been preserved)
   dQsort(crc.address(), crc.size(), sizeof(U32), compareS32);

   return(calculateCRC(crc.address(), sizeof(U32) * crc.size(), 0xffffffff));
}

bool SceneLighting::light(BitSet32 flags)
{
   if(!gClientSceneGraph)
      return(false);

   mStartTime = Platform::getRealMilliseconds();

   // register static lights
   LightManager * lManager = gClientSceneGraph->getLightManager();
   lManager->registerLights(true);

   // grab all the lights
   mLights.clear();
   lManager->getLights(mLights);
   if(!mLights.size())
      return(false);

   // get all the objects and create proxy's for them
   Vector<SceneObject *>   objects;
   gClientContainer.findObjects(InteriorObjectType | TerrainObjectType, findObjectsCallback, &objects);

   for(SceneObject ** itr = objects.begin(); itr != objects.end(); itr++)
   {
      ObjectProxy * proxy;
      if(isInterior(*itr))
      {
         // Reset the dynamic lighting flag where appropriate.
         InteriorInstance *ii = dynamic_cast<InteriorInstance*>(*itr);
         ii->mDoSimpleDynamicRender = ii->mUseGLLighting;

         proxy = new InteriorProxy(*itr);
      }
      else if(isTerrain(*itr))
         proxy = new TerrainProxy(*itr);
      else
      {
         AssertFatal(0, "SceneLighting:: invalid object returned from container search");
         continue;
      }

      if(!proxy->calcValidation())
      {
         Con::errorf(ConsoleLogEntry::General, "Failed to calculate validation info for object.  Skipped.");
         delete proxy;
         continue;
      }

      if(!proxy->loadResources())
      {
         Con::errorf(ConsoleLogEntry::General, "Failed to load resources for object.  Skipped.");
         delete proxy;
         continue;
      }

      mSceneObjects.push_back(proxy);
   }

   if(!mSceneObjects.size())
      return(false);

   // grab the missions crc
   U32 missionCRC = calcMissionCRC();

   // remove the '.mis' extension from the mission name
   char misName[256];
   dSprintf(misName, sizeof(misName), "%s", Con::getVariable("$Client::MissionFile"));
   char * dot = dStrstr((const char*)misName, ".mis");
   if(dot)
      *dot = '\0';

   // get the mission name
   dSprintf(mFileName, sizeof(mFileName), "%s_%x.ml", misName, missionCRC);
   if(!ResourceManager->isValidWriteFileName(mFileName))
   {
      Con::warnf("Invalid filename '%s'.  Failed to light mission.", mFileName);
      return(false);
   }

   // check for some persisted data, check if being forced..
   if(!flags.test(ForceAlways|ForceWritable))
   {
      if(loadPersistInfo(mFileName))
      {
         Con::printf(" Successfully loaded mission lighting file: '%s'", mFileName);

         // touch this file...
         if(!dFileTouch(mFileName))
            Con::warnf("  Failed to touch file '%s'.  File may be read only.", mFileName);

         return(false);
      }

      // texture manager must have lighting complete now
      if(flags.test(LoadOnly))
      {
         Con::errorf(ConsoleLogEntry::General, "Failed to load mission lighting!");
         return(false);
      }
   }

   // don't light if file is read-only?
   if(!flags.test(ForceAlways))
   {
      FileStream fileStream;
      if(!ResourceManager->openFileForWrite(fileStream, mFileName))
      {
         Con::errorf(ConsoleLogEntry::General, "SceneLighting::Light: Failed to light mission.  File '%s' cannot be written to.", mFileName);
         return(false);
      }
   }

   // initialize the objects for lighting
   for(ObjectProxy ** proxyItr = mSceneObjects.begin(); proxyItr != mSceneObjects.end(); proxyItr++)
      (*proxyItr)->init();

   // get things started
   Sim::postEvent(this, new SceneLightingProcessEvent(0, -1), Sim::getTargetTime() + 1);
   return(true);
}

//------------------------------------------------------------------------------
void SceneLighting::processEvent(U32 light, S32 object)
{
   // cancel lighting?
   if(gTerminateLighting)
   {
      completed(false);
      deleteObject();
      return;
   }

   ObjectProxy ** proxyItr;

   // last object?
   if(object == mLitObjects.size())
   {
      for(proxyItr = mLitObjects.begin(); proxyItr != mLitObjects.end(); proxyItr++)
      {
         if(!(*proxyItr)->getObject())
         {
            AssertFatal(0, "SceneLighting:: missing sceneobject on light end");
            continue;
         }
         (*proxyItr)->postLight(light == (mLights.size() - 1));
      }
      mLitObjects.clear();

      Canvas->paint();
      Sim::postEvent(this, new SceneLightingProcessEvent(light + 1, -1), Sim::getTargetTime() + 1);
   }
   else
   {
      // done lighting?
      if(light == mLights.size())
      {
         Con::printf(" Scene lit in %3.3f seconds", (Platform::getRealMilliseconds()-mStartTime)/1000.f);

         // save out the lighting?
         if(Con::getBoolVariable("$pref::sceneLighting::cacheLighting", true))
         {
            if(!savePersistInfo(mFileName))
               Con::errorf(ConsoleLogEntry::General, "SceneLighting::light: unable to persist lighting!");
            else
               Con::printf(" Successfully saved mission lighting file: '%s'", mFileName);
         }

         // wrap things up...
         completed(true);
         deleteObject();
      }
      else
      {
         // start of this light?
         if(object == -1)
         {
            for(proxyItr = mSceneObjects.begin(); proxyItr != mSceneObjects.end(); proxyItr++)
            {
               if(!(*proxyItr)->getObject())
               {
                  AssertFatal(0, "SceneLighting:: missing sceneobject on light start");
                  Con::errorf(ConsoleLogEntry::General, "SceneLighting:: missing sceneobject on light start");
                  continue;
               }
               if((*proxyItr)->preLight(mLights[light]))
                  mLitObjects.push_back(*proxyItr);
            }
         }
         else
         {
            if(mLitObjects[object]->getObject())
            {
               gLightingProgress = (F32(light) / F32(mLights.size())) + ((F32(object + 1) / F32(mLitObjects.size())) / F32(mLights.size()));
               mLitObjects[object]->light(mLights[light]);
            }
            else
            {
               AssertFatal(0, "SceneLighting:: missing sceneobject on light update");
               Con::errorf(ConsoleLogEntry::General, "SceneLighting:: missing sceneobject on light update");
            }
         }

         Canvas->paint();
         Sim::postEvent(this, new SceneLightingProcessEvent(light, object + 1), Sim::getTargetTime() + 1);
      }
   }
}

//------------------------------------------------------------------------------

struct CacheEntry {
   ResourceObject *  mFileObject;
   const char *      mFileName;

   CacheEntry() {
      mFileObject = 0;
      mFileName = 0;
   };
};

// object list sort methods: want list in reverse
static int QSORT_CALLBACK minSizeSort(const void * p1, const void * p2)
{
   const CacheEntry * entry1 = (const CacheEntry *)p1;
   const CacheEntry * entry2 = (const CacheEntry *)p2;

   return(entry2->mFileObject->fileSize - entry1->mFileObject->fileSize);
}

static int QSORT_CALLBACK maxSizeSort(const void * p1, const void * p2)
{
   const CacheEntry * entry1 = (const CacheEntry *)p1;
   const CacheEntry * entry2 = (const CacheEntry *)p2;

   return(entry1->mFileObject->fileSize - entry2->mFileObject->fileSize);
}

static int QSORT_CALLBACK lastCreatedSort(const void * p1, const void * p2)
{
   const CacheEntry * entry1 = (const CacheEntry *)p1;
   const CacheEntry * entry2 = (const CacheEntry *)p2;

   FileTime create[2];
   FileTime modify;

   bool ret[2];

   ret[0] = Platform::getFileTimes(entry1->mFileName, &create[0], &modify);
   ret[1] = Platform::getFileTimes(entry2->mFileName, &create[1], &modify);

   // check return values
   if(!ret[0] && !ret[1])
      return(0);
   if(!ret[0])
      return(1);
   if(!ret[1])
      return(-1);

   return(Platform::compareFileTimes(create[1], create[0]));
}

static int QSORT_CALLBACK lastModifiedSort(const void * p1, const void * p2)
{
   const CacheEntry * entry1 = (const CacheEntry *)p1;
   const CacheEntry * entry2 = (const CacheEntry *)p2;

   FileTime create;
   FileTime modify[2];

   bool ret[2];

   ret[0] = Platform::getFileTimes(entry1->mFileName, &create, &modify[0]);
   ret[1] = Platform::getFileTimes(entry2->mFileName, &create, &modify[1]);

   // check return values
   if(!ret[0] && !ret[1])
      return(0);
   if(!ret[0])
      return(1);
   if(!ret[1])
      return(-1);

   return(Platform::compareFileTimes(modify[1], modify[0]));
}

void SceneLighting::processCache()
{
   // get size in kb
   S32 quota = Con::getIntVariable("$pref::sceneLighting::cacheSize", -1);

   Vector<CacheEntry> files;

   ResourceObject * match = 0;
   const char * name;

   S32 curCacheSize = 0;
   match = ResourceManager->findMatch("*.ml", &name, 0);
   while(match)
   {
      if(match->flags & ResourceObject::File)
      {
         // dont allow the current file to be removed...
         if(!dStrstr(name, mFileName))
         {
            CacheEntry entry;
            entry.mFileObject = match;

            // get out of vfs...
            char fileName[1024];
            dSprintf(fileName, sizeof(fileName), "%s/%s", match->path, match->name);

            entry.mFileName = StringTable->insert(fileName);
            files.push_back(entry);
         }
         else
            curCacheSize += match->fileSize;
      }

      match = ResourceManager->findMatch("*.ml", &name, match);
   }

   // remove old files
   for(S32 i = files.size() - 1; i >= 0; i--)
   {
      char buf[1024];
      dSprintf(buf, sizeof(buf), "%s/%s", files[i].mFileObject->path, files[i].mFileObject->name);

      Stream * stream = ResourceManager->openStream(buf);
      if(!stream)
         continue;

      // read in the version
      U32 version;
      bool ok = (stream->read(&version) && (version == SceneLighting::PersistInfo::smFileVersion));
      ResourceManager->closeStream(stream);

      // ok?
      if(ok)
         continue;

      // delete the file
      ResourceManager->freeResource(files[i].mFileObject);

      // no sneaky names
      if(!dStrstr(files[i].mFileName, ".."))
      {
         Con::warnf("Removing old lighting file '%s'.", files[i].mFileName);
         dFileDelete(files[i].mFileName);
      }

      files.pop_back();
   }

   // no size restriction?
   if(quota == -1 || !files.size())
      return;

   for(U32 i = 0; i < files.size(); i++)
      curCacheSize += files[i].mFileObject->fileSize;

   // need to remove?
   if(quota > (curCacheSize >> 10))
      return;

   // sort the entries by the correct method
   const char * purgeMethod = Con::getVariable("$pref::sceneLighting::purgeMethod");
   if(!purgeMethod)
      purgeMethod = "";

   // determine the method (default to least recently used)
   if(!dStricmp(purgeMethod, "minSize"))
      dQsort(files.address(), files.size(), sizeof(CacheEntry), minSizeSort);
   else if(!dStricmp(purgeMethod, "maxSize"))
      dQsort(files.address(), files.size(), sizeof(CacheEntry), maxSizeSort);
   else if(!dStricmp(purgeMethod, "lastCreated"))
      dQsort(files.address(), files.size(), sizeof(CacheEntry), lastCreatedSort);
   else
      dQsort(files.address(), files.size(), sizeof(CacheEntry), lastModifiedSort);

   // go through and remove the best candidate first (sorted reverse)
   while(((curCacheSize >> 10) > quota) && files.size())
   {
      curCacheSize -= files.last().mFileObject->fileSize;
      ResourceManager->freeResource(files.last().mFileObject);

      // no sneaky names
      if(!dStrstr(files.last().mFileName, ".."))
      {
         Con::warnf("Removing lighting file '%s'.", files.last().mFileName);
         dFileDelete(files.last().mFileName);
      }

      files.pop_back();
   }
}

//------------------------------------------------------------------------------
// Class SceneLighting::ObjectProxy:
//------------------------------------------------------------------------------
bool SceneLighting::ObjectProxy::calcValidation()
{
   mChunkCRC = getResourceCRC();
   if(!mChunkCRC)
      return(false);

   return(true);
}

bool SceneLighting::ObjectProxy::isValidChunk(PersistInfo::PersistChunk * chunk)
{
   return(chunk->mChunkCRC == mChunkCRC);
}

bool SceneLighting::ObjectProxy::getPersistInfo(PersistInfo::PersistChunk * chunk)
{
   chunk->mChunkCRC = mChunkCRC;
   return(true);
}

bool SceneLighting::ObjectProxy::setPersistInfo(PersistInfo::PersistChunk * chunk)
{
   mChunkCRC = chunk->mChunkCRC;
   return(true);
}

//------------------------------------------------------------------------------
// Class SceneLighting::InteriorProxy:
//------------------------------------------------------------------------------
SceneLighting::InteriorProxy::InteriorProxy(SceneObject * obj) :
   Parent(obj)
{
   mBoxShadowBSP = 0;
}

SceneLighting::InteriorProxy::~InteriorProxy()
{
   delete mBoxShadowBSP;
}

bool SceneLighting::InteriorProxy::loadResources()
{
   InteriorInstance * interior = getObject();
   if(!interior)
      return(false);

   Resource<InteriorResource> & interiorRes = interior->getResource();
   if(!bool(interiorRes))
      return(false);

   if(!gInteriorLMManager.loadBaseLightmaps(interiorRes->getDetailLevel(0)->getLMHandle(),
      interior->getLMHandle()))
      return(false);

   return(true);
}

void SceneLighting::InteriorProxy::init()
{
   InteriorInstance * interior = getObject();
   if(!interior)
      return;

   // clear out the lightmaps
   for(U32 i = 0; i < interior->getResource()->getNumDetailLevels(); i++)
   {
      Interior * detail = interior->getResource()->getDetailLevel(i);
      gInteriorLMManager.clearLightmaps(detail->getLMHandle(), interior->getLMHandle());
   }
}

bool SceneLighting::InteriorProxy::preLight(LightInfo * light)
{
   // create shadow volume of the bounding box of this object
   InteriorInstance * interior = getObject();
   if(!interior)
      return(false);

   if(light->mType != LightInfo::Vector)
      return(false);

   // reset
   mLitBoxSurfaces.clear();

   const Box3F & objBox = interior->getObjBox();
   const MatrixF & objTransform = interior->getTransform();
   const VectorF & objScale = interior->getScale();

   // grab the surfaces which form the shadow volume
   U32 numPlanes = 0;
   PlaneF testPlanes[3];
   U32 planeIndices[3];

   // grab the bounding planes which face the light
   U32 i;
   for(i = 0; (i < 6) && (numPlanes < 3); i++)
   {
      PlaneF plane;
      plane.x = BoxNormals[i].x;
      plane.y = BoxNormals[i].y;
      plane.z = BoxNormals[i].z;

      if(i&1)
         plane.d = (((const float*)objBox.min)[(i-1)>>1]);
      else
         plane.d = -(((const float*)objBox.max)[i>>1]);

      // project
      mTransformPlane(objTransform, objScale, plane, &testPlanes[numPlanes]);

      planeIndices[numPlanes] = i;

      if(mDot(testPlanes[numPlanes], light->mDirection) < gParellelVectorThresh)
         numPlanes++;
   }
   AssertFatal(numPlanes, "SceneLighting::InteriorProxy::preLight: no planes found");

   // project the points
   Point3F projPnts[8];
   for(i = 0; i < 8; i++)
   {
      Point3F pnt;
      pnt.set(BoxPnts[i].x ? objBox.max.x : objBox.min.x,
              BoxPnts[i].y ? objBox.max.y : objBox.min.y,
              BoxPnts[i].z ? objBox.max.z : objBox.min.z);

      // scale it
      pnt.convolve(objScale);
      objTransform.mulP(pnt, &projPnts[i]);
   }

   mBoxShadowBSP = new ShadowVolumeBSP;

   // insert the shadow volumes for the surfaces
   for(i = 0; i < numPlanes; i++)
   {
      ShadowVolumeBSP::SVPoly * poly = mBoxShadowBSP->createPoly();
      poly->mWindingCount = 4;

      U32 j;
      for(j = 0; j < 4; j++)
         poly->mWinding[j] = projPnts[BoxVerts[planeIndices[i]][j]];

      testPlanes[i].neg();
      poly->mPlane = testPlanes[i];

      mBoxShadowBSP->buildPolyVolume(poly, light);
      mLitBoxSurfaces.push_back(mBoxShadowBSP->copyPoly(poly));
      mBoxShadowBSP->insertPoly(poly);

      // create the opposite planes for testing against terrain
      Point3F pnts[3];
      for(j = 0; j < 3; j++)
         pnts[j] = projPnts[BoxVerts[planeIndices[i]^1][j]];
      PlaneF plane(pnts[2], pnts[1], pnts[0]);
      mOppositeBoxPlanes.push_back(plane);
   }

   // grab the unique planes for terrain checks
   for(i = 0; i < numPlanes; i++)
   {
      U32 mask = 0;
      for(U32 j = 0; j < numPlanes; j++)
         mask |= BoxSharedEdgeMask[planeIndices[i]][planeIndices[j]];

      ShadowVolumeBSP::SVNode * traverse = mBoxShadowBSP->getShadowVolume(mLitBoxSurfaces[i]->mShadowVolume);
      while(traverse->mFront)
      {
         if(!(mask & 1))
            mTerrainTestPlanes.push_back(mBoxShadowBSP->getPlane(traverse->mPlaneIndex));

         mask >>= 1;
         traverse = traverse->mFront;
      }
   }

   // there will be 2 duplicate node planes if there were only 2 planes lit
   if(numPlanes == 2)
   {
      for(S32 i = 0; i < mTerrainTestPlanes.size(); i++)
         for(U32 j = 0; j < mTerrainTestPlanes.size(); j++)
         {
            if(i == j)
               continue;

            if((mDot(mTerrainTestPlanes[i], mTerrainTestPlanes[j]) > gPlaneNormThresh) &&
               (mFabs(mTerrainTestPlanes[i].d - mTerrainTestPlanes[j].d) < gPlaneDistThresh))
            {
               mTerrainTestPlanes.erase(i);
               i--;
               break;
            }
         }
   }

   return(true);
}

bool SceneLighting::InteriorProxy::isShadowedBy(InteriorProxy * test)
{
   // add if overlapping world box
   if((*this)->getWorldBox().isOverlapped((*test)->getWorldBox()))
      return(true);

   // test the box shadow volume
   for(U32 i = 0; i < mLitBoxSurfaces.size(); i++)
   {
      ShadowVolumeBSP::SVPoly * poly = mBoxShadowBSP->copyPoly(mLitBoxSurfaces[i]);
      if(test->mBoxShadowBSP->testPoly(poly))
         return(true);
   }

   return(false);
}

void SceneLighting::InteriorProxy::light(LightInfo * light)
{
   InteriorInstance * interior = getObject();
   if(!interior)
      return;

   S32 time = Platform::getRealMilliseconds();

   // create own shadow volume
   ShadowVolumeBSP shadowVolume;

   // add the other objects lit surfaces into shadow volume
   for(ObjectProxy ** itr = gLighting->mLitObjects.begin(); itr != gLighting->mLitObjects.end(); itr++)
   {
      if(!(*itr)->getObject())
         continue;

      if(gLighting->isInterior((*itr)->mObj))
      {
         if(*itr == this)
            continue;

         if(isShadowedBy(static_cast<InteriorProxy*>(*itr)))
            gLighting->addInterior(&shadowVolume, *static_cast<InteriorProxy*>(*itr), light, SceneLighting::SHADOW_DETAIL);
      }

      // insert the terrain squares
      if(gLighting->isTerrain((*itr)->mObj))
      {
         TerrainProxy * terrain = static_cast<TerrainProxy*>(*itr);

         Vector<PlaneF> clipPlanes;
         clipPlanes = mTerrainTestPlanes;
         for(U32 i = 0; i < mOppositeBoxPlanes.size(); i++)
            clipPlanes.push_back(mOppositeBoxPlanes[i]);

         Vector<U16> shadowList;
         if(terrain->getShadowedSquares(clipPlanes, shadowList))
         {
            TerrainBlock * block = static_cast<TerrainBlock*>((*itr)->getObject());
            Point3F offset;
            block->getTransform().getColumn(3, &offset);

            F32 squareSize = block->getSquareSize();

            for(U32 j = 0; j < shadowList.size(); j++)
            {
               Point2I pos(shadowList[j] & TerrainBlock::BlockMask, shadowList[j] >> TerrainBlock::BlockShift);
               Point2F wPos(pos.x * squareSize + offset.x,
                            pos.y * squareSize + offset.y);

               Point3F pnts[4];
               pnts[0].set(wPos.x, wPos.y, fixedToFloat(block->getHeight(pos.x, pos.y)));
               pnts[1].set(wPos.x + squareSize, wPos.y, fixedToFloat(block->getHeight(pos.x + 1, pos.y)));
               pnts[2].set(wPos.x + squareSize, wPos.y + squareSize, fixedToFloat(block->getHeight(pos.x + 1, pos.y + 1)));
               pnts[3].set(wPos.x, wPos.y + squareSize, fixedToFloat(block->getHeight(pos.x, pos.y + 1)));

               GridSquare * gs = block->findSquare(0, pos);

               U32 squareIdx = (gs->flags & GridSquare::Split45) ? 0 : 2;

               for(U32 k = squareIdx; k < (squareIdx + 2); k++)
               {
                  // face plane inwards
                  PlaneF plane(pnts[TerrainSquareIndices[k][2]],
                               pnts[TerrainSquareIndices[k][1]],
                               pnts[TerrainSquareIndices[k][0]]);

                  if(mDot(plane, light->mDirection) > gParellelVectorThresh)
                  {
                     ShadowVolumeBSP::SVPoly * poly = shadowVolume.createPoly();
                     poly->mWindingCount = 3;

                     poly->mWinding[0] = pnts[TerrainSquareIndices[k][0]];
                     poly->mWinding[1] = pnts[TerrainSquareIndices[k][1]];
                     poly->mWinding[2] = pnts[TerrainSquareIndices[k][2]];
                     poly->mPlane = plane;

                     // create the shadow volume for this and insert
                     shadowVolume.buildPolyVolume(poly, light);
                     shadowVolume.insertPoly(poly);
                  }
               }
            }
         }
      }
   }

   // light all details
   for(U32 i = 0; i < interior->getResource()->getNumDetailLevels(); i++)
   {
      // clear lightmaps
      Interior * detail = interior->getResource()->getDetailLevel(i);
      gInteriorLMManager.clearLightmaps(detail->getLMHandle(), interior->getLMHandle());

      // clear out the last inserted interior
      shadowVolume.removeLastInterior();

      bool hasAlarm = detail->hasAlarmState();

      gLighting->addInterior(&shadowVolume, *this, light, i);

      for(U32 j = 0; j < shadowVolume.mSurfaces.size(); j++)
      {
         ShadowVolumeBSP::SurfaceInfo * surfaceInfo = shadowVolume.mSurfaces[j];

         U32 surfaceIndex = surfaceInfo->mSurfaceIndex;

         const Interior::Surface & surface = detail->getSurface(surfaceIndex);

         // alarm lighting
         TextureHandle * normHandle = gInteriorLMManager.duplicateBaseLightmap(detail->getLMHandle(), interior->getLMHandle(), detail->getNormalLMapIndex(surfaceIndex));
         TextureHandle * alarmHandle = 0;

         GBitmap * normLightmap = normHandle->getBitmap();
         GBitmap * alarmLightmap = 0;

         // check if the lightmaps are shared
         if(hasAlarm)
         {
            if(detail->getNormalLMapIndex(surfaceIndex) != detail->getAlarmLMapIndex(surfaceIndex))
            {
               alarmHandle = gInteriorLMManager.duplicateBaseLightmap(detail->getLMHandle(), interior->getLMHandle(), detail->getAlarmLMapIndex(surfaceIndex));
               alarmLightmap = alarmHandle->getBitmap();
            }
         }

         // points right way?
         PlaneF plane = detail->getPlane(surface.planeIndex);
         if(Interior::planeIsFlipped(surface.planeIndex))
            plane.neg();

         const MatrixF & transform = interior->getTransform();
         const Point3F & scale = interior->getScale();

         //
         PlaneF projPlane;
         mTransformPlane(transform, scale, plane, &projPlane);

         F32 dot = mDot(projPlane, -light->mDirection);

         // shadowed?
         if(!surfaceInfo->mShadowed.size())
         {
            // calc the color and convert to U8 rep
            ColorF tmp = (light->mColor * dot) + light->mAmbient;
            tmp.clamp();
            ColorI color = tmp;

            // attempt to light both the normal and the alarm states
            for(U32 c = 0; c < 2; c++)
            {
               GBitmap * lightmap = (c == 0) ? normLightmap : alarmLightmap;
               if(!lightmap)
                  continue;

               //
               // Support for interior light map border sizes.
               //
               U32 xlen, ylen, xoff, yoff;
               U32 lmborder = detail->getLightMapBorderSize();
               xlen = surface.mapSizeX + (lmborder * 2);
               ylen = surface.mapSizeY + (lmborder * 2);
               xoff = surface.mapOffsetX - lmborder;
               yoff = surface.mapOffsetY - lmborder;


               // fill it
               for(U32 y = 0; y < ylen; y++)
               {
                  U8 * pBits = lightmap->getAddress(xoff, yoff + y);
                  for(U32 x = 0; x < xlen; x++)
                  {
#ifdef SET_COLORS
                     *pBits++ = color.red;
                     *pBits++ = color.green;
                     *pBits++ = color.blue;
#else
                     U32 _r = static_cast<U32>( color.red ) + static_cast<U32>( *pBits );
                     *pBits = ( _r <= 255 ) ? _r : 255;
                     pBits++;

                     U32 _g = static_cast<U32>( color.green ) + static_cast<U32>( *pBits );
                     *pBits = ( _g <= 255 ) ? _g : 255;
                     pBits++;

                     U32 _b = static_cast<U32>( color.blue ) + static_cast<U32>( *pBits );
                     *pBits = ( _b <= 255 ) ? _b : 255;
                     pBits++;
#endif
                  }
               }
            }


            continue;
         }

         //
         // Support for interior light map border sizes.
         //
         U32 xlen, ylen, xoff, yoff;
         U32 lmborder = detail->getLightMapBorderSize();
         xlen = surface.mapSizeX + (lmborder * 2);
         ylen = surface.mapSizeY + (lmborder * 2);
         xoff = surface.mapOffsetX - lmborder;
         yoff = surface.mapOffsetY - lmborder;


         // get the lmapGen...
         const Interior::TexGenPlanes & lmTexGenEQ = detail->getLMTexGenEQ(surfaceIndex);

         const F32 * const lGenX = lmTexGenEQ.planeX;
         const F32 * const lGenY = lmTexGenEQ.planeY;

         AssertFatal((lGenX[0] * lGenX[1] == 0.f) &&
                     (lGenX[0] * lGenX[2] == 0.f) &&
                     (lGenX[1] * lGenX[2] == 0.f), "Bad lmTexGen!");
         AssertFatal((lGenY[0] * lGenY[1] == 0.f) &&
                     (lGenY[0] * lGenY[2] == 0.f) &&
                     (lGenY[1] * lGenY[2] == 0.f), "Bad lmTexGen!");

         // get the axis index for the texgens (could be swapped)
         S32 si;
         S32 ti;
         S32 axis = -1;

         //
         if(lGenX[0] == 0.f && lGenY[0] == 0.f)          // YZ
         {
            axis = 0;
            if(lGenX[1] == 0.f) { // swapped?
               si = 2;
               ti = 1;
            } else {
               si = 1;
               ti = 2;
            }
         }
         else if(lGenX[1] == 0.f && lGenY[1] == 0.f)     // XZ
         {
            axis = 1;
            if(lGenX[0] == 0.f) { // swapped?
               si = 2;
               ti = 0;
            } else {
               si = 0;
               ti = 2;
            }
         }
         else if(lGenX[2] == 0.f && lGenY[2] == 0.f)     // XY
         {
            axis = 2;
            if(lGenX[0] == 0.f) { // swapped?
               si = 1;
               ti = 0;
            } else {
               si = 0;
               ti = 1;
            }
         }
         AssertFatal(!(axis == -1), "SceneLighting::lightInterior: bad TexGen!");

         const F32 * pNormal = ((const F32*)plane);

         Point3F start;
         F32 * pStart = ((F32*)start);


         F32 lumelScale = 1 / (lGenX[si] * normLightmap->getWidth());

         // get the start point on the lightmap
         pStart[si] = (((xoff * lumelScale) / (1 / lGenX[si])) - lGenX[3]) / lGenX[si];
         pStart[ti] = (((yoff * lumelScale) / (1 / lGenY[ti])) - lGenY[3]) / lGenY[ti];
         pStart[axis] = ((pNormal[si] * pStart[si]) + (pNormal[ti] * pStart[ti]) + plane.d) / -pNormal[axis];

         start.convolve(scale);
         transform.mulP(start);

         // get the s/t vecs oriented on the surface
         Point3F sVec;
         Point3F tVec;

         F32 * pSVec = ((F32*)sVec);
         F32 * pTVec = ((F32*)tVec);

         F32 angle;
         Point3F planeNormal;

         // s
         pSVec[si] = 1.f;
         pSVec[ti] = 0.f;

         planeNormal = plane;
         ((F32*)planeNormal)[ti] = 0.f;
         planeNormal.normalize();

         angle = mAcos(mClampF(((F32*)planeNormal)[axis], -1.f, 1.f));
         pSVec[axis] = (((F32*)planeNormal)[si] < 0.f) ? mTan(angle) : -mTan(angle);

         // t
         pTVec[ti] = 1.f;
         pTVec[si] = 0.f;

         planeNormal = plane;
         ((F32*)planeNormal)[si] = 0.f;
         planeNormal.normalize();

         angle = mAcos(mClampF(((F32*)planeNormal)[axis], -1.f, 1.f));
         pTVec[axis] = (((F32*)planeNormal)[ti] < 0.f) ? mTan(angle) : -mTan(angle);

         sVec *= lumelScale;
         tVec *= lumelScale;

         // project vecs
         transform.mulV(sVec);
         sVec.convolve(scale);

         transform.mulV(tVec);
         tVec.convolve(scale);

         Point3F & curPos = start;
         Point3F sRun = sVec * xlen;

         // get the lexel area
         Point3F cross;
         mCross(sVec, tVec, &cross);
         F32 maxLexelArea = cross.len();

         const PlaneF & surfacePlane = shadowVolume.getPlane(surfaceInfo->mPlaneIndex);

         // get the world coordinate for each lexel
         for(U32 y = 0; y < ylen; y++)
         {
            U8 * normBits = normLightmap->getAddress(xoff, yoff + y);
            U8 * alarmBits = alarmLightmap ? alarmLightmap->getAddress(xoff, yoff + y) : 0;

            for(U32 x = 0; x < xlen; x++)
            {
               ShadowVolumeBSP::SVPoly * poly = shadowVolume.createPoly();
               poly->mPlane = surfacePlane;
               poly->mWindingCount = 4;

               // set the poly indices
               poly->mWinding[0] = curPos;
               poly->mWinding[1] = curPos + sVec;
               poly->mWinding[2] = curPos + sVec + tVec;
               poly->mWinding[3] = curPos + tVec;

//               // insert poly which has been clipped to own shadow volume
//               ShadowVolumeBSP::SVPoly * store = 0;
//               shadowVolume.clipToSelf(surfaceInfo->mShadowVolume, &store, poly);
//
//               if(!store)
//                  continue;
//
//               F32 lexelArea = shadowVolume.getPolySurfaceArea(store);
//               F32 area = shadowVolume.getLitSurfaceArea(store, surfaceInfo);

               F32 area = shadowVolume.getLitSurfaceArea(poly, surfaceInfo);
               F32 shadowScale = mClampF(area / maxLexelArea, 0.f, 1.f);

               // get the color into U8
               ColorF tmp = (light->mColor * dot * shadowScale) + light->mAmbient;
               tmp.clamp();
               ColorI color = tmp;

               // attempt to light both normal and alarm lightmaps
               for(U32 c = 0; c < 2; c++)
               {
                  U8* & pBits = (c == 0) ? normBits : alarmBits;
                  if(!pBits)
                     continue;

#ifdef SET_COLORS
                  *pBits++ = color.red;
                  *pBits++ = color.green;
                  *pBits++ = color.blue;
#else
                  U32 _r = static_cast<U32>( color.red ) + static_cast<U32>( *pBits );
                  *pBits = ( _r <= 255 ) ? _r : 255;
                  pBits++;

                  U32 _g = static_cast<U32>( color.green ) + static_cast<U32>( *pBits );
                  *pBits = ( _g <= 255 ) ? _g : 255;
                  pBits++;

                  U32 _b = static_cast<U32>( color.blue ) + static_cast<U32>( *pBits );
                  *pBits = ( _b <= 255 ) ? _b : 255;
                  pBits++;
#endif
               }

               curPos += sVec;
            }

            curPos -= sRun;
            curPos += tVec;
         }
      }
   }

   Con::printf("    = interior lit in %3.3f seconds", (Platform::getRealMilliseconds()-time)/1000.f);
}

void SceneLighting::InteriorProxy::postLight(bool lastLight)
{
   delete mBoxShadowBSP;
   mBoxShadowBSP = 0;

   InteriorInstance * interior = getObject();
   if(!interior)
      return;

   // only rebuild the vertex colors after the last light
   if(lastLight)
      interior->rebuildVertexColors();
}

//------------------------------------------------------------------------------
U32 SceneLighting::InteriorProxy::getResourceCRC()
{
   InteriorInstance * interior = getObject();
   if(!interior)
      return(0);
   return(interior->getCRC());
}

//------------------------------------------------------------------------------
bool SceneLighting::InteriorProxy::setPersistInfo(SceneLighting::PersistInfo::PersistChunk * info)
{
   if(!Parent::setPersistInfo(info))
      return(false);

   SceneLighting::PersistInfo::InteriorChunk * chunk = dynamic_cast<SceneLighting::PersistInfo::InteriorChunk*>(info);
   AssertFatal(chunk, "SceneLighting::InteriorProxy::setPersistInfo: invalid info chunk!");

   InteriorInstance * interior = getObject();
   if(!interior)
      return(false);

   U32 numDetails = interior->getNumDetailLevels();

   // check the lighting method
   AssertFatal(SceneLighting::smUseVertexLighting == Interior::smUseVertexLighting, "SceneLighting::InteriorProxy::setPersistInfo: invalid vertex lighting state");
   if(SceneLighting::smUseVertexLighting != Interior::smUseVertexLighting)
      return(false);

   // process the vertex lighting...
   if(chunk->mDetailVertexCount.size() != numDetails)
      return(false);

   AssertFatal(chunk->mVertexColorsNormal.size(), "SceneLighting::InteriorProxy::setPersistInfo: invalid chunk info");
   AssertFatal(!chunk->mHasAlarmState || chunk->mVertexColorsAlarm.size(), "SceneLighting::InteriorProxy::setPersistInfo: invalid chunk info");
   AssertFatal(!(chunk->mHasAlarmState ^ interior->getDetailLevel(0)->hasAlarmState()), "SceneLighting::InteriorProxy::setPersistInfo: invalid chunk info");

   U32 curPos = 0;
   for(U32 i = 0; i < numDetails; i++)
   {
      U32 count = chunk->mDetailVertexCount[i];
      Vector<ColorI>* normal = interior->getVertexColorsNormal(i);
      Vector<ColorI>* alarm  = interior->getVertexColorsAlarm(i);
      AssertFatal(normal != NULL && alarm != NULL, "Error, bad vectors returned!");

      normal->setSize(count);
      dMemcpy(normal->address(), &chunk->mVertexColorsNormal[curPos], count * sizeof(ColorI));

      if(chunk->mHasAlarmState)
      {
         alarm->setSize(count);
         dMemcpy(alarm->address(), &chunk->mVertexColorsAlarm[curPos], count * sizeof(ColorI));
      }

      curPos += count;
   }

   // need lightmaps?
   if(!SceneLighting::smUseVertexLighting)
   {
      if(chunk->mDetailLightmapCount.size() != numDetails)
         return(false);

      LM_HANDLE instanceHandle = interior->getLMHandle();
      U32 idx = 0;

      for(U32 i = 0; i < numDetails; i++)
      {
         Interior * detail = interior->getDetailLevel(i);

         LM_HANDLE interiorHandle = detail->getLMHandle();
         Vector<TextureHandle*> & baseHandles = gInteriorLMManager.getHandles(interiorHandle, 0);

         if(chunk->mDetailLightmapCount[i] > baseHandles.size())
            return(false);

         for(U32 j = 0; j < chunk->mDetailLightmapCount[i]; j++)
         {
            U32 baseIndex = chunk->mDetailLightmapIndices[idx];
            if(baseIndex >= baseHandles.size())
               return(false);

            AssertFatal(chunk->mLightmaps[idx], "SceneLighting::InteriorProxy::setPersistInfo: bunk bitmap!");
            if(chunk->mLightmaps[idx]->getWidth() != baseHandles[baseIndex]->getWidth() ||
               chunk->mLightmaps[idx]->getHeight() != baseHandles[baseIndex]->getHeight())
               return(false);

            TextureHandle * tHandle = gInteriorLMManager.duplicateBaseLightmap(interiorHandle, instanceHandle, baseIndex);

            // create the diff bitmap
            U8 * pDiff = chunk->mLightmaps[idx]->getAddress(0,0);
            U8 * pBase = baseHandles[baseIndex]->getBitmap()->getAddress(0,0);
            U8 * pDest = tHandle->getBitmap()->getAddress(0,0);

            Point2I extent(tHandle->getWidth(), tHandle->getHeight());
            for(U32 y = 0; y < extent.y; y++)
               for(U32 x = 0; x < extent.x; x++)
               {
                  *pDest++ = *pBase++ + *pDiff++;
                  *pDest++ = *pBase++ + *pDiff++;
                  *pDest++ = *pBase++ + *pDiff++;
               }

            idx++;
         }
      }
   }

   return(true);
}

bool SceneLighting::InteriorProxy::getPersistInfo(SceneLighting::PersistInfo::PersistChunk * info)
{
   if(!Parent::getPersistInfo(info))
      return(false);

   SceneLighting::PersistInfo::InteriorChunk * chunk = dynamic_cast<SceneLighting::PersistInfo::InteriorChunk*>(info);
   AssertFatal(chunk, "SceneLighting::InteriorProxy::getPersistInfo: invalid info chunk!");

   InteriorInstance * interior = getObject();
   if(!interior)
      return(false);

   LM_HANDLE instanceHandle = interior->getLMHandle();

   AssertFatal(!chunk->mDetailLightmapCount.size(), "SceneLighting::InteriorProxy::getPersistInfo: invalid array!");
   AssertFatal(!chunk->mDetailLightmapIndices.size(), "SceneLighting::InteriorProxy::getPersistInfo: invalid array!");
   AssertFatal(!chunk->mLightmaps.size(), "SceneLighting::InteriorProxy::getPersistInfo: invalid array!");

   U32 numDetails = interior->getNumDetailLevels();
   U32 i;
   for(i = 0; i < numDetails; i++)
   {
      Interior * detail = interior->getDetailLevel(i);
      LM_HANDLE interiorHandle = detail->getLMHandle();

      Vector<TextureHandle*> & baseHandles = gInteriorLMManager.getHandles(interiorHandle, 0);
      Vector<TextureHandle*> & instanceHandles = gInteriorLMManager.getHandles(interiorHandle, instanceHandle);

      U32 litCount = 0;

      // walk all the instance lightmaps and grab diff lighting from them
      for(U32 j = 0; j < instanceHandles.size(); j++)
      {
         if(!instanceHandles[j])
            continue;

         litCount++;
         chunk->mDetailLightmapIndices.push_back(j);

         GBitmap * baseBitmap = baseHandles[j]->getBitmap();
         GBitmap * instanceBitmap = instanceHandles[j]->getBitmap();

         Point2I extent(baseBitmap->getWidth(), baseBitmap->getHeight());

         GBitmap * diffLightmap = new GBitmap(extent.x, extent.y, false, GBitmap::RGB);

         U8 * pBase = baseBitmap->getAddress(0,0);
         U8 * pInstance = instanceBitmap->getAddress(0,0);
         U8 * pDest = diffLightmap->getAddress(0,0);

         // fill the diff lightmap
         for(U32 y = 0; y < extent.y; y++)
            for(U32 x = 0; x < extent.x; x++)
            {
               *pDest++ = *pInstance++ - *pBase++;
               *pDest++ = *pInstance++ - *pBase++;
               *pDest++ = *pInstance++ - *pBase++;
            }

         chunk->mLightmaps.push_back(diffLightmap);
      }

      chunk->mDetailLightmapCount.push_back(litCount);
   }

   // process the vertex lighting...
   AssertFatal(!chunk->mDetailVertexCount.size(), "SceneLighting::InteriorProxy::getPersistInfo: invalid chunk info");
   AssertFatal(!chunk->mVertexColorsNormal.size(), "SceneLighting::InteriorProxy::getPersistInfo: invalid chunk info");
   AssertFatal(!chunk->mVertexColorsAlarm.size(), "SceneLighting::InteriorProxy::getPersistInfo: invalid chunk info");

   chunk->mHasAlarmState = interior->getDetailLevel(0)->hasAlarmState();
   chunk->mDetailVertexCount.setSize(numDetails);

   U32 size = 0;
   for(i = 0; i < numDetails; i++)
   {
      Interior * detail = interior->getDetailLevel(i);

      U32 count = detail->getWindingCount();
      chunk->mDetailVertexCount[i] = count;
      size += count;
   }

   chunk->mVertexColorsNormal.setSize(size);
   if(chunk->mHasAlarmState)
      chunk->mVertexColorsAlarm.setSize(size);

   U32 curPos = 0;
   for(i = 0; i < numDetails; i++)
   {
      Vector<ColorI>* normal = interior->getVertexColorsNormal(i);
      Vector<ColorI>* alarm  = interior->getVertexColorsAlarm(i);
      AssertFatal(normal != NULL && alarm != NULL, "Error, no normal or alarm vertex colors!");

      U32 count = chunk->mDetailVertexCount[i];
      dMemcpy(&chunk->mVertexColorsNormal[curPos], normal->address(), count * sizeof(ColorI));

      if(chunk->mHasAlarmState)
         dMemcpy(&chunk->mVertexColorsAlarm[curPos], alarm->address(), count * sizeof(ColorI));

      curPos += count;
   }

   return(true);
}

//-------------------------------------------------------------------------------
// Class SceneLighting::TerrainProxy:
//-------------------------------------------------------------------------------
SceneLighting::TerrainProxy::TerrainProxy(SceneObject * obj) :
   Parent(obj)
{
   mLightmap = 0;
}

SceneLighting::TerrainProxy::~TerrainProxy()
{
   delete [] mLightmap;
}

//-------------------------------------------------------------------------------
void SceneLighting::TerrainProxy::init()
{
   mLightmap = new ColorF[TerrainBlock::LightmapSize * TerrainBlock::LightmapSize];
   dMemset(mLightmap, 0, TerrainBlock::LightmapSize * TerrainBlock::LightmapSize * sizeof(ColorF));
   mShadowMask.setSize(TerrainBlock::BlockSize * TerrainBlock::BlockSize);
}

bool SceneLighting::TerrainProxy::preLight(LightInfo * light)
{
   if(!bool(mObj))
      return(false);

   if(light->mType != LightInfo::Vector)
      return(false);

   mShadowMask.clear();
   return(true);
}

void SceneLighting::TerrainProxy::light(LightInfo * light)
{
   TerrainBlock * terrain = getObject();
   if(!terrain)
      return;

   AssertFatal(light->mType == LightInfo::Vector, "wrong light type");

   S32 time = Platform::getRealMilliseconds();

   // reset
   mShadowVolume = new ShadowVolumeBSP;
   // build interior shadow volume
   for(ObjectProxy ** itr = gLighting->mLitObjects.begin(); itr != gLighting->mLitObjects.end(); itr++)
   {
      if(!gLighting->isInterior((*itr)->mObj))
         continue;

      if(markInteriorShadow(static_cast<InteriorProxy*>(*itr)))
         gLighting->addInterior(mShadowVolume, *static_cast<InteriorProxy*>(*itr), light, SceneLighting::SHADOW_DETAIL);
   }
   lightVector(light);

   // set the lightmap...
   U16 * lPtr = (U16*)terrain->lightMap->getAddress(0,0);
   for(U32 i = 0; i < (TerrainBlock::LightmapSize * TerrainBlock::LightmapSize); i++)
      lPtr[i] = convertColor(mLightmap[i]);

   terrain->buildMaterialMap();

   delete mShadowVolume;

   Con::printf("    = terrain lit in %3.3f seconds", (Platform::getRealMilliseconds()-time)/1000.f);
}

//------------------------------------------------------------------------------
S32 SceneLighting::TerrainProxy::testSquare(const Point3F & min, const Point3F & max, S32 mask, F32 expand, const Vector<PlaneF> & clipPlanes)
{
   expand = 0;
   S32 retMask = 0;
   Point3F minPoint, maxPoint;
   for(S32 i = 0; i < clipPlanes.size(); i++)
   {
      if(mask & (1 << i))
      {
         if(clipPlanes[i].x > 0)
         {
            maxPoint.x = max.x;
            minPoint.x = min.x;
         }
         else
         {
            maxPoint.x = min.x;
            minPoint.x = max.x;
         }
         if(clipPlanes[i].y > 0)
         {
            maxPoint.y = max.y;
            minPoint.y = min.y;
         }
         else
         {
            maxPoint.y = min.y;
            minPoint.y = max.y;
         }
         if(clipPlanes[i].z > 0)
         {
            maxPoint.z = max.z;
            minPoint.z = min.z;
         }
         else
         {
            maxPoint.z = min.z;
            minPoint.z = max.z;
         }
         F32 maxDot = mDot(maxPoint, clipPlanes[i]);
         F32 minDot = mDot(minPoint, clipPlanes[i]);
         F32 planeD = clipPlanes[i].d;
         if(maxDot <= -(planeD + expand))
            return(U16(-1));
         if(minDot <= -planeD)
            retMask |= (1 << i);
      }
   }
   return(retMask);
}

bool SceneLighting::TerrainProxy::getShadowedSquares(const Vector<PlaneF> & clipPlanes, Vector<U16> & shadowList)
{
   TerrainBlock * terrain = getObject();
   if(!terrain)
      return(false);

   SquareStackNode stack[TerrainBlock::BlockShift * 4];

   stack[0].mLevel = TerrainBlock::BlockShift;
   stack[0].mClipFlags = 0xff;
   stack[0].mPos.set(0,0);

   U32 stackSize = 1;

   Point3F blockPos;
   terrain->getTransform().getColumn(3, &blockPos);
   S32 squareSize = terrain->getSquareSize();

   bool marked = false;

   // push through all the levels of the quadtree
   while(stackSize)
   {
      SquareStackNode * node = &stack[stackSize - 1];

      S32 clipFlags = node->mClipFlags;
      Point2I pos = node->mPos;
      GridSquare * sq = terrain->findSquare(node->mLevel, pos);

      Point3F minPoint, maxPoint;
      minPoint.set(squareSize * pos.x + blockPos.x,
                   squareSize * pos.y + blockPos.y,
                   fixedToFloat(sq->minHeight));
      maxPoint.set(minPoint.x + (squareSize << node->mLevel),
                   minPoint.y + (squareSize << node->mLevel),
                   fixedToFloat(sq->maxHeight));

      // test the square against the current level
      if(clipFlags)
      {
         clipFlags = testSquare(minPoint, maxPoint, clipFlags, squareSize, clipPlanes);
         if(clipFlags == U16(-1))
         {
            stackSize--;
            continue;
         }
      }

      // shadowed?
      if(node->mLevel == 0)
      {
         marked = true;
         shadowList.push_back(pos.x + (pos.y << TerrainBlock::BlockShift));
         stackSize--;
         continue;
      }

      // setup the next level of squares
      U8 nextLevel = node->mLevel - 1;
      S32 squareHalfSize = 1 << nextLevel;

      for(U32 i = 0; i < 4; i++)
      {
         node[i].mLevel = nextLevel;
         node[i].mClipFlags = clipFlags;
      }

      node[3].mPos = pos;
      node[2].mPos.set(pos.x + squareHalfSize, pos.y);
      node[1].mPos.set(pos.x, pos.y + squareHalfSize);
      node[0].mPos.set(pos.x + squareHalfSize, pos.y + squareHalfSize);

      stackSize += 3;
   }

   return(marked);
}

bool SceneLighting::TerrainProxy::markInteriorShadow(InteriorProxy * proxy)
{
   U32 i;
   // setup the clip planes: add the test and the lit planes
   Vector<PlaneF> clipPlanes;
   clipPlanes = proxy->mTerrainTestPlanes;
   for(i = 0; i < proxy->mLitBoxSurfaces.size(); i++)
      clipPlanes.push_back(proxy->mLitBoxSurfaces[i]->mPlane);

   Vector<U16> shadowList;
   if(!getShadowedSquares(clipPlanes, shadowList))
      return(false);

   // set the correct bit
   for(i = 0; i < shadowList.size(); i++)
      mShadowMask.set(shadowList[i]);

   return(true);
}

//-------------------------------------------------------------------------------
// BUGS: does not work with x or y directions of 0
//    : light dir of 0.1, 0.3, -0.8 causes strange behavior
void SceneLighting::TerrainProxy::lightVector(LightInfo * light)
{
   TerrainBlock * terrain = getObject();
   if(!terrain)
      return;

   Point3F lightDir = light->mDirection;
   lightDir.normalize();

   ColorF & ambient = light->mAmbient;
   ColorF & lightColor = light->mColor;

   if(lightDir.x == 0 && lightDir.y == 0)
      return;

   S32 generateLevel = Con::getIntVariable("$pref::sceneLighting::terrainGenerateLevel", 0);
   generateLevel = mClamp(generateLevel, 0, 4);

   bool allowLexelSplits = Con::getBoolVariable("$pref::sceneLighting::terrainAllowLexelSplits", false);

   U32 generateDim = TerrainBlock::LightmapSize << generateLevel;
   U32 generateShift = TerrainBlock::LightmapShift + generateLevel;
   U32 generateMask = generateDim - 1;

   F32 zStep;
   F32 frac;

   Point2I blockColStep;
   Point2I blockRowStep;
   Point2I blockFirstPos;
   Point2I lmFirstPos;

   F32 terrainDim = F32(terrain->getSquareSize()) * F32(TerrainBlock::BlockSize);
   F32 stepSize = F32(terrain->getSquareSize()) / F32(generateDim / TerrainBlock::BlockSize);

   if(mFabs(lightDir.x) >= mFabs(lightDir.y))
   {
      if(lightDir.x > 0)
      {
         zStep = lightDir.z / lightDir.x;
         frac = lightDir.y / lightDir.x;

         blockColStep.set(1, 0);
         blockRowStep.set(0, 1);
         blockFirstPos.set(0, 0);
         lmFirstPos.set(0,0);
      }
      else
      {
         zStep = -lightDir.z / lightDir.x;
         frac = -lightDir.y / lightDir.x;

         blockColStep.set(-1, 0);
         blockRowStep.set(0, 1);
         blockFirstPos.set(255, 0);
         lmFirstPos.set(TerrainBlock::LightmapSize - 1,0);
      }
   }
   else
   {
      if(lightDir.y > 0)
      {
         zStep = lightDir.z / lightDir.y;
         frac = lightDir.x / lightDir.y;

         blockColStep.set(0, 1);
         blockRowStep.set(1, 0);
         blockFirstPos.set(0, 0);
         lmFirstPos.set(0,0);
      }
      else
      {
         zStep = -lightDir.z / lightDir.y;
         frac = -lightDir.x / lightDir.y;

         blockColStep.set(0, -1);
         blockRowStep.set(1, 0);
         blockFirstPos.set(0, 255);
         lmFirstPos.set(0, TerrainBlock::LightmapSize - 1);
      }
   }
   zStep *= stepSize;

   F32 * heightArray = new F32[generateDim];

   S32 fracStep = -1;
   if(frac < 0)
   {
      fracStep = 1;
      frac = -frac;
   }

   F32 * nextHeightArray = new F32[generateDim];
   F32 oneMinusFrac = 1 - frac;

   U32 blockShift = generateShift - TerrainBlock::BlockShift;
   U32 lightmapShift = generateShift - TerrainBlock::LightmapShift;

   U32 blockStep = 1 << blockShift;
   U32 blockMask = blockStep - 1;
   U32 lightmapStep = 1 << lightmapShift;
   U32 lightmapNormalOffset = lightmapStep >> 1;
   U32 lightmapMask = lightmapStep - 1;

   Point2I bp = blockFirstPos;
   F32 terrainHeights[2][TerrainBlock::BlockSize];
   U32 i;

   F32 * pTerrainHeights = static_cast<F32*>(terrainHeights[0]);
   F32 * pNextTerrainHeights = static_cast<F32*>(terrainHeights[1]);

   // get first set of heights
   for(i = 0; i < TerrainBlock::BlockSize; i++) {
      pTerrainHeights[i] = fixedToFloat(terrain->getHeight(bp.x, bp.y));
      bp += blockRowStep;
   }

   // get second set of heights
   bp = blockFirstPos + blockColStep;
   for(i = 0; i < TerrainBlock::BlockSize; i++) {
      pNextTerrainHeights[i] = fixedToFloat(terrain->getHeight(bp.x, bp.y));
      bp += blockRowStep;
   }

   F32 heightStep = 1.f / blockStep;

   F32 terrainZRowStep[TerrainBlock::BlockSize];
   F32 nextTerrainZRowStep[TerrainBlock::BlockSize];
   F32 terrainZColStep[TerrainBlock::BlockSize];

   // fill in the row steps
   for(i = 0; i < TerrainBlock::BlockSize; i++)
   {
      terrainZRowStep[i] = (pTerrainHeights[(i+1) & TerrainBlock::BlockMask] - pTerrainHeights[i]) * heightStep;
      nextTerrainZRowStep[i] = (pNextTerrainHeights[(i+1) & TerrainBlock::BlockMask] - pNextTerrainHeights[i]) * heightStep;
      terrainZColStep[i] = (pNextTerrainHeights[i] - pTerrainHeights[i]) * heightStep;
   }

   // get first row of process heights
   for(i = 0; i < generateDim; i++)
   {
      U32 bi = i >> blockShift;
      heightArray[i] = pTerrainHeights[bi] + (i & blockMask) * terrainZRowStep[bi];
   }

   bp = blockFirstPos;
   if(generateDim == TerrainBlock::BlockSize)
      bp += blockColStep;

   // generate the initial run
   U32 x, y;
   for(x = 1; x < generateDim; x++)
   {
      U32 xmask = x & blockMask;

      // generate new height step rows?
      if(!xmask)
      {
         F32 * tmp = pTerrainHeights;
         pTerrainHeights = pNextTerrainHeights;
         pNextTerrainHeights = tmp;

         bp += blockColStep;

         Point2I bwalk = bp;
         for(i = 0; i < TerrainBlock::BlockSize; i++, bwalk += blockRowStep)
            pNextTerrainHeights[i] = fixedToFloat(terrain->getHeight(bwalk.x, bwalk.y));

         // fill in the row steps
         for(i = 0; i < TerrainBlock::BlockSize; i++)
         {
            terrainZRowStep[i] = (pTerrainHeights[(i+1) & TerrainBlock::BlockMask] - pTerrainHeights[i]) * heightStep;
            nextTerrainZRowStep[i] = (pNextTerrainHeights[(i+1) & TerrainBlock::BlockMask] - pNextTerrainHeights[i]) * heightStep;
            terrainZColStep[i] = (pNextTerrainHeights[i] - pTerrainHeights[i]) * heightStep;
         }
      }

      Point2I bwalk = bp - blockRowStep;
      for(y = 0; y < generateDim; y++)
      {
         U32 ymask = y & blockMask;
         if(!ymask)
            bwalk += blockRowStep;

         U32 bi = y >> blockShift;
         U32 binext = (bi + 1) & TerrainBlock::BlockMask;

         F32 height;

         // 135?
         if((bwalk.x ^ bwalk.y) & 1)
         {
            U32 xsub = blockStep - xmask;
            if(xsub > ymask) // bottom
               height = pTerrainHeights[bi] + xmask * terrainZColStep[bi] +
                        ymask * terrainZRowStep[bi];
            else // top
               height = pNextTerrainHeights[bi] - xmask * terrainZColStep[binext] +
                        ymask * nextTerrainZRowStep[bi];
         }
         else
         {
            if(xmask > ymask) // bottom
               height = pTerrainHeights[bi] + xmask * terrainZColStep[bi] +
                        ymask * nextTerrainZRowStep[bi];
            else // top
               height = pTerrainHeights[bi] + xmask * terrainZColStep[binext] +
                        ymask * terrainZRowStep[bi];
         }

         F32 intHeight = heightArray[y] * oneMinusFrac + heightArray[(y + fracStep) & generateMask] * frac + zStep;
         nextHeightArray[y] = getMax(height, intHeight);
      }

      // swap the height rows
      F32 * tmp = heightArray;
      heightArray = nextHeightArray;
      nextHeightArray = tmp;
   }

   F32 squareSize = terrain->getSquareSize();
   F32 lexelDim = squareSize * F32(TerrainBlock::BlockSize) / F32(TerrainBlock::LightmapSize);

   // calculate normal runs
   Point3F normals[2][TerrainBlock::BlockSize];

   Point3F * pNormals = static_cast<Point3F*>(normals[0]);
   Point3F * pNextNormals = static_cast<Point3F*>(normals[1]);

   // calculate the normal lookup table
   F32 * normTable = new F32 [blockStep * blockStep * 4];

   Point2F corners[4] = {
      Point2F(0.f, 0.f),
      Point2F(1.f, 0.f),
      Point2F(1.f, 1.f),
      Point2F(0.f, 1.f)
   };

   U32 idx = 0;
   F32 step = 1.f / blockStep;
   Point2F pos(0.f, 0.f);

   // fill it
   for(x = 0; x < blockStep; x++, pos.x += step, pos.y = 0.f) {
      for(y = 0; y < blockStep; y++, pos.y += step) {
         for(i = 0; i < 4; i++, idx++)
            normTable[idx] = 1.f - getMin(Point2F(pos - corners[i]).len(), 1.f);
      }
   }

   // fill first column
   bp = blockFirstPos;
   for(x = 0; x < TerrainBlock::BlockSize; x++)
   {
      pNextTerrainHeights[x] = fixedToFloat(terrain->getHeight(bp.x, bp.y));
      Point2F pos(bp.x * squareSize, bp.y * squareSize);
      terrain->getNormal(pos, &pNextNormals[x]);
      bp += blockRowStep;
   }

   // get swapped on first pass
   pTerrainHeights = static_cast<F32*>(terrainHeights[1]);
   pNextTerrainHeights = static_cast<F32*>(terrainHeights[0]);

   // get the world offset of the terrain
   const MatrixF & transform = terrain->getTransform();
   Point3F worldOffset;
   transform.getColumn(3, &worldOffset);

   F32 ratio = F32(1 << lightmapShift);
   F32 ratioSquared = ratio * ratio;
   F32 inverseRatioSquared = 1.f / ratioSquared;

   F32 lightScale[TerrainBlock::LightmapSize];

   // walk it...
   bp = blockFirstPos - blockColStep;
   for(x = 0; x < generateDim; x++)
   {
      U32 xmask = x & blockMask;
      U32 lxmask = x & lightmapMask;

      // generate new runs?
      if(!xmask)
      {
         bp += blockColStep;

         // do the normals
         Point3F * temp = pNormals;
         pNormals = pNextNormals;
         pNextNormals = temp;

         // fill the row
         Point2I bwalk = bp + blockColStep;
         for(i = 0; i < TerrainBlock::BlockSize; i++)
         {
            Point2F pos(bwalk.x * squareSize, bwalk.y * squareSize);
            terrain->getNormal(pos, &pNextNormals[i]);
            bwalk += blockRowStep;
         }

         // do the heights
         F32 * tmp = pTerrainHeights;
         pTerrainHeights = pNextTerrainHeights;
         pNextTerrainHeights = tmp;

         bwalk = bp + blockColStep;
         for(i = 0; i < TerrainBlock::BlockSize; i++, bwalk += blockRowStep)
            pNextTerrainHeights[i] = fixedToFloat(terrain->getHeight(bwalk.x, bwalk.y));

         // fill in the row steps
         for(i = 0; i < TerrainBlock::BlockSize; i++)
         {
            terrainZRowStep[i] = (pTerrainHeights[(i+1) & TerrainBlock::BlockMask] - pTerrainHeights[i]) * heightStep;
            nextTerrainZRowStep[i] = (pNextTerrainHeights[(i+1) & TerrainBlock::BlockMask] - pNextTerrainHeights[i]) * heightStep;
            terrainZColStep[i] = (pNextTerrainHeights[i] - pTerrainHeights[i]) * heightStep;
         }
      }

      // reset the light scale table
      if(!lxmask)
         for(i = 0; i < TerrainBlock::LightmapSize; i++)
            lightScale[i] = 1.f;

      Point2I bwalk = bp - blockRowStep;
      for(y = 0; y < generateDim; y++)
      {
         U32 lymask = y & lightmapMask;
         U32 ymask = y & blockMask;
         if(!ymask)
            bwalk += blockRowStep;

         U32 bi = y >> blockShift;
         U32 binext = (bi + 1) & TerrainBlock::BlockMask;

         F32 height;
         F32 xstep, ystep;

         // 135?
         if((bwalk.x ^ bwalk.y) & 1)
         {
            U32 xsub = blockStep - xmask;
            if(xsub > ymask) // bottom
            {
               xstep = terrainZColStep[bi];
               ystep = terrainZRowStep[bi];
               height = pTerrainHeights[bi] + xmask * xstep + ymask * ystep;
            }
            else // top
            {
               xstep = -terrainZColStep[binext];
               ystep = nextTerrainZRowStep[bi];
               height = pNextTerrainHeights[bi] + xsub * xstep + ymask * ystep;
            }
         }
         else
         {
            if(xmask > ymask) // bottom
            {
               xstep = terrainZColStep[bi];
               ystep = nextTerrainZRowStep[bi];
               height = pTerrainHeights[bi] + xmask * xstep + ymask * ystep;
            }
            else // top
            {
               xstep = terrainZColStep[binext];
               ystep = terrainZRowStep[bi];
               height = pTerrainHeights[bi] + xmask * xstep + ymask * ystep;
            }
         }

         F32 intHeight = heightArray[y] * oneMinusFrac + heightArray[(y + fracStep) & generateMask] * frac + zStep;

         U32 lsi = y >> lightmapShift;

         Point2I lmPos = lmFirstPos + blockColStep * (x >> lightmapShift) + blockRowStep * lsi;

         ColorF & col = mLightmap[lmPos.x + (lmPos.y << TerrainBlock::LightmapShift)];

         // lexel shaded by an interior?
         Point2I terrPos = lmPos;
         terrPos.x >>= TerrainBlock::LightmapShift - TerrainBlock::BlockShift;
         terrPos.y >>= TerrainBlock::LightmapShift - TerrainBlock::BlockShift;

         if(!lxmask && !lymask && mShadowMask.test(terrPos.x + (terrPos.y << TerrainBlock::BlockShift)))
         {
            U32 idx = (xmask + lightmapNormalOffset + ((ymask + lightmapNormalOffset) << blockShift)) << 2;

            // get the normal for this lexel
            Point3F normal = pNormals[bi] * normTable[idx++];
            normal += pNormals[binext] * normTable[idx++];
            normal += pNextNormals[binext] * normTable[idx++];
            normal += pNextNormals[bi] * normTable[idx];
            normal.normalize();

            nextHeightArray[y] = height;
            F32 colorScale = -mDot(normal, lightDir);

            if(colorScale > 0.f)
            {
               // split lexels which cross the square split?
               if(allowLexelSplits)
               {
                  // jff:todo
               }
               else
               {
                  Point2F wPos((lmPos.x) * lexelDim + worldOffset.x,
                               (lmPos.y) * lexelDim + worldOffset.y);

                  F32 xh = xstep * ratio;
                  F32 yh = ystep * ratio;

                  ShadowVolumeBSP::SVPoly * poly = mShadowVolume->createPoly();
                  poly->mWindingCount = 4;
                  poly->mWinding[0].set(wPos.x, wPos.y, height);
                  poly->mWinding[1].set(wPos.x + lexelDim, wPos.y, height + xh);
                  poly->mWinding[2].set(wPos.x + lexelDim, wPos.y + lexelDim, height + xh + yh);
                  poly->mWinding[3].set(wPos.x, wPos.y + lexelDim, height + yh);
                  poly->mPlane.set(poly->mWinding[0], poly->mWinding[1], poly->mWinding[2]);

                  F32 lexelSize = mShadowVolume->getPolySurfaceArea(poly);
                  F32 intensity = mShadowVolume->getClippedSurfaceArea(poly) / lexelSize;
                  lightScale[lsi] = mClampF(intensity, 0.f, 1.f);
               }
            }
            else
               lightScale[lsi] = 0.f;
         }

         // non shadowed?
         if(height >= intHeight)
         {
            U32 idx = (xmask + (ymask << blockShift)) << 2;

            Point3F normal = pNormals[bi] * normTable[idx++];
            normal += pNormals[binext] * normTable[idx++];
            normal += pNextNormals[binext] * normTable[idx++];
            normal += pNextNormals[bi] * normTable[idx];
            normal.normalize();

            nextHeightArray[y] = height;
            F32 colorScale = -mDot(normal, lightDir);

            if(colorScale > 0.f)
               col += ambient + lightColor * colorScale * lightScale[lsi];
            else
               col += ambient;
         }
         else
         {
            nextHeightArray[y] = intHeight;
            col += ambient;
         }
      }

      F32 * tmp = heightArray;
      heightArray = nextHeightArray;
      nextHeightArray = tmp;
   }

   // set the proper color
   for(i = 0; i < TerrainBlock::LightmapSize * TerrainBlock::LightmapSize; i++)
   {
      mLightmap[i] *= inverseRatioSquared;
      mLightmap[i].clamp();
   }

   delete [] normTable;
   delete [] heightArray;
   delete [] nextHeightArray;
}

//--------------------------------------------------------------------------
U32 SceneLighting::TerrainProxy::getResourceCRC()
{
   TerrainBlock * terrain = getObject();
   if(!terrain)
      return(0);
   return(terrain->getCRC());
}

//--------------------------------------------------------------------------
bool SceneLighting::TerrainProxy::setPersistInfo(SceneLighting::PersistInfo::PersistChunk * info)
{
   if(!Parent::setPersistInfo(info))
      return(false);

   SceneLighting::PersistInfo::TerrainChunk * chunk = dynamic_cast<SceneLighting::PersistInfo::TerrainChunk*>(info);
   AssertFatal(chunk, "SceneLighting::TerrainProxy::setPersistInfo: invalid info chunk!");

   TerrainBlock * terrain = getObject();
   if(!terrain || !terrain->lightMap)
      return(false);

   dMemcpy(terrain->lightMap->getAddress(0,0), chunk->mLightmap, sizeof(U16) * TerrainBlock::LightmapSize * TerrainBlock::LightmapSize);
   terrain->buildMaterialMap();
   return(true);
}

bool SceneLighting::TerrainProxy::getPersistInfo(SceneLighting::PersistInfo::PersistChunk * info)
{
   if(!Parent::getPersistInfo(info))
      return(false);

   SceneLighting::PersistInfo::TerrainChunk * chunk = dynamic_cast<SceneLighting::PersistInfo::TerrainChunk*>(info);
   AssertFatal(chunk, "SceneLighting::TerrainProxy::getPersistInfo: invalid info chunk!");

   TerrainBlock * terrain = getObject();
   if(!terrain || !terrain->lightMap)
      return(false);

   chunk->mLightmap = new U16[TerrainBlock::LightmapSize * TerrainBlock::LightmapSize];
   dMemcpy(chunk->mLightmap, terrain->lightMap->getAddress(0,0), sizeof(U16) * TerrainBlock::LightmapSize * TerrainBlock::LightmapSize);
   return(true);
}
