Skip to content

Commit 8b9ec62

Browse files
authored
Merge pull request #23 from alt-romes/wip/romes/teapot
Add Utah Teapot demo and object loading
2 parents 426e996 + 38de01f commit 8b9ec62

File tree

8 files changed

+239
-35
lines changed

8 files changed

+239
-35
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ The working examples are the following. In sequence, they build up to a
1212
Cooler examples:
1313

1414
* `planets-core` implements Sebastian Lague's procedural planets
15+
* `teapot-obj` renders the Utah teapot using Blinn-Phong lighting and a perspective camera
1516

1617
Solid examples:
1718

examples/planets-core/Planet.hs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -82,12 +82,12 @@ newPlanetMesh rp Planet{..} = enterD "newPlanetMesh" $ Linear.do
8282

8383
(planetPs, elevations)
8484
= V.unzip $ V.map (\(p :&: _) -> pointOnPlanet planetShape p) (V.convert us)
85-
planetNs = computeNormals (SV.map fromIntegral is) (V.convert planetPs)
86-
planetVs = SV.zipWith (:&:) (V.convert planetPs) planetNs
85+
planetNs = computeNormals (SV.map fromIntegral is) planetPs
86+
planetVs = V.zipWith (:&:) (planetPs) planetNs
8787

8888
minmax = MinMax (P.minimum elevations) (P.maximum elevations)
8989

90-
in (, Ur minmax) <$> (createMeshWithIxsSV rp (DynamicBinding (Ur mempty) :## GHNil) planetVs is )
90+
in (, Ur minmax) <$> (createMeshWithIxsSV rp (DynamicBinding (Ur mempty) :## GHNil) (V.convert planetVs) is )
9191

9292
--------------------------------------------------------------------------------
9393
-- * Material

examples/teapot-obj/Main.hs

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
{-# LANGUAGE PartialTypeSignatures #-}
2+
module Main where
3+
4+
import Ghengin.Core.Prelude as Linear
5+
import qualified Prelude
6+
7+
import Geomancy
8+
import Geomancy.Transform
9+
import Ghengin.Core
10+
import Ghengin.Core.Shader.Data
11+
import Ghengin.Core.Mesh
12+
import Ghengin.Core.Material
13+
import Ghengin.Core.Render
14+
import Ghengin.Core.Render.Property
15+
import Ghengin.Core.Render.Pipeline
16+
import Ghengin.Core.Render.Queue
17+
18+
import qualified FIR
19+
import qualified Math.Linear as FIR
20+
21+
import qualified Data.Monoid.Linear as LMon
22+
import qualified Data.Linear.Alias as Alias
23+
24+
-- ghengin
25+
import Ghengin.Camera
26+
import Ghengin.Geometry.Obj
27+
28+
import Shaders
29+
30+
main :: Prelude.IO ()
31+
main = do
32+
withLinearIO $
33+
runCore (640, 480) Linear.do
34+
35+
(rp1, rp2) <- (Alias.share =<< createSimpleRenderPass )
36+
37+
pipeline <- (makeRenderPipelineWith defaultGraphicsPipelineSettings{cullMode=CullNone} rp1 shaderPipeline (StaticBinding (Ur camera) :## GHNil) )
38+
(emptyMat, pipeline) <- (material GHNil pipeline )
39+
(mesh, pipeline) <- (loadObjMesh "examples/teapot-obj/assets/teapot.obj" pipeline
40+
(DynamicBinding (Ur (scale 1.2)) :## GHNil) )
41+
42+
let !(rq, Ur pkey) = insertPipeline pipeline LMon.mempty
43+
(rq, Ur mkey) <- pure (insertMaterial pkey emptyMat rq)
44+
(rq, Ur mshkey) <- pure (insertMesh mkey mesh rq)
45+
46+
rq <- gameLoop mshkey rp2 rq
47+
48+
(freeRenderQueue rq )
49+
50+
return (Ur ())
51+
52+
gameLoop :: MeshKey _ _ _ _ '[Transform] -- ^ rq key to cube mesh
53+
-> Alias RenderPass
54+
RenderQueue ()
55+
Core (RenderQueue ())
56+
gameLoop mkey rp rq = Linear.do
57+
should_close <- (shouldCloseWindow )
58+
if should_close then (Alias.forget rp ) >> return rq else Linear.do
59+
(pollWindowEvents )
60+
61+
(rp, rq) <- render rp rq
62+
rq <- (editMeshes mkey rq (traverse' $ propertyAt @0 (\(Ur tr) -> pure $ Ur $
63+
rotateY 0.01 <> tr)) )
64+
65+
gameLoop mkey rp rq
66+
67+
camera :: Camera "view_matrix" "proj_matrix"
68+
camera = cameraLookAt (vec3 0 (-5) (-5){- move camera up and back-}) (vec3 0 0 0) (1280, 720)
69+
70+
-- non-compositional instance for "Transform", just for demo
71+
instance ShaderData Transform where
72+
type FirType Transform = FIR.Struct '[ "m" 'FIR.:-> FIR.M 4 4 Float ]

examples/teapot-obj/Shaders.hs

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
{-# OPTIONS_GHC -Wno-missing-local-signatures #-}
2+
3+
{-# LANGUAGE CPP #-}
4+
{-# LANGUAGE BlockArguments #-}
5+
{-# LANGUAGE DataKinds #-}
6+
{-# LANGUAGE FlexibleContexts #-}
7+
{-# LANGUAGE PartialTypeSignatures #-}
8+
{-# LANGUAGE OverloadedLabels #-}
9+
{-# LANGUAGE RebindableSyntax #-}
10+
{-# LANGUAGE ScopedTypeVariables #-}
11+
{-# LANGUAGE TypeApplications #-}
12+
{-# LANGUAGE NamedWildCards #-}
13+
{-# LANGUAGE TypeFamilies #-}
14+
{-# LANGUAGE TypeOperators #-}
15+
{-# LANGUAGE ViewPatterns #-}
16+
17+
module Shaders where
18+
19+
-- fir
20+
import FIR
21+
import FIR.Syntax.Labels
22+
import Math.Linear
23+
24+
-- ghengin
25+
import qualified Ghengin.Core.Shader as G
26+
import Ghengin.Camera.Shader.Lighting
27+
28+
-- See FIR.Validation.Layout about locations/component layouts...
29+
type VertexInput
30+
= '[ Slot 0 0 ':-> V 3 Float
31+
, Slot 1 0 ':-> V 3 Float ]
32+
33+
shaderPipeline :: G.ShaderPipeline _
34+
shaderPipeline
35+
= G.ShaderPipeline (StructInput @VertexInput @(Triangle List))
36+
G.:>-> vertex
37+
G.:>-> fragment
38+
39+
type VertexDefs =
40+
'[ "in_position" ':-> Input '[ Location 0 ] (V 3 Float)
41+
, "in_normal" ':-> Input '[ Location 1 ] (V 3 Float)
42+
, "out_position" ':-> Output '[ Location 0 ] (V 4 Float)
43+
, "out_normal" ':-> Output '[ Location 1 ] (V 4 Float)
44+
45+
-- Descriptor Set 0 : Pipeline properties (bound once per pipeline)
46+
, "camera" ':-> Uniform '[ DescriptorSet 0, Binding 0 ]
47+
(Struct [ "view_matrix" ':-> M 4 4 Float
48+
, "proj_matrix" ':-> M 4 4 Float
49+
])
50+
-- Descriptor Set 1 : Material properties (bound once per material)
51+
-- ...
52+
53+
-- Descriptor Set 2 : Mesh properties (bound once per mesh)
54+
, "model" ':-> Uniform '[ DescriptorSet 2, Binding 0 ] (Struct '[ "m" ':-> M 4 4 Float ])
55+
]
56+
57+
58+
vertex :: G.VertexShaderModule VertexDefs _
59+
vertex = shader do
60+
~(Vec3 x y z) <- #in_position
61+
~(Vec3 nx ny nz) <- #in_normal
62+
proj_matrix <- use @(Name "camera" :.: Name "proj_matrix")
63+
view_matrix <- use @(Name "camera" :.: Name "view_matrix")
64+
model_mat <- use @(Name "model" :.: Name "m")
65+
let view_p = (view_matrix !*! model_mat) !*^ (Vec4 x y z 1)
66+
#gl_Position .= proj_matrix !*^ view_p
67+
#out_position .= view_p
68+
#out_normal .= (view_matrix !*! model_mat) !*^ (Vec4 nx ny nz 0) -- careful! don't translate (hence the w=0)
69+
70+
fragment :: G.FragmentShaderModule
71+
'[ "in_position" ':-> Input '[ Location 0 ] (V 4 Float) -- in view space
72+
, "in_normal" ':-> Input '[ Location 1 ] (V 4 Float) -- in view space
73+
] _
74+
fragment = shader do
75+
let lightDir = Vec3 0 (-1) 0
76+
let lightCol = Vec3 0.3 0.3 0.3
77+
let objectCol = Vec3 1 1 1
78+
let shininess = 16
79+
80+
lightValue <- blinnPhong 0.05 shininess lightDir lightCol
81+
82+
let Vec3 colx coly colz
83+
= gammaCorrection defaultGamma (lightValue `pointwiseMult` objectCol)
84+
85+
#out_colour .= Vec4 colx coly colz 1
86+
File renamed without changes.

ghengin/ghengin-geometry/Ghengin/Geometry/Normals.hs

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
{-# LANGUAGE CPP #-}
12
{-# LANGUAGE NoImplicitPrelude #-}
23
-- | Module with utilities for computing normals of a Mesh.
34
--
@@ -18,7 +19,7 @@ import qualified Geomancy.Vec3 as Vec3
1819

1920
import Prelude.Linear
2021
import qualified Data.Vector as V
21-
import qualified Data.Vector.Storable as SV
22+
import qualified Data.Vector.Generic as GV
2223
import qualified Data.Array.Mutable.Linear as Array
2324

2425
{- | Compute the smooth normal vectors of a mesh surface.
@@ -58,12 +59,14 @@ have all allocated buffers automatically initialized to zero. In that case, if
5859
this is the first and only normalization for a given mesh, you may skip the
5960
first loop on the function too of course.
6061
-}
61-
computeNormals :: SV.Vector Int -- ^ Every three indices into the vertices array forms a face
62-
-> SV.Vector Vec3 -- ^ The position of every vertex
63-
-> SV.Vector Vec3 -- ^ The normal vector for each vertex
62+
{-# INLINEABLE computeNormals #-}
63+
computeNormals :: (GV.Vector v Int, GV.Vector w Vec3)
64+
=> v Int -- ^ Every three indices into the vertices array forms a face
65+
-> w Vec3 -- ^ The position of every vertex
66+
-> V.Vector Vec3 -- ^ The normal vector for each vertex
6467
computeNormals ixs vs =
65-
V.convert $ unur $ -- todo: freeze variant which constructs SV.Vector?
66-
Array.alloc (SV.length vs) (vec3 0 0 0) $ \arr0 ->
68+
unur $ -- todo: freeze variant which constructs SV.Vector?
69+
Array.alloc (GV.length vs) (vec3 0 0 0) $ \arr0 ->
6770
Array.freeze $
6871
Array.map Vec3.normalize $
6972
foldl' (\arr (Ur i) -> let
@@ -76,14 +79,23 @@ computeNormals ixs vs =
7679
in arr & ia += no
7780
& ib += no
7881
& ic += no
79-
) arr0 [Ur i | i <- [0,3..SV.length ixs - 1]]
82+
) arr0 [Ur i | i <- [0,3..GV.length ixs - 4]]
8083
where
81-
(!) :: SV.Storable x => SV.Vector x -> Int -> x
82-
(!) = SV.unsafeIndex
84+
(!) :: GV.Vector t x => t x -> Int -> x
85+
#ifdef DEBUG
86+
(!) = (GV.!)
87+
#else
88+
(!) = GV.unsafeIndex
89+
#endif
8390

8491
(+=) :: Int -> Vec3 -> Array.Array Vec3 %1 -> Array.Array Vec3
92+
#ifdef DEBUG
93+
(+=) i new arr0 = case Array.get i arr0 of
94+
(Ur exists, arr1) -> Array.set i (new ^+^ exists) arr1
95+
#else
8596
(+=) i new arr0 = case Array.unsafeGet i arr0 of
8697
(Ur exists, arr1) -> Array.unsafeSet i (new ^+^ exists) arr1
98+
#endif
8799

88100
-- TODO: Try backpermute
89101

ghengin/ghengin-geometry/Ghengin/Geometry/Obj.hs

Lines changed: 54 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -4,28 +4,37 @@
44
{-# LANGUAGE OverloadedRecordDot #-}
55
module Ghengin.Geometry.Obj where
66

7-
import Prelude
8-
import qualified Prelude.Linear as Linear
7+
import Prelude hiding (log)
98
import Control.Monad
9+
import qualified Prelude.Linear as Linear
1010

1111
import qualified Data.Vector as V
12+
import qualified Data.Vector.Storable as SV
1213

1314
import Geomancy
1415

1516
import Codec.Wavefront
1617

18+
import Ghengin.Core.Log
1719
import Ghengin.Core.Type.Compatible
18-
import Ghengin.Core.Prelude (GHList(..))
1920
import Ghengin.Core.Render.Pipeline
2021
import Ghengin.Core.Render.Property
2122
import Ghengin.Core.Renderer
2223
import Ghengin.Core.Mesh
23-
import Ghengin.Core.Mesh.Vertex
2424
import qualified Data.Linear.Alias as Alias
2525

2626
import qualified Control.Functor.Linear as Linear
2727
import qualified Control.Monad.IO.Class.Linear as Linear
2828

29+
import Ghengin.Geometry.Normals
30+
31+
-- | Loads a 'WavefrontOBJ' from a file.
32+
--
33+
-- Useful if one desires to embed the model with template haskell
34+
loadObjFile :: FilePath -> IO (Either String WavefrontOBJ)
35+
loadObjFile = fromFile
36+
37+
-- | Load an .obj file and create a mesh from it.
2938
loadObjMesh :: (CompatibleMesh ts π, CompatibleVertex '[Vec3, Vec3] π)
3039
=> FilePath
3140
-> RenderPipeline π ps
@@ -34,27 +43,50 @@ loadObjMesh :: (CompatibleMesh ts π, CompatibleVertex '[Vec3, Vec3] π)
3443
-- The property bindings need to set when creating it.
3544
Renderer (Mesh '[Vec3, Vec3] ts, RenderPipeline π ps)
3645
loadObjMesh filepath rp props = Linear.do
37-
Linear.liftSystemIOU (fromFile filepath) Linear.>>= \case
46+
Linear.liftSystemIOU (loadObjFile filepath) Linear.>>= \case
3847
Linear.Ur (Left err) -> Linear.do
3948
destroyRenderPipeline rp
4049
Alias.forget props
4150
Linear.liftSystemIO (fail err)
4251
Linear.Ur (Right wavefrontObj) ->
43-
let
44-
locs = V.map getLoc $ wavefrontObj.objLocations
45-
normals = V.map getNormal $ wavefrontObj.objNormals
46-
faces = V.map (.elValue) wavefrontObj.objFaces
47-
48-
getLoc :: Location -> Vec3
49-
getLoc (Location x y z _) = vec3 x (-y) z
50-
51-
getNormal :: Normal -> Vec3
52-
getNormal (Normal x y z) = vec3 x y z
53-
54-
vertices = V.zipWith (:&:) locs normals
55-
ixs = V.concatMap (\(Face a b c d) -> V.fromList $
56-
[ a.faceLocIndex , b.faceLocIndex , c.faceLocIndex ] ++ map (.faceLocIndex) d
57-
) faces
58-
in
59-
createMeshWithIxsSV rp props (V.convert vertices) (V.convert $ V.map fromIntegral ixs)
52+
createObjMesh wavefrontObj rp props
53+
54+
-- | Create a mesh from a 'WavefrontOBJ'
55+
createObjMesh :: (CompatibleMesh ts π, CompatibleVertex '[Vec3, Vec3] π)
56+
=> WavefrontOBJ
57+
-> RenderPipeline π ps
58+
PropertyBindings ts
59+
-- ^ These must NOT be added after creating the mesh.
60+
-- The property bindings need to set when creating it.
61+
Renderer (Mesh '[Vec3, Vec3] ts, RenderPipeline π ps)
62+
createObjMesh wavefrontObj rp props =
63+
let
64+
locs = V.map getLoc $ wavefrontObj.objLocations
65+
normals = V.map getNormal $ wavefrontObj.objNormals
66+
faces = V.map (.elValue) wavefrontObj.objFaces
67+
68+
getLoc :: Location -> Vec3
69+
getLoc (Location x y z _) = vec3 x (-y) z
70+
71+
-- get normal, currently ignoring face
72+
getNormal :: Normal -> Vec3
73+
getNormal (Normal x y z) = vec3 x y z
74+
75+
vertices
76+
| V.length normals == 0
77+
|| V.length locs /= V.length normals {- bad object file! -}
78+
= V.zipWith (:&:) locs (computeNormals ixs locs)
79+
| otherwise
80+
= V.zipWith (:&:) locs normals
81+
82+
ixs = V.concatMap (\(Face a b c _) -> V.fromList $ map (\x -> x-1) {-face indices are 1-indexed-}
83+
[ a.faceLocIndex , b.faceLocIndex , c.faceLocIndex ]
84+
) faces
85+
in Linear.do
86+
if (V.length normals > 0 && V.length locs /= V.length normals)
87+
then log $ toLogStr $ "createObjMesh: length of normals ("
88+
++ show (V.length normals) ++ ") is not the same as length of locations ("
89+
++ show (V.length locs) ++ ")! Ignoring and recomputing normals from scratch..."
90+
else Linear.pure ()
91+
createMeshWithIxsSV rp props (V.convert vertices) (SV.map fromIntegral $ V.convert ixs)
6092

ghengin/ghengin-geometry/Ghengin/Geometry/Sphere.hs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import Ghengin.Core.Renderer
1515
import Ghengin.Core.Render.Pipeline
1616
import Ghengin.Core.Mesh
1717

18+
import qualified Data.Vector as V
1819
import qualified Data.Vector.Storable as SV
1920

2021
import Ghengin.Geometry.Normals
@@ -69,7 +70,7 @@ newUnitSphere res =
6970
ps = SV.fromList $ v1 <> v2 <> v3 <> v4 <> v5 <> v6
7071
ns = computeNormals is ps
7172
in
72-
UnitSphere (SV.zipWith (\a b -> a :&: b) ps ns) (SV.map fromIntegral is)
73+
UnitSphere (SV.zipWith (\a b -> a :&: b) ps (V.convert ns)) (SV.map fromIntegral is)
7374

7475
newSphereMesh :: (CompatibleMesh '[] π, CompatibleVertex [Vec3, Vec3] π)
7576
=> RenderPipeline π bs

0 commit comments

Comments
 (0)