使用 Three.js 实现流光特效

大家好!我是 数擎AI,一位热爱探索新技术的前端开发者,在这里分享前端和 Web3D、AI 技术的干货与实战经验。如果你对技术有热情,欢迎关注我的文章,我们一起成长、进步!
开发领域 :前端开发 | AI 应用 | Web3D | 元宇宙
技术栈 :JavaScript、React、ThreeJs、WebGL、Go
经验经验 :6 年+ 前端开发经验,专注于图形渲染和 AI 技术
开源项目 :AI简历、元宇宙、数字孪生

源码地址: github.com/dezhizhang/...

演示地址: shader.shuqin.cc/xx3fdh

在本文中,我们将深入探讨如何使用 Three.js 和 GLSL(OpenGL Shading Language)编写一个火焰效果的着色器。通过这个教程,你将学习如何创建动态的火焰效果,以及如何将它应用到 3D 渲染中。

着色器简介

着色器是图形渲染管线中的一个重要组成部分,负责处理图形渲染过程中每个像素的颜色计算。通常,着色器分为两个主要部分:

  • 顶点着色器(Vertex Shader):用于处理每个顶点的位置和属性。
  • 片段着色器(Fragment Shader):用于计算每个像素的颜色和纹理。

在这个示例中,我们将重点关注如何编写片段着色器来模拟火焰的视觉效果。

火焰着色器的结构

我们的火焰着色器由以下几个部分组成:

  1. Uniforms:这部分包含了着色器所需的外部变量,如时间、分辨率等。
  2. 顶点着色器:负责传递 UV 坐标给片段着色器。
  3. 片段着色器:执行火焰效果的核心计算,包括噪声生成、分形布朗运动(FBM)等。

以下是整个着色器的代码实现:

javascript 复制代码
const flameShader = {
    uniforms: {
        time: { value: 0 },
        resolution: { value: new THREE.Vector2() }
    },
    vertexShader: `
        varying vec2 vUv;
        void main() {
            vUv = uv;
            gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
        }
    `,
    fragmentShader: `
        precision highp float;
        #define NUM_OCTAVES 5
        varying vec2 vUv;
        uniform float time;
        uniform vec2 resolution;

        // 生成随机数
        float rand(vec2 n) {
            return fract(sin(dot(n, vec2(12.9898, 4.1414))) * 43758.5453);
        }

        // 生成噪声
        float noise(vec2 p) {
            vec2 ip = floor(p);
            vec2 u = fract(p);
            u = u * u * (3.0 - 2.0 * u);
            float res = mix(
                mix(rand(ip), rand(ip + vec2(1.0, 0.0)), u.x),
                mix(rand(ip + vec2(0.0, 1.0)), rand(ip + vec2(1.0, 1.0)), u.x),
                u.y
            );
            return res * res;
        }

        // 分形布朗运动
        float fbm(vec2 x) {
            float v = 0.0;
            float a = 0.5;
            vec2 shift = vec2(100);
            mat2 rot = mat2(cos(0.5), sin(0.5), -sin(0.5), cos(0.5));
            for (int i = 0; i < NUM_OCTAVES; ++i) {
                v += a * noise(x);
                x = rot * x * 2.0 + shift;
                a *= 0.5;
            }
            return v;
        }

        void main() {
            vec2 shake = vec2(sin(time * 1.5) * 0.01, cos(time * 2.7) * 0.01);
            vec2 fragCoord = vUv * resolution;
            vec2 p = ((fragCoord + shake * resolution) - resolution * 0.5) / resolution.y;
            p *= mat2(8.0, -6.0, 6.0, 8.0);

            vec2 v;
            vec4 o = vec4(0.0);
            float f = 3.0 + fbm(p + vec2(time * 7.0, 0.0));

            for (float i = 0.0; i < 50.0; i++) {
                v = p + cos(i * i + (time + p.x * 0.1) * 0.03 + i * vec2(11.0, 9.0)) * 5.0
                    + vec2(sin(time * 4.0 + i) * 0.005, cos(time * 4.5 - i) * 0.005);

                float tailNoise = fbm(v + vec2(time, i)) * (1.0 - (i / 50.0));
                vec4 currentContribution = (cos(sin(i) * vec4(1.0, 2.0, 3.0, 1.0)) + 1.0)
                    * exp(sin(i * i + time)) / length(max(v, vec2(v.x * f * 0.02, v.y)));

                float thinnessFactor = smoothstep(0.0, 1.0, i / 50.0);
                o += currentContribution * (1.0 + tailNoise * 2.0) * thinnessFactor;
            }

            o = tanh(pow(o / 1e2, vec4(1.5)));
            gl_FragColor = o;
        }
    `
};

着色器解析

1. 顶点着色器

顶点着色器的任务非常简单,它将 UV 坐标传递给片段着色器,并根据模型视图矩阵和投影矩阵计算每个顶点的位置。

glsl 复制代码
void main() {
    vUv = uv;
    gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}

2. 片段着色器

随机数生成与噪声:我们通过 rand 函数生成一个伪随机数,结合 noise 函数生成了基础的噪声,用于创建火焰的动态效果。

分形布朗运动(FBM):使用多个频率和振幅的噪声叠加,生成复杂的纹理和动态效果,这使得火焰的效果更加自然。

动态效果:通过 time 控制火焰的变化,使用不同的参数让火焰不断变化,模拟出自然的火焰流动。

3. 渲染火焰效果

通过循环并叠加每一层的噪声和贡献,我们可以模拟火焰的扩散和衰退。使用 exp 和 tanh 函数对输出进行平滑处理,使得最终的火焰效果看起来更加柔和自然。

如何使用该着色器

  • 创建一个材质: 使用 THREE.ShaderMaterial 创建一个自定义材质,将上述着色器代码传递给它。

  • 设置 time 和 resolution: time 是一个动态变化的值,用来驱动火焰的动画效果。resolution 则表示画布的尺寸,影响渲染的比例。

  • 应用到对象上: 创建一个平面或其他几何体,并将自定义的火焰材质应用到该几何体上。

javascript 复制代码
const material = new THREE.ShaderMaterial({
    uniforms: flameShader.uniforms,
    vertexShader: flameShader.vertexShader,
    fragmentShader: flameShader.fragmentShader
});

完整代码

js 复制代码
import * as THREE from 'three';

// 使用示例
const camera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1);
const scene = new THREE.Scene();

const renderer = new THREE.WebGLRenderer();
renderer.setSize(window.innerWidth,window.innerHeight);
renderer.setPixelRatio(window.devicePixelRatio);

document.body.appendChild(renderer.domElement);


const flameShader = {
    uniforms: {
        time: { value: 0 },
        resolution: { value: new THREE.Vector2() }
    },
    vertexShader: `
        varying vec2 vUv;
        void main() {
            vUv = uv;
            gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
        }
    `,
    fragmentShader: `
        precision highp float;
        #define NUM_OCTAVES 5
        varying vec2 vUv;
        uniform float time;
        uniform vec2 resolution;

        float rand(vec2 n) { 
            return fract(sin(dot(n, vec2(12.9898, 4.1414))) * 43758.5453);
        }

        float noise(vec2 p) {
            vec2 ip = floor(p);
            vec2 u = fract(p);
            u = u*u*(3.0-2.0*u);
            float res = mix(
                mix(rand(ip), rand(ip+vec2(1.0,0.0)), u.x),
                mix(rand(ip+vec2(0.0,1.0)), rand(ip+vec2(1.0,1.0)), u.x), 
                u.y);
            return res*res;
        }

        float fbm(vec2 x) {
            float v = 0.0;
            float a = 0.5;
            vec2 shift = vec2(100);
            mat2 rot = mat2(cos(0.5), sin(0.5), -sin(0.5), cos(0.50));
            for (int i = 0; i < NUM_OCTAVES; ++i) {
                v += a * noise(x);
                x = rot * x * 2.0 + shift;
                a *= 0.5;
            }
            return v;
        }

        void main() {
            vec2 shake = vec2(sin(time * 1.5) * 0.01, cos(time * 2.7) * 0.01);
            vec2 fragCoord = vUv * resolution;
            
            vec2 p = ((fragCoord + shake * resolution) - resolution * 0.5) / resolution.y;
            p *= mat2(8.0, -6.0, 6.0, 8.0);
            
            vec2 v;
            vec4 o = vec4(0.0);
            float f = 3.0 + fbm(p + vec2(time * 7.0, 0.0));
            
            for(float i = 0.0; i < 50.0; i++) {
                v = p + cos(i*i + (time + p.x*0.1)*0.03 + i*vec2(11.0,9.0)) *5.0 
                    + vec2(sin(time*4.0 + i)*0.005, cos(time*4.5 - i)*0.005);
                
                float tailNoise = fbm(v + vec2(time, i)) * (1.0 - (i/50.0));
                vec4 currentContribution = (cos(sin(i)*vec4(1.0,2.0,3.0,1.0)) +1.0) 
                    * exp(sin(i*i + time)) / length(max(v, vec2(v.x*f*0.02, v.y)));
                
                float thinnessFactor = smoothstep(0.0, 1.0, i/50.0);
                o += currentContribution * (1.0 + tailNoise*2.0) * thinnessFactor;
            }
            
            o = tanh(pow(o/1e2, vec4(1.5)));
            gl_FragColor = o;
        }
    `
};


const mesh = new THREE.Mesh(
    new THREE.PlaneGeometry(2, 2),
    new THREE.ShaderMaterial({
        ...flameShader,
        blending: THREE.AdditiveBlending,
        transparent: true
    })
);

scene.add(mesh);

function animate() {
    mesh.material.uniforms.time.value = performance.now() / 1000;
    mesh.material.uniforms.resolution.value.set(window.innerWidth, window.innerHeight);
    requestAnimationFrame(animate);
    renderer.render(scene, camera);
}


animate();

总结

通过本文的介绍,你可以了解如何使用 Three.js 和 GLSL 创建一个动态的火焰效果。这个效果是基于噪声和分形布朗运动的,模拟了自然界火焰的动态变化。你可以根据自己的需要调整参数,进一步优化火焰效果。

相关推荐
lovingsoft14 分钟前
Chrome Lighthouse 详解
前端·chrome
Qlly24 分钟前
全栈工程师怎么接需求?从一句「加个功能」聊到能开发、能验收
前端·后端
马可家的菠萝28 分钟前
你的微信“文件传输助手”,是不是已经成了第二个网盘?
前端·后端·架构
略略略啦29 分钟前
Vibe Coding 基于Spec DD和TDD的实践指南
前端
shmily麻瓜小菜鸡1 小时前
前端实现图片与PDF文字识别敏感信息打码方案
前端·pdf·状态模式
打工仔折腾 AI1 小时前
LangChain 工具调用实战:从 Tool 定义到 Agent 执行链路拆解
java·前端·人工智能·后端·python·langchain·ai agent 实战
汉堡大王95271 小时前
单次输出拉到100万Token:拆完 Gemini 4 Argon 的续写协议,才看懂谷歌这7个月在憋什么
前端·人工智能·后端
看浪的路人1 小时前
第9讲:大模型应用监控与可观测性——OpenTelemetry + Prometheus + Grafana 全链路追踪
java·前端·spring
内存过载1 小时前
用表格模拟甘特图:项目排期的土办法
前端·低代码
葡萄城技术团队1 小时前
用 SpreadJS 数据验证实现省市区三级联动
前端