mirror of
https://port.numenaute.org/aleajactaest/khanat-opennel-code.git
synced 2024-12-15 22:08:43 +00:00
598 lines
16 KiB
C++
598 lines
16 KiB
C++
|
// NeL - MMORPG Framework <http://dev.ryzom.com/projects/nel/>
|
||
|
// 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 "std3d.h"
|
||
|
|
||
|
#include "nel/3d/cluster.h"
|
||
|
#include "nel/3d/portal.h"
|
||
|
#include "nel/misc/stream.h"
|
||
|
#include "nel/misc/string_mapper.h"
|
||
|
#include "nel/3d/scene.h"
|
||
|
#include "nel/3d/transform_shape.h"
|
||
|
//#include "mesh_instance.h"
|
||
|
#include "nel/3d/scene_group.h"
|
||
|
|
||
|
using namespace NLMISC;
|
||
|
using namespace std;
|
||
|
|
||
|
namespace NL3D
|
||
|
{
|
||
|
|
||
|
// 0.5 cm of precision
|
||
|
#define CLUSTERPRECISION 0.005
|
||
|
|
||
|
|
||
|
// ***************************************************************************
|
||
|
CCluster::CCluster ()
|
||
|
{
|
||
|
/* ***********************************************
|
||
|
* WARNING: This Class/Method must be thread-safe (ctor/dtor/serial): no static access for instance
|
||
|
* It can be loaded/called through CAsyncFileManager for instance
|
||
|
* ***********************************************/
|
||
|
|
||
|
FatherVisible = VisibleFromFather = false;
|
||
|
FatherAudible = AudibleFromFather = false;
|
||
|
Father = NULL;
|
||
|
Group = NULL;
|
||
|
|
||
|
// map a no fx string
|
||
|
_EnvironmentFxId = CStringMapper::map("no fx");
|
||
|
// map a no soundgroup string
|
||
|
_SoundGroupId = CStringMapper::map("");
|
||
|
|
||
|
// I am a transform cluster
|
||
|
CTransform::setIsCluster(true);
|
||
|
|
||
|
// Default: not traversed
|
||
|
_Visited= false;
|
||
|
_CameraIn= false;
|
||
|
}
|
||
|
|
||
|
|
||
|
// ***************************************************************************
|
||
|
CCluster::~CCluster()
|
||
|
{
|
||
|
/* ***********************************************
|
||
|
* WARNING: This Class/Method must be thread-safe (ctor/dtor/serial): no static access for instance
|
||
|
* It can be loaded/called through CAsyncFileManager for instance
|
||
|
* ***********************************************/
|
||
|
|
||
|
unlinkFromClusterTree();
|
||
|
}
|
||
|
|
||
|
|
||
|
void CCluster::setSoundGroup(const std::string &soundGroup)
|
||
|
{
|
||
|
_SoundGroupId = CStringMapper::map(soundGroup);
|
||
|
}
|
||
|
void CCluster::setSoundGroup(const NLMISC::TStringId &soundGroupId)
|
||
|
{
|
||
|
_SoundGroupId = soundGroupId;
|
||
|
}
|
||
|
const std::string &CCluster::getSoundGroup()
|
||
|
{
|
||
|
return CStringMapper::unmap(_SoundGroupId);
|
||
|
}
|
||
|
|
||
|
NLMISC::TStringId CCluster::getSoundGroupId()
|
||
|
{
|
||
|
return _SoundGroupId;
|
||
|
}
|
||
|
void CCluster::setEnvironmentFx(const std::string &environmentFx)
|
||
|
{
|
||
|
_EnvironmentFxId = CStringMapper::map(environmentFx);
|
||
|
}
|
||
|
void CCluster::setEnvironmentFx(const NLMISC::TStringId &environmentFxId)
|
||
|
{
|
||
|
_EnvironmentFxId = environmentFxId;
|
||
|
}
|
||
|
const std::string &CCluster::getEnvironmentFx()
|
||
|
{
|
||
|
return CStringMapper::unmap(_EnvironmentFxId);
|
||
|
}
|
||
|
|
||
|
NLMISC::TStringId CCluster::getEnvironmentFxId()
|
||
|
{
|
||
|
return _EnvironmentFxId;
|
||
|
}
|
||
|
|
||
|
|
||
|
|
||
|
// ***************************************************************************
|
||
|
void CCluster::unlinkFromParent()
|
||
|
{
|
||
|
/* ***********************************************
|
||
|
* WARNING: This Class/Method must be thread-safe (ctor/dtor/serial): no static access for instance
|
||
|
* It can be loaded/called through CAsyncFileManager for instance
|
||
|
* ***********************************************/
|
||
|
|
||
|
// unlink from father sons list
|
||
|
if (Father)
|
||
|
{
|
||
|
Father->Children.erase(remove(Father->Children.begin(), Father->Children.end(), this), Father->Children.end());
|
||
|
Father = NULL;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// ***************************************************************************
|
||
|
void CCluster::unlinkSons()
|
||
|
{
|
||
|
/* ***********************************************
|
||
|
* WARNING: This Class/Method must be thread-safe (ctor/dtor/serial): no static access for instance
|
||
|
* It can be loaded/called through CAsyncFileManager for instance
|
||
|
* ***********************************************/
|
||
|
|
||
|
// tells all sons that they have no more father
|
||
|
for(uint k = 0; k < Children.size(); ++k)
|
||
|
{
|
||
|
if (Children[k]->Father == this)
|
||
|
{
|
||
|
Children[k]->Father = NULL;
|
||
|
}
|
||
|
}
|
||
|
NLMISC::contReset(Children);
|
||
|
}
|
||
|
|
||
|
|
||
|
|
||
|
// ***************************************************************************
|
||
|
void CCluster::unlinkFromClusterTree()
|
||
|
{
|
||
|
/* ***********************************************
|
||
|
* WARNING: This Class/Method must be thread-safe (ctor/dtor/serial): no static access for instance
|
||
|
* It can be loaded/called through CAsyncFileManager for instance
|
||
|
* ***********************************************/
|
||
|
|
||
|
unlinkFromParent();
|
||
|
unlinkSons();
|
||
|
}
|
||
|
|
||
|
|
||
|
|
||
|
// ***************************************************************************
|
||
|
void CCluster::registerBasic ()
|
||
|
{
|
||
|
CScene::registerModel (ClusterId, 0, CCluster::creator);
|
||
|
}
|
||
|
|
||
|
// ***************************************************************************
|
||
|
bool CCluster::makeVolume (const CVector& p1, const CVector& p2, const CVector& p3)
|
||
|
{
|
||
|
uint i;
|
||
|
// Check if the plane is not close to a plane that already define the cluster
|
||
|
for (i = 0; i < _LocalVolume.size(); ++i)
|
||
|
{
|
||
|
float f1 = fabsf (_LocalVolume[i]*p1);
|
||
|
float f2 = fabsf (_LocalVolume[i]*p2);
|
||
|
float f3 = fabsf (_LocalVolume[i]*p3);
|
||
|
if ((f1 < CLUSTERPRECISION) && (f2 < CLUSTERPRECISION) && (f3 < CLUSTERPRECISION))
|
||
|
return true;
|
||
|
}
|
||
|
// Check if we want to add a triangle not completely in the predefined volume
|
||
|
for (i = 0; i < _LocalVolume.size(); ++i)
|
||
|
{
|
||
|
float f1 = _LocalVolume[i]*p1;
|
||
|
float f2 = _LocalVolume[i]*p2;
|
||
|
float f3 = _LocalVolume[i]*p3;
|
||
|
if ((f1 > CLUSTERPRECISION) && (f2 > CLUSTERPRECISION) && (f3 > CLUSTERPRECISION))
|
||
|
return false;
|
||
|
}
|
||
|
// Build volume
|
||
|
CPlane p;
|
||
|
p.make (p1, p2, p3);
|
||
|
p.normalize();
|
||
|
_LocalVolume.push_back (p);
|
||
|
// Build BBox
|
||
|
if (_LocalVolume.size() == 1)
|
||
|
_LocalBBox.setCenter(p1);
|
||
|
else
|
||
|
_LocalBBox.extend(p1);
|
||
|
_LocalBBox.extend(p2);
|
||
|
_LocalBBox.extend(p3);
|
||
|
_Volume = _LocalVolume;
|
||
|
_BBox = _LocalBBox;
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
// ***************************************************************************
|
||
|
bool CCluster::isIn (const CVector& p)
|
||
|
{
|
||
|
for (uint i = 0; i < _Volume.size(); ++i)
|
||
|
if (_Volume[i]*p > CLUSTERPRECISION)
|
||
|
return false;
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
|
||
|
// ***************************************************************************
|
||
|
bool CCluster::isIn (const CAABBox& b)
|
||
|
{
|
||
|
for (uint i = 0; i < _Volume.size(); ++i)
|
||
|
{
|
||
|
if (!b.clipBack (_Volume[i]))
|
||
|
return false;
|
||
|
}
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
// ***************************************************************************
|
||
|
bool CCluster::isIn (const NLMISC::CVector& center, float size)
|
||
|
{
|
||
|
for (uint i = 0; i < _Volume.size(); ++i)
|
||
|
if (_Volume[i]*center > size)
|
||
|
return false;
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
// ***************************************************************************
|
||
|
bool CCluster::clipSegment (NLMISC::CVector &p0, NLMISC::CVector &p1)
|
||
|
{
|
||
|
for (uint i = 0; i < _Volume.size(); ++i)
|
||
|
{
|
||
|
if (!_Volume[i].clipSegmentBack(p0, p1))
|
||
|
return false;
|
||
|
}
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
// ***************************************************************************
|
||
|
void CCluster::resetPortalLinks ()
|
||
|
{
|
||
|
_Portals.clear();
|
||
|
}
|
||
|
|
||
|
// ***************************************************************************
|
||
|
void CCluster::link (CPortal* portal)
|
||
|
{
|
||
|
_Portals.push_back (portal);
|
||
|
}
|
||
|
|
||
|
// ***************************************************************************
|
||
|
void CCluster::unlink (CPortal* portal)
|
||
|
{
|
||
|
uint32 pos;
|
||
|
for (pos = 0; pos < _Portals.size(); ++pos)
|
||
|
{
|
||
|
if (_Portals[pos] == portal)
|
||
|
break;
|
||
|
}
|
||
|
if (pos < _Portals.size())
|
||
|
_Portals.erase (_Portals.begin()+pos);
|
||
|
}
|
||
|
|
||
|
// ***************************************************************************
|
||
|
void CCluster::serial (NLMISC::IStream&f)
|
||
|
{
|
||
|
/* ***********************************************
|
||
|
* WARNING: This Class/Method must be thread-safe (ctor/dtor/serial): no static access for instance
|
||
|
* It can be loaded/called through CAsyncFileManager for instance
|
||
|
* ***********************************************/
|
||
|
|
||
|
sint version = f.serialVersion (3);
|
||
|
|
||
|
if (version >= 1)
|
||
|
f.serial (Name);
|
||
|
|
||
|
f.serialCont (_LocalVolume);
|
||
|
f.serial (_LocalBBox);
|
||
|
f.serial (FatherVisible);
|
||
|
f.serial (VisibleFromFather);
|
||
|
if (f.isReading())
|
||
|
{
|
||
|
_Volume = _LocalVolume;
|
||
|
_BBox = _LocalBBox;
|
||
|
}
|
||
|
|
||
|
if (version >= 2)
|
||
|
{
|
||
|
if (f.isReading())
|
||
|
{
|
||
|
std::string soundGroup;
|
||
|
std::string envFxName;
|
||
|
|
||
|
f.serial(soundGroup);
|
||
|
_SoundGroupId = CStringMapper::map(soundGroup);
|
||
|
|
||
|
f.serial(envFxName);
|
||
|
if (envFxName == "")
|
||
|
envFxName = "no fx";
|
||
|
_EnvironmentFxId = CStringMapper::map(envFxName);
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
// write the sound group
|
||
|
std::string soundGroup = CStringMapper::unmap(_SoundGroupId);
|
||
|
f.serial(soundGroup);
|
||
|
// write the env fx name
|
||
|
std::string envFxName = CStringMapper::unmap(_EnvironmentFxId);
|
||
|
if (envFxName == "no fx")
|
||
|
envFxName = "";
|
||
|
f.serial(envFxName);
|
||
|
}
|
||
|
|
||
|
// nldebug("Cluster %s, sound group [%s]", Name.c_str(), CStringMapper::unmap(_SoundGroupId).c_str());
|
||
|
}
|
||
|
|
||
|
if (version >= 3)
|
||
|
{
|
||
|
f.serial(AudibleFromFather);
|
||
|
f.serial(FatherAudible);
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
// copy the visual property
|
||
|
AudibleFromFather = VisibleFromFather;
|
||
|
FatherAudible = FatherVisible;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// ***************************************************************************
|
||
|
void CCluster::setWorldMatrix (const CMatrix &WM)
|
||
|
{
|
||
|
uint32 i;
|
||
|
CMatrix invWM = WM;
|
||
|
invWM.invert();
|
||
|
|
||
|
// Transform the volume
|
||
|
for (i = 0; i < _LocalVolume.size(); ++i)
|
||
|
_Volume[i] = _LocalVolume[i] * invWM;
|
||
|
|
||
|
_BBox = NLMISC::CAABBox::transformAABBox(WM, _LocalBBox);
|
||
|
|
||
|
// Transform the bounding box
|
||
|
/*CVector p[8];
|
||
|
p[0].x = _LocalBBox.getMin().x;
|
||
|
p[0].y = _LocalBBox.getMin().y;
|
||
|
p[0].z = _LocalBBox.getMin().z;
|
||
|
|
||
|
p[1].x = _LocalBBox.getMax().x;
|
||
|
p[1].y = _LocalBBox.getMin().y;
|
||
|
p[1].z = _LocalBBox.getMin().z;
|
||
|
|
||
|
p[2].x = _LocalBBox.getMin().x;
|
||
|
p[2].y = _LocalBBox.getMax().y;
|
||
|
p[2].z = _LocalBBox.getMin().z;
|
||
|
|
||
|
p[3].x = _LocalBBox.getMax().x;
|
||
|
p[3].y = _LocalBBox.getMax().y;
|
||
|
p[3].z = _LocalBBox.getMin().z;
|
||
|
|
||
|
p[4].x = _LocalBBox.getMin().x;
|
||
|
p[4].y = _LocalBBox.getMin().y;
|
||
|
p[4].z = _LocalBBox.getMax().z;
|
||
|
|
||
|
p[5].x = _LocalBBox.getMax().x;
|
||
|
p[5].y = _LocalBBox.getMin().y;
|
||
|
p[5].z = _LocalBBox.getMax().z;
|
||
|
|
||
|
p[6].x = _LocalBBox.getMin().x;
|
||
|
p[6].y = _LocalBBox.getMax().y;
|
||
|
p[6].z = _LocalBBox.getMax().z;
|
||
|
|
||
|
p[7].x = _LocalBBox.getMax().x;
|
||
|
p[7].y = _LocalBBox.getMax().y;
|
||
|
p[7].z = _LocalBBox.getMax().z;
|
||
|
|
||
|
for (i = 0; i < 8; ++i)
|
||
|
p[i] = WM.mulPoint(p[i]);
|
||
|
|
||
|
CAABBox boxTemp;
|
||
|
|
||
|
boxTemp.setCenter(p[0]);
|
||
|
for (i = 1; i < 8; ++i)
|
||
|
boxTemp.extend(p[i]);
|
||
|
_BBox = boxTemp;*/
|
||
|
}
|
||
|
|
||
|
// ***************************************************************************
|
||
|
void CCluster::traverseHrc ()
|
||
|
{
|
||
|
CTransform::traverseHrc ();
|
||
|
|
||
|
setWorldMatrix (_WorldMatrix);
|
||
|
|
||
|
for (uint32 i = 0; i < getNbPortals(); ++i)
|
||
|
{
|
||
|
CPortal *pPortal = getPortal(i);
|
||
|
pPortal->setWorldMatrix (_WorldMatrix);
|
||
|
}
|
||
|
|
||
|
// Re affect the cluster to the accelerator if not the root
|
||
|
if (!isRoot())
|
||
|
{
|
||
|
Group->_ClipTrav->unregisterCluster(this);
|
||
|
Group->_ClipTrav->registerCluster (this);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// ***************************************************************************
|
||
|
bool CCluster::clip ()
|
||
|
{
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
|
||
|
// ***************************************************************************
|
||
|
void CCluster::traverseClip ()
|
||
|
{
|
||
|
// This is the root call called by the SceneRoot
|
||
|
recursTraverseClip(NULL);
|
||
|
}
|
||
|
|
||
|
|
||
|
// ***************************************************************************
|
||
|
void CCluster::recursTraverseClip(CTransform *caller)
|
||
|
{
|
||
|
if (_Visited)
|
||
|
return;
|
||
|
_Visited = true;
|
||
|
|
||
|
// The cluster is visible because we are in it
|
||
|
// So clip the models attached (with MOT links) to the cluster
|
||
|
uint num= clipGetNumChildren();
|
||
|
uint32 i;
|
||
|
for(i=0;i<num;i++)
|
||
|
clipGetChild(i)->traverseClip();
|
||
|
|
||
|
// Debug visible clusters
|
||
|
CClipTrav &clipTrav = getOwnerScene()->getClipTrav();
|
||
|
if (clipTrav.getClusterVisibilityTracking())
|
||
|
{
|
||
|
clipTrav.addVisibleCluster(this);
|
||
|
}
|
||
|
|
||
|
// And look through portals
|
||
|
for (i = 0; i < getNbPortals(); ++i)
|
||
|
{
|
||
|
CPortal*pPortal = getPortal (i);
|
||
|
vector<CPlane> WorldPyrTemp = clipTrav.WorldPyramid;
|
||
|
bool backfaceclipped = false;
|
||
|
CCluster *pOtherSideCluster;
|
||
|
if (pPortal->getCluster(0) == this)
|
||
|
pOtherSideCluster = pPortal->getCluster (1);
|
||
|
else
|
||
|
pOtherSideCluster = pPortal->getCluster (0);
|
||
|
|
||
|
if (Father != NULL)
|
||
|
if (caller == Father) // If the caller is the father
|
||
|
if (VisibleFromFather)
|
||
|
// Backface clipping
|
||
|
if( !pPortal->isInFront( clipTrav.CamPos ))
|
||
|
backfaceclipped = true;
|
||
|
|
||
|
if (!backfaceclipped && pOtherSideCluster)
|
||
|
{
|
||
|
/* If the otherSide cluster is fully visible because the camera is IN, then don't need to clip.
|
||
|
This is important to landscape test, to ensure that pyramid are strictly equal from 2 paths which
|
||
|
come from the 2 clusters where the camera start
|
||
|
*/
|
||
|
if (pOtherSideCluster->isCameraIn() || pPortal->clipPyramid (clipTrav.CamPos, clipTrav.WorldPyramid))
|
||
|
{
|
||
|
pOtherSideCluster->recursTraverseClip(this);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
clipTrav.WorldPyramid = WorldPyrTemp;
|
||
|
}
|
||
|
|
||
|
// Link up in hierarchy
|
||
|
if ((FatherVisible)&&(Father != NULL))
|
||
|
{
|
||
|
Father->recursTraverseClip(this);
|
||
|
}
|
||
|
|
||
|
// Link down in hierarchy
|
||
|
for (i = 0; i < Children.size(); ++i)
|
||
|
if (Children[i]->VisibleFromFather)
|
||
|
{
|
||
|
Children[i]->recursTraverseClip(this);
|
||
|
}
|
||
|
|
||
|
_Visited = false;
|
||
|
}
|
||
|
|
||
|
|
||
|
// ***************************************************************************
|
||
|
void CCluster::applyMatrix(const NLMISC::CMatrix &m)
|
||
|
{
|
||
|
uint32 i;
|
||
|
CMatrix invM = m;
|
||
|
invM.invert();
|
||
|
nlassert(_Volume.size() == _LocalVolume.size());
|
||
|
|
||
|
// Transform the volume
|
||
|
for (i = 0; i < _LocalVolume.size(); ++i)
|
||
|
{
|
||
|
_Volume[i] = _Volume[i] * invM;
|
||
|
_LocalVolume[i] = _LocalVolume[i] * invM;
|
||
|
}
|
||
|
|
||
|
// Transform the bounding boxes
|
||
|
_BBox = NLMISC::CAABBox::transformAABBox(m, _BBox);
|
||
|
_LocalBBox = NLMISC::CAABBox::transformAABBox(m, _LocalBBox);
|
||
|
}
|
||
|
|
||
|
|
||
|
// ***************************************************************************
|
||
|
void CCluster::cameraRayClip(const CVector &start, const CVector &end, std::vector<CCluster*> &clusterVisited)
|
||
|
{
|
||
|
uint i;
|
||
|
if (_Visited)
|
||
|
return;
|
||
|
_Visited = true;
|
||
|
|
||
|
// The cluster is visible because we are in it. add it to the list of cluster (if not already inserted)
|
||
|
for(i=0;i<clusterVisited.size();i++)
|
||
|
{
|
||
|
if(clusterVisited[i]==this)
|
||
|
break;
|
||
|
}
|
||
|
if(i==clusterVisited.size())
|
||
|
clusterVisited.push_back(this);
|
||
|
|
||
|
// look through portals
|
||
|
for (i = 0; i < getNbPortals(); ++i)
|
||
|
{
|
||
|
CPortal*pPortal = getPortal (i);
|
||
|
CCluster *pOtherSideCluster;
|
||
|
if (pPortal->getCluster(0) == this)
|
||
|
pOtherSideCluster = pPortal->getCluster (1);
|
||
|
else
|
||
|
pOtherSideCluster = pPortal->getCluster (0);
|
||
|
|
||
|
/*
|
||
|
bool backfaceclipped = false;
|
||
|
if (Father != NULL)
|
||
|
if (caller == Father) // If the caller is the father
|
||
|
if (VisibleFromFather)
|
||
|
// Backface clipping
|
||
|
if( !pPortal->isInFront( clipTrav.CamPos ))
|
||
|
backfaceclipped = true;
|
||
|
if (!backfaceclipped && pOtherSideCluster)*/
|
||
|
|
||
|
if (pOtherSideCluster)
|
||
|
{
|
||
|
if (pPortal->clipRay (start, end))
|
||
|
{
|
||
|
pOtherSideCluster->cameraRayClip(start, end, clusterVisited);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/* Link up in hierarchy. Test the Inverse Flag, cause the path is inverted here!!!
|
||
|
ie: if I allow the camera to go out, it MUST can re-enter (ie if I am VisibleFromFather)
|
||
|
*/
|
||
|
if ((VisibleFromFather)&&(Father != NULL))
|
||
|
{
|
||
|
Father->cameraRayClip(start, end, clusterVisited);
|
||
|
}
|
||
|
|
||
|
// Link down in hierarchy
|
||
|
for (i = 0; i < Children.size(); ++i)
|
||
|
{
|
||
|
// same remark. test FatherVisible, not VisibleFromFather
|
||
|
if (Children[i]->FatherVisible)
|
||
|
{
|
||
|
Children[i]->cameraRayClip(start, end, clusterVisited);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
_Visited = false;
|
||
|
}
|
||
|
|
||
|
|
||
|
} // NL3D
|