WaveFront 0.1.2.0 → 0.5.0.0
raw patch · 14 files changed
+1296/−651 lines, 14 filesdep +QuickCheckdep +attoparsecdep +eitherdep −GLUtildep −OpenGLdep ~lens
Dependencies added: QuickCheck, attoparsec, either, text, transformers, vector
Dependencies removed: GLUtil, OpenGL
Dependency ranges changed: lens
Files
- README.md +23/−6
- WaveFront.cabal +51/−14
- src/Graphics/WaveFront.hs +37/−0
- src/Graphics/WaveFront/Foreign.hs +38/−14
- src/Graphics/WaveFront/Lenses.hs +54/−0
- src/Graphics/WaveFront/Load.hs +48/−37
- src/Graphics/WaveFront/Model.hs +323/−0
- src/Graphics/WaveFront/Parse.hs +87/−0
- src/Graphics/WaveFront/Parse/Common.hs +166/−0
- src/Graphics/WaveFront/Parse/MTL.hs +142/−0
- src/Graphics/WaveFront/Parse/OBJ.hs +172/−0
- src/Graphics/WaveFront/Parsers.hs +0/−358
- src/Graphics/WaveFront/Types.hs +155/−106
- src/Graphics/WaveFront/Utilities.hs +0/−116
README.md view
@@ -9,10 +9,13 @@ Supports the basic MTL and OBJ attributes. My ambition is to add full support for the entire specification. -Please note that this package is completely unaware of rendering and graphics. The data structures generated by the parsers are oblivious to technologies such as Direct3D and OpenGL; creating eg. GPU buffers is up to the client. +Please note that this package is *completely unaware of rendering and graphics*. The data structures generated by the parsers are oblivious to technologies such as Direct3D and OpenGL; creating eg. GPU buffers is up to the client. I may at some point implement the FFI and add direct OpenGL support, in separate modules. +## Examples + + ## Maintainers Jonatan H Sundqvist @@ -20,8 +23,22 @@ See source files (.hs) for additional items. -- [ ] Use Parsec instead (branch off) (?) -- [ ] Add sample models and demos -- [ ] Add profiling and checks -- [ ] GHCi support -- [ ] Proper ticket system +- [ ] Performance (currently, it's atrocious) (...) + - Parallelism (...) +- [ ] Add sample models and demos (...) +- [ ] Add profiling and checks (cf. QuickCheck) + - Travis-CI integration +- [x] GHCi support (added .ghci file, works quite well) +- [x] Proper ticket system ([here](https://github.com/SwiftsNamesake/3DWaves/issues/)) +- [ ] Polymorphism (...) +- [ ] Querying + - Asking questions about a particular model (bounds, storage, number of faces, material types, etc.) + - Asking questions about the OBJ and MTL formats (eg. 'what does the various illum values mean') + - Screenshots (maybe in the wavefront-render package) +- [ ] Serialising (eg. writing to OBJ and MTL) +- [ ] Rendering + - Create a separate package (wavefront-render) with an OpenGL backend +- [ ] Executables + - OBJ viewer (...) + - Command line tool (?) +- [ ] Foreign function interface
WaveFront.cabal view
@@ -10,7 +10,7 @@ -- PVP summary: +-+------- breaking API changes -- | | +----- non-breaking API additions -- | | | +--- code changes with no API change -version: 0.1.2.0 +version: 0.5.0.0 -- A short (one-line) description of the package. synopsis: Parsers and utilities for the OBJ WaveFront 3D model format @@ -24,7 +24,7 @@ -- The file containing the license text. license-file: LICENSE.md --- The package author(s). +-- The package <div class=""><s></s></div> author: Jonatan H Sundqvist -- An email address to which users can send suggestions, bug reports, and @@ -46,29 +46,66 @@ cabal-version: >=1.10 +flag pedantic + description: Enable warnings + default: True + + +flag optimise + description: Enable optimisations + -- TODO: Should probably be True + default: False + + +flag profile + description: Enable profiling options + default: False + + +source-repository head + type: git + -- TODO: Rename the GitHub repo (?) + location: https://github.com/swiftsnamesake/3DWaves + + library -- Modules exported by the library. - exposed-modules: Graphics.WaveFront.Types, Graphics.WaveFront.Utilities, Graphics.WaveFront.Parsers, Graphics.WaveFront.Load, - Graphics.WaveFront.Foreign + exposed-modules: Graphics.WaveFront, + Graphics.WaveFront.Types, + Graphics.WaveFront.Lenses, + Graphics.WaveFront.Parse, + Graphics.WaveFront.Load, + Graphics.WaveFront.Foreign, + Graphics.WaveFront.Model -- Modules included in this library but not exported. - -- other-modules: + other-modules: Graphics.WaveFront.Parse.Common, + Graphics.WaveFront.Parse.OBJ, + Graphics.WaveFront.Parse.MTL + -- Compiler arguments + ghc-options: -Wall + -- LANGUAGE extensions used by modules in this package. + -- TODO: This list is incomplete other-extensions: UnicodeSyntax, TupleSections, ForeignFunctionInterface -- Other library packages from which modules are imported. - build-depends: base == 4.*, - lens <= 4.13.0.0, - OpenGL, - GLUtil, - linear, - filepath, - containers, - Cartesian + build-depends: base == 4.* + , lens <= 4.14 + , either + , transformers + , linear + , vector + , text + , attoparsec + , filepath + , containers + , QuickCheck + , Cartesian -- Directories containing source files. hs-source-dirs: src -- Base language which the package is written in. - default-language: Haskell2010 + default-language: Haskell2010
+ src/Graphics/WaveFront.hs view
@@ -0,0 +1,37 @@+-- | +-- Module : Graphics.WaveFront +-- Description : Re-exports public API +-- Copyright : (c) Jonatan H Sundqvist, 2016 +-- License : MIT +-- Maintainer : Jonatan H Sundqvist +-- Stability : experimental|stable +-- Portability : POSIX (not sure) + +-- TODO | - Logging +-- - + +-- SPEC | - +-- - + + + +-------------------------------------------------------------------------------------------------------------------------------------------- +-- GHC Extensions +-------------------------------------------------------------------------------------------------------------------------------------------- + + + +-------------------------------------------------------------------------------------------------------------------------------------------- +-- API +-------------------------------------------------------------------------------------------------------------------------------------------- +module Graphics.WaveFront () where -- TODO: Fill this in + + + +-------------------------------------------------------------------------------------------------------------------------------------------- +-- We'll need these +-------------------------------------------------------------------------------------------------------------------------------------------- +-- import Graphics.WaveFront.Types +-- import qualified Graphics.WaveFront.Parse as Parse +-- import qualified Graphics.WaveFront.Parse.Common as Parse +-- import Graphics.WaveFront.Model
src/Graphics/WaveFront/Foreign.hs view
@@ -9,7 +9,7 @@ -- February 24 2015 --- TODO | - Possible to get rid of newtypes +-- TODO | - Possible to get rid of newtypes (?) -- - Decide on an API -- SPEC | - @@ -17,7 +17,7 @@ --- TODO: Why do same extensions start with 'X'? +-- TODO: Why do some extensions start with 'X'? {-# LANGUAGE ForeignFunctionInterface #-} @@ -32,12 +32,12 @@ -------------------------------------------------------------------------------------------------------------------------------------------- -- We'll need these -------------------------------------------------------------------------------------------------------------------------------------------- -import System.IO.Unsafe (unsafePerformIO) -import Foreign.Storable -import qualified Foreign.C as C +-- import System.IO.Unsafe (unsafePerformIO) +-- import Foreign.Storable +-- import qualified Foreign.C as C -import Graphics.WaveFront.Types -import qualified Graphics.WaveFront.Parsers as Parsers +-- import Graphics.WaveFront.Types +-- import qualified Graphics.WaveFront.Parse as Parse @@ -45,15 +45,39 @@ -- Functions -------------------------------------------------------------------------------------------------------------------------------------------- --- | --- I feel dirty... -parseOBJ :: C.CString -> COBJ -parseOBJ = COBJ . Parsers.parseOBJ . unsafePerformIO . C.peekCString +-- -- | +-- -- I feel dirty... +-- parseOBJ :: C.CString -> COBJ +-- parseOBJ = COBJ . Parsers.parseOBJ . unsafePerformIO . C.peekCString +-- +-- -- | +-- parseMTL :: C.CString -> CMTL +-- parseMTL = CMTL . Parsers.parseMTL . unsafePerformIO . C.peekCString --- | -parseMTL :: C.CString -> CMTL -parseMTL = CMTL . Parsers.parseMTL . unsafePerformIO . C.peekCString + +-- -- | +-- newtype COBJ = COBJ OBJ +-- +-- +-- -- | +-- newtype CMTL = CMTL MTL +-- +-- +-- -- | We +-- instance Storable COBJ where +-- sizeOf = const 0 +-- alignment = const 0 +-- peek _ = error "Work in progress" +-- poke _ = error "Work in progress" +-- +-- +-- -- | We +-- instance Storable CMTL where +-- sizeOf = const 0 +-- alignment = const 0 +-- peek _ = error "Work in progress" +-- poke _ = error "Work in progress" --------------------------------------------------------------------------------------------------------------------------------------------
+ src/Graphics/WaveFront/Lenses.hs view
@@ -0,0 +1,54 @@+-- | +-- Module : Graphics.WaveFront.Lenses +-- Description : +-- Copyright : (c) Jonatan H Sundqvist, 2016 +-- License : MIT +-- Maintainer : Jonatan H Sundqvist +-- Stability : experimental|stable +-- Portability : POSIX (not sure) +-- + +-- Created July 9 2016 + +-- TODO | - +-- - + +-- SPEC | - +-- - + + + +-------------------------------------------------------------------------------------------------------------------------------------------- +-- GHC Pragmas +-------------------------------------------------------------------------------------------------------------------------------------------- +{-# LANGUAGE TemplateHaskell #-} +{-# LANGUAGE MultiParamTypeClasses #-} +{-# LANGUAGE FunctionalDependencies #-} +{-# LANGUAGE FlexibleInstances #-} + + + +-------------------------------------------------------------------------------------------------------------------------------------------- +-- API +-------------------------------------------------------------------------------------------------------------------------------------------- +module Graphics.WaveFront.Lenses where + + + +-------------------------------------------------------------------------------------------------------------------------------------------- +-- We'll need these +-------------------------------------------------------------------------------------------------------------------------------------------- +import Control.Lens (makeLensesWith, abbreviatedFields) + +import Graphics.WaveFront.Types + + + +-------------------------------------------------------------------------------------------------------------------------------------------- +-- Lenses +-------------------------------------------------------------------------------------------------------------------------------------------- +makeLensesWith abbreviatedFields ''VertexIndices +makeLensesWith abbreviatedFields ''Face +makeLensesWith abbreviatedFields ''Colour +makeLensesWith abbreviatedFields ''Material +makeLensesWith abbreviatedFields ''Model
src/Graphics/WaveFront/Load.hs view
@@ -10,7 +10,7 @@ -- Created July 26 2015 --- TODO | - +-- TODO | - Logging -- - -- SPEC | - @@ -33,59 +33,70 @@ -------------------------------------------------------------------------------------------------------------------------------------------- -- We'll need these -------------------------------------------------------------------------------------------------------------------------------------------- -import System.FilePath (splitFileName, (</>)) -import System.IO (hFlush, stdout) +import System.FilePath (splitFileName, takeDirectory, (</>)) -import Data.Either (rights, isLeft) +import Data.Text (Text) +import qualified Data.Text as T +import qualified Data.Text.IO as T +import Data.Vector (Vector) -import Control.Lens ((^.), _2) +import Control.Applicative ((<$>)) +import Control.Monad.Trans.Either +import Control.Monad.Trans.Class (lift) -import Graphics.WaveFront.Types -import Graphics.WaveFront.Parsers (parseOBJ, parseMTL, createMTLTable, createModel) -import Graphics.WaveFront.Utilities +import qualified Data.Attoparsec.Text as Atto +import Graphics.WaveFront.Types +import qualified Graphics.WaveFront.Parse as Parse +import qualified Graphics.WaveFront.Parse.Common as Parse +import Graphics.WaveFront.Model (createMTLTable, createModel) + -------------------------------------------------------------------------------------------------------------------------------------------- -- Functions (IO) -------------------------------------------------------------------------------------------------------------------------------------------- + -- Loading data ---------------------------------------------------------------------------------------------------------------------------- + -- | --- --- TODO: Use bytestrings (?) --- -loadOBJ :: String -> IO OBJ -loadOBJ fn = do - rawOBJ <- readFile fn -- - return $ parseOBJ rawOBJ -- +-- TODO | - Use bytestrings (?) +-- - Deal with IO and parsing errors +obj :: (Fractional f, Integral i) => String -> IO (Either String (OBJ f Text i [])) +obj fn = runEitherT $ do + lift $ putStrLn $ "Loading obj file: " ++ fn + EitherT $ Atto.parseOnly (Parse.wholeFile Parse.obj) <$> T.readFile fn -- | --- --- TODO: Use bytestrings (?) --- TODO: Merge OBJ and MTL parsers (and plug in format-specific code as needed) (?) --- -loadMTL :: String -> IO MTL -loadMTL fn = do - rawMTL <- readFile fn -- Unparsed MTL data (text) - return $ parseMTL rawMTL -- +-- TODO | - Use bytestrings (?) +-- - Merge OBJ and MTL parsers (and plug in format-specific code as needed) (?) +-- - Deal with IO and parsing errors +mtl :: (Fractional f) => String -> IO (Either String (MTL f Text [])) +mtl fn = do + putStrLn $ "Loading mtl file: " ++ fn + Atto.parseOnly (Parse.wholeFile Parse.mtl) <$> T.readFile fn -- | --- TODO: Better names (than 'mtls' and 'fns') (?) --- TODO: Refactor, simplify --- TODO: Improve path handling (cf. '</>') -loadMaterials :: [String] -> IO MTLTable -loadMaterials fns = do - mtls <- mapM loadMTL fns -- - return . createMTLTable . zip (map (snd . splitFileName) fns) . map tokensOf $ mtls -- - where tokensOf = rights . map (^._2) +-- TODO | - Better names (than 'mtls' and 'fns') (?) +-- - Refactor, simplify +-- - Improve path handling (cf. '</>') +-- - Graceful error handling +materials :: (Fractional f) => [FilePath] -> IO (Either String (MTLTable f Text)) +materials fns = runEitherT $ do + tokens <- mapM (EitherT . mtl) fns + EitherT . return $ createTableFromMTLs tokens + where + createTableFromMTLs :: [[MTLToken f Text]] -> Either String (MTLTable f Text) + createTableFromMTLs = createMTLTable . zip (map (T.pack . snd . splitFileName) fns) -- | Loads an OBJ model from file, including associated materials -loadModel :: String -> IO Model -loadModel fn = do - obj <- loadOBJ fn - materials <- loadMaterials [ (fst $ splitFileName fn) </> name | LibMTL name <- rights $ map (^._2) obj ] - return $ createModel obj materials - -- where loadWithName name = loadMTL name >>= return . (name,) +-- TODO | - Graceful error handling +model :: (Fractional f, Integral i) => FilePath -> IO (Either String (Model f Text i Vector)) +model fn = runEitherT $ do + obj <- EitherT $ obj fn + materials <- EitherT $ materials [ fst (splitFileName fn) </> T.unpack name | LibMTL name <- obj ] + EitherT . return $ createModel obj materials (Just $ takeDirectory fn) + -- where loadWithName name = mtl name >>= return . (name,)
+ src/Graphics/WaveFront/Model.hs view
@@ -0,0 +1,323 @@+-- | +-- Module : Graphics.WaveFront.Model +-- Description : +-- Copyright : (c) Jonatan H Sundqvist, 2016 +-- License : MIT +-- Maintainer : Jonatan H Sundqvist +-- Stability : stable +-- Portability : portable +-- + +-- TODO | - Single-pass (eg. consume all tokens only once) for additional performance (?) +-- - + +-- SPEC | - +-- - + + + +-------------------------------------------------------------------------------------------------------------------------------------------- +-- GHC Extensions +-------------------------------------------------------------------------------------------------------------------------------------------- +{-# LANGUAGE UnicodeSyntax #-} +{-# LANGUAGE TupleSections #-} +{-# LANGUAGE NamedFieldPuns #-} +{-# LANGUAGE FlexibleContexts #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE ScopedTypeVariables #-} +--{-# LANGUAGE OverloadedLists #-} + + + +-------------------------------------------------------------------------------------------------------------------------------------------- +-- Section +-------------------------------------------------------------------------------------------------------------------------------------------- +-- TODO | - Clean this up +module Graphics.WaveFront.Model ( + BoundingBox(..), + facesOf, materialsOf, + tessellate, bounds, + hasTextures, textures, + createModel, createMTLTable, + fromIndices, fromFaceIndices, diffuseColours +) where + + + +-------------------------------------------------------------------------------------------------------------------------------------------- +-- We'll need these +-------------------------------------------------------------------------------------------------------------------------------------------- +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 + + + +-------------------------------------------------------------------------------------------------------------------------------------------- +-- Functions +-------------------------------------------------------------------------------------------------------------------------------------------- + +-------------------------------------------------------------------------------------------------------------------------------------------- + +-- TODO | - Factor out these combinators + +-- | Performs a computation on adjacent pairs in a list +-- TODO | - Factor out and make generic +pairwise :: (a -> a -> b) -> [a] -> [b] +pairwise f xs = zipWith f xs (drop 1 xs) + + +-- | Convers an Either to a Maybe +eitherToMaybe :: Either a b -> Maybe b +eitherToMaybe (Right b) = Just b +eitherToMaybe (Left _) = Nothing + + +-- | Converts a Maybe to an Either +maybeToEither :: a -> Maybe b -> Either a b +maybeToEither _ (Just b) = Right b +maybeToEither a (Nothing) = Left a + +-- Parser output churners (OBJ) ------------------------------------------------------------------------------------------------------------ + +-- TODO | - Move to separate module (eg. WaveFront.Model) + +-- | Creates a mapping between group names and the corresponding bounds ([lower, upper)). +-- +-- TODO | - Figure out how to deal with multiple group names (eg. "g mesh1 nose head") +-- - Include not just face indices but vertex indices (makes it easier to 'slice' GPU buffers) (maybe in a separate function) +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 + + +-- | Creates a mapping between object names and the corresponding bounds ([lower, upper)). +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 + + +-- | Creates a mapping between names (of groups or objects) to face indices +-- +-- TODO | - Refactor, simplify +-- - What happens if the same group or object appears multiple times (is that possible?) +-- - Rename or add function parameter (the -With suffix implies a function parameter) +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)) + + -- TODO | - Separate Group and Object lists + -- - Rename (?) + -- - Factor out (might be useful for testing) (?) + 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 + + +-- | Filters out faces from a stream of OBJTokens and attaches the currently selected material, +-- as defined by the most recent LibMTL and UseMTL tokens. +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 + -- | It's not always rude to make faces + -- TODO | - Keep refactoring... + -- - Rename (?) + 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' } + + +-- | Tries to find a given material in the specified MTL table +-- TODO | - Specify missing material or library name (would require additional constraints on 's') +-- - Refactor +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) + +-- Parser output churners (MTL) ------------------------------------------------------------------------------------------------------------ + +-- | Constructs an MTL table from a list of (libraryName, token stream) pairs. +-- TODO | - Refactor, simplify +createMTLTable :: Ord s => [(s, [MTLToken f s])] -> Either String (MTLTable f s) +createMTLTable = fmap M.fromList . mapM (\(name, tokens) -> (name,) <$> materialsOf tokens) + + +-- | Constructs a map between names and materials. Incomplete material definitions +-- result in an error (Left ...). +-- +-- TODO | - Debug information (eg. attributes without an associated material) +-- - Pass in error function (would allow for more flexible error handling) (?) +-- - Deal with duplicated attributes (probably won't crop up in any real situations) +materialsOf :: Ord s => [MTLToken f s] -> Either String (Map s (Material f s)) +materialsOf = fmap M.fromList . mapM createMaterial . partitionMaterials + + +-- | Creates a new (name, material) pair from a stream of MTL tokens. +-- The first token should be a new material name. +createMaterial :: [MTLToken f s] -> Either String (s, Material f s) +createMaterial (NewMaterial name:attrs) = (name,) <$> fromAttributes attrs +createMaterial attrs = Left $ "Free-floating attributes" + + +-- | Breaks a stream of MTL tokens into lists of material definitions +-- TODO | - Rename (eg. groupMaterials) (?) +partitionMaterials :: [MTLToken f s] -> [[MTLToken f s]] +partitionMaterials = groupBy (\_ b -> not $ isNewMaterial b) + where + isNewMaterial (NewMaterial _) = True + isNewMaterial _ = False + + +-- | Creates a material +fromAttributes :: [MTLToken f s] -> Either String (Material f s) +fromAttributes attrs = case colours' of + Nothing -> Left $ "Missing colour(s)" -- TODO: More elaborate message (eg. which colour) + Just (amb, diff, spec) -> Right $ Material { fAmbient=amb,fDiffuse=diff, fSpecular=spec, fTexture=texture' } + where + colours' = materialColours attrs + texture' = listToMaybe [ name | MapDiffuse name <- attrs ] + + +-- | Tries to extract a diffuse colour, a specular colour, and an ambient colour from a list of MTL tokens +-- TODO | - Should we really require all three colour types (?) +-- - Rename (?) +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 ] + +-- API functions --------------------------------------------------------------------------------------------------------------------------- + +-- | Constructs a model from a stream of OBJ tokens, a materials table and an optional path to root of the model (used for textures, etc.) +-- +-- TODO | - Performance, how are 'copies' of coordinates handled (?) +-- - Performance, one pass (with a fold perhaps) +-- +-- I never knew pattern matching in list comprehensions could be used to filter by constructor +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 } -- indices %~ (_) -- TODO: Type-changing lenses + + packFaces :: [] (Face f s i []) -> Vector (Face f s i Vector) + packFaces = V.fromList . map (packFace . tessellate) + + +-- | +-- TODO | - Specialise to [[Face]] (?) +-- - Check vertex count (has to be atleast three) +-- - Better names (?) +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 + + +-- | Finds the axis-aligned bounding box of the model +-- TODO | - Deal with empty vertex lists (?) +-- - Refactor +-- - Folding over applicative (fold in parallel) +-- - Make sure the order is right +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 + -- TODO | - Factor out 'minmax' + 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 -- TODO: Factor out + + axisBounds vs axis = minmaxBy (^.axis) vs + +-- Orphaned TODOs? + +-- TODO | - Deal with missing values properly +-- - Indexing should be defined in an API function + +-------------------------------------------------------------------------------------------------------------------------------------------- + +-- TODO | - Polymorphic indexing and traversing +-- - Profile, optimise +-- - Index buffers + + +-- | Takes a vector of data, an index function, a choice function, a vector of some type with indices +-- and uses the indices to constructs a new Vector with the data in the original vector. +-- +-- TODO | - Factor out the buffer-building logic +-- - Rewrite the docs... +fromIndices :: Vector v -> (Vector v -> i -> b) -> (a -> i) -> Vector a -> Vector b +fromIndices data' index choose = V.map (index data' . choose) + + +-- | +-- . fromIntegral . subtract 1 +-- . (^.indices) +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' + +-- Model queries --------------------------------------------------------------------------------------------------------------------------- + +-- | Does the model have textures? +hasTextures :: Ord s => Model f s i m -> Bool +hasTextures = not . S.null . textures + + +-- | The set of all texture names +textures :: Ord s => Model f s i m -> S.Set s +textures = S.fromList . catMaybes . map (^.texture) . concatMap M.elems . M.elems . (^.materials)
+ src/Graphics/WaveFront/Parse.hs view
@@ -0,0 +1,87 @@+-- | +-- Module : Graphics.WaveFront.Parse +-- Description : +-- Copyright : (c) Jonatan H Sundqvist, February 8 2015 +-- License : MIT +-- Maintainer : Jonatan H Sundqvist +-- Stability : experimental|stable +-- Portability : POSIX (not sure) +-- +-- Created February 8 2015 +-- Wavefront - Parsers.hs +-- Migrated to separate project on February 21 2015 + +-- TODO | - Appropriate container types (eg. bytestring, vector) +-- - Grammar specification +-- - Incremental parsing (?) +-- - Improve naming scheme +-- -- Remove 'parse-' prefix, import qualified (eg. 'Parse.obj') +-- +-- - Separate MTL and OBJ parsers (?) (...) +-- - Separate parsing, processing, logging, IO and testing (...) +-- -- Proper path handling (eg. include root in MTLTable or not) +-- +-- - Additional attributes (lighting, splines, etc.) +-- - FFI (...) +-- - Debugging information (line number, missing file, missing values, etc.) (...) +-- - Proper Haddock coverage, including headers (...) +-- - Model type (✓) +-- - Caching (?) +-- - Performance, profiling, optimisations +-- -- Strict or lazy (eg. with Data.Map) (?) +-- -- Multi-threading +-- -- Appropriate container types +-- +-- - PrintfArg instances for the types defined in this module +-- - Reconciling Cabal and hierarchical modules +-- - Dealing with paths in lib statements (requires knowledge of working directories) +-- - Move comments and specification to separate files (eg. README) +-- - Inline comments (for internals, implementation) +-- +-- - Full OBJ spec compliance +-- -- Do the usemtl and libmtl statements affect vertices or faces (?) +-- +-- - Parser bugs +-- -- Negative coordinates enclosed in parentheses (✓) +-- +-- - Decide on a public interface (exports) (API) +-- -- Model will be the main API type +-- -- Processing utils (eg. iterating over model faces; withModelFaces :: ((Material, [(Vertex, Maybe Normalcoords, Maybe Texcoords)]) -> b) -> Model -> [b]) +-- -- Export functions for working with the output data (eg. unzipIndices :: [(Int, Int, Int)] -> ([Int], [Int], [Int])) +-- -- Export certain utilities (eg. second, perhaps in another module) (?) + +-- SPEC | - +-- - + + + +-------------------------------------------------------------------------------------------------------------------------------------------- +-- GHC Extensions +-------------------------------------------------------------------------------------------------------------------------------------------- +{-# LANGUAGE UnicodeSyntax #-} +{-# LANGUAGE TupleSections #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE NamedFieldPuns #-} + + + +-------------------------------------------------------------------------------------------------------------------------------------------- +-- API +-------------------------------------------------------------------------------------------------------------------------------------------- +-- TODO: Clean this up +module Graphics.WaveFront.Parse ( + module Graphics.WaveFront.Types, -- TODO: Don't export internal types (duh) + obj, mtl, + comment, lineSeparator +) where + + + +-------------------------------------------------------------------------------------------------------------------------------------------- +-- We'll need these +-------------------------------------------------------------------------------------------------------------------------------------------- +import Graphics.WaveFront.Parse.Common +import Graphics.WaveFront.Parse.OBJ (obj) +import Graphics.WaveFront.Parse.MTL (mtl) + +import Graphics.WaveFront.Types
+ src/Graphics/WaveFront/Parse/Common.hs view
@@ -0,0 +1,166 @@+-- | +-- Module : Graphics.WaveFront.Parse.Common +-- Description : +-- Copyright : (c) Jonatan H Sundqvist, October 2 2016 +-- License : MIT +-- Maintainer : Jonatan H Sundqvist +-- Stability : experimental|stable +-- Portability : POSIX (not sure) + +-- TODO | - Fully polymorphic (even in the string and list types) (?) +-- - + +-- SPEC | - +-- - + + + +-------------------------------------------------------------------------------------------------------------------------------------------- +-- GHC Extensions +-------------------------------------------------------------------------------------------------------------------------------------------- +{-# LANGUAGE OverloadedStrings #-} + + + +-------------------------------------------------------------------------------------------------------------------------------------------- +-- Section +-------------------------------------------------------------------------------------------------------------------------------------------- +module Graphics.WaveFront.Parse.Common where + + + +-------------------------------------------------------------------------------------------------------------------------------------------- +-- We'll need these +-------------------------------------------------------------------------------------------------------------------------------------------- +import Data.Text (Text, pack) +import qualified Data.Attoparsec.Text as Atto + +import Control.Applicative (pure, liftA2, (<$>), (<*>), (<*), (*>), (<|>)) +import Linear (V2(..), V3(..)) + +import Graphics.WaveFront.Types + + + +-------------------------------------------------------------------------------------------------------------------------------------------- +-- Functions (pure) +-------------------------------------------------------------------------------------------------------------------------------------------- + +-- Jon's little helpers -------------------------------------------------------------------------------------------------------------------- + +-- | Consumes all input, including any leading or trailing comments and whitespace +-- TODO | - Rename (?) +wholeFile :: Atto.Parser a -> Atto.Parser a +wholeFile p = cutToTheChase *> p <* cutToTheChase <* Atto.endOfInput + + +-- | Skips any leading comments, line breaks and empty lines +-- TODO | - Rename (?) +-- - Skip whitespace +cutToTheChase :: Atto.Parser () +cutToTheChase = Atto.skipMany ((comment *> pure ()) <|> (Atto.satisfy isLinearSpace *> pure ()) <|> Atto.endOfLine) + + +-- | OBJ rows may be separated by one or more lines of comments and whitespace, or empty lines. +-- TODO | - Make sure this is right +lineSeparator :: Atto.Parser () +lineSeparator = Atto.skipMany1 $ ignore space *> ignore comment *> Atto.endOfLine + + +-- | Parses a comment (from the '#' to end of the line), possibly preceded by whitespace +-- TODO | - Break out the whitespace part (?) +comment :: Atto.Parser Text +comment = Atto.skipSpace *> Atto.char '#' *> Atto.takeTill (\c -> (c == '\r') || (c == '\n')) -- TODO: Is the newline consumed (?) + + +-- | Tries the given parser, falls back to 'Nothing' if it fails +-- TODO | - Use 'try' to enforce backtracking (?) +optional :: Atto.Parser a -> Atto.Parser (Maybe a) +optional p = Atto.option Nothing (Just <$> p) + + +-- | Like Atto.skipMany, except it skips one match at the most +ignore :: Atto.Parser a -> Atto.Parser () +ignore p = optional p *> pure () + + +-- | +atleast :: Int -> Atto.Parser a -> Atto.Parser [a] +atleast n p = liftA2 (++) (Atto.count n p) (Atto.many' p) + + +-- | Skips atleast one white space character (not including newlines and carriage returns) +space :: Atto.Parser () +space = Atto.skipMany1 (Atto.satisfy isLinearSpace) + + +-- | Predicate for linear space (eg. whitespace besides newlines) +-- TODO | - Unicode awareness (cf. Data.Char.isSpace) +-- - Come up with a better name (?) +isLinearSpace :: Char -> Bool +isLinearSpace c = (c == ' ') || (c == '\t') + + +-- | One or more letters (cf. 'Atto.letter' for details) +word :: Atto.Parser Text +word = pack <$> Atto.many1 Atto.letter + + +-- | Used for texture, material, object and group names (and maybe others that I have yet to think of) +-- TODO | - Use Unicode groups, make more robust (?) +name :: Atto.Parser Text +name = pack <$> Atto.many1 (Atto.satisfy $ \c -> (c /= ' ') && (c /= '\t') && (c /= '\r') && (c /= '\n')) + + +-- | Parses the strings "off" (False) and "on" (True) +toggle :: Atto.Parser Bool +toggle = (Atto.string "off" *> pure False) <|> (Atto.string "on" *> pure True) + + +-- | Wraps a parser in a '(' and a ')', with no whitespace in between +parenthesised :: Atto.Parser a -> Atto.Parser a +parenthesised p = Atto.char '(' *> p <* Atto.char ')' + + +-- TODO | - Allow scientific notation (?) + +-- | +coord :: Fractional f => Atto.Parser f +coord = space *> (parenthesised Atto.rational <|> Atto.rational) + + +-- | A single colour channel +-- TODO | - Clamp to [0,1] (cf. partial from monadplus) (?) +-- - Can channels be parenthesised (?) +channel :: Fractional f => Atto.Parser f +channel = space *> (parenthesised Atto.rational <|> Atto.rational) + + +-- | A colour with three or four channels (RGB[A]) +colour :: Fractional f => Atto.Parser (Colour f) +colour = Colour <$> channel <*> channel <*> channel <*> Atto.option 1 channel + + +-- | A point in 3D space +point3D :: Fractional f => Atto.Parser (V3 f) +point3D = V3 <$> coord <*> coord <*> coord + + +-- | A point in 2D space +point2D :: Fractional f => Atto.Parser (V2 f) +point2D = V2 <$> coord <*> coord + + +-- | +clamp :: Ord n => n -> n -> n -> Atto.Parser n +clamp lower upper n + | between lower upper n = pure n + | otherwise = fail "Number not in range" + where + between lw up n = (lower <= n) && (n <= upper) + -- between 0 <. n <. 5 + +-- | +-- TODO | - Clean up and generalise +clamped :: Integral i => i -> i -> Atto.Parser i +clamped lower upper = Atto.decimal >>= clamp lower upper
+ src/Graphics/WaveFront/Parse/MTL.hs view
@@ -0,0 +1,142 @@+-- | +-- Module : Graphics.WaveFront.Parse.MTL +-- Description : +-- Copyright : (c) Jonatan H Sundqvist, October 2 2016 +-- License : MIT +-- Maintainer : Jonatan H Sundqvist +-- Stability : experimental|stable +-- Portability : POSIX (not sure) + +-- TODO | - +-- - + +-- SPEC | - +-- - + + + +-------------------------------------------------------------------------------------------------------------------------------------------- +-- GHC Extensions +-------------------------------------------------------------------------------------------------------------------------------------------- +{-# LANGUAGE UnicodeSyntax #-} +{-# LANGUAGE TupleSections #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE NamedFieldPuns #-} + + + +-------------------------------------------------------------------------------------------------------------------------------------------- +-- API +-------------------------------------------------------------------------------------------------------------------------------------------- +module Graphics.WaveFront.Parse.MTL ( + mtl, row, token, + ambient, diffuse, specular, + mapDiffuse, newMaterial +) where + + + +-------------------------------------------------------------------------------------------------------------------------------------------- +-- We'll need these +-------------------------------------------------------------------------------------------------------------------------------------------- +-- import qualified Data.Map as M +-- import qualified Data.Set as S +-- import qualified Data.Vector as V +import Data.Text (Text) + +import qualified Data.Attoparsec.Text as Atto + +import Control.Applicative ((<$>), (<*), (*>), (<|>)) + +import Graphics.WaveFront.Parse.Common + +import Graphics.WaveFront.Types hiding (ambient, diffuse, specular) + + + +-------------------------------------------------------------------------------------------------------------------------------------------- +-- Functions +-------------------------------------------------------------------------------------------------------------------------------------------- + +-- MTL parsing ----------------------------------------------------------------------------------------------------------------------------- + +-- | Produces a list of MTL tokens +mtl :: (Fractional f) => Atto.Parser (MTL f Text []) +mtl = Atto.sepBy row lineSeparator + + +-- | Parses a single MTL row. +row :: (Fractional f) => Atto.Parser (MTLToken f Text) +row = token <* ignore comment + +-------------------------------------------------------------------------------------------------------------------------------------------- + +-- | Parse an MTL token +-- TODO: How to deal with common prefix (Ka, Kd, Ks) (backtrack?) +token :: (Fractional f) => Atto.Parser (MTLToken f Text) +token = (Atto.string "Ka" *> ambient) <|> + (Atto.string "Kd" *> diffuse) <|> + (Atto.string "Ks" *> specular) <|> + (Atto.string "Ns" *> specExp) <|> + (Atto.string "illum" *> illum) <|> + (Atto.string "Ni" *> refraction) <|> + (Atto.string "d" *> dissolve) <|> -- TODO: Handle inverse as well (cf. 'Tr' attribute) + (Atto.string "map_Kd" *> mapDiffuse) <|> + (Atto.string "map_Ka" *> mapAmbient) <|> + (Atto.string "newmtl" *> newMaterial) + +-------------------------------------------------------------------------------------------------------------------------------------------- + +-- TODO: Expose these parsers for testing purposes (?) + +-- TODO | - Change definition of 'colour' and 'Colour' to only allow three channels (alpha is handled by the 'dissolve' attribute) +-- - Change the definition of 'Colour' or use the one defined in the colour package + +-- | Three or four channel values (RGB[A]) +ambient :: (Fractional f) => Atto.Parser (MTLToken f s) +ambient = Ambient <$> colour + + +-- | Three or four channel values (RGB[A]) +diffuse :: (Fractional f) => Atto.Parser (MTLToken f s) +diffuse = Diffuse <$> colour + + +-- | Three or four channel values (RGB[A]) +specular :: (Fractional f) => Atto.Parser (MTLToken f s) +specular = Specular <$> colour + + +-- | A rational number, preceded by whitespace (specular exponent) +specExp :: (Fractional f) => Atto.Parser (MTLToken f s) +specExp = space *> (SpecularExponent <$> Atto.rational) + + +-- | A number between 0 and 10 (inclusive) (illumination model) +illum :: Atto.Parser (MTLToken f s) +illum = space *> (Illum <$> clamped 0 10) + + +-- | A rational number, preceded by whitespace (refraction index) +refraction :: (Fractional f) => Atto.Parser (MTLToken f s) +refraction = space *> (Refraction <$> Atto.rational) + + +-- | A rational number, preceded by whitespace (doss) +dissolve :: (Fractional f) => Atto.Parser (MTLToken f s) +dissolve = space *> (Dissolve <$> Atto.rational) + + +-- | A texture name, preceded by whitespace +mapDiffuse :: Atto.Parser (MTLToken f Text) +mapDiffuse = space *> (MapDiffuse <$> name) + + +-- | A texture name, preceded by whitespace +mapAmbient :: Atto.Parser (MTLToken f Text) +mapAmbient = space *> (MapAmbient <$> name) + + +-- | A material name, preceded by whitespace +newMaterial :: Atto.Parser (MTLToken f Text) +newMaterial = space *> (NewMaterial <$> name)
+ src/Graphics/WaveFront/Parse/OBJ.hs view
@@ -0,0 +1,172 @@+-- | +-- Module : Graphics.WaveFront.Parse.OBJ +-- Description : +-- Copyright : (c) Jonatan H Sundqvist, October 2 2016 +-- License : MIT +-- Maintainer : Jonatan H Sundqvist +-- Stability : experimental|stable +-- Portability : POSIX (not sure) + +-- TODO | - Fully polymorphic (even in the string and list types) (?) +-- - + +-- SPEC | - +-- - + + + +-------------------------------------------------------------------------------------------------------------------------------------------- +-- GHC Extensions +-------------------------------------------------------------------------------------------------------------------------------------------- +{-# LANGUAGE UnicodeSyntax #-} +{-# LANGUAGE TupleSections #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE NamedFieldPuns #-} + + + +-------------------------------------------------------------------------------------------------------------------------------------------- +-- API +-------------------------------------------------------------------------------------------------------------------------------------------- +module Graphics.WaveFront.Parse.OBJ ( + obj, row, face, + normal, texture, vertex, object, group, + lib, use, + vertexIndices, +) where + + + +-------------------------------------------------------------------------------------------------------------------------------------------- +-- We'll need these +-------------------------------------------------------------------------------------------------------------------------------------------- +import Data.Text (Text) +-- import qualified Data.Vector as V +import qualified Data.Set as S + +import qualified Data.Attoparsec.Text as Atto + +import Control.Applicative ((<$>), (<*>), (<*), (*>), (<|>)) + +-- import Linear (V2(..), V3(..)) + +import Graphics.WaveFront.Parse.Common +import Graphics.WaveFront.Types hiding (texture) + + + +-------------------------------------------------------------------------------------------------------------------------------------------- +-- Functions +-------------------------------------------------------------------------------------------------------------------------------------------- + +-- OBJ parsing ----------------------------------------------------------------------------------------------------------------------------- + +-- | This function creates an OBJToken or error for each line in the input data +obj :: (Fractional f, Integral i) => Atto.Parser (OBJ f Text i []) +obj = Atto.sepBy row lineSeparator -- <* Atto.endOfInput + + +-- | Parses a token given a single valid OBJ row +-- +-- TODO | - Correctness (total function, no runtime exceptions) +-- - Handle invalid rows (how to deal with mangled definitions w.r.t indices?) +-- - Use ListLike or Monoid (or maybe Indexable, since that's the real requirement) (?) +row :: (Fractional f, Integral i) => Atto.Parser (OBJToken f Text i []) +row = token <* ignore comment -- TODO: Let the separator handle comments (?) + + +-- | +-- Parses an OBJ token +token :: (Fractional f, Integral i) => Atto.Parser (OBJToken f Text i []) +token = (Atto.string "f" *> face) <|> + (Atto.string "l" *> line) <|> + -- TODO: How to deal with common prefix (v, vn, vt) (backtrack?) (doesn't seem to be a problem) + (Atto.string "vn" *> normal) <|> + (Atto.string "vt" *> texture) <|> + (Atto.string "v" *> vertex) <|> + (Atto.string "o" *> object) <|> + (Atto.string "g" *> group) <|> + (Atto.string "s" *> smooth) <|> + (Atto.string "mtllib" *> lib) <|> + (Atto.string "usemtl" *> use) + + +-- TODO: Expose these parsers for testing purposes (?) + +-------------------------------------------------------------------------------------------------------------------------------------------- + +-- | Three or more vertex definitions (cf. 'vertexIndices' for details) +face :: Integral i => Atto.Parser (OBJToken f Text i []) +face = OBJFace <$> vertexIndices + + +-- | A single vertex definition with indices for vertex position, normal, and texture coordinates +-- +-- TODO: | - Should the slashes be optional? +-- - Allowed trailing slashes (I'll have to check the spec again) (?) +-- +-- f Int[/((Int[/Int])|(/Int))] +vertexIndices :: Integral i => Atto.Parser [VertexIndices i] +vertexIndices = atleast 3 (space *> (ivertex <*> index <*> index)) <|> -- vi/ti/ni + atleast 3 (space *> (ivertex <*> nothing <*> skipIndex)) <|> -- vi//ni + atleast 3 (space *> (ivertex <*> index <*> nothing)) <|> -- vi/ti + atleast 3 (space *> (ivertex <*> nothing <*> nothing)) -- vi + where + ivertex :: Integral i => Atto.Parser (Maybe i -> Maybe i -> VertexIndices i) + ivertex = VertexIndices <$> Atto.decimal + + index :: Integral i => Atto.Parser (Maybe i) + index = Just <$> (Atto.char '/' *> Atto.decimal) + + skipIndex :: Integral i => Atto.Parser (Maybe i) + skipIndex = Atto.char '/' *> index + + nothing :: Atto.Parser (Maybe i) + nothing = pure Nothing + +-- Geometry primitives --------------------------------------------------------------------------------------------------------------------- + +-- | Two integers, separated by whitespace +line :: Integral i => Atto.Parser (OBJToken f Text i m) +line = Line <$> (space *> Atto.decimal) <*> (space *> Atto.decimal) + +-------------------------------------------------------------------------------------------------------------------------------------------- + +-- | Three cordinates, separated by whitespace +normal :: (Fractional f) => Atto.Parser (OBJToken f Text i m) +normal = OBJNormal <$> point3D + + +-- | Two coordinates, separated by whitespace +texture :: (Fractional f) => Atto.Parser (OBJToken f Text i m) +texture = OBJTexCoord <$> point2D + + +-- | Three coordinates, separated by whitespace +vertex :: (Fractional f) => Atto.Parser (OBJToken f s i m) +vertex = OBJVertex <$> point3D + + +-- | Object names, separated by whitespace +object :: Atto.Parser (OBJToken f Text i m) +object = Object . S.fromList <$> atleast 1 (space *> name) + + +-- | Group names, separated by whitespace +group :: Atto.Parser (OBJToken f Text i m) +group = Group . S.fromList <$> atleast 1 (space *> name) + + +-- | Either 'on' or 'off' +smooth :: Atto.Parser (OBJToken f s i m) +smooth = SmoothShading <$> (space *> toggle) + + +-- | An MTL library name +lib :: Atto.Parser (OBJToken f Text i m) +lib = LibMTL <$> (space *> name) + + +-- | An MTL material name +use :: Atto.Parser (OBJToken f Text i m) +use = UseMTL <$> (space *> name)
− src/Graphics/WaveFront/Parsers.hs
@@ -1,358 +0,0 @@--- | --- Module : Graphics.WaveFront.Parsers --- Description : --- Copyright : (c) Jonatan H Sundqvist, February 8 2015 --- License : MIT --- Maintainer : Jonatan H Sundqvist --- Stability : experimental|stable --- Portability : POSIX (not sure) --- --- Created February 8 2015 --- Wavefront - Parsers.hs --- Migrated to separate project on February 21 2015 - --- TODO | - Appropriate container types (eg. bytestring, vector) --- - Grammar specification --- - Incremental parsing (?) --- - Improve naming scheme --- --- - Separate MTL and OBJ parsers (?) (...) --- - Separate parsing, processing, logging, IO and testing (...) --- -- Proper path handling (eg. include root in MTLTable or not) --- --- - Additional attributes (lighting, splines, etc.) --- - FFI (...) --- - Debugging information (line number, missing file, missing values, etc.) (...) --- - Proper Haddock coverage, including headers (...) --- - Model type (...) --- - Caching (?) --- - Performance, profiling, optimisations --- -- Strict or lazy (eg. with Data.Map) (?) --- -- Multi-threading --- -- Appropriate container types --- --- - PrintfArg instances for the types defined in this module --- - Reconciling Cabal and hierarchical modules --- - Dealing with paths in lib statements (requires knowledge of working directories) --- - Move comments and specification to separate files (eg. README) --- - Inline comments (for internals, implementation) --- --- - Full OBJ spec compliance --- -- Do the usemtl and libmtl statements affect vertices or faces (?) --- --- - Parser bugs --- -- Negative coordinates enclosed in parentheses --- --- - Decide on a public interface (exports) (API) --- -- Model will be the main API type --- -- Processing utils (eg. iterating over model faces; withModelFaces :: ((Material, [(Vertex, Maybe Normalcoords, Maybe Texcoords)]) -> b) -> Model -> [b]) --- -- Export functions for working with the output data (eg. unzipIndices :: [(Int, Int, Int)] -> ([Int], [Int], [Int])) --- -- Export certain utilities (eg. second, perhaps in another module) (?) - --- SPEC | - --- - - - - --------------------------------------------------------------------------------------------------------------------------------------------- --- GHC Extensions --------------------------------------------------------------------------------------------------------------------------------------------- -{-# LANGUAGE UnicodeSyntax #-} -{-# LANGUAGE TupleSections #-} - - - --------------------------------------------------------------------------------------------------------------------------------------------- --- Section --------------------------------------------------------------------------------------------------------------------------------------------- -module Graphics.WaveFront.Parsers (parseOBJ, parseMTL, - facesOf, materialsOf, - modelAttributes, tessellate, boundingbox, - hasTextures, textures, - module Graphics.WaveFront.Types, - BoundingBox(..), Vector(..), - createModel, createMTLTable) where - - - --------------------------------------------------------------------------------------------------------------------------------------------- --- We'll need these --------------------------------------------------------------------------------------------------------------------------------------------- -import Data.List (groupBy, unzip4) -import Data.Maybe (listToMaybe, catMaybes) -import Data.Either (rights) -import Data.Char (isSpace) -import qualified Data.Map as Map -import qualified Data.Set as Set - -import Text.Read (readMaybe, readEither) -import Control.Monad (liftM) -import Control.Lens hiding (indices) - --- import Southpaw.Utilities.Utilities (pairwise, cuts) - -import Cartesian.Space.Types (BoundingBox(..), Vector3D(..)) - -import Graphics.WaveFront.Types -import Graphics.WaveFront.Utilities - - - --------------------------------------------------------------------------------------------------------------------------------------------- --- Functions (pure) --------------------------------------------------------------------------------------------------------------------------------------------- - --- OBJ parsing ----------------------------------------------------------------------------------------------------------------------------- - --- | This function creates an OBJToken or error for each line in the input data --- --- TODO: Use appropriate container type (cf. TODO section) --- TODO: Extract filter predicate (isComment, isEmpty) --- TODO: Is it even necessary to strip whitespace? --- TODO: Function for composing predicates (?) --- TODO: Should this function separate the various fields (eg. [(Vertices, Faces, Materials, Groups)] instead of [Maybe OBJToken]) --- -parseOBJ :: String -> OBJ -parseOBJ = enumerate . map parseOBJRow . lines -- . rows - - --- | Generates a token given a single valid OBJ row, or an error value if the input is malformed. --- --- TODO: Correctness (total function, no runtime exceptions) --- TODO: Rename 'which' (?) --- TODO: Handle invalid rows (how to deal with mangled definitions w.r.t indices?) --- TODO: Extract value parsing logic (eg. pattern matching, converting, handle errors) - --- TODO: Named errors (typed?) rather than Nothing (cf. Either) (?) --- Type for unsupported but valid (according to spec) attributes (?) --- Type for specific attribute that failed to parse (eg. "f 1/2 0/p 1.5/x") --- --- TODO: Additional values, currently unsupported attributes (ignore?) (pattern match against the entire line, eg. ["vn", x, y, z]) --- TODO: Dealing with MTL definitions (pass in names, MTL value, return list of MTL dependencies) --- TODO: Take 1-based indexing into account straight away (?) --- TODO: Deal with absent texture and normal indices (✓) --- TODO: Strip trailing comments (✓) --- TODO: Don't ignore leftover values (errors?) (...) --- -parseOBJRow :: String -> (Int -> OBJRow) -- Maybe OBJToken -parseOBJRow ln = parseTokenWith ln $ \ line -> let (attr:values) = words line in case (attr:values) of - ("f":_:_:_:_) -> either (Left . noparse . const line) (Right . OBJFace) . sequence . map (ivertex . cuts '/') $ values -- Face - ["v", sx, sy, sz] -> vector (\ [x, y, z] -> OBJVertex x y z) [sx, sy, sz] (noparse line) -- Vertex - ["vn", sx, sy, sz] -> vector (\ [x, y, z] -> OBJNormal x y z) [sx, sy, sz] (noparse line) -- Normal - ["vt", sx, sy] -> vector (\ [x, y] -> OBJTexture x y) [sx, sy] (noparse line) -- Texture - ("g":_:_) -> Right $ Group values -- Group - ("o":_:_) -> Right $ Object values -- Object - ("s":_:_) -> Left $ noparse line -- Smooth shading - ["mtllib", lib] -> Right $ LibMTL lib -- - ["usemtl", mtl] -> Right $ UseMTL mtl -- - _ -> Left $ noparse line -- TODO: More informative errors - where - -- ivertex :: [String] -> Either OBJNoParse OBJToken - ivertex [svi, sti, sni] = readEither svi >>= \ vi -> Right $ (vi, readMaybe sti, readMaybe sni) -- TODO: Refactor, simplify - ivertex [svi, sti] = readEither svi >>= \ vi -> Right $ (vi, readMaybe sti, Nothing) -- TODO: Refactor, simplify - ivertex [svi] = readEither svi >>= \ vi -> Right $ (vi, Nothing, Nothing) -- TODO: Refactor, simplify - ivertex _ = Left ln - - noparse line - | null line || all isSpace line = OBJEmpty - | isComment line = OBJComment line - | otherwise = OBJNoParse line - - --- | Returns a list of annotated parsing errors or the original OBJ data structure if --- validateOBJ :: OBJ -> Either [OBJNoParse] OBJ --- validateOBJ _ = undefined - --- MTL parsing ----------------------------------------------------------------------------------------------------------------------------- - --- | Produces a list of MTL tokens, with associated line numbers and comments -parseMTL :: String -> MTL -parseMTL = enumerate . map parseMTLRow . lines - - --- | Parses a single MTL row. --- --- TOOD: Simplify 'withChannels' --- TOOD: Process the MTL tokens (✗) --- TODO: cf. parseOBJRow --- -parseMTLRow :: String -> (Int -> MTLRow) -parseMTLRow ln = parseTokenWith ln $ \ line -> let (which:values) = words line in case which:values of - ("Ka":sr:sg:sb:rest) -> withChannels Ambient sr sg sb rest line -- Ka - ("Kd":sr:sg:sb:rest) -> withChannels Diffuse sr sg sb rest line -- Kd - ("Ks":sr:sg:sb:rest) -> withChannels Specular sr sg sb rest line -- Ks - ["map_Kd", name] -> Right $ MapDiffuse name -- map_Kd - ["newmtl", name] -> Right $ NewMaterial name -- newmtl - _ -> Left $ noparse line -- - where - withChannels f sr sg sb [] line = vector (\[r, g, b] -> f r g b Nothing) [sr, sg, sb] (noparse line) -- TODO: Refactor, simplify - withChannels f sr sg sb [sa] line = vector (\[r, g, b] -> f r g b $ readMaybe sa) [sr, sg, sb] (noparse line) -- TODO: Refactor, simplify - withChannels _ _ _ _ _ line = Left $ noparse line - - noparse line - | null line || all isSpace line = MTLEmpty - | isComment line = MTLComment line - | otherwise = MTLNoParse line - --- Parser output churners (OBJ) ------------------------------------------------------------------------------------------------------------ - --- | Creates a mapping between group names and the corresponding bounds ([lower, upper)). Invalid --- tokens are simply discarded by this function. --- --- TODO: Figure out how to deal with multiple group names (eg. "g mesh1 nose head") -groupsOf :: [OBJToken] -> Map.Map [String] (Int, Int) -groupsOf = buildIndexMapWith . filter notObject - where notObject (Object _) = False - notObject _ = True - - --- | -objectsOf :: [OBJToken] -> Map.Map [String] (Int, Int) -objectsOf = buildIndexMapWith . filter notGroup - where notGroup (Group _) = False - notGroup _ = True - - --- | Creates a mapping between names (of groups or objects) to face indices --- --- TODO: Refactor, simplify --- -buildIndexMapWith :: [OBJToken] -> Map.Map [String] (Int, Int) -buildIndexMapWith tokens = Map.fromList . pairwise zipIndices . reverse . addLastIndex $ foldl update (0, []) $ tokens - where addLastIndex (nfaces, groups') = ([], nfaces):groups' - zipIndices (names, low) (_, upp) = (names, (low, upp)) - update (nfaces, groups') token = case token of - Group names -> (nfaces, (names, nfaces):groups') - Object names -> (nfaces, (names, nfaces):groups') - OBJFace _ -> (nfaces+1, groups') - _ -> (nfaces, groups') - - --- | Filters out faces from a stream of OBJTokens and attaches the currently selected material, --- as defined by the most recent LibMTL and UseMTL tokens. --- --- TODO: Don't use foldl (?) --- TODO: Deal with errors (eg. missing materials) --- TODO: Improve naming scheme (lots of primes) --- TODO: Default material, take 'error-handling' function (?) --- -facesOf :: [OBJToken] -> MTLTable -> [Either String Face] -facesOf tokens table = reverse . (^._3) . foldl update ("", "", []) $ tokens - where retrieve lib mat = Map.lookup lib table >>= Map.lookup mat - createFace lib mat ind = case retrieve lib mat of - Nothing -> Left $ "No such material: " ++ lib ++ "." ++ mat - Just m -> Right $ Face { indices=ind, material=m } - update (lib', material', faces') token = case token of - OBJFace ind -> (lib', material', createFace lib' material' ind : faces') - LibMTL lib -> (lib, material', faces') - UseMTL mat -> (lib', mat, faces') - _ -> (lib', material', faces') - --- Parser output churners (MTL) ------------------------------------------------------------------------------------------------------------ - --- | Constructs a map between names and materials. Partially or wholly undefined materials --- are mapped to a string detailing the error (eg. Left "missing specular"). --- --- TODO: Debug information (eg. attributes without an associated material) --- TODO: Pass in error function (would allow for more flexible error handling) (?) --- TODO: Filter out parser failures (?) --- TOOD: Deal with duplicated attributes (probably won't crop up in any real situations) -materialsOf :: [MTLToken] -> Map.Map String (Either String Material) -materialsOf tokens = Map.fromList . rights $ map createMaterial thegroups - where - thegroups = groupBy (((not . isnew) .) . flip const) tokens -- TODO: Refactor this atrocity - isnew (NewMaterial _) = True -- TODO: Rename isnew - isnew _ = False - createMaterial (NewMaterial name:attrs) = Right $ (name, fromAttributes attrs) - createMaterial attrs = Left $ "Free-floating attributes: " ++ show attrs - fromAttributes attrs - | any null colours = Left $ "Missing colour(s)" -- TODO: More elaborate message (eg. which colour) - | otherwise = Right $ Material { ambient=head amb, diffuse=head diff, specular=head spec, texture=listToMaybe [ name | MapDiffuse name <- attrs ] } - where colours@[diff, spec, amb] = [[ (r, g, b, maybe 1.0 id a) | Diffuse r g b a <- attrs ], - [ (r, g, b, maybe 1.0 id a) | Specular r g b a <- attrs ], - [ (r, g, b, maybe 1.0 id a) | Ambient r g b a <- attrs ]] - - --- | --- TODO: Debug information (eg. how many invalid materials were filtered out) --- TODO: Refactor, simplify -createMTLTable :: [(String, [MTLToken])] -> MTLTable -createMTLTable mtls = Map.fromList . map (\ (name, tokens) -> (name, Map.mapMaybe prune . materialsOf $ tokens)) $ mtls - where prune (Right mat) = Just mat - prune (Left _) = Nothing - --- API functions --------------------------------------------------------------------------------------------------------------------------- - --- | --- TODO: Use map for materials (?) --- TODO: How to retrieve MTL data --- TODO: How to deal with errors, including no-parse, index errors, etc. (use applicative?) --- TODO: Performance, how are 'copies' of coordinates handled (?) --- TODO: Performance, one pass (with a fold perhaps) --- TODO: Use a more efficient data structure (especially w.r.t indexing; cf. Vector) --- TODO: Consider preserving the indices (rather than generating a list of duplicated vertices). --- This would preserve space (in cases where vertices are often re-used), as well as being --- very compatible with index buffers on graphics cards. --- --- TODO: Keep map of materials and list of textures in final output (inside model or as items in a tuple) (?) --- --- I never knew pattern matching in list comprehensions could be used to filter by constructor --- let rows = parseOBJ data in ([ v | @v(Vertex {}) <- rows], [ v | @v(Vertex {}) <- rows]) -createModel :: OBJ -> MTLTable -> Model -createModel tokens thematerials = let modeldata = rights $ map (^._2) tokens -- TODO: Vat do vee du viz ze dissidents, kommandant? - in Model { vertices = [ (x, y, z) | OBJVertex x y z <- modeldata ], - normals = [ (x, y, z) | OBJNormal x y z <- modeldata ], - texcoords = [ (x, y) | OBJTexture x y <- modeldata ], - faces = map tessellate . rights $ facesOf modeldata thematerials, - groups = groupsOf modeldata, - objects = objectsOf modeldata, - materials = thematerials } - - --- | Extracts vertex, normal, texture and material data from a model --- TODO: Figure out how to deal with missing indices -modelAttributes :: Model -> (Vertices, TexCoords, Normals, Materials) -modelAttributes model = unzip4 $ concat [ map (attributesAt mat) theindices | Face { material=mat, indices=theindices } <- faces model] - where - vertexAt = (vertices model !!) . subtract 1 -- - normalAt = liftM $ (normals model !!) . subtract 1 -- - texcoordAt = liftM $ (texcoords model !!) . subtract 1 -- - attributesAt mat (vi, ti, ni) = (vertexAt vi, texcoordAt ti, normalAt ni, mat) - -- collect (vs, ts, ns, mats) = (Vertices vs, TexCoords ts, Normals ns, Materials mats) - - --- | --- TODO: Specialise to [[Face]] (?) --- TODO: Check vertex count (has to be atleast three) --- TODO: Better names (?) -tessellate :: Face -> Face -tessellate face@(Face { indices=ind }) = face { indices=triangles ind } - where triangles (a:rest) = concat $ pairwise (\b c -> [a, b, c]) rest - - --- | --- unpackModelAttributes :: - - --- | --- TODO: Deal with empty vertex lists (?) --- boundingbox :: (RealFloat n, Ord n) => Model -> BoundingBox (Vector3D n) -boundingbox :: Model -> BoundingBox (Vector3D Float) -boundingbox model = BoundingBox { centreOf=Vector3D (minx+maxx) (miny+maxy) (minz+maxz) * Vector3D 0.5 0 0, sizeOf=Vector3D (maxx-minx) (maxy-miny) (maxz-minz) } - where - minmax :: (Ord o) => [o] -> (o, o) - minmax (v:alues) = foldr (\val acc -> (min val (fst acc), max val (snd acc))) (v, v) alues -- TODO: Factor out - - [(minx, maxx), (miny, maxy), (minz, maxz)] = [ minmax . map (^.f) $ vertices model | f <- [_1, _2, _3]] -- TODO: Make sure the order is right - --- Model queries --------------------------------------------------------------------------------------------------------------------------- - --- | -hasTextures :: Model -> Bool -hasTextures = not . Set.null . textures -- (/= Nothing) - - --- | All texture names as a list --- TODO: Wrap in ;aybe (instead of empty list) (?) -textures :: Model -> Set.Set String -textures = Set.fromList . catMaybes . map texture . concatMap Map.elems . Map.elems . materials
src/Graphics/WaveFront/Types.hs view
@@ -21,7 +21,12 @@ -------------------------------------------------------------------------------------------------------------------------------------------- -- GHC Pragmas -------------------------------------------------------------------------------------------------------------------------------------------- - +{-# LANGUAGE DuplicateRecordFields #-} -- I love GHC 8.0 +{-# LANGUAGE FlexibleContexts #-} +{-# LANGUAGE StandaloneDeriving #-} +{-# LANGUAGE DeriveFunctor #-} +{-# LANGUAGE UndecidableInstances #-} +{-# LANGUAGE DeriveFoldable #-} @@ -35,8 +40,10 @@ -------------------------------------------------------------------------------------------------------------------------------------------- -- We'll need these -------------------------------------------------------------------------------------------------------------------------------------------- -import qualified Data.Map as Map -import Foreign.Storable +import Data.Functor.Classes (Show1) --Eq1, Show1, showsPrec1, eq1) +import Data.Map as M +import Data.Set as S (Set) +import Linear (V2(..), V3(..)) @@ -45,161 +52,203 @@ -------------------------------------------------------------------------------------------------------------------------------------------- -- OBJ parser types ------------------------------------------------------------------------------------------------------------------------ + +-- TODO | - Add strictness annotations (?) + + -- | Represents a single (valid) OBJ token -- --- TODO: Polymorphic numerical types (?) --- TODO: Add context, metadata (eg. line numbers, filename) (?) --- TODO: Naming scheme (added OBJ prefix to prevent name clashes; cf. Face type) --- TODO: Comment token (preserve comments in parser output or remove them) (?) -data OBJToken = OBJVertex Float Float Float | - OBJNormal Float Float Float | - OBJTexture Float Float | - OBJFace [(Int, Maybe Int, Maybe Int)] | -- TODO: Associate material with each face, handle absent indices +-- TODO | - Polymorphic numerical types (?) +-- - Add context, metadata (eg. line numbers, filename) (?) +-- - Naming scheme (added OBJ prefix to prevent name clashes; cf. Face type) +-- - Comment token (preserve comments in parser output or remove them) (?) +-- +-- - Cover the entire spec (http://www.martinreddy.net/gfx/3d/OBJ.spec) +-- (and handle unimplemented attributes gracefully) +data OBJToken f s i m = OBJVertex (V3 f) | + OBJNormal (V3 f) | + OBJTexCoord (V2 f) | + OBJFace (m (VertexIndices i)) | -- TODO: Associate material with each face, handle absent indices - UseMTL String | -- - LibMTL String | -- TODO: Use actual MTL type + Line i i | -- Line (I'm assuming the arguments are indices to the endpoint vertices) - Group [String] | -- TODO: Do grouped faces have to be consecutive? - Object [String] -- TODO: What is the difference between group and object? - deriving (Eq, Show) -- TODO: Derive Read (?) + UseMTL s | -- TODO: Rename (eg. 'UseMaterial') (?) + LibMTL s | -- + SmoothShading Bool | -- s --- | --- TODO: Rename (?) -data OBJNoParse = OBJComment String | OBJEmpty | OBJNoSuchAttribute String | OBJNoParse String deriving (Show) + -- TODO: Use OBJ prefix (?) + Group (Set s) | -- TODO: Do grouped faces have to be consecutive? + Object (Set s) -- TODO: What is the difference between group and object? + -- deriving (Show, Eq) -- TODO: Derive Read (?) + -- | --- TODO: Use error type instead of String, allowing us to distinguish invalid data --- from eg. comments and blank lines (?) -type OBJRow = (Int, Either OBJNoParse OBJToken, String) +-- TODO: Rename (?) +-- TODO: Use union instead of Maybe (?) +data VertexIndices i = VertexIndices { + fIvertex :: i, + fItexcoord :: Maybe i, + fInormal :: Maybe i +} deriving (Show, Eq) --- | Output type of the OBJ parser. Currently a list of line number and token (or error string) pairs +-- | Output type of the OBJ parser. -- --- TODO: Rename (?) --- TODO: Use Integral for line number (?) +-- TODO | - Rename (?) +-- - Use Integral for line number (?) -- -type OBJ = [OBJRow] +type OBJ f s i m = m (OBJToken f s i m) -- MTL parser types ------------------------------------------------------------------------------------------------------------------------ -- | Represents a single (valid) MTL token -- --- TODO: Is the alpha channel optional, ignored, disallowed? --- TODO: Include support for ('Ns', 'Ni', 'd', 'Tr', 'illum') --- -data MTLToken = Ambient Float Float Float (Maybe Float) | -- Ka - Diffuse Float Float Float (Maybe Float) | -- Kd - Specular Float Float Float (Maybe Float) | -- Ks +-- TODO | - Is the alpha channel optional, ignored, disallowed? +-- - Include support for ('Ns', 'Ni', 'd', 'Tr', 'illum') +-- - Assume no colours have an alpha channel, since transparency is handled by the 'd' attribute (?) +data MTLToken f s = Ambient (Colour f) | -- Ka + Diffuse (Colour f) | -- Kd + Specular (Colour f) | -- Ks - MapDiffuse String | -- map_Kd - NewMaterial String -- newmtl - deriving (Eq, Show) + SpecularExponent f | -- Ns (TODO: Find out exactly what this entails) + Illum Illumination | -- illum (TODO: Find out what this means) --- | --- TODO: Rename (?) -data MTLNoParse = MTLComment String | MTLEmpty | MTLNoSuchAttribute String | MTLNoParse String deriving (Show) + Dissolve f | -- d (Dissolve; transparency) + Refraction f | -- Ni (Index of refraction; optical_density) + MapDiffuse s | -- map_Kd + MapAmbient s | -- map_Ka + NewMaterial s -- newmtl + deriving (Show, Eq) --- | Output type of the single-row MTL parser. -type MTLRow = (Int, Either MTLNoParse MTLToken, String) +-- | +-- 0. Color on and Ambient off +-- 1. Color on and Ambient on +-- 2. Highlight on +-- 3. Reflection on and Ray trace on +-- 4. Transparency: Glass on, Reflection: Ray trace on +-- 5. Reflection: Fresnel on and Ray trace on +-- 6. Transparency: Refraction on, Reflection: Fresnel off and Ray trace on +-- 7. Transparency: Refraction on, Reflection: Fresnel on and Ray trace on +-- 8. Reflection on and Ray trace off +-- 9. Transparency: Glass on, Reflection: Ray trace off +-- 10. Casts shadows onto invisible surfaces +type Illumination = Int + -- | Output type of the MTL parser. Currently a list of line number and token (or error string) pairs --- --- TODO: Add type for processed MTL (eg. a map between names and materials) --- -type MTL = [MTLRow] -- (line number, MTL token, comment) +-- TODO | - Add type for processed MTL (eg. a map between names and materials) +type MTL f s m = m (MTLToken f s) -- (line number, MTL token, comment) -- | -type MTLTable = Map.Map String (Map.Map String Material) +type MTLTable f s = M.Map s (M.Map s (Material f s)) -- Model ----------------------------------------------------------------------------------------------------------------------------------- --- | --- data Attributes = Attributes { - -- vertexdata :: Vertices - -- texcoorddata :: TexCoords --- } - --- newtype Vertices = Vertices [Vector Float] --- newtype TexCoords = TexCoords [Maybe (Point Float)] --- newtype Normals = Normals [Maybe (Vector Float)] --- newtype Materials = Materials [Material] - -type Vertices = [Vector Float] -type TexCoords = [Maybe (Point Float)] -type Normals = [Maybe (Vector Float)] -type Materials = [Material] - --- General types --------------------------------------------------------------------------------------------------------------------------- - -type Vector num = (num, num, num) -- Queen Vectoria -type Point num = (num, num) -- Haskell is no longer Point-free +type Vertices f m = m (V3 f) +type TexCoords f m = m (Maybe (V2 f)) +type Normals f m = m (Maybe (V3 f)) +type Materials f s m = m (Material f s) -- API types ------------------------------------------------------------------------------------------------------------------------------- -- | --- TODO: Validation (eg. length ivertices == length == ivertices == length itextures if length isn't 0) --- TOOD: Pack indices in a tuple (eg. indices :: [(Int, Int, Int)]) (?) --- TOOD: Use (String, String) for the names of the mtl file and material instead of Material (?) --- TODO: Use types so as not to confuse the indices (eg. newtype INormal, newtype ITexcoord) -data Face = Face { indices :: [(Int, Maybe Int, Maybe Int)], material :: Material } deriving (Show) +-- TODO | - Validation (eg. length ivertices == length == ivertices == length itextures if length isn't 0) +-- - Pack indices in a tuple (eg. indices :: [(Int, Int, Int)]) (?) +-- - Use (String, String) for the names of the mtl file and material instead of Material (?) +-- - Use types so as not to confuse the indices (eg. newtype INormal, newtype ITexcoord) +data Face f s i m = Face { + fIndices :: m (VertexIndices i), + fMaterial :: Material f s +} --deriving (Show, Eq) -- | -type Colour = (Float, Float, Float, Float) +-- TODO | - Use a type from the colour package instead (?) +data Colour f = Colour { + fRed :: f, + fGreen :: f, + fBlue :: f, + fAlpha :: f +} deriving (Show, Eq, Functor, Foldable) -- | --- TODO: Do all materials have an ambient, a diffuse and a specular colour (?) --- TODO: Support more attributes (entire spec) (?) --- TODO: Lenses (?) -data Material = Material { ambient :: Colour, diffuse :: Colour, specular :: Colour, texture :: Maybe String } deriving (Show) +-- TODO | - Do all materials have an ambient, a diffuse and a specular colour (?) +-- - Support more attributes (entire spec) (?) +-- - Lenses (?) +data Material f s = Material { + fAmbient :: Colour f, + fDiffuse :: Colour f, + fSpecular :: Colour f, + fTexture :: Maybe s +} deriving (Show, Eq) -- | Abstract representation of an OBJ model with associated MTL definitions. -- --- TODO: Rename (?) --- TODO: Include metadata, comments, rejected data (?) --- TODO: Separate type for processed OBJTokens (ie. token + context) --- TODO: Perform index lookups (?) --- TODO: Reconsider the types (especially of the materials) --- TODO: Rename accessor functions (eg. texcoords instead of textures) (?) +-- TODO | - Rename (?) +-- - Include metadata, comments, rejected data (?) +-- - Separate type for processed OBJTokens (ie. token + context) +-- - Perform index lookups (?) +-- - Reconsider the types (especially of the materials) +-- - Rename accessor functions (eg. texcoords instead of textures) (?) -- -data Model = Model { vertices :: [Vector Float], - normals :: [Vector Float], - texcoords :: [Point Float], - faces :: [Face], - materials :: MTLTable, -- TODO: Type synonym (?) - groups :: Map.Map [String] (Int, Int), -- TODO: Type synonym - objects :: Map.Map [String] (Int, Int) -- TODO: Type synonym - } deriving (Show) +-- fTextures :: Set s, +-- data Model f s i m = Model { +data Model f s i m = Model { + fVertices :: m (V3 f), + fNormals :: m (V3 f), + fTexcoords :: m (V2 f), + fFaces :: m (Face f s i m), + fMaterials :: MTLTable f s, -- TODO: Type synonym (?) + fGroups :: M.Map (Set s) (i, i), -- TODO: Type synonym + fObjects :: M.Map (Set s) (i, i), -- TODO: Type synonym + fRoot :: Maybe FilePath -- This is where we should look for related assets +} -- deriving (Show, Eq) --- Foreign --------------------------------------------------------------------------------------------------------------------------------- +-- Monomorphic defaults -------------------------------------------------------------------------------------------------------------------- --- | -newtype COBJ = COBJ OBJ +-- Instances ------------------------------------------------------------------------------------------------------------------------------- --- | -newtype CMTL = CMTL MTL +-- TODO: Use Show1, Eq1, etc. (?) +-- deriving instance (Show1 m) => Show1 (m a) +-- deriving instance (Show1 m) => Show1 (m a) +-- deriving instance (Show1 m) => Show1 (m a) +-- TODO: Clean this up --- | We -instance Storable COBJ where - sizeOf = const 0 - alignment = const 0 - peek _ = error "Work in progress" - poke _ = error "Work in progress" +-- showsPrec1 :: (Show1 f, Show a) => Int -> f a -> ShowS +deriving instance (Show1 m, + Show (m f), + Show (m (V2 f)), + Show (m (V3 f)), + Show (m (Face f s i m)), + Show (m s), + Show f, + Show s, + Show i) => Show (Model f s i m) -- where showsPrec = showsPrec1 +deriving instance (Show1 m, + Show (m f), + Show (m (VertexIndices i)), + Show (m (V3 f)), + Show (m s), + Show f, + Show s, + Show i) => Show (Face f s i m) -- where showsPrec = _ --- | We -instance Storable CMTL where - sizeOf = const 0 - alignment = const 0 - peek _ = error "Work in progress" - poke _ = error "Work in progress" +deriving instance (Show1 m, + Show (m f), + Show (m (VertexIndices i)), + Show (m (V3 f)), + Show (m s), + Show f, + Show s, + Show i) => Show (OBJToken f s i m) -- where showsPrec = _
− src/Graphics/WaveFront/Utilities.hs
@@ -1,116 +0,0 @@--- | --- Module : Graphics.WaveFront.Utilities --- Description : Parsing utilities --- Copyright : (c) Jonatan H Sundqvist, 2015 --- License : MIT --- Maintainer : Jonatan H Sundqvist --- Stability : experimental|stable --- Portability : POSIX (not sure) --- - --- Created July 15 2015 - --- TODO | - --- - - --- SPEC | - --- - - - - --------------------------------------------------------------------------------------------------------------------------------------------- --- API --------------------------------------------------------------------------------------------------------------------------------------------- -module Graphics.WaveFront.Utilities where - - - --------------------------------------------------------------------------------------------------------------------------------------------- --- We'll need these --------------------------------------------------------------------------------------------------------------------------------------------- -import Data.List (isPrefixOf, unfoldr) -import Data.Char (isSpace) -import Text.Read (readEither) - - - --------------------------------------------------------------------------------------------------------------------------------------------- --- Functions --------------------------------------------------------------------------------------------------------------------------------------------- - --- Parsing utilities ----------------------------------------------------------------------------------------------------------------------- - --- | This strikes me as overly convoluted. Should probably be refactored or removed entirely. -parseTokenWith :: String -> (String -> Either e token) -> Int -> (Int, Either e token, String) -parseTokenWith line parse = withoutComment line parse - - --- | Passes the input line (stripped of any trailing comments) to the specified parser -withoutComment :: String -> (String -> a) -> Int -> (Int, a, String) -withoutComment (l:ine) parse lnum = let (l:tokens, comment) = span (/= '#') ine in (lnum, parse tokens, comment) - - --- | Predicate for determining if a single line of text is a comment. --- Comments are preceded by a '#' and any number of whitespace characters. --- --- TODO: Drop comments at the end of a line (?) --- TODO: Add stripComment (or extractComment) which consumes a line up until the first '#'. --- This would allow for tokens and comments to appear on the same line. -isComment :: String -> Bool -isComment = isPrefixOf "#" . dropWhile isSpace - - --- | Strips a trailing comment from an MTL or OBJ line. -dropComment :: String -> String -dropComment = takeWhile (/= '#') - - --- | -takeComment :: String -> String -takeComment = dropWhile (/= '#') - - --- | -enumerate :: [(Int -> (Int, token, comment))] -> [(Int, token, comment)] -enumerate = zipWith (flip ($)) [1..] - - --- | Splits a string into rows and filters out unimportant elements (empty lines and comments) --- NOTE: This function is probably obsolete due to comments being included by the parsers --- TODO: Higher order function for composing predicates -rows :: String -> [String] -rows = filter (not . satisfiesAny [null, isComment]) . lines - where - satisfiesAny :: [a -> Bool] -> a -> Bool - satisfiesAny ps x = any ($ x) ps - - --- | --- TODO: Use readMaybe (?) --- TODO: Variadic 'unpacking' (or is that sinful?) --- TODO: More informative error message (?) --- TODO: Rename (?) --- TODO: Generic function for mapping and sequencing readEither (cf. "vt" case in parseOBJRow) (?) -vector :: Read r => ([r] -> b) -> [String] -> e -> Either e b -vector f coords e = either (Left . const e) (Right) (sequence (map readEither coords) >>= Right . f) --- vector _ _ = Left $ "Wrong number of coordinates for vector" - --- From Southpaw --------------------------------------------------------------------------------------------------------------------------- - --- | -splitWith :: Eq a => ([a] -> ([a], [a])) -> [a] -> [[a]] -splitWith f = unfoldr cut - where cut [] = Nothing - cut xs = Just $ f xs - - --- | Same as Python's str.split (I think) (eg. split '|' "a||c" == ["a", "", "c"]) --- TODO: Rename --- TODO: Easier to implement with groupBy (?) -cuts :: Eq a => a -> [a] -> [[a]] -cuts c = splitWith $ \ xs -> let (token, rest) = span (/=c) xs in (token, drop 1 rest) - - --- | Combine every adjacent pair in the list with the given function -pairwise :: (a -> a -> b) -> [a] -> [b] -pairwise f xs = zipWith f xs (drop 1 xs)