summaryrefslogtreecommitdiff
path: root/src/Graphics/WaveFront/Model.hs
blob: 96172a8aa3fdc6f92237f230dbf2b09ac4adf678 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
-- |
-- 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
--        - Decide on API
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 (V2(..), 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 above docstring...
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))


-- |
-- TODO: Factor out per-vertex logic so we don't have to redefine this function entirely for each colour type
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'

-- TODO | - Do not create intermediate vectors (automatic fusion?)
--        - Allow fallback values (or function), or use Either
--        - Add docstrings

-- |
unindexedVertices :: Model f Text Int Vector -> Maybe (Vector (V3 f))
unindexedVertices model = sequence $ fromFaceIndices (model^.vertices) (index) (^.ivertex) (model^.faces)
  where
    index coords i = coords !? (i-1)

unindexedNormals :: Model f Text Int Vector -> Maybe (Vector (V3 f))
unindexedNormals model = sequence $ fromFaceIndices (model^.normals) (index) (^.inormal) (model^.faces)
  where
    index coords mi = mi >>= \i -> coords !? (i-1)

unindexedTexcoords :: Model f Text Int Vector -> Maybe (Vector (V2 f))
unindexedTexcoords model = sequence $ fromFaceIndices (model^.texcoords) (index) (^.itexcoord) (model^.faces)
  where
    index coords mi = mi >>= \i -> coords !? (i-1)

-- Model queries ---------------------------------------------------------------------------------------------------------------------------

-- TODO: Turn into Lenses/Getters/Isos (?)

-- | 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)