module Graphics.WaveFront.Model (
BoundingBox(..),
facesOf, materialsOf,
tessellate, bounds,
hasTextures, textures,
createModel, createMTLTable,
fromIndices, fromFaceIndices, diffuseColours
) where
import qualified Data.Vector as V
import Data.Vector (Vector, (!?))
import Data.Text (Text)
import qualified Data.Map as M
import Data.Map (Map)
import qualified Data.Set as S
import Data.Set (Set)
import Data.List (groupBy)
import Data.Maybe (listToMaybe, catMaybes)
import Linear (V3(..))
import Control.Lens ((^.), (.~), (%~), (&), _1, _2, _3)
import Cartesian.Core (BoundingBox(..), fromExtents, x, y, z)
import Graphics.WaveFront.Types
import Graphics.WaveFront.Lenses
pairwise :: (a -> a -> b) -> [a] -> [b]
pairwise f xs = zipWith f xs (drop 1 xs)
eitherToMaybe :: Either a b -> Maybe b
eitherToMaybe (Right b) = Just b
eitherToMaybe (Left _) = Nothing
maybeToEither :: a -> Maybe b -> Either a b
maybeToEither _ (Just b) = Right b
maybeToEither a (Nothing) = Left a
groupsOf :: (Ord s, Integral i) => [OBJToken f s i m] -> Map (Set s) (i, i)
groupsOf = buildIndexMapWith . filter notObject
where
notObject (Object _) = False
notObject _ = True
objectsOf :: (Ord s, Integral i) => [OBJToken f s i m] -> Map (Set s) (i, i)
objectsOf = buildIndexMapWith . filter notGroup
where
notGroup (Group _) = False
notGroup _ = True
buildIndexMapWith :: (Ord s, Integral i) => [OBJToken f s i m] -> Map (Set s) (i, i)
buildIndexMapWith = M.fromList . pairwise zipIndices . update 0
where
zipIndices (names, low) (_, upp) = (names, (low, upp))
update faceCount [] = [(S.empty, faceCount)]
update faceCount (Group names:xs) = (names, faceCount) : update faceCount xs
update faceCount (Object names:xs) = (names, faceCount) : update faceCount xs
update faceCount (OBJFace _:xs) = update (faceCount + 1) xs
update faceCount (_:xs) = update faceCount xs
facesOf :: forall f s i m. Ord s => MTLTable f s -> [OBJToken f s i m] -> [Either String (Face f s i m)]
facesOf materials' = makeFaces Nothing Nothing
where
makeFaces :: Maybe s -> Maybe s -> [OBJToken f s i m] -> [Either String (Face f s i m)]
makeFaces _ _ [] = []
makeFaces lib@(Just libName) mat@(Just matName) (OBJFace is:xs) = createFace materials' libName matName is : makeFaces lib mat xs
makeFaces lib@Nothing mat (OBJFace _:xs) = Left "No library selected for face" : makeFaces lib mat xs
makeFaces lib mat@Nothing (OBJFace _:xs) = Left "No material selected for face" : makeFaces lib mat xs
makeFaces _ mat (LibMTL libName:xs) = makeFaces (Just libName) mat xs
makeFaces lib _ (UseMTL matName:xs) = makeFaces lib (Just matName) xs
makeFaces lib mat (_:xs) = makeFaces lib mat xs
createFace :: Ord s => MTLTable f s -> s -> s -> m (VertexIndices i) -> Either String (Face f s i m)
createFace materials' libName matName indices' = do
material' <- lookupMaterial materials' libName matName
Right $ Face { fIndices=indices', fMaterial=material' }
lookupMaterial :: Ord s => MTLTable f s -> s -> s -> Either String (Material f s)
lookupMaterial materials' libName matName = do
library <- maybeToEither "No such library" (M.lookup libName materials')
maybeToEither "No such material" (M.lookup matName library)
createMTLTable :: Ord s => [(s, [MTLToken f s])] -> Either String (MTLTable f s)
createMTLTable = fmap M.fromList . mapM (\(name, tokens) -> (name,) <$> materialsOf tokens)
materialsOf :: Ord s => [MTLToken f s] -> Either String (Map s (Material f s))
materialsOf = fmap M.fromList . mapM createMaterial . partitionMaterials
createMaterial :: [MTLToken f s] -> Either String (s, Material f s)
createMaterial (NewMaterial name:attrs) = (name,) <$> fromAttributes attrs
createMaterial attrs = Left $ "Free-floating attributes"
partitionMaterials :: [MTLToken f s] -> [[MTLToken f s]]
partitionMaterials = groupBy (\_ b -> not $ isNewMaterial b)
where
isNewMaterial (NewMaterial _) = True
isNewMaterial _ = False
fromAttributes :: [MTLToken f s] -> Either String (Material f s)
fromAttributes attrs = case colours' of
Nothing -> Left $ "Missing colour(s)"
Just (amb, diff, spec) -> Right $ Material { fAmbient=amb,fDiffuse=diff, fSpecular=spec, fTexture=texture' }
where
colours' = materialColours attrs
texture' = listToMaybe [ name | MapDiffuse name <- attrs ]
materialColours :: [MTLToken f s] -> Maybe (Colour f, Colour f, Colour f)
materialColours attrs = (,,) <$>
listToMaybe [ c | (Diffuse c) <- attrs ] <*>
listToMaybe [ c | (Specular c) <- attrs ] <*>
listToMaybe [ c | (Ambient c) <- attrs ]
createModel :: (Ord s, Integral i) => OBJ f s i [] -> MTLTable f s -> Maybe FilePath -> Either String (Model f s i Vector)
createModel tokens materials root = do
faces' <- sequence $ facesOf materials tokens
return $ Model { fVertices = V.fromList [ vec | OBJVertex vec <- tokens ],
fNormals = V.fromList [ vec | OBJNormal vec <- tokens ],
fTexcoords = V.fromList [ vec | OBJTexCoord vec <- tokens ],
fFaces = packFaces faces',
fGroups = groupsOf tokens,
fObjects = objectsOf tokens,
fMaterials = materials,
fRoot = root }
where
packFace :: Face f s i [] -> Face f s i Vector
packFace face@Face{fIndices} = face { fIndices=V.fromList fIndices }
packFaces :: [] (Face f s i []) -> Vector (Face f s i Vector)
packFaces = V.fromList . map (packFace . tessellate)
tessellate :: Face f s i [] -> Face f s i []
tessellate = indices %~ triangles
where
triangles [] = []
triangles (a:rest) = concat $ pairwise (\b c -> [a, b, c]) rest
bounds :: (Num f, Ord f, Foldable m, HasVertices (Model f s i m) (m (V3 f))) => Model f s i m -> BoundingBox (V3 f)
bounds model = fromExtents $ axisBounds (model^.vertices) <$> V3 x y z
where
minmaxBy :: (Ord o, Num o, Foldable m) => (a -> o) -> m a -> (o, o)
minmaxBy f values = foldr (\val' acc -> let val = f val' in (min val (fst acc), max val (snd acc))) (0, 0) values
axisBounds vs axis = minmaxBy (^.axis) vs
fromIndices :: Vector v -> (Vector v -> i -> b) -> (a -> i) -> Vector a -> Vector b
fromIndices data' index choose = V.map (index data' . choose)
fromFaceIndices :: Integral i => Vector (v f) -> (Vector (v f) -> a -> b) -> (VertexIndices i -> a) -> Vector (Face f Text i Vector) -> Vector b
fromFaceIndices data' index choose = V.concatMap (fromIndices data' index (choose) . (^.indices))
diffuseColours :: Vector (Face f s i Vector) -> Vector (Colour f)
diffuseColours faces' = V.concatMap (\f -> V.replicate (V.length $ f^.indices) (f^.material.diffuse)) faces'
hasTextures :: Ord s => Model f s i m -> Bool
hasTextures = not . S.null . textures
textures :: Ord s => Model f s i m -> S.Set s
textures = S.fromList . catMaybes . map (^.texture) . concatMap M.elems . M.elems . (^.materials)