基于天地图 Cesium 三维服务实现三维地球及地球自转实践

目录

前言

[一、准备工作:申请天地图 Key](#一、准备工作:申请天地图 Key)

二、完整代码展示

三、核心模块解析

[3.1 页面基础与 Cesium 环境引入](#3.1 页面基础与 Cesium 环境引入)

[3.2 Viewer 初始化与相机交互参数配置](#3.2 Viewer 初始化与相机交互参数配置)

[3.3 天地图影像、国界瓦片图层加载](#3.3 天地图影像、国界瓦片图层加载)

[3.4 📌重点:天地图地形瓦片加载](#3.4 📌重点:天地图地形瓦片加载)

[3.5 三维地名 WTFS 服务接入](#3.5 三维地名 WTFS 服务接入)

[3.6 📌重点:地球自转功能实现](#3.6 📌重点:地球自转功能实现)

四、新手入门实操提示

五、常见踩坑说明

六、总结与拓展方向


前言

对于 WebGIS 的初学者来说,搭建一个可以运行的三维数字地球,是非常有成就感的入门实践。很多同学刚接触 Cesium 的时候,第一想法就是做出一个可以旋转、带有真实地形起伏的地球,但是直接使用官方示例,大多对接的是国外地图服务,网络访问不稳定,对国内开发者并不友好。

天地图作为国家地理信息公共服务平台,提供了免费可供开发者调用的影像、国界、高程地形、三维地名等全套三维 GIS 服务。同时官方提供了适配好的 Cesium 扩展库,不用我们从零去做投影转换、瓦片格式适配的底层工作。本文就是一套完整可复现的实战案例,从零搭建三维地球,加载天地图地形瓦片还原真实地表高低起伏,并且通过相机接口实现地球自动自转动画效果。非常适合 GIS 新手拿来练习学习,完整 HTML 示例,只需要替换自己申请的天地图 Key,借助简单的 Web 服务即可直接运行看到效果。

注意:示例全部引用天地图官方 CDN 提供 Cesium 以及扩展插件,不需要本地下载、编译部署 Cesium 库,降低新手的上手门槛。

一、准备工作:申请天地图 Key

·访问天地图开发者控制台申请开发 Key:具体在天地图中注册并且生成访问key的方法在之前的博客中已经介绍过,需要的朋友可以查阅之前的博文。

  1. 注册并登录天地图开发者控制台;
  2. 创建应用,应用类型选择浏览器端 Key
  3. 将代码中 token = '你申请的天地图key'替换成自己获取到的 tk 密钥。

⚠️新手特别注意:浏览器端 Key 支持配置域名白名单,如果没有配置白名单,本地直接双击打开 html 文件(file 本地协议访问)会出现 403 鉴权报错。本地调试推荐使用 VS Code 的 Live Server 插件、Nginx 等简易 Web 服务运行项目,避免跨域与鉴权问题。

二、完整代码展示

复制代码
<!DOCTYPE html>
<html lang="en">
<head>
    <title>天地图三维服务接入</title>
    <script type="text/javascript" cesium="true" src="https://api.tianditu.gov.cn/cdn/demo/sanwei/static/cesium/Cesium.js"></script>
    <script type="text/javascript" cesium="true" src="https://api.tianditu.gov.cn/cdn/plugins/cesium/Cesium_ext_min.js"></script>
    <script type="text/javascript" cesium="true" src="https://api.tianditu.gov.cn/cdn/plugins/cesium/long.min.js"></script>
    <script type="text/javascript" cesium="true" src="https://api.tianditu.gov.cn/cdn/plugins/cesium/bytebuffer.min.js"></script>
    <script type="text/javascript" cesium="true" src="https://api.tianditu.gov.cn/cdn/plugins/cesium/protobuf.min.js"></script>
    <link rel="stylesheet" cesium="true" href="https://api.tianditu.gov.cn/cdn/demo/sanwei/static/cesium/Widgets/widgets.css">
    <style>
        html, body{
            margin: 0;
            padding: 0;
            overflow: hidden;
        }
        #cesiumContainer {
            width: 100%;
            height: 100%;
        }
        #cesiumContainer .cesium-viewer-bottom{
            display: none;
        }
    </style>
</head>
<body>
    <div id="cesiumContainer"></div>
	
	<script>
		var token = '你申请的天地图key';
		// 服务域名
		var tdtUrl = 'https://t{s}.tianditu.gov.cn/';
		// 服务负载子域
		var subdomains=['0','1','2','3','4','5','6','7'];
		// cesium 初始化
		var viewer = new Cesium.Map('cesiumContainer', {
			shouldAnimate: true, //是否允许动画
			selectionIndicator: false,
			baseLayerPicker: false,
			fullscreenButton: false,
			geocoder: false,
			homeButton: false,
			infoBox: false,
			sceneModePicker: false,
			timeline: false,
			navigationHelpButton: false,
			navigationInstructionsInitiallyVisible: false,
			showRenderLoopErrors: false,
			shadows: false,
		});
		// 抗锯齿
		viewer.scene.fxaa = true;
		viewer.scene.postProcessStages.fxaa.enabled=false;
		// 水雾特效
		viewer.scene.globe.showGroundAtmosphere = true;
		// 设置最大俯仰角,[-90,0]区间内,默认为-30,单位弧度
		viewer.scene.screenSpaceCameraController.constrainedPitch = Cesium.Math.toRadians(-20);
		viewer.scene.screenSpaceCameraController.autoResetHeadingPitch = false;
		viewer.scene.screenSpaceCameraController.inertiaZoom = 0.5;
		viewer.scene.screenSpaceCameraController.minimumZoomDistance = 50;
		viewer.scene.screenSpaceCameraController.maximumZoomDistance = 20000000;
		viewer.scene.screenSpaceCameraController.zoomEventTypes = [
			Cesium.CameraEventType.RIGHT_DRAG,
			Cesium.CameraEventType.WHEEL,
			Cesium.CameraEventType.PINCH,
		];
		viewer.scene.screenSpaceCameraController.tiltEventTypes = [
			Cesium.CameraEventType.MIDDLE_DRAG,
			Cesium.CameraEventType.PINCH,
			{
				eventType: Cesium.CameraEventType.LEFT_DRAG,
				modifier: Cesium.KeyboardEventModifier.CTRL,
			},
			{
				eventType: Cesium.CameraEventType.RIGHT_DRAG,
				modifier: Cesium.KeyboardEventModifier.CTRL,
			},
		];
		// 取消默认的双击事件
		viewer.cesiumWidget.screenSpaceEventHandler.removeInputAction(Cesium.ScreenSpaceEventType.LEFT_DOUBLE_CLICK);
		// 叠加影像服务
		var imgMap = new Cesium.UrlTemplateImageryProvider({
			url: tdtUrl + 'DataServer?T=img_w&x={x}&y={y}&l={z}&tk=' + token,
			subdomains: subdomains,
			tilingScheme : new Cesium.WebMercatorTilingScheme(),
			maximumLevel : 18
		});
		viewer.imageryLayers.addImageryProvider(imgMap); 
		// 叠加国界服务
		var iboMap = new Cesium.UrlTemplateImageryProvider({
			url: tdtUrl + 'DataServer?T=ibo_w&x={x}&y={y}&l={z}&tk=' + token,
			subdomains: subdomains,
			tilingScheme : new Cesium.WebMercatorTilingScheme(),
			maximumLevel : 10
		});
		viewer.imageryLayers.addImageryProvider(iboMap);
		// -------------------【重点:加载天地图地形瓦片】-------------------
		var terrainUrls = new Array();
		for (var i = 0; i < subdomains.length; i++){
			var url = tdtUrl.replace('{s}', subdomains[i]) + 'mapservice/swdx?T=elv_c&tk=' + token;
			terrainUrls.push(url);
		}
		var provider = new Cesium.GeoTerrainProvider({
			urls: terrainUrls
		});
		viewer.terrainProvider = provider;

		// 将三维球定位到中国
		viewer.camera.flyTo({
			destination: Cesium.Cartesian3.fromDegrees(103.84, 31.15, 17850000),
			orientation: {
				heading :  Cesium.Math.toRadians(348.4202942851978),
				pitch : Cesium.Math.toRadians(-89.74026687972041),
				roll : Cesium.Math.toRadians(0)
			},
			complete:function callback() {
				// 定位完成之后的回调函数
			}
		});
		// 叠加三维地名服务
		var wtfs = new Cesium.GeoWTFS({
			viewer,
			subdomains:subdomains,
			metadata:{
				boundBox: {
					minX: -180,
					minY: -90,
					maxX: 180,
					maxY: 90
				},
				minLevel: 1,
				maxLevel: 20
			},
			depthTestOptimization: true,
			dTOElevation: 15000,
			dTOPitch: Cesium.Math.toRadians(-70),
			aotuCollide: true, //是否开启避让
			collisionPadding: [5, 10, 8, 5], //开启避让时,标注碰撞增加内边距,上、右、下、左
			serverFirstStyle: true, //服务端样式优先
			labelGraphics: {
				font:"28px sans-serif",
				fontSize: 28,
				fillColor:Cesium.Color.WHITE,
				scale: 0.5,
				outlineColor:Cesium.Color.BLACK,
				outlineWidth: 2,
				style:Cesium.LabelStyle.FILL_AND_OUTLINE,
				showBackground:false,
				backgroundColor:Cesium.Color.RED,
				backgroundPadding:new Cesium.Cartesian2(10, 10),
				horizontalOrigin:Cesium.HorizontalOrigin.LEFT,
				verticalOrigin:Cesium.VerticalOrigin.TOP,
				eyeOffset:Cesium.Cartesian3.ZERO,
				pixelOffset: new Cesium.Cartesian2(5, 5),
				disableDepthTestDistance:undefined
			},
			billboardGraphics: {
				horizontalOrigin:Cesium.HorizontalOrigin.CENTER,
				verticalOrigin:Cesium.VerticalOrigin.CENTER,
				eyeOffset:Cesium.Cartesian3.ZERO,
				pixelOffset:Cesium.Cartesian2.ZERO,
				alignedAxis:Cesium.Cartesian3.ZERO,
				color:Cesium.Color.WHITE,
				rotation:0,
				scale:1,
				width:18,
				height:18,
				disableDepthTestDistance:undefined
			}
		});
		wtfs.getTileUrl = function(){
			return tdtUrl + 'mapservice/GetTiles?lxys={z},{x},{y}&VERSION=1.0.0&tk='+ token; 
		}
		wtfs.getIcoUrl = function(){
			return tdtUrl + 'mapservice/GetIcon?id={id}&tk='+ token;
		}
		wtfs.initTDT([{"x":6,"y":1,"level":2,"boundBox":{"minX":90,"minY":0,"maxX":135,"maxY":45}},{"x":7,"y":1,"level":2,"boundBox":{"minX":135,"minY":0,"maxX":180,"maxY":45}},{"x":6,"y":0,"level":2,"boundBox":{"minX":90,"minY":45,"maxX":135,"maxY":90}},{"x":7,"y":0,"level":2,"boundBox":{"minX":135,"minY":45,"maxX":180,"maxY":90}},{"x":5,"y":1,"level":2,"boundBox":{"minX":45,"minY":0,"maxX":90,"maxY":45}},{"x":4,"y":1,"level":2,"boundBox":{"minX":0,"minY":0,"maxX":45,"maxY":45}},{"x":5,"y":0,"level":2,"boundBox":{"minX":45,"minY":45,"maxX":90,"maxY":90}},{"x":4,"y":0,"level":2,"boundBox":{"minX":0,"minY":45,"maxX":90,"maxY":90}},{"x":6,"y":2,"level":2,"boundBox":{"minX":90,"minY":-45,"maxX":135,"maxY":0}},{"x":6,"y":3,"level":2,"boundBox":{"minX":90,"minY":-90,"maxX":135,"maxY":-45}},{"x":7,"y":2,"level":2,"boundBox":{"minX":135,"minY":-45,"maxX":180,"maxY":0}},{"x":5,"y":2,"level":2,"boundBox":{"minX":45,"minY":-45,"maxX":90,"maxY":0}},{"x":4,"y":2,"level":2,"boundBox":{"minX":0,"minY":-45,"maxX":45,"maxY":0}},{"x":3,"y":1,"level":2,"boundBox":{"minX":-45,"minY":0,"maxX":0,"maxY":45}},{"x":3,"y":0,"level":2,"boundBox":{"minX":-45,"minY":45,"maxX":0,"maxY":90}},{"x":2,"y":0,"level":2,"boundBox":{"minX":-90,"minY":45,"maxX":-45,"maxY":90}},{"x":0,"y":1,"level":2,"boundBox":{"minX":-180,"minY":0,"maxX":-135,"maxY":45}},{"x":1,"y":0,"level":2,"boundBox":{"minX":-135,"minY":45,"maxX":-90,"maxY":90}},{"x":0,"y":0,"level":2,"boundBox":{"minX":-180,"minY":45,"maxX":-135,"maxY":90}}]);
		
		// -------------------【重点:实现地球自转】-------------------
		// 开启时钟动画
		viewer.clock.shouldAnimate = true;
		// 每帧旋转弧度,数值越大转速越快
		const angle = 0.001; 
		// 时钟每帧更新回调
		viewer.clock.onTick.addEventListener(() => {
			// 相机绕世界Z轴旋转,模拟地球自转
			// UNIT_Z:地心垂直向上Z轴;angle:旋转弧度,正值逆时针,负值顺时针
			viewer.scene.camera.rotate(Cesium.Cartesian3.UNIT_Z, angle);
		});		
	</script>
</body>
</html>

三、核心模块解析

3.1 页面基础与 Cesium 环境引入

很多刚接触 Cesium 的新手,第一步就会被本地部署 Cesium 库难住,需要下载庞大的资源包、处理静态资源路径。在这个案例中,我们直接全部引用天地图官方 CDN 资源,无需本地部署 Cesium。除了基础的 Cesium.js,还必须引入天地图的扩展库Cesium_ext_min.js。天地图封装的地形提供者GeoTerrainProvider、三维地名GeoWTFS全部都在这个扩展 JS 里面,如果缺失该脚本,代码直接会报对象未定义的错误。

javascript 复制代码
<script type="text/javascript" cesium="true" src="https://api.tianditu.gov.cn/cdn/demo/sanwei/static/cesium/Cesium.js"></script>
<script type="text/javascript" cesium="true" src="https://api.tianditu.gov.cn/cdn/plugins/cesium/Cesium_ext_min.js"></script>

页面 CSS 部分把 html、body 设置为宽高 100% 并且隐藏滚动条,保证三维容器占满浏览器整个视口;同时隐藏 Cesium 默认底部版权水印 DOM,得到一个干净的三维画布。

3.2 Viewer 初始化与相机交互参数配置

这是天地图二次封装的对象,内部做了很多适配天地图服务的逻辑,如果直接替换成 Viewer,部分扩展功能会失效。初始化的时候关闭了 Cesium 自带的大量 UI 控件,包括图层选择器、全屏按钮、搜索地理编码器、时间轴、帮助提示等等,目的是得到纯净的三维地球效果。

同时我们针对相机交互做了细致的约束:限制最大俯仰角度、缩放的最近和最远距离,自定义拖拽、滚轮、手势的事件类型,禁用双击放大。对于新手来说,读懂这一部分参数,可以理解 Cesium 相机控制的底层逻辑,后续做自定义漫游的时候会受益匪浅。

javascript 复制代码
// cesium 初始化
var viewer = new Cesium.Map('cesiumContainer', {
    shouldAnimate: true, //是否允许动画
	selectionIndicator: false,
	baseLayerPicker: false,
	fullscreenButton: false,
	geocoder: false,
	homeButton: false,
	infoBox: false,
	sceneModePicker: false,
	timeline: false,
	navigationHelpButton: false,
	navigationInstructionsInitiallyVisible: false,
	showRenderLoopErrors: false,
	shadows: false,
});

3.3 天地图影像、国界瓦片图层加载

WebGIS 三维地图的画面,本质上是由无数张分级的瓦片图片拼接而成。

  • img_w 代表天地图卫星影像瓦片服务,提供真实卫星拍摄的地表影像;
  • ibo_w 为国界注记瓦片,叠加在影像上层,用来渲染国家边界线。

UrlTemplateImageryProvider是 Cesium 通用的瓦片影像加载类,配置 subdomains 子域名数组可以实现浏览器多请求并发,加快瓦片加载速度;tilingScheme 指定 Web 墨卡托投影,maximumLevel 设置瓦片最大请求层级。最后调用addImageryProvider把图层添加到场景图层集合当中。新手可以在这里尝试调整图层顺序,体会多图层叠加渲染的逻辑。

3.4 📌重点:天地图地形瓦片加载

地形加载是本案例的核心难点,也是很多新手踩坑重灾区。很多同学运行代码之后,看到影像正常加载,但是地球始终是光滑的球面,看不到高山、盆地的起伏,问题几乎都出在这里。

天地图的高程瓦片拥有自己独特的数据格式,不能使用 Cesium 原生的CesiumTerrainProvider,必须使用天地图扩展的GeoTerrainProvider 。天地图地形服务接口地址为mapservice/swdx?T=elv_c&tk=token,我们循环遍历子域数组生成多域名请求地址数组,传入 GeoTerrainProvider,最后赋值给viewer.terrainProvider,场景就会启用高程数据渲染真实地形。

小实验:新手可以注释掉viewer.terrainProvider = provider;这一行,刷新页面对比前后效果,直观感受地形服务开启和关闭的差异。

3.5 三维地名 WTFS 服务接入

GeoWTFS是天地图提供的三维地名标注服务,可以在三维球体上渲染全球城市地名标签,支持标签避让、字体样式、描边、偏移量等大量自定义配置。getTileUrl用来定义地名瓦片请求地址;getIcoUrl获取地名配套图标资源;wtfs.initTDT()传入全球瓦片范围配置,完成地名服务初始化。新手可以修改 labelGraphics 里面字体大小、颜色、描边宽度,观察页面地名标签的变化,练习三维标签样式调整。

3.6 📌重点:地球自转功能实现

网上有不少地球自转的示例,部分方案是修改球体本身的旋转,这种方式会造成地形、标签、实体全部发生扭曲,体验很差。

本案例采用业界更推荐的实现思路:地理数据球体本身保持静止,移动相机的位置来模拟自转效果

  1. 开启 clock 时钟动画 viewer.clock.shouldAnimate = true
  2. 定义每帧旋转弧度 angle,数值越大转速越快,正数逆时针旋转,负数顺时针旋转;
  3. 监听 viewer.clock.onTick事件,该事件会在引擎每一帧渲染前触发,调用camera.rotate()让相机绕地心 Z 轴Cartesian3.UNIT_Z持续旋转。

代码如下:

javascript 复制代码
// 1. 开启时钟动画(可选,但建议开启)
viewer.clock.shouldAnimate = true;

// 2. 定义每帧旋转的弧度,数值越大转得越快
// 例如,这里设置每帧旋转 0.001 弧度
const angle = 0.001; 

// 3. 监听 clock.onTick 事件
viewer.clock.onTick.addEventListener(() => {
    // 让相机绕 Z 轴(垂直轴)旋转
	// 参数1: 旋转轴 (Cartesian3.UNIT_Z 表示垂直向上)
	// 参数2: 旋转角度(弧度),正值为逆时针,负值为顺时针[reference:3][reference:4]
	viewer.scene.camera.rotate(Cesium.Cartesian3.UNIT_Z, angle);
});		

💡新手拓展练习:可以定义按钮,点击移除 onTick 监听,实现 "开启自转 / 停止自转" 的交互;修改 angle 数值,体验不同旋转速度。

四、新手入门实操提示

  1. 直接复制完整 HTML 代码,不要遗漏任何 CDN 的 script 标签,缺失脚本会直接报错;
  2. 优先使用 Live Server、Nginx 等 Web 服务运行,拒绝双击 html 用 file 协议打开;
  3. 密钥申请完成后记得配置域名白名单,本地调试可以配置localhost
  4. 调试过程善用浏览器 F12 开发者工具:Console 控制台看 JS 报错,Network 网络面板查看瓦片、地形请求是否成功;
  5. 不要直接大段修改代码,建议小改动、刷新看效果,理解每一段代码的作用之后,再进行二次开发。

完整的视频效果如下:

基于天地图三维Cesium地球自转实践

这个案例覆盖了密钥鉴权、瓦片图层、地形加载、相机动画、三维标注多个 WebGIS 高频知识点,非常适合作为 Cesium 入门练手项目。大家可以基于这份 Demo 做二次修改,慢慢扩展自己的功能。

五、常见踩坑说明

  1. 页面空白,瓦片不加载
    • 检查 token 密钥是否正确;浏览器 F12 看网络请求 403,大概率 key 域名白名单没有配置。
    • 不能直接 file 打开 html,需要部署 web 服务访问。
  2. 地形没有起伏,全是平面
    • 确认使用的是 Cesium.GeoTerrainProvider,不要用原生 TerrainProvider;
    • 网络面板检查地形瓦片请求是否正常返回。
  3. 地球自转不生效
    • 确认viewer.clock.shouldAnimate=true
    • 不要把 rotate 放到 render 循环,推荐使用 clock.onTick。
  4. 三维地名不显示
    • WTFS 服务依赖天地图扩展 JS,确认 CDN 脚本加载成功。

六、总结与拓展方向

本篇实战案例基于天地图官方 Cesium 三维扩展库,完整实现了卫星影像图层、国界线图层、天地图高程地形、全球三维地名,并且通过相机旋转实现流畅的地球自转动画。整套代码降低了新手的学习成本,不需要搭建复杂工程,一个 HTML 文件就可以跑起来完整的三维地球。对于国内开发者来说,天地图服务网络访问稳定,摆脱对国外地图服务依赖。学会基础运行之后,大家还可以继续拓展更多功能:添加点位标记、弹窗信息、轨迹漫游动画、做自转启停开关、调速滑块、叠加自定义业务矢量图层等。希望本篇文章能够帮助更多 WebGIS 新手快速入门 Cesium 三维开发。行文仓促,定有不足之处,欢迎各位朋友在评论区批评指正,不胜感激。

相关推荐
IT毕设实战小研1 小时前
基于大数据处理的京东商品销售态势分析与可视化设计
大数据·科技·机器学习·信息可视化·数据分析
Q26433650232 小时前
【有源码】基于大数据的零售交易者行为特征与生存状况数据分析及可视化 Hadoop+Spark大数据项目
大数据·hadoop·信息可视化·数据挖掘·数据分析·spark·毕业设计
2601_9669496511 小时前
为什么量化策略需要大量历史股票数据?从回测可信度理解数据规模
开发语言·python·数据分析·pandas·量化交易·股票数据·quantdash
小王毕业啦14 小时前
2012-2024年 机构投资者实地调研数据 xlsx
大数据·人工智能·数据挖掘·数据分析·社科数据·实证分析·经管数据
Dxy123931021614 小时前
Python如何结合AI解决数据分析问题
人工智能·python·数据分析
quantdash_cc15 小时前
股票历史数据为什么比实时行情更重要?回测结果失真的根源可能就在 K 线数据
开发语言·python·数据分析·量化交易·股票数据·quantdash
2601_9669496519 小时前
从轮询到策略消费:量化系统如何高效处理五档盘口数据
开发语言·python·数据分析·pandas·量化交易·股票数据·quantdash
跨境卫士—小依19 小时前
2026跨境电商数据分析入门:用指标判断选品与投放是否有效
大数据·人工智能·数据分析·跨境电商·营销策略
m0_5474866621 小时前
《大数据可视化技术》全套PPT课件2026
信息可视化·数据分析