-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathoverview.pandoc
387 lines (270 loc) · 14.3 KB
/
overview.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
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
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
<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>
Conceptual Overview
===================
Basic terminology
-----------------
**Graphics engine** is the part of an application which is responsible for the GPU graphics.
LambdaCube 3D is a DSL for implementing graphics engines.
From a purely functional point of view, the graphics engine is a pure function.
We call this function the **pipeline**.
A **frame** is the output of the pipeline.
A frame is something which can be put on the screen.
**Rendering** is the computation of one frame given the pipeline and its input.
Pipelines can be assembled from smaller parts.
**Multi-pass rendering** is rendering with a pipeline which is a composition of several pipelines where
the output of a pipeline is part of the input of another pipeline. LambdaCube 3D supports multi-pass rendering.
A **dynamic pipeline** is a composed pipeline in which the composition depends on the pipeline input.
Currently LambdaCube 3D does not support dynamic pipelines. This limitation is planned to be lifted.
Until then a workaround is to use several pipelines and the application logic
chooses at every rendering the right pipeline depending on the input.
(If a predefined set of pipelines is not enough, it is possible to create pipelines on the fly.)
Pipeline inputs
---------------
There are limitations on possible pipeline inputs, which reflects the fact that GPUs are specialised hardware.
The current limitations are inherited from OpenGL 3.3.
The input of a pipeline consists of zero or more vertex streams, zero or more uniforms and zero or more textures.
A **uniform** is a piece of data with a statically known size.
Supported uniform types are `Bool`, `Int`, `Float`, `Vec 3 Bool`, `Mat 4 4 Float`, etc.
For example, a camera position can be part of the input as a 4 dimensional `Float` matrix uniform.
The terminology 'uniform' comes from the fact that uniforms are constant values during the rendering.
A **texture** is a uniform with type `Texture`.
A `Texture` is an image with sampling configurations (discussed later).
Currently textures are treated specially in LambdaCube 3D but this is planned to be changed.
A **stream** is a vector. In LambdaCube 3D there are two kind of streams: primitive streams and fragment streams.
A **primitive** is either a point, a line, or a triangle.
A **point** is a vertex.
A **line** is a pair of vertices.
A **triangle** is a triplet of vertices.
A **vertex** is a tuple of attributes.
An **attribute** is a piece of data with a statically known size.
Supported attribute types are for example `Float`, `Bool` or `Vec 3 Float`.
Typical attributes are the spatial position and the color of a vertex.
<div style="float:right">
![](simplex3d.png)
</div>
For example, the primitive stream of a tetrahedron with one position attribute looks something like
~~~~~ {.haskell}
Triangle (V3 0 0 0) (V3 1 0 0) (V3 0 1 0) -- 1st triangle
Triangle (V3 1 0 0) (V3 0 1 0) (V3 0 0 1) -- 2nd triangle
Triangle (V3 0 1 0) (V3 0 0 1) (V3 0 0 0) -- 3rd triangle
Triangle (V3 0 0 1) (V3 0 0 0) (V3 1 0 0) -- 4th triangle
~~~~~
The primitive stream of a tetrahedron with a position and an additional Boolean attribute looks something like
~~~~~ {.haskell}
Triangle (V3 0 0 0, True ) (V3 1 0 0, True ) (V3 0 1 0, True ) -- 1st triangle
Triangle (V3 1 0 0, True ) (V3 0 1 0, True ) (V3 0 0 1, False) -- 2nd triangle
Triangle (V3 0 1 0, False) (V3 0 0 1, True ) (V3 0 0 0, True ) -- 3rd triangle
Triangle (V3 0 0 1, False) (V3 0 0 0, True ) (V3 1 0 0, True ) -- 4th triangle
~~~~~
Note that the representation of streams and primitives are hidden. Usually the primitive stream is stored as a
flattened stream of vertices by the application.
~~~~~ {.haskell}
tetrahedron = map (\v -> V4 v%x v%y v%z 1)
[ V3 0 0 0, V3 1 0 0, V3 0 1 0
, V3 1 0 0, V3 0 1 0, V3 0 0 1
, V3 0 1 0, V3 0 0 1, V3 0 0 0
, V3 0 0 1, V3 0 0 0, V3 1 0 0
]
tetrahedronStream = fetchArrays @Triangle tetrahedron
~~~~~
Frames and images
-----------------
A **frame** consists of several images of the same dimension.
An **image** is a two dimensional array of values.
There are different kind of images depending on the role the image plays.
A **color image** is to store color values. Supported color values are `Float`, and `Float` vectors with dimension at most 4.
For example, a color image of `Float` values can be used to store a grayscale image, but the `Float` values can be interpreted in any
color space, or they can be used to a totally different purpose too.
The `emptyColorImage` function creates an empty color image given a color value:
~~~~~ {.haskell}
emptyColorImage navy
~~~~~
![](navy-color-image.png)
A **depth image** is to store depth values. A depth value has type `Float` and it is usually used to store the distance between the
viewpoint and the object shown on a color image.
In LambdaCube 3D, a depth image has different type than a color image with `Float` values because graphics hardware has dedicated
operations for depth images.
The `emptyDepthImage` function creates an empty depth image given a depth value:
~~~~~ {.haskell}
emptyDepthImage 1
~~~~~
A **stencil image** is to used for masking (discussed later).
### Creating frames from images
The `imageFrame` function creates a frame given a tuple of images:
~~~~~ {.haskell}
imageFrame (emptyDepthImage 1, emptyColorImage navy)
~~~~~
There are limitations about how many images a frame can contain.
A frame can contain at most one depth image and at most one stencil image.
Pipelines
---------
Ideally, a pipeline is an arbitrary computable function which produces a frame given an input.
This is not the case.
There are limitations on possible pipelines, which reflects the fact that GPUs are specialised hardware.
The current limitations are inherited from OpenGL 3.3.
Here is a step-by-step overview about how to construct pipelines.
### Constant frames
The simplest pipeline has no input and outputs a constant frame:
~~~~~ {.haskell}
makeFrame = imageFrame (emptyDepthImage 1, emptyColorImage navy)
~~~~~
### From primitive streams to frames
A frame can be constructed from a vertex stream in several phases. We go step-by-step through the phases.
Again, if these steps seem ad-hoc, think about the specialised hardware.
(We believe that some of these limitiation can be lifted by the compiler and the libraries. We plan to gradually improve
the current interface to be more and more higher-level.)
#### Primitive stream transformations
Usually the first phase is the transfromation of the vertices in the primitive stream.
Typically the vertex coordinates are projected onto the viewport and the depth (the distance of the vertex from the viewer) is calculated.
If we use [homogeneous coordinates](https://en.wikipedia.org/wiki/Homogeneous_coordinates), the projection and
the depth can be calculated at the same time by multiplying the vertex with a projection matrix.
The vertices usually have other attributes next to the coordinates, and these attributes can be transformed
also in the first phase.
The restriction is that at least the vertex coordinates used in the rasterization phase should be produced.
The `mapPrimitives` function transforms a primitive stream into another primitive stream.
The following example we project and scale the vertices, and we also store the original
coordinates of the vertices, because we would like to use them later:
~~~~~ {.haskell}
mapPrimitives (\((x)) -> (scale 0.5 (projmat *. x), x))
~~~~~
Here we suppose that the vertices are `Vec 4 Float` values and `projmat` has type `Mat 4 4 Float`.
The `((x))` is the [special syntax](lang-specification#heterogeneous-lists) for tuples with one element.
The `projmat` is embodies the camera and prespective projection transformations. It is a sequential composition of simpler
transformations, using
[rotMatrixY](api-documentation#transformation-functions),
[lookat](api-documentation#transformation-functions),
[perspective](api-documentation#transformation-functions) functions from prelude.
~~~~~ {.haskell}
projmat = perspective 0.1 100.0 (30 * pi / 180) 1.0
.*. lookat (V3 3.0 1.3 0.3) (V3 0.0 0.0 0.0) (V3 0.0 1.0 0.0)
.*. rotMatrixY (pi / 24.0 * time)
~~~~~
#### Rasterization
In the rasterization phase, each primitive is turned into a set of disjunct unshaded (not-yet-colored) fragments.
A **fragment** has viewport coordinates and custom attributes.
It is similar to a pixel.
The rasterization step is not programmable, but configrable by a **rasterization context**.
An example rasterization context for rasterizing triangles:
~~~~~ {.haskell}
TriangleCtx CullNone PolygonFill NoOffset LastVertex
~~~~~
Some explanation (do not mind if you don't understands these now):
`CullNone` tells that both side of the triangles are visible.
`PolygonFill` tells that not only the edges of the trianges are visible.
`NoOffset` tells that rasterization will not modify depth values.
`LastVertex` tells that in case of no interpolation which vertex attribute to use in fragments.
The `rasterizePrimitives` creates a functions primitive stream to a fragment stream, given a
rasterization context and a tuple of `Smooth` and `Flat` values:
~~~~~ {.haskell}
rasterizePrimitives (TriangleCtx CullNone PolygonFill NoOffset LastVertex) ((Smooth))
~~~~~
Here the tuple contains only one `Smooth` value, so we use the [special syntax](lang-specification#heterogeneous-lists).
The rule is that the tuple size should be equal to the number of vertex attributes minus the position attribute used
by the rasterisation phase. Each element of this tuple tells how to interpolate the individual attribute values.
#### Filtering fragment streams
It is possible to filter fragment streams. Filtering is used for example to make fragments look transparent.
If we do not want filtering, we can write
~~~~~ {.haskell}
filterFragments (\_ -> True)
~~~~~
or we can skip this phase.
#### Shading
In the shading phase we transform the fragment stream usually to give color to fragments.
This phase is programmable, the program is given by the `mapFragments` function:
~~~~~ {.haskell}
mapFragments (\_ -> ((V4 0 1 0 1))) -- RGBA
mapFragments (\_ -> ((green)))
~~~~~
![](green-tetrahedron-screenshot.png)
We can paint the fragment green or some time varying color:
~~~~~ {.haskell}
mapFragments (\((x)) -> (( (rotMatrixZ time *. rotMatrixY time *. x) *! f time )) )
~~~~~
`f` is here a helper function:
~~~~~ {.haskell}
f x = (x + sin x + sin (1.1 * x)) `mod` 4 * 2
~~~~~
#### Accumulation
In the accumulation phase sets of overlapping fragments are accumulated into pixels.
**Accumulation** means that we combine the color values of overlapping fragments to get the color of the pixel.
Accumulation is not programmable, but configurable by an **accumulation context**.
The accumulation context tells how to combine previous values on the frame with new incoming values.
An example accumulation context:
~~~~~ {.haskell}
(DepthOp Less True, ColorOp NoBlending (V4 True True True True))
~~~~~
`DepthOp` tells how to accumulate depth values.
`Less` tells that depth test succeed if the fragment's depth values is less (it is closer).
`True` tells that the fragments are opaque, and should we written on the frame if the depth test succeeds.
`ColorOp` tells how to accumulate (the first) color values.
`NoBlending` tells that we don't want to blend (mix) values, the new value is written.
`(V4 True True True True)` gives the write masks for individual color channels.
We can pair a shaded fragment stream with an accumulation context with `accumulateWith`:
~~~~~ {.haskell}
accumulateWith (DepthOp Less True, ColorOp NoBlending (V4 True True True True))
~~~~~
The actual accumulation is done with `overlay` (used as operator here):
~~~~~ {.haskell}
...
imageFrame (emptyDepthImage 1, emptyColorImage navy)
`overlay` accumulablefragments
where
accumulablefragments
= vertexstream
& mapPrimitives (\((x)) -> (scale 0.5 (projmat *. x), x))
& rasterizePrimitives (TriangleCtx CullNone PolygonFill NoOffset LastVertex) ((Smooth))
& filterFragments (\((x)) -> True)
& mapFragments (\((x)) -> (((rotMatrixZ time *. rotMatrixY time *. x) *! f time)))
& accumulateWith (DepthOp Less True, ColorOp NoBlending (V4 True True True True))
~~~~~
Here `(&)` is the flipped function application, and `vertexstream` is a vertex stream of (Vec 4 Float) attributes.
### Putting all together
The pure function which creates the final frame, given the time, the projection matrix and a triangle vertex stream:
~~~~~ {.haskell}
f x = (x + sin x + sin (1.1 * x)) `mod` 4 * 2
makeFrame (time :: Float)
(vertexstream :: PrimitiveStream Triangle ((Vec 4 Float)))
= imageFrame (emptyDepthImage 1, emptyColorImage navy)
`overlay` accumulablefragments
where
projmat = perspective 0.1 100.0 (30 * pi / 180) 1.0
.*. lookat (V3 3.0 1.3 0.3) (V3 0.0 0.0 0.0) (V3 0.0 1.0 0.0)
.*. rotMatrixY (pi / 24.0 * time)
accumulablefragments
= vertexstream
& mapPrimitives (\((x)) -> (scale 0.5 (projmat *. x), x))
& rasterizePrimitives (TriangleCtx CullNone PolygonFill NoOffset LastVertex) ((Smooth))
& filterFragments (\((x)) -> True)
& mapFragments (\((x)) -> (((rotMatrixZ time *. rotMatrixY time *. x) *! f time)))
& accumulateWith (DepthOp Less True, ColorOp NoBlending (V4 True True True True))
~~~~~
Interfacing pipelines with the outer world
------------------------------------------
We can connect the inputs of the previous pipeline to our application by naming the input uniforms and input streams.
The `renderFrame` function turns a frame into an action of producing it on the GPU.
~~~~~ {.haskell}
main = renderFrame $
makeFrame (Uniform "Time")
(fetch "stream4" ((Attribute "position4")))
~~~~~
Or we can use our tetrahedron, which we defined before.
~~~~~ {.haskell}
tetrahedron = map (\v -> V4 v%x v%y v%z 1)
[ V3 0 0 0, V3 1 0 0, V3 0 1 0
, V3 1 0 0, V3 0 1 0, V3 0 0 1
, V3 0 1 0, V3 0 0 1, V3 0 0 0
, V3 0 0 1, V3 0 0 0, V3 1 0 0
]
tetrahedronStream = fetchArrays Triangle ((tetrahedron))
main = renderFrame $
makeFrame (Uniform "Time")
tetrahedronStream
~~~~~
![](tetrahedron-screenshot.png)