crocodile-0.1: app/src/Primitive.hs
-- Module for general primitives and intersections
{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE MagicHash #-}
{-# LANGUAGE UnboxedTuples #-}
module Primitive (primitiveBoundingRadius,
primitiveClosestIntersect,
primitiveAnyIntersect,
primitiveTangentSpace,
Object(Object),
Primitive(Sphere, Plane, TriangleMesh),
primitive,
material,
makeTriangle,
makeQuad,
quadsToTriangles,
vertPosition,
vertUV,
vertTangentSpace,
transform,
radius,
triangles,
getCentre,
planeDistance,
primitiveBoundingBox,
objectListBoundingBox,
intersectsBox,
infinite,
boundingBoxValid,
sphereIntersect,
TangentSpace,
Vertex) where
import Ray
import Vector
import Material
import Matrix
import BoundingBox
import Misc
import Data.Maybe
import Data.List
-- Triangle object used for triangle meshes
data Vertex = Vertex { vertPosition :: {-# UNPACK #-} !Position,
vertUV :: {-# UNPACK #-} !Position,
vertTangentSpace :: {-# UNPACK #-} !TangentSpace } deriving (Show, Eq)
data Triangle = Triangle { vertices :: ![Vertex], plane :: !Primitive, halfPlanes :: ![Primitive] } deriving (Show, Eq)
-- General object definition
data Object = Object { primitive :: Primitive,
material :: Material,
transform :: !Matrix} deriving (Show, Eq)
-- Different kinds of primitives that an object can have
data Primitive = Sphere { radius :: {-# UNPACK #-} !Double }
| Plane { planeTangentSpace :: {-# UNPACK #-} !TangentSpace, planeDistance :: {-# UNPACK #-} !Double }
| TriangleMesh { triangles :: [Triangle] } deriving (Show, Eq)
-- Get the centre of an object
getCentre :: Object -> Vector
getCentre !object = getTranslation $ transform object
-- Surface normal for 3 points
surfaceNormal :: Position -> Position -> Position -> Direction
surfaceNormal !v1 !v2 !v3 = (v2 - v1) `cross` (v3 - v1)
-- Make a plane
makePlane :: Position -> Position -> Position -> Primitive
makePlane !v1 !v2 !v3 = Plane (tangent, binormal, normal) (-(v1 `dot3` normal))
where
!normal = normalise (surfaceNormal v1 v2 v3)
!tangent = normalise (v2 - v1)
!binormal = normalise (v3 - v1)
-- -------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Triangle base functionality
-- Make a triangle
makeTriangle :: Position -> Position -> Position -> Triangle
makeTriangle !v1 !v2 !v3 = Triangle verts newPlane newHalfPlanes
where newPlane = makePlane v1 v2 v3
newTanSpace = planeTangentSpace newPlane
verts = map (\v -> Vertex v zeroVector newTanSpace) [v1, v2, v3]
edgeVertices = [v1, v2, v3]
edges = map normalise [v2 - v1, v3 - v2, v1 - v3]
edgeNormals = map (\edge -> normalise $ thr newTanSpace `cross` edge) edges
-- TODO - The two vectors passed here are just dummies but they can fairly easily be derived
newHalfPlanes = zipWith (\edgeNormal edgeVertex -> Plane (Vector 1 0 0 1, Vector 0 1 0 1, edgeNormal) (-(edgeNormal `dot3` edgeVertex))) edgeNormals edgeVertices
makeQuad :: [Position] -> [Triangle]
makeQuad [vert1, vert2, vert3, vert4] = [makeTriangle vert1 vert2 vert3, makeTriangle vert1 vert3 vert4]
makeQuad _ = error "makeQuad: List was invalid size"
-- Turn a list of quad vertices into a triangle list
quadsToTriangles :: [Position] -> [Triangle]
quadsToTriangles positions = quadsToTriangles' positions []
where
quadsToTriangles' verts currentList
| length verts >= 4 = quadsToTriangles' (drop 4 verts) (currentList ++ makeQuad (take 4 verts))
| otherwise = currentList
-- Area of a triangle
triangleArea :: Position -> Position -> Position -> Double
triangleArea !v1 !v2 !v3 = 0.5 * magnitude (surfaceNormal v1 v2 v3)
-- Calculate the barycentric co-ordinates of a point on a triangle
calculateBarycentricCoordinates :: Position -> Triangle -> (Double, Double, Double)
calculateBarycentricCoordinates !pos !triangle = (alpha, beta, gamma)
where !alpha = triangleArea pos v2 v3 / area
!beta = triangleArea pos v1 v3 / area
!gamma = 1 - alpha - beta
!area = triangleArea v1 v2 v3
[!v1, !v2, !v3] = map vertPosition (vertices triangle)
-- Distance to a plane
distanceToPlane :: Primitive -> Vector -> Double
distanceToPlane (Plane !(_, _, norm) !dist) !pos = (pos `dot3` norm) + dist
distanceToPlane _ _ = error "distanceToPlane: Unsupported primitive for this function"
-- Use halfplanes to test if a point is inside a triangle
pointInsideTriangle :: Triangle -> Position -> Bool
pointInsideTriangle !tri !point = all (\pln -> distanceToPlane pln point >= 0) (halfPlanes tri)
-- Intersect a ray with a triangle
intersectRayTriangle :: Ray -> Object -> Triangle -> Bool -> (# Bool, Double, Triangle #)
intersectRayTriangle !ray !obj !triangle !doubleSided
| not doubleSided && direction ray `dot3` (thr . planeTangentSpace . plane) triangle > 0 = (# False, 0, triangle #)
| otherwise = case primitiveClosestIntersect (plane triangle) ray obj of
Nothing -> (# False, 0, triangle #)
Just (!dist', _) -> if pointInsideTriangle triangle (pointAlongRay ray dist')
then (# True, dist', triangle #)
else (# False, 0, triangle #)
-- Intersect against a list of triangles
intersectRayTriangleList :: [Triangle] -> Int -> Maybe (Double, Int) -> Ray -> Object -> Maybe (Double, Int)
intersectRayTriangleList !(x:xs) !index !currentResult !currentRay !obj = intersectRayTriangleList xs (index + 1) newResult newRay obj
where
(!newRay, !newResult) = case intersectRayTriangle currentRay obj x False of
(# False, _, _ #) -> (currentRay, currentResult)
(# True, !dist, _ #) -> (shortenRay currentRay dist, Just (dist, index))
intersectRayTriangleList [] _ !currentResult _ _ = currentResult
-- Intersect against any triangle
intersectRayAnyTriangleList :: [Triangle] -> Int -> Ray -> Object -> Maybe (Double, Int)
intersectRayAnyTriangleList !(x:xs) !index !ray !obj = case intersectRayTriangle ray obj x True of
(# False, _, _ #) -> intersectRayAnyTriangleList xs (index + 1) ray obj
(# True, !dist, _ #) -> Just (dist, index)
intersectRayAnyTriangleList [] _ _ _ = Nothing
-- Get the interpolated vertex normal
interpolatedTangentSpace :: Triangle -> Double -> Double -> Double -> TangentSpace
interpolatedTangentSpace !triangle !triAlpha !triBeta !triGamma = (tangent, binormal, normal)
where [!(tan1, bi1, norm1), !(tan2, bi2, norm2), !(tan3, bi3, norm3)] = map vertTangentSpace (vertices triangle)
tangent = normalise $ (tan1 <*> triAlpha) + (tan2 <*> triBeta) + (tan3 <*> triGamma)
binormal = normalise $ (bi1 <*> triAlpha) + (bi2 <*> triBeta) + (bi3 <*> triGamma)
normal = normalise $ (norm1 <*> triAlpha) + (norm2 <*> triBeta) + (norm3 <*> triGamma)
-- -------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Family of intersection functions
primitiveClosestIntersect :: Primitive -> Ray -> Object -> Maybe (Double, Int)
-- This function intersects a ray with a sphere, and returns the closest intercept
primitiveClosestIntersect (Sphere !sphereRadius) (Ray !rayOrg !rayDir !rayLen) obj
| discriminant < 0 = Nothing
| discriminant == 0 = Just (((-b) ** 0.5), 0)
| root1 >= 0 && root1 <= rayLen = Just (root1, 0)
| root2 >= 0 && root2 <= rayLen = Just (root2, 0)
| otherwise = Nothing
where
!delta = rayOrg - getCentre obj
!b = 2 * (delta `dot3` rayDir)
!c = (delta `dot3` delta) - sphereRadius ** 2
!discriminant = b ** 2 - 4 * c -- A is 1 because the ray direction is normalised
!root1 = ((-b) - sqrt discriminant) * 0.5
!root2 = ((-b) + sqrt discriminant) * 0.5
-- This function intersects a ray with a plane and returns the closest intercept
primitiveClosestIntersect (Plane !(_, _, planeNormal) !planeD) (Ray !rayOrg !rayDir !rayLen) _
| dirDotNormal == 0 = Nothing
| intercept >= 0 && intercept <= rayLen = Just (intercept, 0)
| otherwise = Nothing
where !dirDotNormal = rayDir `dot3` planeNormal
!intercept = ((-planeD) - (rayOrg `dot3` planeNormal)) / dirDotNormal
-- Find intersection with a triangle mesh
primitiveClosestIntersect (TriangleMesh !tris) !ray !obj = intersectRayTriangleList tris 0 Nothing ray obj
primitiveAnyIntersect :: Primitive -> Ray -> Object -> Maybe (Double, Int)
primitiveAnyIntersect (TriangleMesh !tris) !ray !obj = intersectRayAnyTriangleList tris 0 ray obj
primitiveAnyIntersect primitive' ray obj = primitiveClosestIntersect primitive' ray obj
-- -------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Family of normal query functions
primitiveTangentSpace :: Primitive -> Int -> Position -> Object -> TangentSpace
-- Normal for a sphere
primitiveTangentSpace (Sphere !sphereRadius) _ !intersectionPoint obj = (tangent, binormal, normal)
where
tangent = Vector 1 0 0 0 -- This is clearly incorrect - fix this later!
binormal = Vector 0 1 0 0
normal = (intersectionPoint - getCentre obj) <*> (1 / sphereRadius)
primitiveTangentSpace (Plane !planeNormal _) _ _ _ = planeNormal
primitiveTangentSpace (TriangleMesh !tris) !triId !intersectionPoint _ = interpolatedTangentSpace triangle triAlpha triBeta triGamma
where !triangle = tris !! triId
(!triAlpha, !triBeta, !triGamma) = calculateBarycentricCoordinates intersectionPoint triangle
-- -------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Family of bounding radius functions
primitiveBoundingRadius :: Primitive -> Matrix -> Vector -> Double
primitiveBoundingRadius (Sphere sphereRadius) sphereTransform pos = sphereRadius + (pos `distance` getTranslation sphereTransform)
primitiveBoundingRadius (Plane _ _) _ _ = 0
primitiveBoundingRadius (TriangleMesh tris) _ centre = triangleListRadius 0 tris centre
triangleListRadius :: Double -> [Triangle] -> Vector -> Double
triangleListRadius maximumRadius (tri:tris) centre = triangleListRadius (Prelude.max maximumRadius triangleRadius) tris centre
where
radii = map (\v -> centre `distance` (vertPosition v)) (vertices tri)
triangleRadius = foldl' Prelude.max 0 radii
triangleListRadius maximumRadius [] _ = maximumRadius
-- -------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Family of bounding box functions
-- Find the bounding box of a primitive
primitiveBoundingBox :: Primitive -> Object -> Maybe AABB
primitiveBoundingBox (Sphere sphereRadius) obj = Just (boxMin, boxMax)
where
boxMin = getCentre obj - Vector sphereRadius sphereRadius sphereRadius 0
boxMax = getCentre obj + Vector sphereRadius sphereRadius sphereRadius 0
primitiveBoundingBox (Plane _ _) _ = Nothing
primitiveBoundingBox (TriangleMesh tris) obj = Just $ triangleListBoundingBox initialInvalidBox (transform obj) tris
-- Bounding box of a list of something
triangleListBoundingBox :: AABB -> Matrix -> [Triangle] -> AABB
triangleListBoundingBox currentBox transformMatrix (tri:tris) = triangleListBoundingBox (boundingBoxUnion currentBox thisTriangleBox) transformMatrix tris
where
worldSpaceVertices = map (transformVector transformMatrix . vertPosition) (vertices tri)
(invalidMin, invalidMax) = initialInvalidBox
thisTriangleBox = (foldl' Vector.min invalidMin worldSpaceVertices, foldl' Vector.max invalidMax worldSpaceVertices)
triangleListBoundingBox currentBox _ [] = currentBox
objectListBoundingBox :: [Object] -> AABB
objectListBoundingBox = foldr (boundingBoxUnion . (\obj -> fromMaybe initialInvalidBox (primitiveBoundingBox (primitive obj) obj))) initialInvalidBox
-- Does a primitive intersect a box?
-- Could maybe generalise the primitiveClosestIntersect function above via further pattern matching?
intersectsBox :: Primitive -> Matrix -> AABB -> Bool
intersectsBox (Sphere sphereRadius) matrix (boxMin, boxMax) = (centreX + sphereRadius) >= vecX boxMin && (centreX - sphereRadius) <= vecX boxMax &&
(centreY + sphereRadius) >= vecY boxMin && (centreY - sphereRadius) <= vecY boxMax &&
(centreZ + sphereRadius) >= vecZ boxMin && (centreZ - sphereRadius) <= vecZ boxMax
where
!centre = getTranslation matrix
!centreX = vecX centre
!centreY = vecY centre
!centreZ = vecZ centre
intersectsBox (Plane (_, _, planeNormal) planeD) _ box = signum minDistance /= signum maxDistance
where
!minX = selectMinBoxComponent vecX planeNormal box
!minY = selectMinBoxComponent vecY planeNormal box
!minZ = selectMinBoxComponent vecZ planeNormal box
!maxX = selectMaxBoxComponent vecX planeNormal box
!maxY = selectMaxBoxComponent vecY planeNormal box
!maxZ = selectMaxBoxComponent vecZ planeNormal box
!minAgainstPlane = Vector minX minY minZ 1
!maxAgainstPlane = Vector maxX maxY maxZ 1
!minDistance = (planeNormal `dot3` minAgainstPlane) + planeD
!maxDistance = (planeNormal `dot3` maxAgainstPlane) + planeD
intersectsBox (TriangleMesh tris) matrix box = boundingBoxOverlaps box triListBox
where
triListBox = triangleListBoundingBox initialInvalidBox matrix tris
-- -------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Aspect querying of objects
infinite :: Primitive -> Bool
infinite (Plane _ _) = True
infinite _ = False
-- -------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Specialised intersection code for bounding volume hierarchies
sphereIntersect :: Double -> Vector -> Ray -> Maybe Double
sphereIntersect !rad !centre (Ray !rayOrg !rayDir !rayLen)
| (centre `distanceSq` rayOrg) < (rad * rad) = Just 0 -- Inside the sphere!
| discriminant < 0 = Nothing
| discriminant == 0 = Just ((-b) / 2)
| root1 >= 0 && root1 <= rayLen = Just root1
| root2 >= 0 && root2 <= rayLen = Just root2
| otherwise = Nothing
where
!delta = rayOrg - centre
!b = 2.0 * (delta `dot3` rayDir)
!c = (delta `dot3` delta) - rad**2
!discriminant = b**2 - 4 * c -- A is 1 because the ray direction is normalised
!root1 = (-b - sqrt discriminant) / 2
!root2 = (-b + sqrt discriminant) / 2