-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathgetting-started.pandoc
255 lines (190 loc) · 9.91 KB
/
getting-started.pandoc
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
<div style="float:left;width:100%;">
<a href='http://lambdacube3d.com'>
<img src='/lambdacube-logo.svg' width="14%" style="float:left;margin: 0 3% 4% 0"/>
</a>
LambdaCube 3D
=============
<a href='http://lambdacube3d.com'>lambdacube3d.com</a>
</div>
Getting Started
===============
If you want to understand the governing principles behind LambdaCube 3D, you should consult the [Conceptual Overview](overview).
If you just want to see something running right away, we recommend checking out the [online editor](http://lambdacube3d.com/editor.html).
If you want to develop a Haskell application using LambdaCube 3D, just read on.
Hello world in Haskell
----------------------
LambdaCube is available on Hackage, which should make it easy for Haskell developers to get things rolling. For the purpose of this guide we assume that you already have a working Haskell environment (GHC with `cabal-install`) set up. It is best to start by installing the [`lambdacube-gl`](https://github.com/lambdacube3d/lambdacube-gl) package, which provides the Haskell OpenGL backend for LambdaCube 3D.
~~~~~
$ cabal install lambdacube-gl
~~~~~
Note that this step also installs [`lambdacube-ir`](https://github.com/lambdacube3d/lambdacube-ir), which is the core library (IR stands for "intermediate representation"). This library provides the machinery to load compiled pipeline descriptions -- a JSON based format -- for use in the backend.
The `lambdacube-gl` package contains a Hello World example to demonstrate the basic usage of the GL backend. To gain access to the example, we need to unpack the backend package:
~~~~~
$ cabal unpack lambdacube-gl
$ cd lambdacube-gl-[VERSION]/examples
~~~~~
Afterwards, we can run the example that executes the precompiled pipeline (`hello.json`):
~~~~~
$ cabal install GLFW-b
$ ghc --make Hello
$ ./Hello
~~~~~
![](hello-screenshot.png)
In order to turn LambdaCube 3D source files into JSON descriptions, we will also need [`lambdacube-compiler`](https://github.com/lambdacube3d/lambdacube-compiler):
~~~~~
$ cabal install lambdacube-compiler
~~~~~
The compiler package installs an executable called `lc` and a library that exposes the same functionality. With the compiler present, we can produce the JSON description from the `hello.lc` source file also included in the example:
~~~~~
$ lc hello.lc
~~~~~
When developing a LambdaCube 3D application, the recommended way of dealing with `*.lc` files is to invoke the compiler as an external tool (which would be bundled with the application upon release), and load the resulting JSON pipeline description using the backend API.
As an alternative, it is also possible to access the compiler functionality through the library. This option is demonstrated in the second variant of the example, which directly reads the `hello.lc` pipeline source:
~~~~~
$ ghc --make HelloEmbedded
$ ./HelloEmbedded
~~~~~
Let's have a closer look at the first Hello World variant. Below follows the full source of the example.
`Hello.hs`
----------
First we get the imports out of the way.
~~~~~ {.haskell}
{-# LANGUAGE PackageImports, LambdaCase, OverloadedStrings #-}
import "GLFW-b" Graphics.UI.GLFW as GLFW
import qualified Data.Map as Map
import qualified Data.Vector as V
import LambdaCube.GL as LambdaCubeGL -- renderer
import LambdaCube.GL.Mesh as LambdaCubeGL
import Codec.Picture as Juicy
import Data.Aeson
import qualified Data.ByteString as SB
~~~~~
Then we start with the entry point of the program.
~~~~~ {.haskell}
main :: IO ()
main = do
~~~~~
As a first step, we initialise the window using GLFW. This has to be done before accessing any GPU functionality.
~~~~~ {.haskell}
win <- initWindow "LambdaCube 3D DSL Hello World" 640 640
~~~~~
Next, we define the shape of the input to the rendering pipeline as required by the backend. The schema is a pure data structure that lists the names and types of primitive streams (`defObjectArray`) and uniforms (`defUniforms`) that the pipeline can process. The names and types have to agree with those in the pipeline description (see [`hello.lc`] below).
For the curious, the schema is a writer monad at heart, and `do` notation is only used for convenience to easily merge different schema fragments.
~~~~~ {.haskell}
let inputSchema = makeSchema $ do
defObjectArray "objects" Triangles $ do
"position" @: Attribute_V2F
"uv" @: Attribute_V2F
defUniforms $ do
"time" @: Float
"diffuseTexture" @: FTexture2D
~~~~~
Given the schema, we create a container that will hold the input to the pipeline at any given time. If we think of the pipeline as a pure function, this is the place that stores the argument for the next application.
~~~~~ {.haskell}
storage <- LambdaCubeGL.allocStorage inputSchema
~~~~~
Now that we have the storage, we can populate it with the pipeline input. We have two triangles that form a square when put together (just to demonstrate the use of multiple meshes), and a texture.
~~~~~ {.haskell}
LambdaCubeGL.uploadMeshToGPU triangleA >>= LambdaCubeGL.addMeshToObjectArray storage "objects" []
LambdaCubeGL.uploadMeshToGPU triangleB >>= LambdaCubeGL.addMeshToObjectArray storage "objects" []
Right img <- Juicy.readImage "logo.png"
textureData <- LambdaCubeGL.uploadTexture2DToGPU img
~~~~~
At this point we can load the pipeline itself. Note that the input data is independent of the pipeline itself, and the two can be freely swapped out separately.
~~~~~ {.haskell}
Just pipelineDesc <- decodeStrict <$> SB.readFile "hello.json"
renderer <- LambdaCubeGL.allocRenderer pipelineDesc
~~~~~
All the building blocks are in place, but we still need to connect them. The `setStorage` function associates the storage with the pipeline and checks that the two are compatible. If there is a schema mismatch, it returns the error message wrapped in `Just`, otherwise it returns `Nothing`. In the latter case we are ready to enter the rendering loop.
~~~~~ {.haskell}
LambdaCubeGL.setStorage renderer storage >>= \case
Just err -> putStrLn err
Nothing -> loop
where loop = do
~~~~~
In this example our main loop is directly implemented in the IO monad. First we tell the pipeline the current dimensions of the window so it can configure the viewport.
~~~~~ {.haskell}
(w, h) <- GLFW.getWindowSize win
LambdaCubeGL.setScreenSize storage (fromIntegral w) (fromIntegral h)
~~~~~
Afterwards, we update the input of the pipeline. In this case only uniforms need to be changed on every frame. Since the texture actually stays the same, we could have set `diffuseTexture` just once in the beginning before starting the loop.
~~~~~ {.haskell}
LambdaCubeGL.updateUniforms storage $ do
"diffuseTexture" @= return textureData
"time" @= do
Just t <- GLFW.getTime
return (realToFrac t :: Float)
~~~~~
Now that the input is properly set, we can render the frame. This is where we conceptually apply the pipeline function to the currently stored argument.
~~~~~ {.haskell}
LambdaCubeGL.renderFrame renderer
GLFW.swapBuffers win
~~~~~
Finally, we check whether Escape is pressed and leave the loop if it is.
~~~~~ {.haskell}
GLFW.pollEvents
let keyIsPressed k = fmap (==KeyState'Pressed) $ GLFW.getKey win k
escape <- keyIsPressed Key'Escape
if escape then return () else loop
~~~~~
The only thing left is the final cleanup.
~~~~~ {.haskell}
LambdaCubeGL.disposeRenderer renderer
LambdaCubeGL.disposeStorage storage
GLFW.destroyWindow win
GLFW.terminate
~~~~~
After the main function we define the input geometry: two separate triangles. Both meshes have a position and a UV attribute defined for each vertex.
~~~~~ {.haskell}
triangleA :: LambdaCubeGL.Mesh
triangleA = Mesh
{ mAttributes = Map.fromList
[ ("position", A_V2F $ V.fromList [V2 1 1, V2 1 (-1), V2 (-1) (-1)])
, ("uv", A_V2F $ V.fromList [V2 1 1, V2 0 1, V2 0 0])
]
, mPrimitive = P_Triangles
}
triangleB :: LambdaCubeGL.Mesh
triangleB = Mesh
{ mAttributes = Map.fromList
[ ("position", A_V2F $ V.fromList [V2 1 1, V2 (-1) (-1), V2 (-1) 1])
, ("uv", A_V2F $ V.fromList [V2 1 1, V2 0 0, V2 1 0])
]
, mPrimitive = P_Triangles
}
~~~~~
At the end we define the window initialising function, which is basically GLFW boilerplate.
~~~~~ {.haskell}
initWindow :: String -> Int -> Int -> IO Window
initWindow title width height = do
GLFW.init
GLFW.defaultWindowHints
mapM_ GLFW.windowHint
[ WindowHint'ContextVersionMajor 3
, WindowHint'ContextVersionMinor 3
, WindowHint'OpenGLProfile OpenGLProfile'Core
, WindowHint'OpenGLForwardCompat True
]
Just win <- GLFW.createWindow width height title Nothing Nothing
GLFW.makeContextCurrent $ Just win
return win
~~~~~
`hello.lc`
----------
The hello pipeline starts with a dark blue background, and renders 2D triangle meshes by rotating them around the Z axis and applying a texture. For details on the building blocks of the pipeline you can refer to the [Conceptual Overview](overview).
~~~~~ {.haskell}
makeFrame (time :: Float)
(texture :: Texture)
(prims :: PrimitiveStream Triangle (Vec 2 Float, Vec 2 Float))
= imageFrame ((emptyColorImage (V4 0 0 0.4 1)))
`overlay`
prims
& mapPrimitives (\(p,uv) -> (rotMatrixZ time *. (V4 p%x p%y (-1) 1), uv))
& rasterizePrimitives (TriangleCtx CullNone PolygonFill NoOffset LastVertex) ((Smooth))
& mapFragments (\((uv)) -> ((texture2D (Sampler PointFilter MirroredRepeat texture) uv)))
& accumulateWith ((ColorOp NoBlending (V4 True True True True)))
main = renderFrame $
makeFrame (Uniform "time")
(Texture2DSlot "diffuseTexture")
(fetch "objects" (Attribute "position", Attribute "uv"))
~~~~~