not-gloss-0.7.7.0: src/Vis/Vis.hs
{-# OPTIONS_GHC -Wall #-}
{-# Language ScopedTypeVariables #-}
module Vis.Vis ( Options(..)
, Antialiasing(..)
, vis
, visMovie
, visMovieImmediately
, FullState
) where
import Codec.BMP ( BMP, packRGBA32ToBMP32, writeBMP )
import Control.Concurrent ( MVar, readMVar, swapMVar, newMVar, takeMVar, putMVar, forkIO, threadDelay )
import Control.Monad ( unless, forever )
import qualified Data.ByteString.Unsafe as BS
import Data.Maybe ( fromMaybe )
import Data.IORef ( newIORef, readIORef, writeIORef )
import Data.Time.Clock ( getCurrentTime, diffUTCTime, addUTCTime )
import Data.Word ( Word8 )
import Foreign.Marshal.Alloc ( free )
import Foreign.Marshal.Array ( mallocArray )
import Foreign.Ptr ( Ptr, castPtr )
import Foreign.Storable ( sizeOf )
import qualified Graphics.UI.GLUT as GLUT
import Graphics.UI.GLUT ( Capability(..), ClearBuffer(..), Color4(..), ColorMaterialParameter(..)
, ComparisonFunction(..), Cursor(..), DisplayMode(..), Face(..)
, Key(..), KeyState(..), Light(..), Modifiers(..), Position(..)
, ShadingModel(..), Size(..)
, DisplayCallback, ReshapeCallback
, ($=)
)
import Graphics.GL
import Text.Printf ( printf )
import System.Exit ( exitSuccess )
import Vis.Camera ( Camera, Camera0(..), setCamera, makeCamera, cameraKeyboardMouse, cameraMotion )
import Vis.VisObject ( VisObject(..), drawObjects, setPerspectiveMode )
import qualified Vis.GlossColor as GC
-- | user state and internal states
type FullState a = (a, Float)
data Antialiasing =
Aliased
| Smoothed
| Multisampled Int
deriving (Eq, Show, Ord)
data Options =
Options
{ optBackgroundColor :: Maybe GC.Color -- ^ optional background color
, optWindowSize :: Maybe (Int,Int) -- ^ optional (x,y) window size in pixels
, optWindowPosition :: Maybe (Int,Int) -- ^ optional (x,y) window origin in pixels
, optWindowName :: String -- ^ window name
, optInitialCamera :: Maybe Camera0 -- ^ initial camera position
, optAntialiasing :: Antialiasing -- ^ which antialiasing strategy to use
} deriving Show
myGlInit :: Options -> IO ()
myGlInit opts = do
let displayMode = [ DoubleBuffered, RGBAMode, WithDepthBuffer ] ++
case optAntialiasing opts of
Multisampled numSamples -> [ GLUT.Multisampling
, GLUT.WithSamplesPerPixel numSamples
]
_ -> []
GLUT.initialDisplayMode $= displayMode
Size x y <- GLUT.get GLUT.screenSize
let intScale d i = round $ d*(realToFrac i :: Double)
x0 = intScale 0.3 x
xf = intScale 0.95 x
y0 = intScale 0.05 y
yf = intScale 0.95 y
(xsize, ysize) = fromMaybe (xf - x0, yf - y0) (optWindowSize opts)
(xpos, ypos) = fromMaybe (x0,y0) (optWindowPosition opts)
GLUT.initialWindowSize $= Size (fromIntegral xsize) (fromIntegral ysize)
GLUT.initialWindowPosition $= Position (fromIntegral xpos) (fromIntegral ypos)
_ <- GLUT.createWindow (optWindowName opts)
case optBackgroundColor opts of
Nothing -> GLUT.clearColor $= Color4 0 0 0 0
Just col -> GLUT.clearColor $= Color4 (realToFrac r) (realToFrac g) (realToFrac b) (realToFrac a)
where
(r,g,b,a) = GC.rgbaOfColor col
GLUT.shadeModel $= Smooth
GLUT.depthFunc $= Just Less
GLUT.lighting $= Enabled
GLUT.light (Light 0) $= Enabled
GLUT.ambient (Light 0) $= Color4 1 1 1 1
GLUT.materialDiffuse Front $= Color4 0.5 0.5 0.5 1
GLUT.materialSpecular Front $= Color4 1 1 1 1
GLUT.materialShininess Front $= 100
GLUT.colorMaterial $= Just (Front, Diffuse)
case optAntialiasing opts of
Aliased -> do
GLUT.lineSmooth $= Disabled
GLUT.pointSmooth $= Disabled
GLUT.multisample $= Disabled
Smoothed -> do
GLUT.hint GLUT.LineSmooth $= GLUT.Nicest
GLUT.hint GLUT.PointSmooth $= GLUT.Nicest
GLUT.lineSmooth $= Enabled
GLUT.pointSmooth $= Enabled
GLUT.multisample $= Disabled
Multisampled _ -> do
GLUT.lineSmooth $= Disabled
GLUT.pointSmooth $= Disabled
GLUT.multisample $= Enabled
glEnable GL_BLEND
glBlendFunc GL_SRC_ALPHA GL_ONE_MINUS_SRC_ALPHA
drawScene :: MVar (FullState a) -> MVar Bool -> IO () -> (FullState a -> IO ()) -> DisplayCallback
drawScene stateMVar visReadyMVar setCameraFun userDrawFun = do
GLUT.clear [ ColorBuffer, DepthBuffer ]
-- draw the scene
GLUT.preservingMatrix $ do
-- set the camera's position and orientation
setCameraFun
-- call user function
state <- readMVar stateMVar
userDrawFun state
GLUT.flush
GLUT.swapBuffers
_ <- swapMVar visReadyMVar True
GLUT.postRedisplay Nothing
reshape :: ReshapeCallback
reshape size@(Size _ _) = do
GLUT.viewport $= (Position 0 0, size)
setPerspectiveMode
GLUT.loadIdentity
GLUT.postRedisplay Nothing
vis :: Real b =>
Options -- ^ user options
-> Double -- ^ sample time
-> a -- ^ initial state
-> (FullState a -> IO a) -- ^ sim function
-> (FullState a -> IO (VisObject b, Maybe Cursor)) -- ^ draw function, can give a different cursor
-> (a -> IO ()) -- ^ set camera function
-> Maybe (a -> Key -> KeyState -> Modifiers -> Position -> a) -- ^ keyboard/mouse callback
-> Maybe (a -> Position -> a) -- ^ motion callback
-> Maybe (a -> Position -> a) -- ^ passive motion callback
-> IO ()
vis opts ts x0 userSimFun userDraw userSetCamera
userKeyMouseCallback userMotionCallback userPassiveMotionCallback = do
-- init glut/scene
_ <- GLUT.getArgsAndInitialize
myGlInit opts
-- create internal state
let fullState0 = (x0, 0)
stateMVar <- newMVar fullState0
visReadyMVar <- newMVar False
-- start sim thread
_ <- forkIO $ simThread stateMVar visReadyMVar userSimFun ts
-- setup the callbacks
let makePictures x = do
(visobs,cursor') <- userDraw x
drawObjects $ (fmap realToFrac) visobs
case cursor' of Nothing -> return ()
Just cursor'' -> GLUT.cursor $= cursor''
setCamera' = do
(state,_) <- readMVar stateMVar
userSetCamera state
-- kill sim thread when someone hits ESC
exitOverride k0 k1 k2 k3 = case (k0,k1) of
(Char '\27', Down) -> exitSuccess
_ -> case userKeyMouseCallback of
Nothing -> return ()
Just cb -> do
(state0',time) <- takeMVar stateMVar
putMVar stateMVar (cb state0' k0 k1 k2 k3, time)
GLUT.postRedisplay Nothing
motionCallback' pos = case userMotionCallback of
Nothing -> return ()
Just cb -> do
(state0',ts') <- takeMVar stateMVar
putMVar stateMVar (cb state0' pos, ts')
GLUT.postRedisplay Nothing
passiveMotionCallback' pos = case userPassiveMotionCallback of
Nothing -> return ()
Just cb -> do
(state0',ts') <- takeMVar stateMVar
putMVar stateMVar (cb state0' pos, ts')
GLUT.postRedisplay Nothing
GLUT.displayCallback $= drawScene stateMVar visReadyMVar setCamera' makePictures
GLUT.reshapeCallback $= Just reshape
GLUT.keyboardMouseCallback $= Just exitOverride
GLUT.motionCallback $= Just motionCallback'
GLUT.passiveMotionCallback $= Just passiveMotionCallback'
-- start main loop
GLUT.mainLoop
simThread :: MVar (FullState a) -> MVar Bool -> (FullState a -> IO a) -> Double -> IO ()
simThread stateMVar visReadyMVar userSimFun ts = do
let waitUntilDisplayIsReady :: IO ()
waitUntilDisplayIsReady = do -- todo: why not just block?
visReady <- readMVar visReadyMVar
unless visReady $ do
threadDelay 10000
waitUntilDisplayIsReady
waitUntilDisplayIsReady
t0 <- getCurrentTime
lastTimeRef <- newIORef t0
forever $ do
-- calculate how much longer to sleep before taking a timestep
currentTime <- getCurrentTime
lastTime <- GLUT.get lastTimeRef
let usRemaining :: Int
usRemaining = round $ 1e6*(ts - realToFrac (diffUTCTime currentTime lastTime))
secondsSinceStart = realToFrac (diffUTCTime currentTime t0)
if usRemaining <= 0
-- slept for long enough, do a sim iteration
then do
lastTimeRef $= addUTCTime (realToFrac ts) lastTime
let getNextState = do
state <- readMVar stateMVar
userSimFun state
putState x = swapMVar stateMVar (x, secondsSinceStart)
nextState <- getNextState
_ <- nextState `seq` putState nextState
GLUT.postRedisplay Nothing
-- need to sleep longer
else threadDelay usRemaining
movieSimThread :: [VisObject a] -> MVar ([VisObject a], Camera) -> MVar Bool -> Double -> IO ()
movieSimThread objects0 stateMVar visReadyMVar ts = do
let waitUntilDisplayIsReady :: IO ()
waitUntilDisplayIsReady = do -- todo: why not just block?
visReady <- readMVar visReadyMVar
unless visReady $ do
threadDelay 10000
waitUntilDisplayIsReady
waitUntilDisplayIsReady
t0 <- getCurrentTime
lastTimeRef <- newIORef t0
forever $ do
-- calculate how much longer to sleep before taking a timestep
currentTime <- getCurrentTime
lastTime <- GLUT.get lastTimeRef
let usRemaining :: Int
usRemaining = round $ 1e6*(ts - realToFrac (diffUTCTime currentTime lastTime))
if usRemaining <= 0
-- slept for long enough, do a sim iteration
then do
lastTimeRef $= addUTCTime (realToFrac ts) lastTime
let getNextState = do
state <- readMVar stateMVar
let next = case state of
(_:xs, cs) -> (xs, cs)
([], cs) -> (objects0, cs)
return next
putState x = swapMVar stateMVar x
nextState <- getNextState
_ <- nextState `seq` putState nextState
GLUT.postRedisplay Nothing
-- need to sleep longer
else threadDelay usRemaining
-- | Make a series of images, one from each 'VisObject'.
-- When 'visMovie' is executed a window pops up and loops the animation
-- until you are happy with the camera angle.
-- Hit spacebar and the images will be created and saved to disk.
visMovie
:: forall b
. Real b
=> Options -- ^ user options
-> (Int -> FilePath) -- ^ where to write the bitmaps
-> Double -- ^ sample time
-> [VisObject b] -- ^ movie to draw
-> Maybe Cursor -- ^ optional cursor
-> IO ()
visMovie = visMovie' False
-- | Make a series of images, one from each 'VisObject'.
-- When 'visMovieImmediately' is executed a window is opened and without
-- waiting the images are created and saved to disk.
visMovieImmediately
:: forall b
. Real b
=> Options -- ^ user options
-> (Int -> FilePath) -- ^ where to write the bitmaps
-> Double -- ^ sample time
-> [VisObject b] -- ^ movie to draw
-> Maybe Cursor -- ^ optional cursor
-> IO ()
visMovieImmediately = visMovie' True
visMovie'
:: forall b
. Real b
=> Bool -- ^ start immediately
-> Options -- ^ user options
-> (Int -> FilePath) -- ^ where to write the bitmaps
-> Double -- ^ sample time
-> [VisObject b] -- ^ movie to draw
-> Maybe Cursor -- ^ optional cursor
-> IO ()
visMovie' startImmediately opts toFilename ts objectsToDraw maybeCursor = do
-- init glut/scene
_ <- GLUT.getArgsAndInitialize
myGlInit opts
let defaultCam =
Camera0 { phi0 = 60
, theta0 = 20
, rho0 = 7}
cameraState0 = makeCamera $ fromMaybe defaultCam (optInitialCamera opts)
-- create internal state
areWeDrawingRef <- newIORef False
stateMVar <- newMVar (objectsToDraw, cameraState0)
visReadyMVar <- newMVar startImmediately
-- start sim thread
_ <- forkIO $ movieSimThread objectsToDraw stateMVar visReadyMVar ts
-- setup the callbacks
let makePictures :: VisObject b -> Camera -> IO ()
makePictures visobj cam = do
GLUT.clear [ ColorBuffer, DepthBuffer ]
-- draw the scene
GLUT.preservingMatrix $ do
setCamera cam
drawObjects $ (fmap realToFrac) visobj
case maybeCursor of
Nothing -> return ()
Just cursor -> GLUT.cursor $= cursor
GLUT.flush
GLUT.swapBuffers
_ <- swapMVar visReadyMVar True
GLUT.postRedisplay Nothing
screenShot :: Int -> Camera -> (VisObject b, Int) -> IO ()
screenShot n camera (visobj, imageNumber) = do
-- todo: are width/height reversed?
size@(Size width height) <- GLUT.get GLUT.windowSize
let pos = Position 0 0
ubytePtr <- mallocArray (fromIntegral (4*width*height)) :: IO (Ptr GLubyte)
let pixelData = GLUT.PixelData GLUT.RGBA GLUT.UnsignedByte ubytePtr
makePictures visobj camera
-- "glFinish" will do the job, but it may be overkill.
-- "swapBuffers" is probably good enough.
-- http://stackoverflow.com/questions/2143240/opengl-glflush-vs-glfinish
-- We just need to make sure that readPixels will do the right thing
GLUT.finish
GLUT.readPixels pos size pixelData
let wordPtr :: Ptr Word8
wordPtr
| sizeOf (0 :: GLubyte) == sizeOf (0 :: Word8) = castPtr ubytePtr
| otherwise = error "GLubyte size /= Word8 size"
bs <- BS.unsafePackCStringFinalizer
wordPtr (fromIntegral (4*width*height)) (free ubytePtr)
let bmp :: BMP
bmp = packRGBA32ToBMP32 (fromIntegral width) (fromIntegral height) bs
let filename = toFilename imageNumber
percent :: Double
percent = 100 * fromIntegral imageNumber / fromIntegral n
printf "writing \"%s\" (%d / %d == %6.2f %%) ...\n" filename imageNumber n percent
writeBMP filename bmp
drawFun = do
areWeDrawing <- readIORef areWeDrawingRef
(state,cam) <- readMVar stateMVar
if areWeDrawing
then do let n = length objectsToDraw
state' <- takeMVar stateMVar
mapM_ (screenShot n cam) (zip objectsToDraw [0..])
putStrLn "finished writing files"
putStrLn "you might want to try some command like:"
putStrLn "\"ffmpeg -framerate 50 -i data/movie.%03d.bmp -c:v libx264 -r 30 -pix_fmt yuv420p out.mp4\""
putMVar stateMVar state'
writeIORef areWeDrawingRef False
else do let visobj = case (state, objectsToDraw) of
(y:_, _) -> y -- draw head object
([], y:_) -> y -- empty state so just draw first object
([], []) -> VisObjects [] -- nothing available
makePictures visobj cam
exitOverride k0 k1 _k2 _k3 = case (k0,k1) of
-- ESC button exits the program
(Char '\27', Down) -> exitSuccess
-- space bar starts screenshots
(Char ' ', Down) -> writeIORef areWeDrawingRef True
_ -> do
(state0', cs) <- takeMVar stateMVar
putMVar stateMVar (state0', cameraKeyboardMouse cs k0 k1)
GLUT.postRedisplay Nothing
motionCallback' pos = do
(state0', cs) <- takeMVar stateMVar
putMVar stateMVar (state0', cameraMotion cs pos)
GLUT.postRedisplay Nothing
-- passiveMotionCallback' pos = case userPassiveMotionCallback of
-- Nothing -> return ()
-- Just cb -> do
-- (state0', cs) <- takeMVar stateMVar
-- putMVar stateMVar (cb state0' pos, cs)
-- GLUT.postRedisplay Nothing
GLUT.displayCallback $= drawFun
GLUT.reshapeCallback $= Just reshape
GLUT.keyboardMouseCallback $= Just exitOverride
GLUT.motionCallback $= Just motionCallback'
-- GLUT.passiveMotionCallback $= Just passiveMotionCallback'
-- start main loop
GLUT.mainLoop