ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

基于planetary.js与Three.js的动态地球可视化实战

基于planetary.js与Three.js的动态地球可视化实战

1. 从一张静态地球图说起:为什么需要动态地球

很多人第一次接触三维地球可视化,都是从一张静态的卫星影像贴图开始的。把一张世界地图贴到球体上,转一转,看着挺像那么回事。但真正做过项目的人都知道,静态贴图的问题非常明显:它没有昼夜变化,没有云层流动,没有大气辉光,更没有那种"活着"的感觉。用户看两眼就腻了,因为它本质上还是一张图片,只不过被卷成了一个球。

planetary.js这个库解决的正是这个问题。它基于 WebGL 技术栈,通过着色器程序在球体表面实时渲染出动态效果,包括但不限于昼夜交替的明暗过渡、云层的缓慢漂移、大气层的边缘散射光晕,以及星空背景的视差效果。你不需要自己写一行 GLSL 着色器代码,只需要准备好几张纹理贴图,调用几个配置参数,就能在浏览器里跑出一个看起来相当唬人的动态地球。

这个库适合谁用?我总结下来大概是三类人:第一类是做数据可视化大屏的前端开发者,需要一个有视觉冲击力的地球作为背景或交互载体;第二类是对图形学感兴趣但不想从零造轮子的学习者,想通过一个现成的库来理解动态地球的渲染逻辑;第三类是做科普展示、教学演示或者个人作品集的人,需要一个开箱即用的三维地球组件。

我最初接触这个库是因为一个数据展示项目,需要在页面上放一个能旋转的地球,并且要能根据数据在地球上打点标记。当时试过好几个方案,有的太重,有的效果太假,最后选了planetary.js,原因后面会详细说。这篇文章我会把这个库从环境搭建到实际跑通的完整过程拆开讲,包括我踩过的坑和总结出来的参数调优经验。

2. 环境搭建与依赖梳理:别急着写代码

2.1 这个库到底依赖什么

planetary.js本身是一个 JavaScript 库,但它的渲染能力来自底层依赖。核心依赖是Three.js,它负责场景管理、相机控制、几何体创建和渲染循环。planetary.js在 Three.js 的基础上封装了地球特有的逻辑,比如纹理加载、着色器材质配置、光照模型等。

除了 Three.js,你还需要注意以下几点:

  • 纹理贴图资源:动态地球的效果好坏,八成取决于贴图质量。你需要准备至少三张图:地球表面颜色贴图(day map)、夜间灯光贴图(night map)、云层贴图(cloud map)。如果还想要法线效果,需要额外的法线贴图。
  • 浏览器 WebGL 支持:这个不用多说,现代浏览器基本都支持,但如果你要兼容一些老旧环境,需要提前检测。
  • 构建工具:如果你用 npm 管理依赖,直接安装即可;如果是传统 script 标签引入,需要确保 Three.js 和 planetary.js 的版本匹配。

我建议在动手之前先确认一下你的 Three.js 版本。planetary.js在不同版本的 Three.js 下表现有差异,尤其是着色器相关的 API 在 r120 之后有过一些调整。我自己用的是 r128 左右的版本,跑下来比较稳定。

2.2 安装方式的选择与理由

安装方式有两种,我分别说一下适用场景。

第一种是 npm 安装:

npm install planetary.js three

这种方式适合现代前端项目,配合 webpack、vite 等构建工具使用。好处是版本管理清晰,依赖关系明确。但要注意,planetary.js的 npm 包可能不是最新版本,你需要去确认一下。

第二种是直接引入脚本文件:

<script src="path/to/three.min.js"></script> <script src="path/to/planetary.js"></script>

这种方式适合快速原型验证或者简单的静态页面。好处是不需要构建工具,打开浏览器就能跑。坏处是版本管理靠手动,容易出问题。

我个人的建议是:如果你只是想做个小 demo 看看效果,用第二种方式,五分钟就能跑起来;如果你要集成到正式项目里,用第一种方式,别偷懒。

2.3 纹理资源的准备与处理

纹理贴图是动态地球的灵魂。我见过很多人代码写得没问题,但地球看起来就是不对劲,问题基本都出在贴图上。

首先说尺寸。地球表面贴图的常见分辨率有 2048×1024、4096×2048、8192×4096 等。分辨率越高越清晰,但显存占用也越大。我的经验是,桌面端用 4096×2048 就够了,移动端降到 2048×1024。再高的话,除非你的用户都是高配设备,否则没必要。

其次是贴图的投影方式。地球贴图通常使用等距圆柱投影,也就是把球面展开成一个矩形。你拿到贴图之后,需要确认它的经纬度对齐是否正确。有些贴图是偏移过的,直接贴上去会发现大陆位置不对。

还有一个容易被忽略的点:贴图的色彩空间。Three.js 在 r152 之后对色彩空间的管理更严格了,如果你用的贴图没有正确设置色彩空间,渲染出来的颜色会偏暗或者偏灰。解决办法是在加载纹理时设置texture.colorSpace = THREE.SRGBColorSpace,具体写法取决于你的 Three.js 版本。

提示:贴图资源尽量选择无版权争议的公开素材,或者自己用图像工具生成。不要随便从网上扒图用在正式项目里。

3. 核心渲染机制拆解:动态效果是怎么来的

3.1 昼夜交替的实现逻辑

昼夜交替是动态地球最基础也最核心的效果。它的原理其实不复杂:在着色器里,根据球面每个点与光源方向的夹角,决定该点显示白天贴图还是夜晚贴图,夹角过渡区域做混合。

具体来说,planetary.js内部会创建一个自定义着色器材质。这个着色器接收两个纹理:白天贴图和夜晚贴图。然后在片元着色器中,计算当前片元的法线方向与光源方向的点积。点积大于某个阈值时,显示白天贴图;小于某个阈值时,显示夜晚贴图;中间区域做平滑过渡。

这个阈值和过渡宽度是可以调的。过渡太窄,昼夜分界线会很生硬,像一刀切;过渡太宽,又会导致白天和夜晚的区分不明显。我实测下来,过渡宽度设置在 0.1 到 0.2 之间比较自然。

还有一个细节:夜晚贴图通常是灯光分布图,显示的是城市灯光。如果你直接把夜晚贴图贴上去,灯光会显得很平。好的做法是在着色器里对夜晚贴图做一点增强处理,让灯光更亮、更有层次感。

3.2 云层漂移的渲染技巧

云层效果是让地球"活"起来的关键。它的实现方式是在地球表面之上再叠加一层半透明的球体,贴上云层纹理,然后让这个球体缓慢旋转。

这里有几个技术点需要注意:

  • 云层球体的半径:要比地球本体略大一点,通常大 1% 到 2%。太大了会显得云层飘在地球外面,太小了会穿插到地球表面里。
  • 云层的透明度:不能太高,否则会遮挡地球表面的细节;也不能太低,否则看不出云层的存在。我一般设置在 0.4 到 0.6 之间。
  • 旋转速度:云层的旋转速度要比地球本体略快或略慢,这样才能产生相对运动的效果。如果两者速度一样,看起来就像云层长在地球上一样,很假。
  • 云层的阴影:如果追求更真实的效果,可以让云层在地球表面投射阴影。但这需要额外的阴影计算,性能开销会增大。对于大多数场景来说,不做阴影也够看了。

3.3 大气辉光与边缘散射

大气辉光是让地球看起来有"厚度"的关键。它的原理是在地球边缘添加一层渐变的光晕,模拟光线穿过大气层时的散射效果。

planetary.js实现大气辉光的方式通常有两种:一种是用一个略大的球体,配合菲涅尔效应的着色器,让边缘部分发光;另一种是在后处理阶段添加泛光效果。

菲涅尔效应的核心思想是:当视线与表面法线的夹角越大(也就是越接近边缘),反射越强。在着色器里,用dot(viewDirection, normal)来计算这个夹角,然后根据结果调整透明度或亮度。

我试过几种参数组合,总结下来:大气辉光的颜色偏蓝一点比较自然,强度不要太高,否则会显得像霓虹灯。边缘的过渡要柔和,不能有明显的硬边。

3.4 星空背景的视差处理

星空背景看起来简单,但要做好也不容易。最简单的做法是放一张静态星空图作为场景背景。但这样没有视差效果,转动地球的时候星空不动,缺乏沉浸感。

好一点的做法是用粒子系统生成星空。在场景中随机分布大量点精灵,每个点代表一颗星星。当相机移动时,星星的位置会产生视差,看起来更有空间感。

planetary.js默认可能不包含星空背景,你需要自己添加。我的做法是创建一个大的球体,把星空纹理贴在内表面,然后让这个球体跟随相机旋转。这样无论相机怎么转,星空始终在背景中,而且有一定的视差效果。

4. 从零跑通一个动态地球:完整实操步骤

4.1 初始化场景与相机

第一步是创建 Three.js 的基础场景。这部分代码比较模板化,但我还是把关键参数说一下。

const scene = new THREE.Scene(); const camera = new THREE.PerspectiveCamera( 45, // 视场角 window.innerWidth / window.innerHeight, // 宽高比 0.1, // 近裁剪面 1000 // 远裁剪面 ); camera.position.set(0, 0, 300);

视场角选 45 度是比较通用的选择。太小了会有透视压缩感,太大了会有鱼眼效果。相机位置放在 z 轴 300 的位置,这个距离需要根据地球的半径来调整。如果地球半径是 100,那 300 的距离大概是地球直径的 1.5 倍,看起来比较合适。

渲染器方面:

const renderer = new THREE.WebGLRenderer({ antialias: true }); renderer.setSize(window.innerWidth, window.innerHeight); renderer.setPixelRatio(window.devicePixelRatio); document.body.appendChild(renderer.domElement);

antialias开启抗锯齿,setPixelRatio适配高分辨率屏幕。这两个参数对视觉质量影响很大,建议都加上。

4.2 加载纹理与创建地球本体

纹理加载是异步的,需要等所有贴图都加载完成后再创建地球。我用THREE.TextureLoader来加载:

const loader = new THREE.TextureLoader(); const dayTexture = loader.load('textures/earth_day.jpg'); const nightTexture = loader.load('textures/earth_night.jpg'); const cloudTexture = loader.load('textures/earth_clouds.png');

加载完成后,创建地球球体:

const earthGeometry = new THREE.SphereGeometry(100, 64, 64); const earthMaterial = new THREE.MeshPhongMaterial({ map: dayTexture, // 其他参数... }); const earth = new THREE.Mesh(earthGeometry, earthMaterial); scene.add(earth);

球体的分段数我用了 64×64,这个精度在大多数场景下够用了。如果你要在地球表面做很精细的交互,可以提高到 128×128,但性能开销会明显增加。

4.3 配置光照与着色器参数

光照方面,需要一个方向光来模拟太阳:

const sunLight = new THREE.DirectionalLight(0xffffff, 1.2); sunLight.position.set(500, 300, 500); scene.add(sunLight);

方向光的位置决定了昼夜分界线的方向。你可以通过调整这个位置来改变"太阳"的照射角度。

环境光也需要加一点,否则夜晚面会完全黑掉:

const ambientLight = new THREE.AmbientLight(0x333333); scene.add(ambientLight);

环境光的强度不要太高,否则会冲淡昼夜对比效果。

4.4 添加云层与大气层

云层的创建和地球类似,但半径略大,材质需要设置透明:

const cloudGeometry = new THREE.SphereGeometry(101, 64, 64); const cloudMaterial = new THREE.MeshPhongMaterial({ map: cloudTexture, transparent: true, opacity: 0.5, depthWrite: false }); const clouds = new THREE.Mesh(cloudGeometry, cloudMaterial); scene.add(clouds);

注意depthWrite: false这个参数。如果不设置,云层可能会在地球表面产生奇怪的深度冲突。

大气层用一个略大的球体,配合自定义着色器实现菲涅尔效果。这部分代码稍微复杂一些,但核心就是根据视线角度调整透明度。

4.5 启动渲染循环与交互控制

最后是渲染循环:

function animate() { requestAnimationFrame(animate); earth.rotation.y += 0.001; clouds.rotation.y += 0.0015; renderer.render(scene, camera); } animate();

地球和云层的旋转速度不同,这样才有相对运动的效果。

交互控制方面,可以用OrbitControls来实现鼠标拖拽旋转和滚轮缩放:

const controls = new THREE.OrbitControls(camera, renderer.domElement); controls.enableDamping = true; controls.dampingFactor = 0.05; controls.minDistance = 150; controls.maxDistance = 600;

enableDamping开启惯性效果,让拖拽更顺滑。minDistance和maxDistance限制缩放范围,防止用户把相机拉得太近或太远。

5. 参数调优与视觉打磨:让地球看起来更真

5.1 昼夜过渡的平滑度调整

昼夜过渡的平滑度直接影响到地球的真实感。我在前面提到过渡宽度设置在 0.1 到 0.2 之间,但具体值需要根据你的光照方向和贴图来微调。

如果你发现昼夜分界线太生硬,可以增大过渡宽度;如果发现白天和夜晚的区分不明显,可以减小过渡宽度。另外,过渡区域的混合曲线也很重要。线性混合看起来会比较平淡,用 smoothstep 函数做平滑插值效果会更好。

5.2 云层透明度与旋转速度的平衡

云层的透明度和旋转速度需要配合调整。透明度太高,云层会遮挡地球表面的细节;透明度太低,又看不出云层的存在。

我的经验是:如果云层贴图本身比较浓密,透明度可以设低一点,0.3 到 0.4;如果云层贴图比较稀疏,透明度可以设高一点,0.5 到 0.6。

旋转速度方面,云层和地球的速度差不要太大。差太多会显得云层在"飞",差太少又看不出相对运动。我一般让云层的速度比地球快 30% 到 50%。

5.3 大气辉光的颜色与强度

大气辉光的颜色我试过几种方案:纯蓝色看起来比较科幻,偏白的蓝色看起来更自然,带一点紫色的蓝色看起来更有艺术感。具体选哪种,取决于你的项目风格。

强度方面,建议从低到高慢慢调。一开始可以设得很低,然后逐渐增加,直到刚好能看出边缘有光晕为止。过强的大气辉光会让地球看起来像一颗发光的灯泡。

5.4 性能优化:什么时候该降精度

动态地球的渲染涉及大量的着色器计算和纹理采样,性能开销不小。如果你在低端设备上跑,或者页面上还有其他复杂内容,就需要做性能优化。

优化的手段包括:

  • 降低球体分段数,从 64×64 降到 32×32
  • 降低纹理分辨率,从 4096 降到 2048
  • 关闭抗锯齿
  • 减少云层和大气层的渲染精度
  • 在页面不可见时暂停渲染循环

我一般会先在全精度下跑一遍,看看帧率如何。如果稳定在 60fps,就不用优化;如果掉到 30fps 以下,就需要逐项排查了。

6. 踩坑记录与排查思路

6.1 地球全黑或全亮的问题定位

这是最常见的问题。地球要么全黑,要么全亮,看不到昼夜交替。原因通常有以下几种:

第一种可能是光照方向不对。方向光的位置如果和相机位置重合,或者方向光没有正确添加到场景中,就会导致光照异常。排查方法是把方向光的位置调到一个明显不同的方向,看看效果有没有变化。

第二种可能是着色器中的法线计算有误。如果法线方向反了,光照计算就会完全错误。这种情况需要检查着色器代码中的法线变换矩阵。

第三种可能是纹理没有正确加载。如果白天贴图加载失败,地球就会显示为默认颜色。排查方法是打开浏览器的开发者工具,看网络请求有没有报错。

6.2 纹理接缝与极点畸变的处理

地球贴图在经度 180 度附近会有一条接缝,这是等距圆柱投影的固有问题。如果贴图本身没有做好无缝处理,接缝处会有一条明显的线。

解决办法有两个:一是选择本身无缝的贴图;二是在着色器中对经度接近 180 度的区域做混合处理,让接缝不那么明显。

极点畸变是另一个问题。在球体的南北极附近,纹理会被严重拉伸,看起来很不自然。这个问题很难完全消除,但可以通过在极点区域做模糊处理来缓解。

6.3 移动端适配的常见异常

移动端的问题主要集中在性能和触摸交互上。

性能方面,移动端的 GPU 能力有限,全精度的动态地球可能会卡顿。解决办法是降低分段数和纹理分辨率,关闭一些非必要的效果。

触摸交互方面,OrbitControls默认支持触摸操作,但有时候会和页面的滚动冲突。解决办法是在触摸事件中阻止默认行为,或者限制控制器的交互区域。

还有一个容易被忽略的问题:移动端的像素比通常很高,如果setPixelRatio设置不当,会导致渲染分辨率过高,性能急剧下降。我一般会把像素比限制在 2 以内。

6.4 版本兼容性问题的排查链路

Three.js 的版本更新比较频繁,不同版本之间的 API 有差异。planetary.js如果依赖了特定版本的 Three.js,版本不匹配就会出问题。

排查版本兼容性问题的思路是:先确认planetary.js要求的 Three.js 版本范围,然后检查你实际使用的版本。如果版本不匹配,要么升级/降级 Three.js,要么修改planetary.js的源码来适配。

我遇到过一次问题是着色器编译失败,报错信息很模糊。后来发现是 Three.js 在新版本中修改了着色器内置变量的命名,导致planetary.js的着色器代码无法编译。解决办法是手动修改着色器代码中的变量名。

7. 这个库还能怎么玩:扩展思路与个人体会

planetary.js的基础功能是渲染一个动态地球,但它的扩展空间其实很大。

一个常见的扩展方向是数据可视化。你可以在地球表面根据经纬度坐标打点,显示城市位置、业务分布、物流路径等信息。这需要把经纬度坐标转换为三维空间中的坐标,然后在地球表面添加标记物体。

另一个方向是交互增强。比如点击地球表面的某个区域,弹出该区域的详细信息;或者根据鼠标位置实时计算经纬度,显示在页面上。这些交互需要用到射线检测和坐标转换。

还有一个方向是视觉效果升级。比如添加流星效果、极光效果、城市灯光闪烁效果等。这些效果需要在着色器中做文章,对图形学基础有一定要求。

我个人在实际操作中的体会是:planetary.js是一个很好的起点,它帮你解决了动态地球渲染中最繁琐的部分,让你可以专注于业务逻辑和视觉打磨。但它的文档和示例相对有限,很多参数需要自己去试。我的建议是先把基础效果跑通,然后逐步调整参数,观察每个参数对最终效果的影响。这个过程本身就是一种学习。

最后分享一个小技巧:如果你觉得默认的地球效果不够惊艳,可以尝试在后期处理阶段添加泛光效果。泛光可以让地球边缘的大气辉光更柔和,夜晚的城市灯光更璀璨,整体质感会提升一个档次。具体做法是在渲染完成后,用EffectComposer和UnrealBloomPass做一次后处理。参数方面,泛光强度不要太高,否则会显得很假。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进