Ray tracing kontra rastrering
Dagens grafikkort bygger på en annan grundprincip än ray tracing, de använder något som kallas rastrering. Det finns flera sätt att beskriva geometri än trianglar, men för enkelhetens skull utgår alla beskrivningar med trianglar.
Följande tabell pekar på några av de fundamentala skillnaderna mellan hur ray tracing och rendering tar en 3D-beskrivning av världen till det som visas på skärmen
Koncept | Rastrering | Ray tracing |
---|---|---|
Grundfråga | Vilka pixlar täcker en viss triangel? | Vilka trianglar är synliga utefter en kamerastråle? |
Mest frekventa operation | Testa om en pixel befinner sig utanför eller innanför en triangel. | Testa om en stråle passerar genom en viss triangel. |
Huvudslinga | För-alla-trianglar-i-världen ställ grundfrågan. | För-alla-pixlar-på-skärmen ställ grundfrågan. |
Primärt problem | Kräver hög bandbredd då samma pixel kan läsas/skrivas många gånger (overdraw). | Många trianglar måste testas per stråle, vissa material ger upphov till många sekundärstrålar. |
Viktigast optimering | Hålla reda på djupkoordinaten för varje pixel, Z-buffer. Tidig verifiering mot Z-buffer gör att många pixlar i trianglar helt kan skippas. | Många trianglar måste testas per stråle. Objekt i 3D-världen måste grupperas så kollision kan utföras mot hela grupper först. |
Nackdel | Väldigt ineffektivt att hantera enskilda pixlar inom en triangel på ett annorlunda sätt, vilket behövs för vissa effekter. | Även strålar nära varandra kan kräva väldigt olika beräkningar, vilket gör processen latenskänslig i stället för bandbreddskrävande. |
FLOPS
En term som ofta nämns ihop med grafikkort är deras maximala kapacitet i att göra beräkningar med så kallade flyttal – FLOPS – floating-point operations per second. Flyttal är en datorrepresentation av reella tal, tänk tal med decimaltecken i sig.
Så hur mycket krävs då för att följa en primärstråle genom varje pixel på en skärm med upplösningen 1 920 x 1 080 pixlar (drygt två miljoner pixlar)? Ponera att det åker iväg lika många strålar som skärmens upplösning, en per pixel. Antag att en modern speltitel har cirka en miljon trianglar per scen.
Utan att gå in på detaljer så kommer trianglar i praktiken vara ordnade på ett "smart" sätt så bara en lite fraktion scannas för att avgöra om det är en träff och i så fall vilken triangel som träffas. Antag att hundratals trianglar behöver undersökas, är någonstans i den häraden man hamnar.
Att avgöra om en stråle passerar igenom en enskild triangel kräver ett tiotal multiplikationer och subtraktioner, låt oss säga 100 flyttalsoperationer per triangel för att ha marginal.
I ett spel som Battlefield V är det relativt få av primärstrålarna som kommer träffa ytor som kräver vidare behandling, så är inte rå beräkningskapacitet som hindrar tidigare grafikkort från att överhuvudtaget inte kunna köra ray tracing i realtid.
Parallellism
Kikar man på huvudslingan för rastrering respektive ray tracing är det värt att notera att de egentligen gör samma sak, fast med omkastad yttre och inre ström av objekt. Rastrering jobbar över alla trianglar medan ray tracing jobbar över alla pixlar på skärmen.
Gemensamt för båda metoder är att den yttre slingan består av ett stort antal, sinsemellan oberoende, uppgifter. "Stort antal" kombinerat med "sinsemellan oberoende" är grundreceptet för att framgångsrikt hantera saker parallellt.
Är då slutsatsen att fler CUDA-kärnor likväl hade kunnat lösa problemet som den väg Nvidia tog med sina specialiserade RT-kärnor? Tyvärr inte, då det finns två huvudklasser av parallellism. Rastrering och ray tracing tillhör varsitt läger.
CUDA-kärnor och processorkärnor
Ett sätt att visualisera skillnaden mellan sekventiella uppgifter ("enkeltrådat"), dataparallella ("CUDA-kärnor" och SSE/AVX på x86-processorer) och uppgiftsparallella ("RT-kärnor" och multipla kärnor på processorer) är att tänka sig två matematiska funktioner, F() samt G().
Det är inte viktigt vad funktionerna gör mer än att de tar någon form av indata, xN, gör en beräkning baserat på detta och ger tillbaka ett resultat, yN.
Enkeltrådat
I det sekventiella fallet får man indata från ena funktionen och utdata till den andra, det vill säga:
y0 = F(G(x0))
Det är i detta läge inte möjligt att beräkna F() innan beräkningen för G(x0) är klar.
Enda praktiska formen av parallellism som kan utnyttjas här är den som kallas "Instruction Level Parallelism" (ILP). Nivån av ILP avgör hur mycket en krets kan utför per cykel, dess "Instructions Per Clock" (IPC).
Grafikprocessorer är riktigt dålig på denna typ av problem, vilka därför istället körs på en processor.
Uppgiftsparallellt
Det uppgiftsparallella fallet är när F() och G() är olika funktioner och de inte är beroende av varandra:
y0 = F(x0)
y1 = G(x1)
Orsakerna till att ray tracing hamnar i denna kategori är flera. Hanteringen av primärstrålar är initialt identiskt oavsett vilken pixel på skärmen de motsvarar, men då hanteringen av sekundärstrålar beror på egenskaperna hos ytan som träffats blir effekten att olika strålar i många fall behövs olika sekvenser med instruktioner för att beräkna resultatet.
Kort och gott är olika trådar är oberoende varandra, men arbetet som utförs är typiskt inte identiskt.
Detta fall benämns ibland "Multiple Instruction Multiple Data", (MIMD). En CPU med flera kärnor är exempel på MIMD.
Dataparallellt
Vissa typer av problem behöver applicera samma funktion på massor med indata. Det vill säga i detta fall gäller att F() = G()
y0 = F(x0)
y1 = F(x1)
Det finns en rad optimeringar att göra på kiselnivå i detta fall, vilket är orsaken till att grafikkort har långt mycket högre "giga-/teraflops" (matematisk beräkningar som kan utföras per sekund) värde jämfört med en CPU.
För CPU kallas detta för "Single Instruction Multiple Data", (SIMD) medan GPU-tillverkarna vill benämna det "Single Instruction Multiple Thread", (SIMT), där tråd motsvarar en CUDA-kärna i Nvidias fall. Båda är i grunden samma sak.
På x86-processorer realiseras SIMD via SSE/AVX.
Rastrering är väldigt effektivt då det till stora delar är kraftigt dataparallellt. Vidare hanterar GPU:er ett mycket stort antal samtida dataelement, det vill säga väldigt många samtida xn värden i exemplet ovan.
Då GPU:n kan jobba på tusentals pixlar samtidigt är det inte superkritiskt att varje pixel får sin data med väldigt kort varsel, latensen är därmed rätt oviktig för rastrering medan det krävs rejält med bandbredd.
Prestanda mot korrekthet
Vid rendering skapas skuggor genom att en siluett av det som ska kasta skugga läggs i en textur. Texturen renderas mer eller mindre genomskinlig i den riktning som är rimlig givet primära ljuskällas placering. Det går väldig snabbt och illusionen av halvskugga kan uppnås genom att gradvis göra texturen mer genomskinlig i kanterna.
För bättre resultat används tricks som "ambient occlusion" (AO). AO är verkligen ett "trick" som råkar se riktigt bra ut, tekniken vilar inte på någon verklig fysikalisk beskrivning.
Ray tracing får i princip alla skuggeffekter på köpet från skuggstrålarna, bara det genereras tillräckligt många skuggstrålar. Reflektion uppnår spel genom tekniker som "environment maps" där omgivningen läggs i en textur och blandas in i vad som exempelvis representerar plåten på en bil. Ger ett trovärdigt resultat, i alla fall på relativt plana ytor.
Ray tracing hanterar alla typer av geometrier. Här fås även självreflektion med, alltså när yttre backspegeln på en bil reflekteras i bilens dörr.
Häri ligger "heliga gralen" med ray tracing, om det bara kan göras tillräckligt snabbt får man effekter som kräver rätt komplicerad och tidskrävande kodning och anpassning från grafikernas sida från en egentligen trivial algoritm.
Inga gratisluncher
Egenskaperna hos rastrering kan sammanfattas som: i sammanhanget väldigt enkelt att få riktigt bra prestanda, men kräver komplicerade lösningar för att simulera mer avancerade grafiska effekter.
Ray tracing kan sammanfattas som: väldigt enkelt att producera riktigt avancerade resultat, men kräver i praktiken specialdesignat kisel och en hel del kluriga optimeringar för att ge någorlunda acceptabel prestanda.