OpenGL - GLSL
Tak už máte za sebou několik kapitol o matematice, další kapitoly o různých knihovnách s OpenGL souvisejících, ale stále žádný pořádný OpenGL program. Pořád vás to neodradilo? Super. V této kapitole se dočkáte zasloužené odměny a konečně začnete programovat. Podíváme se na dvě třetiny z grafické pipeline, které jsou potřebné pro vykreslení obrazu s OpenGL. Na shadery.
O GLSL
Z grafické pipeline vás budou zajímat hlavě tyto 3 kroky: nahrání vertex dat do bufferů, vertex shader a fragment shader. S výkladem začnu od prostředka, tedy vertex a fragment shaderem.
Oba tyto kroky v pipeline se programují pomocí GLSL (OpenGL Shader Language).
Připomínám, že vertex shader má za úkol spočítat výslednou pozici vertexu (vrcholu trojúhelníka). Volá se pro každý vertex (vrchol).
Fragment shader se volá pro každý fragment, tedy pro každý čtvereček uvnitř trojůhelníka, na které je trojúhelník rozdělen. Jeho hlavním úkolem je zpočítat barvu.
Struktura jazyka GLSL
GLSL je jazyk trochu podobný céčku. Takto bude vypadat vertex shader, který použiji v prvním příkladu:
Vstupním bodem je funkce main()
. V příkladu dělá jen dvě věci.
Spočítá výslednou pozici vertexu a uloží ji do globální proměnné gl_Position.
Dále vezme hodnotu proměnné aVertexColor a předá jí do proměnné vColor. Tato proměnná je označena jako varying, což znamená, že se stane vstupní hodnotou pro proměnnou ve fragment shaderu stejného jména a typu.
GLSL program pro fragment shader vypadá takto jednoduše:
Pouze předá barvu vColor do globální proměnné gl_FragColor, která se použije jako barva fragmentu.
Do proměnné typu varying se dosadí hodnota interpolovaná z vrcholů trojúhelníka.
Na obrázku můžete vidět trojůhelník, který měl pro levý vrchol barvu vColor modrou, pro pravý vrchol zelenou a pro horní vrchol červenou. Barvy uvnitř trojúhelníku jsou interpolované.
Do fragment shaderu z vertex shaderu můžete přenášet leccos. Třeba i pozici vrcholu. Interpolovaná hodnota pozice vrcholu bude v fragment shaderu odpovídat pozici shaderu.
Datové typy v GLSL
bool, int, uint, float, double | Znáte z jazyka C. Typ uint odpovídá unsigned int. |
---|---|
vec2, vec3, vec4 | Struktury, které obsahují atributy (x,y), (x,y,z), (x,y,z,w) typu float |
ivec2, ivec3, ivec4 | Struktury, které mají atributy (x,y, …) typu int |
uvec2, uvec3, uvec4 | Sruktury, které mají atributy typu unsigned int |
mat2, mat3, mat4 | Čtvercové matice (s prvky typu float) |
sampler2D | 2D textura. |
S datovými typy je v GLSL větší sranda než v C. Vektor vec4
má 4 atributy (typu float). Jsou pojmenované x,y,z,w.
Ale také r,g,b,a.
Takže např. vColor.w obsahuje totéž, co vColor.a. Můžete si vybrat jméno, které chcete. Pro barvy je vhodnější druhé označení, pro souřadnice to první.
Další legrační způsob zápisu je takový, který vám umožní atributy různě zkombinovat a vytvořit
tak nový vektor. Např: vColor.xyz
je proměnná typu
vec3
, která obsahuje první 3 atributy z vColor.
Můžete zapsat i vColor.xx
, což je vec2
, obsahující
v prvním i druhém argumentu x-ovou hodnotu z vColor. Hustý, co?
Nový vektor můžete vytvořit třeba takto: vec3 souradnice = vec3(1.0, 0.0, 1.0);
.
Nebo třeba takto: vec3 jinaSouradnice = vec3(souradnice.xy, 0.0);
.
Ten druhý způsob můžete vidět v příkladu vertex shaderu, kde vytvářím z proměnné aVertexPosition typu vec3 vektor typu vec4. (Protože matice jsou typu 4x4, tudíž je mohu násobit jen vektorem vec4.)
A tím se dostáváme k další zvlášnosti, a to je násobení struktur. V GLSL můžete normálně vynásobit matice, matice s vektory, vektory se scaláry atd. Cokoliv, pro co je definována matematická operace, je možné (i sčítání matic, například).
V jazyku C něco takového možné není, ale v jazyku C++ ano. Díky tzv. přetěžování operátorů můžete definovat, co je výsledkem nějaké operace (nejen) nad strukturami. V GLSL to sami definovat nemůžete, ale pro matice a vektory už to definováno je.
Datový typ sampler2D se používá pro uložení textur (obrázků). K jeho použití se vrátím v kapitole o používání textur :-).
Kvalifikátory proměnných
Proměnné mohou mít krom svého typu ještě vzv. kvalifikátory. Pro globální proměnné (definované vně jakékoliv funkce) existují tyto základní kvalifikátory:
attribute | Používá se jako vstupní proměnná ve vertex shaderu (ve fragment shaderu toto použít nemůžete). Při každém volání programu ve vertex shaderu se naplní další hodnotou v řadě (souřadnicemi vertexu, jeho barvou atp.). Jak se určí, co se tu má dosazovat, to je otázka prvního kroku v pipeline, který popíši v další kapitole. (Při vykreslení trojúhelníka se zavolá vertex shader 3x (pro každý vrchol), tudíž za attribute proměnné se 3x dosadí nějaká hodnota.) |
---|---|
uniform | Používá se jako vstupní proměnná pro vertex nebo i fragment shader. Tato hodnota se během provádění pipeline nemění. (Dosadí se jen 1x před začátkem provádění pipeline.) |
varying | Ve vertex shaderu je to výstupní proměnná, která se předává do fragment shaderu. Ve fragment shadederu je to tedy vstupní proměnná. Proměnná musí existovat v obou shaderech, mít stejný datový typ a stejné jméno. Do každého fragment shaderu se dostane hodnota interpolovaná z tří vertexů, viz trojúhelník výše. |
Teď už by vám měly být programy pro vertex a fragment shader jasné.
Ve vertex shaderu si nejdříve definuji vstupní proměnné pro souřadnice a barvy vrcholu
(které jsou pro každý vertex jiné).
Pak si definuji proměnnou pro matici kombinující modelu a pohled (model a view)
uMVMatrix a proměnnou matici perspektivy uPMatrix.
Tyto matice jsou stejné pro každý vrchol, proto jsou uniform
.
Pak vypočítám transformovanou polohu vertexu jako násobek uMVMatrix, uPMatrix a původních suřadnic vertexu (aVertexPosition). Výsledek uložím do globální proměnné gl_Position, ve které OpenGL výsledek očekává.
Dále ještě předám barvu vrcholu do fragment shaderu skrze proměnou vColor. Fragment shader jen vezme interpolovanou barvu ze tří vertexů a uloží ji do globální proměnné gl_FragColor, která určuje výslednou barvu fragmentu.
Tak to alespoň funguje ve starší verzích OpenGL, nebo např. v OpenGL ES, na kterém je postaveno WebGL. V nové verzi OpenGL se věci trochu změnili. Podívejte se na modernější verzi vertex a fragment shaderů:
Na prvním řádku je makro, které říká, o jakou verzí jazyka GLSL. (V nových verzích toto číslo odpovídá verzi OpenGL.)
Místo attribute
se používá kvalifikátor in
.
Pomocí layout (location = 0)
explicitně určuji, pod jakým
číslem (identifikátorem, ID) budu k proměnné přistupovat v C
programu.
Funkce OpenGL pracující s proměnnými z GLSL očekávají jako jeden
z parametrů toto ID.
To se týká první fáze pipeline (nahrávání vertex dat) a budu se tomu
věnovat v další kapitole.
Kvalifikátor uniform
zůstává uniform
. Místo
varying
se pro výstupní proměnnou používá out
.
V fragment shaderu musí existovat proměnná stejného jména a stejného
datového typu s kvalifikátorem in
.
V fragment shaderu je ještě jedna změna. Jako výstup se místo globální
proměnné gl_FragColor používá libovolná proměnná typu vec4
označená kvalifikátorem out
. Resp. jde o proměnnou libovolného
jména, která je typu vec4
a má kvalifikátor
Podmínky a cykly
V GLSL můžete používat podmínky a cykly pro řízení programu, jak to znáte z jazyka C. Používat byste je však měli co nejméně, protože jsou to zabijácí výkonu.
Iterace | for(;;;) {break, continue} while() {break, continue} do {break, continue} while(); |
---|---|
Výběr | if() else {} switch() { case integer: ... break; ... default:} |
Ukončení | return discard // Ukončení fragment shaderu |
Funkce
GLSL obsahuje několik užitečných funkcí. V tabulce jsou vypsány
ty nejužitečnější z nejužitečnějších :-). Typy argumentů a návratových
hodnot jsou většinou float, vecX, nebo matX.
Nebudu je v tabulce specifikovat (vyjma funkce texture()
),
myslím, že si je snadno domyslíte.
radians(s), degrees(r) | Funkce pro převod stupně na radiány a opačně. |
---|---|
sin(a), cos(a),tan(a),asin(a),acos(a),atan(y,x) | goniometrické funkce |
pow(x,y) | xy |
sqrt(x) | odmocnina |
abs(x) | Absolutní hodnota |
sign(x) | vrací -1.0, 0.0, 1.0, dle znamánka x |
clamp(x, minVal, maxVal) | min(max(x,minVal), maxVal) (x se ořízne mezi minVal a maxVal) | length(v) | Délka vektoru |
distance(v1,v2) | Vzdálenost dvou bodů |
dot(v1,v2) | Dot product |
cross(v1,v2) | Cross product |
normalize(v) | Normalizace vektoru |
reflect(v,n) | Spočte směr odrazu |
transpose(m) | Transponuje matici |
inverse(m) | Inverzní matice |
texture(sampler2d t, vec2 coord) | Vrátí barvu textury t na pozici coord. |
mix(v1,v2,x) | smíchá hodnoty v1 a v2 v poměru udaném
číslem x, kde x je mezi 0.0 a 1.0. (Pokud je x = 0, výsledkem je v1, pokud je x = 1.0, výsledkem je v2.) |
Díky funkci sign()
se můžete vyvarovat použití
pomalého if
.
Operátory
Operátory jsou v GLSL podobné, jako v C. Máte tu operátor přiřazení
=, +=, *= …
, operátory relační ==,!=,<,>,<=,>=
,
aritmetické operátory +,-,*,/,%
, binární operátory, unární operátory …
Závěr
Toto byl stručný popis GLSL. Pokud je na vás moc stručný, můžete si na stránce www.khronos.org/developers/reference-cards stáhnout free PDF Reference Guide pro OpenGL. Jedná se o stručný přehled funkcí OpenGL i jazyka GLSL. Najdete tam všechny funkce, operátory, datové typy, globální proměnné atd. (Těch je mimochodem opravdu málo. Modře označené jsou navíc deprecated, takže byste je neměli používat).
Teď už umíte napsat program pro vertex a fragment shader. Příští
kapitola se ještě nebude zabývat první fází s pipeline – jak připravit
data pro in
a uniform
proměnné, ale a tím, jak o
vytvořit OpenGL program ze zdrojových souborů pro vertex a fragment shader.