什么是极光效果?
极光是一种绚丽的自然发光现象,由太阳风与地球大气层中的气体分子相互作用形成。在 3D 渲染中,极光通常表现为:
- 流动的彩色光带,以绿色、粉色为主
- 半透明、模糊的边缘
- 随时间缓慢变化的形态
- 与星空、夜空背景融合的层次感
以下代码通过 WebGL 着色器实现了上述特征,集成到 Three.js 场景中。






接下来我们将通过自定义材质实现极光
自定义材质 useAurora
js
import { Mesh,Vector3,ShaderMaterial,UniformsUtils} from 'three';
export class useAurora extends Mesh{
constructor(geometry,options = {} ) {
super( geometry );
const shader = options.shader || useAuroraShader;
const speed = options.speed ?? 0.1;
const steps = options.steps ?? 50; // 光线步进步数
const iterations = options.iterations ?? 5; // 噪声迭代次数
const hueOffset = options.hueOffset || new Vector3(2.15, -0.5, 1.8); // 色相偏移参数 vec3(2.15, -0.5, 1.8)
this.material = new ShaderMaterial( {
blending: 1,
transparent: true,
side: 2,
uniforms: UniformsUtils.merge( [
shader.uniforms
]),
vertexShader: shader.vertexShader,
fragmentShader: shader.fragmentShader,
});
this.material.uniforms[ 'uSteps' ].value = steps
this.material.uniforms[ 'iterations' ].value = iterations
this.material.uniforms[ 'uHueOffset' ].value = hueOffset
this.onBeforeRender = ()=> {
this.material.uniforms.iTime.value += speed
};
}
}
useAuroraShader
着色器核心
vertexShader
着色器是 WebGL 渲染的核心,分为顶点着色器(处理顶点位置)和片段着色器(处理像素颜色)。顶点着色器的主要工作是将 "世界空间方向向量" 传递给片段着色器,这个向量决定了每个像素对应的 "视线方向",是计算极光的基础。
js
varying vec2 vUv;
varying vec3 vWorldDirection;
void main() {
vUv = uv; // 计算世界空间位置
vec4 worldPosition = modelMatrix * vec4(position, 1.0); // 传递世界空间方向向量(用于片段着色器计算视线方向)
vWorldDirection = worldPosition.xyz; // 计算最终屏幕位置
gl_Position = projectionMatrix * viewMatrix * worldPosition;
}
片段着色器:生成极光与星空
片段着色器是实现极光效果的关键,包含多个核心函数:
1. 噪声函数:模拟自然形态
自然界的极光没有规则形状,因此需要用 "噪声函数" 生成随机但连续的图案。代码中triNoise2d
函数通过迭代计算三角形噪声,生成极光的流动纹理:
js
float triNoise2d(in vec2 p, float spd) {
float z = 1.8; // 初始缩放因子
float z2 = 2.5; // 缩放衰减因子
float rz = 0.; // 噪声结果
p *= mm2(p.x * 0.06); // 初始旋转
vec2 bp = p;
// 多尺度迭代:叠加不同频率的噪声,增强细节
for (float i = 0.; i < iterations; i++) {
vec2 dg = tri2(bp * 1.85) * .75; // 计算三角形噪声偏移
dg *= mm2(time * spd); // 随时间旋转偏移,产生流动感
p -= dg / z2;
bp *= 1.3; // 放大坐标,增加高频细节
z2 *= .45; // 衰减缩放因子
z *= .42; // 衰减权重
p *= 1.21 + (rz - 1.0) * .02; // 反馈调整,增强混沌感
rz += tri(p.x + tri(p.y)) * z; // 叠加噪声
p *= -m2; // 反向旋转,增加随机性
}
return clamp(1. / pow(rz * 29., 1.3), 0., .55); // 归一化结果
}
2. 极光颜色计算:aurora 函数
aurora
函数根据视线方向计算极光的颜色和透明度:核心逻辑是 "光线步进":从观察者位置出发,沿视线方向逐步采样,每个采样点的极光强度由噪声函数决定,颜色会通过正弦函数生成冷暖色交替的效果(模拟真实极光的绿、粉、紫等颜色)。
js
vec4 aurora(vec3 ro, vec3 rd) {
vec4 col = vec4(0); // 最终颜色
vec4 avgCol = vec4(0); // 平均颜色(用于平滑过渡)
// 光线步进:模拟从观察者到远方的采样
for (float i = 0.; i < uSteps; i++) {
// 随机偏移,减少带状纹理的规律性
float of = 0.006 * hash21(gl_FragCoord.xy) * smoothstep(0., 15., i);
// 计算采样点位置
float pt = ((.8 + pow(i, 1.4) * .002) - ro.y) / (rd.y * 2. + 0.4);
pt -= of;
vec3 bpos = ro + pt * rd; // 世界空间采样点
// 计算该点的噪声值(极光强度)
vec2 p = bpos.zx;
float rzt = triNoise2d(p, 0.06);
// 计算颜色:基于迭代次数和色相偏移
vec4 col2 = vec4(0, 0, 0, rzt);
col2.rgb = (sin(1. - uHueOffset + i * 0.043) * 0.5 + 0.5) * rzt;
// 平滑颜色过渡
avgCol = mix(avgCol, col2, .5);
// 随距离衰减极光强度
col += avgCol * exp2(-i * 0.065 - 2.5) * smoothstep(0., 5., i);
}
// 根据视线方向的Y分量调整极光可见性(顶部更亮)
col *= (clamp(rd.y * 15. + .4, 0., 1.));
return col * 1.8;
}
3. 星空和夜空背景:增强沉浸感
js
// 星空效果
vec3 stars(in vec3 p) {
vec3 c = vec3(0.);
float res = 1920.0; // 分辨率相关缩放
// 多层星星:不同大小和亮度
for (float i = 0.; i < 4.; i++) {
vec3 q = fract(p * (.15 * res)) - 0.5; // 计算局部坐标
vec3 id = floor(p * (.15 * res)); // 网格ID(用于随机)
vec2 rn = nmzHash33(id).xy; // 随机数
// 绘制星星(圆形光斑)
float c2 = 1. - smoothstep(0., .6, length(q));
// 随机生成星星(部分网格才会有星星)
c2 *= step(rn.x, .0005 + i * i * 0.001);
// 星星颜色(偏白或偏蓝)
c += c2 * (mix(vec3(1.0, 0.49, 0.1), vec3(0.75, 0.9, 1.), rn.y) * 0.1 + 0.9);
p *= 1.3; // 放大坐标,生成更多星星
}
return c * c * .8; // 增强对比度
}
// 夜空背景
vec3 bg(in vec3 rd) {
// 基于视线方向与"参考方向"的夹角计算渐变色
float sd = dot(normalize(vec3(-0.5, -0.6, 0.9)), rd) * 0.5 + 0.5;
sd = pow(sd, 5.);
// 从深蓝到靛蓝的渐变
vec3 col = mix(vec3(0.05, 0.1, 0.2), vec3(0.1, 0.05, 0.2), sd);
return col * .63;
}
4. 主函数:整合所有效果
js
void main() {
vec3 rd = vWorldDirection; // 从顶点着色器获取视线方向
// 应用相机旋转,让极光随视角变化
float cameraYaw = atan(cameraDirection.x, cameraDirection.z);
float cameraPitch = asin(cameraDirection.y);
rd.xz *= mm2(cameraYaw * 0.1 + sin(time * 0.05) * 0.2);
rd.yz *= mm2(cameraPitch * 0.1);
vec3 col = vec3(0.);
vec3 brd = rd;
// 边缘淡化:避免极光在屏幕边缘过于突兀
float fade = smoothstep(0., 0.01, abs(brd.y)) * 0.1 + 0.9;
col = bg(brd) * fade; // 基础夜空背景
// 上方天空(brd.y > 0):显示极光和星空
if (brd.y > 0.) {
vec4 aur = smoothstep(0., 1.5, aurora(vec3(0), brd)) * fade;
col += stars(brd); // 添加星空
col = col * (1. - aur.a) + aur.rgb; // 混合极光与背景
}
// 下方天空(地面方向):弱化的极光和地面反光
else {
brd.y = abs(brd.y);
col = bg(brd) * fade * 0.6; // 更暗的背景
vec4 aur = smoothstep(0.0, 2.5, aurora(vec3(0), brd));
col += stars(brd) * 0.1; // 更少的星星
col = col * (1. - aur.a) + aur.rgb;
// 模拟地面反射的极光颜色
vec3 pos = ((0.5) / brd.y) * brd;
float nz2 = triNoise2d(pos.xz * vec2(.5, .7), 0.);
col += mix(vec3(0.2, 0.25, 0.5) * 0.08, vec3(0.3, 0.3, 0.5) * 0.7, nz2 * 0.4);
}
gl_FragColor = vec4(col, 1.); // 输出最终颜色
}
gl_FragColor
是最终输出的颜色
5. 在多说一句 色相动态变化
颜色参数已经放出来了 大家可以通过hueOffset
参数进行调整
js
// 随时间动态变化色相
vec3 hueShift = vec3(
2.15 + sin(iTime*0.1)*0.5,
-0.5 + cos(iTime*0.2)*0.3,
1.2 + sin(iTime*0.15)*0.4 );
col2.rgb = (sin(1. - hueShift + i*0.043)*0.5+0.5)*rzt;
graph LR
A[原始噪声值] --> B[色相偏移向量]
B --> C[正弦函数处理]
C --> D[0-1范围]
D --> E[乘以噪声强度]
E --> F[最终极光颜色]
全部shander
js
const useAuroraShader = {
uniforms:{
iTime: { value: 0 },
uSteps: {value: 50},//光线步进步数
iterations: {value: 5},//噪声迭代次数
uHueOffset: {value: new Vector3(2.15, -0.5, 1.8)},//色相偏移参数 vec3(2.15, -0.5, 1.8)
cameraDirection: { value: new Vector3() }
},
vertexShader: `
varying vec2 vUv;
varying vec3 vWorldDirection;
void main() {
vUv = uv;
vec4 worldPosition = modelMatrix * vec4(position, 1.0);
vWorldDirection = worldPosition.xyz;
gl_Position = projectionMatrix * viewMatrix * worldPosition;
}
`,
fragmentShader: `
#define time iTime
varying vec3 vWorldDirection;
uniform float iTime;
uniform float uSteps;
uniform float iterations;
uniform vec3 uHueOffset;
uniform vec3 cameraDirection;
mat2 mm2(in float a) {
float c = cos(a), s = sin(a);
return mat2(c, s, -s, c);
}
mat2 m2 = mat2(0.95534, 0.29552, -0.29552, 0.95534);
float tri(in float x) {
return clamp(abs(fract(x) - .5), 0.01, 0.49);
}
vec2 tri2(in vec2 p) {
return vec2(tri(p.x) + tri(p.y), tri(p.y + tri(p.x)));
}
float triNoise2d(in vec2 p, float spd) {
float z = 1.8;
float z2 = 2.5;
float rz = 0.;
p *= mm2(p.x * 0.06);
vec2 bp = p;
for (float i = 0.; i < iterations; i++) {
vec2 dg = tri2(bp * 1.85) * .75;
dg *= mm2(time * spd);
p -= dg / z2;
bp *= 1.3;
z2 *= .45;
z *= .42;
p *= 1.21 + (rz - 1.0) * .02;
rz += tri(p.x + tri(p.y)) * z;
p *= -m2;
}
return clamp(1. / pow(rz * 29., 1.3), 0., .55);
}
float hash21(in vec2 n) {
return fract(sin(dot(n, vec2(12.9898, 4.1414))) * 43758.5453);
}
vec4 aurora(vec3 ro, vec3 rd) {
vec4 col = vec4(0);
vec4 avgCol = vec4(0);
for (float i = 0.; i < uSteps; i++) {
float of = 0.006 * hash21(gl_FragCoord.xy) * smoothstep(0., 15., i);
float pt = ((.8 + pow(i, 1.4) * .002) - ro.y) / (rd.y * 2. + 0.4);
pt -= of;
vec3 bpos = ro + pt * rd;
vec2 p = bpos.zx;
float rzt = triNoise2d(p, 0.06);
vec4 col2 = vec4(0, 0, 0, rzt);
col2.rgb = (sin(1. - uHueOffset + i * 0.043) * 0.5 + 0.5) * rzt;
avgCol = mix(avgCol, col2, .5);
col += avgCol * exp2(-i * 0.065 - 2.5) * smoothstep(0., 5., i);
}
col *= (clamp(rd.y * 15. + .4, 0., 1.));
return col * 1.8;
}
vec3 nmzHash33(vec3 q) {
uvec3 p = uvec3(ivec3(q));
p = p * uvec3(374761393U, 1103515245U, 668265263U) + p.zxy + p.yzx;
p = p.yzx * (p.zxy ^ (p >> 3U));
return vec3(p ^ (p >> 16U)) * (1.0 / vec3(0xffffffffU));
}
vec3 stars(in vec3 p) {
vec3 c = vec3(0.);
float res = 1920.0; // 固定分辨率值
for (float i = 0.; i < 4.; i++) {
vec3 q = fract(p * (.15 * res)) - 0.5;
vec3 id = floor(p * (.15 * res));
vec2 rn = nmzHash33(id).xy;
float c2 = 1. - smoothstep(0., .6, length(q));
c2 *= step(rn.x, .0005 + i * i * 0.001);
c += c2 * (mix(vec3(1.0, 0.49, 0.1), vec3(0.75, 0.9, 1.), rn.y) * 0.1 + 0.9);
p *= 1.3;
}
return c * c * .8;
}
vec3 bg(in vec3 rd) {
float sd = dot(normalize(vec3(-0.5, -0.6, 0.9)), rd) * 0.5 + 0.5;
sd = pow(sd, 5.);
vec3 col = mix(vec3(0.05, 0.1, 0.2), vec3(0.1, 0.05, 0.2), sd);
return col * .63;
}
void main() {
vec3 rd = vWorldDirection;
// 相机旋转应用
float cameraYaw = atan(cameraDirection.x, cameraDirection.z);
float cameraPitch = asin(cameraDirection.y);
rd.xz *= mm2(cameraYaw * 0.1 + sin(time * 0.05) * 0.2);
rd.yz *= mm2(cameraPitch * 0.1);
vec3 col = vec3(0.);
vec3 brd = rd;
float fade = smoothstep(0., 0.01, abs(brd.y)) * 0.1 + 0.9;
col = bg(brd) * fade;
if (brd.y > 0.) {
vec4 aur = smoothstep(0., 1.5, aurora(vec3(0), brd)) * fade;
col += stars(brd);
col = col * (1. - aur.a) + aur.rgb;
} else {
brd.y = abs(brd.y);
col = bg(brd) * fade * 0.6;
vec4 aur = smoothstep(0.0, 2.5, aurora(vec3(0), brd));
col += stars(brd) * 0.1;
col = col * (1. - aur.a) + aur.rgb;
vec3 pos = ((0.5) / brd.y) * brd;
float nz2 = triNoise2d(pos.xz * vec2(.5, .7), 0.);
col += mix(vec3(0.2, 0.25, 0.5) * 0.08, vec3(0.3, 0.3, 0.5) * 0.7, nz2 * 0.4);
}
gl_FragColor = vec4(col, 1.);
}
`
}
使用示例
js
const geometry = new SphereGeometry(1,100,100,0,Math.PI * 2, 0,Math.PI / 2);// 锅盖形状
// const geometry = new SphereGeometry(1,100,100);// 完全的球体
const mesh = new useAurora(geometry,{
uSteps: 30,//光线步进步数
iterations: 2,//噪声迭代次数
hueOffset: new Vector3(2.15, -0.5, 1.8),//色相偏移参数 vec3(2.15, -0.5, 1.8)
});
ThreeModel.scene.add(mesh);
性能影响参考
迭代类型 | 默认值 | 移动端建议 | 高端PC建议 |
---|---|---|---|
噪声迭代 | 5 | 3-4 | 6-8 |
光线步进 | 50 | 20-30 | 70-100 |
https://juejin.cn/post/7537618412908380196
https://www.shadertoy.com/view/XtGGRt