1. 项目简介
GitHub地址
在线预览地址
xiaxiangfeng.github.io/sky-cloud-3…
本文将详细讲解如何在Three.js中实现真实感的天空和体积云渲染效果。这种技术常用于游戏、可视化和交互式Web应用中,可以创建出逼真的天空场景,包括动态变化的体积云、太阳光照效果和大气透视。
我们的实现基于着色器(Shader)技术,使用了噪声函数、光线传输模拟和分形布朗运动(FBM)等技术,最终得到了可交互调节的真实云层效果。
2. 核心原理概述
体积云的渲染主要基于以下几个核心概念:
- 体积渲染:云不是简单的2D贴图,而是通过在3D空间中模拟光线传输来实现
- 噪声函数:使用Perlin噪声或类似的噪声函数生成云的形状
- 光线传输:模拟阳光穿过云层时的散射和吸收
- 分形布朗运动(FBM):叠加不同尺度的噪声,创造更自然的云形状
- 光照模型:模拟云朵边缘的银光效果和内部阴影
接下来,我们将从技术实现的角度,逐步讲解这些概念。
3. 项目架构
我们的实现主要分为以下几个部分:
- Three.js场景设置
- 天空球和着色器材质
- 顶点着色器和片元着色器
- 用户交互控制界面
3.1 Three.js基础设置
// Three.js基本场景设置
scene = new THREE.Scene();
camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 30000);
renderer = new THREE.WebGLRenderer({ antialias: true });
// 创建天空球
const skyGeometry = new THREE.SphereGeometry(20000, 64, 32);
skyMesh = new THREE.Mesh(skyGeometry, skyMaterial);
scene.add(skyMesh);
在这段代码中,我们创建了一个大型的球体几何体作为天空的容器,并应用自定义的着色器材质。
4. 着色器实现
着色器是本项目的核心,分为顶点着色器和片元着色器两部分。
4.1 顶点着色器
顶点着色器非常简单,主要负责传递世界坐标给片元着色器:
varying vec3 vWorldPosition;
varying vec2 vUv;
void main() {
// 传递UV坐标给片元着色器
vUv = uv;
// 计算并传递世界坐标
vec4 worldPosition = modelMatrix * vec4(position, 1.0);
vWorldPosition = worldPosition.xyz;
// 投影变换
gl_Position = projectionMatrix * viewMatrix * worldPosition;
}
这个着色器的主要目的是传递每个顶点的世界空间坐标(vWorldPosition),这对于后续在片元着色器中计算光线方向至关重要。
4.2 片元着色器
片元着色器是实现云和天空渲染的核心部分。下面逐部分解析其实现:
4.2.1 基本参数和变量声明
// 从JavaScript传入的参数
uniform float uTime; // 当前时间,用于动画
uniform vec3 uSunDirection; // 太阳方向
uniform sampler2D t_PerlinNoise; // Perlin噪声纹理
uniform float uCloudCoverage; // 云层覆盖率
uniform float uCloudHeight; // 云层高度
uniform float uCloudThickness; // 云层厚度
uniform float uCloudAbsorption; // 光线吸收率
uniform float uWindSpeedX; // X轴风速
uniform float uWindSpeedZ; // Z轴风速
uniform float uMaxCloudDistance; // 云的最大可见距离
// 从顶点着色器传入的变量
varying vec3 vWorldPosition; // 世界坐标
varying vec2 vUv; // UV坐标
// 常量定义
#define TWO_PI 6.28318530718
#define STEPS 22 // 光线步进次数
#define LIGHT_STEPS 5 // 光照计算步进次数
4.2.2 天空颜色计算
// 天空颜色计算函数
vec3 Get_Sky_Color(vec3 rayDir) {
// 计算太阳影响因子(点积)
float sunAmount = max(0.0, dot(rayDir, uSunDirection));
// 天空渐变强度(基于y轴高度)
float skyGradient = pow(max(0.0, rayDir.y), 0.5);
// 混合深蓝和阳光颜色
vec3 skyColor = mix(
vec3(0.1, 0.2, 0.4), // 深蓝色天空
vec3(0.8, 0.7, 0.5), // 柔和的阳光色
pow(sunAmount, 12.0) // 阳光影响的分布曲线
);
// 地平线效果
vec3 horizonColor = vec3(0.7, 0.75, 0.8); // 较淡的地平线颜色
skyColor = mix(horizonColor, skyColor, skyGradient);
// 太阳光晕效果
if (sunAmount > 0.999) {
// 添加一个亮点作为太阳
skyColor += vec3(1.0, 0.7, 0.3) * pow(sunAmount, 1000.0) * 3.0;
}
return skyColor;
}
在这个函数中,我们根据视线方向和太阳方向计算天空的颜色,实现了从地平线到天顶的自然渐变,以及太阳周围的光晕效果。
4.2.3 噪声和分形布朗运动(FBM)函数
// 从噪声纹理采样
float noise3D(in vec3 p) {
vec2 uv = p.xz * 0.01;
return texture(t_PerlinNoise, uv).x;
}
// 分形布朗运动实现
float fbm(vec3 p) {
float t;
float mult = 2.76434; // 频率倍增因子
// 叠加不同频率和振幅的噪声
t = 0.51749673 * noise3D(p); p = m * p * mult;
t += 0.25584929 * noise3D(p); p = m * p * mult;
t += 0.12527603 * noise3D(p); p = m * p * mult;
t += 0.06255931 * noise3D(p);
return t;
}
分形布朗运动(FBM)是一种通过叠加不同尺度的噪声函数来创建自然纹理的技术。在这里,我们使用它生成云的自然形状,每一层噪声的权重递减(0.517, 0.255, 0.125, 0.062),这符合自然界中的分形特性。
4.2.4 云密度计算
// 计算给定位置的云密度
float cloud_density(vec3 pos, vec3 offset, float h) {
// 应用偏移和缩放
vec3 p = pos * 0.0212242 + offset;
// 使用FBM计算基础密度
float dens = fbm(p);
// 应用云覆盖率阈值
float cov = 1.0 - uCloudCoverage;
dens *= smoothstep(cov, cov + 0.05, dens);
// 高度衰减
float height = pos.y - uCloudHeight;
float heightAttenuation = 1.0 - clamp(height / uCloudThickness, 0.0, 1.0);
// 平方加强边界过渡效果
heightAttenuation = heightAttenuation * heightAttenuation;
dens *= heightAttenuation;
return clamp(dens, 0.0, 1.0);
}
这个函数计算空间中任意点的云密度。首先,它使用FBM生成基础密度值,然后应用两个重要修改:
- 云覆盖率阈值:使用
smoothstep函数根据覆盖率参数过滤低密度区域 - 高度衰减:云在垂直方向上的密度应该在云层顶部和底部减小
4.2.5 光线传输模拟
// 模拟光线传输,计算从给定点到光源的透明度
float cloud_light(vec3 pos, vec3 dir_step, vec3 offset, float cov) {
float T = 1.0; // 初始透射率为1(完全透明)
// 沿着光源方向进行采样
for (int i = 0; i < LIGHT_STEPS; i++) {
// 获取当前点的云密度
float dens = cloud_density(pos, offset, 0.0);
// 计算透射率衰减
float T_i = exp(-uCloudAbsorption * dens);
// 累积透射率
T *= T_i;
// 向光源方向前进
pos += dir_step;
}
return T; // 返回最终透射率
}
这个函数模拟了光线从给定点传输到光源的过程。通过多次沿光源方向采样,并根据比尔-朗伯定律(Beer-Lambert law)计算光的衰减,得到达到该点的光量,这是产生真实感云朵所必须的。
4.2.6 云渲染主函数
// 主云渲染函数
vec4 render_clouds(vec3 rayOrigin, vec3 rayDirection) {
// 计算光线与云层平面相交点
float t = (uCloudHeight - rayOrigin.y) / rayDirection.y;
if (t < 0.0) return vec4(0.0); // 光线不与云层相交
// 距离过远就不渲染
if (t > uMaxCloudDistance) return vec4(0.0);
// 计算距离淡出系数
float distanceFade = 1.0 - smoothstep(uMaxCloudDistance * 0.6, uMaxCloudDistance, t);
// 云层起始点
vec3 startPos = rayOrigin + rayDirection * t;
// 风的偏移动画
vec3 windOffset = vec3(uTime * -uWindSpeedX, 0.0, uTime * -uWindSpeedZ);
vec3 pos = startPos;
// 步进大小计算
float march_step = uCloudThickness / float(STEPS);
vec3 dir_step = rayDirection * march_step;
vec3 light_step = uSunDirection * 5.0;
// 变化云层覆盖率
float covAmount = (sin(mod(uTime * 0.02, TWO_PI))) * 0.1 + 0.5;
float coverage = mix(0.4, 0.6, clamp(covAmount, 0.0, 1.0));
// 初始化光线传输变量
float T = 1.0; // 透射率
vec3 C = vec3(0); // 积累的颜色
float alpha = 0.0; // 积累的不透明度
// 光线步进渲染
for (int i = 0; i < STEPS; i++) {
// 跳过云层范围外的部分
if (pos.y < uCloudHeight || pos.y > uCloudHeight + uCloudThickness) {
pos += dir_step;
continue;
}
// 计算当前样本高度比例
float h = float(i) / float(STEPS);
// 获取当前位置云密度
float dens = cloud_density(pos, windOffset, h);
// 只处理有意义的密度值
if (dens > 0.01) {
// 计算透射率
float T_i = exp(-uCloudAbsorption * dens * march_step);
T *= T_i;
// 计算光照
float cloudLight = cloud_light(pos, light_step, windOffset, coverage);
// 模拟高度光照效果
float lightFactor = (exp(h) / 1.75);
// 计算边缘银光效果
float sunContribution = pow(max(0.0, dot(rayDirection, uSunDirection)), 2.0);
vec3 edgeColor = mix(vec3(1.0), vec3(1.0, 0.8, 0.5), sunContribution);
// 颜色混合
vec3 cloudColor = mix(
vec3(0.15, 0.15, 0.2), // 暗部颜色
edgeColor, // 亮边颜色
cloudLight * lightFactor
);
// 积累颜色和不透明度
C += T * cloudColor * dens * march_step * 1.5;
alpha += (1.0 - T_i) * (1.0 - alpha);
}
// 前进一步
pos += dir_step;
if (T < 0.01) break; // 透明度太低就提前结束
}
// 应用太阳光颜色
vec3 sunColor = vec3(0.9, 0.7, 0.5);
vec3 skyColor = vec3(0.4, 0.5, 0.6);
C = C * mix(skyColor, sunColor, 0.5 * pow(max(0.0, dot(rayDirection, uSunDirection)), 2.0));
// 应用距离淡出
alpha *= distanceFade;
C *= distanceFade;
return vec4(C, alpha);
}
这是最复杂的函数,实现了体积云的渲染。主要步骤如下:
- 光线求交:计算视线与云层平面的交点
- 距离检查:过滤太远的云以提高性能
- 光线步进:沿视线方向进行多次采样
- 密度采样:在每个采样点计算云密度
- 光照计算:模拟光线穿过云层的散射和吸收
- 颜色累积:根据密度和光照,累积每个采样点的贡献
- 提前退出:当累积不透明度很高时,停止采样以提高性能
4.2.7 主函数
void main() {
// 计算从相机到当前片元的光线方向
vec3 rayDirection = normalize(vWorldPosition - cameraPosition);
// 计算基础天空颜色
vec3 skyColor = Get_Sky_Color(rayDirection);
// 只有向上的光线才渲染云
vec4 clouds = vec4(0.0);
if (rayDirection.y > 0.0) {
clouds = render_clouds(cameraPosition, rayDirection);
}
// 混合天空和云
vec3 finalColor = mix(skyColor, clouds.rgb, clouds.a);
// 大气透视效果
float t = pow(1.0 - max(0.0, rayDirection.y), 5.0);
finalColor = mix(finalColor, vec3(0.65, 0.7, 0.75), 0.5 * t);
// 简单的Reinhard色调映射
finalColor = finalColor / (finalColor + vec3(1.0));
gl_FragColor = vec4(finalColor, 1.0);
}
主函数将所有部分组合在一起:首先计算天空的基础颜色,然后在适当的方向上渲染云层,最后混合这些元素,并应用大气透视和色调映射以增强真实感。
4.3 核心数学函数详解
本节将深入解析渲染过程中使用的几个关键数学函数和算法。
4.3.1 分形布朗运动(FBM)详解
分形布朗运动是一种模拟自然现象的强大技术,它通过叠加多个不同频率和振幅的噪声函数来创建复杂的纹理。在我们的实现中,FBM用于生成自然的云形状。
数学原理
FBM的基本数学表达式为:
FBM(P) = ∑ A_i * noise(F_i * P)
其中:
- P是采样点坐标
- noise是基础噪声函数
- A_i是每层噪声的振幅(影响强度)
- F_i是每层噪声的频率(影响细节尺度)
在我们的实现中:
float fbm(vec3 p) {
float t;
float mult = 2.76434; // 频率倍增因子
t = 0.51749673 * noise3D(p); p = m * p * mult;
t += 0.25584929 * noise3D(p); p = m * p * mult;
t += 0.12527603 * noise3D(p); p = m * p * mult;
t += 0.06255931 * noise3D(p);
return t;
}
这里我们使用了4个倍频(octave),每层的振幅大约减半(0.517, 0.256, 0.125, 0.063),而频率通过每次应用矩阵变换和乘以倍增因子(约2.76)来增加。
视觉效果
FBM视觉效果示意图:
分形布朗运动(FBM)视觉效果可以想象为一系列噪声叠加的过程:
- 第一层:基础噪声,呈现平滑变化,犹如波浪般的起伏
- 增加第二层:开始出现更多细节,形成更复杂的纹理结构
- 增加第三层:更加丰富的细节出现,轮廓变得更加自然
- 最终四层叠加:展现出自然的、丰富的、具有分形特性的云状结构
这种分层叠加的方法创造出的噪声具有自相似性,与自然界中的云非常接近。
4.3.2 光线传输和比尔-朗伯定律
在体积渲染中,光线如何通过云层传播是真实感的关键。我们使用比尔-朗伯定律(Beer-Lambert law)来模拟这一过程。
数学原理
比尔-朗伯定律描述了光线通过吸收介质时的衰减:
T = e^(-σd)
其中:
- T是透射率(剩余光量与入射光量的比值)
- σ是吸收系数(在我们的实现中由
uCloudAbsorption控制) - d是介质中的行进距离(在我们的实现中结合了云密度dens)
在代码中的应用:
float T_i = exp(-uCloudAbsorption * dens);
T *= T_i;
光线传输模型图解
光线传输示意图:
想象一个场景:
- 太阳位于画面左上方
- 中间是一个椭圆形的云层,内部有不同密度的区域
- 右下方是观察者
光线在云层中的传输过程:
- 太阳光以初始强度100%进入云层
- 光线穿过不同密度的云区域,强度逐渐减弱:100% → 70% → 40% → 25%
- 最终到达观察者的光量取决于沿途吸收的积累效果
- 在云的边缘区域,由于密度较低,光线能够穿透更多,形成明亮的银光效果(Silver Lining)
4.3.3 光线步进算法详解
体积渲染的核心是光线步进(Ray Marching)算法,它沿视线方向对体积进行采样。
算法原理
光线步进算法的基本步骤:
- 确定光线与体积(云层)的交点
- 从交点开始,沿光线方向以固定步长进行采样
- 在每个采样点计算介质属性(如云密度)
- 累积这些点的贡献,得到最终的光照和颜色
光线步进示意图
光线步进算法示意图:
想象一个横截面场景:
- 左下方有一个相机
- 右侧是一个矩形的云层区域
- 相机发出多条光线,其中一条与云层相交
光线步进过程:
- 确定光线与云层的交点(绿色标记)
- 从交点开始,沿光线方向以固定步长进行多次采样(红色点标记)
- 在每个采样点计算云密度值(比如:0.2, 0.5, 0.7, 0.4, 0.1)
- 根据各点的密度值和光照,累积计算最终像素颜色
其工作步骤为:
- 计算当前位置云密度
- 累积光照贡献
- 沿光线方向前进一步
- 重复直到穿过云层或达到最大步数
4.3.4 SmoothStep函数详解
SmoothStep函数是GLSL中的内置函数,用于创建平滑的过渡效果。我们在多处使用了这个函数,特别是在距离淡出和云密度阈值处理中。
数学原理
SmoothStep函数的数学表达式为:
smoothstep(edge0, edge1, x) = t * t * (3 - 2 * t)
其中t = clamp((x - edge0) / (edge1 - edge0), 0.0, 1.0)
这个函数将x值从[edge0, edge1]范围映射到[0,1]范围,并应用平滑的S形曲线。
曲线图解
SmoothStep函数图:
SmoothStep函数可视为一条S形曲线:
- 在x轴上从0到1
- 在y轴上从0到1
- 曲线形状为平滑的S形,而非直线
SmoothStep函数的曲线特征:
- 当x ≤ edge0(默认0)时,结果为0
- 当x ≥ edge1(默认1)时,结果为1
- 在中间区域,结果沿着平滑的S形曲线从0过渡到1,避免了阶跃函数的硬边界
在云渲染中的应用:
- 云密度阈值过滤:
dens *= smoothstep(cov, cov + 0.05, dens); - 距离淡出效果:
float distanceFade = 1.0 - smoothstep(uMaxCloudDistance * 0.6, uMaxCloudDistance, t);
这种平滑过渡可以产生更自然的视觉效果,避免了突变边界。
5. 用户交互控制界面
为了方便调整效果,我们添加了GUI控制面板:
function setupGUI() {
gui = new GUI();
// 云的外观控制
const cloudFolder = gui.addFolder('Cloud Appearance');
cloudFolder.add(params, 'cloudCoverage', 0.0, 1.0, 0.01).name('Cloud Density').onChange(value => {
skyMaterial.uniforms.uCloudCoverage.value = value;
});
cloudFolder.add(params, 'cloudHeight', 100.0, 1000.0, 10.0).name('Cloud Height').onChange(value => {
skyMaterial.uniforms.uCloudHeight.value = value;
});
cloudFolder.add(params, 'cloudThickness', 10.0, 100.0, 5.0).name('Cloud Thickness').onChange(value => {
skyMaterial.uniforms.uCloudThickness.value = value;
});
cloudFolder.add(params, 'cloudAbsorption', 0.5, 2.0, 0.01).name('Light Absorption').onChange(value => {
skyMaterial.uniforms.uCloudAbsorption.value = value;
});
// 云的移动控制
const movementFolder = gui.addFolder('Cloud Movement');
movementFolder.add(params, 'windSpeedX', 0.0, 20.0, 0.1).name('Wind Speed X').onChange(value => {
skyMaterial.uniforms.uWindSpeedX.value = value;
});
movementFolder.add(params, 'windSpeedZ', 0.0, 20.0, 0.1).name('Wind Speed Z').onChange(value => {
skyMaterial.uniforms.uWindSpeedZ.value = value;
});
movementFolder.add(params, 'maxCloudDistance', 1000.0, 20000.0, 500.0).name('Cloud View Distance').onChange(value => {
skyMaterial.uniforms.uMaxCloudDistance.value = value;
});
// 太阳和光照控制
const sunFolder = gui.addFolder('Sun & Lighting');
sunFolder.add(params, 'sunSpeed', 0.001, 0.1, 0.001).name('Sun Cycle Speed');
sunFolder.add(params, 'exposure', 0.1, 2.0, 0.1).name('Exposure').onChange(value => {
renderer.toneMappingExposure = value;
});
}
这个界面允许用户动态调整以下参数:
- 云的外观:密度、高度、厚度和光吸收率
- 云的移动:X轴和Z轴的风速,以及最大可见距离
- 太阳和光照:太阳循环速度和整体曝光度
6. 动画和更新
最后,我们需要每帧更新着色器的uniforms值:
function animate() {
requestAnimationFrame(animate);
elapsedTime = clock.getElapsedTime();
// 更新太阳位置
sunAngle = (elapsedTime * params.sunSpeed) % (Math.PI + 0.2) - 0.11;
sunDirection.set(Math.cos(sunAngle), Math.sin(sunAngle), -Math.cos(sunAngle) * 2.0);
sunDirection.normalize();
// 更新着色器uniforms
if (skyMaterial) {
skyMaterial.uniforms.uSunDirection.value.copy(sunDirection);
skyMaterial.uniforms.uTime.value = elapsedTime;
}
controls.update();
renderer.render(scene, camera);
}
这个函数负责:
- 计算太阳的位置和方向
- 更新着色器中的时间和太阳方向
- 渲染场景
7. 性能优化
体积云渲染是计算密集型的,以下是一些性能优化策略:
- 距离裁剪:远处的云不渲染,使用距离淡出
- 提前退出:当积累的不透明度很高时,提前结束采样
- 密度阈值:只处理密度大于阈值的采样点
- 采样步数:通过GUI可以间接控制采样步数,平衡质量和性能
8. 进一步拓展
此实现还可以进一步拓展:
- 多层云:实现不同高度的云层,如积云、层云和卷云
- 天气变化:模拟从晴天到阴天的渐变过程
- 雨雪效果:添加降水效果
- 时间系统:实现日出、日落和夜晚的变化
9. 总结
通过体积渲染技术和噪声函数的结合,我们成功实现了逼真的动态天空和云效果。这种方法的核心在于模拟光线在体积介质中的传输行为,以及使用分形布朗运动创建自然的云形状。
最终的实现既美观又可交互,用户可以通过GUI轻松调整各种参数,创造出不同的天空效果。同时,我们也应用了多种性能优化策略,使其能在现代浏览器中流畅运行。
希望这篇教程能帮助你理解体积云渲染的基本原理和实现方法!