news 2026/7/29 11:33:35

Cesium 空间分析:把 “坡向” 计算搬到浏览器

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Cesium 空间分析:把 “坡向” 计算搬到浏览器

前端做 GIS 空间分析不是新鲜事,但把坡向计算搬到浏览器里跑、还要能交互式选范围,中间藏了多少细节?这篇文章把完整实现拆开给你看。

开篇

CesiumJS 提供了sampleTerrainAPI 可以采样地形高度,但没有内置"坡向分析"这个功能。你得自己把整个计算流水线搭起来:从 DEM 采样到梯度计算,从色带映射到 Canvas 叠图,最后再盖一层方向箭头。每一步都有坑。

这篇文章就是我的实测笔记——我会逐段拆解aspect_analysis.html的核心实现,环境搭建和底图加载一笔带过,重点放在 @@计算链路@@ 和 @@可视化策略@@ 上。

核心思路:从地形到坡向的四步流水线

整个工具的逻辑其实不复杂——就四步:

  1. ++地形采样++ —— 在用户绘制的矩形范围内,按网格密度采样地形高度
  2. ++梯度计算++ —— 用中心差分法算出每个格网的东西向和南北向坡度
  3. ++坡向转换++ —— 把梯度分量转为 0°~360° 的方位角(0°=北,顺时针)
  4. ++可视化呈现++ —— 用 Canvas 生成色带叠加层 + 箭头矢量表示方向

这四步串起来就是一条流水线,每一步的输出是下一步的输入。贴个简化版流程:

矩形范围→ sampleTerrain → 高度矩阵 → 梯度计算 → 坡度+坡向 → Canvas色带叠图 → 箭头渲染

看代码的时候你会注意到,整条流水线全部塞在analyzeCurrentRectangle()这一个方法里。这么做不是偷懒——是为了保证每一步的数据都是最新的,避免参数改了但结果没刷新这种前端常见的状态不同步问题。

下面我一步步拆。

地形采样:sampleTerrain的几个坑

坑一:采样点数组不能随便构造

采样是整个链条的第一步,也是最容易翻车的一步。Cesium 的sampleTerrain接受一个Cartographic数组和一个采样层级,返回带高度的Cartographic数组。

关键点在构造这个数组:

letpositions=[];for(letj=0;j<rows;j++){letlat=south+(latSpan*j)/(rows-1);for(leti=0;i<cols;i++){letlon=west+(lonSpan*i)/(cols-1);positions.push(newCesium.Cartographic(lon,lat,0));}}letsampled=awaitCesium.sampleTerrain(this.viewer.terrainProvider,this.params.terrainLevel,positions,);

注意这里terrainLevel可调的(8-15),并且这里精度其实没设置。两个采样点的间距直接由矩形范围的经纬度跨度和网格数决定。这点和桌面 GIS 不太一样——桌面软件通常让你指定像元大小,而这里是纯数学网格,每个网格点就是一个采样点。

坑二:采样层级是个性能杠杆

terrainLevel的值越大,采样精度越高,但耗时也越长。我默认设了 12——经过实测,100×100 的网格在 level 12 时大概 2-3 秒,level 15 直接上 6-8 秒。

这就是为什么我在面板里加了autoAnalyze开关——关掉它,你可以先调好所有参数再点"开始分析",省得每改一个参数就触发一次 3 秒的重新计算。

坑三:地形返回的高度可能 undefined

DEM 数据不是全地球都有。采样点落在 DEM 覆盖范围之外时,sampleTerrain返回的height就是undefined代码里必须兜底:

letheights=newFloat64Array(cols*rows);letvalid=newUint8Array(cols*rows);for(letk=0;k<sampled.length;k++){letheight=sampled[k].height;if(height===undefined||Number.isNaN(height))continue;heights[k]=height;valid[k]=1;}

valid数组标记有效点,后续梯度计算时遇到无效点就直接跳过。这个细节不处理,后面梯度计算会给你各种 NaN 污染。

梯度和坡向:这部分纯数学,但很容易算错方向

拿到高度矩阵之后,就可以计算每个格网的坡度和坡向了。

梯度:中心差分法

代码用的是最简单的中心差分法——每个格网的东西向梯度用左右两个邻点的高度差除以两倍格网间距,南北向同理。这是标准做法,不稀奇。

但这里有两个细节值得说一下:

leteastMeterPerRad=earthRadius*Math.cos(lat);

第一,经纬度换算成米。东西向 1 度的实际距离随纬度变化——赤道最大,极点趋近于零。所以每次迭代都要乘以Math.cos(lat)。你直接用(nextLon - prevLon) * earthRadius算东西向梯度的话,高纬度地区会严重失真。

第二,坡向角的定义。GIS 里坡向是坡面面向的方向(坡面法向量在水平面上的投影),不是坡面下降的方向。所以代码里传到getAspectAzimuthDeg时梯度分量都取了反号:

letaspectDeg=this.getAspectAzimuthDeg(-dhdEast,-dhdNorth);

dhdEastdhdNorth是高度沿东西、南北方向的变化率。坡面上升最快的方向是(dhdEast, dhdNorth),但坡向要输出的是坡面朝哪个方向,所以取反——下降最快的方向和坡面朝向一致。

坡向角到色带

算出来的 0°~360° 方位角,要映射到色带颜色上。代码支持 4 种内置色带:

色带视觉风格适合场景
++八方向++8 色离散,区分度高快速识别 N/NE/E/SE/S/SW/W/NW
++冷暖方位++蓝→白→红渐变南北坡对比明显
++彩虹方位++多色渐变精细方位辨识
++灰度方位++黑白渐变叠加在影像上不抢视觉

色带切换是onChange→ 直接触发reanalyzeIfReady()全程实时重算,不用等、不用手动点刷新。

可视化:Canvas 叠图 + 箭头,两个图层各有门道

Canvas 叠图:给每个格网"画"一个像素

这部分是整个工具最"手工"的地方。计算完所有格网的坡向后,不是用 Cesium 的 Entity 或 Primitive 来渲染——而是直接画到 Canvas 上,再把 Canvas 转成SingleTileImageryProvider贴到地球上。

letcanvas=document.createElement("canvas");canvas.width=cols;canvas.height=rows;letcontext=canvas.getContext("2d");letimageData=context.createImageData(cols,rows);// ... 逐像素 fillcontext.putImageData(imageData,0,0);letprovider=newCesium.SingleTileImageryProvider({url:canvas.toDataURL("image/png"),rectangle,});this.resultLayer=this.viewer.imageryLayers.addImageryProvider(provider);

这个方案的好处是:

  • 性能好——无论多少格网点,最终都是一张图
  • 渲染可控——每个像素的颜色、透明度完全自己掌控
  • 不需 Entity——不用创建成千上万个 Entity 来渲染

但它有个限制:Canvas 的cols × rows不能太大。我试过 1000×1000,浏览器直接卡死。代码里用Math.min(1000, ...)做了上限保护——这个保护不是可有可无,是生产环境必须加的。

箭头:坡向的方向可视化

色带用颜色表示方向,但人眼对颜色的方向感知其实不直观——东是红的、西是蓝的,这需要看色卡才能读懂。所以代码里又加了一层箭头层——在格网范围内按固定间隔绘制箭头,箭头从低处指向高处(坡向的反方向…不对,应该是指向坡面朝向)。

等一下,让我再确认一下箭头的方向:

getArrow(lon,lat,lonLen,latLen,aspectDeg){letrad=Cesium.Math.toRadians(aspectDeg);letdx=Math.sin(rad)*lonLen;letdy=Math.cos(rad)*latLen;return{startLon:lon-dx*0.5,startLat:lat-dy*0.5,endLon:lon+dx*0.5,endLat:lat+dy*0.5,};}

aspectDeg是坡向的方位角——坡面面向的方向。箭头从中心向(dx, dy)方向延伸,这就是箭头指向的方向。所以箭头指向的就是坡面朝的方向——(地理上北是 0°,箭头往北就是朝北坡)。

箭头用的是 Cesium 的PolylineArrowMaterialProperty,以线条形式 clamp 在地形上,方向感非常直截了当。arrowStep控制箭头间距——值越小箭头越密,但性能也越差。默认 10 格一跳,100×100 的网格大概产生 100 个箭头,渲染压力不大。

交互:矩形绘制也得自己搞

Cesium 没有内置的"画矩形选择范围"功能,得自己实现。代码用的是ScreenSpaceEventHandler

  • 第一次左键点击 → 记录第一个角点
  • 鼠标移动 → 实时预览半透明青色矩形
  • 第二次左键点击 → 确定范围,触发分析

这里有 一个容易忽略的坑:矩形绘制用LEFT_CLICK,但 Cesium 默认的LEFT_CLICK还会触发选中 Entity。如果你的场景里有很多 Entity(比如之前分析留下的箭头),点下去可能选中的是箭头而不是角点。代码里clearAll()再开始绘制,解决了这个问题。

另外,预览矩形的heightReference: CLAMP_TO_GROUND让它贴在地形表面,classificationType: TERRAIN确保它跟随高低起伏——而不是悬在空中。

写在最后

回头看这个工具,最有价值的不是"坡向分析"本身——桌面 GIS 早就有这个功能了——而是它证明了前端可以做空间分析这件事,而且能做到可交互、可调参、即时出结果。

有几个经验以后做类似项目时能复用:

把采样计算和 Canvas 渲染拆成独立步骤。采样参数改了 → 重新跑计算 → 重新画 Canvas。每次改渲染参数(透明度、色带)→ 只重建 Canvas 叠图,不重新采样。这样参数切换的响应速度从几秒降到几十毫秒。

能用一个图层解决就别用 Entity。SingleTileImageryProvider是 Cesium 里被低估的功能——可以当成一个"万能贴图工具"用。

地形精度和性能是跷跷板。terrainLevel+gridCols/gridRows这两个参数决定了计算耗时。100×100 + level 12 大概 2-3 秒,是一个比较甜的性能点。

说说不足——这个工具目前最明显的短板是不支持不规则范围。只能画矩形,多边形不行、流域边界更不行。这是下一步要考虑迭代的方向。如果读者有需求,我后面补一篇多边形的实现。

这篇文章的完整代码可以在aspect_analysis.html里找到,配图用的色带效果见正文。有问题或更好的实现方案,欢迎留言交流。

完整代码

见原文:https://mp.weixin.qq.com/s/iewp3NNTWps0EhcoX7QwNQ

关注公众号 “GIS 开发手记”,及时获取首发内容!

往期精选

  • Cesium 空间分析:坡度分析
  • Cesium 通视分析:3D 城市模型上"划线"看可见区
  • Cesium 3D 热力图:从 Canvas 热力到 GPU 顶点着色器
  • Cesium 点聚合:海量点地图可视化方案
  • 自定义虚线箭头材质:把两个内置 Material 焊在一起
  • Cesium 中文字体:贴地 / 贴墙 / 动态文字方案
  • 网络地图坐标系完全指南:WGS84 / GCJ02 / BD09 / CGCS2000 与坐标转换实战
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/7/29 11:24:54

三维凹凸地形图渲染实战(1):软件工具下载与安装

我们在《三维凹凸地形图渲染实战&#xff1a;课程简介》一文中&#xff0c;为你详细介绍了渲染凹凸地图的必要性、数据来源以及应用场景。 或许你已经跃跃欲试&#xff0c;想要亲手制作一张炫酷的三维地形图了。 正所谓“工欲善其事&#xff0c;必先利其器”&#xff0c;在开…

作者头像 李华
网站建设 2026/7/29 11:24:40

数字人对口型技巧,2026年音频驱动数字工作流,5款横评实测

数字人口型对不上&#xff0c;问题到底出在哪 做口播、做数字人分身&#xff0c;最怕的就是嘴型跟声音对不上——观众看两秒就划走。很多人一上来就找「数字人对嘴型工具」&#xff0c;其实真正影响效果的不是某一款软件&#xff0c;而是整条音频驱动数字人的工作流&#xff1…

作者头像 李华
网站建设 2026/7/29 11:24:36

Visual Studio 2022 C++项目创建与配置全指南:从环境搭建到调试优化

1. 项目概述&#xff1a;为什么从VS2022开始你的C之旅如果你正准备踏入C编程的世界&#xff0c;或者从其他开发环境迁移过来&#xff0c;选择Visual Studio 2022作为你的第一个“工坊”&#xff0c;这个决定本身就非常明智。我见过太多新手在配置环境这一步就耗尽了热情&#x…

作者头像 李华
网站建设 2026/7/29 11:24:30

Python对象序列化技术详解与最佳实践

1. Python对象序列化的本质与核心诉求对象序列化&#xff08;Object Serialization&#xff09;是将内存中的对象转换为可存储或传输的字节流的过程&#xff0c;这个过程对于分布式计算、数据持久化和进程间通信至关重要。在Python生态中&#xff0c;我们通常需要处理三种典型场…

作者头像 李华
网站建设 2026/7/29 11:23:48

Python多线程编程实战:从基础到爬虫优化

1. Python多线程编程核心概念解析多线程编程是现代软件开发中提升程序执行效率的重要手段&#xff0c;特别是在处理I/O密集型任务时效果显著。Python通过内置的threading模块提供了对多线程编程的支持&#xff0c;虽然由于GIL&#xff08;全局解释器锁&#xff09;的存在&#…

作者头像 李华
网站建设 2026/7/29 11:23:43

SSM框架实现社区空巢老人帮扶管理系统开发指南

1. 项目背景与核心价值社区空巢老人帮扶管理系统是一个典型的毕业设计选题方向&#xff0c;它结合了当前社会老龄化趋势和技术栈的实用性。2026届计算机相关专业学生选择这个题目&#xff0c;既能体现社会责任感&#xff0c;又能展示SSM框架和Java技术的综合应用能力。这个系统…

作者头像 李华