mirror of
https://port.numenaute.org/aleajactaest/khanat-opennel-code.git
synced 2024-11-23 07:36:16 +00:00
2361 lines
68 KiB
C++
2361 lines
68 KiB
C++
// Ryzom - MMORPG Framework <http://dev.ryzom.com/projects/ryzom/>
|
|
// Copyright (C) 2010 Winch Gate Property Limited
|
|
//
|
|
// This program is free software: you can redistribute it and/or modify
|
|
// it under the terms of the GNU Affero General Public License as
|
|
// published by the Free Software Foundation, either version 3 of the
|
|
// License, or (at your option) any later version.
|
|
//
|
|
// This program is distributed in the hope that it will be useful,
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
// GNU Affero General Public License for more details.
|
|
//
|
|
// You should have received a copy of the GNU Affero General Public License
|
|
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
#include "screenshot_islands.h"
|
|
|
|
#include <game_share/scenario_entry_points.h>
|
|
|
|
#include <game_share/season.h>
|
|
#include <game_share/dir_light_setup.h>
|
|
#include <game_share/bmp4image.h>
|
|
|
|
#include <nel/misc/path.h>
|
|
#include <nel/misc/file.h>
|
|
#include <nel/misc/config_file.h>
|
|
#include <nel/misc/big_file.h>
|
|
#include <nel/misc/i18n.h>
|
|
#include <nel/misc/progress_callback.h>
|
|
#include <nel/misc/random.h>
|
|
#include <nel/misc/common.h>
|
|
#include <nel/3d/u_material.h>
|
|
|
|
#include <nel/3d/u_driver.h>
|
|
#include <nel/3d/u_scene.h>
|
|
#include <nel/3d/u_landscape.h>
|
|
#include <nel/3d/u_camera.h>
|
|
#include <nel/3d/landscapeig_manager.h>
|
|
#include <nel/3d/u_material.h>
|
|
|
|
#include <nel/georges/u_form_loader.h>
|
|
#include <nel/georges/u_form.h>
|
|
#include <nel/georges/u_form_elm.h>
|
|
|
|
// AI share
|
|
#include <ai_share/world_map.h>
|
|
|
|
#include <nel/3d/material.h>
|
|
|
|
#include <math.h>
|
|
#include <limits>
|
|
|
|
using namespace NLMISC;
|
|
using namespace NL3D;
|
|
using namespace std;
|
|
using namespace EGSPD;
|
|
using namespace NLGEORGES;
|
|
using namespace RYAI_MAP_CRUNCH;
|
|
|
|
|
|
// The 3d driver
|
|
UDriver *driver = NULL;
|
|
CLandscapeIGManager LandscapeIGManager;
|
|
|
|
uint ScreenShotWidth;
|
|
uint ScreenShotHeight;
|
|
|
|
UMaterial sceneMaterial;
|
|
|
|
namespace R2
|
|
{
|
|
|
|
const TBufferEntry InteriorValue = std::numeric_limits<TBufferEntry>::max()-1;
|
|
const TBufferEntry ValueBorder = std::numeric_limits<TBufferEntry>::max()-2;
|
|
const uint32 BigValue= 15*5;
|
|
const float limitValue = 200.0;
|
|
|
|
//-------------------------------------------------------------------------------------------------
|
|
CScreenshotIslands::CScreenshotIslands()
|
|
{
|
|
_BackColor = CRGBA(255, 255, 255, 255);
|
|
}
|
|
|
|
//-------------------------------------------------------------------------------------------------
|
|
void CScreenshotIslands::init()
|
|
{
|
|
// Create a driver
|
|
driver = UDriver::createDriver(0, true);
|
|
nlassert(driver);
|
|
|
|
sceneMaterial = driver->createMaterial();
|
|
sceneMaterial.getObjectPtr()->setLighting(true);
|
|
sceneMaterial.getObjectPtr()->setSpecular(CRGBA(255, 255, 255, 255));
|
|
sceneMaterial.getObjectPtr()->setShininess(50);
|
|
sceneMaterial.getObjectPtr()->setDiffuse(CRGBA(100, 100, 100, 255));
|
|
sceneMaterial.getObjectPtr()->setEmissive(CRGBA(25, 25, 25, 255));
|
|
|
|
// load and parse the configfile
|
|
CConfigFile cf;
|
|
cf.load("island_screenshots.cfg");
|
|
|
|
// get the value of searchPaths
|
|
CConfigFile::CVar * searchPaths = cf.getVarPtr("SearchPaths");
|
|
if(searchPaths)
|
|
{
|
|
for(int i = 0; i < searchPaths->size(); i++)
|
|
{
|
|
CPath::addSearchPath(searchPaths->asString(i).c_str(), true, false);
|
|
}
|
|
}
|
|
CPath::remapExtension("dds", "tga", true);
|
|
CPath::remapExtension("dds", "png", true);
|
|
|
|
// get the scenario entry points file
|
|
CConfigFile::CVar * epFile = cf.getVarPtr("CompleteIslandsFile");
|
|
if(epFile)
|
|
{
|
|
_CompleteIslandsFile = epFile->asString();
|
|
}
|
|
|
|
// get the out directory path
|
|
CConfigFile::CVar * outDir = cf.getVarPtr("OutDir");
|
|
if(outDir)
|
|
{
|
|
_OutDirectory = outDir->asString();
|
|
}
|
|
|
|
// get the vegetation option
|
|
CConfigFile::CVar * veget = cf.getVarPtr("Vegetation");
|
|
if(veget)
|
|
{
|
|
_Vegetation = veget->asBool();
|
|
}
|
|
|
|
// get the vegetation option
|
|
CConfigFile::CVar * inverseZTest = cf.getVarPtr("InverseZTest");
|
|
if(inverseZTest)
|
|
{
|
|
_InverseZTest = inverseZTest->asBool();
|
|
}
|
|
|
|
// get list of continents
|
|
CConfigFile::CVar * continents = cf.getVarPtr("Continents");
|
|
vector<string> continentsName(continents->size());
|
|
for(int i = 0; i < continents->size(); i++)
|
|
{
|
|
continentsName[i] = continents->asString(i);
|
|
}
|
|
|
|
// get continents data (light, coarseMesh,...)
|
|
UFormLoader * formLoader;
|
|
for(uint i=0; i<continentsName.size(); i++)
|
|
{
|
|
string georgeFileName = continentsName[i]+".continent";
|
|
|
|
formLoader = UFormLoader::createLoader();
|
|
CSmartPtr<UForm> form = formLoader->loadForm(CPath::lookup(georgeFileName).c_str());
|
|
if(form)
|
|
{
|
|
CContinentData continentData;
|
|
|
|
UFormElm &formRoot = form->getRootNode();
|
|
|
|
const UFormElm *elm;
|
|
if(formRoot.getNodeByName(&elm, "LightLandscapeDay") && elm)
|
|
{
|
|
CDirLightSetup landscapeLightDay;
|
|
landscapeLightDay.build(*elm);
|
|
continentData.Ambiant = landscapeLightDay.Ambiant;
|
|
continentData.Diffuse = landscapeLightDay.Diffuse;
|
|
|
|
if(continentsName[i]=="r2_jungle" || continentsName[i]=="r2_forest" || continentsName[i]=="r2_roots")
|
|
{
|
|
continentData.Ambiant = CRGBA(255, 255, 255, 255);
|
|
continentData.Diffuse = CRGBA(255, 255, 255, 255);
|
|
}
|
|
}
|
|
|
|
formRoot.getValueByName(continentData.IGFile, "LandscapeIG");
|
|
string zoneMin, zoneMax;
|
|
formRoot.getValueByName(zoneMin, "ZoneMin");
|
|
formRoot.getValueByName(zoneMax, "ZoneMax");
|
|
|
|
getPosFromZoneName(zoneMin, continentData.ZoneMin);
|
|
getPosFromZoneName(zoneMax, continentData.ZoneMax);
|
|
|
|
string filename;
|
|
if(formRoot.getValueByName(filename, "Ecosystem"))
|
|
{
|
|
UFormLoader *formLoaderEco = UFormLoader::createLoader();
|
|
if(formLoaderEco)
|
|
{
|
|
// Load the form
|
|
CSmartPtr<UForm> formEco = formLoaderEco->loadForm(filename.c_str());
|
|
|
|
if(formEco)
|
|
{
|
|
// Root node
|
|
UFormElm &formRootEco = formEco->getRootNode();
|
|
|
|
// Small bank.
|
|
formRootEco.getValueByName(continentData.SmallBank, "SmallBank");
|
|
|
|
// Far bank.
|
|
formRootEco.getValueByName(continentData.FarBank, "FarBank");
|
|
|
|
// Coarse mesh texture.
|
|
formRootEco.getValueByName(continentData.CoarseMeshMap, "CoarseMeshMap");
|
|
}
|
|
else
|
|
{
|
|
nlwarning("CScreenshotIslands::init : Can't load form %s.", filename.c_str());
|
|
}
|
|
UFormLoader::releaseLoader(formLoaderEco);
|
|
}
|
|
}
|
|
|
|
_ContinentsData[continentsName[i]] = continentData;
|
|
}
|
|
UFormLoader::releaseLoader(formLoader);
|
|
}
|
|
|
|
|
|
// load islands
|
|
loadIslands();
|
|
|
|
searchIslandsBorders();
|
|
|
|
// get seasons
|
|
CConfigFile::CVar * seasonSuffixes = cf.getVarPtr("SeasonSuffixes");
|
|
if(seasonSuffixes)
|
|
{
|
|
for(uint i = 0; i < (uint)seasonSuffixes->size(); i++)
|
|
{
|
|
_SeasonSuffixes.push_back(seasonSuffixes->asString(i));
|
|
}
|
|
}
|
|
|
|
// get the meter size in pixels
|
|
CConfigFile::CVar * meterSize = cf.getVarPtr("MeterPixelSize");
|
|
if(meterSize)
|
|
{
|
|
_MeterPixelSize = meterSize->asInt();
|
|
}
|
|
}
|
|
|
|
//-------------------------------------------------------------------------------------------------
|
|
bool CScreenshotIslands::getPosFromZoneName(const std::string &name, NLMISC::CVector2f &dest)
|
|
{
|
|
if(name.empty())
|
|
{
|
|
nlwarning ("getPosFromZoneName(): empty name, can't getPosFromZoneName");
|
|
return false;
|
|
}
|
|
|
|
static std::string zoneName;
|
|
static string xStr, yStr;
|
|
xStr.clear();
|
|
yStr.clear();
|
|
zoneName = CFile::getFilenameWithoutExtension(name);
|
|
uint32 i = 0;
|
|
while(zoneName[i] != '_')
|
|
{
|
|
if(!::isdigit(zoneName[i])) return false;
|
|
yStr += zoneName[i]; ++i;
|
|
if(i == zoneName.size())
|
|
return false;
|
|
}
|
|
++i;
|
|
while(i < zoneName.size())
|
|
{
|
|
if(!::isalpha(zoneName[i])) return false;
|
|
xStr += (char) ::toupper(zoneName[i]); ++i;
|
|
}
|
|
if(xStr.size() != 2) return false;
|
|
dest.x = 160.f * ((xStr[0] - 'A') * 26 + (xStr[1] - 'A'));
|
|
dest.y = 160.f * -atoi(yStr.c_str());
|
|
return true;
|
|
}
|
|
|
|
//-------------------------------------------------------------------------------------------------
|
|
void CScreenshotIslands::searchIslandsBorders()
|
|
{
|
|
vector<string> filenames;
|
|
list<string> zonelFiles;
|
|
|
|
map< CVector2f, bool> islandsMap;
|
|
|
|
TContinentsData::iterator itCont(_ContinentsData.begin()), lastCont(_ContinentsData.end());
|
|
for( ; itCont != lastCont ; ++itCont)
|
|
{
|
|
// for each continent we recover a map of zonel files whith position of
|
|
// left/bottom point of each zone for keys
|
|
filenames.clear();
|
|
zonelFiles.clear();
|
|
|
|
string bnpFileName = itCont->first + ".bnp";
|
|
CBigFile::getInstance().list(bnpFileName.c_str(), filenames);
|
|
|
|
for(uint i=0; i<filenames.size(); i++)
|
|
{
|
|
if(CFile::getExtension(filenames[i]) == "zonel")
|
|
{
|
|
zonelFiles.push_back(filenames[i]);
|
|
}
|
|
}
|
|
|
|
list<string>::iterator itZonel(zonelFiles.begin()), lastZonel(zonelFiles.end());
|
|
for( ; itZonel != lastZonel ; ++itZonel)
|
|
{
|
|
CVector2f position;
|
|
getPosFromZoneName(*itZonel, position);
|
|
|
|
islandsMap[position] = true;
|
|
}
|
|
|
|
// search for island borders
|
|
CContinentData & continent = itCont->second;
|
|
list< string >::const_iterator itIsland(continent.Islands.begin()), lastIsland(continent.Islands.end());
|
|
for( ; itIsland != lastIsland ; ++itIsland)
|
|
{
|
|
if(_IslandsData.find(itIsland->c_str()) != _IslandsData.end())
|
|
{
|
|
const CProximityZone & islandData = _IslandsData[itIsland->c_str()];
|
|
|
|
sint32 xmin = islandData.getBoundXMin();
|
|
sint32 xmax = islandData.getBoundXMax();
|
|
sint32 ymin = islandData.getBoundYMin();
|
|
sint32 ymax = islandData.getBoundYMax();
|
|
|
|
sint32 width = xmax-xmin;
|
|
sint32 height = ymax-ymin;
|
|
|
|
sint32 zonelXMin = ((uint)(xmin/160)) * 160;
|
|
sint32 zonelYMin = ((uint)(ymin/160) - 1) * 160;
|
|
sint32 zonelXMax = ((uint)(xmax/160)) * 160;
|
|
sint32 zonelYMax = ((uint)(ymax/160) - 1) * 160;
|
|
|
|
list< CVector2f > leftBorders, rightBorders, bottomBorders, topBorders;
|
|
|
|
// search for left and right borders on lines
|
|
for(sint32 y = zonelYMin; y<=zonelYMax; y+=160 )
|
|
{
|
|
sint32 x=zonelXMin;
|
|
CVector2f vec((float)x, (float)y);
|
|
bool lastZoneFull = (islandsMap.find(vec) != islandsMap.end());
|
|
bool currentZoneFull;
|
|
|
|
while(x<=zonelXMax)
|
|
{
|
|
vec = CVector2f((float)x, (float)y);
|
|
currentZoneFull = (islandsMap.find(vec) != islandsMap.end());
|
|
|
|
if(lastZoneFull && !currentZoneFull && vec.x-1 >= xmin)
|
|
{
|
|
rightBorders.push_back(CVector2f(vec.x-1, vec.y));
|
|
}
|
|
else if(!lastZoneFull && currentZoneFull)
|
|
{
|
|
leftBorders.push_back(vec);
|
|
}
|
|
x += 160;
|
|
lastZoneFull = currentZoneFull;
|
|
}
|
|
}
|
|
|
|
// search for bottom and top borders on columns
|
|
for(sint32 x = zonelXMin; x<=zonelXMax; x+=160 )
|
|
{
|
|
sint32 y=zonelYMin;
|
|
CVector2f vec((float)x, (float)y);
|
|
bool lastZoneFull = (islandsMap.find(vec) != islandsMap.end());
|
|
bool currentZoneFull;
|
|
|
|
while(y<=zonelYMax)
|
|
{
|
|
vec = CVector2f((float)x, (float)y);
|
|
currentZoneFull = (islandsMap.find(vec) != islandsMap.end());
|
|
|
|
if(lastZoneFull && !currentZoneFull && vec.y-1 >= ymin)
|
|
{
|
|
topBorders.push_back(CVector2f(vec.x, vec.y-1));
|
|
}
|
|
else if(!lastZoneFull && currentZoneFull)
|
|
{
|
|
bottomBorders.push_back(vec);
|
|
}
|
|
y += 160;
|
|
lastZoneFull = currentZoneFull;
|
|
}
|
|
}
|
|
|
|
_BorderIslands[*itIsland + "/right"] = rightBorders;
|
|
_BorderIslands[*itIsland + "/left"] = leftBorders;
|
|
_BorderIslands[*itIsland + "/bottom"] = bottomBorders;
|
|
_BorderIslands[*itIsland + "/top"] = topBorders;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
//-------------------------------------------------------------------------------------------------
|
|
void CScreenshotIslands::attenuateIslandBorders(const std::string & islandName, CBitmap & islandBitmap,
|
|
const CProximityZone & islandData)
|
|
{
|
|
list< CVector2f > leftBorders, rightBorders, bottomBorders, topBorders;
|
|
rightBorders = _BorderIslands[islandName + "/right"];
|
|
leftBorders = _BorderIslands[islandName + "/left"];
|
|
bottomBorders = _BorderIslands[islandName + "/bottom"];
|
|
topBorders = _BorderIslands[islandName + "/top"];
|
|
|
|
sint32 xmin = islandData.getBoundXMin();
|
|
sint32 xmax = islandData.getBoundXMax();
|
|
sint32 ymin = islandData.getBoundYMin();
|
|
sint32 ymax = islandData.getBoundYMax();
|
|
|
|
sint32 width = xmax-xmin;
|
|
sint32 height = ymax-ymin;
|
|
uint8 *dest = &(islandBitmap.getPixels(0)[0]);
|
|
|
|
list< CVector2f >::iterator itBorder;
|
|
|
|
for(itBorder=leftBorders.begin(); itBorder!=leftBorders.end(); itBorder++)
|
|
{
|
|
const CVector2f initPoint = *itBorder;
|
|
sint32 x = (sint32)initPoint.x - xmin;
|
|
sint32 y = (sint32)initPoint.y - ymin;
|
|
sint32 maxBorder = 160;
|
|
if(y<0)
|
|
{
|
|
maxBorder += y;
|
|
y = 0;
|
|
}
|
|
sint32 inity = y;
|
|
|
|
while(y<(inity+maxBorder) && y<(sint32)height)
|
|
{
|
|
double noiseValue = (1+cos(((y-inity)*2*Pi)/maxBorder))*5;
|
|
double evalNoise = 10 + noiseValue;
|
|
double diffAlpha = 255/evalNoise;
|
|
|
|
CRGBA color = islandBitmap.getPixelColor(x, height-y-1);
|
|
sint32 currentX = x-1;
|
|
|
|
while((currentX>=x-evalNoise) && currentX>=0)
|
|
{
|
|
uint8 *pixel = &(islandBitmap.getPixels(0)[((height-y-1)*width + currentX)*4]);
|
|
uint alpha = (uint)(255-diffAlpha*(x-currentX));
|
|
uint invAlpha = 255-alpha;
|
|
|
|
*pixel = (uint8) (((invAlpha * *pixel) + (alpha * color.R)) >> 8);
|
|
*(pixel + 1) = (uint8) (((invAlpha * *(pixel + 1)) + (alpha * color.G)) >> 8);
|
|
*(pixel + 2) = (uint8) (((invAlpha * *(pixel + 2)) + (alpha * color.B)) >> 8);
|
|
*(pixel + 3) = (uint8) 255;
|
|
|
|
currentX--;
|
|
}
|
|
y++;
|
|
}
|
|
}
|
|
|
|
for(itBorder=rightBorders.begin(); itBorder!=rightBorders.end(); itBorder++)
|
|
{
|
|
const CVector2f initPoint = *itBorder;
|
|
sint32 x = (sint32)initPoint.x - xmin;
|
|
sint32 y = (sint32)initPoint.y - ymin;
|
|
sint32 maxBorder = 160;
|
|
if(y<0)
|
|
{
|
|
maxBorder += y;
|
|
y = 0;
|
|
}
|
|
sint32 inity = y;
|
|
|
|
while(y<(inity+maxBorder) && y<(sint32)height)
|
|
{
|
|
double noiseValue = (1+cos(((y-inity)*2*Pi)/maxBorder))*5;
|
|
double evalNoise = 10 + noiseValue;
|
|
double diffAlpha = 255/evalNoise;
|
|
|
|
CRGBA color = islandBitmap.getPixelColor(x, height-y-1);
|
|
sint32 currentX = x+1;
|
|
|
|
while((currentX<=x+evalNoise) && currentX<width)
|
|
{
|
|
uint8 *pixel = &(islandBitmap.getPixels(0)[((height-y-1)*width + currentX)*4]);
|
|
uint alpha = (uint)(255-diffAlpha*(currentX-x));
|
|
uint invAlpha = 255-alpha;
|
|
|
|
*pixel = (uint8) (((invAlpha * *pixel) + (alpha * color.R)) >> 8);
|
|
*(pixel + 1) = (uint8) (((invAlpha * *(pixel + 1)) + (alpha * color.G)) >> 8);
|
|
*(pixel + 2) = (uint8) (((invAlpha * *(pixel + 2)) + (alpha * color.B)) >> 8);
|
|
*(pixel + 3) = (uint8) 255;
|
|
|
|
currentX++;
|
|
}
|
|
|
|
y++;
|
|
}
|
|
}
|
|
|
|
for(itBorder=bottomBorders.begin(); itBorder!=bottomBorders.end(); itBorder++)
|
|
{
|
|
const CVector2f initPoint = *itBorder;
|
|
sint32 x = (sint32)initPoint.x - xmin;
|
|
sint32 y = (sint32)initPoint.y - ymin;
|
|
sint32 maxBorder = 160;
|
|
if(x<0)
|
|
{
|
|
maxBorder += x;
|
|
x = 0;
|
|
}
|
|
sint32 initx = x;
|
|
|
|
while(x<(initx+maxBorder) && x<(sint32)width)
|
|
{
|
|
double noiseValue = (1+cos(((x-initx)*2*Pi)/maxBorder))*5;
|
|
double evalNoise = 10 + noiseValue;
|
|
double diffAlpha = 255/evalNoise;
|
|
|
|
CRGBA color = islandBitmap.getPixelColor(x, height-y-1);
|
|
sint32 currentY = y-1;
|
|
|
|
while((currentY>=y-evalNoise) && currentY>=0)
|
|
{
|
|
uint8 *pixel = &(islandBitmap.getPixels(0)[((height-currentY-1)*width + x)*4]);
|
|
uint alpha = (uint)(255-diffAlpha*(y-currentY));
|
|
uint invAlpha = 255-alpha;
|
|
|
|
*pixel = (uint8) (((invAlpha * *pixel) + (alpha * color.R)) >> 8);
|
|
*(pixel + 1) = (uint8) (((invAlpha * *(pixel + 1)) + (alpha * color.G)) >> 8);
|
|
*(pixel + 2) = (uint8) (((invAlpha * *(pixel + 2)) + (alpha * color.B)) >> 8);
|
|
*(pixel + 3) = (uint8) 255;
|
|
|
|
currentY--;
|
|
}
|
|
|
|
x++;
|
|
}
|
|
}
|
|
|
|
for(itBorder=topBorders.begin(); itBorder!=topBorders.end(); itBorder++)
|
|
{
|
|
const CVector2f initPoint = *itBorder;
|
|
sint32 x = (sint32)initPoint.x - xmin;
|
|
sint32 y = (sint32)initPoint.y - ymin;
|
|
sint32 maxBorder = 160;
|
|
if(x<0)
|
|
{
|
|
maxBorder += x;
|
|
x = 0;
|
|
}
|
|
sint32 initx = x;
|
|
|
|
while(x<(initx+maxBorder) && x<(sint32)width)
|
|
{
|
|
double noiseValue = (1+cos(((x-initx)*2*Pi)/maxBorder))*5;
|
|
double evalNoise = 10 + noiseValue;
|
|
double diffAlpha = 255/evalNoise;
|
|
|
|
CRGBA color = islandBitmap.getPixelColor(x, height-y-1);
|
|
sint32 currentY = y+1;
|
|
|
|
while((currentY<=y+evalNoise) && currentY<height)
|
|
{
|
|
uint8 *pixel = &(islandBitmap.getPixels(0)[((height-currentY-1)*width + x)*4]);
|
|
uint alpha = (uint)(255-diffAlpha*(currentY-y));
|
|
uint invAlpha = 255-alpha;
|
|
|
|
*pixel = (uint8) (((invAlpha * *pixel) + (alpha * color.R)) >> 8);
|
|
*(pixel + 1) = (uint8) (((invAlpha * *(pixel + 1)) + (alpha * color.G)) >> 8);
|
|
*(pixel + 2) = (uint8) (((invAlpha * *(pixel + 2)) + (alpha * color.B)) >> 8);
|
|
*(pixel + 3) = (uint8) 255;
|
|
|
|
currentY++;
|
|
}
|
|
|
|
x++;
|
|
}
|
|
}
|
|
}
|
|
|
|
//-------------------------------------------------------------------------------------------------
|
|
void CScreenshotIslands::buildScreenshots()
|
|
{
|
|
init();
|
|
buildIslandsTextures();
|
|
}
|
|
|
|
//-------------------------------------------------------------------------------------------------
|
|
void CScreenshotIslands::writeProximityBufferToTgaFile(const std::string& fileName,const TBuffer& buffer,uint32 scanWidth,uint32 scanHeight)
|
|
{
|
|
uint imageWidth = (scanWidth); // (scanWidth+15)&~15;
|
|
uint imageHeight = (scanHeight);
|
|
|
|
CTGAImageGrey tgaImage;
|
|
tgaImage.setup((uint16)imageWidth, (uint16)imageHeight, fileName, 0, 0);
|
|
for (uint32 y=0;y<scanHeight;++y)
|
|
{
|
|
for (uint32 x=0; x<scanWidth; ++x)
|
|
{
|
|
uint32 value= buffer[y*scanWidth+x];
|
|
tgaImage.set(x, (value>255*5)?255:value/5);
|
|
}
|
|
tgaImage.writeLine();
|
|
}
|
|
}
|
|
|
|
//-------------------------------------------------------------------------------------------------
|
|
void CScreenshotIslands::processProximityBuffer(TBuffer & inputBuffer, uint32 lineLength, TBuffer& resultBuffer)
|
|
{
|
|
// a couple of constants to control the range over which our degressive filter is to be applied
|
|
const uint32 smallValue= 2*5;
|
|
|
|
float a = 5*((255.0 - limitValue)/(float(100-BigValue)));
|
|
float b = (float)(limitValue*5 - a*BigValue);
|
|
|
|
// determine numer of lines in the buffer...
|
|
uint32 numLines= inputBuffer.size()/ lineLength;
|
|
|
|
// clear out the result buffer and reset all values to 5*255, remembering that this is the correct value for the image edges
|
|
resultBuffer.clear();
|
|
resultBuffer.resize(inputBuffer.size(),(TBufferEntry)5*255);
|
|
|
|
for (uint32 y=1;y<numLines-1;++y)
|
|
{
|
|
uint32 lineOffset= y* lineLength;
|
|
for (uint32 x=1;x<lineLength-1;++x)
|
|
{
|
|
uint32 offset= lineOffset+x;
|
|
|
|
uint32 value=(uint32)inputBuffer[offset];
|
|
|
|
// apply a clip and cosine function
|
|
/*
|
|
if (value<smallValue) value=0;
|
|
else if (value>BigValue) value=5*255;
|
|
else value= (uint32)(((1.0-cos(Pi*(float(value-smallValue)/(float)(BigValue-smallValue))))/2.0)*float(5*255));
|
|
*/
|
|
|
|
if (value==ValueBorder);
|
|
else if ((value>=0) && (value<smallValue)) value=0;
|
|
else if (value>BigValue)
|
|
{
|
|
if(value==InteriorValue)
|
|
{
|
|
value = (uint)(5*limitValue);
|
|
}
|
|
else
|
|
{
|
|
value = (uint)(a*value+b);
|
|
if(value > 5*255) value = 5*255;
|
|
}
|
|
}
|
|
else if((value>=smallValue) && (value<=BigValue))
|
|
{
|
|
value= (uint32)(((1.0-cos(Pi*(float(value-smallValue)/(float)(BigValue-smallValue))))/2.0)*float(5*limitValue));
|
|
}
|
|
|
|
// store the value into the result buffer
|
|
resultBuffer[offset]= (TBufferEntry)value;
|
|
}
|
|
}
|
|
|
|
// modify inputBuffer to store "bigValue" limit
|
|
for (uint32 y=1;y<numLines-1;++y)
|
|
{
|
|
uint32 lineOffset= y* lineLength;
|
|
for (uint32 x=1;x<lineLength-1;++x)
|
|
{
|
|
uint32 offset= lineOffset+x;
|
|
|
|
uint32 value=(uint32)inputBuffer[offset];
|
|
|
|
if(value==BigValue) value = 255*5;
|
|
else value = 0;
|
|
|
|
inputBuffer[offset]= (TBufferEntry)value;
|
|
}
|
|
}
|
|
|
|
/*
|
|
// lines
|
|
for (uint32 y=0;y<numLines;++y)
|
|
{
|
|
uint32 lineOffset= y* lineLength;
|
|
|
|
for (uint32 x=0;x<lineLength;++x)
|
|
{
|
|
uint32 offset = lineOffset+x;
|
|
uint16 value = resultBuffer[offset];
|
|
|
|
if(value==ValueBorder)
|
|
{
|
|
resultBuffer[offset]= (TBufferEntry)(5*limitValue);
|
|
|
|
// increase x
|
|
if(x+1<lineLength-1)
|
|
{
|
|
uint16 nextValue = resultBuffer[offset+1];
|
|
uint16 prevValue = (TBufferEntry)(5*limitValue);
|
|
if(nextValue!=(TBufferEntry)(5*limitValue))
|
|
{
|
|
uint32 count = x+1;
|
|
while((count<x+7) && (count<lineLength-1) && ((nextValue=resultBuffer[lineOffset+count])!=0)
|
|
&& nextValue!=(TBufferEntry)(5*limitValue) && (nextValue!=ValueBorder))
|
|
{
|
|
uint16 newValue = (TBufferEntry)(prevValue+5*7);
|
|
if(newValue < nextValue)
|
|
{
|
|
resultBuffer[lineOffset+count] = newValue;
|
|
}
|
|
|
|
prevValue = newValue;
|
|
count++;
|
|
}
|
|
|
|
x = count-1;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// decrease x
|
|
if(x-1>0)
|
|
{
|
|
uint16 nextValue = resultBuffer[offset-1];
|
|
uint16 prevValue = (TBufferEntry)(5*limitValue);
|
|
if(nextValue!=(TBufferEntry)(5*limitValue))
|
|
{
|
|
uint32 count = x-1;
|
|
while((count>x-7) && (count>0) && ((nextValue=resultBuffer[lineOffset+count])!=0)
|
|
&& nextValue!=(TBufferEntry)(5*limitValue) && (nextValue!=ValueBorder))
|
|
{
|
|
uint16 newValue = (TBufferEntry)(prevValue+5*7);
|
|
if(newValue < nextValue)
|
|
{
|
|
resultBuffer[lineOffset+count] = newValue;
|
|
}
|
|
|
|
prevValue = newValue;
|
|
count--;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
//columns
|
|
for (uint32 x=0;x<lineLength;++x)
|
|
{
|
|
// setup start and end offsets marking first and last 'zero' value pixels in the column
|
|
uint32 startOffset=x, endOffset=x+(numLines-1)*lineLength;
|
|
|
|
for (uint32 offset=startOffset; offset<=endOffset; offset+=lineLength)
|
|
{
|
|
uint16 value = resultBuffer[offset];
|
|
|
|
if(value==ValueBorder)
|
|
{
|
|
resultBuffer[offset]= (TBufferEntry)(5*limitValue);
|
|
|
|
// increase y
|
|
if(offset+lineLength<=endOffset)
|
|
{
|
|
uint16 nextValue = resultBuffer[offset+lineLength];
|
|
uint16 prevValue = (TBufferEntry)(5*limitValue);
|
|
if(nextValue!=(TBufferEntry)(5*limitValue))
|
|
{
|
|
uint32 count = offset+lineLength;
|
|
while((count<offset+7*lineLength) && (count<=endOffset) &&
|
|
((nextValue=resultBuffer[count])!=0) && nextValue!=(TBufferEntry)(5*limitValue) && (nextValue!=ValueBorder))
|
|
{
|
|
uint16 newValue = (TBufferEntry)(prevValue+5*7);
|
|
if(newValue < nextValue)
|
|
{
|
|
resultBuffer[count] = newValue;
|
|
}
|
|
|
|
prevValue = newValue;
|
|
count += lineLength;
|
|
}
|
|
|
|
offset = count-lineLength;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// decrease y
|
|
if(offset-lineLength>=startOffset)
|
|
{
|
|
uint16 nextValue = resultBuffer[offset-lineLength];
|
|
uint16 prevValue = (TBufferEntry)(5*limitValue);
|
|
if(nextValue!=(TBufferEntry)(5*limitValue))
|
|
{
|
|
uint32 count = offset-lineLength;
|
|
while((count>offset-7*lineLength) && (count>=startOffset) &&
|
|
((nextValue=resultBuffer[count])!=0) && nextValue!=(TBufferEntry)(5*limitValue) && (nextValue!=ValueBorder))
|
|
{
|
|
uint16 newValue = (TBufferEntry)(prevValue+5*7);
|
|
if(newValue < nextValue)
|
|
{
|
|
resultBuffer[count] = newValue;
|
|
}
|
|
|
|
prevValue = newValue;
|
|
count -= lineLength;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
*/
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// search for pixels of borders
|
|
map< CVector2f, bool > bordersPixels;
|
|
for (uint32 y=0;y<numLines;++y)
|
|
{
|
|
uint32 lineOffset= y* lineLength;
|
|
|
|
for (uint32 x=0;x<lineLength;++x)
|
|
{
|
|
uint32 offset = lineOffset+x;
|
|
uint16 value = resultBuffer[offset];
|
|
|
|
if(value==ValueBorder)
|
|
{
|
|
bordersPixels[CVector2f((float)x, (float)y)] = true;
|
|
resultBuffer[offset] = (TBufferEntry)(5*limitValue);
|
|
}
|
|
}
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// search for global borders
|
|
// search on lines for top and bottom borders
|
|
map< CVector2f, uint32 > leftBorders, rightBorders, bottomBorders, topBorders;
|
|
for (uint32 y=0;y<numLines;++y)
|
|
{
|
|
uint32 lineOffset= y* lineLength;
|
|
|
|
bool lastValue = false;
|
|
CVector2f firstPixelBorder;
|
|
firstPixelBorder.set(0.f, 0.f);
|
|
uint32 nbPixelsBorder = 0;
|
|
|
|
for (uint32 x=0;x<lineLength;++x)
|
|
{
|
|
bool value = (bordersPixels.find(CVector2f((float)x, (float)y))!=bordersPixels.end());
|
|
|
|
if(value)
|
|
{
|
|
if(!lastValue)
|
|
{
|
|
firstPixelBorder = CVector2f((float)x, (float)y);
|
|
}
|
|
nbPixelsBorder ++;
|
|
}
|
|
else if(lastValue)
|
|
{
|
|
// store border line
|
|
if(nbPixelsBorder !=1) //column instead of line
|
|
{
|
|
bool top=false, bottom=false;
|
|
if(y>0)
|
|
{
|
|
for(uint32 xc=(uint32)firstPixelBorder.x; xc<(uint32)(firstPixelBorder.x+nbPixelsBorder); xc++)
|
|
{
|
|
if(resultBuffer[(y-1)*lineLength+xc]==(TBufferEntry)(5*255))
|
|
{
|
|
bottom = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if(y+1<numLines)
|
|
{
|
|
for(uint32 xc=(uint32)firstPixelBorder.x; xc<(uint32)(firstPixelBorder.x+nbPixelsBorder); xc++)
|
|
{
|
|
if(resultBuffer[(y+1)*lineLength+xc]==(TBufferEntry)(5*255))
|
|
{
|
|
top = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// TOP
|
|
if(top)
|
|
topBorders[firstPixelBorder] =nbPixelsBorder;
|
|
|
|
// BOTTOM
|
|
if(bottom)
|
|
bottomBorders[firstPixelBorder] =nbPixelsBorder;
|
|
}
|
|
nbPixelsBorder =0;
|
|
}
|
|
lastValue = value;
|
|
}
|
|
}
|
|
|
|
// search on columns for left and right borders
|
|
for (uint32 x=0;x<lineLength;++x)
|
|
{
|
|
bool lastValue = false;
|
|
CVector2f firstPixelBorder;
|
|
firstPixelBorder.set(0.f, 0.f);
|
|
uint32 nbPixelsBorder = 0;
|
|
|
|
for(uint32 y=0; y<numLines; y++)
|
|
{
|
|
bool value = (bordersPixels.find(CVector2f((float)x, (float)y))!=bordersPixels.end());
|
|
|
|
if(value)
|
|
{
|
|
if(!lastValue)
|
|
{
|
|
firstPixelBorder = CVector2f((float)x, (float)y);
|
|
}
|
|
nbPixelsBorder ++;
|
|
}
|
|
else if(lastValue)
|
|
{
|
|
// store border line
|
|
if(nbPixelsBorder !=1) //line instead of column
|
|
{
|
|
bool left = false;
|
|
bool right = false;
|
|
|
|
if(x>0)
|
|
{
|
|
for(uint32 yc=(uint32)firstPixelBorder.y; yc<(uint32)(firstPixelBorder.y+nbPixelsBorder); yc++)
|
|
{
|
|
if(resultBuffer[yc*lineLength+x-1]==(TBufferEntry)(5*255))
|
|
{
|
|
left = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if(x+1<lineLength)
|
|
{
|
|
for(uint32 yc=(uint32)firstPixelBorder.y; yc<(uint32)(firstPixelBorder.y+nbPixelsBorder); yc++)
|
|
{
|
|
if(resultBuffer[yc*lineLength+x+1]==(TBufferEntry)(5*255))
|
|
{
|
|
right = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// LEFT
|
|
if(left)
|
|
leftBorders[firstPixelBorder] =nbPixelsBorder;
|
|
|
|
// RIGHT
|
|
if(right)
|
|
rightBorders[firstPixelBorder] =nbPixelsBorder;
|
|
}
|
|
nbPixelsBorder =0;
|
|
}
|
|
lastValue = value;
|
|
}
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// attenuate borders
|
|
// bottom and top borders (lines)
|
|
for (uint32 y=0;y<numLines;++y)
|
|
{
|
|
uint32 lineOffset= y* lineLength;
|
|
|
|
for (uint32 x=0;x<lineLength;++x)
|
|
{
|
|
uint32 offset = lineOffset+x;
|
|
CVector2f firstPixelBorder((float)x, (float)y);
|
|
|
|
// BOTTOM
|
|
if(bottomBorders.find(CVector2f((float)x, (float)y))!=bottomBorders.end())
|
|
{
|
|
uint32 nbPixelsBorder = bottomBorders[firstPixelBorder];
|
|
uint32 xc;
|
|
for(xc=(uint32)firstPixelBorder.x; xc<(uint32)(firstPixelBorder.x+nbPixelsBorder); xc++)
|
|
{
|
|
uint nbZones = (uint)(nbPixelsBorder/160)+1;
|
|
uint period = (uint)(nbPixelsBorder/(2*nbZones));
|
|
double noiseValue = (1-cos(((xc-firstPixelBorder.x)*Pi)/period))*5;
|
|
double evalNoise = 7 + noiseValue;
|
|
double diffAlpha = (255.0-limitValue)/evalNoise;
|
|
|
|
uint32 yc = y-1;
|
|
while((yc>=(uint32)(y-evalNoise)) && yc>=0)
|
|
{
|
|
uint16 newValue = (TBufferEntry)(5*(limitValue + diffAlpha*(y-yc)));
|
|
uint16 currentValue = (TBufferEntry)resultBuffer[yc*lineLength+xc];
|
|
if(newValue<currentValue)
|
|
resultBuffer[yc*lineLength+xc] = (TBufferEntry)(5*(limitValue + diffAlpha*(y-yc)));
|
|
yc--;
|
|
}
|
|
}
|
|
x=xc;
|
|
}
|
|
|
|
// TOP
|
|
if(topBorders.find(CVector2f((float)x, (float)y))!=topBorders.end())
|
|
{
|
|
uint32 nbPixelsBorder = topBorders[firstPixelBorder];
|
|
uint32 xc;
|
|
for(xc=(uint32)firstPixelBorder.x; xc<=(uint32)(firstPixelBorder.x+nbPixelsBorder); xc++)
|
|
{
|
|
uint nbZones = (uint)(nbPixelsBorder/160)+1;
|
|
uint period = (uint)(nbPixelsBorder/(2*nbZones));
|
|
double noiseValue = (1-cos(((xc-firstPixelBorder.x)*Pi)/period))*5;
|
|
double evalNoise = 7 + noiseValue;
|
|
double diffAlpha = (255.0-limitValue)/evalNoise;
|
|
|
|
uint32 yc = y+1;
|
|
while((yc<=(uint32)(y+evalNoise)) && yc<numLines)
|
|
{
|
|
uint16 newValue = (TBufferEntry)(5*(limitValue + diffAlpha*(yc-y)));
|
|
uint16 currentValue = (TBufferEntry)resultBuffer[yc*lineLength+xc];
|
|
if(newValue<currentValue)
|
|
resultBuffer[yc*lineLength+xc] = (TBufferEntry)(5*(limitValue + diffAlpha*(yc-y)));
|
|
yc++;
|
|
}
|
|
}
|
|
x=xc;
|
|
}
|
|
}
|
|
}
|
|
|
|
// left and right borders (columns)
|
|
for (uint32 x=0;x<lineLength;++x)
|
|
{
|
|
for (uint32 y=0; y<numLines; y++)
|
|
{
|
|
CVector2f firstPixelBorder((float)x, (float)y);
|
|
|
|
// LEFT
|
|
if(leftBorders.find(CVector2f((float)x, (float)y))!=leftBorders.end())
|
|
{
|
|
uint32 nbPixelsBorder = leftBorders[firstPixelBorder];
|
|
uint32 yc;
|
|
for(yc=(uint32)firstPixelBorder.y; yc<(uint32)(firstPixelBorder.y+nbPixelsBorder); yc++)
|
|
{
|
|
uint nbZones = (uint)(nbPixelsBorder/160)+1;
|
|
uint period = (uint)(nbPixelsBorder/(2*nbZones));
|
|
double noiseValue = (1-cos(((yc-firstPixelBorder.y)*Pi)/period))*5;
|
|
double evalNoise = 7 + noiseValue;
|
|
double diffAlpha = (255.0-limitValue)/evalNoise;
|
|
|
|
uint32 xc = x-1;
|
|
while((xc>=(uint32)(x-evalNoise)) && xc>=0)
|
|
{
|
|
uint16 newValue = (TBufferEntry)(5*(limitValue + diffAlpha*(x-xc)));
|
|
uint16 currentValue = (TBufferEntry)resultBuffer[yc*lineLength+xc];
|
|
if(newValue<currentValue)
|
|
resultBuffer[yc*lineLength+xc] = (TBufferEntry)(5*(limitValue + diffAlpha*(x-xc)));
|
|
xc--;
|
|
}
|
|
}
|
|
y=yc;
|
|
}
|
|
|
|
// RIGHT
|
|
if(rightBorders.find(CVector2f((float)x, (float)y))!=rightBorders.end())
|
|
{
|
|
uint32 nbPixelsBorder = rightBorders[firstPixelBorder];
|
|
uint32 yc;
|
|
for(yc=(uint32)firstPixelBorder.y; yc<=(uint32)(firstPixelBorder.y+nbPixelsBorder); yc++)
|
|
{
|
|
uint nbZones = (uint)(nbPixelsBorder/160)+1;
|
|
uint period = (uint)(nbPixelsBorder/(2*nbZones));
|
|
double noiseValue = (1-cos(((yc-firstPixelBorder.y)*Pi)/period))*5;
|
|
double evalNoise = 7 + noiseValue;
|
|
double diffAlpha = (255.0-limitValue)/evalNoise;
|
|
|
|
uint32 xc = x+1;
|
|
while((xc<=(uint32)(x+evalNoise)) && xc<lineLength)
|
|
{
|
|
uint16 newValue = (TBufferEntry)(5*(limitValue + diffAlpha*(xc-x)));
|
|
uint16 currentValue = (TBufferEntry)resultBuffer[yc*lineLength+xc];
|
|
if(newValue<currentValue)
|
|
resultBuffer[yc*lineLength+xc] = (TBufferEntry)(5*(limitValue + diffAlpha*(xc-x)));
|
|
xc++;
|
|
}
|
|
}
|
|
y=yc;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
//--------------------------------------------------------------------------------
|
|
void CScreenshotIslands::loadIslands()
|
|
{
|
|
// load entryPoints
|
|
map< string, CVector2f > islands;
|
|
CScenarioEntryPoints scenarioEntryPoints = CScenarioEntryPoints::getInstance();
|
|
|
|
scenarioEntryPoints.loadFromFile();
|
|
const CScenarioEntryPoints::TEntryPoints& entryPoints = scenarioEntryPoints.getEntryPoints();
|
|
|
|
CScenarioEntryPoints::TEntryPoints::const_iterator entry(entryPoints.begin()), entryPoint(entryPoints.end());
|
|
for( ; entry != entryPoint ; ++entry)
|
|
{
|
|
islands[entry->Island] = CVector2f((float)entry->X, (float)entry->Y);
|
|
}
|
|
|
|
// search islands of each continent
|
|
map< string, CVector2f >::iterator itIsland(islands.begin()), lastIsland(islands.end());
|
|
for( ; itIsland != lastIsland ; ++itIsland)
|
|
{
|
|
const CVector2f & entryPoint = itIsland->second;
|
|
|
|
// search continent of this island
|
|
TContinentsData::iterator itCont(_ContinentsData.begin()), lastCont(_ContinentsData.end());
|
|
for( ; itCont != lastCont ; ++itCont)
|
|
{
|
|
CContinentData & continent = itCont->second;
|
|
CVector2f zoneMax = continent.ZoneMax;
|
|
CVector2f zoneMin = continent.ZoneMin;
|
|
|
|
if((zoneMin.x <= entryPoint.x) && (entryPoint.x <= zoneMax.x)
|
|
&& (zoneMax.y <= entryPoint.y) && (entryPoint.y <= zoneMin.y))
|
|
{
|
|
continent.Islands.push_back(itIsland->first);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// search data of each island
|
|
TContinentsData::iterator itCont(_ContinentsData.begin()), lastCont(_ContinentsData.end());
|
|
for( ; itCont != lastCont ; ++itCont)
|
|
{
|
|
string aiFileName = itCont->first+"_0.cwmap2";
|
|
|
|
const CContinentData & continent = itCont->second;
|
|
|
|
CProximityMapBuffer continentBuffer;
|
|
continentBuffer.load(CPath::lookup(aiFileName));
|
|
|
|
CProximityMapBuffer::TZones zones;
|
|
continentBuffer.calculateZones(zones);
|
|
|
|
for (uint32 i=0;i<zones.size();++i)
|
|
{
|
|
TBuffer zoneBuffer;
|
|
continentBuffer.generateZoneProximityMap(zones[i], zoneBuffer);
|
|
|
|
TBuffer cleanBuffer;
|
|
processProximityBuffer(zoneBuffer, zones[i].getZoneWidth(), cleanBuffer);
|
|
|
|
string fileName;
|
|
list< string >::const_iterator itIsland(continent.Islands.begin()), lastIsland(continent.Islands.end());
|
|
for( ; itIsland != lastIsland ; ++itIsland)
|
|
{
|
|
const CVector2f & entryPoint = islands[*itIsland];
|
|
sint32 xmin = zones[i].getBoundXMin();
|
|
sint32 xmax = zones[i].getBoundXMax();
|
|
sint32 ymin = zones[i].getBoundYMin();
|
|
sint32 ymax = zones[i].getBoundYMax();
|
|
|
|
if((xmin <= entryPoint.x) && (entryPoint.x <= xmax)
|
|
&& (ymin <= entryPoint.y) && (entryPoint.y <= ymax))
|
|
{
|
|
fileName = _OutDirectory + "/" + *itIsland + "_prox.tga";
|
|
_IslandsData[*itIsland] = zones[i];
|
|
break;
|
|
}
|
|
}
|
|
|
|
// write the processed proximity map to an output file
|
|
if(!fileName.empty())
|
|
{
|
|
writeProximityBufferToTgaFile(fileName, cleanBuffer, zones[i].getZoneWidth(), zones[i].getZoneHeight());
|
|
_TempFileNames.push_back(fileName);
|
|
|
|
fileName = _OutDirectory + "/" + *itIsland + "_limit.tga";
|
|
writeProximityBufferToTgaFile(fileName, zoneBuffer, zones[i].getZoneWidth(), zones[i].getZoneHeight());
|
|
_TempFileNames.push_back(fileName);
|
|
}
|
|
else
|
|
{
|
|
nlinfo("Zone of island not found, tga not build");
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
CScenarioEntryPoints::TCompleteIslands completeIslands(entryPoints.size());
|
|
|
|
uint completeIslandsNb = 0;
|
|
for(uint e=0; e<entryPoints.size(); e++)
|
|
{
|
|
const CScenarioEntryPoints::CEntryPoint & entry = entryPoints[e];
|
|
|
|
CScenarioEntryPoints::CCompleteIsland completeIsland;
|
|
completeIsland.Island = entry.Island;
|
|
completeIsland.Package = entry.Package;
|
|
|
|
for(itCont=_ContinentsData.begin(); itCont!=_ContinentsData.end(); ++itCont)
|
|
{
|
|
list< string >::const_iterator itIsland(itCont->second.Islands.begin()), lastIsland(itCont->second.Islands.end());
|
|
for( ; itIsland != lastIsland ; ++itIsland)
|
|
{
|
|
if(*itIsland == entry.Island)
|
|
{
|
|
completeIsland.Continent = CSString(itCont->first);
|
|
if(_IslandsData.find(entry.Island)!=_IslandsData.end())
|
|
{
|
|
completeIsland.XMin = _IslandsData[entry.Island].getBoundXMin();
|
|
completeIsland.YMin = _IslandsData[entry.Island].getBoundYMin();
|
|
completeIsland.XMax = _IslandsData[entry.Island].getBoundXMax();
|
|
completeIsland.YMax = _IslandsData[entry.Island].getBoundYMax();
|
|
completeIslands[completeIslandsNb] = completeIsland;
|
|
completeIslandsNb++;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
completeIslands.resize(completeIslandsNb);
|
|
|
|
CScenarioEntryPoints::getInstance().saveXMLFile(completeIslands, _CompleteIslandsFile);
|
|
}
|
|
|
|
//--------------------------------------------------------------------------------
|
|
|
|
void CScreenshotIslands::getBuffer(UScene * scene, ULandscape * landscape, CBitmap &btm)
|
|
{
|
|
//
|
|
if (ScreenShotWidth && ScreenShotHeight)
|
|
{
|
|
UCamera camera = scene->getCam();
|
|
|
|
// Destination image
|
|
CBitmap dest;
|
|
btm.resize(ScreenShotWidth, ScreenShotHeight, CBitmap::RGBA);
|
|
|
|
uint windowWidth = driver->getWindowWidth();
|
|
uint windowHeight = driver->getWindowHeight();
|
|
|
|
uint top;
|
|
uint bottom = min(windowHeight, ScreenShotHeight);
|
|
for (top=0; top<ScreenShotHeight; top+=windowHeight)
|
|
{
|
|
uint left;
|
|
uint right = std::min(windowWidth, ScreenShotWidth);
|
|
for(left=0; left<ScreenShotWidth; left+=windowWidth)
|
|
{
|
|
driver->clearBuffers(_BackColor);
|
|
|
|
// store initial frustum and viewport
|
|
CFrustum Frustum = scene->getCam().getFrustum();
|
|
CViewport Viewport = scene->getViewport();
|
|
|
|
// Build a new frustum
|
|
CFrustum frustumPart;
|
|
frustumPart.Left = Frustum.Left+(Frustum.Right-Frustum.Left)*((float)left/(float)ScreenShotWidth);
|
|
frustumPart.Right = Frustum.Left+(Frustum.Right-Frustum.Left)*((float)right/(float)ScreenShotWidth);
|
|
frustumPart.Top = ceil(Frustum.Top+(Frustum.Bottom-Frustum.Top)*((float)top/(float)ScreenShotHeight));
|
|
frustumPart.Bottom = ceil(Frustum.Top+(Frustum.Bottom-Frustum.Top)*((float)bottom/(float)ScreenShotHeight));
|
|
frustumPart.Near = Frustum.Near;
|
|
frustumPart.Far = Frustum.Far;
|
|
frustumPart.Perspective = Frustum.Perspective;
|
|
|
|
// Build a new viewport
|
|
CViewport viewport;
|
|
viewport.init(0, 0, (float)(right-left)/windowWidth,
|
|
(float)(bottom-top)/windowHeight);
|
|
|
|
// Activate all this
|
|
scene->getCam().setFrustum(frustumPart);
|
|
scene->setViewport(viewport);
|
|
|
|
scene->setMaxSkeletonsInNotCLodForm(1000000);
|
|
scene->setPolygonBalancingMode(UScene::PolygonBalancingOff);
|
|
|
|
if(_InverseZTest)
|
|
{
|
|
// render scene with inversed ZBuffer test (keep greater distances)
|
|
driver->setColorMask(false, false, false, false);
|
|
sceneMaterial.setZFunc(UMaterial::less);
|
|
|
|
// initialize ZBuffer with leak value
|
|
driver->setMatrixMode2D11();
|
|
CQuad quad;
|
|
quad.V0 = CVector(0.0, 0.0, 0.0);
|
|
quad.V1 = CVector(1.0, 0.0, 0.0);
|
|
quad.V2 = CVector(1.0, 1.0, 0.0);
|
|
quad.V3 = CVector(0.0, 1.0, 0.0);
|
|
|
|
driver->drawQuad(quad, sceneMaterial);
|
|
driver->setMatrixMode3D(camera);
|
|
driver->setColorMask(true, true, true, true);
|
|
|
|
scene->enableElementRender(UScene::FilterWater, false);
|
|
}
|
|
|
|
scene->render();
|
|
|
|
// display vegetables with normal ZBuffer test
|
|
if(_InverseZTest && _Vegetation)
|
|
{
|
|
scene->enableElementRender(UScene::FilterWater, false);
|
|
scene->enableElementRender(UScene::FilterLandscape, false);
|
|
scene->enableElementRender(UScene::FilterWater, true);
|
|
scene->render();
|
|
scene->enableElementRender(UScene::FilterLandscape, true);
|
|
}
|
|
|
|
// Get the bitmap
|
|
driver->getBuffer(dest);
|
|
btm.blit(dest, 0, windowHeight-(bottom-top), right-left, bottom-top, left, top);
|
|
|
|
// restore camera
|
|
scene->getCam().setFrustum(Frustum);
|
|
scene->setViewport(Viewport);
|
|
|
|
driver->flush();
|
|
driver->swapBuffers();
|
|
|
|
// Next
|
|
right = std::min(right+windowWidth, ScreenShotWidth);
|
|
}
|
|
|
|
// Next
|
|
bottom = std::min(bottom+windowHeight, ScreenShotHeight);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
driver->getBuffer(btm);
|
|
}
|
|
}
|
|
|
|
|
|
|
|
void CScreenshotIslands::buildIslandsTextures()
|
|
{
|
|
int loop = 0;
|
|
int maxLoop = 6;
|
|
|
|
// Create the window with config file values
|
|
driver->setDisplay(UDriver::CMode(512, 512, 32, true));
|
|
|
|
// Create a scene
|
|
UScene * scene = driver->createScene(true);
|
|
scene->animate(CTime::ticksToSecond(CTime::getPerformanceTime()));
|
|
scene->setMaxSkeletonsInNotCLodForm(1000000);
|
|
scene->setPolygonBalancingMode(UScene::PolygonBalancingOff);
|
|
|
|
// Create a camera
|
|
UCamera camera = scene->getCam();
|
|
camera.setTransformMode(UTransformable::DirectMatrix);
|
|
|
|
// Create and load landscape
|
|
ULandscape * landscape = scene->createLandscape();
|
|
landscape->setThreshold(0.0005);
|
|
if(_InverseZTest)
|
|
{
|
|
landscape->setZFunc(UMaterial::greaterequal);
|
|
}
|
|
else
|
|
{
|
|
landscape->setZFunc(UMaterial::less);
|
|
}
|
|
|
|
//Iteration on seasons
|
|
list< string >::iterator itSeason(_SeasonSuffixes.begin()), lastSeason(_SeasonSuffixes.end());
|
|
for( ; itSeason != lastSeason ; ++itSeason)
|
|
{
|
|
string seasonSuffix = *itSeason;
|
|
|
|
sint season = -1;
|
|
|
|
if (seasonSuffix == "_sp") season = CSeason::Spring;
|
|
else if (seasonSuffix == "_su") season = CSeason::Summer;
|
|
else if (seasonSuffix == "_au") season = CSeason::Autumn;
|
|
else if (seasonSuffix == "_wi") season = CSeason::Winter;
|
|
|
|
if (season == -1)
|
|
{
|
|
nlwarning("Unknown season suffix %s, skipping...", seasonSuffix.c_str());
|
|
continue;
|
|
}
|
|
|
|
// Iterations on Continents
|
|
TContinentsData::iterator itCont(_ContinentsData.begin()), lastCont(_ContinentsData.end());
|
|
for( ; itCont != lastCont ; ++itCont)
|
|
{
|
|
const CContinentData & continent = itCont->second;
|
|
|
|
// Light init
|
|
landscape->setupStaticLight(continent.Diffuse, continent.Ambiant, 1.0f);
|
|
|
|
string coarseMeshFile = continent.CoarseMeshMap;
|
|
string coarseMeshWithoutExt = CFile::getFilenameWithoutExtension(coarseMeshFile);
|
|
string coarseMeshExt = CFile::getExtension(coarseMeshFile);
|
|
coarseMeshFile = coarseMeshWithoutExt + seasonSuffix + "." + coarseMeshExt;
|
|
nldebug("Coarse mesh texture: '%s'", coarseMeshFile.c_str());
|
|
scene->setCoarseMeshManagerTexture(coarseMeshFile.c_str());
|
|
|
|
// Load the landscape
|
|
string farBank = continent.FarBank;
|
|
string farBankWithoutExt = CFile::getFilenameWithoutExtension(farBank);
|
|
string farBankExt = CFile::getExtension(farBank);
|
|
farBank = farBankWithoutExt + seasonSuffix + "." + farBankExt;
|
|
landscape->loadBankFiles(continent.SmallBank, farBank);
|
|
|
|
// Load vegatables
|
|
LandscapeIGManager.initIG(scene, continent.IGFile, driver, season, NULL);
|
|
|
|
// Iterations on Islands
|
|
list< string >::const_iterator itIsland(continent.Islands.begin()), lastIsland(continent.Islands.end());
|
|
for( ; itIsland != lastIsland ; ++itIsland)
|
|
{
|
|
loop = 0;
|
|
|
|
if(_IslandsData.find(itIsland->c_str()) != _IslandsData.end())
|
|
{
|
|
const CProximityZone & islandData = _IslandsData[itIsland->c_str()];
|
|
|
|
sint32 xmin = islandData.getBoundXMin();
|
|
sint32 xmax = islandData.getBoundXMax();
|
|
sint32 ymin = islandData.getBoundYMin();
|
|
sint32 ymax = islandData.getBoundYMax();
|
|
|
|
float width = (float)(xmax-xmin);
|
|
float height = (float)(ymax-ymin);
|
|
ScreenShotWidth = (uint)(width*_MeterPixelSize);
|
|
ScreenShotHeight = (uint)(height*_MeterPixelSize);
|
|
|
|
// position in island center
|
|
float posx = ((float)(xmax+xmin))/2;
|
|
float posy = ((float)(ymax+ymin))/2;
|
|
CVector entryPos(posx, posy, 400);
|
|
|
|
// Setup camera
|
|
CMatrix startCamMatrix;
|
|
startCamMatrix.setPos(entryPos);
|
|
startCamMatrix.rotateX(-(float)Pi/2);
|
|
camera.setMatrix(startCamMatrix);
|
|
camera.setFrustum(width, height, -10000.0f, 10000.0f, false);
|
|
|
|
// init lanscape
|
|
landscape->postfixTileFilename(seasonSuffix.c_str());
|
|
|
|
while(loop<maxLoop)
|
|
{
|
|
// refresh lanscape
|
|
vector<string> zonesAdded;
|
|
vector<string> zonesRemoved;
|
|
IProgressCallback progress;
|
|
landscape->refreshAllZonesAround(camera.getMatrix().getPos(), 1000, zonesAdded, zonesRemoved, progress);
|
|
if(_Vegetation)
|
|
{
|
|
LandscapeIGManager.unloadArrayZoneIG(zonesRemoved);
|
|
LandscapeIGManager.loadArrayZoneIG(zonesAdded);
|
|
|
|
vector<string>::iterator itName(zonesAdded.begin()), lastName(zonesAdded.end());
|
|
for( ; itName != lastName ; ++itName)
|
|
{
|
|
UInstanceGroup * instanceGr = LandscapeIGManager.getIG(*itName);
|
|
if(instanceGr)
|
|
{
|
|
uint nbInst = instanceGr->getNumInstance();
|
|
for(uint i=0; i<instanceGr->getNumInstance(); i++)
|
|
{
|
|
instanceGr->setCoarseMeshDist(i, 100000);
|
|
instanceGr->setDistMax(i, 100000);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
scene->animate(CTime::ticksToSecond(CTime::getPerformanceTime()));
|
|
|
|
// Clear all buffers
|
|
driver->clearBuffers(_BackColor);
|
|
|
|
if(_InverseZTest)
|
|
{
|
|
// render scene with inversed ZBuffer test (keep greater distances)
|
|
driver->setColorMask(false, false, false, false);
|
|
sceneMaterial.setZFunc(UMaterial::less);
|
|
|
|
// initialize ZBuffer with leak value
|
|
driver->setMatrixMode2D11();
|
|
CQuad quad;
|
|
quad.V0 = CVector(0.0, 0.0, 0.0);
|
|
quad.V1 = CVector(1.0, 0.0, 0.0);
|
|
quad.V2 = CVector(1.0, 1.0, 0.0);
|
|
quad.V3 = CVector(0.0, 1.0, 0.0);
|
|
|
|
driver->drawQuad(quad, sceneMaterial);
|
|
driver->setMatrixMode3D(camera);
|
|
driver->setColorMask(true, true, true, true);
|
|
|
|
scene->enableElementRender(UScene::FilterWater, false);
|
|
}
|
|
|
|
scene->render();
|
|
|
|
// display vegetables with normal ZBuffer test
|
|
if(_InverseZTest && _Vegetation)
|
|
{
|
|
scene->enableElementRender(UScene::FilterLandscape, false);
|
|
scene->enableElementRender(UScene::FilterWater, true);
|
|
scene->render();
|
|
scene->enableElementRender(UScene::FilterLandscape, true);
|
|
}
|
|
|
|
// Swap 3d buffers
|
|
driver->flush();
|
|
driver->swapBuffers();
|
|
|
|
// Pump user input messages
|
|
driver->EventServer.pump();
|
|
|
|
loop += 1;
|
|
|
|
// Screenshot
|
|
if(loop==maxLoop-1)
|
|
{
|
|
CBitmap islandBitmap;
|
|
getBuffer(scene, landscape, islandBitmap);
|
|
|
|
buildBackTextureHLS(*itIsland, islandBitmap);
|
|
}
|
|
if(loop==maxLoop)
|
|
{
|
|
// create srcennshot bitmap of full island
|
|
CBitmap islandBitmap;
|
|
getBuffer(scene, landscape, islandBitmap);
|
|
|
|
attenuateIslandBorders(*itIsland, islandBitmap, islandData);
|
|
|
|
// load proximity bitmap
|
|
CBitmap proxBitmap;
|
|
std::string proxFileName = _OutDirectory + "/" + *itIsland + "_prox.tga";
|
|
CIFile proxFS(proxFileName.c_str());
|
|
proxBitmap.load(proxFS);
|
|
|
|
// resize proximity bitmap
|
|
CBitmap tempBitmap;
|
|
int newWidth = islandBitmap.getWidth();
|
|
int newHeight = islandBitmap.getHeight();
|
|
tempBitmap.resize(newWidth, newHeight, islandBitmap.PixelFormat);
|
|
// blit src bitmap
|
|
//tempBitmap.blit(proxBitmap, 0, 0, newWidth, newHeight, 0, 0);
|
|
{
|
|
const uint8 *prox = &(proxBitmap.getPixels(0)[0]);
|
|
uint8 *temp = &(tempBitmap.getPixels(0)[0]);
|
|
for (uint y = 0; y < newHeight; ++y)
|
|
for (uint x = 0; x < newWidth; ++x)
|
|
{
|
|
uint ys = (y * proxBitmap.getHeight()) / newHeight;
|
|
uint xs = (x * proxBitmap.getWidth()) / newWidth;
|
|
uint addr = ((y * newWidth) + x) * 4;
|
|
uint addrs = ((ys * proxBitmap.getWidth()) + xs) * 4;
|
|
temp[addr] = prox[addrs];
|
|
temp[addr+1] = prox[addrs+1];
|
|
temp[addr+2] = prox[addrs+2];
|
|
temp[addr+3] = prox[addrs+3];
|
|
}
|
|
}
|
|
|
|
// swap them
|
|
proxBitmap.resize(newWidth, newHeight, proxBitmap.PixelFormat);
|
|
proxBitmap.swap(tempBitmap);
|
|
|
|
//proxBitmap.resample(newWidth, newHeight);
|
|
|
|
|
|
// create final bitmap
|
|
CBitmap bitmapDest;
|
|
bitmapDest.resize(islandBitmap.getWidth(), islandBitmap.getHeight(), islandBitmap.PixelFormat);
|
|
|
|
|
|
// mix black and full island bitmaps with blend factor of proximity bitmap pixels
|
|
uint numPix = islandBitmap.getWidth() * islandBitmap.getHeight();
|
|
|
|
const uint8 *alphaProx = &(proxBitmap.getPixels(0)[0]);
|
|
const uint8 *srcIsland = &(islandBitmap.getPixels(0)[0]);
|
|
uint8 *dest = &(bitmapDest.getPixels(0)[0]);
|
|
|
|
|
|
const uint8 *srcBack = &(_BackBitmap.getPixels(0)[0]);
|
|
|
|
uint8 *endDest = dest + (numPix << 2);
|
|
|
|
do
|
|
{
|
|
uint invblendFact = (uint) alphaProx[0];
|
|
uint blendFact = 256 - invblendFact;
|
|
|
|
// blend 4 component at each pass
|
|
*dest = (uint8) (((blendFact * *srcIsland) + (invblendFact * *srcBack)) >> 8);
|
|
*(dest + 1) = (uint8) (((blendFact * *(srcIsland + 1)) + (invblendFact * *(srcBack + 1))) >> 8);
|
|
*(dest + 2) = (uint8) (((blendFact * *(srcIsland + 2)) + (invblendFact * *(srcBack + 2))) >> 8);
|
|
*(dest + 3) = (uint8) 255;
|
|
|
|
alphaProx = alphaProx + 4;
|
|
srcIsland = srcIsland + 4;
|
|
dest = dest + 4;
|
|
|
|
srcBack = srcBack + 4;
|
|
}
|
|
while(dest != endDest);
|
|
|
|
|
|
// create tga file of avoidable place in island
|
|
string textureName = _OutDirectory + "/" + *itIsland + seasonSuffix + ".tga";
|
|
|
|
CBitmap bitmapLittle;
|
|
bitmapLittle.resize(bitmapDest.getWidth(), bitmapDest.getHeight(), bitmapDest.PixelFormat);
|
|
bitmapLittle = bitmapDest;
|
|
if(!isPowerOf2(bitmapDest.getWidth()) || !isPowerOf2(bitmapDest.getHeight()) )
|
|
{
|
|
uint pow2w = NLMISC::raiseToNextPowerOf2(bitmapDest.getWidth());
|
|
uint pow2h = NLMISC::raiseToNextPowerOf2(bitmapDest.getHeight());
|
|
CBitmap enlargedBitmap;
|
|
enlargedBitmap.resize(pow2w, pow2h, bitmapDest.PixelFormat);
|
|
// blit src bitmap
|
|
enlargedBitmap.blit(&bitmapDest, 0, 0);
|
|
// swap them
|
|
bitmapDest.swap(enlargedBitmap);
|
|
}
|
|
|
|
COFile fsDest(textureName.c_str());
|
|
bitmapDest.writeTGA(fsDest,32);
|
|
|
|
|
|
// little tga
|
|
bitmapLittle.resample(bitmapLittle.getWidth()/10, bitmapLittle.getHeight()/10);
|
|
if(!isPowerOf2(bitmapLittle.getWidth()) || !isPowerOf2(bitmapLittle.getHeight()) )
|
|
{
|
|
uint pow2w = NLMISC::raiseToNextPowerOf2(bitmapLittle.getWidth());
|
|
uint pow2h = NLMISC::raiseToNextPowerOf2(bitmapLittle.getHeight());
|
|
CBitmap enlargedBitmap;
|
|
enlargedBitmap.resize(pow2w, pow2h, bitmapLittle.PixelFormat);
|
|
// blit src bitmap
|
|
enlargedBitmap.blit(&bitmapLittle, 0, 0);
|
|
// swap them
|
|
bitmapLittle.swap(enlargedBitmap);
|
|
}
|
|
|
|
textureName = _OutDirectory + "/" + *itIsland + seasonSuffix + "_little.tga";
|
|
COFile fsLittle(textureName.c_str());
|
|
bitmapLittle.writeTGA(fsLittle,32);
|
|
|
|
_BackColor = CRGBA(255, 255, 255, 255);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
LandscapeIGManager.reset();
|
|
landscape->removeAllZones();
|
|
}
|
|
}
|
|
|
|
// remove proximity tga
|
|
list<string>::iterator itProx(_TempFileNames.begin()), lastProx(_TempFileNames.end());
|
|
for( ; itProx != lastProx ; ++itProx)
|
|
{
|
|
CFile::deleteFile(*itProx);
|
|
};
|
|
}
|
|
|
|
//--------------------------------------------------------------------------------
|
|
inline bool RGB2HSV(const CRGBA & rgba, uint & Hue, uint & Sat, uint & Val)
|
|
{
|
|
double Min_, Max_, Delta, H, S, V;
|
|
|
|
Min_ = min(min(rgba.R, rgba.G), rgba.B);
|
|
Max_ = max(max(rgba.R, rgba.G), rgba.B);
|
|
Delta = ( Max_ - Min_);
|
|
|
|
if(Max_ != 0.0)
|
|
{
|
|
S = 255.0*Delta/Max_;
|
|
}
|
|
else
|
|
{
|
|
Hue = 0;
|
|
Sat = 0;
|
|
Val = 0;
|
|
return false;
|
|
}
|
|
|
|
H = 0.0;
|
|
V = Max_;
|
|
|
|
if(rgba.R == Max_)
|
|
{
|
|
H = (rgba.G - rgba.B) / Delta;
|
|
}
|
|
else if(rgba.G == Max_)
|
|
{
|
|
H = 2.0 + (rgba.B - rgba.R) / Delta;
|
|
}
|
|
else
|
|
{
|
|
H = 4.0 + (rgba.R - rgba.G) / Delta;
|
|
}
|
|
|
|
H = H * 60;
|
|
if(H < 0.0)
|
|
{
|
|
H = H + 360.0;
|
|
}
|
|
|
|
Hue = (uint)H ; // Hue -> 0..360
|
|
Sat = (uint)S * 100 / 255; // Saturation -> 0..100 %
|
|
Val = (uint)V * 100 / 255; // Value - > 0..100 %
|
|
|
|
return true;
|
|
}
|
|
|
|
//-------------------------------------------------------------------------------------------------
|
|
inline bool infHLS(const CRGBA & rgba1, const CRGBA & rgba2)
|
|
{
|
|
uint h1, s1, v1, h2, s2, v2;
|
|
RGB2HSV(rgba1, h1, s1, v1);
|
|
RGB2HSV(rgba2, h2, s2, v2);
|
|
|
|
if(h1 != h2)
|
|
{
|
|
return (h1 < h2);
|
|
}
|
|
else if(s1 != s2)
|
|
{
|
|
return (s1 < s2);
|
|
}
|
|
else
|
|
{
|
|
return (v1 < v2);
|
|
}
|
|
}
|
|
|
|
//-------------------------------------------------------------------------------------------------
|
|
void CScreenshotIslands::buildBackTextureHLS(const std::string & islandName, const CBitmap & islandBitmap)
|
|
{
|
|
// load limit bitmap
|
|
CBitmap limBitmap;
|
|
std::string limFileName = _OutDirectory + "/" + islandName + "_limit.tga";
|
|
|
|
CIFile limFS(limFileName.c_str());
|
|
limBitmap.load(limFS);
|
|
|
|
list< CRGBA > limitPixels;
|
|
|
|
// search for colors of limit pixels
|
|
for(uint x=0; x<islandBitmap.getWidth(); x++)
|
|
{
|
|
for(uint y=0; y<islandBitmap.getHeight(); y++)
|
|
{
|
|
CRGBA lim = limBitmap.getPixelColor(x, y);
|
|
if((lim == CRGBA::White) && (islandBitmap.getPixelColor(x, y)!=CRGBA::White))
|
|
{
|
|
limitPixels.push_back(islandBitmap.getPixelColor(x, y));
|
|
}
|
|
}
|
|
}
|
|
|
|
// HLS order
|
|
list< CRGBA > sortedHLS;
|
|
list< CRGBA >::iterator itCol, itHLS;
|
|
bool inserted = false;
|
|
for(itCol=limitPixels.begin(); itCol!=limitPixels.end(); itCol++)
|
|
{
|
|
inserted = false;
|
|
for(itHLS=sortedHLS.begin(); itHLS!=sortedHLS.end(); ++itHLS)
|
|
{
|
|
if(infHLS(*itCol, *itHLS))
|
|
{
|
|
sortedHLS.insert(itHLS, *itCol);
|
|
inserted = true;
|
|
break;
|
|
}
|
|
}
|
|
if(inserted==false) sortedHLS.push_back(*itCol);
|
|
}
|
|
|
|
|
|
// keep more filled eighth of circle
|
|
nlassert(!sortedHLS.empty()); // If it crashes here, you may be missing .zonel's.
|
|
itHLS = sortedHLS.begin();
|
|
uint h, s, v;
|
|
RGB2HSV(*itHLS, h, s, v);
|
|
list< CRGBA > currentList, maxList;
|
|
|
|
for(uint i=0; i<8; i++)
|
|
{
|
|
while(itHLS!=sortedHLS.end() && h<i*45)
|
|
{
|
|
currentList.push_back(*itHLS);
|
|
RGB2HSV(*itHLS, h, s, v);
|
|
++itHLS;
|
|
}
|
|
|
|
if(currentList.size() > maxList.size())
|
|
{
|
|
maxList.clear();
|
|
maxList = currentList;
|
|
currentList.clear();
|
|
}
|
|
}
|
|
|
|
vector< CRGBA > sortedColors(maxList.size());
|
|
uint colorsNb = 0;
|
|
CRGBA lastColor(0, 0, 0, 0);
|
|
CRGBA maxColor;
|
|
uint maxColorNb = 0;
|
|
uint currentColorNb = 0;
|
|
for(itHLS=maxList.begin(); itHLS!=maxList.end(); ++itHLS)
|
|
{
|
|
if(lastColor==*itHLS)
|
|
{
|
|
currentColorNb++;
|
|
}
|
|
else
|
|
{
|
|
currentColorNb = 1;
|
|
}
|
|
|
|
if(currentColorNb>maxColorNb)
|
|
{
|
|
maxColorNb = currentColorNb;
|
|
maxColor = *itHLS;
|
|
}
|
|
|
|
lastColor = *itHLS;
|
|
|
|
RGB2HSV(*itHLS, h, s, v);
|
|
if(v>25 && v<75 && s>25 && s<75)
|
|
{
|
|
sortedColors[colorsNb] = *itHLS;
|
|
colorsNb++;
|
|
}
|
|
}
|
|
|
|
if(colorsNb < 5)
|
|
{
|
|
colorsNb = 0;
|
|
for(itHLS=maxList.begin(); itHLS!=maxList.end(); ++itHLS)
|
|
{
|
|
sortedColors[colorsNb] = *itHLS;
|
|
colorsNb++;
|
|
}
|
|
}
|
|
|
|
sortedColors.resize(colorsNb);
|
|
|
|
_BackBitmap.resize(islandBitmap.getWidth(), islandBitmap.getHeight(), islandBitmap.PixelFormat);
|
|
|
|
if(sortedColors.size()!=0)
|
|
{
|
|
_BackColor = maxColor;
|
|
|
|
CRandom randomGenerator;
|
|
|
|
uint8 * backPixels = &(_BackBitmap.getPixels(0)[0]);
|
|
|
|
for(uint x=0; x<_BackBitmap.getWidth(); x++)
|
|
{
|
|
for(uint y=0; y<_BackBitmap.getHeight(); y++)
|
|
{
|
|
sint32 randomVal = randomGenerator.rand(colorsNb-1);
|
|
const CRGBA & color = sortedColors[randomVal];
|
|
|
|
*backPixels = (uint8) color.R;
|
|
*(backPixels + 1) = (uint8) color.G;
|
|
*(backPixels + 2) = (uint8) color.B;
|
|
*(backPixels + 3) = (uint8) 255;
|
|
|
|
backPixels = backPixels+4;
|
|
}
|
|
}
|
|
}
|
|
|
|
/*
|
|
//TEST
|
|
CBitmap HLSBitmap;
|
|
HLSBitmap.resize(640, sortedColors.size()*4, islandBitmap.PixelFormat);
|
|
|
|
uint8 * hlsPixels = &(HLSBitmap.getPixels(0)[0]);
|
|
|
|
for(uint i=0; i < sortedColors.size(); i++)
|
|
{
|
|
uint count = 0;
|
|
while(count<640*4)
|
|
{
|
|
*hlsPixels = (uint8) sortedColors[i].R;
|
|
*(hlsPixels + 1) = (uint8) sortedColors[i].G;
|
|
*(hlsPixels + 2) = (uint8) sortedColors[i].B;
|
|
*(hlsPixels + 3) = (uint8) sortedColors[i].A;
|
|
|
|
hlsPixels = hlsPixels+4;
|
|
count++;
|
|
}
|
|
}
|
|
|
|
|
|
string textureName = _OutDirectory + "/" + islandName + "_HLS2.tga";
|
|
COFile fsHLS(textureName.c_str());
|
|
HLSBitmap.writeTGA(fsHLS,32);
|
|
*/
|
|
}
|
|
|
|
|
|
//--------------------------------------------------------------------------------------------------------------
|
|
//--------------------------------------------------------------------------------------------------------------
|
|
|
|
//-------------------------------------------------------------------------------------------------
|
|
// methods CProximityMapBuffer
|
|
//-------------------------------------------------------------------------------------------------
|
|
|
|
void CProximityMapBuffer::load(const std::string& name)
|
|
{
|
|
// load the AI collision map file
|
|
CWorldMap worldMap;
|
|
CIFile f(name);
|
|
f.serial(worldMap);
|
|
|
|
// lookup the map bounds
|
|
CMapPosition min, max;
|
|
worldMap.getBounds(min, max);
|
|
|
|
// calculate a handful of constants relating to the bounds of the image...
|
|
_ScanWidth = max.x()-min.x();
|
|
_ScanHeight = max.y()-min.y();
|
|
_XOffset= min.x();
|
|
_YOffset= (sint16)min.y();
|
|
|
|
// redimension buffer to correct size
|
|
_Buffer.resize(_ScanWidth*_ScanHeight);
|
|
|
|
// setup a position variable to mark the start point of each line
|
|
CMapPosition scanpos(min.x(),min.y());
|
|
|
|
// iterate over the scan area looking for points that are accessible
|
|
for (uint32 y=0; y<_ScanHeight; ++y, scanpos = scanpos.getStepN())
|
|
{
|
|
CMapPosition pos(scanpos);
|
|
|
|
// scan a line of the map
|
|
for (uint32 x=0; x<_ScanWidth; ++x, pos = pos.getStepE())
|
|
{
|
|
bool isAccessible= false;
|
|
// if the cell pointer is NULL it means that the 16x16 cell in question is inaccessible
|
|
if (worldMap.getRootCellCst(pos) != NULL)
|
|
{
|
|
// run through the surfaces in the cell looking for a match for this position (may be as many as 3 surfaces per cell max)
|
|
for (uint32 ns=0; ns<3; ++ns)
|
|
{
|
|
isAccessible |= worldMap.getSafeWorldPosition(pos, CSlot(ns)).isValid();
|
|
}
|
|
}
|
|
// setup the next pixel in the output buffers...
|
|
_Buffer[y*_ScanWidth+x]= (isAccessible? 0:std::numeric_limits<TBufferEntry>::max());
|
|
}
|
|
}
|
|
}
|
|
|
|
//-------------------------------------------------------------------------------------------------
|
|
void CProximityMapBuffer::calculateZones(TZones& zones)
|
|
{
|
|
// clear out the result buffer before starting work
|
|
zones.clear();
|
|
|
|
// setup a container to hold the accessible points within this buffer
|
|
typedef std::set<uint32> TAccessiblePoints;
|
|
TAccessiblePoints accessiblePoints;
|
|
|
|
// start by building the set of all accessible points
|
|
for (uint32 i=0;i<_Buffer.size();++i)
|
|
{
|
|
if (_Buffer[i]==0)
|
|
accessiblePoints.insert(i);
|
|
}
|
|
|
|
// while there are still points remaining in the set we must have another zone to process
|
|
while (!accessiblePoints.empty())
|
|
{
|
|
// append a new zone to the zones vector and get a refference to it
|
|
zones.push_back( CProximityZone(_ScanWidth,_ScanHeight,_XOffset,_YOffset) );
|
|
CProximityZone& theZone= zones.back();
|
|
|
|
// setup a todo list representing points that are part of the surface that we are dealing with
|
|
// that haven't yet been treated to check for neighbours, etc
|
|
std::vector<uint32> todo;
|
|
|
|
// get hold of the first point in the accessilbe points set and push it onto the todo list
|
|
todo.push_back(*accessiblePoints.begin());
|
|
accessiblePoints.erase(todo.back());
|
|
|
|
// while we have more points to deal with ...
|
|
while (!todo.empty())
|
|
{
|
|
// pop the next point off the todo list
|
|
uint32 thePoint= todo.back();
|
|
todo.pop_back();
|
|
|
|
// add the point to the zone
|
|
theZone.add(thePoint);
|
|
|
|
// a little macro for the code to perform for each movement test...
|
|
#define TEST_MOVE(xoffs,yoffs)\
|
|
{\
|
|
TAccessiblePoints::iterator it= accessiblePoints.find(thePoint+xoffs+_ScanWidth*yoffs);\
|
|
if (it!=accessiblePoints.end())\
|
|
{\
|
|
todo.push_back(*it);\
|
|
accessiblePoints.erase(it);\
|
|
}\
|
|
}
|
|
|
|
// N, S, W, E moves
|
|
TEST_MOVE( 0, 1);
|
|
TEST_MOVE( 0,-1);
|
|
TEST_MOVE( 1, 0);
|
|
TEST_MOVE(-1, 0);
|
|
|
|
// NW, NE, WS, SE moves
|
|
TEST_MOVE( 1, 1);
|
|
TEST_MOVE(-1, 1);
|
|
TEST_MOVE( 1,-1);
|
|
TEST_MOVE(-1,-1);
|
|
|
|
#undef TEST_MOVE
|
|
}
|
|
}
|
|
|
|
nlinfo("Found %u zones",zones.size());
|
|
}
|
|
|
|
//-------------------------------------------------------------------------------------------------
|
|
void CProximityMapBuffer::_prepareBufferForZoneProximityMap(const CProximityZone& zone,TBuffer& zoneBuffer,TOffsetsVector& accessiblePoints)
|
|
{
|
|
// the length of runs that we consider too short to deal with...
|
|
const uint32 shortRunLength=5;
|
|
|
|
// redimention and initialise the zone buffer
|
|
uint32 zoneWidth= zone.getZoneWidth();
|
|
uint32 zoneHeight= zone.getZoneHeight();
|
|
zoneBuffer.clear();
|
|
zoneBuffer.resize(zoneWidth*zoneHeight, std::numeric_limits<TBufferEntry>::max());
|
|
|
|
// setup the buffer's accessible points and prime vects[0] with the set of accessible points in the zone buffer
|
|
for (uint32 i=0;i<zone.getOffsets().size();++i)
|
|
{
|
|
// lookup the next offset in the zone's offsets vector and remap to a zone-relative offset
|
|
uint32 zoneOffset= zone.remapOffset(zone.getOffsets()[i]);
|
|
// flag the appropriate entry in the zoneBuffer as 'distance=0'
|
|
zoneBuffer[zoneOffset]= 0;
|
|
// add the zone offset to the accessible points vector
|
|
accessiblePoints.push_back(zoneOffset);
|
|
}
|
|
|
|
// run through the zone buffer looking for points that are surrounded that we can just throw out...
|
|
// start by flagging all points that belong to a short run in the y direction
|
|
for (uint32 i=0;i<zoneWidth;++i)
|
|
{
|
|
// setup start and end offsets marking first and last 'zero' value pixels in the column
|
|
uint32 startOffset=i, endOffset=i+(zoneHeight-1)*zoneWidth;
|
|
for (; startOffset<endOffset && zoneBuffer[startOffset]!=0; startOffset+= zoneWidth) {}
|
|
for (; endOffset>startOffset && zoneBuffer[endOffset]!=0; endOffset-= zoneWidth) {}
|
|
|
|
for (uint32 offset=startOffset, marker=startOffset;offset<=endOffset;offset+=zoneWidth)
|
|
{
|
|
// see if this is an accessible position
|
|
if (zoneBuffer[offset]!=0)
|
|
{
|
|
zoneBuffer[offset]= InteriorValue;
|
|
|
|
if(offset-1>=startOffset && zoneBuffer[offset-1] == std::numeric_limits<TBufferEntry>::max())
|
|
{
|
|
zoneBuffer[offset-1] = ValueBorder;
|
|
}
|
|
if(offset+1<=endOffset && zoneBuffer[offset+1] == std::numeric_limits<TBufferEntry>::max())
|
|
{
|
|
zoneBuffer[offset+1] = ValueBorder;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// continue by dealing with all points that belong to a short run in the x direction
|
|
for (uint32 i=0;i<zoneHeight;++i)
|
|
{
|
|
// setup start and end offsets marking first and last 'zero' value pixels in the column
|
|
uint32 startOffset= i*zoneWidth;
|
|
uint32 endOffset= startOffset+zoneWidth-1;
|
|
|
|
uint32 startOffsetInit = startOffset;
|
|
uint32 endOffsetInit = endOffset;
|
|
|
|
for (; startOffset<endOffset && zoneBuffer[startOffset]!=0; ++startOffset) {}
|
|
for (; endOffset>startOffset && zoneBuffer[endOffset]!=0; --endOffset) {}
|
|
|
|
for (uint32 offset=startOffset, marker=startOffset;offset<=endOffset;++offset)
|
|
{
|
|
// see if this is an accessible position
|
|
if (zoneBuffer[offset]!=0)
|
|
{
|
|
zoneBuffer[offset]= InteriorValue;
|
|
|
|
if(offset>zoneWidth && zoneBuffer[offset-zoneWidth] == std::numeric_limits<TBufferEntry>::max())
|
|
{
|
|
zoneBuffer[offset-zoneWidth] = ValueBorder;
|
|
}
|
|
if(offset+zoneWidth<zoneHeight*zoneWidth && zoneBuffer[offset+zoneWidth] == std::numeric_limits<TBufferEntry>::max())
|
|
{
|
|
zoneBuffer[offset+zoneWidth] = ValueBorder;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
//-------------------------------------------------------------------------------------------------
|
|
void CProximityMapBuffer::generateZoneProximityMap(const CProximityZone& zone,TBuffer& zoneBuffer)
|
|
{
|
|
// a set of vectors to hold sets of points that need to be treated
|
|
TOffsetsVector vects[16];
|
|
// a counter that should always contain sum of all vects[i].size()
|
|
uint32 entriesToTreat= 0;
|
|
|
|
// setup the buffer's accessible points and prime vects[0] with the set of accessible points in the zone buffer
|
|
_prepareBufferForZoneProximityMap(zone, zoneBuffer, vects[0]);
|
|
entriesToTreat= vects[0].size();
|
|
|
|
// lookup the buffer dimentions
|
|
uint32 zoneWidth= zone.getZoneWidth();
|
|
uint32 zoneHeight= zone.getZoneHeight();
|
|
|
|
// for dist=0 to ? treat points with distance 'dist' from centre, iterating until all vects are empty
|
|
for (TBufferEntry dist=0; entriesToTreat!=0; ++dist)
|
|
{
|
|
// setup refference to the vector that we are supposed to be iterating over for this dist
|
|
TOffsetsVector &vect= vects[dist&15];
|
|
|
|
// iterate over contents of points for this distance, treating NSWE neighbours
|
|
for(TOffsetsVector::iterator it=vect.begin(); it!=vect.end(); ++it)
|
|
{
|
|
uint32 val=(*it);
|
|
|
|
// deal with the case where this point has already been refferenced via a better route
|
|
if (zoneBuffer[val]<dist || (zoneBuffer[val]==InteriorValue && dist > BigValue)
|
|
|| (zoneBuffer[val]==ValueBorder && dist > BigValue))
|
|
continue;
|
|
|
|
// write the new distance into this buffer entry
|
|
zoneBuffer[val]=dist;
|
|
|
|
// decompose into x and y in order to manage identification of neighbour cells correctly
|
|
uint32 x= val % zoneWidth;
|
|
uint32 y= val / zoneWidth;
|
|
|
|
#define TEST_MOVE(xoffs,yoffs,newDist)\
|
|
{\
|
|
if (((uint32)(x+xoffs)<zoneWidth) && ((uint32)(y+yoffs)<zoneHeight))\
|
|
{\
|
|
uint32 newVal= val+xoffs+(yoffs*zoneWidth);\
|
|
bool isInterior = ((zoneBuffer[newVal] == InteriorValue && newDist > BigValue) || (zoneBuffer[newVal] == ValueBorder && newDist > BigValue));\
|
|
if (zoneBuffer[newVal] > newDist && !isInterior)\
|
|
{\
|
|
zoneBuffer[newVal] = newDist;\
|
|
vects[newDist & 15].push_back(newVal);\
|
|
++entriesToTreat;\
|
|
}\
|
|
}\
|
|
}
|
|
|
|
// N, S, W, E moves
|
|
TEST_MOVE( 0, 1,dist+5);
|
|
TEST_MOVE( 0,-1,dist+5);
|
|
TEST_MOVE( 1, 0,dist+5);
|
|
TEST_MOVE(-1, 0,dist+5);
|
|
|
|
// NW, NE, WS, SE moves
|
|
TEST_MOVE( 1, 1,dist+7);
|
|
TEST_MOVE(-1, 1,dist+7);
|
|
TEST_MOVE( 1,-1,dist+7);
|
|
TEST_MOVE(-1,-1,dist+7);
|
|
|
|
// NNW, NNE, SSW, SSE moves
|
|
TEST_MOVE( 1, 2,dist+11);
|
|
TEST_MOVE(-1, 2,dist+11);
|
|
TEST_MOVE( 1,-2,dist+11);
|
|
TEST_MOVE(-1,-2,dist+11);
|
|
|
|
// WNW, WSW, ENE, ESE moves
|
|
TEST_MOVE( 2, 1,dist+11);
|
|
TEST_MOVE(-2, 1,dist+11);
|
|
TEST_MOVE( 2,-1,dist+11);
|
|
TEST_MOVE(-2,-1,dist+11);
|
|
|
|
#undef TEST_MOVE
|
|
}
|
|
|
|
// clear out the vector
|
|
entriesToTreat-= vect.size();
|
|
vect.clear();
|
|
}
|
|
}
|
|
|
|
//-------------------------------------------------------------------------------------------------
|
|
const TBuffer& CProximityMapBuffer::getBuffer() const
|
|
{
|
|
return _Buffer;
|
|
}
|
|
|
|
//-------------------------------------------------------------------------------------------------
|
|
uint32 CProximityMapBuffer::getScanHeight() const
|
|
{
|
|
return _ScanHeight;
|
|
}
|
|
|
|
//-------------------------------------------------------------------------------------------------
|
|
uint32 CProximityMapBuffer::getScanWidth() const
|
|
{
|
|
return _ScanWidth;
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------------------
|
|
//-----------------------------------------------------------------------------------------
|
|
|
|
//-------------------------------------------------------------------------------------------------
|
|
// methods CProximityZone
|
|
//-------------------------------------------------------------------------------------------------
|
|
|
|
CProximityZone::CProximityZone(uint32 scanWidth,uint32 scanHeight,sint32 xOffset, sint32 yOffset)
|
|
{
|
|
_ScanWidth = scanWidth;
|
|
_ScanHeight = scanHeight;
|
|
_XOffset = xOffset;
|
|
_YOffset = yOffset;
|
|
|
|
_MaxOffset = scanWidth * scanHeight -1;
|
|
|
|
_XMin = std::numeric_limits<uint32>::max();
|
|
_YMin = std::numeric_limits<uint32>::max();
|
|
_XMax = 0;
|
|
_YMax = 0;
|
|
|
|
_BorderPixels = 30;
|
|
}
|
|
|
|
//-------------------------------------------------------------------------------------------------
|
|
bool CProximityZone::add(uint32 offset)
|
|
{
|
|
// make sure the requested point is in the zone
|
|
if (offset>_MaxOffset)
|
|
return false;
|
|
|
|
// calculate the x and y coordinates of the point
|
|
uint32 y= offset/ _ScanWidth;
|
|
uint32 x= offset% _ScanWidth;
|
|
|
|
// update the bounding coordinates for this zone
|
|
if (x<_XMin) _XMin= x;
|
|
if (x>_XMax) _XMax= x;
|
|
if (y<_YMin) _YMin= y;
|
|
if (y>_YMax) _YMax= y;
|
|
|
|
// add the point to the vector of points
|
|
_Offsets.push_back(offset);
|
|
return true;
|
|
}
|
|
|
|
//-------------------------------------------------------------------------------------------------
|
|
const CProximityZone::TOffsets& CProximityZone::getOffsets() const
|
|
{
|
|
return _Offsets;
|
|
}
|
|
|
|
//-------------------------------------------------------------------------------------------------
|
|
uint32 CProximityZone::getZoneWidth() const
|
|
{
|
|
return getZoneXMax()- getZoneXMin() +1;
|
|
}
|
|
|
|
//-------------------------------------------------------------------------------------------------
|
|
uint32 CProximityZone::getZoneHeight() const
|
|
{
|
|
return getZoneYMax()- getZoneYMin() +1;
|
|
}
|
|
|
|
//-------------------------------------------------------------------------------------------------
|
|
sint32 CProximityZone::getZoneXMin() const
|
|
{
|
|
return _XMin-_BorderPixels;
|
|
}
|
|
|
|
//-------------------------------------------------------------------------------------------------
|
|
sint32 CProximityZone::getZoneYMin() const
|
|
{
|
|
return _YMin-_BorderPixels;
|
|
}
|
|
|
|
//-------------------------------------------------------------------------------------------------
|
|
uint32 CProximityZone::getZoneXMax() const
|
|
{
|
|
return _XMax+_BorderPixels;
|
|
}
|
|
|
|
//-------------------------------------------------------------------------------------------------
|
|
uint32 CProximityZone::getZoneYMax() const
|
|
{
|
|
return _YMax+_BorderPixels;
|
|
}
|
|
|
|
//-------------------------------------------------------------------------------------------------
|
|
uint32 CProximityZone::getBoundXMin() const
|
|
{
|
|
return _XMin+_XOffset-_BorderPixels;
|
|
}
|
|
|
|
//-------------------------------------------------------------------------------------------------
|
|
uint32 CProximityZone::getBoundYMin() const
|
|
{
|
|
return _YMin+_YOffset-_BorderPixels;
|
|
}
|
|
|
|
//-------------------------------------------------------------------------------------------------
|
|
uint32 CProximityZone::getBoundXMax() const
|
|
{
|
|
return _XMax+_XOffset+_BorderPixels;
|
|
}
|
|
|
|
//-------------------------------------------------------------------------------------------------
|
|
uint32 CProximityZone::getBoundYMax() const
|
|
{
|
|
return _YMax+_YOffset+_BorderPixels;
|
|
}
|
|
|
|
//-------------------------------------------------------------------------------------------------
|
|
uint32 CProximityZone::remapOffset(uint32 bufferOffset) const
|
|
{
|
|
// decompose input coordinates into x and y parts
|
|
uint32 bufferX= bufferOffset% _ScanWidth;
|
|
uint32 bufferY= bufferOffset/ _ScanWidth;
|
|
|
|
// remap the offset from a _Buffer-relative offset to a zone-relative offset
|
|
return bufferX-getZoneXMin()+ (bufferY-getZoneYMin())*getZoneWidth();
|
|
}
|
|
|
|
}
|
|
|