Jak naprawdę działa Ray Tracing? Od promienia światła do RT Cores w GPU.

Ray tracing to technika renderowania obrazu, która symuluje transport światła poprzez śledzenie jego drogi w scenie 3D. W przeciwieństwie do klasycznej rasteryzacji, która przede wszystkim określa, które trójkąty znajdują się przed kamerą, ray tracing pozwala bezpośrednio obliczać, czy z danego punktu widoczny jest inny obiekt, skąd dociera światło oraz w którym miejscu promień odbije się, załamie lub zostanie pochłonięty.

Współczesne karty graficzne, takie jak układy NVIDIA RTX, AMD Radeon czy Intel Arc, posiadają sprzętowe mechanizmy przyspieszające obliczenia związane z ray tracingiem. W przypadku architektury NVIDIA są to między innymi RT Cores, czyli wyspecjalizowane jednostki odpowiedzialne za najbardziej kosztowne operacje związane z przechodzeniem przez struktury przyspieszające oraz obliczaniem przecięć promieni z geometrią.


Ray tracing zaczyna się od promienia

Podstawową jednostką ray tracingu jest promień. Matematycznie można go opisać równaniem:

R(t) = O + tD

gdzie O oznacza początek promienia, D jest jego kierunkiem, a t określa odległość od początku promienia. Dla wartości t > 0 otrzymujemy kolejne punkty znajdujące się na jego drodze.

Jeżeli kamera znajduje się w punkcie O, a promień przechodzi przez konkretny piksel, kierunek D wskazuje dokładnie w stronę fragmentu sceny, który ma zostać zbadany.

🎯 Jeden piksel może oznaczać jeden promień

Najprostszy renderer może dla każdego piksela utworzyć promień wychodzący z kamery i sprawdzić, jaki obiekt znajduje się na jego drodze. Jeżeli promień trafi w trójkąt, renderer otrzymuje między innymi:

punkt przecięcia, odległość od kamery, normalną powierzchni oraz informacje o materiale.

Na tej podstawie można rozpocząć obliczenia oświetlenia. Problem polega na tym, że współczesna scena może zawierać miliony trójkątów. Sprawdzanie każdego promienia względem każdego trójkąta byłoby absurdalnie kosztowne.


Dlaczego nie sprawdzamy każdego trójkąta?

Załóżmy, że scena zawiera 10 milionów trójkątów, a obraz ma rozdzielczość 3840 × 2160. Już jeden promień na piksel oznacza:

3840 × 2160 = 8 294 400 promieni

Gdyby każdy z nich musiał zostać sprawdzony względem 10 milionów trójkątów, otrzymalibyśmy około:

82 944 000 000 000 potencjalnych testów przecięcia.

Czyli ponad 82 biliony potencjalnych testów dla jednego przebiegu obrazu. A rzeczywisty renderer wykonuje znacznie więcej promieni, ponieważ oprócz promieni pierwotnych potrzebne są również promienie cieni, odbić, załamań i oświetlenia pośredniego.

Dlatego ray tracing potrzebuje struktur przyspieszających.


BVH, czyli sposób na szybkie znalezienie obiektu

Jedną z najważniejszych struktur danych wykorzystywanych przez współczesny ray tracing jest BVH – Bounding Volume Hierarchy.

Zamiast przechowywać geometrię jako płaską listę milionów trójkątów, renderer organizuje ją hierarchicznie.

📦 Bounding Volume

Grupa obiektów zostaje otoczona prostym obszarem ograniczającym, najczęściej Axis-Aligned Bounding Box, czyli AABB.

AABB można opisać za pomocą dwóch punktów:

min = (xmin, ymin, zmin)
max = (xmax, ymax, zmax)

Jeżeli promień nie przecina takiego pudełka, nie ma potrzeby sprawdzania żadnego trójkąta znajdującego się wewnątrz niego.

BVH tworzy z takich pudeł hierarchię. Na najwyższym poziomie może znajdować się jedno duże pudełko obejmujące całą scenę. Następnie dzieli się ono na mniejsze obszary, które zawierają kolejne grupy obiektów.

Renderer może więc najpierw sprawdzić duży obszar, następnie tylko te jego podobszary, które przecina promień, a dopiero na końcu wykonać dokładne testy względem trójkątów.


Traversal – przechodzenie przez BVH

Proces przechodzenia przez strukturę BVH nazywany jest traversal.

Dla każdego promienia GPU wykonuje serię testów:

promień → AABB → AABB → AABB → liść BVH → trójkąt.

Jeżeli promień nie przecina danego węzła, cała jego gałąź może zostać pominięta.

To właśnie jest kluczowa optymalizacja. Zamiast testować miliony trójkątów, algorytm eliminuje ogromną część sceny jeszcze zanim dotrze do właściwej geometrii.

⚡ RT Core nie „renderuje całego ray tracingu”

Częstym uproszczeniem jest stwierdzenie, że RT Core wykonuje ray tracing. Dokładniej mówiąc, RT Core przyspiesza przede wszystkim traversal struktur BVH oraz testy przecięcia promieni z geometrią.

Pozostała część procesu, między innymi generowanie promieni, wykonywanie shaderów, obliczenia materiałów, oświetlenie i przygotowanie kolejnych promieni, jest wykonywana przez inne jednostki GPU.


Jak promień trafia w trójkąt?

Po przejściu przez BVH renderer musi dokładnie sprawdzić, czy promień przecina konkretny trójkąt.

Jednym z klasycznych algorytmów wykorzystywanych do tego celu jest algorytm Möllera-Trumbore'a.

Trójkąt można opisać trzema wierzchołkami:

V0, V1, V2

Tworzymy dwa wektory krawędzi:

E1 = V1 - V0
E2 = V2 - V0

Następnie algorytm wykorzystuje iloczyny wektorowe i skalarne, aby znaleźć parametry określające punkt przecięcia.

Jeżeli rozwiązanie istnieje i punkt znajduje się wewnątrz trójkąta, otrzymujemy trafienie.


Współrzędne barycentryczne

Sam fakt przecięcia z trójkątem nie wystarcza. Renderer musi wiedzieć dokładnie, gdzie na powierzchni trójkąta nastąpiło trafienie.

Wykorzystuje się do tego między innymi współrzędne barycentryczne.

Punkt wewnątrz trójkąta można przedstawić jako kombinację jego wierzchołków:

P = (1 - u - v)V0 + uV1 + vV2

Parametry u oraz v pozwalają określić położenie punktu względem powierzchni trójkąta.

Te informacje są niezwykle ważne, ponieważ dzięki nim można interpolować między innymi normalne, współrzędne UV, kolory wierzchołków oraz inne dane potrzebne przez shader.


Co dzieje się po trafieniu?

Po znalezieniu przecięcia renderer posiada punkt P, normalną powierzchni oraz dane materiału.

W tym momencie rozpoczyna się właściwe obliczanie oświetlenia.

Załóżmy, że promień trafił w metalową kulę. Renderer może sprawdzić kierunek do źródła światła, obliczyć kąt pomiędzy normalną powierzchni a kierunkiem światła oraz określić, czy pomiędzy punktem a światłem znajduje się przeszkoda.


Shadow Ray – promień cienia

Aby sprawdzić, czy punkt jest oświetlony, renderer może wysłać dodatkowy promień w kierunku źródła światła.

Taki promień nazywany jest shadow ray.

Schemat wygląda następująco:

punkt powierzchni → światło

Jeżeli promień dociera do światła bez trafienia w przeszkodę, punkt może otrzymać bezpośrednie oświetlenie.

Jeżeli po drodze znajdzie się inny obiekt, światło jest zasłonięte i punkt znajduje się w cieniu.

🌑 Dlaczego ray-traced shadows wyglądają inaczej?

W klasycznych technikach cienie często są generowane za pomocą shadow maps. Kamera światła renderuje scenę do mapy głębokości, a następnie renderer porównuje pozycję powierzchni z zapisanymi wartościami.

Ray tracing może zamiast tego bezpośrednio sprawdzić, czy pomiędzy powierzchnią a światłem istnieje przeszkoda. Dzięki temu geometria sceny może być wykorzystywana bezpośrednio do określania widoczności.


Reflection Ray – promienie odbicia

Jeżeli materiał jest odbijający, renderer może wygenerować kolejny promień.

Kierunek idealnego odbicia można obliczyć za pomocą:

R = I - 2(I · N)N

gdzie I oznacza kierunek padającego promienia, a N normalną powierzchni.

Nowy promień zaczyna się w punkcie trafienia i podróżuje w kierunku R.

Jeżeli trafi w kolejny obiekt, proces może zostać powtórzony.

W ten sposób powstaje łańcuch odbić:

kamera → obiekt → metal → ściana → światło

Każde kolejne odbicie zwiększa jednak koszt obliczeń.


Refraction – załamanie światła

Materiały przezroczyste wymagają jeszcze bardziej złożonych obliczeń.

Kierunek załamania może być obliczany na podstawie prawa Snelliusa:

n1 sin(θ1) = n2 sin(θ2)

gdzie n1 i n2 są współczynnikami załamania obu ośrodków.

Dlatego światło przechodzące przez szkło nie tylko zmienia swoją intensywność, ale również kierunek.


Ray Tracing nie oznacza jednego promienia na piksel

W praktycznych rendererach jeden promień na piksel zazwyczaj nie wystarcza.

Rzeczywiste oświetlenie jest znacznie bardziej skomplikowane. Światło może docierać do powierzchni z wielu kierunków, odbijać się od różnych materiałów i wielokrotnie zmieniać kierunek.

Dlatego stosuje się Monte Carlo path tracing.

🎲 Path Tracing

Path tracing losowo wybiera kierunki kolejnych promieni zgodnie z właściwościami materiału i rozkładem światła. Wykonując dużą liczbę takich próbek, renderer przybliża rozwiązanie równania transportu światła.

Problem polega na tym, że mała liczba próbek powoduje ogromny szum.

Dlatego współczesne gry nie wykonują zazwyczaj pełnego fizycznego path tracingu z nieskończoną liczbą próbek. Zamiast tego wykorzystują ograniczoną liczbę promieni oraz rekonstrukcję i odszumianie obrazu.


Równanie renderingu

Teoretyczne podstawy renderowania opisuje między innymi równanie renderingu Kajiya.

W uproszczeniu mówi ono, że światło wychodzące z danego punktu jest sumą światła emitowanego przez powierzchnię oraz światła przychodzącego z innych kierunków, pomnożonego przez właściwości materiału i czynnik zależny od kąta padania.

Jedna z popularnych postaci równania ma postać:

Lo(x,ωo) = Le(x,ωo) + ∫Ω fr(x,ωi,ωo) Li(x,ωi) |n · ωi| dωi

W praktyce oznacza to, że ilość światła wychodzącego z powierzchni zależy od światła przychodzącego, właściwości materiału oraz orientacji powierzchni.


BRDF – jak materiał reaguje na światło?

BRDF, Bidirectional Reflectance Distribution Function, opisuje sposób, w jaki powierzchnia odbija światło.

Dla materiałów typu PBR szczególne znaczenie mają takie właściwości jak:

Base Color – podstawowy kolor materiału.
Roughness – określa rozmycie odbić.
Metallic – określa charakter materiału metalicznego.
Normal – zmienia sposób interpretacji orientacji powierzchni.
IOR – współczynnik załamania materiału.

Dla idealnego lustra odbicie może być bardzo skoncentrowane. Dla powierzchni o wysokiej chropowatości energia odbicia jest rozłożona na większy zakres kierunków.


Dlaczego ray tracing jest tak ciężki dla GPU?

Problemem nie jest wyłącznie liczba operacji matematycznych. Ray tracing ma również niekorzystny dla klasycznych GPU charakter dostępu do pamięci.

W rasteryzacji przepływ danych jest stosunkowo regularny. GPU przetwarza ogromne liczby fragmentów w przewidywalny sposób.

Ray tracing jest bardziej chaotyczny. Każdy promień może trafić w zupełnie inny fragment sceny, następnie wygenerować kolejny promień w innym kierunku i rozpocząć kolejne przejście przez BVH.

Powoduje to problemy z:

lokalnością pamięci, predykcją dostępu, wykorzystaniem cache oraz divergence.


SIMT i divergence

Nowoczesne GPU nie wykonują niezależnego wątku dokładnie tak jak CPU. NVIDIA wykorzystuje model SIMT – Single Instruction, Multiple Threads.

Wątki są grupowane w warpy. Jeżeli wiele wątków wykonuje podobne operacje, GPU może efektywnie wykorzystać swoje jednostki obliczeniowe.

Ray tracing komplikuje sytuację.

Jeden promień może trafić w metalową ścianę, drugi w szkło, trzeci w niebo, a czwarty w skomplikowaną geometrię. Każdy może więc wymagać zupełnie innego dalszego przebiegu.

Powstaje wtedy branch divergence, czyli sytuacja, w której wątki tego samego warpa muszą wykonywać różne ścieżki programu.


RT Cores – co dokładnie robią?

NVIDIA wprowadziła sprzętowe RT Cores wraz z architekturą Turing.

Ich głównym zadaniem jest przyspieszanie operacji, które są charakterystyczne dla ray tracingu.

🔬 Dwie kluczowe operacje

1. BVH traversal
Przechodzenie przez strukturę hierarchii ograniczających objętości.

2. Ray-Triangle Intersection
Obliczanie, czy promień przecina konkretny trójkąt i gdzie następuje przecięcie.

Są to operacje wykonywane ekstremalnie często. Przeniesienie ich do wyspecjalizowanego sprzętu pozwala zwolnić klasyczne jednostki shaderowe z części pracy.

RT Core nie zastępuje więc CUDA Cores. Obie jednostki współpracują.


CUDA Cores + RT Cores + Tensor Cores

Współczesny układ RTX można traktować jako zestaw różnych rodzajów jednostek wyspecjalizowanych w różnych zadaniach.

CUDA Cores wykonują ogólne obliczenia shaderowe i wiele innych operacji matematycznych.

RT Cores przyspieszają traversal BVH oraz przecięcia promieni z geometrią.

Tensor Cores są wyspecjalizowane w operacjach macierzowych i są wykorzystywane między innymi przez technologie AI, takie jak DLSS.

W praktycznym pipeline mogą więc współpracować wszystkie trzy rodzaje jednostek.


DLSS i ray tracing

Sam ray tracing może wygenerować obraz o bardzo wysokiej jakości, ale koszt obliczeniowy rośnie gwałtownie wraz z rozdzielczością i liczbą promieni.

Dlatego NVIDIA wykorzystuje między innymi DLSS.

Gra może renderować obraz w niższej rozdzielczości wewnętrznej, a następnie wykorzystać dane czasowe, informacje o ruchu oraz sieć neuronową do rekonstrukcji obrazu w wyższej rozdzielczości wyjściowej.

W efekcie GPU nie musi obliczać pełnej liczby próbek dla każdego piksela końcowego.


Denoising – jak pozbyć się szumu?

Path tracing z ograniczoną liczbą próbek generuje szum.

Jeżeli dla danego piksela wykonamy tylko kilka losowych próbek, wynik może być bardzo niestabilny. Zwiększenie liczby próbek poprawia jakość, ale jednocześnie zwiększa koszt renderowania.

Dlatego stosuje się denoising.

Odszumianie może wykorzystywać między innymi:

kolor obrazu, normalne, głębokość, motion vectors, historię poprzednich klatek oraz dane o powierzchniach.

Nowoczesne rozwiązania wykorzystują również modele AI do rekonstrukcji brakujących informacji.


Ray Tracing a rasteryzacja

🖥️ Rasteryzacja

Rasteryzacja bierze geometrię 3D i przekształca ją w fragmenty obrazu. GPU ustala, które trójkąty pokrywają dane piksele, a następnie uruchamia shadery dla tych fragmentów.

Jest ekstremalnie szybka i dlatego przez dziesięciolecia była podstawą grafiki czasu rzeczywistego.

🔬 Ray tracing

Ray tracing zaczyna od promieni i pyta, co znajduje się na ich drodze. Pozwala to naturalnie obliczać odbicia, cienie, załamania oraz wiele efektów związanych z transportem światła.

Jego główną wadą jest koszt obliczeniowy.

Współczesne gry dlatego często wykorzystują hybrydowy rendering: rasteryzacja wykonuje większość pracy, a ray tracing zajmuje się wybranymi efektami.


Dlaczego odbicia RT są tak wymagające?

Wyobraźmy sobie metalową powierzchnię.

Kamera wysyła promień → promień trafia w metal → renderer oblicza odbicie → wysyła kolejny promień → ten trafia w następny obiekt → obliczane jest kolejne oświetlenie.

Każde odbicie oznacza kolejne operacje traversal oraz intersection.

Jeżeli scena pozwala na kilka odbić, liczba wymaganych operacji może bardzo szybko wzrosnąć.

Dlatego silniki często ograniczają liczbę odbić do określonej wartości.


Temporal Accumulation – wykorzystywanie poprzednich klatek

Renderer może wykorzystywać informacje z poprzednich klatek.

Jeżeli kamera porusza się stosunkowo płynnie, wiele informacji dotyczących oświetlenia i powierzchni pozostaje podobnych. Można więc połączyć dane z aktualnej klatki z historią poprzednich klatek.

Pozwala to zwiększyć efektywną liczbę próbek bez wykonywania ich wszystkich od początku.

Problemem są jednak szybkie ruchy, obiekty zmieniające pozycję oraz sytuacje, w których poprzednia informacja przestaje być poprawna. Powstają wtedy artefakty takie jak ghosting czy smużenie.


Co naprawdę dzieje się w jednej klatce?

Uproszczony pipeline współczesnego renderera wykorzystującego ray tracing może wyglądać tak:

1. Dane geometrii trafiają do GPU.
2. Budowana lub aktualizowana jest struktura BVH.
3. Kamera generuje promienie primary rays.
4. RT Core wykonuje traversal BVH.
5. RT Core znajduje potencjalne przecięcie z geometrią.
6. Shader wykonuje obliczenia materiału.
7. Generowane są shadow rays, reflection rays lub transmission rays.
8. Kolejne promienie ponownie przechodzą przez BVH.
9. Wyniki są łączone.
10. Obraz może zostać poddany denoisingowi.
11. Algorytmy rekonstrukcji mogą wykorzystać dane temporalne.
12. Finalny obraz trafia na ekran.


BVH nie jest statyczne w każdej scenie

Jeżeli obiekty się poruszają, ich położenie względem świata ulega zmianie.

Renderer musi więc odpowiednio aktualizować struktury przyspieszające.

W praktyce można rozróżniać między innymi struktury opisujące geometrię obiektu oraz struktury opisujące położenie instancji w świecie.

Pozwala to uniknąć przebudowy całej geometrii za każdym razem, gdy na przykład samochód przesunie się o kilka metrów.


Ray Tracing nie symuluje dokładnie pojedynczych fotonów

Popularne określenie „symulowanie światła” może być mylące.

W klasycznym ray tracingu komputer nie śledzi wszystkich rzeczywistych fotonów znajdujących się w scenie. Ich liczba jest astronomiczna.

Zamiast tego renderer wykorzystuje matematyczny model transportu światła i próbkowanie wybranych ścieżek.

Path tracing jest więc przede wszystkim numerycznym przybliżeniem równania transportu światła, a nie dosłowną symulacją każdego fotonu.


Dlaczego RT potrafi wyglądać bardziej realistycznie?

Największą zaletą ray tracingu jest spójność różnych efektów oświetleniowych.

Ten sam model geometrii może być wykorzystywany do ustalania widoczności dla kamery, cieni, odbić oraz kolejnych odbić światła.

Jeżeli światło odbije się od czerwonej ściany i oświetli biały obiekt, renderer może uwzględnić tę pośrednią zmianę oświetlenia.

Właśnie takie zjawiska są jednym z powodów, dla których pełny path tracing może wyglądać bardzo naturalnie.


Największy problem: wydajność

Ray tracing nie jest magicznym przełącznikiem „realistyczna grafika”.

Każdy dodatkowy promień oznacza kolejne wyszukiwanie w strukturze BVH, potencjalne przecięcia z geometrią, wykonanie shaderów oraz kolejne operacje pamięci.

W wysokiej rozdzielczości liczba promieni może stać się ogromna.

Dlatego współczesne GPU wykorzystują kombinację:

RT Cores + CUDA/Shader Cores + Tensor Cores + BVH + denoising + temporal accumulation + upscaling.

Dopiero połączenie tych technologii pozwala uzyskać ray tracing w czasie rzeczywistym przy rozsądnej liczbie klatek na sekundę.



⚠️ Ray tracing nie zastąpił rasteryzacji

Mimo ogromnego postępu sprzętowego rasteryzacja nadal jest niezwykle ważna. Jest bardzo szybka, dobrze wykorzystuje architekturę GPU i doskonale nadaje się do większości standardowego renderowania.

Dlatego współczesne silniki graficzne najczęściej wykorzystują hybrydowy pipeline, w którym rasteryzacja odpowiada za dużą część obrazu, a ray tracing poprawia konkretne elementy, takie jak odbicia, cienie, global illumination czy ambient occlusion.


Ray tracing w kilku zdaniach

Ray tracing można sprowadzić do jednego podstawowego pytania:

„Co zobaczy ten promień i co stanie się ze światłem po jego trafieniu w powierzchnię?”

Aby odpowiedzieć na to pytanie wystarczająco szybko, GPU wykorzystuje struktury BVH do ograniczenia liczby testów, RT Cores do przyspieszenia traversal i przecięć, jednostki shaderowe do obliczeń materiałów i oświetlenia oraz technologie rekonstrukcji obrazu do zmniejszenia liczby koniecznych próbek.

Ostatecznie ray tracing nie polega więc po prostu na „wysyłaniu promieni”. Jest połączeniem geometrii, algebry liniowej, struktur danych, algorytmów przecięcia, probabilistyki, modeli materiałów, architektury GPU oraz metod rekonstrukcji obrazu.

To właśnie dlatego współczesny ray tracing jest jednym z najbardziej wymagających obliczeniowo zastosowań procesorów graficznych.