Tworzenie gry w C# z użyciem silnika Ogre - cz.7

21.07.2011 - Mateusz Osowski
TrudnośćTrudność

Program teksturujący

Drugi fragment-program jaki napiszemy będzie potrafił teksturować siatki:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
struct FragmentIn
{
  float2 TexCoord : TEXCOORD0;
};
 
 
 
struct FragmentOut
{
  float4 Color : COLOR;
};
 
 
 
FragmentOut fragment_func(FragmentIn In,
  uniform sampler2D DiffuseMap : TEXUNIT0
  ) 
{
  FragmentOut Out;                      
  Out.Color = tex2D(DiffuseMap, In.TexCoord);
  return Out;
}
JustTextureFP.cg


Jak widać, program konstrukcją przypomina poprzedni. Funkcja główna jednak zyskała drugi argument, jest nim zmienna oznaczona atrybutem uniform. Tego typu zmienne są pochodzenia zewnętrznego, tj. są przekazywane przez silnik. Do semantyki TEXUNIT0 przekazywana jest pierwsza użyta w przebiegu tekstura. Zwyczajne tekstury są w CG obsługiwane przez typ sampler2D. Ten program robi użytek z otrzymywanych współrzędnych mapowania tekstury - po prostu wydobywa odpowiedni teksel z tekstury za pomocą funkcji tex2D().
Pierwszym argumentem funkcji tex2D() jest tekstura, z której odczytany ma być kolor, drugim zaś dwie współrzędne z przedziału [0,1].

Aby program zadziałał, musimy dostarczyć w przebiegu teksturę:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
material Vase/TEXFACE/vase.jpg
{
  technique
  {
    pass
    {
      fragment_program_ref FragmentProgram/JustTexture
      {
      }
 
      texture_unit
      {
        texture vase.jpg
      }
    }
  }
}
Vase.material


W bardzo łatwy sposób możemy dokonywać rozmaitych operacji na kolorach, np. zwiększyć kontrast tekstury dwukrotnie:

1
2
3
4
FragmentOut Out;                
float4 middle = float4(0.5);
Out.Color = middle + (tex2D(DiffuseMap, In.TexCoord) - middle) * 2;
return Out;
JustTextureFP.cg




Vertex program

Dotychczas przetwarzaniem wierzchołków zajmował się fixed pipeline. Aby znalazły się one w naszej gestii, musimy użyć vertex programu:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
struct VertexIn
{
        Position : POSITION;
        TexCoord : TEXCOORD0;
};
 
 
 
struct VertexOut
{
        Position : POSITION;
        TexCoord : TEXCOORD0;
}
 
 
 
VertexOut vertex_func(VertexIn In,
        uniform mat4x4 WorldViewProj
        )
{
        VertexOut Out;
        Out.Position = mul(WorldViewProj, In.Position);
        Out.TexCoord = In.TexCoord;
        return Out;
}
BasicVP.cg


Program cieniujący dla wierzchołków jest w istocie funkcją zwracającą pozycję wierzchołka w przestrzeni ekranu, zatem jego najprostszy wariant po prostu przepuszcza wejściową pozycję wierzchołka (jest to pozycja względem środka modelu) przez konkatenację macierzy świata (modelu), widoku i projekcji (przekształcenia modelu, kamery i rzutowanie na ekran). Ponieważ przejęliśmy kontrolę nad przetwarzaniem wierzchołków, musimy sami przekazywać dane potrzebne fragment-programowi, w tym przypadku współrzędne mapowania tekstury. Program wierzchołków rzadko kiedy jest bardziej rozbudowany. Często jednak warto przerzucić do niego kosztowne obliczenia z fragment-programu, jeżeli tylko jest taka możliwość.

Większość zmiennych typu uniform przekazuje się poprzez pliki materiałów. W przypadku zmiennej WorldViewProj:

1
2
3
4
5
6
7
8
...
 
vertex_program_ref VertexProgram/SimpleLight
{
  param_named_auto WorldViewProj worldviewproj_matrix
}
 
...
materiał


Silnik Ogre sam zadba o to, by przekazywać do zmiennej WorldViewProj aktualną macierz. Opis wszystkich zmiennych silnika dostępnych dla shaderów znajduje się pod adresem http://www.ogre3d.org/docs/manual/manual_23.html#SEC125.

Oświetlenie

Potrafimy już pisać programy cieniujące dla wierzchołków i fragmentów. Możemy więc rozpocząć implementację własnego oświetlenia. Na początku stworzymy bardzo proste shadery nieuwzględniające tekstur:

Wejście i wyjście vertex-programu:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct VertexIn
{
  float4 Position : POSITION;
  float3 Normal : NORMAL;
};
 
 
 
struct VertexOut
{
  float4 Position : POSITION;
  float3 LightDir : TEXCOORD1;
  float3 Normal : TEXCOORD2;
};
SimpleLightVP.cg


Do obliczenia oświetlenia potrzebujemy wektora normalnego. Dostarcza nam go Ogre pod semantykę NORMAL. Pozycję światła przekażemy jako zmienną uniform. Chcemy, aby pozycja światła była podana w przestrzeni obiektu (takiej, w której obiekt ma współrzędne (0,0,0)), by nie trzeba było przekształcać jej dodatkowymi macierzami.
Do fragment-programu przesyłamy obliczony wektor kierunku światła (znormalizowany wektor kierunku od światła do wierzchołka), a także wektor normalny wierzchołka.

1
2
3
4
5
6
7
8
9
10
11
VertexOut vertex_func(VertexIn In,
  uniform mat4x4 WorldViewProj,
  uniform float4 LightPosition
  )
{
  VertexOut Out;
  Out.Position = mul(WorldViewProj, In.Position);       
  Out.LightDir = normalize(LightPosition.xyz -  (In.Position * LightPosition.w));
  Out.Normal = In.Normal;
  return Out;
}
SimpleLightVP.cg


Podobnie jak w poprzednim vertex-programie, przekształcamy pozycję wierzchołka za pomocą macierzy. Następnie obliczamy wektor światła dla tego wierzchołka. Korzystamy z dostępnej wbudowanej funkcji normalize. Zmienna LightPosition jest oznaczona jako uniform, jej wartość uaktualniać będzie silnik. Dla świateł punktowych jest to (x,y,z,1), gdzie wartości (x,y,z) są współrzędnymi światła, zaś dla świateł kierunkowych (-x,-y,-z,0), gdzie (x,y,z) jest wektorem kierunku światła. Kierunek padania światła kierunkowego jest niezależny od pozycji wierzchołka, toteż w tym przypadku pomijamy pozycję wierzchołka zerując ją mnożąc przez LightPosition.w. Przejdźmy do fragment-programu.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
struct FragmentIn
{
  float2 LightDir : TEXCOORD1;
  float3 Normal : TEXCOORD2;
};
 
 
 
struct FragmentOut
{
  float4 Color : COLOR;
};
 
 
 
FragmentOut fragment_func(FragmentIn In) 
{
  FragmentOut Out;
  float4 orange = float4(1,0.5,0,1);
  Out.Color = orange * dot(In.Normal, In.LightDir);
  return Out;
}
SimpleLightFP.cg


Fragment-program jest prosty - oblicza natężenie oświetlenia fragmentu - iloczyn skalarany wektora normalnego i wektora światła.

Musimy pamiętać, by przekazać dane światła do zmiennej uniform w plikach materiału. Zaaplikujmy programy cieniujące na postaci:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
material Material
{
  receive_shadows on
  technique
  {
    pass
    {
      vertex_program_ref VertexProgram/SimpleLight
      {
        param_named_auto WorldViewProj worldviewproj_matrix
        param_named_auto LightPosition light_position_object_space 0 
      }
 
      fragment_program_ref FragmentProgram/SimpleLight
      {
      }
    }
  }
}
Man.material


Liczba 0 przy zmiennej LightPosition oznacza numer światła. W tej prostej wersji obsługujemy tylko jedno światło, lecz gdy będziemy chcieli iterować po kilku światłach na przebieg, wtedy możemy pozycję każdego z nich zapisać do osobnej zmiennej. Indeks jest względny w zależności od parametrów iterowania.



5
Twoja ocena: Brak Ocena: 5 (1 ocena)

Copyright © 2008-2010 Wrocławski Portal Informatyczny

design: rafalpolito.com