Tutorials

XNA Shader Programmering, Tutorial 1

Nivå : 200

XNA Shader Programmering

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

 


Velkommen til XNA Shader Programming tutorial serien! Mitt navn er Petri Wilhelmsen og jeg er et medlem av gruppen Dark Codex, som bl.a deltar på The Gathering konkuransene. Dette er min første tutorial, så feedback hadde vært veldig bra!

Denne tutorial serien vil ta deg med i gjennom mange temaer innenfor shadere, samt hvordan en kan bruke dem til å lage kul grafikk og funksjonalitet ved hjelp av GPUen din. Vi starter med noe teori (ja, jeg veit det er kjedelig, men jeg skal prøve å være rask ), og går deretter over til praktisk shader programmering hvor jeg vil introdusere bl.a. noen lys algoritmer, normal mapping, parallax mapping og mye annet.
Den første delen av tutorial serien vil ta for seg teori biten, en shader som transformerer et objekt og en en algoritme for Ambient lighting. Deretter vil jeg ta for meg mer avanserte shadere.
Teori-delen er ikke veldig detaljert, men er skrevet for at du skal få deg en kort og enkel oversikt over de viktige emnene ved shader programmering. Jeg vil forklare emner nærmere da vi faktisk får bruk for dem, så fortvil ikke om du ikke helt henger med gjennom teorien!

Den pratiske delen går steg for steg i gjennom hvordan du skal lage et prosjekt, kode shaderen, implementere og bruke den i XNA. Dette er på ingen måte den beste metoden for å gjøre dette på, men skal være forholdsvis lett å følge.

Forkunnskaper: Noe programmering i XNA, slik at du forstår hvordan du kan rendre ut et 3D objekt, og sette opp view og projection matriser, samt utføre regne-operasjoner med matriser. Jeg forklarer litt om dette i denne tutorialen, men ikke nok til at dere som ikke har vært borti dette før, kan forstå hva som skjer.

Kildekoden til tutorial 1 kan du finne her: Tutorial1, Kildekode

En historie om Shaders

Før DirectX8 kom hadde GPU programmererne en fast måte å transformere pixels og vertexes på, noe som heter ”Fixed pipeline”. Dette gjorde at utviklerene ikke hadde noen mulighet for å endre hvordan disse skulle bli tranformert på, noe som gjorde at grafikken på f.eks DirectX 7.0 spillene kunne bli ganske like hverandre. DirectX 8.0 introduserte Vertex og Pixel shadere, som var en metode utviklere kunne bruke for å selv bestemme og endre hvordan vertex og pixels skulle bli tranformert, noe som ga utrolig mye fleksibilitet.

Utviklerene brukte da et assembly liknende språk for å skrive shadere, noe som var vanskelig og tungvindt, og det var kun støtte for Shader Model 1.0. Dette ble forbedret da DirectX 9.0 kom, hvor vi ble introdusert til High Level Shader Language (HLSL), som erstattet assembly-programmeringen med noe som liknet mer på C-programmering. Dette gjorde shadere mye enklere å lage, lese og lære.

Nå nylig ble DirectX 10.0 sluppet ut, som støtter Shader Model 4.0, samt introduserer en ny shader: Geometry Shader. Ulempen er at DirectX 10.0 krever et ganske nytt skjermkort, og fungerer kun i Windows Vista.

XNA støtter kun Shader Model 1.0 – 3.0, men fungerer da på Windows XP, Vista og Xbox 360!

Ok, hva er shaders?

Etter å ha pratet litt rundt historien om shaders, er det kanskje på tide å fortelle litt om hva shadere egentlig er? Som nevnt kan du bruke shaders til å endre måten vertexer og pixler blir tranformert og prosessert på. Vertex shaders tranformerer vertexer, og pixel shaders transformerer pixler. I figuren under ser vi hvordan et program, vertex shader og en pixel shader jobber sammen for å produsere et bilde i frame bufferen( som da tilslutt skal vises på skjermen ).

 

En ting som er viktig å huske på er at mange skjermkort ikke støtter de nyeste funksjonene i en shader. Dette bør en da ta hensyn til da en utvikler shadere, slik at en kan lage alternative metoder for eldre skjermkort, og la de nye bruke andre metoder.

Vertex Shaders
Vertex shader brukes da til å manipulere vertex-data, per vertex. Dette kan f.eks være en shader som skal gjøre objekter “fetere” under rendering, ved å flytte vertexene fra sin orginale posisjon til en ny posisjon langs vertexen sin normal, noe som gjøres for hver vertex på de objektene som blir tegned ved bruk av shaderen.

Vertex shaderen tar input fra en vertex struktur definert i f.eks koden, og loader dette da inn I shaderen fra vertex bufferen du sender til shadere. Dette kan f.eks være Position, Color, Normal og Tangent. Jeg vil introdusere de forskjellige bitene i en senere tutorial.
Vertex shaderen kan gi output for senere bruk i f.eks pixel shaderen, og fungerer enten ved at du definerer en struktur i shaderen og får vertex shader funksjonen til å returnere denne, eller ved å definere parametere med out syntaksen. Output kan f.eks være Position, Fog, Color og Texture coordinates.

struct VS_OUTPUT
{
    float4 Pos: POSITION;
};
VS_OUTPUT VS( float4 Pos: POSITION )
{
    VS_OUTPUT Out = (VS_OUTPUT) 0;
    ...
    return Out;
}
// eller..
float3 VS(out float2 tex : TEXCOORD0) : POSITION { tex = float2(1.0, 1.0); return float3(0.0, 1.0, 0.0); }
 

Pixel Shaders
Pixel shaderen manipulerer pixelene til et objekt som blir sendt I gjennom shaderen. Dette kan f.eks være et objekt vi ønsker å beregne en per pixel lys algoritme på. Pixel shaderen henter data fra vertex shaderns output verdier, som position, normals, color og texture coordinates. Input til pixel shaderen kan f.eks være posisjon til pixelen( x,y ), color, texture coordinates osv.

float4 PS(float vPos : VPOS, float2 tex : TEXCOORD0) : COLOR
{
      ...
      return float4(1.0f, 0.3f, 0.7f, 1.0f);
}

Pixel shaders kan bare ha to verdier som output, og det er Color og Depth. Color forteller hvilken farge pixelen har, og depth er en ting vi kommer tilbake til litt senere.

 

HLSL

High Level Shading Language brukes til å programmere shadere. Ved hjelp av dette språket kan en definere variabler, funksjoner, datatyper, tester( if/else/for/do/while..) og mye annet, samt lage logikk for hvordan pixel og vertex shaderne skal fungere.
Under har vi noen nøkkelord definert av HLSL, dette er selvfølgelig ikke alle men det viktigste er at du er klar over at det finnes mye å velge i. Da jeg introduserer nye datatyper( ol. ) senere I tutorialen, vil jeg forklare disse grundig!


Eksempler på lovlige datatyper i HSLS
bool true eller false
int 32-bit heltall
half 16bit heltall
float 32bit desimaltall
double 64bit desimaltall


Eksempler på lovlige vektorer i HSLS
float3 vektorTest
float vektorTest[3]
vector vektorTest
float2 vektorTest
bool3 vectorTest


Eksempler på lovlige matriser i HSLS
float3x3 en 3x3 matrise av type float
float2x2 en 2x2 matrise av type float

 

Vi har også en rekke med funksjoner som kan brukes I HLSL til bl.a regne operasjoner.

cos( x ) Returnerer cosnius til x
sin( x) Returnerer sinus til x
cross( a, b ) Returnerer kryssproduktet av to vektor a og b
dot( a,b ) Returnerer dotproduktet av to vektorer a og b
normalize( v ) Returnerer en normalisert vektor av v ( v / lengden av v )


For en komplett liste, gå til http://msdn2.microsoft.com/en-us/library/bb509611.aspx


HLSL tilbyr en rekke med funkjonaliteter for å gjøre shader utviklingen enklere, og det er nettopp dette vi skal lære oss i denne tutorialen, så mer om dette temaet etter teori delen.

 


Effect filer

Effect filene ( .fx ) gjør bruken av HLSL enklere, og du kan samle det meste av shaderkoden i en .fx fil! Filen inneholder f.eks globale variabler, funksjoner, strukturer ( f.eks Out ), Vertex shaderen, Pixel shaderen, en eller flere teknikker med en eller flere pass, shadere, teksturer mm.
Vi har tidligere sett kort på hvordan vi kan definere stukturer, variabler og funksjoner i en shader, og jeg vil nå forklare alt dette med teknikker og pass. En shader kan inneholde en eller flere teknikker. Hver teknikk har et unikt navn, og vi kan fra applikasjonen bestemme hvilken teknikk vi vil bruke ved å sette CurrentTechnique egenskapen til Effect.

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


Her sier vi at effect( som er en ferdig lastet shader ) skal bruke en teknikk med navn AmbientLight. En teknikk kan ha en eller flere pass, og vi må huske å prosessere alle passene til en teknikk om vi vil ha det resultatet vi ønsker.

Dette er et eksempler på en shader som har en teknikk og et pass:

technique Shader
{
    pass P0
    {
        VertexShader = compile vs_1_1 VS();
        PixelShader = compile ps_1_1 PS();
    }
}

 

Dette er et eksempler på en shader som har en teknikk og to pass:

technique Shader
{
    pass P0
    {
        VertexShader = compile vs_1_1 VS();
        PixelShader = compile ps_1_1 PS();
    }
pass P1
    {
        VertexShader = compile vs_1_1 VS_Other();
        PixelShader = compile ps_1_1 PS_Other();
    }
}

 

Dette er et eksempler på en shader som har to teknikker:

technique Shader_11
{
    pass P0
    {
        VertexShader = compile vs_1_1 VS();
        PixelShader = compile ps_1_1 PS();
    }
}

technique Shader_2a
{
    pass P0
    {
        VertexShader = compile vs_1_1 VS2();
        PixelShader = compile ps_2_a PS2();
    }
}

Vi ser her at shaderen har to teknikker, den ene bruker f.eks Shader Model 1, mens den andre er for nyere skjemkort med støtte for Pixel Shader 2. Disse to teknikkene kan f.eks gjøre det samme, men ved bruk av forskjellig kode slik at det passer for all hardware!

Vi kan også sende inn teksturer til shadere, noe vi vil komme tilbake til senere!

Implementering av Shadere I XNA

Heldigvis er det veldig enkelt å bruke shaderei XNA. Dette gjøres faktisk med veldig få linjer kode. Under har vi en liten liste vi kan følge for å implementere shadere:
1. Programmer en shader
2. Legg shaderfilen( .fx ) i f.eks “Contents”
3. Lag en instanse av Effect klassen
4. Initier denne instansen
5. Velg hvilken teknikk du vil bruke i shaderen
6. Start shaderen
7. Sett parametere til shaderen
8. Tegn objetet du vil skal bruke shaderen
9. Stopp shaderen

1.For å kode en shader kan vi bruke f.eks notepad, visual studio eller 3. parts programmer. Jeg bruker nVidia FX Composer 2.0 som kan lastes ned gratis: http://developer.nvidia.com/object/fx_composer_home.html

2. Da shaderen er laget, kan du kopiere filen inn i prosjektet, slik at den får et asset name:
 

3. XNA Framework har en Effect klasse, som kan brukes til å laste inn effekter og kompilere dem. For å lage en instanse av Effekt klassen gjør du slik:

Effect effect;


Effect er en del av “Microsoft.Xna.Framework.Graphics”, så husk å inkludere denne i prosjektet:

using Microsoft.Xna.Framework.Graphics

4. For å initiere shaderen kan vi bruke noe av funksjonaliteten Content gir oss. Vi kan loade shaderen fra prosjektet, eller fra en fil.

effect = Content.Load<Effect>("Shader");

Her er "Shader" asset navnet og effect er en instanse av class Effect.

5. Valg av teknikk er like enkelt som alt annet, bare meld i fra til effect at du vil bruke en teknikk med gitt navn, f.eks AmbientLight.

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

Techniques gir deg tilgang til alle teknikkene i effect, og du kan sende inn en key for å hente ut en spesifikk teknikk. Dette kan gjøres ved at du tester hva slags støtte det er for shadere i GPUen, og bruke resulatet til å velge den rette shaderen for de rette grafikk-kortene.

6. For å starte å bruke en Effect, kaller du bare Begin() funksjonen:

effect.Begin();

7. Mange shadere har parametere, og det finnes forskjellige metoder å hente/sette de på. Vi vil i denne tutorialen kun bruke en enkel måte for å gjøre det litt lettere å følge med. Andre metoder vil bli brukt senere!
Dette er ikke den raskeste måten å sette parametere på, men den fungerer bra:

effect.Parameters["matWorldViewProj"].SetValue(
worldMatrix * viewMatrix * projMatrix);


"matWorldViewProj" er definert i shaderen: float4x4 matWorldViewProj; og worldMatrix * viewMatrix * projMatrix er en matrise.

SetValue setter en verdi til parameteren og sender den til shaderen, mens GetValue<Type> henter en verdi fra parameteren, der Type er datatypen du vil hente ut. GetValueInt32() henter et heltall fra shaderen.

8. Tegning av objekter kan bli gjort på mange forskjellige måter. I mitt eksempel, tegner jeg ut et 3D objekt hentet fra en .X fil ved å gå gjennom hver mesh og meshpart i .X filen.

9. For å stoppe shaderen, eller effekten, kaller du End() metoden til Effect:

effect.End();

Da er vi i gjennom de forskjellige stegene for å implementere og bruke en shader i XNA, og vi er dermed ferdig med den teoretiske delen av tutorialen! Gratulerer, du har kommet langt.

Ambient Lighting Algoritmen

Alle objekter har som oftest et slags globalt basiskt minimums lys, et slags lys som bare eksisterer i rommet og treffer alle objekter fra alle kanter. Det er dette vi kaller for Ambient Lighting, og skal implementere nå!

Ambient lighting er ikke noe spesielt spennende, men kan være et godt hjelpemiddel for at ikke alle objekter som ikke blir truffet av andre ”lysstråler ”skal være 100% mørke.

Formelen for Ambient lighting er:

I = Aintensity x Acolor ( 1.1)

Der I står for lys intensitiviteten, Aintensity står for intensitiviteten for Ambient lyset, og Acolor står for fargen på Ambient lyset.

Det er relativt enkelt å implementere denne lys algoritmen, og er derfor en god start på tutorialen J.

 
Dette er hva vi skal lage i dag!

Programmere selve effekten
Det kan være lurt å laste ned sourcekoden, slik at du har tilgang til 3D objektene og en kildekode som fungerer, og har en referanse om du skulle ha problemer med stegne under.
Start med å lage et nytt prosjekt i VS2005 eller VC# Express og velg Windows Game( 2.0 ) templaten under XNA Game Studio

 

Velg et ordentlig navn på prosjektet, og trykk OK.

Trykk F5 for å se om prosjektet kjører og at alt er greit.

Høyreklikk på Content, eller på den folderen du vil ha effekt filene dine liggende, velg Add->New Item. Da vil et vindu komme fram, der du skal velge ”Effect File”. Gi den navnet  Shader.fx .

 

Da vil Shader.fx komme fram under Contents. Dobbeltklikk på Shader.fx, og et default effekt dokument vil bli åpnet. Slett alt innholdet i denne, slik at vi kan begynne fra blanke ark!

For å lage en shader må vi være klar over hva den skal gjøre. For det første, skal vi ha en Vertex Shader som skal transformere alle vertexene basert på matWorldViewProj ( slik at objektet blir vist der vi vil at den skal bli vist, matWorldViewProj er en variabel som vi setter utenfor shaderen ). Så skal vi ha en pixel shader som vil gi ambient lighting til pixlene i objektet.

Vi starter med å definere en matrise som skal inneholde World*View*Projection matrisene( De som definerer hvor objektet står, hvor du ser og hvordan du ser):

float4x4 matWorldViewProj;
 

Deretter trenger vi en struktur som definerer hva Vertex shaderen skal returnere av data.

struct OUT
{
    float4 Pos: POSITION;
};
 

Her ser vi at vi lager en struktur med navnet OUT, som inneholder en variabel av type float4 med navn Pos, der ”:POSITION” forteller noe om hvilket register en vil ha verdien i.

Register? Et ”lager” i skjermkortet som kan inneholde data. Skjermkortet har egne registre for posisjon, normaler, texcoord osv. Dette er noe du ikke trenger å tenke på i første omgang, bare vit at skal du ha posisjonen til et objekt, brus :POSITION.

 

Da vi har stukturen klar, kan vi begynne å skrive selve Vertex Shader logikken. Her henter vi World/Model view posisjonen til objektet, og transformerer den i forhold til matWorldViewProj.


Vi starter med å lage funksjonen VertexShader( float4 Pos: POSITION ) med returdata OUT, og parameteren Pos.

Deretter definere vi en instanse av OUT med navn Out, og nullstiller det slik at vi ikke har noe random data. Så setter vi Out.Pos til å være lik resultatet av mul(Pos, matWorldViewProj). mul(a,b) multipliserer matrise a med en annen matrise b, og returnerer resultatet. Da vi er ferdig med å behandle/endre den dataen vi vil, kan vi returnere Out objektet for videre behandling i render pipelinen.

OUT VertexShader( float4 Pos: POSITION )
{
    OUT Out = (OUT) 0;
    Out.Pos = mul(Pos, matWorldViewProj);
    return Out;
}
 

Deretter lager vi logikken for Pixel shaderen. Vi starter med å lage funkjsonen PixelShader() : COLOR med returtype float4. ”: COLOR” gjør så vi behandler returdata fra denne funksjonen som en COLOR verdi.

float4 PixelShader() : COLOR
{
    float Ai = 0.8f;
    float4 Ac = float4(0.075, 0.075, 0.2, 1.0);
    
    return Ai * Ac;
}
 

Innholdet til pixelshaderen er ganske rett fram, vi definerer intensitiviteten til A i float Ai = 0.8f; og fargen til A i Ac = float4(0.075, 0.075, 0.2, 1.0);. Ac tilsvarer en ganske mørk lilla farge. Da variabelene er definert, returnerer vi Ai * Ac slik at vi får den fargen og intensitiviteten på ambient lyset vi vil ha. Legg merke til at vi returnerer en verdi som følger formelen for Ambient Lighting( 1.1 ). Ai brukes til å velge ”styrken” på fargen, mens Ac bestemmer hvilken farge objektet skal få.

Fordi vi lager en effektfil, må vi ha en teknikk med minst et pass. Denne kodebiten gjør ikke mye annet enn å definere en teknikk med navnet AmbientLight, som inneholder et pass P0. Deretter setter vi at denne effektfilens VertexShader ligger i funksjonen VertexShader(), og PixelShaderen ligger i PixelShader(). compile vs_1_1 og compile ps_1_1 kompilerer koden i shaderen med støtte for vertex shader 1.1 og pixel shader 1.1.

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

Da har vi endelig fått skrevet shaderen vår, og mangler da bare å implementere denne til XNA og bruke den! Jeg kommer ikke til å forklare hvordan matriser og hvordan tegning av et 3D objekt fungerer i detalj, så hvis du ikke forstår hva som står her, burde du lese litt om dette.

Dobbeltklikk på Game1.cs og lag en instanse av Effect med navn effect.
Skriv dette der du definerer dine variabler for klassen:

Effect effect;
 

Vi trenger også noen hjelpevariabler slik at vi kan sette opp en view, world og projection matrise:


Skriv inn dette over Effect effect; :

float width, height;
float x = 0, y = 0;
float zHeight = 15.0f;
float moveObject = 0;
 

Så kan vi definere matrisene våre:

Matrix renderMatrix, objectMatrix, worldMatrix, viewMatrix, projMatrix;
Matrix[] bones;
 

Og 3D objektet vårt:

Model m_Model;
 

Koden skal så langt se slik ut:

float width, height;
float x = 0, y = 0;
float zHeight = 15.0f;
float moveObject = 0;

GraphicsDeviceManager graphics;
SpriteBatch spriteBatch;

// 3D Object
Model m_Model;

Effect effect;

// Matrices
Matrix renderMatrix, objectMatrix, worldMatrix, viewMatrix, projMatrix;
Matrix[] bones;
 
Så må vi nullstille modell objektet vårt i konstruktoren.
Legg til m_Model = null; i konstruktoren:
public Game1()
{
    graphics = new GraphicsDeviceManager(this);
    Content.RootDirectory = "Content";
    m_Model = null;
}
 

Så går vi videre til Initialize funksjonen, hvor vi først henter dimensjonen/oppløsningen på vinduet vårt, nullstiller worldMatrix, og setter en projection. Vi henter også effekt filen vår, ”Shader”, fra Content og legger den i effect slik:

effect = Content.Load<Effect>("Shader");

Koden for Initialize ser slik ut:

protected override void Initialize()
{
      width = graphics.GraphicsDevice.Viewport.Width;
      height = graphics.GraphicsDevice.Viewport.Height;

      // Set worldMatrix to Identity
      worldMatrix = Matrix.Identity;
            
      float aspectRatio = (float)width / (float)height;
      float FieldOfView = (float)Math.PI / 2,
                NearPlane = 1.0f, 
                FarPlane = 1000.0f;
                projMatrix = Matrix.CreatePerspectiveFieldOfView( 
FieldOfView, aspectRatio, NearPlane, FarPlane ); // Load and compile our Shader into our Effect instance. effect = Content.Load<Effect>("Shader"); // Vertex declaration for rendering our 3D model. graphics.GraphicsDevice.VertexDeclaration = new VertexDeclaration(
graphics.GraphicsDevice,
VertexPositionNormalTexture.VertexElements); base.Initialize(); }
 

Det viktigste her er VertexDeclaration.og Content.Load<Effect>("Shader"); Det er her vi bestemmer hvilke data som skal bli sendt til Vertex Shaderen! Vi ser her at vi skal sende inn Vertex, Position, Normal og Texture til shaderen, noe vi henter fra 3D objekt filen.

Da vi har fått hentet inn shaderen vår, og satt opp matrisene, kan vi gå videre til LoadContent() funksjonen. Her loader vi 3D modellen, og setter opp diverse data for 3D modellen vår.

Koden ser slik ut:

protected override void LoadContent()
{
      spriteBatch = new SpriteBatch(GraphicsDevice);

      // Load our 3D model and transform bones.
      m_Model = Content.Load<Model>("dclogo__");
      bones = new Matrix[this.m_Model.Bones.Count];
      this.m_Model.CopyAbsoluteBoneTransformsTo(bones);
}
 

Update inneholder bare kode for å kalkulere og oppdatere viewMatrix, renderMatrix og objectMatrix( for å rotere objektet vårt) .
Koden ser slik ut:

protected override void Update(GameTime gameTime)
{
      zHeight = 80;

      float m = (float)gameTime.ElapsedGameTime.Milliseconds/1000;
      moveObject += m;

      // Move our object by doing some simple matrix calculations.
      objectMatrix = Matrix.CreateRotationX( moveObject / 2 )
* Matrix.CreateRotationZ(-moveObject / 4 ); renderMatrix = Matrix.CreateScale( 0.5f ); viewMatrix = Matrix.CreateLookAt(
new Vector3(x, y, zHeight), Vector3.Zero, Vector3.Up ); renderMatrix = objectMatrix * renderMatrix; base.Update(gameTime); }
 

Da er vi kommet til siste steg i denne tutorialen, og det er Draw(..) funksjonen! Her skjer det mye i Shader verdenen.

Koden for Draw() ser slik ut:

protected override void Draw(GameTime gameTime)
{
      graphics.GraphicsDevice.Clear(Color.Black);


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

      // Begin our effect
      effect.Begin();

      foreach (EffectPass pass in effect.CurrentTechnique.Passes)
      {
          // Begin current pass
            pass.Begin();

            foreach (ModelMesh mesh in m_Model.Meshes)
            {
                foreach (ModelMeshPart part in mesh.MeshParts)
                  {
                        worldMatrix = 
bones[mesh.ParentBone.Index]*renderMatrix;
effect.Parameters["matWorldViewProj"].SetValue(
worldMatrix * viewMatrix * projMatrix); // Render our meshpart graphics.GraphicsDevice.Vertices[0].SetSource(
mesh.VertexBuffer,
part.StreamOffset,
part.VertexStride);
graphics.GraphicsDevice.Indices = mesh.IndexBuffer;
graphics.GraphicsDevice.DrawIndexedPrimitives(
PrimitiveType.TriangleList,
part.BaseVertex, 0,
part.NumVertices,
part.StartIndex,
part.PrimitiveCount); } } // Stop current pass pass.End(); } // Stop using this effect effect.End(); base.Draw(gameTime); }

Vi starter med å velge hvilken teknikk vi vil bruke, og da skal vi så klart bruke teknikken vi definerte i starten av denne delen, AmbientLight.

Deretter kaller vi effect.Begin() slik at vi begynner å bruke shaderen vår.

Så går vi i gjennom alle passene som er definert i AmbientLight teknikken( som her bare er en ). Da vi er i et pass, kan vi tegne det vi vil. Vi starter med å gå i gjennom alle delene av et 3D objekt og setter opp matrisene for den. Da matrisene er klare, kan vi sende den til shaderen, slik at objektet vil få riktig posisjon:

effect.Parameters["matWorldViewProj"].SetValue(
worldMatrix * viewMatrix * projMatrix);


Her setter vi da matrisen matWorldViewProj i shaderen, til å være worldMatrix * viewMatrix * projMatrix.

Da alle parameterene våre, her bare en, er satt, kan vi tegne ut selve 3D objektet!
Denne lille kodebiten er ikke så farlig som den ser ut til å være:

graphics.GraphicsDevice.Vertices[0].SetSource(mesh.VertexBuffer,
part.StreamOffset, part.VertexStride);
graphics.GraphicsDevice.Indices = mesh.IndexBuffer;

SetSource(..) er en funksjon som brukes til å sette kilden der vi skal hente vertex streamen fra! Funksjonen er definert slik:

public void SetSource (
            VertexBuffer vb,
            int offsetInBytes,
            int vertexStride )

 


Der vb er vertex buffer kilden, offsetInBytes er hvor fra vi vil starte streamen, og vertexStride er størrelsen på elementene i vertex bufferen.

Alt den gjør er da å hente ut vertex bufferen og index bufferen til det gjeldende 3D objekt delen, for så å bruke dem til å skrive ut alle vertexene:

graphics.GraphicsDevice.DrawIndexedPrimitives(PrimitiveType.TriangleList,
                                              part.BaseVertex, 0, 
                                              part.NumVertices,
                                              part.StartIndex, 
                                              part.PrimitiveCount);

DrawIndexedPrimitives(..) har mange parametere, men er ganske mye enklere enn den ser ut til å være:

public void DrawIndexedPrimitives (
                       PrimitiveType primitiveType,
                       int baseVertex,
                       int minVertexIndex,
                       int numVertices,
                       int startIndex,
                       int primitiveCount)
 

Parameterene til denne funksjonen kan du lese mer om her:
http://msdn2.microsoft.com/en-us/library/microsoft.xna.framework.graphics.graphicsdevice.drawindexedprimitives.aspx

Vi spesifiserer at vi skal skrive ut en index buffer, ved å bruke TriangleList.

Da skal funksjonene være ferdig programmert! Du må huske å sette inn et 3D objekt som skal tegnes ved bruk av shaderen. Hele koden for tutorial 1 kan lastes ned her, ta gjerne en titt om det skulle være noen problemer J.

Phew, det var mye på en gang! Jeg håper du nå har begynt å forstå hva shadere egentlig er, og hvordan du kan implementere enkle shadere ved hjelp av XNA! Lek deg gjerne med shaderen, prøv å gi den forskjellige farger og varier Ai for å vise hvilken innvirking Ai har på Ambient Lighting algoritmen, og gjør oppgavene gitt under!

Nå som du har fått en god forståelse på hvordan du kan implementere og bruke shadere i XNA vil jeg i neste tutorial gå dypere inn på shader programmering. Tutorial 2 skal være om Diffuse Lighting algoritmen, slik at objektet vårt faktisk kan få en illusjon av 3D :P

Oppgaver

Her er et par enkle oppgaver du kan prøve deg på:

1: Endre fargen på ambient lyset slik at den er helt blå.

2: Prøv å set fargen på Ac til å være gitt av en global parameter i shaderen.
(Tips: effect.Parameters["AmbientColor"].SetValue( color );)

3: Lag en ny teknikk i samme shader file, som skal bruke samme Vertex Shader, men en ny Pixel shader. Den nye pixel shaderen skal gi en helt grønn farge til objektet.
(Tips: Lag en ny teknikk ”AmbientLight2” og bruk effect.Techniques["AmbientLight2"];)

4: Bruk Xbox kontrollen din, eller tastaturet, til å rotere objektet og/eller kameraet!

Har du noen spørsmål, så ta gjerne kontakt via petriw@gmail.com eller via www.gamecamp.no.

Takk for meg!

Comments

 

dan said:

Genialt! Tack :-)

desember 7, 2007 1:40

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 :)