Not many people know that I used to do a lot of OpenGL back when I was way younger. I'll say about three or four people know that I wanted to do my MsC in Computer Graphics and Multimedia Applications, but «things happened» and I ended up switching back to good ol' Programming Languages (again).

Just today this showed up in my timeline

Since the Orange Combinator meetups have been suspended due to the COVID-19 situation, I thought I'd try to write an equivalent program using Haskell.

I wanted the program to behave in exactly the same way as the one written in C, raw OpenGL, and SDL. This means no window resizing, use double-buffered rendering for a smoother animation, and exit on window close. I never took the time to learn how to use SDL in Haskell, so I used GLUT. Call me old fashioned.

I felt very proud of myself because it took me about fifteen minutes to come up with this

import Graphics.Rendering.OpenGL 
import Graphics.UI.GLUT 

width, height :: GLint
width  = 640
height = 480

main = do 
  (progname, _) <- getArgsAndInitialize
  initialDisplayMode $= [DoubleBuffered]
  initialWindowSize  $= Size width height
  createWindow "OpenGL Rendering"
  matrixMode         $= Projection
  loadIdentity
  ortho (-w) w (-h) h 1 (-1)
  matrixMode         $= Modelview (0 :: GLsizei)
  loadIdentity
  clearColor         $= black
  displayCallback    $= render
  idleCallback       $= Just render
  mainLoop
    where
      w, h :: GLdouble
      w     = fromIntegral width  / 2
      h     = fromIntegral height / 2
      black :: Color4 GLfloat
      black = Color4 0 0 0 1

render :: DisplayCallback
render = do 
  clear [ColorBuffer]
  rotate oneDegree aroundZ
  renderPrimitive Triangles $ do
    color red   >> vertex bot
    color green >> vertex upr
    color blue  >> vertex upl
  swapBuffers
    where
      red, green, blue :: Color3 GLfloat
      red   = Color3 1 0 0
      green = Color3 0 1 0
      blue  = Color3 0 0 1
      bot, upr, upl :: Vertex2 GLint
      bot   = Vertex2     0 32
      upr   = Vertex2 (-32)  0
      upl   = Vertex2    32  0
      aroundZ :: Vector3 GLfloat
      aroundZ = Vector3 0 0 1
      oneDegree :: GLfloat
      oneDegree = 1

And it runs like so

Weeeeeeeeeeee!

It's not much, but it's honest work. :-)

Comparing OpenGL programming in C/C++ versus using Haskell bindings for OpenGL, there are a few things that make the latter truly enjoyable:

  • Managing OpenGL and GLUT internal state machines resemble «assignment» (the $= operator above) with the added bonus of type safety: you will only be able to change a part of state with a value that has the appropriate type. No more, «ah, I was supposed to use "this" but used "that" instead» family of bugs.

  • There's way less ambiguity thanks to strong types. There are specific types for vertices, vectors, matrices, colors, etc. Type names follow the names used in the «Red Book» but are actually different types -- you can't mix them by accident. I used lots of type annotations just to show that. Had I forfeited full type signatures like

    red, green, blue :: Color3 GLfloat
    red   = Color3 1 0 0
    green = Color3 0 1 0
    blue  = Color3 0 0 1
    
    in favor of idiomatic type annotations like
    red   = Color3 1 0 (0 :: GLfloat)
    green = Color3 0 1 (0 :: GLfloat)
    blue  = Color3 0 0 (1 :: GLfloat)
    

    the resulting program would have been five lines shorter than the equivalent C one. An it would've been even shorter with the annotated values inline, instead of providing those self documenting definitions for colors, vectors, and whatnot. I find the latter way better.

  • Notice how there's no (annoying) glBegin and glEnd block. You will never make the mistake of forgetting either anymore, because you don't have to. The only way to perform OpenGL drawing primitives is exclusively through the renderPrimitive function that will do the glBegin/glEnd for you implicitly -- one less thing to worry about.

  • Visualization actions are first class and compose. You can have functions that are one visualization action (say «draw a cube of size N and color C»), and then combine that as many times as you want, into other visualization actions that you can keep combining. They will not be performed until you do renderPrimitive. This affords lots of flexibility when creating a complex model that's better split in smaller portions. It also helps in separating the inevitable math involved from the actual rendering.

If you're interested in learning more about using OpenGL with Haskell, and related practice, I encourage you to peruse this tutorial.