packages feed

keid-core-0.1.6.0: src/Engine/Camera.hs

module Engine.Camera
  ( ProjectionKind(..)
  , Projection(..)
  , ProjectionParams
  , ProjectionInput(..)
  , ProjectionProcess
  , spawnPerspective
  , mkTransformPerspective

  , spawnOrthoPixelsCentered
  , mkTransformOrthoPixelsCentered

  , spawnProjectionWith

  , spawnProjection
  , pattern PROJECTION_NEAR
  , pattern PROJECTION_FAR

  , View(..)
  , ViewProcess
  , ViewOrbitalInput(..)
  , initialOrbitalInput
  , mkViewOrbital
  , mkViewOrbital_
  ) where

import RIO

import Geomancy (Transform(..), Vec3, vec3)
import Geomancy.Transform qualified as Transform
import Geomancy.Quaternion qualified as Quaternion
import Geomancy.Vec3 qualified as Vec3
import Geomancy.Vulkan.Projection qualified as Projection
import Geomancy.Vulkan.View qualified as View
import Vulkan.Core10 qualified as Vk
import Vulkan.NamedType ((:::))

import Engine.Types qualified as Engine
import Engine.Worker qualified as Worker

-- * Projection

data ProjectionKind
  = Perspective
  | Orthographic
  deriving (Eq, Ord, Show, Enum, Bounded, Generic)

data Projection (pk :: ProjectionKind) = Projection
  { projectionTransform :: Transform
  , projectionInverse   :: ~Transform
  }
  deriving (Show, Generic)

type ProjectionProcess pk = Worker.Cell (ProjectionInput pk) (Projection pk)

data ProjectionInput (pk :: ProjectionKind) = ProjectionInput
  { projectionParams :: ProjectionParams pk
  , projectionNear   :: Float
  , projectionFar    :: Float
  }

-- XXX: undecidable
-- deriving instance (Show (ProjectionParams pk)) => Show (ProjectionInput pk)

type family ProjectionParams (pk :: ProjectionKind) where
  ProjectionParams 'Perspective = "fov-v" ::: Float
  ProjectionParams 'Orthographic = ()

spawnProjection
  :: (Vk.Extent2D -> ProjectionInput pk -> Transform)
  -> ProjectionParams pk
  -> Engine.StageRIO env (ProjectionProcess pk)
spawnProjection mkTransform params =
  spawnProjectionWith mkTransform ProjectionInput
    { projectionNear    = PROJECTION_NEAR
    , projectionFar     = PROJECTION_FAR
    , projectionParams  = params
    }

pattern PROJECTION_NEAR :: (Eq a, Num a, Fractional a) => a
pattern PROJECTION_NEAR = 0x0.02 -- i.e. 1/2048

pattern PROJECTION_FAR :: (Eq a, Num a) => a
pattern PROJECTION_FAR = 16384

spawnProjectionWith
  :: (Vk.Extent2D -> ProjectionInput pk -> Transform)
  -> ProjectionInput pk
  -> Engine.StageRIO env (ProjectionProcess pk)
spawnProjectionWith mkTransform projectionInput = do
  screen <- Engine.askScreenVar
  input <- Worker.newVar projectionInput
  fmap (input,) $
    Worker.spawnMerge2
      (\s i ->
          let
            transform = mkTransform s i
          in
            Projection
              { projectionTransform = transform
              , projectionInverse   = Transform.inverse transform -- XXX: better provide an inverse directly
              }
      )
      screen
      input

spawnPerspective :: Engine.StageRIO env (ProjectionProcess 'Perspective)
spawnPerspective = spawnProjection mkTransformPerspective (τ / 4)

mkTransformPerspective :: Vk.Extent2D -> ProjectionInput 'Perspective -> Transform
mkTransformPerspective Vk.Extent2D{width, height} ProjectionInput{..} =
  Projection.perspective
    projectionParams
    projectionNear
    projectionFar
    width
    height

spawnOrthoPixelsCentered :: Engine.StageRIO env (ProjectionProcess 'Orthographic)
spawnOrthoPixelsCentered = spawnProjectionWith mkTransformOrthoPixelsCentered ProjectionInput
  { projectionNear   = 0
  , projectionFar    = 1
  , projectionParams = ()
  }

mkTransformOrthoPixelsCentered :: Vk.Extent2D -> ProjectionInput 'Orthographic -> Transform
mkTransformOrthoPixelsCentered Vk.Extent2D{width, height} ProjectionInput{..} =
  Projection.orthoOffCenter projectionNear projectionFar width height

-- * View

data View = View
  { viewTransform    :: Transform
  , viewTransformInv :: Transform
  , viewPosition     :: Vec3
  , viewDirection    :: Vec3
  }
  deriving (Show)

type ViewProcess = Worker.Cell ViewOrbitalInput View

-- | Camera orbiting its target
data ViewOrbitalInput = ViewOrbitalInput
  { orbitAzimuth  :: Float
  , orbitAscent   :: Float
  , orbitDistance :: Float
  , orbitScale    :: Float
  , orbitTarget   :: Vec3
  , orbitUp       :: Vec3
  , orbitRight    :: Vec3
  }
  deriving (Show)

initialOrbitalInput :: ViewOrbitalInput
initialOrbitalInput = ViewOrbitalInput
  { orbitAzimuth  = 0 -- τ/8
  , orbitAscent   = τ/7
  , orbitDistance = 8.0
  , orbitScale    = 1
  , orbitTarget   = 0
  , orbitUp       = vec3 0 (-1) 0
  , orbitRight    = vec3 1 0 0
  }

mkViewOrbital :: Vec3 -> ViewOrbitalInput -> View
mkViewOrbital cameraTarget ViewOrbitalInput{..} = View{..}
  where
    viewTransform = View.lookAt viewPosition cameraTarget orbitUp
    viewTransformInv = Transform.inverse viewTransform

    viewPosition =
      orbitTarget +
      Quaternion.rotate
        ( Quaternion.axisAngle orbitUp orbitAzimuth *
          Quaternion.axisAngle orbitRight orbitAscent
        )
        (vec3 0 0 $ orbitDistance * orbitScale)

    viewDirection = Vec3.normalize $ cameraTarget - viewPosition

{-# INLINE mkViewOrbital_ #-}
mkViewOrbital_ :: ViewOrbitalInput -> View
mkViewOrbital_ voi = mkViewOrbital (orbitTarget voi) voi

τ :: Float
τ = 2 * pi