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