DirectX (22.)

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.

22.1. Projekt D3DEngine1

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í.

22.2. Ztráta 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:

  1. Uvolnit všechny zdroje - textury, meshe, shadery, index a vertex buffery.

  2. Resetovat zařízení a chvilku počkat.

  3. 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.

22.3. 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ů:

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:

Struktury:

22.3.1. XTexture

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:

  1. 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.

  2. Restore() - Obnova textury.

  3. Release() - Uvolnění textury.

  4. GetTexture() - Vrací ukazatel na rozhraní textury.

  5. 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;
}

22.3.2. XMesh

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:

  1. 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.

  2. 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.

  3. D3DXCreateBox - vytvoří kvádr o zadaných rozměrech. Parametry: zařízení, šířka, výška, hloubka, výsledný mesh.

  4. 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.

  5. D3DXCreateTeapot - vytvoří konvičku:) Parametry: zařízení, výsledný mesh. Konvička nemá žádné další parametry.

  6. 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.

22.3.3. XResourceManager

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.

22.3.4. 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ě:

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:

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:

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.:

  1. Uvolní všechny zdroje - textury, meshe, shadery, index a vertex buffery.

  2. Resetuje zařízení a chvilku počká.

  3. 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.

22.4. Testovací aplikace

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.

22.5. Závěr

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.

Jiří Formánek