Posts tagged ‘textures’

Pule pamięci w DirectX

2009-11-21 12:24

Jedną z rzeczy, która na początku programowania w DirectX może wydawać się dziwna, jest tajemniczy parametr Pool. Pojawia się on w każdej funkcji, która tworzy jakiś zasób graficzny: teksturę, bufor wierzchołków, siatkę modelu (ID3DXMesh), bufor głębokości, itp.
Rolą tego parametru jest określenie, w której puli pamięci znajdzie się tworzony zasób. DX wyróżnia bowiem ich kilka, co jest związane przede wszystkim z (przynajmniej) dwoma rodzajami pamięci, z jakimi możemy mieć do czynienia programując grafikę: zwykłym systemowym RAM-em oraz pamięcią karty graficznej.

W jakiej puli powinniśmy więc umieszczać swoje obiekty? To zależy od kilku czynników. Na początek na pewno warto przyjrzeć możliwościom:

  • D3DPOOL_DEFAULT, jak wskazuje na to nazwa, oznacza pulę domyślną. Gdy użyjemy tej flagi, DirectX umieści nasz zasób w najlepszym – pod względem wydajności – miejscu, bazując przy tym na innym parametrze, Usage (określa on, mówiąc w skrócie, sposób wykorzystania danego zasobu). Tym najlepszym miejscem jest prawie zawsze pamięć karty graficznej.
    To sprawia jednak, że przy utracie urządzenia (o ile nie programujemy co najmniej w DX9Ex) taki zasób należy zwolnić, a potem utworzyć ponownie – musimy więc pamiętać sposób, w jaki go utworzyliśmy. Ponadto istnieją też pewne ograniczenia (bezwzględne lub wydajnościowe) w dostępie do obiektów z puli domyślnej: o ile nie zadeklarujemy ich jako dynamiczne (D3DUSAGE_DYNAMIC), ich blokowanie wiąże się ze stratą szybkości albo jest wręcz niemożliwe (w przypadku tekstur).
  • D3DPOOL_MANAGED to pula zarządzana. Oznacza to, że pieczę nad nią sprawują sam DirectX i to on decyduje, w którym rodzaju pamięci zasoby z tej puli zostaną umieszczone. Zazwyczaj oznacza to, że w pamięci operacyjnej trzymana jest kopia obiektu, znajdującego się też w pamięci graficznej. Dzięki temu nie trzeba go tworzyć ponownie w przypadku straty urządzenia, a także można go blokować i modyfikować niezależnie od typu i flag Usage; w tym przypadku DX zadba o odpowiednią synchronizację.
  • D3DPOOL_SYSTEMMEM oznacza pulę pamięci systemowej. Zasoby tu stworzone będą znajdowały się w zwykłym RAM-ie i nie będą mogły być bezpośrednio renderowane. Dane z nich mogą jednak być kopiowane do zasobów znajdujących się w puli domyślnej, jak chociażby poprzez funkcję UpdateTexture.
  • D3DPOOL_SCRATCH jest również usytuowana w pamięci systemowej (RAM). W odróżnieniu od poprzedniej, zasoby z tej puli nie są jednak w żaden sposób (także pośredni) dostępne dla urządzenia i nie mogą być używane podczas renderowania. Oznacza to też, że nie podlegają one ograniczeniom związanym z kartą graficzną. Można więc, przykładowo, tworzyć w tej puli tekstury o rozmiarach niebędących potęgami dwójki także wtedy, gdy karta nie wspiera takich tekstur.

Spotkałem się z dwiema ogólnymi wytycznymi dotyczącymi tego, z których pul pamięci należy korzystać:

  1. Pierwsza z nich mówi, że należy wszystko, co się da, umieszczać w D3DPOOL_DEFAULT, bo to zapewnia największą wydajność, jako że wtedy zasoby generalnie umieszczane są w pamięci karty graficznej.
  2. Druga szkoła sugeruje z kolei, żeby wszystko, co się da, umieszczać w D3DPOOL_MANAGED, gdyż wtedy pozwalamy DirectX-owi zdecydować, w jakim rodzaju pamięci trzymać nasz zasób – a już on powinien o tym wiedzieć lepiej.

W sumie więc wygląda na to, że nic nie wiadomo :) Oczywiście są przypadki, gdy wyboru nie mamy, jak to się dzieje choćby dla tekstur typu render target, które muszą być stworzone w D3DPOOL_DEFAULT. Zawsze jednak będziemy mieli takie zasoby, które “równie dobrze” dałoby się umieścić w puli domyślnej, jak i zarządzanej. Co wtedy?
Otóż wydaje mi się, że to zależy od wielkości naszej aplikacji (w sensie rozmiaru używanych zasobów) oraz… stopnia zaawansowania w programowaniu w DirectX. Na początek bowiem D3DPOOL_MANAGED jest pulą bezpieczniejszą: nie musimy się w niej martwić o stratę urządzenia i możemy każdy zasób blokować i zmieniać. Ceną za to jest wzrost zużycia pamięci przez aplikację, spowodowany trzymaniem przez DX kopii obiektów w pamięci systemowej.
Jeśli jednak nasza gra (nie bójmy się użyć tego słowa ;)) używa dużej ilości zasobów, to takie marnotrawstwo jest zwykle nie do przyjęcia. Wtedy przynajmniej część z nich należy przenieść do D3DPOOL_DEFAULT. Istnieje szansa, że na tym etapie będziemy już wiedzieć lepiej, którą część ;-)

Na koniec pozwolę sobie też wspomnieć o – często pomijanej – puli D3DPOOL_SCRATCH. Jej przeznaczeniem są wszelkiego rodzaju zasoby pomocnicze, których nie renderujemy, ale mimo to wykorzystujemy do jakiegoś celu – na przykład tworzenia innych zasobów. Typowym przykładem są wszelkiego rodzaju pomocnicze, narzędziowe tekstury – jak choćby mapy wysokości (heightmap), na podstawie których generujemy ukształtowanie terenu.
Najlepszą pulą dla takich obiektów jest właśnie D3DPOOL_SCRATCH. Użycie jakiejkolwiek innej spowodowałoby uszczerbek na wydajności lub wręcz błędy, jak np. niezamierzone przeskalowanie tekstury do rozmiaru 2n.

Tags: , ,
Author: Xion, posted under Programming » 2 comments

Powtórka z DirectX

2009-10-29 20:58

Za sprawą przedmiotu o nazwie Grafika Komputerowa 3D musiałem ostatnio przypomnieć sobie, jak tak naprawdę i w praktyce koduje się w DirectX. Pewnie brzmi to dziwnie, ale w rzeczywistości przez ładnych kilka miesięcy nie pisałem większych ilości kodu, który by z tej biblioteki korzystał.

GK3D - screen
Piękna scena ;-)

Projekt, który musiałem teraz napisać, nie był ani trochę ambitny, bo polegał li tylko na wyświetleniu zupełnie statycznej sceny z kilkoma modelami, oświetleniu jej i zapewnieniu możliwości poruszania się w stylu strzelanek FPP. Oczywiście nie było też mowy o żadnych shaderach.

Niby banalne, ale jednak rzecz zajęła mi w sumie jakieś cztery znormalizowane wieczory (czyli od 3 do 4 godzin każdy). Częściowo było tak pewnie dlatego, że pokusiłem się jeszcze o teksturowanie, możliwość regulacji paru opcji renderowania czy bardzo, bardzo prosty menedżer sceny – czytaj: drzewko obiektów + stos macierzy ;)
Wydaje mi się jednak, że ważniejszą przyczyną był nieszczęsny fixed pipeline, którego byłem zmuszony używać. Jeszcze kilka lat temu nigdy bym nie przypuszczał, że to powiem, ale… shadery są po prostu łatwiejsze w użyciu. Porównując chociażby trywialne mieszanie koloru diffuse wierzchołka z teksturą przy użyciu pixel shadera:

  1. psOut.color = psIn.diffuse * tex2D(tex0, psIn.Tex0)

oraz stanów urządzenia:

  1. device->SetTextureStageState (0, D3DTSS_COLORARG1, D3DTA_CURRENT);
  2. device->SetTextureStageState (0, D3DTSS_COLORARG2, D3DTA_TEXTURE);
  3. device->SetTextureStageState (0, D3DTSS_COLOROP, D3DTOP_MODULATE);

nietrudno jest ocenić, w której znacznie lepiej widać, co faktycznie dzieje się z kolorem piksela. No, chyba że dla kogoś multum stałych w rodzaju D3DABC_SOMESTRANGEOPTION jest czytelniejsze niż po prostu mnożenie ;P

Inną sprawą jest też to, że w DirectX napisanie aplikacji od podstaw jest stosunkowo pracochłonne. Brak “złych” funkcji typu glVertex* ma rzecz jasna swoje zalety, lecz jest też jednym z powodów, dla których tak opłaca się posiadanie własnego frameworka, a może nawet i – tfu tfu – silnika ;-)

Fragmenty a piksele

2008-04-22 22:25

W terminologii DirectX programowalny potok graficzny ma dwie ważne części: vertex shader i pixel shader. Nazwa tej drugiej jest zasadniczo trochę myląca. Sugeruje ona, że shader jest wykonywany dla każdego piksela na ekranie, co tak naprawdę jest dalekie od prawdy.
Tak naprawdę bowiem “piksele” te są dopiero kandydatami do zapisania w buforze tylnym. Po drodze mają bowiem wiele okazji na to, aby być całkowicie wyeliminowane z renderingu. Może to się stać z któregoś z poniższych powodów, które jednak nie wyczerpują wszystkich możliwości:

  • Test alfa. W przypadku włączenia tego rodzaju testu, możliwe jest odrzucenie “pikseli”, których wartość kanału alfa nie jest dostatecznie duża. Zwykle jako wartość porównawczą wykorzystuje się 127, 128 albo domyślnie 0. Nie zawsze można sobie oczywiście pozwolić na takie skwantowanie informacji o przezroczystości, ale ma ono niebagatelną przewagę wydajnością nad alpha-blendingiem.
  • Test głębi. To najbardziej oczywisty test, a związany jest z przesłanianiem obiektów. W wielu przypadkach możliwe jest wykonanie go przed pixel shaderem, lecz w określonych sytuacjach może być konieczne testowanie już przetworzonych “pikseli”. W zależności od powodzenia testu wartość głębi zapisana w buforze może być oczywiście uaktualniona, co wpływa na dalsze testy.
  • Test stencila. Test wykorzystujący stencil buffer jest sprzężony z testem głębi, wobec czego podlega podobnym ograniczeniom. W szczególności możliwe jest na przykład zapisanie innej wartości do wspomnianego bufora w zależności od tego, czy “piksel” wyłoży się na teście głębi czy stencila.

A zatem wyjście pixel shadera niekoniecznie musi bezpośrednio trafić na ekran. Nie powinniśmy więc brać liczby pikseli w buforze ekranu za średnią liczbę wywołań tego shadera, a już na pewno nie jako górną granicę.
Dlatego też trzeba przyznać, że używana w OpenGL nazwa ‘fragment program‘ jest o wiele lepsza niż ‘pixel shader’. Fragment (wyjście shadera) nie jest bowiem jeszcze pikselem, a jedynie kandydatem na niego, który może odpaść przy wielu okazjach.

Tags: , , , ,
Author: Xion, posted under Programming » 1 comment

Tekstury do wszystkiego

2008-02-17 21:10

W potocznym rozumieniu tekstura to taki obrazek, który nakłada się obiekt trójwymiarowy, aby w ten sposób imitować wygląd jego powierzchni. Rzeczywiście, dawno temu była to ich jedyna funkcja. Tego rodzaju tekstury (nazywane teksturami rozproszenia) są oczywiście nadal niezbędne. Obok nich powstało jednak całe mnóstwo innych rodzajów tekstur, które są wykorzystywane podczas renderowania scen 3D.

Wśród nich są na przykład takie, które zawierają pewne niezbędne informacje na temat obiektów na scenie – nie tylko zresztą geometrii. Są to chociażby:

  • Przykład mapy wysokości
    Przykład mapy wysokości

    Mapy wysokości (height maps). To czarno-białe tekstury, używane do modelowania terenu. Jasność konkretnego piksela odpowiada wysokości terenu w danym punkcie. Taka tekstura musi być naturalnie przetworzona na odpowiednie trójkąty (co specjalnie trudne nie jest), ale jej używanie zamiast innych reprezentacji ma dwie wyraźne zalety. Po pierwsze, umożliwia regulowanie stopnia szczegółowości (Level of Detail, LoD) wyświetlanego terenu. Po drugie, mapy wysokości są bardzo łatwe do wykonania za pomocą dowolnego programu graficznego nawet przez średnio uzdolnionego w tym kierunku kodera :)

  • Przykład mapy normalnych
    Odpowiadająca jej
    mapa normalnych

    Mapy normalnych (normal maps) obrazują z kolei wektory normalne punktów powierzchni. Pomysł jest bardzo prosty: kolor każdego piksela w formacie RGB odpowiada normalnej o współrzędnych XYZ. Ponieważ współrzędne te są ograniczone (długość normalnej to zawsze 1), mogą być zapisane jako kolor. Mapa normalnych jest potem wykorzystywana przy obliczeniu oświetlenia per-pixel.

  • Mapy światła (light maps) reprezentują natomiast rozkład oświetlenia na scenie lub wokół źródła światła. W tym pierwszym przypadku chodzi o użycie wygenerowanego wcześniej jakąś kosztowną metodą (np. śledzenia fotonów) “wzorca” oświetlenia sceny. Ma to rzecz jasna sens tylko wtedy, gdy oświetlenie jest statyczne. Z kolei lightmapa dla źródła światła obrazuje kształt promieni świetlnych, które rzekomo z niego wychodzą. W obu przypadkach jasność pikseli odpowiada intensywności oświetlenia, chociaż mapy światła nie muszą być monochromatyczne.

Tego rodzaju tekstury są przygotowywane wcześniej i obok modeli, tekstur rozproszenia i innych danych stanowią informacje umożliwiają renderowanie sceny. Oprócz nich w trakcie samego rysowania wykorzystywane są też inne tekstury, tworzone na bieżąco i zwykle niezachowywane na później. Wśród tych efemerycznych tekstur mamy na przykład:

  • Mapy cieni (shadow maps). Są one używane przy jednej z technik liczenia cieni. Pojedyncza mapa to po prostu zapis bufora głębokości dla sceny widzianej z punktu widzenia źródła światła. Dzięki temu możliwe jest następnie określenie, który piksel jest widoczny dla tegoż źródła, a który jest w cieniu. To dość prosty sposób obliczenia cieniowania, w podstawowej wersji wymaga jednak dodatkowego przebiegu dla każdego źródła światła.
  • Imbryk z mapą sześcienną
    Imbryczek z cubemapą

    Mapy odbić środowiskowych (environmental maps) służą do symulowania przedmiotów o powierzchniach lustrzanych. Podobnie jak wyżej, wymagają osobnego przebiegu renderowania, i to często nawet niejednego (jak w przypadku map sześciennych). Tak powstałe obrazy odbić są potem nakładane na przedmiot, który dzięki temu sprawia wrażenie, jakby odbijał w sobie resztę sceny.

  • Bufory geometrii (G-buffers) to w zasadzie nie jedna, a zestaw tekstur, z których każda zawiera informacje o pewnym parametrze materiału dla danego piksela z gotowego obrazu sceny. Po wypełnieniu ich informacjami, “bufor” ten jest wykorzystywany np. dla obliczeń związanych z oświetleniem i cieniowaniem we wszystkich technikach opatrzonych modnym przydomkiem ‘deferred‘. Dzięki temu oszczędza się każdorazowego przekształcania całej geometrii dla każdego przebiegu renderowania przez wszystkie macierze.

Potencjalnych i aktualnych zastosowań tekstur jest o wiele więcej. Ale już na tych przykładach widać, że tekstury tak naprawdę nie są obrazkami, a jedynie zbiorami jakichś informacji, które tylko z konieczności są zapisywane w postaci kolorów pikseli. Być może niedługo staną się one pełnoprawną “pamięcią operacyjną” kart graficznych, którą można będzie np. alokować i zwalniać w kodzie shaderów. Jak dotąd możliwy jest ich odczyt oraz w pewnym stopniu zapis (zależnie od modelu shaderów), ale kto wie – może wkrótce doczekamy się czegoś więcej?…

Czcionki i tekst(ury)

2007-07-30 9:08

Żadna porządna biblioteka do grafiki 2D nie może obyć się bez narzędzi służących do wypisywania tekstu. Kiedy jednak mamy na uwadze głównie programowanie gier (lub pokrewnych aplikacji), sprawa wygląda nieco inaczej niż w bardziej “ogólnych” zastosowaniach. Nie trzeba na przykład rozkładać tekstu na czynniki pierwsze:

Linie pisma

Pozwalałoby to oczywiście w razie potrzeby dodać kursywę, pod-, nad- i przekreślenie. Zazwyczaj jednak nie jest to potrzebne.

Tak więc chociaż wygląda to na krok wstecz, stosuje się najczęściej czcionki bitmapowe. Pomysł polega na tym, że cały używany zestaw znaków danego kroju i danej wielkości jest rysowany na jednej teksturze w taki sposób, by łatwo można było obliczyć pozycję każdego znaku:

Tekstura czcionki Arial stworzona programem Bitmap Font Builder Tekstura czcionki Verdana stworzona programem Bitmap Font Generator

Wyświetlanie napisu polega wtedy na renderowaniu oteksturowanych prostokątów – po jednym dla każdego znaku. Jest to bardzo wydajne, bo chociaż trójkątów może być bardzo dużo, to ich tekstura jest zawsze taka sama. Można zatem wyrzucić całe połacie tekstu na ekran tylko jednym wywołaniem Draw[Indexed]Primitive.

Tylko skąd wziąć taką sprytną teksturę? Można rzecz jasna generować ją samemu przy pomocy funkcji GDI; choć jest to wolne, byłoby wykonywane tylko raz, więc nie stanowiłoby problemu. Lepszym rozwiązaniem jest użycie odpowiednich programów, z których najlepszym jest chyba Bitmap Font Generator. Potrafi on w sprytny sposób upakować w jednym obrazku sporą ilość znaków, zaś ich parametry opisuje w łatwym do odczytania formacie tekstowym przypominającym szczątkowy INI.

Obecnie używam więc właśnie jego i dzięki temu mogłem w końcu dodać do swojego silnika podstawowy i zupełnie niezbędny element: licznik FPSów :)

 


© 2017 Karol Kuczmarski "Xion". Layout by Urszulka. Powered by WordPress with QuickLaTeX.com.