packages feed

csg-0.1: exe/raycaster.hs

{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE OverloadedStrings #-}

{-|

Standalone raycaster for CSG objects powered by Gloss.

|-}

import Prelude hiding (FilePath)

import Control.Applicative
import qualified Data.Strict.Maybe as S

import GHC.Float
import Data.String
import Data.Monoid
import Data.Version

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 Test.QuickCheck hiding ((><))
import Turtle.Options

import Filesystem.Path.CurrentOS

import Data.CSG
import Data.CSG.Parser

import Paths_csg

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
                   }


-- | Command line options for caster.
data Options = Options
    { geoFile    :: Maybe FilePath
    , width      :: Int
    , height     :: Int
    , pixels     :: Int
    , brightRGBA :: (Float, Float, Float, Float)
    -- ^ Color for bright surfaces parallel to view plane (RGB)
    , darkRGBA   :: (Float, Float, Float, Float)
    -- ^ Color for dark surfaces perpendicular to view plane (RGB)
    }


optParser :: Parser Options
optParser = Options
  <$> optional (argPath "geo-file" "Geometry definition file")
  <*> (optInt "width" 'w' "Window width" <|> pure 500)
  <*> (optInt "height" 'h' "Window height" <|> pure 500)
  <*> (optInt "pixels" 'p' "Pixels per ray (set to 1 for top quality)"
       <|> pure 1)
  <*> (optRead "bright-color" 'b'
       (fromString $
        "Color for bright surfaces (parallel to the view plane) " <>
        "as (R, G, B, A) with each component between 0.0 and 1.0")
       <|> pure (0.9, 0.9, 0.9, 1))
  <*> (optRead "dark-color" 'd' "Color for dark surfaces"
       <|> pure (0, 0, 0, 1))


-- | The factor between distance and view port width/height.
scaleFactor :: Double
scaleFactor = 225.0
{-# INLINE scaleFactor #-}


initialDistance :: Double
initialDistance = 225


-- | Initial world.
start :: World
start = World initialDistance 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 / 180


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


-- | 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
           , dist = initialDistance
           }
      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


-- | Build cartesian axes from yaw and pitch with 0 roll. Angles are
-- in radians.
buildCartesian :: Double -> Double -> (Vec3, Vec3, Vec3)
buildCartesian yaw pitch = (u, v, w)
    where u = fromXYZ (cos yaw * cos pitch, sin yaw * cos pitch, sin pitch)
          v = fromXYZ (- (sin yaw), cos yaw, 0)
          w = u >< v
{-# INLINE buildCartesian #-}


programName :: String
programName = "csg-raycaster " ++ showVersion version


casterField :: Int
            -- ^ Window width.
            -> Int
            -- ^ Window height.
            -> Int
            -- ^ Pixels per point.
            -> Solid
            -- ^ Solid to show.
            -> Color
            -- ^ Bright color.
            -> Color
            -- ^ Dark color.
            -> IO ()
casterField width height pixels solid bright' dark' =
    let
        display = InWindow programName (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 = Ray (p
                            <+> (sX .^ (float2Double x * wScale))
                            <+> (sY .^ (float2Double y * hScale)), n)
            in
              case ray `cast` solid of
                S.Just (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 solid def and program arguments, run the actual caster on
-- success.
main :: IO ()
main = do
    Options{..} <- options (fromString programName) optParser
    solid <-
      case geoFile of
        Nothing -> Right <$> generate arbitrary
        Just fp -> parseGeometryFile (encodeString fp)
    case solid of
        Right b -> do
          putStrLn $ "Rendering " <> show b
          casterField width height pixels b
            (uncurry4 makeColor brightRGBA)
            (uncurry4 makeColor darkRGBA)
        Left e -> error $ "Problem when reading solid definition: " ++ e