Haskell Raytracer Project - Part 2
Parts of this tutorial
This is the second article of a series that show how to make a basic raytracer (3D renderer) with Haskell.
In the first article we made a function to output bitmap (PPM) image files from Haskell. In this article we'll introduce some functions to help manipulate vectors.
Vectors
Our raytracer will involve a lot of calculation with vectors, so for clarity, rather than using [Double], we'll introduce a new type.
This code should go in Vector.hs.
module Vector where data Vector = Vector [Scalar] -- a vector is a list of scalars instance Num Vector where (+) (Vector xs) (Vector ys) = Vector (zipWith (+) xs ys) (-) (Vector xs) (Vector ys) = Vector (zipWith (-) xs ys) (*) (Vector xs) (Vector ys) = Vector (zipWith (*) xs ys) negate (Vector xs) = Vector (map negate xs) fromInteger x = error "you probably didn't mean to do that" -- you can add, subtract, check equality of and multiply vectors term-by-term instance Eq Vector where (==) (Vector xs) (Vector ys) = all id (zipWith (==) xs ys) instance Show Vector where show (Vector xs) = show xs -- to convert a vector to a string, just convert its value list to string mult :: Scalar -> Vector -> Vector mult l (Vector xs) = Vector (map (*l) xs) normalise :: Vector -> Vector normalise v@(Vector vs) = Vector (map (/mag) vs) where mag = (sqrt . vectorsum) (v * v) type Scalar = Double -- store scalars as floating-point doubles vectorsum :: Vector -> Scalar vectorsum (Vector xs) = sum xs -- get the sum of the scalar elements of a vector
Surfaces
The code from here onwards should go in Intersect.hs.
Let's think about what 3D objects we could have in our scene - we'll call them surfaces.
For now, we'll just consider spheres, since the maths is simple.
A sphere has:
- a centre (vector)
- a radius (scalar)
- a material it's made from - we'll just store ore a colour property.
data Surface = Sphere Vector Scalar Colour
Intersections
We're going to use raytracing to render our scene. In short, we take a point in our scene, then fire lots of rays outwards from it (one per pixel in the image) then see what they hit.
Clearly a ray is simply a source and a direction vector:
data Ray = Ray Vector Vector
Now, let's introduce a new function that gives the distance at which a ray (half-line) first intersects a surface (or returns Nothing if no such intersection exists).
intersect :: Ray -> Surface -> Maybe Scalar -- Intersect returns Nothing if the ray does not intersect the surface -- or if it does intersect, Just d where rx+(d*rv) is where intersects the surface intersect (Ray rx rv) (Sphere sx radius col) | insersect' == [] = Nothing | otherwise = Just (minimum insersect') where insersect' | dscr < 0 = [] | otherwise = filter (>0.0000001) [((-b) + sqrtdscr) / (2.0 * a), ((-b) - sqrtdscr) / (2.0 * a)] -- we don't to see out of the back of our head! se = (sx - rx) b = (-2.0) * vectorsum (rv * se) dscr = (b * b) - (4.0 * a * c) a = vectorsum (rv * rv) c = vectorsum (se * se) - (radius * radius) sqrtdscr = sqrt dscr
The maths is quite simple - we want to find points on the half-line that are at a distance radius from the point centre, so we solve the resulting quadratic equation, and then pick the first intersection if any exists. We also make sure we only consider intersections that occur forward in time - i.e. in front of the camera, not behind it!
Next part: Scene drawing.
Copyright (C) 2006-8 Ryan Lothian. All rights reserved.
