Electrics / raymarching-workshop

stworzony i zaprezentowany przez AJ Weeks & Huw Bowles

przegląd

renderowanie obrazu polega na określeniu koloru każdego piksela na obrazie, co wymaga ustalenia, jaka powierzchnia znajduje się za pikselem na świecie, a następnie „cieniowania” go, aby obliczyć ostateczny kolor.

procesory graficzne bieżącej generacji pobierają siatki trójkątów jako dane wejściowe, rasteryzują je na piksele (zwane fragmentami, zanim zostaną narysowane na wyświetlaczu), a następnie cieniują je, aby obliczyć ich udział w obrazie. Chociaż ten rurociąg jest obecnie wszechobecny, jest to również skomplikowany i niekoniecznie najlepszy sposób na naukę Grafiki.

alternatywnym podejściem jest rzucenie promienia przez każdy piksel i przecięcie go z powierzchniami na scenie, a następnie obliczenie cieniowania.

ten kurs wprowadza jedną technikę raycastingu poprzez „pola odległości”. Pole odległości to funkcja, która zwraca, jak blisko danego punktu znajduje się najbliższa powierzchnia sceny. Odległość ta określa promień kuli pustej przestrzeni wokół każdego punktu. Pola odległości podpisanej (SDF) są polami odległości, które są zdefiniowane zarówno wewnątrz, jak i na zewnątrz obiektów; jeśli zapytana Pozycja znajduje się „wewnątrz” powierzchni, jej odległość będzie podawana jako ujemna, w przeciwnym razie będzie dodatnia.

co jest możliwe z Rayem?

gra „Claybook” wykorzystuje wyłącznie pola odległości do reprezentowania sceny. Daje mu to wiele ciekawych możliwości, takich jak całkowicie dynamiczne topologie powierzchni i morfing kształtów. Efekty te byłyby bardzo trudne do osiągnięcia w przypadku siatek trójkątnych. Inne korzyści obejmują łatwe do wdrożenia i wysokiej jakości miękkie cienie i ambient occlusion.

https://www.claybookgame.com/

poniższy obraz został również renderowany w czasie rzeczywistym przy użyciu technik, które omówimy dzisiaj (plus wiele fantazyjnych technik,w które nie będziemy mieli czasu zanurkować).

możesz go uruchomić na żywo w przeglądarce tutaj: https://www.shadertoy.com/view/ld3Gz2

używając SDF (signed distance field), geometria tej sceny nie musiała być tworzona w DCC takim jak Maya, ale jest reprezentowana całkowicie parametrycznie. To sprawia, że animowanie kształtu jest trywialne, po prostu zmieniając wejścia do funkcji mapowania sceny.

inne efekty graficzne są prostsze przez raymarching w porównaniu z tradycyjnymi alternatywami rasteryzacji. Rozpraszanie podpowierzchniowe, na przykład, wymaga po prostu wysłania kilku dodatkowych promieni na powierzchnię, aby zobaczyć, jak gruba jest. Ambient occlusion, anti-aliasing i głębia ostrości to trzy inne techniki, które wymagają tylko kilku dodatkowych linii, a jednocześnie znacznie poprawiają jakość obrazu.

Raymarching distance fields

będziemy maszerować wzdłuż każdego promienia i szukać skrzyżowania z powierzchnią w scenie. Jednym ze sposobów, aby to zrobić, jest rozpoczęcie od źródła promienia (na płaszczyźnie kamery) i podjęcie jednolitych kroków wzdłuż promienia, oceniając pole odległości w każdym punkcie. Gdy odległość do sceny jest mniejsza niż wartość progowa, wiemy, że uderzyliśmy w powierzchnię i dlatego możemy wtedy zakończyć raymarch i cień tego piksela.

bardziej wydajnym podejściem jest użycie odległości zwracanej przez SDF do określenia następnego rozmiaru kroku. Jak wspomniano powyżej, odległość zwracana przez SDF może być traktowana jako promień kuli pustej przestrzeni wokół punktu wejściowego. Dlatego bezpiecznie jest kroczyć o tę ilość wzdłuż promienia, ponieważ wiemy, że nie przejdziemy przez żadne powierzchnie.

w poniższej reprezentacji 2D raymarching, środek każdego okręgu jest miejscem, z którego próbkowano scenę. Promień był następnie maszerowany wzdłuż tej odległości (rozciągając się do promienia okręgu), a następnie ponownie próbkowany.

jak widać, próbkowanie SDF nie daje dokładnego punktu przecięcia promieni, ale raczej minimalną odległość, którą można pokonać bez przechodzenia przez powierzchnię.

gdy odległość ta jest poniżej pewnego progu, raymarch kończy piksel może być zacieniony na podstawie właściwości powierzchni, z którą się przecinają.

Pobaw się tym shaderem w przeglądarce tutaj: (Kliknij i przeciągnij obraz, aby ustawić kierunek promienia) https://www.shadertoy.com/view/lslXD8

porównanie do ray tracingu

w tym momencie można zapytać, dlaczego nie obliczamy przecięcia ze sceną bezpośrednio za pomocą matematyki analitycznej, używając techniki zwanej Ray Tracingiem. W ten sposób renderowanie offline zazwyczaj działa – wszystkie trójkąty na scenie są indeksowane do jakiejś struktury danych przestrzennych, takiej jak hierarchia objętości (BVH) lub drzewo KD, które umożliwiają efektywne przecięcie trójkątów położonych wzdłuż promienia.

zamiast tego raymarchujemy pola dystansowe, ponieważ:

  • wdrożenie procedury odlewania promieni jest bardzo proste
  • unikamy całej złożoności implementacji przecięć promieni-trójkątów i struktur danych BVH
  • nie musimy tworzyć wyraźnej reprezentacji sceny – siatki trójkątów, koordy tex, kolory itp
  • korzystamy z szeregu przydatnych funkcji pól odległości, z których niektóre są wymienione powyżej

powiedziawszy to powyżej, istnieje kilka eleganckich/prostych punktów wejścia w ray tracing. Ray Tracing w jeden Weekend darmowa książka (i kolejne rozdziały) są bardzo polecane i są niezbędną lekturą dla wszystkich zainteresowanych grafiką.

zaczynajmy!

ShaderToy

ShaderToy jest stroną internetową i platformą do tworzenia shaderów do przeglądania, udostępniania i omawiania shaderów.

chociaż możesz od razu wejść i zacząć pisać nowy shader bez tworzenia konta, jest to niebezpieczne, ponieważ możesz łatwo stracić pracę, jeśli wystąpią problemy z połączeniem lub zawiesisz GPU (łatwo to zrobić, np. tworząc nieskończoną pętlę).Dlatego zdecydowanie zalecamy utworzenie konta (jest szybkie / łatwe / bezpłatne), wpisując tutaj: https://www.shadertoy.com/signin i regularnie oszczędzając.

aby uzyskać przegląd ShaderToy i przewodnik po rozpoczęciu, zalecamy skorzystanie z samouczka takiego jak ten z @The_ArtOfCode: https://www.youtube.com/watch?v=u5HAYVHsasc. Podstawy tutaj są niezbędne, aby śledzić resztę warsztatu.

2D SDF demo

oferujemy prosty framework do definiowania i wizualizacji pól odległości podpisanych 2D.

https://www.shadertoy.com/view/Wsf3Rj

przed zdefiniowaniem pola odległości wynik będzie całkowicie biały. Celem tej sekcji jest zaprojektowanie SDF, który nadaje pożądany kształt sceny (biały kontur). W kodzie odległość ta jest obliczana przez funkcję sdf(), która jako wejście otrzymuje pozycję 2D w przestrzeni. Koncepcje, których się tutaj nauczysz, uogólnią się bezpośrednio na przestrzeń 3D i pozwolą na modelowanie sceny 3D.

Start prosty-spróbuj najpierw użyć komponentu x lub y punktu p i obserwuj wynik:

float sdf(vec2 p){ return p.y;}

wynik powinien wyglądać następująco:

zielony oznacza powierzchnie „zewnętrzne”, czerwony oznacza powierzchnie „wewnętrzne”, biała linia wyznacza samą powierzchnię, a cieniowanie w regionach wewnętrznych/zewnętrznych ilustruje odległość iso-lines – lines w ustalonych odległościach. W 2D ten SDF modeluje poziomą linię w 2D w y=0. Jaki rodzaj geometrycznego prymitywu reprezentowałby to w 3D?

kolejną dobrą rzeczą do spróbowania jest użycie odległości, na przykład: return length(p);. Operator ten zwraca wielkość wektora i w tym przypadku podaje nam odległość punktu bieżącego do początku.

punkt nie jest zbyt interesującą rzeczą do renderowania, ponieważ punkt jest nieskończenie mały, a nasze promienie zawsze za nim tęsknią!Możemy nadać punktowi pewien obszar, odejmując żądany promień od odległości: return length(p) - 0.25;.Możemy również zmodyfikować punkt wejściowy przed podjęciem jego wielkości: length(p - vec2(0.0, 0.2)) - 0.25;.Jaki to ma wpływ na kształt?Jakie wartości może zwracać funkcja dla punktów „wewnątrz” okręgu?

Gratulacje-właśnie wymodelowałeś koło za pomocą matematyki:). To trywialnie rozszerzy się na 3D, w którym to przypadku modeluje kulę. Kontrastuj tę reprezentację sceny z innymi „wyraźnymi” reprezentacjami scen, takimi jak siatki trójkąta lub powierzchnie NURBS. Stworzyliśmy sferę w kilka minut z pojedynczą linią kodu, a nasz kod bezpośrednio odwzorowuje jedną matematyczną definicję sfery – „zbiór wszystkich punktów, które są w równej odległości od punktu środkowego”.

dla innych typów prymitywów funkcje odległości są podobnie eleganckie. iq stworzyło świetną stronę referencyjną ze zdjęciami: http://iquilezles.org/www/articles/distfunctions/distfunctions.htm

gdy już zrozumiesz jak działa dystans do prymitywu-umieść go w pudełku-zdefiniuj dla niego funkcję, dzięki czemu nie musisz za każdym razem pamiętać i wypisywać kodu. Istnieje już zdefiniowana funkcja dla okręgu sdCircle(), którą można znaleźć w shaderze. Dodaj dowolne prymitywy, które chcesz.

łączenie kształtów

teraz wiemy, jak tworzyć pojedyncze prymitywy, jak możemy je połączyć, aby zdefiniować scenę z wieloma kształtami?

jednym ze sposobów jest operator „union” – który jest zdefiniowany jako minimum dwóch odległości. Najlepiej jest eksperymentować z kodem, aby uzyskać silne zrozumienie tego, ale intuicja jest taka, że SDF podaje odległość do najbliższej powierzchni, a jeśli scena ma wiele obiektów, chcesz odległość do najbliższego obiektu, która będzie minimalną odległością do każdego obiektu.

w kodzie może to wyglądać następująco:

float sdf(vec2 p){ float d = 1000.0; d = min(d, sdCircle(p, vec2(-0.1, 0.4), 0.15)); d = min(d, sdCircle(p, vec2( 0.5, 0.1), 0.35)); return d;}

w ten sposób możemy zwarto łączyć wiele kształtów. Po zrozumieniu tego należy użyć funkcji opU(), co oznacza „operacja Unia”.

to tylko zarysowanie powierzchni tego, co jest możliwe. Możemy uzyskać gładkie mieszanki za pomocą fantazyjnej funkcji soft min-spróbuj użyć podanego opBlend(). Istnieje wiele innych ciekawych technik, które można zastosować, zainteresowany czytelnik odnosi się do tego rozszerzonego wprowadzenia do budowania scen z SDFs: https://www.youtube.com/watch?v=s8nFqwOho-s

przykład:

przejście do 3D

mam nadzieję, że uzyskałeś podstawową wiedzę na temat tego, w jaki sposób pola odległości mogą być używane do reprezentowania danych sceny i jak wykorzystamy raymarching do znajdowania punktów przecięcia ze sceną. Teraz zaczniemy pracować w trzech wymiarach, gdzie dzieje się prawdziwa magia.

zalecamy zapisanie bieżącego modułu cieniującego i uruchomienie nowego, aby później móc wrócić do wizualizacji 2D.Większość pomocników może skopiować do nowego shadera i pracować w 3D, zamieniając vec2s na vec3s.

Ray marching loop

zamiast wizualizować SDF, jak to zrobiliśmy w 2D, przejdziemy od razu do renderowania sceny. Oto podstawowa idea jak zaimplementujemy ray marching (w pseudo kodzie):

Main function Evaluate camera Call RenderRayRenderRay function Raymarch to find intersection of ray with scene Shade

te kroki zostaną teraz opisane bardziej szczegółowo.

aparat

vec3 getCameraRayDir(vec2 uv, vec3 camPos, vec3 camTarget){ // Calculate camera's "orthonormal basis", i.e. its transform matrix components vec3 camForward = normalize(camTarget - camPos); vec3 camRight = normalize(cross(vec3(0.0, 1.0, 0.0), camForward)); vec3 camUp = normalize(cross(camForward, camRight)); float fPersp = 2.0; vec3 vDir = normalize(uv.x * camRight + uv.y * camUp + camForward * fPersp); return vDir;}

funkcja ta najpierw oblicza trzy osie matrycy „widoku” kamery; wektory do przodu, w prawo i w górę.Wektor do przodu jest znormalizowanym wektorem od pozycji kamery do pozycji docelowej look.Prawy wektor znajduje się przez przekroczenie wektora do przodu z osią świata w górę.Wektory do przodu i do prawej są następnie krzyżowane, aby uzyskać wektor kamery w górę.

wreszcie promień kamery jest obliczany za pomocą tej klatki, biorąc punkt przed kamerą i kompensując go w prawo i w górę za pomocą współrzędnych pikseli uv.fPersp pozwala nam pośrednio kontrolować pole widzenia naszej kamery. Możesz myśleć o tym mnożeniu jako o przesuwaniu bliskiej płaszczyzny bliżej i dalej od kamery. Eksperymentuj z różnymi wartościami, aby zobaczyć wynik.

definicja sceny

float sdSphere(vec3 p, float r){ return length(p) - r;} float sdf(vec3 pos){ float t = sdSphere(pos-vec3(0.0, 0.0, 10.0), 3.0); return t;}

jak widzisz, dodaliśmy sdSphere(), który jest identyczny z sdCircle, zapisując liczbę komponentów w naszym punkcie wejściowym.

Raymarching

Pseudo code:

castRay for i in step count: sample scene if within threshold return dist return -1

spróbuj napisać to sam-jeśli tylko utkniesz, spójrz na rozwiązanie poniżej.

prawdziwy kod:

float castRay(vec3 rayOrigin, vec3 rayDir){ float t = 0.0; // Stores current distance along ray for (int i = 0; i < 64; i++) { float res = SDF(rayOrigin + rayDir * t); if (res < (0.0001*t)) { return t; } t += res; } return -1.0;}

dodamy teraz funkcję render, która ostatecznie będzie odpowiedzialna za cieniowanie znalezionego punktu przecięcia. Na razie jednak, pozwala wyświetlić odległość do sceny, aby sprawdzić, że jesteśmy na dobrej drodze. Będziemy skalować i odwracać, aby lepiej zobaczyć różnice.

vec3 render(vec3 rayOrigin, vec3 rayDir){ float t = castRay(rayOrigin, rayDir); // Visualize depth vec3 col = vec3(1.0-t*0.075); return col;}

aby obliczyć kierunek każdego promienia, będziemy chcieli przekształcić wejście współrzędnych pikseli fragCoord z zakresu , , gdzie w i h to szerokość i wysokość ekranu w pikselach, a a to współczynnik kształtu ekranu. Następnie możemy przekazać wartość zwróconą z tego helpera do funkcji getCameraRayDir zdefiniowanej powyżej, aby uzyskać kierunek promienia.

vec2 normalizeScreenCoords(vec2 screenCoord){ vec2 result = 2.0 * (screenCoord/iResolution.xy - 0.5); result.x *= iResolution.x/iResolution.y; // Correct for aspect ratio return result;}

nasza główna funkcja obrazu wygląda następująco:

void mainImage(out vec4 fragColor, vec2 fragCoord){ vec3 camPos = vec3(0, 0, -1); vec3 camTarget = vec3(0, 0, 0); vec2 uv = normalizeScreenCoords(fragCoord); vec3 rayDir = getCameraRayDir(uv, camPos, camTarget); vec3 col = render(camPos, rayDir); fragColor = vec4(col, 1); // Output to screen}

ćwiczenia:

  • eksperymentuj z liczbą kroków i obserwuj, jak zmienia się wynik.
  • eksperymentuj z progiem zakończenia i obserwuj zmiany wyniku.

aby uzyskać pełny program roboczy, zobacz Shadertoy: Part 1a

Ambient term

aby uzyskać kolor sceny, którą najpierw rozróżnimy między obiektami a tłem.

aby to zrobić, możemy zwrócić -1 w castRay, aby zasygnalizować, że nic nie zostało trafione. Możemy wtedy obsłużyć ten przypadek w render.

vec3 render(vec3 rayOrigin, vec3 rayDir){ vec3 col; float t = castRay(rayOrigin, rayDir); if (t == -1.0) { // Skybox colour col = vec3(0.30, 0.36, 0.60) - (rayDir.y * 0.7); } else { vec3 objectSurfaceColour = vec3(0.4, 0.8, 0.1); vec3 ambient = vec3(0.02, 0.021, 0.02); col = ambient * objectSurfaceColour; } return col;}

https://www.shadertoy.com/view/4tdBzj

Diffuse term

aby uzyskać bardziej realistyczne oświetlenie, obliczmy powierzchnię normalną, abyśmy mogli obliczyć podstawowe oświetlenie Lambertian.

aby obliczyć normę, obliczymy gradient powierzchni we wszystkich trzech osiach.

co to oznacza w praktyce jest próbkowanie SDF cztery dodatkowe razy, każdy nieznacznie przesunięty od naszego pierwotnego promienia.

vec3 calcNormal(vec3 pos){ // Center sample float c = sdf(pos); // Use offset samples to compute gradient / normal vec2 eps_zero = vec2(0.001, 0.0); return normalize(vec3( sdf(pos + eps_zero.xyy), sdf(pos + eps_zero.yxy), sdf(pos + eps_zero.yyx) ) - c);}

świetnym sposobem na sprawdzenie Normalsów jest wyświetlanie ich tak, jakby reprezentowały kolor. Tak powinna wyglądać kula podczas wyświetlania jej przeskalowanej i tendencyjnej wartości normalnej (przeniesiona z do , ponieważ monitor nie może wyświetlać ujemnych wartości kolorów)

col = N * vec3(0.5) + vec3(0.5);

teraz, gdy mamy normę, możemy wziąć iloczyn punktowy pomiędzy nią a kierunkiem światła.

to powie nam, jak bezpośrednio powierzchnia jest skierowana do światła, a zatem jak jasna powinna być.

bierzemy max tej wartości z 0, aby zapobiec negatywnym wartościom powodującym niepożądane efekty na ciemnej stronie obiektów.

// L is vector from surface point to light, N is surface normal. N and L must be normalized!float NoL = max(dot(N, L), 0.0);vec3 LDirectional = vec3(0.9, 0.9, 0.8) * NoL;vec3 LAmbient = vec3(0.03, 0.04, 0.1);vec3 diffuse = col * (LDirectional + LAmbient);

bardzo ważną częścią renderowania, którą można łatwo przeoczyć, jest korekcja gamma. Wartości pikseli wysyłane do monitora znajdują się w przestrzeni gamma, która jest nieliniową przestrzenią używaną do maksymalizacji precyzji, poprzez użycie mniejszej ilości bitów w zakresach intensywności, na które ludzie są mniej wrażliwi.

ponieważ monitory nie działają w przestrzeni „liniowej”, musimy skompensować ich krzywą gamma przed wyprowadzeniem koloru. Różnica jest bardzo zauważalna i zawsze należy ją skorygować. W rzeczywistości nie znamy krzywej gamma dla konkretnego urządzenia wyświetlającego, więc cała sytuacja z technologią wyświetlania jest okropnym bałaganem (stąd krok strojenia gamma w wielu grach), ale powszechnym założeniem jest następująca krzywa gamma:

stała 0.4545 jest po prostu 1.0 / 2.2

col = pow(col, vec3(0.4545)); // Gamma correction

https://www.shadertoy.com/view/4t3fzn

Cienie

aby obliczyć cienie, możemy wystrzelić promień rozpoczynający się w punkcie, w którym przecięliśmy scenę i idący w kierunku źródła światła.

Jeśli ten marsz promieni spowoduje, że coś uderzymy, to wiemy, że światło również zostanie zablokowane, a więc ten piksel jest w cieniu.

float shadow = 0.0;vec3 shadowRayOrigin = pos + N * 0.01;vec3 shadowRayDir = L;IntersectionResult shadowRayIntersection = castRay(shadowRayOrigin, shadowRayDir);if (shadowRayIntersection.mat != -1.0){ shadow = 1.0;}col = mix(col, col*0.2, shadow);

płaszczyzna uziemienia

dodajmy płaszczyznę uziemienia, abyśmy mogli lepiej widzieć cienie rzucane przez nasze kule.

składowa w Z n reprezentuje odległość płaszczyzny od początku.

float sdPlane(vec3 p, vec4 n){ return dot(p, n.xyz) + n.w;}

miękkie cienie

cienie w prawdziwym życiu nie zatrzymują się od razu, mają pewne spadki, zwane półcieniem.

możemy modelować to poprzez maszerowanie kilkoma promieniami z naszego punktu powierzchni, każdy z nieco innymi kierunkami.

możemy wtedy zsumować wynik i uśrednić liczbę iteracji, które wykonaliśmy. Spowoduje to, że krawędzie cienia będą miały

niektóre promienie uderzą, a inne chybią, dając 50% ciemności.

znalezienie nieco pseudolosowej liczby można zrobić na wiele sposobów, użyjemy jednak następujących:

float rand(vec2 co){ return fract(sin(dot(co.xy ,vec2(12.9898,78.233))) * 43758.5453);}

funkcja zwróci liczbę z zakresu [0, 1). Wiemy, że wyjście jest związane z tym przedziałem, ponieważ najbardziej zewnętrzną operacją jest frakt, który zwraca ułamkową składową liczby zmiennoprzecinkowej.

następnie możemy użyć tego do obliczenia naszego promienia cienia w następujący sposób:

float shadow = 0.0;float shadowRayCount = 1.0;for (float s = 0.0; s < shadowRayCount; s++){ vec3 shadowRayOrigin = pos + N * 0.01; float r = rand(vec2(rayDir.xy)) * 2.0 - 1.0; vec3 shadowRayDir = L + vec3(1.0 * SHADOW_FALLOFF) * r; IntersectionResult shadowRayIntersection = castRay(shadowRayOrigin, shadowRayDir); if (shadowRayIntersection.mat != -1.0) { shadow += 1.0; }}col = mix(col, col*0.2, shadow/shadowRayCount);

mapowanie tekstur

zamiast definiować jednolity kolor pojedynczej powierzchni (lub inną charakterystykę) na całej powierzchni, można zdefiniować wzorce do zastosowania na powierzchni za pomocą tekstur.Omówimy trzy sposoby osiągnięcia tego celu.

mapowanie tekstur 3D

w shadertoy dostępne są Tekstury woluminów, które można przypisać do kanału. Spróbuj pobrać próbkę jednej z tych tekstur, używając położenia 3D punktu powierzchni:

// assign a 3D noise texture to iChannel0 and then sample based on world positionfloat textureFreq = 0.5;vec3 surfaceCol = texture(iChannel0, textureFreq * surfacePos).xyz;

jednym ze sposobów próbkowania szumu jest zsumowanie wielu skal, używając czegoś takiego jak poniżej:

// assign a 3D noise texture to iChannel0 and then sample based on world positionfloat textureFreq = 0.5;vec3 surfaceCol = 0.5 * texture(iChannel0, 1.0 * textureFreq * surfacePos).xyz + 0.25 * texture(iChannel0, 2.0 * textureFreq * surfacePos).xyz + 0.125 * texture(iChannel0, 4.0 * textureFreq * surfacePos).xyz + 0.0625 * texture(iChannel0, 8.0 * textureFreq * surfacePos).xyz ;

powyższe stałe / wagi są zwykle używane do szumu fraktalnego, ale mogą przyjmować dowolne pożądane wartości. Spróbuj eksperymentować z wagami / skalami / kolorami i zobacz, jakie ciekawe efekty możesz osiągnąć.

spróbuj animować obiekt przy użyciu iTime i obserwować, jak zachowuje się Tekstura woluminu. Czy takie zachowanie można zmienić?

mapowanie tekstur 2D

zastosowanie tekstury 2D to interesujący problem-jak wyświetlić teksturę na powierzchni? W normalnej grafice 3D każdy trójkąt w obiekcie ma przypisany jeden lub więcej UVs, które zapewniają współrzędne obszaru tekstury, który powinien być zmapowany do trójkąta (mapowanie tekstur). W naszym przypadku nie mamy pod warunkiem UVs, więc musimy dowiedzieć się, jak próbki tekstury.

jednym z podejść jest próbkowanie tekstury za pomocą odgórnej projekcji świata, poprzez próbkowanie tekstury na podstawie współrzędnych x & Z:

// top down projectionfloat textureFreq = 0.5;vec2 uv = textureFreq * surfacePos.xz; // sample texturevec3 surfaceCol = texture2D(iChannel0, uv).xyz;

jakie ograniczenia widzisz w takim podejściu?

mapowanie Triplanarne

bardziej zaawansowanym sposobem mapowania tekstur jest wykonanie 3 rzutów z osi głównych, a następnie mieszanie wyniku za pomocą mapowania triplanarnego. Celem mieszania jest wybranie najlepszej tekstury dla każdego punktu na powierzchni. Jedną z możliwości jest zdefiniowanie masy mieszanki na podstawie wyrównania powierzchni normalnej z każdą osią świata. Powierzchnia zwrócona przodem do jednej z osi otrzyma dużą masę mieszanki:

vec3 triplanarMap(vec3 surfacePos, vec3 normal){ // Take projections along 3 axes, sample texture values from each projection, and stack into a matrix mat3 triMapSamples = mat3( texture(iChannel0, surfacePos.yz).rgb, texture(iChannel0, surfacePos.xz).rgb, texture(iChannel0, surfacePos.xy).rgb ); // Weight three samples by absolute value of normal components return triMapSamples * abs(normal);}

jakie ograniczenia widzisz w takim podejściu?

materiały

wraz z odległością, którą zwracamy z funkcji castRay, możemy również zwrócić indeks, który reprezentuje materiał uderzonego obiektu. Możemy użyć tego indeksu do odpowiedniego pokolorowania obiektów.

nasi operatorzy będą musieli wziąć vec2s zamiast pływaków i porównać pierwszy składnik każdego.

teraz, definiując naszą scenę, określimy również materiał dla każdego prymitywu jako składnik y vec2:

vec2 res = vec2(sdSphere(pos-vec3(3,-2.5,10), 2.5), 0.1);res = opU(res, vec2(sdSphere(pos-vec3(-3, -2.5, 10), 2.5), 2.0));res = opU(res, vec2(sdSphere(pos-vec3(0, 2.5, 10), 2.5), 5.0));return res;

następnie możemy pomnożyć ten indeks materiału przez niektóre wartości w funkcji render, aby uzyskać różne kolory dla każdego obiektu. Wypróbuj różne wartości.

col = vec3(0.18*m, 0.6-0.05*m, 0.2)if (m == 2.0){ col *= triplanarMap(pos, N, 0.6);}

pokolorujmy płaszczyznę ziemi za pomocą wzoru szachownicy. Wziąłem tę fantazyjną funkcję checkerbox anty-aliased ze strony Inigo Quilez.

float checkers(vec2 p){ vec2 w = fwidth(p) + 0.001; vec2 i = 2.0*(abs(fract((p-0.5*w)*0.5)-0.5)-abs(fract((p+0.5*w)*0.5)-0.5))/w; return 0.5 - 0.5*i.x*i.y;}

przechodzimy przez składowe XZ naszej pozycji płaszczyzny, aby uzyskać wzór do powtórzenia w tych wymiarach.

https://www.shadertoy.com/view/Xl3fzn

mgła

możemy teraz dodać mgłę do sceny na podstawie odległości każdego skrzyżowania od kamery.

zobacz czy możesz dostać coś podobnego do poniższych:

https://www.shadertoy.com/view/Xtcfzn

kształt & mieszanie materiałów aby uniknąć ostrego zagniecenia podanego przez operatora min, możemy użyć bardziej wyrafinowanego operatora, który płynnie łączy kształty.

// polynomial smooth min (k = 0.1);float sminCubic(float a, float b, float k){ float h = max(k-abs(a-b), 0.0); return min(a, b) - h*h*h/(6.0*k*k);} vec2 opBlend(vec2 d1, vec2 d2){ float k = 2.0; float d = sminCubic(d1.x, d2.x, k); float m = mix(d1.y, d2.y, clamp(d1.x-d,0.0,1.0)); return vec2(d, m);}

wygładzanie

poprzez wielokrotne próbkowanie sceny za pomocą lekko przesuniętych wektorów kierunku kamery, możemy uzyskać wygładzoną wartość, która unika aliasingu.

wprowadziłem obliczenie koloru sceny do własnej funkcji, aby wywołanie w pętli było wyraźniejsze.

float AA_size = 2.0;float count = 0.0;for (float aaY = 0.0; aaY < AA_size; aaY++){ for (float aaX = 0.0; aaX < AA_size; aaX++) { fragColor += getSceneColor(fragCoord + vec2(aaX, aaY) / AA_size); count += 1.0; }}fragColor /= count;

optymalizacja liczby kroków

jeśli wizualizujemy, ile kroków wykonujemy dla każdego piksela na Czerwono, wyraźnie widzimy, że promienie, które uderzają w nic, są odpowiedzialne za większość naszych iteracji.

może to znacznie zwiększyć wydajność niektórych scen.

if (t > drawDist) return backgroundColor;

kształt & interpolacja materiału

możemy interpolować między dwoma kształtami za pomocą funkcji mix i używając iTime do modulacji w czasie.

vec2 shapeA = vec2(sdBox(pos-vec3(6.5, -3.0, 8), vec3(1.5)), 1.5);vec2 shapeB = vec2(sdSphere(pos-vec3(6.5, -3.0, 8), 1.5), 3.0);res = opU(res, mix(shapeA, shapeB, sin(iTime)*0.5+0.5));

powtórzenie domeny

dość łatwo jest powtórzyć kształt za pomocą pola odległości podpisanej, zasadniczo wystarczy modulo pozycji wejściowej w jednym lub więcej wymiarach.

ta technika może być użyta na przykład do kilkukrotnego powtórzenia kolumny bez zwiększania rozmiaru reprezentacji sceny.

tutaj powtórzyłem wszystkie trzy składniki pozycji wejściowej, a następnie użyłem operatora odejmowania (max ()), aby ograniczyć powtórzenie do obwiedni.

jednym z nich jest to, że musisz odjąć połowę wartości, którą modulujesz, aby wyśrodkować powtórzenie na swoim kształcie, aby nie przeciąć go na pół.

float repeat(float d, float domain){ return mod(d, domain)-domain/2.0;}

efekty postprocessingu

winieta

przyciemniając piksele znajdujące się dalej od środka ekranu możemy uzyskać prosty efekt winiety.

kontrast

ciemniejsze i jaśniejsze wartości mogą być podkreślone, powodując wzrost postrzeganego zakresu dynamiki wraz z intensywnością obrazu.

col = smoothstep(0.0,1.0,col);

„ambient occlusion”

jeśli weźmiemy odwrotność obrazu pokazanego powyżej (w optymalizacji), możemy uzyskać dziwny efekt podobny do AO.

col *= (1.0-vec3(steps/maxSteps));

Ad infinitum

jak widać, wiele efektów przetwarzania końcowego można zaimplementować trywialnie; baw się różnymi funkcjami i zobacz, jakie inne efekty możesz stworzyć.

www.shadertoy.com/view/MtdBzs

co dalej?

właśnie omówiliśmy tutaj podstawy; jest wiele więcej do odkrycia w tej dziedzinie, takich jak:

  • rozpraszanie podpowierzchniowe
  • Okluzja otoczenia
  • animowane prymitywy
  • prymitywne funkcje wypaczania (twist, bend, …)
  • przezroczystość (refrakcja, kaustyka, …)
  • optymalizacje (obwiedniowe hierarchie woluminów)

Przeglądaj ShaderToy, aby uzyskać inspirację na temat tego, co można zrobić, i przeglądaj różne shadery, aby zobaczyć, jak różne efekty są implementowane. Wiele shaderów ma zmienne, które można dostosować i natychmiast zobaczyć efekty (alt-enter jest skrótem do kompilacji!).

przeczytaj również referencje, jeśli chcesz dowiedzieć się więcej!

dzięki za przeczytanie! Pamiętaj, aby wysłać nam swoje fajne shadery! Jeśli masz jakieś uwagi na temat kursu, chętnie je usłyszymy!

skontaktuj się z nami na Twitterze @liqwidice & @ hdb1

Electric Square zatrudnia!

zalecane czytanie:

funkcje SDF: http://jamie-wong.com/2016/07/15/ray-marching-signed-distance-functions/

Claybook demo: https://www.youtube.com/watch?v=Xpf7Ua3UqOA

Ray Tracing w jeden Weekend: http://in1weekend.blogspot.com/2016/01/ray-tracing-in-one-weekend.html

Biblia renderowania fizycznego, PBRT: https://www.pbrt.org/

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany.