Poglavlje 21: Shader Optimizacija

Poglavlje 21: Shader Optimizacija

"Najbrzi shader je onaj koji nikada ne mora da se izvrsi. Drugi najbrzi je onaj koji radi sto manje posla da bi postigao isti vizuelni rezultat."


Stigli smo do jednog od najvaznijih poglavlja u citavoj knjizi. Mozete imati najlepse materijale na svetu, ali ako vam svaki frame traje 30 milisekundi samo zato sto su shaderi preskupi -- nista od toga. Igra mora da radi glatko, a shaderi su cesto najveci krivac za probleme sa performansama na GPU strani.

U prethodnim poglavljima naucili ste kako GPU zapravo radi (Poglavlje 08), kako shaderi funkcionisu i sta je branching (Poglavlje 17), i uskoro cete uciti o UE5 material sistemu u praksi (Poglavlje 22). Ovo poglavlje je most izmedu teorije i prakse -- ovde ucimo kako da merimo, analiziramo i optimizujemo shadere da bi nasi projekti radili brzo na ciljnom hardveru.

Hajde da krenemo.


21.1 Instruction Count -- Merenje Kompleksnosti Shadera

Sta je Instruction Count?

Svaki shader, kada se kompajlira, pretvara se u niz instrukcija koje GPU izvrsava. Instruction count je jednostavno broj tih instrukcija. Sto vise instrukcija shader ima, to vise posla GPU mora da obavi za svaki pixel (ili vertex) koji taj shader obradjuje.

Ali nije svaka instrukcija jednako "skupa". Postoje dve glavne kategorije:

ALU (Arithmetic Logic Unit) instrukcije -- to su matematicke operacije: sabiranje, mnozenje, deljenje, trigonometrijske funkcije, normalizacija vektora, itd. Ove instrukcije se izvrsavaju na compute jedinicama GPU-a.

TEX (Texture) instrukcije -- to su operacije citanja iz tekstura. Svaki Texture Sample node u vasem materijalu rezultuje jednom ili vise TEX instrukcija. Ove instrukcije zahtevaju pristup memoriji, sto moze biti sporije od cistog racunanja.

Kako Videti Instruction Count u UE5

Unreal Engine vam daje direktan uvid u instruction count vaseg materijala. Evo kako:

  1. Otvorite Material Editor
  2. U donjem levom uglu prozora videcete statistiku materijala
  3. Tu se prikazuje broj instrukcija, razvrstan po tipu

Videcete nesto poput:

Base pass shader: 127 instructions
   62 ALU, 14 TEX, 51 other
Vertex shader: 23 instructions

Ovo vam odmah govori koliko je vas shader "tezak". Ali -- i ovo je bitno -- instruction count sam po sebi nije dovoljan za procenu performansi. Shader sa 200 instrukcija moze da radi brze od shadera sa 100 instrukcija, zavisno od toga koje su to instrukcije i kako GPU moze da ih paralelizuje.

Sta Se Smatra "Skupim"?

Ovo zavisi od konteksta -- koji GPU ciljate, kolika je rezolucija ekrana, koliko piksela taj materijal zauzima na ekranu. Ali evo nekih okvirnih smernica:

Kategorija Instruction Count (priblizno) Komentar
Jednostavan < 50 instrukcija UI elementi, jednostavni unlit materijali
Umeren 50-150 instrukcija Tipicni PBR materijali sa par tekstura
Skup 150-300 instrukcija Kompleksni materijali sa blendingom, parallax
Veoma skup 300-500 instrukcija Landscape materijali sa mnogo layera
Opasno skup > 500 instrukcija Specijalni efekti, proceduralni materijali

Zapamtite: ovi brojevi su okvirni. Shader sa 200 instrukcija koji se primenjuje na mali objekat u daljini je sasvim prihvatljiv. Isti shader na fullscreen quad-u koji pokriva ceo ekran moze da ubije performanse.

Instruction Count i Overdraw

Jedan koncept koji je kriticno vazan a cesto se previdi: overdraw. Ako imate dva poluprozirna objekta jedan iza drugog, GPU mora da izvrsi shader za oba, za svaki piksel gde se preklapaju. Ako imate pet slojeva translucent cestica -- to je pet puta vise shader izvrsavanja za te piksele.

Zato je instruction count posebno bitan za:

Za opaque materijale na malim objektima, mozete sebi priustiti vise instrukcija. Za translucent materijale koji pokrivaju pola ekrana -- svaka instrukcija se racuna.

Kako UE5 Kompajlira Shadere

Kada napisete material u Material Editoru, UE5 ga kompajlira u HLSL (High Level Shading Language) kod, a zatim koristi platform-specificni kompajler da ga pretvori u strojni kod za ciljni GPU. Na Windows/DirectX 12, koristi se DXC (DirectX Shader Compiler). Za Vulkan, koristi se SPIR-V.

Mozete da vidite generisani HLSL kod tako sto otvorite Material Editor, kliknete na Window > HLSL Code (ili pritisnete odgovarajuci shortcut). Ovo je korisno kada zelite da razumete sta vas material zaista radi "ispod haube".

// Primer generisanog HLSL koda za jednostavan PBR material
float3 BaseColor = Texture2DSample(Material_Texture2D_0, 
                                    Material_Texture2D_0Sampler, 
                                    TexCoords).rgb;
float Roughness = Texture2DSample(Material_Texture2D_1,
                                   Material_Texture2D_1Sampler,
                                   TexCoords).r;
float Metallic = Texture2DSample(Material_Texture2D_1,
                                  Material_Texture2D_1Sampler,
                                  TexCoords).g;

Ovaj kod se zatim kompajlira u nesto poput:

sample_texture r0, t0, s0, v1.xy    // TEX: sample base color
sample_texture r1, t1, s1, v1.xy    // TEX: sample roughness/metallic
mov o0.rgb, r0.rgb                   // ALU: output base color
mov o1.r, r1.r                       // ALU: output roughness
mov o1.g, r1.g                       // ALU: output metallic

Primetite da imamo 2 TEX instrukcije i 3 ALU instrukcije. Ovo je krajnje pojednostavljen primer -- pravi shaderi su mnogo kompleksniji jer ukljucuju osvetljenje, senke, ambient occlusion, i mnogo toga drugog.


21.2 Texture Samples -- Cena Svakog Citanja

Osnove Texture Samplinga

Svaki put kada vas shader procita vrednost iz teksture, to je texture sample. Ova operacija izgleda jednostavno -- "daj mi boju na koordinati (u, v)" -- ali ispod povrsine se desava mnogo toga:

  1. Adresiranje: GPU racuna koja texel adresa odgovara datim UV koordinatama
  2. Filtering: Ako koristite bilinearni ili trilinearni filtering, GPU mora da procita 4 (bilinearni) ili 8 (trilinearni) texela i interpolira izmedu njih
  3. Mipmap selekcija: GPU odredjuje koji mipmap nivo da koristi na osnovu velicine piksela na ekranu
  4. Memory fetch: Podaci se citaju iz VRAM-a, prolaze kroz cache hijerarhiju

Svaki od ovih koraka ima svoju cenu. Ali najskuplji deo je obicno memory fetch -- pristup VRAM memoriji.

Koliko Texture Samplea je "Previse"?

Nema magicnog broja, ali evo prakticnih smernica:

Platforma Preporuceni max texture samples po shaderu
High-end PC (RTX 3080+) 16-24
Mid-range PC (GTX 1660) 8-16
Low-end PC / Stari hardware 4-8
Konzole (PS5/Xbox Series X) 12-20
Konzole (Switch) 4-8
Mobile (high-end) 4-8
Mobile (low-end) 2-4

Ovi brojevi su za base pass -- glavno renderovanje. Ako imate i shadow pass, forward pass, itd., ukupan broj samplea po pikselu moze biti mnogo veci.

U tipicnom UE5 PBR materijalu, imate otprilike:

To su 3-4 samplea -- sasvim razumno. Problem nastaje kada:

Dependent Texture Reads -- Tihi Ubica Performansi

Ovo je jedan od najvaznijih koncepata u shader optimizaciji, a mnogi ga ne razumeju.

Normalan (nezavisan) texture read:

UV koordinate su poznate unapred → GPU moze da prefetch-uje podatke

Dependent texture read:

UV koordinate zavise od rezultata prethodnog citanja → GPU mora da saceka

Hajde da ovo objasnimo detaljnije.

Moderni GPU-ovi imaju mogucnost prefetch-ovanja -- dok se izvrsavaju ALU instrukcije, GPU u pozadini vec pocinje da cita sledece teksture iz memorije. Ovo radi odlicno kada GPU unapred zna odakle ce da cita (tj. UV koordinate su poznate).

Ali kod dependent texture read-a, UV koordinate za drugo citanje zavise od rezultata prvog citanja. GPU mora da:

  1. Izvrsi prvo citanje
  2. Saceka da podaci stignu iz memorije
  3. Izracuna nove UV koordinate na osnovu tih podataka
  4. Tek onda posalje zahtev za drugo citanje
  5. Opet saceka da ti podaci stignu

Ovo "cekanje" unistava paralelizam koji je kljucan za GPU performanse.

Primer 1: Parallax Mapping (DEPENDENT!)

// Korak 1: Citamo heightmap da odredimo offset
float height = Texture2DSample(HeightMap, Sampler, UV).r;

// Korak 2: Koristimo height da pomerimo UV koordinate
float2 newUV = UV + height * viewDir.xy;

// Korak 3: Citamo base color sa NOVIM UV koordinatama
// OVO JE DEPENDENT READ! GPU mora da saceka rezultat koraka 1.
float3 color = Texture2DSample(BaseColor, Sampler, newUV).rgb;

Primer 2: Flow Map za Vodu (DEPENDENT!)

// Citamo flow map da dobijemo smer toka vode
float2 flowDir = Texture2DSample(FlowMap, Sampler, UV).rg;

// Koristimo flow direction da distortujemo UV za normal mapu
// OVO JE DEPENDENT READ!
float3 normal = Texture2DSample(NormalMap, Sampler, UV + flowDir * Time).rgb;

Primer 3: Lookup Table (DEPENDENT!)

// Racunamo neku vrednost
float value = dot(Normal, LightDir);

// Koristimo tu vrednost kao UV za LUT teksturu
// OVO JE DEPENDENT READ!
float3 ramp = Texture2DSample(RampTexture, Sampler, float2(value, 0)).rgb;

Svi ovi primeri su legitimni shader efekti, ali morate biti svesni njihove cene. Jedan dependent read nece napraviti veliku razliku. Pet ili sest u nizu -- to vec moze biti ozbiljan problem.

Kako Minimizovati Texture Samples

1. Channel Packing

Umesto da imate odvojene teksture za Roughness, Metallic, i Ambient Occlusion, spakovajte ih u kanale jedne teksture:

Tekstura "ORM":
  R kanal = Occlusion (AO)
  G kanal = Roughness
  B kanal = Metallic

Rezultat: 3 texture samplea pretvorena u 1!

Evo tipicnog channel packing-a za PBR materijale:

Tekstura R G B A
BaseColor Red Green Blue (opciono: Opacity)
Normal Normal.X Normal.Y (izracunato) --
ORM Occlusion Roughness Metallic (opciono: Height)

Sa ovim pristupom, kompletan PBR material sa AO, Roughness, Metallic i height mapom zahteva samo 3 texture samplea umesto 5-6.

2. Computed Patterns Umesto Tekstura

Mnoge jednostavne paterne mozete izracunati matematicki umesto da koristite teksturu:

// UMESTO texture samplea za grid patern:
// float grid = Texture2DSample(GridTexture, Sampler, UV).r;

// KORISTITE racunanje:
float2 gridUV = frac(UV * 10.0); // 10x10 grid
float grid = step(0.05, gridUV.x) * step(0.05, gridUV.y);
// Rezultat: identican grid patern, nula texture samplea!
// UMESTO noise teksture za jednostavan noise:
// float noise = Texture2DSample(NoiseTexture, Sampler, UV).r;

// KORISTITE proceduralni noise:
float noise = frac(sin(dot(UV, float2(12.9898, 78.233))) * 43758.5453);
// OPREZ: ovo je veoma jednostavan hash, ne pravi Perlin noise
// Za kvalitetniji noise, tekstura je cesto bolja opcija

3. Koristite Manje Tekstura Generalno

Postavite sebi pitanje za svaku teksturu:

Svaka tekstura koju eliminisete je jedan texture sample manje po pikselu, za svaki frame, za svaki objekat koji koristi taj material. To se brzo sabira.

4. Texture Atlasi

Umesto mnogo malih tekstura, koristite jednu veliku teksturu (atlas) sa razlicitim delovima na razlicitim UV koordinatama. Ovo ne smanjuje broj texture samplea direktno, ali moze poboljsati cache koherenciju jer GPU cita iz jedne teksture umesto iz vise.

Medjutim, budite pazljivi sa texture atlasima -- mipmapping moze da napravi probleme na ivicama izmedju delova atlasa (bleeding artefakti). UE5 ima podrsku za Virtual Texturing koji resava neke od ovih problema.

5. Virtual Texturing (VT)

UE5 podrzava Runtime Virtual Texturing (RVT) koji moze znacajno pomoci sa landscape materijalima. Umesto da svaki landscape layer ima svoje teksture koje se blend-uju u pixel shaderu, VT moze da "ispece" (bake) rezultat blendinga u virtualnu teksturu, efektivno pretvarajuci kompleksan multi-layer material u jednostavan single-texture lookup.

Ovo je posebno korisno za landscape materijale gde biste inace imali 12-20+ texture samplea.


21.3 ALU Bound vs TEX Bound -- Identifikacija Bottleneck-a

Dva Razlicita Bottleneck-a

Kada optimizujete shadere, kriticno je da razumete gde je vas bottleneck. GPU ima dva glavna resursa koje shaderi koriste:

  1. Compute jedinice (ALU) -- izvrsavaju matematicke operacije
  2. Memorijski bandwidth (TEX) -- cita podatke iz tekstura

Ova dva resursa rade paralelno. Dok ALU jedinice racunaju, memorijski kontroler moze da fetch-uje teksture. Idealan shader drzi oba sistema zauzeta -- nema cekanja.

Ali u praksi, jedan od ova dva sistema je obicno bottleneck:

ALU Bound (Ogranicen Racunanjem)

Vas shader radi previse matematike. GPU compute jedinice su 100% zauzete, a memorijski sistem ceka -- mogao bi da fetch-uje vise tekstura, ali nema sta da radi jer shader ne trazi teksture.

Simptomi ALU bound shadera:

Primeri ALU bound situacija:

Proceduralni noise sa vise oktava:
  50+ ALU instrukcija po oktavi
  0 texture samplea
  → Cist ALU bottleneck

Kompleksna Fresnel kalkulacija:
  30+ ALU instrukcija
  0 texture samplea
  → ALU bound

Toon shading sa vise rampi racunatih matematicki:
  40+ ALU instrukcija
  2-3 texture samplea
  → Verovatno ALU bound

TEX Bound (Ogranicen Memorijom)

Vas shader cita previse tekstura. Memorijski bandwidth je zasicen, a compute jedinice cekaju -- mogle bi da racunaju vise, ali nemaju podatke na kojima bi radile.

Simptomi TEX bound shadera:

Primeri TEX bound situacija:

Landscape sa 4 layera, svaki sa BaseColor + Normal + ORM:
  4 x 3 = 12 texture samplea minimum
  + macro texture + weightmap = 14-16 samplea
  → Gotovo sigurno TEX bound

Character sa diffuse + normal + ORM + detail normal + subsurface:
  5-7 texture samplea
  + shadow map citanja
  → Moze biti TEX bound na slabijim GPU-ovima

Kako Identifikovati Bottleneck

Ovo je kljucna tehnika koju svaki graficki programer mora da zna:

Metod 1: Smanjite jedno, posmatrajte rezultat

Korak 1: Zabelezite baseline framerate
Korak 2: Smanjite broj ALU operacija (pojednostavite matematiku)
         → Ako framerate poraste ZNACAJNO: bili ste ALU bound
         → Ako framerate ostane ISTI: niste ALU bound
Korak 3: Vratite ALU na original
Korak 4: Smanjite broj texture samplea (uklonite neke teksture)
         → Ako framerate poraste ZNACAJNO: bili ste TEX bound
         → Ako framerate ostane ISTI: niste TEX bound

Metod 2: Smanjite rezoluciju tekstura

Promenite sve teksture sa 2048x2048 na 512x512. Ako performanse znacajno porastu, verovatno ste TEX bound (jer su manji podaci brzi za citanje i bolje se cache-iraju).

Metod 3: UE5 Console Komande

UE5 ima korisne console komande za testiranje:

r.ScreenPercentage 50    // Renderuje na pola rezolucije
                          // Ako ovo MNOGO pomaze, shader je skup po pikselu
                          
r.MipMapLODBias 4        // Forsira najnize mipmap nivoe (male teksture)
                          // Ako ovo pomaze, TEX bound

stat gpu                  // Prikazuje GPU timing po pass-u
stat unit                 // Prikazuje ukupne timinge (Game, Draw, GPU)

Metod 4: GPU Profiler (RenderDoc, Nsight, PIX)

Za detaljnu analizu, koristite spoljne alate:

Ovi alati mogu da vam kazu tacno koliko vremena GPU provodi na ALU operacijama vs cekanje na memoriju, sto je neuporedivo preciznije od "probaj-pa-vidi" metoda.

Balansiran Shader

Idealan shader je balansiran -- ALU i TEX su podjednako zauzeti. Dok GPU cita teksturu iz memorije (sto traje vise taktova), compute jedinice rade matematiku. Kada matematika zavrsi, podaci iz teksture su vec stigli.

U praksi, ovo je tesko postici savrseno, ali mozete teziti tome:

Ovo je kontra-intuitivan koncept, ali je izuzetno koristan. Ponekad dodavanje posla zapravo ne usporava shader -- ako je taj posao na drugom resursu od onog koji je bottleneck.


21.4 Shader Complexity Visualization

Sta je Shader Complexity View?

UE5 ima ugradjeni rezim vizualizacije koji vam prikazuje koliko je svaki piksel na ekranu "skup" za renderovanje. Ovo je jedan od najkorisnijih alata za shader optimizaciju.

Da biste ga aktivirali:

  1. U Viewport-u, kliknite na padajuci meni za vizualizaciju (gore levo, gde pise "Lit")
  2. Izaberite "Optimization Viewmodes" > "Shader Complexity"
  3. Alternativno: koristite shortcut Alt+8 u editoru

Kako Citati Boje

Shader Complexity koristi toplinski (heat map) kolorni sistem:

Boja Znacenje Instruction Count (priblizno)
Tamno zelena Veoma jeftino < 20 instrukcija
Zelena Jeftino 20-50 instrukcija
Svetlo zelena Umereno 50-100 instrukcija
Zuta Skupo 100-200 instrukcija
Narandzasta Veoma skupo 200-300 instrukcija
Crvena Opasno skupo 300-500 instrukcija
Tamno crvena Kriticno skupo 500-700 instrukcija
Bela/Rozi Alarm! 700+ instrukcija

Evo kako ovo izgleda u praksi:

[Tipicna scena u Shader Complexity View]

Nebo:                  ZELENO (jednostavan gradient, jeftino)
Teren/Landscape:       ZUTO do NARANDZASTO (mnogo layera, skupo)
Obican zid:            ZELENO (1-2 teksture, jeftino)
Character:             ZELENO do ZUTO (PBR sa subsurface)
Translucent cestice:   CRVENO do BELO (overdraw!)
Staklo:                NARANDZASTO (translucent + refleksije)
Voda:                  CRVENO (refleksije + refrakcije + flow maps)
Postprocess zone:      Dodaje se na sve (ceo ekran postaje "skuplji")

Shader Complexity sa Quads

Postoji i poseban rezim: "Shader Complexity with Quads". Ovaj rezim dodatno prikazuje quad overdraw -- situaciju gde GPU trosi resurse na piksele koji su zapravo van trougla (jer GPU obradjuje piksele u 2x2 blokovima, pa mali trouglovi imaju mnogo "bespotrebnih" piksela).

Ovo je posebno bitno za scene sa mnogo malih trouglova -- npr. gusta vegetacija u daljini.

Sta Shader Complexity Prikazuje

Sta Shader Complexity NE Prikazuje

Ovo je jednako vazno:

Kako Koristiti Shader Complexity Efektivno

Koristite ga kao PRVU trijazu, ne kao jedinu dijagnozu.

  1. Otvorite Shader Complexity View
  2. Identifikujte oblasti koje su crvene ili bele
  3. Kliknite na objekte u tim oblastima da vidite koji materijal koriste
  4. Otvorite taj materijal i analizirajte instruction count
  5. Koristite GPU profiler za stvarne timinge

Pratite relativan odnos boja:

Posebno obratite paznju na:

Drugi Korisni Visualization Modovi

Pored Shader Complexity, UE5 nudi i druge korisne vizualizacije:

Light Complexity (Optimization Viewmodes > Light Complexity)

Lightmap Density (Optimization Viewmodes > Lightmap Density)

Stationary Light Overlap (Optimization Viewmodes > Stationary Light Overlap)


21.5 Sta Kosta u Shaderima -- Detaljan Pregled

Tabela Relativnih Cena Operacija

Evo detaljne tabele sa relativnim cenama razlicitih shader operacija. Cene su priblizne i variraju izmedju GPU arhitektura, ali relativni odnosi su generalno tacni:

Operacija Relativna cena Kategorija Napomene
add, sub (sabiranje, oduzimanje) 1x ALU Najjeftinija operacija
mul (mnozenje) 1x ALU Jednako jeftino kao sabiranje na modernim GPU
mad (multiply-add: a*b+c) 1x ALU Jedna instrukcija za dve operacije!
min, max, clamp, saturate 1x ALU Besplatno ili gotovo besplatno
abs, negate 0x (besplatno) ALU Source modifier, ne zauzima ALU
mov (kopiranje) 0-1x ALU Cesto eliminisano optimizacijom
dot (dot product) 1x ALU Hardverski podrzano, veoma brzo
cross (cross product) 2-3x ALU Par mul+mad operacija
lerp (linearna interpolacija) 1x ALU Obicno se mapira na mad
step 1x ALU Jednostavno poredjenje
smoothstep 3-4x ALU Ukljucuje mnozenje i sabiranje
rcp (reciprocal, 1/x) 1x ALU Brzo na vecini GPU-ova
rsqrt (1/sqrt(x)) 1-2x ALU Specijalizovan hardver
sqrt 2-3x ALU Obicno rsqrt + rcp
normalize 3-4x ALU dot + rsqrt + mul
length 2-3x ALU dot + sqrt
div (deljenje) 2-4x ALU Obicno rcp + mul
frac 1x ALU Brzo na vecini GPU
floor, ceil, round 1x ALU Hardverski podrzano
fmod (modulo) 2-4x ALU Obicno vise operacija
sin, cos 4-8x ALU Transcendentalna -- skupa!
tan 8-12x ALU sin/cos -- duplo skupa
asin, acos, atan 8-16x ALU Veoma skupa!
atan2 10-20x ALU Najskuplja trigonometrija
pow (stepen) 4-8x ALU exp2(y * log2(x))
exp, exp2 4-8x ALU Transcendentalna
log, log2 4-8x ALU Transcendentalna
Texture2DSample 4-20x+ TEX Zavisi od cache-a!
Texture2DSample (dependent) 20-100x+ TEX Blokira pipeline
TextureCubeSample 6-25x+ TEX Skuplje od 2D
Texture3DSample (volume) 8-30x+ TEX Skuplje od Cube
Branch (koherentan) 1-2x Control Svi pikseli u warp-u idu istim putem
Branch (divergentan) 2-10x+ Control Pikseli u warp-u idu razlicitim putevima

Transcendentalne Funkcije -- Detaljno

Transcendentalne funkcije (sin, cos, pow, exp, log) zasluzuju posebnu paznju jer su znacajno skuplje od osnovne aritmetike.

Zasto su skupe?

GPU ima specijalizovane hardware jedinice za ove operacije, ali ih ima manje nego opste ALU jedinice. Kada mnogo piksela istovremeno treba sin(), ove specijalizovane jedinice postaju bottleneck.

Neke GPU arhitekture (posebno starije i mobilne) koriste aproksimacije kroz serije operacija umesto specijalizovanog hardvera, sto ih cini jos skupljim.

Ceste zamke:

// SKUPO: pow u petlji
for (int i = 0; i < 8; i++) {
    result += pow(something, 2.0 + i); // pow se poziva 8 puta!
}

// SKUPO: sin za animaciju na svakom pikselu
float wave = sin(Time * 3.14159 + UV.x * 10.0); // sin po pikselu!

// SKUPO: normalize na svakom pikselu kada nije potrebno
float3 dir = normalize(someVector); // 3-4 ALU operacije
// Ako vam ne treba jedinicni vektor, ne normalizujte!

Jeftinije alternative:

// UMESTO pow(x, 2.0): koristite x * x
float result = x * x; // 1x umesto 4-8x

// UMESTO pow(x, 4.0): koristite (x*x)*(x*x)
float x2 = x * x;
float result = x2 * x2; // 2x umesto 4-8x

// UMESTO pow(x, 0.5): koristite sqrt(x)
float result = sqrt(x); // 2-3x umesto 4-8x

// UMESTO sin() za jednostavnu oscilaciju: koristite triangle wave
float triangle = abs(frac(Time) * 2.0 - 1.0); // ~2x umesto 4-8x

// UMESTO normalize() kada treba samo smer:
float3 dir = someVector * rsqrt(dot(someVector, someVector)); // ekvivalentno
// Ali kompajler obicno vec ovo radi za normalize -- proverite!

Texture Sample Cena -- Detaljno

Texture sample je posebna zver. Njegova "cena" varira enormno zavisno od okolnosti:

Cache Hit (najbolji slucaj): ~4-8 taktova

Cache Miss (najgori slucaj): ~200-800 taktova

Sto utice na cache performanse:

DOBRO za cache:
  + Bilinearni filtering (cita 4 susedna texela)
  + Koriscenje mipmapa (manji podaci, bolji cache)
  + Sekvencijalan pristup (susedni pikseli citaju susedne texele)
  + Manje teksture (cela tekstura staje u cache)

LOSE za cache:
  - Nasumicne UV koordinate (skakanje po teksturi)
  - Ogromne teksture bez mipmapa
  - Dependent reads (nepredvidiv pristup)
  - Trilinearni filtering (8 texela umesto 4)
  - Anizotropni filtering (do 16 texela po sample!)

Anizotropni filtering (AF) zasluzuje poseban komentar. Kada gledate povrsinu pod uglom (npr. pod koji se prostire u daljinu), AF poboljsava kvalitet filtriranja tako sto uzima vise uzoraka duz jedne ose. AF 16x znaci do 16 uzoraka po jednom "texture sample" -- sto moze znacajno povecati TEX cost.

Branching -- Kontekst je Sve

O branching-u smo detaljno govorili u Poglavlju 17, ali hajde da rezimiramo u kontekstu optimizacije:

Koherentan branch (svi pikseli u warp-u idu istim putem):

// Ovo je JEFTINO jer su svi pikseli koherentni
if (MaterialID == 1) {
    // svi pikseli ovog materijala idu ovde
    color = red;
} else {
    color = blue;
}

Cena: minimalna, samo branch instrukcija.

Divergentan branch (pikseli u istom warp-u idu razlicitim putevima):

// Ovo je SKUPO jer pikseli u istom warp-u divergiraju
if (UV.x > 0.5) {
    // neki pikseli idu ovde
    color = complexCalculation1();
} else {
    // drugi pikseli idu ovde
    color = complexCalculation2();
}

Cena: GPU izvrsava OBA puta za sve piksele u warp-u, maskirajuci rezultate. Efektivna cena je zbir obe grane.

Prakticna pravila za branching u shaderima:

  1. Branch na uniform vrednosti (ista za sve piksele) je gotovo besplatan
  2. Branch na vertex interpoliranoj vrednosti koja se menja postepeno -- obicno koherentan, relativno jeftin
  3. Branch na per-pixel kalkulaciji sa nasumicnim rezultatom -- potencijalno divergentan, moze biti skup
  4. Branch je isplativ samo ako preskace DOVOLJNO posla -- za 2-3 instrukcije, bolje je uvek ih izvrsiti

Dependent Reads -- Detaljno

Vec smo pomenuli dependent reads u sekciji o texture samples, ali hajde da dublje razumemo zasto su toliko problematicni.

Normalan tok GPU pipeline-a:

Takt 1-2:   GPU salje zahtev za teksturu A na UV koordinate (0.5, 0.3)
Takt 3-10:  Dok ceka podatke iz memorije, GPU radi ALU operacije
Takt 8-12:  Podaci za teksturu A stizu iz cache/memorije
Takt 9-10:  GPU salje zahtev za teksturu B na UV koordinate (0.7, 0.2)
Takt 11-20: GPU radi vise ALU operacija
Takt 18-22: Podaci za teksturu B stizu

Ukupno: ~22 taktova, ali GPU je radio korisne stvari vecinu vremena.

Tok sa dependent read-om:

Takt 1-2:   GPU salje zahtev za teksturu A na UV koordinate (0.5, 0.3)
Takt 3-12:  GPU CEKA jer mu trebaju podaci iz A da bi znao UV za B
            (ne moze da radi ALU jer nema sta da racuna)
Takt 12:    Podaci za A stizu. GPU racuna UV za B.
Takt 13-14: GPU salje zahtev za teksturu B na IZRACUNATE UV koordinate
Takt 15-24: GPU OPET CEKA na podatke iz B
            (opet ne moze nista korisno da radi)
Takt 24:    Podaci za B stizu. Shader nastavlja.

Ukupno: ~24 taktova, ali GPU je bio idle vecinu vremena! Propustena je ogromna kolicina paralelizma.

U realnosti, GPU pokusava da maskira ovu latenciju prebacivanjem na drugi warp dok ceka, ali ako svi warps imaju dependent reads, nema gde da se prebaci i GPU ceka.


21.6 Kako Smanjiti Cenu Shadera

1. Uklonite Nekorisne Feature-e

Ovo je najjednostavnija i najefikasnija optimizacija: ne radite ono sto ne morate.

Normal mapa na ravnoj povrsini? Ako je povrsina potpuno ravna (npr. zid od jednog velikog poligona), normal mapa mozda ne dodaje dovoljno vizuelnog kvaliteta da opravda cenu. Razmislite da li vam zaista treba.

SA normal mapom:     +1 texture sample, +~10 ALU (tangent space transform)
BEZ normal mape:     Usteda od 1 TEX + 10 ALU po pikselu

Metallic mapa za nemetalne objekte? Ako je objekat potpuno nemetalan (drvo, kamen, tkanina), ne treba vam metallic tekstura. Postavite Metallic = 0 kao konstantu.

AO mapa u prostoriji sa screen-space AO? Ako vec koristite SSAO ili GTAO, per-material AO mapa mozda nije neophodna, posebno na objektima u unutrasnjosti.

Detail textures na objektu koji se nikada ne vidi iz blizine? Ako je objekat uvek u srednjoj ili velikoj daljini, detail textures su bespotrebni trosak.

2. Koristite Jednostavniju Matematiku

Mnoge "skupe" operacije imaju jeftinije aproksimacije koje su vizuelno gotovo identicne.

Fresnel aproksimacija:

// TACNO ali skupo:
float fresnel = pow(1.0 - dot(Normal, ViewDir), 5.0);
// Cena: dot(1x) + sub(1x) + pow(4-8x) = ~6-10x

// SCHLICK APROKSIMACIJA (skoro identicno vizuelno):
float NdotV = dot(Normal, ViewDir);
float fresnel = pow(1.0 - NdotV, 5.0);
// Mozemo dalje optimizovati:
float x = 1.0 - NdotV;
float x2 = x * x;
float fresnel = x2 * x2 * x; // x^5 bez pow()!
// Cena: dot(1x) + sub(1x) + 3xmul = ~5x
// Usteda: ~50% od pow verzije

// JOS JEFTINIJE za mnoge slucajeve:
float fresnel = 1.0 - NdotV; // Linearni fresnel
// Cena: dot(1x) + sub(1x) = ~2x
// Nije fizicki tacno, ali za mnoge efekte izgleda OK

Brz sqrt za normalizaciju:

// UE5 Material Editor vec koristi optimizovane verzije,
// ali u custom HLSL kodu:

// UMESTO:
float len = length(vec);           // sqrt(dot(vec,vec))
float3 normalized = vec / len;     // skupo deljenje

// KORISTITE:
float invLen = rsqrt(dot(vec, vec)); // rsqrt je brz na GPU
float3 normalized = vec * invLen;     // mnozenje umesto deljenja

Aproksimacija za pow sa malim eksponentima:

// pow(x, 2) = x * x                    // 1 mul
// pow(x, 3) = x * x * x                // 2 mul
// pow(x, 4) = (x*x) * (x*x)           // 2 mul (ne 3!)
// pow(x, 5) = (x*x) * (x*x) * x       // 3 mul
// pow(x, 8) = ((x*x)*(x*x)) * ((x*x)*(x*x))  // 3 mul

3. Channel Packing Tekstura

Ovo smo vec pomenuli, ali zasluzuje detaljniji pregled jer je jedna od najefikasnijih tehnika.

Standardni UE5 PBR Setup (neoptimizovan):

Tekstura 1: Base Color (RGB)          → 1 sample
Tekstura 2: Normal Map (RG)           → 1 sample
Tekstura 3: Roughness (grayscale)     → 1 sample
Tekstura 4: Metallic (grayscale)      → 1 sample
Tekstura 5: AO (grayscale)            → 1 sample
Tekstura 6: Height/Displacement       → 1 sample
Tekstura 7: Emissive (RGB)            → 1 sample
─────────────────────────────────────────────────
UKUPNO: 7 texture samplea

Optimizovan Setup sa Channel Packing:

Tekstura 1: Base Color (RGB) + Opacity (A)    → 1 sample
Tekstura 2: Normal Map (RG)                    → 1 sample  
Tekstura 3: ORM packed:                        → 1 sample
             R = Ambient Occlusion
             G = Roughness  
             B = Metallic
             A = Height
Tekstura 4: Emissive (RGB) - samo ako treba    → 0-1 sample
─────────────────────────────────────────────────
UKUPNO: 3-4 texture samplea  (USTEDA: 3-4 samplea!)

Vazne napomene o channel packing:

Preporuceni kompresioni formati za packed teksture:

Tekstura Format Kanali Napomena
Base Color BC1 (DXT1) RGB Ako nema alpha
Base Color + Opacity BC3 (DXT5) ili BC7 RGBA BC7 je kvalitetniji
Normal Map BC5 RG Najbolji kvalitet za normale
ORM BC1 (DXT1) RGB Ako nema height u A
ORM + Height BC3 (DXT5) ili BC7 RGBA BC7 za bolji kvalitet

4. Prebacite Kalkulacije iz Pixel u Vertex Shader

Vertex shader se izvrsava jednom po vertexu, a pixel shader jednom po pikselu. Na tipicnom mesu, imate mnogo vise piksela nego vertexova. Na primer:

Objekat sa 10,000 vertexova koji zauzima 500x300 piksela:
  Vertex shader: 10,000 izvrsavanja
  Pixel shader:  150,000 izvrsavanja
  
  Odnos: 1:15 u korist vertex shadera!

Ako mozete da prebacite kalkulaciju iz pixel u vertex shader, ustedecete 15x izvrsavanja te kalkulacije (u ovom primeru).

Sta MOZE da se prebaci u vertex shader:

Sta NE MOZE (ili ne treba) da se prebaci:

Kako u UE5 Material Editoru:

Koristite "Vertex Interpolator" cvor (Custom UV u starijim verzijama):

  1. Spojite izracunavanje na ulaz Vertex Interpolator-a
  2. Rezultat ce se izracunati u vertex shaderu
  3. GPU ce ga automatski interpolirati za svaki piksel
  4. Koristite izlaz Vertex Interpolator-a u ostatku materijala
[Vertex Shader]
  TextureCoordinate → Multiply (Tiling) → [Vertex Interpolator Input]

[Pixel Shader]  
  [Vertex Interpolator Output] → Texture Sample → Base Color
  
  Umesto:
  TextureCoordinate → Multiply (Tiling) → Texture Sample → Base Color
  (gde bi se Multiply racunao za svaki piksel)

U ovom jednostavnom primeru usteda je minimalna (jedan multiply), ali za kompleksnije UV kalkulacije (rotacija, world-aligned teksture, proceduralne UV) usteda moze biti znacajna.

5. Material Quality Switches

UE5 ima ugradjeni sistem za razlicite nivoe kvaliteta materijala. Mozete definisati razlicite verzije istog materijala za razlicite platforme ili kvalitetne postavke.

Quality Switch Node:

U Material Editoru, dodajte Quality Switch cvor. Ovaj cvor ima ulaze za razlicite nivoe kvaliteta:

Quality Switch primer za Roughness:

HIGH:   Texture Sample (2048x2048 roughness map) → 
MEDIUM: Texture Sample (1024x1024 roughness map) →  Quality Switch → Roughness
LOW:    Constant (0.5, nema teksture uopste)     →

Ovo vam omogucava da imate jedan material koji se automatski prilagodjava hardveru.

Material Quality Level se postavlja sa:

r.MaterialQualityLevel 0  // Low
r.MaterialQualityLevel 1  // Medium  
r.MaterialQualityLevel 2  // High
r.MaterialQualityLevel 3  // Epic

Ovo generise odvojene shader permutacije za svaki nivo kvaliteta. Nema runtime cost-a za prebacivanje -- odgovarajuca permutacija se jednostavno koristi.

6. Precompute u Teksturama (Lookup Tables / LUT)

Ako imate skupu matematicku funkciju koja zavisi od jednog ili dva parametra, mozete da je "ispeczete" u teksturu i koristite texture sample umesto racunanja.

Primer: Custom Lighting Ramp

// SKUPO: Racunanje custom lighting rampe matematicki
float NdotL = dot(Normal, LightDir);
float lighting = smoothstep(-0.1, 0.1, NdotL) * 0.5 +
                 smoothstep(0.4, 0.6, NdotL) * 0.3 +
                 smoothstep(0.8, 0.9, NdotL) * 0.2;
// Cena: 3x smoothstep (3x4=12 ALU) + 2 mul + 2 add = ~16 ALU

// JEFTINO: Precomputed u 1D LUT teksturi
float NdotL = dot(Normal, LightDir);
float2 lutUV = float2(NdotL * 0.5 + 0.5, 0.5); // mapiranje iz [-1,1] u [0,1]
float lighting = Texture2DSample(LUT_Texture, Sampler, lutUV).r;
// Cena: 1 dot + 1 mad + 1 TEX = ~6 ukupno
// NAPOMENA: Ovo JE dependent read, ali je usteda u ALU velika

Primer: Atmosferski Scattering

// VEOMA SKUPO: Racunanje scattering-a u realnom vremenu
// (Rayleigh + Mie, visestruko integriranje)
// Moze da kosta 100+ ALU instrukcija

// JEFTINO: 2D ili 3D LUT
// Pretkorissite visinu (altitude) i ugao sunca kao UV koordinate
float2 lutUV = float2(altitude / maxAltitude, sunAngle / PI);
float3 scattering = Texture2DSample(ScatteringLUT, Sampler, lutUV).rgb;
// Cena: 2 div + 1 TEX = ~6-10 ukupno

Prednosti LUT-ova:

Mane LUT-ova:

7. Koristite Manje Interpolatora

Interpolatori (varyings) su podaci koji se prenose iz vertex shadera u pixel shader. GPU automatski interpolira ove vrednosti za svaki piksel unutar trougla.

Svaki interpolator zauzima registar i bandwidth u rasterizer fazi. UE5 koristi ogranicen broj interpolatora, i kada ih je previse, kompajler mora da "pakuje" podatke, sto moze dodati ALU instrukcije za pakovanje/raspakovanjе.

Tipicni interpolatori u UE5 materijalu:

UV Set 0:           2 floata (interpolator 0)
UV Set 1:           2 floata (interpolator 1)
Vertex Color:       4 floata (interpolator 2)
World Position:     3 floata (interpolator 3)
World Normal:       3 floata (interpolator 4)
Tangent, Bitangent: 6 floatova (interpolator 5-6)
Custom UVs:         2+ floatova po custom UV setu

Moderni GPU-ovi obicno podrzavaju 16-32 interpolatora (svaki je vec4, dakle 4 floata). Ako predjete ovu granicu, shader ce imati dodatne instrukcije za pakovanje.

Kako smanjiti broj interpolatora:


21.7 Shader LOD -- Jednostavniji Materijali na Daljini

Koncept Shader LOD

Vec znate za mesh LOD -- smanjivanje broja poligona za objekte u daljini. Shader LOD je isti koncept primenjen na materijale: koristite jednostavniji (jeftiniji) shader za objekte koji su dalje od kamere.

Logika je jednostavna: ako je objekat daleko i zauzima samo 20 piksela na ekranu, nema smisla koristiti materijal sa parallax occlusion mappingom, detail teksturama, i 15 texture samplea. Na 20 piksela, ne mozete da vidite te detalje. A GPU i dalje placa punu cenu za svaki od tih piksela.

Material Quality Levels za LOD

UE5 omogucava da razliciti mesh LOD nivoi koriste razlicite materijale. Ovo je najdirektiji nacin za implementaciju shader LOD-a.

Primer setup-a:

LOD 0 (blizu):    Material_Full
                  - Base Color (2048x2048)
                  - Normal Map (2048x2048)
                  - ORM packed (2048x2048)
                  - Detail Normal (1024x1024)
                  - Detail Tiling Mask
                  - Parallax Occlusion Mapping
                  → 8 texture samplea, 200+ instrukcija

LOD 1 (srednje):  Material_Medium
                  - Base Color (1024x1024)
                  - Normal Map (1024x1024)
                  - ORM packed (1024x1024)
                  - BEZ detail textura
                  - BEZ parallax-a
                  → 3 texture samplea, 80 instrukcija

LOD 2 (daleko):   Material_Simple
                  - Base Color (512x512)
                  - BEZ normal mape
                  - Roughness/Metallic kao konstante
                  → 1 texture sample, 30 instrukcija

LOD 3 (najdalje): Material_Minimal
                  - Boja iz vertex color-a ili konstanta
                  - BEZ tekstura
                  → 0 texture samplea, 15 instrukcija

Da biste ovo podesili u UE5:

  1. Otvorite Static Mesh Editor
  2. U LOD Settings, za svaki LOD nivo izaberite Material Slots
  3. Dodelite odgovarajuci materijal svakom LOD nivou

Automatic LOD Material Simplification

UE5 takodze podrzava automatsko pojednostavljivanje materijala za LOD-ove kroz Material Editor:

1. Feature Switches sa World Position Offset:

// U Material Editoru:
// Koristite "Distance to Camera" da automatski iskljucite feature-e

Distance to Camera → Divide (po MaxDistance) → Saturate → 

Ako je distance > Threshold1: iskljuci detail textures
Ako je distance > Threshold2: iskljuci normal map
Ako je distance > Threshold3: iskljuci sve osim base color

Ali PAZNJA: ovo je losiji pristup od pravih LOD materijala, jer GPU i dalje kompajlira sve feature-e -- samo branch-uje oko njih. Pravi benefit dolazi od potpuno odvojenih materijala sa manje feature-a, gde kompajler moze da elimise nekorisni kod.

2. Material Function LOD Blending:

Kreirajte Material Function koja na osnovu rastojanja od kamere bira izmedju dva ulaza:

Input A (High Quality) ──┐
                          ├── LOD Blend Function → Output
Input B (Low Quality)  ──┘
                          ↑
                     CameraDistance

Ovo je cist nacin da organizujete LOD logiku unutar jednog materijala.

Texture Resolution na Daljini

UE5 automatski koristi mipmap-e na osnovu rastojanja, tako da teksture na daljim objektima koriste manje mipmap nivoe (manje rezolucije). Ali mozete i eksplicitno kontrolisati texture streaming priority:

// Console komande za texture streaming
r.Streaming.PoolSize 1000        // Velicina texture streaming pool-a u MB
r.Streaming.FullyLoadedTextures   // Lista tekstura koje se uvek ucitavaju u punoj rezoluciji
r.Streaming.MipBias 0             // Globalni mip bias (vece = nize rezolucije)

Za specifican materijal, mozete koristiti Texture Object sa podesenim MipValueMode da forsirate nizi mip nivo na daljini.

Feature Stripping na Daljini

Ovo je lista feature-a po prioritetu uklanjanja (prvi se uklanja na najvecoj daljini, poslednji se cuva najduze):

Prioritet uklanjanja (prvi se uklanja):
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1. Parallax Occlusion Mapping    -- nevidljiv na daljini, veoma skup
2. Detail Normal Maps            -- nevidljive na daljini
3. Detail Textures (tiling)      -- nevidljive na daljini
4. Subsurface Scattering         -- efekat je suptilan na daljini
5. Clear Coat                    -- jedva vidljiv na daljini
6. Height-based Blending         -- zameni sa tezinskim blendingom
7. Emissive Map                  -- zameni sa konstantom ili ukloni
8. AO Map                       -- SSAO pokriva na daljini
9. Normal Map                   -- na dovoljnoj daljini nevazna
10. Roughness/Metallic Map       -- zameni sa konstantama

UVEK zadrzi:
  - Base Color (bar u nekoj formi)
  - Osnovne PBR konstante

Implementacija u Praksi: LOD Material System

Evo kompletnog workflow-a za implementaciju shader LOD sistema u UE5 projektu:

Korak 1: Definisajte LOD nivoe

LOD Nivo    Rastojanje (m)    Screen Size    Cilj Instrukcija
────────    ──────────────    ───────────    ─────────────────
LOD 0       0 - 10            > 0.5          Do 250
LOD 1       10 - 30           0.2 - 0.5      Do 120
LOD 2       30 - 100          0.05 - 0.2     Do 60
LOD 3       100+              < 0.05         Do 30

Korak 2: Kreirajte Material Instance-ove

Koristite Material Parameter Collections i Static Switches da kreirate varijante materijala:

Master Material:
  Static Switch: "Use Normal Map"
  Static Switch: "Use Detail Textures"  
  Static Switch: "Use Parallax"
  Static Switch: "Use Emissive"

Material Instance LOD0: sve ON
Material Instance LOD1: Parallax OFF, Detail OFF
Material Instance LOD2: + Normal Map OFF, Emissive OFF
Material Instance LOD3: samo Base Color

Korak 3: Dodelite materijale LOD-ovima

U Static Mesh Editoru ili u Blueprint-u, dodelite odgovarajuci Material Instance svakom LOD nivou.


21.8 Prakticni Primeri Optimizacije

Primer 1: Landscape Material -- Od Skupog do Optimizovanog

Landscape materijali su notorno skupi jer moraju da blend-uju vise layera na svakom pikselu. Hajde da analiziramo tipican skup landscape material i optimizujemo ga.

Pocetno stanje (NEOPTIMIZOVAN):

Landscape Material sa 4 layera:

Svaki layer ima:
  - Base Color (2048x2048)           1 sample
  - Normal Map (2048x2048)           1 sample
  - Roughness (2048x2048, odvojena)  1 sample
  - AO (2048x2048, odvojena)         1 sample
  - Height Map (za blending)         1 sample
  
Per-layer: 5 samplea × 4 layera = 20 samplea
Weightmap:                           1 sample (RGBA za 4 layera)
Macro Variation Texture:             1 sample
Distance-based Tiling Variation:     + 4-8 samplea (per layer, na blizini)

UKUPNO: 22-30 texture samplea!
Instruction count: 400-600+

DIJAGNOZA: Definitivno TEX bound. Crvena/bela u Shader Complexity.

Optimizacija Korak 1: Channel Packing

Svaki layer sada ima:
  - Base Color (2048x2048)           1 sample
  - Normal Map (2048x2048)           1 sample
  - ORM packed (R=AO, G=Rough, B=Height) 1 sample

Per-layer: 3 samplea × 4 layera = 12 samplea
Weightmap:                           1 sample
Macro Variation:                     1 sample

UKUPNO: 14 samplea
Usteda: 8-16 samplea! (36-53% manje)

Optimizacija Korak 2: Weight-based Layer Skipping

// Umesto da blend-ujemo SVI layeri za svaki piksel,
// preskocimo layere sa nultom tezinom

float4 weights = Texture2DSample(Weightmap, Sampler, UV);

float3 finalColor = float3(0,0,0);
float3 finalNormal = float3(0,0,1);

// Samo sampleujemo layer ako ima nenultu tezinu
// NAPOMENA: Ovo zahteva branch, ali je cesto koherentno
// jer su landscape layeri prostorno grupisani

if (weights.r > 0.001) {
    // Sample Layer 0
    finalColor += SampleLayer0(UV) * weights.r;
}
if (weights.g > 0.001) {
    // Sample Layer 1
    finalColor += SampleLayer1(UV) * weights.g;
}
// ... itd za ostale layere

Ovo moze da smanji efektivni broj samplea na 3-6 za vecinu piksela (jer vecina piksela koristi samo 1-2 layera), ali sa cenom branch instrukcija. Na landscape-u, ovo je obicno isplativo jer su layeri prostorno koherentni.

Optimizacija Korak 3: Runtime Virtual Texturing

Sa RVT:
  - Svi layeri se "ispeku" u virtualnu teksturu
  - Pixel shader cita samo iz virtualne teksture
  
RVT Texture:      1-2 samplea (base color + normal)
Weightmap:         NE TREBA (vec ispeccena u RVT)
Per-layer:         0 samplea u runtime-u!

UKUPNO: 1-3 texture samplea!!
Instruction count: 40-80

Ovo je dramticna usteda, ali RVT ima svoje troskove:

Optimizacija Korak 4: Distance-based Simplifikacija

Blizu (< 20m):    Puni material sa detail teksturama
                  14 samplea, 200 instrukcija
                  
Srednje (20-100m): Bez detail tekstura, macro variation samo
                   8 samplea, 120 instrukcija
                   
Daleko (> 100m):   RVT sa pre-baked rezultatom
                   2 samplea, 50 instrukcija

Finalni rezultat:

Metrika Pre Posle Usteda
Max texture samples 22-30 2-14 (zavisno od daljine) 50-90%!
Max instruction count 400-600 50-200 (zavisno od daljine) 50-90%!
Shader Complexity boja Crvena/Bela Zelena/Zuta Drasticno bolje

Primer 2: Character Skin Material -- Od Kompleksnog do Mobile-Friendly

Skin (koza) materijali su posebno zahtevni jer zahtevaju Subsurface Scattering (SSS) da bi izgledali realisticno.

Pocetno stanje (HIGH-END PC verzija):

Character Skin Material:

Teksture:
  - Base Color (4096x4096)                 1 sample
  - Normal Map (4096x4096)                 1 sample
  - Roughness Map (2048x2048)              1 sample
  - Subsurface Color (2048x2048)           1 sample
  - Subsurface Opacity/Thickness (2048x2048) 1 sample
  - Specular Map (2048x2048)               1 sample
  - Cavity/AO Map (2048x2048)              1 sample
  - Detail Normal (1024x1024)              1 sample
  - Pore Detail Mask (1024x1024)           1 sample
  - Makeup/Tattoo layer (opciono)          0-1 sample

Shading Model: Subsurface Profile
  - Screen-space subsurface scattering pass
  - Burley diffusion model

Features:
  - Dual-lobe specular
  - Microgeometry from detail normal
  - Screen-space contact shadows
  
UKUPNO: 9-10 texture samplea
Instruction Count: 180-250 (pixel shader samo)
+ SSS post-process pass: dodatnih 50+ instrukcija po pikselu!

Mobile-Friendly Verzija (Optimizovana):

Korak 1: Smanjite broj tekstura sa channel packing

Teksture (optimizovane):
  - Base Color (1024x1024)                 1 sample
    R: Red, G: Green, B: Blue
  - Normal Map (1024x1024)                 1 sample
    R: Normal.X, G: Normal.Y
  - Combined Map (1024x1024)               1 sample
    R: AO/Cavity
    G: Roughness
    B: Subsurface Opacity
    A: Specular variation

UKUPNO TEKSTURA: 3 samplea (usteda: 6-7 samplea!)

Korak 2: Zamenite Subsurface Scattering sa aproksimacijom

Pravi SSS zahteva screen-space post-process pass koji je veoma skup. Za mobile, koristite jeftinu aproksimaciju:

// JEFTINA SSS APROKSIMACIJA (Pre-Integrated Skin Shading)

// Umesto screen-space scattering,
// koristimo pre-integrated BRDF lookup teksturu

float NdotL = dot(Normal, LightDir);
float curvature = length(fwidth(Normal)); // koliko je zakrivljena povrsina

// LUT: X os = NdotL, Y os = curvature
// Pre-computed u LUT teksturi: kako SSS izgleda za dati ugao i zakrivljenost
float2 sssUV = float2(NdotL * 0.5 + 0.5, curvature);
float3 sssLighting = Texture2DSample(SSS_LUT, Sampler, sssUV).rgb;

// Rezultat izgleda slicno pravom SSS-u za vecinu slucajeva
// Cena: 1 dot + fwidth + 1 TEX (LUT) ≈ 15-20 ALU + 1 TEX
// vs pravi SSS: 50+ ALU + fullscreen post-process pass

Korak 3: Uklonite Detail Normal i Pore Detail

Na mobilnim uredjajima, ekran je manji i rezolucija niza. Detail normal map koji prikazuje pore koze je nevidljiv na malom ekranu. Uklonite ga.

Usteda: 2 samplea + ~15 ALU (tangent space blending)

Korak 4: Zamenite Dual-Lobe Specular sa Single-Lobe

// HIGH-END: Dual-lobe specular (dva GGX lobe-a)
float spec = GGX(Roughness1, NdotH) * weight1 + 
             GGX(Roughness2, NdotH) * weight2;
// Cena: ~30-40 ALU (dva GGX izracunavanja)

// MOBILE: Single-lobe specular
float spec = GGX(Roughness, NdotH);
// Cena: ~15-20 ALU
// Vizuelna razlika je minimalna na mobilnim ekranima

Korak 5: Koristite Unlit + Custom Lighting za najnize uredjaje

Za najslabije mobilne uredjaje, mozete potpuno izbeci PBR lighting i koristiti pre-baked lighting u teksturi:

Teksture:
  - Baked Lighting Texture (512x512): 1 sample
    (sadrzi base color * ambient + baked directional light)
  - Opciono: 1D ramp za toon-style dodatno svetlo

Shading Model: Unlit
Instruction Count: 5-10!

Ovo je drasticno pojednostavljenje koje gubi mnogo vizuelnog kvaliteta, ali na 5-inchnom ekranu sa ogranicenim hardverom, razlika je prihvatljiva.

Rezultati optimizacije skin materijala:

Metrika High-End PC Mobile (optimizovan) Mobile (minimal)
Texture Samples 9-10 3-4 1
ALU Instructions 180-250 60-80 5-10
Shading Model Subsurface Profile Default Lit Unlit
SSS Screen-space LUT aproksimacija Baked
Detail Normal Da Ne Ne
Specular Dual-lobe GGX Single-lobe GGX Nema (baked)
Vizuelni kvalitet Fotorealistican Dobar Prihvatljiv

Primer 3: Kompletna Optimizaciona Sesija -- Workflow

Hajde da prodjemo kroz tipicnu optimizacionu sesiju korak po korak, kao da sedite za racunarom i optimizujete materijale u svom projektu.

Korak 1: Identifikujte problem

1. Pokrenite igru u PIE (Play in Editor)
2. Otvorite console: stat unit
   
   Rezultat:
   Frame: 22.3ms (44 FPS)  -- cilj je 16.6ms (60 FPS)
   Game: 3.2ms
   Draw: 4.1ms
   GPU: 18.7ms  ← GPU je bottleneck!
   
3. Otvorite console: stat gpu
   
   Rezultat:
   BasePass: 12.4ms  ← vecina GPU vremena je ovde!
   Shadows: 2.1ms
   Lights: 1.8ms
   PostProcess: 2.4ms
   
   BasePass je skup → problem su materijali/shaderi

Korak 2: Vizualizujte problem

4. Prebacite na Shader Complexity view (Alt+8)
   
   Rezultat: Landscape je BELA, character je ZUTA,
             ostalo je ZELENO
   
   → Landscape material je daleko najskuplji element!
   
5. Testirajte da li je TEX ili ALU bound:
   Console: r.MipMapLODBias 8
   
   Rezultat: GPU vreme padne na 14ms
   → Delimicno TEX bound!
   
   Vratite: r.MipMapLODBias 0

Korak 3: Analizirajte najskuplji materijal

6. Otvorite Landscape material u Material Editoru
   
   Statistika:
   Base pass: 487 instrukcija
   82 ALU, 38 TEX, 367 other
   
   38 texture samplea! To je MNOGO.
   
7. Analizirajte strukturu materijala:
   - 5 landscape layera × (BaseColor + Normal + Roughness + AO + Height)
   - = 5 × 5 = 25 layer samplea
   - + weightmap: 2 samplea (5 layera zahtevaju 2 RGBA teksture)
   - + macro variation: 1 sample
   - + distance blend: +10 samplea (detail na blizini)
   - = 38 samplea ukupno

Korak 4: Primenite optimizacije

8. Channel packing: Spojite AO + Roughness + Height u ORM teksturu
   Usteda: 2 samplea po layeru × 5 = 10 samplea
   Novi total: 28 samplea → bolje, ali i dalje mnogo
   
9. Smanjite na 4 layera (uklonite najmanje koriseni)
   Usteda: 3 samplea + nesto ALU za blending
   Novi total: 25 samplea
   
10. Implementirajte weight-based skipping
    Efektivni samplea: ~8-12 (vecina piksela = 1-2 layera)
    
11. Uklonite distance blend za layere koji nisu dominantni
    Usteda: ~6-8 samplea na blizini
    
12. Razmatrajte RVT za daleke rastojanja
    Na daljini: 2-3 samplea umesto 20+

Korak 5: Verifikujte rezultate

13. Ponovo pokrenite stat unit:
    
    Frame: 14.8ms (67 FPS!)  -- iznad cilja!
    GPU: 11.2ms
    
14. Shader Complexity: Landscape je sada ZUTA umesto BELA
    
15. Verifikujte vizuelni kvalitet:
    - Da li landscape i dalje izgleda dobro? DA
    - Da li ima vidljivih artefakata? NE
    - Da li su prelazi izmedju layera glatki? DA
    
    OPTIMIZACIJA USPESNA!

Cheat Sheet: Brze Optimizacije

Evo liste "quick wins" -- optimizacija koje mozete primeniti brzo sa minimalnim rizikom od vizuelne degradacije:

┌─────────────────────────────────────────────────────────────────┐
│                    BRZE SHADER OPTIMIZACIJE                     │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│ TEKSTURE:                                                       │
│ □ Channel pack ORM (AO + Roughness + Metallic u jednu)         │
│ □ Koristite konstantne vrednosti umesto tekstura gde mozete     │
│ □ Smanjite nepotrebno velike teksture (4K → 2K gde se ne vidi) │
│ □ Uklonite detail textures na objektima daleko od kamere        │
│                                                                 │
│ MATEMATIKA:                                                     │
│ □ Zamenite pow(x,2) sa x*x                                    │
│ □ Zamenite pow(x,n) sa uzastopnim mnozenjem za male n          │
│ □ Zamenite sin/cos sa aproksimacijama gde je moguce            │
│ □ Ne normalizujte vektore koji vec jesu (ili skoro) jedinicni  │
│                                                                 │
│ ARHITEKTURA:                                                    │
│ □ Prebacite UV kalkulacije u vertex shader                     │
│ □ Koristite Quality Switch za Low/Medium/High verzije          │
│ □ Implementirajte shader LOD (prostiji materijali na daljini)  │
│ □ Koristite Material Instances umesto kopija materijala        │
│                                                                 │
│ LANDSCAPE SPECIFIČNO:                                           │
│ □ Max 4 layera po komponenti                                   │
│ □ Channel pack sve layer teksture                              │
│ □ Razmatrajte RVT za srednje/daleke rastojanje                 │
│ □ Uklonite height-based blending na daljini                    │
│                                                                 │
│ TRANSLUCENT/PARTICLE SPECIFIČNO:                                │
│ □ Koristite Unlit gde god mozete                               │
│ □ Minimizirajte overdraw                                       │
│ □ Koristite "Responsive AA" (smanjuje sample count)            │
│ □ Pojednostavite particle shader koliko god mozete             │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

21.9 Napredne Teme

Shader Permutacije i Kompilaciono Vreme

Kada koristite Static Switches u materijalu, UE5 generise odvojenu shader permutaciju za svaku kombinaciju. Sa N static switch-eva, potencijalno imate 2^N permutacija!

Primer:
  Static Switch: Use Normal Map (ON/OFF)
  Static Switch: Use Emissive (ON/OFF)
  Static Switch: Use Detail (ON/OFF)
  
  Permutacije: 2 × 2 × 2 = 8 verzija shadera

  Sa 10 static switch-eva: 2^10 = 1024 permutacija!
  Svaka se kompajlira odvojeno → dugo kompilaciono vreme

Kako upravljati permutacijama:

Async Shader Compilation

UE5 moze da kompajlira shadere asinhrono, sto sprečava stuttering pri prvom koriscenju materijala. Ali ovo znaci da ce se pri prvom vidjenju materijala prikazati default (obicno sivi) materijal dok se pravi shader kompajlira.

// Ukljucite/iskljucite async kompilaciju:
r.ShaderPipelineCacheAsync 1   // asinhrona kompilacija (default)
r.ShaderPipelineCacheAsync 0   // sinhrona (moze da uzrokuje stuttering)

// Pipeline State Object (PSO) caching:
r.ShaderPipelineCache.Enabled 1  // kesiranje kompajliranih shadera

Za shipping buildove, koristite PSO Caching da pre-kompajlirate sve shader permutacije. Ovo eliminise runtime kompilaciju i stuttering.

Ocekivanja po Platformi

Razlicite platforme imaju razlicite "budzete" za shadere:

HIGH-END PC (RTX 3080+, 1440p@60fps):
  Budget po pixel shaderu: ~300-400 instrukcija (opaque)
  Budget texture samplea: ~20-25
  Napomena: Na 4K rezoluciji, smanjite budzet za ~40%

MID-RANGE PC (GTX 1660, 1080p@60fps):
  Budget po pixel shaderu: ~150-200 instrukcija
  Budget texture samplea: ~12-16
  
KONZOLE (PS5/XSX, 4K@30fps ili 1080p@60fps):
  Budget po pixel shaderu: ~200-300 instrukcija
  Budget texture samplea: ~16-20
  Napomena: Konzole imaju predvidiv hardver, lakse za optimizaciju

MOBILE (High-end, 1080p@30fps):
  Budget po pixel shaderu: ~50-80 instrukcija
  Budget texture samplea: ~4-8
  
MOBILE (Low-end, 720p@30fps):
  Budget po pixel shaderu: ~20-40 instrukcija
  Budget texture samplea: ~2-4
  
SWITCH (720p@30fps docked):
  Budget po pixel shaderu: ~60-100 instrukcija
  Budget texture samplea: ~6-10

Ovi budzeti su okvirni i zavise od kompleksnosti scene, broja objekata, overdraw-a, i mnogo drugih faktora. Ali daju vam startnu tacku za planiranje.

Half Precision (FP16)

Moderni mobilni GPU-ovi (i neki desktop GPU-ovi) mogu da rade sa half precision (16-bit) floating point brojevima umesto punog 32-bit precision-a. Half precision je obicno duplo brzi za ALU operacije.

// Full precision (FP32):
float3 color = baseColor * lightIntensity;

// Half precision (FP16):
half3 color = (half3)baseColor * (half)lightIntensity;
// Na mobilnim GPU-ovima: potencijalno 2x brze!

Kada koristiti half precision:

Kada NE koristiti half precision:

U UE5 Material Editoru, mozete kontrolisati preciznost nekih izracunavanja, ali engine uglavnom automatski odlucuje sta moze da bude half precision. Za custom HLSL, eksplicitno koristite half tip gde je to bezbedno.

Wave Intrinsics i Optimizacija na Nivou Warp-a

Napredna tehnika: koriscenje wave intrinsics (HLSL Shader Model 6.0+) za komunikaciju izmedju piksela unutar istog warp-a.

// Umesto da svaki piksel racuna prosek:
float avg = someExpensiveCalculation();

// Sa wave intrinsics, jedan piksel racuna i deli rezultat:
float avg = WaveReadLaneFirst(someExpensiveCalculation());
// Samo prvi lane racuna, ostali citaju rezultat

Ovo je napredna tehnika koja zahteva razumevanje GPU arhitekture (Poglavlje 08). UE5 koristi wave intrinsics interno za neke optimizacije (npr. u Nanite i Lumen sistemima), ali obicno ne u korisnickim materijalima.


21.10 Alati za Profajliranje Shadera

UE5 Ugradeni Alati

1. stat gpu

Console: stat gpu
Prikazuje: vreme po GPU pass-u
Koristi za: identifikaciju koji pass je najskuplji

2. stat unit

Console: stat unit
Prikazuje: ukupne timinge (Game, Draw, GPU)
Koristi za: identifikaciju da li je CPU ili GPU bottleneck

3. stat rhi

Console: stat rhi
Prikazuje: RHI statistike (draw calls, primitives, triangles)
Koristi za: identifikaciju prekomerne geometrije

4. ProfileGPU (Ctrl+Shift+,)

Shortcut: Ctrl+Shift+, (zarez)
Prikazuje: detaljni GPU profil sa vremenima po pass-u
Koristi za: najdetaljniji pregled GPU performansi u editoru

5. Shader Complexity Viewmode

Viewport: Optimization Viewmodes > Shader Complexity
Prikazuje: vizuelnu heat mapu instruction cost-a
Koristi za: brzu identifikaciju skupih materijala

6. Material Statistics

Material Editor: donji levi ugao
Prikazuje: instruction count, texture sample count
Koristi za: poredjenje razlicitih verzija istog materijala

Spoljni Alati

RenderDoc (Besplatan)

Koraci:
1. Skinite sa renderdoc.org
2. U UE5: Edit > Project Settings > Engine > Rendering
   → Omogucite "Render Doc Plugin"
3. Pritisnite F12 da snimite frame (ili Alt+F12)
4. RenderDoc se otvara sa snimljenim frame-om
5. Mozete da analizirate svaki draw call, vidite shadere,
   meriti vreme po operaciji

NVIDIA Nsight Graphics

Najbolji alat za NVIDIA GPU-ove:
- Prikazuje ALU vs TEX utilizaciju
- Warp occupancy
- Cache hit/miss rate
- Detaljne metrike po shaderu
- Requires NVIDIA GPU

Koraci:
1. Instalirajte Nsight Graphics (besplatan za NVIDIA GPU)
2. Pokrenite UE5 kroz Nsight ("Launch" mode)
3. Snimite frame
4. Analizirajte shader metrike

PIX (Microsoft)

Za Windows i Xbox development:
- Detaljni GPU timings
- Shader debugging
- Pipeline state analiza
- Resource viewer

Koraci:
1. Skinite PIX sa Microsoft sajta
2. Pokrenite UE5 pod PIX-om
3. Snimite frame (GPU Capture)
4. Analizirajte u PIX interfejsu

AMD Radeon GPU Profiler (RGP)

Za AMD GPU-ove:
- Wavefront occupancy
- Cache utilizacija
- Instruction timing
- Pipeline bottleneck analiza

Prakticni Saveti za Profajliranje

1. UVEK profajlirajte na CILJNOM hardveru
   - Profajliranje na RTX 4090 vam nece pomoci da
     optimizujete za GTX 1650
   
2. Profajlirajte u SHIPPING ili DEVELOPMENT buildu
   - Editor ima overhead koji moze da zamaskira prave probleme
   
3. Iskljucite V-SYNC pri profajliranju
   - V-Sync limitira framerate i sakriva pravu GPU cenu
   Console: r.VSync 0
   
4. Koristite FIKSIRANU scenu za poredjenje
   - Snimite tacnu poziciju kamere
   - Koristite istu scenu pre i posle optimizacije
   
5. Merite VISE puta i uzmite prosek
   - GPU timings variraju od frame-a do frame-a
   - Uzmite prosek od 100+ frejmova za pouzdane rezultate
   
6. Optimizujte NAJSKUPLJE stvari prvo
   - Nemojte gubiti vreme na optimizaciju shadera koji
     zauzima 0.1ms kada imate shader koji zauzima 5ms

21.11 Rezime i Kljucni Pojmovi

Kljucne Lekcije

  1. Instruction count nije sve -- tip instrukcija i memory access patern su jednako vazni
  2. Channel packing je "besplatna" optimizacija -- uvek radite, nema vizuelnog gubitka
  3. Identifikujte bottleneck pre optimizacije -- ALU bound vs TEX bound zahtevaju razlicite pristupe
  4. Shader LOD je kriticno vazan -- ne koristite isti materijal na svim rastojanjima
  5. Profajlirajte na ciljnom hardveru -- GPU profiler je vas najbolji prijatelj
  6. Dependent texture reads su tihi ubica -- pazite na lanac zavisnosti u texture samplima
  7. Translucent materijali traze posebnu paznju -- overdraw mnozbeno povecava cenu

Tabela Kljucnih Pojmova

Pojam (EN) Opis
Instruction Count Broj instrukcija u kompajliranom shaderu
ALU Instruction Aritmeticko-logicka instrukcija (matematika)
TEX Instruction Instrukcija za citanje iz teksture
ALU Bound Stanje gde su compute jedinice GPU-a bottleneck
TEX Bound Stanje gde je memorijski bandwidth bottleneck
Shader Complexity UE5 vizualizacioni rezim koji prikazuje cenu shadera
Dependent Texture Read Texture sample ciji UV zavisi od rezultata prethodnog samplea
Channel Packing Pakovanje vise grayscale mapa u kanale jedne teksture
Texture Atlas Velika tekstura koja sadrzi vise manjih tekstura
LUT (Lookup Table) Tekstura koja sadrzi preracunate vrednosti kompleksnih funkcija
Shader LOD Koriscenje prostijih shadera za objekte na vecoj daljini
Transcendental Function Matematicka funkcija (sin, cos, pow, exp, log) koja je skuplja od osnovne aritmetike
Overdraw Renderovanje istog piksela vise puta (zbog preklapanja objekata)
Shader Permutation Varijanta shadera generisana za specificnu kombinaciju feature-a
Quality Switch UE5 node koji bira razlicite puteve na osnovu quality level-a
RVT (Runtime Virtual Texturing) UE5 sistem koji "pece" kompleksne materijale u virtualne teksture
Half Precision (FP16) 16-bitni floating point, brzi ali manje precizan od FP32
Warp/Wavefront Grupa piksela (obicno 32 ili 64) koja se izvrsava zajedno na GPU
PSO Cache Pre-kompajlirani shader cache za eliminaciju runtime kompilacije
Interpolator/Varying Podatak koji se prenosi iz vertex u pixel shader
MAD (Multiply-Add) Operacija a*b+c izvedena kao jedna instrukcija
Cache Hit/Miss Da li su trazeni podaci vec u brzom kesu GPU-a
Mipmap Niz sve manjih verzija teksture za razlicita rastojanja
Anisotropic Filtering Filtriranje teksture koje poboljsava kvalitet pod uglom

21.12 Reference i Dalje Citanje

Poglavlja u Ovoj Knjizi

Zvanicna Dokumentacija

GPU Proizvodjaci -- Best Practices

Prezentacije i Clanci

Alati


Sledece poglavlje (22): UE5 Material System -- Sada kada znate kako da merite i optimizujete shadere, naucicemo kako da efikasno koristite UE5 Material Editor, Material Instances, Material Functions, i sve alate koje vam engine pruza za kreiranje materijala koji izgledaju odlicno I rade brzo.


Zapamtite: optimizacija nije neprijatelj lepote. Optimizovan shader koji radi na 60 FPS je lepsi od fotorealisticnog shadera koji radi na 15 FPS. Vas cilj je da postignete najbolji moguchi vizuelni kvalitet u okviru budzeta koji vam hardver dozvoljava.