Teksturowanie i Oświetlenie  Udostępnij na: Facebook

Autor: Dawid Pośliński

Opublikowano: 2011-05-06

W grach łączy się wiedzę z różnych dziedzin życia, które twórcy gier próbują zasymulować w świecie wirtualnym. Jedną z ważniejszych jest – związana bezpośrednio z tym, dlaczego widzimy – optyka. Ogólnie pojęta optyka zajmuje się opisywaniem zachowania światła. W przypadku gier wiedza z zakresu tej nauki pozwala tworzyć bardziej realistyczne sceny na ekranie monitora dzięki zastosowaniu różnorodnego oświetlenia obiektu, wyliczaniu cieni obiektów, kolorów obiektów czy w końcu odbić światła na inne obiekty znajdujące się na scenie.

Wprowadzenie do mapowania UV

Mapowanie tekstur polega na zamianie tekstury dwuwymiarowej na trójwymiarowy model. Tak jak w przypadku modeli 3D do opisania punktu wykorzystuje się trzy współrzędne (x,y,z), tak w przypadku opisywania powierzchni modelu konieczne jest zastosowanie jeszcze dwóch współrzędnych tekstury 2D. Innymi słowy, dzięki mapowaniu 3D możliwe jest nakładanie barw na poligon modelu, pochodzących z obrazka. W przypadku mapy UV konkretne piksele na obrazku przypisane są do powierzchni konkretnych poligonów, w ten sposób obrazek „rozkładany” jest na pojedyncze poligony i nakładany na odpowiedni z nich.

**Rys.1. Siatka modelu 3D.

W trakcie tworzenia modelu w programach do modelowania 3D (Blender, 3dsMax itp.) dodatkowo generuje się współrzędne UV dla każdego z verteksów tworzących model. Modeler ma możliwość rozłożenia siatki modelu i wygenerowania tekstury UV 2D, przesłanej następnie do grafika, który ją odpowiednio pomaluje. Modeler, tworząc taką teksturę może wygenerować ją tak, aby standardowo miała już jakieś oświetlenie, lub pomalować obszar, gdzie znajdują się konkretne verteksy, określonym kolorem, który pozwoli grafikowi trafniej pomalować konkretny obszar tekstury. Od grafika tekstura ponownie wróci do modelera, który sprawdzi, czy prezentuje się ona odpowiednio, i albo zawróci ją ponownie do grafika do poprawki, albo przekaże do twórców gry, aby zaczęli ją wykorzystywać razem z modelem w aplikacji.

Podsumowując, mapowanie UV sprowadza się do trzech operacji:

  1. Rozłożenia siatki modelu na płaszczyźnie:

  2. Stworzenia tekstury:

  3. Nałożenie tekstury na model:

Mapowanie tekstur w XNA

Założeniem, które przyświecało twórcom XNA, było przygotowanie frameworka tak, aby elementy najczęściej powtarzające się przy tworzeniu gier były już gotowe. Jednym z nich jest właściwe nakładanie tekstur na ładowane przez ContentManagera modele. Istotne jest, aby grafik przygotował odpowiednio model z informacjami, który fragment tekstury ma pojawić się na odpowiednim verteksie. Jako przykład zostanie teraz zaprezentowane załadowanie modelu do projektu.

  1. Klikając prawym przyciskiem myszy na PierwszaAplikacjaContent, należy wybrać Add->ExistingItem i model, który zostanie załadowany. W przykładzie tym wykorzystam wcześniej przygotowany model młyna w programie Blender3D.

  2. Klikając ponownie na PierwszaAplikacjaContent w Solution Explorerze, zaleca się stworzenie nowego folderu, korzystając z opcji New Folder, i niech będzie on nazwany „Models”. W tym folderze umieszczane będą modele 3D, które będą ładowane na scenie. Warto pamiętać o dowolnej możliwości definiowania struktury katalogów w projekcie, ułatwia to bowiem znacznie późniejszą nawigację, szczególnie gdy projekt składa się z wielu plików. Plik modelu można teraz przenieść metodą „chwyć i upuść” (drag&drop). Visual Studio automatycznie przeniesie do fizycznie stworzonego na dysku folderu Models plik z modelem.

  3. Brakuje jeszcze tekstury modelu, którą należy dodać, klikając na folder Models prawym przyciskiem myszy i ponownie wybierając Add->ExistingItem. Zawartość folderu Models wyglądać będzie następująco:

  4. Pozostaje jeszcze na końcu ciała metody LoadContent() dodać linię kodu ładującą model. Sprowadza się to do następującego kodu:

    mill = Content.Load<Model>("Models/mill");

    Oczywiście Visual Studio zgłosi błąd, ponieważ zmienna mill nie został zdefiniowana, a ponieważ chcemy mieć do niej dostęp z poziomu innych metod, nie może być ona zmienną lokalną dostępną tylko w obrębie metody LoadContent. Klikając prawym przyciskiem myszy na mill, wystarczy wybrać Generate->Property.

Właściwie to już wszystko, co jest potrzebne XNA, aby załadować model do kompilowanej aplikacji. Pozostało jeszcze narysowanie go na scenie. Warto się jednak na chwilę zatrzymać w tym miejscu i zastanowić, gdzie tutaj mapowanie tekstur? Otóż, gdy klikniemy prawym przyciskiem myszy w Solution Explorerze na plik mill.fbx zawierający ładowany model i wybierzemy Properties, pojawi się następujące okno:

Widać w nim takie informacje, jak nazwa Assetu dostępna z poziomu kodu, a ponieważ nie zawiera ona rozszerzenia, to nie jest konieczne przy wykorzystaniu metody Content.Load jej podawanie. Nazwa importera wykorzystanego do przetworzenia z programu Blender3D do zrozumiałego dla XNA formatu, czy też najważniejsza właściwość z obecnie omawianych, czyli Content Processor. Otóż XNA wykorzystuje do przetwarzania różnych obiektów odpowiednie klasy, które są uruchamiane jeszcze przed procesem właściwej kompilacji projektu. Model Content Processor wykonuje między innymi mapowanie tekstur związanych z modelem, ale również wykonuje za programistę dość istotną czynność – serializuje model we właściwej dla siebie formie tak, aby był on przygotowany wprost do wpisania do pamięci. Zatem tworząc proste aplikacje wykorzystujące standardowe Content Processory, nie trzeba pamiętać o wielu dość złożonych aplikacjach. Takie rozwiązanie ma jednak również swoje wady. Nie mamy wpływu na pełny proces przetwarzania modelu, tym samym nałożenie dodatkowych efektów, które można by zrealizować już w Content Procesorze – trzeba je przenosić do metody Draw(), co nie tylko jest mniej wydajne, ale również kod aplikacji staje się przez to mniej czytelny.

Warto o tym pamiętać, ale ponieważ są to tematy dużo bardziej zaawansowane, a zarazem rozległe, zostały tylko wspomniane w tym rozdziale jako informacja na przyszłość.

Narysujmy go!

Model jest już załadowany, pozostało już tylko go narysować. Jak to zrobić – będzie można przeczytać w rozdziale „Basic Effect i Efekty oświetleniowe”.

Różne modele oświetlenia

Wprowadzenie na scenę świateł wymusza na programiście uwzględnienie, w trakcie wyliczania kolorów kolejnych pikseli na scenie, natężenia światła padającego na ów piksel. Jeżeli jest ono bardziej intensywne i pada bardziej prostopadle do powierzchni verteksa, to jasność tego piksela jest większa. Jeżeli światło jest bardziej odbite – jasność jest mniejsza. Obrazuje tę sytuację poniższy rysunek:

W zależności od pożądanego przez twórcę aplikacji efektu, wyróżniamy kilka najczęściej używanych modeli oświetlenia. Każdy z nich różni się przede wszystkim inną koncepcją wyliczania koloru piksela na obiekcie.

Cieniowanie płaskie (ang. flatshading)

Ten model oświetlenia zakłada, że dla każdego piksela w obrębie danego verteksa światło wyliczane jest tylko raz. Oznacza to, że po wyliczeniu natężenia światła dla verteksa kolor każdego z jego pikseli jest mieszany z tą samą wartością natężenia światła. Zaletą tego modelu jest przede wszystkim szybkość, ze względu na niską złożoność obliczeń zależną jedynie od ilości verteksów.

 

Cieniowanie Gourauda

W modelu Gourauda obliczane jest natężenie światła dla każdego z wierzchołków z verteksa, natomiast piksele między wierzchołkami wyliczane są poprzez interpolacje wierzchołków. Mówiąc najprościej –  poprzez uśrednianie wartości między trzema wierzchołkami a odległością obliczanego piksela od konkretnych wierzchołków, których siłę oświetlenia znamy. Oferuje znacznie bardziej realistyczny efekt oświetlenia modelu, jednak jest bardziej skomplikowany niż Flat shading.

Cieniowanie Phonga

Model ten zwany jest również cieniowaniem z interpolacją wektora normalnego. Zakłada on obliczanie wartości każdego z pikseli poprzez obliczanie wartości natężenia światła w zależności od kąta padania światła na każdy z pikseli. W modelu tym obliczane są wektory normalne wierzchołków, a następnie poprzez interpolacje obliczane są wektory normalne „wewnątrz” verteksa. Następnie stosowana jest dowolna metoda wyliczania koloru danego piksela, ponieważ są już dostępne wszystkie niezbędne dane do wyliczenia jego wartości. Zaletą tej metody jest najlepsze odwzorowanie prawdziwego zachowania światła, jednak wadą jest największa złożoność obliczeń.

Warto wspomnieć jeszcze o tzw. Global Ilumination, które uwzględnia zarówno światło otoczenia (ambientlight) czy światło rozproszone (diffuselight), jak również odbicia obiektów tzw. specular. Jeżeli zastosowano na scenie inne modele oświetlenia, wchodzą one również w skład global illumination (mówiąc najprościej, źródłami światła dla obiektu są zarówno promienie bezpośrednie, jak i odbite od innych obiektów).

Basic Effect i efekty oświetleniowe

Omówione zostały już najważniejsze modele oświetleniowe, więc najwyższa pora zastanowić się, gdzie i jak je można wykorzystać w XNA. Rozdział ten jest kontynuacją rozdziału „Mapowanie tekstur w XNA”, w którym to załadowano model do projektu. Pozostało jeszcze go wyświetlić na scenie – wykorzystana zostanie do tego klasa BasicEffect, którą omówimy w tym rozdziale, to znaczy do czego służy i jakimi możliwościami dysponuje.

Klasę BasicEffect należy rozumieć jako predefiniowany przez twórców XNA efekt rysowania na ekranie monitora określonego obiektu. Własne efekty dają twórcy aplikacji możliwości ograniczone jedynie jego wyobraźnią. Jednak co to są w ogóle efekty w XNA? Efekty określają, jak karta graficzna ma narysować określony piksel na ekranie, w zależności od podanych zmiennych (położenia piksela, tekstur składowych itd.). Do pisania efektów wykorzystuje się język zwany HLSL, czyli High Level Shader Language, który w procesie kompilacji jest przetwarzany na zrozumiałą dla karty graficznej postać. Dla każdego modelu, a nawet mesha modelu, tj. oddzielnej bryły składowej modelu, można wykorzystać inny shadereffect. Dla przykładu, model przedstawiający rycerza może mieć nałożony na zbroję shader, w którym odbija się otoczenie, natomiast twarz rycerza będzie już matowa. Oczywiście przykłady można mnożyć w nieskończoność, a liczne gry komputerowe znakomicie korzystają z tych możliwości.

Wiadomo już zatem, że efekty muszą mieć określone parametry oraz że dla każdego mesha, z którego składa się model, można przyporządkować oddzielny efekt. Konieczne jest jeszcze określenie wzajemnego położenia między meshami, aby model nie został „rozbity” w trakcie rysowania. Wzajemne relacje między meshami, na które składa się model, to po raz kolejny zadanie dla grafika. W trakcie tworzenia modelu musi on połączyć go ze szkieletem modelu, aby wszystkie meshe były ze sobą na stałe związane. Jeśli tego nie zrobi, XNA przyjmie jako początek osi współrzędnych punkt (0,0,0), a co za tym idzie – jeżeli na model składa się więcej niż jeden mesh, rozsypią się one i wyświetlą w jednym miejscu. Jeżeli jednak model zostanie prawidłowo wykonany, w trakcie rysowania, możliwe będzie przeliczenie przesunięcia kolejnych meshy względem siebie i poprawne wyrysowanie modelu na scenie.

Podstawy teoretyczne zostały już omówione, czas na odrobinę praktyki. W metodzie Draw(), po linii:

  spriteBatch.End();

dodany zostanie następujący kod:

Matrix[] transforms = newMatrix[mill.Bones.Count];

mill.CopyAbsoluteBoneTransformsTo(transforms);

foreach (ModelMesh mesh inmill.Meshes)
            {
foreach (BasicEffect effect inmesh.Effects)
                {
effect.World = transforms[mesh.ParentBone.Index] * world;
effect.View = view;
effect.Projection = projection;
}

mesh.Draw();
            }

Co tutaj się dzieje? W pierwszej i drugiej linii stworzona zostaje macierz transformacji meshów, której rozmiar określony jest ilością kości. Następnie każdy z meshów modelu jest pobierany w pętli foreach i dla każdego z nich pobierane są efekty (może ich być więcej niż jeden). Ustawiane są wymagane parametry dla BasicEffectu, którymi są:

  1. Macierzświata
  2. Macierzwidoku
  3. Macierzprojekcji

Ponieważ są one wymagane, a nie zostały zdefiniowane wcześniej, konieczne będzie ich stworzenie. Niech będą właściwościami klasy głównej aplikacji Game1, w tym samym pliku:

publicMatrix view = Matrix.Identity;
publicMatrix projection = Matrix.Identity;
publicMatrix world = Matrix.Identity;

Po uruchomieniu aplikacji pojawi się uwaga:

Aby usunąć ten błąd, wystarczy prawym przyciskiem myszy kliknąć na dodaną teksturę modelu w solution explorerze i wybrać Exclude from Project. Kompilator wykrył, że tekstura jest połączona z modelem znajdującym się w pliku mill.fbx i sam próbuje ją automatycznie załadować, a ponieważ w projekcie również ona występuje, niepotrzebnie pobierana jest dwukrotnie. Ważne jest to, że przy wybieraniu Exclude from Project plik z teksturą nie został usunięty i dalej fizycznie znajduje się w tym samym miejscu co model. Dzięki temu XNA nie będzie miał problemu z załadowaniem tekstury, mimo że nie znajduje się ona bezpośrednio w listingu Solution Explorera.

Problem błędu zniknął, ale dalej coś jest jeszcze nie tak. Mianowicie, nie widać ładowanego modelu. Dzieje się tak dlatego, że znajdujemy się w jego wnętrzu i jest on dla nas niewidoczny – ponieważ zadeklarowana macierz świata znajduje się w początku układu współrzędnych, podobnie jak środek modelu młyna. Dodatkowo, macierz projekcji oraz macierz widoku nie zostały wyliczone. W metodzie Initialize() dodany zostanie następujący kod:

float aspectRatio = graphics.GraphicsDevice.Viewport.Width / (float)graphics.GraphicsDevice.Viewport.Height;
projection = Matrix.CreatePerspectiveFieldOfView(MathHelper.ToRadians(45.0f), aspectRatio, 1f, 10000);

Vector3 position = newVector3(0, 100, 100);
view = Matrix.CreateLookAt(newVector3(400), position, Vector3.Up);
world = Matrix.Identity *
Matrix.CreateScale(10f) *
Matrix.CreateRotationX(-90) *
Matrix.CreateTranslation(position);

Macierz projekcji dla określenia widoczności kamery potrzebuje określenia kąta widzenia kamery, proporcji ekranu oraz odległości bliskiego i dalekiego planu, który ma być wyświetlony. Następnie, w trzeciej linii, określona jest pozycja obiektu, która wykorzystana jest w kolejnych dwóch liniach. Macierz widoku określa położenie kamery, natomiast macierz świata określa położenie modelu.

Efekt końcowy jest następujący:

Ponieważ wielu twórców wykorzystuje tę najprostszą metodę rysowania modeli, autorzy framework przygotowali w wersji 4.0 specjalnie dla nich uproszczenie, które sprowadza się do jednej linii kodu w metodzie Draw(), zamiast ww. pętli i tworzenia macierzy transformacji:

               mill.Draw(world, view, projection);

Warto wspomnieć jeszcze o innych efektach standardowo wbudowanych w XNA 4.0, które pojawiły się z powodu platformy Windows Phone 7, na której są w pełni wspierane. Mowa tutaj o:

  1. SkinnedEffect  – animowane modele 3d.
  2. EnvironmentMapEffect – mapa środowiska, czyli odbicia otoczenia.
  3. DualTextureEffect – dwie tekstury na jednym obiekcie, przydatne, gdy chcemy np. stworzyć szybką aplikację, która nie wykorzystuje dynamicznych świateł i jedna tekstura pełni funkcję światłocieni po nałożeniu na teksturę właściwą.
  4. AlphaTestEffect – przydatne, gdy wykorzystywane są np. sprite’y z kanałami alpha.

Warto poeksperymentować z tymi klasami, definiując kolejne właściwości dla obiektu Effect. Gwarantują one wysoką wydajność aplikacji przy małym nakładzie zasobów programistów tworzących daną grę, a zaoszczędzony czas wykorzystać np. przy dodawaniu nowych funkcjonalności do aplikacji, czyniąc ją bogatszą.

Bibliografia

  1. https://blogs.msdn.com/b/shawnhar/archive/2010/04/28/new-built-in-effects-in-xna-game-studio-4-0.aspx
  2. http://zgk.wi.ps.pl/cms/data/wyklad_gk_shading.pdf
  3. http://pl.wikipedia.org/wiki/Grafika_trójwymiarowa
  4. https://msdn.microsoft.com/en-us/library/microsoft.xna.framework.graphics.basiceffect_members.aspx