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
Hello triangle. (full res image at:https://t.co/ZUcxHRUZwW) pic.twitter.com/77xs43Ow3j
— Jari Komppa (@Sol_HSA) September 18, 2019
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
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
in favor of idiomatic type annotations likered, green, blue :: Color3 GLfloat red = Color3 1 0 0 green = Color3 0 1 0 blue = Color3 0 0 1
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
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.