Pull to refresh

Графика через OpenGL на Haskell

Reading time4 min
Views11K
Введение

Бытует мнение, что Хаскелл — это язык для нердов-математиков. Конечно, это все стереотипы, но туториалов по нему действительно мало, что несколько препятствует изучению. Особенно мало таких, где пишутся реальные приложения (тут выделяется замечательный Real World Haskell, который, впрочем, несколько сумбурен). Поэтому возникла идея написать этот туториал, об одной из наименее освещенных областей в Хаскелле — выводе графики. Постараюсь сделать его подробным, но предполагается, что читатель знаком с основами Хаскелла, особенно с понятием монад. Если нет — рекомендую почитать этот топик, а также книги, которые советует Skiminok в комментариях к нему.
Дисклеймер: работа будет вестись с экспериментальной библиотекой. Так что не удивляйтесь всяким извратам, для того, чтобы все работало.

Shall we?


Установка

По моему скромному мнению, весьма нетривиальна. Особенно для не-системщиков, как я.

Linux (Ubuntu 10.10)

Незнаю, в каких версиях OpenGL либы поставляются изначально, но я скачал и скомпилил GHC и Haskell Platform отсюда. В процессе пришлось скачать пару библиотек (точно помню, был нужен freeglut3-dev). Компилим тестовый пример, и — облом. Окно показывается на доли секунды и сворачивается. Запускаю из наутилуса — работает. На irc-канале на этот вопрос мне никто отвечать не захотел :) Если кто может предположить причину — прозьба высказаться в комментах.

Windows 7 (x86_64)

Традиционно больше возни. Большую помощь в установке оказал этот туториал.
1. Ставим MinGW. При инсталяции выбирайте минимальную установку. Главное, не ставте MinGW make.

2. Ставим MSys. Соглашаемся на post install, отвечаем, что MinGW установлен, и указываем путь к нему. Запускаем msys, пишем в консоли «gcc --version» и убеждаемся, что все работает.

3. Качаем Freeglut отсюда, распаковываем в Путь_к_MinGW/1.0/home/Имя_пользователя (Парсер — лох). Имя_пользователя — это имя вашей учетной записи в винде. После, запускаем msys и пишем:
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

Естественно, если директория названа по-другому, в команде ее надо изменить :)
На выходе получаем два файла — glut32.dll и libglut32.a. Копируем dll в Системный_диск/Windows/System. Если вы устанавливали стандартный Haskell-platform то это все (и libglut32.a не понадобится). Если же у вас все как-то по-другому (ghc ставился отдельно, к примеру) — отсылаю к тому-же туториалу, чтобы не раздувать топик.

Важно: А можно просто воспользоваться Cabal.

Для проверки можете использовать этот кусок кода. Если все работает, вы увидите затененную сферу.

Практикум

Haskell и OpenGL не очень гармонируют, так как практически все действия совершаются через монады. Также используются аналоги переменных, которым присваивается значение через оператор $=. Попробуем написать и скомпилировать примитивную программу, создающую окно c красным фоном.
Примечание: компиляция под виндой проходит как обычно, под линуксом же используется команда ghc -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


Итак, мы очищаем экран предварительно заданым цветом. Если окно не очищать, в нем будут виден скриншот рабочего окружения. Стоит отметить тип Сolor4 — задает цвет в формате RGBA, за каждый цвет отвечает GLFloat (который суть самый обыкновенный 32-битный float) от 0 до 1. Цепочка монад всегда завершается вызовом функции flush. Это гарантирует, что вся цепочка действий отправилась отрисовываться на видеокарту. Результат:

Самое время что-то отобразить в окне. Это делается через функцию renderPrimitive, которая принимает 2 аргумента: тип примитива и координаты вершин. Вершины в 3D пространстве задаются как
vertex (Vertex3 x y z)

или
vertex$Vertex3 x y z

OpenGL использует Декартову систему координат:

Попробуем отрисовать 3 синих точки на черном фоне:
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

Как видите, вершины для отрисовки помещаются в монаду — единственный путь, как отрисовать больше одной вершины. Результат:

Поскольку мы оперируем вершинами, а не точками, логично все триплеты точек конвертировать в вершины:
map (\(x,y,z)->vertex$Vertex3 x y z)

И получившиеся монады преобразовать в одну с помощью sequence. Впрочем есть более вкусный синтаксический сахар — mapM_, который включает в себя обе эти функции:
mapM_ (\(x,y,z)->vertex$Vertex3 x y z)

Создадим вспомогательный модуль с горстью синтаксического сахара:
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)
      ]

Как вы заметили, мы определили еще и список точек, а также фунцию, которая отображает заданный примитив на этих точках. Теперь можно писать программы из одной строчки:
import PointsRendering
import Graphics.UI.GLUT
import Graphics.Rendering.OpenGL

main = exampleFor Polygon

Результат:

Вместо Polygon можно подставить любое другое значение из ADT PrimitiveMode
data PrimitiveMode =
Points
| Lines
| LineLoop
| LineStrip
| Triangles
| TriangleStrip
| TriangleFan
| Quads
| QuadStrip
| Polygon
deriving ( Eq, Ord, Show )

Вместо заключения

Эта статья еле затронула основы отображения через HOpenGL. За кадром остались другие примитивы (типа круга), трансформации, 3D и еще много всего. Если сообществу интересно, могу написать еще пару статей по теме.

Источники

Tags:
Hubs:
Total votes 27: ↑25 and ↓2+23
Comments20

Articles