Tutorials

XNA Shader Programmering, Tutorial 2

Nivå : 200

XNA Shader Programmering

Tutorial 2 av Petri T. Wilhelmsen aka. digitalerr0r
www.gamecamp.no

 

I forrige artikkel snakket vi litt om hvordan en kunne implementere shadere i XNA, og touchet såvidt borti shader programmering. I dag skal jeg gå gjennom en annen lys algoritme, som bygger på Ambient Litghting som vi sist gikk gjennom.

Jeg skal ikke fokusere på hvordan en skal hente og bruke shadere via XNA nå, så om du ikke kan dette, ta en titt på XNA Shader Programming - Tutorial 1 før du går videre med denne.

Ambient lighting er ikke akuratt eye-candy, og objektene ser heller ikke veldig 3D ut. Men, det er en viktig del i mange sammenhenger, og kan se pent ut da en blander det med noe annet. Derfor skal vi her se på Diffuse Lighting, som kan gjøre scenene dine mye mer pene og dynamiske!

Føt vi starter:

For å forstå denne tutorialen her burde du ha noe kjennskap til regning med vektorer og matriser, og vært igjennom Tutorial 1 av denne serien.

Kildekode: de_ShaderTutorial_2.zip

 

 

Diffuse Lighting

Vi skal nå se på noe som heter Diffuse Lighting! Algoritmen bygger på ambient lighting algoritmen fra Tutorial 1:

I = Aintensity * Acolor

Som vi ser, så er dette et lys som ikke har noen retning, den bare er der og gir et "lys grunnlag" til alle objekter. Diffuse lighting er vel en av de enkleste lys-algortimene som støtter at lyset har en retning.

Diffuse lighting algoritmen er litt tyngre, sett fra et matematiskt perspektiv, enn ambient lighting, men det er ikke noe å være redd for. Dette er egentlig veldig lett! Selv om jeg ikke er en matte-lærer av noe slag, skal jeg gjøre mitt beste på å forklare alt som står bak denne algoritmen vi nå skal se på.

For å kalkulere diffuse lighting, må vi bruke to vektorer, en vektor N som er normalen til en flate, og en vektor L som er retningen på lyset. Disse to vektorene bestemmer hvor "mye" lys flaten skal reflektere. Er L paralell med N, vil den reflektere mest, og er L paralell med flaten, vil den reflektere minst. Vi kan si at vinkelen mellom L og N er med på å bestemme hvor mye flaten skal reflektere lys!

For å beregne dette kan vi bruke en lov fra vektorverdenen som heter Dot-produktet, eller skalar produktet. Denne regelen blir brukt til å bl.a finne vinkelen mellom to vektorer, og kan defineres slik: Gitt to vektorer N og L har vi

N.L = |N| x |L| x cos(a)      der |N| er lengden av vektor N, |L| er lengden av vektor L og cos(a) er vinkelen mellom vektorene.

Dette kan vi bruke i formelen vår. Men vi skal også bruke to hjelpevariabler: Dintensity og Dcolor. Dintesity skal bestemme hvor kraftig Diffuse Lightingen skal være, og Dcolor bestemmer fargen på lyset.

Da har vi gått igjennom de forskjellie komponentene ved Diffuse Lighting, og kan dermed se på hele formelen.

 

Diffuse Lighting algoritmen:

I = Aintensity x Acolor + Dintensity x Dcolor x N.L

Vi ser at vi fortsatt har tatt vare på Ambient Lighting formelen. Dette er fordi vi fortsatt vil at et basislys i bunn av alt, slik at alle delene av en modell kan i hvertfall ha litt lys. Så plusser vi på intensitiviteten, fargen og Dot-produktet mellom N og L for å beregne Diffuse Lighting.

 

 

Implementere shaderen

Da er det på tide å programmere selve shaderen, og få noen resultater ut på skjermen!


Denne tutorialen baserer seg på Tutorial 1, så du kan gjerne åpne solution filen for forrige tutorial, og bygge den videre slik at den støtter Diffuse Lighting! Hvis ikke, må du lage et nytt prosjekt og skrive koden for å initialisere og hente inn shaderen vi nå skal lage.

 

Vi starter med å definere noen globale variabler, som skal bli satt til shaderen gjennom applikasjonen:

float4x4    matWorldViewProj;
float4x4    matInverseWorld;
float4      vLightDirection;

Her har vi et par nye variabler: matInverseWorld og vLightDirection. matInverseWorld blir beregnet i applikasjonen ved å invertere matrisen til World matrisen, og brukes til å regne ut en korrekt normal. vLightDirection blir brukt til å sette retningen på lyset vårt!

 

OUT er en struktur som vår Vertex shader skal returnere. Denne inneholder fortsatt posisjonen på vertexen, men i tillegg vil vi ha ut retningen på lyset L, og normalen til vertexen N. Disse blir definert som TEXCOORD0 og TEXCOORD1, som sier i hvilket register på skjermkortet vi vil lagre denne informasjonen.TEXCOORDn blir ofte brukt til å lagre litt ymse data, der n er større eller lik 0, og mindre eller lik antallet støttet av skjermkortet du programmerer mot( som oftest et sted mellom 0 og 8 ).

struct OUT 
{ 
    float4 Pos: POSITION; 
    float3 L:    TEXCOORD0; 
    float3 N:    TEXCOORD1; 
}; 

 

Så definerer vi selve funkjsonen for Vertex Shaderen, og som vi ser, skal den returnere OUT. Vertex shaderen tar inn to parametere som er posisjonen på- og normalen N til vertexen som passerer gjennom shaderen:

OUT VertexShader( float4 Pos: POSITION, float3 N: NORMAL ) 
{ 
    OUT Out = (OUT) 0; 
    Out.Pos = mul(Pos, matWorldViewProj); 
    Out.L = normalize(vLightDirection); 
    Out.N = normalize(mul(matInverseWorld, N)); 
    return Out; 
} 

La oss dele opp koden over i små biter:

  1. "Out.Pos = mul(Pos, matWorldViewProj);" transformerer vertexposisjonen med matWorldViewProj, slik at den havner rett sted på skjermen.
  2. "Out.L = normalize(vLightDirection);" normaliserer vektoren vLightDirection.
  3. "Out.N = normalize(mul(matInverseWorld, N));" multipliserer matrisen matInverseWorld med vektoren N, og normaliserer resultatet.

normalize(x) er en funksjon definert av HLSL, der normalize returnerer en normalisert versjon av vektoren x.

 

PixelShader skal også denne gangen bare returnere fargen på pixelen som passerer gjennom shaderen, men tar nå inn to nye verdier: L og N. L er da retningen på lyset til den gjeldende pixelen, beregnet av VertexShader, og N er normalen til Pixelen, som også beregnes av VertexShader.

Vi starter med å regne ut Ambient Lighting, så Diffuse Lighting:

float4 PixelShader(float3 L: TEXCOORD0, float3 N: TEXCOORD1) : COLOR 
{ 
    // ambient lighting
    float Ai = 0.8f; 
    float4 Ac = float4(0.075, 0.075, 0.2, 1.0); 

// diffuse lighting float Di = 1.0f; float4 Dc = float4(1.0, 1.0, 1.0, 1.0); return Ai * Ac + Di * Dc * saturate(dot(L, N)); }

Her ser vi noe nytt: saturate(dot(L, N))

saturate(x) er en funksjon definert av HLSL språket, der x kan være et skalar, en vektor eller en matrise. Funksjonen "klemmer" verdien x til å befinne seg i et område mellom 0 og 1.

Dette betyr at vi her bergener Dot-produktet til L og N, også endre resultatet slik at det havner på en skala mellom 0 og 1 ved å passere resultatet inni saturate funksjonen.

 

"return Ai * Ac + Di * Dc * saturate(dot(L, N));" bør nå ikke se helt ukjent ut?

I = Aintensity x Acolor + Dintensity x Dcolor x N.L

 

Til slutt definerer vi teknikken DiffuseLight, med et pass P0. Denne kan vi da kalle fra koden for å bruke den shaderen vi nå har laget :)

technique DiffuseLight 
{ 
    pass P0 
    { 
        VertexShader = compile vs_1_1 VertexShader(); 
        PixelShader = compile ps_1_1 PixelShader(); 
    } 
}

 

Hele koden for shaderen skal nå se slik ut:

float4x4    matWorldViewProj;
float4x4    matInverseWorld;
float4      vLightDirection;

struct OUT
{
    float4 Pos: POSITION;
    float3 L:    TEXCOORD0;
    float3 N:    TEXCOORD1;
};

OUT VertexShader( float4 Pos: POSITION, float3 N: NORMAL )
{
    OUT Out = (OUT) 0;
    Out.Pos = mul(Pos, matWorldViewProj);
    
    Out.L = normalize(vLightDirection);
    Out.N = normalize(mul(matInverseWorld, N));
    
    return Out;
}

float4 PixelShader(float3 L: TEXCOORD0, float3 N: TEXCOORD1) : COLOR
{
    float Ai = 0.8f;
    float4 Ac = float4(0.075, 0.075, 0.2, 1.0);
    float Di = 1.0f;
    float4 Dc = float4(1.0, 1.0, 1.0, 1.0);
    
    return Ai * Ac + Di * Dc * saturate(dot(L, N));
}

technique DiffuseLight
{
    pass P0
    {
        VertexShader = compile vs_1_1 VertexShader();
        PixelShader = compile ps_1_1 PixelShader();
    }
}

 

Bruke shaderen i applikasjonen vår

Da shaderen er ferdig, er vi klare til å bruke denne i applikasjonene/spillene vi lager. Jeg skal ikke gå gjennom hele prosessen her, fordi dette ble gjort i XNA Shader Programmering - Tutorial 1, men jeg skal gå gjennom det som er forskjellig fra Tutorial 1.

 

Det nye her, er egentlig bare det som ligger i Render funksjonen vår. Vi må jo sette teknikken vår effekt skal bruke:

effect.CurrentTechnique = effect.Techniques["DiffuseLight"];

Der DiffuseLight er navnet på teknikken vi lagde.

 

Neste steg er å sende inn de forskjellige parameterverdiene som shaderen krever for å fungere:

worldMatrix = bones[mesh.ParentBone.Index] * renderMatrix;

Matrix worldInverse = Matrix.Invert(worldMatrix);
Vector4 vLightDirection = new Vector4(0.0f, 0.0f, 1.0f, 1.0f);
effect.Parameters["matWorldViewProj"].SetValue(worldMatrix * viewMatrix * projMatrix);
effect.Parameters["matInverseWorld"].SetValue(worldInverse);
effect.Parameters["vLightDirection"].SetValue(vLightDirection);
 

worldInverse blir beregnet av en funksjon Invert som finnes i Matrix klassen. Denne funksjonen brukes til å invertere en matrise. Vi ser her at worldInverse er det samme som den inverse av worldMatrix. Vi sender så worldInverse til matInverseWorld i shaderen.

Så definerer vi en vektor som skal fortelle shaderen hvilke retning lyset har( husker du vektoren L som vi bruker i vertex shaderen? Dette er denne).

 

Dette er det som er nytt i Render funksjonen. Kjører du applikasjonen nå, vil du se det samme objektet som i Tutorial 1, men med en litt penere lys-setting! Nå ser det faktisk 3D ut, eller hva? ;)

 

Oppgaver
1. Lek deg gjerne med de forskjellige verdiene i shaderen, slik at du forstår hvilken innvirkning de forskjellige komponenetene har for lys-settingen!
2. Gjør det mulig å sette Diffuse fargen via en parameter til shaderen( som en global variabel ).
3. Lag to effekter i shaderen, en som bare er Ambient Lighting( se tutorial 1 ) og en som er Diffuse Lighting, slik at du selv kan velge hvilken lys-algoritme du vil bruke da du tegner objekter.

 

 

Da er vi ferdige med Tutorial 2!

Som vi ser er det ikke veldig avansert å lage lys som ser ganske bra ut, eller hva?
Nestegang skal jeg ta for meg Specular Lighting, som igjen bygger på Diffuse Lighting algoritmen vi har skrevet idag!

 

Spørsmål? ta kontakt via petriw@gmail.com eller via forumet på www.gamecamp.no

Comments

 

Stack said:

Jeg har problemer med negative verdier i vLightDirection variabelen. Dersom jeg bruker for eksempel [0, 5, 3] så fungerer det helt fint, men dersom jeg prøver [0, 5, -3] så fungerer det ikke som det skall.. Ser kun ut til å fungere med positive verdier? kan ikke få rett lys på andre siden av objektet mitt.

juni 2, 2008 12:23

About digitalerr0r

My name is Petri Wilhelmsen and I live in Norway. I love programming and GameDev, and have been doing that for many years now. I mostly code in C/C++ and C#, using DirectX and OpenGL. Feel free to ask me if you have any questions :)