Uniwersytet Zielonogórski Instytut Sterowania i Systemów Informatycznych Systemy wirtualnej rzeczywistości Laboratorium Komponenty i serwisy Wstęp: W trzeciej części przedstawione zostaną podstawowe techniki obsługi modeli 3D oraz komponentowy sposób tworzenia aplikacji. Zaprezentowane zostaną wbudowane mechanizmy komponentów (GameComponent) i serisów (Services). 1. Wczytanie modelu XNA posiada wygodną klasę do reprezentacji modeli 3D, które mogą się składać z wielu siatek, z przypisanymi wieloma materiałami, efektami, animacjami oraz hierarchią. Posiada również gotowe importery z plików X oraz FBX. Postępując analogicznie do ładowania tekstury trawy załadujemy teraz model czołgu (czolg.fbx). Tworzymy obiekt typu Model: Model czolg; Dodajemy plik czolg.fbx do projektu i w LoadContent wczytujemy go do zmiennej czolg. czolg = Content.Load<Model>("czolg"); 2. Rysowanie modelu Przechodzimy do metody Draw. Poniżej kodu rysowania terenu umieścimy kod rysowania modelu czołgu. Model ten składa się z dwóch siatek ( korpus i wiezyczka ). Każda siatka ma przypisany własny zestaw efektów. W naszym przypadku wszystkie siatki będą miały po jednym efekcie, ale utworzymy pętlę dla przypadku, w którym miałyby ich więcej. W pierwszym kroku tworzymy tablice transformacji, która zawiera transformacje dla każdej siatki. Jeśli nie uwzględnimy tych transformacji poszczególne siatki będą rysowane w początku lokalnego układu współrzędnych modelu zamiast w odpowiednim dla siebie 1
miejscu. Następnie ustawiamy macierze świata, widoku i projekcji dla każdego efektu każdej siatki wczytanego modelu. Macierze widoku i projekcji pozostawiamy bez zmian, natomiast macierzy świata przypisujemy wartości pobrane przed chwilą do tablicy. Dodatkowo wymnożymy je przez macierz skalowania ponieważ wczytany model jest zdecydowanie za duży względem naszego terenu. Użyjemy współczynnika skalowania równego 0.07. Ponieważ nie wprowadziliśmy żadnego oświetlenia dodatkowo włączymy oświetlenie domyślne poprzez metodę EnableDefaultLighting. Na samym końcu rysujemy model metodą Draw dla każdej siatki. Matrix[] transformacje = new Matrix[czolg.Bones.Count]; czolg.copyabsolutebonetransformsto(transformacje); foreach (ModelMesh mesh in czolg.meshes) foreach (BasicEffect effect in mesh.effects) effect.enabledefaultlighting(); effect.view = widok; effect.projection = projekcja; effect.world = transformacje[mesh.parentbone.index] * Matrix.CreateScale(0.07f); mesh.draw(); Wynikiem powinien być obraz jak na rys. 1. Rys. 1 - Model czołgu 2
3. Komponenty i serwisy Nadszedł czas na uporządkowanie kodu. Stworzymy trzy komponenty reprezentujące odpowiednio kamerę, teren i gracza. Wykorzystamy do tego wbudowany mechanizm obsługi komponentów. Komponent jest reprezentowany przez klasę GameComponent (lub DrawableGameComponent w przypadku gdy komponent będzie rysowany). Łatwo można zauważyć, że część komponentów będzie wymagać dostępu do innych komponentów lub zmiennych z klasy głównej. Przykładem może tu być komponent terenu, który będzie potrzebował dostępu do komponentu kamery w celu prawidłowego ustawienia macierzy widoku. Eleganckim rozwiązaniem tego problemu są serwisy (Services). Umożliwiają one udostępnienie potrzebnych obiektów dowolnym komponentom. Praktyczne działanie komponentów i serwisów zostanie przedstawione poniżej. 3.1 Kamera Pierwszym komponentem będzie kamera. Tworzymy nowy komponent (Add->New Item..., wybieramy Game Component i nadajemy mu odpowiednią nazwę). W klasie kamery będziemy przechowywać informacje o jej położeniu i punkcie, na który jest zwrócona oraz parametry projekcji. Deklarujemy odpowiednie zmienne: Vector3 pozycja; Vector3 cel; float polewidzenia; float proporcjeobrazu; float bliskaplaszczyzna; float dalekaplaszczyzna; Następnie dodajemy właściwości do pobierania i ustawiania pozycji i celu: public Vector3 Pozycja get return pozycja; set pozycja = value; public Vector3 Cel get return cel; set cel = value; Udostępniamy macierz projekcji, korzystając z metody CreatePerspectiveFieldOfView klasy Matrix: 3
public Matrix Projekcja get return Matrix.CreatePerspectiveFieldOfView(poleWidzenia, proporcjeobrazu, bliskaplaszczyzna, dalekaplaszczyzna); Udostępniamy macierz widoku, korzystając z metody CreatLookAt: public Matrix Widok get return Matrix.CreateLookAt(pozycja, cel, Vector3.Up); Na koniec dodamy metodę do wprowadzenia parametrów projekcji: public void UstawProjekcje(float polewidzenia, float proporcjeobrazu, float bliskaplaszczyzna, float dalekaplaszczyzna) this.polewidzenia = polewidzenia; this.proporcjeobrazu = proporcjeobrazu; this.bliskaplaszczyzna = bliskaplaszczyzna; this.dalekaplaszczyzna = dalekaplaszczyzna; 3.2 Teren Następnym komponentem będzie Teren. Większość potrzebnego kodu jest już napisana. Tworzymy zatem nowy komponent i nadajemy mu odpowiednią nazwę. W przeciwieństwie do kamery teren będzie rysowany więc zmieniamy dziedziczenie z GameComponent na DrawableGameComponent. Deklarujemy potrzebne zmienne: VertexBuffer wierzcholki; IndexBuffer indeksy; BasicEffect basiceffect; Texture2D tekstura; Następnie tworzymy konstruktor, w którym wywołujemy metodę StworzTeren i ustawiamy parametry efektu (do tego będziemy potrzebowali urządzenie renderujące, do którego odwołujemy się poprzez Services później stworzymy ten serwis w klasie głównej) i tekstury: 4
public Teren(Game game, Texture2D tekstura, float rozmiar, float rozmiartekstury) : base(game) this.tekstura = tekstura; StworzTeren(rozmiar, rozmiartekstury); GraphicsDevice urzadzenie = (GraphicsDevice)Game.Services.GetService(typeof(GraphicsDevice)); basiceffect = new BasicEffect(urzadzenie, null); basiceffect.textureenabled = true; basiceffect.texture = tekstura; Przenosimy metodę StworzTeren z klasy głównej do klasy Teren. Dodajemy odwołanie do urządzenia poprzez serwis. Ponadto tym razem metoda pobiera dwa parametry (skala i skalatekstury) oraz nastąpiła drobna zmiana nazw buforów (z terenwierzcholki na wierzcholki oraz z terenindeksy na indeksy). Na koniec musimy zadbać o rysowanie terenu. Tutaj również wykorzystamy gotowy już kod, lekko go modyfikując. Ponownie odwołujemy się do serwisów. W tym przypadku odwołamy się również do kamery aby rysować teren z uwzględnieniem położenia kamery. public override void Draw(GameTime gametime) GraphicsDevice urzadzenie = (GraphicsDevice)Game.Services.GetService(typeof(GraphicsDevice)); Kamera kamera = (Kamera)Game.Services.GetService(typeof(Kamera)); basiceffect.projection = kamera.projekcja; basiceffect.view = kamera.widok; basiceffect.world = Matrix.Identity; basiceffect.begin(); urzadzenie.vertexdeclaration = new VertexDeclaration(urzadzenie, VertexPositionTexture.VertexElements); urzadzenie.vertices[0].setsource(wierzcholki, 0, VertexPositionTexture.SizeInBytes); urzadzenie.indices = indeksy; basiceffect.techniques[0].passes[0].begin(); urzadzenie.drawindexedprimitives(primitivetype.trianglelist, 0, 0, 4, 0, 2); basiceffect.techniques[0].passes[0].end(); basiceffect.end(); base.draw(gametime); 5
3.3 Gracz Ostatnim komponentem będzie Gracz. Tworzymy zatem komponent i deklarujemy odpowiednie zmienne. W przyszłości będziemy chcieli sterować czołgiem dlatego dodamy również zmienne opisujące rotację korpusu czołgu i jego wieżyczki. Poza tym stworzymy właściwości dla pozycji i rotacji: Vector3 pozycja; float rozmiar; float rotacjakorpusu; float rotacjawiezyczki; Model model; public Vector3 Pozycja get return pozycja; set pozycja = value; public float Rotacja get return rotacjakorpusu; Następnie tworzymy konstruktor: public Gracz(Game game, Model model, float rozmiar) : base(game) this.model = model; this.rozmiar = rozmiar; Dodajemy metodę do ustawiania rotacji czołgu i wieżyczki: public void UstawRotacje(float rotacjakorpusu, float rotacjawiezyczki) this.rotacjakorpusu = rotacjakorpusu; this.rotacjawiezyczki = rotacjawiezyczki; Na koniec dodajemy rysowanie czołgu. Również tutaj wykorzystamy napisany już kod wprowadzając jedynie drobne zmiany analogiczne do rysowania terenu. Jedyna poważna zmiana polega na uwzględnieniu położenia i rotacji gracza względem globalnego układu współrzędnych oraz obliczaniu dodatkowej transformacji dla siatki wieżyczki. Umożliwi nam to obracanie jej niezależnie od korpusu. 6
public override void Draw(GameTime gametime) GraphicsDevice urzadzenie = (GraphicsDevice)Game.Services.GetService(typeof(GraphicsDevice)); Kamera kamera = (Kamera)Game.Services.GetService(typeof(Kamera)); Matrix[] transformacje = new Matrix[model.Bones.Count]; model.copyabsolutebonetransformsto(transformacje); foreach (ModelMesh mesh in model.meshes) Matrix swiat = Matrix.CreateScale(rozmiar) * Matrix.CreateRotationY(rotacjaKorpusu) * Matrix.CreateTranslation(pozycja); if (mesh.name == "korpus") swiat = transformacje[mesh.parentbone.index] * swiat; if (mesh.name == "wiezyczka") swiat = Matrix.CreateRotationZ(rotacjaWiezyczki) * transformacje[mesh.parentbone.index] * swiat; foreach (BasicEffect effect in mesh.effects) effect.enabledefaultlighting(); effect.projection = kamera.projekcja; effect.view = kamera.widok; effect.world = swiat; mesh.draw(); base.draw(gametime); 3.4 Zmiany w klasie głównej Pozostało jedynie zaktualizować klasę główną. Zaczniemy od deklaracji nowoutworzonych komponentów: Kamera kamera; Teren teren; Gracz gracz; Przejdźmy do LoadContent. Pierwszą zmianą jaką tu wprowadzimy będzie dodanie serwisu udostępniającego urządzenie renderujące. Wczytywanie tekstury trawy i modelu czołgu pozostawimy bez zmian. protected override void LoadContent() spritebatch = new SpriteBatch(GraphicsDevice); 7
trawa = Content.Load<Texture2D>("trawa"); czolg = Content.Load<Model>("czolg"); Services.AddService(typeof(GraphicsDevice), graphics.graphicsdevice); Następnie tworzymy kamerę, dodajmy ją do listy komponentów i udostępnijmy ją jako serwis: kamera = new Kamera(this); kamera.ustawprojekcje(mathhelper.toradians(50), graphics.graphicsdevice.viewport.aspectratio, 0.1f, 1000.0f); kamera.pozycja = new Vector3(13.0f, 15.0f, 8.0f); Services.AddService(typeof(Kamera), kamera); Components.Add(kamera); Tworzymy teren i również dodajemy go do listy komponentów: teren = new Teren(this, trawa, 10.0f, 10.0f); Components.Add(teren); Ostatnim komponentem, który dodamy jest Gracz. Na koniec skierujemy kamerę na czołg ustawiając współrzędne czołgu jako celu kamery. gracz = new Gracz(this, czolg, 0.02f); gracz.pozycja = new Vector3(5.0f, 0.0f, -3.0f); gracz.ustawrotacje(0.0f, 0.0f); Components.Add(gracz); kamera.cel = gracz.pozycja; Usuwamy wcześniej dodaną zawartość metody Draw. Dzięki temu, że dodaliśmy nasze komponenty do listy komponentów nie musimy umieszczać w metodzie Draw żadnych dodatkowych odwołań do metod rysujących. Polecenie base.draw(gametime) spowoduje wywołanie metod Draw wszystkich komponentów na liście. protected override void Draw(GameTime gametime) graphics.graphicsdevice.clear(color.cornflowerblue); base.draw(gametime); 8
Wynikiem uruchomienia aplikacji powinien być obraz jak na rys. 2. Rys. 2 - Działająca aplikacja 9