Created Vector Data Representation for WebGL (markdown)

twpayne
2013-01-28 07:13:08 -08:00
parent e3f50720aa
commit 6307d85055
+126
@@ -0,0 +1,126 @@
# Vector data representation for WebGL
## Introduction
WebGL provides access to the GPU. GPUs are very different to CPUs: GPUs are effectively massively parallel SIMD processors. That this, they apply the same instruction to multiple data. Not just to exploit the high performance on large vector data sets that WebGL enables, but simply to get reasonable performance even on small vector data sets, it is necessary to structure the vector data so that it can be consumed effectively by the GPU. These same strucures will likely also have a small but positive effect on performance of canvas-based renderers.
## How WebGL renders
WebGL is a low-level API. It is only capable of drawing circular points, 1-pixel wide lines, and triangles. In all but the most trivial cases, every geometry type must be converted into triangles before it is drawn. For example, to draw a line, it is converted into a long thin rectangle, which is then cut into two triangles.
WebGL is designed for batch processing of tens of thousands of triangles with each call. The overhead of each call is non-trivial, and so drawing triangles one-by-one is extremely slow. Before a triangle can be rendered by WebGL, it must first be copied to the graphics card memory, which is also a slow operation. For data sets that do not change, WebGL provides a mechanism to copy the data once and then re-use them.
The data must be structured to allow this batch processing. Here is a worked example for two lines or rectangular polygon features, in pseudocode. To simplify the explanation, the functions used in the pseudocode do not correspond exactly to indvidual WebGL functions, but instead represent how WebGL behaves.
We represent each of our features as two triangles, and label the vertices:
```
A-----B E-----F
| \ | | \ |
| \ | | \ |
C-----D G-----H
```
This contains four triangles: ABD, ACD, EFH, EGH
We can render each triangle individually:
```javascript
function render() {
drawTriangle(A, B, D);
drawTriangle(A, D, C);
drawTriangle(E, F, H);
drawTriangle(E, H, G);
}
```
However, this has very poor performance: we pay the overhead of calling `drawTriangle` four times, and we copy all vertices to the graphics card memory every time we draw.
An improvement is to pack all the vertices into a single array and pass this to WebGL to draw all four triangles with a single call:
```javascript
function render() {
drawTrianglesArray([A, B, D, A, D, C, E, F, H, E, H, G]);
}
```
WebGL automatically takes successive triples of vertices to define each coordinate. Here we only pay the cost of calling `drawTrianglesArray` once, but the array is large and we still copy it to the graphics card memory every time we draw.
The next step is to avoid passing duplicate vertices by passing two arrays: one containing the unique vertices, and the second containing a list of integer indexes into that array:
```javascript
function render() {
drawTriangles({
vertices: [A, B, C, D, E, F, G, H],
indexes: [0, 1, 2, 0, 3, 2, 4, 5, 7, 4, 7, 6]
});
}
```
Here there is a single call. Although it slightly more data is copied to the graphics card (indexes are 16-bit unsigned integers, and so half the size of 32-bit floats), the graphics card can be more efficient as it only needs to transform each unique vertex once. When a vertex is shared between multiple triangles (as is the case when complex polygons are triangulated) this results in a significant speed-up, and also reduces the amount of memory that must be copied to the graphics card.
Finally, we can avoid copying the data each time:
```javascript
var indexArrayRef, vertexArrayRef;
function init() {
// copy data to GPU, return reference to copied array
vertexArrayRef = createVertexArray([A, B, C, D, E, F, G, H]);
indexArrayRef = createIndexArray([0, 1, 2, 0, 3, 2, 4, 5, 7, 4, 7, 6]);
}
function render() {
drawTrianglesWithArrays(vertexArrayRef, indexArrayRef);
}
```
This has excellent performance: we copy the data to the graphics card only once, and we issue a single draw call to draw all the triangles. Using this technique, WebGL can draw up to 65536 / 3 = 21845 triangles with a single call.
## Styling
In the above example, all the triangles are drawn with the same style. We can add extra arrays to contain styling information, e.g. color, so that each triangle is drawn in a different style. Because of the way that WebGL works, we need to repeat the color for each vertex. In pseudocode:
```javascript
var indexArrayRef, vertexArrayRef, colorArrayRef;
function init() {
// copy data to GPU, return reference to copied array
vertexArrayRef = createVertexArray([A, B, C, D, E, F, G, H]);
indexArrayRef = createIndexArray([0, 1, 2, 0, 3, 2, 4, 5, 7, 4, 7, 6]);
colorArrayRef = createColorArray(['red', 'red', 'red', 'green', 'green', 'green',
'blue', 'blue', 'blue', 'white', 'white', 'white']);
}
function render() {
drawTrianglesWithArraysAndColor(vertexArrayRef, indexArrayRef, colorArrayRef);
}
```
This demonstrates the principle of how per-triangle styles can be implemented. Furthermore, if we change the style of an element, we only need to re-upload the color array to the GPU, allowing us to be responsive to style changes.
## Performance gains for non-WebGL renderers
These same structures will bring performance benefits to non-WebGL renderers. These include:
* the ability to batch feature drawing, including minimising changes in the canvas state
* efficient loops that can be optimized well by the JavaScript VM and access memory in a linear fashion
* if re-projection is required, then each unique vertex need only be transformed once
* hit-testing against polygonal features is easier if they are cut into triangles
## Efficiency of implementation
For efficiency, a good implementation should have the following properties:
* when parsing vector formats, the vertices should be stored directly in an array suitable for upload to the GPU
* when features or styles are modified, the changes should be tracked so that only the mimimum changes are uploaded to the graphics card
## Real life examples
These best practices are well demonstrated in Cesium's [PolylineCollection](https://github.com/AnalyticalGraphicsInc/cesium/blob/master/Source/Scene/PolylineCollection.js) and [BillboardCollection](https://github.com/AnalyticalGraphicsInc/cesium/blob/master/Source/Scene/BillboardCollection.js).