Painting in Guile with OpenGL
(disclaimer: Several bugs were found in this version of the article. I have the updated version somewhere. I'll re-upload it when it's convenient)
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
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 Gazeintotheabyss) (set-display-callback (lambda () (gl-clear (clear-buffer-mask color-buffer)) (swap-buffers)))
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 -
(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
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
which swaps the front and back buffers of a double buffered window
If you comment out
(gl-clear (clear-buffer-mask color-buffer))
(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 Agreensquare!) (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
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
(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
low-level) module, which contains the glLight function, and we need
(gl-enable), which lets us enable certain functionality (such
(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,
(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.
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
(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.EEEE
Fog, distance, and backgrounds
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 Somefoggyteapots) (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
(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!
Also, check out the bi-annual Lisp Gamejam
That's it for this article. Stay sharp! -Muto