raytrace-0.2.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, dot)
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)
import Data.Maybe (listToMaybe)
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)
, cs_redirectTargets :: [(Double, Point3, Vec3, Vec3)]
-- ^ List of parallelograms to send scattered rays toward (if the material allows it) along with
-- probabilities, which should add up to less than 1. Sending more rays toward lights in
-- scenes with small light sources can lead to faster convergence (i.e. less noise).
}
-- | 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
-- cs_redirectTargets = []
-- @
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
, cs_redirectTargets = []
}
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
-- [private]
data RedirectTarget = RedirectTarget
{ rt_origin :: Point3
, rt_U :: Vec3
, rt_V :: Vec3
, rt_cross :: Vec3
, rt_hit :: Ray -> Maybe Double
}
-- | 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
redirectTargets = flip map cs_redirectTargets $ \(_, q, s0, s1) -> RedirectTarget
{ rt_origin = q
, rt_U = s0
, rt_V = s1
, rt_cross = cross s0 s1
, rt_hit =
let Geometry _ hit = parallelogram q s0 s1 in
\ray -> fmap (hr_t . fst) (runIdentity (hit undefined ray (0, infinity)))
}
probs = map (\(p, _, _, _) -> p) cs_redirectTargets
remProb = 1 - sum probs
thresholds = zip redirectTargets (scanl1 (+) probs)
getTarget r = fmap fst (listToMaybe (dropWhile ((r >=) . snd) thresholds))
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 (normalize (target - origin)))
rayColor :: Int -> Double -> Ray -> State StdGen Color
rayColor depth time ray@(Ray _ rayDir)
| depth <= 0 = pure zero
| otherwise =
toRandom (hitWorld time ray (0.0001, infinity)) >>= \case
Nothing -> pure (cs_background ray)
Just (hit, Material mat) -> do
let (emitted, genRes) = mat rayDir hit
res <- genRes
(emitted +) <$> case res of
Absorb -> pure zero
Scatter attenuation dir -> (attenuation *) <$> rayColor (depth - 1) time (Ray (hr_point hit) dir)
HemisphereF matF -> do
choice <- getTarget <$> state random
dir <- case choice of
Nothing -> do
uu <- randomUnitVector
pure (normalize (hr_normal hit + uu))
Just RedirectTarget {..} -> do
(i, j) <- state random
let lightPt = rt_origin + i *^ rt_U + j *^ rt_V
pure (normalize (lightPt - hr_point hit))
let pdf1 = dot dir (hr_normal hit) / pi
if pdf1 <= 0 then pure zero else do
let pdfs = flip map redirectTargets $ \RedirectTarget {..} ->
case rt_hit (Ray (hr_point hit) dir) of
Nothing -> 0
Just t -> t * t / abs (dot rt_cross dir)
-- The pdf from which 'dir' was generated
let pdf = remProb * pdf1 + sum (zipWith (*) probs pdfs)
c <- rayColor (depth - 1) time (Ray (hr_point hit) dir)
pure (matF dir * c ^* (pdf1 / pdf))
SphereF matF -> do
choice <- getTarget <$> state random
dir <- case choice of
Nothing -> randomUnitVector
Just RedirectTarget {..} -> do
(i, j) <- state random
let lightPt = rt_origin + i *^ rt_U + j *^ rt_V
pure (normalize (lightPt - hr_point hit))
let pdf1 = 0.25 / pi
let pdfs = flip map redirectTargets $ \RedirectTarget {..} ->
case rt_hit (Ray (hr_point hit) dir) of
Nothing -> 0
Just t -> t * t / abs (dot rt_cross dir)
-- The pdf from which 'dir' was generated
let pdf = remProb * pdf1 + sum (zipWith (*) probs pdfs)
c <- rayColor (depth - 1) time (Ray (hr_point hit) dir)
pure (matF dir * c ^* (pdf1 / pdf))
pixelColor :: Int -> Int -> State StdGen Color
pixelColor i j = do
colors <- replicateM cs_samplesPerPixel $ do
time <- state random
ray <- getRay i j
rayColor cs_maxRecursionDepth time ray
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))