Введение
Бытует мнение, что Хаскелл — это язык для нердов-математиков. Конечно, это все стереотипы, но туториалов по нему действительно мало, что несколько препятствует изучению. Особенно мало таких, где пишутся реальные приложения (тут выделяется замечательный 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 и еще много всего. Если сообществу интересно, могу написать еще пару статей по теме.
Источники
- «HOpenGL – 3D Graphics with Haskell» — Sven Eric Panitz
- Официальный туториал по HOpenGL