-- Copyright (c) 2000 Galois Connections, Inc. -- All rights reserved. This software is distributed as -- free software under the license in the file "LICENSE", -- which is included in the distribution. module Intersections ( intersectRayWithObject, quadratic ) where import Data.Maybe(isJust) import Construct import Geometry import Interval import Misc -- This is factored into two bits. The main function `intersections' -- intersects a line with an object. -- The wrapper call `intersectRayWithObject' coerces this to an intersection -- with a ray by clamping the result to start at 0. intersectRayWithObject ray p = clampIntervals is where is = intersections ray p clampIntervals (True, [], True) = (False, [(0, True, undefined)], True) clampIntervals empty@(False, [], False) = empty clampIntervals (True, is@((i, False, p) : is'), isOpen) | i `near` 0 || i < 0 = clampIntervals (False, is', isOpen) | otherwise = (False, (0, True, undefined) : is, isOpen) clampIntervals ivals@(False, is@((i, True, p) : is'), isOpen) | i `near` 0 || i < 0 -- can unify this with first case... = clampIntervals (True, is', isOpen) | otherwise = ivals intersections ray (Union p q) = unionIntervals is js where is = intersections ray p js = intersections ray q intersections ray (Intersect p q) = intersectIntervals is js where is = intersections ray p js = intersections ray q intersections ray (Difference p q) = differenceIntervals is (negateSurfaces js) where is = intersections ray p js = intersections ray q intersections ray (Transform m m' p) = mapI (xform m) is where is = intersections (m' `multMR` ray) p xform m (i, b, (s, p0)) = (i, b, (transformSurface m s, p0)) intersections ray (Box box p) | intersectWithBox ray box = intersections ray p | otherwise = emptyIList intersections ray p@(Plane s) = intersectPlane ray s intersections ray p@(Sphere s) = intersectSphere ray s intersections ray p@(Cube s) = intersectCube ray s intersections ray p@(Cylinder s) = intersectCylinder ray s intersections ray p@(Cone s) = intersectCone ray s negateSurfaces :: IList (Surface, Texture a) -> IList (Surface, Texture a) negateSurfaces = mapI negSurf where negSurf (i, b, (s,t)) = (i, b, (negateSurface s, t)) negateSurface (Planar p0 v0 v1) = Planar p0 v1 v0 negateSurface (Spherical p0 v0 v1) = Spherical p0 v1 v0 negateSurface (Cylindrical p0 v0 v1) = Cylindrical p0 v1 v0 negateSurface (Conic p0 v0 v1) = Conic p0 v1 v0 transformSurface m (Planar p0 v0 v1) = Planar p0' v0' v1' where p0' = multMP m p0 v0' = multMV m v0 v1' = multMV m v1 transformSurface m (Spherical p0 v0 v1) = Spherical p0' v0' v1' where p0' = multMP m p0 v0' = multMV m v0 v1' = multMV m v1 -- ditto as above transformSurface m (Cylindrical p0 v0 v1) = Cylindrical p0' v0' v1' where p0' = multMP m p0 v0' = multMV m v0 v1' = multMV m v1 transformSurface m (Conic p0 v0 v1) = Conic p0' v0' v1' where p0' = multMP m p0 v0' = multMV m v0 v1' = multMV m v1 -------------------------------- -- Plane -------------------------------- intersectPlane :: Ray -> a -> IList (Surface, Texture a) intersectPlane ray texture = intersectXZPlane PlaneFace ray 0.0 texture intersectXZPlane :: Face -> Ray -> Double -> a -> IList (Surface, Texture a) intersectXZPlane n (r,v) yoffset texture | b `near` 0 = -- the ray is parallel to the plane - it's either all in, or all out if y `near` yoffset || y < yoffset then openIList else emptyIList -- The line intersects the plane. Find t such that -- (x + at, y + bt, z + ct) intersects the X-Z plane. -- t may be negative (the ray starts within the halfspace), -- but we'll catch that later when we clamp the intervals | b < 0 -- the ray is pointing downwards = (False, [mkEntry (t0, (Planar p0 v0 v1, (n, p0, texture)))], True) | otherwise -- the ray is pointing upwards = (True, [mkExit (t0, (Planar p0 v0 v1, (n, p0, texture)))], False) where t0 = (yoffset-y) / b x0 = x + a * t0 z0 = z + c * t0 p0 = point x0 0 z0 v0 = vector 0 0 1 v1 = vector 1 0 0 x = xCoord r y = yCoord r z = zCoord r a = xComponent v b = yComponent v c = zComponent v -------------------------------- -- Sphere -------------------------------- intersectSphere :: Ray -> a -> IList (Surface, Texture a) intersectSphere ray@(r, v) texture = -- Find t such that (x + ta, y + tb, z + tc) intersects the -- unit sphere, that is, such that: -- (x + ta)^2 + (y + tb)^2 + (z + tc)^2 = 1 -- This is a quadratic equation in t: -- t^2(a^2 + b^2 + c^2) + 2t(xa + yb + zc) + (x^2 + y^2 + z^2 - 1) = 0 let c1 = sq a + sq b + sq c c2 = 2 * (x * a + y * b + z * c) c3 = sq x + sq y + sq z - 1 in case quadratic c1 c2 c3 of Nothing -> emptyIList Just (t1, t2) -> entryexit (g t1) (g t2) where x = xCoord r y = yCoord r z = zCoord r a = xComponent v b = yComponent v c = zComponent v g t = (t, (Spherical origin v1 v2, (SphereFace, p0, texture))) where origin = point 0 0 0 x0 = x + t * a y0 = y + t * b z0 = z + t * c p0 = point x0 y0 z0 v0 = vector x0 y0 z0 (v1, v2) = tangents v0 -------------------------------- -- Cube -------------------------------- intersectCube :: Ray -> a -> IList (Surface, Texture a) intersectCube ray@(r, v) texture = -- The set of t such that (x + at, y + bt, z + ct) lies within -- the unit cube satisfies: -- 0 <= x + at <= 1, 0 <= y + bt <= 1, 0 <= z + ct <= 1 -- The minimum and maximum such values of t give us the two -- intersection points. case intersectSlabIval (intersectCubeSlab face2 face3 x a) (intersectSlabIval (intersectCubeSlab face5 face4 y b) (intersectCubeSlab face0 face1 z c)) of Nothing -> emptyIList Just (t1, t2) -> entryexit (g t1) (g t2) where g ((n, v0, v1), t) = (t, (Planar p0 v0 v1, (n, p0, texture))) where p0 = offsetToPoint ray t face0 = (CubeFront, vectorY, vectorX) face1 = (CubeBack, vectorX, vectorY) face2 = (CubeLeft, vectorZ, vectorY) face3 = (CubeRight, vectorY, vectorZ) face4 = (CubeTop, vectorZ, vectorX) face5 = (CubeBottom, vectorX, vectorZ) vectorX = vector 1 0 0 vectorY = vector 0 1 0 vectorZ = vector 0 0 1 x = xCoord r y = yCoord r z = zCoord r a = xComponent v b = yComponent v c = zComponent v intersectCubeSlab n m w d | d `near` 0 = if (0 <= w) && (w <= 1) then Just ((n, -inf), (m, inf)) else Nothing | d > 0 = Just ((n, (-w)/d), (m, (1-w)/d)) | otherwise = Just ((m, (1-w)/d), (n, (-w)/d)) intersectSlabIval Nothing Nothing = Nothing intersectSlabIval Nothing (Just i) = Nothing intersectSlabIval (Just i) Nothing = Nothing intersectSlabIval (Just (nu1@(n1, u1), mv1@(m1, v1))) (Just (nu2@(n2, u2), mv2@(m2, v2))) = checkInterval (nu, mv) where nu = if u1 < u2 then nu2 else nu1 mv = if v1 < v2 then mv1 else mv2 checkInterval numv@(nu@(_, u), (m, v)) -- rounding error may force us to push v out a bit | u `near` v = Just (nu, (m, u + epsilon)) | u < v = Just numv | otherwise = Nothing -------------------------------- -- Cylinder -------------------------------- intersectCylinder :: Ray -> a -> IList (Surface, Texture a) intersectCylinder ray texture = isectSide `intersectIntervals` isectTop `intersectIntervals` isectBottom where isectSide = intersectCylSide ray texture isectTop = intersectXZPlane CylinderTop ray 1.0 texture isectBottom = complementIntervals $ negateSurfaces $ intersectXZPlane CylinderBottom ray 0.0 texture intersectCylSide (r, v) texture = -- The ray (x + ta, y + tb, z + tc) intersects the sides of the -- cylinder if: -- (x + ta)^2 + (z + tc)^2 = 1 and 0 <= y + tb <= 1. if (sq a + sq c) `near` 0 then -- The ray is parallel to the Y-axis, and does not intersect -- the cylinder sides. It's either all in, or all out if (sqxy `near` 1.0 || sqxy < 1.0) then openIList else emptyIList else -- Find values of t that solve the quadratic equation -- (a^2 + c^2)t^2 + 2(ax + cz)t + x^2 + z^2 - 1 = 0 let c1 = sq a + sq c c2 = 2 * (x * a + z * c) c3 = sq x + sq z - 1 in case quadratic c1 c2 c3 of Nothing -> emptyIList Just (t1, t2) -> entryexit (g t1) (g t2) where sqxy = sq x + sq y g t = (t, (Cylindrical origin v1 v2, (CylinderSide, p0, texture))) where origin = point 0 0 0 x0 = x + t * a y0 = y + t * b z0 = z + t * c p0 = point x0 y0 z0 v0 = vector x0 0 z0 (v1, v2) = tangents v0 x = xCoord r y = yCoord r z = zCoord r a = xComponent v b = yComponent v c = zComponent v ------------------- -- Cone ------------------- intersectCone :: Ray -> a -> IList (Surface, Texture a) intersectCone ray texture = isectSide `intersectIntervals` isectTop `intersectIntervals` isectBottom where isectSide = intersectConeSide ray texture isectTop = intersectXZPlane ConeBase ray 1.0 texture isectBottom = complementIntervals $ negateSurfaces $ intersectXZPlane ConeBase ray 0.0 texture intersectConeSide (r, v) texture = -- Find the points where the ray intersects the cond side. At any points of -- intersection, we must have: -- (x + ta)^2 + (z + tc)^2 = (y + tb)^2 -- which is the following quadratic equation: -- t^2(a^2-b^2+c^2) + 2t(xa-yb+cz) + (x^2-y^2+z^2) = 0 let c1 = sq a - sq b + sq c c2 = 2 * (x * a - y * b + c * z) c3 = sq x - sq y + sq z in case quadratic c1 c2 c3 of Nothing -> emptyIList Just (t1, t2) -> -- If either intersection strikes the middle, then the other -- can only be off by rounding error, so we make a tangent -- strike using the "good" value. -- If the intersections straddle the origin, then it's -- an exit/entry pair, otherwise it's an entry/exit pair. let y1 = y + t1 * b y2 = y + t2 * b in if y1 `near` 0 then entryexit (g t1) (g t1) else if y2 `near` 0 then entryexit (g t2) (g t2) else if (y1 < 0) `xor` (y2 < 0) then exitentry (g t1) (g t2) else entryexit (g t1) (g t2) where g t = (t, (Conic origin v1 v2, (ConeSide, p0, texture))) where origin = point 0 0 0 x0 = x + t * a y0 = y + t * b z0 = z + t * c p0 = point x0 y0 z0 v0 = normalize $ vector x0 (-y0) z0 (v1, v2) = tangents v0 x = xCoord r y = yCoord r z = zCoord r a = xComponent v b = yComponent v c = zComponent v -- beyond me why this isn't defined in the prelude... xor False b = b xor True b = not b ------------------- -- Solving quadratics ------------------- quadratic :: Double -> Double -> Double -> Maybe (Double, Double) quadratic a b c = -- Solve the equation ax^2 + bx + c = 0 by using the quadratic formula. let d = sq b - 4 * a * c d' = if d `near` 0 then 0 else d in if d' < 0 then Nothing -- There are no real roots. else if a > 0 then Just (((-b) - sqrt d') / (2 * a), ((-b) + sqrt d') / (2 * a)) else Just (((-b) + sqrt d') / (2 * a), ((-b) - sqrt d') / (2 * a)) ------------------- -- Bounding boxes ------------------- data MaybeInterval = Interval !Double !Double | NoInterval isInterval (Interval _ _) = True isInterval _ = False intersectWithBox :: Ray -> Box -> Bool intersectWithBox (r, v) (B x1 x2 y1 y2 z1 z2) = isInterval interval where x_interval = intersectRayWithSlab (xCoord r) (xComponent v) (x1, x2) y_interval = intersectRayWithSlab (yCoord r) (yComponent v) (y1, y2) z_interval = intersectRayWithSlab (zCoord r) (zComponent v) (z1, z2) interval = intersectInterval x_interval (intersectInterval y_interval z_interval) intersectInterval :: MaybeInterval -> MaybeInterval -> MaybeInterval intersectInterval NoInterval _ = NoInterval intersectInterval _ NoInterval = NoInterval intersectInterval (Interval a b) (Interval c d) | b < c || d < a = NoInterval | otherwise = Interval (a `max` c) (b `min` d) {-# INLINE intersectRayWithSlab #-} intersectRayWithSlab :: Double -> Double -> (Double,Double) -> MaybeInterval intersectRayWithSlab xCoord alpha (x1, x2) | alpha == 0 = if xCoord < x1 || xCoord > x2 then NoInterval else infInterval | alpha > 0 = Interval a b | otherwise = Interval b a where a = (x1 - xCoord) / alpha b = (x2 - xCoord) / alpha infInterval = Interval (-inf) inf