packages feed

gloss-relative-0.1.1.0: src/Graphics/Gloss/Relative/Internal/Raster.hs

module Graphics.Gloss.Relative.Internal.Raster where

import Graphics.Gloss.Relative.Internal.Dimension
import Graphics.Rendering.OpenGL (($=))
import qualified Graphics.Rendering.OpenGL as GL
import Graphics.Gloss.Rendering as Gloss
import Graphics.Gloss
import Data.ByteString.Internal (create)
import Foreign.Ptr (Ptr)
import Data.Word (Word8)
import Foreign
import Foreign.ForeignPtr
import Data.Word
import Data.IORef
import Control.Monad

data Offscreen = Offscreen
    { offFBO  :: GL.FramebufferObject
    , offTex  :: GL.TextureObject
    , offDim  :: ScreenSize
    }

createOffscreen :: ScreenSize -> IO Offscreen
createOffscreen dim@(w,h) = do
    fbo <- GL.genObjectName
    GL.bindFramebuffer GL.Framebuffer $= fbo
    
    tex <- GL.genObjectName
    GL.textureBinding GL.Texture2D $= Just tex
    GL.texImage2D GL.Texture2D GL.NoProxy 0 GL.RGBA8 (GL.TextureSize2D (fromIntegral w) (fromIntegral h)) 0 (GL.PixelData GL.RGBA GL.UnsignedByte nullPtr)
    
    GL.framebufferTexture2D GL.Framebuffer (GL.ColorAttachment 0) GL.Texture2D tex 0
    
    GL.drawBuffer $= GL.FBOColorAttachment 0
    GL.readBuffer $= GL.FBOColorAttachment 0
    
    status <- GL.get (GL.framebufferStatus GL.Framebuffer)
    when (status /= GL.Complete) $ error $ "Offscreen FBO incomplete " ++ show status
    
    GL.bindFramebuffer GL.Framebuffer $= GL.defaultFramebufferObject
    return Offscreen { offFBO = fbo, offTex = tex, offDim = dim }

deleteOffscreen :: Offscreen -> IO ()
deleteOffscreen off = do
  -- Unbind first
  GL.bindFramebuffer GL.Framebuffer $= GL.defaultFramebufferObject
  GL.textureBinding GL.Texture2D $= Nothing

  -- Delete GPU objects
  GL.deleteObjectName (offFBO off)
  GL.deleteObjectName (offTex off)

getResizeOffscreen :: IORef (IO Offscreen) -> ScreenSize -> IO Offscreen
getResizeOffscreen r (w,h) = do
    moff <- readIORef r
    off <- moff
    let (offw,offh) = offDim off
    off' <- if w > offw || h > offh
        then do
            deleteOffscreen off
            off' <- createOffscreen (w,h)
            return off'
        else return off
    writeIORef r $! return $! off'
    return off'
    
rasterPicture
    :: Gloss.State -> Offscreen
    -> ScreenSize
    -> Picture
    -> IO RasteredPicture
rasterPicture state offscreen dim@(w,h) pic = do
    -- Save OpenGL state
    oldVP   <- GL.get GL.viewport
    
    -- Bind offscreen target
    GL.bindFramebuffer GL.Framebuffer $= (offFBO offscreen)
    GL.viewport $= (GL.Position 0 0, GL.Size (fromIntegral w) (fromIntegral h))
    
    -- Clear if needed (alpha = 0 for transparency)
    GL.clearColor $= GL.Color4 0 0 0 0
    GL.clear [GL.ColorBuffer]
    
    -- Set projection to match Gloss expectations
    GL.matrixMode $= GL.Projection
    GL.loadIdentity
    GL.ortho (-fromIntegral w / 2) ( fromIntegral w / 2) (-fromIntegral h / 2) ( fromIntegral h / 2) (-1) 1
    
    GL.matrixMode $= GL.Modelview 0
    GL.loadIdentity
    
    -- Render Gloss picture
    renderPicture state 1 pic
    
    -- Read pixels (RGBA, bottom-up)
    let bytes = w * h * 4
    fptr <- mallocForeignPtrBytes bytes
    withForeignPtr fptr $ \ptr ->
        GL.readPixels (GL.Position 0 0) (GL.Size (fromIntegral w) (fromIntegral h)) (GL.PixelData GL.RGBA GL.UnsignedByte ptr)
    
    -- Restore GL state
    GL.bindFramebuffer GL.Framebuffer $= GL.defaultFramebufferObject
    GL.viewport $= oldVP
    
    -- Convert to Picture
    return (dim,BitmapFormat BottomToTop PxRGBA,fptr)

type RasteredPicture = (ScreenSize,BitmapFormat,ForeignPtr Word8)

fromRasteredPicture :: RasteredPicture -> Picture
fromRasteredPicture ((w,h),fmt,fptr) = bitmapOfForeignPtr w h fmt fptr True

-- | Checks if the pixel of a certain point (with (0,0) centered in the image) is transparent.
getPixelAlpha :: RasteredPicture -> Point -> IO Word8
getPixelAlpha pic@(dim,_,_) p = do
    alpha <- lookupAlpha pic (centerToTopLeft dim p)
    return $ alpha

centerToTopLeft :: ScreenSize -> Point -> (Int,Int)
centerToTopLeft (w,h) (xc, yc) =
  ( round $ xc + realToFrac w / 2
  , round $ realToFrac h / 2 - yc
  )

topLeftToCenter :: ScreenSize -> (Int,Int) -> Point
topLeftToCenter (w,h) (xt, yt) =
  ( realToFrac xt - realToFrac w / 2
  , realToFrac h / 2 - realToFrac yt
  )

-- coordinates (x,y) with (0,0) being the top-left corner.
lookupAlpha :: RasteredPicture -> (Int,Int) -> IO Word8
lookupAlpha pic@((w,h),fmt,fptr) (x,y) =
  withForeignPtr fptr $ \ptr -> do
    let off = pixelOffset pic x y + alphaOffset (pixelFormat fmt)
    peekByteOff ptr off

alphaOffset :: PixelFormat -> Int
alphaOffset PxRGBA = 3
alphaOffset PxABGR = 0

pixelStride :: Int
pixelStride = 4

pixelOffset :: RasteredPicture -> Int -> Int -> Int
pixelOffset (dim,fmt,fptr) x y =
  let (w, h)   = dim
      BitmapFormat ro pf = fmt
      ry       = rowIndex ro h y
      base     = (ry * w + x) * pixelStride
  in base

rowIndex :: RowOrder -> Int -> Int -> Int
rowIndex TopToBottom h y = y
rowIndex BottomToTop h y = h - 1 - y