HaGL-0.1.0.0: src/Graphics/HaGL/Backend/GLUT.hs
{-# LANGUAGE ScopedTypeVariables #-}
module Graphics.HaGL.Backend.GLUT (
GlutOptions(..),
GlutRunMode(..),
runGlut
) where
import Prelude hiding (id)
import Control.Monad (when)
import Data.Functor.Identity
import Data.IORef
import Data.Time.Clock
import qualified Data.ByteString as BS
import Graphics.Rendering.OpenGL
import Graphics.Rendering.OpenGL.Capture
import Graphics.UI.GLUT
import Graphics.HaGL.Backend
import Graphics.HaGL.GLType
import Graphics.HaGL.GLExpr
import Graphics.HaGL.ExprID
import Graphics.HaGL.GLObj (GLObj)
import Graphics.HaGL.Eval
import Graphics.HaGL.CodeGen (UniformVar(..))
import qualified Graphics.HaGL.Util.DepMap as DepMap
-- | Options specific to a GLUT window
data GlutOptions = GlutOptions {
-- | The position of the window
winPosition :: Maybe (GLint, GLint),
-- | The size of the window
winSize :: (GLsizei, GLsizei),
-- | Whether to draw in fullscreen mode
winFullscreen :: Bool,
-- | The title of the window
winTitle :: Maybe String,
-- | The (background) color to use when clearing the screen
clearCol :: (Float, Float, Float, Float),
-- | The 'GlutRunMode' under which to run the application
runMode :: GlutRunMode,
-- | Any additional OpenGL-specific setup to run just after the window has
-- been set up. The typical use-case is to import the OpenGL bindings
-- ('Graphics.Rendering.OpenGL') and define a @StateVar@ such as @lineWidth@
openGLSetup :: IO ()
}
-- | 'GlutRunMode' specifies how to run the resulting application
data GlutRunMode =
-- | Display the output in a window
GlutNormal |
-- | Display the output in a window, saving the latest frame in the
-- specified file location
GlutCaptureLatest String |
-- | Display the output in a window, saving all frames in the specified
-- directory
GlutCaptureFrames String |
-- | Display the output in a window for a brief period time, saving the
-- latest frame in the specified file location
GlutCaptureAndExit String
runGlut :: GlutOptions -> [GLObj] -> IO ()
runGlut options glObjs = do
initWindow options
ioState <- initIOState
runObjs <- mapM genRunObj glObjs
idleCallback $= Just (update (runMode options) ioState runObjs)
displayCallback $= display runObjs
mouseCallback $= Just (mouse ioState)
motionCallback $= Just (motion ioState)
passiveMotionCallback $= Just (motion ioState)
clearColor $= let (r, g, b, a) = clearCol options in Color4 r g b a
-- override default OpenGL values
-- TODO: should access to values like these be provided
-- directly, in the form of additional GlutOptions?
lineWidth $= 3
pointSize $= 3
openGLSetup options
mainLoop
initWindow :: GlutOptions -> IO ()
initWindow options = do
(progName, _) <- getArgsAndInitialize
_ <- createWindow progName
maybe (return ()) (\(x, y) -> windowPosition $= Position x y)
(winPosition options)
windowSize $= (\(x, y) -> Size x y) (winSize options)
when (winFullscreen options) fullScreen
maybe (return ()) (windowTitle $=) (winTitle options)
actionOnWindowClose $= MainLoopReturns
initialDisplayMode $= [RGBAMode, WithAlphaComponent]
depthFunc $= Just Lequal
blend $= Enabled
blendEquation $= FuncAdd
blendFunc $= (SrcAlpha, OneMinusSrcAlpha)
-- I/O state
data IOState = IOState {
initTime :: Float,
precMap :: IORef (DepMap.DepMap (GLExpr HostDomain) Identity),
mouseLeftDown :: Bool,
mouseRightDown :: Bool,
mouseWheel :: Float,
curMouseX :: Int,
curMouseY :: Int,
-- for stats such as FPS
totUpdates :: Int,
curNumUpdates :: Int,
lastStatsUpdate :: Float
}
defIOState :: IOState
defIOState = IOState {
initTime = 0,
precMap = undefined,
mouseLeftDown = False,
mouseRightDown = False,
mouseWheel = 0,
curMouseX = 0,
curMouseY = 0,
totUpdates = 0,
curNumUpdates = 0,
lastStatsUpdate = 0
}
initIOState :: IO (IORef IOState)
initIOState = do
epoch <- getCurrentTime
let initTime = fromRational $ toRational $ utctDayTime epoch
pm <- newIORef DepMap.empty
newIORef defIOState { initTime = initTime, lastStatsUpdate = initTime, precMap = pm }
-- Update logic
update :: GlutRunMode -> IORef IOState -> [RunObj] -> IdleCallback
update runMode ioState objs = do
outputStatsAndCapture runMode ioState
ioStateUpdate ioState
ioState <- readIORef ioState
let updateObj obj = do
currentProgram $= Just (prog obj)
mapM_ (setUniform ioState obj) (uniformVars obj)
mapM_ updateObj objs
-- prepare precMap for the next iteration
updatePrecMap ioState
postRedisplay Nothing
outputStatsAndCapture :: GlutRunMode -> IORef IOState -> IO ()
outputStatsAndCapture runMode ioStateRef = do
ioState <- readIORef ioStateRef
let numUpdates = curNumUpdates ioState
epoch <- getCurrentTime
let t = fromRational $ toRational $ utctDayTime epoch
dt = t - lastStatsUpdate ioState
if dt > 1
then do
writeIORef ioStateRef $ ioState {
totUpdates = totUpdates ioState + 1, curNumUpdates = 0, lastStatsUpdate = t }
putStrLn $ "FPS: " ++ show (floor $ fromIntegral numUpdates / dt :: Int)
else
writeIORef ioStateRef $ ioState {
totUpdates = totUpdates ioState + 1, curNumUpdates = numUpdates + 1 }
-- TODO: implement own capturePPM/PNG to remove the unecessary dependency
let captureToFile fname = capturePPM >>= BS.writeFile (fname ++ ".ppm")
case runMode of
GlutNormal -> return ()
GlutCaptureLatest fname ->
when (dt > 0.1) (captureToFile fname)
GlutCaptureFrames fname ->
captureToFile $ fname ++ "/frame" ++ show (totUpdates ioState)
GlutCaptureAndExit fname ->
when (totUpdates ioState > 30) $ do
captureToFile fname
leaveMainLoop
-- note that shared uniforms are evaluated separately for each object
-- (except for any prec subexpressions)
-- it's possible that this may cause unexpected behaviour, in
-- which case a map of pre-computed shared values will be needed
setUniform :: IOState -> RunObj -> UniformVar -> IO ()
setUniform ioState obj (UniformVar id x) = do
(UniformLocation ul) <- get (uniformLocation (prog obj) (idLabel id))
val <- hostEval (ioEval ioState) x
uniformSet ul val
updatePrecMap :: IOState -> IO ()
updatePrecMap ioState = do
let updateVal ioState (GLAtom _ (IOPrec _ x) :: GLExpr HostDomain t) _ =
Identity <$> hostEval (ioEval ioState) x
pm <- readIORef $ precMap ioState
_ <- DepMap.traverseWithKey (updateVal ioState) pm
-- pm might have new keys so we need to read it again
-- FIXME: find an alternative to this really ugly solution
pm <- readIORef $ precMap ioState
pm1 <- DepMap.traverseWithKey (updateVal ioState) pm
writeIORef (precMap ioState) pm1
-- Draw logic
display :: [RunObj] -> DisplayCallback
display objs = do
clear [ColorBuffer, DepthBuffer]
let doVao obj = do
currentProgram $= Just (prog obj)
bindVertexArrayObject $= Just (vao obj)
draw (primitiveMode obj) (indices obj) (numVerts obj)
mapM_ doVao objs
flush
draw :: PrimitiveMode -> Maybe [ConstExpr UInt] -> Int -> IO ()
draw mode Nothing n =
drawArrays mode 0 (fromIntegral n)
draw mode (Just inds) _ =
drawElements mode (fromIntegral $ length inds) UnsignedInt (makeOff 0)
-- I/O
ioStateUpdate :: IORef IOState -> IO ()
ioStateUpdate ioState =
let decayWheel ioState = ioState { mouseWheel = 0.1 * mouseWheel ioState }
in modifyIORef ioState decayWheel
mouse :: IORef IOState -> MouseCallback
mouse ioState LeftButton mouseState _ =
let updateLeft ioState = ioState { mouseLeftDown = mouseState == Down }
in modifyIORef ioState updateLeft
mouse ioState RightButton mouseState _ =
let updateRight ioState = ioState { mouseRightDown = mouseState == Down }
in modifyIORef ioState updateRight
mouse ioState WheelUp _ _ =
let updateWheel ioState = ioState { mouseWheel = 1 }
in modifyIORef ioState updateWheel
mouse ioState WheelDown _ _ =
let updateWheel ioState = ioState { mouseWheel = -1 }
in modifyIORef ioState updateWheel
mouse _ _ _ _ = return ()
motion :: IORef IOState -> MotionCallback
motion ioState (Position x y) =
let updatePos ioState = ioState { curMouseX = fromIntegral x, curMouseY = fromIntegral y }
in modifyIORef ioState updatePos
ioEval :: IOState -> GLExpr HostDomain t -> IO t
ioEval ioState e@(GLAtom _ (IOPrec x0 _)) = do
pm <- readIORef $ precMap ioState
case DepMap.lookup e pm of
Just val -> return $ runIdentity val
Nothing -> do
val <- hostEval (ioEval ioState) x0
writeIORef (precMap ioState) $ DepMap.insert e (Identity val) pm
return val
ioEval ioState (GLAtom _ (IOFloat "time")) = do
let t0 = initTime ioState
epoch <- getCurrentTime
let t = fromRational $ toRational $ utctDayTime epoch
return $ t - t0
ioEval ioState (GLAtom _ (IOBool "mouseLeft")) =
return $ (toEnum . fromEnum) $ mouseLeftDown ioState
ioEval ioState (GLAtom _ (IOBool "mouseRight")) =
return $ (toEnum . fromEnum) $ mouseRightDown ioState
ioEval ioState (GLAtom _ (IOFloat "mouseWheel")) =
return $ mouseWheel ioState
ioEval ioState (GLAtom _ (IOFloat "mouseX")) = do
(Size width _) <- get windowSize
return $ fromIntegral (curMouseX ioState) / fromIntegral width
ioEval ioState (GLAtom _ (IOFloat "mouseY")) = do
(Size _ height) <- get windowSize
return $ 1 - fromIntegral (curMouseY ioState) / fromIntegral height