A first example of connecting Haskell OpenGL code with GLSL is the skeleton of a 2D side-scroller game (think Super Mario Bros.). In these games, levels are often drawn as a set of reusable tiles, and player movement causes a camera to translate across the level. Here's what it will look like,

I will begin by presenting the GLSL code to be used. I am going to be drawing textured 2D triangles to make up the game level. Our vertex shader will take in a 3x3 matrix that captures the 2D transformations our geometry may undergo. A vertex coordinate and a texture coordinate will comprise the vertex input for the shader, which will then output clip-space coordinates for the vertex data ( gl_Position ), and pass the texture coordinates through to the fragment shader.

#version 150 uniform mat3 cam ; in vec2 vertexCoord ; in vec2 texCoord ; out vec2 texCoordFrag ; void main () { texCoordFrag = texCoord; gl_Position = vec4(cam * (vec3(vertexCoord, 1) * 2 - 1), 1); }

The fragment shader simply samples the texture.

#version 150 uniform sampler2D tex ; in vec2 texCoordFrag ; out vec4 fragColor ; void main () { fragColor = texture(tex, texCoordFrag); }

In summary, we have uniform inputs cam and tex . We also have per-vertex inputs vertexCoord and texCoord .

Now we can write some Haskell to feed the GLSL monster. We start by pulling in vinyl , GLUtil , and vinyl-gl in addition to some of the usual suspects.

{-# LANGUAGE DataKinds, TypeOperators #-} import Control.Applicative import Data.Foldable (foldMap, traverse_) import Data.Vinyl import Graphics.GLUtil import Graphics.GLUtil.Camera2D import Graphics.Rendering.OpenGL import Graphics.UI.GLFW ( Key ( KeyEsc )) import Graphics.VinylGL import Linear ( V2 ( .. ), _x, M33 ) import System.FilePath (( </> ))

We also have some helper modules used in these examples. These modules are tied to the GLFW-b package's windowing and input facilities. They serve to kick off our main window with an event loop, and map keyboard input to camera movements.

import Keyboard2D (moveCamera) import Window (initGL, UI ( .. ))

The rendering parts of our application need to refer to some common rendering state. In our case, we just have a 2D transformation matrix that operates on homogenous 2D points, but we use a vinyl record to get used to techniques that let us avoid coupling the rendering state across all rendering functions. Take a look Demo3D.hs example and its associated rendering functions in Geometry.hs to see this in action. Note the association between the field named "cam" and the vertex shader uniform named cam .

type AppInfo = PlainRec '[ "cam" ::: M33 GLfloat ]

Our game is just going to have ground in front of a blue sky background. The ground is made up of columns of dirt tiles, each topped by a grass tile. Given that design, a level in our game is simply a list of column heights. Here we work with game maps made up of columns whose height ranges from 0 to 10.

gameLevel :: [ Int ] gameLevel = [3,3,3,4,5,4,3,3,3,4,5,5,6,7,6,6,6,7,6,5,4,3,3]

We will need to convert a tile height to a 2D square in order to draw the tile on the screen. We define this mapping once so we have a compact game level representation and a consistent-by-construction tile size. Since we are only given the height of the tile to produce, we set its left edge to have an X coordinate of 0. We also here convert our game level's vertical coordinate system of [0,10] to normalized coordinates that range from 0 to 1. The tile function gives us the four vertices of a square for a tile.

tile :: Int -> [ V2 GLfloat ] tile h = let h' = fromIntegral h / 10 in V2 <$> [0,0.2] <*> [h', h' - 0.2]

The tile function can be used to build a column of tiles whose left edge has an X coordinate of 0. We now lay these columns out side-by-side extending from a leftmost edge at the Y axis. Each successive column is placed farther along the positive X axis.