packages feed

crocodile-0.1: app/src/PhotonMap.hs

-- Photon mapping
{-# LANGUAGE BangPatterns #-}

module PhotonMap(buildPhotonMap, PhotonMap(photonList), irradiance, PhotonMapContext(PhotonMapContext)) where

import {-# SOURCE #-} Light hiding (position)
import Vector
import Distribution
import Material
import Colour
import SceneGraph
import RayTrace
import Ray hiding (direction)
import Control.Monad.State
import BoundingBox
import KDTree
import Debug.Trace
import Misc
import Control.Parallel.Strategies
import Control.DeepSeq
import Data.Heap hiding (partition)
import System.Random.Mersenne.Pure64
import Data.List hiding (union, insert)
import Primitive

type GeneratorState = State PureMT

data PhotonMapContext = PhotonMapContext {
      photonGatherDistance :: Double,
      maxGatherPhotons :: Int,
      coneFilterK :: Double,
      directVisualisation :: Bool }

data Photon = Photon { power :: {-# UNPACK #-} !Colour, posDir :: {-# UNPACK #-} !(Position, Direction) } deriving (Show, Eq, Ord)

data PhotonMapTree = PhotonMapNode {-# UNPACK #-} !Int {-# UNPACK #-} !Double PhotonMapTree PhotonMapTree
                   | PhotonMapLeaf {-# UNPACK #-} !Photon deriving (Show, Eq)

data PhotonMap = PhotonMap { photonList :: [Photon],
                             photonMapTree :: PhotonMapTree } deriving(Show, Eq)

data PhotonChoice = DiffuseReflect | SpecularReflect | Absorb

instance NFData Photon where
    rnf (Photon power' posDir') = rnf power' `seq` rnf posDir'

seedToRefactor :: Int
seedToRefactor = 12345

-- Generate a list of photon position and direction tuples to emit
-- I zip up each pos,dir tuple with a random number generator to give each photon a different sequence of random values
-- Helps parallelisation...
-- TODO Eliminate magic number seeds from here
emitPhotons :: Light -> Int -> [(Position, Direction, PureMT, Colour)]
emitPhotons (PointLight (CommonLightData lightPower True) pos _) numPhotons = zipWith (\dir num -> (pos, dir, pureMT (fromIntegral num), flux)) (generatePointsOnSphere numPhotons 1 seedToRefactor) [1..numPhotons]
    where
      flux = lightPower Colour.<*> (1.0 / fromIntegral numPhotons)
emitPhotons (QuadLight (CommonLightData lightPower True) corner _ du dv) numPhotons = zipWith3 (\pos dir num -> (pos, transformDir dir tanSpace, pureMT (fromIntegral num), flux)) randomPoints randomDirs [1..numPhotons]
    where
      randomPoints = generatePointsOnQuad corner du dv numPhotons seedToRefactor
      randomDirs = generatePointsOnHemisphere numPhotons 1 (seedToRefactor * 10)
      area =  Vector.magnitude (du `cross` dv)
      flux = lightPower Colour.<*> (area / fromIntegral numPhotons)
      tanSpace = (normalise du, normalise dv, normalise (du `cross` dv))
emitPhotons _ _ = []

-- Compute russian roulette coefficients
russianRouletteCoefficients :: Material -> (Double, Double)
russianRouletteCoefficients mat = (diffuseP, specularP)
    where
      (Colour diffuseR diffuseG diffuseB _) = Material.diffuse mat
      (Colour specularR specularG specularB _) = Material.specular mat
      diffuseP = (diffuseR + diffuseG + diffuseB) / 3
      specularP = (specularR + specularG + specularB) / 3

-- Decide what to do with a photon
choosePhotonFate :: (Double, Double) -> GeneratorState PhotonChoice
choosePhotonFate (diffuseP, specularP) = do
  generator <- get
  let (p, generator') = randomDouble generator
  let result | p < diffuseP = DiffuseReflect
             | p < (diffuseP + specularP) = SpecularReflect
             | otherwise = Absorb
  put generator'
  return $! result

-- Compute new power for a photon
computeNewPhotonPower :: PhotonChoice -> (Double, Double) -> Colour -> Material -> Colour
computeNewPhotonPower fate (diffuseP, specularP) photonPower mat = case fate of
                                                                     DiffuseReflect -> photonPower * diffuse mat Colour.</> diffuseP
                                                                     SpecularReflect -> photonPower * specular mat Colour.</> specularP
                                                                     Absorb -> colBlack

-- Compute a new diffuse reflection in spherical co-ordinates
generateUV :: GeneratorState (Double, Double)
generateUV = do generator <- get
                let (u, generator') = randomDouble generator
                let (v, generator'') = randomDouble generator'
                put generator''
                return $! (u, v)

-- Find a diffuse reflection direction in the hemisphere of the normal
-- Realistic Image Synthesis Using Photon Mapping - Eq 2.24
diffuseReflectionDirection :: PureMT -> TangentSpace -> (Direction, PureMT)
diffuseReflectionDirection stdGen tanSpace = (transformDir dir tanSpace, stdGen')
    where
      ((u, v), stdGen') = runState generateUV stdGen
      theta = acos (sqrt u)
      phi = 2 * pi * v
      dir = sphericalToDirection theta phi

-- Main working photon tracing function
-- Realistic Image Synthesis Using Photon Mapping p60
tracePhoton :: [Photon] -> Photon -> SceneGraph -> PureMT -> (Int, Int) -> [Photon]
tracePhoton currentPhotons (Photon photonPower photonPosDir) sceneGraph rndState (bounce, maxBounces) = 
    -- See if the photon intersects a surfaces
    case findNearestIntersection sceneGraph ray of
      Nothing -> currentPhotons
      Just (obj, t, subId) -> case photonFate of
                                -- Diffuse reflection. Here, we store the photon that got reflected, and trace a new photon - but only if it's bright enough to be worthwhile
                                DiffuseReflect -> if Colour.magnitude newPhotonPower > brightnessEpsilon && (bounce + 1) <= maxBounces
                                                  then tracePhoton (storedPhoton : currentPhotons) reflectedPhoton sceneGraph rndState'' (bounce + 1, maxBounces)
                                                  else storedPhoton : currentPhotons
                                    where
                                      reflectedPhoton = Photon newPhotonPower (surfacePos, reflectedDir)
                                      (reflectedDir, rndState'') = diffuseReflectionDirection rndState' tanSpace

                                -- Specular reflection. Here, we reflect the photon in the fashion that the surface would reflect towards the viewer and
                                -- aim to absorb it somewhere else in the photon map
                                SpecularReflect -> if Colour.magnitude newPhotonPower > brightnessEpsilon && (bounce + 1) <= maxBounces
                                                   then tracePhoton currentPhotons reflectedPhoton sceneGraph rndState' (bounce + 1, maxBounces)
                                                   else currentPhotons
                                    where
                                      reflectedPhoton = Photon newPhotonPower (surfacePos, reflectedDir)
                                      reflectedDir = Vector.negate (snd photonPosDir) `reflect` normal

                                -- Absorb. The photon simply gets absorbed into the map
                                Absorb -> storedPhoton : currentPhotons
          where
            (photonFate, rndState') = runState (choosePhotonFate coefficients) rndState
            coefficients = russianRouletteCoefficients (material obj)
            newPhotonPower = computeNewPhotonPower photonFate coefficients photonPower (material obj)
            tanSpace = primitiveTangentSpace (primitive obj) subId hitPosition obj
            normal = thr tanSpace
            hitPosition = pointAlongRay ray t
            surfacePos = hitPosition + (normal Vector.<*> surfaceEpsilon)
            brightnessEpsilon = 0.1
            storedPhoton = Photon photonPower (surfacePos, snd photonPosDir)
    where
      ray = rayWithPosDir photonPosDir 10000

-- Build a list of photons for a light source
tracePhotonsForLight :: Int -> SceneGraph -> Light -> [Photon]
tracePhotonsForLight numPhotons sceneGraph light = concat (map (\(pos, dir, rndState, flux) -> tracePhoton [] (Photon flux (pos, dir)) sceneGraph rndState (0, maxBounces)) posDirGens `using` parListChunk photonsPerChunk rdeepseq)
    where
      posDirGens = emitPhotons light numPhotons -- Positions, directions, random number generators
      maxBounces = 500
      photonsPerChunk = 256

-- High-level function to build a photon map
buildPhotonMap :: SceneGraph -> [Light] -> Int -> (PhotonMap, [Light])
buildPhotonMap sceneGraph lights numPhotonsPerLight = photons `seq` kdTree `seq` (PhotonMap photons kdTree, lightsNotForPhotonMap)
    where
      (lightsForPhotonMap, lightsNotForPhotonMap) = partition (addToPhotonMap . common) lights
      photons = concatMap (tracePhotonsForLight numPhotonsPerLight sceneGraph) lightsForPhotonMap
      kdTree = buildKDTree photons

-- Make a bounding box of a list of photons
photonsBoundingBox :: [Photon] -> AABB
photonsBoundingBox = foldl' (\box photon -> enlargeBoundingBox (fst . posDir $ photon) box) initialInvalidBox

-- Construct a balanced kd tree of photons
-- Realistic Image Synthesis Using Photon Mapping p72
buildKDTree :: [Photon] -> PhotonMapTree
buildKDTree (x:[]) = PhotonMapLeaf x
buildKDTree [] = error "buildKDTree [] should never get called"
buildKDTree photons = let (boxMin, boxMax) = photonsBoundingBox photons
                          axis = largestAxis (boxMax - boxMin)
                          numPhotons = fromIntegral (length photons)
                          photonsMedian = foldl' (\box photon -> box + (fst . posDir $ photon)) zeroVector photons Vector.</> numPhotons
                          value = component photonsMedian axis
                          photonsGT = Prelude.filter (\p -> component ((fst . posDir) p) axis > value) photons
                          photonsLE = Prelude.filter (\p -> component ((fst . posDir) p) axis <= value) photons
                      in if length photonsGT > 0 && length photonsLE > 0
                         then let gtTree = buildKDTree photonsGT
                                  leTree = buildKDTree photonsLE
                              in gtTree `seq` leTree `seq` PhotonMapNode axis value gtTree leTree
                         else let (photons0', photons1') = trace "Using degenerate case" $ degenerateSplitList photons in PhotonMapNode axis value (buildKDTree photons0') (buildKDTree photons1')

-- Use a max heap to make it easy to eliminate distant photons
data GatheredPhoton = GatheredPhoton Double Photon deriving (Show)
type PhotonHeap = MaxHeap GatheredPhoton

instance Ord GatheredPhoton where
    compare (GatheredPhoton dist1 _) (GatheredPhoton dist2 _) = dist1 `compare` dist2

instance Eq GatheredPhoton where
    (GatheredPhoton dist1 _) == (GatheredPhoton dist2 _) = dist1 == dist2

instance NFData GatheredPhoton where
    rnf (GatheredPhoton dist photon) = rnf dist `seq` rnf photon

-- Return the minimum squared search radius from that specified, versus the furthest photon in the heap
-- We don't want to locate any photons further away than our current furthest - we're looking for the closest ones, after all
minimalSearchRadius :: Double -> PhotonHeap -> Double
minimalSearchRadius !rSq photonHeap = case viewHead photonHeap of
                                       Nothing -> rSq
                                       Just (GatheredPhoton !dSq _) -> Prelude.min rSq dSq

-- Gather photons for irradiance computations
-- Algorithm adapted from Realistic Image Synthesis Using Photon Mapping p73
gatherPhotons :: PhotonMapTree -> Position -> Double -> PhotonHeap -> Int -> PhotonHeap
gatherPhotons (PhotonMapNode axis value gtChild leChild) pos rSq photonHeap maxPhotons
    -- In this case, the split plane bisects the search sphere - search both halves of tree
    | (value - posComponent) ** 2 <= rSq = let heap1 = gatherPhotons gtChild pos rSq' photonHeap maxPhotons
                                               rSq'' = minimalSearchRadius rSq' heap1
                                               heap2 = gatherPhotons leChild pos rSq'' photonHeap maxPhotons
                                               newHeap = union heap1 heap2
                                           in heap1 `seq` heap2 `seq` newHeap `seq` Data.Heap.drop (size newHeap - maxPhotons) newHeap

    -- One side of the tree...
    | posComponent > value = gatherPhotons gtChild pos rSq' photonHeap maxPhotons

    -- ... or the other
    | posComponent <= value = gatherPhotons leChild pos rSq' photonHeap maxPhotons

    -- Prolapse
    | otherwise = error "gatherPhotons: unexplained/unexpected case here"
    where
      posComponent = component pos axis
      rSq' = minimalSearchRadius rSq photonHeap -- Refine search radius as we go down tree to search no further than closest allowed photon
gatherPhotons (PhotonMapLeaf p) pos rSq photonHeap maxPhotons
    | distSq < rSq = let newHeap = insert (GatheredPhoton distSq p) photonHeap
                     in Data.Heap.drop (size newHeap - maxPhotons) newHeap -- Discard any excess photons - we get rid of the furthest ones
    | otherwise = photonHeap
    where distSq = pos `distanceSq` (fst . posDir) p

-- Return the contribution of a given photon, including a simple cos term to emulate BRDF plus the cone filter
-- Cone filter is from Realistic Image Synthesis Using Photon Mapping p81
photonContribution :: Double -> SurfaceLocation -> Photon -> Colour
photonContribution kr (pos, (_, _, normal)) photon = power photon Colour.<*> ((Vector.negate normal `sdot3` (snd . posDir) photon) * weight)
    where
      weight = 1 - (pos `distance` (fst . posDir) photon) / (kr + 0.000000001) -- Add on an epsilon to prevent div0 in cone filter

-- Find the overall contribution of a list of photons
-- Radiance estimate algorithm from Realistic Image Synthesis Using Photon Mapping p81
sumPhotonContribution :: Double -> Double -> SurfaceLocation -> [Photon] -> Colour
sumPhotonContribution r k posTanSpace photons = foldl' (\y x -> y + photonContribution (k * r) posTanSpace x) colBlack photons Colour.<*> (1.0 / ((1.0 - 2.0 / (3.0 * k)) * pi * r * r))

-- Look up the resulting irradiance from the photon map at a given point
-- Realistic Image Synthesis Using Photon Mapping, e7.6
irradiance :: PhotonMap -> PhotonMapContext -> Material -> SurfaceLocation -> Colour
irradiance photonMap photonMapContext mat posTanSpace = sumPhotonContribution r k posTanSpace gatheredPhotons * diffuse mat
    where
      r = photonGatherDistance photonMapContext
      maxPhotons
          | directVisualisation photonMapContext = 1
          | otherwise = maxGatherPhotons photonMapContext
      k = coneFilterK photonMapContext
      photonHeap = gatherPhotons (photonMapTree photonMap) (fst posTanSpace) (r * r) Data.Heap.empty maxPhotons
      gatheredPhotons = map (\(GatheredPhoton _ photon) -> photon) (Data.Heap.take maxPhotons photonHeap)