Biopunk című demónkban volt egy érdekes effekt, ami egyfajta sejtosztódást próbált meg utánozni. Maga az effekt nem túl bonyolult, egy metaballs implementáció volt 2D-ben. Röviden arról van szó, hogy a képernyőn részecskék mozognak, de minden pixelnél az összes részecskéktől való távolságot összeadjuk, és ez alapján határozzuk meg az adott pixel színét. Itt sokkal részletesebben lehet olvasni róluk.
Organikusnak tűnő alakzatokat lehet vele készíteni, amelyek "nehezen szakadnak" el egymástól. Ideálisak sejtosztódás szimulálására. A megjelenítés módja pedig óhatatlanul azt vonta maga után, hogy fragment shaderben kell elkészíteni.
Az én implementációm csak annyiban különbözött a hétköznapi metaballtól, hogy bizonyos idő elteltével minden részecskéből egy új született. Az új részecske tulajdonságait a GPU-n kívül akartam beállítani. Itt jött az első probléma: hogyan küldjem el a részecskék tulajdonságait (sebesség, életkor, pozíció) a fragment shadernek? Kis számú részecske esetén változóként beadhatom a shadereknek, ez nem probléma, de én 80 darab sejtet akarta.
A megoldás az volt, hogy textúraként adom át a részecske adatokat. CPU-n meghatározom a tulajdonságokat, majd textúrát készítek belőle és hagyom, hogy a megjelenítés nehézségét a shader elvégezze. Csupán egy uniform változót adok át, a részecskék számát (a textúrán kívül, természetesen).
A textúra minden egyes oszlopa egy részecskének felelt meg, a sorok pedig az egyes tulajdonságoknak. Az egyszerúség kedvéért csak egycsatornás textúrát készítettem.
glGenTextures(1, &celltexture);
glBindTexture(GL_TEXTURE_2D, celltexture);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RED, MAXCELLNUM, CELLATTRIBNUM, 0, GL_RED, GL_UNSIGNED_BYTE, 0);
glBindTexture(GL_TEXTURE_2D, 0);
Ezután készítettem egy pixel buffer objectet (továbbiakban PBO).
glGenBuffers(1, &cellbuffer);
A kirajzolásnál frissítettem a PBO-t, majd átmásoltam azt a textúrába (glTexSubImage2D függvény segítségével):
glBindBuffer(GL_PIXEL_UNPACK_BUFFER, cellbuffer);
glBufferData(GL_PIXEL_UNPACK_BUFFER, sizeof(GLubyte) * MAXCELLNUM * CELLATTRIBNUM, 0, GL_DYNAMIC_DRAW);
glBindTexture(GL_TEXTURE_2D, celltexture);
Az updatePixelData tartalmazza a PBO frissítését. A részecskékkel kapcsolatos részt most kihagyom és csak az OpenGL kódra összpontosítok:
glBindBuffer(GL_PIXEL_UNPACK_BUFFER, cellbuffer);
updatePixelData(time);
/* Copy from PBO to texture */
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, MAXCELLNUM, CELLATTRIBNUM, GL_RED, GL_UNSIGNED_BYTE, 0);
glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0);
ptr = glMapBuffer(GL_PIXEL_UNPACK_BUFFER, GL_WRITE_ONLY);
Gyakorlatilag a glMapBuffer és glUnmapBuffer a lényeges. Kapunk egy pointert, amin keresztül új értékekkel tölthetjük fel a PBO-t. Mivel nekem megvolt a részecskék összes tulajdonsága a CPU-ban is, ezért GL_WRITE_ONLY-ként határoztam meg a puffert.
for(i = 0; i < cells.bornedcells; i++){
/* Particle part skipped */
ptr[0 * MAXCELLNUM + i] = (65535.0 * cells.cells[i].xpos) / 255.0;
ptr[1* MAXCELLNUM + i] = (int)(65535.0 * cells.cells[i].xpos) % 255;
ptr[2 * MAXCELLNUM + i] = (65535.0 * cells.cells[i].ypos) / 255.0;
ptr[3 * MAXCELLNUM + i] = (int)(65535.0 * cells.cells[i].ypos) % 255;
}
glUnmapBuffer(GL_PIXEL_UNPACK_BUFFER);
A shader a celldata nevű textúrában kapja meg a részecskéket és a texelFetch utasítással olvasom ki az értékeket.
#version 150
A sejtek pozícióit nem elég 8 biten tárolni, ezért a pozíciókat a textúrában két sorban tárolom, azért van ennyi bűvészkedés a sejtek helyzetének meghatározása körül. Gyakorlatilag ez egy tervezési hiba, ha a textúrák nem 1 byte-on tárolnák az adatokat, a kód egyszerűbb lehetne. Aki fel akarja használni a kódot, annak célszerű kijavítani ezt.
#define MIN_THRESHOLD 0.99f
in vec2 texcoord;
out vec4 gl_FragColor;
uniform sampler2D celldata;
uniform int bornedcellnum;
float equation(float radius, float mx, float my, float x, float y){
return( radius / sqrt( (x-mx)*(x-mx) + (y-my)*(y-my) ) );
}
void main(void){
int i;
vec2 cellsize = textureSize(celldata, 0);
float sum = 0.0;
for(i = 0; i < bornedcellnum && sum < MIN_THRESHOLD; i++){
float mxl = texelFetch(celldata, ivec2(i,0), 0).r;
float mxh = texelFetch(celldata, ivec2(i,1), 0).r;
float myl = texelFetch(celldata, ivec2(i,2), 0).r;
float myh = texelFetch(celldata, ivec2(i,3), 0).r;
float mx = mxl + mxh/256.0;
float my = myl + myh/256.0;
float x = texcoord.x;
float y = texcoord.y;
sum += equation(0.003, mx, my, x, y);
}
if(sum >= MIN_THRESHOLD){
float c = sum / 3.0;
c = max(c, 0.5);
gl_FragColor = vec4(c,c,c,1.0);
}
else{
gl_FragColor = vec4(0.0, 1.0 - distance(texcoord, vec2(0.5)), 0.0, 1.0);
}
}
A teljes forrás letölthető innen. Az src könyvtárban található celldivide.c fájl tartalmazza a sejtosztódós részt.