raytrace-0.1.0.0: src/Graphics/Ray.hs
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE InstanceSigs #-}
{-# LANGUAGE FlexibleInstances #-}
module Graphics.Ray
( -- * Camera
CameraSettings(..), defaultCameraSettings
-- * Ray Tracing
, ToRandom(toRandom), raytrace
-- * Image IO
, readImage, writeImage, writeImageSqrt
-- * Re-exports
, module Graphics.Ray.Core
, module Graphics.Ray.Geometry
, module Graphics.Ray.Material
, module Graphics.Ray.Texture
, module Graphics.Ray.Noise
) where
import Graphics.Ray.Core
import Graphics.Ray.Geometry
import Graphics.Ray.Material
import Graphics.Ray.Texture
import Graphics.Ray.Noise
import Linear (V2(V2), V3(V3), (*^), (^*), normalize, cross, (^/), zero)
import System.Random (StdGen, random, splitGen)
import Data.Massiv.Array (B, D, S, U, Ix2((:.)), (!))
import qualified Data.Massiv.Array as A
import qualified Data.Massiv.Array.IO as I
import Graphics.Pixel.ColorSpace (SRGB, Linearity(Linear, NonLinear))
import qualified Graphics.Pixel.ColorSpace as C
import Control.Monad.State (State, state, evalState)
import Control.Monad (replicateM)
import Data.Functor.Identity (Identity, runIdentity)
data CameraSettings = CameraSettings
{ cs_center :: Point3 -- ^ Camera position
, cs_lookAt :: Point3 -- ^ Point for the camera to look at
, cs_up :: Vec3 -- ^ Camera \"up\" vector
, cs_vfov :: Double -- ^ Vertical field of view (in radians)
, cs_aspectRatio :: Double -- ^ Width-to-height ratio of image
, cs_imageWidth :: Int -- ^ Image width in pixels
, cs_samplesPerPixel :: Int -- ^ Number of top-level rays created per pixel
, cs_maxRecursionDepth :: Int -- ^ Number of times a ray can reflect before recursion stops
, cs_background :: Ray -> Color -- ^ Background color (which can depend on direction)
, cs_defocusAngle :: Double -- ^ If this is positive, the image will be somewhat blurry in the foreground and background,
-- with only a single plane in focus (like an image produced by a real camera)
, cs_focusDist :: Double -- ^ Distance from the camera to the plane of focus (only matters if defocus angle is nonzero)
}
-- | By default, the camera is positioned at the origin looking in the negative z direction, with the positive y direction being upward
-- (and the positive x direction being rightward). The remaining attributes are as follows:
--
-- @
-- cs_vfov = pi / 2
-- cs_aspectRatio = 1.0
-- cs_imageWidth = 100
-- cs_samplesPerPixel = 10
-- cs_maxRecursionDepth = 10
-- cs_background = const (V3 1 1 1)
-- cs_defocusAngle = 0.0
-- cs_focusDist = 10.0
-- @
defaultCameraSettings :: CameraSettings
defaultCameraSettings = CameraSettings
{ cs_center = V3 0 0 0
, cs_lookAt = V3 0 0 (-1)
, cs_up = V3 0 1 0
, cs_vfov = pi / 2
, cs_aspectRatio = 1.0
, cs_imageWidth = 100
, cs_samplesPerPixel = 10
, cs_maxRecursionDepth = 10
, cs_background = const (V3 1 1 1)
, cs_defocusAngle = 0.0
, cs_focusDist = 10.0
}
class ToRandom m where
toRandom :: m a -> State StdGen a
instance ToRandom Identity where
toRandom :: Identity a -> State StdGen a
toRandom = pure . runIdentity
instance ToRandom (State StdGen) where
toRandom :: State StdGen a -> State StdGen a
toRandom = id
-- | Produce an image from the given camera settings, world, and seed.
raytrace :: ToRandom m => CameraSettings -> Geometry m Material -> StdGen -> A.Matrix D Color
raytrace (CameraSettings {..}) (Geometry _ hitWorld) seed = let
imageHeight = round (fromIntegral cs_imageWidth / cs_aspectRatio)
viewportHeight = cs_focusDist * tan (cs_vfov / 2) * 2
viewportWidth = viewportHeight * fromIntegral cs_imageWidth / fromIntegral imageHeight
w = normalize (cs_center - cs_lookAt)
u = normalize (cross cs_up w)
v = cross w u
across = viewportWidth *^ u
down = -(viewportHeight *^ v)
topLeft = cs_center - w ^* cs_focusDist - across ^/ 2 - down ^/ 2
pixelU = across ^/ fromIntegral cs_imageWidth
pixelV = down ^/ fromIntegral imageHeight
defocusRadius = cs_focusDist * tan (cs_defocusAngle / 2)
defocusDiskU = u ^* defocusRadius
defocusDiskV = v ^* defocusRadius
sampleDefocusDisk :: State StdGen Point3
sampleDefocusDisk = do
V2 x y <- randomInUnitDisk
pure (cs_center + x *^ defocusDiskU + y *^ defocusDiskV)
samplePixel :: Int -> Int -> State StdGen Point3
samplePixel i j = do
x <- state random
y <- state random
pure (topLeft + (fromIntegral i + x) *^ pixelU + (fromIntegral j + y) *^ pixelV)
getRay :: Int -> Int -> State StdGen Ray
getRay i j = do
origin <- sampleDefocusDisk
target <- samplePixel i j
pure (Ray origin (target - origin))
rayColor :: Int -> Ray -> State StdGen Color
rayColor depth ray
| depth <= 0 = pure zero
| otherwise =
toRandom (hitWorld ray (0.0001, infinity)) >>= \case
Nothing -> pure (cs_background ray)
Just (hit, Material mat) ->
mat ray hit >>= \case
(emitted, Nothing) -> pure emitted
(emitted, Just (attenuation, ray')) -> do
c <- rayColor (depth - 1) ray'
pure (emitted + attenuation * c)
pixelColor :: Int -> Int -> State StdGen Color
pixelColor i j = do
colors <- replicateM cs_samplesPerPixel (getRay i j >>= rayColor cs_maxRecursionDepth)
pure (sum colors ^/ fromIntegral cs_samplesPerPixel)
-- array of random seeds for each pixel (constructed using splitGen)
seeds :: A.Matrix B StdGen
(_, seeds) = A.randomArrayS seed (A.Sz (imageHeight :. cs_imageWidth)) splitGen
in A.makeArray A.Par (A.Sz (imageHeight :. cs_imageWidth)) (\ix@(j :. i) -> evalState (pixelColor i j) (seeds ! ix))
-- | Read an image file, converting each pixel to linear RGB color space.
readImage :: FilePath -> IO (A.Matrix U Color)
readImage path = A.compute . A.map fromPixel <$> (I.readImageAuto path :: IO (A.Matrix S (C.Pixel (SRGB 'Linear) Double)))
where
fromPixel :: C.Pixel (SRGB 'Linear) Double -> Color
fromPixel (C.Pixel (C.ColorSRGB r g b)) = V3 r g b
-- | Write an array of linear RGB colors to an image file.
writeImage :: (A.Source r Color) => FilePath -> A.Matrix r Color -> IO ()
writeImage path m = I.writeImageAuto path (A.map toPixel m)
where
toPixel :: Color -> C.Pixel (SRGB 'Linear) Double
toPixel (V3 r g b) = C.Pixel (C.ColorSRGB r g b)
-- | Write an array to an image file, using a slightly incorrect color space conversion function.
-- This function exists for testing purposes.
writeImageSqrt :: (A.Source r Color) => FilePath -> A.Matrix r Color -> IO ()
writeImageSqrt path m = I.writeImageAuto path (A.map toPixel m)
where
toPixel :: Color -> C.Pixel (SRGB 'NonLinear) Double
toPixel (V3 r g b) = C.Pixel (C.ColorSRGB (sqrt r) (sqrt g) (sqrt b))