Painting in Guile with OpenGL
December 05, 2018
OpenGL is a common tool used for drawing shapes (like squares, circles, cubes, etc.) on your computer screen. You are able to rotate and scale these shapes, as well as illuminate them with lights (and many other interesting graphical tricks)!
Guile-OpenGL is a set of bindings that allow us to access OpenGL's functions from the Guile programming language! This article demonstrates Guile-OpenGL's usage and syntax through short explanations and examples. I do not assume any prior experience of OpenGL or Guile.
If you are using the Guix package manager, you can install both Guile and Guile-OpenGL with:
guix package -i guile guile-opengl
If you are using some other package manager (like Apt, Aur, Yum, or RPM), you can try searching them for Guile and Guile-Opengl. You can also download the source package for Guile and Guile-OpenGL.
Once you have Guile-OpenGL installed, let's test it out with a short program that simply displays a black window with no shapes in it, so create a new file named "blank.scm" and put the following code in it:
(use-modules (gl) (glut)) (make-window "Gaze into the abyss") (set-display-callback (lambda () (gl-clear (clear-buffer-mask color-buffer)) (swap-buffers))) (glut-main-loop)
You can run the program with
or within a Guile REPL:
(disclaimer: if you get an error about "swrast" drivers malfunctioning, it may be because you are using an NVIDIA driver. The program runs fine under Nouveau but I don't know how to make it work under NVIDIA drivers. Sorry.)
This very short program does a few things. We start off by telling Guile which modules to use - (gl) and (glut) - which contain the main OpenGL functions and the OpenGL Utility Toolkit respectively.
We make a window with (make-window). We also give our window a name - "Gaze into the abyss" (which is part of a quote by Friedrich Nietzsche, when he talks about playing Monster Hunter).
We then set a display callback with (set-display-callback), which, to my understanding, says "Determine which window we're going to draw on, then draw on it". We pass two parameters to this function - (gl-clear (clear-buffer-mask color-buffer)), which clears the color buffer (our background). The second parameter is (swap-buffers), which swaps the front and back buffers of a double buffered window
If you comment out (gl-clear (clear-buffer-mask color-buffer)) and/or (swap-buffers), you'll notice that the window becomes clear. We don't want a clear window, nobody likes clear wind- Oh wait... Nobody likes clear windows besides *literal* windows!
Drawing a square
Now that we're done drawing nothing, let's see if we can draw something! A green 2D square is simple enough. Last I checked, a square is a four-cornered polygon. Let's open up a new file (square.scm) and write the following program:
(use-modules (gl) (glut)) (define (draw) (gl-clear (clear-buffer-mask color-buffer)) (gl-begin (begin-mode polygon) (gl-color 0 1 0) (gl-vertex -0.3 0.3) (gl-vertex 0.3 0.3) (gl-vertex 0.3 -0.3) (gl-vertex -0.3 -0.3)) (swap-buffers)) (make-window "A green square!") (set-display-callback (lambda () (draw))) (glut-main-loop)
As you can see, we defined our square as a function (draw) and call it inside set-display-callback. This makes our program look very clean (this practice is helpful when you're drawing many things at once).
Since a square is a polygon, we use (gl-begin (begin-mode polygon)) to tell OpenGL that we're going to draw a polygon. We then tell OpenGL what color we want our square to be (green, since the three numbers after gl-color are Red, Green, and Blue. 1 1 1 is white, 0 0 0 is black), then we point out where our vertices (corners) are going to be. We specify each corner one-by-one in a clockwise (or counter-clockwise) manner. If we place our corners in a sort of X shape, for example:
(gl-vertex -0.3 0.3) (gl-vertex 0.3 -0.3) (gl-vertex 0.3 0.3) (gl-vertex -0.3 -0.3)
then we would end up with a funky looking square.
If we add a (gl-color) line before each vertex, then each corner of our square will be a different color!
(gl-color 0 1 0) (gl-vertex -0.3 0.3) (gl-color 1 0 0) (gl-vertex 0.3 0.3) (gl-color 0 0 1) (gl-vertex 0.3 -0.3) (gl-color 1 0 1) (gl-vertex -0.3 -0.3)
Drawing 3D models
3D modeling is what OpenGL was built for. The OpenGL Utility Toolkit (GLUT) comes with a few 3D models that we can use right off the bat (glutSolidSphere, glutSolidTeapot, glutSolidCube, and more)! Lets try drawing a teapot with glutSolidTeapot. For this we'll need the (glut low-level) module, but don't worry, it's not actually low level. It just gives us access to certain functions that haven't been renamed in the Guile bindings yet (one of which is glutSolidTeapot):
(use-modules (gl) (glut) (glut low-level)) (define (draw) (gl-clear (clear-buffer-mask color-buffer)) (gl-color 1.0 0.1 0.0) (glutSolidTeapot 0.5) (swap-buffers)) (initialize-glut #:window-size '(800 . 800)) (make-window "A lovely red teapot") (set-display-callback (lambda () (init))) (glut-main-loop)
Wow cool... Wait hold up! This teapot doesn't look 3D at all! We've been smeckledorfed! Well actually, as you might have guessed, we need a light source to illuminate our subject. To do this we need the (gl low-level) module, which contains the glLight function, and we need to use (gl-enable), which lets us enable certain functionality (such as lighting).
(use-modules (gl) (gl low-level) (glut) (glut low-level)) (define (init) (gl-enable (enable-cap light0)) (gl-enable (enable-cap lighting)) (gl-enable (enable-cap color-material)) (glLightf (light-name light0) (light-parameter position) 1.0)) (define (draw) (gl-clear (clear-buffer-mask color-buffer) (gl-color 1.0 0.1 0.0) (glutSolidTeapot 0.5))) (define (on-display) (init) (draw) (swap-buffers)) (initialize-glut #:window-size '(800 . 800)) (make-window "Guile OpenGL") (set-display-callback (lambda () (on-display))) (glut-main-loop)
As you can see by this example, we define two functions, (init) and (draw). (init) holds all the (gl-enable) procedures along with the glLightf function, which (draw) does the actual drawing. (on-display) holds all our functions together, which can all be called together in our display callback function.
We enable light0, which is the name of the light source we're going to use (light0, light1, light2, etc. are different light sources we can enable). We also enable lighting, which allows light0 to shine. (color-material) lets us use the red color of our teapot. If you comment out this line, the teapot will be grey.
glLightf (the f at the end stands for "float") lets us change the parameters to our light. We specify the light that we want to change (light0), and tell OpenGL which setting should be changed - in this case, we want to change the position of the light.
(At the time of writing, I am only able to illuminate the subject from the left or right with 1.0 and -1.0. If anybody knows how to use this function correctly, with 3 floats in a list or array, please contact me: shack[at]muto[dot]ca )
Setting fog, distance, and background color
Let's wrap this article up with a somewhat complex program. We're going to draw 5 red teapots all at different distances from the camera, and make the ones further away fade out into fog. I'll make a new file (fog.scm) and write the following program in it:
(use-modules (gl) (gl low-level) (glut) (glut low-level) (glu)) (define (init) (set-gl-clear-color 0.0 0.02 0.02 1.0) (gl-enable (enable-cap light0)) (gl-enable (enable-cap lighting)) (gl-enable (enable-cap color-material)) (gl-enable (enable-cap depth-test)) (gl-enable (enable-cap fog)) (glPushMatrix) (set-gl-matrix-mode (matrix-mode projection)) (gl-load-identity) (glu-perspective 90 1 0.1 20) (set-gl-matrix-mode (matrix-mode modelview)) (glPopMatrix)) (define (my-lighting) (glLightf (light-name light0) (light-parameter position) 1.0) (glFogi (fog-parameter fog-mode) (fog-mode exp)) (glFogf (fog-parameter fog-color) 0.0) (glFogf (fog-parameter fog-density) 1.0) (glHint (hint-target fog-hint) (hint-mode nicest))) (define (red-teapot x y z) (glPushMatrix) (gl-translate x y z) (gl-color 1.0 0.1 0.0) (glutSolidTeapot 0.2) (glPopMatrix)) (define (draw) (gl-clear (clear-buffer-mask color-buffer depth-buffer)) (red-teapot -0.2 -0.2 -0.5) (red-teapot 0.0 0.0 -1.0) (red-teapot 0.4 0.4 -2.0) (red-teapot 1.0 1.0 -2.9)) (define (on-display) (init) (my-lighting) (draw) (swap-buffers)) (initialize-glut #:window-size '(800 . 800)) (make-window "Some foggy teapots") (set-display-callback (lambda () (on-display))) (glut-main-loop)
Okay, so I'll set the new functions in a list:
- (set-gl-clear-color) sets the background color.
- (depth-test) makes sure that all the teapots render correctly.
- (glPushMatrix) keeps the current matrix so that nothing gets done more than once.
- (set-gl-matrix-mode (matrix-mode projection)) allows us to set a projection matrix, which is important for our point-of-view
- (gl-load-identity) replaces the current matrix with the "identity" matrix.
- (set-gl-matrix-mode (matrix-mode modelview)) switches the matrix mode back to modelview, which is used for drawing our teapots.
- (glFogi) lets us change the fog settings. We use exp mode and set the color to black. We set the fog density to 1.0 and the fog hinting to nicest.
- We define our red teapot as a function (red-teapot) with three arguments (x y z), which allows us to use the function red-teapot as many times as we want, which we do 5 times in (draw).
(the fog color is the same issue as with the light position. I can only switch between red and black fog)
This article, although not a replacement for the Redbook (link below), hopefully helps someone out there (maybe it was you!). Guile-OpenGL, although it's not maintained much anymore, is a very efficient and promising set of bindings for OpenGL development in Guile! Procedurally generated images and graphical recursive functions feel very natural in Guile-OpenGL and is a powerhouse for algorithmic creativity!
As for learning resources, I'd recommend the Guile-OpenGL documentation, Common OpenGL mistakes, and of course, the Redbook
Also, check out the bi-annual Lisp Gamejam
That's it for this article. Stay sharp! -Muto