程序化天空、日落与行星渲染指南:大气散射原理与着色器实战
发布: 2026-05-13 阅读: 15 分钟 3D 图形 · 着色器 · 教程
你有没有在游戏里盯着日落发呆过 想过为什么白天天空是蓝色 日落却变成红色 为什么太空游戏里的行星边缘会有一圈朦胧的光晕 答案藏在大气散射里 这篇指南从零开始 教你用 JavaScript 和着色器程序化渲染这一切
本文基于 Maxime Heckel 的经典博客 "On Rendering the Sky, Sunsets, and Planets"(HN 392 分) 他从物理原理讲到代码实现 亲手构建了一个实时天空渲染器 我们会深入拆解这一切 从让天空变蓝的瑞利散射 到把日落烧成橙红色的米氏散射
为什么要程序化生成天空
在 3D 图形里 天空不只是一张背景图 程序化生成的天空会随着太阳位置动态变化 形成真实的昼夜交替和日出日落效果 这对以下场景至关重要
- 游戏 — 真实户外环境 动态时间变化
- 飞行模拟器 — 高空环境下的准确天空表现
- 建筑可视化 — 自然光照条件模拟
- 行星渲染 — 打造可信的异星世界
与使用静态天空盒贴图不同 程序化生成能实时适配任何光照条件 无需预烘焙素材就能创造无限变化
天空颜色的物理原理
天空的颜色来自大气散射 — 太阳光与地球大气中粒子的相互作用 你需要理解两种主要类型
瑞利散射:为什么天空是蓝色的
瑞利散射发生在光与远小于其波长的粒子相互作用时 — 在地球上就是空气分子(氮气和氧气) 关键公式:散射强度与 1/λ⁴(波长的四次方)成正比 这意味着蓝光(短波长 ~450nm)的散射强度大约是红光(长波长 ~650nm)的 16 倍
当太阳在头顶时 光线穿过相对较薄的大气层 蓝光向四面八方散射 填满整片天空 红光则基本不受影响——所以正午的太阳看起来偏黄白色
在代码中 瑞利散射建模如下
// 各波长的瑞利散射系数
const rayleighBeta = (lambda) => {
const n = 1.00029; // 空气折射率
const N = 2.504e25; // 分子数密度
const pn = 0.035; // 退偏振因子
const lambdaM = lambda * 1e-9; // 纳米转米
return (8 * Math.PI ** 3 * (n ** 2 - 1) ** 2 * (6 + 3 * pn)) /
(3 * N * lambdaM ** 4 * (6 - 7 * pn));
};
米氏散射:为什么日落是红色的
米氏散射发生在光与粒径接近其波长的粒子相互作用时 — 比如气溶胶、灰尘、水滴 与瑞利散射不同 米氏散射对波长依赖很小(约 λ⁻¹) 这意味着它均匀散射所有颜色的光 在太阳周围形成白色的晖光
但日落的效果另有玄机:当太阳低垂在地平线上 光线需要穿过厚得多的大气层——正午的 40 倍以上 蓝光在到达你眼睛之前 早就被瑞利散射光了 红橙光凭借更长的波长 才能一路坚持到最后 这就是为什么日落天空燃烧着温暖的颜色 而头顶的天空渐变成深蓝紫色
构建程序化天空渲染器
现在我们来动手实现 Heckel 的渲染器使用 Three.js 配合自定义的顶点着色器和片元着色器(GLSL 编写) 核心流程如下
- 用一个巨大的球体围绕相机作为天空
- 对每个片元计算视线穿过大气的路径
- 沿视线积分散射(入射散射 + 出射散射)
- 根据太阳角度混合瑞利(蓝色)和米氏(白色/红色)贡献
- 添加地面颜色和星星
第一步:天空球体
最简单的方法是用一个大球体包围场景 法线朝内 Three.js 代码如下
const skyGeo = new THREE.SphereGeometry(400, 64, 40);
const skyMat = new THREE.ShaderMaterial({
vertexShader: skyVertexShader,
fragmentShader: skyFragmentShader,
uniforms: {
sunDirection: { value: new THREE.Vector3(0, 0.5, -1).normalize() },
rayleighCoefficient: { value: 0.0025 },
mieCoefficient: { value: 0.001 },
turbidity: { value: 2.0 }
},
side: THREE.BackSide
});
const sky = new THREE.Mesh(skyGeo, skyMat);
第二步:片元着色器中的大气散射
片元着色器是核心魔法所在
// GLSL — 片元着色器
uniform vec3 sunDirection;
uniform float rayleighCoefficient;
uniform float mieCoefficient;
uniform float turbidity;
varying vec3 vWorldPosition;
const float PI = 3.14159265359;
// 瑞利相位函数(偶极散射)
float rayleighPhase(float cosTheta) {
return (3.0 / (16.0 * PI)) * (1.0 + cosTheta * cosTheta);
}
// 米氏相位函数(Henyey-Greenstein)
float miePhase(float cosTheta, float g) {
return (3.0 / (8.0 * PI)) * ((1.0 - g * g) * (1.0 + cosTheta * cosTheta)) /
((2.0 + g * g) * pow(1.0 + g * g - 2.0 * g * cosTheta, 1.5));
}
void main() {
vec3 viewDir = normalize(vWorldPosition - cameraPosition);
float cosTheta = dot(viewDir, sunDirection);
// 散射系数
vec3 rayleighScattering = vec3(
rayleighCoefficient / pow(0.650, 4.0), // 红
rayleighCoefficient / pow(0.570, 4.0), // 绿
rayleighCoefficient / pow(0.475, 4.0) // 蓝
);
// 米氏散射(大致与波长无关)
float mieScattering = mieCoefficient;
// 光学深度(简化版 — 穿过大气的路径长度)
float rayleighDepth = exp(-turbidity * 0.5);
float mieDepth = exp(-turbidity * 0.3);
// 最终颜色
vec3 color = rayleighPhase(cosTheta) * rayleighScattering * rayleighDepth
+ miePhase(cosTheta, 0.76) * mieScattering * mieDepth;
gl_FragColor = vec4(color, 1.0);
}
第三步:日落效果
要获得真实的日落 需要一个多次散射近似 单次散射模型(光只反弹一次)能渲染出蓝天 但会丢失温暖的日落颜色 Heckel 的方法增加了一个二次散射项 捕捉穿过大气向前散射的红橙色光
// 日落贡献 — 二次散射
vec3 sunsetGlow = vec3(1.0, 0.6, 0.2) *
exp(-3.0 * (1.0 - cosTheta)) *
(1.0 - exp(-turbidity * 2.0));
color += sunsetGlow * 0.5;
这就在太阳低角度时 在地平线附近制造出标志性的温暖光晕 需要调节的关键参数:turbidity(气溶胶浓度 越大日落越红) sunAngle(10° 以下进入日落模式) 以及瑞利和米氏系数的平衡
渲染行星
现在让我们给场景加上行星 一个有说服力的行星需要三层
- 带纹理的球体 — 行星本体 程序化或基于图片的材质
- 大气光晕 — 一个透明外壳 附加散射效果
- 阴影 — 相位效果(行星的暗面)
着色器生成行星材质
用噪声函数就可以程序化生成令人印象深刻的行星纹理
// 片元着色器 — 程序化行星纹理
uniform float seed;
float terrain(vec3 p) {
float n = snoise(p * 0.5 + seed);
n += snoise(p * 1.0 + seed) * 0.5;
n += snoise(p * 2.0 + seed) * 0.25;
return n;
}
void main() {
vec3 p = normalize(vPosition);
float height = terrain(p);
// 基于高度的调色板
vec3 ocean = vec3(0.1, 0.2, 0.5);
vec3 land = vec3(0.2, 0.5, 0.1);
vec3 mountain = vec3(0.4, 0.3, 0.2);
vec3 color = mix(ocean, land, smoothstep(0.0, 0.3, height));
color = mix(color, mountain, smoothstep(0.4, 0.6, height));
gl_FragColor = vec4(color, 1.0);
}
大气层光晕
行星大气层渲染为一个稍大的球体包裹行星 用边缘光效果——光穿过最多大气层的边缘散射最强
// 大气层光晕着色器
float rim = 1.0 - max(0.0, dot(viewDir, normal));
float atmosphere = pow(rim, 3.0) * 0.8;
vec3 atmosColor = mix(vec3(0.5, 0.7, 1.0), vec3(1.0, 0.5, 0.2),
dot(viewDir, lightDir) * 0.5 + 0.5);
gl_FragColor = vec4(atmosColor, atmosphere);
这给出行星特有的蓝色或橙色边缘光晕 取决于大气成分
星空背景:噪声生成的星场
没有星星的行星场景是不完整的 使用基于哈希的噪声函数程序化生成星星 而不是静态贴图
// 使用哈希噪声生成星星
float stars(vec3 p) {
vec3 i = floor(p * 200.0);
vec3 f = fract(p * 200.0);
float star = hash(i);
return step(0.997, star); // 稀疏 — 只有最亮点
}
void main() {
vec3 color = stars(vPosition) * vec3(1.0);
// 添加闪烁变化
color *= 0.5 + 0.5 * sin(time * 2.0 + hash(i) * 6.28);
gl_FragColor = vec4(color, 1.0);
}
关键在于让星星稀疏(只有约 0.3% 的随机位置成为星星)且亮度不一 加上轻微闪烁就能打造生动的夜空
性能优化技巧
大气散射计算量较大 以下是实时渲染的优化策略
- 减少采样次数 — 视线的积分步长从 32 减少到 8-16
- 预计算散射表 — 将散射系数烘焙到查找纹理中
- 降低天空球分辨率 — 用 32x16 分段代替 64x40
- 合并行星大气与主天空着色器 — 减少绘制调用
- 将大气光晕限制在边缘几像素 — 不做全屏计算
总结
渲染天空是一个"一点物理知识就能走很远"的问题 理解了瑞利散射和米氏散射 你就能用几百行着色器代码 创造出令人惊叹的实时天空、逼真的日落和有说服力的行星大气
本文涵盖的技术——多次散射近似、相位函数、光学深度积分、程序化噪声——构成了现代天空渲染的基础 从简单的天空球开始 逐步添加散射效果 最终构建完整的行星渲染系统
准备好了吗? 完整源码、交互式演示和更详细的解释 请参考 Maxime Heckel 的原博客文章(HN 392 分)