Gtk2Hs/Demos/GtkGLext/terrain.hs

From HaskellWiki
< Gtk2Hs
Revision as of 01:23, 20 June 2008 by Mrd (talk | contribs) (somewhat more involved gtkglext demo)
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)
Jump to navigation Jump to search
The printable version is no longer supported and may have rendering errors. Please update your browser bookmarks and please use the default browser print function instead.

module Main (main) where

import qualified Graphics.UI.Gtk as Gtk import Graphics.UI.Gtk (AttrOp((:=))) import qualified Graphics.UI.Gtk.OpenGL as GtkGL

import Graphics.Rendering.OpenGL as GL import Data.Maybe (fromMaybe) import Data.Array import Data.Array.Base (unsafeRead) import Data.Array.Storable import Data.Word import Data.IntSet as IS import Data.IORef import Control.Monad (forM_)

data ProgramState = PS { keysPressed :: IntSet

                      , px          :: GLfloat
                      , py          :: GLfloat
                      , pz          :: GLfloat
                      , heading     :: GLfloat
                      , pitch       :: GLfloat
                      , dx          :: GLfloat
                      , dz          :: GLfloat
                      , dheading    :: GLfloat
                      , dpitch      :: GLfloat }

main :: IO () main = do

 Gtk.initGUI
 
 -- Initialise the Gtk+ OpenGL extension
 -- (including reading various command line parameters)
 GtkGL.initGL
 state <- newIORef $ PS { keysPressed = IS.empty 
                        , px          = 0
                        , py          = 0
                        , pz          = 5.0
                        , heading     = 0
                        , pitch       = 0
                        , dx          = 0
                        , dz          = 0
                        , dheading    = 0
                        , dpitch      = 0 }
 -- Load the image data and flip it.
 pb' <- loadImage
 pb  <- Gtk.pixbufFlipVertically pb' 
 -- We need a OpenGL frame buffer configuration to be able to create other
 -- OpenGL objects.
 glconfig <- GtkGL.glConfigNew [GtkGL.GLModeRGBA,
                                GtkGL.GLModeDepth,
                                GtkGL.GLModeDouble]
 
 -- Create an OpenGL drawing area widget
 canvas <- GtkGL.glDrawingAreaNew glconfig
 
 Gtk.widgetSetSizeRequest canvas canvasWidth canvasHeight
 -- Initialise some GL setting just before the canvas first gets shown
 -- (We can't initialise these things earlier since the GL resources that
 -- we are using wouldn't heve been setup yet)
 Gtk.onRealize canvas $ GtkGL.withGLDrawingArea canvas $ \_ -> do
   initialize pb
   reconfigure canvasWidth canvasHeight
   return ()
 -- Set the repaint handler
 Gtk.onExpose canvas $ \_ -> do
   GtkGL.withGLDrawingArea canvas $ \glwindow -> do
     GL.clear [GL.DepthBuffer, GL.ColorBuffer]
     display state
     GtkGL.glDrawableSwapBuffers glwindow
   return True


 -- Setup the animation
 Gtk.timeoutAddFull (do
   update state
   Gtk.widgetQueueDraw canvas
   return True)
   Gtk.priorityDefaultIdle animationWaitTime
 --------------------------------
 -- Setup the rest of the GUI:
 --
 window <- Gtk.windowNew
 Gtk.onDestroy window Gtk.mainQuit
 Gtk.set window [ Gtk.containerBorderWidth := 8,
                  Gtk.windowTitle := "Gtk2Hs + HOpenGL demo" ]
 vbox <- Gtk.vBoxNew False 4
 Gtk.set window [ Gtk.containerChild := vbox ]
 label <- Gtk.labelNew (Just "Gtk2Hs using OpenGL via HOpenGL!")
 button <- Gtk.buttonNewWithLabel "Close"
 Gtk.onClicked button Gtk.mainQuit
 Gtk.set vbox [ Gtk.containerChild := canvas,
                Gtk.containerChild := label,
                Gtk.containerChild := button ]
 -- "reshape" event handler
 Gtk.onConfigure canvas $ \ (Gtk.Configure _ _ _ w h) -> do
   (w', h') <- reconfigure w h
   texW   <- Gtk.pixbufGetWidth pb
   texH   <- Gtk.pixbufGetHeight pb
   texBPS <- Gtk.pixbufGetBitsPerSample pb
   texRS  <- Gtk.pixbufGetRowstride pb
   texNCh <- Gtk.pixbufGetNChannels pb
   Gtk.labelSetText label $ unwords ["Width:",show w',"Height:",show h',
                                     "TexW:",show texW,"TexH:",show texH,
                                     "BPS:",show texBPS,"RS:",show texRS,
                                     "NCh:",show texNCh]
   return True

 Gtk.onKeyPress window $ \ (Gtk.Key rel _ _ mods _ _ _ val name char) -> do
   keyEvent state rel mods val name char
 Gtk.onKeyRelease window $ \ (Gtk.Key rel _ _ mods _ _ _ val name char) -> do
   keyEvent state rel mods val name char
 Gtk.widgetShowAll window
 Gtk.mainGUI

update :: IORef ProgramState -> IO () update state = do

 ps@PS { dx       = dx
       , dz       = dz
       , px       = px
       , py       = py
       , pz       = pz
       , pitch    = pitch
       , heading  = heading
       , dpitch   = dpitch
       , dheading = dheading }
   <- readIORef state
 preservingMatrix $ do
   loadIdentity
   -- rotate to current heading and pitch
   GL.rotate pitch   (Vector3 1.0 0.0 0.0 :: Vector3 GLfloat)
   GL.rotate heading (Vector3 0.0 1.0 0.0 :: Vector3 GLfloat)
   -- perform motion
   translate (Vector3 (-dx) 0 (-dz))
   -- get changes in location components
   mat   <- get (matrix Nothing) :: IO (GLmatrix GLfloat)
   comps <- getMatrixComponents ColumnMajor mat
   let [dx, dy, dz, _] = drop 12 comps
       (heading', pitch') = (heading + dheading, pitch + dpitch)
   writeIORef state $
     ps { px      = px + dx
        , py      = py + dy
        , pz      = pz + dz
        , pitch   = pitch' 
        , heading = heading' }
        
 return ()

display :: IORef ProgramState -> IO () display state = do

 ps@PS { px       = px
       , py       = py
       , pz       = pz
       , pitch    = pitch
       , heading  = heading
       , dpitch   = dpitch
       , dheading = dheading }
   <- readIORef state
 loadIdentity
 GL.rotate (-pitch)   (Vector3 1.0 0.0 0.0 :: Vector3 GLfloat)
 GL.rotate (-heading) (Vector3 0.0 1.0 0.0 :: Vector3 GLfloat)
 translate (Vector3 (-px) (-py) (-pz))
 position (Light 0) $= Vertex4 0.0 0.0 (2.0) 1.0
 texture Texture2D $= Enabled
 color (Color4 1 1 1 1 :: Color4 GLfloat)
 preservingMatrix $ do
   translate (Vector3 (-10.0) (-1.0) 10.0 :: Vector3 GLfloat)
   GL.rotate (-90.0) (Vector3 1.0 0.0 0.0 :: Vector3 GLfloat)
   drawTerrain 20 20
 preservingMatrix $ do
   translate (Vector3 0.0 0.0 (-1.0) :: Vector3 GLfloat)
   drawPlane
 texture Texture2D $= Disabled
 color (Color4 0.0 0.0 1.0 1.0 :: Color4 GLfloat)
 preservingMatrix $ do
   translate (Vector3 0.0 2.0 0.0 :: Vector3 GLfloat)
   drawSphere

-- GLU Quadric example. drawSphere = do

 renderQuadric (QuadricStyle 
                 (Just Smooth)
                 GenerateTextureCoordinates
                 Outside
                 FillStyle)
               (Sphere 1.0 48 48)

drawPlane = do

 renderPrimitive Quads $ do
   glNormal3f(0.0,0.0,1.0)
   glTexCoord2f(0.0, 0.0); glVertex3f(-1.0, -1.0, 0.0);
   glTexCoord2f(1.0, 0.0); glVertex3f(1.0, -1.0, 0.0);
   glTexCoord2f(1.0, 1.0); glVertex3f(1.0, 1.0, 0.0);
   glTexCoord2f(0.0, 1.0); glVertex3f(-1.0, 1.0, 0.0);

drawTerrain :: GLfloat -> GLfloat -> IO () drawTerrain w h = do

 forM_ [0 .. h - 1] $ \ j ->
   renderPrimitive TriangleStrip $ do
     glNormal3f(0.0,0.0,1.0)
     glTexCoord2f(0.0,1.0+j); glVertex3f(0.0,1.0+j,0.0)
     glTexCoord2f(0.0,0.0+j); glVertex3f(0.0,0.0+j,0.0)
     forM_ [0 .. w - 1] $ \ i -> do
       glTexCoord2f(1.0+i,1.0+j); glVertex3f(1.0+i,1.0+j,0.0)
       glTexCoord2f(1.0+i,0.0+j); glVertex3f(1.0+i,0.0+j,0.0)

glTexCoord2f (x,y) = texCoord (TexCoord2 x y :: TexCoord2 GLfloat) glVertex3f (x,y,z) = vertex (Vertex3 x y z :: Vertex3 GLfloat) glNormal3f (x,y,z) = normal (Normal3 x y z :: Normal3 GLfloat)

reconfigure :: Int -> Int -> IO (Int, Int) reconfigure w h = do

 -- maintain aspect ratio
 let aspectRatio = (fromIntegral canvasWidth) / (fromIntegral canvasHeight)
     (w1, h1)    = (fromIntegral w, (fromIntegral w) / aspectRatio)
     (w2, h2)    = ((fromIntegral h) * aspectRatio, fromIntegral h)
     (w', h')    = if h1 <= fromIntegral h
                     then (floor w1, floor h1)
                     else (floor w2, floor h2)
 reshape $ Just (w', h') 
 return (w', h')

-- Called by reconfigure to fix the OpenGL viewport according to the -- dimensions of the widget, appropriately. reshape :: Maybe (Int, Int) -> IO () reshape dims = do

 let (width, height) = fromMaybe (canvasWidth, canvasHeight) dims
 viewport $= (Position 0 0, Size (fromIntegral width) (fromIntegral height))
 matrixMode $= Projection
 loadIdentity
 let (w, h) = if width <= height
               then (fromIntegral height, fromIntegral width )
               else (fromIntegral width,  fromIntegral height)
 perspective 60.0 (fromIntegral canvasWidth / fromIntegral canvasHeight) 1.0 20.0
 matrixMode $= Modelview 0
 loadIdentity

initialize :: Gtk.Pixbuf -> IO () initialize pb = do

 materialAmbient   Front $= Color4 0.4 0.4 0.4 1.0
 materialDiffuse   Front $= Color4 0.4 0.4 0.4 1.0
 materialSpecular  Front $= Color4 0.8 0.8 0.8 1.0
 materialShininess Front $= 25.0
 ambient  (Light 0) $= Color4 0.3 0.3 0.3 1.0
 diffuse  (Light 0) $= Color4 1.0 1.0 1.0 1.0
 specular (Light 0) $= Color4 0.8 0.8 0.8 1.0
 lightModelAmbient  $= Color4 0.2 0.2 0.2 1.0
 lighting        $= Enabled 
 light (Light 0) $= Enabled
 depthFunc       $= Just Less
 clearColor $= Color4 0.0 0.0 0.0 0.0
 drawBuffer $= BackBuffers
 colorMaterial $= Just (Front, Diffuse)
 textureWrapMode Texture2D S $= (Repeated, Repeat)
 textureWrapMode Texture2D T $= (Repeated, Repeat)
 textureFilter Texture2D $= ((Nearest, Nothing), Nearest)
 uploadTexture pb
 texture Texture2D $= Enabled
 shadeModel $= Smooth

-- A somewhat ugly function. Sorry. Gtk hands me the texture data as -- a PixbufData, but I need a C-style array to hand to OpenGL. So, -- this function reads the data byte by byte out of the PixbufData -- into a Storable array, and then can hand that address off to -- OpenGL as a GHC.Ptr. uploadTexture :: Gtk.Pixbuf -> IO () uploadTexture pb = do

 pbd <- Gtk.pixbufGetPixels pb :: IO (Gtk.PixbufData Int Word8)
 (l,u) <- getBounds pbd
 storray <- newArray (l,u) 0 :: IO (StorableArray Int Word8)
 forM_ [l .. u] $ \ i -> do
   x <- unsafeRead pbd (i - l)
   writeArray storray i x
 withStorableArray storray $ \ texPtr -> 
   texImage2D 
     Nothing NoProxy 0 RGBA' 
     (TextureSize2D 64 64) 0
     (PixelData RGBA UnsignedByte texPtr)

loadImage :: IO Gtk.Pixbuf loadImage = do

 putStrLn $ "Loading " ++ texFileName
 Gtk.pixbufNewFromFile texFileName 

keyEvent state rel mods val name char = do

 ps@PS { keysPressed = kp
       , dx          = dx
       , dz          = dz
       , px          = px
       , py          = py
       , pz          = pz
       , pitch       = pitch
       , heading     = heading
       , dpitch      = dpitch
       , dheading    = dheading }
   <- readIORef state
 -- Only process the key event if it is not a repeat
 if (fromIntegral val `member` kp && rel) ||
    (fromIntegral val `notMember` kp && not rel)
    then do
       let return' ps' b = do
             -- maintain list of currently pressed keys
             writeIORef state $! 
               if rel
                 then ps' { keysPressed = fromIntegral val `IS.delete` kp }
                 else ps' { keysPressed = fromIntegral val `IS.insert` kp }
             return b
           -- accept/decline to handle the key event
           accept ps'  = return' ps' True
           decline ps' = return' ps' False
       -- putStrLn $ unwords [name , show rel]  -- debugging
       -- process keys
       case rel of
         -- on PRESS only
         False
           | name == "Escape" -> Gtk.mainQuit >> accept ps
           | name == "e"      -> accept $ ps { dz = dz + deltaV }
           | name == "d"      -> accept $ ps { dz = dz - deltaV }
           | name == "w"      -> accept $ ps { dx = dx + deltaV }
           | name == "r"      -> accept $ ps { dx = dx - deltaV }
           | name == "s"      -> accept $ ps { dheading = dheading + deltaH }
           | name == "f"      -> accept $ ps { dheading = dheading - deltaH }
           | otherwise        -> decline ps
         -- on RELEASE only
         True
           | name == "e"      -> accept $ ps { dz = dz - deltaV }
           | name == "d"      -> accept $ ps { dz = dz + deltaV }
           | name == "w"      -> accept $ ps { dx = dx - deltaV }
           | name == "r"      -> accept $ ps { dx = dx + deltaV }
           | name == "s"      -> accept $ ps { dheading = dheading - deltaH }
           | name == "f"      -> accept $ ps { dheading = dheading + deltaH }
           | otherwise        -> decline ps
    else return True


animationWaitTime, canvasWidth, canvasHeight :: Int animationWaitTime = 3 canvasWidth = 640 canvasHeight = 480

deltaV = 0.02 deltaH = 0.35 deltaP = 0.04

texFileName = "terrain.xpm"