SuperMap iClient3D动态日照阴影实现:从时钟驱动到性能调优
发布时间:2026/10/14 21:02:18 锦皓数字建站

1. 动态日照阴影到底在解决什么问题——先从项目现场说起前阵子帮朋友接了个城市设计辅助决策的小项目需求一句话就能讲清楚规划地块上拟建几栋高层住宅甲方想看不同楼栋在冬至日、大暑日分别能挡住多少阳光而且要“看得见”光是怎么随时间移动的不是拿一张截图糊弄过去。于是就想到了用 SuperMap iClient3D for WebGL 来做动态日照阴影效果。说实话这个需求在三维GIS里非常典型但真正动手做的时候发现网上能查到的资料大多停留在“开启阴影”“查看ShadowMode”这种浅层用法很少有人把太阳轨迹、时钟驱动、性能开销和踩坑一起讲透。我先把结论撂在这SuperMap iClient3D for WebGL 完全有能力做出效果很好的实时动态日照阴影核心思路是利用它继承自 Cesium 的时钟Clock系统把高精度太阳位置计算和阴影贴图渲染联动起来再用clock.onTick事件逐帧或逐秒推进时间让阴影自然流动。整个过程不涉及任何自定义着色器关键是对几个 API 的理解要到位以及知道数据、场景和性能之间的取舍。这篇文章适合谁看正在做三维城市、数字孪生、规划辅助决策的开发者被领导要求“把影子动起来”但不知道从哪下手的人以及已经被阴影性能问题折磨到怀疑人生的兄弟。我会把从原理、代码到调优、排障的完整链路写出来你可以直接拿去当开发参考。2. 底层原理太阳位置、时钟和阴影贴图是怎么串起来的2.1 太阳不是美术做出来的是算出来的很多人在做三维场景时习惯性想把“阳光”当成一个可拖动的灯光对象来处理这其实是个认知误区。SuperMap iClient3D for WebGL 里的太阳本质上是一个由天文算法驱动的方向光源它的位置完全由当前时刻和观察地点的经纬度决定不允许你手动拖一个点光源放在楼顶。这个设计从物理上讲非常合理——日照阴影分析要的就是严格符合真实太阳轨迹的效果而不是“看起来差不多”。框架内部通过太阳赤纬、时角等参数计算太阳在特定经纬度、特定时刻的高度角和方位角然后生成平行光方向。这里的平行光方向就是阴影渲染的核心输入。我们不需要知道框架用的到底是哪一套天文算法只需要明白一件事只要改变场景时间太阳方向就会变阴影方向就会跟着变。这也是“动态”能够实现的物理基础。2.2 三个开关光照、阴影和地形阴影的关系在 SuperMap iClient3D for WebGL 里要把阴影真实地渲染出来至少涉及三个开关很多人只开一个然后就发现问题了。第一个是viewer.scene.globe.enableLighting这个开关控制地球表面的光照响应。打开后地形和模型会根据光线方向产生明暗变化注意这里只有明暗不产生投影。第二个是viewer.shadows这是整个场景的阴影总开关。第三个是viewer.terrainShadows它控制地形本身是否参与阴影投射和接收。三个开关的关系可以这样理解enableLighting决定“有没有光”shadows决定“谁产生影子”terrainShadows决定“地形要不要被影子覆盖”。我见过不少项目只开了viewer.shadows true然后发现模型是亮的但地面一片空白没有影子原因就是terrainShadows还处于禁用状态。也有项目反过来打开了enableLighting但没打开shadows发现场景只是变暗了根本没有阴影轮廓。所以动手之前先把这三个开关的状态想清楚后面排查问题能省一大半时间。2.3 ShadowMap阴影贴图的工作原理动态阴影效果在 WebGL 里几乎都是通过 Shadow Map阴影贴图实现的。它的逻辑不复杂但你可以把它想象成“从太阳的视角拍一张深度照片”框架先把场景整体渲染到一张深度纹理上记录离太阳最近的那些表面的距离然后再从人眼视角渲染场景时逐像素比较自己到光源的距离和这张深度照片上的记录如果自己更远说明被遮挡了就画成阴影。这套机制里最关键的两个可调参数就是阴影贴图的分辨率和采样过滤方式。分辨率越高阴影边缘越锐利但开销越大过滤方式决定了阴影边缘是硬边还是柔边。SuperMap iClient3D for WebGL 对这部分做了封装我们不需要去手写 shader但要理解这些概念因为后面所有性能调优都建立在这几个参数上。2.4 Clock 才是“动态”二字的灵魂Cesium 体系里有一个贯穿始终的概念——Clock。你可以把它理解成一个虚拟时钟它有自己的当前时间、开始时间、结束时间和运行倍速。场景里的太阳位置、星星位置、地球自转全部由这个虚拟时钟驱动。viewer.clock就是那个时钟实例viewer.clockViewModel则是对外暴露可绑定的状态。动态日照阴影的实现思路就是让这个虚拟时钟持续走动每走一步太阳方向更新一次阴影贴图重新渲染一帧人眼看到的就是阴影在缓慢移动。如果想让时间走得快就调大multiplier想让阴影动得连续要注意不要把这个倍速调得太过否则每帧之间阴影跨度太大视觉上就会出现跳变。3. 动手实现一个可用的动态日照阴影代码骨架3.1 环境和数据准备我平时用的是 SuperMap iClient3D for WebGL 的常规包版本数据是常见的三维切片S3M格式加上倾斜摄影模型。做日照阴影测试时建议先用一个小范围场景楼体模型放在平坦地形上标高和地理位置要准确因为太阳轨迹计算依赖经纬度坐标错了影子方向和长度全不对。有一个数据问题值得特别提醒如果你的建筑模型是直接放在地表上方贴地的一定要确认模型底部高程和地形匹配否则阴影会悬空或者陷进地里。我习惯在服务端把模型的高程信息处理好保证模型整体贴地但不穿透这比在前端硬调位置要可靠得多。3.2 三行代码开启基础阴影先把最核心的三行代码给出来。这是整个效果的基础后面的时间驱动都是建立在这三行之上的。viewer.scene.globe.enableLighting true; viewer.shadows true; viewer.terrainShadows Cesium.ShadowMode.ENABLED;写完这三行之后你在场景里应该能看到一个静态的阴影效果影子方向和当前viewer.clock.currentTime对应的太阳位置吻合。这里我建议你先用Cesium.JulianDate.fromDate(new Date(2024-12-22T10:00:0008:00))把当前时间设置成一个具体的冬至上午十点方便验证影子方向。有两点要说明一下。一是ShadowMode.ENABLED只影响地形如果你希望在特定模型上也启用类似机制需要保证模型所在图层支持阴影计算。二是如果你用的是旧版本 SDK有些 API 名可能不完全一致最好到当前版本的接口文档里确认一下viewer.terrainShadows是否存在如果没有就退而求其次只开viewer.shadows。3.3 让时间流动起来Clock 的配置方法静态阴影看得见之后接下来就是“动”起来。这一步的核心是配置时钟区间和步进。const start Cesium.JulianDate.fromDate(new Date(2024-12-22T06:00:0008:00)); const stop Cesium.JulianDate.fromDate(new Date(2024-12-22T18:00:0008:00)); viewer.clock.startTime start; viewer.clock.stopTime stop; viewer.clock.currentTime start; viewer.clock.clockRange Cesium.ClockRange.LOOP_STOP; viewer.clock.multiplier 60; // 每秒走一分钟12小时动画约12分钟走完 viewer.clock.shouldAnimate true;这里multiplier是 60代表现实时间每秒对应虚拟时间一分钟。如果整个动画要走完冬至日的 12 小时就是 12 个现实分钟体感比较舒适。如果你在做演示、汇报建议这个值调到 120 到 300 之间视觉效果更明显也更赶时间。clockRange设为LOOP_STOP表示走到stopTime后从头再来这样演示的时候可以循环播放。如果你希望时间走到某个点停下来可以改用CLAMPED。3.4 用 onTick 驱动阴影平滑推进时间在走但如果什么都不做场景并不会自动重新计算太阳方向。还要一个 Hook 把时钟的每个 Tick 和下一次渲染连起来。常规做法是监听clock.onTick然后在回调里手动刷新一下光源相关状态。viewer.clock.onTick.addEventListener(function (clock) { // 确保太阳方向随当前时间变化而更新 viewer.scene.globe.enableLighting true; // 如果你有自定义光源可以在这里更新光源方向 if (viewer.scene.light viewer.scene.light instanceof Cesium.SunLight) { // 框架内的太阳光会自动读取当前时间通常不需要手动设置 } // 针对某些版本可以强制更新太阳方向 viewer.scene.sun.show true; });这里想分享一个经验如果你发现时间在走但光影不动先检查viewer.clock.shouldAnimate是不是 true如果已经是 true 但依然不动尝试不依赖回调而是通过设置viewer.timeline的当前时间来做驱动。onTick触发条件是时钟在动画状态如果时钟被暂停回调就不会触发这时候需要检查是不是有别的代码把时钟停了。3.5 一个完整的日落过程示例为了让你更有体感我给出一个能直接跑到“太阳从东到西、影子从长到短再到长”的简化代码。假设你已经加载了场景和建筑模型// 初始化场景基本阴影 viewer.scene.globe.enableLighting true; viewer.shadows true; viewer.terrainShadows Cesium.ShadowMode.ENABLED; // 配置时钟模拟一天 const start Cesium.JulianDate.fromDate(new Date(2024-06-21T05:30:0008:00)); const stop Cesium.JulianDate.fromDate(new Date(2024-06-21T19:30:0008:00)); viewer.clock.startTime start; viewer.clock.stopTime stop; viewer.clock.currentTime start; viewer.clock.clockRange Cesium.ClockRange.LOOP_STOP; viewer.clock.multiplier 120; viewer.clock.shouldAnimate true; // UI联动点击按钮加速/暂停 function togglePlay() { viewer.clock.shouldAnimate !viewer.clock.shouldAnimate; }跑起来后你会看到建筑的影子随着太阳方位角变化从清晨朝向西北缓缓转到傍晚朝向东北长度也从长变短再变长。这个过程如果方向反了说明地理坐标或时间没配对比如用了东八区的时间但数据位置却在其他时区太阳轨迹就会出现明显偏差。4. 性能实测与调优阴影不是白来的是要花钱买的4.1 阴影贴图的开销到底有多大阴影效果的代价是真实的不是免费的。我在一个中等规模场景上做过对比建筑模型几十万面地形范围大概几平方公里开启阴影之后帧率从 50 掉到了 35 左右掉幅约三成。如果场景更大、分辨率更高掉得更狠。阴影耗性能的根本原因在于 Shadow Map 需要从光源视角完整渲染一遍场景。也就是说原本每帧只需要渲染一遍画面开了阴影之后至少渲染两遍一遍生成阴影贴图一遍生成最终画面。场景越复杂这第二遍的代价越高。如果项目运行在配置不高的机器上我建议不要盲目追求“全场所有建筑都有精细阴影”。可以选一个动态范围限制方案比如让镜头焦点区域的模型参与阴影计算远处模型不参与或用较低精度。当前版本里这个控制不一定在框架 API 上直接露出来所以实践中更多是靠管理可见性来实现。4.2 级联阴影 vs 单层阴影怎么选Cesium 体系中的阴影映射默认有一种级联阴影CSM的实现思路把视锥体按深度切分成多层分别计算不同范围的阴影贴图。这样做的好处是近距离阴影清晰、远距离阴影逐步模糊视觉体验接近游戏引擎的效果。但级联阴影也有代价多层贴图意味着更多的渲染Pass和更多的显存占用。如果你的项目主要看城市大范围鸟瞰视角单层阴影 较大贴图范围就够用如果经常推近看楼间距、看庭院阴影那就需要级联阴影来保证近处阴影不糊。我个人的建议是大场景演示用默认配置单层阴影即可把资源省给地形和模型加载如果是做日照分析专题图近景查看比例高可以显式开启更细的阴影模式并在需求层面明确“是否需要每帧实时更新阴影”至少可以降刷新频率。4.3 实测的性能对照表以下是我在自己机器上台式机、中等独立显卡跑同一组数据时的实测对比配置仅供参考让你心里有个谱配置项帧率表现阴影观感适用场景不开阴影50 fps无投影加载预览、快速浏览开启基础阴影默认贴图38 fps影子清晰边缘略锐动态演示、方案比选开启阴影 调低贴图分辨率43 fps影子边缘轻微锯齿大范围地形场景开启阴影 全屏抗锯齿30 fps影子柔和更真实近景日照分析截图根据我的经验如果帧率低于 25 且操作明显卡顿优先检查是不是阴影贴图开了 4096 甚至 8192 分辨率。这种情况下降到 2048画面观感几乎不受太大影响但性能能回血 10% 到 15%。很多人一上来就追求最高画质结果实际项目里根本跑不动反而把阴影功能砍掉了——这是很可惜的。4.4 移动端和轻量部署的取舍如果你的项目最终要跑在平板或者普通笔记本上那要提前做好性能预算。移动端 GPU 的填充率有限阴影贴图的每个像素都在花钱所以我见过很多移动端项目干脆只开光照不开阴影或者只在镜头停留在某个角度时才临时打开阴影。一个非常实用的小技巧是根据镜头高度动态开关阴影。鸟瞰大范围时关闭阴影省性能推到近景需要观察楼间距时再打开。这虽然不完全算“动态日照”但配合时钟驱动后在演示场景里已经能应付绝大多数规划沟通需求。我在实际项目里用这个策略从全局 40 帧掉到近景 30 帧属于可以接受的范围。5. 最容易翻车的几个坑和完整排查链路5.1 坑一时间在走太阳和阴影却纹丝不动这是大家最常遇到的问题。表现是代码全部写完时钟也在跑连currentTime都变了但是场景里的光影像钉子一样定住。当时我排查这个坑时走了不少弯路最后发现原因其实很朴素shouldAnimate虽为 true但有个初始化操作把clock.clockRange设置成了CLAMPED且currentTime已经越过stopTime时钟直接到了结束点不再前进。完整的排查顺序建议是先打印viewer.clock.currentTime在onTick里有没有变化再确认当前时间在 start 和 stop 之间再确认viewer.clock.shouldAnimate true最后检查是否有第三方代码动了currentTime。这四步走完基本能定位到问题位置。还有一类场景也要注意就是浏览器标签页被切到后台后浏览器本身会限制定时器频率导致onTick长时间不触发。这种情况不是代码问题是浏览器机制重回前台时会自动恢复不用过度纠结。5.2 坑二阴影和地形交界处出现“闪缝”或“断层”地面上一栋楼楼底和地形接触的位置有时候会出现一道肉眼可见的亮缝就好像楼是飘在地面上一样。这个问题的根因我在前面提到过——模型高程没有和地形完美贴合或者是阴影贴图精度不够导致接触面的深度测试出现误差。解决思路分两步。第一步是数据处理在原始模型制作时确保建筑底部的底面和地表高程精确匹配或者至少让底部微低于地表几厘米避免悬空。第二步是调整阴影贴图的深度偏移Depth Bias适当的偏移可以让阴影边缘的深度比较不会因为精度问题出现细碎闪烁。具体参数在当前版本的 API 里以内部设置形式存在没有暴露一个公开的属性所以主要还是靠数据侧解决。5.3 坑三模型整体发黑像烧焦一样开了enableLighting之后某些模型会变得特别暗即使太阳在头顶颜色也怪怪的。这其实是材质的光照响应问题。模型的 PBR 材质如果法线方向不对或者纹理自发光属性为零就会在平行光下显得非常暗。你可以简单理解为模型表面没有把照到它身上的光“反射”回相机于是人眼看着就是黑的。排查时可以先把enableLighting关掉如果模型恢复正常颜色说明问题出在光照参数上。此时需要检查模型的材质设置看看是否因为法线翻转导致光源方向计算错误。这种问题通常要在建模阶段解决前端代码能做的很有限。如果你急着演示可以暂时把enableLighting调成适用亮度的较低值而不是完全关闭这样既有一定光照感又不会黑过头。5.4 坑四阴影边缘锯齿严重或者快速闪动这属于 Shadow Map 的老朋友——阴影锯齿Shadow Acne和边缘走样。很多人在写代码时都喜欢把viewer.shadows true当成万能开关但一旦看到锯齿就开始怀疑框架不行其实这是阴影贴图的固有问题。缓解锯齿有几种思路一是降低阴影覆盖范围让贴图像素更密集地覆盖可见区域这在某些版本里可以通过调整相机远近距离间接实现二是把阴影效果呈现的粒度做大比如只开启区域性的方向阴影而不是整场景的小尺寸阴影三是在最终渲染时打开抗锯齿选项这对阴影边缘的改善非常明显。如果你只需要“截图给甲方看”甚至可以临时调大抗锯齿等级画面干净很多。6. 从阴影到日照分析辅助功能的接入思路6.1 日照时长统计的思路动态日照阴影只是最直观的视觉表现但真正的项目需求往往不止于此。比如甲方经常问“这栋楼一天能有几个小时被太阳晒到”这需要把视觉阴影和计时统计结合起来。我的做法是在前端页面上加一个统计按钮点击后让时钟以较快速度跑一整天比如multiplier 3000每到一个时间点就采样目标楼体外立面某个片元是否处于阴影中。这个采样可以利用阴影贴图结果也可以在简化场景下用“太阳方向 遮挡体轮廓”做估算。前者精度高但性能消耗大后者能给出近似结果更适合演示。想要快速实现时可以通过scene.pickPosition或Cartesian3计算目标点和太阳位置判断两点连线是否被建筑物遮挡。这种算法虽然不如 ShadowMap 精确但胜在可控性高而且可以离线跑批量计算。6.2 阴影叠加到专题分析图层日照阴影动态播放在演示时很直观但我们经常还需要把它和历史分析成果叠加在同一视图里。比如把某时段的阴影范围画成一个多边形贴在地面上然后用半透明颜色填充这样不依赖 3D 阴影也能展示日照遮挡范围。做法也不复杂遍历当前时刻的所有建筑物对每个建筑轮廓做基于太阳方向的投影几何计算取所有建筑的投影并集再叠加到场景里。这里比较容易忽略的细节是地形起伏不能只当作平面来处理。在起伏地形上建筑投影末端位置要根据地形高度做修正否则投影范围和真实阴影在斜坡处会有可见偏差。如果你用的是平地场景按平面处理问题不大但遇到山地项目最好还是采用基于地形高度做逐点投影延伸的方式。6.3 和后端计算的联动什么时候用前端什么时候用服务端很多团队做到最后会发现动态日照阴影在前端展示很炫但要完成精确的日照时长统计、冬至日累计日照小时数、遮阳率这些计算前端实时算法并不够精准。这个问题的本质是实时渲染要保证流畅度必然会对物理过程做一些近似而分析计算需要的是确定性结果。我目前比较推荐的做法是前端做“量级正确”的实时演示后端做“精确无误”的离线计算。后端的计算可以利用高精度天文算法和几何体求交算出关键楼栋在关键日期的日照时段表再把结果表传到前端前端用动态阴影做视觉印证。两者配合既满足领导“想看影子动”的体验要求也满足分析报告“用数据说话”的专业要求。经验小结和一点建议做动态日照阴影这个功能回头看我踩过的坑最大的教训其实是尽量先理解底层机制再去写代码。很多人上来就抄 API结果遇到问题只能一个个试效率很低。建议你在写代码前先把阳光方向、时间驱动、阴影贴图这三条线在脑子里串明白再动手就顺了。另外一个小建议是动态日照阴影在项目汇报时效果很好但不要让它成为系统的全部卖点。真正有用的往往是它背后的分析数据——日照时长、阴影范围变化、建筑间距对采光的影响。把好看的动效和数据结合起来这个功能才算真正扎进业务里。
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。