Posts tagged ‘graphics programming’

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?…

Nie całkiem brakujące ogniwo DirectX

2008-02-12 20:49

Wszyscy wiedzą, że przesiadka na Vistę ma jedną niezaprzeczalną zaletę. Jeśli mianowicie dysponujemy odpowiednią kartą graficzną, to możemy cieszyć się całym bogactwem Shader Model 4 oraz dziesiątej wersji DirectX. Nadal jednak niewiele gier posiada jakieś efekty (oczywiście w(y)łączalne) uzyskiwane przy pomocy nowej wersji biblioteki. Najwyraźniej Microsoft nieumyślnie wpadł tutaj w rodzaj błędnego koła, które zapobiega powszechnemu wykorzystaniu DirectX 10.
Nie pomaga tu też za bardzo specjalna wersja “dziewiątki”, czyli tak zwany Direct3D 9Ex. Cóż to za zwierz?…

Jest to mianowicie pewne rozszerzenie znanej i lubianej wersji biblioteki, które wprowadza nowe możliwości, częściowo zbieżne z tym, czego można doświadczyć w D3D10. Wśród nich mamy chociażby:

  • Praktyczny brak zjawiska utraty urządzenia. W zasadzie począwszy od DirectDraw, gdzie mieliśmy tzw. utratę powierzchni, była to zawsze spora niedogodność. Jak wiemy, utrata urządzenia DirectX dotąd następowała wtedy, gdy użytkownik przełączał się z aplikacji pełnoekranowej, zmieniając tryb graficzny (rozdzielczość, głębię kolorów, itd.). Powodowała ona stratę wszystkich zasobów przechowywanych w pamięci karty graficznej, czyli głównie tekstur, i konieczność ich ponownego ładowania – co było i jest dość kłopotliwe. W D3D9Ex utrata urządzenia następuje tylko w dwóch ekstremalnych sytuacjach sprzętowo-sterownikowych i właściwie nie trzeba się nią przejmować.
  • Współdzielenie zasobów. Funkcje DirectX 9 miały od dawna tajemniczy parametr pSharedHandle. Był on opisany jako zarezerwowany i należało w jego miejsce przekazywać NULL. Rezerwacja ta jednak nie przepadła i w 9Ex jest on wykorzystywany do współdzielenia zasobów między urządzeniami – nawet takimi, które są w oddzielnych procesach. Mogę sobie wyobrazić zastosowanie tego mechanizmu chociażby do przełączania trybów graficznych bez konieczności ponownego tworzenia wszystkich tekstur, buforów, itp.
  • Przydatne funkcje w nowym interfejsie IDirect3DDevice9Ex. Jest wśród nich na przykład WaitForVBlank, służąca do ręcznej synchronizacji pionowej.

Wszystkie te możliwości prezentują się całkiem nieźle. Żeby jednak z nich korzystać, muszą być spełnione dwa warunki. Po pierwsze, sprzęt musi wspierać tak zwany WDDM (Windows Device Driver Model), co w przypadku popularnych kart graficznych dotyczy z grubsza tych, które udostępniają Shader Model 3.
A drugi warunek?… Niejako wynika on z pierwszego, bowiem WDDM jest częścią systemu Windows Vista. Aplikacje wykorzystujące Direct3D 9Ex będą więc działały wyłącznie na tym systemie. To pewnie trochę zniechęcające, prawda? Ale cóż, Microsoft próbuje jak może ;-)

Wektor prostopadły do danego

2008-01-29 17:46

Wektor wyznacza pewną linią na płaszczyźnie lub w przestrzeni. A skoro tak jest, to czasami możemy potrzebować wektora, który będzie prostopadły do jakiegoś innego, który jest nam znany. Wyznaczenie go nie jest specjalnie trudne, ale w zależności od tego, czy wszystko dzieje się w 2D czy 3D, trzeba uwzględnić pewne szczegóły.
Takim szczegółem na pewno nie jest jednak długość wynikowego wektora, bo możemy go przecież zawsze przeskalować do żądanego rozmiaru. W tym celu wystarczy go najpierw znormalizować (podzielić przez aktualną długość), a potem pomnożyć przez nową długość.

Wektory prostopadłe na płaszczyźnieNa płaszczyźnie wektor prostopadły jest wyznaczony w miarę jednoznacznie:

  1. const VEC2 Orthogonal(const VEC2& v)
  2. {
  3.    return VEC2(-v.y, v.x);
  4. }

Mówiąc ściślej, znany jest jego kierunek, a przy ustalonej długości może mieć jedynie dwa ustalone, przeciwne zwroty. To w zasadzie dość oczywiste: prostą prostopadłą do wektora wyznaczyć jest… prosto (;D), natomiast kwestia jej skierowania zależy już od nas.
Dla wektora [x, y] prostopadłymi do niego będą więc wektory [-y, x] i [y, –x].

W przestrzeni trójwymiarowej jedno-(a właściwie dwu-)znacznie można wyznaczyć jedynie wektor prostopadły do dwóch innych. Oczywiście w takim przypadku tworzą one płaszczyznę, więc tak naprawdę poszukujemy wektora (prostej), przecinającego pod kątem prostym tę właśnie płaszczyznę. Jak wiadomo, można to uczynić, obliczając iloczyn wektorowy (cross product) tych dwóch wektorów.
W przypadku, gdy mamy tylko jeden wektor, musimy się rzecz jasna liczyć z tym, że kierunków prostopadłych do niego jest nieskończenie wiele. Dlatego możliwe jest co najwyżej wyznaczenie dowolnego z nich, co jednak czasami jest zadowalające. (Możemy mieć np. płaszczyznę wyznaczoną przez punkt i normalną; jeżeli chcemy otrzymać wektor leżący na tej płaszczyźnie, musimy znaleźć wektor prostopadły do normalnej). Aby tego dokonać, trzeba po prostu dobrać drugi wektor do iloczynu. Jedynym wymaganiem dla niego jest to, aby nie był on równoległy z pierwszym:

  1. const VEC3 AnyOrthogonal(const VEC3& v)
  2. {
  3.    if (!Equal(v.x, v.y) || !Equal(v.y, v.z))
  4.       // cross z permutacją składowych
  5.       return Cross(v, VEC3(v.z, v.x, v.y));
  6.    else
  7.       // cross z wersorem osi X
  8.       return Cross(v, VEC3(1.0f, 0.0f, 0.0f));
  9. }

W celu wybrania drugiego argumentu możemy zastosować powyższą sztuczkę. Zamieniamy po prostu składowe pierwotnego wektora tak, aby żadna nie była na swoim miejscu – tutaj dla wektora [x, y, z] taką permutacją jest np. [z, x, y]. Taki wektor prawie na pewno nie będzie równoległy z pierwowzorem.
Wiemy jednak, że prawie robi wielką różnicę :) Problematyczne są te wektory, których wszystkie trzy elementy mają taką samą wartość. Musimy więc sprawdzić ten przypadek. Zamiast permutacji można wtedy zastosować chociażby wektor równoległy do którejś z osi układu współrzędnych.

Tags:
Author: Xion, posted under Uncategorized » 8 comments

Kolejność przekształceń macierzowych

2008-01-28 20:38

Kiedy uczyłem się biblioteki DirectX, miałem dość spore kłopoty z kwestią właściwej kolejności przekształceń opisanych przez macierze. Jak wiadomo, w grafice każdą transformację możemy opisać macierzą, a złożenie takich przekształceń możemy być reprezentowane przez odpowiedni iloczyn macierzy. Wówczas pomnożenie wektora (odpowiednio rozszerzonego o czwartą współrzędną) przez taką macierz skutkuje zastosowaniem do niego tych wszystkich przekształceń. Może być ich bardzo wiele, lecz wymagana jest tylko jedna macierz i jedno mnożenie przezeń wektora. Jest to więc efektywne, jeśli mamy dużą ilość geometrii do przetworzenia – czyli, co tu ukrywać, w zasadzie zawsze :)

Rzeczone macierze opisujące przekształcenia są kwadratowe; w przypadku grafiki 3D mają rozmiar 4×4. Dlatego też możliwe jest ich mnożenie w dowolnej kolejności. Wiemy jednak, że operacja mnożenia macierzy nie jest przemienna. Odpowiada to zresztą temu, iż przy przekształcaniu punktów w przestrzeni też liczy się kolejność: obrót, a potem przesunięcie to nie to samo, co przesunięcie, a potem obrót.
I tu się zaczyna problem, bowiem w bardzo wielu źródłach wprowadzone jest niezłe zamieszanie, jeśli chodzi o kolejność mnożenia macierzy opisujących geometryczne przekształcenia. Najczęściej pomieszane są konwencje tego, jaki porządek jest poprawny w danej bibliotece graficznej, a jaki “w matematyce”. Ostatecznie więc nie wiadomo, czy trzeba iloczyn macierzy zapisywać w kolejności, w jakiej chcemy aplikować przekształcenia, które reprezentują – czy może na odwrót. Dość prosto można oczywiście sprawdzić, jak to jest akurat w naszej bibliotece graficznej, lecz to nie mówi nic o istocie problemu…

Wektor kolumnowyWłaściwie to dopiero niedawno dowiedziałem się, gdzie jest tu pies pogrzebany. Otóż matematycy z pewnych przyczyn lubią traktować wektory jako kolumnowe, tj. jako macierze Nx1 (N wierszy, 1 kolumna). Przy takiej interpretacji tylko iloczyn w postaci:

macierz1 * wektor_kolumnowy

daje w wyniku wektor (także kolumnowy, rzecz jasna). W tym przypadku będzie on przekształcony przez macierz1. Jeżeli teraz zechcemy dodać drugie przekształcenie, to mnożenie przez odpowiednią macierz również musimy zapisać z przodu:

macierz2 * (macierz1 * wektor_kolumnowy)

Ale mnożenie jest oczywiście łączne, więc:

(macierz2 * macierz1) * wektor_kolumnowy = macierz * wektor_kolumnowy

a wynikowa macierz = macierz2 * macierz1 opisuje złożenie naszych przekształceń. Jak widać wyżej, najpierw jest stosowane to opisane przez macierz1, a dopiero potem to z macierzy2 – mimo że są one mnożone w porządku odwrotnym. Tak bowiem wygląda sprawa kolejności przekształceń dla wektorów kolumnowych.

Twórcy DirectX uznali prawdopodobnie, że jest to nieintuicyjne dla nie-matematyków i dokonali pewnego “triku”. Opiera się on na tym, że gdy w dwóch macierzach zamienimy ze sobą wiersze i kolumny – czyli dokonamy transpozycji – pomnożymy je przez siebie, a następnie transponujemy wynik, to rezultat będzie taki, jakbyśmy mnożyli wyjściowe macierze w odwrotnej kolejności. Wyjątkowo trzeba tutaj przyznać, że wzór mówi więcej niż jego opis, więc spójrzmy na ten wzór :)

(A * B)T = BT * AT

W DirectX dokonano więc transpozycji wszystkich macierzy opisujących przekształcenia. Przykładowo, funkcja D3DXMatrixTranslation zwraca macierz z wartościami przesunięć wpisanych w ostatnim wierszu, podczas gdy w wersji “matematycznej” powinny być one w ostatniej kolumnie. Podobnie jest ze wszystkimi innymi macierzami… ale także z wektorami!
Wektor wierszowyChociaż wektory z programistycznego punktu widzenia to cztery składowe i nic więcej, to w DirectX należy je traktować jako wektory wierszowe, czyli macierze 1xN. Dla nich zaś sensownym sposobem mnożenia przez macierz jest tylko następujący:

wektor_wierszowy * macierz1

Dodając kolejne przekształcenie, mamy:

(wektor_wierszowy * macierz1) * macierz2

i znów opierając się na łączności mnożenia otrzymujemy ostatecznie:

wektor_wierszowy * (macierz1 * macierz2) = wektor_wierszowy * macierz

Tutaj z kolei widać wyraźnie, że przekształcenia są stosowane w takiej samej kolejności, w jakiej odpowiadające im macierze występują w iloczynie.

Ponieważ, jak wspomniałem wyżej, cała sprawa jest kwestią czysto arbitralną (wystarczy transpozycja, aby odwrócić porządek), powinniśmy tym bardziej zwrócić na nią uwagę. A jeśli programujemy w DirectX, nie należy dopuścić do tego, by matematycy wmawiali nam ‘właściwą’ kolejność :P

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

Trójkątyzacja

2008-01-21 23:03

Wyświetlanie pojedynczych trójkątów – nawet najpiękniej pokolorowanych – to oczywiście dopiero początek. Niezależnie od tego, czy uczymy się obsługi jakiejś biblioteki 3D czy też piszemy własną, chcemy zająć się przede wszystkim wyświetlaniem brył. Rodzajów możliwych brył jest oczywiście sporo, ale te często używane i “regularne” ich rodzaje charakteryzują się przede wszystkim tym, że dają się opisać równaniami matematycznymi i zależnościami geometrycznymi. Wiemy na przykład, że sfera jest taką figurą, która składa się ze wszystkich punktów znajdujących się dokładnie w określonej odległości od danego – środka.
Problem w tym, że takich punktów jest “dość” dużo, bo nieprzeliczalnie wiele. Zarówno tą, jak i każdą inną bryłę w tradycyjnym renderingu zwykło się więc przybliżać automatycznie generowanymi trójkątami (jest to różnica np. w stosunku do raytracingu, czyli śledzenia promieni). Nazywa się to triangulacją i wymaga kilku operacji, takich jak:

  1. Wyznaczenie odpowiedniej liczby punktów należących do powierzchni. Ich ilość powinna być regulowana – tak, aby w razie potrzeby otrzymywać siatki o różnych gęstościach punktów, a więc wpływać na jakość obrazu wynikowego (i, oczywiście, szybkość rysowania).
  2. Połączenie wygenerowanych wierzchołków w trójkąty. W przypadku stosowania backface cullingu, czyli eliminowania powierzchni zwróconych przeciwnie do obserwatora, należy zadbać o właściwą kolejność wierzchołków w tych trójkątach.
  3. Obliczenie wektorów normalnych dla wierzchołków. Wiadomo, że w gotowych siatkach możliwe jest przybliżone wyznaczenie tych wektorów w oparciu o sam rozkład trójkątów i kilka iloczynów wektorowych, lecz sposób ten jest niedokładny. Nie powinniśmy też z niego korzystać, gdy mamy dodatkową informację o powierzchni, którą nasze trójkąty przybliżają. W sferze na przykład dokładnie wiadomo, że normalne muszą być równoległe do wektorów biegnących od środka do punktu na powierzchni sfery.

Przykładowa scena (siatka) Przykładowa scena (cieniowanie Gouraud)

I to w zasadzie wszystko. Warto oczywiście natychmiast sprawdzić rezultat przy pomocy renderowania w trybie wireframe, czyli rysowania tylko konturów figur. Jeśli wszystko dobrze pójdzie, to można się już pochwalić czymś więcej niż tylko jednym trójkątem, co też niniejszym czynię :)

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

Sam zrobiłem ten trójkąt!

2008-01-17 20:53

Software’owo rasteryzowany trójkątGdyby zorganizować konkurs na jak największą ilość kodu napisaną celem osiągnięcia jak najprostszego efektu, to pewnie obrazek po lewej (i kod, który za nim stoi) mógłby zająć w nim całkiem dobrą lokatę. Na pierwszy rzut oka to tylko niebieski trójkąt na żółtym tle; na drugi, trzeci i każdy następny zresztą też :) Na tym arcyprostym obrazku nie widać całego, dość skomplikowanego mechanizmu, dzięki któremu możemy go oglądać.

Rzeczony trójkąt jest bowiem efektem pracy programowego renderera, którego to od jakiegoś czasu – z konieczności, acz nie bez pewnej przyjemności – staram się popełnić. Taki kawałek oprogramowania ma za zadanie robić mniej więcej to, co potrafią zaawansowane biblioteki graficzne w rodzaju DirectX i OpenGL. Są oczywiście istotne różnice, wśród których największą jest brak wykorzystania typowych możliwości współczesnych kart graficznych – czyli właśnie przetwarzania trójkątów. Wręcz przeciwnie: wszystkie obliczenia pracowicie wykonuje główny procesor, zajmując się po kolei nie tylko każdym wielokątem, ale także każdym pikselem. Ma więc wyjątkowo dużo roboty, z którą jednak potrafi sobie poradzić.
O czym świadczy więc pokazany tutaj trójkąt? Ano o tym, że podstawowy potok renderowana ma się całkiem dobrze. W jego skład wchodzi transformowanie trójkątów przekształceniami macierzowymi, oświetlenie per-vertex, sprawdzanie widoczności pikseli przy pomocy bufora Z oraz rzutowanie perspektywiczne i rasteryzacja wynikowej płaskiej geometrii. Zgadza się, to zupełne podstawy podstaw, nieobejmujące chociażby teksturowania, lecz i tak realizujący je kod nie wiadomo kiedy rozrósł się do ponad dwóch tysięcy linijek. Faktycznie więc to był dosyć pracochłonny trójkąt :)

Zabłysnę jeszcze przykładowym kodem wykorzystującym renderer, który to wyglądać może mniej więcej tak:

  1. Device dev = new Device(/* ... */);
  2. // ...
  3. dev.clear (new Color(1.0f, 1.0f, 0.0f));
  4. dev.begin();
  5.    dev.setRenderState (RenderStates.FillMode, FillModes.Solid);
  6.    dev.setRenderState (RenderStates.ShadeModes, ShadeModes.Flat);
  7.  
  8.    // światło punktowe z pozycją i kolorem
  9.    dev.addLight (new PointLight(new Vector3(0f, 0f, -10f), new Color(1f, 1f, 1f)));
  10.  
  11.    // wierzchołek zawiera pozycję, normalną i kolor rozproszenia
  12.    Vertex[] vertices = new Vertex[]
  13.    {
  14.       new Vertex(new Vector3(-10f, 0f, -1f),
  15.          new Vector3(0f, 0f, -1f),
  16.          new Color(0f, 0f, 1f)),
  17.       new Vertex(new Vector3(0f, 2f, -1f),
  18.          new Vector3(0f, 0f, -1f),
  19.          new Color(0f, 0f, 1f)),
  20.       new Vertex(new Vector3(10f, -3f, -1f),
  21.          new Vector3(0f, 0f, -1f),
  22.          new Color(0f, 0f, 1f))
  23.    };
  24.    dev.drawTriangle (vertices);
  25. dev.end();

Inspiracje pewną popularną biblioteką w zakresie interfejsu są, jak sądzę, doskonale widoczne :)

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

Efektywne stawianie pikseli

2008-01-07 22:00

Obecnie biblioteki graficzne umieją już bardzo, bardzo dużo. W narzędziach takich jak DirectX czy OpenGL mamy ogromne możliwości efektywnego wyświetlania grafiki trójwymiarowej, a jednocześnie ich obsługa staje się z kolejnymi wersjami coraz łatwiejsza. Należy oczywiście poznać nieco matematycznych podstaw stojących za całą grafiką 3D: przekształceniami, oświetleniem, buforem głębi, i tak dalej. Zasadnicza część – czyli cały potok renderowania oraz rasteryzacja – jest już jednak wykonana za nas.
Powiedzmy jednak, że nie zadowala nas ten stan rzeczy i ze względów edukacyjnych – lub możliwych czynników zewnętrznych :) – chcemy sami dowiedzieć (w sposób najbardziej praktyczny, czyli poprzez implementację), jak to wszystko działa. Wówczas do szczęścia potrzebujemy właściwie tylko jednej operacji: wyświetlenia pojedynczego piksela w określonym kolorze. I jest tylko jedno ale: aby to wszystko działało w jakikolwiek sensowny sposób, operacja ta musi być diabelnie szybka.

Niestety (a może właśnie ‘stety’) czasy, w których łatwo możemy pisać bezpośrednio po pamięci ekranu, najwyraźniej już dawno się skończyły. Nowoczesny system operacyjny nie pozwala po prostu na takie niskopoziomowe operacje żadnym programom użytkownika. Z drugiej strony korzystanie z tego, co w zakresie wyświetlania grafiki ów system oferuje, może być niewystarczająco – pod względem wydajnościowym, rzecz jasna.
Dlatego wydaje się, że do eksperymentów dobrze nadają istniejące biblioteki graficzne – tyle że ograniczone do jednej funkcjonalności: renderowania sprite’ów punktowych (point sprites). Polega ona na rysowaniu quadów (kwadratów złożonych z dwóch trójkątów) w taki sposób, że zawsze są one zwrócone przodem do kamery. Biorąc pod uwagę to, że wielkość takich punktów możemy określić, ustawienie jej na równą jednemu pikselowi sprawi, że wyświetlanie sprite’ów punktowych będzie dobrym substytutem “stawiania” pojedyncznych pikseli.
Gdzie w zakamarkach bibliotek graficznych ukryta jest tego rodzaju funkcjonalność? Otóż:

  • W DirectX należy ustawić stan renderowania D3DRS_POINTSCALEENABLE na FALSE (co zresztą jest domyślną wartością) oraz D3DRS_POINTSIZE na 1.0f, czyli wielkość odpowiadającą jednemu pikselowi. Co ciekawe, trzeci o nazwie D3RS_POINTSPRITEENABLE wbrew pozorom lepiej jest zostawić ustawiony domyślnie na FALSE. Nie dotyczy on bowiem (co jest dość mylące) samej możliwości rysowania sprite’ów punktowych, a jedynie sposobu mapowania współrzędnych tekstur – co w przypadku symulowania pikseli nie jest nam potrzebne.
    Po tych zabiegach wstępnych możemy rysować punkty, podając format wierzchołków z polami D3DFVF_XYZRHW oraz D3DFVF_DIFFUSE przy pomocy dowolnych wywołań DrawPrimitive[UP] z typem prymitywów D3DPT_POINTLIST.
  • W OpenGL nie musimy ustawiać niczego związanego specyficznie ze sprite’tami punktowymi (bo np. ich domyślna wielkość to 1.0f). Jeśli zaś chodzi o samo rysowanie punktów to jednym ze sposobów (i pewnie nie najlepszym :)) jest ustawienie wszystkich macierzy na jednostkowe i podawanie współrzędnych punktów jako wektorów 4D z ustaloną współrzędną Z (np. 0) oraz W równą 1, do funkcji glVertex4*. Musi być ona umieszczona oczywiście między glBegin (z parametrem GL_POINTS) a glEnd, zaś kolor punktu-piksela określić możemy przez glColor*.

Jak widać, mechanizmy wspomagające nas w ręcznej, pikselowej robocie są nieco ukryte. Co więcej, w DirectX 10 na przykład sprite’y punktowe zostaną wyeliminowane z biblioteki i konieczne będzie samodzielne tworzenie odpowiednich quadów, jeśli zechcemy uzyskać podobną funkcjonalność. To prawdopodobnie słuszna decyzja, bowiem nawet w swoim głównym zastosowaniu – czyli systemach cząsteczkowych – są one zbyt ograniczone (choćby dlatego, że są cały czas zwrócone do kamery, co wyklucza możliwość obrotu cząstek). A przyznajmy, że samodzielne stawianie “pikseli” jest dość egzotyczne – co nie znaczy, że nie warto wiedzieć, jak to się robi, jeśli kiedykolwiek będzie to nam potrzebne…

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


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