Programabilnost grafičnega cevovoda
Na področju grafične strojne opreme v ozadju že nekaj let poteka tiha revolucija, ki so jo prezrli ne le navadni uporabniki računalnikov, temveč tudi večina igričarjev, ki izmed vseh komponent računalnika najpogosteje menjavajo prav grafične kartice. Ob splovitvi novih grafičnih procesorjev (GPU) je zanimanje usmerjeno predvsem v številke - višji takt procesorja in pomnilnika, večja količina pomnilnika za teksture, večje število cevovodov itd. Povečanje omenjenih zmogljivosti in arhitekturne izboljšave grafične strojne opreme vsekakor vplivajo na povečanje števila upodobljenih sličic na sekundo, to pa v osnovi najbolj zanima zagrete potrošnike zabavne elektronike. S stališča ponudnikov iger in programerjev nasploh pa gre v tem primeru za bolj kot ne pasivni napredek. Seveda lahko z več megahertzi in megabajti v istem času upodobimo geometrijsko bolj izpopolnjene modele navideznih okolij in dvignemo kakovost tekstur ter s tem posredno izboljšamo njihov videz. Vendar pa na interaktivno uporabo v računalniški grafiki in tudi širše čaka veliko zamisli in postopkov, ki so bili zaradi pomanjkanja ustrezne strojne podpore doslej omejeni le na teorijo ali kvečjemu akademske kroge.
Računalniška grafika je pri strojni podpori namreč že pred leti naletela na oviro, ki jo je premostila šele z razvojem programabilnih grafičnih procesorjev. Začetni namen teh je bil omogočiti programerjem izvedbo poljubnega modela senčenja, ki je bil dotlej omejen na fiksno funkcionalnost, kakršno sta ponujala DirectX in OpenGL. S pomočjo kratkih programčkov, imenovanih osenčevalniki ("shaders"), napisanih v posebnem zbirniku ali katerem od višjenivojskih osenčevalnih jezikov ("shading languages"), so lahko programerji po novem povsem spremenili vrstni red in način pretoka podatkov skozi grafični cevovod in strojno pospeševanje posplošili za doseganje skoraj poljubnih posebnih učinkov v računalniških igrah. Neposredna podpora programabilnosti in razvoj lastnih osenčevalnih jezikov sta bila seveda takoj vključena tako v DirectX kot v OpenGL in vse odtlej predstavljajo spremembe na področju programabilnosti bistvo in poglavitni vzrok za izdajanje novih specifikacij omenjenih knjižnic.
Prvi programabilni grafični procesor je bil nVidia GeForce 3 leta 2001, pravi razmah pa je razvoj doživel z nastankom DirectX 9.0 in ATI Radeon 9700 konec leta 2002. Danes je programabilnost GPU presegla začetne okvire uporabe za doseganje posebnih učinkov v računalniških igrah in postala gonilo razvoja novega področja, imenovanega splošnonamensko računanje z GPU (General Purpose Computation on GPU - GPGPU).
Oglejmo si dosedanji razvoj in trenutno stanje na področju osenčevalnih jezikov in GPGPU ter podajmo nekaj kratkih praktičnih zgledov.
Programabilnost grafičnega cevovoda
Z izrazom grafični cevovod ("graphics pipeline") označujemo zaporedje postopkov, ki pretvorijo programski opis navideznega okolja v sliko na monitorju. Značilno strukturo grafičnega cevovoda prikazuje slika 1.
Slika 1 - Tipična struktura grafičnega cevovoda
Danes prevladujeta dva namenska programska vmesnika (API) za programiranje in upodabljanje tridimenzionalnih svetov. Prvi je OpenGL, ki se je nekako ustalil v znanstveno-akademski sferi. Drugi je Microsoftov DirectX, ki je konec leta 2004 izšel v različici 9.0c, pravkar pa je bil z Visto in novimi nVidiami G80 splavljen tudi njegov naslednik - DirectX 10. Velika večina današnjih 3D iger v Oknih temelji prav na DirectX.
Ob namestitvi katere izmed novejših računalniških iger lahko uporabniki med nastavitvami opazijo nekaj novosti, povezanih z osenčevalniki ali, angleško, shaders. Gre za posebne programčke, ki jih gonilnik grafične kartice naloži v grafični procesor. Izvajanje teh programčkov je v novejših karticah nadomestilo fiksno ožičeno funkcionalnost strojne opreme, ki je se je izkazala kot nespremenljiva. Slika 2 prikazuje, katere faze grafičnega cevovoda je programabilna arhitektura nadomestila s programskimi moduli. Tako so bili programerji grafičnih programov v preteklosti omejeni s točno določenimi modeli transformacije, senčenja in programa tekstur, zdaj pa so te faze grafičnega cevovoda popolnoma programabilne, prilagodljive in, kar je najpomembneje, strojno pospešene. To je omogočilo, da sodobne igre s pomočjo osenčevalnikov v realnem času oziroma interaktivno ponudijo posebne učinke, kot so realistični proceduralni materiali (npr. les, marmor itd.), mehke sence brez ostrih robov, simulacija naravnih pojavov (ogenj, dim ...) in tako naprej.
Slika 2 - Osenčevalnik oglišč in osenčevalnik pik sta danes izvedena s programskimi moduli.
Prav tako je programabilna faza obdelave slikovnih pik omogočila uporabo tekstur v namene, ki si jih prej ne bi mogli niti zamisliti. Tako teksture niso več nujno samo barvne slike, ki jih lepimo na objekte zato, da povečamo njihovo realističnost, temveč lahko vanje shranimo poljubne vnaprej izračunane podatke.
Programabilnost grafičnih procesorjev pa ni prinesla koristi samo uporabnikom računalniške grafike. Grafični procesorji (GPU) imajo v primerjavi z običajnimi procesorji veliko prednost - so specializirani za vzporedno izvajanje operacij na veliki količini podatkov hkrati. V prvih fazah grafičnega cevovoda predstavljajo podatke oglišča specificirane geometrije, po rasterizaciji pa to postanejo slikovne pike. GPU sicer obdeluje vsako oglišče oz. piko neodvisno drugo od drugega, vendar jih lahko obdeluje več naenkrat, to pa imenujemo tokovna obdelava ("stream processing"). Zato lahko GPU uporabimo tudi za izvajanje vseh vrst algoritmov, ki jih je mogoče prevesti na tokovno obdelavo. Podatke, ki jih je treba obdelati, v tem primeru navadno serviramo v obliki teksture, ki jo prilepimo na preprost štirikotnik. Izris tega štirikotnika sproži njegov prenos skozi grafični cevovod, kjer lahko v programabilnih fazah obdelave oglišč in pik izvedemo vzporedno obliko ustreznega algoritma. Tako lahko, denimo, s pomočjo GPU vzporedno izvajamo sortiranje, fizikalne izračune v igrah, hitro Fourierovo transformacijo, obdelavo zvoka in slik oz. videa itd. Izvajanje postopkov, ki niso neposredno namenjeni upodabljanju vhodne geometrije, označujemo s skupnim izrazom splošnonamensko računanje na GPU ali, z angleško kratico, GPGPU.
Osenčevalniki in osenčevalni jeziki
Najprej si oglejmo klasično vlogo osenčevalnikov v programih z računalniško grafiko. Odvisno od tega, v kateri fazi grafičnega cevovoda nastopajo, ločimo dve vrsti osenčevalnikov. Osenčevalniki oglišč ("vertex shaders") delujejo na ogliščih geometrije, ki jo definira uporabniški program, in njim prirejenih atributih. Med atributi, ki jih lahko program priredi posameznemu oglišču, so denimo normala, barva in koordinate teksture, uporabnik pa lahko določi tudi lastne atribute. Vhodi v osenčevalnik oglišč so dveh vrst: spremenljivi ("varying") atributi so definirani za vsako oglišče geometrijskega gradnika posebej (npr. normala), nespremenljivi ("uniform") pa so podani za celoten gradnik ali objekt (npr. barva lastnosti materiala, transformacijske matrike).
Operacije fiksnega cevovoda, ki jih osenčevalnik oglišč nadomesti, so:
Izhodi iz osenčevalnika oglišč so torej preslikane koordinate oglišč z normalami in pripadajoča barva in koordinate tekstur (slika 3). Ti podatki potujejo naprej skozi grafični cevovod v postopek rasterizacije, kjer se izračunane vrednosti v ogliščih interpolirajo za vsako zaslonsko piko. Interpolirane vrednosti služijo kot vhod v osenčevalnik pik ("pixel shader, fragment shader"), ki izvaja zajemanje in uporabo tekstur. Ker lahko v obliki teksture zapišemo kakršnekoli predhodno izračunane podatke (tudi tiste, ki smo jih pridobili iz predhodnih upodabljanj), je z osenčevalniki pik mogoče izvesti skoraj poljubno obliko večprehodnega upodabljanja ("multi-pass rendering") ali obdelave slik. Izhod iz osenčevalnika pik (slika 4) je končna barva pike (skupaj s pripadajočo vrednostjo alfa, ki lahko ponazarja prosojnost ali pokritost pike) in njena globina (na podlagi katere se v nadaljnjih fazah cevovoda izvaja zakrivanje ploskev).
Operacije fiksnega cevovoda, ki jih nadomesti osenčevalnik oglišč.
Izhod iz osenčevalnika pik je končna barva pike in njena globina.
In kako lahko napišemo svoj osenčevalnik? V začetnih dneh programabilnih GPU ste morali za pisanje osenčevalnikov uporabiti poseben zbirni jezik, ki je bil odvisen od vrste grafičnega procesorja. Danes so za to opravilo mišljeni višjenivojski jeziki. Najstarejši med njimi je nVidiin in se imenuje Cg, kar je kratica za "C for graphics". Nalaganje in prevajanje programčkov v Cg je podprto od DirectX 8 naprej, v OpenGL pa je omogočeno prek posebnih razširitev (npr. ARB_vertex_program). Čeprav je bil Cg podprt s strani obeh glavnih izdelovalcev grafičnih procesorjev, sta specifikaciji DirectX 9 in OpenGL 2.0 definirali lastna osenčevalna jezika. To sta HLSL ("High Level Shader Language") pri DirectX in GLSL ("GL Shading Language") pri OpenGL in bosta v prihodnosti najverjetneje povsem nadomestila Cg. Osenčevalniki v novejših 3D igrah so pravzaprav že pisani v HLSL, zato ga bomo uporabili tudi v naših zgledih.
Programčki, spisani v HLSL ali GLSL, se prevajajo v strojni jezik GPU, ki se nenehno razvija. Vsaka nova različica ponuja več funkcij in manj omejitev glede dolžine programa, nabora registrov GPU itd. Pri DirectX oz. HLSL označujejo različice s t. i. modeli ("shader model"). Grafične kartice največkrat podpirajo model 3, s prihodom DirectX 10 in modela 4 pa je zdaj na voljo tudi strojna podpora zanju (GeForce 8800).
Oglejmo si preprost zgled učinka, ki ga lahko dosežemo s kombinacijo osenčevalnika oglišč in osenčevalnika pik. Program v DirectX izriše štirikotnik v ravnini z=0, ki je podan kot mreža 6464 točk s koordinatami (x,y) med 0 in 1. Za senčenje štirikotnika naloži izvirno kodo HLSL obeh osenčevalnikov iz datotek in ju prevede. V ta namen so bile specifikaciji DirectX dodane funkcije za delo z osenčevalniki, kot so npr. D3DXCompileShaderFromFile, CreateVertexShader in CreatePixelShader. Pred samim izrisom geometrije ustrezen osenčevalnik vklopimo z uporabo funkcij SetVertexShader oz. SetPixelShader.
Učinek, ki ga izriše naš primer.
Del učinka, ki je prikazan na sliki 5, dosežemo z naslednjim osenčevalnikom oglišč:
1: float4x4 mWorldViewProj;
2: struct OUTPUT
3: {
4: float4 position : POSITION;
5: float4 diffuse : COLOR0;
6: float2 texcoord : TEXCOORD0;
7: };
8: OUTPUT valovi( in float2 pos : POSITION )
9: {
10: OUTPUT output;
11: float s, c;
12: float2 p = 3.14f * pos - 1.57f;
13: float x = 0.2 * length( p );
14: sincos( x, s, c );
15: output.position = mul( float4( p.x, p.y, 0.1f * s, 1.0f ), mWorldViewProj );
16: output.diffuse = 0.5f - 0.5f * c;
17: output.texcoord = float2(pos.x, pos.y);
18: return output;
19: }
Funkcija valovi predstavlja telo osenčevalnika oglišč, ki na vhodu sprejme koordinati x in y oglišča, kot ju specificira program (vrstica 8). Izhod funkcije je struktura OUTPUT, ki vsebuje transformiran položaj oglišča, njegovo barvo in koordinate teksture (vrstice 2-7). Transformacijo oglišča izvedemo tako, da koordinate štirikotnika najprej raztegnemo na interval [-/2,/2] in ga pomnožimo z globalno transformacijsko matriko, zatem pa izvedemo sinusni odmik v smeri z (vrstici 12 in 15). Vrednosti transformacijske matrike program nastavi prek globalne spremenljivke mWorldViewProj. V vrsticah 16 in 17 oglišču določimo še barvo (dejansko odtenek sivine) in 2D koordinati teksture z intervala [0,1]. Te vrednosti se v nadaljevanju interpolirajo za vsako piko in postanejo vhod v naslednji osenčevalnik pik:
1: float4 color1;
2: float4 color2;
3: struct VS_OUTPUT
4: {
5: float4 diffuse : COLOR;
6: };
7: VS_OUTPUT sahovnica( in float2 tex : TEXCOORD0, in float4 col : COLOR0 )
8: {
9: VS_OUTPUT output;
10: float a = floor(10*tex.x);
11: float b = floor(10*tex.y);
12: float c = fmod(a+b,2);
13: output.diffuse = col*lerp(color1,color2,c);
14: return output;
15: }
Prek globalnih spremenljivk color1 in color2 lahko program nastavi barvi šahovnice. Telo osenčevalnika pik je v funkciji sahovnica, ki na izhodu vrne končno barvo pike. Ta je kombinacija interpolirane vhodne barve, določene že v osenčevalniku oglišč, in ene izmed obeh barv šahovnice (vrstica 13). Kateremu polju šahovnice pripada pika, ugotovimo v vrsticah 10-12, katerih preučitev prepuščamo bralcu.
Iz kode osenčevalnikov je razvidno, da HLSL podpira vektorske in matrične tipe podatkov (float4, float4x4), poseben pomen vhodnih in izhodnih parametrov pa je določen s ključnimi besedami (COLORn, POSITIONn, TEXCOORDn). Jezik vključuje veliko število uporabnih funkcij, od standardnih matematičnih (floor, sincos, fmod) do takih za delo z barvnimi komponentami ali vektorji (lerp, mul). Prikazani učinek je sicer razmeroma preprost in bi ga lahko dosegli tudi brez uporabe osenčevalnikov s klasičnimi teksturami. Razlika je v tem, da se v našem primeru tekstura s strojno pospešitvijo računa sproti, to pa odpira neomejene možnosti dinamičnega in interaktivnega preoblikovanja in senčenja geometrije. Dejansko gre za revolucionaren premik v zasnovi grafičnih procesorjev, ki bo omogočil nadaljnji napredek na področju fotorealističnega upodabljanja umetnih okolij.
Uporaba programabilnosti GPU na drugih področjih
Kot rečeno, pa ni le računalniška grafika tista, ki je veliko pridobila s programabilnostjo grafičnih procesorjev. Kot enega bolj izjemnih primerov GPGPU omenimo vzporedno izvajanje poizvedb po zbirkah podatkov s pomočjo osenčevalnikov. Denimo, da želimo poiskati vse zapise v zbirki podatkov, pri katerih je vrednost nekega atributa a enaka konstantni vrednosti D. Postopek izvedemo s simulacijo dveh prehodov upodabljanja. Vrednosti atributov iz zbirke podatkov najprej zapišemo v obliki teksture, ki jo priredimo štirikotniku. Zdaj izvedemo izris štirikotnika, tako da le-ta zapolni celotno površino okna, vrednosti teksture pa s pomočjo osenčevalnika pik prepišemo v pomožni pomnilnik za globinske koordinate ("depth buffer"). Pred drugim prehodom vklopimo test globine in primerjalno funkcijo nastavimo tako, da bodo test opravile le pike z globino, ki je enaka tisti že shranjeni v pomožnem pomnilniku. Zdaj ponovimo izris štirikotnika z zaslonsko globino D, pri čemer zapisujemo rezultate globinskega testa v drugem pomožnem pomnilniku. Pri tem dejanski izris na zaslonu sploh ni pomemben, zato lahko zapisovanje v slikovni pomnilnik izklopimo. Koordinate pik, ki so prestale test globine, označujejo iskane zapise v zbirki podatkov.
Slišati je nekoliko zapleteno, saj si je zapise zbirke podatkov težko predstavljati kot teksturo. Pa vendar se pojavljajo vedno nove oblike izkoriščanja vzporedne narave procesiranja pri GPU za pospeševanje operacij, ki nimajo niti najmanj skupnega z računalniško grafiko ali upodabljanjem sploh. Kogar zanima kaj več, se lahko obrne na spletno stran www.gpgpu.org, posvečeno posebej temu področju.
Prihodnost
Po pričakovanjih bo prihajajoča grafična strojna oprema vsebovala vedno širši nabor programabilnih komponent GPU ob vedno manjših omejitvah glede kompleksnosti osenčevalnikov. V skladu s tem lahko tudi pri osenčevalnih jezikih pričakujemo razširitev zdajšnje sintakse. Prav zanimivo pa bo spremljati vedno večji prenos operacij iz centralnega procesorja na GPU in, kdo ve, morda bo prav kmalu večina zahtevnih obdelav v računalniku potekala na grafični kartici. Nedavni odmevni nakup izdelovalca grafičnih procesorjev (ATI) s strani izdelovalca procesorjev (AMD) pa kaže na to, da se ti dve komponenti računalnika utegneta nekega dne združiti v eno samo.
Reference