Gtk2Hs/Demos/GtkGLext/terrain.hs

From HaskellWiki
Jump to navigation Jump to search
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"