3D Plot with Clojure and JOGL
Categories: Technical

3D support for Java is made particularly difficult because Java is cross platform and writing a library that abstracts away the differences of writing native 3D applications in Linux, OSX, Windows, etc. is not an easy task. One solution to this is JOGL, which uses Gluegen to generate JNI bindings for C libraries. In the case of JOGL, the C library is OpenGL, a lower level 3D graphics API supported on all of the major platforms.

Because the JOGL library is basically just a wrapper around OpenGL, it is not very object oriented in nature and therefore it’s overkill to write Java when there are many less verbose languages for the JVM, like Groovy, Clojure, Scala, Jython, JRuby, etc. I chose Clojure because I am using it for a project and I needed a simple 3D plot that could scale and rotate for part of the application.

Start by importing the appropriate Swing and JOGL libraries:

(import
'(java.awt Frame Dimension)
'(javax.swing JFrame)
'(java.awt.event MouseMotionAdapter MouseEvent MouseAdapter MouseWheelListener MouseWheelEvent)
'(javax.media.opengl GLCanvas GLEventListener GL GLAutoDrawable)
'(javax.media.opengl.glu GLU)
'(com.sun.opengl.util GLUT))

Next we define point and line data structures:

(defstruct Point2D :X :Y)
(defstruct Point3D :X :Y :Z)
(defstruct Line :X :Y :Z :label)

Now define singletons that JOGL calls will be made on:

(def glu (new GLU))
(def glut (new GLUT))
(def canvas (new GLCanvas))

Now some mutable state for keeping track of click and drag points and a set of lines and vertices to draw in the 3D space:

(def lastDragPoint (ref nil))
(def currentScale (ref 1.0))
(def maxCoordinateValue (ref 100.0))
(def lines (ref []))
(def vertices (ref []))
(def rotationDegreesX (ref 0.0))
(def rotationDegreesY (ref 0.0))
(def rotationDegreesZ (ref 0.0))

Here are the methods for scaling and rotating the plot:

(defn rotateMatrix [#^GL gl]
(if (not= 0.0 @rotationDegreesX) (.glRotated gl @rotationDegreesX 1.0 0.0 0.0))
(if (not= 0.0 @rotationDegreesY) (.glRotated gl @rotationDegreesY 0.0 1.0 0.0))
(if (not= 0.0 @rotationDegreesZ) (.glRotated gl @rotationDegreesZ 0.0 0.0 1.0)))

(defn scaleMatrix [#^GL gl scale]
(if (not= 1.0 scale) (.glScaled gl scale scale scale)))

Now we define a lazy sequence for getting random data points off of a surface, for brevity the getRandomVertex method is omitted:

(defn getSomeVertices ([] (cons (getRandomVertex) (getSomeVertices 1)))
([x]
(lazy-seq
(cons (getRandomVertex) (getSomeVertices 1)))))

Here is a method for drawing vertices, there are similar methods for drawing lines and drawing and labeling the axes in the source file:

(defn drawVertices [#^GL gl vertices]
(if (seq vertices)
(let [vertex (first vertices)]
(if (not (nil? vertex))
(.glVertex3d gl (vertex :X) (vertex :Y) (vertex :Z)))
(drawVertices gl (rest vertices)))))

Here the display method of the GLEventListener for the GLCanvas is what gets called explicitly after a response to any user input. The other way to display the screen would be to use an instance of JOGL Animator which creates an animation thread the refreshes the screen at very high frame rates depending on how fast the underlying graphics hardware is:

(display [#^GLAutoDrawable drawable]
(let [gl (.getGL drawable)]
(doto gl
(.glClear (bit-or GL/GL_COLOR_BUFFER_BIT GL/GL_DEPTH_BUFFER_BIT))
(.glMatrixMode GL/GL_MODELVIEW)
(.glLoadIdentity))
(scaleMatrix gl @currentScale)
(rotateMatrix gl)
(drawAxes gl @maxCoordinateValue)
(drawAllVertices gl @vertices)
(drawAllLines gl @lines)
(drawLabel gl "X" (struct Point3D @maxCoordinateValue 0.0 0.0))
(drawLabel gl "Y" (struct Point3D 0.0 @maxCoordinateValue 0.0))
(drawLabel gl "Z" (struct Point3D 0.0 0.0 @maxCoordinateValue))
(.glFlush gl)))

For the mouse handlers, we updated mutable state in dosync blocks in response to user input. For those who are new to concurrent programming or Clojure, it is worth pointing out that dosync blocks are NOT the same as synchronized blocks in Java. In Java, synchronized blocks are really monitors whose lock object and notification object are one in the same and is either implicitly or explicitly associated with an instance of a class. There are a couple of downsides to this approach. One is that the order in which threads acquire and release locks becomes important, deadlocks can occur and deadlock testing is inherently difficult. Another is that the parts of the program that wish to synchronize on the same data need know how each other’s code handles thread synchronization among the shared objects, or the behavior must be well published enough so that the program will be freedom from deadlock and livelock. Performance also suffers with this approach, because readers block writers, writers block readers, readers block readers, and writers block writers. Clojure, on the other hand, uses an STM implementation that provides an easier model for concurrency. Every write operation to mutable state must happen in a dosync block that runs an all-or-nothing transaction optimistically. In this way writers don’t block writers, with the trade-off being that some operations may have to run more than once. All reads to mutable state provide the current value of the state at the time it is dereferenced, allowing writers to not block writers. And readers do not block writers nor do they block other readers. The order in which dosync blocks are acquired, and nested dosync blocks, are handled by Clojure, and so are no longer important to the programmer. Further, code that mutates and reads the same data can be decoupled and coded without knowledge of each other’s synchronization techniques. Here is the handler for mouse drags, which in this program uses the left mouse button drag to rotate the graph:

(def mouseMotionHandler (proxy [MouseMotionAdapter] []
;Fired once before a sequence of 0 or more mouse drag events
(mouseMoved [#^MouseEvent event]
(dosync (ref-set lastDragPoint (struct Point2D (.getX event) (.getY event)))))
;Fired many times during a mouse drag
(mouseDragged [#^MouseEvent event]
(if (not (nil? lastDragPoint))
(let [deltaX (- (.getX event) (@lastDragPoint :X))
deltaY (- (.getY event) (@lastDragPoint :Y))]
(dosync
(ref-set lastDragPoint (struct Point2D (.getX event) (.getY event)))
(alter rotationDegreesY (fn [x] (+ x (/ deltaX 2.0))))
(alter rotationDegreesZ (fn [x] (+ x (* -1.0 (/ deltaY 4.0)))))
(alter rotationDegreesX (fn [x] (+ x (/ deltaY 4.0)))))
(.display canvas))))))

Finally, here is the initialization code to test the Plot. This creates a Swing JFrame and places a JOGL GLCanvas inside of it:

(defn testPlot3D []
(let [frame (new JFrame "3D Plot")
inputVertices (take 5000 (getSomeVertices))
inputLines (list (struct Line 1.5 1.5 1.0))]
(dosync (ref-set vertices inputVertices) (ref-set lines inputLines) (ref-set maxCoordinateValue 120.0))
(.addMouseListener canvas mouseHandler)
(.addMouseMotionListener canvas mouseMotionHandler)
(.addMouseWheelListener canvas mouseWheelHandler)
(.addGLEventListener canvas canvasEventHandler)
(. canvas (setPreferredSize (new Dimension 800 600)))
(.. frame (getContentPane) (add canvas))
(doto frame
(.setSize 800 600)
(.setDefaultCloseOperation JFrame/EXIT_ON_CLOSE)
(.pack)
(.setVisible true))
(.requestFocus canvas)))

Here is a screenshot on Linux 32-bit with an NVIDIA GForce card:
plot

The complete source for the plot can be downloaded here.

Categories: Technical - Tags: , , ,

1 Comment to “3D Plot with Clojure and JOGL”

  1. Erik says:

    Cool post! The code reminds me of my .emacs ini file :D

Leave a Reply