It turns out that using OpenGL from Arc is harder than I expected, but possible, given enough hacking on Arc's underlying Scheme implementation. In this article, I discuss how I got OpenGL to work.
Challenge 1: How to access libraries from Arc
The first challenge is that the official Arc release does not let you access Scheme libraries or functions. Not at all. Even though Arc is implemented on top of MzScheme, there is no mechanism to access the underlying MzScheme implementation. If you want to access Scheme, you must actually modify the Arc language implementation by hacking on the Scheme code that implements Arc.The unofficial Anarki implementation is a modified version of Arc that provides access to Scheme (as well as many other useful improvements). However, I decided to base my OpenGL project on the offical Arc implementation rather than Anarki.
I replaced the Arc implementation file ac.scm
with a modified version called arc-gl.scm
that gives me access to the necessary Scheme functions. The relevant Scheme code is:
(require (lib "mred.ss" "mred") (lib "class.ss") (lib "math.ss") (prefix gl- (lib "sgl.ss" "sgl")) (lib "gl-vectors.ss" "sgl")) ; List of functions to export from Scheme to Arc (map (lambda (s) (xdef s (eval s))) '(gl-shade-model gl-normal gl-begin gl-end gl-vertex gl-clear-color gl-clear gl-push-matrix gl-pop-matrix gl-rotate gl-translate gl-call-list gl-flush gl-light-v gl-enable gl-new-list gl-gen-lists gl-material-v gl-viewport gl-matrix-mode gl-load-identity gl-frustum gl-light-v gl-enable gl-end-list gl-scale sin cos)) ; Arc doesn't provide access to vector, so make gl-float-vector ; take individual arguments instead of a vector (xdef 'gl-float-vector (lambda (a b c d) (vector->gl-float-vector (vector a b c d))))First, the code imports the necessary libraries. Next, it uses
xdef
to allow access to the list of specified Scheme functions. I included the OpenGL functions I used; you will need to extend this list if you want additional OpenGL functionality. I also include sin
and cos
; they are missing from Arc, which is almost pervesely inconvenient.
To get my code:
- Download arc-gl.zip.
arc-gl.scm
, the updated Scheme code; arch.arc
, the arch demo in Arc; and gears.arc
, the gears demo in Arc. It is also available from the Anarki git.
Challenge 2: Where is OpenGL?
OpenGL isn't part of the plain vanilla MzScheme that is recommended for Arc, but it is part of DrScheme. Both DrScheme and MzScheme are versions of Scheme under the PLT Scheme umbrella; MzScheme is the lightweight version, while DrScheme is the graphical version that includes MrEd graphics toolbox and the OpenGL bindings for Scheme. Thus:- Download and install DrScheme version 352.
An alternative to OpenGL would be using MrEd's 2-d graphics; I've described before how to add simple graphics to Arc. However, I wanted to use the opportunity to learn more about OpenGL.
Challenge 3: Running an Arc REPL in PLT Scheme
It's straightforward to runas.scm
inside PLT Scheme and get an Arc REPL. However, a problem immediately turns up with OpenGL
An OpenGL animation will lock up as soon as the Arc REPL is waiting for input. The problem is that MrEd is built around an event loop, which needs to keep running (similar to the Windows message loop). When the REPL blocks on a read call, the entire system blocks.
The solution is to implement a new GUI-based Arc REPL instead of the read-based REPL. MrEd provides text fields that can be used to provide non-blocking input. When the input is submitted, the event loop executes a callback, which can then run the Arc code. Of course, the Arc code needs to return reasonably promptly, or else things will be locked up again. However, the Arc code can start a new thread if it needs to do a long-running computation.
The following MzScheme code creates a frame, text-field for output, text-field for input, and a submit button, and executes the submitted code through arc-eval
. It is analogous to the REPL code in ac.scm
. I put this code in arc-gl.scm
.
(define frame (instantiate frame% ("Arc REPL"))) (send frame show #t) (define (cb-submit a b) (on-err (lambda (c) (append-tf (exn-message c))) (lambda () (append-tf (send in-field get-value)) (append-tf (format "~a" (ac-denil (arc-eval (read (open-input-string (send in-field get-value))))))) (send in-field set-value "")))) (define tf (instantiate text-field% ("" frame) (style '(multiple)) (min-width 600) (min-height 150) (enabled #f))) (define (append-tf str) (send tf set-value (string-append (send tf get-value) str "\n"))) (define in-field (instantiate text-field% ("" frame) (style '(single)) )) (define bb (instantiate button% ("submit" frame) (style '(border)) (callback cb-submit)))To start up Arc with the new REPL:
- Load the new REPL code into the same directory as the original Arc files.
- Run
drscheme
. - Go to Language -> Choose Language -> PLT -> Graphical. Click Ok.
- Go to File -> Open -> arc-gl.scm
- Click Run
Challenge 4: Using Scheme's object model
The mechanism above can't be used to access PLT Scheme's windowing operations, because they are heavily based on the Scheme object implementation, which is implemented through complex Scheme macros. Thus, I can't simply map the windowing operations into Arc, as I did withsin
. If the operations are called directly, they will try to apply Scheme macros to Arc code, which won't work. If they're called after Arc evaluation, the Arc implementation will have already tried to evaluate the Scheme macros as Arc code, which won't work either.
I tried several methods of welding the Scheme objects into Arc, none of which are entirely satisfactory. The first approach was to encapsulate everything in Scheme and provide simple non-object-based methods that can be called from Arc. For example, an Arc function make-window
could be implemented by executing the necessary Scheme code. This works for simple operations, and is the approach I used for simple 2-d graphics, but the encapsulation breaks when the code gets more complex, for example with callbacks and method invocations. It is also unsatisfying because most of the interesting code is written in Scheme, not Arc.
Another approach would be to fully implement Scheme's object model in Arc, so everything could be written in Arc. That was way more difficulty and work than I wanted to do, especially since the object system is implemented in very complex Scheme macros.
My next approach was to implement an exec-scheme
function that allows chunks of Scheme code to be called directly from inside Arc. This worked, but was pretty hacky.
Minor semantic differences between Arc and Scheme add more ugliness. Arc converts #f
to nil
, which doesn't work for Scheme code that is expecting #f
. I hacked around this by adding a symbol false
that gets converted to #f
. Another problem is Arc lists get nil
added at the end; so the lists must be converted going to and from Scheme.
Finally, I ended up with a somewhat hybrid approach. On top of eval-scheme
, I implemented Arc macros to wrap the object operations of send
and invoke
. These macros try to do Arc compilation on the Arc things, and leave the Scheme things untouched. Even so, it's still kind of an ugly mix of Scheme and Arc with lots of quoting. I found writing these macros surprisingly difficult, mixing evaluated and unevaluated stuff.
As an aside, Scheme's object framework uses send foo bar baz
to invoke bar
on object foo
with argument baz
. I.e. foo.bar(baz)
in Java. I found it interesting that this semantic difference made me think about object-oriented programming differently: Scheme objects are getting sent a message, doing something with the message, and providing a reply. Of course, this is the same as invoking a method, but it feels more "distant" somehow.
At the end of this, I ended up with a moderately ugly way of creating Scheme objects from Arc, providing callbacks to Arc functions, and implementing instantiate
and send
in Arc. This isn't a full object implementation, but it was enough to get the job done. For example, to instantiate the MrEd frame%
class and assign it to an Arc variable:
(= frame (instantiate 'frame% '("OpenGL Demo" 'false)))A function to send a refresh message to the canvas:
(def refresh () (send arc-canvas refresh))Defining a subclass of
canvas%
to display the image is uglier as it is Arc code that's mostly in Scheme, but using the Arc callbacks:
(eval-scheme `(define arc-canvas% (class* canvas% () (inherit refresh with-gl-context swap-gl-buffers get-parent) (define/public (run) (,ex-run)) (define/override (on-size width height) (with-gl-context (lambda () (,ex-on-size width height)))) (define/override (on-paint) (with-gl-context ,ex-on-paint)) (super-instantiate () (style '(gl no-autoclear))))))
Finally doing something with OpenGL in Arc
Here's a simple example of an animated arch displayed in OpenGL. To run this example, download the code and start up DrScheme as described above. Then:- From the Arc REPL (not the Scheme REPL) execute:
(load "arch.arc") (motion)
The code, in arch.arc
, has several parts. The arch
function does the OpenGL work to create the Arch out of triangles and quadrilaterals. It uses archfan
to generate the triangle fan for half of the front of the arch. The full code is too long to include here, but the following code, which generates the inside of the arch, should give a taste:
(for i 0 (+ n n -1) (withs (angle (* (/ 3.1415926 n 2) i) angle2 (* (/ 3.1415926 n 2) (+ i 1))) (gl-normal (* r (cos angle)) (* r -1 (sin angle)) 0) (gl-vertex (* r -1 (cos angle)) (* r (sin angle)) z) (gl-vertex (* r -1 (cos angle)) (* r (sin angle)) negz) (gl-normal (* r (cos angle2)) (* r -1 (sin angle2)) 0) (gl-vertex (* r -1 (cos angle2)) (* r (sin angle2)) negz) (gl-vertex (* r -1 (cos angle2)) (* r (sin angle2)) z)))The next section of the code contains the graphics callback functions:
- ex-run: the animation entry point called by the timer. Updates the rotation and refreshes the image.
- ex-on-paint: uses OpenGL commands to draw the arch
- ex-on-size: handles window resize and initial size. The OpenGL model (arch, lighting, projection) is set up here.
I find the arch-generation code somewhat unsatisfying stylistically, as there is a lot of duplicated code to generate the vertices and normal vectors for the front, back, and sides. I couldn't come up with a nice way to fold everything together. I suppose the Arc-y solution would be to write a DSL to express graphical objects, but that's beyond the scope of this project.
Let me mention that low-level OpenGL is not particuarly friendly for exploratory programming. It's tricky to generate complex shapes correctly: it's really easy to end up with the wrong normals, vertices that aren't clockwise, non-convex polygons, and many other random problems. I find it works much better to sketch out what I'm doing on paper first; if I just start coding, I end up with a mess of bad polygons. In addition, I've found that doing the wrong thing in OpenGL will lock up DrScheme and/or crash my machine if I'm unlucky.
The gears example
I also ported the classic OpenGL "gears" demo from Scheme to Arc. This demo includes GUI buttons to rotate the gears. (The animated GIF at the top of the page shows the program in operation.) This is a fairly straightforward port ofgears.scm
that comes with DrScheme. To run it:
- Enter into the Arc REPL:
(load "gears.arc")
The interesting thing to note in gear.arc
is the horizontal-panel%
, vertical-panel%
, and button%
objects that provide the UI controls. They are linked to Arc functions to update the viewing parameters. For example, in the following, note that instantiate
is passing Arc code (using fn
) to the Scheme constructor for button%
. The tricky part is making sure the right things get evaluated in the right language:
(instantiate 'button% (list "Right" h (fn x (ex-move-right))) '(stretchable-width #t))How did I generate the animated gifs? Just brute force: I took some screenshots and joined them into an animated gif using gimp. The real animation is smoother. I found the animated gifs are a bit annoying, so I added JavaScript to start and stop them. The animation is stopped by substituting a static gif.