Ceisum官方教程2 -- 项目实例(workshop)
2020-03-05 17:27:22
1639次阅读
0个评论
最后修改时间:2020-03-17 16:11:31

原文地址:https://cesiumjs.org/tutorials/Cesium-Workshop/


概述


我们很高兴欢迎你加入Cesium社区!为了让你能基于Cesium开发自己的3d 地图项目,这个教程将从头到尾讲解一个基础的Cesium程序的开发过程。这个教程将用到很多重要的CesiumAPI,但是并不是所有的(CesiumJS有很多很多功能)。我们目标是教会你基于Cesium做开发的基本原则和工具,在你的项目里能举一反三,解决其他问题。


我们创建一个简单的程序去可视化纽约市的一些地理位置。我们将加载各种类型各种样式的二维和三维数据,并且创建若干个相机位置,并且展示一些用户交互的UI。最后,做为一个高科技地图,我们加载了一个无人机三维模型,充分利用3d可视化的优势去观察一些地理位置。


在完成教程后,你对Cesium的功能会有几个基本概念,包括配置viewer、加载数据、创建各种样式的几何体、使用3d tiles(三维模型切片)、控制相机、增加鼠标交互事件。


  

带交互的可视化纽约城地理位置


步骤


在开发前的几个必备步骤:


  1. 1.访问这个页面确认你的电脑环境适合Cesium Cesium Viewer. 如果没有看到地球? 点这个链接 Troubleshooting.

  2. 2.安装Node.js.

  3. 3.下载教程代码 workshop code。使用git clone 或者手动下载zip并解压缩。

  4. 4.在cmd命令行下,使用cd命令定位到cesium-workshop目录下.

  5. 5.运行npm install

  6. 6.运行npm start


控制台应该输出下面信息:


 

workshop运行


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


注意不能关闭控制台窗口,开发中需要保证这个进程运行着。


下一步, 在浏览器里打开 localhost:8080。你应该能看到我们的程序已经运行了。


注意


这个教程里提到的workshop是基于cesium1.45开发的,里面的地形服务器已经失效了,导致cesium加载并不成功,使用这个代码看不到效果。


    

workshop已经运行不起来了


解决方法也很简单,我们使用Cesium最新版1.51里的文件替换到如下目录


 

替换cesium库


再次刷新页面,就可以了,效果如下:


    

替换cesiumjs库后的加载效果

程序目录


在程序根目录下,有如下文件和文件夹. 这个程序已经被设计为尽可能的简单,只包含cesiumjs的库。


  • Source/ : 我们项目的代码。

  • ThirdParty/ : 外部js库,目前只包含cesium。

  • LICENSE.md : 我们项目的说明条款。

  • index.html : 主页,包含项目程序代码和页面结构。

  • server.js : 简单的基于nodejs的http服务器。


CesiumJS是完全兼容现代javascript 库和框架,所以放心大但的使用。
下面是一些示例:



页面结构


下来我们看看index.html。为cesium的控件创建div,以及一些输入元素。我们注意到,Cesium的控件就是一个普通的div,它可以被css样式设置,并且和其他div交互。
有一些关键的行:


引入CesiumJS


受限在html的标签内引用cesium.js。这个定义了Cesium对象,并且包含整个CesiumJS的库。

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


为了减小开发的项目最终的js文件大小,当然你也可以包含ThirdParty/Cesium/Source/目录下的独立的Cesium源码模块。不过我们为了简单的测试API,我们直接包含了整个CesiumJS库。


HTML结构


在HTML的body部分,有一个div为了创建Cesium控件。

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


为了在div创建成功后再执行其他代码,可以再HTML的body部分增加script标签去引用js文件。

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


页面样式


使用index.css文件定义了HTML元素的样式,可以在HTML的head元素里引用它。

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


Cesium的所有小控件下面这个CSS来定义样式。需要在index.css之前引用。

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


我们的页面已经有了基本样式,并且我们在index.css设定的样式可以覆盖Cesium默认的控件样式。


工作流程


步骤如下:


  1. 1. 使用你最擅长的文本编辑器(推荐sublime)打开Source/App.js,并且清空里面内容。

  2. 2. 把文件Source/AppSkeleton.js的内容拷贝到Source/App.js

  3. 3. 确认你的http服务还在cesium-workshop目录运行着。

  4. 4. 使用你的浏览器打开 localhost:8080.推荐使用chrome,但是现在浏览器都可以. 你应该能看到一个黑色背景。

  5. 5. 在代码里去掉注释,保存Source/App.js,刷新浏览器,应该有些效果改变了。


还有问题? 那你先跟着sandcastle去做一个没有UI的简单程序:



下来我们真正开始。


创建Viewer


Cesium的最基础对象就是 Viewer, 一个具有很多功能的3d地球的黑盒子. 使用下面的代码创建viewer并附着到id为 "cesiumContainer"`的div上。

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


这简单的一行代码实际包含了很多内容,成功后你应该能看见基础的地球,像下面一样:


 

基础地球


默认情况下这个场景能处理鼠标和触摸事件。 试下下面的相机控制方法:


左键单击和拖拽 - 沿着地球表面平移(调整相机位置).
右键单击和拖拽 - 相机放大缩小(调整相机距离).
滚轮 - 相机放大缩小(调整相机距离).
中间按下和拖拽 - 围绕地球表面旋转相机(调整相机方向)。


除了地球, Viewer还默认包含了一些有用的控件:


 

Cesium控件


  1. Geocoder : 地理位置查询定位控件,默认使用bing地图服务.

  2. HomeButton : 默认相机位置。

  3. SceneModePicker : 3D、2D和哥伦布模式的切换按钮.

  4. BaseLayerPicker : 选择地形、影像等图层。

  5. NavigationHelpButton : 显示默认的相机控制提示.

  6. Animation : 控制场景动画的播放速度.

  7. CreditsDisplay : 展示数据版权属性。

  8. Timeline : 时间滚动条。

  9. FullscreenButton : 全屏切换。


可以传递一个options对象做为配置参数,去控制上面这些控件的显示或者不显示。对于示例代码,删除第一行,打开后面几行的注释,代码如下:

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


这几行代码创建了一个不包含选择指示器(selection indicators),基础底图选择控件的viewer。

完整的options配置看文档Viewer


影像图层


影像是Cesium程序一个关键元素。它是覆盖在地表的各种不同精度的图像集合。根据相机的朝向和距离,Cesium将请求和渲染不同LOD或者缩放级别下的图像。
Cesium支持多个影像图层同时加载、删除、排序和调整。
Cesium为影像图层提供了大量方法,类似调整颜色、混合等。下面是Sandcastle中的一些示例代码:



Cesium提供了多种影像数据来源 多用影像数据源


支持的格式:


  • WMS

  • TMS

  • WMTS (with time dynamic imagery)

  • ArcGIS

  • Bing Maps

  • Google Earth

  • Mapbox

  • Open Street Map


Cesium默认使用Bing map的影像图层。这个影像图层经常用来做demo演示。为了使用这个影像,需要创建一个Cesium ion账户,并且生成一个访问token。
(译者注:考虑到国内的环境,修改了官方的示例,直接加载谷歌地图的影像)

 // 删除默认的影像图层
 viewer.imageryLayers.remove(viewer.imageryLayers.get(0));
 
 // 增加谷歌影像底图
 viewer.imageryLayers.addImageryProvider(new Cesium.UrlTemplateImageryProvider({
                    url: 'http://www.google.cn/maps/vt?lyrs=s&x={x}&y={y}&z={z}',
                    tilingScheme: new Cesium.WebMercatorTilingScheme()
                })
 );


运行后有如下效果:


 

添加谷歌底图效果


后续教程还有一篇专门讲影像图层的 影像图层教程.


地形图层


Cesium支持渐进流式加载和渲染全球高精度地形,并且包含海、湖、河等水面效果。相对2D地图,山峰、山谷等其他地形特征的更适宜在这种3D地球中展示。和影像图层一样,Cesium需要在服务端预先把地形数据处理为切片形式,在客户端基于当前相机位置去请求和渲染地形切片。


下面是一些示例和地形数据集以及配置选项:


支持的格式:


  • Quantized-mesh, Cesium团队定义的不规则地形三角网格式。

  • Heightmap

  • Google Earth Enterprise

    为了增加一个地形数据,我们需要创建一个 CesiumTerrainProvider, 设置一个url以及很少的几个配置项,然后把这个provider设置到 viewer.terrainProvider.


这里,我们使用 Cesium全球地形,这个数据存储在Cesium ion服务器上,已经默认到你的账户里的“My Assets”中。这种前提下,我们使用createWorldTerrain辅助函数去创建 Cesium全球地形 .

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


requestWaterMaskrequestVertexNormals两个选项都是可选的,他们告知Cesium去请求额外的水面数据和光照数据。 默认都为false.
最终,我们有了地形效果,我们可能需要再写一行代码,确保地形以下的物体不可见。


// 打开深度检测,那么在地形以下的对象不可见
viewer.scene.globe.depthTestAgainstTerrain = true;


纽约的地表非常平,可以漫游到其他地方去浏览. 为了明显看到效果,可以到珠峰附近去查看。


    

珠峰地形


后续有一个地形的详细教程 地形教程.


场景配置


为了我们的viewer的展示时间和空间正确,需要一些更多的配置。这部分主要和 viewer.scene打交道, 这个类控制了我们的viewer中所有的图形元素。
使用下面这句话,开启全球光照,光照方向依据太阳方向。

 // 开启全球光照
 viewer.scene.globe.enableLighting = true;


随着时间的变化,光照方向也在变换。如果缩小后,我们能看到一部分的地球是黑色的,因为这部分此时晚上。
在初始化视图之前,先学下基本的cesium 类型:


  • Cartesian3 : 三维笛卡尔(直角)坐标 – 当用来表示位置的时候,这个坐标指在地固坐标系(Earth fixed-frame (ECEF))下,相对地球中心的坐标位置,单位是米。

  • Cartographic :使用经纬度(弧度)和高度(WGS84地球高程)描述的三维坐标 。

  • HeadingPitchRoll :
    在ENU(East-North-Up)坐标系中,相对坐标轴的旋转(弧度)。Heading 相对负z轴(垂直向下). Pitch 相对负y轴. Roll相对正x轴.

  • Quaternion :使用四维坐标描述的三维旋转。
    这是在Cesium的scene中摆放对象的基本类型,Cesium提供了一系列的方便的转换函数。具体请查看cesium文档。


现在,我们把相机定位到我们数据所在的位置--纽约。


相机控制


Cameraviewer.scene的一个属性,用来控制当前可见范围。使用Cesium Camera API 我们可以直接设置相机的位置和朝向。


一些最常用的方法:



更详细的可以去学习下面两个示例:


// 创建相机初始位置和朝向
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    
        }
};
// 设置视图
viewer.scene.camera.setView(homeCameraView);


使用一个js对象保存相机的参数,设置后,相机此时是垂直俯视曼哈顿(Manhattan)。
事实上,我们可以使用这个view参数来更改home按钮的效果。与其设置地球的默认视图参数,我们还不如重写这个按钮,点击之后飞行到曼哈顿。可以通过其他参数来调节动画过程,并且可以设置一个事件监听取消默认的飞行过程,然后调用新的flyto()函数飞到我们设置的位置:

// 增加相机飞行动画参数
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教程.


时间控制


下来,我们通过配置viewer的 时钟(Clock)时间线(Timeline) 去控制场景中的时间流逝。


时钟(clock)API教程.
Cesium使用 JulianDate 描述某个时刻,这个时间存储了自从公元前4712年1月1日中午的天数。为了提高精度,这个类里分开存储了时刻的日期部分和时刻的秒部分。为了数学运算的安全和闰秒(leap seconds)的问题,这个时刻是按照国际原子时标准(International Atomic Time standard)存储的。


下面是一些关于scene中时间的配置选项:

// 设置时钟和时间线
viewer.clock.shouldAnimate = true; // 当viewer开启后,启动动画
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; // 设置加速倍率
viewer.clock.clockStep = Cesium.ClockStep.SYSTEM_CLOCK_MULTIPLIER; // tick computation mode(还没理解具体含义)
viewer.clock.clockRange = Cesium.ClockRange.LOOP_STOP; // 循环播放
viewer.timeline.zoomTo(viewer.clock.startTime, viewer.clock.stopTime); // 设置时间的可见范围


上述代码设定了场景动画播放速率,开始和结束时间,并且设置为循环播放。并且设置了时间线控件在合适的时间范围。使用这个 示例 去试验更多时间设置
初始化配置完成了,当你运行代码,能看到如下效果


 

初始化程序


Entities加载和样式配置


上面我们程序里已经添加了viewer 、影像图层、地形图层。下来重点说项目里的示例点位数据(the sample geocache data)。


为了更方便的可视化,Cesium支持流行的矢量格式GeoJson和KML,同时也支持我们团队定义的一种格式 CZML.


无论最初是什么格式,所有的空间矢量数据在Cesium里都是使用Entity 相关API去展示的。Entity API 使用了灵活高效的可视化渲染方式。 Entity是一种对几何图形做空间和时间展示的数据对象。sandcastle 里提供了很多简单的entity


为了能快速的学习Entity API,建议先花点时间去读下 空间数据可视化教程


下面一些使用Entity API的示例:



一旦你已经理解了Entity是什么东西,使用Cesium加载数据就很容易理解了。为了读取数据文件,需要根据你的数据格式创建一个合适的 DataSource ,它将负责解析你配置的url里的数据,然后创建一个[EntityCollection]用来存储从数据里加载的每一个Entity。DataSource 只是定义一些接口,依据数据格式的不同会有不同的解析过程。比如,KML使用KmlDataSource。如下面代码:

var kmlOptions = {
    camera : viewer.scene.camera,
    canvas : viewer.scene.canvas,
    clampToGround : true
};
// 从这个KML的url里加载POI点位  : 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(optinos) 来从KML文件中读取点位数据。 对于KmlDataSource,cameracanvas选项必须要配置。clampToGround选项控制数据是否贴地, 贴地效果是最常见的矢量数据可视化效果,保证数据紧贴地形起伏,而不是仅仅相对WGS84绝对球表面。


因为数据是异步加载的,所以这个函数实际返回一个 Promise , 最后使用KmlDataSource 存储我们新创建的Entity。


Promise是一种异步处理机制,这里的“异步”是指需要在.then函数里操作数据,而不是直接在.load函数之后立即操作。为了能在scene中使用这些载入的entity,只有当这个promise的then回调中才可以把KmlDataSource添加到 viewer.datasources

geocachePromise.then(function(dataSource) {
    // 把所有entities添加到viewer中显示
    viewer.dataSources.add(dataSource);
});


这些新加入到场景的entity默认有很多功能。单击它们会在 Infobox 显示属性, 双击它相机转换为居中观察模式(look at). 使用HOME按钮或者infobox旁边的相机按钮可以停止这种模式。下来我们来自定义样式。


KML和CZML格式,在文件内有明确的样式定义。为了学习,我们手动去创建样式。数据载入之后,我们依据这个 示例 遍历所有entity修改或者增加属性。我们的POI点默认都是使用 BillboardsLabels 显示, 根据下面的代码来修改某些entity的显示样式:

geocachePromise.then(function(dataSource) {
  // 把所有entities添加到viewer中显示
    viewer.dataSources.add(dataSource);

    // 获得entity列表
    var geocacheEntities = dataSource.entities.values;

    for (var i = 0; i < geocacheEntities.length; i++) {
        var entity = geocacheEntities[i];
        if (Cesium.defined(entity.billboard)) {
            // 对这个entity设置样式
        }
    }
});


通过调整锚点(anchor point)来改进显示效果,并且为了避免杂乱删除了文字标注(labels),最后设置了 displayDistanceCondition 控制只显示和相机一定距离内的点.

 if (Cesium.defined(entity.billboard)) {
            // 调整垂直方向的原点,保证图标里的针尖对着地表位置 
            entity.billboard.verticalOrigin = Cesium.VerticalOrigin.BOTTOM;
            // 去掉文字的显示
            entity.label = undefined;
            // 设置可见距离
            entity.billboard.distanceDisplayCondition = new Cesium.DistanceDisplayCondition(10.0, 20000.0);
        }


关于distanceDisplayCondition,可以学习下 sandcastle 示例.


下来,我们改进下 Infobox 。Infobox的标题栏显示的是entity的name属性, 它的内容显示的是description属性(使用HTML文本显示)。


你发现我们这个数据默认的description属性没什么意义,我们把这个属性更改为显示每个点的经纬度。


首先我们把entity的position属性转换为Cartographic,然后把经度和纬度构造一个HTML的table并赋值到description属性里。 现在单击我们的点在Infobox会显示一个格式规整的信息。

         if (Cesium.defined(entity.billboard)) {
           
            entity.billboard.verticalOrigin = Cesium.VerticalOrigin.BOTTOM; 
            entity.label = undefined; 
            entity.billboard.distanceDisplayCondition = new Cesium.DistanceDisplayCondition(10.0, 20000.0);
            // 计算经度和纬度(角度表示)
            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);
            // 修改描述信息 
            var description = '<table class="cesium-infoBox-defaultTable cesium-infoBox-defaultTable-lighter"><tbody>' +
                '<tr><th>' + "经度" + '</th><td>' + longitude.toFixed(5) + '</td></tr>' +
                '<tr><th>' + "纬度" + '</th><td>' + latitude.toFixed(5) + '</td></tr>' +
                '</tbody></table>';
            entity.description = description;
        }


最后效果:


 

修改description属性


或许把每个POI点所在的行政区展示出来非常有用。我们试着通过一个GeoJson文件来创建NYC的所有行政区域多边形。加载GeoJson和上面加载KML基本没什么区别,只是使用 GeoJsonDataSource 。和前面一样,也必须在promise的then函数里把数据添加到viewer.datasources中,数据才能显示。

var geojsonOptions = {
    clampToGround : true
};
// 从geojson文件加载行政区多边形边界数据
var neighborhoodsPromise = Cesium.GeoJsonDataSource.load('./Source/SampleData/neighborhoods.geojson', geojsonOptions);
 
 var neighborhoods;
 neighborhoodsPromise.then(function(dataSource) { 
    viewer.dataSources.add(dataSource);
});


下来设置多边形数据的样式。和上面调整billboard样式一样,我们设置行政区域多边形也必须在数据完全载入后去做。

var neighborhoods;
neighborhoodsPromise.then(function(dataSource) {
  
    viewer.dataSources.add(dataSource);
    neighborhoods = dataSource.entities;

    // 获取enity列表遍历
    var neighborhoodEntities = dataSource.entities.values;
    for (var i = 0; i < neighborhoodEntities.length; i++) {
        var entity = neighborhoodEntities[i];

        if (Cesium.defined(entity.polygon)) {
            // 设置样式代码
        }
    }
});


首先,我们重新设置每个entity的name属性和行政区的名称相同。原始的GeoJson文件有一个neighborhood的属性。Cesium使用entity.properties来存储GeoJson的属性。所以我们这么设置:


// 设置样式代码
// 把properties里的neighborhood设置到name
entity.name = entity.properties.neighborhood;


为了避免所有多边形颜色都相同,可以使用一个随机颜色 Color去设置每个多边形的 ColorMaterialProperty属性。

// 设置一个随机半透明颜色
entity.polygon.material = Cesium.Color.fromRandom({
    red : 0.1,
    maximumGreen : 0.5,
    minimumBlue : 0.5,
    alpha : 0.6
});
// 设置这个属性让多边形贴地,ClassificationType.CESIUM_3D_TILE 是贴模型,ClassificationType.BOTH是贴模型和贴地
entity.polygon.classificationType = Cesium.ClassificationType.TERRAIN;


最后,我们再创建一个基本的文字标注 Label。 为了保证显示效果清晰,我们设置了一个 disableDepthTestDistance 确保这个标注不会被其他对象盖住。


可是,Label需要通过entity.position属性设置位置。但是Polygon是有一个positions列表组成的边界,我们使用这个positions列表的中心点来计算。

// 获取多边形的positions列表 并计算它的中心点
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;
// 生成文字标注
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
};


最终效果:


 

多边形的文字标注


最后,增加一个无人机飞跃城市上空的高科技效果。


因为飞行路径只是一系列带着时间属性的位置点,我们通过CZML 文件来加载。CZML是一种在Cesium里描述时序图形场景的文件格式。它包含折线(lines)、点(points)、图标(billboards)、模型(models)和其他图形元素,以及他们随时间变化的属性。如同Google Earth的KML,CZML通过一种描述性语言(基于json格式)来存储Cesium大部分的功能。


我们得CZML文件定义一个包含不同时刻得一个位置列表Entity(默认显示为一个point)。在Entity API中有一些处理时间序列数据的属性类型。参考下面的示例:

// 从CZML中载入无人机轨迹
var dronePromise = Cesium.CzmlDsataSource.load('./Source/SampleData/SampleFlight.czml');

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


这个CZML中使用 Path去展示无人机轨迹, 以及一个展示不同时刻位置的属性.。使用插值算法把一个路径的离散点链接为一个连续的折线。


我们继续改进下无人机的显示样式。我们可以用一个三维模型去表示我们的无人机,并把它设置到entity上,而不是仅仅用一个简单的点。



Cesium支持加载glTF格式的三维模型格式。glTF是一个由Cesium团队和 Khronos group一起开发的开源三维模型格式,这种格式尽量减少传输和实时处理过程中的模型数据量。如果没有glTF模型,我们提供了一个 在线转换工具 把DAE,obj等格式转为glTF。


我们载入一个效果不错的,又带动画的无人机模型 Model

var drone;
dronePromise.then(function(dataSource) {
    viewer.dataSources.add(dataSource);
    // 使用id获取在CZML 数据中定义的无人机entity
    drone = dataSource.entities.getById('Aircraft/Aircraft1');
    // 附加一些三维模型
    drone.model = {
        uri : './Source/SampleData/Models/CesiumDrone.gltf',
        minimumPixelSize : 128,
        maximumScale : 1000,
        silhouetteColor : Cesium.Color.WHITE,
        silhouetteSize : 2
    };
});


现在我们的模型看起来还不错,不像最初那个简单的点效果,这个无人机模型有方向,但是效果有点奇怪,并没有朝向无人机的前进方向。幸好,Cesium提供了VelocityOrientationProperty ,这个会根据entity的位置点信息和时间来自动计算朝向。

 // 基于无人机轨迹的位置点,自动计算朝向 
 drone.orientation  =  new Cesium.VelocityOrientationProperty(drone.position);


现在我们的无人机模型朝向正确了。我们还可以改进下无人机飞行效果。Cesium依据离散点,使用线形插值构造了一条折线,虽然远处看不明显,但是这些折线段让路径看着不自然。有一些插值配置选项:


  • 插值示例
    为了飞行路径更平滑,可以如下修改配置 :

// 光滑的路径插值
drone.position.setInterpolationOptions({
    interpolationDegree : 3,
    interpolationAlgorithm : Cesium.HermitePolynomialApproximation
});


 

平滑的飞行路径


3D Tiles


我们的团队有时候描述Cesium像一个使用真实世界数据的三维游戏引擎。可是,加载真实世界的数据要比游戏引擎的数据困难很多,主要因为真实数据有非常高得分辨率,而且要求精确得可视化。幸好,Cesium和开源社区合作开发了3D Tiles格式。它是一个流式载入海量各种类型得空间三维数据的 开放协议


使用一种类似Cesium的地形和影像数据切片技术,3d tiles格式使原先那些不可能做可视化交互的大模型数据能够展示出来,包括建筑物数据、CAD(或者BIM)模型,点云,倾斜模型。


  • 3D Tiles 调试器 ,它是一个能够查看各种3d tile后台信息的调试工具。


这是一些不同类型的3d tile模型数据:



这个项目中,使用 Cesium3DTileset 类添加整个纽约的真实建筑物模型,改进了可视化效果的真实性。

// 加载纽约建筑物模型
var city = viewer.scene.primitives.add(new Cesium.Cesium3DTileset({ url: Cesium.IonResource.fromAssetId(3839) }));


你会发现这些建筑物的高度好像不正确。这个可以简单修正下。通过一个 modelMatrix,我们可以调整这个数据的位置。


把数据当前的包围球转为Cartographic,就能计算出模型现在相对于地面的偏移,然后增加这个偏移值,然后重设modelMatrix:

// 调整3dtile模型的高度,让他刚好放在地表
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样式语言去对一部分数据进行样式配置。
3D Tiles的样式依据一个表达式,根据Cesium3DTileFeature模型属性去修改某一部分甚至某一栋建筑物的颜色(RGB和透明度)。这些元素属性(feature property)通常存储在每个模型切片的batchtable中。元素属性可以是任意属性,比如高度,名称,坐标,创建日期等等。样式语言使用JSON格式定义,并且支持JavaScript的表达式(a small subset of JavaScript augmented)。另外,样式语言提供了一些内置的函数,支持数学计算。


Cesium3DTileStyle示例如下:

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


这个样式只是简单的让纽约的所有建筑都可见。把它设置到city.style就可以看到可视化效果。


city.style = defaultStyle;  


 

默认效果


下面这个样式让模型半透明:

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

 

半透明效果


所有元素使用相同样式只是小儿科。我们可以使用属性对每个元素设置不同样式。下面是一个依据建筑高度去着色的示例:

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


 

依据高度着色


为了在这些样式之间切换,我们增加一点点代码去监听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的示例:


交互


最后,我们添加一些鼠标交互。我们改进下效果,当鼠标划过的时候,高亮图标。 为了做出这个效果,我们使用拾取技术(picking),它能够根据一个屏幕上的像素位置返回三维场景中的对象信息。


有好几种拾取:


  • Scene.pick : 返回窗口坐标对应的图元的第一个对象。

  • Scene.drillPick :返回窗口坐标对应的所有对象列表。

  • Globe.pick : 返回一条射线和地形的相交位置点。


这是一些示例:


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


下来我们写高亮函数。我们可以在回调函数里获得一个窗口坐标,并传递到pick()方法里。 如果拾取到一个billboard对象,我们就知道目前鼠标在一个图标上了。然后使用我们前面学过的相关Entity接口,去修改它的样式做高亮效果。

// 当鼠标移到了我们关注的图标上,修改entity 的billboard 缩放和颜色
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);


高亮样式设置成功了。可是,当鼠标不在图标上,这个高亮样式依然有效。为了解决这个问题,我们使用一个变量来存储上次的高亮图标,当鼠标不在它上面的时候,恢复它原来的样式。
这是包含高亮和不高亮完整功能的代码:

var previousPickedEntity = undefined;
handler.setInputAction(function(movement) {
    var pickedPrimitive = viewer.scene.pick(movement.endPosition);
    var pickedEntity = (Cesium.defined(pickedPrimitive)) ? pickedPrimitive.id : undefined;
    // 取消上一个高亮对象的高亮效果
    if (Cesium.defined(previousPickedEntity)) {
        previousPickedEntity.billboard.scale = 1.0;
        previousPickedEntity.billboard.color = Cesium.Color.WHITE;
    }
    // 当前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);


好了,我们添加了完整的图标entity的鼠标交互响应。


 

鼠标交互


相机模式


为了炫耀我们的无人机飞行,我们来实验下相机模式。在两种相机模式下可以简单的切换:


  • 自由模式 :默认的相机控制方式

  • 无人机模式 : 以一个固定距离跟随无人机
    自由模式下不需要任何代码。无人机跟随模式下,我们使用viewer内置的跟随函数,确保相机一直居中观察无人机。这种模式下,即便对象是移动的,相机也能和目标之间保持一个固定的偏移距离。只需要简单的设置
    viewer.trackedEntity
    切换到自由模式,只需要把viewer.trackedEntity设置为undefined,然后可以使用camera.flyTo()返回到初始位置。


这是相机模式代码:

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


当我们双击entity的时候,就会自动进行跟随模式。如果用户通过点击跟踪无人机,添加一些处理去自动更新UI界面:

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



我们可以通过界面自由切换相机模式了:


 

相机模式


其他


剩下的代码我们增加一些其他的可视化效果。如同前面提到的HTML元素交互方式,我们可以添加阴影的切换界面,以及行政区多边形的可见性控制。
首先,简单的控制下行政区划的可见性。通常,通过设置Entity.show 属性来隐藏entity。可是,这个仅仅设置一个entity,我们希望一次性控制所有行政区划面的可见性。
可以像这个示例一样,把所有行政区entity放在一个父entity中,或者通过设置EntityCollectionshow属性来控制。只需要设置一次neighborhoods.show属性即可控制所有entity的可见性。

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

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


如同切换阴影一样:

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

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


因为3D Tiles数据可能不是瞬间载入,可以添加一个载入指示器,当所有切片都载入后隐藏。

// 当城市数据初始化完成后,移除加载指示器
var loadingIndicator = document.getElementById('loadingIndicator');
loadingIndicator.style.display = 'block';
city.readyPromise.then(function () {
    loadingIndicator.style.display = 'none';
});


接下来


恭喜!你已经成功完成了CesiumJS项目。在Cesium的培训过程中,请随意使用我们提供的代码去测试和开发。我们很高兴欢迎你加入Cesium社区,并且期望看到你基于CesiumJS开发的酷炫程序。


开发资源


为了你的Cesium开发事业,我们鼓励你访问下面的资源:


  • 官方文档 : 完整的CesiumAPI文档,包含一些示例代码

  • Sandcastle :一个所见即所得的编码环境,包含大量的代码示例.

  • 官方教程 :详细描述基于Cesium开发的方方面面。

  • 官方论坛 : Cesium相关问题的主要讨论平台.

  • Cesium实验室 : 中国最专业的Cesium的讨论学习QQ群:595512567。
    一旦有问题,上面的资源可能有你的答案。


在cesiumjs.org展示你的项目


我们很乐意去分享所有Cesium社区创建的酷炫项目。遍布世界的的开发者创建了很多有意思的我们从来没考虑过的项目。一旦你的项目准备分享给全世界,请跟我们联系放到[CesiumJS示例页面] (https://cesiumjs.org/demos)。具体请阅读 这个博客提交你的项目示例。

    


收藏 0 0

登录 后评论。没有帐号? 注册 一个。

西部世界

VIP会员 工作人员
  • 0 回答
  • 0 粉丝
  • 0 关注