Using OpenGL with Common Lisp

Edit: After posting this article to Reddit I received comments that my OpenGL is stone age and doesn’t reflect modern practice in that uses the slow, inefficient API (“direct mode”) instead of drawing objects using vertex buffers, and doesn’t make use of programmable shaders. Guilty as charged, although I would say that OpenGL 1 still offers the least painful way of getting something to display if you’re not worried about performance. I may post a more “modern” version in the future. In the meantime, the information below offers the path of least resistance, with the above caveats.

OpenGL is an API for three-dimensional computer graphics rendering. Its main advantages over other 3D graphics APIs are that it is cross-platform, an industry standard and published freely (i.e. you don’t need to pay to peek at the documentation!). Another API typically used in conjunction with OpenGL, called GLU (GL Utility library), provides a number of convenience functions for OpenGL. While there are no standard Common Lisp language bindings, the OpenGL and GLU APIs can be used with Common Lisp through a CFFI (C Foreign Function Interface).

Platform-specific things such as managing windows, mouse and keyboard events etc. are not within the scope of OpenGL and must be handled separately. An early library called the GLUT was developed for the purpose of providing a window for OpenGL rendering and mouse and keyboard handling. Nowadays GLFW nowadays provides a more modern alternative. Alternatively, other libraries such as for games and graphical user interface libraries, such as SDL and Qt, provide facilities for OpenGL.

Using OpenGL with Common Lisp

To use OpenGL with Common Lisp, you need the following bits and bobs.

  1. An OpenGL library and drivers installed on your computer. Most desktop computers nowadays ship with an OpenGL C library and 3D graphics hardware drivers already installed. While these are usually proprietary, Mesa provides an open source alternative (but is a software renderer only).
  2. A GLU library if you want to use GLU functions. Most OpenGL library packages also throw in a GLU library as well so if you already have OpenGL installed you probably won’t need to worry about it.
  3. A library that will provide OpenGL windows, events etc., installed where your Common Lisp will find it. (See my blog post for caveats when installing libraries for LispCabinet on Windows.)
  4. Common Lisp bindings for the above.

CLiki lists a number of Common Lisp packages related to OpenGL.

For interfacing to OpenGL and GLU, I use cl-opengl for no other reason than that it was the first I tried and it works. Installation via quicklisp on Mac OS X, Microsoft Windows 7 and Linux is straightforward: just (ql:quickload :cl-opengl) and (ql:quickload :cl-glu) at the REPL. Once they’re installed, with SBCL you can just put (require 'cl-opengl) and (require 'cl-glu) at the top of any source file that uses them and they will be loaded automagically. (As of the time of writing, April 2013, CLISP under Mac OS X 10.8 won’t work because the CFFI library it uses is broken on that platform.)

Interfacing OpenGL with the platform (window system, mouse, keyboard etc.) is where you have the most choice and it partly depends on what you want to do with OpenGL. For a lightweight library that just provides an OpenGL window and input, GLFW appears to fit the bill and has a set of Common Lisp CFFI bindings provided by cl-glfw. While simple to use, I found that under both Linux and Mac OS X, the window didn’t double buffer so there was an annoying flicker when refreshing the graphics. (I’ve not tried it on Windows.)

I therefore instead used the Simple Directmedia Layer (SDL) cross-platform multimedia library and the LispBuilder SDL Common Lisp bindings. SDL is cross-platform, seems well-supported and has good documentation.

A problem with Common Lisp CFFI binding libraries in general is a lack of documentation. This is understandable to some extent – there’s usually documentation and tutorials for the original library (usually in C) and the bulk of the Common Lisp bindings are generated from the C API automatically. However, there’s often not much documentation on how to use the Common Lisp bindings and it seems that more often that not, you’re supposed to work out the mapping convention for function names etc. (and what functions/features have been left out) yourself! Some packages thoughtfully provide examples and limited docs. With the quicklisp package management system, look in the appropriate directory under quicklisp/dists/quicklisp/software below the quicklisp directory itself. Lispbuilder-SDL stands out from the crowd in that it provides installation and download instructions, a user guide, an API reference and a sample code snippet on its web site. Kudos!

Installing Lispbuilder-SDL is explained in its documentation, except that it assumes the use of ASDF rather than quicklisp. There’s a minor hiccup when installing lispbuilder-sdl on Mac OS X. If you download it with quicklisp, there will be an error during compilation because it requires a ‘cocoahelper’ library to be compiled. To fix this problem, go into the lispbuilder-sdl/cocoahelper directory under the appropriate quicklisp directory (e.g. quicklisp/software/lispbuilder-XXXX-svn/lispbuilder-sdl/cocoahelper), type make to build and install the helper, then retry.

A simple framework

The following code is a very simple skeleton program that uses the SDL library to open a window for rendering OpenGL, then draws an image which is updated 20 times a second.

(require 'cl-opengl)
(require 'cl-glu)
(require 'lispbuilder-sdl)

(defun main-loop ()
  (sdl:with-init ()
    (sdl:window 800 800
        :title-caption "OpenGL Test"
        :opengl t
        :opengl-attributes '((:sdl-gl-doublebuffer 1)
                             (:sdl-gl-depth-size 16)))
    (setf (sdl:frame-rate) 20)

    (init-gl)
    (draw-frame)
    (sdl:update-display)

    (sdl:with-events ()
      (:quit-event () t)
      (:video-expose-event ()
        (sdl:update-display))
      (:idle ()
        (draw-frame)
        (sdl:update-display)))))

Using SBCL, if the cl-opengl and Lispbuilder-SDL libraries have been installed using ASDF or quicklisp, they can be pulled by code that requires them using the require expressions at lines 1–3.

(require 'cl-opengl)
(require 'cl-glu)
(require 'lispbuilder-sdl)

When opening a window, you can specify it to be used for OpenGL rendering using :opengl T and specify a list of OpenGL attributes. Here, we enable double buffering and set the depth buffer to 16-bits.

    (sdl:window 800 800
        :title-caption "OpenGL Test"
        :opengl t
        :opengl-attributes '((:sdl-gl-doublebuffer 1)
                             (:sdl-gl-depth-size 16)))

The following code then carries out OpenGL initialisation (set up the viewing parameters, lighting, culling, modelview matrix etc.) by evaluating (init-gl) and draws a frame with (draw-frame). The function sdl:update-display refreshes the window, swapping the front and back rendering buffers.

    (init-gl)
    (draw-frame)
    (sdl:update-display)

Lines 18–24 form the main event loop. The program enters the loop, waits for events (user closes the window, the window is ‘exposed’, mouse movement, key press etc.) and then takes the action specified in the program. :idle event specifies what action to take if no other events are pending. If we set the frame rate (line 12 of the program (setf (sdl:frame-rate) 20)), it results in the :idle handling code being called that many times per second. Here we simply draw the image again (which may have been animated).

    (sdl:with-events ()
      (:quit-event () t)
      (:video-expose-event ()
        (sdl:update-display))
      (:idle ()
        (draw-frame)
        (sdl:update-display)))))

That’s the basic framework of an SDL program. The rest is the OpenGL rendering code and the program logic.

Sample OpenGL Program

ogl-cubeIn response to a request in the comments, I’ve added some OpenGL bones to the framework as a simple demo that draws a spinning cube. The program is presented below.

(require 'cl-opengl)
(require 'cl-glu)
(require 'lispbuilder-sdl)

(defconstant +window-width+  600)
(defconstant +window-height+ 600)

(defconstant +cube-vertices+
  #(#(0 0 0)
    #(0 1 0)
    #(1 1 0)
    #(1 0 0)
    #(0 0 1)
    #(0 1 1)
    #(1 1 1)
    #(1 0 1)))

(defconstant +cube-faces+
  '((#(4 7 6 5) #(0 0 1))
    (#(5 6 2 1) #(0 1 0))
    (#(1 2 3 0) #(0 0 -1))
    (#(0 3 7 4) #(0 -1 0))
    (#(4 5 1 0) #(-1 0 0))
    (#(3 2 6 7) #(1 0 0))))

(defun draw-figure (verts faces)
  (labels ((set-normal (n)
             (gl:normal (aref n 0) (aref n 1) (aref n 2)))
           (set-vertex (index)
             (let ((v (aref verts index)))
               (gl:vertex (aref v 0) (aref v 1) (aref v 2))))
           (draw-face (vertex-indices normal)
             (set-normal normal)
             (gl:begin :quads)
             (map 'nil #'set-vertex vertex-indices)
             (gl:end)))

    (map 'nil #'(lambda (x) (draw-face (first x) (second x))) faces)))


(defun draw-frame (rotx roty rotz)
  (gl:matrix-mode :modelview)
  (gl:push-matrix)
  (gl:translate 0.5 0.5 0.5)
  (gl:rotate rotx 1 0 0)
  (gl:rotate roty 0 1 0)
  (gl:rotate rotz 0 0 1)
  (gl:translate -0.5 -0.5 -0.5)
  (draw-figure +cube-vertices+ +cube-faces+)
  (gl:pop-matrix))
  

(defun start ()
  (let ((rotx 0)
        (roty 0)
        (rotz 0))
    (sdl:with-init ()
      (sdl:window +window-width+ +window-height+ 
                  :opengl t
                  :opengl-attributes '((:sdl-gl-depth-size   16)
                                       (:sdl-gl-doublebuffer 1)))
      (setf (sdl:frame-rate) 10)

      (gl:viewport 0 0 +window-width+ +window-height+)
      (gl:matrix-mode :projection)
      (gl:load-identity)
      (glu:perspective 50 (/ +window-height+ +window-width+) 1.0 10.0)
      (glu:look-at -2 2 4 
                    0.5 0.5 0.5 
                    0 1 0)

      (gl:matrix-mode :modelview)
      (gl:load-identity)

      (gl:clear-color 0 0 0 0)
      (gl:shade-model :flat)
      (gl:cull-face :back)
      (gl:polygon-mode :front :fill)
      (gl:draw-buffer :back)
      (gl:material :front :ambient-and-diffuse #(0.7 0.7 0.7 0.4))
      (gl:light :light0 :position #(0 0 1 0))
      (gl:light :light0 :diffuse #(1 0 0 0))
      (gl:light :light1 :position #(-1 2 -0.5 0))
      (gl:light :light1 :diffuse #(0 1 0 0))
      (gl:enable :cull-face :depth-test
                 :lighting :light0 :light1)

      (gl:clear :color-buffer :depth-buffer)
      (draw-frame rotx roty rotz)
      (sdl:update-display)

      (sdl:with-events ()
        (:quit-event () t)
        (:video-expose-event () (sdl:update-display))
        (:idle
          (setq rotx (mod (+ rotx 2.5) 360.0))
          (setq roty (mod (+ roty 0.7) 360.0))
          (setq rotz (mod (+ rotz 4.4) 360.0))
          (gl:clear :color-buffer :depth-buffer)
          (draw-frame rotx roty rotz)
          (sdl:update-display))))))

The cube vertices are stored in the +cube-vertices+ array. We will draw each face of the cube as a GL quad primitive (four-side polygon), and store the vertex indices and normal vector of each face in +cube-faces+.

(defconstant +cube-vertices+
  #(#(0 0 0)
    #(0 1 0)
    #(1 1 0)
    #(1 0 0)
    #(0 0 1)
    #(0 1 1)
    #(1 1 1)
    #(1 0 1)))

(defconstant +cube-faces+
  '((#(4 7 6 5) #(0 0 1))
    (#(5 6 2 1) #(0 1 0))
    (#(1 2 3 0) #(0 0 -1))
    (#(0 3 7 4) #(0 -1 0))
    (#(4 5 1 0) #(-1 0 0))
    (#(3 2 6 7) #(1 0 0))))

The function draw-figure draws these. There are a couple of points of note regarding the interface provided by cl-opengl. The fact that we are using a C interface binding makes it difficult to pass a Common Lisp array (vector) to functions like glVertex3fv because they expect a pointer; I instead use helper functions set-normal and set-vertex to pass the x, y and z values as separate parameters. It is possible to use vertex buffers with cl-opengl for better performance, but it involves some kludgery with C data structures and their associated memory management — see the example program opengl-array.lisp provided with the cl-opengl release.

(defun draw-figure (verts faces)
  (labels ((set-normal (n)
             (gl:normal (aref n 0) (aref n 1) (aref n 2)))
           (set-vertex (index)
             (let ((v (aref verts index)))
               (gl:vertex (aref v 0) (aref v 1) (aref v 2))))
           (draw-face (vertex-indices normal)
             (set-normal normal)
             (gl:begin :quads)
             (map 'nil #'set-vertex vertex-indices)
             (gl:end)))

    (map 'nil #'(lambda (x) (draw-face (first x) (second x))) faces)))

The draw-frame function draws the cube rotated about the x, y and z axes by specified angles by appropriate manipulation of OpenGL’s modelview matrix (which maps from local object coordinates to world coordinates). We translate to the centre of the cube before applying the rotations so that the cube will be spin around its middle rather than a corner.

(defun draw-frame (rotx roty rotz)
  (gl:matrix-mode :modelview)
  (gl:push-matrix)
  (gl:translate 0.5 0.5 0.5)
  (gl:rotate rotx 1 0 0)
  (gl:rotate roty 0 1 0)
  (gl:rotate rotz 0 0 1)
  (gl:translate -0.5 -0.5 -0.5)
  (draw-figure +cube-vertices+ +cube-faces+)
  (gl:pop-matrix))

Finally, the top-level form. We use Lispbuilder SDL for window management in this case, so we must specify :opengl t and pass any OpenGL attributes as :opengl-attributes when the window is created: here we request a 16-bit depth buffer and double buffering. We also set sdl:frame-rate to set how often the idle event is called in the SDL main event loop.

(sdl:with-init ()
      (sdl:window +window-width+ +window-height+ 
                  :opengl t
                  :opengl-attributes '((:sdl-gl-depth-size   16)
                                       (:sdl-gl-doublebuffer 1)))
      (setf (sdl:frame-rate) 10)

There then follows some OpenGL initialisation to set the viewport to the whole window, and set the viewing (projection) and world coordinate transformation (modelview) matrices. The GLU convenience functions glu:perspective and glu:look-at are used to set the projection and viewing parameters.

      
      (gl:viewport 0 0 +window-width+ +window-height+)
      (gl:matrix-mode :projection)
      (gl:load-identity)
      (glu:perspective 50 (/ +window-height+ +window-width+) 1.0 10.0)
      (glu:look-at -2 2 4 
                    0.5 0.5 0.5 
                    0 1 0)

      (gl:matrix-mode :modelview)
      (gl:load-identity)

We now set the background to be cleared to black with gl:clear-color and configure polygon shading and culling, the cube colour properties and the lighting. The cube is white, and illuminated by a green light and a red light.

  
      (gl:clear-color 0 0 0 0)
      (gl:shade-model :flat)
      (gl:cull-face :back)
      (gl:polygon-mode :front :fill)
      (gl:draw-buffer :back)
      (gl:material :front :ambient-and-diffuse #(0.7 0.7 0.7 0.4))
      (gl:light :light0 :position #(0 0 1 0))
      (gl:light :light0 :diffuse #(1 0 0 0))
      (gl:light :light1 :position #(-1 2 -0.5 0))
      (gl:light :light1 :diffuse #(0 1 0 0))
      (gl:enable :cull-face :depth-test
                 :lighting :light0 :light1)

With those initialisations done, we clear the window and draw the first frame. The function sdl:update-display swaps the OpenGL buffers.

 
      (gl:clear :color-buffer :depth-buffer)
      (draw-frame rotx roty rotz)
      (sdl:update-display)

Finally, we enter the SDL main event loop. Handling the quit-event will cause the main loop to exit when we close the window. The idle event is called at the set frame rate and animates the cube by updating the rotation angles and redrawing it.

  
      (sdl:with-events ()
        (:quit-event () t)
        (:video-expose-event () (sdl:update-display))
        (:idle
          (setq rotx (mod (+ rotx 2.5) 360.0))
          (setq roty (mod (+ roty 0.7) 360.0))
          (setq rotz (mod (+ rotz 4.4) 360.0))
          (gl:clear :color-buffer :depth-buffer)
          (draw-frame rotx roty rotz)
          (sdl:update-display))))))
About these ads

3 thoughts on “Using OpenGL with Common Lisp

  1. Josh

    Very informative! BTW, can you show how to add some gl code to framework? like draw a simple triangle? I think it would be very good if you extend it to lisp opengl3+ tutorial.

    Reply
    1. markbrown778 Post author

      Thanks for your feedback Josh. I don’t have the time to create a full OpenGL tutorial but I’ve expanded the blog post with a sample program that draws a spinning cube. Hope this is of help.

      Reply

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s