做三维GIS开发的朋友应该都遇到过这种需求:领导拿着规划方案回来说,把这片楼群在不同季节、不同时段的日照阴影动态模拟一下,我要看看哪里被遮挡。传统做法是导出多张静态图,但真正实现无缝动态日照阴影,还是得靠 WebGL 三维渲染。SuperMap iClient3D for WebGL 作为直接嵌在浏览器里的三维GIS开发库,正好能把这一步做成一个可拖拽时间条、实时滚动播放的日照动画,而且不用装插件、不用预渲染,加载完场景就能跑。
这篇文章我结合自己在某规划展示项目里的实操经验,把用 SuperMap iClient3D for WebGL 做动态日照阴影的完整方法拆给你看。从思路、环境准备、步骤实现到参数微调、问题排查,每个环节都给出我实际用过的配置和踩过的坑,适合刚接触三维GIS、或者已经在做Cesium二次开发但没玩过日照效果的人参考。
1. 动态日照阴影的核心思路与功能拆解
1.1 到底什么是动态日照阴影
日照阴影本身并不复杂,就是太阳光照射到建筑物后,在地面和相邻建筑上投下的暗区。太阳的位置决定了阴影的方向和长短:早上太阳高度角低,影子长;中午太阳接近天顶,影子短;不同季节太阳轨迹不一样,阴影自然也会变。
动态日照阴影,简单说就是让太阳的位置随着时间连续变化,阴影也跟着实时更新。它不同于传统方案里把几张时间点的截图让用户手动切换,而是一段连续可交互的动画,用户可以通过时间轴任意跳到某个时刻,看到那一刻的真实阴影状态。这在城市规划、建筑日照分析、太阳能选址、周边遮挡评估这些业务里非常实用。
1.2 核心能力与适用场景
SuperMap iClient3D for WebGL 在 Cesium 三维地球引擎基础上做了深度的数据渲染和场景管理优化,所以不光能显示卫星影像、地形、倾斜摄影模型,还能控制场景中太阳的方位和强度,让阴影贴图跟随太阳位置实时计算。
我实际接触到的需求中,最多的场景是建筑设计方案评审。设计院把一个片区的倾斜摄影模型或者手工白模导入场景,然后要求模拟出从早晨八点到傍晚六点不同时段的阴影范围,用来判断相邻楼栋之间的日照间距是否达标。另外还有些客户做屋顶光伏评估,需要统计不同季节某几个屋顶的有效日照时长,这时动态日照阴影可以直观地展示阳光照不到的区域。
1.3 实现的技术要点
要在 WebGL 环境下实现动态日照,核心是三件事:太阳位置计算、阴影实时渲染、时间轴联动。
太阳位置计算需要根据当前日期和时间、观测点的经纬度算出太阳的高度角和方位角。阴影实时渲染依赖 WebGL 的阴影贴图(Shadow Map)技术,把太阳方向当作灯光方向,先生成深度图再投影到场景中。时间轴联动则通过三维引擎统一的时钟系统,让太阳位置、阴影、光照强度全都绑定到同一个时间变量上,用户拖动时间条时场景自然随之变化。
SuperMap iClient3D for WebGL 实际上把这三件事封装成了几个可以直接调用的 API,这也是它比从底层写 WebGL 省事得多的原因。接下来我会按环境准备、代码实现、参数调节的顺序把完整流程走一遍。
2. 环境准备与基础场景配置
2.1 引入库与初始化三维场景
首先是搭建页面框架。SuperMap iClient3D for WebGL 支持模块化引入,也有打包好的独立脚本文件。我习惯在 Vite 项目里用 npm 包引入,按需加载,还能配合分包策略减小首屏体积。
import { GlobalAnimation } from '@supermap/iclient3d-webgl'; import * as Cesium from 'cesium';初始化场景时,需要指定一个容器 div,然后创建 Viewer 实例:
const viewer = new Cesium.Viewer('sceneContainer', { animation: true, // 显示动画控件 timeline: true, // 显示时间轴 shouldAnimate: true, // 默认自动运行 baseLayer: { url: 'https://example.com/your-map-tile', isTileLayer: true } });这里注意shouldAnimate代表时钟是否自动走动。如果做动态日照,建议一开始就打开,让太阳在场景里“活”起来。如果只是静态展示某个时刻,那把时间设为 fixed 也完全可以。
2.2 加载影像、地形与模型数据
场景数据是日照阴影的“画布”。影像图层通常用本地发布的瓦片服务或在线底图,地形用 DEM 数据加载,模型则分倾斜摄影和手工模型两种。日照阴影一般关心建筑物的阴影,所以至少要有一批带高度信息的模型数据。
viewer.scene.addS3MTilesLayer('https://example.com/tiles/building', { name: '某片区建筑' });我在项目中遇到过一个细节:如果是倾斜摄影模型,需要确保它有正确的坐标和高度信息。如果模型自己漂在半空中或者整体偏了,阴影的位置会完全错乱。加载完成后最好先飞到某个建筑,检查模型与影像是否贴合。
2.3 设置场景时间与坐标系
动态日照的核心驱动是时间。SuperMap iClient3D for WebGL 继承了 Cesium 的时钟机制,从viewer.clock可以读取和修改当前时间。设置上需要注意统一成东八区,否则会出现太阳位置和本地时间对不上的问题。
const startTime = Cesium.JulianDate.fromDate(new Date('2024-06-22 08:00:00')); const stopTime = Cesium.JulianDate.fromDate(new Date('2024-06-22 18:00:00')); viewer.clock.startTime = startTime; viewer.clock.stopTime = stopTime; viewer.clock.currentTime = startTime; viewer.clock.multiplier = 60; // 每分钟模拟60秒,即以60倍速度播放multiplier决定时间流逝速度,60 表示现实中的1秒对应场景中的1分钟,一个白天的10小时只用10分钟就能播完。这组参数我一般放在初始化函数里,后面动态日照逻辑都基于这个时钟。
3. 实现动态日照阴影的完整步骤
3.1 开启阴影并设置太阳参数
有了场景和时间系统,最核心的一步是把阴影打开。SuperMap iClient3D for WebGL 在场景级别提供了阴影控制开关,默认可能是关闭的,需要显式置为 true。
// 开启阴影 viewer.scene.shadowMap.enabled = true; // 可选:开启深度偏移,防止阴影和地面出现条纹 viewer.scene.shadowMap.normalOffset = true; // 设置阳光方向(一般不用手动设,会跟随时间自动计算) viewer.scene.globe.sun = Cesium.Sun;这里的底层原理是,引擎会根据当前时间计算出太阳在天空中的位置,然后把它作为平行光源的方向,通过多级阴影贴图生成整个场景的分层阴影。开启后,即使你不写任何动画逻辑,只要场景里有模型,也能看到当前时刻的静态阴影。
3.2 绑定太阳路径与时钟联动
当viewer.clock开始走动,太阳位置并不会自动跟着更新,还需要把太阳的方向和时间绑定起来。在 Cesium 内核里,太阳本身是场景里的天空球对象,会影响环境光照。在 SuperMap iClient3D for WebGL 中,我通常用动画循环的方式,每秒同步一次太阳位置。
viewer.clock.onTick.addEventListener(function () { const now = viewer.clock.currentTime; // 更新太阳位置:根据场景经纬度、时间和日期计算 const solarPos = getSolarPosition(lng, lat, Cesium.JulianDate.toDate(now)); scene.light.direction = Cesium.Cartesian3.normalize(solarPos, new Cesium.Cartesian3()); });这里的getSolarPosition需要自己写一个太阳位置算法。其实也可以直接修改场景中太阳实体的位置,让它始终与当前时间匹配。下面是我在项目里用过的简化算法:
function getSolarPosition(longitude, latitude, date) { const rad = Math.PI / 180; const dayOfYear = getDayOfYear(date); // 太阳赤纬角(近似公式) const declination = 23.45 * Math.sin(2 * Math.PI * (284 + dayOfYear) / 365) * rad; // 时角,正午为0,每小时15度 const hourAngle = (date.getHours() - 12) * 15 * rad; // 纬度弧度 const latRad = latitude * rad; // 太阳高度角 const elevation = Math.asin( Math.sin(latRad) * Math.sin(declination) + Math.cos(latRad) * Math.cos(declination) * Math.cos(hourAngle) ); // 太阳方位角(略,忽略大气折射) const azimuth = Math.atan2( Math.sin(hourAngle), Math.cos(hourAngle) * Math.sin(latRad) - Math.tan(declination) * Math.cos(latRad) ); // 转成三维方向向量(北半球) const dirX = Math.cos(elevation) * Math.sin(azimuth); const dirY = Math.sin(elevation); const dirZ = Math.cos(elevation) * Math.cos(azimuth); return new Cesium.Cartesian3(dirX, dirY, dirZ); }这个函数虽然简单,但足够让太阳在一天里的轨迹符合大致规律。若要追求精确到分钟级别的城市日照阴影,建议引入专业天文算法库来计算太阳位置,减少误差。
3.3 时间轴驱动与动画控制
为了让用户能自由拖动时间,可以使用内置的 Timeline 控件。点击 timeline 上的不同时间点,时钟会跳转,进而自动触发太阳位置更新。你也可以在界面上加一个单独的“播放/暂停”按钮,控制时间动画的启停,这样比每次都靠键盘操作方便得多。
// 暂停 viewer.clock.shouldAnimate = false; // 播放 viewer.clock.shouldAnimate = true; // 直接跳到指定时间 viewer.clock.currentTime = Cesium.JulianDate.fromDate(new Date('2024-12-22 15:30:00'));如果你希望不依赖默认的 Timeline 控件,也可以自己写一个滑块,动态修改currentTime,效果是一样的。但要注意:直接设置currentTime时,如果shouldAnimate为 true,它会立刻被时钟继续推进,所以更好的做法是先暂停时钟,设置时间,需要播放时再继续。
3.4 调整阴影显示的细节参数
阴影不是开完就万事大吉,它有几个参数直接影响视觉效果。我常用的几个配置如下:
viewer.scene.shadowMap.maximumDistance = 2000; // 阴影可见最大距离,单位米 viewer.scene.shadowMap.size = 2048; // 阴影贴图分辨率 viewer.scene.shadowMap.softShadows = true; // 开启软阴影,更自然 viewer.scene.shadowMap.radius = 1.0; // 软阴影采样半径maximumDistance太大会让远处建筑和近处建筑的阴影精度互相打架,太小则远处没有阴影。合理的做法是先把相机放在需要重点观察的区域上空,设定一个刚好覆盖区域的数值。size是阴影贴图的分辨率,越大越精细,但性能开销也越高,我通常在离屏渲染分辨率是 2048 和 4096 之间选,弱设备上可以降到 1024。
软阴影能明显提升画面的真实感,但会对低端移动设备造成负担,所以要不要开得结合项目实际设备环境来定。
4. 太阳位置计算与参数微调实录
4.1 太阳高度角与方位角的工程化计算
前面给的简化公式在大多数场景够用,但一旦需要精确到“上午九点某个塔吊的影子会不会压到临时会议室”,就得上更严谨的计算。可以用 Solar Position Algorithm 或者内业常用的天文算法,把计算步骤拆成:儒略日、黄赤交角、太阳黄经、赤纬、时角、高度角、方位角。
不过写代码时不必自己从头造轮子。许多项目里直接用Cesium.solarPosition或者第三方库suncalc,两行代码拿到太阳坐标:
import { getTimes, getPosition } from 'suncalc'; const sun = getPosition(date, lat, lng); const elevation = sun.altitude; const azimuth = sun.azimuth;这样即插即用,还不用自己维护复杂的公式。我在做过一个日照分析项目后,就再也没写过纯手算的太阳函数了。
4.2 阴影效果与场景感平衡
日照阴影的最终效果不是“有影子就行”,而是要在画面观感和硬件性能之间找平衡。我整理了一份自己常用的参数对照表:
| 参数名 | 推荐值 | 作用 | 注意事项 |
|---|---|---|---|
| shadowMap.enabled | true | 阴影总开关 | 关闭后模型无影子 |
| maximumDistance | 500~2000 | 阴影影响范围 | 太大影响精度,太小远处无影 |
| shadowMap.size | 2048 | 阴影纹理分辨率 | 低端机可设 1024 |
| softShadows | true | 柔和阴影 | 性能开销大 |
| radius | 1.0~3.0 | 软阴影模糊范围 | 过大显得脏 |
| normalOffset | true | 消除阴影与表面的重叠条纹 | 模型贴地时建议开启 |
在调节时,我的经验是:先不追求真实度,把阴影开出来,确认方向和时间正确。第二步再调软阴影和距离,最后逐一测试性能。直接一上来拉满参数,往往会卡得没法看。
4.3 动态日照与场景其他光源的冲突
有些场景里为了好看会额外加环境光、半球光或者多个点光源。这时要注意,阴影贴图绑定的是主光源方向,增量光源也可能产生第二套阴影,形成“鬼影”。
我在某项目中遇到过这样的情况:建筑模型既开了阴影贴图,又加了一个Cesium.DirectionalLight来补光,结果地面上出现了两套方向相反的阴影。解决办法是把自定义光源的affectShadow关掉,或者干脆让日照效果由场景默认的光照统一负责,不要叠太多光源。
更稳妥的做法是,在动态日照模式下只保留 sun 作为唯一光源,灯光只影响模型表面颜色,不参与阴影计算。
5. 常见问题与排查技巧实录
5.1 阴影不显示或断断续续
最常出现的情况是shadowMap.enabled = true之后,地面上一块阴影都没有。原因通常是:
- 模型没有高度信息,或者地形是平的但模型没有接地。
maximumDistance设置得太小,阴影被裁剪掉。- 相机距离模型太远,阴影贴图精度不够,影子已经淡到看不见。
- 太阳位置计算错误,阳光从地下照射。
排查时可以先把maximumDistance调到很大,比如 100000,看看阴影能不能出现,能出现就说明是距离问题,再逐步缩小。再看模型的包围盒和实际高度是否合理。
5.2 动画时间轴卡顿的问题
时间轴联动本身并不复杂,但一旦回调里写复杂逻辑,就会出现帧率下降。常见的高级数据可视化里会在onTick里同时更新业务数据、刷新弹窗、重新计算属性,导致一秒钟内执行多次高消耗运算。
优化办法很简单:在onTick回调里只做最轻量的状态同步,把费时的计算放到数据侧,用事件触发而不是每帧计算。比如某个时刻的阴影范围统计,可以预先算好全时段数据,动画过程中只查表。
另一个技巧是适当降低播放倍速。multiplier设为 5 到 20 之间,动画流畅度会明显好很多,因为在相同时间内,太阳位置变化更小,阴影跳变也更平滑。
5.3 太阳光照方向和阴影方向不匹配
有时候用户会反馈“早上影子应该朝西,为什么场景里朝南了?”多数原因是系统默认的太阳位置计算没有考虑场景所在经纬度,而用了一个固定的地心坐标原点。常见解法是在初始化时强制指定场景中心点的经纬度,并让太阳位置计算基于这个中心点。
5.4 性能优化与内存泄漏
阴影贴图是很吃显存的,尤其在多图层大场景里。如果频繁切换时间段,可能会造成内存持续增长。我在项目里有个习惯:在不需要显示阴影时,动态关闭shadowMap.enabled,比如切换到属性查询模式时,把日照效果暂停。另外每次从时间轴跳转前,先清除缓存的地理要素,再加载当前时间范围内的数据。
6. 实操心得与扩展思路
6.1 我在项目里踩过的坑
第一个坑是时区问题。有个项目用的服务在服务器上跑的东八区,但浏览器时间在本地用户设备上可能是任意时区,结果用户根据北京时间点击时间轴,太阳位置却总对不上。最后我统一把时间都转换成 UTC 表示,再用JulianDate.toDate转成本地时间计算太阳位置,问题才解决。
第二个坑是批量模型数量太大。几千栋楼同时参与阴影计算,即使开了软阴影,帧率也掉到个位数。后来我学了引擎的包围体裁剪,把离视点特别远或者特别小的建筑剔除出阴影参与队列,保住了重点区域的实时性。
6.2 扩展思路:从日照动画到日照分析
动态日照阴影真正落地以后,可以往上叠加很多业务功能。比如给每个建筑绑定一个“日照时长”统计模块,记录一天中每栋楼外立面被阳光照射的累计时间,颜色渐变着色。这种“体块日照热力图”在规划审查时特别有说服力,也能反过来验证建筑间距是否合规。
另外,动态日照可以跟天气系统联动:云层、雾、空气透明度都影响实际到达地面的光照强度。SuperMap iClient3D for WebGL 的材质系统可以叠加一个光照强度因子,把阴天效果也模拟出来。有了这些扩展,日照阴影就从一个“展示特效”变成了真正能辅助决策的分析工具。
这里借用户需要的内容,其实我最后还有一点想分享:做动态日照阴影时,不一定要一开始就把代码写得特别复杂。先用默认参数跑通整体流程,再逐步调优,是最效率的方式。特别是在三维GIS这种容器里,渲染状态的调试往往比业务逻辑更繁琐,只有多做几次完整流程,才会对阴影的脾气摸得越来越清楚。
如果你手头也正在做类似的项目,欢迎把参数调出来的截图发出来一起看看。毕竟日照阴影这东西,在屏幕前看着好看,和实际业务里真的能用,中间隔着的就是我们这些调试参数的日日夜夜。祝你的场景里,阳光始终准确。