Poglavlje 07: Render Pipeline -- Pregled

Poglavlje 07: Render Pipeline -- Pregled


Uvod: Zašto ti je ovo poglavlje važno

Zamisli da si reditelj filma. Imaš scenario (tvoju scenu u Unreal Engineu), imaš glumce (modele), kostimografiju (materijale i teksture), rasvetu (svetla), kameru -- ali da bi sve to postalo slika na platnu, mora da prođe kroz čitav lanac koraka. Svaki korak radi nešto specifično, svaki zavisi od prethodnog, i ako bilo koji zakaže -- film ne izgleda kako treba.

Taj lanac koraka u računarskoj grafici zovemo render pipeline (pipeline za renderovanje). To je srce svega što radiš u Unreal Engine 5 kada je u pitanju vizuelni prikaz. Bez razumevanja ovog pipeline-a, optimizacija postaje pogađanje. Sa razumevanjem -- postaje inženjerstvo.

U ovom poglavlju prolazimo kroz kompletan grafički pipeline od trenutka kada tvoja aplikacija kaže "iscrtaj ovu scenu" do trenutka kada se piksel pojavi na ekranu. Proći ćemo svaki stage (fazu), objasniti šta radi, zašto postoji, i gde se troši vreme. Posebnu pažnju ćemo posvetiti programmable (programabilnim) fazama -- vertex shader, tessellation, geometry shader, pixel shader -- jer tu imaš kontrolu, a samim tim i moć da napraviš nešto prelepo ili da ubiješ performanse.

Ako si pročitao poglavlje 06 o koordinatnim prostorima i transformacijama, ovde ćeš videti gde se te transformacije zapravo dešavaju unutar pipeline-a. Ako nisi -- ne brini, objašnjavaćemo stvari i ovde, ali ti toplo preporučujem da se vratiš na poglavlje 06 posle čitanja ovog.

Hajde da počnemo.


1. Pogled iz ptičje perspektive: Tri velike faze

Pre nego što zaronimo u detalje, hajde da uspostavimo mapu terena. Ceo render pipeline možemo podeliti u tri velike konceptualne faze:

1.1 Application Stage (Faza aplikacije)

Ovo je faza koja se izvršava na CPU-u. Tu živi tvoj game engine -- Unreal Engine 5 u našem slučaju. Ova faza je odgovorna za:

Analogija iz stvarnog života: Application stage je kao producent filma. Ne snima scene sam, ali organizuje sve -- odlučuje šta se snima, priprema glumce, postavlja scenografiju, i daje instrukcije ekipi.

Ključna stvar: Ova faza je potpuno pod kontrolom engine-a i tvog koda. Svaka optimizacija ovde se odnosi na CPU performanse. Ako je tvoj CPU bottleneck -- ovde tražiš problem.

1.2 Geometry Stage (Faza geometrije)

Kada CPU pripremi sve podatke i pošalje ih GPU-u, geometrijska faza preuzima. Ova faza se izvršava na GPU-u i bavi se vertex-ima (temenima) i primitivima (trouglovima, linijama, tačkama). Ovde se dešava:

Analogija: Geometry stage je kao tim koji pravi maketu za film. Uzimaju nacrt (vertex podatke), oblikuju svaki element (transformacije), po potrebi dodaju više detalja (tessellation), i sve pozicioniraju na pravo mesto u kadru.

1.3 Rasterization Stage (Faza rasterizacije)

Ovo je finalna velika faza, i ona se takođe izvršava na GPU-u. Njena uloga je da pretvori geometriju (trouglove definisane vertex-ima u 2D prostoru ekrana) u piksele na ekranu. Ovde se dešava:

Analogija: Rasterization stage je kao ekipa za post-produkciju filma. Uzimaju sirovi snimak (geometriju na ekranu), dodaju boju, svetlo, efekte, i proizvode finalnu sliku koju publika vidi.

Vizuelni pregled toka

Application Stage (CPU)         Geometry Stage (GPU)           Rasterization Stage (GPU)
┌─────────────────────┐    ┌──────────────────────────┐    ┌─────────────────────────┐
│                     │    │                          │    │                         │
│  Scene Management   │    │   Vertex Shader          │    │   Rasterizer            │
│  Culling            │───>│   Tessellation (opt.)    │───>│   Fragment/Pixel Shader │
│  Draw Call Prep     │    │   Geometry Shader (opt.) │    │   Output Merger         │
│  Data Upload        │    │   Clipping               │    │   Framebuffer           │
│                     │    │   Screen Mapping         │    │                         │
└─────────────────────┘    └──────────────────────────┘    └─────────────────────────┘

Ovo je makro pogled. Sada hajde da zaronimo u svaku od ovih faza sa mnogo više detalja.


2. Vertex Shader: Gde svaki vertex dobija svoje mesto

2.1 Šta je Vertex Shader?

Vertex shader je prvi programmable stage u grafičkom pipeline-u. To je mali program koji se izvršava jednom za svaki vertex tvoje geometrije. Kada pošalješ trougao koji ima tri vertex-a, vertex shader se pokreće tri puta -- jednom za svaki.

Zamišljaj to ovako: imaš listu gostiju za žurku (vertex-e), i svaki gost prolazi kroz istu proceduru na ulazu -- provera identiteta, dodela stola, prilagođavanje odela. Svaki gost prolazi kroz istu proceduru, ali sa svojim ličnim podacima.

2.2 Ulazi (Inputs)

Vertex shader prima takozvane vertex attributes -- podatke koji su vezani za svaki vertex. Tipični atributi su:

Atribut Opis Tipičan format
Position Pozicija vertex-a u lokalnom prostoru objekta float3 (x, y, z)
Normal Normala -- vektor koji pokazuje "spolja" u odnosu na površinu float3
Tangent Tangent vektor -- koristi se za normal mapping float4 (xyz + sign)
UV (TexCoord) Teksturne koordinate -- gde na teksturi ovaj vertex "gleda" float2 (u, v)
Color Boja vertex-a (vertex color) float4 (RGBA)
Bone Indices Za skinned mesh-eve -- indeksi kostiju koje utiču na ovaj vertex int4
Bone Weights Težine uticaja svake kosti float4

Pored vertex atributa, vertex shader ima pristup i uniform/constant podacima -- podacima koji su isti za sve vertex-e u jednom draw call-u. Najvažniji su:

2.3 Šta radi Vertex Shader?

MVP Transformacija

Najfundamentalnija operacija vertex shader-a je MVP (Model-View-Projection) transformacija. Kao što smo detaljno objasnili u poglavlju 06, svaki vertex počinje u svom lokalnom prostoru (object space) i mora da prođe kroz seriju transformacija da bi stigao u clip space, odakle ga pipeline dalje procesira.

Object Space → (Model Matrix) → World Space → (View Matrix) → Camera Space → (Projection Matrix) → Clip Space

U kodu, ovo izgleda otprilike ovako (pseudo-HLSL):

float4 worldPosition = mul(ModelMatrix, float4(inputPosition, 1.0));
float4 viewPosition = mul(ViewMatrix, worldPosition);
float4 clipPosition = mul(ProjectionMatrix, viewPosition);

// Output pozicija u clip space
output.Position = clipPosition;

U praksi, ove tri matrice se često kombinuju u jednu MVP matricu pre slanja na GPU, tako da je cela transformacija samo jedno množenje matrice:

output.Position = mul(MVPMatrix, float4(inputPosition, 1.0));

Ovo je efikasnije jer umesto tri množenja matrica 4x4 sa vektorom (ukupno 48 multiply-add operacija), imaš samo jedno (16 multiply-add operacija). Kad imaš mesh sa 100.000 vertex-a, ta razlika se oseti.

Transformacija normala

Normale se ne transformišu istom matricom kao pozicije! Ovo je česta greška. Ako objekat ima neuniformno skaliranje (recimo, rastegnut je po X osi), normala transformisana Model matricom neće biti korektno orijentisana. Umesto toga, koristimo inverse transpose Model matrice:

float3 worldNormal = normalize(mul((float3x3)InverseTransposeModelMatrix, inputNormal));

Ovo smo objasnili u poglavlju 06, ali vredi ponoviti jer je to jedna od onih stvari koje kad pogrešiš -- osvetljenje izgleda čudno i satima tražiš grešku.

Skeletal Animation (Skinning)

Kada imaš animiran lik (karakter), mesh tog lika je vezan za skelet -- set "kostiju" (bones) koje se pomeraju tokom animacije. Svaki vertex je pod uticajem jedne ili više kostiju, sa određenim težinama. Vertex shader računa finalnu poziciju vertex-a kao weighted blend pozicija koje bi vertex imao pod uticajem svake kosti:

float4 skinnedPosition = float4(0, 0, 0, 0);
for (int i = 0; i < 4; i++)
{
    float4x4 boneMatrix = BoneMatrices[input.BoneIndices[i]];
    skinnedPosition += input.BoneWeights[i] * mul(boneMatrix, float4(input.Position, 1.0));
}

Ovo je razlog zašto animirani likovi mogu biti skupi -- svaki vertex zahteva do 4 množenja sa matricom samo za skinning, plus standardnu MVP transformaciju. Ako imaš lik sa 50.000 vertex-a i 4 bone uticaja po vertex-u, to je 200.000 množenja matrice samo za skinning. Na modernim GPU-ovima ovo nije problem za jednog lika, ali kada imaš 50 likova na ekranu -- postaje relevantno.

U Unreal Engine 5 ovo je dodatno optimizovano. UE5 koristi GPU skinning po default-u, što znači da se skinning izračunava na GPU-u umesto na CPU-u, što je mnogo efikasnije jer GPU ima hiljade jezgara koja mogu da procesiraju vertex-e paralelno.

Vertex Deformation (Deformacija vertex-a)

Vertex shader je idealno mesto za proceduralne deformacije -- efekte koji menjaju oblik geometrije u realnom vremenu bez potrebe za animacijom:

Primer jednostavnog vetar efekta:

// Jednostavan wind shader - gornji deo mesh-a se pomera više
float windStrength = sin(Time * WindSpeed + worldPosition.x * 0.5) * WindIntensity;
float heightFactor = saturate(input.Position.y / MeshHeight); // 0 na dnu, 1 na vrhu
worldPosition.x += windStrength * heightFactor * heightFactor; // Quadratic falloff

Ovaj pristup je genijalan u svojoj jednostavnosti -- jeftin je (par sinusa i množenja), a vizuelni rezultat je ubedljiv.

Prosleđivanje podataka ka narednim fazama

Vertex shader ne samo da izračunava clip space poziciju vertex-a -- on takođe priprema podatke koji će biti potrebni u kasnijim fazama, naročito u pixel shader-u. Tipični izlazi, pored pozicije, su:

Ovi podaci se onda interpoliraju tokom rasterizacije (o čemu ćemo govoriti u sekciji o rasterizeru) i stižu u pixel shader kao glatko interpilirani vrednosti za svaki fragment.

2.4 Zašto je Vertex Shader programabilan?

U ranim danima računarske grafike (pre shader-a), vertex procesiranje je bilo fixed-function -- hardver je mogao da radi samo unapred definisane operacije: transformaciju, osvetljenje (Gouraud shading), i to je bilo to. Nije bilo moguće implementirati wind shader, proceduralni ocean, ili bilo šta kreativno sa vertex-ima.

Sa uvođenjem programabilnih vertex shader-a (DirectX 8, oko 2001. godine), developeri su dobili mogućnost da napišu proizvoljan program koji se izvršava za svaki vertex. Ovo je otvorilo vrata za:

Programabilnost vertex shader-a je jedna od fundamentalnih stvari koje su omogućile modernu grafiku kakvu danas znamo. Bez nje, svaki novi vizuelni efekat bi zahtevao novi hardver.

2.5 Performansne implikacije

Vertex shader se izvršava za svaki vertex u sceni. Ako imaš scenu sa 10 miliona vertex-a (što nije neuobičajeno za modernu igru), vertex shader se izvršava 10 miliona puta po frejmu. To znači:

Ipak, na modernim GPU-ovima, vertex shader retko predstavlja bottleneck osim u ekstremnim slučajevima (ogromna geometrija bez LOD-a, veoma kompleksni vertex shaderi). Pixel shader je obično mnogo veći problem, ali o tome kasnije.


3. Tessellation: Kada ti treba više geometrije

3.1 Šta je Tessellation?

Zamisli da imaš origami figuru napravljenu od velikih komada papira -- izgleda grubо, uglasto. Sada zamisli da svaki taj komad papira zameniš sa mnogo manjih komadića koji prate isti oblik, ali sa mnogo više detalja i glatkim prelazima. To je suština tessellation-a.

Tessellation je proces deljenja postojeće geometrije na veći broj manjih primitiva (trouglova). Umesto da šalješ mesh sa milionima trouglova sa diska u memoriju i preko bus-a na GPU, šalješ jednostavan mesh (low-poly) i kažeš GPU-u "podeli svaki trougao na N manjih trouglova i pomeri nove vertex-e prema ovoj displacement mapi".

Tessellation u modernom grafičkom pipeline-u sastoji se od tri pod-faze:

3.2 Hull Shader (Faza 1)

Hull shader je programmable stage koji prima kontrolne tačke (control points) jednog patch-a (parče površine, obično trougao ili quad) i proizvodi:

  1. Modifikovane kontrolne tačke: Hull shader može da pomeri ili modifikuje kontrolne tačke pre tessellation-a.
  2. Tessellation faktore: Ovo je ključni izlaz -- koliko se svaka ivica i unutrašnjost patch-a treba podeliti.

Tessellation faktor je, u suštini, broj koji kaže "podeli ovu ivicu na N segmenata". Ako je faktor 1, nema deljenja. Ako je faktor 4, svaka ivica se deli na 4 segmenta, što daje mnogo više trouglova.

Hull shader tipično izračunava tessellation faktor na osnovu:

// Pseudo-kod za distance-based tessellation faktor
float ComputeTessellationFactor(float3 worldPos)
{
    float distance = length(worldPos - CameraPosition);
    float factor = MaxTessellation * saturate(1.0 - distance / MaxDistance);
    return max(1.0, factor); // Minimum 1 (nema deljenja)
}

Ovo je odličan primer adaptivnog pristupa -- trošiš resurse tamo gde se vide, a štediš gde se ne vide.

3.3 Tessellator (Faza 2 -- Fixed-Function)

Tessellator je fixed-function hardware koji prima tessellation faktore od hull shader-a i generiše nove vertex-e i trouglove prema tim faktorima. Ti ne programiraš tessellator -- on radi uvek isto:

  1. Prima patch sa tessellation faktorima.
  2. Generiše mrežu novih vertex-a unutar patch-a.
  3. Povezuje te vertex-e u trouglove.
  4. Za svaki novi vertex izračunava barycentric coordinates (baricentrične koordinate) -- koji govore gde se novi vertex nalazi u odnosu na originalne kontrolne tačke.

Razlog zašto je ovo fixed-function je efikasnost -- generisanje mreže vertex-a prema faktorima je deterministički, matematički jasno definisan proces. Implementacija u hardveru je mnogo brža nego što bi bila u programabilnom shader-u.

3.4 Domain Shader (Faza 3)

Domain shader je programmable stage koji se izvršava za svaki novi vertex koji tessellator generiše. Prima:

Domain shader je mesto gde se dešava "prava magija" tessellation-a. Ovde tipično radiš:

  1. Interpolaciju atributa: Koristeći baricentrične koordinate, interpoliraš poziciju, normale, UV koordinate između kontrolnih tačaka.
  2. Displacement: Koristiš displacement mapu (teksturu) da pomeriš interpoliranu poziciju duž normale. Ovo je razlog zašto tessellation postoji -- dodaje geometrijski detalj iz teksture.
  3. Finalnu transformaciju: MVP transformacija novog vertex-a.
// Pseudo-kod domain shader-a
DomainShaderOutput DS(PatchConstOutput patchConst, float3 bary : SV_DomainLocation,
                       const OutputPatch<HullOutput, 3> patch)
{
    // Interpolacija pozicije korišćenjem baricentričnih koordinata
    float3 position = bary.x * patch[0].Position
                    + bary.y * patch[1].Position
                    + bary.z * patch[2].Position;

    float2 uv = bary.x * patch[0].UV
              + bary.y * patch[1].UV
              + bary.z * patch[2].UV;

    float3 normal = normalize(bary.x * patch[0].Normal
                            + bary.y * patch[1].Normal
                            + bary.z * patch[2].Normal);

    // Displacement!
    float displacement = DisplacementMap.SampleLevel(sampler, uv, 0).r;
    position += normal * displacement * DisplacementScale;

    // MVP transformacija
    output.Position = mul(MVPMatrix, float4(position, 1.0));
    return output;
}

3.5 Kada se koristi Tessellation?

Tessellation je moćan alat, ali nije besplatan. Tipični use-case-ovi su:

3.6 Zašto je Tessellation skup?

Tessellation može dramatično povećati broj vertex-a i trouglova. Ako imaš tessellation faktor 8 na trouglu, taj jedan trougao postaje 64 trougla (8x8). Ako imaš mesh sa 10.000 trouglova i tessellation faktor 8 svuda, odjednom imaš 640.000 trouglova. Ovo opterećuje:

  1. Domain shader: Koji se izvršava za svaki novi vertex.
  2. Rasterizer: Koji mora da procesira mnogo više trouglova.
  3. Pixel shader: Ako su trouglovi veoma mali (sub-pixel), dobijamo quad overdraw problem -- o čemu ćemo detaljno u poglavlju 15 o shader optimizaciji.
  4. Memory bandwidth: Više vertex-a = više podataka koji se kreću kroz pipeline.

Zato je adaptivni tessellation (na osnovu distance) kritičan. Koristiti uniformno visok tessellation faktor je recept za loše performanse.

3.7 Tessellation u Unreal Engine 5 i Nanite

Zanimljiva stvar: UE5-ov Nanite sistem u velikoj meri zamenjuje tradicionalni tessellation za statičnu geometriju. Nanite radi virtuelizaciju geometrije -- dinamički bira nivo detalja na nivou klastera trouglova i stream-uje geometriju po potrebi. Ovo je fundamentalno drugačiji pristup od tessellation-a, ali rešava sličan problem: kako prikazati pravu količinu detalja tamo gde je potrebna.

Ipak, tessellation je i dalje relevantan za:

O Nanite-u detaljno govorimo u poglavlju 14.


4. Geometry Shader: Moćan ali problematičan

4.1 Šta je Geometry Shader?

Geometry shader je programmable stage koji dolazi posle vertex shader-a (i tessellation-a, ako postoji). Dok vertex shader radi sa jednim vertex-om, geometry shader radi sa celim primitivom -- trouglom (3 vertex-a), linijom (2 vertex-a), ili tačkom (1 vertex).

Ključna sposobnost geometry shader-a je nešto što nijedan drugi shader ne može: on može da promeni broj primitiva. Može da:

Ovo zvuči neverovatno moćno, i jeste -- konceptualno. U praksi, geometry shader ima ozbiljne performansne probleme.

4.2 Šta geometry shader može da radi?

Evo nekih klasičnih primera:

Generisanje billboarda od tačaka

Imaš particle sistem gde je svaki particle samo tačka (jedan vertex). Geometry shader prima tu tačku i generiše quad (dva trougla) koji je uvek okrenut ka kameri:

[maxvertexcount(4)]
void GS(point VSOutput input[1], inout TriangleStream<GSOutput> stream)
{
    float3 center = input[0].WorldPos;
    float size = input[0].Size;
    float3 right = CameraRight * size;
    float3 up = CameraUp * size;

    // Emituj 4 vertex-a koji čine quad
    EmitVertex(center - right - up, float2(0, 1));
    EmitVertex(center + right - up, float2(1, 1));
    EmitVertex(center - right + up, float2(0, 0));
    EmitVertex(center + right + up, float2(1, 0));
    stream.RestartStrip();
}

Wireframe rendering

Primaš trougao, izračunavaš udaljenost svakog piksela od ivice trougla, i renderoviješ samo ivice. Geometry shader prosleđuje informacije o ivicama ka pixel shader-u.

Fur/Grass rendering (Shell method)

Primaš trougao i emituješ više kopija tog trougla, svaku malo pomerenu po normali, što stvara iluziju krzna ili trave.

Shadow volume generisanje

Primaš trougao i na osnovu pravca svetla generišeš silhouette geometriju za stencil shadow volumes.

4.3 Stream Output

Geometry shader ima još jednu jedinstvenu sposobnost: stream output. Umesto (ili pored) slanja rezultata dalje u pipeline ka rasterizeru, geometry shader može da upiše rezultate nazad u buffer na GPU-u. Ovi podaci se onda mogu ponovo koristiti kao input za sledeći render pass ili čak za drugi draw call.

Stream output je koristan za:

Ipak, u modernoj praksi, većinu ovih zadataka obavljaju compute shaderi koji su efikasniji i fleksibilniji (o čemu ćemo govoriti na kraju poglavlja).

4.4 Zašto se Geometry Shader retko koristi u praksi?

Ovo je važna lekcija o razlici između teorije i prakse u game developmentu. Geometry shader zvuči kao savršen alat -- možeš da generišeš geometriju na GPU-u! Ali u praksi, ima ozbiljne probleme:

Problem 1: Serijalizacija

GPU je dizajniran za masivnu paralelizaciju. Vertex shader obrađuje hiljade vertex-a paralelno. Pixel shader obrađuje hiljade piksela paralelno. Ali geometry shader ima problem: pošto može da emituje promenljiv broj primitiva, hardver ne zna unapred koliko izlaznih podataka da alocira. Ovo dovodi do serijalizacije -- GPU mora da čeka da jedan geometry shader invocation završi pre nego što zna gde u memoriji da stavi rezultate sledećeg.

Problem 2: Ograničen output buffer

Geometry shader ima ograničen maksimalan broj vertex-a koje može da emituje po invokaciji (tipično 256-1024, zavisi od hardvera i dužine izlaznih podataka). Ovo ograničava kompleksnost onoga što možeš da radiš.

Problem 3: Loša interakcija sa hardverskim optimizacijama

Moderni GPU-ovi imaju sofisticirane mehanizme za keširanjevertex-a (post-transform vertex cache). Geometry shader može da poremeti ove optimizacije jer generiše nove vertex-e koji ne mogu da se kešraju na isti način.

Problem 4: Bolji alternativni pristupi

Za većinu toga što geometry shader radi, postoje bolje alternative:

Zbog svega ovoga, u Unreal Engine 5 i modernom game developmentu generalno, geometry shader se veoma retko koristi. Ako vidiš geometry shader u nečijem kodu, to je obično ili legacy code, ili veoma specifičan use-case gde alternative nisu praktične.


5. Rasterizer: Od trouglova do fragmenata

5.1 Šta je rasterizacija?

Sada dolazimo do jednog od najvažnijih koraka u celom pipeline-u. Do ovog trenutka, imali smo geometriju definisanu vertex-ima u 2D prostoru ekrana (nakon projekcije i perspective divide). Ali ekran ne prikazuje vertex-e i ivice -- prikazuje piksele. Rasterizacija je proces pretvaranja geometrije (trouglova) u fragmente koji odgovaraju pikselima na ekranu.

Analogija: Zamisli da imaš geometrijski oblik nacrtan na papiru (trougao), i treba da popuniš grid papir (piksel grid) tako da svaki kvadratić koji je unutar trougla bude označen. To je rasterizacija.

5.2 Šta je fragment?

Pre nego što nastavimo, hajde da razjasnimo razliku između fragmenta i piksela, jer se ovi termini često pogrešno koriste kao sinonimi.

Zašto razlika? Zato što za jedan piksel može postojati više fragmenata. Ako se dva trougla preklapaju na istom pikselu, svaki generiše svoj fragment. Depth test (test dubine) kasnije odlučuje koji fragment "pobedi" i postane konačni piksel. Kod transparentnih objekata, više fragmenata doprinosi finalnoj boji piksela kroz blending.

5.3 Kako rasterizer radi?

Rasterizacija se konceptualno odvija u dva koraka:

Triangle Setup

Za svaki trougao, rasterizer izračunava edge equations (jednačine ivica). Svaka ivica trougla se može predstaviti kao linearna jednačina u 2D prostoru:

E(x, y) = A*x + B*y + C

Gde su A, B, C konstante izvedene iz pozicija dva vertex-a koja definišu tu ivicu. Ova jednačina ima korisno svojstvo: za tačku (x, y) koja je na jednoj strani ivice, E je pozitivno; na drugoj strani, negativno; na samoj ivici, nula.

Trougao ima tri ivice, dakle tri jednačine E0, E1, E2. Tačka je unutar trougla ako i samo ako su sve tri edge function-e istog znaka (sve pozitivne ili sve negativne, zavisno od konvencije):

Fragment pripada trouglu ako: E0(x,y) >= 0 AND E1(x,y) >= 0 AND E2(x,y) >= 0

Ovo je matematički elegantan i hardverski efikasan test.

Triangle Traversal

Kada su edge equations postavljene, rasterizer mora da prođe kroz piksele i odredi koji su unutar trougla. Postoji više strategija:

  1. Brute force: Proveri svaki piksel na ekranu. Ovo je očigledno neefikasno.
  2. Bounding box: Izračunaj pravougaonik koji ograničava trougao (bounding box) i proveri samo piksele unutar njega. Ovo je mnogo bolje.
  3. Tile-based: Podeli ekran na tile-ove (obično 8x8 ili 16x16 piksela) i prvo odredi koji tile-ovi se preklapaju sa trouglom, pa onda proveri piksele samo unutar tih tile-ova. Ovo je pristup koji koristi većina modernih GPU-ova.
  4. Hierarchical: Hijerarhijski pristup koji kombinuje prethodne -- prvo na nivou velikih blokova, pa sve sitnije. Slično quad-tree pristupu.

Moderni GPU-ovi koriste varijacije tile-based pristupa i procesiraju piksele u grupama (obično 2x2 blokovi zvani quads). Ovo procesiranje u quad-ovima je važno i vraćamo se na njega u poglavlju 15, jer ima značajne implikacije za performanse pixel shader-a.

5.4 Interpolacija atributa

Kada rasterizer odredi da je fragment unutar trougla, on mora da izračuna vrednosti atributa za taj fragment. Vertex shader je izračunao atribute samo za vertex-e (temena) trougla -- ali fragment je negde između tih temena. Rasterizer koristi baricentričnu interpolaciju da odredi vrednosti atributa za svaki fragment.

Baricentrične koordinate (u, v, w) su tri broja koji opisuju gde se tačka nalazi u odnosu na tri temena trougla. Ako je tačka na temenu A, onda je (u=1, v=0, w=0). Ako je na sredini ivice između A i B, onda je (u=0.5, v=0.5, w=0). Bilo koja tačka unutar trougla može se izraziti kao (u, v, w) gde je u+v+w=1.

Vrednost atributa u fragmentu se računa kao:

AttributeFragment = u * AttributeA + v * AttributeB + w * AttributeC

Ovo se primenjuje na sve atribute: UV koordinate, normale, boje, pozicije, i sve ostalo što vertex shader prosledi.

Perspektivno-korektna interpolacija

Postoji jedan suptilni ali važan detalj: interpolacija mora da bude perspektivno korektna. Linearna interpolacija u screen space-u ne daje korektne rezultate kada je perspektivna projekcija u igri (a gotovo uvek jeste). Bez perspektivne korekcije, teksture bi izgledale iskrivljeno na površinama koje su pod uglom u odnosu na kameru.

Rasterizer automatski radi perspektivno-korektnu interpolaciju koristeći homogenu koordinatu (w) iz clip space-a. Ovo je jedan od razloga zašto je vertex shader dužan da izračuna clip space poziciju sa korektnom w koordinatom.

Ako si nekada video stare PS1 igre i primetio kako teksture "plivaju" i iskrivljuju se na površinama -- to je upravo posledica nedostatka perspektivno-korektne interpolacije. PlayStation 1 je radio linearnu interpolaciju u screen space-u umesto perspektivno-korektne, jer je ovo drugo bilo preskupo za hardver tog doba.

5.5 Zašto je rasterizer fixed-function?

Rasterizer je jedan od delova pipeline-a koji je fixed-function -- ne programiraš ga, hardver ga radi automatski. Zašto?

  1. Determinizam: Rasterizacija mora da bude konzistentna -- isti trougao mora uvek da generiše iste fragmente. Ovo je kritično za korektno renderovanje (da nema rupa između trouglova, da se ivice pravilno poklapaju).

  2. Brzina: Rasterizacija je operacija koja se mora izvršiti za svaki trougao u sceni, i mora biti neverovatno brza. Hardverska implementacija je reda veličine brža od bilo čega što bismo mogli da napišemo u programmable shader-u.

  3. Jednostavnost zadatka: Iako je internalno kompleksan, koncept rasterizacije je jasno definisan -- "za dati trougao, odredi koji pikseli su unutar njega i interpoliraj atribute". Nema potrebe za fleksibilnošću -- svi žele istu stvar od rasterizera.

  4. Paralelizacija: Hardverska implementacija može da iskoristi specifične optimizacije (hijerarhijsko testiranje, early-Z rejection, quad procesiranje) koje bi bile teške ili nemoguće u generalnom programabilnom okruženju.

5.6 Dodatne operacije rasterizera

Rasterizer obavlja i neke dodatne operacije pored samog određivanja fragmenata:


6. Fragment/Pixel Shader: Srce vizuelnog kvaliteta

6.1 Šta je Pixel Shader?

Sada dolazimo do najvažnijeg programabilnog stage-a u celom pipeline-u -- barem sa stanovišta vizuelnog kvaliteta i performansi. Pixel shader (DirectX terminologija) ili fragment shader (OpenGL terminologija) je program koji se izvršava za svaki fragment koji rasterizer generiše.

Dok vertex shader određuje gde je geometrija, pixel shader određuje kako izgleda -- koju boju, sjaj, transparentnost, emisiju ima svaki piksel na ekranu. Sve ono što čini igru vizuelno impresivnom -- realistični materijali, osvetljenje, senke, refleksije -- dešava se ovde.

6.2 Ulazi pixel shader-a

Pixel shader prima interpolirane atribute od rasterizera. Svaki atribut koji je vertex shader izračunao za vertex-e trougla, rasterizer je interpolirao za ovaj specifičan fragment. Tipični ulazi su:

Pored interpoliranih atributa, pixel shader ima pristup i:

6.3 Texture Sampling

Jedna od najčešćih operacija u pixel shader-u je texture sampling -- čitanje boje iz teksture na datim UV koordinatama. Ovo zvuči jednostavno, ali iza scene se dešava mnogo toga:

Filtriranje

Teksture retko savršeno odgovaraju pikselima na ekranu. Kada jedan piksel "pokriva" deo teksture koji ne odgovara tačno jednom texel-u (piksel teksture), koristi se filtriranje:

Mipmapping

Mipmaps su prethodno izračunate umanjene verzije teksture. Originalna tekstura 1024x1024 ima mipmaps: 512x512, 256x256, 128x128, ... 1x1. Kada je objekat daleko od kamere i tekstura zauzima malo piksela, koristi se manji mipmap nivo. Ovo:

  1. Poboljšava kvalitet: Bez mipmaps, daleke teksture imaju moiré pattern i aliasing.
  2. Poboljšava performanse: Manji mipmap nivo znači bolje iskorišćenje texture cache-a. O texture cache-u i jeho uticaju na performanse govorimo u poglavlju 11.

GPU hardver automatski bira pravi mipmap nivo na osnovu toga koliko se UV koordinate menjaju između susednih piksela (takozvani texture gradients ili derivatives). Ovo je još jedan razlog zašto pixel shaderi rade u 2x2 quad-ovima -- da bi mogli da izračunaju ove derivate.

6.4 Izračunavanje osvetljenja

Pixel shader je mesto gde se dešava lighting -- izračunavanje kako svetlo interaguje sa površinom. Ovo je ogroman i kompleksan topic koji zaslužuje posebno poglavlje (poglavlje 08 o Deferred vs Forward renderovanju), ali hajde da pokrijemo osnove.

Lighting modeli

Moderni rendering koristi Physically Based Rendering (PBR) model koji simulira kako svetlo zapravo interaguje sa površinama u stvarnom svetu. Ključni koncepti su:

U Unreal Engine 5, PBR model koristi ove ključne parametre (poznate svakom UE5 korisniku):

Izračunavanje osvetljenja za jedan fragment u PBR modelu uključuje:

// Pojednostavljen PBR lighting (pseudo-kod)
float3 N = normalize(worldNormal);        // Normala površine
float3 V = normalize(CameraPos - worldPos); // Pravac ka kameri
float3 L = normalize(LightPos - worldPos);  // Pravac ka svetlu
float3 H = normalize(V + L);                // Half vector

float NdotL = saturate(dot(N, L));
float NdotH = saturate(dot(N, H));
float NdotV = saturate(dot(N, V));

// Diffuse (Lambert)
float3 diffuse = BaseColor / PI;

// Specular (Cook-Torrance BRDF)
float D = DistributionGGX(NdotH, Roughness);     // Normal distribution
float G = GeometrySmith(NdotV, NdotL, Roughness); // Geometry term
float3 F = FresnelSchlick(dot(H, V), F0);         // Fresnel

float3 specular = (D * G * F) / (4.0 * NdotV * NdotL + 0.001);

// Kombinovanje
float3 kD = (1.0 - F) * (1.0 - Metallic); // Metali nemaju diffuse
float3 finalColor = (kD * diffuse + specular) * LightColor * NdotL;

I ovo je samo za jedno svetlo! U sceni sa N svetala, ovo se ponavlja za svako svetlo (u forward rendering pristupu) ili se svetla procesiraju u zasebnom pass-u (u deferred rendering pristupu). O ovoj razlici detaljno u poglavlju 08.

Normal Mapping

Možda jedan od najvažnijih trikova u real-time grafici. Umesto da modeliraš svaku sitnicu na površini (što bi zahtevalo milione trouglova), koristiš normal mapu -- teksturu koja sadrži modifikovane normale za svaki piksel.

Pixel shader čita normalu iz normal mape i koristi je umesto interpolirane geometrijske normale za izračunavanje osvetljenja. Rezultat: površina izgleda kao da ima mnogo više geometrijskog detalja nego što zapravo ima.

// Normal mapping (u tangent space)
float3 normalMap = NormalTexture.Sample(sampler, uv).rgb;
normalMap = normalMap * 2.0 - 1.0; // Iz [0,1] u [-1,1]

// Transformacija iz tangent space u world space
float3x3 TBN = float3x3(worldTangent, worldBitangent, worldNormal);
float3 modifiedNormal = normalize(mul(normalMap, TBN));

Ovo je jedan od onih slučajeva gde razumevanje koordinatnih prostora (poglavlje 06) postaje praktično važno -- moraš da transformišeš normalu iz tangent space-a teksture u world space gde se računa osvetljenje.

6.5 Render Targets i Multiple Render Targets (MRT)

Pixel shader ne mora da upisuje samo jednu boju. Može da upisuje u više render target-a istovremeno (Multiple Render Targets, MRT). Ovo je fundamentalno za deferred rendering:

// Deferred rendering - GBuffer pass
struct GBufferOutput
{
    float4 Albedo   : SV_Target0; // Boja materijala
    float4 Normal   : SV_Target1; // Normala površine (encoded)
    float4 Material : SV_Target2; // Metallic, Roughness, AO, ...
    float  Depth    : SV_Target3; // Dubina
};

GBufferOutput PS(PSInput input)
{
    GBufferOutput output;
    output.Albedo = AlbedoTexture.Sample(sampler, input.UV);
    output.Normal = float4(EncodeNormal(modifiedNormal), 1.0);
    output.Material = float4(Metallic, Roughness, AO, 1.0);
    output.Depth = input.Position.z;
    return output;
}

Ovo omogućava da se geometrija renderuje u jednom pass-u, a osvetljenje izračunava u drugom, na osnovu podataka upisanih u GBuffer. O ovome mnogo detaljnije u poglavlju 08.

6.6 Discard (Clip) -- Odbacivanje fragmenta

Pixel shader ima mogućnost da odbaci fragment -- da kaže "ovaj fragment ne postoji". Ovo se koristi za:

// Alpha test
float alpha = AlbedoTexture.Sample(sampler, input.UV).a;
if (alpha < AlphaThreshold)
    discard; // Ne piši ovaj fragment

Važna napomena: discard (u HLSL-u clip()) može da utiče na performanse jer sprečava neke hardverske optimizacije, naročito early-Z rejection. O tome više u poglavlju 15 o shader optimizaciji.

6.7 Zašto je Pixel Shader najskuplji stage?

Pixel shader je gotovo uvek najskuplji deo pipeline-a u modernim igrama. Evo zašto:

  1. Broj invokacija: Za Full HD rezoluciju (1920x1080), imaš 2.073.600 piksela. Ali svaki piksel može biti pokriven sa više trouglova (overdraw), tako da se pixel shader može izvršiti mnogo više od 2 miliona puta. Za 4K (3840x2160), to je 8.294.400 piksela -- četiri puta više!

  2. Kompleksnost: Pixel shader tipično sadrži mnogo više koda od vertex shader-a -- texture sampling, normal mapping, PBR osvetljenje, shadow map sampling, refleksije, efekti...

  3. Texture sampling latency: Čitanje iz teksture je spora operacija (sa stanovišta GPU-a). GPU pokušava da sakrije ovu latenciju izvršavajući druge instrukcije dok čeka podatke, ali sa mnogo texture sampel-ova, ovo postaje teško.

  4. Overdraw: Ako se objekti preklapaju (a to se gotovo uvek dešava), pixel shader se izvršava za fragmente koji će na kraju biti odbačeni depth testom. Ovo je čist gubitak. Zato je draw call sortiranje (front-to-back za opaque objekte) važno -- o čemu govorimo u poglavlju 10.

  5. Rezolucija: Broj piksela raste kvadratno sa rezolucijom. Prelazak sa 1080p na 4K znači 4x više pixel shader invokacija. Ovo je razlog zašto je rezolucija jedan od najvažnijih faktora performansi, i zašto tehnike kao Temporal Super Resolution (TSR) u UE5 postoje -- renderuješ na nižoj rezoluciji i upscale-uješ. O TSR-u govorimo u poglavlju 16.


7. Output Merger: Poslednja stanica pre ekrana

7.1 Šta je Output Merger?

Kada pixel shader izračuna boju fragmenta, to još nije kraj putovanja. Fragment mora da prođe kroz Output Merger (u OpenGL terminologiji: Per-Fragment Operations) -- seriju testova i operacija koje određuju da li i kako se fragment upisuje u framebuffer.

Output Merger je uglavnom fixed-function (iako se pojedini aspekti mogu konfigurisati), i obavlja sledeće operacije redom:

7.2 Depth Test (Test dubine)

Depth test (z-test) je fundamentalni mehanizam koji rešava problem vidljivosti -- koji objekat je ispred kojeg?

Kako funkcioniše

Pored color buffer-a (koji čuva boje piksela), postoji i depth buffer (z-buffer) -- tekstura iste rezolucije kao ekran, ali umesto boja, čuva dubinu svakog piksela (koliko je daleko od kamere).

Kada novi fragment stigne do output merger-a, njegova dubina se poredi sa dubinom koja je već zapisana u depth buffer-u za taj piksel:

Depth test: if (fragment.depth < depthBuffer[x][y])
{
    colorBuffer[x][y] = fragment.color;
    depthBuffer[x][y] = fragment.depth;
}
// inače: fragment se odbacuje

Ovo je genijalno jednostavan algoritam, a rešava kompleksan problem. Bez depth buffer-a, morali bismo da sortiramo sve trouglove od najdaljih ka najbližima (Painter's algorithm), što je sporo i ne radi korektno za sve slučajeve (interpenetrirajuća geometrija).

Depth comparison funkcije

Depth test ne mora uvek da bude "bliže = bolje". Comparison funkcija se može konfigurisati:

Funkcija Uslov Tipična upotreba
Less fragment.z < buffer.z Standardno renderovanje (bliže pobjeđuje)
LessEqual fragment.z <= buffer.z Isto kao Less, ali dozvoljava jednaku dubinu
Greater fragment.z > buffer.z Obrnuti z-buffer (reversed-Z), koji UE5 koristi
Equal fragment.z == buffer.z Decal rendering, multi-pass rendering
Always uvek prolazi Sky rendering, overlay efekti
Never nikada ne prolazi Debugging, specifične optimizacije

Reversed-Z u Unreal Engine 5

UE5 koristi reversed-Z -- near plane se mapira na Z=1, a far plane na Z=0. Ovo zvuči kontraintuitivno, ali ima matematički razlog: floating-point brojevi imaju veću preciznost oko 0, a dubina je najvažnija blizu kamere (gde se objekti preklapaju). Sa standardnim Z-om, preciznost je loša blizu kamere i dobra daleko -- suprotno od onoga što trebaš. Reversed-Z rešava ovaj problem.

Early-Z (Early Depth Test)

Standardno, depth test se dešava posle pixel shader-a. Ali ako pixel shader ne menja dubinu fragmenta (a to je najčešći slučaj), GPU može da uradi depth test pre pixel shader-a. Ovo se zove Early-Z ili early depth test.

Prednost je enormna: ako fragment ne prođe depth test, pixel shader se uopšte ne izvršava za njega. Ovo eliminiše sav rad na fragmentima koji bi ionako bili odbačeni.

Ali pazi: ako pixel shader koristi discard/clip() ili ručno menja dubinu fragmenta (SV_Depth), Early-Z se ne može koristiti jer GPU ne može da zna unapred da li će fragment preživeti pixel shader. Ovo je razlog zašto je discard potencijalno skup -- ne samo da dodaje instrukciju, nego sprečava Early-Z optimizaciju. O ovome detaljno u poglavlju 15.

Depth prepass

Tehnika gde se prvo renderuje scena sa veoma jednostavnim (ili praznim) pixel shader-om, samo da bi se popunio depth buffer. Drugi pass onda renderuje scenu sa punim pixel shader-om, ali sada Early-Z može da odbaci sve fragmente koji su iza nečega, eliminišući overdraw. U poglavlju 10 o draw call-ovima ćemo analizirati kada se ovo isplati.

7.3 Stencil Test (Test šablona)

Stencil buffer je još jedan buffer iste rezolucije kao ekran, koji čuva celobrojnu vrednost (obično 8-bitnu) za svaki piksel. Stencil test poredi vrednost fragmenta sa vrednošću u stencil buffer-u i, na osnovu rezultata, odlučuje da li fragment prolazi ili ne.

Stencil buffer je neverovatno fleksibilan alat. Možeš ga koristiti za:

Stencil operacije se mogu konfigurisati za tri slučaja:

  1. Šta se dešava kada stencil test padne
  2. Šta se dešava kada stencil test prođe ali depth test padne
  3. Šta se dešava kada oba testa prođu

Za svaki slučaj možeš izabrati operaciju: Keep (ne menjaj), Zero (postavi na 0), Replace (postavi na reference vrednost), Increment, Decrement, Invert.

Ova fleksibilnost čini stencil buffer jednim od najmoćnijih alata u render pipeline-u, iako se često zanemaruje.

U Unreal Engine 5, stencil buffer se koristi interno za mnoge stvari -- custom depth/stencil za post-process efekte, decal rendering, i mnogo više.

7.4 Blending (Mešanje boja)

Poslednji korak pre upisivanja u framebuffer je blending -- mešanje boje novog fragmenta sa bojom koja je već u framebuffer-u. Za neprovidne (opaque) objekte, blending je jednostavno "prepiši" -- nova boja potpuno zamenjuje staru. Ali za transparentne objekte, blending je ono što stvara efekat providnosti.

Standardna alpha blending formula

FinalColor = SourceColor * SourceAlpha + DestColor * (1 - SourceAlpha)

Gde je:

Ako je alpha = 1.0, fragment je potpuno neprovidan i formula daje samo SourceColor. Ako je alpha = 0.5, dobijaš 50/50 mešavinu. Ako je alpha = 0.0, fragment je potpuno providan.

Ostali blending mode-ovi

Blending nije ograničen na alpha blending. Možeš konfigurisati razne blending operacije:

Blending mode Formula Upotreba
Additive Final = Src + Dst Svetleći efekti (oganj, magija, lens flare)
Multiplicative Final = Src * Dst Shadows, tinting
Alpha Blend Final = SrcA + Dst(1-A) Standardna transparentnost
Premultiplied Alpha Final = Src + Dst*(1-A) UI, particle efekti (efikasniji od standardnog)

Problem sortiranja transparentnih objekata

Blending ima jednu veliku zamku: zavisi od redosleda (order-dependent). Ako imaš dva transparentna objekta A i B, gde je A bliži kameri, rezultat je različit zavisno od toga da li prvo crtaš A pa B, ili B pa A.

Korektno renderovanje zahteva da se transparentni objekti sortiraju od najdaljeg ka najbližem (back-to-front). Ovo je suprotno od opaque objekata koji se sortiraju front-to-back (za efikasniji Early-Z).

Ovo je razlog zašto render pipeline u UE5 ima dva glavna prolaza:

  1. Opaque pass: Neprovidni objekti, sortirani front-to-back, sa depth write uključenim.
  2. Translucent pass: Transparentni objekti, sortirani back-to-front, sa depth write isključenim (ili selektivnim).

Problem sortiranja transparentnih objekata je jedan od najtežih problema u real-time grafici, i za njega postoje napredne tehnike kao Order-Independent Transparency (OIT), ali to je tema za naprednija poglavlja.

7.5 Framebuffer: Krajnje odredište

Na kraju celokupnog pipeline-a, boja se upisuje u framebuffer -- buffer koji predstavlja finalnu sliku koja će biti prikazana na ekranu. U praksi, koristi se double buffering ili triple buffering:

Kada se renderovanje novog frejma završi, front i back buffer se "zamenjuju" (swap) -- back buffer postaje front i prikazuje se, a stari front buffer postaje novi back buffer za sledeći frejm. Ovo sprečava tearing -- artifakt gde gornji deo ekrana prikazuje novi frejm a donji stari, jer se buffer menja dok monitor čita podatke za prikaz.


8. Programabilne vs Fixed-Function faze pipeline-a

8.1 Pregled

Sada kada smo prošli kroz ceo pipeline, hajde da eksplicitno sumiramo koje faze su programabilne, a koje fixed-function, i razjasnimo zašto.

Pipeline Stage              Tip               Programabilnost
─────────────────────────────────────────────────────────────
Input Assembler             Fixed-Function     Ne
Vertex Shader               Programmable       Da (obavezno)
Hull Shader                 Programmable       Da (opciono)
Tessellator                 Fixed-Function     Ne (konfigurabilan)
Domain Shader               Programmable       Da (ako je tessellation aktivan)
Geometry Shader             Programmable       Da (opciono)
Rasterizer                  Fixed-Function     Ne (konfigurabilan)
Pixel/Fragment Shader       Programmable       Da (obavezno*)
Output Merger               Fixed-Function     Ne (konfigurabilan)

(*) Pixel shader je tehički opcion za depth-only pass, ali u praksi je gotovo uvek prisutan.

8.2 Zašto su neke faze fixed-function?

Postoji jasna logika iza odluke šta je programabilno a šta nije:

Fixed-function faze imaju zajedničke karakteristike:

  1. Dobro definisano ponašanje: Svi žele istu stvar od rasterizera -- pretvaranje trouglova u fragmente. Nema potrebe za kreativnom slobodom.

  2. Izuzetno visok throughput: Ove faze moraju da procesiraju ogromne količine podataka. Input Assembler čita milione vertex-a po frejmu. Rasterizer procesira milione trouglova. Specijalizovani hardver je značajno brži od generalnog.

  3. Determinizam: Rezultat mora biti konzistentan i predvidljiv. Rasterizer mora da garantuje da dva susedna trougla koji dele ivicu neće ostaviti rupu između sebe (takozvani rasterization rules).

  4. Paralelizacija: Specijalizovani hardver može da iskoristi specifične oblike paralelizma koji bi bili teški u generalnom programabilnom okruženju.

Programabilne faze imaju zajedničke karakteristike:

  1. Kreativna sloboda je potrebna: Kako se transformiše vertex? Koja je boja piksela? Odgovori na ova pitanja zavise od toga šta developer želi da postigne.

  2. Stalno se razvijaju novi algoritmi: Novi modeli osvetljenja, novi vizuelni efekti, nova tehnika deformacije -- hardver ne može da predvidi šta će developeri hteti sutra.

  3. Relativno uniformni ulazi/izlazi: Vertex shader uvek prima vertex atribute i proizvodi transformisan vertex. Pixel shader uvek prima interpolirane atribute i proizvodi boju. Format je fiksan, ali logika je slobodna.

8.3 Konfigurabilan vs Programabilan

Neke fixed-function faze su konfigurabilan -- ne možeš da napišeš proizvoljan program, ali možeš da biraš između predefinisanih opcija:

Ovo pruža dovoljno fleksibilnosti za razne use-case-ove bez troška potpunog programabilnog stage-a.

8.4 Trend ka više programabilnosti: Mesh Shaderi

Zanimljivo je da se trend u modernom GPU hardveru kreće ka većoj programabilnosti. Najnoviji GPU-ovi podržavaju mesh shadere (uvedene sa NVIDIA Turing/AMD RDNA2 generacijom), koji zamenjuju ceo front-end pipeline-a:

Umesto: Input Assembler → Vertex Shader → Tessellation → Geometry Shader Mesh shaderi nude: Task Shader → Mesh Shader

Mesh shader je potpuno programabilan i može da:

Ovo je budućnost, i Nanite u UE5 koristi koncept sličan mesh shader-ima (na platformama koje ih podržavaju).


9. Compute Shaderi: GPU kao general-purpose procesor

9.1 Šta su Compute Shaderi?

Do sada smo govorili o grafičkom pipeline-u -- sekvenci koraka koji transformišu geometriju u piksele. Ali moderni GPU-ovi su sposobni za mnogo više od renderovanja grafike. Compute shaderi su programi koji se izvršavaju na GPU-u van grafičkog pipeline-a, za proizvoljne kalkulacije.

Compute shader nema predefinisane ulaze ili izlaze. Nema vertex-e, nema fragmente, nema trouglove. Umesto toga, compute shader:

To je, u suštini, način da koristiš GPU kao masivno paralelni procesor za bilo šta.

9.2 Kako rade Compute Shaderi?

Compute shaderi se izvršavaju u thread grupama (workgroups). Svaki thread u grupi izvršava isti shader program, ali sa različitim indeksom (thread ID). Thread-ovi u istoj grupi mogu da dele podatke kroz shared memory (groupshared u HLSL-u) i da se sinhronizuju.

Dispatch (pokretanje) compute shader-a specificira koliko thread grupa da se pokrene u 3D gridu:

Dispatch(groupsX, groupsY, groupsZ)

A sam shader definiše koliko thread-ova ima u svakoj grupi:

[numthreads(16, 16, 1)]
void CS(uint3 threadID : SV_DispatchThreadID,
        uint3 groupID : SV_GroupID,
        uint3 localID : SV_GroupThreadID)
{
    // threadID je globalni ID thread-a
    // groupID je ID grupe
    // localID je ID thread-a unutar grupe
}

9.3 Gde se koriste Compute Shaderi u UE5?

Compute shaderi su postali nezaobilazan deo modernog rendering-a. U UE5, koriste se za:

9.4 Compute vs Graphics Pipeline

Ključna razlika između compute shader-a i grafičkog pipeline-a:

Aspekt Graphics Pipeline Compute Shader
Struktura Fiksna sekvenca faza Slobodna forma
Ulazi Vertex-i, indeksi Proizvoljni bufferi
Izlazi Framebuffer (pikseli) Proizvoljni bufferi
Paralelizam Implicitno (per-vertex, per-pixel) Eksplicitno (thread grupe)
Shared memory Ne Da
Sinhronizacija Automatska (pipeline) Ručna (barriers)
Rasterizacija Da Ne
Fixed-function HW Da (rasterizer, etc.) Ne

Compute shaderi nisu zamena za grafički pipeline -- oni ga dopunjuju. Za renderovanje geometrije, grafički pipeline je i dalje najefikasniji. Ali za "sve ostalo" -- simulacije, post-processing, data processing -- compute shaderi su pravi alat.

9.5 Async Compute

Moderna GPU arhitektura podržava async compute -- izvršavanje compute shader-a paralelno sa grafičkim pipeline-om. GPU ima različite tipove hardvera (compute units, rasterizer, ROP-ovi), i dok grafički pipeline čeka na nešto (recimo, rasterizer je zagušen), compute units mogu da rade na compute shader-u.

Ovo je napredna tehnika optimizacije koja može da značajno poboljša GPU iskorišćenost, ali zahteva pažljivo profilisanje da bi se odredilo gde async compute pomaže, a gde smeta (jer compute i graphics pipeline dele neke resurse, posebno memory bandwidth i cache).

UE5 koristi async compute interno za razne zadatke, a ti možeš kontrolisati neke aspekte kroz project settings i RHI (Rendering Hardware Interface) konfiguraciju.


Zaključak

Prošli smo kroz ceo grafički pipeline -- od trenutka kada CPU pripremi scenu, kroz vertex shader koji transformiše geometriju, tessellation koji dodaje detalj, rasterizer koji pretvara trouglove u fragmente, pixel shader koji izračunava boju svakog piksela, do output merger-a koji odlučuje šta se zapravo vidi na ekranu.

Razumevanje ovog pipeline-a je fundamentalno za svaki dalji rad na optimizaciji. Kada tvoja igra ima loše performanse, moraš znati gde je bottleneck -- da li je CPU (application stage)? Da li je vertex processing? Da li je rasterizacija (previše sitnih trouglova)? Da li je pixel shader (prekompleksni materijali, previsoka rezolucija)? Da li je output merger (previše blending-a)?

Svaka od ovih faza ima svoje alate za profilisanje i svoje strategije za optimizaciju. U narednim poglavljima ćemo zaroniti u svaku od ovih oblasti:

Ali pre svega toga, u sledećem poglavlju (08) ćemo videti kako se ceo ovaj pipeline organizuje za efikasno renderovanje scena sa mnogo svetala -- Deferred vs Forward rendering. To je jedna od najvažnijih arhitektonskih odluka u svakom modernom render engine-u.


Tabela ključnih pojmova

Pojam Značenje
Render Pipeline Sekvenca koraka koja transformiše 3D scenu u 2D sliku na ekranu
Application Stage Faza na CPU-u: logika scene, culling, priprema draw call-ova
Geometry Stage Faza na GPU-u: vertex processing, tessellation, transformacije
Rasterization Stage Faza na GPU-u: pretvaranje trouglova u fragmente i finalna obrada piksela
Vertex Shader Programmable shader koji se izvršava za svaki vertex; radi transformacije
MVP Transform Model-View-Projection transformacija -- iz lokalnog prostora objekta u clip space
Skinning Deformacija mesh-a na osnovu skeleta (kostiju) za animaciju
Tessellation Deljenje geometrije na sitnije trouglove za više detalja
Hull Shader Programmable faza tessellation-a koja određuje tessellation faktore
Tessellator Fixed-function hardver koji generiše nove vertex-e prema faktorima
Domain Shader Programmable faza koja procesira svaki novi vertex tessellation-a
Displacement Mapping Pomeranje vertex-a na osnovu teksture za geometrijski detalj
Geometry Shader Programmable shader koji može dodati/ukloniti primitive (retko korišćen)
Stream Output Mogućnost upisivanja rezultata geometry shader-a nazad u buffer
Rasterizer Fixed-function hardver koji pretvara trouglove u fragmente
Fragment Kandidat za piksel -- sadrži interpolirane podatke za izračunavanje boje
Edge Equations Linearne jednačine koje definišu ivice trougla za rasterizaciju
Baricentrične koordinate Koordinate koje opisuju poziciju tačke unutar trougla (u, v, w)
Pixel/Fragment Shader Programmable shader koji izračunava boju svakog fragmenta
Texture Sampling Čitanje podataka iz teksture na datim UV koordinatama
Mipmaps Prethodno izračunate umanjene verzije teksture za efikasno sampling
Normal Mapping Tehnika koja koristi teksturu za modifikaciju normala za detaljno osvetljenje
PBR Physically Based Rendering -- model osvetljenja baziran na fizici
MRT Multiple Render Targets -- pisanje u više tekstura istovremeno
Output Merger Finalna faza: depth test, stencil test, blending
Depth Buffer (Z-Buffer) Buffer koji čuva dubinu svakog piksela za test vidljivosti
Stencil Buffer Buffer celobrojnih vrednosti za maskiranje i specijalne efekte
Blending Mešanje boje novog fragmenta sa postojećom bojom u framebuffer-u
Early-Z Optimizacija gde se depth test radi pre pixel shader-a
Overdraw Višestruko izvršavanje pixel shader-a za isti piksel
Framebuffer Buffer koji sadrži finalnu sliku za prikaz na ekranu
Reversed-Z Tehnika gde se near plane mapira na Z=1, far na Z=0 za bolju preciznost
Compute Shader GPU program van grafičkog pipeline-a za proizvoljne kalkulacije
Async Compute Paralelno izvršavanje compute i graphics pipeline-a
Mesh Shader Novi programmable stage koji zamenjuje vertex/tessellation/geometry shadere

📖 Dalje čitanje: