V dnešní lekci přidáme podporu vertex a index bufferů do našeho enginu. V druhé části si povíme něco o technice mipmap, takže dále rozšíříme třídu XTexture z předminulé lekce.
Tyto buffery jsou podobně jako textury považovány za zdroje, takže je nutné je při ztrátě zařízení smazat, znovu vytvořit a naplnit. Zde nastává problém jak uchovat data z bufferu. Mohli bychom vytvořit kopii těchto dat a z této kopie obnovit obsah, ale tento způsob by byl značně paměťově neefektivní, neboť bychom data měli v paměti dvakrát! Na druhou stranu bychom se o obnovu bufferu nemuseli vůbec starat. Problém můžeme vyřešit tak, že po obnovení bufferů (tj. po volání funkce RestoreBuffer()) znovu naplníme buffer, takže o obsah se bude starat klientská aplikace (v našem příkladu program Tester). U dynamických bufferů tento problém vůbec nenastává, protože tyto buffery jsou plněny v každém cyklu aplikace, takže stačí pouze znovu vytvořit buffer.
Do projektu Display přidáme dvě nové třídy:
- XVertexBuffer
- XIndexBuffer
Obě tyto třídy budou mít velice podobné metody:
- Create()
- Release()
- Restore()
- GetBuffer()
- GetDevice()
Všimněte si, že metody jsou velice podobné těm z třídy
XTexture. Obě třídy se budou lišit jen v několika parametrech metody
Create().
Deklarace tříd:
class
XVertexBuffer
{
UINT m_uiLength;
DWORD m_dwUsage;
DWORD m_dwFVF;
D3DPOOL m_d3dPool;
IDirect3DVertexBuffer8* m_pVertexBuffer;
IDirect3DDevice8* m_lpDevice;
public:
int Create(UINT uiLength, DWORD dwUsage, DWORD dwFVF, D3DPOOL
dwPool, LPDIRECT3DDEVICE8 lpDevice);
int Restore();
void Release();
LPDIRECT3DVERTEXBUFFER8 GetBuffer() { return m_pVertexBuffer;
}
LPDIRECT3DDEVICE8 GetDevice() {return m_lpDevice; }
public:
XVertexBuffer(void);
~XVertexBuffer(void);
};
class XIndexBuffer
{
UINT m_uiLength;
DWORD m_dwUsage;
D3DFORMAT m_dwFormat;
D3DPOOL m_d3dPool;
IDirect3DIndexBuffer8* m_pIndexBuffer;
IDirect3DDevice8* m_lpDevice;
public:
int Create(UINT uiLength, DWORD dwUsage, D3DFORMAT dwFormat,
D3DPOOL dwPool, LPDIRECT3DDEVICE8 lpDevice);
int Restore();
void Release();
LPDIRECT3DINDEXBUFFER8 GetBuffer() { return m_pIndexBuffer; }
LPDIRECT3DDEVICE8 GetDevice() {return m_lpDevice; }
public:
XIndexBuffer(void);
~XIndexBuffer(void);
};
Z minulých lekcí již víme jaké parametry mají tyto buffery. Všechny tyto
parametry musíme uložil ve třídě, abychom mohli buffer obnovit. Ukládáme také
ukazatel na zařízení a samotný buffer. Implementace metod bude velice podobná
jako u třídy XTexture. Uvedu zde pouze implementaci
metod třídy XVertexBuffer, protože implementace
třídy XIndexBuffer je obdobná.
XVertexBuffer::XVertexBuffer(void)
{
m_lpDevice = NULL;
m_pVertexBuffer = NULL;
}
XVertexBuffer::~XVertexBuffer(void)
{
Release();
m_lpDevice = NULL;
}
int XVertexBuffer::Create(UINT uiLength, DWORD dwUsage, DWORD dwFVF, D3DPOOL
dwPool, LPDIRECT3DDEVICE8 lpDevice)
{
if(!lpDevice)
{
THROW("CreateVertexBuffer: Device is
invalid.");
}
if(!m_pVertexBuffer)
{
m_lpDevice = lpDevice;
m_uiLength = uiLength;
m_dwUsage = dwUsage;
m_dwFVF = dwFVF;
m_dwPool = dwPool;
DWORD dwRet = m_lpDevice->CreateVertexBuffer(uiLength,
dwUsage, dwFVF, dwPool, &m_pVertexBuffer);
if(dwRet != S_OK) {
THROWERR("Cannot
create Vertex buffer due", dwRet);
}
return 0;
}
return -1;
}
int XVertexBuffer::Restore()
{
if(m_lpDevice && !m_pVertexBuffer)
{
DWORD dwRet = m_lpDevice->CreateVertexBuffer(m_uiLength,
m_dwUsage, m_dwFVF, m_dwPool, &m_pVertexBuffer);
if(dwRet != S_OK) {
THROWERR("Cannot
create Vertex buffer due", dwRet);
}
return 0;
}
return -1;
}
void XVertexBuffer::Release()
{
SAFE_RELEASE(m_pVertexBuffer);
}
Ve funkci Create() je nutné zálohovat všechny parametry vytvářeného bufferu. Tyto parametry později využijeme v metodě Restore().
Na závěr této části ještě přidáme podporu bufferů do našeho manažeru zdrojů. Přidejme dvě dynamická pole:
std::vector <XVertexBuffer*>
m_arVB;
std::vector <XIndexBuffer*> m_arIB;
a dvojici metod pro obsluhu těchto polí:
int AddVertexBuffer(XVertexBuffer *
pBufferToAdd);
int AddIndexBuffer(XIndexBuffer * pBufferToAdd);
Princip je obdobný jako v případě textur a meshů.
V této části si vysvětlíme co je to mipmapping a upravíme třídu XTexture tak, aby mipmapping podporovala.
Základním problémem u textur je použití správné velikosti nebo detailnosti textury. Pokud použijete texturu o rozměrech 1024x1024, tak sice docílíte perfektního vzhledu blízkých objektů, ale vzdálené objekty budou vypadat divně (je vidět, že textura je zbytečně detailní) a navíc bude vykreslování mnohem pomalejší! Pokud naopak použijete texturu 16x16, tak budou všechny objekty vyloženě ošklivé! Doporučená velikost textury je 256x256. S touto texturou by měly grafické karty pracovat nejrychleji. Někdy ale potřebujete jen 64x64, pak vytvoříte texturu 256x256 a v ní políčka o rozměrech 64x64. V každém políčku pak bude jiná textura. Správnou texturu vybereme pomocí texturových souřadnic. Problém s detaily však přetrvává. Nejlepší by bylo, kdyby objekty, které jsou blízko měly detailní texturu a objekty vzdálené měly méně detailní texturu. A přesně tohle dělá mipmapping!
Jak to pracuje?
Mějme základní texturu o rozměrech 256x256. K této základní textuře
vytvoříme další textury o rozměrech 128x128, 64x64, 32x32, 16x16, 8x8, 4x4, 2x2,
1x1 podle úrovně mipmap. Systém DirectD3 pak chytře nanáší na nejbližší objekty
nejdetailnější texturu, na vzdálenější objekty méně detailní texturu atd. Ve
skutečnosti tak zabijete dvě mouchy jednou ranou: všechny objekty ve scéně
vypadají tak jak mají a zároveň nepoužíváte zbytečně velké textury na vzdálené
objekty, takže vykreslování je rychlejší.
Jak vytvoříme mipmapy?
Zde je několik možností. Buď vytvoříte ručně všechny textury jednotlivých
úrovní mipmap, které pak ručně nahrajete. Nebo textury vytvoříte programově ze
základní textury anebo použijete funkci
D3DXLoadTextrureFromFileEx(), která udělá vše za vás:) V našem příkladě
použijeme třetí možnost.
Nyní zavedeme podporu mipmap do našeho projektu. Přidáme metodu LoadTextureFromFileEx(). Metoda bude vypadat podobně jako LoadTextureFromFile(), ale přidáme navíc jeden parametr typu int. Tento parametr určí počet úrovní mimap. Je zbytečné vytvářet mipmapy až do úrovně 1x1. Pokud v tomto parametru zadáte 1, textura se vytvoří bez mipmap (k tomuto účelu je ale lepší volat metodu LoadTextureFromFile()). V případě, že předáte parametr rovný 0, vytvoří se kompletní řetěz mipmap tj. až do úrovně 1x1.. Dále je třeba přidat barevný formát textury, protože tento parametr vyžaduje rozšířená funkce D3DXLoadTextureFromFileEx(). Formát získáme ze třídy XDisplay. Uvedené dva parametry musíme uložit do třídy XTexture, abychom mohli texturu obnovit v metodě Restore().
int XTexture::LoadTextureFromFileEx(LPCSTR
szFileName, int iMipLevels, D3DFORMAT formatTextureFormat, LPDIRECT3DDEVICE8
lpDevice)
{
if(!lpDevice)
{
THROW("LoadTextureFromFileEx: 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.");
}
if(D3D_OK !=
D3DXCreateTextureFromFileEx(lpDevice, m_szFilePath, 0, 0, iMipLevels,
D3DUSAGE_RENDERTARGET, formatTextureFormat, D3DPOOL_DEFAULT,
D3DX_DEFAULT , D3DX_DEFAULT, 0, NULL, NULL, &m_lpTexture))
{
TRACE("Texture
'%s' was loaded.", szFileName);
}
else
{
TRACE("Texture
'%s' wasn't loaded.", szFileName);
}
// save information to restore
m_lpDevice = lpDevice;
m_iMipLevels = iMipLevels;
m_formatTextureFormat =
formatTextureFormat;
return 0;
}
return -1;
}
Upravená metoda LoadTextureFromFileEx() je vlastně stejná jako primitivní verze, jen místo D3DXLoadTextureFromFile() používá D3DXLoadTextureFromFileEx() a ukládá počet mipmap úrovní a formát textury.
Pro úplnost ještě uvedu původní metodu LoadTextureFromFile():
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.");
}
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;
m_iMipLevels = 1;
return 0;
}
return -1;
}
Zde si musíme poznamenat, že textura je bez mipmap, takže má pouze jednu úroveň.
Na závěr si uvedeme rozdvojenou metodu Restore():
int XTexture::Restore()
{
// reinit texture
if(m_lpDevice)
{
if(m_iMipLevels == 1)
{
// try to
create 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);
}
}
else
{
if(D3D_OK !=
D3DXCreateTextureFromFileEx(m_lpDevice, m_szFilePath, 0, 0, m_iMipLevels,
D3DUSAGE_RENDERTARGET, m_formatTextureFormat, D3DPOOL_DEFAULT,
D3DX_DEFAULT , D3DX_DEFAULT, 0, NULL, NULL, &m_lpTexture))
{
TRACE("Texture '%s' was reloaded.", m_szFilePath);
}
else
{
TRACE("Texture '%s' wasn't reloaded.", m_szFilePath);
}
}
}
return -1;
}
Možná jste si všimli těchto tří řádků v metodě Init() třídy XDisplay:
// 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);
Takto nastavíme filtrování textur a filtrování mezi jednotlivými úrovněmi mipmap.
Proměnné m_tftXXXTextureFilter nastavujeme podle následující tabulky v projektu Setup:
Typ filtrování | Typ MAG filtru | Typ MIN filtru | Typ MIPMAP filtru |
Bilinear filtering | linear magnification | linear minification | point mipmapping |
Trilinear filtering | linear magnification | linear minification | linear mipmapping |
Bilinear Anisotropic filtering | anisotropic magnification | anisotropic minification | point mipmapping |
Trilinear Anisotropic filtering | anisotropic magnification | anisotropic minification | linear mipmapping |
Na závěr lekce vyzkoušíme nové funkce našeho enginu v projektu Tester. Vytvoříme jednoduchý čtverec v prostoru, který se bude skládat ze čtyř vertexů. Tyto vrcholy uložíme do vertexu bufferu a indexovat je budeme pomocí index bufferu. Dále nahrajeme texturu, u které vytvoříme mipmap řetěz. Aby byl vidět efekt mipmap, použil jsem texturu velkých rozměrů, kterou naneseme na relativně malou plochu.
Nadeklarujete tyto nové proměnné:
XVertexBuffer g_vbQuad;
XIndexBuffer g_ibQuad;
XTexture g_texMipmap;
BOOL g_bQuadVis = FALSE;
BOOL g_bQuadStop = FALSE;
// world matrix
D3DXMATRIX g_matWorld1;
D3DXMATRIX g_matWorld2;
Rotující čtverec má jinou světovou transformační matici (otáčí se kolem osy Z) než ostatní objekty (otáčí se kolem osy X), takže jsem pro obě transformace vytvořil zvláštní světové matice. V příští lekci tuto techniku trochu vylepšíme. Rotaci čtverce navíc bude možno zastavit mezerníkem a to určí proměnná g_bQuadStop. To, zda-li je čtverec vidět, určí proměnná g_bQuadVis. Ostatní proměnné jsem popsal výše. Dále přidejme funkci, která naplní oba buffery daty:
void FillBuffers();
Tyto řádky přidejte do funkce WinMain():
// init world matrices
D3DXMatrixIdentity(&g_matWorld1);
D3DXMatrixIdentity(&g_matWorld2);
// prepare quad
g_vbQuad.Create(4 * sizeof(VERTEX),
D3DUSAGE_WRITEONLY, VERTEXFORMAT, D3DPOOL_DEFAULT, g_theDisplay.GetDevice());
g_ibQuad.Create(6 * sizeof(WORD), D3DUSAGE_WRITEONLY, D3DFMT_INDEX16 ,
D3DPOOL_DEFAULT, g_theDisplay.GetDevice());
g_texMipmap.LoadTextureFromFileEx("grass.bmp", 5, g_theDisplay.GetTextureFormat(),
g_theDisplay.GetDevice());
g_theDisplay.GetResourceManager()->AddIndexBuffer(&g_ibQuad);
g_theDisplay.GetResourceManager()->AddVertexBuffer(&g_vbQuad);
g_theDisplay.GetResourceManager()->AddTexture(&g_texMipmap);
FillBuffers();
Zde inicializujeme obě matice, dále vytvoříme oba buffery.
Máme 4 vrcholy a 6 indexů (dva trojúhelníky po třech vrcholech), vytvoříme
texturu pomocí nové metody LoadTextureFromFileEx()
a všechny zdroje vložíme do manažeru zdrojů. Nakonec zavoláme funkci
FillBuffers(), která naplní oba buffery daty:
void FillBuffers()
{
VERTEX *pVertices;
g_vbQuad.GetBuffer()->Lock(0, 0, (BYTE**) &pVertices, 0);
pVertices[0].vecPos = D3DXVECTOR3(-4.0f, -4.0f, 2.0f);
pVertices[1].vecPos = D3DXVECTOR3(4.0f, -4.0f, 2.0f);
pVertices[2].vecPos = D3DXVECTOR3(-4.0f, 4.0f, 2.0f);
pVertices[3].vecPos = D3DXVECTOR3(4.0f, 4.0f, 2.0f);
pVertices[0].vecNormal = D3DXVECTOR3(0.0f, 0.0f, 1.0f);
pVertices[1].vecNormal = D3DXVECTOR3(0.0f, 0.0f, 1.0f);
pVertices[2].vecNormal = D3DXVECTOR3(0.0f, 0.0f, 1.0f);
pVertices[3].vecNormal = D3DXVECTOR3(0.0f, 0.0f, 1.0f);
pVertices[0].tu1 = 0.0f;
pVertices[0].tv1 = 0.0f;
pVertices[1].tu1 = 1.0f;
pVertices[1].tv1 = 0.0f;
pVertices[2].tu1 = 0.0f;
pVertices[2].tv1 = 1.0f;
pVertices[3].tu1 = 1.0f;
pVertices[3].tv1 = 1.0f;
pVertices[0].dwDiffuse = 0xFFFFFFFF;
pVertices[1].dwDiffuse = 0xFFFFFFFF;
pVertices[2].dwDiffuse = 0xFFFFFFFF;
pVertices[3].dwDiffuse = 0xFFFFFFFF;
g_vbQuad.GetBuffer()->Unlock();
WORD *pIndices;
g_ibQuad.GetBuffer()->Lock(0, 0, (BYTE**) &pIndices, 0);
pIndices[0] = 0;
pIndices[1] = 1;
pIndices[2] = 2;
pIndices[3] = 1;
pIndices[4] = 3;
pIndices[5] = 2;
g_ibQuad.GetBuffer()->Unlock();
}
Tento postup jsme již použili v některé předešlé lekci. Nejprve je třeba buffer
uzamknout a tak získat přímý přístup do paměti tohoto bufferu. Na závěr je
samozřejmě třeba volat metodu Unlock() pro oba
buffery.
Nyní upravme obslužnou proceduru okna tak, aby se stiskem klávesy F8 zobrazil náš nový čtverec, ale zároveň se musí všechny ostatní objekty skrýt. Pod mezerník navíc namapujeme funkci zastavení rotace čtverce:
case VK_F9:
g_theDisplay.RestoreDisplay();
RestoreUserObjects();
FillBuffers();
break;
case VK_F1:
g_Sphere.Visible(!g_Sphere.IsVisible());
g_bQuadVis = FALSE;
break;
case VK_F2:
g_Torus.Visible(!g_Torus.IsVisible());
g_bQuadVis = FALSE;
break;
case VK_F3:
g_Box.Visible(!g_Box.IsVisible());
g_bQuadVis = FALSE;
break;
case VK_F4:
g_Cylinder.Visible(!g_Cylinder.IsVisible());
g_bQuadVis = FALSE;
break;
case VK_F5:
g_Teapot.Visible(!g_Teapot.IsVisible());
g_bQuadVis = FALSE;
break;
case VK_F6:
g_Tiger.Visible(!g_Tiger.IsVisible());
g_bQuadVis = FALSE;
break;
case VK_F7:
g_Airplane.Visible(!g_Airplane.IsVisible());
g_bQuadVis = FALSE;
break;
case VK_F8:
g_Torus.Visible(FALSE);
g_Sphere.Visible(FALSE);
g_Box.Visible(FALSE);
g_Tiger.Visible(FALSE);
g_Airplane.Visible(FALSE);
g_Teapot.Visible(FALSE);
g_Cylinder.Visible(FALSE);
g_bQuadVis = TRUE;
break;
case VK_SPACE:
if(g_bQuadVis)
g_bQuadStop = !g_bQuadStop;
break;
case VK_ESCAPE:
PostQuitMessage(0);
break;
Dále si všimněte volání funkce FillBuffers() po volání RestoreUserObjects(). Zde právě plníme již nové buffery, které systém vytvořil po resetu zařízení. Nakonec zde uvedu funkci UpdateFrame():
void UpdateFrame()
{
// svetlo se bude pohybovat po kruznici
g_PointLight.Position.x = 0.0f;
g_PointLight.Position.y = float(4.0f * sin(double(GetTickCount())/300.0f));
g_PointLight.Position.z = float(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();
if(g_bQuadVis)
{
D3DXMATRIX matRot;
float fFactor = cmnGetTime(TIMER_GETELAPSEDTIME1);
if(fFactor != 0 && !g_bQuadStop) {
D3DXMatrixRotationZ(&matRot, 0.9f * fFactor);
D3DXMatrixMultiply(&g_matWorld2, &g_matWorld2, &matRot);
g_theDisplay.GetDevice()->SetTransform(D3DTS_WORLD,
&g_matWorld2);
}
g_theDisplay.GetDevice()->SetRenderState(D3DRS_LIGHTING,
FALSE);
g_theDisplay.GetDevice()->SetVertexShader(VERTEXFORMAT);
g_theDisplay.GetDevice()->SetStreamSource(0,
g_vbQuad.GetBuffer(), sizeof(VERTEX));
g_theDisplay.GetDevice()->SetIndices(g_ibQuad.GetBuffer(),
0);
g_theDisplay.GetDevice()->SetTexture(0,
g_texMipmap.GetTexture());
g_theDisplay.GetDevice()->DrawIndexedPrimitive(D3DPT_TRIANGLELIST,
0, 4, 0, 2);
g_theDisplay.GetDevice()->SetRenderState(D3DRS_LIGHTING,
TRUE);
}
else
{
D3DXMATRIX matRot;
float fFactor = cmnGetTime(TIMER_GETELAPSEDTIME1);
if(fFactor != 0) {
D3DXMatrixRotationX(&matRot, 0.9f * fFactor);
D3DXMatrixMultiply(&g_matWorld1, &g_matWorld1, &matRot);
g_theDisplay.GetDevice()->SetTransform(D3DTS_WORLD,
&g_matWorld1);
}
}
g_theDisplay.GetDevice()->EndScene();
if(D3DERR_DEVICENOTRESET == g_theDisplay.Present())
{
RestoreUserObjects();
FillBuffers();
}
}
Zde rozlišujeme případ, kdy je rotující čtverec vidět a kdy
ne. Pokud je vidět, je třeba aplikovat jinou transformaci na světovou matici
scény (matice máme dvě, aby pohyb obou soustav začal vždy tam kde skončil) a pak
vykreslíme čtverec v těchto krocích:
1) Vypnutí světla
2) Nastavění formátu vrcholů
3) Nastavění zdrojového vertex bufferu
4) Nastavení zdrojového index bufferu
5) Nastavení textury
6) Samotné vykreslení čtverce
7) Zapnutí světla
Všechny tyto kroky jsme již rozebírali podrobněji v minulých lekcích. V druhém případě, pokud není rotující čtverec vidět, nastavíme jinou světovou matici, kterou provádíme rotaci ostatních objektů kolem osy X.
Nyní se již třída XDisplay nestará o transformaci světové matice, takže tento kód můžete zcela odstranit (jedná se o deklaraci matice, vytvoření a samotnou transformaci matice). Dále jsme použili strukturu VERTEX deklarovanou následovně:
struct VERTEX
{
D3DXVECTOR3 vecPos;
D3DXVECTOR3 vecNormal;
DWORD dwDiffuse;
float tu1, tv1;
};
V našem příkladu inicializujeme i normálové vektory, ale v praxi je vlastně nepoužijeme, protože vypínáme osvětlení.
Uvedený příklad samozřejmě najedete v sekci Downloads. Klávesou F8 zobrazíte rotující čtverec, na kterém je nanesena velká textura. Když si čtverec zastavíte (mezerníkem) ve správné poloze, jsou vidět jednotlivá rozhraní mipmap (nejlépe je to vidět, když máte nastaveno obyčejné bilineární filtrování).
V příští lekci se konečně podíváme na transformaci jednotlivých objektů pomocí matic.
Těším se příště nashledanou.