2010. október 16., szombat

SlimDX9 06 - Textúrázás alapfokon

Az előző néhány bejegyzésben láttuk, hogy hogyan építhetünk fel egyszerű objektumokat és azokhoz hogyan rendelhetünk különféle anyagtulajdonságokat. Azonban, ha realizmusra törekszünk, egy komplex jelenet felépítése óriási munkát jelentene, gondoljunk csak egy csempézett falra vagy egy hajópadlóra. Itt jön a képbe a texture mapping eljárás ami Dr. Edwin Catmull 1974-es doktori disszertációjából származik. Ebben a leckében ezért a textúrázás alapjaival fogunk megismerkedni a DirectX API keretein belül.

Tex-tor-túra

Kezdetben a textúra nem volt más, mint egy kétdimenziós tömb, melynek minden pontja az adott pontbeli szín koordinátáit tartalmazta (körülbelül, mint egy bitmap formátumú kép (.bmp), a neve is hasonló: bittérképes textúra). Később megjelentek összetettebb textúrázási módszerek is, például a procedurális textúrázás, illetve a 3D textúrák. Előbbi lényege, hogy a textúránkat olyan módszerekkel generáljuk, melyek különféle zajokat adnak a kapott bittérképhez, például fraktál- vagy turbulencia-alapú zajokat. Ezek hasonlítanak a természetben előforduló „zajokra”, így egy igen realisztikus képet kapunk, amely minden újra-generáláskor egy kicsit változik. Ezzel a módszerrel például fa, szikla, gránit és márvány anyagokat szokás generálni. A másik módszer, a háromdimenziós textúrázás esetén n darab (ahol n kettő hatványa) kétdimenziós textúránk van, és mindig a mélységtől függ, melyiket használjuk.


(1) 3D-s modell textúra nélkül, (2) 3D-s modell textúrával

De térjünk is vissza a bittérképes textúrázáshoz. Egy textúrázott test azt jelenti, hogy a felszínének pontjait kölcsönös kapcsolatba hozzuk egy kép pontjaival, azaz a test pontjainak színét egy képből kapjuk. A testet, mint megbeszéltük, felbonthatjuk háromszögekre. Ezek a háromszögek és egy kép pontjai között kell leképezési módszert találnunk. Ez DirectX-ben nagyon egyszerű, a háromszög minden pontjához megmondjuk, hogy a textúra melyik pontja esik rá. Ehhez csupán a textúra-koordinátarendszert kell ismerni. A textúra egy kép, azaz egy téglalap. A téglalap bal felső sarka az origó, a két, ebből a pontból kiinduló oldalak pedig a bázisvektorok. Azaz a jobb felső sarok az (1,0), a balalsó sarok pedig a (0,1) pont:


Mivel a textúrák mérete nem egységes, be kellett vezetni egy egységes rendszert, amelyben és UV értékei 0 és 1 között mozognak. A DirectX-ben megengedett a mintakoordináták határértékeinek a túllépése. Azon értékek esetében, amelyek a [0,1] intervallumon kívül esnek, az éppen beállított szabály lép életbe. Megadhatjuk, hogy a minta egész koordinátaérték túllépésekor tükröződjön, ismétlődjön stb.

Hárdkódolás: Textúra betöltés

Az elmélet után vegyünk egy nagy levegőt és vágjunk bele a megvalósításba. Az első és egyik legfontosabb dolog a megfelelő vertexformátum kialakítása:
  [StructLayout(LayoutKind.Sequential)]
struct Vertex
{
public Vector3 Position;
public Vector2 Texture;
public static readonly VertexFormat Format = VertexFormat.Position | VertexFormat.Texture1;
public static readonly int Stride = Marshal.SizeOf(typeof(Vertex));

public Vertex(float x, float y, float z, float u, float v)
{
Position = new Vector3(x, y, z);
Texture = new Vector2(u, v);
}
}
Ez azt jelenti, hogy a vertexeink természetesen transzformálatlanok, és egyetlen textúrát akarunk rá kifeszíteni (nincs jelenleg multitextúrázás). Több textúra használatáról majd később. A Stride a Vertex struktúra méretét tárolja, később még szükség lesz, mivel bevezetjük majd a Vertex Buffer fogalmát. Most azonban egyenlőre maradjunk a textúrázásnál.
A textúrához felhasználni kívánt képet a SlimDX-ben a Texture.FromFile() metódus felhasználásával tölthetjük be, ami a következő paramétereket várja: device a DirectX eszköz, fileName a betöltendő kép neve, usage a használat körülményeit rögzíti, míg a pool a memóriakezelést állítja be. A függvény a .bmp, .dds, .dib, .hdr, .jpg, .pfm, .png, .ppm, és .tga típusú képeket tud feldolgozni:

Texture texture = Texture.FromFile(device, "directx.png", 0, Pool.Managed);

A textúrák megjelenítését szűrők alkalmazásával finomíthatjuk. A következő két sor egy anizotróp nagyító, illetve kicsinyítő szűrőt állít be, amelyeket az első (nulla indexű) megjelenítési szakaszban alkalmazunk:

device.SetSamplerState(0, SamplerState.MinFilter, TextureFilter.Anisotropic);
device.SetSamplerState(0, SamplerState.MagFilter, TextureFilter.Anisotropic);

Megjelenítéskor a létrehozott textúrát hozzárendeljük a mintavételezési szakaszhoz (sampler stage) a SetTexture() hívással: device.SetTexture(0, texture);

A textúrázott kockát az OnResourceLoad() metódusban töltjük fel, csak itt a színek helyett a textúrakoordinátákat adjuk meg a pozíció mellett:

data = new Vertex[]
{
// elülső oldal
new Vertex(-1.5f, +1.5f, -1.5f, 0, 0),
...

Ezzel tulajdon képen végeztünk is a textúrázással, a részletek a mintakódban megtalálhatóak. Persze, nagyon nagy ez a téma, és itt csak az alapokat beszéltük át. Most viszont ismerkedjünk meg a Vertex Buffer fogalmával, mivel nélküle elképzelhetetlen a hatékony renderelés.

Hárdkódolás: Vertex buffer

A vertex buffer egy olyan tároló, persze a memóriában, mely a vertex adatokat tárolja. A Direct3D ezt használja adatfolyamként, azaz ebből veszi ki a vertexeket sorrendben. Ezért fontos ismernünk a vertex buffer létrehozási módját, illetve feltöltését. Tehát nézzük, hogy kell létrehozni egy vertex buffert. Nyilván kell egy buffer:

private VertexBuffer vertices;

Ezt kell most létrehoznunk:

vertices = new VertexBuffer(device, 36 * Vertex.Stride, Usage.WriteOnly, Vertex.Format, Pool.Managed);

Itt csupán a paramétereket kell megbeszélni. Ez első paraméter a DirectX eszköz, amit létrehoztunk. A következő a természetesen a vertex buffer mérete, ami a vertexek száma és a Vertex struktúra méretének szorzata. Mivel egy kockát jelenítünk meg ezért 36 vertexre van szükség. A következő paraméter általánosan a használat körülményeiről szól. Komoly optimalizálást jelenthet a csak írható puffer létrehozása, amelyet az Usage.WriteOnly jelez. Ezáltal közöljük a rendszerrel, hogy csak is írni fogunk az adott buffer memóriaterületére, ami lehetővé teszi az írás és képszintézis szempontjából optimális memóriaterület kiválasztását. A következő paraméter a tárolt vertexek típusát adja meg, ezt tudni kell a tárolás módja miatt. A Pool.Managed a memóriakezeléssel kapcsolatos (hova rakja a buffert), ilyenkor a DirectX automatikusan abba a memóriába menti a vertexeket, amihez hozzáfér (általában a rendszermemória). Most már csak fel kellene tölteni a buffert. Ezért először a buffer címe kell, amit a Lock paranccsal tudjuk lekérni (illetve ez engedélyezi az írást illetve olvasást, mivel a Lock és az Unlock között lezárjuk a memóriát, kívülről nem elérhető). Ehhez létrehozunk egy DataStream objektumot és zároljuk a buffert:

DataStream stream = vertices.Lock(0, 0, LockFlags.None);

Az első paraméter azt adja meg, hogy hányadik bájttól kezdve akarjuk zárolni, a második pedig azt, hogy onnantól kezdve mekkora területet (bájtokban). Ez mindkettő 0, hiszen ekkor az egész buffert zárolja. Az utolsó paraméter a zárolás módját jellemzik, flagek, nekünk most nem kell. Ekkor a data tömbben tárolt vertexeket át kell ide másolni: stream.WriteRange(data);
Ezután lezárjuk a buffert: vertices.Unlock();
Kész is lennénk, már csak meg kell jeleníteni. Ez a vertex buffer-es módszer azért jó, mert sokkal gyorsabb, mint sima tömbből töltögetni be a vertexek adatait, illetve az adatfolyamban a típusigazítás is gyorsít. A következő beállítandó az aktuális adatfolyam, amelyből a vertexeket kivesszük:

device.SetStreamSource(0, vertices, 0, Vertex.Stride);

Az első paraméter az adatfolyam száma, azaz ezt állítjuk be. Alapértelmezésben a 0 aktív. Ez azért jó, mert több adatfolyamot egyszerre beállíthatunk, majd azt megjelenítésnél gyorsan váltogathatjuk a SetStreamSourceFreq() függvénnyel, de ez most nem fontos. A következő annak a vertexbuffernek a mutatója, amelyikhez akarjuk kötni az adatfolyamot, ez nyilván az előbb létrehozott bufferünk, benne a háromszögek pontjaival. A következő paraméter egy eltolás paraméter, azaz honnan kezdődnek az adatfolyamban a vertexek adatai (bájtokban). Ezt nem is biztos, hogy támogatja az eszköz. Az utolsó paraméter FVF vertexeknél egy vertex mérete, ugyanis ekkora „adagokban” kapjuk a vertexeket. Most pedig jön az érdemi rajzolás, méghozzá a

device.DrawPrimitives(PrimitiveType.TriangleList, 0, 12);

függvénnyel. Ez most különálló háromszögeket rajzol. Ez a függvény az aktuális adatfolyamban található, második paraméterben megadott sorszámú vertextől kezdődően rajzol annyi primitívet, ahányat az utolsó paraméterben megadtunk. Vertex Buffer használata esetén a DrawPrimitives függvény használatos a DrawUserPrimitives helyett. Most már nincs más hátra, mint letölteni a példa forrását és gyönyörködni a textúrázott objektumokban: DOWNLOAD

Textúrázott kockák...

Ezzel a végére is értünk a mai SlimDX anyagnak és egy nagy lépést tettünk a hatékony és látványos DirectX alkalmazások felé. A bejegyzés megírásában nagy segítségemre voltak Nyisztor Károly DirectX-el kapcsolatos könyvei.

0 megjegyzés :

Megjegyzés küldése