Graphics via OpenGL on Haskell

    Introduction

    There is an opinion that Haskell is a language for nerds-mathematicians. Of course, these are all stereotypes, but there are really few tutorials on it, which somewhat impedes the study. There are especially few where real applications are written (the remarkable Real World Haskell stands out here , which, however, is somewhat messy). Therefore, the idea came up to write this tutorial, about one of the least illuminated areas in Haskell - drawing graphics. I will try to make it detailed, but it is assumed that the reader is familiar with the basics of Haskell, especially with the concept of monads. If not, I recommend reading this topic , as well as books that Skiminok advises in the comments on it.
    Disclaimer:work will be done with the experimental library. So do not be surprised at all sorts of perverts, so that everything works.

    Shall we?


    Installation

    In my humble opinion, it’s very nontrivial. Especially for non-system engineers like me.

    Linux (Ubuntu 10.10)

    I don’t know which versions of OpenGL are originally shipped, but I downloaded and compiled GHC and Haskell Platform from here . In the process, I had to download a couple of libraries (I remember exactly, I needed freeglut3-dev). We compile a test case, and - a bummer. The window is shown for a split second and minimized. I launch from nautilus - it works. On the irc-channel, no one wanted to answer this question :) If anyone can suggest a reason, please ask in the comments.

    Windows 7 (x86_64)

    Traditionally more fuss. This tutorial provided a great help in the installation .
    1. We put MinGW . When installing, select the minimum installation. The main thing is not to put MinGW make.

    2. We put MSys . We agree to post install, answer that MinGW is installed, and indicate the path to it. We start msys, we write in the console "gcc --version" and we are convinced that everything works.

    3. Download Freeglut from here , unpack it into Путь_к_MinGW/1.0/home/Имя_пользователя(Parser - goof). Username is the name of your Windows account. After, run msys and write:
    cd freeglut-2.4.0/src/
    gcc -O2 -c -DFREEGLUT_EXPORTS *.c -I../include
    gcc -shared -o glut32.dll *.o -Wl,--enable-stdcall-fixup,--out-implib,libglut32.a -lopengl32 -lglu32 -lgdi32 -lwinm

    Naturally, if the directory is named differently, you need to change it in the command :)
    At the output, we get two files - glut32.dll and libglut32.a. Copy the dll to Системный_диск/Windows/System. If you installed the standard Haskell-platform then that is all (and libglut32.a will not be needed). If everything is somehow different for you (ghc was installed separately, for example) - I refer to the same tutorial so as not to inflate the topic.

    Important: Or you can just use Cabal.

    You can use this piece of code for verification . If everything works, you will see a shaded sphere.

    Workshop

    Haskell and OpenGL are not very harmonious, since almost all actions are performed through monads. Analogs of variables are also used, which are assigned a value through the operator $ = . Let's try to write and compile a primitive program that creates a window with a red background.
    Note: compilation under Windows runs as usual, under Linux the command is usedghc -package GLUT -lglut Program.hs -o Program

    import Graphics.UI.GLUT
    import Graphics.Rendering.OpenGL
    main = do
        getArgsAndInitialize
        createAWindow "Red Window"
        mainLoop
    createAWindow windowName = do
        createWindow windowName
        displayCallback $= display
    display = do
        clearColor $= Color4 1 0 0 1
        clear [ColorBuffer]
        flush
    


    So, we clear the screen with a predefined color. If the window is not cleared, a screenshot of the working environment will be visible in it. It is worth noting the Color4 type - it sets the color in RGBA format, GLFloat (which is the most ordinary 32-bit float) from 0 to 1 is responsible for each color. The monad chain always ends with a call to the flush function . This ensures that the entire chain of actions is sent to the graphics card. Result: It's

    time to display something in the window. This is done through the renderPrimitive function , which takes 2 arguments: the type of the primitive and the coordinates of the vertices. Vertices in 3D space are defined as
    vertex (Vertex3 x y z)
    

    or
    vertex$Vertex3 x y z
    

    OpenGL uses a Cartesian coordinate system:

    Let's try to draw 3 blue dots on a black background:
    import Graphics.UI.GLUT
    import Graphics.Rendering.OpenGL
    main = do
        getArgsAndInitialize
        createAWindow "Points Window"
        mainLoop
    createAWindow windowName = do
        createWindow windowName
        displayCallback $= display
    display = do
        clear [ColorBuffer]
        currentColor $= Color4 0 0.3 1 1
        renderPrimitive Points(
            do
                vertex (Vertex3 (0.1::GLfloat) 0.5 0)
                vertex (Vertex3 (0.1::GLfloat) 0.2 0)
                vertex (Vertex3 (0.2::GLfloat) 0.1 0))
        flush
    

    As you can see, the vertices for rendering are placed in the monad - the only way to draw more than one vertex. Result:

    Since we operate with vertices, not points, it is logical to convert all triplets of points to vertices:
    map (\(x,y,z)->vertex$Vertex3 x y z)

    And convert the resulting monads into one using sequence . However, there is a more delicious syntactic sugar - mapM_, which includes both of these functions:
    mapM_ (\(x,y,z)->vertex$Vertex3 x y z)

    Create a helper module with a handful of syntactic sugar:
    module PointsRendering where
    import Graphics.UI.GLUT
    import Graphics.Rendering.OpenGL
    import Random
    renderInWindow displayFunction = do
        (progName,_) <- getArgsAndInitialize
        createWindow "Primitive shapes"
        displayCallback $= displayFunction
        mainLoop
    getRand::IO Float
    getRand = getStdRandom( randomR (0,1))
    displayPoints points primitiveShape = do
        renderAs primitiveShape points
        flush
    renderAs figure ps = renderPrimitive figure(makeVertx ps)
    makeVertx = mapM_ (\(x,y,z)->vertex$Vertex3 x y z)
    exampleFor primitiveShape
        = renderInWindow (displayExmplPoints primitiveShape)
    displayExmplPoints primitiveShape = do
        clear [ColorBuffer]
        r <- getRand
        currentColor $= Color4 0 0.3 r 1
        displayPoints myPoints primitiveShape
    myPoints
        = [(0.2,-0.4,0::GLfloat)
          ,(0.46,-0.26,0)
          ,(0.6,0,0)
          ,(0.6,0.2,0)
          ,(0.46,0.46,0)
          ,(0.2,0.6,0)
          ,(0.0,0.6,0)
          ,(-0.26,0.46,0)
          ,(-0.4,0.2,0)
          ,(-0.4,0,0)
          ,(-0.26,-0.26,0)
          ,(0,-0.4,0)
          ]
    

    As you noticed, we also defined a list of points, as well as a function that displays the given primitive on these points. Now you can write programs from one line:
    import PointsRendering
    import Graphics.UI.GLUT
    import Graphics.Rendering.OpenGL
    main = exampleFor Polygon
    

    Result:

    Instead of Polygon, you can substitute any other value from ADT PrimitiveMode
    data PrimitiveMode =
    Points
    | Lines
    | LineLoop
    | LineStrip
    | Triangles
    | TriangleStrip
    | TriangleFan
    | Quads
    | QuadStrip
    | Polygon
    deriving ( Eq, Ord, Show )
    

    Instead of a conclusion

    This article barely touched on the basics of mapping through HOpenGL. Behind the scenes were other primitives (such as a circle), transformations, 3D, and much more. If the community is interested, I can write a couple more articles on the topic.

    Sources


    Also popular now: