= Visualization with OpenGL hands-on = [[PageOutline]] Tutorial aims to introduce visualization techniques with modern Open Graphics Language (OpenGL) approaches standardized with version 3.x+. OpenGL Shading Language (GLSL) is used for that without tendency to introduce ''photo-realism''as output but rather useful colors for scientific data exploration. Running this tutorial on Linux desktop one requires at least the OpenGL 2.0 graphics with the GLSL 1.1 and supporting libraries GL, GLU, GLUT, GLEW. This can be verified with the following commands: {{{ #!sh $ glxinfo |grep OpenGL.*version OpenGL version string: 2.1 Mesa 8.0.5 OpenGL shading language version string: 1.20 $ ls /usr/include/GL/{glut.h,glew.h,gl.h,glu.h} /usr/include/GL/glew.h /usr/include/GL/glu.h /usr/include/GL/gl.h /usr/include/GL/glut.h }}} == Legacy OpenGL == Create the following {{{first.c}}} using your favorite editor. {{{ #!c #include void display() { glClear(GL_COLOR_BUFFER_BIT); glColor3f(1.0, 0.4, 1.0); glBegin(GL_LINES); glVertex2f(0.1, 0.1); glVertex3f(0.8, 0.8, 1.0); glEnd(); glutSwapBuffers(); } int main(int argc, char *argv[]) { glutInit(&argc,argv); glutInitDisplayMode(GLUT_DOUBLE); glutCreateWindow("first.c GL code"); glutDisplayFunc(display); glutMainLoop(); return 0; } }}} Create {{{Makefile}}} to build your program. [[Image(first.png,right)]] {{{ #!sh CFLAGS=-Wall LDFLAGS=-lGL -lGLU -lglut -lGLEW ALL=first default: $(ALL) first : first.o clean: rm -rf *.o *~ [!m]*.obj core* $(ALL) }}} Beware that Makefile is TAB aware. So the last line should contain TAB indentation and not spacing. Make and run the program with {{{ #!sh make ./first }}} Try the same program in Python {{{ #!python from OpenGL.GLUT import * from OpenGL.GL import * import sys def display(): glClear(GL_COLOR_BUFFER_BIT) glColor3f(1.0, 0.4, 1.0) glBegin(GL_LINES) glVertex2f(0.1, 0.1) glVertex3f(0.8, 0.8, 1.0) glEnd() glutSwapBuffers() if __name__ == "__main__": glutInit(sys.argv) glutInitDisplayMode(GLUT_DOUBLE) glutCreateWindow("first.py GL code") glutDisplayFunc(display) glutMainLoop() }}} and run it with {{{ #!sh python first.py }}} === Exercises #1: === 1. Add RGB color to vertices with {{{ glColor3f(0.0, 0.4, 1.0);}}}. 2. Replace single line drawing in {{{display()}}} with the following snippet {{{ #!c GLfloat vertices[][2] = { { -0.90, -0.90 }, // Triangle 1 { 0.85, -0.90 }, { -0.90, 0.85 }, { 0.90, -0.85 }, // Triangle 2 { 0.90, 0.90 }, { -0.85, 0.90 } }; }}} and try to draw two wireframe triangles in a loop. Change primitive to {{{GL_LINE_LOOP}}}. 3. Draw two primitives with {{{GL_TRIANGLES}}}. [[Image(ex1-5.png, right)]] 4. Add different color to each vertex. {{{ #!c GLfloat color[][3] = { {1, 0, 0}, {0, 1, 0}, {0, 0, 1}, {1, 1, 0}, {0, 1, 1}, {1, 0, 1}}; }}} 5. Replace loop with the following {{{ #!c glVertexPointer(2, GL_FLOAT, 0, &vertices[0][0]); glEnableClientState(GL_VERTEX_ARRAY); glDrawArrays(GL_TRIANGLES, 0, 6); glDisableClientState(GL_VERTEX_ARRAY); }}} How can we add color to vertices? See [http://www.opengl.org/sdk/docs/man2/xhtml/glColorPointer.xml glColorPointer] and [http://www.opengl.org/sdk/docs/man2/xhtml/glEnableClientState.xml glEnableClientState]. 6. Change background to {{{glClearColor(0.9,1,1,1.0);}}} and suggest initial window in `main()` {{{ #!c glutInitWindowSize(512, 512); glutInitWindowPosition((glutGet(GLUT_SCREEN_WIDTH)-512)/2, (glutGet(GLUT_SCREEN_HEIGHT)-512)/2); }}} 7. Add [http://www.opengl.org/resources/libraries/glut/spec3/node49.html keyboard event] to quit the program when pressing ESCape key with keycode 27 by adding callback function {{{ #!c void keyboard(unsigned char key, int x, int y) { if (key == 27) exit(0); } }}} and registering event within `main()` by `glutKeyboardFunc(keyboard);`. Some prefer `key == 'q'`, though. == Modern OpenGL == [[Image(OpenGL-pipeline.svg, 320px, right, title=OpenGL pipeline)]] We extend previous exercise with example that introduces OpenGL 3.x techniques: * OpenGL Shading Language (GLSL 1.2) where simple vertex and fragment shader are required. * Vertex Aray Objects (VAOs) and vertex buffers (VBOs) stored in GPU. Create {{{triangle.c}}} and update {{{Makefile}}} with new target {{{ #!sh #include #include #include #include static const GLchar * vertex_shader[] = {"void main()" "{" " gl_Position = ftransform();" "}" }; static const GLchar * fragment_shader[] = {"void main()" "{" " gl_FragColor = vec4(0.4,0.4,0.8,1.0);" "}" }; void setShaders() { GLuint v, f, p; v = glCreateShader(GL_VERTEX_SHADER); f = glCreateShader(GL_FRAGMENT_SHADER); glShaderSource(v, 1, vertex_shader, NULL); glShaderSource(f, 1, fragment_shader, NULL); glCompileShader(v); glCompileShader(f); p = glCreateProgram(); glAttachShader(p,f); glAttachShader(p,v); glLinkProgram(p); glUseProgram(p); } enum VAO_IDs { Triangles, NumVAOs }; enum Buffer_IDs {ArrayBuffer,NumBuffers}; enum Attrib_IDs { vPosition = 0 }; GLuint VAOs[NumVAOs]; GLuint Buffers[NumBuffers]; #define NumVertices 6 void init(void) { glGenVertexArrays(NumVAOs, VAOs); glBindVertexArray(VAOs[Triangles]); GLfloat vertices[NumVertices][2] = { { -0.90, -0.90 }, // Triangle 1 { 0.85, -0.90 }, { -0.90, 0.85 }, { 0.90, -0.85 }, // Triangle 2 { 0.90, 0.90 }, { -0.85, 0.90 } }; glGenBuffers(NumBuffers, Buffers); glBindBuffer( GL_ARRAY_BUFFER, Buffers[ArrayBuffer]); glBufferData( GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW); glVertexAttribPointer(vPosition, 2, GL_FLOAT, GL_FALSE, 0, (const void*)0); glEnableVertexAttribArray(vPosition); } void display(void) { glClear(GL_COLOR_BUFFER_BIT); glBindVertexArray(VAOs[Triangles]); glDrawArrays(GL_TRIANGLES, 0, NumVertices); glutSwapBuffers(); } int main(int argc, char **argv) { glutInit(&argc, argv); glutInitDisplayMode(GLUT_RGBA | GLUT_DOUBLE); glutCreateWindow("GLSL Intro"); glutDisplayFunc(display); glewInit(); if (!glewIsSupported("GL_VERSION_2_0")) { printf("GLSL not supported\n"); exit(EXIT_FAILURE); } glClearColor(0.9,1.0,1.0,1.0); init(); setShaders(); glutMainLoop(); return 0; } }}} [[Image(triangle.png,align=bottom,right,title=Triangles )]] === Exercises #2 === 1. To be able to continue and not get lost introduce shader compiler logs in case of compilation errors by adding the following code into {{{setShaders()}}} right at after vertex shader compilation: {{{ #!c GLint compiled; glGetShaderiv(v, GL_COMPILE_STATUS, &compiled ); if ( !compiled ) { GLsizei len; glGetShaderiv( v, GL_INFO_LOG_LENGTH, &len ); GLchar* log = malloc(sizeof(GLchar)*(len+1)); printf("Vertex Shader compilation failed: %s\n", log); free(log); } }}} Do not forget to repeat the same thing for fragment shader. Add linker debugging {{{ #!c GLint linked; glGetProgramiv(p, GL_LINK_STATUS, &linked ); if ( !linked ) { GLsizei len; glGetProgramiv( p, GL_INFO_LOG_LENGTH, &len ); GLchar* log = malloc(sizeof(GLchar)*(len+1)); glGetProgramInfoLog( p, len, &len, log ); printf("Shader linking failed: %s\n", log); free(log); } }}} Create some error to verify if it works. For general (core) OpenGL errors we can use the following `glcheck()` utility at suspicious places. {{{ #!c #define glcheck() {GLenum s; if ((s=glGetError()) != GL_NO_ERROR) \ fprintf (stderr, "OpenGL Error: %s at line %d\n", \ gluErrorString(s), __LINE__);} }}} 2. Introduce vertex temperature with additional array and buffer at the end off `init()` [[Image(temperature.png,right)]] {{{ #!c GLfloat vertex_temperature[] = {0, 1, 0.2, 0.1, 0.5, 0.9}; glBindBuffer(GL_ARRAY_BUFFER, Buffers[TempBuffer]); glBufferData(GL_ARRAY_BUFFER, sizeof(vertex_temperature), vertex_temperature, GL_STATIC_DRAW); glVertexAttribPointer(tPosition, 1, GL_FLOAT, GL_FALSE, 0, NULL); glEnableVertexAttribArray(tPosition);//toggle this }}} and adding corresponding IDs to `VAOs` and buffers. Replace shaders with {{{ #!c static const GLchar * vertex_shader[] = { "" "attribute float temperature;" // custom variable along with vertex position "varying float t;" // communicate between the vertex and the fragment shader "void main()" "{" " t = temperature;" " gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;" "}" }; static const GLchar * fragment_shader[] = { "vec3 Cool = vec3(0, 0, 1);" // Red "vec3 Hot = vec3(1, 0, 0);" // Blue "varying float t;" // Interpolated by fragment "void main()" "{" " vec3 color = mix(Cool, Hot, t);" // use the built-in mix() function " gl_FragColor = vec4(color, 1.0);" // append alpha channel "}" }; }}} What happens if we don't enable temperature vertex array? Confer [attachment:temperature.c] attached if having troubles. 3. Add additional custom vertex array for the pressure. Change the temperature array to have values in Celsius for water boiling range [0-100]°C. Pressure should be in the range of [0-1] MPa. Scaling to color range [0-1] should be done in shaders. Toggle between them with the keyboard event by using the keys `'p'` and `'t`' that {{{glEnableVertexAttribArray()}}} and {{{glDisableVertexAttribArray()}}} corresponding vertex attribute arrays. == Interactivity == [[Image(teapot.png,right)]] Assemble the following Utah teapot model and attached virtual [attachment:trackball.h] and [attachment:trackball.c] sources from SGI. We use {{{ #!c #include #include #include #include #include #include "trackball.h" GLuint p; // program needs to be global! float lpos[4] = {1, 0.5, 1, 0}; GLfloat m[4][4]; // modelview rotation matrix float last[4], cur[4]; // rotation tracking quaternions int width, height, beginx, beginy; float p1x, p1y, p2x, p2y; void display(void) { GLuint location = glGetUniformLocation(p, "RotationMatrix"); build_rotmatrix(m, cur); glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); glLightfv(GL_LIGHT0, GL_POSITION, lpos); if( location >= 0 ) glUniformMatrix4fv(location, 1, GL_FALSE, &m[0][0]); glutSolidTeapot(0.6); glutSwapBuffers(); } void processNormalKeys(unsigned char key, int x, int y) { if (key == 27) exit(0); } void mouse(int button,int state, int x, int y) { beginx = x; beginy = y; } void motion(int x,int y) { p1x = (2.0*beginx - width)/width; p1y = (height - 2.0*beginy)/height; p2x = (2.0 * x - width) / width; p2y = (height - 2.0 * y) / height; trackball(last, p1x, p1y, p2x, p2y); add_quats(last, cur, cur); beginx = x; beginy = y; glutPostRedisplay(); } void reshape (int w, int h) { double l = 1; width=w; height=h; glViewport (0, 0, w, h); glMatrixMode (GL_PROJECTION); glLoadIdentity(); glOrtho(-l, l, -l, l, -l, l); glMatrixMode(GL_MODELVIEW); glLoadIdentity(); } static const GLchar * vertex_shader[] ={"\ varying vec3 normal, lightDir;\ uniform mat4 RotationMatrix;\ void main()\ { \ lightDir=normalize(vec3(gl_LightSource[0].position));\ normal=normalize(gl_NormalMatrix*gl_Normal);\ gl_Position = gl_ProjectionMatrix * \ RotationMatrix*gl_ModelViewMatrix*gl_Vertex;\ }"}; static const GLchar * fragment_shader[] ={"\ /* simple toon fragment shader */\ /* www.lighthouse3d.com */\ \ varying vec3 normal, lightDir;\ \ void main()\ {\ float intensity;\ vec3 n;\ vec4 color;\ \ n = normalize(normal);\ intensity = max(dot(lightDir,n),0.0);\ if (intensity > 0.98)\ color = vec4(0.8,0.8,0.8,1.0);\ else if (intensity > 0.5)\ color = vec4(0.4,0.4,0.8,1.0);\ else if (intensity > 0.25)\ color = vec4(0.2,0.2,0.4,1.0);\ else\ color = vec4(0.1,0.1,0.1,1.0);\ gl_FragColor = color;\ }"}; void setShaders() { GLuint v, f; v = glCreateShader(GL_VERTEX_SHADER); f = glCreateShader(GL_FRAGMENT_SHADER); glShaderSource(v, 1, vertex_shader, NULL); glShaderSource(f, 1, fragment_shader, NULL); glCompileShader(v); glCompileShader(f); p = glCreateProgram(); glAttachShader(p,f); glAttachShader(p,v); glLinkProgram(p); glUseProgram(p); } int main(int argc, char **argv) { glutInit(&argc, argv); glutInitDisplayMode(GLUT_DEPTH | GLUT_DOUBLE | GLUT_RGBA); glutInitWindowSize(512, 512); glutInitWindowPosition((glutGet(GLUT_SCREEN_WIDTH)-512)/2, (glutGet(GLUT_SCREEN_HEIGHT)-512)/2); glutCreateWindow("Use mouse to rotate"); trackball(cur, 0.0, 0.0, 0.0, 0.0); glutDisplayFunc(display); glutReshapeFunc(reshape); glutMouseFunc(mouse); glutMotionFunc(motion); glutKeyboardFunc(processNormalKeys); glEnable(GL_DEPTH_TEST); glClearColor(1.0,1.0,1.0,1.0); glewInit(); if (!glewIsSupported("GL_VERSION_2_0")) { printf("GLSL not supported\n"); exit(EXIT_FAILURE); } setShaders(); glutMainLoop(); return 0; } }}} === Exercises #3 === 1. OK, it rotates. But how come the light rotates with the teapot? I'm pretty sure that the light is not rotated while the teapot vertices are. Answer: Take a look into the `normal`. You need to rotate the vertex normal in the `vertex_shader[]` too! With a code like: {{{ #!c vec4 n = RotationMatrix*vec4(gl_NormalMatrix*gl_Normal, 1);\ normal = normalize(n.xyz); \ }}} [[Image(copper-mix.png, right)]] 2. Introduce zoom in/out functionality of the viewer by adding mouse wheel events to the end of `mouse()` {{{ #!c if (button == 3 && state == GLUT_DOWN) { zoom *= 1.1; glutPostRedisplay(); } else if (button == 4 && state == GLUT_DOWN) { zoom /= 1.1; glutPostRedisplay(); } }}} and introduction of global variable `float zoom = 1.0;` that is communicated to GPU by additional `uniform float Zoom;` in the `vertex_shader[]`. Last line is replaced by {{{ #!c gl_Position = gl_ProjectionMatrix * RotationMatrix \ * gl_ModelViewMatrix*vec4(Zoom*gl_Vertex.xyz, 1.0); \ }}} In the `display()` we send `zoom` to GPU before drawing the `glutSolidTeapot()` by adding {{{ #!c location = glGetUniformLocation(p, "Zoom"); if (location >= 0) glUniform1f(location, zoom); }}} 3. Simplify the cartoon shader to [http://en.wikipedia.org/wiki/Gouraud_shading Gouraud shader] by using just greyscale {{{gl_FragColor = vec4(intensity);}}} or copper mix {{{ #!c vec4 copper_ambient = vec4(0.191250, 0.073500, 0.022500, 1.000000); \ vec4 copper_diffuse = vec4(0.703800, 0.270480, 0.082800, 1.000000); \ gl_FragColor = copper_ambient + intensity*copper_diffuse;\ }}} == Reading Objects == [[Image(motorBike-subset.png,right)]] Sometimes we hit limitations of the visualisation tools for the data that we want to visualize. For example [attachment:motorBike.obj] from [http://openfoam.org OpenFOAM] contains object groups that we want to show colored separately and not as whole. Neither [http://wci.llnl.gov/codes/visit/ VisIt] and [http://paraview.org ParaView] can read [http://en.wikipedia.org/wiki/Wavefront_.obj_file Wavefront OBJ] file with group separation. We are forced to convert [attachment:motorBike.obj] into bunch of files and read them one by one. The following {{{wavefront.c}}} converts [attachment:motorBike.obj] into 67 files. Try to open them in VisIt and ParaView. {{{ #!c #include #define MaxVertices 400000 #define MaxFaces 400000 #define MaxGroups 100 float vertex[MaxVertices*3]; unsigned int face[MaxFaces*3]; char group_name[MaxGroups][80]; int start_face[MaxGroups]; int vertices = 0; int faces = 0; int groups = 0; void read_wavefront(const char *filename) { char line[80]; FILE *f = fopen(filename, "r"); while(fgets(line, sizeof(line), f)) switch(line[0]) { case 'v': sscanf(&line[1], "%f %f %f", &vertex[vertices+3], &vertex[vertices*3+1], &vertex[vertices*3+2]); ++vertices; break; case 'g': sscanf(&line[1], "%s", group_name[groups]); start_face[groups++] = faces; break; case 'f': sscanf(&line[1], "%d %d %d", &face[faces*3], &face[faces*3+1], &face[faces*3+2]); --face[faces*3]; --face[faces*3+1]; --face[faces*3+2]; ++faces; break; } fclose(f); start_face[groups] = faces; printf("Read %d vertices and %d faces within %d groups from %s\n", vertices, faces, groups, filename); } void write_wavefront(int group_number) { int i = 0; char n[80], *p = group_name[group_number]; while (*p != '%' && *p != '\0') n[i++] = *p++; // remove % from name n[i++] = '.'; n[i++] = 'o'; n[i++] = 'b'; n[i++] = 'j'; n[i] = '\0'; FILE *f = fopen(n, "w"); fprintf(f, "# Wavefront OBJ file\n"); for (i = 0; i < vertices; i++) fprintf(f, "v %g %g %g\n", vertex[i*3], vertex[i*3+1], vertex[i*3+2]); fprintf(f, "g %s\n", group_name[group_number]); for (i = start_face[group_number]; i < start_face[group_number+1]; ++i) fprintf(f, "f %d %d %d\n", face[i*3]+1, face[i*3+1]+1, face[i*3+2]+1); fclose(f); } int main(int argc, char **argv) { int i; read_wavefront("motorBike.obj"); for(i = 0; i < groups; i++) write_wavefront(i); return 0; } }}} [[Image(point-cloud.png,right)]] === Exercises #3 === 1. Insert {{{teapot.c}}} interactivity example into {{{wavefront.c}}} above and save it as {{{motorbike.c}}}. Verify that there are no compile problems and that the {{{main()}}} contains {{{read_wavefront("motorBike.obj");}}}. Disable lengthy wavefront saving in {{{main()}}}. 2. Instead of drawing of the teapot in {{{display()}}} we will draw single vertex array object as a point cloud with {{{ #!c //glutSolidTeapot(0.6); glDrawArrays(GL_POINTS, 0, vertices); }}} To be able to draw this, we need to prepare Vertex Array Object and buffer with {{{init();}}} that is called from {{{main()}}} just before {{{glutMainLoop();}}} {{{ #!c void init() { GLuint VAO[1]; GLuint Buffer[1]; glGenVertexArrays(1, VAO); glBindVertexArray(VAO[0]); glGenBuffers(1, Buffer); glBindBuffer(GL_ARRAY_BUFFER, Buffer[0]); glBufferData(GL_ARRAY_BUFFER, vertices*3*sizeof(GLfloat), vertex, GL_STATIC_DRAW); glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, NULL); glEnableVertexAttribArray(0); } }}} 3. We see that rotation of the motorbike around the front wheel is not really nice. To compensate we translate all points in the {{{vertex_shader[]}}} by adding {{{vec3(-0.75, 0, -0.7);}}} to every vertex in world coordinates and thus moving motor bike to origin. Last part of the {{{vertex_shader[]}}}now reads: {{{ #!c vec3 position = gl_Vertex.xyz + vec3(-0.75, 0, -0.7); \ gl_Position = gl_ProjectionMatrix * RotationMatrix \ * gl_ModelViewMatrix*vec4(Zoom*position, 1.0); \ }}}