Cesium的一个完整示例 Workshop

杀马特。学长 韩版系。学妹 提交于 2020-08-11 17:27:09

概述

欢迎您了解Cesium!我们很高兴有你。为了让您尽快开发自己的网络地图应用程序,本教程将引导您逐步开发一个简单的Cesium应用程序。本教程将涉及Cesium API的许多重要方面,但不是全面的(Cesium有很多功能!)。我们的目标是介绍您需要的基础知识和工具,以探索Cesium的其余部分。

我们将创建一个简单的应用程序,用于可视化纽约市的示例geocache位置。我们将加载和设置多种类型的二维和三维数据,并创建多个摄像机和显示选项,供用户交互设置。最后,作为高科技的geocachers,我们将加载一个3D无人驾驶飞机模型来侦察geocache的位置,充分利用我们的3D可视化。

在本教程结束时,您将对Cesium的功能进行工作概述,并了解如何配置Cesium查看器,加载数据集,创建和设置几何图形,使用3D Tiles,控制摄像头以及为应用程序添加鼠标交互功能。

Finished app

geocache位置交互式可视化示例

安装Cesium

只需要几个设置步骤,然后就能开发。

  1. 通过访问 Hello World确保您的系统兼容Cesium . 无法看到地球的话?请参阅故障排除.
  2. 安装 Node.js.
  3. 获取 本教材源代码cesium-workshop, 拷贝或下载zip并提取内容.
  4. 在您的cmd控制台窗口中,切换至 cesium-workshop 目录.
  5. cmd运行 npm install.
  6. cmd运行 npm start.

控制台应该提示你"Cesium开发服务器在本地运行。请访问http://localhost:8080/"。不要关闭控制台!我们需要保持这个过程运行。

接下来,在浏览器中输入地址localhost:8080。 你应该看到我们的Cesium示例应用程序正在运行。如果出问题了?请至快速开始教程 去了解更多的Cesium安装。

 

应用程序目录

现在浏览我们的应用程序目录!请注意,这个应用程序目录设计得尽可能简单,完全忽略了当今使用的各种各样的现代JS框架。 但是,一旦你掌握了一些更好的框架或类库,请随时尝试!

  • Source : 我们的应用程序代码和数据。
  • ThirdParty : 外部库,在这种情况下只是Cesium.
  • LICENSE.md :应用程序的使用条款.
  • index.html : 我们的主要html页面。
  • server.js : 将运行我们的应用程序的服务处理js

现在看看 index.html. 我们的Cesium Widget小部件和一些基本的输入元素创建了一个div 。 使用Cesium只是一个普通的 div 可以像其他任意div一样可以设置他的样式或事件.

有几条重要代码可以设置:

首先,在在HTML头中包含一个 Cesium.js 脚本标签,这定义了Cesium包含整个Cesium库的对象。

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

 

Cesium附带的Wideget部件需要导入以下的css文件

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

在HTML的内容中,我们为Cesium Viewer部件创建一个div

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

最后,在另一个script标签中,我们在HTML的末尾为这个程序增加一个js文件。

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

That’s about it! 剩下的HTML内容用于在使用时接收用户输入的内容。

开发资源

对于本教程以及你的Censium开发工作的其他部分,我们鼓励你去使用相关的依赖资源:

当你陷入困境的时候,这些资源中的一种可能会得到你想要的答案。

工作教程

去跟随教程的指导学习:

  1. 在你最爱的编辑器内打开 Source/App.js并且删除里面的内容 。
  2. Source/AppSkeleton.js复制到Source/App.js中 , 其中包含了代码的注释版本。
  3. 确保你的服务一直时运行在 cesium-workshop 目录中, 像设置里的描述一样 Setup.
  4. 跳到 localhost:8080. 你可以看见一个大部分时黑色的页面。
  5. 本教程指导你, 取消注释的代码, 保存Source/App.js 和刷新页面以查看新的变化。

困惑?你可以在sandcastle中使用简化版的应用程序(没有UI):

现在让我们开始吧!

创建一个Viewer

任何Cesium应用的基础都是Viewer, 一个具有很多功能的交互式3D地球仪,通过取消第一行的注释来添加Viewer到 ‘cesiumContainer’ div中。

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

这一行有很多内容!你应该看到这样一个基本的地球:

Cesium Viewer

默认的,可通过触摸和鼠标来进行输入。可通过相机控制来对此地球进行探索:

  • 左键点击和拖动 - 将摄像机拖到地球表面。
  • 右键点击和拖动 - 放大和缩小。
  • 滚轮滚动 - 也是放大和缩小。
  • 中教案点击和拖动 - 将摄像机旋转到地球表面的点上。

除了地球本身之外,在默认情况下,Viewer还附带了一些有用的小部件,并在上面的图像中标记。

  1. Geocoder : 一种定位搜索工具,它可以让摄像机飞到查询位置。默认使用必应地图数据。
  2. Home Button : 将视野带回到默认窗口。
  3. Scene Mode Picker : 3D, 2D 和 Columbus视图(CV)模式之间的切换。
  4. Base Layer Picker : 选择在地球上展示的图像和地形图。
  5. Navigation Help Button :展示默认的相机控件
  6. Animation : 控制播放视图动画的动画速度。
  7. Timeline : 指示当前时间,允许用户跳转到特定的时间。
  8. Credits Display : 展示数据的属性,这总是被要求的。
  9. Fullscreen Button : 使视图全屏!

我们可以将Viewer配置为包含或排除这些特性,并在创建它时将一个选项对象作为参数传入。对于这个应用程序,删除第一行并通过取消下面几行注释来配置一个新的Viewer:

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

这将会创建一个没有选择指示器、基础层选择器或场景模式选择器的浏览器,因为这些对于我们的应用来说是不必要的,对于完整的Viewer选项,请查看 Viewer documentation.

添加Imagery

我们Cesium应用的下一个元素是图像, 这是一组图像,在我们的虚拟地球上以各种分辨率进行。为了提供最优的性能,Cesium只请求和渲染在当前视图中可见的并且在适当的分辨率(缩放级别)下的图像块,这与相机与地球表面的距离以及地球最大的最大屏幕空间误差maximumScreenSpaceError有关。

为了更直观地理解图像是如何工作的,请检查Cesium检查器。 Cesium Inspector.

Cesium提供了大量的工具来处理图像层,比如颜色调整和图层混合。以下代码示例:

Cesium 为许多不同的图像供应商many different providers 提供了支持。

支持的图像格式:

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

在默认情况下,Cesium会使用必应地图作为图像。请注意,不同的数据提供者有不同的属性要求——确保你有使用来自某个特定提供者的数据的权限,如果适用的话,可以将它们放在信用容器中。 与Viewer打包的图像主要是为了演示目的。 为了在我们的应用中使用必应图像, 我们必须拥有自己的Bing key 。用这样的一行设置Bing Key(在我们的应用程序的顶部,在查看器创建之前):

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

同样,不同的图像提供者将有不同的使用需求。现在我们有了使用这个图像集的权限,我们可以添加图像层。首先, 我们创建一个 ImageryProvider, 通过一个数据url和一些配置选项,然后我们将viewer.imageryLayers添加到viewer.imagery图层。

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

有了以上代码,我们的应用程序在放大时应该是这样的:

Imagery

这和默认的图像样式是一样的,但是可以随意使用bing的样式来查看差异。

有关图像的更多信息,请参阅我们的图像图层教程。Imagery Layers Tutorial.

添加Terrain

Cesium支持steaming和高分辨率的地形可视化以及水对海洋、湖泊、河流的影响。 与2D地图相比,山峰、山谷和其他地形特征确实显示了3D地球的好处。 和图像一样, Cesium会从服务器中传输地形数据,只需要根据当前的相机位置请求和渲染瓦片。

下面是一些地形数据集和配置选项的演示:

支持的地形格式:

  • 我们自己的 quantized-mesh 格式
  • Heightmap
  • Google Earth Enterprise

为了增加地形数据, 我们创建一个 CesiumTerrainProvider, 指定数据的url和部分配置参数, 然后设定 提供者为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
});

requestWaterMaskrequestVertexNormals 是配置参数,设定了Cesium额外的水和光照的效果,默认为false。

最后,既然我们已经有了地形,我们只需要再多一条线来确保在地形后面的物体被正确地遮挡。只有最前端的对象才可见。

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

我们现在有了地形和水。纽约是相当平坦的,所以你可以自由地探索,以便看到新的地形。对于一个特别明显的例子,你可以导航到一个更崎岖的区域,比如大峡谷或旧金山。

Terrain

更多的关于地形的信息,请参照Terrain Tutorial

配置Scene

现在我们再来设置一下我们的Viewer在正确的位置和时间。这涉及到和 viewer.scene的交互,用于查看Viewer中所有图形元素的容器。

首先,我们可以根据太阳的位置来配置我们的Scene。

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

这会使我们的场景亮度根据一天中时间的改变而改变,这样你就能看到地球的一部分在太阳离开的时候从太空进入黑暗。

接着,在我们开始初始化视图之前,让我们去认识下Cesium的类型:

  • Cartesian3 : 一个3D的笛卡尔点 - 当被用作位置时,它与地球的中心是相对的,使用地球固定框架(ECR)。
  • Cartographic : 由纬度/经度(弧度)定义的位置和地球表面的高度。
  • Heading Pitch Roll :一种旋转(在弧度上)关于在中轴上的局部轴的旋转。标题是关于负z轴的旋转。纵轴是负y轴的旋转。滚动是关于正x轴的旋转。
  • Quaternion : 3D通过旋转表示为4D坐标。

这些是在场景中定位和定位Cesium物体所必需的基本类型,并且有许多有用的转换方法。

现在让我们把我们的场景设在纽约市,我们的数据就在那里。

Camera Control(相机控件)

Cameraviewer.scene 的属性,控制当前可见的内容。我们可以通过设置正确的位置和方向来控制相机, 或者通过使用Cesium camera API来操作相机,该API可以灵活的指定相机的位置和方向。

以下是一些常用的方法:

要了解API可以做什么,请查看这些相机演示:

现在让我们尝试使用一种方法,把相机移到纽约。 设置初始化视图 camera.setView(), 使用Cartesian3和HeadingPitchRoll来确定位置和方向 :

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

相机现在定位并向下俯视曼哈顿,我们的视图参数被保存在一个可以传递给其他相机方法的对象中。

事实上,我们可以使用相同的视图来更新按home键的效果。我们不需要让它从远处返回地球的默认视图,我们可以重写这个按钮,让我们看到曼哈顿的最初景象。 我们可以通过添加更多的选项来调整动画,然后添加一个取消默认移动的事件侦听器,并调用flyTo()移动到新home视图。

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

更多关于相机控件的知识,请参照 Camera Tutorial.

Clock Control(时钟控件)

接着,我们可以在我们的场景中控制时间的流逝,通过配置 viewer ClockTimeline

这是相关的API: the clock API in action.

当在一个特殊的时间工作时, Cesium 使用 JulianDate 类型, 从January 1, -4712 (4713 BC)开始计算天数。 为了提高精确度,这个类将日期的整个数字部分存储在单独的组件中,为了保证算法的安全,并表示闰秒,该日期总是存储在国际原子时间标准中。

以下是设置时间的例子:

// 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

这将设置场景动画的速率、开始和结束时间,并告诉时钟在到达结束时循环。它还将时间轴设置为适当的时间范围。请查看这个时钟示例代码clock example code,以试验时钟设置 。

这就是我们最初的场景配置!现在,当运行应用程序时,你应该看到以下内容:

Initial Application

加载实体和为实体添加样式

现在,我们已经为我们的应用程序设置了查看器配置、图像和地形,我们可以继续添加我们的应用程序的主要部分,示例geocache数据。

对于简单的可视化操作, Cesium 支持 GeoJson 和 KML 格式, 也支持我们自己的向量格式CZML。

不管最初的格式是什么,Cesium所有空间数据都使用实体API表示, 一个几何图形库,它提供了一种灵活的可视化格式,这种格式对Cesium的渲染是非常有效的。 Cesium Entity 是一种数据对象,它可以与一种样式的图形表示相匹配,并在空间和时间中定位。sandcastle gallery提供了很多简单的例子. 为了在Entity API的基础上加快速度, 先暂停使用去都 可视化空间数据 资料。

这里有不同的实体类型:

一旦你掌握了一个实体的样子,装载数据集就很容易理解了。在数据文件中读取数据,创建适合你的数据格式的数据源 DataSource。 它将解析托管在指定url中的数据文件,并创建一个EntityCollection ,其中包含数据集中的每个地理空间对象的实体。 数据源只定义了一个接口——你需要的确切类型的数据源将依赖于数据格式。 例如, KML 使用 KmlDataSource。它看起来像:

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

该代码通过调用KmlDataSource.load 从KML文件读取我们的示例geocache点。加载几个选项。 对于KmlDataSource,需要使用相机和画布选项(用于处理网络链接)。 clampToGround选项支持ground clamping(贴近地面),这是一种很受欢迎的显示选项,可以让实体服从地形,而不只是椭球面。

由于该加载是异步处理的,他返回一个KmlDataSourcePromise, 该数据源将包含所有新创建的实体。

如果你不熟悉Promise API在异步加载时时如何工作的, 这里的“异步”基本上意味着你应该按照所提供的回调中的数据执行操作。 为了实际向场景中添加实体集合,我们必须等待返回的promise,然后将KmlDataSource添加到 viewer.datasources中。取消以下注释:

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

这些新创建的实体默认提供了有用的功能。尝试点击(显示信息框)和双击(放大)在一个点或附近。接下来,我们将添加定制样式来改进我们的应用程序的外观。

对于KML和CZML文件,可以将声明式样式构建到文件中。但是,对于这个应用程序,让我们手动地对实体进行样式设置。 为此,我们将采用类似的方法来处理这个样式的示例,等待我们的数据源加载,然后遍历数据源集合中的所有实体,并修改和添加属性。 默认情况下,我们的geocache点标记是作为Billboards创建的,因此为了修改这些实体的外观,我们做了如下的工作:

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

我们可以通过调整锚点,去掉标签来减少混乱,设置displayDistanceCondition,从而提高我们的标记的外观,这样在设置的相机的距离内就可以看到。

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

关于distanceDisplayCondition更多, 请看sandcastle example.

然后,去改变geocache实体的infobox 。 信息框的标题是实体名称,内容是实体描述,显示为HTML。

你会注意到,默认的描述并不是很有用。因为我们在显示geocache位置,所以更新它们来显示我们的点的经度和纬度。

首先,我们将把实体的位置转换为一个地图,然后从地图上读取纬度和经度,并将其添加到HTML表中的描述中。

在单击时,我们的geocache point实体将会显示一个拥有我们需要的数据并且格式良好的信息框。

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

我们的geocache标记现在会像下面这样显示出来:

App with Point Styling

对于我们的geocahe的应用, 也可以帮助你想象一个特定的点会落在什么地方。 让我们尝试为每个纽约市社区加载一个包含多边形的GeoJson文件。 加载一个GeoJson文件最终非常类似于我们刚刚为KML所使用的加载过程。 如果这样, 用一个GeoJsonDataSource,并将相机和画布选项排除在外 。 和前面的数据源一样,我们需要将它添加到viewer中。数据源实际上是将数据添加到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);
});

由于我们的geocache点已经看起来很干净,所以我们来设计一下我们所加载的邻域多边形。就像我们刚刚做的billboard 样式一样,当数据源加载后,我们开始遍历邻居的数据源实体,这一次检查每个实体的多边形的定义:

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

因为我们在显示相邻区域,所以让我们使用它的名称重命名每个实体。我们所读的GeoJson文件具有的相邻的属性。 Cesium储存了 entity.properties下的Geojson属性, 所以我们可以这样设置相邻区域的名称:

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

不让我们所有的相邻区域有相同的颜色, 我们可以通过将material设置为随机Color(颜色)来为每个多边形赋予一个新的ColorMaterialProperty

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

最后,让我们为每个带有一些基本样式选项的实体生成一个Label (标签)。 为了保持整洁,我们可以使用 disableDepthTestDistance让Cesium呈现在任何可以挡得住它的3D物体之前。

但是,标签总是位于Entity.position。 一个多边形是由一个点位置的列表创建的,因为它有一个组成。我们可以通过多边形位置的中心来生成一个多边形:

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

这使多边形看起来是这样的:

Labeled Polygons

虽然我们的标签多边形现在已经很好地设计了,但整个场景已经变得有点混乱,所以让我们创建一个隐藏多边形的方法。通常,我们可以通过使用entity.showEntity.show设置可见性来隐藏实体。 然而,这只会为单个实体设置可见性,我们想要同时隐藏或显示所有的相邻的实体。

我们可以通过将所有的邻居实体添加到父实体中,如 this example中所示,或者仅使用实体集合的show属性。然后,我们可以通过改变 neighborhoods.show的值来设置所有子实体的可见性。

    neighborhoods.show = false;

最后,让我们通过在城市上空增加一架无人机。

因为飞行轨迹是动态的,所以我们可以加载一个CZML 文件。 CZML是一种描述时间与动态图形场景的关系的格式。主要运行在web浏览器上,它描述了线、点、billboard、模型和其他图形,并指定了它们是如何随时间变化的。 CZML is to Cesium what KML is to Google Earth,一种允许大多数Cesium特性通过声明性样式语言使用的标准格式(在本例中是JSON模式)。

我们的CZML文件定义了一个实体(默认为一个点),它的位置被定义为在不同时间点上的一系列位置。它是实体API用来存储动态值的属性系统的一个很好的例子。

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

在这种情况下,CZML文件中使用PathGraphics来显示无人机飞行,这是一个实体的属性,随着时间的推移显示它的位置。将离散的样本连接到一条连续的直线上形成路径,用插值来可视化。

现在,我们已经拥有了完成纽约市geocache应用程序所需的所有数据,所有这些数据都是在我们的场景中加载和样式化的。

最后,让我们改进无人机飞行的外观。首先,我们可以加载一个3D模型来表示我们的无人机,并将其附加到实体上。

Cesium支持基于glTF(GL传输格式)加载3D模型,无税版规定,有效地传输和加载3D 场景和模型,通过应用程序, 最小化文件大小和运行时间进程。 没有glTF吗?我们提供了一个online converter(在线转换程序),用于将COLLADA和OBJ文件转换为glTF。

让我们来加载一个拥有物理阴影和一些动画效果的无人机Model(模型):

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

现在我们的模型看起来已经很好了,但是与开始的观点不同,无人机模型有方向,当无人机在移动时不会转弯,这看起来很奇怪。 幸运的是,Cesium提供VelocityOrientationProperty自动计算方向,它可以根据当前的时间和位置来自动计算向前或向后的移动方向。

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

现在,我们的无人机模型将会按照我们预期的来运行。

我们还可以做一件事来改善无人机飞行的外观。 从远处看,这可能不是很明显,但无人机的路径是由看起来不自然的线性段组成的——这是因为Cesium利用线性插值来构造一条从采样点到默认值的路径。但是,可以配置插值选项。

为了获得更平滑的飞行路径,我们可以改变插值选项:

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

Flightpath

3D Tiles

我们的团队有时将Cesium描述为真实世界数据的3D游戏引擎。 然而,使用真实的世界数据比使用典型的视频游戏资源要困难得多,因为真实的数据可以非常高的分辨率,并且需要精确的可视化。 幸运的是,与开源社区合作的Cesium已经开发出了3D Tiles,这是一种开放的大规模异构三维地理空间数据集的规范open specification

利用概念上与Cesium的地形和图像相似的图像流技术,3D Tiles使得使用大型模型的视图变为一种可能, 包括建筑物的数据集、CAD(或BIM)模型、点云和摄影测量模型,否则不可能在交互中查看。

下面是一些展示不同格式的3D瓷砖演示:

在我们的应用中,我们将使用一个Cesium3DTileset,通过显示纽约所有建筑物的完整3D模型来增加我们的视觉效果。添加一个小方块很容易:

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

就像添加普通的3D模型一样,我们指定一个url来对数据进行检索,然后将对象添加到场景中。在本例中,我们将tileset添加到scene.primitive而不是scene.entity,因为3D Tiles还不是实体API的一部分。 maximumScreenSpaceError指定了一个给定的视图中Cesium的具体细节,数字越低,视觉效果就越好。 高细节的视觉效果当然是伴随着性能成本的,所以当你改变这个设置时wbe很清楚。

你可能会注意到建筑物在地面上没有正确的位置。这是3D tilesets面临的一个常见问题,幸运的是,它很容易修复。 我们可以通过修改modelMatrix模型矩阵来调整位置。

我们可以通过将tileset的边界球体转换成一个Cartographic(地图),然后添加所需的偏移量和重置模型矩阵来找到模型当前的偏移量。

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

我们的场景中有超过110万的建筑模型!

3D Tiles还允许我们使用 3D Tiles styling language来设定我们的样式部分。 3D Tiles风格定义了用来评估颜色(RGB和半透明)的表达式,并显示了一个Cesium3DTileFeature特性的属性。这是城市的一部分,就像城市里的个人建筑一样。 样式基于特征属性并且存储在 tile’s batch table中。特征属性可以是高度,名称,坐标,构建日期等,但它是建立在tileset上的。风格是用JSON和表达式定义的,在用于样式化的JavaScript的一小部分中编写。 此外,样式化语言提供了一组内置函数来支持常见的数学操作。

一个Cesium3DTileStyle 的定义如下所示:

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

这种风格会使我们纽约的所有建筑都变得白色,而且总是可见的。为了真正地让tileset使用这种风格,我们设置了city.style:

city.style = defaultStyle;

 

3D Tiles Styling

我们可以定义任意多的样式。这是另一种,让建筑变得透明:

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

Transparent Styling on 3D Tiles

我们还可以使用特定于每个特性的属性来确定样式。这里有一个例子,根据他们的高度来给建筑物颜色:

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

为了在样式之间进行切换,我们可以添加更多的代码来侦听HTML输入:

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

要了解更多3D Tiles的例子,以及如何使用和制作它们,请查看3D Tiles演示 the 3D Tiles sandcastle demos

3D Tiles demos:

如果你很好奇3D Tilesets是如何产生的,或者你有一些自己的数据可以转换,你可以在这里读到更多read more here

Interactivity(交互)

最后,让我们添加一些鼠标交互。为了提高我们的geocache标记的可见性,当用户在一个标记上停留以突出显示时,我们可以改变他们的样式。

为了实现这一点,我们将使用picking,它从3D场景中返回数据,并在查看器画布上显示一个像素位置。

有几种不同类型的选择。

  • Scene.pick : 在给定的窗口位置返回一个包含primitive的对象。
  • Scene.drillPick : 返回一个包含给定窗口位置的所有primitives的对象列表。
  • Globe.pick : 返回给定光线与地形的交点。

下面是一些例子:

由于我们希望在悬浮上触发高光效果,所以首先需要创建一个鼠标事件。使用ScreenSpaceEventHandler, 一组在用户输入操作上触发指定功能的处理程序。 ScreenSpaceEventHandler.setInputAction() 监听操作类型 ScreenSpaceEventType的种类, 并运行一个特定的函数,将用户操作作为参数传递。这里,我们将传递一个函数,在鼠标移动时触发:

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

下面我们来写一下我们的高亮显示函数。处理程序将传入鼠标移动事件,从中我们可以提取一个窗口位置,以便使用pick()。 如果pick返回一个billboard对象,我们知道我们在一个标记上。然后,使用我们所学到的Entity样式,我们可以应用一个突出显示样式。

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

这成功地触发了标记的高亮样式更改。但是,当我们将光标移开时,您会注意到这些标记仍然突出显示。我们可以通过跟踪高亮显示的最后一个标记来修复它,并恢复原来的样式。

以下是完整的功能代码,带有标记高亮和未高亮显示的工作:

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

就是这样!我们现在已经成功地为我们的标记实体添加了鼠标移动处理程序和悬停行为。

Marker Entities

Camera Modes(相机模式)

为了展示我们的无人机飞行,让我们来试验一下相机模式。我们将简单地使用两种基本的相机模式,用户可以在其中切换。

  • Free Mode : 默认的相机控件
  • Drone Mode :让摄像机跟随无人机在一个固定的距离之外飞行

free mode(自由模式)不需要任何代码,因为它使用默认控件。至于drone follow mode(无人机跟随模式),我们可以将摄像头定位在无人机上,并使用viewer内置的实体跟踪功能。这使得相机即使在它移动的时候,在一个指定的实体中也处于固定的偏移量。 为了跟踪一个实体,我们简单的设置viewer.trackedEntity.

为了切换到免费的相机模式,我们可以设置 viewer.trackedEntity来返回到未定义的,然后使用camera.flyTo()返回到我们的主视图。

这是相机模式功能:

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

为了将这个附加到HTML输入,我们可以附加这个函数来change更改元素上的事件:

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

最后,当用户双击这些实体时,将自动跟踪实体。如果用户开始通过点击来跟踪无人机,我们可以添加一些处理来自动更新用户界面:

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

这就是我们的两种相机模式——我们现在可以自由地切换到一个像这样的无人机摄像头视图:

Drone Mode

Extras

其余的代码只是添加了一些额外的可视化选项,类似于我们之前与HTML元素的交互,我们可以附加侦听器函数来切换阴影,以及附近的多边形可见性。

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

由于由于3D瓷砖可能不会立即加载,我们还可以添加一个加载指示器,只有在tileset加载时才会移除 。

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

祝贺你!这是我们的应用程序!现在你了解了完整的Cesium应用程序的开发过程。请随意探索和测试我们在这里提供的代码,以进一步Cesium的学习。

下一步呢?试着看一些教程和Cesium的例子,你可以用Cesium做的更多。请记住,Cesium不仅仅是一个API,它是一个社区!在论坛上与你的Cesium项目保持联系。

 

 

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!