首页
/ three.js Sky 天穹:基于 Preetham 大气散射模型的天穹实现与参数调校指南

three.js Sky 天穹:基于 Preetham 大气散射模型的天穹实现与参数调校指南

2026-09-07 16:44:31作者:温玫谨Lighthearted

本文以 three.js 官方文档中的 Sky 类为主体,结合 Sky.js 源码 与官方示例 webgl_shaders_sky.html,完整讲解这一解析式天穹类的定位、导入方式、全部着色器 uniform 的取值与物理含义、太阳光球(sun disc)的处理技巧,以及如何为场景实时渲染并烘焙环境贴图。读完后你可以独立搭建一个 Preetham 天空背景,理解 turbidity、rayleigh、mie 等参数分别影响画面的哪个环节,并知道在 WebGPU 渲染器下应如何切换到对应的 SkyMesh 替代方案。

three.js webgl_shaders_sky 示例运行效果:Preetham 模型天空、太阳圆盘与程序化云层

1. Sky 是什么:解析式天穹与 Preetham 模型

Sky 表示一个用于场景背景的天穹(skydome)。它基于论文 A Practical Analytic Model for Daylight(即 Preetham 模型)实现——这是解析式天穹领域的事实标准。与用 HDR 全景贴图做天空不同,Preetham 模型在着色器中逐像素计算大气散射:

  • 瑞利散射(Rayleigh scattering):气体分子对短波长(蓝/紫)光散射更强,是晴空蓝色的来源;
  • 米氏散射(Mie scattering):气溶胶粒子造成前向散射,决定太阳周围光晕与雾霾感。

文档明确了两条硬性约束:

  1. Sky 只能配合 WebGLRenderer 使用。如果你使用 WebGPURenderer,应改用 SkyMesh(二者算法同源,SkyMesh 用 TSL/NodeMaterial 重写,uniform 直接挂在实例属性上,见第 7 节)。
  2. Sky 是 addon,需要显式导入(见第 2 节)。

继承链为:EventDispatcher → Object3D → Mesh,即 Sky 本质是一个带特殊材质的网格,可直接参与场景图、变换与渲染管线的常规操作。

2. 导入与构造

Sky 属于 addons,必须显式导入:

import { Sky } from 'three/addons/objects/Sky.js';

构造函数无参数:

const sky = new Sky();
sky.scale.setScalar( 10000 );
scene.add( sky );

.isSky : boolean(只读):类型测试标志,默认 true,可用于 if ( object.isSky ) 式的运行时类型判断。

从源码看,构造函数 内部完成了三件关键事情:

  1. UniformsUtils.clone( shader.uniforms ) 克隆一套独立 uniforms(每个 Sky 实例互不影响);
  2. 创建 ShaderMaterial,并设置 side: BackSidedepthWrite: false——因为天穹是从内部观看的,且永远作为背景,不应写深度;
  3. 网格本体是 new BoxGeometry( 1, 1, 1 ),所以文档示例中用 scale.setScalar( 10000 ) 把它放大成一个包围相机的大盒子(官方示例中甚至放大到 450000,见第 5 节)。

注意:顶点着色器中有 gl_Position.z = gl_Position.w,即把深度钳到 camera.far 平面。这意味着天穹永远贴在最远处,但你的相机 far 值仍必须大于天穹盒子的实际距离,否则会被视锥裁剪掉(示例中相机 far 为 2000000,天穹缩放 450000,两者是配套的)。

3. 完整 Uniform 参数表

Sky.SkyShaderuniforms 定义 给出了全部参数与默认值。结合着色器实现,其物理含义如下:

Uniform 默认值 含义与影响
turbidity 2 大气浑浊度 T,控制米氏系数 totalMie( T ),越大天空越浑浊、越偏暖白
rayleigh 1 瑞利散射强度系数,越大蓝色天空越饱和;顶点着色器中会被 vSunfade 衰减(太阳在地平线下时天空变暗)
mieCoefficient 0.005 米氏散射总系数缩放,控制前向散射光晕的强弱
mieDirectionalG 0.8 Henyey-Greenstein 相函数的各向异性 g 值,越大太阳周围光晕越尖锐、越集中在太阳方向
sunPosition (0, 0, 0) 太阳方向向量(单位化后使用),是驱动整个散射模型的核心输入
up (0, 1, 0) 世界"天顶"方向,用于计算天顶角与夜间底色
cloudScale 0.0002 云层噪波空间频率,越小云越大
cloudSpeed 0.0001 云层漂移速度(配合 time 使用)
cloudCoverage 0.4 云层覆盖率 0~1,决定噪声阈值裁剪出多大范围的云
cloudDensity 0.4 云层光学密度,通过 Beer 定律 1.0 - exp( depth * cloudDensity * -12.0 ) 影响云的不透明度
cloudElevation 0.5 云平面高度,0 表示云贴近头顶、1 表示云压到地平线附近(源码注释:higher elevation = clouds appear lower/closer
showSunDisc 1 是否渲染太阳光盘。注意在 WebGL 版中它是 0/1 数值型 uniform,示例里可传 true/false(JS 布尔值会隐式转为 1/0)
time 0.0 动画时钟,驱动云层漂移与形态演化

几个容易混淆的点:

  • turbiditymieCoefficient 是相乘关系:顶点着色器中 vBetaM = totalMie( turbidity ) * mieCoefficientSky.js),所以两者任一调到 0 都会消除米氏散射。
  • 瑞利系数的动态衰减rayleighCoefficient = rayleigh - ( 1.0 * ( 1.0 - vSunfade ) ),当太阳沉入地平线以下时 vSunfade 趋近 0,有效瑞利系数被减 1,天空随之变暗——这是"日落变暗"效果的主要来源之一。
  • up 参数:如果场景坐标系不是 Y 朝天(少见情况),需要同步修改 up,否则散射计算的天顶角会错误。

4. 着色器内部原理速览

片段着色器(fragmentShader)是 Preetham 模型的主体,计算链可以概括为:

  1. 光学路径长度:由视线方向与 up 的点积得到天顶角 zenithAngle,再用经验公式 1.0 / ( cos( zenithAngle ) + 0.15 * pow( 93.885 - deg, -1.253 ) ) 修正地平线附近的路径伸长,得到 sR(瑞利)与 sM(米氏)路径长度;
  2. 消光因子 Fex = exp( -( vBetaR * sR + vBetaM * sM ) ):视线方向上大气吸收 + 外向散射的总衰减,地平线方向 Fex 趋近 0(天空变白变亮),天顶方向趋近 1;
  3. 入射散射:瑞利相函数 rayleighPhase 与米氏相函数 hgPhase( cosTheta, mieDirectionalG ) 分别计算太阳方向对视线方向的散射贡献,合成内散射项 Lin
  4. 夜间底色与太阳光盘:太阳角直径(66 角秒)通过 sundisc = clamp( ( cosTheta - sunAngularDiameterCos ) * 50000.0, 0.0, 1.0 ) 硬边抠出光盘,乘以 showSunDisc 开关后加入颜色;
  5. 程序化云层:当 direction.y > 0.0 && cloudCoverage > 0.0 时启用。使用无正弦的 2D 梯度噪声 + 4 倍频 FBM 生成密度场,按 cloudCoverage 阈值裁剪云形;云自身光照取自天空辐射(skyAmbient)与太阳直射(sunColor),并用 Beer 定律自阴影 + Henyey-Greenstein 前向瓣(g = 0.7)在云缘勾出"银边";最后按 Fex 把云合成进大气消光,使远云自然融入雾色。
  6. 色调映射与色彩空间:着色器结尾包含 #include <tonemapping_fragment>#include <colorspace_fragment>,因此天穹的颜色会经过渲染器的 toneMapping 管线——这正是示例中要设置 ACESFilmicToneMappingtoneMappingExposure 的原因。

顶点着色器中还有一处"地球阴影 hack":sunIntensity() 用截断角(cutoffAngle ≈ pi / 1.95)与陡度参数对太阳强度做指数衰减,避免太阳刚落下时天空仍亮如白昼。

5. 实战:官方示例 webgl_shaders_sky.html 的参数用法

webgl_shaders_sky.html 是最完整的可运行参考。它除天空外还挂了一个 CubeCamera,实时把天空烘焙成球面环境贴图贴在一个金属球上,演示"天空 → IBL 环境光"的完整工作流。关键代码:

sky = new Sky();
sky.scale.setScalar( 450000 );          // 天穹盒子放大到 45 万单位
scene.add( sky );

camera = new THREE.PerspectiveCamera( 60, aspect, 100, 2000000 );
// camera.far 必须远大于天穹盒子尺寸,否则天穹被裁剪

const cubeRenderTarget = new THREE.WebGLCubeRenderTarget( 256, { type: THREE.HalfFloatType } );
cubeCamera = new THREE.CubeCamera( 1, 1000, cubeRenderTarget );

renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure = 0.5;    // 曝光必须随天空亮度调节

sky.material.uniforms[ 'time' ].value = performance.now() * 0.001; // 每帧驱动云层

太阳方向由仰角(elevation)与方位角(azimuth)转换得到——这是把"直觉参数"喂给 sunPosition 的标准写法:

const phi = THREE.MathUtils.degToRad( 90 - effectController.elevation );
const theta = THREE.MathUtils.degToRad( effectController.azimuth );
sun.setFromSphericalCoords( 1, phi, theta );
sky.material.uniforms[ 'sunPosition' ].value.copy( sun );

示例中 GUI 暴露的取值范围可作为调参参考区间:

GUI 项 范围 默认
turbidity 0.0 ~ 20.0 10
rayleigh 0.0 ~ 4 3
mieCoefficient 0.0 ~ 0.1 0.005
mieDirectionalG 0.0 ~ 1 0.7
elevation(太阳仰角) 0 ~ 90(度) 65
azimuth(太阳方位角) -180 ~ 180(度) 0
exposure(渲染器曝光) 0 ~ 1 0.05
cloudCoverage / cloudDensity / cloudElevation 0 ~ 1 各 0.4 / 0.4 / 0.5
showSunDisc 布尔 true

注意 GUI 里 turbidity=10、rayleigh=3、mieDirectionalG=0.7 等默认值刻意比着色器原始默认值更"戏剧化",用于凸显各参数的视觉差异;如果你的场景偏写实,从第 3 节表格的着色器默认值出发调参更稳妥。

6. 关键技巧:环境贴图烘焙时关闭太阳光盘

文档原文指出:在生成环境贴图时,关闭太阳圆盘可以避免伪影(artifacts)。原理是太阳光盘是一小块极高亮度的近硬边区域,被立方体贴图采样后会在环境贴图里留下刺眼的采样异常与插值条纹。标准做法是:

// disable before rendering environment map
sky.material.uniforms.showSunDisc.value = false;
// ...
// re-enable before scene sky box rendering
sky.material.uniforms.showSunDisc.value = true;

webgl_lights_sunlight.html 展示了另一种更彻底的用法:用 PMREMGenerator 从天空生成 PBR 环境贴图时,永久关闭太阳圆盘,因为太阳的照明改由一个实际的平行光(spotlight/sunlight)来承担:

pmremGenerator = new THREE.PMREMGenerator( renderer );
// ...
sky.material.uniforms.showSunDisc.value = false; // the sun is represented by the light

两条经验:

  • 若你的场景里太阳光由 DirectionalLight/SpotLight 表达,环境贴图里就不该再出现光盘,用 webgl_lights_sunlight 的写法;
  • 若没有独立太阳光源、想让 IBL 自己携带太阳的高光信息,则按文档写法在"烘焙 → 渲染"两步之间切换 showSunDisc 的值即可。

7. WebGPU 用户:切换到 SkyMesh

再次强调文档的限制:Sky 仅兼容 WebGLRenderer。使用 WebGPURenderer 时应导入 SkyMesh

import { SkyMesh } from 'three/addons/objects/SkyMesh.js';

SkyMeshSky 共享同一套 Preetham 模型与云层算法,但参数访问方式不同——uniform 直接以 UniformNode 属性挂在实例上,而不是 material.uniforms 字典:

// WebGPU 版:直接操作实例属性
sky.turbidity.value = 10;
sky.showSunDisc.value = false;   // 对应 WebGL 版 sky.material.uniforms.showSunDisc.value
sky.sunPosition.value.copy( sun );

对应示例为 webgpu_sky.html,其 GUI 参数集(turbidity、rayleigh、mieCoefficient、mieDirectionalG、elevation、azimuth、exposure 及三个云层参数)与 WebGL 版完全一致,迁移时只需把 uniforms[ 'xxx' ].value 的访问路径替换为属性访问。另外 SkyMesh 使用 isSkyMesh 作为类型标志(其 isSky 已标记为 deprecated),做运行时类型判断时注意区分。

8. 小结

  • Sky 是 three.js 中基于 Preetham 解析模型的实时大气散射天穹,addon 导入路径为 three/addons/objects/Sky.js,仅支持 WebGLRenderer;WebGPU 下使用同源实现 SkyMesh
  • 实现上是一个 BackSide + depthWrite: false 的 1×1×1 盒体,靠 scale.setScalar() 放大包围场景,深度钳到 camera.farcamera.far 需配套放大。
  • 全部 13 个 uniform 中,turbidity/rayleigh/mieCoefficient/mieDirectionalG/sunPosition 决定散射外观,cloud* 五个参数决定程序化云层,time 驱动动画,showSunDisc 控制太阳光盘。
  • 调曝光要配合 renderer.toneMapping = ACESFilmicToneMapping;烘焙环境贴图时按第 6 节两种策略处理太阳光盘,即可获得干净、无伪影的天空背景与 IBL 环境光。

主要参考文件:Sky.js 源码WebGL 示例阳光与环境光示例SkyMesh 源码WebGPU 示例

登录后查看全文
热门项目推荐
相关项目推荐