ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Cesium光照与倾斜摄影日照阴影实战调优

Cesium光照与倾斜摄影日照阴影实战调优 1. 为什么光照和阴影是Cesium场景的门面做三维地球项目时间久了你会发现一个很尴尬的现象明明模型精度、纹理质量都砸了钱可画面一加载出来还是像贴了张扁平贴纸。问题往往不在数据而在光照。Cesium本身自带太阳光、天空、大气这一整套渲染链路但默认状态下的表现偏素尤其是加了倾斜摄影以后如果只开个基础光照楼房和地面的阴影关系是缺失的整个场景会显得浮、没有体积感。日照阴影效果的价值就在这儿——它让建筑投影落到地面、让山体有明暗过渡一眼就能看出哪栋楼更高、哪条街更窄。标题里要解决的是两件事叠加在一起的问题一是Cesium光照效果添加二是加载倾斜摄影后实现日照阴影。这两件事单独做都不难难在组合起来。倾斜摄影osgb转3DTiles后的模型通常数据量大、材质特殊、层级深很多情况下它的材质是烘焙过的、不带法线信息甚至部分是unlit无光照材质你直接开阴影会发现模型不吃光或者阴影只在部分瓦片上出现。所以这篇东西我主要讲三块光照与阴影渲染链路的底层逻辑、倾斜摄影模型的响应机制、以及一套能落地调参的实操流程。适合谁看做过基础Cesium项目、能跑起一个Viewer、加载过3D Tiles的人最合适。纯新手也能看但建议先把官方Viewer初始化那段跑通。我会把参数取舍、性能账、以及我自己踩过的坑都写出来不搞纯API罗列尽量做到你复制代码之后能直接改。先说结论这套方案的核心思路是用Cesium内置的太阳光作为主光源通过时间系统驱动太阳角度利用ShadowMap渲染阴影倾斜摄影tileset按角色分配CAST_ONLY或ENABLED再配合maximumDistance和softShadows控制性能与画质平衡。下面拆开讲。2. 光照与阴影渲染链路的核心原理解读2.1 Cesium光源体系是怎么工作的Cesium的默认光源是SunLight注意它不是一个可以随便摆放位置的点光源而是一个方向光方向由当前的模拟时间决定。你改变viewer.clock.currentTime太阳的方位角、高度角就会跟着变这跟真实天文计算是对齐的。很多人一开始想手动指定太阳位置绕半天其实正确姿势是调时间——你想让太阳在东边、高度角30度就去算对应的时间而不是去改光源向量。除了太阳光场景里还有SkyAtmosphere大气散射、SkyBox星空盒、Moon月亮。大气散射负责地平线那圈蓝白光晕它和光照是联动的scene.globe.dynamicAtmosphereLighting打开以后大气颜色会随时间变化。这里有个容易忽略的点如果地形和模型没有同时响应光照会出现地面亮、模型暗的割裂感这就是光不同步问题。光照对地形的控制主要是scene.globe.enableLighting对模型的响应则取决于模型自身的材质和tileset的shadows属性。这两套是分开的所以你会看到有人只开了地形光照模型依然是平的——这不是bug是没配对。2.2 阴影贴图ShadowMap的运作机制Cesium的阴影用的是ShadowMap技术简单理解就是从光源视角拍一张深度图记录每个方向最近的遮挡物渲染时再拿这个深度图去比对每个片元的深度落在阴影里的就压暗。方向光的阴影贴图是一张正交投影图覆盖范围有限所以才有maximumDistance这个参数超出范围的物体不参与阴影计算。关键参数我列一下这几个是你必须理解的scene.shadowMap.enabled总开关不开这个后面全白搭。scene.shadowMap.softShadows是否柔和阴影开了画面更好看代价是采样次数增加性能下降明显。scene.shadowMap.darkness阴影暗度0到1之间默认0.3左右调太高阴影死黑会很不自然。scene.shadowMap.maximumDistance阴影最大距离单位米。这个值直接决定阴影贴图的覆盖半径太小远处没阴影太大近处阴影糊成一团。scene.shadowMap.size阴影贴图分辨率默认2048往上调是2048的倍数比如4096画质提升但显存占用也涨。阴影贴图越精细、覆盖越大显存和GPU压力越大。这一块需要有性能预算意识不是所有场景都值得开4096。2.3 倾斜摄影模型为什么不吃光倾斜摄影数据本质是密集三角网纹理从原生的osgb转换到3DTiles后材质往往是按片烘焙的。问题出在两个地方第一个是法线。光照计算需要法线如果模型法线缺失或者朝向错误光照算出来就是平的、甚至反的。第二个是unlit材质。有些处理流程会把材质导成KHR_materials_unlit这种材质直接忽略光照你开多少光它都一个样。判断方法很简单把时间拨到夜里如果模型亮度几乎不变那就是unlit或者法线有问题。还有一点倾斜摄影的瓦片是有LOD层级的不同层级的瓦片可能来自不同的转换批次阴影开启后可能出现近处有阴影、远处瓦片没有的现象这通常是LOD切换时的属性不一致导致的需要在加载时统一设置。理解了这三层后面调起来就有方向了先确认光照链路通不通再确认模型是否响应光照最后调阴影参数。3. 基础场景与光照环境搭建实操3.1 Viewer初始化参数的取舍初始化这块很多人直接抄官网demo结果一些默认值对阴影是帮倒忙的。我一般会显式写清楚下面这些const viewer new Cesium.Viewer(cesiumContainer, { animation: true, timeline: true, baseLayerPicker: false, geocoder: false, homeButton: false, sceneModePicker: false, navigationHelpButton: false, infoBox: false, selectionIndicator: false, // 关闭默认影像换成自己的底图 imageryProvider: false, // 地形这里先不开后面单独加避免加载顺序乱 terrainProvider: undefined }); const scene viewer.scene; const globe scene.globe;为什么要把animation和timeline打开因为太阳角度靠时间驱动你需要一个可视化的时间轴来拖动太阳调试阶段非常方便。上线时可以再决定是否隐藏。接着是光照相关的初始化// 开启地形光照让地表明暗随太阳变化 globe.enableLighting true; // 大气随光照动态变化 globe.dynamicAtmosphereLighting true; globe.dynamicAtmosphereLightingFromSun true; // 阴影总开关 scene.shadowMap.enabled true; scene.shadowMap.softShadows true; scene.shadowMap.darkness 0.3; scene.shadowMap.maximumDistance 5000; // 高动态范围光照层次更丰富 scene.highDynamicRange true; scene.highDynamicRangeSupported true;highDynamicRange这个参数值得单独说。开了以后亮部不过曝、暗部不死黑整体观感会好一截代价是有一定性能开销移动端要谨慎。3.2 时间系统与太阳位置控制太阳角度由viewer.clock.currentTime决定。想固定一个漂亮的日照角度直接设时间最省事// 固定到某天某个时刻太阳高度角比较舒服 viewer.clock.currentTime Cesium.JulianDate.fromIso8601(2024-06-21T02:00:00Z); // 停止时间推进画面静止 viewer.clock.shouldAnimate false;UTC时间和北京时间差8小时比如想让太阳在北京时间上午10点、高度角约50度那就写02:00:00Z。这个换算很容易搞错我第一次调的时候把时间设成下午两点结果太阳低得快掉地平线了还以为光照没生效。如果要动态光照就打开动画配合时间倍率viewer.clock.shouldAnimate true; viewer.clock.multiplier 1800; // 每秒推进半小时太阳快速走一遍倍率别调太大太快容易在阴影重计算的时候掉帧。做演示用600到3600之间的值比较稳。3.3 地形光照与底图配合地形是阴影最终要落的地方如果地形没开光照阴影会显得飘在空中。加载地形const terrain await Cesium.createWorldTerrainAsync({ requestWaterMask: true, requestVertexNormals: true // 关键请求法线光照更准 }); viewer.terrainProvider terrain;requestVertexNormals一定要开不开的话地表光照几乎是平的。这个参数会让地形服务返回带法线的数据光照计算才有正确结果。底图方面如果用的是影像瓦片注意亮度不要和光照冲突。影像太亮、光照太暗会显得发灰。我一般会把影像亮度稍微压一点让光照的明暗对比更突出const baseLayer viewer.imageryLayers.get(0); baseLayer.brightness 0.9; baseLayer.contrast 1.1;到这里一个能响应光照和阴影的基础场景就搭好了。下一步是往里塞倾斜摄影。4. 倾斜摄影加载与阴影开启实战4.1 3D Tiles加载的关键参数加载倾斜摄影一般用Cesium3DTileset。这里有几个参数对光照和阴影影响很大const tileset await Cesium.Cesium3DTileset.fromUrl(/data/tileset.json, { // 阴影模式 shadows: Cesium.ShadowMode.ENABLED, // 允许模型投射和接收阴影 maximumScreenSpaceError: 16, // 打开光照响应 lightColor: new Cesium.Cartesian3(1.0, 1.0, 1.0), // 让模型参与光照计算 enableLighting: true }); viewer.scene.primitives.add(tileset);shadows这个属性是核心它有几个枚举值枚举值含义适用场景DISABLED不参与阴影背景附属模型ENABLED投射并接收阴影主角模型但性能开销最大CAST_ONLY只投射阴影建筑群性能友好RECEIVE_ONLY只接收阴影地面、平台倾斜摄影里建筑通常是投射者所以CAST_ONLY是性价比最高的选择只有确实需要模型自身也接收阴影比如楼与楼之间的遮挡落在另一栋楼上才用ENABLED。我做过对比一个中等城市范围的倾斜摄影全开ENABLED帧率大概掉30%到40%换成CAST_ONLY能保住八成帧率视觉上差别没有想象中大。4.2 模型位置与高度修正倾斜摄影数据经常出现位置偏移或悬空/埋地的问题这会直接影响阴影。模型悬空阴影会飘模型埋地阴影被裁。修正方式// 获取模型包围盒用于计算贴地偏移 const boundingSphere tileset.boundingSphere; const cartographic Cesium.Cartographic.fromCartesian(boundingSphere.center); const surface Cesium.Cartesian3.fromRadians( cartographic.longitude, cartographic.latitude, 0.0 ); const offset Cesium.Cartesian3.fromRadians( cartographic.longitude, cartographic.latitude, cartographic.height ); const translation Cesium.Cartesian3.subtract( offset, surface, new Cesium.Cartesian3() ); tileset.modelMatrix Cesium.Matrix4.fromTranslation(translation);这段代码的作用是把模型从当前高度平移到地表。注意如果模型本身有正确的基准高度这段就别用了会把它拉下来。判断方法加载后放大看楼脚如果楼脚悬空或者沉进地里就用它。4.3 完整可运行的代码示例把上面几块拼起来一个完整的HTML示例大致是这样!DOCTYPE html html langzh-CN head meta charsetutf-8 titleCesium光照与倾斜摄影阴影/title script src/libs/Cesium/Cesium.js/script link relstylesheet href/libs/Cesium/Widgets/widgets.css style html, body, #cesiumContainer { width: 100%; height: 100%; margin: 0; padding: 0; overflow: hidden; } /style /head body div idcesiumContainer/div script const viewer new Cesium.Viewer(cesiumContainer, { animation: true, timeline: true, baseLayerPicker: false, geocoder: false, homeButton: false, sceneModePicker: false, navigationHelpButton: false, infoBox: false, selectionIndicator: false }); const scene viewer.scene; const globe scene.globe; // 光照配置 globe.enableLighting true; globe.dynamicAtmosphereLighting true; globe.dynamicAtmosphereLightingFromSun true; // 阴影配置 scene.shadowMap.enabled true; scene.shadowMap.softShadows true; scene.shadowMap.darkness 0.3; scene.shadowMap.maximumDistance 5000; // 时间固定太阳角度稳定 viewer.clock.currentTime Cesium.JulianDate.fromIso8601(2024-06-21T02:00:00Z); viewer.clock.shouldAnimate false; // 加载倾斜摄影 async function loadTileset() { const tileset await Cesium.Cesium3DTileset.fromUrl(/data/tileset.json, { shadows: Cesium.ShadowMode.CAST_ONLY, maximumScreenSpaceError: 16 }); viewer.scene.primitives.add(tileset); viewer.zoomTo(tileset); } loadTileset().catch((e) console.error(3DTiles加载失败, e)); /script /body /html这段代码最短可以跑到效果。想更精细就接着看下一节的参数调优。5. 光照与阴影参数调优的实战经验5.1 时间、太阳角度与阴影长度的对照关系阴影的长度和方向完全由太阳高度角决定这是物理规律调参数只能顺应它。我整理了一张常见时间段的对照表方便你做场景设定时快速选时间北京时间太阳高度角约阴影表现适合场景08:0025°~35°阴影很长偏西清晨氛围、强调体量10:0045°~55°阴影适中方向清晰常规展示、规划审查12:0070°~80°阴影极短接近正下方正射感、日照分析15:0050°~60°阴影偏东长度适中下午光照、斜阳效果17:3020°~30°阴影拉长偏东黄昏氛围、长影效果做建筑日照分析的项目通常用正午前后因为此时遮挡关系最直观。做城市漫游演示则偏爱上午10点或下午3点阴影有方向感但不夸张。需要注意的是斜影太长时阴影贴图的maximumDistance如果设得不够远处建筑的阴影会被截断看起来像影子没了。解决办法是适当加大距离或者接受近景阴影、远景关闭的折中。5.2 阴影质量与性能的取舍方案阴影参数没有万能值得看你的目标设备和场景规模。我总结了几个不同档位档位softShadowssizemaximumDistance适用高画质true40968000桌面端演示均衡true20485000桌面端常规项目流畅false20483000笔记本、集成显卡移动端false10242000手机、平板softShadows是最影响画质也最影响性能的开关。关掉以后阴影边缘有明显锯齿但帧率能提升一大截。做移动端项目时我基本默认关掉靠适当提高darkness和缩小maximumDistance来弥补观感。size这里有个坑不是所有浏览器和显卡都支持4096超过设备上限可能直接黑屏或渲染异常。上线前一定要在目标设备上验证。5.3 阴影太黑、太糊、边缘抖动怎么调这三个问题我遇到最多分别说阴影太黑第一反应是把darkness调小。但更根本的原因往往是环境光不足——现实里阴影里也有漫反射光纯黑是不自然的。可以配合scene.light或者调整模型的lightColor让阴影区域保留一点亮度。我一般darkness控制在0.2到0.35之间。阴影太糊通常是size偏低或者maximumDistance过大。大范围加低分辨率等于用一张小图盖一大片肯定糊。要么提高size要么缩小阴影覆盖范围聚焦到重点区域。边缘抖动学术叫阴影粉刺是深度比较精度不够导致的可以通过scene.shadowMap.normalOffset较新版本有类似参数或者适当偏移模型位置缓解。如果抖动只在特定角度出现多半是深度偏移问题把softShadows打开一般能掩盖掉。5.4 倾斜摄影全场景适配的注意事项结合实际项目我总结几个倾斜摄影开阴影时的注意点数据量大时阴影计算会拖慢瓦片加载。建议把maximumScreenSpaceError适当调大比如从16调到20减少高精度瓦片数量。如果倾斜摄影有多个区域不要全部ENABLED按重要性分配主角区域ENABLED边缘CAST_ONLY甚至DISABLED。模型有透明或半透明纹理时阴影可能出现异常透明部分也投影这种情况需要用材质处理或者干脆关掉该区域的阴影投射。阴影和地形贴合要检查globe.depthTestAgainstTerrain true否则可能出现阴影穿过地面的现象。6. 常见问题排查与避坑记录6.1 阴影完全不显示的排查顺序遇到阴影不出来别急着改参数按这个顺序排查最快scene.shadowMap.enabled是否真的为true。很多人写在Viewer初始化之后但被异步覆盖了。模型shadows属性是否为DISABLED。加载时如果没显式设置某些版本默认是不投影的。太阳角度是否太低。太阳接近地平线时阴影几乎拉平看起来像没有。maximumDistance是否太小导致阴影范围外。是否开了highDynamicRange但设备不支持导致整体渲染异常。我遇到过一次排查两小时最后发现是模型shadows属性写在异步回调外面被覆盖了。所以异步加载的代码属性设置一定要放在await之后。6.2 倾斜摄影发黑、发灰的处理模型加载后整体发暗通常有三种原因一是材质是unlit但环境光没给够二是法线朝内三是纹理本身就是暗的。第一种最常见可以通过tileset.style调整颜色tileset.style new Cesium.Cesium3DTileStyle({ color: { conditions: [ [true, color(#ffffff, 1.0)] ] } });不过要注意style里的color是叠加式的不是替换。如果只是整体偏暗也可以调viewer.scene.light.intensity部分版本支持或者统一提升影像图层亮度。法线问题相对难处理需要在数据转换阶段修正Cesium端没法直接改。这也是为什么我强调倾斜摄影的数据处理流程和渲染流程是绑在一起的做渲染的人最好也了解一点转换时的法线导出设置。6.3 性能崩溃的典型场景Cesium场景崩溃通常不是单一原因而是多个资源叠加。我见过一次典型倾斜摄影全开ENABLED 4096阴影贴图 软阴影 高精度地形结果飞行到城市中心直接白屏。日志里没有明显报错就是GPU显存爆了。所以有资源预算的概念很重要。一个中等复杂度的城市场景我建议的配置是倾斜摄影CAST_ONLY、size2048、softShadowstrue、maximumDistance5000、maximumScreenSpaceError20。在这个基础上如果帧率还有余量再往上升。反过来先堆最高配置再往下砍往往要返工。另外Cesium在物体远离相机时会自动做瓦片卸载阴影计算也应该跟着卸载。如果发现远离相机后帧率还是低检查是不是有tileset被强制常驻了。7. 一些延伸玩法与面试高频问题7.1 动态光照与时间轴联动把时间做成可交互的时间轴用户拖动就能看不同时刻的阴影变化这在规划类项目里非常实用。实现上一个简单的时间轴拖动条联动viewer.clock.currentTime即可。注意每次时间变化会触发阴影重计算拖动时要做节流比如每100毫秒更新一次而不是每帧更新。如果想做日照分析可以每隔固定时间采一次阴影图统计某点被遮挡的时长。这个属于进阶功能需要结合自定义着色器这里不展开但思路是把阴影深度图作为输入在片元里判断遮挡。7.2 和其他图层的组合光照和阴影只是基础渲染层实际项目里常和这些叠加热力图如人流、温度、雷达扫描效果扩散圆环、模型节点高亮如点击某栋楼等。组合的时候要注意渲染顺序和透明度光照图层一般最先特效图层在后。如果特效用了后处理PostProcessStage要确认它不会把阴影计算结果覆盖掉。倾斜摄影和热力图叠加时热力图通常是贴在地表的阴影投在上面会自然一些如果热力图是悬浮的阴影关系就会怪。这个是视觉经验没有硬性规则多试几次就有感觉了。7.3 面试里常问的几个相关问题搜索热词里出现了Cesium面试题我顺手整理几个和光照阴影相关的高频问题答案我按自己的理解给问Cesium的阴影是怎么实现的答基于ShadowMap从光源视角渲染深度图渲染主画面时比对深度判断遮挡方向光的阴影用正交投影覆盖有限范围。问为什么倾斜摄影开了阴影没有效果答常见原因有三个模型shadows属性未开启、材质是unlit不响应光照、法线数据缺失或错误。问阴影性能怎么优化答优先降低阴影贴图分辨率和覆盖距离关闭软阴影用CAST_ONLY替代ENABLED减少高精度瓦片数量。问maximumScreenSpaceError和阴影有关系吗答间接有它控制瓦片精细度值越小瓦片越精细、数量越多阴影计算负担也越重。这些问题能答上说明你对渲染链路是有理解的而不是只会调API。8. 我踩过的几个坑和最后的小技巧最后说几个纯粹是经验层面的东西文档里一般不写。第一个坑是时间设置的时区问题。我前面提过UTC和北京时间的换算这个真的会让人抓狂。建议在调试阶段做一个时间显示的小面板把当前时间和太阳高度角打出来一眼就能看出对不对。第二个坑是模型和高程不匹配。倾斜摄影如果和高程地形叠加模型可能出现穿模或悬空阴影随之错乱。稳妥的做法是先单独加载模型确认位置再叠加地形微调。第三个小技巧是用scene.debugShowShadowMap部分版本支持来可视化阴影贴图调参的时候非常有用能看到阴影覆盖范围和分辨率到底够不够。这个开关在官方文档里藏得比较深但排查问题效率极高。第四个小技巧是分区域管理tileset。一个大范围的倾斜摄影不要一口气全加进来按行政区块或网格拆成多个tileset每个单独控制阴影属性性能和可控性都会好很多。关于光照和阴影说到底是一个物理正确和性能现实之间的平衡活。没有一套参数打遍所有项目但有清晰的排查顺序和资源预算意识绝大多数问题都能自己解决。这套东西我用了挺久从桌面演示到移动端适配都跑过整体是稳的。你如果正在做类似的项目建议先把基础光照链路跑通再一点点加阴影参数别一上来就堆最高配置那样很容易卡在性能和画面的两难里出不来。
返回列表