【视觉震撼】我用Three.js让极光在网页里跳舞!

什么是极光效果?

极光是一种绚丽的自然发光现象,由太阳风与地球大气层中的气体分子相互作用形成。在 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

ThreeModel可以参考我的另一片文章

https://juejin.cn/post/7537618412908380196

shader效果来源

https://www.shadertoy.com/view/XtGGRt

相关推荐
霍格沃兹_测试5 分钟前
软件测试 | 测试开发 | H5页面多端兼容测试与监控
前端
toooooop814 分钟前
本地开发环境webScoket调试,保存html即用
前端·css·websocket
山有木兮木有枝_21 分钟前
手动封装移动端下拉刷新组件的设计与实现
前端
阳光阴郁大boy22 分钟前
大学信息查询平台:一个现代化的React教育项目
前端·react.js·前端框架
小菜全29 分钟前
uniapp新增页面及跳转配置方法
开发语言·前端·javascript·vue.js·前端框架
AlexMercer101231 分钟前
[前端]1.html基础
前端·笔记·学习·html
白水清风40 分钟前
关于Js和Ts中类(class)的知识
前端·javascript·面试
小菜全1 小时前
uniapp基础组件概述
前端·css·vue.js·elementui·css3
小天呐1 小时前
qiankun 微前端接入实战
前端·js·微前端
周航宇JoeZhou1 小时前
JP4-7-MyLesson后台前端(五)
java·前端·vue·elementplus·前端项目·mylesson·管理平台