-- author: Samuel GĂ©lineau (gelisam) -- in response to https://www.reddit.com/r/haskell/comments/3u5s4e/is_there_a_way_to_write_the_frames_of_a_gloss/ -- slightly improved module Graphics.Gloss.Export.Image ( Size , Animation , initialize , pictureToImageRGB8 , pictureToImageRGBA8 , exportPictureToFormat , exportPicturesToFormat ) where import Codec.Picture.Types (Image(..), PixelRGBA8, PixelRGB8, PixelBaseComponent) import Control.Monad (forM_) import Data.Vector.Storable (Vector, unsafeFromForeignPtr0, Storable, slice, concat) import qualified Graphics.Gloss.Rendering as Gloss import Graphics.GL -- as GL* import qualified Graphics.UI.GLFW as GLFW import Foreign (newForeignPtr_, Ptr) import Foreign.Marshal.Alloc (free) import Foreign.Marshal.Array (mallocArray) import Text.Printf (printf) import GHC.Int import qualified Graphics.UI.GLUT as GLUT import Prelude hiding (concat) type Size = (Int, Int) type Animation = Float -> Gloss.Picture -- | Save a gloss Picture to a file. exportPictureToFormat :: (FilePath -> Image PixelRGBA8 -> IO ()) -- ^ function that saves an intermediate representation to a format. Written with writeXY from Codec.Picture in mind -> Size -- ^ (width, heigth) in pixels - as in Gloss.Display -> Gloss.Color -- ^ Background color -> FilePath -> Gloss.Picture -> IO () exportPictureToFormat savefunc size bgc f p = do s <- initialize size --initialize is not part of pictureToImage so in --exportPicturesToFormat we only have to initialize once (img,ptr) <- pictureToImageRGBA8 size bgc s p savefunc f img free ptr -- | If you want to write your own functions, call this function before pictureToImage* initialize :: Size -> IO Gloss.State initialize size = do _ <- GLUT.exit -- otherwise 'illegal reinitialization' (_,_) <- GLUT.getArgsAndInitialize -- needed for text https://github.com/elisehuard/game-in-haskell/pull/3 s <- Gloss.initState initOpenGL size return s -- let GLFW bother with the OpenGL initialization initOpenGL :: (Int, Int) -- ^ windowWidth, windowHeight -> IO () initOpenGL (windowWidth, windowHeight) = do True <- GLFW.init GLFW.windowHint (GLFW.WindowHint'Visible False) Just w <- GLFW.createWindow windowWidth windowHeight "gloss-to-file demo" Nothing Nothing GLFW.makeContextCurrent (Just w) -- | Save a series of gloss Picture to files of spcified format. exportPicturesToFormat :: (FilePath -> Image PixelRGBA8 -> IO ()) -- ^ function that saves an intermediate representation to a format. Written with writeXY from Codec.Picture in mind -> Size -- ^ (width, height) in pixels - as in Gloss.Display -> Gloss.Color -- ^ background color -> FilePath -- ^ must contain "%d", will be replaced by frame number -> Animation -- ^ function that maps from point in time to Picture. analog to Gloss.Animation -> [Float] -- ^ list of points in time at which to evaluate the animation -> IO () exportPicturesToFormat savefunc size bgc f anim ts = do s <- initialize size forM_ (zip [1..] ts) $ \(n, t) -> do let filename = printf f (n :: Int) let picture = anim t (img,ptr) <- pictureToImageRGBA8 size bgc s picture savefunc filename img free ptr -- I couldn't find a way to generalize over the type of pixel so I made RGBA8 and RGB8. -- Keep in mind: different pixel types have different sizes! -- GL_RGBA, 4 would also have to be abstracted -- | convert a gloss 'Picture' into an 'Image'. -- The pointer should be freed after the image is no longer needed in memory -- Use 'free ptr' to free the memory. pictureToImageRGBA8 :: Size -- ^ (width, height) in pixels - as in Gloss.Display -> Gloss.Color -- ^ Background color -> Gloss.State -- ^ Result of 'initialize' -> Gloss.Picture -> IO (Image PixelRGBA8, Ptr (PixelBaseComponent PixelRGBA8)) -- ^ image and a pointer to memory pictureToImageRGBA8 (windowWidth, windowHeight) bgc s p = do drawReadBuffer (windowWidth, windowHeight) bgc s p imageData <- mallocArray (windowWidth * windowHeight * 4) let wW = fromIntegral windowWidth :: GHC.Int.Int32 let wH = fromIntegral windowHeight :: GHC.Int.Int32 glReadPixels 0 0 wW wH GL_RGBA GL_UNSIGNED_BYTE imageData foreignPtr <- newForeignPtr_ imageData let vector = unsafeFromForeignPtr0 foreignPtr (windowWidth * windowHeight * 4) let vectorFlipped = reverseImage 4 windowWidth windowHeight vector let image :: Image PixelRGBA8 image = Image windowWidth windowHeight vectorFlipped return (image, imageData) drawReadBuffer :: Size -> Gloss.Color -- ^ Background color -> Gloss.State -> Gloss.Picture -> IO () drawReadBuffer size bg s p = do glDrawBuffer GL_BACK Gloss.withClearBuffer bg $ Gloss.withModelview size $ do glColor3f 0 0 0 Gloss.renderPicture s 1 p glReadBuffer GL_BACK -- the drawn image is flipped ([rowN,...,row1]) so we need to reverse the order of rows -- I guess this is because the origin is specified as topleft and bottomleft by different functions reverseImage :: Storable a => Int -- ^ #Bits per pixel. 4 for rgba8, 3 for rgb8 -> Int -> Int -> Vector a -> Vector a reverseImage components width height vec = concat [slice i widthC vec | i <- map (*widthC) [(height-1),(height-2)..0] ] where widthC = width*components -- Below is the version without transparency for making gifs. -- ideally there would just be one function and the result would be casted to either ::PixelRGBA8 or ::PixelRGB8 -- | convert a gloss 'Picture' into an 'Image'. -- The pointer should be freed after the image is no longer needed in memory -- Use 'free ptr' to free the memory. pictureToImageRGB8 :: Size -- ^ (width, height) in pixels - as in Gloss.Display -> Gloss.Color -- ^ Background color -> Gloss.State -- ^ Result of 'initializate' -> Gloss.Picture -> IO (Image PixelRGB8, Ptr (PixelBaseComponent PixelRGB8)) -- ^ image and a pointer to memory pictureToImageRGB8 (windowWidth, windowHeight) bgc s p = do drawReadBuffer (windowWidth, windowHeight) bgc s p imageData <- mallocArray (windowWidth * windowHeight * 3) let wW = fromIntegral windowWidth :: GHC.Int.Int32 let wH = fromIntegral windowHeight :: GHC.Int.Int32 glReadPixels 0 0 wW wH GL_RGB GL_UNSIGNED_BYTE imageData foreignPtr <- newForeignPtr_ imageData let vector = unsafeFromForeignPtr0 foreignPtr (windowWidth * windowHeight * 3) let vectorFlipped = reverseImage 3 windowWidth windowHeight vector let image :: Image PixelRGB8 image = Image windowWidth windowHeight vectorFlipped return (image, imageData)