DirectX (23.)

A je tu další lekce o Direct3D, ve které se budeme podrobněji věnovat některým detailům, které jsme minule nestihli. Například rozšíříme třídu XMesh. V druhé části lekce se podíváme na světla.

23.1 Materiály

Na začátku lekce se zmíním o materiálech. Podrobněji se jimi budeme zabývat v poslední části kapitoly, nyní si jen povíme, co to vlastně je! Materiálem určíme, jaké světelné složky se odrážejí od povrchu a jaké naopak povrch pohlcuje. Později se dovíme, že máme několik typů osvětlení scény: ambient, diffuse, specular a emissive. V materiálu určíme, jak určitý povrch reaguje na všechny tyto typy světla. Zatím se netrapte tím, že nevíte, jak si který typ osvětlení představit. To se dozvíte v části o světlech. Můžeme například nastavit, že ambient osvětlení se od daného povrchu částečně odráží a třeba jen modrá složka tohoto světla je pohlcena. Pak se tento objekt zdá být žlutý, protože do vašeho oka dopadají pouze červené a zelené složky světla. Samozřejmě také záleží, jakou barvu má dopadající světlo a jakou barvu má objekt samotný (textura, barva vertexu).

V Direct3D je materiál reprezentován datovou strukturou D3DMATERIAL8:

typedef struct _D3DMATERIAL8
{
    D3DCOLORVALUE Diffuse;
    D3DCOLORVALUE Ambient;
    D3DCOLORVALUE Specular;
    D3DCOLORVALUE Emissive;
    float Power;
} D3DMATERIAL8;

D3DCOLORVALUE je další struktura, která obsahuje složky červené, modré, zelené a alpha kanálu. Tyto hodnoty nastavujeme v rozmezí od 0.0 do 1.0, kde 0.0 je černá a 1.0 bílá. Hodnotami, které jsou vyšší než 1.0 můžete způsobit další efekty "zvýšeného" odrazu nebo naopak zápornými hodnotami vytvoříte "černou díru".

Aby se materiál použil, musíte zavolat metodu zařízení SetMaterial() s ukazatelem na materiál. Materiály se nechovají jako zdroje, tudíž je nemusíte uvolňovat při ztrátě zařízení, ale po resetu zařízení je třeba opětovně nastavit poslední materiál.

Nevědomky jsme tuto strukturu použili v minulé lekci, protože jsme potřebovali nastavit nějaký základní materiál pro všechny objekty ve scéně. V další části zjistíte, že meshe mají pro každou svou pod-část jiný materiál.

Příklad použití materiálu z minulé lekce:

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);

23.2. Třída XMesh

Nejprve trochu rozšíříme tuto třídu z minulé lekce. Řekli jsme si, že mesh se může (a taky většinou skládá) z více částí. Každá tato část má svůj materiál a texturu. Informace o materiálech a texturách jsou uloženy přímo v souboru .X, ze kterého model načítáme. Nyní trochu upravíme třídu XMesh, abychom získali informace o texturách a materiálech meshe. Za prvé musíme přidat pole textur a materiálu, dále počet materiálů:

// number of materials
DWORD m_dwNumMat;
// arrays of textures and materials
XTexture *m_pTextures;
D3DMATERIAL8 *m_pMaterials;

Upravíme metodu LoadMeshFromFile(). Přidáme třetí parametr typu ukazatel na XResourceManager:

int LoadMeshFromFile(LPCSTR szFileName, LPDIRECT3DDEVICE8 lpDevice, XResourceManager * pResMan);

A upravíme implementaci metody:

if(D3D_OK == D3DXLoadMeshFromX(m_szFilePath, 0, lpDevice, NULL, &pD3DXMtrlBuffer, &m_dwNumMat, &m_lpMesh))
{
    TRACE("Mesh '%s' was loaded.", szFileName);
    // get material buffer
    D3DXMATERIAL* d3dxMaterials = (D3DXMATERIAL*)pD3DXMtrlBuffer->GetBufferPointer();
    // create arrays of materials and textures
    m_pTextures = new XTexture[m_dwNumMat];
    m_pMaterials = new D3DMATERIAL8[m_dwNumMat];
    //
    // get materials and load textures
    for(int i = 0; i < (int)m_dwNumMat; i++)
    {
        m_pMaterials[i] = d3dxMaterials[i].MatD3D;
        m_pMaterials[i].Ambient = m_pMaterials[i].Diffuse;

        if(d3dxMaterials[i].pTextureFilename)
        {
            m_pTextures[i].LoadTextureFromFile(d3dxMaterials[i].pTextureFilename, lpDevice);
            pResMan->AddTexture(&m_pTextures[i]);
        }
    }
    pD3DXMtrlBuffer->Release();
}

Nyní si vysvětlíme, co vlastně provádíme. Voláním funkce D3DXLoadMeshFromX() nyní navíc získáme naplněný buffer ID3DXBuffer, ve kterém jsou uloženy informace o materiálech a jména textur. Ty získáme pomocí objektu D3DXMATERIAL. Ukazatel na tuto strukturu získáme přímo z ID3DXBufferu. Dále vytvoříme potřebný počet materiálů a textur. Nakonec vše v cyklu zinicializujeme. Nesmíme zapomenout přidat vytvořené textury do manažeru zdrojů (to je ten třetí parametr metody). Zde je důležité si uvědomit, že ne všechny materiály mají k sobě příslušnou texturu. V souboru .x může být uložena informace o materiálu, ale samotný objekt je bez textury. V tomto případě je řetězec obsahující jméno textury prázdný (null).

Dále upravíme destruktor třídy XMesh tak, aby se zrušily všechny materiály a textury:

XMesh::~XMesh(void)
{
    Release();
    m_lpDevice = NULL;
    SAFE_DELETE_ARRAY(m_pMaterials);
    SAFE_DELETE_ARRAY(m_pTextures);
}

Nyní můžeme mesh vykreslit. Vytvoříme metodu Draw(). Minule jsme pro každý mesh měli pomocnou proměnnou, kde jsme uchovávali informaci o viditelnosti meshe ve scéně. Nyní tuto proměnnou m_bVisible přesuneme do samotné třídy XMesh.

int XMesh::Draw()
{
    if(m_lpDevice && m_bVisible && m_lpMesh)
    {
        if(m_meshType == CUSTOM)
        {
            for(int i = 0; i < (int)m_dwNumMat; i++)
            {
                m_lpDevice->SetMaterial(&m_pMaterials[i]);
                m_lpDevice->SetTexture(0, m_pTextures[i].GetTexture());
                m_lpMesh->DrawSubset(i);
            }
        }
        else
        {
            // set default material
            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_lpDevice->SetMaterial(&mtrl);

            m_lpDevice->SetTexture(0, NULL);
            m_lpMesh->DrawSubset(0);
        }
        return 0;
    }
    return -1;
}

V této metodě nejdříve zkontrolujeme správnou inicializaci a viditelnost objektu. Dále metodu rozdělíme na dva případy. Předdefinované objekty mají pouze jednu část a není definována textura ani materiál (mesh dokonce neobsahuje texturové souřadnice). Proto je třeba nastavit standardní materiál a texturu "vynulovat". Pokud je mesh nahrán ze souboru, může obsahovat více částí a ke každé se zvlášť nastaví materiál a textura.

K proměnné m_bVisible navíc připíšeme dvojici inline metod, pro nastavení a získání stavu:

BOOL IsVisible() { return m_bVisible; }
void Visible(BOOL bVis = TRUE) { m_bVisible = bVis; }

V další části si povíme více o světlech a implementujeme podporu světel do našeho "enginu".


23.3. Světla v Direct3D

Už jsme si pověděli k čemu slouží materiál. Materiálem nastavujeme vlastnosti povrchu. V této části si povíme o osvětlení scény v Direct3D. Rozlišujeme dva základní zdroje světla: tzv. okolní (ambient) a směrové (directional) osvětlení. Okolním osvětlením nastavíme celkovou světlost scény. Toto světlo nemá žádnou pozici ani směr - je všudypřítomné. Jedinou vlastností tohoto světla je barva. Ambient světlo nastavíme pomocí metody zařízení SetRenderState() s parametrem D3DRS_AMBIENT a barvou světla ve formátu RGBA. Zde můžete použit makro D3DCOLOR_RGBA(r, g, b, a), kde jednotlivé parametry jsou v rozmezí 0 - 255. Například bílé světlo nastavíme pomocí příkazu:

m_lpD3DDevice->SetRenderState(D3DRS_AMBIENT, D3DCOLOR_RGBA(255, 255, 255, 255));

Tímto příkazem ovšem celou scénu "přesvětlíte", takže uvidíte všechny objekty bílé! Aby světla začala fungovat, musíte zapnout osvětlení Direct3D, navíc je třeba mít nastavený nějaký materiál. V našem příkladu nastavujeme standardní materiál pro předdefinované meshe a pro tygra nastavujeme vlastní materiál. Aby osvětlení správně fungovalo, musí mít ten který objekt správně nastavené normálové vektory - tyto vektory jsou v problematice světel klíčové! Normálový vektor je vektor kolmý na plochu, například:

kde N je právě normálový vektor, podle kterého se následně spočítá osvětlení. Odražené světlo je dáno úhlem, který svírá paprsek světla a normálový vektor. Světlo zapneme příkazem:

m_lpD3DDevice->SetRenderState(D3DRS_LIGHTING, TRUE);

V případě tygra je třeba světlo zase vypnout, protože mesh tygra nemá definované normálové vektory a Direct3D nemá podle čeho spočítat světlo. V příkladu si ukážeme mesh, který normálové vektory definované má. Zde pak budeme moci použít osvětlení Direct3D a mesh bude stínovaný. Samotným ambient světlem však stínování neuděláme, protože toto světlo působí na všechny plochu meshe stejně.

23.3.1. Směrová světla

V Direct3D je několik typů těchto světel, každé má rozdílné vlastnosti a použití. Společné pro ně je ovšem struktura D3DLIGHT8, která představuje samotné světlo. Můžeme použít tyto typy:

Bodové světlo - point light

Toto světlo má pozici a barvu, ale svítí všemi směry stejně intenzivně. Příkladem může být třeba žárovka. Dále je možné nastavit parametr Range, který představuje dosah paprsků. Za touto vzdáleností již světlo nebude ovlivňovat objekty.

Plošné světlo - directional light

Plošné světlo má směr a barvu, nikoliv pozici. Toto světlo by se dalo přirovnat ke světlu Slunce! Paprsky tohoto světla jsou tudíž rovnoběžné, jako kdyby zdroj světla byl v nekonečnu. Toto světlo také používáme v našem jednoduchém příkladu, kde takto osvětlujeme rovnoměrně celou scénu. U tohoto světla parametr Range nenastavujeme. Paprsky mají stále stejnou intenzitu.

Směrové světlo - spot light

Poslední typ je spojení obou předchozích světel dohromady, protože má směr, pozici a samozřejmě i barvu. Toto světlo osvětluje tzv. vnitřní kužel rovnoměrně. Vnější kužel je osvětlen nerovnoměrně, tato nerovnoměrnost je určena parametrem Falloff. Tento parametr většino nastavujeme na hodnotu 1.0 pro rovnoměrné rozložení světla mezi vnitřním a vnějším kuželem. Protože toto světlo je docela složité, ukážeme si všechno na obrázku:

Velikost vnitřního a vnějšího kužele je definována dvěma úhly Phi a Theta, které zadáváme v radiánech.

Nakonec si ještě povíme o parametrech Attenuation0, Attenuation1 a Attenuation2. Pomocí těchto parametrů řídíme intenzitu světla v prostoru - definujeme vlastně průběh klesající intenzity s rostoucí vzdálenosti od zdroje ke vzdálenosti určené parametrem Range (z toho vyplývá, že tyto parametry opět nemají význam pro plošné světla). Tyto hodnotu mohou být v rozsahu od 0 do nekonečna, přičemž všechny najednou by neměly být 0. Standardně nastavujeme Attenuation0 a Attenuation2 na 0 a Attenuation1 na 1.0. Pak intenzita klesá nepřímo úměrně se vzdáleností od zdroje.

Nyní se podívejme na strukturu D3DLIGHT8:

typedef struct _D3DLIGHT8 {
    D3DLIGHTTYPE Type;
    D3DCOLORVALUE Diffuse;
    D3DCOLORVALUE Specular;
    D3DCOLORVALUE Ambient;
    D3DVECTOR Position;
    D3DVECTOR Direction;
    float Range;
    float Falloff;
    float Attenuation0;
    float Attenuation1;
    float Attenuation2;
    float Theta;
    float Phi;
} D3DLIGHT8;

Všechny parametry jsme probrali výše. Každé světlo má několik "světelných" složek: diffuse - rozptýlené světlo, ambient - světlo okolí a specular - odrazy. Všechny tyto složky jsou reprezentovány strukturou D3DCOLORVALUE, se kterou jsme se již setkali v první části a i zde platí stejné pravidla pro hodnoty jednotlivých barevných složek. Typ světla může nabývat těchto hodnot: D3DLIGHT_POINT pro bodové světlo, D3DLIGHT_SPOT pro směrové a D3DLIGHT_DIRECTIONAL pro plošné.

23.3.2. Příklad

Nyní přidáme podporu světel do našeho příkladu. Nejprve je třeba otestovat, kolik můžeme mít maximálně aktivních světel. Ve struktuře D3DCAPS8 je atribut MaxActiveLights. Takže nejprve ve funkci Init() zavoláme metodu zařízení GetCaps() a získáme potřebný parametr, který uložíme ve třídě XDisplay. Poté sledujeme počet zapnutých světel a tento počet omezíme výše uvedenou konstantou (dnešní karty mají většinou 8 světel). Maximální počet světel si uložíme například do proměnné m_iMaxLights.

Přidáme následující dvě metody:

int XDisplay::EnableLight(int iIndex, BOOL bEnable)
{
    if(m_lpD3DDevice && (iIndex >= 0 && iIndex < m_iMaxLights))
    {
        return m_lpD3DDevice->LightEnable(iIndex, bEnable);
    }
    return -1;
}

int XDisplay::SetLight(int iIndex, D3DLIGHT8 * pLight)
{
    if(m_lpD3DDevice && pLight && (iIndex >= 0 && iIndex < m_iMaxLights))
    {
        return m_lpD3DDevice->SetLight(iIndex, pLight);
    }
    return -1;
}

Metody LightEnable() a SetLight() jsme volali již v minulé lekci. Ve scéně můžete mít aktivních jen několik málo světel a to s jakým právě pracujete určuje první parametr těchto metod.

Na závěr lekce ještě všechny nové funkce vyzkoušíme v programu Tester.exe. Přidáme další mesh a dvě světla. Tento mesh budeme nahrávat ze souboru .X a konečně bude mít definované normálové vertexy, takže na něm budou vidět světelné efekty jako na předdefinovaných objektech. První světlo bude jakési slunce, které svítí stále v jednom směru modrou barvou. Toto světlo bude plošné. Dále vytvoříme jedno bodové světlo, které budou obíhat kolem objektů ve scéně.

Přidejme tyto objekty:

XMesh g_Airplane;

D3DLIGHT8 g_PointLight;
D3DLIGHT8 g_Sun;

Dále upravíme funkci WinMain():

// inicializace Direct3D
g_theDisplay.Init(g_hWnd);

// 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, 64, 64, g_theDisplay.GetDevice());
g_Teapot.CreateTeapot(g_theDisplay.GetDevice());
// model tygra
g_Tiger.LoadMeshFromFile("tiger.x", g_theDisplay.GetDevice(),
g_theDisplay.GetResourceManager());
g_Airplane.LoadMeshFromFile("airplane 2.x", g_theDisplay.GetDevice(), g_theDisplay.GetResourceManager());

// 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()->AddMesh(&g_Airplane);

// torus is visible by default
g_Torus.Visible();

// zde vytvorime slunicko
ZeroMemory( &g_Sun, sizeof(D3DLIGHT8) );
g_Sun.Type = D3DLIGHT_DIRECTIONAL;
g_Sun.Direction.x = -1.0f;
g_Sun.Direction.y = 0.0f;
g_Sun.Direction.z = -1.0f;
g_Sun.Diffuse.r = 0.0f;
g_Sun.Diffuse.g = 0.0f;
g_Sun.Diffuse.b = 0.7f;
g_Sun.Ambient.r = 0.4f;
g_Sun.Ambient.g = 0.4f;
g_Sun.Ambient.b = 0.4f;
g_Sun.Range = 1000.0f;
g_theDisplay.SetLight(0, &g_Sun);
g_theDisplay.EnableLight(0, TRUE);

// dalsi svetlo
ZeroMemory( &g_PointLight, sizeof(D3DLIGHT8) );
g_PointLight.Type = D3DLIGHT_POINT;
g_PointLight.Position.x = 0.0f;
g_PointLight.Position.y = 0.0f;
g_PointLight.Position.z = 0.0f;
g_PointLight.Diffuse.r = 1.0f;
g_PointLight.Diffuse.g = 1.0f;
g_PointLight.Diffuse.b = 0.0f;
g_PointLight.Range = 1000.0f;
g_PointLight.Attenuation1 = 1.0f;

Nejdříve vytvoříme mesh ze souboru airplane 2.x, dále tento mesh vložíme do správce zdrojů a nastavíme, že implicitně bude viditelný pouze prstenec (torus). V další části vytvoříme a nastavíme dvě světla. U plošného světla (Slunce) nastavíme pouze směr a barvu. Po té voláme dvojici metod SetLight() a EnableLight(), abychom toto světlo aktivovali. U bodového světla nastavíme počáteční pozici, barvu, dosah a rozložení intenzity. Samotná aktivace spolu s vypočtením nové pozice je provedena až ve funkci UpdateFrame(). Po ztrátě zařízení se všechno nastavení ztratí, takže i světla musíme znovu nastavit a aktivovat. Proto přidejme funkci RestoreUserObjects(), která obnoví uživatelské objekty. Tuto funkci budeme volat vždy po funkci RestoreDisplay().

Upravíme proceduru okna tak, aby šel zobrazit i náš nový mesh:

case WM_KEYUP:
    switch( wParam )
    {
        case VK_F8:
            g_theDisplay.RestoreDisplay();

            RestoreUserObjects();
            break;
       
case VK_F1:
            g_Sphere.Visible(!g_Sphere.IsVisible());
            break;
        case VK_F2:
            g_Torus.Visible(!g_Torus.IsVisible());
            break;
        case VK_F3:
            g_Box.Visible(!g_Box.IsVisible());
            break;
        case VK_F4:
            g_Cylinder.Visible(!g_Cylinder.IsVisible());
            break;
        case VK_F5:
            g_Teapot.Visible(!g_Teapot.IsVisible());
            break;
        case VK_F6:
            g_Tiger.Visible(!g_Tiger.IsVisible());
            break;

        case VK_F7:
            g_Airplane.Visible(!g_Airplane.IsVisible());
            break;
      
 case VK_ESCAPE:
            PostQuitMessage(0);
            break;
        }
    break;

Zde také nezapomeňte zavolat již zmíněnou funkci na obnovu světla. Nakonec upravíme funkci UpdateFrame(), která se oproti minulé verzi značně zjednoduší:

void UpdateFrame()
{
    // svetlo se bude pohybovat po kruznici
    g_PointLight.Position.x = 0.0f;
    g_PointLight.Position.y = 4.0f * sin(double(GetTickCount())/300.0f);
    g_PointLight.Position.z = 5.0f * cos(double(GetTickCount())/300.0f);
    g_theDisplay.SetLight(1, &g_PointLight);
    g_theDisplay.EnableLight(1, TRUE);
    // draw scene
    g_theDisplay.UpdateBackground();
    g_theDisplay.GetDevice()->BeginScene();
    g_Sphere.Draw();
    g_Torus.Draw();
    g_Box.Draw();
    g_Cylinder.Draw();
    g_Teapot.Draw();
    // tygr nema definovane normalove vektory, takze je treba vypnout osvetleni
    g_theDisplay.GetDevice()->SetRenderState(D3DRS_LIGHTING, FALSE);
    g_Tiger.Draw();
    g_theDisplay.GetDevice()->SetRenderState(D3DRS_LIGHTING, TRUE);
    g_Airplane.Draw();
    g_theDisplay.GetDevice()->EndScene();
    if(D3DERR_DEVICENOTRESET == g_theDisplay.Present())
    {
        RestoreUserObjects();
    }
}

Nejprve spočítáme novou pozici bodového světla (zde využijeme trochu matematiky ze střední školy), dále toto světlo nastavíme a aktivujeme. Další část vykresluje postupně všechny objekty. Mesh se samozřejmě nevykreslí, pokud není viditelný. U tygra nesmíme zapomenout vypnout osvětlení. Nakonec musíme u metody Present() sledovat návratovou hodnotu D3DERR_DEVICENOTRESET, kterou metoda vrací po obnovení zdrojů a je to vhodná chvíle k opětovnému nastavení našeho slunce.

23.4. Závěr

V dnešní kratší lekci jste se dověděli něco málo o světlech a přidali jsme pár nových funkcí do našeho projektu. Příště si budeme hrát s transformacemi jednotlivých objektů.

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

Jiří Formánek