Hlavní navigace

Grafické karty a grafické akcelerátory (23)

10. 8. 2005
Doba čtení: 11 minut

Sdílet

Dnešní pokračování seriálu o grafických kartách a grafických akcelerátorech je věnováno problémově orientovanému programovacímu jazyku Cg, který může být použit pro programování pixel shaderů i vertex shaderů na moderních grafických akcelerátorech s GPU. Při popisu jazyka Cg se zaměříme především na práci s uživatelsky vytvořenými funkcemi, práci s vektory a maticemi, využívání knihovních funkcí a tvorbu podmínek i programových smyček. Nebudou chybět ani ukázky pixel a vertex shaderů napsaných pomocí Cg.

Obsah

1. Praktické informace o jazyku Cg
2. Profily
3. Datové typy
4. Konstanty a globální proměnné
5. Pole, podmínky a smyčky
6. Funkce, operátory, standardní funkce
7. Ukázka vertex shaderů
8. Ukázka pixel shaderů
9. Obsah dalšího pokračování tohoto seriálu

1. Praktické informace o jazyku Cg

„(Cg and CineFx) The biggest revolution in graphics in 10 years, and the foundation for the next 10.“
Kurt Akeley (on Cg & CineFX)
Graphics Architect, NVIDIA
Co-founder of SGI
Designer of OpenGL

V předchozím pokračování tohoto seriálu jsme si již řekli základní informace o programovacím jazyku Cg. Zbývá dodat, jakým způsobem se programy zapsané v Cg dostanou z operační paměti počítače do paměti na grafické kartě. Stručně řečeno, vše funguje následujícím způsobem: program napsaný v jazyce Cg se nejprve přeloží do assembleru dané grafické karty. Posléze je přeložený program přes definované rozhraní (tím je OpenGL od verze 1.4, samozřejmě včetně verze 2.0 a DirectX od verze 8.0) poslán do paměti na grafické kartě a z této paměti je po nutné inicializaci a povolení daného programu přenesen přímo na GPU (to je možné díky tomu, že samotný shader může obsahovat poměrně malé množství instrukcí). Přeložených programů může být v paměti grafické karty uloženo víc, vždy je však aktivní pouze jeden pixel shader a jeden vertex shader. Na výše zmíněném postupu je dosti zásadní a potěšitelný fakt, že překladač je, alespoň v podání nVidie, open source (v původním významu těchto slov). Další varianty překladače dodávají přímo výrobci grafických procesorů, tj. firmy nVidia, ATI a další.

Základní i podrobnější informace o programovacím jazyku Cg lze získat například na stránkách:
developer.nvi­dia.com/page/cg_ma­in.html
developer.nvi­dia.com/page/do­cumentation.html

Na stránce www.blacksmith-studios.dk/pro­jects/download­s/bumpmapping_u­sing_cg.php se nachází velmi pěkný návod na vytvoření bump-mappingu (simulace zvlněného povrchu beze změny geometrie objektů) pomocí shaderů. Veškeré ukázky jsou vytvořeny právě pomocí jazyka Cg.

2. Profily

Jednou z důležitých vlastností jazyka Cg jsou takzvané profily. Vzhledem k tomu, že možnosti grafických akcelerátorů jsou obecně rozdílné, bylo zapotřebí nějakým předem známým způsobem specifikovat, které jazykové konstrukce a datové typy je možné pro daný grafický akcelerátor (resp. pro jeho GPU – grafický procesor) použít a které naopak nemají žádný smysl nebo je nelze z nějakých důvodů aplikovat. U datových typů float, half a fixel je také v každém profilu nastavena jejich bitová šířka a přesnost (tj. počet bitů za binární tečkou).

Profily se uplatňují již v době překladu zdrojového kódu z Cg do assembleru konkrétního GPU. Zajímavé je, že profily se uplatňují pouze pro přímo či nepřímo volané funkce (ve skutečnosti jsou všechny funkce vkládány do hlavní funkce – to odpovídá inline funkcím známým například z programovacího jazyka C++). To například znamená, že ve zdrojovém kódu mohou být uloženy i funkce (a uvnitř nich datové typy), které sice v daném profilu nejsou povolené, ale daná funkce není volána, takže se pravidla pro překlad daná profilem neporuší. Zdrojový kód tak může být pouze jeden pro více typů grafických akcelerátorů.

3. Datové typy

V předchozím pokračování tohoto seriálu jsem se zmínil o podporovaných datových typech, které je možné v programovacím jazyku Cg využít. Mezi skalární datové typy (skaláry) patří:

Tabulka 1: Skalární datové typy jazyka Cg
Název typu význam
int celočíselný datový typ, typicky uložený na 32 bitech, v některých profilech je nastaven jako typ float
float reálný datový typ; tento datový typ může (ale nutně nemusí) odpovídat 32 bitovým typům podle normy IEEE
half podobné typu float, ale obecně s menší přesností, v některých profilech je nastaven jako typ float
double podobné typu float, ale obecně s větší přesností, v některých profilech je nastaven jako typ float
fixed typ s pevnou řádovou tečkou, podle specifikace by měl mít přesnost alespoň deset bitů
bool booleovské hodnoty true/false

Podle v současnosti platné normy jazyka Cg musí být ve všech implementacích zaručeno, že existují datové typy float a half. Ve skutečnosti však mohou být float a half totožné, stejně tak i int může být reprezentován jako float (tím se mimochodem nezaručí plný 32bitový rozsah celočíselných hodnot). Stejně tak i typ fixed může být reprezentován stejným způsobem jako float – celá grafická pipeline může být postavena na jednom datovém typu. Podobná pravidla jsou ostatně platná i pro datové typy programovacího jazyka C – viz podstatné rozdíly mezi šestnáctibitovými, třicetidvoubitovými a šedesátičtyřbi­tovými překladači.

Kromě skalárních datových typů je možné používat i strukturované datové typy, jak je naznačeno ve druhé tabulce:

Tabulka 2: Strukturované datové typy
Název typu význam
sampler obecný texturovací objekt
sampler1D texturovací objekt 1D textur
sampler2D texturovací objekt 2D textur
sampler3D texturovací objekt 3D textur
array pole skalárních hodnot (pomocí typedef se z něj odvozují další datové typy)
struct struktura složená obecně z více rozdílných položek

Pomocí polí, která jsou modifikována klíčovým slovem packed (úsporné uložení), se odvozují další datové typy – vektory a matice. Opět musí být zaručeno, že z každého číselného datového typu je možné vytvořit matici (až do velikosti 4×4 prvky) či vektor (až do délky osmi prvků).

Tabulka 3: Vektory a matice
Název typu význam
float1 vektor o jednom prvku
float2 vektor o dvou prvcích
float3 vektor o třech prvcích
float4 vektor o čtyřech prvcích
float2×1 matice velikosti 2×1 prvek
float2×2 matice velikosti 2×2 prvky
float3×3 matice velikosti 3×3 prvky
float4×4 matice velikosti 4×4 prvky

4. Konstanty a globální proměnné

Při zápisu konstant je zapotřebí určit, jakého typu daná konstanta je. Podobně jako v C-čku se i zde používá písmene, které se zapíše za hodnotu konstanty (jedná se o takzvaný suffix). Je možné použít následujících suffixů:

Tabulka 4: Suffixy pro konstanty
Suffix význam
f datový typ float – odpovídá C-čku
d datový typ double
h datový typ half
x datový typ fixed

Pro celočíselné konstanty se suffix nemusí používat, což je rozdíl oproti C-čku, kde se odlišovaly typy se znaménkem a bez znaménka.

Globální proměnné se vytvářejí stejným způsobem jako v C-čku. Jediný rozdíl je v tom, že hodnotu globální proměnné je možné nastavit pomocí vestavěných funkcí i pomocí výrazů používajících tyto funkce. Je však zapotřebí dát pozor na to, že se NESMÍ používat neinicializované proměnné, protože mají nedefinovanou hodnotu – oproti C-čku se do těchto proměnných implicitně nedosazují nuly, což by bylo výpočetně náročné (ubíralo by to vzácné takty dostupné pro daný pixel či vertex shader).

5. Pole, podmínky a smyčky

Pole se indexuje opět podobným způsobem jako v C-čku, tj. zápisem indexu do lomených závorek. Existuje i alternativní zápis, kdy se za jméno pole přidá tečka a napíše se kombinace následujících písmen: x, y, z, w, a, r, g, b. Pokud se napíše jedno písmeno, znamená to výběr jednoho prvku pole, více písmen se používá pro výběr více prvků pole.

Zápis podmínky plně odpovídá C-čkovskému zápisu. Povolen je tedy „plný“ příkaz typu if s větví else i „poloviční“ příkaz bez této větve. Podmínky nemusí být v některých profilech podporovány. Pokud podporovány jsou, musí být výraz použitý za příkazem if vytvořen tak, aby vracel hodnotu typu bool. Vyhodnocení jiných datových typů je neefektivní a k tomu platformově závislé – viz typ float, který obecně neodpovídá normě IEEE.

Pro zápis smyček jsou k dispozici příkazy for a while. Jejich zápis a význam do značné míry odpovídá programovacímu jazyku C. Je však potřeba mít na paměti, že v mnoha profilech (viz druhou kapitolu) není provádění smyček povoleno. Pokud jsou smyčky v daném profilu povoleny, musí být u nich uvedená podmínka zkonstruována tak, aby vracela hodnotu typu bool, nikoli int či float.

6. Funkce, operátory, standardní funkce

Funkce se volají naprosto stejným způsobem jako v C-čku, tj. zápisem jména funkce, otevírací závorky, případných parametrů a uzavírací závorky. Funkce mohou jako své parametry akceptovat i pole (vektory, matice) a tyto datové typy dokonce vracet. Při kompilaci se veškeré funkce vkládají do hlavní funkce – ve skutečnosti se tedy volání funkcí, jež je implementačně náročné (skoky, zásobník, návratové hodnoty), neprovádí.

Programovací jazyk Cg obsahuje značné množství operátorů. Většina běžných operátorů je známých z jiných programovacích jazyků, zde však díky existenci vektorů a matic nastávají některé odlišnosti. Pokud je operátor použit na vektor či matici, je aplikován po složkách – tím je možné například sečíst matice či vektory. Kromě toho existují i standardní funkce, které nahrazují maticové a vektorové operátory. Většina těchto funkcí je značným způsobem optimalizována, proto je jejich provádění rychlé a efektivní. Mezi tyto standardní funkce patří:

Tabulka 5: Standardní funkce jazyka Cg
Zápis funkce Význam
mul násobení vektorů a matic
dot skalární součin vektorů
cross vektorový součin třísložkových vektorů
determinant determinant matice
lerp lineární slučovací (blending) funkce
lit výpočet osvětlení dle Phongova osvětlovacího modelu
refract výpočet lomu paprsku na hranici dvou prostředí
Tabulka 6: Operátory jazyka Cg
Zápis operátoru Význam
&& booleovský operátor and
|| booleovský operátor or
! booleovský operátor not
== relační operátor pro rovnost
!= relační operátor pro nerovnost
< relační operátor „menší než“
<= relační operátor „menší nebo rovno“
> relační operátor „větší než“
>= relační operátor „větší nebo rovno“
+ přetížený operátor pro sčítání
 – přetížený operátor pro odečítání
* přetížený operátor pro násobení
/ přetížený operátor pro dělení
% přetížený operátor pro dělení modulo
++ inkrementace
 – dekrementace
?: ternární operátor pracující dle C-čka

7. Ukázka vertex shaderů

Při práci s programovacím jazykem Cg je zapotřebí udělat cílené změny i v uživatelském programu, který vertex a pixel shadery využívá. Úprava spočívá především v přilinkování podpůrných knihoven, inicializací prostředí Cg, nahrání vertex a pixel shaderů (kvůli nim ostatně vše podstupujeme) a následně v předání parametrů těmto shaderům. Následuje kód, který využívá Cg, OpenGL a dále nadstavbovou knihovnu GLUT (viz seriál Tvorba přenositelných grafických aplikací využívajících knihovnu GLUT). Program je určen pro MSVC, po mírných úpravách by však měl být přeložitelný i v jiných překladačích. Kromě toho je zapotřebí mít k dispozici i Cg toolkit, který je dostupný na stránce developer.nvi­dia.com/view.as­p?IO=cg_toolkit. Tento kód vznikl úpravou příkladu uvedeného v dokumentu Hello, Cg! od Alexe D'Angela.

// přilinkování knihoven pro Cg - následující řádky
// bude potřeba pro různé překladače upravit, nebo
// knihovny specifikovat při vlastním překladu
// (resp. linku)
#ifdef _MSC_VER
#pragma comment( lib, "cg.lib" )
#pragma comment( lib, "cgGL.lib" )
#endif

// načtení hlavičkových souborů - pozor na korektní
// nastavení cesty k těmto souborům
#include <GL/glut.h>
#include <Cg/cg.h>

#include <Cg/cgGL.h>

// proměnné, do kterých se bude shader nahrávat
static CGcontext Context = NULL;
static CGprogram VertexProgram = NULL;

// parametry pro shader
static CGparameter KdParam = NULL;
static CGparameter ModelViewProjParam = NULL;
static CGparameter VertexColorParam = NULL;

// nastavení profilu vytvářeného shaderu
static CGprofile VertexProfile = CG_PROFILE_VP20;

// inicializace Cg a shaderu
void onInit(void)
{
    // inicializace Cg
    // (je nutné provést až po inicializaci grafické knihovny OpenGL)
    Context = cgCreateContext();

    // načtení vertex shaderu ze specifikovaného souboru
    VertexProgram = cgCreateProgramFromFile(Context,
                                            CG_SOURCE,
                                            "vertexShader.cg",
                                            VertexProfile,
                                            NULL,
                                            NULL);
    cgGLLoadProgram(VertexProgram);

    // navázání parametrů pro shader
    KdParam = cgGetNamedParameter(VertexProgram, "Kd");
    ModelViewProjParam = cgGetNamedParameter(VertexProgram, "ModelViewProj");
    VertexColorParam = cgGetNamedParameter(VertexProgram, "IN.VertexColor");
}

// při ukončení aplikace se shader i kontext Cg zruší
void onDestroy(void)
{
    cgDestroyProgram(VertexProgram);
    cgDestroyContext(Context);
}

void onRedraw(void)
{
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
    // použití prvního shaderu pro vykreslování
    cgGLBindProgram(program);
    cgGLEnableProfile(profile);
    // ukázka nastavení parametrů (argumentů) pro shader
    cgGLSetParameter4f(KdParam, 1.0, 1.0, 0.0, 1.0);
    drawing_code(); // zde se něco vykreslí (v originále to byla krychle)
    cgGLDisableProfile(profile);

    // použití druhého shaderu pro vykreslování
    cgGLBindProgram(program2);
    cgGLEnableProfile(profile2);
    drawing_code2(); // zde se opět něco vykresluje
    cgGLDisableProfile(profile2);

    // prohození předního a zadního barvového bufferu
    glutSwapBuffers();
}

// inicializace nadstavbové knihovny GLUT
void initGlut(int *argc, char *argv[])
{
    glutInit(argc, argv);

    // nastavení framebufferu
    glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGB | GLUT_DEPTH);
    glutCreateWindow(argv[0]);
    glutDisplayFunc(onRedraw);
    glEnable(GL_DEPTH_TEST);

    // nastavení transformačních matic
    glMatrixMode(GL_PROJECTION);
    gluPerspective(40.0f, 1.0f, 1.0f, 10.0f);
    glMatrixMode(GL_MODELVIEW);
    gluLookAt(0.0f, 0.0f, 5.0f,
              0.0f, 0.0f, 0.0f,
              0.0f, 1.0f, 0.0f);
}

// hlavní funkce programu
int main(int argc, char *argv[])
{
    // inicializace knihovny GLUT
    initGlut(&argc, argc);

    // inicializace Cg a shaderu
    onInit();
    glutMainLoop();

    // zrušení kontextu Cg i shaderu
    onDestroy();
    return 0;
} 

Samotný program pro vertex shader vypadá následovně:

struct appdata
{
    float4 position : POSITION;
    float3 normal : NORMAL;
    float3 color : DIFFUSE;
    float3 VertexColor : SPECULAR;
};

struct vfconn
{
    float4 HPOS : POSITION;
    float4 COL0 : COLOR0;
};

vfconn main(appdata IN,
            uniform float4 Kd,
            uniform float4x4 ModelViewProj)
{
    vfconn OUT;
    // použití vestavěné funkce mul
    OUT.HPOS = mul(ModelViewProj, IN.position);
    // operátor, který zde pracuje v režimu SIMD
    OUT.COL0.xyz = Kd.xyz * IN.VertexColor.xyz;
    // nastavení poslední složky vektoru
    OUT.COL0.w = 1.0;
    return OUT;
} // main 

Přeložený vertex shader může v assembleru vybraného GPU vypadat například následovně (porovnejte si přehlednost zápisu s jazykem Cg):

DP3 R0, c[11].xyzx, c[11].xyzx;
RSQ R0, R0.x;
MUL R0, R0.x, c[11].xyzx;
MOV R1, c[3];
MUL R1, R1.x, c[0].xyzx;
DP3 R2, R1.xyzx, R1.xyzx;
RSQ R2, R2.x;
MUL R1, R2.x, R1.xyzx;
ADD R2, R0.xyzx, R1.xyzx;
DP3 R3, R2.xyzx, R2.xyzx;
RSQ R3, R3.x;
MUL R2, R3.x, R2.xyzx;
DP3 R2, R1.xyzx, R2.xyzx;
MAX R2, c[3].z, R2.x;
MOV R2.z, c[3].y;
MOV R2.w, c[3].y;
LIT R2, R2; 

8. Ukázka pixel shaderů

Pixel shader v následující ukázce je velmi jednoduchý. Pro zadaný vektor světla a normálový vektor spočítá intenzitu odraženého světla. Všimněte si použití skalárního součinu, který vlastně znamená, že odražené světlo má největší intenzitu, pokud dopadá kolmo na povrch, a nejmenší intenzitu v případě, že je s povrchem rovnoběžné. Toto chování odpovídá difúznímu světlu v Phongově osvětlovacím modelu.

CS24_early

float4 PS(float3 Light: TEXCOORD0, float3 Norm : TEXCOORD1) : COLOR
float4 result=Dintensity*Dcolour*(dot(Norm,Light));
return Aintensity*Acolour+result; 

9. Obsah dalšího pokračování tohoto seriálu

V příštím pokračování seriálu o grafických kartách a grafických akcelerátorech se budeme zabývat způsobem zobrazení třírozměrné grafiky pomocí výkonných grafických subsystémů pracovních stanic.

Byl pro vás článek přínosný?

Autor článku

Vystudoval VUT FIT a v současné době pracuje na projektech vytvářených v jazycích Python a Go.