Skip to main content

Performance Tips for Visualizing Lots of Points

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 PointPrimitiveobjects 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 pointsBillboards with circlesPointPrimitive
90,000FPS: 59 Memory: 400 MBFPS: 59 Memory: 347 MB
250,000FPS: 59 Memory: 635 MBFPS: 59 Memory: 506 MB
1,000,000FPS: 59 Memory: 1,563 MBFPS: 59 Memory: 1,110 MB
1,562,500FPS: 44 Memory: 2,143 MBFPS: 44 Memory: 1,459 MB
2,250,000Page Timed OutFPS: 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 pointsBillboards with circlesPointPrimitive
10,00060 FPS60 FPS
90,00046 FPS56 FPS
160,00027 FPS37 FPS
250,00015 FPS23 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
3214 FPS
1627 FPS
843 FPS
458 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

33 FPS

Point

Scale by distance points

60 FPS

Point

Fade by distance points

60 FPS

Point

Key points

  • If you need to create many points, use PointPrimitive
  • Optimize PointPrimitive by
    undefinedundefined

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.