miércoles, 28 de diciembre de 2016

Drawing data structures in 3D with Haskell

In this small tutorial I will explain all I can about how to draw a Graph in 3D using OpenGL bindings for Haskell and GLUT. Additionally, I will use the System.Random module to select random 3D points in space for drawing the graph.
  1. Seting up the environment

  2. First, you have to download and install the Haskell platform from:


    For the creation and configuration of a cabal sandbox, I strongly recomend following this tutorial:


  3. Compiling and running the project

  4. Every time you change one or more files from the folder that composes the sandbox, you have to run the following command:

    cabal build

    or 

    cabal run

  5. Defining the data structure to draw

  6. First, we name the module "DataGraph":

    module DataGraph where

    Then, import the required libraries:

    import Graphics.Rendering.OpenGL
    import Graphics.UI.GLUT
    import Data.Graph
    import System.Random


    For convenience, we create the synonym type "GraphPoint", to be a triple of GLfloat (the float type for OpenGL bindings):

    type GraphPoint = (GLfloat,GLfloat,GLfloat)

    Then, we use the built-in function of module Data.Graph "buildG" to generate the graph to be drawn. The function takes an interval (n,m) of ints that will be the graph's vertex set, and a list of directed edges (v1,v2) to build the graph:

    graph1 :: Graph
    graph1 = buildG (0,10) [(1,2),
                            (2,3),
                            (3,1), (3,5), (3,4),
                            (4,1), (4,2),
                            (5,4),
                            (6,2), (6,5),
                            (7,1),
                            (8,2), (8,5),
                            (9,3),
                            (9,4), (9,5),
                            (10,9), (10,8)]



    The function points3D will take every vertex from the graph previously defined, and for each one, it will return a 3D point to be drawn:

    points3D :: Graph -> Int -> Int -> [ GraphPoint ]
    points3D graph r1 r2 = [ getPoint r1 r2 v | v <- (vertices graph) ]


    The next function in the module is getPoint. It takes two int seeds to generate two random lists, and returns a triple, where the x-coordinate is the point divided by 10 (to obtain a float between 0 and 1) and the other two coordinates are random floats also between 0 and 1, extracted as the v-th component of each of the random lists... I think that was not very clear, I will clarify soon:

    getPoint :: Int -> Int -> Data.Graph.Vertex -> GraphPoint
    getPoint r1 r2 v = let
                         randList1 = randomList r1
                         randList2 = randomList r2
                       in ((fromIntegral v)/10, randList1!!v, randList2!!v)


    The function myPoints is just a synonym of points3D, but without the graph parameter in the middle:

    myPoints :: Int -> Int -> [ GraphPoint ]
    myPoints r1 r2 = points3D graph1 r1 r2

    The function myEdges is a list of pairs of GraphPoints, where each component is the point previously generated for the corresponding vertex. The unit cube is just a reference that will be useful when we rotate the graph:

    myEdges :: Int -> Int -> [ (GraphPoint, GraphPoint) ]
    myEdges r1 r2 = [ (getPoint r1 r2 v1, getPoint r1 r2 v2) | (v1,v2) <- (edges graph1) ] ++ unitCube


    The origin is represented by a constant, not to repeat too many times the same triple:

    origin = (0.0, 0.0, 0.0)


    These three functions add one to each component of a point:

    plusX (x, y, z) = (x+1, y, z)
    plusY (x, y, z) = (x, y+1, z)
    plusZ (x, y, z) = (x, y, z+1)

    These functions are used to generate the reference axis:

    unitCube = [ (origin , plusZ origin ), -- +z
                 (plusZ origin , plusX $ plusZ origin), -- +x
                 (plusX $ plusZ origin , plusX origin ), -- -z
                 (plusX origin , origin ), -- -x
                 (origin , plusX origin ), -- +x
                 (plusX origin , plusY $ plusX origin), -- +y
                 (plusY $ plusX origin , plusY origin ), -- -x
                 (plusY origin , origin ), -- -y
                 (origin , plusY origin ), -- +y
                 (plusY origin , plusZ $ plusY origin), -- +z
                 (plusZ $ plusY origin , plusZ origin ), -- -y
                 (plusZ origin , origin ) -- -z
               ]

    Finally, the function randomList takes a seed and returns an infinite list of floats:

    randomList :: Int -> [Float]
    randomList seed = randoms (mkStdGen seed) :: [Float]


More code and graphics comming soon...