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 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
33 FPS
Scale by distance points
60 FPS
Fade by distance points
60 FPS
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),
});
}
}
}
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);
}
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
});
}
}
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.