Painting in Guile with OpenGL

by Muto — Wed 05 December 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.

Installation

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

guile blank.scm

or within a Guile REPL:

(load "blank.scm")

(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:

(the fog color is the same issue as with the light position. I can only switch between red and black fog)

Conclusion

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