Cesium Workshop

Overview

Welcome to the Cesium community! We are happy to have you join us. In order to get you on your way to developing your own web map applications, this tutorial will walk you through the development of a simple but wide-reaching Cesium application from beginning to end. This tutorial will touch on many of the most important aspects of the CesiumJS API, but is no means comprehensive (CesiumJS has a lot of features!). Our goal is to introduce the fundamentals and the tools you’ll need to explore the rest of Cesium.

We’ll create a simple application for visualizing sample geocache locations in New York City. We’ll load and style multiple types of 2D and 3D data and create several camera and display options that a user can set interactively. Finally, as high-tech geocachers, we’ll load a 3D drone model to scout the geocache locations and take full advantage of our 3D visualization.

By the end of this tutorial, you will have a working overview of Cesium’s features including configuring a Cesium viewer, loading datasets, creating and styling geometry, working with 3D Tiles, controlling the camera, and adding mouse interactivity to your application.

Finished app Our application for interactively visualizing sample geocache locations.

Setup

Just a few setup steps before we can get to development.

  1. Make sure your system is Cesium compatible by visiting Cesium Viewer. No globe? See Troubleshooting.
  2. Install Node.js.
  3. Get the workshop code. Either clone or download the zip and extract the contents.
  4. In your console, navigate to the root cesium-workshop directory.
  5. Run npm install.
  6. Run npm start.

The console should show the following.

Cesium development server running locally.  Connect to http://localhost:8080

Don’t close the console! We’ll need to keep this process running.

Next, navigate to localhost:8080 in your browser. You should see our workshop application up and running. Stuck? The Getting Started Tutorial goes more in depth about CesiumJS setup.

The app directory

Within the application directory, we should see the following directories and files. Note that this application directory was designed to be as simple as possible and only includes the CesiumJS library.

  • Source/ : Our application code and data.
  • ThirdParty/ : External libraries, in this case just the CesiumJS library.
  • LICENSE.md : Terms of use for this application.
  • index.html : Our main html page, requiring our app code and containing the app structure.
  • server.js : The simple server we’ll run our application from.

CesiumJS is compatible with all modern JavaScript libraries and frameworks, so feel free to experiment. Here’s a few curated examples:

Page structure

Now take a look at index.html. This creates a div for our Cesium widget and a few basic input elements. Observe that Cesium Widget is just an ordinary div that can be styled and interacted with like any other div.

There are a few crucial lines to set this up:

Include CesiumJS

First we include Cesium.js in a script tag in the HTML head. This defines the Cesium object, which contains the entire CesiumJS library.

<script src="ThirdParty/Cesium/Cesium.js"></script>

Optionally, you can include individual modules from the Cesium source ThirdParty/Cesium/Source/. This is preferred for a production application as it will reduce the overall size of the deployed application. Here, we include the entire module for flexibility and easy experimentation with the API.

Structure the HTML

In the HTML body, we create a new div for the Cesium Viewer widget.

<div id="cesiumContainer"></div>

To include our application code which will activate the Cesium Viewer, in another script tag we add the JavaScript for the app at the end of the HTML body.

<script src="Source/App.js"></script>
Add styling

Let’s add some styling to our app’s html elements. Here, we’ve create an index.css file and added it within the <head> element. This will allow us to style our own html, as well as any html overlays within our Cesium Viewer.

<link rel="stylesheet" href="index.css" media="screen">

Cesium ships with a collection of widgets that require this CSS. We’ll include that before our app-specific CSS.

@import url(ThirdParty/Cesium/Widgets/widgets.css);

After, we have some sample css styling on our html div elements. Additional styling can be added to override the default Cesium CSS.

The Workflow

To follow along with this tutorial:

  1. Open Source/App.js in your favorite text editor and delete the contents.
  2. Copy the contents of Source/AppSkeleton.js into Source/App.js.
  3. Make sure your server is still running in the cesium-workshop directory, as described in Setup.
  4. Open your favorite web browser and navigate to localhost:8080. Most of us use Chrome, but Cesium runs in all modern web browsers. You should see a mostly black page now.
  5. As the tutorial directs you, uncomment code, save Source/App.js and refresh the page to see your new changes reflected.

Really stuck? You can follow along in sandcastle with a simplified version of the app (no UI):

Now let’s get started!

Creating the Viewer

The basis of any Cesium application is the Viewer, an interactive 3D globe with lots of functionality right out of the box. Create the viewer and attach it to the div with id "cesiumContainer" with the following line.

var viewer = new Cesium.Viewer('cesiumContainer');

There is a lot included in that one line! You should see a basic globe like this:

Cesium Viewer

By default, the scene handles mouse and touch input. Try exploring the globe using the default camera controls:

  • Left click and drag - Pans the camera over the surface of the globe.
  • Right click and drag - Zooms the camera in and out.
  • Middle wheel scrolling - Also zooms the camera in and out.
  • Middle click and drag - Rotates the camera around the point on the surface of the globe.

In addition to the globe itself, the Viewer comes with some helpful widgets by default.

Cesium Viewer

  1. Geocoder : A location search tool that flies the camera to queried location. Uses Bing Maps data by default.
  2. HomeButton : Flies the viewer back to a default view.
  3. SceneModePicker : Switches between 3D, 2D and Columbus View (CV) modes.
  4. BaseLayerPicker : Chooses the imagery and terrain to display on the globe.
  5. NavigationHelpButton : Displays the default camera controls.
  6. Animation : Controls the play speed for view animation.
  7. CreditsDisplay : Displays data attributions. Almost always required!
  8. Timeline : Indicates current time and allows users to jump to a specific time using the scrubber.
  9. FullscreenButton : Makes the Viewer fullscreen.

We can configure our viewer to include or exclude these features and more by passing in a options object as a parameter when we create it. For this application, delete that first line and configure a new viewer by uncommenting the next few lines:

var viewer = new Cesium.Viewer('cesiumContainer', {
    scene3DOnly: true,
    selectionIndicator: false,
    baseLayerPicker: false
});

This will create a viewer without selection indicators, base layer picker or scene mode picker widgets, since these will be unnecessary for our app. For the full set of Viewer options, see the Viewer documentation.

Cesium ion

Cesium ion is a platform for tiling and hosting 3D geospatial data which you can then add to your CesiumJS app. Here, we’ll be using Sentinal-2 imagery and Cesium World Terrain, both of which are hosted on ion.

To gain access to these and other terrain and imagery assets, we’ll first need to get an access token by signing up for a free Cesium ion account.

Go to https://cesium.com/ion/ and create a new account or sign in using a previous account.

Click on “Access Tokens” to navigate to the Access Tokens page.

Find your “Default” access token and copy the contents.

Cesium ion access tokens

Add the following line before the block creating the Cesium Viewer, replacing the access token with your own.

Cesium.Ion.defaultAccessToken = '<YOUR ACCESS TOKEN HERE>';

This will give your app access to all of your assets in Cesium ion.

For more information on uploading and processing your own data, take a look at Getting Started with Cesium ion.

Adding Imagery

The next key element of our Cesium application is imagery. This is the set of images that tile over our virtual globe at various resolutions. Depending on the camera’s orientation and distance from the globe’s surface, Cesium will requests and render imagery tiles at different levels of detail or “zoom level”.

Multiple imagery layers can be added, removed, ordered, and adjusted.

Cesium provides lots of methods for working with imagery layers, such as color adjustment and layer blending. Some code examples:

Cesium provides support for imagery from many different providers out of the box.

Supported Imagery Formats:

  • WMS
  • TMS
  • WMTS (with time dynamic imagery)
  • ArcGIS
  • Bing Maps
  • Google Earth
  • Mapbox
  • Open Street Map

Be careful, different data providers have different attribution requirements – make sure you have permission to use data from a particular provider, and use the credit option to attribute the sources accordingly.

By default, Cesium uses Bing Maps for imagery. The imagery packaged with the Viewer is mostly for demo purposes. Cesium requires you to create an ion account and generate an access key to use the imagery.

For this demo, we’re going to use the Sentinel-2 imagery in Cesium ion.

First, go to Cesium ion and add the Sentinel-2 imagery to your assets. Click “Asset Depot” in the navbar to navigate to the Asset Depot page.

Cesium ion Asset Depot

Click “Add to my assets”. Sentinel-2 will now be available in the list of assets when you go to the “My Assets” page, and will now be available to use in our app.

First, we create an IonImageryProvider, passing in the assetId that corresponds to the Sentinel-2 imagery. Then we add the ImageryProvider to viewer.imageryLayers.

// Remove default base layer
viewer.imageryLayers.remove(viewer.imageryLayers.get(0));

// Add Sentinel-2 imagery
viewer.imageryLayers.addImageryProvider(new Cesium.IonImageryProvider({ assetId : 3954 }));

With the above code additions, our application should look like this when you zoom in:

Imagery

For more information on Imagery, see our Imagery Layers Tutorial.

Adding Terrain

Cesium supports streaming and visualizing global high-resolution terrain and water effects for oceans, lakes, and rivers. Mountain peaks, valleys, and other terrain features really show the benefit of a 3D globe compared to a 2D map. Like imagery, the Cesium engine will stream terrain data from a server, only requesting and rendering tiles as needed based on the current camera position.

Here are some demos of terrain datasets and configuration options:

Supported Terrain Formats:

  • Quantized-mesh, an open format developed by the Cesium team
  • Heightmap
  • Google Earth Enterprise

In order to add terrain data, we create a CesiumTerrainProvider, specifying a url and a few configuration options, then assign the provider to viewer.terrainProvider.

Here, we’ll use the Cesium World Terrain tileset hosted on Cesium ion, which is included in “My Assets” by default. In this case, we can use the createWorldTerrain helper function to create the Cesium World Terrain tileset hosted on Cesium ion.

// Load Cesium World Terrain
viewer.terrainProvider = Cesium.createWorldTerrain({
    requestWaterMask : true, // required for water effects
    requestVertexNormals : true // required for terrain lighting
});

requestWaterMask and requestVertexNormals are optional configuration options which tell Cesium to request extra data for water and lighting effects. By default these are set to false.

Finally, now that we have have terrain, we need just one more line to make sure objects behind the terrain are correctly occluded. Only the front-most objects will be visible.

// Enable depth testing so things behind the terrain disappear.
viewer.scene.globe.depthTestAgainstTerrain = true;

We now have terrain and animated water. New York is pretty flat, so feel free to explore in order to see the new terrain in action. For a particularly obvious example, you can navigate to a more rugged area like the Grand Canyon or San Francisco.

Terrain

For more information on terrain, see the Cesium Terrain Tutorial.

Configuring the Scene

The next step is to add a little more setup to start our viewer in the right location and time. This involves interacting with viewer.scene, the class that controls all of the graphical elements in our viewer.

To start, we can configure our scene to enable lighting based on the sun’s position with this line.

// Enable lighting based on sun/moon positions
viewer.scene.globe.enableLighting = true;

This will make the lighting in our scene change with the time of day. If you zoom out, you’ll see that part of the globe is dark because the sun has set in that part of the world.

Next, before we get started with setting up our initial view, let’s go over a few basic Cesium types:

  • Cartesian3 : a 3D Cartesian coordinate – when used as a position it is relative to the center of the globe in meters using the Earth fixed-frame (ECEF)
  • Cartographic : a position defined by longitude, latitude (in radians) and height from the WGS84 ellipsoid surface
  • HeadingPitchRoll : A rotation (in radians) about the local axes in the East-North-Up frame. Heading is the rotation about the negative z axis. Pitch is the rotation about the negative y axis. Roll is the rotation about the positive x axis.
  • Quaternion : A 3D rotation represented as 4D coordinates.

These are the basic types necessary to position and orient Cesium objects within a scene and have a number of helpful conversion methods. See the documentation for each type to learn more.

Now let’s position the camera to view our scene in NYC, where our data is located.

Camera Control

The Camera is a property of viewer.scene and controls what is currently visible. We can control the camera by setting its position and orientation directly, or by using the Cesium Camera API, which is designed to specify camera position and orientation over time.

Some of the most commonly used methods are:

To get an idea of what the API can do, check out these camera demos:

Let’s try one of these methods by moving the camera to New York. Set the initial view with camera.setView(), using a Cartesian3 and a HeadingPitchRoll for position and orientation :

// Create an initial camera view
var initialPosition = new Cesium.Cartesian3.fromDegrees(-73.998114468289017509, 40.674512895646692812, 2631.082799425431);
var initialOrientation = new Cesium.HeadingPitchRoll.fromDegrees(7.1077496389876024807, -31.987223091598949054, 0.025883251314954971306);
var homeCameraView = {
    destination : initialPosition,
    orientation : {
        heading : initialOrientation.heading,
        pitch : initialOrientation.pitch,
        roll : initialOrientation.roll
    }
};
// Set the initial view
viewer.scene.camera.setView(homeCameraView);

The camera is now positioned and oriented to look down at Manhattan, and our view parameters are saved in a object that we can pass to other camera methods.

In fact, we can use this same view to update the effect of pressing the home button. Rather than having it return us to the default view of the globe from a distance, we can override the button to bring us to that initial view of Manhattan. We can adjust the animation by adding a few more options, then add an event listener that cancels the default flight, and calls flyTo() our new home view:

// Add some camera flight animation options
homeCameraView.duration = 2.0;
homeCameraView.maximumHeight = 2000;
homeCameraView.pitchAdjustHeight = 2000;
homeCameraView.endTransform = Cesium.Matrix4.IDENTITY;
// Override the default home button
viewer.homeButton.viewModel.command.beforeExecute.addEventListener(function (e) {
    e.cancel = true;
    viewer.scene.camera.flyTo(homeCameraView);
});

For more on basic camera controls, check out our Camera Tutorial.

Clock Control

Next, we can configure the viewer Clock and Timeline to control the passage of time within our scene.

Here’s the clock API in action.

When working with specific times, Cesium uses the JulianDate type, which stores the number of days since noon on January 1, -4712 (4713 BC). For increased precision, this class stores the whole number part of the date and the seconds part of the date in separate components. In order to be safe for arithmetic and represent leap seconds, the date is always stored in the International Atomic Time standard.

Here’s an example of how we can set up our scene time options:

// Set up clock and timeline.
viewer.clock.shouldAnimate = true; // make the animation play when the viewer starts
viewer.clock.startTime = Cesium.JulianDate.fromIso8601("2017-07-11T16:00:00Z");
viewer.clock.stopTime = Cesium.JulianDate.fromIso8601("2017-07-11T16:20:00Z");
viewer.clock.currentTime = Cesium.JulianDate.fromIso8601("2017-07-11T16:00:00Z");
viewer.clock.multiplier = 2; // sets a speedup
viewer.clock.clockStep = Cesium.ClockStep.SYSTEM_CLOCK_MULTIPLIER; // tick computation mode
viewer.clock.clockRange = Cesium.ClockRange.LOOP_STOP; // loop at the end
viewer.timeline.zoomTo(viewer.clock.startTime, viewer.clock.stopTime); // set visible range

This sets the rate of the scene animation, the start and stop times, and tells the clock to loop back to the beginning when it hits the stop time. It also sets the timeline widget to the appropriate time range. Check out this clock example code to experiment with clock settings.

That’s it for our initial scene configuration! Now when you run your application, you should see the following:

Initial Application

Loading and Styling Entities

Now that we’ve set the stage for our application with viewer configuration, imagery, and terrain, we can add the main focus of our application – the sample geocache data.

For easy visualization, Cesium supports popular vector formats GeoJson and KML, as well as an open format we developed specifically for describing a scene in Cesium called CZML.

Regardless of the initial format, all spatial data in Cesium is represented using the Entity API. The Entity API provides flexible visualization in a format that is efficient for Cesium to render. A Cesium Entity is a data object that can be paired with a styled graphical representation and positioned in space and time. The sandcastle gallery provides many examples of simple entities. To get up to speed on the basics of the Entity API, take a break from this application and read the Visualizing Spatial Data tutorial.

Here are examples of different entity types:

Once you’ve got a handle on what an Entity looks like, loading datasets with Cesium will be easy to understand. To read in a data file, create a DataSource appropriate to your data’s format, which will parse the data file hosted at a specified url and create an EntityCollection containing an Entity for each geospatial object in the dataset. DataSource just defines an interface – the exact kind of data source you’ll need will depend on the data format. For example, a KML uses a KmlDataSource. Here’s what it looks like:

var kmlOptions = {
    camera : viewer.scene.camera,
    canvas : viewer.scene.canvas,
    clampToGround : true
};
// Load geocache points of interest from a KML file
// Data from : http://catalog.opendata.city/dataset/pediacities-nyc-neighborhoods/resource/91778048-3c58-449c-a3f9-365ed203e914
var geocachePromise = Cesium.KmlDataSource.load('./Source/SampleData/sampleGeocacheLocations.kml', kmlOptions);

This code reads our sample geocache points from a KML file by calling KmlDataSource.load(optinos) with a few options. For a KmlDataSource, the camera and canvas options are required. The clampToGround option enables ground clamping, a popular display option that makes ground geometry entities like polygons and ellipses conform to terrain rather than curve to the WGS84 ellipsoid surface.

Since this data is loaded asynchronously, this returns a Promise to a KmlDataSource which will hold all our newly created entities.

If you’re not familiar with the Promise API for working with asynchronous functions, the “asynchronous” here basically means you should do what you need to do with the data in a callback function provided to .then. In order to actually add this collection of entities to the scene, we must wait until the promise resolves, then add the KmlDataSource to viewer.datasources. Uncomment the following lines:

// Add geocache billboard entities to scene and style them
geocachePromise.then(function(dataSource) {
    // Add the new data as entities to the viewer
    viewer.dataSources.add(dataSource);
});

These newly-created entities come with useful functionality by default. Clicking will display the Infobox with metadata related to the entity, and double-clicking zooms in and looks at the entity. To stop looking at the entity, click the home button or click the crossed out camera icon on the infobox. Next we’ll work on adding custom-styling to improve the look of our app.

For KML and CZML files, declarative styling can be built into the file. However, for this application, let’s practice manually styling our entities. To do this, we’ll take a similar approach to this styling example by waiting for our datasources to load, then iterating though all the entities in a datasource collection and modifying and adding attributes. Our geocache point markers are created as Billboards and Labels by default, so to modify the appearance of any of those entities, we do this:

// Add geocache billboard entities to scene and style them
geocachePromise.then(function(dataSource) {
    // Add the new data as entities to the viewer
    viewer.dataSources.add(dataSource);

    // Get the array of entities
    var geocacheEntities = dataSource.entities.values;

    for (var i = 0; i < geocacheEntities.length; i++) {
        var entity = geocacheEntities[i];
        if (Cesium.defined(entity.billboard)) {
            // Entity styling code here
        }
    }
});

We can improve the appearance of our markers by adjusting their anchor points, removing the labels to reduce clutter and setting the displayDistanceCondition so that only points within a set distance from the camera are visible.

// Add geocache billboard entities to scene and style them

        if (Cesium.defined(entity.billboard)) {
            // Adjust the vertical origin so pins sit on terrain
            entity.billboard.verticalOrigin = Cesium.VerticalOrigin.BOTTOM;
            // Disable the labels to reduce clutter
            entity.label = undefined;
            // Add distance display condition
            entity.billboard.distanceDisplayCondition = new Cesium.DistanceDisplayCondition(10.0, 20000.0);
        }

For more help with distanceDisplayCondition, see the sandcastle example.

Next, let’s improve the Infobox for each of our geocache entities. The title of the info box is the entity name, and the contents are the entity description, displayed as HTML.

You’ll notice that the default descriptions aren’t very helpful. Since we’re displaying geocache locations, let’s update them to display the longitude and latitude of our points.

First, we’ll convert the entity’s position into a Cartographic, then read the longitude and latitude from the Cartographic and add it to the description in an HTML table.

On click, our geocache point entities will now display a nicely-formatted Infobox with just the data we need.

// Add geocache billboard entities to scene and style them
        if (Cesium.defined(entity.billboard)) {
            // Adjust the vertical origin so pins sit on terrain
            entity.billboard.verticalOrigin = Cesium.VerticalOrigin.BOTTOM;
            // Disable the labels to reduce clutter
            entity.label = undefined;
            // Add distance display condition
            entity.billboard.distanceDisplayCondition = new Cesium.DistanceDisplayCondition(10.0, 20000.0);
            // Compute longitude and latitude in degrees
            var cartographicPosition = Cesium.Cartographic.fromCartesian(entity.position.getValue(Cesium.JulianDate.now()));
            var longitude = Cesium.Math.toDegrees(cartographicPosition.longitude);
            var latitude = Cesium.Math.toDegrees(cartographicPosition.latitude);
            // Modify description
            // Modify description
            var description = '<table class="cesium-infoBox-defaultTable cesium-infoBox-defaultTable-lighter"><tbody>' +
                '<tr><th>' + "Longitude" + '</th><td>' + longitude.toFixed(5) + '</td></tr>' +
                '<tr><th>' + "Latitude" + '</th><td>' + latitude.toFixed(5) + '</td></tr>' +
                '</tbody></table>';
            entity.description = description;
        }

Our geocache markers now should look like this:

App with Point Styling

For our geocaching application, it might also be helpful to visualize what neighborhood a particular point will fall into. Let’s try loading a GeoJson file containing polygons for each of the NYC neighborhoods. Loading a GeoJson file is ultimately very similar to the load process we just used for a KML. But in this case, we use a GeoJsonDataSource instead. And like with the previous datasource, we need to add it to viewer.datasources to actually add data to the scene.

var geojsonOptions = {
    clampToGround : true
};
// Load neighborhood boundaries from KML file
var neighborhoodsPromise = Cesium.GeoJsonDataSource.load('./Source/SampleData/neighborhoods.geojson', geojsonOptions);

// Save an new entity collection of neighborhood data
var neighborhoods;
neighborhoodsPromise.then(function(dataSource) {
    // Add the new data as entities to the viewer
    viewer.dataSources.add(dataSource);
});

Let’s style the neighborhood polygons we loaded. Just like with the billboard styling we just did, we begin by iterating through the neighborhood dataSource entities once the dataSource has loaded, this time checking that each entity’s polygon is defined:

// Save an new entity collection of neighborhood data
var neighborhoods;
neighborhoodsPromise.then(function(dataSource) {
    // Add the new data as entities to the viewer
    viewer.dataSources.add(dataSource);
    neighborhoods = dataSource.entities;

    // Get the array of entities
    var neighborhoodEntities = dataSource.entities.values;
    for (var i = 0; i < neighborhoodEntities.length; i++) {
        var entity = neighborhoodEntities[i];

        if (Cesium.defined(entity.polygon)) {
            // entity styling code here
        }
    }
});

Since we’re displaying neighborhoods, let’s rename each entity to use the neighborhood as its name. The original GeoJson file that we read in has neighborhood as a property. Cesium stores the GeoJson properties under entity.properties, so we can set the neighborhood names like this:

// entity styling code here

// Use geojson neighborhood value as entity name
entity.name = entity.properties.neighborhood;

Rather than leaving all our neighborhoods the same color, we can assign each polygon a new ColorMaterialProperty by setting the material to a random Color.

// entity styling code here

// Set the polygon material to a random, translucent color.
entity.polygon.material = Cesium.Color.fromRandom({
    red : 0.1,
    maximumGreen : 0.5,
    minimumBlue : 0.5,
    alpha : 0.6
});

// Tells the polygon to color the terrain. ClassificationType.CESIUM_3D_TILE will color the 3D tileset, and ClassificationType.BOTH will color both the 3d tiles and terrain (BOTH is the default)
entity.polygon.classificationType = Cesium.ClassificationType.TERRAIN;

Finally, let’s generate a Label for each entity with a few basic styling options. To keep things neat, we can use disableDepthTestDistance to have Cesium always render the labels in front of whatever 3D object might occlude it.

However, note that a Label is always positioned at entity.position. A Polygon is created with an undefined position since it has a list of positions that define the polygon border. We can generate a position by taking the center of the polygon positions:

// entity styling code here

// Generate Polygon position
var polyPositions = entity.polygon.hierarchy.getValue(Cesium.JulianDate.now()).positions;
var polyCenter = Cesium.BoundingSphere.fromPoints(polyPositions).center;
polyCenter = Cesium.Ellipsoid.WGS84.scaleToGeodeticSurface(polyCenter);
entity.position = polyCenter;
// Generate labels
entity.label = {
    text : entity.name,
    showBackground : true,
    scale : 0.6,
    horizontalOrigin : Cesium.HorizontalOrigin.CENTER,
    verticalOrigin : Cesium.VerticalOrigin.BOTTOM,
    distanceDisplayCondition : new Cesium.DistanceDisplayCondition(10.0, 8000.0),
    disableDepthTestDistance : 100.0
};

This gives us labelled polygons that look like this:

Labeled Polygons

Finally, let’s add a high-tech view of our nyc geocaches by adding a drone flight over the city.

Since a flight path is just a series of positions over time, we can add this data from a CZML file. CZML is a format for describing a time-dynamic graphical scene, primarily for display in a web browser running Cesium. It describes lines, points, billboards, models, and other graphical primitives, and specifies how they change with time. CZML is to Cesium what KML is to Google Earth, a standard format that allows for most Cesium features to be used via a declarative styling language (in this case a JSON schema)

Our CZML file defines an entity (visualized by default as a point) with its position defined as a series of positions at different time points. There are several property types in the Entity API that can be used for handling time dynamic data. See the below demo for an example:

// Load a drone flight path from a CZML file
var dronePromise = Cesium.CzmlDataSource.load('./Source/SampleData/SampleFlight.czml');

dronePromise.then(function(dataSource) {
    viewer.dataSources.add(dataSource);
});

The CZML file has Cesium display the drone flight using a Path, a property of the entity which displays its position over time. A path joins discrete samples into a continuous line to be visualized using interpolation.

Finally, let’s improve the look of our drone flight. First of all, rather then settling for a simple point, we can load a 3D model to represent our drone and attach it to the entity.

Cesium supports loading 3D models based on glTF (GL Transmission Format), an open-specification the Cesium team developed in conjunction with the Khronos group for efficiently loading 3D models by applications by minimizing file size and runtime processing. Don’t have a glTF model? We provide an online converter for converting COLLADA and OBJ files to the glTF format.

Let’s load a drone Model with nice physically-based shading and some animations:

var drone;
dronePromise.then(function(dataSource) {
    viewer.dataSources.add(dataSource);
    // Get the entity using the id defined in the CZML data
    drone = dataSource.entities.getById('Aircraft/Aircraft1');
    // Attach a 3D model
    drone.model = {
        uri : './Source/SampleData/Models/CesiumDrone.gltf',
        minimumPixelSize : 128,
        maximumScale : 1000,
        silhouetteColor : Cesium.Color.WHITE,
        silhouetteSize : 2
    };
});

Now our model looks nice, but unlike the original point, the drone model has orientation, which looks strange when the drone doesn’t turn as it moves forward. Fortunately, Cesium provides VelocityOrientationProperty that will automatically compute an orientation based on an entity’s positions sampled forward and backwards in time:

// Add computed orientation based on sampled positions
drone.orientation = new Cesium.VelocityOrientationProperty(drone.position);

Now our drone model will turn as expected.

There’s one more thing we can do to improve the look of our drone flight. It may not be obvious from a distance, but the drone’s path is made of linear segments that look unnatural – this is because Cesium uses linear interpolation to construct a path from sampled points by default. However, the interpolation options can be configured.

To get a smoother looking flight path, we can change the interpolation options like this:

// Smooth path interpolation
drone.position.setInterpolationOptions({
    interpolationDegree : 3,
    interpolationAlgorithm : Cesium.HermitePolynomialApproximation
});

Flightpath

3D Tiles

Our team sometimes describes Cesium as being like a 3D game engine for real world data. However, working with real world data is much more difficult than working with typical video game assets since real data can be incredibly high-resolution, and require accurate visualization. Fortunately, Cesium in collaboration with the open source community has developed 3D Tiles, an open specification for streaming massive heterogeneous 3D geospatial datasets.

Using a technique conceptually similar to Cesium’s terrain and imagery streaming, 3D Tiles make it possible to view gigantic models, including buildings datasets, CAD (or BIM) models, point clouds, and photogrammetry models, which would otherwise be impossible to view interactively.

Here are some 3D Tiles demos showcasing different formats:

In our application, we’ll use a Cesium3DTileset to add realism to our visualization by displaying full 3D models of all the buildings in New York! This New York City tilese is hosted in Cesium Ion and we can add it using IonResource.fromAssetId:

// Load the NYC buildings tileset
var city = viewer.scene.primitives.add(new Cesium.Cesium3DTileset({ url: Cesium.IonResource.fromAssetId(3839) }));

You may notice that the buildings are not correctly positioned at ground level. Fortunately it’s easy to fix. We can adjust the position of the tileset by modifying its modelMatrix.

We can find the model’s current offset from the ground by converting the tileset’s bounding sphere into a Cartographic, then adding the desired offset and resetting the modelMatrix:

// Adjust the tileset height so its not floating above terrain
var heightOffset = -32;
city.readyPromise.then(function(tileset) {
    // Position tileset
    var boundingSphere = tileset.boundingSphere;
    var cartographic = Cesium.Cartographic.fromCartesian(boundingSphere.center);
    var surface = Cesium.Cartesian3.fromRadians(cartographic.longitude, cartographic.latitude, 0.0);
    var offset = Cesium.Cartesian3.fromRadians(cartographic.longitude, cartographic.latitude, heightOffset);
    var translation = Cesium.Cartesian3.subtract(offset, surface, new Cesium.Cartesian3());
    tileset.modelMatrix = Cesium.Matrix4.fromTranslation(translation);
});

We now have over 1.1 million building models streaming into our scene!

3D Tiles also allows us to style parts of our tileset using the 3D Tiles styling language. A 3D Tiles style defines expressions to evaluate color (RGB and translucency) and show properties for a Cesium3DTileFeature, a part of the tileset such as an individual building in a city. Styling is often based on the feature’s properties stored in the tile’s batch table. A feature property can be anything like height, name, coordinates, construction date, etc. but is built into the tileset asset. Styles are defined with JSON and expressions written in a small subset of JavaScript augmented for styling. Additionally the styling language provides a set of built-in functions to support common math operations.

A Cesium3DTileStyle is defined like this:

var defaultStyle = new Cesium.Cesium3DTileStyle({
    color : "color('white')",
    show : true
});

This style simply will make all the buildings in our NYC tileset white and always visible. In order to actually set the tileset to use this style, we set city.style:

city.style = defaultStyle;

3D Tiles Styling

We can define as many styles as we’d like. Here’s another, making the building transparent:

var transparentStyle = new Cesium.Cesium3DTileStyle({
    color : "color('white', 0.3)",
    show : true
});

Transparent Styling on 3D Tiles

Applying the same style to every feature in our tileset is only scratching the surface. We can also use properties specific to each feature to determine styling. Here’s an example that colors buildings based on their height:

var heightStyle = new Cesium.Cesium3DTileStyle({
    color : {
        conditions : [
            ["${height} >= 300", "rgba(45, 0, 75, 0.5)"],
            ["${height} >= 200", "rgb(102, 71, 151)"],
            ["${height} >= 100", "rgb(170, 162, 204)"],
            ["${height} >= 50", "rgb(224, 226, 238)"],
            ["${height} >= 25", "rgb(252, 230, 200)"],
            ["${height} >= 10", "rgb(248, 176, 87)"],
            ["${height} >= 5", "rgb(198, 106, 11)"],
            ["true", "rgb(127, 59, 8)"]
        ]
    }
});

Style by Height

In order to swap between styles, we can add just a little more code to listen for HTML input:

var tileStyle = document.getElementById('tileStyle');
function set3DTileStyle() {
    var selectedStyle = tileStyle.options[tileStyle.selectedIndex].value;
    if (selectedStyle === 'none') {
        city.style = defaultStyle;
    } else if (selectedStyle === 'height') {
        city.style = heightStyle;
    } else if (selectedStyle === 'transparent') {
        city.style = transparentStyle;
    }
}

tileStyle.addEventListener('change', set3DTileStyle);

For more examples of 3D Tiles and how to use and style them, check out the 3D Tiles sandcastle demos.

3D Tiles demos:

If you have data and need help converting it to 3D tiles, stay tuned for updates about the Cesium ion platform! Subscribe for updates here.

Interactivity

Finally, let’s add some mouse interactivity. To improve the visibility of our geocache markers, we can change their styling when a user hovers over a marker to highlight it.

To achieve this, we’ll use picking, a Cesium feature that returns data from the 3D scene given a pixel position on the viewer canvas.

There are several different types of picking.

  • Scene.pick : returns an object containing the primitive at the given window position.
  • Scene.drillPick : returns a list of objects containing all the primitives at the given window position.
  • Globe.pick : returns the intersection point of a given ray with the terrain.

Here are some examples of picking in action:

Since we want our highlight effect to trigger on hover, first we’ll need to create a mouse action handler. For this we’ll use a ScreenSpaceEventHandler, a set of handler that triggers specified functions on user input actions. ScreenSpaceEventHandler.setInputAction()](/Cesium/Build/Documentation/ScreenSpaceEventHandler.html#setInputAction) listens for a type of user action -- a [ScreenSpaceEventType`, and runs a specific function, passing in the user action as a parameter. Here, we’ll pass it a function that take movement as input:

var handler = new Cesium.ScreenSpaceEventHandler(viewer.scene.canvas);
handler.setInputAction(function(movement) {}, Cesium.ScreenSpaceEventType.MOUSE_MOVE);

Next let’s write our highlighting function. The handler will pass in a mouse movement from which we can extract a window position to use with pick(). If the pick returns a billboard object, we know that we’re hovering over a marker. Then, using what we learned about Entity styling, we can apply a highlight style.

// If the mouse is over a point of interest, change the entity billboard scale and color
handler.setInputAction(function(movement) {
    var pickedPrimitive = viewer.scene.pick(movement.endPosition);
    var pickedEntity = (Cesium.defined(pickedPrimitive)) ? pickedPrimitive.id : undefined;
    // Highlight the currently picked entity
    if (Cesium.defined(pickedEntity) && Cesium.defined(pickedEntity.billboard)) {
        pickedEntity.billboard.scale = 2.0;
        pickedEntity.billboard.color = Cesium.Color.ORANGERED;
    }
}, Cesium.ScreenSpaceEventType.MOUSE_MOVE);

This successfully triggers the highlight style change for markers. However, you’ll notice that the markers stay highlighted when we move the cursor away. We can fix that by keeping track of the last marker that was highlighted, and restoring the original styling.

Here’s the full function, with the marker highlighting and unhighlighting working:

// If the mouse is over a point of interest, change the entity billboard scale and color
var previousPickedEntity = undefined;
handler.setInputAction(function(movement) {
    var pickedPrimitive = viewer.scene.pick(movement.endPosition);
    var pickedEntity = (Cesium.defined(pickedPrimitive)) ? pickedPrimitive.id : undefined;
    // Unhighlight the previously picked entity
    if (Cesium.defined(previousPickedEntity)) {
        previousPickedEntity.billboard.scale = 1.0;
        previousPickedEntity.billboard.color = Cesium.Color.WHITE;
    }
    // Highlight the currently picked entity
    if (Cesium.defined(pickedEntity) && Cesium.defined(pickedEntity.billboard)) {
        pickedEntity.billboard.scale = 2.0;
        pickedEntity.billboard.color = Cesium.Color.ORANGERED;
        previousPickedEntity = pickedEntity;
    }
}, Cesium.ScreenSpaceEventType.MOUSE_MOVE);

That’s it! We’ve now successfully added a mouse movement handler and on hover behavior for our marker entities.

Marker Entities

Camera Modes

To show off our drone flight, let’s experiment with camera modes. We’ll keep it simple with two basic camera modes that users can toggle between.

  • Free Mode : default camera controls
  • Drone Mode : have the camera follow the drone through its flight at a fixed distance away

No code is necessary for free mode, since it uses the default controls. As for the drone follow mode, we can position the camera looking at the drone with an offset using the viewer’s built in entity tracking functionality. This sets the camera to be at a fixed offset from a specified entity, even as it moves. To track an entity, we simply set viewer.trackedEntity.

To switch back to the free camera mode, we can just set viewer.trackedEntity back to undefined, then use camera.flyTo() to return to our home view.

Here’s the camera mode function:

// Create a follow camera by tracking the drone entity
function setViewMode() {
    if (droneModeElement.checked) {
        viewer.trackedEntity = drone;
    } else {
        viewer.trackedEntity = undefined;
        viewer.scene.camera.flyTo(homeCameraView);
    }
}

In order to attach this to the HTML input, we can attach this function to change events on the appropriate elements:

var freeModeElement = document.getElementById('freeMode');
var droneModeElement = document.getElementById('droneMode');

// Create a follow camera by tracking the drone entity
function setViewMode() {
    if (droneModeElement.checked) {
        viewer.trackedEntity = drone;
    } else {
        viewer.trackedEntity = undefined;
        viewer.scene.camera.flyTo(homeCameraView);
    }
}

freeModeElement.addEventListener('change', setCameraMode);
droneModeElement.addEventListener('change', setCameraMode);

Finally, entities are tracked automatically when a user double-clicks on them. We can add some handling to automatically update the UI if the user starts tracking the drone via clicking:

viewer.trackedEntityChanged.addEventListener(function() {
    if (viewer.trackedEntity === drone) {
        freeModeElement.checked = false;
        droneModeElement.checked = true;
    }
});

That’s it for our two camera modes – we can now freely switch to a drone camera view that looks like this:

Drone Mode

Extras

The rest of the code just adds a few extra visualization options. Similar to our previous interactions with the HTML elements, we can attach listener functions to toggle shadows, and the neighborhood polygon visibility.

Let’s start by creating an easy way to toggle the neighborhood polygons. In general, we can hide entities by setting visibility with Entity.show However, this only sets visibility for a single entity, and we’d like to hide or show all the neighborhood entities at once.

We can do this by adding all our neighborhood entities to a parent entity, as shown in this example or by simply using the show property of EntityCollection. We can then set visibility for all the child entities at once by changing the value of neighborhoods.show:

var neighborhoodsElement =  document.getElementById('neighborhoods');

neighborhoodsElement.addEventListener('change', function (e) {
    neighborhoods.show = e.target.checked;
});

We can do something similar for toggling the visiblity of shadows:

var shadowsElement = document.getElementById('shadows');

shadowsElement.addEventListener('change', function (e) {
    viewer.shadows = e.target.checked;
});

Finally, since the 3D Tiles may take not load instantaneously, we can also add a loading indicator that is removed only when the tileset has loaded (and hence the promise has resolved).

// Finally, wait for the initial city to be ready before removing the loading indicator.
var loadingIndicator = document.getElementById('loadingIndicator');
loadingIndicator.style.display = 'block';
city.readyPromise.then(function () {
    loadingIndicator.style.display = 'none';
});

Next Steps

Congratulations! You have successfully completed your CesiumJS application! Feel free to explore and experiment with the code we’ve provided here to further your Cesium education. We’re excited to welcome you to the Cesium community and look forward to seeing the amazing apps you build with the CesiumJS library.

So what’s next?

Development Resources

For this tutorial and throughout the rest of your Cesium development career, we encourage you to rely on the following resources:

  • Reference Documentation : A complete guide to the Cesium API containing many code snippets.
  • Sandcastle : A live-coding environment with a large gallery of code examples.
  • Tutorials : Detailed introductions to areas of Cesium development.
  • Cesium Forum : A resource for asking and answering Cesium-related questions.

Any time you get stuck, odds are one of these resources will have the answers you’re looking for.

Showcase your work on cesiumjs.org

We love sharing all of the incredible apps the Cesium community builds. Developers around the world have created more interesting applications than we could have ever imagined! Once your application is ready to share with the world, get in touch with us about featuring your app on the CesiumJS Demos Page. See this blog post for information about submitting your application for a showcase.

Discover and process content on Cesium ion

Cesium ion is new commercial platform composed of web services and tools to complement CesiumJS’s visualization, creating a complete 3D mapping platform. The Cesium team is particularly excited about ion since it is our first product built around CesiumJS to support developing open-source CesiumJS.

Our vision for ion includes subscriptions for

  • 3D content, such as imagery, terrain, 3D tilesets, and glTF models curated from open data and commercial data providers;
  • 3D tiling and hosting of your own massive datasets, such as imagery, terrain, photogrammetry, point clouds, BIM, CAD, 3D buildings, and vector data;
  • Analytics, such as measurement tools, volume and visibility computations, and terrain profiles; and
  • Map making and sharing workflows for creating 3D maps without coding.

Learn more on cesium.com.

Happy developing!