three.js Sky 天穹:基于 Preetham 大气散射模型的天穹实现与参数调校指南
本文以 three.js 官方文档中的 Sky 类为主体,结合 Sky.js 源码 与官方示例 webgl_shaders_sky.html,完整讲解这一解析式天穹类的定位、导入方式、全部着色器 uniform 的取值与物理含义、太阳光球(sun disc)的处理技巧,以及如何为场景实时渲染并烘焙环境贴图。读完后你可以独立搭建一个 Preetham 天空背景,理解 turbidity、rayleigh、mie 等参数分别影响画面的哪个环节,并知道在 WebGPU 渲染器下应如何切换到对应的 SkyMesh 替代方案。
1. Sky 是什么:解析式天穹与 Preetham 模型
Sky 表示一个用于场景背景的天穹(skydome)。它基于论文 A Practical Analytic Model for Daylight(即 Preetham 模型)实现——这是解析式天穹领域的事实标准。与用 HDR 全景贴图做天空不同,Preetham 模型在着色器中逐像素计算大气散射:
- 瑞利散射(Rayleigh scattering):气体分子对短波长(蓝/紫)光散射更强,是晴空蓝色的来源;
- 米氏散射(Mie scattering):气溶胶粒子造成前向散射,决定太阳周围光晕与雾霾感。
文档明确了两条硬性约束:
Sky只能配合WebGLRenderer使用。如果你使用WebGPURenderer,应改用 SkyMesh(二者算法同源,SkyMesh用 TSL/NodeMaterial 重写,uniform 直接挂在实例属性上,见第 7 节)。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 ) 式的运行时类型判断。
从源码看,构造函数 内部完成了三件关键事情:
- 用
UniformsUtils.clone( shader.uniforms )克隆一套独立 uniforms(每个Sky实例互不影响); - 创建
ShaderMaterial,并设置side: BackSide、depthWrite: false——因为天穹是从内部观看的,且永远作为背景,不应写深度; - 网格本体是
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.SkyShader 的 uniforms 定义 给出了全部参数与默认值。结合着色器实现,其物理含义如下:
| 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 | 动画时钟,驱动云层漂移与形态演化 |
几个容易混淆的点:
turbidity与mieCoefficient是相乘关系:顶点着色器中vBetaM = totalMie( turbidity ) * mieCoefficient(Sky.js),所以两者任一调到 0 都会消除米氏散射。- 瑞利系数的动态衰减:
rayleighCoefficient = rayleigh - ( 1.0 * ( 1.0 - vSunfade ) ),当太阳沉入地平线以下时vSunfade趋近 0,有效瑞利系数被减 1,天空随之变暗——这是"日落变暗"效果的主要来源之一。 up参数:如果场景坐标系不是 Y 朝天(少见情况),需要同步修改up,否则散射计算的天顶角会错误。
4. 着色器内部原理速览
片段着色器(fragmentShader)是 Preetham 模型的主体,计算链可以概括为:
- 光学路径长度:由视线方向与
up的点积得到天顶角zenithAngle,再用经验公式1.0 / ( cos( zenithAngle ) + 0.15 * pow( 93.885 - deg, -1.253 ) )修正地平线附近的路径伸长,得到sR(瑞利)与sM(米氏)路径长度; - 消光因子
Fex = exp( -( vBetaR * sR + vBetaM * sM ) ):视线方向上大气吸收 + 外向散射的总衰减,地平线方向 Fex 趋近 0(天空变白变亮),天顶方向趋近 1; - 入射散射:瑞利相函数
rayleighPhase与米氏相函数hgPhase( cosTheta, mieDirectionalG )分别计算太阳方向对视线方向的散射贡献,合成内散射项Lin; - 夜间底色与太阳光盘:太阳角直径(66 角秒)通过
sundisc = clamp( ( cosTheta - sunAngularDiameterCos ) * 50000.0, 0.0, 1.0 )硬边抠出光盘,乘以showSunDisc开关后加入颜色; - 程序化云层:当
direction.y > 0.0 && cloudCoverage > 0.0时启用。使用无正弦的 2D 梯度噪声 + 4 倍频 FBM 生成密度场,按cloudCoverage阈值裁剪云形;云自身光照取自天空辐射(skyAmbient)与太阳直射(sunColor),并用 Beer 定律自阴影 + Henyey-Greenstein 前向瓣(g = 0.7)在云缘勾出"银边";最后按Fex把云合成进大气消光,使远云自然融入雾色。 - 色调映射与色彩空间:着色器结尾包含
#include <tonemapping_fragment>与#include <colorspace_fragment>,因此天穹的颜色会经过渲染器的 toneMapping 管线——这正是示例中要设置ACESFilmicToneMapping与toneMappingExposure的原因。
顶点着色器中还有一处"地球阴影 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';
SkyMesh 与 Sky 共享同一套 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.far,camera.far需配套放大。 - 全部 13 个 uniform 中,
turbidity/rayleigh/mieCoefficient/mieDirectionalG/sunPosition决定散射外观,cloud*五个参数决定程序化云层,time驱动动画,showSunDisc控制太阳光盘。 - 调曝光要配合
renderer.toneMapping = ACESFilmicToneMapping;烘焙环境贴图时按第 6 节两种策略处理太阳光盘,即可获得干净、无伪影的天空背景与 IBL 环境光。
主要参考文件:Sky.js 源码、WebGL 示例、阳光与环境光示例、SkyMesh 源码、WebGPU 示例。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0627
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
