Ha a háromszög túl sok, ha a vonal túl unalmas, ha a jelszavunk: zéró poligon, akkor a pontok segítségével kápráztathatjuk el a nézőket. Már a korai OpenGL verziókban is használhattuk a glBegin(GL_POINTS) hívást, de a pontok koordinátáit kénytelenek voltunk CPU-n számolni.
Ahogy fejlődtek a grafikus kártyák és nőtt az OpenGL verziószám, úgy tudtunk egyre több kódot átpakolni GPU-ra. Most egy olyan technikát akarok bemutatni, ami az összes logikát shaderekből valósítja meg. Miért jó ez? Már korábban is törekedtem arra, hogy a shadereket egyfajta szkript nyelvként használjam és úgy tudjak változtatni a jeleneteken, hogy ne kelljen újra fordítani a kódot. Ez remélhetőleg el fog vezetni egy viszonylag egyszerű demo toolhoz.
Az ötlet nem a sajátom, a vertexshaderart.com az alapja. Nagy vonalakban úgy néz ki, hogy a CPU-n lefoglaljuk a memóriát a csúcsoknak, de nem állítunk be semmit, betöltjük a shadereket, majd kirajzolunk mindent pontként. A koordinátákat a vertex shader határozza meg a csúcsok sorszáma alapján. A fragment shaderrel még játszunk a megjelenítésen, és kész.
Lássuk a kódot! Az ablaknyitástól és egyéb "unalmas" lépésektől most eltekintek. Ha hatalmas komment áradat követeli, akkor beteszem a teljes kódot, de ettől jelenleg nem tartok.
glEnable(GL_TEXTURE_2D);
glEnable(GL_PROGRAM_POINT_SIZE);
glGenVertexArrays(1, &pointsprite);
glBindVertexArray(pointsprite);
GLfloat *spritecoords;
spritecoords = (GLfloat*)malloc(POINTNUM * sizeof(GL_FLOAT));
glGenBuffers(1, &pointsprites_vba);
glBindBuffer(GL_ARRAY_BUFFER, pointsprites_vba);
glBufferData(GL_ARRAY_BUFFER, POINTNUM * sizeof(GL_FLOAT), spritecoords, GL_STATIC_DRAW);
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 0, NULL);
glEnableVertexAttribArray(0);
free(spritecoords);
Létre hozunk egy vertex arrayt, abban egy buffert, lefoglaljuk a memóriát, átmásoljuk a GPU-ra, majd a CPU-n mindent felszabadítunk. Még arra sem vesztegetjuk a drága erőforrásokat, hogy a pointjainknak textúra koordinátát tartsunk fenn. Pedig fogunk textúrázni is! A GL_PROGRAM_POINT_SPRITE bekapcsolása fontos lépés, mert a pontunk méretét is meg akarjuk változtatni a shaderben.
A kirajzolás szintén semmi újat nem tartalmaz:
glUseProgram(textsprite);
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, index);
GLint loc = glGetUniformLocation(textsprite, "img");
glUniform1i(loc, 0);
glBindVertexArray(pointsprite);
glDrawArrays(GL_POINTS, 0, POINTNUM / 2);
Természetesen további uniform változókat is használhatunk, hogy további érdekes effekteket hozzunk létre. Például átadhatnánk az időt, mint paramétert. A képernyő felbontást, egér pozíciót, stb. Továbbá mellőzöm a shader betöltő kódot is.
Térjünk rá a móka részére! Vertex shaderünk bemeneti változói közül így csak a gl_VertexID-t tudjuk használni. Ez nem más, mint egy egész szám 0-tól POINTNUM-ig (csak, hogy konzekvens legyek a kóddal). Ebből határozhatjuk meg a sprite-ok helyzetét. Például ha van 8000 pontunk, akkor a következő kóddal színusz hullám formájában rajzolhatjuk ki őket:
#version 410
void main(){
float x = gl_VertexID / 8000.0 - 1.0;
float y = sin(x) * 200.0);
vec2 vertex = vec2(x,y);
gl_Position = vec4(vertex, 0.0, 1.0);
}
Ez elég érdekes, de tudunk ennél jobbat is! Például textúrát húzhatunk rájuk. Egy pontra nem sok értelme van textúrát húzni, de a pontjaink méretét tudjuk változtatni a gl_PointSize változóval. Meglepő módon pixelben kell megadni a méretet, ami (legalábbis nekem) szokatlan, hiszen minden más értéket 0-1 közötti számmal kell definiálni. Ezt a felbontás független kód írásánál érdemes észben tartani.
#version 410
out vec4 FragColor;
uniform sampler2D img;
void main(){
FragColor = texture(img, vec2(1.0, -1.0) * gl_PointCoord);
}
A fenti fragment shader használatával sprite-jaink mindegyike ugyan azzal a textúrával lesz bevonva. A gl_PointCoord változó kiszámítja nekünk a megfelelő textúra koordinátákat. Azért sok, egyforma alakzat unalmas. Adjunk egyéniséget spritejainknak! Például pozíciótól függően változtassuk a textúrát. Vertex shaderünk egy kicsit átalakul:
#version 410
out vec2 textcoord;
void main(){
float x = gl_VertexID / 8000.0 - 1.0;
float y = sin(x) * 200.0);
vec2 vertex = vec2(x,y);
textcoord = vertex;
gl_Position = vec4(vertex, 0.0, 1.0);
}
A fragment shader is módosul, de nem sokat:
#version 410
out vec4 FragColor;
in vec2 textcoord;
uniform sampler2D img;
void main(){
FragColor = texture(img, vec2(1.0, -1.0) * textcoord);
}
Ez már sokkal érdekesebb hatás. Kísérletezzünk bátran. Próbáljunk ki őrült ötleteket, hátha valamelyik megtetszik.