All pastes #940677 Raw Edit

Birdray

public text v1 · immutable
·published 2008-03-13 04:33 UTC
rendered paste body
module Main where

import Graphics.Imlib
import Data.Array.IArray

type Point = (Double, Double, Double)
type Vector = (Double, Double, Double)
type Color = (Double, Double, Double)

data Ray = Ray Point Vector
data Light = Light Point Color
data Surface = Surface {color :: Color
                       ,reflectivity :: Double
                       ,smoothness :: Double
                       ,opacity :: Double
                       ,refractiveIx :: Double}
data Object = Plane {point :: Point
                    ,normal :: Vector
                    ,surface :: Surface}
            | Sphere {point :: Point
                     ,radius :: Double
                     ,surface :: Surface}
data Hit = NoHit
         | Hit Double Point Vector Surface

type View = Array (Int, Int) Ray
type Scene = ([Light], [Object])

black = (0, 0, 0) :: Color
white = (1, 1, 1) :: Color
red = (1, 0, 0) :: Color
green = (0, 1, 0) :: Color
blue = (0, 0, 1) :: Color
yellow = (1, 1, 0) :: Color
blueGreen = (0, 1, 1) :: Color
violet = (1, 0, 1) :: Color

instance Num a => Num (a, a, a) where
    (x1, y1, z1) + (x2, y2, z2) = (x1 + x2, y1 + y2, z1 + z2)
    (x1, y1, z1) - (x2, y2, z2) = (x1 - x2, y1 - y2, z1 - z2)
    (x1, y1, z1) * (x2, y2, z2) = (y1 * z2 - z1 * y2
                                  ,z1 * x2 - x1 * z2
                                  ,x1 * y2 - x2 * y1)
    negate (x, y, z) = (-x, -y, -z)

k `scl` (x, y, z) = (k * x, k * y, k * z)

(x1, y1, z1) `dot` (x2, y2, z2) = x1 * x2 + y1 * y2 + z1 * z2

mag (x, y, z) = sqrt (x^2 + y^2 + z^2)

a `dist` b = mag (b - a)

sq v = v `dot` v

a `dotScl` b = (a `dot` b) / (mag a * mag b)

v `sclTo` k = (k / mag v) `scl` v

rotateBy :: Vector -> (Double, Double) -> Vector
v@(x, y, z) `rotateBy` (dtheta, dphi) =
    let r = sqrt (x^2 + y^2)
        theta = atan (y / x)
        phi = atan (z / r)
    in (r * cos (theta + dtheta)
       ,r * sin (theta + dtheta)
       ,mag v * sin (phi + dphi))

normalAt :: Object -> Point -> Vector
(Sphere c _ _) `normalAt` p = p - c
(Plane _ n _) `normalAt` _ = n

newRay :: Point -> Vector -> Ray
newRay s d = Ray s (d `sclTo` 1)

rayTo :: Point -> Point -> Ray
a `rayTo` b = Ray a ((b - a) `sclTo` 1)

ptAt :: Ray -> Double -> Point
Ray s d `ptAt` l = s + (d `sclTo` l)

reflect :: Vector -> Vector -> Vector
reflect v n = v - (2 * (v `dot` n) / sq n) `scl` n

refract :: Vector -> Vector -> Double -> Vector
refract v n i = negate n `sclTo` i + v

hitDist :: Ray -> Object -> Double
(Ray s d) `hitDist` (Sphere p r _) =
    let v = s - p
--      a = sq d --because d has magnitude 1
        b = 2 `scl` v `dot` d
        c = v `dot` v - r^2
        dis = sqrt (b^2 - 4 {-* a-} * c)
    in min ((-b + dis) / (2 {-* a-})) ((-b - dis) / (2 {-* a-}))
(Ray s d) `hitDist` (Plane p n _) = (n `dot` (p - s)) / (n `dot` d)

intersect :: Ray -> Object -> Hit
ray `intersect` obj =
    let t = ray `hitDist` obj
        p = ray `ptAt` t
    in if isNaN t || isInfinite t || t <= 1e-11
       then NoHit
       else Hit t p (obj `normalAt` p) (surface obj)

intersects :: Ray -> [Object] -> Hit
intersects ray objs = foldl1 minHit $ map (intersect ray) objs
    where minHit a@(Hit x _ _ _) b@(Hit y _ _ _) = if x < y
                                                   then a
                                                   else b
          minHit NoHit hit = hit
          minHit hit NoHit = hit

lighted :: Hit -> Scene -> Color
lighted _ ([], _) = black
lighted hit@(Hit _ p n _) (((Light q color) : rest), objs) =
    let d = q - p
    in lighted hit (rest, objs) +
           (case intersects (newRay p d) objs of
              NoHit -> (d `dotScl` n / sq d) `scl` color
              Hit _ _ _ _ -> black)

reflected :: Ray -> Hit -> Scene -> Color
reflected (Ray _ v) (Hit _ p n sfc) scene = 
    reflectivity sfc `scl`
    rayTrace scene (newRay p (reflect v n))

refracted :: Ray -> Hit -> Scene -> Color
refracted (Ray _ v) (Hit _ p n sfc) scene =
    opacity sfc `scl`
    rayTrace scene (newRay p (refract v n (refractiveIx sfc)))

onSurface :: Color -> Surface -> Color
(r, g, b) `onSurface` (Surface (x, y, z) _ _ _ _) = (x * r, y * g, z * b)

rayTrace :: Scene -> Ray -> Color
rayTrace scene ray =
    case intersects ray (snd scene) of
      NoHit -> black
      hit@(Hit _ _ _ sfc) -> (lighted hit scene +
                              refracted ray hit scene +
                              reflected ray hit scene)
                             `onSurface` sfc

newView :: Ray -> (Double, Double) -> (Int, Int) -> View
newView (Ray s d) (theta, phi) (x, y) =
    let stepAngles angle num =
            let num' = fromIntegral num
            in zip [0 .. num] $ map (* angle) $
               map (/ num') [- num' / 2 .. num' / 2]
    in array ((0, 0), (x, y)) $ concat $
       map (\(j, dphi) ->
            map (\(i, dtheta) ->
                 ((i, j), newRay s (d `rotateBy` (dtheta, dphi)))) $
            stepAngles theta x) $ stepAngles phi y

viewTrace :: Scene -> View -> Array (Int, Int) Color
viewTrace scene = amap (rayTrace scene)

--and now, actually using the raytracer:

defaultSurface = Surface {color = white
                         ,reflectivity = 0.5
                         ,smoothness = 0.5
                         ,opacity = 0.5
                         ,refractiveIx = 0.5}

scene = ([Light (0, 0, 5) white]
        ,[Sphere (1, 1, 3) 1 defaultSurface
         ,Plane (0, 0, 0) (0, 0, 1) defaultSurface])

view = newView ((-5, -5, 5) `rayTo` (0, 0, 0)) (pi / 2, pi / 2) (512, 512)

main :: IO ()
main = (createImageUsingArray $ amap toImlibColor $ viewTrace scene view)
       >>= contextSetImage
       >> saveImage "out.png"
    where toImlibColor :: Color -> ImlibColor
          toImlibColor (r, g, b) = ImlibColor 0 (s255 r) (s255 g) (s255 b)
              where s255 s = round (max 0 (min 1 s) * 255)

instance Show Hit where
    show NoHit = "NoHit"
    show (Hit a b c _) = show a ++ show b ++ show c