module Main (main) where -------------------------------------------------------------------------------- import Control.Concurrent.STM (TQueue, atomically, newTQueueIO, tryReadTQueue, writeTQueue) import Control.Monad (unless, when, void) import Control.Monad.RWS.Strict (RWST, ask, asks, evalRWST, get, liftIO, modify, put) import Control.Monad.Trans.Maybe (MaybeT(..), runMaybeT) import Data.List (intercalate) import Data.Maybe (catMaybes) import Text.PrettyPrint import Control.Applicative import Control.Lens import Control.Monad (forever) import Data.Distributive (distribute) import Foreign (Ptr, castPtr, nullPtr, sizeOf, with) import Foreign.C (CFloat) import Graphics.Rendering.OpenGL (($=)) import qualified Graphics.Rendering.OpenGL as GL import qualified Graphics.Rendering.OpenGL.Raw as GL import qualified Graphics.UI.GLFW as GLFW import qualified Data.Text as Text import qualified Data.Text.Encoding as Text import qualified Data.Vector.Storable as V import Linear as L import Linear ((!*!)) import Data.IORef -------------------------------------------------------------------------------- data Env = Env { envEventsChan :: TQueue Event , envWindow :: !GLFW.Window , envGear1 :: !GL.DisplayList , envGear2 :: !GL.DisplayList , envGear3 :: !GL.DisplayList , envZDistClosest :: !Double , envZDistFarthest :: !Double } data State = State { stateWindowWidth :: !Int , stateWindowHeight :: !Int , stateXAngle :: !Double , stateYAngle :: !Double , stateZAngle :: !Double , stateGearZAngle :: !Double , stateZDist :: !Double , stateMouseDown :: !Bool , stateDragging :: !Bool , stateDragStartX :: !Double , stateDragStartY :: !Double , stateDragStartXAngle :: !Double , stateDragStartYAngle :: !Double } type Demo = RWST Env () State IO -------------------------------------------------------------------------------- data Event = EventError !GLFW.Error !String | EventWindowPos !GLFW.Window !Int !Int | EventWindowSize !GLFW.Window !Int !Int | EventWindowClose !GLFW.Window | EventWindowRefresh !GLFW.Window | EventWindowFocus !GLFW.Window !GLFW.FocusState | EventWindowIconify !GLFW.Window !GLFW.IconifyState | EventFramebufferSize !GLFW.Window !Int !Int | EventMouseButton !GLFW.Window !GLFW.MouseButton !GLFW.MouseButtonState !GLFW.ModifierKeys | EventCursorPos !GLFW.Window !Double !Double | EventCursorEnter !GLFW.Window !GLFW.CursorState | EventScroll !GLFW.Window !Double !Double | EventKey !GLFW.Window !GLFW.Key !Int !GLFW.KeyState !GLFW.ModifierKeys | EventChar !GLFW.Window !Char deriving Show -------------------------------------------------------------------------------- -------------------------------------------------------------------------------- triangleTransformation :: (Epsilon a, Floating a) => a -> M44 a triangleTransformation = liftA2 (!*!) triangleTranslation triangleRotation -------------------------------------------------------------------------------- triangleRotation :: (Epsilon a, Floating a) => a -> M44 a triangleRotation t = m33_to_m44 $ fromQuaternion $ axisAngle (V3 0 1 0) (t * 2) triangleTranslation :: Floating a => a -> M44 a triangleTranslation t = eye4 & translation .~ V3 (sin t * 2) 0 (-5) -------------------------------------------------------------------------------- main :: IO () main = do let width = 640 height = 480 eventsChan <- newTQueueIO :: IO (TQueue Event) withWindow width height "GLFW-b-demo" $ \win -> do let z = 0 let vertices = V.fromList [ 0, 1, 0 , -1, -1, z , 1, -1, z ] :: V.Vector Float vertexAttribute = GL.AttribLocation 0 cubeVbo <- GL.genObjectName GL.bindBuffer GL.ArrayBuffer $= Just cubeVbo V.unsafeWith vertices $ \v -> GL.bufferData GL.ArrayBuffer $= (fromIntegral $ V.length vertices * sizeOf (0 :: Float), v, GL.StaticDraw) GL.vertexAttribPointer vertexAttribute $= (GL.ToFloat, GL.VertexArrayDescriptor 3 GL.Float 0 nullPtr) GL.vertexAttribArray vertexAttribute $= GL.Enabled GL.bindBuffer GL.ArrayBuffer $= Just cubeVbo vertexShader <- GL.createShader GL.VertexShader fragmentShader <- GL.createShader GL.FragmentShader GL.shaderSourceBS vertexShader $= Text.encodeUtf8 (Text.pack $ unlines [ "#version 130" , "uniform mat4 projection;" , "uniform mat4 model;" , "in vec3 in_Position;" , "void main(void) {" , " gl_Position = projection * model * vec4(in_Position, 1.0);" , "}" ]) GL.shaderSourceBS fragmentShader $= Text.encodeUtf8 (Text.pack $ unlines [ "#version 130" , "out vec4 fragColor;" , "void main(void) {" , " fragColor = vec4(1.0,1.0,1.0,1.0);" , "}" ]) GL.compileShader vertexShader GL.compileShader fragmentShader shaderProg <- GL.createProgram GL.attachShader shaderProg vertexShader GL.attachShader shaderProg fragmentShader GL.attribLocation shaderProg "in_Position" $= vertexAttribute GL.linkProgram shaderProg GL.currentProgram $= Just shaderProg let fov = 90 s = recip (tan $ fov * 0.5 * pi / 180) f = 1000 n = 1 let perspective = V.fromList [ s, 0, 0, 0 , 0, s, 0, 0 , 0, 0, -(f/(f - n)), -1 , 0, 0, -((f*n)/(f-n)), 0 ] GL.UniformLocation loc <- GL.get (GL.uniformLocation shaderProg "projection") V.unsafeWith perspective $ \ptr -> GL.glUniformMatrix4fv loc 1 0 ptr tr <- newIORef 0 forever $ do t <- readIORef tr GL.clearColor $= GL.Color4 0.5 0.2 1 1 GL.clear [GL.ColorBuffer] GL.UniformLocation loc <- GL.get (GL.uniformLocation shaderProg "model") with (distribute $ triangleTransformation t) $ \ptr -> GL.glUniformMatrix4fv loc 1 0 (castPtr (ptr :: Ptr (M44 CFloat))) GL.drawArrays GL.Triangles 0 3 GLFW.swapBuffers win writeIORef tr (t + 0.1) -------------------------------------------------------------------------------- -- GLFW-b is made to be very close to the C API, so creating a window is pretty -- clunky by Haskell standards. A higher-level API would have some function -- like withWindow. withWindow :: Int -> Int -> String -> (GLFW.Window -> IO ()) -> IO () withWindow width height title f = do GLFW.setErrorCallback $ Just simpleErrorCallback r <- GLFW.init when r $ do m <- GLFW.createWindow width height title Nothing Nothing case m of (Just win) -> do GLFW.makeContextCurrent m f win GLFW.setErrorCallback $ Just simpleErrorCallback GLFW.destroyWindow win Nothing -> return () GLFW.terminate where simpleErrorCallback e s = putStrLn $ unwords [show e, show s] -------------------------------------------------------------------------------- -- Each callback does just one thing: write an appropriate Event to the events -- TQueue. errorCallback :: TQueue Event -> GLFW.Error -> String -> IO () windowPosCallback :: TQueue Event -> GLFW.Window -> Int -> Int -> IO () windowSizeCallback :: TQueue Event -> GLFW.Window -> Int -> Int -> IO () windowCloseCallback :: TQueue Event -> GLFW.Window -> IO () windowRefreshCallback :: TQueue Event -> GLFW.Window -> IO () windowFocusCallback :: TQueue Event -> GLFW.Window -> GLFW.FocusState -> IO () windowIconifyCallback :: TQueue Event -> GLFW.Window -> GLFW.IconifyState -> IO () framebufferSizeCallback :: TQueue Event -> GLFW.Window -> Int -> Int -> IO () mouseButtonCallback :: TQueue Event -> GLFW.Window -> GLFW.MouseButton -> GLFW.MouseButtonState -> GLFW.ModifierKeys -> IO () cursorPosCallback :: TQueue Event -> GLFW.Window -> Double -> Double -> IO () cursorEnterCallback :: TQueue Event -> GLFW.Window -> GLFW.CursorState -> IO () scrollCallback :: TQueue Event -> GLFW.Window -> Double -> Double -> IO () keyCallback :: TQueue Event -> GLFW.Window -> GLFW.Key -> Int -> GLFW.KeyState -> GLFW.ModifierKeys -> IO () charCallback :: TQueue Event -> GLFW.Window -> Char -> IO () errorCallback tc e s = atomically $ writeTQueue tc $ EventError e s windowPosCallback tc win x y = atomically $ writeTQueue tc $ EventWindowPos win x y windowSizeCallback tc win w h = atomically $ writeTQueue tc $ EventWindowSize win w h windowCloseCallback tc win = atomically $ writeTQueue tc $ EventWindowClose win windowRefreshCallback tc win = atomically $ writeTQueue tc $ EventWindowRefresh win windowFocusCallback tc win fa = atomically $ writeTQueue tc $ EventWindowFocus win fa windowIconifyCallback tc win ia = atomically $ writeTQueue tc $ EventWindowIconify win ia framebufferSizeCallback tc win w h = atomically $ writeTQueue tc $ EventFramebufferSize win w h mouseButtonCallback tc win mb mba mk = atomically $ writeTQueue tc $ EventMouseButton win mb mba mk cursorPosCallback tc win x y = atomically $ writeTQueue tc $ EventCursorPos win x y cursorEnterCallback tc win ca = atomically $ writeTQueue tc $ EventCursorEnter win ca scrollCallback tc win x y = atomically $ writeTQueue tc $ EventScroll win x y keyCallback tc win k sc ka mk = atomically $ writeTQueue tc $ EventKey win k sc ka mk charCallback tc win c = atomically $ writeTQueue tc $ EventChar win c -------------------------------------------------------------------------------- runDemo :: Env -> State -> IO () runDemo env state = do printInstructions void $ evalRWST (adjustWindow >> run) env state run :: Demo () run = do win <- asks envWindow draw liftIO $ do GLFW.swapBuffers win GL.flush -- not necessary, but someone recommended it GLFW.pollEvents processEvents state <- get if stateDragging state then do let sodx = stateDragStartX state sody = stateDragStartY state sodxa = stateDragStartXAngle state sodya = stateDragStartYAngle state (x, y) <- liftIO $ GLFW.getCursorPos win let myrot = (x - sodx) / 2 mxrot = (y - sody) / 2 put $ state { stateXAngle = sodxa + mxrot , stateYAngle = sodya + myrot } else do (kxrot, kyrot) <- liftIO $ getCursorKeyDirections win (jxrot, jyrot) <- liftIO $ getJoystickDirections GLFW.Joystick'1 put $ state { stateXAngle = stateXAngle state + (2 * kxrot) + (2 * jxrot) , stateYAngle = stateYAngle state + (2 * kyrot) + (2 * jyrot) } mt <- liftIO GLFW.getTime modify $ \s -> s { stateGearZAngle = maybe 0 (realToFrac . (100*)) mt } q <- liftIO $ GLFW.windowShouldClose win unless q run processEvents :: Demo () processEvents = do tc <- asks envEventsChan me <- liftIO $ atomically $ tryReadTQueue tc case me of Just e -> do processEvent e processEvents Nothing -> return () processEvent :: Event -> Demo () processEvent ev = case ev of (EventError e s) -> do printEvent "error" [show e, show s] win <- asks envWindow liftIO $ GLFW.setWindowShouldClose win True (EventWindowPos _ x y) -> printEvent "window pos" [show x, show y] (EventWindowSize _ width height) -> printEvent "window size" [show width, show height] (EventWindowClose _) -> printEvent "window close" [] (EventWindowRefresh _) -> printEvent "window refresh" [] (EventWindowFocus _ fs) -> printEvent "window focus" [show fs] (EventWindowIconify _ is) -> printEvent "window iconify" [show is] (EventFramebufferSize _ width height) -> do printEvent "framebuffer size" [show width, show height] modify $ \s -> s { stateWindowWidth = width , stateWindowHeight = height } adjustWindow (EventMouseButton _ mb mbs mk) -> do printEvent "mouse button" [show mb, show mbs, showModifierKeys mk] when (mb == GLFW.MouseButton'1) $ do let pressed = mbs == GLFW.MouseButtonState'Pressed modify $ \s -> s { stateMouseDown = pressed } unless pressed $ modify $ \s -> s { stateDragging = False } (EventCursorPos _ x y) -> do let x' = round x :: Int y' = round y :: Int printEvent "cursor pos" [show x', show y'] state <- get when (stateMouseDown state && not (stateDragging state)) $ put $ state { stateDragging = True , stateDragStartX = x , stateDragStartY = y , stateDragStartXAngle = stateXAngle state , stateDragStartYAngle = stateYAngle state } (EventCursorEnter _ cs) -> printEvent "cursor enter" [show cs] (EventScroll _ x y) -> do let x' = round x :: Int y' = round y :: Int printEvent "scroll" [show x', show y'] env <- ask modify $ \s -> s { stateZDist = let zDist' = stateZDist s + realToFrac (negate $ y / 2) in curb (envZDistClosest env) (envZDistFarthest env) zDist' } adjustWindow (EventKey win k scancode ks mk) -> do printEvent "key" [show k, show scancode, show ks, showModifierKeys mk] when (ks == GLFW.KeyState'Pressed) $ do -- Q, Esc: exit when (k == GLFW.Key'Q || k == GLFW.Key'Escape) $ liftIO $ GLFW.setWindowShouldClose win True -- ?: print instructions when (k == GLFW.Key'Slash && GLFW.modifierKeysShift mk) $ liftIO printInstructions -- i: print GLFW information when (k == GLFW.Key'I) $ liftIO $ printInformation win (EventChar _ c) -> printEvent "char" [show c] adjustWindow :: Demo () adjustWindow = do state <- get let width = stateWindowWidth state height = stateWindowHeight state zDist = stateZDist state let pos = GL.Position 0 0 size = GL.Size (fromIntegral width) (fromIntegral height) h = fromIntegral height / fromIntegral width :: Double znear = 1 :: Double zfar = 40 :: Double xmax = znear * 0.5 :: Double liftIO $ do GL.viewport GL.$= (pos, size) GL.matrixMode GL.$= GL.Projection GL.loadIdentity GL.frustum (realToFrac $ -xmax) (realToFrac xmax) (realToFrac $ -xmax * realToFrac h) (realToFrac $ xmax * realToFrac h) (realToFrac znear) (realToFrac zfar) GL.matrixMode GL.$= GL.Modelview 0 GL.loadIdentity GL.translate (GL.Vector3 0 0 (negate $ realToFrac zDist) :: GL.Vector3 GL.GLfloat) draw :: Demo () draw = do env <- ask state <- get let gear1 = envGear1 env gear2 = envGear2 env gear3 = envGear3 env xa = stateXAngle state ya = stateYAngle state za = stateZAngle state ga = stateGearZAngle state liftIO $ do GL.clear [GL.ColorBuffer, GL.DepthBuffer] GL.preservingMatrix $ do GL.rotate (realToFrac xa) xunit GL.rotate (realToFrac ya) yunit GL.rotate (realToFrac za) zunit GL.preservingMatrix $ do GL.translate gear1vec GL.rotate (realToFrac ga) zunit GL.callList gear1 GL.preservingMatrix $ do GL.translate gear2vec GL.rotate (-2 * realToFrac ga - 9) zunit GL.callList gear2 GL.preservingMatrix $ do GL.translate gear3vec GL.rotate (-2 * realToFrac ga - 25) zunit GL.callList gear3 where gear1vec = GL.Vector3 (-3) (-2) 0 :: GL.Vector3 GL.GLfloat gear2vec = GL.Vector3 3.1 (-2) 0 :: GL.Vector3 GL.GLfloat gear3vec = GL.Vector3 (-3.1) 4.2 0 :: GL.Vector3 GL.GLfloat xunit = GL.Vector3 1 0 0 :: GL.Vector3 GL.GLfloat yunit = GL.Vector3 0 1 0 :: GL.Vector3 GL.GLfloat zunit = GL.Vector3 0 0 1 :: GL.Vector3 GL.GLfloat getCursorKeyDirections :: GLFW.Window -> IO (Double, Double) getCursorKeyDirections win = do x0 <- isPress `fmap` GLFW.getKey win GLFW.Key'Up x1 <- isPress `fmap` GLFW.getKey win GLFW.Key'Down y0 <- isPress `fmap` GLFW.getKey win GLFW.Key'Left y1 <- isPress `fmap` GLFW.getKey win GLFW.Key'Right let x0n = if x0 then (-1) else 0 x1n = if x1 then 1 else 0 y0n = if y0 then (-1) else 0 y1n = if y1 then 1 else 0 return (x0n + x1n, y0n + y1n) getJoystickDirections :: GLFW.Joystick -> IO (Double, Double) getJoystickDirections js = do maxes <- GLFW.getJoystickAxes js return $ case maxes of (Just (x:y:_)) -> (-y, x) _ -> ( 0, 0) isPress :: GLFW.KeyState -> Bool isPress GLFW.KeyState'Pressed = True isPress GLFW.KeyState'Repeating = True isPress _ = False -------------------------------------------------------------------------------- printInstructions :: IO () printInstructions = putStrLn $ render $ nest 4 ( text "------------------------------------------------------------" $+$ text "'?': Print these instructions" $+$ text "'i': Print GLFW information" $+$ text "" $+$ text "* Mouse cursor, keyboard cursor keys, and/or joystick" $+$ text " control rotation." $+$ text "* Mouse scroll wheel controls distance from scene." $+$ text "------------------------------------------------------------" ) printInformation :: GLFW.Window -> IO () printInformation win = do version <- GLFW.getVersion versionString <- GLFW.getVersionString monitorInfos <- runMaybeT getMonitorInfos joystickNames <- getJoystickNames clientAPI <- GLFW.getWindowClientAPI win cv0 <- GLFW.getWindowContextVersionMajor win cv1 <- GLFW.getWindowContextVersionMinor win cv2 <- GLFW.getWindowContextVersionRevision win robustness <- GLFW.getWindowContextRobustness win forwardCompat <- GLFW.getWindowOpenGLForwardCompat win debug <- GLFW.getWindowOpenGLDebugContext win profile <- GLFW.getWindowOpenGLProfile win putStrLn $ render $ nest 4 ( text "------------------------------------------------------------" $+$ text "GLFW C library:" $+$ nest 4 ( text "Version:" <+> renderVersion version $+$ text "Version string:" <+> renderVersionString versionString ) $+$ text "Monitors:" $+$ nest 4 ( renderMonitorInfos monitorInfos ) $+$ text "Joysticks:" $+$ nest 4 ( renderJoystickNames joystickNames ) $+$ text "OpenGL context:" $+$ nest 4 ( text "Client API:" <+> renderClientAPI clientAPI $+$ text "Version:" <+> renderContextVersion cv0 cv1 cv2 $+$ text "Robustness:" <+> renderContextRobustness robustness $+$ text "Forward compatibility:" <+> renderForwardCompat forwardCompat $+$ text "Debug:" <+> renderDebug debug $+$ text "Profile:" <+> renderProfile profile ) $+$ text "------------------------------------------------------------" ) where renderVersion (GLFW.Version v0 v1 v2) = text $ intercalate "." $ map show [v0, v1, v2] renderVersionString = text . show renderMonitorInfos = maybe (text "(error)") (vcat . map renderMonitorInfo) renderMonitorInfo (name, (x,y), (w,h), vms) = text (show name) $+$ nest 4 ( location <+> size $+$ fsep (map renderVideoMode vms) ) where location = int x <> text "," <> int y size = int w <> text "x" <> int h <> text "mm" renderVideoMode (GLFW.VideoMode w h r g b rr) = brackets $ res <+> rgb <+> hz where res = int w <> text "x" <> int h rgb = int r <> text "x" <> int g <> text "x" <> int b hz = int rr <> text "Hz" renderJoystickNames pairs = vcat $ map (\(js, name) -> text (show js) <+> text (show name)) pairs renderContextVersion v0 v1 v2 = hcat [int v0, text ".", int v1, text ".", int v2] renderClientAPI = text . show renderContextRobustness = text . show renderForwardCompat = text . show renderDebug = text . show renderProfile = text . show type MonitorInfo = (String, (Int,Int), (Int,Int), [GLFW.VideoMode]) getMonitorInfos :: MaybeT IO [MonitorInfo] getMonitorInfos = getMonitors >>= mapM getMonitorInfo where getMonitors :: MaybeT IO [GLFW.Monitor] getMonitors = MaybeT GLFW.getMonitors getMonitorInfo :: GLFW.Monitor -> MaybeT IO MonitorInfo getMonitorInfo mon = do name <- getMonitorName mon vms <- getVideoModes mon MaybeT $ do pos <- liftIO $ GLFW.getMonitorPos mon size <- liftIO $ GLFW.getMonitorPhysicalSize mon return $ Just (name, pos, size, vms) getMonitorName :: GLFW.Monitor -> MaybeT IO String getMonitorName mon = MaybeT $ GLFW.getMonitorName mon getVideoModes :: GLFW.Monitor -> MaybeT IO [GLFW.VideoMode] getVideoModes mon = MaybeT $ GLFW.getVideoModes mon getJoystickNames :: IO [(GLFW.Joystick, String)] getJoystickNames = catMaybes `fmap` mapM getJoystick joysticks where getJoystick js = fmap (maybe Nothing (\name -> Just (js, name))) (GLFW.getJoystickName js) -------------------------------------------------------------------------------- printEvent :: String -> [String] -> Demo () printEvent cbname fields = liftIO $ putStrLn $ cbname ++ ": " ++ unwords fields showModifierKeys :: GLFW.ModifierKeys -> String showModifierKeys mk = "[mod keys: " ++ keys ++ "]" where keys = if null xs then "none" else unwords xs xs = catMaybes ys ys = [ if GLFW.modifierKeysShift mk then Just "shift" else Nothing , if GLFW.modifierKeysControl mk then Just "control" else Nothing , if GLFW.modifierKeysAlt mk then Just "alt" else Nothing , if GLFW.modifierKeysSuper mk then Just "super" else Nothing ] curb :: Ord a => a -> a -> a -> a curb l h x | x < l = l | x > h = h | otherwise = x -------------------------------------------------------------------------------- joysticks :: [GLFW.Joystick] joysticks = [ GLFW.Joystick'1 , GLFW.Joystick'2 , GLFW.Joystick'3 , GLFW.Joystick'4 , GLFW.Joystick'5 , GLFW.Joystick'6 , GLFW.Joystick'7 , GLFW.Joystick'8 , GLFW.Joystick'9 , GLFW.Joystick'10 , GLFW.Joystick'11 , GLFW.Joystick'12 , GLFW.Joystick'13 , GLFW.Joystick'14 , GLFW.Joystick'15 , GLFW.Joystick'16 ]