Posts tagged ‘normal vectors’

Normalne w terenie

2009-11-24 22:23

Siatka terenuParę dni temu przyszło mi w końcu napisać coś, co wielu programistów grafiki pewnie już nie jeden raz miało okazję kodować. Chodzi o generowanie siatki terenu na podstawie predefiniowanej mapy wysokości (heightmap), zapisanej w pliku jako obrazek w odcieniach szarości.

Cała procedura nie jest bardzo skomplikowana. W sumie sprowadza się ona do równomiernego próbkowania obrazka mapy i tworzenia wierzchołków leżących na ustalonej płaszczyźnie, z uwzględnieniem odczytanej wysokości.
Pozycje tych wierzchołków wyznaczyć jest prosto, chociaż zależy to trochę od przyjętej reprezentacji płaszczyzny (a właściwie prostokąta) “poziomu morza”. Trochę więcej zabawy jest natomiast z wektorami normalnymi, które niewątpliwie przydadzą się, jeśli nasz teren będziemy chcieli oświetlić. Właśnie o ich znajdowaniu chciałem napisać.

Jak wiadomo, wierzchołki dowolnej siatki możemy wyposażyć w normalne, posługując się niezwykle przydatną operacją iloczynu wektorowego. Przy jego pomocy można obliczyć normalne dla poszczególnych trójkątów; w przypadku wierzchołków należy po prostu uśrednić wyniki dla sąsiadujących z nimi face‘ów (oznaczonych niżej jako T(v)):

\displaystyle n(v) = \frac{1}{|T(v)|} \sum_{(a,b,c) \in T(v)}{(b-a) \times (c-a)}

Konieczną normalizację niektórzy przeprowadzają tu na końcu, a inni dla poszczególnych trójkątów. Prawdę mówiąc nie wiem, które podejście jest właściwsze – jeśli którekolwiek.

W powyższy sposób można oczywiście wyliczać normalne również dla utworzonego terenu, bo przecież czym on jest, jak właśnie siatką :) Jednak w tym przypadku mamy dostęp do większej liczby informacji o nim. Mamy przecież źródłową mapę wysokości, z której na wierzchołki przerobiliśmy tylko niektóre piksele (plus ew. jakieś ich otoczenia). Czemu by nie wykorzystać jej w większym stopniu, generując być może lepsze przybliżenie normalnych?
Ano właśnie, dlaczego nie :) W tym celu można by wprowadzić nieco wyższej (dosłownie) matematyki i zauważyć, że nasza heightmapa jest zbiorem wartości pewnej funkcji z = f(x,y) i że wobec tego normalną w jakimś punkcie x0, y0 da się wyliczyć jako:

\displaystyle \frac{\partial z}{\partial x}(x_0, y_0) \times \frac{\partial z}{\partial y}(x_0, y_0)

o ile tylko rzeczone pochodne istnieją. Można by – ale przecież nie będziemy tego robili ;-) Zamiast tego wystarczy zastanowić się, co by było, gdybyśmy wygenerowali skrajnie gęstą siatkę dla naszego terenu: tak gęstą, że każdemu pikselowi heightmapy odpowiadałby dokładnie jeden wierzchołek tej siatki. Wówczas opisana wyżej metoda liczenia normalnych korzystałaby ze wszystkich informacji zawartych w mapie.
Nie musimy jednak generować tych wszystkich wierzchołków. Do obliczenia wektora normalnego w punkcie wystarczą tylko dwa, odpowiadające – na przykład – pikselowi heightmapy położonemu bezpośrednio na prawo i u dołu tego, z którego “wzięliśmy” dany wierzchołek siatki. Z tych trzech punktów możemy następnie złożyć trójkąt, obliczyć wektor normalny i zapisać go w wierzchołku siatki:

n(v) = (\mathit{pos3d}(p_{x+1,y}) - v) \times (\mathit{pos3d}(p_{x,y+1}) - v) \\ \text{gdzie} \quad v = \mathit{pos3d}(p_{x,y})

Tutaj p_{x,y} oznacza odpowiedni piksel mapy wysokości, a funkcja pos3d jest tą, która dla owego pikseli potrafi wyliczyć pozycję odpowiadającego mu wierzchołka w wynikowej siatce. (Taką funkcję mamy, bo przecież jakoś generujemy tę siatkę, prawda? :])

Wektor normalnyZ podanych sposobów obliczania normalnych terenu można oczywiście korzystać niezależnie od tego, z jaką biblioteką graficzną pracujemy. Jak to jednak często bywa, w DirectX sporo rzeczy mamy zaimplementowanych od ręki w postaci biblioteki D3DX i nie inaczej jest z liczeniem normalnych.
I tak funkcja D3DXComputeNormals potrafi wyliczyć wektory normalne dla dowolnej siatki – warunkiem jest to, żeby była ona zapisana w postaci obiektu ID3DXMesh, więc w razie potrzeby musielibyśmy takowy obiekt stworzyć. Z kolei D3DXComputeNormalMap potrafi stworzyć mapę normalnych na podstawie mapy wysokości; tę pierwszą możemy później indeksować w celu pobrania “wektorów normalnych pikseli”.

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

Magiczna interpolacja

2007-10-07 16:36

Shaderami ostatni raz zajmowałem się (dość teoretycznie) parę lat temu, więc od tego czasu minęła już cała epoka – albo i kilka. W ramach przypomnienia oraz nauki HLSLa (bardzo prostego i przyjemnego języka swoją drogą) przeglądam więc implementację różnych technik graficznych, głównie oświetlenia i cieniowania.
Przy okazji natrafiłem tam na pewnego rodzaju shaderową sztuczkę. Bardziej zaawansowanym programistom grafiki pewnie nie będzie się ona wydawała niczym nadzwyczajnym. Dla mnie jednak jej pomysłowość i prostota była – że użyję modnego ostatnio słowa – porażająca :)

Otóż, jak powszechnie wiadomo, programowalny potok grafiki to przede wszystkim dwie części: vertex i pixel shader. W tym pierwszym transformujemy wierzchołki i wyliczamy inne ich dane, które potem trafiają do shadera pikseli. Ten zaś z racji częstości wykonywania musi być jak najlżejszy i dlatego jak najwięcej operacji trzeba wykonywać per vertex. Z drugiej strony, aby cokolwiek ładnie wyglądało (oświetlenie, nierówności, itd.) musi być liczone per pixel.
I tutaj pomaga część potoku włączająca się między shaderami, czyli interpolator. Jego głównym zadaniem jest interpolacja wartości koloru rozproszenia i współrzędnych tekstur na poszczególnych pikselach. To dzięki niemu piksel w środku trójkąta może mieć właściwy kolor rozproszenia i mieć przypisany odpowiedni teksel.

Interpolacja normalnychTrik polega na tym, że w interpolować możemy też inne dane. Typowy przykład to choćby pozycja w przestrzeni 3D – już przetransformowana, ale jeszcze nie zrzutowana na płaszczyznę projekcji. Podobnie może być z wektorami normalnymi. Już te dwie dane (plus kierunek światła) wystarczają, by otrzymać proste oświetlenie per pixel, które wygląda realistycznie, jeżeli tylko powierzchnia nie wymaga mapowania nierówności.
Żeby to wszystko było możliwe, wystarczy nieco oszukać sprzęt i oznaczyć określone pole wyjścia vertex shadera – zawierające np. wspomnianą normalną czy pozycję – jako… współrzędne tekstury. Wtedy zostaną poddane interpolacji i wyliczone w ten sposób wartości będą dostępne w pixel shaderze. A tam już wcale nie musimy ich traktować jako koordynaty tekstury i znów mogą być pozycją lub normalną.

W sumie wspomniane proste oświetlenie per pixel to kwestia użycia takich dwóch shaderów:

  1. float4 vLightPos; // pozycja światła
  2.  
  3. // macierze przekształceń
  4. float4x4 mtxRotation;  // obrót (dla normalnych)
  5. float4x4 mtxTranslationAndScaling; // translacja i obrót
  6. float4x4 mtxViewProjection; // widok i projekcja
  7.  
  8. // wyjście VS i wejście PS
  9. struct Vertex2Pixel
  10. {
  11.    // zwykłe dane wierzchołka
  12.    float4 Pos   : POSITION;
  13.    float4 Diffuse : COLOR0;
  14.    float2 Tex0   : TEXCOORD0;
  15.    
  16.    // dane dla oświetlenia per pixel
  17.    float3 Normal : TEXCOORD1;
  18.    float3 Pos3D : TEXCOORD2;
  19. };
  20.  
  21.  
  22. // vertex shader
  23. Vertex2Pixel VS(float4 inPos : POSITION, float4 inDiffuse : COLOR0,
  24. float4 inNormal : NORMAL, float2 inTex0 : TEXCOORD0)
  25. {
  26.    Vertex2Pixel v2p = (Vertex2Pixel)0;
  27.  
  28.    // liczenie macierzy transformacji (preshader)
  29.    float4x4 mtxWorld = mul(mtxRotation, mtxTranslationAndScaling);
  30.    float4x4 mtxWVP = mul(mtxWorld, mtxViewProjection);
  31.  
  32.    // transformacja wierzchołka
  33.    v2p.Pos = mul(inPos, mtxWVP);
  34.    v2p.Pos3D = v2p.Pos;   // dla interpolacji
  35.  
  36.    // transformacja (obrót) normalnych
  37.    v2p.Normal = mul(inNormal, mtxRotation); // też będzie interpolowane
  38.  
  39.    // reszta
  40.    v2p.Tex0 = inTex0;
  41.    v2p.Diffuse = inDiffuse;
  42.    return v2p;
  43. }
  44.  
  45. // pixel shader
  46. sampler Tex;
  47. float4 PS(Vertex2Pixel inV2P) : COLOR0
  48. {
  49.    Pixel px = (Pixel)0;
  50.  
  51.    // obliczenie oświetlenia dla piksela
  52.    // (korzystamy z interpolowanej pozycji piksela i z interpolowanej normalnej)
  53.    float3 vPixel2Light = normalize(vLightPos - inV2P.Pos3D);
  54.    float fLightFactor = dot(vPixel2Light, inV2P.Normal);
  55.  
  56.    // liczymy kolor (tekstura + kolor rozproszenia + oświetlenie)
  57.    float4 clDiffuse = tex2D(Tex, inV2P.Tex0) * inV2P.Diffuse * fLightFactor;
  58.    return clDiffuse;
  59. }

Oświetlenie per pixel
Różnica między oświetleniem wierzchołkowym a pikselowym
Źródło

Oczywiście nie ma tutaj wygaszania ani kontroli kształtu światła, ale to i tak długi przykład ;) Widać jednak, że to co kosztowne – przekształcenia macierzowe – są wykonywane dla wierzchołków, a nie pikseli. W pixel shaderze liczymy tylko oświetlenie, a to – jak wiemy i widzimy powyżej – tylko kwestia odpowiedniego iloczynu skalarnego. Możemy go obliczyć, bo w wyniku interpolacji mamy zarówno pozycję piksela w przestrzeni, jak i jego normalną.

A efekty wyglądają mniej więcej tak:

Screen prostego oświetlenia per pixel Screen prostego oświetlenia per pixel

 


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