The best way to start your HOpenGL journey is to learn how to create the easiest thing that can be done with it: a program that doesn't make anything at all, besides opening a black window. Before we start, let's understand how the source code of a HOpenGL program is structured:
main :: IO () main = do Initialize GLUT Create a window Set up the main configurations of the program Declare callback procedures Indicate loop
Now we'll explain each one of the "blue items" listed above, and how to turn them into real HOpenGL code, in order to create our first program.
This command line concerns about the initialization of one auxiliary tool of our program, called GLUT. By now, do not worry about it. The following code will fit our purposes:
GLUT.init Nothing
Generally, that's the structure of a procedure that creates a window:
createWindow Window_Name Function_To_Be_Called Configuration_List Window_Position Window_Size
The Window_Name
is a String. This is the title that will appear in the top bar of our window (where else would it appear?). Let's leave it as "Hello World"
by now.
When a window is created, we may wish that a function is immediately executed. This is not the case in our example, so we'll leave the field Function_To_Be_Called
simply as
.
The Configuration_List
indicates, among other things, the type of color pallet we are going to use, and the type of window bufferization. Let's leave this field as
to indicate that we are goind to use single buffering and a RGB pallet.
The way single buffering works is this one: everything you draw is directly applied to the memory area indicated by the program window. If you draw a lot of things, one after another, a very irritating "blinking effect" may be observed. On the other hand, if you use double buffering, there are two memory areas with which you work: one that is indicated by the program window (front buffer), and one that is hidden. This hidden memory area, called back buffer, is the one in which you really draw. When you call the function swapBuffers
, these two buffers are swapped. This makes everything that was drawn in the back buffer to appear in the program window, and this won't cause the "blinking effect". In the following lessons I'll show you more examples and explanations.
A RGB pallet indicates that the colors we are going to use will follow the RED, GREEN and BLUE pattern. All colors are created by a composition of these three colors. The black color is created by the "total absence" of colors (0% of red, green and blue) and the white color is created by the "total presence" of colors (100% of red, green and blue). If we use the same percentage to red, green and blue, we'll obtain a color in the gray scale. A RGBA pallet contains an extra information (A or ALPHA) that indicates how opaque a color will be. This will be explained more precisely in the following lessons.
If we wished to use double buffering and a RGBA pallet, the Configuration_List
would be
. But we won't do this, since we nothing is drawn in this first example.
Both the Window_Position
and the Window_Size
are Maybe
type variables. In other words, they may not exist (so we'd use Nothing
as a parameter). In our example, however, the do exist, so we'll use something like Just(parameters)
.
The Window_Position
is measured from the top left corner of the screen, in pixels. The Window_Size
is also measured in pixels. Let
(Just (WindowPosition 100 100)) (Just (WindowSize 300 250))
Notice that the use of WindowPosition
and WindowSize
before the values is necessary. Here you have all the collected code we developed for our example:
main :: IO () main = do GLUT.init Nothing createWindow "Hello World" (return ()) [ Single, GLUT.Rgb ] (Just (WindowPosition 100 100)) (Just (WindowSize 300 250)) Set up the main configurations of the program Declare callback procedures Indicate loop
When we talk about the "main configurations of the program", we are referring to some particular characteristics we'd like our program to have just after it starts. To do this job, we'll create a function (generally called myInit
) that will be called in the main
function. A very used example of myInit
(which we'll also use) is the following one:
myInit :: IO () myInit = do clearColor (Color4 0.0 0.0 0.0 0.0) matrixMode Projection loadIdentity ortho 0.0 1.0 0.0 1.0 (-1.0) 1.0
Do not worry about understanding eveything myInit
does by now. Just keep in mind that the function clearColor
sets the clearing color of the program (in this case, the black color). The last three lines refers to how we are going to "view" the objects in our 3D world (once more, this will be explained in the following lessons).
The use of callback procedures is one of the most powerful characteristics of
Type_Of_Callback_Procedure_1 (Callback_Procedure_1) Type_Of_Callback_Procedure_2 (Callback_Procedure_2) Type_Of_Callback_Procedure_3 (Callback_Procedure_3) ...
Check out the following example:
keyboardFunc (Just (keyboard)) mouseFunc (Just (mouse))
Here, is said that a function called keyboard
will be the callback procedure that manages keyboard routines, and another function, called mouse
, will be the callback procedure that manages mouse routines. Both of these functions must be created by the programmer.
In this first example, we'll use a kind of callback procedure called displayFunc
. It decides what our program must do when anything related to the program window happens (such as maximization, resizing or moving). When we create the window, the funcion indicated by displayFunc
will also be called.
As we said before, the main purpose of this lesson is to create a program that opens an empty black window. Hence, a great function for our displayFunc
callback procedure would be the display
function described below (its name doesn't need to be display
necessarily):
display :: DisplayAction display = do clear [ColorBufferBit] flush
The command
clears the buffer with the current clearing color of our program. You may remember that, in the function myInit
, we said this color would be the black color. Hence, this will be the color used to "paint" the buffer, and everything that was previously drawn in it will be lost. Because we're using single buffering, a call to
will be observed immediately in our window, which will be automatically painted. If we were working with double buffering, however, only the back buffer would be painted, and no difference would be observed in our program window until the buffers are swapped (via swapBuffers
).
The flush
function forces all previous flush
is not done just after the draw commands, it could be the case that the buffers may be swaped before the program draws everything.
Finally, notice that the type returned by display
must be DisplayAction
, because it is a displayFunc
callback procedure. Now we only need to declare display
in the main
function. This is done by the following code line:
displayFunc (display)
To validate our callback procedures, in order to execute them every time an event related to them happens, we need to declare a "loop indication". This is done by a pre-defined function called mainLoop
. We only need to call it in the main function.
Now the only thing left to do is to import modules GL and GLUT. This way, our first example is finally created. Here you have the complete code:
import GLUT import GL myInit :: IO () myInit = do clearColor (Color4 0.0 0.0 0.0 0.0) matrixMode Projection loadIdentity ortho 0.0 1.0 0.0 1.0 (-1.0) 1.0 display :: DisplayAction display = do clear [ColorBufferBit] flush main :: IO () main = do GLUT.init Nothing createWindow "Hello World" (return ()) [ Single, GLUT.Rgb ] (Just (WindowPosition 100 100)) (Just (WindowSize 300 250)) myInit displayFunc (display) mainLoop
By executing this program, the following window will appear:
Now try to remove the following line from our source code:
displayFunc (display)
Execute again our program. Move the window around the screen, resize and maximize it. Can you understand what has happened?