Performance Tips for Visualizing Lots of Points

by

In Cesium we’ve historically been able to create points using billboards with circles, but now we can create millions of points with the faster, less memory-intensive PointPrimitive and PointPrimitiveCollection, introduced back in Cesium 1.10. Here’s a code sample of 64,800 PointPrimitive objects covering the globe, as well as an image of the result.

var viewer = new Cesium.Viewer('cesiumContainer');
var points = viewer.scene.primitives.add(new Cesium.PointPrimitiveCollection());

for (var longitude = -180; longitude < 180; longitude++) {
    var color = Cesium.Color.PINK;
    if ((longitude % 2) === 0) {
        color = Cesium.Color.CYAN;
    }

    for (var latitude = -90; latitude < 90; latitude++) {
        points.add({
            position : Cesium.Cartesian3.fromDegrees(longitude, latitude),
            color : color
        });
    }
}

Why would users want to draw so many points? One application of PointPrimitive is tracking objects in space, as is done by ComSpOC, which currently tracks 15,422 satellites. That number is expected to grow to about 200,000 once ComSpOC tracks objects down to two centimeters in diameter. This is a great use case for PointPrimitive, which provides the needed performance.

PointPrimitive and Billboard

PointPrimitive provides a dramatic performance increase over the billboards with circles. Since PointPrimitive is tailored just for points, it has less complex shaders and uses less memory. The tables below show the performance difference, which is especially good when the number of points is large.

The first table shows the performance and memory usage of static points. PointPrimitive maintains a lower memory usage over billboards with circles.

Static points, 10 pixel point size created with this code

Number of points

Billboards with circles

PointPrimitive

90,000

FPS: 59
Memory: 400 MB

FPS: 59
Memory: 347 MB

250,000

FPS: 59
Memory: 635 MB

FPS: 59
Memory: 506 MB

1,000,000

FPS: 59
Memory: 1,563 MB

FPS: 59
Memory: 1,110 MB

1,562,500

FPS: 44
Memory: 2,143 MB

FPS: 44
Memory: 1,459 MB

2,250,000

Page Timed Out

FPS: 35
Memory: 1,743 MB

The memory column is the amount of memory used by Chrome’s GPU process and the tab.

The table below shows performance for dynamic points, where each point’s position is updated each frame. Compared to billboards, PointPrimitive has both memory and frames per second improvements in the dynamic case.

Dynamic points, 10 pixel point size created with this code

Number of points

Billboards with circles

PointPrimitive

10,000

60 FPS

60 FPS

90,000

46 FPS

56 FPS

160,000

27 FPS

37 FPS

250,000

15 FPS

23 FPS

PointPrimitive Optimization

PointPrimitive can be further optimized by changing a few attributes.

The table below shows a performance increase as the size of the points decreases. This is because there are fewer pixels per point that the GPU needs to render.

2,073,600 static point primitives at different points sizes created with this code

Point size (pixels)

Frames per second

32

14 FPS

16

27 FPS

8

43 FPS

4

58 FPS

PointPrimitive can be optimized using scaleByDistance or translucencyByDistance. The performance increase is most notable when the camera is farther from the points. Points in the distance will be very small (scaleByDistance) or completely translucent (translucencyByDistance), reducing the number of pixels to be rendered. This can also declutter the scene.

1,250,000 static point primitives created with this code

Unmodified points

Scale by distance points

Fade by distance points

33 FPS

60 FPS

60 FPS

Key points

  • If you need to create many points, use PointPrimitive
  • Optimize PointPrimitive by
    • Changing pixelSize
    • Using scaleByDistance or translucencyByDistance

For more details, check out the reference documentation for PointPrimitive and PointPrimitiveCollection.

Appendix

Code used for performance tests

Table 1

var viewer = new Cesium.Viewer('cesiumContainer');
var scene = viewer.scene;
scene.debugShowFramesPerSecond = true;
var numberOfPoints = 90000;
var gridSize = 360 / Math.sqrt(numberOfPoints);
var isPointPrimitive = true;

if (isPointPrimitive) {
    var points = scene.primitives.add(new Cesium.PointPrimitiveCollection());
} else {
    var billboards = scene.primitives.add(new Cesium.BillboardCollection());
    var canvas = document.createElement('canvas');
    canvas.width = 10;
    canvas.height = 10;
    var context2D = canvas.getContext('2d');
    context2D.beginPath();
    context2D.arc(5, 5, 5, 0, Cesium.Math.TWO_PI, true);
    context2D.closePath();
    context2D.fillStyle = 'rgb(255, 255, 255)';
    context2D.fill();
}

for (var longitude = -180; longitude < 180; longitude += gridSize) {
    for (var latitude = -90; latitude < 90; latitude += gridSize / 2) {
        if (isPointPrimitive) {
            points.add({
                position : Cesium.Cartesian3.fromDegrees(longitude, latitude),
                pixelSize : 10
            });
        } else {
            billboards.add({
                imageId : 'billboard point',
                image : canvas,
                position : Cesium.Cartesian3.fromDegrees(longitude, latitude),
            });
        }
    }
}

Table 2

var viewer = new Cesium.Viewer('cesiumContainer');
var scene = viewer.scene;
scene.debugShowFramesPerSecond = true;
var numberOfPoints = 10000;
var gridSize = 360 / Math.sqrt(numberOfPoints);
var isPointPrimitive = true;

if (isPointPrimitive) {
    var pointCollection = scene.primitives.add(new Cesium.PointPrimitiveCollection());
} else {
    var billboardCollection = scene.primitives.add(new Cesium.BillboardCollection());
    var canvas = document.createElement('canvas');
    canvas.width = 10;
    canvas.height = 10;
    var context2D = canvas.getContext('2d');
    context2D.beginPath();
    context2D.arc(5, 5, 5, 0, Cesium.Math.TWO_PI, true);
    context2D.closePath();
    context2D.fillStyle = 'rgb(255, 255, 255)';
    context2D.fill();
}

for (var longitude = -180; longitude < 180; longitude += gridSize) {
    for (var latitude = -90; latitude < 90; latitude += gridSize / 2) {
        if (isPointPrimitive) {
            pointCollection.add({
                position : Cesium.Cartesian3.fromDegrees(longitude, latitude),
                pixelSize : 10
            });
        } else {
            billboardCollection.add({
                imageId : 'billboard point',
                image : canvas,
                position : Cesium.Cartesian3.fromDegrees(longitude, latitude),
            });
        }
    }
}

function animatePoints() {
    var positionScratch = new Cesium.Cartesian3();
    var points = pointCollection._pointPrimitives;
    var length = points.length;
    for (var i = 0; i < length; ++i) {
        var point = points[i];
        Cesium.Cartesian3.clone(point.position, positionScratch);
        Cesium.Cartesian3.add(
            positionScratch,
            new Cesium.Cartesian3(1000, 1000, 1000),
            positionScratch);
        point.position = positionScratch;
    }
}

function animateBillboards() {
    var positionScratch = new Cesium.Cartesian3();
    var billboards = billboardCollection._billboards;
    var length = billboards.length;
    for (var i = 0; i < length; ++i) {
        var billboard = billboards[i];
        Cesium.Cartesian3.clone(billboard.position, positionScratch);
        Cesium.Cartesian3.add(
            positionScratch,
            new Cesium.Cartesian3(1000, 1000, 1000),
            positionScratch);
        billboard.position = positionScratch;
    }
}

if (isPointPrimitive) {
    scene.preRender.addEventListener(animatePoints);
} else {
    scene.preRender.addEventListener(animateBillboards);
}

Table 3

var viewer = new Cesium.Viewer('cesiumContainer');
viewer.scene.debugShowFramesPerSecond = true;
var points = viewer.scene.primitives.add(new Cesium.PointPrimitiveCollection());
var pixelSize = 32;

for (var longitude = -180; longitude < 180; longitude += 0.25) {
    for (var latitude = -90; latitude < 90; latitude += 0.125) {
        points.add({
            position : Cesium.Cartesian3.fromDegrees(longitude, latitude),
            pixelSize : pixelSize
        });
    }
}

Table 4

var viewer = new Cesium.Viewer('cesiumContainer');
var scene = viewer.scene;
scene.debugShowFramesPerSecond = true;
var points = scene.primitives.add(new Cesium.PointPrimitiveCollection());
var ellipsoid = scene.globe.ellipsoid;
var e = new Cesium.Rectangle(-1.8, 0, -.8, 1);
var gridSize = 250;

for (var z = 0; z < 20; z++) {
    var color = Cesium.Color.WHITE;
    if ((z % 2) === 0) {
        color = Cesium.Color.RED;
    }
    for (var y = 0; y < gridSize; ++y) {
        for (var x = 0; x < gridSize; ++x) {
            var longitude = Cesium.Math.lerp(e.west, e.east, x / (gridSize - 1));
            var latitude = Cesium.Math.lerp(e.south, e.north, y / (gridSize - 1));
            var altitude = z * 500000;
            var position = new Cesium.Cartographic(longitude, latitude, altitude);
            points.add({
                position : ellipsoid.cartographicToCartesian(position),
                color : color,
                //scaleByDistance : new Cesium.NearFarScalar(1.5e2, 15, 1.5e7, 0.0)
                //translucencyByDistance : new Cesium.NearFarScalar(1.5e2, 1.0, 1.5e7, 0.0)
            });
        }
    }
}

viewer.camera.flyTo({
    destination : new Cesium.Cartesian3(
        -9171727.512461,
        -19629387.206475668,
        11458919.120130632)
});

Software used for performance tests

Operation System: OS X El Capitan

Cesium: Cesium 1.18

Hardware used for performance tests

CPU: Intel Core i7 2.8 GHz

GPU: AMD Radeon R9 M370X 2048 MB

RAM: 16 GB

On lower-end hardware, PointPrimitive should provide an even more significant improvement compared to billboards.