3d drawing of an opened book
Solarian Developer
My programming ramblings
OpenGL 101: Drawing primitives - points, lines and triangles
Posted on May thirteen, 2013 by Paul
The code for this postal service is on GitHub: https://github.com/sol-prog/OpenGL-101.
This is the second article from my OpenGL 101 serial. In the first commodity we've seen how to open up a window for our OpenGL application with the GLFW library and how to compile and run the code on Windows, Os Ten and Linux. It is time to actually draw something using OpenGL.
First, let me mention that OpenGL is a depression level API, this means that it has no support for drawing circuitous geometrical objects. It is the developer'south job to combine the geometrical primitives from OpenGL in complex shapes and bodies. The basic geometrical primitives that the core OpenGL contour provide to us are points, lines and triangles.
For simplicity, we are going to utilize only ii dimensional drawings in this article, but keep in listen that OpenGL allows united states to represent three dimensional objects, more on this in a future commodity. For at present, permit's effort to draw the four triangles from the side by side figure:
From a geometrical point of view, a triangle is completely defined by the position in space of his three corners or vertices. In OpenGL terminology, a vertex can be seen as a collection of attributes similar position, colour, texture coordinates etc …
In the higher up figure nosotros have iv triangles and twelve vertices. Each vertex from our figure has a position aspect, let'due south ignore for the moment any other possible attribute like a color. We can shop these vertices in a C++ array:
1 GLfloat vertices_position [ 24 ] = { two 0.0 , 0.0 , iii 0.5 , 0.0 , four 0.5 , 0.5 , 5 6 0.0 , 0.0 , vii 0.0 , 0.5 , eight - 0.v , 0.five , ix 10 0.0 , 0.0 , 11 - 0.v , 0.0 , 12 - 0.v , - 0.5 , xiii fourteen 0.0 , 0.0 , 15 0.0 , - 0.5 , xvi 0.5 , - 0.five , 17 };
If you look closely, you may observe that the points are written counterclockwise, this is important to keep in mind. By default, in OpenGL, a triangle with his vertices stored in counterclockwise order is said to be front facing. Why is this distinction important? Nosotros can instruct OpenGL to render only one of the two faces of a triangle surface (the front or the back); the default is to return both faces. Another observation most the in a higher place assortment is that it stores only the x, y coordinates for our triangles, this is considering the z coordinate is goose egg for all twelve vertices.
And so, how does OpenGL draws our triangles, now that we have their vertices in an assortment ? The get-go stride is to transfer the content of the higher up array in a Vertex Buffer Object. A Vertex Buffer Object, or VBO, is a clamper of memory managed past OpenGL, basically information technology is a piece of the memory of your video card.
A VBO needs to be created, allocated and filled with data. We can as well fill the VBO with data in the allotment step:
ane // Create a Vector Buffer Object that will shop the vertices on video retentiveness two GLuint vbo ; 3 glGenBuffers ( 1 , & vbo ); 4 5 // Allocate space and upload the data from CPU to GPU 6 glBindBuffer ( GL_ARRAY_BUFFER , vbo ); 7 glBufferData ( GL_ARRAY_BUFFER , sizeof ( vertices_position ), vertices_position , GL_STATIC_DRAW );
Line 3 from the above piece of lawmaking will create a handle for our VBO, using the glGenBuffers function. Keep in heed that this function can create an array of handles if needed; for our particular instance we accept a single VBO then one handle will suffice. Once a VBO is created, nosotros need to demark information technology in order to modify or use information technology, this is what line vi does with the glBindBuffer part. The last line will allocate space for the VBO and fill it with the content of our vertices_position array.
One time we have our data in a VBO, we can send it through the OpenGL pipeline - a number of steps and transformations through which our vertices will pass; the event is written in a framebuffer. The glfwSwapBuffers() function from GLFW will replace the current, visible framebuffer (the surface of our window), with the effect of the rendering process. A simplified scheme of the OpenGL pipeline is presented in the next figure:
The above effigy is a simplified model of the entire OpenGL pipeline, it doesn't include any of the optional steps. From the point of view of a beginner, what is important in the above scheme are the vertex shader and the fragment shader. A shader is a, typically small, program that is executed on the video card. A shader is written in GLSL, the OpenGL Shading Language, a language similar with C.
A vertex shader is executed for every vertex in a VBO, his role is to, potentially, use various transformations on the vertices position attribute and pass through other attributes similar color, texture coordinates etc … It is the programmer's responsibility to write a vertex shader for every OpenGL based awarding.
The next step, in our simplified model of the OpenGL pipeline, is the Primitive Setup stage that will organize the vertices into geometric primitives (points, lines and triangles) for the adjacent 2 stages.
In the clipping phase, the primitives that lies exterior of the viewing volume are split in smaller primitives. The default viewing volume in OpenGL is a cube, [-1, +1] x [-1, +i] x [-1, +1], with the origin in the eye of the current viewport (a rectangular surface area of our window, for our case the viewport has the aforementioned dimensions as our window), the positive x axis points to the right, the positive y axis points upwards and the positive z axis points toward the viewer. If, for example, one of our triangles corners will exist outside of the viewing volume, say at -2.0, 0, the clipping stage volition dissever this triangle in smaller triangles and remove the triangles that are outside of the viewing volume. This stage is executed by OpenGL.
In the rasterization stage, the primitives that go out the clipping stage are transformed into fragments. Which of these fragments will terminate every bit a pixel value in the concluding framebuffer is decided in the side by side stage of the pipeline. In the book recommended at the end of this article, information technology is suggested to retrieve at these fragments every bit potential pixels.
The fragment shader, also the programmer'southward responsibility, tin potentially determine the fragment final color, discard some fragments, or use texture mapping.
After the fragment shader, the color of a fragment can potentially exist further modified if the depth and stencil tests are enabled, or if blending was enabled.
From our simplified approach signal of view, we are interested in the two mandatory shaders of whatever OpenGL cadre application, the vertex and the fragment shaders. In other words, nosotros need to implement these two shaders if we desire to describe something.
Let's start with the implementation of a simple vertex shader:
ane #version 150 two three in vec4 position ; 4 v void main () { 6 gl_Position = position ; 7 }
The first line of the above shader specifies the shader linguistic communication used, for OpenGL 3.two the corresponding shader language version is 1.l. Side by side, nosotros accept a global variable of type vec4 that volition receive the position of a vertex, a GLSL vector that tin shop four values, by default this is initialized with (0,0,0,1). A small note here, OpenGL represents internally any vertex position as a iv value vector, we are going to talk more near this in a future article about Math in OpenGL :). From our point of view, if nosotros ship a two value ten, y position to the shader, the last ii numbers volition remain with the default values of 0 and i. Whatsoever shader needs a primary function, like the one declared in line v. Line 6 sets the value of an internal variable from GLSL, gl_position, to the value of our vertex position.
Adjacent, we nowadays a unproblematic fragment shader:
1 #version 150 2 3 out vec4 out_color ; 4 5 void main () { 6 out_color = vec4 ( 1.0 , 1.0 , 1.0 , i.0 ); 7 }
Line iii of the fragment shader defines a global variable that will exist used to set up the color of every fragment to white, run into line 6. Every bit mentioned in the offset commodity of this series, OpenGL uses internally a four dimensional color space, RGBA, in this space 1.0, ane.0, 1.0, i.0 represents white opaque (no transparency).
For maximum flexibility, nosotros are going to save the above two shaders in 2 divide files vert.shader and frag.shader. Some authors continue the shaders in C-style strings in their lawmaking, while this avoids the need to read the shader files from the disk it will also require the recompilation of the entire awarding for any small modify in the shaders. The existent problem with the shader code stored as string in the C++ code is that when you have an error in your shader lawmaking it could be a chip difficult to find the corresponding error line. Another disadvantage is that you don't have syntax highlighting in what your editor interprets equally a constant string.
Commencement, we are going to demand a function to read the shader from the disk, nothing spectacular here, simply open a file and read the content, also check if the file is open, y'all tin can meet the complete function on the Github repository:
1 void read_shader_src ( const char * fname , std :: vector < char > & buffer );
Next, nosotros are going to compile the shader, check the upshot of the compilation and print any error message:
1 // Compile a shader 2 GLuint load_and_compile_shader ( const char * fname , GLenum shaderType ) { iii // Load a shader from an external file four std :: vector < char > buffer ; 5 read_shader_src ( fname , buffer ); 6 const char * src = & buffer [ 0 ]; 7 8 // Compile the shader 9 GLuint shader = glCreateShader ( shaderType ); 10 glShaderSource ( shader , i , & src , NULL ); eleven glCompileShader ( shader ); 12 // Cheque the issue of the compilation 13 GLint test ; 14 glGetShaderiv ( shader , GL_COMPILE_STATUS , & test ); 15 if ( ! test ) { 16 std :: cerr << "Shader compilation failed with this message:" << std :: endl ; 17 std :: vector < char > compilation_log ( 512 ); 18 glGetShaderInfoLog ( shader , compilation_log . size (), Zilch , & compilation_log [ 0 ]); 19 std :: cerr << & compilation_log [ 0 ] << std :: endl ; 20 glfwTerminate (); 21 get out ( - ane ); 22 } 23 render shader ; 24 }
The terminal step in beingness able to utilise the shaders is to create a programme from them:
ane // Create a program from ii shaders 2 GLuint create_program ( const char * path_vert_shader , const char * path_frag_shader ) { 3 // Load and compile the vertex and fragment shaders 4 GLuint vertexShader = load_and_compile_shader ( path_vert_shader , GL_VERTEX_SHADER ); 5 GLuint fragmentShader = load_and_compile_shader ( path_frag_shader , GL_FRAGMENT_SHADER ); 6 7 // Adhere the above shader to a program viii GLuint shaderProgram = glCreateProgram (); ix glAttachShader ( shaderProgram , vertexShader ); 10 glAttachShader ( shaderProgram , fragmentShader ); 11 12 // Flag the shaders for deletion 13 glDeleteShader ( vertexShader ); 14 glDeleteShader ( fragmentShader ); fifteen 16 // Link and use the program 17 glLinkProgram ( shaderProgram ); 18 glUseProgram ( shaderProgram ); 19 xx return shaderProgram ; 21 }
What remains to be done in order to successfully utilize the above shader program is to make the connection between the code that runs on CPU and the one that runs on GPU (the shader programme). Basically, nosotros need to connect the input name from the vertex shader to the position attribute of our VBO. We likewise need to bind the output name from the fragment shader.
OpenGL stores the information about the links betwixt the attributes and the VBO in a special variable named Vertex Array Object, or VAO. Once a VAO is created and binded, all the information almost where the information is located is stored in the VAO. This means that nosotros need to bind a VAO before we transfer the data to a VBO or create the shader program and the links between inputs/outputs from the shader program.
Time to put the pieces together. We are going to start with the last code from our terminal article, ex_3.cpp, let's relieve this as ex_4.cpp and start adding the pieces presented higher up, you tin find the complete code on the Github repository for this article as ex_4.cpp.
Starting time, nosotros are going to add the functions used for managing shaders: read_shader_src, load_and_compile_shader and create_program. In the hereafter, I volition probably refactor the above three functions in a separate C++ class.
We'll also do some changes in the main function of our lawmaking:
ane ... 2 three // Initialize GLEW 4 ... 5 6 // Create a vertex array object seven GLuint vao ; 8 ix // Initialize the information to be rendered x initialize ( vao ); eleven 12 // Create a rendering loop thirteen int running = GL_TRUE ; xiv xv while ( running ) { 16 // Display scene 17 display ( vao ); xviii 19 // Pool for events 20 glfwPollEvents (); 21 // Check if the window was closed 22 running = glfwGetWindowParam ( GLFW_OPENED ); 23 } 24 25 // Cease GLFW 26 glfwTerminate (); 27 28 ...
Line vii from the above code creates a VAO, we are going to demand this later, come across the other two highlighted lines for changes.
Line 10 calls a new part, initialize, which is used to store all the constant data from our code. Since we want to draw a static epitome, nosotros could add nigh of the code in this new office. I've also moved the 2 lines of code used for filling the background with red, in a separate function, display.
Let'southward dissect the initialize office a bit:
1 void initialize ( GLuint & vao ) { ii // Apply a Vertex Array Object 3 glGenVertexArrays ( 1 , & vao ); 4 glBindVertexArray ( vao ); v 6 // four triangles to exist rendered 7 GLfloat vertices_position [ 24 ] = { 8 9 ... 10 11 }; 12 13 // Create a Vector Buffer Object that volition shop the vertices on video memory 14 GLuint vbo ; 15 glGenBuffers ( one , & vbo ); 16 17 // Allocate space and upload the data from CPU to GPU 18 glBindBuffer ( GL_ARRAY_BUFFER , vbo ); 19 glBufferData ( GL_ARRAY_BUFFER , sizeof ( vertices_position ), vertices_position , GL_STATIC_DRAW ); 20 21 GLuint shaderProgram = create_program ( "shaders/vert.shader" , "shaders/frag.shader" ); 22 23 // Get the location of the attributes that enters in the vertex shader 24 GLint position_attribute = glGetAttribLocation ( shaderProgram , "position" ); 25 26 // Specify how the data for position tin can be accessed 27 glVertexAttribPointer ( position_attribute , 2 , GL_FLOAT , GL_FALSE , 0 , 0 ); 28 29 // Enable the attribute xxx glEnableVertexAttribArray ( position_attribute ); 31 }
Nosotros enable a VAO the same way as a VBO, except that in this case nosotros are going to apply glGenVertexArrays and glBindVertexArray, see lines 3 and 4 from the above code.
The lawmaking from lines 5 to 19 was already discussed.
With the helper functions we've discussed earlier, the procedure of creating a shader programme was reduced to a single line of code, see line 21. Please note that I keep the shaders in dissever files, in a folder named shaders.
The following line gets the location of the attribute named position that enters in the vertex shader. Line thirty specifies how the data for the position attribute is sent to the shader. The last step that nosotros need to exercise in order to accept a working connection betwixt the shaders and our C++ code is to enable the aspect.
The display role contains two new instructions for actually drawing our four triangles:
1 void display ( GLuint & vao ) { 2 glClear ( GL_COLOR_BUFFER_BIT ); 3 4 glBindVertexArray ( vao ); 5 glDrawArrays ( GL_TRIANGLES , 0 , 12 ); 6 7 // Swap forepart and back buffers eight glfwSwapBuffers (); 9 }
The first highlighted line from the above lawmaking ensures that nosotros are going to use the proper VAO and the adjacent line actually draws the triangles specified in the VBO by 12 vertices:
If nosotros want to draw points instead of triangles from the above vertices, we need to change line 5 from display to:
i glDrawArrays ( GL_POINTS , 0 , 12 );
We also need to enable GL_PROGRAM_POINT_SIZE in the initialize part:
1 glEnable ( GL_PROGRAM_POINT_SIZE );
With the above changes nosotros can control the point size in pixels in the vertex shader with:
1 gl_PointSize = x.0 ;
The default point size in OpenGL is 1.0. With a betoken size of 10px, this is what y'all should see (ex_5.cpp from the Github repository):
What practice yous expect to see if yous change GL_POINTS to GL_LINES ?
By default, OpenGL fills a triangle with colour, information technology is however possible to alter this behavior if we utilise the function glPolygonMode. Copy ex_4 to ex_6 and add this line at the end of the initialize function:
1 glPolygonMode ( GL_FRONT_AND_BACK , GL_LINE );
Now, OpenGL volition draw for united states a wireframe triangle:
It's fourth dimension to add some colour to our triangles. Suppose that we have a color associated with each vertex from our image and that we've generated somehow these colors; nosotros are going to need 12 RGB colors. How can we send them to the fragment shader ? If we look again at our simplified scheme of the OpenGL pipeline, we run into that the information goes first through the vertex shader. This means that nosotros demand to add our 12 colors to the VBO and include a new input variable, for colors, in the vertex shader. After the color information is in the vertex shader, we are going to demand to laissez passer it through for the next stages of the pipeline toward the fragment shader, we will utilize an out variable for this:
1 #version 150 two iii in vec4 position ; 4 in vec4 colour ; 5 out vec4 color_from_vshader ; 6 7 void main () { 8 gl_Position = position ; 9 color_from_vshader = color ; 10 }
Similarly, the fragment shader needs to have an in variable (exist sure to utilize the aforementioned proper noun here equally the name of the output from the vertex shader):
1 #version 150 ii 3 in vec4 color_from_vshader ; 4 out vec4 out_color ; 5 6 void main () { seven out_color = color_from_vshader ; eight }
The out_color variable is implicitly leap to the output colour from the fragment shader.
Dorsum to the C++ code now, we are going to need a new array for the color data, technically we could store the colour data in the same assortment equally the vertex positions. Using a dissimilar assortment, will let every bit try a new technique for transferring data to the VBO. Suppose the color information is stored in an array named colors.
Beginning, nosotros demand to classify plenty infinite for the VBO to store both the positions and the colors:
1 glBindBuffer ( GL_ARRAY_BUFFER , vbo ); ii glBufferData ( GL_ARRAY_BUFFER , sizeof ( vertices_position ) + sizeof ( colors ), NULL , GL_STATIC_DRAW );
We could employ glBufferSubData to transfer vertices_position and colors to the VBO:
1 // Transfer the vertex positions: 2 glBufferSubData ( GL_ARRAY_BUFFER , 0 , sizeof ( vertices_position ), vertices_position ); 3 4 // Transfer the vertex colors: five glBufferSubData ( GL_ARRAY_BUFFER , sizeof ( vertices_position ), sizeof ( colors ), colors );
The second argument of glBufferSubData is the offset at which the data is located, because nosotros put the position information beginning in the VBO, the offset is 0. When we need to put the colour data, the offset is equal to the size of the data that is already in the VBO sizeof(vertices_positons) in our example. The third parameter is the size of the data transferred and the 4th parameter a arrow to the data.
Now, we demand to add a new attribute, the colour attribute, that enters the vertex shader:
1 // Color attribute ii GLint color_attribute = glGetAttribLocation ( shaderProgram , "color" ); 3 glVertexAttribPointer ( color_attribute , 3 , GL_FLOAT , GL_FALSE , 0 , ( GLvoid * ) sizeof ( vertices_position )); four glEnableVertexAttribArray ( color_attribute );
The last argument from glVertexAttribPointer is a pointer to where the commencement element of the aspect is located.
For a more interesting effect, we can make full colors with random numbers from 0 to 1:
1 GLfloat colors [ 36 ]; ii 3 // Initialize the random seed from the system time 4 srand ( fourth dimension ( NULL )); 5 6 // Fill up colors with random numbers from 0 to 1, employ continuous polynomials for r,g,b: 7 int k = 0 ; eight for ( int i = 0 ; i < sizeof ( colors ) / sizeof ( bladder ) / three ; ++ i ) { ix float t = ( bladder ) rand () / ( bladder ) RAND_MAX ; 10 colors [ 1000 ] = 9 * ( one - t ) * t * t * t ; xi chiliad ++ ; 12 colors [ chiliad ] = 15 * ( one - t ) * ( 1 - t ) * t * t ; xiii k ++ ; 14 colors [ k ] = viii.v * ( 1 - t ) * ( 1 - t ) * ( one - t ) * t ; 15 thou ++ ; 16 17 }
The complete code for the last practice can be found on the Github repository for this article equally ex_7.
If y'all run the code, you should see an paradigm similar with this one:
Suppose at present that we want to draw a foursquare with the lower left corner at -0.five, -0.5 and the upper right corner at 0.five, 0.five. Nosotros can depict a square as a reunion of two triangular surfaces, basically we could use a strategy similar with the one we've used earlier, except that in this example we are going to take six vertices instead of twelve:
1 // i square (made by 2 triangles) to be rendered 2 GLfloat vertices_position [ 12 ] = { iii - 0.5 , - 0.5 , 4 0.5 , - 0.v , v 0.5 , 0.v , 6 seven 0.5 , 0.5 , 8 - 0.five , 0.5 , 9 - 0.5 , - 0.5 ten };
In this case, the colors array will have 18 elements. Another change that should be made is in the display function, glDrawArrays should draw now 6 vertices. You can run across the consummate lawmaking every bit ex_8.cpp on Github.
If you lot run the code, you volition run into an paradigm like with this one:
Starting time thing that we should notice from the above image, is that the color is not smoothly distributed on the square surface, this is because the vertices -0.five, -0.5 and 0.5, 0.v are repeated in our listing of vertices and have different colors (see the manner the color list is randomly generated).
Please note that we apply sizeof(GLfloat)∗12 bytes for storing the vertices and sizeof(GLfloat)∗xviii to shop the colors, this means that we need 120 bytes to store the information for cartoon a square.
A ameliorate way to stand for our information will be to store the triangles as an array of indices pointing to an array of vertices, basically we will have one index per vertex:
one // 1 square (made past two triangles) to be rendered ii GLfloat vertices_position [ viii ] = { iii - 0.5 , - 0.5 , 4 0.5 , - 0.5 , 5 0.v , 0.5 , 6 - 0.5 , 0.v , 7 }; 8 9 GLuint indices [ 6 ] = { x 0 , ane , two , eleven 2 , 3 , 0 12 }; 13 14 GLfloat colors [ 12 ]; xv sixteen ...
In this case, the array of vertices will take unique entries, no vertices is repeated, this will also effect in a reduction in the memory used to store the data for drawing a square. The memory consumption is now sizeof(GLfloat)∗8 for the vertices, sizeof(GLuint)∗half dozen for the indices and sizeof(GLfloat)∗12 for the colors, on my car this is 104 bytes. This may not sound like much for this particular example, but for a complex 3D scene the reduction in the memory consumption can be significative. Take as example a 3D cube, nosotros accept 6 faces and 8 vertices, using the first method our listing of vertices will demand to accept 36 entries (in this case the memory consumption will exist even larger because nosotros'll demand to explicitly shop three coordinates for each vertex), while with the second, more than constructive method, the list of vertices will take just 8 entries.
The side by side figure exemplifies meliorate how nosotros can go the position for a square corner using the indices and the vertices_position arrays:
How do we get the position for a particular triangle ? First we go in the indices array and we go the indices, for example the 2nd triangle volition accept as indices 2, 3, 0. The 2nd pace is to go in the vertices_position array and go the actual position, for alphabetize = 3 we'll have x = 0.v and y = 0.five.
Fortunately for us, OpenGL already has a special command for cartoon data stored equally arrays of indices and vertices, glDrawElements, and a special type of buffer information for storing the indices assortment, GL_ELEMENT_ARRAY_BUFFER.
We'll need to create an Chemical element Assortment Buffer, bind it, classify space and transfer the indices assortment to this buffer:
one // Create an Element Array Buffer that volition shop the indices assortment: 2 GLuint eab ; three glGenBuffers ( one , & eab ); four 5 // Transfer the data from indices to eab half-dozen glBindBuffer ( GL_ELEMENT_ARRAY_BUFFER , eab ); 7 glBufferData ( GL_ELEMENT_ARRAY_BUFFER , sizeof ( indices ), indices , GL_STATIC_DRAW );
And the draw command is at present:
1 glDrawElements ( GL_TRIANGLES , half dozen , GL_UNSIGNED_INT , 0 );
The complete lawmaking for this example is on the Github repository for this article every bit ex_9.cpp.
If you run the lawmaking, yous should see an epitome similar with this one:
In the next tutorial, we are going to larn how to work with textures in OpenGL and how to load an paradigm from the disk, with FreeImage.
All posts from this series:
- OpenGL 101: Windows, OS X and Linux - Getting Started
- OpenGL 101: Cartoon primitives - points, lines and triangles
- OpenGL 101: Textures
- OpenGL 101: Matrices - projection, view, model
If you are interested to learn more near OpenGL, I would recommend reading OpenGL SuperBible past G. Sellers, South Wright and N. Haemel:
or
OpenGL Programming Guide by D. Shreiner, G. Sellers, J. M. Kessenich, B. M. Licea-Kane:
Source: https://solarianprogrammer.com/2013/05/13/opengl-101-drawing-primitives/
Post a Comment for "3d drawing of an opened book"