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:
- Logiku scene: Gde se nalaze objekti? Da li su se pomerili od prošlog frejma? Koja animacija se igra? Koji particle efekti su aktivni?
- Culling (odbacivanje nevidljivih objekata): Zašto bi GPU crtao nešto što kamera ne vidi? Application stage određuje koji objekti su vidljivi (frustum culling, occlusion culling -- o čemu ćemo detaljno u poglavlju 12).
- Pripremu draw call-ova: Za svaki objekat koji treba da se iscrta, CPU priprema takozvani draw call -- instrukciju za GPU koja kaže "uzmi ovu geometriju, primeni ovaj materijal, i nacrtaj je". O draw call-ovima detaljno govorimo u poglavlju 10.
- Sortiranje i batching: Redosled iscrtavanja je bitan (transparentni objekti moraju ići posle neprovidnih, na primer). CPU takođe pokušava da grupiše slične objekte da bi smanjio overhead.
- Upload podataka ka GPU-u: Transformacione matrice, parametri materijala, pozicije svetala -- sve to CPU pakuje u buffere i šalje GPU-u.
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:
- Vertex Shader: Transformacija svakog vertex-a iz lokalnog prostora objekta u prostor ekrana (o čemu smo detaljno govorili u poglavlju 06).
- Tessellation (opciono): Deljenje geometrije na sitnije trouglove za veći nivo detalja.
- Geometry Shader (opciono, i retko korišćen): Mogućnost dodavanja ili uklanjanja celih primitiva.
- Clipping: Odbacivanje delova geometrije koji su van vidnog polja kamere (view frustum). Trougao koji je samo delimično vidljiv se seče tako da ostane samo vidljivi deo.
- Screen Mapping: Preslikavanje koordinata iz normalized device coordinates (NDC) u stvarne pixel koordinate ekrana.
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:
- Triangle Setup i Traversal: Priprema trougla za rasterizaciju i određivanje koji pikseli pripadaju kom trouglu.
- Fragment/Pixel Shader: Izračunavanje boje svakog piksela -- teksture, osvetljenje, senke, refleksije, sve to se dešava ovde.
- Output Merger: Finalni koraci -- depth test (da li je ovaj piksel ispred ili iza nečega?), stencil test, blending (mešanje boja za transparentne objekte), i konačno upisivanje u framebuffer.
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:
- Model matrica (World matrix): Transformiše iz lokalnog prostora u world space.
- View matrica: Transformiše iz world space u camera space.
- Projection matrica: Transformiše iz camera space u clip space (perspektivna projekcija).
- Parametri materijala: Razni parametri koje shader koristi.
- Vreme:
_Timeili sličan uniform za animacije.
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:
- Wind effect na vegetaciji: Pomeranje vertex-a na osnovu sinusne funkcije i vremena. Drveće i trava se ljuljaju na vetru bez ijedne animacije -- samo vertex shader koji pomera vertex-e.
- Ocean waves: Gerstner talasi ili FFT-based talasi se implementiraju pomeranjem vertex-a na ravnom mesh-u.
- Breathing/pulsating efekti: Skaliranje vertex-a na osnovu sinusoide za efekat disanja kod organskih objekata.
- Terrain morphing: Morphing terena na osnovu distance od kamere za LOD prelaze.
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:
- World position: Pozicija u svetskom prostoru (za izračunavanje osvetljenja)
- World normal: Transformisana normala (za osvetljenje)
- UV koordinate: Prosleđene (ponekad modifikovane) teksturne koordinate
- Tangent i Bitangent: Za normal mapping
- View direction: Vektor od vertex-a ka kameri (za specular osvetljenje, Fresnel efekat)
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:
- Skeletal animation na GPU-u
- Proceduralne deformacije
- Instancing (renderovanje hiljada objekata sa jednim draw call-om, gde vertex shader pozicionira svaku instancu)
- Morphing između oblika (morph targets / blend shapes)
- Projekciju senki (shadow map rendering)
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:
- Kompleksnost shader-a je važna: Svaka dodatna instrukcija se množi sa brojem vertex-a.
- Skinning je skup: Likovi sa velikim brojem kostiju i vertex-a mogu značajno opteretiti vertex shader fazu.
- Tessellation povećava posao: Ako koristiš tessellation, vertex shader (ili tačnije, domain shader) se izvršava i za sve novo-generisane vertex-e.
- LOD je tvoj prijatelj: Smanjivanje broja vertex-a za udaljene objekte direktno smanjuje opterećenje vertex shader-a.
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:
- Modifikovane kontrolne tačke: Hull shader može da pomeri ili modifikuje kontrolne tačke pre tessellation-a.
- 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:
- Distance od kamere: Bliži objekti dobijaju više tessellation-a, dalji manje. Ovo je najčešći pristup.
- Veličine trougla na ekranu: Ako je trougao mali na ekranu (zauzima malo piksela), nema smisla ga deliti na sitnije.
- Zakrivljenosti površine: Ravni delovi ne trebaju tessellation, zakrivljeni trebaju.
// 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:
- Prima patch sa tessellation faktorima.
- Generiše mrežu novih vertex-a unutar patch-a.
- Povezuje te vertex-e u trouglove.
- 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:
- Baricentrične koordinate novog vertex-a (gde se nalazi u okviru patch-a)
- Kontrolne tačke iz hull shader-a
- Originalne vertex podatke
Domain shader je mesto gde se dešava "prava magija" tessellation-a. Ovde tipično radiš:
- Interpolaciju atributa: Koristeći baricentrične koordinate, interpoliraš poziciju, normale, UV koordinate između kontrolnih tačaka.
- Displacement: Koristiš displacement mapu (teksturu) da pomeriš interpoliranu poziciju duž normale. Ovo je razlog zašto tessellation postoji -- dodaje geometrijski detalj iz teksture.
- 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:
- Terrain rendering: Teren blizu kamere ima visok tessellation za detalj, daleki teren ima nizak. Ovo je klasičan i jedan od najboljih primera korišćenja tessellation-a.
- Displacement mapping: Dodavanje geometrijskog detalja zidovima, stenama, terenu na osnovu teksture. Umesto da modeliraš svaku pukotinu u zidu, imaš displacement mapu koja "gura" geometriju.
- Dinamički LOD: Umesto da praviš 5 verzija istog mesh-a za različite LOD nivoe, imaš jedan mesh i tessellation koji kontroliše nivo detalja.
- Organic surfaces: Glatke, zakrivljene površine (PN triangles) koje izgledaju bolje sa više geometrije.
- Water surfaces: Talasi koji deformišu geometriju vode.
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:
- Domain shader: Koji se izvršava za svaki novi vertex.
- Rasterizer: Koji mora da procesira mnogo više trouglova.
- Pixel shader: Ako su trouglovi veoma mali (sub-pixel), dobijamo quad overdraw problem -- o čemu ćemo detaljno u poglavlju 15 o shader optimizaciji.
- 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:
- Dinamičke površine (voda, tkanina)
- Materijale sa displacement-om na non-Nanite mesh-ovima
- Terene (mada UE5 ima i Nanite podršku za teren u novijim verzijama)
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:
- Primi jedan trougao i emituje više trouglova (amplifikacija geometrije)
- Primi trougao i ne emituje ništa (uklanjanje geometrije)
- Promeni tip primitiva (primi trougao, emituje linije ili tačke)
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:
- Particle simulation: Ažuriranje pozicija particle-a na GPU-u.
- Proceduralno generisanje geometrije: Generisanje geometrije u jednom pass-u i renderovanje u drugom.
- Transform feedback: Snimanje transformisanih vertex-a za ponovnu upotrebu.
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:
- Instancing umesto geometrijske amplifikacije
- Compute shaderi umesto stream output-a
- Mesh shaderi (noviji hardver) umesto geometry shader-a za generisanje geometrije
- Tessellation za dodavanje detalja geometriji
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.
- Piksel (pixel = picture element) je jedno mesto na ekranu sa jednom bojom.
- Fragment je kandidat za piksel. To je rezultat rasterizacije koji sadrži sve podatke potrebne da se izračuna boja za taj piksel -- interpolirane UV koordinate, normalu, dubinu, itd.
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:
- Brute force: Proveri svaki piksel na ekranu. Ovo je očigledno neefikasno.
- Bounding box: Izračunaj pravougaonik koji ograničava trougao (bounding box) i proveri samo piksele unutar njega. Ovo je mnogo bolje.
- 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.
- 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?
-
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).
-
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.
-
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.
-
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:
-
Face culling: Na osnovu redosleda vertex-a (clockwise ili counter-clockwise), rasterizer može da odbaci trouglove koji gledaju od kamere (back-face culling). Ovo eliminiše otprilike pola trouglova zatvorenih mesh-ova i potpuno je besplatno jer ga radi hardver.
-
Scissor test: Odbacivanje fragmenata koji su van definisanog pravougaonog regiona na ekranu.
-
Multisampling (MSAA): Umesto testiranja samo centra piksela, rasterizer može da testira više tačaka (sample-ova) unutar svakog piksela za antialiasing. O ovome detaljno u poglavlju 16 o post-processingu.
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:
- Screen position: Pozicija fragmenta na ekranu (SV_Position)
- Interpolirane UV koordinate: Za texture sampling
- Interpolirana world position: Za izračunavanje osvetljenja
- Interpolirana normala: Za osvetljenje i shading
- Interpolirane tangent/bitangent: Za normal mapping
- Bilo koji custom podatak: Koji je vertex shader prosledio
Pored interpoliranih atributa, pixel shader ima pristup i:
- Teksturama: Slike koje su bind-ovane na shader
- Constant/Uniform bufferima: Parametri materijala, pozicije svetala, camera data
- Samplerimaima: Koji definišu kako se teksture čitaju (filtriranje, wrapping)
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:
- Point/Nearest filtering: Uzmi najbliži texel. Brzo, ali pikselizovano -- svaki texel se vidi kao oštri blok. Korisno za pixel art stilove.
- Bilinear filtering: Interpoliraj između 4 najbliža texel-a. Glatko, ali može biti blurry.
- Trilinear filtering: Bilinear na dva mipmap nivoa, pa interpolacija između njih. Rešava problem oštrih prelaza između mipmap nivoa.
- Anisotropic filtering: Uzima u obzir ugao pod kojim se površina gleda u odnosu na kameru. Daje mnogo bolji kvalitet na površinama pod uglom (podovi, putevi). Skuplje od trilinear-a, ali razlika u kvalitetu je ogromna. Detaljnije o ovome u poglavlju 11 o teksturama.
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:
- Poboljšava kvalitet: Bez mipmaps, daleke teksture imaju moiré pattern i aliasing.
- 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:
-
Diffuse reflection (difuzna refleksija): Svetlo koje ulazi u površinu, rasipa se unutar materijala, i izlazi u svim pravcima. Ovo daje materijalu njegovu "boju". Mat materijali (drvo, beton, tkanina) imaju dominantno difuznu refleksiju.
-
Specular reflection (spekularna refleksija): Svetlo koje se odbija od površine kao od ogledala. Glatke površine (metal, plastika, voda) imaju jaku specularnu refleksiju.
-
Fresnel effect: Na ivicama objekata (gde je ugao gledanja oštar u odnosu na površinu), refleksija je jača. Ovo je fizički fenomen koji možeš videti u stvarnom životu -- pogledaj prozor pod pravim uglom i vidiš kroz njega; pogledaj pod oštrim uglom i vidiš refleksiju.
-
Microsurface roughness: Koliko je površina "hrapava" na mikroskopskom nivou. Glatka površina ima oštru, fokusiranu specularnu refleksiju (kao ogledalo). Hrapava površina ima rasejanu, blurry refleksiju (kao brušeni metal).
U Unreal Engine 5, PBR model koristi ove ključne parametre (poznate svakom UE5 korisniku):
- Base Color: Osnovna boja materijala
- Metallic: Da li je materijal metal ili nemetalic (0 ili 1, ređe između)
- Roughness: Hrapavost površine (0 = savršeno glatko, 1 = potpuno hrapavo)
- Normal: Modifikovana normala za simulaciju sitnih detalja površine
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 testing: Ako je alpha vrednost ispod praga, odbaci fragment. Koristi se za vegetaciju, ograde, prozirne objekte.
- Dithered transparency: Odbacivanje fragmenata u uzorku za simulaciju polu-transparentnosti bez pravog blending-a.
- Custom clipping planes: Odbacivanje fragmenata na osnovu custom uslova.
// 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:
-
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!
-
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...
-
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.
-
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.
-
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:
- Ako je novi fragment bliži kameri (manja dubina), on prolazi test -- njegova boja i dubina se upisuju.
- Ako je novi fragment dalje od kamere (veća dubina), on je iza nečega što je već nacrtano -- odbacuje se.
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:
- Masking: Označi regione ekrana gde se nešto sme ili ne sme crtati. Na primer, portal rendering -- nacrtaj zid sa rupom, označi rupu u stencil buffer-u, pa onda crtaj scenu iza portala samo tamo gde je stencil oznaka.
- Mirror rendering: Označi površinu ogledala u stencil buffer-u, pa onda crtaj reflektovanu scenu samo unutar te oblasti.
- Shadow volumes: Klasičan stencil shadow tehnika koja koristi stencil buffer za određivanje koji pikseli su u senci.
- Outline efekti: Nacrtaj objekat u stencil buffer, pa nacrtaj malo veći objekat samo tamo gde stencil nije setovan -- dobijaš outline.
- Decal rendering: Kontrola gde se dekali smeju primeniti.
Stencil operacije se mogu konfigurisati za tri slučaja:
- Šta se dešava kada stencil test padne
- Šta se dešava kada stencil test prođe ali depth test padne
- Š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:
SourceColor-- boja novog fragmenta (iz pixel shader-a)SourceAlpha-- alpha (providnost) novog fragmentaDestColor-- boja koja je već u framebuffer-u
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:
- Opaque pass: Neprovidni objekti, sortirani front-to-back, sa depth write uključenim.
- 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:
- Front buffer: Buffer koji se trenutno prikazuje na ekranu.
- Back buffer: Buffer u koji se renderuje nova slika.
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:
-
Dobro definisano ponašanje: Svi žele istu stvar od rasterizera -- pretvaranje trouglova u fragmente. Nema potrebe za kreativnom slobodom.
-
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.
-
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).
-
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:
-
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.
-
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.
-
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:
- Rasterizer: Možeš konfigurisati face culling (front, back, none), fill mode (solid, wireframe), scissor test, depth bias...
- Output Merger: Možeš konfigurisati depth comparison funkciju, stencil operacije, blending mode...
- Tessellator: Možeš konfigurisati partitioning mode (integer, fractional_even, fractional_odd), output topology (triangle, quad)...
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:
- Sam čita vertex i index podatke (nema Input Assembler)
- Generiše proizvoljnu geometriju (nema potrebe za Geometry Shader)
- Radi amplifikaciju geometrije (zamena za tessellation u nekim slučajevima)
- Radi culling na nivou meshlet-a (grupe od ~64 trougla)
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:
- Čita podatke iz buffera i tekstura
- Izvršava proizvoljne kalkulacije
- Piše rezultate u buffere i teksture
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:
- Post-processing: Bloom, tone mapping, depth of field, motion blur -- sve su to compute shader pasevi koji čitaju render target i pišu modifikovanu sliku.
- Particle simulation: Ažuriranje pozicija, brzina, i stanja miliona particle-a paralelno. Niagara sistem u UE5 intenzivno koristi GPU compute za simulaciju.
- GPU culling: Određivanje vidljivosti objekata na GPU-u umesto na CPU-u, što smanjuje CPU overhead.
- Light culling: Za tiled/clustered deferred rendering -- određivanje koja svetla utiču na koji tile ekrana. O ovome detaljno u poglavlju 08.
- SSAO (Screen-Space Ambient Occlusion): Izračunavanje ambient occlusion-a iz depth buffer-a.
- SSR (Screen-Space Reflections): Ray-marching u screen space za refleksije.
- Cloth/Hair simulation: Fizička simulacija tkanine i kose na GPU-u.
- Lumen: UE5-ov globalni iluminacioni sistem koristi compute shadere za ray tracing u software mode-u, radiance caching, i mnogo više. O Lumen-u detaljno u poglavlju 13.
- Virtual Shadow Maps: Compute shaderi za upravljanje virtuelnim shadow mapama.
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:
- Poglavlje 08: Deferred vs Forward rendering -- kako se pipeline organizuje za efikasno osvetljenje
- Poglavlje 10: Draw calls -- kako CPU komunicira sa GPU-om i zašto je to često bottleneck
- Poglavlje 11: Teksture i memory -- kako texture sampling utiče na performanse
- Poglavlje 14: Nanite -- kako UE5 reimaginira geometrijski pipeline
- Poglavlje 15: Shader optimizacija -- kako pisati efikasne shadere
- Poglavlje 16: Post-processing i upscaling -- compute shaderi u akciji
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:
- Real-Time Rendering, 4th Edition (Akenine-Moller, Haines, Hoffman) -- Biblija real-time grafike. Poglavlja 2-5 detaljno pokrivaju pipeline.
- UE5 dokumentacija -- Rendering Overview -- Zvanični pregled UE5 rendering sistema.
- UE5 dokumentacija -- Materials -- Kako UE5 materijali mapiraju na shader pipeline.
- A trip through the Graphics Pipeline (Fabian Giesen) -- Detaljan blog serijal o tome kako hardver zapravo implementira pipeline.
- GPU Gems 3, Chapter 29: Real-Time GPU Rendering of Water -- Praktičan primer tessellation-a i vertex deformacije.
- Microsoft DirectX 12 Graphics Pipeline -- Zvanična dokumentacija D3D12 pipeline-a.
- Advances in Real-Time Rendering (SIGGRAPH courses) -- Godišnji pregledi naprednih rendering tehnika iz industrije.