Az OpenGL 4.0-tól kezdve nem csak csúcspontokként, pixelenként, de primitívenként is futtathatunk programokat. Ez azt jelenti, hogy ha van egy összetett alakzatunk, akkor az azt felépítő háromszögek mindegyikét feldolgozhatjuk shaderrel.
Mit jelent mindez a gyakorlatban? A vertex shader csak egyfajta "ajánlás", hogy miként jelenjen meg az alakzat, a tényleges megvalósítást a geometry shaderre bízhatjuk. Ha ehhez még hozzávesszük azt is, hogy a geometry shaderben új csúcspontokat hozhatunk létre, akkor nem meglepő, ha az ember rögtön azon kezd el gondolkodni, miként használhatja ezt egy demóban.
A koncepció bemutatására egy nagyon egyszerű részecske rendszert fogunk látni, ami halványan egy tüzijátékra emlékeztet. A csúcspontok a CPU-ban kerülnek kiszámításra, majd a geometry shaderben repeszeket adunk hozzá. Mindent pontokként rajzolunk ki.
Először meghatározzuk a csúcspontok helyzetét:
for(i = 0; i < PARTICLE_NUM * 3; i++){
Véletlenszerűen kiválasztunk néhány koordinátát. Ezután létrehozunk egy vertex array objectet.
pos[i] = drand48() - 0.5;
}
glGenVertexArrays(1, &fire_vao);
További attribútumokat is megadhatunk újabb bufferek hozzáadásával. Minden attól függ, mennyire összetett részecskéket akarunk. Most csak a pozíció kerül átadásra.
glBindVertexArray(fire_vao);
glGenBuffers(1, &fire_vbo);
glBindBuffer(GL_ARRAY_BUFFER, fire_vbo);
glBufferData(GL_ARRAY_BUFFER, PARTICLE_NUM * 3 * sizeof(GLfloat), pos, GL_STATIC_DRAW);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, NULL);
glEnableVertexAttribArray(0);
void PlayFirework(){
A megjelenítés abból áll, hogy a csúcspontokat kirajzoljuk a megfelelő shaderrel. Mivel animációt is akarunk, ezért az eltelt időt is elküldjük egy változón keresztül. (time) Végül lássuk a shadereket.
double time;
GLuint uni;
time = getTimeInterval();
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
uni = glGetUniformLocation(fire_prg, "time");
glUniform1f(uni, time);
glUseProgram(fire_prg);
glBindVertexArray(fire_vao);
glDrawArrays(GL_POINTS, 0, PARTICLE_NUM);
}
#version 410
A vertex shader nem csinál semmi izgalmasat. Megmondjuk neki, hogy a buffer mely attribútumát használja fel, majd tovább is adjuk ezt az információt.
layout (location = 0) in vec3 vertex;
void main(void){
gl_Position = vec4(vertex, 1.0);
}
#version 410
A geometry shader végzi a munka oroszlán részét. Megmondjuk, hogy milyen formában fogadjuk a primitíveket. Most pontok vannak beállítva, de természetesen lehetnek háromszöget is, a program felépítésétől függően. Azt is megadjuk, hogy mi a kimeneti primitív típusa. A példában ez is pont. Érdekes, hogy nincs szükség arra, hogy a bemeneti és kimeneti típusok megegyezzenek. Tehát én akár háromszögeket is felépíthetek a bemeneti pontok alapján.
layout (points) in;
layout (points, max_vertices = 40) out;
out vec4 color;
uniform float time;
void main(){
float theta;
vec4 emitter = gl_in[0].gl_Position;
float offset = mod(time, 2) / 22.0;
gl_Position = emitter;
color = vec4(1.0);
EmitVertex();
EndPrimitive();
for(theta = 0.0; theta < 3.14 * 2.0; theta += 0.2){
gl_Position.x = emitter.x + offset * sin(theta);
gl_Position.y = emitter.y + offset * cos(theta);
color = vec4(1.0 - (offset * 10.0), 0.0, 0.0, 1.0);
EmitVertex();
EndPrimitive();
}
}
Leírásokban általában nem utalnak rá, de logikus, hogy nem lehet akár mennyi új csúcspontot képezni. Az általam birtokolt kártya megkövetelte, hogy állítsam be ennek maximális számát. Ennek hiányában nem generált hibaüzenetet, de nem is jelenített meg semmit. Nálam maximálisan 256 csúcspontot lehet emittálni. (Eredetileg gömböket akartam rajzolni, de nagyon hamar túlléptem a keretet.) A color kimeneti változót a fragment shader fogja megkapni. A time pedig az idő intervallum, amit szintén a CPU küld át.
Ami a shader szempontjából a legfontosabb, az a gl_in struktúra tömb. A tömb annyi elemet tartalmaz, ahány csúcspontot a primitív. Mivel pontokból kapjuk a csúcspontokat, ezért egyetlen elemet tartalmaz a tömb. A struktúra egyik eleme a pozíció információ (gl_Position)
Az idő alapján kiszámoljuk, mennyire mozdulhatnak el a repeszek a középponttól, erre szolgál az offset. Most jön az első érdekes rész. A gl_Position változónak megadjuk pontosan ugyan azt az értéket, amit megkaptunk. Ez csupán azért kell, hogy lássuk a középpontját a tüzijátéknak. A színe is más a pontnak. Két geometry shader specifikus függvény zárja a szakaszt. Az EmitVertex() jelzi, hogy elkészültünk egy új csúcsponttal, majd az EmitPrimitive(), hogy a primitív is elkészült. Ha háromszögekkel dolgoznánk, akkor háromszor annyi EmitVertex() kellene, mint EmitPrimitive().
Egy ciklusban is csúcspontokat hozunk létre. Egy folyamatosan növekvő kör érintőjén helyezkednek el a csúcsok, és az idővel halványulnak. Két másodpercenként újabb adag részecske keletkezik.
#version 410
Végezetül a korábban beállított szín segítségével megjelenítjük a pontokat.
layout (location = 0) out vec4 FragColor;
in vec4 color;
void main(void){
FragColor = color;
}
Az új shaderek segítségével egyre több feladatot adhatunk át a GPU-nak. Új alakzatokat hozhatunk létre, és akár egy jó kis demót is írhatunk segítségükkel, mint amilyen a Texas is a keyborderstől. Ez a demó DirectX-es ugyan, de az OpenGL 4 segítségével mi is használhatjuk a technikát.