[QUIZ#7] Matthias Reitinger (C/GLSL)


#1
Hallo,

ich präsentiere die wohl umständlichste Lösung, die aber manchen vielleicht Einblicke in ein bisher unbekanntes Gebiet bescheren könnte. Das Schlagwort lautet GPGPU.

Kurz zum Prinzip: Echtzeitgrafik-APIs wie DirectX oder OpenGL verfolgen das Rasterisierungsprinzip. D.h. man übergibt dem API ein Primitiv (Dreieck, Rechteck…), welches dann in das Pixelraster des Bildschirms (eigentlich: des Backbuffers bzw. des Render Targets) übertragen wird. Die dabei überdeckten Pixel nennt man Fragmente. Für jedes dieser Fragmente wird (sofern vorhanden) der Fragment Shader ausgewertet. Dieser bestimmt (u.a.), mit welcher Farbe der zugehörige Pixel nachher auf dem Bildschirm erscheinen soll.

Das Raster für die Hefezellen wird über eine 2D-Textur dargestellt, wobei ein schwarzer Pixel für ein unbewohntes Feld steht und der weißer für ein Feld mit einer Hefezelle darauf. Wir wollen jetzt den Fragment Shader dazu missbrauchen, die nächste Generation zu berechnen. Dazu soll der Shader für jedes Texel (Textur-Pixel) der Textur genau einmal aufgerufen werden. Dies erreicht man, indem man den Viewport (der Bereich, in den gerendert wird) genau auf die Texturgröße setzt und ein „Screen Aligned Quad“ rendert, also ein Rechteck, das den gesamten Bildschirmbereich überdeckt. Insbesondere wird also jeder Pixel des Bildbereichs überdeckt und für jeden wird der Fragment Shader aufgerufen. Dieser kann nun die Lookups in die Hefezellen-Textur durchführen und das Ergebnis als Fragmentfarbe (gl_FragColor) zurückgeben. Dieses schreiben wir an die entsprechende Stelle in einer weiteren Textur (indem wir das Render Target entsprechend auf die Textur setzen). Schlussendlich können wir diese Textur auf dem Bildschirm darstellen, indem wir wieder ein Screen Aligned Quad rendern, auf das wir die gerade berechnete Textur legen. Im nächsten Simulationsschritt vertauschen wir einfach die Rollen der beiden beteiligten Texturen (das vorherige Render Target wird zur Lookup-Textur des Fragment Shaders und umgekehrt).

Das klingt jetzt wahrscheinlich alles ziemlich konfus, aber das Prinzip lässt sich in einem Absatz nicht so gut zusammenfassen. Wer Interesse daran hat, kann ja einfach mal nach GPGPU im Internet suchen oder einfach mich fragen :)

Hier aber nun der Quellcode:
C:
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <GL/glew.h>
#include <GL/glut.h>

GLuint fb;
GLuint tex[2];
GLenum attach_point[2] = { GL_COLOR_ATTACHMENT0_EXT, GL_COLOR_ATTACHMENT1_EXT };
int write_tex = 0, read_tex = 1;

int width, height;
GLhandleARB simulation_program;

int full_speed = 0;
int steps;
int generation = 0;
unsigned char *data;

void InitGLUT(int argc, char **argv) {
  glutInit(&argc, argv);
  glutInitWindowPosition(0, 0);
  glutInitWindowSize(320, 320);
  glutInitDisplayMode(GLUT_RGBA | GLUT_DOUBLE);
  glutCreateWindow("Johns Hefe");
}

void InitGLEW(void) {
  int err = glewInit();
  if (err != GLEW_OK) {
    printf("%s\n", (char*)glewGetErrorString(err));
    exit(-1);
  }
  if (!(GLEW_ARB_fragment_shader &&
        GLEW_EXT_framebuffer_object &&
        GLEW_ARB_texture_non_power_of_two)) {
    printf("Sorry, your GPU isn't supported.\n");
    exit(-1);
  }
}

void InitFBO(void) {
  glGenFramebuffersEXT(1, &fb);
  glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, fb);

  glGenTextures(2, tex);
  glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
  int i;
  for (i = 0; i < 2; ++i) {
    glBindTexture(GL_TEXTURE_2D, tex[i]);
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_LUMINANCE,
      GL_UNSIGNED_BYTE, data);
    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_TO_BORDER);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_BORDER);
    glFramebufferTexture2DEXT(GL_FRAMEBUFFER_EXT, attach_point[i],
      GL_TEXTURE_2D, tex[i], 0);
  }

  free(data);

  glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, 0);
}

void InitShaders(void) {
  const char *str_fragment_shader =
    "uniform sampler2D texture;\n"
    "uniform vec2 tex_unit;\n"
    "void main() {\n"
    "  int cell = texture2D(texture, gl_TexCoord[0]).r;\n"
    "  int neighbors = 0;\n"
    "  for (int s = -1; s <= 1; ++s)\n"
    "    for (int t = -1; t <= 1; ++t) {\n"
    "      if (s == 0 && t == 0) continue;\n"
    "      neighbors += texture2D(texture, gl_TexCoord[0] + tex_unit*vec2(s, t)).r;\n"
    "    }\n"
    "  if (neighbors == 3 || (cell == 1 && neighbors == 2))\n"
    "    gl_FragColor = 1.0;\n"
    "  else\n"
    "    gl_FragColor = 0.0;\n"
    "}\n";
  GLint length = strlen(str_fragment_shader);

  GLhandleARB fragment_shader = glCreateShaderObjectARB(GL_FRAGMENT_SHADER_ARB);
  glShaderSourceARB(fragment_shader, 1, &str_fragment_shader, &length);
  glCompileShaderARB(fragment_shader);

  simulation_program = glCreateProgramObjectARB();
  glAttachObjectARB(simulation_program, fragment_shader);
  glLinkProgramARB(simulation_program);

  glUseProgramObjectARB(simulation_program);
  glUniform2fARB(glGetUniformLocationARB(simulation_program, "tex_unit"),
                 1.0f/(GLfloat)width,
                 1.0f/(GLfloat)height);
  glUseProgramObjectARB(0);
}

void DrawScreenAlignedQuad(int flip_y) {
  flip_y = !!flip_y;
  glMatrixMode(GL_MODELVIEW);
  glPushMatrix();
  glLoadIdentity();
  glMatrixMode(GL_PROJECTION);
  glPushMatrix();
  glLoadIdentity();
  glBegin(GL_QUADS);
    glTexCoord2f(0, !flip_y); glVertex2f(-1, -1);
    glTexCoord2f(1, !flip_y); glVertex2f( 1, -1);
    glTexCoord2f(1,  flip_y); glVertex2f( 1,  1);
    glTexCoord2f(0,  flip_y); glVertex2f(-1,  1);
  glEnd();
  glMatrixMode(GL_PROJECTION);
  glPopMatrix();
  glMatrixMode(GL_MODELVIEW);
  glPopMatrix();
}

void Simulate(void) {
  // Textur als Render-Target setzen
  glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, fb);
  glDrawBuffer(attach_point[write_tex]);

  // Viewport setzen
  glPushAttrib(GL_VIEWPORT_BIT);
  glViewport(0, 0, width, height);

  // Textur binden
  glEnable(GL_TEXTURE_2D);
  glBindTexture(GL_TEXTURE_2D, tex[read_tex]);

  // Programm laden und rendern
  glUseProgramObjectARB(simulation_program);
  DrawScreenAlignedQuad(1);
  glUseProgramObjectARB(0);

  glPopAttrib();

  glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, 0);

  // Rolle der Texturen tauschen
  write_tex = 1 - write_tex;
  read_tex = 1 - read_tex;

  generation++;
}

void Display(void) {
  // Textur bildschirmfüllend rendern
  glEnable(GL_TEXTURE_2D);
  glBindTexture(GL_TEXTURE_2D, tex[read_tex]);

  DrawScreenAlignedQuad(0);

  glutSwapBuffers();
}

void Resize(int w, int h) {
  if (h == 0) h = 1;
  glViewport(0, 0, w, h);
}

void Idle(void) {
  Simulate();
  glutPostRedisplay();
}

void DisplayHelp(void) {
  printf("Keys:\n");
  printf("h           print this help screen\n");
  printf("[space]     simulate n steps\n");
  printf("+           increase n\n");
  printf("-           decrease n\n");
  printf("f           run full speed (toggle)\n");
  printf("g           display current generation\n");
  printf("q/[escape]  quit\n");
  printf("\n");
}

void Keyboard(unsigned char key, int x, int y) {
  switch (key) {
    case 27:
    case 'q':
    case 'Q':
      exit(0);
      break;
    case ' ': {
      int i;
      for (i = 0; i < steps; ++i) Simulate();
      glutPostRedisplay();
      break;
    }
    case 'H':
    case 'h':
      DisplayHelp();
      break;
    case '+':
      steps++;
      printf("n = %d\n", steps);
      break;
    case '-':
      steps--;
      if (steps < 1) steps = 1;
      printf("n = %d\n", steps);
      break;
    case 'f':
    case 'F':
      full_speed = !full_speed;
      if (full_speed) {
        printf("Running at full speed\n");
        glutIdleFunc(Idle);
      } else {
        printf("Running in step mode\n");
        glutIdleFunc(NULL);
      }
      break;
    case 'g':
    case 'G':
      printf("Current generation: #%d\n", generation);
      break;
  }
}

void ReadInput(void) {
  scanf("%d\n%d\n%d\n", &height, &width, &steps);
  data = malloc(width*height*sizeof(unsigned char));
  char *line = malloc(width+2);
  int i, j;
  for (i = 0; i < height; ++i) {
    fgets(line, width+2, stdin);
    for (j = 0; j < width; ++j) {
      char ch = line[j];
      if (ch == 'o' || ch == 'O') data[i*width+j] = 255;
      else data[i*width+j] = 0;
    }
  }
  free(line);
}

void Cleanup(void) {
  free(data);
}

int main(int argc, char **argv) {
  ReadInput();
  DisplayHelp();

  InitGLUT(argc, argv);
  InitGLEW();
  InitFBO();
  InitShaders();

  glutDisplayFunc(Display);
  glutReshapeFunc(Resize);
  glutKeyboardFunc(Keyboard);

  glutMainLoop();

  return 0;
}
Kompilieren z.B. mit:
Code:
gcc -Wall -o sim -lglut -lGLEW -lGLU -lGL sim.c
Aufruf z.B. mit:
Code:
./sim <glider_cannon.txt
Die glider_cannon.txt habe ich angehängt.

Wie man sieht benötigt man GLEW. Die Grafikkarte/der Treiber muss die Erweiterungen ARB_fragment_shader, EXT_framebuffer_object und ARB_texture_non_power_of_two unterstützen. Ich konnte das Programm leider nur unter Linux testen, aber vielleicht läuft es ja auch unter Windows.

Der Übersichtlichkeit halber hier nochmal nur der GLSL-Shader:
C:
uniform sampler2D texture;
uniform vec2 tex_unit;
void main() {
  int cell = texture2D(texture, gl_TexCoord[0]).r;
  int neighbors = 0;
  for (int s = -1; s <= 1; ++s)
    for (int t = -1; t <= 1; ++t) {
      if (s == 0 && t == 0) continue;
      neighbors += texture2D(texture, gl_TexCoord[0] + tex_unit*vec2(s, t)).r;
    }
  if (neighbors == 3 || (cell == 1 && neighbors == 2))
    gl_FragColor = 1.0;
  else
    gl_FragColor = 0.0;
}
Grüße, Matthias
 

Anhänge

Zuletzt bearbeitet: