尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
Cesium全球体积云渲染实战:光线步进与性能优化
简介这份资源面向在Cesium中实现全球体积云效果的WebGL开发者与图形学爱好者针对原生积云仅适合小范围离散云朵、难以生成全球连续云层的痛点给出了一套基于体渲染的可行方案。压缩包共4个文件约9KB以2个JavaScript脚本为核心分别承担云层体数据生成与体积云渲染逻辑另含1个HTML页面用于效果演示与调试以及1个inscode工程配置文件便于快速运行与二次修改。方案将perlin噪声与worley噪声组合并引入分形布朗运动增强云层细节体数据由CPU生成改为GPU渲染到纹理加速配合光线步进与射线球体求交优化性能且不依赖外部噪声纹理实现步骤简洁。目前已有444人学习下载适合仿真要求不高、希望快速上手体积云渲染的读者参考可从中获取噪声组合、三维纹理生成与光线步进渲染的完整代码思路。1. 全球体积云在 Cesium 里到底难在哪第一次在 Cesium 里做全球体积云很多人会下意识觉得“不就是贴一层带透明度的云图吗”。真上手才发现平面云图和体积云完全是两回事平面云图是二维贴图视角一压低就穿帮体积云要在三维空间里做光线步进全球尺度下还要处理球面坐标、大气散射、昼夜过渡和相机远近切换。标题里的“全球体积云方案”说的就是一套能在 Cesium 地球上跑、覆盖全球、随视角和光照实时变化的云层渲染实现通常以自定义 Primitive 或后处理阶段的形式挂进场景。它解决的问题很具体让地球表面上方那层云有厚度、有自阴影、有边缘透光而不是一张贴图。适合谁适合已经能用 Cesium 加载地形影像、写过自定义着色器、想给地球加一层可信大气氛围的开发者。如果你连 Cesium 的 Scene、Camera、Primitive 还没摸熟建议先把官方沙盒里的自定义几何体跑通再回来。这一章先把概念和边界立住后面几章直接上代码和参数。2. 体积云的核心原理与 Cesium 集成选型2.1 光线步进为什么是体积云的地基体积云的本质是在视线方向上对云层密度场做积分。相机发出的一条射线穿过云层区间我们把它切成若干小段每段采样一次密度累加透射率和散射贡献。这个“切段采样”就是光线步进Ray Marching。密度场通常用三维噪声生成最常用的是 Perlin-Worley 组合Worley 噪声负责云团的絮状边缘Perlin 噪声负责大尺度起伏。采样点密度由噪声值、高度衰减、覆盖度共同决定。在 Cesium 里做这件事关键是把射线从屏幕空间转到地球局部空间。Cesium 的相机在世界坐标系ECEF里地球是个椭球所以射线和云层的求交不能简单用平面。常见做法是在片元着色器里拿到世界坐标和相机位置构造射线与云层内外两个球面求交得到步进区间再在区间内循环采样。这个循环次数直接决定画质和帧率是后面调参的主战场。2.2 用 CustomPrimitive 还是 PostProcess选型理由Cesium 提供两条路一是自定义 Primitive把云当成一个包住地球的几何体渲染二是 PostProcessStage在全屏后处理里做。两者差别很大。自定义 Primitive 的优点是深度关系正确云能被地形和建筑遮挡适合做“云在地球上方”的物理感。缺点是几何体要足够大且要处理背面剔除和相机进入云层的情况。PostProcessStage 的优点是实现简单直接拿深度缓冲重建世界坐标不用管几何体。缺点是它默认在全屏上做云和地形的遮挡要靠深度判断稍不注意云就糊在山前面。我一般会选自定义 Primitive 方案因为全球体积云最怕的就是“云飘在地形前面”这种穿帮。下面给一个最小可跑的骨架用 Cesium 的 CustomPrimitive 思路实际项目里可以封装成自己的 Primitive 类。// 云层外壳几何体一个比地球略大的球包住整个地球 const cloudRadius 6470000; // 地球平均半径约 6371km云层顶再高约 100km const cloudGeometry new Cesium.SphereGeometry({ radius: cloudRadius, vertexFormat: Cesium.VertexFormat.POSITION_ONLY }); // 自定义外观把着色器塞进去 const cloudAppearance new Cesium.MaterialAppearance({ material: new Cesium.Material({ fabric: { type: CloudVolume, uniforms: { uTime: 0.0, uCloudCoverage: 0.45, // 云量0~1越大云越多 uCloudDensity: 0.02, // 密度系数影响不透明度 uStepCount: 64, // 步进次数画质与性能的平衡点 uLightDir: new Cesium.Cartesian3(1.0, 0.5, 0.3) }, source: cloudShaderSource // 见下一节的 GLSL } }), translucent: true, closed: false }); const cloudPrimitive new Cesium.Primitive({ geometryInstances: new Cesium.GeometryInstance({ geometry: cloudGeometry }), appearance: cloudAppearance, asynchronous: false }); viewer.scene.primitives.add(cloudPrimitive);这段代码做了三件事造一个比地球大的球体作为云层容器用 Material 挂自定义着色器把 Primitive 加进场景。参数里uCloudCoverage控制云量uCloudDensity控制浓淡uStepCount是步进次数。注意translucent: true必须开否则云不透明。closed: false是为了让相机在球内时也能看到云避免整个球被剔除。2.3 着色器里的射线求交与步进循环着色器是这套方案的核心。下面这段 GLSL 是简化版重点看射线与球面求交、步进循环和密度采样三部分。// 云层内外半径单位米 const float INNER_RADIUS 6371000.0; const float OUTER_RADIUS 6470000.0; // 射线与球求交返回最近和最远距离 vec2 raySphereIntersect(vec3 rayOrigin, vec3 rayDir, float radius) { float b dot(rayOrigin, rayDir); float c dot(rayOrigin, rayOrigin) - radius * radius; float d b * b - c; if (d 0.0) return vec2(-1.0); d sqrt(d); return vec2(-b - d, -b d); } // 简化的密度采样用高度和噪声模拟云 float sampleDensity(vec3 pos, float time) { float height length(pos); // 高度衰减云集中在中间层 float h (height - INNER_RADIUS) / (OUTER_RADIUS - INNER_RADIUS); float heightFalloff smoothstep(0.0, 0.3, h) * smoothstep(1.0, 0.6, h); // 三维噪声实际项目用纹理或程序化噪声 float noise fract(sin(dot(pos.xyz time, vec3(12.9898, 78.233, 45.164))) * 43758.5453); float density max(0.0, noise - (1.0 - uCloudCoverage)) * heightFalloff * uCloudDensity; return density; } void main() { vec3 rayOrigin czm_viewerPositionWC; vec3 rayDir normalize(czm_windowToEyeCoordinates(gl_FragCoord.xy).xyz); // 转到世界方向 rayDir normalize((czm_inverseView * vec4(rayDir, 0.0)).xyz); vec2 inner raySphereIntersect(rayOrigin, rayDir, INNER_RADIUS); vec2 outer raySphereIntersect(rayOrigin, rayDir, OUTER_RADIUS); float tNear max(inner.x, outer.x); float tFar min(inner.y, outer.y); if (tNear tFar || tFar 0.0) discard; float stepSize (tFar - tNear) / float(uStepCount); float transmittance 1.0; vec3 scatteredLight vec3(0.0); for (int i 0; i 128; i) { if (i uStepCount) break; float t tNear (float(i) 0.5) * stepSize; vec3 pos rayOrigin rayDir * t; float density sampleDensity(pos, uTime); if (density 0.0) { // 简单的光照累积实际要加自阴影和相位函数 float lightAtten exp(-density * stepSize * 10.0); scatteredLight transmittance * density * stepSize * uLightDir * lightAtten; transmittance * lightAtten; if (transmittance 0.01) break; } } gl_FragColor vec4(scatteredLight, 1.0 - transmittance); }逻辑说明先算射线和内外球面的交点得到有效步进区间然后在区间内均匀步进每个采样点算密度累加散射光并衰减透射率。uStepCount控制循环次数代码里写死 128 上限是为了兼容部分 GPU 的循环限制实际用 uniform 控制。参数上INNER_RADIUS和OUTER_RADIUS决定云层厚度heightFalloff决定云集中在哪个高度uCloudCoverage决定云量。注意czm_viewerPositionWC和czm_inverseView是 Cesium 内置变量直接用即可。3. 把全球体积云跑起来从噪声到光照的完整链路3.1 三维噪声纹理的生成与采样上一节的噪声是凑数的真实云层必须用三维噪声纹理。常见做法是预生成一张 128x128x128 的 3D 纹理里面存 Perlin-Worley 组合噪声。生成可以在 CPU 端用脚本做也可以运行时用计算着色器但 Cesium 环境里最稳的是预生成后作为纹理传入。// 用 canvas 模拟生成一张 2D 噪声图实际项目应生成 3D 纹理 function generateNoiseTexture(size) { const canvas document.createElement(canvas); canvas.width size; canvas.height size; const ctx canvas.getContext(2d); const imageData ctx.createImageData(size, size); for (let y 0; y size; y) { for (let x 0; x size; x) { const i (y * size x) * 4; // 简化的值噪声实际用 Worley Perlin 组合 const v Math.random() * 255; imageData.data[i] v; imageData.data[i 1] v; imageData.data[i 2] v; imageData.data[i 3] 255; } } ctx.putImageData(imageData, 0, 0); return new Cesium.Material.ImageUniform({ image: canvas, repeat: new Cesium.Cartesian2(4.0, 4.0) }); }这段代码只是示意真实项目里我会用离线工具生成 3D 纹理再通过Cesium.Texture或自定义 uniform 传入。参数上纹理分辨率 128 够用256 更细腻但显存翻倍。采样时用texture(noiseTex, pos * scale)scale控制云团大小一般 0.0001 到 0.001 之间调。3.2 光照模型自阴影与相位函数体积云好看的关键在光照。最简单的做法是只算太阳方向的环境光云会显得平。要出效果得加自阴影从每个采样点再向光源方向步进几次看有多少密度挡住光。这就是所谓的“二次步进”性能开销大但云会有立体感。// 向光源方向做短距离步进估算阴影 float lightMarch(vec3 pos, vec3 lightDir, float stepSize) { float shadow 0.0; float t 0.0; for (int i 0; i 6; i) { vec3 p pos lightDir * t; shadow sampleDensity(p, uTime) * stepSize; t stepSize * 2.0; } return exp(-shadow * 3.0); }参数上二次步进次数一般 4 到 8 次再多帧率扛不住。相位函数用 Henyey-Greenstein 近似控制云边缘的银边效果g值取 0.6 到 0.8 之间越大银边越集中。3.3 昼夜过渡与大气散射的融合全球云不能只有白天。太阳方向变化时云的受光面要跟着变。Cesium 自带czm_sunDirectionWC直接拿来用。夜晚云不能全黑要加一点环境光否则地球暗面云就消失了。常见做法是混合一个极低的环境色再叠加城市灯光反射的暖色。vec3 sunDir czm_sunDirectionWC; float sunAmount max(dot(rayDir, sunDir), 0.0); vec3 dayColor vec3(1.0, 0.98, 0.95); vec3 nightColor vec3(0.05, 0.06, 0.1); vec3 ambient mix(nightColor, dayColor, smoothstep(-0.1, 0.2, sunDir.z));这段把太阳方向和视线方向点乘决定云的受光强度再用smoothstep做昼夜过渡。参数-0.1和0.2是过渡区间调大过渡更柔和。注意czm_sunDirectionWC是 Cesium 内置的不用自己算。4. 性能与画质的平衡参数怎么调不翻车4.1 步进次数、分辨率与帧率的三角关系体积云是典型的“拿 GPU 换画质”。步进次数从 32 提到 64画质提升明显64 到 128提升变小但帧率掉一半。我的经验是桌面端 64 次起步移动端 32 次配合时间抖动Temporal Dithering用低步数也能出可接受的效果。分辨率上不要全屏跑可以半分辨率渲染再上采样Cesium 的 PostProcessStage 支持textureScale设 0.5 能省一半像素。// 半分辨率后处理降低体积云像素开销 const cloudPostProcess new Cesium.PostProcessStage({ fragmentShader: cloudCompositeShader, textureScale: 0.5, // 0.5 表示半分辨率 uniforms: { uCloudTexture: () cloudRenderTarget, uStepCount: 48 } }); viewer.scene.postProcessStages.add(cloudPostProcess);textureScale是性价比最高的参数0.5 到 0.75 之间肉眼几乎看不出差别帧率能涨 30% 以上。uStepCount配合时间抖动每帧偏移采样起点用几帧累积出平滑效果。4.2 相机高度与云层厚度的联动全球体积云有个特殊问题相机从地面拉到太空云层在屏幕上的占比变化巨大。如果云层厚度固定近地时云糊满屏太空时云薄成一条线。常见做法是根据相机高度动态调整云层外半径和步进区间。相机低时云层外半径调小步进区间缩短相机高时外半径调大步进区间拉长。// 根据相机高度动态调整云层参数 function updateCloudParams(cameraHeight) { const t Cesium.Math.clamp(cameraHeight / 2000000.0, 0.0, 1.0); const outerRadius Cesium.Math.lerp(6400000, 6600000, t); const stepCount Math.floor(Cesium.Math.lerp(32, 96, t)); cloudAppearance.material.uniforms.uOuterRadius outerRadius; cloudAppearance.material.uniforms.uStepCount stepCount; }这段逻辑是相机越高云层外半径越大步进次数越多保证太空视角下云层有足够采样。lerp的区间根据实际地球半径调别照抄。注意每帧更新 uniform 有开销可以隔几帧更新一次。4.3 用时间抖动掩盖低步数伪影低步数最明显的伪影是层状条纹。时间抖动Temporal Jitter在每帧给采样起点加一个随机偏移多帧累积后条纹被平均掉。实现很简单在着色器里加一个基于帧号的随机数。// 基于帧号的时间抖动打散步进起点 float jitter fract(sin(float(uFrameNumber) * 12.9898) * 43758.5453); float t tNear (float(i) jitter) * stepSize;uFrameNumber每帧递增jitter在 0 到 1 之间。这样每帧采样位置错开人眼看到的是平均结果。注意抖动要配合 TAA 或简单的帧混合否则会有噪点。Cesium 没有内置 TAA可以自己用一个累积缓冲把上一帧和当前帧按 0.9 和 0.1 混合。5. 避坑与排查全球体积云最容易翻车的 5 个点5.1 云飘在地形前面遮挡关系全乱现象山体明明在云下面云却画在山前面或者云穿过建筑。原因自定义 Primitive 的深度测试没开或者云层几何体的深度写入被关了。解决确保appearance.translucent为 true 的同时深度测试保持开启closed设 false 让相机进云时也能渲染。如果还是不对检查云层几何体是否真的包住了地形半径别设太小。5.2 步进循环次数一高就黑屏或花屏现象把uStepCount调到 128 以上画面直接黑掉或出现彩色噪点。原因部分 GPU 对循环次数有硬限制或者着色器编译超时。解决把循环写成固定上限加 break别用动态循环条件步进次数用 uniform 控制但循环体里判断if (i uStepCount) break;。另外检查显卡驱动老驱动对长循环支持差。5.3 云层边缘出现明显锯齿或硬边现象云和天空交界处像刀切一样。原因密度场在边界处没有平滑衰减或者步进区间算错导致突然截断。解决在sampleDensity里加高度衰减的smoothstep让密度在云层顶底平滑归零射线求交时如果tNear和tFar太接近直接 discard 避免硬边。5.4 帧率骤降但 GPU 占用不高现象帧率掉到 20 但 GPU 使用率只有 50%。原因CPU 端每帧更新 uniform 或重建几何体造成 CPU 瓶颈。解决把 uniform 更新频率降下来几何体只建一次用 Cesium 的Primitive而不是每帧重建。另外检查是不是在preRender里做了重计算。5.5 昼夜过渡时云突然闪一下现象太阳方向变化时云的颜色跳变。原因smoothstep的过渡区间太窄或者太阳方向向量没归一化。解决把过渡区间拉宽比如smoothstep(-0.2, 0.3, sunDir.z)确保czm_sunDirectionWC是归一化的如果不是就手动 normalize。6. 进阶让全球体积云更可信的两个技巧第一个技巧是分层云。真实大气里云不止一层低层积云、高层卷云密度和高度都不同。可以在着色器里叠两套密度场一套低层厚实一套高层稀薄用不同的高度衰减和覆盖度。这样地球边缘的云会有层次感而不是一坨。实现上就是sampleDensity里调两次噪声按高度加权混合。参数上低层覆盖度 0.5高层 0.2高度区间错开。第二个技巧是用预计算的透射率查找表LUT替代实时二次步进。二次步进太贵可以预生成一张 2D LUT横轴是高度纵轴是太阳角度存透射率。运行时直接采样 LUT省掉内层循环。LUT 分辨率 256x64 就够生成一次可以一直用。这个做法在业内很常见代价是 LUT 生成要写个离线脚本但跑起来帧率能翻倍。验证方法很简单把相机拉到不同高度、不同经纬度看云层是否始终贴合地球曲率昼夜线扫过时云的颜色是否平滑过渡。如果某个角度云突然消失或变黑多半是射线求交的边界条件没处理好。我自己的习惯是每调一次参数就截三张图赤道白天、极地夜晚、高空俯视对比着看。这套方案值不值得做如果你要的是能随视角和光照变化的全球云层它比贴图方案贵但效果是另一个量级。先从 32 步进跑通再往上加别一上来就拉满。希望帮到你。本文还有配套的精品资源点击获取
RELATED

相关推荐

跌倒检测数据集全解析:格式转换、清洗与训练避坑指南

跌倒检测数据集全解析:格式转换、清洗与训练避坑指南

简介:跌倒检测数据集面向计算机视觉、模式识别与机器学习研究者,以及关注老年人跌倒防护的智能安防开发者,可用于训练和验证跌倒识别模型。压缩包内含2000个文件,主要由1440个XML标注文件与560张JPG图像构成,整体约65.…

📅 2026/10/18 15:23:43
Mistral AI 发布第二代开源编码模型:Devstral2 和 Devstral Small2 实测与 TaoToken 接入

Mistral AI 发布第二代开源编码模型:Devstral2 和 Devstral Small2 实测与 TaoToken 接入

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

📅 2026/10/18 15:23:43
Stata 17 模型代码版本兼容与回归结果复现指南

Stata 17 模型代码版本兼容与回归结果复现指南

简介:这份资源面向使用Stata进行计量经济分析的研究生、科研人员与数据分析从业者,围绕Stata17的版本选择与常用模型代码展开,帮助读者厘清MP、SE、BE三个版本在大型数据集与多核处理器场景下的差异,并快速上手DID、DDD、面板数据…

📅 2026/10/18 15:23:43
MORE NEWS

更多资讯

📰

Stata 17实证模型代码全流程:从数据导入到面板固定效应与表格输出

简介:这份资源面向使用Stata进行计量经济分析的研究生、科研人员与数据分析从业者,围绕Stata17的版本选型与常用模型代码展开。内容梳理了MP、SE、BE三个版本的特点与适用场景,其中MP为最高配版,适合大型数据集与多核处理器&#…

📰

Harness工程实践:从YAML声明式流水线到策略即代码的CI/CD工程化

简介:这份资源面向关注企业级DevOps落地与AI工程化的开发者、架构师及技术管理者,围绕Harness平台在真实业务场景中的渐进式落地路径展开,涵盖试点、扩展、深化、优化四阶段方法论,并引入OpenAI基于Codex与GPT-5实现零手写代码构建…

📰

R语言提取GEO数据:从GSE编号到表达矩阵的完整指南

简介:面向生物信息学研究者的R语言GEO数据提取源码包,聚焦基因芯片下载、表达矩阵与临床信息提取,并针对多GPL平台处理及CSV导出列名左移等常见问题给出了可运行解法。资源共7个文件,压缩包约12KB,包含两个R脚本&#…

📰

740,957行数据从哪来?jeV-distill-corpus-v3 三大数据流 yuri_v3、yuri_v1、openjev_v2 深度剖析

数据集模型蒸馏人工智能 【免费下载链接】jev-distill-corpus-v3 项目地址: https://ai.gitcode.com/hf_mirrors/SargeDev/jev-distill-corpus-v3 点击查看 免费下载 jeV-distill-corpus-v3 是一个包含 740,957 行校准型"类型化决策"语料库,采…

📰

论文学习AI助手:基于RAG与向量检索的智能阅读系统构建指南

“看不懂论文”这件事,本质上是信息密度太高,卡在大量陌生术语和专业背景上,光靠通读很难形成结构化的理解。更麻烦的是,读论文的过程是高度非线性的——你得反复跳转图表、公式、引用文献,脑子里同时维护好几个“未解…

📰

3 步把微信聊天记录导出成本地文件:WeChatMsg 免费实操指南

3 步把微信聊天记录导出成本地文件:WeChatMsg 免费实操指南 【免费下载链接】WeChatMsg 提取微信聊天记录,将其导出成HTML、Word、CSV文档永久保存,对聊天记录进行分析生成年度聊天报告 项目地址: https://gitcode.com/GitHub_Trending/we/…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬