You are on page 1of 8

Implementing Shaders and the Graphics Pipeline

Let’s implement this graphics ‘pipeline’ thing. As we saw in the note, we do most of the
computation in GPU. In this lab, we will draw a triangle and this triangle will be a base for most
of the 3D graphics that we will draw. As a starter code, please take a look at basic_shader.py
file which has the almost empty functions of init, draw, and main.
This lab manual could be long and it's long because of the explanation. The actual work is very
small as always. Please try to understand each point, the assignments from now on will get
harder after this. If you don’t understand them, ask your friend or the lab instructor.
First let’s list the main tasks in drawing a triangle using shaders using the pipeline.
1. Create a “shader” program, compile and load it in OpenGL
2. Create our triangle vertices and load it to GPU
3. Draw the vertexes
Only 3, pretty simple, huh? If you already understand those tasks and can implement them,
please wait for lab 5. Don’t forget, Task 1, 2 are written under the init function. Task 3 is written
under the draw function.
Let’s do each of the tasks one by one.

Create a “shader” program, compile and load it in OpenGL


Shaders are written with the Shader programming Language and we will see them in detail in
the next lab. For this lab, there are two shaders files, triangle.vertex.shader,
triangle.fragment.shader in the shaders folder. If you are curious, please open them up with any
text editor and take a look at them. No doubt you will understand them, but we will see them
deeply.
a. Our first task is to load the content of the file. Simply, we read the file with the python
file reader function. But let’s have a function that reads the content of a file and returns the file’s
content.

Since we work with many shaders, this function returns the shaders’ file contents whenever we
want and this function loads contents from the shaders folder with the given filename. Don’t
forget to import the os module.
Now let’s read both vertex and fragment shaders using the above getFileContents function.

if you print vertexShaderContent and fragmentShaderContent you will see the shaders’ source
code. Make sure you do that before passing to the next.
b. Having the contents of the shaders, let’s compile them in OpenGL with the
compileShader function in the OpenGL.GL.shaders module which means you have import the
function from this module

We compile the vertex shader content as GL_VERTEX_SHADER program and the fragment
shader content as GL_FRAGMENT_SHADER. These two, GL_VERTEX_SHADER and
GL_FRAGMENT_SHADER are generally called flags and they notify the GPU to treat the inputs
(vertexShaderContent and fragmentShaderContent) in a special way. GPUs support many
shaders and among those shaders, we are telling it the specific shader we want to compile with.
c. Finally we create a program that can run and attach the compiled shaders to it.

The code is self-explanatory. The glLinkProgram is a function that brings different files under a
single executable file for our program to work properly.
Congrats, you finished the first task. You should have the init() function like below.
Create our triangle vertices and load it to GPU
a. First thing we do is create triangle vertices in numpy.

This array contains three vertices, the rows and (x, y, z) of the point, the columns. Z = 0.0 for all
points since we don’t need it for now.
b. To load this on our GPU first we have to create a space on the GPU. If you want to
move out from your current home, you have to make sure you have a new home, right?
Otherwise, where would you put your stuff?
To create a space on a GPU, we first create a name for our space, then bind this name with a
vertex buffer object (VBO).

VBOs are OpenGL objects that deal with vertex data(position, color….etc). We can manage
vertex data using VBO. In this case, we upload our numpy vertexes to GPU using VBO

The glGenBuffers creates only unused names for our VBOs. It takes an integer, which defines
how many VBOs we want. In our case, it gives us one VBO name.
The glBindBuffer function binds the VBO’s name to a target buffer. In our case, the target buffer
is GL_ARRAY_BUFFER and there are many buffers. In OpenGL, we work with one buffer of a
kind at a time. So, the glBindBuffer breaks if the array buffer is bound and binds our now VBO to
a buffer.
Now we load our vertexes to the GPU using glBufferData

this glBufferData uploads the vertices to our new space in GPU.


It accepts four arguments. The first argument is what kind of buffer is the space. The second is
how many bytes of data. Vertexes.nbytes gives us how many bytes we have in the vertices
array. The third is the actual vertices we want to copy, which is our numpy vertexes. The fourth
tells how often these data changes and for now don’t mind what it is that much.

Now we loaded our vertexes to our GPU, but how should the vertices be interpreted? We give it
x, y and z but what if we also included color data. In that case we introduce a new object that
handles this just like VBO handles buffers, a vertex array object (VAO). This VAO handles
everything about arrays, how many elements it contains, what data type it has …

we create name for our VAO with glGenVertexArrays just like we did for our VBO

we bind the VAO to a vertex array object with glBindVertexArray


glVertexAttribPointer is the function that defines an array of generic vertex data. Somehow this
function is related to the shader we write, so let’s jump to it for now and we will discuss it briefly
later. For now, just get the idea that it interprets the data we load on the GPU

finally, we enable the vertex attribute array with glEnableVertexAttribArray function, we also
discuss this later with the glVertexAttribPointer.

Now we have everything we need. We did set up our shaders, we also loaded the data we
needed on the GPU with its interpretation. But we need to address a small concept before
drawing.

Whenever we want to work with VBOs and VAOs, OpenGL gives us the currently bound VBO
and VAO. Let’s say we are drawing a triangle and a cube. These two objects have different
VBO, probably the same VAO. If we are currently working on the cube, it means OpenGL is
bound to ‘cube VBO’. If we want to make changes on the triangle, we have to unbind the cube
first, bind the triangle and work with it. This means we have to always unbind the current VBO
after we finish working on it. This helps not to mess up the graphics by applying unwanted
changes on ‘mis-bound’ VBO. Same applies to VAO.

So let’s go and unbind both VAO and VBO.

Yes, you might say these are binding functions. But look at the arguments we gave them. When
bound our VAO and VBO, we gave them trinagleVAO and trinagleVBO but here, we gave them
0 each. Whenever we give it 0, it means we are unbinding any bound OpenGL object.

At this point the whole init() function looks like below.


If you don’t understand any part of the code, please feel free to ask the instructor, since the next
step is dependent on this understanding.
Draw the vertexes
The time you’ve been waiting for. Here are the sub tasks here.
a. Use the shader program we created in init() function
b. Bind the VAO
c. Draw the arrays
d. Unbind the VAO
Each takes only 1 line of code.
Then bring the program and traingleVAO variable we created in the init function, let’s make them
global.
Just after import statements,

add this line to the first line of init and draw functions

since the init function is called before draw, it instantiates both variables.
This will be our draw function

a. We use the shade program we made in init function


b. We bind the traingleVAO
c. We draw the vertices loaded on the GPU with glDrawArrays
It takes three arguments. What we want to draw, where our vertexes start in the loaded
array and how many vertex we want to draw
d. Unbind the the current VAO
If you run your program and there is no error you will see the following image
It’s a simple triangle but you will see this simple triangle can be used to create any object we
want.

You might also like