Uniwersytet Zielonogórski Instytut Sterowania i Systemów Informatycznych Systemy wirtualnej rzeczywistości Laboratorium Podstawy grafiki 3D Wstęp: W drugiej części przedstawione zostaną podstawowe mechanizmy tworzenia 3-wymiarowej grafiki czasu rzeczywistego, tworzenia buforów wierzchołków i indeksów, macierzy przekształceń i renderowania. 1. Bufory wierzchołków i indeksów oraz teksturowanie Celem niniejszej instrukcji jest stworzenie prostego terenu, po którym będzie mógł poruszać się czołg. Większość modeli 3D opisana jest za pomocą trójkątów. Trójkąty natomiast opisane są trzema wierzchołkami. Chcielibyśmy aby nasz teren był kwadratem o zadanej wielkości. Do zbudowania kwadratu potrzeba 2-ch trójkątów, a zatem 6 wierzchołków (rys. 3a). Łatwo zauważyć, że 2 wierzchołki z pierwszego trójkąta pokrywają się z 2-ma wierzchołkami drugiego trójkąta. Aby ograniczyć powtarzanie zbędnych danych stworzono indeksowanie. Każdy wierzchołek podawany jest tylko raz, a strukturę trójkątów opisujemy w buforze indeksów. Mechanizm ten ilustruje rys. 3b. Należy pamiętać, że domyślnie każdy trójkąt jest widoczny tylko z jednej strony. DirectX uznaje trójkąt za widoczny jeśli jego wierzchołki układają się zgodnie z ruchem wskazówek zegara (rys. 4), w przeciwnym razie trójkąt jest niewidoczny. 1 2 4 1 2 bufor indeksów = [1,2,4,2,3,4] 1 2 2 widoczny niewidoczny 3 6 5 4 3 Rys. 1 - a) kwadrat złożony z 6 wierzchołków, b) kwadrat złożony z 4 wierzchołków i bufor indeksów 1 3 3 1 Rys. 2 - Widoczność trójkąta
Dodatkowo chcielibyśmy aby nasz teren pokryty był uprzednio dodaną teksturą trawy. Aby poprawnie teksturować obiekt 3D potrzeba dodatkowych danych zwanych współrzędnymi UV lub po prostu współrzędnymi tekstury. Górnemu lewemu rogowi tekstury przypisuje się współrzędne (0, 0), a dolnemu prawemu (1, 1). Następnie dodając do wierzchołków siatki odpowiednie współrzędne UV można opisać jak tekstura ma być nałożona na trójkąt (rys. 5). (0,0) (1,0) (0,0) (2,0) (0.3,0.3) (1,0.3) (0,0) (2,0) (0,1) (1,1) (0,2) (2,2) (0.3,1) (1,1) (1,0) (2,1) Rys. 3 - Tekstura nałożona na kwadrat przy różnych wartościach UV Potrzebny jest zatem bufor wierzchołków i indeksów. Deklarujemy dwie zmienne w klasie głównej gry: VertexBuffer terenwierzcholki; IndexBuffer terenindeksy; Stwórzmy też metodę, która zainicjuje oba bufory: protected void StworzTeren() { } Najpierw utworzymy wierzchołki. Posłuży nam do tego tablica typu VertexPositionTexture. Typ ten reprezentuje wierzchołek zawierający współrzędne pozycji i tekstury. Dodatkowe dwie zmienne typu float pomogą nam w prawidłowym zainicjowaniu wierzchołków. Pierwsza z nich określa wielkość terenu, natomiast druga określa stopień rozciągnięcia tekstury na płaszczyźnie terenu. VertexPositionTexture[] wierzcholki = new VertexPositionTexture[4]; float skala = 10.0f; float skalatekstury = 1.0f; Następnie wypełniamy tablicę wierzchołków odpowiednimi wartościami współrzędnych: 2
wierzcholki[0].position = new Vector3(-skala, 0.0f, -skala); wierzcholki[0].texturecoordinate.x = 0.0f; wierzcholki[0].texturecoordinate.y = 0.0f; wierzcholki[1].position = new Vector3(skala, 0.0f, -skala); wierzcholki[1].texturecoordinate.x = skalatekstury; wierzcholki[1].texturecoordinate.y = 0.0f; wierzcholki[2].position = new Vector3(-skala, 0.0f, skala); wierzcholki[2].texturecoordinate.x = 0.0f; wierzcholki[2].texturecoordinate.y = skalatekstury; wierzcholki[3].position = new Vector3(skala, 0.0f, skala); wierzcholki[3].texturecoordinate.x = skalatekstury; wierzcholki[3].texturecoordinate.y = skalatekstury; Wreszcie tworzymy i wypełniamy bufor wierzchołków: terenwierzcholki = new VertexBuffer(graphics.GraphicsDevice, typeof(vertexpositiontexture), 4, BufferUsage.WriteOnly); terenwierzcholki.setdata<vertexpositiontexture>(wierzcholki); W analogiczny sposób stworzymy bufor indeksów: short[] indeksy = new short[6]; indeksy[0] = 0; indeksy[1] = 1; indeksy[2] = 2; indeksy[3] = 1; indeksy[4] = 3; indeksy[5] = 2; terenindeksy = new IndexBuffer(graphics.GraphicsDevice, typeof(short), 6, BufferUsage.WriteOnly); terenindeksy.setdata<short>(indeksy); Na koniec pozostaje jedynie wywołanie napisanej właśnie metody w metodzie LoadContent klasy gry: StworzTeren(); 2. Macierze transformacji Przed przystąpieniem do renderingu musimy jeszcze zdefiniować macierze transformacji. Posłużą one do określenia gdzie znajdują się obiekty w scenie, jak są zorientowane, gdzie jest kamera, w którą stronę jest zwrócona oraz w jaki sposób obiekty 3D są rzutowane na 2- wymiarowy ekran. W zależności od pełnionej funkcji rozróżnia się trzy rodzaje macierzy: 3
macierz świata opisuje przesunięcie obiektu w przestrzeni, jego obrót oraz skalę macierz widoku reprezentuje obserwatora, w którym miejscu się znajduje oraz na co patrzy macierz projekcji opisuje w jaki sposób 3-wymiarowy obiekt jest rzutowany na płaszczyznę ekranu Ważną właściwością macierzy transformacji jest to, że można je łączyć wymnażając je ze sobą. Np. posiadając macierz translacji A i macierz obrotu B można utworzyć jedną macierz reprezentującą jednocześnie translację i obrót wykonując następujące działanie: 1 0 0 2.3 0.7 0.7 0 0 0.7 0.7 0 1.6 0 1 0 0 0.7 0.7 0 0 0.7 0.7 0 1.6 = = 0 0 1 0 0 0 1 0 0 0 1 0 0 0 0 1 0 0 0 1 0 0 0 1 Należy jednak pamiętać o zachowaniu odpowiedniej kolejności mnożenia. Działanie A*B da inny efekt niż B*A (rys. 6). przesunięcie obrót obrót przesunięcie Rys. 4 - Wynik zastosowania tych samych transformacji w różnej kolejności Macierze transformacji są bardzo istotnym elementem zagadnień związanych z grafiką trójwymiarową. Więcej informacji na temat macierzy transformacji można znaleźć w literaturze związanej z grafiką 3D. Deklarujemy zatem trzy macierze: Matrix projekcja; Matrix widok; Matrix swiat; Następnie przypisujemy im wartości korzystając z pomocniczych metod klasy Matrix. Metoda CreatePerspectiveFieldOfView pobiera jako parametr pole widzenia (w radianach), proporcje obrazu, przednią i tylną płaszczyznę (rys. 7). Pole widzenia oraz płaszczyzny 4
przednia i tylna określają obszar widzenia kamery. Wszystkie obiekty znajdujące się bliżej niż przednia płaszczyzna lub dalej niż tylna płaszczyzna są obcinane. CreateLookAt tworzy macierz widoku po podaniu położenia kamery, punktu na który jest zwrócona oraz wektora wskazującego górę. Chcemy aby teren znajdował się w środku sceny bez żadnych transformacji dlatego macierz świata ustawimy na macierz jedynkową. tylna płaszczyzna obserwator przednia płaszczyzna Rys. 5 - Przednia i tylna płaszczyzna projekcja = Matrix.CreatePerspectiveFieldOfView(MathHelper.ToRadians(50), graphics.graphicsdevice.viewport.aspectratio, 0.1f, 1000.0f); widok = Matrix.CreateLookAt(new Vector3(24.0f, 20.0f, 10.0f), new Vector3(0.0f, 0.0f, 0.0f), Vector3.Up); swiat = Matrix.Identity; 3. Rendering Ostatnim brakującym elementem potrzebnym do narysowania sceny jest shader. Shader y to małe programy odpowiedzialne za cieniowanie obiektów. Inaczej mówiąc shader y obliczają gotowy obraz na podstawie danych o wierzchołkach, oświetleniu, teksturach, itd. Rozwój sprzętu spowodował, że wykonywane na procesorze karty graficznej shader y służą już nie tylko cieniowaniu ale również rozmaitym efektom i obliczeniom nie związanym bezpośrednio z obliczaniem grafiki. W XNA klasą reprezentującą zbiór shader ów jest klasa Effect. My jednak skorzystamy z uproszczonej jej wersji BasicEffect, która zawiera wbudowane podstawowe mechanizmy renderowania. Deklarujemy zmienną typu BasicEffect: BasicEffect basiceffect; Następnie w LoadContent tworzymy nowy efekt i ustawiamy w nim macierze transformacji: basiceffect = new BasicEffect(graphics.GraphicsDevice, null); basiceffect.projection = projekcja; 5
basiceffect.view = widok; basiceffect.world = swiat; Wreszcie możemy przystąpić do rysowania sceny. Przechodzimy do metody Draw. Usuwamy kod rysujący obraz trawy, nie będziemy go już potrzebować. W podobny do rysowania 2- wymiarowych obrazów sposób rozpoczynamy i kończymy rysowanie metodami Begin oraz End ale na rzecz obiektu basiceffect. Określamy typ wierzchołków (VertexDeclaration) jakich zamierzamy używać w naszym przypadku będą to wierzchołki zawierające współrzędne 3D oraz współrzędne tekstury (VertexPositionTexture). Następnie ustawiamy na urządzeniu nasze bufory wierzchołków i indeksów. Każda siatka 3D może mieć przypisane różne materiały wymagające różnych shader ów. Dlatego efekt może zawierać różne techniki, a każda z technik może składać się z kilku przebiegów. W naszym przypadku wykorzystujemy tylko podstawowe mechanizmy, jedną technikę i jeden przebieg. Rozpoczynamy zatem rysowanie metodą Begin pierwszego przebiegu pierwszej techniki i rysujemy teren metodą DrawIndexedPrimitives. Po narysowaniu obiektu zamykamy przebieg metodą End. basiceffect.begin(); graphics.graphicsdevice.vertexdeclaration = new VertexDeclaration( graphics.graphicsdevice, VertexPositionTexture.VertexElements); graphics.graphicsdevice.vertices[0].setsource(terenwierzcholki, 0, VertexPositionTexture.SizeInBytes); graphics.graphicsdevice.indices = terenindeksy; basiceffect.techniques[0].passes[0].begin(); graphics.graphicsdevice.drawindexedprimitives(primitivetype.trianglelist, 0, 0, 4, 0, 2); basiceffect.techniques[0].passes[0].end(); basiceffect.end(); Po skompilowaniu powinniśmy otrzymać wynik jak na rys. 8. Ostatnią rzeczą jaką chcielibyśmy zrealizować jest nałożenie na teren tekstury trawy. Wracamy więc do metody LoadContent i określamy dwa dodatkowe parametry efektu: basiceffect.textureenabled = true; basiceffect.texture = trawa; Rys. 6 - Teren bez tekstury Rys. 8 - Teren bez tekstury 6
Teraz obraz powinien wyglądać jak na rys. 9. Tekstura wydaje się być zbyt rozciągnięta. Możemy to bardzo prosto naprawić zmieniając wartość zmiennej skalatekstury w metodzie StworzTeren. Jeżeli zmienimy ją na 10 uzyskamy efekt jak na rys. 10. Rys. 8 - Teren z teksturą dla skalatekstury = 1 Rys. 7 Teren z teksturą dla skalatekstury = 10 7