raytrace-0.1.0.0: src/Graphics/Ray/Material.hs
{-# LANGUAGE RecordWildCards #-}
module Graphics.Ray.Material
( Material(Material)
, lightSource, pitchBlack, lambertian, mirror, metal, dielectric, transparent, isotropic
) where
import Graphics.Ray.Core
import Graphics.Ray.Texture
import Linear (V3(V3), zero, normalize, dot, quadrance, (*^))
import System.Random (StdGen, random)
import Control.Monad.State (State, state)
-- | A material is a function that, given the details of a ray-surface intersection, produces an emitted color
-- (which is @0@ for all of the materials in this module save 'lightSource') and potentially a reflected ray
-- along with a color to scale the result of tracing said ray by.
newtype Material = Material (Ray -> HitRecord -> State StdGen (Color, Maybe (Color, Ray)))
-- | A material that emits light and does not reflect rays.
lightSource :: Texture -> Material
lightSource (Texture tex) = Material $
\_ (HitRecord {..}) -> pure (tex hr_point hr_uv, Nothing)
-- | A material that absorbs all light. Identical in principle to @'lambertian' ('constantTexture' 0)@, @'isotropic' ('constantTexture' 0)@,
-- and so on, but avoids unecessary computation.
pitchBlack :: Material
pitchBlack = Material $ \ _ _ -> pure (zero, Nothing)
-- | A material that exhibits Lambertian reflectance. The direction of the reflected ray is independent of the direction
-- of the incoming ray, resulting in a diffuse (non-shiny) appearance.
lambertian :: Texture -> Material
lambertian (Texture tex) = Material $
\_ (HitRecord {..}) -> do
u <- randomUnitVector
pure (zero, Just (tex hr_point hr_uv, Ray hr_point (hr_normal + 0.9999 *^ u)))
-- | A colored mirror. (For no color, use @'constantTexture' 1@.)
mirror :: Texture -> Material
mirror (Texture tex) = Material $
\(Ray _ dir) (HitRecord {..}) ->
pure (zero, Just (tex hr_point hr_uv, Ray hr_point (reflect hr_normal dir)))
-- | A metallic-looking material that reflects rays inexactly. The larger the first argument is, the less shiny
-- the material. @'metal' 0@ behaves the same as 'mirror'.
metal :: Double -> Texture -> Material
metal fuzz (Texture tex) = Material $
\(Ray _ dir) (HitRecord {..}) -> do
u <- randomUnitVector
let dir' = normalize (reflect hr_normal dir) + (fuzz *^ u)
let scatter = dot dir' hr_normal > 0
pure (zero, if scatter then Just (tex hr_point hr_uv, Ray hr_point dir') else Nothing)
-- [private]
refract :: Double -> Double -> Vec3 -> Vec3 -> Vec3
refract iorRatio cosTheta normal u = let
perp = iorRatio *^ (u + cosTheta *^ normal)
para = -(sqrt (abs (1 - quadrance perp)) *^ normal)
in perp + para
-- | A material that either reflects or refracts all incoming rays, like clear glass.
-- The argument is the index of refraction relative to the surrounding medium.
dielectric :: Double -> Material
dielectric ior = Material $
\(Ray _ dir) (HitRecord {..}) -> do
let iorRatio = if hr_frontSide then 1/ior else ior
let u = normalize dir
let cosTheta = min 1 (dot hr_normal (-u))
let sinTheta = sqrt (1 - cosTheta * cosTheta)
let cannotRefract = iorRatio * sinTheta > 1
let r0 = (1 - iorRatio) / (1 + iorRatio)
let r0' = r0 * r0
let reflectance = r0' + (1 - r0') * (1 - cosTheta)**5 -- Schlick approximation
x <- state random
let dir' = if cannotRefract || x < reflectance
then reflect hr_normal u
else refract iorRatio cosTheta hr_normal u
pure (zero, Just (V3 1 1 1, Ray hr_point dir'))
-- | A material that lets all light through, with the given tint.
transparent :: Texture -> Material
transparent (Texture tex) = Material $
\(Ray _ dir) (HitRecord {..}) ->
pure (zero, Just (tex hr_point hr_uv, Ray hr_point dir))
-- | A material that scatters an incoming ray in a direction chosen uniformly at random from the unit sphere.
-- (Typically used with 'Graphics.Geometry.constantMedium'.)
isotropic :: Texture -> Material
isotropic (Texture tex) = Material $
\_ (HitRecord {..}) -> do
dir <- randomUnitVector
pure (zero, Just (tex hr_point hr_uv, Ray hr_point dir))