Skip to main content

Graphics Tech in Cesium - Vertex Compression

A common practice in computer graphics is to pack and compress vertex attributes. It reduces the memory footprint, time to transfer data across the bus from the CPU to the GPU, and GPU memory bandwidth at the cost of extra instructions in the vertex shader. Another benefit may be that there are more attributes than the maximum number of vertex attributes supported.

One way to reduce the number of vertex attributes is to make all attributes a four-component vector and make sure all of the components are used. For example, instead of

attribute vec3 axis;
attribute float rotation;

we could use one vec4 attribute:

attribute vec4 axisAndRotation;
//...
vec3 axis = axisAndRotation.xyz;
float rotation = axisAndRotation.w;

We can take it a step further by packing multiple attributes into a single floating-point number. For example, why store a boolean attribute in an unsigned byte when it only needs one bit? I expect there to be GLSL bitwise operators in WebGL 2.0, but, until then, we can use multiplication and division by powers of two to shift bits.

A 32-bit floating-point number has 24 bits of precision so we can pack 24 bits into a single floating-point value. As an example, we will walk through one packed component of a compressed Billboard attribute in Cesium. First, we pack in JavaScript:

var UPPER_BOUND = 32768.0;  // 2^15

var LEFT_SHIFT7 = 128.0;
var LEFT_SHIFT5 = 32.0;
var LEFT_SHIFT3 = 8.0;
var LEFT_SHIFT2 = 4.0;

function clamp(value, min, max) {
  return value < min ? min : value > max ? max : value;
}
    
// The pixel offset is a screen space offset in pixels. We assume it can be represented as a short.
// First, clamp it to the range +/- 2^15 which is representable by a short.
var compressed = CesiumMath.clamp(pixelOffsetX, -UPPER_BOUND, UPPER_BOUND);

// Add back the UPPER_BOUND so it is in the range [0, 2^16]
compressed = Math.floor(compressed + UPPER_BOUND);

// Left shift so we can use the remaining bits
compressed = * LEFT_SHIFT7;

// The horizontal origin can be center, left or right. The three value can be represented in two bits.
// The value is an integer in the range [-1, 1]. We add one to be in the range [0, 2] and shift it left
compressed += (horizontalOrigin + 1.0) * LEFT_SHIFT5;

// Similar to the horizontal origin
compressed += (verticalOrigin + 1.0) * LEFT_SHIFT3;

// Show is a boolean that only needs one bit. We will have one bit remaining unused.
compressed += (show ? 1.0 : 0.0) * LEFT_SHIFT2;

We simply reverse these operations in GLSL to extract the values. See BillboardCollectionVS.glsl.

For more information on vertex compression, see the references.

Normal Compression

For a survey of different techniques to compress unit vectors, see [Cigolle14]. We chose to use the oct representation for unit vectors in Cesium. This representation compresses a three-component unit-length vector to a two component vector where each component is stored in 8 bits. See AttributeCompression.octEncode for the JavaScript code to convert a unit length Cartesian3 to an oct encoded Cartesian2.

The oct encoded vectors can be stored as a two-component unsigned byte vertex attribute. Though, if we have more data available, we may be able to further pack the vectors. We can pack the two 8-bit components into a single float leaving 8 more bits for other data. See AttributeCompression.octEncodeFloat for the JavaScript to encode them and czm_octDecode for the GLSL to extract them. Three unit vectors can be packed into two floating-point numbers. See AttributeCompression.octPack for the JavaScript code to encode them and czm_octDecode for the GLSL code to extract them.

Encoding three unit vectors into two floating-point numbers is only useful for unrelated unit vectors. For example, tangent space vectors only needs two vectors to be encoded. The third vector can be computed using the cross product of the other two.

Texture Coordinate Compression

For geometry generated by Cesium and billboards, the full 24-bit precision of floating-point numbers is not needed for texture coordinates. We only use 12 bits of precision for each texture coordinate. The texture wrap mode must not be needed and each coordinate must be strictly in the zero to one range. See AttributeCompression.compressTextureCoordinates which will compress the texture coordinates into a single floating-point number.

In Cesium, we use this for generated geometry where we know the texture coordinates will be in the zero-to-one range and we do not make use of the texture wrap mode. Similarly, we use this compression for billboards. It may cause artifacts for billboards if the texture atlas gets large enough, but we haven’t seen any artifacts in practice.

Vertex Compression in Cesium

The Primitive has a compressVertices option, which defaults to true, that causes the tangent space vectors and the texture coordinates to be compressed as described above.

The BillboardCollection and LabelCollection have a total of 18 attributes per vertex, each with various types and number of components. After packing and compression, the number is down to eight four-component floating-point attributes per vertex. For more details, see the BillboardCollection or its vertex shader.

References

[Calver02] Dean Calver. Vertex Decompression in a Shader. In Direct3D ShaderX: Vertex and Pixel Shader Tips and Tricks. Edited by Wolfgang F. Engel. 2002.

[Cigolle14] Cigolle, Donow, Evangelakos, Mara, McGuire, Meyer, A Survey of Efficient Representations for Independent Unit Vectors, Journal of Computer Graphics Techniques (JCGT), vol. 3, no. 2, 1-30, 2014.

[Persson12] Emil Persson. Creating Vast Game Worlds. 2012.

[Pranckevičius09] Aras Pranckevičius. Compact Normal Storage for Small G-Buffers. 2009.