Destroyed Vector Data Representation for WebGL (markdown)

Tim Schaub
2017-09-25 23:29:18 -07:00
parent 74817fb524
commit 51e19dab16
-331
@@ -1,331 +0,0 @@
# 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).
# Concrete implementation
## Objective
The objective of this section is to outline a vector data representation that will allow the implementation of efficient Canvas 2D and WebGL renderers. "Efficient" in this context means:
* coordinates do not need to be copied into new data structures, the data structures are usable directly by both Canvas 2D and WebGL
* styling information does not need to be copied into data structures or re-calculated
* when geometries are added, updated, or deleted, the changes to the data structures are minimised
* when styles are updated, changes to the data structures are minimised
This document primarily focuses on geometries, although some suggestions as to how styling can be implemented will be given.
## High level view
Canvas 2D is a sequential renderer: features are rendered one at a time, in sequence. WebGL rendering is parallel: multiple features are rendered simultaneously. Therefore, we store related geometries in "collections" that can be passed as a unit to the WebGL renderer, and trivially iterated over in the Canvas 2D renderer. In general, all geometries in a collection will be of the same type (e.g. point, linestring, polygon).
It remains to be decided how many collections are used. As an initial guideline, a vector source will likely have one collection per type of geometry present in the source.
A collection contains:
* Meta information about the type of geometry stored.
* An `Array.<number>` containing the raw, unpacked coordinates (e.g. `[x1, y1, x2, y2, ...]`), called the vertex array, length 2N where N is the number of coordinates.
* For linestring and polygon collections, an `Array.<number>` containing indexes 0..N-1 into the vertex array, whose length will vary according to the geometries present.
* One or more `Array.<number>`s containing cached styling information (e.g. line width, point size, color components). For point collections, this will have as many elements as there are vertices. For linestring and polygon collections, this will have as many elements as the index array.
* Further meta information to allow the extraction and modification of individual geometries in the collection.
## Reading external data
Format parsers should create collections directly, or extend existing collections.
## Rendering points
Point collections contain a vertex array of unpacked coordinates and zero or more styling arrays.
### Canvas 2D
The renderer loops over the vertex arrays, drawing points according to the contents of the styling array(s). Points are drawn in series. The styling arrays can be used to avoid unnecessary canvas state changes.
### WebGL
The renderer uploads the vertex arrays and styling array(s) to the GPU, and tells the GPU to draw the points in parallel.
## Rendering linestrings
A linestring collection contains multiple linestrings. They contain a vertex array of unpacked coordinates, an index array for Canvas 2D, an index array for WebGL, and zero or more styling arrays. The WebGL-specific index array should be created only if needed. For example, consider the linestrings:
```
A
\ D----E
\
B---C
```
In this case, the vertex array contains:
```
[Ax, Ay, Bx, By, Cx, Cy, Dx, Dy, Ex, Ey]
```
We populate the index array with successive pairs of start and stop indexes in the vertex array for each linestring. That is, it contains a linestring using vertices [0..3) (i.e. A, B, C) and a linestring using vertices [3, 5) (i.e. D, E):
```
[0, 3, 3, 5]
```
### Canvas 2D
The Canvas 2D renderer iterates over the pairs of indexes in the index array, drawing lines one by one. The style arrays can be used to avoid unnecessary state changes.
### WebGL
The following is subject to change as rendering styled lines in WebGL is non-trivial.
#### Approach 1: rendering simple lines with a single style
WebGL can draw simple line segments. Nominally these can be of any width, but practically only width 1 is widely supported. We compute an index array with pairs of line vertices. For example:
```
[0, 1, 1, 2, 2, 3, 4, 5]
```
The vertex array and index arrays are uploaded to the GPU, and the line segments are drawn in parallel.
#### Approach 2: rendering lines with two triangles
See [drawing nearly perfect 2D line segments in OpenGL](http://www.codeproject.com/Articles/199525/Drawing-nearly-perfect-2D-line-segments-in-OpenGL) for one promising technique. It may be possible to calculate the triangle vertices in the vertex shader, and therefore avoid having to calculate them in JavaScript.
## Rendering polygons
A polygon collection contains multiple polygons. Once again we use a common vertex array, and renderer-specific index arrays. In this description, we do not consider polygons with holes, but this does not preclude the implementation of polygons with holes in the future.
Consider the polygons:
```
A----B E
| | / \
| | / \
D----C G-----F
```
The vertex array contains the unpacked vertices:
```
[Ax, Ay, Bx, By, Cx, Cy, Dx, Dy, Ex, Ey, Fx, Fy, Gx, Gy]
```
During construction of polygon collection, we specify which vertices belong to which polygon:
```
[[0, 1, 2, 3], [4, 5, 6]]
```
These nested arrays can equivalently be represented as two arrays, the first containing the vertex indexes and the second containing successive pairs of ranges:
```
[0, 1, 2, 3, 4, 5, 6]
[0, 4, 4, 7]
```
### Canvas 2D
The canvas 2D renderer loops over the polygons, drawing them one by one.
### WebGL
As described above, the WebGL renderer will need to split polygons into triangles. The polygons above would be split as:
```
A----B E
| \ | / \
| \ | / \
D----C G-----F
```
We can re-use the existing vertex array and the triangulation step need only output a new index array in which successive triples of indexes represent triangles:
```
[0, 1, 2, 0, 2, 3, 4, 5, 6]
```
Note that a suitably advanced triangulation algorithm can cope with polygons with holes.
We upload the vertex array, the index array and the styling arrays to the GPU, and WebGL draws the triangles in parallel.
## Updating geometries
Vertex, index and style arrays only need to re-uploaded to the GPU if they change. Re-scanning a geometry collection to see if anything has changed is clearly expensive and undesirable. Therefore, a suitable API is needed to efficiently flag when the array elements have changed.
## Extracting the original geometries
It is clearly necessary to be able to extract the original geometries from the collection, e.g. to be able to return them to the server as GeoJSON objects. Necessary metadata needs to be stored in the collection to enable this. Renderers, however, should use the arrays directly for efficiency.
## Notes
### Geometry editing
Typically a user will only edit one geometry at time, while the other geometries remain constant. To avoid re-drawing all features while a single geometry is being changed, the following technique should work well:
1. Temporarily disable the geometry in its collection.
2. Add the geometry to a new collection (layer), containing only that geometry.
3. While editing proceeds, only the collection (layer) containing the editable feature needs to be redrawn.
4. When editing is complete, remove the temporary collection (layer) and re-insert the geometry in the original collection.
This also has the nice side effect of ensuring that the edited feature is always on top.
### Avoiding floating point errors in WebGL
WebGL internally uses 32-bit floating point values. These are insufficiently precise for geographical coordinates. A work around is to give each collection a local origin and make its coordinates relative to this local origin.
The same technique could be used in the canvas renderer, allowing 32-bit floats to be used for geographical coordinates and therefore halving memory usage.
### Garbage collection
Although this structure of arrays is more complex to manage, it places much less load on the garbage collector since there are far fewer objects created. Good JavaScript implementations will recognize that the arrays only contain number values, and therefore not traverse them during the mark and sweep phase. Contrast this with the GC impact of arrays of `ol.Coordinate`s.
### TopoJSON
[TopoJSON](https://github.com/mbostock/topojson/) uses shared vertices for significant speed and size improvements over GeoJSON. It is not possible to take advantage TopoJSON without using a shared coordinate array.
### Binary representation of vector data
As more and more vector features are added, the overhead of XML or JSON parsing can become significant. Using [ArrayBuffers](https://developer.mozilla.org/en-US/docs/JavaScript/Typed_arrays/ArrayBuffer), it is possible to read vertex and index data directly into a typed array and therefore avoid all parsing overhead. This is one of the techniques used by [Here (Nokia) Maps 3D](http://here.com/50.0508706,-1.3870925,4,0,0,3d.day) to achieve such high performance.
### 3D terrain and models
3D models, e.g. Triangular Irregular Network elevation tiles and buildings, are typically generated on the server and served to the client as an array of vertices and an array of triangle indexes. With the above representation, the same vector representation can be used for 2D vector data, 3D models and terrain tiles.
## Not covered in this document
* howto handle polygons with holes
* how to structure metadata so that multipoints, multilinestrings and multipolygons can be extracted
* how to draw non-trivial lines in WebGL
* how to symbolize points with an image
* how to implement hit detection