C/C++ & Visual C++

Kurz DirectX (33.)

Úvodem  |  Datové struktury |  Kurz DirectX  |  Downloads  |  Otázky a odpovědi

V dnešní lekci vylepšíme kameru, přidáme režim, kdy se kamera bude pohybovat po předem definované cestě. Dále přidáme několik metod do třídy XDisplay starající se o mlhu.

33.1. Pohyb kamery

Do teď byl pohyb kamery zajištěn pouze pomocí interaktivního ovládání, které vyžadovalo vstup z klávesnice. V dnešní lekci přidáme do kamery neinteraktivní režim, kdy pohyb kamery bude řízen automaticky. Abychom pohyb jednoznačně určili, jsou třeba tři základní informace: poloha kamery, směr a čas, kdy se v této konfiguraci kamera nachází.

Soubor cesty

Výše uvedeny informace budou uloženy v souboru path.txt, který bude mít následující formát:

Na každém řádku tedy bude jedna klíčová hodnoty polohy kamery. Mezi dvěma polohami se bude plynule interpolovat. Soubor budeme číst po řádcích, jednotlivé řádky rozkládat do jednotlivých položek pomocí oddělovače (zde středník).

Struktura PathSegment

Z této struktury postavíme spojový seznam všech segmentů načtených ze souboru.

struct PathSegment
{
    float fTime;
    D3DXVECTOR3 vPos;
    D3DXVECTOR3 vDir;

    PathSegment *pNext;
    PathSegment *pPrevious;
};

Struktura uchovává všechny parametry kamery a navíc ještě ukazatel na následníka a předchůdce ve spojovém seznamu. Řetěz bude vypadat následovně:

Nyní upravíme třídu XCamera. Za prvé přidáme několik členských proměnných:

// Cela path (1)
PathSegment *m_arPath;
// Current position of the camera (2)
PathSegment *m_curSeg;
// Aktualni kamera (3)
PathSegment m_curInterpolation;
// Stav prehravani kamery (4)
PATHSTATUS m_ePathStatus;
// Motion modifier (5)
float m_fSpeed;

// Rezim kamery (6)
CAMERAMODE m_cm;
// Informace o nahrani cesty (7)
BOOL m_bPathLoaded;

1) Ukazatel na první segment spojového seznamu cesty
2) Aktuální segment, mezi tímto a následujícím probíhá interpolace
3) Aktuální konfigurace kamery (tedy interpolovaná hodnota)
4) Stav přehrávání - kamera lze v libovolném místě pozastavit. Zde je použitý typ PATHSTATUS:

enum PATHSTATUS
{
    PLAY, PAUSE, STOP
};

5) Modifikátor rychlosti pohybu. Implicitně je roven 1.0 (čas v souboru path odpovídá skutečnosti).
6) Režim kamery. Kamera může být buď v režimu PATH nebo INTERACTIVE. Typ CAMERAMODE je definován následovně:

enum CAMERAMODE
{
    PATH, INTERACTIVE
};

7) Informace o tom, zda je cesta načtena ze souboru. Nejdříve je pochopitelně nutno soubor path načíst a teprve pak je možno spustit vlastní pohyb.

Dále přidáme šest veřejných a jednu interní metodu:

virtual HRESULT LoadPath(LPCSTR szPath); (1)
virtual HRESULT StartPath(); (2)
virtual HRESULT PausePath(); (3)
virtual HRESULT StopPath();// (4) reset
virtual HRESULT SetSpeed(float fRelativeSpeed) {m_fSpeed += fRelativeSpeed;return S_OK;} (5)
virtual HRESULT SetCameraMode(CAMERAMODE cm){m_cm = cm;return S_OK; } (6)

float ExtractNumber(int i, char *line); (7)

1) Metoda načte soubor path a vytvoří spojový seznam popsaný výše. Nakonec připraví kameru pro spuštění interpolace.
2) Spustí pohyb kamery podle předem nahrané cesty.
3) Pozastaví pohyb kamery. Kamera může být znovu spuštěna opětovným zavolám metody StartPath().
4) Zastaví pohyb kamery a nastaví výchozí pozici.
5) Nastaví rychlost pohybu. Nastavení je prováděno relativně vůči hodnotě 1.0.
6) Nastaví režim kamery - buď PATH nebo INTERACTIVE.
7) Poslední neveřejná metoda slouží k rozkládání řádky ze souboru path.

Nyní všechny tyto metody naimplementujeme. Nejsložitější metoda je LoadPath(), která využívá metodu ExtractNumber(). Začneme tedy od této metody:

float XCamera::ExtractNumber(int i, char *line)
{   
    char number[20];
    char *pline = line;
    int c;

    for(int u = 0; u < i; u++)
    {
        // Find first ;
        c = strcspn(pline, ";");
        // Next number + ;
        if(c > 0)
        {
            pline += (c+1);
        }
    }

    // Find first ;
    c = strcspn(pline, ";");
    // Copy number
    strncpy(number, pline, c);
    // Ends number
    number[c] = '\0';
    // Convert number
    return (float)atof(number);
}

Možná není na první pohled vidět, co tato metoda vlastně dělá. Přijímá dva parametry: celé číslo jako pořadí čísla, které chceme extrahovat a řetězec řádky ze souboru path. Z tohoto řetězce je vybráno číslo podle parametru i, poté se převede na float, který je vrácen. V první smyčce tedy přeskáčeme všechny čísla před požadovaným. Funkce strspn() vrací index výskytu oddělovače. Tuto hodnotu ve vstupním řetězci přeskočím a to opakujeme tak dlouho až se dostaneme před požadované číslo. V další části se použije stejný postup na zkopírování části vstupu do pomocné proměnné number, který ještě zakončíme znakem '\0' a nakonec převedeme funkci atof() na float.

Metoda LoadPath() načte ze souboru cestu a vytvoří spojový seznam.

HRESULT XCamera::LoadPath(LPCSTR szPath)
{
    DWORD size;
    char line[256];
    char *pline = line;
    char c;    
    PathSegment *pLast = NULL;
    PathSegment ps;

    char szFullPath[MAX_PATH];
    // Build full path to path file
    cmnGetDataFilePath(szFullPath, szPath);
    // Load file contains
    cmnLoadFileFromPath(szFullPath, NULL, &size);
    char * file = new char[size+1];
    ZeroMemory(file, (size+1) * sizeof(char));
    cmnLoadFileFromPath(szFullPath, (LPBYTE)file, &size);

    // Release previous path if any
    if(m_arPath)
    {
        PathSegment *pNext = NULL;
        PathSegment *pToDelete = m_arPath;
        // Release path
        while(pToDelete)
        {
            pNext = pToDelete->pNext;
            SAFE_DELETE(pToDelete);
            pToDelete = pNext;
        }
    }
    char *pfile = file;
    m_arPath = NULL;

V první části načteme soubor pomocí funkce cmnLoadFileFromPath(). Tato funkce je v knihovně common.dll. Protože nevíme velikost souboru, nejprve zavoláme funkci s hodnotou NULL. Takto nám vrátí velikost bufferu, který musíme alokovat pro soubor. V druhém volání již použijeme buffer file. Funkce cmnGetDataFilePath() je opět z knihovny common.dll a pouze spojí cestu spuštěného programu a zadaného souboru, tím vytvoří úplnou cestu k souboru cesty. Na závěr této části vymažeme předchozí cestu, pokud taková existuje. pfile je ukazatel stejně jako file, ale s pfile budeme později hýbat. file nesmíme změnit, protože ho nakonec budeme dealokovat.

while(pfile[0] != '\0')
{
    while(1)
    {
        c = pfile++[0];
        if(c == 0x0D)
        {
            pfile++; // jump over 0x0A
            break;
        }
        if(c == '\0')
        {
            pfile--; // return before '\0'
            break;
        }
        // Copy line char
        pline[0] = c;
        // Next char
        pline++;
    }
    // End of string
    pline[0] = '\0';
    // Reset pline
    pline = line;
    // Parse line
    ps.fTime = ExtractNumber(0, line);
    ps.vPos.x = ExtractNumber(1, line);
    ps.vPos.y = ExtractNumber(2, line);
    ps.vPos.z = ExtractNumber(3, line);
    ps.vDir.x = ExtractNumber(4, line);
    ps.vDir.y = ExtractNumber(5, line);
    ps.vDir.z = ExtractNumber(6, line);

    D3DXVec3Normalize(&ps.vDir, &ps.vDir);
    
    if(!m_arPath)
    {
        // Create root segment
        pLast = m_arPath = new PathSegment;
        *m_arPath = ps;
        m_arPath->pPrevious = NULL;
        m_arPath->pNext = NULL;
    }
    else
    {
        // Create new segment
        pLast->pNext = new PathSegment;
        *pLast->pNext = ps;
        pLast->pNext->pPrevious = pLast;
        pLast->pNext->pNext = NULL;
        pLast = pLast->pNext;
    }
     // Reset pline
    pline = line;
}

SAFE_DELETE_ARRAY(file);


V této části čteme řetězec souboru (od začátku do konce) a vybíráme z něj jednotlivé řádky. Mezi řádky jsou znaky 0x0D a 0x0A, podle kterých poznáme kde řádek končí. Vnitřní while() tedy čte znaky tak dlouho, než narazí na znak 0x0D (konec řádky) nebo na '\0' (konec "souboru"). V prvním případě ještě ukazatel zvýšíme, abychom přeskočili znak 0x0A. Ve druhém naopak snížíme, abychom nastavili znak '\0' jako první takže se ukončí vnější cyklus a další řádka se nečte. Zároveň se každý znak řádky kopíruje do pomocného pole pline. Opět je třeba po přečtení řádky řetězec řádně ukončit. V dalším kroku již můžeme rozložit řádku na jednotlivé položky pomocí metody ExtractNumber(). Dále normalizujeme vektor směru. Teď již máme všechny potřebné informace, abychom mohli vytvořit nový objekt PathSegment. Zde se problém rozštěpí na dva případy. Pokud se jedná o první segment, je třeba uložit ukazatel do proměnné m_arPath. Předchozí i následující segment je NULL a do proměnné pLast uložíme poslední přidaný segment - tedy m_arPath. Pokud se jedná o další segment (ne první), připojíme tento segment za poslední přidaný v předcházejícím kroku. Předchozí toho aktuálního segmentu bude minulý a následující NULL. Nesmíme zapomenout opět uložit poslední přidaný, kterým se stane aktuální segment. V obou případech kopírujeme data z pomocného objektu ps. Nakonec resetujeme proměnnou pline pro další řádek.


// Path is ready
m_bPathLoaded = TRUE;
m_curInterpolation = *m_arPath;
m_curSeg = m_arPath;
m_vEyePt = m_curInterpolation.vPos;
m_vLookAtPoint = m_vEyePt + m_curInterpolation.vDir;

V poslední části připravíme ostatní proměnné pro spuštění pohybu. Za prvé říkáme, že cesta je načtena. Do proměnné m_curInterpolation uložíme počáteční segment stejně jako do proměnné m_curSeg. Nakonec nastavíme pozorovací a pozorovaný bod. K tomu použijeme objekt aktuální polohy m_curInterpolation. m_curInterpolation.vPos je přímo pozorovací bod a pozorovaný dostaneme jednoduše tak, že k této hodnotě přičteme směr m_curInterpolation.vDir.

Následující trojicí metod se dá ovládat pohyb kamery. Každá z těchto metod se nejprve přesvědčí, zda-li je načtena nějaká cesta. Metoda StartPath() provede dvě věci. Nastavením příznaku m_ePathStatus na hodnotu PLAY způsobí, že se začne počítat čas interpolace, který je uložen v proměnné m_curInterpolation.fTime. Pomocí této hodnoty se interpoluje (k tomu se dostaneme za chvilku) a pokud se mění, kamera se pohybuje. Za druhé je třeba kameře říci, aby přešla z interaktivního režimu do režimu PATH. Toto se dá rovněž nastavit metodou SetCameraMode().
Metoda PausePath() pouze nastaví příznak m_ePathStatus na hodnotu PAUSE. To zajistí, že jakoby zastaví čas.
Poslední metoda StopPath() funguje podobně jako PausePath() s tím rozdílem, že navíc nastaví aktuální segment na první segment. Čili po opětovném spuštění pohybu, začne sekvence od začátku.

Aby se pohyb kamery projevil po vizuální stránce, je třeba ještě trochu upravit metodu ProcessCamera(). Ta se rozdělí na dva případe: interaktivní a automatický s cestou. Interaktivní režim zůstane stejný jako doposud. Zajímavější bude druhý zmíněný způsob:

// Path mode
if(m_cm == PATH)
{
    if(m_ePathStatus == PLAY)
    {
        fElapsedTime *= m_fSpeed;
        fDif = m_curSeg->pNext->fTime - m_curSeg->fTime;

        s = (m_curInterpolation.fTime - m_curSeg->fTime) / fDif;
        if(s > 1.0)
        {
            // Next segment
            if(m_curSeg->pNext->pNext)
            {
                m_curSeg = m_curSeg->pNext;
            }
            else
            {
                // First segment
                m_curSeg = m_arPath;
                m_curInterpolation.fTime = 0.0f;
            }
            // Recompute factor
            fDif = m_curSeg->pNext->fTime - m_curSeg->fTime;
            s = (m_curInterpolation.fTime - m_curSeg->fTime) / fDif;
        }
        if(s < 0.0f)
        {
            // Previous segment
            if(m_curSeg->pPrevious)
            {
                m_curSeg = m_curSeg->pPrevious;
            }
            else
            {
                // First segment
                m_curSeg = m_arPath;
                m_curInterpolation.fTime = 0.0f;
                m_fSpeed = 1.0;
            }
            // Recompute factor
            fDif = m_curSeg->pNext->fTime - m_curSeg->fTime;
            s = (m_curInterpolation.fTime - m_curSeg->fTime) / fDif;
        }

        D3DXVec3Lerp(&m_curInterpolation.vPos, &m_curSeg->vPos, &m_curSeg->pNext->vPos, s);
        D3DXVec3Lerp(&m_curInterpolation.vDir, &m_curSeg->vDir, &m_curSeg->pNext->vDir, s);
        m_curInterpolation.fTime += fElapsedTime;
    }
    m_vEyePt = m_curInterpolation.vPos;
    m_vLookAtPoint = m_vEyePt + m_curInterpolation.vDir;
}

Pokud je nastaven příslušný režim pomocí metody SetCameraMode() (či metodou StartPath()), je pozorovaný a pozorovací bod počítán zcela jinak. Interpolace probíhá pouze pokud je sekvence spuštěna (první podmínka). V tomto případě se vynásobí čas modifikátorem zrychlení, spočítá se časový rozdíl mezi aktuálním a následujícím segmentem a určí se faktor interpolace. Ten je 0.0 pokud je kamera na začátku aktuálního segmentu nebo 1.0 pokud se kamera nachází na začátku následujícího segmentu. V případě, že faktor je větší než 1.0, znamená to, že jsme již v dalším segmentu a je třeba přepnout aktuální segment, tedy z následujícího se stane aktuální. V opačném případě, tedy když je faktor záporný, znamená to, že jsme se dostali do předchozího segmentu, zde je třeba použít ukazatel na předcházející segment (kamera se pohybuje v opačném směru). Pokud ukazatele pNext nebo pPrevious ukazují na NULL, znamená to, že jsme na konci sekvence a je třeba se vrátit na začátek. Po přepnutí aktuálního segmentu je třeba znovu spočítat faktor.
V dalším kroku provedeme samotnou interpolací pomocí metody D3DXVec3Lerp(), která přijímá čtyři parametry: první je výstupní vektor (interpolovaný), následuje dvojice vektorů, mezi kterými se interpoluje a nakonec faktor.

Na závěr nastavíme pozorovací a pozorovaný bod pro matici pohledu. Všimněte si, že tyto řádky jsou stejné jako v metodě LoadPath().

33.2. Podpora mlhy ve třídě XDisplay

Minule jsme si říkali něco málo o mlze, ale nastavovali jsme ji přímo ve třídě XTerrain dost neohrabaným způsobem. Proto jsem přidal tři nové funkce do třídy XDisplay.

EnableFog(BOOL bEnable) - vypne nebo zapne mlhu
SetPixelFog(UINT uFogType, float fDensity, float fStart, float fEnd, D3DCOLOR Color) - nastaví parametry pixelové mlhy
SetVertexFog(UINT uFogType, float fDensity, float fStart, float fEnd, D3DCOLOR Color) - nastaví parametry vertexové mlhy

Parametr uFogType může nabývat hodnot: D3DFOG_LINEAR, D3DFOG_EXP nebo D3DFOG_EXP2. U lineární mlhy je třeba nastavit parametry fStart a fEnd. U exponenciální musí být nastaven parametr fDensity. Parametry Color určuje barvu mlhy.

33.3. Závěr

A je tu opět konec! Příklad si samozřejmě můžete stáhnout v sekci Download včetně zdrojových kódů. Abyste viděli výsledek dnešní práce, stiskněte klávesu C, kterou spustíte pohyb kamery podle cesty zadané v souboru path.txt, který si samozřejmě můžete upravit dle libosti.

A jaký problém budeme řešit příště? Příště bych Vám chtěl ukázat další optimalizační techniku LOD (Level Of Detail), která pracuje na principu snižování složitosti terénu.

Těším se příště nashledanou.

 

Ondřej Burišin a Jiří Formánek