Muto - Blog - Feed


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 tool used for drawing 3D shapes on a computer screen. You can rotate and scale these shapes, as well as illuminate them with lights (and many other coolio 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 don't assume any prior experience in OpenGL or Guile.


Installation

You can try searching your package manager for guile and guile-opengl. You can also download the source packages for Guile and Guile-OpenGL.

Once you have Guile-OpenGL, let's test it out with a short program that displays a black window with no shapes in it. 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)))

You can run the program with guile blank.scm, or from the REPL with (load "blank.scm").

(If you get an error about "swrast" drivers, it may be Nvidia's fault. You may try Nouveau, but I don't know how to make it work under Nvidia. Sorry.)

blank.png

This program does a few things:

Tell Guile to use the (gl) and (glut) modules, which contain the functions we want to use.

Make the window with make-window and give it a name, "Gaze into the abyss", which is a quote by Friedrich Nietzsche, when he talked 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), and
  • (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 clear windows, well, 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)       ; R G B
            (gl-vertex -0.3  0.3)  ; Top left
            (gl-vertex  0.3  0.3)  ; Top right
            (gl-vertex  0.3 -0.3)  ; Low right
            (gl-vertex -0.3 -0.3)) ; Low left
  (swap-buffers))

(make-window "A Green Square!"!)
(set-display-callback (lambda () (draw)))
(glut-main-loop)

greensquare.png

We define our square as a function (draw) and call it in set-display-callback. This makes our code clean (this practice is helpful when you're drawing many things at once).

A square is a polygon, so we use (gl-begin (begin-mode polygon)) to tell OpenGL that we're going to draw a polygon.

Then we 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) ; Top left
(gl-vertex  0.3 -0.3) ; Low right
(gl-vertex  0.3  0.3) ; Top right
(gl-vertex -0.3 -0.3) ; Low left

then we would end up with a funky looking square.

funkysquare.png

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)      ; Green
(gl-vertex -0.3 0.3)  ; Top left

(gl-color 1 0 0)      ; Red
(gl-vertex 0.3 0.3)   ; Top right

(gl-color 0 0 1)      ; Blue
(gl-vertex 0.3 -0.3)  ; Low right

(gl-color 1 0 1)      ; Magenta
(gl-vertex -0.3 -0.3) ; Low left

colorsquare.png

Yeehaw!


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.

(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 () (draw)))
  (glut-main-loop)

noshadepot.png

Wow cool… Wait hold up! This teapot doesn't look 3D at all! We've been smeckledorfed! Well actually we need a light source to illuminate our teapot. 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)

shadepot.png

Here, 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.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)

fog.png

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)


Conclusion

This article, although not a replacement for the Redbook (link below), hopefully helps someone out there. 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.