Poglavlje 42: Draw Calls i Batching

Poglavlje 42: Draw Calls i Batching

U ovom poglavlju ulazimo u jednu od najvaznijih tema optimizacije u real-time renderovanju: draw call-ove. Razumecete sta je draw call, zasto je skup, koliko ih je "previse", i koje tehnike -- instancing, mesh merging, auto-instancing, Nanite -- stoje na raspolaganju da ih drzite pod kontrolom. Ovo nije teoretsko poglavlje: ovo je poglavlje koje ce direktno odrediti da li vasa igra radi na 60 FPS ili na 20 FPS.


Uvod: Nevidljivi ubica performansi

Zamislite sledeci scenario. Napravili ste prekrasnu scenu u Unreal Engine 5. Imate lepe materijale, odlicno osvetljenje, sve izgleda fantasticno u editoru. Pokrenete igru, pogledate FPS counter -- 22 FPS. Otvorite GPU profiler -- GPU nije ni blizu maksimalnog opterecenja. Koristi mozda 40% svog kapaciteta. Pa gde je onda problem?

Otvorite CPU profiler. I tu vidite -- rendering thread je potpuno zagusljen. CPU provodi vecinu svog vremena pripremajuci i salajuci komande GPU-u. GPU ceka. CPU se gusi.

Dobrodosli u svet draw call-ova.

Draw call-ovi su jedan od onih problema koji su potpuno nevidljivi dok vas ne udare. Ne mozete ih videti u viewportu. Ne postoji crveno upozorenje koje iskoci i kaze "imate previse draw call-ova". Scena moze izgledati jednostavno -- par stotina objekata, nista spektakularno -- a opet da ima 15.000 draw call-ova koji guse vas CPU.

Ovo poglavlje je posveceno tome da razumete ovaj problem do kosti, i da naucite svaki alat koji UE5 nudi za njegovo resavanje. Ako ste procitali poglavlje 07 (Render Pipeline) i poglavlje 08 (GPU Arhitektura), vec imate solidne temelje. Sada cemo ih primeniti na jedan od najprakticnijih problema u game developmentu.

Krenimo.


42.1 Sta je Draw Call?

Definicija

Draw call je komanda koju CPU salje GPU-u sa instrukcijom: "Nacrtaj ovaj mesh, sa ovim materijalom, na ovoj poziciji u svetu."

To je to. Na najvisem nivou apstrakcije, draw call je upravo to -- jedna instrukcija za crtanje. Ali kao sto cemo videti, djavao je u detaljima.

Anatomija jednog draw call-a

Hajde da razlozimo sta se zapravo dogadja kada CPU treba da naredi GPU-u da nacrta jedan objekat -- recimo, jednu drvenu kutiju na podu vase scene.

Pre nego sto GPU moze da nacrta tu kutiju, CPU mora da uradi sledece:

  1. Postavi vertex buffer -- kaze GPU-u odakle da cita podatke o temenima (verteksima) tog mesh-a
  2. Postavi index buffer -- kaze GPU-u u kojem redosledu da povezuje ta temena u trouglove
  3. Postavi shader program -- kaze GPU-u koji vertex shader i pixel shader da koristi (tj. koji materijal da primeni)
  4. Postavi teksture -- binduje sve teksture koje taj materijal koristi (diffuse, normal, roughness, itd.) na odgovarajuce texture slot-ove
  5. Postavi uniform/constant buffer -- uploaduje parametre specifigne za ovaj objekat (World transformacija matrica, boja materijala, custom parametri, itd.)
  6. Postavi render state -- blending mode, depth testing, stencil state, face culling, itd.
  7. Izda draw komandu -- konacno, kaze GPU-u "nacrtaj"
CPU priprema za jedan draw call:
    
    SetVertexBuffer(kutija_vertices)        // Koji mesh?
    SetIndexBuffer(kutija_indices)          // Koji trouglovi?
    SetShaderProgram(wood_shader)           // Koji shader/materijal?
    SetTexture(0, wood_diffuse)             // Koja diffuse tekstura?
    SetTexture(1, wood_normal)              // Koja normal mapa?
    SetTexture(2, wood_roughness)           // Koja roughness mapa?
    SetConstantBuffer(world_matrix, ...)    // Gde u svetu?
    SetRenderState(opaque, depth_test_on)   // Kako crtati?
    
    DrawIndexed(num_triangles)              // NACRTAJ!

Vidite li obrazac? Sama komanda DrawIndexed na kraju je trivijalna -- to je bukvalno jedno pozivanje funkcije. Ali svih sedam koraka pre nje? To je ono sto kosta.

Draw call sam po sebi je jeftin -- state changes su skupi

Ovo je mozda najvaznija recenica u celom poglavlju, pa je ponovimo:

Sam draw call je brz. Promena stanja (state change) koja ga okruzuje je skupa.

Kada kazemo "draw call-ovi su skupi", mi zapravo mislimo na sav taj setup koji prethodi svakom draw call-u. Svaki put kada CPU menja koji mesh se koristi, ili koji shader, ili koje teksture -- to je state change. I svaki state change ima svoju cenu.

Zasto? Zato sto svaka promena stanja zahteva:

Zamislite to kao fabriku za proizvodnju automobila. Sam proces montiranja jednog automobila je brz kada je linija podesnea. Ali ako za svaki automobil morate da promenite alate, boju, delove -- svaka ta promena zahteva zaustavljanje linije, zamenu opreme, i ponovo pokretanje. "Presvlacenje" linije je skupo, ne sama montaza.

CPU priprema, GPU izvrsava

Zapamtite ovo jasno (i ako ste procitali poglavlje 08, ovo ce vam biti poznato):

Ovo znaci da problem sa draw call-ovima je CPU problem, ne GPU problem. Vas GPU moze biti najjaci na svetu, ali ako CPU ne moze da pripremi dovoljno draw call-ova na vreme, GPU ce sedeti i cekati.

U profajleru, ovo izgleda ovako:

CPU: ████████████████████████░░░░░░  (rendering thread zauzet 80% vremena)
GPU: ██████░░░░░░░░░░░░░░░░░░░░░░░  (GPU zauzet samo 25% vremena)
                                     
                                     GPU CEKA CPU!

Ovo je klasicna slika CPU-bound scenarija uzrokovanog prevelikim brojem draw call-ova. GPU ima kapacitet da renderuje mnogo vise, ali CPU ga ne moze hraniti dovoljno brzo.


42.2 Zasto su Draw Calls skupi -- Detaljno

Sada kada znamo sta je draw call, hajde da detaljno razumemo zasto je skup. Ovo znanje je kljucno za razumevanje svih optimizacionih tehnika koje slede.

42.2.1 CPU overhead: Drajver kao posrednik

Izmedju vaseg koda (ili engine-a) i GPU-a stoji graficki drajver (graphics driver). Drajver je softverski sloj koji prevodi API pozive (DirectX, Vulkan, OpenGL) u instrukcije koje konkretni GPU razume.

Svaki put kada vas engine pozove neku DirectX ili Vulkan funkciju, drajver mora da:

  1. Validira parametre -- da li je ovaj buffer zaista alociiran? Da li je ovaj shader kompajliran? Da li su sve teksture u validnom stanju?
  2. Prevede poziv -- DirectX API je genricki, ali svaki GPU (NVIDIA GeForce, AMD Radeon, Intel Arc) ima razlicitu internu arhitekturu. Drajver mora da prevede genericke pozive u hardware-specifine komande.
  3. Puni command buffer -- drajver pakuje komande u buffer koji ce se poslati GPU-u. Ovaj buffer se zove command buffer (ili command list u DX12 terminologiji).
  4. Upravlja resursima -- prati koji resursi su u upotrebi, gde se nalaze u memoriji, da li treba nesto da se premesti iz sistemskog RAM-a u VRAM.

Sve ovo trosi CPU cikluse. I sto je kriticno -- u tradicionalnim API-jima kao sto su DirectX 11 i OpenGL, ovo se sve desava na jednoj niti (single-threaded). To znaci da bez obzira koliko CPU jezgara imate, sav ovaj posao ide kroz jedno usko grlo.

Tradicionalni API (DX11/OpenGL):

Thread 0 (Rendering):  ████████████████████████████████  (100% zauzet)
Thread 1:              ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░  (slobodan)
Thread 2:              ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░  (slobodan)
Thread 3:              ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░  (slobodan)
...

Samo jedna nit radi sav rendering posao!

42.2.2 State Changes: Pravi krivac

Kao sto smo vec pomenuli, draw call sam po sebi je brz. Promena stanja je skupa. Ali nisu sve promene stanja jednako skupe. Evo grubog rangiranja, od najskuplje do najjeftinije:

Promena stanja Relativna cena Objasnjenje
Promena Render Target-a ★★★★★ GPU mora da flushuje pipeline, sacuva trenutni render target, ucita novi
Promena Shader programa ★★★★ GPU mora da ucita novi shader u izvrsne jedinice, resetuje registre
Promena tekstura ★★★ GPU mora da azurira texture descriptor-e, potencijalno ucita teksturu iz RAM-a u kes
Promena vertex/index buffer-a ★★ Relativno jeftino -- samo azurira pointer na podatke
Promena constant buffer-a (uniformi) Najjeftinije -- mali transfer podataka

Primetite nesto vazno: promena shader programa (tj. promena materijala) je medju najskupljim operacijama. Ovo je direktna veza sa temom ovog poglavlja -- svaki razlicit materijal u vasoj sceni potencijalno zahteva skup state change.

42.2.3 DX11 vs DX12/Vulkan: Razlika u overhead-u

Razlika izmedju "starijih" i "novijih" grafickih API-ja je fundamentalna za razumevanje draw call problematike.

DX11 / OpenGL -- Visok per-call overhead

DirectX 11 i OpenGL su high-level API-ji sa debelim slojem apstrakcije. Drajver radi ogroman posao za vas:

Ovo je udobno za programera, ali ima cenu: svaki API poziv prolazi kroz debeo sloj drajverskog koda. Za 5.000 draw call-ova, to je 5.000 puta prolazak kroz ceo taj sloj. Na DX11, svaki draw call moze da kosta negde izmedju 5 i 20 mikrosekundi CPU vremena (zavisno od kompleksnosti state change-a i drajvera). Na prvi pogled to zvuci malo, ali:

5.000 draw calls x 10 us (mikrosekundi) prosecno = 50.000 us = 50 ms

Za 60 FPS, imate samo 16.67 ms po frejmu!
50 ms samo za pripremu draw call-ova = nemoguci 60 FPS.

DX12 / Vulkan -- Nizak per-call overhead

DirectX 12 i Vulkan su low-level API-ji koji prebacuju mnogo odgovornosti na programera (tj. na engine):

Rezultat: drajverski overhead po draw call-u je dramaticno manji. Svaki draw call moze da kosta samo 1-3 mikrosekunde. Ali -- i ovo je kriticno -- overhead nije nula. Cak i sa DX12/Vulkan, 20.000 draw call-ova ce vas kostati:

20.000 draw calls x 2 us prosecno = 40.000 us = 40 ms

I dalje je to previse za 60 FPS!

Dakle, DX12/Vulkan vam daju vise prostora, ali ne eliminisu problem draw call-ova u potpunosti. Takodje, DX12 omogucava multithreaded command recording -- vise CPU niti moze paralelno da priprema komande, sto dramaticno poboljsava skalabilnost.

DX12/Vulkan multithreaded:

Thread 0:  ████████░░░░░░░░  (25% zauzet -- priprema draw calls 0-2499)
Thread 1:  ████████░░░░░░░░  (25% zauzet -- priprema draw calls 2500-4999)
Thread 2:  ████████░░░░░░░░  (25% zauzet -- priprema draw calls 5000-7499)
Thread 3:  ████████░░░░░░░░  (25% zauzet -- priprema draw calls 7500-9999)

Isti posao, ali podeljen na 4 niti!

UE5 koristi RHI (Rendering Hardware Interface) koji apstrahuje razlike izmedju API-ja. Na Windows-u, UE5 primarno koristi DX12 (mada podrzava i DX11 za stariji hardver). Na konzolama koristi native API-je. Poenta je -- UE5 vec koristi DX12 gde moze, ali vi i dalje morate paziti na broj draw call-ova.

42.2.4 Drajver vs Engine overhead

Vazno je razlikovati dva tipa overhead-a:

Drajver overhead -- vreme koje drajver (NVIDIA/AMD/Intel softver) trosi na validaciju, prevod i slanje komandi GPU-u. Ovo je ono sto DX12 smanjuje.

Engine overhead -- vreme koje sam Unreal Engine trosi na pripremu podataka za svaki draw call. Ovo ukljucuje:

Ovaj engine overhead postoji bez obzira koji API koristite. Cak i sa DX12, Unreal i dalje mora da prodje kroz svaki objekat i pripremi podatke. Ovo je razlog zasto Nanite (poglavlje 30) predstavlja tako radikalnu promenu -- on ne samo da smanjuje drajver overhead, vec eliminise i veliki deo engine overhead-a prebacivanjem posla na GPU.


42.3 Koliko je previse? Brojke i benchmarks

"Koliko draw call-ova je previse?" -- ovo je pitanje koje svaki developer postavi. I odgovor je, naravno: zavisi.

42.3.1 Faktori koji uticu na limit

Limit draw call-ova zavisi od:

  1. CPU brzine -- brzi CPU = vise draw call-ova moze da pripremi u 16.67 ms (za 60 FPS)
  2. Grafickog API-ja -- DX12/Vulkan imaju manji overhead po draw call-u nego DX11/OpenGL
  3. Kompleksnosti state change-ova -- mnogo razlicitih materijala = skuplje nego mnogo objekata sa istim materijalom
  4. Engine overhead-a -- koliko posla engine radi po objektu (culling, sorting, itd.)
  5. Ciljnog frame rate-a -- 30 FPS vam daje duplo vise vremena nego 60 FPS
  6. Sta jos CPU radi -- gameplay logika, fizika, AI, audio -- sve to deli CPU vreme sa rendering thread-om

42.3.2 Okvirne brojke

Evo nekih okvirnih smernica, ali zapamtite -- ovo su samo orijentacione vrednosti, ne apsolutna pravila:

API / Platforma Okvirni limit (60 FPS) Komentar
DX11 (mid-range CPU) ~2.000 - 4.000 Tradicionalni limit, jednoniten rendering
DX11 (high-end CPU) ~4.000 - 6.000 Brzi CPU pomaze, ali jednoniten je i dalje problem
DX12 (mid-range CPU) ~5.000 - 10.000 Manji per-call overhead, multithreading pomaze
DX12 (high-end CPU) ~10.000 - 20.000+ Moze i vise, ali engine overhead i dalje postoji
Konzole (PS5, Xbox Series X) ~5.000 - 15.000 Native API, optimizovani drajveri
Mobilne platforme ~500 - 2.000 Veoma ograniceni CPU-ji, OpenGL ES overhead

Za 30 FPS, mozete grube otprilike da udvostrucite ove brojke, posto imate duplo vise vremena po frejmu.

42.3.3 UE5 sa Nanite vs bez Nanite

Evo kljucne razlike koja je specificna za UE5:

Bez Nanite (tradicionalni pipeline):

Sa Nanite:

Prakticna implikacija: u tipicnom UE5 projektu, Nanite vam resava problem draw call-ova za staticnu geometriju (zgrade, teren, kamenje, namestaj, itd.), ali morate i dalje paziti na draw call-ove od non-Nanite objekata.

42.3.4 Kako videti broj draw call-ova u UE5

Najbrzi nacin da vidite koliko draw call-ova vasa scena ima:

Metoda 1: stat scenerendering

Unesite u konzolu (tilde taster ~ ili apostof taster, zavisno od tastature):

stat scenerendering

Ovo prikazuje, izmedju ostalog:

Mesh draw calls: 3247           // <-- Ovo vas zanima
Num Primitives: 1203847

Metoda 2: stat RHI

stat RHI

Prikazuje:

DrawPrimitive calls: 4521
Triangles drawn: 2847291

Metoda 3: Viewport statistika

U editoru, kliknite na strelicu pored Lit u viewportu i izaberite Stat > Advanced > Draw Calls. Ovo prekriva viewport sa brojem draw call-ova.

Metoda 4: RenderDoc ili PIX

Za najdetaljniju analizu, mozete koristiti eksterne alate kao sto su RenderDoc (besplatan, open-source) ili PIX (Microsoft-ov alat za DX12). Oni vam omogucavaju da vidite svaki pojedinacni draw call, sa svim state change-ovima, i da izmerite koliko svaki kosta.

RenderDoc capture jednog frejma:

Draw Call #1: SetShader(M_Wood), SetTexture(T_Wood_D), Draw(Cube_01)    -- 3.2 us
Draw Call #2: SetConstantBuffer(Transform_02), Draw(Cube_02)            -- 0.8 us  (isti shader!)
Draw Call #3: SetShader(M_Metal), SetTexture(T_Metal_D), Draw(Pipe_01)  -- 4.1 us  (shader change!)
Draw Call #4: SetConstantBuffer(Transform_04), Draw(Pipe_02)            -- 0.9 us  (isti shader)
...

Primetite u primeru iznad: draw call #2 je mnogo jeftiniji od #1 jer koristi isti shader -- nema skupe promene shader programa. Draw call #3 je skup jer menja shader. Ovo je razlog zasto je sortiranje po materijalu tako vazno -- ako grupisete sve objekte sa istim materijalom, smanjujete skupe state change-ove.

42.3.5 Pravilo palca

Ako vas zanima brza, pragmaticna smernica za UE5 projekte:

Ali ponavljam: uvek profajlirajte. Broj sam po sebi ne znaci nista bez konteksta. 5.000 draw call-ova sa istim materijalom moze biti brze od 2.000 draw call-ova sa 2.000 razlicitih materijala.


42.4 Instancing -- Isto nacrtaj mnogo puta, jeftino

Instancing je prva i jedna od najefektivnijih tehnika za smanjenje draw call-ova. Princip je jednostavan: ako imate isti mesh sa istim materijalom na vise mesta u sceni, umesto da izdajete zaseban draw call za svaki -- izdajete jedan draw call koji kaze GPU-u "nacrtaj ovaj mesh N puta, evo N transformacija (pozicija/rotacija/skala)".

42.4.1 Hardware Instancing -- Kako radi na GPU-u

Na najnizem nivou, hardware instancing koristi specijalne GPU feature-e koji omogucavaju efikasno ponavljanje iste geometrije.

Bez instancinga:

Draw Call 1: SetMesh(tree), SetMaterial(M_Tree), SetTransform(pos_1), Draw()
Draw Call 2: SetMesh(tree), SetMaterial(M_Tree), SetTransform(pos_2), Draw()
Draw Call 3: SetMesh(tree), SetMaterial(M_Tree), SetTransform(pos_3), Draw()
...
Draw Call 1000: SetMesh(tree), SetMaterial(M_Tree), SetTransform(pos_1000), Draw()

= 1000 draw calls, 1000 state setups

Sa instancingom:

Draw Call 1: SetMesh(tree), SetMaterial(M_Tree), 
             SetInstanceBuffer([pos_1, pos_2, ..., pos_1000]),
             DrawInstanced(1000)

= 1 draw call, 1 state setup, 1000 instanci

GPU prima jedan draw call i interno pokrece vertex shader 1.000 puta, svaki put sa drugom transformacijom iz instance buffer-a. Ovo je izuzetno efikasno jer:

42.4.2 Per-Instance Data

Hardware instancing ne mora da se ogranici samo na transformaciju (pozicija/rotacija/skala). Svaka instanca moze imati custom per-instance data -- dodatne podatke koji se razlikuju od instance do instance:

U UE5, per-instance custom data se postavlja preko CustomDataFloat na HISM/ISM komponentama, i cita u materijalu preko PerInstanceCustomData node-a.

Material Graph:

[PerInstanceCustomData Index: 0] ---> [Lerp A: Green, B: Brown] ---> [Base Color]

Svaka instanca moze da ima razlicitu boju, a opet je to JEDAN draw call!

42.4.3 ISM -- Instanced Static Mesh Component

U UE5, InstancedStaticMeshComponent (ISM) je komponenta koja vam omogucava da dodate mnogo instanci istog static mesh-a u jednu komponentu. Sve instance se crtaju u jednom (ili malom broju) draw call-ova.

Kako koristiti ISM u Blueprint-u:

  1. Dodajte InstancedStaticMeshComponent na vas Actor
  2. U Blueprint-u ili C++-u pozovite AddInstance(FTransform) za svaku instancu
  3. Sve instance dele isti mesh i materijal
  4. Engine ih crta u jednom draw call-u (ili malom broju, zavisno od internog batching-a)
// C++ primer
UInstancedStaticMeshComponent* ISM = CreateDefaultSubobject<UInstancedStaticMeshComponent>("ISM");
ISM->SetStaticMesh(TreeMesh);
ISM->SetMaterial(0, TreeMaterial);

for (int32 i = 0; i < 1000; i++)
{
    FTransform Transform;
    Transform.SetLocation(FVector(FMath::RandRange(-5000, 5000), 
                                   FMath::RandRange(-5000, 5000), 0));
    Transform.SetRotation(FRotator(0, FMath::RandRange(0, 360), 0).Quaternion());
    ISM->AddInstance(Transform);
}

// Rezultat: 1000 drveca, ~1 draw call

Prednosti ISM-a:

Ogranicenja ISM-a:

Ovo ogranicenje je razlog zasto postoji HISM.

42.4.4 HISM -- Hierarchical Instanced Static Mesh Component

HierarchicalInstancedStaticMeshComponent (HISM) je napredna verzija ISM-a koja resava kljucna ogranicenja:

  1. Hijerarhijski culling -- HISM organizuje instance u prostornu hijerarhiju (cluster tree). Engine moze da efikasno odbaci citave grupe instanci koje nisu vidljive, bez da proverava svaku pojedinacno.

  2. Per-instance LOD -- svaka instanca moze da koristi drugaciji LOD nivo na osnovu svoje udaljenosti od kamere. Daleka drveca koriste LOD2, bliza LOD1, najbliza LOD0.

  3. Per-instance occlusion -- instance koje su iza zida ili brda se ne crtaju.

ISM (Instanced Static Mesh):
    
    CPU: "GPU, nacrtaj svih 10.000 instanci"
    GPU: Crta svih 10.000, cak i one iza kamere
    Problem: Mnogo nepotrebnog posla za GPU

HISM (Hierarchical Instanced Static Mesh):

    CPU: Proverava cluster tree, odbacuje nevidljive grupe
    CPU: "GPU, nacrtaj ovih 1.200 vidljivih instanci (sa razlicitim LOD-ovima)"
    GPU: Crta samo 1.200, sa odgovarajucim LOD-ovima
    Rezultat: Mnogo manje posla za GPU, a i dalje malo draw call-ova

Kako HISM interno radi

HISM organizuje instance u stablo klastera (cluster tree). Ovo stablo se gradi automatski kada dodate instance:

                    [Root Node]
                   /           \
          [Cluster A]         [Cluster B]
          /        \          /        \
    [Leaf 1]  [Leaf 2]  [Leaf 3]  [Leaf 4]
    (250 inst) (250 inst) (250 inst) (250 inst)

Kada engine radi frustum culling, prolazi kroz ovo stablo odozgo nadole. Ako ceo Cluster A nije u vidnom polju kamere -- odmah odbacuje svih 500 instanci ispod njega, bez da proverava svaku pojedinacno. To je O(log N) kompleksnost umesto O(N).

Za LOD, HISM proverava udaljenost svake vidljive grupe od kamere i dodeljuje odgovarajuci LOD. Razliciti LOD nivoi se crtaju u zasebnim draw call-ovima (jer imaju razlicitu geometriju), ali broj draw call-ova je i dalje dramaticno manji od jednog po instanci.

42.4.5 Foliage System koristi HISM

Ako ste ikada koristili Foliage tool u UE5 (onaj kojim "slikate" travu, drveece, kamenje po terenu), vec ste koristili HISM a da to mozda niste ni znali!

Foliage system interno koristi HISM za svaki tip vegetacije koji dodate. Kada "naslikate" 50.000 busenja trave, engine ne pravi 50.000 zasebnih actor-a -- pravi jednu (ili nekoliko) HISM komponentu sa 50.000 instanci. To znaci umesto 50.000 draw call-ova, mozda imate 5-20, zavisno od broja LOD nivoa i vidljivosti.

Ovo je razlog zasto Foliage tool moze da podnese toliko vegetacije -- bez HISM-a, scena sa 100.000 busenja trave bi bila potpuno nerendabilna.

42.4.6 Kada instancing pomaze, a kada ne

Instancing je mocan, ali nije univerzalno resenje. Evo jasnih smernica:

Instancing POMAZE kada:

Instancing NE POMAZE (ili ne moze da se primeni) kada:

42.4.7 Prakticni primer: Pre i posle instancinga

Zamislite scenu: ulica u gradu, sa 200 ulicnih lampi. Svaka lampa je isti Static Mesh sa istim materijalom.

Bez instancinga:

200 zasebnih StaticMeshActor-a
= 200 draw call-ova (minimum, vise ako lampa ima vise material slot-ova)
+ 200 x frustum culling provera
+ 200 x occlusion culling provera
CPU: Tezak posao

Sa ISM (InstancedStaticMeshComponent):

1 Actor sa ISM komponentom, 200 instanci
= 1 draw call
- Nema per-instance culling (potencijalni problem ako su lampe rasute po velikom prostoru)
CPU: Minimalan posao

Sa HISM (HierarchicalInstancedStaticMeshComponent):

1 Actor sa HISM komponentom, 200 instanci
= 2-5 draw call-ova (zavisno od LOD nivoa i vidljivosti)
+ Efikasan hijerarhijski culling
+ Per-instance LOD
CPU: Minimalan posao, a i GPU radi samo ono sto mora

Za 200 ulicnih lampi, HISM je idealno resenje. Dobijate dramaticno smanjenje draw call-ova (od 200 na 2-5), a zadrzavate efikasan culling i LOD.


42.5 Mesh Merging / Actor Merging

Instancing radi samo kada imate vise kopija istog mesh-a sa istim materijalom. Ali sta ako imate mnogo razlicitih mesh-eva blizu jedan drugome? Tu na scenu stupa mesh merging (spajanje mesh-eva).

42.5.1 Koncept

Mesh merging uzima vise zasebnih static mesh-eva i kombinuje ih u jedan jedini mesh. Ovo je radikalan pristup: umesto da imate 50 razlicitih objekata koji generisu 50 draw call-ova, imate jedan objekat koji generise 1 draw call (ili onoliko draw call-ova koliko razlicitih materijala koristi).

Pre merge-a:

[Sto]   [Stolica_1]  [Stolica_2]  [Stolica_3]  [Vaza]  [Tanjir_1]  [Tanjir_2]
  |         |            |            |          |         |            |
  v         v            v            v          v         v            v
 DC1       DC2          DC3          DC4        DC5       DC6          DC7

= 7 draw call-ova (minimum)

Posle merge-a:

[Merged_DiningSet]
        |
        v
       DC1

= 1 draw call (ako svi dele isti materijal) ili 2-3 (ako imaju razlicite materijale)

42.5.2 Kako merge-ovati mesh-eve u UE5

UE5 nudi nekoliko nacina za mesh merging:

Metoda 1: Merge Actors tool

  1. Selektujte vise actor-a u sceni

  2. Idite na Window > Developer Tools > Merge Actors (ili pretrazite u meniju)

  3. Izaberite jednu od opcija:

    • Merge Meshes -- kombinuje mesh-eve u jedan, opciono kombinuje materijale
    • Create Proxy Mesh -- pravi pojednostavljenu verziju (proxy) grupe objekata, ukljucujuci automatsko generisanje LOD-a
    • Batch -- kombinuje u HISM komponentu umesto jednog mesh-a
  4. Podesite opcije (merge materijale u texture atlas, generisi lightmap UV-ove, itd.)

  5. Kliknite Merge

Metoda 2: HLOD (Hierarchical Level of Detail)

HLOD je automatiski sistem u UE5 koji merge-uje grupe objekata na velikom rastojanju. Kada se kamera udalji dovoljno, umesto da renderuje 100 zasebnih objekata, engine prikazuje jedan merged mesh sa pojednostavljenom geometrijom i kombinovanim materijalima.

HLOD je narocito koristan za otvorene svetove i koristi se zajedno sa World Partition sistemom (poglavlje 32).

Blizu kamere:     [Kuca] [Drvo] [Ograda] [Bunar] = 4+ draw call-a, pun detalj
                     |      |      |        |
                     v      v      v        v
                    Individualni mesh-evi, puni LOD

Daleko od kamere:  [HLOD_Proxy] = 1 draw call, smanjen detalj
                        |
                        v
                   Jedan merged mesh, uprosen materijal

42.5.3 Tradeoffs -- Sta dobijate i sta gubite

Mesh merging nije besplatan. Evo svih tradeoff-ova koje morate razumeti:

Prednosti:

Mane:

42.5.4 Kada merge-ovati, a kada ne

Merge-ujte:

Ne merge-ujte:

42.5.5 Merge vs Instancing: Kada sta koristiti

Kriterijum Instancing (ISM/HISM) Mesh Merging
Isti mesh, isti materijal Idealno Nepotrebno
Razliciti mesh-evi, isti materijal Ne moze Dobro
Razliciti mesh-evi, razliciti materijali Ne moze Ograniceno (merge materijale u atlas)
Objekti rasuti po velikom prostoru HISM (ima culling) Lose (gubi culling)
Objekti grupisani u malom prostoru ISM dovoljno Dobro
Potreba za pomeranjem delova ISM (moze update instance) Ne moze
Mnogo kopija (100+) Idealno Nepotrebno
Malo razlicitih objekata (5-20) Ne moze (razliciti mesh-evi) Idealno

42.6 Nanite kao resenje za Draw Calls

O Nanite-u smo detaljno govorili u poglavlju 30. Ovde cemo se fokusirati iskljucivo na to kako Nanite utice na draw call problematiku.

42.6.1 GPU-Driven Rendering: Paradigmatska promena

Tradicionalni rendering pipeline radi ovako:

Tradicionalni pipeline:

CPU:  [Traverse scene] -> [Cull objects] -> [Sort by material] -> [Submit draw calls]
                                                                        |
                                                                        v
GPU:  [Execute draw calls] -> [Vertex shader] -> [Rasterize] -> [Pixel shader]

CPU radi OGROMNU kolicinu posla. Za 10.000 objekata, CPU mora da
prodje kroz svih 10.000, proveri vidljivost, sortira, pripremi draw calls.

Nanite koristi GPU-driven rendering koji fundamentalno menja ovu dinamiku:

Nanite GPU-driven pipeline:

CPU:  [Upload instance list to GPU]  (minimalan posao)
                |
                v
GPU:  [GPU visibility test] -> [GPU LOD selection] -> [GPU clustering] -> [GPU rasterize]

CPU salje samo listu instanci (pozicije, materijali).
GPU SAM odlucuje sta je vidljivo, koji LOD da koristi, i kako da rastera.

Razlika u CPU overhead-u je ogromna. Umesto da CPU prolazi kroz svaki objekat i priprema draw call za njega, CPU samo kaze GPU-u "evo, ovo je lista svih Nanite objekata u sceni" -- i GPU preuzima sve ostalo.

42.6.2 Koliko draw call-ova generise Nanite?

Odgovor ce vas mozda iznenaditi: Nanite geometrija se ne renderuje kroz tradicionalne draw call-ove uopste!

Nanite koristi compute shader-e za visibility testing i rasterizaciju. Umesto DrawIndexed() poziva, Nanite koristi Dispatch() pozive za compute shader-e. Rezultat se renderuje u Visibility Buffer koji se zatim koristi u material evaluation fazi.

U praksi, Nanite moze da renderuje scenu sa milionima objekata uz svega nekoliko compute dispatch-ova sa CPU strane. Ovo je razlog zasto Nanite scene mogu imati ogromne kolicine geometrije bez CPU bottleneck-a.

Primer: Soba sa 500 komada namestaja

Bez Nanite:
  - 500+ draw call-ova (vise ako mesh-evi imaju multiple material slotove)
  - CPU: Frustum culling za svih 500, sortiranje, state setup za svaki
  - Vreme: 2-5 ms na CPU (zavisno od kompleksnosti)

Sa Nanite:
  - ~2-5 compute dispatch-a (nezavisno od broja objekata!)
  - CPU: Minimalan posao -- upload instance data
  - Vreme: ~0.1-0.3 ms na CPU
  
Usteda: 10-50x manje CPU vremena za rendering submission

42.6.3 Non-Nanite objekti: I dalje morate paziti

Nanite nije resenje za sve. Sledeci tipovi objekata i dalje prolaze kroz tradicionalni draw call pipeline:

Za ove objekte, sve tehnike koje smo diskutovali (instancing, merging, batching) su i dalje potpuno relevantne. U tipicnom projektu, cak i sa agresivnom upotrebom Nanite-a, mozete imati znacajan broj non-Nanite draw call-ova od VFX sistema, likova, transparentnih materijala, itd.

42.6.4 Prakticna strategija za UE5 projekte

Evo preporucene strategije za minimizaciju draw call-ova u UE5 projektu:

  1. Svu staticnu, opaque geometriju prebacite na Nanite -- zgrade, teren (Nanite podrzava Landscape od UE5.2+), kamenje, namestaj, dekoracije, industrijski elementi
  2. Za vegetaciju koristite HISM -- Foliage tool to vec radi automatski. Za Nanite-enabled vegetaciju, Nanite preuzima rendering. Za non-Nanite vegetaciju, HISM pruza efikasan instancing
  3. Transparentne objekte minimizujte -- svaki transparentni objekat je non-Nanite draw call. Gde mozete, koristite dithered opacity (masked) umesto prave transparencije
  4. Particle sisteme optimizujte -- koristite GPU particles (Niagara GPU Simulation) umesto CPU particles gde mozete. Merge-ujte slicne emittere
  5. Profajlirajte non-Nanite draw calls -- koristite stat scenerendering da vidite koliko draw call-ova dolazi od non-Nanite objekata, i ciljajte ta mesta za optimizaciju

42.7 Auto-Instancing u UE5

UE5 ima ugradjeni mehanizam koji automatski prepoznaje identicne mesh-eve sa identicnim materijalima i batch-uje ih u jednu instanced draw call -- bez ikakvog dodatnog rada sa vase strane. Ovo se zove auto-instancing.

42.7.1 Kako auto-instancing radi

Kada UE5 rendering sistem prolazi kroz listu objekata za renderovanje, on trazi grupe objekata koje imaju:

  1. Identican static mesh (ista geometrija)
  2. Identican materijal (isti parent materijal, iste teksture, isti parametri -- ili Material Instance istog parent-a sa istim parametrima koji nisu per-instance)
  3. Isti render state (isto osvetljenje, isti shadow settings, itd.)

Kada pronadje takvu grupu, umesto da izdaje zaseban draw call za svaki objekat, engine ih automatski batch-uje u jednu instanced draw.

42.7.2 Uslovi za auto-instancing

Auto-instancing nece raditi ako se objekti razlikuju u bilo cemu od sledeceg:

Auto-instancing primer:

Scena sa 100 StaticMeshActor-a:
  - 50 koristi Mesh_A sa Material_1           -> Batch 1: 50 instanci, 1 draw call
  - 30 koristi Mesh_A sa Material_2           -> Batch 2: 30 instanci, 1 draw call
  - 10 koristi Mesh_B sa Material_1           -> Batch 3: 10 instanci, 1 draw call
  - 10 koristi Mesh_B sa Material_3           -> Batch 4: 10 instanci, 1 draw call

Ukupno: 4 draw call-a umesto 100!

42.7.3 Kako verifikovati da auto-instancing radi

Postoji vise nacina da proverite da li auto-instancing radi u vasoj sceni:

Metoda 1: stat scenerendering

Ukljucite stat scenerendering i posmatrajte Mesh draw calls. Ako auto-instancing radi, ovaj broj ce biti znacajno manji od ukupnog broja mesh-eva u sceni.

Metoda 2: RenderDoc / PIX

Uhvatite jedan frejm u RenderDoc-u. Pronadjite draw call-ove i proverite InstanceCount parametar. Ako je veci od 1, znate da je instancing aktivan za taj draw call.

Metoda 3: Freeze rendering i brojanje

U editoru mozete koristiti FreezeRendering komandu da zamrznete rendering u trenutnom stanju, a zatim koristiti stat scenerendering da vidite brojeve.

Metoda 4: Konzolna promenljiva

r.MeshDrawCommands.DynamicInstancing 1    // Ukljuceno (default u UE5)
r.MeshDrawCommands.DynamicInstancing 0    // Iskljuceno (za poredjenje)

Probajte da iskljucite auto-instancing i uporedite broj draw call-ova sa i bez njega. Razlika ce vam jasno pokazati koliko auto-instancing pomaze.

42.7.4 Kako maksimizirati auto-instancing

Evo prakticnih saveta za maksimiziranje efikasnosti auto-instancinga:

  1. Koristite Material Instances umesto zasebnih materijala. 10 Material Instance-a istog parent materijala sa istim parametarskim vrednostima ce se auto-instancirati. 10 zasebnih kopija materijala -- nece, cak i ako izgledaju identicno.

  2. Standardizujte mesh-eve -- ako imate 5 varijacija jednog zida, a razlika je samo u teksturi, razmislite da koristite jedan mesh sa Material Instance parametrima za varijaciju, umesto 5 razlicitih mesh-eva.

  3. Ujednacite settings -- pobrinite se da svi objekti istog tipa imaju iste shadow settings, mobility, i rendering options. Jedna stolica sa Cast Shadow = true i druga sa Cast Shadow = false nece biti u istom batch-u.

  4. Minimizujte per-object uniqueness -- svaka razlika izmedju dva naizgled identicna objekta moze da razbije batch. Pregledajte properties i uklonite nepotrebne razlike.


42.8 Uticaj materijala na Draw Calls

Materijali imaju direktan i ogroman uticaj na broj draw call-ova. Ovo je tema koja se direktno nadovezuje na poglavlje 22 (UE5 Material System), ali ovde je posmatramo iskljucivo kroz prizmu draw call optimizacije.

42.8.1 Fundamentalno pravilo: Razlicit materijal = Potencijalni state change

Svaki put kada GPU treba da predje sa jednog materijala na drugi, to zahteva state change:

Sto vise jedinstvenih materijala imate u sceni, to vise state change-ova, to vise draw call-ova (jer se objekti sa razlicitim materijalima ne mogu batch-ovati zajedno).

Scena A: 1.000 objekata, 5 materijala
  - Prosecno 200 objekata po materijalu
  - Auto-instancing + sortiranje: ~5-20 draw call-ova
  - Efikasno!

Scena B: 1.000 objekata, 500 materijala
  - Prosecno 2 objekta po materijalu
  - Skoro nikakav batching: ~500-1.000 draw call-ova
  - Vrlo neefikasno!

42.8.2 Material Instances: Varijacije bez dodatnih draw call-ova (uslovno)

Material Instance je varijacija parent materijala. Kljucna stvar: Material Instances istog parent materijala koriste ISTI shader program. Razlika je samo u parametrima (boja, roughness, teksture, itd.).

Ovo znaci da prelazak sa jednog Material Instance-a na drugi istog parent-a zahteva manji state change nego prelazak izmedju dva potpuno razlicita materijala:

Razliciti parent materijali:
  M_Wood -> M_Metal
  = Promena shader programa (SKUPO) + promena tekstura + promena parametara

Material Instances istog parent-a:
  MI_Wood_Oak -> MI_Wood_Pine
  = Promena tekstura + promena parametara (isti shader ostaje!)

Ali vazno upozorenje: cak i Material Instances sa razlicitim teksturama ili parametrima ne mogu biti u istom instanced draw call-u (auto-instancing). Oni mogu da smanje cenu state change-a, ali ne eliminisu draw call. Za pravi instancing, instance moraju imati identican materijal sa identicnim parametrima.

Izuzetak: parametri implementirani kao per-instance custom data mogu da variraju izmedju instanci bez razbijanja batch-a. Ovo je tehnika koju smo opisali u sekciji 42.4.2.

42.8.3 Texture Atlasing -- Vise tekstura u jednoj

Texture atlas je tehnika gde se vise manjih tekstura kombinuje u jednu veliku teksturu. Umesto da imate 10 zasebnih diffuse tekstura za 10 razlicitih materijala, imate jednu veliku teksturu koja sadrzi sve, a UV koordinate svakog mesh-a su pomerene da referenciraju odgovarajuci deo atlas-a.

Bez atlasa:

Materijal 1: [Drvo_Diffuse.png] [Drvo_Normal.png]     -> Zaseban materijal
Materijal 2: [Kamen_Diffuse.png] [Kamen_Normal.png]   -> Zaseban materijal
Materijal 3: [Metal_Diffuse.png] [Metal_Normal.png]    -> Zaseban materijal

= 3 razlicita materijala, 3 draw call-a minimum

Sa atlasom:

Atlas materijal: [Atlas_Diffuse.png] [Atlas_Normal.png]   -> JEDAN materijal
                  Drvo je na UV (0,0)-(0.33,1)
                  Kamen je na UV (0.33,0)-(0.66,1)
                  Metal je na UV (0.66,0)-(1,1)

= 1 materijal, 1 draw call!

Prednosti atlasinga:

Mane atlasinga:

UE5 Merge Actors tool ima opciju za automatsko kreiranje texture atlasa pri merge-ovanju mesh-eva. Ovo je najlaksi nacin za atlasing u UE5.

42.8.4 Strategija za minimizaciju materijala

Evo proverene strategije koja balansira vizuelni kvalitet i efikasnost draw call-ova:

  1. Napravite "master" materijale -- kreirajte mali broj fleksibilnih parent materijala koji mogu da pokriju mnogo razlicitih izgleda. Na primer, jedan M_Surface_Master koji podrzava drvo, kamen, metal, plastiku -- zavisno od tekstura i parametara.

  2. Koristite Material Instances za sve varijacije -- umesto da pravite novi materijal za svaku novu povrsinu, napravite Material Instance vaseg master materijala i promenite parametre.

  3. Minimizujte broj aktivnih material slotova -- svaki material slot na mesh-u generise zaseban draw call. Mesh sa 5 material slotova = 5 draw call-ova. Gde mozete, kombinujte u jedan slot sa jednim atlasom.

  4. Per-instance custom data za varijacije boja -- umesto zasebnih Material Instance-a za crvene, plave i zelene stolice, koristite jedan materijal sa per-instance custom data koji kontrolise boju. Sve stolice ostaju u istom batch-u.

  5. Texture arrays za varijacije tekstura -- napredna tehnika gde koristite texture array umesto zasebnih tekstura. Per-instance custom data bira index u array-u. Ovo omogucava razlicite teksture bez razbijanja batch-a, ali zahteva da sve teksture imaju istu rezoluciju.

Primer "master" materijal pristupa:

Tradicionalno:
  M_Wood_Floor       -> Parent: M_Wood_Floor        (zaseban shader)
  M_Wood_Wall        -> Parent: M_Wood_Wall          (zaseban shader)
  M_Stone_Floor      -> Parent: M_Stone_Floor        (zaseban shader)
  M_Stone_Wall       -> Parent: M_Stone_Wall          (zaseban shader)
  = 4 razlicita shader programa, nikakav batching

Master materijal pristup:
  MI_Wood_Floor      -> Parent: M_Surface_Master     (ISTI shader)
  MI_Wood_Wall       -> Parent: M_Surface_Master     (ISTI shader)
  MI_Stone_Floor     -> Parent: M_Surface_Master     (ISTI shader)
  MI_Stone_Wall      -> Parent: M_Surface_Master     (ISTI shader)
  = 1 shader program, state change pri prelasku izmedju instance-a je jeftiniji

42.8.5 Material Slots na Mesh-u: Svaki slot = Draw Call

Ovo je detalj koji mnogi developeri previde: svaki material slot na mesh-u generise zaseban draw call. Mesh sa 3 material slota generise 3 draw call-a, bez obzira na to da li su svi slotovi isti materijal ili ne.

Mesh "Kuca":
  Slot 0: M_Zidovi (cigla)      -> Draw Call 1
  Slot 1: M_Krov (crep)         -> Draw Call 2  
  Slot 2: M_Prozori (staklo)    -> Draw Call 3
  Slot 3: M_Vrata (drvo)        -> Draw Call 4

= 4 draw call-a po instanci kuce!
  100 kuca = 400 draw call-ova (sa auto-instancingom moze biti manje,
  ali i dalje 4x vise nego da je mesh imao 1 slot)

Preporuka: u fazi modelovanja, minimizujte broj material slotova. Koristite texture atlas da svedete sve povrsine na jedan materijal gde je to vizuelno prihvatljivo. Za props (manje objekte), ciljajte 1-2 material slota. Za vece objekte (zgrade), 3-4 slota je razumno.


42.9 Prakticna optimizacija -- Od problema do resenja

Sada kada razumemo sve alate koji nam stoje na raspolaganju, hajde da prodjemo kroz prakticni workflow za identifikaciju i resavanje draw call problema.

42.9.1 Korak 1: Identifikacija bottleneck-a

Pre nego sto bilo sta optimizujete, morate da potvrdite da su draw call-ovi zaista vas problem. Ako je GPU bottleneck (GPU je na 100%, CPU ceka), smanjenje draw call-ova nece pomoci -- problem je u slozenosti shadera, rezoluciji, overdraw-u, itd.

Kako proveriti:

  1. Otvorite konzolu i unesite:

    stat unit
    

    Ovo prikazuje:

    Frame: 28.5 ms
    Game:   3.2 ms
    Draw:  18.7 ms    <-- Rendering thread (CPU strana renderinga)
    GPU:    8.1 ms    <-- GPU strana
    

    Ako je Draw znacajno veci od GPU, imate CPU-bound rendering -- draw call-ovi su verovatno problem.

  2. Unesite:

    stat scenerendering
    

    Pronadjite Mesh draw calls. Ako je broj veci od 3.000-5.000, imate potencijalnog krivca.

  3. Za detaljniju analizu:

    stat initviews
    

    Prikazuje vreme provedeno na visibility determination (frustum culling, occlusion culling). Ako je ovo vreme veliko, imate mnogo objekata za procesiranje.

42.9.2 Korak 2: Identifikacija izvora draw call-ova

Znate da imate previse draw call-ova. Sada morate da pronadjete odakle dolaze.

Tehnika 1: Wireframe mode

Prebacite viewport u Wireframe mode (Alt+3 u editoru ili konzolna komanda viewmode wireframe). Ovo vam daje vizuelni pregled koliko geometrije imate i gde. Gusti wireframe u odredjenoj oblasti = mnogo objekata = mnogo draw call-ova.

Tehnika 2: Selektivno sakrivanje

Selektujte grupe objekata i sakrijte ih (H taster u editoru). Posmatrajte stat scenerendering kako se menja. Kada sakrijete grupu koja dramaticno smanji draw call count -- pronasli ste krivca.

Tehnika 3: Scene Outliner analiza

Otvorite Scene Outliner i sortirajte po tipu. Pronadjite tipove actor-a koji imaju najvise instanci. Cest krivac su dekorativni objekti: kamencici, trava (ako nije Foliage), sitni props koji nisu instancirani.

Tehnika 4: Statistics window

Window > Statistics vam daje detaljan pregled scene, ukljucujuci broj primitiva, tekstura, materijala, i slicno.

42.9.3 Korak 3: Primena odgovarajuce strategije

Na osnovu onoga sto ste otkrili, primenite odgovarajucu strategiju:

Situacija Resenje Ocekivano smanjenje
Mnogo kopija istog mesh-a (vegetacija, kamenje, dekoracije) ISM/HISM ili Foliage tool 90-99% smanjenje za te objekte
Mnogo malih razlicitih objekata grupisanih na malom prostoru Mesh merging (Merge Actors) 70-90% smanjenje za tu grupu
Mnogo razlicitih materijala na slicnim objektima Master materijal + Material Instances, texture atlasing 30-60% smanjenje materijala
Staticna opaque geometrija na DX12/DX11 Konvertovanje u Nanite Skoro potpuno eliminise CPU overhead za te objekte
Daleki objekti sa mnogo detalja HLOD system Dramaticno smanjenje na daljinu
Mesh-evi sa previse material slotova Re-modelovanje sa manje slotova, atlas tekstura Linearno smanjenje

42.9.4 Primer iz prakse: Optimizacija gradske scene

Hajde da prodjemo kroz realistican primer. Imate scenu: mali gradski blok sa zgradama, ulicnim namestajem, vegetacijom.

Inicijalno stanje:

stat scenerendering:
  Mesh draw calls: 8.743
  
stat unit:
  Draw: 22.4 ms
  GPU:   9.1 ms

Jasno CPU-bound! Draw call overhead je problem.

Analiza izvora:

Detaljni pregled:
  - 24 zgrade, svaka sa 6-8 material slotova          = ~170 draw calls
  - 300 ulicnih lampi (isti mesh, isti materijal)      = 300 draw calls
  - 200 kanti za djubre (isti mesh, isti materijal)    = 200 draw calls  
  - 150 klupa (isti mesh, isti materijal)              = 150 draw calls
  - 2.000 malih dekorativnih kamencica                  = 2.000 draw calls
  - 500 zidnih dekoracija (razliciti mesh-evi)         = 500 draw calls
  - 5.000 busenja trave (nisu kroz Foliage tool)       = 5.000 draw calls
  - Ostalo (likovi, VFX, UI, itd.)                     = ~423 draw calls

Optimizacija, korak po korak:

Korak A: Prebacite travu na Foliage/HISM

5.000 zasebnih StaticMeshActor-a za travu -> 1 HISM komponenta
Pre: 5.000 draw calls
Posle: ~10-20 draw calls (zavisno od vidljivosti i LOD nivoa)
Usteda: ~4.980 draw calls

Korak B: Instancirajte ponavljajuce objekte

300 lampi -> 1 HISM (lampe)
200 kanti -> 1 HISM (kante)
150 klupa -> 1 HISM (klupe)
Pre: 650 draw calls
Posle: ~15 draw calls
Usteda: ~635 draw calls

Korak C: Merge-ujte male dekorativne kamencice u grupe

2.000 kamencica -> merge u 20 grupa po 100
Pre: 2.000 draw calls
Posle: ~20 draw calls
Usteda: ~1.980 draw calls

Korak D: Konvertujte zgrade u Nanite

24 zgrade sa 6-8 material slotova svaka
Pre: ~170 draw calls
Posle: Nanite GPU-driven, skoro 0 CPU draw call overhead
Usteda: ~170 draw calls

Korak E: Merge-ujte zidne dekoracije u grupe po zgradi

500 dekoracija -> merge u 24 grupe (po zgradi)
Pre: 500 draw calls
Posle: ~24-48 draw calls (zavisno od broja materijala)
Usteda: ~450 draw calls

Rezultat:

stat scenerendering:
  Mesh draw calls: 556          (smanjeno sa 8.743!)
  
stat unit:
  Draw: 4.8 ms                 (smanjeno sa 22.4 ms!)
  GPU:  8.9 ms                 (skoro nepromenjeno -- GPU nije bio bottleneck)

CPU rendering overhead smanjen za 78%!
Scena sada radi na 60+ FPS umesto ~35 FPS.

42.9.5 Iterativni pristup

Optimizacija draw call-ova je iterativan proces. Ne morate (i ne treba) da radite sve odjednom. Preporuceni redosled:

  1. Prvo profajlirajte -- potvrdite da su draw calls zaista bottleneck
  2. Identifikujte najvece krivce -- pronadjite oblasti/objekte koji generisu najvise draw call-ova
  3. Primenite najlakse resenje prvo -- prebacivanje na Nanite, HISM za ponavljajuce objekte
  4. Ponovo profajlirajte -- vidite koliko ste usteli
  5. Ako treba vise -- mesh merging, texture atlasing, smanjenje material slotova
  6. Ponovo profajlirajte -- svaki put merite uticaj
  7. Zaustavite se kada je dovoljno -- optimizacija je balans izmedju performansi i fleksibilnosti. Ne unistavajte svoju scenu radi poslednjiheg 2% unapredjenja

42.10 Napredne teme

42.10.1 Sortiranje draw call-ova po stanju (State Sorting)

UE5 interno sortira draw call-ove da bi minimizovao skupe state change-ove. Redosled sortiranja je otprilike:

  1. Po render pass-u -- base pass, shadow pass, itd. (ovo je najgrublji nivo)
  2. Po shader programu -- grupisanje objekata sa istim shader-om zajedno
  3. Po material parametrima -- unutar istog shader-a, grupisanje po teksturama i parametrima
  4. Po vertex buffer-u -- grupisanje objekata sa istim mesh-om zajedno
  5. Po dubini (front-to-back za opaque, back-to-front za transparentne)
Optimalno sortiran rendering:

  Shader A: [Obj1] [Obj5] [Obj12] [Obj7]    // Svi koriste shader A
  Shader B: [Obj2] [Obj8] [Obj14]            // Svi koriste shader B
  Shader C: [Obj3] [Obj9] [Obj4] [Obj15]    // Svi koriste shader C

  State changes: 2 (A->B, B->C)

Neoptimalno sortiran rendering:

  [Obj1:A] [Obj2:B] [Obj3:C] [Obj5:A] [Obj8:B] [Obj9:C] ...

  State changes: Svaki draw call menja shader! Uzasno neefikasno.

Vi kao developer ne morate rucno da sortirate -- UE5 to radi za vas. Ali mozete da pomognete engine-u tako sto koristite sto manje razlicitih materijala, sto daje engine-u manje state change-ova za sortiranje.

42.10.2 Draw Call Overhead u Shadow Pass-u

Ovde je cesta zamka: draw call-ovi se ne broje samo u main (base) rendering pass-u. Shadow pass takodje generise draw call-ove!

Za svako svetlo koje baca dinamicke senke, engine mora da renderuje shadow map -- a to znaci da prolazi kroz sve objekte koji bacaju senke i izdaje draw call-ove za svaki od njih, iz perspektive svetla.

Scena sa 1.000 objekata i 3 dinamicka svetla:

Base pass:     ~1.000 draw calls (za ono sto kamera vidi)
Shadow pass 1: ~800 draw calls   (za ono sto svetlo 1 vidi)
Shadow pass 2: ~600 draw calls   (za ono sto svetlo 2 vidi)
Shadow pass 3: ~700 draw calls   (za ono sto svetlo 3 vidi)

UKUPNO: ~3.100 draw calls!

Sama scena ima "samo" 1.000 objekata, ali shadow pass-ovi utrostrucuju 
broj draw call-ova.

Resenja za shadow pass draw call-ove:

42.10.3 Prepass / Early Z

UE5 koristi depth prepass (takodje poznat kao Early Z pass ili Z-prepass) koji renderuje dubinu (depth) scene pre main rendering pass-a. Ovo ima dva efekta na draw call-ove:

  1. Dodatan pass = dodatni draw call-ovi -- depth prepass generise svoje draw call-ove (mada su ovi jeftiniji jer ne koriste pune materijale)
  2. Smanjuje overdraw u main pass-u -- posto se dubina vec zna, pixel shader ne mora da se izvrsava za piksele koji su iza nesto drugog. Ovo smanjuje GPU rad u main pass-u.

Da li je net efekat pozitivan ili negativan zavisi od scene. Za scene sa mnogo overdraw-a (mnogo objekata koji se preklapaju), depth prepass je gotovo uvek koristan. Za scene sa malo preklapanja, moze da doda nepotreban overhead.

42.10.4 Batching transparentnih objekata

Transparentni objekti su posebna muka za draw call batching:

  1. Moraju se crtati back-to-front -- od najdaljeg ka najblizem, da bi blending radio ispravno
  2. Ne mogu se koristiti za depth prepass -- jer su prozirni, ne pisu u depth buffer
  3. Tesko se instanciraju -- sortiranje po dubini razbija instancing grupe

Rezultat: transparentni materijali su inherentno skupi u smislu draw call-ova. Svaki transparentni objekat je potencijalno zaseban draw call koji se ne moze optimizovati standardnim tehnikama.

Preporuke za transparentne objekte:

42.10.5 Dynamic vs Static Batching

UE5 razlikuje dva tipa batching-a:

Static batching -- objekti koji se nikada ne pomeraju mogu biti pre-batch-ovani. Engine zna unapred koji objekti mogu da budu zajedno i ne mora da ih sortira svaki frejm.

Dynamic batching -- objekti koji se pomeraju moraju biti ponovo sortirani i batch-ovani svaki frejm. Ovo zahteva vise CPU rada, ali omogucava batching za dinamicne objekte.

UE5 auto-instancing sistem radi kao hybrid -- koristi cached mesh draw commands za staticne objekte (koji se invalidiraju samo kada se objekat promeni) i dynamic instancing za objekte koji se menjaju.

Prakticna posledica: Movable objekti su nesto skupji od Static objekata u smislu batching overhead-a. Ako objekat nikada ne treba da se pomera, postavite njegov Mobility na Static.


42.11 Kompletna slika: Draw Call Lifecycle u UE5

Hajde da sumiramo ceo zivotni ciklus jednog draw call-a u UE5, od scene do piksela:

1. SCENE TRAVERSAL (CPU)
   |
   |- Engine prolazi kroz sve actor-e u sceni
   |- Proverava vidljivost (frustum culling)
   |- Proverava occlusion (occlusion culling)
   |- Filtrira na osnovu distance culling i relevancy
   |
   v
2. MESH DRAW COMMAND GENERATION (CPU)
   |
   |- Za svaki vidljivi mesh, engine generise MeshDrawCommand
   |- MeshDrawCommand sadrzi: mesh reference, materijal, transformaciju, 
   |  render state, shader bindings
   |- Commands se kesiraju za staticne objekte (ne regenerisu se svaki frejm)
   |
   v
3. SORTING & BATCHING (CPU)
   |
   |- Engine sortira MeshDrawCommands po state-u (shader, materijal, mesh)
   |- Auto-instancing: identican commands se merge-uju u instanced draws
   |- Sortiranje po dubini za opaque (front-to-back) i transparent (back-to-front)
   |
   v
4. COMMAND BUFFER FILLING (CPU)
   |
   |- Engine prevodi MeshDrawCommands u RHI (Rendering Hardware Interface) pozive
   |- RHI pozivi se prevode u DX12/Vulkan/Metal komande
   |- Komande se pakuju u command buffer (DX12 command list)
   |
   v
5. SUBMISSION TO GPU (CPU -> GPU)
   |
   |- Command buffer se submituje GPU-u za izvrsavanje
   |- Na DX12, ovo moze biti multithreaded (vise command list-a paralelno)
   |
   v
6. GPU EXECUTION (GPU)
   |
   |- GPU izvrsava komande redom
   |- Za svaki draw call: vertex fetch -> vertex shader -> rasterization -> pixel shader
   |- State changes izmedju draw call-ova kostaju vreme
   |
   v
7. FRAMEBUFFER OUTPUT (GPU)
   |
   |- Rezultat se upisuje u framebuffer
   |- Post-processing pass-ovi se primenjuju
   |- Finalna slika se prikazuje na ekranu

Za Nanite objekte, koraci 2-5 su dramaticno pojednostavljeni:

1. SCENE TRAVERSAL (CPU) -- Samo za non-Nanite objekte
   
2. NANITE INSTANCE UPLOAD (CPU)
   |- Minimalan posao: upload instance data (pozicije, materijali)
   
3. GPU-DRIVEN PIPELINE (GPU)
   |- GPU sam radi visibility, LOD, clustering, rasterization
   |- Nema tradicionalnih draw call-ova
   |- Rezultat ide u Visibility Buffer
   
4. MATERIAL EVALUATION (GPU)
   |- Na osnovu Visibility Buffer-a, evaluira materijale
   |- Minimalan number of pixel shader invocations (tacno 1 po pikselu)

42.12 Ceste greske i anti-patterni

Evo najcescih gresaka koje developeri prave u vezi sa draw call-ovima:

Greska 1: "Imam Nanite, ne trebam da brinem o draw calls"

Zasto je greska: Nanite pokriva samo staticnu, opaque geometriju. Sve ostalo (likovi, VFX, transparentni objekti, skeletal mesh-evi) i dalje koristi tradicionalni pipeline.

Resenje: Profajlirajte non-Nanite draw calls. Koristite stat scenerendering i obratite paznju na koliko draw call-ova dolazi od non-Nanite objekata.

Greska 2: Stotine zasebnih actor-a za vegetaciju umesto Foliage tool-a

Zasto je greska: Svaki zasebni actor = zaseban draw call. 10.000 travki kao zasebni actor-i = 10.000 draw call-ova. Isti rezultat sa Foliage tool-om = 10-30 draw call-ova.

Resenje: Uvek koristite Foliage tool za vegetaciju. Ako vegetaciju generisite proceduralno (Blueprint, PCG), koristite HISM komponentu.

Greska 3: Previise material slotova na mesh-ovima

Zasto je greska: Svaki material slot = zaseban draw call. Mesh sa 8 material slotova generise 8 draw call-ova po instanci.

Resenje: Saradjujte sa 3D artistima da minimizuju material slotove. Koristite texture atlase. Ciljajte 1-2 slota za male objekte, 3-4 za velike.

Greska 4: Svaki objekat ima unikatan materijal

Zasto je greska: Unistava mogucnost batching-a. 500 objekata sa 500 razlicitih materijala = 500 draw call-ova, nema sanse za auto-instancing ili state sort optimizaciju.

Resenje: Master materijal pristup. Material Instances. Per-instance custom data za varijacije. Texture atlasing.

Greska 5: Ignorisanje shadow pass draw call-ova

Zasto je greska: Developeri cesto gledaju samo base pass draw calls. Shadow pass moze da generise jednako ili vise draw call-ova, narocito sa vise dinamickih svetala.

Resenje: Koristite Virtual Shadow Maps. Ogranicite shadow casting distance. Iskljucite senke za male objekte. Koristite baked senke gde mozete.

Greska 6: Premature optimization -- merge-ovanje svega

Zasto je greska: Preterano merge-ovanje unistava culling efikasnost, povecava memoriju, i otezava iteraciju. Merge-ovanje objekata koji nisu blizu jedan drugom moze zapravo pogorsati performanse.

Resenje: Merge-ujte samo objekte koji su geografski blizu i koji se uvek vide zajedno. Uvek profajlirajte pre i posle merge-a da potvrdite da je rezultat pozitivan.


Rezime poglavlja

Ovo poglavlje je pokrilo jedan od najkritcnijih aspekata optimizacije u real-time renderovanju. Hajde da sumiramo kljucne takeaway-e:

  1. Draw call je komanda CPU-a ka GPU-u -- "nacrtaj ovaj mesh sa ovim materijalom". Sam call je jeftin; state changes koji ga okruzuju su skupi.

  2. Draw call overhead je CPU problem -- ako imate previse draw call-ova, CPU se gusi pripremajuci komande, a GPU ceka.

  3. DX12/Vulkan smanjuju per-call overhead, ali ne eliminisu ga. Nanite ga fundamentalno resava za staticnu geometriju prebacivanjem posla na GPU.

  4. Instancing (ISM/HISM) je najefektivnija tehnika za ponavljajuce objekte. HISM dodaje hijerarhijski culling i per-instance LOD. Foliage system koristi HISM interno.

  5. Mesh merging kombinuje razlicite mesh-eve u jedan, ali gubi individualni culling. Koristite ga za grupe malih objekata koji su blizu jedan drugom.

  6. Materijali direktno uticu na draw calls -- vise jedinstvenih materijala = vise state change-ova = losiji batching. Master materijali, Material Instances, i texture atlasing su kljucne strategije.

  7. Auto-instancing u UE5 automatski batch-uje identicne mesh+material kombinacije. Pomozite mu tako sto ujednacavate materijale i settings objekte.

  8. Nanite eliminise CPU draw call overhead za staticnu opaque geometriju. Non-Nanite objekti i dalje zahtevaju tradicionalne optimizacije.

  9. Uvek profajlirajte -- stat unit, stat scenerendering, stat RHI. Brojevi su jedini nacin da znate gde je problem i da li vasa optimizacija pomaze.


Kljucni pojmovi

Termin Objasnjenje
Draw Call Komanda CPU-a ka GPU-u za crtanje jednog mesh-a sa jednim materijalom
State Change Promena GPU stanja (shader, teksture, render state) izmedju draw call-ova; glavni izvor overhead-a
Command Buffer Buffer u koji CPU pakuje komande za GPU; GPU ga izvrsava asinhrono
Instancing Tehnika crtanja vise kopija istog mesh-a u jednom draw call-u
ISM Instanced Static Mesh -- UE5 komponenta za instanciranje bez hijerarhijskog culling-a
HISM Hierarchical Instanced Static Mesh -- ISM sa hijerarhijskim culling-om i per-instance LOD
Mesh Merging Kombinovanje vise razlicitih mesh-eva u jedan, smanjuje draw calls ali gubi individualni culling
Auto-Instancing UE5 mehanizam koji automatski batch-uje identicne mesh+material kombinacije
Texture Atlas Jedna velika tekstura koja sadrzi vise manjih tekstura; omogucava jedan materijal umesto mnogo
Material Instance Varijacija parent materijala sa promenjenim parametrima; koristi isti shader program
GPU-Driven Rendering Paradigma gde GPU sam odlucuje sta i kako da crta, minimizujuci CPU overhead (Nanite)
RHI Rendering Hardware Interface -- UE5 apstraktni sloj iznad DX11/DX12/Vulkan/Metal
Depth Prepass Dodatni rendering pass koji popunjava depth buffer pre main pass-a; smanjuje overdraw
HLOD Hierarchical Level of Detail -- automatsko merge-ovanje i pojednostavljivanje udaljenih objekata
Per-Instance Custom Data Podaci koji variraju po instanci (boja, parametri) bez razbijanja instancing batch-a
Shadow Pass Rendering pass za generisanje shadow mapa; generise dodatne draw call-ove
Batching Grupisanje vise draw call-ova u jedan ili manji broj, smanjujuci CPU overhead

Veze sa drugim poglavljima


Preporuceno citanje i resursi

  1. Epic Games dokumentacija: Rendering Optimization -- Zvanicna dokumentacija o rendering optimizaciji u UE5

  2. "GPU Gems 2, Chapter 3: Inside GPU" -- Detaljno objasnjenje GPU pipeline-a i zasto su state changes skupi

  3. "Real-Time Rendering, 4th Edition" (Akenine-Moller et al.) -- Poglavlje 18 pokriva pipeline optimization ukljucujuci batching i instancing

  4. GDC Talk: "Optimizing the Graphics Pipeline with Compute" (Wihlidal, 2016) -- GPU-driven rendering koncepti koji su inspirisali Nanite

  5. Epic Games: "Nanite - A Deep Dive" (Brian Karis, SIGGRAPH 2021) -- Tehnicko objasnjenje Nanite sistema

  6. Unreal Engine Profiling Guide: stat komande, Unreal Insights, i GPU profiling workflow

  7. RenderDoc dokumentacija: renderdoc.org -- Besplatan GPU debugger za analizu draw call-ova

  8. Microsoft PIX: GPU profiling alat za DirectX 12 -- izuzetno koristan za draw call analizu na Windows-u