raytrace-0.2.0.0: src/Graphics/Ray/Material.hs
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE TupleSections #-}
module Graphics.Ray.Material
( Material(..), MaterialResult(..)
, lightSource, pitchBlack, lambertian, lommelSeeliger, mirror, metal, dielectric, transparent, isotropic, anisotropic
) where
import Graphics.Ray.Core
import Graphics.Ray.Texture
import Linear (V3(V3), zero, dot, quadrance, (*^), normalize)
import System.Random (StdGen, random)
import Control.Monad.State (State, state)
-- | A material is a function that takes the details of a ray-surface intersection (specifically, the direction of the
-- ray, along with a 'HitRecord' object) and produces an emitted color as well as a 'MaterialResult' (see below).
newtype Material = Material (Vec3 -> HitRecord -> (Color, State StdGen MaterialResult))
-- | The ray tracer uses the 'MaterialResult' to compute the color from scattering, which is added to the color from
-- emission.
data MaterialResult
= Absorb
-- ^ Do not create a new ray.
| Scatter Color Vec3
-- ^ Create a new ray in the given direction (which must be a unit vector)
-- and multiply the result of recursively ray tracing the ray by the given color.
| HemisphereF (Vec3 -> Color)
-- ^ Leave the task of computing a direction to the ray tracer (allowing it to send more rays toward light sources).
-- The function is the BRDF of the material times \(\pi\). Its argument (the new ray direction) is guaranteed to be a unit
-- vector and to have a positive dot product with 'hr_normal'.
| SphereF (Vec3 -> Color)
-- ^ Similar to 'HemisphereF', but the new ray direction can be anywhere on the sphere. (Such materials are typically used
-- with volumes like 'Graphics.Geometry.constantMedium'.) The function is the albedo times the material's phase function
-- times \(4\pi\). Its argument is guaranteed to be a unit vector.
-- NOTE: I could generalize 'HemisphereF' and 'SphereF' by allowing the material to specify a unit vector generator (and pdf)
-- to use in the case of no redirection. 'anisotropic' and 'lommelSeeliger' could benefit from this. One issue is that it would require
-- converting a single vector into an orthonormal basis.
-- | A material that emits light equally in all directions and does not scatter rays.
lightSource :: Texture -> Material
lightSource (Texture tex) = Material $ \ _ HitRecord{..} -> (tex hr_point hr_uv, pure Absorb)
-- | A material that absorbs all light. This is the same as @'lightSource' ('constantTexture' 0)@, but the name reflects
-- the fact that it is not actually a light source.
pitchBlack :: Material
pitchBlack = Material $ \ _ _ -> (zero, pure Absorb)
-- | 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 {..}) -> (zero, pure $ HemisphereF $ const (tex hr_point hr_uv))
-- | A material that exhibits Lommel-Seeliger reflectance.
lommelSeeliger :: Texture -> Material
lommelSeeliger (Texture tex) = Material $
\inDir (HitRecord {..}) -> (zero,) $ pure $ HemisphereF $ \outDir -> let
mu0 = -(dot inDir hr_normal)
mu1 = dot outDir hr_normal
in 0.25 / (mu0 + mu1) *^ tex hr_point hr_uv
-- | A colored mirror. (For no color, use @'constantTexture' 1@.)
mirror :: Texture -> Material
mirror (Texture tex) = Material $
\dir (HitRecord {..}) ->
(zero, pure $ Scatter (tex hr_point hr_uv) (reflect hr_normal dir))
-- TODO: make this more physically-based?
-- | 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 $
\dir (HitRecord {..}) -> (zero,) $ do
u <- randomUnitVector
let dir' = reflect hr_normal dir + fuzz *^ u
let scatter = dot dir' hr_normal > 0
pure (if scatter then Scatter (tex hr_point hr_uv) (normalize dir') else Absorb)
-- [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) -- NOTE: quadrance perp = (iorRatio * sinTheta)^2
in perp + para -- unit vector
-- | 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 $
\dir (HitRecord {..}) -> (zero,) $ do
let iorRatio = if hr_frontSide then 1/ior else ior
let cosTheta = min 1 (dot hr_normal (-dir))
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 dir
else refract iorRatio cosTheta hr_normal dir
pure (Scatter (V3 1 1 1) dir')
-- | A material that lets light through, with the given tint.
transparent :: Texture -> Material
transparent (Texture tex) = Material $
\dir (HitRecord {..}) ->
(zero, pure $ Scatter (tex hr_point hr_uv) 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 {..}) -> (zero, pure $ SphereF $ const (tex hr_point hr_uv))
-- | A material that scatters an incoming ray according to the Henyey-Greenstein distribution. The first parameter
-- should be in the range (-1, 1); negative values result in more backward scattering and positive values result in
-- more forward scattering.
-- (Typically used with 'Graphics.Geometry.constantMedium'.)
anisotropic :: Double -> Texture -> Material
anisotropic g (Texture tex) = Material $
\inDir (HitRecord {..}) -> (zero,) $ pure $ SphereF $ \outDir -> let
mu = dot inDir outDir
hg = (1 - g*g) / (1 + g*g - 2*g*mu)**1.5
in hg *^ tex hr_point hr_uv