{-| Module : Graphics.Rendering.Handa.Viewer Copyright : (c) 2015 Brian W Bush License : MIT Maintainer : Brian W Bush Stability : Stable Portability : Portable Functions for managing perspectives and frusta. -} {-# LANGUAGE DeriveAnyClass #-} {-# LANGUAGE DeriveDataTypeable #-} {-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE RecordWildCards #-} module Graphics.Rendering.Handa.Viewer ( -- * Viewer Geometry ViewerParameters(..) , viewerGeometry , displayAspectRatio , displayThrowRatio , fieldOfView -- * Typical Devices , phoneViewer , laptopViewer , desktopViewer , projectorViewer , glassesViewer -- * Callbacks and Rendering , reshape , loadViewer , dlpViewerDisplay , dlpViewerDisplay' ) where import Data.AdditiveGroup (AdditiveGroup) import Data.AffineSpace ((.+^)) import Data.Aeson (FromJSON) import Data.Binary (Binary) import Data.Data (Data) import Data.VectorSpace ((*^)) import Data.Default (Default, def) import Data.IORef (IORef) import Foreign.Storable (Storable) import GHC.Generics (Generic) import Graphics.Rendering.DLP (DlpEncoding, DlpEye(..)) import Graphics.Rendering.DLP.Callbacks (DlpDisplay(..)) import Graphics.Rendering.Handa.Projection (OffAxisProjection(VTKOffAxis), Screen(..), aspectRatio, projection, throwRatio) import Graphics.Rendering.Handa.Util (degree) import Graphics.Rendering.OpenGL (GLdouble, MatrixComponent, MatrixMode(..), Position(..), Vector3(..), Vertex3(..), ($=!), get, loadIdentity, lookAt, matrixMode, scale, viewport) import Graphics.Rendering.OpenGL.GL.Tensor.Instances () import Graphics.UI.GLUT (DisplayCallback, ReshapeCallback) -- | Paramaters specifying a viewer, including the frustum of the view. data ViewerParameters a = ViewerParameters { screen :: Screen a -- ^ The screen location. , nearPlane :: a -- ^ The distance to the near plane of the frustum. , farPlane :: a -- ^ The distance to the far plane of the frustum. , eyePosition :: Vertex3 a -- ^ The position of the eyes. , eyeSeparation :: Vector3 a -- ^ The separation between the eyes. , eyeUpward :: Vector3 a -- ^ The upward direction. , sceneCenter :: Vertex3 a -- ^ The center of the scene. , sceneScale :: Vector3 a -- ^ The factor by which to scale the scene. } deriving (Binary, Data, Eq, FromJSON, Generic, Read, Show) instance Functor ViewerParameters where fmap f ViewerParameters{..} = ViewerParameters { screen = fmap f screen , nearPlane = f nearPlane , farPlane = f farPlane , eyePosition = fmap f eyePosition , eyeSeparation = fmap f eyeSeparation , eyeUpward = fmap f eyeUpward , sceneCenter = fmap f sceneCenter , sceneScale = fmap f sceneScale } instance (Fractional a, Storable a) => Default (ViewerParameters a) where def = laptopViewer -- | Construct viewer geometry from physical geometry. viewerGeometry :: (Fractional a, Storable a) => a -- ^ The aspect ratio (width over height) of the screen or display. -> a -- ^ The throw ratio (distance over width) of the screen or display. -> a -- ^ The distance from the eyes to the screen or display, in inches. -> ViewerParameters a -- ^ The corresponding viewer parameters. viewerGeometry aspect throw distance = ViewerParameters { screen = Screen { lowerLeft = Vertex3 (- width / 2) (- height / 2) 0 , lowerRight = Vertex3 ( width / 2) (- height / 2) 0 , upperLeft = Vertex3 (- width / 2) ( height / 2) 0 } , nearPlane = 1 , farPlane = 5 * distance , eyePosition = Vertex3 0 0 distance , eyeSeparation = Vector3 (typical * (1 - comfort)) 0 0 , eyeUpward = Vector3 0 1 0 , sceneCenter = Vertex3 0 0 1 , sceneScale = size *^ Vector3 aspect 1 1 } where width = distance / throw height = width / aspect margin = 0.10 -- 10% margin size = (1 - margin) / (1 / height + 1 / distance) -- See : typical = 2.5 -- inches for typical maximum eye separation comfort = 0.50 -- 25% reduction in eye separation for comforat at mid-range focus -- | Viewer parameters for a typical smartphone screen. phoneViewer :: (Fractional a, Storable a) => ViewerParameters a phoneViewer = viewerGeometry (1280 / 768) 0.5 18 -- | Viewer parameters for a typical laptop screen. laptopViewer :: (Fractional a, Storable a) => ViewerParameters a laptopViewer = viewerGeometry (1920 / 1080) 1.8 24 -- | Viewer parameters for a typical desktop monitor. desktopViewer :: (Fractional a, Storable a) => ViewerParameters a desktopViewer = viewerGeometry (1920 / 1080) 1.6 32 -- | Viewer parameters for a typical projector. projectorViewer :: (Fractional a, Storable a) => ViewerParameters a projectorViewer = viewerGeometry 1.6 1.5 36 -- | Viewer parameters for typical VR glasses. glassesViewer :: (Fractional a, Storable a) => ViewerParameters a glassesViewer = viewerGeometry (realToFrac $ pixelWidth / pixelHeight) (distance / width) $ 0.2 * distance where distance = 4 * 39.37 pixelWidth = 852 pixelHeight = 480 pixelDiagonal = sqrt (pixelWidth * pixelWidth + pixelHeight * pixelHeight) width = 100 * realToFrac (pixelWidth / pixelDiagonal :: Double) -- | The aspect ratio of the viewer. displayAspectRatio :: (AdditiveGroup a, RealFloat a, Storable a) => ViewerParameters a -- ^ The viewer parameters. -> a -- ^ The aspect ratio, namely the screen width divided by its height. displayAspectRatio ViewerParameters{..} = aspectRatio screen -- | The throw ratio of the viewer. displayThrowRatio :: (AdditiveGroup a, RealFloat a, Storable a) => ViewerParameters a -- ^ The viewer parameters. -> a -- ^ The throw ratio, namely the distance to the screen divided by its height. displayThrowRatio ViewerParameters{..} = throwRatio screen eyePosition -- | Compute the field of view for viewer parameters. fieldOfView :: (AdditiveGroup a, RealFloat a, Storable a) => ViewerParameters a -- ^ The viewer parameters -> a -- ^ The field of view, in degrees. fieldOfView ViewerParameters{..} = 2 * atan2 0.5 (throwRatio screen eyePosition) * degree -- | Construct a reshape callback from viewer parameters. This simply sets the frustum based on the viewer parameters and the size of the viewport. reshape :: (AdditiveGroup a, MatrixComponent a, RealFloat a, Storable a) => ViewerParameters a -- ^ The viewer parameters. -> ReshapeCallback -- ^ The reshape callback. reshape ViewerParameters{..} wh = do viewport $=! (Position 0 0, wh) matrixMode $=! Projection loadIdentity projection VTKOffAxis screen eyePosition nearPlane farPlane matrixMode $=! Modelview 0 -- | Create an action look at the scene according to the viewer parameters. loadViewer :: (AdditiveGroup a, MatrixComponent a, RealFloat a, Storable a) => Bool -- ^ Whether to use an on-axis projection. -> ViewerParameters a -- ^ The viewer parameters. -> DlpEye -- ^ The eye from which to view. -> IO () -- ^ An action for looking at the scene using the specified eye and viewer parameters. loadViewer onAxis ViewerParameters{..} eye = let offset = case eye of LeftDlp -> -0.5 RightDlp -> 0.5 eyePosition' = eyePosition .+^ offset *^ eyeSeparation Vector3 sx sy sz = realToFrac <$> sceneScale :: Vector3 GLdouble in if onAxis then do loadIdentity lookAt (realToFrac <$> eyePosition') (realToFrac <$> sceneCenter) (realToFrac <$> eyeUpward) scale sx sy sz else do matrixMode $=! Projection loadIdentity projection VTKOffAxis screen eyePosition' nearPlane farPlane matrixMode $=! Modelview 0 loadIdentity scale sx sy sz -- | Construct a DLP display from a display callback. dlpViewerDisplay :: (AdditiveGroup a, MatrixComponent a, RealFloat a, Storable a) => Bool -- ^ Whether to use on-axis projection. -> DlpEncoding -- ^ The DLP encoding. -> ViewerParameters a -- ^ The viewer parameters. -> DisplayCallback -- ^ The display callback. -> DlpDisplay -- ^ The DLP display data for using the specified encoding, viewer parameters, and display callback. dlpViewerDisplay onAxis encoding viewerParameters display = def { dlpEncoding = encoding , doDisplay = \eye -> loadViewer onAxis viewerParameters eye >> display } -- | Construct a DLP display from a display callback. dlpViewerDisplay' :: (AdditiveGroup a, MatrixComponent a, RealFloat a, Storable a) => Bool -- ^ Whether to use on-axis projection. -> DlpEncoding -- ^ The DLP encoding. -> IORef (ViewerParameters a) -- ^ A reference to the viewer parameters. -> DisplayCallback -- ^ The display callback. -> DlpDisplay -- ^ The DLP display data for using the specified encoding, viewer parameters, and display callback. dlpViewerDisplay' onAxis encoding viewerParameters display = def { dlpEncoding = encoding , doDisplay = \eye -> do viewerParameters' <- get viewerParameters loadViewer onAxis viewerParameters' eye display }