In this section of the ILNumerics Documentation, you'll learn more about:
- Positions Buffer
- Colors Buffer
- Indices Buffer
- Updating Vertex Buffers, Multithreading
- Buffers, Clones, Updates
- Cloning Scene Nodes
The actual definition of the rendering data for any shape is done by means of vertices. Every shape contains an arbitrary number of those vertices. They describe points in 3D space: their positions, color and normal vectors – we’ll get to the meaning of each attribute shortly.
Shapes are constructed out of those vertices. ILPoints is indeed very simple: every vertex defines one point on the rendering display. Every line in ILLines will use 2 points, every triangle to be displayed will use 3 points.
The actual configuration of the vertices is managed in buffers. Buffers are organized by their meaning – and not by the vertex. So, all the positions of points in a shape are grouped together into a Positions buffer. Four buffers exist for every shape:
|Positions||Defines the positions of the vertices.||Yes||3 x n matrix, n: number of vertices X, Y, Z float values in columns|
|Colors||Defines individual colors of the vertices.||No||4 x n matrix, n: number of vertices R, G, B, A float values in columns, 0..1f|
|Normals||Defines normal vectors for the vertices, used for lit scenarios.||No||3 x n matrix, n: number of vertices X, Y, Z float values in columns, no normalization necessary|
|Indices||Connects vertices to basic shapes by their index||No||
1 x p vector
Indices into vertices as int values
We will start with an example, similar to the Shapes Tutorial:
Let’s go through the code: At first, a new points shape is created. This would create a single red point at (0, 0, 0). Next, the Positions buffer is changed:
Positions buffers always expect floating point values in single precision as 3 x n matrix, n being the number of vertices to update. The 100 random positions that were created must be converted to single precision. Subsequently, the Update function is used to overwrite the whole buffer. This results in 100 points, evenly distributed in the quadrant 0..1 for X, Y and Z coordinates.
Next, another overload for the Update function is used to determine, which section of the buffer is supposed to be replaced:
In this line the vertex positions are updated at index 100 (i.e. ‚count‘). The next 100 positions are transferred from the array provided. Since the buffer currently only contains 100 positions, it will be expanded automatically. Expanding a buffer is always possible this way.
Since ILMath.randn and not ILMath.rand is used here, normally distributed values, which concentrate around the origin were generated . As a result there are two different sets of points. However, they are hardly distringuishable, because the single colored mode was used and consequently all points were colored red.
Let’s improve this situation by using individual colors for every point:
Only the last 3 lines have changed. Now, the points from rand are clearly distinguishable from those created by randn.
The colors buffer code works very similar to the one for the positions buffer. Again, we use the Update function to determine the colors for both sets of vertices. The first count vertices correspond to the positions created by rand. We set them to a fully opaque green color. This is done by creating the color (ILMath.array<float>(0f,1f,0f,1f)) and replicating the color for all vertices count times (ILMath.repmat(…,1,count)).
The second set of data is configured by providing a matrix with random color data for the next 100 points to the Update function. Again, the overload is used, which allows the specification of the update range within the buffer.
Colors Buffer Format
Note the format of the colors data: The color for every vertex is given as one column in the matrix provided to Update(). The elements in the column define the Red, Green, Blue and Alpha tuples for the color. Every number must be within the range of 0…1f. For the R, G and B channel a value of 0 means: the color component does not exist, a value of 1 means: the color component is set to its maximum level.
For the alpha channel, a value of 0 means: the vertex color is fully transparent (not visible). A value of 1 means: the vertex color is fully opaque.
Single Color Mode versus Individual Colors Mode
As we have seen, shapes can be colored in two different modes: single color and individual colors mode.
- Single color mode is activated by providing a value to the ILShape.Color property. The color will be used to color all vertices in the shape. In our example, this is done by default for ILPoints, which are colored red in the ILPoints constructor. Single color mode is deactivated by providing null to the ILShape.Color property.
- Individual colors mode is configured by providing the correct number of colors to the Colors buffer of a shape. That number must always equal (at least) the number of positions configured to the Positions buffer.
Individual colors are activated by deactivating the single color mode for the shape. Therefore, single color mode always takes precedence over individual colors!
This is the reason why the last line is required in our example. Since ILPoint is created in single color mode, we have to deactivate, it in order to use the individual colors from the Colors buffer:
Indices buffers are used to connect vertices to individual ‚faces‘ – lines or triangles. Points are too simple, since points don’t have any ‘structure’ with more than one vertex involved. Lines and triangles are different. So let’s start with lines.
Back to the main example. Add the following lines to the existing example code:
We created a new line shape. Instead of defining any new vertices, we simply link the new line shape with the Positions buffer of the points shape above.
The Positions buffer of the points holds 200 vertices. Every simple line connects two points. The lines shape interpretes the vertices in the positions buffer as end points for lines. Every two vertices form a new line. So, in contrast to points, the order of the vertices is important for the result.
In order to get more control of the lines created and how which vertices are connected, indices buffers come in handy. Add the following lines to the end of our function:
Note how only those vertices are connected which were originally created by the ILMath.rand function in the first quadrant. All other vertices from the positions buffer are not used.
This is achieved by providing indices to the index buffer. If the index buffer of a shape is not empty or null, those indices are used as references to the vertex positions rather than using the vertex positions directly. In our case, ILMath.vec<int>(0, count - 1) creates numbers from 0 to 99. Every line is drawn by subsequently going through the index buffer (not the positions buffer!) and taking the numbers found as indices into the positions buffer. Every two indices build a new line. Our indices do not reference any vertex after index 99. The second set of points, which were created by ILMath.randn start at 100 and hence are not addressed.
Advanced Indices Buffer Example
Assuming the following task: „The hull of those points which are created by ILMath.randn should be drawn. The color of each hull segment corresponds to the colors of its end points, interpolated over the length of the line.“
While this could be achieved with the same lines shape type, we will choose another line type: line strip. Line strips create connected lines. Every new line connects the new vertex with the last one. Therefore, roughly half the vertices / indices are saved for long lines.
Except for the difference in the vertex buffer interpretation, buffers for line strips are handled exactly in the same way as other shapes:
Now, we have connected all lines, but they certainly do not form the hull of the randn points! In order to achieve this, the order of the vertices needs to be refined. A perfect task for the index buffer! Let‘s remember the Graham Scan Algorithm. A simplified version is sufficient here: We sort the vertices by their angles in the X-Y plane. The sort order will be the order of the vertices needed for drawing:
A buffer allows the read access to its internal storage via the ILBuffer.Storage property. Storage returns a common ILNumerics array, hence enables all common subarray features and direct usage in ILMath functions. We need the X and the Y coordinates. The pos variable now holds the relevant part of the first and the second row from the Positions buffer.
The angles of the vertices to the X-axis are computed via ILMath.atan2 and sorted. ILMath.sort provides the indices of the sorting order as output argument Indices. Now, these indices reflect the order of the vertices in ascending angle values. This is exactly the order we need to draw the line strip in!
However, our indices start at 0. We need to map the indices to the needed vertex position range by adding the starting index in the buffer: indices + count. The result is used to configure the line strip indices buffer.
The seamless integration of rendering objects within ILNumerics core allows the convenient implementation of arbitrary rendering tasks. Imagine how much more effort even the simple solution above would have required, without ILMath, subarray features and index keeping sort() … !
Updating Vertex Buffers, Multithreading
Vertex buffers can be updated at any time during the lifetime of an application. It is most efficient – but not required – to realize all updates to a scene from the same thread.
When handling updates to any vertex buffer, two important rules should be kept in mind:
- Use one of the Update() function overloads on a vertex buffer, in order to change its values.
- In order to propagate buffer manipulations to the rendering output, it is necessary to call Configure() on the shape node or any node above it.
The first point is important for efficiency reasons: ILNumerics buffers support incremental updates for populating changes to renderers. Renderers, which support it (e.g. OpenGL) profit from the ability to only update parts of the buffer in graphic memory. Using one of the Update() function overloads enables this feature. It is recommended to only update the region of the buffer which has really changed.
Technically, Configure() is always necessary to be called once before rendering to signal updates made to any buffers. Two exceptions exist:
- the ILNumerics Web Code Component and
- scene definitions in the load handler of ILPanel
ILNumerics considers both places to be an initial setup of a scene. Therefore, Configure() is called automatically for you. This is the reason, why Configure() is omitted in most examples in the online documentation.
Full Update Example
The example takes a regular sphere object and alters its vertex positions by "cutting" a slice from the upper hemisphere. The full set of vertex positions is then written back to the buffer.
In the last example we updated the full positions buffer. If possible, one should use the
overload of a buffer. It allows the renderer to optimize the upload behavior for frequent updates to video memory or to remote client renderers (f.e. WebGL).
The second point to note for updating vertex buffers, is to call Configure() on the node or any parent. This prevents complicated and potentially long lasting buffer updates to cause partial renderings and distorted results. Furthermore, many complex composed objects rely on Configure() to be called before rendering. Configure() signals the renderer that the manipulation by the user is finished and changes should be populated to the renderer. Technically, the rendering result potentially does not even change before Configure() is called. The renderers hold their own clone of the scene and use that for rendering. Once the user calls Configure(), changes made to an object and/or its buffers are populated to the cloned scene within the renderers and only then become ready for rendering.
Buffers, Clones, Updates
The concept of creating clones, buffer sharing and property overriding in ILNumerics still deserves some more attention. The whole idea is based on the fact that many interactive applications need to visualize large and complex scenes, often composed out of several thousands of shapes, rendering several hundreds of thousands or even millions of lines and triangles. At the same time they are required to
- allow updates to any part of the scene, even during rendering.
- The vertex buffer is where large data is stored. A scene should minimize redundant storage and share vertex buffers whenever possible. A CAD application, for example, should only use one set of buffers for the left, top and 3D view of the tool model it is editing.
- Even if objects share their vertex base, some amount of individual configuration should be possible nevertheless. To recite the CAD application example: the display in the left view may hide some objects from the scene or display them in a color different to the one in the 3D view.
ILNumerics achieves that by creating clones of (sub-) scenes and shapes. Clones are created in many situations:
- When a shape which is already contained in a scene is added to a scene again.
- When a subtree which is already contained in a scene is added to a scene again.
- When a driver (WebGL, GDI, OpenGL, etc.) is rendering a scene.
- When a scene is shared between several individual drivers, for example for printing, exporting or duplicating for sharing with other forms.
Another aspect profits from the cloning scheme: Multithreading. ILNumerics allows a single scene to be rendered by several renderers simultaneously. A setup might use 2 OpenGL plotting panels, a GDI panel and a WebGL export control at the same time. As soon as multiple renderers – possibly running in multiple threads – are involved, considerations for preventing from multithreading issues are necessary. This is especially true for interactive scenes with frequent updates. The naive approach would be to use locking mechanisms to synchronize the rendering threads with updates to a scene. This solution often causes high contention and performance degradation.
By maintaining individual clones of the scene for every driver, locking is limited to a short synchronization step: Both, original and the cloned version are constantly synchronized. Updates to the scene are transferred to the clone within each rendering frame.
Cloning Scene Nodes
When a node is added to the scene and the node is already part of a scene, the node is cloned and the cloned copy is added instead. That way ILNumerics ensures that all nodes in a scene are unique.
How the clone is created, depends on the type of the node:
- Labels, Lights, Groups and Camera nodes are cloned by creating a new node and copying all properties from the source.
- Group nodes clone the nodes of their subtree recursively.
- Shape nodes (ILPoints, ILLines, ILLineStrip, ILTriangles, ILTriangleFan, ILTriangleStrip) are cloned by creating a new shape of the same kind and sharing the same set of vertex buffers with the original shape.
Therefore, most scalar properties of a cloned scene node are individually configurable for the clone. The collection, for example, includes: Color, Tag, Visible and the Transform property of group nodes.
Vertex buffers (Positions, Colors, Normals and Indices) are shared between the clone and its original node. This means, when the vertex buffer of a clone is altered, the original buffer is altered as well! In order to make the cloned node behave completely independent from its original, the Detach() function of a node is used. After calling Detach(), buffers on the node can be altered independently from any other node. Here, a lazy copy on write mechanism is used. Hence, new storage is acquired only as soon as the buffer is really changed.