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

21.07.2011 - Mateusz Osowski
TrudnośćTrudność

Oświetlenie Blinna-Phonga

Poprawki kamery

Zanim zaczniemy majstrować przy oświetleniu musimy dokonać usprawnień kamery. Do tej pory bardzo brzydko przenikała przez ściany planszy. Da się temu zaradzić - Newton ma możliwość sprawdzania kolizji promieni - wypuścimy więc promień z głowy postaci do pożądanej pozycji kamery i w przypadku kolizji wyciągniemy informację o miejscu jej wystąpienia.

Przygotujemy sobie klasę do rzucania promieni, która będzie pozwalała nam odfiltrować wybrane obiekty, np. wyzwalacze przestrzenne, które nie powinny blokować kamery:

1
2
3
4
5
6
7
8
public class PredicateRaycast : Raycast
{
  public class ContactInfo
  {
    public float Distance;
    public Body Body;
    public Vector3 Normal;
  }
Raycaster.cs


W obiektach podklasy ConctactInfo będziemy przchowywać informacje o pojedynczych kolizjach promienia.

1
2
3
4
5
6
7
8
9
10
11
12
13
  public Predicate<Body> Predicate;
  public List<ContactInfo> Contacts;
 
  public PredicateRaycast(Predicate<Body> pred)
  {
    Predicate = pred;
    Contacts = new List<ContactInfo>();
  }
 
  public override bool UserPreFilterCallback(Body body)
  {
    return Predicate(body);
  }
Raycaster.cs


Za filtrowanie ciał będzie odpowiadać predykat ustalny przy konstruowaniu promienia. Metoda UsePreFilterCallback() jest wywoływana w momencie natrafienia przez promień prostopadłościanu otaczającego ciało.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
  public void SortContacts()
  {
    Contacts.Sort((a, b) => a.Distance.CompareTo(b.Distance));
  }
 
  public override bool UserCallback(Body body, float distance, Vector3 normal, int collisionID)
  {
    ContactInfo contact = new ContactInfo();
    contact.Distance = distance;
    contact.Body = body;
    contact.Normal = normal;
    Contacts.Add(contact);
    return true;
  }
}
Raycaster.cs


Nie jest określone w jakiej kolejności Newton będzie sprawdzać ciała, więc musimy sami zadbać o sortowanie kontaktów w kolejności. Metoda UserCallback() jest wywoływana w momencie natrafienia przez promień ciała i uprzednim zaakceptowaniu przez UserPreFilterCallback().

Klasa kamery

W klasie kamery wprowadzane zmiany nie będą duże. Dopisujemy pole:

1
public Vector3 InterPosition;
GameCamera.cs


Będzie ono przechowywać porządaną pozycję kamery po interpolacji.

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
26
public void Update()
{
  Vector3 offset =
  Character.Node.Orientation * (-Vector3.UNIT_Z +
    (Vector3.UNIT_Y * (float)System.Math.Tan(Angle.ValueRadians))
    ).NormalisedCopy * Distance;
 
  Vector3 head = Character.Node.Position + Character.Profile.HeadOffset;
  Vector3 desiredPosition = head + offset;
 
  InterPosition += (desiredPosition - InterPosition) * 0.1f;
 
  PredicateRaycast raycast = new PredicateRaycast((b => !(b.UserData is TriggerVolume)));
  raycast.Go(Engine.Singleton.NewtonWorld, head, InterPosition);
  if (raycast.Contacts.Count != 0)
  {
    raycast.SortContacts();
    Engine.Singleton.Camera.Position = head 
      + (InterPosition - head) * raycast.Contacts[0].Distance
      + raycast.Contacts[0].Normal * 0.15f;
  }
  else
    Engine.Singleton.Camera.Position = InterPosition;
 
  Engine.Singleton.Camera.LookAt(head);
}
GameCamera.cs


Najpierw obliczamy porządaną pozycję po interpolacji (wiersz 11), następnie tworzymy promień pomijający wyzwalacze (wiersz 13), wypuszczamy go z głowy do pożądanej pozycji (wiersz 14), w przypadku wystąpienia kolizji ustawiamy kamerę w miejscu wystąpienia kolizji przesuniętym o 15 cm wzdłuż wektora normalnego kolidującej płaszczyzny, ponieważ kamera posiada określony minimalną odległość widzenia (near clip plane).

Programy cieniujące

Chcemy stworzyć użyteczny model oświetlenia, który będzie w stanie obsłużyć wiele świateł. Nie obejdzie się więc bez renderowania wielobrzebiegowego. Aby nie komplikować problemu, posłużymy się dodawaniem przebiegów. Ponieważ będziemy dodawać wartości kolorów do poprzednich przebiegów, musimy przygotować sobie punkt wyjściowy - wyrenderować geometrię modelu na czarno. W przeciwnym przypadku oświetlenie modelu dodałoby się do tła sceny. Można by to uznać za pożądane, gdy chcielibyśmy stworzyć postać ducha:



Potrzebujemy więc napisać proste programy cieniujące tworzące czarny podkład pod oświetlenie. Pozostawimy sobie pewną swobodę, kolor podkładu uzależnimy od koloru tła sceny (ambientu).

Ambient: Vertex-program

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
struct VertexIn
{
  float4 Position : POSITION;   
};
 
struct VertexOut
{
  float4 Position : POSITION;
};
 
VertexOut vertex_func(VertexIn In,
  uniform mat4x4 WorldViewProj
  )
{
  VertexOut Out;
  Out.Position = mul(WorldViewProj, In.Position);       
  return Out;
}
AmbientVP.cg


Najprostszy vertex-program.

Ambient: Fragment-program

1
2
3
4
5
6
7
8
9
10
11
struct FragmentOut
{
        float4 Color : COLOR;
};
 
FragmentOut fragment_func(uniform float4 Ambient)
{
        FragmentOut Out;
        Out.Color = Ambient;
        return Out;
}
AmbientVP.cg


Pobieramy zmienną Ambient jako ambient_light_colour z Ogre.

Te programy będą wykorzystane w pierwszym przebiegu. Teraz przejdziemy do sedna. Będziemy korzystać z modelu cieniowania zwanego modelem Blinna-Phonga. Z częścią tego modelu mieliśmy już do czynienia pisząc najprostszy shader oświetlenia. Teraz dodamy światło odbite - specular - które bardzo ładnie podkreśli detale zawarte w mapach wypukłości. W modelu Blinna-Phonga kolor światła odbitego określony jest wzorem:



N jest wektorem normalnym powierzchni, a H wektorem połowy kąta patrzenia (wektor będący uśrednieniem wektora kierunku światła i wektora kierunku patrzenia). Potęga n reguluje "ostrość" odbicia. Pomimo, że dystrybucja światła tego modelu nie posiada odbicia w rzeczywistości, rezultaty są całkowicie akceptowalne.

Oświetlenie: Vertex-program

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
26
27
28
29
30
31
32
33
34
35
36
struct VertexIn
{
  float4 Position : POSITION;
  float2 TexCoord : TEXCOORD0;
  float3 Normal : NORMAL;
  float3 Tangent : TANGENT;
};
 
struct VertexOut
{
  float4 Position : POSITION;
  float2 TexCoord : TEXCOORD0;
  float3 LightDir : TEXCOORD1;
  float3 HalfAngle : TEXCOORD2;
};
 
VertexOut vertex_func(VertexIn In,
  uniform mat4x4 WorldViewProj,
  uniform float4 LightPosition,
  uniform float3 EyePosition
  )
{
  VertexOut Out;
  Out.Position = mul(WorldViewProj, In.Position);       
  Out.TexCoord = In.TexCoord;
 
  float3 lightDir = LightPosition.xyz -  (In.Position * LightPosition.w);
  float3 binormal = cross(In.Normal, In.Tangent);
  float3x3 tbnMatrix = float3x3(In.Tangent, binormal, In.Normal);
  Out.LightDir = mul(tbnMatrix, lightDir);
 
  float3 eyeDir = normalize(EyePosition - In.Position.xyz);
  Out.HalfAngle = mul(tbnMatrix, normalize(eyeDir + lightDir));
 
  return Out;
}
BumpVP.cg


Kod wygląda podobnie do kodu programu SimpleBump. Dodatkowo liczymy wspomniany wektor HalfAngle i przenosimy go do przestrzeni stycznej.

Oświetlenie: Fragment-program

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
26
27
28
29
30
31
32
33
34
35
36
37
38
struct FragmentIn
{
  float2 TexCoord : TEXCOORD0;
  float3 LightDir : TEXCOORD1;  
  float3 HalfAngle : TEXCOORD2; 
};
 
struct FragmentOut
{
  float4 Color : COLOR;
};
 
FragmentOut fragment_func(FragmentIn In,
  uniform sampler2D DiffuseMap : TEXUNIT0,
  uniform sampler2D NormalMap : TEXUNIT1,
  uniform sampler2D SpecularMap : TEXUNIT2,
  uniform float4 LightAttenuation,
  uniform float4 LightColour
  ) 
{
  FragmentOut Out;
  float3 normal = (tex2D(NormalMap, In.TexCoord) - 0.5) * 2.0;  
  
  float lightDist = length(In.LightDir);
  float attenuation = 1.0 / (LightAttenuation.y 
    + LightAttenuation.z * lightDist 
    + LightAttenuation.w * lightDist * lightDist
    );
  
  Out.Color = tex2D(DiffuseMap, In.TexCoord) 
    * saturate(dot(normal, normalize(In.LightDir))) * attenuation;
  Out.Color *= LightColour;     
 
  float specular = saturate(pow(dot(In.HalfAngle, normal), 16));
  Out.Color += tex2D(SpecularMap, In.TexCoord) 
    * LightColour * specular * attenuation;     
  return Out;
}
BumpFP.cg


Pierwszą znaczącym ulepszniem, które wprowadzamy jest wygasanie światła (attenuation). Określone jest przez trzy parametry: constant, linear i quadratic. Mamy pełną dowolność w kalibracji tych współczynników. Najczęściej dobierane są tak, by wartość wygasania na granicy zasięgu światła była bliska zeru. Parametry te będzie przekazywać silnik, a ustalać je będziemy mogli za pomocą metody obiektu światła w kodzie gry.

Obliczamy współczynnik światła rozproszonego zgodnie z modelem Blinna-Phonga. Funkcja saturate() przycina wartość zmiennej do przedziału [0,1]. Tekstura SpecularMap pozwoli nam kontrolować odbijanie światła przez różne materiały.

Oświetlenie: Szablon materiału

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
material Templates/Bump
{
  technique
  {
    pass
    {
                        lighting off
                        vertex_program_ref VertexProgram/Ambient
      {
      param_named_auto WorldViewProj worldviewproj_matrix
      }
      fragment_program_ref FragmentProgram/Ambient
      {
        param_named_auto Ambient ambient_light_colour
      }
    }
 
    pass
    {
      iteration once_per_light
      scene_blend add
 
      vertex_program_ref VertexProgram/Bump
      {
        param_named_auto WorldViewProj worldviewproj_matrix
        param_named_auto LightPosition light_position_object_space 0
        param_named_auto EyePosition camera_position_object_space
      }
      fragment_program_ref FragmentProgram/Bump
      {
        param_named_auto LightAttenuation light_attenuation 0
        param_named_auto LightColour light_diffuse_colour 0
      }
      texture_unit DiffuseMap
      {
        texture empty
      }
      texture_unit NormalMap
      {
        texture empty
      }
      texture_unit SpecularMap
      {
        texture empty
      }
    }
  }
}
Templates.material


Pierwszy przebieg renderuje geometrię bez oświetlenia, kolejne mogą ją tylko rozjaśniać. Korzystając z tego szablonu musimy pamiętać o ustaleniu tekstury SpecularMap.

Testowy świat

Dla celów testowych zmieńmy świat. Potrzebujemy mapki z wygenerowanymi wektorami stycznymi i wszystkimi potrzebnymi teksturami. Ponieważ nowych zasobów jest dużo, w paczce znajduje się cała aplikacja wraz ze wszystkimi zaktualizowanymi zasobami.

Całość aplikacji i zasobów

Aktualny kod źródłowy



Usunięte ze sceny zostały wazy i dodane zostały światła w miejscach świecących kryształów. Model cieniowania został zmieniony na cienie wolumetryczne sumujące się - Ogre po prostu zamaskuje zacienione obszary zaopobiegając wykonywaniu fragment-programu dla nich.

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
26
27
static void Main(string[] args)
{
  // ustalamy kolor geometrii tła (nieoświetlonej)
  Engine.Singleton.SceneManager.AmbientLight = new ColourValue(0,0,0);
  
  ...
  
  CreateLight(new ColourValue(0.2f, 0.8f, 1.0f), new Vector3(11.1f, 1.18f, -4.8f));
  CreateLight(new ColourValue(0.2f, 0.8f, 1.0f), new Vector3(13.1f, -0.44f, -12.68f));
  CreateLight(new ColourValue(0.2f, 0.8f, 1.0f), new Vector3(9.34f, 1.57f, -20.61f));
  CreateLight(new ColourValue(0.2f, 0.8f, 1.0f), new Vector3(22.85f, -0.44f, -10.15f));
  
  CreateLight(new ColourValue(1f, 0.8f, 0.55f), new Vector3(0, 0, 0));
        
        Engine.Singleton.SceneManager.ShadowTechnique = ShadowTechnique.SHADOWTYPE_STENCIL_ADDITIVE;
  
  ...
}
 
static void CreateLight(ColourValue colour, Vector3 pos)
{
  Light crystalLight = Engine.Singleton.SceneManager.CreateLight();
  crystalLight.Type = Light.LightTypes.LT_POINT;
  crystalLight.Position = pos;
  crystalLight.DiffuseColour = colour;
  crystalLight.SetAttenuation(32, 1.0f, 0.14f, 0.07f);  
}
Program.cs








Udało nam się uzyskać dużą poprawę w stosunku do poprzedniej szaty graficznej. Oczywiście wciąż wiele zostało do zrobienia, przykładowo miękkie cienie teksturowe, które wymagają szerszego omówienia. Jednakże poprawa jakości grafiki ze strony programistycznej pociąga za sobą konieczność spędzenia dodatkowego czasu nad jej plastyczną stroną. Aby bowiem cieszyć się zaawansowanymi efektami musimy dostarczyć mapy wektorów normalnych i światła odbitego. Czy warto?
5
Twoja ocena: Brak Ocena: 5 (1 ocena)

Copyright © 2008-2010 Wrocławski Portal Informatyczny

design: rafalpolito.com