Texture
Introduzione al concetto di texture, di coordinate texture. Come animare le coordinate texture
Il concetto di texture bidimensionale in computer grafica è molto semplice: una texture è una griglia di pixel colorati. I parametri che la caratterizzano sono sostanzialmente le sue dimensioni e la memoria occupata dal singolo pixel (bpp, bit per pixel) che indica anche il numero di possibili colori che può rappresentare.
Come abbiamo visto nel precedente articolo, un colore può essere rappresentato da una singola variabile 32 bit. Nessuno ci vieta però di rappresentare un colore in 16 bit, assegnandone 5 al rosso, 6 al verde e 5 al blu. Credo siano ovvi i vantaggi che derivano da questo fatto: minori requisiti di memoria e quindi maggiore velocità di trasferimento della texture dalla memoria di sistema all’acceleratore. Ovviamente avendo a disposizione un numero limitato di bit, limiteremo le possibili “sfumature” di colore rappresentabili. Se non consideriamo il canale alpha, con 24bit possiamo rappresentare 2 alla 24 colori, ovverosia 16 milioni e rotti, mentre con 16 bit ne possiamo rappresentare “solo” 65.536. Ovviamente non sono solo questi i formati di colore che esistono in Direct3D, ma è possibile assegnare più o meno bit di precisione ad ogni canale (scegliendo tra alcuni formati standard supportati da buona parte degli acceleratori in giro).
L’enumerazione D3DFORMAT in D3D definisce i vari tipi di “pixel format”. Alcuni valori di quest’enumerazione hanno nomi del tipo D3DFMT_xAxRxGxB, dove al posto delle “x” ci sono i bit di precisione del pixel format. Ecco alcuni esempi tratti dalla documentazione del DXSDK:
D3DFMT_X1R5G5B5
16-bit pixel con 5 bit per ogni canale
D3DFMT_A1R5G5B5
16-bit pixel format con 1 bit per il canale alpha, e 5 per i canali di colore
D3DFMT_A4R4G4B4
16-bit ARGB pixel format con 4 bit per ogni canale
D3DFMT_A8
8-bit solo alpha
D3DFMT_R3G3B2
8-bit RGB texture format con 3 bit per il rosso, 3 per il verde e 2 per il blu
Oltre a questi formati esistono anche degli altri un po’ meno diffusi, e i formati delle texture compresse DDS, un formato speciale di DirectX che permette di risparmiare memoria.
In DirectXGraphics le texture bidimensionali sono rappresentate da un oggetto COM di classe IDirect3DTexture8 creabile attraverso IDirect3DDevice8::CreateTexture(). Solitamente le texture vengono lette da un file creato con un qualsiasi programma di grafica. DirectXGraphics non può leggere questi file, ma grazie al cielo la libreria di supporto D3DX compie questo noioso lavoro per noi. Tramite D3DXCreateTextureFromFile() possiamo creare una texture leggendola dal disco.
HRESULT hr;
IDirect3DTexture8* texture;
hr = D3DXCreateTextureFromFile(lpDev,"MiaTexture.bmp",&texture);
if(FAILED(hr))
{
//Non sono riuscito a leggere la texture!!!
}
Questa funzione ci risparmia un sacco di lavoro perché provvede da sola a dare alcuni valori di default ragionevoli alla sua sorella più complessa, D3DXCreateTextureFromFileEx(…) che permette di specificare molti più parametri.
Considerazioni sull’hardware
Nella creazione di una texture dovete sempre fare attenzioni alle limitazioni dell’hardware. Alcuni (vecchi) acceleratori hanno problemi con texture che non siano quadrate e dimensioni di due (per esempio 128x128, 256x256 etc), altre non supportano alcuni formati di colore etc. In teoria dovreste sempre controllare il membro TextureCaps di D3DCAPS8 per assicurarvi della presenza di una certa caratteristica, in pratica potete fare sempre affidamento su alcune cose.Fate questi noiosi controlli solo quando avete effettivamente bisogno di qualche formato “strano” o poco supportato. Ecco alcuni esempi:
if(wrapper->currCaps.TextureCaps&D3DPTEXTURECAPS_CUBEMAP)
{
//il device supporta le cubemaps
}
if(wrapper->currCaps.TextureCaps&D3DPTEXTURECAPS_VOLUMEMAP)
{
//il device supporta le textures volumetriche
}
Coordinate texture
L’esempio chiamato “LaMiaPrimaTexture” è una piccola modifica dell’originale “QuadratoArcobaleno”, solo che ora nel FVF abbiamo aggiunto spazio per un set di coordinate texture e le abbiamo impostate durante il Lock/Unlock
struct MyVertex
{
D3DXVECTOR2 pos;
float z,w;
DWORD color;
D3DXVECTOR2 tex;
};
const DWORD MyVertex_FVF = (D3DFVF_XYZRHW|D3DFVF_DIFFUSE|D3DFVF_TEX1); //...
MyVertex* vertices;
hr = vb->Lock(0,0,(BYTE**)&vertices,D3DLOCK_NOSYSLOCK);
CHECKHR(hr);
//...
vertices[0].tex = D3DXVECTOR2(0,0);
vertices[1].tex = D3DXVECTOR2(1,0);
vertices[2].tex = D3DXVECTOR2(1,1);
vertices[3].tex = D3DXVECTOR2(0,1);
//...
vb->Unlock();
Innanzitutto cerchiamo di capire COSA sono le coordinate texture. Già il termine “coordinate” ci fa capire che si tratta di un VETTORE che indica un punto, il fatto che ho usato il plurale ci dice che ce ne sono più di una: esse rappresentano i vertici del rettangolo (o più in generale del poligono) che sono usati per riempire un triangolo (in questo caso un quadrato, formato da due triangoli) con la texture. Se andando a modificare l’esempio provate a cambiare i valori delle coordinate texture vedrete come cambierà il modo in cui essa è mappata a schermo.
L’unità di misura delle coordinate texture NON è il pixel come si potrebbe pensare, e tutto ciò ha perfettamente senso. Immaginate che io abbia sviluppato il mio giochino, con un suo set di textures ad una certa risoluzione. Due giorni dopo che l’ho terminato esce la GeForce X con X appartenente all’insieme dei numeri naturali, che ha un paio di Giga di memoria ram. Che facciamo, lasciamo tutto così com’è?Assolutamente no, prendiamo il nostro grafico, lo mettiamo sotto torchio e gli diciamo di rifare tutte le texture quadruplicandone la dimensione. Secondo voi noi programmatori, pigri come siamo ce ne andiamo in mezzo ai sorgenti o quant’altro a cambiare le coordinate texture a mano? Assolutamente no, allora facciamo una cosa molto intelligente, invece che usare il pixel come unità di misura usiamo la “percentuale”. Se una texture ha dimensione 256x256 pixel e voglio beccare il punto (128, 128) al centro della texture, in coordinate “percentuali” ci basta fare 128/256=0.5. Ora se noi raddoppiamo, triplichiamo, dimezziamo le dimensioni della texture, quel punto avrà SEMPRE coordinata (0.5, 0.5). Il termine coordinata “percentuale” è stato inventato dal sottoscritto solo per chiarirne la funzione, in gergo queste si chiamano COORDINATE UV. U e V sono i nomi convenzionali degli assi dello spazio UV (in cui appunto vivono queste coordinate). Invece di chiamarli X e Y li chiamiamo U e V giusto per distinguere i due spazi e non fare confusione. In coordinate UV è possibile fare anche cose più simpatiche, per esempio potremmo fare in modo che invece di mappare la nostra texture per intero così com’è sul device di rendering vogliamo farla mappare in “tiling”, cioè affiancando diverse copie della texture.Ecco un esempio:
vertices[0].tex = D3DXVECTOR2(0,0) ;
vertices[1].tex = D3DXVECTOR2(2,0);
vertices[2].tex = D3DXVECTOR2(2,2);
vertices[3].tex = D3DXVECTOR2(0,2);
Animare le coordinate texture
Modificando in tempo reale le coordinate texture, si possono ottenere effetti interessanti, soprattutto se combinati con il texture blending come vedremo più avanti nel libro. Per ora cerchiamo di capire come sia possibile modificare le coordinate texture in tempo reale in maniera “efficiente”. Per efficiente ovviamente intendo SENZA effettuare il Lock/Unlock ad ogni frame che, come già spiegato, è una tecnica da usarsi solo in determinate occasioni. DirectX ci viene incontro con la possibilità di trasformare le coordinate texture attraverso una matrice. Questa matrice ovviamente potrà essere anche ottenuta combinando diverse trasformazioni. In questo modo sarà l’hardware ad effettuarle e noi saremo molto contenti. L’esempio “BasicTexCoordTransform” usa questa tecnica: crea due matrici, la matrice t1 attraverso una semplice traslazione e la matrice t2 di rotazione attorno al centro.Da notare che per quest’ultima trasformazione siamo costretti a combinare 3 trasformazioni: la prima che porta il centro di rotazione in mezzo alla texture, la seconda che ruota la texture, la terza che rimette a posto il centro di rotazione. Se avessimo usato solo la seconda trasformazione, la texture non ruoterebbe attorno al suo centro. Provate ad eliminare la riga
t2 = tmp1*t2*tmp2
per rendervene conto. Con la barra spazio nell’esempio si può scegliere una delle due trasformazioni.
L’ultima IMPORTANTISSIMA cosa che volevo mettere in risalto è che dato che le coordinate texture “vivono” in uno spazio 2D, basta una matrice 3x3 per effettuare tutte le trasformazioni elementari (rotazioni,traslazioni,scale etc.), mentre le D3DXMATRIX che usiamo sono matrici 4x4. In questo caso stiamo aggirando la problematica descritta precedentemente nel paragrafo “Geometrie 2D” e quindi per esempio per dobbiamo usare i membri _31 e _32 della matrice, D3DXMatrixTranslation(...) non funzionerebbe in questo caso.
bool OnDraw()
{
IDirect3DDevice8* lpDev = GetDevice();
D3DXMATRIX t1,t2;
D3DXMatrixIdentity(&t1);
D3DXMatrixIdentity(&t2);
//Una semplice traslazione
t1._31= wrapper->fTime*0.1f;
t1._32= wrapper->fTime*0.1f;
//Una semplice rotazione sul secondo
D3DXMATRIX tmp1,tmp2;
D3DXMatrixIdentity(&tmp1);
D3DXMatrixIdentity(&tmp2);
//Ruotiamo attorno all'asse Z (perpendicolare ad x e y)
D3DXMatrixRotationZ(&t2,wrapper->fTime);
//Centriamo la texture...
tmp1._31 = -0.5f;
tmp1._32 = -0.5f;
tmp2._31 = 0.5f;
tmp2._32 = 0.5f;
//Trasliamo prima la texture al centro,
//poi ruotiamo e poi la trasliamo in senso
//opposto
t2 = tmp1*t2*tmp2;
lpDev->SetVertexShader(MyVertex_FVF);
lpDev->SetStreamSource(0,vb,sizeof(MyVertex));
lpDev->SetTextureStageState(0,D3DTSS_TEXTURETRANSFORMFLAGS,D3DTTFF_COUNT2);
if(GetAsyncKeyState(VK_SPACE))
lpDev->SetTransform(D3DTS_TEXTURE0,&t1);
else
lpDev->SetTransform(D3DTS_TEXTURE0,&t2);
lpDev->SetTextureStageState(0,D3DTSS_COLOROP,D3DTOP_SELECTARG1);
lpDev->SetTextureStageState(0,D3DTSS_COLORARG1,D3DTA_TEXTURE);
lpDev->SetTextureStageState(1,D3DTSS_COLOROP,D3DTOP_DISABLE);
lpDev->SetTexture(0,texture);
lpDev->DrawPrimitive(D3DPT_TRIANGLEFAN,0,2);
return true;
}
Dal punto di vista più “tecnico” dobbiamo impostare il render state D3DTSS_TEXTURETRANSFORMFLAGS al valore D3DTTFF_COUNT2, che dice a Direct3D di usare la matrice di trasformazione passata con IDirect3DDevice8::SetTransform(D3DTS_TEXTUREn, &matrice). In verità non dobbiamo impostare questo render state semplicemente con SetRenderState, ma utilizzare la sua sorella SetTextureStageState() e impostare i render state specificati nel listato. Il significato di questa funzione e di questi renderstate sarà ampiamente trattato nel capitolo sul texture blending, non ha senso spiegarli ora senza aver fatto un’adeguata introduzione all’argomento. Per ora li prendiamo per buoni, poi tutto diventerà più chiaro.
Provate anche ora a fare qualche prova utilizzando e combinando diverse matrici e vedendo quali effetti si possono ottenere. Quando fate questo vi raccomando di rendere sempre queste trasformazioni dipendenti dal tempo (utilizzando wrapper->fTime o wrapper->fElapsedTime) per evitare i ben noti problemi di temporizzazione delle animazioni al computer.
In questo caso abbiamo fatto in modo da mappare a schermo la nostra texture di fianco quattro volte. Provate a modificare in questo o in altri modi l’esempio “LaMiaPrimaTexture” per capire bene cosa sta succedendo.
Surface
Una surface in generale è una griglia di pixel allocati in un qualche pool di memoria. La differenza principale con le texture è che le surface in generale servono solo per “modificare” i pixel contenuti.
Non potete utilizzare una semplice surface per texturizzare un poligono, però potete ottenere una surface da una texture, modificarla e vedrete tali modifiche nella texture vera e propria. Il metodo che permette di fare ciò è IDirect3DTexture8::GetSurfaceLevel(), da utilizzarsi come segue:
IDirect3DSurface8* surface;
texture->GetSurfaceLevel(0,&surface);
//La usiamo...
//...
surface->Release(); //La rilasciamo
Ogni texture quindi è composta da più surfaces in generale, e possiamo accedere a questi “livelli” attraverso tale metodo. Una texture non rappresenta necessariamente solo una regione rettangolare di pixel, ma può per esempio contenere diverse MIP-MAP. Una Mip-Map non è altro che una “catena” di surfaces di cui ognuna ha le dimensioni uguali alla metà dell’altra. Il Mip-Mapping (quando abilitato) permette di usare diverse “copie” della stessa texture a seconda della distanza del poligono da texturizzare rispetto alla camera, per evitare effetti di “aliasing” (scalettatura) quando il esso è molto piccolo. Approfondiremo questo concetto nel capitolo sul 3D, comunque per ora dobbiamo sapere che se utilizziamo questa tecnica, se vogliamo modificare una texture con MipMap dobbiamo farlo per OGNI livello (come i layers di un qualsiasi programma di grafica, ad esempio il Photoshop).
Ogni surface è dotata di un suo metodo IDirect3DSurface8::LockRect() che permette di accedere direttamente ai pixel contenuti e UnlockRect fa il lavoro opposto. Questo binomio si comporta esattamente come per i vertex buffer, e ovviamente il modo in cui bisogna indirizzare questi pixel DIPENDE dal D3DFORMAT specificato in fase di creazione della texture (leggere il paragrafo sui Colori del precedente capitolo). L’esempio LokkaQuelRect mostra come sia possibile scrivere direttamente i pixel della texture attraverso LockRect/UnlockRect. In verità questo esempio esegue il LockRect direttamente sulla texture, invece che chiamare GetSurfaceLevel e poi LockRect su d essa per fare prima (è la stessa cosa).
D3DLOCKED_RECT rect;
texture->LockRect(0,&rect,0,D3DLOCK_NOSYSLOCK);
D3DSURFACE_DESC desc;
texture->GetLevelDesc(0,&desc);
for(int y=0; y < desc.Height; y++)
for(int x =0; x < desc.Width; x++)
{
float t = wrapper->fTime;
float f1 = (float)x/(float)desc.Width;
float f2 = (float)y/(float)desc.Height;
D3DXCOLOR color=D3DXCOLOR(
sinf(t)*f1, //Rosso
cosf(t*2.0f)*f2, //Verde
sinf(t/3*5), //Blu
1); //Alpha
//Clamp dei valori all'intervallo [0-1]
color.r = min(max(color.r,0),1);
color.g = min(max(color.g,0),1);
color.b = min(max(color.b,0),1);
color.a = min(max(color.a,0),1);
switch(desc.Format)
{
case D3DFMT_X8R8G8B8:
case D3DFMT_R8G8B8:
{
DWORD* pixel = ((DWORD*)(((BYTE*)rect.pBits)+rect.Pitch*y+x*sizeof(DWORD)));
*pixel = color; //D3DXCOLOR ha l'overloading x
//la trasformazione in DWORD...
}
break;
case D3DFMT_R5G6B5:
{
WORD* pixel = ((WORD*)(((BYTE*)rect.pBits)+rect.Pitch*y+x*sizeof(WORD)));
*pixel =((BYTE)(color.r*31.0f)<<11)|
((BYTE)(color.g*63.0f)<<5)|
((BYTE)(color.b*31.0f));
break;
}
}
texture->UnlockRect(0);
Facendo partire l’esempio provate a cambiare la texture da 16 a 32 bit e vi accorgerete della maggiore definizione di quella a 32 bit (ovviamente se il vostro desktop sta girando a 32 bit, se no ne vedrete sempre e solo 16). L’unica cosa che andrebbe menzionata è la modalità d’indirizzamento dei pixel. In quest’esempio indirizziamo una DWORD se la texture è a 32 bit e una WORD se è a 16 bit. Per ottenere un puntatore al pixel in coordinate (x,y) prendiamo i bits ritornati da LockRect e ci posizioniamo alla “Riga” y. Il membro Pitch di D3DLOCKED_RECT indica la dimensione in BYTE di una riga della nostra surface. In teoria tale dimensione dovrebbe essere LarghezzaTexture*sizeof(DWORD) se usiamo 32 bit o LarghezzaTexture*sizeof(WORD) se usiamo i 16 bit ma NON possiamo fare affidamento su questa cosa. Per motivazioni dipendenti dall’hardware non è detto che le righe della texture siano salvate sequenzialmente all’interno della memoria video, quindi NON possiamo assumere questa costanza della dimensione di riga. Insomma, fidatevi sempre di Pitch quando dovete indirizzare i pixel di un D3DLOCKED_RECT.
I pixel all’interno di una stessa riga invece sono salvati sequenzialmente, quindi possiamo utilizzare la formula x*sizeof(DWORD) o x*sizeof(WORD) a seconda della profondità del colore.
Convertire da formati
Se avete bisogno di copiare rettangoli di pixel da una surface ad un’altra, potete usare le comodissime D3DXLoadSurfaceFromXXXX che effettuano anche operazioni di filtering, stretch e conversione dei colori. Vi consiglio di dare un’occhiata alla documentazione delle DirectX è chiarissima a riguardo.Noi useremo D3DXLoadSurfaceFromSurface in alcuni esempi.
Voto medio articolo: 4.5 Numero Voti: 4