St°edem pozornosti v dneÜnφ novΘ lekci bude t°φda CTerrain a XCamera. S druhou jmenovanou t°φdou jsme ji₧ pracovali v minulΘ a p°edminulΘ lekci, dnes ji tedy pouze doplnφme, abychom mohli o°ezßvat viditelnou scΘnu. Naopak t°φdu CTerrain budeme vytvß°et ·pln∞ od zaΦßtku.
O Frustum jsme si pov∞d∞li, ₧e je to komol² jehlan, kter² vymezuje prostor. VÜechny objekty, kterΘ jsou uvnit° tohoto prostoru, by se m∞ly vykreslit, proto₧e budou vid∞t. U ostatnφch objekt∙ je jistota, ₧e vid∞t nebudou a tφm pßdem nßs nezajφmajφ. D∙le₧itΘ je tedy zajistit, abychom vykreslovali jen ty objekty, kterΘ uvnit° jsou a zbyteΦn∞ nepl²tvali v²kon t∞mi ostatnφmi. Abychom takov² test mohli v∙bec provΘst, je t°eba mφt Frustum spoΦφtanΘ a to je ·kol tΘto Φßsti.
Op∞t budeme pou₧φvat hojn∞ pojm∙ z analytickΘ geometrie. Zßklad je struktura CLIPVOLUME, kterß pomocφ obecn²ch rovnic rovin p°esn∞ definuje Frustum:
typedef struct _CLIPVOLUME
{
D3DXPLANE pLeft, pRight;
D3DXPLANE pTop, pBottom;
D3DXPLANE pNear, pFar;
} CLIPVOLUME;
D3DXPLANE urΦuje Φty°mi koeficienty a, b, c a d rovnici roviny ve tvaru:
ax + by + cz + d = 0
Co₧ je obecnß rovnice roviny v prostoru. Koeficienty a, b a c tedy urΦujφ normßlov² vektor roviny. Atributy pLeft a pRight urΦujφ boky, pTop a pBottom je strop a podlaha a nakonec pNear a pFar jsou p°ednφ a zadnφ st∞na.
Obr. 1: Frustum
┌kolem je tedy urΦit vÜech Üest rovin co nejp°esn∞ji. Vypadß to jako nep°ekonateln² problΘm, ale ve skuteΦnosti je to trochu analytickΘ geometrie ze st°ednφ Ükoly. Nßsleduje novß metoda t°φdy XCamera, kterß urΦφ Frustum naÜφ kamery:
void XCamera::ComputeClipVolume()
{
FLOAT dist, t1, t2;
D3DXVECTOR3 p, pt[8];
D3DXVECTOR3 v1, v2, n;
D3DXVECTOR3 vUpDir = D3DXVECTOR3(0.0f, 0.0f, 1.0f);
D3DXVECTOR3 vDir = m_vLookAtPoint - m_vEyePt ;
D3DXVECTOR3 vCross;
D3DXVec3Cross(&vCross, &vUpDir, &vDir);
D3DXVec3Cross(&vUpDir, &vDir, &vCross);
D3DXVec3Normalize(&vDir, &vDir);
D3DXVec3Normalize(&vCross, &vCross);
D3DXVec3Normalize(&vUpDir, &vUpDir);
for(INT i = 0; i < 8; i++)
{
dist = (i & 0x4) ? FAR_CLIP : NEAR_CLIP;
pt[i] = dist * vDir;
t1 = dist * tanf(FOV/2);
t1 = (i & 0x2) ? t1 : -t1;
pt[i] += vUpDir * t1;
t2 = dist * tanf(FOV/2) * ASPECT; // take into account screen proportions
t2 = (i & 0x1) ? -t2 : t2;
pt[i] += vCross * t2;
pt[i] += m_vEyePt;
}//compute the near plane
v1 = pt[2] - pt[0];
v2 = pt[1] - pt[0];
D3DXVec3Cross(&n, &v2, &v1);
D3DXVec3Normalize(&n, &n);
m_CV.pNear.a = n.x;
m_CV.pNear.b = n.y;
m_CV.pNear.c = n.z;
m_CV.pNear.d = -(n.x * pt[0].x + n.y * pt[0].y + n.z * pt[0].z);//compute the far plane
v1 = pt[5] - pt[4];
v2 = pt[6] - pt[4];
D3DXVec3Cross(&n, &v2, &v1);
D3DXVec3Normalize(&n, &n);
m_CV.pFar.a = n.x;
m_CV.pFar.b = n.y;
m_CV.pFar.c = n.z;
m_CV.pFar.d = -(n.x * pt[4].x + n.y * pt[4].y + n.z * pt[4].z);// - 10.0f;//compute the top plane
v1 = pt[6] - pt[2];
v2 = pt[3] - pt[2];
D3DXVec3Cross(&n, &v2, &v1);
D3DXVec3Normalize(&n, &n);
m_CV.pTop.a = n.x;
m_CV.pTop.b = n.y;
m_CV.pTop.c = n.z;
m_CV.pTop.d = -(n.x * pt[2].x + n.y * pt[2].y + n.z * pt[2].z);//compute the bottom plane
v1 = pt[1] - pt[0];
v2 = pt[4] - pt[0];
D3DXVec3Cross(&n, &v2, &v1);
D3DXVec3Normalize(&n, &n);
m_CV.pBottom.a = n.x;
m_CV.pBottom.b = n.y;
m_CV.pBottom.c = n.z;
m_CV.pBottom.d = -(n.x * pt[0].x + n.y * pt[0].y + n.z * pt[0].z);//compute the left plane
v1 = pt[3] - pt[1];
v2 = pt[5] - pt[1];
D3DXVec3Cross(&n, &v2, &v1);
D3DXVec3Normalize(&n, &n);
m_CV.pLeft.a = n.x;
m_CV.pLeft.b = n.y;
m_CV.pLeft.c = n.z;
m_CV.pLeft.d = -(n.x * pt[1].x + n.y * pt[1].y + n.z * pt[1].z);//compute the right plane
v1 = pt[4] - pt[0];
v2 = pt[2] - pt[0];
D3DXVec3Cross(&n, &v2, &v1);
D3DXVec3Normalize(&n, &n);
m_CV.pRight.a = n.x;
m_CV.pRight.b = n.y;
m_CV.pRight.c = n.z;
m_CV.pRight.d = -(n.x * pt[0].x + n.y * pt[0].y + n.z * pt[0].z);
}
Vypadß to mo₧nß d∞siv∞, ale nynφ si vÜe podrobn∞ vysv∞tlφme. Pomohou nßm nßsledujφcφ obrßzky. V hornφ Φßsti je vid∞t vztah mezi t°emi d∙le₧it²mi vektory: vDir, vUpDir a vCross. To se takΘ t²kß prvnφ Φßsti v k≤du, kde od sebe odeΦteme LookAtPoint a EyePoint a tφm zφskßme vektor kamery vDir. Dßle pot°ebujeme vektor vCross, kter² je kolm² k vDir, ale zßrove≥ musφ b²t kolm² k vektoru vUpDir. Pou₧ijeme tedy vektorov² souΦin. Potφ₧ je v tom, ₧e vUpDir nemusφ mφ°it p°φmo nahoru, ale bude naklon∞n tak, aby byl kolm² na vDir, kter² bude v naÜem p°φpad∞ mφ°it spφÜ k terΘnu. Musφme tedy zp∞tn∞ vyu₧φt vektor vCross a spoΦφtat sprßvn² svisl² vektor (op∞t pomocφ vektorovΘho souΦinu). Nakonec vÜechny tyto vektory znormalizujeme, aby m∞ly jednotkovou velikost.
Obr. 2: Frustum v detailu
Na dalÜφch obrßzcφch je vid∞t Frustum sm∞rem od kamery a shora. FOV je ·hel pohledu, kter² mßme definovan² jako PI/4, tedy 45 stup≥∙. V cyklu inicializujeme osm bod∙ komolΘho jehlanu (Φφsla bod∙ jsou vyznaΦena na obrßzku: 0-7). Nejprve poΦφtßme p°ednφ st∞nu, potΘ zadnφ, pou₧ijeme tedy bu∩ konstantu NEAR_CLIP nebo FAR_CLIP. PotΘ protßhneme vektor vDir o vzdßlenost st∞ny od kamery, tφm se dostaneme do st°edu danΘ st∞ny. Prom∞nnΘ t1 a t2 p°edstavujφ posun ve vertikßlnφm resp. horizontßlnφm sm∞ru. SpoΦφtßme je ·pln∞ jednoduÜe pomocφ tangenty ·hlu FOV/2 (protilehlß ku p°ilehlΘ, protilehlß je zde po₧adovanß prom∞nnß t1/t2 a p°ilehlß je vzdßlenost od kamery - tedy bu∩ NEAR_CLIP nebo FAR_CLIP. ProΦ bereme jen polovinu uhlu FOV je vid∞t s obrßzku. U t2 navφc musφme poΦφtat s tφm, ₧e Frustum nemß Φtvercovou podstavu, ale ₧e hrany jsou v pom∞ru 4:3 (konstanta ASPECT). Podle bodu, kter² zrovna poΦφtßme bude t1/t2 bu∩ kladnΘ nebo zßpornΘ a posuneme bod ve sm∞ru vDirUp a vCross. Na zßv∞r jen posuneme Frustum do skuteΦnΘ pozice kamery.
Nynφ u₧ bude hraΦka urΦit st∞ny Frustum, nebo¥ mßme hned Φty°i body, kterΘ le₧φ v ka₧dΘ rovin∞ (dokonce by staΦily t°i). Ji₧ jsem zmφnil, ₧e koeficienty a, b, c obecnΘ rovnice roviny w, jsou slo₧ky normßlovΘho vektoru n. UrΦφme tedy normßlovΘ vektory. To provedeme zcela jednoduÜe. Vybereme si libovolnΘ t°i body u ka₧dΘ st∞ny a spoΦφtßme z nich dva sm∞rovΘ vektory s1 a s2, kterΘ le₧φ v rovin∞ w. PotΘ pou₧ijeme vektorov² souΦin k urΦenφ vektoru kolmΘho na oba sm∞rovΘ vektory s1 a s2. To jsme ale zφskali vektor kolm² na rovinu w, proto₧e s1 a s2 le₧ely v rovin∞ w a zφskali jsme tudφ₧ po₧adovan² vektor n. Zb²vß umφstit rovinu v prostoru, tedy urΦit koeficient d. Ten zφskßme zp∞tn²m dosazenφm libovolnΘho bodu, kter² le₧φ v rovin∞, do stßvajφcφ rovnice. Po ·pravßch p°edchozφ rovnice dostaneme:
d = -(ax + by + cz)
a, b a c ji₧ mßme. x, y a z urΦujφ bod v rovin∞ (m∙₧eme si vybrat libovoln² z t∞ch Φty°, kterΘ znßme). Tyto v²poΦty provedeme pro ka₧dou st∞nu Frustum zvlßÜ¥.
V dalÜφ Φßsti se budeme zab²vat t°φdou CTerrain, kterß zapouzd°φ to, co jsme d∞lali v minulΘ lekci, to znamenß tvorbu terΘnu a navφc k tomu p°idßme podporu optimalizace pomocφ quad tree. O stromech se dozvφte vφce v p°φÜtφ lekci Kurzu o datov²ch strukturßch. Nßm ale budou staΦit ·plnΘ zßklady. Navφc teorii jsme ji₧ probrali minule a implementace nenφ p°φliÜ slo₧itß (pokud znßte rekurzi).
ZaΦneme tedy postupn∞. Zßklad bude struktura QTNode, kterß reprezentuje jeden uzel, pop°φpad∞ list stromu.
struct QTNode {
//
// Type of node - NODE, LEAF
NODETYPE ntType;
//
// Bounding points
BoundingPoint arBounds[4];
//
// Each node has four branches/children/kids
// LEAF has not any kids
QTNode *pBranches[4];
//
// leaf's tiles, each LEAF has 25 vertices (2 triangles per tile -> 32 triangles per LEAF)
// NODE has not any vertices
IVertexBuffer *pVB;
BOOL bVis;
};
Struktura obsahuje typ uzlu (bu∩ se jednß o obyΦejn² uzel nebo o list - tedy uzel bez potomk∙). Typ NODETYPE je definovßn takto:
typedef enum NODETYPE {
NODE_TYPE,
LEAF_TYPE,
};
NODE_TYPE tedy p°edstavuje obyΦejn² uzel a LEAF_TYPE list. Dßle uchovßvßme okrajovΘ body uzlu. Tato informace se nßm bude hodit p°i urΦovßnφ, zda-li je uzel uvnit° nebo vn∞ Frustum. Prom∞nna pBraches je pole Φty° ukazatel∙ na potomky danΘho uzlu. Uzly na nejni₧Üφ ·rovni naz²vßme listy, to nenφ nic novΘho a listy takΘ uchovßvajφ informaci o terΘnu. V naÜem p°φpad∞ se jednß p°φmo o Vertex buffer, co₧ nenφ zcela ideßlnφ, ale lΘpe to vy°eÜφme v p°φÜtφ lekci. Poslednφ atribut nßm jen °φkß, zda-li je list vid∞t Φi nikoliv. Tuto vlastnost bychom ani nepot°ebovali, ale chceme vykreslovat mapu viditeln²ch list∙ (viz. dßle). Na dalÜφm obrßzku vidφte v²znam v∞tÜiny prom∞nn²ch:
Obr. 3: Detail QTNode
ZelenΘ kuliΦky v rozφch p°edstavujφ body ulo₧enΘ v arBounds.
P°ejd∞me k samotnΘ t°φd∞ CTerrain:
class CTerrain
{
DWORD m_dwFlags;
// Common IB, heightmap and material
IIndexBuffer *m_pTerrainIB;
ITexture *m_pTerrainSurface;
ITexture *m_pHeightMap;
// Terrain dimensions
int m_iTerrainTilesX;
int m_iTerrainTilesY;
int m_iTerrainVerticesX;
int m_iTerrainVerticesY;
// Current terrain method
TERRAIN_METHOD m_iTerrainMethod;
I3DFont *m_pDebugFont;
std::vector <QTNode*> m_arLeaves;
std::vector <QTNode*> m_arVisQuads;
//
// Root of the quad tree
QTNode *m_pRoot;
//
// Vertices...
VERTEX **m_arTerrain;
//
// Common inicialization
HRESULT InternalInit();
int CreateNode(QTNode *pNode);
int FillLeaves();
int DestroyNode(QTNode *pNode);
int CircleAlgorithm();
int ComputeNormals();
int CreateQuadTree();
// Fills queue of the visible quads
HRESULT CullTerrain(QTNode *pNode, CLIPVOLUME& cv);
public:
HRESULT GenerateTerrain(int x, int y, TERRAIN_METHOD tmMethod = TM_CIRCLE);
HRESULT GenerateTerrainFromFile(LPCSTR szFile);
HRESULT DestroyTerrain();
int GetTitlesX() {return m_iTerrainTilesX;}
int GetTitlesY() {return m_iTerrainTilesY;}
void SetFlag(DWORD dwFlag) {m_dwFlags |= dwFlag;}
void UnsetFlag(DWORD dwFlag) {m_dwFlags &= ~dwFlag;}
void InvertFlag(DWORD dwFlag) { if(dwFlag & m_dwFlags) { UnsetFlag(dwFlag); } else { SetFlag(dwFlag);}}
void ResetFlags() {m_dwFlags = 0;}
HRESULT Render();
HRESULT Restore();
CTerrain(void);
~CTerrain(void);
};
V nßsledujφcφ tabulce najdete popis metod:
Nßzev |
Popis |
GenerateTerrain(int x, int y, TERRAIN_METHOD tmMethod); | Generuje nßhodn² terΘn po₧adovanou metodou o dan²ch rozm∞rech. Inicializuje prom∞nnΘ m_iTerrainTilesX, m_iTerrainTilesY, m_iTerrainVerticesX, m_iTerrainVerticesY a vytvo°φ pole m_arTerrain. Uklßdß metodu terΘnu: m_iTerrainMethod. |
GenerateTerrainFromFile(LPCSTR szFile); | Generuje terΘn z heightmapy danΘ parametrem. Inicializuje prom∞nnΘ m_iTerrainTilesX, m_iTerrainTilesY, m_iTerrainVerticesX, m_iTerrainVerticesY a vytvo°φ pole m_arTerrain. Dßle naΦte heightmapu a ukazatel ulo₧φ do m_pHeightMap. Uklßdß metodu terΘnu: m_iTerrainMethod. |
DestroyTerrain(); | Odstranφ terΘn z pam∞ti (uvolnφ se vÜechny pomocnΘ objekty, textury, VB a IB). |
GetTitlesX() | Vracφ velikost terΘnu ve sm∞ru x. |
GetTitlesY() | Vracφ velikost terΘnu ve sm∞ru y. |
SetFlag(DWORD dwFlag) | Nastavφ dan² p°φznak. Modifikuje prom∞nnou m_dwFlags. |
UnsetFlag(DWORD dwFlag) | Vynuluje p°φznak. Modifikuje prom∞nnou m_dwFlags. |
InvertFlag(DWORD dwFlag) | Invertuje p°φznak. Modifikuje prom∞nnou m_dwFlags. |
ResetFlags() | Vynuluje vÜechny p°φznaky. Modifikuje prom∞nnou m_dwFlags. |
Render() | Provede o°φznutφ viditeln²ch list∙ a vykreslφ terΘn, p°φpadn∞ mapu. Metoda by m∞la b²t volßna v bloku BeginScene()-EndScene(). |
Restore() | Obnovφ vÜechny zdroje po ztrßt∞ za°φzenφ (textury, IB a VB jednotliv²ch list∙). |
InternalInit() | Alokuje n∞kterΘ zdroje spoleΦnΘ pro oba typy tvorby terΘnu. Jednß se o texturu povrchu: m_pTerrainSurface, index buffer list∙ m_pTerrainIB a font m_pDebugFont. |
CreateNode(QTNode *pNode) | Rekurzivnφ metoda vytvß°ejφcφ strukturu quad tree. |
FillLeaves() | Naplnφ listy stromu daty, zkopφruje tedy vrcholy terΘnu do dφlΦφch VB. |
DestroyNode(QTNode *pNode) | Zcela uvolnφ pam∞¥ stromu tj. vyma₧e vÜechny uzle a listy i s jejich VB. |
CircleAlgorithm() | Aplikuje algoritmus kopeΦk∙ (viz. minulß lekce)). |
ComputeNormals() | SpoΦφtß normßly celΘho terΘnu. Op∞t jsme tento postup probφrali v minulΘ lekci. |
CreateQuadTree() | Vytvo°φ cel² quad tree. Nejprve tedy zinicializuje ko°en stromu a potΘ spustφ metody CreateNode(). |
CullTerrain(QTNode *pNode, CLIPVOLUME& cv) | Zajistφ o°ezßvßnφ terΘnu tzn. naplnφ pole viditeln²ch list∙. |
╚erven∞ jsou oznaΦeny soukromΘ metody. Nynφ si ukß₧eme jednotlivΘ metody podrobn∞ji.
Tvorba terΘnu:
HRESULT CTerrain::GenerateTerrainFromFile(LPCSTR szFile)
{
DWORD dwRet = S_FALSE;
int i = 0, x, y;
//
// Init common object (texture, font etc.)
InternalInit();
//
// Load heightmap texture
dwRet = CreateDisplayObject(DISIID_ITexture, (void**)&m_pHeightMap);
if(dwRet == S_OK)
{
dwRet = m_pHeightMap->LoadTextureFromFile(szFile);
if(dwRet != S_OK)
{
XException exp("Cannot load texture for heightmap!", dwRet);
THROW(exp);
}
if(m_pHeightMap->GetFormat() != D3DFMT_A8R8G8B8 && m_pHeightMap->GetFormat() != D3DFMT_X8R8G8B8)
{
XException exp("Invalid heightmap format! Must be 32-bit with alpha channel.");
THROW(exp);
}
//set dim of the terrain according dim of the height map
m_iTerrainTilesX = m_pHeightMap->Width() - 1;
m_iTerrainTilesY = m_pHeightMap->Height() - 1;
m_iTerrainVerticesX = m_pHeightMap->Width();
m_iTerrainVerticesY = m_pHeightMap->Height();
}
m_arTerrain = new VERTEX*[m_iTerrainVerticesX];
for(i = 0; i < m_iTerrainVerticesX; i++)
{
m_arTerrain[i] = new VERTEX[m_iTerrainVerticesY];
}
//
// Open heightmap
D3DLOCKED_RECT lr;
m_pHeightMap->GetTexture()->LockRect(0, &lr, NULL, D3DLOCK_READONLY);
TEXTURE_PIXEL * data = (TEXTURE_PIXEL*)lr.pBits;
int p = lr.Pitch/4;
// Init basic terrain parameters
for(y = 0; y < m_iTerrainVerticesY; y++)
{
for(x = 0; x < m_iTerrainVerticesX; x++)
{
TEXTURE_PIXEL tp = data[y*p + x];m_arTerrain[x][y].vecPos = D3DXVECTOR3(float(x), float(y), float(tp.a)/255.0f*20.0f-10.0f);
m_arTerrain[x][y].dwDiffuse = D3DCOLOR_ARGB(128, tp.r, tp.g, tp.b);
m_arTerrain[x][y].tu1 = (x % 2) ? 1.0f : 0.0f;
m_arTerrain[x][y].tv1 = (y % 2) ? 1.0f : 0.0f;
m_arTerrain[x][y].vecNormal = D3DXVECTOR3(0.0f, 0.0f, 1.0f);
}
}
m_pHeightMap->GetTexture()->UnlockRect(0);ComputeNormals();
//
// Create quad tree
CreateQuadTree();return dwRet;
}
Jak jsem ji₧ zmφnil, tato metoda vytvo°φ terΘn z heightmapy. Prakticky shrnuje vÜe, co jsme napsali v minulΘ lekci. Nejprve vytvo°φme spoleΦnΘ objekty jako je font nebo textura povrchu, pak naΦteme texturu heightmapy do pam∞ti, vytvo°φme 2D pole terΘnu a naplnφme ho daty podle textury. Nakonec spoΦteme normßly a vytvo°φme strom.
HRESULT CTerrain::GenerateTerrain(int x, int y, TERRAIN_METHOD tmMethod /*= TM_CIRCLE*/)
{
DWORD dwRet = S_FALSE;
int i = 0;
//
// Init common object (texture, font etc.)
InternalInit();m_iTerrainTilesX = x;
m_iTerrainTilesY = y;
m_iTerrainVerticesX = x + 1;
m_iTerrainVerticesY = y + 1;
m_arTerrain = new VERTEX*[m_iTerrainVerticesX];
for(i = 0; i < m_iTerrainVerticesX; i++)
{
m_arTerrain[i] = new VERTEX[m_iTerrainVerticesY];
}
//
// Init basic terrain parameters
for(y = 0; y < m_iTerrainVerticesY; y++)
{
for(x = 0; x < m_iTerrainVerticesX; x++)
{
m_arTerrain[x][y].vecPos = D3DXVECTOR3(float(x), float(y), 0.0f);
m_arTerrain[x][y].dwDiffuse = D3DCOLOR_ARGB(128,128,255,128);
m_arTerrain[x][y].tu1 = (x % 2) ? 1.0f : 0.0f;
m_arTerrain[x][y].tv1 = (y % 2) ? 1.0f : 0.0f;
m_arTerrain[x][y].vecNormal = D3DXVECTOR3(0.0f, 0.0f, 1.0f);
}
}
//
// Apply circle algorithm on the terrain
CircleAlgorithm();
ComputeNormals();
//
// Create quad tree
CreateQuadTree();
return dwRet;
}
Tato metoda je podobnß, op∞t se volß metoda InternalInit() a pak se vytvo°φ 2D pole vertex∙, kterΘ se naplnφ daty. Pou₧ije se algoritmus kopeΦk∙ a spoΦφtajφ se normßly. Na zßv∞r postavφme strom. V p°edchozφch metodßch takΘ uklßdßme velikost terΘnu. V prvnφm p°φpad∞ se bere podle velikost textury heightmapy. Ve druhΘm je pak dßna dv∞ma parametry.
Proto₧e s p°φpravou terΘnu ·zce souvisφ metoda InternalInit(), podφvejme se te∩ na nφ:
HRESULT CTerrain::InternalInit()
{
DWORD dwRet = S_FALSE;
DWORD dwTerrainIBSize, dwIndicesCount;
IDisplay *pDis;
CreateDisplayObject(DISIID_IDisplay, (void**) &pDis);
//
// Create font
if(S_OK == (dwRet = CreateDisplayObject(DISIID_I3DFont, (void**) &m_pDebugFont)))
{
m_pDebugFont->CreateFont(15, 8, "Arial", TRUE, D3DCOLOR_ARGB(255,150,255,100));
}
else
{
TRACE("Cannot create font: %d", dwRet);
return dwRet;
}
//
// Create common index buffer for all quad leaves
dwRet = CreateDisplayObject(DISIID_IIndexBuffer, (void**) &m_pTerrainIB);
if(dwRet == S_OK)
{
dwTerrainIBSize = LEAF_I_SIZE * LEAF_I_SIZE * 6 * sizeof(WORD);
dwIndicesCount = LEAF_I_SIZE * LEAF_I_SIZE * 6;
dwRet = m_pTerrainIB->Create(dwTerrainIBSize, D3DUSAGE_WRITEONLY, D3DFMT_INDEX16, D3DPOOL_DEFAULT);
if(dwRet != S_OK)
{
XException exp("Cannot create IB for terrain!", dwRet);
THROW(exp);
}
}//
// Load terrain texture
dwRet = CreateDisplayObject(DISIID_ITexture, (void**)&m_pTerrainSurface);
if(dwRet == S_OK)
{
dwRet = m_pTerrainSurface->LoadTextureFromFileEx("grass64.bmp", 6, pDis->GetTextureFormat());
if(dwRet != S_OK)
{
XException exp("Cannot load texture for terrain surface!", dwRet);
THROW(exp);
}
}SAFE_RELEASE(pDis);
return dwRet;
}
Nejednß se o nic slo₧itΘho, proto₧e metoda mß jen za ·kol vytvo°it pßr objekt∙: font, kter²m pφÜeme informace o terΘnu na obrazovku, index buffer jednoho listu, kter² vyu₧ijeme p°i vykreslovßnφ a textura povrchu terΘnu - naÜe znßmß trßva.
Veled∙le₧itou metodou a vlastn∞ podstatou je CreateNode(). Tato metoda je rekurzivnφ tzn. ₧e volß sama sebe pro ka₧d² uzel stromu. ZaΦφnßme od ko°ene v metod∞ CreateQuadTree():
int CTerrain::CreateQuadTree()
{
//
// Check terrain validity
if(!m_arTerrain)
{
XException exp("Terrain is not initialized. You cannot apply any algorithm.");
THROW(exp);
}
if(m_iTerrainVerticesX >= LEAF_I_SIZE && m_iTerrainVerticesY >= LEAF_I_SIZE)
{
m_pRoot = new QTNode;
// Init ROOT
m_pRoot->ntType = NODE_TYPE;
m_pRoot->pVB = NULL;
m_pRoot->arBounds[0].x = m_arTerrain[0][0].vecPos.x;
m_pRoot->arBounds[0].y = m_arTerrain[0][0].vecPos.y;
m_pRoot->arBounds[0].z = m_arTerrain[0][0].vecPos.z;
m_pRoot->arBounds[1].x = m_arTerrain[m_iTerrainVerticesX-1][0].vecPos.x;
m_pRoot->arBounds[1].y = m_arTerrain[m_iTerrainVerticesX-1][0].vecPos.y;
m_pRoot->arBounds[1].z = m_arTerrain[m_iTerrainVerticesX-1][0].vecPos.z;
m_pRoot->arBounds[2].x = m_arTerrain[0][m_iTerrainVerticesY-1].vecPos.x;
m_pRoot->arBounds[2].y = m_arTerrain[0][m_iTerrainVerticesY-1].vecPos.y;
m_pRoot->arBounds[2].z = m_arTerrain[0][m_iTerrainVerticesY-1].vecPos.z;
m_pRoot->arBounds[3].x = m_arTerrain[m_iTerrainVerticesX-1]
[m_iTerrainVerticesY-1].vecPos.x;
m_pRoot->arBounds[3].y = m_arTerrain[m_iTerrainVerticesX-1]
[m_iTerrainVerticesY-1].vecPos.y;
m_pRoot->arBounds[3].z = m_arTerrain[m_iTerrainVerticesX-1]
[m_iTerrainVerticesY-1].vecPos.z;CreateNode(m_pRoot);
//
// Fill index buffer and leaves with data
Restore();
}
else
{
return S_FALSE;
}
return S_OK;
}
Zde tedy p°ipravφme prvnφ uzel - ko°en - a zavolßme CreateNode(). Na zßv∞r naplnφme vÜechny listy daty pomocφ metody Restore(). VÜimn∞te si inicializace okrajov²ch bod∙. JednoduÜe pou₧ijeme krajnφ body pole m_arTerrain, proto₧e ko°en je p°es cel² terΘn, uzel s nejv∞tÜφ rozlohou.
int CTerrain::CreateNode(QTNode *pNode)
{
UINT uiWidth, uiHeight;
//
// Check terrain validity
if(!m_arTerrain)
{
XException exp("Terrain is not initialized. You cannot apply any algorithm.");
THROW(exp);
}
if(pNode)
{//
// Compute node width and height
uiWidth = int(pNode->arBounds[1].x - pNode->arBounds[0].x);
uiHeight = int(pNode->arBounds[2].y - pNode->arBounds[0].y);
//
// Get node type
if(uiWidth == LEAF_I_SIZE)
{
pNode->ntType = LEAF_TYPE;
// Create vertex buffer
if(S_OK == CreateDisplayObject(DISIID_IVertexBuffer, (void**) &pNode->pVB))
{
pNode->pVB->Create(LEAF_V_SIZE*LEAF_V_SIZE*sizeof(VERTEX),
D3DUSAGE_WRITEONLY, VERTEXFORMAT, D3DPOOL_DEFAULT);
pNode->bVis = FALSE;
m_arLeaves.push_back(pNode);
}
return 1;
}
else
{pNode->ntType = NODE_TYPE;
//
// Create 4 children// First
pNode->pBranches[0] = new QTNode;
pNode->pBranches[0]->pVB = NULL;
// Init new bounding box
pNode->pBranches[0]->arBounds[0] = pNode->arBounds[0];pNode->pBranches[0]->arBounds[1].x = pNode->arBounds[1].x - uiWidth / 2;
pNode->pBranches[0]->arBounds[1].y = pNode->arBounds[1].y;
pNode->pBranches[0]->arBounds[1].z = m_arTerrain[int(pNode->pBranches[0]->arBounds[1].x)]
[int(pNode->pBranches[0]->arBounds[1].y)].vecPos.z;pNode->pBranches[0]->arBounds[2].x = pNode->arBounds[2].x;
pNode->pBranches[0]->arBounds[2].y = pNode->arBounds[2].y - uiHeight / 2;
pNode->pBranches[0]->arBounds[2].z = m_arTerrain[int(pNode->pBranches[0]->arBounds[2].x)](
[int(pNode->pBranches[0]->arBounds[2].y)].vecPos.z;pNode->pBranches[0]->arBounds[3].x = pNode->arBounds[3].x - uiWidth / 2;
pNode->pBranches[0]->arBounds[3].y = pNode->arBounds[3].y - uiHeight / 2;
pNode->pBranches[0]->arBounds[3].z = m_arTerrain[int(pNode->pBranches[0]->arBounds[3].x)]
[int(pNode->pBranches[0]->arBounds[3].y)].vecPos.z;
// Second
pNode->pBranches[1] = new QTNode;
pNode->pBranches[1]->pVB = NULL;
// Init new bounding box
pNode->pBranches[1]->arBounds[0].x = pNode->arBounds[0].x + uiWidth / 2;
pNode->pBranches[1]->arBounds[0].y = pNode->arBounds[0].y;
pNode->pBranches[1]->arBounds[0].z = m_arTerrain[int(pNode->pBranches[1]->arBounds[0].x)]
[int(pNode->pBranches[1]->arBounds[0].y)].vecPos.z;pNode->pBranches[1]->arBounds[1] = pNode->arBounds[1];
pNode->pBranches[1]->arBounds[2].x = pNode->arBounds[2].x + uiWidth / 2;
pNode->pBranches[1]->arBounds[2].y = pNode->arBounds[2].y - uiHeight / 2;
pNode->pBranches[1]->arBounds[2].z = m_arTerrain[int(pNode->pBranches[1]->arBounds[2].x)]
[int(pNode->pBranches[1]->arBounds[2].y)].vecPos.z;pNode->pBranches[1]->arBounds[3].x = pNode->arBounds[3].x;
pNode->pBranches[1]->arBounds[3].y = pNode->arBounds[3].y - uiHeight / 2;
pNode->pBranches[1]->arBounds[3].z = m_arTerrain[int(pNode->pBranches[1]->arBounds[3].x)]
[int(pNode->pBranches[1]->arBounds[3].y)].vecPos.z;// Third
pNode->pBranches[2] = new QTNode;
pNode->pBranches[2]->pVB = NULL;
// Init new bounding box
pNode->pBranches[2]->arBounds[0].x = pNode->arBounds[0].x;
pNode->pBranches[2]->arBounds[0].y = pNode->arBounds[0].y + uiHeight / 2;
pNode->pBranches[2]->arBounds[0].z = m_arTerrain[int(pNode->pBranches[2]->arBounds[0].x)]
[int(pNode->pBranches[2]->arBounds[0].y)].vecPos.z;
pNode->pBranches[2]->arBounds[1].x = pNode->arBounds[1].x - uiWidth / 2;
pNode->pBranches[2]->arBounds[1].y = pNode->arBounds[1].y + uiHeight / 2;
pNode->pBranches[2]->arBounds[1].z = m_arTerrain[int(pNode->pBranches[2]->arBounds[1].x)]
[int(pNode->pBranches[2]->arBounds[1].y)].vecPos.z;pNode->pBranches[2]->arBounds[2] = pNode->arBounds[2];
pNode->pBranches[2]->arBounds[3].x = pNode->arBounds[3].x - uiWidth / 2;
pNode->pBranches[2]->arBounds[3].y = pNode->arBounds[3].y;
pNode->pBranches[2]->arBounds[3].z = m_arTerrain[int(pNode->pBranches[2]->arBounds[3].x)]
[int(pNode->pBranches[2]->arBounds[3].y)].vecPos.z;// Fourth
pNode->pBranches[3] = new QTNode;
pNode->pBranches[3]->pVB = NULL;
// Init new bounding box
pNode->pBranches[3]->arBounds[0].x = pNode->arBounds[0].x + uiWidth / 2;
pNode->pBranches[3]->arBounds[0].y = pNode->arBounds[0].y + uiHeight / 2;
pNode->pBranches[3]->arBounds[0].z = m_arTerrain[int(pNode->pBranches[3]->arBounds[0].x)]
[int(pNode->pBranches[3]->arBounds[0].y)].vecPos.z;
pNode->pBranches[3]->arBounds[1].x = pNode->arBounds[1].x;
pNode->pBranches[3]->arBounds[1].y = pNode->arBounds[1].y + uiHeight / 2;
pNode->pBranches[3]->arBounds[1].z = m_arTerrain[int(pNode->pBranches[3]->arBounds[1].x)]
[int(pNode->pBranches[3]->arBounds[1].y)].vecPos.z;pNode->pBranches[3]->arBounds[2].x = pNode->arBounds[2].x + uiWidth / 2;
pNode->pBranches[3]->arBounds[2].y = pNode->arBounds[2].y;
pNode->pBranches[3]->arBounds[2].z = m_arTerrain[int(pNode->pBranches[2]->arBounds[2].x)]
[int(pNode->pBranches[3]->arBounds[2].y)].vecPos.z;pNode->pBranches[3]->arBounds[3] = pNode->arBounds[3];
// Create children of the children
CreateNode(pNode->pBranches[0]);
CreateNode(pNode->pBranches[1]);
CreateNode(pNode->pBranches[2]);
CreateNode(pNode->pBranches[3]);}
return 0;}
return -1;
}
Metoda vypadajφcφ na prvnφ pohled slo₧it∞. Na druh² je to ·pln∞ jednoduchΘ. Nejprve pot°ebujeme spoΦφtat velikost aktußlnφho uzlu (tj. uzlu, pro kter² byla metoda volßna). Otec tohoto uzlu ovÜem pro nßs spoΦφtal okrajovΘ body, staΦφ tedy od sebe odeΦφst ty sprßvnΘ dva a dostaneme tak Üφ°ku uiWidth a "v²Üku" uiHeight uzlu. Dßle testujeme, zda-li jsme ji₧ nedosßhli nejni₧Üφ "listovΘ" ·rovn∞. Velikost listu definuje konstanta LEAF_I_SIZE. Pokud ano, aktußlnφ uzel je list a provedeme t°i operace: uzel si musφ pamatovat, ₧e je list, vytvo°φ se pro n∞j vertex buffer (zatφm se neplnφ) a vlo₧φ se do pole list∙ - v tomto bod∞ je metoda ukonΦena a vracφme se o ·rove≥ v²Ü.
Pokud je ale uzel jeÜt∞ p°φliÜ velk², je t°eba ho znovu rozΦtvrtit a pro ka₧dou Φtvrtinu zavolat CreateNode(). Tak₧e vytvo°φme Φty°i novΘ uzly a nastavφme jim novΘ krajnφ body tak, aby pokryly p°edchßzejφcφ uzel. Tohle si urΦit∞ nakreslete na kousek papφru!
Listy mßme ulo₧eny v poli m_arLeaves a nynφ je musφme naplnit daty z hlavnφho pole celΘho terΘnu m_arTerrain:
int CTerrain::FillLeaves()
{
QTNode *pNode;
VERTEX *pVertices;
int v, y, x, i;
for(i = 0; i < (int)m_arLeaves.size(); i++)
{
pNode = (QTNode*)m_arLeaves[i];
pNode->pVB->GetBuffer()->Lock(0, 0, (BYTE**) &pVertices, 0);
v = 0;
for(y = (int)pNode->arBounds[0].y; y <= (int)pNode->arBounds[2].y; y++)
{
for(x = (int)pNode->arBounds[0].x; x <= (int)pNode->arBounds[1].x; x++)
{
pVertices[v] = m_arTerrain[x][y];
v++;
}
}
pNode->pVB->GetBuffer()->Unlock();
}
return -1;
}
Zb²vß metoda pro napln∞nφ a obnovu Vertex a Index bufferu. P°i ztrßt∞ za°φzenφ (a i p°i inicializaci) se automatick² volß metoda Restore(), kterß naplnφ IB a volß metodu pro napln∞nφ vÜech list∙:
HRESULT CTerrain::Restore() if(m_arTerrain)
{
TRACE("Restoring terrain surface...");
dwRet = m_pTerrainIB->GetBuffer()->Lock(0, 0, (BYTE**)&pIndices, 0);
for(y = 0; y < LEAF_I_SIZE; y++)
{
for(x = 0; x < LEAF_I_SIZE; x++)
{
pIndices[i + 0] = x + y * LEAF_V_SIZE;
pIndices[i + 1] = (x+1) + y * LEAF_V_SIZE;
pIndices[i + 2] = x + (y+1) * LEAF_V_SIZE;
i += 3;
pIndices[i + 0] = (x+1) + y * LEAF_V_SIZE;
pIndices[i + 1] = (x+1) + (y+1) * LEAF_V_SIZE;
pIndices[i + 2] = x + (y+1) * LEAF_V_SIZE;
i += 3;
}
}
dwRet = m_pTerrainIB->GetBuffer()->Unlock();
dwRet = FillLeaves();
}
return dwRet;
}
V Index bufferu jsou odkazy v rßmci jednoho listu, proto₧e listy budeme vykreslovat postupn∞. Princip naleznete v minulΘ lekci - je to jako kdybychom m∞li mal² povrch o rozm∞rech jednoho listu.
Nynφ si ukß₧eme jak o°ezßvat listy, kterΘ jsou mimo Frustum. O°ezßvßnφ se provßdφ v metod∞ CullTerrain(). Jednß se op∞t o rekurzivnφ metodu (vÜimn∞te si, ₧e pokud pracujeme se stromem, rekurze nßm podstatn∞ zjednoduÜuje ₧ivot). V²sledek metody je seznam viditeln²ch list∙:
// Fills queue of the visible quads
HRESULT CTerrain::CullTerrain(QTNode *pNode, CLIPVOLUME& cv)
{
DWORD zones[4] = {0, 0, 0, 0};
FLOAT x, y, z;
float temp;
for(int i = 0; i < 4; i++)
{
x = pNode->arBounds[i].x;
y = pNode->arBounds[i].y;
z = pNode->arBounds[i].z;
temp = cv.pNear.a * x + cv.pNear.b * y + cv.pNear.c * z + cv.pNear.d;
if (temp > CULL_TOLERANCE) {
zones[i] |= 0x01;
}
else {
temp = cv.pFar.a * x + cv.pFar.b * y + cv.pFar.c * z + cv.pFar.d;
if (temp > CULL_TOLERANCE) {
zones[i] |= 0x02;
}
}
temp = cv.pLeft.a * x + cv.pLeft.b * y + cv.pLeft.c * z + cv.pLeft.d;
if (temp > CULL_TOLERANCE) {
zones[i] |= 0x04;
}
else {
temp = cv.pRight.a * x + cv.pRight.b * y + cv.pRight.c * z + cv.pRight.d;
if (temp > CULL_TOLERANCE) {
zones[i] |= 0x08;
}
}
temp = cv.pTop.a * x + cv.pTop.b * y + cv.pTop.c * z + cv.pTop.d;
if (temp > CULL_TOLERANCE+2.0f) {
zones[i] |= 0x10;
}
else
{
temp = cv.pBottom.a * x + cv.pBottom.b * y + cv.pBottom.c * z + cv.pBottom.d;
if (temp > CULL_TOLERANCE) {
zones[i] |= 0x20;
}
}
}
//if all of the corners are outside of the boundaries
// this node is excluded, so stop traversing
DWORD res = zones[0] & zones[1] & zones[2] & zones[3];
if(res)
{
return -1;
}
// if this is a leaf add the triangle lists to the render queue
if (pNode->ntType == LEAF_TYPE)
{
pNode->bVis = TRUE;
m_arVisQuads.push_back(pNode);
return 1;
}
else
{
//this is not a leaf traverse deeper
for(i = 0; i < 4; i++) {
CullTerrain(pNode->pBranches[i], cv);
}
}
return 0;
}
Princip metody spoΦφvß v tom, ₧e zkoumßme vÜechny Φty°i krajnφ body ka₧dΘho uzlu a testujeme, zda-li jsou uvnit° Frustum nebo ne. Pokud je alespo≥ jeden bod uvnit°, pova₧ujeme uzel za viditeln² a pokud se jednß o list, p°idßme ho do pole m_arVisQuads. V opaΦnΘm p°φpad∞ volßme metody CullTerrain() pro vÜechny potomky.
JeÜt∞ se vrßtφm k testu. Zde koneΦn∞ vyu₧ijeme naÜφ novou strukturu CLIPVOLUME. Postupn∞ vezmeme vÜechny st∞ny Frustum a dosazujeme krajnφ body uzlu. Pokud by bod le₧el p°esn∞ v rovin∞ (co₧ se ve sv∞te float∙ prakticky nem∙₧e stßt), vyÜel by v²sledek 0. Nßs ale zajφmß situace, kdy je bod venku (uvnit°). Pak vyjde v²sledek kladn² (v naÜem p°φpad∞ pou₧φvßme konstantu CULL_TOLERANCE, pomocφ kterΘ m∙₧eme °φdit p°esah) a to zaznamenßme v poli zones, kam se postupn∞ p°idßvß informace o bodech, kterΘ jsou mimo. Pokud jsou mimo vÜechny, metoda se ukonΦφ.
Na zßv∞r si jeÜt∞ ukß₧eme vykreslovacφ metodu Render().
HRESULT CTerrain::Render()
{
IDisplay *pDis;
CLIPVOLUME cv;
PIXEL v;
int i;
char szInfo[255];
CreateDisplayObject(DISIID_IDisplay, (void**) &pDis);if(m_dwFlags & TF_DRAWQUADSMAP)
{
for(i = 0; i < (int)m_arLeaves.size(); i++)
{
((QTNode*)m_arLeaves[i])->bVis = FALSE;
}
}m_arVisQuads.clear();
pDis->GetCamera()->GetClipVolume(cv);
CullTerrain(m_pRoot, cv);pDis->GetDevice()->SetVertexShader(VERTEXFORMAT);
pDis->GetDevice()->SetIndices(m_pTerrainIB->GetBuffer(), 0);
pDis->GetDevice()->SetTexture(0, m_pTerrainSurface->GetTexture());if(m_dwFlags & TF_USEQUADS)
{
for(i = 0; i < (int)m_arVisQuads.size(); i++)
{
pDis->GetDevice()->SetStreamSource(0,((QTNode*)m_arVisQuads[i])->pVB->GetBuffer(),
sizeof(VERTEX));
pDis->GetDevice()->DrawIndexedPrimitive(D3DPT_TRIANGLELIST,0,LEAF_V_SIZE*LEAF_V_SIZE,0,
LEAF_I_SIZE*LEAF_I_SIZE * 2);
}
sprintf(szInfo, "Using quad tree culling\nTotal leaves: %d\nTotal faces: %d",
(int)m_arVisQuads.size(),(int)m_arVisQuads.size() * LEAF_I_SIZE * LEAF_I_SIZE * 2);
}
else
{
for(i = 0; i < (int)m_arLeaves.size(); i++)
{
pDis->GetDevice()->SetStreamSource(0, ((QTNode*)m_arLeaves[i])->pVB->GetBuffer(),
sizeof(VERTEX));
pDis->GetDevice()->DrawIndexedPrimitive(D3DPT_TRIANGLELIST,0,LEAF_V_SIZE*LEAF_V_SIZE,0, LEAF_I_SIZE*LEAF_I_SIZE * 2);
}
sprintf(szInfo, "Without quad tree culling\nTotal leaves: %d\nTotal faces: %d",
(int)m_arLeaves.size(), (int)m_arLeaves.size() * LEAF_I_SIZE * LEAF_I_SIZE * 2);
}if(m_dwFlags & TF_DRAWQUADSMAP)
{
pDis->GetDevice()->SetVertexShader(PIXELFORMAT);
pDis->GetDevice()->SetRenderState(D3DRS_LIGHTING, FALSE);
pDis->GetDevice()->SetTexture(0, NULL);
for(i = 0; i < (int)m_arLeaves.size(); i++)
{
QTNode *pNode = (QTNode*)m_arLeaves[i];
v.vecPos.x = pNode->arBounds[0].x/LEAF_I_SIZE +
pDis->GetResolution()->x - m_iTerrainTilesX/LEAF_I_SIZE - 10;
v.vecPos.y = pNode->arBounds[0].y/LEAF_I_SIZE + 10;
v.vecPos.z = 0.0f;
v.rhw = 1.0f;
if(pNode->bVis)
{
v.dwDiffuse = D3DCOLOR_ARGB(255,255,255,0);
}
else
{
v.dwDiffuse = D3DCOLOR_ARGB(255,128,128,128);
}
pDis->GetDevice()->DrawPrimitiveUP(D3DPT_POINTLIST, 1, &v, sizeof(PIXEL));
}
pDis->GetDevice()->SetRenderState(D3DRS_LIGHTING, TRUE);
}
m_pDebugFont->Draw(szInfo, 0, 300);SAFE_RELEASE(pDis);
return 0;
}
Pokud mßme aktivnφ mapu, je t°eba vymazat viditelnost vÜech list∙. V opaΦnΘm p°φpad∞ atribut bVis v∙bec k niΦemu nepou₧φvßme, tak₧e tuto smyΦku m∙₧eme p°eskoΦit. Dßle pou₧ijeme objekt CLIPVOLUME z naÜφ kamery a provedeme o°ezßnφ metodou CullTerrain().
P°ed vykreslenφm musφme nastavit znßmΘ parametry jako formßt vertex∙, texturu a index bufferu. PotΘ se rozhoduje, jak²m zp∙sobem budeme listy vykreslovat. Pokud je zapnutß optimalizace, vykreslujφ se pouze viditelnΘ listy z pole m_arVisQuads. V opaΦnΘm p°φpad∞ se vykreslφ vÜechny listy z pole m_arLeaves.
V poslednφ Φßsti vykreslujeme mapu (pokud o to u₧ivatel stojφ). Mapa je tvo°ena v pravΘm hornφm rohu a zatφm ji vykreslujeme po pixelech. ka₧d² pixel p°edstavuje list. ViditelnΘ jsou ₧lutΘ, ostatnφ ÜedΘ. Pou₧φvßme k tomu transformovanΘ vrcholy PIXEL, u nich₧ nastavujeme pouze polohu (p°φmo v sou°adnicφch obrazovky) a barvu.
Na zßv∞r vykreslφme informace o terΘnu. Vypisuje se pouze poΦet viditeln²ch list∙ a celkov² poΦet polygon∙.
A co nßs Φekß v p°φÜtφ lekci? Dßle budeme zdokonalovat nßÜ jednoduch² engine. Nap°φklad p°idßme oblohu a podφvßme se, jak dßle upravit a zrychlit vykreslovanφ terΘnu (optimalizovat lze tΘm∞° donekoneΦna). Dßle bych cht∞l vylepÜit podporu sv∞tel. Budou to tedy spφÜe takovΘ "kosmetickΘ" zm∞ny ne₧ revoluce, kterou jste vid∞li dnes.
T∞Üφm se p°φÜt∞ nashledanou.