B12. HLSL: le tecniche Textured e MultiTextured
La tecnica Textured
Le parti di codice che riguardano questa tecnica sono le seguenti:
Cominciamo con l'introdurre un nuovo tipo di dato: texture. Molto semplicemente, esso rappresenta una texture, ed è l'equivalente di Texture2D in XNA. Strettamente legato a quest'ultimo c'è sampler (dall'inglese "sample" = "campione"). Questo oggetto permette di ottenere il colore di un determinato pixel di una texture. La sua inizializzazione è alquanto complessa e prevede che gli si assegni un valore creato da texture_sampler, con l'aggiunta di alcuni parametri tra le paretesi graffe. Ecco una descrizione dei parametri://... texture ATexture;sampler TextureSampler =sampler_state { Texture = <ATexture>; Magfilter = Linear; Minfilter = Linear; Mipfilter = Linear; AddressU = Mirror; AddressV = Mirror; };//... //-------------------------------------------- //Tecnica 3 : Textured //-------------------------------------------- VertexToPixel TexturedVertexShader(float4 inPos :POSITION ,float3 inNormal:NORMAL ,float2 inTexCoords:TEXCOORD0 ) {VertexToPixel Output = (VertexToPixel )0;float4x4 ViewProjection = mul(View, Projection);float4x4 WorldViewProjection = mul(World, ViewProjection); Output.Position = mul(inPos, WorldViewProjection); Output.TextureCoords = inTexCoords; Output.LightingFactor = 1;if (LightEnabled) {float3 Normal = normalize(mul(normalize(inNormal), World)); Output.LightingFactor = saturate(dot(Normal, -LightDirection)); }return Output; }PixelToFrame TexturedPixelShader(VertexToPixel inPixel) {PixelToFrame Output = (PixelToFrame )0; Output.Color = tex2D(TextureSampler, inPixel.TextureCoords); Output.Color.rgb *= saturate(inPixel.LightingFactor + AmbientFactor);return Output; }technique Textured {pass Pass0 { VertexShader =compile vs_1_1 TexturedVertexShader(); PixelShader =compile ps_1_1 TexturedPixelShader(); } }
- Texture : determina la texture dal quale prelevare i pixel. Ogni sampler, quindi, avrà una texture unica associata
- Magfilter : determina l'operazione compiuta durante l'ingrandimento (Magnification) della texture. Infatti, le modalità di rendering e la superficie
di un modello, potrebbero richiedere che la texture da applicare sia scalata oltre le sue normali dimensioni. Specificare un valore per
magfilter equivale a scegliere quale metodo usare per calcolare i dati dell'immagine ingrandita. Ci sono quattro possibilità:
- None : nessuna tecnica usata. L'immagine viene semplicemente aumentata di dimensioni e appare, come si direbbe volgarmente, "pixelosa"
- Point : nessun cambiamento significativo da None, dato che ogni colore viene preso un punto alla volta
- Linear : viene effettuata un'interpolazione bilinare con anti-antias per estrarre i colori opportuni in modo da dare un'impressione di liscezza e levigatura anche alle linee che dovrebbero apparire spezzate
- Anisotropic : quasi simile a Linear, ma riduce la sfocatura ottenuta, aumentando i dettagli
- Minfilter : al pari di di magfilter, determina quale operazione venga compiuta durante il rimpicciolimento (Minification) dell'immagine
- Mipfilter : determina l'operazione compiuta nell'uso delle Mip Maps. Questo particolare tipo di immagini costituisce, in pratica, una versione ingrandita o ridotta della texture iniziale, ma ottimizzata al meglio prima dell'uso nel programma. In questo modo, non ci si deve calcolare la texture risultante, ma si prende la mip map corrispondente alla nuova dimensione, ottenendo prestazioni migliori e dettagli più elevati. Noi non useremo Mip Maps durante questo tutorial
- AddressU / AddressV : coordinate UV della texture. In grafica 3D si è soliti mappare una texture non con le coordinate dello
spazio 3D, ma con coordinate 2D, che non vengono dette x e y, ma U e V. Semplificando, le si potrebbe assimilare alla griglia di meridiani
e paralleli della Terra: con due sole coordinate (latitudine e longitudine), si ottiene un punto sulla superficie di una sfera. Lo stesso
viene fatto per ogni modello 3D, anche se la sua forma è complessa.
Questo parametro permette di scegliere in quale modo le coordinate vengono mappate. Le opzioni sono queste:- Wrap : letteralmente, "impacchettare". Se la superficie 3D è più grande della texture, l'immagine viene ripetuta uguale per tutto lo spazio disponibile
- Mirror : come Wrap, ma l'immagine viene ripetuta al contrario
- Clamp : la texture viene ripetuta una volta sola, e i suoi bordi vengono stirati fino a coprire tutta la superficie
- Border : la texture viene ripetuta una volta sola, e il resto della superficie rimane nera
Esempi di Wrap e Mirror
Il vertex shader è praticamente uguale a quello della tecnica ColoredPlus, con un'unica eccezione: c'è un parametro in più (la coordinate texture).
Il pixel shader presenta anch'esso una sola differenza col precedente:
Output.Color = tex2D(TextureSampler, inPixel.TextureCoords);Questa linea è molto semplice: la funzione tex2D non fa altro che prelevare il colore del pixel che si trova alle coordinate inPixel.TextureCoords della texture contenuta nel sampler TextureSampler.
La tecnica MultiTextured
Questa tecnica è abbastanza complessa, molto più delle precedenti. Ecco il suo codice:
Analizziamo il codice un pezzo alla volta:struct MultiVertexToPixel {float4 Position :POSITION ;float4 Color :COLOR0 ;float3 Normal :TEXCOORD0 ;float2 TextureCoords :TEXCOORD1 ;float4 LightDirection :TEXCOORD2 ;float4 TextureWeights :TEXCOORD3 ;float Depth :TEXCOORD4 ; };struct MultiPixelToFrame {float4 Color :COLOR0 ; };Texture Texture0;Texture Texture1;Texture Texture2;Texture Texture3;sampler TextureSampler0 =sampler_state { Texture = <Texture0>; Magfilter = Linear; Minfilter = Linear; Mipfilter = Linear; AddressU = Wrap; AddressV = Wrap; };sampler TextureSampler1 =sampler_state { Texture = <Texture1>; Magfilter = Linear; Minfilter = Linear; Mipfilter = Linear; AddressU = Wrap; AddressV = Wrap; };sampler TextureSampler2 =sampler_state { Texture = <Texture2>; Magfilter = Linear; Minfilter = Linear; Mipfilter = Linear; AddressU = Mirror; AddressV = Mirror; };sampler TextureSampler3 =sampler_state { Texture = <Texture3>; Magfilter = Linear; Minfilter = Linear; Mipfilter = Linear; AddressU = Mirror; AddressV = Mirror; };MultiVertexToPixel MultiTexturedVertexShader(float4 inPos :POSITION ,float3 inNormal:NORMAL ,float4 inTexCoords:TEXCOORD0 ,float4 inTexWeights:TEXCOORD1 ) {MultiVertexToPixel Output = (MultiVertexToPixel )0;float4x4 ViewProjection = mul(View, Projection);float4x4 WorldViewProjection = mul(World, ViewProjection); Output.Position = mul(inPos, WorldViewProjection); Output.Normal = mul(normalize(inNormal), World); Output.TextureCoords = inTexCoords; Output.LightDirection.xyz = -LightDirection; Output.LightDirection.w = 1; Output.TextureWeights = inTexWeights; Output.Depth = Output.Position.z / Output.Position.w;return Output; }MultiPixelToFrame MultiTexturedPixelShader(MultiVertexToPixel inPixel) {MultiPixelToFrame Output = (MultiPixelToFrame )0;float lightingFactor = 1;if (LightEnabled) lightingFactor = saturate(saturate(dot(inPixel.Normal, inPixel.LightDirection)) + AmbientFactor);float blendDistance = 0.99f;float blendWidth = 0.005f;float blendFactor = clamp((inPixel.Depth - blendDistance) / blendWidth, 0, 1);float4 farColor; farColor = tex2D(TextureSampler0, inPixel.TextureCoords) * inPixel.TextureWeights.x; farColor += tex2D(TextureSampler1, inPixel.TextureCoords) * inPixel.TextureWeights.y; farColor += tex2D(TextureSampler2, inPixel.TextureCoords) * inPixel.TextureWeights.z; farColor += tex2D(TextureSampler3, inPixel.TextureCoords) * inPixel.TextureWeights.w;float4 nearColor;float2 nearTextureCoords = inPixel.TextureCoords * 3; nearColor = tex2D(TextureSampler0, nearTextureCoords) * inPixel.TextureWeights.x; nearColor += tex2D(TextureSampler1, nearTextureCoords) * inPixel.TextureWeights.y; nearColor += tex2D(TextureSampler2, nearTextureCoords) * inPixel.TextureWeights.z; nearColor += tex2D(TextureSampler3, nearTextureCoords) * inPixel.TextureWeights.w; Output.Color = lerp(nearColor, farColor, blendFactor); Output.Color *= lightingFactor;return Output; }technique MultiTextured {pass Pass0 { VertexShader =compile vs_2_0 MultiTexturedVertexShader(); PixelShader =compile ps_2_0 MultiTexturedPixelShader(); } }
Abbiamo definito una nuova struttura perchè i dati di cui abbiamo bisogno per questa tecnica sono diversi. In aggiunta ai precedenti Position, Color e TextureCoords, ci sono i nuovi Normal, LightDirection, TextureWeights e Depth. Tra i membri figurano anche due variabili che non ci saremmo aspettati, ossia Normal e LightDirection, poiché essi venivano usati direttamente dal vertex shader per calcolare LightingFactor. I motivi che mi hanno spinto a operare questa scelta sono essenzialmente due: per prima cosa, non avevo voglia di cambiare il sorgente che avevo trovato (XD); e poi, vorrei mostrare che non si è obbligati a tenere sempre le stesse rigide regole e a seguirle stoicamente senza magari capirle. Il calcolo del LightingFactor può essere effettuato dovunque: sia nel vertex che nel pixel shader. Inoltre, le variabili di una struttura possono essere modificate, tolte o aggiunte a piacimento, a seconda delle necessità.struct MultiVertexToPixel {float4 Position :POSITION ;float4 Color :COLOR0 ;float3 Normal :TEXCOORD0 ;float2 TextureCoords :TEXCOORD1 ;float4 LightDirection :TEXCOORD2 ;float4 TextureWeights :TEXCOORD3 ;float Depth :TEXCOORD4 ; };struct MultiPixelToFrame {float4 Color :COLOR0 ; };
Passiamo ora a spiegare gli ultimi due membri. TextureWeights rappresenta il peso di ciascuna texture: come ricorderete, il peso (e quindi la trasparenza) di ogni texture varia con l'altezza del triangolo considerato. Depth rappresenta la profondità, ossia la distanza tra la telecamera e il punto osservato, e verrà calcolato in seguito facendo uso della misteriosa quarta componente della posizione. Notate che tutti i membri, tranne i primi due, sono dichiarati con la stessa funzione (TEXCOORD), a indice crescente. La keyword NORMAL vale solo quando la si riferisce a un parametro di funzione.
Questo lungo pezzo di sorgente non fa altro che dichiarare quattro texture e associarvi quattro sampler. Come potrete intuire, Texture0 rappresenta la sabbia (sand), Texture1 l'erba (grass), Texture2 le rocce (rock) e Texture3 la neve (snow). Niente di nuovo da aggiungere. Alcuni sampler sono inizializzati con UV su Wrap, altri su Mirror, ma vi accorgerete che non fa nessuna differenza (nel nostro caso), poiché le texture usate sono abbastanza uniformi.Texture Texture0;Texture Texture1;Texture Texture2;Texture Texture3;sampler TextureSampler0 =sampler_state { Texture = <Texture0>; Magfilter = Linear; Minfilter = Linear; Mipfilter = Linear; AddressU = Wrap; AddressV = Wrap; };sampler TextureSampler1 =sampler_state { Texture = <Texture1>; Magfilter = Linear; Minfilter = Linear; Mipfilter = Linear; AddressU = Wrap; AddressV = Wrap; };sampler TextureSampler2 =sampler_state { Texture = <Texture2>; Magfilter = Linear; Minfilter = Linear; Mipfilter = Linear; AddressU = Mirror; AddressV = Mirror; };sampler TextureSampler3 =sampler_state { Texture = <Texture3>; Magfilter = Linear; Minfilter = Linear; Mipfilter = Linear; AddressU = Mirror; AddressV = Mirror; };
Il vertex shader non fa altro che passare gli argomenti ricevuti al pixel shader, aggiungendo qualche piccola elaborazione. Per procedere è necessario introdurre una chiarificazione sulla misteriosa coordinata w. In hlsl, quando si opera con una posizione, non si usano tre dimensioni, ma sempre quattro, dove la quarta - w - si assume per semplicità che valga 1. Questo sistema di coordinate individua ciò che viene chiamato spazio omogeneo. Questo artificio risulta utile soprattutto per i calcoli che bisogna svolgere con le matrici, e rende possibile e più semplice l'applicazione di svariate trasformazioni. Quando si opera con gli shader (ossia il vertex e il pixel shader), lo spazio cambia, poiché solo ciò che viene visto dalla telecamera raggiungere questo stadio di elaborazione. A questo punto, tutti gli oggetti visibili sono posti all'interno di un cubo immaginario che ha il suo vertice inferiore sinistro nel punto (-1, -1, 0) e il suo vertice superiore destro nel punto (1, 1, 1). Questo cubo, che alle volte può anche essere deformato, rappresenta il mondo 3D ottenuto dai calcoli compiuti con la matrice di proiezione. Per individuare un punto al suo interno, quindi, occorre avere dei valori compresi tra -1 e 1 (per x e y) o tra 0 e 1 (per z). Comme accennato in precedenza, si ottengono queste coordinate dividendo x, y e z per il quarto incomodo, w: tramite tale passaggio, si ottengono le coordinate normalizzate per il device grafico (la parola "normalizzata", in questo contesto ha ancora un significato differente!). Notate bene che, poiché tutti gli oggetti sono inclusi nello stesso cubo, e chi guarda è sempre dietro di esso, la distanza tra un punto e la telecamera non è altro che la coordinata z del punto.MultiVertexToPixel MultiTexturedVertexShader(float4 inPos :POSITION ,float3 inNormal:NORMAL ,float4 inTexCoords:TEXCOORD0 ,float4 inTexWeights:TEXCOORD1 ) {MultiVertexToPixel Output = (MultiVertexToPixel )0;float4x4 ViewProjection = mul(View, Projection);float4x4 WorldViewProjection = mul(World, ViewProjection); Output.Position = mul(inPos, WorldViewProjection); Output.Normal = mul(normalize(inNormal), World); Output.TextureCoords = inTexCoords; Output.LightDirection.xyz = -LightDirection; Output.LightDirection.w = 1; Output.TextureWeights = inTexWeights; Output.Depth = Output.Position.z / Output.Position.w;return Output; }
Le linee:Output.LightDirection.xyz = -LightDirection; Output.LightDirection.w = 1;
Servono a ottenere il vettore opposto a LightDirection. Quando si usano dei float4 con funzione di Position, è possibile richiamare le proprietà x, y, z e w o una qualsiasi combinazione di queste lettere per ottenere il loro valore, esattamente come si faceva con Color nella lezione scorsa. Dopo aver invertito il vettore, si avrà anche un w diverso: impostandolo a 1, si corregge questo difetto, poiché come già detto per lo spazio omogeneo, si assume che w valga sempre 1 (notare che LightDirection e Normal sono coordinate 3D dello spazio omogeneo e non vengono divise per w). Invece, la riga:Output.Depth = Output.Position.z / Output.Position.w;
Ottiene la profondità come numero compreso tra 0 e 1. Assumendo semplicemente che la coordinata z sia la distanza dalla telecamera, basta dividere per w per ottenere la z normalizzata per il device.
La prima parte del codice calcola il LightingFactor, nello stesso modo in cui eravamo abituati prima. Il codice successivo, invece, risulta più arduo. L'ottimizzazione che si introduce a questo punto può essere così riassunta: le texture più lontane vengono disegnate con meno dettagli (ossia ripetute meno volte), mentre quelle più vicine possiedono più dettagli (ossia vengono ripetute più volte). Per prima cosa si imposta la distanza al di là della quale, la texture verrà diminuita di qualità (blendDistance). Poi si determina arbitrariamente la lunghezza della sfumatura, ossia quanto spazio impiegherà la texture a passare dal dettaglio elevato al dettaglio basso (blendWidth). Quindi si calcola il fattore di sfumatura (blendFactor): esso sarà 0 per tutti i triangoli la cui distanza dalla telecamera è minore di 0.99 (in coordinate normalizzate per il device), 1 per quelli compresi tra 0.995 e 1 e un valore variabile per quelli tra 0.99 e 0.995 (ossia nella fascia di sfumatura).MultiPixelToFrame MultiTexturedPixelShader(MultiVertexToPixel inPixel) {MultiPixelToFrame Output = (MultiPixelToFrame )0;float lightingFactor = 1;if (LightEnabled) lightingFactor = saturate(saturate(dot(inPixel.Normal, inPixel.LightDirection)) + AmbientFactor);float blendDistance = 0.99f;float blendWidth = 0.005f;float blendFactor = clamp((inPixel.Depth - blendDistance) / blendWidth, 0, 1);float4 farColor; farColor = tex2D(TextureSampler0, inPixel.TextureCoords) * inPixel.TextureWeights.x; farColor += tex2D(TextureSampler1, inPixel.TextureCoords) * inPixel.TextureWeights.y; farColor += tex2D(TextureSampler2, inPixel.TextureCoords) * inPixel.TextureWeights.z; farColor += tex2D(TextureSampler3, inPixel.TextureCoords) * inPixel.TextureWeights.w;float4 nearColor;float2 nearTextureCoords = inPixel.TextureCoords * 3; nearColor = tex2D(TextureSampler0, nearTextureCoords) * inPixel.TextureWeights.x; nearColor += tex2D(TextureSampler1, nearTextureCoords) * inPixel.TextureWeights.y; nearColor += tex2D(TextureSampler2, nearTextureCoords) * inPixel.TextureWeights.z; nearColor += tex2D(TextureSampler3, nearTextureCoords) * inPixel.TextureWeights.w; Output.Color = lerp(nearColor, farColor, blendFactor); Output.Color *= lightingFactor;return Output; }
Dopo aver fatto questi calcoletti, si trova il colore del pixel nel caso sia lontano (farColor), prendendo il colore da ogni texture e moltiplicandolo per il suo peso: questa operazione è quella che ci permetteva di sfumare gradualmente le texture (da sabbia ad erba, da erba a roccia, eccetera...). Successivamente si calcola il colore nel caso il punto sia vicino, usando una precisione tripla, ma sempre con lo stesso procedimento. Infine, si calcola il colore risultante con la funzione lerp. Tale funzione esegue una interpolazione lineare (Linear Interpolation) tra il primo parametro e il secondo, con un fattore pari al terzo parametro. In pratica, potremmo pensarla in questo modo: ammettiamo di eseguire una interpolazione lineare tra il blu e l'arancio con un fattore 0.2. Disegniamo una striscia che da una parte abbia il blu, e dall'altra l'arancio, riempiendola con una sfumatura graduale dei colori intermedi:Da 0 a 1, tutti i colori possibili
Ora, il colore che corrisponde al numero 0.2 nella figura sarà il risultato ottenuto dalla funzione lerp(blu, arancio, 0.2). Allo stesso modo, in questo caso otterremo una texture a dettagli elevati se il punto si trova tra 0 e 0.99; una texture intermdia, ottenuta con lerp, quando il punto sta tra 0.99 e 0.995; una texture a dettagli bassi se il punto si trova tra 0.995 e 1.
The Totem's Lair - Copyright (C) 2009
È vietata la riproduzione sia totale che parziale del sito.



