Drawing a Triangle with Idris and OpenGL
Some time ago I decided to simultaneously learn Idris and 3D graphics programming. Idris had interested me for some time, and drawing things seemed such a nice application...
I took some time, but now there is a binding for OpenGL in Idris, which you can find it here: https://github.com/eckart/gl-idris/
This project is basically a direct binding of the core API. And since I am a beginner in both OpenGL and Idris the code is not very refined at this point. However we can still do some fun things with it - like drawing a triangle. Ok, so that's no so much fun in itself, but the triangle is the OpenGL version of the customary "hello world" program.
It is a rather intricate and verbose "hello world" since even for a single triangle we have to
- install OpenGL
- install some required libraries
- install a a few depencies the Idris code
- open a window
- define the triangle geometry and tell OpenGL about it
- create shader programs that will do the actual rendering
- run the drawing code in an event loop
Until we finally get this:
This post is actually a literate idris file, which you can download and directly execute to produce the above triangle (provided all the dependencies are installed). It is not intended to be an OpenGL tutorial since there are a vast number of OpenGL tutorials available. (I like the tutorials from Learn OpenGL (a series of written tutorials with code in C++) and the video tutorials by Thinmatrix (which is Java, but he does a good job explaining the basic concepts). For some more detailed information about OpenGL concepts see Anton's OpenGL 4 Tutorials .
The source code for this file is available on github in case you want to try it out.
Of Idris' capabilities we will see very little. The OpenGL binding is intended to be a direct and low-level binding of OpenGL, and that means : IO Monad and more IO Monad.
However the binding comes with some additional goodies, which we might see in future posts, but for now we will keep to the basic stuff.
Imports
As mentioned, this post is a literate idris file and we want to produce an executuable that will show us the triangle, so we
need to provide a Main
module with a main
function:
module Main
Of course we need to import the OpenGL binding which lives in the module Graphics.Rendering.Gl
.
import Graphics.Rendering.Gl
import Graphics.Rendering.Config -- bring in some C flags
import Data.Floats -- OpenGL loves floats
To be able to see the "hello world" triangle in a window, we need a some functionality to open a display. This functionality is not provided by the OpenGL binding, since OpenGL itself is also completely agnostic to these things. So we need to use an idris package that does not about windows (and how to handle user input). Fortunately there is such a package and in fact even more than one.
For simplicity and to run in parallel to the many OpenGL tutorials we will use the GLFW library and the Idris bindings for GLFW which are available here: https://github.com/eckart/glfw-idris.
We won't go into detail about how to open the display here. If you are interested you can read this post about it.
import Graphics.Util.Glfw
%flag C "-Wno-pointer-sign" -- suppress an annoying warning
For Idris to be able to compile the executable, we need to pass a C flag to the compiler:
%include C "GL/glew.h"
This line is absolutely necessary and needs to be in the Main module even if it is only used by the bindings from Graphics.Rendering.Gl
.
If we do not include the GLEW library, which provides something like a header with all the OpenGL functions in it, Idris - or rather the C compiler - will not be able to
resolve any of the OpenGL functions.
Actually I have not really understood all the details about, but once the line is present the compile should work.
Vertices
We are now ready to create the triangle.
A triangle can be represented by 3 points. Since we are dealing with 3D we will
provide the points as 3-dimensional vectors (in the mathematical sense), like (1.0, 2.0, -0.5)
.
The components of the vector will represent the values on the x
, y
and z
axis of a cartesian coordinate system.
(Read more about the coordinate systems in OpenGL here
In OpenGL speak, each point in space is usually called a vertex
. For a triangle we need three vertices:
vertices : List Double
vertices = [
-0.8, -0.8, 0.0, -- left
0.0, 0.8, 0.0, -- top
0.8, -0.8, 0.0 -- right
]
For simplicity we store all three vertices in a flat list instead of, say a Vect 3 (Vect 3 Double)
.
The z component usually denotes the depth of the point - how far away the vertex is.
Additionally we choose the values of x and y components from the range of [-1,1]
, because otherwise
OpenGL will not show it.
In addition to the vertices for the position of the corners of the triangle we will provide colors for each vertex.
The colors will be in RGBA order, that is: a red, a green and a blue component followed by an alpha value for transparency. The color component values will range from 0.0 to 1.0.
colors : List Double
colors = [
1.0, 0.0, 0.0, 1.0, -- red
0.0, 1.0, 0.0, 1.0, -- green
0.0, 0.0, 1.0, 1.0 -- blue
]
We now need a way to "upload" our data to the GPU. OpenGL will store the vertex data (positions and colors) in something called a vertex array object (VAO). The VAO is a container or grouping of several vertex array buffer object (VBOs), which is where data really is stored.
For the triangle we need one VAO and two VBOs (one VBO for positions and another VBO for colors).
OpenGL will create the VAOs and VBOs for us and will give us back a handle or location in form of a number. We need to carefully track these numbers, since we will need them for the actual drawing. Furthermore we need to personally deallocate the resources afterwards (much like file handles).
In this tutorial we will use a simple record to store the VAO:
record Vao where
constructor MkVao
id : Int -- VAO location
buffers : List Int -- a list of VBO locations
We could have used dependent types here, but remember - this is just the "hello world" and nothing more.
The first thing to do is to create a VAO and bind it. Binding activates the resource so that we can do things with it. With OpenGl you will be constantly binding and unbinding things. The general rule is: if a resource is not bound you cannot use it.
createBuffers : IO Vao
createBuffers = do
(vao :: _) <- glGenVertexArrays 1 -- allocate a single VAO
glBindVertexArray vao -- and bind it
We now have the location of a VAO. The VAO is bound so we need to create vertex buffer objects to store the position and color data in:
(buffer :: colorBuffer :: _) <- glGenBuffers 2 -- create 2 buffers
glBindBuffer GL_ARRAY_BUFFER buffer -- bind the position buffer
The position data itself now needs to be uploaded to the GPU via the OpenGL API function glBufferData
.
This functon wants a pointer to the data, so we need to copy the Idris data to a C-Array and pass the pointer to this array to OpenGL:
ds <- sizeofDouble -- here be 'malloc' ...
ptr <- doublesToBuffer vertices
glBufferData GL_ARRAY_BUFFER (ds * (cast $ length vertices)) ptr GL_STATIC_DRAW
free ptr -- don't forget this line
This is very ugly and no doubt we could improve this code, but did not want to hide all the ugly stuff that is happening as it will motivate some functionality in future posts.
Having uploaded the buffer data we need to tell OpenGL what structure the data has, in our case we uploaded a byte array with the following properties:
- the real data starts at position 0
- every vertex contains 3 values
- the components of the vertices are of type
Double
- and there is no gap between two vertices
glEnableVertexAttribArray 0
glVertexAttribPointer 0 3 GL_DOUBLE GL_FALSE 0 prim__null
For colors we need to do something very similar:
glBindBuffer GL_ARRAY_BUFFER colorBuffer -- bind the color buffer
ptr2 <- doublesToBuffer colors
glBufferData GL_ARRAY_BUFFER (ds * (cast $ length colors)) ptr2 GL_STATIC_DRAW
free ptr2
glEnableVertexAttribArray 1
glVertexAttribPointer 1 4 GL_DOUBLE GL_FALSE 0 prim__null
With our position and color data now safely on the GPU, we can unbind the VBOs and VAO and return the numbers (i.e the locations) OpenGL has given us in our VAO record :
glDisableVertexAttribArray 0
glDisableVertexAttribArray 1
glBindBuffer GL_ARRAY_BUFFER 0
glBindVertexArray 0
pure $ MkVao vao [buffer, colorBuffer]
Having provided the data we now need to tell OpenGL how to draw it.
Shaders
All the actual drawing in OpenGl will be done by scripts that are also uploaded to the GPU. These scripts are written in a DSL called the "GL Shader Language" (GLSL). A GLSL programm looks a lot like a C program.
The scripts are called shaders and there are different types of shader for different aspects of the actual rendering. For the triangle (and most other uses) we will need a vertex shader and a fragment shader.
Multiple shaders will be linked together to form a program. They will be processed in a defined order and the vertex shader is always the first to be called.
For more information about shaders see here.
This is what our vertex shader looks like:
#version 410 core
layout(location=0) in vec3 in_Position;
layout(location=1) in vec4 in_Color;
out vec4 ex_Color;
void main(void)
{
gl_Position = vec4(in_Position, 1.0);
ex_Color = in_Color;
}
The Vertex shader will be called once for each vertex.
For each call of the vertex shader the variable in_Position
will be set to the i-th positional vertex (which was a 3-dimensional vector, hence the type vec3
), and the variable in_Color
to the corresponding i-th color vertex (a vector of length 4).
As a result the shader will calculate the final position of the vertex, which must be a 4-dimensional vertex. For the triangle we simply return the original unaltered vertex position.
The vertex color is directly passed on to the fragment shader.
The fragment shader itself looks like this:
#version 410
in vec4 ex_Color;
out vec4 out_Color;
void main(void)
{
out_Color = ex_Color;
}
Fragment shaders are responsible for drawing the pixels of each triangle in the VAO. A fragment shader will be called at least once for every pixel that needs to be drawn on screen for a triangle.
Since we have only provided per vertex data for colors, OpenGL will interpolate the color values of the triangle corners, which is why the resulting triangle has pure colors at the corners and blended ones in the middle.
Shaders are provided to OpenGL as strings. Each shader will be compiled and - on successful compilation - linked to final program. As with the vertex data OpenGL will refer to the shaders using integer handles.
And so we define a data type for shaders handles similarily to the VAO:
record Shaders where
constructor MkShaders
shaders : List Int -- individual handles of the shaders
program : Int -- handle / location of the linked program
As with buffers we will allocate a shader before uploading the data:
createShaders : IO Shaders
createShaders = do
vertexShader <- glCreateShader GL_VERTEX_SHADER
vtx <- readFile "shaderHelloWorld.vert"
glShaderSource vertexShader 1 [vtx] [(cast $ length vtx)]
glCompileShader vertexShader
fragmentShader <- glCreateShader GL_FRAGMENT_SHADER
frg <- readFile "shaderHelloWorld.frag"
glShaderSource fragmentShader 1 [frg] [(cast $ length frg)]
glCompileShader fragmentShader
Remember this is tutorial code. In a real application any number of things might go wrong: a shader source file might be missing, the shader might not compile, etc. We will pretend (for now) that everything will work just fine.
When the shaders are compiled they need to be linked (like a normal C program):
program <- glCreateProgram -- allocate a program location
glAttachShader program vertexShader -- attach the shaders
glAttachShader program fragmentShader -- to the program
glLinkProgram program -- and link the program
printShaderLog vertexShader
printShaderLog fragmentShader -- see if something went wrong
Hopefully we now have a shader program, and we can finish the set up:
glUseProgram 0 -- unbinds the shader program
pure $ MkShaders [vertexShader, fragmentShader] program
We have now written code to allocate resources on GPU and symmetrically we need code to deallocate the resources after use.
destroyShaders : Shaders -> IO ()
destroyShaders (MkShaders shaders program) = do
glUseProgram 0 -- unbind the program
traverse (glDetachShader program) shaders -- detach each shader
glDeleteProgram program -- delete the program
destroyBuffers : Vao -> IO ()
destroyBuffers (MkVao vao buffers) = do
glDisableVertexAttribArray 1
glDisableVertexAttribArray 0
glBindBuffer GL_ARRAY_BUFFER 0 -- unbind VBO
glDeleteBuffers 2 buffers -- and delete VBOs
glBindVertexArray 0 -- unbind the VAO
glDeleteVertexArrays 1 [vao] -- ... and delete it
Drawing
Whenever we draw some geometry in OpenGL - in our case a single triangle - we need to bind the VAO containing the mesh data, bind the shader program and tell OpenGL how we would like to draw the vertices.
However that means we need to carry around the VAO and Shader location during the main loop.
data State = MkState GlfwWindow Vao Shaders
OpenGL stores the result of drawing in a frame buffer. This frame buffer is not visible. By default OpenGl uses a front buffer that is currently shown and a back buffer we draw into. After drawing we swap the buffers and only then can we see any changes.
Before we begin drawing in the back buffer we need to clear any previous drawings, like a erasing a white board. In OpenGL we do this by drawing the entire buffer using a single clear color.
draw : State -> IO ()
draw (MkState win vao (MkShaders _ prog)) = do
glClearColor 0 0 0 1 -- set the clear color
glClear GL_COLOR_BUFFER_BIT -- clear the color buffer
glUseProgram prog -- activate the shaders
glBindVertexArray (id vao) -- activate the VAO
glEnableVertexAttribArray 0 -- enable the vertex postion buffer
glEnableVertexAttribArray 1 -- enable the color buffer
Until now we have only done some state management, bringing OpenGL into a state, that a draw
command will actually be
able to draw something.
We can now do the actual drawing:
glDrawArrays GL_TRIANGLES 0 3
This tells OpenGL to draw three vertices (starting with the zero-th vertex) using the Triangles drawing primitive. The drawing primitive needs to match the vertex data. With the triangles primitive, OpenGL draws a triangle out of every three consecutive vertices. There are other options like Lines or Triangle Strips, but we won't go into them.
The only thing left to do is swapping the back and front buffer:
glfwSwapBuffers win
You will notice that back and front buffer swapping is not done by OpenGL itself but by the GLFW library, which makes sense, since we essentially are asking the windowing system to display something else and OpenGL does not deal with displays.
However: this is it! If you made it so far - congratulation.
You could now try this out by typing:
$ idris -p glfw -p gl -p contrib -o hello 2015-09-13-hello-world.lidr
$ ./hello
The rest of the post deals with the main method, the event loop and the display.
For more details about this part see here.
Appendix A: The rest of the code
initDisplay : String -> Int -> Int -> IO GlfwWindow
initDisplay title width height = do
glfw <- glfwInit
glfwWindowHint GLFW_CONTEXT_VERSION_MAJOR 4
glfwWindowHint GLFW_CONTEXT_VERSION_MINOR 1
glfwWindowHint GLFW_OPENGL_FORWARD_COMPAT 1
glfwWindowHint GLFW_OPENGL_PROFILE (toInt GLFW_OPENGL_CORE_PROFILE)
win <- glfwCreateWindow title width height defaultMonitor
-- now we pretend every thing is going to be ok
glfwMakeContextCurrent win
glewInit
info <- glGetInfo
putStrLn info
return win
main : IO ()
main = do win <- initDisplay "Hello Idris" 640 480
glfwSetInputMode win GLFW_STICKY_KEYS 1
glfwSwapInterval 0
shaders <- createShaders
vao <- createBuffers
eventLoop $ MkState win vao shaders
destroyBuffers vao
destroyShaders shaders
glfwDestroyWindow win
glfwTerminate
pure ()
where
eventLoop : State -> IO ()
eventLoop state@(MkState win vao prog) = do
draw state
glfwPollEvents
key <- glfwGetFunctionKey win GLFW_KEY_ESCAPE
shouldClose <- glfwWindowShouldClose win
if shouldClose || key == GLFW_PRESS
then pure ()
else do
eventLoop state