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.nvidia.com/page/cg_main.html
developer.nvidia.com/page/documentation.html
Na stránce www.blacksmith-studios.dk/projects/downloads/bumpmapping_using_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ří:
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řbitový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:
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ů).
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ů:
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ří:
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í |
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.nvidia.com/view.asp?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.
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.