packages feed

dsmc-tools-0.1.0.0: src/Caster.hs

{-# LANGUAGE DeriveDataTypeable #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE BangPatterns #-}

{-|

Standalone raycasting runner for traceable objects.

|-}

import Prelude hiding (reverse)

import GHC.Float

import Graphics.Gloss.Data.Color
import Graphics.Gloss.Data.Display
import qualified Graphics.Gloss.Data.Point as G
import qualified Graphics.Gloss.Interface.Pure.Game as G
import Graphics.Gloss.Raster.Field hiding (Point)

import System.Console.CmdArgs.Implicit

import qualified Data.Strict.Maybe as S
import qualified Data.Strict.Tuple as S

import DSMC.Particles
import DSMC.Traceables
import DSMC.Traceables.Parser
import DSMC.Util.Vector


data InteractionMode = None | Rotate | Pan


-- | World state with observation point parameters and event helpers.
-- Roll is always 0.
data World = World { dist :: Double
                   -- ^ Distance to origin.
                   , pitch :: Double
                   , yaw :: Double
                   -- ^ Yaw of camera as if it was in origin.
                   , target :: Point
                   -- ^ Where camera looks at.
                   , holdPoint :: Maybe (Float, Float)
                   -- ^ Point where mouse button was held down.
                   , mode :: InteractionMode
                   }


-- | Static command line options for caster.
data Options = Options
    { bodyDef :: FilePath
    , width :: Int
    , height :: Int
    , pixels :: Int
    , brightRGBA :: (Float, Float, Float, Float)
    -- ^ Color for bright surfaces parallel to view plane (RGBA)
    , darkRGBA :: (Float, Float, Float, Float)
    -- ^ Color for dark surfaces perpendicular to view plane (RGBA)
    }
    deriving (Data, Typeable)


type Ray = Particle


-- | Pixels in meter at unit distance.
scaleFactor :: Double
scaleFactor = 50.0


-- | Initial world.
start :: World
start = World 5 0 0 origin Nothing None


uncurry4 :: (a -> b -> c -> d -> e) -> (a, b, c, d) -> e
uncurry4 f (a, b, c, d) = f a b c d


-- |  Scale deltas between hold and release coordinates by this number.
dragFactor :: Double
dragFactor = pi / 1800


-- | Change distance by this amount per one mouse wheel step.
zoomFactor :: Double
zoomFactor = 0.01


-- | Handle mouse drag to change pitch & yaw and mouse wheel to zoom.
handleEvents :: G.Event -> World -> World
handleEvents e w =
    case e of
      G.EventKey (G.MouseButton G.LeftButton) G.Down _ c ->
          w{holdPoint = Just c, mode = Rotate}
      G.EventKey (G.MouseButton G.RightButton) G.Down _ c ->
          w{holdPoint = Just c, mode = Pan}
      G.EventKey (G.MouseButton _) G.Up _ _ -> 
          w{holdPoint = Nothing}
      G.EventKey (G.MouseButton G.WheelDown) _ _ _ -> 
          w{dist = dist w + zoomFactor}
      G.EventKey (G.MouseButton G.WheelUp) _ _ _ -> 
          w{dist = dist w - zoomFactor}
      G.EventKey (G.Char 'r') G.Down _ _ ->
          w{ target = origin
           , yaw = 0
           , pitch = 0}
      G.EventMotion p@(x, y) ->
          case (holdPoint w) of
            Nothing -> w
            Just (u, v) -> 
                let
                    xdelta = (float2Double (x - u)) * dragFactor
                    ydelta = (float2Double (y - v)) * dragFactor
                in
                  case (mode w) of
                    Rotate -> w{ holdPoint = Just p
                               , yaw = (yaw w) - xdelta
                               , pitch = (pitch w) + ydelta
                               }
                    Pan -> 
                        let
                            !(_, sX, sY) = buildCartesian (yaw w) (pitch w)
                        in 
                          w{ holdPoint = Just p
                           , target = (target w) <+> (sX .^ xdelta) <-> (sY .^ ydelta)
                           }
                    _ -> w
      _ -> w


casterField :: Int
            -- ^ Window width.
            -> Int
            -- ^ Window height.
            -> Int
            -- ^ Pixels per point.
            -> Body
            -- ^ Body to show.
            -> Color
            -- ^ Bright color.
            -> Color
            -- ^ Dark color.
            -> IO ()
casterField width height pixels body bright' dark' =
    let
        display = InWindow "dsmc-tools CSG raycaster" (width, height) (0, 0)
        makePixel :: World -> G.Point -> Color
        !wS = fromIntegral (width `div` 2)
        !hS = fromIntegral (height `div` 2)
        makePixel !w !(x, y) =
            let
                !d = dist w
                !wScale = -(wS * d / scaleFactor)
                !hScale = (hS * d / scaleFactor)
                !(n, sX, sY) = buildCartesian (yaw w) (pitch w)
                !p = n .^ (-d) <+> target w
                ray :: Ray
                !ray = ((p
                        <+> (sX .^ ((float2Double x) * wScale))
                        <+> (sY .^ ((float2Double y) * hScale))), n)

                !hp = trace body ray
            in
              case hp of
                (((S.:!:) (HitPoint _ (S.Just hn)) _):_) ->
                    mixColors factor (1 - factor) bright' dark'
                    where
                      factor = abs $ double2Float $ invert n .* hn
                _ -> white
        {-# INLINE makePixel #-}
    in
      playField display (pixels, pixels) 5 start makePixel
                    handleEvents
                    (flip const)


-- | Read body def and program arguments, run the actual caster on
-- success.
main :: IO ()
main =
    let
        sample = Options
                 { bodyDef = def &= argPos 0 &= typ "BODY-FILE"
                 , width = 500 &= help "Window width"
                 , height = 500 &= help "Window height"
                 , pixels = 1
                   &= help ("Number of pixels to draw per point," ++
                            "in each dimension")
                 , brightRGBA = (0.9, 0.9, 0.9, 1)
                   &= help ("Color for surface parallel to view plane," ++
                            " with each component within [0..1]")
                   &= typ "R,G,B,A"
                 , darkRGBA = (0, 0, 0, 1)
                   &= help "Color for surface perpendicular to view plane"
                   &= typ "R,G,B,A"
                 }
                 &= program "dsmc-caster"
    in do
      Options{..} <- cmdArgs $ sample
      body <- parseBodyFile bodyDef
      case body of
        Right b -> casterField width height pixels b
                   (uncurry4 makeColor brightRGBA)
                   (uncurry4 makeColor darkRGBA)
        Left e -> error $ "Problem when reading body definition: " ++ e