Created Vector Data Representation for WebGL (markdown)
@@ -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).
|
||||
Reference in New Issue
Block a user