Posts tagged ‘graphics programming’

Triki z PowerShellem #4 – DirectX

2008-06-10 11:13

Jakiś czas temu pokazałem, że w PowerShellu można zrobić rzeczy, które są – jak na konsolkę tekstową – co najmniej niespotykane. Przedstawiłem na przykład sposób na wyświetlenie zwykłego windowsowego okienka. Nie było ono aczkolwiek zbyt interesujące, jako że w środku było zupełnie puste – chociaż, oczywiście, Aero robił co mógł, by wyglądało ono na atrakcyjniejsze niż było w rzeczywistości ;)
Jako że zajmowania się rzeczami zupełnie nieprzydatnymi nigdy dość, postanowiłem więc pewnego razu spróbować wnieść w stworzoną formę nieco więcej życia. Efekty okazały się raczej ciekawe, przedstawiając się następująco:

DirectX w PowerShellu - trójkąt (screen 1) DirectX w PowerShellu - trójkąt (screen 2) DirectX w PowerShellu - trójkąt (screen 3)

Tak, to jest trójkąt. Tak, on się obraca. Tak, na pasku tytułowym okna jest napisane Direct3D… Wszystkie te trzy fakty nie są żadnym zbiegiem okoliczności :) Za wszystko odpowiedzialny jest ten ono skrypt:

  1. # D3D.ps1 - próba użycia Direct3D w PowerShellu
  2.  
  3. # Ładujemy assemblies
  4. [Reflection.Assembly]::LoadWithPartialName("System.Windows.Forms") | Out-Null
  5. [Reflection.Assembly]::LoadWithPartialName("System.Drawing") | Out-Null
  6. [Reflection.Assembly]::LoadWithPartialName("Microsoft.DirectX") | Out-Null
  7. [Reflection.Assembly]::LoadWithPartialName("Microsoft.DirectX.Direct3D") | Out-Null
  8.  
  9. # Tworzymy okienko
  10. $form = New-Object Windows.Forms.Form
  11. $form.ControlBox = $false # Bo zamykanie niespecjalnie działa :P
  12. $form.Text = "Direct3D"
  13.  
  14. # Tworzymy urządzenie
  15. $pp = New-Object Microsoft.DirectX.Direct3D.PresentParameters
  16. $pp.BackBufferCount = 1
  17. $pp.Windowed = $true
  18. $pp.SwapEffect = [Microsoft.DirectX.Direct3D.SwapEffect]::Discard
  19. $pp.DeviceWindow = $form
  20. $pp.PresentationInterval = [Microsoft.DirectX.Direct3D.PresentInterval]::Immediate
  21. $dev = New-Object Microsoft.DirectX.Direct3D.Device @(0,
  22.     [Microsoft.DirectX.Direct3D.DeviceType]::Hardware, $form,
  23.     [Microsoft.DirectX.Direct3D.CreateFlags]::HardwareVertexProcessing, $pp)
  24.    
  25. # Przygotowujemy się do rysowania
  26. $dev.RenderState.Lighting = $false  # Wyłączamy oświetlenie
  27. $tri = New-Object Microsoft.DirectX.Direct3D.CustomVertex+PositionColored[] @(3)
  28. $tri[0] = New-Object Microsoft.DirectX.Direct3D.CustomVertex+PositionColored @(
  29.     0.0, 0.0, 0.5, [Drawing.Color]::Blue.ToArgb())
  30. $tri[1] = New-Object Microsoft.DirectX.Direct3D.CustomVertex+PositionColored @(
  31.     0.5, -0.5, 0.5, [Drawing.Color]::Red.ToArgb())
  32. $tri[2] = New-Object Microsoft.DirectX.Direct3D.CustomVertex+PositionColored @(
  33.     -0.5, -0.5, 0.5, [Drawing.Color]::Green.ToArgb())
  34.  
  35. # Wyświetlamy
  36. [Windows.Forms.Application]::EnableVisualStyles()
  37. $form.Show()
  38. $time = 0
  39. $lastTickCount = [Environment]::TickCount
  40.  
  41. # Pętlimy się
  42. while ($time -lt 5000) {
  43.     # Przetwarzanie komunikatów
  44.     [Windows.Forms.Application]::DoEvents()
  45.     $time += [Environment]::TickCount - $lastTickCount
  46.     $lastTickCount = [Environment]::TickCount
  47.    
  48.     # Rysujemy
  49.     $dev.Clear([Microsoft.DirectX.Direct3D.ClearFlags]::Target,
  50.         [Drawing.Color]::Lime, [float]1.0, 0)
  51.     $dev.BeginScene()
  52.         # Ustawiamy macierz obrotu
  53.         $angle = (10 * $time / 1000.0) / (2 * [Math]::PI)
  54.         $dev.Transform.World = [Microsoft.DirectX.Matrix]::RotationZ($angle)
  55.        
  56.  
  57.         # Rysujemy trójkąt
  58.         $dev.VertexFormat = [Microsoft.DirectX.Direct3D.CustomVertex+PositionColored]::Format
  59.         $dev.DrawUserPrimitives([Microsoft.DirectX.Direct3D.PrimitiveType]::TriangleList,
  60.             1, $tri)
  61.     $dev.EndScene()
  62.     $dev.Present()
  63. }

Przyznam, że byłem mocno zaskoczony, iż taki trik jest w ogóle możliwy przy pomocy czegoś, co w założeniu jest tylko powłoką tekstową dla poleceń administracyjnych. A tu proszę: DirectX w całej (aczkolwiek zarządzanej) okazałości!
Nietrudno rzecz jasna zauważyć, że powyższy skrypt jest w dużym stopniu tłumaczeniem na język PowerShella odpowiedniego kodu, który mógłby zostać napisany w dowolnym języku programowania z platformy .NET. Trzeba było jedynie poradzić sobie z pewnymi niedogodnościami, jak na przykład koniecznością ręcznego załadowania assemblies czy jakimś obejściem braku sensownej możliwości reakcji na zdarzenia – tutaj “program” po prostu kończy się po 5 sekundach.

Właśnie ta niemożność sprawia, że tandem PowerShell + DirectX zapewne nie ma przed sobą świetlanej przyszłości w grach :) Przez chwilę zastanawiałem się, czy wobec tego nie da się go użyć do tworzenia dem… Niestety, już powyższy skrypt zajmuje ponad 2 kilobajty, nie potrafiąc przy tym zbyt wiele, więc ta możliwość też prawdopodobnie odpada.
Pozostaje zatem pokazywanie podobnych trików linuksowcom i wytykanie im, że ich bash czegoś takiego nie potrafi ;D

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

Nadmuchiwanie obiektów

2008-06-08 15:24

Od kilku dni nadspodziewanie popularnym sportem jest piłka nożna, co zapewne nie jest przypadkowe ;-) A jeśli już chodzi o piłkę, to musi być ona odpowiedniej jakości. I mówię tu o tym niewielkim, prawie okrągłym obiekcie: aby był przydatny, musi być… nadmuchany :) I właśnie o ‘nadmuchiwaniu’ powiem dzisiaj słów kilka.
Chodzi mi oczywiście o graficzny efekt rozszerzania się obiektu 3D, wyglądający tak, jakby ktoś w ów obiekt pompował powietrze. Można by pomyśleć, że osiągnięcie go jest niezmiernie trudne, bo przecież wchodząca w grę mechanika płynów (ruch gazu “wypełniającego” obiekt) jest jakąś kosmiczną fizykę. Ale – jak to często bywa – przybliżony a wyglądający niemal równie dobrze rezultat możemy dostać o wiele mniejszym wysiłkiem.

Jak? Sztuczka jest bardzo prosta. Aby nasz mesh zaczął się rozciągać na wszystkie strony, należy po prostu odpowiednio poprzesuwać mu wierzchołki. Wspomniane ‘wszystkie strony’ oznaczają tak naprawdę tyle, że każdy punkt powierzchni obiektu oddala się od niej w kierunku prostopadłym. Kierunek tego ruchu jest wyznaczony po prostu przez normalną.
Efekt jest w istocie trywialny i jego kod w HLSL/Cg może wyglądać na przykład tak:

  1. float fPumpFactor;  // współczynnik nadmuchania obiektu
  2.  
  3. struct VS_INPUT
  4. {
  5.     float3 Position : POSITION;
  6.     float3 Normal : NORMAL;
  7.     // ...
  8. }
  9.  
  10. void vs_main(VS_INPUT In, /* ... */)
  11. {
  12.     // nadmuchujemy
  13.     In.Position += fPumpFactor * normalize(In.Normal);
  14.  
  15.     // (reszta vertex shadera)
  16. }

Wśród zastosowań tego prostego triku można wymienić chociażby wyświetlanie poświaty wokół obiektów. Wystarczy do tego narysować obiekt najpierw w wersji zwykłej, a potem półprzezroczystej i nieco napompowanej.

A tak prezentują się rezultaty, gdy zaczniemy nadmuchiwać poczciwą futbolówkę:

Nadmuchana piłka nożna (PF = -1) Nadmuchana piłka nożna (PF = 2) Nadmuchana piłka nożna (PF = 8)

Na Euro jak znalazł ;]

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

Potęga w 4 kilobajtach

2008-05-09 19:49
Screen z demka 4k autorstwa Charibo
Screen z demka 4k
autorstwa Charibo

Ostatnio na Warsztacie zapanował niecodzienny pęd do sceny – demosceny, rzecz jasna. Jego główną przyczyną, a może i skutkiem, jest oczywiście rozpoczynający się dzisiaj konkurs na najlepsze demo 4k. Pod tym tajemniczym skrótem kryje się maksymalny rozmiar takiego dema (co obejmuje plik wykonywalny i wszystkie wykorzystywane dane), który nie może przekroczyć granicy 4096 bajtów.
“A co można napisać na takim malutkim kawałeczku?”, pewnie chciałoby się spytać. Okazuje się, że całkiem sporo – pod warunkiem, że znamy kilka sztuczek oraz korzystamy z odpowiednich narzędzi. Jest to na przykład specjalny linker, który dokonuje wyjątkowo efektywnej kompresji wynikowego pliku. Oprócz należy też odpowiednio skonfigurować kompilator, włączając wszelkie optymalizacje rozmiaru generowanego kodu (kosztem jego szybkości) oraz pozbywając się niepotrzebnych dodatków.

Takich dodatkiem jest na przykład biblioteka czasu wykonania (runtime). Jej wykluczenie sprawia jednak, że tracimy kilka mechanizmów języka C++, jak chociażby część funkcji matematycznych. Do szczególnie dotkliwych może należeć na przykład brak operacji potęgowania w jakiejkolwiek formie – zarówno funkcji pow, jak i exp. Jeśli ich potrzebujemy, musimy sami je sobie zapewnić, co w przypadku niecałkowitego wykładnika może być kłopotliwe. Chyba że odwołamy się bezpośrednio do jednostki zmiennoprzecinkowej – na przykład tak:

  1. float pow(float a, float b)  // a^b
  2. {
  3.     __asm
  4.     {
  5.         fld1
  6.         fld a
  7.         fyl2x
  8.         fld b
  9.         fmulp ST(1), ST(0)    // na wierzchu b * log2(a)
  10.        
  11.         // obliczenie 2^(b * log2(a)), czyli a^b
  12.         fst ST(1)
  13.         frndint
  14.         fld1
  15.         fscale
  16.         fxch ST(1)
  17.         fsubr ST(0), ST(2)
  18.         f2xm1
  19.         fld1
  20.         faddp ST(1), ST(0)
  21.         fmulp ST(1), ST(0)
  22.         fstp ST(1)
  23.     }
  24. }

Samodzielnie wyprodukowanie tego kawałka nie było aczkolwiek takie proste (głównie ze względu na kłopotliwe wymagania co do argumentu instrukcji F2XM1, która dokonuje potęgowania). Dlatego prezentuję go tu dla potomności i pożytku wszystkich tworzących małe dema, licząc, że komuś może się przydać :)

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

Podział wielokątów na trójkąty

2008-04-27 21:39

Podział wypukłego wielokąta na trójkątyKolega rr- rzucił dzisiaj na kanale #warsztat ciekawy problem rysowania dowolnych wielokątów za pomocą samych trójkątów. Jak wiadomo, karty graficzne posługują się właśnie trójkątami, zatem w celu wyświetlenia wielokąta należy go odpowiednio podzielić. Matematycy dowodzą, że jest to zawsze możliwe, i podają prosty sposób dla wielokątów wypukłych: należy po prostu narysować wszystkie przekątne wychodzące z jednego wierzchołka. Nie jest trudno przełożyć ten przepis na kod.

Co jednak z dowolnymi wielokątami? Tu sprawa wydaje się bardziej skomplikowana, chociaż – jak się ukazuje – rysowanie przekątnych można na ten przypadek uogólnić. Wpadłem jednak na inny pomysł, polegający na odpowiednim “chodzeniu” po wierzchołkach naszego wielokąta i “odcinaniu” od niego kolejnych trójkątów brzegowych. Wygląda to mniej więcej tak:

  1. Startujemy od dowolnego wierzchołka wielokąta.
  2. Mając i-ty wierzchołek sprawdzamy, czy da się go połączyć z (i+2)-im (modulo liczba wierzchołków) tak, aby powstały przy tym odcinek mieścił się w wielokącie:
    • jeśli tak, to z wierzchołków: i-tego, (i+1)-ego i (i+2)-ego tworzymy nowy trójkąt, wierzchołek (i+1)-szy usuwamy z wielokąta i przechodzimy do (i+2)-ego
    • jeśli nie da się tak połączyć wierzchołków, przechodzimy do wierzchołka (i+1)-ego i próbujemy dalej
  3. Po wykonaniu pełnego cyklu kontynuujemy przechodzenie po wielokącie, usuwanie wierzchołków i tworzenie trójkątów – aż do momentu, gdy sam nasz wielokąt stanie się trójkątem. Będzie to oczywiście ostatni składający się na niego trójkąt.

Na tym rysunku można prześledzić, jak wyglądają kolejne cykle spaceru po krawędziach wielokąta – oznaczyłem je różnymi kolorami:

Podział dowolnego wielokąta na trójkąty

Z tym sposobem wiąże się oczywiście problem stwierdzenia, czy dany odcinek należy do wielokąta – co w ogólności nie musi być takie proste ani efektywne (może mieć złożoność liniową). Dodatkowo wielokąt może być “wredny” i niezbyt dobrze poddawać się operacji obcinania trójkątów. Na szczęście można udowodnić, że w każdym cyklu da się przynajmniej jeden taki trójkąt wyodrębnić. Te dwa fakty powodują, że cała operacja może mieć złożoność sięgającą nawet O(n3), chociaż pewnie da się ją zaimplementować lepiej.
Jest naturalnie bardzo możliwe, że algorytm ten jest znany od dawna, a ja po prostu nie przeczesałem Internetu dość dokładnie w poszukiwaniu już istniejącego opisu. Jednak biorąc pod uwagę to, co przed chwilą powiedziałem o jego możliwej “efektywności”, nie jest to znów takie pewne ;-) Istnieje aczkolwiek szansa, że może się on przydać komuś, kto implementuje bibliotekę graficzną 2D w oparciu o API w rodzaju DirectX czy OpenGL.

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

MRT i techniki typu deferred

2008-03-16 21:42

Zdążyliśmy się już przyzwyczaić do tego, że w programowaniu grafiki bardzo często wykorzystujemy zastane mechanizmy w zupełnie inny sposób niż ten formalnie założony. I tak: tekstury nie muszą być tylko bitmapami nakładanymi na geometrię, ich rzekome współrzędne mogą tak naprawdę przechowywać pozycję 3D lub normalne, a piksele nie muszą wcale zawierać danych o kolorach.
Analogicznie potok renderowania nie musi też produkować pikseli do wyświetlenia na ekranie! Zamiast tego możliwe jest, aby dokonywał on najpierw wyliczania pewnych właściwości materiałów dla każdego piksela na powierzchni ekranu i to właśnie te informacje zapisywał jako wynikowy “kolor” w teksturze typu Render Target. Później byłyby one wykorzystywane do obliczeń związanych np. z oświetleniem i cieniowaniem sceny. Zaletą takiego podejścia (znanego powszechnie jako Deferred Shading) jest między innymi oszczędzenie sobie wielokrotnego przetwarzania wierzchołków przez vertex shader, np. wtedy, gdy potrzebnych jest wiele przebiegów dla wielu świateł. Z drugiej strony w ten sposób znacznie bardziej obciąża się jednostki Pixel Shader, których liczba w starszych karta jest stała i niezbyt duża. Korzyścią jest jednak uproszczenie całego procesu renderowania, nawet jeśli z powyższego opisu nieszczególnie to wynika ;-)

Pozostaje jeszcze pewien drobny szkopuł. Otóż materiały mają wiele parametrów, które często są złożone, i nie da się ich wszystkich upakować w pojedynczym pikselu, składającym się jedynie z czterech wartości zmiennoprzecinkowych. Dlatego też stosuje się tutaj “wielokrotne cele renderowania” (Multiple Render Targets – MRT), a więc produkowanie przez potok więcej niż jednej wartości koloru naraz. Zwykle po prostu każdy parametr materiału jest zapisywany osobno, do innej powierzchni typu Render Target. Rozróżnienie odbywa się na poziomie pixel shadera. Zamiast zwracać jedną wartość o semantyce COLOR0 (która domyślnie trafia do bufora ramki, czyli – w przybliżeniu – na ekran), może on wypluć także COLOR1, COLOR2 i tak dalej:

Pixel shader dla MRT

Rezultaty te chcielibyśmy rzecz jasna odebrać i zachować, ale w DirectX wystarcza do tego zwykła metoda SetRenderTarget, której podajemy powierzchnię działającą jako Render Target o danym indeksie. Są tutaj oczywiście pewne obostrzenia (łącznie z tym najbardziej oczywistym – rozmiaru powierzchni).
Największym mankamentem jest jednak to, że jedynie stosunkowo nowe karty (np. nVidii od serii 6) obsługują MRT. Można to sprawdzić, czytając wartość pola NumSimultaneousRTs struktury D3DCAPS, które da nam – niezbyt imponującą, bo wynoszącą zwykle 4 lub 8 – maksymalną liczbę Render Targets podpiętych jednocześnie. W tak niewielkiej ilości (zwłaszcza, jeśli równa jest ona nawet mniej, czyli… 1 :]) może być niełatwo zmieścić wszystkie potrzebne informacje o materiałach.

Ale w programowaniu grafiki zwykle bywa tak, że najlepiej jest wybierać techniki, które jeszcze wydają się nowe. Wtedy bowiem jest całkiem prawdopodobne, że w chwili kończenia projektu wsparcie dla nich będzie już powszechne. Zważywszy na to, że w moim przypadku ciężko jest powiedzieć, kiedy pisanie właściwego potoku renderowania zdołam chociaż zacząć – o zakończeniu nie wspominając – techniki typu deferred zdają się być całkiem rozsądnym wyborem do rozważenia :)

Nie zabijaj pikseli swoich nadaremno

2008-02-20 15:12

Począwszy od wersji 1.1, w pixel shaderach istnieje instrukcja texkill, której odpowiednikiem w HLSL jest funkcja clip. Działanie ich obu polega z grubsza na całkowitym odrzuceniu danego piksela, czyli nierysowaniu go. Shader wprawdzie nadal produkuje jakiś rezultat, ale nie jest on uwzględniany, a więc rzeczony piksel po prostu nie pojawia się na ekranie.

Biorąc pod uwagę to, że w kodzie pixel shadera możemy już całkiem sporo, można by uznać, że texkill to niezłe narzędzie do eliminowania niechcianych fragmentów sceny (przycinanych np. ustalonymi płaszczyznami). Lecz jak zawsze jest pewne ‘ale’, a nawet kilka. Oto one:

  • Wykonanie instrukcji texkill nie powoduje natychmiastowego zakończenia działania shadera dla danego piksela. Jak wiadomo, piksele (wierzchołki zresztą też) są przetwarzane na GPU równolegle i dlatego nie ma dla nich odpowiednika instrukcji ret(urn) ze zwykłych programów.
  • Piksel usunięty przez texkill może zepsuć anti-aliasing. Shader jest bowiem wywoływany tylko raz dla jednego piksela, zaś ponownie wygładzenie krawędzi wymagałoby modyfikacji koloru pikseli sąsiadujących z tym usuniętym – czyli ponownego uruchomienia dla nich pixel shadera.
  • Używanie instrukcji texkill uniemożliwia szybkie odrzucenie pikseli poprzez tzw. wczesny Z-Test, czyli test dokonywany przed uruchomieniem pixel shadera. Skoro bowiem piksel może zniknąć z innych powodów, to testowanie głębokości trzeba zostawić na później (chyba że wyłączony został zapis do Z bufora).

Nie znaczy to oczywiście, że texkill jest zły, bo w niektórych sytuacjach bywa nieoceniony. Przykładem jest choćby opisane przez Blinda i Rega rzucanie cieni przez obiekty z częściowo przezroczystymi teksturami. Ważne, aby z tego “triku” korzystać ze świadomością możliwych konsekwencji – wydajnościowych, rzecz jasna.

Tags: ,
Author: Xion, posted under Programming » Comments Off on Nie zabijaj pikseli swoich nadaremno
 


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