Jak jsem slíbil v minulé lekci, dnes se budeme věnovat většímu projektu, který jsem pro Vás připravil a který tu podrobněji rozebereme.
Projekt bude mít podobnou strukturu jako projekt z minulých lekcí o DirectDraw. Dnes zatím vytvoříme tři pod-projekty: Display, Tester a Setup. Navíc ještě budeme používat knihovnu Common, aniž bychom ji vkládali do projektu.
Co bude náš projekt zatím umět? Nic moc, ale postupně budeme přidávat nové funkce, které budeme testovat pomocí aplikace Tester. Dnes se tedy budeme zabývat především projektem Display. Výsledkem projektu Setup bude aplikace Setup.exe, kterou použijeme k nastavení grafiky na konkrétním PC. Program generuje soubor Setup.ini, kde jsou všechny tyto nastavení uloženy. To pak použijeme v našem "enginu" pro správné nastavení vlastností device atd. Touto aplikací se podrobněji zabývat nebudeme.
Nechte-li projekt psát sami, můžete použít ten, který najdete v sekci Downloads. Projekt má následující strukturu:
Pro ty, co si chtějí projekt vytvořit sami jen upřesním typy jednotlivých aplikací. Nepoužíváme žádné MFC, takže všechny projekty jsou výhradně Win32 API. Display je dynamická knihovna a Tester aplikace. Nezapomeňte nastavit správně závislosti (Dependencies) a také cestu k výstupním souborům jako společný adresář Debug či Release. Všechny projekty musí vkládat knihovnu Common.dll pomocí Common.lib (tento soubor najdete u projektu v sekci Downloads). Dále Display a Setup vkládají knihovny d3d8.lib a D3dx8.lib, abychom v nich mohli použít funkce Direct3D. Projektem Setup se zde zabývat nebudu, takže ho zkopírujte a vložte jako existující projekt.
V další části si povíme trochu teorie o ztrátě zařízení.
Pamatujete si na ztráty povrchů v DirectDraw? Bohužel ani v D3D se nevyhneme problémům, pokud se uživatel přepne násilím z fullscreenu do Windows. V Direct3D nastane tzv. ztráta zařízení. V tomto stavu je zařízení nepoužitelné a je potřeba udělat pár kroků k jeho nápravě.
Poznámka: Pokud jedete ve window režimu, může dojít ke ztrátě také a to tak, že zároveň spustíte aplikaci, která půjde do fullscreenu.
Co musíte udělat pro obnovu? Tyto tři kroky:
Uvolnit všechny zdroje - textury, meshe, shadery, index a vertex buffery.
Resetovat zařízení a chvilku počkat.
Obnovit všechny zdroje tj. znovu načíst všechny textury, meshe atd.
Proto budou mít všechny objekty, které mají charakter zdroje, metodu Release(), která objekt uvolní a metodu Restore(), který požadovaný objekt uvede do původního stavu. Dále je třeba volat metodu Reset() rozhraní zařízení s původním nastavením zařízení (objekt D3DPRESENT_PARAMETERS).
Důležité je tedy zjišťovat, zda-li nedošlo ke ztrátě zařízení. Problém je ten, že vlastně všechny metody vrací D3D_OK, i když došlo ke ztrátě. Jediná metoda zařízení, která to odhalí je TestCooperativeLevel(), která vrací D3DERR_DEVICELOST při ztrátě a D3DERR_DEVICENOTRESET v případě, že je potřeba udělat reset zařízení.
Nyní se podrobněji podíváme na projekt Display.
Jak bychom asi čekali, tento projekt bude spravovat grafiku založenou na Direct3D. Po dnešní lekci toho ještě příliš umět nebude, ale proberme si těch pár bodů:
inicializace systému Direct3D. Nastavení grafiky se nahrává ze souboru "Setup.ini".
jednoduchá (prozatím) správa zdrojů. Zatím jen textur a meshů.
korektní reakce na ztrátu zařízení včetně destrukce a obnovy zdrojů.
nahrávaní meshů ze souborů .x nebo vytváření předdefinovaných tvarů pomocí funkcí knihovny D3DX.
jednoduché sledování výkonu aplikace pomocí ukazatele FPS a omezovač FPS.
Všechny tyto věci jsou potřebné a budeme je v dalších lekcích rozšiřovat. Navíc jsem přidal do inicializace jedno světlo, aby byly vidět kontury objektů. Nicméně světla si necháme na příští lekci.
Třídy knihovny:
XDisplay - inicializace, FPS, obnova po ztrátě device
XResourceManager - správa zdrojů
XTexture - objekt textury
XMesh - objekt mesh - síťový model
Struktury:
prozatím jen struktura Resolution, která uchovává informaci o rozlišení
Začneme pěkně od spodu. Tento objekt představuje texturu. Mohli bychom sice přímo použít rozhraní IDirect3DTexture8, ale minimálně budeme potřebovat informace k tomu, abychom mohli texturu kdykoliv zrušit a znovu obnovit. Textury budeme zatím vytvářet z externích souborů, takže zřejmě budeme potřebovat cestu k tomuto souboru:
class DISPLAY_API
XTexture
{
LPDIRECT3DTEXTURE8 m_lpTexture;
// information
for restoring
char m_szFilePath[MAX_PATH];
LPDIRECT3DDEVICE8 m_lpDevice;
public:
int LoadTextureFromFile(LPCSTR szFileName, LPDIRECT3DDEVICE8
lpDevice);
int Restore();
void Release();
LPDIRECT3DTEXTURE8 GetTexture() { return m_lpTexture; }
LPDIRECT3DDEVICE8 GetDevice() {return m_lpDevice; }
public:
XTexture(void);
~XTexture(void);
};
Takže krom samotného rozhraní IDirect3DTexture8, tu máme ukazatel na zařízení a cestu k souboru bitmapy, ze které je textura vytvořena.
Metody:
LoadTextureFromFile() - Nahrávaní textury ze souboru. Soubor bitmapy se hledá v adresáři Debug nebo Release podle kompilace. Je nutné předávat zařízení, které si interně uložíme pro obnovu textury.
Restore() - Obnova textury.
Release() - Uvolnění textury.
GetTexture() - Vrací ukazatel na rozhraní textury.
GetDevice() - Vrací ukazatel na rozhraní zařízení.
Implementace:
1) Konstruktor a destruktor
XTexture::XTexture(void)
{
m_lpTexture = NULL;
m_lpDevice = NULL;
}
XTexture::~XTexture(void)
{
Release();
m_lpDevice = NULL;
}
2) Nahrávání textury ze souboru
int XTexture::LoadTextureFromFile(LPCSTR
szFileName, LPDIRECT3DDEVICE8 lpDevice)
{
if(!lpDevice)
{
THROW("LoadTextureFromFile: Device is
invalid.");
}
if(!m_lpTexture)
{
// create full path to the texture
if(!cmnGetDataFilePath(m_szFilePath, szFileName))
{
TRACE("Cannot
create full path to the texture.");
}
// try to create texture
if(D3D_OK == D3DXCreateTextureFromFile(lpDevice, m_szFilePath, &m_lpTexture))
{
TRACE("Texture
'%s' was loaded.", szFileName);
}
else
{
TRACE("Texture
'%s' wasn't loaded.", szFileName);
}
// save information to restore
m_lpDevice = lpDevice;
return 0;
}
return -1;
}
Nejprve otestujeme zařízení, případně vyhodíme výjimku. Dále testujeme zda-li
textura již není vytvořena. Vytvoříme cestu k souboru pomocí funkce
cmnGetDataFilePath() a pokusíme se nahrát texturu
pomocí funkce D3DXCreateTextureFromFile(). Nakonec
uložíme ukazatel na zařízení.
3) Uvolnění textury
void XTexture::Release()
{
// release
texture
SAFE_RELEASE(m_lpTexture);
}
4) Obnova textury
int XTexture::Restore()
{
// reinit texture
if(m_lpDevice)
{
// try to recreate texture
if(D3D_OK == D3DXCreateTextureFromFile(m_lpDevice, m_szFilePath, &m_lpTexture))
{
TRACE("Texture
'%s' was reloaded.", m_szFilePath);
}
else
{
TRACE("Texture
'%s' wasn't reloaded.", m_szFilePath);
}
}
return -1;
}
Další v řadě je objekt XMesh. Nejprve si povíme, co je to vlastně mesh. Objekty v Direct3D jsou sestaveny z polygonů, nejčastěji z trojúhelníků. To již víte. Mesh je síťový model objektu - čili informace o tvaru objektu, případně materiálu či textuře. Informace jsou uloženy v index a vertex bufferech (více si o tomto povíme v dalších lekcích). Podívejme se na obrázek:
Toto je drátový model konvičky (teapot). Tento objekt je reprezentován rozhraním ID3DXMesh. Meshe můžete načítat z externího souboru s příponou .x, případně ze souboru 3D Studia .3ds. Existuje plugin pro 3D Studio, který exportuje model do souboru .x. Tento plugin si můžete stáhnout v sekci Downloads.
V našem enginu budeme prozatím vytvářet předdefinované tvary pomocí knihovny D3DX nebo ze souboru .x. V knihovně D3DX je mnoho funkcí pro práci s meshi. Prozatím budeme využívat tyto funkce:
D3DXLoadMeshFromX - načte mesh ze souboru .x. Parametry: jméno souboru, nastavení, zařízení, pole 3 DWORD pro každý face meshe - určuje sousední facy, pole materiálů, počet materiálů, výsledný mesh. Dnes nás budou zajímat pouze zelené parametry, ostatní jsou NULL nebo 0.
D3DXCreateSphere - vytvoří kouli o zadaném poloměru. Parametry: zařízení, poloměr, počet svislých "krajíců", počet vodorovných "pater", výsledný mesh.
D3DXCreateBox - vytvoří kvádr o zadaných rozměrech. Parametry: zařízení, šířka, výška, hloubka, výsledný mesh.
D3DXCreateCylinder - vytvoří válec o zadaných rozměrech. Parametry: zařízení, poloměr1, poloměr2, délka, počet svislých "krajíců", počet vodorovných "pater", výsledný mesh.
D3DXCreateTeapot - vytvoří konvičku:) Parametry: zařízení, výsledný mesh. Konvička nemá žádné další parametry.
D3DXCreateTorus - vytvoří prstenec (kobliha s dírou:) Parametry: zařízení, vnitřní poloměr, vnější poloměr, počet stran, počet prstenců, výsledný mesh.
V naší třídě XMesh musí být opět informace, abychom mohli mesh v případě ztráty zařízení obnovit. U meshe, který je nahráván ze souboru to není problém (je to stejné jako u textury). Menší problém nastává u předdefinovaných objektů. Každý má několik parametrů, nejvíce jich má válec: 3x float, 2x int. I tyto proměnné musí být zahrnuty, ale ne vždy se všechny využijí. Tím dochází k mírnému plýtvání místa a pokud budete používat převážně meshe ze souboru, je lepší udělat zvláštní třídu. Navíc objekt meshe musí sám vědět co je zač.
Třída XMesh:
enum MESHTYPE {BOX,
CYLINDER, SPHERE, TEAPOT, TORUS, CUSTOM };
class DISPLAY_API XMesh
{
LPD3DXMESH m_lpMesh;
// information
for restoring
char m_szFilePath[MAX_PATH];
LPDIRECT3DDEVICE8 m_lpDevice;
MESHTYPE m_meshType;
float m_fPar1, m_fPar2, m_fPar3;
UINT m_uPar4, m_uPar5;
public:
int LoadMeshFromFile(LPCSTR szFileName, LPDIRECT3DDEVICE8
lpDevice);
int CreateSphere(float fRadius, UINT uSlices, UINT uStacks,
LPDIRECT3DDEVICE8 lpDevice);
int CreateBox(float fWidth, float fHeight, float fDepth,
LPDIRECT3DDEVICE8 lpDevice);
int CreateCylinder(float fRadius1, float fRadius2, float
fLenght, UINT uSlices, UINT uStacks, LPDIRECT3DDEVICE8 lpDevice);
int CreateTeapot(LPDIRECT3DDEVICE8 lpDevice);
int CreateTorus(float fInnerRadius, float fOuterRadius, UINT
uSides, UINT uRings, LPDIRECT3DDEVICE8 lpDevice);
int Restore();
void Release();
LPD3DXMESH GetMesh() { return m_lpMesh; }
LPDIRECT3DDEVICE8 GetDevice() {return m_lpDevice; }
public:
XMesh(void);
~XMesh(void);
};
Metody jsou podobné jako u textury, takže je zde nebudu podrobně rozepisovat. Jen si všimněte parametrů m_fPar1 - m_fPar5. To jsou právě parametry předdefinovaných objektů. Metody na inicializaci meshe jsem popsal o něco výše.
Implementace:
1) Konstruktor a destruktor
XMesh::XMesh(void)
{
m_lpMesh = NULL;
m_lpDevice = NULL;
}
XMesh::~XMesh(void)
{
Release();
m_lpDevice = NULL;
}
2) Nahráváme mesh ze souboru - metoda je podobná jako
LoadTextureFromFile()
int XMesh::LoadMeshFromFile(LPCSTR
szFileName, LPDIRECT3DDEVICE8 lpDevice)
{
if(!lpDevice)
{
THROW("LoadMeshFromFile: Device is
invalid.");
}
if(!m_lpMesh) {
// create full path to the texture
if(!cmnGetDataFilePath(m_szFilePath, szFileName))
{
TRACE("Cannot
create full path to the texture.");
}
// load mesh
if(D3D_OK == D3DXLoadMeshFromX(m_szFilePath, 0, lpDevice, NULL, NULL, NULL, &m_lpMesh))
{
TRACE("Mesh
'%s' was loaded.", szFileName);
}
else
{
TRACE("Mesh
'%s' wasn't loaded.", szFileName);
}
// save par to restore
m_meshType = CUSTOM;
m_lpDevice = lpDevice;
return 0;
}
return -1;
}
3) Vytvoří mesh ve tvaru koule
int XMesh::CreateSphere(float fRadius, UINT
uSlices, UINT uStacks, LPDIRECT3DDEVICE8 lpDevice)
{
if(!lpDevice)
{
THROW("CreateSphere: Device is
invalid.");
}
if(!m_lpMesh) {
if(D3D_OK == D3DXCreateSphere(lpDevice,
fRadius, uSlices, uStacks, &m_lpMesh, NULL))
{
TRACE("Sphere
was loaded.");
}
else
{
TRACE("Sphere
wasn't loaded.");
}
// save par to restore
m_meshType = SPHERE;
m_lpDevice = lpDevice;
m_fPar1 = fRadius;
m_uPar4 = uSlices;
m_uPar5 = uStacks;
return 0;
}
return -1;
}
4) Vytvoří mesh ve tvaru kvádru
int XMesh::CreateBox(float fWidth, float
fHeight, float fDepth, LPDIRECT3DDEVICE8 lpDevice)
{
if(!lpDevice)
{
THROW("CreateBox: Device is
invalid.");
}
if(!m_lpMesh) {
if(D3D_OK == D3DXCreateBox(lpDevice,
fWidth, fHeight, fDepth, &m_lpMesh, NULL))
{
TRACE("Box
was loaded.");
}
else
{
TRACE("Box
wasn't loaded.");
}
// save par to restore
m_meshType = BOX;
m_lpDevice = lpDevice;
m_fPar1 = fWidth;
m_fPar2 = fHeight;
m_fPar3 = fDepth;
return 0;
}
return -1;
}
5) Vytvoří mesh ve tvaru válce
int XMesh::CreateCylinder(float fRadius1,
float fRadius2, float fLenght, UINT uSlices, UINT uStacks, LPDIRECT3DDEVICE8
lpDevice)
{
if(!lpDevice)
{
THROW("CreateCylinder: Device is
invalid.");
}
if(!m_lpMesh) {
if(D3D_OK == D3DXCreateCylinder(lpDevice,
fRadius1, fRadius2, fLenght, uSlices, uStacks, &m_lpMesh, NULL))
{
TRACE("Cylinder
was loaded.");
}
else
{
TRACE("Cylinder
wasn't loaded.");
}
// save par to restore
m_meshType = CYLINDER;
m_lpDevice = lpDevice;
m_fPar1 = fRadius1;
m_fPar2 = fRadius2;
m_fPar3 = fLenght;
m_uPar4 = uSlices;
m_uPar5 = uStacks;
return 0;
}
return -1;
}
6) Vytvoří mesh ve tvaru konvičky
int XMesh::CreateTeapot(LPDIRECT3DDEVICE8
lpDevice)
{
if(!lpDevice)
{
THROW("CreateTeapot: Device is
invalid.");
}
if(!m_lpMesh) {
if(D3D_OK == D3DXCreateTeapot(lpDevice,
&m_lpMesh, NULL))
{
TRACE("Teapot
was loaded.");
}
else
{
TRACE("Teapot
wasn't loaded.");
}
// save par to restore
m_meshType = TEAPOT;
m_lpDevice = lpDevice;
return 0;
}
return -1;
}
7) Vytvoří mesh ve tvaru prstence
int XMesh::CreateTorus(float fInnerRadius,
float fOuterRadius, UINT uSides, UINT uRings, LPDIRECT3DDEVICE8 lpDevice)
{
if(!lpDevice)
{
THROW("CreateTorus: Device is
invalid.");
}
if(!m_lpMesh) {
if(D3D_OK == D3DXCreateTorus(lpDevice,
fInnerRadius, fOuterRadius, uSides, uRings, &m_lpMesh, NULL))
{
TRACE("Torus
was loaded.");
}
else
{
TRACE("Torus
wasn't loaded.");
}
// save par to restore
m_meshType = TORUS;
m_lpDevice = lpDevice;
m_fPar1 = fInnerRadius;
m_fPar2 = fOuterRadius;
m_uPar4 = uSides;
m_uPar5 = uRings;
return 0;
}
return -1;
}
8) Obnoví mesh podle typu
int XMesh::Restore()
{
if(m_lpDevice)
{
switch(m_meshType) {
case CUSTOM:
// load mesh
if(D3D_OK == D3DXLoadMeshFromX(m_szFilePath, 0, m_lpDevice, NULL, NULL, NULL,
&m_lpMesh))
{
TRACE("Mesh '%s' was loaded.", m_szFilePath);
return 0;
}
else
{
TRACE("Mesh '%s' wasn't loaded.", m_szFilePath);
}
break;
case SPHERE:
return CreateSphere(m_fPar1, m_uPar4, m_uPar5, m_lpDevice);
case BOX:
return CreateBox(m_fPar1, m_fPar2, m_fPar3, m_lpDevice);
case CYLINDER:
return CreateCylinder(m_fPar1, m_fPar2, m_fPar3, m_uPar4, m_uPar5, m_lpDevice);
case TORUS:
return CreateTorus(m_fPar1, m_fPar2, m_uPar4, m_uPar5, m_lpDevice);
case TEAPOT:
return CreateTeapot(m_lpDevice);
}
}
return -1;
}
9) Uvolní rozhraní meshe
void XMesh::Release()
{
SAFE_RELEASE(m_lpMesh);
}
Všimněte si, že metody využívají funkce a makra z knihovny common.dll, takže nezapomeňte vložit soubor common.h.
Jak jsme si už pověděli, objekty typu mesh nebo textury je třeba obnovit po resetu zařízení. Abychom měli přehled o těchto objektech, vytvoříme jakýsi manager těchto objektů: XResourceManager, který bude udržovat seznam všech používaných textur a meshů. Takže budeme potřebovat nějaké dynamické pole, do kterého budeme ukládat ukazatele na tyto objekty. Dále tyto objekty budeme vkládat (později různě upravovat) a také budeme chtít všechny uvolnit a poté obnovit.
Náš resource manager bude pro začátek úplně jednoduchý:
#include <vector>
class DISPLAY_API XResourceManager
{
std::vector <XTexture*> m_arTextures;
std::vector <XMesh*> m_arMeshes;
public:
int AddTexture(XTexture * pTextureToAdd);
int AddMesh(XMesh * pMeshToAdd);
int ReleaseAll();
int RestoreAll();
public:
XResourceManager(void);
~XResourceManager(void);
};
Máme zatím textury a meshe, takže pro ně uděláme dynamické pole. K tomu použijeme objekt knihovny STL vector. Dále tu máme metody pro uložení ukazatele na texturu a meshe, metody pro uvolnění všech objektů a jejich obnovení.
Implementace:
1) Konstruktor a destruktor
XResourceManager::XResourceManager(void)
{
}
XResourceManager::~XResourceManager(void)
{
ReleaseAll();
m_arTextures.clear();
m_arMeshes.clear();
}
2) Vloží ukazatel na texturu - tím texturu
"zaregistrujete" a už se o ní nemusíte vůbec starat
int XResourceManager::AddTexture(XTexture *
pTextureToAdd)
{
if(pTextureToAdd) {
m_arTextures.push_back(pTextureToAdd);
return 0;
}
return -1;
}
3) Vloží ukazatel na mesh - tím mesh
"zaregistrujete" a už se o něj nemusíte vůbec starat
int XResourceManager::AddMesh(XMesh *
pMeshToAdd)
{
if(pMeshToAdd) {
m_arMeshes.push_back(pMeshToAdd);
return 0;
}
return -1;
}
4) Uvolní všechny objekty - textury i meshe
int XResourceManager::ReleaseAll()
{
// release
textures
for(int i = 0; i < (int)m_arTextures.size(); i++) {
m_arTextures[i]->Release();
}
for(int i = 0; i < (int)m_arMeshes.size(); i++) {
m_arMeshes[i]->Release();
}
return 0;
}
5) Obnoví všechny objekty
int XResourceManager::RestoreAll()
{
// restore
textures
for(int i = 0; i < (int)m_arTextures.size(); i++) {
m_arTextures[i]->Restore();
}
for(int i = 0; i < (int)m_arMeshes.size(); i++) {
m_arMeshes[i]->Restore();
}
return 0;
}
Jistě jste si všimli, že prozatím všechny metody vraceli hodnotu 0 v případě úspěchu, jinak hodnotu -1.
Z knihovny Display už nám zbývá pouze třída XDisplay.
Tato třída je prakticky nejdůležitější a proto si ji rozebereme na závěr. Pracuje totiž s předchozími objekty. Hlavní úkol této třídy je správně zinicializovat systém Direct3D, vyčistit pozadí, prohození bufferů a další věci, které budeme teprve časem doplňovat.
Atributy třídy můžeme rozdělit následovně:
objekty pro Direct3D (zařízení a objekt D3D + nastavení zařízení).
atributy nastavení formátu, rozlišení, typu zařízení, pozadí atd.
atributy pro práci s FPS - zde potřebujeme více atributů, protože nechceme počítat FPS každý cyklus (podrobnosti se dovíte níže).
atributy pro práci s časem - uchováváme čas od spuštění aplikace a čas od posledního obnovení snímku.
manager zdrojů
dočasné atributy (světlo a světová matice). Tyto objekty jsou zde pouze do příště, protože náš "engine" ještě neumí transformace a světla.
Poznámka: Rozlišení ukládáme do samostatné struktury Resolution, která pouze obsahuje velikost rozlišení v obou směrech.
Třída XDisplay v celé své kráse:
class DISPLAY_API
XDisplay
{
// D3D objects
IDirect3D8 *
m_lpD3DObject;
IDirect3DDevice8* m_lpD3DDevice;
D3DPRESENT_PARAMETERS m_d3dDeviceParam;
XResourceManager m_resManager;
//
// Display settings
WORD m_wFlags;
// Display
parameters
Resolution m_sRes;
UINT m_iDepth;
D3DDEVTYPE m_typeDeviceType;
int m_iAdapterOrdinal;
D3DFORMAT m_formatScreenDepth;
D3DFORMAT m_formatTextureDepth;
D3DTEXTUREFILTERTYPE m_tftMagTextureFilter;
D3DTEXTUREFILTERTYPE m_tftMinTextureFilter;
D3DTEXTUREFILTERTYPE m_tftMipTextureFilter;
//
// FPS atributes
float m_fStartTime;
float m_fStopTime;
FLOAT m_Frames;
float m_fCurrentFPS;
LPD3DXFONT m_lpFPSFont;
float m_fFrameRate;
int m_iDesiredFPS;
//
// Time between two frames
float m_fTime;
// Time from app
start
float m_fTotalTime;
// Background
color
D3DCOLOR m_dwBackgroundColor;
// Temporary var.
D3DXMATRIX *m_matWorld;
D3DLIGHT8 Light;
private:
int UpdateFPS();
void LimitFPS();
void Clean(void);
void BuildUpMatrices(D3DXVECTOR3 *pvEyePt, D3DXVECTOR3 *pvLookAtPt);
public:
int Init(HWND hWnd);
int UpdateBackground();
int Present();
int RestoreDisplay();
LPDIRECT3DDEVICE8 GetDevice() {return m_lpD3DDevice;}
XResourceManager* GetResourceManager() {return &m_resManager;
}
public:
XDisplay(void);
~XDisplay(void);
};
Všimněte si, že všechny třídy jsme doposud exportovali z knihovny. Atributy si podrobněji popíšeme až u implementace metod.
Metody:
Init() - načtení nastavení ze setup.ini a inicializace zařízení.
UpdateBackground() - vyčištění pozadí a Z-bufferu (dočasně transformace světové matice)
Present() - prohození bufferů, test ztráty zařízení, vykreslení a omezení FPS.
RestoreDisplay() - uvolnění všech zdrojů, reset zařízení a znovunačtení zdrojů.
UpdateFPS() - výpočet a zobrazení FPS.
LimitFPS() - omezení FPS.
Clean() - uvolnění objektů D3D.
BuildUpMatrices() - nastavení matic (dočasně také nastavení světla)
Implementace:
1) Konstruktor, destruktor a čistící funkce Clean()
XDisplay::XDisplay(void)
{
m_lpD3DObject = NULL;
m_lpD3DDevice = NULL;
m_matWorld = new D3DXMATRIX;
}
XDisplay::~XDisplay(void)
{
Clean();
delete m_matWorld;
}
void XDisplay::Clean(void)
{
if(m_lpD3DDevice) {
m_lpD3DDevice->SetTexture(0, NULL);
m_resManager.ReleaseAll();
SAFE_RELEASE(m_lpD3DDevice);
SAFE_RELEASE(m_lpD3DObject);
}
}
Na začátku musíme vynulovat ukazatel na zařízení, abychom poznali, zda-li je třída již zinicializována. Naopak při uvolnění musíme vyjmout texturu "ze zařízení", aby tato textura šla uvolnit. Dále uvolníme všechny zdroje a nakonec objekty Direct3D.
2) Inicializace systému
int XDisplay::Init(HWND
hWnd)
{
TRACE("Initializing Direct3D system...");
HRESULT dwRet;
DWORD BehaviorFlags;
//
// Already initialized?
if(m_lpD3DDevice)
{
THROW("Direct3D system is already
initialized.");
}
//
// As first, create one Direct3D object
m_lpD3DObject =
Direct3DCreate8(D3D_SDK_VERSION);
if(!m_lpD3DObject) {
cmnMessageBoxA("Please, install
DirectX 8.1.", MSG_ERROR);
THROW("Cannot create object of
Direct3D.");
}
//
//
// Read desired display format from ini file
//
// Get resolution values from setup.ini file
m_sRes.cx =
cmnReadSetupInt(_S_SECTION_DISPLAY, _S_KEY_WIDTH);
m_sRes.cy = cmnReadSetupInt(_S_SECTION_DISPLAY, _S_KEY_HEIGHT);
//
// Get screen depth from ini file (may be 16 or 32)
m_formatScreenDepth
= (D3DFORMAT)cmnReadSetupInt(_S_SECTION_DISPLAY, _S_KEY_FORMAT);
//
// If system fullsreen or windowed
if(cmnReadSetupInt(_S_SECTION_DISPLAY,
_S_KEY_WINDOWED)) {
m_wFlags |= FLAG_WINDOWED;
}
//
// Is FPS enabled? Default is disabled.
if(cmnReadSetupInt(_S_SECTION_DISPLAY,
_S_KEY_FPS)) {
m_wFlags |= FLAG_FPS;
}
//
// Get desired FPS from ini. Default is off.
// If m_iDesiredFPS == 0, then FPS limiter is off.
m_iDesiredFPS =
cmnReadSetupInt(_S_SECTION_DISPLAY, _S_KEY_DESIREDFPS);
//
// Read type of vertex processing
BehaviorFlags =
cmnReadSetupInt(_S_SECTION_DISPLAY, _S_KEY_BEHAVIOR);
//
// Read texture format
m_formatTextureDepth
= (D3DFORMAT)cmnReadSetupInt(_S_SECTION_DISPLAY, _S_KEY_TEXDEPTH);
//
// Set size of back buffer from window position
if(m_wFlags &
FLAG_WINDOWED) {
D3DDISPLAYMODE displaymode;
RECT rcWindowClient;
//
// Get window position
GetClientRect(hWnd, &rcWindowClient);
// Get current display mode
dwRet = m_lpD3DObject->GetAdapterDisplayMode(D3DADAPTER_DEFAULT, &displaymode);
if(dwRet != D3D_OK) {
THROWERR("Cannot
get display format in window mode due ", dwRet);
}
//
// Set size of back buffer
m_sRes.cx = rcWindowClient.right - rcWindowClient.left;
m_sRes.cy = rcWindowClient.bottom -
rcWindowClient.top;
// Set display mode to current
m_formatScreenDepth = displaymode.Format;
}
//
// Read ordinal number of used adapter
m_iAdapterOrdinal
= cmnReadSetupInt(_S_SECTION_DISPLAY, _S_KEY_ADAPTER);
// Read type of
device on this adapter
m_typeDeviceType
= (D3DDEVTYPE) cmnReadSetupInt(_S_SECTION_DISPLAY, _S_KEY_DEVICE);
m_tftMagTextureFilter = (D3DTEXTUREFILTERTYPE)
cmnReadSetupInt(_S_SECTION_DISPLAY, _S_KEY_MAGTEXTUREFILTER);
m_tftMinTextureFilter = (D3DTEXTUREFILTERTYPE)
cmnReadSetupInt(_S_SECTION_DISPLAY, _S_KEY_MINTEXTUREFILTER);
m_tftMipTextureFilter = (D3DTEXTUREFILTERTYPE)
cmnReadSetupInt(_S_SECTION_DISPLAY, _S_KEY_MIPTEXTUREFILTER);
//
// Set properties of device
ZeroMemory(&m_d3dDeviceParam,
sizeof(m_d3dDeviceParam));
m_d3dDeviceParam.Windowed = (m_wFlags & FLAG_WINDOWED) ? TRUE
: FALSE;
m_d3dDeviceParam.SwapEffect = D3DSWAPEFFECT_DISCARD;
m_d3dDeviceParam.BackBufferWidth = m_sRes.cx;
m_d3dDeviceParam.BackBufferHeight = m_sRes.cy;
m_d3dDeviceParam.BackBufferFormat = m_formatScreenDepth;
m_d3dDeviceParam.BackBufferCount = 1;
m_d3dDeviceParam.hDeviceWindow = hWnd;
m_d3dDeviceParam.Flags = 0;
//
// -> Fullscreen AA
m_d3dDeviceParam.MultiSampleType
= (D3DMULTISAMPLE_TYPE)cmnReadSetupInt(_S_SECTION_DISPLAY, _S_KEY_FSAA);
m_d3dDeviceParam.EnableAutoDepthStencil = TRUE;
m_d3dDeviceParam.AutoDepthStencilFormat = D3DFMT_D16;
//
// Set some settings for fullscreen mode
// -> PresentationInterval
// -> RefreshRate
if(!(m_wFlags &
FLAG_WINDOWED)) {
//
// -> PresentationInterval
if(cmnReadSetupInt(_S_SECTION_DISPLAY, _S_KEY_VSYNC) == 0) {
m_d3dDeviceParam.FullScreen_PresentationInterval = D3DPRESENT_INTERVAL_IMMEDIATE;
}
else {
m_d3dDeviceParam.FullScreen_PresentationInterval = D3DPRESENT_INTERVAL_ONE;
}
//
// -> RefreshRate
m_d3dDeviceParam.FullScreen_RefreshRateInHz = cmnReadSetupInt(_S_SECTION_DISPLAY,
_S_KEY_REFRESHRATE);
}
// Now we can
create D3D device
dwRet =
m_lpD3DObject->CreateDevice(m_iAdapterOrdinal, m_typeDeviceType, hWnd,
BehaviorFlags, &m_d3dDeviceParam, &m_lpD3DDevice);
if(dwRet != D3D_OK) {
cmnMessageBoxA("Please, run Setup to
set display settings.", MSG_ERROR);
Clean();
THROWERR("Cannot create D3D device
due ", dwRet);
}
//
// Set texture filter for first stage
m_lpD3DDevice->SetTextureStageState(0,
D3DTSS_MIPFILTER, m_tftMipTextureFilter);
m_lpD3DDevice->SetTextureStageState(0, D3DTSS_MAGFILTER, m_tftMagTextureFilter);
m_lpD3DDevice->SetTextureStageState(0, D3DTSS_MINFILTER, m_tftMinTextureFilter);
m_dwBackgroundColor = 56564;
BuildUpMatrices(&D3DXVECTOR3(5.0f, 5.0f, 5.0f),
&D3DXVECTOR3(0.0f, 0.0f, 0.0f));
return 0;
}
Tato funkce načte všechny možné informace ze souboru setup.ini. Většina těchto dat je pro nás zatím nedůležitých, ale postupně je začneme využívat. Nejprve vytvoříme objekt Direct3D a poté postupně načítáme tyto data:
rozlišení obrazovky
formát (barevná hloubka)
nastavení zda-li aplikace běží v okně či nikoliv
nastavení viditelnosti ukazatele FPS
požadované FPS - omezovač. Pokud je 0, FPS je neomezené.
načtení "chování" zařízení - softwarové či hardwarové zpracovaní vertexů
formát textur
ordinální číslo zvoleného adaptéru
typ zařízení - HAL nebo REF
filtrování textur
antialiasing
vertikální synchronizace
obnovovaní frekvence
Dále musíme nastavit nějaké speciální vlastnosti, pokud aplikace běží v okně. Hlavní je velikost zadního bufferu, kterou spočítáme podle velikosti okna. Dále je třeba získat aktuální formát použitý Windows.
V dalším kroku již nastavíme vlastnosti zařízení velice podobně jako v minulé lekci a vytvoříme zařízení. V posledním kroku nastavíme matice pohledu a projekce pomocí metody BuildUpMatrices(). Některé atributy nastavujeme jen při fullscreen režimu (např. vertikální synchronizaci nebo obnovovací frekvenci).
3) Nastavení matic a vytvoření světla
void XDisplay::BuildUpMatrices(D3DXVECTOR3
*pvEyePt, D3DXVECTOR3 *pvLookAtPt)
{
D3DXMATRIX matWorld, matView, matProj;
D3DXVECTOR3 vUpDir;
// Check
parameters
if(!pvEyePt || !pvLookAtPt)
{
THROW("BuildUpMatrices: Invalid input
vectors.");
}
//
// Vertical axis is Z
vUpDir =
D3DXVECTOR3(0.0f, 0.0f, 1.0f);
//
// Set transformations
D3DXMatrixIdentity(m_matWorld);
//
// Create view and projection matrices
D3DXMatrixLookAtLH(&matView, pvEyePt, pvLookAtPt, &vUpDir);
D3DXMatrixPerspectiveFovLH(&matProj, D3DX_PI / 4, 4.0f/3.0f,
1.0f, 50.0f);
//
// Set matrices
m_lpD3DDevice->SetTransform(D3DTS_WORLD,
m_matWorld);
m_lpD3DDevice->SetTransform(D3DTS_VIEW, &matView);
m_lpD3DDevice->SetTransform(D3DTS_PROJECTION, &matProj);
//
// Enable Z-buffer
m_lpD3DDevice->SetRenderState(D3DRS_ZENABLE,
TRUE);
//
// Set wireframe, if user wants to render wireframe model
if(cmnReadSetupInt(_S_SECTION_DISPLAY,
_S_KEY_WIREFRAME)) {
m_lpD3DDevice->SetRenderState(D3DRS_FILLMODE,
D3DFILL_WIREFRAME);
m_lpD3DDevice->SetRenderState(D3DRS_CULLMODE,
D3DCULL_NONE);
}
D3DLIGHT8 Light;
ZeroMemory( &Light, sizeof(D3DLIGHT8) );
Light.Type = D3DLIGHT_DIRECTIONAL;
Light.Direction.x = -1.0f;
Light.Direction.y = 0.0f;
Light.Direction.z = -1.0f;
Light.Diffuse.r = 0.7f;
Light.Diffuse.g = 0.7f;
Light.Diffuse.b = 0.0f;
Light.Ambient.r = 0.5f;
Light.Ambient.g = 0.5f;
Light.Ambient.b = 0.5f;
Light.Range = 1000.0f;
D3DMATERIAL8 mtrl;
ZeroMemory( &mtrl, sizeof(D3DMATERIAL8) );
mtrl.Diffuse.r = mtrl.Ambient.r = 1.0f;
mtrl.Diffuse.g = mtrl.Ambient.g = 1.0f;
mtrl.Diffuse.b = mtrl.Ambient.b = 1.0f;
mtrl.Diffuse.a = mtrl.Ambient.a = 1.0f;
m_lpD3DDevice->SetMaterial(&mtrl);
m_lpD3DDevice->SetLight(0, &Light);
m_lpD3DDevice->LightEnable(0, TRUE);
m_lpD3DDevice->SetRenderState(D3DRS_LIGHTING, TRUE);
}
První část funkce je celkem srozumitelná, protože podobné nastavení jsme dělali i v minulé lekci. Dále ale musíme zapnout Z-buffer a v případě, že uživatel chce drátový model i Wireframe. O Z-bufferu si povíme v některé příští lekci. V poslední části vytvoříme a nastavíme světlo a materiál. O tomto jsme si zatím také nic nepověděli, ale doženeme to příště. Tuto funkci je třeba volat i po resetu zařízení, protože při zařízení ztratí všechny stavy, matice atd.
4) Vyčištění zadního bufferu a Z-buffer
int XDisplay::UpdateBackground()
{
if(m_lpD3DDevice) {
//
// Clear the back buffer to a blue
color
m_lpD3DDevice->Clear(0, NULL,
D3DCLEAR_TARGET| D3DCLEAR_ZBUFFER, m_dwBackgroundColor, 1.0f, 0);
D3DXMATRIX matWorld, matRot;
float fFactor = m_fTime;
if(fFactor != 0) {
D3DXMatrixRotationX(&matRot, 0.9f * fFactor);
D3DXMatrixMultiply(m_matWorld, m_matWorld, &matRot);
m_lpD3DDevice->SetTransform(D3DTS_WORLD, m_matWorld);
}
return 0;
}
return 1;
}
Zde pouze nastavíme požadovanou barvu pozadí a vyčistíme Z-Buffer (viz. minulá lekce). V druhé části transformujeme světovou matici a tím způsobíme rotaci scény.
5) Prohození bufferů
int XDisplay::Present()
{
DWORD dwRet;
if(m_lpD3DDevice) {
// Get time
m_fTime = cmnGetTime(TIMER_GETELAPSEDTIME);
m_fTotalTime += m_fTime;
// Test the cooperative level to see if it's okay to render
dwRet = m_lpD3DDevice->TestCooperativeLevel();
if(dwRet != D3D_OK)
{
// If the device was lost, do not render until we get it back
if( D3DERR_DEVICELOST == dwRet ) {
}
// Check if the device needs to be resized.
if( D3DERR_DEVICENOTRESET == dwRet ) {
RestoreDisplay();
}
return (int)dwRet;
}
if(m_wFlags & FLAG_FPS) {
UpdateFPS();
}
// Set limitations
if(m_iDesiredFPS != 0) {
LimitFPS();
}
m_lpD3DDevice->Present(NULL, NULL,
NULL, NULL);
}
return 0;
}
Zde postupně určíme čas od posledního volání metody, tento čas připočteme k celkovému času. Dále testujeme zda-li není zařízení ztraceno. Pokud k tomu dojde, přestaneme prohazovat povrchy a čekáme až bude potřeba zařízení obnovit. Poté voláme metodu RestoreDisplay(). V normálním chodu zobrazujeme a omezujeme FPS. Nakonec provedeme present.
6) Zobrazení a omezeni FPS
int XDisplay::UpdateFPS()
{
//
// Get time of system start
m_fStartTime =
cmnGetTime(TIMER_GETAPPTIME);
//
// Increment frames counter
m_Frames++;
//
// Compute number of frames per one second
if((m_fStartTime
- m_fStopTime) >= FPS_REFRESH_RATE) {
m_fCurrentFPS = m_Frames / (m_fStartTime
- m_fStopTime);
m_Frames = 0;
//
// Save old-new time
m_fStopTime = m_fStartTime;
TRACE("%4.1f FPS - ElapsedT: %4.8f:
%d", m_fCurrentFPS, m_fTime);
}
//
return 1;
}
void XDisplay::LimitFPS()
{
static float last = 0;
do
{
float now = cmnGetTime(TIMER_GETAPPTIME);
float passed = now - last;
m_fFrameRate = float(1.0 / float(passed));
}
while(m_fFrameRate > m_iDesiredFPS);
last = cmnGetTime(TIMER_GETAPPTIME);
}
V metodě UpdateFPS() musíme spočítat snímky za vteřinu. Už jsme podobnou funkci psali pro DirectDraw. Po jisty časový okamžik počítáme počet cyklů, pak toto číslo převedeme tak, aby odpovídalo 1 vteřině. V metodě LimitFPS() je cyklus, ve kterém spočítáme aktuální framerate. Cyklus ukončíme, pokud je tento framerate alespoň tak velký jako požadované FPS.
7) Obnovení zařízení
int XDisplay::RestoreDisplay()
{
TRACE("Restoring device...");
//
// Release all resources
if(!m_resManager.ReleaseAll())
{
TRACE("Resources released.");
}
//
// Reset the device
if(D3D_OK !=
m_lpD3DDevice->Reset(&m_d3dDeviceParam))
{
TRACE("Cannot RESET device object.");
}
else {
TRACE("Device is reset.");
}
// Wait until
device is reset
Sleep(150);
// restore world/view/proj
matrices and set render stages
BuildUpMatrices(&D3DXVECTOR3(5.0f,
5.0f, 5.0f), &D3DXVECTOR3(0.0f, 0.0f, 0.0f));
// reinit
resources
m_resManager.RestoreAll();
return 0;
}
Tato metoda provede ty tři kroky, které jsem uváděl v části 22.2.:
Uvolní všechny zdroje - textury, meshe, shadery, index a vertex buffery.
Resetuje zařízení a chvilku počká.
Obnoví všechny zdroje tj. znovu načte všechny textury, meshe atd.
Zde právě použijeme náš resource manager, který ví o všech alokovaných zdrojích.
Na závěr lekce ještě vyzkoušíme všechny nové funkce knihovny Display. K tomu účelu vložíme do projektu Win32 aplikaci Tester. Zde zinicializujeme systém Direct3D a vytvoříme pár meshů: koule, kvádr, válec, konvička, prstenec a mesh ze souboru tiger.x. Na model tygra je dále třeba načíst texturu tiger.bmp.
Zde jsou použité objekty:
XDisplay g_theDisplay;
XTexture g_Tex;
XMesh g_Sphere;
XMesh g_Box;
XMesh g_Cylinder;
XMesh g_Torus;
XMesh g_Teapot;
XMesh g_Tiger;
K inicializace XDisplay je třeba znát handle okno:
HWND g_hWnd;
Jednotlivé meshe půjdou vypnout klávesami F1-F6. Informaci o viditelnosti meshe je uložena v poli:
BOOL g_VisObj[6];
Přidáme jedinou funkci na obnovení snímku:
void UpdateFrame();
Do funkce WinMain() přidáme následující kód:
// inicializace logovaciho souboru,
pokud je zapnuta funkce Tracetofile
if(cmnReadSetupInt(_S_DEBUG, _S_TRACETOFILE)
== 1) {
cmnInitLog("log.txt", FALSE);
}
// inicializace Direct3D
g_theDisplay.Init(g_hWnd);
// textura tygra
g_Tex.LoadTextureFromFile("tiger.bmp",
g_theDisplay.GetDevice());
// preddefinovane objekty
g_Sphere.CreateSphere(2.0f, 48, 48,
g_theDisplay.GetDevice());
g_Box.CreateBox(1.0f, 1.0f, 6.0f, g_theDisplay.GetDevice());
g_Cylinder.CreateCylinder(1.5f, 2.5f, 4.0f, 24, 8, g_theDisplay.GetDevice());
g_Torus.CreateTorus(1.0f, 2.0f, 24, 24, g_theDisplay.GetDevice());
g_Teapot.CreateTeapot(g_theDisplay.GetDevice());
// model tygra
g_Tiger.LoadMeshFromFile("tiger.x",
g_theDisplay.GetDevice());
// zaregistrovani vsech zdroju
g_theDisplay.GetResourceManager()->AddMesh(&g_Tiger);
g_theDisplay.GetResourceManager()->AddMesh(&g_Sphere);
g_theDisplay.GetResourceManager()->AddMesh(&g_Box);
g_theDisplay.GetResourceManager()->AddMesh(&g_Cylinder);
g_theDisplay.GetResourceManager()->AddMesh(&g_Teapot);
g_theDisplay.GetResourceManager()->AddMesh(&g_Torus);
g_theDisplay.GetResourceManager()->AddTexture(&g_Tex);
// po spusteni je pouze druhy objekt
viditelny
g_VisObj[0] = 0;
g_VisObj[1] = 1;
g_VisObj[2] = 0;
g_VisObj[3] = 0;
g_VisObj[4] = 0;
g_VisObj[5] = 0;
// Main message loop:
while( TRUE )
{
// Look for
messages, if none are found then
// update the state and display it
if( PeekMessage( &msg, NULL, 0, 0, PM_NOREMOVE ) )
{
if( 0 == GetMessage(&msg, NULL, 0, 0
) )
{
// WM_QUIT was posted, so exit
return (int)msg.wParam;
}
// Translate and dispatch the message
TranslateMessage( &msg );
DispatchMessage( &msg );
}
else
{
// Obnova snimku
UpdateFrame();
}
}
return (int) msg.wParam;
K tomuto snad není co dodat. Všechny potřebné informace najdete v
komentářích. Nyní trochu upravíme funkci WndProc(),
protože chceme využit klávesy F1-F6 na ovládání modelů a klávesu F7 na "umělý"
reset zařízení, aby bylo zřejmé, že všechno funguje tak jak má. Do příkazu
switch přidejte tuto větev:
case WM_KEYUP:
switch( wParam )
{
case VK_F7:
g_theDisplay.RestoreDisplay();
break;
case VK_F1:
g_VisObj[0] =
!g_VisObj[0];
break;
case VK_F2:
g_VisObj[1] =
!g_VisObj[1];
break;
case VK_F3:
g_VisObj[2] =
!g_VisObj[2];
break;
case VK_F4:
g_VisObj[3] =
!g_VisObj[3];
break;
case VK_F5:
g_VisObj[4] =
!g_VisObj[4];
break;
case VK_F6:
g_VisObj[5] =
!g_VisObj[5];
break;
case VK_ESCAPE:
PostQuitMessage(0);
break;
}
break;
Jako poslední uvedu funkci UpdateFrame(), která provede vyčištění pozadí, vykreslení příslušných modelů a present.
void UpdateFrame()
{
g_theDisplay.UpdateBackground();
g_theDisplay.GetDevice()->BeginScene();
if(g_VisObj[0]) {
if(g_Sphere.GetMesh())
g_Sphere.GetMesh()->DrawSubset(0);
}
if(g_VisObj[1]) {
if(g_Torus.GetMesh())
g_Torus.GetMesh()->DrawSubset(0);
}
if(g_VisObj[2]) {
if(g_Box.GetMesh())
g_Box.GetMesh()->DrawSubset(0);
}
if(g_VisObj[3]) {
if(g_Cylinder.GetMesh())
g_Cylinder.GetMesh()->DrawSubset(0);
}
if(g_VisObj[4]) {
if(g_Teapot.GetMesh())
g_Teapot.GetMesh()->DrawSubset(0);
}
if(g_VisObj[5]) {
g_theDisplay.GetDevice()->SetTexture(0,
g_Tex.GetTexture());
if(g_Tiger.GetMesh())
g_Tiger.GetMesh()->DrawSubset(0);
g_theDisplay.GetDevice()->SetTexture(0,
NULL);
}
g_theDisplay.GetDevice()->EndScene();
g_theDisplay.Present();
}
Zde je třeba volat metody BeginScene() a EndScene(). Dále si něco povíme o vykreslovaní meshe, které je zde značně zjednodušeno a postupně ho vylepšíme. Obecně se totiž mesh skládá z několika tzv. subsetů, což je vlastně pod-mesh daného meshe, který má svůj materiál a texturu. V našem jednoduchém případě má každý objekt pouze jeden subset, tudíž ho můžeme vykreslit pomocí metody DrawSubset() s parametrem 0. Pro objekt tygra je třeba nastavit texturu tiger.bmp. Tento způsob vykreslovaní meshů není vhodný a příště vytvoříme metodu XMesh::Draw(), která se bude starat o vše, včetně nastavení textury a materiálu.
Pomocí kláves F1-F6 můžete libovolně zobrazovat nahrané modely. Pomocí klávesy F7 vynutíte umělý reset zařízení. Klávesou escape ukončíte aplikaci.
V dnešní lekci jsem splnil, co jsem slíbil, ale nedověděli jste se prakticky nic nového! Příště budeme dále budovat náš engine a podíváme se podrobněji na některé další vlastnosti. Také bych chtěl probrat světla, takže přidáme podporu světel.
Těším se příště nashledanou.