Cesium Workshop

Contents

Overview

Welcome to the Cesium community! We’re happy to have you. In order to get you developing your own web map applications as soon as possible, this tutorial will walk you through the development of a simple Cesium application from beginning to end. This tutorial will touch on many of the most important aspects of the Cesium API, but is no means comprehensive (Cesium 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 for us to take full advantage of our 3D visualization.

By the end of this tutorial, you will have a working overview of Cesium’s features and know how to configure a Cesium viewer, load datasets, create and style geometry, work with 3D Tiles, control the camera, and add 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 Hello World. 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 tell you “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 Cesium setup.

The Application Directory

Now for a tour of our application directory! Note that this application directory was designed to be as simple as possible, and totally ignores the many varied modern JS frameworks in use today. But once you have a handle on things, feel free to experiment!

  • Source : Our application code and data.
  • ThirdParty : External libraries, in this case just Cesium.
  • LICENSE.md : Terms of use for this application.
  • index.html : Our main html page.
  • server.js : The server we’ll run our application from.

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:

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

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

Cesium ships with a collection of widgets that require this CSS.

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

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

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

Finally, in another script tag we add the JavaScript for the app at the end of the HTML body.

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

That’s about it! The rest of the HTML in this file is for collecting user input, which we’ll use later.

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.

The Workflow

To follow along with this tutorial:

  1. Open Source/App.js in your favorite text editor and delete the contents.
  2. Copy into Source/App.js the contents of Source/AppSkeleton.js , which contains the commented version of the code.
  3. Make sure your server is still running in the cesium-workshop directory, as described in Setup.
  4. Navigate to localhost:8080. 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. Add the viewer to the ‘cesiumContainer’ div by uncommenting the first line.

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

There’s 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, labelled in the above image.

  1. Geocoder : A location search tool that flies the camera to queried location. Uses Bing Maps data by default.
  2. Home Button : Flies the viewer back to a default view.
  3. Scene Mode Picker : Switches between 3D, 2D and Columbus View (CV) modes.
  4. Base Layer Picker : Chooses the imagery and terrain to display on the globe.
  5. Navigation Help Button : Displays the default camera controls.
  6. Animation : Controls the play speed for view animation.
  7. Timeline : Indicates current time and allows users to jump to a specific time using the scrubber.
  8. Credits Display : Displays data attributions. Almost always required!
  9. Fullscreen Button : 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.

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. To provide optimal performance, Cesium only requests and renders imagery tiles that are visible in the current view and that are at a resolution (also known as zoom level) appropriate to the camera’s distance from the globe’s surface and the globe’s maximumScreenSpaceError.

For a more visual understanding of how imagery works, check out the Cesium Inspector.

Cesium provides lots of tools 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 servers
  • Single tile

By default, Cesium uses Bing Maps for imagery. Be careful, different data providers have different attribution requirements – make sure you have permission to use data from a particular provider, crediting them in the credits container if applicable. The imagery packaged with the Viewer is mostly for demo purposes. In order to use the Bing imagery set in our application, we need to acquire a Bing key of our own. Set the Bing key with a line like this (at the top of our application, before the viewer is created):

Cesium.BingMapsApi.defaultKey = 'AsarFiDvISunWhi137V7l5Bu80baB73npU98oTyjqKOb7NbrkiuBPZfDxgXTrGtQ'; // For use in this application only. Do not reuse!

Again, different imagery providers will have different requirements for usage. Now that we have permission to use this imagery set, we can actually add the imagery layer. First, we create an ImageryProvider, passing in a data url and a few configuration options, then we add the ImageryProvider to viewer.imageryLayers.

// Add Bing imagery
viewer.imageryLayers.addImageryProvider(new Cesium.BingMapsImageryProvider({
    url : 'https://dev.virtualearth.net',
    mapStyle: Cesium.BingMapsStyle.AERIAL // Can also use Cesium.BingMapsStyle.ROADS
}));

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

Imagery

This is actually the same as the default imagery styling, but feel free to play with the BingMapsStyle to see the differences.

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:

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

// Load STK World Terrain
viewer.terrainProvider = new Cesium.CesiumTerrainProvider({
    url : 'https://assets.agi.com/stk-terrain/world',
    requestWaterMask : true, // required for water effects
    requestVertexNormals : true // required for terrain lighting
});

requestWaterMask and requestVertexNormals are 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 Terrain Tutorial.

Configuring the Scene

Now just a little more setup to start our viewer in the right location and time. This involves interacting with viewer.scene, a container for all the graphical elements in our viewer.

To start, we can configure our scene to optionally 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, such that you can see part of the globe go dark from space when the sun is out of view.

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

  • Cartesian3 : a 3D Cartesian point – when used as a position it is relative to the center of the globe in meters using the Earth fixed-frame (ECR)
  • Cartographic : a position defined by latitude/longitude (in radians) and height off the globe’s surface
  • Heading Pitch Roll : 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.

Now let’s position 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 flexibly 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:

Now 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, and 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; // default
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 will set the rate of the scene animation, the start and end times and tell the clock to loop when it hits the end time. It also sets the timeline 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 go ahead and add the main focus of our application, sample geocache data.

For easy visualization, Cesium supports popular vector formats GeoJson and KML, as well as our own vector format, CZML.

Regardless of the initial format, all spatial data in Cesium are represented using the Entity API, a geometry library that 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 Visualizing Spatial Data first.

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 with a few options. For a KmlDataSource, the camera and canvas options are required (necessary for working with network links). The clampToGround option enables ground clamping, a popular display option that moves entities to conform to terrain rather than just the ellipsoid surface.

Since this load is handled 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 loads, the “asynchronous” here basically means you should do what you need to do with the data in a callback 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. Try clicking (display infobox) and double-clicking (zoom in) on a point or neighborhood. 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 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 latitude and longitude of our points.

First, we’ll convert the entity’s position into a Cartographic, then read the latitude and longitude 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 latitude and longitude in degrees
            var cartographicPosition = Cesium.Cartographic.fromCartesian(entity.position.getValue(Cesium.JulianDate.now()));
            var latitude = Cesium.Math.toDegrees(cartographicPosition.latitude);
            var longitude = Cesium.Math.toDegrees(cartographicPosition.longitude);
            // Modify description
            var description = '<table class="cesium-infoBox-defaultTable cesium-infoBox-defaultTable-lighter"><tbody>';
            description += '<tr><th>' + "Latitude" + '</th><td>' + latitude + '</td></tr>';
            description += '<tr><th>' + "Longitude" + '</th><td>' + longitude + '</td></tr>';
            description += '</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 exclude the camera and canvas options. 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);
});

Since our geocache points are already looking clean, 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
});

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 constituent point positions. We can generate a polygon 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 : Number.POSITIVE_INFINITY
};

This gives us labelled polygons that look like this:

Labeled Polygons

Although our labelled polygons are now nicely styled, our entire scene has become a bit cluttered, so let’s make create a way to hide the 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 Entity Collection. We can then set visibility for all the child entities at once by changing the value of neighborhoods.show:

    neighborhoods.show = false;

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. It’s a nice example of the Property system that the Entity API uses to store dynamic values.

// 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);
});

In this case, the CZML file has Cesium display the drone flight using a PathGraphics, 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.

We now have all the data we need to finish our nyc geocache application, all loaded and styled as entities within our scene.

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), a royalty-free specification for the efficient transmission and loading of 3D scenes and models by applications which minimizes file size and runtime processing. Don’t have a glTF? We provide an online converter for converting COLLADA and OBJ files to glTF.

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

var drone;
dronePromise.then(function(dataSource) {
    viewer.dataSources.add(dataSource);
    drone = dataSource.entities.values[0];
    // Attach a 3D model
    drone.model = {
        uri : './Source/SampleData/Models/CesiumDrone.gltf',
        minimumPixelSize : 128,
        maximumScale : 2000
    };
});

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. 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! Adding a tileset is easy:

// Load the NYC buildings tileset
var city = viewer.scene.primitives.add(new Cesium.Cesium3DTileset({
    url: 'https://beta.cesium.com/api/assets/1461?access_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJqdGkiOiJkYWJmM2MzNS02OWM5LTQ3OWItYjEyYS0xZmNlODM5ZDNkMTYiLCJpZCI6NDQsImFzc2V0cyI6WzE0NjFdLCJpYXQiOjE0OTkyNjQ3NDN9.vuR75SqPDKcggvUrG_vpx0Av02jdiAxnnB1fNf-9f7s',
    maximumScreenSpaceError: 16 // default value
}));

Like adding an ordinary 3D model, we specify a url from which to retrive our data, then add the object to the scene. In this case, we add the tileset to scene.primitives, not scene.entities because 3D Tiles is not yet part of the Entity API. The maximumScreenSpaceError specifies how much how much detail Cesium will render for a given view, so the lower the number, the more detailed the visuals. Highly detailed visuals of course come with a performance cost, so wbe aware when changing this setting.

You may notice that the buildings are not correctly positioned at ground level. This is a common problem with 3D tilesets, and 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’re curious about how 3D Tilesets are generated or have some data of your own to convert, you can read more 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() 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 actually 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.

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

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

neighborhoodsElement.addEventListener('change', function (e) {
    neighborhoods.show = e.target.checked;
    tileStyle.value = 'transparent';
    city.style = transparentStyle;
});

And 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';
});

Congratulations! That’s it for our app! You’ve now walked through the development of a complete Cesium application from end-to-end. Feel free to explore and experiment with the code we’ve provided here to further your Cesium education.

What’s next? Try looking at a few more tutorials and sandcastle examples for more ideas about what you can do with Cesium. And remember, Cesium is more than an API – it’s a community! Keep in touch about your Cesium projects on the forum.

Happy developing!