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).
A tweet showed up in my timeline on 2019-09-18. It’s no longer there, but I kept a link to the image showing the insane amount of code to accomplish a simple OpenGL render using C, C++, or Vulkan.
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
height :: GLint
width,= 640
width = 480
height
= do
main <- getArgsAndInitialize
(progname, _) $= [DoubleBuffered]
initialDisplayMode $= Size width height
initialWindowSize "OpenGL Rendering"
createWindow $= Projection
matrixMode
loadIdentity-w) w (-h) h 1 (-1)
ortho ($= Modelview (0 :: GLsizei)
matrixMode
loadIdentity$= black
clearColor $= render
displayCallback $= Just render
idleCallback
mainLoopwhere
h :: GLdouble
w,= fromIntegral width / 2
w = fromIntegral height / 2
h black :: Color4 GLfloat
= Color4 0 0 0 1
black
render :: DisplayCallback
= do
render ColorBuffer]
clear [
rotate oneDegree aroundZTriangles $ do
renderPrimitive >> vertex bot
color red >> vertex upr
color green >> vertex upl
color blue
swapBufferswhere
blue :: Color3 GLfloat
red, green,= Color3 1 0 0
red = Color3 0 1 0
green = Color3 0 0 1
blue upl :: Vertex2 GLint
bot, upr,= Vertex2 0 32
bot = Vertex2 (-32) 0
upr = Vertex2 32 0
upl aroundZ :: Vector3 GLfloat
= Vector3 0 0 1
aroundZ oneDegree :: GLfloat
= 1 oneDegree
And it runs like so
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
blue :: Color3 GLfloat red, green,= Color3 1 0 0 red = Color3 0 1 0 green = Color3 0 0 1 blue
in favor of idiomatic type annotations like
= Color3 1 0 (0 :: GLfloat) red = Color3 0 1 (0 :: GLfloat) green = Color3 0 0 (1 :: GLfloat) blue
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
andglEnd
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 therenderPrimitive
function that will do theglBegin
/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.